fix(esp_eth): avoid per-frame descriptor ring scan in RX task

The RX task's drain loop called emac_esp_dma_get_remain_frames()
after every received frame, an O(backlog) walk of the descriptor
ring with a cache invalidate per descriptor. Under sustained load
the scan cost grows with the backlog, so the deeper the backlog
gets, the more time goes into scanning instead of draining, until
the task can no longer keep up and the DMA drops frames
continuously (measured ~3000 missed frames/s at 16k pkt/s
bidirectional on ESP32-P4 with 20 descriptors).

The loop only needs to know whether at least one more complete
frame is waiting, which emac_esp_dma_alloc_recv_buf() already
answers by checking a single descriptor, so exit the loop on that
instead. The full remain-frames count is kept only under
CONFIG_ETH_SOFT_FLOW_CONTROL, which genuinely needs it for the
pause-frame watermarks.

Related to https://github.com/espressif/esp-idf/issues/18800
This commit is contained in:
Scramble Tools
2026-08-25 15:02:55 -07:00
parent 0276131402
commit 4bc48f3ac4
+16 -4
View File
@@ -615,7 +615,7 @@ static void emac_esp32_rx_task(void *arg)
while (1) {
// block indefinitely until got notification from underlay event
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
do {
while (1) {
/* set max expected frame len */
uint32_t frame_len = ETH_MAX_PACKET_SIZE;
buffer = emac_esp_dma_alloc_recv_buf(emac->emac_dma_hndl, &frame_len);
@@ -645,9 +645,21 @@ static void emac_esp32_rx_task(void *arg)
ESP_LOGE(TAG, "no mem for receive buffer");
/* ensures that interface to EMAC does not get stuck with unprocessed frames */
emac_esp_dma_flush_recv_frame(emac->emac_dma_hndl);
}
emac_esp_dma_get_remain_frames(emac->emac_dma_hndl, &emac->frames_remain, &emac->free_rx_descriptor);
} else {
/* no valid frame: either the ring is drained or an erroneous frame was flushed with frames still behind it */
emac_esp_dma_get_remain_frames(emac->emac_dma_hndl, &emac->frames_remain, &emac->free_rx_descriptor);
if (emac->frames_remain == 0) {
#if CONFIG_ETH_SOFT_FLOW_CONTROL
/* ring drained, release any standing pause before idling */
emac_hal_send_pause_frame(&emac->hal, false);
#endif
break;
}
/* erroneous frame flushed; keep draining the frames behind it */
continue;
}
#if CONFIG_ETH_SOFT_FLOW_CONTROL
emac_esp_dma_get_remain_frames(emac->emac_dma_hndl, &emac->frames_remain, &emac->free_rx_descriptor);
// we need to do extra checking of remained frames in case there are no unhandled frames left, but pause frame is still undergoing
if ((emac->free_rx_descriptor < emac->flow_control_low_water_mark) && emac->do_flow_ctrl && emac->frames_remain) {
emac_hal_send_pause_frame(&emac->hal, true);
@@ -655,7 +667,7 @@ static void emac_esp32_rx_task(void *arg)
emac_hal_send_pause_frame(&emac->hal, false);
}
#endif
} while (emac->frames_remain);
}
}
}