资讯动态

GD32F407裸机LWIP以太网移植与ping调通实战

发布时间:2026/10/26 2:42:35 来源:尧图企业网站定制
简介面向GD32F407裸机联网开发的完整工程源码聚焦无操作系统环境下移植LWIP轻量级TCP/IP协议栈这一常见难点适合具备Cortex-M4外设开发基础、希望打通以太网通信链路的嵌入式工程师与高年级学生参考。压缩包共641个文件、约12.15MB以216个h头文件与165个c源文件为主体辅以78个d、78个o、77个crf等编译中间文件并包含Keil工程文件、分散加载脚本与map、lst映射清单readme及数份txt补充了移植说明。目前已有1317人学习下载。可据此对照以太网MAC初始化、DMA收发描述符配置与中断收包流程理解静态内存池划分与协议选项裁剪思路并借助httpd、mdns、socket等应用层示例完成从底层驱动到上层接口的贯通也可将map与lst作为排查内存占用和链接问题的参照。1. 一块没上系统的 GD32F407怎么把 ping 打通手上这块 GD32F407 板子插着网线串口里能打印 CS8900 的移植日志但 PC 那边ping 192.168.1.100一直返回Request timed out。没有 RTOS没有tcpip_thread甚至连信号量都没有——这就是裸机移植 LWIP 的起点。GD32F4 系列的 ENET 外设和常见的那套 STM32F4 以太网 MAC 高度同源但引脚复用、RCU 时钟门控、DMA 描述符地址对齐这几处细节一旦对不上表现出来就是「PHY 显示 link up、协议栈一声不响」。这篇笔记拆的就是这条链路从 RMII 时钟怎么喂、lwipopts.h 里六个必须自己算的参数、ethernetif.c 四个函数怎么改一直到 ping 通之后怎么用丢包率和时延做回归。适合已经能把工程编译通过、但卡在「网口就是不通」这一步的开发者。2. 裸机模型与 ENET 外设先把数据通路对上再谈移植移植 LWIP 这件事玄学感最强的地方不在协议栈本身而在「数据到底有没有从 PHY 走进内存」。协议栈是纯软件逻辑写错了顶多跑不通硬件通路上有一个环节没配好现象和软件 bug 一模一样但排查方向完全相反。所以动手之前先把三件事定下来PHY 接口用 RMII 还是 MII、裸机下收包走哪种模型、DMA 描述符和缓冲区放在哪块 RAM。2.1 RMII 还是 MII引脚、时钟和 PHY 地址的三笔账MII 要 16 根信号线RMII 只要 9 根代价是 MAC 和 PHY 必须共享一个 50MHz 参考时钟。GD32F407 上这个选择不是软件配置出来的而是硬件决定之后在 SYSCFG 里写一位。RMII 模式下需要把SYSCFG_CFG1的ENET_PHY_SEL置位很多工程忘了这一步结果 MAC 一直按 MII 的时序去采样收到的全是垃圾帧。50MHz 参考时钟有两种常见供法账要算清楚供法走线优点代价PHY 侧 25MHz 晶振 内部 PLL 倍频到 50MHz从 REF_CLK 引脚输出给 MCUPHY 出、MCU 进PA1MCU 时钟配置简单不占 MCOPHY 的 PLL 上电稳定需要时间初始化要留延时MCU 用 MCO 输出 25MHz 给 PHY 的 XTAL1MCU 出、PHY 进省一颗晶振MCO 分频必须精确 25MHzPA8 要配成 AF0且 PHY 内部再倍频引入额外抖动我自己更倾向第一种理由是出问题时可以用示波器直接量 PHY 的 REF_CLK 引脚有 50MHz 方波就说明时钟这一层没问题排查范围立刻收窄一半。第二种供法一旦 MCO 频率偏了现象是「偶尔能通几个包」比彻底不通更难查。PHY 地址是第二个容易翻车的地方。MDIO 只有 5 位地址空间031LAN8720A 由 PHYAD0 引脚决定是 0x00 还是 0x01DP83848 常见 0x01。板子上的下拉电阻焊没焊、焊的是 4.7k 还是 10k都会改变地址。与其翻原理图不如上电直接扫一遍/* 遍历 MDIO 0~31读 PHYIDR1(0x02)/PHYIDR2(0x03)全 0xFFFF 视为该地址无器件 */ static uint8_t phy_scan(void) { for (uint8_t addr 0; addr 32; addr) { uint16_t id1 enet_phy_read(addr, 0x02); uint16_t id2 enet_phy_read(addr, 0x03); if ((id1 ! 0xFFFF) (id1 ! 0x0000)) { printf(PHY addr%u id%04X%04X\r\n, addr, id1, id2); return addr; } } return 0xFF; /* 一个都没扫到先回去查 MDC/MDIO 引脚和 PHY 供电 */ } /* GD32 的 MDIO 读PHY 地址在 [15:11]寄存器地址在 [10:6]置 MB 后等硬件清位 */ static uint16_t enet_phy_read(uint8_t phy_addr, uint8_t reg) { ENET_MAC_PHY_CTL ((uint32_t)phy_addr 11) | ((uint32_t)reg 6) | ENET_MAC_PHY_CTL_MB; uint32_t timeout 0x000FFFFF; while ((ENET_MAC_PHY_CTL ENET_MAC_PHY_CTL_MB) --timeout) { } if (timeout 0) { return 0xFFFF; /* 超时MDC 没波形或者 PHY 没上电 */ } return (uint16_t)(ENET_MAC_PHY_DATA 0xFFFF); }phy_scan的返回值直接写进ethernetif-phy_addr后面链路检测全用它。这里有个参数细节ENET_MAC_PHY_CTL_MB是硬件自清位轮询它比死等固定延时靠谱超时值给 0xFFFFF 是经验值主频 168MHz 下大约几十毫秒足够覆盖一次 MDIO 事务又不会在 PHY 掉电时把主循环卡死。2.2 NO_SYS1 下的三种收包模型裸机意味着没有tcpip_threadnetif-input最终会同步走到ethernet_input整个协议栈都在调用者的上下文里跑。收包模型就三种选错了要么丢包要么在主循环里埋一颗定时炸弹。纯轮询主循环里不停查接收描述符的 OWN 位。实现最简单代价是主循环转得再快也有间隙100M 线速下几个包连发就可能覆盖还没处理的描述符。中断置标志 主循环处理ENET 的接收中断里只做一件事——把描述符从 DMA 环上摘下来挂到一个软件队列然后置rx_pending标志主循环看到标志再调ethernetif_input。这是我最常用的做法中断服务程序短到几十个周期重活全在主循环不会出现「中断里跑 TCP 状态机」这种让时序彻底失控的场面。中断里直接处理能跑但等于把协议栈塞进中断上下文任何一次pbuf_alloc失败或者 ARP 表操作都会拉长中断响应时间。串口、定时器这些外设的中断会被拖后现象是「网通了串口偶发丢字节」。选第二种的时候要注意一件事软件队列的长度必须大于等于接收描述符数量否则中断里摘下来的描述符没地方放还是得丢。我一般让描述符和队列长度都是 4 的幂位掩码取模比取余快也不容易写错边界。2.3 描述符、pbuf 与内存布局CCM 是个陷阱GD32F407 有 192KB SRAM其中 64KB 是 CCMCore Coupled Memory地址从 0x10000000 起。CCM 直连内核CPU 访问零等待看起来很香——但 ENET 的 DMA 走的是 AHB 总线矩阵根本访问不到 CCM。描述符或者收发缓冲区一旦被链接脚本分配到 CCMDMA 读到的就是一片无效地址表现是发送计数涨、接收计数不动或者收到的帧长度全是 0而且不会有任何硬件报错。这类问题的排查成本极高因为代码逻辑完全正确。血泪经验是在链接脚本里给以太网缓冲单独开一个段显式落在 0x20000000 起始的 SRAM1 上并且加编译期断言。/* 描述符必须 4 字节对齐缓冲区大小按最大帧 1518 向上取整 */ #define ENET_RX_DESC_NUM 4 #define ENET_TX_DESC_NUM 4 #define ENET_FRAME_MAX 1524 /* 用 section 属性把 DMA 可见的内存钉在 SRAM1别让它掉进 CCM */ __attribute__((section(.enet_dma), aligned(4))) static enet_dma_desc_t g_rx_desc[ENET_RX_DESC_NUM]; __attribute__((section(.enet_dma), aligned(4))) static enet_dma_desc_t g_tx_desc[ENET_TX_DESC_NUM]; __attribute__((section(.enet_dma), aligned(4))) static uint8_t g_rx_buf[ENET_RX_DESC_NUM][ENET_FRAME_MAX]; __attribute__((section(.enet_dma), aligned(4))) static uint8_t g_tx_buf[ENET_TX_DESC_NUM][ENET_FRAME_MAX]; /* 编译期兜底地址落在 0x10000000 段就直接报错别等到板子上抓瞎 */ _Static_assert(sizeof(g_rx_desc) ENET_RX_DESC_NUM * 16, desc size mismatch);链接脚本里对应加一段.enet_dma (NOLOAD) : { *(.enet_dma) } SRAM1。NOLOAD很关键这段内存不需要初始化加了它启动文件的.data拷贝会跳过省下几毫秒启动时间。_Static_assert那句检查的是描述符结构体大小ENET 的描述符是 4 个 32 位字共 16 字节如果哪天结构体定义被人加了字段这里会直接编译失败比运行时收到错帧再回头查强得多。另外一个容易忽略的点是 pbuf 的对齐。PBUF_POOL_BUFSIZE必须按MEM_ALIGNMENTCortex-M4 上是 4向上取整LwIP 内部的mem_malloc会对齐但pbuf_alloc从池里切的时候如果大小不是 4 的倍数后续的memcpy会走非对齐访问M4F 支持非对齐但会多花周期极端情况下配合 DMA 缓冲区偏移还会踩到总线错误。3. lwipopts.h 与 ethernetif.c把协议栈焊进工程的两个文件LWIP 的移植面其实很窄真正需要动的就两个文件lwipopts.h决定协议栈的形态和内存预算ethernetif.c决定数据怎么进出 MAC。剩下的sys_arch.c在 NO_SYS1 下只剩几个符号要补。把这三块拆清楚移植工作量从「几天」压到「半天」。3.1 lwipopts.h 里必须自己算的六个参数网上流传的lwipopts.h大多是抄来抄去值给得偏小能 ping 通但一跑 TCP 就 assert。下面这六个参数得按自己的场景算不能照抄宏建议值怎么算出来的NO_SYS1裸机必须为 1否则会去找sys_thread_newLWIP_SOCKET/LWIP_NETCONN0 / 0关掉 socket 和 netconn API只留 raw API代码体积能省 30KB 以上PBUF_POOL_SIZE16至少 2 倍于接收描述符数量留出 ARP 队列和 TCP 重传占用的余量PBUF_POOL_BUFSIZE1524最大帧 1518 2 字节对齐余量给 1516 会在收到 1518 字节带 VLAN tag 的帧时分配失败MEM_SIZE16 * 1024raw API 下mem_malloc主要给应用层缓存用16KB 够跑一个 echo 服务TCP_MSS14601500MTU- 20IP 头- 20TCP 头设大了会触发 IP 分片设小了浪费带宽PBUF_POOL_BUFSIZE这个值最容易被低估。给 1516 的时候绝大多数 ping 和 HTTP 请求都没问题但一旦对端发来带 802.1Q tag 的帧长度变 1522pbuf_alloc返回 NULLlow_level_input里如果没判空就直接memcpyHardFault 就来了。这种事现场调试能查一整天因为「平时都是好的」。另外两个先关掉、跑通之后再考虑开的开关/* 第一版先关硬件校验和卸载用软件算把变量降到最少 */ #define CHECKSUM_BY_HARDWARE 0 /* 发送时如果 pbuf 是链直接拷进单块 DMA 缓冲避免描述符链的复杂度 */ #define LWIP_NETIF_TX_SINGLE_PBUF 1 /* 调试期把统计打开memp 的 used/max 是排查内存泄漏的唯一黑匣子 */ #define LWIP_STATS 1 #define MEM_STATS 1 #define MEMP_STATS 1 #define LWIP_STATS_DISPLAY 1CHECKSUM_BY_HARDWARE这一项值得单独说。GD32 的 ENET 确实支持 IP/TCP/UDP 校验和卸载但需要在 TDES0 的 CIC 位域里指定「哪些层由硬件算」而且初始化时要把伪首部的累加和预置进 TDES1 的高 16 位。只要偏移算错一个字节发出去的包校验和就是错的对端静默丢弃你在本机抓包看到的帧完全正常——这就是典型的「软件层查不出问题硬件层配置错了」。先用软件校验和把链路跑通再回头开硬件卸载并做 A/B 对比是更稳的顺序。3.2 ethernetif.c 的四个函数逐个改ethernetif.c是 LwIP 提供的模板四个函数需要按自己的硬件重写。这里面low_level_output和low_level_input是核心一个管发一个管收剩下的low_level_init管初始化ethernetif_input管把 pbuf 递交给协议栈。发送路径的关键是「单缓冲 补零到 60 字节」static err_t low_level_output(struct netif *netif, struct pbuf *p) { struct pbuf *q; uint32_t len 0; uint8_t *buf; /* 等上一轮的发送描述符被 DMA 释放128 次轮询大概是 1us 量级 */ uint32_t guard 128; while ((ENET_TDES0(g_tx_idx) ENET_TDES0_OWN) --guard) { } if (guard 0) { return ERR_MEM; /* 描述符还没空出来交给上层重试 */ } buf g_tx_buf[g_tx_idx]; for (q p; q ! NULL; q q-next) { if (len q-len ENET_FRAME_MAX) { return ERR_MEM; /* 超长帧直接拒截断发出去会污染对端 */ } memcpy(buf[len], q-payload, q-len); len q-len; } /* 以太网最小帧 60 字节不含 FCS不补齐对端会当 runt 丢弃 */ if (len 60) { memset(buf[len], 0, 60 - len); len 60; } ENET_TDES1(g_tx_idx) len; ENET_TDES2(g_tx_idx) (uint32_t)buf; /* 置 OWN 让 DMA 接管同时标记首片和末片单缓冲帧必须两个都置 */ ENET_TDES0(g_tx_idx) ENET_TDES0_OWN | ENET_TDES0_FD | ENET_TDES0_LD; if (ENET_DMA_STAT ENET_DMA_STAT_TBU) { ENET_DMA_TPOLL | ENET_DMA_TPOLL_TPD; /* 发送缓冲空闲时手动拉一次 */ } g_tx_idx (g_tx_idx 1) % ENET_TX_DESC_NUM; return ERR_OK; }guard那个轮询循环是最容易出争议的地方。理论上发送应该异步描述符满了就返回ERR_MEM让上层重试但裸机下没有tcpip_thread帮你重试raw API 的tcp_write返回ERR_MEM之后应用层必须自己处理。实践中给一个短轮询绝大多数情况下一次就过只有在连续大量发送时才会触发重试路径这样应用层代码能简单很多。接收路径的两个判断必须做static struct pbuf *low_level_input(struct netif *netif) { struct pbuf *p NULL, *q; uint32_t len; uint8_t *src; /* DMA 还占着这个描述符说明没有新帧 */ if (ENET_RDES0(g_rx_idx) ENET_RDES0_OWN) { return NULL; } /* 错误帧CRC 错、对齐错、runt 都汇总在 ES 位必须先归还描述符再返回 */ if (ENET_RDES0(g_rx_idx) ENET_RDES0_ES) { ENET_RDES0(g_rx_idx) ENET_RDES0_OWN; g_rx_idx (g_rx_idx 1) % ENET_RX_DESC_NUM; return NULL; } len (ENET_RDES0(g_rx_idx) ENET_RDES0_FL_MASK) ENET_RDES0_FL_SHIFT; p pbuf_alloc(PBUF_RAW, len, PBUF_POOL); if (p ! NULL) { src (uint8_t *)ENET_RDES2(g_rx_idx); for (q p; q ! NULL; q q-next) { memcpy(q-payload, src, q-len); src q-len; } } /* 无论分配成功与否描述符都要还给 DMA否则接收环跑几帧就卡死 */ ENET_RDES0(g_rx_idx) ENET_RDES0_OWN; g_rx_idx (g_rx_idx 1) % ENET_RX_DESC_NUM; return p; }这两个ENET_RDES0(g_rx_idx) ENET_RDES0_OWN是整个接收函数里最不能漏的语句。漏了第一处错误帧分支错误帧会一直占着描述符接收环很快耗尽漏了第二处正常路径现象是「ping 通第一个包之后再也没反应」因为 DMA 已经没有可写的描述符了。这两处我在不同项目里都踩过现在写完必查。3.3 sys_now 与 LWIP_RAND裸机下最容易漏的两个符号NO_SYS1让很多人以为sys_arch.c可以整个删掉编译时才发现链接器不认账。LwIP 内部至少还依赖两个符号sys_now()提供毫秒时基给 TCP 重传定时器、ARP 表老化、sys_check_timeouts用LWIP_RAND()给 TCP 初始序列号、DHCP 事务 ID 提供随机源。sys_now的实现有一个隐蔽的坑必须单调递增而且不能被优化成每次都从硬件计数器重读。用 SysTick 中断里累加一个volatile uint32_t是最稳的static volatile uint32_t g_ms_tick 0; /* SysTick_Handler 里每 1ms 调一次 */ void SysTick_Handler(void) { g_ms_tick; } /* LwIP 的时基单位毫秒32 位回绕在 49 天后裸机场景基本碰不到 */ u32_t sys_now(void) { return g_ms_tick; }如果 SysTick 被别的模块占用了做 1us 的调度那就得用DWT-CYCCNT换算但要注意CYCCNT是 32 位、168MHz 下 25 秒就回绕一次直接返回会触发 LwIP 的定时器逻辑错乱。这类问题特别隐蔽现象是「设备跑 25 秒之后 TCP 连接莫名断开」。LWIP_RAND在 LwIP 2.1 的opt.h里有默认定义会调rand()。裸机工程如果没链标准库的rand链接阶段报未定义引用如果链了但没调srand每次上电序列号都一样某些对端会触发 PAWS保护 Against Wrapped Sequence检查导致新建连接被拒。我一般直接覆盖掉/* 用 SysTick 的低位和 ADC 的噪声做种子避免每次上电序列号相同 */ u32_t LWIP_RAND(void) { static u32_t seed 0; if (seed 0) { seed SysTick-VAL ^ 0x5A5A5A5A; } seed seed * 1103515245u 12345u; return seed; }SysTick-VAL在启动时读出来是个不确定值配合固定的乘数就够用了不需要真随机。这个函数被调用的频率很低每次 TCP 建连一次性能不用考虑。4. 从 ping 到 TCP echo协议栈跑通的三级验证移植完成之后不要直接上应用按「链路 → ARP/ICMP → TCP」三级往下走每一级都有明确的判断依据。这样出问题时能立刻定位到是哪一层而不是对着一堆抓包文件猜。4.1 第一级PHY 自协商与链路状态PHY 的链路状态位在 BMSR寄存器 0x01的 bit2但这个位是 latch-low 的链路断过一次之后读出来是 0直到你读两次才恢复真实值。第一次读清 latch第二次读才是当前状态。这个细节在自协商过程中尤其重要因为协商期间链路会短暂 down 再 up。/* 每 100ms 在主循环里调一次返回 1 表示链路 up */ uint8_t phy_link_poll(uint8_t phy_addr) { /* 第一次读清 latch-low第二次读才是当前值这个顺序不能省 */ (void)enet_phy_read(phy_addr, 0x01); uint16_t bmsr enet_phy_read(phy_addr, 0x01); uint16_t bmcr enet_phy_read(phy_addr, 0x00); if (!(bmsr 0x0004)) { return 0; /* link down */ } /* bit11 是自协商完成bit8 是 100Mbit13 是全双工 */ if ((bmsr 0x0020) (bmcr 0x0100)) { printf(link up: 100M %s\r\n, (bmcr 0x2000) ? full : half); } else { printf(link up: 10M\r\n); } return 1; }判断里用 BMCR 而不是 BMSR 读速率是因为 BMSR 的 bit8/bit13 反映的是「对端通告能力」BMCR 的 bit8/bit13 才是「本端当前协商结果」。半双工在 100M 下必须开 CSMA/CDLwIP 的netif-flags里有对应的处理配错了现象是「小流量正常、大流量丢包严重」。链路状态变化的时候要通知协议栈调netif_set_link_up/netif_set_link_down。开了LWIP_NETIF_LINK_CALLBACK的话这两个函数会回调netif_status_callback可以在里面重连 DHCP 或者清 ARP 表。4.2 第二级ARP 与 ICMPping 通的判断顺序ping 不通的时候按下面的顺序走每一步都有明确的观测点不要跳步看 ENET 的收包计数。GD32 的 MAC 有Frames Received统计寄存器主循环里每秒打印一次。如果 PC 在 ping 而计数完全不涨问题在硬件层PHY、时钟、DMA协议栈一行都不用看。计数涨了但 ping 不通看netif-ip_addr、netif-netmask、netif-gw是否设置正确特别注意netif_set_up之后才能收包。IP 对了还不通看 ARP。PC 上arp -a有没有学到板子的 MAC。学不到说明 ARP 请求收到了但回复没发出去回头查low_level_output的描述符 OWN 位是不是被别的地方误清了。ARP 学到了但 ICMP 不回查lwipopts.h里的LWIP_ICMP是否为 1以及ethernetif_input的返回值处理。/* 主循环里的收包入口返回值不是 ERR_OK 时必须 free否则内存池几秒就漏光 */ void ethernetif_input(struct netif *netif) { struct pbuf *p; do { p low_level_input(netif); if (p ! NULL) { if (netif-input(p, netif) ! ERR_OK) { pbuf_free(p); /* 上层拒收所有权还在我们手上 */ } } } while (p ! NULL); /* 一次把接收环取空别留到下一轮 */ }do-while循环一次性把接收环清空这一点在 4 个描述符的配置下很重要。如果主循环一次只处理一个包而 PC 正在快速发 ARP 请求描述符很快被 DMA 覆盖丢包率会莫名其妙很高。4.3 第三级TCP echo 服务端的最小实现TCP 跑通意味着 pbuf 的分配释放、序列号管理、超时重传都正常了是比 ping 强得多的验证。裸机下用 raw API代码不长但有两个所有权陷阱。static err_t echo_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) { if (err ! ERR_OK) { if (p ! NULL) pbuf_free(p); return err; } if (p NULL) { /* 对端发了 FIN关连接并清回调顺序不能反 */ tcp_arg(tpcb, NULL); tcp_recv(tpcb, NULL); tcp_close(tpcb); return ERR_OK; } /* 收到的数据原样写回tcp_write 只是入队要 tcp_output 才真的发 */ if (tcp_sndbuf(tpcb) p-tot_len) { tcp_write(tpcb, p-payload, p-tot_len, TCP_WRITE_FLAG_COPY); tcp_output(tpcb); } /* 通知协议栈这段数据已消费窗口才会往前推漏了这句大流量下会卡死 */ tcp_recved(tpcb, p-tot_len); pbuf_free(p); return ERR_OK; } static err_t echo_accept(void *arg, struct tcp_pcb *newpcb, err_t err) { tcp_recv(newpcb, echo_recv); tcp_arg(newpcb, NULL); tcp_nagle_disable(newpcb); /* echo 场景关掉 Nagle否则小包合并会加延迟 */ return ERR_OK; }两个陷阱写在注释里了但值得再说一遍tcp_write带TCP_WRITE_FLAG_COPY时会自己拷一份数据所以pbuf_free是安全的如果不带这个标志tcp_write只是引用指针pbuf_free之后 DMA 发出去的就是已释放内存现象是「收到的回显数据偶尔是乱码」。tcp_recved漏调不会立刻出错但接收窗口会逐渐收缩到 0对端发不动数据表现为「TCP 连接还在但传不了东西」。5. 避坑五个把调试时间从半天拖到一周的坑下面这五条都是实际调试中记录下来的按「现象 → 原因 → 解决」写每条都对应前面某一节的具体配置。看的时候对照自己的工程中了的立刻改。5.1 链路与 DMA 层的坑现象一PHY 显示 link upENET 的接收帧计数一直是 0。原因DMA 描述符或者收发缓冲区被链接脚本分配到了 CCM0x10000000 起始的 64KB。CPU 能读写这块内存所以代码里的memset、memcpy都不会报错但 ENET 的 DMA 走 AHB 总线物理上访问不到 CCM读到的全是无效数据。解决用__attribute__((section(.enet_dma)))把描述符和缓冲区钉到 SRAM1链接脚本里显式声明这一段并在启动后打印一下缓冲区地址确认在 0x20000000 范围内。现象二能 ping 通十几个包然后彻底没反应复位后重复。原因接收描述符数量太少常见是配了 2 个而主循环里还有其他耗时任务DMA 在描述符还没被处理的时候就绕回来覆盖了。ENET 的 DMA 是环形结构CPU 处理速度跟不上就丢帧丢到一定程度描述符的 OWN 位状态错乱环就死了。解决描述符数量至少 4 个处理速度跟不上的场景给 8 个ethernetif_input里用do-while一次取空接收环别一个主循环只处理一个包。现象三小包 ping 通ping -l 1400全丢。原因g_rx_buf每个缓冲区只分配了 512 字节或者PBUF_POOL_BUFSIZE给得比 MTU 小。大帧进来时 DMA 写入超出缓冲区范围直接踩坏相邻内存或者描述符的帧长度字段读到异常值。解决接收缓冲区每个至少 1524 字节PBUF_POOL_BUFSIZE同样给 1524如果 SRAM 紧张宁可减描述符数量也别减缓冲区大小。5.2 内存与协议栈配置层的坑现象四跑几分钟后pbuf_alloc返回 NULL串口里memp_malloc: out of memory刷屏。原因PBUF_POOL_SIZE太小或者应用层收到 pbuf 之后没释放。最容易漏的是netif-input返回值不是ERR_OK的分支——上层拒收时 pbuf 的所有权还在调用者手上不pbuf_free就漏一个。解决打开LWIP_STATS和MEMP_STATS每秒打印一次memp_pbuf_pool的 used/max看 used 是不是单调上涨。同时检查ethernetif_input和echo_recv里所有pbuf_free的路径。现象五开了CHECKSUM_BY_HARDWARE之后 TCP 连不上但 ARP 和 ICMP 正常。原因GD32 的校验和卸载需要在 TDES0 的 CIC 位域指定卸载层级并且把伪首部累加和预置到 TDES1 的高 16 位。ICMP 走的是 IP 层校验和配置错也可能碰巧对TCP 要同时算伪首部偏移错一个字节就全错对端静默丢弃。解决先把CHECKSUM_BY_HARDWARE设回 0 用软件校验和验证链路再单独开硬件卸载用 Wireshark 抓包对比开关前后 TCP 头的 checksum 字段确认一致再上生产。提示这五条里有三条是「代码逻辑正确但硬件配置错」现象和软件 bug 无法区分。建议在low_level_init结束时打印一行完整的配置摘要PHY 地址、描述符地址、缓冲区地址、RMII 模式位、时钟源出问题时先看这一行。6. 用 ping 时延和吞吐做回归把移植成果变成可量化的指标「能 ping 通」只说明协议栈活着不说明它健康。每次改配置、换 PHY、调描述符数量之后如果没有量化指标很容易把一次性能退化当成「网络本来就这样」。我现在固定用三个指标做回归都在 PC 侧一条命令能跑完。指标测法裸机 LwIP 的合理区间退化通常意味着丢包率ping -n 200 -l 1400 ip0%偶发 0.5% 以内可接受描述符不够或缓冲区太小平均时延同上看 avg13ms百兆直连主循环里有阻塞操作或者 Nagle 没关TCP 吞吐iperf3 或自写的 sink 服务单缓冲 memcpy 方式 1020Mbps零拷贝能到 40Mbpspbuf 拷贝次数多或者TCP_SND_BUF太小吞吐这一项差距很大原因全在low_level_output的拷贝次数上。上面给的实现是「pbuf 链 → 单块 DMA 缓冲」一次拷贝配合LWIP_NETIF_TX_SINGLE_PBUF1在 168MHz 的 GD32F407 上实测能到 15Mbps 左右。要往上走就得做零拷贝让low_level_output直接把 pbuf 的 payload 地址填进描述符用多描述符链式发送省掉那次memcpy。代价是 pbuf 的生命周期要跟 DMA 绑定发送完成中断里才能pbuf_free中断处理复杂度上一个台阶。还有一个我每次都做的动作把 ENET 的统计寄存器Frames Received、Frames Transmitted、Receive CRC Error、Alignment Error在每次回归前后各读一次差值记在表格里。CRC Error 涨说明物理层有问题网线、变压器、时钟抖动Alignment Error 涨说明帧长度异常这两类错误在协议栈层面完全看不出来。这个习惯救过我一次某块板子 ping 时延从 2ms 抖到 20ms协议栈日志一切正常最后是 CRC Error 计数在以每秒几十个的速度上涨换了一根网线就好了。# 回归脚本200 个大包 200 个小包输出丢包和时延分布 ping -n 200 -l 1400 192.168.1.100 big.txt ping -n 200 -l 32 192.168.1.100 small.txt grep -E Lost|Average big.txt small.txt大包和小包分开测是有讲究的。小包考验的是中断响应和协议栈处理速度大包考验的是缓冲区大小和 DMA 搬运能力两类问题的现象完全不同混在一起测会互相掩盖。从那以后我每次改完lwipopts.h或者碰了ethernetif.c都强制走一遍「小包 200 大包 200 吞吐 统计寄存器差值」这套流程哪怕只改了一个宏。裸机环境没有内存保护一次配置失误可能要花两天才能从现象倒推回原因而跑一遍回归只要五分钟。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