GD32H759以太网驱动移植实战:RT-Thread + RMII + LAN8720A
1. 写在前面为什么先啃 ENET 驱动GD32H759 这颗芯片熟悉的人应该知道它最大的亮点是主频跑到 600MHz带 FPU 和 DSP 指令片上集成了大容量 Flash 和 SRAM。在工控场景下基本可以把它当成国产替代 STM32H7 系列的优先选择。但芯片性能再强如果网络这一关过不去很多项目就只能干瞪眼。我手上这套平台是 GD32H759 RT-Thread前面已经完成了最小系统搭建和点灯测试这次直接进入正经的通信环节ENET 驱动移植。为什么先把 ENET 搞定而不是先去研究串口、CAN 这些因为工业现场的设备联网需求太普遍了不管是走 Modbus TCP、MQTT 上云还是做远程运维都离不开以太网。把网口驱动跑通后续的协议栈、业务逻辑才有落脚点。这篇文章的目的很明确从零开始把 GD32H759 的 ENET 外设驱动在 RT-Thread 上跑起来全程使用 RMII 接口 LAN8720A 物理层芯片方案配套讲解外设初始化、DMA 描述符配置、中断处理、性能验证以及我在实际调试中踩过的坑。内容偏实战适合已经能跑 RT-Thread 基础工程、想快速把网络拉通的开发者参考。如果是刚接触 GD32H759 的新手建议先过一遍芯片手册的 Ethernet 章节再动手否则后面有些细节会看得比较吃力。2. 方案选型和硬件连接2.1 外设资源盘点GD32H759 内置了两个以太网 MAC 控制器分别是 ENET0 和 ENET1都支持 10/100Mbps 速率支持 MII 和 RMII 两种接口模式。每个 ENET 控制器支持独立的 DMA也支持双 MAC 共用 DMA 的配置方式不过那属于进阶玩法我这次只跑单网口所以只用 ENET0 独立 DMA资源分配最清晰。RMII 相比 MII 最大的优势是引脚少。MII 需要 16 根信号线RMII 只需要 7 根TXD[1:0]、RXD[1:0]、TX_EN、REF_CLK、MDIO/MDC。在 PCB 布局紧张的工控板上能省下 9 根 IO 非常可观。我这次选用的物理层芯片是 LAN8720A这是目前工控板上的常客。它支持 RMII 接口自带 50MHz 时钟输出可以反向给 MCU 提供 REF_CLK省掉一颗有源晶振。供电 3.3VI/O 电压可以直接和 GD32H759 对接不必额外做电平转换。2.2 RMII 接口的实际接线这里整理了 ENET0 在 RMII 模式下的引脚对应关系以我手上的这块板子为例信号芯片引脚说明ETH_RMII_TXD0PE2发送数据位 0ETH_RMII_TXD1PE3发送数据位 1ETH_RMII_TX_ENPE1发送使能ETH_RMII_RXD0PE5接收数据位 0ETH_RMII_RXD1PE6接收数据位 1ETH_RMII_CRS_DVPE4载波侦听/数据有效ENET_MDCPE0管理接口时钟ENET_MDIOPE7管理接口数据REF_CLKPB2由 LAN8720A 提供 50MHz注意 REF_CLK 这一路是重点。LAN8720A 的 XI 引脚接 25MHz 晶振芯片内部 PLL 把时钟倍频到 50MHz通过 CLKOUT 引脚输出给 MCU 的 ETH_RMII_REF_CLK。这种时钟方向是PHY - MAC在初始化 RMII 时对应的时钟模式要选对GD32H759 的 ENET 寄存器里有一个 MAC 配置位专门用来选择 REF_CLK 是由内部产生还是外部输入选错直接导致 LINK UP 但收发不同步。提示GD32H759 的 RMII REF_CLK 最高支持 50MHz外部输入时钟经过内部同步处理后供 MAC 和 DMA 使用。PCB 布线时这根时钟线尽量短不要打过孔避免时钟抖动影响通信稳定性。2.3 为什么 LAN8720A 很少踩雷LAN8720A 作为 PHY 芯片对比 DP83848、KSZ8081 这些同类器件优势主要体现在三个方面第一外围电路简单。它把终端电阻、偏置电阻大部分都集成到芯片内部了数据手册推荐的 BOM 只有十来个阻容哪怕 PCB 布局不是特别讲究也能正常工作。第二功耗低。工作电流大概 60mA 左右在 3.3V 供电的工控板上发热很小不需要额外的散热处理。第三兼容性好。Linux、RT-Thread、裸机环境下都有大量现成的驱动参考遇到问题了网上搜一圈基本都能找到类似案例。当然它也有短板。LAN8720A 的寄存器映射遵循的是行业通用标准但每个 PHY 芯片的厂商 ID 和具体行为细节有差异。比如软复位后需要等待一段时间才能访问寄存器这个时序不同芯片不一样后面我会专门讲到。3. 驱动架构和代码模块划分3.1 RT-Thread 的 ENET 驱动框架RT-Thread 的设备驱动框架里网卡驱动是需要实现struct eth_device结构体的一系列回调函数。框架层负责对接 lwIP 协议栈底层驱动只需要关注最核心的几件事初始化 MAC 和 PHY、收发数据包、处理中断、设置 MAC 地址。驱动代码的基本骨架如下struct eth_device gd32_enet_dev; static rt_err_t gd32_enet_init(rt_device_t dev) { /* 初始化 MAC、DMA、PHY */ } static rt_err_t gd32_enet_open(rt_device_t dev, rt_uint16_t oflag) { /* 启动网卡使能接收 */ } static rt_err_t gd32_enet_close(rt_device_t dev) { /* 停止网卡 */ } static rt_size_t gd32_enet_read(rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size) { /* 接收数据包入口 */ } static rt_size_t gd32_enet_write(rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size) { /* 发送数据包入口 */ }这套框架设计得比较合理驱动的职责和 lwIP 的职责分得很清楚。驱动只需要做到能收包、能发包、有事件通知剩下 TCP 分片重组、ARP 缓存、路由选择这些全是协议栈的事。所以移植 ENET 驱动时不建议在驱动层玩太多花活保证数据通路顺畅就行。3.2 如何用 RT-Thread Studio 快速创建项目如果用的是 RT-Thread Studio创建带 ENET 的工程有两种路径。第一种在 SDK 管理器里安装 GD32H759 的 BSP然后直接在 BSP 的基础上改驱动文件优先参考 BSP 里的drv_enet.c。第二种用芯片创建普通 RT-Thread 项目然后手动加入 ENET 驱动文件。这里建议直接走 BSP 路线因为 GD32H759 的 BSP 里已经包含了外设驱动框架ENET 驱动文件的结构、依赖的库函数版本都比较配套省去很多适配的时间。RT-Thread 的 ENET 驱动依赖几个关键配置项必须在 rtconfig.h 里正确开启#define RT_USING_LWIP #define RT_USING_ETH #define RT_LWIP_DHCP_ENABLE #define RT_LWIP_IPADDR 192.168.1.30 #define RT_LWIP_GWADDR 192.168.1.1 #define RT_LWIP_MSKADDR 255.255.255.0如果是调试阶段建议暂时不开 DHCP直接用静态 IP减少变量。注意RT_USING_ETH和RT_USING_LWIP必须同时使能。有些开发者只开了 LWIP忘了开 ETH结果 lwIP 协议栈初始化正常但网卡设备没有注册到设备管理器里netdev列表始终为空链路状态永远显示 DOWN。4. GD32H759 ENET 驱动核心细节4.1 时钟使能和引脚复用配置GD32H759 的 ENET 控制器使用两个时钟域。一个挂在 APB2 总线上用于寄存器读写和 DMA 控制另一个是 AHB 总线时钟用于 DMA 访问存储器。初始化时需要在rcu_periph_clock_enable()里把对应时钟打开rcu_periph_clock_enable(RCU_ENET0); rcu_periph_clock_enable(RCU_ENET0TX); rcu_periph_clock_enable(RCU_ENET0RX);这三个时钟分别对应 ENET0 的核心逻辑、发送 DMA、接收 DMA。只开第一个会导致 DMA 收发无响应只开前两个会导致能发不能收。这是新手最容易漏掉的地方。引脚复用方面GPIO 要配置为 AF 模式复用号需要查芯片数据手册的 AF 映射表。GD32H759 上 ENET0 的引脚基本都映射到 AF5具体配置如下gpio_af_set(GPIOE, GPIO_AF_5, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7); gpio_mode_set(GPIOE, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7); gpio_output_options_set(GPIOE, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7);这里有一个细节PB2 作为 REF_CLK 输入引脚配置时要特别注意。它不能和普通信号一样配置成 AF 推挽输出而是要配置成输入模式因为时钟信号是从 LAN8720A 反向输入给 MCU 的。如果照抄其他引脚的 AF 配置REF_CLK 根本没有波形进来MAC 侧的状态寄存器会一直显示无链路。4.2 MAC 配置和 DMA 描述符初始化GD32H759 的 ENET 初始化流程不走库函数的一键初始化而是需要按照手册推荐的顺序来。我当时的初始化顺序是复位 ENET 控制器配置 MAC 工作模式全双工、100Mbps、RMII 模式设置 MAC 地址配置 DMA 总线模式初始化 DMA 描述符链表使能 DMA 接收和发送状态使能中断MAC 配置的核心代码如下enet_init_status enet_software_reset(); if (ENET_RESET_COMPLETE ! enet_init_status) { return -1; } enet_init_struct enet_init_para; enet_init_para.enet_phystatus ENET_AUTO_NEGOTIATION; // 自动协商 enet_init_para.enet_checksum ENET_NO_AUTO_CHECKSUM; // 关闭硬件校验和 enet_init_para.enet_nidedup ENET_NO_NIDEDUP; // 不检测重复帧 enet_init_para.enet_retrans ENET_NO_RETRANSMISSION; // 不重发 enet_init_para.enet_droplongerr ENET_DROP_LONG_FRAME_ERROR; enet_init_para.enet_drop_udp ENET_NO_DROP_UDP; enet_init_para.enet_wo_uf ENET_NO_WAKEUP_FRAME; enet_init_para.enet_am ENET_NO_MULTICAST_FILTER; enet_init_para.enet_bam ENET_NO_BROADCAST_FILTER; enet_init_para.enet_tisen ENET_NO_TIMESTAMP; enet_init_para.enet_sim ENET_RMII_MODE; enet_init_para.enet_debug ENET_DEBUG_DISABLE; enet_init(ENET0, enet_init_para);这里重点解释两个容易误配的选项。enet_checksum我建议关闭。虽然开启后可以让 MAC 硬件自动计算 IP/TCP/UDP 校验和减轻 CPU 负担但在 lwIP 环境下协议栈默认也做校验和计算如果两边都开了就会出现重复计算导致收到的包校验失败被丢弃。要么全交给硬件要么全交给软件混用就有问题。我工程里直接用软件校验稳定性最高性能损失在可接受范围内。enet_sim必须设置成ENET_RMII_MODE这一点不用多说。但是要注意GD32H759 的 ENET 控制器在 RMII 模式下对 REF_CLK 的边沿采样方式有一个隐藏要求如果 REF_CLK 由外部 PHY 提供MAC 侧需要额外配置一个时钟极性相关寄存器否则接收数据可能不稳定。DMA 描述符是整个驱动里最关键的一环。GD32H759 的 ENET 支持环形描述符每个描述符对应一个数据缓冲区发送和接收各有一组。我用的是发送 4 个描述符、接收 4 个描述符的配置接收缓冲区大小 1536 字节足够容纳标准以太网帧。描述符初始化的核心操作enet_descriptors_configure(ENET0, rx_desc_tab[0], tx_desc_tab[0], RX_DESC_NUM, TX_DESC_NUM);每个接收描述符必须提前关联一个缓冲区并且把描述符状态置为 owned by DMA。DMA 收到数据后会自动把数据搬进缓冲区然后清除 owned 标志。驱动收到中断后遍历描述符找到没有 owned 标志的描述符就说明有新的数据包到了。4.3 PHY 芯片 LAN8720A 的初始化LAN8720A 在上电后需要一个启动过程。硬件复位引脚如果有接 MCU建议做一次软件时序复位如果没接直接通过 MDIO 总线发送软复位命令也可以。我的板子上复位引脚接了 MCU 的 PB1初始化顺序是拉低 PB1延时 100ms拉高 PB1延时 150ms通过 MDIO 读取 PHY 的 ID 寄存器确认和 LAN8720A 的 VID 一致配置 PHY 的 Basic Control Register启动自动协商等待自动协商完成读取 Link Status等待自动协商完成这一步不同 PHY 芯片的时间差异很大。LAN8720A 一般 2~3 秒内能完成但也有个别批次比较慢。我这里的处理方式是循环检测超时时间设为 5 秒超过就报错但不阻塞系统。自动协商完成后的状态检查while (timeout--) { phy_basic_status enet_phy_read(ENET0, PHY_ADDRESS, PHY_REG_BCR); if (phy_basic_status PHY_LINK_STATUS_UP) { break; } rt_thread_mdelay(100); }这里有个经验不要用延时 10ms 就去轮询一次这样会频繁占用 MDIO 总线影响其他操作。100ms 的轮询间隔比较合理5 秒内一般都能检测到链路变化。注意LAN8720A 的 PHY 地址由 RXER/PHYAD0 引脚的电平决定。我板子上这个引脚下拉接地所以 PHY 地址是 0x00。如果你的板子把该引脚上拉到 3.3V地址会变成 0x01。读写 PHY 寄存器时地址一定要和硬件匹配否则读到的寄存器全是 0xFF驱动会误判为 PHY 不存在。4.4 中断处理的正确姿势GD32H759 的 ENET 有多个中断源接收中断、发送中断、总线错误中断、PHY 中断等。在中断服务函数里最忌讳的是做耗时操作。我实际工程里的中断处理只做两件事清中断标志、调用rt_sem_release()唤醒接收线程。接收中断的处理流程void ENET0_IRQHandler(void) { if (enet_interrupt_flag_get(ENET0, ENET_INT_RX_FINISH)) { enet_interrupt_flag_clear(ENET0, ENET_INT_RX_FINISH); rt_sem_release(rx_sem); } }对应的接收线程static void eth_rx_thread_entry(void *param) { while (1) { rt_sem_take(rx_sem, RT_WAITING_FOREVER); while (rx_desc_has_data()) { process_rx_packet(); } } }使用信号量而不是直接调用 lwIP 的回调函数好处是接收线程有独立的上下文不会在中断里执行协议栈处理。lwIP 协议栈有自己的线程模型如果在中断里直接调用会打乱它的调度顺序严重的会触发断言。5. 实操过程与验证记录5.1 驱动代码的完整流程我把驱动移植的完整流程梳理一遍按照这个顺序做基本上能少走弯路第一步确认硬件连接。对照原理图明确 ENET0 引脚、PHY 芯片型号、PHY 地址、复位引脚。这一步花 10 分钟能省后面两小时。第二步配置时钟。在board.c里确认 ENET0 相关时钟已经使能确保 REF_CLK 有 50MHz 输入。第三步初始化 GPIO 复用。把 ENET0 用到的所有引脚按 AF 模式配置好特别注意 PB2 对于本项目是输入模式。第四步编写 PHY 驱动。至少实现 PHY 复位、ID 读取、自动协商配置、链路状态检测这四个函数。第五步初始化 MAC 和 DMA 描述符。这一部分代码量最大也是调试的重点。第六步注册网卡设备。调用eth_device_init()把驱动挂到 RT-Thread 设备框架上。第七步启动接收线程和中断编译烧录然后依次测试ifconfig是否能看到网卡接口网线插拔是否能检测到链路变化ping 网关是否通通过 tcp/udp 工具做收发测试5.2 首次联调实测记录我这次在 RT-Thread 终端里执行ifconfig输出如下msh /ifconfig network interface: e0 (Default) MTU: 1500 MAC: 00 80 e1 12 34 56 FLAGS: UP LINK_UP DHCP_ENABLE ip address: 192.168.1.30 gw address: 192.168.1.1 net mask : 255.255.255.0 dns server #0: 192.168.1.1链路状态是 LINK_UP说明 PHY 协商成功MAC 已经连接上网络。接着在 PC 上执行ping 192.168.1.30第一次 ping 结果如下来自 192.168.1.30 的回复: 字节32 时间1ms TTL64 来自 192.168.1.30 的回复: 字节32 时间1ms TTL64 来自 192.168.1.30 的回复: 字节32 时间1ms TTL64丢包率 0%延迟 1ms 内驱动基本正常。但是有一个现象值得注意如果 PC 端强行设置静态 IP 且和板子网关在同一网段板子以 DHCP 客户端模式启动则会一直拿不到地址。后来检查发现是 DHCP 报文在 lwIP 里处理时使用了广播地址如果 MAC 地址没有正确写入 ENET 控制器DHCP 请求会被 PHY 丢弃。所以建议调试阶段先静态 IP确认数据通路完全正常后再切 DHCP。不要一开始就 DHCP lwIP 一起跑出了错很难定位到底是网络配置问题还是驱动问题。5.3 数据收发压力测试ping 通只是第一步真正的考验是大流量收发。我写了一个简单的压力测试脚本板子作为 TCP ServerPC 端用工具以最大速率循环发送数据。测试结果单次发送 1400 字节数据包连续发送 10000 次零丢包接收方向PC 端以 10Mbps 速率连续发送 3 分钟板子侧统计接收字节数丢包率 0%CPU 占用率大约 25% 左右主要开销在 lwIP 协议栈处理上压力测试通过后我心里才真正踏实了。驱动能不能用不是看 ping 通一次而是要看长时间、大流量下能不能稳定跑。6. 常见问题与排查技巧这一节都是我在调试过程中真实遇到过的坑整理成速查表方便以后自己查阅也给读者省点时间。现象可能原因解决方法PHY 寄存器读不到 IDPHY 地址配置错误确认 PHYAD0 引脚电平核对 PHY 地址链路状态始终 DOWNREF_CLK 无时钟或 GPIO 配置错误示波器量 PB2 是否有 50MHz 波形能 LINK UP 但 ping 不通MAC 地址未写入或写错检查 MAC 地址寄存器能收不能发发送 DMA 时钟未使能补上RCU_ENET0TX时钟能发不能收接收 DMA 时钟未使能补上RCU_ENET0RX时钟丢包严重描述符缓冲区太小接收缓冲区改成 1536 字节lwIP 校验和失败硬件校验和与软件校验和冲突关闭硬件校验和板子重启后 DHCP 失败MAC 地址每次随机生成固定 MAC 地址并写入 flash排查问题时强烈建议先用示波器或逻辑分析仪确认 REF_CLK 和 TX_EN 信号再进软件调试。很多看起来是软件问题的坑其实根源在硬件信号没通。6.1 最容易忽略的坑MAC 地址工控设备出货前必须考虑 MAC 地址的唯一性。如果每台设备用的 MAC 地址都一样网络上有冲突轻则通信异常重则整个 LAN 里的设备都受牵连。我的做法是从 MCU 的 UID 寄存器读取 96 位唯一 ID通过简单哈希生成低 48 位 MAC 地址再把最高字节固定为 0x00。这样每台设备上电后自动拥有唯一 MAC不需要额外烧录也不用外挂 EEPROM。static void get_mac_from_uid(uint8_t mac[6]) { uint32_t uid[3]; uid[0] *((uint32_t *)UID_BASE); uid[1] *((uint32_t *)(UID_BASE 4)); uid[2] *((uint32_t *)(UID_BASE 8)); mac[0] 0x00; mac[1] 0x80; mac[2] (uid[0] 8) 0xFF; mac[3] (uid[0] 16) 0xFF; mac[4] (uid[1] 8) 0xFF; mac[5] (uid[1] ^ uid[2]) 0xFF; }这个方案在量产项目里验证过没有遇到 MAC 冲突的问题。6.2 中断优先级和 lwIP 线程优先级中断优先级的配置也会影响网络稳定性。ENET0 中断优先级如果设置得比系统心跳中断低可能在高负载情况下丢中断导致接收线程一直等不到信号量网络出现假死。我这边统一把 ENET 中断设置成抢占优先级 5子优先级 0。同时接收线程的优先级设为 RT_THREAD_PRIORITY_MAX - 2保证 lwIP 线程被唤醒后能尽快处理。调试期间还遇到一个经典问题板子在 RT-Thread 空闲线程里跑rt_thread_mdelay()结果 ping 一会通一会不通。后来定位发现是 lwIP 的tcpip_thread优先级设得太低总是被空闲线程抢占协议栈处理不及时导致 Rx 缓冲区溢出。把tcpip_thread优先级调到 10 左右问题就解决了。7. 从驱动到工控方案落地的一点体会ENET 驱动跑通只是开始。这个驱动后续要真正用在工控项目里还有几件事值得提前规划。第一链路检测要搬上应用层。驱动层虽然有 LINK_UP/LINK_DOWN 状态但工控设备需要一个心跳机制或者连接计数断网后能在几秒内给出告警而不是等到业务超时才被动发现。第二考虑双网口冗余。GD32H759 有双 ENET 控制器如果项目对可靠性要求高建议 ENET0 做主、ENET1 做备用软件实现主备切换。驱动层面两个网口共用一套代码只是基址和中断号不同初始化和注册时做两次即可。第三关注 lwIP 的内存开销。默认配置下lwIP 会占掉几十 KB 内存。如果设备 RAM 紧张需要调整MEM_SIZE、PBUF_POOL_SIZE、TCP_SND_BUF这几个宏。建议实测不同配置下的内存占用和吞吐量找到平衡点。最后再分享一个小技巧驱动联调阶段建议在代码里保留一个自环测试入口通过配置 MAC 进入环回模式不接网线也能验证收发通路。GD32H759 的 ENET 控制器支持 MAC 内部环回这在排查硬件问题时非常有用。我在做 PCB 改版之前都会先跑一次环回测试确认板子链路没问题再交给硬件同事审查能省下不少沟通成本。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →