这篇是 STM32F407 + lwIP 系列的第二篇。上一篇我们把最基本的网络工程跑通,这次进入真正有价值的部分:把 lwIP 协议栈完整移植到 F407 上,并且基于它搭建一个 HTTPD 服务器。HTTPD 在这套方案里不算复杂,但它牵扯到协议栈配置、PHY 驱动、静态资源打包、动态数据接口这几条线,任何一个环节掉链子,最终表现都是“网页打不开”,排查起来很费时间。
这篇文章的实验环境是 STM32F407 开发板加一颗常见 PHY 芯片,IDE 用 Keil 或 STM32CubeIDE 都可以,中间会大量用到 CubeMX 生成代码,但我不打算只讲“点几个勾就完事”。lwIP 移植这件事,真正考验人的不是生成代码的速度,而是出问题时有没有足够扎实的底层认知。我会把我在实际操作中踩过的坑、验证过的参数和能直接抄的代码都放出来,适合正在做 F407 网络项目、想给板子加一个 Web 控制页面或者需要上报设备状态的开发者。
1. 移植路线怎么选:CubeMX 辅助和手动搬运源码,到底差在哪
先说结论:如果你问我“移植 lwIP 到底要不要手写”,我的建议是不要为了“显得懂底层”而去手写,CubeMX 能省掉大量机械步骤,但你要有本事解释它生成的每一段关键代码是干什么的。很多时候我们看到网上老教程还在教怎么从 lwIP 官网下载源码、怎么手动复制 core 目录、怎么改 cc.h 和 lwipopts.h,这些知识没有过时,只是对于 F407 这种有官方 Cube 包的芯片,完全可以先让工具把框架搭好,再把精力集中在真正容易出问题的几个点上。
这里我给自己定的目标是:用 CubeMX 生成基础网络工程,接着手工确认 PHY 初始化链路、补上 HTTPD 应用层源码,最后通过 CGI 和 SSI 让网页能显示和操控设备。整条链路跑通之后,再回头看底层配置,会比纯手写一遍印象深得多。
1.1 CubeMX 路线和手写路线的关键差异
用表格列一下两条路线在我实际使用中的区别:
| 对比维度 | CubeMX 辅助路线 | 手动搬运源码路线 |
|---|---|---|
| 项目搭建速度 | 快,外设和时钟一次生成 | 慢,需要自己整理文件列表 |
| lwIP 版本自由度 | 取决于 Cube 包内置版本 | 随意,想要哪个版本用哪个 |
| 底层掌握程度 | 需要额外看生成代码 | 每一步都经过自己手 |
| 排错成本 | 出错时先怀疑自己的配置 | 出错时可能同时怀疑源码和配置 |
| 后续维护 | 重新生成代码有覆盖风险 | 完全可控 |
CubeMX 内置的 lwIP 版本未必是最新的,而且应用层组件(比如 httpd)不一定自动加入工程,这些是它不够完美的地方。但大部分项目不需要追新版本,稳定能用更重要。我的习惯是先用 CubeMX 搭建一个最小可行工程,把网络先 ping 通,再逐步加应用,这个节奏比一上来就深挖源码舒服很多。
1.2 动手之前必须梳理清楚的文件关系
F407 本身只带以太网 MAC,没有 PHY,所以硬件上必须外接一颗物理层芯片,通常见到的方案是 LAN8720A 或者 DP83848。lwIP 的代码结构可以分成三块:最底层是网卡适配层,负责把 lwIP 发下来的包通过 F407 的 MAC 和 PHY 发出去,同时把收到的包交给协议栈;中间是协议栈核心,包含 TCP/IP 协议处理;应用层则包括 HTTPD、DHCP、DNS 等。
很多人移植 lwIP 失败,不是协议栈本身出了问题,而是没分清这三层。比如页面打不开,可能问题在 PHY 没有 link up,也可能问题在 httpd.c 没编译进工程,这两类错误的表现很相似,但如果能先定位到具体层次,排查速度会快很多。CubeMX 生成的工程里,ethernetif.c属于第一层,lwip.c负责第二层的初始化,而 HTTPD 属于第三层,它需要我们自己手动调用初始化函数,CubeMX 默认不会帮你打开服务器。
2. 网卡驱动避坑实录:先解决 PHY 和 RMII,再谈协议栈
HTTPD 即使写对了,只要底层网卡没有真正通,所有努力都白费。我见过不少人花大量时间调 lwIP 参数,结果最后发现是 RMII 的参考时钟方向就不对。所以在搭建 HTTPD 之前,我建议先把网卡这部分彻底过一遍。
F407 的 MAC 支持 MII 和 RMII 两种接口,实际项目里绝大多数用 RMII,因为它从 16 根信号线省到了 7 根左右。RMII 需要的信号包括参考时钟、发送数据 TXD0/TXD1、发送使能 TX_EN、接收数据 RXD0/RXD1、接收载波侦听 CRS_DV,以及 MDIO 管理接口。具体引脚编号每个开发板不一样,不要照抄文档里的默认映射,必须对着自己的原理图逐个确认。
2.1 RMII 参考时钟:一个方向错了就全盘皆输的配置
RMII 模式要求有一个 50MHz 的参考时钟 REF_CLK,这是整个 RMII 接口最容易出问题的点,因为它存在“由谁提供时钟”的方向问题。有些板子的设计是 PHY 负责产生 50MHz 时钟交给 MCU,有些则反过来由 MCU 的 MCO 引脚输出时钟给 PHY,还有些方案是外部有源晶振直接同时供给两边。
我遇到过的情况是 PHY 的寄存器能正常读写,MAC 初始化也没有报错,但收发包就是不对,最后用示波器一看才发现 REF_CLK 完全没有信号,原因是板子上的 PHY 需要 MCU 这边提供时钟,而 CubeMX 里没有打开对应的 MCO 输出配置。这种问题靠看代码很难发现,排查方向必须一开始就放对。拿到 F407 网络板后,第一件事就是画一条信号方向线:REF_CLK 到底从哪来,到哪去,在 CubeMX 里要对齐这个方向。
2.2 PHY 地址读不出来?先查硬件复位时序和 MDIO 引脚
lwIP 在初始化网卡时需要读取 PHY 的寄存器来确认链路状态,读 PHY 的第一步是保证 PHY 地址写对。PHY 芯片的地址通常由硬件引脚的电平决定,常见的是 0x00 或 0x01,如果你在代码里配的地址和硬件不一致,读回来的寄存器值会一直是 0xFFFF 或者 0。另一个容易翻车的点是 PHY 的硬件复位引脚时序——如果复位后立刻去读寄存器,芯片可能还没准备好,读出来的数据就是乱的。
我建议在网卡初始化前写一个简单的延时流程:把复位引脚拉低一段时间,再拉高,等 PHY 内部完成上电初始化,然后再调用 HAL 的 PHY 寄存器读取函数确认 ID。这个“等待”不是形式主义,PHY 的上电稳定时间比 MCU 慢很多,跳过这一步,后续所有链路状态判断都会建立在不可靠的数据上。
2.3 用串口把链路状态打出来,比瞎猜高效得多
在调试网络不通时,我最常用的一招是在网卡状态回调里加串口打印。lwIP 提供了网卡 link up 和 link down 的回调机制,只要 PHY 检测到网线插入或者拔出,协议栈就会调用对应函数。通过串口输出可以快速分清问题到底出在物理链路还是协议栈配置。
一个典型的裸机初始化流程是在MX_LWIP_Init()完成之后,注册网卡链路回调,然后在主循环里周期调用超时处理函数。如果串口上永远没有 link up 打印,说明物理层就没通,这时候应该回到 PHY 的硬件电路和时钟去查,而不是反复调整 IP 地址和 DHCP 配置。这点经验对后面调试 HTTPD 尤其有用,因为 HTTPD 必须建立在链路已经起来的基础上。
3. 内存与协议栈参数:HTTPD 对 lwIP 配置的隐藏要求
很多人以为 HTTPD 只是一个简单的应用层模块,加上就能用,没想过它会在什么条件下崩溃。实际上 HTTPD 涉及 TCP 连接管理、文件数据发送、动态模板替换,这些操作全部依赖 lwIP 的内存池和缓冲区配置。如果参数给得太小,页面会不定时打不开,甚至整个协议栈卡死。
lwIP 的内存机制可以粗略分成两类:一类是堆内存,通过mem_malloc分配,给协议栈内部动态使用;另一类是 PBUF 池,固定大小的缓冲区池,用于接收和发送网络数据。HTTPD 发送网页时会把文件数据分成一个个 PBUF,如果池里的数量不够,发送就会失败,表现是浏览器能连上服务器但页面内容迟迟不出来。
3.1 针对 F407 的参考参数,以及它们各自影响什么
F407 系列的 SRAM 通常有 192KB,如果不跑复杂的 UI 或者算法,给 lwIP 留的空间是比较充裕的。以下是我在 F407 + lwIP + HTTPD 工程里常用的一组参数,注意这是经验值,不是标准答案:
| 参数 | 经验值 | 作用 |
|---|---|---|
| TCP_MSS | 1460 | TCP 单段最大负载,受网卡 MTU 限制 |
| TCP_WND | 8 * TCP_MSS | 接收窗口大小,影响网页下载速度 |
| TCP_SND_BUF | 8 * TCP_MSS | 发送缓冲区大小,影响上传和页面响应 |
| MEMP_NUM_TCP_SEG | 24 ~ 32 | 可缓存的 TCP 分段数量 |
| PBUF_POOL_SIZE | 16 ~ 32 | 收发 PBUF 池数量,页面越大多给一些 |
| MEM_SIZE | 相对保守即可 | 协议栈内部动态堆 |
这组参数背后的逻辑是:HTTPD 的响应通常是一段连续的 HTML 内容,发送缓冲区太小会导致协议栈频繁等待,浏览器看起来就像页面打开了一半卡住了。TCP_WND 如果太小,高速请求时会有明显延迟,但也不是越大越好,每个缓冲都要占用实际 SRAM,尤其当工程里同时跑 FreeRTOS 时,系统堆和 lwIP 内存池会互相争抢空间。
3.2 怎么判断内存是不是不够用了
有一种很典型的现象:页面第一次能打开,刷新几次之后开始卡住,过一会又恢复。这种“时好时坏”很多情况下并不是代码 bug,而是内存池耗尽后分配失败,后续请求无法建立新连接。lwIP 内置了统计功能,可以把LWIP_STATS打开,然后在串口或日志里观察 PBUF 分配失败次数。
我个人建议在联调阶段把统计宏打开,等项目稳定后再关掉以减少开销。如果能看到某个计数持续增长,就说明对应的池或者堆尺寸不够,需要在 lwipopts.h 里调大对应参数。比起盲改,让数据说话能节省大量时间。另外还要注意,CubeMX 生成的 lwipopts.h 中有很多宏没有显式定义,它们会落到 lwIP 源码里的默认值,如果你发现某个参数改了没生效,查一下是不是宏名拼错或者被默认值覆盖了。
4. HTTPD 静态页面上线全流程:宏、打包、初始化三件事
协议栈通了以后,HTTPD 的搭建可以分为三个动作:第一,确保 httpd.c 被编译进工程;第二,把网页文件打包成 lwIP 能识别的 C 语言数组;第三,在系统初始化完成后调用httpd_init()。每一步都有对应的检查点,按顺序做完以后,浏览器访问板子 IP 就能看到页面。
HTTPD 的源码在 lwIP 的应用层目录,不同版本路径稍有不同,一般在src/apps/http/httpd.c。CubeMX 生成的基础工程不一定