☰
GD32F407裸机移植lwIP:以太网DMA、pbuf与网络调试实战
2026/10/26 18:52:52 网站建设 项目流程

简介:面向GD32F407裸机网络开发的LWIP移植工程包,适合已有Cortex-M4与以太网基础、希望在无操作系统环境下为MCU补齐TCP/IP通信能力的嵌入式开发者。内容围绕以太网MAC驱动适配、lwipopts配置、静态内存池管理、接收中断处理、定时轮询与应用层Socket接口展开,可帮助读者理解裸机协议栈的初始化流程、数据收发路径及常见丢包与内存问题排查。压缩包共641个文件,约12.15MB,以216个h头文件、165个c源文件为主,并含Keil工程、编译中间文件、说明文本及少量网页与脚本,便于直接打开工程对照源码学习。已有1317人学习下载,可作为课程设计、工业控制联网或智能家居节点开发时的移植参考与调试素材。

1. 从一块点不亮的网口说起:GD32F407 裸机跑 lwIP 到底难在哪

GD32F407 无操作系统移植 LWIP,最容易翻车的不是协议栈代码,而是把裸机工程按带系统的思路接:中断里直接喂协议栈、sys_now没实现、pbuf 和 DMA 缓冲区共用一块内存。网口灯亮不代表能 ping 通,能 ping 通也不代表 TCP 能跑。这套环境适合已经能把 GD32F407 点灯、串口打印调通,想让板子直接跑静态 IP 或 DHCP 的开发者;如果连 PHY 地址、RMII 引脚和 50MHz 参考时钟都没确认,先别急着编译协议栈。

2. 环境清单与工程骨架:GD32F407 移植 lwIP 前先对齐这四样

2.1 芯片、PHY 与接口模式确认

GD32F407 的以太网 MAC 可以接 RMII 或 MII 的 PHY,裸机移植 lwIP 第一步不是往工程里塞源码,而是把板子上的硬件事实固定下来。PHY 地址、复位引脚、RMII 参考时钟来源、MDIO/MDC 引脚,这四项只要有一项和例程不一致,后面所有配置都像在猜。常见做法是先把原理图翻到以太网那页,把 PHY 地址拨码或 strap 电阻确认清楚,再拿串口把 PHY ID 读出来打印。

检查项常见取值怎么确认
MAC 接口模式RMII看原理图 REF_CLK 是否接 50MHz
PHY 地址0 或 1看 PHY 地址 strap 电阻,或扫 MDIO 地址
PHY 复位普通 GPIO看复位引脚是否由 MCU 控制
MDIO/MDC固定复用脚对照芯片数据手册的引脚复用表
参考时钟50MHz用示波器看 REF_CLK 是否有波形

我一般会先写一个不跑 lwIP 的小测试,只初始化 GPIO 和 MDIO,把 PHY 的 ID 寄存器读出来。读不到就停在硬件层查,不把问题带进协议栈。这个顺序看起来慢,实际省时间。

# 把工程里与以太网相关的文件先列出来,避免拿到旧例程后混入两套驱动 find . -maxdepth 4 -type f | grep -Ei 'enet|ethernet|phy|lwip|netif'

这段命令只做一件事:把工程里所有可能和以太网、PHY、lwIP 相关的文件摊开。重点看有没有两个版本的ethernetif.c,有没有把带系统的sys_arch.c一起塞进裸机工程。参数不用改,路径按你的工程根目录替换。列出来的文件如果超过两套命名,先删旧留新,别一边编译一边猜链接顺序。

2.2 源码目录与编译宏的放置

lwIP 的源码目录不要直接平铺到工程根目录,裸机工程最怕头文件搜索路径一锅粥。常见做法是建一个独立的third_party/lwip,下面分src、include、port三块,port里只放lwipopts.h、ethernetif.c、sys_now.c这些和板子强相关的文件。这样以后换 PHY 或换工程,协议栈本体不动,只改 port 层。

mkdir -p third_party/lwip/src \ third_party/lwip/include \ third_party/lwip/port # 目录只建骨架,源码按你手头 lwIP 2.x 的目录结构拷入 # port 层放:lwipopts.h、ethernetif.c、sys_now.c

