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:
- External MAC: Microchip LAN924x, Broadcom BCM5241
- External PHY: TI DP83848, Microchip LAN87x, Analog Devices ADIN1200
- External MAC + PHY: Wiznet W5500, Microchip ENC28J60
- MCU with built-in MAC: ESP32, NXP i.MX RT, STM32
- MCU with built-in MAC + PHY: Microchip PIC32MX, TI TM4C129x
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:
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:
- 2 pins for for data transmission:
TXD0andTXD1, MAC -> PHY - 2 pins for for data reception:
RXD0andRXD1, PHY -> MAC - 1 pin for transmit enable,
TX_EN, MAC -> PHY - 1 pin
CRS_DVfor carrier sense / receive data valid, PHY -> MAC - 1 pin for PHY management register access,
MDIO, bidirectional - 1 pin for PHY management clock,
MDC - 1 pin for 50Mhz reference clock
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.
Let's take a look at the schematics used by the Nucleo-H723ZG:
In this schematic snippet we see the typical STM32 board design where all 9 Ethernet pins are connected to the PHY. Also,
- Resistor pull-ups on MODE0, MODE1, MODE2 pins: these set auto-negotiation on
- 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.
- 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:
- Driver layer - Initializes the controller, transmits and receives frames by communicating with the MAC controller, and monitors link status.
- TCP/IP stack - Constructs outgoing frames and parses incoming frames, recognizing ARP, IP, ICMP, TCP, and UDP protocols.
- TLS stack - Optional; used only when secure communication is required.
- Library layer - Includes HTTP and MQTT libraries; parses incoming HTTP/MQTT data and constructs outgoing HTTP/MQTT data.
- User application - Uses the HTTP, MQTT, and other library APIs to implement web UIs, cloud connectivity, custom protocols, and more.
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:
- init - initialises the driver
- poll - checks the link speed, duplex and status
- tx - transmit a frame
- ETH_IRQHandler - IRQ handler. Receives frames
Let's start with the driver init function. It performs the following tasks:
- Fetches the PHY address and clocking direction from the interface configuration
- Initializes the RX and TX DMA descriptors (see note below)
- Resets the controller and sets the MDC clock divider and other parameters
- Initializes the PHY via the MDC/MDIO management pins
- Sets up MAC address filtering
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:
- Add a section to the linker script:
.eth_ram (NOLOAD) : { *(.eth_ram* .eth_ram.*) } >RAM_D2 AT> ROM
- 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:
- The browser sends an Ethernet frame containing an HTTP request.
- The Ethernet hardware receives the frame, the MAC controller copies it
to the DMA buffer, and an Ethernet interrupt is triggered. - The device’s IRQ handler is called and copies the frame to the TCP/IP stack.
- The TCP/IP stack parses the frame, locates the corresponding connection or socket, copies frame data to the connection or socket receive buffer, and triggers the HTTP library's "new data" event for that connection.
- The HTTP library parses the received data, determines that it is a new HTTP
request, and calls the user-defined callback written by the firmware developer. - The user-defined callback creates a response and calls the HTTP library’s
mg_http_reply()function to send it. - The HTTP library call triggers the TCP/IP stack's send function.
- The TCP/IP stack splits the outgoing data into frames.
- It then calls the driver’s
tx()function to transmit them.
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 |