STM32F429 Ethernet with LAN8720 Using STM32CubeMX, LwIP, and FreeRTOS
This guide walks through bringing up 10/100M Ethernet on an STM32F429 board using STM32CubeMX to integrate FreeRTOS and LwIP with an external LAN8720 RMII PHY and an RJ45 (with integrated magnetics).
1. Hardware and pinout
The LAN8720 connects to the STM32F429 via RMII. The typical signal mapping is:
- ETH_MDIO → PA2
- ETH_MDC → PC1
- RMII_TXD0 → PG13
- RMII_TXD1 → PG14
- RMII_TX_EN→ PB11
- RMII_RXD0 → PC4
- RMII_RXD1 → PC5
- RMII_CRS_DV → PA7
- RMII_REF_CLK → PA1
- PHY reset → via PCF8574 I/O expander P7 (active-low reset line to LAN8720)
Adjust the pin assignment in CubeMX too match your exact schematic (for example, ensure TXD0/TXD1 use PG13/PG14 if your board routes them there).
2. LAN8720 PHY notes
- 10/100M, RMII interface, auto-negotiation with full/half duplex supported.
- Auto-MDIX for automatic MDI/MDI-X crossover.
- PHY address is strap-configured. With RXER/PHYAD0 floating (internal pull-down), the address is 0.
- The nINT/REFCLKO pin function is selected by nINTSEL. Configure this according to your board design.
- nINT asserts low on enabled events when interrupts are configured.
Refer to the LAN8720 datasheet for detailed strap options, registers (BCR/BSR/PHY-specific), and interrupt mapping.
3. CubeMX configuration summary
- Clocks: enable HSE and configure system clock at 180 MHz (STM32F429 overdrive as required).
- USART1: asynchronous 115200-8-N-1 for logging.
- GPIO: set PB0 as push-pull output, pulled-up, high speed (used as a heartbeat LED in this example).
- Debug: enable SWD and ST-LINK debugging.
- I2C2: enable default settings for the PCF8574 I/O expander controlling the LAN8720 reset line.
- ETH: select RMII mode, set PHY address to 0, and define a user PHY named "LAN8720A." Choose appropriate ETH GPIO pins per your board (e.g., switch TXD0/TXD1 to PG13/PG14).
4. FreeRTOS setup
- Enable FreeRTOS and keep the default thread. Increase the default task stack to at least 1024 words.
- Change the HAL time base from SysTick to a hardware timer (e.g., TIMx) to avoid conflicts with the RTOS tick. CubeMX will configure the selected timer as the time base.
5. LwIP configuration
- Enable LwIP and set a static IPv4 address (disable DHCP). Provide the IP address, netmask, and gateway for your local network.
- Leave other defaults unless you need features such as DHCP, AutoIP, or higher throughput TCP settings.
6. Code inetgration
6.1 PCF8574 driver (I2C I/O expander)
Create pcf8574.h and pcf8574.c to manage the LAN8720 reset pin through the expander. The implementation below uses simple port read/modify/write helpers and maintains a shadow of the last written value to minimize I2C transactions.
pcf8574.h:
#ifndef __PCF8574_H__
#define __PCF8574_H__
#include "main.h"
#ifdef __cplusplus
extern "C" {
#endif
// Adjust the address for your board.
// Many boards use PCF8574 7-bit address 0x20 (8-bit write 0x40/read 0x41).
#ifndef PCF8574_ADDR
#define PCF8574_ADDR (0x40U) // 8-bit address for HAL (write). For read, OR with 0x01.
#endif
// Optional: memory-less device, but if using Mem API a dummy address is used.
#ifndef PCF8574_DUMMY_MEM
#define PCF8574_DUMMY_MEM (0x00U)
#endif
extern I2C_HandleTypeDef hi2c2;
uint8_t PCF8574_Init(void);
uint8_t PCF8574_ReadPort(void);
HAL_StatusTypeDef PCF8574_WritePort(uint8_t value);
uint8_t PCF8574_ReadBit(uint8_t bit);
HAL_StatusTypeDef PCF8574_WriteBit(uint8_t bit, uint8_t state);
#ifdef __cplusplus
}
#endif
#endif // __PCF8574_H__
pcf8574.c:
#include "pcf8574.h"
#include <string.h>
static uint8_t pcf8574_latch = 0xFF; // default high on quasi-bidirectional pins
static inline HAL_StatusTypeDef pcf8574_tx(uint8_t v) {
return HAL_I2C_Master_Transmit(&hi2c2, PCF8574_ADDR, &v, 1, 0xFF);
}
static inline HAL_StatusTypeDef pcf8574_rx(uint8_t *v) {
return HAL_I2C_Master_Receive(&hi2c2, PCF8574_ADDR | 0x01U, v, 1, 0xFF);
}
uint8_t PCF8574_Init(void) {
// Initialize the expander port and read back for verification.
// Example default: upper nibble high, lower nibble low.
uint8_t init_val = 0xF0;
pcf8574_latch = init_val;
(void)pcf8574_tx(init_val);
uint8_t readback = 0x00;
if (HAL_OK == pcf8574_rx(&readback)) {
// Return the value read from the device for diagnostics.
return readback;
}
return 0x00;
}
uint8_t PCF8574_ReadPort(void) {
uint8_t v = 0;
if (HAL_OK == pcf8574_rx(&v)) {
// Keep the shadow aligned with observed state (optional).
pcf8574_latch = v;
return v;
}
return pcf8574_latch;
}
HAL_StatusTypeDef PCF8574_WritePort(uint8_t value) {
pcf8574_latch = value;
return pcf8574_tx(value);
}
uint8_t PCF8574_ReadBit(uint8_t bit) {
if (bit > 7) return 0;
uint8_t v = PCF8574_ReadPort();
return (v >> bit) & 0x01U;
}
HAL_StatusTypeDef PCF8574_WriteBit(uint8_t bit, uint8_t state) {
if (bit > 7) return HAL_ERROR;
uint8_t v = pcf8574_latch;
if (state) v |= (uint8_t)(1U << bit);
else v &= (uint8_t)~(1U << bit);
return PCF8574_WritePort(v);
}
6.2 PHY reset in ethernetif.c
In the low_level_init() function of ethernetif.c, toggle the LAN8720 reset pin through the PCF8574 before starting the MAC. Define the bit index that controls LAN8720 reset (active-low) according to your wiring.
// Example: PCF8574 P7 controls LAN8720 reset (active low)
#define LAN8720_RESET_BIT (7U)
static void ETH_PHY_Reset(void) {
// Hold reset
(void)PCF8574_WriteBit(LAN8720_RESET_BIT, 0);
HAL_Delay(50);
// Release reset
(void)PCF8574_WriteBit(LAN8720_RESET_BIT, 1);
HAL_Delay(50);
}
// In low_level_init() before configuring the MAC/PHY link
// USER CODE BEGIN MACADDRESS
ETH_PHY_Reset();
// USER CODE END MACADDRESS
6.3 Static IP in lwip.c
If CubeMX leaves the static IP arrays uninitialized, fill them in MX_LWIP_Init():
/* IP addresses initialization */
IP_ADDRESS[0] = 192;
IP_ADDRESS[1] = 168;
IP_ADDRESS[2] = 1;
IP_ADDRESS[3] = 10;
NETMASK_ADDRESS[0] = 255;
NETMASK_ADDRESS[1] = 255;
NETMASK_ADDRESS[2] = 255;
NETMASK_ADDRESS[3] = 0;
GATEWAY_ADDRESS[0] = 192;
GATEWAY_ADDRESS[1] = 168;
GATEWAY_ADDRESS[2] = 1;
GATEWAY_ADDRESS[3] = 1;
Alternative, if your template uses ip4_addr_t and IP4_ADDR macros, set ipaddr/netmask/gw accordingly.
6.4 System startup (main.c)
Initialize HAL, clocks, peripherals, and start the RTOS. LwIP is brought up from the default thread.
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_I2C2_Init();
MX_USART1_UART_Init();
// Initialize PCF8574 early so the PHY reset can be controlled
(void)PCF8574_Init();
printf("Ethernet bring-up test\r\n");
// Create RTOS objects and tasks (generated in freertos.c)
MX_FREERTOS_Init();
// Start the scheduler
osKernelStart();
while (1) {
// Should never reach here
}
}
6.5 Default task (freertos.c)
Bring up LwIP in the default task and blink PB0 as a heartbeat.
void StartDefaultTask(void const * argument) {
// Initialize the TCP/IP stack
MX_LWIP_Init();
for (;;) {
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
osDelay(100);
}
}
7. Verification
- Program the board; the PB0 LED should toggle continuously, indicating the RTOS is running.
- Connect the STM32 board and a PC with an Ethernet cable. Configure the PC’s IPv4 address to be on the same subnet (e.g., 192.168.1.x/24) and set gateway as needed.
- Use the ping command from the PC to verify connectivity to 192.168.1.10.