
简介一套基于STM32F407和LAN8720A以太网模块的嵌入式WebServer完整例程适合正在学习嵌入式网络应用、RTOS或STM32外设驱动的开发者。例程在野火STM32F4xx开发板上实现通过网页控制LED、实时显示ADC采样数值与RTC时钟并给出LAN8720A物理层驱动、lwIP协议栈接入、RTC/ADC采集及HTTP应答等关键代码帮助读者打通从底层驱动到网页呈现的完整链路。压缩包共599个文件约17.22MB主要包含工程源码156个h头文件、130个c源文件、Keil工程配置文件uvprojx/uvoptx、编译生成的hex/axf镜像以及丰富的文档与网页素材html、htm、shtml等源码内还有fsdata.c等网页转换文件便于理解浏览器请求如何映射到片上资源。已有825人学习资料结构清晰既能作为WebServer入门的参考模板也可移植到其他F4系列板卡是一份实操性很强的嵌入式网络学习资料。 刚把 STM32F407 的以太网部分调通那会儿我最大的感受是嵌入式 WebServer 这东西听起来挺唬人拆开了其实就是“MCU 里跑一个精简版网站”。你不需要会写浏览器也不需要懂复杂的网络协议栈原理只要把硬件接对、把协议栈跑起来、再把 HTTP 的响应逻辑写清楚就能让手机、电脑在浏览器里直接看到设备状态、下发控制指令。这篇博文我就拿“基于 STM32F407 的嵌入式 WebServer 例程”这个经典项目把从硬件选型到网页跑通的全过程捋一遍。这套例程的目标很明确用一块 STM32F407 开发板、一颗 PHY 芯片配合 LwIP 协议栈在设备上建立一个 HTTP 服务。浏览器访问板子的 IP就能看到实时数据页面还能通过页面上按钮远程控制 LED、读取 ADC 电压。它适合刚接触嵌入式网络开发的工程师也适合想给产品加“本地网页配置”功能但不知道从哪下手的同学。理解了这一套后续不管是做智能家居网关、设备调试工具还是小型数据采集终端底层思路都是通用的。1. 项目本质让 MCU 变成一个“小型网站服务器”1.1 它到底解决了什么问题在很多工控、物联网场景里设备本身不带屏幕用户却需要查看设备参数、修改配置项。传统做法是上位机软件通过串口或者私有协议去访问但上位机软件需要单独安装、匹配操作系统、处理驱动非常麻烦。WebServer 的思路是完全换一个交互入口MCU 内部维护一个 HTTP 服务把状态页做成网页把参数读写做成 HTTP 接口。用户用浏览器输个 IP所有操作都在网页里完成不用装任何额外工具。STM32F407 自身主频 168MHz、带硬件以太网 MAC做一个小型 WebServer 绰绰有余。这个方案把“设备调试”“现场运维”的门槛拉得非常低这也是为什么直到现在各种融合了以太网的 MCU 例程里WebServer 永远挤在第一梯队。1.2 为什么选 F407而不是 F103 或者更高端的芯片F407 在 STM32 家族里属于“网络特性明显增强”的一代。它内置的以太网 MAC 支持 10/100Mbps可以通过 RMII 接口外接一颗便宜的 PHY比如 LAN8720A整体 BOM 成本很低。相比之下F103 虽然也能通过 SPI 接 W5500 之类的芯片实现网络功能但那属于“外部协议栈芯片”数据拷贝和命令交互的效率、灵活度都差一截。而 F407 直接走“MAC 外部 PHY”的方案LwIP 协议栈跑在 MCU 内部以太网帧收发由硬件 DMA 完成CPU 干预少性能上限高。512KB Flash、192KB SRAM 的配置对 LwIP 这种轻量协议栈来说也非常充裕甚至还能跑 FreeRTOS把 WebServer 放在独立任务里跟其他业务逻辑互不干扰。1.3 这套例程适合谁如果你是刚转嵌入式网络开发的新手这套例程是最好的“第一块跳板”。不需要先啃完整的 TCP/IP 卷也不用从零写协议栈只要跟着配置、理解调用关系、会改页面和参数就能在几天内把项目跑起来。如果你是做产品的工程师这套例程则提供了一个很好的原型后续按需替换 PHY、加文件系统、加加密认证都是在这个骨架上继续长肉。2. 整体方案与关键技术选型2.1 三层架构硬件、协议栈、应用层我习惯把嵌入式 WebServer 拆成三层看待这个分层能帮你快速定位问题底层硬件接口层。包括 STM32F407 的以太网 MAC、外接 PHY 芯片、RJ45 连接器、网络变压器以及初始化这些外设的 HAL 驱动。中间层协议栈层。任务很单纯就是把 IP、TCP、UDP、ARP、ICMP 这些协议实现出来。例程里用的是 LwIP它专门为嵌入式系统设计占用资源小API 也灵活。应用层HTTP 服务与页面逻辑。监听 TCP 80 端口处理浏览器发来的 GET/POST 请求返回 HTML 页面或 JSON 数据。这种分层最大的好处是调试时可以“逐层验证“。ping 不通就查底层端口连不上就查 LwIP 配置网页内容不对就查应用层逻辑不互相甩锅。2.2 LwIP 协议栈配套方案为什么成熟嵌入式可选的网络协议栈不少但 LwIP 的综合性价比最高。它开源、资料多、被 ST 官方集成进了 CubeMX。你新建工程时勾一下 LWIPHAL 库版本、以太网驱动、内存管理配置就都生成好了不用自己处理寄存器级的细节。LwIP 还提供了三种 API 模式raw API裸机回调、netconn API线程化 BSD-socket 风格、socket API完整 socket 接口。例程里我推荐 netconn API因为它逻辑直观配合 FreeRTOS 的线程模型天然适合 WebServer 这种“一个客户端连接处理一下”的交互模式。2.3 HTTP 服务端的三条实现路线应用层怎么写是新手最容易纠结的地方。我梳理一下常见做法路线一用 LwIP 自带 httpd。它包含 SSI服务端嵌入、CGI通用网关接口机制适合动态数据量不大但页面比较多的场景。缺点是 httpd 的源码文件在 lwip-contrib 里CubeMX 默认不帮你加全需要手动拷贝工程。路线二自己写一个精简 HTTP 解析器。只解析请求行、URL、请求头的前几项然后按需返回页面或数据。代码量不大、逻辑完全可控特别适合学习。我这次例程就跑在这条路线上。路线三集成 Mongoose / CivetWeb 等第三方库。功能全面支持 WebSocket、HTTPS但资源占用明显更大F407 上也能跑不过更像“移植完整 Web 框架”不建议第一版就上。如果是做产品原型我建议先用路线二把页面和接口调通再根据需求升级到 httpd 或第三方库。起步越简单后面越有信心。3. 硬件要点F407 LAN8720A 的网络最小系统3.1 RMII 接口与引脚分配F407 以太网 MAC 支持 MII 和 RMII 两种接口模式。MII 需要 16 根数据线RMII 只要 7 根换算下来就是“把 MAC 和 PHY 之间的通路从 4 位数据线变成 2 位”。对 F407 这种引脚资源珍贵的芯片我几乎无脑选 RMII。RMII 模式下 F407 与 LAN8720A 的标准接线如下不同开发板略有差异但 CubeMX 自动分配基本一致信号功能MCU 引脚方向ETH_RMII_REF_CLKPA1输入50MHz 参考时钟ETH_RMII_CRS_DVPA7输入ETH_RMII_RXD0PC4输入ETH_RMII_RXD1PC5输入ETH_RMII_TX_ENPB11输出ETH_RMII_TXD0PB12输出ETH_RMII_TXD1PB13输出ETH_RMII_MDCPC1输出ETH_RMII_MDIOPA2双向注意 RMII 收发共用同一个 50MHz 参考时钟。这个时钟必须稳定起不来或者频率不对PHY 就永远协商不上表现就是插上网线Link 指示灯不亮LwIP 的 netif 一直报 link down。3.2 时钟方案与 PHY 地址LAN8720A 的参考时钟有两种常见来源方案 APHY 使用外部 25MHz 晶振内部倍频到 50MHz然后从 CLKOUT 脚输出 50MHz 给 MCU 的 PA1。这也是市面上大多数 F407 LAN8720A 开发板的标准做法。方案 BMCU 的 MCO1 输出 50MHz 时钟给 PHY 的 REF_CLK 脚同时 PHY 使用外部 50MHz 有源晶振。这种做法不常用但对部分要求时钟同源的设计有帮助。我在例程里采用方案 A因为它最通用。硬件上还需要特别注意 PHY 地址LAN8720A 的地址由 PHYAD0 引脚的电平决定默认拉低是 0x00拉高是 0x01。CubeMX 里 ETH 外设配置的 PHY Address 必须跟硬件一致否则 MDIO 读出来的 PHY ID 全是错的后续 Link 检测、自动协商都无从谈起。3.3 复位、电源与网络变压器的坑LAN8720A 的复位设计很容易被忽略。有些开发板把 PHY 的 NRST 简单接到 RC 复位电路上上电时序没问题但如果你的 PHY 在主控复位之后再复位就必须用一个 GPIO 控制 NRST复位结束后保持至少 3ms 低电平再拉高之后延时 150ms~1s 再操作 MDIO。否则容易出现“代码怎么调都读不到 PHY ID”的尴尬。另外RMII 接口的 50MHz 时钟质量直接影响收发。PCB 走线时 REF_CLK 尽量短、少打过孔远离开关电源。LAN8720A 的数字电源和模拟电源建议各加一颗 100nF 去耦电容并在系统电源入口处预留磁珠滤波。RJ45 连接器前方一般自带网络变压器注意变压器中心抽头旁路电容的接法不然信号幅度不足会导致偶发丢包。4. LwIP 移植与内存调优4.1 CubeMX 一键生成的骨架用 STM32CubeMX 配置 F407 的 ETH 和 LWIP 非常省心它会自动生成eth.cMAC 和 DMA 描述符的初始化lwip.cLwIP 协议栈的初始化、网卡接口配置ethernetif.c底层收发回调、中断与协议栈之间的数据传递你要做的核心工作就是先把时钟树配对ETH 选择 RMII 模式、填好 PHY 地址和 PHY 型号然后使能 LWIP 并在中间件设置里填静态 IP。F407 的普通 SRAM 有 128KB另有 64KB CCM需要特别提醒以太网 DMA 无法访问 CCMLwIP 的缓冲、描述符这些内存必须放在普通 SRAM 里。别开到 CCM否则莫名其妙的内存故障会让人怀疑人生。4.2 内存池参数网页卡不卡就看这里LwIP 的内存配置主要在lwipopts.h或者 CubeMX 的 LWIP 中间件设置里。我以 VET6192KB SRAM为例给一组实测可用的参数参数推荐值说明MEM_SIZE12KB ~ 20KBLwIP 堆内存存放 PCB 控制块、IP 分片等PBUF_POOL_SIZE16 ~ 32接收/发送 pbuf 池数量越大越抗突发流量PBUF_POOL_BUFSIZE1536单包缓冲大小必须大于一个以太网帧TCP_MSS1460单段最大负载MTU 1500 减去 IP/TCP 头TCP_WND2 * TCP_MSS接收窗口决定 TCP 吞吐率TCP_SND_BUF2 * TCP_MSS发送缓冲太小会导致大页面发送阻塞TCP_SND_QUEUELEN6 * TCP_SND_BUF / TCP_MSS发送队列长度够用即可这组配置的总体思路是优先保证 pbuf 池足够因为网络收发包是高频操作TCP_WND 和 TCP_SND_BUF 不用开太大F407 处理简单页面足够了MEM_SIZE 别舍不得HTTP 响应拼接时会在协议栈内分配内存太小会反复tcp_write失败。4.3 多线程与中断如何协作例程如果跑 FreeRTOSLwIP 里通常有两个后台任务tcpip_thread负责协议栈处理ethernetif_thread负责从网卡读取数据包。收到以太网帧后底层中断或线程把 pbuf 丢进协议栈邮箱tcpip_thread再继续处理。要特别注意以太网中断回调函数里绝对不能执行耗时的 HTTP 业务逻辑。正确做法是只做标志置位或者发送信号量让应用层线程去完成页面拼接、GPIO 控制这类操作。很多初学者把HAL_ETH_GetReceivedFrame和http_handle全写在中断里导致中断嵌套过深系统不定时死机排查起来非常痛苦。5. 实操从 CubeMX 到网页跑通的完整过程5.1 工程配置步骤我用一个具体步骤清单来说照着操作基本不会跑偏在 CubeMX 新建工程选择芯片型号 STM32F407VET6配置外部高速晶振 HSE调试口选 SWD。时钟树里把系统主频配到 168MHzAPB1 42MHz、APB2 84MHz确保 ETH 的时钟源正确。打开 ETH 外设模式选 RMIIPHY Address 按开发板填 0 或 1PHY 芯片型号 LAND8720A或 Generic。开启 FreeRTOS建议默认 CMSIS_V1在任务列表里添加一个httpServerTask栈大小给 1024 字就够。打开 LWIP按表 4.2 的参数设置内存IP 地址设静态192.168.1.10/24网关192.168.1.1。生成工程后在 app 层把eth.c里 PHY 复位脚如果有 GPIO 控制正确初始化保证 PHY 上电完成。5.2 自写 HTTP 服务的核心代码我用 netconn API 写了一个最精简的 HTTP 服务线程。结构很简单创建 TCP 连接、监听 80 端口、接受客户端、读取请求、解析 URL、返回内容。static void http_server_thread(void *argument) { struct netconn *server netconn_new(NETCONN_TCP); struct netconn *client; err_t err; netconn_bind(server, IP_ADDR_ANY, 80); netconn_listen(server); for (;;) { err netconn_accept(server, client); if (err ! ERR_OK) continue; http_handle_client(client); } }关键在于http_handle_client它要负责读请求、判断路径、写响应。核心代码大致如下static void http_handle_client(struct netconn *client) { struct netbuf *inbuf; char *buf; u16_t len; const char *html html.../html; char response[512]; if (netconn_recv(client, inbuf) ERR_OK) { netbuf_data(inbuf, (void **)buf, len); if (strstr(buf, GET /led1) ! NULL) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); } else if (strstr(buf, GET /led0) ! NULL) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); } snprintf(response, sizeof(response), HTTP/1.1 200 OK\r\n Content-Type: text/html; charsetutf-8\r\n Content-Length: %d\r\n Connection: close\r\n Cache-Control: no-cache\r\n \r\n %s, strlen(html), html); netconn_write(client, response, strlen(response), NETCONN_COPY); } netconn_close(client); netconn_delete(client); }这段逻辑有两个关键点一是 HTTP 头必须带Content-Length否则部分浏览器会一直等待响应体结束二是Connection: close让每次请求处理完就断开避免客户端复用连接引发 TIME_WAIT 堆积。5.3 页面内容和动态数据怎么组装例程一开始直接用字符串拼 HTML页面简单还可以但页面一大就会非常难维护。我的做法是在工程里新建一个webpage.c把写好的 HTML 文件通过xxd -i index.html webpage.c转换成一个 C 数组然后放到 Flash 里。动态数据不能写死在 HTML 里。我先在页面里放一个div idadc_value--/div然后让页面每秒通过 JavaScript 请求一个 JSON 接口GET /api/status服务器端收到这个请求后构建 JSON 字符串返回例如{ adc: 2048, temp: 36.5 }前端再用fetch或者XMLHttpRequest更新对应元素。这样数据和页面展示彻底解耦后续加曲线、加按钮都很方便。很多设备配置页配不上“动态效果”大多就是栽在“只想在页面里加变量”但又不想处理 CGI 的路子上。5.4 编译下载与验证方法工程编译通过后烧录进板子用网线把 F407 和电脑直连电脑网卡设置静态 IP192.168.1.2/24然后分三步验证用ping 192.168.1.10验证网络是否通。ping 不通先查 PHY 时钟和 Link 状态别急着查代码。用浏览器访问http://192.168.1.10能看到页面基本就成功一半。点击页面 LED 控制按钮观察开发板 LED 动作同时串口打印请求日志确认请求确实到了 MCU。我自己的习惯是同时在电脑上开一个网络抓包工具Wireshark来看请求和响应帧遇到 404、内容缺失、格式错误都能立刻看出是哪一端的问题。6. 实战中高频问题和排查方法6.1 ping 不通 / Link 灯不亮这是最常见的问题90% 出在硬件或 PHY 初始化顺序上。排查思路按优先级确认 PHY 电源、复位正常。用 GPIO 控制 NRST 的话先示波器量复位脚波形。确认 50MHz REF_CLK 有没有输出到 MCU。LAN8720A 的 CLKOUT 频率不对多半是晶振或倍频配置出错。确认 PHY Address 与 CubeMX 一致。可以通过读 PHY ID寄存器 2/3来验证如果读回来全是 0 或 0xFFFF就是 MDIO 通信问题。确认 RMII 模式下网络变压器接法正确尤其是中心抽头电压。6.2 能 ping 通但浏览器打不开页面能 ping 通说明 IP/TCP 底层已经通了问题在应用层的 TCP 端口监听或 HTTP 响应。重点检查HTTP 服务线程是否成功创建并netconn_listen在 80 端口。是否开启了防火墙把客户端 IP 加入例外。浏览器是否能正常访问静态 IP 下的其他端口排除浏览器缓存问题。用 telnet 或者网络工具直接输入GET / HTTP/1.1试一下看返回内容。6.3 页面出现但数据一直不刷新页面首次打开正常、后续数据不更新大多是 HTTP 缓存导致。浏览器会对重复请求复用本地缓存如果不希望缓存在响应头里加Cache-Control: no-cache就行。还有一种情况是前端 JS 请求的 URL 对大小写敏感服务器端解析用的是字符串匹配路径对不上就返回了404或者默认页面。6.4 刷几次页面之后 MCU 内存耗尽高频刷新页面时TCP 连接会频繁建立和销毁。如果每次都netconn_write大量数据pbuf 池可能被短暂耗尽。除了调大PBUF_POOL_SIZE还要检查每个连接是否都走完了 recv - write - close - delete 的完整流程。如果粗心漏了netconn_delete内存泄漏几轮之后必然崩溃。6.5 网络空闲时 MCU 偶发死机这类问题频发在 CubeMX 默认工程上最大嫌疑是中断优先级配置。以太网接收中断、FreeRTOS 的 SysTick 中断、ETH DMA 中断之间如果优先级分配不合理可能发生中断嵌套或者锁死。我习惯把 ETH 中断优先级调到比 SysTick 低但又要高于普通外设中断并且确保 FreeRTOS 配置中PRIO_BITS与实际中断优先级分组一致。现象最可能原因快速处理完全 ping 不通PHY 时钟或复位示波器量 50MHz检查 NRSTPHY ID 读不出来MDIO 配置、PHY 地址核对 CubeMX PHY Address页面打不开80 端口没监听检查 httpServer 线程状态数据不更新HTTP 缓存加Cache-Control: no-cache多次刷新后卡死pbuf 或内存耗尽调大PBUF_POOL_SIZE、检查连接释放最后说一点我个人的体会这套 F407 WebServer 例程我前前后后给好几个项目打过底。第一次调通那个瞬间感觉就是一个不带屏幕的裸板子居然能跟浏览器对话了这种“设备互联网化”的跃迁感很直接。后来做产品我发现很多复杂功能——用户认证、参数持久化、配置导出、远程升级——都是在这个 WebServer 骨架上往上加的。所以我的建议是别只把例程当成“跑一个网页”要把 HTTP 请求处理、动态数据接口、内存管理机制真正吃透。这几样吃透了以后不管换什么 MCU、换什么协议栈都能很快上手。最后再分享一个小技巧调试阶段把每个 HTTP 请求都通过串口打一条日志带上 URL 和返回状态码比任何调试器都直观。本文还有配套的精品资源点击获取