4、TCP/IP协议栈适配:模组内置协议栈 vs 外部协议栈,Socket创建与数据收发实战

说到TCP/IP协议栈,很多刚入行的朋友会问:模组自己带了协议栈,为什么还要外挂一个?这个问题,我当年也纠结过。说白了,这就是一个「够用」和「灵活」之间的选择。

咱们先看一张图,把两种方案的结构理清楚。

内置协议栈 vs 外部协议栈架构对比 内置协议栈 vs 外部协议栈架构对比 内置协议栈方案 MCU(AT指令控制) 蜂窝模组(内置TCP/IP栈) LTE网络 MCU只发AT,不参与TCP/IP处理 外部协议栈方案 MCU(运行LWIP/uC-TCP/IP) 蜂窝模组(仅PPP/NDIS透传) LTE网络 MCU负责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系列为例,流程如下:

  1. AT+QIOPEN — 创建Socket连接
  2. AT+QISEND — 发送数据
  3. AT+QIRD — 接收数据
  4. 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强不强?实时性高不高?开发周期紧不紧?

协议栈选择决策流程 协议栈选择决策流程 开始评估 MCU资源是否充足? 否 是 内置协议栈 外部协议栈 决策要点: ① MCU主频<100MHz或RAM<64KB → 内置栈 ② 需要多连接(>10路)或低延迟(<30ms) → 外部栈

说白了,没有绝对的好坏。内置协议栈省心,外部协议栈灵活。我个人的习惯是:原型阶段用内置栈快速验证,量产前再根据实际性能决定要不要切外部栈。但如果你做的是NB-IoT或者Cat.1的低功耗设备,内置栈几乎是唯一选择——因为外部栈的PPP保活功耗你扛不住。

最后一个小技巧:无论用哪种方案,一定要在弱信号环境下测试。我在一个项目中,内置栈在信号好的时候一切正常,到了地下室就频繁断连。后来发现是模组的TCP KeepAlive超时太短,调大后问题解决。这些坑,只有实际跑过才知道。