简介面向嵌入式开发者提供在NXP LPC1768ARM Cortex-M3平台上裸机移植FreeRTOS并集成LWIP协议栈的完整工程实例。资源包内含可直接参考的源代码与Keil工程配置覆盖系统时钟与中断控制器设置、任务创建及调度器启动、以太网控制器DM9161驱动、LWIP内存池分配与收发数据包处理等核心内容同时提供了从裸机环境到多任务实时系统切换的完整移植流程示例压缩包还附有编译产物便于对照运行效果快速上手。包共478个文件以C源文件和头文件为主另有链接脚本、启动汇编文件、工程配置文件、txt说明及readme文档整体仅1.59MB目录结构清晰便于按需检索、裁剪移植。已有615人学习使用适合具备一定单片机基础、希望为LPC1768快速引入实时操作系统和TCP/IP网络能力的开发者在物联网设备网络通信开发中可作参考。对于希望深入理解Cortex-M3下RTOS上下文切换和TCP/IP协议栈线程模型的读者这份实例也能提供直观参考。 接手 LPC1768 板子的第一周你大概率还在裸机点灯等接到设备要联网上报数据的需求时才会发现裸机主循环已经 hold 不住 TCP/IP 协议栈这种复杂组件。最省事、最稳的路线就是把 LPC1768 裸机工程先移植上 FreeRTOS 系统再在 FreeRTOS 上挂一个 LWIP实现完整的 TCP/IP 协议栈。这篇文章是我自己移植的完整记录从 FreeRTOS 内核到 LWIP 的 sys_arch 适配层、EMAC 网卡驱动再到实测验证每一步都讲清楚为什么要这么做以及文档里不会写的那些坑。1. 为什么偏偏是 LPC1768 FreeRTOS LWIP 这套组合先给结论这套组合不是最新的但绝对是最稳的。LPC1768 是 NXP 的 Cortex-M3主频 100MHz512KB Flash、64KB SRAM自带 10/100M 以太网 MACEMAC。自带 MAC 意味着不用外挂 SPI 网卡芯片只要配一颗 PHY 收发器就能从板子上拉出网线跑真正的 TCP/IP 协议栈。对比 W5500 这类硬件协议栈方案LPC1768 的方式吞吐量高一个量级CPU 占用也更低缺点是移植工作量全在自己这边。FreeRTOS 选它做 RTOS 的理由很实际源码全开放、社区量大、Cortex-M3 的移植包官方维护得很成熟。LWIP 则是嵌入式 TCP/IP 协议栈的事实标准内存占用可以压到十几 KB在 64KB SRAM 的 LPC1768 上完全跑得动。三者组合在一起典型的软件架构是这样App 任务netconn APITCP 回环、数据上报等 ↓ tcpip_threadLWIP 内核线程负责 TCP/UDP/ARP/ICMP 协议处理 ↑↓ EMAC 底层驱动low_level_input / low_level_output ↑↓ LPC1768 EMAC 外部 PHYMII/RMII 接口也就是说我们做的移植工作拆成两半先把 FreeRTOS 跑起来再用 FreeRTOS 的信号量、队列、线程接口把 LWIP 的操作系统抽象层填上最后把 LPC1768 的 EMAC 驱动挂到 LWIP 的 netif 接口上。这套东西做出来之后绝大多数物联网网关、工业数据采集、串口转以太网设备的底子都是这个骨架。这个方案适合谁来参考呢手里有 LPC1768 开发板、写过裸机驱动、想上 RTOS 和网络功能的人。如果你完全没碰过嵌入式建议先跑通裸机串口和 LED 再来看。2. 移植前先摸清 LPC1768 的家底时钟、内存和中断优先级很多人一上来就复制 FreeRTOS 源码进 Keil编译过了就以为成功了一半。实际上 LPC1768 有它自己的脾气三个硬件底细先搞清楚后面能省一整天的排错时间。2.1 64KB SRAM 是分三块的DMA 内存别乱放LPC1768 的 SRAM 不是一整块连续内存而是 32KB 16KB 16KB 三块地址分别在 0x10000000、0x2007C000、0x20080000。最关键的是EMAC 的 DMA 只能访问部分 SRAM 区域NXP 自带 LWIP 示例习惯把以太网描述符和收发 buffer 放在 0x2007C000 这一块。如果你把 LWIP 的 pbuf 内存池放到 EMAC 够不到的内存区就会出现DMA 一直收不到数据这种诡异问题。我的习惯是EMAC 的 RX/TX 描述符和 buffer 用分散加载文件或__attribute__((section()))固定到 0x2007C000 段FreeRTOS 的堆和任务栈放在其余区域。另外这也影响 FreeRTOS 堆选择。默认情况下大家都会用 heap_4因为它支持分配释放和碎片合并。但如果你发现任务一多内存不够用可以考虑换 heap_5。heap_5 允许通过vPortDefineHeapRegions()把多块不连续 SRAM 都登记进堆正好适配 LPC1768 这种三段内存。前提是别把那块 EMAC 专用的 DMA 内存也登记进去否则 pbuf 落在不可访问区域网络照样跑不起来。2.2 时钟配置和 NVIC 优先级位数FreeRTOS 的configCPU_CLOCK_HZ必须填 100000000也就是 LPC1768 系统时钟 100MHz。填错的话tick 周期和实际时间对不上后面sys_now()返回的毫秒数全是错的LWIP 的超时重传机制会乱套。还有个容易被忽略的细节LPC17xx 器件头文件里__NVIC_PRIO_BITS是 5也就是说 NVIC 中断优先级是 0~31不是 STM32 那种 4 位的 0~15。这直接关系到 FreeRTOS 的中断屏蔽宏。之前有人把 STM32 那套配置原封不动搬过来结果configMAX_SYSCALL_INTERRUPT_PRIORITY算出来的 BASEPRI 值不对ISR 里调用xQueueSendFromISR偶发卡死。寄存器层面的原因不细说你只需要记住LPC1768 的configMAX_SYSCALL_INTERRUPT_PRIORITY要写成(5 (8 - __NVIC_PRIO_BITS))configKERNEL_INTERRUPT_PRIORITY写成(31 (8 - __NVIC_PRIO_BITS))也就是把数值优先级移位到 BASEPRI 的高字节位置。2.3 编译器和源码包的选型我用的 Keil MDKFreeRTOS 直接从官方 GitHub 拉 kernel 源码版本是 V10.xLWIP 用的 2.1.3这个版本和 FreeRTOS 配合的资料最多。如果你用的是新版 MDK默认 ARM Compiler 6AC6LWIP 和 FreeRTOS 的旧代码偶尔会因为类型转换严格检查报错。我的建议是先切回 AC5 把整个流程跑通再考虑迁移 AC6。网上常见的 VSCode 里报#include freertos/freertos.h 检测到 #include 错误那是 IntelliSense 的 includePath 没配好不是源码坏了Keil 里对应就是 Options → C/C → Include Paths 里要加 FreeRTOS 的 include 目录、portable 目录和 LWIP 的 src/include 目录。3. FreeRTOS 移植的三个关键动作向量表、配置文件和优先级规则FreeRTOS 官方给 Cortex-M3 提供了完善的移植层不需要像老式 Linux 移植那样改汇编。真正要做的是三件事让内核的三个异常入口对上向量表、把 FreeRTOSConfig.h 配到匹配你的硬件、把外设中断优先级放进 FreeRTOS 允许调 API 的范围内。3.1 向量表映射的两种做法Cortex-M3 的上下文切换依赖 SVC、PendSV、SysTick 这三个异常。FreeRTOS 的移植代码里对应的入口名字是vPortSVCHandler、xPortPendSVHandler、xPortSysTickHandler但 Keil 的 startup_LPC17xx.s 向量表里写的是SVC_Handler、PendSV_Handler、SysTick_Handler。名字对不上链接器就会报 undefined symbol。我比较推荐在 FreeRTOSConfig.h 里加几行宏定义让内核源码里的入口名在预处理阶段变成 CMSIS 标准名字#define vPortSVCHandler SVC_Handler #define xPortPendSVHandler PendSV_Handler #define xPortSysTickHandler SysTick_Handler原理很简单FreeRTOS 内核源码里定义这些函数时宏会让符号名被替换成SVC_Handler等名字于是向量表不用改一行汇编。另一种做法是直接改 startup 文件里的向量表但那样每次升级工程都要重新手动合并麻烦。3.2 FreeRTOSConfig.h 里值得注意的参数configTOTAL_HEAP_SIZE我给了 20KB这个数取决于你的任务数量和栈大小不是越大越好——LPC1768 总共才 64KB SRAMLWIP 还要吃十几 KB。另外务必开configCHECK_FOR_STACK_OVERFLOW为 2并实现vApplicationStackOverflowHook()这个钩子会在任务栈溢出时被调用开发阶段能救你很多次。#define configCPU_CLOCK_HZ 100000000UL #define configTICK_RATE_HZ 1000 #define configUSE_PREEMPTION 1 #define configUSE_TIME_SLICING 1 #define configUSE_MUTEXES 1 #define configUSE_COUNTING_SEMAPHORES 1 #define configMAX_PRIORITIES 8 #define configMINIMAL_STACK_SIZE 128 #define configTOTAL_HEAP_SIZE (20 * 1024) #define configMAX_SYSCALL_INTERRUPT_PRIORITY ( 5 (8 - __NVIC_PRIO_BITS) ) #define configKERNEL_INTERRUPT_PRIORITY ( 31 (8 - __NVIC_PRIO_BITS) ) #define configCHECK_FOR_STACK_OVERFLOW 2 #define configASSERT( x ) if( (x) 0 ) { taskDISABLE_INTERRUPTS(); for( ;; ); }configASSERT一定要留着调试时打开发布前再关。它能在你错误地在高优先级中断里调用 FreeRTOS API 时立刻让你知道而不是等到系统随机死机才去查。3.3 外设中断优先级能不能调用 FromISR API 的分界线FreeRTOS 在 Cortex-M3 上靠 BASEPRI 屏蔽中断。规则是中断优先级数值大于等于configMAX_SYSCALL_INTERRUPT_PRIORITY对应的值才允许在里面调用xxxFromISR系列函数。按上面配置NVIC 里优先级是 5 或更低的数值更大实际优先级更低中断可以安全调用 FreeRTOS API。以太网中断我会把优先级设置为 5~10 这个区间比如NVIC_SetPriority(Ethernet_IRQn, 6);。如果设得太高数值小ISR 里调用xSemaphoreGiveFromISR会触发断言或者直接卡死设得太低网络响应延迟会拖累吞吐。SysTick 和 PendSV 的低优先级不用你操心FreeRTOS 移植层自己会设。搞完这三步先别急着上 LWIP。写两个任务一个翻转 LED一个串口打印确认任务调度正常、tick 正常再进下一步。裸机时代那些delay_ms()轮询函数从此刻起就不要再用了全部换成vTaskDelay()。4. LWIP 移植的真正工作量sys_arch 适配层和 EMAC 网卡驱动LWIP 本身不依赖任何操作系统但一旦把NO_SYS设为 0它就要求你提供一套操作系统抽象层也就是 sys_arch。这一层加上 EMAC 驱动才是整个移植工作里真正花时间的地方。4.1 用 FreeRTOS 原语实现 sys_archsys_arch 要实现的接口大概分三类信号量、邮箱、线程创建外加一个取系统时间的sys_now()。我在arch.h里直接把 LWIP 的类型映射到 FreeRTOS#include FreeRTOS.h #include queue.h #include semphr.h #include task.h typedef SemaphoreHandle_t sys_sem_t; typedef QueueHandle_t sys_mbox_t; typedef TaskHandle_t sys_thread_t;邮箱是 LWIP 内核和协议线程之间通信的核心我直接用 FreeRTOS 队列实现队列元素是void*容量就是 LWIP 的LWIP_MBOX_SIZE。核心代码就几行err_t sys_mbox_new(sys_mbox_t *mbox, int size) { *mbox xQueueCreate(size, sizeof(void *)); return (*mbox NULL) ? ERR_MEM : ERR_OK; } err_t sys_mbox_fetch(sys_mbox_t *mbox, void **msg) { BaseType_t r xQueueReceive(*mbox, msg, portMAX_DELAY); return (r pdTRUE) ? ERR_OK : ERR_ABRT; } u32_t sys_now(void) { return (u32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS); }sys_now()能这么写的前提是configTICK_RATE_HZ是 1000这样 tick 数直接就是毫秒。如果你把 tick 降到 100这里就得分毫秒了LWIP 的 TCP 超时计算会受影响。线程创建也要特别注意栈大小单位。LWIP 的sys_thread_new()参数里stacksize是字还是字节不同移植包约定不一样。我统一把它当作字直接传给xTaskCreate()并在 lwipopts.h 里把TCPIP_THREAD_STACKSIZE配成 1024默认优先级 5。如果你约定不一致第一个 tcpip_thread 跑起来就会栈溢出现象是系统随机 HardFault极难排查。4.2 lwipopts.h 的配置思路lwipopts.h 是 LWIP 的性格配置文件我这份配置的核心思想是在 64KB SRAM 的 MCU 上宁可裁剪功能也要保证内存够用。#define NO_SYS 0 #define LWIP_SOCKET 0 #define LWIP_NETCONN 1 #define LWIP_DHCP 0 #define LWIP_STATS 1 #define LWIP_IPV4 1 #define LWIP_TCP 1 #define MEM_SIZE (8 * 1024) #define MEMP_NUM_PBUF 16 #define PBUF_POOL_SIZE 10 #define PBUF_POOL_BUFSIZE 1524 #define TCP_MSS 1460 #define TCP_WND (4 * TCP_MSS) #define TCPIP_THREAD_STACKSIZE 1024 #define TCPIP_THREAD_PRIO 5LWIP_SOCKET我关了因为 LPC1768 上跑 socket API 对内存的消耗太大netconn API 完全够用而且和底层 TCP/IP 线程配合更直接。LWIP_DHCP初始也关掉先用静态 IP 验证链路链路通了再开 DHCP。内存预算大概是这样FreeRTOS 堆 20KBLWIP 的 MEM_SIZE 8KBPBUF 池 10 × 1524 约 15KB加上几个任务的栈总量能压在 55KB 以内在 LPC1768 的 64KB SRAM 里是能落地的。4.3 EMAC 驱动别从零写站在 NXP 例程肩膀上LPC1768 的 EMAC 寄存器不少从零写驱动不现实。我的做法是拿 NXP 官方 LPC17xx LWIP 例程里的 EMAC 驱动做底子改造成贴合当前 FreeRTOS LWIP 版本的结构。核心要处理的部分有三个PHY 初始化、描述符环、中断收包路径。PHY 部分常见的板载 PHY 是 LAN8720、DP83848、KSZ8721 这类。初始化时先通过 MDIO/MDC 读 PHY 的 ID 寄存器能正确读到厂商 ID说明 MDIO 时序和引脚复用配置对了。MDC 时钟不要超过 2.5MHzLPC1768 的外设时钟需要适当分频否则读出来的数据是乱的。我调试时是先让它自动协商 100M 全双工确认链路起来之后再考虑固定速率。描述符环就是内存里一组 RX/TX 描述符数组每个描述符指向一个 buffer。RX 路径的典型做法是EMAC 中断来了清标志位然后给一个专用 RX 任务发二值信号量RX 任务拿到信号量后从接收描述符里把数据包取出来封装成 pbuf调用netif-input(pbuf)在 NO_SYS0 时就是tcpip_input()投递给 LWIP 内核线程。这样 ISR 里只做最轻量的事绝不在中断上下文里直接跑协议栈。TX 路径相对简单low_level_output()会收到一个 pbuf 链表我先把数据拷贝到 DMA buffer然后设置 TX 描述符的地址和长度置位所有权标志让 DMA 发送。拷贝虽然少了零拷贝的高效率但在 100M 网口上对付轻量级 TCP 应用完全够用而且逻辑简单不容易出错。5. 联调实测从 ping 不通到 TCP 回环稳定收发移植完成后最激动也最折磨的阶段就是联调。我习惯按顺序验证先确认任务调度再确认 PHY 链路然后静态 IP ping最后跑 TCP 回环应用。每个阶段都有固定的坑。5.1 最简单的 TCP 回环服务器先写一个小任务用 netconn API 起一个监听 8080 端口的 TCP 服务器收到什么就原样发回去。这是验证整条链路最直接的方式static void tcp_echo_task(void *arg) { struct netconn *conn netconn_new(NETCONN_TCP); netconn_bind(conn, IP_ADDR_ANY, 8080); netconn_listen(conn); while (1) { struct netconn *newconn; if (netconn_accept(conn, newconn) ERR_OK) { struct netbuf *buf; if (netconn_recv(newconn, buf) ERR_OK) { void *data; u16_t len; netbuf_data(buf, data, len); netconn_write(newconn, data, len, NETCONN_COPY); netbuf_delete(buf); } netconn_close(newconn); netconn_delete(newconn); } } }用 PC 上的网络调试助手连接板子的 IP 的 8080 端口发什么回什么说明从 EMAC 收包 → RX 任务 → tcpip_thread → netconn API → 回包发送的整条链路都通了。5.2 ping 不通的排查顺序ping 不通是联调阶段最普遍的问题我吃过不少亏总结出的排查顺序是先看 PHY 链路再看 ARP再看 ICMP。链路没过的话网线插上后 PHY 状态寄存器里的 link 位是 0。如果一直起不来先查 MDC/MDIO 引脚复用再查 PHY 复位引脚是否被正确拉高最后查自动协商有没有关掉。链路 OK 但 ping 不通就在 LWIP 里打开LWIP_STATS看 ICMP 和 ARP 的统计。我遇到最多的一种情况是TX 描述符没正确设置所有权标志导致发送的 ARP 请求其实没发出去表现为 PC 端 ARP 表里看不到板子的 MAC。另外强调一个反直觉的坑netif_set_up()不等于物理链路就绪。如果 PHY 还没完成自动协商你就把 netif 置 up 并配置了 IPLWIP 可能一直认为链路是断的。稳妥做法是先轮询 PHY 寄存器直到 link 建立再netif_set_up()。5.3 堆栈、堆和内存的坑位总结这一套系统跑起来之后内存问题是长期伴随的。FreeRTOS 的configCHECK_FOR_STACK_OVERFLOW一定要开着它会捕获任务栈溢出LWIP 那边任务栈溢出则往往表现为 tcpip_thread 忽然消失或者 HardFault。还有一种常见内存问题是 pbuf 泄漏netconn_recv拿到的 buf 用完必须netbuf_delete丢了就会让系统跑一两天后网络假死。中断优先级方面如果某个中断的优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY又调用了 FreeRTOS APIconfigASSERT会当场抓住。我踩过最隐蔽的优先级坑是定时器中断裸机工程里遗留的定时器中断里用了osDelay或类似阻塞操作这在裸机时代没问题上了 FreeRTOS 之后就成了定时炸弹。优先级反转在网络任务里也值得提一嘴。LWIP 的 netconn API 配合 tcpip_thread 模型应用任务通过邮箱和内核通信结构上不容易出现经典优先级反转但如果你图省事把LWIP_TCPIP_CORE_LOCKING设为 1让应用线程直接持有内核锁低优先级任务持锁时被高优先级任务抢占就会造成 tcpip_thread 被拖住。我的经验是保持默认的 tcpip_msg 模型别轻易开 core locking。最后再分享一个小技巧联调时把LWIP_STATS打开lwip_stats结构体里每一项统计都值得看一遍。ICMP 收包有计数但回包没计数方向就是 TX 路径ARP 表刷不出来方向就是 RX 描述符或者 DMA buffer 内存区不对。这套诊断方法比瞎猜寄存器快得多我后来移植到其他 MCU 上也都是这套思路通吃。本文还有配套的精品资源点击获取