
简介面向使用GD32F4系列进行裸机网络开发的嵌入式工程师与学生这份工程包围绕GD32F407平台完成LWIP协议栈的无操作系统移植重点解决以太网MAC驱动适配、内存池与pbuf管理、接收中断处理以及周期性协议定时任务等关键问题可作为从零搭建TCP/IP通信能力的参考工程。压缩包共641个文件、约12.15MB以216个h头文件、165个c源文件为主并包含Keil工程配置、编译中间文件、说明文档及少量网页与脚本素材便于直接打开工程梳理源码组织与构建流程。已有1317人学习下载。读者可从中获得较完整的裸机LWIP移植框架、驱动与协议栈衔接思路、静态内存配置样例以及调试排错线索适合具备Cortex-M4与基础网络概念、希望深入理解GD32F407以太网通信实现的中高级开发者对照研读。1. 一块没跑 RTOS 的板子把 lwIP 塞了进去手上一块 GD32F407 的板子PHY 焊好、MDIO 能读到 ID真正卡住的是软件要求在 GD32F407 上做无操作系统的 lwIP 移植。板子只负责一路 TCP 上报加一路 UDP 心跳跑 RTOS 反而给内存和调试都添负担。这类需求在工控小板、采集节点上很常见我前后调过几版最后落成一套裸机主循环加接收中断的结构没有任务调度、没有信号量lwIP 的定时器靠主循环里的sys_check_timeouts推着走。这套东西适合手上已有 GD32F4 硬件、想在裸机上直接跑 socket 风格接口的人也适合拿它当理解 lwIP 内部结构的底子——RTOS 那层壳剥掉之后协议栈到底怎么收发反而看得更清楚。2. 技术选型为什么这块 GD32F407 上不装 RTOS2.1 裸机跑 lwIP 的边界NO_SYS1 放弃了什么lwIP 有两种运行模式分水岭就是一个宏NO_SYS。设成 1 之后协议栈不再依赖任何操作系统抽象层sys_arch.c里那套信号量、邮箱、线程创建全部不需要实现代价是netconn和socket两套 API 直接作废——它们内部依赖信号量做阻塞裸机下没人给你调度回来。所以配置里必须成组地关掉#define NO_SYS 1 #define SYS_LIGHTWEIGHT_PROT 0 #define LWIP_NETCONN 0 /* 依赖 sys_sem必须关 */ #define LWIP_SOCKET 0 /* 同上 */ #define LWIP_TIMERS 1 /* 保留TCP 重传/ARP 老化都要它 */剩下的接口只有 raw/callback API也就是tcp_new()之后注册tcp_recv、tcp_sent这些回调函数所有逻辑在回调里执行回调里不能死等。这一点是移植的分界线能接受回调式写法裸机方案就成立非要写while(recv(...) 0)那种顺序代码就得老实上 RTOS。什么样的项目适合这么干我给个自己的判断标准场景特征裸机 lwIP建议上 RTOS并发连接数14 路固定连接动态增减超过 8 路通信形态周期上报、短连接查询长连接 多业务并行阻塞需求无全部状态机需要阻塞 accept / DNS 解析RAM 预算主 SRAM 128KB 以内富余可开到 256KB 以上调试成本单线程断点随便打需处理栈溢出、优先级反转表里最后一行是我最看重的。裸机调试最大的好处是没有看不见的调度出问题时寄存器现场干净不像多任务那样一堆栈回溯。代价是应用层必须自己写成非阻塞状态机这个后面第 6 章会给具体写法。2.2 硬件与工程前提PHY、时钟、内存分区GD32F407 片内集成了以太网 MAC外接 PHY 才能出网口。常见搭配是 DP83848支持 MII 和 RMII和 LAN8720只支持 RMII。选 RMII 能省 8 根线但要求一路 50MHz 参考时钟要么外部有源晶振直接喂给 PHY 的 X1要么用 MCU 的 MCO 引脚PA8输出 50MHz 给 PHY。我一般优先选外部晶振MCO 输出受主频配置影响主频一改时钟就飘这种玄学问题排查起来很折磨。确认 PHY 通不通第一步不是看灯是读寄存器 ID/* enet_system_setup() 之后GPIO 和 MDIO 时钟已就绪 */ uint16_t id1 enet_phy_read(ENET_PHY_ADDRESS, PHY_REG_ID1); uint16_t id2 enet_phy_read(ENET_PHY_ADDRESS, PHY_REG_ID2); if ((id1 0xFFFF) || (id1 0x0000)) { /* MDIO 没通先查 PHY 地址、再查 MDC 分频、最后查上电时序 */ while (1); }ENET_PHY_ADDRESS是 PHY 的 MDIO 地址由 PHY 芯片上几个 strap 引脚在上电时决定DP83848 常见是 0x01LAN8720 常见是 0x00。读出来全 F 说明 MDIO 线上没人应答全 0 多半是时序太快或者 PHY 没复位完成。这一步跑不通后面所有 lwIP 的事都是白搭。内存分区是第二个必须提前想清楚的事。GD32F407 的主 SRAM 是 128KB 连续区SRAM0 SRAM1另有一块 64KB 的 CCM 数据 RAM 与主区不连续。CCM 速度快、只能给 CPU 访问以太网 DMA 描述符和收发缓冲区绝对不能放进去否则 DMA 读到的永远是 0现象是网口灯正常、一包收不到。我一般在链接脚本或启动文件里把描述符段显式钉在主 SRAM 的前 32KB同时保证 4 字节对齐。2.3 源码裁剪与工程搭建把 lwIP 剪到只剩要用的部分完整 lwIP 源码树不小裸机场景能砍掉一大半。我的裁剪原则是只留 core、ipv4、netif 三块api 目录整个不要ipv6、ppp、snmp 全部排除在编译之外。具体清单如下路径内容处理src/core/init、mem、memp、pbuf、tcp、udp、timeouts全留src/core/ipv4/ip4、icmp、arp、dhcp全留src/core/ipv6/IPv6 协议族不加入编译src/api/netconn、socket、api_msg不加入编译src/netif/ethernet.c以太网帧封装/解封装保留src/netif/ppp/PPP 拨号不加入编译contrib/ports/各平台的移植模板只参考不直接用工程搭建按这个顺序走避免一上来满屏报错不知道从哪下手新建 Keil/GCC 工程先把 GD32F4xx 标准外设库的 ENET 例程跑通确认能发出 ARP 或者能收到广播帧——这一步用抓包工具看比看代码直观。建ThirdParty/lwip目录把裁剪后的源码拷进去把src/include和src/include/ipv4加进头文件搜索路径。从模板拷一份lwipopts.h到User/目录放在头文件路径最前面确保它优先被找到。lwIP 源码里自带的lwipopts.h是空壳不覆盖就会用一堆默认值后面排查参数会怀疑人生。自己写ethernetif.c实现ethernetif_init、low_level_init、low_level_output、low_level_input四个函数这是 mcu 与协议栈之间唯一的接缝。写main.c系统时钟 → SysTick → ENET 引脚与 DMA →lwip_init()→netif_add()→ 主循环。第 1 步单独拎出来强调是因为我见过太多次直接上 lwIP 然后调不通的情况最后发现底层 MAC 根本没发出去包协议栈背了黑锅。先用裸 ENET 例程确认硬件链路等于给自己留了后悔药。3. 调用链与配置一次 ping 从 PHY 到 netif 的完整路径3.1 目录结构哪些是自己写的哪些是搬来的工程落地后目录大概是这样把自己写的和第三方分开放后面换 lwIP 版本不会互相污染GD32F407_LWIP_NOOS/ ├── CMSIS/ # 内核头文件与启动文件 ├── Firmware/ │ └── GD32F4xx_standard_peripheral/ │ ├── inc/ # enet.h、gpio.h、rcu.h 等 │ └── src/ # enet.c、gpio.c、rcu.c 等 ├── ThirdParty/ │ └── lwip/ │ ├── src/ │ │ ├── core/ # 协议栈核心 │ │ ├── core/ipv4/ # ARP、ICMP、DHCP │ │ ├── netif/ # ethernet.c │ │ └── include/ │ └── COPYING ├── User/ │ ├── main.c # 初始化 主循环 │ ├── lwipopts.h # 全部可调参数集中在这 │ ├── ethernetif.c # 网卡适配层核心工作量 │ ├── ethernetif.h │ ├── sys_now.c # 毫秒时基 │ └── app_tcp.c # 应用层状态机 └── Project/ # 工程文件、链接脚本真正需要动脑的只有ethernetif.c和app_tcp.c两个文件其余全是搬运。这也解释了为什么这类项目上手快难度集中在两三百行适配代码里而不是整个协议栈。3.2 一次 ping 的完整调用链把这条件链路记住90% 的问题都能定位到具体一层。收到一个 ICMP 请求包时顺序是这样的接收方向PHY 收到差分信号 → MAC 校验 FCS → DMA 把帧写进接收描述符指向的缓冲区 → 置位接收中断 → 中断服务函数只置一个rx_flag不做任何协议处理→ 主循环发现标志 → 调用ethernetif_input()→low_level_input()把描述符里的数据搬成pbuf→ 归还描述符给 DMA →netif-input()即ethernet_input()按 ethertype 分流 →ip4_input()→icmp_input()。发送方向icmp_input()构造回包 →ip4_output_if()填 IP 头 →etharp_output()查 ARP 表拿目的 MAC →ethernet_output()填以太网头 →netif-linkoutput()即low_level_output()→ 拷进发送描述符缓冲区 → 置 DAV 位交给 DMA → MAC 自动发送。这条链上有三个我自己踩过的关键点第一中断里千万别直接调ethernetif_input()lwIP 不是可重入的中断里跑协议栈会随机崩第二etharp_output第一次会因为 ARP 表为空先把包丢掉这是正常的别以为丢包第三发送描述符被 DMA 占用时要返回ERR_MEM而不是死等上层会自动重试。3.3 lwipopts.h几个不配就一定出事的项这份配置是整个移植里最像调参的部分写错一个现象可能完全不相干。下面是我在这块板子上验证过的一组值/* ---------- 内存池 ---------- */ #define MEM_ALIGNMENT 4 #define MEM_SIZE (16 * 1024) /* 堆ARP 表、TCP 控制块 */ #define MEMP_NUM_PBUF 16 /* 应用层申请的 pbuf 数 */ #define PBUF_POOL_SIZE 16 /* 从网卡收上来的 pbuf 数 */ #define PBUF_POOL_BUFSIZE 1524 /* 单帧最大长度 余量 */ /* ---------- 以太网对齐 ---------- */ #define ETH_PAD_SIZE 2 /* 关键让 IP 头 4 字节对齐 */ /* ---------- TCP ---------- */ #define TCP_MSS 1460 #define TCP_SND_BUF (4 * TCP_MSS) #define TCP_WND (4 * TCP_MSS) /* ---------- 功能开关 ---------- */ #define LWIP_DHCP 1 #define LWIP_ICMP 1 #define LWIP_UDP 1 #define LWIP_TCP 1 #define LWIP_NETIF_LINK_CALLBACK 1MEM_SIZE是协议栈自己的堆ARP 表项、TCP 控制块、pbuf结构体都从这里出。16KB 对 23 路 TCP 够用接 8 路以上建议直接翻倍到 32KB否则现象是连接建立到一半返回ERR_MEM看起来像对端拒绝。PBUF_POOL_SIZE决定同时能缓存多少帧。它和PBUF_POOL_BUFSIZE相乘就是接收侧固定占用的内存16×1524 大约 24KB。包突发时会耗尽耗尽后中断里拿不到 pbuf 只能丢帧表现是 ping 大包丢、小包正常。ETH_PAD_SIZE只有 2但它是 Cortex-M4 上最容易翻车的一个宏。以太网头是 14 字节如果 pbuf 从 4 字节边界开始IP 头就落在偏移 14 的位置编译器优化出的LDRD双字加载会因为非对齐直接进 HardFault。加 2 字节填充后 IP 头落在偏移 16问题消失。这条是血泪经验当年查了两天以为是 DMA 的问题。3.4 sys_now 与超时基准SysTick 的正确接法lwIP 内部所有定时——TCP 重传、ARP 表项老化、DHCP 重试——都依赖一个毫秒时间戳函数sys_now()。裸机下没人提供得自己写而且必须单调递增、不能回绕出错static volatile u32_t s_ms_tick 0; /* SysTick 配成 1ms 中断一次 */ void SysTick_Handler(void) { s_ms_tick; } u32_t sys_now(void) { return s_ms_tick; }u32_t是 32 位无符号按毫秒累加大约 49.7 天回绕一次。lwIP 内部用的是差值比较(u32_t)(now - start)回绕本身不会出错所以放心用。真正会出事的是两个地方一是 SysTick 中断优先级被设得比 ENET 中断低很多接收中断长时间占用 CPU 时 tick 丢步DHCP 就会一直重试失败二是有人图省事把s_ms_tick在主循环里自增主循环一旦被长任务阻塞时间就停了TCP 重传立刻乱套。配套地主循环里必须定期调sys_check_timeouts()while (1) { ethernetif_input(gnetif); /* 收包并喂给协议栈 */ sys_check_timeouts(); /* 驱动所有超时定时器 */ app_poll(); /* 应用层非阻塞状态机 */ }三个函数调用顺序不重要但sys_check_timeouts()的调用频率有讲究TCP 重传定时器精度取决于它主循环转一圈的周期最好控制在 1ms 以内实在做不到就 5ms别拖到几十毫秒。4. 核心模块拆解ethernetif 与主循环轮询4.1 DMA 描述符与缓冲区内存布局先定对ENET 收发靠描述符环每个描述符指向一块缓冲区。数量不用多4 发 4 收足够跑满百兆多了浪费 RAM#define ENET_RXBUF_NUM 4 #define ENET_TXBUF_NUM 4 #define ENET_BUF_SIZE 1524 /* 描述符必须 4 字节对齐GCC 用 __attribute__Keil 用 __align(4) */ static enet_descriptor_struct enet_tx_desc[ENET_TXBUF_NUM] __attribute__((aligned(4))); static enet_descriptor_struct enet_rx_desc[ENET_RXBUF_NUM] __attribute__((aligned(4))); static uint8_t enet_tx_buf[ENET_TXBUF_NUM][ENET_BUF_SIZE] __attribute__((aligned(4))); static uint8_t enet_rx_buf[ENET_RXBUF_NUM][ENET_BUF_SIZE] __attribute__((aligned(4)));ENET_BUF_SIZE取 1524 是 1518 最大帧加 6 字节余量的常见值留点空间可以避开描述符跨界。定义成static是为了让链接器把它们放进.bss默认就在主 SRAM 里如果工程里用了分散加载文件把某些段划到 CCM务必确认这四个数组没被划过去。初始化时把描述符和缓冲区绑起来再交给 MACstatic void descriptors_bind(void) { for (uint32_t i 0; i ENET_TXBUF_NUM; i) { enet_tx_desc[i].status ENET_TDES0_DAV; /* 初始标记为空闲 */ enet_tx_desc[i].control_buffer_size 0; enet_tx_desc[i].buffer1_addr (uint32_t)enet_tx_buf[i][0]; enet_tx_desc[i].buffer2_next_desc_addr (uint32_t)enet_tx_desc[(i 1) % ENET_TXBUF_NUM]; } for (uint32_t i 0; i ENET_RXBUF_NUM; i) { enet_rx_desc[i].status ENET_RDES0_DAV; /* 初始标记为 DMA 可用 */ enet_rx_desc[i].control_buffer_size ENET_BUF_SIZE; enet_rx_desc[i].buffer1_addr (uint32_t)enet_rx_buf[i][0]; enet_rx_desc[i].buffer2_next_desc_addr (uint32_t)enet_rx_desc[(i 1) % ENET_RXBUF_NUM]; } }status这一位在不同文档里叫法不一样GD32 固件库里是ENET_TDES0_DAVDescriptor Available本质是这个描述符归 DMA 所有。发送时置 1 表示交给硬件发硬件发完清 0接收时相反硬件收到包清 0软件取完数据重新置 1。搞反了就是永远收发不了这一点和 STM32 的 OWN 位语义一致。4.2 low_level_initPHY 上电到自协商完成low_level_init是 lwIP 回调进网卡驱动的入口职责是把 MAC、DMA、PHY 全部拉起来并填好netif的硬件信息static err_t low_level_init(struct netif *netif) { netif-hwaddr_len ETHARP_HWADDR_LEN; netif-mtu 1500; netif-flags NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP; /* MAC 地址实际产品从 Flash 读调试阶段先写死 */ netif-hwaddr[0] 0x02; netif-hwaddr[1] 0x00; netif-hwaddr[2] 0x00; netif-hwaddr[3] 0x00; netif-hwaddr[4] 0x00; netif-hwaddr[5] 0x01; enet_gpio_config(); /* RMII 引脚 MDIO/MDC */ enet_system_setup(); /* MAC 时钟、DMA 软复位 */ descriptors_bind(); enet_initpara.auto_nego ENET_AUTO_NEGOTIATION; enet_initpara.phy_address ENET_PHY_ADDRESS; enet_init(enet_initpara); enet_interrupt_enable(ENET_INT_NORMAL); /* 只开正常接收中断 */ enet_descriptors_init(enet_tx_desc, ENET_TXBUF_NUM, enet_rx_desc, ENET_RXBUF_NUM); enet_start(); /* 等自协商完成超时就当链路异常处理 */ uint32_t guard 0; while (enet_phy_link_status() 0) { if (guard 20000) { return ERR_IF; /* 让 netif_add 返回失败别硬撑 */ } delay_ms(1); } return ERR_OK; }几个参数值得单说。auto_nego打开后 MAC 会等 PHY 自协商出速率和双工百兆全双工是目标如果对端是强制百兆的旧交换机协商可能失败这时改enet_initpara.speed和duplex强制配置更稳。phy_address必须和实际 strap 一致前面读 ID 那步就是为了确认它。自协商等待那段加了超时保护是我改掉的原始写法。原始模板是死循环while(link 0);PHY 没插网线时程序直接卡死在初始化里看门狗都不一定救得回来。改成有上限的等待返回ERR_IFnetif_add会返回 NULL上层能明确知道网卡没起来而不是整机假死。4.3 low_level_output 与 low_level_input收发两个方向发送侧把 pbuf 链摊平到描述符缓冲区static err_t low_level_output(struct netif *netif, struct pbuf *p) { enet_descriptor_struct *desc enet_tx_desc[s_tx_idx]; uint8_t *buf; uint32_t len 0; struct pbuf *q; if (desc-status ENET_TDES0_DAV) { return ERR_MEM; /* DMA 还没发完让协议栈稍后重试 */ } buf (uint8_t *)desc-buffer1_addr; for (q p; q ! NULL; q q-next) { memcpy(buf len, q-payload, q-len); len q-len; } if (len 60) { /* 以太网最小帧 60 字节不含 FCS */ memset(buf len, 0, 60 - len); len 60; } desc-control_buffer_size len; desc-status ENET_TDES0_LS | ENET_TDES0_FS | ENET_TDES0_DAV; s_tx_idx (s_tx_idx 1) % ENET_TXBUF_NUM; return ERR_OK; }control_buffer_size填实际帧长MAC 自动补 FCS。LSLast Segment和FSFirst Segment同时置位表示这一帧独占一个描述符不跨段。返回ERR_MEM的路径必须有TCP 层收到这个返回值会保留数据到下次tcp_sent回调再发等于天然的背压机制。接收侧反过来static struct pbuf *low_level_input(struct netif *netif) { enet_descriptor_struct *desc enet_rx_desc[s_rx_idx]; struct pbuf *p NULL; uint32_t len; if (desc-status ENET_RDES0_DAV) { return NULL; /* DMA 还没写完 */ } if (desc-status ENET_RDES0_ES) { goto release; /* 帧有错丢弃但必须归还描述符 */ } len (desc-status ENET_RDES0_FRML_MASK) 16; p pbuf_alloc(PBUF_RAW, len ETH_PAD_SIZE, PBUF_POOL); if (p NULL) { goto release; /* 内存池耗尽丢帧 */ } pbuf_take_at(p, (uint8_t *)desc-buffer1_addr, len, ETH_PAD_SIZE); release: desc-status ENET_RDES0_DAV; /* 无论成败都要还给 DMA */ s_rx_idx (s_rx_idx 1) % ENET_RXBUF_NUM; return p; }pbuf_take_at的第三个参数是偏移把数据往后挪ETH_PAD_SIZE字节配合前面的对齐配置。最关键的是release标签任何一条退出路径都必须把描述符 Status 重新置 DAV。我见过一次丢描述符导致跑几小时后彻底收不到包就是错误分支提前 return 忘了归还。4.4 主循环与中断的协作谁来喂 lwIP中断服务函数只做一件事——标记有包到达然后清中断标志void ENET_IRQHandler(void) { if (enet_interrupt_flag_get(ENET_INT_NORMAL) ! RESET) { s_rx_pending 1; /* 只置标志不碰协议栈 */ enet_interrupt_flag_clear(ENET_INT_NORMAL); } }主循环负责真正的处理void ethernetif_input(struct netif *netif) { struct pbuf *p; if (s_rx_pending 0) { return; } s_rx_pending 0; do { p low_level_input(netif); if (p ! NULL) { if (netif-input(p, netif) ! ERR_OK) { pbuf_free(p); /* 上层处理失败必须自己释放 */ } } } while (p ! NULL); }netif-input在以太网接口下就是ethernet_input处理成功返回ERR_OK且由协议栈接管 pbuf 所有权返回非 OK 时所有权还在调用方必须pbuf_free。这条规则是内存泄漏的高发区后面避坑那章会具体讲。do-while 循环的意义在于一次中断可能对应多帧到达4 个描述符都满了循环取空为止。如果主循环周期较长建议把s_rx_pending换成计数器而不是布尔标志避免两帧之间标志被覆盖只处理一帧。5. 避坑与排查裸机 lwIP 最容易翻车的五处5.1 小包能 ping 通1400 字节大包一 ping 就丢现象ping 192.168.1.10 -l 32正常-l 1400全部超时抓包能看到请求但看不到回应。原因接收 pbuf 池或协议栈堆不够。大包会走分片或者占用多个PBUF_POOL条目PBUF_POOL_SIZE只有 4 的时候必然失败另一种可能是MEM_SIZE太小回包时pbuf_alloc返回 NULL。解决先把PBUF_POOL_SIZE提到 16、PBUF_POOL_BUFSIZE提到 1524再把MEM_SIZE提到 16KB 以上。改完记得重新编译整个协议栈只改头文件不重新编译 部分目标是无效的表现是改了没反应。5.2 运行随机进 HardFault断点位置每次都不一样现象能跑几秒到几分钟然后 HardFault调用栈指向ip4_input或ethernet_input内部。原因Cortex-M4 的非对齐访问。以太网头 14 字节若 pbuf 起点在 4 字节边界IP 头的地址就是x14编译器对 32 位字段做LDR时触发 UsageFault。这个问题和优化等级强相关-O0时可能看不出来-O2一开必炸。解决ETH_PAD_SIZE设成 2让 IP 头对齐到 16 偏移。另外检查所有从pbuf-payload强转成结构体指针的地方能改成memcpy就改别赌对齐。5.3 网口灯正常闪但一包都收不到现象PHY 链路已 up指示灯有闪烁netif显示 link up但ethernetif_input里永远拿不到 pbuf。原因描述符或缓冲区被链接到了 CCM 区。CCM 只有 CPU 能访问DMA 读到的全是 0描述符 Status 永远不会变成已接收。解决查.map文件确认enet_rx_desc、enet_tx_desc、两个 buffer 数组的地址落在主 SRAM 范围通常是 0x20000000 起始的 128KB 内不在 0x10000000 段。有分散加载文件的话显式给这些变量指定 section。5.4 DHCP 反复重试拿不到地址手动配 IP 就正常现象日志里 DHCP DISCOVER 一直发收不到 OFFER或者收到后不 ACK。原因sys_now()没有正确实现或者 SysTick 中断没使能。DHCP 的租约和重试全靠这个时基返回常数 0 会让超时判断永远不成立。解决在sys_now()里打个断点看返回值是否随运行递增。另外确认SysTick_Handler的名字和启动文件里的向量表一致用 GCC 时函数名拼错不会报错只会在链接时被弱符号顶掉静默失效。5.5 跑几小时后死机或彻底失联现象前几小时一切正常之后再也收不到包复位后恢复。原因多数是 pbuf 泄漏。netif-input返回非ERR_OK时没有pbuf_free池子慢慢被吃光另一种是中断里直接跑协议栈导致内部状态被破坏还有一种是接收描述符在异常分支没归还。解决在pbuf_free和pbuf_alloc里加计数器周期性打印剩余 pbuf 数量正常应该稳定在某个区间。排查顺序是先看监控数字是否单调下降再看中断里有没有协议栈调用最后核对low_level_input的所有 return 路径。6. 二次开发换 PHY、加协议、调参数换 PHY 是这类项目最常见的改造需求比如原设计用 DP83848量产要换成 LAN8720 降成本。要动的地方其实只有三处一是ENET_PHY_ADDRESS改成新 PHY 的 strap 地址二是引脚配置从 MII 改成 RMII涉及的 GPIO 有十几根漏一根就是收不到包三是 RMII 的 50MHz 时钟源原来靠外部晶振的话要改成 MCO 输出或者反过来。改完先跑 2.2 节那段读 ID 的代码读不对就别往下查协议栈。应用层协议推荐用明确的非阻塞状态机别在回调里写业务逻辑。我一般这么组织typedef enum { APP_IDLE, APP_CONNECTING, APP_SEND, APP_WAIT_ACK } app_state_t; static app_state_t s_state APP_IDLE; static uint32_t s_tick 0; void app_poll(void) { switch (s_state) { case APP_IDLE: if (s_tcp_pcb NULL) { tcp_connect(s_tcp_pcb, s_server, 5000, on_connected); s_state APP_CONNECTING; } break; case APP_SEND: if (s_send_buf_len 0) { tcp_write(s_tcp_pcb, s_send_buf, s_send_buf_len, TCP_WRITE_FLAG_COPY); tcp_output(s_tcp_pcb); s_send_buf_len 0; } s_state APP_WAIT_ACK; break; case APP_WAIT_ACK: if (sys_now() - s_tick 3000) { /* 3 秒没确认就当断线 */ tcp_abort(s_tcp_pcb); s_state APP_IDLE; } break; default: break; } }状态机放在主循环里跑tcp_recv、tcp_sent这些回调只负责置标志和更新状态不写阻塞逻辑这样任何异常都能超时退出不会卡死整机。参数调优可以按这张表上手试参数初始值适用场景调整方向PBUF_POOL_SIZE16单连接周期上报并发多时提到 32MEM_SIZE16KB23 路 TCP8 路以上提到 32KBTCP_SND_BUF4×MSS小包高频大文件上传提到 8×MSSTCP_WND4×MSS局域网低延迟有丢包链路提到 8×MSS主循环周期1ms交互类应用纯上报可放宽到 5ms调TCP_SND_BUF时有个限制TCP_SND_BUF必须小于MEM_SIZE的一半否则memp分配失败连接能建立但发不出数据现象很迷惑。我最早做这套移植的时候调通了 ping 就以为大功告成结果现场设备跑了一晚上掉线回来查是一处pbuf没释放。从那以后我每次改完收发路径都在pbuf_alloc和pbuf_free两侧各加一个计数让主循环每隔几秒串口打印一次数字不回落就不算改完。这个习惯后来帮我省了不知道多少个通宵。希望帮到你。本文还有配套的精品资源点击获取