Esp32, NuttX, and Ethernet on a WT32-ETH01

This is a follow-up from the previous post about getting started with ESP32 and NuttX. This time, we'll be looking at trying to enable the Ethernet module on the LAN8720-based WT32-ETH01, and use NuttX micro-IP networking stack as a dual interface stack.

So, let's get started with the ethernet configuration first.

Configuring the Ethernet peripheral

Looking at the NuttX configuration for network drivers, one can notice that there is a flag for the 8720. So let's try to enable it from the menu config:

Device Drivers  --->
	Network Device/PHY Support  ----
    		*** External Ethernet MAC Device Support ***

Unfortunately, there is nothing under External Ethernet MAC Device Support , and that's where one should enable the LAN8720. So looking a bit further into the config, I noticed that for the external ethernet to be supported, the ARCH_HAVE_PHY config should be enabled.

Unfortunately, that's not the case in any of the ESP32 default config, and there is no trace of a ARCH_HAVE_PHY config in the ESP KConfig.  But looking at the specific ESP32 config, I noticed that ETH could be enabled by selecting Ethernet MAC under the peripheral selection.

System Type  --->
    ESP32 Peripheral Selection  --->
        Ethernet MAC
    Ethernet configuration  --->
        (9) RX description number
        (8) TX description number
        (23) MDC Pin
        (18) MDIO Pin
        (5) Reset PHY Pin -->>> Change to 16
        (1) PHY address     

The default configuration, when compared to the indication here is correct, at least for MDC and MDIO.

mdc_pin: GPIO23
mdio_pin: GPIO18
clk_mode: GPIO0_IN
phy_addr: 1
power_pin: GPIO16

However, the reset phy pin, which is referred as a power pin in esp-idf, is wrong- it should be  16 and not the default 5.

As concerns the clock mode, it is handled by this code in the esp-idf:

    if (emac_config.clock_mode != ETH_CLOCK_GPIO0_IN) {
#if CONFIG_SPIRAM_SUPPORT
        // make sure Ethernet won't have conflict with PSRAM
        if (emac_config.clock_mode >= ETH_CLOCK_GPIO16_OUT) {
            if (esp_spiram_is_initialized()) {
                ESP_LOGE(TAG, "GPIO16 and GPIO17 are occupied by PSRAM, please switch to ETH_CLOCK_GPIO_IN or ETH_CLOCK_GPIO_OUT mode");
                ret = ESP_FAIL;
                goto _verify_err;
            } else {
                ESP_LOGW(TAG, "Using GPIO16/17 to output Ethernet RMII clock, make sure you don't have PSRAM on board");
            }
        }
#endif
        // 50 MHz = 40MHz * (6 + 4) / (2 * (2 + 2) = 400MHz / 8
        rtc_clk_apll_enable(1, 0, 0, 6, 2);
        REG_SET_FIELD(EMAC_EX_CLKOUT_CONF_REG, EMAC_EX_CLK_OUT_H_DIV_NUM, 0);
        REG_SET_FIELD(EMAC_EX_CLKOUT_CONF_REG, EMAC_EX_CLK_OUT_DIV_NUM, 0);

        if (emac_config.clock_mode == ETH_CLOCK_GPIO0_OUT) {
            PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO0_U, FUNC_GPIO0_CLK_OUT1);
            REG_WRITE(PIN_CTRL, 6);
            ESP_LOGD(TAG, "EMAC 50MHz clock output on GPIO0");
        } else if (emac_config.clock_mode == ETH_CLOCK_GPIO16_OUT) {
            PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO16_U, FUNC_GPIO16_EMAC_CLK_OUT);
            ESP_LOGD(TAG, "EMAC 50MHz clock output on GPIO16");
        } else if (emac_config.clock_mode == ETH_CLOCK_GPIO17_OUT) {
            PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO17_U, FUNC_GPIO17_EMAC_CLK_OUT_180);
            ESP_LOGD(TAG, "EMAC 50MHz inverted clock output on GPIO17");
        }
    }

There is no such thing yet int he NuttX version, but fortunately for us, the NuttX SW assumes that the clock mode is GPIO0.

So, voila, the configuration looks good - with the exception of the power pin - and so we are ready to compile the SW.

Compilation

Well, after all the struggles met during the first phase, for enabling WIFI, expecting that the compilation would work without any error the first time would have be utopia. And indeed, here is what the compiler said:

 CC:  chip/esp32_emac.c
