简介:STM32轻量级网络服务器(WebServer)源码包,面向嵌入式网络开发者与物联网工程师,可帮助解决STM32设备通过网页实现远程监控、状态查询与数据交互的问题,适用于智能家居、工业自动化及设备联网管理等场景。压缩包内共329个文件,主体为145个C语言头文件与144个C源码文件,另有6个动态网页模板、说明文档、编译脚本、固件镜像等,整体仅2.44MB。源码实现涵盖TCP/IP协议栈、HTTP服务器、CGI动态页面生成、嵌入式文件系统、FreeRTOS任务调度,以及GPIO、ADC、PWM等硬件接口,并基于正点原子探索者开发板提供完整例程,可编译烧录后直接在开发板上验证。目前已有509人学习,适合希望系统掌握嵌入式网络编程、协议栈与硬件协同开发的工程师参考。
1. STM32 WEBSERVER源码到底是什么:一段能让你用浏览器控制硬件的代码
STM32 WEBSERVER源码,说白了就是让一片Cortex-M单片机在以太网上提供HTTP服务。很多人第一次拿到这类代码时,都会觉得“单片机当服务器是不是太勉强”,但实际上它非常适合做没有屏幕的设备面板:一个STM32设备接上网线,手机或电脑浏览器打开一个IP地址,就能看到实时传感器数据、下发控制指令、甚至完成配置检查。这类源码解决的正是“设备有网口,但没人愿意写一套专用上位机”的尴尬。
做这件事的代价没有想象中高。常见方案是在STM32上跑lwIP协议栈,lwIP自带HTTP服务器模块(httpd),它负责监听80端口、解析请求、返回页面资源。你写的网页不是被存进SD卡,而是被工具转换成C语言数组,直接编译进Flash里。真正吃掉RAM的不是网页本身,而是TCP连接缓冲区和处理动态数据时的临时字符串。接下来会从原理和选型讲起,依次覆盖最小工程、SSI和CGI、典型坑位和上线前的验证方法,按“能不能跑起来、能不能扛住用、能不能上产线”来展开。
2. 选型与原理:拿到源码之前先想清这三件事
2.1 STM32的Web Server链路:从PHY到HTTP回调
当你在浏览器里输入http://192.168.1.100/,请求会经过几个固定环节:先是网线把电平交给PHY芯片,PHY通过RMII/MII接口把数据交给STM32内部的以太网MAC外设;MAC把以太网帧交给lwIP协议栈,协议栈层层剥开TCP/IP头后,把HTTP请求体交给httpd模块;httpd解析出URI和参数后,去fsdata中找网页文件,或者调用注册好的CGI/SSI回调,最终把响应数据原路发回浏览器。这条链路里,PHY是外部芯片,MAC是内部外设,lwIP负责TCP/IP,httpd只关注HTTP应用层。
实际应用中,静态页和动态内容在httpd里有完全不同的执行路径。静态页是查表找文件,找到后把数组内容分段发送;动态内容则会先进入CGI/SSI回调,由回调函数生成数据,再发出去。这意味着如果页面打不开,要先判断是静态资源缺失还是回调没有产出内容。用调试器在回调里打断点,能快速定位是哪一段路径出了问题。
为什么建议直接用lwIP的httpd而不是自己写HTTP解析?我在早期做过一版自研解析器,处理GET和POST不难,难的是连接复用、TCP粘包、超时断开和URL编码。一个请求被拆成多个TCP段时,解析器必须维护半包状态,否则网页一旦超过一个MSS就会随机失败。httpd模块把这些底层行为都接管了,应用层只剩两个任务:提供网页数据和注册规范的回调,开发量大幅下降。httpd既可以跑在裸机上,也可以跑在RTOS线程里,不依赖操作系统。
2.2 芯片选型和内存预算:一个经验公式
选芯片之前,先记住一句话:STM32 WEBSERVER源码的瓶颈不是CPU频率,而是RAM。HTTP请求本身不复杂,但每个TCP连接都占独立缓冲区;如果同时开4个连接,每个连接收发缓冲各2KB,再加上协议栈内存池,一下子就要掉几十KB RAM。
| 芯片系列 | 典型RAM | 典型Flash | 适合的玩法 |
|---|---|---|---|
| F103系列 | 20~64KB | 64~512KB | 静态页面+极简CGI;开SSI要小心 |
| F407系列 | 128~192KB | 512KB~1MB | 静态页+SSI+CGI都够,舒适区 |
| F429/H7系列 | 256KB以上 | 1MB以上 | 多连接、JSON频繁生成、带文件系统 |
很多F103设备拿到源码后编译都过不了,原因不是CPU不够,而是网页文件转成的C数组太大,直接塞爆Flash,或者RAM不足以支撑多连接。
内存预算我习惯按这个公式估算:总RAM ≈MEM_SIZE(lwIP堆) +PBUF_POOL_SIZE × PBUF_POOL_BUFSIZE+ 连接数 ×(TCP_SND_BUF+TCP_WND) + httpd自身缓冲 + 任务栈。对于F407的典型工程,我一般先给MEM_SIZE=8192、PBUF_POOL_SIZE=10、TCP_SND_BUF=2048、TCP_WND=2048,再开4个httpd连接,这样起步RAM在60KB以内。如果编译后堆不足,优先压缩MEM_SIZE和连接数,而不是调大,因为调大往往掩盖了网页资源或回调中的内存泄漏。
常见误用是把所有宏调到最大,结果编译报错或启动时进入HardFault。原因是各个缓冲区定义之间会互相挤占RAM布局。从默认值保守往上加,加一次就做一次整盘RAM检查,才是稳妥做法。
2.3 源码里决定生死的几个文件
拿到一套源码,别急着看main.c,先找这五个位置:lwipopts.h负责协议栈的全局参数,httpd.c/h是HTTP服务器主体,fsdata.c是网页资源的C数组,ethernetif.c是网卡接入层,main.c里的初始化顺序是所有模块连接起来的线索。
lwipopts.h最常见的坑是:httpd的一些功能通过LWIP_HTTPD_CGI和LWIP_HTTPD_SSI宏独立控制。很多源码默认只开CGI不开SSI,或者反过来,你写了对应回调却不生效。改宏之后必须重新编译整个工程,因为lwipopts会被多个文件include,增量编译经常漏掉依赖。
httpd.c里有一组配置常量,比如最大连接数、CGI参数数量上限。这些常量有的和lwipopts.h联动,有的直接在httpd文件里定义。我的习惯是优先改lwipopts.h,只在它没有对应项时才动httpd源码,这样便于以后升级lwIP版本。fsdata.c不要手写,它由makefsdata工具生成,手写很容易出现数组长度对不上、URL路径错误的问题。只要记住一句话:网页显示异常先怀疑fsdata没重新打包,CGI/SSI不生效先怀疑宏没打开。
3. 把源码跑起来:最小HTTP服务部署步骤
3.1 用图形化配置工具生成带lwIP和HTTPD的工程
拿到源码后在真实板子上跑,最常见做法是先用芯片厂商的图形化初始化工具(通常叫CubeMX)生成一个带lwIP中间件的基础工程,再把自己需要的源码片段移植过去。如果你手里的源码本来就是某个板子配套的,这一步可以跳过一半,但至少要把芯片型号、PHY型号和引脚晶体核对一遍。
具体步骤大概是这样:先配置时钟树,给以太网外设一个50MHz的参考时钟;再使能ETH外设,选择RMII模式,填好PHY地址;接着打开lwIP中间件,在高级选项里勾选HTTP Server,同时关闭用不到的协议模块;然后设置一个固定的IP地址,比如192.168.1.100,方便调试;最后生成代码,在main.c的MX_LWIP_Init末尾确认httpd_init()被调用。
许多工程生成后,httpd_init()不在初始化函数里,需要自己加。加的位置必须是tcpip_init执行之后,否则httpd启动时协议栈还没就绪,开启监听会失败。如果用了DHCP,IP地址是后到的,但httpd仍然可以提前启动,等地址分配完成后客户端才能访问。
/* MX_LWIP_Init 是图形化工具生成的初始化函数之一 */ void MX_LWIP_Init(void) { tcpip_init(NULL, NULL); /* 添加网卡接口并注册链路回调 */ netif_add(&g_netif, &ipaddr, &netmask, &gateway, NULL, eth_lwip_if_init, tcpip_input); /* 设置默认网卡并启用 */ netif_set_default(&g_netif); netif_set_up(&g_netif); /* 静态IP场景下,本地地址已经是刚设置的 */ httpd_init(); /* HTTP Server 监听 80 端口 */ }netif_add的最后一个参数是tcpip_input,lwIP会从驱动收到以太网帧后交给TCPIP线程。httpd_init放在这里能保证协议栈已就绪。如果你在源码里看到httpd_init放在别的地方,要确认它在tcpip_init之后。ipaddr/netmask/gateway通常用IP_ADDR4宏来定义,比如写死192.168.1.100、255.255.255.0、192.168.1.1。
3.2 用makefsdata把网页打包成C源码
先写一个干净的首页,保存为html/index.html,这是后面所有处理的基础。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>STM32 WebServer</title> </head> <body> <h1>Hello from STM32</h1> <p>Web server is running.</p> </body> </html>然后打开命令行,进入lwIP的makefsdata工具目录,执行:
makefsdata html/这个命令会把html/下的所有文件打成fsdata.c,生成的是一个只读C数组。如果你手动编辑过网页,记得重新执行一次,否则网页变了工程里跑的还是旧内容。把fsdata.c加入IDE工程后,还需要确认httpd的FS后端用的是数组而不是外部文件系统,否则静态页面匹配不上。这类配置一般在lwipopts.h或httpd自带的配置文件里。
makefsdata生成的数组大小直接占用Flash。一个几十KB的网页文件夹,在F103上会显著挤压代码空间。我的经验是首页尽量控制在10KB以内,样式能内联就内联,图片转成很小的base64或干脆不放。这样既省Flash,也减少响应发送时间。
3.3 最小验收:给板子上电后看到的第一个页面
完成上面的步骤后,插上网线,把电脑有线网卡配成同网段(例如192.168.1.2),然后用命令行HTTP客户端去访问:
curl http://192.168.1.100/如果返回的是你写的HTML第一行,说明STM32 WEBSERVER源码已经在正常工作。如果没有任何响应,先检查网口指示灯、ARP解析和电脑防火墙。如果返回404,说明网页文件没有正确打进fsdata,或者URI不对。
我通常会再执行一次curl -I http://192.168.1.100/,看响应头里的HTTP/1.1 200 OK,这比浏览器更直接。浏览器缓存有时会让人误判页面没更新,但命令行客户端不会。另一个小技巧是打开lwipopts.h里的debug开关,能够在串口看到HTTP请求和错误码,调试静态IP和连接问题非常有用。
4. 从能访问到能用:SSI动态数据与CGI控制接口
4.1 动态网页的两种姿势:SSI替换和CGI分发
| 机制 | 处理位置 | 请求流程 | 最适场景 |
|---|---|---|---|
| SSI | 服务器返回静态文件时 | 读取fsdata中的HTML,扫描标记,回调替换后发送 | 局部更新数值、状态 |
| CGI | URI匹配后直接调用函数 | httpd收到/cgi?param=1,解析参数,调用回调,返回重定向或动态内容 | 按钮控制、表单提交、JSON接口 |
选型原则:页面主体固定、只有几个数字会变的,用SSI就够;需要改变设备状态或生成独立内容的,用CGI。如果把所有控制逻辑硬塞进SSI也可以,但回调里必须自己拼接大段HTML,代码会非常难读。我一般会这样做:传感器数值走SSI,按钮控制走CGI,两者结合,页面主体保持静态。
另外注意SSI有性能代价:httpd在发送页面时要在每个包中扫描标记,网页越大,扫描时间越长。CGI则只影响动态生成内容,不拖累其他页面。能用CGI生成的小接口就尽量不承载整个页面。
4.2 用SSI在网页上实时显示传感器温度
在index.html中加入一行:
<p>当前温度:<!--#temp--> ℃</p><!--#temp-->就是SSI标记,httpd在返回这个页面时会把标记替换成回调返回的字符串。接下来,在源码里增加一个SSI处理函数:
#include "lwip/apps/httpd.h" static int read_temperature(void) { return 26; } static const char* ssi_handler(const char* tag, int* tag_len, char* output, int output_len) { if (*tag_len >= 4 && memcmp(tag, "temp", 4) == 0) { int n = snprintf(output, output_len, "%d", read_temperature()); if (n > 0 && n < output_len) { *tag_len = n; return output; } } *tag_len = 0; return NULL; } static const char* ssi_tags[] = { "temp" }; /* 在初始化时调用 */ void my_httpd_init(void) { httpd_set_ssi_handler(ssi_handler, ssi_tags, 1); httpd_init(); }ssi_tags数组告诉httpd哪些标记需要回调。tag_len传入时是匹配到的标记长度,回调里把它改成输出数据的长度,httpd才知道替换多少字节。output缓冲区大小有限,所以snprintf后要判断长度越界。如果返回NULL,httpd就把原标记原样发送到浏览器,页面会露出<!--#temp-->。看到这个现象时,多半是回调没被注册或标记名拼写不一致。
如果你的页面有多个动态点,把ssi_tags数组扩充,并在回调里用strncmp区分。不要在每次请求时重新读取慢速传感器,可以缓存一个全局值,由定时器周期性刷新,避免网络线程被慢速I/O拖住。
4.3 用CGI做一个LED开关,并顺手提供JSON状态接口
HTML中加入一个按钮表单:
<form action="/led.cgi" method="get"> <button name="state" value="1">开LED</button> <button name="state" value="0">关LED</button> </form>在C源码中注册CGI处理函数:
static const char* led_cgi(int index, int num_params, char** params, char** values) { int i; for (i = 0; i < num_params; i++) { if (strcmp(params[i], "state") == 0) { set_led_state(values[i][0] == '1'); } } return "/index.html"; } static const tCGI cgi_table[] = { { "/led.cgi", led_cgi } }; /* 初始化时注册 */ httpd_set_cgi_handler(NULL, cgi_table, 1);当浏览器访问/led.cgi?state=1时,lwIP会把查询参数拆成两个数组,params是参数名,values是值。index表明当前URI在cgi_table中的序号,如果有多个CGI可以区分。返回值是一个网页路径,httpd会发送302重定向让浏览器跳过去,这种模式适合控制后回到原页面。
如果想做JSON接口,比如前端JS轮询设备状态,有两种实现思路。一种是把status.cgi做成fsdata里的空占位文件,再用SSI片段生成JSON;另一种是使用lwIP httpd的扩展接口直接输出缓冲区内容,这依赖具体版本。我常用的折中方案是让status.cgi重定向到/status.json,而status.json本身是一个SSI模板,里面只有一条JSON数据,例如{"temp": <!--#temp-->}。这样复用SSI能力,不需要改httpd源码,前端拿到后自己解析就能定时刷新整个页面。注意在CGI回调里对参数做白名单校验,防止把非法状态传给GPIO控制函数。
5. STM32 WEBSERVER源码里的那些坑:从连不上到中文乱码
5.1 连接与内存相关的三条教训
坑一:浏览器能打开首页,但快速刷新几次后页面一直转圈。
现象:同时开多个标签页或按Ctrl+F5,HTTP请求卡住,等很久才有响应。 原因:LWIP_HTTPD_MAX_CONNECTIONS默认值太小,httpd接受连接数达到上限后,新来的TCP连接只能排队。如果浏览器同时发起多个并行请求(HTML+CSS+小图标),瞬间就会占满连接槽。 解决:在lwipopts.h里把LWIP_HTTPD_MAX_CONNECTIONS提到4或8,并同步提高PBUF_POOL_SIZE。改完先测RAM,确认堆余量够,再考虑是否开更高。
坑二:页面返回很慢,尤其是一个5KB以上的页面要好几秒。
现象:单连接访问时,页面内容分很多段到达,浏览器渲染断断续续。 原因:TCP_SND_BUF太小,一次只能塞下很小一段数据,发送完要等确认再继续。lwIP的慢启动机制在小缓冲下表现尤其明显。 解决:把TCP_SND_BUF和TCP_WND都设成2048或4096,同时保证TCP_MSS与网卡一致(常见1460或1512)。不要只调一处,这两个值是一个收发窗口的两端。另外,把网页里不需要的内联图片删掉,体积小了,发送自然快。
坑三:设备重启后网页打不开,拔插网线后恢复。
现象:上电第一次能访问,按下复位后就失联,有时串口能看到ARP请求但设备不回。 原因:PHY复位时序不稳定或MAC地址配置全零。很多源码里写了一个固定的本地MAC,如果多块板子共用同一个值,也会导致网络冲突。 解决:在以太网初始化前加几十毫秒延时,等待PHY完成上电复位;给板子写入唯一串号,用它的某几位生成MAC地址。检查ethernetif.c中MAC数组是否为全0,如果是,必须填充。
5.2 页面与数据相关的两条教训
坑四:浏览器显示中文乱码,英文正常。
现象:页面标题和按钮的中文全是问号或方块。 原因:HTML文件保存为GBK编码,而浏览器默认按UTF-8解析;或者源文件本身不是UTF-8,却被当成UTF-8输出。 解决:写HTML时用UTF-8无BOM保存,在<head>中加入<meta charset="utf-8">。修改后必须重新运行makefsdata html/,否则编译进来的数组还是旧编码。如果用了外部文件系统,还需要保证读取时按二进制字节流读取,不要在中间做字符集转换。
坑五:/led.cgi提交后要么404,要么参数被截断。
现象:按钮提交后跳到404页面,或者回调里收到的num_params是0。 原因:lwIP的httpd在处理CGI时,会把URI先当作文件查询一遍,有些实现要求CGI的URI在fsdata中有一个占位文件,否则还没进入CGI回调就返回404。另外,GET参数超过HTTPD_CGI_MAX_PARAMS或单个参数过长时,多余的会被截掉。 解决:在fsdata.c中补一个空的led.cgi条目,保证它能通过文件检查;接着检查lwipopts.h里CGI参数上限,适当调大。我一般会把参数上限从默认的4调到8,单个参数长度调到64,足够LED、PWM、阈值配置这类场景。
6. 上线前的最后一步:验证、优化与安全加固的常见做法
当你的STM32 WEBSERVER源码已经稳定跑起来,下一步不是急着美化网页,而是验证边界。我会先做三件事:第一,用命令行HTTP客户端反复测试,curl -I http://192.168.1.100/看响应头,curl http://192.168.1.100/index.html检查返回长度,再连续发10次连接,确认不会掉链子;第二,用抓包工具捕获一段时间流量,看TCP三次握手是否正常、HTTP响应是否一次性送出去,如果出现大量重传,多半是缓冲区或MSS配置不匹配;第三,写一个简单的循环脚本每100ms请求一次,持续几分钟,观察RAM堆余量。如果剩余内存持续下降,优先怀疑CGI里用了malloc没有释放,尽量改成静态缓冲区。
再往后可以做三件提升。一是把httpd放进RTOS独立任务,给它较高优先级栈,传感器读取放到另一个任务,避免网络卡顿影响采集;二是加应用层Token校验,登录页面输入密码后,服务端生成一个随机token放进Cookie,CGI回调先校验token再执行动作。TLS在MCU上成本很高,不一定划算,用HTTP加自定义token能挡住绝大多数误操作;三是把网页从C数组改挂到外部文件系统,便于以后在线升级网页内容,代价是启动时要读Flash且要考虑磨损均衡。
我个人的习惯是,会在板上留一个隐藏的恢复默认按钮:按住后HTTP服务回滚到最小配置,相当于给这套Web Server装了一颗后悔药。调这种源码的关键就是不断确认“你改的宏真的生效了”,每次修改都重新打包fsdata、重新编译、用命令行客户端验证,而不是凭直觉觉得页面该变了。希望帮到你。
本文还有配套的精品资源,点击获取