STM32F407 + lwIP 这个系列写到第二篇,前一篇我们把以太网物理层、DMA收发、中断这些底层都调通了,这篇接着做 lwIP协议栈移植 和 HTTPD服务器搭建。所谓HTTPD,是lwIP里自带的一个轻量HTTP服务器,不用在单片机上跑Linux,一块F407就能直接给浏览器提供网页,用来做设备配置页、状态监控、远程开关控制特别合适。文章会从CubeMX工程怎么配、lwIP内存参数怎么调、HTTPD怎么启用、网页文件怎么打包进固件,一直讲到实际排错,尽量让手里有开发板的同学按着步骤就能把网页跑起来。
如果你现在的情况是:开发板网口能接上路由器,PHY也通了,甚至裸机下能收到ARP请求,但离“浏览器输入IP就能打开控制页面”还差一口气,那这篇正适合你。下面所有内容均以我手头这块STM32F407VET6 + LAN8720A的板子为例,不同板子的PHY型号和引脚会有差异,但思路是通用的。
1. 移植前的整体思路:先把框架搭起来
1.1 为什么我建议用CubeMX生成基础工程
lwIP的裸移植,网上能搜到很多手写版本的教程,一般会给你一个ethernetif.c,然后让你手动实现low_level_output、low_level_input、ethernetif_input这些函数,再把pbuf、netif、DMA描述符串起来。这种方式确实能帮你把lwIP每个角落都弄明白,但如果你是冲着快速出活来的,我强烈建议先用STM32CubeMX生成基础工程。
CubeMX帮你解决掉的是最容易被低级错误绊倒的部分:时钟树、GPIO复用、ETH的DMA描述符初始化、中断向量、lwIP内核线程创建,这些在生成代码里都已经处理好了。你拿到手的工程,理论上只要改一下IP地址就能ping通,剩下的精力可以用在HTTPD和上层应用上。
但这不意味着生成之后就直接盲用。我仍然建议你把生成的关键文件过一遍,至少要知道这么几个地方:
- lwip.c里的MX_LWIP_Init到底干了什么,静态IP是在哪里配的
- ethernetif.c里的low_level_init是怎么拿到PHY地址的,DMA描述符数量是多少
- lwipopts.h里那些宏分别控制哪块内存
- CubeMX开启了HTTPD之后,httpd_init是不是在MX_LWIP_Init里自动调用了
原因很简单:后面出问题的时候,你大概率要在这些文件里加日志、改参数。如果连文件在哪都不知道,排错会非常痛苦。
1.2 这篇要解决的核心问题
我把“能ping通”和“能打开网页”之间缺的东西拆开来看,其实是三层:
第一层是lwIP协议栈本身要能跑起来,TCP/IP协议栈初始化成功,ARP、ICMP这些基础协议正常响应。这一层没做好,表现就是ping不通或者时通时断。
第二层是HTTPD服务要起来。lwIP自带的httpd是一个独立应用,它依赖netconn或raw API,需要监听80端口并处理HTTP请求。这一层没做好,表现就是IP能ping通,但浏览器打不开页面。
第三层是网页内容要能送达浏览器。lwIP的httpd默认把网页文件打包成C数组,通过fs模块读取。你写的index.html如果没被转换进固件,或者转换后路径不对,浏览器拿到404或者白屏。
这三层是递进关系。我见过很多朋友卡在第二层和第三层之间,最典型的就是改了一堆网页代码,结果发现固件里跑的始终是旧的index.html,折腾半天才发现是fsdata.c没重新生成。
这篇文章顺序也是按这三层走的:先讲协议栈配置和内存参数,再讲HTTPD怎么启用,最后讲网页资源怎么打包、怎么通过SSI和CGI让网页和MCU交互。
2. 硬件和时钟:别在物理层翻车
2.1 板子与PHY芯片怎么选
我用的是STM32F407VET6,板载一颗LAN8720A PHY芯片,支持RMII和MII两种模式,RMII下引脚占用少,GPIO紧张的项目基本都选它。F407自带的MAC是支持10/100M以太网的,所以外部只需要PHY和网络变压器。
注意PHY的地址问题。LAN8720A通常通过外围电路把地址配置为0,也就是PHY address = 0x00。但有些板子或者PHY芯片会用0x01、0x1F这样的地址,你在CubeMX里配置的PHY地址必须和硬件一致,否则MDIO读写不到PHY寄存器,以太网链路直接起不来。
这件事怎么确认?最简单的方法是看原理图PHYAD0引脚的上下拉,也可以看官方例程里的默认值。如果你不确定,直接在ethernetif.c的low_level_init里,把HAL_ETH_Init之前的那句phyaddr = /* 根据硬件修改 */改成对应值,然后用串口打印出来验证。
另外还有一个坑:CubeMX的以太网中间件里,PHY芯片型号一般提供LAN8742、DP83848这些选项,不一定有LAN8720A。如果选不到LAN8720A,选LAN8742通常也能用,因为多数寄存器行为是兼容的。但底层驱动里PHY的HAL_ETH_ReadPHYRegister、HAL_ETH_WritePHYRegister还是要确认一下,严格说不同PHY的中断和状态寄存器位定义有差异。
2.2 RMII时钟到底谁来提供
RMII模式下,所有PHY都需要一个50MHz的REF_CLK时钟。这个50MHz从哪里来,不同开发板设计差别很大,这是以太网移植里最容易翻车的地方。
以常见的LAN8720A方案为例,很多核心板上是给PHY接一颗25MHz晶振,由PHY内部PLL倍频出50MHz,然后通过CLKOUT引脚输出给STM32F407的PA1(ETH_RMII_REF_CLK)。这种情况下,STM32的PA1是输入,你不需要配置MCO1输出。
但另一些设计正好反过来:PHY的REF_CLK由STM32的PA8(MCO1)输出50MHz直接供给,PA1同样是输入。如果你开了MCO1,而板子其实不需要,或者反过来,ETH的RMII时钟就会不稳,轻则ping不通,重则工作时不时掉线。
我的建议是:拿到板子先看原理图,确认PA1上50MHz信号是来自PHY还是来自MCU。如果来自PHY,CubeMX里不要开MCO1;如果来自MCU,记得在时钟树里把MCO1配成PLL P=2的50MHz输出。
这块我曾经踩过一次,当时为了省事把MCO1打开,结果网络时通时断,查了两天才发现PHY自己已经在送时钟,两个时钟源互相干扰。后来把MCO1关掉就稳定了。
2.3 CubeMX里常用的配置项
新建工程选好芯片之后,进入Pinout & Configuration,我基本会检查这几个位置:
- Connectivity > ETH:勾选ETH激活,模式选RMII,PHY Address按实际改,如果是LAN8720A就写0。
- 时钟树:先把HCLK拉满到168MHz,ETH的APB2时钟和RMII参考时钟会自动分配,不用手动算。
- Middleware > LWIP:是否启用DHCP、IP地址、子网掩码、网关都在这里设置;HTTPD的开关也在这里。
- Middleware > FREERTOS:如果你准备用RTOS,把lwIP的No Sys选项关掉,让协议栈跑在专门的线程里。
生成代码后,在lwip.c的MX_LWIP_Init里能看到类似这样的配置:
IP4_ADDR(&ipaddr, 192, 168, 1, 10); IP4_ADDR(&netmask, 255, 255, 255, 0); IP4_ADDR(&gw, 192, 168, 1, 1);这里就是你板子的静态IP所在。调试阶段我习惯先固定IP,等网络功能和业务逻辑都跑通,再切回DHCP。
还有ETH中断优先级。在FreeRTOS环境下,ETH_IRQHandler里会有从ISR调用的操作,优先级数值必须不小于configMAX_SYSCALL_INTERRUPT_PRIORITY,否则FreeRTOS会断言。CubeMX默认生成的优先级可能是5或7,我一般明确设为5,能调用FreeRTOS API,也不会被其他高优先级中断频繁打断。
3. lwIP内存与协议栈配置
3.1 lwIP的内存模型:池加堆
lwIP的内存管理分两块:内存池(memp)和内存堆(mem)。池子用于固定大小的结构体,比如pcb、netif、pbuf头、UDP/TCP控制块;堆用于可变长度分配,比如TCP发送缓冲、协议头拼接区。
理解这个模型对调节参数很重要。你改了某个宏,实际影响的是池子数量还是堆大小,得有概念。
F407内部有192KB SRAM,其中64KB是CCM内存,位于0x10000000地址段。这个CCM内存CPU能直接访问,但DMA控制器访问不到。以太网的DMA描述符、DMA数据缓冲区绝对不能用CCM内存,否则网卡收发包直接异常,表现就是收到数据全是乱的,或者一次收发就HardFault。
所以如果你的FreeRTOS堆分配在主RAM里,ETH驱动里动态分配DMA缓冲区一般没问题;如果你为了省主RAM把部分缓冲区放到了CCM,那就要小心了。
3.2 核心配置项解析
CubeMX生成的lwipopts.h里,有一堆宏。很多人看见就头大,其实真正影响HTTPD稳定性的不多,我整理了一张表:
| 配置项 | 控制内容 | 常用值 | 说明 |
|---|---|---|---|
| MEM_SIZE | lwIP堆大小,单位字节 | 1600~40960 | TCP报文组包、协议头处理用到,设太小会导致内存分配失败 |
| PBUF_POOL_SIZE | pbuf池数量 | 8~32 | 每个池大小默认约1.5KB,用于接收缓冲区,数量不够会丢包 |
| MEMP_NUM_TCP_PCB | TCP控制块数量 | 16~32 | HTTPD每开一个TCP连接占一个PCB,太小则并发连接上不去 |
| TCP_MSS | 最大报文段长度 | 1460 | 和网卡MTU对应,一般不要改 |
| TCP_WND | TCP接收窗口 | 4*TCP_MSS或更大 | 窗口越大吞吐越好,但内存消耗也大 |
| TCP_SND_BUF | TCP发送缓冲 | 8*TCP_MSS | 发送大数据时需要注意,太小则大文件传输会很慢 |
| LWIP_HTTPD | 启用HTTPD | 1 | CubeMX里勾选后会自动定义为1 |
| LWIP_HTTPD_CGI | 启用CGI | 1 | 网页动态动作靠它 |
| LWIP_HTTPD_SSI | 启用SSI | 1 | 网页动态内容插入靠它 |
我手头这块F407,HTTPD场景下PBUF_POOL_SIZE配16,MEMP_NUM_TCP_PCB配20,TCP_WND配8TCP_MSS,TCP_SND_BUF配8TCP_MSS,跑起来还算稳。
有一点要提醒:这些参数不是越大越好。F407总共就192KB RAM,你还要给FreeRTOS任务栈、应用程序变量留空间。以前我图省事把PBUF_POOL_SIZE加到64,结果系统起来后RTOS堆差点不够,程序跑一会儿就分配失败。后来老老实实回调,按实际并发需求来。
3.3 裸机还是RTOS,影响很大
lwIP本身设计上支持NO_SYS和带OS两种模式。NO_SYS模式下,整个协议栈没有独立线程,由你在主循环里不断调用ethernetif_input和tcpip_thread相关处理流程,比较适合简单应用。
但lwIP自带的HTTPD,默认实现是基于netconn API的,裸机不配合轮询很难搞,而且HTTPD本身要监听端口、要维护多个连接状态,没有独立线程会非常别扭。所以我实际工程里都选择给lwIP配一个RTOS,CubeMX生成代码时配套使用FreeRTOS。
配置方式是:在CubeMX的LWIP配置里,把No Sys设置为false,然后确保FreeRTOS被加入工程。生成代码后你会发现,lwIP会自动创建一个tcpip_thread,网络协议栈的核心处理都在这个线程里跑。你的应用代码只要负责调API就行,不用操心逻辑和中断的耦合。
tcpip_thread的栈大小建议给到2048以上。如果网页功能多、CGI处理复杂、调试打印多,2048都可能不够。我曾经遇到SD卡日志加上HTTPD后,tcpip_thread栈被顶爆,程序不定时挂掉,用FreeRTOS的栈检测钩子才发现栈使用率已经超过90%。后来调到4096才稳定。
4. 移植步骤与编译全过程实录
4.1 从CubeMX生成工程到第一个Ping
整体步骤如下:
- 新建STM32F407VET6工程,配置RCC外部晶振为HSE,调试口SWD。
- 时钟树里把系统时钟配到168MHz。
- Connectivity > ETH,启用ETH,模式选RMII。
- Middleware > LWIP,启用,IP参数先填静态IP,No Sys保持默认或按RTOS设置。
- 在LWIP的“Application / HTTPD”里把HTTPD打勾,同时把CGI和SSI的选项打开。
- Middleware > FREERTOS,确认CMSIS_V2版本,任务列表里能看到tcpip_thread。
- 生成代码,编译烧录。
烧录后,把网线插到路由器或交换机,PC设置成同一网段的固定IP(比如192.168.1.2),然后ping 192.168.1.10。
如果ping不通,先检查三处:PHY地址对不对,RMII时钟来源是不是和硬件一致,ETH中断优先级有没有被FreeRTOS卡住。
我调试时习惯在MX_LWIP_Init末尾加一句串口打印,确认lwIP初始化没挂:
printf("LWIP init done, IP:%s\r\n", ipaddr_ntoa((const ip_addr_t *)&ipaddr));如果这句都打不出来,说明前面ETH初始化阶段就卡死了,优先查PHY和时钟。
4.2 需要手动改的几个关键点
CubeMX生成代码后,建议做以下几件事:
- 在main.c里确认MX_LWIP_Init在MX_FREERTOS_Init之前调用。如果顺序反了,lwIP依赖的RTOS组件还没准备好,初始化会失败。
- 给串口重定向加上printf支持,后面调试网页请求、打印HTTP状态码都靠它。
- 把LED、GPIO等控制引脚初始化放好,后面CGI控制用。
- 修改lwip.c中的IP配置为实际IP。
如果你的板子PHY地址不是0,还要在ethernetif.c里改phyaddr = 0为实际值。很多CubeMX版本的生成代码里,这个值是通过#define PHY_ADDRESS 0控制的。
4.3 编译过程中常见的链接错误
移植时最讨厌的是编译报错。根据不同CubeMX版本,常遇到这几个:
- 提示找不到
lwip/apps/httpd.h:说明HTTPD的源码没被包含进工程,检查一下middleware组件是否完整。 - 提示
LWIP_RAND未定义:lwIP 2.1以上版本需要你提供随机数函数,在lwipopts.h里加上#define LWIP_RAND() ((u32_t)rand()),并在工程里包含stdlib.h。 - 提示
tcpip_thread相关定义冲突:一般是No Sys选项和RTOS配置不一致,确认lwIP配置里No Sys=false。 - 提示
httpd_cgi_ssi相关函数重复定义:CubeMX生成的文件里自带一个模板httpd_cgi_ssi.c,如果你又手动添加了一份,就会冲突。只保留一个。
每次编译完,我建议先用串口把启动日志打出来,确认lwIP版本、IP地址、HTTPD初始化是否执行到了。日志系统越早做,调试越省事。
5. HTTPD服务器搭建与网页分发
5.1 HTTPD在工程里的位置和作用
lwIP的HTTPD组件由几部分构成:
- httpd.c:HTTP协议解析和响应逻辑
- fs.c:文件系统抽象层,给httpd提供文件读取接口
- fsdata.c:静态文件系统的数据源,所有网页内容以C数组形式存在这里
- httpd_cgi_ssi.c:CGI和SSI的处理函数,这是你主要修改的文件
HTTPD启动后,会在80端口等待HTTP请求。浏览器发起TCP连接,HTTPD解析请求行,比如GET /index.html HTTP/1.1,然后通过fs模块找到对应的文件数据,以HTTP响应包的形式返回给浏览器。
由于lwIP默认不依赖外部文件系统,它把网页资源全部编译进了固件。你改一个网页文件,需要重新生成fsdata.c,然后重新编译整个固件,这点和PC上改网页文件即时生效完全不同。
5.2 网页文件如何打包进固件
先建一个目录,比如叫fs,把你的index.html、style.css、favicon.ico这些都放进去。然后使用lwIP源码包里的makefsdata工具,把fs目录打包成一个fsdata.c。
makefsdata的使用方式,我常用的是Windows命令行:
makefsdata.exe fs运行后会在当前目录生成一个fsdata.c。把这个文件替换到工程原有的fsdata.c位置,重新编译即可。
如果你想验证生成结果是否正确,可以打开fsdata.c看一眼,里面是所有文件内容按字节存放的数组,以及fsdata_file结构体数组。
这中间有几个容易踩的细节:
- index.html这个名字不要改,HTTPD默认找的就是index.html。如果你放的是myPage.html,浏览器访问根路径会404。
- 文件名大小写敏感。lwIP的fs模块匹配路径时是区分大小写的,你生成的文件叫INDEX.HTML,浏览器请求index.html就找不到。
- 文件大小不要超过FS_MAX_PAGE_LENGTH。这个宏在lwipopts.h里可以改,默认可能只有128或者更大,如果你页面很大,要记得调大。
- 网页里引用资源时路径要完整。
/style.css和style.css在lwIP的fs里是有区别的,外部CSS/JS引用容易踩这个坑。
每次重新生成fsdata.c后,最好串口打印一下fsdata_file里文件个数和大小,确认真实生效了。我发现很多时候浏览器打不开页面,是因为固件里还是旧的空的fsdata.c。
5.3 用SSI和CGI让网页操作MCU
静态网页只能看,不能动。真正和MCU交互,靠的是HTTPD里的SSI和CGI。
SSI的作用是在网页里插入一段动态内容。比如网页里写一个<!--#led_state-->占位符,浏览器请求这个页面时,HTTPD解析到这个tag,会调用你实现的SSI处理函数,把返回字符串替换到页面对应位置。这样就能实时显示LED当前状态、温度、电压等信息。
CGI的作用是处理带动作的请求。比如浏览器访问/led.cgi?led=1,HTTPD识别到这个CGI路径后,调用对应的CGI处理函数,你在函数里去做实际的GPIO操作,然后返回一个页面让浏览器跳转。这样用户点击按钮就能控制MCU。
CubeMX生成的工程里,httpd_cgi_ssi.c里通常已经有一个模板。下面是我在一个项目里实际用的写法,lwIP版本是2.1.x。如果你的版本API有出入,以你的头文件声明为准。
先看SSI部分:
#include "lwip/apps/httpd.h" #include "lwip/def.h" static u16_t ssi_led_state(int iIndex, char *pcInsert, int iInsertLen) { if (iIndex == 0) { if (LED_STATE) { snprintf(pcInsert, iInsertLen, "ON"); } else { snprintf(pcInsert, iInsertLen, "OFF"); } } return (u16_t)strlen(pcInsert); } static const char *ssi_tags[] = { "led_state" }; void httpd_cgi_ssi_init(void) { http_set_ssi_handler(ssi_led_state, ssi_tags, LWIP_ARRAYSIZE(ssi_tags)); }再看CGI部分:
static const char *cgi_led_control(int iIndex, int iNumParams, char *pcParam[], char *pcValue[]) { for (int i = 0; i < iNumParams; i++) { if (strcmp(pcParam[i], "led") == 0) { if (strcmp(pcValue[i], "1") == 0) { LED_ON(); } else { LED_OFF(); } } } return "/index.html"; } static const tCGI cgi_handlers[] = { {"/led.cgi", cgi_led_control} }; void httpd_cgi_ssi_init(void) { http_set_ssi_handler(ssi_led_state, ssi_tags, LWIP_ARRAYSIZE(ssi_tags)); http_set_cgi_handlers(cgi_handlers, LWIP_ARRAYSIZE(cgi_handlers)); }这里有几个注意点:
- HTTPD调用CGI时,URL后的参数列表是动态解析的,
pcParam和pcValue分别存参数名和值,iNumParams是参数个数。 - CGI处理函数返回的字符串,是要跳转到的页面路径。一般返回
/index.html,让页面刷新回主页。 - SSI的tags数组顺序要和SSI handler里的iIndex对应。第0个tag就是iIndex为0的处理函数。
snprintf写插入内容时,注意iInsertLen是缓冲区大小,别写越界。
如果lwIP版本比较老,可能不是http_set_ssi_handler这种方式,而是直接在httpd_cgi_ssi.c里定义一个handler数组,通过宏配置调用。不过逻辑是一样的,你把文件和页面里的tag对应上就好。
5.4 一个可复用的LED控制页面示例
网页部分,我在fs目录下放了index.html,内容大概这样:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>STM32F407 Web Control</title> </head> <body> <h1>STM32F407 控制面板</h1> <p>LED 当前状态:<!--#led_state--></p> <a href="/led.cgi?led=1"><button>打开LED</button></a> <a href="/led.cgi?led=0"><button>关闭LED</button></a> </body> </html>烧录后,浏览器访问开发板IP,页面会显示一个实时LED状态和两个按钮。点击“打开LED”,请求发送到/led.cgi?led=1,CGI函数把GPIO拉高,返回index.html,浏览器刷新页面,SSI标签被替换成“ON”。
如果打开页面发现<!--#led_state-->原样显示,说明SSI没生效,先查LWIP_HTTPD_SSI宏是否打开,再查tag名字是不是完全一致。
如果点按钮没反应,先把URL放到浏览器地址栏直接访问,看看开发板串口有没有CGI处理日志。没有日志,说明CGI表里的路径没匹配到;有日志但LED没动,查GPIO初始化。
6. 常见问题与排查速查表
6.1 ping不通类
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全ping不通 | RMII 50MHz时钟异常 | 确认PHY的CLKOUT是否到PA1,或MCO1是否按需配置 |
| 完全ping不通 | PHY地址不对 | 读PHY寄存器,确认MDIO通信正常 |
| 完全ping不通 | 网线或路由器问题 | 换一根网线,确认开发板网口LED亮 |
| ping时通时断 | 时钟源冲突或干扰 | 关掉多余的MCO输出,改短时钟走线 |
| ping第一次通后面断 | ETH中断优先级和FreeRTOS冲突 | 检查ETH中断优先级数值是否满足FreeRTOS条件 |
| ping通但丢包 | DMA描述符数量或pbuf池太小 | 增加PBUF_POOL_SIZE,检查ETH描述符数量 |
排查ping问题,我建议把eth_link状态打印出来,在ethernetif.c里读PHY基本状态寄存器,确认链路是up的。链路都没up,后面全白搭。
6.2 能ping通但网页打不开
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 浏览器一直转圈 | HTTPD没启动 | 确认LWIP_HTTPD宏为1,httpd_init被调用 |
| 返回404 | fsdata里没有对应文件 | 用makefsdata重新生成fsdata.c,检查文件名 |
| 返回200但白屏 | fsdata里index.html是旧的 | 确认新的fsdata.c参与编译,清浏览器缓存 |
| 能打开但显示tag原样 | SSI宏或handler没生效 | 确认LWIP_HTTPD_SSI为1,tag名完全一致 |
| 按钮点击无反应 | CGI路径不匹配或参数不对 | 浏览器手动访问CGI URL,看串口日志 |
这里特别提醒一句:网上搜“httpd syntax error”出来的基本都是Apache的报错,比如/applications/phpstudy/extensions/apache2之类的,那是PC上Web服务器配置文件语法错误,跟单片机lwIP的HTTPD没有任何关系。lwIP的HTTPD是纯C代码静态编译,不存在配置文件语法错误这种说法。你要是因为这个关键词搜到一堆无关内容,不要被带到沟里去。
6.3 运行中不稳定或随机挂掉
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 运行几分钟后假死 | tcpip_thread栈溢出 | 增大tcpip_thread栈,开启栈检测钩子 |
| 不定时HardFault | DMA缓冲区放在CCM | 确认ETH缓冲区在普通SRAM |
| 高并发访问挂机 | MEMP_NUM_TCP_PCB不足 | 增大TCP控制块数量 |
| HTTPD返回内容截断 | FS_MAX_PAGE_LENGTH太小 | 调大FS_MAX_PAGE_LENGTH |
| 大文件传输慢或失败 | TCP_SND_BUF太小 | 增大TCP_SND_BUF,配合TCP_WND调整 |
排查运行时问题,最好的工具是FreeRTOS的任务栈统计功能和lwIP自带的lwip_stats。前者能查任务栈水位,后者能看pbuf、pcb等运行时的内存峰值。
7. 从能跑通到能用:几个进阶建议
7.1 性能和稳定性调优
前面把基本功能跑通了,下一步就是让它在实际环境里更稳。
网络这块,我一般会做这几件事:
- 增加网线热插拔检测。ethernetif.c里有个
link_timer轮询,通过PHY状态寄存器检测链路up/down,链路恢复后主动调用netif_set_link_up,避免拔了网线再插上网络恢复不了。 - 把TCP参数稍微调大。带HTTPD的设备经常同时有手机和电脑访问,
TCP_WND和TCP_SND_BUF至少为8*TCP_MSS,不然并发性能很拉胯。 - 增加看门狗和状态上报。网络任务挂了,至少要能通过串口或LED看出来,然后自动重启网卡。
- 对CGI参数做长度和安全校验。浏览器是可控的,但你不能假设只有正常页面会发请求。参数缓冲区要按实际长度比较,不要用超长的value。
如果你把HTTPD和其他业务放一起,比如SD卡日志、传感器采集,最好把业务逻辑和网络逻辑分成两个任务,用队列或信号量通信。别在CGI handler里做耗时的测量或存储操作,否则网页响应会卡住。
7.2 扩展方向:日志存储与远程设备管理
HTTPD跑通后,能玩的方向就多了。最常见的扩展是把设备运行状态做成网页实时展示,比如CPU负载、温度、网络流量统计。
另一个和热词“基于stm32f407的日志存储记录方法”相关的方向,是把运行日志远程化。做法大致是:MCU把日志写入SD卡上的文件,HTTPD提供/download.cgi?file=xxx接口,用户通过浏览器下载日志。也可以直接用SSI把最近几条日志实时渲染到网页上,这样不用连串口就能看设备运行情况。
做日志时要注意,HTTPD基于netconn,如果你在CGI handler里直接操作SD卡,耗时可能会让HTTP连接超时。建议把日志查询做成异步,CGI只负责启动一个查询任务,任务完成后再通过SSI回显结果。
还有一点经验,网页里加一个“重启设备”的CGI按钮,看起来简单,但实际项目里特别有用。远程设备出问题时,能通过网页软复位,省去跑现场。
最后分享一个我的调试习惯:每个新移植的HTTPD工程,我都会先做一个只有一行的index.html,内容只写“HTTPD OK”,然后把CGI和SSI一个个加到实际页面里。这样每一步都有明确的验证点,排查问题范围小,心理压力也小。你按这个节奏来,lwIP移植和HTTPD搭建这件事,基本不会出大乱子。