chip/esp32_emac.c: In function 'phy_enable_interrupt':
chip/esp32_emac.c:1243:38: error: 'MII_INT_REG' undeclared (first use in this function); did you mean 'MISC_REG'?
   ret = emac_read_phy(EMAC_PHY_ADDR, MII_INT_REG, &regval);
                                      ^~~~~~~~~~~
                                      MISC_REG
chip/esp32_emac.c:1243:38: note: each undeclared identifier is reported only once for each function it appears in
chip/esp32_emac.c:1243:52: error: 'regval' undeclared (first use in this function); did you mean 'sigval'?
   ret = emac_read_phy(EMAC_PHY_ADDR, MII_INT_REG, &regval);
                                                    ^~~~~~
                                                    sigval
chip/esp32_emac.c:1249:39: error: 'MII_INT_CLREN' undeclared (first use in this function)
                            (regval & ~MII_INT_CLREN) | MII_INT_SETEN);
                                       ^~~~~~~~~~~~~
chip/esp32_emac.c:1249:56: error: 'MII_INT_SETEN' undeclared (first use in this function); did you mean 'MII_MSR_ESTATEN'?
                            (regval & ~MII_INT_CLREN) | MII_INT_SETEN);
                                                        ^~~~~~~~~~~~~
                                                        MII_MSR_ESTATEN
chip/esp32_emac.c:1240:12: warning: unused variable 'phyval' [-Wunused-variable]
   uint16_t phyval;
            ^~~~~~
chip/esp32_emac.c: In function 'emac_ioctl':
chip/esp32_emac.c:186:24: warning: initialization of 'struct esp32_emacmac_s *' from incompatible pointer type 'struct esp32_emac_s *' [-Wincompatible-pointer-types]
 #define NET2PRIV(_dev) ((struct esp32_emac_s *)(_dev)->d_private)
                        ^
chip/esp32_emac.c:2098:38: note: in expansion of macro 'NET2PRIV'
   FAR struct esp32_emacmac_s *priv = NET2PRIV(dev);
                                      ^~~~~~~~
chip/esp32_emac.c:2111:17: warning: implicit declaration of function 'phy_notify_subscribe' [-Wimplicit-function-declaration]
           ret = phy_notify_subscribe(dev->d_ifname, req->pid, &req->event);
                 ^~~~~~~~~~~~~~~~~~~~
chip/esp32_emac.c:2116:21: error: too many arguments to function 'phy_enable_interrupt'
               ret = phy_enable_interrupt(priv);
                     ^~~~~~~~~~~~~~~~~~~~
chip/esp32_emac.c:1238:12: note: declared here
 static int phy_enable_interrupt(void)
            ^~~~~~~~~~~~~~~~~~~~
make[1]: *** [esp32_emac.o] Error 1
make: *** [arch/xtensa/src/libarch.a] Error 2

Something is obviously wrong in the ESP code here. For example, from the esp32_emac.c, one can see that there is a conflict in the signature of the phy_enable_interrupt function, which is declared without any argument.

#if defined(CONFIG_NETDEV_PHY_IOCTL) && defined(CONFIG_ARCH_PHY_INTERRUPT)
static int phy_enable_interrupt(void)
{
  ...
}
#endif

But later called, in the same file, with an extra priv argument.

static int emac_ioctl(struct net_driver_s *dev, int cmd, unsigned long arg)
{
    ...
#ifdef CONFIG_NETDEV_PHY_IOCTL
#ifdef CONFIG_ARCH_PHY_INTERRUPT
	...
    ret = phy_enable_interrupt(priv);
    ...
#endif

Most likely, the ESP team did not try yet to verify the ethernet driver with the CONFIG_NETDEV_PHY_IOCTL configuration.

So, after disabling the IO control under Networking support->Network Device Operations->Enable PHY ioctl , bingo!, the compilation is working :-)

Starting the Ethernet driver

make download ESPTOOL_PORT=/dev/cu.SLAB_USBtoUART ESPTOOL_BAUD=115200 ESPTOOL_BINDIR={$NUTTX_SPACE}/esp-bins

After a successful flashing, of course,  no trace of the Ethernet driver initialisation... Looking deeper into the code, the issue is conflict on the NETDEV_LATEINIT config - which must be disabled for ethernet:

#if !defined(CONFIG_NETDEV_LATEINIT)
void up_netinitialize(void)
{
  esp32_emac_init();
}
#endif

So, let's disable it under Networking Support ->  Link layer support -> Late driver initialization (I wonder why I enabled it in first place). After that, the interface is finally visible:

