☰
STM32F4 LWIP实战:DHCP、DNS与HTTP Client全链路打通
2026/10/14 1:41:31 网站建设 项目流程

1. 从一个“跑不通”的例程说起

搞嵌入式网络开发的朋友,大概率都经历过这样的场景:板子上电,网线插好,串口打印出一行DHCP...,然后就卡住了,或者干脆返回一个0.0.0.0的地址,再或者 DHCP 拿到了地址,但一发起 HTTP 请求就石沉大海。STM32F4 加上 LWIP 这套组合,几乎是每个嵌入式工程师绕不开的经典搭配,但真正把 DHCP、DNS、HTTP Client 这三样东西串起来跑通,中间踩的坑远比想象中多。

这篇内容围绕一个具体的例程展开——STM32F4 平台,跑 LWIP 协议栈,实现 DHCP 自动获取 IP、DNS 域名解析、以及 HTTP Client(本质上是 TCP Client)发起请求。我会把这个例程的整体设计思路、关键配置、代码实现细节、以及实际调试中遇到的各种问题,尽可能完整地拆开来讲。不管你是刚接触 LWIP 的新手,还是已经用过但总在某些环节卡住的老手,应该都能从中找到对自己有用的东西。

先明确一下这个例程能做什么:板子上电后,通过 DHCP 自动从路由器获取 IP 地址、子网掩码、网关和 DNS 服务器地址;然后通过 DNS 把域名解析成 IP;最后用 HTTP Client 向目标服务器发起 GET 请求,拿到响应数据。整条链路打通之后,你就可以在此基础上扩展出 MQTT、OTA、数据上报等各种应用。适合有一定 STM32 基础、想搞明白 LWIP 网络编程完整流程的开发者参考。

2. 整体架构与方案选型思路

2.1 为什么选 LWIP 而不是其他协议栈

在 STM32F4 这个级别的 MCU 上跑网络,可选的协议栈其实不多。商用方案如 ThreadX NetX、FreeRTOS+TCP 都有各自的生态门槛,而 LWIP 的优势在于:开源、轻量、可裁剪、资料多、和 STM32 的适配层成熟。STM32CubeMX 里直接集成了 LWIP 中间件,配合 ETH 外设的 HAL 驱动,基本可以做到“勾选即用”。

但“勾选即用”只是假象。CubeMX 生成的代码只保证编译通过,真正跑起来还需要理解几个关键点:内存池配置、PHY 芯片驱动匹配、中断优先级、以及 DHCP/DNS/HTTP 这几个模块之间的协作关系。我见过太多人 CubeMX 生成完代码,编译没问题,下载进去就是不通,然后开始怀疑硬件。

2.2 DHCP、DNS、HTTP Client 三者的协作关系

这三个模块不是孤立的,它们有明确的依赖顺序:

  • DHCP负责拿到 IP 地址和 DNS 服务器地址。没有这一步,后面全是空谈。
  • DNS依赖 DHCP 拿到的 DNS 服务器地址,把域名翻译成 IP。
  • HTTP Client依赖 DNS 解析出的 IP,建立 TCP 连接,发送 HTTP 请求。

所以整个流程是一条链,任何一环断了,后面都跑不起来。这也是为什么调试的时候要分段验证,而不是一上来就跑完整流程。

2.3 裸机 vs RTOS 的选择

这个例程可以在裸机环境下跑,也可以跑在 FreeRTOS 上。裸机的好处是简单直接,主循环里轮询sys_check_timeouts()和dhcp_fine_tmr()等定时任务;RTOS 的好处是可以用信号量、消息队列来同步各个模块的状态,代码结构更清晰。

我个人的建议是:如果你只是验证功能,裸机足够;如果要做成产品,强烈建议上 FreeRTOS。因为 HTTP 请求是阻塞式的,裸机环境下你只能靠状态机来管理,代码会变得很臃肿。用 RTOS 的话,DNS 解析和 HTTP 请求可以放在独立线程里,主线程该干嘛干嘛。

注意:不管选哪种方式,LWIP 的sys_check_timeouts()必须被周期性调用,否则协议栈里的定时器全部失效,DHCP 和 TCP 重传都会出问题。

3. 核心细节解析与实操要点

3.1 CubeMX 中的关键配置项

用 CubeMX 配置这个例程,有几个地方必须手动调整,默认值大概率跑不通。

ETH 外设配置:PHY 地址要确认清楚,常见的是 0 或 1。PHY 芯片型号要选对,比如 LAN8720 和 DP83848 的寄存器操作就不一样。RMII 模式下,REF_CLK 的来源要搞清楚是外部晶振还是 MCU 输出。

