STM32F407+lwIP:从CubeMX到HTTPD网页控制服务器
发布时间:2026/9/8 13:48:34 作者:尧图编辑部 阅读量:1,286

STM32F407 lwIP 这个系列写到第二篇前一篇我们把以太网物理层、DMA收发、中断这些底层都调通了这篇接着做 lwIP协议栈移植 和 HTTPD服务器搭建。所谓HTTPD是lwIP里自带的一个轻量HTTP服务器不用在单片机上跑Linux一块F407就能直接给浏览器提供网页用来做设备配置页、状态监控、远程开关控制特别合适。文章会从CubeMX工程怎么配、lwIP内存参数怎么调、HTTPD怎么启用、网页文件怎么打包进固件一直讲到实际排错尽量让手里有开发板的同学按着步骤就能把网页跑起来。如果你现在的情况是开发板网口能接上路由器PHY也通了甚至裸机下能收到ARP请求但离“浏览器输入IP就能打开控制页面”还差一口气那这篇正适合你。下面所有内容均以我手头这块STM32F407VET6 LAN8720A的板子为例不同板子的PHY型号和引脚会有差异但思路是通用的。1. 移植前的整体思路先把框架搭起来1.1 为什么我建议用CubeMX生成基础工程lwIP的裸移植网上能搜到很多手写版本的教程一般会给你一个ethernetif.c然后让你手动实现low_level_output、low_level_input、ethernetif_input这些函数再把pbuf、netif、DMA描述符串起来。这种方式确实能帮你把lwIP每个角落都弄明白但如果你是冲着快速出活来的我强烈建议先用STM32CubeMX生成基础工程。CubeMX帮你解决掉的是最容易被低级错误绊倒的部分时钟树、GPIO复用、ETH的DMA描述符初始化、中断向量、lwIP内核线程创建这些在生成代码里都已经处理好了。你拿到手的工程理论上只要改一下IP地址就能ping通剩下的精力可以用在HTTPD和上层应用上。但这不意味着生成之后就直接盲用。我仍然建议你把生成的关键文件过一遍至少要知道这么几个地方lwip.c里的MX_LWIP_Init到底干了什么静态IP是在哪里配的ethernetif.c里的low_level_init是怎么拿到PHY地址的DMA描述符数量是多少lwipopts.h里那些宏分别控制哪块内存CubeMX开启了HTTPD之后httpd_init是不是在MX_LWIP_Init里自动调用了原因很简单后面出问题的时候你大概率要在这些文件里加日志、改参数。如果连文件在哪都不知道排错会非常痛苦。1.2 这篇要解决的核心问题我把“能ping通”和“能打开网页”之间缺的东西拆开来看其实是三层第一层是lwIP协议栈本身要能跑起来TCP/IP协议栈初始化成功ARP、ICMP这些基础协议正常响应。这一层没做好表现就是ping不通或者时通时断。第二层是HTTPD服务要起来。lwIP自带的httpd是一个独立应用它依赖netconn或raw API需要监听80端口并处理HTTP请求。这一层没做好表现就是IP能ping通但浏览器打不开页面。第三层是网页内容要能送达浏览器。lwIP的httpd默认把网页文件打包成C数组通过fs模块读取。你写的index.html如果没被转换进固件或者转换后路径不对浏览器拿到404或者白屏。这三层是递进关系。我见过很多朋友卡在第二层和第三层之间最典型的就是改了一堆网页代码结果发现固件里跑的始终是旧的index.html折腾半天才发现是fsdata.c没重新生成。这篇文章顺序也是按这三层走的先讲协议栈配置和内存参数再讲HTTPD怎么启用最后讲网页资源怎么打包、怎么通过SSI和CGI让网页和MCU交互。2. 硬件和时钟别在物理层翻车2.1 板子与PHY芯片怎么选我用的是STM32F407VET6板载一颗LAN8720A PHY芯片支持RMII和MII两种模式RMII下引脚占用少GPIO紧张的项目基本都选它。F407自带的MAC是支持10/100M以太网的所以外部只需要PHY和网络变压器。注意PHY的地址问题。LAN8720A通常通过外围电路把地址配置为0也就是PHY address 0x00。但有些板子或者PHY芯片会用0x01、0x1F这样的地址你在CubeMX里配置的PHY地址必须和硬件一致否则MDIO读写不到PHY寄存器以太网链路直接起不来。这件事怎么确认最简单的方法是看原理图PHYAD0引脚的上下拉也可以看官方例程里的默认值。如果你不确定直接在ethernetif.c的low_level_init里把HAL_ETH_Init之前的那句phyaddr /* 根据硬件修改 */改成对应值然后用串口打印出来验证。另外还有一个坑CubeMX的以太网中间件里PHY芯片型号一般提供LAN8742、DP83848这些选项不一定有LAN8720A。如果选不到LAN8720A选LAN8742通常也能用因为多数寄存器行为是兼容的。但底层驱动里PHY的HAL_ETH_ReadPHYRegister、HAL_ETH_WritePHYRegister还是要确认一下严格说不同PHY的中断和状态寄存器位定义有差异。2.2 RMII时钟到底谁来提供RMII模式下所有PHY都需要一个50MHz的REF_CLK时钟。这个50MHz从哪里来不同开发板设计差别很大这是以太网移植里最容易翻车的地方。以常见的LAN8720A方案为例很多核心板上是给PHY接一颗25MHz晶振由PHY内部PLL倍频出50MHz然后通过CLKOUT引脚输出给STM32F407的PA1ETH_RMII_REF_CLK。这种情况下STM32的PA1是输入你不需要配置MCO1输出。但另一些设计正好反过来PHY的REF_CLK由STM32的PA8MCO1输出50MHz直接供给PA1同样是输入。如果你开了MCO1而板子其实不需要或者反过来ETH的RMII时钟就会不稳轻则ping不通重则工作时不时掉线。我的建议是拿到板子先看原理图确认PA1上50MHz信号是来自PHY还是来自MCU。如果来自PHYCubeMX里不要开MCO1如果来自MCU记得在时钟树里把MCO1配成PLL P2的50MHz输出。这块我曾经踩过一次当时为了省事把MCO1打开结果网络时通时断查了两天才发现PHY自己已经在送时钟两个时钟源互相干扰。后来把MCO1关掉就稳定了。2.3 CubeMX里常用的配置项新建工程选好芯片之后进入Pinout Configuration我基本会检查这几个位置Connectivity ETH勾选ETH激活模式选RMIIPHY Address按实际改如果是LAN8720A就写0。时钟树先把HCLK拉满到168MHzETH的APB2时钟和RMII参考时钟会自动分配不用手动算。Middleware LWIP是否启用DHCP、IP地址、子网掩码、网关都在这里设置HTTPD的开关也在这里。Middleware FREERTOS如果你准备用RTOS把lwIP的No Sys选项关掉让协议栈跑在专门的线程里。生成代码后在lwip.c的MX_LWIP_Init里能看到类似这样的配置IP4_ADDR(ipaddr, 192, 168, 1, 10); IP4_ADDR(netmask, 255, 255, 255, 0); IP4_ADDR(gw, 192, 168, 1, 1);这里就是你板子的静态IP所在。调试阶段我习惯先固定IP等网络功能和业务逻辑都跑通再切回DHCP。还有ETH中断优先级。在FreeRTOS环境下ETH_IRQHandler里会有从ISR调用的操作优先级数值必须不小于configMAX_SYSCALL_INTERRUPT_PRIORITY否则FreeRTOS会断言。CubeMX默认生成的优先级可能是5或7我一般明确设为5能调用FreeRTOS API也不会被其他高优先级中断频繁打断。3. lwIP内存与协议栈配置3.1 lwIP的内存模型池加堆lwIP的内存管理分两块内存池memp和内存堆mem。池子用于固定大小的结构体比如pcb、netif、pbuf头、UDP/TCP控制块堆用于可变长度分配比如TCP发送缓冲、协议头拼接区。理解这个模型对调节参数很重要。你改了某个宏实际影响的是池子数量还是堆大小得有概念。F407内部有192KB SRAM其中64KB是CCM内存位于0x10000000地址段。这个CCM内存CPU能直接访问但DMA控制器访问不到。以太网的DMA描述符、DMA数据缓冲区绝对不能用CCM内存否则网卡收发包直接异常表现就是收到数据全是乱的或者一次收发就HardFault。所以如果你的FreeRTOS堆分配在主RAM里ETH驱动里动态分配DMA缓冲区一般没问题如果你为了省主RAM把部分缓冲区放到了CCM那就要小心了。3.2 核心配置项解析CubeMX生成的lwipopts.h里有一堆宏。很多人看见就头大其实真正影响HTTPD稳定性的不多我整理了一张表配置项控制内容常用值说明MEM_SIZElwIP堆大小单位字节1600~40960TCP报文组包、协议头处理用到设太小会导致内存分配失败PBUF_POOL_SIZEpbuf池数量8~32每个池大小默认约1.5KB用于接收缓冲区数量不够会丢包MEMP_NUM_TCP_PCBTCP控制块数量16~32HTTPD每开一个TCP连接占一个PCB太小则并发连接上不去TCP_MSS最大报文段长度1460和网卡MTU对应一般不要改TCP_WNDTCP接收窗口4*TCP_MSS或更大窗口越大吞吐越好但内存消耗也大TCP_SND_BUFTCP发送缓冲8*TCP_MSS发送大数据时需要注意太小则大文件传输会很慢LWIP_HTTPD启用HTTPD1CubeMX里勾选后会自动定义为1LWIP_HTTPD_CGI启用CGI1网页动态动作靠它LWIP_HTTPD_SSI启用SSI1网页动态内容插入靠它我手头这块F407HTTPD场景下PBUF_POOL_SIZE配16MEMP_NUM_TCP_PCB配20TCP_WND配8TCP_MSSTCP_SND_BUF配8TCP_MSS跑起来还算稳。有一点要提醒这些参数不是越大越好。F407总共就192KB RAM你还要给FreeRTOS任务栈、应用程序变量留空间。以前我图省事把PBUF_POOL_SIZE加到64结果系统起来后RTOS堆差点不够程序跑一会儿就分配失败。后来老老实实回调按实际并发需求来。3.3 裸机还是RTOS影响很大lwIP本身设计上支持NO_SYS和带OS两种模式。NO_SYS模式下整个协议栈没有独立线程由你在主循环里不断调用ethernetif_input和tcpip_thread相关处理流程比较适合简单应用。但lwIP自带的HTTPD默认实现是基于netconn API的裸机不配合轮询很难搞而且HTTPD本身要监听端口、要维护多个连接状态没有独立线程会非常别扭。所以我实际工程里都选择给lwIP配一个RTOSCubeMX生成代码时配套使用FreeRTOS。配置方式是在CubeMX的LWIP配置里把No Sys设置为false然后确保FreeRTOS被加入工程。生成代码后你会发现lwIP会自动创建一个tcpip_thread网络协议栈的核心处理都在这个线程里跑。你的应用代码只要负责调API就行不用操心逻辑和中断的耦合。tcpip_thread的栈大小建议给到2048以上。如果网页功能多、CGI处理复杂、调试打印多2048都可能不够。我曾经遇到SD卡日志加上HTTPD后tcpip_thread栈被顶爆程序不定时挂掉用FreeRTOS的栈检测钩子才发现栈使用率已经超过90%。后来调到4096才稳定。4. 移植步骤与编译全过程实录4.1 从CubeMX生成工程到第一个Ping整体步骤如下新建STM32F407VET6工程配置RCC外部晶振为HSE调试口SWD。时钟树里把系统时钟配到168MHz。Connectivity ETH启用ETH模式选RMII。Middleware LWIP启用IP参数先填静态IPNo Sys保持默认或按RTOS设置。在LWIP的“Application / HTTPD”里把HTTPD打勾同时把CGI和SSI的选项打开。Middleware FREERTOS确认CMSIS_V2版本任务列表里能看到tcpip_thread。生成代码编译烧录。烧录后把网线插到路由器或交换机PC设置成同一网段的固定IP比如192.168.1.2然后ping 192.168.1.10。如果ping不通先检查三处PHY地址对不对RMII时钟来源是不是和硬件一致ETH中断优先级有没有被FreeRTOS卡住。我调试时习惯在MX_LWIP_Init末尾加一句串口打印确认lwIP初始化没挂printf(LWIP init done, IP:%s\r\n, ipaddr_ntoa((const ip_addr_t *)ipaddr));如果这句都打不出来说明前面ETH初始化阶段就卡死了优先查PHY和时钟。4.2 需要手动改的几个关键点CubeMX生成代码后建议做以下几件事在main.c里确认MX_LWIP_Init在MX_FREERTOS_Init之前调用。如果顺序反了lwIP依赖的RTOS组件还没准备好初始化会失败。给串口重定向加上printf支持后面调试网页请求、打印HTTP状态码都靠它。把LED、GPIO等控制引脚初始化放好后面CGI控制用。修改lwip.c中的IP配置为实际IP。如果你的板子PHY地址不是0还要在ethernetif.c里改phyaddr 0为实际值。很多CubeMX版本的生成代码里这个值是通过#define PHY_ADDRESS 0控制的。4.3 编译过程中常见的链接错误移植时最讨厌的是编译报错。根据不同CubeMX版本常遇到这几个提示找不到lwip/apps/httpd.h说明HTTPD的源码没被包含进工程检查一下middleware组件是否完整。提示LWIP_RAND未定义lwIP 2.1以上版本需要你提供随机数函数在lwipopts.h里加上#define LWIP_RAND() ((u32_t)rand())并在工程里包含stdlib.h。提示tcpip_thread相关定义冲突一般是No Sys选项和RTOS配置不一致确认lwIP配置里No Sysfalse。提示httpd_cgi_ssi相关函数重复定义CubeMX生成的文件里自带一个模板httpd_cgi_ssi.c如果你又手动添加了一份就会冲突。只保留一个。每次编译完我建议先用串口把启动日志打出来确认lwIP版本、IP地址、HTTPD初始化是否执行到了。日志系统越早做调试越省事。5. HTTPD服务器搭建与网页分发5.1 HTTPD在工程里的位置和作用lwIP的HTTPD组件由几部分构成httpd.cHTTP协议解析和响应逻辑fs.c文件系统抽象层给httpd提供文件读取接口fsdata.c静态文件系统的数据源所有网页内容以C数组形式存在这里httpd_cgi_ssi.cCGI和SSI的处理函数这是你主要修改的文件HTTPD启动后会在80端口等待HTTP请求。浏览器发起TCP连接HTTPD解析请求行比如GET /index.html HTTP/1.1然后通过fs模块找到对应的文件数据以HTTP响应包的形式返回给浏览器。由于lwIP默认不依赖外部文件系统它把网页资源全部编译进了固件。你改一个网页文件需要重新生成fsdata.c然后重新编译整个固件这点和PC上改网页文件即时生效完全不同。5.2 网页文件如何打包进固件先建一个目录比如叫fs把你的index.html、style.css、favicon.ico这些都放进去。然后使用lwIP源码包里的makefsdata工具把fs目录打包成一个fsdata.c。makefsdata的使用方式我常用的是Windows命令行makefsdata.exe fs运行后会在当前目录生成一个fsdata.c。把这个文件替换到工程原有的fsdata.c位置重新编译即可。如果你想验证生成结果是否正确可以打开fsdata.c看一眼里面是所有文件内容按字节存放的数组以及fsdata_file结构体数组。这中间有几个容易踩的细节index.html这个名字不要改HTTPD默认找的就是index.html。如果你放的是myPage.html浏览器访问根路径会404。文件名大小写敏感。lwIP的fs模块匹配路径时是区分大小写的你生成的文件叫INDEX.HTML浏览器请求index.html就找不到。文件大小不要超过FS_MAX_PAGE_LENGTH。这个宏在lwipopts.h里可以改默认可能只有128或者更大如果你页面很大要记得调大。网页里引用资源时路径要完整。/style.css和style.css在lwIP的fs里是有区别的外部CSS/JS引用容易踩这个坑。每次重新生成fsdata.c后最好串口打印一下fsdata_file里文件个数和大小确认真实生效了。我发现很多时候浏览器打不开页面是因为固件里还是旧的空的fsdata.c。5.3 用SSI和CGI让网页操作MCU静态网页只能看不能动。真正和MCU交互靠的是HTTPD里的SSI和CGI。SSI的作用是在网页里插入一段动态内容。比如网页里写一个!--#led_state--占位符浏览器请求这个页面时HTTPD解析到这个tag会调用你实现的SSI处理函数把返回字符串替换到页面对应位置。这样就能实时显示LED当前状态、温度、电压等信息。CGI的作用是处理带动作的请求。比如浏览器访问/led.cgi?led1HTTPD识别到这个CGI路径后调用对应的CGI处理函数你在函数里去做实际的GPIO操作然后返回一个页面让浏览器跳转。这样用户点击按钮就能控制MCU。CubeMX生成的工程里httpd_cgi_ssi.c里通常已经有一个模板。下面是我在一个项目里实际用的写法lwIP版本是2.1.x。如果你的版本API有出入以你的头文件声明为准。先看SSI部分#include lwip/apps/httpd.h #include lwip/def.h static u16_t ssi_led_state(int iIndex, char *pcInsert, int iInsertLen) { if (iIndex 0) { if (LED_STATE) { snprintf(pcInsert, iInsertLen, ON); } else { snprintf(pcInsert, iInsertLen, OFF); } } return (u16_t)strlen(pcInsert); } static const char *ssi_tags[] { led_state }; void httpd_cgi_ssi_init(void) { http_set_ssi_handler(ssi_led_state, ssi_tags, LWIP_ARRAYSIZE(ssi_tags)); }再看CGI部分static const char *cgi_led_control(int iIndex, int iNumParams, char *pcParam[], char *pcValue[]) { for (int i 0; i iNumParams; i) { if (strcmp(pcParam[i], led) 0) { if (strcmp(pcValue[i], 1) 0) { LED_ON(); } else { LED_OFF(); } } } return /index.html; } static const tCGI cgi_handlers[] { {/led.cgi, cgi_led_control} }; void httpd_cgi_ssi_init(void) { http_set_ssi_handler(ssi_led_state, ssi_tags, LWIP_ARRAYSIZE(ssi_tags)); http_set_cgi_handlers(cgi_handlers, LWIP_ARRAYSIZE(cgi_handlers)); }这里有几个注意点HTTPD调用CGI时URL后的参数列表是动态解析的pcParam和pcValue分别存参数名和值iNumParams是参数个数。CGI处理函数返回的字符串是要跳转到的页面路径。一般返回/index.html让页面刷新回主页。SSI的tags数组顺序要和SSI handler里的iIndex对应。第0个tag就是iIndex为0的处理函数。snprintf写插入内容时注意iInsertLen是缓冲区大小别写越界。如果lwIP版本比较老可能不是http_set_ssi_handler这种方式而是直接在httpd_cgi_ssi.c里定义一个handler数组通过宏配置调用。不过逻辑是一样的你把文件和页面里的tag对应上就好。5.4 一个可复用的LED控制页面示例网页部分我在fs目录下放了index.html内容大概这样!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleSTM32F407 Web Control/title /head body h1STM32F407 控制面板/h1 pLED 当前状态!--#led_state--/p a href/led.cgi?led1button打开LED/button/a a href/led.cgi?led0button关闭LED/button/a /body /html烧录后浏览器访问开发板IP页面会显示一个实时LED状态和两个按钮。点击“打开LED”请求发送到/led.cgi?led1CGI函数把GPIO拉高返回index.html浏览器刷新页面SSI标签被替换成“ON”。如果打开页面发现!--#led_state--原样显示说明SSI没生效先查LWIP_HTTPD_SSI宏是否打开再查tag名字是不是完全一致。如果点按钮没反应先把URL放到浏览器地址栏直接访问看看开发板串口有没有CGI处理日志。没有日志说明CGI表里的路径没匹配到有日志但LED没动查GPIO初始化。6. 常见问题与排查速查表6.1 ping不通类现象可能原因排查方法完全ping不通RMII 50MHz时钟异常确认PHY的CLKOUT是否到PA1或MCO1是否按需配置完全ping不通PHY地址不对读PHY寄存器确认MDIO通信正常完全ping不通网线或路由器问题换一根网线确认开发板网口LED亮ping时通时断时钟源冲突或干扰关掉多余的MCO输出改短时钟走线ping第一次通后面断ETH中断优先级和FreeRTOS冲突检查ETH中断优先级数值是否满足FreeRTOS条件ping通但丢包DMA描述符数量或pbuf池太小增加PBUF_POOL_SIZE检查ETH描述符数量排查ping问题我建议把eth_link状态打印出来在ethernetif.c里读PHY基本状态寄存器确认链路是up的。链路都没up后面全白搭。6.2 能ping通但网页打不开现象可能原因排查方法浏览器一直转圈HTTPD没启动确认LWIP_HTTPD宏为1httpd_init被调用返回404fsdata里没有对应文件用makefsdata重新生成fsdata.c检查文件名返回200但白屏fsdata里index.html是旧的确认新的fsdata.c参与编译清浏览器缓存能打开但显示tag原样SSI宏或handler没生效确认LWIP_HTTPD_SSI为1tag名完全一致按钮点击无反应CGI路径不匹配或参数不对浏览器手动访问CGI URL看串口日志这里特别提醒一句网上搜“httpd syntax error”出来的基本都是Apache的报错比如/applications/phpstudy/extensions/apache2之类的那是PC上Web服务器配置文件语法错误跟单片机lwIP的HTTPD没有任何关系。lwIP的HTTPD是纯C代码静态编译不存在配置文件语法错误这种说法。你要是因为这个关键词搜到一堆无关内容不要被带到沟里去。6.3 运行中不稳定或随机挂掉现象可能原因排查方法运行几分钟后假死tcpip_thread栈溢出增大tcpip_thread栈开启栈检测钩子不定时HardFaultDMA缓冲区放在CCM确认ETH缓冲区在普通SRAM高并发访问挂机MEMP_NUM_TCP_PCB不足增大TCP控制块数量HTTPD返回内容截断FS_MAX_PAGE_LENGTH太小调大FS_MAX_PAGE_LENGTH大文件传输慢或失败TCP_SND_BUF太小增大TCP_SND_BUF配合TCP_WND调整排查运行时问题最好的工具是FreeRTOS的任务栈统计功能和lwIP自带的lwip_stats。前者能查任务栈水位后者能看pbuf、pcb等运行时的内存峰值。7. 从能跑通到能用几个进阶建议7.1 性能和稳定性调优前面把基本功能跑通了下一步就是让它在实际环境里更稳。网络这块我一般会做这几件事增加网线热插拔检测。ethernetif.c里有个link_timer轮询通过PHY状态寄存器检测链路up/down链路恢复后主动调用netif_set_link_up避免拔了网线再插上网络恢复不了。把TCP参数稍微调大。带HTTPD的设备经常同时有手机和电脑访问TCP_WND和TCP_SND_BUF至少为8*TCP_MSS不然并发性能很拉胯。增加看门狗和状态上报。网络任务挂了至少要能通过串口或LED看出来然后自动重启网卡。对CGI参数做长度和安全校验。浏览器是可控的但你不能假设只有正常页面会发请求。参数缓冲区要按实际长度比较不要用超长的value。如果你把HTTPD和其他业务放一起比如SD卡日志、传感器采集最好把业务逻辑和网络逻辑分成两个任务用队列或信号量通信。别在CGI handler里做耗时的测量或存储操作否则网页响应会卡住。7.2 扩展方向日志存储与远程设备管理HTTPD跑通后能玩的方向就多了。最常见的扩展是把设备运行状态做成网页实时展示比如CPU负载、温度、网络流量统计。另一个和热词“基于stm32f407的日志存储记录方法”相关的方向是把运行日志远程化。做法大致是MCU把日志写入SD卡上的文件HTTPD提供/download.cgi?filexxx接口用户通过浏览器下载日志。也可以直接用SSI把最近几条日志实时渲染到网页上这样不用连串口就能看设备运行情况。做日志时要注意HTTPD基于netconn如果你在CGI handler里直接操作SD卡耗时可能会让HTTP连接超时。建议把日志查询做成异步CGI只负责启动一个查询任务任务完成后再通过SSI回显结果。还有一点经验网页里加一个“重启设备”的CGI按钮看起来简单但实际项目里特别有用。远程设备出问题时能通过网页软复位省去跑现场。最后分享一个我的调试习惯每个新移植的HTTPD工程我都会先做一个只有一行的index.html内容只写“HTTPD OK”然后把CGI和SSI一个个加到实际页面里。这样每一步都有明确的验证点排查问题范围小心理压力也小。你按这个节奏来lwIP移植和HTTPD搭建这件事基本不会出大乱子。