资讯动态

STM32F407+LAN8720A实战:LwIP嵌入式WebServer登录注册

发布时间:2026/10/19 13:42:41 来源:尧图企业网站定制
简介这份资源面向嵌入式开发初学者与物联网方向学习者围绕STM32F407微控制器搭配LAN8720A以太网PHY芯片构建一个可运行于开发板上的嵌入式Web服务器并实现简单的登录与注册功能。内容涉及TCP/IP协议栈如lwIP、HTTP请求解析、用户数据存储、HTML表单页面设计以及密码加密等安全考量是理解嵌入式网络编程与前后端协作的完整实践案例。压缩包共588个文件约16.37MB以c与h源码为主辅以o、d、crf等编译中间文件以及htm、html、shtml网页文件、readme说明和hex固件等便于直接烧录与二次开发。目前已有881人学习下载。通过该工程读者可掌握PHY初始化、MAC地址配置、HTTP服务器框架搭建与表单响应生成等关键环节并借助Keil调试与网络抓包工具排查通信问题适合作为课程设计或物联网入门的参考项目。1. 从一块吃灰的开发板说起STM32F407 上跑 WebServer 到底能做什么手头有块 STM32F407 开发板网口旁边焊着 LAN8720A 这颗 PHY 芯片很多人第一次把网线插上去、ping 通之后就不知道下一步该干嘛了。其实这套硬件组合最实用的一个练手方向就是在片子上跑一个嵌入式 WebServer用浏览器打开一个页面输入账号密码完成登录注册把「嵌入式 HTML」这条链路彻底打通。它适合刚学完以太网外设、想找个完整项目练手的嵌入式新手也适合需要给设备加一个轻量配置页面的开发者——毕竟不是所有设备都值得上一套 Linux。这篇就按「能干什么 → 怎么跑 → 怎么改」的顺序把这份 STM32F407 LAN8720A 的 WebServer 登录注册项目拆开讲清楚。2. 项目架构与目录结构LwIP 协议栈怎么和 HTTP 服务对接2.1 整体分层从网线到浏览器页面这个项目的本质是在 STM32F407 上跑一个精简的 TCP/IP 协议栈LwIP在它之上再实现一个极简的 HTTP 服务端最后由 HTTP 服务端去处理浏览器发来的登录、注册表单请求。整条链路可以拆成四层来看硬件层STM32F407 的 ETH 外设通过 RMII 接口连接 LAN8720ALAN8720A 再接到 RJ45 网口。F407 内部自带 MAC 控制器LAN8720A 只负责物理层收发。驱动层ETH 驱动 LAN8720A 的初始化包括复位、PHY 地址探测、自协商、RMII 时钟配置。这一层跑通的标准就是能 ping 通。协议层LwIP 协议栈提供 TCP/UDP/IP/ICMP 等。WebServer 用的是 TCP监听 80 端口。应用层HTTP 服务端 登录注册业务逻辑。它解析 HTTP 请求行和请求体判断是 GET 还是 POST然后返回对应的 HTML 页面或处理结果。分层的好处是排查问题时能快速定位ping 不通就是驱动层的事能 ping 通但页面打不开就是 LwIP 或 HTTP 层的事页面能打开但登录失败就是业务逻辑的事。2.2 目录结构文件都放在哪一份典型的工程目录大致长这样不同模板可能略有差异但核心文件跑不出这几类Project/ ├── Core/ │ ├── Src/ │ │ ├── main.c // 主函数初始化 主循环 │ │ ├── ethernetif.c // 网卡驱动接口LwIP 和 ETH 的桥梁 │ │ └── gpio.c // GPIO 初始化 │ └── Inc/ │ └── main.h ├── Drivers/ │ ├── STM32F4xx_HAL_Driver/ // HAL 库 │ └── CMSIS/ // 内核相关 ├── LWIP/ │ ├── App/ │ │ └── http_server.c // HTTP 服务端核心逻辑 │ ├── Target/ │ │ └── ethernetif.c // 网卡底层收发 │ └── lwipopts.h // LwIP 配置裁剪 ├── Middlewares/ │ └── Third_Party/LwIP/ // LwIP 源码 └── MDK-ARM/ 或 STM32CubeIDE/ // 工程文件关键要盯住三个文件ethernetif.c负责把网卡收到的数据递给 LwIPhttp_server.c负责处理 HTTP 请求lwipopts.h决定协议栈开多大内存、支持多少连接。这三个文件改明白了项目基本就吃透了。2.3 内存与连接数配置lwipopts.h 里几个不能乱动的参数LwIP 在单片机上跑内存是硬约束lwipopts.h里几个参数直接决定 WebServer 能不能稳定工作参数典型值作用调小的后果MEM_SIZE16*1024LwIP 堆总大小页面大一点就分配失败PBUF_POOL_SIZE16pbuf 池数量并发请求直接丢包TCP_SND_BUF2*TCP_MSSTCP 发送缓冲大页面发不完整TCP_WND4*TCP_MSSTCP 接收窗口传输效率骤降MEMP_NUM_TCP_PCB5最大 TCP 连接数多开一个浏览器标签就拒绝我一般会先把MEM_SIZE给到 16KB 以上因为一个带 CSS 的登录页面本身就不小堆太小会出现「页面加载一半卡住」这种玄学问题。MEMP_NUM_TCP_PCB至少留 5 个浏览器会同时开好几个连接去请求 favicon、css、js只留 1 个的话页面永远转圈。3. 跑起来从 ping 通到浏览器弹出登录页3.1 硬件连接与网络参数确认先把硬件接对。LAN8720A 和 F407 之间是 RMII 接口需要确认这几根线REF_CLK50MHz 时钟、TXD0/TXD1、RXD0/RXD1、TX_EN、CRS_DV、MDIO、MDC。很多板子把 REF_CLK 由外部晶振提供也有的是从 STM32 的 MCO 引脚输出这个必须和代码里的配置对上否则 PHY 初始化直接失败。网络参数在代码里通常是写死的先找到这几行/* 静态 IP 配置位于 main.c 或 ethernetif.c 中 */ #define IP_ADDR0 192 #define IP_ADDR1 168 #define IP_ADDR2 1 #define IP_ADDR3 100 #define NETMASK_ADDR0 255 #define NETMASK_ADDR1 255 #define NETMASK_ADDR2 255 #define NETMASK_ADDR3 0 #define GW_ADDR0 192 #define GW_ADDR1 168 #define GW_ADDR2 1 #define GW_ADDR3 1把开发板 IP 设成和电脑同网段比如电脑是 192.168.1.10板子就设 192.168.1.100。注意别和路由器 DHCP 池冲突否则会出现「一会儿能 ping 通一会儿不通」的诡异现象。3.2 编译烧录与 ping 测试用 Keil 或 STM32CubeIDE 打开工程编译前先确认两件事一是lwipopts.h里的LWIP_DHCP如果没开就走静态 IP二是ethernetif.c里的 PHY 地址要和硬件匹配LAN8720A 默认地址是 0但有些板子会拉高变成 1。烧录完成后把网线一头插开发板一头插电脑或者同一个交换机。电脑端先关掉无线只留有线的那个网卡然后# Windows 下测试连通性 ping 192.168.1.100 # Linux/Mac 下 ping -c 4 192.168.1.100能稳定返回就说明 ETH 驱动、LAN8720A、LwIP 三层都通了。如果 ping 不通先看电脑网卡是不是自动获取了别的网段地址再检查网线是不是插在 LAN8720A 对应的那个口上。3.3 浏览器访问与登录注册流程ping 通之后直接在浏览器地址栏输入http://192.168.1.100正常情况下会返回一个登录页面。这个页面的 HTML 是存在单片机里的可能是以字符串常量形式写在http_server.c里也可能是放在外部 Flash 或 SD 卡里按需读取。登录注册的交互流程是这样的浏览器 GET 请求/服务端返回登录页 HTML。用户在页面填账号密码点登录浏览器 POST 到/login。服务端解析 POST 请求体取出username和password字段。和存储的账号密码比对成功返回欢迎页失败返回错误提示。注册流程类似POST 到/register服务端把新账号存起来。存储方式取决于项目实现简单版本可能就是一个全局数组掉电就丢讲究一点的会写到内部 Flash 的某个扇区或者外挂一颗 EEPROM。这个在二次开发时是要重点改的地方。4. 核心代码拆解HTTP 请求怎么被解析和响应4.1 HTTP 服务端主循环接收、解析、分发HTTP 服务端的核心是一个循环等待连接、读取请求、解析请求行、根据路径分发处理、发送响应、关闭连接。用 LwIP 的 raw API 写出来大概是这样/* http_server.c 中的连接回调LwIP 收到新连接时调用 */ static err_t http_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) { if (p NULL) { /* 对端关闭连接 */ tcp_close(tpcb); return ERR_OK; } /* 把 pbuf 链里的数据拷到连续缓冲区方便按字符串解析 */ char *req (char *)malloc(p-tot_len 1); pbuf_copy_partial(p, req, p-tot_len, 0); req[p-tot_len] \0; /* 解析请求行例如 POST /login HTTP/1.1 */ if (strncmp(req, GET / , 6) 0) { send_login_page(tpcb); /* 返回登录页 */ } else if (strncmp(req, POST /login, 11) 0) { handle_login(tpcb, req); /* 处理登录 */ } else if (strncmp(req, POST /register, 14) 0) { handle_register(tpcb, req); /* 处理注册 */ } else { send_404(tpcb); /* 其他路径返回 404 */ } free(req); tcp_recved(tpcb, p-tot_len); /* 告诉 LwIP 数据已处理 */ pbuf_free(p); return ERR_OK; }这段代码有几个关键点。pbuf_copy_partial把分散的 pbuf 拼成连续内存因为 HTTP 是文本协议按字符串处理最省事。tcp_recved必须调用否则 LwIP 的接收窗口不会前移传几个包之后就卡死了。malloc的大小是p-tot_len 1多出来的 1 是给字符串结束符\0的这个不加会出现越界读属于血泪经验。4.2 登录请求的解析从 POST 请求体里抠出账号密码POST 请求的账号密码在请求体里格式是usernamexxxpasswordyyy。解析函数要做的就是找到请求体和响应体之间的空行然后按和切分/* 从 POST 请求体中提取指定字段的值 */ static int get_form_value(const char *req, const char *key, char *out, int out_len) { /* 请求头和请求体之间是一个空行 \r\n\r\n */ const char *body strstr(req, \r\n\r\n); if (body NULL) return -1; body 4; /* 跳过空行 */ /* 构造 key 用于查找 */ char pattern[32]; snprintf(pattern, sizeof(pattern), %s, key); const char *start strstr(body, pattern); if (start NULL) return -1; start strlen(pattern); /* 值的结束位置是下一个 或字符串结尾 */ const char *end strchr(start, ); int len (end NULL) ? strlen(start) : (end - start); if (len out_len) len out_len - 1; memcpy(out, start, len); out[len] \0; return 0; }strstr找\r\n\r\n是定位请求体的标准做法HTTP 协议规定头和体之间必须有一个空行。snprintf拼key是为了避免手写字符串拼接出错。值的结束判断要同时考虑和字符串结尾两种情况只判断的话最后一个字段会读越界。这个函数在登录和注册里都能复用注册时多调几次取不同字段就行。4.3 账号存储与比对一个够用但不优雅的实现最简单的账号存储就是一个结构体数组注册时往空位写登录时遍历比对#define MAX_USERS 8 #define USERNAME_LEN 16 #define PASSWORD_LEN 16 typedef struct { char username[USERNAME_LEN]; char password[PASSWORD_LEN]; uint8_t used; /* 该槽位是否已被占用 */ } user_t; static user_t g_users[MAX_USERS]; /* 注册找一个空槽位写入 */ static int user_register(const char *name, const char *pwd) { for (int i 0; i MAX_USERS; i) { if (!g_users[i].used) { strncpy(g_users[i].username, name, USERNAME_LEN - 1); strncpy(g_users[i].password, pwd, PASSWORD_LEN - 1); g_users[i].used 1; return 0; } } return -1; /* 用户已满 */ } /* 登录遍历比对 */ static int user_login(const char *name, const char *pwd) { for (int i 0; i MAX_USERS; i) { if (g_users[i].used strcmp(g_users[i].username, name) 0 strcmp(g_users[i].password, pwd) 0) { return 0; /* 登录成功 */ } } return -1; /* 账号或密码错误 */ }这个实现够跑通流程但有两个明显问题一是密码明文存储二是掉电丢失。做产品肯定不行但作为学习项目先把流程跑通再谈优化。真要改密码至少做个简单哈希存储换成内部 Flash 的某个扇区这些在二次开发章节会展开说。4.4 响应发送把 HTML 页面塞进 TCP 流发送响应时要注意 HTTP 响应头必须带Content-Length否则浏览器不知道页面传完没有会一直转圈/* 发送 HTTP 响应body 是页面内容 */ static void http_send(struct tcp_pcb *tpcb, const char *status, const char *body) { char header[128]; int body_len strlen(body); /* 拼响应头Content-Length 必须和实际长度一致 */ int hlen snprintf(header, sizeof(header), HTTP/1.1 %s\r\n Content-Type: text/html; charsetutf-8\r\n Content-Length: %d\r\n Connection: close\r\n \r\n, status, body_len); tcp_write(tpcb, header, hlen, TCP_WRITE_FLAG_COPY); tcp_write(tpcb, body, body_len, TCP_WRITE_FLAG_COPY); tcp_output(tpcb); /* 立即把数据推出去 */ }TCP_WRITE_FLAG_COPY表示让 LwIP 自己拷贝一份数据这样函数返回后 header 和 body 的栈内存被回收也不影响发送。tcp_output是必须的不调用的话数据会留在发送缓冲里等下一个包凑一起浏览器那边就一直等。Connection: close表示响应完就关连接省得维护 keep-alive 状态对单片机来说更省内存。5. 避坑与排查那些让页面打不开的常见问题5.1 ping 不通但网口灯是亮的现象网口指示灯正常闪烁但电脑 ping 开发板完全没反应。原因大概率是 PHY 地址配错了或者 RMII 的 REF_CLK 没有正确供给。LAN8720A 需要 50MHz 时钟如果板子上没有独立晶振就得靠 STM32 的 MCO 输出而 MCO 的时钟源配置经常被忽略。解决先用示波器量 REF_CLK 引脚有没有 50MHz 波形没有就先解决时钟。有波形但还 ping 不通就把 PHY 地址从 0 试到 1LAN8720A 的 PHYAD0 引脚状态决定地址硬件上拉就是 1下拉就是 0。5.2 能 ping 通但浏览器打开是空白页现象ping 正常浏览器输入 IP 后一直转圈最后超时或显示空白。原因HTTP 响应里Content-Length和实际 body 长度不一致或者根本没发Content-Length。浏览器收不到长度信息就会一直等。解决检查http_send里body_len的计算确认strlen(body)和实际发送的字节数一致。如果 body 里有中文注意strlen返回的是字节数不是字符数UTF-8 下一个中文占 3 字节这个一般不会错但拼接字符串时容易多算或少算。5.3 登录点下去没反应或者返回 404现象登录页能打开填完账号密码点登录页面跳转后显示 404 或者干脆没反应。原因表单的action路径和服务端解析的路径对不上。比如 HTML 里写的是form action/login methodpost但服务端判断的是POST /login.html那就匹配不上。解决把 HTML 里的action和服务端strncmp的字符串对齐。建议在服务端加一句调试打印把收到的请求行原样输出到串口一眼就能看出路径差在哪。5.4 连续刷新几次后板子死机现象第一次打开页面正常刷新几次之后板子不响应了ping 也断。原因TCP 连接没正确关闭MEMP_NUM_TCP_PCB被耗尽。每次刷新浏览器会开新连接旧连接如果没走tcp_closePCB 就一直占着。解决在http_recv里处理完请求后确保调用tcp_close并且tcp_recved和pbuf_free都不能漏。另外把MEMP_NUM_TCP_PCB从默认的 5 调到 8 左右给浏览器并发留点余量。5.5 注册成功但重启后账号没了现象注册完能登录板子断电重启后账号消失。原因账号存在 RAM 里掉电自然丢。这是设计如此不是 bug。解决如果项目要求持久化把g_users数组写到 STM32 内部 Flash 的某个扇区。注意 Flash 写之前要先擦除整个扇区而且擦写次数有限不要每次注册都写可以攒几个一起写或者加个手动保存按钮。6. 二次开发把登录注册改成设备配置页的实战思路这个项目最有价值的二次开发方向是把登录注册那套表单处理逻辑改造成设备参数配置页面。因为登录注册本质上就是「浏览器提交表单 → 单片机解析字段 → 执行动作」配置页面也是同一个套路只是字段从账号密码变成了 IP 地址、端口号、采集周期这些。改造的第一步是换页面。把send_login_page里的 HTML 字符串换成配置表单字段名改成ip、port、interval之类。这里有个技巧HTML 不要用snprintf一点点拼直接写成一个大的字符串常量编译器会把它放到 Flash 里不占 RAM。我一般会这样组织/* 配置页面整个 HTML 作为常量存 Flash */ static const char config_page[] !DOCTYPE htmlhtmlheadmeta charsetutf-8 titleDevice Config/title/headbody h2Device Configuration/h2 form action/save methodpost IP: input nameip value%sbr Port: input nameport value%dbr Interval: input nameinterval value%dbr input typesubmit valueSave /form/body/html;注意这个常量里有%s和%d发送前要用snprintf把当前配置填进去生成一个临时缓冲区再发。临时缓冲区大小要按页面最大长度给够一般 1KB 到 2KB 足够。第二步是改解析。get_form_value函数不用动直接复用只是 key 从username换成ip。解析出来的字符串要转成数值IP 地址按.切分四段分别atoi端口和周期直接atoi就行。转换完做范围校验IP 每段 0 到 255端口 1 到 65535超出范围就返回错误页面。第三步是存储。配置参数和账号不一样它必须持久化否则重启就丢。写到内部 Flash 时要注意STM32F407 的 Flash 扇区比较大最小 16KB写几个字节也要擦一整个扇区所以最好把配置结构体对齐到一个扇区起始地址擦写时整块处理。写之前先解锁写完再上锁中间别被打断。第四步是加个「重启生效」的提示。很多配置改完需要重启网络才能生效页面上给个提示或者干脆在保存后自动软重启。软重启用NVIC_SystemReset()就行但重启前要把配置写完并确认落盘。验证方法很简单改完 IP 保存断电重启看新 IP 能不能 ping 通。能通就说明整条链路没问题。如果 ping 不通先用串口打印当前配置确认 Flash 读出来的值是对的再排查网络初始化是不是用了旧值。从那以后我每次改这类配置页面都会先在串口把解析出来的字段原样打印一遍确认「浏览器发的」和「单片机收的」完全一致再往下走存储和生效逻辑。这个习惯帮我省了无数次来回烧录的时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