
1. 这套组合到底解决什么问题值得花几天时间啃下来STM32F407、FreeRTOS、LwIP 这三个词放一起基本就是带网口的工控/物联网设备的标准技术底座。我做过的几个数据采集网关、远程 IO 模块、串口服务器底层全是这个组合F407 负责外设收数FreeRTOS 负责把采集、协议解析、上报拆成互不阻塞的任务LwIP 负责把数据打包成 TCP/UDP 送出去。裸机能跑通的功能一旦加上网络并发代码结构会迅速烂掉——这是逼着上 RTOS 的直接原因。先把结论摆出来这套移植真正难的不是抄一份 port.c 进去编译过而是三个容易被忽视的点。第一是 F407 的 FPU 与 FreeRTOS 的上下文切换配合配错会表现为浮点运算结果莫名其妙跳变这类 bug 极难查。第二是 LwIP 的 sys_arch 移植层它是协议栈和多任务内核之间的桥桥没搭好ping 能通但 TCP 一压就崩。第三是中断优先级分组FreeRTOS 的临界区和 ST 库的中断优先级必须统一到一套规则里否则会出现跑十分钟偶尔死一次的经典随机故障。这篇文章写给谁看如果你已经能在 F407 上点灯、跑通串口会用 Keil 或 IAR 建工程但一提到移植就头大那这篇就是给你准备的。我会把每一步为什么这么做讲清楚参数怎么算、坑在哪里、怎么验证全部给到可以直接抄的程度。整套流程我实测过三块板子正点原子探索者、自制的 407 核心板、以及一块带 DP83848 的工控板下面的参数配置都是从那几轮调试里留下来的。2. 移植前的地基时钟、FPU、内存一个都不能含糊很多人移植失败根子不在 FreeRTOS 也不在 LwIP而在工程基座没打牢。F407 是 Cortex-M4F带硬件浮点单元主频能到 168MHz但它默认的启动配置和 RTOS 的假设之间有几处不一致必须先抹平。2.1 系统时钟和 FPU 的开启直接影响 RTOS 能否正常切换先说时钟。F407 的标准配置是外部 8MHz 晶振HSE经 PLL 倍频到 168MHz。PLL 参数是 PLL_M8、PLL_N336、PLL_P2、PLL_Q7算一下8MHz / 8 1MHz 作为 PLL 输入1MHz × 336 336MHz 作为 VCO 输出再除以 P 分频 2得到 168MHz 的 SYSCLK。PLL_Q7 是给 USB/SDIO 用的 48MHz 时钟这次用不到可以先不管但配置里留着没坏处。APB1 分频 4 得到 42MHz不能超过 42MHzAPB2 分频 2 得到 84MHz。这一堆数字为什么要较真因为 FreeRTOS 的 tick 中断来自 SysTickSysTick 挂在 HCLK 上。如果你时钟配错了比如实际只有 84MHz而代码里按 168MHz 算 tick那你的任务延时、超时时间会整体差一倍网络协议栈的重传计时也会全乱。SysTick 的重装载值计算公式是SystemCoreClock / configTICK_RATE_HZ - 1configTICK_RATE_HZ 取 1000 时168MHz 下重装载值就是 167999。我见过有人把 configTICK_RATE_HZ 设成 1000 但没改 SystemCoreClock结果 tick 频率实际是 500Hz表现为所有延时都变长了。FPU 这块是 F407 移植 FreeRTOS 最关键的一个开关。Cortex-M4F 的浮点寄存器 s0-s31 属于懒保存lazy stacking机制异常发生时硬件默认不立刻保存浮点上下文靠 FPCCR 寄存器的 ASPEN/LSPEN 位来控制。FreeRTOS 的 ARM_CM4F 移植层专门处理了这个逻辑在 PendSV 里判断 EXC_RETURN 的 bit4 决定要不要压栈浮点寄存器。但你得保证两件事一是编译器宏__FPU_PRESENT和__FPU_USED都是 1Keil 里在 Options → C/C → Define 中加上__FPU_PRESENT1,__FPU_USED1或者在 stm32f4xx.h 里确认已定义二是 FreeRTOSConfig.h 里要定义__FPU_PRESENT 1让 port.c 知道目标有 FPU。注意如果你用的是 ARM_CM3 的移植文件而不是 ARM_CM4F即使芯片有 FPUFreeRTOS 也不会保存浮点上下文任务里用 float 就会出现数据被其他任务踩坏的现象。选 port 目录时一定认准ARM_CM4F。2.2 FreeRTOS 堆方案怎么选内存不够是第一大拦路虎FreeRTOS 有五种堆管理方案从 heap_1 到 heap_5。对于 F407 这种带 192KB SRAM112KB16KB64KB CCM的芯片我一般用 heap_4原因是它支持内存释放且带碎片合并LwIP 的动态内存、任务创建、队列创建都会用到。heap_1 只能分配不能释放做静态系统还行一上网络协议栈就捉襟见肘。heap_5 支持多个不连续内存区如果你的工程要把堆放到 CCM RAM 或者外部 SRAM 上才需要它。configTOTAL_HEAP_SIZE给多少合适我实测下来纯 FreeRTOS 几个普通任务10KB 够用加上 LwIP 并跑 TCP 服务建议 30KB 到 40KB。别一次给满留点给裸内存申请。有个细节heap_4 的 ucHeap 数组是静态定义的它会被算进 .bss 段。如果给 40KB编译出来 RAM 占用会明显上涨用 Keil 的 map 文件确认一下别把 128KB 主 SRAM 挤爆。我见过有人给了 80KB 堆链接直接通过不了报 region RAM overflow。关于 CCM RAM那 64KB 的 Core Coupled Memory它只能被 CPU 内核访问DMA 不能碰。所以千万别把 LwIP 的 pbuf 池或者以太网 DMA 描述符放到 CCM 里去否则网络收发会静默失败——数据看着像是发出去了但实际 DMA 搬不动。我一般把任务栈、纯计算的变量放 CCM把网络缓冲区留在主 SRAM。2.3 工程目录怎么组织后期维护省一半力气移植这事目录结构一开始就要理清否则加到后面自己都找不到文件。我的习惯是三层CMSIS/和STM32F4xx_StdPeriph_Driver/或 HAL 库芯片原厂的东西原样放。FreeRTOS/里面再分Source/内核源码、Source/portable/RVDS/ARM_CM4F/Keil 用 RVDS 目录IAR 用 IAR 目录、Source/portable/MemMang/heap_4.c。LwIP/分src/协议栈核心、src/arch/你自己的 sys_arch.c、ethernetif.c、port/lwipopts.h 和 include 头。Keil 工程里要把这几条 include 路径都加进去漏一条就是几百个 undefined symbol。IAR 的话portable 目录换成 IAR 版本其他一致。这一步没什么技术含量但它决定了你后面加文件、切版本时的痛苦程度。3. FreeRTOS 在 F407 上落地逐个参数讲透地基打完进入正题。FreeRTOS 的移植实质是填三个东西把 portable 层的两个文件放对位置、写好 FreeRTOSConfig.h、实现几个钩子函数。听起来简单但配置文件的每个宏都值得说清楚。3.1 源文件从哪儿来哪些必须、哪些可以砍FreeRTOS 内核源码从官方仓库拿一份版本建议 10.4.x 或 202112.00 这两个 LTS 版本和 LwIP 2.1.2 配合稳核心就这几个 .ctasks.c、list.c、queue.c、timers.c、event_groups.c、stream_buffer.c。croutine.c是协程早就不推荐用了直接不加。portable 层必须的是port.c和portmacro.h内存管理挑一个 heap_x.c。在 Keil 里选中port.c打开它的编译选项——这里有个大坑port.c 里包含了__forceinline之类内联汇编的宏Keil AC5 和 AC6 对它的处理不同。AC6armclang编译 FreeRTOS 10.4 以前的版本时会报__get_MSP之类的内联汇编错误。解决办法要么用 AC5 编译器要么升级 FreeRTOS 到 202112.00 之后并改用对应的 GCC 移植层。我踩过这个坑当时排查了一下午才意识到是编译器版本问题。3.2 FreeRTOSConfig.h 逐项拆解这些值背后都有讲究下面是我那份能跑图的配置挑关键的讲#define configUSE_PREEMPTION 1 // 抢占式调度网络任务需要它 #define configUSE_TIME_SLICING 1 // 同优先级轮转 #define configUSE_PORT_OPTIMISED_TASK_SELECTION 1 #define configMAX_PRIORITIES (7) #define configTICK_RATE_HZ ((TickType_t)1000) #define configMINIMAL_STACK_SIZE ((unsigned short)128) #define configTOTAL_HEAP_SIZE ((size_t)(40 * 1024)) #define configMAX_TASK_NAME_LEN (16) #define configUSE_16_BIT_TICKS 0 #define configIDLE_SHOULD_YIELD 1 #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 #define configKERNEL_INTERRUPT_PRIORITY (configLIBRARY_LOWEST_INTERRUPT_PRIORITY) #define configUSE_MUTEXES 1 #define configUSE_COUNTING_SEMAPHORES 1 #define configUSE_RECURSIVE_MUTEXES 1 #define configUSE_TASK_NOTIFICATIONS 1 #define configQUEUE_REGISTRY_SIZE 10 #define configCHECK_FOR_STACK_OVERFLOW 2 #define configUSE_MALLOC_FAILED_HOOK 1 #define configUSE_IDLE_HOOK 1 #define configUSE_TICK_HOOK 0挑几个重点说。configTICK_RATE_HZ取 1000也就是 tick 周期 1ms。为什么不用 100LwIP 内部的定时器线程sys_timeouts依赖 tick 精度1ms 能让 TCP 重传和超时更准。代价是 tick 中断更频繁但对 168MHz 的 M4 来说完全无压力ISR 开销不到 1%。configMAX_SYSCALL_INTERRUPT_PRIORITY是最容易错的一项。它决定了哪些优先级的中断可以调用FromISR结尾的 API。Cortex-M 的中断优先级是数值越小优先级越高这个宏填 5 意味着优先级数值 5 到 15 的中断里才能调 FreeRTOS 的 ISR API数值 0-4 的中断被内核临界区屏蔽不到绝对不能在里面调 API。配合它的是configLIBRARY_LOWEST_INTERRUPT_PRIORITY一般填 15即最低优先级。这两个值协同工作等于把系统中断分成两档高优先级0-4不受 RTOS 管用不了它的 API5 及以上归 RTOS 管。对应的你必须在main里设置 NVIC 优先级分组为第 4 组NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);第 4 组意味着 4 位全用来表示抢占优先级没有子优先级。为什么是这组因为 ST 库默认用的是组 22 位抢占、2 位子优先级而 FreeRTOS 的临界区靠 BASEPRI 屏蔽它只认抢占优先级。如果不改成组 4中断优先级的实际数值和你配置的对不上就会导致我明明设了优先级 5结果还是能打断临界区积累成偶发死机。configCHECK_FOR_STACK_OVERFLOW填 2是最严格的栈检查模式会在任务切换时把栈的末尾几个字节和预设的填充值比对。配套要自己实现这个钩子void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { (void)xTask; printf(STACK OVERFLOW: %s\r\n, pcTaskName); taskDISABLE_INTERRUPTS(); for(;;); }别小看这个钩子。我第一次移植时没开栈溢出检测一个 LwIP 收发任务的栈给少了 128 字跑网络压力测试大概二十分钟后随机重启查了整整两天开了这个钩子之后一进压力测试就立刻定位到任务名。3.3 移植层文件的实际改动MHz 与时钟频率的关联port.c本身一般不用改但有一处要留意configCPU_CLOCK_HZ这个宏它用来算 SysTick 重装载值。我习惯把它定义为SystemCoreClock但如果你的工程里 SystemCoreClock 变量在 RTOS 启动前还没更新比如时钟初始化在很后面就要显式写死成 168000000。我就遇到过、SystemCoreClock 还停留在默认的 16MHz内部 HSI值导致 tick 慢了近 10 倍所有延时都变成龟速。vPortSVCHandler、xPortPendSVHandler、xPortSysTickHandler这三个函数名要么在stm32f4xx_it.c里把SVC_Handler、PendSV_Handler、SysTick_Handler注释掉、改用 FreeRTOS 里带 xPort 前缀的宏定义#define vPortSVCHandler SVC_Handler这种方式也行要么在 FreeRTOSConfig.h 里把它们映射到标准名字#define vPortSVCHandler SVC_Handler #define xPortPendSVHandler PendSV_Handler #define xPortSysTickHandler SysTick_Handler两种方式选一种就行别同时做否则会重复定义。这个报错很典型Symbol SVC_Handler multiply defined。3.4 任务调度验证先点亮三个不同频率的灯移植完成后别急着接网络先写一个能证明调度正常的最小验证void vTaskLed1(void *pvParameters) { while(1) { GPIO_ToggleBits(GPIOF, GPIO_Pin_9); vTaskDelay(pdMS_TO_TICKS(500)); } } void vTaskLed2(void *pvParameters) { while(1) { GPIO_ToggleBits(GPIOF, GPIO_Pin_10); vTaskDelay(pdMS_TO_TICKS(200)); } } int main(void) { SystemInit(); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); Led_Init(); uart_init(115200); xTaskCreate(vTaskLed1, LED1, 128, NULL, 2, NULL); xTaskCreate(vTaskLed2, LED2, 128, NULL, 2, NULL); vTaskStartScheduler(); while(1); }两个灯分别以 500ms 和 200ms 翻转如果它们互不干扰、节奏稳定说明 tick、调度器、上下文切换全部正常。这一步省不了网络那一堆问题排查起来你至少得先确定内核是好的。4. LwIP 接上 FreeRTOS真正的移植硬骨头在这儿FreeRTOS 跑通只是热身LwIP 才是重头戏。LwIP 的设计假设是有一个操作系统提供信号量、互斥锁、消息邮箱和线程所以它把和 OS 交互的部分抽象成sys_arch.c。你移植的本质就是实现这个文件再把网卡驱动的收发和 LwIP 的 pbuf 对接起来。4.1 版本选择和目录裁剪2.1.2 是当前最稳的平衡点我用的是 LwIP 2.1.2它的 API 是sys_相对于老版本的sys_arch_配合 FreeRTOS 10.4 以上很顺。目录组织上src/core/下是tcp.c、udp.c、ip.c、pbuf.c等核心协议src/api/是 socket 接口sockets.csrc/netif/是网卡抽象层ethernetif.c的模板就在这里src/apps/放 httpd、mqtt 这类应用。Keil 里我一般加这几个目录src/api、src/core、src/core/ipv4、src/netif、src/system以及自己的arch目录。裁剪方面ipv6相关的整个目录不用加ppp、pppos、snmp这些不用也砍掉能省不少 flash。sys_arch.c和sys_arch.h要自己写放在 arch 目录里。4.2 lwipopts.h 关键配置内存参数决定 TCP 吞吐上限这份配置直接决定你的网络性能上限几个值最要紧#define NO_SYS 0 // 使用操作系统 #define LWIP_SOCKET 1 #define LWIP_NETCONN 1 // netconn APIsocket 基于它 #define LWIP_TIMERS 1 #define MEM_ALIGNMENT 4 // Cortex-M 按 4 字节对齐 #define MEM_SIZE (16 * 1024) #define MEMP_NUM_PBUF 16 #define MEMP_NUM_TCP_PCB 8 #define MEMP_NUM_TCP_PCB_LISTEN 4 #define PBUF_POOL_SIZE 16 #define PBUF_POOL_BUFSIZE 1536 #define IP_REASSEMBLY 0 #define TCP_MSS 1460 #define TCP_SND_BUF (4 * TCP_MSS) #define TCP_WND (4 * TCP_MSS) #define LWIP_NETIF_HOSTNAME 1 #define TCPIP_THREAD_STACKSIZE 1024 #define TCPIP_THREAD_PRIO 3 #define DEFAULT_THREAD_STACKSIZE 512 #define DEFAULT_THREAD_PRIO 2 #define LWIP_DHCP 1 #define LWIP_NETIF_STATUS_CALLBACK 1PBUF_POOL_BUFSIZE给的 1536 是有讲究的——以太网帧最大 1514 字节加上协议头、对齐1536 是常见的整块大小保证一个 pbuf 能装下一整帧避免数据被拆成多个链式 pbuf减少处理开销。TCP_SND_BUF给 4 个 MSS 是经验值太小限制发送窗口大文件传输上不去太大占内存且对只有几百 KB 内存的 F407 不划算。TCP_MSS取 1460 是标准以太网的经典值考虑了 PPPoE 场景也可以设 1400但纯以太网内网 1460 最优。MEM_SIZE是 LwIP 的堆16KB 够跑几个并发 TCP 连接。如果你要跑 HTTP 服务器同时开 MQTT建议加到 24KB 甚至 32KB但别忘了这也要和 FreeRTOS 的 heap 抢 SRAM总量控制一下。4.3 sys_arch.c 怎么写邮箱和信号量是核心这是移植的重头戏。LwIP 要求你实现这几个东西信号量、互斥锁、邮箱message box、线程创建以及一个系统超时机制。我的实现思路是——FreeRTOS 的 Queue 天生适合做邮箱Semaphore 直接做信号量和互斥锁Task 做线程。先看邮箱的实现LwIP 用它传递 pbuf 指针err_t sys_mbox_new(sys_mbox_t *mbox, int size) { *mbox xQueueCreate(size, sizeof(void *)); if (*mbox NULL) { return ERR_MEM; } return ERR_OK; } void sys_mbox_post(sys_mbox_t *mbox, void *msg) { while (xQueueSendToBack(*mbox, msg, portMAX_DELAY) ! pdTRUE); } err_t sys_mbox_trypost(sys_mbox_t *mbox, void *msg) { if (xQueueSend(*mbox, msg, 0) pdTRUE) return ERR_OK; return ERR_MEM; } u32_t sys_arch_mbox_fetch(sys_mbox_t *mbox, void **msg, u32_t timeout) { void *dummyptr; TickType_t start, end; if (msg NULL) msg dummyptr; if (timeout ! 0) { start xTaskGetTickCount(); if (xQueueReceive(*mbox, msg, pdMS_TO_TICKS(timeout)) pdTRUE) { end xTaskGetTickCount(); return (u32_t)((end - start) * portTICK_PERIOD_MS); } return SYS_ARCH_TIMEOUT; } else { xQueueReceive(*mbox, msg, portMAX_DELAY); return 0; } }这里有个细节sys_arch_mbox_fetch的返回值是阻塞了多久毫秒LwIP 靠它判断超时。如果你直接返回 0 或者搞错了单位TCP 的超时重传就会失效表现为连接卡死但不断开。我第一次写的时候用了 tick 单位没乘portTICK_PERIOD_MS结果所有网络超时都缩短了表现为极高频的重传日志刷爆串口。信号量和互斥锁更直接直接把 FreeRTOS 的句柄当 LwIP 的句柄用err_t sys_sem_new(sys_sem_t *sem, u8_t count) { vSemaphoreCreateBinary(*sem); if (*sem NULL) return ERR_MEM; if (count 0) xSemaphoreTake(*sem, 0); return ERR_OK; } void sys_sem_signal(sys_sem_t *sem) { xSemaphoreGive(*sem); } u32_t sys_arch_sem_wait(sys_sem_t *sem, u32_t timeout) { TickType_t start, end; start xTaskGetTickCount(); if (timeout ! 0) { if (xSemaphoreTake(*sem, pdMS_TO_TICKS(timeout)) pdTRUE) { end xTaskGetTickCount(); return (u32_t)((end - start) * portTICK_PERIOD_MS); } return SYS_ARCH_TIMEOUT; } xSemaphoreTake(*sem, portMAX_DELAY); return 0; }互斥锁的实现要注意一件事LwIP 里互斥锁sys_mutex和信号量分开了FreeRTOS 直接用递归互斥量的创建 API。这里有个陷阱如果在内核还没启动时就调用 sys_mutex_new会失败——因为互斥量创建需要内核对象而内核调度器还没跑。LwIP 初始化一般是在主任务里做的所以问题不大但纯中断或 main 早期就调 LwIP 初始化的写法就会踩这个坑。线程创建最简单sys_thread_t sys_thread_new(const char *name, lwip_thread_fn thread, void *arg, int stacksize, int prio) { TaskHandle_t h; xTaskCreate(thread, name, stacksize, arg, prio, h); return (sys_thread_t)h; }4.4 网卡驱动对接DP83848 PHY 和 ETH MAC 的握手F407 自带 MAC外面配一个 PHY 芯片常见是 LAN8720 或 DP83848。我用的是 DP83848走 RMII 接口PHY 地址通常是 0x01。这里的关键是ethernetif.c里那个low_level_init函数——它负责初始化 MAC、配置 DMA 描述符、打开中断。static void low_level_init(struct netif *netif) { OS_Mem_Init(); ETH_InitTypeDef ETH_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_ETH_MAC | RCC_AHB1Periph_ETH_MAC_Tx | RCC_AHB1Periph_ETH_MAC_Rx, ENABLE); /* PB5 50MHz 时钟输出给 PHYRMII 需要 */ RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); GPIO_PinAFConfig(GPIOB, GPIO_PinSource5, GPIO_AF_ETH); GPIO_InitStructure.GPIO_Pin GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed GPIO_Speed_100MHz; GPIO_Init(GPIOB, GPIO_InitStructure); ETH_InitStructure.ETH_AutoNegotiation ETH_AutoNegotiation_Enable; ETH_InitStructure.ETH_Speed ETH_Speed_100M; ETH_InitStructure.ETH_Mode ETH_Mode_FullDuplex; ETH_InitStructure.ETH_LoopbackMode ETH_LoopbackMode_Disable; ETH_InitStructure.ETH_RetryTransmission ETH_RetryTransmission_Disable; ETH_InitStructure.ETH_AutomaticPadCRCStrip ETH_AutomaticPadCRCStrip_Disable; ETH_InitStructure.ETH_ReceiveAll ETH_ReceiveAll_Disable; ETH_InitStructure.ETH_BroadcastFramesReception ETH_BroadcastFramesReception_Enable; ETH_InitStructure.ETH_PromiscuousMode ETH_PromiscuousMode_Disable; ETH_InitStructure.ETH_MulticastFramesFilter ETH_MulticastFramesFilter_Perfect; ETH_InitStructure.ETH_UnicastFramesFilter ETH_UnicastFramesFilter_Perfect; ETH_InitStructure.ETH_ChecksumOffload ETH_ChecksumOffload_Enable; ETH_Init(ETH_InitStructure, DP83848_PHY_ADDRESS); ETH_DMAITConfig(ETH_DMA_IT_NIS | ETH_DMA_IT_R, ENABLE); ETH_Start(); }几个配置点背后都有原因。ETH_AutoNegotiation开自动协商让 PHY 和交换机协商速率和双工模式。有人图省事写死 100M 全双工结果对端是半双工出现大量冲突和丢包。ETH_ChecksumOffload打开校验和卸载MAC 硬件算 TCP/IP 校验和能明显减少 CPU 占用——但在混杂情况比如某些调试工具发出的畸形包下可能误判如果抓包工具报校验和错误可以关掉试试。RCC_AHB1Periph_ETH_MAC_Tx | Rx这几个时钟必须开漏一个就是网口完全无反应。PHY 的地址确认方法有的板子 DP83848 的 PHYAD0 引脚通过上下拉决定地址常见 0x01 或 0x00。如果初始化返回失败串口打印 ETH 寄存器看看或者用示波器量 PHY 的 MDC/MDIO 是否有波形。我那块自制板就是地址搞错改一行宏就通了但当时不知道白白浪费了半天。4.5 接收中断到 LwIP 线程pbuf 的流转路径网络数据到达的路径是这样的PHY 收到帧 → MAC DMA 写入接收描述符缓冲区 → 触发 ETH_IRQHandler → ISR 里检查描述符并给出信号量 →ethernetif_input线程被唤醒 → 把数据封装成 pbuf → 交给netif-input也就是ethernet_input。ISR 要控制在极短只做一个信号量 givevoid ETH_IRQHandler(void) { portBASE_TYPE xHigherPriorityTaskWoken pdFALSE; if (ETH_GetDMAFlagStatus(ETH_DMA_FLAG_R) SET) { ETH_DMAClearITPendingBit(ETH_DMA_IT_R); xSemaphoreGiveFromISR(s_xSemaphore, xHigherPriorityTaskWoken); } ETH_DMAClearITPendingBit(ETH_DMA_IT_NIS); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }关键提醒ETH_IRQHandler 的中断优先级必须配置成数值 ≥ 5也就是逻辑优先级不高于 configMAX_SYSCALL_INTERRUPT_PRIORITY否则在里面调xSemaphoreGiveFromISR会触发 FreeRTOS 的断言如果开了 configASSERT。这是无数人踩过的坑。ETH 中断我一般给优先级 5。接收线程的循环大概长这样static void ethernetif_input(void *p_arg) { struct ethernetif *ethernetif (struct ethernetif *)p_arg; struct pbuf *p; while (1) { if (xSemaphoreTake(ethernetif-sem, portMAX_DELAY) pdTRUE) { do { p low_level_input(ethernetif-netif); } while (p NULL); if (ethernetif-netif-input(p, ethernetif-netif) ! ERR_OK) { pbuf_free(p); } } } }这里有个性能细节如果每来一帧就唤醒一次线程高流量下任务切换开销很大。LwIP 的实践是在 ISR 里判断已经收了多少帧超过阈值再唤醒。我在千兆环境下把接收缓冲加到 4 个描述符让线程一次处理多帧吞吐明显提升。5. 联调踩坑实录和问题速查代码写完了不等于能跑通。网络联调是个必须静下心来的活我按着从物理层往上查的顺序把常见问题整理成速查表。5.1 从物理灯到 ping 通分级验证不能跳我的验证流程固定如下板子上电看网口指示灯Link 灯和 Activity 灯。Link 灯不亮说明物理连接或 PHY 初始化有问题别急着查协议栈。串口打印 PHY 的 BSR 寄存器确认 Link 和速率状态。初始化 netif 后调用netif_set_up和netif_set_link_up打印分配的 IP。从电脑 ping 板子。ping 不通时先看板子是不是收到了 ARP 请求在ethernetif_input加打印。ping 通后用板子主动发 UDP 包到 PC验证发送路径。最后测 TCP 服务端跑一个 echo server用 PC 的调试工具连。这套分级流程的好处是出问题时能立刻定位在哪一层。我见过直接上来就测 TCP、报个连接失败就抓瞎的人其实他 Link 都没协商上。5.2 常见问题速查表现象可能原因排查方向Link 灯不亮PHY 时钟没输出、PHY 地址错量 PB5 是否有 50MHz改 PHY 地址宏能 Link 上但 ping 不通中断优先级低于 RTOS 要求、MAC 没开检查 ETH 中断优先级 ≥ 5MAC 时钟是否全部使能ping 通但 TCP 卡死TCP_SND_BUF 太小、超时返回值单位错加大 SND_BUF核对 sys_arch_mbox_fetch 返回值压力测试后随机死机栈溢出、互斥量在中断里用、内存越界开栈溢出钩子检查临界区保护大数据量丢包pbuf 池用完、内存不够增大 PBUF_POOL_SIZE 和 MEM_SIZE编译报 __get_MSP 未定义Keil AC6 编译旧版 FreeRTOS换 AC5 或升级 FreeRTOS 到 202112 以后浮点结果乱跳用了 ARM_CM3 移植层、FPU 宏未开换 ARM_CM4F 目录加 __FPU_PRESENT1任务延时不准SystemCoreClock 未更新、tick 配置错确认时钟 168MHztick 10005.3 性能与稳定性调优的几个心得调优这事先量再调。我一般的观测手段是在ethernetif_input里统计每秒处理帧数用空闲任务钩子统计 CPU 占用率在 vApplicationIdleHook 里累加计数器1 秒清零一次除以 tick 数就是空闲比例。实际调优上几个有效的动作。一是把 TCP 任务和网络接收任务的优先级拉开。TCP/IP 线程tcpip_thread优先级设 3接收线程设 4比它高应用任务设 2。这样收包不受应用计算阻塞。二是启用LWIP_TCPIP_CORE_LOCKING。LwIP 2.x 支持内核锁定模式它把 TCP/IP 的处理从专门线程搬到调用者上下文去省掉一层消息传递吞吐能提升约 20%代价是你要确保不在核心区外调用需要锁的 API。这个模式对应用写法有影响初学者先用默认的线程模式。三是中断里别做重活。我见过有人在 ETH 中断里直接解析协议结果丢包严重。正确做法就是给信号量剩下的交给线程。6. 那几个真正花了我时间的坑讲完流程聊几个让我印象最深的坑这些在文档里基本找不到。第一个是内存对齐导致的硬 fault。我把 LwIP 的接收缓冲区定义成了一个结构体数组结果编译器没按 4 字节边界对齐DMA 往奇数地址写直接进 HardFault_Handler而且 fault 是间歇性的加打印就没、去掉就复现。解决办法是用__attribute__((aligned(4)))或者__align(4)强制对齐并且确认缓冲区不放在 CCM。这类问题的典型特征就是单步调试正常、全速运行就崩本质是时序和地址对齐问题。第二个是sys_jiffies和sys_now的实现。LwIP 需要用它们拿系统时间毫秒。标准实现就是return xTaskGetTickCount() * portTICK_PERIOD_MS;。但如果你改了 tick 频率不更新或者用了 16 位 tickconfigUSE_16_BIT_TICKS 1这里会溢出。有一次我的 tick 是 1000Hz16 位 tick 只能存 65 秒就回绕导致所有基于 sys_now 的超时在运行一分钟后乱套表现为连接稳定一段时间后突然全部掉线。改成 32 位 tick 就好了。第三个是串口打印的坑。很多人移植喜欢到处加printf调试但printf在 Keil 下不一定线程安全多个任务同时打印会互相打断输出乱码甚至卡死。更麻烦的是 HAL 的 UART 发送如果用了阻塞式一个任务在打印时其他网络任务全停性能骤降。我的做法是做一个串口任务所有打印走队列或者至少在调试阶段用简单的putchar直接写寄存器避开 semihosting。最后再分享一个实用的小配置如果你发现网络吞吐上不去先看TCP_WND和TCP_SND_BUF是不是对称地小。接收窗口太小对端发一波就得等 ACK白白浪费带宽。我做过一个文件传输测试把这两个从 2 MSS 提到 4 MSS同样的板子吞吐从 3MB/s 涨到了 6MB/s 出头。当然再往上加就得看你内存扛不扛得住了。