资讯动态

GD32H759 + RT-Thread以太网驱动移植与调试实战

发布时间:2026/9/19 12:15:55 来源:尧图企业网站定制
做工控的逃不过联网这一步。就算你做的只是一台电机控制器或者数据采集盒子现在甲方也普遍希望能在办公室电脑上直接看到现场数据。我手头这个基于 GD32H759 搭 RT-Thread 的项目就是这样前期环境、时钟、串口和 GPIO 都理顺了下一步必须把以太网跑起来。这个系列的第 1 篇讲了工程搭建和基础外设这一篇就专门啃 ENET 驱动也就是 GD32H759 的以太网 MAC 外设配上外部 PHY 芯片再接入 RT-Thread 的 lwIP 协议栈最终目标是板子一上电就能拿 IP、能 ping 通、能跑 TCP/UDP 业务。说实话ENET 驱动在 GD32 的整棵外设树里算是比较复杂的模块涉及引脚复用、时钟树、MAC 控制器、DMA 描述符、PHY 管理、协议栈对接这几层。很多朋友卡在“明明是照着参考代码写的为什么就是不通”这个阶段本质是这几层中间有细节被忽略。这篇文章我就从我的实际调试过程出发把驱动拆开讲清楚尽量把容易踩的坑都摆出来。适合正在做 GD32H759 RT-Thread 以太网开发的同学参考也适合想从裸机以太网过渡到 RTOS 以太网的朋友。1. 项目整体拆解这一篇到底在做什么1.1 为什么是 GD32H759 RT-Thread 这个组合先交代一下背景。GD32H759 是 Cortex-M7 内核主频能跑到比较高的水平片上资源很丰富尤其是带硬件以太网 MAC这对工控设备来说太关键了。很多工控场景不光是采集 IO 和控制电机还需要把设备接入现场总线或者工厂局域网以太网几乎是标配。用 RT-Thread 是因为它成熟、组件丰富而且 lwIP 协议栈的集成度很高不用自己从头去写 TCP/IP 那套复杂的东西。这个组合最吸引我的一点是GD32H759 的 ENET 外设支持 RMII 和 MII 两种接口模式配合外部 PHY 芯片可以用很少的引脚实现 10M/100M 以太网通信。在工控主板上引脚资源很紧张RMII 模式只需要 7 根信号线加 2 根管理线比 MII 省了一半。所以这篇文章我会重点以 RMII 模式为例。1.2 ENET 驱动要解决的三件事MAC、PHY、协议栈对接在动手写代码之前一定要把一个概念理清楚GD32H759 片上的 ENET 模块只是 MAC 控制器它本身不能直接收发网络信号必须通过 MII/RMII 接口连接一颗外部 PHY 芯片再接网口变压器和 RJ45。所以“ENET 驱动”严格来说包含三部分工作第一是 MAC 配置。需要初始化 GD32H759 的 ENET 控制器设置工作模式全双工/半双工、速率、MAC 地址、帧过滤规则、流控策略以及 DMA 传输模式。这部分寄存器比较多但流程很固定。第二是 PHY 管理。MCU 通过 MDIO/MDC 两根线访问 PHY 芯片的内部寄存器读取链路状态、配置自协商、获取速度和双工模式。不同 PHY 的寄存器布局大同小异但要特别注意 PHY 地址和复位时序。第三是协议栈对接。RT-Thread 的 lwIP 组件跑在 MAC 之上驱动要提供“收包上送”和“发包下发”的接口。也就是说网卡收到数据要通知 lwIP协议栈要发送数据时驱动能把帧塞进 DMA 描述符发出去。这三个部分环环相扣任何一个环节断掉表现都是“网络不通”。所以调试的时候也要按这个链路分层排查不要一上来就怀疑协议栈。1.3 这篇内容适合谁看如果你是刚接触 GD32 以太网开发这篇文章可以帮你把整个驱动框架搭起来并且理解每个初始化步骤为什么会存在。如果你已经在用 RT-Thread 做项目但网络一直不稳定那第 4 章的排查实录和速查表应该能帮你少走不少弯路。如果你用的是其他型号的 MCU只要也是 Cortex-M 内核加 RMII 接口这套思路同样可以平移。2. enet 驱动移植前先把环境和硬件看明白2.1 确认硬件连接与 PHY 选型这一步看着基础但很容易被忽略。我见过不止一个朋友代码写得没问题结果发现是板子上的 PHY 芯片地址和代码里写的不一致。不同 PHY 芯片的上电默认地址不一样比如有的芯片把地址引脚拉高拉低可以配出不同地址你要从原理图上确认硬件到底把 PHY 地址设置成了多少。以我用的板子为例外部接的 PHY 芯片地址是 0x01。这个地址通常由 PHY 芯片的地址配置引脚决定比如 LAN8720A 的默认地址可以通过 PHYAD0 引脚配置硬件上拉就是 0x01下拉就是 0x00。所以代码里mdio_read(phy_addr, reg)的第一个参数不是随便写的必须和原理图对应。另外要看清楚 RMII 的时钟供给方式。RMII 模式需要一个 50MHz 的参考时钟这个时钟可以由外部有源晶振提供也可以由 PHY 芯片自己产生并输出给 MCU还有少数设计是从 MCU 输出时钟给 PHY。不同方案下代码里时钟树的配置逻辑完全不同。我这边板子的设计是从 PHY 芯片引 REF_CLK 到 MCU所以 GD32H759 的 ENET 引脚里那个和 REF_CLK 相关的引脚要配置成输入模式而不是输出时钟。2.2 引脚复用与时钟树配置GD32H759 的 ENET 引脚复用比较讲究因为同一组外设功能可能分布在多个引脚上你需要对照数据手册的 AFIO 映射表逐一确认。在 RMII 模式下典型的引脚分配大概是ETH_MDCMDIO 管理时钟一般是 PA2ETH_MDIOMDIO 管理数据一般是 PA3ETH_REF_CLK参考时钟 50MHz一般是 PA1ETH_CRS_DV载波检测/数据有效一般是 PA7ETH_RXD0接收数据位 0一般是 PC4ETH_RXD1接收数据位 1一般是 PC5ETH_TXD0发送数据位 0一般是 PG13ETH_TXD1发送数据位 1一般是 PG14ETH_TX_EN发送使能一般是 PG11不同封装、不同板子会有差异千万不要照抄。我的习惯是在原理图上把每个网络标号对应的 MCU 引脚画出来再对照芯片手册的复用表一条一条核对。引脚复用配置错了后续所有调试都白搭。时钟树方面除了 RMII 需要的 50MHz 参考时钟ENET 外设的 APB 时钟也要打开。GD32H759 的以太网 MAC 和 DMA 挂在不同的时钟总线上具体要看参考手册。我踩过的坑是只开了 MAC 时钟忘了开 DMA 时钟结果初始化时读写 DMA 寄存器全无反应一度以为是芯片坏了。2.3 软件工程准备软件这边我在第 1 篇里用 RT-Thread Studio 建好了基础工程这一步就是在这个工程基础上加上 ENET 相关的驱动文件。如果你用的是 Keil RT-Thread Env流程也类似先把 lwIP 组件和 eth 驱动组件加进来再把 GD32 的 ENET 库文件添加到工程。需要注意RT-Thread 的 lwIP 组件对内存的需求会比裸机大不少。默认的堆大小可能要调大尤其是后面要跑 TCP 多连接的时候。我习惯把RT_LWIP_TCP_SND_BUF和RT_LWIP_TCP_WND这些宏稍微调大一点但也不能无限大因为 GD32H759 的 RAM 虽多还要留给业务逻辑和 DMA 描述符缓冲。具体的平衡建议我放到第 5 章讲。3. 驱动核心实现MAC/DMA/PHY 三条线逐个打通3.1 MAC 初始化与工作模式配置ENET 驱动的第一步是初始化 MAC 控制器让它知道“我工作在什么模式”、“用什么速率”、“怎么收发数据”。这里我建议按下面的顺序来不要乱跳先给 ENET 外设和相关的 DMA 时钟使能再把 PHY 芯片的复位引脚拉低再拉高给 PHY 一个可靠的复位。PHY 复位后需要等待一段时间通常是几毫秒到几十毫秒具体看 PHY 数据手册我一般延时 10ms 以上。接着配置 MAC 的帧过滤寄存器一般叫 MACFFR决定接收哪些帧。比如单播地址过滤要开广播帧要接收组播根据需求决定。如果工控设备需要被任意主机访问那可能还要开启混杂模式不过生产环境不建议长期开着。然后是 MAC 控制寄存器MACCR。这里要设置接口模式是 RMII还要设置全双工还是半双工、速度是 100M 还是 10M。如果你打算让 PHY 自协商那 MAC 这边最好也保持和自协商结果一致。实际驱动里一般是在 PHY 自协商完成后再把 MACCR 里的速度和双工位更新成 PHY 上报的结果。这里有个很关键的细节MACCR 里的软件复位位SWR在初始化时一定要先置位然后等待硬件自动清零这代表 MAC 内部状态机复位完成。如果这个等待超时说明外设时钟或者总线配置有问题要继续检查时钟树。3.2 DMA 描述符设计与缓存一致性处理ENET 驱动里最容易被绕晕的就是 DMA 描述符。GD32H759 的 ENET 发送和接收各使用一个描述符链表每个描述符包含 4 个 32 位字分别是控制/状态、缓冲区地址、缓冲区地址扩展等。驱动要做的就是维护一个发送描述符环形队列和一个接收描述符环形队列。接收链路的工作流程是这样初始化时把每个接收描述符指向一个缓冲区并设置所有权位OWN 位为硬件所有。当 PHY 收到数据并通过 DMA 写入缓冲区后硬件会清掉 OWN 位然后触发中断或者状态标志。驱动发现接收描述符的 OWN 位被清除就说明有新包到了可以把缓冲区里的数据交给协议栈然后重新分配缓冲区再把 OWN 位设置回去让硬件继续使用这个描述符。发送链路相反驱动要发数据时把数据地址填到发送描述符里设置数据长度和 OWN 位然后告诉 DMA“可以发送了”。硬件发送完成后会清除 OWN 位驱动收到完成标志后回收缓冲区。这里我重点提醒一个新手特别容易忽略的地方GD32H759 是 Cortex-M7 内核有 D-Cache。如果你的 DMA 缓冲区位于可缓存的 RAM 区域DMA 写入的数据可能还停留在内存里没有刷新到实际物理内存或者 CPU 读到的可能是 Cache 里的旧数据。这会导致一个非常诡异的现象寄存器配置全是对的但收到的数据总是乱码或者发出去的数据总是不对。解决办法有两个方向。一是把 DMA 缓冲区放在非 Cache 的 RAM 区域有些芯片有多块 RAM其中某块不支持 Cache可以直接使用。二是对缓冲区做 Cache 维护接收数据前 invalidate发送数据前 clean发送完成后 invalidate。我在 GD32H759 上用的是第二种配合描述符缓冲区的 8 字节对齐要求能稳定跑满速。3.3 PHY 管理接口与自协商轮询PHY 管理接口通过 MDIO 总线和 PHY 芯片通信。GD32H759 的 ENET 外设提供 MAC MII 管理寄存器你只需要按寄存器配置发起一次 MDIO 读或者写操作然后等待完成标志即可。底层时序由硬件完成驱动要关心的主要是读写的目标地址和寄存器地址。PHY 的自协商流程建议放在一个单独的线程或者状态机里轮询不要阻塞在初始化函数中。因为自协商通常需要一两秒如果放在 MAC 初始化后面会导致系统启动很慢甚至触发看门狗。我的做法是先完成 MAC 和 DMA 描述符的初始化注册好设备接口然后开一个 PHY 监控线程每隔几百毫秒读一次 PHY 状态寄存器检测链路是否建立速度和双工模式是否有变化。PHY 状态寄存器里最重要的几个位是链路建立状态位、自协商完成位、速度位、双工位。不同 PHY 芯片这些位的位置不一样要对着数据手册看。比如链路状态位就有高有效和低有效两种风格用错了就会永远读不到 Link Up。自协商完成后把协商出来的速度100M 还是 10M和双工模式全双工还是半双工同步到 MACCR。这一步一定要做否则 MAC 和 PHY 的工作模式不匹配可能出现能 link 上但数据传输一直出错的情况。3.4 对接 RT-Thread 的 eth 框架与 lwIP硬件层跑通之后剩下的工作就是把驱动挂到 RT-Thread 的网络框架上。RT-Thread 的 lwIP 协议栈提供了标准的以太网驱动接口你只要实现几个关键函数然后注册一个网卡设备协议栈就会通过回调来收发数据。我这边实现的 eth 接口大致是初始化函数负责构造 eth 设备结构体把 ops 指针指向自己实现的接口比如 init、open、close、link_change、tx、rx 相关的回调然后调用rt_ether_device_register把网卡注册为 eth0。注册完成后RT-Thread 的 netdev 组件会自动为这个网卡创建网络接口可以通过 DHCP 或者静态 IP 配置地址。收包路径可以走中断加信号量也可以直接收但更推荐中断加线程的方式。以太网中断里只做一件事把接收事件通过信号量通知给一个专用的收包线程。收包线程里遍历接收描述符把数据打包成 lwIP 的struct pbuf结构然后调用netif-input交给协议栈处理。这样处理的好处是协议栈里的耗时操作不会阻塞中断上下文系统响应更稳定。发送路径要简单一点lwIP 要发包时会调用驱动提供的tx回调驱动把 pbuf 中的数据复制到发送缓冲区或者直接让 DMA 读取 pbuf 的数据内存然后描述符交给硬件发送。最理想的是做零拷贝但 pbuf 的数据可能是不连续的实际调试中直接复制到连续缓冲区更简单稳妥牺牲一点性能换稳定性。4. 联调记录与问题排查速查4.1 第一次上电PHY link 不上我最初移植 ENET 驱动时第一个遇到的问题就是网口灯不亮ping网关也不通代码逻辑翻来覆去看都觉得没问题。后来用调试器读 PHY 的 Basic Status 寄存器发现链路状态位一直是 Down才知道根本没到协议栈那一步。排查链路问题时我一般先量 PHY 的主时钟是否起振。RMII 模式必须有 50MHz 参考时钟如果时钟没起来PHY 完全无法工作。其次是复位引脚很多 PHY 的复位是低有效如果硬件上没有正确连接或者驱动没有正确拉高PHY 会一直处于复位状态。再就是 MDIO 通信是否正常。可以读 PHY 的 ID 寄存器如果能读出一个非零的值说明 MDIO 通路没问题如果读出来全是 0 或者 0xFFFF那要检查 MDC/MDIO 引脚配置和 PHY 地址是否匹配。我遇到过地址写错的情况读 ID 读到的是旁边另一颗芯片的值排查了好久才反应过来。这里有个实用的排查技巧用示波器或者逻辑分析仪抓 MDIO 引脚的波形看有没有读写动作。如果波形一直静默说明 MCU 根本没发起访问问题在 MCU 这边如果有波形但 PHY 没有回应问题大概率在 PHY 的地址或者电源上。4.2 能 link 但 ping 不通或丢包链路起来了灯也亮了但ping的时候要么超时要么丢包这是第二个高频问题。这种情况说明 MAC 和 PHY 之间基本通信正常问题往往出在 DMA 描述符、缓冲区管理或者 MAC 过滤配置上。我遇到过一次典型场景板子能收到 PC 发来的数据PC 端 ARP 请求能触发 MCU 中断但 MCU 发的数据 PC 收不到。排查后发现是发送描述符的缓冲区地址没有做 Cache clean。因为 lwIP 构造的报文写在 Cache 里DMA 去内存读数据时读到的还是旧数据PC 收到的自然是垃圾帧。还有一种情况是丢包率很高接收描述符队列被耗尽。RTOS 环境下收包线程如果优先级太低可能长时间被其他任务抢占导致中断里虽然有信号量通知但收包线程迟迟得不到执行硬件就把后面的包丢了。我的调整思路是把收包线程优先级提高并且把接收描述符数量从默认的 4 个增加到 8 个或者 16 个缓冲深度够容错性就好很多。如果 PC 端ping第一次通后面全部超时还要考虑是不是 MAC 地址没有正确配置。有些驱动初始化时 MAC 地址全是 0导致网卡发出的请求无人应答。我习惯在驱动里把 MAC 地址写死成一个合法的单播地址并且确认 lwIP 拿到的是这个地址而不是随机生成的一个地址。4.3 HardFault 与描述符异常以太网驱动是 DMA 密集型外设最容易触发 HardFault 的地方就是缓冲区越界和描述符指针错乱。我调试时曾经遇到过跑几分钟突然 HardFault中断里查看现场发现接收描述符的缓冲区地址已经变成一个异常值猜测是入包太频繁某个描述符被重复使用缓冲区被覆盖了。这种问题很难靠肉眼看出逻辑错误我的排查办法是先保证描述符初始化时所有字段都清零再确保每个描述符的缓冲区都分配了足够的空间。GD32H759 的 DMA 接收描述符要求缓冲区按字节对齐具体对齐要求查手册但一般至少 4 字节对齐。如果你分配的缓冲区是普通数组编译器默认对齐可能不够最好使用 RT-Thread 的对齐宏来分配。还有一个经验是接收缓冲区的长度一定要留够。以太网最大帧长是 1518 字节加上 VLAN 标签可能到 1522如果缓冲区只给到 1518 字节碰到大包就会溢出。我一般直接给到 1600 字节以上宁可浪费一点内存也不要踩越界的坑。后面通过性能测试再慢慢调整到紧凑值。4.4 问题速查表故障现象可能原因排查/解决建议网口灯不亮PHY 寄存器读不到值PHY 复位引脚未释放/时钟未起振/MDIO 地址不对检查复位时序量 50MHz 时钟用调试器读 PHY ID 寄存器能 linkping 超时MAC 地址全 0/DMA 缓存一致性问题/过滤配置错误设置合法 MAC 地址检查 Cache clean/invalidate核对 MAC 过滤寄存器高负载时丢包接收描述符数量不足/收包线程优先级太低增加描述符数量提高收包线程优先级随机 HardFault缓冲区越界/描述符被覆盖/对齐不满足缓冲区长留余量描述符和 buf 严格对齐查 DMA 中断状态收发速率只有 10MPHY 自协商结果没同步到 MAC自协商完成后更新 MACCR 速度位和双工位DHCP 拿不到 IP协议栈到驱动链路未打通/收包回调没调用确认网卡已注册成功抓包确认 ARP 是否发出5. 性能验证与稳定运行建议5.1 回环测试与外网测速跑通 ping 只是第一步工控设备长期运行不能只看“能通”还要看“稳不稳”。我的做法是先做板子到 PC 的压力测试用 iperf 或者简单的 socket 测试工具打流量。PC 端开一个 TCP 服务器板子作为客户端持续发送数据观察吞吐率有没有掉坑丢包率是否为零。如果吞吐率明显低于理论值就要检查是不是轮询收包的方式太占 CPU还是发送路径上做了不必要的数据复制。我实测下来把中断收包 独立线程处理的方式改成中断 信号量唤醒线程后吞吐率有了明显改善CPU 占用也降了下来。如果你对实时性要求特别高可以再针对 DMA 描述符做零拷贝优化但前提是先保证数据正确性。然后是长时间稳定性测试。我会让设备连续跑 72 小时每小时记录一次丢包率和内存使用情况。以太网驱动常见的内存泄漏点在收包路径如果每次收到包都没有正确释放 pbuf长期运行会慢慢吃光内存。这个只能靠反复压测和调阅 RT-Thread 的内存统计接口去发现。5.2 内存与线程资源规划GD32H759 虽然 RAM 资源相比普通 Cortex-M4 芯片宽裕不少但 lwIP 本身是个“吃内存大户”。我建议在工程里给 lwIP 单独规划内存池而不是和其他业务共用一套堆。RT-Thread 的 lwIP 默认配置里 PBUF 池和内存堆的大小要按实际流量估算如果板子只是做 Modbus TCP 这种低频小包协议内存池可以给小一点把资源让给业务如果要跑文件传输或者日志上传就要把发送缓冲和接收窗口调大。线程资源方面以太网驱动至少会引入收包线程、PHY 监控线程加上 lwIP 自己的 tcpip 线程。这几个线程的栈大小要合理分配尤其是收包线程如果栈太小处理大包时可能会溢出。我给收包线程的栈一般是 2048 字节PHY 监控线程 1024 字节足够tcpip 线程按 lwIP 默认配置来。5.3 从 ping 通到业务落地驱动跑到这一步底层收发链路已经通了剩下的就是业务层面的活了。对我来说这个 ENET 驱动打通之后设备才有了真正的“工控联网”能力。后面可以在这个基础上做 Modbus TCP 从站、MQTT 数据上报、OTA 升级甚至预留一个简单 HTTP 配置页面。有一个小建议保留一条稳定的调试通道。以太网调试不像是串口那么直观有时候代码改坏了网络直接断开你连日志都看不到。我习惯在 ENET 驱动里加一个开关能通过串口命令开启或关闭 lwIP 的调试输出这样就算网口挂了还能通过串口摸清状态不用反复插拔调试器。我个人在实际项目中的体会是ENET 驱动移植难的不是某个寄存器而是整套链路里各层组件之间的配合。尤其从裸机思维切换到 RTOS 思维后中断、线程、缓存一致性这些问题都会浮出水面。把这篇里的三层架构MAC、DMA、PHY和协议栈对接捋清楚后面再遇到网络问题基本都能快速定位。等到第 3 篇我准备把 Modbus TCP 从站挂上去到时候再把应用层和这个驱动怎么配合的经验整理出来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价