ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

正点原子FreeRTOS lwIP移植实战:从网卡驱动到网络应用开发全解析

正点原子FreeRTOS lwIP移植实战:从网卡驱动到网络应用开发全解析 我从第一次把正点原子开发板的网线插上路由器、看到电脑上ping通了那个瞬间开始就被lwIP这套东西勾住了。但说句实话能ping通和能做出一个稳定的网络应用中间隔着的距离比很多人想象的要大得多。正点原子官方的lwIP例程给了我们一个几乎开箱即用的起点可一旦你想换PHY、加业务逻辑、调性能光靠例程那点注释完全不够。这篇文章就围绕正点原子平台把FreeRTOS上移植lwIP再到网络应用开发的完整链路拆开讲清楚包括底层网卡怎么对接、系统适配层做了什么、任务优先级怎么安排、常见的坑又在哪里。适合正在用正点原子开发板学习网络协议栈的同学也适合那些例程能跑通但想真正理解原理、准备做产品的工程师。1. 移植lwIP之前先搞清楚它和FreeRTOS的分工1.1 lwIP在嵌入式里到底是什么角色很多人第一次看lwIP源码会被吓到——一大堆文件core、netif、api、arch各自里面还有一堆子目录。其实抛掉表象lwIP就是个用C语言写的TCP/IP协议栈它负责处理IP分片、TCP状态机、UDP收发、ARP缓存、路由表这些网络逻辑。而FreeRTOS负责的任务调度、信号量、队列、内存管理lwIP本身并不重新实现它通过一个叫做sys_arch的适配层把需要操作系统帮忙的活全部转发给FreeRTOS。所以说移植lwIP这件事的核心不是把源码加到工程里就完事而是要把三样东西对接好时间节拍lwIP需要知道自己活了多久来判断超时重传、ARP老化等事件这个时间从哪里来线程与同步lwIP内部可能需要创建线程、使用信号量/邮箱来传递消息这些怎么映射到FreeRTOS网卡驱动以太网帧怎么从网线进入协议栈协议栈处理完后又怎么发出去这三样分别对应sys_arch.c、ethernetif.c以及你的底层ETH驱动。正点原子例程里做得好的一点是它把上述内容都封装好了但坏处也在这——很多人看例程时根本分不清哪些是lwIP自己的代码、哪些是正点原子写的适配代码出了问题也不知道从哪下手。1.2 两种常见的lwIP运行模式RAW API与Socket APIlwIP提供了两套差异很大的编程接口。理解它们是读懂正点原子例程的一把钥匙。RAW API是回调驱动的。使用这种接口时你不需要创建任何线程lwIP协议栈的逻辑是在发生事件时被调用的回调函数里执行。比如你调用tcp_write()发送数据协议栈内部会在函数调用中一路执行到网卡发送。接收数据时协议栈解析完TCP报文会直接调用你注册的tcp_recv回调函数。这种模式的优点是开销小、节省RAM缺点是应用代码必须非阻塞不能在里面干等、不能睡死否则整个协议栈都会被卡住。Socket API则复杂一些。它底层建立在netconn之上需要系统创建一个专门处理协议栈消息的线程——tcpip_thread。应用线程通过socket()、bind()、send()、recv()这些标准函数与协议栈交互lwIP通过邮箱mbox把消息传递给tcpip_thread去处理。对于用惯了PC网络编程的开发者来说Socket API爽多了代码逻辑清晰、可读性强但占用的资源也比RAW API多一些。正点原子的例程两种都有涉及。裸机版的例程基本用RAW API因为裸机上没有线程概念Socket API跑不起来FreeRTOS版的例程则提供了Socket API的选项因为此时已经有了多线程环境。我建议初学阶段先把RAW API搞明白因为它能逼着你理解lwIP内部的回调机制和状态机等真正做产品时再根据项目需求在两种模式里选型。2. 移植前的硬件认知与工程文件脉络2.1 正点原子各个平台的网络硬件差异正点原子的开发板覆盖了很多芯片方案不同平台的网络硬件结构差异非常大这是移植时很多人没意识到的第一个点。以STM32F103系列为例芯片内部并没有以太网MAC控制器正点原子例程里用的是SPI接口的ENC28J60网卡芯片。此时lwIP的底层驱动面对的不是MAC/PHY寄存器而是SPI读写函数和数据缓冲区的搬运。STM32F407、H743这些芯片则内置了以太网MAC只需要外接一颗PHY芯片典型如LAN8720A通过RMII接口连接。这种情况下底层驱动要处理MAC的DMA描述符、接收描述符链、发送描述符链以及通过MDIO总线读写PHY寄存器。再到IMX6ULL或者RK3568这类应用处理器平台情况又变了。它们跑的是完整Linux或RTOS系统网络栈、驱动框架、中断管理都高度复杂。虽然正点原子也提供了基于这些平台的网络例程但如果你只是想在嵌入式MCU上跑轻量协议栈F407/H743或F103平台其实更适合入门。所以移植的第一步是搞清楚你的硬件平台属于哪一类外置SPI网卡、MCU内置MAC外接PHY还是应用处理器自带完整网络接口。这三者的驱动代码写法和调试手段是完全不一样的。2.2 工程里哪些文件属于lwIP哪些属于适配层正点原子lwIP例程的工程结构看起来文件很多但层次清晰。如果按下图分类一切就明白了lwIP核心源码位于LWIP文件夹下的core/、netif/、api/目录。例如core文件夹里有tcp.c、udp.c、ip4.c、raw.c等协议实现api文件夹里有socket.c、api_lib.c、api_msg.c、netbuf.cnetif文件夹里有ethernet.c、loopif.c。这些文件一般不需要修改。移植层代码位于LWIP/arch/目录下主要包括cc.h、sys_arch.h、sys_arch.c、perf.h。cc.h定义了lwIP用到的基础数据类型的别名和字节序宏sys_arch.c就是前面说的胶水层——把lwIP需要的信号量、邮箱、互斥锁、线程创建、系统时间等映射到FreeRTOS。网卡驱动层通常是LWIP/netif/ethernetif.c。它本来只是lwIP官方给的一个模板驱动正点原子例程里一般会把它改造或替换成自己的实现比如有的改名为stm32_eth.c或者enc28j60.c。网络配置接口层正点原子自己写的netconf.c和其他类似文件。它负责调用lwIP的API完成IP地址设置、DHCP启动、网卡链接状态检测、netif初始化等操作并在main.c中被调用。应用层代码比如tcp_echoserver.c、tcp_client.c、udp_demo.c、httpd相关代码等这层就是用RAW API或Socket API写的实际业务逻辑。很多人在配置lwIP时找不到该改哪里其实就是没区分开这几层的职责。比如配置头文件lwipopts.h是编译期的总开关控制协议栈功能开关和内存分配参数而netconf.c里的函数是运行时的行为比如调用dhcp_start()启动DHCP客户端。两者是不同层面的东西。2.3 lwipopts.h里最重要的十几个宏lwipopts.h是lwIP的配置文件。正点原子例程里这份文件一般已经有了比较合理的初始值但你仍然需要知道每一个关键项背后的意义。挑几个对稳定性和功能影响最大的来说配置宏含义典型值NO_SYS是否无操作系统模式0表示带OS0LWIP_SOCKET开启Socket API支持1LWIP_NETCONN开启netconn API支持1LWIP_DHCP使能DHCP客户端1LWIP_DNS使能DNS客户端1MEM_SIZE内存堆大小供TCP段等使用16KB~64KBMEMP_NUM_PBUFPBUF池数量用于存储帧16~64TCP_MSSTCP最大报文段长度1460TCP_WNDTCP接收窗口大小4*TCP_MSSTCP_SND_BUFTCP发送缓冲区大小8*TCP_MSSCHECKSUM_GEN_IP等各层校验和生成方式一般开硬件加速LWIP_STATS是否开启统计信息调试时开产品可关这些宏直接决定了lwIP的内存占用大小。正点原子例程的默认值往往比较富裕适合确保功能正常但产品化时如果RAM紧张这些宏就是你要反复权衡的地方。比如MEMP_NUM_PBUF太小会导致收发帧时分配失败表现为丢包、DHCP异常TCP_SND_BUF太小则导致大流量发送时send()频繁返回内存不足。反过来如果RAM很宽裕适度调大这些参数能明显提升吞吐。我给一个调试经验在出问题的时候把LWIP_STATS打开在串口打印API中可以输出内存池使用率、IP收发包统计、TCP重传次数等数据。这些数字比任何感觉都直观。3. 网卡驱动对接整个移植过程中最容易翻车的环节3.1 low_level_output和low_level_input是怎么工作的如果说lwIP核心是大脑那ethernetif.c就是四肢。官方模板里low_level_output负责把协议栈要发送的数据从pbuf链表拷贝到网卡的发送缓冲区然后触发硬件发送low_level_input负责从网卡接收缓冲区把一帧数据读出来包装成pbuf交给上层。发送时经常要处理多个pbuf的情况。比如一个TCP报文协议栈可能把一个pbuf分成头部、数据、尾部等多个链表节点。驱动需要遍历整个pbuf链表把每一段拷贝到连续的发送缓冲区中。有的网卡支持DMA分散/聚集可以不做拷贝直接让DMA去读多个指针但STM32 F407这种通常需要先memcpy到一个连续缓冲区里再启动发送拷贝本身会消耗CPU也是吞吐量瓶颈之一。接收时则是另一条逻辑。正点原子例程里以太网DMA接收完成后会触发中断。中断里做的事情很少通常是发一个信号量给专门的接收线程接收线程被唤醒后调用low_level_input()读取帧数据再调用netif-input()把帧送进协议栈。为什么不在中断里直接处理因为lwIP协议栈处理TCP要运行状态机、分配内存、设置定时器过程太长中断里干这些会严重影响系统实时性而且lwIP很多函数本身就要求线程上下文调用不保证中断安全。3.2 PHY芯片地址和link状态为什么能让人排查两天PHY芯片是物理层的翻译官lwIP通过MDIO接口读写它的寄存器来获取链路状态、协商网速、设置工作模式。第一个让人栽跟头的就是PHY地址。正点原子F407例程里默认LAN8720A地址是0x01但如果你自己画板子、或者换了一颗不同封装的PHY地址可能变成0x00或其他值。地址没对上后面所有MDIO读写都是错的表现为网卡link status永远为down、ping不通但你查代码逻辑却完全没问题。我建议移植第一步先写个简单的MDIO读函数把PHY的ID寄存器读出来打印到串口确认能正确的读到PHY型号和地址然后才往下走。这一步能排掉大量底层问题。link状态检测同样重要。PHY的register 0x1BSR寄存器的bit 2表示链路是否建立。正点原子例程里一般会开一个周期性的PHY检测如果发现link掉了就通知协议栈关闭接口恢复后再重新开启。比如网线拔了再插如果没有正确的link处理DHCP可能拿不到新IP、ARP也会超时。这对于以太网应用来说算是一个隐形大坑——很多例程跑得好好的一拔网线就死掉多半是link状态处理不完善。3.3 基于正点原子例程改造网卡驱动时的注意事项如果你把正点原子的例程换到另一块板子上最常遇到的几处需要改的点PHY地址和复位引脚例程中通过#define或宏定义指定可能分布在ethernetif.c或hal_conf.h里。RMII/MII模式选择F407例程默认RMII如果你的板子用了MII接口要先改GPIO配置和MAC配置寄存器。DMA描述符数量例程一般配置4个发送描述符和4个接收描述符。如果你期望更高吞吐或更大接收缓冲适当增加到8或16。时钟源配置RMII需要一个50MHz的REF_CLK正点原子F407板子从MCO引脚输出换成自己板子时如果时钟信号不对MAC和PHY之间根本无法通信问题非常隐蔽。这些改造过程中强烈建议逐步验证而不是一把梭。先确认PHY的MDIO能读到寄存器再确认loopback模式下以太网控制器能自己发自己收最后才把lwIP接上去做应用层联调。4. sys_arch适配层FreeRTOS和lwIP之间的桥4.1 信号量、邮箱、互斥锁的映射关系sys_arch.c是让lwIP跑在FreeRTOS上的核心适配层。lwIP协议栈这个人需要操作系统帮它三件事等信号量、收邮箱、抢锁对应三个典型函数族。信号量在lwIP里主要用于同步比如接收线程等待一个网卡收到数据的信号量。正点原子例程中实现方式是直接封装FreeRTOS的xSemaphoreCreateBinary和xSemaphoreTake。注意一个细节lwIP的sys_arch_sem_wait支持超时参数FreeRTOS的xSemaphoreTake也支持阻塞时间可以直接映射。但如果超时参数是0意味着不等待要返回SYS_ARCH_TIMEOUT。邮箱在lwIP中本质是能装指针的定长队列最常见用途是向tcpip_thread投递消息。在FreeRTOS里可以用xQueueCreate实现队列元素大小设为指针大小通常4字节。需要特别注意的是lwIP的sys_mbox_fetch同样支持超时映射到xQueueReceive时要注意超时时间转换另外邮箱创建时的队列长度不能太小否则高负载时消息投递失败会导致连接异常。互斥锁用于保护临界代码段。lwIP在开启LWIP_TCPIP_CORE_LOCKING后Socket API的线程在进入协议栈时会先获取这个锁。FreeRTOS的互斥锁xSemaphoreCreateMutex自带优先级继承机制比使用二值信号量更安全可降低优先级反转导致的问题。这个细节在正点原子例程里没有特别强调但实际产品中影响很大。4.2 线程创建与时间戳的适配lwIP在带OS模式下需要一个tcpip_thread用来处理所有进入协议栈的消息和定时事件。这个线程在sys_thread_new里被创建。正点原子例程通常在lwip_init()或者netconf.c里显式调用tcpip_init()而tcpip_init内部会创建tcpip_thread。lwIP还要求sys_now()返回当前毫秒数。在FreeRTOS下最直观的写法是xTaskGetTickCount() * portTICK_PERIOD_MS但要注意乘法溢出问题——如果系统tick频率是1000Hztick值本身已经是毫秒就别再乘了如果是100Hz需要乘以10。更稳妥的方式是用一个高精度定时器或者专门的毫秒计数器来维护。这里踩过一个大坑如果sys_now()返回不正确TCP的重传和保活机制会完全混乱。现象是连接建立后过一会就断、收发数据时有莫名其妙的卡顿但ARP和ping却正常——因为ping走的是ICMP对时间精度要求不高。我排查了很久最后发现是tick换算溢出导致时间戳突然跳变。4.3 任务优先级分配的经验值在FreeRTOS中移植lwIP任务优先级的分配直接决定了系统的实时性和网络吞吐表现。以下是一套经过实际验证的优先级方案可作为参考以太网接收任务优先级最高比如configMAX_PRIORITIES-1。它需要快速响应中断投递的信号量及时把网卡缓冲区的帧取出来否则DMA描述符被占满网卡只能丢弃新包。tcpip_thread优先级稍低比如configMAX_PRIORITIES-2。它负责协议栈消息处理和应用定时事件优先级太高会抢占用户业务任务太低则协议栈响应慢表现为吞吐上不去。用户业务任务根据实时性需求设定一般低于上述两者。高实时性的传感器读取、电机控制等任务可以高于tcpip_thread毕竟网络数据晚几毫秒可以容忍但控制环路不能卡顿。正点原子例程里的优先级设置不一定适合你的业务场景。例程追求能跑你的系统要追求跑得稳。如果网络任务优先级给得太高可能会把界面刷新、按键扫描这些任务饿死给得太低ping延时忽高忽低TCP吞吐感人。建议实际测试时用压测工具同时观察业务任务的最大响应时间找到一个平衡点。5. 从一个能ping通的板子到真正做网络应用5.1 调试网络应用前把静态IP这步做扎实正点原子例程里既有静态IP也有DHCP。很多初学者一上来就开DHCP结果调试界面一会拿不到IP、一会IP变了本来网络问题就难查再叠加这个变量完全是在给自己添堵。我的建议是前期调试一律用静态IPPC端设置同一网段关掉PC的防火墙ping通了再进行后续应用层开发。等TCP/UDP业务都验证通过再切换到DHCP测试自动获取IP的场景。这不是说DHCP不好而是调试阶段要尽可能减少变量。具体配置时注意以下细节IP地址不能和其他设备冲突最好使用192.168.1.x这类私有网段。网关地址用来进行跨网段通信如果只在本机调试可以不用配置。子网掩码默认255.255.255.0即可。修改静态IP后重新插拔网线或者调用netifapi_netif_set_down/up让网卡重新初始化新的IP才会生效。在正点原子的netconf.c中LwIP_Init()函数会根据宏定义决定使用静态IP还是DHCP。DHCP模式下还会创建一个dhcp_start()调用。需要注意的是DHCP过程是异步的函数返回不代表已经拿到IP之后要通过回调或轮询判断dhcp状态很多老例程里是通过轮询方式检测是否分配成功。5.2 用RAW API写一个稳定的TCP Server正点原子例程里有一个经典的TCP server demo用RAW API实现。它的核心逻辑是一套tcp_xxx回调函数的组合创建PCB、绑定端口、监听、接收连接、接收数据、发送完成。一个标准的RAW API TCP server骨架大概是这样struct tcp_pcb *server_pcb; server_pcb tcp_new(); tcp_bind(server_pcb, IP_ADDR_ANY, 8080); server_pcb tcp_listen(server_pcb); tcp_accept(server_pcb, tcp_server_accept_callback);在accept回调里为新的连接注册recv回调、sent回调和tcp_poll回调。recv回调是数据处理的入口每次协议栈收到数据就会调用它static err_t tcp_server_recv_callback(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) { if (p NULL) { // 对端关闭连接释放资源 tcp_close(tpcb); return ERR_OK; } // 处理数据... tcp_recved(tpcb, p-len); // 告诉协议栈接收缓冲区已释放 pbuf_free(p); return ERR_OK; }这里有一个特别容易出问题的地方tcp_recved()必须调用。它告诉lwIP应用层已经读走了数据内核可以释放这些缓冲并扩大TCP接收窗口。如果你漏掉这行对端发送窗口会逐渐变小最终表现为通信几次之后就卡死。这是RAW API调试中最常见的坑。发送数据时tcp_write()只是把数据复制到发送队列真正发出要等tcp_output()或者协议栈自行触发。如果tcp_write返回ERR_MEM说明发送缓冲区不足需要注册tcp_sent回调在发送完成后再继续发送而不是立刻重试。总结成一句话RAW API的回调里不能做长时间阻塞操作否则整个协议栈就僵住了。5.3 什么时候选用Socket API正点原子例程中以RAW API为主但我要明确说如果你的MCU资源足够产品代码我建议优先考虑Socket API。原因很朴素——代码可读性和可维护性。RAW API的回调模式写复杂业务时很容易变成回调地狱不同状态之间靠函数指针跳来跳去三个月后你自己都不一定看得懂。而Socket API的代码结构和PC端网络编程几乎一模一样逻辑线性清晰。一个简单的Socket API TCP echo server任务可以这样写void tcp_echoserver_task(void *arg) { int sock socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr; server_addr.sin_family AF_INET; server_addr.sin_port htons(8080); server_addr.sin_addr.s_addr INADDR_ANY; bind(sock, (struct sockaddr *)server_addr, sizeof(server_addr)); listen(sock, 5); while (1) { int client_fd accept(sock, NULL, NULL); if (client_fd 0) { char buf[512]; int len recv(client_fd, buf, sizeof(buf), 0); if (len 0) { send(client_fd, buf, len, 0); } closesocket(client_fd); } } }这种代码给新人看一行一行读下去就能理解而RAW API那种回调跳转的方式新人基本一头雾水。当然Socket API的代价是资源占用更高而且有一些线程限制——同一个socket不能同时被多个任务读写否则需要加应用层锁。另外lwIP的select()在资源紧张时可能有bug或限制复杂场景要谨慎测试。6. 实测中的常见问题与性能调优思路6.1 ping不通时的排查顺序无论你是移植新人还是被客户问题缠身的工程师ping不通都是绕不开的噩梦。我根据正点原子平台的实践经验整理出一套固定的排查顺序按这个顺序走绝大多数问题都能找到根源排查步骤检查内容常见结论1网线、路由器、PC网卡物理状态物理层不通换网线/接口2PHY的BSR寄存器bit 2link状态PHY未link查RMII时钟、PHY地址、复位电路3PHY的ID寄存器MDIO通信异常PHY地址错误4PC与板子IP是否同网段网段不一致重新配置5板子能否收到ARP请求收不到检查eth_rx任务、中断信号量、DMA接收6板子是否回复ARP不回复检查netif-input是否上抛帧、IP配置是否正常7板子是否回复ICMP echo不回复检查协议栈任务、RAW API处理路径实际操作中我一般会让板子在调试串口打印调试信息收到ARP时打印ARP received收到ICMP时打印ICMP received。这样能很快判断出数据走到了哪一步断掉。正点原子例程里如果加上了这些打印排查效率能提升一个量级。另外一个排查窍门是打开lwIP的统计功能。LWIP_STATS设置为1后可以在代码里调用stats_display()需要串口驱动配合把IP/ICMP/TCP/UDP/内存池的收发计数全部打出来。看到IP层的Rx计数在增长说明帧进来了看到ICMP的Tx在增长说明协议栈有回复那问题大概率在PC端。6.2 DHCP获取不到IP的经典原因正点原子例程中DHCP跑不起来归纳起来不外乎几个原因PBUF池不足。DHCP过程需要大量的UDP小包交互如果MEMP_NUM_PBUF太小包接收时会分配失败典型表现为DHCP DISCOVER发出了但收不到OFFER。这种情况需要调大MEMP_NUM_PBUF。协议栈的定时器没有运转。如果用了旧版lwIP需要额外调用sys_check_timeouts()新版lwIP已经内置于tcpip_thread但如果你用了NO_SYS或者手动创建线程的方式可能漏了这个关键调用。没有等待link up。有些例程在网卡link尚未建立时就启动DHCPPHY还没协商好就发DISCOVER自然会失败。应确保link up后再启动DHCP。DHCP服务器没响应。路由器可能没开DHCP或者交换机的DHCP snooping配置限制了端口这个需要从路由器日志或者换个路由器测试。DHCP调试时在netconf.c里打印dhcp状态机的状态值非常有用。DHCP状态机从0开始逐步推进停在哪个状态就说明哪一步出了问题配合上述几个方向去查成功率会高很多。6.3 吞吐量和稳定性的优化手段当你的lwIP应用从能跑通走向要跑快时优化方向主要集中在以下几个方面调整内存池和缓冲区的数量。把MEMP_NUM_PBUF、MEMP_NUM_TCP_SEG、MEMP_NUM_NETBUF适当调大减少高负载下内存分配失败的概率。但要注意RAM有限调大这些会挤占其他空间需要在稳定和资源之间做平衡。增加DMA描述符数量。正点原子例程默认4个接收描述符如果收到的帧速度快而任务调度不及时4个描述符很快会被占满并丢弃新包。增加到8~16个后在突发流量下的丢包率会明显下降。利用硬件校验和卸载功能。STM32F407的MAC支持IP/TCP/UDP校验和的硬件计算开启后CPU不需要参与校验和计算能节省不少开销。在lwIP中通过CHECKSUM_GEN_IP、CHECKSUM_GEN_TCP等宏配合寄存器设置来完成。增大TCP发送窗口和接收窗口。TCP_WND和TCP_SND_BUF越大单次能传输的数据越多TCP_MSS一般为1460在MTU为1500的以太网上基本不需要改。优化拷贝路径。正点原子例程的发送路径里一般有一个memcpy把pbuf拷贝到连续的DMA缓冲区这一步无法避免但可以考虑使用更大的连续缓冲区来减少碎片或者直接让DMA读多个不连续缓冲区如果硬件支持。我实测过一组对比数据在F407平台上默认配置PBUF池64、描述符4个、TCP_WND为4MSS时TCP下载速度只有约3~5Mbps把PBUF池加到128、描述符加到16、TCP_WND扩大到16MSS后吞吐能跑到10Mbps以上。当然这个数字还取决于CPU频率和你的应用任务负载。IMX6ULL这类平台跑ethercat或大流量网络时调优空间会更大。最后再分享一个小技巧调试lwIP性能时不要只看ping延时那玩意儿参考价值有限。真正的性能指标是TCP大块数据传输速率和在高频小数据包场景下的丢包率。正点原子例程结合串口printf配合PC端的iperf或网络调试助手压测一轮下来所有瓶颈都会现出原形。我之前遇到的绝大多数网络问题最后都是靠这个组合定位出来的。
返回列表