STM32H743+LAN8720+LWIP以太网调通实战:从CubeMX到Ping通全记录

📅 发布时间:2026/9/24 12:57:26
STM32H743+LAN8720+LWIP以太网调通实战:从CubeMX到Ping通全记录
搞嵌入式网络通信的兄弟应该都体会过那种代码编译全过、下载零报错、上电就是不通的绝望。尤其是 STM32H743 这颗芯片性能是够猛但你要是拿它配 LAN8720 做以太网踩坑的概率一点不比老平台低。最近正好帮一个项目调通了 H743 LAN8720 LWIP 的环境从 CubeMX 生成代码到最终 Ping 通中间踩了不少坑也改了几处关键代码。这篇文章就把整个配置过程、踩过的坑和最终的代码修改一次性说清楚希望能帮你少走弯路。别看 CubeMX 能帮你生成一堆初始化代码它生成的东西只能保证能编译离能跑通还差着十万八千里。尤其是 H7 系列牵扯到缓存一致性、DMA 描述符对齐、PHY 时钟配置这些东西任何一个环节没搞对结果都是网络不通。这篇文章适合两类人一是刚接触 STM32 以太网、想快速跑通 LWIP 的初学者二是在 H7 LAN8720 上折腾了好几天还没 Ping 通的准资深玩家。1. 硬件连接与原理图检查9 个引脚能折腾出多少事先把硬件底子打好。LAN8720 是 RMII 接口的百兆 PHY 芯片和 H743 连接只需要 9 根线比 MII 接口动辄 16 根线清爽多了。但连线越少对时序和时钟的要求反而越苛刻。1.1 RMII 接口的标准接法废话不多说直接上标准连接表STM32H743 引脚LAN8720 引脚说明PA1ETH_RMII_REF_CLK50MHz 参考时钟PA2MDIO管理接口数据线PC1MDC管理接口时钟线PA7ETH_RMII_CRS_DV载波侦听/数据有效PC4ETH_RMII_RXD0接收数据位 0PC5ETH_RMII_RXD1接收数据位 1PB11ETH_RMII_TX_EN发送使能PB12ETH_RMII_TXD0发送数据位 0PB13ETH_RMII_TXD1发送数据位 1这里有个特别容易搞错的点RMII 接口的 REF_CLK 必须是 50MHz而不是 25MHz。很多人第一次接触 RMII 想当然地以为和 MII 一样用 25MHz结果 PHY 芯片压根不工作。LAN8720 芯片本身可以自己产生 50MHz 时钟也可以由外部提供但更推荐用外部有源晶振或者 MCU 的 MCO 引脚输出。1.2 时钟来源是第一个分水岭LAN8720 的时钟配置有两种模式一种是把外部 50MHz 有源晶振直接接到 PHY 的 XI 引脚另一种是让 PHY 自己从 25MHz 晶振倍频到 50MHz然后把 50MHz 时钟从 CLKOUT 引脚输出给 MCU。两种模式由芯片的 REF_CLK_IN 引脚即第 14 脚的电平决定。REF_CLK_IN 拉低PHY 工作在时钟输出模式CLKOUT 引脚输出 50MHz 给 MCUREF_CLK_IN 拉高PHY 工作在时钟输入模式由外部直接提供 50MHz我当时选的是让 PHY 自己倍频输出也就是 REF_CLK_IN 拉低。这样 MCU 的 ETH_RMII_REF_CLK 引脚直接接 LAN8720 的 CLKOUT 就行省一个有源晶振。但要注意这种模式下 LAN8720 的 XI 引脚需要接 25MHz 无源晶振两边的电容按芯片手册推荐值来一般是 18pF 到 22pF。1.3 复位电路和地址引脚别忽略很多人的板子网络不通问题不在软件而在硬件复位电路。LAN8720 的 nRST 引脚是低电平有效复位复位脉冲至少要保持 100us 以上。如果 RC 复位电路的电容选得太大复位时间过长上电后 PHY 初始化时序就会乱。PHY 地址引脚也要重点检查。LAN8720 的 PHYAD0 引脚内部有下拉电阻默认地址是 0。STM32 HAL 库默认访问的 PHY 地址是 0如果你的硬件设计把 PHYAD0 拉高了地址就变成了 1那 HAL_ETH_Init 里面的 PHY 通信直接失败表现出来就是 HAL_ETH_Init 返回 HAL_ERROR。检查这个最快的方法是用示波器量 MDIO 引脚看看有没有时钟和数据波形。要是 MDC 有波形、MDIO 没反应十有八九是 PHY 地址不对。2. CubeMX 配置全流程细节决定能否生成出可用的工程CubeMX 版本不同界面细节会有些差异但核心配置项是一样的。我用的是 6.x 版本H743 的 HAL 库版本是 1.11 左右。2.1 RCC 和 SYS 的基础配置时钟树配置是整个项目的地基。我用的是外部 25MHz 晶振通过 PLL 倍频到 480MHz。注意 H743 的以太网 MAC 时钟来自 APB3 或 APB4这里要确保 ETH 的时钟源选择正确。在 Clock Configuration 页面找 ETH 相关的时钟来源H743 的 ETH 时钟可以选择 PLL1Q、PLL2P 等具体要看你的时钟树怎么配。官方推荐的是给 ETH 提供 50MHz 的时钟但 RMII 的 REF_CLK 实际上是从外部进来的所以 MCU 内部 ETH 的时钟源其实需要根据你的硬件设计来定。SYS 配置里面Debug 建议选 Serial Wire不然有些板子下完程序后第二次就无法连接调试器了。Timebase Source 选 SysTick 即可不要和 FreeRTOS 的时基冲突。2.2 ETH 外设的界面配置在 Connectivity 菜单下打开 ETH接口选择 RMII。注意H743 的 RMII 模式需要手动配置引脚CubeMX 会自动把 PA1、PA2、PA7 等引脚分配给 ETH但你要检查一下特别是 PA1 这个 REF_CLK 引脚有时候会被别的外设抢占。PHY Address 填 0如果硬件上改了地址也要同步修改。MAC Address 随便填一个就行只要不是全 0比如 02:00:00:00:00:01 这种本地管理的 MAC 地址就不会冲突。2.3 LWIP 中间件配置Middleware 里面打开 LWIP协议栈版本选 2.1.2 或者 2.1.3别选 2.0.3新版本的稳定性好不少。配置项里我一般这样设置MEM_SIZE1600 以上默认值一般够用MEMP_NUM_PBUF16MEMP_NUM_UDP_PCB8MEMP_NUM_TCP_PCB8MEMP_NUM_TCP_SEG16PBUF_POOL_SIZE16PBUF_POOL_BUFSIZE1600TCP_WND4096 左右别开太大H743 的内存虽然多但 DMA 描述符和收发缓冲区也要占空间TCP_SND_BUF4096LWIP_DHCP打开方便调试但如果你想固定 IP也可以关掉 DHCP 手动指定在 Key Options 里面注意把 LWIP_HTTPD、LWIP_SNMP 这些用不到的功能全部关掉能省不少内存也能减少一些隐蔽的编译错误。2.4 生成代码前的检查清单生成代码前要像飞机起飞前检查一样过一遍这些配置项Project Manager 里的 Toolchain 选对MDK-ARM 就选 MDK-ARM别选成 STM32CubeIDE最小堆栈大小默认就行但如果你后续要跑协议栈和任务调度建议把堆调到 0x1000 以上确认勾选了 Generate peripheral initialization as a pair of .c/.h files per peripheral方便维护3. 编译通过后必改的代码改错位置等于白改CubeMX 生成的代码默认情况下编译是能过的但几乎不可能直接 Ping 通。原因在于 H7 系列和 F1/F4 有本质区别其中最大的坑就是缓存一致性。3.1 开启 DCache 后的 DMA 描述符问题H743 默认开启了 D-Cache而 DMA 描述符和收发缓冲区都存放在普通 RAM 区域。CPU 写数据到 DMA 描述符后会留在 Cache 里DMA 控制器去读的时候读到的还是旧数据结果就是以太网 MAC 根本没法正常工作。解决办法有两个一是把 DMA 描述符放到特定的不缓存内存区域二是在每次 DMA 操作前后做 Cache 维护操作。第二个方法太繁琐而且容易漏我推荐第一种。在链接脚本.icf 或 .sct 文件里增加一段不缓存区域然后把 ETH DMA 描述符定义到这段区域里面。以 MDK 的分散加载文件为例LR_IROM1 0x08000000 0x00200000 { ER_IROM1 0x08000000 0x00200000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) .ANY (XO) } RW_IRAM1 0x20000000 0x00020000 { .ANY (RW ZI) } RW_IRAM_NOCACHE 0x20020000 0x00020000 { *(EthBuffers) } }分散加载文件不同编译器写法差异很大如果你用的是 AC6注意语法要匹配新版编译器。改完之后把以太网的 DMA 描述符和缓冲区数组都用__attribute__((section(EthBuffers)))修饰__attribute__((section(EthBuffers))) ETH_DMADescTypeDef hdma_tx_desc[ETH_TX_DESC_CNT]; __attribute__((section(EthBuffers))) ETH_DMADescTypeDef hdma_rx_desc[ETH_RX_DESC_CNT]; __attribute__((section(EthBuffers))) uint8_t tx_buff[ETH_TX_DESC_CNT][ETH_TX_BUFFER_SIZE]; __attribute__((section(EthBuffers))) uint8_t rx_buff[ETH_RX_DESC_CNT][ETH_RX_BUFFER_SIZE];这套操作做完能解决 80% 的H743 网口不通问题。3.2 修改 lwipopts.h 的两个关键宏CubeMX 生成的 lwipopts.h 里有几个宏需要手动调整不然 LWIP 协议栈跑不起来。首先是NO_SYS。如果你没用 RTOS这个宏要设为 1表示 LWIP 运行在裸机模式下。如果你用了 FreeRTOS 但没开启LWIP_TIMERS相关的配置也会出问题。其次是MEM_ALIGNMENT默认是 1。对于 32 位 MCU建议改成 4。因为 ETH DMA 要求缓冲区四字节对齐如果MEM_ALIGNMENT是 1LWIP 内部申请的 PBUF 可能不对齐送进 DMA 描述符后数据就错位了。#define MEM_ALIGNMENT 43.3 在 lwip.c 里加入网卡初始化函数生成的 lwip.c 里有一个MX_LWIP_Init函数但这个函数默认情况下只负责协议栈初始化不会主动把网卡加入协议栈。你要在MX_LWIP_Init里手动添加netif_add和netif_set_default等操作。看程序是 Cortex-M7 内核日常工作环境是 STM32H743 搭配 LAN8720 跑以太网这些代码都是熟面孔了。H7 的以太网 MAC 内核是 Synopsys 的 DesignWare 系列和 F4 的 MAC 驱动模型有很大差异特别是 DMA 描述符的管理方式用的是增强型描述符每个描述符占用 16 字节比 F4 的旧版描述符多了不少字段。初始化网卡的关键代码逻辑如下static void ethernet_link_thread(void const *arg) { struct netif *netif (struct netif *)arg; while (1) { ethernetif_set_link(netif); ethernetif_update_config(netif); osDelay(1000); } }这个线程的作用是周期性地检查 PHY 的链路状态。LAN8720 的链路状态寄存器在地址 0x1F基本状态寄存器bit 2 表示链路是否建立。如果一直是 down 状态就要回头查硬件了。3.4 注意 ETH 驱动中两个隐藏的系统调用H7 的 HAL 以太网库在HAL_ETH_TransmitFrame和HAL_ETH_ReceiveFrame里面有对 DMA 描述符的状态轮询。默认情况下这两个函数是阻塞式等待的如果长时间没收到数据会一直卡在 while 循环里。这就需要一个超时机制。我在实际项目里修改了以太网驱动在轮询循环里增加一个计数判断超过一定次数直接返回超时错误避免在 LWIP 的轮询机制里造成死循环。这种感觉就像是给看门狗喂狗一样虽然简单但关键时刻能救系统一命。4. Ping 不通的完整排查链路从寄存器到波形逐步定位如果上面的配置都做了还 Ping 不通那就要进入正儿八经的调试环节了。别急着烦躁按照这个链路一步步排查保证能定位到问题。4.1 第一步确认 PHY 芯片是否活了上电后先别急着跑 LWIP先用一个最简单的程序读 LAN8720 的 ID 寄存器。LAN8720 的 PHY ID 寄存器是地址 2 和地址 3读出来的值应该是 0x0007 和 0xC0F1。用 HAL 库的 MDIO 接口读uint32_t phy_id_high 0, phy_id_low 0; HAL_ETH_ReadPHYRegister(heth, PHY_ADDRESS, PHY_REG_ID_HIGH, phy_id_high); HAL_ETH_ReadPHYRegister(heth, PHY_ADDRESS, PHY_REG_ID_LOW, phy_id_low); printf(PHY ID: 0x%04X 0x%04X\r\n, phy_id_high, phy_id_low);如果读出来是 0xFFFF说明 MDIO 通信就没建立起来。这时候检查三样东西PHY 地址是否匹配寄存器地址里 bit 5~bit 8 是 PHY 地址MDC 时钟是否正常MDC 的频率不能太高STM32 的 MDC 时钟要低于 2.5MHzPHY 供电是否到位特别是 1.2V 的内核电压LAN8720 需要 3.3V 和 1.2V 两组电源如果 MDIO 读到了正确的 ID说明 PHY 的寄存器访问没问题问题出在后面。4.2 第二步检查 RMII 时钟波形这一步需要一台示波器最好带宽在 100MHz 以上。把探头点到 STM32 的 ETH_RMII_REF_CLK 引脚上看有没有 50MHz 的方波输出。如果一点波形都没有问题大概率在时钟配置上。一个隐蔽的坑是如果你用的是 PHY 输出 50MHz 时钟给 MCU 的模式那 MCU 的 ETH_RMII_REF_CLK 引脚要配置成输入模式而不是输出模式。CubeMX 在生成代码的时候会把所有 ETH 引脚都默认配置成复用功能但 PA1 这个引脚如果作为 RMII 参考时钟的输入要检查它的 GPIO 模式是不是被正确设置成了GPIO_MODE_AF_PP且没有上拉下拉。时钟波形有了继续检查 CRS_DV、RXD0、RXD1 这三个接收引脚的数据。在网线插入的时候如果没有数据传输这些引脚应该是低电平。如果 CRS_DV 有电平跳动但 MAC 仍收不到数据检查一下这几个引脚的复用功能是否正确映射到了 ETH 外设上。4.3 第三步检查 DMA 描述符状态如果寄存器通信正常、时钟也正常那问题就出在 DMA 描述符的管理上。在调试器里查看heth.RxDesc指向的第一个描述符里面有个Status字段bit 31 表示描述符是否被 DMA 占用。如果一直没被 DMA 回写说明 RX 路径就没跑起来。H7 的 DMA 描述符回写有一个很坑的行为如果一个描述符在收到数据后没有及时被 CPU 处理后续的数据包就会被丢弃。LWIP 的裸机轮询模式是在ethernetif_input里不断调用HAL_ETH_ReceiveFrame来回收描述符。如果这个函数在中断里被调用要保证处理速度足够快不然后面来的包就会被丢掉表现出来就是 Ping 丢包严重甚至完全不通。4.4 第四步重点排查 Cache 问题我遇到过一种情况所有寄存器都正常、描述符状态也一直在变化但收到的数据内容全是乱码。最后定位到是 D-Cache 没关闭DMA 直接往内存写的 Rx Buffer 数据被 Cache 吃掉了CPU 读到的还是老数据。处理方式除了前面说的把缓冲区放到 Non-Cacheable 区域还有一个临时验证手段在初始化 ETH 之前直接调用SCB_DisableDCache()禁用 D-Cache来看问题是不是真的出在缓存上。如果禁了缓存之后 Ping 通了那就是缓存一致性的问题老老实实按 3.1 说的方法去改链接脚本别想着偷懒。4.5 第五步检查 LWIP 协议栈的 netif 状态上面的步骤都排除之后就要看看协议栈层面的状态。在调试器里查看netif结构体重点看几个字段flags是否包含NETIF_FLAG_UP和NETIF_FLAG_LINK_UPip_addr是否赋值成功mtu默认 1500正常如果NETIF_FLAG_LINK_UP没有置位说明链路层状态没建立。这时候要在ethernetif_set_link里检查 PHY 状态寄存器的读取逻辑。LAN8720 的 Basic Mode Status Register地址 0x1F的 bit 2 是链路状态位如果读出来是 0就是物理链路没起来。常见原因有网线是交叉线但设备不支持自动翻转、对端设备没启动、LAN8720 的差分信号线走线太长或者阻抗不匹配。4.6 一个容易被忽略的坑MAC 地址全零LWIP 启动的时候会对 MAC 地址做合法性检查如果六个字节全是 0netif_add会返回 NULL。这个问题在调试的时候特别迷惑人因为编译不报错看起来初始化代码都执行了但 Ping 的时候就是没有响应。CubeMX 默认生成的 MAC 地址是00:00:00:00:00:00你需要在MX_LWIP_Init里显式赋值比如netif_add(gnetif, ipaddr, netmask, gw, NULL, ethernetif_init, ethernet_input);在ethernetif_init的底层会从heth.Init.MACAddr里读 MAC 地址。CubeMX 生成的代码里heth.Init.MACAddr是硬编码的六个 0要改成有效的 MACheth.Init.MACAddr[0] 0x02; heth.Init.MACAddr[1] 0x00; heth.Init.MACAddr[2] 0x00; heth.Init.MACAddr[3] 0x00; heth.Init.MACAddr[4] 0x00; heth.Init.MACAddr[5] 0x01;这个坑在 F4 系列上也可能遇到但在 H7 上更隐蔽因为 HAL 库对 MAC 地址的处理逻辑有些差异你感觉初始化都做了实际上 netif 压根没起来。说句题外话前一阵子帮人排查一个 ESP32 接 LAN8720 的问题遇到的故障表现和 STM32 上一模一样能读到 PHY ID、有 50MHz 时钟、但就是不通。最后发现是 LAN8720 的 nINT 引脚没做上拉处理导致 PHY 中断一直误触发把主控的中断资源吃光了。这个教训说明不管是什么主控平台PHY 的中断引脚处理不能省能不用中断就不要用中断链路状态轮询已经足够满足绝大多数场景。5. 最终收尾Ping 通之后的性能调优等你能稳定 Ping 通之后事情还没完。嵌入式网络设备好不好用还得看收发数据的效率和稳定性。5.1 调整 LWIP 的收发缓冲区大小LAN8720 是百兆 PHY理论吞吐率能跑到 90Mbps 以上。LWIP 默认的 PBUF 池数量偏少跑大流量测试的时候会频繁出现内存不足导致丢包。我一般习惯把 PBUF_POOL_SIZE 调到 24 到 32PBUF_POOL_BUFSIZE 保持 1600 不变。TCP_SND_BUF 和 TCP_WND 可以调到 8K 甚至 16K但要注意总内存占用H743 虽然 RAM 有 1MB但还有别的应用在用。5.2 开启 Ethernet DMA 中断裸机轮询模式下HAL_ETH_ReceiveFrame是靠主循环不断调用ethernetif_input来获取数据的。如果主循环里的任务耗时太长网络数据包处理就会延迟Ping 延迟就会飙升。更稳的做法是开启以太网 DMA 接收中断在中断里调用HAL_ETH_ReadData把数据搬出来然后标记给 LWIP 处理。CubeMX 生成的代码默认没有开启 ETH 中断需要手动在HAL_ETH_MspInit里配置中断优先级和使能还要在中断服务函数里调用HAL_ETH_IRQHandler。5.3 DHCP 还是静态 IP调试阶段用 DHCP 方便但产品交付阶段我更建议用静态 IP。DHCP 依赖路由器如果路由器没开 DHCP 服务设备就永远拿不到 IP表现为 Ping 不通。静态 IP 的配置方式是在MX_LWIP_Init里把 IP 地址、子网掩码、网关地址换成你需要的值然后注释掉 DHCP 相关的代码。一个我在实际项目里常用的技巧把静态 IP 和 DHCP 做成编译开关#define USE_DHCP 0 #if USE_DHCP ip4_addr_set_zero(ipaddr); ip4_addr_set_zero(netmask); ip4_addr_set_zero(gw); #else IP4_ADDR(ipaddr, 192, 168, 1, 100); IP4_ADDR(netmask, 255, 255, 255, 0); IP4_ADDR(gw, 192, 168, 1, 1); #endif这个开关在做联调的时候特别有用现场不方便接路由器就切静态 IP需要验证 DHCP 功能就切回来。5.4 性能测试时的一个观察按这套配置跑下来我用 iperf 测过 TCP 吞吐稳定在 40~60Mbps 左右这个数据对大多数嵌入式应用来说完全够用。如果你需要更高的吞吐率可以考虑开启 LWIP 的零拷贝功能或者把 RX 描述符的数量增加到 12 个甚至 16 个但复杂度会上升不少。我个人的建议是稳定压倒一切别盲目追求极限性能尤其是用 LWIP 这种通用协议栈的场合达到业务需求就足够了。6. 从这次调通中总结的几个经验最后分享几个在这次调通过程中比较深刻的心得都是拿时间和头发换来的。第一做硬件验证比软件调试更重要。凡是网络不通先确认 PHY 的 ID 寄存器能读出来再谈协议栈。MDIO 都通不了软件再怎么改都是白搭。手边常备逻辑分析仪或者示波器抓一下 MDC/MDIO 波形比盲改代码高效十倍。第二H7 系列的缓存问题一定要在设计阶段就想好。很多人都是跑通了 F4 的代码直接平移到 H7 上发现不通然后一脸懵。H7 的 D-Cache 和 I-Cache 默认是开启的以太网这种 DMA 外设必须处理缓存一致性问题。做好了这一步后面能省出好几个通宵。第三不要在调试阶段把自己困在 LWIP 里出不来。如果 Ping 不通先在裸机环境下测试 MAC 层的回环功能。STM32 的以太网 MAC 自带回环模式可以把发送的数据直接回收到接收路径。先用一个裸机程序发送一个固定的 UDP 包然后在接收中断里看能不能收到这样就把问题隔离在了 MAC 层以内还是以外。第四资料要看官方手册别只看网上教程。网上很多教程是 F4 时代的拿到 H7 上压根不适用因为 HAL 库版本和寄存器结构已经大变样了。推荐直接看参考手册里 Ethernet 章节的 DMA 描述符部分再配合 HAL 库源码理解起来反而更快。说到底STM32H743 的网络调通并没有那么玄乎只要按照硬件时钟、PHY 通信、DMA 缓存、协议栈状态这个顺序逐层排查问题总能定位到。希望这篇实战记录能帮你少踩几个坑顺利把板子上的网络跑起来。