ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32H743裸机以太网:LwIP+DHCP+MPU/Cache避坑指南

STM32H743裸机以太网:LwIP+DHCP+MPU/Cache避坑指南 干这行久了你会发现很多朋友拿到 STM32H743 这种带以太网 MAC 的高性能 MCU第一反应就是“先上个 FreeRTOS 再说”。但我自己调试网络协议栈这么多年最深的感受是别急着上系统裸机方案往往更容易排错也足够稳。这次要聊的项目就是典型的例子——用 H743 驱动板载 LAN8742 百兆 PHY不用 FreeRTOS直接裸机循环配合 LwIP 的 NO_SYS 模式让板子上电后通过 DHCP 自动从路由器拿到 IP。整套流程里真正折磨人的不是 DHCP 报文也不是 PHY 驱动而是 H7 系绕不开的 MPU 与 Cache 一致性问题。如果你也准备在 H743 裸机上跑以太网这篇文章的实操过程应该能帮你省下一整周的调试时间。1. 方案拆解为什么裸机跑以太网完全可行1.1 需求审视这个项目真的需要 RTOS 吗先看项目需求STM32H743 内置的以太网 MAC 负责收发帧外部接一颗 LAN8742 作为 PHY模块上电后需要自动请求 IP拿到 IP 后能被上位机 ping 通并回应简单的网络请求。整个网络任务其实就两件事MAC 层收发以及协议栈里的 DHCP 状态推进。这里没有多路传感器采集、没有复杂的任务调度、没有严苛的实时响应要求CPU 绝大部分时间都是空闲的。这种场景下上 FreeRTOS 当然可以但收益很有限。跑 RTOS 意味着你要考虑任务栈分配、中断与任务的同步、信号量或者消息队列任何一个环节出问题都会增加调试成本。反观裸机方案一个 while 循环加一个以太网接收中断再配合 LwIP 提供的 NO_SYS 模式逻辑非常清晰。出了问题可以直接从中断、主循环、协议栈三层逐段定位根本不需要像 RTOS 那样层层猜。我见过不少工程师被“以太网必须配 RTOS”这句话带偏其实 LwIP 这个协议栈本身是高度模块化的它既可以跑在 FreeRTOS 上也支持无操作系统模式。无系统的 LwIP 占用资源更少行为更可控对只做 DHCP 加简单 TCP/UDP 的应用非常合适。1.2 整体架构三层结构各司其职整个以太网功能可以拆成三层从下往上分别是硬件驱动层H743 的 ETH 外设MAC DMA加上对 LAN8742 PHY 的寄存器操作。协议栈层LwIP配置为 NO_SYS1也就是不需要线程通过定时器回调推进协议状态。应用层DHCP 自动获取 IP以及后续自定义的网络处理逻辑。这样的分层在裸机环境里同样成立。底层驱动负责把 PHY 链接起来、让 DMA 能收发报文LwIP 负责解析和封装 IP/UDP 报文DHCP 则是跑在 UDP 之上的一套状态机。裸机模式下以太网中断负责把数据包塞给 LwIP主循环定期调用sys_check_timeouts()推进 DHCP 的定时器逻辑另外再轮询一下 PHY 的链接状态即可。这种架构下你甚至不需要理解 TCP/IP 的每个细节LwIP 帮你处理了绝大部分协议逻辑。你要做的就是把底层跑通把内存访问问题解决干净然后把 DHCP 的启动和结果判定接进主循环。2. 硬件与工程准备先把 LAN8742 点亮2.1 引脚连接与 PHY 地址确认LAN8742 是 Microchip原 SMSC的百兆以太网 PHY 芯片ST 自家的 Nucleo-H743ZI 开发板板载 PHY 就是它所以参考资料非常多很多 ST 官方的示例工程可以直接参考。它支持 MII 和 RMII 两种接口实际项目里几乎都用 RMII因为引脚少、布线简单。RMII 接口的信号列表如下信号方向说明ETH_REF_CLK输入50MHz 参考时钟ETH_MDIO双向管理接口数据ETH_MDC输出管理接口时钟ETH_CRS_DV输入载波侦听/数据有效ETH_RXD0 / RXD1输入接收数据ETH_TX_EN输出发送使能ETH_TXD0 / TXD1输出发送数据H743 的典型引脚映射是 PA1 作为 REF_CLKPA2 作为 MDIOPC1 作为 MDCPA7 作为 CRS_DVPC4/PC5 作为 RXD0/RXD1PB11 作为 TX_ENPB12/PB13 作为 TXD0/TXD1。不同板卡可能引脚有差异但功能绑定基本一致用 CubeMX 直接勾选即可。这里有一个必须注意的点LAN8742 的 PHY 地址。这颗芯片的地址由 PHYAD0/PHYAD1 引脚的电平决定绝大多数板子默认拉低所以 PHY 地址是 0x00。但如果你用的板子把 PHYAD0 拉高了地址就变成了 0x01。这个地址会用在 MDIO 读写和 HAL 的 ETH 初始化参数里。建议上电后先读一下 PHY 的两个 ID 寄存器0x02 和 0x03确认能读到预期值再继续往下走。2.2 CubeMX 配置要点用 CubeMX 配置 H743 的以太网外设并不复杂关键点集中在以下几个地方。第一选择 ETH 外设接口类型选 RMII。如果选 MII引脚数量会多出不少而且对时钟要求也不太一样建议默认 RMII。第二配置 PHY 地址。在 ETH 外设参数里填上你板子的 PHY 地址通常就是 0。CubeMX 会根据这个地址来生成 HAL 初始化代码里 HAL_ETH_Init 的 PHY 地址参数后续读写 PHY 寄存器都会用到。第三MDC 分频。MDIO 管理时钟最高一般建议不超过 2.5MHz。H743 的 HCLK 如果跑在 240MHzMDC 分频至少要选 HCLK/128 才能压到 1.875MHz。分频太低会导致 MDIO 读取失败或数据抖动。这个参数在 CubeMX 里可以直接选优先选安全值。第四也是最容易忽略的——RMII 参考时钟。RMII 要求 PHY 和 MAC 共享一个 50MHz 的参考时钟。这个时钟可以来自外部无源晶振也可以由 MCU 的 MCO2 引脚输出或者直接由外部有源时钟提供。如果这个 50MHz 没起来PHY 的寄存器都读不到更别提 link up 了。我在实际项目里遇到过一次很隐蔽的问题板子原理图上 REF_CLK 接了外部 25MHz 晶振PHY 内部有 PLL 可以倍频到 50MHz但 LAN8742 的配置要求 RMII 模式下必须外部给 50MHz不能用 25MHz 晶振。结果就是 MDIO 能读PHY ID 也对但 CRS_DV 一点反应都没有。后来换了 50MHz 有源晶振才正常。2.3 内存与 DMA 描述符规划H743 的内存分布比 F1/F4 复杂得多光是 SRAM 就有好几块0x24000000 的 AXI SRAM512KB、0x30000000 的 SRAM1128KB、0x30020000 的 SRAM2、0x30040000 的 SRAM3 等。以太网 DMA 的描述符和缓冲区应该放在哪一块这是很多人第一次用 H7 会踩的坑。一般情况下CubeMX 会生成默认的 DMA 描述符数组和缓冲区数组比如这样DMA_RxDesc[ETH_RX_DESC_CNT] __attribute__((section(.RxDescSection), aligned(4))); DMA_TxDesc[ETH_TX_DESC_CNT] __attribute__((section(.TxDescSection), aligned(4))); uint8_t Rx_Buff[ETH_RX_DESC_CNT][ETH_RX_BUF_SIZE] __attribute__((section(.RxArraySection), aligned(4)));这些section在链接脚本里通常会被放到 AXI SRAM 区域。AXI SRAM 在主频高的情况下访问最快但它默认是可由 Cache 缓存的这就为后面的 MPU 配置埋下了伏笔。除了选择内存位置你还得保证描述符和缓冲区都是 4 字节对齐更稳妥的是按 32 字节对齐因为对齐到 cache line 能避免一些边界情况下的数据撕裂问题。3. MPU 配置避坑指南高能预警3.1 Cache 和 DMA 吵架到底是谁的错H743 的 Cortex-M7 核心默认启动了 D-Cache数据缓存CPU 读写内存时数据可能不会真正落到物理内存里而是暂时停留在 Cache 中。问题是以太网 DMA 控制器直接读写物理内存它看不到 CPU 的 Cache。于是就会发生两种情况CPU 往缓冲区写了一包数据还没来得及 flush 到内存DMA 就把内存里的旧数据发出去了或者 DMA 已经从网络收到数据并写进了物理内存但 CPU 去读时命中了 Cache 里的旧缓存读到的还是之前的内容。用个不太严谨但好理解的比喻CPU 和 DMA 就像仓库的两个管理员CPU 习惯先把东西放在自己口袋Cache里等有空再放进货架内存。DMA 要去货架上取货看到货架是空的当然取不到。反过来DMA 把新货放到货架上CPU 却翻开自己的口袋找旧记录当然也找不到。解决这个问题有三个方向关闭 D-Cache治标不治本性能损失太大。每次 DMA 操作前后手动调用SCB_CleanDCache()和SCB_InvalidateDCache()但在接收路径上很容易漏而且调用时机错了反而更麻烦。用 MPU 把 DMA 缓冲区所在的内存区域配置成 non-cacheable让 CPU 访问这块区域时直接绕过 Cache。这是最干净、最推荐的做法。3.2 用 MPU 给 DMA 缓冲区划一块 Non-Cacheable 区域MPU 是 Cortex-M 内核提供的内存保护单元它不只是用来做访问权限控制的还能给不同内存区域设置不同的缓存策略。CubeMX 默认生成的 H7 工程里其实有一个MPU_Config函数但它通常把整个 AXI SRAM 配置成了 write-back 类型这种配置如果直接拿来跑以太网 DMA很容易翻车。我自己倾向的做法是单独拿一块地址区域给以太网 DMA 描述符和缓冲区并在 MPU 里把这块区域配置为Normal memory, Non-cacheable。下面的代码展示了如何用 HAL 配置一个从 0x24040000 起始、大小 256KB 的 MPU Regionstatic void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct {0}; HAL_MPU_Disable(); MPU_InitStruct.Enable MPU_REGION_ENABLE; MPU_InitStruct.Number MPU_REGION_NUMBER0; MPU_InitStruct.BaseAddress 0x24040000; MPU_InitStruct.Size MPU_REGION_SIZE_256KB; MPU_InitStruct.SubRegionDisable 0x00; MPU_InitStruct.TypeExtField MPU_TEX_LEVEL1; MPU_InitStruct.AccessPermission MPU_REGION_FULL_ACCESS; MPU_InitStruct.DisableExec MPU_INSTRUCTION_ACCESS_ENABLE; MPU_InitStruct.IsShareable MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.IsCacheable MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsBufferable MPU_ACCESS_BUFFERABLE; HAL_MPU_ConfigRegion(MPU_InitStruct); HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); }这里的关键参数是TypeExtField MPU_TEX_LEVEL1、IsCacheable NOT_CACHEABLE、IsBufferable BUFFERABLE组合起来就是 non-cacheable 的普通内存。如果你用的 CubeMX 生成代码是另外一套参数那多半是 write-back 类型。不是说 write-back 完全不能跑只是你必须在 ethernetif.c 的收发路径里做一整套 cache clean/invalidate 处理这对新手来说是个不小的坑。把 MPU 配好之后还要注意链接脚本或者数组声明要把以太网缓冲区放到这个区域。我常用一种简单粗暴的办法直接用__attribute__((section(.ARM.__at_0x24040000)))把描述符和缓冲区放到指定地址这样就能保证它们落在 non-cacheable 区域内。3.3 最典型的两个踩坑现场先说第一个。现象是板子能发数据出去但是接收方向完全没反应。查代码发现接收 DMA 描述符的 OWN 位所有权位一直停留在 1意思是 CPU 始终认为描述符还是 DMA 拥有的。实际上 DMA 早就收完包并把描述符更新到物理内存了但 CPU 从 Cache 里读到的还是旧值。这就是典型的 Cache 一致性导致的“睁眼瞎”。第二个现象是DHCP 有时能拿到 IP有时不行ping 大包时丢包率很高。这种问题多半不是协议问题而是缓冲区经过了 Cache 的 Write-Back 机制数据在 DMA 和 CPU 之间交替访问时某一段被覆盖或者读到旧数据。尤其是 1500 字节以上的大包更容易暴露问题。排查这类问题有一个非常高效的技巧在 main 函数最开头临时调用SCB_DisableDCache()如果一切恢复稳定那基本可以断定是 Cache 一致性问题。关掉 D-Cache 跑的吞吐可能差点但能帮你快速把问题范围缩小。确认之后再回去精调 MPU 配置而不是一上来就怀疑 DHCP 报文写错了。4. LAN8742 驱动从寄存器到 Link Up4.1 通过 MDIO 读写 PHY 寄存器LAN8742 的控制通过 MDIO/MDC 管理接口完成HAL 库封装好了读写函数分别是HAL_ETH_ReadPHYRegister和HAL_ETH_WritePHYRegister。调这些函数之前ETH 外设的时钟和引脚必须已经初始化完成。上电后首先建议读 PHY ID 寄存器确认 MDIO 通路正常uint32_t id1 0, id2 0; HAL_ETH_ReadPHYRegister(heth, PHY_ADDRESS, 0x02, id1); HAL_ETH_ReadPHYRegister(heth, PHY_ADDRESS, 0x03, id2);如果读回来的值全是 0xFFFF说明 MDIO 没通或者 PHY 没上电如果读到一串看似合理但和预期不符的值可能是 PHY 地址配置错了。LAN8742 的 PHY ID 两个寄存器合并之后是一个 32 位的厂商编号具体数值可以查数据手册只要能稳定读到一个非 0xFFFF、非 0x0000 的值通路基本就算正常。PHY 的 Basic Control Register地址 0x00里有几个关键位bit15 是软件复位bit12 是自动协商使能bit9 是重启自动协商。Basic Status Register地址 0x01里 bit5 表示自动协商是否完成bit2 表示链路是否已建立。实际操作时配置 LAN8742 的推荐顺序是向 BCR 写入 bit151触发软复位。等待复位完成bit15 自动清零同时给 PHY 一点稳定时间。配置 BCR 开启自动协商。轮询 BSR直到 bit5 和 bit2 都置位。确认 MAC 侧的 PHY 配置比如 speed、duplex与协商结果一致然后启动以太网 DMA。4.2 裸机下检测网线连接的轮询策略有操作系统时通常会有专门的任务阻塞等待 link 事件。裸机环境里我们没有阻塞等待的资本因为整个 CPU 就一个 while 循环在跑一旦阻塞别的活全干不了。正确的思路是在主循环里做周期性轮询。我会在主循环里维护一个prev_link变量每 200ms 左右读一次 PHY 的链路状态。如果发现从断到通就重新启动 ETH 的收发流程并重新拉起 DHCP如果从通到断可以做些清理工作或者干脆停止 DHCP 重试。伪代码如下uint8_t prev_link 0; while (1) { uint8_t link eth_link_status_read(); if (link !prev_link) { HAL_ETH_Start(heth); dhcp_start(gnetif); } sys_check_timeouts(); prev_link link; }很多人在这一步会犯一个错误在初始化时调用HAL_ETH_Start然后在主循环里一旦检测到 link up 又调用了一次。以太网 DMA 重复启动会导致描述符状态错乱所以最好用一个标志位保证只在链路从断到通的边沿执行一次启动操作。5. 裸机 DHCP 实现报文的艺术5.1 DHCP 四个阶段在裸机状态机里怎么落地DHCP 本质上是一套基于 UDP 的客户端服务器协议标准流程是四步Discover、Offer、Request、Ack。客户端在 UDP 端口 68 监听服务器端口 67 接收请求。报文格式沿用了 BOOTP 格式选项字段通过类型-长度-值TLV的方式排列。裸机下实现这套流程核心是把它做成一个有限状态机。常见状态包括IDLE初始状态还没发起过 DHCP。DISCOVERING已经发出 Discover等待 Offer。REQUESTING收到 Offer正在发送 Request等待 Ack。BOUND拿到合法 IP正常运行。在主循环里我们根据当前状态、定时器超时时间、以及收到的报文类型来迁移状态。很多人纠结“为什么不用阻塞等待”本质原因是裸机只有一个线程你阻塞了网络接收中断即便收到数据后续的协议处理也没有机会执行。所以状态机必须是非阻塞的发送完 Discover 就立刻回到主循环等下一次 tick 检查是否超时或者是否有 Offer 进来。如果使用 LwIP 的 NO_SYS 模式这些状态机已经被 LwIP 内部实现了你不需要自己写状态迁移逻辑。你只需要在工程里调用dhcp_start()然后周期调用sys_check_timeouts()最后轮询dhcp_supplied_address()判断是否成功拿到 IP。我的经验是先用 LwIP 跑通业务稳定后再去看它的源码理解细节这样效率最高。5.2 手写 Discover 与 Request 的关键字段如果你想更进一步自己写一个 mini DHCP 客户端或者想彻底搞懂 LwIP 里那堆代码在干嘛那么报文的构造逻辑必须清楚。一个 DHCP Discover 报文op 字段填 1表示请求htype 填 1以太网hlen 填 6xid 是一个随机数用来匹配请求和应答。flags 可以设置为 0x8000表示如果服务器不能在当前子网内广播就以广播方式发送应答。chaddr 就是设备的 MAC 地址。Options 必须以魔数 0x63825363 开头然后按 TLV 格式添加选项。最关键的两个选项是选项类型值用途531Discover, 2Offer, 3Request, 5Ack表示 DHCP 消息类型544 字节 IP服务器标识专用于 Request 里指定应答服务器504 字节 IP请求的 IP 地址Request 里填 Offer 给的 IP55若干字节参数请求列表比如 1(子网掩码)、3(网关)、6(DNS)收到 Offer 后客户端需要检查报文的 xid 是否和自己发出的 Discover 一致Option 53 是否为 2并且记录 Option 54 里的服务器 IP。随后构造 Request 报文xid 保持不变op 还是 1但在 Option 里带上 Option 50要请求的 IP和 Option 54服务器 IP。最后服务器收到 Request 后回复 Ackxid 依然不变客户端再从中解析出子网掩码、网关、DNS、租期等参数。裸机编写时建议用一个结构体保存 DHCP 运行所需的临时变量包括当前状态、重传次数、XID、服务器 IP、租期、启动时间戳。比如typedef struct { uint8_t state; uint8_t retries; uint32_t xid; uint8_t server_ip[4]; uint8_t request_ip[4]; uint32_t lease_time; uint32_t t0; } dhcp_client_t;状态机转移逻辑在每轮主循环里检查sys_now()与t0的差值超过重传阈值就重发当前报文超过最大重试次数就回到 IDLE。5.3 超时重传和租期续约DHCP 是 UDP 协议天然不保证可靠送达所以客户端要有超时重传机制。常见的做法是Discover 发出后 4 秒没收到 Offer重发 Discover收到 Offer 后发送 Request4 秒没收到 Ack重发 Request。连续重试几次都失败就放弃本轮等待下个周期重新开始。重传间隔可以简单固定也可以做指数退避比如第一次 1 秒、第二次 2 秒、第三次 4 秒。对于裸机系统我建议用一个简单的 4 秒固定间隔代码容易写也足够应对大多数家庭路由器和工业交换机。租期续约也值得提前考虑。DHCP 服务器下发的 IP 是有租期的典型值是 24 小时。客户端不能拿到 IP 就一辈子不闻不问需要大约在租期过半时发起 Renew 请求。Renew 的报文其实就是把 ciaddr 填上当前自己用的 IP再带一个 Option 533 的 Request 报文即可。如果续约失败继续尝试直到最后租期到了还没成功就必须释放 IP 并重新走一遍 Discover 流程。6. 调试验证与问题排查实录6.1 用 Wireshark 排查 DHCP 链路调试网络最直观的工具就是抓包。如果板子和路由器中间有个镜像口交换机或者直接在电脑上跑一个 Wireshark能看到完整的 DHCP 交互过程。抓包的最好习惯是先把板子启动让它自己跑一轮 DHCP 流程然后用 Wireshark 里的过滤器bootp来过滤所有 DHCP/BOOTP 报文。正常的抓包序列应该是 Discover、Offer、Request、Ack 四条报文清晰可见。如果只看到 Discover 看不到 Offer问题大概率出在链路层或者服务器侧。如果能看到 Offer但收不到 Request 后的 Ack就要对比 Request 里的 XID 和服务器标识是否正确。有一种情况特别容易忽视板子发出来的 UDP 报文可能校验和错了尤其是你手写 DHCP 报文时IP 头或者 UDP 头的校验和没有正确计算。Wireshark 会通过校验和验证机制标红错误帧这时候优先级不是去调 DHCP 状态机而是先把校验和计算修对。6.2 常见问题原因速查表把我在实际调试中遇到的故障现象整理在下面方便排查时直接对照。现象可能原因排查方向MDIO 读 PHY ID 全是 FFFFPHY 没上电、RMII 50MHz 时钟没起、MDC 分频不对先量 REF_CLK 频率复查 PHY 供电与复位PHY 能读 ID但 link 始终 up 不了RMII 时钟异常、PHY 地址错、自动协商没开启检查 REF_CLK 波形读 BSR 确认 AN 状态抓不到板子发出的任何报文以太网 DMA 没启动、描述符配置错、缓冲区地址不在可访问区域确认 HAL_ETH_Start 调用了检查描述符所有权位能发 Discover收不到 Offer链路没真正 up、交换机端口隔离、DHCP 校验和错误确认 link 状态抓包看是否被标记为校验和错误Offer 之后 Request 发不出去XID 没在 Request 中保持一致、chaddr 填错对比抓包报文中的 XID 和 MAC 地址DHCP 偶发超时重试多次才成功Cache 一致性问题临时关闭 D-Cache 验证再精调 MPU 配置拿到 IP 后 ping 大包丢包缓冲区跨 cache line、clean/invalidate 顺序错误将缓冲区 32 字节对齐检查收发路径缓存处理6.3 从 MPU、PHY 到 DHCP 的最终检查清单最后分享一个我每次调试以太网都会从头到尾过一遍的检查清单按照这个顺序排查可以过滤掉九成问题。第一步硬件与时钟上电后用示波器量 RMII 50MHz 时钟确认 PHY 供电和复位正常。第二步MDIO 通路读 PHY ID 寄存器确认 PHY 地址正确、MDC 分频合理。第三步链路建立读 BSR 确认 link up 和自动协商完成。第四步内存与 MPU确认 DMA 描述符和缓冲区地址落在 non-cacheable 区域或 cache clean/invalidate 调用无误。第五步协议栈状态确认 LwIP 的 NO_SYS 定时器回调在主循环里执行dhcp_start()在链路 up 后调用。第六步抓包验证用 Wireshark 抓完整的 DHCP 四步交互逐段确认 XID、校验和、选项字段。这套流程我每次都会走尤其是从别的板子迁移到 H743 时MPU 和内存地址的调整是必查项。H743 的 Cache 配置是性能的加速器但也是网络稳定的隐形杀手一旦处理好裸机跑 DHCP 的稳定性完全可以满足量产需求。如果你也准备动手建议先不要急着把整个 LwIP 都啃一遍先把 MPU 配置好然后跑一个静态 IP确认两板互通之后再接 DHCP这个顺序能帮你把复杂的网络调试拆成几个小块每一块都是可控的。使用 H743 和 LAN8742 的组合记住“时钟正常、链路 up、Cache 不捣乱”这三件事剩下的就是协议栈内部的事情了。
返回列表