尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

GD32H759+RT-Thread以太网驱动实战:从PHY调试到丢包排查

GD32H759+RT-Thread以太网驱动实战:从PHY调试到丢包排查 做工业控制项目通信链路是最先要打通的关卡之一。这次项目主控选了 GD32H759操作系统用 RT-Thread设备要接进产线数据网络所以 enet 驱动就成了第二块必须啃下的硬骨头。这篇系列第2篇专门记录 enet 驱动的落地过程从硬件接口选型、RT-Thread 网络框架对接、PHY 调试、DMA 收发优化到最后一个真实丢包问题的完整排查。如果你也在 GD32H7 这类带 EMAC 的国产 MCU 上接 RT-Thread这篇文章里的大部分坑你应该都会遇到。没动手之前我以为 enet 驱动无非就是初始化 MAC、配一下 PHY然后把 RT-Thread 的 lwIP 组件打开就行。实际做下来才发现真正的难点根本不在驱动代码本身而在时钟、复位、描述符管理、缓存一致性、netdev 注册时机这些不显眼的地方。这篇文章会把过程中踩过的坑、对应的排查思路和最终的验证结果都记录下来给后面要做 GD32H7 系列加 RT-Thread 以太网的朋友当个参考。1. 硬件设计和 enet 外设资源分配1.1 GD32H759 EMAC 的能力边界很多人拿到 GD32H759 的第一反应是先看主频、Flash、SRAM以太网接口反而容易被忽略。但实际上这颗芯片的 EMAC 外设能力不算弱它支持 10/100Mbps 自适应速率符合 IEEE 802.3 和 802.3u 规范内部自带 DMA 控制器和描述符管理单元支持 MII、RMII 两种介质无关接口。对于大多数工控场景这个带宽已经完全够用除非你要做视频流或高速数据采集否则没必要纠结千兆。在驱动设计之前需要先把 EMAC 的几个关键能力边界搞清楚。第一收发路径是否支持硬件 CRC 校验和 TCP/IP 校验和卸载GD32H759 是支持的这意味着 lwIP 上层的校验工作可以交给 MAC 硬件完成节省 CPU 开销。第二地址过滤机制支持单播、组播、广播过滤这对工控设备做多播报文分发很有用。第三MAC 和 DMA 之间的连接方式采用描述符环的结构驱动需要维护 TX 和 RX 两组描述符每一组描述符又指向对应的数据缓冲区。这个机制和 ST 系列很像但寄存器地址和位定义不同不能直接照搬。还有个容易被忽视的点EMAC 外设的时钟源在 GD32H759 上是通过 RCC 时钟树单独配置的和系统主频不是简单的一比一关系。如果时钟配置不对MAC 寄存器读出来全是 0xFFFFFFFF或者 DMA 收发超时这时候第一反应别去怀疑代码先用逻辑分析仪或示波器看 ETX_CLK / ERX_CLK 上有没有正确的时钟波形。我自己就因为在时钟树配置上少开了一个 bit白排查了整整一个下午。1.2 MII 与 RMII项目中的选择逻辑MII 和 RMII 的选择是硬件设计阶段就要定下来的事驱动层面的代码差很多后面再改会非常痛苦。MII 接口是标准 4 位数据线需要独立的 TX_CLK 和 RX_CLK速率分别是 25MHz100M和 2.5MHz10M总共占用 16 根引脚左右。RMII 接口则把数据线缩到 2 位收发共享一个 50MHz 参考时钟总引脚数大概 10 根左右。从引脚数量上看RMII 明显更省资源而且主控只需要给 PHY 提供一个 50MHz 参考时钟就能工作PCB 布线压力小很多。但从抗干扰和时序余量的角度MII 在恶劣电磁环境下更占优势因为它的数据率低、时钟沿和数据窗口更宽线缆长度容忍度也更高。工控场景下很多设备会被装在电柜里旁边就是变频器、伺服驱动器、开关电源这类强干扰源。我最终选择的是 RMII 加外部 50MHz 有源晶振方案原因有三点PCB 面积已经非常紧张省掉的 6 根信号线可以留给其他外设RMII 信号速率虽然高一点但只要走线长度差控制在 500mil 以内配合地包线处理EMI 风险完全可控GD32H759 的 EMAC 内置了接收 FIFO 和发送 FIFO配合溢出丢弃策略可以在一定程度上补偿 RMII 在高负载下的抖动。需要注意RMII 对参考时钟的要求比 MII 严格。如果时钟偏差超过正负 50ppm长时间运行会偶发 CRC 错误帧。有些设计偷懒直接从 MCU 的 MCO 引脚输出 50MHz 给 PHY实测也能通但相位噪声和抖动指标不理想。有条件的还是用独立有源晶振。我这边用的是 50MHz 温补晶振链路跑了 72 小时没有出现因为时钟引起的丢包。1.3 PHY 选型和硬件接线的注意点PHY 芯片选择直接决定了 MDIO 驱动代码的工作量。市面上常见的高性价比百兆 PHY 有 LAN8720A、IP101GRI、DP83848C、KSZ8081 这几款。GD32 官方评估板上常见的是 IP101GRI地址默认由硬件引脚 PHYAD[2:0] 决定通常配置为 1。LAN8720A 默认地址是 0KSZ8081 默认地址是 0x01。每个 PHY 的具体寄存器偏移和 bit 定义略有差异尤其是状态寄存器的 Link Status 位位置代码里不能一刀切。硬件接线上有几个容易踩的雷区。首先是 MDIO 的上拉电阻MDIO 和 MDC 信号一般要求有 2.2k 到 10k 欧姆的上拉部分 PHY 芯片内部自带弱上拉但如果外部没接遇到某些 PHY 在复位后 MDIO 释放总线时电平不稳会导致寄存器读回全 0 或者偶发读写错误。其次是 PHY 复位引脚建议用一个 MCU GPIO 控制而不是直接接到系统复位这样驱动代码可以在需要的时候对 PHY 做软复位或者硬复位。另外一个很多人忽略的细节是 LED 指示灯的接法。工控设备往往需要在面板上显示网络状态PHY 的 LINK/ACT LED 引脚驱动能力通常不强直接接 LED 需要串联限流电阻而且要确认 PHY 的 LED 模式下是高电平有效还是低电平有效。IP101GRI 默认是推挽输出LAN8720A 则可以通过寄存器配成不同的工作模式这些信息在 PHY 数据手册里都有但很容易被驱动开发人员忽略最后硬件那边抱怨灯不亮一查发现是极性反了。2. RT-Thread 下使能 enet 驱动从 Studio 到 ping 通2.1 工程配置中容易被忽略的依赖关系RT-Thread Studio 是目前官方主推的 IDE基于 Eclipse 魔改集成了工程创建、图形化配置、编译下载和调试功能。创建 GD32H759 工程时Studio 会从 SDK 仓库拉取对应的 BSP但这个 BSP 默认不一定启用了以太网组件需要在 RT-Thread Settings 里手动打开。很多人配置完 lwIP 组件后发现编译报错提示缺少rt_emac或者phy相关头文件原因是没有开启设备驱动框架下的EMAC抽象层以及组件里的PHY管理组件。这两个并不是由 lwIP 自动依赖拉起的得手动勾选。更隐蔽的一个依赖是netdev组件它负责网络接口的上层抽象如果没开lwIP 即使正常跑起来控制台里也看不到网卡状态ifconfig命令会直接提示找不到网络设备。所以我建议配置顺序是勾选 lwIP、勾选 netdev、勾选 PHY 组件、勾选 EMAC 设备驱动框架然后再去配置 lwIP 的内存池大小和协议栈选项。这样四层依赖一次到位后面编译阶段不用频繁返工。2.2 驱动适配层的代码结构RT-Thread 的 EMAC 驱动框架做了比较好的抽象。驱动层只需要实现rt_emac_ops_t结构体里的几个函数包括init、open、close、tx和rx以及底层的中断处理、PHY 状态回调、发送完成回收。上层 lwIP 通过 netdev 组件访问统一的网络接口不关心具体是哪颗芯片的 MAC。这套抽象的一个好处是如果后续项目要换 PHY只需要修改 PHY 驱动部分的寄存器操作和状态解析EMAC 主驱动基本不需要动。如果换主控芯片只要新芯片的 EMAC 接口兼容类似机制驱动框架也能复用一部分。对于像咱们这种系列化产品迭代的场景这个收益是实打实的。驱动适配层通常会放在 BSP 目录下的drivers/文件夹我习惯把 Ethernet 相关代码独立成一个eth/子目录里面分三个文件gemac.c放 MAC/DMA 初始化phy_xxx.c放特定 PHY 的读写和状态解析eth_link.c负责把 EMAC 回调转换成 RT-Thread 的事件。这样的分层在排查问题时非常舒服网络不通先在gemac里查链路一直 down 去phy里查收发卡住再看eth_link。2.3 从串口日志判断初始化是否成功驱动写好之后第一次烧录进板卡先别急着敲 ping 命令先看串口控制台日志。RT-Thread 启动后如果以太网驱动正常注册会出现类似下面这样的输出[I/eth] eth device initialized [I/eth] phy 0x01 reset done [I/eth] auto-negotiation... link up 100Mbps full-duplex [I/netdev] netdev (eth0) up如果看到phy reset done但一直没有link up大概率是 PHY 地址不对或者 MDIO 读写失败。如果连eth device initialized都没有说明 EMAC 驱动的 init 函数没有执行成功优先检查时钟树和引脚复用配置。ping能通只是第一步ifconfig能看到 IP 地址和收发计数才说明 RT-Thread 网络层已经完整接管。我建议在每个阶段都保留一段日志输出比如链路状态变化、PHY 寄存器异常、DMA 收发超时后期定位现场问题会非常有价值。工控设备跑在现场不像开发板可以随时接调试器日志就是你唯一的眼睛。3. PHY 调试自动协商和链路状态处理的几个关键点3.1 通过 MDIO 验证 PHY 是否存在MDIO 总线是 MAC 管理 PHY 的唯一通道驱动跑起来之后第一件事是确认能不能稳定读到 PHY 寄存器。可以把 PHY 的寄存器 2PHY ID 高 16 位和寄存器 3PHY ID 低 16 位读出来和 PHY 数据手册上的 ID 值比对。比如 LAN8720A 的 PHY ID 是 0x0007C0F1IP101GRI 是 0x02430C54。如果能读到正确 ID说明 MDIO 时序、PHY 地址、硬件连接基本没问题。如果你读到的 ID 全是 0x0000 或者 0xFFFF别急着改代码先用示波器看 MDC 和 MDIO 引脚的波形。MDC 频率一般配到 2.5MHz 以下部分 PHY 对 MDC 最高频率有限制超了会读不到正确数据。还有 MDIO 是双向信号MAC 发地址时要切换方向PHY 回数据时也要正确接管总线驱动里如果方向控制 GPIO 没配置对同样会读到无效数据。读 PHY ID 还有一个好处可以在驱动里加一个校验逻辑初始化的时候读出 PHY ID如果和编译期配置的 ID 不匹配就输出警告日志。这个逻辑在量产阶段很管用可以及时发现硬件贴错 PHY 或者空焊的问题。我以前就遇到过一次产线反馈设备网络不通远程一看日志发现 PHY ID 是 0x00000000最后确认是 PHY 的供电电感虚焊导致的。3.2 自动协商完成的等待时机自动协商是 PHY 上电后默认开始的一个过程它和链路对端交换能力信息协商出双方都支持的最高速率和工作模式。驱动代码里不能在上电后立刻读取协商结果因为 PHY 内部需要几百毫秒到一两秒不等的时间完成这个过程。标准做法是轮询 PHY 的状态寄存器一般是寄存器 1BMSR查看Auto-Negotiation Complete位。这个位从 0 变成 1表示协商完成这时候再去读取Link Partner Ability寄存器获取对端的速率和双工模式。但有两点需要特别注意。第一很多 PHY 的寄存器 1 里还有一个Link Status位这个位是锁存型的链路断开后它会保持之前的 0 状态需要先读一次清掉锁存再读一次才能拿到真实状态。驱动里如果没做二次读取会出现链路明明已经断开软件却认为还在线的“假链路”状态。第二自动协商超时处理一定要做不能无限轮询。我一般设 3 秒超时超时后强制配置为 100Mbps 全双工然后继续监听链路状态一旦对端能力变化再重新触发协商。这样既不会卡死系统也不会因为协商失败而永久断链。3.3 PHY 复位和延迟对链路建立的影响PHY 复位分为硬件复位和软件复位两种。硬件复位通过复位引脚控制软件复位是通过写 PHY 寄存器 0 的 Bit 15 触发。项目里两种情况都需要支持硬件复位用于上电初始化软件复位用于系统运行中 PHY 异常时的自恢复。PHY 数据手册通常会给出一个复位完成时间典型值是 10ms 到 100ms 不等。有些驱动的做法是写完复位命令后立刻去读寄存器结果读到的是复位过程中的随机值。正确做法是写完复位命令后至少延时 100ms再开始读 ID 和配置寄存器。同时硬件复位引脚如果由 MCU GPIO 控制复位完成后的释放时机也要注意不能和 EMAC 初始化并行进行否则 DMA 配置可能在 PHY 还没就绪时就启动了。在调试过程中我总结了一个规律遇到 PHY 链路建立不起来先看电源引脚的电平再看复位引脚的时序然后确认时钟输入最后才去怀疑 MDIO 配置。这个顺序出错的概率从高到低排列绝大多数“link up 不了”的问题最后都定位在电源或复位上而不是 MDIO 驱动代码。4. DMA 收发的数据面优化与实测4.1 描述符环和缓存一致性的关系网卡驱动的核心数据路径是 DMA 描述符环。发送时驱动把待发送的数据包地址填入 TX 描述符然后告诉 DMA 引擎可以发送了接收时DMA 把收到的帧写入 RX 描述符指向的缓冲区然后唤醒驱动去处理。RT-Thread 的 EMAC 框架在tx函数里接收上层传下来的pbuf发送完成后需要把pbuf释放回 lwIP 的内存池。GD32H759 的 Cortex-M7 内核带有数据 Cache如果 DMA 写入了数据缓冲区而 CPU 的 Cache 里还保留着旧数据就会读到脏数据。这是很多新手在 M7 平台上移植网卡驱动时最容易遇到的问题。解决办法有两个方向一是禁用相关内存区域的 Cache二是手动做 Cache 清理和无效化操作。禁用 Cache 实现简单但性能损失明显手动操作又容易遗漏。实践下来比较合理的方案是把 DMA 描述符和数据缓冲区放在一个单独的内存区域通过链接脚本或者内存管理配置把这部分区域标记为non-cacheable。RT-Thread 支持配置HEAP_END和SRAM分区如果芯片支持 MPU还可以用 MPU 把以太网缓冲区区域配置成normal memory, non-cacheable。这样既保证了 DMA 和 CPU 数据一致又不会影响其他程序代码使用 Cache。4.2 吞吐量和 CPU 占用的实测结果驱动调通之后我做了几轮吞吐量测试。测试环境是开发板通过网线直连 PC使用 iperf 工具打流。100Mbps 的链路满双工状态下TCP 下行PC 到开发板实测稳定在 92Mbps 到 95Mbps 之间上行开发板到 PC略低一点大概 88Mbps 左右。这个数据对于工控场景已经非常充足毕竟 MODBUS TCP 一个典型请求也就几百字节。CPU 占用率的改善是缓存一致性方案带来的最大收益。在禁用 Cache 的配置下iperf 跑满速时 CPU 占用率接近 60%改用 MPU non-cacheable 区域之后CPU 占用率降到 35% 左右。对于需要同时处理多路模拟量采集、控制逻辑和显示刷新的工控应用节省下来的 CPU 资源非常可观。RX 描述符数量对吞吐量的影响也比较明显。默认配置 8 个 RX 描述符时阻塞式 TCP 接收偶尔会触发 DMA 溢出因为 lwIP 处理栈还在处理上层协议时DMA 已经把新到的帧放满了描述符环。把 RX 描述符改成 32 个之后连续跑 30 分钟打流没有出现一次 DMA overrun 计数器增加。代价只是多占用 32 乘 2KB 约 64KB 的内存对于 GD32H759 的片上 SRAM 体量来说完全值得。4.3 中断与轮询在工控场景下的取舍以太网数据到达后可以触发接收中断通知 RT-Thread 处理也可以由应用层周期轮询接收状态。中断方式响应快但高流量下频繁触发中断会加大调度器的负担轮询方式实现简单但实时性难以保证。两种方式各有问题工控场景我更推荐中断加汇聚的方式。所谓中断加汇聚就是保留接收中断但在中断处理函数里只做状态标记和帧接收然后通过一个线程或工作队列去集中处理收到的数据包。如果不关闭中断就逐包处理高流量时可能陷入中断风暴如果关中断处理又可能导致后续帧的接收超时。我在项目里是这样实现的中断里把所有可读的 RX 描述符一次性取出来通过信号量交给网络接收线程处理同时保证中断尽可能快地退出来。实测在 1ms 中断频率内每个中断里最多可以处理 4 到 8 个数据包CPU 占用率依然可控。这里面有个容易被忽略的点RT-Thread 的中断优先级设计。EMAC 中断优先级不能设得过高否则会打断系统时钟和更高实时性要求的控制中断也不能设得过低否则网络延迟会变大。我的建议是放在中等偏高的位置同时保证网络接收线程的优先级高于低于通常的应用线程即可。5. 丢包排查复盘一次定位到“假链路”的过程5.1 现象先看这里设备能 ping 通但偶发丢包项目联调阶段现场反馈设备存在不规则丢包现象ping 10000 个包大概丢 20 到 30 个丢包位置没有明显规律。从应用来说这个丢包率对 MODBUS TCP 的影响不算致命但对设备整体质量来说不可接受必须查清楚。我拿到现场问题后的第一阶段排查包括以下几步用网线直连复现发现丢包率维持在 0.2% 左右通过ifconfig查看网卡统计信息发送和接收的错误计数都是 0PHY 的链路状态也一直是 up 状态协议栈层面看不到任何异常。到这里问题被缩小到数据面或驱动层面。5.2 根因定位过程从链路状态到 DMA 描述符丢包问题的定位过程比较曲折。我先怀疑是 PHY 的链路质量问题于是一直盯着 BMSR 寄存器看。观察了 20 分钟Link Status 位一直保持 1说明物理链路确实没有断开过。然后又怀疑是 lwIP 的内存池不足但打印内存统计后pbuf和mem池都没有溢出。最终把目光转向 DMA。我给驱动加了一个统计变量记录 RX 描述符处理失败的次数。运行一段时间后发现描述符的中断处理循环里偶尔会出现一个“描述符所有权还没有交还给 DMA”的情况。这个描述符的状态位显示它已经收到了一个新帧但驱动下次读取时发现它的 DMA-owned 位还是 0没有正常复位。进一步分析发现这个问题的根源是 RX 描述符在释放回 DMA 的时机不对。驱动在取走数据后必须先写完新的缓冲区地址、清空状态位再把所有权位交还给 DMA。但代码里处理顺序是先交还所有权再更新缓冲区和状态导致有时候 DMA 在驱动更新地址前就收到了新的接收请求把数据写入了一个尚未初始化的缓冲区从而触发了异常保护机制。这个 bug 在低流量下几乎不会出现因为两次接收之间间隔足够长但在持续流量下就会偶发。5.3 从这次排查沉淀下来的检查清单这次问题解决之后我把以太网丢包的排查流程整理成了一个固定检查清单后面再遇到类似问题就直接对照着查效率高了很多。第一确认物理链路状态。PHY 的 Link Status、速率、双工模式是否正确有没有 CRC 错误计数增加的现象。第二检查驱动数据面。RX 描述符的所有权交接顺序、缓冲区地址是否在每次接收前被正确更新、DMA 溢出统计有没有增加。第三排查协议栈内存。lwIP 的pbuf池和内存池是否有耗尽尤其注意连续小包场景下的碎片问题。第四观察系统调度。网络接收线程是否被高优先级任务饿死中断是否出现过长的关中断窗口。这张清单基本覆盖了我在 GD32H759 平台上遇到的所有以太网丢包场景分享给团队之后大家排查问题的时间从原来的几天缩短到半天以内。5.4 这个 bug 给驱动设计带来的反思修复完这个描述符交接 bug 之后我回过来审视整个驱动代码发现类似“顺序敏感”的问题其实不止这一处。比如 PHY 状态机的轮询周期如果周期太短可能在 PHY 内部尚未稳定时读到中间状态如果太长链路恢复的响应速度又不够。又比如发送完成中断的处理如果回收缓冲区时没同步好后续发送线程的等待状态也可能出现发送队列卡死。这些都是驱动代码固有的易错点规避方法并不复杂但需要养成习惯一是所有对 DMA 描述符的操作严格遵循“配置-提交-确认”的顺序不要为了省几条指令而打乱二是每次修改涉及数据面的代码都要跑一次满速率压力测试只看 ping 通不够必须用 iperf 长时间打流来验证三是保留充分的调试统计接口平时不一定要打开但需要的时候能立刻看到内部状态。从这次实际项目经验来讲以太网驱动的调试更像一道串联题物理链路、DMA、协议栈、调度任何一环出现弱连接都会表现出看起来一样的“丢包”或者“不通”。把每一环的验证方法标准化才是长期收益最大的投入。这个驱动后续我还会继续优化比如加入对 IEEE 1588 PTP 协议的支持进一步扩展它在工控同步场景下的能力。如果你也在做 GD32H7 加 RT-Thread 的以太网方案欢迎一起交流踩坑经历。
返回列表