STM32 Ethernet explained

In this article we will talk about STM32 microcontrollers with a built-in Ethernet controller. That includes microcontrollers from F1, F2, F4, F7, H5, H7 series. We will cover both the hardware part, and the software part. Let's start with the hardware.

Ethernet hardware

The first thing to understand is that the Ethernet controller, unlike other controllers, is divided into two parts: the MAC controller and the PHY controller. This separation exists because Ethernet can operate over different media - for example, a standard wired cable with eight wires, or an optical cable, or a single-pair Ethernet cable with two wires. The MAC controller performs media-independent tasks such as building and parsing Ethernet frames, using DMA for reading and writing frames, and performing frame checksumming. The PHY controller, on the other hand, performs media-dependent tasks such as converting the digital frames prepared by the MAC controller into voltage waveforms on the Ethernet cable, handling link speed and duplex negotiation, and reporting link status to the MAC controller.

Typically, the two controllers are implemented as separate chips. This allows manufacturers to create devices for different media - wired, optical, and so on. Some manufacturers also produce chips that combine both the MAC and PHY. Additionally, some microcontrollers include a built-in MAC and require an external PHY chip to operate, while others have both a built-in MAC and PHY. The latter require minimal external components to implement Ethernet connectivity.

Here are some examples of different MAC and PHY configurations:

As we can see, STM32 microcontrollers with a built-in MAC require an external PHY chip to operate. For example, the Nucleo-H723ZG development board uses an external LAN8742 PHY from Microchip:

Nucleo-H723ZG board

We'll use this board as our reference in the following sections, but other ST Nucleo and Discovery boards operate in nearly the same way.

The two controllers, MAC and PHY, are interconnected. There are several connection standards: MII (Media Independent Interface), RMII (Reduced MII), and RGMII (Reduced Gigabit MII) (see Appendix A). These standards differ in several characteristics, the most important being the number of pins required to connect the MAC and PHY, and their clocking methods. The MII interface is the most complex, requiring 18 pins to interconnect the MAC and PHY, whereas RMII requires only 9 pins.

The table in Appendix B summarises which STM32 microcontrollers have an integrated MAC.

As we can see, all such STM32 MCUs support the RMII interface, which is the simplest to implement and requires the fewest pins. That's why most STM32 development boards use RMII. The RMII interface requires nine pins:

So, to implement Ethernet at the hardware level, we need an external PHY chip and must connect it to the STM32 microcontroller using the Ethernet pins. Assuming RMII, that requires nine pins in total.

At the software level, we need to configure all nine pins for the Ethernet alternate function, meaning they will be controlled by the built-in MAC controller. It may look like this:

// Nucleo-H723ZG bare metal Ethernet Web UI example. See
// https://mongoose.ws/wizard/#/output?board=h723&ide=GCC+make&rtos=baremetal&file=hal.h
static inline void hal_ethernet_init(void) {
  uint16_t pins[] = {PIN('A', 1),  PIN('A', 2),  PIN('A', 7),
                     PIN('B', 13), PIN('C', 1),  PIN('C', 4),
                     PIN('C', 5),  PIN('G', 11), PIN('G', 13)};
  for (size_t i = 0; i < sizeof(pins) / sizeof(pins[0]); i++) {
    hal_gpio_init(pins[i], HAL_GPIO_MODE_AF, HAL_GPIO_OTYPE_PUSH_PULL, HAL_GPIO_SPEED_INSANE,
        HAL_GPIO_PULL_NONE, 11);  // 11 is the Ethernet function
  }
  NVIC_EnableIRQ(ETH_IRQn);                     // Setup Ethernet IRQ handler
  CLRSET(SYSCFG->PMCR, 7 << 21, 4 << 21);       // Use RMII (12.3.1)
  RCC->AHB1ENR |= BIT(15) | BIT(16) | BIT(17);  // Enable Ethernet clocks
}

This is a bare-metal CMSIS-only example. If we want to achieve the same result using CubeMX, we should enable the Ethernet peripheral controller, set it to RMII mode, and make sure all Ethernet pins match the schematic.

Cube MX ethernet settings

Let's take a look at the schematics used by the Nucleo-H723ZG:

LAN8742 chip

