From eaa9ae6f8f0cb89bbe5b9598d59f8965c9484eda Mon Sep 17 00:00:00 2001 From: Scramble Tools <162384439+scrambletools@users.noreply.github.com> Date: Fri, 21 Aug 2026 12:09:43 -0700 Subject: [PATCH] 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 --- components/esp_eth/src/mac/esp_eth_mac_esp.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/components/esp_eth/src/mac/esp_eth_mac_esp.c b/components/esp_eth/src/mac/esp_eth_mac_esp.c index c71f8272b2e..7e68f72b3f9 100644 --- a/components/esp_eth/src/mac/esp_eth_mac_esp.c +++ b/components/esp_eth/src/mac/esp_eth_mac_esp.c @@ -443,7 +443,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); @@ -473,9 +473,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); @@ -483,7 +495,7 @@ static void emac_esp32_rx_task(void *arg) emac_hal_send_pause_frame(&emac->hal, false); } #endif - } while (emac->frames_remain); + } } }