
简介这份资源面向使用CDMA调制解调器进行短信开发的C#程序员聚焦一个常见却棘手的问题CDMA猫不支持PDU模式无法在超级终端直接输入中文短信只能通过编程以UNICODE编码发送。资源以PDF形式提供压缩包内共1个文件大小约38KB内容围绕串口通信、编码转换与AT命令配置展开包含可直接参考的C#代码模板。代码演示了用BigEndianUnicode转换中文字符串、初始化SerialPort并设置115200波特率、通过ATWSCL与ATCMGF切换文本模式、处理号码前“86”前缀以及用CTRLZ结束短信等关键环节。目前已有256人学习适合需要快速打通CDMA中文短信发送流程、排查乱码与发送失败的开发者参考也可作为不同厂商猫型号适配调试的基础起点。1. CDMA 猫发中文短信为什么 PDU 模式是绕不开的坎手里有一台 CDMA 模块串口能通、AT 能回 OK但一发送中文短信就翻车——要么收到一串问号要么直接 ERROR。这个场景在电力抄表、车载终端、工业 DTU 里非常常见设备侧往往是一块 C# 写的上位机通过 SerialPort 或 TCP 连到模块业务要求把告警文本、点位名称、中文提示原样发到运维人员手机上。英文短信用 Text 模式几行就搞定中文却必须切到 PDU 模式把整条短信按 GSM 03.38 的编码规则重新组装成十六进制串再交给 ATCMGS 发送。标题里的三个关键词——CDMA、AT 命令、C#——分别对应硬件通道、控制协议和宿主语言。需要先厘清一个容易混淆的点很多人口中的“CDMA 猫”实际是 CDMA 2000 制式的模块它的短信编码并不完全等同于 GSM 的 PDU部分模块走的是 CDMA 自己的 IS-637 短消息格式但市面上大量模块为了兼容上位机生态仍然提供了类 GSM 的 PDU 接口或者干脆用 Text 模式加 UCS2 编码。所以真正落地时第一件事不是写代码而是拿模块手册确认它支持哪种中文编码路径。这篇内容面向正在做 C# 上位机、需要把中文短信稳定发出去的工程师从编码原理讲到可复现的代码再到实际调试中那些让人抓狂的坑。2. 先搞懂 CDMA 模块的中文短信编码路径2.1 Text 模式为什么发不了中文Text 模式ATCMGF1下短信内容直接以字符串形式跟在 ATCMGS 后面模块内部按默认字符集处理。问题在于绝大多数 CDMA 模块的默认字符集是 ASCII 或 GSM 7-bit遇到中文字符时模块要么截断、要么替换成?、要么直接返回 ERROR。有些模块手册会写“支持 UCS2”但实际测试发现Text 模式下即使把字符集设成 UCS2模块对多字节的处理也不稳定尤其是短信长度接近 70 个字符时分包逻辑容易出错。我一般会先做一次快速验证用串口工具手动发ATCMGF1然后ATCSCSUCS2再发一条ATCMGS手机号内容写“测试”看模块返回什么。如果返回 OK 但手机收到乱码说明模块的 Text 模式 UCS2 支持不完整如果直接 ERROR那就彻底断了 Text 模式的念想老老实实走 PDU。2.2 PDU 模式的中文编码逻辑PDU 模式ATCMGF0下短信内容不再是明文字符串而是一整段十六进制表示的协议数据单元。对于中文核心是把每个汉字按 Unicode 码点转成 UCS2 大端字节序再整体转成十六进制字符串。举例“中”的 Unicode 是 U4E2DUCS2 大端就是4E2D。整条短信的 PDU 串由短信中心号码、目标号码、协议标识、编码方式、有效期、用户数据长度、用户数据等字段拼接而成。这里有一个关键参数DCSData Coding Scheme。中文短信必须把 DCS 设为0x08表示 UCS2 编码。如果 DCS 设成0x00模块会按 7-bit 处理中文必然乱码。另一个容易忽略的是用户数据长度UDL它表示的是用户数据的字节数不是字符数。一个汉字占 2 字节所以 10 个汉字的 UDL 是 20转成十六进制是14。2.3 CDMA 与 GSM 在 PDU 上的差异虽然很多模块共用 PDU 接口但 CDMA 模块在短信中心号码格式、目标号码编码上可能与 GSM 有细微差别。GSM 的 PDU 里目标号码需要做半字节交换并补 F例如13800138000会变成3108108300F0这样的形式。CDMA 模块有的要求号码前加86有的要求直接写 11 位号码的十六进制具体要看模块手册的 AT 命令集。我的做法是先拿模块厂商提供的 PDU 示例串用串口工具手动发一次确认手机能收到正确中文再把这条示例串拆解成字段反推出模块对号码格式、短信中心格式的具体要求。这一步花十分钟能省掉后面几小时的瞎猜。2.4 用 C# 组装 PDU 串的最小步骤在 C# 里组装 PDU核心是几个转换函数字符串转 UCS2 十六进制、电话号码半字节交换、短信中心号码处理、长度计算。下面是一个最小可用的组装流程先不涉及串口发送只把 PDU 串拼出来。// 将中文字符串转为 UCS2 大端十六进制字符串 public static string StringToUcs2Hex(string text) { byte[] bytes Encoding.BigEndianUnicode.GetBytes(text); StringBuilder sb new StringBuilder(); foreach (byte b in bytes) { sb.Append(b.ToString(X2)); } return sb.ToString(); } // 电话号码半字节交换并补 F用于 PDU 中的目标号码字段 public static string PhoneToPdu(string phone) { // 去掉 86 等前缀只保留数字 string digits new string(phone.Where(char.IsDigit).ToArray()); // 如果长度为奇数补 F if (digits.Length % 2 ! 0) { digits F; } StringBuilder sb new StringBuilder(); for (int i 0; i digits.Length; i 2) { sb.Append(digits[i 1]); sb.Append(digits[i]); } return sb.ToString(); } // 计算用户数据长度字节数转十六进制 public static string UdlHex(string ucs2Hex) { int byteLen ucs2Hex.Length / 2; return byteLen.ToString(X2); }逻辑说明StringToUcs2Hex用Encoding.BigEndianUnicode拿到大端字节序再逐字节转十六进制。PhoneToPdu先过滤非数字字符奇数长度补F然后两两交换位置。UdlHex把 UCS2 十六进制串的长度除以 2 得到字节数再转成两位十六进制。参数说明Encoding.BigEndianUnicode对应 UCS2 大端不要用Encoding.Unicode后者是小端。电话号码过滤时如果模块要求保留86需要根据手册调整有的模块要求86转成A1之类的特殊编码不能简单过滤。拼出这三段后再按模块手册的顺序拼接短信中心号码、目标号码、PID、DCS、有效期、UDL、UD最后在前面加上ATCMGS和长度长度是 PDU 串去掉短信中心字段后的字节数具体算法见下一章。3. 在 C# 里把 PDU 串通过串口发出去3.1 串口参数与 AT 命令交互节奏C# 里用System.IO.Ports.SerialPort打开串口参数一般是波特率 115200、8 数据位、无校验、1 停止位。但 CDMA 模块的 AT 命令交互有节奏要求每条命令后要等模块返回OK或才能发下一条不能连续猛发。我见过有人用WriteLine连发三条 AT结果模块只回了第一个 OK后面全乱。正确的做法是封装一个SendAtCommand方法写完后阻塞读取直到收到OK、ERROR或。读取时设置ReadTimeout一般 2000ms 到 5000ms短信发送阶段可能更久因为要等网络侧返回。private string SendAtCommand(SerialPort port, string cmd, int timeoutMs 3000) { port.DiscardInBuffer(); port.WriteLine(cmd); StringBuilder response new StringBuilder(); DateTime start DateTime.Now; while ((DateTime.Now - start).TotalMilliseconds timeoutMs) { try { string line port.ReadLine(); response.AppendLine(line); if (line.Contains(OK) || line.Contains(ERROR) || line.Contains()) { break; } } catch (TimeoutException) { break; } } return response.ToString(); }逻辑说明先清空输入缓冲区避免上一条命令的残留数据干扰。写入命令后循环读取遇到OK、ERROR或就退出。超时后也退出返回已读到的内容。参数说明timeoutMs默认 3000发短信时建议调到 10000 以上因为ATCMGS后要等提示符发送 PDU 后要等网络确认时间可能超过 5 秒。port.ReadLine()依赖NewLine属性一般设为\r\n。3.2 发送中文短信的完整命令序列一条中文短信的发送流程分四步设置 PDU 模式、设置字符集、发送目标号码和长度、发送 PDU 串并以 CtrlZ 结束。下面是对应的 C# 代码片段。public bool SendChineseSms(SerialPort port, string phoneNumber, string message) { // 1. 切换到 PDU 模式 string r1 SendAtCommand(port, ATCMGF0); if (!r1.Contains(OK)) return false; // 2. 设置字符集为 UCS2部分模块需要部分不需要 SendAtCommand(port, ATCSCS\UCS2\); // 3. 组装 PDU 串 string ucs2Hex StringToUcs2Hex(message); string udl UdlHex(ucs2Hex); string phonePdu PhoneToPdu(phoneNumber); // 短信中心号码字段通常用模块默认这里以 00 表示使用默认短信中心 string sca 00; // 目标号码长度数字个数不含 86 string phoneLen (phoneNumber.Where(char.IsDigit).Count()).ToString(X2); // PDU 串SCA 目标号码长度 目标号码 PID DCS 有效期 UDL UD string pdu sca phoneLen phonePdu 0008 AA udl ucs2Hex; // 计算 ATCMGS 后面的长度参数PDU 串去掉 SCA 后的字节数 int pduLen (pdu.Length - sca.Length) / 2; // 4. 发送 ATCMGS string r2 SendAtCommand(port, $ATCMGS{pduLen}, 5000); if (!r2.Contains()) return false; // 5. 发送 PDU 串并以 CtrlZ 结束 port.Write(pdu \x1A); string r3 SendAtCommand(port, , 15000); return r3.Contains(OK) || r3.Contains(CMGS); }逻辑说明先切 PDU 模式再设字符集。组装 PDU 时sca用00表示使用模块默认短信中心这是最常见做法。phoneLen是目标号码的数字个数十六进制表示。PID用00DCS用08表示 UCS2有效期AA是默认值。pduLen是 PDU 串去掉 SCA 后的字节数因为ATCMGS后面的长度参数不包含短信中心字段。参数说明sca如果模块要求显式指定短信中心需要按91 半字节交换的号码 00格式填写。phoneLen如果号码带86有的模块要求长度包含86需要实测。DCS固定08不要改成00。有效期用AA表示相对格式的最大值也可以按需改成具体天数。3.3 短信中心号码字段的两种处理方式短信中心号码SCA在 PDU 串里是可选的。如果填00表示使用模块当前设置的默认短信中心这是最省事的方式。如果模块要求显式指定SCA 格式是长度字节 类型字节91 半字节交换的号码 00。例如短信中心8613800100500去掉后是8613800100500奇数长度补F半字节交换后是683108100005F0加上类型和长度SCA 就是0B91683108100005F0。我一般先用00试如果模块返回 ERROR 或手机收不到再改成显式 SCA。显式 SCA 的长度字节是号码字节数加 1类型字节例如上面号码交换后是 14 个十六进制字符即 7 字节加类型字节共 8 字节长度写08。但不同模块对长度字节的计算可能不同需要看手册。3.4 用 telnet 命令先验证模块的 TCP 通道有些 CDMA 模块不是通过串口而是通过网口转串口或内置 TCP Server 暴露 AT 命令。这种情况下可以用telnet命令先连上模块的 IP 和端口手动发几条 AT 确认通道正常。比如telnet 192.168.1.100 23连上后输入AT看是否回OK。如果 telnet 连不上先检查端口是否通可以用telnet ip 端口的方式看通不通或者用ping确认网络层。这一步能排除掉“模块根本没起来”的低级问题避免在 C# 代码里瞎调。提示telnet 命令在部分 Windows 版本默认未安装需要在“启用或关闭 Windows 功能”里勾选 Telnet 客户端。Linux 下一般自带直接telnet ip port即可。4. 中文短信发送的避坑与排查清单4.1 手机收到问号或乱码现象短信能发出去模块返回 OK但手机收到一串问号或方块。原因DCS 字段没设成08或者 UCS2 编码时用了小端字节序。另一个常见原因是模块的字符集没切到 UCS2虽然 PDU 模式理论上不依赖ATCSCS但部分模块在 PDU 模式下仍会参考字符集设置。解决检查 PDU 串里 DCS 是否为08检查Encoding.BigEndianUnicode是否用对。如果还不行在ATCMGF0后加一条ATCSCSUCS2再发一次。4.2 模块返回 ERROR 且无任何提示现象发ATCMGS长度后直接 ERROR连都不回。原因长度参数算错了。ATCMGS后面的长度是 PDU 串去掉 SCA 后的字节数不是整个 PDU 串的长度。如果 SCA 填了00长度就是(pdu.Length - 2) / 2如果 SCA 是显式号码长度是(pdu.Length - sca.Length) / 2。很多人直接把整个 PDU 串长度除以 2导致模块认为数据长度不对。解决打印出 PDU 串和计算出的长度手动核对。可以用模块手册里的示例 PDU 串反推长度算法。4.3 发送后卡在等待或 OK现象ATCMGS长度发出后模块不回或者发了 PDU 后一直等不到 OK。原因串口读取超时设置太短或者模块正在注册网络。CDMA 模块在信号弱或未注册时短信发送会阻塞。另一个原因是ATCMGS后没有等就直接发了 PDU模块把 PDU 当成新命令解析自然不回 OK。解决把ATCMGS的超时调到 5000ms 以上发送 PDU 后的超时调到 15000ms。发送前用ATCREG?确认网络注册状态返回0,1或0,5才表示已注册。4.4 长短信被截断或只收到前半段现象超过 70 个汉字的短信手机只收到前半段或者收到多条不完整的短信。原因单条 PDU 短信的 UCS2 用户数据最多 140 字节即 70 个汉字。超过后需要拆分成多条短信并在 PDU 里加 UDH用户数据头表示拼接关系。很多模块不自动处理长短信拆分需要上位机自己拆。解决在 C# 里按 67 个汉字一组拆分留出 UDH 的 6 字节每组加 UDH050003 参考号 总条数 当前序号。这部分逻辑较复杂建议先确认业务是否真的需要长短信如果只是告警短文本控制在 70 字以内最省事。4.5 换了一张 SIM 卡后短信发不出去现象同一套代码和模块换卡后发送失败。原因不同运营商的短信中心号码不同如果 PDU 里显式写了 SCA换卡后 SCA 不匹配。另外部分物联网卡默认关闭短信功能需要联系运营商开通。解决优先用00作为 SCA让模块自动读取 SIM 卡里的短信中心。如果必须显式指定换卡后同步更新 SCA。物联网卡的问题只能找运营商确认短信权限。5. 进阶把短信发送封装成可复用的 C# 服务5.1 用委托和事件解耦串口收发在实际上位机里短信发送不应该阻塞主线程。我一般会把串口收发封装成一个独立服务类用委托把发送结果回调给 UI 层。C# 的委托和事件机制在这里很合适定义一个SmsSentHandler委托服务类里声明事件发送完成后触发UI 层订阅事件更新状态。这样串口读写跑在后台线程界面不会卡死。public delegate void SmsSentHandler(string phone, bool success, string detail); public class SmsService { public event SmsSentHandler OnSmsSent; private SerialPort _port; public void SendAsync(string phone, string message) { Task.Run(() { bool ok SendChineseSms(_port, phone, message); OnSmsSent?.Invoke(phone, ok, ok ? 发送成功 : 发送失败); }); } }逻辑说明SendAsync把发送逻辑丢到线程池避免阻塞调用方。发送完成后通过事件通知订阅者。OnSmsSent?.Invoke用空条件运算符避免没有订阅者时抛异常。参数说明phone和message是发送目标和内容。detail可以带上模块返回的原始信息方便排查。事件订阅方在 UI 线程更新控件时记得用Invoke切回 UI 线程。5.2 用文档注释生成可维护的接口说明C# 的 XML 文档注释在团队协作里很实用。给SendChineseSms加上summary、param、returnsVisual Studio 里调用时能直接看到参数含义。尤其是 PDU 组装这种参数多、容易传错的逻辑文档注释能减少很多沟通成本。/// summary /// 发送中文短信内部使用 PDU 模式。 /// /summary /// param nameport已打开的串口对象波特率建议 115200。/param /// param namephoneNumber目标手机号11 位数字不带 86。/param /// param namemessage短信内容建议不超过 70 个汉字。/param /// returns发送成功返回 true失败返回 false。/returns public bool SendChineseSms(SerialPort port, string phoneNumber, string message)逻辑说明文档注释不参与编译但会被编译器提取成 XML 文件供 IDE 和其他工具使用。参数说明里写清楚边界条件比如“不带 86”“不超过 70 个汉字”比事后口头交代靠谱。5.3 验证短信是否真正到达的两种手段模块返回 OK 只代表短信提交给了网络不代表对方收到。要确认到达可以在短信内容里加一个唯一序号让对方回复确认或者用模块的ATCMGL读取发送状态报告需要先ATCSMP设置状态报告请求。状态报告在 PDU 模式下也是十六进制串解析起来比发送更麻烦一般业务里用“发送成功即认为完成”就够了除非是关键告警。我自己的习惯是先在串口工具里手动发通一条中文短信把 PDU 串和 AT 命令序列截图存下来再照着写 C# 代码。代码写完先发给自己手机确认收到后再接入业务。这样出问题时能快速判断是 PDU 组装错了还是串口交互节奏不对。希望帮到你。本文还有配套的精品资源点击获取