LWIP 配置:LWIP_DHCP必须使能,LWIP_DNS必须使能,LWIP_TCP必须使能。MEM_SIZE默认值往往偏小,建议至少调到 16KB 以上。PBUF_POOL_SIZE也要适当加大,否则 HTTP 响应数据稍微大一点就分配不到 pbuf。

中断优先级:ETH 中断的优先级要设置合理。如果用了 FreeRTOS,ETH 中断优先级必须高于configMAX_SYSCALL_INTERRUPT_PRIORITY对应的数值,否则在中断里调用 FreeRTOS API 会触发断言。

下面是一个典型的 LWIP 关键参数配置参考:

参数默认值建议值说明
MEM_SIZE160016384内存堆大小,HTTP 响应需要较大缓冲
PBUF_POOL_SIZE1632pbuf 池数量,影响并发处理能力
TCP_SND_BUF25605840TCP 发送缓冲区
TCP_WND20485840TCP 接收窗口
DHCP_DOES_ARP_CHECK10关闭 ARP 检查可加快 DHCP 速度
DNS_TABLE_SIZE44DNS 缓存表项数

3.2 PHY 芯片初始化与链路检测

PHY 初始化是第一个容易翻车的地方。CubeMX 生成的MX_ETH_Init()会调用 HAL 的 ETH 初始化,但 PHY 的具体配置(比如自协商、速率设置)往往需要自己补充。

关键步骤是:复位 PHY,等待复位完成,配置自协商,然后轮询 BSR 寄存器等待链路建立。链路没建立之前,LWIP 的netif状态不会是LINK_UP,DHCP 也不会启动。

/* PHY 链路检测示例 */ uint32_t phy_link_status(void) { uint32_t bsr; HAL_ETH_ReadPHYRegister(&heth, PHY_BSR, &bsr); if (bsr & PHY_LINKED_STATUS) { return 1; } return 0; }

实际调试中,如果串口一直打印“link down”,先检查网线、路由器端口,再检查 PHY 地址和 RMII 时钟配置。我遇到过一次是 REF_CLK 没有正确输出,PHY 根本没工作,换了时钟配置就好了。

3.3 DHCP 启动与地址获取

DHCP 的启动很简单,dhcp_start(&gnetif)一行就够了。但要注意,调用之前netif必须已经netif_add并且netif_set_up。

DHCP 的状态可以通过dhcp_supplied_address(&gnetif)来判断是否拿到了地址。在裸机环境下,通常在主循环里轮询这个状态;在 RTOS 环境下,可以用回调或者信号量。

/* 等待 DHCP 获取地址 */ while (dhcp_supplied_address(&gnetif) == 0) { sys_check_timeouts(); HAL_Delay(1); }

这里有个细节:sys_check_timeouts()的调用频率直接影响 DHCP 的响应速度。如果主循环里还有其他耗时操作,DHCP 可能会超时重试。建议至少每 1ms 调用一次。

实操心得:如果路由器开启了 MAC 地址过滤,或者 DHCP 地址池已满,DHCP 会一直拿不到地址。调试时先用 PC 确认路由器 DHCP 正常,再排查板子这边。

3.4 DNS 解析的实现

DNS 解析用dns_gethostbyname()函数。这个函数是非阻塞的,如果缓存里有结果直接返回,没有的话会发起 DNS 查询,并在完成后调用回调函数。

static void dns_found_callback(const char *name, const ip_addr_t *ipaddr, void *callback_arg) { if (ipaddr != NULL) { /* 解析成功,保存 IP */ target_ip = *ipaddr; dns_done = 1; } else { /* 解析失败 */ dns_done = -1; } } /* 发起 DNS 查询 */ err_t err = dns_gethostbyname("example.com", &target_ip, dns_found_callback, NULL); if (err == ERR_OK) { /* 缓存命中,直接可用 */ dns_done = 1; } else if (err == ERR_INPROGRESS) { /* 等待回调 */ } else { /* 出错 */ }

DNS 解析失败的原因通常有几个:DNS 服务器地址没拿到(DHCP 没配 DNS)、DNS 服务器不可达、域名拼写错误。调试时可以先在 PC 上用nslookup确认域名能解析,再排查板子。

3.5 HTTP Client 的 TCP 连接与请求构造

HTTP Client 本质上是 TCP Client。流程是:创建 TCP PCB,连接到目标 IP 的 80 端口,发送 HTTP 请求报文,接收响应,解析,关闭连接。

