ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C# 通过 CDMA 猫发送中文短信:PDU 编码与 UCS2 实战

C# 通过 CDMA 猫发送中文短信:PDU 编码与 UCS2 实战 简介这份资源面向使用CDMA调制解调器进行短信开发的C#程序员聚焦一个常见却棘手的问题CDMA猫不支持PDU模式无法在超级终端直接输入中文短信只能通过程序以UNICODE编码发送。资源以PDF形式给出可运行的C#代码模板演示如何用SerialPort类完成串口通信、BigEndianUnicode编码转换、ATWSCL与ATCMGF等命令配置以及CTRLZ结束标识的写入细节并提示号码前需去掉86、波特率通常为115200等易错点。压缩包共1个PDF文件约38KB内容紧凑适合作为调试基础模板按具体猫的厂商与型号调整。目前已有257人学习可帮助读者绕开乱码与发送失败问题快速掌握CDMA中文短信的编程发送思路。1. CDMA 猫发中文短信为什么 PDU 编码是绕不开的第一道坎手里有一台 CDMA 模块常见的是工业级串口猫插在工控机或上位机上要用 C# 通过 AT 命令发一条中文短信出去——这个需求听起来简单真动手就会发现英文短信一条ATCMGS就完事中文却十有八九卡在编码上发出去是乱码或者干脆返回ERROR或者短信发出去了但对方收到一串问号。问题不在 AT 命令本身而在于中文短信必须走 PDUProtocol Data Unit模式而 PDU 里的中文要经过 UCS2 编码再转十六进制长度、字节序、拼接方式一个都不能错。这篇讲的就是这条链路从串口打开、AT 命令握手、ATCMGF切换模式到 PDU 组包、UCS2 编码、ATCMGS发送、CMGS回执解析全部用 C# 落地。适合正在做 C# 上位机、工控数据采集、设备告警短信推送的工程师也适合刚接触 CDMA 模块、被中文编码折腾过一轮的人。下面按「先跑通最小链路再抠参数和坑」的顺序来代码可以直接抄进项目改。2. 从串口到 AT 握手把 CDMA 猫拉进可通信状态2.1 先确认模块到底认哪套 AT 指令集CDMA 模块和 GSM 模块在 AT 指令上有交集也有差异。发短信的核心命令ATCMGF、ATCMGS、ATCPMS基本通用但 CDMA 侧对 PDU 模式的支持程度、字符集命令ATCSCS的取值、以及短信存储位置编号规则不同厂商比如一些基于高通平台的模块会有出入。所以第一步不是写代码是拿串口工具手动敲一遍确认模块的真实反应。常见做法是用串口调试助手波特率一般 115200也有 9600 的看模块手册8 数据位、1 停止位、无校验。连上后依次发AT ATCPMS? ATCMGF? ATCSCS?AT返回OK说明串口通了。ATCPMS?看短信存储区返回类似CPMS: SM,0,20,SM,0,20表示 SIM/模块存储可用。ATCMGF?返回CMGF: (0,1)说明支持 0PDU和 1文本两种模式。ATCSCS?看支持的字符集通常有IRA、UCS2、GSM。提示如果ATCMGF?只返回(0)说明这个模块只支持 PDU 模式文本模式发中文这条路直接堵死必须走 PDU。这一步别省。我见过有人代码写得没问题就是模块固件把ATCSCS锁死在IRA导致后面 UCS2 编码发出去全乱。手动确认一遍比在代码里 debug 半天快得多。2.2 C# 串口初始化别用默认值超时和编码都要显式设C# 里操作串口用System.IO.Ports.SerialPort。这个类有几个默认值很坑ReadTimeout默认是InfiniteTimeout一旦模块不回应读操作会永久阻塞上位机直接假死。所以初始化时超时必须显式给。using System; using System.IO.Ports; using System.Text; public class CdmaModem { private SerialPort _port; public bool Open(string portName, int baudRate 115200) { _port new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One); // 关键显式设置超时避免模块无响应时线程永久阻塞 _port.ReadTimeout 3000; _port.WriteTimeout 3000; // 关闭 DTR/DSR 流控多数工业模块不需要 _port.Handshake Handshake.None; // 编码用 ASCIIAT 命令本身是 ASCII中文在 PDU 里已转成十六进制字符串 _port.Encoding Encoding.ASCII; try { _port.Open(); return true; } catch (Exception ex) { Console.WriteLine($串口打开失败: {ex.Message}); return false; } } }逻辑说明SerialPort构造函数里dataBits8、stopBitsOne是 AT 通信的标准配置。ReadTimeout设 3000ms 是经验值——模块响应 AT 命令通常在几百毫秒内3 秒足够又不至于卡太久。Encoding设 ASCII 是因为我们发给模块的 AT 命令和 PDU 字符串全是 ASCII 可见字符中文在组包阶段就已经转成 UCS2 十六进制了串口层不碰中文。参数说明portName在 Windows 上是COM3这种格式可以用SerialPort.GetPortNames()枚举。baudRate必须和模块实际波特率一致不一致会收到乱码或完全无响应。如果模块支持自动波特率先发AT试探但工业场景建议固定波特率避免协商失败。2.3 写一个带结束符判断的 AT 命令收发方法AT 命令的收发不是简单的「写进去读出来」。模块的响应以\r\n分隔最后以OK、ERROR或CMx:开头的结果码结束。如果只读一次很可能读到半截。正确做法是循环读到出现终止标志为止。public string SendAt(string command, int waitMs 500) { _port.DiscardInBuffer(); // 清掉上次残留 _port.Write(command \r); // AT 命令以 \r 结尾 System.Threading.Thread.Sleep(waitMs); var sb new StringBuilder(); var deadline DateTime.Now.AddMilliseconds(_port.ReadTimeout); while (DateTime.Now deadline) { try { string chunk _port.ReadExisting(); if (!string.IsNullOrEmpty(chunk)) { sb.Append(chunk); // 出现终止标志就提前退出 if (sb.ToString().Contains(OK\r\n) || sb.ToString().Contains(ERROR\r\n) || sb.ToString().Contains( )) // CMGS 等待输入提示符 break; } } catch (TimeoutException) { break; } System.Threading.Thread.Sleep(50); } return sb.ToString(); }逻辑说明DiscardInBuffer()清空接收缓冲防止上一条命令的残留响应干扰判断。Write时命令末尾加\r这是 AT 规范要求的回车符有些模块也接受\r\n但\r最通用。循环里用ReadExisting()非阻塞读取累积到StringBuilder一旦检测到OK、ERROR或ATCMGS特有的等待输入提示符就跳出。参数说明waitMs是发送后等待模块开始响应的时间500ms 对多数模块够用。ReadTimeout决定整个读取循环的上限。注意这个提示符——发ATCMGS后模块不会立刻返回OK而是回一个等你输入短信内容这个判断必须加否则会一直等到超时。3. PDU 组包中文短信的 UCS2 编码与长度计算3.1 PDU 结构拆解为什么中文必须走 UCS2一条 PDU 格式的短信核心结构是SMSC 地址 短信首字节 目标号码 PID DCS 时间戳 用户数据长度 用户数据。其中 DCSData Coding Scheme决定编码方式00是 7-bit GSM 编码英文08是 UCS2 编码中文。中文一个字符在 UCS2 下占 2 字节转成十六进制就是 4 个字符。很多人第一次发中文失败是因为用了文本模式ATCMGF1然后直接ATCMGS号码发中文。文本模式下模块对中文的支持取决于固件很多 CDMA 模块根本不认返回ERROR。稳妥路线是ATCMGF0走 PDU自己组包。PDU 里目标号码的编码也有讲究号码要按半字节反转比如13800138000要写成3108103800F0补 F 凑偶数位然后两两反转。这一步错了短信会发到错误号码或者直接被拒。3.2 UCS2 编码与号码半字节反转的 C# 实现先解决两个基础转换字符串转 UCS2 十六进制以及号码转 PDU 格式。// 中文转 UCS2 十六进制字符串 public static string ToUcs2Hex(string text) { var bytes Encoding.BigEndianUnicode.GetBytes(text); // UCS2 大端 var sb new StringBuilder(); foreach (var b in bytes) sb.Append(b.ToString(X2)); return sb.ToString(); } // 目标号码转 PDU 半字节反转格式 public static string EncodePhoneNumber(string number) { // 去掉 86 等前缀只留数字 number number.TrimStart(); if (number.Length % 2 ! 0) number F; // 奇数位补 F var sb new StringBuilder(); for (int i 0; i number.Length; i 2) { // 每两位反转 sb.Append(number[i 1]); sb.Append(number[i]); } return sb.ToString(); }逻辑说明Encoding.BigEndianUnicode就是 UCS2 大端序中文「你好」会得到4F60597D。注意不能用Encoding.Unicode那是小端字节序反了对方收到就是乱码。号码反转是因为 PDU 规范里号码按半字节nibble存储低位在前。13800138000补 F 后是13800138000F两两反转得到3108103800F0。参数说明ToUcs2Hex输入是任意中英文字符串输出是纯十六进制大写字符串长度一定是 4 的倍数。EncodePhoneNumber输入带不带都行内部会去掉输出长度是偶数。如果号码本身是偶数位不补 F直接反转。3.3 组装完整 PDU 串长度字段是最容易算错的地方有了 UCS2 和号码编码就可以拼 PDU 了。以ATCMGS为例PDU 串里第一个字节是 SMSC 地址长度通常设00表示用模块默认短信中心。public static string BuildPdu(string phoneNumber, string message) { string smsc 00; // 用默认短信中心 string firstOctet 11; // 短信首字节普通提交 string tpMr 00; // 消息参考号 string tpDa EncodePhoneNumber(phoneNumber); string tpDaLength (phoneNumber.TrimStart().Length).ToString(X2); string tpPid 00; // 协议标识 string tpDcs 08; // UCS2 编码 string tpScts 00000000000000; // 时间戳模块可自动填 string userData ToUcs2Hex(message); string userDataLength (userData.Length / 2).ToString(X2); // 字节数 return smsc firstOctet tpMr tpDaLength tpDa tpPid tpDcs tpScts userDataLength userData; }逻辑说明tpDaLength是目标号码的「数字位数」不是字节数13800138000是 11 位写成0B。userDataLength是用户数据的字节数UCS2 下每个中文 2 字节userData.Length / 2就是字节数。tpScts设全 0 让模块自己填时间戳多数模块支持。参数说明firstOctet11表示普通短信提交如果需要状态报告可以改成31。tpDcs08是 UCS2 的固定值改成00就变英文 7-bit 了。userDataLength最大FF255 字节但实际单条短信 UCS2 最多 70 个字符140 字节超了要拆多条这是另一个话题。注意ATCMGS的 PDU 长度参数是「除去 SMSC 部分后的长度」也就是从firstOctet开始算的字节数不是整个 PDU 串的长度。这个坑我第一次踩的时候短信死活发不出去返回ERROR查了半天才发现长度多算了 SMSC 那两个字符。4. 发送与回执ATCMGS 的交互时序和 CMGS 解析4.1 ATCMGS 的两段式交互先发命令再发内容ATCMGS和普通 AT 命令不一样它是两段式先发ATCMGS长度模块回提示符再发 PDU 串 CtrlZ0x1A模块才真正发送并返回CMGS: mr和OK。这个时序必须严格遵守中间不能乱插命令。public bool SendSms(string phoneNumber, string message) { string pdu BuildPdu(phoneNumber, message); // PDU 长度 总长度/2 - SMSC 部分长度(2字符1字节) int pduLength pdu.Length / 2 - 1; // 第一段发 CMGS 命令 string resp1 SendAt($ATCMGS{pduLength}, 300); if (!resp1.Contains()) { Console.WriteLine($未进入输入模式: {resp1}); return false; } // 第二段发 PDU CtrlZ _port.Write(pdu \x1A); System.Threading.Thread.Sleep(3000); // 发送需要时间等长一点 string resp2 _port.ReadExisting(); // 成功标志CMGS: xx 后跟 OK return resp2.Contains(CMGS:) resp2.Contains(OK); }逻辑说明pduLength的计算是pdu.Length / 2 - 1因为 PDU 串是十六进制字符串两个字符代表一个字节减去 SMSC 那一个字节00两个字符。SendAt里已经判断了提示符这里再确认一次。第二段写入后必须等足够时间短信发送涉及网络交互3 秒是保守值网络差可以加到 5 秒。参数说明\x1A是 CtrlZ 的 ASCII 码是 PDU 模式发送的结束标志不能少也不能用别的字符代替。resp2里CMGS:后面的数字是消息参考号可以用来和状态报告对应。如果返回CMS ERROR: 500通常是 PDU 格式错误CMS ERROR: 515可能是模块没注册网络。4.2 回执解析与错误码对照发送失败时模块返回的错误码是排查的唯一线索。常见错误码和对应原因整理如下错误码含义常见原因500未知错误PDU 格式错误、长度算错515模块未注册网络无信号、SIM 未就绪21短信中心地址无效SMSC 未设置330短信存储满需要先删旧短信305无效的 PDU 模式参数DCS 或编码不匹配解析回执时不要只判断OK要把CMS ERROR:后面的码提取出来打日志。工业现场没有日志出了问题只能靠猜。private void ParseError(string resp) { int idx resp.IndexOf(CMS ERROR:); if (idx 0) { string code resp.Substring(idx 11).Trim().Split(\r)[0]; Console.WriteLine($短信发送失败错误码: {code}); } }逻辑说明CMS ERROR:后面跟的是错误码可能带\r\n所以要截断。把错误码打出来对照上面的表基本能定位问题。参数说明idx 11是CMS ERROR:这个字符串的长度Split(\r)[0]取第一段避免把换行符带进日志。4.3 一个完整的发送流程串起来把前面的方法串成一次完整调用var modem new CdmaModem(); if (modem.Open(COM3)) { modem.SendAt(AT); // 握手 modem.SendAt(ATCMGF0); // PDU 模式 modem.SendAt(ATCSCS\UCS2\); // 字符集 bool ok modem.SendSms(13800138000, 设备告警温度过高); Console.WriteLine(ok ? 发送成功 : 发送失败); }逻辑说明ATCMGF0切 PDU 模式ATCSCSUCS2告诉模块后续用 UCS2 字符集。注意ATCSCS在某些模块上对 PDU 模式没影响但设了不亏。SendSms内部完成组包和两段式发送。参数说明ATCSCS的值要用双引号包起来在 C# 字符串里要转义成\。如果模块不支持 UCS2 字符集这条命令会返回ERROR但不影响 PDU 发送可以忽略。5. 避坑与排查中文短信发不出去时先看这几条5.1 坑一短信发出去了对方收到乱码现象ATCMGS返回OK流程全对但接收方看到的是乱码或问号。原因UCS2 编码用了小端序。Encoding.Unicode在 C# 里是小端Encoding.BigEndianUnicode才是 UCS2 大端。PDU 规范要求大端。解决确认ToUcs2Hex里用的是BigEndianUnicode。可以拿「你好」测试正确输出是4F60597D如果输出604F7D59就是字节序反了。5.2 坑二ATCMGS 返回 ERROR长度参数怎么算都不对现象ATCMGS长度后模块直接回ERROR连提示符都不给。原因长度参数算错。这个长度是「PDU 串去掉 SMSC 部分后的字节数」不是整个 PDU 串的字符数也不是字节数。解决pduLength pdu.Length / 2 - 1。pdu.Length / 2是整个 PDU 的字节数减 1 是去掉 SMSC 那一个字节。如果 SMSC 不是00而是实际号码减去的长度要按 SMSC 实际字节数算。5.3 坑三模块无响应上位机界面卡死现象点了发送按钮界面直接卡住任务管理器显示进程无响应。原因SerialPort.ReadTimeout默认是InfiniteTimeout模块不回应时读操作永久阻塞如果在 UI 线程调用就直接卡死。解决显式设ReadTimeout并且把短信发送放到后台线程或Task.Run里不要在主 UI 线程做串口读写。工业上位机里这是血泪经验界面卡一次现场就投诉一次。5.4 坑四发送成功但对方很久才收到或者收不到现象模块返回OK但短信延迟几分钟甚至丢失。原因模块信号弱、短信中心繁忙或者ATCMGS后等待时间不够就发了下一条命令导致上一条还没真正提交。解决发送后至少等 3 秒再操作模块。如果要做批量发送每条之间加 2-3 秒间隔并且检查CMGS:回执确认提交成功。信号质量可以用ATCSQ查返回值第一个数小于 10 说明信号很差。5.5 坑五换了一张 SIM 卡就发不出去了现象同样的代码换 SIM 卡后全部失败。原因不同运营商的短信中心号码不同PDU 里 SMSC 设00让模块用默认值但有些模块的默认 SMSC 是空的。解决用ATCSCA?查当前短信中心如果是空用ATCSCA8613800xxx500设置号码问运营商要。设置后再发。6. 进阶批量发送、状态报告与长短信拆分单条中文短信跑通后实际项目里很快就会遇到三个进阶需求批量发送、确认对方收到、以及超过 70 个字符的长短信。批量发送的关键不是循环调用SendSms而是控制节奏。我一般会在每条之间加 2 秒延迟并且用ATCPMS?检查存储区避免模块缓存满导致后续失败。如果一次要发几百条建议加一个发送队列失败的重试两次两次都失败就记日志跳过不要死磕一条把整个队列堵死。状态报告需要两步PDU 首字节从11改成31开启状态报告请求然后模块收到状态报告后会通过CDS:主动上报。C# 侧要开一个后台线程持续读串口解析CDS:里的状态码。CDS: 0表示对方已收到非 0 表示失败。这个机制在告警场景很有用能确认短信真的送达了而不是只发出去。长短信拆分是中文短信最麻烦的部分。UCS2 下单条最多 70 个字符超过就要拆成多条每条带 UDH用户数据头标识序号接收方按序号拼接。UDH 占 6 字节所以拆分后每条实际只能放 67 个字符。拆分逻辑要自己实现把消息按 67 字符切段每段前面加050003参考号总段数当前段号的 UDH然后userDataLength要把 UDH 的 6 字节算进去。参考号随便取一个 0-255 的值同一条长短信的所有分段用同一个参考号。// 长短信拆分核心逻辑示意 public static Liststring SplitLongSms(string message, byte refNum) { const int maxPerPart 67; // UCS2 下扣除 UDH 后的容量 var parts new Liststring(); int total (int)Math.Ceiling(message.Length / (double)maxPerPart); for (int i 0; i total; i) { string seg message.Substring(i * maxPerPart, Math.Min(maxPerPart, message.Length - i * maxPerPart)); string udh $050003{refNum:X2}{total:X2}{(i 1):X2}; parts.Add(udh ToUcs2Hex(seg)); } return parts; }逻辑说明050003是 UDH 固定头表示后续是 16 位参考号的分段信息。refNum同一条短信的所有分段必须一致。total和(i1)分别是总段数和当前段号接收方靠这两个值拼接。每段的userDataLength要算成UDH 字节数 内容字节数。参数说明maxPerPart67是 UCS2 长短信的标准值如果模块支持更长的 UDH 可以调整但 67 最通用。refNum建议用随机数或递增计数器避免和别的长短信冲突。最后说个我自己的习惯每次改完 PDU 组包逻辑先拿「你好」这种两个字的短消息测确认4F60597D这个已知结果对得上再测长消息。PDU 这东西长度和字节序错一个字符就全盘失败用已知结果做锚点比对着日志猜快十倍。希望帮到你。本文还有配套的精品资源点击获取
返回列表