ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LWIP HTTPD + makefsdata:嵌入式MCU网页配置实战指南

LWIP HTTPD + makefsdata:嵌入式MCU网页配置实战指南 这两年MCU卷得厉害连一个温控器都恨不得带网页配置界面。我在不少项目里把LWIP自带的HTTPD当作轻量级Web服务器用网页资源则通过makefsdata.exe打包成C数组直接烧进Flash。这条路走通之后产品调试时手机连上设备、浏览器一开就能改参数确实方便——但第一次走这条路的人十个有八个会卡在“HTML怎么变成C文件”这个环节。makefsdata.exe看着只是一个工具实际上它生成的东西能不能用取决于你对LWIP HTTPD的工作原理、文件组织、编码细节了解多少。我自己也踩过不少坑从生成的文件编译不过到网页打开404再到中文全是乱码一步步踩过来才弄明白。这篇文章就把这条链路逐段拆开讲清楚配合makefsdata.exe使用教程帮大家少走弯路。适合正在用STM32、GD32这类MCU做LWIP联网产品、想快速搞定网页配置界面的工程师也适合刚接触HTTPD的初学者。1. 先搞明白LWIP HTTPD为什么非要“C文件网页”先说个很多人没想通的问题为什么网页不能像电脑上的Nginx那样把HTML文件放到某个目录里服务器自己去读因为在MCU上没有这套东西。1.1 MCU上的Web服务器不读硬盘读内存LWIP是一个TCP/IP协议栈HTTPD是跑在它上面的一个小模块。这个HTTPD非常轻量它默认不依赖任何文件系统也不去读SD卡、Flash里的某个目录。它要的数据全部来自一个叫fsdata.c的C文件——网页的每个字节都被转成了C数组编译后变成固件里的const数据。这样设计的好处很明显没有文件系统就没有路径解析、没有磁盘IO、没有缓存管理整个HTTP服务器逻辑可以做到极简。代价就是“更新一次网页就得重新编译固件”。我经常跟人开玩笑说这相当于把网页塞进了单片机的大脑里想改一个字都得给它重新“洗脑”。很多新手会在这时候犯一个错误用各种在线工具把HTML转成C数组然后直接往工程里塞。结果要么编译不过要么浏览器打开完全不对。原因就是LWIP HTTPD期望的并不是单纯的文件字节而是一个带有HTTP响应头、文件长度、查找哈希的结构体。makefsdata.exe正是干这个的。1.2 makefsdata.exe到底做了什么makefsdata.exe是LWIP官方contrib里提供的一个命令行工具它的核心逻辑其实非常简单扫描指定目录下的所有网页文件逐字节读取每个文件转成十六进制C数组为每个文件生成一个fsdata_file结构体多个文件通过指针串成链表为每个文件生成对应的HTTP响应头包含状态行、Content-Type、Content-Length等输出到fsdata.c并定义FS_ROOT指向链表的第一个文件。如果你懂C语言文件读写完全可以自己写一个工具fopen读文件fread进缓冲区再逐字节printf成0xXX。很多人也这么干过但自己写很容易漏掉HTTP头和文件哈希这些关键细节。理解它本质上就是一个“文件读取格式化输出”的过程之后makefsdata.exe就不再神秘了遇到问题也知道该往哪个方向排查。另外提醒一句makefsdata.exe生成的数据默认是const的也就是说这些网页数据最终会被放进Flash而不是RAM。如果你拿到的某个转换工具生成的是非const数组或者你为了改网页方便故意把const去掉在RAM紧张的MCU上会非常酸爽——分分钟超内存。2. 开工前准备网页目录与lwipopts.h配置很多人拿到makefsdata.exe就直接对着网页文件夹一顿操作结果生成的文件各种不对。我建议先花5分钟把网页目录理顺再把LWIP的配置宏检查一遍后面会省很多事。2.1 网页文件应该怎么组织才不容易翻车先说目录结构。我的经验是“能平则平能少则少”。下面是我常用的一个示例C:\web\ index.html style.css app.js logo.png尽量把所有资源放在根目录不要搞五六层子目录。原因后面会详细说LWIP HTTPD对路径的处理非常原始目录层级越多越容易出幺蛾子。文件命名规则更关键全部用小写字母不要有空格不要有中文不要有特殊字符下划线可以但没必要首页必须叫index.html。CSS、JS、图片能合并就合并。很多MCU的HTTPD并发连接数非常有限一个网页如果引了10个外部资源浏览器就得建立10次TCP连接。每次握手、断开在MCU上都是有开销的页面打开慢不说还容易把连接数耗尽。所以我的建议是如果页面不大干脆把CSS和JS都内联到HTML里只保留一两个必要的外部文件。图片是大头。一个几百KB的PNG放进固件Flash直接爆掉。能用CSS画的图标就不要用图片必须用的图尽量压缩到几KB以内。记住makefsdata.exe是不压缩的HTML多大生成的C数组就多大。2.2 关键配置宏先改对再折腾LWIP HTTPD的行为基本靠lwipopts.h里的宏控制。经常有人makefsdata生成的文件没问题但网页就是打不开最后发现是宏没开对。这里列一份最基础的清单#define LWIP_TCP 1 #define LWIP_HTTPD 1 #define LWIP_HTTPD_CUSTOM_FILES 0 #define LWIP_HTTPD_DYNAMIC_HEADERS 0 #define LWIP_HTTPD_MAX_REQ_LENGTH 1024几个宏的含义LWIP_HTTPD总开关不开这个HTTPD根本不工作LWIP_HTTPD_CUSTOM_FILES如果为0HTTPD使用fsdata.c里的静态文件如果为1你需要自己实现文件读取接口比如接到LittleFS上。我们这篇文章走默认的0LWIP_HTTPD_DYNAMIC_HEADERS如果为0HTTP响应头直接用makefsdata生成好的如果为1可以动态修改响应头但需要额外写httpd_custom_headers函数LWIP_HTTPD_MAX_REQ_LENGTH请求URL和GET参数的最大长度。默认值在某些版本里只有128如果你用GET方式提交一个比较长的表单参数会被截断。我一般调到1024以上。如果你后面打算做动态数据刷新或表单提交还要提前规划这两个宏#define LWIP_HTTPD_SSI 1 #define LWIP_HTTPD_CGI 1这两个宏我放在第5章详细讲这里先记住一个原则先让静态页面跑通再上SSI和CGI。一上来全开出了问题都不知道该查哪边。3. makefsdata.exe从编译到使用完整实操这一章是很多人点进来最想看的我尽量写得能直接“抄作业”。3.1 工具从哪来源码编译 vs 现成exemakefsdata并没有一个官方发布的独立安装包它是以源码形式放在LWIP contrib仓库里的。路径是lwip-contrib/apps/httpd/makefsdata/里面有一个makefsdata.c文件你需要自己把它编译成可执行文件。Windows下最简单的编译方式装一个MinGW-w64然后gcc -o makefsdata.exe makefsdata.c如果没有MinGW用Visual Studio的开发者命令行也可以cl makefsdata.c编译好的makefsdata.exe只有几十KB放哪个目录都行。另外很多开发板的SDK里会自带编译好的版本比如ESP8266的NONOS SDK、某些STM32网络例程包里都有搜一下就能找到。还有一个常见问题老版本的makefsdata.c在Windows下编译会报gettimeofday未定义。这很正常毕竟这工具当年是在Linux环境下开发的。遇到这种情况要么换成新版本我建议直接用lwip-contrib最新release要么在源码里加一个Windows下的时间函数补丁但没必要直接换新版最省事。3.2 命令行正确姿势参数、路径和踩坑先一句话说清楚makefsdata的用法在不同版本里略有差异拿到工具后先执行makefsdata.exe -h看一下参数说明。常见用法是这样的makefsdata.exe -s C:\web -f fsdata.c这条命令的意思是把C:\web目录下的所有文件打包输出到fsdata.c。常用参数有参数作用-s dir指定网页源目录-f file指定输出的C文件名默认是fsdata.c-d dir指定输出目录-i忽略文件名大小写匹配-v输出详细日志执行后如果一切正常屏幕上会列出扫描到的文件最后生成一个fsdata.c。这里有几个我踩过的坑特意说一下路径不要带中文和空格。我一开始把网页目录放在C:\Users\张三\Desktop\网页结果工具要么找不到目录要么生成异常。后来统一改成C:\web这种纯英文路径问题消失。Windows路径里有空格时记得用引号括起来但最好还是别用。在cmd里跑别在PowerShell里折腾。你要是习惯用PowerShell可能会遇到“无法加载文件因为在此系统上禁止运行脚本”之类的报错。那个报错大多是执行策略限制脚本用的makefsdata是独立exe双击或直接在cmd里跑就行。我的建议是这环节老老实实打开cmd别跟执行策略较劲。多看一眼输出日志。有时候你写了3个文件makefsdata只扫到2个原因可能是其中一个文件在子目录里但目录为空或者文件名是中文被识别异常。跑完以后数一下文件数量对不对比你后面调试半天强。3.3 生成的fsdata.c该怎么读现在打开生成的fsdata.c内容大概长这样不同LWIP版本有差异但思路一致#include lwip/apps/fs.h #include lwip/def.h static const unsigned char data_index_html[] { 0x3c, 0x21, 0x44, 0x4f, 0x43, 0x54, 0x59, 0x50, 0x45, 0x20, 0x68, 0x74, /* ... 后面是一大串十六进制字节 */ 0x00 }; static const struct fsdata_file file_index_html[] {{ file_NULL, data_index_html, data_index_html 4, sizeof(data_index_html) - 4, 1 }}; #define FS_ROOT file_index_html这个结构体里的每个字段都不是随便定义的第一个字段指向下一个fsdata_file的指针多个文件靠它串成链表第二个字段指向完整数据区的指针数据区包含HTTP响应头和网页内容第三个字段指向实际页面内容的起始位置第四个字段页面内容长度第五个字段文件名哈希新版本用它加快文件查找。在文件末尾通常还会有#define FS_ROOT file_index_html #define fs_file_count 1如果文件多了你会看到file_index_html[]后面跟着file_style_css[]、file_app_js[]每个都指向上一个文件节点形成一条链。我建议你把这个文件从头翻一遍尤其是搜索一下自己的文件名确认每个文件都生成进去了。这一步只要30秒能避免后面一堆莫名其妙的404。3.4 工程集成替换文件与Keil编译接下来把生成的fsdata.c放到LWIP源码的src/apps/httpd/目录下替换掉原来的fsdata.c。注意原来的fsdata.c是一个空模板里面好像没有实际网页数据但你不能放两个fsdata.c同时在工程里否则编译器直接报重复定义。直接把旧文件从工程里移除或者用新文件覆盖掉它。如果你是Keil用户工程里带“号”的项只是源文件分组文件夹不是特殊编译选项。把fsdata.c拖进任意一个组里都行。我习惯单独建一个“HTTPD_FS”分组专门放网页生成的文件这样以后更新网页时容易找。编译烧录后浏览器输入设备的IP地址如果能打开页面恭喜你已经走通了最关键的一步。如果打不开或者异常别急下一章的内容基本就是为你准备的。4. 从HTML到C文件六个高频坑逐一拆解这一章是我最想写的部分因为这些坑每一个我都踩过而且很多坑在官方文档里根本找不到答案。4.1 编码与BOM乱码的头号元凶网页在自己电脑上打开一切正常烧进板子里中文全部变成“锟斤拷”这是最常见的问题。别急着怀疑LWIP先看看你的HTML文件编码。正确做法是HTML文件必须保存为UTF-8无BOM格式并且HTML头部要有meta charsetutf-8如果你用Windows记事本另存为UTF-8它会偷偷加一个BOM头文件开头几个不可见字节。这个BOM在浏览器里可能表现为页面顶部多了一个小方框或者导致HTTP解析错位。我最早用记事本保存HTMLmakefsdata生成后怎么调都是乱码后来换成VS Code右下角把编码改成“UTF-8”另存为覆盖问题立刻消失。还有个容易忽略的点makefsdata生成的HTTP响应头里Content-Type是否带了charsetutf-8。新版工具默认会带但某些老版本可能不带。如果响应头里没有charset浏览器可能按系统默认编码解析中文照样乱。这种情况下可以用makefsdata.exe -s C:\web -f fsdata.c -c text/html; charsetutf-8不过-c这个参数不是所有版本都支持建议先看-h输出。实在不行就在HTML里把meta标签写清楚浏览器一般会认。4.2 文件名大小写、子目录与404这个坑非常隐蔽。你在Windows上做网页文件名Logo.PNGHTML里写img srclogo.png双击打开页面正常——因为Windows文件系统不区分大小写。但LWIP HTTPD的文件查找是精确匹配的它不认大小写。你请求logo.png而makefsdata生成的文件名是Logo.PNG结果就是404。所以我在第2章就强调所有文件名统一小写。等出了事再去排查大小写问题就是在浪费生命。子目录的问题更阴间。LWIP HTTPD虽然能在URI里解析目录但如果你在子目录里引用资源时用了../这种相对路径往上跳十有八九会失败。HTTPD不会像Nginx那样把路径规范化成绝对路径。我的建议要么所有文件都放根目录要么子目录不要超过一层引用路径直接从根开始写比如/images/logo.png。4.3 首页命名为什么IP一开就是404LWIP HTTPD收到一个根路径请求/时默认会去查找一个叫index.html的文件。有人把自己的配置页命名为main.html访问IP直接404然后疯狂怀疑makefsdata没打包成功。解决办法很简单把首页命名成index.html。如果你确实想用别的名字可以用一个index.html当跳板meta http-equivrefresh content0;urlmain.html但我不推荐这种跳转方式多一次请求不说还容易被浏览器拦截。老老实实叫index.html最稳。4.4 HTTP头与Content-Type要么显示要么下载前面说makefsdata会为每个文件生成HTTP响应头这个头里最关键的字段就是Content-Type。它决定浏览器把内容当HTML解析、当CSS渲染、当图片显示还是当成下载附件。makefsdata有一套内置的扩展名到Content-Type的映射表类似这样扩展名Content-Type.htmltext/html.csstext/css.jsapplication/javascript.pngimage/png.jpgimage/jpeg.svgimage/svgxml.icoimage/x-icon.jsonapplication/json如果你用的扩展名不在表里比如.webp、.woff2makefsdata会按二进制流处理返回的Content-Type可能是application/octet-stream。浏览器收到这种头就可能出现“网页文件下载了而不是打开”的诡异现象。解决办法有三个尽量选用上表里的常见格式更新makefsdata源码里的映射表重新编译工具直接把资源内联到HTML里绕开这个问题。我实际项目里最后基本都走到第三种方案了图片转base64内联CSS和JS全塞进HTML。一个文件搞定所有事再也不担心Content-Type也不担心并发连接数不够。4.5 浏览器缓存改了代码页面不变这是最让人抓狂的坑改完HTML重新跑makefsdata重新烧录浏览器打开还是旧页面。你以为是烧录失败了其实多半是浏览器缓存。LWIP HTTPD返回的响应头里默认没有Cache-Control相关字段一些浏览器会根据自己的策略缓存页面。开发调试阶段我建议开发时用无痕窗口或者打开开发者工具勾选“Disable cache”在HTML里加一个版本号注释或者给引用的资源加查询参数比如link relstylesheet hrefstyle.css?v3也可以在HTML的head里加meta http-equivCache-Control contentno-store至少能减少一部分缓存问题。注意即使页面HTML本身不缓存外部CSS/JS文件也可能被缓存。所以给资源文件名加版本号是最有效的一招。4.6 固件体积与对齐别忘了这是MCUmakefsdata生成的C数组是1:1存储的不压缩。一个100KB的网页固件就会增加100KB。如果你的MCU Flash本来就不宽裕这可能是压垮骆驼的最后一根稻草。控制方法前面提过一部分这里补充几个细节移除HTML里的注释和多余空白能省一点是一点CSS和JS用工具压缩后再打包图片尽量用png压缩或转成webp实在不行缩小尺寸字体文件是大户尽量不要用自定义字体。还有一个不是体积的问题字节对齐。某些RISC-V内核或较老的ARM编译器对const数组的访问要求对齐到4字节边界。如果你发现HTTPD在传输数据时出现断言失败或内存访问异常检查一下生成的数组前面是否有足够的对齐。部分版本的makefsdata会生成类似LWIP_ALIGNED的修饰如果没生成可以通过修改makefsdata源码里输出模板增加__attribute__((aligned(4)))或者在fsdata.c里手动加。5. 进阶玩法SSI和CGI让网页真正“活”起来静态网页只能展示不能交互。想显示温度、IP地址、开关状态或者让用户在网页上配置参数就得用到LWIP HTTPD的SSI和CGI机制。5.1 SSI动态插入设备状态.shtml模板怎么做SSIServer Side Include可以理解为“服务端动态替换”。你在HTML里写一个特殊标签HTTPD在发送文件时把标签替换成你提供的内容。具体做法分三步第一步在HTML里写占位标签。比如要显示设备温度当前温度!--#temp:value-- ℃注意这个文件的扩展名必须是.shtml不是.html。LWIP HTTPD只对.shtml后缀的文件做SSI扫描。如果你把内容放在index.html里它是不会执行的。第二步在lwipopts.h里开启SSI#define LWIP_HTTPD_SSI 1第三步实现SSI回调函数并且在lwipopts.h里注册处理函数。不同版本的注册方式略有差异常见写法是u16_t my_ssi_handler(int iIndex, char *pcInsert, int iInsertLen) { if (strcmp(pcInsert, temp) 0) { int n snprintf(pcInsert, iInsertLen, %d, get_temperature()); if (n 0) return 0; if (n iInsertLen) return iInsertLen - 1; return n; } return 0; }然后在lwipopts.h里加#define LWIP_HTTPD_SSI_ENTRIES 1具体的SSI回调注册宏不同版本有差异有的用LWIP_HTTPD_SSI_CUSTOM有的用数组注册你以自己源码里的注释为准。核心逻辑都一样把标签名对应的字符串塞进pcInsert缓冲区返回长度。这里有个非常重要的细节替换后的字符串长度不能超过iInsertLen否则会截断甚至导致内存写越界。我见过一个同事在SSI回调里返回了一个超长的JSON字符串结果HTTPD直接崩溃。数值类数据注意用snprintf限制长度字符串类数据记得提前截断。5.2 CGI处理表单配置页怎么提交才靠谱CGI在LWIP里并不是传统意义上的CGI程序它本质上是一个URL回调。你注册一个URL当HTTPD收到匹配的请求时执行你写好的处理函数然后重定向到另一个页面。举个典型例子网页里要控制LED。HTML里写一个表单form action/led methodget input typeradio namestate value1开 input typeradio namestate value0关 input typesubmit value提交 /form然后在C代码里实现CGI处理器static const char *my_cgi_handler(int iIndex, int iNumParams, char *pcParam[], char *pcValue[]) { int i; for (i 0; i iNumParams; i) { if (strcmp(pcParam[i], state) 0) { led_ctrl(pcValue[i][0] 1); } } return /index.html; }注册CGI条目并配置宏static const tCGI my_cgis[] { {/led, my_cgi_handler}, }; #define LWIP_HTTPD_CGI 1 #define LWIP_HTTPD_MAX_CGI_ENTRIES 1当用户在浏览器点提交表单会发一个/led?state1的GET请求。HTTPD截获这个URL把参数名和参数值传给my_cgi_handler。处理完之后函数返回一个URL浏览器自动跳转回/index.html。注意几个坑CGI注册的URL和表单action要完全一致包括开头的斜杠不匹配就进不了回调表单请求会占用一个TCP连接处理完马上返回不要在回调里做阻塞操作比如延时、等待Flash写入完成否则页面会转圈很久GET方式提交的参数受LWIP_HTTPD_MAX_REQ_LENGTH限制参数多了会被截断。数据量大可以考虑POST但LWIP的POST回调实现复杂得多我的建议是配置页的数据通常很小GET加上限调大就够用了。6. 常见问题速查与排查思路这里把这一路最常见的故障整理成一张表方便你遇到问题时直接查不用重新把文章翻一遍。6.1 高频问题对照表症状可能原因解决办法打开IP返回404首页文件名不是index.html把首页改成index.html重新生成fsdata.c打开IP显示目录列表或空白文件没被打包进fsdata.c搜下fsdata.c里是否有对应的文件名中文乱码文件不是UTF-8、缺BOM或缺meta标签另存为UTF-8无BOM加meta charsetutf-8CSS不生效引用路径不对或大小写不匹配统一小写文件名检查路径是否和实际目录一致JS点击没反应JS文件名大小写不一致或Content-Type错误统一小写确认makefsdata输出日志里包含该文件浏览器下载HTML而不是显示Content-Type不是text/html检查makefsdata的扩展名映射或者把资源内联修改网页烧录后无变化浏览器缓存无痕窗口调试加版本号参数编译报重复定义fsdata工程里有两个fsdata.c移除工程里原来的fsdata.c模板访问页面明显卡顿外部资源请求次数太多合并CSS/JS减少图片数量表单提交后没反应CGI没注册成功或URL不匹配检查WWW_CGI数组和表单action是否完全一致6.2 我的调试三板斧遇到疑难杂症我一般按这个顺序排查第一板斧先确认文件真的在。用文本编辑器打开生成的fsdata.c搜索你请求的文件名。如果搜不到别调试HTTP了先回去跑makefsdata。这个排查30秒搞定能过滤掉八成的问题。第二板斧打开浏览器开发者工具的Network面板。看每个请求的响应状态码和响应头。404说明文件查找失败看请求URL和大小写200但内容不对看Content-Type和响应体内容。这一步能看出HTTP层到底发生了什么。第三板斧串口开LWIP HTTPD调试输出。在lwipopts.h里加#define LWIP_HTTPD_DEBUG LWIP_DBG_ON重新编译烧录后串口会打印HTTPD接收到的请求和响应信息。有时候浏览器把问题藏起来了串口日志会直白地告诉你它收到了什么。如果还解决不了就抓包。别怕Wireshark过滤一下TCP端口80几次交互就能看清问题。7. 最后分享几个个人习惯写到最后分享几个我做了几年LWIP网页开发后沉淀下来的习惯算不上标准答案但对提高效率很有帮助。习惯一先本地验证页面再打包进固件。我在电脑上起一个静态文件服务器把HTML写完先本地跑通所有交互、样式都没问题了最后才跑makefsdata。别一边调前端一边烧录效率太低而且MCU环境不好调试JS。习惯二尽可能做单文件页面。把CSS、JS都内联进index.html图片转base64或者干脆不用图片。虽然看起来“不专业”但在MCU这种资源受限的环境里单文件方案能避免掉大量HTTP层的坑。我做过一个配置页面包含表单、状态展示、图标总共也就20KB一个HTML全搞定。习惯三保留makefsdata的生成脚本。我通常写一个批处理文件双击就完成整条打包流程makefsdata.exe -s C:\web -f C:\project\src\fsdata.c下次修改HTML后双击一下等输出日志显示文件数量正确直接去编译固件就行。习惯四小改动可以直接改fsdata.c里的字节。如果你只是改一个词、一个数字重新跑makefsdata要好几步这时可以直接在生成的fsdata.c里定位对应字符串的UTF-8字节原地修改。前提是修改前后长度必须一致否则整个数组的偏移就乱了。我自己改过好多次“版本号”和“设备名”省了不少事。但要注意这只是临时救急正经更新还是要重新生成。另外提醒一点不同版本的LWIP和makefsdata行为细节有差异网上很多教程用的老版本宏名、文件名都可能对不上。以你自己工程里的源码为准我的经验可以作为参考但别死记参数。LWIP的HTTPD虽然不 fancy但把它调通之后再回头看你会觉得整个系统的能力上了一个台阶——设备不再只是能ping通而是真的可以通过浏览器去管理了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进