
做工控项目网口几乎是标配。上位机要通讯、车间要数据采集、设备要远程升级都靠一根网线。这次我在GD32H759上跑RT-Thread把enet以太网驱动从零调通中间把lwIP协议栈也过了一遍过程挺折腾但收获也大。今天把这些东西整理出来给准备用GD32H7系列做以太网功能的工程朋友当参考。这篇内容适合两类人。一类是已经拿到GD32H759开发板、正准备移植网口驱动但不知道从哪下手的另一类是做过别的平台网口驱动想快速了解RT-Thread的enet框架长什么样的。我尽量把硬件原理、驱动结构、代码路径、调试方法串成一条线讲不搞那种拿手册截图凑数的水文全是自己动手调过的经验。1. 方案选型与整体思路为什么是GD32H759 RT-Thread1.1 GD32H759这颗芯片的定位先说说为什么要选GD32H759。工业控制场景里主控芯片的考量点跟消费电子完全不同。消费电子追求跑分和性价比工控追求的是外设齐整、接口稳定、供货周期长还有最关键的一点芯片厂商的技术支持跟得上。GD32H759是兆易创新Cortex-M7系列里的旗舰型号主频最高550MHz这个性能在工控圈子里意味着什么意味着你可以在一个核上同时跑实时控制任务和网络协议栈不用像M4那样开个TCP连接就担心CPU资源被吃光。片上外设也是照着工业场景堆的。双Bank Flash设计非常实用跑程序的同时可以擦写另一个bank做远程固件升级的时候不用停机大容量SRAM给网络协议栈当缓冲池完全够用不需要外扩RAM硬件加密和真随机数发生器在工控通讯加解密和身份认证场景里也能派上用场。我选这颗料还有个原因就是它的以太网MAC是完整的千兆MAC内部还集成了DMA控制器可以直接访问片内SRAM外围电路只要一颗PHY芯片加网络变压器就能把网口做出来成本和时间都省了一大截。不过选芯片从来不是只看参数。我拿GD32H759对比过几颗同类M7芯片有的虽然带MAC但DMA描述符数量受限高流量下容易丢包有的库文档和寄存器手册写得很潦草位定义都对不上。GD32H759的英文参考手册也不是十全十美但至少寄存器描述、时序图、DMA流程写得清清楚楚对于要手写驱动的工程师来说这比什么都重要。1.2 RT-Thread在工程里的角色硬件选好了软件层怎么搭这次用的是RT-Thread。它是个开源实时操作系统在工控圈子里用得非常广组件生态比较成熟更重要的是它把网络这套东西集成得不错内核带完整调度驱动框架带设备模型网络组件带lwIP协议栈还有FinSH控制台可以边调试边看状态。在工控项目里RT-Thread带来最大的好处是可维护性。裸机写以太网驱动你会陷入别人看不懂、自己也看不懂的泥潭用RT-Thread之后驱动按设备模型去注册网络应用层按socket接口去开发整个工程的分工一下子清晰了。还有一个实际好处RT-Thread社区和商业服务都很活跃遇到问题能找到人问这对产线项目来说非常关键——故障处理的时间就是钱。1.3 enet驱动在整条链路中的位置很多人一开始会把enet驱动想得很神秘其实它就是一块翻译层。上层是lwIP协议栈要吃IP包要发IP包下层是GD32H759的ENET硬件能收发的是原始以太网帧。驱动的工作就是两头对接把lwIP交下来的IP包加上以太网头、算好校验、塞进DMA描述符把硬件收上来的原始帧解析出有效载荷往上交给lwIP。如果你把整条网络链路比作快递系统lwIP是快递公司的调度中心负责分单、路由enet驱动是分拣员负责把包裹搬到正确的货车上DMA是传送带MAC是货车司机PHY是路。任何一个环节堵了快递就送不出去。所以调驱动的时候要学会判断问题出在哪个环节这是后面调试章节的重点。2. ENET 外设结构拆解MAC、PHY、DMA 和描述符环2.1 硬件通路一帧数据从内存到网线以太网控制器这个外设内部其实分成三块MAC核、DMA控制器、以及PHY管理接口。我习惯把数据通路拆成发送和接收两条线来理解。发送方向CPU在内存里准备好一帧数据把缓冲区地址告诉DMADMA把数据搬到MAC的FIFO里MAC按802.3协议自动加上前导码、帧起始定界符、源MAC地址这个在寄存器里配好、帧头校验和帧尾CRC最后通过MII或RMII接口一个bit一个bit地发给PHY芯片PHY把收到的数字信号调制成差分模拟电平从网线传出去。接收方向完全反过来PHY从网线收到模拟信号解调成数字bit流按接收时钟送给MACMAC判断帧边界、校验CRC、过滤地址把有效数据写进FIFODMA再把FIFO里的数据搬到内存里的接收缓冲区然后写描述符触发中断告诉CPU有包到了。理解这个通路有多重要调试的时候你会发现很多问题不是驱动代码写错了而是某个环节没配对——比如DMA搬完数据但PHY没收到时钟或者MAC配了千兆而PHY工作在百兆两边节奏对不上数据就丢了。2.2 PHY芯片与MDIO管理接口PHY芯片是MAC和网线之间的物理层器件负责编解码、时钟恢复、链路协商。这次板上用的是常见的千兆PHY工作在百兆RMII模式。很多初学者会忽略PHY和MAC之间还有个管理通道就是MDC和MDIO两根线。MDIO协议很简单类似I2C时钟线MDC由MAC提供数据线MDIO双向传输通过帧格式来读PHY的寄存器。PHY的寄存器地址空间是标准的前16个寄存器基本各厂家都兼容。最常用的是寄存器0BMCR控制复位、自动协商、速度、双工。寄存器1BMSR状态寄存器能看到链路是否连接、协商是否完成。寄存器2、3PHY ID用来识别PHY芯片型号。驱动启动时第一件事就是通过MDIO读PHY ID确认PHY型号和驱动期望一致然后写BMCR开启自动协商接着轮询BMSR等链路状态变成已连接。这个流程写在驱动初始化里跑不通就直接卡在第一步所以我把PHY检测写成了独立函数先单独验证。2.3 DMA描述符环驱动的心跳GD32H759的ENET DMA用的是描述符环结构这是驱动最核心的数据结构。所谓描述符就是一块内存区域里面记录这个缓冲区的地址、长度、状态、下一个描述符指针。一帧数据要发送或接收CPU和DMA之间就是靠这些描述符传递信息的。以接收方向举例。初始化的时候驱动会创建一张RX描述符环每个描述符关联一个接收缓冲区。DMA收到一帧数据先找到环中当前要写入的描述符把数据搬到对应缓冲区然后更新描述符状态位再把当前描述符指针推进到下一个。CPU在处理中断时就是通过扫描描述符的状态位来判断哪个缓冲区有新数据。这里有个性能相关的关键点发送方向CPU准备好了数据把描述符填好让DMA搬数据但如果数据还没发完DMA还会占用这个描述符你在重填之前必须确认状态位已经清掉。很多丢包和崩溃问题都是发送描述符被覆盖造成的。后面调试章节我会细说。3. 驱动移植实录从引脚配置到lwIP对接3.1 引脚和时钟配置先跑通硬件再说写驱动前先把原理图捋清楚。我用的板子是RMII接口共需要9根信号线TXD0、TXD1、TX_EN、RXD0、RXD1、CRS_DV、REF_CLK再加上MDC和MDIO。GD32H759手册里给了复用功能映射把这些脚全部配置为ENET的AF功能。值得一提的坑输入输出方向千万别配错。MDC、TXD、TX_EN、REF_CLK是输出MDIO、RXD、CRS_DV是输入。我第一版GPIO配置把MDIO配置成了推挽输出结果MDIO上的时序完全乱掉PHY ID读出来全是0xFF。这种错误用示波器一眼就看出来但纯靠代码排查会卡很久。时钟方面RMII模式要求PHY提供50MHz的REF_CLK或者由MAC侧输出50MHz。GD32H759可以通过系统时钟PLL分频产生50MHz再把引脚绑定到合适的复用功能上。注意RMII接收方向是源同步的PHY会把50MHz的参考时钟和RXD数据同步送过来MAC靠这个时钟采样数据。如果REF_CLK频率不对PHY和MAC两边根本没法对齐数据表现为链路能link up但一收包就CRC错误。核心代码的逻辑大概是这个样子static void enet_gpio_config(void) { rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_GPIOC); rcu_periph_clock_enable(RCU_ENET); /* RMII: MDC、MDIO、TXD0、TXD1、TX_EN、RXD0、RXD1、CRS_DV、REF_CLK * 按原理图实际引脚配置为 ENET 复用功能 */ gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_1 | GPIO_PIN_2); gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_1 | GPIO_PIN_2); gpio_af_set(GPIOA, GPIO_AF_5, GPIO_PIN_1 | GPIO_PIN_2); /* 其余引脚按同样方式配置 */ }注意GD32H7不同型号、不同封装的复用功能编号未必一样务必对照芯片手册Alternate function mapping那节去核对。我在这个项目里就发现手册里某个引脚的AF编号跟固件库头文件里的宏定义有出入遇到这种看起来都对、实际完全不对的情况最好是拿示波器戳引脚量一下看时钟和信号到底有没有出来。3.2 PHY初始化和链路检测确认物理层活着引脚配好以后先用MDIO总线把PHY读通。这一步建议单独写一个小函数不要跟后面的DMA初始化混在一起。原因很简单如果MDIO都读不通后面的分析全是空谈。我习惯在PHY初始化里打印寄存器值确认读到的PHY ID跟芯片手册里的一致。MDIO读寄存器的核心逻辑大概是这样static uint16_t enet_phy_read(uint32_t phy_addr, uint32_t reg_addr) { /* 构造MDIO读帧起始位、操作码、PHY地址、寄存器地址 */ enet_phy_write_cmd(ENET_PHY_READ, phy_addr, reg_addr); while (ENET_DMA_STAT ENET_DMA_STAT_MDIO_BUSY) { } return (uint16_t)(ENET_DMA_STAT ENET_DMA_STAT_MDIO_DATA_MASK); }接下来复位PHY、开自动协商。PHY复位是写PHY寄存器0的bit15然后等它自清。自动协商开启后PHY会自动跟对端交换能力信息确定双方都支持的最高速率和双工模式。链路检测就是不停地读寄存器1的bit2这个位叫链接状态位为1说明网线插好、链路协商完成。static uint8_t enet_phy_wait_link(uint32_t timeout_ms) { while (timeout_ms--) { if (enet_phy_read(PHY_ADDR, PHY_REG_BMSR) PHY_BMSR_LINK_STATUS) { return 1; } rt_thread_mdelay(1); } return 0; }这个循环看起来很土但在工控任务里很实用。链路一旦掉线你需要在应用层立刻感知到而不是等TCP超时——很多冗余控制、断线重连逻辑都依赖这个状态。这里可以做得更细不只有链路状态还要读出协商完成的速率和双工配置到MAC寄存器里。RMII百兆模式下MAC必须按100M全双工来设置如果自动协商结果跟MAC配置不一致表现出来的问题通常是发包正常、收包不通或者反过来。3.3 描述符环初始化给DMA搭好舞台链路通了以后才轮到真正复杂的部分DMA描述符环。GD32H759的ENET DMA支持链式描述符结构描述符本身是内存中的一块连续区域每个描述符保存缓冲区地址、控制位、状态位以及指向下一个描述符的指针。RX和TX是两个独立的环。初始化RX环我要做这些事为每个描述符分配一个接收缓冲区缓冲区大小要大于等于最大以太网帧长。我用的是2048字节兼容1518字节标准帧再多留一点余量给VLAN标签或CRC工作区。初始化每个描述符的缓冲地址、清空状态标志。把最后一个描述符的链表末尾位置位让DMA知道这里是环的终点。设置DMA接收描述符链表首地址寄存器指向环的第一个描述符。使能DMA的接收状态让硬件可以在帧到达时自动写缓冲区。TX环类似但初始化时不用预先分配缓冲区只在发包的时候才把应用层的缓冲区地址填进去。注意TX描述符有个OWN位所有权位置1表示DMA拥有这个描述符CPU不能碰DMA发送完成后会清掉这个位把控制权交还给CPU。所以发包前要检查这个位发完之后也要等它被清掉才能复用。关于描述符数量这里给个经验值RX环上我用16个描述符TX环用8个。RX比TX多是因为接收方向一旦描述符耗尽后续帧就会被硬件丢掉属于不可恢复的损失发送方向则可以在驱动层等一会儿重试所以数量可以少一些。如果你的应用有瞬时大流量RX描述符数量可以再翻倍代价是SRAM占用增加每个描述符就要吃掉一块2KB的buffer16个就是32KB这个账要在项目初期就算清楚。3.4 中断处理与收发路径驱动的主干DMA跑起来后驱动的主干就是中断处理和收发函数了。GD32H759的ENET中断可以分成几类接收完成中断、发送完成中断、链路状态变化中断、错误中断。在RT-Thread的驱动里我做了简单的分工接收和链路状态处理放在中断上下文发送则放到应用调用点直接触发。接收中断处理函数大概是这个思路void eth_irq_handler(void *param) { rt_base_t level; while (rx_desc_is_owned_by_dma() 0) { frame_len rx_desc_get_frame_len(); pkt pbuf_alloc(PBUF_RAW, frame_len, PBUF_RAM); memcpy(pkt-payload, rx_desc_get_buffer(), frame_len); rx_desc_set_owner_to_dma(); /* 还回所有权 */ level rt_hw_interrupt_disable(); netif-input(pkt, netif); /* 交给lwIP处理 */ rt_hw_interrupt_enable(level); } }这段代码有几个细节第一用一个while循环把环里所有已收到的帧都处理完不要每次都只处理一帧就退出中断否则高流量下很容易丢包第二把数据拷贝到lwIP的pbuf里拷贝开销在百兆甚至千兆下其实不小但胜在安全——拷贝完成就能把接收缓冲区还给DMA不用等lwIP处理完避免DMA停在同一个描述符上第三调用netif-input要关中断或者用锁保护防止lwIP内部状态被并发访问搞乱。发送路径跟接收类似把lwIP下来的pbuf地址填到TX描述符里置OWN位然后写DMA的发送请求寄存器让硬件立刻把数据搬出去。发送完成中断可以等也可以靠轮询状态位。在工控场景里我建议所有发送都走同一把锁避免多个线程同时调用netif输出函数导致描述符竞争。3.5 对接RT-Thread netdev框架GD32H759的驱动代码写好后最后一步是接进RT-Thread的网络框架。老版本RT-Thread用netdev和eth_device两层模型新版主要用netdev加网卡设备。简单说你要实现一套操作函数集netdev_ops让协议栈知道怎么初始化网卡、怎么发、怎么读还要把网卡信息IP、掩码、网关、MAC通过netdev管理起来。我推荐不要自己造框架直接用RT-Thread官方提供的netdev接口。驱动注册的流程是先调用初始化函数把网络的netdev结构体初始化填好ops再通过rt_netdev_add把设备挂到系统中。这样lwIP在枚举网络接口时就能找到你的网卡DHCP客户端也能直接跑在之上。static struct netdev_ops enet_netdev_ops { .init enet_netdev_init, .open enet_netdev_open, .close enet_netdev_close, .link_status enet_netdev_link_status, .set_mac enet_netdev_set_mac, ... }; struct netdev *netdev enet_netdev; netdev-ops enet_netdev_ops; netdev-hwaddr_len 6; memcpy(netdev-hwaddr, mac_addr, 6); rt_netdev_add(netdev);接入netdev之后你可以用FinSH命令行直接查网口状态这在调试阶段特别方便。输入ifconfig能看到网卡有没有upIP有没有拿到输入ping xxx能测通不通。这套调试体验裸机开发想都不敢想。4. 调试实录5个典型问题和排查方法4.1 能link up但DHCP拿不到地址这是我调这个驱动时遇到的最典型的怪问题。链路状态明明是通的PHY也协商好了但DHCP一直在发DISCOVER就是收不到OFFER。排查下来发现问题出在MAC的地址过滤设置上——MAC默认可能在某种混杂模式也可能没开启接收所有广播帧导致DHCP请求的响应包在硬件层就被丢了。这种情况先用抓包工具看网线上到底有没有返回包。如果线上有返回包但lwIP收不到就检查MAC地址过滤寄存器。很多MAC控制器默认只接收发给本机MAC帧和广播帧对于DHCP这种先发广播、在后续阶段才拿到单播地址的协议得确保广播帧和当前MAC地址都配置正确。我最后把MAC地址写到MAC地址寄存器并且开启了接收广播帧位问题就消失了。4.2 收包正常发包一多就丢这种问题多半出在TX描述符管理上。我的一个错误是把一个描述符装满了数据后没有等上一次DMA把它发完就重新填了数据导致第一帧的后半段被覆盖对端收到CRC错误或者帧被丢弃。后面我改成发送前必查OWN位如果上一次发送没完成就返回忙或者等一拍再发问题明显缓解。TX发送丢失的另一个常见原因是DMA没有真正启动发送或者发送描述符链表指针没有指向正确位置。我曾在初始化TX环之后忘记写DMA的发送描述符链表地址寄存器结果所有发送都被DMA悬空表现得像是网口只收不发。这种低级错误靠日志和寄存器回读排了半天才发现。4.3 PHY ID读不到PHY ID读不到先别怀疑PHY芯片坏了。排查顺序是先量MDC时钟有没有出来再量MDIO线上有没有数据变化然后检查GPIO方向是不是配错了。我前面提到过MDIO配成输出的那次数据线上永远是高电平PHY回的应答全部被总线冲突吞掉。还有个小坑有些PHY需要先拉高复位脚或者复位后要等几十毫秒才能响应MDIO命令。我用的这颗PHY手册里写了复位后至少等50ms我没看就急着初始化结果也读不到ID。把复位延时加上后一切正常。PHY初始化慢是常态不是bug别一上来就怀疑代码。4.4 CPU占用过高网口驱动调通后我用FinSH一看lwIP线程CPU占用率动不动就80%多这在工控现场完全不能忍。排查发现我的接收中断处理里做了太多事每次从描述符取帧、申请pbuf、拷贝数据、释放缓冲区全都挤在中断里。短期功能能用但流量一高系统就卡。后来我把大部分工作挪到了专用收包线程里中断里只标记有包到了用信号量唤醒线程去处理实际的RX流程。这个改动显著降低了中断持有时间CPU占用率也降了下来。这里要提醒一点在RT-Thread里不要轻易让lwIP的input回调直接跑在中断上下文除非你很清楚自己在做什么。4.5 协议栈接口问题速查最后整理一个我踩过的协议栈接口速查表方便遇到类似问题的人直接对照排查现象优先排查项常见原因网卡状态downnetdev状态、驱动open回调驱动没有正确调用netdev开启逻辑拿不到IPPHY链路、MAC过滤、DHCP客户端线程广播帧被过滤、PHY协商未完成能ping通别人别人ping不回MAC地址配置、ARP回复路径本机MAC没有正确注册到netdevTCP连接建立慢重传超时配置、接收窗口lwIP的TCP定时器周期不合理重启后MAC地址变化MAC地址存储区没有用唯一ID生成MAC每次随机高负载下系统卡死中断优先级、锁使用中断里调用了可能阻塞的函数这张表不是我凭空写的全部来自这两个星期的实际调试记录。有些问题看起来复杂最后定位到原因其实很基础所以排查一定要按物理层、链路层、协议栈、应用层的顺序来切忌跳过步骤直接猜。这台项目到现在enet驱动已经稳定跑了快一个月现场压测下丢包率几乎为零CPU占用也到了可以接受的范围。回头再看整个调试过程我觉得做网口驱动最考验人的不是寄存器怎么写而是问题定位的顺序。你先搞清楚信号有没有出来、帧有没有到MAC、DMA有没有搬到内存、lwIP有没有收到再往下追局面就清楚了。最后再分享一个小技巧调试阶段一定要在RX和TX路径的关键节点加上日志和计数变量比如收到原始帧数DMA中断次数lwIP丢弃次数。不要嫌它难看等你遇到时好时坏的问题这些计数能帮你把故障范围缩小一大半。等驱动稳定了再把日志开关关掉或者在FinSH里做成动态命令现场维护的时候还能继续用。如果后面有机会我会再写一篇如何在这个驱动基础上做Modbus TCP网关和远程固件升级的实战文章那才是工控场景里真正落地的东西。