
1. 移植前准备硬件、工具链与知识储备1.1 硬件平台与PHY选型F407凭什么配LAN8720A做嵌入式网络开发硬件平台的选择基本决定了后面所有工作的复杂程度。STM32F407内置了MAC控制器但PHY物理层收发器必须外接这几乎是所有MCU做以太网的通用做法。我这边用的板子是常规的F407ZGT6核心板PHY芯片选的是LAN8720A板载RJ45带网络变压器整体方案非常成熟网上资料多出了问题也好排查。选PHY的一个关键点是接口类型。PHY和MAC之间通常有MII和RMII两种接口。STM32F407的ETH外设引脚在100脚以上的封装里可以映射到特定的GPIO上其中RMII接口占用的引脚更少7根信号线加一路50MHz参考时钟能把宝贵的GPIO省下来干别的。但RMII对时钟的要求更高——它要求50MHz的参考时钟而且MAC和PHY必须共用这个时钟。LAN8720A内部有时钟输出功能可以让STM32F407输出50MHz给LAN8720A或者反过来用50MHz有源晶振同时供给两边。实际用下来由MCU的MCO1引脚输出50MHz给LAN8720A是常见做法省一颗晶振但要注意MCO输出能力有限负载不能太重否则时钟质量不好网络会时好时坏。PHY的I2C地址也是个大坑。LAN8720A的默认PHY地址是0x00很多PHY默认是0x01如果你的板子上的PHY地址配置电阻接法不同那么MAC访问PHY的时候要么读不到寄存器要么读到全FF会导致初始化失败。我第一次移植时没注意板子上PHY地址配置CubeMX里填的0x00结果死活link不上折腾了半宿。后来冷静看了原理图才发现板子把PHYAD0拉高了地址实际是0x01。这种问题不会报错表现出来就是网络不通、初始化返回HAL_ERROR排查起来非常耗时间。1.2 软件基础CubeMX版本、lwIP版本选择、RTOS与否移植lwIP现在很少有人手动从零开始把源码拖到工程里了一般都用STM32CubeMX来生成配置。为什么因为CubeMX能把ETH外设的GPIO、时钟、DMA、中断这些底层全给你配好生成的lwIP代码已经帮你把netif、Ethernetif、ethernetif_input这些关键环节接好了。你真正要动手写的其实是应用层的HTTPD、Socket或者MQTT等业务逻辑。但CubeMX生成的代码也不是拿来就能跑原因在于lwIP协议栈的配置项非常多而CubeMX生成的lwipopts.h文件只是给了一套“保守能用”的参数。比如默认的内存池大小、PBUF数量对于简单的TCP通信够用但对HTTPD这种会同时处理多个连接的场景往往需要手动调优。另外CubeMX默认生成的ethernetif.c里关于PHY的部分某些库版本和LAN8720A的配合并不完美需要自己补一些PHY复位、自协商等待的代码。关于版本我建议用CubeMX自带的lwIP版本就好不要纠结是不是最新版。lwIP的接口在不同版本间有一些差异比如lwIP 2.0和2.1的内存管理接口、低功耗相关宏都有变化但核心API变化不大。我用的STM32CubeMX 6.x对应的lwIP版本是2.1.x稳定性很好。如果你从网上扒老工程的代码很可能遇到API不兼容的问题这时以你本地CubeMX生成的头文件为准手动改一改即可。还有一点必须提前决定是否使用RTOS。HTTPD服务器本身是事件驱动的单靠轮询也能工作但如果你还要同时处理传感器采集、串口日志、按键控制等任务裸机的“主循环轮询”会越来越难维护。我用的是FreeRTOS lwIP的组合CubeMX里把RTOS勾上然后在任务里分别跑tcpip_threadlwIP自带和应用线程。注意在CubeMX配置lwIP时如果选了“带RTOS”的选项lwIP内部会创建tcpip_thread、ethipt_thread并且使用信号量做同步。结合HTTPD时可以省心不少。2. CubeMX里完成lwIP移植的第一步参数配置与底层驱动2.1 使能ETH外设与PHY的RMII连接打开CubeMX后第一步是选中STM32F407ZGT6然后在Pinout Configuration界面里把ETH外设打开。这时你会看到两种接口模式可选选RMII。接着手动分配引脚好在F407的ETH引脚是重映射到特定引脚的CubeMX会帮你把RMII需要的TX_EN、TXD0、TXD1、RXD0、RXD1、CRS_DV、MDC、MDIO这些引脚锁定到对应的GPIO上你不需要背引脚号只需要确认这些引脚没有被别的功能占用。这里要注意LAN8720A的RMII接口里时钟方案选择会影响引脚占用。如果采用MCO输出50MHz给PHY的方案那么PA8MCO1会被占用CubeMX里也要把PA8对应的MCO1功能配出来。如果你直接用板载有源晶振给PHY提供50MHz那MCU这边就不需要配MCO但两边的时钟必须同源或严格同频否则RMII的时序可能出问题。对于PHY的相关参数CubeMX提供了很多字段。比较关键的是PHY Address绝大部分LAN8720A模块上的地址是0x00如果你不能确定去看原理图上的PHYAD0电阻。PHY的Reset引脚也要在CubeMX里配置成一普通GPIO输出并设置为低电平复位然后软件上拉高等待PHY复位完成。LAN8720A的复位时间大概是几十毫秒量级如果你上电后立刻访问PHY寄存器可能读到异常值所以初始化代码里要有延时。2.2 lwIP参数配置内存池大小、DHCP、netif参数CubeMX的Middleware选项里能找到lwIP。进入后需要选择“带RTOS”还是“无RTOS”然后设置一堆参数。这里我按实际项目经验给一套参考值IP地址填你板子要用的静态IP比如192.168.1.10子网掩码255.255.255.0网关192.168.1.1。如果你需要DHCP可以在初始化代码里把静态IP改为DHCP启动但这套静态IP保留作后备比较稳妥。内存参数是重头戏。lwIP的内存管理包括全局内存堆MEM_SIZE、PBUF池PBUF_POOL_SIZE、TCP段的内存TCP_MSS、TCP_WND、TCP_SND_BUF等。CubeMX默认给的数值偏小比如PBUF_POOL_SIZE可能只有4个TCP_WND可能只有4KB。对HTTPD来说如果网页文件较大或者有大量并发连接内存不够会导致丢包、连接建立失败、页面刷新很慢。我实际把PBUF_POOL_SIZE调到16TCP_WND调到8 * TCP_MSSTCP_SND_BUF也调到8 * TCP_MSS整个设备运行的稳定度明显提升。内存的数值也不是越大越好F407的RAM是192KB还有一部分要给FreeRTOS的堆、任务栈以及其他业务数据。lwIP默认的内存堆、PBUF池加起来几十KB已经算比较大了。我建议先按默认值跑通再用内存统计函数观察峰值占用按需逐步增大。盲目开大内存可能会挤占RTOS堆空间导致任务创建失败那就是另一种恶心问题了。DHCP选项可以开启或关闭。HTTPD调试阶段建议用固定IP避免路由器DHCP分配到了冲突地址而排查半天。如果产品最终要进入用户局域网建议开启DHCP但要设计好“DHCP失败回退静态IP”的逻辑否则设备一旦拿不到地址就失联了。2.3 时钟与中断STM32F407的PLL时钟配置和网口中断STM32F407的以太网MAC需要一个25MHz或50MHz的参考时钟。CubeMX的时钟树配置里以太网外设会自动从PLL或某个时钟源取时钟。如果用的是LAN8720A通常建议让MAC的时钟源配置为“来自RMII外部时钟输入”也就是PHY或晶振提供的50MHz直接给ETH模块。这个过程有个常见坑如果你让MCU的PLL输出25MHz给ETH而PHY侧需要的是50MHz参考两边时钟频率不匹配RMII根本跑不起来。因为在RMII模式下PHY接收数据需要50MHz的时钟边沿来采样数据而MAC侧的时钟也必须同频。CubeMX会在时钟树中把可用的ETH参考时钟选项列出来你需要仔细确认。很多犯错的案例都出在这一步现象是网口灯亮但ping不通或者时通时不通。中断设置上STM32的ETH中断ETH_WKUP、ETH可以打开配合lwIP在FreeRTOS环境下的邮箱机制接收数据时触发中断然后在中断处理函数里调用HAL_ETH_IRQHandler在中断回调或主循环里把接收到的帧交给netif-inputlwIP内部会投递到tcpip_thread。这一套流程CubeMX生成的代码已经写好我们不需要自己写但要在stm32f4xx_it.c里确认ETH的中断处理函数有被调用。有时候用户手动清中断向量表把ETH的IRQHandler弄丢了就会导致数据接收不到但网络“看起来”是通的。我个人的习惯是在CubeMX里把网络相关的引脚中断优先级配置成高于一般任务但不要高于系统节拍中断。网络数据接收是频繁操作如果优先级太低高负载下容易丢帧。3. HTTPD服务器的搭建思路静态文件、CGI与SSI3.1 HTTPD是什么、为什么用它而不是手写裸Socket很多新手一提到“嵌入式Web服务器”第一反应是能不能自己用lwIP的RAW API或者Socket API写一个HTTP服务。当然可以但这么做等于把HTTP协议解析、TCP连接管理、静态文件服务这些轮子重新造一遍工作量不小而且容易留坑。lwIP官方其实自带一个HTTPD模块它历史悠久、使用广泛可以支持静态网页、CGICommon Gateway Interface、SSIServer Side Include。就是说你只需要把HTML页面放到程序里注册几个CGI回调函数就可以实现“网页上点个按钮控制LED、网页上显示传感器数据”这类非常典型的产品功能。HTTPD在lwIP源码里位于src/apps/httpd/目录下。在CubeMX中打开lwIP后可以在lwIP的配置选项“Application”或者“Network interfaces”里找到HTTPD模块的启用开关也可以手动在socket.h之外引入httpd.h。注意一点HTTPD支持两种模式一种是基于RAW API的直接调用httpd_init内部自己处理连接另一种是基于Socket API的需要额外开启lwip_socket支持。如果已经用了FreeRTOS从易用程度来说基于Socket API的HTTPD会更自然一些不容易和RTOS调度产生优先级反转问题。3.2 制作网页数据把html转成C数组的几种方式HTTPD经典的做法是把HTML文档打包进固件。lwIP官方提供了一个转换工具makefsdata它可以把一个目录里的HTML、CSS、JS、图片等文件压缩转换成一个fsdata.c文件里面是一个个C数组。这个工具采用“数据对齐长度前缀”的存储格式HTTPD通过它找到对应URI并发送数据。实际使用makefsdata有几种途径。lwIP源码目录下有编译好的可执行文件Windows下是makefsdata.exe你在命令行运行它时需要在当前目录下放置一个fs文件夹这个文件夹里就是你的网页源文件。运行后生成的fsdata.c必须放到你的工程里同时把fsdata.h引用进来并且在httpd_conf.h里启用LWIP_HTTPD_CUSTOM_FILES或者直接让编译系统包含fsdata.c。如果你的网页文件比较多中文文件名、中文网页内容是常见需求。fsdata_custom.c的生成方式支持utf-8编码但要注意HTTP响应头里需要标注Content-Type: text/html; charsetutf-8否则浏览器会按默认字符集解析中文乱码。lwIP HTTPD有一个httpd_conf.h参数LWIP_HTTPD_CONTENT_TYPE但它只决定默认类型不会决定字符集。比较省事的方法是在HTML文件里放一个meta charsetutf-8标签让浏览器就算在没有响应头的情况下也能尽量正确解析。还有一点容易被忽略makefsdata生成的网页数组里文件名会被用作URL匹配。比如fs/index.html对应URL/index.html。HTTPD默认首页是/index.html如果访问根路径/它也会尝试查找index.html。如果你的首页不叫这个名字需要在httpd_conf.h里修改LWIP_HTTPD_INDEXFILENAME宏。3.3 核心源码修改lwipopts.h、httpd_conf.h要让默认生成的lwIP工程支持HTTPD必须改几个头文件。首先是lwipopts.h在这个文件里除了内存参数还要打开以下宏LWIP_HTTPD1、LWIP_HTTPD_CGI1、LWIP_HTTPD_SSI1以及对应的LWIP_HTTPD_SSI_INCLUDE_TAG之类的配置。httpd_conf.h是HTTPD模块自己的配置文件它默认在lwip/apps/httpd/目录里但lwIP还提供了一种机制允许你在工程里通过LWIP_HTTPD_CONF头文件覆盖默认配置。HTTPD常用的可调参数包括HTTPD_MAX_REQ_LENGTH请求行最大长度、HTTPD_SERVER_IDServer响应头内容、HTTPD_SSI_MAX_TAGSSSI标签数量上限、HTTPD_CGI_NUM_PARAMSCGI参数个数上限。这些参数在调试阶段可能一开始不需要改但等你在网页里加了很多动态内容、CGI传递的参数变多时不改就会遇到“参数解析失败”或“请求被截断”的怪异问题。在CubeMX生成的代码里lwipopts.h和httpd_conf.h的位置和宏默认值因版本而异。我建议不要一股脑复制网上的配置正确姿势是先在工程里找到这两个文件查看当前默认值然后针对你的需求做增量修改。同时要注意CubeMX在重新生成代码时会把lwipopts.h覆盖掉如果你在CubeMX的“Additional parameters”里没把这些宏写进去代码就不见了。我的习惯是把自定义配置放到一个独立的头文件里比如my_lwip_config.h然后在初始化代码里包含它避免被CubeMX覆盖。4. 实际调试中踩过的坑从网口灯不亮到页面打不开4.1 问题一PHY地址配置错误link状态检测失败网络调试最让人抓狂的不是有报错而是什么都没报错但就是不通。我遇到最多的就是PHY地址问题。CubeMX在配置lwIP时有一个LWIP_PHY_ADDRESS参数这个值必须和板子上PHY的硬件地址一致。前面讲过原理图会决定PHYAD引脚的电平LAN8720A的地址计算方式是默认0PHYAD0拉高就变成1。很多核心板厂家为了和别的PHY兼容会默认把PHYAD0上拉地址就是1。排查方法是在系统初始化后、启动HTTPD之前手动调用HAL_ETH_ReadPHYRegister读PHY的基本寄存器0BCR和寄存器1BSR。如果读出来不是合法值大概率地址不对。如果想看link状态读BSR的bit2Link Status0表示断开1表示连接。如果你在串口打印里看到Link Status始终为0而网线明明插着八成是PHY配置问题。还有一个隐藏坑LAN8720A在默认情况下如果没有外部复位信号上电后内部可能处于低功耗模式或未完成自协商导致link状态不对。软复位方法是写BCR寄存器的bit15置1等待至少1ms后再读。我一般在初始化流程里加一段“PHY软复位延时50ms等待自协商完成”的代码实测对解决Link检测失败非常有帮助。4.2 问题二内存不足导致多个连接失败HTTPD服务器一旦跑起来浏览器可不会只开一个TCP连接。一个网页通常包含HTML、CSS、JS、favicon.ico等多个请求浏览器会建立多个并发TCP连接每个连接都要占用lwIP的PCB进程控制块、PBUF和内存堆。如果内存配置偏小会出现“第一个网页打开正常刷新几次或打开子页面后就卡住”的问题。这种问题的直接表现是netconn或socket创建失败但串口日志通常看不出异常。解决方法是先确认lwIP内存统计是否开启。在lwipopts.h里打开LWIP_STATS_DIAG和LWIP_STATS在串口输出连接统计数据。注意lwIP的stats有TCP、UDP、PBUF、SYS等多个维度看tcp_connects、tcp_actives的op/s和max值如果op/s频繁接近上限就说明需要增大内存池。我实际调试HTTPD加WebSocket场景时把MEM_SIZE从默认的几百字节提高到十几KBTCP_MSS设为1460TCP_WND设为8个MSS大小TCP_SND_BUF同步调大PBUF_POOL_SIZE增加到16个左右才稳定支撑了一个包含CSS和图片的页面。如果你的页面有大量图片图片通过HTTPD发送时会被拆成多个TCP段内存消耗会陡增。4.3 问题三HTTPD中没有放入网页数据/数据未对齐这个坑相当隐蔽。makefsdata生成的fsdata.c里网页数据是放在只读数组里的而lwIP HTTPD在发送数据时会以指针的方式直接引用这些数组。如果数组未对齐到4字节边界在某些平台会导致HardFault。lwIP官方通常建议在链接脚本里把fsdata段放到一个4字节对齐的地址。对于STM32F407 IAR或者Keil环境你可能需要修改链接脚本或指定__attribute__((aligned(4)))或者直接编译时对齐。我在Keil工程中遇到过一次奇怪问题HTTPD能响应HTTP请求但响应内容长度不对页面内容被截断或混杂乱码。排查下来发现是fsdata.c中每个文件的长度字段读到的值和实际数组长度不一致。最后通过把网页文件全部换成小写文件名、重新生成fsdata.c、清理编译缓存解决。很多奇怪问题其实是缓存了旧文件导致的凡是改了网页内容后务必全量重新编译一遍。另外如果你想在运行时动态生成网页内容、而不是用静态网页可以走CGI/SSI路线。HTTPD会在发送静态数据时扫描SSI标签如果数据量很大扫描过程会消耗CPU但通常网页只有几KB影响基本可以忽略。CGI是另一种思路HTTPD解析到某个URL后调用你注册的回调函数把动态生成的HTML片段返回给浏览器。CGI回调函数里注意不要做耗时的阻塞操作比如大延时、等待UART接收否则会影响服务器的并发能力。4.4 问题四PC连不上检查ARP和ping移植网络功能时很多人习惯连上板子就直接在浏览器里敲IP但是如果说页面打不开应该先从最底层的连通性开始排查。第一步PC上设置和板子同一个网段的静态IP然后ping目标IP。ping不通的时候先看有线网卡是否有黄色感叹号再用arp -a检查ARP缓存确认PC是否发出了ARP请求。如果ARP表里没有板子的MAC地址说明二层通信没建立起来问题多半在PHY链路或MAC初始化。如果ARP有MAC但ping不通问题可能出在协议栈的IP处理上。在FreeRTOS lwIP的环境里有一个很常见的原因是tcpip_thread没跑起来或者优先级太低。如果你用CubeMX自动生成的代码它已经把MX_LWIP_Init()调用放到了main()里并且默认启动了tcpip_thread。但如果你的任务优先级配置不合理tcpip_thread始终得不到调度那板子看起来“没反应”。调试时可以在串口打印netif_is_up( netif )的结果确认协议栈是否认为网络已就绪。还有一类问题是网线类型。现在的千兆网卡一般都能自适应百兆百兆但个别老式交换机的Auto MDI/MDIX做得不好交叉线和直连线都要试一下。我的经验是先尽量用直连线接电脑网口不要一上来就塞到路由器上减少变量。5. 功能扩展用HTTPD实现LED控制与状态查询5.1 设计思路GET请求携带参数CGI解析即可HTTPD做动态功能最常用的就是CGI。当浏览器请求一个特殊URL时lwIP HTTPD会调用你注册的CGI处理器这个函数接收URL和参数字符串处理完后返回一串HTML。以“网页控制LED”为例思路是定义一个URL/led.cgi浏览器请求/led.cgi?led1onCGI回调里判断led1参数的值执行对应的GPIO操作再返回一个带按钮的页面。在httpd_conf.h里注册CGI的方式是写一个tCGI数组映射URL和处理函数。lwIP有httpd_cgi_handler的注册接口通常是在httpd_init()之后调用。在FreeRTOS环境中CGI回调运行在tcpip_thread的上下文中所以回调里最好不要直接调用HAL_GPIO_WritePin之外的阻塞操作。如果你要在CGI里读取大量数据应该先把数据准备好CGI只负责快速输出。5.2 动态显示传感器数据SSI是最好的选择除了CGI你还可以用SSI在静态页面里插入动态值。比如HTML里写!--#temp--HTTPD扫描到这种特殊标签后会调用你注册的SSI回调函数把标签替换成实际的传感器值。相比CGISSI更适合“页面模板固定、只有数值变化”的场景比如仪表盘、状态页。SSI配置需要两步第一步在HTML里插入标签标签格式在lwIP里可以自定义默认是!--#tagname--。第二步在代码里注册一个tSSIHandler回调它根据传入的标签名查表把对应的字符串拷贝到输出缓冲区。注意SSI回调的执行频率是“每次页面被请求时都会执行”如果回调里做太多计算会影响HTTP响应速度。我实现传感器状态页时用SSI插入温度、湿度、系统运行时间三个变量。实际测试下来刷新整个页面串口打印显示HTTPD扫描和处理数据规模都不大CPU占用率很低。SSI标签数量有上限HTTPD_SSI_MAX_TAGS如果你不小心把一个动态页面写了几十上百个标签可能需要调大这个宏。5.3 多页面/资源文件的管理建议当你的设备Web界面比较复杂包含多个页面、CSS、JS甚至图片时建议把网页源文件用文件夹分类管理比如fs/index.html、fs/control.html、fs/style.css、fs/jquery.min.js。makefsdata工具会递归扫描目录并把文件路径转换成URL路径。注意文件路径分隔符统一用/不要用\。还有一个小技巧把JS和CSS文件压缩后放进固件可以明显减少传输时间因为HTTPD每次发送文件都要占用内存和带宽。如果你用了jQuery库这个库文件通常上百KB势必压占内存。建议要么精简JS功能要么把页面拆成多个小文件按需加载。轻量页面体验远好于一个巨型页面。6. 常见故障速查一张表解决80%的问题总的来说STM32F407 lwIP HTTPD的移植过程说难不难但坑也不少。我把实际调试中遇到的典型问题和排查方法整理成了一张表方便你以后对照排查。现象可能原因排查思路网口灯不亮PHY未复位、RMII时钟异常、PHY地址错误检查PHY复位引脚时序用万用表/示波器确认50MHz时钟读PHY寄存器能ping通但浏览器打不开TCP端口未监听、HTTPD未初始化、网页数据未生成确认httpd_init被调用检查PC浏览器访问的IP和端口串口打印HTTPD错误日志页面打开一半就断了内存不足、TCP窗口太小、文件过大增大PBUF_POOL_SIZE、TCP_WND、TCP_SND_BUF拆分大文件中文乱码文件编码不是UTF-8、HTML未声明字符集HTML里加meta charsetutf-8确保源文件也是UTF-8编码开机后等很久才通网PHY自协商耗时、DHCP超时适当调短DHCP超时打印link状态日志确认协商结果频繁HardFault数据未对齐、内存越界、中断优先级不当检查fsdata数组对齐打开MPU或查看回溯地址降低中断优先级实际项目中我建议在串口调试里把网络的连接、断开、HTTP请求都打出来特别是HTTPD自带的调试宏一旦打开它会打印请求方式、URI、返回码等信息。定位问题会快很多。等产品稳定落地后再把这部分日志关掉或者压到DEBUG级别。HTTPD这套方案的开发难度主要集中在底层适应上一旦跑通后面增加页面、增加接口都只是“重复劳动”。对于产品原型验证、小型物联网设备的管理页面它是最省的方案之一。如果你后续需要更复杂的功能比如WebSocket推送、RESTful API、HTTPS加密则可以考虑在lwIP上引入mbedTLS和第三方网络框架但那是另外一回事了。