/* 创建 TCP PCB */ struct tcp_pcb *pcb = tcp_new(); if (pcb == NULL) { /* 内存不足 */ } /* 连接到服务器 */ err_t err = tcp_connect(pcb, &target_ip, 80, tcp_connected_callback);

tcp_connected_callback里发送 HTTP 请求:

static err_t tcp_connected_callback(void *arg, struct tcp_pcb *tpcb, err_t err) { const char *request = "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"; tcp_write(tpcb, request, strlen(request), TCP_WRITE_FLAG_COPY); tcp_output(tpcb); tcp_recv(tpcb, tcp_recv_callback); return ERR_OK; }

tcp_recv_callback里处理响应数据,收到FIN后关闭连接。

这里有几个坑:tcp_write之后必须调用tcp_output,否则数据不会立即发送;tcp_recv要在连接建立后设置,否则收不到数据;响应数据可能分多次到达,要处理好拼接。

4. 完整实操流程与关键环节实现

4.1 硬件准备与基础验证

先确认硬件:STM32F4 开发板、网线、路由器(开启 DHCP)、串口调试工具。下载一个最简单的 ETH 回环测试或者 ping 测试例程,确认硬件和 PHY 驱动没问题。这一步不能跳过,否则后面出了问题你分不清是硬件还是软件。

基础验证通过后,再上 LWIP + DHCP。串口打印出获取到的 IP 地址,用 PC ping 一下,通了再继续。

4.2 DHCP 调试与验证

DHCP 调试的核心是看状态机走到哪一步了。LWIP 的 DHCP 状态可以通过gnetif.dhcp->state查看,常见状态有DHCP_STATE_SELECTING、DHCP_STATE_REQUESTING、DHCP_STATE_BOUND等。

如果一直卡在SELECTING,说明 DISCOVER 包发出去了但没收到 OFFER。检查网线、路由器 DHCP 服务、防火墙设置。如果卡在REQUESTING,说明收到了 OFFER 但 REQUEST 没被 ACK,可能是 IP 冲突。

拿到地址后,串口打印一下 IP、掩码、网关、DNS,确认都合理。DNS 地址如果是0.0.0.0,说明 DHCP 服务器没下发 DNS 选项,需要手动配置。

4.3 DNS 解析调试

DNS 调试相对简单,主要看回调有没有被调用,以及解析出的 IP 对不对。如果回调一直不触发,检查 DNS 服务器地址是否有效,可以用 PC 的nslookup指定同一个 DNS 服务器测试。

如果解析出的 IP 明显不对(比如是内网地址),可能是 DNS 缓存污染或者域名被劫持。换个域名测试,或者直接用 IP 地址跳过 DNS 步骤。

4.4 HTTP 请求发送与响应处理

HTTP 请求发送后,重点看响应。可以用串口把收到的数据打印出来,确认状态码是 200。如果收到 301/302,说明需要处理重定向;如果收到 404,说明路径不对;如果什么都没收到,检查 TCP 连接是否建立成功。

响应数据可能很大,要注意 pbuf 的释放。每次tcp_recved之后要确认数据已经处理完,避免内存泄漏。

static err_t tcp_recv_callback(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) { if (p == NULL) { /* 连接关闭 */ tcp_close(tpcb); return ERR_OK; } /* 处理数据 */ tcp_recved(tpcb, p->tot_len); pbuf_free(p); return ERR_OK; }

4.5 整体联调与稳定性测试

单独模块都通了之后,做整体联调。上电后自动完成 DHCP -> DNS -> HTTP 全流程,串口打印每一步的状态。然后做稳定性测试:反复上电、断网重连、长时间运行,观察是否有内存泄漏、连接失败等问题。

我实测下来,连续运行 24 小时,每 10 秒发一次 HTTP 请求,LWIP 的内存池没有明显泄漏。但如果请求频率太高,或者响应数据太大,还是需要加大内存配置。

5. 常见问题与排查技巧实录

5.1 网络不通的排查思路

网络不通是最常见的问题,排查要按层次来:

现象可能原因排查方法
串口无输出程序没跑起来检查复位、时钟、串口配置
link downPHY 未初始化/网线问题检查 PHY 地址、RMII 时钟、网线
DHCP 拿不到地址路由器 DHCP 未开/地址池满PC 验证路由器 DHCP
DNS 解析失败DNS 服务器地址无效PC nslookup 验证
TCP 连接失败目标 IP/端口不对PC telnet 验证
HTTP 无响应请求格式错误/服务器拒绝抓包分析

5.2 内存不足与 pbuf 分配失败

