尧图精选

STM32 LWIP HTTPD服务器搭建实战:5步避坑指南

🕒 发布时间:2026/9/19 1:54:47 📁 来源:尧图网络
1. 为什么要在 STM32 上跑一个 HTTP 服务器很多人第一次听到在单片机上跑 HTTP 服务器会觉得有点小题大做——一个几十块钱的 STM32F103Flash 才 64KB、RAM 才 20KB怎么可能像电脑一样对外提供网页服务但实际做过工业网关、设备配置页、数据采集终端的人都知道这个需求非常真实设备部署到现场之后你不可能每次都抱着笔记本插串口去改参数最省事的办法就是让设备自己开一个网页用手机或电脑浏览器连上去改 IP、改采集周期、看实时数据改完点保存完事。STM32 上实现这件事的标准路径就是LWIP HTTPD。LWIPLightweight IP是专门为嵌入式场景裁剪过的 TCP/IP 协议栈完整实现可以压到 40KB 左右的 Flash 和十几 KB 的 RAM配合 STM32 内置的以太网外设比如 F107、F407、F429、H723 这些带 ETH MAC 的型号再加上一颗 PHY 芯片常见的 LAN8720A、DP83848硬件链路就齐了。软件层面CubeMX 已经把 LWIP 的移植工作做成了勾选项HTTPD 组件也内置在中间件里理论上点几下就能生成一个能跑的工程。但理论上和实际上之间隔着一堆编译错误。我自己第一次搭的时候Keil 报了十几个错从sys_arch.h找不到到lwipopts.h重复定义再到ethernetif.c里的PHY读写超时前后折腾了大半天。后来帮别人看工程发现大家踩的坑高度重合——基本都是那几个PHY 地址不对、时钟配置漏了、LWIP_HTTPD相关的宏没开、fsdata.c没生成或者路径不对。这篇内容就是把这套流程拆成 5 个能落地的步骤每一步说清楚为什么这么做和这里最容易出什么问题。适合两类人看一类是刚接触 STM32 网络功能、想快速跑通一个 Demo 的另一类是老手但每次换芯片型号都要重新踩一遍坑、想找个清单对照的。代码基于 STM32F407 LAN8720A Keil MDK 环境CubeMX 版本用 6.xLWIP 版本 2.1.2其他型号思路一致差异点我会单独标出来。2. 动手之前先把硬件链路和软件版本理清楚2.1 硬件最小系统需要哪些东西跑 LWIP 不是光有 STM32 就行以太网这部分是MCU PHY 网络变压器 RJ45四件套。以 F407 为例MCU 通过RMII 接口比 MII 少一半引脚连到 PHYRMII 需要的外部时钟是 50MHz这个时钟可以由 MCU 的 MCO 引脚输出给 PHY也可以由外部晶振单独供给 PHY。我建议用 MCU 的 MCO 输出省一颗晶振但要注意 MCO 的分频配置必须和 PHY 期望的时钟一致LAN8720A 要求 50MHzF407 主频 168MHz 时 MCO 分频要设成 5 分频168/5 不是整数实际要用 PLL 的 25MHz 输出路径这个细节后面配置时钟树的时候会具体说。PHY 地址是个高频坑点。LAN8720A 的 PHY 地址由PHYAD0引脚在上电时决定接下拉就是地址 0接上拉就是地址 1。很多开发板原理图上没标清楚代码里默认写 0结果HAL_ETH_ReadPHYRegister一直返回超时。判断方法很简单在初始化之后读一下 PHY 的 ID 寄存器地址 0x02 和 0x03LAN8720A 的 ID 应该是0x0007C0F1读出来是0xFFFF或者0x0000就说明地址错了或者硬件没通。网络变压器和 RJ45 一般用集成在一起的一体化接口比如 HR911105A这个没什么好说的注意差分线走线尽量等长、远离高频干扰源就行。2.2 CubeMX 和固件包的版本选择CubeMX 我用的 6.10固件包用 STM32Cube FW_F4 V1.28.0。这里有个经验不要盲目追最新版。新版本固件包有时候会改 LWIP 的默认配置比如某个版本把LWIP_NETIF_HOSTNAME默认关了导致 HTTPD 里用主机名的地方编译不过。如果你跟着网上的老教程做建议固件包版本和教程对齐能省掉很多为什么我的宏名字不一样的困惑。Keil MDK 用 5.38 以上记得装好对应芯片的 Device Family Pack。另外 LWIP 的 HTTPD 组件依赖fsdata.c这个文件系统数据文件它是用 ST 提供的makefsdata工具把网页文件转成 C 数组生成的这个工具在固件包路径Middlewares/Third_Party/LwIP/system/下面Windows 下是个 exeLinux 下需要自己编译。很多人卡在网页改了但设备上还是旧的就是因为忘了重新跑 makefsdata。2.3 一个容易被忽略的前置检查在打开 CubeMX 之前先确认你的芯片型号确实带 ETH 外设。F103 系列除了 F107是没有以太网 MAC 的如果你手上是 F103C8T6 这种最小系统板那这条路走不通得换 F107 或者外挂 W5500 这类 SPI 转以太网的芯片。这个检查花不了两分钟但能避免你配了半天发现 CubeMX 里根本没有 ETH 选项。3. CubeMX 里的五个关键配置区域3.1 时钟树RMII 的 50MHz 从哪来打开 CubeMX 新建工程选好芯片型号后第一件事是配时钟。以 F407 为例外部晶振 8MHzPLL 配置成主频 168MHz。然后在 Clock Configuration 页面找到MCO1或MCO2的输出配置把它设成 50MHz 输出给 PHY。具体操作MCO2 的时钟源选 PLLI2SCLKPLLI2S 的 N 值设成 50 的倍数关系让输出正好是 50MHz。如果嫌麻烦也可以让 PHY 用独立晶振这样 MCO 就不用管了但硬件上要多一颗 50MHz 的有源晶振成本上不划算。注意MCO 输出的引脚是固定的F407 上 MCO2 是 PC9配置完之后要确认这个引脚没有被其他外设占用否则 CubeMX 会报引脚冲突。3.2 ETH 外设RMII 模式和 PHY 地址在 Connectivity 里找到 ETHMode 选RMII下面的参数里 Advanced Parameters 中把 PHY Address 设成你硬件上实际的地址默认 0。Auto Negotiation 勾上Speed 和 Duplex 设成 Auto。RX 和 TX 的 DMA 描述符数量保持默认就行一般 RX 4 个、TX 4 个够用如果后面发现丢包严重可以适当加大。引脚方面CubeMX 会自动分配 RMII 的 9 根线REF_CLK、MDIO、MDC、CRS_DV、RXD0、RXD1、TX_EN、TXD0、TXD1。检查一下这些引脚和你原理图是否一致特别是 REF_CLK它既可以是 PHY 给 MCU 的也可以是 MCU 给 PHY 的方向搞反了链路起不来。3.3 LWIP 中间件HTTPD 相关的宏怎么开在 Middleware 里勾选 LWIP然后进入它的配置页面。这里分几个标签页重点是General Settings和Key Options。General Settings 里LWIP_DHCP建议先关掉用静态 IP 调试跑通之后再开 DHCP。LWIP_ICMP和LWIP_UDP保持开启ping 工具和后面可能的 UDP 调试都用得上。Key Options 里要手动打开这几个宏LWIP_HTTPD设为 1这是总开关LWIP_HTTPD_SUPPORT_POST设为 1如果你要做表单提交比如改参数LWIP_HTTPD_DYNAMIC_HEADERS设为 1支持动态生成响应头LWIP_HTTPD_SSI设为 1支持 SSI服务器端包含做实时数据刷新要用LWIP_HTTPD_CGI设为 1支持 CGI处理表单和按钮操作这几个宏不开后面httpd.c编译的时候会有一堆函数找不到定义。我见过有人只开了LWIP_HTTPD结果httpd_post_begin报未定义就是因为 POST 支持没开。3.4 生成代码前的最后检查在 Project Manager 页面Toolchain 选 MDK-ARM注意不要勾选 Generate peripheral initialization as a pair of .c/.h files这个选项会让 ETH 的初始化代码分散到单独文件里和 LWIP 的ethernetif.c配合时容易出问题。保持默认的集中生成方式就好。另外Code Generator 里勾上 Copy only necessary library files这样 LWIP 的源码会复制到工程目录下方便你直接改lwipopts.h里的参数不用去固件包目录里翻。3.5 生成之后先别急着编译点 Generate Code 之后CubeMX 会生成完整的工程。这时候先别点编译打开工程目录看一眼Middlewares/Third_Party/LwIP/src/apps/http/下面有没有httpd.c以及fsdata.c在不在。如果fsdata.c缺失说明 CubeMX 没有自动生成需要你手动跑 makefsdata 工具把网页文件转出来放进去。这个文件不在的话编译会报FS_FILE相关的符号找不到。4. 编译阶段最常见的六类错误及处理4.1sys_arch.h找不到或者sys_mbox_t未定义这个错误的根因是 LWIP 的sys_arch层没有正确包含。CubeMX 生成的工程里sys_arch.c和sys_arch.h应该在Middlewares/Third_Party/LwIP/system/OS/下面。如果 Keil 的 Include Paths 里没有这个目录编译器就找不到。处理办法在 Keil 的 Options for Target - C/C - Include Paths 里确认包含以下路径以 F407 工程为例Middlewares/Third_Party/LwIP/src/include Middlewares/Third_Party/LwIP/system Middlewares/Third_Party/LwIP/system/OS Middlewares/Third_Party/LwIP/src/include/lwip Middlewares/Third_Party/LwIP/src/include/lwip/apps如果路径都在但还是报错检查lwipopts.h里NO_SYS是不是设成了 0。NO_SYS0表示使用操作系统模式需要sys_arch支持如果你没跑 RTOS应该设成NO_SYS1用裸机模式这样就不需要sys_arch了。CubeMX 默认会根据你是否启用 FreeRTOS 来设这个值但有时候手动改过 FreeRTOS 配置后会不同步。4.2lwipopts.h重复定义或者宏冲突这个错误通常长这样warning: LWIP_DHCP redefined。原因是 CubeMX 生成的lwipopts.h和你手动添加的另一个配置文件同时被包含了。检查一下工程里是不是有两个lwipopts.h一个在Core/Inc/下一个在Middlewares/下。CubeMX 生成的是前者后者可能是你从别处拷来的。解决办法只保留Core/Inc/lwipopts.h把另一个删掉或者从 Include Paths 里移除。如果确实需要自定义配置直接改Core/Inc/lwipopts.h里的内容不要另起炉灶。4.3ethernetif.c里的 PHY 读写超时编译能过但下载运行后卡在ethernetif_init或者low_level_init里串口打印PHY read timeout。这个问题的排查链路是这样的第一步确认 PHY 地址。在ethernetif.c里找到LAN8720_GetLinkState或者类似的函数看它用的地址是不是 0。如果不是改成你硬件上的实际地址。第二步确认 MDC 时钟。MDC 是 PHY 管理接口的时钟由 MCU 的 ETH_MDC 引脚输出频率不能超过 2.5MHz。CubeMX 里 ETH 的 Advanced Parameters 有个 MDC Clock RangeF407 主频 168MHz 时选 150-168MHz 这一档分频后大约是 2.1MHz符合要求。如果选错了档位MDC 太快或太慢都会导致读写失败。第三步用示波器或者逻辑分析仪看 MDIO 线上有没有波形。如果没有说明 MCU 的 ETH 外设根本没启动回去检查 ETH 的时钟使能和 GPIO 复用配置。4.4fsdata.c相关的FS_FILE未定义这个错误说明 HTTPD 的文件系统数据没有正确链接。fsdata.c里定义了一个static const unsigned char data_index_html[]这样的数组以及一个struct fsdata_file file_index_html[]的结构体。如果fsdata.c没有被加入编译或者LWIP_HTTPD_USE_CUSTOM_FSDATA宏设成了 1 但你没有提供自定义的 fsdata就会报这个错。处理办法确认fsdata.c在 Keil 工程的 Source Group 里并且LWIP_HTTPD_USE_CUSTOM_FSDATA设为 0用默认的 fsdata。如果你要自己生成网页数据把这个宏设成 1然后把你生成的fsdata.c放到工程里替换掉默认的。4.5 中断向量表里ETH_IRQHandler重复定义CubeMX 生成的stm32f4xx_it.c里已经定义了ETH_IRQHandler如果你在别的地方比如自己写的ethernetif.c里又定义了一遍链接时会报重复符号。解决办法是只保留一处通常保留stm32f4xx_it.c里的然后在里面调用 LWIP 的中断处理函数ethernetif_input。4.6 堆栈溢出导致的 HardFaultLWIP 运行需要一定的堆内存lwipopts.h里的MEM_SIZE默认可能是 1600 字节对于 HTTPD 来说偏小。建议改成 4096 或者 8192。同时 Keil 的启动文件里Heap_Size也要相应加大至少 0x1000。如果跑起来之后随机 HardFault优先怀疑堆不够。5. 让网页真正跑起来的收尾工作5.1 静态 IP 配置和 ping 测试在lwipopts.h里确认LWIP_DHCP是 0然后在main.c的MX_LWIP_Init()之后用netif_set_addr设置静态 IP。或者更简单直接在 CubeMX 的 LWIP General Settings 里填 IP 地址、子网掩码、网关。生成之后这些值会写到lwipopts.h或者ethernetif.c里。下载运行后把电脑的网口和板子用网线直连或者接到同一个交换机电脑 IP 设成和板子同网段比如板子是 192.168.1.100电脑设 192.168.1.10。打开命令行 ping 一下ping 192.168.1.100能通说明链路层和网络层都正常。如果不通先看板子上的 Link 灯和 Speed 灯亮不亮不亮就是 PHY 没协商成功回去检查硬件和 PHY 配置。5.2 浏览器访问和 SSI 数据刷新ping 通之后浏览器输入http://192.168.1.100应该能看到默认的网页。CubeMX 生成的默认网页很简单就是一个 STM32 的标题。如果你要显示实时数据比如 ADC 采样值需要在网页里用 SSI 标签比如!--#adc_value--然后在httpd_cgi_ssi.c里实现对应的处理函数把变量值填进去。SSI 的处理函数签名是这样的const char *ssi_tags[] {adc_value, NULL}; u16_t ssi_handler(int iIndex, char *pcInsert, int iInsertLen) { if (iIndex 0) { snprintf(pcInsert, iInsertLen, %d, get_adc_value()); } return strlen(pcInsert); }然后在httpd_init之后调用http_set_ssi_handler(ssi_handler, ssi_tags, 1)注册进去。这样每次网页刷新adc_value的位置就会显示当前的 ADC 值。5.3 POST 表单处理参数保存如果你要做参数配置页比如改采集周期网页里放一个form methodpost action/save里面一个输入框nameperiod。在httpd_cgi_ssi.c里实现httpd_post_begin和httpd_post_data_recved两个回调把收到的数据解析出来存到 Flash 或者备份寄存器里。这里有个坑httpd_post_data_recved可能会被多次调用因为 POST 数据是分片到达的。你需要自己维护一个缓冲区把所有分片拼起来再解析。我一般用一个 256 字节的静态数组加一个索引变量在httpd_post_begin里清零在httpd_post_data_recved里追加在httpd_post_finished里做最终解析。5.4 实测中的性能表现和优化方向F407 主频 168MHz跑 LWIP HTTPD用浏览器访问静态页面响应时间在 10ms 以内感觉不到延迟。如果页面里有很多图片首次加载会慢一些因为fsdata.c是把所有文件都编译进 Flash 的读取速度受 Flash 访问速度限制。优化办法是把不常变的图片放到外部 SPI Flash 里用 LWIP 的fs_open_custom回调按需读取。并发方面LWIP 默认支持 5 个 TCP 连接MEMP_NUM_TCP_PCB对于设备配置页来说够用了。如果要做多客户端同时访问把这个值加大到 10同时把MEMP_NUM_TCP_PCB_LISTEN也相应调整。6. 几个我踩过之后才明白的细节第一个是关于LWIP_NETIF_HOSTNAME。这个宏默认是关的但 HTTPD 的某些示例代码里会用netif-hostname不开就编译不过。如果你遇到struct netif has no member named hostname把这个宏打开就行。第二个是TCPIP_THREAD_STACKSIZE。如果你跑 FreeRTOS LWIP这个值默认可能是 1024对于 HTTPD 来说偏小处理 POST 数据的时候容易栈溢出。改成 2048 或者 4096具体看你的页面复杂度。第三个是网页文件的路径。makefsdata工具默认会把当前目录下的所有文件打包包括子目录。但生成的fsdata.c里文件路径是相对的比如index.html对应/index.html。如果你在网页里用了/images/logo.png这样的绝对路径确保makefsdata运行时images目录就在当前目录下否则会 404。第四个是关于缓存。浏览器会缓存网页你改了设备上的网页之后浏览器可能还是显示旧的。调试的时候按 CtrlF5 强制刷新或者用无痕模式打开。这套流程走下来从新建工程到浏览器能看到页面熟练的话半小时以内能搞定。第一次做的话把编译错误那一节对照着排查基本两三个小时也能跑通。关键是要理解每一步在做什么而不是照着步骤点一遍——因为换个芯片型号或者换个 PHY总有一两个参数要改理解了原理才能自己定位问题。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →