ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

GD32F407裸机lwIP移植:RMII、DMA描述符与sys_now

GD32F407裸机lwIP移植:RMII、DMA描述符与sys_now

简介:面向嵌入式网络开发者的GD32F407裸机移植LWIP工程包,基于Cortex-M4内核与内置以太网MAC,解决无操作系统环境下TCP/IP协议栈落地问题,适合具备一定MCU基础、希望理解协议栈移植流程的初中级工程师。包内共641个文件,以216个h头文件、165个c源文件为核心,辅以Keil工程文件、编译中间文件、少量说明文档与网页文件,压缩包约12.15MB,覆盖协议栈源码、网卡驱动、定时器、套接字接口及内存管理模块,目录按驱动、协议栈与应用层组织。已有1317人学习下载,说明其在裸机网络开发场景中具有较好的参考价值。读者可从中获得LWIP配置裁剪、静态内存池与pbuf管理、MAC中断收发、裸机任务轮询、应用层API对接以及调试优化等排错思路,便于对照搭建自己的网络通信实验环境,并理解资源受限MCU上的稳定联网设计与性能监控思路。

1. 在 GD32F407 上跑裸机 lwIP:先把“能 ping 通”拆成可验证的三件事

我见过太多 GD32F407 无操作系统移植 LWIP 的工程,第一版就卡在“PHY 链路灯亮、串口打印网卡 up,但 PC ping 不通”。这类问题往往不是 lwIP 协议栈本身,而是裸机移植里三个接缝没对齐:RMII 的 50MHz 参考时钟、ENET DMA 描述符所在的内存区域,以及sys_now()的毫秒时基。先把这三件事做成可观测的检查点,再谈 TCP 回环和吞吐。这份资源适合已经能把 GD32F4 点灯、串口跑通,准备把裸机网络做进采集板、网关板或调试接口的工程师;也适合从带系统方案迁回裸机、想砍掉线程和信号量开销的人。下面按工程骨架、驱动接缝、netif 注册、排错、压测的顺序拆。

2. 工程骨架与 lwIP 目录裁剪:NO_SYS=1 时到底保留哪些文件

2.1 源码树怎么摆:core、netif、arch 三层

裸机移植最容易犯的错,是把带系统的 lwIP 工程整个拖进来,结果api/下的 socket、netconn、tcpip 线程全在编译,sys_arch.c里还要补一堆信号量和邮箱。NO_SYS=1的路线不需要这些,保留core/、core/ipv4/、netif/ethernet.c、include/,再单独放一个arch/目录即可。网卡驱动ethernetif.c建议放在工程自己的bsp或applications下,不要和 lwIP 源码混在一起,后面换 PHY 或换引脚时好定位。

目录可以按下面这种方式摆,编译器的 include 路径只加src/include和arch两个入口:

Middlewares/lwip/ ├── src │ ├── core │ │ ├── init.c │ │ ├── mem.c │ │ ├── memp.c │ │ ├── pbuf.c │ │ ├── netif.c │ │ ├── tcp.c │ │ ├── udp.c │ │ ├── ipv4 │ │ └── ... │ ├── netif │ │ ├── ethernet.c │ │ └── ethernetif.c │ └── include └── arch ├── cc.h └── sys_arch.c

逻辑说明:src/core是协议栈主体,src/netif/ethernet.c负责以太网帧头处理,arch/cc.h负责平台类型和字节序,arch/sys_arch.c只保留裸机需要的时基和临界区。参数说明:cc.h里至少定义LWIP_PLATFORM_DIAG、LWIP_PLATFORM_ASSERT、sys_prot_t,并把BYTE_ORDER设为小端;sys_arch.c不要放sys_sem_new、sys_mbox_new这类带系统的接口,NO_SYS=1下用不到,放了反而容易把编译链引乱。

提示:如果工程里同时存在带系统和不带系统的 lwIP 配置,先确认当前编译的是哪一份lwipopts.h,不要在 IDE 的多个 include 路径里“撞配置”。

2.2 lwipopts.h 的关键开关:内存池、TCP 窗口和校验和

lwipopts.h是裸机 lwIP 的黑匣子,参数改错时现象往往不是编译报错,而是 ping 通、TCP 一连就断。第一版建议先关 DHCP、关 socket、关 netconn,只用 raw API 和静态 IP 跑通;校验和先全部走软件,等链路和 TCP 回环都正常,再考虑开 ENET 硬件校验和。这样排错时少一个变量。

下面是一份可以起步的配置骨架,数值按 GD32F407 的 SRAM 和实际业务调,不要直接照搬到大缓冲业务里:

