
上一篇文章把 GD32H759 的开发环境折腾完点灯和串口都能跑了我以为后面就是按着用户手册一步步配外设的事。直到开始弄以太网才发现自己低估了 enet 驱动这件事。GD32H759 这颗 Cortex-M7 主频能到 600MHz在 RT-Thread 里跑起来之后网络几乎是所有工控项目的刚需Modbus TCP 采集、远程升级、边缘节点上传数据哪个都绕不开以太网。这篇笔记就把我从 RT-Thread 的 BSP 工程开始到把 enet 驱动完全跑通、ping 通、测速、再踩完一轮坑的完整过程写下来给同样在折腾 GD32H759 的人一个可以直接参考的路线。1. 为什么 enet 驱动会是工控移植里的头号工程1.1 工控节点对网络的依赖比你想的更重很多从单片机转过来的人第一次接触以太网驱动时都会有同一个错觉以太网不就是把数据发出去收回来吗能有多麻烦等真上手才发现点灯是直接写寄存器串口是中断加 FIFO而以太网是三层东西叠在一起最下面是 MAC 和 PHY 的硬件交互中间是 DMA 描述符和中断最上面还有一整个 lwIP 协议栈在等着喂数据。工控场景里这个难度又被放大了一截。现场设备不是只在实验室里 ping 通就算完事要应对的是长时间不掉线、数据不丢包、网络抖动时能自愈。Modbus TCP 的主站会频繁轮询远程 IO 节点的报文往往很小但频率很高如果驱动在收发路径上有一个隐性 bug可能跑几个小时才暴露一次。这类问题一旦上线之后再排查代价远比开发阶段高得多。所以我的建议一直是工控项目里的网络驱动宁可前期多花时间把原理搞清楚也不要抱着能通就行的心态。数据链路层的问题协议栈根本兜不住。1.2 GD32H759 的 MAC 控制器与 Cortex-M7 的组合拳GD32H759 内置的是一个 10M/100M 自适应以太网 MAC 控制器带 DMA 引擎支持 MII 和 RMII 两种介质无关接口。从架构上看它和很多 Cortex-M 芯片上集成的 Synopsys DesignWare MAC 属于同一类方案所以如果你之前调过其他 M7 芯片的以太网寄存器布局和流程上会有很强的既视感。这颗外设最舒服的一点是 DMA 和 MAC 做在同一个外设里不需要 CPU 逐字节搬运。CPU 要做的是初始化时把描述符链表准备好收发过程中在中断里确认状态剩下的数据搬运 DMA 全包了。600MHz 的 M7 跑 lwIP 处理应用层协议绰绰有余真正的性能瓶颈往往不在 CPU 主频而是在 DMA 描述符数量、内存 cache 一致性、以及协议栈线程的调度方式上。另外GD32H759 的 enet 外设还支持接收和发送校验和的硬件卸载这个在工控场景里是加分项。开启之后IP/TCP/UDP 的校验和计算由 MAC 硬件完成CPU 负担更小也少了一部分软件处理的出错点。1.3 这一篇实战笔记覆盖到什么程度这篇不是把官方例程抄一遍而是记录一条从零把 enet 驱动在 RT-Thread 里跑起来的完整路径。我会先讲硬件接法的权衡和 PHY 选型再说咋把驱动挂到 RT-Thread 的 eth 设备框架上然后分析收发数据的流动过程。最关键的是把我在实际调试中遇到的几个经典问题的排查链路完整写出来包括 MDIO 读不到 PHY、小包通大包不通、Cache 一致性导致收包数据异常这些每个都是先把现象摆出来再一步步定位到根因。这个过程比结果更重要因为换一块板子、换一颗 PHY这些问题还会换着花样再出现一次。2. 先查硬件底账再写代码RMII 接法、PHY 选型和时钟来源2.1 MII 和 RMII 怎么选GD32H759 的 enet 控制器同时支持 MII 和 RMII硬件设计时就要二选一。MII 需要 16 根信号线数据位是 4 位发送和接收各有独立的时钟RMII 把信号线压缩到 7 根数据位改成 2 位收发共用同一个 50MHz 参考时钟。两者的对比如下项目MIIRMII信号线数量约16根约7根数据位宽TXD/RXD 各4位TXD/RXD 各2位参考时钟TX_CLK/RX_CLK 独立共用一个REF_CLK时钟频率2.5MHz(10M) / 25MHz(100M)固定50MHz适用场景性能优先、引脚不紧张引脚紧张、PCB布线简洁工控产品里绝大多数会选 RMII因为引脚省下来一大半GPIO 复用也更容易排。代价是 50MHz 参考时钟在 PCB 上的走线必须干净REF_CLK 如果存在抖动或干扰会直接表现为丢包或 link 不稳定。如果你的板子还在设计阶段REF_CLK 的建议是走短、走直、远离其他高速开关信号。已经拿到现成板子的也没关系重点是去原理图里确认清楚 REF_CLK 是从哪儿来的这决定了后面代码里怎么配时钟。2.2 RMII 信号清单与引脚复用细节无论用哪颗 PHYRMII 模式下的信号清单都是固定的ENET_TX_EN发送使能ENET_TXD0、ENET_TXD12 位发送数据ENET_RXD0、ENET_RXD12 位接收数据ENET_CRS_DV载波侦听/数据有效ENET_REF_CLK50MHz 参考时钟ENET_MDC、ENET_MDIO管理接口用来读写 PHY 寄存器外加 PHY 的复位引脚和中断引脚GD32H759 的引脚复用非常灵活同一个外设信号可能映射到好几组引脚上这时候千万别凭感觉写。我的做法是先打开芯片数据手册里的 AF 复用表把每个 enet 信号实际用到的引脚和 AF 号列一个表格对照原理图逐项确认。曾经有一次我把 ENET_RXD1 的 AF 选错了代码编译没问题但接收路径完全不通查了大半天才发现是引脚复用号少查了一行。2.3 REF_CLK 到底谁来给RMII 模式下50MHz 的 REF_CLK 有两种供给方式这是新手最容易踩的硬件坑之一。第一种是 MAC 提供时钟芯片内部产生 50MHz 信号通过某个引脚输出到 PHY。很多 PHY 的 XI 引脚直接接这个时钟就能工作。第二种是 PHY 自己带时钟源常见的是板上放一颗 50MHz 有源晶振或者 PHY 内部振荡后从 REF_CLK 引脚输出给 MAC。代码里的时钟配置必须和硬件接法对应上否则要么 PHY 完全没有时钟不工作要么 MAC 侧采样不到稳定的参考时钟导致 link 状态一直跳。我在 GD32H759 的板子上用的 PHY 是 YT8531它的 REF_CLK 是从 MAC 侧过来还是自己产生取决于外围电路的连接方式。拿到一张新板子时第一件事就是看原理图上 REF_CLK 引脚连到了哪里然后对照 PHY 手册确认它的时钟模式配置。不要假设官方 demo 的配置就一定适用于你的板子这个和 PHY 型号、晶振都有关系。2.4 PHY 复位时序与型号识别PHY 比 MAC 更娇气上电后必须等它内部初始化完成才能通过 MDIO 访问寄存器。很多 PHY 的复位时序要求低电平保持一段时间然后拉高之后再延时等待内部 PLL 锁定。这个延时不够最常见的现象就是读 PHY ID 读出全 F 或者全 0。我现在的代码里PHY 复位后的固定延时是至少等 10ms某些老型号 PHY 我会等到 30ms。具体看你用的 PHY 数据手册但宁多勿少。复位完成之后第一件事是读 PHY 的 ID 寄存器通常是寄存器 0x02 和 0x03拿读到的值和 datasheet 对比。这样做的目的是确认 MDIO 通路是通的同时也确认代码里写的 PHY 地址是否正确。PHY 地址不是默认就是 0有些 PHY 靠引脚电平决定地址有些默认是 0x01 甚至 0x1F这一步不确认后面所有 PHY 操作都是空中楼阁。3. RT-Thread 里挂接 eNET 驱动的常规流程3.1 从 BSP 工程开始还是从零开始RT-Thread 的官方仓库里已经有 GD32H759 系列的 BSP如果板型和官方的 START 或者 EVAL 板接近直接用 BSP 工程改效率最高。BSP 里已经把芯片启动、时钟树、串口、GPIO 这些基础都搭好了我们要做的就是确认 enet 相关的宏打开然后根据实际板子的 PHY 和引脚配置把驱动补齐。如果你的硬件是自定义核心板和我这次的情况一样那就需要从 BSP 工程里把原有的板级配置改掉。改动的范围一般集中在三块引脚复用配置、PHY 的型号和地址、以及 enet 外设初始化时传入的介质接口类型。这三块不变其他部分可以沿用官方驱动。在 RT-Thread 的配置里需要确认打开的关键宏包括RT_USING_LWIP使能 lwIP 协议栈RT_USING_NETDEV使能网络设备管理层BSP_USING_ENET使能板级 enet 外设这几个宏打开的顺序有个小细节RT_USING_NETDEV依赖RT_USING_LWIP而BSP_USING_ENET依赖RT_USING_NETDEV。用 RT-Thread Studio 的图形化配置界面时勾选层级会自动联动但如果手改 rtconfig.h就要注意配置顺序和依赖关系。3.2 初始化链路GPIO → MAC → PHY → DMA → 网卡注册enet 驱动的初始化顺序我总结下来是固定的五步顺序不能乱使能相关时钟复用 GPIO 引脚复位 PHY等待就绪通过 MDIO 读取 PHY ID确认 PHY 在线配置 MAC 和 DMA包括介质接口、双工模式、描述符地址把网卡设备注册到 RT-Thread 的设备框架中GPIO 和时钟配置部分GD32H759 的代码风格是先用rcu_periph_clock_enable打开对应 GPIO 端口和外设时钟再用gpio_af_set设置引脚复用功能。比如 RMII 模式下TXD0/TXD1、RXD0/RXD1、TX_EN、CRS_DV、MDC、MDIO、REF_CLK 这 7 组信号都要逐个配置。这里要特别提醒一点REF_CLK 对应的引脚如果走的是复用功能也要正确设置 AF 号。有的开发者觉得 REF_CLK 是时钟信号就不需要配置结果 PHY 起来了但 MAC 采样时钟不对link 状态起不来。3.3 贴一份可运行的初始化伪代码实际驱动代码比较长我这里把关键逻辑抽出来结构大概是这样static struct eth_device eth0_dev; static void enet_gpio_config(void) { /* 使能 GPIO 和相关外设时钟 */ rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_GPIOB); rcu_periph_clock_enable(RCU_GPIOE); rcu_periph_clock_enable(RCU_ENET); /* 配置 ENET_TX_EN / ENET_TXD0 / ENET_TXD1 等引脚 */ gpio_af_set(GPIOB, GPIO_AF_11, GPIO_PIN_11); /* TX_EN */ gpio_af_set(GPIOB, GPIO_AF_11, GPIO_PIN_12); /* TXD0 */ gpio_af_set(GPIOB, GPIO_AF_11, GPIO_PIN_13); /* TXD1 */ ... gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_11); gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_11); } static void enet_phy_reset(void) { /* 拉低复位引脚延时再拉高再延时 */ gpio_bit_reset(RST_PORT, RST_PIN); rt_thread_mdelay(20); gpio_bit_set(RST_PORT, RST_PIN); rt_thread_mdelay(30); } static rt_err_t eth0_init(rt_device_t dev) { enet_gpio_config(); enet_phy_reset(); /* 配置 MACRMII 模式自适应速率和双工 */ enet_init_struct enet_init; enet_init.media_interface ENET_RMII; enet_init.checksum_offload ENET_CHECKSUM_OFFLOAD_ENABLE; ... enet_init(enet_init); /* 注册网卡设备 */ eth_device_init(eth0_dev, e0); return RT_EOK; }这段代码里的eth_device_init就是把网卡设备挂到 RT-Thread 的 eth 设备框架上。设备名字我习惯用 e0也可以叫 eth0取决于你后续应用层代码习惯用哪个名字查网卡。做完这一步lwIP 通过 netdev 层就能看到这块网卡了。4. 收发数据是怎么流动的描述符、中断与协议栈线程4.1 DMA 描述符与数据缓冲区的关系以太网驱动的数据通路里DMA 描述符是核心数据结构。每个描述符对应一块数据缓冲区描述符里记录了缓冲区的地址、长度、状态和控制信息。GD32H759 的 enet 外设使用一组环形描述符表发送描述符和接收描述符各占一块连续内存。描述符的数量可以在初始化时配置常见配置是 RX 描述符 8 个、TX 描述符 8 个。描述符和实际数据缓冲区的关系可以理解成货架和货物的关系描述符就是货架上的标签DMA 把收到的以太网帧放进标注好的货位里然后改写标签状态告诉 CPU这里有货。CPU 处理完一帧数据后重新初始化描述符让它继续接收下一帧。一个特别容易被忽略的点是描述符本身必须放在 DMA 可以访问的内存区域而且很多 MCU 要求描述符的起始地址按照某种对齐方式排布。比如 GD32 官方库默认用 32 字节对齐如果结构体定义或者数组声明时没有注意对齐DMA 可能会从错误的地址读写数据现象非常诡异不是完全不工作而是跑一段时间丢几个包或者某一次收到的数据整体偏移了几个字节。4.2 收包路径和时间开销一次完整的收包过程是这样的PHY 从网线上收到以太网帧MAC 将帧写入 DMA 描述符指向的缓冲区DMA 更新描述符状态触发接收中断中断服务程序里清除中断标志通知协议栈线程lwIP 将数据从缓冲区拷贝到 pbuf 中交给上层协议处理中断服务程序里不应该有耗时操作。我见过有人直接在中断里做完整性校验甚至做业务处理这是非常危险的。网络中断频率高处理时间一长其他中断和线程都会被拖累。我的做法是中断里只做三件事读状态寄存器、清标志、发信号量或调用eth_device_ready通知协议栈。eth_device_ready这个动作是 RT-Thread eth 框架里通知 lwIP有数据来了的关键调用。协议栈线程被唤醒后会调用驱动注册的接收回调函数把数据从 DMA 缓冲区拿出去。这里有一个经验性问题如果中断里唤醒的时机太早缓冲区数据还没写完协议栈读到的就是半截数据。实际操作中DMA 更新描述符和触发中断是有时序保证的中断触发时数据已经在缓冲区里了所以不需要额外延时。但前提是 Cache 一致性问题必须处理好这部分后面单独说。4.3 发包路径和失败重传处理发送路径的逻辑流程如下协议栈把待发送数据封装成 mbuf/pbuf驱动把数据地址和长度填入 TX 描述符设置描述符为已就绪触发 DMA 发送DMA 把数据搬运到 MAC加上帧头和 CRC 后发到 PHY发送完成DMA 触发发送完成中断这里容易出问题的是 pbuf 不连续。lwIP 的 pbuf 可以按链表方式组织一个完整的 TCP 段可能由多个 pbuf 节点组成每个节点的内存地址不是连续的。DMA 要求一次发送使用一个连续的缓冲区所以驱动需要做聚合操作把多个 pbuf 节点拷贝到一块连续内存里或者逐个填充到多个 TX 描述符中。我最初实现时偷懒直接认为 pbuf 是连续的结果大包发送时系统偶发卡死。后来改成遍历整个 pbuf 链表把每个节点拷入同一个静态发送缓冲区再交给 DMA问题才消失。代价是多了一次内存拷贝但工控场景下报文普遍不大这个损耗完全能接受换来的是稳定可靠。4.4 中断里该干什么、不该干什么把中断处理好以太网驱动就成功了一半。我踩过的坑是一开始我在接收中断里直接调用 lwIP 的收包函数看起来高效实际上一遇到高速率数据流就崩溃。原因是 eth 设备框架里很多函数不是中断安全的协议栈内部有锁和信号量操作在中断上下文中调用可能造成死锁或调度异常。正确的做法是中断里用信号量唤醒一个专用的接收线程由接收线程去调用协议栈接口。RT-Thread 的 eth 框架做了这个封装中断里调用eth_device_ready框架内部会唤醒对应的接收线程在接收线程里执行驱动注册的接收回调。这样既保证了响应速度又避开了中断上下文的约束。中断优先级也要注意。以太网中断优先级不能太低否则在大量其他中断干扰下可能丢帧但也不能一路设成最高否则会影响系统实时性。我一般把 enet 中断设为略低于系统心跳中断高于普通串口中断。具体数值要看你的中断分组配置不能照搬别人的数字。5. 从灯不亮到 ping 通一段完整的排查记录5.1 第一步MDIO 读不到 PHY ID我这次移植的第一步就被卡住了。代码里初始化完 GPIO 和时钟后去读 PHY ID返回的一直是 0xFFFFFFFF。这个现象说明 MDIO 总线上根本没有设备响应或者时序不对。我的排查顺序是这样的检查 PHY 复位引脚电平用万用表量复位引脚的直流电平确认代码拉高后确实是高电平。有些 PHY 复位时序要求低电平保持时间比较长我这边量下来电平没问题。检查 MDC 和 MDIO 两个引脚是否真的配置成了复用功能重新查了一遍 AF 复用号发现 MDC 引脚我写错了复用号。修正后依旧读不到。检查 PHY 地址把 PHY 地址从 0 到 31 全部扫了一遍最终在地址 0x1F 处读到了正确的 PHY ID。原来这块板子的 PHY 地址由硬件引脚配置成了 0x1F而不是我假设的 0。总结下来MDIO 读不到 PHY ID 时不要反复改代码先把地址扫描、引脚复用、复位时序这三个变量全部验证一遍。其中地址扫描是最快的验证手段写一个循环读 0 到 31 地址的 PHY ID几秒钟就能确认总线上设备是否存在、地址是多少。5.2 第二步链接状态起来了但是 ping 超时PHY 能正常读完 ID 后link 状态寄存器显示 link up速率也协商完成了但电脑 ping 板子一直超时。这个阶段问题出在收发数据通路的可能性最大。我用了两条腿走路一边用逻辑分析仪抓 RMII 总线信号一边在板子上加打印确认 RX 中断是否触发。结果发现逻辑分析仪上能看到 PHY 收到了来自电脑的 ARP 请求RXD 数据线上有波形但板子的 ENET 接收中断一直没有触发。这个结果把问题范围缩小到了 MAC 到 DMA 这一段。检查后发现是 DMA 接收描述符初始化时我把描述符链表的尾指针没设对DMA 扫描描述符时认为没有空闲缓冲区于是接收数据无法落地。修正描述符链表的链接关系后再接上网络抓包工具ARP 应答就正常出来了ping 也通了。这个阶段的调试最忌惮的是没有工具意识。你光靠 printf 看代码很难定位逻辑分析仪抓引脚上的信号是直接证据能迅速区分是 PHY 以外的问题还是 MAC/DMA 的问题。5.3 第三步小包通、大包不通ping 小包小于 100 字节通了之后我开始测大包结果 ping 1500 字节的包一直不通表现为第一个包可能通后面的全部超时。这个现象非常经典通常指向两个地方接收缓冲区和 Cache 一致性。先看接收缓冲区DMA 为每个 RX 描述符分配的数据缓冲区如果小于 1518 字节收大帧时 DMA 会把数据截断或者写入相邻区域。我检查代码里配置的接收缓冲区长度发现确实设小了只有 512 字节。这个长度只适合小包测试大包一来缓冲区就溢出。修改缓冲区长度为 1600 字节后大包仍然不稳定。这时我意识到第二个嫌疑Cortex-M7 的 D-Cache。CPU 使能 D-Cache 后DMA 写入内存的数据CPU 从缓存里读到的可能是旧数据。也就是说DMA 明明已经把一帧完整的数据写进了内存但 CPU 从 Cache 里读到的还是之前的内容导致协议栈判断出数据错误或长度不对。5.4 第四步Cache 一致性问题确认与解决确认 Cache 问题的方法很直接把 RX 缓冲区每次读取之前对相应内存区域执行一次 invalidate 操作清掉 CPU 侧的缓存强制从内存重新读取。做了这一步之后大包立即通了。这不是花哨的技巧而是 Cortex-M7 平台做 DMA 驱动必须处理的常规问题。干净的方案有三种用 MPU 把 DMA 缓冲区所在的内存区域配置成非缓存device 或 strongly ordered属性这样 DMA 和 CPU 访问的都是同一份内存数据不需要手动维护。在每次 DMA 接收的前后对缓冲区执行 invalidate 操作。在每次 DMA 发送之前对缓冲区执行 clean 操作。推荐的还是第一种MPU 配置非缓存区。虽然会牺牲部分 CPU 访问这块区域的速度但以太网缓冲区本身就是一个中转站不需要 CPU 频繁访问性能影响可以忽略。而且它从根源上避免了忘了 invalidate 导致数据不对这类问题。我现在所有的 M7 平台网络驱动都会专门划一块非缓存内存给 DMA 描述符和数据缓冲区用。6. 性能摸底和工控稳定性优化6.1 iperf 测出的真实吞吐量ping 通了只是第一步项目要落地还得知道这块网卡真实能跑多少吞吐量。RT-Thread 那边跑一个 iperf 服务端电脑上跑客户端测发送方向和接收方向分别的吞吐量。第一次测出来的结果很一般发送大约 40Mbps接收甚至不到 30Mbps。对于 100M 网口来说这个数值还有较大优化空间。优化从三个方向同时动手DMA 描述符数量、lwIP 的内存配置、中断负载。RX 描述符从 8 个加到 16 个TX 描述符从 4 个加到 8 个同时把 lwIP 的PBUF_POOL_BUFSIZE调整为 1536保证每个 pbuf 能容纳一整帧。这些改动做完后收发吞吐量都接近 90Mbps基本接近百兆网口的理论极限了。工控场景下不建议为了跑满线速去过度优化。90Mbps 和 95Mbps 的差距对 Modbus TCP 来说毫无意义反而可能因为占用过多中断和内存资源影响其他任务的实时性。我的原则是吞吐量够用后把精力放在稳定性和低延迟上。6.2 Cortex-M7 的 Cache 一致性问题必踩这部分值得单独说因为它不是偶发问题而是每个用 M7 做网络的人都会遇到的一关。Cortex-M7 有 D-Cache 和 I-CacheCPU 访问内存时优先访问缓存DMA 访问的是物理内存。以太网收发缓冲区就是一个典型的共享数据区域DMA 往内存里写入数据CPU 读内存数据如果 Cache 里还留着旧值CPU 读到的就是错误数据。这个问题的隐蔽性在于不是每次都会出错而是和代码执行路径、数据大小、内存地址甚至编译优化有关。小包可能一直正常大包偶尔出错关闭编译优化后可能就消失了。这类 bug 往往藏到现场才爆发排查成本极高。我的建议是不要依赖手动 invalidate/clean 来维护直接在初始化时把以太网 DMA 使用的内存区域配置成非缓存。这样从逻辑上消除了问题。如果项目已经跑起来了不方便改那至少要把 invalidate 和 clean 的操作点放在收发的关键路径上而且要保证每个路径都覆盖到漏一个就是一颗定时炸弹。6.3 lwIP 内存池和描述符数量怎么配lwIP 的内存配置决定协议栈能同时处理多少个包。默认配置偏向节省内存适合小型物联网应用但工控现场如果同时有多个 TCP 连接、周期性报文和突发数据默认值很容易在峰值时丢包。几个关键的配置项配置项作用我的建议值PBUF_POOL_BUFSIZE单个 pbuf 的缓冲区大小1536PBUF_POOL_SIZE接收 pbuf 池的数量20~40MEM_SIZE堆内存大小根据应用调整TCP_WNDTCP 接收窗口超过一个 MSS 的整数倍即可6.4 中断优先级和线程优先级怎么排RT-Thread 里 lwIP 的接收线程默认优先级在 10 左右网络中断优先级一般要高于线程优先级。但中断优先级也不能设太高否则会频繁打断实时任务的执行。我的经验值enet 中断优先级比 SysTick 略低比串口中断高。这里还有一个细节RT-Thread 的 eth 框架里eth_device_ready触发的接收线程优先级直接决定数据从网卡到协议栈的响应速度。如果这个线程优先级太低在系统繁忙时网卡 buffer 可能被塞满后面到的帧只能丢弃。工控项目如果没有硬实时要求保持默认优先级即可如果担心把接收线程优先级提到比普通应用线程高一级。7. 移植完成之后我自己留的几个习惯7.1 PHY 寄存器访问先做成调试命令以太网驱动调试过程中读写 PHY 寄存器是最频繁的操作。我建议把 MDIO 读写封装成 RT-Thread 的 MSH 命令这样在运行时可以直接输入命令读某个 PHY 寄存器的值改起来非常方便。比起每次改代码加打印、重新编译烧录效率高太多了。我在工程里加了两个简单的命令phy_read addr reg和phy_write addr reg value整个调试过程基本脱离了对调试器的依赖。后面在产线或者现场排查网络问题时这个命令也成了保留的排查工具。RT-Thread 的 MSH 命令导出也很简单一个宏就能搞定MSH_CMD_EXPORT_ALIAS(phy_read_cmd, phy_read, read phy register); MSH_CMD_EXPORT_ALIAS(phy_write_cmd, phy_write, write phy register);7.2 把网络状态上报给应用层工控项目里应用层常常需要知道网络是不是通的。比协议栈层面感知到 TCP 断开更快的是 PHY 的物理链路状态变化。PHY 的 BMSR 寄存器里有 link status 位状态一旦变化驱动应该在第一时间读取并通知上层。RT-Thread 的 netdev 框架提供了 link up/link down 的回调机制。在驱动里实现一个周期性检查任务每隔一段时间读一次 PHY 的 link 状态寄存器发现变化就调用 netdev 层的状态更新函数。这样应用层可以实时在界面上看到网络状态断线时也能立刻进入本地缓存模式或者报警。实际使用中很多工控项目要求网络断开后设备继续运行恢复后自动重连。这个状态上报机制是整个功能的基础。没有它应用层只能靠 TCP 连接超时去猜测链路状态往往要等几十秒远远达不到现场要求。7.3 驱动代码的版本管理习惯最后说一个工程上的习惯。驱动调试通过后我会把整份板级相关的配置、PHY 型号、地址、复位时序、RMII 时钟来源、以及调试过程中确认过的关键寄存器值全部写进代码注释的头部同时更新到项目的版本管理提交信息里。很多人觉得注释写这些太啰嗦实际上过三个月再看这些信息能节省大量回忆时间。尤其是在多块相近的板卡上维护不同 PHY 的情况硬件版本号不同、PHY 地址不同、甚至 RMII 时钟方向不同如果这些关键参数没有在代码里留档换块板子就变成一次重新排查的灾难。我在上一家公司就是这样吃过亏后来定下规矩每块板子的 PHY 配置独立成文件文件名带上板卡版本号代码仓库里一眼就能看出每块板子的差异。这次调试 GD32H759 的 enet 驱动说起来是移植一个官方外设驱动实际走下来遇到的问题一点都不少。最深的体会是Cortex-M7 平台的网络驱动真正难的不是寄存器操作而是 Cache 一致性、DMA 描述符管理、中断与协议栈线程的配合这些系统工程问题。把这些底层机制理清楚之后换 PHY、换板子、甚至换芯片平台都只是重新适应一遍接口的问题了。