
简介这是一份面向通信工程专业学生与移动通信初学者的GSM设备短信收发实验报告文档围绕短消息业务SMS的网络结构、信令流程与模块控制方法展开可用于课程实验预习、报告撰写参考或短消息开发入门。压缩包内仅1个doc文件约471KB内容完整收录实验目的、实验原理与操作要点。文档系统梳理了SMS的存储转发与传递确认两大机制、点对点短消息与小区广播业务的区别以及移动台发送和接收短消息时MSC、VLR、HLR与SMSC之间的信令交互过程并对比Block、Text、PDU三种收发模式给出PDU编码字段构成与UCS2中文编码实例还有通过AT命令控制GSM模块完成短信收发的实现思路。目前已有80人学习下载适合希望理解短信底层流程并动手实践的读者参考。1. 为什么 GSM 短信收发实验卡住的往往不是模块很多人第一次做 GSM 设备短信收发实验流程都差不多买一块 SIM800C 或 SIM900A 的板子插上开通短信功能的 SIM 卡USB 转 TTL 接到电脑串口助手敲AT回一个OK心里一松接着ATCMGF1、ATCMGS138xxxxxxxx敲中文进去模块回了CMGS: 12 OK手机上也确实收到了——但收到的是一串问号或者干脆空内容。真正的坑从这一刻才开始。短信收发这条链路上AT 指令只是最外面一层壳壳里面是 GSM 07.05/07.07 定义的一整套编码规则GSM 7-bit 默认字符集、UCS2、PDU 报文的字段顺序、长短信的 UDH 头、有效期与数据编码方案 DCS。模块不会报错它只会照着你给的字节发出去解不出来就是解不出来。这套东西适合三类人做物联网设备告警、远程抄表、短信网关的嵌入式工程师要写实验报告、把「GSM设备短信收发实验.doc」交上去的学生以及需要把短信通道接进自己业务系统、又不想被第三方云短信绑死的后端开发。往下走我把选型、AT 底座、编码、收发实战和排错按顺序拆开。2. GSM 模块选型与 AT 指令底座怎么搭2.1 SIM800C、SIM900A、EC20 的差别与串口接线选型不用纠结参数表有多长看三个点你所在位置的网络制式、模块是否带独立串口电平转换、以及短信相关的 AT 指令集是否完整。SIM900A 只吃 2G很多地区 2G 已经在退网买之前先确认本地还有没有 2G 信号这是最容易白干一天的坑。SIM800C 同样是 2G但价格低、资料多做实验够用。EC20、EC200 这类 4G Cat.1/Cat.4 模块向下兼容短信指令集是 3GPP 27.007 那一套只是开机流程更长、功耗更高。模块制式短信 AT 兼容性典型串口常见坑SIM900A2GGSM 07.05 完整3.3V TTL2G 退网后注册不上SIM800C2G完整支持 TEXT/PDU3.3V TTL供电电流不足会反复重启EC20 / EC2004G完整含长短信3.3V TTL默认 115200开机到可用要 10~20 秒MC35 / TC352G 老模块部分只支持 PDU3.3V TTL无ATCMGS长度校验接线本身只有四根模块 TXD 接 USB-TTL 的 RXD模块 RXD 接 USB-TTL 的 TXDGND 共地VCC 单独供电。供电这一点要单独强调模块在发射瞬间电流能冲到 2A直接用 USB-TTL 模块上的 3.3V 引脚供电十次里有八次会看到模块无限重启或者AT时有时无。提示先用万用表确认模块电源脚在发射瞬间的压降掉到 3.0V 以下就该换独立电源别怀疑串口线。2.2 用 pyserial 打通最小 AT 指令闭环串口助手只能验证一次做收发实验必须把交互写成代码。下面这段是最小闭环能开串口、发指令、拿回显后面所有章节都建立在它上面。import serial import time PORT COM3 # Linux/macOS 下一般是 /dev/ttyUSB0 或 /dev/ttyACM0 BAUD 115200 # SIM800C 常见 9600/115200EC20 默认 115200 PER_CMD_TIMEOUT 1.0 # 单条指令等待回显的时间上限秒 def open_port(): ser serial.Serial(PORT, BAUD, timeoutPER_CMD_TIMEOUT) ser.write(bAT\r\n) # 先打一次 AT唤醒处于睡眠态的模块 time.sleep(0.3) return ser def send_at(ser, cmd, wait1.0): ser.reset_input_buffer() # 清掉上一轮残留回显避免误判上一指令的结果 ser.write((cmd \r\n).encode(ascii)) time.sleep(wait) raw ser.read(ser.in_waiting or 1) return raw.decode(utf-8, errorsreplace)timeout设成 1.0 秒是为了让read在没有数据时能及时返回而不是永久阻塞reset_input_buffer()是关键模块回显是异步的不清缓冲就会出现「上一条的 OK 被当成这一条的结果」。wait参数按指令耗时给AT给 0.3ATCMGS这种要等网络侧的给 5 到 6 秒。接上模块后按下面的顺序打一遍任何一条不符合预期就先停在它上面ATE0 # 关回显命令行干净脚本解析也简单 ATCPIN? # 期望 CPIN: READY卡没识别就查卡座和 PIN 锁 ATCSQ # 期望 CSQ: 15,0 以上第一位是信号强度 0~31 ATCREG? # 期望 CREG: 0,1已注册本地网或 0,5漫游 ATCMEE2 # 打开详细错误码后面报错会告诉你原因而不是 ERROR ATCPMSSM,SM,SM # 短信读写都走 SIM 卡存储容量小但兼容性最好ATCPIN?返回CME ERROR: 10表示没插卡CME ERROR: 11是需要 PIN 码ATCSQ第一位长期低于 10就别测收发了先解决天线和位置。ATCMEE2是我建议一开始就打开的同样是发送失败它会把 500、515 这类具体错误码抛出来省掉半天猜测。2.3 TEXT 模式和 PDU 模式ATCMGF 到底怎么选ATCMGF1是 TEXT 模式内容直接写字符串ATCMGF0是 PDU 模式内容要自己拼十六进制。很多人问该用哪个结论是做纯英文实验可以 TEXT只要涉及中文、长短信、状态报告、自定义有效期就必须 PDU。对比项TEXT 模式CMGF1PDU 模式CMGF0中文支持依赖ATCSCS设置模块实现不一致明确走 UCS2可控长短信部分模块自动拆分部分不支持自己控制 UDH行为一致状态报告结果字段少解析困难完整 TPDU可查每段状态阅读成本低高需要理解字段偏移调试难度出错信息模糊可以用在线 PDU 工具逐字段核对TEXT 模式下最典型的翻车是ATCSCSUCS2之后ATCMGS后面的号码和正文都要写成 UCS2 十六进制写中文反而更别扭。所以实验里我一般先用 TEXT 模式跑通一条纯英文确认链路没问题再切ATCMGF0做正式的短信收发实验。3. 短信编码GSM 7-bit、UCS2 与 PDU 报文拆解3.1 为什么发中文必须落到 UCS2GSM 07.05 里的默认字符集是 7-bit一共 128 个字符的位置覆盖拉丁字母、数字、常用符号和少量希腊字符中文根本不在里面。模块在 7-bit 模式下遇到中文字节要么截断、要么当成扩展字符表里的东西发出去接收端自然是一串乱码。解决路径有两条DCS 字段填0x08走 UCS2每个字符 2 字节或者填0x00走 7-bit只发英文和数字。国内实验最常见的配置就是ATCSMP17,167,0,8第四个参数 8 就是 UCS2 的 DCS发送中文不用再做其他设置。判断某条短信用了哪种编码只需要看 PDU 里的 DCS 字段00是 7-bit08是 UCS2F1之类留给厂商自定义。7-bit 还有个隐藏细节叫「七位压缩」140 字节的载荷如果按 7 位算能塞进 160 个字符代价是字节边界不对齐拼包和拆包都要做位移运算。UCS2 没这个问题代价是容量直接砍到 70 个字符。这就是为什么一条中文短信超过 70 个字会被拆成多条。3.2 一条 PDU 报文的字段级拆解以发送「你好」到8613800138000为例完整 PDU 是0011000D91683108103800F000 08AA044F60597D按字段拆开看每一段的边界和含义如下字段示例值长度含义SCA 长度001 字节服务中心号码长度填 00 表示用模块里存的服务中心号SCA无0 字节长度字段为 00 时这一段整体省略首字节111 字节MTI01 表示 SUBMITTP-VPF10 表示用相对格式有效期MR001 字节消息参考号模块自己维护实测时不对应任何业务DA 长度0D1 字节目标号码共 13 位数字DA 类型911 字节91 表示国际号码本地号码是 81DA683108103800F07 字节号码按半字节交换后的结果奇数位补 FPID001 字节协议标识普通短信固定 00DCS081 字节数据编码方案08 表示 UCS2VPAA1 字节相对格式有效期AA 约等于 4 天UDL041 字节用户数据长度UCS2 下按字节算这里是 4 字节UD4F60597D4 字节「你好」的 UTF-16BE 编码号码的「半字节交换」最容易被写错861380013800这种奇数长度要先补F变成8613800138000F再两两互换得到683108103800F0。写代码时如果发现发出的短信直接进了对方的垃圾箱多半是这里的长度字段或者 TYPE-OF-ADDRESS 取值错了。注意DA 长度统计的是「数字个数」不是字节数8613800138000是 13 个数字长度字段就该写0D写成07会直接被网络侧拒绝。3.3 用 Python 组装和解析 PDU先处理字符集转换再组装 SUBMIT 报文两段逻辑分开写方便调试。def text_to_ucs2_hex(text: str) - str: 中文按 UTF-16BE 编码转成大写十六进制字符串 return text.encode(utf-16-be).hex().upper() def ucs2_hex_to_text(hex_str: str) - str: return bytes.fromhex(hex_str).decode(utf-16-be) def encode_number(number: str): 把号码转成 PDU 里的 (长度, 类型, 交换后数字) 三元组 digits number.lstrip() if len(digits) % 2: digits F # 奇数位补 F保证两两成对 swapped .join(digits[i 1] digits[i] for i in range(0, len(digits), 2)) toa 91 if number.startswith() else 81 return %02X % len(number.lstrip()), toa, swapped def build_submit_pdu(number: str, text: str) - str: smsc 00 # 使用模块内置服务中心号 pdu_type 11 # MTI01 SUBMITTP-VPF10 相对有效期不要状态报告 mr 00 # 消息参考号随便给模块会自己重排 da_len, da_toa, da encode_number(number) pid 00 # 普通点对点短信 dcs 08 # UCS2中文必须 vp AA # 有效期约 4 天 ud text_to_ucs2_hex(text) udl %02X % (len(ud) // 2) # UDL 按字节数计 return smsc pdu_type mr da_len da_toa da pid dcs vp udl ud三个参数值得单独说dcs08决定后面 UD 必须按 UCS2 处理换成00就会按 7-bit 解析中文立刻变成乱码vpAA是相对有效期范围大致是从 5 分钟到 63 周测试时可以临时改成10缩短到几小时避免积压的短信干扰后续实验pdu_type位 5 置 1也就是写成31就会要求网络回状态报告后面能拿到CDS上报。4. 收发实战CMGS 交互、CMTI 中断与长短信拼接4.1 发送ATCMGS 的两次回车与 CtrlZPDU 模式下发短信不是一条指令搞定而是一段交互先ATCMGS长度模块回一个提示符再发 PDU 十六进制串最后跟一个 CtrlZ0x1A表示结束。很多人卡在这里因为串口助手不会自动帮你补0x1A。def send_sms(ser, number, text): send_at(ser, ATCSCSUCS2) # 让后续字符集设置一致避免解析歧义 send_at(ser, ATCMGF0) # 切到 PDU 模式 send_at(ser, ATCSMP17,167,0,8) # 第四位 8 表示 DCSUCS2 pdu build_submit_pdu(number, text) tpdulen (len(pdu) - 2) // 2 # 减去 SCA 长度字段本身只报 TPDU 长度 ser.reset_input_buffer() ser.write((ATCMGS%d\r\n % tpdulen).encode(ascii)) time.sleep(0.5) ser.read(ser.in_waiting) # 吃掉 提示符 ser.write(pdu.encode(ascii) b\x1a) # CtrlZ 结束输入并触发发送 time.sleep(6) # 等网络侧回 CMGS: mr 和 OK return ser.read(ser.in_waiting).decode(utf-8, errorsreplace)tpdulen的算法是最容易出错的一处当 SCA 写00时整个 PDU 里有一部分是 SCA 自身的长度字段ATCMGS只要 TPDU 的字节数所以要把开头那 2 个十六进制字符减掉再折半。少算一个字节模块会回CMS ERROR: 304长度不匹配多算一个字节会卡在提示符后面等不到结果。返回内容里CMGS: 12的 12 是消息参考号不是短信条数真正的发送成功以最后那个OK为准。如果等了 6 秒只看到ERROR先把ATCMEE2打开重跑一次拿到具体错误码再定位。4.2 接收ATCNMI 与 CMTI 的处理循环接收侧的关键是让模块主动告诉你「来短信了」而不是轮询。这靠ATCNMI控制ATCNMI2,1,0,0,0表示新短信先存进存储然后通过CMTI: SM,index上报索引程序拿到索引再用ATCMGR去读。这样即使程序在处理上一条短信也不会丢。def read_sms(ser, index: int) - str: raw send_at(ser, ATCMGR%d % index, wait1.0) for line in raw.splitlines(): line line.strip() # CMGR 后面那一行才是 PDU跳过状态行和空行 if line and all(c in 0123456789ABCDEFabcdef for c in line) and len(line) 20: return parse_deliver_pdu(line) return def watch_incoming(ser, interval1.0): send_at(ser, ATCNMI2,1,0,0,0) # 新短信存 SIM用 CMTI 上报索引 send_at(ser, ATCPMSSM,SM,SM) # 读、写、收统一走 SIM buf while True: buf ser.read(ser.in_waiting or 1).decode(utf-8, errorsreplace) for line in buf.splitlines(): if line.startswith(CMTI:): idx int(line.split(,)[-1]) print(read_sms(ser, idx)) send_at(ser, ATCMGD%d % idx) # 读完立刻删防止 SIM 写满 buf time.sleep(interval)ATCNMI四个参数依次是新短信处理方式2 表示存起来再上报、上报方式1 表示CMTI2 表示直接推CMT全文、状态报告上报、小区广播。做实验我建议用2,1,0,0,0因为 SIM 卡本身容量只有几十条删得勤一点比调试时丢短信强。ATCMGD放在读取之后立刻执行这个顺序不能反。解析 DELIVER 报文时字段偏移和 SUBMIT 不一样SCA 后面第一个字节是首字节接着才是发件人长度、类型和号码然后跳过 PID、DCS 和 7 字节的时间戳 SCTS才到 UDL 和 UD。SCTS 是 7 字节的半字节交换 BCD写解析时别按 6 字节读。4.3 长短信 UDH 拼接与乱序去重中文超过 70 个字UCS2 无 UDH 时就会被拆成多条每条载荷里带一个 UDH 头用来标识「这是第几段」。常见两种格式UDH 字节含义可用载荷UCS2 每段字数05 00 03 CC 02 018 位参考号总 2 段本段第 1 段134 字节67 个字符06 08 04 12 34 02 0116 位参考号总 2 段本段第 1 段133 字节66 个字符拼接逻辑就是按「发件人 参考号」分组组内按段序号排序段齐了再合并def merge_parts(parts): parts: [(sender, ref, total, seq, text), ...] groups {} for sender, ref, total, seq, text in parts: groups.setdefault((sender, ref), {})[seq] text merged [] for (sender, ref), seg in groups.items(): total max(seg) if seg else 0 if len(seg) total and all(i in seg for i in range(1, total 1)): merged.append((sender, .join(seg[i] for i in range(1, total 1)))) return merged实际抓包里经常遇到乱序第 2 段比第 1 段先到或者只有部分段到达。别用「收到第一条就立即展示」也别用「等固定秒数」这种写法前者会显示半句话后者在弱信号下必然出问题。按段序号补齐再输出给一个合理的超时比如 30 秒丢弃不完整的组是工业现场最稳的做法。提示发送端也要配合超过 70 字的文本要在代码里主动分段、生成 UDH别指望所有模块的自动拆分行为一致。5. 排错与实验记录掉网、乱码、重复上报怎么定位5.1 故障对照表现象、根因、先跑哪条指令现象常见根因验证指令ATCMGS一直不给未切 PDU 模式或长度算错ATCMGF?、重算 TPDU 长度手机收到乱码DCS 与 UD 编码不匹配ATCSMP?看第四位是否 8CMS ERROR: 500号码格式或服务中心号问题ATCSCA?确认服务中心号反复CREG: 0,2搜不到网或 SIM 欠费ATCSQ、ATCOPS?收到重复CMTI读完之后没删或 CPMS 存储混用ATCPMS?、补ATCMGD中文显示成问号接收端用了 7-bit 解码检查首字节 TP-UDHI 与 DCS模块随机重启供电不足万用表测发射瞬间压降这张表的用法是从上往下排先确认模式设置再确认编码最后才怀疑硬件。顺序反了会浪费大量时间在拆线接线上。5.2 用 CSQ、CREG、CMEE 做自检与自动恢复长时间跑收发实验最怕的是模块悄无声息地掉网代码还在循环调用ATCMGS结果全部堆在错误分支里。加一个轻量自检循环一分钟一次成本很低def health_check(ser): csq send_at(ser, ATCSQ, wait0.5) reg send_at(ser, ATCREG?, wait0.5) if CREG: 0,1 not in reg and CREG: 0,5 not in reg: send_at(ser, ATCFUN0, wait1.0) # 关射频 send_at(ser, ATCFUN1, wait10.0) # 重新开机注册比反复复位稳 return False try: rssi int(csq.split(CSQ:)[1].split(,)[0]) except (IndexError, ValueError): rssi 0 return rssi 10 # 低于 10 就认为链路质量不够暂停发送任务ATCFUN0/1比直接给模块断电温和得多能避免插卡状态丢失ATCFUN1之后一定要等够 10 秒4G 模块注册更慢等 20 秒更保险。信号强度低于 10 时别发长短信分段短信在弱信号下丢段概率明显上升宁可进队列等下一轮。5.3 把 SMS 实验文档写成可复现的记录「GSM设备短信收发实验.doc」这类报告最容易写成流水账接了线、敲了指令、成功了。真正有用的记录要包含四样东西缺一样别人就复现不了。第一是模块型号、固件版本ATCGMR能读、供电方式第二是完整指令序列和每条的实际回显包括失败的那几条第三是关键 PDU 原文最好是收和发各一条完整的十六进制串附上字段对照第四是 SIM 卡的运营商、卡内服务中心号ATCSCA?和测试时间点。记录时有个小技巧把发送成功的 PDU 和接收到的 PDU 并排放在同一张表里因为 SUBMIT 和 DELIVER 的字段偏移不同并排看能立刻发现「首字节后面是不是多了 SCTS」这类问题。实验做十次不如把一次的结果记到能照着重跑一遍这才是这条链路真正值钱的部分。本文还有配套的精品资源点击获取