这段命令建的是目录骨架,不是让你把源码解压到根目录。src放协议栈实现,include放公共头文件,port放板级适配。参数上唯一要改的是third_party/lwip这个前缀,按你的工程习惯替换成middlewares/lwip也可以。建完后把编译器头文件搜索路径指到include和port,顺序上port放在前面,保证lwipopts.h先被找到。

提示:裸机工程里不要保留sys_arch.c的线程、信号量实现,NO_SYS=1下这些文件即使不参与链接,也容易让后来接手的人误以为要跑系统。

2.3 最小工程骨架与文件分组

工程骨架按“启动文件、芯片外设、以太网驱动、lwIP port、应用”五组放,编译顺序上先芯片后驱动再协议栈。应用层只留一个main.c,主循环里做三件事:轮询网卡输入、跑 lwIP 超时、做链路检测。不要把 DHCP 定时器、ARP 定时器手动塞进 SysTick 中断,lwIP 2.x 的超时机制由sys_check_timeouts()统一收口。

/* main.c 的最小骨架:只体现初始化顺序,业务代码放后面 */ #include "gd32f4xx.h" #include "lwip/init.h" #include "lwip/netif.h" #include "lwip/dhcp.h" #include "ethernetif.h" struct netif gnetif; static ip4_addr_t ipaddr, netmask, gw; int main(void) { systick_config(); /* 1ms 时基,sys_now 依赖它 */ lwip_init(); /* 协议栈初始化,放在 netif_add 之前 */ IP4_ADDR(&ipaddr, 192, 168, 10, 50); IP4_ADDR(&netmask, 255, 255, 255, 0); IP4_ADDR(&gw, 192, 168, 10, 1); /* netif_add 会回调 ethernetif_init,完成 MAC 和 DMA 初始化 */ netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ethernetif_init, ethernet_input); netif_set_default(&gnetif); netif_set_up(&gnetif); while (1) { ethernetif_input(&gnetif); /* 裸机轮询收包 */ sys_check_timeouts(); /* 处理 ARP、DHCP、TCP 超时 */ ethernetif_link_check(&gnetif); } }

这段骨架的关键在顺序:systick_config()必须早于任何依赖sys_now()的协议栈调用;lwip_init()要在netif_add()前;netif_add()的最后一个参数是输入函数ethernet_input,不是你自己写的ethernetif_input。参数里的 IP 地址只是静态示例,跑 DHCP 时也先给一个占位地址,后面用dhcp_start()覆盖。ethernetif_link_check()不是 lwIP 自带函数,需要在 port 层自己实现,用来读 PHY 状态并调用netif_set_link_up/down。

2.4 编译选项和链接脚本的检查

裸机 lwIP 的编译选项不需要花哨,重点是结构体对齐、优化等级和链接脚本里的堆栈。lwipopts.h里MEM_ALIGNMENT要和编译器对齐一致,通常 4 字节;以太网 DMA 描述符和收发缓冲区要按 4 字节对齐,链接脚本里不要把它们放到未对齐的段。堆栈方面,主栈至少留 2KB,lwIP 的MEM_SIZE单独走内存池,不和启动文件的 heap 混在一起。

# 先做一次干净编译,确认没有混入带系统的头文件 make clean make -j4 2>&1 | tee build.log # 重点看 build.log 里有没有 sys_arch.h、semphr.h、thread.h 这类带系统的头

make clean是为了清掉旧对象文件,避免上一版ethernetif.o还在链接时被带进来。-j4按机器核数改,编译失败时tee build.log留下的日志比终端滚屏好查。检查日志里如果出现sys_arch.h、semphr.h、thread.h,说明有人把带系统的 port 层带进来了,NO_SYS=1下要先清掉。

编译项建议值说明
优化等级-O1或-O2先别上-O3,调试收包问题时变量容易被优化
结构体对齐4 字节和MEM_ALIGNMENT保持一致
浮点 ABI按工程统一不要只给 lwIP 文件换 ABI
链接段描述符单独段用aligned(4)或链接脚本对齐
启动文件堆栈主栈 ≥ 2KB协议栈内存走MEM_SIZE,不靠 heap

