
简介这份资源面向嵌入式开发初学者与物联网方向学习者围绕STM32F407微控制器与LAN8720A以太网PHY芯片构建一个可运行于开发板上的嵌入式Web服务器实现登录与注册功能。内容涉及TCP/IP协议栈如lwIP、HTTP请求解析、用户数据存储、HTML表单页面设计及密码加密等安全考量是理解嵌入式网络编程与前后端协作的完整实践案例。压缩包共588个文件以c与h源码为主辅以o、d、crf等编译中间文件以及htm、html、shtml网页文件、readme说明和hex固件整体约16.37MB目录结构便于按模块查阅。目前已有881人学习下载。通过该工程读者可获取可直接烧录运行的完整源码掌握PHY初始化、MAC地址配置、HTTP响应生成与表单验证等关键环节并借助调试工具排查网络通信问题适合作为课程设计或物联网入门的参考项目。1. 从一块吃灰的开发板说起这个 WebServer 到底能跑出什么效果手头有块 STM32F407 开发板网口旁边焊着 LAN8720A 这颗 PHY 芯片很多人第一次把网线插上去、ping 通之后就不知道下一步该干嘛了。这个项目干的事情很具体在 STM32F407 LAN8720A 的硬件组合上跑一个轻量级嵌入式 WebServer浏览器访问开发板 IP能打开一个登录页输入账号密码后跳转到注册/登录结果页整个流程走 HTTP 协议页面用 HTML 写数据存在芯片内部 Flash 或内存里。说白了就是把「单片机联网」这件事从 ping 通推进到「浏览器里能交互」。它适合两类人一类是刚接触 STM32 以太网外设、想找个完整闭环练手的嵌入式新手照着跑一遍能把 LwIP 协议栈、HTTP 解析、Flash 读写串起来另一类是做工业设备或物联网终端、需要在设备上塞一个配置页面的开发者这套登录注册的骨架可以直接改成参数配置、固件升级入口。关键词里提到的「stm32 嵌入式 HTML」本质就是这种把网页塞进单片机的玩法下面从工程结构一路拆到能改的地方。2. 硬件与协议栈选型LAN8720A 怎么接、LwIP 怎么裁2.1 硬件连接与时钟配置LAN8720A 是一颗 10/100M 自协商 PHY通过 RMII 接口和 STM32F407 的 MAC 控制器通信。RMII 比 MII 省引脚只需要 9 根线REF_CLK、TXD0、TXD1、TX_EN、RXD0、RXD1、CRS_DV、MDIO、MDC。这里有个新手最容易翻车的点——REF_CLK 的 50MHz 时钟从哪来。LAN8720A 内部可以输出 50MHz 时钟给 STM32也可以由 STM32 的 MCO 引脚输出给 PHY两种方案二选一配置错了就是 ping 不通。我一般用 PHY 内部时钟方案接线时把 LAN8720A 的 nINT/REFCLKO 引脚配置成 REF_CLK 输出模式STM32 侧 PA1 接这个 50MHz。CubeMX 里 ETH 配置选 RMIIPHY Address 填 0LAN8720A 的 PHYAD0 引脚决定接地就是 0Auto Negotiation 打开。时钟树要保证 ETH 的 25MHz 或 50MHz 参考时钟正确否则 MAC 初始化直接失败。/* eth.c 中 PHY 初始化关键片段 */ void MX_ETH_Init(void) { heth.Instance ETH; heth.Init.MACAddr macaddr; // 6 字节 MAC别用全 0 heth.Init.MediaInterface HAL_ETH_RMII_MODE; heth.Init.TxDesc DMATxDescTab; // 发送描述符表 heth.Init.RxDesc DMARxDescTab; // 接收描述符表 heth.Init.RxBuffLen 1524; // 单帧最大长度 HAL_ETH_Init(heth); }这段代码里MACAddr必须唯一同一局域网里两个设备 MAC 相同会冲突RxBuffLen设 1524 是为了容纳标准以太网帧改小了遇到大包会丢。DMATxDescTab和DMARxDescTab是 DMA 描述符数组LwIP 收发数据全靠它们数量一般各给 4 到 8 个给太少高并发时会卡。2.2 LwIP 裁剪与内存池STM32F407 有 192KB SRAM跑 LwIP 不能全量搬得裁。常见做法是只保留 IP、ICMP、TCP、HTTP 相关模块关掉 UDP 里用不到的部分如果不需要 DHCP 也可以关。lwipopts.h是裁剪的核心文件几个关键参数参数建议值作用MEM_SIZE16*1024堆内存HTTP 响应拼接用PBUF_POOL_SIZE16pbuf 池数量影响并发连接TCP_MSS1460单包最大段别超过 MTUTCP_SND_BUF4*TCP_MSS发送缓冲页面大就调大MEMP_NUM_TCP_PCB8同时活跃的 TCP 连接数MEMP_NUM_TCP_PCB给 8 意味着最多 8 个客户端同时连浏览器一个页面可能开多个连接给太少会出现页面加载一半卡住。TCP_SND_BUF如果比要发送的 HTML 页面小LwIP 会分多次发逻辑上没问题但慢页面超过 4KB 就适当加大。2.3 HTTP 服务线程的挂载LwIP 裸机模式下HTTP 服务通常跑在一个httpd线程或者主循环轮询里。如果用 FreeRTOS就单独开一个任务优先级低于网络接收任务避免抢 CPU 导致丢包。/* http_server.c 启动 HTTP 服务 */ void http_server_start(void) { httpd_init(); // LwIP 自带 httpd 初始化 // 注册 CGI 处理函数登录注册走这里 http_set_cgi_handlers(cgi_handlers, NUM_CGI_HANDLERS); }httpd_init()会创建监听 80 端口的 TCP PCBhttp_set_cgi_handlers把 URL 路径和处理函数绑定。LwIP 的 httpd 是精简版不支持 POST 大文件但登录注册这种小表单够用。如果项目里用的是自己写的 socket 服务器而不是 LwIP httpd逻辑类似只是要手动解析 HTTP 请求头。3. 登录注册功能拆解从 HTML 表单到 Flash 存储的完整链路3.1 前端页面怎么塞进单片机嵌入式 HTML 页面不能像 PC 那样放一堆外部 CSS 和 JS 文件常见做法是把整个页面压成一个字符串常量放在fsdata.c或者直接写在 C 文件里。LwIP 有个makefsdata工具能把一个文件夹里的 HTML 转成 C 数组但登录注册页面简单手写字符串更可控。/* login_page.h 登录页 HTML */ const char login_html[] htmlheadmeta charsetutf-8title设备登录/title/head bodyh2设备配置登录/h2 form action/login methodpost 用户名:input typetext nameuserbr 密码:input typepassword namepassbr input typesubmit value登录 /form/body/html;这个字符串直接作为 HTTP 响应体返回。注意action/login对应后面 CGI 注册的路径methodpost决定数据在请求体里而不是 URL 里。页面里不要引外部资源单片机没地方存图片和字体浏览器请求 favicon.ico 都会让 httpd 返回 404可以在 CGI 里加个空处理避免日志刷屏。3.2 CGI 处理函数解析表单与校验LwIP httpd 的 CGI 处理函数签名固定拿到请求后从hs-params里取 POST 参数。登录逻辑就是比对用户名密码注册逻辑就是写入存储。/* cgi_login.c 登录处理 */ const char *cgi_login(int iIndex, int iNumParams, char *pcParam[], char *pcValue[]) { char user[16] {0}, pass[16] {0}; for (int i 0; i iNumParams; i) { if (strcmp(pcParam[i], user) 0) strncpy(user, pcValue[i], sizeof(user)-1); if (strcmp(pcParam[i], pass) 0) strncpy(pass, pcValue[i], sizeof(pass)-1); } if (check_user(user, pass)) { return /success.html; // 校验通过跳转 } return /fail.html; // 失败跳转 }iNumParams是表单字段个数pcParam和pcValue是键值对数组。这里用strncpy限制长度防止超长输入把栈冲了——嵌入式里栈很小一个strcpy就能让程序跑飞。check_user去 Flash 里读已存的账号比对返回页面路径字符串httpd 会自动把对应文件发回去。3.3 账号数据存哪Flash 扇区规划STM32F407 内部 Flash 从 0x08000000 开始程序代码占前面账号数据放最后一个扇区比如 Sector 110x080E0000 起128KB。写之前要先擦除整个扇区再按字或半字写入。/* flash_store.c 账号读写 */ #define USER_ADDR 0x080E0000 #define USER_MAGIC 0x55AA55AA typedef struct { uint32_t magic; // 标记是否已初始化 char user[16]; char pass[16]; } user_info_t; int save_user(const char *u, const char *p) { user_info_t info {USER_MAGIC, {0}, {0}}; strncpy(info.user, u, sizeof(info.user)-1); strncpy(info.pass, p, sizeof(info.pass)-1); HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef erase {.TypeErase FLASH_TYPEERASE_SECTORS, .Sector FLASH_SECTOR_11, .NbSectors 1, .VoltageRange FLASH_VOLTAGE_RANGE_3}; uint32_t err; HAL_FLASHEx_Erase(erase, err); uint32_t *pdata (uint32_t *)info; for (int i 0; i sizeof(info)/4; i) { HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, USER_ADDR i*4, pdata[i]); } HAL_FLASH_Lock(); return 0; }magic字段用来判断这个扇区是不是第一次用读出来不等于USER_MAGIC就说明没存过账号走默认密码逻辑。擦除必须整扇区擦不能只擦几个字节这是 Flash 的物理特性。写入前HAL_FLASH_Unlock写完Lock忘了锁在某些型号上会影响后续操作。注意 Flash 擦写寿命约 1 万次别把登录次数也往里写要写就做磨损均衡或者换外部 EEPROM。3.4 请求响应时序一次完整的登录请求链路是这样的浏览器发 POST /loginLwIP 收到后交给 httpdhttpd 解析出 CGI 路径匹配到cgi_login函数读参数、查 Flash、返回跳转路径httpd 再把目标 HTML 作为响应发回。整个过程在同一个 TCP 连接里完成浏览器拿到 302 或者直接返回页面内容。如果页面跳转后白屏先看cgi_login返回的路径字符串是不是以/开头路径错了 httpd 找不到文件就返回 404。4. 避坑与排查调不通时先看这几个地方4.1 ping 不通PHY 灯不亮现象网口插上后 LINK 灯不亮或者亮了但 ping 超时。原因通常是 RMII 时钟没配对或者 PHY 地址填错。解决先用示波器量 PA1 有没有 50MHz没有就检查 CubeMX 里 ETH 的时钟源选择PHY 地址用 MDIO 扫描一遍0 到 31 挨个读 ID 寄存器能读出 0x0007 开头的基本就是 LAN8720A。4.2 能 ping 通但浏览器打不开现象命令行 ping 开发板 IP 正常浏览器访问一直转圈或连接被拒。原因多半是 httpd 没启动或者 80 端口被别的任务占了。解决确认httpd_init()被调用检查MEMP_NUM_TCP_PCB是不是被其他 socket 用完了用netstat类工具看不到嵌入式端就在 httpd 的 accept 回调里加串口打印看有没有新连接进来。4.3 登录提交后返回 404现象点登录按钮后页面跳到 404。原因cgi_login返回的路径字符串对应的 HTML 没注册进 httpd 的文件系统。解决确认success.html和fail.html已经通过fsdata或者手动注册的方式加进去了路径大小写敏感/Success.html和/success.html是两个东西。4.4 账号存进去读出来是乱码现象注册后重新登录密码对不上。原因Flash 写入时没对齐或者结构体有填充字节导致读写偏移不一致。解决结构体加__attribute__((packed))去掉填充写入按 4 字节对齐读的时候用memcpy而不是直接指针强转避免非对齐访问触发 HardFault。4.5 多次登录后程序卡死现象连续登录几次后单片机不响应。原因CGI 函数里用了局部大数组栈溢出或者 TCP PCB 没释放连接数耗尽。解决把大 buffer 改成 static 或者全局CGI 函数返回后 httpd 会关连接但如果自己写了 socket 服务器记得tcp_close。栈大小在 FreeRTOS 里由任务创建参数决定默认 128 字往往不够加到 512 字以上。5. 二次开发改造把登录注册换成你自己的业务这套代码的骨架价值在于「HTTP 请求 → 参数解析 → 业务处理 → 页面响应」这条链路登录注册只是最简示例。要改成设备参数配置页把cgi_login换成cgi_config表单字段从 user/pass 改成 ip/mask/gateway存储从 Flash 换成结构体变量响应页面从 success.html 换成 config_ok.html半小时就能改完。如果要做固件升级入口思路类似但要注意文件上传不能用 LwIP 自带 httpd它不支持 multipart 大文件得自己写 socket 接收边收边写 Flash收完校验 CRC 再跳转 Bootloader。我一般会在升级页加一个进度条用 AJAX 轮询一个/progress接口返回已接收字节数这样浏览器不会以为卡死了。验证改造是否成功我习惯分三步先用curl -v发请求看响应头和状态码排除浏览器缓存干扰再用串口打印把 CGI 收到的参数原样输出确认解析没错最后断电重启确认存储的数据还在。这三步走完基本不会出现「在我电脑上好好的」这种玄学问题。有个血泪经验改 CGI 返回路径时别直接返回http://开头的绝对地址嵌入式端拼不出完整 URL浏览器会跳到一个不存在的地址。返回相对路径/xxx.html最稳。从那以后我每次改完 CGI 返回值都强制用 curl 走一遍完整流程再交给测试希望帮到你。本文还有配套的精品资源点击获取