简介这份资源面向嵌入式开发初学者与物联网实践者围绕STM32F407微控制器与LAN8720A以太网PHY芯片构建一个可运行于开发板上的嵌入式Web服务器实现登录与注册功能。内容涉及TCP/IP协议栈如lwIP、HTTP请求解析、用户数据存储、HTML表单页面设计以及密码加密等安全考量是学习嵌入式网络编程与前后端交互的完整案例。压缩包共588个文件约16.37MB以c与h源码文件为主辅以o、d、crf等编译中间文件以及htm、html、shtml网页文件、readme说明、uvprojx工程文件与hex固件等覆盖从源码到可烧录程序的完整工程结构。目前已有881人学习下载。通过该工程读者可获取可直接编译运行的Keil工程、HTTP服务器框架与网页交互示例理解PHY初始化、MAC地址配置及中断处理流程并借助Wireshark等工具验证网络通信适合作为物联网与嵌入式Web应用的入门实践参考。1. 需求先立住为什么要在 STM32F407 上跑一个带登录注册的 WebServer很多做 stm32 嵌入式 HTML 方向毕设的同学一开始都会卡在同一个问题上板子能点灯、能串口打印但一到做个能演示的完整系统就不知道从哪下手。登录注册这个需求看着简单实际上它逼着你把网络协议栈、HTTP 解析、会话管理、前端页面、数据存储这几块全部串起来是一个非常适合作为课程设计或毕设骨架的题目。这套方案要解决的核心问题是在 STM32F407 LAN8720A 这套经典硬件组合上跑通一个能通过浏览器访问、支持用户注册和登录、登录后能进入受保护页面的嵌入式 WebServer。它适合正在做嵌入式方向毕设、需要一份可演示可答辩完整方案的学生也适合想快速搭一个设备本地配置页面的从业者参考。下面从选型、架构、实现到验收把整条链路拆开讲清楚。2. 技术选型与总体架构LwIP 裸机还是 RTOS页面放哪2.1 为什么是 LwIP 而不是自己写协议栈STM32F407 自带以太网 MAC 控制器LAN8720A 负责 PHY 层两者通过 RMII 接口连接。MAC 有了但 TCP/IP 协议栈必须自己解决。常见选择有三个LwIP、uIP、自己撸一个精简栈。uIP 太老功能弱做 HTTP 会话管理会很难受自己写协议栈在毕设周期内基本不现实。LwIP 是 ST 官方和社区都支持得最好的方案STM32CubeMX 里可以直接勾选配合 FreeRTOS 或裸机都能跑。我一般建议裸机 LwIP raw API 的路线原因是登录注册这种低频交互场景并发量极低裸机轮询完全够用而且省掉了 RTOS 任务划分和信号量的复杂度调试时出问题更容易定位。如果你后续要加多客户端并发或者实时性要求高的任务再上 FreeRTOS LwIP socket API 也不迟。2.2 总体架构四个模块的职责划分整个系统按职责切成四层从下往上依次是层级模块职责硬件层STM32F407 LAN8720AMAC/PHY 通信RMII 接口时钟 50MHz协议层LwIP 2.xARP、IP、TCP、HTTP 底层收发应用层HTTP 服务 会话管理请求解析、路由分发、Cookie/Session 校验数据层内部 Flash / 外部 SPI Flash用户账号密码存储页面资源HTML/CSS/JS通常有两种放法一是转成 C 数组直接编译进固件二是放到外部 SPI Flash 或 SD 卡里按文件系统读取。前者简单但改页面要重新烧录后者灵活但要多写一套文件读取逻辑。毕设场景我推荐先用 C 数组方式把页面调通再考虑迁移到外部存储。2.3 网络参数配置示例LwIP 初始化时几个关键参数必须配对否则会出现能 ping 通但打不开网页的玄学问题// lwipopts.h 关键配置 #define LWIP_DHCP 0 // 毕设用静态 IP方便调试 #define IP_ADDR0 192 #define IP_ADDR1 168 #define IP_ADDR2 1 #define IP_ADDR3 100 // 板子固定 IP #define LWIP_TCP 1 #define TCP_WND (4 * TCP_MSS) // 窗口别开太大F407 内存有限 #define MEM_SIZE (16 * 1024) // LwIP 堆内存页面大就往上加 #define PBUF_POOL_SIZE 16MEM_SIZE是最容易翻车的参数。默认值往往只有几 KB页面稍微大一点比如带 CSS 的登录页就会分配失败表现为浏览器一直转圈或者返回 500。我一般会先给到 16KB跑通后再根据实际占用往下压。TCP_WND不要超过MEM_SIZE的四分之一否则高负载时会内存耗尽。3. HTTP 服务与登录注册逻辑请求怎么解析会话怎么维持3.1 HTTP 请求解析从原始 TCP 数据到路由LwIP 的 raw API 收到 TCP 数据后回调里拿到的是一段原始字节流。你需要自己解析出请求方法、路径、头部和 body。核心逻辑是找到第一个\r\n\r\n前面是请求行和头部后面是 body。请求行格式固定为METHOD PATH VERSION按空格切分即可。// 简化的请求解析提取方法和路径 void http_parse_request(char *buf, http_req_t *req) { char *line_end strstr(buf, \r\n); if (line_end NULL) return; // 解析请求行 GET /login.html HTTP/1.1 sscanf(buf, %15s %63s, req-method, req-path); // 找 body 起始位置POST 请求才有 char *body strstr(buf, \r\n\r\n); if (body ! NULL) { req-body body 4; } // 提取 Cookie 中的 session id char *cookie strstr(buf, Cookie:); if (cookie ! NULL) { char *sid strstr(cookie, sid); if (sid ! NULL) { sscanf(sid, sid%31s, req-session_id); } } }这里有个细节sscanf的%63s限制了路径长度防止缓冲区溢出。嵌入式环境里所有字符串操作都要带长度限制这是血泪经验。Cookie 解析只做了最简单的sid匹配实际项目中如果 Cookie 有多个字段需要按分号切分后再逐个匹配。3.2 路由分发用查表代替 if-else 堆叠路径多了以后一长串if (strcmp(path, /xxx) 0)会非常难维护。我一般用一个路由表typedef struct { const char *path; const char *method; void (*handler)(int conn, http_req_t *req); } route_t; static const route_t routes[] { {/, GET, handle_index}, {/login.html, GET, handle_login_page}, {/register, POST, handle_register}, {/login, POST, handle_login}, {/dashboard, GET, handle_dashboard}, // 受保护页面 }; void http_dispatch(int conn, http_req_t *req) { for (int i 0; i sizeof(routes)/sizeof(routes[0]); i) { if (strcmp(req-path, routes[i].path) 0 strcmp(req-method, routes[i].method) 0) { routes[i].handler(conn, req); return; } } http_send_404(conn); // 没匹配到统一返回 404 }路由表的好处是新增页面只加一行不用动分发逻辑。/dashboard这类受保护页面在 handler 里先校验 session校验不过就 302 重定向到登录页。3.3 会话管理一个够用就好的 Session 方案嵌入式设备上跑完整 Session 框架不现实我一般用一个固定大小的会话表#define MAX_SESSIONS 4 typedef struct { char sid[33]; // 32 位随机字符串 \0 char username[16]; uint32_t expire; // 过期时间戳秒 uint8_t valid; } session_t; static session_t sessions[MAX_SESSIONS]; // 登录成功后创建会话 const char *session_create(const char *username) { for (int i 0; i MAX_SESSIONS; i) { if (!sessions[i].valid) { gen_random_sid(sessions[i].sid, 32); // 用硬件 RNG 生成 strncpy(sessions[i].username, username, 15); sessions[i].expire sys_now() / 1000 1800; // 30 分钟 sessions[i].valid 1; return sessions[i].sid; } } return NULL; // 会话满了 }MAX_SESSIONS设 4 是因为毕设演示场景同时在线不会超过两三个人设太大浪费 RAM。sid用 STM32F407 自带的硬件随机数发生器生成比rand()安全得多。过期时间设 30 分钟答辩演示足够用。会话校验函数在每次访问受保护页面时调用过期或无效就清掉对应槽位。3.4 用户数据存储内部 Flash 扇区读写账号密码不能明文存至少要做一次哈希。F407 内部 Flash 按扇区擦除我一般划出最后一个扇区扇区 11128KB专门存用户数据// 用户记录结构按扇区偏移依次存放 typedef struct { uint8_t used; // 0xFF 表示空槽 char username[16]; uint8_t pwd_hash[32]; // SHA-256 结果 uint8_t salt[16]; // 每个用户独立盐值 } user_record_t; // 写入前必须先擦除整个扇区 int user_save(user_record_t *rec) { HAL_FLASH_Unlock(); // 找到第一个空槽写入 for (int i 0; i MAX_USERS; i) { uint32_t addr USER_FLASH_BASE i * sizeof(user_record_t); if (*(uint8_t *)addr 0xFF) { // 按字写入F407 要求 32 位对齐 flash_write_words(addr, (uint32_t *)rec, sizeof(user_record_t)/4); break; } } HAL_FLASH_Lock(); return 0; }注意 F407 写 Flash 必须按 32 位字对齐写之前要确保目标区域已擦除。擦除操作会阻塞 CPU 几百毫秒期间网络会卡顿所以注册操作不要频繁触发。密码哈希用 SHA-256 每用户独立盐值虽然比不上 bcrypt但在嵌入式场景下已经能挡住明文泄露的风险。4. 前端页面与交互嵌入式 HTML 怎么写才不臃肿4.1 页面精简原则能内联就内联嵌入式设备带宽和内存都紧张页面要尽量小。CSS 和 JS 直接内联在 HTML 里不要外链省掉额外的 HTTP 请求。一个登录页控制在 2KB 以内比较理想!DOCTYPE html html head meta charsetutf-8 meta nameviewport contentwidthdevice-width,initial-scale1 titleLogin/title style body{font-family:sans-serif;max-width:320px;margin:40px auto;padding:0 16px} input{width:100%;padding:8px;margin:6px 0;box-sizing:border-box} button{width:100%;padding:10px;background:#2c3e50;color:#fff;border:none} .err{color:#e74c3c;font-size:13px} /style /head body h2设备登录/h2 form methodpost action/login input nameuser placeholder用户名 required input namepass typepassword placeholder密码 required button typesubmit登录/button /form p classerr%ERROR%/p pa href/register.html没有账号注册/a/p /body /html%ERROR%是占位符服务端返回时替换成具体错误信息比如用户名或密码错误。这种模板替换方式比动态拼接字符串安全也更容易维护。页面里没有用任何外部字体、图标库、框架全部手写这是嵌入式 HTML 的基本素养。4.2 表单提交与响应处理注册和登录都用 POST 表单提交Content-Type是application/x-www-form-urlencoded。服务端解析 body 时按和切分// 解析 userabcpass123 格式的 body int parse_form(const char *body, const char *key, char *out, int out_len) { char pattern[32]; snprintf(pattern, sizeof(pattern), %s, key); const char *p strstr(body, pattern); if (p NULL) return -1; p strlen(pattern); int i 0; while (*p *p ! i out_len - 1) { out[i] *p; } out[i] \0; return 0; }这个解析函数没有处理 URL 编码比如%20代表空格如果用户名允许特殊字符需要额外做解码。毕设场景下限制用户名只能是字母数字下划线就能绕开这个问题在注册校验里加一条正则判断即可。4.3 登录态校验与页面跳转受保护页面在返回内容前先查 sessionvoid handle_dashboard(int conn, http_req_t *req) { session_t *s session_find(req-session_id); if (s NULL) { // 未登录302 跳转到登录页 http_send_redirect(conn, /login.html); return; } // 已登录返回带用户名的页面 char page[512]; snprintf(page, sizeof(page), h2欢迎%s/h2p设备状态正常/p, s-username); http_send_html(conn, page); }302 重定向比直接返回 403 体验好用户不会看到一个空白错误页。session_find内部会检查过期时间过期就顺手把槽位清掉相当于惰性回收。5. 避坑与常见问题排查那些让你调一整天的地方5.1 ping 通但网页打不开现象电脑能 ping 通板子 IP但浏览器访问一直转圈或超时。原因通常是 LwIP 的MEM_SIZE或PBUF_POOL_SIZE太小TCP 连接建立后分配不到足够内存发送数据。解决把MEM_SIZE加到 16KB 以上PBUF_POOL_SIZE加到 16重新编译烧录。如果还不行检查TCP_WND是否设得比MEM_SIZE还大。5.2 LAN8720A 时钟配置错误导致完全不通现象网口灯不亮或者灯亮但完全无法通信。原因LAN8720A 需要 50MHz 参考时钟通常由 STM32 的 MCO 引脚输出如果 CubeMX 里没配置 MCO 或者时钟源选错PHY 根本不工作。解决确认RCC里MCO1配置为HSE或PLL输出 50MHz并且硬件上 MCO 引脚确实连到了 LAN8720A 的REF_CLK。这个坑硬件软件各占一半先量时钟再查代码。5.3 Flash 写入后数据读出来是 0xFF现象注册用户后重启账号消失。原因F407 写 Flash 前必须先擦除整个扇区如果只写不擦或者擦除的扇区和写入的地址不在同一个扇区数据写不进去。解决确认USER_FLASH_BASE落在扇区 11 的起始地址0x080E0000写入前调用HAL_FLASHEx_Erase擦除该扇区。另外注意擦除会阻塞不要在中断里调用。5.4 浏览器缓存导致页面不更新现象改了 HTML 重新烧录浏览器看到的还是旧页面。原因浏览器对静态资源做了缓存尤其是路径没变的情况下。解决在 HTTP 响应头里加Cache-Control: no-cache或者调试时用无痕窗口。更彻底的做法是给页面 URL 加版本号比如/login.html?v2但嵌入式场景下加响应头更省事。5.5 多客户端同时访问时死机现象两个人同时打开页面板子卡死或重启。原因裸机 LwIP 的 raw API 回调是单线程的如果 handler 里做了耗时操作比如 Flash 擦除会阻塞整个协议栈。解决把耗时操作拆成状态机分步执行或者临时切到 FreeRTOS 把 HTTP 处理放到独立任务里。毕设演示通常单人操作但答辩时老师可能会多设备同时访问提前想到这一点能避免现场翻车。6. 验收与答辩怎么证明这套 WebServer 真的跑通了验收分三块功能、稳定性、代码质量。功能验收按这个顺序走一遍每一步都要能复现验收项操作预期结果网络连通电脑 ping 板子 IP延迟稳定无丢包首页访问浏览器打开http://192.168.1.100/返回登录页样式正常注册流程填写新用户名密码提交提示注册成功Flash 中可读出记录登录流程用刚注册的账号登录跳转到 dashboard显示用户名会话保持刷新 dashboard 页面仍显示登录状态不跳回登录页会话过期等待 30 分钟后刷新自动跳回登录页错误处理用错误密码登录页面提示用户名或密码错误重复注册用已存在用户名注册提示用户名已存在稳定性验收我一般会做两件事一是连续快速刷新页面 50 次看是否出现内存泄漏导致死机二是注册 4 个用户把会话表占满再尝试登录看是否正确返回会话已满而不是崩溃。这两项能过基本说明内存管理和边界处理没问题。答辩时老师最常问的几个问题提前准备好答案为什么选 LwIP 不选其他协议栈、密码怎么存的安不安全、会话 ID 怎么生成的、Flash 擦写寿命怎么考虑。最后一个问题可以这样答F407 内部 Flash 擦写寿命约 1 万次按每天注册 10 个用户算能用好几年而且实际项目中会加磨损均衡或者换外部 Flash毕设场景下够用。代码质量方面至少保证每个函数都有入参校验所有strcpy换成strncpy所有数组访问带边界检查。答辩前用-Wall -Wextra编译一遍把警告清干净这个习惯能帮你提前发现不少隐藏 bug。从那以后我每次做嵌入式网络项目都会先把 ping 和内存参数这两关过了再写业务逻辑不然调到最后都不知道是协议栈的问题还是自己代码的问题。希望这套方案能帮你把毕设的骨架搭起来剩下的就是往里填细节了。本文还有配套的精品资源点击获取