先交代一下背景。前面两篇我们把 FreeRTOS 和 lwIP 的 TCP 基础通信跑通了,板子能通过网线上网、能和 PC 端工具收发数据。这一篇做的是一件更“实用”的事——在 lwIP 上把自带的 httpd 服务组件用起来,让浏览器直接访问开发板,看到网页、读到设备状态,为后面真正的 IAP 网页升级、网页改配置做铺垫。
老实说,很多人看到“网页访问开发板”第一反应就是“这不就是个嵌入式 Web Server 嘛”,但真上手做会发现细节非常多:lwIP 的 httpd 到底是干嘛的、它的文件系统怎么挂、网页资源怎么变成 C 数组、怎么在 FreeRTOS 里调度,还有那些 SSI、CGI 机制到底怎么用。这篇就从原理到实操完整走一遍,适合已经跑通 lwIP TCP 通信、想把设备做成“可以被网页管理”的开发者参考。
1. 选型思路:为什么直接用 lwIP 自带 httpd,而不是裸写 Socket
1.1 方案对比:自研 HTTP Server 的成本与风险
在做网页访问之前,第一步必须想清楚:你是打算自己从零撸一个 HTTP 服务器,还是直接用 lwIP 自带的 httpd 组件?这个决定会影响后面所有开发节奏。
我自己第一次做的时候,第一反应是“HTTP 协议又不难,我自己开个 TCP socket,然后接收浏览器请求、解析 GET / POST、回一个 HTML 响应不就行了?”确实,对极简单的场景确实可以。但你会发现,浏览器的行为比你想象中“挑剔”得多。它可能会发条件请求(If-Modified-Since)、会发起多个并发 TCP 连接来加载页面上的 CSS/JS/图片资源、会要求正确的 Content-Type、还会因为响应头格式不规范而直接白屏。这些细节如果你一个个去调,开发周期会被拉得很长。
对比下来,lwIP 官方自带 httpd 组件有几个明显的优势:第一,它是专门为嵌入式场景设计的,内存占用可控,跑在 MCU 上没有多余负担;第二,它天然适配 lwIP 的协议栈,不用自己做 socket 和 TCP 状态机的复杂交互;第三,它自带了文件系统抽象层,把网页打包成 C 数组就能直接用,省去了外部 Flash 文件系统的挂载工作;第四,内置的 SSI(Server Side Include)和 CGI(Common Gateway Interface)机制可以很方便地实现动态数据和设备控制。
当然,用自带组件也有付出的代价:lwIP httpd 的代码风格比较老派,注释不算多,宏配置也非常多,刚接触很容易在配置阶段就被绕晕。但从长期维护和稳定性角度来看,这个代价是值得的。
1.2 lwIP httpd 的能力边界:它到底能做什么
先搞清楚 httpd 能做什么、不能做什么,这样后续做功能规划才不会过度设计。
lwIP 的 httpd(在 2.x 版本中由旧版 httpd 重构而来)本质上是一个运行在 TCP 协议之上的 HTTP/1.1 服务器,支持静态文件服务、SSI 动态标签替换、CGI 回调处理三大核心能力:
- 静态文件服务:比如 index.html、style.css、main.js、图片资源,这些文件被打包成 C 数组,httpd 收到请求后从“文件系统”中查找对应文件并返回。
- SSI 动态标签替换:HTML 页面里可以嵌入
<!--#tag-->这种格式的标签,httpd 在返回页面时扫描标签,调用你注册的回调函数,把实际数据填进去。比如页面写<!--#heap-->,设备端就把当前内存使用量替换进去,浏览器看到的就是实时数值。 - CGI 处理:处理 URL 中的查询参数或者表单 POST 请求。比如网页上有一个“重启设备”按钮,点击后请求
reboot.cgi?confirm=1,httpd 检测到.cgi后缀就调用对应的处理函数,执行重启逻辑。
那它不能做什么呢?要特别注意:lwIP 原生 httpd 不支持文件上传(比如直接 POST 一个二进制固件包然后保存到 Flash),不支持 HTTPS/TLS(除非你用 altcp 版本配合 mbedTLS),也没有高并发处理能力。在正常情况下,同时有几个浏览器客户端访问是没问题的,但如果负载太重,内存可能被吃掉一大块。
这意味着什么?意味着你如果想要做“网页升级固件”,不能完全指望 httpd 自带的机制一步到位。常见的做法是:用 httpd 提供操作页面和升级入口页面,固件数据本身通过自定义的 socket 服务、或者扩展 HTTP POST 处理逻辑来接收。这个我们会在后面的篇章展开,这篇先把 httpd 跑起来。
2. 环境准备与关键配置:lwIP httpd 上手的三个隐藏门槛
2.1 硬件与基础环境确认
开始动手前,先确认你的环境具备以下条件,否则后面排查问题会很痛苦:
- 开发板已经能稳定运行 lwIP 协议栈,
ping通 PC 或者能通过 TCP socket 收发数据。这是底线,如果这一步还不稳,先回去把 MAC/PHY/时钟/中断优先级这些查一遍。 - 有一块可用的空闲 Flash 空间,至少几 KB 到几十 KB。网页资源最终会以 C 数组形式存在 Flash 里,页面越多、越精美,占用的空间越大。
- 操作系统层面已经跑通 FreeRTOS,并且 lwIP 的 tcpip_thread 和 FreeRTOS 的任务调度正常。httpd 本身可以在非 OS 环境下运行,但在 FreeRTOS 环境里它依赖操作系统的线程调度和信号量机制,这部分如果没配对会直接影响网络吞吐。
在硬件上,我实测下来,STM32F407 或 F429 这类带以太网 MAC 的芯片跑 httpd 绰绰有余;如果是 F103 系列外加 SPI 接口的 ENC28J60 之类的网卡芯片,性能会紧张一些,但小页面访问也够用。
2.2 lwipopts.h 中的关键宏开关
lwIP 的组件都是“默认关闭”的,httpd 也不例外。要让 httpd 生效,必须在lwipopts.h或lwip/opt.h中打开以下宏:
#define LWIP_HTTPD 1 #define LWIP_HTTPD_CGI 1 #define LWIP_HTTPD_SSI 1 #define LWIP_HTTPD_DYNAMIC_HEADERS 1 #define LWIP_HTTPD_SSI_MULTIPART 1 // 可选这几个宏的含义,逐个说清楚:
LWIP_HTTPD是总开关,必须设 1,否则整个 httpd 组件不会参与编译。LWIP_HTTPD_CGI开启 CGI 支持,建议开发初期就打开,因为哪怕你现在用不到,后面做表单控制和交互也会用到,先开着省得后头反复改配置。LWIP_HTTPD_SSI开启 SSI 支持,如前面所说,这是实现动态数据的关键,想实时显示系统信息就必须开。LWIP_HTTPD_DYNAMIC_HEADERS允许动态定制 HTTP 响应头,可以在响应里加 Cache-Control、Connection 等字段,能解决一些浏览器缓存导致的“怪问题”,建议一起打开。
如果你是 lwIP 2.1.x 或更新的版本,可能还会遇到一个更有“迷惑性”的宏:LWIP_HTTPD_SUPPORT_HTTP_0_9。这个宏开着的时候,httpd 会同时支持非常古老的 HTTP/0.9 协议格式请求,这在某些嵌入式场景有用,但也会引入一些意想不到的兼容问题。大多数情况下保持默认即可,除非我明确知道自己的浏览器环境比较老旧。
除了 httpd 本身的宏,lwIP 的 TCP 参数也需要跟着调一调。特别是并发连接数,lwIP 默认 TCP 最大连接数很小,如果你要用浏览器访问,同时打开多个标签页或者页面引用了多个子资源,就可能触发“连接不够用”的问题。建议在 lwipopts.h 里确认:
#define MEMP_NUM_TCP_PCB (10) // 并发 TCP 控制块数 #define MEMP_NUM_NETCONN (10) // 网络连接结构体数量 #define TCP_SND_BUF (2 * TCP_MSS) #define TCP_WND (4 * TCP_MSS)具体数值根据内存大小微调,但MEMP_NUM_TCP_PCB至少别低于 5,不然浏览器访问稍微频繁一点就会报连接错误。
2.3 FreeRTOS 与 httpd 的主线程怎么配合
很多人在“要不要给 httpd 单独建一个 FreeRTOS 任务”这个问题上纠结。实际上,lwIP 的 httpd 组件在初始化时就会自己完成内部状态机和工作线程(当然,这取决于你用的是哪种 API 变体)。
关键是搞清楚 httpd 内部怎么调用 lwIP 的接口。老版本的 lwIP(1.x 时代)httpd 直接基于 RAW API,它会在你调用httpd_init()时注册 TCP 回调,后续所有收发都在tcpip_thread上下文中完成。而新版 lwIP(2.x)的 httpd 则默认基于netconnAPI 或socketAPI,它会创建一个独立的httpd线程(在线程初始化时创建),在这个线程里循环接收连接、处理请求。
因此,你在 FreeRTOS 中不需要为 httpd 单独“开一条业务任务”。httpd 的线程由sys_thread_new创建,由 FreeRTOS 统一调度。你只需要在系统启动的过程中,在 lwIP 已经初始化完成之后,调用一次:
httpd_init();这里有个特别重要的顺序问题。httpd_init()内部会尝试创建 netconn 和线程,如果 lwIP 的tcpip_init()还没完成就调用 httpd_init,会导致初始化失败,运行时页面全部打不开。最稳妥的做法是在一个“网络就绪”任务里调用 httpd_init,或者在 main 函数中确保 lwIP 初始化完成后立刻调用。
我自己踩过的一个坑是:在 FreeRTOS 初始化所有任务之前就调用了httpd_init(),结果sys_thread_new在调度器还没启动的时候创建线程,直接断言失败,整个系统卡死。后来改成在 main 函数完成了 lwIP 初始化、操作系统调度器启动后的首个网络任务里调用 httpd_init,才恢复正常。这个细节值得记一下,尤其是从 CubeMX 工程模板直接改代码的人,很容易忽略。
3. 核心实操:三步让开发板的网页跑起来
3.1 第一步:做好网页资源并转换成 C 数组
httpd 不吃独立网页文件(至少默认配置下不直接吃),它需要你把网页文件打包成 C 语言数组的形式。这个转换工作,lwIP 官方提供了一款命令行工具:makefsdata。
先说我自己的网页资源组织习惯。一般我会在工程目录下新建一个http_web文件夹,里面放好index.html、style.css、main.js、favicon.ico这些文件。开发调试阶段我甚至会在前端页面里加上一些“假数据”,保证静态页面在浏览器里能正常展示,然后再用 SSI 标签去替换动态内容。
这里有个提高效率的小心得:做网页资源时先本地调试,再打包嵌入,不要每次改一行 HTML 就重新生成 arrays、重新编译固件、重新烧录,那效率太低了。我一般是先写一个完整静态页面,用浏览器本地打开观察样式和 JS 交互,全部没问题了再用 makefsdata 打包。
makefsdata工具在 lwIP 的 contrib 目录下,在 Windows 上,我通常这样用:
makefsdata.exe -s ..\http_web -f .\fsdata.c -i .\fsdata.h参数说明:
-s指定网页资源目录-f指定输出 C 文件路径-i指定输出头文件路径
执行成功后,工程里会多出fsdata.c和fsdata.h,httpd 通过这两个文件里定义的静态数组来“读取”网页文件。然后把这俩文件加入编译工程即可。
如果你对 makefsdata 生成的代码结构好奇,可以打开 fsdata.c 看一眼,里面会是一长串static const unsigned char data_..._html[] = {0x3c, 0x68, 0x74, ...}这样的数组,以及一个文件索引结构体数组。它本质上就是把字节流硬编码进程序里,构成了一个“简易文件系统”。
3.2 第二步:把 httpd 初始化代码嵌进系统
文件准备好之后,就是初始化了。前面提到过,httpd 初始化就一句httpd_init()。但放在哪里、什么时候调用,需要讲究一下。
我在 FreeRTOS 项目里的推荐顺序如下:
int main(void) { // ... 硬件初始化(时钟、GPIO、以太网 MAC/PHY 等) MCU_Init(); // 1. 初始化 lwIP 协议栈 tcpip_init(NULL, NULL); // 2. 初始化 netif、DHCP 或静态 IP netif_add(&g_netif, &ipaddr, &netmask, &gw, NULL, ethernetif_init, tcpip_input); netif_set_default(&g_netif); netif_set_up(&g_netif); // 3. 等待网络就绪(这里根据情况轮询 DHCP 状态,或固定延时) while (!netif_is_up(&g_netif)) delay_ms(10); // 4. 初始化 httpd httpd_init(); // 5. 创建 FreeRTOS 任务,启动调度器 osKernelStart(); while (1); }注意这里有一个细节:在httpd_init()之前,lwIP 必须已经完成了tcpip_init,而且 netif 至少要已经注册好。有些 mcu 平台上的移植是在tcpip_init的回调里完成网卡驱动的,所以如果你发现 httpd_init 之后页面打不开,先回头检查网络是否真的“通”了。
如果你的项目里有多个网卡或者你在用 RTOS 的启动线程做初始化,那么 httpd_init 放在启动线程里也完全可以,只要满足“lwIP 先初始化、httpd 后初始化”的顺序即可。
3.3 第三步:编译烧录并验证页面访问
把fsdata.c加入工程,加上httpd_init()调用,编译烧录。在浏览器地址栏输入开发板的 IP 地址,正常情况下就能看到你写的网页了。
这里值得提一个很容易被忽略的验证技巧:不要只用电脑浏览器测,还要用手机、用不同内核的浏览器(Chrome、Edge、Firefox)都测一遍。不同的浏览器对 HTTP 解析的宽容度不一样,有些时候 Chrome 能正常打开,但 Safari 白屏,那很可能是你的 HTTP 响应头字段有问题。lwIP httpd 默认返回的 Header 非常精简,在某些强制安全校验的浏览器环境下可能被拦截。
如果你的页面加载出来是空白,按 F12 打开开发者工具看 Network 面板,如果看到请求状态是“Provisional headers are shown”,基本可以断定是连接被重置或响应头不规范。
确认基本访问没问题后,再做一个简单验证——通过 PC 端 curl 命令行去看看服务返回的原始内容:
curl -v http://192.168.1.100/这条命令会把完整的 HTTP 响应头打印出来。你会看到类似:
HTTP/1.1 200 OK Content-type: text/html Content-length: 512 Connection: close如果看到这些,就说明 httpd 已经正常工作了。如果没有 200、或者 Content-length 异常,排查方向就转向 lwIP 配置和 FS 文件挂载是否正确。
3.4 进阶:用 SSI 让网页显示实时数据
静态页面能访问,只完成了 30% 的工作。嵌入式网页访问的真正价值,是能看到 MCU 上的实时状态。比如 CPU 使用率、内存剩余量、传感器采集值、设备运行时长等。在 lwIP httpd 中实现这个效果,就是靠 SSI。
SSI 的用法很简单:
第一步,在 HTML 文件中插入特殊标记:
<!DOCTYPE html> <html> <head><title>Device Status</title></head> <body> <h1>CPU Usage: <!--#cpu--></h1> <h1>Free Heap: <!--#heap--></h1> </body> </html>第二步,在 C 代码中实现 SSI 回调函数:
#include "lwip/apps/httpd.h" static u16_t ssi_handler(const char* ssi_tag_name, char* pcInsert, int iInsertLen) { if (strcmp(ssi_tag_name, "cpu") == 0) { snprintf(pcInsert, iInsertLen, "%d%%", (int)get_cpu_usage()); return (u16_t)strlen(pcInsert); } else if (strcmp(ssi_tag_name, "heap") == 0) { snprintf(pcInsert, iInsertLen, "%d bytes", (int)xPortGetFreeHeapSize()); return (u16_t)strlen(pcInsert); } return 0; }第三步,注册回调:
tSSIHandler g_ssi_handler = ssi_handler; http_set_ssi_handler(ssi_handler);这里有个大坑必须提醒:SSI 的标签名默认是大小写敏感的,而且标签名两端的空格、换行要特别注意。如果你在 HTML 里写的是<!--#cpu -->(带空格),lwIP 在解析时匹配到的 tag 名字可能是"cpu "(带尾随空格),这会导致回调函数里strcmp(ssi_tag_name, "cpu")匹配不上,页面直接显示原始标签而不是数据。排查这类问题很费眼神,建议写 HTML 时严格保持<!--#tag-->格式,不留多余空格。
另外,lwIP 对 SSI 的替换内容长度有限制,默认大约 300 字节,如果需要返回更长的动态内容,需要调大LWIP_HTTPD_MAX_TAG_INSERT_LEN这个宏。
对于实时性要求高的数据(比如波形、频率刷新的传感器值),SSI 只能在页面刷新时更新数据(因为 SSI 是在响应时替换的)。如果要做页面内局部实时刷新,需要用 JavaScript 定时请求一个动态接口,这就要用 CGI 来做了。
3.5 进阶:用 CGI 响应网页操作
CGI 是让网页“操控”设备的关键机制。举个例子:页面上放一个按钮,点击后设备就重启、或者切换某个 GPIO。
它的原理是:httpd 在解析 URL 时发现以.cgi结尾的对象,就调用你注册的 CGI 回调。回调解析 URL 中的查询字符串,执行对应的动作,然后返回一个“接下来的响应文件路径”,httpd 会把该文件作为响应内容发给浏览器。
注册方式:
#include "lwip/apps/httpd.h" static const char* cgi_reboot_handler(int iIndex, int iNumParams, char* pcParam[], char* pcValue[]) { if (iNumParams >= 1 && strcmp(pcParam[0], "cmd") == 0 && strcmp(pcValue[0], "reboot") == 0) { NVIC_SystemReset(); } return "/index.html"; } static const tCGI g_cgi_handlers[] = { {"/reboot.cgi", cgi_reboot_handler}, }; void web_server_init(void) { http_set_cgi_handlers(g_cgi_handlers, 1); }然后网页端按钮的请求就是:
<button onclick="location.href='/reboot.cgi?cmd=reboot'">重启设备</button>这里要注意的是 CGIndex 参数:如果你有多个 CGI handler,iIndex会标识当前命中的是哪个 handler,不要混淆。另外,函数返回的字符串必须是一个存在于文件系统里、能被 httpd 找到的页面路径,否则 HTTP 会返回 404。
CGI 处理逻辑里尽量不要做阻塞操作(比如延迟等待),因为 httpd 的线程在处理完成之前不会响应其他请求。如果需要执行较长的耗时流程,可以先把任务丢给 FreeRTOS 队列,另外起一个任务去执行,CGI 回调立即返回一个“操作已受理”的页面。
4. 常见问题与排查技巧实录
4.1 页面上不去:IP 能 ping 通,但浏览器打不开
这是我在很多项目里遇到过的高频问题,现象是开发板能 ping 通,但浏览器访问 IP 时一直在转圈或直接显示“无法访问此网站”。
先排查 httpd 有没有正常初始化。常规套路:在httpd_init()前、后各加一个串口日志,确认函数确实执行到了。然后在 httpd_init 之后加一个延时,再打印当前 netconn 状态,看看 TCP 服务是否真的监听在 80 端口。很多时候问题出在httpd_init()之前 lwIP 的 netif 还没完全就绪,导致 httpd 创建 netconn 时拿不到合法地址。
再一个常见原因是 lwipopts.h 中的LWIP_NETCONN或LWIP_SOCKET宏没有打开。新版 httpd 走 netconn API,如果LWIP_NETCONN是 0,httpd 的依赖就不完整,初始化会失败但又不一定报错,表现就是“看起来正常实际无响应”。我建议在 lwipopts.h 中把这些网络 API 都打开:
#define LWIP_NETCONN 1 #define LWIP_SOCKET 14.2 页面 404:文件系统里找不到文件
如果浏览器收到了 HTTP 状态 404,说明 httpd 是正常工作的,但在当前“文件系统”里找不到对应的文件。
排查步骤也很直接:第一步,确认你请求的页面文件确实在网页资源目录里,并且文件名完全一致,包括大小写(在嵌入式文件系统里,“index.html”和“Index.html”是不同文件)。比如我遇到过用 makefsdata 打包了index.html,但页面里 JS 请求/Index.html,结果 404。
第二步,检查 makefsdata 生成的文件名是否带了路径前缀。在 httpd 的 FS 索引中,文件名是以字符串形式存储的,比如/index.html和index.html可能不同。有些版本的工具默认在文件名前加/,有些不加。如果匹配不上,可以手动打开 fsdata.c 检查索引表里的文件名字符串,或者在注册 URL 时手动与索引中的名字保持一致。
第三步,确认LWIP_HTTPD_INDEX_PAGE宏设置是否正确。当你访问根路径/时,httpd 会去寻找默认首页。这个默认页的名字由LWIP_HTTPD_INDEX_PAGE宏控制,默认值是/index.html。如果你把首页做成了home.html,那访问根路径就会 404,必须用LWIP_HTTPD_INDEX_PAGE指定新的默认页,或者保持默认文件名别改。
4.3 SSI 标签显示出来了,但就是没被替换
这个问题的典型现象是:页面能打开,但页面上显示的是<!--#cpu-->这种原始文本,而不是替换后的实际数据。
先检查你的页面文件后缀名。lwIP httpd 默认只对特定后缀的文件执行 SSI 替换,一般由LWIP_HTTPD_SSI_EXTENSION宏控制,默认是.shtml或其他几个扩展名。如果你把页面存成了.html,SSI 可能根本不扫描。解决方式是:把动态页面命名为index.shtml,或者修改宏把.html也加入 SSI 扫描范围。
还有一种情况是注册了 tSSIHandler 但没生效。注意有些版本的 lwIP 要求你在初始化时使用http_set_ssi_handler(tSSIHandler)注册,有些版本则要求直接修改变量g_ssi_handler。这个跟你的 lwIP 版本强相关,看源码最靠谱。我遇到过一种情况是使用了新版 lwIP 的 httpd,它的 API 改成了http_set_ssi_handler(tSSIHandler, NULL)这种带额外参数的形式,注册接口不对就会导致 SSI 完全失效。
另外,如果你在回调函数里拿到的ssi_tag_name和你预期的不一致,可以在回调入口加一行串口打印,把实际收到的 tag 名打印出来看。这是个非常高效的调试手段——你可以直接观察到是否有多余空格、大小写差异等。
4.4 浏览器缓存和响应头导致的间歇性“灵异”问题
嵌入式 web 调试里最让人抓狂的是“刷新一次能打开,再刷新就打不开”“改完 HTML 内容重新烧录后浏览器还是显示旧页面”这类问题。
第一个问题是浏览器缓存。开发阶段,网页打包在固件里,每次改版都要重新烧录,如果没有禁用缓存,浏览器会优先加载缓存里的旧页面。有两个处理方式:一是在 HTML 的 head 区加 meta 禁止缓存:
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> <meta http-equiv="Pragma" content="no-cache" /> <meta http-equiv="Expires" content="0" />二是打开LWIP_HTTPD_DYNAMIC_HEADERS,在 httpd 返回的响应头里加Cache-Control: no-cache之类的字段。这样每次刷新都会向服务器请求最新内容。
第二个问题是 HTTP keep-alive 导致的连接堆积。浏览器默认会尝试复用 TCP 连接,但某些 lwIP httpd 配置下不支持(或支持得不好),如果连接复用状态机没处理好,就会出现“第一次访问正常,后面越刷越卡”的现象。可以临时关掉浏览器的 cache 和 keep-alive 做交叉验证,或者在 httpd 的配置宏中明确设置不支持连接复用,让每次请求都走全新的 TCP 连接,虽然效率低一点,但稳定性反而更好。
4.5 内存不足和性能优化思路
最后说说资源开销。httpd 跑起来以后,Flash 要存页面数据,RAM 要为每个 TCP 连接分配缓冲区。如果你的页面比较花哨、图片比较多,Flash 会非常吃紧。比如一个 50KB 的图片转成 C 数组,就得占 50KB Flash,对于 F103C8T6(64KB Flash)来说基本上一个图片就快挤爆了。
调优的方向有三个:一是页面精简,能用纯 HTML/CSS 就不用图片,能用 CSS 画的效果就别用图片素材,图标尽量用 base64 内联或者 Font Awesome 之类的矢量方案,这样可以显著减少页面体积。二是调整 lwIP 内存池大小,如果设备内存确实紧张,可以调小TCP_SND_BUF和TCP_WND,减少每个连接锁占的内存,但会影响大页面加载速度。三是加载外部资源,把大体积的 JS/CSS 放在服务器上而不是打进固件——但这样就失去了完全离线的意义,要按场景权衡。
实际项目里我一般把单页面控制在 20KB 以内,整个 web 目录控制在 50KB 以内,这样 F103 级别的小片子也能从容应对。如果必须用大资源,建议换用 Flash 更大的芯片,或者挂 SPI Flash 配合文件系统来做资源存储,这就涉及到对 httpd 文件系统做底层重定向了,是更复杂的进阶玩法,等用到时再单独写一篇。
5. 与后续 IAP 功能的衔接思路
这篇把 httpd 跑起来了,也实现了动态状态展示和按钮控制。那么回到整个系列的核心目标——网页升级固件、更新配置——还差一步最关键的工作:怎么让浏览器把固件数据传输到开发板并完成 IAP 升级。
我在前面的方案里提过,lwIP 自带 httpd 并不天然支持文件上传,但你可以借助它的 CGI 机制自己扩展。比如在页面上放一个文件选择控件,选择固件 bin 文件后用 JavaScript 读取二进制数据,把它分割成小块,通过 HTTP POST 请求依次发送给设备;设备端写一个 CGI handler 或者自定义 socket server 来接收这些数据块,写入外部 Flash 或内部 App 分区,收齐后校验完整性,然后跳转到 bootloader 执行升级。
这个链路里,httpd 承担的职责主要是“升级入口页面”和“升级状态展示”,数据传输本身则建议走出独立的 TCP 通道来做,速度和可靠性更好控制。比方说,你做升级页面的时候,可以把固件传输设计成一个纯 socket 的私有协议,由 bootloader 或者 App 阶段的后台任务负责接收。这部分涉及到的分块策略、Flash 扇区擦写、升级失败回滚机制,内容很多,也是这个系列后续要写的重头。
就我个人经验,把网页直接访问先跑通的收益非常大——它意味着你的设备不再只是一个“能收发 TCP 数据”的黑盒子,而是一个可以被浏览器可视化管理、可交互运维的联网设备。日常调试的时候,打开网页就能看状态、改参数,比每次用串口工具敲指令舒服太多了。建议你也动手把这一步打通,然后再往上传升级的方向推进。