1. 长短信发送的技术背景与挑战
作为一名在通信领域摸爬滚打多年的工程师,我经常遇到需要发送长短信的场景。标准的短信协议(GSM 03.40)规定,单条短信的最大长度是140字节。采用USC2编码(支持中文等Unicode字符)时,每个字符占用2字节,因此实际可发送的汉字数量是70个。这个限制源于上世纪80年代GSM标准制定时的技术条件。
但在实际应用中,我们经常需要发送超过70个汉字的内容——比如物流跟踪信息、系统告警详情、验证码附带说明等。这时候就需要使用"长短信"(也称为"串联短信")技术。这项技术通过以下机制实现:
- 分包传输:将长内容拆分成多个标准短信(通常每条134字节,原因后文会解释)
- 重组标识:在每条分包短信中添加特殊头信息(UDH),告知接收手机这些分片属于同一条消息
- 按序重组:接收端根据UDH中的序号信息,将所有分片按正确顺序组合显示
重要提示:不是所有手机都支持长短信重组功能。虽然现代智能手机基本都支持,但一些老式功能机可能只会显示分片短信。在关键业务场景需要考虑兼容性方案。
2. 长短信的PDU格式深度解析
2.1 PDU基础结构
PDU(Protocol Data Unit)是短信在GSM网络中传输时的二进制格式。完整的长短信PDU包含以下字段(以发送为例):
code复制[SCA][PDU-Type][MR][DA][PID][DCS][VP][UDL][UD]
各字段详细说明:
| 字段缩写 | 全称 | 长度(字节) | 说明 | 长短信特别注意 |
|---|---|---|---|---|
| SCA | 服务中心地址 | 1-12 | 短信中心号码 | 通常可留空(00) |
| PDU-Type | 协议数据单元类型 | 1 | 控制参数集合 | 长短信必须设为0x51 |
| MR | 消息参考号 | 1 | 发送序列号 | 每条递增,不重复即可 |
| DA | 目标地址 | 2-12 | 接收方手机号 | 需编码处理 |
| PID | 协议标识 | 1 | 消息类型 | 普通短信设为00 |
| DCS | 数据编码方案 | 1 | 编码方式 | USC2编码为08 |
| VP | 有效期 | 0/1/7 | 保存时长 | 可设为FF(最长) |
| UDL | 用户数据长度 | 1 | UD字段长度 | 含UDH时需计算 |
| UD | 用户数据 | 0-140 | 实际内容 | 包含UDH+正文 |
2.2 长短信关键字段设置
PDU-Type字段:
- 普通短信:0x11(表示无UDH)
- 长短信:0x51(表示包含UDH)
这个字节的低四位(bit3~bit0)表示TP-MTI和TP-RD,高四位(bit7~bit4)包含TP-SRR等标志。对于长短信发送,我们通常设置为01010001(即0x51)。
DCS字段:
USC2编码对应值为0x08。如果使用7-bit编码(仅支持ASCII),则为0x00,但中文必须使用USC2。
3. 用户数据头(UDH)实现细节
3.1 UDH结构剖析
UDH(User Data Header)是插入在用户数据前的特殊控制信息。长短信UDH通常采用6字节格式:
code复制[UDHL][IEI][IEDL][IED:RefNum][IED:Total][IED:Current]
具体字节解析:
| 偏移 | 名称 | 长度 | 值 | 说明 |
|---|---|---|---|---|
| 0 | UDHL | 1 | 05 | UDH总长度(不含自身) |
| 1 | IEI | 1 | 00 | 信息元素标识(长短信) |
| 2 | IEDL | 1 | 03 | 信息元素数据长度 |
| 3 | IED1 | 1 | C6 | 参考编号(随机) |
| 4 | IED2 | 1 | 02 | 总分片数 |
| 5 | IED3 | 1 | 01 | 当前分片序号 |
3.2 UDH实战示例
假设要发送一条150个汉字的长短信(需要拆分为3条):
-
第一条短信UDH:
- 参考编号:0xA1(随机生成)
- 总分片数:0x03
- 当前序号:0x01
hex复制05 00 03 A1 03 01 -
第二条短信UDH:
hex复制05 00 03 A1 03 02 -
第三条短信UDH:
hex复制05 00 03 A1 03 03
经验之谈:参考编号最好每次随机生成,避免与前次发送冲突。我常用
(millis() & 0xFF)获取随机值。
4. 完整AT指令发送流程
4.1 准备工作
-
设置短信文本模式:
at复制AT+CMGF=0 // 设置为PDU模式 -
确认编码方式:
at复制AT+CSCS="UCS2" // 使用USC2编码
4.2 计算PDU长度
关键计算公式:
code复制UDL = UDH长度(6) + 实际文本长度
最大文本长度 = 140 - 6 - 1 = 133字节 (66个USC2字符)
例如发送80个汉字:
- 总字节:80×2=160
- 需要分2条发送:
- 第一条:67字符(134字节)
- 第二条:13字符(26字节)
4.3 实际发送示例
假设发送到手机号13800138000,内容为80个汉字:
-
第一条短信PDU:
code复制0011000B813108001380F00008045005 00 03 A1 02 01 [134字节编码后的文本]对应AT指令:
at复制AT+CMGS=149 // 149是PDU总长度 > 0011000B813108001380F000080450050003A10201[文本...] -
第二条短信PDU:
code复制0011000B813108001380F00008045005 00 03 A1 02 02 [26字节编码后的文本]
5. 常见问题与调试技巧
5.1 发送失败排查清单
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 返回ERROR | PDU格式错误 | 检查各字段长度和编码 |
| 发送成功但接收不全 | UDH参数不一致 | 确保参考号、总分片数相同 |
| 中文乱码 | 编码设置错误 | 确认DCS=08和CSCS="UCS2" |
| 部分手机接收异常 | UDH兼容性问题 | 尝试5字节UDH(去掉IEI) |
5.2 单片机实现注意事项
-
缓冲区管理:
c复制#define MAX_PDU_LEN 180 // 建议比实际需求大20% char pduBuffer[MAX_PDU_LEN]; -
分片算法示例:
c复制void splitLongSms(const char* text) { int totalLen = strlen(text); int segmentSize = 66; // 每个分片最大字符数 int segmentCount = (totalLen + segmentSize - 1) / segmentSize; for(int i=0; i<segmentCount; i++) { int start = i * segmentSize; int len = (i == segmentCount-1) ? (totalLen-start) : segmentSize; sendPduSegment(text+start, len, i+1, segmentCount); } } -
内存优化技巧:
- 使用PROGMEM存储常量字符串
- 动态计算PDU长度避免固定大数组
- 分片发送后立即释放内存
6. 性能优化与高级技巧
6.1 发送超时处理
在嵌入式系统中,建议实现以下重试机制:
c复制#define MAX_RETRY 3
int sendWithRetry(const char* pdu) {
for(int i=0; i<MAX_RETRY; i++) {
if(sendAtCommand(pdu) == SUCCESS) {
return SUCCESS;
}
delay(1000 * (i+1)); // 指数退避
}
return FAILURE;
}
6.2 短信编码优化
对于纯ASCII内容,可以采用7-bit编码节省空间:
- 设置DCS=0x00
- 使用GSM 7-bit字母表
- 最大可发送160字符(但中文仍需USC2)
6.3 接收长短信处理
当模块收到长短信时,PDU中会包含类似的UDH结构。重组算法要点:
- 收集相同参考号的所有分片
- 按序号排序
- 拼接用户数据部分
- 超时处理(建议30秒未收全则丢弃)
在实际项目中,我发现最稳妥的方式是每条分片短信单独存储,等所有分片到达后再重组。这样可以避免内存碎片问题,特别适合资源受限的嵌入式设备。