尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

GD32H759+RT-Thread以太网驱动移植实战:从PHY到D-Cache避坑指南

GD32H759+RT-Thread以太网驱动移植实战:从PHY到D-Cache避坑指南 GD32H759 配上 RT-Thread 之后第一个真正让我觉得“这块板子活过来了”的功能就是 enet 驱动。第 1 篇我们把系统跑起来、串口控制台通了但那时候它还只算是个单片机等到以太网驱动调通板子才算是真正能进工控网络的设备。这篇我就把 enet 驱动移植的过程完整复盘一遍从硬件管脚、PHY 选型到 RT-Thread 的 eth 框架接线再到调试中那些让人挠头的坑一次说透。GD32H759 这颗料本身就带千兆 MAC主频又高拿来做人机界面、协议转换网关、视觉预处理节点都很合适。RT-Thread 的 eth 框架经过这些年迭代已经不光是 lwIP 的搬运工从驱动对接、PHY 管理到中断调度都有比较成熟的套路。真正费劲的不是把示例跑通而是改到自己的板子上还能稳定跑满带宽这才是工控设备该有的底线。1. 整体设计与思路拆解1.1 工控设备为什么必须联网过去工控板上最常见的通信就是串口、CAN、485调试时插根线运行时走 Modbus 轮询。可一旦设备数量多起来、数据量涨上去串口和 CAN 的瓶颈就很明显了。以太网在这时候的优势是降维打击带宽大、距离远、能直接接交换机、能跑 TCP/IP 协议栈还能用现成的上位机组态软件。所以这块 GD32H759 工控板的定位就不只是“单片机屏幕”而是边缘 IO 采集器、协议网关、视觉设备的预处理单元。以太网口是刚需。1.2 GD32H759 的 ENET 外设到底有多强GD32H759 内置的 ENET 外设支持 10/100/1000Mbps 自适应MAC 层走 MII、RMII、RGMII 三种接口都能接。DMA 部分做得比较完整发送接收有独立的描述符链表支持 TCP/IP 校验和卸载这对高带宽场景非常关键因为校验和计算挪到硬件里做CPU 负载能低不少。更值得说的是这颗 MCU 是 Cortex-M7 内核主频可以达到 600MHz并且带了 I-Cache 和 D-Cache。Cache 对性能是双刃剑代码跑得快但 DMA 搬运的数据如果 Cache 没处理好就会出现收包乱码、发送数据错位这种极其隐蔽的问题。这个坑我在后面会单独拎出来讲。1.3 驱动方案怎么选官方 BSP 打底二次开发为主我第一次调这个驱动的时候也纠结过要不要从头照着参考手册撸寄存器。后来忍住了因为 RT-Thread 仓库里其实已经有 GD32H759I-EVAL 的 BSP里面就有 drv_eth.c 可以参考。直接在那基础上改自己的管脚、PHY 型号、中断优先级比自己从零写省太多时间。我的原则是方案选型阶段用官方的东西跑通链路然后再逐层深挖细节。因为以太网驱动牵扯 MAC、DMA、PHY、lwIP 四层如果一上来什么都想自己搞出问题的时候根本分不清是哪一层挂了。2. 硬件设计与连接要点2.1 原理图阶段就要核对管脚以太网这东西不像串口串口接错 RX/TX 大不了没数据但以太网的 RMII 信号线、时钟线、MDIO/MDC 一旦接错调起来非常痛苦。我这次用的是 RMII 接口接百兆 PHY只用 7 根信号线加一对 MDIO/MDCTXD0、TXD1发送数据TX_EN发送使能RXD0、RXD1接收数据CRS_DV载波侦听/数据有效REF_CLK50MHz 参考时钟MDC、MDIO管理接口画原理图之前一定要对着 GD32H759 数据手册把每个引脚的复用功能查清楚。不同封装下同一个复用功能的管脚位置可能完全不同打板回来再改就是白花花的时间和钱。2.2 PHY 选型与 50MHz 时钟纪律PHY 我选的是 LAN8720A这个芯片在百兆方案里非常常见价格便宜、资料多、上电默认地址是 0x00省去一堆地址配置的麻烦。它特别方便的一点是REF_CLK 可以由 PHY 自己产生并输出给 MCU这样 MCU 这边的 REF_CLK 脚配置成输入就行省一颗 50MHz 有源晶振。如果你用其他 PHY一定先确认时钟方向。有些 PHY 要求 MCU 给它提供 50MHz 时钟有些 PHY 是把 50MHz 时钟输出给 MCU。搞反了就是链路 up 不了或者时好时坏。PCB 布线的时候RMII 的信号线要等长、少打过孔REF_CLK 尤其要短尽量包地。50MHz 的频率不算特别高但工控现场电磁环境复杂时钟线被干扰了表现出来就是网络偶发断流、ping 丢包。2.3 这些硬件细节最容易踩坑复位脚必须处理好。我之前一块板子PHY 的复位脚直接并在了 MCU 的 NRST 上看起来没问题但 MCU 复位的时候 PHY 也在复位两者上电时序对不上PHY 经常起不来。正确做法是PHY 的复位脚接 MCU 一个普通 GPIO软件里上电后先拉低 10ms 再释放然后延迟 150ms 等 PHY 稳定再去读 PHY ID。另外 MDC 和 MDIO 的上拉电阻不能省不然管理接口读写会不稳定。3. RT-Thread 工程配置与 enet 驱动移植3.1 先打开 RT-Thread 里的 ETH 组件我用的是 RT-Thread Studio工程配置在图形界面里点一点就行。先打开 RT-Thread Settings把 Ethernet 相关的组件勾上使能 lwIP 协议栈使能 PHY 驱动框架选择对应的 PHY 芯片类型如果列表里没有 LAN8720A可以选 Generic PHY 或者自己预留 PHY 驱动入口使能 DHCP 或者固定静态 IP工控场景我一般先用静态 IP调试起来简单配置 lwIP 的线程优先级和内存池大小先用默认值把链路跑通再说这一步做完工程里会自动加入 lwIP 组件和 eth 驱动框架。但注意RT-Thread 的通用框架只是搭好了壳子具体 GD32H759 的 MAC 初始化、DMA 描述符、中断处理还是得靠驱动文件来完成。3.2 管脚和 PHY 地址怎么改官方 BSP 里的驱动代码管脚映射是基于官方评估板的。我的自研板 RMII 引脚重新分配过所以第一步就是改 GPIO 初始化代码。核心部分是打开 GPIO 时钟和 ENET 时钟然后把每个引脚复用为 ENET 功能void enet_gpio_config(void) { /* 打开相关 GPIO 时钟 */ rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_GPIOB); rcu_periph_clock_enable(RCU_GPIOC); /* 打开 ENET 接口时钟 */ rcu_periph_clock_enable(RCU_ENET); /* 以 RMII 接口为例配置 TXD0/TXD1/TX_EN/CRS_DV/RXD0/RXD1 */ gpio_af_set(GPIOA, GPIO_AF_11, GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_7); gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_7); gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_7); /* REF_CLK 如果是 PHY 输出给 MCU那 MCU 这边就是输入模式 */ gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_0); gpio_af_set(GPIOA, GPIO_AF_11, GPIO_PIN_0); /* MDC/MDIO 一般也走 AF 复用不能省 */ gpio_af_set(GPIOC, GPIO_AF_11, GPIO_PIN_1 | GPIO_PIN_2); gpio_mode_set(GPIOC, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_1 | GPIO_PIN_2); }这段代码里有个容易出错的地方不同型号的 GD32GPIO_AF 编号可能是不同的同一个引脚在不同封装下复用编号也不一样。比如有的引脚复用 ENET 是 AF_11有的是 AF_7不能想当然照抄。PHY 地址也要改成 LAN8720A 默认的 0x00。在 RT-Thread 的 eth 驱动框架里一般会定义一个 PHY 地址宏在初始化的时候传给 MDIO 读写接口#define LAN8720A_PHY_ADDRESS 0x00如果修改后发现 MDIO 读出来的 ID 不对优先怀疑这个地址。3.3 enet 驱动初始化的关键流程驱动初始化其实就是把 MAC、PHY、DMA 三件事串起来。以 GD32 的库函数为例流程大概是static rt_err_t eth_phy_init(struct rt_eth_device *eth_dev) { /* 复位 PHY */ enet_phy_reset(LAN8720A_PHY_ADDRESS); /* 读取 PHY ID检查通信是否正常 */ uint16_t id1 enet_phy_read(LAN8720A_PHY_ADDRESS, PHY_REG_IDR1); uint16_t id2 enet_phy_read(LAN8720A_PHY_ADDRESS, PHY_REG_IDR2); rt_kprintf(PHY ID: 0x%04X 0x%04X\n, id1, id2); /* 配置 PHY 自动协商100M 全双工优先 */ enet_phy_write(LAN8720A_PHY_ADDRESS, PHY_REG_BCR, PHY_AUTO_NEGOTIATION); return RT_EOK; }这里有个值得说的地方PHY 的复位不能太着急。上电之后要给 PHY 足够的启动时间一般至少等 100ms 再读 ID否则读回来的全是 0xFFFF。这属于硬件时序问题代码加再多的重试都只是在掩盖问题。3.4 发送和接收路径的实现以太网驱动最核心的部分就是发送和接收。先看接收路径。DMA 描述符是环形链表收到数据后 DMA 会把数据填到 buffer 里然后触发接收中断。我们在中断里申请一个 pbuf把数据拷过去交给 lwIP 协议栈void enet_rx_isr(void) { /* 检查标志位确认是接收完成中断 */ if (ENET_DMA_STAT ENET_DMA_RXINT) { /* 通知 eth 框架有包到达 */ rt_eth_device_ready(eth_dev); } }发送路径的套路相反。lwIP 要发包时驱动把数据从 pbuf 里取出来放到 DMA 描述符指向的 buffer 里然后触发 DMA 发送。发送完成中断可以释放 pbuf。这块的难点是 pbuf 的内存管理要搞清楚哪些 buffer 是 DMA 要用的哪些是 lwIP 协议栈管理的。驱动代码里如果把 pbuf 直接给了 DMA发送完成之后必须及时释放否则内存池很快就耗光了。3.5 D-Cache 一致性M7 内核的必修课Cortex-M7 的 D-Cache 是很多人栽跟头的地方。DMA 访问内存不走 Cache它直接把数据写进物理内存。如果 CPU 先从 Cache 里读了旧数据而 DMA 已经把新数据写到内存了CPU 读到的就是脏数据。反过来CPU 把数据写进了 CacheDMA 去内存里搬运搬走的可能是旧数据。解决方法是在 DMA 发送之前先把 Buffer 区域 Clean也就是把 Cache 里的数据刷回内存在 DMA 接收完成之后先把 Buffer 区域 Invalidate也就是让 Cache 失效强制 CPU 下一次从内存读。/* 接收完成读取数据前使 Cache 失效 */ rt_hw_cpu_dcache_ops.invalidate(desc_buffer, buffer_len); /* 发送之前先把 Cache 里的数据刷到内存 */ rt_hw_cpu_dcache_ops.clean(tx_buffer, data_len);如果不做这两步常见的现象是小流量时不明显偶尔通偶尔不通大流量时直接乱套丢包、CRC 错误、系统卡死。光这一条就够新手调好几天了。4. 编译烧录与基础联调4.1 上电后的日志怎么看驱动代码改完编译烧录之后先开串口终端看 RT-Thread 的启动日志。正常的 eth 初始化日志大概是这样的msh /[E/ETH] PHY reset failed.如果看到这句说明 PHY 和 MCU 之间管理接口就没通后面链路基本是废的。优先排查 MDC/MDIO 接线、上拉电阻、PHY 复位脚、PHY 地址。初始化成功之后再执行 ifconfig看看网卡状态msh /ifconfig Network interface: e0 (Default) MTU: 1500 MAC: 00 80 e1 01 23 45 IPv4 address: 192.168.1.20 netmask : 255.255.255.0 gateway : 192.168.1.1 link status : up link speed : 100 Mbps能看到 link up 和 100Mbps说明 MAC 和 PHY 的协商已经成功了接下来才有资格谈 ping 通这件事。4.2 ping 通只是起点把电脑的网口 IP 设置成同网段比如 192.168.1.10然后用 msh 里的 ping 命令测msh /ping 192.168.1.10 60 bytes from 192.168.1.10 icmp_seq1 ttl128 time1 ms 60 bytes from 192.168.1.10 icmp_seq2 ttl128 time1 ms看到这个结果驱动基本算活了。但别高兴太早ping 通只代表 ICMP 小包没问题工控场景更关心的是 TCP/UDP 大包能不能长时间稳定跑。第一次调通时我发现 ping 包都正常一跑 TCP 传数据就断后来排查到是 lwIP 的 pbuf 池太小大包分段后内存不足直接丢包。所以建议 ping 通之后立刻做一次 TCP 压力测试。4.3 跑一个简单的 TCP 回显服务RT-Thread 支持用 msh 命令直接跑网络应用不过为了验证驱动稳定性我习惯在应用层写一个简单的回显线程#include rtthread.h #include arpa/inet.h #include netinet/in.h #include sys/socket.h static void tcp_echo_server(void *param) { int server_fd, client_fd; struct sockaddr_in server_addr, client_addr; socklen_t addr_len sizeof(client_addr); char buf[2048]; int len; server_fd socket(AF_INET, SOCK_STREAM, 0); server_addr.sin_family AF_INET; server_addr.sin_addr.s_addr INADDR_ANY; server_addr.sin_port htons(5000); bind(server_fd, (struct sockaddr *)server_addr, sizeof(server_addr)); listen(server_fd, 5); while (1) { client_fd accept(server_fd, (struct sockaddr *)client_addr, addr_len); while (1) { len recv(client_fd, buf, sizeof(buf), 0); if (len 0) break; send(client_fd, buf, len, 0); } closesocket(client_fd); } } MSH_CMD_EXPORT(tcp_echo_server, start tcp echo server);然后电脑上用网络调试助手连板子的 5000 端口循环往里丢大量数据观察是否长时间不掉线、数据是否完整。如果驱动或 lwIP 内存配置有问题这轮肯定是暴露得最充分的。5. 调试过程中我踩过的坑5.1 PHY 地址读回来全是 0xFFFF这个问题我单独列出来说因为太典型了。现象是串口打印 PHY ID 时全是 F或者 RT-Thread 启动日志直接报 PHY reset failed。排查路径按顺序走量 PHY 的供电是否正常LAN8720A 有些版本是 3.3V 单电源有些需要 1.2V 内核电压查 PHY 复位脚是否在上电后被拉高LAN8720A 复位期间 MDIO 是没法操作的看 MDC/MDIO 的上拉电阻这两个脚必须接 10k 左右上拉不接管理层通信会不稳定核对 PHY 地址引脚LAN8720A 默认地址是 0x00但如果 PHYAD0 引脚被拉高地址就变成 0x01代码里写死 0x00 就会读不到用示波器抓 MDC 波形确认 MDC 时钟确实有输出我最后那次排查发现是板子上的 PHY 复位电阻贴错了导致复位脚一直在低电平PHY 根本没起来。硬件问题往往比代码问题更磨人。5.2 链路起来了但 ping 大包必丢小包正常ping 1500 字节的大包就丢或者 TCP 传文件传一半断掉这种问题多半和 DMA 描述符数量有关系。我的板子一开始用的是官方默认的 4 个接收描述符每个描述符指向的 buffer 只能缓存一个包。在突发流量下DMA 收包速度快lwIP 还没来得及把包取走新一轮接收就把描述符占满了新包直接被丢弃。解决办法很简单把接收描述符数量从 4 改成 16发送描述符我保留了 8 个。改完大包丢包率明显下降。5.3 高负载时收包乱码查了半天是 D-Cache 没刷这个坑我从理论上知道但实际遇到还是花了不少时间。现象是低负载跑了一天没问题一跑大流量 UDP收到的数据偶尔会错几个字节而且位置不固定。后来在接收中断里加了对 DMA buffer 的 Invalidate 操作再把 CPU 读取数据的顺序调整了一下问题立刻消失。从那以后只要代码里涉及 DMA 和 Cache我都会把 clean/invalidate 的时机标注得清清楚楚并把 buffer 都对齐到 32 字节这也是 M7 上 D-Cache line 的对齐要求。5.4 接收中断太频繁系统都卡住了以太网全速收包时中断频率是相当高的。如果每个包都在中断里完成 pbuf 分配、数据拷贝、协议栈处理系统别的事都别干了。我的做法是中断里只做最简单的标志置位和 ready 通知把 lwIP 的接收处理放到协议栈线程上下文中。RT-Thread 的 eth 框架本身也推荐这种模式中断里越短越好否则不仅卡业务逻辑还会因为中断优先级问题造成丢包。另外中断优先级也要注意不能太高也不能太低。太高会抢占系统 tick导致调度异常太低的话在流量大的时候会频繁被其他中断打断影响吞吐。我这边 ENET 中断优先级设为 5实测稳定。5.5 常见问题速查表现象可能原因排查手段PHY ID 读到 0xFFFFMDIO 时序异常、PHY 未复位检查复位脚、上拉电阻、MDC 时钟链路 up 但 ping 不通MAC 速率/双工与 PHY 不一致手动固定 100M 全双工再测ping 小包正常、大包丢DMA 描述符不足或 lwIP 内存不足增加描述符数量、调大 pbuf 池高流量下数据错乱D-Cache 未 clean/invalidate检查 DMA buffer 的 Cache 操作跑一段时间网络卡死中断被长时间占用中断里只做置位协议栈处理下放线程TCP 传大文件自动断开lwIP 内存或 TCP 窗口配置过小调大 lwIP 内存池检查内存回收逻辑6. 性能与稳定性调优建议6.1 描述符数量和 buffer 大小怎么定工控设备通常对实时性有要求不能把内存无限度地扔给网络栈。我的建议是接收描述符给 16 个每个 buffer 按最大 MTU 1500 字节再加一点余量来配置。发送描述符 8 个足够因为发送路径是 lwIP 主动触发的突发性比接收弱。内存开销其实没有想象中大16 个 1600 字节的 buffer 也就 25KB 左右。对于 GD32H759 这种内置大容量 SRAM 的芯片完全负担得起。6.2 中断与线程优先级怎么配合RT-Thread 里 lwIP 是跑在独立线程里的默认优先级一般不高。如果网络负载重可以适当提高 lwIP 线程的优先级但不能比系统 tick 线程和定时器线程更高。一个比较稳的配置是ENET 硬件中断优先级5lwIP 接收处理线程优先级10 左右业务应用线程优先级15 以下这样既能保证网络包被及时处理又不会影响关键控制任务的实时响应。6.3 与协议栈相关的参数调整lwIP 默认参数偏向省内存工控场景建议做几处调整增大 MEM_SIZE给协议栈留更多堆内存增大 PBUF_POOL_SIZE接收路径的缓冲不足往往从这里开始开启 TCP_WND 和 TCP_SND_BUF提高 TCP 吞吐如果设备长时间运行开启内存统计并定时检查是否有泄漏这些参数改完之后再跑一轮 TCP 回显压力测试看内存占用是否稳定连续跑 24 小时有没有涨。以太网驱动调到这里链路已经不是瓶颈后面真正决定工控设备品质的是协议层和应用层。我的建议是先把这套驱动冻结下来任何硬件改版都重新过一遍 PHY 初始化和 Cache 配置流程。百兆 RMII 跑通之后下一步可以尝试 RGMII 接千兆 PHY思路是类似的只是时钟和管脚约束更严格。这块 GD32H759 的上限远不止一个百兆网口。
返回列表