In this schematic snippet we see the typical STM32 board design where all 9 Ethernet pins are connected to the PHY. Also,

  1. Resistor pull-ups on MODE0, MODE1, MODE2 pins: these set auto-negotiation on
  2. Resistor pull-down on PHYAD0 sets the PHY address to 0. Theoretically, several PHY chips can be connected to a single MAC. Therefore, when the MAC communicates with a PHY, it specifies the PHY address. The Ethernet driver must be configured with the correct PHY address for proper initialization, which is typically 0.
  3. Resistor pull-down on nINTSEL sets PHY pin 14 as REFCLKO, reference clock output. The PHY takes its clock reference from a 25MHz crystal and an internal PLL doubles it to 50MHz. REFCLKO is connected to the MAC controller clock reference pin, and therefore clocks the MAC. The alternative is when MAC clocks the PHY. That setting should also be reflected in the sofware

The other PHY pins are connected to the Ethernet connector.

We can see from the list of RMII pins that there is an MDIO pin used for communication between the MAC and PHY. The Ethernet driver uses this pin to initialize the PHY chip and to read the link status from it. This way, if the Ethernet cable is unplugged, the driver can notify the user firmware and stop receiving or transmitting data.

It’s time to take a closer look at the software side of things - specifically, the Ethernet driver.

Ethernet driver

The Ethernet driver is the first layer in the firmware’s TCP/IP functionality. The layers are structured as follows:

4-layer network software stack

There are multiple options when it comes to networking software. Traditionally, Cube provides its own Ethernet driver, the open-source LwIP network stack, and example projects. The table below summarizes some common choices:

Cube ThreadX Zephyr Mongoose
Driver stm32h7xx_hal_eth.c 3.3k LoC Cube HAL driver. Plugin API via weak functions nx_stm32_eth_driver.c 2.8k LoC driver that sits on top of the Cube HAL driver. eth_stm32_hal_v2.c ~700 LoC, sits on top of the Cube HAL driver stm32h.c 250 LoC, completely independent
TCP/IP stack LwIP built-in built-in built-in
TLS stack mbedTLS mbedTLS mbedTLS built-in
Library no. ad-hoc example built-in built-in built-in

The Ethernet driver provided by Cube is a complex piece of software, consisting of more than 3,000 lines of code. The ThreadX and Zephyr drivers sit on top of Cube's HAL implementation. Mongoose is the best option for studying the software, as it is tiny in comparison, completely independent, and can be used as a stand-alone component - either in bare-metal firmware or with any RTOS such as FreeRTOS, Zephyr, or ThreadX.

Mongoose provides its own TCP/IP and TLS stack along with an STM32 driver, which is very small - only about 250 lines of code. Let’s take a look at the Mongoose Ethernet driver to see exactly what it does and better understand the software side of Ethernet functionality.

Like any other Mongoose network driver, it implements only 4 functions:

Let's start with the driver init function. It performs the following tasks:

The Ethernet MAC uses DMA to read and write frames to RAM. Note that an MCU may have several RAM regions across different domains, and not all of them may be accessible to the DMA controller. To ensure that DMA-accessible data is placed in the correct RAM region:

  1. Add a section to the linker script:
  .eth_ram (NOLOAD) : { *(.eth_ram* .eth_ram.*) } >RAM_D2 AT> ROM
  1. Make sure the Ethernet driver source code puts DMA data into that section. In our case it looks like this:
static uint8_t s_rxbuf[ETH_DESC_CNT][ETH_PKT_SIZE]
    __attribute__((section(".eth_ram")))
    __attribute__((aligned((8U))));

This linker script mechanism is a common reason why developers struggle to make their TCP/IP stack work - they either forget to add a linker script snippet or have it configured incorrectly.

The next function is poll(), which is called periodically to detect changes in link status. This function communicates with the PHY. If the link status changes from down to up, it updates the duplex and speed parameters.

The next function is tx(), used to transmit a frame constructed by the TCP/IP stack. The STM32 Ethernet controller operates using a circular list of buffers for both receiving and transmitting. The tx() function locates the next free TX buffer and copies the frame into it.

The Ethernet IRQ handler checks whether a new frame has been received. If it has, the handler copies the frame from the next RX DMA buffer into the interface's queue. The Mongoose event loop later retrieves that frame from the queue and passes it to the TCP/IP stack for processing.

That, in essence, is how Mongoose, and most other Ethernet drivers, operate.

Upper layers

Next after the driver comes the TCP/IP stack and the upper layers. These layers no longer deal with Ethernet-specific details, so we’ll provide only a high-level overview of what happens next. The best way to understand this is to follow the “life of a frame” - how it is processed by the entire stack. As an example, let’s consider an incoming frame that contains an HTTP request to the embedded web server, and see how the device processes it and responds:

Life of a frame

Practical example

Mongoose Wizard provides an easy way to build network-enabled applications for various architectures. It generates source code using the Mongoose Library. Let's implement a simple Web UI dashboard with LED toggling.

In Mongoose Wizard, start a new project, select Nucleo-H723ZG as the target architecture, GCC Make as the build environment, and the LED toggle template. Then generate the project.

Copy the default LED callback handlers into main.c, and modify them to operate on the real LED pins instead of the mock variable. Instruct Mongoose to use your custom callbacks instead of the default ones.

Open a serial console. Rebuild and reflash the firmware. Copy the device’s IP address from the serial logs into your web browser - you’ll see that the dashboard is now served by the device and that LED toggling works as expected.

The generated project is a minimal Make/GCC setup for the H723 microcontroller that uses CMSIS headers, Mongoose, and no other software. The files hal.h and hal.c implement a minimal hardware abstraction layer (HAL) that sets up the system clock, initializes UART for console debugging, and initializes Ethernet by configuring all nine RMII Ethernet pins, enabling the STM332 Ethernet MAC controller, and enabling the Ethernet IRQ handler.

The rest is handled by Mongoose. In the file mongoose_config.h — the Mongoose configuration file — we enable the built-in TCP/IP stack and other networking components.

If we open mongoose_config.h, we can see that it enables the ARM GCC build environment, the built-in TLS stack, and the built-in TCP/IP stack with the STM32 Ethernet driver, along with a few other options. It also constructs the device’s MAC address from its unique hardware ID.

As a result, we have a minimal implementation that can run in a bare-metal environment as well as under any RTOS.

Summary

This article provides a comprehensive explanation of how to implement Ethernet on STM32 microcontrollers, focusing on both hardware design and software integration. On the hardware side, it covers the Ethernet architecture based on the STM32 MAC and external PHY connection, explaining how the RMII interface simplifies wiring by requiring only nine pins and is widely used across STM32 development boards. The discussion includes key details such as PHY address configuration, reference clock options, and CubeMX pin setup.

On the software side, the article outlines how the STM32 Ethernet driver serves as the foundation of the networking stack — handling frame transmission, reception, and link monitoring. It compares popular driver and TCP/IP stack implementations from Cube, ThreadX, Zephyr, and Mongoose, emphasizing the advantages of the Mongoose networking stack: a lightweight, portable solution that runs on bare metal or any RTOS, with a compact and independent Ethernet driver. The article also explains how DMA and linker configuration affect Ethernet reliability and performance.

Overall, it offers a step-by-step overview for developers who want to build efficient and reliable STM32 Ethernet applications, from hardware design using RMII to software integration using Mongoose or other stacks.

Appendix A. MII, RMII, RGMII

Feature MII (Media Independent Interface) RMII (Reduced MII) RGMII (Reduced Gigabit MII)
Max Speed 100 Mbps 100 Mbps 1 Gbps
Clock Source Separate TX & RX clocks (25 MHz for 100 M) Single 50 MHz reference clock Single 125 MHz clock (DDR)
Pin Count 18 9 14
Complexity High Low Medium

Appendix B. STM32 MCUs with Ethernet MAC

Family Interface Speed, Mbps Example MCU
STM32F107 MII / RMII 10/100 F107VC
STM32F2x7 MII / RMII 10/100 F207ZG
STM32F4x7/9 MII / RMII 10/100 F407VG, F429ZI
STM32F7x6/9 MII / RMII 10/100 F767ZI, F769NI
STM32H5x3 MII / RMII 10/100 H563ZI, H573ZI
STM32H7x3 MII / RMII / RGMII 10/100/1000 H753ZI
STM32MP1 (MPU) MII / RMII / RGMII 10/100/1000 MP157C