3. 网卡驱动落地:GD32F407 以太网 DMA 描述符与 pbuf 怎么对接

3.1 以太网外设初始化顺序

GD32F407 的以太网外设初始化顺序错了,最常见的表现是 MDIO 读不到 PHY、DMA 描述符挂上去不触发。顺序按“时钟、GPIO 复用、PHY 复位、MAC 配置、DMA 描述符、使能收发”走,中间任何一步都不要提前开中断。血泪经验是先把 PHY 复位和 MDIO 读 ID 单独调通,再开 MAC 和 DMA,否则日志里只有“收不到包”,没有更细的线索。

/* ethernetif_init 里的硬件初始化顺序,外设库函数名按工程现有版本替换 */ static void low_level_init(struct netif *netif) { uint8_t mac[6] = {0x02, 0x00, 0x00, 0x12, 0x34, 0x56}; rcu_periph_clock_enable(RCU_ENET); /* MAC 时钟 */ rcu_periph_clock_enable(RCU_ENETTX); /* 发送时钟 */ rcu_periph_clock_enable(RCU_ENETRX); /* 接收时钟 */ enet_gpio_config(); /* RMII 引脚复用,按原理图改 */ phy_reset(); /* 硬复位 PHY,至少延时 10ms */ enet_init(); /* MAC 基本配置 */ enet_mac_address_set(ENET_MAC_ADDRESS0, mac); dma_desc_chain_init(); /* 描述符和缓冲区绑定 */ enet_rx_enable(); enet_tx_enable(); netif->hwaddr_len = ETHARP_HWADDR_LEN; memcpy(netif->hwaddr, mac, 6); netif->mtu = 1500; netif->flags |= NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP; }

这段代码的重点不是函数名,而是顺序和边界。RCU_ENET、RCU_ENETTX、RCU_ENETRX三路时钟缺一路都可能出现“发送有计数、接收没反应”。phy_reset()后要留够复位时间,常见做法是拉低 10ms 以上再释放。MAC 地址用本地管理地址,别和局域网里已有设备撞。netif->flags里先不要急着加NETIF_FLAG_LINK_UP,等 PHY 自协商完成后再由链路检测函数设置。

3.2 DMA 描述符和收发缓冲区的内存布局

GD32F407 的以太网 DMA 靠描述符链搬数据,描述符里的 OWN 位表示当前归 DMA 还是归 CPU。接收缓冲区、发送缓冲区、描述符数组三块内存要分开对齐,常见做法是描述符按 4 字节对齐,缓冲区按 4 字节对齐,接收缓冲区每个至少 1524 字节。不要把所有东西塞进一个uint8_t buf[4096]再手工切,后面查 OWN 位卡死时连黑匣子都没有。

/* 描述符和缓冲区布局:数组用 aligned(4) 保证 DMA 访问对齐 */ typedef struct { volatile uint32_t status; uint32_t controlsize; uint32_t buffer1; uint32_t buffer2; } enet_dma_desc_t; #define RX_DESC_NUM 4 #define TX_DESC_NUM 4 #define ETH_RX_BUF_SIZE 1524 static enet_dma_desc_t rx_desc[RX_DESC_NUM] __attribute__((aligned(4))); static enet_dma_desc_t tx_desc[TX_DESC_NUM] __attribute__((aligned(4))); static uint8_t rx_buf[RX_DESC_NUM][ETH_RX_BUF_SIZE] __attribute__((aligned(4))); static uint8_t tx_buf[TX_DESC_NUM][ETH_RX_BUF_SIZE] __attribute__((aligned(4))); /* 初始化时把每个描述符的 buffer1 指向对应缓冲区,并设置 OWN 位 */

这里RX_DESC_NUM和TX_DESC_NUM是描述符数量,裸机入门先给 4 个就够,数量上去会多吃内存。ETH_RX_BUF_SIZE取 1524 是为了覆盖最大以太网帧,别取 1500 后把 CRC 或填充字节截掉。aligned(4)是让 DMA 访问不产生对齐异常,链接脚本如果自己分段,也要保证这些数组落在对齐段里。描述符初始化时接收描述符的 OWN 位要置给 DMA,发送描述符的 OWN 位先清掉。