nsh> ifconfig
eth0    Link encap:Ethernet HWaddr b4:e6:2d:95:b1:05 at DOWN
        inet addr:0.0.0.0 DRaddr:0.0.0.0 Mask:0.0.0.0

wlan0   Link encap:Ethernet HWaddr b4:e6:2d:95:b1:05 at UP
        inet addr:192.168.1.218 DRaddr:192.168.1.1 Mask:255.255.255.0

Both interface share the same ethernet address. This will definitely confuse the router, which both WIFI and ETH are connected to, but let's try anyways. Next step is to get the interface up using  the ifup eth0 command.

nsh> ifup eth0
netdev_ifr_ioctl: cmd: 1818
emac_init_phy: PHY register 0x0 is: 0x3100
emac_init_phy: PHY register 0x2 is: 0x0007
emac_init_phy: PHY register 0x3 is: 0xc0f1
emac_wait_linkup: PHY register 0x1 is: 0x782d
ifup eth0...OK

Good news, it works, and frames can be received on the ethernet port. But patience, the IP address is still not resolved.

One of the reason the DHCP doe not work for ETH0 is the conflict on the mac addresses. The way the ethernet driver gets its address is using this code:

static int emac_read_mac(uint8_t *mac)
{
  uint32_t regval[2];
  uint8_t *data = (uint8_t *)regval;
  uint8_t crc;
  int i;

  /* The MAC address in register is from high byte to low byte */

  regval[0] = getreg32(MAC_ADDR0_REG);
  regval[1] = getreg32(MAC_ADDR1_REG);

  crc = data[6];
  for (i = 0; i < 6; i++)
    {
      mac[i] = data[5 - i];
    }

  if (crc != esp_crc8(mac, 6))
    {
      nerr("ERROR: Failed to check MAC address CRC\n");

      return -EINVAL;
    }

  return 0;
}

There is nothing wrong with this function, expect that the WIFI adapter uses the same "copy pasted" function. There's definitely room for a clean network architecture here. But anyways, let's hack a mac[5]+=1 just after the CRC check to ensure the mac is unique.

nsh> ifconfig
eth0    Link encap:Ethernet HWaddr a8:03:2a:62:5a:89 at DOWN
        inet addr:0.0.0.0 DRaddr:0.0.0.0 Mask:0.0.0.0

wlan0   Link encap:Ethernet HWaddr a8:03:2a:62:5a:88 at UP
        inet addr:192.168.1.111 DRaddr:192.168.1.1 Mask:255.255.255.0
        
nsh> ifup eth0
netdev_ifr_ioctl: cmd: 1818
emac_init_gpio: emac_init_gpio
emac_config: emac_config: got EMAC_SR_E
emac_read_mac: emac_read_mac -> a8:03:2a:62:5a:89
emac_config: emac_config: macaddr=a8:03:2a:62:5a:8a
emac_init_phy: PHY register 0x0 is: 0x3100
emac_init_phy: PHY register 0x2 is: 0x0007
emac_init_phy: PHY register 0x3 is: 0xc0f1
emac_wait_linkup: emac_wait_linkup:
emac_wait_linkup: PHY register 0x1 is: 0x782d
emac_init_dma: emac_init_dma
emac_start: emac_start
ifup eth0...OK

nsh> ifconfig eth0 up
cmd_ifconfig: Host IP: up
...
dhcpc_request: Received ACK
dhcpc_request: Got IP address 192.168.1.112
dhcpc_request: Got netmask 255.255.255.0
dhcpc_request: Got DNS server 192.168.1.1
dhcpc_request: Got default router 192.168.1.1
dhcpc_request: Lease expires in 43200 seconds
...

nsh> ifconfig
eth0    Link encap:Ethernet HWaddr a8:03:2a:62:5a:89 at UP
        inet addr:192.168.1.112 DRaddr:192.168.1.1 Mask:255.255.255.0

wlan0   Link encap:Ethernet HWaddr a8:03:2a:62:5a:88 at UP
        inet addr:192.168.1.111 DRaddr:192.168.1.1 Mask:255.255.255.0

Et voila, it looks like it's working :-)

Verifying the dual network stack

But well, as usual, something went wrong. Pinging the wlan0 worked, but not the eth0 (no response). Let's have a look at the logs, when do a ping from the mac:

PING 192.168.1.112 (192.168.1.112): 56 data bytes
Request timeout for icmp_seq 0
Request timeout for icmp_seq 1
Request timeout for icmp_seq 2

The corresponding log shows a

wlan_rxpoll: ARP frame

