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 基础设置指令链

发送短信前,必须完成一系列模块初始化设置,这就像给模块上电后进行的“开机设置”。一个稳健的设置流程如下:

  1. AT - 测试指令,用于检查模块与MCU的串口通信是否正常。返回 OK 是后续一切操作的前提。
  2. AT+CPIN? - 查询SIM卡状态。确保返回 +CPIN: READY ,否则需要输入PIN码( AT+CPIN="1234" )。
  3. AT+CSQ - 查询信号强度。这个指令的返回格式通常是 +CSQ: <rssi>,<ber> <rssi> 是信号强度,数值范围0-31,越大越好(99表示未知或不可用)。例如, +CSQ: 24,0 表示信号强度良好。在发送重要短信前检查一下信号,能避免因信号弱导致的发送失败。
  4. 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" 以配合中文编码。
  • 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 (北京移动)。

  1. 去掉 + 号: 8613800100500
  2. 检查是否为偶数位,如果是奇数,在末尾补 F 。本例是13位(奇数),补 F 8613800100500F
  3. 将每两位数字交换位置(奇偶互换):
    • 86 -> 68
    • 13 -> 31
    • 80 -> 08
    • 01 -> 10
    • 00 -> 00
    • 50 -> 05
    • 0F -> F0 (注意,补的 F 在这里参与交换)
  4. 最终得到的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的核心部分。

  1. 第一个字节(TP-MTI, RP, UDHI, SRR等) :通常设置为 11 。表示:

    • TP-MTI=01 (短信发送)
    • TP-RD=0 (替换重复消息)
    • TP-VPF=10 (有效期格式为相对时间)
    • TP-SRR=1 (请求状态报告)
    • TP-UDHI=0 (用户数据无头)
    • TP-RP=0 (不回复路径)
  2. 消息参考(TP-MR) 00 ,由模块自动填充。

  3. 目标地址长度(TP-DA Length) :目标号码 13912345678 ,共11位。11位数字转换为半字节(nibble)的数量,即 0B

  4. 目标地址类型(TP-DA Type) 91 ,表示国际号码。

  5. 目标地址(TP-DA) :对号码 13912345678 进行编码。

    • 去掉 + 13912345678
    • 奇数位补 F 13912345678F
    • 每两位交换: 31 19 32 54 76 F8
    • 编码结果: 3119325476F8
  6. 协议标识(TP-PID) 00 ,普通GSM消息。

  7. 数据编码方案(TP-DCS) 08 。表示:

    • Bit 7-4: 00 (无消息类别)
    • Bit 3-2: 10 (UCS2编码)
    • Bit 1-0: 00 (保留)
    • 08 表示UCS2编码,无有效期。如果要设置有效期和发送等级,可以设为 18 (Class 0,直接显示)等。
  8. 有效期(TP-VP) :由于DCS中未指定有效期格式,这里留空或根据设置。如果我们用 08 ,则VP不出现。如果用 11 作为第一字节,并指定了相对时间格式的VP,则需要一个VP字节。

  9. 用户数据长度(TP-UDL) :用户数据(短信内容)的字节数。中文“测试”的Unicode编码是 6D4B 8BD5 ,共4个字节。所以UDL= 04

  10. 用户数据(TP-UD) :短信内容“测试”的UCS2编码。

    • “测”的Unicode是 0x6D4B
    • “试”的Unicode是 0x8BD5
    • 编码后为: 6D4B8BD5

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

  1. 编码手机号 :将每个数字转换为Unicode。 1 -> 0031 , 3 -> 0033 , ... 8 -> 0038 。连起来: 00310033003900310032003300340035003600370038
  2. 编码短信内容 :“测试”-> 6D4B8BD5
  3. 发送指令
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" )并返回存储索引。

工作流程

  1. 模块初始化时,设置 AT+CNMI=2,1,0,0,0
  2. MCU的串口接收程序一直运行。当收到一条完整的以 \r\n 结尾的串口数据行时,进行解析。
  3. 如果解析到 +CMTI: "SM",5 ,则表示新短信存储在SIM卡索引5的位置。
  4. MCU立即发送 AT+CMGR=5 读取该条短信。
  5. 解析 AT+CMGR 的返回数据,提取发送方号码和短信内容。
  6. 处理完毕后,发送 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 必备调试工具与方法

  1. 硬件连接 :确保TX、RX、GND连接正确,电源稳定(模块峰值电流可能达2A)。最好在电源引脚就近并联一个大电容(如100uF)缓冲电流冲击。
  2. 串口调试助手 :使用支持十六进制显示和发送的调试工具(如SecureCRT、Putty或开源的QCOM)。将发送和接收的数据都勾选“十六进制显示”,这样你能看到每一个字节,包括 \r \n 0x1A
  3. 指令记录 :养成好习惯,将每次成功和失败的完整指令交互日志保存下来。这是排查问题的黄金资料。

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 高级技巧与优化

  1. 状态机设计 :在MCU程序中,不要用简单的 delay 等待AT响应。实现一个 非阻塞的AT指令状态机 。将“发送指令”、“等待响应”、“解析响应”、“执行下一步”拆分成不同状态,由主循环或定时器驱动。这样MCU在等待模块响应时还能处理其他任务,系统响应更灵敏。
  2. 超时与重试 :为每一个等待响应的环节设置合理的超时。特别是 AT+CMGS 后的等待,网络延迟可能导致响应慢,超时可设长些(如30秒)。发送失败后,应有重试逻辑(例如最多3次),并在重试前检查一下网络状态( AT+CSQ )。
  3. PDU编码库 :如果项目频繁使用PDU模式,建议将手机号编码、文本转UCS2、PDU组装等功能封装成独立的函数库。这会极大提高代码的复用性和可读性。
  4. 电源管理 :对于电池供电的设备,在非发送时段,可以尝试使用 AT+CFUN=0 将模块进入最低功耗模式(如果模块支持),需要时再唤醒。但要注意,睡眠模式下无法接收短信。
  5. 短信拼接(长短信) :单条短信有长度限制(UCS2编码为70字符)。发送更长内容需要用到 长短信(Concatenated SMS) 功能。这需要在PDU的UDH(用户数据头)中设置标识符。实现较为复杂,如果需求不迫切,可以考虑将长信息分多条发送,或在应用层设计分段协议。

调试中最深刻的体会是, 耐心和日志 是最重要的工具。每一个成功的指令序列都值得保存,每一个奇怪的错误码都值得深究。当你把PDU那一长串十六进制码流背后的每一个字节都弄明白时,那种对通信协议的理解和掌控感,是任何现成库都无法给予的。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