上篇文章我们把 STM32F407 的 ETH 外设和 PHY 芯片跑通之后,网口其实还只能看到物理层 Link 信号,真正能让板子和电脑通信的是 IP 层以上的协议。这篇我打算把 lwIP 协议栈完整移植进去,然后在上面搭一个 HTTPD 服务器,让浏览器直接访问开发板上的网页,顺便通过 GET/POST 参数控制开发板上的资源。整个系列已经聊过 CubeMX 工程怎么建、ETH 初始化怎么做,这篇会直接用实例带你把 lwIP 跑起来。如果你之前没接触过协议栈移植,看到 HTTPD 三个字母先别慌,它其实就是一个跑在单片机上的、精简到极致的小型 Web 服务器,适合用来做设备状态查看、参数配置、简单控制面板,很多商业产品的出厂配置页面就是这么做的。
lwIP 是开源世界里被用到烂熟的嵌入式 TCP/IP 协议栈,纯 C 编写,几乎能在任何单片机上跑。F407 自带以太网 MAC 控制器,配合外挂 PHY 芯片,跑 lwIP 是非常成熟的组合。要说这和直接在电脑上装 Linux 里跑 Nginx 有什么区别,本质上是资源的巨大差异,F407 只有 192KB RAM,所以协议栈的每个字节都在精打细算。你把它想成在只有 3 平米的小房间里做收纳,每个抽屉放什么、哪块空间留给大件,都要提前规划。这篇会从 CubeMX 配置、内存模型、底层驱动衔接、HTTPD 页面组织这几个方面逐层拆开,最后附上我在联调时踩过的坑和速查表。
1. 动手之前,先认清F407的以太网资源
1.1 硬件层面的关键前提
F407 内部有 MAC,也就是媒体访问控制层,但物理层收发器需要外接 PHY 芯片。市面上常见的搭配有 LAN8720A、LAN8742A、DP83848,这些 PHY 基本都支持 RMII 接口模式。RMII 相比 MII 的最大优势是引脚少,MII 需要 16 根数据线,RMII 只要 4 根信号线加一根 REF_CLK 参考时钟,对板子布线来说压力小很多。
CubeMX 里配置 ETH 外设时,会让你选择 RMII 还是 MII。除非你的 PHY 特别老、只支持 MII,否则我建议你直接选 RMII。选 RMII 的同时要注意 REF_CLK 的 50MHz 时钟来源,不同 PHY 模块的时钟方案可能不一样。有的 PHY 芯片自带 50MHz 晶振,有的则由 MCU 的 MCO1 引脚输出。我手上这块开发板用的是外部有源晶振给 PHY 供时钟,MCU 侧只需要将 PA1 配成 ETH_REF_CLK 输入,所以在 CubeMX 里生成代码后基本不做额外改动。若你的板子是靠 MCO1 输出,记得要在 GPIO 初始化里把 PA8 复用为 MCO1,输出频率配成 50MHz,否则 PHY 时钟不对,link 都建立不起来。
另一个硬件层面的问题是 PHY 地址。同一片 I2C 或 MDIO 总线上挂多个 PHY 时,地址不能冲突。LAN8742A 的地址默认通常是 0,DP83848 常见是 0x01,具体由 PHY 芯片的引脚上下拉决定,必须以你板子的原理图为准。CubeMX 里填的 PHY Address 要和实际芯片一致,否则读 PHY 寄存器会失败或者读到全 F。调试时如果发现 HAL_ETH_Init 卡在 PHY 通信环节,第一件事就是用示波器或逻辑分析仪看 MDC/MDIO 上有没有波形,别看软件参数,先查硬件连接。
1.2 CubeMX + lwIP 的常规落地方案
裸机跑 lwIP 有两种主流路线。一种是从 lwIP 官网或者 GitHub 拉源码,手动加入工程,自己处理 lwipopts.h、cc.h、sys_arch.h 这些头文件,配置灵活性高,但需要提前理解一堆宏;另一种是直接用 CubeMX 集成的 lwIP 中间件,勾选后自动生成 lwip.c、ethernetif.c,你在图形界面里填参数,生成代码后稍作修改就能跑。
很多网友觉得 CubeMX 生成的代码“很黑盒”,不敢用。我的实际体验是,CubeMX 对于 F407 + lwIP 这套组合已经相当成熟,生成代码的可读性也不错。你能在 lwip.c 里清楚看到 netif 的添加、IP 地址的设置、DHCP 的启动逻辑,在 ethernetif.c 里能看到底层收发包和 HAL 库的对接。项目时间紧、需求标准的时候,CubeMX 路线能省掉大量手工配置头文件的时间,尤其是 Keil 工程里源文件分组、编译器宏定义、链接脚本分散加载这些事,CubeMX 都自动处理好了。
CubeMX 的坑在于版本差异。不同版本的 CubeMX 集成的 lwIP 版本不同,有的基于 2.0.3,有的基于 2.1.2,不同版本的 lwipopts.h 中宏定义变化不小。HTTPD 部分尤其明显,2.0 时代 SSI/CGI 的接口和 2.1 差异很大。所以在看资料的时候,先确认自己工程里的 lwIP 版本,别拿 2.0 的教程硬套 2.1 的代码,编译报错的时候你根本不知道哪里不对。
2. 用CubeMX落地lwIP时的关键开关和参数
2.1 lwIP 在 F407 上的内存模型
移植 lwIP 过程中最绕不开、也最容易出问题的就是内存管理。lwIP 内部存在多套内存分配机制,比如内存池 memp、堆内存 mem,以及配合零拷贝的 PBUF 池。CubeMX 生成的 lwipopts.h 里有一大堆选项,初学者往往会直接保持默认,结果发现调试时没问题,一上大流量就死机。
F407 的 RAM 分成两大块,一块是 128KB 的普通 SRAM,另一块是 64KB 的 CCM RAM。CCM RAM 虽然也属于片内存储,但 CPU 可以直接访问,外设 DMA 访问不了。而以太网 DMA 描述符、DMA 接收缓冲区这些数据必须放在普通 SRAM 中。如果你把 lwIP 的 PBUF 池放到 CCM RAM,一旦 DMA 往这块地址写数据,网络直接卡死或者硬件错误。CubeMX 默认工程里不会有这问题,因为链接脚本默认把 DATA/HEAP 放在 SRAM,但如果后期你自己调分散加载文件,一定要注意。
内存分配策略也有讲究。CubeMX 会让你选择内存分配方式,常见是 MEM_LIBC_MALLOC 或 MEM_CUSTOM。裸机下建议让 lwIP 自己管理内存,也就是不把 malloc 重定向到 C 库,而是让 lwIP 使用自己的堆。C 库 malloc 在多线程环境下有锁的开销,裸机下虽然没锁问题,但堆碎片控制不如 lwIP 自己管理直观。网上有些方案喜欢把 lwipopts.h 里的 MEM_LIBC_MALLOC 打开,能用,但调试起来不好观察内存池使用情况,出了问题也不好定位是哪个协议层在申请内存。
实测下来最稳的路线是:裸机工程里关掉 MEM_LIBC_MALLOC,lwIP 内部内存池由 lwIP 自己管理,需要观察内存余量时直接调用 mem_free_count 之类的函数。你可以在串口命令里加一个 stats 命令,打印 LWIP_STATS 统计信息,这样在长时间运行后能确认内存池有没有被耗尽。
2.2 几个直接影响性能和运行稳定性的开关
CubeMX 的 LWIP 中间件配置页中,有一堆参数平时用默认值确实能跑通 ping,但如果你做 TCP 大数据传输或者网页图片较多,下面这几个参数值得单独调整。
第一个是 MEM_SIZE,这个值决定 lwIP 堆的大小,单位是字节。默认值可能比较小,如果网页文件较大、同时打开了多个 TCP 连接,堆不够就会出现内存申请失败,表现是连接频繁断开。裸机下经验值是 10KB 到 40KB 之间,具体看你剩下多少 RAM,毕竟 lwIP 的堆和 PBUF 池、DMA 缓冲都在普通 SRAM 里挤。
第二个是 TCP_WND,代表 TCP 接收窗口大小。默认值如果远小于你的应用单次传输数据量,TCP 吞吐会卡在窗口上限上。F407 上给 16KB 或 32KB 相对合理,再高就要算清楚还有没有足够的 PBUF 容纳数据。调大接收窗口的同时,一般还要把 PBUF 池数量调上去,否则窗口中通告的剩余空间始终上不去。
第三个是 TCP_SND_BUF 和 TCP_SND_QUEUELEN,这两个组合决定发送缓冲大小以及能排队的发送包数量。HTTPD 返回页面时一般几 KB 数据一次性写入,发送缓冲太小会导致 tcp_write 返回 ERR_MEM,页面发不完整。调试时如果浏览器刷新偶尔出现页面残缺,多半是发送缓冲不够,把 TCP_SND_BUF 往上提一档基本能解决。
你可以把这三个参数理解成仓库的三个库位:MEM_SIZE 是总仓库面积,TCP_WND 是收货区能同时摆放多少货,TCP_SND_BUF 是发货区能堆多少货。仓库太小、收货区太小或者发货区太小,都会让流程卡住,只是卡的位置不同而已。
2.3 RTOS 场景的坑位提醒
如果你的工程接了 FreeRTOS,CubeMX 配置 LwIP 时通常会默认把 NO_SYS 设为 0,也就是让 lwIP 跑在 tcpip_thread 线程里。此时所有协议栈内部操作都归 tcpip_thread,你的应用线程通过 API 接口访问网络。裸机虽然也能用 NO_SYS=1 的 raw API 跑 HTTPD,但要自己在主循环里调用 httpd_init 和周期处理函数。
我建议优先用带 RTOS 的方式跑 HTTPD。原因很简单,HTTPD 的 httpd_init 会建立 TCP 监听,连接建立后收包、解析、回调这些过程如果全部塞在主循环里,一旦某个浏览器连接不关闭或页面响应慢,主循环会被拖死。有了 RTOS,协议栈在独立线程里跑,应用层做自己那摊事,即使网络卡住,也不影响按键扫描或 LED 闪烁。
用 RTOS 时一个容易忽视的点是线程栈大小。tcpip_thread 和 httpd 涉及的线程栈不能设太小,否则会莫名其妙死机。如果 HTTPD 的 CGI 回调里需要处理比较大的 JSON 数据,建议把 tcpip_thread 栈调到 1024 字(4KB)以上,否则某些堆栈大的库函数一调用就把栈顶踩穿。另一个和 RTOS 相关的坑是临界区与中断嵌套,Ethernet 中断里尽量不要调用任何可能引起阻塞的 lwIP 函数,正确做法是先通过信号量或消息队列通知 tcpip_thread 来处理,否则中断上下文里等待信号量会直接触发断言。
3. 从 ETH 外设到 netif:底层驱动要做的事
3.1 PHY 与 RMII 连接常见问题
很多人 CubeMX 配置完 ETH 后编译下载,发现开发板网口指示灯根本不亮。这种时候先别怀疑 lwIP 配置,多半是 PHY 的复位或者时钟出了问题。PHY 芯片通常带一个复位引脚,由 MCU 的 GPIO 控制。如果你在 CubeMX 里没有给 PHY 复位引脚分配 GPIO,代码里也没有拉低再拉高的复位动作,PHY 可能处于复位状态或者上电后锁定状态未知,那就彻底没戏。
我在调试时习惯在 MX_ETH_Init 之前加一段延时和复位逻辑,GPIO 先输出低电平保持 20ms,再拉高并延时 100ms,让 PHY 完全启动。这个操作粗看没什么技术含量,但能避免很多“时好时坏”的问题。PHY 芯片上电初始化需要时间,如果你复位后立刻读寄存器,可能读到的是上电暂态值。
RMII 模式下如果出现能 ping 通但吞吐极低、或者频繁超时的情况,要检查 PHY 的 REF_CLK 是否真的稳定。有些开发板为了兼容多种 PHY,REF_CLK 通过跳线选择由 MCU 还是外部晶振提供。跳线帽没插或者插错位置,PHY 虽然能偶尔工作,但时钟不够干净,以太网包就会时不时 CRC 错误。
3.2 底层数据收发路径
CubeMX 生成的 ethernetif.c 已经把 HAL 库和 lwIP 的 netif 结构绑定好了。你需要理解的核心函数就三个,一个是 low_level_init,负责初始化 DMA 描述符和 MAC 地址;一个是 low_level_input,负责从 DMA 接收描述符中取出一帧数据,封装成 PBUF 交给上层;还有一个是 low_level_output,负责把上层传来的 PBUF 数据写入 DMA 发送描述符,触发发送。
接收路径的处理方式决定了你的 CPU 占用。F407 的以太网 DMA 支持接收中断,可以在接收完成时挂起一个二值信号量,让 tcpip_thread 被唤醒后去收数据。裸机模式下则通常在主循环中周期调用 ethernetif_input 函数,每次只处理一帧。如果你跑的是裸机,建议接收中断里只做标志位置位或者信号量释放,主循环里集中处理,避免在中断里做太多事情拖慢其他中断响应。
发送路径一般不用你操心太多,应用调用 tcp_write 之后,lwIP 内部会调用 netif->output,最后走到 low_level_output。这个函数里要注意 DMA 描述符的 ownership 标志,等待上一次发送完成再修改描述符,否则可能覆盖还没发完的包。CubeMX 生成的代码通常已经处理好了,但如果改动过描述符数量或者缓冲区大小,一定要检查描述符链表有没有形成正确的环形结构,否则发送到第几个包就会 HardFault。
4. HTTPD 服务器的页面组织与动态接口设计
4.1 页面资源的三种做法
HTTPD 的核心功能是响应浏览器发来的 HTTP 请求。你需要在 F407 的 Flash 或文件系统里准备好网页内容。最简单的做法是用 fsdata 数组,也就是把 HTML、CSS、JS 经过工具转换成 C 语言数组,直接编译进固件。这种方式适合页面内容基本固定、大小在几十 KB 以内的场景。浏览器请求某个路径时,HTTPD 在 C 数组里查表找到对应数据,返回给浏览器。
第二种做法是外挂 SPI Flash 或 SD 卡,把网页文件放到 FatFS 文件系统里。这个方案的好处是更新网页不用重新烧固件,缺点是要写一层文件读取接口接入 HTTPD,改动量明显增加。产品已经带了 SPI Flash 且需要远程升级页面时,这种方案很值得投入。第三种是混合方案,核心页面编译进数组保证可用性,业务页面从外部文件系统读取,兼顾可靠性和灵活性。
如果你只是做功能验证,我建议先走 fsdata 数组路线。F407 有 1MB Flash,放几个几十 KB 的页面绰绰有余,不必为了“工程感”强行上文件系统,徒增移植工作量。网页转 C 数组可以用 lwIP 官方仓库里自带的 makefsdata 工具,也可以用网上常见的 bin2c 小工具,只要最终生成的数组是 const unsigned char 类型且按 4 字节对齐即可。
我用 makefsdata 批量转换时遇到过编码问题,页面里如果包含中文字符,源文件必须是 UTF-8 编码,否则浏览器打开显示乱码。这个问题不在 lwIP 身上,纯粹是文本编码问题,排查起来却很费劲。所以提前规范化 HTML 文件编码很有必要。
4.2 CGI 动态处理接口
HTTPD 提供两类动态内容接口,一类是 CGI,另一类是 SSI。区别在于 CGI 适合处理“请求时才有结果”的数据,比如 LED 开关状态、传感器读数值、配置参数提交;SSI 适合在静态页面中嵌入运行时变量,比如设备温度、连接状态、运行时间。
以 lwIP 2.1.x 为例,你要使用 CGI 需要先在 httpd_opts.h 里打开 LWIP_HTTPD_CGI 宏,然后在 httpd.c 或专门的 cgi 文件里注册一个处理表。处理表的每一项包含 URL 匹配串和一个回调函数指针。当浏览器请求 http://192.168.1.10/led_on.cgi 时,HTTPD 会解析出 /led_on.cgi,去匹配你登记的 CGI 表,命中后调用相应的回调。回调里可以操作 GPIO,最终返回的字符串会被当作 HTTP 响应内容发给浏览器。
我自己的习惯是让 CGI 处理完操作后返回一个重定向 HTML 页面,而不是直接返回数据,比如:
HTTP/1.1 302 Found Location: /index.shtml这样浏览器地址栏会跳回主页,用户按 F5 也不会重复触发上次的操作,避免了“刷新一次就翻转一次 LED”的尴尬。如果你执着于让 CGI 返回数据,注意返回的字符串生命周期。HTTPD 不会帮你拷贝,如果你返回的是栈上的临时数组,函数一退出内容就失效了,必须用 static 变量或全局变量。
下面给一个简单的 CGI 处理框架,你可以根据自己的需求扩展:
static const char *led_cgi_handler(int iIndex, int iNumParams, char *pcParam[], char *pcValue[]) { int i; static char page_back[256] = "HTTP/1.1 302 Found\r\nLocation: /\r\n\r\n"; for (i = 0; i < iNumParams; i++) { if (strcmp(pcParam[i], "state") == 0) { if (strcmp(pcValue[i], "on") == 0) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); } else { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); } } } return page_back; }注册 CGI 表时,不同版本 lwIP 的方式略有差异。有些版本直接让你改 httpd.c 里的静态表,有些版本提供 httpd_set_cgi_handlers 函数。我的经验是先搜一下当前工程 httpd.c 里的 cgi 相关关键字,找到现成的表结构照着填就行,不用死记硬背 API。
4.3 SSI 动态占位替换
SSI 的典型应用场景是 HTML 页面中有类似<!--#temperature-->的占位符。HTTPD 解析页面时遇到这个标记,会调用你提供的 SSI 回调函数,用返回值替换掉占位符。浏览器最终收到的是完整的数据页面。这样做的好处是把数据显示逻辑嵌入到纯静态 HTML 模板里,前端人员可以直接维护 HTML 文件。
启用 SSI 需要在 httpd_opts.h 打开 LWIP_HTTPD_SSI 宏,同时还要打开 LWIP_HTTPD_SSI_MULTIPART,这个宏决定回调的支持能力。打开后需要实现一个get_ssi_tag之类的回调,按标签索引返回字符串。lwIP 不同版本这里签名变化较大,有的用枚举,有的用字符串查找,我建议你在写代码前先打开当前工程的头文件确认,别直接从网上抄一段就编译。
我做一个气象站页面时用 SSI 显示温湿度,实现思路是给每种动态数据分配一个固定标签,比如<!--#temp-->代表温度,<!--#humi-->代表湿度。HTML 页面编写时先用假数据排版,CSS 验证通过后把假数据替换成占位符,再交给 makefsdata 转数组。这样页面预览效果和实际渲染效果基本一致,前端同事拿到 HTML 也能顺手改样式,不用担心碰业务逻辑。
有一点要留意,SSI 回调返回的动态数据长度不能超过 httpd_opts.h 里设定的缓冲区长度,如果数值格式化后太长,会被截断导致页面出现残缺。预留足够缓冲空间,同时尽量缩短动态数据的长度,比如温度保留一位小数足够,就不要格式化出五位有效数字。
5. 联调阶段的排查清单与实测心得
5.1 分阶段验证避免一锅粥
移植协议栈和 HTTPD 服务器不是写完代码就能直接在浏览器里输 IP 的,建议按下面顺序一步步验证,每步成功后再进入下一步。第一步是确认物理层 Link,开发板上电插上网线,PHY 的 Link 指示灯应该亮起来;如果灯不亮,大概率是硬件问题,先回去查时钟和复位。第二步是 Ping 通,在电脑上把网卡 IP 配成和开发板同一网段,ping 开发板地址,能通说明 IP 层已经工作;不通就抓包看有没有 ARP 请求、有没有回应,重点排查底层驱动。
第三步是访问静态页面,在浏览器输入开发板 IP,如果能看到默认首页,说明 HTTPD 服务已经成功监听 80 端口并能正确返回页面数据。第四步是测试 CGI 和 SSI 接口,逐个验证动态控制、数据刷新功能。如果只是 ping 不通,不要急着翻 HTTPD 代码,大概率是底层问题。
我的经验是调试时优先用网线直连电脑和开发板,不要经过路由器。直连可以减少交换机、路由器带来的变量,IP 地址手动设置,省掉 DHCP 等待。如果开发板配置了 DHCP 功能,而电脑又没有 DHCP 服务器,等 DHCP 超时会浪费很多时间。直连时建议在 lwIP 配置里关闭 DHCP,手动指定静态 IP,比如 192.168.1.10,电脑网卡配 192.168.1.2,掩码 255.255.255.0,确定能通之后再考虑 DHCP 场景。
调试 HTTPD 时利用 Wireshark 是最快的方式。看到浏览器发来 TCP SYN,开发板回应 SYN-ACK,说明 TCP 层已经通。如果一直重传 SYN-ACK,说明开发板发出的包没有回包,很多原因是电脑防火墙拦了陌生网段的入站连接,临时关闭防火墙验证一下。
5.2 常见问题排查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 网口 Link 灯不亮 | PHY 复位、时钟、RMII 引脚配置 | 查 PHY 复位时序,查 REF_CLK,用万用表检查供电 |
| 能 Link 但 Ping 不通 | DMA 描述符、IP 地址、底层收发 | 用Wireshark查ARP,确认PHY地址和寄存器读取 |
| Ping 通但浏览器打不开 | HTTPD 未初始化、端口占用、页面数组未注册 | 确认 httpd_init 已调用,确认 fsdata 包含 index 页面 |
| 页面加载但图片显示不出来 | MIME 类型缺失、图片数组过大 | 检查 fsdata 的 mime 表,是否注册 jpg/png;调大发送窗口 |
| 刷新几次后死机 | 内存池耗尽、DMA 缓冲不足 | 打开 LWIP_STATS 观察,调大 MEM_SIZE、MEMP_NUM_TCP_SEG |
| CGI 触发后无响应 | CGI 表未注册、URI 不匹配 | 串口打印URI,检查匹配串前后是否有空格 |
| SSI 标签原样显示在浏览器 | SSI 宏没开或回调未实现 | 确认 LWIP_HTTPD_SSI 开启,确认回调函数已注册 |
这张表里的每一项我都实地踩过。最典型的莫过于页面数组没注册,makefsdata 做出来的数组单独定义了页面内容,但如果你没有把它挂到 HTTPD 的索引表里,浏览器访问根路径时返回 404。这时不要抱怨 HTTPD 不好用,先检查 pages 数组里有没有 index.shtml 这一项。
5.3 我的几个小习惯
HTTPD 这个服务器本身功能有限,它不会像 PC 上的 Nginx 那样自动处理连接并发、超时重传等一堆问题,所以编写页面和接口时有一些小约定能减少很多毛病。
页面尽量做小。一个首页加一个控制页,总大小控制在 20KB 以内。F407 跑 HTTPD 的优势是低功耗和实时控制,不是搞大文件传输。如果页面里放一张几百 KB 的背景图,首次加载时会长时间占着 TCP 连接,期间其他请求都会被阻塞。真需要图片展示的话,把图压缩成小尺寸 JPEG 或者用纯 CSS 画效果,别拖累性能。
日志输出是定位问题的好朋友。我习惯在 ethernetif、CGI 回调、HTTPD 初始化这些关键路径上加串口打印,专门输出当前状态和错误码。在排查连接异常时,串口日志能直接告诉你数据收发到了哪一步,省得反复猜测。量产时关闭这些打印宏,调试时打开,编码时预留一个宏开关很划算。
6. 如果能重做一次,我会这样优化
HTTPD 服务器搭建完成并跑通了基础功能之后,建议你再往前走一步,把代码结构整理成模块化。比如把所有网页相关的 fsdata 数组单独放到 webpage.c/h,把 CGI 和 SSI 的处理函数统一放到 web_dynamic.c/h,和 lwIP 的底层移植代码分开。这样后续如果想让 HTTPD 支持新的接口,只需要改动业务层文件,不影响底层稳定性。
另外可以考虑添加一个简单的登录校验页面。HTTPD 代码里支持 Basic Auth 需要的逻辑并不多,核心就是判断 HTTP 头里的 Authorization 字段。虽然这种认证方式属于明文传输,谈不上绝对安全,但对内网调试、局域网的设备配置来说已经足够挡住大多数无意访问。再配合 CGI 里对参数长度做限制,避免超长字符串导致内存覆盖,产品化的雏形就出来了。
如果之后要做更大规模的数据交互,比如网页需要实时刷新传感器曲线,用 HTTPD 反复轮询并不是最佳方案。可以考虑在 lwIP 基础上增加 WebSocket 或者 MQTT 通道,但那属于另一个话题了。就目前这个阶段,稳定的静态页面访问、可靠的 CGI 控制、实时的 SSI 数据刷新,已经能覆盖绝大多数轻量级嵌入式 Web 应用需求。实际跑起来后你会发现,F407 在 100M 以太网下撑住几个浏览器的访问毫无压力,真正需要关注的反而是你代码里不小心留下的内存碎片和阻塞点。这大概就是嵌入式软件有意思的地方,板子性能就那么多,但每一个合理的分配都能换回应有的回报。