3.3 ethernetif.c 的 low_level_init/low_level_output

ethernetif.c是 lwIP 和 GD32F407 以太网 DMA 之间的桥。low_level_output只做一件事:把 pbuf 链里的数据拷贝到发送缓冲区,挂到空闲发送描述符上,然后置 OWN 位。不要在low_level_output里直接pbuf_free,也不要把 pbuf 的指针直接塞给 DMA,pbuf 的内存可能来自内存池,生命周期不受网卡控制。

static err_t low_level_output(struct netif *netif, struct pbuf *p) { struct pbuf *q; uint16_t len = 0; uint32_t idx = tx_next_idx; if (tx_desc[idx].status & 0x80000000u) { /* OWN 还没释放,发送忙 */ return ERR_MEM; } for (q = p; q != NULL; q = q->next) { if (len + q->len > ETH_RX_BUF_SIZE) { return ERR_MEM; } pbuf_copy_partial(q, &tx_buf[idx][len], q->len, 0); len += q->len; } tx_desc[idx].buffer1 = (uint32_t)&tx_buf[idx][0]; tx_desc[idx].controlsize = len; tx_desc[idx].status = 0x80000000u | 0x20000000u | 0x10000000u; tx_next_idx = (idx + 1) % TX_DESC_NUM; return ERR_OK; }

这段代码里0x80000000u是发送描述符的 OWN 位,0x20000000u和0x10000000u分别是 LS 和 FS 标志,具体位名按你工程头文件核对。tx_next_idx是发送描述符轮转下标,ERR_MEM表示当前没有空闲描述符,让上层稍后重试。pbuf_copy_partial按 pbuf 链逐段拷贝,避免 pbuf 跨段时只拷了第一段。发送完成后不要在这里释放 pbuf,lwIP 的ethernet_output会在返回ERR_OK后处理。

注意:如果发送描述符一直卡在 OWN,先看是不是low_level_output在描述符忙时没有返回ERR_MEM,而是死等;裸机主循环里死等会把收包也拖住。

3.4 PHY 链路检测与中断/轮询取舍

裸机下我一般用轮询做链路检测,500ms 读一次 PHY 基本状态寄存器,比在中断里处理链路变化更容易排查。PHY 中断当然能用,但中断服务里只置标志,真正的netif_set_link_up/down放到主循环做。这样链路变化和协议栈状态不会在中断上下文里打架。

void ethernetif_link_check(struct netif *netif) { static uint32_t last = 0; uint16_t bsr; if (sys_now() - last < 500) { return; } last = sys_now(); bsr = phy_read(PHY_ADDR, PHY_BSR); /* 读 PHY 基本状态 */ if (bsr & 0x0004) { /* LINK UP 位,按 PHY 手册核对 */ if (!netif_is_link_up(netif)) { netif_set_link_up(netif); } } else { if (netif_is_link_up(netif)) { netif_set_link_down(netif); } } }

sys_now() - last < 500做的是 500ms 节流,避免主循环跑太快把 MDIO 读爆。PHY_ADDR按原理图上的 PHY 地址改,PHY_BSR是基本状态寄存器,LINK UP 位不同 PHY 可能略有差异,按手册核对。链路从 down 到 up 后,lwIP 会重新走 ARP 和 DHCP,不要在链路 up 的瞬间手动dhcp_start多次,容易把 DHCP 状态机打乱。

4. lwIP 配置裁剪:NO_SYS=1 下把内存、pbuf 与超时管住

4.1 lwipopts.h 的关键参数

裸机移植 lwIP 的配置项不用全改,先把NO_SYS=1、socket/netconn 关掉、内存对齐和 pbuf 池定下来。MEM_SIZE是 lwIP 自己的堆,PBUF_POOL_SIZE是 pbuf 池数量,两个都吃 RAM。GD32F407 的 RAM 不算紧,但也不要把MEM_SIZE开到 64KB 再回头查为什么启动文件堆栈不够。