#define NO_SYS 1 #define SYS_LIGHTWEIGHT_PROT 1 #define LWIP_SOCKET 0 #define LWIP_NETCONN 0 #define LWIP_RAW 0 #define LWIP_ARP 1 #define LWIP_ETHERNET 1 #define LWIP_ICMP 1 #define LWIP_UDP 1 #define LWIP_TCP 1 #define LWIP_DHCP 0 #define LWIP_DNS 0 #define MEM_ALIGNMENT 4 #define MEM_SIZE (16 * 1024) #define MEMP_NUM_PBUF 16 #define PBUF_POOL_SIZE 16 #define PBUF_POOL_BUFSIZE 1520 #define TCP_MSS 1460 #define TCP_SND_BUF (2 * TCP_MSS) #define TCP_WND (2 * TCP_MSS) #define MEMP_NUM_TCP_PCB 4 #define MEMP_NUM_TCP_SEG 16 #define CHECKSUM_GEN_IP 1 #define CHECKSUM_GEN_UDP 1 #define CHECKSUM_GEN_TCP 1 #define CHECKSUM_CHECK_IP 1 #define CHECKSUM_CHECK_UDP 1 #define CHECKSUM_CHECK_TCP 1 #define LWIP_STATS 1 #define MEMP_STATS 1 #define MEM_STATS 1 #define LWIP_DEBUG 0

逻辑说明:NO_SYS=1决定协议栈不创建线程,SYS_LIGHTWEIGHT_PROT=1打开内存池临界区保护,LWIP_SOCKET和LWIP_NETCONN关掉后不会把 socket 层编进来。PBUF_POOL_SIZE决定同时能挂多少个收包缓冲,TCP_SND_BUF和TCP_WND决定单连接能压多少未确认数据。参数说明:MEM_SIZE是 lwIP 自己的堆,PBUF_POOL_BUFSIZE要大于最大以太网帧(1500 加帧头余量),TCP_MSS在以太网里常用 1460;如果业务一次发 4KB 以上,先加TCP_SND_BUF,再看MEMP_NUM_TCP_SEG是否够用。

参数建议初值作用调整信号
MEM_SIZE16KB协议栈堆pbuf_alloc返回 NULL
PBUF_POOL_SIZE16收包 pbuf 池连续收包丢包
TCP_SND_BUF2×MSS发送缓冲tcp_write返回ERR_MEM
TCP_WND2×MSS接收窗口大包吞吐上不去
MEMP_NUM_TCP_SEG16TCP 分段队列发送队列满

注意:第一版不要把硬件校验和和软件校验和混开。ENET 描述符里没配校验和插入偏移时,CHECKSUM_GEN_*关掉会出现“ARP 能通、TCP 校验和错”的怪现象。

2.3 sys_arch 只补三件事:sys_now、临界区和超时轮询

裸机下sys_arch.c不需要邮箱和信号量,核心是给 lwIP 一个毫秒时基,再给内存池一把“关中断”的锁。sys_now()被 TCP 重传、ARP 老化、DHCP 超时共用,返回值必须是毫秒并且单调递增;sys_arch_protect()和sys_arch_unprotect()用来保护memp和pbuf的链表操作。sys_check_timeouts()放在主循环里轮询,不要指望中断里自动跑定时器。

volatile uint32_t g_lwip_tick_ms; uint32_t sys_now(void) { return g_lwip_tick_ms; } sys_prot_t sys_arch_protect(void) { uint32_t primask = __get_PRIMASK(); __disable_irq(); return primask; } void sys_arch_unprotect(sys_prot_t p) { __set_PRIMASK(p); } void SysTick_Handler(void) { g_lwip_tick_ms++; }

逻辑说明:SysTick_Handler每 1ms 自增一次,sys_now()直接返回这个计数;sys_arch_protect()保存当前 PRIMASK 再关中断,sys_arch_unprotect()按保存值恢复,避免嵌套调用时把中断状态改错。参数说明:sys_prot_t在cc.h里定义为uint32_t;SysTick 周期按系统时钟配到 1ms,不要用delay_ms()这种阻塞函数替代;主循环里sys_check_timeouts()的调用间隔建议不超过 10ms,否则 TCP 重传和 ARP 表项老化会偏慢。

3. GD32F407 以太网外设与 PHY 的裸机驱动:RMII 引脚、DMA 描述符和收发环

3.1 RMII 接口与 PHY 地址:先把链路层打通再谈 lwIP