Good news, the ARP frame is receive, bad news, it is received by the WIFI driver and not the ETH driver.  Why would this happen? Because the router thinks the IP 192.168.1.112 is located on the wifi interface. And for this to happen, that would mean that the DHCP request has been done for WIFI. So, let's look at how the DHCP client actually works:

nsh> ifconfig eth0 up
dhcpc_open: MAC: a8:03:2a:62:5a:89
dhcpc_request: Broadcast DISCOVER
udp_callback: flags: 0010
sendto_eventhandler: flags: 0010
udp_send: UDP payload: 256 (0) bytes
udp_send: Outgoing UDP packet length: 284
udp_callback: flags: 0010
wlan_rxpoll: IPv4 frame
udp_callback: flags: 0002
udp_eventhandler: flags: 0002
udp_recvfrom_newdata: Received 300 bytes (of 300)
udp_eventhandler: UDP done
dhcpc_request: Received OFFER from c0a80101
...

The only hint is the wlan_rxpoll: IPv4 frame - that just tells us that the frame is received over WIFI. Since we have a dual interface system, the solution in posix consists in "binding to device" the socket used for the DHCP client. So, let's have a look at the NuttX DHCP Client:

FAR void *dhcpc_open(FAR const char *interface, FAR const void *macaddr,
                     int maclen)
{
      ...

      /* Create a UDP socket */
      pdhcpc->sockfd = socket(PF_INET, SOCK_DGRAM, 0);

       ...

#ifdef CONFIG_NET_UDP_BINDTODEVICE
      /* Bind socket to interface, because UDP packets have to be sent 
       * to the broadcast address at a moment when it is not possible 
       * to decide the target network device using the local or 
       * remote address (which is, by definition and purpose of 
       * DHCP, undefined yet).
       */

      ret = setsockopt(pdhcpc->sockfd, IPPROTO_UDP, UDP_BINDTODEVICE,
                       pdhcpc->interface, strlen(pdhcpc->interface));
       ...
#endif

Of course, the NET_UDP_BINDTODEVICE config is not enabled by default. So let's try again after enabling under Networking Support > UDP Networking > UDP Bind-to-device support.

Bingo, it's working :-)

nsh> ifconfig eth0 up
dhcpc_open: MAC: a8:03:2a:62:5a:89
dhcpc_request: Broadcast DISCOVER
emac_txavail_work: ifup: 1
udp_callback: flags: 0010
sendto_eventhandler: flags: 0010
udp_send: UDP payload: 256 (0) bytes
udp_send: Outgoing UDP packet length: 284
emac_transmit: d_buf=0x3ffb6dec d_len=298
udp_callback: flags: 0010
emac_recvframe: RX bytes 342
emac_rx_interrupt_work: IPv4 frame
udp_callback: flags: 0002
net_dataevent: No receive on connection
udp_datahandler: Buffered 300 bytes
udp_readahead: Received 300 bytes (of 317)
dhcpc_request: Received OFFER from c0a80101

Ping is also working :-)

⋊> ~/P/m/n/nuttx on master ⨯ ping 192.168.1.112
PING 192.168.1.112 (192.168.1.112): 56 data bytes
64 bytes from 192.168.1.112: icmp_seq=0 ttl=64 time=5.438 ms
64 bytes from 192.168.1.112: icmp_seq=1 ttl=64 time=5.306 ms
64 bytes from 192.168.1.112: icmp_seq=2 ttl=64 time=5.176 ms
64 bytes from 192.168.1.112: icmp_seq=3 ttl=64 time=4.650 ms
64 bytes from 192.168.1.112: icmp_seq=4 ttl=64 time=5.147 ms
64 bytes from 192.168.1.112: icmp_seq=5 ttl=64 time=5.436 ms

And NuttX log also shows a correct icmp input message after the emac_rx interrupt message.

 nsh> emac_recvframe: RX bytes 98
emac_rx_interrupt_work: IPv4 frame
icmp_input: Outgoing ICMP packet length: 84 (84)
emac_transmit: d_buf=0x3ffb3e5c d_len=98

Conclusion

What I really like with NuttX is this feeling of Posix - like, when one has a dual stack, the solution is to bind to the device, and for NuttX, that's the exact same solution - a no brainer which brings a lot of advantages when talking about portability.

The downside is that the ESP32 port is still far from being stable, but let's hope for the best and wait for Espressif to implement a nice network driver architecture in the short future.

Next step,  will be a deep dive into the NuttX dual stack micro-IP architecture, and how to enable the bridging mode. That will be the focus on the next blog.