/* lwipopts.h:裸机最小配置,先让 ping 和 TCP 跑通 */ #define NO_SYS 1 #define SYS_LIGHTWEIGHT_PROT 0 #define LWIP_NETCONN 0 #define LWIP_SOCKET 0 #define LWIP_NETIF_LINK_CALLBACK 1 #define MEM_ALIGNMENT 4 #define MEM_SIZE (16 * 1024) #define PBUF_POOL_SIZE 16 #define PBUF_POOL_BUFSIZE 1524 #define ETH_PAD_SIZE 2 #define CHECKSUM_BY_HARDWARE 1 #define LWIP_ARP 1 #define LWIP_ICMP 1 #define LWIP_DHCP 1 #define TCP_MSS 1460 #define TCP_SND_BUF (4 * TCP_MSS) #define TCP_WND (4 * TCP_MSS)

NO_SYS=1是裸机的总开关,开了它就不需要sys_arch的线程和信号量。LWIP_NETCONN和LWIP_SOCKET关掉能省不少代码,裸机应用直接用 raw API。ETH_PAD_SIZE=2是在以太网头前留 2 字节,让后面的 IP 头按 4 字节对齐,这个值改了要同步检查接收缓冲区的拷贝偏移。CHECKSUM_BY_HARDWARE=1表示让 GD32F407 的以太网 MAC 做校验和,前提是驱动里正确设置了描述符的校验和插入位。

参数建议值作用
NO_SYS1关闭操作系统适配层
MEM_SIZE16KB 起lwIP 堆,TCP 收发吃这个
PBUF_POOL_SIZE16 起接收 pbuf 数量,太小会丢包
PBUF_POOL_BUFSIZE1524单个 pbuf 能装最大帧
ETH_PAD_SIZE2对齐用,改 0 要查 IP 头偏移
TCP_WND4×MSS 起窗口太小吞吐上不去

4.2 sys_now 与超时轮询

NO_SYS=1下 lwIP 仍然要知道时间,ARP 老化、DHCP 重试、TCP 重传都靠sys_now()。这个函数必须返回毫秒计数,通常由 SysTick 中断累加一个volatile uint32_t。不要在sys_now()里做耗时操作,也不要在中断里调用sys_check_timeouts(),超时处理放到主循环。

static volatile uint32_t g_ms_tick = 0; void SysTick_Handler(void) { g_ms_tick++; /* 1ms 累加,sys_now 的时基 */ } u32_t sys_now(void) { return (u32_t)g_ms_tick; /* lwIP 只要求单调递增的毫秒计数 */ }

g_ms_tick用volatile是因为中断和主循环都会访问它。SysTick_Handler里只做累加,不调用任何协议栈函数。sys_now()返回 32 位毫秒计数,溢出后回绕,lwIP 的超时比较用的是差值,正常使用不用手动处理回绕。主循环里sys_check_timeouts()的调用频率建议 1ms 到 10ms 一次,太慢会让 TCP 重传和 DHCP 重试显得迟钝。

4.3 静态 IP 与 DHCP 的裸机接法

第一次验证建议先用静态 IP 把 ping 跑通,再切 DHCP。静态 IP 下只有netif_set_up和netif_set_link_up到位,ARP 和 ICMP 就能工作。DHCP 则在链路 up 后调用dhcp_start,让sys_check_timeouts()去驱动 DHCP 定时器。不要在主循环里手动反复dhcp_start,状态机会乱。

/* 静态 IP 验证通过后,切 DHCP 只改这一小段 */ ip4_addr_t ipaddr, netmask, gw; IP4_ADDR(&ipaddr, 192, 168, 10, 50); IP4_ADDR(&netmask, 255, 255, 255, 0); IP4_ADDR(&gw, 192, 168, 10, 1); netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ethernetif_init, ethernet_input); netif_set_default(&gnetif); netif_set_up(&gnetif); netif_set_link_up(&gnetif); /* PHY 已 link 时才置位 */ dhcp_start(&gnetif); /* 裸机下由 sys_check_timeouts 推进 */

