MCU 跑以太网上云,最忌自己从以太网帧往上造轮子。本文基于《TCP/IP技术大全》等资料,讲清 TCP/IP 分层模型与 lwIP 模块映射、ARP 解析与 IP 分片、UDP 与 TCP 选型、三次握手、滑动窗口与慢启动、pbuf 三种内存策略,附 STM32+DM9161 初始化与 MQTT 上云 socket 代码,物联网网关以太网七步落地。
给物联网网加一路 100M 以太网上云,硬件上 DM9161 一类 PHY 芯片只解决"电信号↔MAC 帧"这一层,从 MAC 帧到 MQTT 报文之间还隔着整条协议栈——这条栈在 PC 上是操作系统自带的,在 MCU 上必须自己解决。结论先给三条:第一,嵌入式以太网协议栈不要自研,lwIP 是行业事实标准,几十 KB 级 Flash、10KB 级 RAM 起步可裁剪,支持无 OS 与带 OS 两种运行模式;第二,协议栈的坑八成在内存管理和 TCP 参数上——pbuf 分配策略选错会碎片化,MSS/滑动窗口/拥塞窗口不调,吞吐直接腰斩;第三,UDP 与 TCP 按业务选,Modbus TCP、MQTT 走 TCP,SNTP、高频遥测心跳走 UDP,别一刀切。下面按分层模型、ARP/IP、传输层选型、TCP 可靠性机制、内存策略、代码与落地七步讲透。
先看问题本身。以太网物理层给你的是一帧一帧的 MAC 帧:目的 MAC、源 MAC、类型、载荷、CRC。应用层要的却是一个能 connect() 到 MQTT 服务器 1883 端口的"管道"。这两者之间隔着寻址、路由、可靠传输、拥塞控制、字节流切分等一整套机制——《TCP/IP技术大全》把这套机制归结为开放式通信模型的分层处理思想:每层只对上层提供服务、只依赖下层的服务,层间接口固定,任一层的技术演进不影响邻层(第 1 章"开放式通信模型简介")。OSI 参考模型分了七层,TCP/IP 实用模型收敛为四层,嵌入式协议栈就是把这个四层模型装进几百 KB 以内的 MCU 里。
lwIP(Lightweight IP)是瑞典计算机科学研究所(SICS)开源的轻量级 TCP/IP 协议栈,BSD 风格许可,是 STM32、NXP、乐鑫等主控以太网方案的默认选择。它的两个关键开关:NO_SYS 为 1 时以无 OS 轮询方式运行,适合裸机;为 0 时以 TCP/IP 线程方式运行,socket/信号量/邮箱全套可用,适合带 RTOS 的网关。
读懂协议栈先定架构。把 OSI 七层、TCP/IP 四层与 lwIP 实际源码模块对上号,每个问题就知道了该去改哪个文件:
| OSI 七层 | TCP/IP 四层 | lwIP 模块 / 文件 | 网关中解决的问题 |
|---|---|---|---|
| 应用层 / 表示层 / 会话层 | 应用层 | apps/mqtt、apps/httpd、netdb | MQTT 收发、远程配置网页 |
| 传输层 | 传输层 | tcp.c / udp.c / raw.c | TCP 连接维护、UDP 遥测 |
| 网络层 | 网际层 | ip4.c、icmp.c、etharp.c | IP 寻址、ping、ARP 解析 |
| 数据链路层 / 物理层 | 网络接口层 | netif.c + 网卡驱动(STM32 HAL ETH) | MAC 帧收发、DMA 描述符 |
| (物理层独立于协议栈) | DM9161 等 PHY 芯片 | 10/100M 自适应、MDI/MDIX | |
分层铁律是:跨层直接调用的代码今天省十行,明天协议一变就全崩。应用只准走 socket API(或 lwIP 的 netconn/raw 回调 API),MAC 地址、IP 分片这些事交给下面几层。
应用调 connect("192.168.1.100", 1883) 后,协议栈第一件事不是发 TCP SYN,而是查 ARP 缓存(RFC 826):目标 IP 有没有对应 MAC?没有就先广播一条 ARP 请求"谁是 192.168.1.100",拿到应答后缓存表项(典型老化时间几分钟到 20 分钟),再发业务报文。IP 层(RFC 791)随后做三件事:填 20 字节基本报头(无选项时)、按目的地址决定直接交付还是走默认网关、载荷超过 MTU(以太网 1500 字节)时分片——嵌入式端最好通过 MSS 协商让 TCP 报文天然小于 MTU,避免 IP 分片,一片丢全组重传。
工程要点:ICMP(RFC 792)的 ping 一定要开——现场调试 80% 的"连不上云"先用 ping 判定是二层通还是三层通;网关产品建议同时支持 DHCP 与静态 IP 双模式,掉线重连时先免费 ARP 宣告地址,避免与现场设备 IP 冲突后静默丢包。
传输层两条路差别极大,选错方向整个网关的时延与可靠性模型都会错位。核心对比:
| 对比项 | UDP | TCP |
|---|---|---|
| 报头开销 | 8 字节 | 20 字节(无选项) |
| 连接与状态 | 无连接,无状态 | 三次握手建链,11 种状态机 |
| 可靠性 | 不保证送达、不保证顺序 | 确认重传、按序交付、校验和 |
| 拥塞控制 | 无 | 慢启动 + 拥塞避免 + 快重传 |
| 典型时延 | 一跳即发,毫秒级 | 建链 1.5×RTT 后才可发数据 |
| 典型应用端口 | SNTP :123、高频遥测心跳 | Modbus TCP :502、MQTT :1883/:8883、HTTP :80 |
网关选型经验:指令与账目走 TCP,毛刺与心跳走 UDP。Modbus TCP 一条写寄存器指令不能丢也不能重,必须 TCP;MQTT 协议本身指定承载在 TCP 上,QoS 机制建立在可靠字节流之上;而每秒几十点的高频采样如果走 TCP,一次丢包重传会把整条流卡住,拆成带序号的 UDP 小报文、应用层容丢,反而更平滑。
TCP(RFC 793)的可靠不是玄学,是三个机制的组合:
rwnd,发送方未确认字节不得超过窗口——这是"对端能收多少"的流量控制。发送侧还有拥塞窗口 cwnd:慢启动阶段从 1×MSS 起每 RTT 翻倍,达到阈值 ssthresh 后进入拥塞避免,每 RTT 线性加 1×MSS;丢包后减窗重传。两个窗口取小值决定实际发送速度。对以太网 MTU 1500 字节、IP 头 20、TCP 头 20,MSS 满配就是 1460 字节。lwIP 里 tcp_mss、tcp_wnd、tcp_sndbuf 三个参数直接决定吞吐:经验值 tcp_wnd ≥ 4×MSS、tcp_sndbuf ≥ 2×MSS,RAM 富余时窗口翻倍往往比换主频更有效。10M/100M 带宽下,百字节级小包每包都带完整 20+20 字节头部,应用有效载荷率不足 70%——批量遥测在应用层攒包再发,性价比远高于堆硬件。
lwIP 长期跑不跑得稳,八成看内存。它用 pbuf(packet buffer)链描述报文,三种来源策略各有适用:
| 策略 | 机制 | 优点 | 风险 / 适用 |
|---|---|---|---|
| 堆分配(MEM_SIZE) | 类似 malloc 的连续堆 | 灵活,适合变长报文 | 长期运行碎片化,需配 MEM_LIBC_MALLOC 或定期统计 |
| 固定池(MEMP_NUM_xxx) | 定长 pbuf 池,编译期定个数 | 分配 O(1),无碎片,实时确定性好 | 池满即丢包,MEMP_NUM_TCP_PCB/PBUF_POOL_SIZE 要按并发连接数配 |
| 零拷贝(PBUF_ROM/PBUF_REF) | 数据不搬移,pbuf 仅引用应用缓冲 | 大报文省一次拷贝与 RAM | 引用的数据生命周期必须覆盖发送完成回调,易出悬空引用 |
网关型产品的推荐组合:接收走 PBUF_POOL(DMA 描述符直接指向池块),发送走 PBUF_RAM,超大配置文件等静态数据走 PBUF_ROM。同时打开 LWIP_STATS:pbuf 分配失败次数、链路丢帧数、重传段数这三项指标进运行日志,是判断"内存吃紧还是网络拥塞"的第一证据。
以下为 STM32 + DM9161(MII 接口,详见本站 DM9161 PHY 接口文档)上 lwIP 无 OS 模式的初始化骨架:
/* lwIP 无OS模式初始化(STM32 以太网 + DM9161 PHY,MII 接口) */
#include "lwip/init.h"
#include "lwip/netif.h"
#include "netif/etharp.h"
static struct netif gnetif;
void lwip_stack_init(void)
{
ip4_addr_t ip, netmask, gw;
lwip_init(); /* 核心初始化,NO_SYS=1 */
IP4_ADDR(&ip, 192, 168, 1, 50); /* 现场建议 DHCP 与静态双模式 */
IP4_ADDR(&netmask, 255, 255, 255, 0);
IP4_ADDR(&gw, 192, 168, 1, 1);
netif_add(&gnetif, &ip, &netmask, &gw,
NULL, /* state 传给网卡驱动 */
ethernetif_init, /* 底层:ETH MAC DMA + DM9161 读写 */
ethernet_input); /* 链路层入口,自动分流 ARP/IP */
netif_set_default(&gnetif);
netif_set_up(&gnetif);
/* 主循环内周期调用(1~10ms):
ethernetif_input(&gnetif); 收包搬运
sys_check_timeouts(); ARP老化/重传/TCP定时 */
}
应用层用 socket 接口把 Modbus 采集的遥测按 MQTT 发上云(1883 端口、TCP 承载):
/* TCP socket:MQTT 风格遥测上云(简化示例) */
int telemetry_publish(void)
{
int sock = socket(AF_INET, SOCK_STREAM, 0); /* MQTT 固定走 TCP */
if (sock < 0) return -1;
struct sockaddr_in srv = {0};
srv.sin_family = AF_INET;
srv.sin_port = PP_HTONS(1883); /* MQTT 默认端口 */
srv.sin_addr.s_addr = inet_addr("192.168.1.100");
if (connect(sock, (struct sockaddr *)&srv, sizeof(srv)) < 0) {
close(sock); /* 内核自动走完三次握手 */
return -2;
}
/* 此处发送 MQTT CONNECT/PUBLISH 报文;keepalive 期间发 PINGREQ */
/* 发送前可用 getsockopt(SO_SNDBUF) 检查 tcp_sndbuf 剩余空间 */
send(sock, mqtt_publish_frame, mqtt_frame_len, 0);
return 0;
}
把上面的机制收拢成七步落地清单:
| 步骤 | 动作 | 关键参数 / 判据 |
|---|---|---|
| 1 定模式 | 裸机选 NO_SYS=1,RTOS 选线程模式 | 有 RTOS 优先线程模式,socket 全套可用 |
| 2 接 PHY | MII/RMII 接 DM9161,自协商 10/100M | 链路状态引脚或 BMSR 寄存器轮询 |
| 3 配内存 | 接收走 PBUF_POOL,发送走 PBUF_RAM | MEMP 统计零失败为达标 |
| 4 调 TCP | tcp_mss=1460,tcp_wnd≥4×MSS | 吞吐不足先加大窗口再提主频 |
| 5 选传输 | Modbus/MQTT 走 TCP,心跳遥测可走 UDP | 指令不丢,毛刺可容 |
| 6 上应用 | MQTT keepalive 60~120s,断线指数退避重连 | PINGREQ 超时即重连,勿等 TCP 判死 |
| 7 留证据 | 开 LWIP_STATS,丢包/重传/池失败进日志 | 现场问题先看统计再看抓包 |
MCU 以太网上云的捷径是"分层模型照抄、协议栈用 lwIP、力气花在内存与 TCP 参数":OSI 七层对应 TCP/IP 四层,lwIP 用 etharp/ip4/udp/tcp/apps 四组模块逐层实现;UDP 8 字节头无连接、TCP 20 字节头可靠有序,Modbus TCP 与 MQTT 走 TCP、高频心跳走 UDP;MSS 定 1460 字节、tcp_wnd 不小于 4 倍 MSS、pbuf 接收走固定池发送走堆并开启统计,网关就能在 10KB 级 RAM 上跑出稳定吞吐。按"定模式、接 PHY、配内存、调 TCP、选传输、上应用、留证据"七步执行,以太网链路问题都能定位到层。
本文基于《TCP/IP技术大全》(TCP/IP 分层模型、ARP/ICMP/TCP 协议机制等章节)等技术资料整理优化,并结合物联网网关以太网上云的工程实践进行了系统化梳理。
沧州艾诺威电子 — 国家高新技术企业,20+项国家专利
嵌入式系统开发 · 物联网方案 · AI智能硬件 · 一站式交付
电话:13930711029 | 邮箱:tech@czinv.com | 24小时内响应