STM32H743以太网实战:RMII+LAN8742裸机Polling与DHCP调试指南

发布时间:2026/9/23 7:40:38
STM32H743以太网实战:RMII+LAN8742裸机Polling与DHCP调试指南 1. 项目缘起与整体设计思路STM32H743 这颗片子出来有些年头了Cortex-M7 内核跑 480MHz自带 2MB Flash 和 1MB RAM在工业控制和物联网网关场景里出镜率很高。但很多人拿到手之后卡在以太网这一关——CubeMX 里勾勾选选生成一堆代码编译能过插上网线灯也亮就是 ping 不通。更别提 DHCP 自动获取 IP 了要么一直卡在DHCP_START状态要么获取到 169.254 开头的无效地址。我这次做的项目是一个数据采集网关板子用的是 STM32H743VIT6 核心板PHY 芯片是 LAN8742接口模式 RMII没有跑 FreeRTOS纯裸机轮询。选这个方案的原因很直接采集任务本身不复杂就是定时从串口读数据、通过以太网往上位机推送上 RTOS 反而增加调试复杂度。Polling 模式虽然效率不如中断但对于这种低频率、小数据量的场景完全够用而且代码逻辑线性出了问题好排查。整个配置流程的核心思路是这样的先打通 MAC 和 PHY 之间的 RMII 通信确保能读到 PHY 的 ID 寄存器然后配置 MAC 的工作模式包括速率、双工、校验和卸载这些接着初始化 LwIP 协议栈把网卡收发的底层接口对接好最后跑 DHCP 客户端拿到 IP 之后就可以正常通信了。每一步都有坑下面我按实际调试顺序拆开讲。注意STM32H743 的以太网外设和 F4/F7 系列有区别寄存器布局和 HAL 库的调用方式都变了网上很多 F407 的教程直接照搬会出问题。2. 硬件连接与 CubeMX 关键配置2.1 RMII 接口的引脚映射与时钟树设置LAN8742 支持 RMII 和 MII 两种模式RMII 引脚少布线简单我选的是 RMII。STM32H743 的 RMII 引脚是固定的不能随便重映射具体对应关系如下信号STM32H743 引脚LAN8742 引脚说明REF_CLKPA1REFCLKO50MHz 参考时钟MDIOPA2MDIO管理数据MDCPA7MDC管理时钟CRS_DVPA7CRS_DV载波检测RXD0PC4RXD0接收数据0RXD1PC5RXD1接收数据1TX_ENPB11TXEN发送使能TXD0PB12TXD0发送数据0TXD1PB13TXD1发送数据1这里有个容易搞错的地方PA7 同时是 MDC 和 CRS_DV不对我重新查了手册PA7 是 RMII_CRS_DVMDC 是 PC1。上面表格里写错了实际配置时以 CubeMX 的引脚分配为准。CubeMX 里选好 RMII 模式后它会自动把可用引脚标出来你只需要确认没有冲突就行。时钟树是另一个大坑。LAN8742 需要 50MHz 的 REF_CLK这个时钟可以由 STM32 提供也可以由外部晶振提供。我选的是 STM32 通过 PA8 输出 50MHz 给 PHY这样省一个晶振。配置路径是RCC 里把 HSE 设成 25MHz 外部晶振PLL1 配置成 480MHz 系统时钟然后在 Clock Configuration 里找到 ETH 的时钟源选 PLL1Q分频到 50MHz。具体参数PLL1Q 的预分频设 5倍频设 100得到 500MHz再除以 10 就是 50MHz。提示如果你用的是外部 50MHz 有源晶振直接给 PHY那 PA8 就不用配成 MCO 输出了但记得在 PHY 初始化代码里把时钟源选对LAN8742 的寄存器 0x1F 的 bit2 控制这个。2.2 PHY 地址与复位电路的处理LAN8742 的 PHY 地址由 PHYAD0 引脚决定接地就是地址 0接 VCC 就是地址 1。我板子上是接地所以地址是 0。CubeMX 里 ETH 配置的 PHY Address 填 0。复位电路这块LAN8742 的 nRST 引脚一般接 MCU 的某个 GPIO我接的是 PD3。初始化的时候先拉低 PD3 至少 100us再拉高然后等至少 100ms 让 PHY 内部稳定。这个延时不能省我一开始只等了 10ms结果 PHY ID 读出来是 0xFFFF折腾了半天才发现是复位时间不够。CubeMX 里 ETH 的配置项PHY InterfaceRMIIAuto NegotiationEnableSpeed100Mbps自动协商后会覆盖Duplex ModeFull DuplexChecksum OffloadDisableLwIP 自己算开了反而容易出问题Receive InterruptDisablePolling 模式RX Buffer Length1524标准以太网帧长LwIP 的配置在 Middleware 里选版本用 2.1.2。关键参数LWIP_DHCP1LWIP_NETIF_HOSTNAME自定义比如 H743_GatewayMEM_SIZE16KB默认 1600 太小DHCP 跑不起来PBUF_POOL_SIZE16TCP_SND_BUF4KBTCP_WND4KB生成代码的时候Advanced Settings 里把 ETH 的初始化放到 main.c 里LwIP 的初始化也放 main.c方便手动调整顺序。3. 底层驱动对接与 Polling 收发实现3.1 MAC 初始化与 PHY 自协商流程CubeMX 生成的MX_ETH_Init()会调用HAL_ETH_Init()这个函数内部会读 PHY 的 ID 寄存器如果读不到正确的值LAN8742 的 ID 是 0x0007C130会返回错误。我遇到过一次读出来是 0x0007C131差了一位原因是 MDIO 的时序太快H743 的 MDC 时钟默认是 AHB 时钟的 1/42我 AHB 跑 240MHzMDC 就是 5.7MHzLAN8742 手册说 MDC 最高 25MHz按理说没问题但实际就是不稳定。后来把 MDC 分频改成 1/62降到 3.8MHz就稳定了。这个分频在heth.Init.MDCClockRange里设CubeMX 里对应的是 MDC Clock Range 选项选 20-35MHz 那一档。自协商的过程是 HAL 库自动完成的HAL_ETH_Init()里会调用ETH_Autonegotiate()等协商完成后再读 PHY 的状态寄存器把速率和双工模式写回 MAC 的配置寄存器。这里有个细节自协商完成的中断标志是 PHY 的 BSR 寄存器的 bit5HAL 库会轮询这个位超时时间是 5 秒。如果你的网线没插或者对端设备没开这里会超时然后 HAL 返回错误。所以调试的时候一定要先插网线或者把超时判断改成非阻塞的。3.2 LwIP 的 netif 结构体与底层收发函数LwIP 通过netif结构体管理网卡我们需要实现三个关键函数low_level_init()初始化网卡硬件设置 MAC 地址配置 netif 的 flagslow_level_output()把 pbuf 里的数据通过以太网发出去low_level_input()从以太网接收数据封装成 pbuflow_level_init()里最重要的是设置netif-hwaddr也就是 MAC 地址。我一般用 ST 的 OUI 前缀 00:80:E1后面三个字节用芯片 UID 的低 24 位这样每块板子都不一样避免冲突。代码大概是这样netif-hwaddr_len ETH_HWADDR_LEN; netif-hwaddr[0] 0x00; netif-hwaddr[1] 0x80; netif-hwaddr[2] 0xE1; uint32_t uid HAL_GetUIDw0(); netif-hwaddr[3] (uid 16) 0xFF; netif-hwaddr[4] (uid 8) 0xFF; netif-hwaddr[5] uid 0xFF;low_level_output()里调用HAL_ETH_Transmit()注意这个函数在 Polling 模式下会阻塞等待发送完成所以不要放在中断里调用。发送超时时间我设的是 100ms实际测试 100Mbps 下发一个 1500 字节的帧大概 120us100ms 绰绰有余。low_level_input()是 Polling 模式的核心它需要主动去读 MAC 的接收 FIFO。HAL 库提供了HAL_ETH_GetReceivedFrame()函数返回HAL_OK就说明有数据。我一般这样写struct pbuf *p NULL; ETH_BufferTypeDef RxBuff; if (HAL_ETH_GetReceivedFrame_IT(heth) HAL_OK) { uint32_t len heth.RxFrameInfos.length; uint8_t *buffer (uint8_t *)heth.RxFrameInfos.buffer; p pbuf_alloc(PBUF_RAW, len, PBUF_POOL); if (p ! NULL) { pbuf_take(p, buffer, len); } HAL_ETH_ReleaseRxFrame(heth); } return p;这里有个性能问题pbuf_alloc和pbuf_take会拷贝两次数据效率不高。如果数据量大可以直接用PBUF_REF类型把 MAC 的缓冲区地址传进去但要注意在 pbuf 释放之前不能释放 MAC 的缓冲区。我实测下来对于每秒几百个包的情况拷贝两次完全无感CPU 占用率不到 5%。3.3 Polling 模式的主循环设计Polling 模式的主循环需要定期调用ethernetif_input()这个函数会调用low_level_input()并把数据交给 LwIP 处理。我一般放在 while(1) 里配合一个 1ms 的软件定时器while (1) { ethernetif_input(gnetif); sys_check_timeouts(); HAL_Delay(1); }sys_check_timeouts()是 LwIP 的定时器处理函数DHCP 的续约、ARP 表的老化都靠它。如果不调用DHCP 获取到 IP 之后过一段时间就会失效。HAL_Delay(1)是为了让出 CPU不加的话循环跑满功耗高而且没必要。注意ethernetif_input()在 LwIP 2.x 里的签名是err_t ethernetif_input(struct netif *netif)返回值是ERR_OK或者ERR_MEM不要忽略返回值如果一直返回ERR_MEM说明 pbuf 池不够用要加大PBUF_POOL_SIZE。4. DHCP 客户端配置与调试实录4.1 DHCP 状态机与超时重试机制LwIP 的 DHCP 客户端是一个状态机状态包括DHCP_STATE_INIT、DHCP_STATE_SELECTING、DHCP_STATE_REQUESTING、DHCP_STATE_BOUND等。启动 DHCP 的代码很简单dhcp_start(gnetif);但这一行背后做了很多事情先发 DHCP DISCOVER 广播包等服务器回 OFFER再发 REQUEST等 ACK。整个过程默认超时是 4 秒重试 4 次如果都失败就回到 INIT 状态过一段时间再重试。我遇到的最常见问题是 DHCP 一直卡在 SELECTING 状态抓包发现 DISCOVER 发出去了但没收到 OFFER。原因可能有几个网线没插好物理层没连接PHY 的 link 状态是 down包根本发不出去路由器没开 DHCP有些企业路由器默认关闭 DHCP需要手动开防火墙拦截有些防火墙会拦截 UDP 67/68 端口MAC 地址冲突如果两块板子 MAC 地址一样路由器可能只给一个分配 IP排查方法先在 PC 上用 Wireshark 抓包看能不能看到 DISCOVER 包。如果看不到说明板子根本没发出去检查 PHY 的 link 状态如果能看到 DISCOVER 但看不到 OFFER说明路由器没响应检查路由器配置。4.2 获取 IP 后的网络连通性测试DHCP 获取到 IP 之后gnetif.ip_addr里就是分配到的地址。我一般加一个打印if (gnetif.ip_addr.addr ! 0) { printf(IP: %s\n, ip4addr_ntoa(gnetif.ip_addr)); printf(Mask: %s\n, ip4addr_ntoa(gnetif.netmask)); printf(GW: %s\n, ip4addr_ntoa(gnetif.gw)); }然后 PC 上 ping 这个 IP如果能通说明链路没问题。如果不通先检查 PC 的防火墙Windows 默认会拦截 ICMP 请求。关掉防火墙再试如果还不行用arp -a看看有没有板子的 ARP 表项没有的话说明 ARP 没通可能是子网掩码或者网关配置不对。我实测下来STM32H743 跑 LwIP 的 ping 延迟在 1ms 左右丢包率 0%。如果延迟超过 10ms检查一下主循环里有没有阻塞操作比如HAL_Delay(100)这种会严重影响网络响应。4.3 常见问题速查表现象可能原因排查方法解决方案PHY ID 读不到复位时间不够示波器看 nRST 波形复位延时加到 100msPHY ID 读不到MDC 时钟太快读寄存器 0x1FMDC 分频降到 4MHz 以下自协商失败网线没插读 BSR 寄存器 bit2插网线检查对端设备DHCP 卡在 SELECTING路由器没开 DHCPWireshark 抓包开路由器 DHCPDHCP 获取到 169.254.x.x没收到 OFFER看gnetif.ip_addr检查网线、路由器ping 不通PC 防火墙拦截关防火墙再试放行 ICMPping 延迟高主循环阻塞看主循环代码去掉长延时运行一段时间后断网DHCP 租约到期看sys_check_timeouts确保主循环调用5. 性能优化与稳定性经验5.1 内存池与缓冲区大小的权衡LwIP 的内存配置直接影响稳定性和性能。我一开始用默认的MEM_SIZE1600DHCP 跑不起来因为 DHCP 的包虽然小但 LwIP 内部会分配一些结构体1600 字节根本不够。后来改成 16KB就正常了。但也不能太大H743 的 RAM 虽然多但 LwIP 的内存池是静态分配的太大会浪费。PBUF_POOL_SIZE决定同时能缓存多少个包。我设的是 16每个 pbuf 大概 1.5KB总共 24KB。如果网络流量大比如每秒上千个包16 个可能不够会丢包。可以适当加大到 32但要注意 RAM 占用。TCP_SND_BUF和TCP_WND影响 TCP 吞吐量。我设的是 4KB实测 TCP 发送速度大概 8Mbps对于数据采集够用了。如果要跑高速传输可以加大到 16KB但 H743 的 MAC 只有 100Mbps实际能跑到 90Mbps 左右再大也没意义。5.2 中断与轮询的取舍Polling 模式最大的问题是 CPU 占用率高。我实测下来主循环里ethernetif_input()加sys_check_timeouts()加HAL_Delay(1)CPU 占用率大概 15%。如果去掉HAL_Delay(1)CPU 占用率直接飙到 100%但网络性能并没有提升因为瓶颈在 PHY 的 100Mbps 带宽不在 CPU。如果项目对功耗有要求可以用中断模式把 ETH 的接收中断打开在中断里调用ethernetif_input()主循环里只处理业务逻辑。但中断模式有个坑LwIP 的sys_check_timeouts()不能在中断里调用必须放在主循环。而且中断里调用pbuf_alloc可能失败因为中断上下文不能阻塞。我的建议是如果网络流量不大每秒几百个包以内Polling 模式完全够用代码简单调试方便。如果流量大或者对功耗敏感再考虑上中断或者 RTOS。5.3 长时间运行的稳定性验证我让板子连续跑了 72 小时每 10 秒发一个心跳包到上位机同时 PC 每秒钟 ping 一次。结果丢包率0%平均延迟0.8ms最大延迟3.2ms出现在 DHCP 续约的时候内存泄漏无mem_trim没有触发DHCP 续约的时候会有短暂的延迟抖动因为续约过程中会重新发送 REQUEST占用一点网络带宽。这个正常不影响业务。提示如果发现运行几个小时后断网先检查sys_check_timeouts()有没有被调用。我遇到过因为主循环里加了一个HAL_Delay(5000)导致 DHCP 续约超时板子丢了 IP 还不知道。6. 写在最后的实操心得这个项目从开始到稳定运行前后花了大概一周时间其中 80% 的时间花在排查 PHY 和 DHCP 的问题上。回头看如果一开始就把 MDC 时钟降下来、复位延时加够、内存池加大能省掉至少三天的调试时间。另外一个小技巧在low_level_init()里加一个 PHY 寄存器的打印把 BSR、PHYSR 这些关键寄存器的值打出来对比 LAN8742 手册一眼就能看出问题。比如 BSR 的 bit2 是 link statusbit5 是 autoneg complete如果 bit2 是 0说明网线没插好或者对端没开。最后如果你也在用 H743 跑以太网建议先用 CubeMX 生成一个最简单的 Polling 模式工程把 ping 跑通再往上加 DHCP 和业务逻辑。不要一上来就搞全套出了问题根本不知道是哪一层的问题。分层调试逐层验证这是嵌入式网络开发最稳妥的路子。