这段代码里netif_set_link_up要在 PHY 真正 link 后调用,提前置位会让 DHCP 以为链路可用。dhcp_start只调用一次,失败后由 lwIP 自己重试。静态 IP 和 DHCP 不要同时手工设置地址,dhcp_start会接管地址配置。调试时先看串口有没有打印出分配到的 IP,再看 PC 端 ARP 表里有没有板子的 MAC。

4.4 内存池与堆的取舍

lwIP 的内存分两块:MEM_SIZE管的堆和PBUF_POOL_SIZE管的 pbuf 池。TCP 发送缓存、ARP 表、控制块从堆里出;网卡接收的 pbuf 从池里出。裸机工程常见的翻车是只调大MEM_SIZE,结果接收 pbuf 不够,ping 大包就丢;或者只调大 pbuf 池,TCP 一压就分配失败。两个都要看,先把PBUF_POOL_SIZE给够,再按 TCP 窗口调MEM_SIZE。

现象先看哪个参数调整方向
ping 小包通、大包丢PBUF_POOL_BUFSIZE确认不小于 1524
TCP 连接建立后发不出数据MEM_SIZE、TCP_SND_BUF适当加大堆和发送缓存
接收一段时间后死机PBUF_POOL_SIZE、pbuf_free查 pbuf 是否泄漏
初始化就分配失败MEM_SIZE先给 16KB 再逐步加

5. 常见排查:裸机 lwIP 收不到包、ping 不通的四种翻车现场

5.1 现象:PHY 自协商失败,网口灯不亮

现象很直接:板子上电后网口灯不亮,串口打印 PHY ID 是0xFFFF或0x0000,ethernetif_link_check一直读不到 LINK UP。原因通常不在 lwIP,而在硬件初始化:RMII 的 50MHz 参考时钟没起、PHY 复位引脚没释放、MDIO/MDC 引脚复用没配、PHY 地址和原理图不一致。还有一种情况是 PHY 的 strap 电阻把地址拨到了另一个值,代码里还按 0 地址读。

解决顺序是先用示波器看 REF_CLK,再看复位引脚电平,再用 MDIO 扫 0 到 31 地址读 PHY ID。扫到 ID 后把PHY_ADDR改对,重新初始化。如果 REF_CLK 没有波形,先查时钟源和引脚复用,不要继续调 lwIP 参数。

/* 用 MDIO 扫地址,先把 PHY 找出来 */ for (uint8_t addr = 0; addr < 32; addr++) { uint16_t id = phy_read(addr, 0x02); /* PHY ID1 */ if (id != 0xFFFF && id != 0x0000) { printf("phy addr=%u id1=0x%04X\r\n", addr, id); } }

这段扫描代码只读 PHY ID1 寄存器,地址从 0 到 31。读到的值既不是0xFFFF也不是0x0000,说明这个地址上有 PHY 响应。phy_read内部用 MDIO 时序,读之前要保证 MAC 的 MDIO 时钟分频已经配置好。扫不到任何地址时,问题在 MDIO 引脚或 PHY 供电,不在协议栈。

5.2 现象:能发不能收,描述符 OWN 位卡死

现象是 PC 端能看到板子发出的 ARP 请求,但板子收不到 ARP 应答,ping 不通。用调试器看接收描述符,发现 OWN 位一直是 1,DMA 没有把缓冲区交还给 CPU。原因常见有三种:接收描述符初始化时 OWN 位没置给 DMA、接收缓冲区地址没按 4 字节对齐、主循环里ethernetif_input调用太慢导致描述符环用完。

解决时先看接收描述符的 OWN 位和 buffer1 地址,确认地址对齐;再在主循环里提高ethernetif_input的调用频率;最后检查接收中断是否被关掉。如果用了中断收包,中断里只置标志,真正的 pbuf 分配和netif->input放到主循环。

/* 接收路径的关键检查:OWN 清零表示 CPU 可以取数据 */ if ((rx_desc[rx_idx].status & 0x80000000u) == 0) { uint16_t len = rx_desc[rx_idx].controlsize & 0x1FFF; struct pbuf *p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL); if (p != NULL) { pbuf_take(p, (void *)rx_desc[rx_idx].buffer1, len); if (gnetif.input(p, &gnetif) != ERR_OK) { pbuf_free(p); } } rx_desc[rx_idx].status |= 0x80000000u; /* 归还给 DMA */ rx_idx = (rx_idx + 1) % RX_DESC_NUM; }