LWIP 的内存配置是新手最容易忽略的地方。默认的MEM_SIZE只有 1600 字节,跑 DHCP 勉强够,跑 HTTP 肯定不够。表现就是tcp_write返回ERR_MEM,或者pbuf_alloc返回 NULL。

解决办法是加大MEM_SIZE和PBUF_POOL_SIZE。但也不能无限加大,STM32F4 的 RAM 有限,要平衡。一般MEM_SIZE设 16KB,PBUF_POOL_SIZE设 32,大部分场景够用了。

注意:改完内存配置后,要重新编译整个工程,有时候 CubeMX 生成的代码会覆盖你的修改。

5.3 中断优先级引发的死锁

用了 FreeRTOS 之后,ETH 中断优先级如果设置不当,会在中断里调用osSignalSet之类的 API 时触发断言。STM32 的 NVIC 优先级数值越小优先级越高,而 FreeRTOS 的configMAX_SYSCALL_INTERRUPT_PRIORITY是一个阈值,高于这个阈值的中断不能调用 FreeRTOS API。

ETH 中断优先级要设置为低于configMAX_SYSCALL_INTERRUPT_PRIORITY的值(数值上更大)。比如configMAX_SYSCALL_INTERRUPT_PRIORITY是 5,ETH 中断优先级就设 6 或更低。

5.4 DNS 解析超时与重试

DNS 解析超时通常是因为 DNS 服务器不可达。LWIP 的 DNS 模块有重试机制,但重试次数有限。如果网络环境不稳定,可以适当增加DNS_MAX_RETRIES和DNS_TIMEOUT。

另外,DNS 解析是异步的,回调函数里不要做耗时操作,否则会阻塞协议栈的其他任务。把解析结果保存下来,在主循环或独立线程里处理。

5.5 HTTP 响应数据不完整

HTTP 响应数据不完整,通常是 pbuf 处理不当导致的。TCP 数据可能分多个 pbuf 到达,如果只处理了第一个 pbuf,后面的就丢了。正确的做法是遍历 pbuf 链,把所有数据都取出来。

struct pbuf *q; for (q = p; q != NULL; q = q->next) { /* 处理 q->payload, q->len */ }

还有一种情况是tcp_recved调用不及时,导致接收窗口满了,服务器不再发送数据。每次收到数据后要尽快调用tcp_recved。

6. 几个容易被忽略的细节

6.1 sys_check_timeouts 的调用频率

这个函数是 LWIP 的定时器心跳,DHCP、TCP 重传、ARP 超时都依赖它。调用频率太低,协议栈反应迟钝;调用频率太高,浪费 CPU。一般 1ms 到 10ms 调用一次比较合适。在裸机主循环里,可以配合HAL_GetTick()来做非阻塞调用。

6.2 netif 的状态管理

netif有up、down、link_up、link_down几个状态。DHCP 启动前要确保netif是up的,链路是link_up的。如果网线拔了再插,link_down和link_up会切换,DHCP 可能需要重新启动。可以在netif的状态回调里处理这些逻辑。

6.3 HTTP 请求的 Host 头

HTTP/1.1 要求请求里必须带Host头,否则服务器可能返回 400。这个头容易被忽略,尤其是直接用 IP 地址访问的时候。即使是用 IP 访问,Host头也要填对,否则虚拟主机环境下会路由到错误的站点。

6.4 连接关闭的方式

HTTP 请求完成后,关闭 TCP 连接有两种方式:tcp_close()和tcp_abort()。tcp_close()是正常关闭,会发送 FIN;tcp_abort()是强制关闭,发送 RST。一般用tcp_close(),但如果连接已经异常,用tcp_abort()更干脆。

关闭之后要记得把pcb置 NULL,避免野指针。

7. 从例程到产品的扩展思路

这个例程跑通之后,可以往几个方向扩展。一是加上 HTTPS,用 mbedTLS 做 TLS 握手,但 STM32F4 跑 TLS 比较吃力,需要外扩 RAM 或者用硬件加密模块。二是加上 MQTT,把 HTTP 换成 MQTT 协议,适合物联网数据上报场景。三是加上 OTA,通过 HTTP 下载固件,写到 Flash 里,实现远程升级。

每个方向都有各自的坑,但底层这套 DHCP + DNS + TCP 的框架是通用的。把这套框架吃透,后面换协议只是换应用层的事。

我在实际项目里,这套框架跑了三年多,稳定性没问题。关键是要把内存配置留足余量,把异常处理做完善,尤其是断网重连的场景。网络环境复杂的时候,DHCP 租约到期、DNS 服务器切换、TCP 连接被中间设备断开,这些都要考虑到。代码里多打日志,出问题的时候能快速定位。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询