
这块板子拿到手的时候我第一件事不是点灯而是把以太网跑起来。GD32H759这颗料Cortex-M7内核拉到550MHz带以太网MAC跑RT-Thread做工业控制说白了就是个“能联网的PLC大脑”。上一篇把工程框架和时钟树捋完了这一篇就集中火力啃enet驱动目标只有一个让板子的eth0在RT-Thread里被ifconfig认出来能ping通能收发TCP报文。很多人一提到“移植以太网驱动”就头皮发麻觉得要靠寄存器堆代码。实际上当你把硬件电路、PHY芯片、MAC、RT-Thread的驱动模型这四层拆开之后你会发现这件事的复杂度远低于预期但坑却一点不少。尤其是GD32H759这种带D-Cache的Cortex-M7芯片缓存一致性问题才是最难缠的这属于“文档里不会写、只有调板子才会撞见”的经验活。这篇东西适合下面这几类人看一是手里刚好有GD32H759或者其他GD32H7系列板子想把以太网用起来的二是想搞明白RT-Thread里eth设备到底是怎么跟LWIP交互的三是做工业网关、协议转换器这类产品需要在国产MCU上把有线网络搞稳定的人。我把整个驱动移植从硬件接线开始讲到最后跑通包括中间踩过的坑和程序里那些容易写错的地方整个过程尽量说人话别担心看不懂。1. 项目定位与方案选型1.1 为什么在工控场景中选 GD32H759 以太网先讲个基本判断工控场景里以太网不是可选项而是刚需。你要做设备数据采集、远程维护、上位机通信哪怕只是把现场设备连到SCADA系统以太网都是最通用的物理通道。以前很多设备用串口但串口天生速度慢、距离短、组网麻烦。Modbus TCP普及之后工业现场对以太网口的需求几乎是标配所以你选MCU的时候有没有带以太网MAC直接决定了后续产品的硬件成本。GD32H759在这个价位上把Cortex-M7、大容量RAM、以太网MAC都给了确实是一颗很适合做“边缘控制联网”的芯片。GD32H759的以太网MAC是千兆级别的支持百兆和千兆两种模式接口上可以做MII/RMII也能做RGMII跑千兆。很多人一看到“千兆”这两个字就兴奋觉得必须要上RGMII。但实际做工业产品我建议你先冷静一下大部分工控协议Modbus TCP、EtherNet/IP这些本质上跑百兆已经绰绰有余甚至很多现场设备只有10Mbps。百兆的网络用RMII接口只需要4根数据线加一根时钟PCB好画EMC好处理芯片面积小PCB布线省心成本也低。千兆RGMII虽然很香但接口复杂、引脚占用多、对PCB等长要求高不是必须。所以下面所有操作我都以RMII百兆方案来展开这也是绝大多数工控设备的合理选择。1.2 为什么基于 RT-Thread 而不是裸机裸机也能跑以太网我以前也这么干过但结论是你可以踩死自己。以太网驱动本身不难难的是协议栈。裸机方案要么你自己移植lwIP把信号量和网卡中断绑在一起调要么用厂家提供的裸机例程阉割版功能少得可怜。GD32官方例程里确实有lwIP的移植但那只是跑通demo要做成真正的产品任务调度、内存管理、多路socket并发这些裸机上写起来极其痛苦。RT-Thread解决的就是这个生态问题。它本身是个RTOS又集成了完整网络框架从下往上分别是驱动层、lwIP协议栈、socket层。你在实现驱动时只需要把“发一包数据”“收一包数据”这两个能力交给上层其他所有事比如TCP重传、IP分片、ARP缓存、内存池管理协议栈全干了。更关键的是RT-Thread里已经有了统一的eth设备驱动框架在STM32上写的驱动到GD32上改改底层寄存器就能复用这对做多平台产品的人来说省下的工作量可不是一点半点。所以整个项目的技术栈就是GD32H759芯片 RT-Thread操作系统 lwIP协议栈 自研enet驱动。这一篇我默认你已经把RT-Thread系统在板子上跑起来了内核时钟、串口打印都正常下面直接切入以太网这一层。1.3 本篇的边界只聊 enet 驱动可能有人会问你这是“第2篇”那以后是不是还有第三篇第四篇对后续我还会写怎么在这个基础上做Modbus TCP从站、怎么接PLC、怎么做远程固件升级。但这一篇的边界非常明确只讲驱动的移植和验证让以太网“通”起来。至于业务层那些协议那是后面的文章。我这里说的“驱动”是指整个链路中从GD32的ENET外设寄存器配置到RT-Thread的eth0设备注册成功的这一段。下层的PHY初始化算不算驱动算但PHY芯片本身有标准MDIO接口所以我的做法是把PHY探测和配置也依赖mdio读写统一放在drv_enet.c里。上层到lwIP之间的接口就是eth_device框架的那几个函数指针。到这步扎实了后面跑什么协议都顺手。2. 硬件环境和外设规划2.1 核心板/开发板选型我这边的板子是自己画的一块双层核心板主芯片GD32H759IKLQFP176封装外部挂了一颗8MB的QSPI Flash电源用一颗RT8295把12V降到3.3V板子上还带了RS485和CAN接口反正就是个典型的工控小主板。如果你手里是官方评估板逻辑一样只是引脚编号要对得上自己的原理图。这里我重点提醒一下在开始写驱动之前你手里的硬件原理图必须确认四样东西缺一样后面都会卡壳。第一以太网MAC和PHY之间用的是RMII还是MII引脚定义别猜以原理图为准第二PHY芯片的MDIO地址是多少这是由PHY的某个引脚上拉下拉决定的第三PHY的复位脚接到了MCU的哪个GPIO需要软件控制复位就得驱动里操作第四RMII的50MHz参考时钟由谁提供是PHY自己有源晶振还是MCU的MCO输出或者是外部无源晶振。这四样东西要是不清楚你那驱动写多久都调不通的。2.2 选用 PHYLAN8720A 还是 DP83848市面上常见的外置PHY芯片我个人用得最多的是LAN8720A和DP83848这两款。LAN8720A是瑞昱的性价比极高几块钱一颗RMII接口设计非常简洁很多开发板都在用它。它的缺点是温度范围是商用级如果你做的是户外工业设备环境温度动不动六七十度我不建议用它。DP83848是TI的工业级PHY支持RMII和MII自适应温度范围宽稳定性好很多工业控制板上都能看到它但价格贵一些封装也不小不太适合空间受限的板子。我的项目因为是在环境可控的车间里做设备联网就选了LAN8720A。它的MDIO默认地址是0但这取决于原理图上PHYAD0这个引脚的接法实不相瞒很多人卡在这个地址上后面MDIO读不到寄存器第一反应是驱动有问题结果查了半天是地址不对。驱动代码里我会用宏定义的方式来控制这个地址改起来方便。2.3 引脚分配与 RMII 连线细节RMII接口一共就9根线主控和PHY之间的数据通路不复杂但连线必须严格按照原理图来。为了便于理解我这里把关键的信号和引脚功能列一下ENET_RMII_TXD0 / ENET_RMII_TXD1发送数据线2位并行发送由MCU发送给PHY。ENET_RMII_TX_EN发送使能MCU拉高时表示总线上有有效数据。ENET_RMII_RXD0 / ENET_RMII_RXD1接收数据线2位并行接收。ENET_RMII_CRS_DV载波监听/数据有效PHY告诉MCU“收到了数据”。ENET_RMII_REF_CLK50MHz参考时钟RMII模式下收发两侧都靠这一个时钟对齐这是RMII最核心的信号。ENET_MDC / ENET_MDIOMDIO管理接口用于MCU读写PHY内部寄存器配置PHY状态和查询链路。引脚编号这一块GD32H759这颗芯片的GPIO复用功能非常丰富同一个以太网信号可能同时映射到好几组不同的引脚上所以你必须以自己板子的原理图为准。我在驱动里会把所有用到的GPIO配置集中放在一个enet_gpio_config()函数里到时候流水账一样列出来大家对着自己的板子抄就行。这里额外说一句RMII的REF_CLK 50MHz哪怕一丁点不干净都会导致丢包进而表现为网络时通时断。所以硬件的晶振电路布局上晶振尽量靠近PHY芯片地平面不要断开否则你后面驱动再怎么调都修不好这个问题。2.4 时钟、复位和电源设计以太网控制器本身是AHB总线上的一个外设它的工作时钟一般跟AHB时钟同频。GD32H759的AHB时钟可以跑到200多MHzMAC内部自己会做分频处理。所以你只需要在初始化外设的时候打开ENET所在的RCU时钟就好rcu_periph_clock_enable(RCU_ENET)。但如果PHY侧需要MCU提供50MHz时钟那这块时钟源还得单独处理。复位时序在工控环境里特别容易被忽略。PHY芯片上电之后内部寄存器和状态机需要一定时间才能稳定你必须在复位引脚释放之后再延时几十毫秒再去访问MDIO否则MDIO读回来全是0xFF或者垃圾数据。我通常的流程是拉低PHY_RST_GPIO→延时50ms→拉高→再延时至少100ms→才开始MDIO通信。很多工程师只在仿真器里跑没问题一旦掉电重启就“死”了大概率就是没处理PHY复位延时。3. RT-Thread 网络框架与驱动分层3.1 驱动框架总览RT-Thread的网络子系统是分层的理解这个分层结构是写驱动的基础。从上往下看最上层是用户程序调用的是一套标准的socket API比如socket()、connect()、send()。这些API的实现由lwIP提供lwIP是整个网络协议栈的核心它负责处理TCP/IP协议、ARP、ICMP等等一堆东西。再往下lwIP不关心MAC芯片具体是谁家的它通过一个抽象接口跟网卡驱动打交道这个抽象接口在RT-Thread里的落地就是struct eth_device。所以驱动要做的核心事情就是把自己包装成一个符合struct eth_device约束的设备注册到RT-Thread的设备管理器里让lwIP能通过统一的接口调用我。这个结构体里有几个关键的字段比如parent这是一个rt_device意味着eth0本质上也是设备管理器的一个节点、priv驱动私有数据、tx_ack发送完成信号量、rx_ack接收完成信号量以及最重要的操作函数指针就是下面这几个init初始化网卡包括GPIO、MAC、DMA描述符、开启PHY。open打开设备一般在这里启动链路和接收。close关闭设备。read/write对以太网设备来说这两个接口用不到可置空。control用来处理MAC地址设置、设备状态查询等控制命令。eth_rx底层收到一包数据后通过它把数据交给上层协议栈。eth_tx上层协议栈要发数据时通过它把数据交给硬件。还有一个点要注意RT-Thread传统的eth设备模型里驱动主动通知上层“我来收包”的方式不是直接调用netif-input()而是调用eth_device_ready(dev)这个函数。这个函数内部会触发协议栈去调用驱动自己注册的接收函数从而把数据包“拉”上去。这个回调关系要理清楚不然后面写中断处理程序的时候很容易踩到死锁或递归调用的坑。3.2 一包数据的旅程从实际调板子的角度咱们把一次最普通的“上位机发命令、板子回响应”的流程拆开看这样你对硬件和软件边界就特别清楚了。先说收包方向。PHY从网线上收到一串差分信号经过内部解码还原成位流然后通过RMII接口按2位一组把数据送到MAC控制器。MAC把校验、地址过滤这些活了结之后把数据放到DMA描述符指向的内存缓冲区里。DMA写完数据后会置一个接收状态标志位同时产生一个接收中断。MCU的中断处理器响应这个中断读出描述符里的接收长度然后调eth_device_ready(),通知协议栈协议栈再把数据逐层解析最后你的TCP或者UDP应用在recv()里拿到的是干净的payload。发送方向反过来。应用程序send()之后lwIP会把数据包数据组织成若干个pbuf节点通过eth_tx传给驱动。驱动要做的事情是把这些pbuf里的数据搬到DMA发送缓冲区然后配置发送描述符启动DMA发送。硬件自动把数据从内存搬移到MAC加上CRC和帧头最终通过RMII发送给PHY再变成差分信号送上网线。发送完成时MAC会产生中断驱动再回收这个描述符缓冲区给下一包数据腾地方。看到没核心工作就两件事描述给DMA收BUFFER、从DMA拿BUFFER发包。但正是在这两个看似简单的动作上Cortex-M7的Cache会跟你开大玩笑这个我放到第4章详细讲。3.3 为什么不用官方库自带的 lwIP demo说到这必须问一个问题GD32官方库或者RT-Thread包管理器里明明也有以太网demo为什么还要自己移植一遍驱动我自己的看法是直接用demo往往能很快看到现象但是出了问题就抓瞎。官方demo一般会帮你把需要适配的地方写死比如PHY地址、引脚映射、缓冲大小可一旦你的硬件不是它对应的那款评估板这些写死的地方就会变成一个个定时炸弹。自己移植驱动虽然稍微费点劲但好处是每个配置项你都知道为什么这么设。你有自己的原理图自己的PHY自己的BSP项目结构搞懂了整体框架之后再去看demo代码思路就非常清晰了。而且后面如果你要上工业协议比如Modbus TCP你还需要对驱动层做更多的定制到时候没有自己搬过一遍底层的代码会很痛苦。所以这篇我会按手工移植的老式做法来讲这也符合咱们这个系列“实战”的定位。4. 驱动移植与实现细节4.1 从 GD32 标准外设库初始化 ENETGD32的官方固件库名叫GD32H7xx_standard_peripheral里面的ENET驱动文件是gd32h7xx_enet.c和gd32h7xx_enet.h。用习惯STM32 HAL库的人看到这套库的接口会觉得扑面而来的“透明”因为它没有HAL那么厚函数命名风格就是xxx_init()、xxx_enable()这种。对整个系列代码来讲这套库的好处是明了的坏处则是寄存器位操作写得多但这会儿也够用。初始化ENET外设的第一步是要把用到的时钟树打开。我这边盖的一版驱动代码时钟初始化时先确保AHB时钟正常然后单独使能ENET的RCU时钟。如果有MCO输出需求还要使能MCO的GPIO复用时钟。一般GPIO口的RCU也要一并打开因为MDC/MDIO以及RMII数据引脚都挂在GPIO上。来一段关键代码void enet_gpio_config(void) { /* 使能ENET相关GPIO的RCU时钟 */ rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_GPIOB); rcu_periph_clock_enable(RCU_GPIOC); rcu_periph_clock_enable(RCU_GPIOH); /* 使能ENET外设时钟 */ rcu_periph_clock_enable(RCU_ENET); /* 逐个配置RMII引脚为复用功能AF号需要查手册 */ gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, ENET_TXD0_PIN); gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, ENET_TXD0_PIN); gpio_af_set(GPIOA, ENET_GPIO_AF, ENET_TXD0_PIN); /* RXD0 / CRS_DV / TX_EN 类似这里省略 */ }上面代码里的ENET_TXD0_PIN、ENET_GPIO_AF这些宏都要根据你自己的板子实际引脚来改。我建议你至少花十分钟对着原理图把网卡相关的每个引脚都查清楚写一个宏定义然后统一在这个函数里跑。如果你用的是官方评估板直接照官方demo抄引脚定义就行文档里写得很清楚。引脚相关配置完之后就是MAC本身的初始化。GD32H7xx库里提供了一个叫eth_init()的函数它接收一个eth_config类型的配置结构体。这个结构体里有个最重要的参数是eth_speed10M/100M/1000M)和eth_mode全双工/半双工还有一个eth_media_interfaceMII/RMII/RGMII的配置。这里不谈寄存器级细节搬库调用的代码大概长这样eth_config_struct.eth_speed ETH_SPEED_100M; eth_config_struct.eth_mode ETH_MODE_FULLDUPLEX; eth_config_struct.eth_media_interface ETH_MEDIA_INTERFACE_RMII; eth_init(eth_config_struct);但你注意这个eth_init()只是初始化MAC和DMA的框架它并不会帮你自动探测PHY。PHY的寄存器操作必须通过MDIO总线。所以在调用MAC初始化之后紧接着我就要通过MDIO去读写PHY寄存器的值获取到PHY的ID以及链路状态。4.2 DMA 描述符和缓冲区规划以太网DMA不是简单的搬运工它使用“描述符”来描述每一块数据缓冲区的位置、长度、状态。GD32H7xx的以太网DMA里描述符分两类发送描述符TDES0~TDES3)和接收描述符RDES0~RDES3)。每一个描述符都对应一块内存缓冲区。发送时驱动把要发的内容放进缓冲区设置描述符的状态位然后DMA会自动去缓冲区取数据发出去。接收时DMA收到数据会先放到预先分配的接收缓冲区然后更新描述符状态再触发中断。我这里分配4个RX描述符和4个TX描述符缓冲区大小按以太网最大帧1518字节来留稍微富余一点到ENET_FRAME_MAX_SIZE我定义成1568这样方便对齐。分配方式是这样的#define ENET_RX_DESC_NUM 4 #define ENET_TX_DESC_NUM 4 #define ENET_FRAME_MAX_SIZE 1568 eth_desc_reg enet_tx_desc[ENET_TX_DESC_NUM]; eth_desc_reg enet_rx_desc[ENET_RX_DESC_NUM]; uint8_t enet_rx_buf[ENET_RX_DESC_NUM][ENET_FRAME_MAX_SIZE] __attribute__((aligned(4))); uint8_t enet_tx_buf[ENET_TX_DESC_NUM][ENET_FRAME_MAX_SIZE] __attribute__((aligned(4)));这是驱动里最容易写翻车的地方因为描述符在RAM里的存放位置和缓冲区的一致性会直接决定你这板子收发是否稳定。缓冲区不能放在TCM里原因我在下一个章节详细说你先记住这个结论DMA能访问的内存区域和CPU紧密耦合内存是有区隔的描述符要放在DMA可见的SRAM区。描述符初始化时需要注意“所有权”的概念。接收描述符初始状态要让DMA拥有驱动在中断里处理完数据后再把所有权交还给DMA。发送描述符则是驱动拥有指定DMA处理完成后交回。我在初始化时把所有RX描述符的状态都置为DMA可接收并把RDES0里的OWN位写成1。4.3 注册到 RT-Thread 并实现回调现在到了“接线”环节也就是把写好的eth_init、eth_tx、eth_rx这批函数挂到RT-Thread的设备框架上。在drv_enet.c里我会定义一个struct eth_device enet_dev然后通过eth_device_init(enet_dev, eth0)把它注册进系统。先看整体注册代码static rt_err_t drv_enet_init(rt_device_t dev) { /* GPIO MAC DMA描述符 PHY初始化 */ enet_gpio_config(); enet_mac_config(); enet_dma_descriptor_init(); phy_init(); /* 使能MAC接收和DMA接收 */ eth_enable(); return RT_EOK; } static rt_err_t drv_enet_open(rt_device_t dev, rt_uint16_t oflag) { /* 打开网卡使能接收中断 */ enet_rx_interrupt_enable(); return RT_EOK; } static rt_err_t drv_enet_control(rt_device_t dev, int cmd, void *args) { switch (cmd) { case NIOCTL_GADDR: /* 获取MAC地址 */ memcpy(args, enet_local_mac, 6); break; case NIOCTL_SADDR: /* 设置MAC地址 */ memcpy(enet_local_mac, args, 6); break; default: break; } return RT_EOK; }这一层没有玄学就是把硬件操作按照设备驱动的模板包装进去。真正体现驱动能力的还是下面这两个函数enet_tx和eth_isr中断服务。enet_tx是lwIP回调进来的发送接口传进来的参数是一个struct pbuf *p。pbuf是lwIP的内存包描述结构它可能是一个链表每个节点指向不同的内存段。驱动要做的是遍历这个链表把每个节点的payload拷进DMA发送缓冲区组装成一个连续的以太网报文然后交给DMA发送。这里我不会做零拷贝虽然RT-Thread框架理论上支持但对工控产品的稳定优先宁可多发一次拷贝也别冒踩内存的风险。static rt_err_t enet_tx(struct eth_device *dev, struct pbuf *p) { rt_base_t level; struct pbuf *q; uint8_t *tx_data enet_tx_buf[tx_index]; uint32_t len 0; uint32_t i; for (q p; q ! RT_NULL; q q-next) { memcpy(tx_data len, q-payload, q-len); len q-len; } /* 填充发送描述符 */ enet_tx_desc[tx_index].tdes1 len; enet_tx_desc[tx_index].tdes0 | ETH_TDES0_OWN; tx_index (tx_index 1) % ENET_TX_DESC_NUM; /* 触发DMA发送 */ eth_transmit_poll(); return RT_EOK; }发送缓冲区enet_tx_buf作为全局变量它的长度按最大帧大小预留所以memcpy长度不会溢出。前面加个互斥或临界区保护level rt_hw_interrupt_disable()/rt_hw_interrupt_enable(level)防止多线程同时调用enet_tx时出现竞争。实际项目里我建议再加个信号量保护发送描述符因为如果lwIP上层连续快速发包而你DMA还没发送完成新的发送就会把上一个描述符覆盖掉。简单粗暴但有效的做法是先把发送描述符数量调大比如8个让DMA去排队然后在中断里统一回收。这里我在驱动里用了一组互斥量发送时rt_sem_take完成后rt_sem_release从上层看就是阻塞的速率不会失控。4.4 PHY 探测与链路状态机PHY的探测是驱动能否拿到IP的基础。PHY里面的寄存器很多但最关键的也就那么几个寄存器0是厂商型号寄存器1是基本状态寄存器4是千兆能力的通告寄存器5是百兆/十兆能力通告寄存器17是千兆速率协商结果寄存器18是百兆/十兆协商结果。对于百兆RMII你重点看寄存器1的bit5自动协商完成和bit2链接建立以及寄存器18的bit13(100BASE-TX全双工)和bit14(100BASE-TX半双工)。MDIO的读写我写成了底层驱动函数static uint16_t enet_mdio_read(uint32_t phy_addr, uint32_t reg_addr) { uint16_t value 0; eth_phy_register_read(ENET, phy_addr, reg_addr, value); return value; } static void enet_mdio_write(uint32_t phy_addr, uint32_t reg_addr, uint16_t value) { eth_phy_register_write(ENET, phy_addr, reg_addr, value); }有了MDIO读写能力初始化PHY就很简单了我这也算“抄作业”级别的参考实现。FIRST_RESET拉低RST脚延时再拉高等待PHY稳定然后MDIO读PHY_ID1寄存器判断PHY是否存在如果存在把PHY的模式设置为RMIILAN8720A有个寄存器控制RMII模式位要置1然后关闭PHY的功率down模式最后开启自动协商。自动协商是好东西但也有一个坑PHY自动协商完成后MAC侧并不知道。所以我的做法是启动一个RT-Thread定时器线程每隔1秒通过MDIO读一次PHY的链路状态寄存器把状态更新到全局变量里链路发生变化时调用eth_device_linkchange()让lwIP知道链路通断。这样比你只用中断好一点因为很多PHY在链路不稳定时中断会疯狂触发导致系统抖动轮询的方式更稳。这里的轮询周期也可以动态调整拔网线检测会有延迟但工业环境下1秒足够。5. 实战验证与性能测试5.1 上电看 eth0 注册驱动代码写完编译下载到板子里第一件事别急着ping先看系统启动日志。RT-Thread启动时会打印设备和组件的初始化信息。如果驱动注册成功你会看到类似eth0已经挂到设备后面。接着在MSH控制台输入ifconfig命令如果输出里能看到eth0和类似Link up的字段状态是100M全双工恭喜你底层MACPHY已经通了。如果你ifconfig什么都没看到或者提示找不到eth0那问题九成出在注册环节。最大嫌疑人要么是eth_device_init没被调用要么是board.h里没打开宏定义。我在踩坑记录里有详细排查步骤这里先不用慌。5.2 拿到IP静态配置和DHCP链路通了之后就是要给网卡分配IP。在RT-Thread里最简单的验证方式是直接静态配置IP也就是ifconfig eth0 192.168.1.100掩码、网关都设好。然后从电脑上ping 192.168.1.100如果能通说明IP层至少没问题。如果链路状态不通、ping报Destination Host Unreachable那就要回头检查PHY的自动协商了。如果静态配置没问题你再试试DHCP。RT-Thread里DHCP一般在netdev配置里开ifconfig之后会自动获取IP。要注意一点DHCP是UDP广播协议如果驱动在链路没up之前就启动了DHCP客户端大概率拿不到地址。所以我代码里特意在链路状态从down变up之后才触发netdev_dhcp_start()或者让DHCP线程重新发起请求。这也是工业现场稳定性的一个细节不能省。5.3 TCP 收发测试ping只是ICMP报文但工控场景中真正用的更多的是TCP。为了避免上来就写一大堆应用层代码我推荐先用电脑工具测试。我电脑上装了个简单的TCP调试助手板子里用RT-Thread自带的tcpserv示例程序开一个TCP服务端监听端口6000。电脑上用TCP客户端连上去发一串hello from pc板子收到后原样回传电脑能看到同样的字符串说明驱动收发双向都通了。再进一步你可以测一下打流。电脑上用iperf工具板子里跑tcpclient或者单独写一个测试线程往电脑发大块数据。百兆网络理论极限12.5MB/s实际打流能看到7~8MB/s就已经说明DMA和驱动效率不错了。如果打流过程中出现大量丢包或者延迟飙升大概率是DMA缓存一致性问题或者发送描述符回收太慢。板子端的测试线程我用RT-Thread的MSH命令触发代码很短static void net_tcp_test(void) { int sock; struct sockaddr_in server_addr; char buf[1024]; sock socket(AF_INET, SOCK_STREAM, 0); server_addr.sin_family AF_INET; server_addr.sin_port htons(6000); server_addr.sin_addr.s_addr inet_addr(192.168.1.50); if (connect(sock, (struct sockaddr *)server_addr, sizeof(server_addr)) 0) { rt_kprintf(connect failed\n); closesocket(sock); return; } /* 发一块数据然后接收回显 */ send(sock, GD32H759 enet test, 19, 0); recv(sock, buf, sizeof(buf), 0); closesocket(sock); } MSH_CMD_EXPORT(net_tcp_test, tcp client test);这个测试能通驱动层面就算验收合格了接下来你就能在这个基础上跑Modbus、MQTT、HTTP等各种协议。6. 踩坑记录与排查清单6.1 链接始终为 down链接down是新手问得最多的问题。如果你ifconfig看到Link down先从硬到软排查。硬件上确认RMII每个信号线测到对应引脚示波器或万用表看PHY芯片的供电和复位是否正常测量PHY的REF_CLK是不是稳定的50MHz这个最后一个模拟问题量不一定准但能看到波形就说明晶振电路活了。还有一种情况是PHY芯片根本没工作MDIO读PHY_ID全是0xFF这种就要查复位和上电时序。软件上最常犯的错是PHY地址给错了。LAN8720A的MDIO地址由PHYAD0引脚决定上拉到高通常就是0x01接地下拉到低通常为0x00。看你的原理图把驱动里PHY_ADDRESS的定义改掉。另外自动协商没开或者PHY被设成了固定速率也会造成链路能冲上但不能稳定工作我建议先在PHY初始化里强制开自适应。6.2 能收到包但发送失败如果你发现板子能收到电脑的广播包比如能响应ARP但电脑ping不通板子问题十有八九出在发送方向。查发送描述符的初始化看看tdes0的OWN位是否一开始就给错了错误给成0会让DMA以为没有发送任务然后就没有然后了。还有一种可能就是驱动在enet_tx里拷贝pbuf的时候长度算错了少了以太网帧的头部或者长度字段这种问题基本是写代码时疏忽已经给协议栈发去的报文格式不对网卡直接丢弃。再有一种周期性的玄学板子能发出去几包然后停住后面就再也发不了。这个就倾向于是发送完成中断没处理好描述符没回收或者回收时清错了状态位。以太网DMA的中断标志位是清写1的注意不能用普通的赋值要写1清0。6.3 CACHE 一致性问题这是所有Cortex-M7芯片调以太网时都会遇到的坎。GD32H759的主频跑到550MHzCPU内部有D-Cache和I-Cache。以太网DMA是我们外设它把接收到的数据写进内存时是直接从外设侧写的CPU读写这块内存时会受Cache影响。如果你把DMA缓冲区配成了Cacheable那么DMA写的数据可能还在Cache里CPU去读的时候读到的可能是老的缓存内容表现出来就是“收到的报文数据不对但长度看起来正确”。最简单的解法是在系统启动阶段用MPU把以太网DMA相关的这段内存区域配置为Device或者Non-Cacheable这样的话CPU和DMA看到的数据永远是一致的。配置方式在Cortex-M7里就是操作MPU寄存器。如果在RT-Thread里你开了cache组件也可以直接用API把内存段属性改掉。如果你实在不想配MPU也可以在代码里手动做一致性维护接收中断里在处理数据前调用SCB_InvalidateDCache_by_Addr发送前调用SCB_CleanDCache_by_Addr。这样性能会有损耗但能解决问题。我强烈建议能用MPU一次性配置好就配好别图省事手动刷Cache等以后波形一多手刷的方式迟早会出问题。6.4 常见问题速查表现象可能原因排查建议eth0 不出现宏没开启或驱动没初始化检查BSP配置确认RT_USING_ETH打开Link downPHY复位时序、PHY地址错误、REF_CLK异常测量REF_CLK检查PHY_RST核对MDIO地址能收到ARP但ping不通发送方向异常描述符或DMA配置错误检查发送描述符OWN位和copy长度收发很慢DMA缓冲区Cacheable未做一致性维护配置MPU把DMA缓冲设为Non-CacheableDHCP获取不到IP链路未up就发起DHCP在链路up回调里再启动DHCP打流丢包严重RX描述符太少或中断处理过慢增加RX描述符数量优化中断优先级长时间运行后断网PHY死锁或MDIO轮询卡死加看门狗或PHY软复位机制6.5 从驱动到应用的下一步驱动跑到这一步以太网链路算真正“能用”了。但工控产品还远不止这个后面要做的事还不少。比如在应用层我下一步就打算在这个eth0上跑Modbus TCP从站把现场采集到的温度、压力、状态这些变量映射到寄存器表里让上位机直接能读写。再往后还可能加上MQTT把数据推送到云平台实现远程监控。驱动本身也要继续打磨性能。现在的实现是中断收包、轮询链路状态已经满足一般需求。但如果你的工业现场数据量特别大可以考虑使用RT-Thread的零拷贝特性或者把DMA描述符数量再调大一些甚至使用多队列DMA模式。这些都属于从“能跑”到“跑得稳”的优化方向急不来也不是本篇的重点。我的经验是移植驱动与其说是个技术活不如说是个细心活。万事开头难你把硬件原理图看透、框架理清、代码一步步写下来其实很多所谓的“坑”都是因为没按步骤来导致的。GD32H759这颗料在这套方案里跑RT-Thread很顺手功耗性能也符合预期是值得继续投入的。最后再分享一个小技巧在驱动调试阶段记得在MSH里加上ifconfig和ping命令这俩是排查问题的左膀右臂。我的测试方法一般是先在串口控制台手动执行这些命令再谈自动化的压力测试因为自动化脚本屏蔽了过程中的关键信息反而不利于定位Bug。先把驱动复杂的问题用最简单的手段排查掉剩下那点硬骨头再去翻内核源码和芯片手册你会比想象中更快搞定。