这段代码里0x80000000u是接收描述符的 OWN 位,controlsize & 0x1FFF取帧长度,具体掩码按芯片头文件核对。pbuf_alloc失败时不能继续读下一包,否则 pbuf 池会被打空。gnetif.input返回ERR_OK后 pbuf 由协议栈接管,返回其他值才由这里释放。最后把 OWN 位重新置给 DMA,顺序不能反。

5.3 现象:pbuf 踩内存,跑一会儿就死机

现象是 ping 前几包正常,跑几十秒后死机或串口打印乱码。用调试器看 pbuf 池,发现 pbuf 的next指针指向了非法地址。原因通常是pbuf_alloc失败后没有判空就继续pbuf_take,或者pbuf_free之后还在用同一个 pbuf,或者接收长度超过了PBUF_POOL_BUFSIZE导致越界写。

解决时给每个pbuf_alloc加判空,接收长度做上限检查,开启 lwIP 的MEM_DEBUG或PBUF_DEBUG辅助定位。发送路径不要直接引用应用层栈上的缓冲区,必须拷贝到 pbuf 或发送缓冲区。ETH_PAD_SIZE改了以后,接收拷贝偏移也要同步改,否则会写错位置。

/* pbuf 分配失败必须退回去,不能继续写 */ struct pbuf *p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL); if (p == NULL) { rx_desc[rx_idx].status |= 0x80000000u; /* 归还描述符 */ return; /* 丢掉这一包,等下一包 */ } if (pbuf_take(p, src, len) != ERR_OK) { pbuf_free(p); return; }

这段代码演示的是失败分支:pbuf_alloc返回空时先把接收描述符归还,再直接返回,不能继续往空指针里拷。pbuf_take失败时释放 pbuf,避免泄漏。len超过PBUF_POOL_BUFSIZE时要在调用前就截断或丢弃,不能指望pbuf_take帮你兜底。

5.4 现象:DHCP 一直拿不到地址

现象是静态 IP 能 ping 通,切到 DHCP 后串口一直打印dhcp state: SELECTING,PC 端看不到 DHCP Discover。原因可能是netif_set_link_up没调用、sys_check_timeouts()没在主循环里跑、DHCP 服务器不在同一个二层网络,或者发送路径的广播包没发出去。裸机下 DHCP 的定时器靠sys_check_timeouts推进,主循环里漏了它,DHCP 就一直停在初始状态。

解决时先确认链路 up,再在 PC 端抓包看有没有 DHCP Discover。没有 Discover 就查发送路径和广播地址;有 Discover 没 Offer 就查 DHCP 服务器和网段。调试阶段先用静态 IP 验证 ARP 和 ICMP,再切 DHCP,能少走很多弯路。

/* 主循环里这三件事缺一不可 */ while (1) { ethernetif_input(&gnetif); /* 收包 */ sys_check_timeouts(); /* DHCP/ARP/TCP 定时 */ ethernetif_link_check(&gnetif); /* 链路状态 */ }

这段循环里ethernetif_input负责把 DMA 收到的包交给 lwIP,sys_check_timeouts负责推进 DHCP 和 TCP 定时器,ethernetif_link_check负责链路状态。三者的调用频率不需要完全一致,但都不能长时间不跑。DHCP 调试时可以在dhcp_start后打印gnetif.dhcp->state,看状态机卡在哪一步。

6. 装完怎么验证:从 ping 通到 TCP 回环的验收清单

6.1 分层验收:链路、ARP、ICMP、TCP

验证不要一上来就 ping,按链路、ARP、ICMP、TCP 四层走。链路层看 PHY 灯和netif_is_link_up;ARP 层在 PC 上arp -a看板子的 MAC 有没有出现;ICMP 层用 ping 小包再 ping 大包;TCP 层再跑一个回环。每层过了再进下一层,出问题时范围小。

# PC 端验收 <p> <a href="https://download.csdn.net/download/qq_36911234/88536865" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询