简介:基于STM32F407与LAN8720A的嵌入式WebServer源码工程,完整演示如何通过lwIP协议栈在MCU上实现HTTP登录与注册功能,适合正在学习嵌入式网络、物联网设备联网的开发者参考。压缩包共588个文件、约16.37MB,以C语言源码与工程文件为主,包含.uvprojx/.hex/.sct等Keil工程与烧录配置,以及.html/.shtml网页资源和README说明,解压后可直接用Keil打开二次开发。已有870人学习/下载。该工程不仅覆盖MAC/PHY初始化、TCP/IP协议栈接入、HTTP请求解析与响应生成,还展示了将网页资源编译进固件的fsdata.c处理方式,省去外部文件系统依赖;同时包含用户数据存储与表单验证等关键代码,适合作为入门到进阶的对照参考,快速跑通嵌入式WebServer流程。
1. 项目整体思路与硬件平台选型
1.1 为什么用F407配LAN8720A做WebServer
STM32F407这颗M4内核芯片在嵌入式网络产品里出镜率一直很高,主频168MHz、内置MAC控制器、带FPU,做轻量级网络应用绰绰有余。而LAN8720A这颗PHY芯片几乎是低成本百兆以太网方案的默认选择,价格便宜、外围电路简单、支持RMII接口,跟F407的MAC对接非常顺。
很多朋友一开始会纠结一个问题:WebServer不是应该在Linux或者至少带系统的平台上跑吗,为什么要在裸机单片机里折腾HTTP?我的看法是,产品形态决定技术路线。如果你的设备只需要提供一两个配置页面、一个简单登录入口、状态查看接口,那用MCU方案完全可以撑住;而且相比Linux方案,MCU启动快、功耗低、成本压得下来,生产维护也简单。登录注册这类功能放在单片机上做,本质上是给嵌入式设备加一道轻量级的访问控制,比如设备管理后台、智能硬件配置页、远程参数修改入口,都是它的典型用武之地。
这部分的参考价值在于:它不是一套特别复杂的工程,但把“硬件网络连通、协议栈处理、Web后端逻辑、数据持久化”全部走通,是一条非常完整的嵌入式网络开发链路。适合正在做毕业设计、小型物联网网关、工业设备配置界面开发的朋友参考。
1.2 硬件资源开销与功能边界
做嵌入式WebServer前,先想清楚资源分配。F407内置的MAC是100Mbps全双工能力,LAN8720A只走10/100M,实际网页交互数据量非常小,带宽完全够用。关键占用在内存和Flash上——lwIP协议栈加上httpd服务器组件,ROM大概多占20~30KB,RAM根据你配置的内存池大小决定,通常预留10~20KB给网络收发缓冲。
登录注册功能本身不复杂,但需要规划用户表存储空间。F407内部Flash有1MB,留最后几个扇区做数据存储是够用的;如果怕擦写损耗或者需要频繁改密码,可以外挂一颗AT24C02之类的EEPROM,贵不了几毛钱,但省心很多。我这套方案选了内部Flash的末尾扇区,配合擦写均衡逻辑,实测做演示和轻量生产环境都没问题。
1.3 功能边界
要提前说明白:嵌入式WebServer的登录注册跟互联网产品不是一回事。我们不搞验证码、不搞密码找回、不搞多用户权限体系,核心只做两件事:
- 用户注册:往存储区写入一组用户名和密码。
- 用户登录:校验输入,分配一个临时的会话标识,之后页面访问携带标识即可。
注册功能有些场景不需要开放(比如设备出厂预置管理员账号),但保留它能让整套逻辑更完整,也更方便你们改造成自己的业务。所有代码和流程走的是“最小可用产品”思路,你可以在这个基础上继续加功能,比如用户管理、参数修改页面、日志查询,都只需要顺着这套框架扩展。
2. 网络环境搭建与底层驱动要点
2.1 RMII接口硬件连接细节
F407的MAC要通过RMII接口跟LAN8720A对接,这是第一个容易踩坑的点。RMII比MII少了一半引脚,但需要外部提供50MHz参考时钟,方向是从PHY给MCU。LAN8720A可以用25MHz晶振配合内部PLL倍频出50MHz,所以实际电路一般是一个25MHz无源晶振挂在PHY旁边,CLKOUT引脚输出50MHz给F407的PHY_CLK。
对照一下常用的引脚分配:
- ETH_RMII_REF_CLK:PA1
- ETH_RMII_CRS_DV:PA7
- ETH_RMII_RXD0:PC4
- ETH_RMII_RXD1:PC5
- ETH_RMII_TX_EN:PB11
- ETH_RMII_TXD0:PB12
- ETH_RMII_TXD1:PB13
- ETH_RESET:PB15(自己定义的,可换)
- LAN8720A的PHY_AD0引脚默认拉低,地址为0
这个地址很关键。很多人在lwIP里配置PHY地址,流程里第一步读PHY ID,LAN8720A返回的值如果对不上,要先检查PHYAD0引脚的电平,再检查读函数里的地址参数。我一开始用的PHY地址是0,CubeMX默认也填0,但代码里实际读出来是0x0007,排查了半天才发现是寄存器读取时序问题,后面会专门说。
2.2 lwIP与PHY驱动的协作流程
lwIP负责协议栈,但网卡底层收发以及PHY的状态检测需要你自己挂接。F407的HAL库提供了Ethernet驱动接口,CubeMX里勾选ETH、lwIP、LAN8720A三个组件后,生成代码的骨架就搭好了。但别忘了检查以下配置:
- lwIP的堆大小:
MEM_SIZE至少给10KB以上 TCP_MSS:1460即可,网页传输不需要调整NO_SYS:裸机运行必须设为1,不跑RTOS- PHY地址:填0x00
- PHY时钟源:选
RCC_HCLK_DIV2,50MHz
驱动部分还有个容易忽略的PHY状态机问题。LAN8720A的热插拔检测需要周期性轮询PHY寄存器1的bit2(Link Status),lwIP自带的ethernetif.c里有这个逻辑。如果网络拔插后链路恢复不了,多半是PHY地址不对或者轮询的寄存器基础地址配错。
2.3 中断方式还是轮询方式
裸机环境下建议用轮询加定时器穿插的方式。原因很简单,中断收包在高负载下容易导致其他任务饿死,而且lwIP的ethernetif_input函数如果放到中断里处理,处理耗时过长会影响实时性。
我实际采用的是:主循环里调用eth_link_check检测链路状态,用HAL_ETH_GetReceivedFrame收包后喂给tcpip_input(NO_SYS模式下是ethernetif_input),发包通过ERR_OK返回值判断是否成功。实测在同时跑登录页面刷新和串口日志打印时,响应稳定,没出现死机或卡死的现象。
3. WebServer框架选择与动态页面生成
3.1 用lwIP httpd还是自己写解析
lwIP自带的httpd组件(apps/httpd)是个很成熟的选择,支持CGI集成、SSI模板、多连接管理。但它有个特点:偏向静态页面,动态内容需要借助CGI函数向页面填充数据。我们的登录注册页面是高度动态的,最简单的方式是直接用printf拼接HTML字符串,一次性生成整个页面,这样不需要引模板文件,代码也更直观。
httpd组件本身支持多个连接,这对页面调试很重要。因为浏览器一个页面可能同时开多个TCP连接去拉取favicon或者样式文件,如果只支持单连接,会导致页面刷新时卡住。F407资源下,lwIP官方推荐HTTPD_MAX_CONNECTIONS设为4,实测开2就够,但为了浏览器兼容性,我直接开到4。
3.2 动态页面如何避免反复“打模板”
动态页面的难点不在HTML,而在数据填充。我的实现思路是在CGI处理函数里统一根据URI分发逻辑,比如:
/:返回登录页/register:返回注册页/api/login:处理登录POST请求/api/register:处理注册POST请求/dashboard:返回登录成功后的页面
每个页面用一个大char数组作为输出缓冲,用sprintf或snprintf拼出完整HTML。这里要注意缓冲区的长度,页面HTML加动态数据最坏情况可能超过2KB,我直接开了8KB的静态数组,F407的RAM有192KB,完全放得下。
另外,浏览器提交的静态资源请求(比如favicon.ico)也要提前处理,否则日志里会一直刷请求失败。最简单的办法是在CGI里判断favicon.ico直接返回404。
3.3 表单POST解析与URL解码
POST请求的body可能是username=admin&password=123456这种格式,解析起来不复杂:拿到Content-Length字段,按长度读取body数据,然后用strtok按&分割成键值对,再按=拆出字段名和值。
但有个细节必须处理:URL编码。如果密码里包含+、%20这类特殊字符,浏览器会做转义,我们接收到的是编码后的字符串,必须做一次URL解码再存库。解码逻辑就是遍历字符串,遇到%则读取后面两位十六进制转成ASCII字符,遇到+则替换为空格。
顺便说一个坑:lwIP的httpd在NO_SYS模式下,CGI参数解析需要等POST数据全部接收完才能调用。如果请求体比较大,可能需要分多次收包,我们要在收包处理里判断数据长度是否等于Content-Length,不等则不触发业务逻辑,等到完整包再处理,不然会拿到半截数据。
4. 登录注册功能的核心实现
4.1 用户信息在内部Flash中的存储设计
用户表我用一个固定结构体数组放在内部Flash末尾扇区。结构体定义类似于:
#define USER_MAX_COUNT 8 typedef struct { uint8_t valid_flag; // 当前记录是否有效 uint8_t username[16]; // 用户名 uint8_t password[16]; // 密码明文,生产环境建议哈希 uint32_t create_time; // 注册时间(可选) } user_info_t;存储区规划要避开程序代码使用的扇区。F407内部Flash最后一个扇区大小通常是64KB或128KB(不同型号有差异),从末尾往前推,预留一个扇区作为用户数据区。操作流程是:上电读取该扇区数据,解析出所有有效用户;注册时找到一个空槽位并写入;如果槽位写满,做整扇区擦除重写。
这里提醒一个关键点:擦Flash前务必关闭所有中断(__disable_irq()),擦除完成后恢复,否则擦写过程中来一个中断,轻则超时,重则中断里访问Flash导致HardFault。
密码明文存储这道坎绕不过去,但对MCU产品来说,内部Flash本身就比较难拆读,加上大多数场景是局域网,风险可控。如果非要更稳妥,可以用一个简单的字符串混淆或者轻量哈希(比如FNV1a)再存储,代码量不大,但能阻止大多数人用串口工具直接读Flash拿到密码。
4.2 注册流程与登录校验逻辑
注册接口的处理流程:
- 解析POST参数,检查用户名和密码是否为空;
- 遍历用户表,检查用户名是否已存在;
- 检查槽位是否已满;
- 写入Flash,返回注册成功页面。
登录接口的处理流程:
- 解析POST参数;
- 遍历用户表,匹配用户名和密码;
- 匹配成功则生成一个随机的session token,存入内存中的会话表;
- 返回
Set-Cookie: session_token=xxx响应头,浏览器会保存这个Cookie; - 后续访问
/dashboard时,查询Cookie中的token是否在会话表中,且未过期。
session过期时间我设的是10分钟,无操作的连接自动失效。会话表就是一个固定大小的数组,内存中即可,重启后需要重新登录。这个设计不算完善,但已经完全覆盖嵌入式后台管理的业务场景。
4.3 Token生成与Cookie下发
单片机上没有完整的随机数库,但F407自带硬件随机数发生器(RNG),直接读RNG寄存器就行。生成token的示例:
void generate_token(char *buf, uint8_t len) { for (uint8_t i = 0; i < len; i++) { uint32_t r = RNG_GetRandomNumber(); buf[i] = "0123456789abcdef"[r & 0x0F]; } buf[len] = '\0'; }下发Cookie的HTTP响应用标准格式:
HTTP/1.1 200 OK\r\n Content-Type: text/html\r\n Set-Cookie: session_token=xxxxxxxx; Max-Age=600; Path=/\r\n Connection: close\r\n \r\n <body>...</body>浏览器拿到Session Cookie后,每次请求同一路径下的页面都会自动带上,我们就能在CGI入口处统一做鉴权。鉴权失败时直接返回302重定向到登录页,用户体验也比较自然。
4.4 前端页面不加载任何框架
嵌入式设备上的浏览器可能是电脑、手机,也可能是局域网里的老旧终端,所以页面尽量写得朴素。不要引用CDN上的jQuery,不要用Webpack那套构建产物,老老实实用原生HTML加一个<form>标签就够了。
我的页面结构大概是:
<form action="/api/login" method="POST"> <input type="text" name="username" placeholder="用户名" /> <input type="password" name="password" placeholder="密码" /> <button type="submit">登录</button> </form>不加CSS也能看,加一点内联样式更友好。注册页面同理,action指向/api/register即可。登录成功后用window.location.href="/dashboard"跳转,失败则返回一个带错误提示的页面。
5. 调试过程中的常见问题与排查记录
5.1 网线插上但ping不通
这类问题占我调试时间的三分之二。先看硬件指示灯,LAN8720A的Link灯不亮基本是硬件问题:检查网络变压器是否接对、晶振是否起振、PHY地址是否拉对。灯亮了但MCU端ping不通,重点查几个地方:
- CubeMX里PHY时钟源是否配置为
RCC_HCLK_DIV2; ethernetif_init里PHY地址是否填0;- lwIP的netif是否正确
set_up和link_up; - 主循环里是否在调用
HAL_ETH_GetReceivedFrame之前处理了HAL_ETH_ReadData。
最隐蔽的问题在驱动层:HAL库的以太网接收流程中,必须先调用HAL_ETH_GetReceivedFrame_IT或轮询版本,再调用HAL_ETH_ReadData把数据拷贝到lwIP的pbuf里,顺序错了数据永远是空的。
5.2 页面能打开但点击登录无响应
这个问题通常不是网络问题,而是POST数据没有被完整接收。排查思路:
- 在CGI函数入口打印接收到的
Content-Length,对比实际收到的body长度; - 检查
tcpip_input的调用频率,确保主循环足够快; - 确认lwIP的
HTTPD_POST支持开关是否打开(LWIP_HTTPD_SUPPORT_POST为1)。
另外一个坑是:浏览器在POST完成后会等待服务器返回Connection: close才关闭连接。如果是HTTP/1.1的长连接,浏览器会挂起一段时间,页面看起来像卡住了。我统一在响应头部加了Connection: close,问题立刻消失。
5.3 中文乱码
F407内部存储和处理字符串默认是GBK编码,但网页一般用UTF-8,两者混用就会乱码。我的做法是:所有HTML页面头部声明<meta charset="utf-8">,代码源文件用UTF-8保存,字符串常量内部就是UTF-8编码。如果你用的是Keil,注意在Edit -> Configuration里把Encoding改成UTF-8,否则编译器写入的字符串还是GBK,页面照样乱。
5.4 Flash擦写导致死机
前面说过擦除时关中断,还有个问题:擦写次数。F407内部Flash的擦写次数是1万次左右,如果每次登录都写Flash,几个月后扇区就废了。我做的优化是:只在注册、改密码、删除用户时写Flash,登录行为只操作内存中的会话表,不碰Flash。如果是频繁修改配置的产品,建议把数据挪到外部EEPROM或者SPI Flash。
6. 实测结果与这套方案的扩展方向
我在这套方案上跑通的完整流程是:设备上电,浏览器输入IP进入登录页,输入出厂预设的admin账号登录成功,跳转到带设备状态信息的dashboard页面,再通过注册页新增第二个账号,退出后用新账号重新登录成功。整个过程在局域网里响应很快,页面刷新基本无感。
实际测试中同时开着调试串口和浏览器不停地刷新页面,连续跑了两天没有出现死机或网络断连。内存占用情况:lwIP配置大概占用12KB RAM,HTTP缓冲区8KB,用户表加会话表约1KB,整体资源占用在F407上很宽裕。
如果你想把功能做深,这个框架可以直接扩展的方向不少:
- 登录成功后增加操作日志记录,写到串口或Flash日志区;
- 增加密码修改页面,复用注册接口的存储逻辑;
- 在dashboard页面通过Ajax轮询更新传感器数据;
- 把用户表搬到外部SPI Flash,支持更多用户;
- 如果有多台设备,可以通过这个WebServer接口做简单的远程管控协议适配。
最后说一个我自己的体会。嵌入式WebServer调试起来比纯逻辑代码“烦”很多,因为问题可能出在硬件信号、PHY驱动、协议栈配置、HTTP解析逻辑任何一个环节。但一旦把链路完整打通,你就会对整个网络体系有更清晰的认识,之后再去做MQTT、Modbus TCP、OTA升级,都会从容很多。先跑通一个最小模型,再逐步加业务逻辑,是我推荐的做法,这套登录注册功能就是一个非常好的起点。
本文还有配套的精品资源,点击获取