docwilco/w6100
W6100 ESP-IDF component driver for ESP32: MACRAW Ethernet frames into esp_eth/esp_netif, with IRQ/poll RX task, stable 16-bit reads, and PHY link handling.
0
Project description
Cover image generated by Gemini.
W6100 ESP-IDF Component Driver – Key Characteristics
When using the WIZnet W6100 with an ESP32, this project organizes the driver in a way that fits the ESP-IDF standard Ethernet flow (esp_eth / esp_netif). ESP-IDF provides a consistent model for Ethernet drivers (including external SPI-Ethernet modules) and a standard path to attach them to the TCP/IP stack through esp_netif/lwIP.
Repository:
https://github.com/docwilco/w61001) Design direction visible in the project structure
(1) MAC/PHY separation aligned with ESP-IDF Ethernet framework (esp_eth)
The project follows ESP-IDF’s expected split between a MAC driver (esp_eth_mac_t) and a PHY driver (esp_eth_phy_t). With this structure, Ethernet can be integrated into an application using the same standard patterns as other ESP-IDF network interfaces.
(2) Receive path organized around MACRAW (frame-level processing)
In w6100_mac.c, the default setup configures SOCK0 in MACRAW mode and delivers received Ethernet frames upward. This approach matches well with using the ESP-IDF networking stack (esp_netif/lwIP) on the ESP32 side.
(3) Both interrupt-driven and polling-driven operation are considered
Some boards cannot easily use an INT pin. The project supports both interrupt mode and polling mode (via esp_timer), while keeping the actual frame processing consolidated in a dedicated RX task.
2) A convenience initialization API (w6100_init.c, w6100.h)
w6100_init() performs SPI bus initialization → MAC/PHY creation → Ethernet driver install in one place. It also validates configuration values, which helps reduce common integration mistakes.
- Required GPIO checks and ISR service setup
ESP_RETURN_ON_FALSE(config && eth_handle_out, ESP_ERR_INVALID_ARG, TAG, "invalid argument");
ESP_RETURN_ON_FALSE(config->mosi_gpio >= 0, ESP_ERR_INVALID_ARG, TAG, "mosi_gpio required");
ESP_RETURN_ON_FALSE(config->miso_gpio >= 0, ESP_ERR_INVALID_ARG, TAG, "miso_gpio required");
ESP_RETURN_ON_FALSE(config->sclk_gpio >= 0, ESP_ERR_INVALID_ARG, TAG, "sclk_gpio required");
ESP_RETURN_ON_FALSE(config->cs_gpio >= 0, ESP_ERR_INVALID_ARG, TAG, "cs_gpio required");
// Install GPIO ISR service if using interrupt mode if (config->int_gpio >= 0) {
ret = gpio_install_isr_service(0);
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "GPIO ISR service install failed");
return ret;
}
}- SPI bus configuration + MAC/PHY creation + driver install
spi_bus_config_t buscfg = {
.mosi_io_num = config->mosi_gpio,
.miso_io_num = config->miso_gpio,
.sclk_io_num = config->sclk_gpio,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 16384, // 16KB DMA buffer
};
spi_bus_initialize(config->spi_host, &buscfg, SPI_DMA_CH_AUTO);
spi_device_interface_config_t spi_devcfg = {
.mode = 0,
.clock_speed_hz = config->spi_clock_mhz * 1000 * 1000,
.spics_io_num = config->cs_gpio,
.queue_size = 20,
};
eth_w6100_config_t w6100_cfg = {
.int_gpio_num = config->int_gpio,
.poll_period_ms = (config->int_gpio < 0) ? 10 : 0,
.spi_host_id = config->spi_host,
.spi_devcfg = &spi_devcfg,
.custom_spi_driver = ETH_DEFAULT_SPI,
};
esp_eth_mac_t *mac = esp_eth_mac_new_w6100(&w6100_cfg, &mac_cfg);
eth_phy_config_t phy_cfg = ETH_PHY_DEFAULT_CONFIG();
phy_cfg.reset_gpio_num = config->rst_gpio;
esp_eth_phy_t *phy = esp_eth_phy_new_w6100(&phy_cfg);
esp_eth_config_t eth_cfg = ETH_DEFAULT_CONFIG(mac, phy);
esp_eth_driver_install(ð_cfg, eth_handle_out);- Optional MAC address configuration
if (config->mac_addr) {
esp_eth_ioctl(*eth_handle_out, ETH_CMD_S_MAC_ADDR, (void *)config->mac_addr);
}In ESP-IDF, attaching the Ethernet driver to the TCP/IP stack is a separate step and is typically done using
esp_netifglue in the standard Ethernet examples.
3) MACRAW default configuration is explicit (w6100_mac.c)
The default setup makes the following decisions clear:
- Only SOCK0 is used for MACRAW
- TX/RX buffers (16KB each) are assigned to SOCK0
- Only the RECV interrupt is enabled for SOCK0
- Interrupt re-assert level is set to maximum (to reduce the chance of missing an interrupt)
- Global interrupts are enabled
// Unlock network configuration uint8_t unlock = W6100_NETLCKR_UNLOCK;
w6100_write(emac, W6100_REG_NETLCKR, &unlock, sizeof(unlock));
// Only SOCK0 can be used as MAC RAW mode, so we give the whole buffer (16KB TX and 16KB RX) to SOCK0 uint8_t reg_value = 16;
w6100_write(emac, W6100_REG_SOCK_RX_BSR(0), ®_value, sizeof(reg_value));
w6100_write(emac, W6100_REG_SOCK_TX_BSR(0), ®_value, sizeof(reg_value));
reg_value = 0;
for (int i = 1; i < 8; i++) {
w6100_write(emac, W6100_REG_SOCK_RX_BSR(i), ®_value, sizeof(reg_value));
w6100_write(emac, W6100_REG_SOCK_TX_BSR(i), ®_value, sizeof(reg_value));
}
/* Enable MAC RAW mode for SOCK0, enable MAC filter */
reg_value = W6100_SMR_MACRAW | W6100_SMR_MF;
w6100_write(emac, W6100_REG_SOCK_MR(0), ®_value, sizeof(reg_value));
/* Enable receive event for SOCK0 */
reg_value = W6100_SIR_RECV;
w6100_write(emac, W6100_REG_SOCK_IMR(0), ®_value, sizeof(reg_value));
/* Set the interrupt re-assert level to maximum (~1.5ms) to lower the chances of missing it */ uint16_t int_level = __builtin_bswap16(0xFFFF);
w6100_write(emac, W6100_REG_INTPTMR, &int_level, sizeof(int_level));
/* Enable global interrupt */
reg_value = W6100_SYCR1_IEN;
w6100_write(emac, W6100_REG_SYCR1, ®_value, sizeof(reg_value));4) RX processing: ISR/polling → RX task → stack_input() (w6100_mac.c)
(1) Interrupt mode: notify RX task from ISR
IRAM_ATTR static void w6100_isr_handler(void *arg)
{
emac_w6100_t *emac = (emac_w6100_t *)arg;
BaseType_t high_task_wakeup = pdFALSE;
vTaskNotifyGiveFromISR(emac->rx_task_hdl, &high_task_wakeup);
if (high_task_wakeup != pdFALSE) {
portYIELD_FROM_ISR();
}
}(2) Polling mode: notify RX task from esp_timer
static void w6100_poll_timer(void *arg)
{
emac_w6100_t *emac = (emac_w6100_t *)arg;
xTaskNotifyGive(emac->rx_task_hdl);
}(3) RX task: handle RECV event and pass frames to the upper stack
Core flow:
- Check
SIR_RECVon SOCK0 - Clear interrupt using IRCLR
- Allocate receive buffer → call
receive()to read the frame - Deliver to upper layers using
stack_input()
static void emac_w6100_task(void *arg)
{
emac_w6100_t *emac = (emac_w6100_t *)arg;
uint8_t status = 0;
uint8_t *buffer = NULL;
uint32_t frame_len = 0;
uint32_t buf_len = 0;
esp_err_t ret;
while (1) {
if (emac->int_gpio_num >= 0) {
if (ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(1000)) == 0 &&
gpio_get_level(emac->int_gpio_num) != 0) {
continue;
}
} else {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
}
w6100_read(emac, W6100_REG_SOCK_IR(0), &status, sizeof(status));
if (status & W6100_SIR_RECV) {
uint8_t clr = W6100_SIR_RECV;
w6100_write(emac, W6100_REG_SOCK_IRCLR(0), &clr, sizeof(clr));
do {
frame_len = ETH_MAX_PACKET_SIZE;
if ((ret = emac_w6100_alloc_recv_buf(emac, &buffer, &frame_len)) == ESP_OK) {
if (buffer != NULL) {
buf_len = W6100_ETH_MAC_RX_BUF_SIZE_AUTO;
if (emac->parent.receive(&emac->parent, buffer, &buf_len) == ESP_OK) {
if (buf_len == 0) {
free(buffer);
} else if (frame_len > buf_len) {
ESP_LOGE(TAG, "received frame was truncated");
free(buffer);
} else {
emac->eth->stack_input(emac->eth, buffer, buf_len);
}
} else {
ESP_LOGE(TAG, "frame read from module failed");
free(buffer);
}
}
} else if (ret == ESP_ERR_NO_MEM) {
ESP_LOGE(TAG, "no mem for receive buffer");
emac_w6100_flush_recv_frame(emac);
}
} while (emac->packets_remain);
}
}
}ESP-IDF Ethernet drivers typically deliver frames upward through
stack_input()as part of the standard Ethernet-to-TCP/IP stack integration path.
5) Stabilized reads for 16-bit size registers (w6100_mac.c)
RX/TX size registers can change while being read, producing inconsistent high/low bytes. The driver avoids this by reading twice until the values match.
static esp_err_t w6100_get_tx_free_size(emac_w6100_t *emac, uint16_t *size)
{
uint16_t free0, free1 = 0;
do {
w6100_read(emac, W6100_REG_SOCK_TX_FSR(0), &free0, sizeof(free0));
w6100_read(emac, W6100_REG_SOCK_TX_FSR(0), &free1, sizeof(free1));
} while (free0 != free1);
*size = __builtin_bswap16(free0);
return ESP_OK;
}
static esp_err_t w6100_get_rx_received_size(emac_w6100_t *emac, uint16_t *size)
{
uint16_t received0, received1 = 0;
do {
w6100_read(emac, W6100_REG_SOCK_RX_RSR(0), &received0, sizeof(received0));
w6100_read(emac, W6100_REG_SOCK_RX_RSR(0), &received1, sizeof(received1));
} while (received0 != received1);
*size = __builtin_bswap16(received0);
return ESP_OK;
}
6) PHY handling note: inverted bit semantics vs. W5500 (w6100_phy.c)
w6100_phy.c clearly documents PHY status bit semantics, including a common pitfall: Speed/Duplex bits are inverted compared to W5500.
/* W6100 PHYSR bit semantics are inverted from W5500:
* - SPD: 1=10Mbps, 0=100Mbps (W5500: 1=100Mbps)
* - DPX: 1=half, 0=full (W5500: 1=full)
*/
if (physr.speed) {
speed = ETH_SPEED_10M;
} else {
speed = ETH_SPEED_100M;
}
if (physr.duplex) {
duplex = ETH_DUPLEX_HALF;
} else {
duplex = ETH_DUPLEX_FULL;
}
ESP_LOGI(TAG, "Link Up: %s %s",
speed == ETH_SPEED_100M ? "100Mbps" : "10Mbps",
duplex == ETH_DUPLEX_FULL ? "Full-Duplex" : "Half-Duplex");
Link changes and speed/duplex updates are also propagated upward via the ESP-IDF driver state mechanism (on_state_changed()).
7) When this project is useful
This project is best viewed as a driver/reference for integration rather than a “demo application”. It is particularly useful if you want to:
- Integrate W6100 as a standard ESP-IDF Ethernet interface on ESP32
- Keep RX processing organized around an RX task while supporting both interrupt and polling environments
- Handle PHY link state (speed/duplex included) at the driver level for robust operation
8) Minimal usage example (integration-level)
Below is a minimal example showing how w6100_init() can be called. Pin numbers and SPI host should be adjusted for your board.
#include "w6100.h"
void app_main(void)
{
esp_eth_handle_t eth_handle = NULL;
w6100_init_config_t cfg = W6100_INIT_CONFIG_DEFAULT();
cfg.spi_host = SPI2_HOST;
cfg.mosi_gpio = 23;
cfg.miso_gpio = 19;
cfg.sclk_gpio = 18;
cfg.cs_gpio = 5;
cfg.int_gpio = 4; // set to -1 if INT is not used (polling)
cfg.rst_gpio = -1; // set if your board connects RST
cfg.spi_clock_mhz = 33;
ESP_ERROR_CHECK(w6100_init(&cfg, ð_handle));
// Next step: attach to TCP/IP stack using ESP-IDF Ethernet example flow.
// Typically: esp_eth_new_netif_glue() + esp_netif_attach()
}
ESP-IDF’s standard Ethernet examples show the complete sequence: driver install → esp_netif attach → DHCP.
Summary
In short, this project provides a structured way to integrate W6100 with ESP32 in an ESP-IDF-friendly form. From the code itself, the following are notable implementation points:
w6100_init()consolidates SPI/MAC/PHY/driver install into a single entry point- Clear MACRAW-centric default configuration (buffer allocation, interrupts, interrupt re-assert level)
- RX processing converges on an RX task, while supporting both interrupt and polling wake-up methods
- Stabilized double-read for 16-bit size registers
- Explicit PHY status semantics (inverted vs. W5500) and link-state prop

