物联网短信通信实战:AT指令与PDU编码全解析
1. 项目概述与核心价值
最近在调试一个物联网项目,用到了GPRS模块进行数据上报。除了常规的TCP/IP数据透传,我发现短消息(SMS)功能在特定场景下简直是“救命稻草”——网络信号不稳时它能保底通信,设备异常时它能主动告警,而且几乎所有蜂窝模块都支持,通用性极强。不过,真上手调试才发现,AT指令手册看得人头大,PDU编码更是像天书。踩过几个坑后,我把整个调试过程,从模式选择、编码原理到实操避坑,重新梳理成这篇笔记。无论你是用STM32、ESP32还是任何MCU驱动GPRS/4G Cat.1模块,只要涉及短信收发,这里面的思路和代码片段都能直接拿来用,帮你省下大量查资料和试错的时间。
2. 短消息通信的核心原理与模式选择
搞懂短信收发,第一步不是敲指令,而是弄明白模块和网络之间到底在“说”什么。本质上,你的MCU通过串口发送AT指令给模块,模块则扮演了“手机”的角色,与运营商的短信中心(SMSC)进行通信。这里有两个核心概念决定了你的实现路径: TEXT模式 和 PDU模式 。
2.1 TEXT模式:简单直接,但有限制
TEXT模式,顾名思义,就是文本模式。你直接发送可读的英文、数字和部分符号给模块,模块帮你处理后续的编码和提交。它的优点非常突出: 简单 。设置好模式、服务中心号和目标号码,几乎就可以像在串口终端里聊天一样发短信了。
但是,它的缺点也同样明显: 对中文支持不友好 。虽然通过一些特殊的字符集设置(如UCS2)可以实现中文收发,但这个过程本质上模块内部还是在做PDU编码的转换,不同模块厂商的实现可能还有差异,容易导致乱码。因此,TEXT模式最适合的场景是发送纯英文或数字的告警信息、验证码等。如果你的项目只涉及英文,果断选TEXT模式,开发效率最高。
2.2 PDU模式:功能全面,支持中文
PDU(Protocol Data Unit)模式,是GSM短信的底层协议数据单元模式。在这种模式下,你需要自己(或由程序)构造出一串十六进制的数据流,这串数据里包含了短信中心号、目标号码、有效期、编码格式和短信内容本身等所有信息。模块收到这串PDU数据后,几乎原封不动地转发给网络。
PDU模式的优点在于 功能完整且标准化 。它原生支持包括中文在内的多种语言(通过UCS2、8-bit等编码),可以设置短信有效期、送达报告等高级功能。由于它是标准协议,不同模块、不同运营商之间的兼容性最好。缺点是 实现复杂 ,你需要理解PDU的格式,并编写代码进行号码编码、文本编码等操作。对于需要发送中文或需要可靠通信的项目,PDU模式是更专业的选择。
核心选择建议 :如果你的应用场景是 工业物联网的英文状态告警 ,追求快速实现,选TEXT模式。如果你的应用是 智能家居的中文通知 或需要 高可靠性的通信 ,不怕前期编码工作,选PDU模式。我个人的经验是,在资源允许的情况下,优先实现PDU模式,一劳永逸。
3. AT指令集深度解析与使用策略
无论选择哪种模式,都绕不开AT指令。手册上的指令列表往往很全,但实际常用的就那几条。理解每条指令背后的含义和参数,是稳定通信的基础。
3.1 基础设置指令链
发送短信前,必须完成一系列模块初始化设置,这就像给模块上电后进行的“开机设置”。一个稳健的设置流程如下:
- AT - 测试指令,用于检查模块与MCU的串口通信是否正常。返回
OK是后续一切操作的前提。 - AT+CPIN? - 查询SIM卡状态。确保返回
+CPIN: READY,否则需要输入PIN码(AT+CPIN="1234")。 - AT+CSQ - 查询信号强度。这个指令的返回格式通常是
+CSQ: <rssi>,<ber>。<rssi>是信号强度,数值范围0-31,越大越好(99表示未知或不可用)。例如,+CSQ: 24,0表示信号强度良好。在发送重要短信前检查一下信号,能避免因信号弱导致的发送失败。 - AT+CREG? - 查询网络注册状态。返回
+CREG: 0,1或+CREG: 0,5表示已注册到本地网络或已注册漫游网络。0,1或0,5是成功入网的标志。
3.2 短信专用指令详解
完成基础设置后,才进入短信相关的配置。
-
AT+CMGF - 模式选择开关 。这是最重要的指令之一。
AT+CMGF=1:设置为TEXT模式。AT+CMGF=0:设置为PDU模式。- 注意 :这条指令的设置会影响后续几乎所有短信指令的行为,务必在开始收发操作前明确设置好。
-
AT+CSCA - 设置短信中心号码 。这是短信能发出去的“总机号码”。格式为
AT+CSCA="+8613800XXX500",145。后面的145表示号码格式(145代表国际格式+86)。这个号码需要根据你的SIM卡运营商和地区填写,通常可以在手机短信设置里查到,或咨询运营商。 -
AT+CSCS - 设置字符集 。告诉模块你后续输入的文本是什么编码。
- 在TEXT模式下:
AT+CSCS="GSM"用于英文;AT+CSCS="UCS2"用于Unicode字符(如中文)。 - 在PDU模式下:此指令影响的是
AT+CMGS指令中输入的PDU数据如何被解释,通常也需要设置为"UCS2"以配合中文编码。
- 在TEXT模式下:
-
AT+CSMP - 设置TEXT模式参数 。仅在
CMGF=1时有效。用于设置FO、VP、PID、DCS等参数。例如AT+CSMP=17,167,0,240。对于英文短信,240表示GSM 7-bit默认字母表;对于中文,需要设置为24(UCS2编码,Class 0)。 -
AT+CNMI - 新消息指示配置 。这条指令决定了当模块收到新短信时,如何通知你的MCU。这是实现“事件驱动”式接收的关键,比轮询效率高得多。
- 常用设置:
AT+CNMI=2,1,0,0,0 - 参数分解:
2:新短信到来时,直接在串口输出+CMTI通知(如+CMTI: "SM",1),即使串口正忙也会缓存后尽快发出。1:将新短信存储在默认存储位置(通常是SIM卡),并附带索引号。
- 实操心得 :务必在程序初始化时就配置好此指令。这样,当短信到来,模块会自动发送
+CMTI: "SM",X,你的MCU只需要解析这个X(存储索引),然后用AT+CMGR=X去读取即可,实现了即时响应。
- 常用设置:
3.3 短信操作指令
- AT+CMGS - 发送短信 。在TEXT模式下,直接跟目标号码和内容;在PDU模式下,跟PDU数据长度和PDU数据本身。
- AT+CMGR - 读取短信 。参数是短信在存储区中的索引号。
- AT+CMGD - 删除短信 。参数是短信索引号。读取处理完后及时删除,可以防止SIM卡存储满导致无法接收新短信。
- AT+CMGL - 列出短信 。可以列出所有、已读、未读等状态的短信。在轮询方案中可能用到。
指令执行超时与错误处理 :在编写MCU驱动代码时,绝不能假设每条指令都立刻成功。必须为每一条AT指令设置合理的超时时间(例如3-5秒)。如果超时未收到
OK或预期响应,应进行重试(通常最多3次)或记录错误状态。对于AT+CMGS这种交互式指令(先发指令,收到>后再发内容,最后发Ctrl+Z),超时和状态机的设计尤为关键。
4. PDU模式编码全流程实战与代码实现
PDU模式是难点,也是重点。我们以“发送中文短信‘测试’到手机号码 13912345678 ”为例,拆解整个编码和发送过程。
4.1 第一步:确定短信中心地址(SCA)
首先需要获取本地正确的短信中心号码。假设我们得到的是 +8613800100500 (北京移动)。
- 去掉
+号:8613800100500。 - 检查是否为偶数位,如果是奇数,在末尾补
F。本例是13位(奇数),补F:8613800100500F。 - 将每两位数字交换位置(奇偶互换):
86->6813->3180->0801->1000->0050->050F->F0(注意,补的F在这里参与交换)
- 最终得到的SCA编码为:
683108100005F0。其长度(字节数)为0D(13个数字/2 ≈ 6.5字节,按7字节算?这里容易错)。准确计算:编码后的字符串683108100005F0长度为14个字符,即7个字节。所以SCA部分为:0891683108100005F0。 开头的00是啥? 在PDU串开头,通常需要一个字节表示SCA的长度(以半字节计)。0D表示后面有13个半字节(即6.5字节,但实际存储时,683108100005F0是7字节)。更通用的做法是:00表示使用模块内存中AT+CSCA设置的号码,这样更简单,我们采用这种方式。所以SCA字段简化为:00。
4.2 第二步:构建TPDU(传输协议数据单元)
这是PDU的核心部分。
-
第一个字节(TP-MTI, RP, UDHI, SRR等) :通常设置为
11。表示:- TP-MTI=01 (短信发送)
- TP-RD=0 (替换重复消息)
- TP-VPF=10 (有效期格式为相对时间)
- TP-SRR=1 (请求状态报告)
- TP-UDHI=0 (用户数据无头)
- TP-RP=0 (不回复路径)
-
消息参考(TP-MR) :
00,由模块自动填充。 -
目标地址长度(TP-DA Length) :目标号码
13912345678,共11位。11位数字转换为半字节(nibble)的数量,即0B。 -
目标地址类型(TP-DA Type) :
91,表示国际号码。 -
目标地址(TP-DA) :对号码
13912345678进行编码。- 去掉
+:13912345678 - 奇数位补
F:13912345678F - 每两位交换:
3119325476F8 - 编码结果:
3119325476F8
- 去掉
-
协议标识(TP-PID) :
00,普通GSM消息。 -
数据编码方案(TP-DCS) :
08。表示:- Bit 7-4:
00(无消息类别) - Bit 3-2:
10(UCS2编码) - Bit 1-0:
00(保留) 08表示UCS2编码,无有效期。如果要设置有效期和发送等级,可以设为18(Class 0,直接显示)等。
- Bit 7-4:
-
有效期(TP-VP) :由于DCS中未指定有效期格式,这里留空或根据设置。如果我们用
08,则VP不出现。如果用11作为第一字节,并指定了相对时间格式的VP,则需要一个VP字节。 -
用户数据长度(TP-UDL) :用户数据(短信内容)的字节数。中文“测试”的Unicode编码是
6D4B 8BD5,共4个字节。所以UDL=04。 -
用户数据(TP-UD) :短信内容“测试”的UCS2编码。
- “测”的Unicode是
0x6D4B。 - “试”的Unicode是
0x8BD5。 - 编码后为:
6D4B8BD5。
- “测”的Unicode是
4.3 第三步:组装完整PDU串并计算长度
现在我们把所有部分按顺序组装起来,并计算 AT+CMGS 命令所需的长度。
PDU数据部分(从TP-MTI到TP-UD) : 11 00 0B 91 3119325476F8 00 08 04 6D4B8BD5
将其连接起来: 11000B913119325476F80008046D4B8BD5 这个字符串的长度是28个字符,代表14个字节。
AT+CMGS 的长度参数 :这个参数是 整个短信的TPDU部分(即上面这14个字节)的十进制长度 。所以是 14 。
4.4 第四步:发送AT指令序列
通过串口向模块发送以下指令( \r 代表回车, 0x1A 代表Ctrl+Z):
AT+CMGF=0\r\n
OK
AT+CSCS="UCS2"\r\n
OK
AT+CMGS=14\r\n
>
11000B913119325476F80008046D4B8BD5\x1A
模块收到 Ctrl+Z 后,会尝试发送短信,并最终返回 +CMGS: <mr> 和 OK ,表示发送成功。
4.5 代码实现片段(C语言示例)
// 假设已经实现了一个发送AT指令并等待响应的函数:at_send_cmd
// 和一个发送原始数据的函数:uart_send_bytes
int send_sms_pdu(const char *sca, const char *phone, const uint16_t *unicode_msg, uint8_t msg_len) {
// 1. 设置PDU模式
if (!at_send_cmd("AT+CMGF=0", "OK", 3000)) return -1;
// 2. 设置UCS2字符集(可选,但建议设置)
if (!at_send_cmd("AT+CSCS=\"UCS2\"", "OK", 3000)) return -1;
// 3. 编码目标号码 (简化版,假设号码是11位)
char encoded_phone[13] = {0}; // 11位补F后交换,12字符+结束符
// ... 这里省略具体的号码编码函数 encode_phone_number(phone, encoded_phone) ...
// 4. 构造PDU数据 (简化固定格式)
// PDU: 11 00 0B 91 [encoded_phone] 00 08 [UDL] [Unicode Data]
uint8_t tpdu[128];
uint8_t tpdu_len = 0;
tpdu[tpdu_len++] = 0x11; // TP-MTI etc.
tpdu[tpdu_len++] = 0x00; // TP-MR
tpdu[tpdu_len++] = 0x0B; // DA length (11 digits)
tpdu[tpdu_len++] = 0x91; // Type: international
// 将encoded_phone字符串(如"3119325476F8")转换为字节流放入tpdu
// ... hex_str_to_bytes(encoded_phone, &tpdu[tpdu_len], 12) ...
tpdu_len += 6; // 12个字符是6个字节
tpdu[tpdu_len++] = 0x00; // PID
tpdu[tpdu_len++] = 0x08; // DCS: UCS2
tpdu[tpdu_len++] = msg_len * 2; // UDL: Unicode字符数 * 2
// 将unicode_msg数组拷贝到tpdu
for (int i = 0; i < msg_len; i++) {
tpdu[tpdu_len++] = (unicode_msg[i] >> 8) & 0xFF; // 高字节
tpdu[tpdu_len++] = unicode_msg[i] & 0xFF; // 低字节
}
// 5. 发送CMGS指令,参数是tpdu_len (字节数)
char cmd[32];
sprintf(cmd, "AT+CMGS=%d", tpdu_len);
if (!at_send_cmd(cmd, ">", 5000)) return -2; // 等待">"提示符
// 6. 发送PDU数据
char pdu_str[256] = {0};
bytes_to_hex_str(tpdu, tpdu_len, pdu_str); // 将字节数组转换为十六进制字符串
uart_send_bytes(pdu_str, strlen(pdu_str));
uart_send_bytes("\x1A", 1); // 发送Ctrl+Z结束
// 7. 等待最终响应
if (!at_wait_response("+CMGS:", 30000)) return -3; // 等待发送成功响应
if (!at_wait_response("OK", 3000)) return -4;
return 0; // 成功
}
5. TEXT模式发送的简化流程与陷阱
相比PDU,TEXT模式确实简单不少,但坑一点没少。
5.1 发送英文短信
发送英文“Hello”到 13912345678 :
AT+CMGF=1\r\n
OK
AT+CSCS="GSM"\r\n
OK
AT+CSMP=17,167,0,240\r\n // 注意这个参数,240对应GSM 7-bit默认字母表
OK
AT+CMGS="13912345678"\r\n
>
Hello\x1A
关键点 : AT+CSMP 的最后一个参数 DCS 。对于英文, 240 是常见的正确值。如果设置为其他值,可能导致短信中心无法正确解码。
5.2 发送中文短信(TEXT模式下的UCS2)
在TEXT模式下发送中文,模块内部其实帮你做了一次编码转换。你需要将手机号码和短信内容都转换为UCS2编码的十六进制字符串。 发送“测试”到 13912345678 :
- 编码手机号 :将每个数字转换为Unicode。
1->0031,3->0033, ...8->0038。连起来:00310033003900310032003300340035003600370038。 - 编码短信内容 :“测试”->
6D4B8BD5。 - 发送指令 :
AT+CMGF=1\r\n
OK
AT+CSCS="UCS2"\r\n
OK
AT+CSMP=17,167,0,24\r\n // DCS=24,表示UCS2编码,Class 0
OK
AT+CMGS="00310033003900310032003300340035003600370038"\r\n // 注意是编码后的号码字符串
>
6D4B8BD5\x1A
TEXT模式的大坑 : 字符集一致性 。
AT+CSCS设置的是什么字符集,你后面输入的所有文本(包括号码和内容)都必须用该字符集来理解。如果你设置了UCS2,却发送了普通的ASCII数字作为号码,模块会将其当作UCS2编码处理,导致号码错误,短信发送失败。这是TEXT模式下中文发送最容易出错的地方。
6. 短信接收的两种模式与实战编程
短信接收有两种思路: 轮询 和 事件驱动 。轮询简单但低效,事件驱动是生产环境的首选。
6.1 轮询方式(不推荐用于实时应用)
MCU定期(比如每10秒)发送 AT+CMGL="REC UNREAD" 指令,列出所有未读短信。如果返回中有短信,则逐条读取( AT+CMGR )和处理,最后删除( AT+CMGD )。 缺点 :大部分时间都在做无用功,浪费功耗和处理器资源,响应也不及时。
6.2 事件驱动方式(推荐)
这是通过 AT+CNMI 指令实现的。我们将其设置为 AT+CNMI=2,1,0,0,0 。
2:新短信来时,立即在串口输出+CMTI通知。1:将短信存储在SIM卡(存储位置"SM")并返回存储索引。
工作流程 :
- 模块初始化时,设置
AT+CNMI=2,1,0,0,0。 - MCU的串口接收程序一直运行。当收到一条完整的以
\r\n结尾的串口数据行时,进行解析。 - 如果解析到
+CMTI: "SM",5,则表示新短信存储在SIM卡索引5的位置。 - MCU立即发送
AT+CMGR=5读取该条短信。 - 解析
AT+CMGR的返回数据,提取发送方号码和短信内容。 - 处理完毕后,发送
AT+CMGD=5删除该短信,释放存储空间。
代码逻辑片段 :
void uart_rx_callback(char *line) { // 假设每行数据通过回调传入
if (strstr(line, "+CMTI: \"SM\",")) {
int index;
sscanf(line, "+CMTI: \"SM\",%d", &index);
// 触发一个任务或设置标志,去读取index对应的短信
sms_read_index = index;
sms_event_flag = 1;
}
// 其他AT响应处理...
}
void sms_task(void) {
if (sms_event_flag) {
char cmd[32];
sprintf(cmd, "AT+CMGR=%d", sms_read_index);
send_at_cmd_and_wait_response(cmd, response_buffer, sizeof(response_buffer));
// 解析response_buffer,例如:
// +CMGR: "REC UNREAD","+8613912345678",,"24/05/17,15:30:00+32",145,4,0,0,"+8613800100500",145,8
// Hello World
// 提取号码和内容...
// ...
// 删除短信
sprintf(cmd, "AT+CMGD=%d", sms_read_index);
send_at_cmd(cmd);
sms_event_flag = 0;
}
}
接收模式下的存储管理 :务必注意SIM卡的短信存储容量是有限的(通常25-50条)。如果只收不删,存满后将无法接收新短信,造成信息丢失。一个健壮的程序应该在成功读取并处理短信后,立即将其删除。对于需要保存的短信,可以考虑提取内容后存储到MCU的Flash或外置存储器中。
7. 调试技巧、常见问题与故障排查实录
调试GPRS短信功能,串口调试助手是你的眼睛。但光看还不够,还得会分析。
7.1 必备调试工具与方法
- 硬件连接 :确保TX、RX、GND连接正确,电源稳定(模块峰值电流可能达2A)。最好在电源引脚就近并联一个大电容(如100uF)缓冲电流冲击。
- 串口调试助手 :使用支持十六进制显示和发送的调试工具(如SecureCRT、Putty或开源的QCOM)。将发送和接收的数据都勾选“十六进制显示”,这样你能看到每一个字节,包括
\r、\n和0x1A。 - 指令记录 :养成好习惯,将每次成功和失败的完整指令交互日志保存下来。这是排查问题的黄金资料。
7.2 常见问题速查表
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
发送短信返回 ERROR |
1. 短信中心号码错误。 2. PDU模式长度计算错误。 3. 未收到 > 就发送内容。 4. 字符集设置与内容不匹配。 |
1. 用 AT+CSCA? 查询当前设置,与手机核对。 2. 核对PDU串长度,用工具验证编码。 3. 检查代码逻辑,确保收到 > 提示符后再发数据。 4. 确认 AT+CSCS 和 AT+CSMP (或PDU DCS)设置正确。 |
| 发送英文正常,发送中文乱码 | 1. (TEXT模式) AT+CSCS 未设置为 "UCS2" 。 2. (TEXT模式) AT+CSMP 的DCS参数错误。 3. (PDU模式) DCS字段不是 08 或 18 等UCS2标识。 4. 中文文本转Unicode编码错误。 |
1. 确认模式切换和字符集指令已执行且返回OK。 2. TEXT模式下,中文DCS应为 24 或 25 (十进制)。 3. PDU模式下,检查DCS字节是否为 08 。 4. 使用可靠的编码转换函数或在线工具核对Unicode码点。 |
收不到 +CMTI 新短信通知 |
1. AT+CNMI 指令未设置或设置错误。 2. SIM卡短信存储已满。 3. 模块不支持该设置。 |
1. 确认初始化时已发送 AT+CNMI=2,1,0,0,0 并返回OK。 2. 用 AT+CPMS? 查看存储使用情况,用 AT+CMGD 删除旧短信。 3. 查阅模块具体手册,确认 CNMI 参数支持情况。 |
能收到通知,但读取( CMGR )失败 |
1. 读取的索引号错误。 2. 短信已被其他进程删除。 3. 存储位置不匹配(如通知存在 "SM" ,却去 "ME" 读)。 |
1. 仔细解析 +CMTI 响应,提取正确的索引号。 2. 确保没有其他地方(如手动AT指令)删除了短信。 3. 检查 AT+CPMS? 返回的当前存储设置。 |
| 短信发送成功率低 | 1. 信号强度弱( AT+CSQ )。 2. 网络未注册( AT+CREG? )。 3. 短信中心拥堵或模块问题。 |
1. 确保 CSQ 的RSSI值大于10(例如 +CSQ: 18,0 )。 2. 确保 CREG 返回 0,1 或 0,5 。 3. 尝试延长 AT+CMGS 后的等待超时时间(如60秒),并加入重试机制。 |
7.3 高级技巧与优化
- 状态机设计 :在MCU程序中,不要用简单的
delay等待AT响应。实现一个 非阻塞的AT指令状态机 。将“发送指令”、“等待响应”、“解析响应”、“执行下一步”拆分成不同状态,由主循环或定时器驱动。这样MCU在等待模块响应时还能处理其他任务,系统响应更灵敏。 - 超时与重试 :为每一个等待响应的环节设置合理的超时。特别是
AT+CMGS后的等待,网络延迟可能导致响应慢,超时可设长些(如30秒)。发送失败后,应有重试逻辑(例如最多3次),并在重试前检查一下网络状态(AT+CSQ)。 - PDU编码库 :如果项目频繁使用PDU模式,建议将手机号编码、文本转UCS2、PDU组装等功能封装成独立的函数库。这会极大提高代码的复用性和可读性。
- 电源管理 :对于电池供电的设备,在非发送时段,可以尝试使用
AT+CFUN=0将模块进入最低功耗模式(如果模块支持),需要时再唤醒。但要注意,睡眠模式下无法接收短信。 - 短信拼接(长短信) :单条短信有长度限制(UCS2编码为70字符)。发送更长内容需要用到 长短信(Concatenated SMS) 功能。这需要在PDU的UDH(用户数据头)中设置标识符。实现较为复杂,如果需求不迫切,可以考虑将长信息分多条发送,或在应用层设计分段协议。
调试中最深刻的体会是, 耐心和日志 是最重要的工具。每一个成功的指令序列都值得保存,每一个奇怪的错误码都值得深究。当你把PDU那一长串十六进制码流背后的每一个字节都弄明白时,那种对通信协议的理解和掌控感,是任何现成库都无法给予的。
更多推荐



所有评论(0)