ARTICLE DETAIL

资讯详情

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

STM32F407裸机移植lwIP并搭建HTTPD服务器全攻略

STM32F407裸机移植lwIP并搭建HTTPD服务器全攻略 上一篇文章我们把STM32F407的以太网外设、PHY芯片和基础工程跑通了网线一插还能看到以太网链路状态有变化。但只有硬件驱动远不够要想真正上网、收发数据还差一个最关键的部分——TCP/IP协议栈。这第二篇咱们就集中解决两件事第一把lwIP协议栈移植到F407上第二让开发板变成一个能通过浏览器访问的HTTPD服务器。整个过程我都在裸机环境下完成没有上RTOS这样逻辑更直观项目也更容易定位问题。如果你正在做基于STM32F407的网络应用比如参数配置、远程状态查看、数据采集上报这篇内容可以直接当参考。1. 移植前的整体思路与关键决策1.1 为什么选择lwIP而不是其他协议栈在STM32F407这种中等偏上资源的MCU上可以选的TCP/IP方案不少。手上正好有一块带以太网PHY的板子但没带W5500这类硬件协议栈芯片所以最自然的就是用MCU内置的MAC外设加外部PHY软件层跑一个开源协议栈。lwIP的知名度和代码质量都不错文档多遇到问题也容易搜到。先说资源开销。lwIP在裸机模式下不开太多功能RAM占用大概十几到几十KBFlash几十KB。F407有192KB RAM、1MB Flash完全跑得动。如果用uIP它更精简但TCP性能、多连接能力都比较弱功能也不完善。如果用硬件协议栈芯片比如W5500确实能减轻MCU负担但它多了个SPI接口芯片成本高对于F407这种带MAC的MCU反而没必要。所以这个项目用lwIP是最合适的。1.2 裸机方案与RTOS方案的取舍很多人在F407上玩lwIP会配合FreeRTOS因为官方项目里FreeRTOS这类移植很多。但我的这个项目是数据采集类任务简单裸机跑lwIP完全够用。裸机方案下整个系统是一个大循环定时器提供节拍lwIP的协议处理在循环里被周期调用。好处很明显代码简单没有线程同步不会因为调度问题导致数据竞争。坏处是长耗时操作会阻塞协议栈比如在HTTPD内置SSI查询传感器时间的慢操作要小心后面会细说。如果你最后要上RTOS只需要把NO_SYS配置改为0并实现sys_arch层的线程、信号量、邮箱等接口应用层代码基本不用改。这一点我建议你在移植之前就决定好避免中途返工。无论是用STM32CubeMX生成的工程还是参考正点原子或野火的例程以太网外设初始化逻辑都很接近差别主要在PHY芯片和相关引脚上。1.3 硬件准备F407 MAC与PHY的基础配合先交代一下硬件基础。我用的PHY是LAN8720A这是一颗很常见的百兆以太网PHYRMII接口连接STM32F407的MACMDIO引脚负责配置PHY内部寄存器。F407自带以太网外设ETH、RMII、MDIO这些引脚的GPIO初始化在第一篇里已经做了芯片包一般也都支持。LAN8720A需要注意三点PHY地址默认是0上电时通过PHYAD0引脚的电平决定板子通常固定好了。RMII需要外部提供50MHz参考时钟方向、倍数必须配置好否则传输会有问题。有的板子是STM32输出50MHz给PHY有的板子是外部晶振先给PHYPHY再回给STM32。具体要看原理图。PHY复位时序要在初始化时保持足够低电平时间否则读ID会失败。如果这些基础部分还没调通建议先回上一篇把PHY的ID能读出来把网线插拔寄存器能读到变化再做协议栈排查效率会高很多。2. lwIP协议栈移植的详细步骤2.1 源码准备与工程目录组织我用的是lwIP 2.1.2直接去官网下的release包。里面核心目录是src子目录有core、netif、api、apps。其中core里放的是TCP/UDP/IP/ICMP等核心实现netif里是以太网网卡抽象apps里有HTTPD、DHCP客户端、SNTP等现成应用。另一个重要的包是从lwIP官网的contrib仓库里拿的port目录它包含各个平台的移植示例比如UNIX、Win32、STM32等等。这些资料可以作为参考但不建议直接整个拷进来容易把工程搞乱。我自己工程结构大概是这样Middlewares/Third_Party/LwIP/src协议栈源码Middlewares/Third_Party/LwIP/port我自建的移植文件包括lwipopts.h、ethernetif.c、sys_arch.cApp/httpdHTTPD相关网页数据文件和回调代码养成把“需要修改的配置”集中放在一个目录里的习惯。lwIP的默认配置在lwipopts.h里工程里千万不要把每个选项都自己造一份尽量基于官方默认配置修改减少维护负担。2.2 修改lwipopts.h的关键配置这是移植的核心文件。先说几个最关键的宏#define NO_SYS 1 /* 裸机模式 */ #define LWIP_SOCKET 0 /* 关socket API */ #define LWIP_NETCONN 0 /* 关netconn API */ #define LWIP_HTTPD 1 /* 启用HTTPD */ #define LWIP_DHCP 1 /* 启用DHCP */ #define LWIP_STATS 1 /* 统计信息 */ #define MEM_ALIGNMENT 4 /* F407是32位MCU */NO_SYS1意味着lwIP不依赖OS直接运行在裸机上不需要实现sys_arch的线程、信号量只需要提供sys_now这种获取当前时间的函数。LWIP_SOCKET和LWIP_NETCONN是高层API裸机模式下可以关掉因为HTTPD和我们的应用都走的是raw API即tcp_xx、udp_xx回调函数。内存配置要重点说。我遇到过PBUF_POOL_SIZE太小导致TCP传输卡死的情况。一般裸机局域网应用可以这样调整#define MEM_SIZE 1600 * 10 #define PBUF_POOL_SIZE 20 #define PBUF_POOL_BUFSIZE 1512 #define TCP_MSS 1460 #define TCP_WND (4 * TCP_MSS) #define TCP_SND_BUF (4 * TCP_MSS)TCP_WND是接收窗口代表能一次性接收多少字节的数据而不等待ACKTCP_SND_BUF是发送缓冲代表应用层可以交给协议栈多少数据而不会被阻塞。这两个值设置的太小大块数据传输就会变慢设置太大又占RAM。F407的RAM够用我一般按4到6个MSS配数据吞吐大概能到几Mbps左右对于HTTP网页和配置数据量来说已经很充裕。PBUF_POOL_BUFSIZE要能装下一个完整以太网帧再加上链路层头部。1512字节是我实测能跑通的典型值因为一个标准以太网MTU是1500字节加上14字节以太网头正好。如果你以后要开VLAN或者Jumbo Frame就得改大。2.3 网卡驱动接入流程lwIP的网卡抽象层是struct netif网卡驱动要做的就是把以太网外设收到的数据包交给lwIP的pbuf队列同时把lwIP要发送的pbuf拆成帧发送到PHY上。在裸机下我的接收路径是以太网DMA收到数据产生中断或由主循环的ethernetif_input轮询。驱动从接收描述符里取出数据拷贝到pbuf。调用netif-input(pbuf, netif)把数据交给协议栈。发送路径是协议栈调用low_level_output。驱动把pbuf里的数据整理成连续内存填入发送描述符触发DMA发送。这里有个容易踩的坑如果接收路径放在中断里调用netif-input协议栈会在中断上下文里运行一旦HTTPD回调里做了较重的文件读取或格式化操作很容易占用中断太长时间甚至造成栈溢出。所以我推荐裸机环境下采用“中断置标志、主循环轮询”的方式。具体做法是在ETH中断里只置一个标志位比如g_rx_flag1主循环看到标志后就调用ethernetif_input。这个函数会从DMA描述符中取一个包交给lwIP再清除标志继续取下一个。这样协议栈运行在main上下文中断只做最少的操作。2.4 时间基准与超时机制lwIP内部有大量定时器比如ARP老化、TCP重传、DHCP重试等。裸机下必须提供两个时间相关函数一个是毫秒时钟源对应lwIP提供的sys_now()另一个是lwIP内部的超时检查函数sys_check_timeouts()。我用SysTick提供1ms中断在中断里累加一个全局变量g_sys_tick。然后在sys_now里返回这个变量的毫秒值即可。主循环里每隔一段时间调用sys_check_timeouts()让核心协议栈处理所有超时事件放main loop里就行。注意的是如果长时间阻塞在某个地方比如读取外部Flash、等待串口会导致超时检查不及时TCP重传会误判。所以主循环里别写太重的处理逻辑HTTPD处理时也要注意。3. HTTPD服务器搭建与网页资源管理3.1 HTTPD的两种资源访问方式lwIP自带的HTTPD服务器可以实现两类功能提供静态网页以及通过CGI和SSI让网页和MCU交互。资源文件的访问方式有两种一是使用外部文件系统比如FatFS挂SD卡或SPI FlashHTTPD通过文件系统读取网页二是直接把网页打包成C数组编译进固件。我这边的项目因为网页只有几个页面不需要在线更新网页内容所以用了第二种方式最简单稳定。如果以后网页很多需要随时修改内容可以考虑外挂文件系统但代价是文件系统驱动本身也会占用Flash和RAM。3.2 把HTML页面生成C数组假设你有一个index.html里面包含静态文本和一些占位符如何把它变成C数组lwIP官方提供了一个工具makefsdata它可以扫描一个目录下所有网页文件生成fsdata.c里面是HTTP/1.1的完整响应头和页面数据。但我个人更喜欢自己写一个小脚本把网页按字节转成const char数组放入工程原因是我可以精确控制HTTP响应头比如加Cache-Control、Content-Type等。HTTPD在调用fs_open时需要返回一个fs_file结构其中data指针指向页面数据len是数据长度http_header_p等字段可以自定义。注意如果网页中有中文内容生成数组时一定要注意编码。文件保存为UTF-8无BOM数组里就是UTF-8字节流浏览器一般能识别。我之前用GBK编码折腾了好一阵后来统一改成UTF-8才顺了。3.3 配置HTTPD和自定义文件系统接口要使HTTPD使用自定义文件数组需要在lwipopts.h里开启#define LWIP_HTTPD_CUSTOM_FS 1 #define LWIP_HTTPD_DYNAMIC_HEADERS 1然后实现httpd_custom_fs.c文件对外提供fs_open、fs_read、fs_close等接口。最简单的fs_open实现就是根据文件名查表返回对应网页数组的指针和长度。HTTPD初始化只需要调用httpd_init()。它内部会在TCP的80端口创建监听收到连接后根据URL调用你的fs_open。如果URL是/通常要映射到你默认的index.html所以fs_open里看到空路径时也要返回首页数据。3.4 CGI和SSI让网页和单片机交互静态网页只能看不能控制。好在HTTPD支持CGI和SSI两种机制。CGI适合处理表单提交。网页里写一个formaction指向/cgi/setledmethodPOST然后我在代码里注册一个tCGI结构数组把/cgi/setled和对应的处理函数绑定。函数收到参数后解析比如判断ledon还是off然后操作GPIO最后返回一个HTTP重定向或带状态码的响应。SSI适合在页面加载时动态填充数据。页面里放!--#temperature--这样的标签代码里注册SSI标签处理函数当HTTPD解析到该标签时会回调处理函数由函数把温度数值以字符串形式填入。这两个功能能组合出一个非常典型的上位机页面设备状态、参数设置、控制按钮全部在浏览器里完成。做产品原型时很好用。3.5 资源占用与并发连接HTTPD默认可以同时支持多个TCP连接但裸机下内存有限建议把MEMP_NUM_TCP_PCB_LISTEN和MEMP_NUM_TCP_SEG这些参数调小一些。如果并发连接数太多内存紧张时HTTPD会返回503。我一般把MEMP_NUM_TCP_PCB设为6MEMP_NUM_TCP_SEG设为16够用了。浏览器通常会自动建立多个连接来优化页面加载所以网页里有多个图片、CSS、JS时会出现并发请求。此时如果内存不够会出现页面加载不全。解决办法是减少网页内外链资源把样式尽量内联图片转小尺寸。4. 调试方法与常见问题排查4.1 用Ping与抓包工具定位故障层协议栈移植最容易踩的坑是“看着像通了实际收发不了”。我的调试思路是分层排查。第一步确认PHY物理链路。通过MDIO读取PHY寄存器1状态寄存器看看bit2是否为1为1表示网线已连接。或者直接看板载LED灯是否亮起。第二步确认MAC层收发。在low_level_input的入口处打一个错误计数在low_level_output里也打一个。如果从来没有进入说明DMA配置或PHY时钟有问题。第三步确认IP层。给开发板设置静态IP例如192.168.1.10电脑配置在同一个网段然后在电脑上ping开发板。如果ping通说明ARP、ICMP已经正常TCP应该也差不了太多。如果ping不通打开lwIP的DEBUG输出参考lwipopts.h里的LWIP_DEBUG配置把ICMP_DEBUG和ETHARP_DEBUG打开在串口观察协议栈在干什么。如果电脑上安装了Wireshark直接抓包更直观。电脑ping开发板时先看到ARP请求开发板回复然后是ICMP请求和回复。如果只有ARP请求没有回复问题在MAC层或驱动如果ARP有回复但ICMP没回复可能路由、IP配置或ICMP应答有问题。4.2 内存不足的排查与参数调整裸机移植lwIP最常见的问题是内存分配失败。现象是刚开始能ping通但一旦HTTP浏览器发起请求开发板就死机或重连不上。大概率是PBUF或MEM内存不足。建议我每次调参都会开统计在lwipopts.h里设置#define LWIP_STATS 1 #define LWIP_STATS_DISPLAY 1 #define MEM_STATS 1 #define PBUF_STATS 1 #define TCP_STATS 1 #define LINK_STATS 1然后在主循环里每隔5秒调用stats_display()把内存池状态打印到串口。重点关注两个数free pbuf数量和mem_free内存剩余量。如果一直为0说明内存不够用可以适当把PBUF_POOL_SIZE调大或者把TCP_SND_BUF调小来省内存。还有一个坑是内存对齐。F407是32位MCUpbuf和网络缓冲区都需要4字节对齐否则以太网DMA可能报总线错误。如果你用KEIL的默认堆栈分配要注意定义接收缓冲数组时使用__attribute__((aligned(4)))或者放在一个专门的对齐段。4.3 HTTPD页面打不开或显示不全排查顺序我一般是这样浏览器访问http://192.168.1.10/如果能看到部分页面说明HTTPD已经在跑问题多半出在资源文件索引上。打开浏览器的开发者工具看Network标签页看是哪个请求失败了比如favicon.ico、CSS文件404。如果是404去fs_open里对照一下文件名是否完全一致大小写、隐藏后缀都要注意。如果是乱码检查网页编码。HTML文件统一用UTF-8无BOM服务器端动态内容也用UTF-8。另外浏览器会自动请求favicon.ico如果没提供会多一个404在HTTPD调试时容易造成误判。我通常在fs_open里判断文件名是favicon.ico时直接返回404或者在页面里用link标签指定一个小图标。4.4 PHY与硬件疑难问题这里分享两个硬件坑都是我实际遇到的。一个是RMII 50MHz时钟相位问题。有的板子用外部有源晶振给PHY提供50MHz有的用F407的MCO输出50MHz。如果PHY的时钟和STM32 MAC采样沿匹配不好会出现时通时不通。排查时用示波器看PHY的CLKOUT脚确认频率是50MHz且上升沿和RX_DV对齐。代码层面也可以调整MAC的时钟偏差寄存器但通常还是硬件设计问题居多。另一个是PHY地址问题。LAN8720A默认地址是0但有些PHY默认是1。你在移植netif_add之前一定要先读出PHY ID确认地址。如果读错了lwIP初始化时可能把管理接口地址写错导致PHY无法正常工作。最直接的办法是看PHY的LED状态网线插上后Link LED亮不亮。不亮就先查PHY配置和电源。下面的表格是我整理的一套快速故障定位方法现象可能原因排查手段网线插上LED不亮PHY复位、电源、RMII时钟示波器查50MHz读PHY IDping不通Send包计数增加但收不到DMA描述符配置错误检查low_level_input是否进入能ping通HTTPD访问超时内存不足或HTTPD未初始化查PBUF统计确认httpd_init位置网页打开乱码网页编码不一致文件统一UTF-8无BOM传输大文件卡死TCP窗口/发送缓冲太小调TCP_WND和TCP_SND_BUF5. 一个最小可跑的HTTPD示例框架代码参考5.1 main loop的完整流程梳理下面我把裸机环境下的主循环框架列出来方便你有感性认识。这个例子配合上面的移植步骤运行后打开浏览器访问静态IP就能看到首页。volatile uint32_t g_sys_tick 0; void SysTick_Handler(void) { g_sys_tick; } uint32_t sys_now(void) { return g_sys_tick; } int main(void) { SystemInit(); GPIO_Config(); ETH_Config(); PHY_Init(); lwip_init(); netif_add(g_netif, ipaddr, netmask, gw, NULL, ethernetif_init, ethernet_input); netif_set_default(g_netif); netif_set_up(g_netif); httpd_init(); while (1) { if (g_rx_flag) { ethernetif_input(g_netif); g_rx_flag 0; } sys_check_timeouts(); // your custom app task, keep it short app_poll(); } }注意这里netif_add的第六个参数是ethernetif_init第七个是ethernet_input它是把收到的以太网帧转换成IP包的回调入口。在裸机模式下我们手动调用ethernetif_input然后它内部会调用netif-input最终进到ethernet_input。5.2 httpd_fs核心接口的实现要点自定义文件系统最关键的是fs_open。我给出一个最简模板const char http_index[] { 0x48, 0x54, 0x54, 0x50, 0x2f, 0x31, 0x2e, 0x31, 0x20, 0x32, 0x30, 0x30, 0x20, 0x4f, 0x4b, ... }; int fs_open(struct fs_file *file, const char *name) { if (name NULL || name[0] \0 || strcmp(name, /) 0) { file-data http_index; file-len sizeof(http_index); file-http_header NULL; file-pextension NULL; return 1; } return 0; } void fs_close(struct fs_file *file) { /* nothing */ }这里的name是HTTPD根据URL传进来的比如//index.html。所以必须有默认首页映射。如果你有多个页面可以简单写一个if-else或者查表把每个URL对应到不同的数组。还有一个容易忽略的点HTTPD会调用fs_read多次来发送大文件。如果你把整个文件都放在data指针里fs_read可以直接返回文件数据但如果数据是流式生成的比如动态生成CSV就需要在fs_read里根据偏移量拷贝数据。httpd_fs接口设计为“一次给全”其实也可以lwIP内部会把data按照TCP_MSS拆分发送。5.3 CGI处理函数的一个实例假设网页上有个按钮GET请求/cgi/setrelay?state1期望打开继电器。CGI映射和函数可以这样写static const tCGI cgi_handlers[] { { /cgi/setrelay, cgi_setrelay }, }; const tCGI *cgi_handler_find(const char *uri) { int i; for (i 0; i sizeof(cgi_handlers)/sizeof(cgi_handlers[0]); i) { if (strcmp(uri, cgi_handlers[i].uri) 0) { return cgi_handlers[i]; } } return NULL; } static const char *cgi_setrelay(int iIndex, int iNumParams, char *pcParam[], char *pcValue[]) { for (int i 0; i iNumParams; i) { if (strcmp(pcParam[i], state) 0) { relay_set(atoi(pcValue[i])); break; } } return /result.html; /* 处理完成后重定向 */ }注意CGI处理函数返回的字符串是重定向URL。如果用户请求时带了参数lwIP会解析URL中?后面的键值对然后调用处理函数。函数内部尽量别做太耗时的事因为它是被HTTPD的raw回调调用的阻塞期间整个协议栈都可能停顿。5.4 SSI标签替换示例SSI的实现也很简单。网页中写!--#adc0--然后实现static u16_t ssi_handler(int iIndex, char *pcInsert, int iInsertLen) { uint16_t val read_adc(0); int len snprintf(pcInsert, iInsertLen, %d, val); return (u16_t)len; } static const char *ssi_tags[] { adc0, adc1, temp, NULL };使用前要设置SSI线程栈大小裸机下不需要。但要注意SSI标签回调是同步执行的如果你在回调里读取ADC耗时太长会导致HTTPD响应变慢实测在网页打开时会卡一下。解决办法是预先在后台轮询里把数值更新到全局变量SSI回调只读变量。6. 给后续项目的一些建议6.1 从HTTPD扩展到更复杂的Web服务搭建起HTTPD后你可以在这个基础上扩展出很多功能。比如把设备配置界面Web化再配合网页里的JS定时轮询就是一个轻量级的数据监控页面。也可以加入简单的Token验证来做访问控制。不过要提醒一下lwIP本身只提供HTTP/1.1的静态服务不支持WebSocket、HTTPS等高级特性。如果以后要上HTTPS就要引入mbedTLS并对lwIP做TLS层适配复杂度会上一个台阶。6.2 裸机工程迁移到RTOS的思路我前面提过如果项目任务变多裸机模型会显得吃力。这时候把lwIP迁移到FreeRTOS核心工作就是实现sys_arch.c里的接口。你不需要重写网卡驱动只需要把ethernetif_input从主循环挪到一个专用的接收线程里再给任务设定合适的优先级。HTTPD这类应用也可以独立线程化。我之前从裸机迁移到FreeRTOS大概花了半天时间最大的工作量在调试线程优先级和内存分配上。6.3 关于“系列文章”的衔接这第二篇主要讲的是协议栈移植和HTTPD搭建很多细节受篇幅限制没法展开。第三篇如果继续写我想重点聊聊如何做一个稳定可靠的固件升级功能也就是基于lwIP的HTTP或TFTP升级那会让整个设备具备远程运维能力。不过在那之前先把今天的文章消化掉把HTTPD跑通后面的升级功能只是在这个框架上加一层应用逻辑而已。最后再分享一个小技巧每次调整内存参数或者改动网卡驱动后别急着马上测试业务功能先固定IP、跑一轮ping和HTTP页面刷新确认最底层稳定了再做其他功能。这样即使出错也能很快判断是这一层的问题还是上层应用的问题。我踩过太多次“看起来什么都能通一传大数据就崩”的坑后来都是靠这种逐层回归的土办法把问题锁死的。
返回列表