GD32F4 的 ENET 外设支持 RMII 和 MII,板上一般用 RMII 省引脚。RMII 需要 50MHz 参考时钟,常见做法是 PHY 输出 50MHz 给 MCU 的REF_CLK,也有板子由 MCU 输出时钟给 PHY;这两种情况在原理图上必须确认,时钟没起振时 MAC 寄存器可能读得到,但收发包全错。MDIO/MDC 用来读 PHY 寄存器,PHY 地址由硬件上下拉决定,地址不对时phy_read()全返回0xFFFF。

static void enet_gpio_config(void) { /* 打开 GPIO 和 ENET 时钟,函数名按手里固件库替换 */ rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_GPIOC); rcu_periph_clock_enable(RCU_GPIOB); rcu_periph_clock_enable(RCU_ENET); /* RMII 典型引脚:REF_CLK、MDIO、CRS_DV、RXD0、RXD1、TX_EN、TXD0、TXD1 */ 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); /* 其余引脚按原理图补齐:PC1/PC4/PC5、PB11/PB12/PB13 等 */ }

逻辑说明:这段只做两件事,把引脚切到 ENET 复用功能,打开 ENET 时钟;真正的 MAC 初始化、DMA 初始化和 PHY 复位放在后面。参数说明:GPIO_AF_11是 GD32F4 上 ENET 的常见复用编号,但不同封装和引脚可能不同,必须对照具体器件手册的 AF 表;GPIO_PUPD_NONE对 RMII 的 TX/RX 线一般够用,MDIO 线上如果外部没有上拉,可以改内部上拉再试。

PHY 初始化的顺序建议固定成:复位 PHY、延时、读PHY_ID、软复位、等自协商完成、读链路状态。不要一上来就开 MAC 收包,链路没起来时收到的都是垃圾帧。PHY 地址可以写死,也可以从0到31扫一遍,读到的PHY_ID不是0xFFFF且不是0x0000就认为命中。这个扫描逻辑在调试阶段很省时间。

3.2 DMA 描述符与缓冲区对齐:收包为什么总在第二个包崩

ENET 的收发靠 DMA 描述符环,描述符里放状态、控制、缓冲地址和下一描述符指针。GD32F407 有 CCM 区域,CPU 能访问但 DMA 不一定能访问;如果链接脚本把.bss或自定义段放进了 CCM,描述符和缓冲区地址对 DMA 就是无效的,现象是第一个包偶尔能收,第二个包直接死机或OWN位不翻转。血泪经验是:先看 map 文件,确认g_rx_desc、g_tx_desc、g_rx_buf、g_tx_buf的地址不在 DMA 不可达的区域。

#define ENET_RX_DESC_NUM 4 #define ENET_TX_DESC_NUM 4 #define ENET_BUF_SIZE 1536 typedef struct { volatile uint32_t status; uint32_t ctrl; uint32_t addr; uint32_t next; } enet_dma_desc_t; 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_BUF_SIZE] __attribute__((section(".enet_dma"), aligned(4))); static uint8_t g_tx_buf[ENET_TX_DESC_NUM][ENET_BUF_SIZE] __attribute__((section(".enet_dma"), aligned(4)));

逻辑说明:描述符和缓冲区都放进自定义.enet_dma段,链接脚本把这个段定位到普通 SRAM,避开 CCM;aligned(4)满足 DMA 对地址对齐的要求。参数说明:描述符数量 4 对 4 是调试期够用的最小值,包量大时可以加到 8 对 8;ENET_BUF_SIZE取 1536,比最大以太网帧多留一点余量;如果链接脚本没有定义.enet_dma,编译会报段找不到,需要在.ld文件里加一条定位规则。

提示:调试 DMA 时先只开接收,不开发送,用 PC 端发 ARP 请求,观察接收描述符的OWN位和长度字段是否变化。收包环正常后再开发送,排错范围会小很多。

3.3 low_level_init/low_level_output:netif 与 ENET 的接缝

lwIP 的ethernetif.c通过low_level_init()初始化硬件,通过low_level_output()把 pbuf 发出去。low_level_init()里填netif->hwaddr、mtu、flags,再调用 MAC/DMA 初始化;low_level_output()里把 pbuf 链拉平成连续缓冲,启动 DMA 发送。发送函数不要死等 DMA 完成,等不到就返回ERR_MEM或ERR_IF,让上层决定重试还是丢弃。

err_t low_level_init(struct netif *netif) { netif->hwaddr_len = ETH_HWADDR_LEN; netif->hwaddr[0] = 0x02; netif->hwaddr[1] = 0x00; netif->hwaddr[2] = 0x00; netif->hwaddr[3] = 0x00; netif->hwaddr[4] = <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>
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进