4、TCP/IP协议栈适配:模组内置协议栈 vs 外部协议栈,Socket创建与数据收发实战
说到TCP/IP协议栈,很多刚入行的朋友会问:模组自己带了协议栈,为什么还要外挂一个?这个问题,我当年也纠结过。说白了,这就是一个「够用」和「灵活」之间的选择。
咱们先看一张图,把两种方案的结构理清楚。
4.1 两种协议栈方案的核心差异
我习惯把内置协议栈比作「傻瓜相机」,把外部协议栈比作「单反」。傻瓜相机按快门就行,单反能调光圈、快门、ISO,但需要你懂摄影。
| 对比维度 | 内置协议栈 | 外部协议栈 |
|---|---|---|
| MCU资源占用 | 极低,只需串口发AT | 较高,需运行完整IP栈 |
| 开发复杂度 | 低,AT指令搞定 | 高,需移植和调试协议栈 |
| 灵活性 | 受限,只能模组支持的协议 | 高,可自定义TCP参数、多路复用 |
| 调试难度 | 黑盒,出问题难定位 | 白盒,可抓包、可打日志 |
| 典型场景 | 简单传感器、低功耗设备 | 视频传输、多连接、定制协议 |
我的建议:如果你的MCU是Cortex-M0或者资源紧张,老老实实用内置协议栈。如果MCU是M4/M7或者跑Linux,外部协议栈更合适。我在一个智能电表项目里用过内置栈,AT指令一发就通,省心得很。但后来做车载视频监控,必须用外部栈,因为要同时维持4路TCP连接,内置栈根本扛不住。
4.2 内置协议栈实战:AT指令创建Socket
内置协议栈的操作,说白了就是几条AT指令的事。以移远EC200系列为例,流程如下:
- AT+QIOPEN — 创建Socket连接
- AT+QISEND — 发送数据
- AT+QIRD — 接收数据
- AT+QICLOSE — 关闭连接
嗯,这里要注意:不同模组厂商的AT指令集不一样。华为的用^IPOPEN,移远的用QIOPEN,SIMCom的用AT+CIPSTART。我刚开始做的时候,拿着移远的代码去调SIMCom模组,折腾了半天才发现指令不一样。
来个实际例子,创建一个TCP客户端连接:
// 1. 激活PDP上下文(如果还没激活)
AT+QIACT=1
// 返回:OK
// 2. 创建TCP Socket连接
AT+QIOPEN=1,0,"TCP","192.168.1.100",8080,0,1
// 返回:CONNECT OK
// 3. 发送数据
AT+QISEND=0,5
> HELLO
// 返回:SEND OK
// 4. 接收数据(主动查询)
AT+QIRD=0,1024
// 返回:+QIRD: 5
// WORLD
// 5. 关闭连接
AT+QICLOSE=0
// 返回:OK
避坑指南:我曾经在批量发货前发现,模组在弱信号下AT+QIOPEN返回超时。后来加了重试机制,每次重试间隔3秒,最多重试5次。另外,AT+QISEND的响应时间和网络质量直接相关,别设太短的超时。
4.3 外部协议栈实战:LWIP + PPPoS
外部协议栈,我推荐LWIP。轻量、稳定、文档多。配合模组的PPP over Serial(PPPoS)模式,MCU直接获得一个IP接口。
基本架构是这样的:
- 物理层:UART连接模组
- 数据链路层:PPP协议
- 网络层:LWIP的IP栈
- 传输层:TCP/UDP
LWIP的Socket API和标准BSD Socket几乎一样。看代码:
#include "lwip/sockets.h"
int sock_fd;
struct sockaddr_in server_addr;
// 1. 创建Socket
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd < 0) {
printf("Socket创建失败\n");
return -1;
}
// 2. 设置服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr);
// 3. 连接服务器
if (connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
printf("连接失败\n");
close(sock_fd);
return -1;
}
// 4. 发送数据
char *send_buf = "HELLO";
int ret = send(sock_fd, send_buf, strlen(send_buf), 0);
if (ret < 0) {
printf("发送失败\n");
}
// 5. 接收数据
char recv_buf[1024];
ret = recv(sock_fd, recv_buf, sizeof(recv_buf), 0);
if (ret > 0) {
printf("收到: %s\n", recv_buf);
}
// 6. 关闭连接
close(sock_fd);
注意:外部协议栈模式下,模组只做透传。PPP协商、IP分配、TCP重传全在MCU侧。如果你的MCU主频低于100MHz,或者RAM小于64KB,建议别碰外部栈。我有个同事在STM32F103上硬跑LWIP,结果TCP吞吐量只有20KB/s,连个图片都传不动。
4.4 数据收发实战对比
两种方案在数据收发上的表现差异很大。我整理了一张对比表:
| 操作 | 内置协议栈 | 外部协议栈 |
|---|---|---|
| 发送延迟 | AT指令响应时间 + 模组处理时间(约50-200ms) | 直接写入Socket缓冲区,延迟可控(约5-20ms) |
| 接收方式 | 轮询AT+QIRD 或 被动URC通知 | select/epoll回调,实时性好 |
| 最大并发连接 | 通常6-10路(视模组而定) | 取决于MCU资源,几十到几百路 |
| 数据分片处理 | 模组自动处理,用户无感 | 需手动处理TCP分段和粘包 |
| 心跳维持 | 模组内置KeepAlive,AT配置即可 | 需应用层实现心跳包 |
为什么会这样?内置协议栈把TCP/IP处理放在模组内部,MCU和模组之间走AT指令,一来一回就有延迟。外部协议栈直接在MCU内存里操作Socket缓冲区,延迟自然低。
我的经验:做低功耗设备,我选内置协议栈。模组在数据发送完后可以自动进入PSM省电模式,MCU也能睡大觉。但做实时性要求高的设备,比如工业控制器,我肯定用外部协议栈。有一次做AGV小车的远程控制,内置栈的AT响应延迟导致控制指令滞后,差点撞到人。换成外部栈后,延迟从150ms降到了15ms。
4.5 如何选择?一张决策流程图
你想想看,选哪种方案其实就三个问题:MCU强不强?实时性高不高?开发周期紧不紧?
说白了,没有绝对的好坏。内置协议栈省心,外部协议栈灵活。我个人的习惯是:原型阶段用内置栈快速验证,量产前再根据实际性能决定要不要切外部栈。但如果你做的是NB-IoT或者Cat.1的低功耗设备,内置栈几乎是唯一选择——因为外部栈的PPP保活功耗你扛不住。
最后一个小技巧:无论用哪种方案,一定要在弱信号环境下测试。我在一个项目中,内置栈在信号好的时候一切正常,到了地下室就频繁断连。后来发现是模组的TCP KeepAlive超时太短,调大后问题解决。这些坑,只有实际跑过才知道。