ETH(Ethernet,以太网)是STM32H743内置的高速网络外设,兼容IEEE 802.3标准,支持10/100Mbps自适应速率、全双工/半双工通信,集成DMA控制器和硬件校验单元,可实现嵌入式设备的网络通信(如TCP/IP、HTTP、MQTT)。ETH是物联网、工业控制、远程监控等场景的核心接口。
一、STM32H743 ETH 核心硬件特性
STM32H743的ETH外设基于RMII(Reduced Media Independent Interface,简化媒体独立接口)设计,核心特性如下:
特性 | 说明 |
通信速率 | 10Mbps/100Mbps自适应,全双工(同时收发)/半双工(分时收发) |
接口类型 | RMII(仅需7根线,简化硬件),支持外接PHY芯片(如LAN8720、DP83848) |
传输机制 | 硬件DMA(2个发送DMA通道、2个接收DMA通道),无需CPU参与数据搬运 |
校验能力 | 硬件CRC校验、帧长度检查、错误检测,降低CPU开销 |
缓冲区管理 | 支持描述符链表(Descriptor List),灵活管理收发数据缓冲区 |
协议兼容 | 原生支持以太网帧收发,结合LWIP协议栈可实现TCP、UDP、HTTP、MQTT等高层协议 |
典型引脚(RMII) | REF_CLK(PA1)、MDIO(PA2)、MDC(PC1)、RXD0(PC4)、RXD1(PC5)、TX_EN(PB11)、TXD0(PB12)、TXD1(PB13) |
典型应用 | 网络数据传输、远程控制、物联网MQTT通信、HTTP服务器、TCP/IP通信 |
ETH 核心信号(RMII模式)
RMII是ETH外设与PHY芯片(如LAN8720)的通信接口,相比传统MII接口减少了引脚数量:
信号名称 | 功能 | STM32H743引脚 | PHY芯片(LAN8720)引脚 | 备注 |
REF_CLK | 参考时钟(50MHz) | PA1 | XTAL1/CLKIN | 可由PHY提供给STM32,或STM32提供给PHY |
MDIO | 管理数据输入/输出 | PA2 | MDIO | 配置PHY芯片(如速率、双工模式) |
MDC | 管理时钟 | PC1 | MDC | MDIO的时钟(≤2.5MHz) |
RXD0/RXD1 | 接收数据0/1 | PC4/PC5 | RXD0/RXD1 | 从PHY接收以太网数据 |
TX_EN | 发送使能 | PB11 | TX_EN | 指示STM32正在发送数据 |
TXD0/TXD1 | 发送数据0/1 | PB12/PB13 | TXD0/TXD1 | 向PHY发送以太网数据 |
nINT/Link | 中断/链路状态 | 可选(如PA0) | INT/Link | 检测网络连接状态、链路速率 |
关键:
- PHY芯片(如LAN8720)需外接25MHz晶振(生成50MHz REF_CLK),或由STM32的MCO引脚提供50MHz时钟;
- 以太网接口需接网络隔离变压器(如HR911105A),并在PHY芯片与变压器间接100Ω终端电阻;
- STM32H743的ETH时钟需配置为精准的50MHz(RMII参考时钟)。
二、ETH 通信原理
以太网通信的核心是“以帧为单位传输数据,通过MAC地址寻址,结合TCP/IP协议实现跨网络通信”,STM32 ETH外设的工作流程如下:
1. 核心概念
- PHY芯片:物理层芯片,负责将STM32的RMII信号转换为以太网差分信号(RJ45接口),并实现10/100Mbps自适应、链路检测;
- MAC层:STM32 ETH外设实现媒体访问控制层,负责帧的封装/解封装、MAC地址过滤、DMA传输;
- 以太网帧:最小传输单元,包含目标MAC、源MAC、帧类型、数据、CRC校验(硬件自动添加/校验);
- LWIP协议栈:轻量级TCP/IP协议栈(STM32CubeMX可直接集成),将以太网帧封装为TCP/UDP/IP数据包,实现高层协议通信;
- DMA描述符:ETH外设通过描述符链表管理收发缓冲区,每个描述符包含缓冲区地址、长度、状态(如是否完成)。
2. 通信流程(结合LWIP)
- 初始化:
- 配置ETH引脚、时钟、RMII接口参数;
- 初始化PHY芯片(设置速率、双工模式);
- 初始化LWIP协议栈(配置IP地址、子网掩码、网关);
- 数据发送:
- 应用层(如TCP客户端)调用LWIP API发送数据;
- LWIP将数据封装为IP/TCP/UDP包,再封装为以太网帧;
- ETH DMA将帧数据写入发送缓冲区,硬件自动发送到PHY芯片;
- PHY芯片将RMII信号转为差分信号,通过RJ45发送到网络;
- 数据接收:
- PHY芯片接收以太网差分信号,转为RMII信号给STM32 ETH;
- ETH DMA将接收的帧数据写入接收缓冲区,硬件完成CRC校验;
- LWIP协议栈解析帧数据,提取IP/TCP/UDP包,传递给应用层。
三、STM32H743 ETH 配置步骤
LWIP+TCP客户端/服务器,CubeMX配置步骤如下:
步骤1:硬件准备
- 核心板:STM32H743(引出RMII引脚:PA1/PA2/PC1/PC4/PC5/PB11/PB12/PB13);
- PHY模块:LAN8720+HR911105A网络模块(RMII接口,带RJ45);
- 接线(LAN8720为例):
STM32引脚 | LAN8720引脚 | 功能 |
PA1 | CLKIN | 50MHz参考时钟 |
PA2 | MDIO | 管理数据 |
PC1 | MDC | 管理时钟 |
PC4 | RXD0 | 接收数据0 |
PC5 | RXD1 | 接收数据1 |
PB11 | TX_EN | 发送使能 |
PB12 | TXD0 | 发送数据0 |
PB13 | TXD1 | 发送数据1 |
3.3V | VCC | 供电 |
GND | GND | 共地 |
步骤2:CubeMX 配置
ETH配置:
MPU配置:
LWIP配置:
步骤3:LWIP核心API
LWIP提供了简洁的API实现TCP/UDP通信,核心函数如下:
函数 | 功能 |
tcp_new() | 创建TCP控制块(用于客户端/服务器) |
tcp_bind() | 绑定TCP端口(服务器端) |
tcp_listen() | 监听TCP端口(服务器端) |
tcp_accept() | 接受TCP连接(服务器端回调) |
tcp_connect() | 发起TCP连接(客户端) |
tcp_write() | 发送TCP数据 |
tcp_recv() | 注册TCP接收回调函数 |
tcp_sent() | 注册TCP发送完成回调函数 |
udp_new() | 创建UDP控制块 |
udp_bind() | 绑定UDP端口 |
udp_sendto() | 发送UDP数据到指定IP/端口 |
udp_recv() | 注册UDP接收回调函数 |
sys_thread_new() | 创建LWIP线程(处理网络事件) |
四、实战代码示例
示例1:LWIP TCP服务器(接收PC数据并回显)
STM32作为TCP服务器,监听8080端口,接收PC端网络调试助手的数据并回显:
1. 核心代码(main.c)
#include "stm32h7xx_hal.h" #include "lwip/tcp.h" #include "lwip/inet.h" #include <stdio.h> #include <string.h> /* ETH句柄(CubeMX自动生成) */ ETH_HandleTypeDef heth; /* USART1句柄(串口输出,115200 8N1) */ UART_HandleTypeDef huart1; /* TCP服务器参数 */ #define TCP_SERVER_PORT 8080 struct tcp_pcb *tcp_server_pcb; // TCP控制块 /* 错误处理函数 */ void Error_Handler(void) { while(1) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // LED快闪表示错误 HAL_Delay(200); } } /** * @brief 重定向printf到串口 */ int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, 10); return ch; } /** * @brief TCP接收数据回调函数 */ err_t tcp_server_recv_callback(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) { if(p != NULL) { // 1. 点亮LED表示接收到数据 HAL_GPIO_SetPin(GPIOA, GPIO_PIN_5); // 2. 回显数据到客户端(PC) tcp_write(tpcb, p->payload, p->len, TCP_WRITE_FLAG_COPY); tcp_output(tpcb); // 3. 打印接收的数据(串口输出) printf("TCP Server Received: "); for(uint16_t i=0; i<p->len; i++) { printf("%02X ", ((uint8_t*)p->payload)[i]); } printf("\r\n"); // 4. 释放pbuf缓冲区 pbuf_free(p); // 5. 延时后熄灭LED HAL_Delay(500); HAL_GPIO_ResetPin(GPIOA, GPIO_PIN_5); } else if(err == ERR_OK) { // 客户端关闭连接 tcp_close(tpcb); tcp_server_pcb = NULL; printf("TCP Client Disconnected!\r\n"); } return ERR_OK; } /** * @brief TCP连接接受回调函数 */ err_t tcp_server_accept_callback(void *arg, struct tcp_pcb *newpcb, err_t err) { if(err == ERR_OK) { // 注册接收回调函数 tcp_recv(newpcb, tcp_server_recv_callback); tcp_server_pcb = newpcb; printf("TCP Client Connected! IP: %s:%d\r\n", inet_ntoa(newpcb->remote_ip), ntohs(newpcb->remote_port)); } return ERR_OK; } /** * @brief 初始化TCP服务器 */ void tcp_server_init(void) { // 1. 创建TCP控制块 tcp_server_pcb = tcp_new(); if(tcp_server_pcb == NULL) { printf("TCP Server Create Failed!\r\n"); Error_Handler(); } // 2. 绑定端口 ip_addr_t ip_addr; IP4_ADDR(&ip_addr, 192, 168, 1, 100); // 本地IP err_t err = tcp_bind(tcp_server_pcb, &ip_addr, TCP_SERVER_PORT); if(err != ERR_OK) { printf("TCP Server Bind Failed! Err: %d\r\n", err); Error_Handler(); } // 3. 监听端口 tcp_server_pcb = tcp_listen(tcp_server_pcb); if(tcp_server_pcb == NULL) { printf("TCP Server Listen Failed!\r\n"); Error_Handler(); } // 4. 注册连接接受回调 tcp_accept(tcp_server_pcb, tcp_server_accept_callback); printf("TCP Server Initialized! Listen on %s:%d\r\n", inet_ntoa(ip_addr), TCP_SERVER_PORT); } int main(void) { /* 初始化HAL库、系统时钟、GPIO、ETH、USART1 */ HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); // PA5为LED(推挽输出) MX_ETH_Init(); // ETH初始化 MX_LWIP_Init(); // LWIP协议栈初始化 MX_USART1_UART_Init(); // 串口初始化 // 初始化LED为熄灭状态 HAL_GPIO_ResetPin(GPIOA, GPIO_PIN_5); printf("STM32H743 ETH TCP Server Test\r\n"); printf("IP Address: 192.168.1.100\r\n"); // 初始化TCP服务器 tcp_server_init(); while (1) { // LWIP主循环(必须调用,处理网络事件) MX_LWIP_Process(); HAL_Delay(10); } }2. 测试步骤
- 编译下载代码到STM32H743,确保STM32与PC连接到同一路由器(或直连);
- PC端设置IP为192.168.1.xxx(与STM32同网段,如192.168.1.101);
- 打开网络调试助手(如NetAssist),选择TCP客户端,输入STM32的IP(192.168.1.100)和端口(8080),点击连接;
- PC发送数据,STM32接收后回显数据,并点亮LED,串口打印接收的数据。
示例2:LWIP TCP客户端(主动连接PC服务器)
STM32作为TCP客户端,主动连接PC端的TCP服务器,定时发送数据:
/* TCP客户端参数 */ #define TCP_SERVER_IP "192.168.1.101" // PC的IP #define TCP_SERVER_PORT 8080 struct tcp_pcb *tcp_client_pcb; /** * @brief TCP连接成功回调函数 */ err_t tcp_client_connected_callback(void *arg, struct tcp_pcb *tpcb, err_t err) { if(err == ERR_OK) { printf("TCP Client Connected to %s:%d!\r\n", TCP_SERVER_IP, TCP_SERVER_PORT); // 定时发送数据(每隔1秒) while(1) { char send_buf[32]; sprintf(send_buf, "Hello TCP! Time: %lu", HAL_GetTick()); tcp_write(tpcb, send_buf, strlen(send_buf), TCP_WRITE_FLAG_COPY); tcp_output(tpcb); printf("Sent: %s\r\n", send_buf); HAL_Delay(1000); } } else { printf("TCP Client Connect Failed! Err: %d\r\n", err); Error_Handler(); } return ERR_OK; } /** * @brief 初始化TCP客户端 */ void tcp_client_init(void) { // 1. 创建TCP控制块 tcp_client_pcb = tcp_new(); if(tcp_client_pcb == NULL) { printf("TCP Client Create Failed!\r\n"); Error_Handler(); } // 2. 配置服务器IP和端口 ip_addr_t server_ip; IP4_ADDR(&server_ip, 192, 168, 1, 101); // PC的IP // 3. 发起连接 err_t err = tcp_connect(tcp_client_pcb, &server_ip, TCP_SERVER_PORT, tcp_client_connected_callback); if(err != ERR_OK) { printf("TCP Client Connect Request Failed! Err: %d\r\n", err); Error_Handler(); } } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ETH_Init(); MX_LWIP_Init(); MX_USART1_UART_Init(); printf("STM32H743 ETH TCP Client Test\r\n"); printf("Connect to %s:%d...\r\n", TCP_SERVER_IP, TCP_SERVER_PORT); // 初始化TCP客户端 tcp_client_init(); while (1) { MX_LWIP_Process(); HAL_Delay(10); } }示例3:LWIP UDP通信(无连接,简单高效)
UDP无需建立连接,适合小数据高频传输(如传感器数据):
/* UDP参数 */ #define UDP_LOCAL_PORT 8080 #define UDP_REMOTE_IP "192.168.1.101" #define UDP_REMOTE_PORT 8080 struct udp_pcb *udp_pcb; /** * @brief UDP接收回调函数 */ void udp_recv_callback(void *arg, struct udp_pcb *upcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) { if(p != NULL) { printf("UDP Received from %s:%d: ", inet_ntoa(*addr), port); for(uint16_t i=0; i<p->len; i++) { printf("%02X ", ((uint8_t*)p->payload)[i]); } printf("\r\n"); // 回显数据 udp_sendto(upcb, p, addr, port); pbuf_free(p); } } /** * @brief 初始化UDP */ void udp_init(void) { // 1. 创建UDP控制块 udp_pcb = udp_new(); if(udp_pcb == NULL) { printf("UDP Create Failed!\r\n"); Error_Handler(); } // 2. 绑定本地端口 ip_addr_t local_ip; IP4_ADDR(&local_ip, 192, 168, 1, 100); udp_bind(udp_pcb, &local_ip, UDP_LOCAL_PORT); // 3. 注册接收回调 udp_recv(udp_pcb, udp_recv_callback, NULL); printf("UDP Initialized! Local: %s:%d\r\n", inet_ntoa(local_ip), UDP_LOCAL_PORT); // 4. 定时发送数据 while(1) { char send_buf[32]; sprintf(send_buf, "Hello UDP! Time: %lu", HAL_GetTick()); ip_addr_t remote_ip; IP4_ADDR(&remote_ip, 192, 168, 1, 101); struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, strlen(send_buf), PBUF_RAM); memcpy(p->payload, send_buf, strlen(send_buf)); udp_sendto(udp_pcb, p, &remote_ip, UDP_REMOTE_PORT); pbuf_free(p); printf("UDP Sent: %s\r\n", send_buf); HAL_Delay(1000); } } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ETH_Init(); MX_LWIP_Init(); MX_USART1_UART_Init(); printf("STM32H743 ETH UDP Test\r\n"); udp_init(); while (1) { MX_LWIP_Process(); HAL_Delay(10); } }五、常见问题与避坑指南
1. ETH初始化失败(HAL_ETH_Init返回错误)
- 原因1:时钟配置错误:
ETH参考时钟(RMII)不是50MHz,或ETH_MAC时钟不是100MHz;
→ 解决:CubeMX中“Clock Configuration”确保PLL2Q=50MHz(RMII),ETH_MAC时钟=100MHz; - 原因2:PHY地址错误:
CubeMX中PHY Address设为1(LAN8720默认0,DP83848默认1),导致无法通信;
→ 解决:核对PHY芯片手册,设置正确的PHY地址; - 原因3:RMII引脚接线错误:
REF_CLK/RXD0/RXD1/TX_EN/TXD0/TXD1接反,尤其是REF_CLK(PA1);
→ 解决:逐根核对引脚,确保PHY的CLKIN接STM32的PA1(或PHY提供CLK给STM32); - 原因4:PHY芯片未供电:
LAN8720的3.3V供电未接,或电压不足;
→ 解决:确保PHY芯片供电稳定(3.3V/100mA以上)。
2. 能ping通但TCP/UDP无法通信
- 原因1:LWIP主循环未调用:
主循环中未执行MX_LWIP_Process(),LWIP无法处理网络事件;
→ 解决:主循环中高频调用(间隔≤10ms); - 原因2:端口未开放/被占用:
PC端防火墙阻止了目标端口(如8080),或STM32端口绑定失败;
→ 解决:关闭PC防火墙,更换未被占用的端口(如8081); - 原因3:IP地址不在同一网段:
STM32 IP=192.168.1.100,PC IP=192.168.2.101,网段不同;
→ 解决:将PC和STM32设为同一网段(如192.168.1.xxx); - 原因4:TCP回调函数未注册:
服务器未注册tcp_accept()/tcp_recv(),客户端未注册tcp_connect();
→ 解决:确保所有回调函数正确注册。
3. ping不通STM32(网络不可达)
- 原因1:网线未接/链路未通:
RJ45接口的Link灯未亮,PHY芯片未检测到链路;
→ 解决:检查网线是否插紧,更换网线/路由器端口; - 原因2:PHY芯片初始化失败:
自动协商失败,未正确设置10/100Mbps、全双工模式;
→ 解决:手动配置PHY速率(而非自动协商):
// 在MX_ETH_Init后添加 ETH_MACConfigTypeDef mac_config; HAL_ETH_GetMACConfig(&heth, &mac_config); mac_config.Speed = ETH_SPEED_100M; mac_config.DuplexMode = ETH_MODE_FULLDUPLEX; HAL_ETH_SetMACConfig(&heth, &mac_config);- 原因3:静态IP配置错误:
网关/子网掩码设置错误(如网关=192.168.1.0,应为192.168.1.1);
→ 解决:确保IP、子网掩码、网关与路由器一致。
4. 数据发送/接收乱码/丢包
- 原因1:缓冲区未对齐:
ETH DMA缓冲区未按32位对齐,导致数据传输错误;
→ 解决:用__attribute__((aligned(4)))修饰缓冲区; - 原因2:pbuf未释放:
接收数据后未调用pbuf_free(p),导致缓冲区耗尽;
→ 解决:每次接收完成后释放pbuf; - 原因3:网络速率不匹配:
STM32设为100Mbps,路由器设为10Mbps,协商失败;
→ 解决:开启自动协商,或两端统一速率; - 原因4:LWIP参数配置过小:
发送/接收缓冲区大小不足,导致大数据丢包;
→ 解决:CubeMX中LWIP配置增大缓冲区(如TCP_MSS=1460)。
5. ETH DMA中断不触发
- 原因1:NVIC中断未开启:
CubeMX中未勾选“ETH_RX_DMA_IRQn”/“ETH_TX_DMA_IRQn”;
→ 解决:NVIC Settings中启用ETH DMA中断,优先级设为High; - 原因2:DMA描述符配置错误:
收发描述符链表未正确初始化,导致DMA无法识别缓冲区;
→ 解决:使用CubeMX自动生成的描述符初始化代码,不手动修改; - 原因3:PHY芯片未使能中断:
LAN8720的INT引脚未接STM32,无法触发链路状态中断;
→ 解决:接PHY的INT引脚到STM32,或轮询链路状态。
六、调试顺序
1. 入门步骤
- 第一步:完成ETH初始化+ping通测试(验证硬件和IP配置);
- 第二步:实现TCP服务器/客户端,完成双向数据传输;
- 第三步:实现UDP通信,对比TCP(无连接)的差异;
- 第四步:拓展到HTTP服务器(网页控制LED)、MQTT物联网通信;
- 第五步:优化性能(如DMA缓存、大缓冲区),解决丢包/乱码问题。
2. 调试技巧
- Wireshark抓包:
在PC端用Wireshark抓取以太网数据包,分析帧结构、IP/TCP/UDP包,定位通信错误; - 串口日志调试:
在ETH初始化、LWIP启动、连接建立/断开、数据收发等节点打印日志; - PHY状态查询:
读取PHY芯片的状态寄存器(如LAN8720的0x01寄存器),查看链路状态、速率;
uint16_t phy_status = ETH_ReadPHYRegister(&heth, 0x01); printf("PHY Status: 0x%X\r\n", phy_status);- 分步测试:
- 先测试ping通,再测试TCP;
- 先测试小数据(1字节),再测试大数据(1KB)。
3. 重点注意事项
- 时钟精准性:RMII参考时钟必须为50MHz(误差≤0.1%),否则PHY无法同步;
- 硬件隔离:以太网接口必须接隔离变压器,避免静电/浪涌损坏PHY/STM32;
- LWIP主循环:
MX_LWIP_Process()必须高频调用,否则网络事件无法处理; - 资源释放:pbuf、TCP/UDP控制块使用后必须释放,避免内存泄漏;
- 兼容性:不同路由器/PHY芯片的协商机制不同,优先用自动协商。
4. 实际应用练习
- 远程控制:通过TCP/IP控制STM32的LED、电机,PC端发送指令(如“LED_ON”);
- 数据采集:STM32采集传感器数据(温湿度、ADC),通过MQTT上传到阿里云/腾讯云;
- HTTP服务器:STM32搭建简易HTTP服务器,PC浏览器访问IP地址,显示传感器数据;
- 网络升级:通过TCP接收固件bin文件,写入Flash,实现远程固件升级(FOTA)。