feat(esp_eth): a new folder structure of the driver and other improvements
Fixed memory leak in emac_esp_new_dma function. Polished ESP EMAC cache management. Added emac_periph definitions based on SoC features and improved(generalized) ESP EMAC GPIO initialization. Added ESP EMAC GPIO reservation. Added check for frame error condition indicated by EMAC DMA and created a target test.
This commit is contained in:
@@ -0,0 +1,725 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2019-2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/cdefs.h>
|
||||
#include <stdarg.h>
|
||||
#include <inttypes.h>
|
||||
#include "esp_private/periph_ctrl.h"
|
||||
#include "esp_attr.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_check.h"
|
||||
#include "esp_pm.h"
|
||||
#include "esp_mac.h"
|
||||
#include "esp_cpu.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_intr_alloc.h"
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32
|
||||
#include "esp_clock_output.h"
|
||||
#endif // CONFIG_IDF_TARGET_ESP32
|
||||
#include "hal/clk_tree_ll.h"
|
||||
#include "esp_private/esp_clk.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "hal/emac_hal.h"
|
||||
#include "soc/soc.h"
|
||||
#include "clk_ctrl_os.h"
|
||||
#include "sdkconfig.h"
|
||||
#include "esp_rom_sys.h"
|
||||
#include "esp_private/eth_mac_esp_dma.h"
|
||||
#include "esp_private/eth_mac_esp_gpio.h"
|
||||
|
||||
static const char *TAG = "esp.emac";
|
||||
|
||||
#define PHY_OPERATION_TIMEOUT_US (1000)
|
||||
#define MAC_STOP_TIMEOUT_US (2500) // this is absolute maximum for 10Mbps, it is 10 times faster for 100Mbps
|
||||
#define FLOW_CONTROL_LOW_WATER_MARK (CONFIG_ETH_DMA_RX_BUFFER_NUM / 3)
|
||||
#define FLOW_CONTROL_HIGH_WATER_MARK (FLOW_CONTROL_LOW_WATER_MARK * 2)
|
||||
|
||||
#define EMAC_ALLOW_INTR_PRIORITY_MASK ESP_INTR_FLAG_LOWMED
|
||||
|
||||
#define RMII_CLK_HZ (50000000)
|
||||
#define RMII_10M_SPEED_RX_TX_CLK_DIV (19)
|
||||
#define RMII_100M_SPEED_RX_TX_CLK_DIV (1)
|
||||
|
||||
#if CONFIG_IDF_TARGET_ESP32P4
|
||||
// ESP32P4 EMAC interface clock configuration is shared among other modules in registers
|
||||
#define EMAC_IF_RCC_ATOMIC() PERIPH_RCC_ATOMIC()
|
||||
#else
|
||||
#define EMAC_IF_RCC_ATOMIC()
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
esp_eth_mac_t parent;
|
||||
esp_eth_mediator_t *eth;
|
||||
emac_hal_context_t hal;
|
||||
intr_handle_t intr_hdl;
|
||||
TaskHandle_t rx_task_hdl;
|
||||
emac_esp_dma_handle_t emac_dma_hndl;
|
||||
uint32_t sw_reset_timeout_ms;
|
||||
uint32_t frames_remain;
|
||||
uint32_t free_rx_descriptor;
|
||||
uint32_t flow_control_high_water_mark;
|
||||
uint32_t flow_control_low_water_mark;
|
||||
uint8_t addr[ETH_ADDR_LEN];
|
||||
bool isr_need_yield;
|
||||
bool flow_ctrl_enabled; // indicates whether the user want to do flow control
|
||||
bool do_flow_ctrl; // indicates whether we need to do software flow control
|
||||
bool use_pll; // Only use (A/M)PLL in EMAC_DATA_INTERFACE_RMII && EMAC_CLK_OUT
|
||||
#ifdef CONFIG_PM_ENABLE
|
||||
esp_pm_lock_handle_t pm_lock;
|
||||
#endif
|
||||
eth_mac_dma_burst_len_t dma_burst_len;
|
||||
|
||||
// ---- Chip specifics ----
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32
|
||||
esp_clock_output_mapping_handle_t rmii_clk_hdl; // we use the esp_clock_output driver to output a pre-configured APLL clock as the RMII reference clock
|
||||
#endif
|
||||
} emac_esp32_t;
|
||||
|
||||
static esp_err_t emac_esp_alloc_driver_obj(const eth_mac_config_t *config, emac_esp32_t **emac_out_hdl);
|
||||
static void emac_esp_free_driver_obj(emac_esp32_t *emac);
|
||||
static esp_err_t emac_esp32_start(esp_eth_mac_t *mac);
|
||||
static esp_err_t emac_esp32_stop(esp_eth_mac_t *mac);
|
||||
|
||||
static esp_err_t emac_esp32_set_mediator(esp_eth_mac_t *mac, esp_eth_mediator_t *eth)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
ESP_GOTO_ON_FALSE(eth, ESP_ERR_INVALID_ARG, err, TAG, "can't set mac's mediator to null");
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
emac->eth = eth;
|
||||
return ESP_OK;
|
||||
err:
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp32_write_phy_reg(esp_eth_mac_t *mac, uint32_t phy_addr, uint32_t phy_reg, uint32_t reg_value)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
ESP_GOTO_ON_FALSE(!emac_hal_is_mii_busy(&emac->hal), ESP_ERR_INVALID_STATE, err, TAG, "phy is busy");
|
||||
emac_hal_set_phy_data(&emac->hal, reg_value);
|
||||
emac_hal_set_phy_cmd(&emac->hal, phy_addr, phy_reg, true);
|
||||
/* polling the busy flag */
|
||||
uint32_t to = 0;
|
||||
bool busy = true;
|
||||
do {
|
||||
esp_rom_delay_us(100);
|
||||
busy = emac_hal_is_mii_busy(&emac->hal);
|
||||
to += 100;
|
||||
} while (busy && to < PHY_OPERATION_TIMEOUT_US);
|
||||
ESP_GOTO_ON_FALSE(!busy, ESP_ERR_TIMEOUT, err, TAG, "phy is busy");
|
||||
return ESP_OK;
|
||||
err:
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp32_read_phy_reg(esp_eth_mac_t *mac, uint32_t phy_addr, uint32_t phy_reg, uint32_t *reg_value)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
ESP_GOTO_ON_FALSE(reg_value, ESP_ERR_INVALID_ARG, err, TAG, "can't set reg_value to null");
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
ESP_GOTO_ON_FALSE(!emac_hal_is_mii_busy(&emac->hal), ESP_ERR_INVALID_STATE, err, TAG, "phy is busy");
|
||||
emac_hal_set_phy_cmd(&emac->hal, phy_addr, phy_reg, false);
|
||||
/* polling the busy flag */
|
||||
uint32_t to = 0;
|
||||
bool busy = true;
|
||||
do {
|
||||
esp_rom_delay_us(100);
|
||||
busy = emac_hal_is_mii_busy(&emac->hal);
|
||||
to += 100;
|
||||
} while (busy && to < PHY_OPERATION_TIMEOUT_US);
|
||||
ESP_GOTO_ON_FALSE(!busy, ESP_ERR_TIMEOUT, err, TAG, "phy is busy");
|
||||
/* Store value */
|
||||
*reg_value = emac_hal_get_phy_data(&emac->hal);
|
||||
return ESP_OK;
|
||||
err:
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp32_set_addr(esp_eth_mac_t *mac, uint8_t *addr)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
ESP_GOTO_ON_FALSE(addr, ESP_ERR_INVALID_ARG, err, TAG, "can't set mac addr to null");
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
memcpy(emac->addr, addr, 6);
|
||||
emac_hal_set_address(&emac->hal, emac->addr);
|
||||
return ESP_OK;
|
||||
err:
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp32_get_addr(esp_eth_mac_t *mac, uint8_t *addr)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
ESP_GOTO_ON_FALSE(addr, ESP_ERR_INVALID_ARG, err, TAG, "can't set mac addr to null");
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
memcpy(addr, emac->addr, 6);
|
||||
return ESP_OK;
|
||||
err:
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp32_set_link(esp_eth_mac_t *mac, eth_link_t link)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
switch (link) {
|
||||
case ETH_LINK_UP:
|
||||
ESP_GOTO_ON_ERROR(esp_intr_enable(emac->intr_hdl), err, TAG, "enable interrupt failed");
|
||||
emac_esp32_start(mac);
|
||||
ESP_LOGD(TAG, "emac started");
|
||||
break;
|
||||
case ETH_LINK_DOWN:
|
||||
ESP_GOTO_ON_ERROR(esp_intr_disable(emac->intr_hdl), err, TAG, "disable interrupt failed");
|
||||
emac_esp32_stop(mac);
|
||||
ESP_LOGD(TAG, "emac stopped");
|
||||
break;
|
||||
default:
|
||||
ESP_GOTO_ON_FALSE(false, ESP_ERR_INVALID_ARG, err, TAG, "unknown link status");
|
||||
break;
|
||||
}
|
||||
return ESP_OK;
|
||||
err:
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp32_set_speed(esp_eth_mac_t *mac, eth_speed_t speed)
|
||||
{
|
||||
esp_err_t ret = ESP_ERR_INVALID_ARG;
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
if (speed >= ETH_SPEED_10M && speed < ETH_SPEED_MAX) {
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32P4
|
||||
// Set RMII clk_rx/clk_tx divider to get 25MHz for 100mbps mode or 2.5MHz for 10mbps mode
|
||||
if (emac_hal_get_phy_intf(&emac->hal) == EMAC_DATA_INTERFACE_RMII) {
|
||||
if (speed == ETH_SPEED_10M) {
|
||||
EMAC_IF_RCC_ATOMIC() {
|
||||
emac_hal_clock_rmii_rx_tx_div(&emac->hal, RMII_10M_SPEED_RX_TX_CLK_DIV);
|
||||
}
|
||||
} else {
|
||||
EMAC_IF_RCC_ATOMIC() {
|
||||
emac_hal_clock_rmii_rx_tx_div(&emac->hal, RMII_100M_SPEED_RX_TX_CLK_DIV);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
emac_hal_set_speed(&emac->hal, speed);
|
||||
ESP_LOGD(TAG, "working in %iMbps", speed == ETH_SPEED_10M ? 10 : 100);
|
||||
return ESP_OK;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp32_set_duplex(esp_eth_mac_t *mac, eth_duplex_t duplex)
|
||||
{
|
||||
esp_err_t ret = ESP_ERR_INVALID_ARG;
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
if (duplex == ETH_DUPLEX_HALF || duplex == ETH_DUPLEX_FULL) {
|
||||
emac_hal_set_duplex(&emac->hal, duplex);
|
||||
ESP_LOGD(TAG, "working in %s duplex", duplex == ETH_DUPLEX_HALF ? "half" : "full");
|
||||
return ESP_OK;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp32_set_promiscuous(esp_eth_mac_t *mac, bool enable)
|
||||
{
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
emac_hal_set_promiscuous(&emac->hal, enable);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp32_enable_flow_ctrl(esp_eth_mac_t *mac, bool enable)
|
||||
{
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
emac->flow_ctrl_enabled = enable;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp32_set_peer_pause_ability(esp_eth_mac_t *mac, uint32_t ability)
|
||||
{
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
// we want to enable flow control, and peer does support pause function
|
||||
// then configure the MAC layer to enable flow control feature
|
||||
if (emac->flow_ctrl_enabled && ability) {
|
||||
emac_hal_enable_flow_ctrl(&emac->hal, true);
|
||||
emac->do_flow_ctrl = true;
|
||||
} else {
|
||||
emac_hal_enable_flow_ctrl(&emac->hal, false);
|
||||
emac->do_flow_ctrl = false;
|
||||
ESP_LOGD(TAG, "Flow control not enabled for the link");
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t emac_esp_custom_ioctl(esp_eth_mac_t *mac, int cmd, void *data)
|
||||
{
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
|
||||
switch (cmd)
|
||||
{
|
||||
case ETH_MAC_ESP_CMD_PTP_ENABLE:
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
case ETH_MAC_ESP_CMD_SET_TDES0_CFG_BITS:
|
||||
ESP_RETURN_ON_FALSE(data != NULL, ESP_ERR_INVALID_ARG, TAG, "cannot set DMA tx desc flag to null");
|
||||
emac_esp_dma_set_tdes0_ctrl_bits(emac->emac_dma_hndl, *(uint32_t *)data);
|
||||
break;
|
||||
case ETH_MAC_ESP_CMD_CLEAR_TDES0_CFG_BITS:
|
||||
ESP_RETURN_ON_FALSE(data != NULL, ESP_ERR_INVALID_ARG, TAG, "cannot clear DMA tx desc flag with null");
|
||||
emac_esp_dma_clear_tdes0_ctrl_bits(emac->emac_dma_hndl, *(uint32_t *)data);
|
||||
break;
|
||||
default:
|
||||
ESP_RETURN_ON_ERROR(ESP_ERR_INVALID_ARG, TAG, "unknown io command: %i", cmd);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp32_transmit(esp_eth_mac_t *mac, uint8_t *buf, uint32_t length)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
uint32_t sent_len = emac_esp_dma_transmit_frame(emac->emac_dma_hndl, buf, length);
|
||||
ESP_GOTO_ON_FALSE(sent_len == length, ESP_ERR_NO_MEM, err, TAG, "insufficient TX buffer size");
|
||||
return ESP_OK;
|
||||
err:
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp32_transmit_multiple_bufs(esp_eth_mac_t *mac, uint32_t argc, va_list args)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
uint8_t *bufs[argc];
|
||||
uint32_t len[argc];
|
||||
uint32_t exp_len = 0;
|
||||
for (int i = 0; i < argc; i++) {
|
||||
bufs[i] = va_arg(args, uint8_t *);
|
||||
len[i] = va_arg(args, uint32_t);
|
||||
exp_len += len[i];
|
||||
}
|
||||
uint32_t sent_len = emac_esp_dma_transmit_multiple_buf_frame(emac->emac_dma_hndl, bufs, len, argc);
|
||||
ESP_GOTO_ON_FALSE(sent_len == exp_len, ESP_ERR_INVALID_SIZE, err, TAG, "insufficient TX buffer size");
|
||||
return ESP_OK;
|
||||
err:
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp32_receive(esp_eth_mac_t *mac, uint8_t *buf, uint32_t *length)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
uint32_t expected_len = *length;
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
ESP_GOTO_ON_FALSE(buf && length, ESP_ERR_INVALID_ARG, err, TAG, "can't set buf and length to null");
|
||||
uint32_t receive_len = emac_esp_dma_receive_frame(emac->emac_dma_hndl, buf, expected_len);
|
||||
emac_esp_dma_get_remain_frames(emac->emac_dma_hndl, &emac->frames_remain, &emac->free_rx_descriptor);
|
||||
/* we need to check the return value in case the buffer size is not enough */
|
||||
ESP_GOTO_ON_FALSE(expected_len >= receive_len, ESP_ERR_INVALID_SIZE, err, TAG, "received buffer longer than expected");
|
||||
*length = receive_len;
|
||||
return ESP_OK;
|
||||
err:
|
||||
*length = expected_len;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void emac_esp32_rx_task(void *arg)
|
||||
{
|
||||
emac_esp32_t *emac = (emac_esp32_t *)arg;
|
||||
uint8_t *buffer = NULL;
|
||||
while (1) {
|
||||
// block indefinitely until got notification from underlay event
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
do {
|
||||
/* 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);
|
||||
/* we have memory to receive the frame of maximal size previously defined */
|
||||
if (buffer != NULL) {
|
||||
uint32_t recv_len = emac_esp_dma_receive_frame(emac->emac_dma_hndl, buffer, EMAC_DMA_BUF_SIZE_AUTO);
|
||||
if (recv_len == 0) {
|
||||
ESP_LOGE(TAG, "frame copy error");
|
||||
free(buffer);
|
||||
/* ensure that interface to EMAC does not get stuck with unprocessed frames */
|
||||
emac_esp_dma_flush_recv_frame(emac->emac_dma_hndl);
|
||||
} else if (frame_len > recv_len) {
|
||||
ESP_LOGE(TAG, "received frame was truncated");
|
||||
free(buffer);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "receive len= %" PRIu32, recv_len);
|
||||
emac->eth->stack_input(emac->eth, buffer, recv_len);
|
||||
}
|
||||
/* if allocation failed and there is a waiting frame */
|
||||
} else if (frame_len) {
|
||||
ESP_LOGE(TAG, "no mem for receive buffer");
|
||||
/* ensure 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);
|
||||
#if CONFIG_ETH_SOFT_FLOW_CONTROL
|
||||
// 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);
|
||||
} else if ((emac->free_rx_descriptor > emac->flow_control_high_water_mark) || !emac->frames_remain) {
|
||||
emac_hal_send_pause_frame(&emac->hal, false);
|
||||
}
|
||||
#endif
|
||||
} while (emac->frames_remain);
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
static esp_err_t emac_config_pll_clock(emac_esp32_t *emac)
|
||||
{
|
||||
uint32_t expt_freq = RMII_CLK_HZ; // 50 MHz
|
||||
uint32_t real_freq = 0;
|
||||
|
||||
#if CONFIG_IDF_TARGET_ESP32
|
||||
// the RMII reference comes from the APLL
|
||||
periph_rtc_apll_acquire();
|
||||
emac->use_pll = true;
|
||||
esp_err_t ret = periph_rtc_apll_freq_set(expt_freq, &real_freq);
|
||||
ESP_RETURN_ON_FALSE(ret != ESP_ERR_INVALID_ARG, ESP_FAIL, TAG, "Set APLL clock coefficients failed");
|
||||
if (ret == ESP_ERR_INVALID_STATE) {
|
||||
ESP_LOGW(TAG, "APLL is occupied already, it is working at %" PRIu32 " Hz", real_freq);
|
||||
}
|
||||
#elif CONFIG_IDF_TARGET_ESP32P4
|
||||
// the RMII reference comes from the MPLL
|
||||
periph_rtc_mpll_acquire();
|
||||
emac->use_pll = true;
|
||||
esp_err_t ret = periph_rtc_mpll_freq_set(expt_freq * 2, &real_freq); // cannot set 50MHz at MPLL, the nearest possible freq is 100 MHz
|
||||
if (ret == ESP_ERR_INVALID_STATE) {
|
||||
ESP_LOGW(TAG, "MPLL is occupied already, it is working at %" PRIu32 " Hz", real_freq);
|
||||
}
|
||||
// Set divider of MPLL clock
|
||||
if (real_freq > RMII_CLK_HZ) {
|
||||
int32_t div = real_freq / RMII_CLK_HZ - 1;
|
||||
clk_ll_pll_f50m_set_divider(div);
|
||||
// compute real RMII CLK frequency
|
||||
real_freq /= div + 1;
|
||||
}
|
||||
#endif
|
||||
// If the difference of real RMII CLK frequency is not within 50 ppm, i.e. 2500 Hz, the (A/M)PLL is unusable
|
||||
ESP_RETURN_ON_FALSE(abs((int)real_freq - (int)expt_freq) <= 2500,
|
||||
ESP_ERR_INVALID_STATE, TAG, "The (A/M)PLL is working at an unusable frequency %" PRIu32 " Hz", real_freq);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp32_init(esp_eth_mac_t *mac)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
esp_eth_mediator_t *eth = emac->eth;
|
||||
|
||||
ESP_GOTO_ON_ERROR(eth->on_state_changed(eth, ETH_STATE_LLINIT, NULL), err, TAG, "lowlevel init failed");
|
||||
/* software reset */
|
||||
emac_hal_reset(&emac->hal);
|
||||
uint32_t to = 0;
|
||||
for (to = 0; to < emac->sw_reset_timeout_ms / 10; to++) {
|
||||
if (emac_hal_is_reset_done(&emac->hal)) {
|
||||
break;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
ESP_GOTO_ON_FALSE(to < emac->sw_reset_timeout_ms / 10, ESP_ERR_TIMEOUT, err, TAG, "reset timeout");
|
||||
/* set smi clock */
|
||||
emac_hal_set_csr_clock_range(&emac->hal, esp_clk_apb_freq());
|
||||
/* init mac registers by default */
|
||||
emac_hal_init_mac_default(&emac->hal);
|
||||
/* init dma registers with selected EMAC-DMA configuration */
|
||||
emac_hal_dma_config_t dma_config = { .dma_burst_len = emac->dma_burst_len };
|
||||
emac_hal_init_dma_default(&emac->hal, &dma_config);
|
||||
/* get emac address from efuse */
|
||||
ESP_GOTO_ON_ERROR(esp_read_mac(emac->addr, ESP_MAC_ETH), err, TAG, "fetch ethernet mac address failed");
|
||||
/* set MAC address to emac register */
|
||||
emac_hal_set_address(&emac->hal, emac->addr);
|
||||
#ifdef CONFIG_PM_ENABLE
|
||||
esp_pm_lock_acquire(emac->pm_lock);
|
||||
#endif
|
||||
return ESP_OK;
|
||||
|
||||
err:
|
||||
eth->on_state_changed(eth, ETH_STATE_DEINIT, NULL);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp32_deinit(esp_eth_mac_t *mac)
|
||||
{
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
esp_eth_mediator_t *eth = emac->eth;
|
||||
#ifdef CONFIG_PM_ENABLE
|
||||
esp_pm_lock_release(emac->pm_lock);
|
||||
#endif
|
||||
emac_hal_stop(&emac->hal);
|
||||
eth->on_state_changed(eth, ETH_STATE_DEINIT, NULL);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp32_start(esp_eth_mac_t *mac)
|
||||
{
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
/* reset descriptor chain */
|
||||
emac_esp_dma_reset(emac->emac_dma_hndl);
|
||||
emac_hal_start(&emac->hal);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp32_stop(esp_eth_mac_t *mac)
|
||||
{
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
esp_err_t ret = ESP_OK;
|
||||
int32_t to = 0;
|
||||
do {
|
||||
if ((ret = emac_hal_stop(&emac->hal)) == ESP_OK) {
|
||||
break;
|
||||
}
|
||||
to += 25;
|
||||
esp_rom_delay_us(25);
|
||||
} while (to < MAC_STOP_TIMEOUT_US);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp32_del(esp_eth_mac_t *mac)
|
||||
{
|
||||
emac_esp32_t *emac = __containerof(mac, emac_esp32_t, parent);
|
||||
emac_esp_free_driver_obj(emac);
|
||||
emac_esp_gpio_deinit_all();
|
||||
// disable bus clock
|
||||
PERIPH_RCC_ATOMIC() {
|
||||
emac_ll_enable_bus_clock(0, false);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// To achieve a better performance, we put the ISR always in IRAM
|
||||
IRAM_ATTR void emac_isr_default_handler(void *args)
|
||||
{
|
||||
emac_hal_context_t *hal = (emac_hal_context_t *)args;
|
||||
uint32_t intr_stat = emac_hal_get_intr_status(hal);
|
||||
emac_hal_clear_corresponding_intr(hal, intr_stat);
|
||||
|
||||
#if EMAC_LL_CONFIG_ENABLE_INTR_MASK & EMAC_LL_INTR_RECEIVE_ENABLE
|
||||
if (intr_stat & EMAC_LL_DMA_RECEIVE_FINISH_INTR) {
|
||||
emac_esp32_t *emac = __containerof(hal, emac_esp32_t, hal);
|
||||
BaseType_t high_task_wakeup = pdFALSE;
|
||||
/* notify receive task */
|
||||
vTaskNotifyGiveFromISR(emac->rx_task_hdl, &high_task_wakeup);
|
||||
if (high_task_wakeup == pdTRUE) {
|
||||
portYIELD_FROM_ISR();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static void emac_esp_free_driver_obj(emac_esp32_t *emac)
|
||||
{
|
||||
if (emac) {
|
||||
if (emac->rx_task_hdl) {
|
||||
vTaskDelete(emac->rx_task_hdl);
|
||||
}
|
||||
if (emac->intr_hdl) {
|
||||
esp_intr_free(emac->intr_hdl);
|
||||
}
|
||||
|
||||
if (emac->use_pll) {
|
||||
#if CONFIG_IDF_TARGET_ESP32
|
||||
periph_rtc_apll_release();
|
||||
#elif CONFIG_IDF_TARGET_ESP32P4
|
||||
periph_rtc_mpll_release();
|
||||
#endif
|
||||
}
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32
|
||||
if (emac->rmii_clk_hdl) {
|
||||
esp_clock_output_stop(emac->rmii_clk_hdl);
|
||||
}
|
||||
#endif // CONFIG_IDF_TARGET_ESP32
|
||||
|
||||
#ifdef CONFIG_PM_ENABLE
|
||||
if (emac->pm_lock) {
|
||||
esp_pm_lock_delete(emac->pm_lock);
|
||||
}
|
||||
#endif // CONFIG_PM_ENABLE
|
||||
|
||||
emac_esp_del_dma(emac->emac_dma_hndl);
|
||||
|
||||
free(emac);
|
||||
}
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp_alloc_driver_obj(const eth_mac_config_t *config, emac_esp32_t **emac_out_hdl)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
emac_esp32_t *emac = NULL;
|
||||
if (config->flags & ETH_MAC_FLAG_WORK_WITH_CACHE_DISABLE) {
|
||||
emac = heap_caps_calloc(1, sizeof(emac_esp32_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
|
||||
} else {
|
||||
emac = calloc(1, sizeof(emac_esp32_t));
|
||||
}
|
||||
ESP_GOTO_ON_FALSE(emac, ESP_ERR_NO_MEM, err, TAG, "no mem for esp emac object");
|
||||
|
||||
ESP_GOTO_ON_ERROR(emac_esp_new_dma(NULL, &emac->emac_dma_hndl), err, TAG, "create EMAC DMA object failed");
|
||||
|
||||
/* alloc PM lock */
|
||||
#ifdef CONFIG_PM_ENABLE
|
||||
ESP_GOTO_ON_ERROR(esp_pm_lock_create(ESP_PM_APB_FREQ_MAX, 0, "emac_esp32", &emac->pm_lock), err, TAG, "create pm lock failed");
|
||||
#endif
|
||||
/* create rx task */
|
||||
BaseType_t core_num = tskNO_AFFINITY;
|
||||
if (config->flags & ETH_MAC_FLAG_PIN_TO_CORE) {
|
||||
core_num = esp_cpu_get_core_id();
|
||||
}
|
||||
BaseType_t xReturned = xTaskCreatePinnedToCore(emac_esp32_rx_task, "emac_rx", config->rx_task_stack_size, emac,
|
||||
config->rx_task_prio, &emac->rx_task_hdl, core_num);
|
||||
ESP_GOTO_ON_FALSE(xReturned == pdPASS, ESP_FAIL, err, TAG, "create emac_rx task failed");
|
||||
|
||||
*emac_out_hdl = emac;
|
||||
err:
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp_config_data_interface(const eth_esp32_emac_config_t *esp32_emac_config, emac_esp32_t *emac)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
switch (esp32_emac_config->interface) {
|
||||
case EMAC_DATA_INTERFACE_MII:
|
||||
/* MII interface GPIO initialization */
|
||||
#if SOC_EMAC_MII_USE_GPIO_MATRIX
|
||||
ESP_GOTO_ON_ERROR(emac_esp_gpio_matrix_init_mii(&esp32_emac_config->emac_dataif_gpio.mii), err, TAG, "failed to initialize EMAC MII GPIO Matrix");
|
||||
#else
|
||||
eth_mac_mii_gpio_config_t *mii_data_gpio = NULL;
|
||||
#if SOC_EMAC_USE_MULTI_IO_MUX
|
||||
mii_data_gpio = &esp32_emac_config->emac_dataif_gpio.mii;
|
||||
#endif // SOC_EMAC_USE_MULTI_IO_MUX
|
||||
ESP_GOTO_ON_ERROR(emac_esp_iomux_init_mii(mii_data_gpio), err, TAG, "invalid EMAC MII data plane GPIO");
|
||||
#endif // SOC_EMAC_MII_USE_GPIO_MATRIX
|
||||
/* Enable MII clock */
|
||||
EMAC_IF_RCC_ATOMIC() {
|
||||
emac_hal_clock_enable_mii(&emac->hal);
|
||||
}
|
||||
break;
|
||||
case EMAC_DATA_INTERFACE_RMII:
|
||||
/* RMII interface GPIO initialization */
|
||||
const eth_mac_rmii_gpio_config_t *rmii_data_gpio = NULL;
|
||||
#if SOC_EMAC_USE_MULTI_IO_MUX
|
||||
rmii_data_gpio = &esp32_emac_config->emac_dataif_gpio.rmii;
|
||||
#endif // SOC_EMAC_USE_MULTI_IO_MUX
|
||||
ESP_GOTO_ON_ERROR(emac_esp_iomux_init_rmii(rmii_data_gpio), err, TAG, "invalid EMAC RMII data plane GPIO");
|
||||
/* If ref_clk is configured as input */
|
||||
if (esp32_emac_config->clock_config.rmii.clock_mode == EMAC_CLK_EXT_IN) {
|
||||
ESP_GOTO_ON_ERROR(emac_esp_iomux_rmii_clk_input(esp32_emac_config->clock_config.rmii.clock_gpio), err, TAG, "invalid EMAC RMII clock input GPIO");
|
||||
EMAC_IF_RCC_ATOMIC() {
|
||||
emac_hal_clock_enable_rmii_input(&emac->hal);
|
||||
}
|
||||
} else if (esp32_emac_config->clock_config.rmii.clock_mode == EMAC_CLK_OUT) {
|
||||
ESP_GOTO_ON_ERROR(emac_config_pll_clock(emac), err, TAG, "Configure (A/M)PLL for RMII failed");
|
||||
#if CONFIG_IDF_TARGET_ESP32P4
|
||||
/* Output RMII clock is routed back to input externally */
|
||||
ESP_GOTO_ON_FALSE(esp32_emac_config->clock_config_out_in.rmii.clock_mode == EMAC_CLK_EXT_IN && esp32_emac_config->clock_config_out_in.rmii.clock_gpio >= 0,
|
||||
ESP_ERR_INVALID_ARG, err, TAG, "invalid EMAC input of output clock mode");
|
||||
ESP_GOTO_ON_ERROR(emac_esp_iomux_rmii_clk_input(esp32_emac_config->clock_config_out_in.rmii.clock_gpio), err, TAG, "invalid EMAC RMII clock input GPIO");
|
||||
EMAC_IF_RCC_ATOMIC() {
|
||||
emac_hal_clock_enable_rmii_input(&emac->hal);
|
||||
}
|
||||
#elif CONFIG_IDF_TARGET_ESP32
|
||||
// we can also use the IOMUX to route the APLL clock to specific GPIO
|
||||
if (esp32_emac_config->clock_config.rmii.clock_gpio == EMAC_APPL_CLK_OUT_GPIO) {
|
||||
ESP_GOTO_ON_ERROR(esp_clock_output_start(CLKOUT_SIG_APLL, EMAC_APPL_CLK_OUT_GPIO, &emac->rmii_clk_hdl),
|
||||
err, TAG, "start APLL clock output failed");
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
ESP_GOTO_ON_ERROR(emac_esp_iomux_rmii_clk_ouput(esp32_emac_config->clock_config.rmii.clock_gpio), err, TAG, "invalid EMAC RMII clock output GPIO");
|
||||
}
|
||||
/* Enable RMII Output clock */
|
||||
EMAC_IF_RCC_ATOMIC() {
|
||||
emac_hal_clock_enable_rmii_output(&emac->hal);
|
||||
}
|
||||
} else {
|
||||
ESP_GOTO_ON_FALSE(false, ESP_ERR_INVALID_ARG, err, TAG, "invalid EMAC clock mode");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
ESP_GOTO_ON_FALSE(false, ESP_ERR_INVALID_ARG, err, TAG, "invalid EMAC Data Interface:%i", esp32_emac_config->interface);
|
||||
}
|
||||
err:
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_eth_mac_t *esp_eth_mac_new_esp32(const eth_esp32_emac_config_t *esp32_config, const eth_mac_config_t *config)
|
||||
{
|
||||
esp_err_t ret_code = ESP_OK;
|
||||
esp_eth_mac_t *ret = NULL;
|
||||
emac_esp32_t *emac = NULL;
|
||||
ESP_RETURN_ON_FALSE(config, NULL, TAG, "can't set mac config to null");
|
||||
if (esp32_config->intr_priority > 0) {
|
||||
ESP_RETURN_ON_FALSE(1 << (esp32_config->intr_priority) & EMAC_ALLOW_INTR_PRIORITY_MASK, NULL,
|
||||
TAG, "invalid interrupt priority: %d", esp32_config->intr_priority);
|
||||
}
|
||||
|
||||
ret_code = emac_esp_alloc_driver_obj(config, &emac);
|
||||
ESP_RETURN_ON_FALSE(ret_code == ESP_OK, NULL, TAG, "alloc driver object failed");
|
||||
|
||||
// enable bus clock for the EMAC module, and reset the registers into default state
|
||||
// this must be called before HAL layer initialization
|
||||
PERIPH_RCC_ATOMIC() {
|
||||
emac_ll_enable_bus_clock(0, true);
|
||||
emac_ll_reset_register(0);
|
||||
}
|
||||
/* initialize hal layer driver */
|
||||
emac_hal_init(&emac->hal);
|
||||
|
||||
/* alloc interrupt */
|
||||
int isr_flags = 0;
|
||||
if (esp32_config->intr_priority > 0) {
|
||||
isr_flags |= 1 << (esp32_config->intr_priority);
|
||||
} else {
|
||||
isr_flags |= ESP_INTR_FLAG_LOWMED;
|
||||
}
|
||||
if (config->flags & ETH_MAC_FLAG_WORK_WITH_CACHE_DISABLE) {
|
||||
isr_flags |= ESP_INTR_FLAG_IRAM;
|
||||
}
|
||||
ret_code = esp_intr_alloc(ETS_ETH_MAC_INTR_SOURCE, isr_flags,
|
||||
emac_isr_default_handler, &emac->hal, &(emac->intr_hdl));
|
||||
ESP_GOTO_ON_FALSE(ret_code == ESP_OK, NULL, err, TAG, "alloc emac interrupt failed");
|
||||
|
||||
/* init GPIO used by SMI interface */
|
||||
ret_code = emac_esp_gpio_init_smi(&esp32_config->smi_gpio);
|
||||
ESP_GOTO_ON_FALSE(ret_code == ESP_OK, NULL, err, TAG, "SMI GPIO init failed");
|
||||
/* init GPIO and CLK for data interface */
|
||||
ret_code = emac_esp_config_data_interface(esp32_config, emac);
|
||||
ESP_GOTO_ON_FALSE(ret_code == ESP_OK, NULL, err, TAG, "config emac interface failed");
|
||||
|
||||
emac->dma_burst_len = esp32_config->dma_burst_len;
|
||||
emac->sw_reset_timeout_ms = config->sw_reset_timeout_ms;
|
||||
|
||||
emac->flow_control_high_water_mark = FLOW_CONTROL_HIGH_WATER_MARK;
|
||||
emac->flow_control_low_water_mark = FLOW_CONTROL_LOW_WATER_MARK;
|
||||
emac->parent.set_mediator = emac_esp32_set_mediator;
|
||||
emac->parent.init = emac_esp32_init;
|
||||
emac->parent.deinit = emac_esp32_deinit;
|
||||
emac->parent.start = emac_esp32_start;
|
||||
emac->parent.stop = emac_esp32_stop;
|
||||
emac->parent.del = emac_esp32_del;
|
||||
emac->parent.write_phy_reg = emac_esp32_write_phy_reg;
|
||||
emac->parent.read_phy_reg = emac_esp32_read_phy_reg;
|
||||
emac->parent.set_addr = emac_esp32_set_addr;
|
||||
emac->parent.get_addr = emac_esp32_get_addr;
|
||||
emac->parent.set_speed = emac_esp32_set_speed;
|
||||
emac->parent.set_duplex = emac_esp32_set_duplex;
|
||||
emac->parent.set_link = emac_esp32_set_link;
|
||||
emac->parent.set_promiscuous = emac_esp32_set_promiscuous;
|
||||
emac->parent.set_peer_pause_ability = emac_esp32_set_peer_pause_ability;
|
||||
emac->parent.enable_flow_ctrl = emac_esp32_enable_flow_ctrl;
|
||||
emac->parent.transmit = emac_esp32_transmit;
|
||||
emac->parent.transmit_vargs = emac_esp32_transmit_multiple_bufs;
|
||||
emac->parent.receive = emac_esp32_receive;
|
||||
emac->parent.custom_ioctl = emac_esp_custom_ioctl;
|
||||
return &(emac->parent);
|
||||
|
||||
err:
|
||||
emac_esp_free_driver_obj(emac);
|
||||
emac_esp_gpio_deinit_all();
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "esp_check.h"
|
||||
#include "esp_dma_utils.h"
|
||||
#include "sdkconfig.h"
|
||||
#include "soc/soc_caps.h"
|
||||
#if SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE
|
||||
#include "esp_cache.h"
|
||||
#endif
|
||||
#include "hal/emac_hal.h"
|
||||
#include "esp_private/eth_mac_esp_dma.h"
|
||||
|
||||
#define ETH_CRC_LENGTH (4)
|
||||
|
||||
#define EMAC_ALLOC_BUF_MAGIC_ID 0x1E1C8416
|
||||
|
||||
#define EMAC_TDES0_FS_CTRL_FLAGS_MASK 0x0FCC0000 // modifiable bits mask associated with the First Segment
|
||||
#define EMAC_TDES0_LS_CTRL_FLAGS_MASK 0x40000000 // modifiable bits mask associated with the Last Segment
|
||||
|
||||
#if SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE
|
||||
#define DMA_CACHE_WB(addr, size) do { \
|
||||
esp_err_t msync_ret = esp_cache_msync((void *)addr, size, ESP_CACHE_MSYNC_FLAG_DIR_C2M); \
|
||||
assert(msync_ret == ESP_OK); \
|
||||
} while(0)
|
||||
#else
|
||||
#define DMA_CACHE_WB(addr, size)
|
||||
#endif
|
||||
|
||||
#if SOC_CACHE_INTERNAL_MEM_VIA_L1CACHE
|
||||
#define DMA_CACHE_INVALIDATE(addr, size) do { \
|
||||
esp_err_t msync_ret = esp_cache_msync((void *)addr, size, ESP_CACHE_MSYNC_FLAG_DIR_M2C); \
|
||||
assert(msync_ret == ESP_OK); \
|
||||
} while(0)
|
||||
#else
|
||||
#define DMA_CACHE_INVALIDATE(addr, size)
|
||||
#endif
|
||||
|
||||
static const char *TAG = "esp.emac.dma";
|
||||
|
||||
struct emac_esp_dma_t
|
||||
{
|
||||
emac_hal_context_t hal;
|
||||
uint32_t tx_desc_flags;
|
||||
uint32_t rx_desc_flags;
|
||||
void *descriptors;
|
||||
eth_dma_rx_descriptor_t *rx_desc;
|
||||
eth_dma_tx_descriptor_t *tx_desc;
|
||||
uint8_t *rx_buf[CONFIG_ETH_DMA_RX_BUFFER_NUM];
|
||||
uint8_t *tx_buf[CONFIG_ETH_DMA_TX_BUFFER_NUM];
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
#ifndef NDEBUG
|
||||
uint32_t magic_id;
|
||||
#endif // NDEBUG
|
||||
uint32_t copy_len;
|
||||
}__attribute__((packed)) emac_esp_dma_auto_buf_info_t;
|
||||
|
||||
void emac_esp_dma_reset(emac_esp_dma_handle_t emac_esp_dma)
|
||||
{
|
||||
/* reset DMA descriptors */
|
||||
emac_esp_dma->rx_desc = (eth_dma_rx_descriptor_t *)(emac_esp_dma->descriptors);
|
||||
emac_esp_dma->tx_desc = (eth_dma_tx_descriptor_t *)(emac_esp_dma->descriptors +
|
||||
sizeof(eth_dma_rx_descriptor_t) * CONFIG_ETH_DMA_RX_BUFFER_NUM);
|
||||
/* init rx chain */
|
||||
for (int i = 0; i < CONFIG_ETH_DMA_RX_BUFFER_NUM; i++) {
|
||||
/* Set Own bit of the Rx descriptor Status: DMA */
|
||||
emac_esp_dma->rx_desc[i].RDES0.Own = EMAC_LL_DMADESC_OWNER_DMA;
|
||||
/* Set Buffer1 size and Second Address Chained bit */
|
||||
emac_esp_dma->rx_desc[i].RDES1.SecondAddressChained = 1;
|
||||
emac_esp_dma->rx_desc[i].RDES1.ReceiveBuffer1Size = CONFIG_ETH_DMA_BUFFER_SIZE;
|
||||
/* Enable Ethernet DMA Rx Descriptor interrupt */
|
||||
emac_esp_dma->rx_desc[i].RDES1.DisableInterruptOnComplete = 0;
|
||||
/* point to the buffer */
|
||||
emac_esp_dma->rx_desc[i].Buffer1Addr = (uint32_t)(emac_esp_dma->rx_buf[i]);
|
||||
/* point to next descriptor */
|
||||
emac_esp_dma->rx_desc[i].Buffer2NextDescAddr = (uint32_t)(emac_esp_dma->rx_desc + i + 1);
|
||||
|
||||
/* For last descriptor, set next descriptor address register equal to the first descriptor base address */
|
||||
if (i == CONFIG_ETH_DMA_RX_BUFFER_NUM - 1) {
|
||||
emac_esp_dma->rx_desc[i].Buffer2NextDescAddr = (uint32_t)(emac_esp_dma->rx_desc);
|
||||
}
|
||||
DMA_CACHE_WB(&emac_esp_dma->rx_desc[i], EMAC_HAL_DMA_DESC_SIZE);
|
||||
}
|
||||
|
||||
/* init tx chain */
|
||||
for (int i = 0; i < CONFIG_ETH_DMA_TX_BUFFER_NUM; i++) {
|
||||
/* Set Own bit of the Tx descriptor Status: CPU */
|
||||
emac_esp_dma->tx_desc[i].TDES0.Own = EMAC_LL_DMADESC_OWNER_CPU;
|
||||
emac_esp_dma->tx_desc[i].TDES0.SecondAddressChained = 1;
|
||||
emac_esp_dma->tx_desc[i].TDES1.TransmitBuffer1Size = CONFIG_ETH_DMA_BUFFER_SIZE;
|
||||
/* point to the buffer */
|
||||
emac_esp_dma->tx_desc[i].Buffer1Addr = (uint32_t)(emac_esp_dma->tx_buf[i]);
|
||||
/* point to next descriptor */
|
||||
emac_esp_dma->tx_desc[i].Buffer2NextDescAddr = (uint32_t)(emac_esp_dma->tx_desc + i + 1);
|
||||
|
||||
/* For last descriptor, set next descriptor address register equal to the first descriptor base address */
|
||||
if (i == CONFIG_ETH_DMA_TX_BUFFER_NUM - 1) {
|
||||
emac_esp_dma->tx_desc[i].Buffer2NextDescAddr = (uint32_t)(emac_esp_dma->tx_desc);
|
||||
}
|
||||
DMA_CACHE_WB(&emac_esp_dma->tx_desc[i], EMAC_HAL_DMA_DESC_SIZE);
|
||||
}
|
||||
|
||||
/* set base address of the first descriptor */
|
||||
emac_hal_set_rx_tx_desc_addr(&emac_esp_dma->hal, emac_esp_dma->rx_desc, emac_esp_dma->tx_desc);
|
||||
}
|
||||
|
||||
void emac_esp_dma_set_tdes0_ctrl_bits(emac_esp_dma_handle_t emac_esp_dma, uint32_t flag)
|
||||
{
|
||||
emac_esp_dma->tx_desc_flags |= flag;
|
||||
}
|
||||
|
||||
void emac_esp_dma_clear_tdes0_ctrl_bits(emac_esp_dma_handle_t emac_esp_dma, uint32_t flag)
|
||||
{
|
||||
emac_esp_dma->tx_desc_flags &= ~flag;
|
||||
}
|
||||
|
||||
uint32_t emac_esp_dma_transmit_frame(emac_esp_dma_handle_t emac_esp_dma, uint8_t *buf, uint32_t length)
|
||||
{
|
||||
/* Get the number of Tx buffers to use for the frame */
|
||||
uint32_t bufcount = 0;
|
||||
uint32_t lastlen = length;
|
||||
uint32_t sentout = 0;
|
||||
while (lastlen > CONFIG_ETH_DMA_BUFFER_SIZE) {
|
||||
lastlen -= CONFIG_ETH_DMA_BUFFER_SIZE;
|
||||
bufcount++;
|
||||
}
|
||||
if (lastlen) {
|
||||
bufcount++;
|
||||
}
|
||||
if (bufcount > CONFIG_ETH_DMA_TX_BUFFER_NUM) {
|
||||
goto err;
|
||||
}
|
||||
|
||||
eth_dma_tx_descriptor_t *desc_iter = emac_esp_dma->tx_desc;
|
||||
/* A frame is transmitted in multiple descriptor */
|
||||
for (size_t i = 0; i < bufcount; i++) {
|
||||
DMA_CACHE_INVALIDATE(desc_iter, EMAC_HAL_DMA_DESC_SIZE);
|
||||
/* Check if the descriptor is owned by the Ethernet DMA (when 1) or CPU (when 0) */
|
||||
if (desc_iter->TDES0.Own != EMAC_LL_DMADESC_OWNER_CPU) {
|
||||
goto err;
|
||||
}
|
||||
/* Clear FIRST and LAST segment bits */
|
||||
desc_iter->TDES0.FirstSegment = 0;
|
||||
desc_iter->TDES0.LastSegment = 0;
|
||||
desc_iter->TDES0.Value &= ~(EMAC_TDES0_FS_CTRL_FLAGS_MASK | EMAC_TDES0_LS_CTRL_FLAGS_MASK);
|
||||
if (i == 0) {
|
||||
/* Setting the first segment bit */
|
||||
desc_iter->TDES0.FirstSegment = 1;
|
||||
desc_iter->TDES0.Value |= emac_esp_dma->tx_desc_flags & EMAC_TDES0_FS_CTRL_FLAGS_MASK;
|
||||
}
|
||||
if (i == (bufcount - 1)) {
|
||||
/* Setting the last segment bit */
|
||||
desc_iter->TDES0.LastSegment = 1;
|
||||
desc_iter->TDES0.Value |= emac_esp_dma->tx_desc_flags & EMAC_TDES0_LS_CTRL_FLAGS_MASK;
|
||||
/* Program size */
|
||||
desc_iter->TDES1.TransmitBuffer1Size = lastlen;
|
||||
/* copy data from uplayer stack buffer */
|
||||
memcpy((void *)(desc_iter->Buffer1Addr), buf + i * CONFIG_ETH_DMA_BUFFER_SIZE, lastlen);
|
||||
sentout += lastlen;
|
||||
} else {
|
||||
/* Program size */
|
||||
desc_iter->TDES1.TransmitBuffer1Size = CONFIG_ETH_DMA_BUFFER_SIZE;
|
||||
/* copy data from uplayer stack buffer */
|
||||
memcpy((void *)(desc_iter->Buffer1Addr), buf + i * CONFIG_ETH_DMA_BUFFER_SIZE, CONFIG_ETH_DMA_BUFFER_SIZE);
|
||||
sentout += CONFIG_ETH_DMA_BUFFER_SIZE;
|
||||
}
|
||||
DMA_CACHE_WB(desc_iter->Buffer1Addr, CONFIG_ETH_DMA_BUFFER_SIZE);
|
||||
/* Point to next descriptor */
|
||||
desc_iter = (eth_dma_tx_descriptor_t *)(desc_iter->Buffer2NextDescAddr);
|
||||
}
|
||||
|
||||
/* Set Own bit of the Tx descriptor Status: gives the buffer back to ETHERNET DMA */
|
||||
for (size_t i = 0; i < bufcount; i++) {
|
||||
emac_esp_dma->tx_desc->TDES0.Own = EMAC_LL_DMADESC_OWNER_DMA;
|
||||
DMA_CACHE_WB(emac_esp_dma->tx_desc, EMAC_HAL_DMA_DESC_SIZE);
|
||||
emac_esp_dma->tx_desc = (eth_dma_tx_descriptor_t *)(emac_esp_dma->tx_desc->Buffer2NextDescAddr);
|
||||
}
|
||||
emac_hal_transmit_poll_demand(&emac_esp_dma->hal);
|
||||
return sentout;
|
||||
err:
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t emac_esp_dma_transmit_multiple_buf_frame(emac_esp_dma_handle_t emac_esp_dma, uint8_t **buffs, uint32_t *lengths, uint32_t buffs_cnt)
|
||||
{
|
||||
/* Get the number of Tx buffers to use for the frame */
|
||||
uint32_t dma_bufcount = 0;
|
||||
uint32_t sentout = 0;
|
||||
uint8_t *ptr = buffs[0];
|
||||
uint32_t lastlen = lengths[0];
|
||||
uint32_t avail_len = CONFIG_ETH_DMA_BUFFER_SIZE;
|
||||
|
||||
eth_dma_tx_descriptor_t *desc_iter = emac_esp_dma->tx_desc;
|
||||
/* A frame is transmitted in multiple descriptor */
|
||||
while (dma_bufcount < CONFIG_ETH_DMA_TX_BUFFER_NUM) {
|
||||
DMA_CACHE_INVALIDATE(desc_iter, EMAC_HAL_DMA_DESC_SIZE);
|
||||
/* Check if the descriptor is owned by the Ethernet DMA (when 1) or CPU (when 0) */
|
||||
if (desc_iter->TDES0.Own != EMAC_LL_DMADESC_OWNER_CPU) {
|
||||
goto err;
|
||||
}
|
||||
/* Clear FIRST and LAST segment bits */
|
||||
desc_iter->TDES0.FirstSegment = 0;
|
||||
desc_iter->TDES0.LastSegment = 0;
|
||||
desc_iter->TDES0.Value &= ~(EMAC_TDES0_FS_CTRL_FLAGS_MASK | EMAC_TDES0_LS_CTRL_FLAGS_MASK);
|
||||
desc_iter->TDES1.TransmitBuffer1Size = 0;
|
||||
if (dma_bufcount == 0) {
|
||||
/* Setting the first segment bit */
|
||||
desc_iter->TDES0.FirstSegment = 1;
|
||||
desc_iter->TDES0.Value |= emac_esp_dma->tx_desc_flags & EMAC_TDES0_FS_CTRL_FLAGS_MASK;
|
||||
}
|
||||
|
||||
while (buffs_cnt > 0) {
|
||||
/* Check if input buff data fits to currently available space in the descriptor */
|
||||
if (lastlen < avail_len) {
|
||||
/* copy data from uplayer stack buffer */
|
||||
memcpy((void *)(desc_iter->Buffer1Addr + (CONFIG_ETH_DMA_BUFFER_SIZE - avail_len)), ptr, lastlen);
|
||||
sentout += lastlen;
|
||||
avail_len -= lastlen;
|
||||
desc_iter->TDES1.TransmitBuffer1Size += lastlen;
|
||||
|
||||
/* Update processed input buffers info */
|
||||
buffs_cnt--;
|
||||
ptr = *(++buffs);
|
||||
lastlen = *(++lengths);
|
||||
/* There is only limited available space in the current descriptor, use it all */
|
||||
} else {
|
||||
/* copy data from uplayer stack buffer */
|
||||
memcpy((void *)(desc_iter->Buffer1Addr + (CONFIG_ETH_DMA_BUFFER_SIZE - avail_len)), ptr, avail_len);
|
||||
sentout += avail_len;
|
||||
lastlen -= avail_len;
|
||||
/* If lastlen is not zero, input buff will be fragmented over multiple descriptors */
|
||||
if (lastlen > 0) {
|
||||
ptr += avail_len;
|
||||
/* Input buff fully fits the descriptor, move to the next input buff */
|
||||
} else {
|
||||
/* Update processed input buffers info */
|
||||
buffs_cnt--;
|
||||
ptr = *(++buffs);
|
||||
lastlen = *(++lengths);
|
||||
}
|
||||
avail_len = CONFIG_ETH_DMA_BUFFER_SIZE;
|
||||
desc_iter->TDES1.TransmitBuffer1Size = CONFIG_ETH_DMA_BUFFER_SIZE;
|
||||
/* The descriptor is full here so exit and use the next descriptor */
|
||||
break;
|
||||
}
|
||||
}
|
||||
DMA_CACHE_WB(desc_iter->Buffer1Addr, CONFIG_ETH_DMA_BUFFER_SIZE);
|
||||
/* Increase counter of utilized DMA buffers */
|
||||
dma_bufcount++;
|
||||
|
||||
/* If all input buffers processed, mark as LAST segment and finish the coping */
|
||||
if (buffs_cnt == 0) {
|
||||
/* Setting the last segment bit */
|
||||
desc_iter->TDES0.LastSegment = 1;
|
||||
desc_iter->TDES0.Value |= emac_esp_dma->tx_desc_flags & EMAC_TDES0_LS_CTRL_FLAGS_MASK;
|
||||
break;
|
||||
}
|
||||
|
||||
/* Point to next descriptor */
|
||||
desc_iter = (eth_dma_tx_descriptor_t *)(desc_iter->Buffer2NextDescAddr);
|
||||
}
|
||||
|
||||
/* Set Own bit of the Tx descriptor Status: gives the buffer back to ETHERNET DMA */
|
||||
for (size_t i = 0; i < dma_bufcount; i++) {
|
||||
emac_esp_dma->tx_desc->TDES0.Own = EMAC_LL_DMADESC_OWNER_DMA;
|
||||
DMA_CACHE_WB(emac_esp_dma->tx_desc, EMAC_HAL_DMA_DESC_SIZE);
|
||||
emac_esp_dma->tx_desc = (eth_dma_tx_descriptor_t *)(emac_esp_dma->tx_desc->Buffer2NextDescAddr);
|
||||
}
|
||||
|
||||
emac_hal_transmit_poll_demand(&emac_esp_dma->hal);
|
||||
return sentout;
|
||||
err:
|
||||
return 0;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp_dma_get_valid_recv_len(emac_esp_dma_handle_t emac_esp_dma, uint32_t *ret_len)
|
||||
{
|
||||
eth_dma_rx_descriptor_t *desc_iter = emac_esp_dma->rx_desc;
|
||||
uint32_t used_descs = 0;
|
||||
DMA_CACHE_INVALIDATE(desc_iter, EMAC_HAL_DMA_DESC_SIZE);
|
||||
|
||||
/* Traverse descriptors owned by CPU */
|
||||
while ((desc_iter->RDES0.Own == EMAC_LL_DMADESC_OWNER_CPU) && (used_descs < CONFIG_ETH_DMA_RX_BUFFER_NUM)) {
|
||||
used_descs++;
|
||||
/* Last segment in frame */
|
||||
if (desc_iter->RDES0.LastDescriptor) {
|
||||
/* Since Store Forward must be disabled on some targets, DMA descriptors may contain erroneous frames */
|
||||
/* In addition, "Descriptor Error" (no free descriptors) may truncate a frame even if Store Forward is enabled */
|
||||
if (desc_iter->RDES0.ErrSummary) {
|
||||
emac_esp_dma_flush_recv_frame(emac_esp_dma);
|
||||
*ret_len = 0;
|
||||
return ESP_FAIL;
|
||||
}
|
||||
/* Get the Frame Length of the received packet: substruct 4 bytes of the CRC */
|
||||
*ret_len = desc_iter->RDES0.FrameLength - ETH_CRC_LENGTH;
|
||||
break;
|
||||
}
|
||||
/* First segment in frame */
|
||||
if (desc_iter->RDES0.FirstDescriptor) {
|
||||
emac_esp_dma->rx_desc = desc_iter;
|
||||
}
|
||||
/* point to next descriptor */
|
||||
desc_iter = (eth_dma_rx_descriptor_t *)(desc_iter->Buffer2NextDescAddr);
|
||||
DMA_CACHE_INVALIDATE(desc_iter, EMAC_HAL_DMA_DESC_SIZE);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void emac_esp_dma_get_remain_frames(emac_esp_dma_handle_t emac_esp_dma, uint32_t *remain_frames, uint32_t *free_descs)
|
||||
{
|
||||
eth_dma_rx_descriptor_t *desc_iter = emac_esp_dma->rx_desc;
|
||||
*remain_frames = 0;
|
||||
uint32_t used_descs = 0;
|
||||
|
||||
DMA_CACHE_INVALIDATE(desc_iter, EMAC_HAL_DMA_DESC_SIZE);
|
||||
/* Traverse descriptors owned by CPU */
|
||||
while ((desc_iter->RDES0.Own == EMAC_LL_DMADESC_OWNER_CPU) && (used_descs < CONFIG_ETH_DMA_RX_BUFFER_NUM)) {
|
||||
used_descs++;
|
||||
/* Last segment in frame */
|
||||
if (desc_iter->RDES0.LastDescriptor) {
|
||||
(*remain_frames)++;
|
||||
}
|
||||
/* point to next descriptor */
|
||||
desc_iter = (eth_dma_rx_descriptor_t *)(desc_iter->Buffer2NextDescAddr);
|
||||
DMA_CACHE_INVALIDATE(desc_iter, EMAC_HAL_DMA_DESC_SIZE);
|
||||
}
|
||||
*free_descs = CONFIG_ETH_DMA_RX_BUFFER_NUM - used_descs;
|
||||
}
|
||||
|
||||
uint8_t *emac_esp_dma_alloc_recv_buf(emac_esp_dma_handle_t emac_esp_dma, uint32_t *size)
|
||||
{
|
||||
uint32_t ret_len = 0;
|
||||
uint32_t copy_len = 0;
|
||||
uint8_t *buf = NULL;
|
||||
|
||||
if (emac_esp_dma_get_valid_recv_len(emac_esp_dma, &ret_len) != ESP_OK) {
|
||||
*size = 0;
|
||||
return NULL;
|
||||
}
|
||||
/* packets larger than expected will be truncated */
|
||||
copy_len = ret_len > *size ? *size : ret_len;
|
||||
|
||||
if (copy_len > 0) {
|
||||
buf = malloc(copy_len);
|
||||
if (buf != NULL) {
|
||||
emac_esp_dma_auto_buf_info_t *buff_info = (emac_esp_dma_auto_buf_info_t *)buf;
|
||||
/* no need to check allocated buffer min length prior writing since we know that EMAC DMA is configured to
|
||||
not forward erroneous or undersized frames (less than 64B) on ESP32, see emac_hal_init_dma_default */
|
||||
#ifndef NDEBUG
|
||||
buff_info->magic_id = EMAC_ALLOC_BUF_MAGIC_ID;
|
||||
#endif // NDEBUG
|
||||
buff_info->copy_len = copy_len;
|
||||
}
|
||||
}
|
||||
/* indicate actual size of received frame */
|
||||
*size = ret_len;
|
||||
return buf;
|
||||
}
|
||||
|
||||
uint32_t emac_esp_dma_receive_frame(emac_esp_dma_handle_t emac_esp_dma, uint8_t *buf, uint32_t size)
|
||||
{
|
||||
uint32_t ret_len = 0;
|
||||
uint32_t copy_len = 0;
|
||||
|
||||
if (size != EMAC_DMA_BUF_SIZE_AUTO) {
|
||||
if (emac_esp_dma_get_valid_recv_len(emac_esp_dma, &ret_len) != ESP_OK) {
|
||||
return 0;
|
||||
}
|
||||
/* packets larger than expected will be truncated */
|
||||
copy_len = ret_len > size ? size : ret_len;
|
||||
} else {
|
||||
emac_esp_dma_auto_buf_info_t *buff_info = (emac_esp_dma_auto_buf_info_t *)buf;
|
||||
#ifndef NDEBUG
|
||||
/* check that buffer was allocated by emac_esp_dma_alloc_recv_buf */
|
||||
assert(buff_info->magic_id == EMAC_ALLOC_BUF_MAGIC_ID);
|
||||
#endif // NDEBUG
|
||||
copy_len = buff_info->copy_len;
|
||||
ret_len = copy_len;
|
||||
}
|
||||
|
||||
if (copy_len) {
|
||||
eth_dma_rx_descriptor_t *desc_iter = emac_esp_dma->rx_desc;
|
||||
while(copy_len > CONFIG_ETH_DMA_BUFFER_SIZE) {
|
||||
DMA_CACHE_INVALIDATE(desc_iter->Buffer1Addr, CONFIG_ETH_DMA_BUFFER_SIZE);
|
||||
memcpy(buf, (void *)(desc_iter->Buffer1Addr), CONFIG_ETH_DMA_BUFFER_SIZE);
|
||||
buf += CONFIG_ETH_DMA_BUFFER_SIZE;
|
||||
copy_len -= CONFIG_ETH_DMA_BUFFER_SIZE;
|
||||
/* Set Own bit in Rx descriptors: gives the buffers back to DMA */
|
||||
desc_iter->RDES0.Own = EMAC_LL_DMADESC_OWNER_DMA;
|
||||
DMA_CACHE_WB(desc_iter, EMAC_HAL_DMA_DESC_SIZE);
|
||||
desc_iter = (eth_dma_rx_descriptor_t *)(desc_iter->Buffer2NextDescAddr);
|
||||
}
|
||||
DMA_CACHE_INVALIDATE(desc_iter->Buffer1Addr, CONFIG_ETH_DMA_BUFFER_SIZE);
|
||||
memcpy(buf, (void *)(desc_iter->Buffer1Addr), copy_len);
|
||||
desc_iter->RDES0.Own = EMAC_LL_DMADESC_OWNER_DMA;
|
||||
DMA_CACHE_WB(desc_iter, EMAC_HAL_DMA_DESC_SIZE);
|
||||
/* `copy_len` does not include CRC (which may be stored in separate buffer), hence check if we reached the last descriptor */
|
||||
while (!desc_iter->RDES0.LastDescriptor) {
|
||||
desc_iter = (eth_dma_rx_descriptor_t *)(desc_iter->Buffer2NextDescAddr);
|
||||
desc_iter->RDES0.Own = EMAC_LL_DMADESC_OWNER_DMA;
|
||||
DMA_CACHE_WB(desc_iter, EMAC_HAL_DMA_DESC_SIZE);
|
||||
}
|
||||
/* update rxdesc */
|
||||
emac_esp_dma->rx_desc = (eth_dma_rx_descriptor_t *)(desc_iter->Buffer2NextDescAddr);
|
||||
/* poll rx demand */
|
||||
emac_hal_receive_poll_demand(&emac_esp_dma->hal);
|
||||
}
|
||||
return ret_len;
|
||||
}
|
||||
|
||||
void emac_esp_dma_flush_recv_frame(emac_esp_dma_handle_t emac_esp_dma)
|
||||
{
|
||||
eth_dma_rx_descriptor_t *desc_iter = emac_esp_dma->rx_desc;
|
||||
|
||||
DMA_CACHE_INVALIDATE(desc_iter, EMAC_HAL_DMA_DESC_SIZE);
|
||||
/* While not last descriptor => return back to DMA */
|
||||
while (!desc_iter->RDES0.LastDescriptor) {
|
||||
desc_iter->RDES0.Own = EMAC_LL_DMADESC_OWNER_DMA;
|
||||
DMA_CACHE_WB(desc_iter, EMAC_HAL_DMA_DESC_SIZE);
|
||||
desc_iter = (eth_dma_rx_descriptor_t *)(desc_iter->Buffer2NextDescAddr);
|
||||
}
|
||||
/* the last descriptor */
|
||||
desc_iter->RDES0.Own = EMAC_LL_DMADESC_OWNER_DMA;
|
||||
DMA_CACHE_WB(desc_iter, EMAC_HAL_DMA_DESC_SIZE);
|
||||
/* update rxdesc */
|
||||
emac_esp_dma->rx_desc = (eth_dma_rx_descriptor_t *)(desc_iter->Buffer2NextDescAddr);
|
||||
/* poll rx demand */
|
||||
emac_hal_receive_poll_demand(&emac_esp_dma->hal);
|
||||
}
|
||||
|
||||
esp_err_t emac_esp_del_dma(emac_esp_dma_handle_t emac_esp_dma)
|
||||
{
|
||||
if (emac_esp_dma) {
|
||||
for (int i = 0; i < CONFIG_ETH_DMA_TX_BUFFER_NUM; i++) {
|
||||
free(emac_esp_dma->tx_buf[i]);
|
||||
}
|
||||
for (int i = 0; i < CONFIG_ETH_DMA_RX_BUFFER_NUM; i++) {
|
||||
free(emac_esp_dma->rx_buf[i]);
|
||||
}
|
||||
free(emac_esp_dma->descriptors);
|
||||
free(emac_esp_dma);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t emac_esp_new_dma(const emac_esp_dma_config_t* config, emac_esp_dma_handle_t *ret_handle)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
*ret_handle = NULL;
|
||||
struct emac_esp_dma_t *emac_esp_dma = calloc(1, sizeof(struct emac_esp_dma_t));
|
||||
ESP_GOTO_ON_FALSE(emac_esp_dma, ESP_ERR_NO_MEM, err, TAG, "no mem for esp emac_esp_dma object");
|
||||
|
||||
/* alloc memory for ethernet dma descriptor */
|
||||
uint32_t desc_size = CONFIG_ETH_DMA_RX_BUFFER_NUM * sizeof(eth_dma_rx_descriptor_t) +
|
||||
CONFIG_ETH_DMA_TX_BUFFER_NUM * sizeof(eth_dma_tx_descriptor_t);
|
||||
esp_dma_mem_info_t dma_mem_info = {
|
||||
.extra_heap_caps = MALLOC_CAP_INTERNAL,
|
||||
.dma_alignment_bytes = 4,
|
||||
};
|
||||
esp_dma_capable_calloc(1, desc_size, &dma_mem_info, (void*)&emac_esp_dma->descriptors, NULL);
|
||||
|
||||
ESP_GOTO_ON_FALSE(emac_esp_dma->descriptors, ESP_ERR_NO_MEM, err, TAG, "no mem for descriptors");
|
||||
/* alloc memory for ethernet dma buffer */
|
||||
for (int i = 0; i < CONFIG_ETH_DMA_RX_BUFFER_NUM; i++) {
|
||||
esp_dma_capable_calloc(1, CONFIG_ETH_DMA_BUFFER_SIZE, &dma_mem_info, (void*)&emac_esp_dma->rx_buf[i], NULL);
|
||||
ESP_GOTO_ON_FALSE(emac_esp_dma->rx_buf[i], ESP_ERR_NO_MEM, err, TAG, "no mem for RX DMA buffers");
|
||||
}
|
||||
for (int i = 0; i < CONFIG_ETH_DMA_TX_BUFFER_NUM; i++) {
|
||||
esp_dma_capable_calloc(1, CONFIG_ETH_DMA_BUFFER_SIZE, &dma_mem_info, (void*)&emac_esp_dma->tx_buf[i], NULL);
|
||||
ESP_GOTO_ON_FALSE(emac_esp_dma->tx_buf[i], ESP_ERR_NO_MEM, err, TAG, "no mem for TX DMA buffers");
|
||||
}
|
||||
emac_hal_init(&emac_esp_dma->hal);
|
||||
*ret_handle = emac_esp_dma;
|
||||
return ESP_OK;
|
||||
err:
|
||||
emac_esp_del_dma(emac_esp_dma);
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <inttypes.h>
|
||||
#include "esp_check.h"
|
||||
#include "sdkconfig.h"
|
||||
#include "esp_rom_gpio.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "soc/soc_caps.h"
|
||||
#include "soc/gpio_sig_map.h"
|
||||
#include "soc/io_mux_reg.h"
|
||||
#include "soc/gpio_periph.h"
|
||||
#include "soc/emac_periph.h"
|
||||
#include "esp_private/gpio.h"
|
||||
#include "esp_private/eth_mac_esp_gpio.h"
|
||||
#include "esp_private/esp_gpio_reserve.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#define GET_GPIO_OR_SINGLE(cfg, num) cfg == NULL ? GPIO_NUM_MAX : cfg->num
|
||||
|
||||
static const char *TAG = "esp.emac.gpio";
|
||||
|
||||
static uint64_t s_emac_esp_used_gpio_mask = 0x0;
|
||||
|
||||
static esp_err_t emac_esp_gpio_matrix_init(gpio_num_t gpio_num, uint32_t signal_in_idx, uint32_t signal_out_idx, gpio_mode_t mode)
|
||||
{
|
||||
// silently skip when user don't want to connect the signal to GPIO pad
|
||||
if (gpio_num == GPIO_NUM_NC) {
|
||||
ESP_LOGD(TAG, "%s skipping signal in_idx %" PRIu32 ", out_idx %" PRIu32, __func__, signal_in_idx, signal_out_idx);
|
||||
return ESP_OK;
|
||||
}
|
||||
ESP_RETURN_ON_ERROR(gpio_set_direction(gpio_num, mode), TAG, "failed to set direction %i at GPIO #%i", mode, gpio_num);
|
||||
switch(mode) {
|
||||
case GPIO_MODE_INPUT:
|
||||
ESP_RETURN_ON_FALSE(signal_in_idx != SIG_GPIO_OUT_IDX, ESP_ERR_NOT_SUPPORTED,
|
||||
TAG, "requested periph signal cannot be connect via GPIO Matrix");
|
||||
ESP_RETURN_ON_FALSE(esp_gpio_is_reserved(BIT64(gpio_num)) == false, ESP_ERR_INVALID_STATE,
|
||||
TAG, "GPIO %i is reserved", gpio_num);
|
||||
esp_rom_gpio_connect_in_signal(gpio_num, signal_in_idx, false);
|
||||
break;
|
||||
case GPIO_MODE_OUTPUT:
|
||||
ESP_RETURN_ON_FALSE(signal_out_idx != SIG_GPIO_OUT_IDX, ESP_ERR_NOT_SUPPORTED,
|
||||
TAG, "requested periph signal cannot be connect via GPIO Matrix");
|
||||
ESP_RETURN_ON_FALSE((esp_gpio_reserve(BIT64(gpio_num)) & BIT64(gpio_num)) == 0, ESP_ERR_INVALID_STATE,
|
||||
TAG, "GPIO %i is already reserved", gpio_num);
|
||||
esp_rom_gpio_connect_out_signal(gpio_num, signal_out_idx, false, false);
|
||||
break;
|
||||
case GPIO_MODE_INPUT_OUTPUT:
|
||||
ESP_RETURN_ON_FALSE(signal_in_idx != SIG_GPIO_OUT_IDX, ESP_ERR_NOT_SUPPORTED,
|
||||
TAG, "requested periph signal cannot be connect via GPIO Matrix");
|
||||
ESP_RETURN_ON_FALSE(signal_out_idx != SIG_GPIO_OUT_IDX, ESP_ERR_NOT_SUPPORTED,
|
||||
TAG, "requested periph signal cannot be connect via GPIO Matrix");
|
||||
ESP_RETURN_ON_FALSE((esp_gpio_reserve(BIT64(gpio_num)) & BIT64(gpio_num)) == 0, ESP_ERR_INVALID_STATE,
|
||||
TAG, "GPIO %i is already reserved", gpio_num);
|
||||
esp_rom_gpio_connect_out_signal(gpio_num, signal_out_idx, false, false);
|
||||
esp_rom_gpio_connect_in_signal(gpio_num, signal_in_idx, false);
|
||||
break;
|
||||
default:
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
s_emac_esp_used_gpio_mask |= BIT64(gpio_num);
|
||||
ESP_RETURN_ON_ERROR(gpio_set_pull_mode(gpio_num, GPIO_FLOATING), TAG, "failed to set pull mode at GPIO %i", gpio_num);
|
||||
ESP_RETURN_ON_ERROR(gpio_func_sel(gpio_num, PIN_FUNC_GPIO), TAG, "failed to set GPIO function at GPIO #%i", gpio_num);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t emac_esp_iomux_init(gpio_num_t gpio_num, const emac_iomux_info_t *iomux_info, bool is_input)
|
||||
{
|
||||
// silently skip undefined iomux functions (for example, ESP32 does not use MII COL_IN/CRS_IN)
|
||||
if (iomux_info == NULL) {
|
||||
ESP_LOGD(TAG, "%s skipping target undefined iomux periph function", __func__);
|
||||
return ESP_OK;
|
||||
}
|
||||
// loop over target iomux_info until reached end of list indicated by invalid GPIO num
|
||||
while (iomux_info->gpio_num != GPIO_NUM_MAX) {
|
||||
// if requested GPIO number can be IO muxed or select single pad that can be muxed on the target
|
||||
if(gpio_num == iomux_info->gpio_num || gpio_num == GPIO_NUM_MAX) {
|
||||
ESP_RETURN_ON_FALSE((esp_gpio_reserve(BIT64(iomux_info->gpio_num)) & BIT64(iomux_info->gpio_num)) == 0, ESP_ERR_INVALID_STATE,
|
||||
TAG, "GPIO %i is already reserved", iomux_info->gpio_num);
|
||||
s_emac_esp_used_gpio_mask |= BIT64(iomux_info->gpio_num);
|
||||
ESP_RETURN_ON_ERROR(gpio_func_sel(iomux_info->gpio_num, iomux_info->func), TAG, "failed to set GPIO function at GPIO %i", iomux_info->gpio_num);
|
||||
if (is_input) {
|
||||
PIN_INPUT_ENABLE(GPIO_PIN_MUX_REG[iomux_info->gpio_num]);
|
||||
} else {
|
||||
PIN_INPUT_DISABLE(GPIO_PIN_MUX_REG[iomux_info->gpio_num]);
|
||||
}
|
||||
ESP_RETURN_ON_ERROR(gpio_set_pull_mode(iomux_info->gpio_num, GPIO_FLOATING),
|
||||
TAG, "failed to set pull mode at GPIO %i", iomux_info->gpio_num);
|
||||
return ESP_OK;
|
||||
}
|
||||
iomux_info++;
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t emac_esp_gpio_init_smi(const emac_esp_smi_gpio_config_t *smi_gpio)
|
||||
{
|
||||
if (smi_gpio->mdc_num >= 0) {
|
||||
/* Setup SMI MDC GPIO */
|
||||
ESP_RETURN_ON_ERROR(emac_esp_gpio_matrix_init(smi_gpio->mdc_num, 0, emac_io_idx.mdc_idx, GPIO_MODE_OUTPUT),
|
||||
TAG, "MDC GPIO matrix config failed");
|
||||
}
|
||||
if (smi_gpio->mdio_num >= 0) {
|
||||
/* Setup SMI MDIO GPIO */
|
||||
ESP_RETURN_ON_ERROR(emac_esp_gpio_matrix_init(smi_gpio->mdio_num, emac_io_idx.mdi_idx, emac_io_idx.mdo_idx, GPIO_MODE_INPUT_OUTPUT),
|
||||
TAG, "MDIO GPIO matrix config failed");
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t emac_esp_gpio_matrix_init_mii(const eth_mac_mii_gpio_config_t *mii_gpio)
|
||||
{
|
||||
ESP_RETURN_ON_FALSE(mii_gpio != NULL, ESP_ERR_INVALID_ARG, TAG, "MII IO matrix config cannot be NULL");
|
||||
|
||||
ESP_RETURN_ON_ERROR(emac_esp_gpio_matrix_init(mii_gpio->tx_clk_num, emac_io_idx.mii_tx_clk_i_idx, 0, GPIO_MODE_INPUT),
|
||||
TAG, "TX_CLK GPIO matrix config failed");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_gpio_matrix_init(mii_gpio->tx_en_num, 0, emac_io_idx.mii_tx_en_o_idx, GPIO_MODE_OUTPUT),
|
||||
TAG, "TX_EN GPIO matrix config failed");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_gpio_matrix_init(mii_gpio->txd0_num, 0, emac_io_idx.mii_txd0_o_idx, GPIO_MODE_OUTPUT),
|
||||
TAG, "TDX0 GPIO matrix config failed");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_gpio_matrix_init(mii_gpio->txd1_num, 0, emac_io_idx.mii_txd1_o_idx, GPIO_MODE_OUTPUT),
|
||||
TAG, "TDX1 GPIO matrix config failed");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_gpio_matrix_init(mii_gpio->txd2_num, 0, emac_io_idx.mii_txd2_o_idx, GPIO_MODE_OUTPUT),
|
||||
TAG, "TDX2 GPIO matrix config failed");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_gpio_matrix_init(mii_gpio->txd3_num, 0, emac_io_idx.mii_txd3_o_idx, GPIO_MODE_OUTPUT),
|
||||
TAG, "TDX3 GPIO matrix config failed");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_gpio_matrix_init(mii_gpio->rx_clk_num, emac_io_idx.mii_rx_clk_i_idx, 0, GPIO_MODE_INPUT),
|
||||
TAG, "RX_CLK GPIO matrix config failed");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_gpio_matrix_init(mii_gpio->rxd0_num, emac_io_idx.mii_rxd0_i_idx, 0, GPIO_MODE_INPUT),
|
||||
TAG, "RXD0 GPIO matrix config failed");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_gpio_matrix_init(mii_gpio->rxd1_num, emac_io_idx.mii_rxd1_i_idx, 0, GPIO_MODE_INPUT),
|
||||
TAG, "RXD1 GPIO matrix config failed");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_gpio_matrix_init(mii_gpio->rxd2_num, emac_io_idx.mii_rxd2_i_idx, 0, GPIO_MODE_INPUT),
|
||||
TAG, "RXD2 GPIO matrix config failed");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_gpio_matrix_init(mii_gpio->rxd3_num, emac_io_idx.mii_rxd3_i_idx, 0, GPIO_MODE_INPUT),
|
||||
TAG, "RXD3 GPIO matrix config failed");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_gpio_matrix_init(mii_gpio->col_in_num, emac_io_idx.mii_col_i_idx, 0, GPIO_MODE_INPUT),
|
||||
TAG, "COL_IN GPIO matrix config failed");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_gpio_matrix_init(mii_gpio->crs_in_num, emac_io_idx.mii_crs_i_idx, 0, GPIO_MODE_INPUT),
|
||||
TAG, "CRS_IN GPIO matrix config failed");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_gpio_matrix_init(mii_gpio->tx_er_num, 0, emac_io_idx.mii_tx_er_o_idx, GPIO_MODE_OUTPUT),
|
||||
TAG, "TX_ER GPIO matrix config failed");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_gpio_matrix_init(mii_gpio->rx_er_num, emac_io_idx.mii_rx_er_i_idx, 0, GPIO_MODE_INPUT),
|
||||
TAG, "RX_ER GPIO matrix config failed");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t emac_esp_iomux_init_mii(const eth_mac_mii_gpio_config_t *mii_gpio)
|
||||
{
|
||||
ESP_RETURN_ON_FALSE(emac_mii_iomux_pins.clk_tx != NULL, ESP_ERR_NOT_SUPPORTED, TAG, "target does not support MII IOMUX");
|
||||
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(mii_gpio, tx_clk_num), emac_mii_iomux_pins.clk_tx, true),
|
||||
TAG, "invalid TX_CLK GPIO number");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(mii_gpio, tx_en_num), emac_mii_iomux_pins.tx_en, false),
|
||||
TAG, "invalid TX_EN GPIO number");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(mii_gpio, txd0_num), emac_mii_iomux_pins.txd0, false),
|
||||
TAG, "invalid TXD0 GPIO number");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(mii_gpio, txd1_num), emac_mii_iomux_pins.txd1, false),
|
||||
TAG, "invalid TXD1 GPIO number");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(mii_gpio, txd2_num), emac_mii_iomux_pins.txd2, false),
|
||||
TAG, "invalid TXD2 GPIO number");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(mii_gpio, txd3_num), emac_mii_iomux_pins.txd3, false),
|
||||
TAG, "invalid TXD3 GPIO number");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(mii_gpio, rx_clk_num), emac_mii_iomux_pins.clk_rx, true),
|
||||
TAG, "invalid RX_CLK GPIO number");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(mii_gpio, rx_dv_num), emac_mii_iomux_pins.rx_dv, true),
|
||||
TAG, "invalid RX_DV GPIO number");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(mii_gpio, rxd0_num), emac_mii_iomux_pins.rxd0, true),
|
||||
TAG, "invalid RXD0 GPIO number");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(mii_gpio, rxd1_num), emac_mii_iomux_pins.rxd1, true),
|
||||
TAG, "invalid RXD1 GPIO number");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(mii_gpio, rxd2_num), emac_mii_iomux_pins.rxd2, true),
|
||||
TAG, "invalid RXD2 GPIO number");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(mii_gpio, rxd3_num), emac_mii_iomux_pins.rxd3, true),
|
||||
TAG, "invalid RXD3 GPIO number");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(mii_gpio, col_in_num), emac_mii_iomux_pins.col_in, true),
|
||||
TAG, "invalid COL_IN GPIO number");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(mii_gpio, crs_in_num), emac_mii_iomux_pins.crs_in, true),
|
||||
TAG, "invalid CRS_IN GPIO number");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t emac_esp_iomux_rmii_clk_input(int num)
|
||||
{
|
||||
ESP_RETURN_ON_FALSE(emac_rmii_iomux_pins.clki != NULL, ESP_ERR_NOT_SUPPORTED, TAG, "target does not support RMII CLKI IOMUX");
|
||||
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(num, emac_rmii_iomux_pins.clki, true), TAG, "invalid RMII CLK input GPIO number");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t emac_esp_iomux_rmii_clk_ouput(int num)
|
||||
{
|
||||
ESP_RETURN_ON_FALSE(emac_rmii_iomux_pins.clko != NULL, ESP_ERR_NOT_SUPPORTED, TAG, "target does not support RMII CLKO IOMUX");
|
||||
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(num, emac_rmii_iomux_pins.clko, false), TAG, "invalid RMII CLK output GPIO number");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t emac_esp_iomux_init_rmii(const eth_mac_rmii_gpio_config_t *rmii_gpio)
|
||||
{
|
||||
ESP_RETURN_ON_FALSE(emac_rmii_iomux_pins.clki != NULL, ESP_ERR_NOT_SUPPORTED, TAG, "target does not support RMII IOMUX");
|
||||
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(rmii_gpio, tx_en_num), emac_rmii_iomux_pins.tx_en, false),
|
||||
TAG, "invalid TX_EN GPIO number");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(rmii_gpio, txd0_num), emac_rmii_iomux_pins.txd0, false),
|
||||
TAG, "invalid TXD0 GPIO number");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(rmii_gpio, txd1_num), emac_rmii_iomux_pins.txd1, false),
|
||||
TAG, "invalid TXD1 GPIO number");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(rmii_gpio, crs_dv_num), emac_rmii_iomux_pins.crs_dv, true),
|
||||
TAG,"invalid CRS_DV GPIO number");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(rmii_gpio, rxd0_num), emac_rmii_iomux_pins.rxd0, true),
|
||||
TAG,"invalid RXD0 GPIO number");
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(GET_GPIO_OR_SINGLE(rmii_gpio, rxd1_num), emac_rmii_iomux_pins.rxd1, true),
|
||||
TAG,"invalid RXD1 GPIO number");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t emac_esp_iomux_rmii_init_tx_er(int num)
|
||||
{
|
||||
ESP_RETURN_ON_FALSE(emac_rmii_iomux_pins.tx_er != NULL, ESP_ERR_NOT_SUPPORTED, TAG, "target does not support RMII TX_ER IOMUX");
|
||||
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(num, emac_rmii_iomux_pins.tx_er, false), TAG, "invalid TX_ER GPIO number");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t emac_esp_iomux_rmii_init_rx_er(int num)
|
||||
{
|
||||
ESP_RETURN_ON_FALSE(emac_rmii_iomux_pins.rx_er != NULL, ESP_ERR_NOT_SUPPORTED, TAG, "target does not support RMII RX_ER IOMUX");
|
||||
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(num, emac_rmii_iomux_pins.rx_er, true), TAG, "invalid RX_ER GPIO number");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t emac_esp_iomux_mii_init_tx_er(int num)
|
||||
{
|
||||
ESP_RETURN_ON_FALSE(emac_mii_iomux_pins.tx_er != NULL, ESP_ERR_NOT_SUPPORTED, TAG, "target does not support MII TX_ER IOMUX");
|
||||
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(num, emac_mii_iomux_pins.tx_er, false), TAG, "invalid TX_ER GPIO number");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t emac_esp_iomux_mii_init_rx_er(int num)
|
||||
{
|
||||
ESP_RETURN_ON_FALSE(emac_mii_iomux_pins.rx_er != NULL, ESP_ERR_NOT_SUPPORTED, TAG, "target does not support RMII RX_ER IOMUX");
|
||||
|
||||
ESP_RETURN_ON_ERROR(emac_esp_iomux_init(num, emac_mii_iomux_pins.rx_er, true), TAG, "invalid RX_ER GPIO number");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t emac_esp_gpio_deinit_all(void)
|
||||
{
|
||||
for (int gpio_num = 0; gpio_num < 64; gpio_num++) {
|
||||
if (BIT64(gpio_num) & s_emac_esp_used_gpio_mask) {
|
||||
gpio_reset_pin(gpio_num);
|
||||
esp_gpio_revoke(BIT64(gpio_num));
|
||||
}
|
||||
s_emac_esp_used_gpio_mask &= ~BIT64(gpio_num);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
Reference in New Issue
Block a user