资讯动态

C语言PDU短信编解码实战:从手机号到十六进制的逐字节解析

发布时间:2026/10/26 11:14:04 来源:尧图企业网站定制
简介本资源是一个面向嵌入式开发与通信协议学习者的C语言PDU短信编解码实践Demo聚焦GSM网络中短信服务中心SMSC与终端间底层二进制通信机制的理解与实现。适用于具备基础C语言能力、希望深入移动通信协议栈或开发短信网关/模组驱动的中高级开发者。压缩包共13个文件含4个头文件如Thread_Modem.h、Sms_Send.h等定义线程与短信模块接口、3个C源文件含thread_test.cpp、Sms_Send.cpp等核心逻辑、2份Word技术文档CMPP3.0长短信方案、bit7/bit8/UCS2编码详解、1个PDFGSM短消息与长短信解码原理及1个OBJ目标文件整体618KB结构紧凑、即下即用。已有652人学习下载提供从编码原理、7位/8位/UCS2多模式处理到长短信拼接的完整代码链路与配套说明覆盖PDU报文各字段SCA、DA、DCS、UD解析与构造是理解短信传输本质、开展二次开发或教学演示的实用参考。1. PDU短信编解码demo一个能真正把“你好”变成00000000000000000000000000000000的C语言黑匣子你有没有试过用串口调试工具往GSM模块发AT指令ATCMGF0切到PDU模式然后拼了一长串十六进制字符串——结果模块回了个CMS ERROR: 302不是格式错不是长度超而是你手写的PDU字节序列里地址域少补了两个0TP-UD长度字段算错了1个字节时间戳用了本地时区没转UTC……这种问题查三天日志不如看一眼标准文档里的字节对齐图。这个C语言PDU短信编解码demo就是专治这类“玄学失败”的手术刀级工具它不依赖任何第三方库纯ANSI C实现从手机号、短信内容、服务中心号码SMSC开始逐字节生成符合3GPP TS 23.040规范的PDU编码反过来也能把设备返回的原始PDU字符串比如07916831082000F0040B916831281511F500008221017120103131精准拆解成可读的发送时间、对方号码、短信正文。它适合嵌入式开发者调试Modem通信、物联网固件工程师做短信透传协议验证也适合刚学完《计算机网络》想亲手摸一摸OSI七层模型最底层数据封装的同学——因为所有逻辑都在pdu_encode.c和pdu_decode.c里摊开写着没有魔法只有位运算和状态机。2. 架构与目录结构五个源文件撑起整个PDU世界这个项目极简但完整全部代码控制在5个C文件1个头文件内没有任何Makefile以外的构建依赖。我下载下来第一件事就是tree -I build|*.o|*.exe确认结构结果就看到pdu_demo/ ├── pdu.h # 核心结构体定义 函数声明 ├── pdu_encode.c # 编码主逻辑手机号→BCD、内容→7bit/UCS2、PDU包组装 ├── pdu_decode.c # 解码主逻辑PDU字符串→字节流→字段提取→内容还原 ├── main.c # 示例入口演示encode/decode双向流程 └── utils.c # 辅助函数hex字符串↔字节数组、BCD转换、7bit unpack等提示所有文件都遵循ANSI C89标准#include stdio.h和string.h之外不引入任何系统头文件确保能在裸机环境如STM32 HAL库下或老版本交叉编译器arm-linux-gcc 3.4.5中直接移植。2.1 pdu.h定义PDU世界的宪法头文件是理解整个项目的第一把钥匙。它没用宏定义一堆常量而是用结构体把PDU帧的物理布局映射成内存结构// pdu.h typedef struct { uint8_t smsc_len; // SMSC地址长度含类型字节单位半字节nibble uint8_t smsc_type; // SMSC地址类型0x91国际格式 uint8_t smsc_addr[12]; // SMSC地址BCD编码最大11字节即22位数字 } pdu_smsc_t; typedef struct { uint8_t tp_mr; // TP-Message-Reference通常为0 uint8_t tp_ra_len; // Recipient Address长度半字节 uint8_t tp_ra_type; // 接收方地址类型0x91国际 uint8_t tp_ra_addr[12]; // 接收方号码BCD uint8_t tp_scts[7]; // Service Centre Time StampUTC7字节压缩格式 uint8_t tp_udl; // User Data Length用户数据字节数非字符数 uint8_t tp_ud[256]; // User Data实际短信内容7bit或UCS2编码 } pdu_submit_t;注意tp_udl字段的注释——这是新手翻车重灾区。PDU里TP-UDL表示的是tp_ud数组里实际占用的字节数不是你输入的中文字符个数。比如你好用UCS2编码占4字节tp_udl就得填4若用7bit编码GSM 03.38字符集Hello 5字符占5字节tp_udl填5。这个细节在pdu_encode.c的pdu_encode_submit()函数里被反复校验。2.2 main.c三分钟跑通的黄金示例main.c只做一件事用真实数据调用编解码接口打印中间过程。它不写UI不连硬件纯粹验证逻辑。关键片段如下// main.c int main() { char smsc[] 8613800138000; // 服务中心号码 char dest[] 8613912345678; // 目标手机号 char text[] 测试PDU编码; // 短信内容UTF-8 uint8_t pdu_buf[512]; int pdu_len 0; // 步骤1编码 pdu_len pdu_encode_submit(smsc, dest, text, pdu_buf, sizeof(pdu_buf)); if (pdu_len 0) { printf(Encode failed: %d\n, pdu_len); return -1; } printf(PDU HEX: ); for (int i 0; i pdu_len; i) { printf(%02X, pdu_buf[i]); } printf(\n); // 步骤2解码用刚生成的pdu_buf反向验证 pdu_submit_t submit; int ret pdu_decode_submit(pdu_buf, pdu_len, submit); if (ret ! 0) { printf(Decode failed: %d\n, ret); return -1; } printf(Decoded: to%s, text%s\n, submit.tp_ra_addr_str, submit.tp_ud_text); return 0; }这段代码的价值在于它强制你关注输入输出的边界条件。比如pdu_encode_submit()返回负值时错误码直接对应pdu.h里定义的PDU_ERR_XXX枚举PDU_ERR_SMSC_TOO_LONG,PDU_ERR_TEXT_TOO_LONG等比查AT指令手册快十倍。2.3 utils.c位操作与编码转换的底层引擎PDU的核心难点全在utils.c——这里没有高级算法全是位移、掩码、查表。比如GSM 7bit编码的unpack函数// utils.c int gsm7_unpack(const uint8_t *src, int src_len, char *dst, int dst_size) { int bit_pos 0; int dst_idx 0; uint8_t byte 0; for (int i 0; i src_len dst_idx dst_size - 1; i) { // 取出src[i]的bit_pos到bit_pos6共7位 uint8_t bits (src[i] bit_pos) | ((src[i1] (7-bit_pos)) 0x7F); dst[dst_idx] bits; bit_pos; if (bit_pos 7) { bit_pos 0; i; // 跳过下一个字节的低7位 } } dst[dst_idx] \0; return dst_idx; }这段代码的精妙在于它用bit_pos跟踪当前读取位置通过和组合把跨字节的7bit块无缝拼接。如果你用Python写过类似逻辑会发现C里手动管理位偏移反而更直观——没有bytes.unpack()的魔法每一步位运算都暴露在阳光下。3. 编码核心流程从手机号到PDU字节流的七步炼金术PDU编码不是简单拼接而是一套严格的状态机。pdu_encode_submit()函数按顺序执行以下7个步骤每步失败都返回对应错误码3.1 SMSC地址预处理国际格式BCD转换PDU要求SMSC地址以BCDBinary Coded Decimal格式存储且必须带国际前缀。例如8613800138000需处理为去掉→8613800138000补零至偶数位13位→14位→86138001380000每两位倒序GSM规范→6831082000F0注意末尾00变F0因0的BCD是00F是填充符// pdu_encode.c 关键片段 int bcd_encode(const char *phone, uint8_t *bcd_out, int max_len) { int len strlen(phone); if (len 20) return -1; // 防溢出 char tmp[22] {0}; // 处理号国际格式标记为0x91 if (phone[0] ) { strncpy(tmp, phone1, sizeof(tmp)-1); // 补零至偶数长度 int pad (strlen(tmp) % 2) ? 1 : 0; for (int i strlen(tmp); i sizeof(tmp)-pad-1; i) tmp[i] 0; } else { strncpy(tmp, phone, sizeof(tmp)-1); } // BCD转换每两位取反序 for (int i 0; i strlen(tmp); i 2) { if (i1 strlen(tmp)) { bcd_out[i/2] ((tmp[i1]-0) 4) | (tmp[i]-0); } else { bcd_out[i/2] 0xF0 | (tmp[i]-0); // 单数位补F } } return (strlen(tmp)1)/2; // 返回BCD字节数 }注意0xF0是GSM规范规定的填充符不是随便选的。如果这里填0x00某些老旧Modem会拒绝解析。3.2 目标号码处理同SMSC但无类型字节目标号码dest的处理逻辑与SMSC一致但tp_ra_type字段单独设置0x91表示国际格式BCD数据存入tp_ra_addr数组。区别在于SMSC地址长度字段smsc_len包含类型字节所以0x0A表示10半字节5字节而tp_ra_len只计地址本身0x0B表示11半字节5.5字节→实际存6字节。3.3 时间戳生成UTC时间的7字节压缩tp_scts字段必须是UTC时间格式为YYMMDDHHMMSSTZ其中TZ是时区偏移单位为15分钟。utils.c中utc_time_to_scts()函数强制将本地时间转为UTCvoid utc_time_to_scts(time_t t, uint8_t *scts) { struct tm *tm_utc gmtime(t); // 强制UTC scts[0] (tm_utc-tm_year % 100) % 10 * 16 (tm_utc-tm_year % 100) / 10; // YY scts[1] (tm_utc-tm_mon 1) % 10 * 16 (tm_utc-tm_mon 1) / 10; // MM scts[2] tm_utc-tm_mday % 10 * 16 tm_utc-tm_mday / 10; // DD scts[3] tm_utc-tm_hour % 10 * 16 tm_utc-tm_hour / 10; // HH scts[4] tm_utc-tm_min % 10 * 16 tm_utc-tm_min / 10; // MM scts[5] tm_utc-tm_sec % 10 * 16 tm_utc-tm_sec / 10; // SS scts[6] 0x00; // UTC偏移0x00 00:00 }提示很多项目在这里栽跟头——用localtime()代替gmtime()导致时间戳偏差8小时Modem收到后显示“已过期”。3.4 用户数据编码7bit vs UCS2的抉择pdu_encode.c通过检测输入文本是否全在GSM 03.38字符集内自动选择编码方式int is_gsm7_text(const char *text) { for (int i 0; text[i]; i) { if (text[i] 0x20 || text[i] 0x7E) { // 非ASCII可打印字符 if (text[i] ! 0xA1 text[i] ! 0xA2 text[i] ! 0xA3) // GSM扩展字符 return 0; } } return 1; }若返回1则用7bit编码gsm7_pack()否则强制UCS2utf8_to_ucs2()。UCS2编码后tp_udl是字节数2×字符数7bit编码后tp_udl是7bit块数向上取整(len*76)/8。3.5 PDU包组装字节序与长度字段的精密咬合最终组装时各字段按TS 23.040定义的顺序填入pdu_buf字段长度说明SMSC length1 bytesmsc_len半字节为单位SMSC type1 bytesmsc_type0x91SMSC addrsmsc_len/2bytesBCD编码地址TP-MR1 byte通常0x00TP-RA length1 bytetp_ra_len半字节TP-RA type1 bytetp_ra_typeTP-RA addrtp_ra_len/2bytesBCD目标号码TP-SCTS7 bytesUTC时间戳TP-UDL1 bytetp_udl字节数或7bit块数TP-UDtp_udlbytes编码后内容pdu_encode_submit()用指针偏移精确控制写入位置避免数组越界——这是C语言项目最值得学习的工程习惯。4. 解码逆向工程如何从一串HEX还原出原始短信解码比编码更易出错因为要处理各种异常PDU如SMSC缺失、时间戳非法。pdu_decode_submit()采用防御式解析4.1 HEX字符串预处理容错清洗输入可能是带空格、换行、大小写的HEX字符串如07 91 68 31 ...utils.c的hexstr_to_bytes()先做标准化int hexstr_to_bytes(const char *hex, uint8_t *out, int max_len) { int len 0; for (int i 0; hex[i] len max_len; i) { if (hex[i] || hex[i] \t || hex[i] \n) continue; uint8_t nibble 0; if (hex[i] 0 hex[i] 9) nibble hex[i] - 0; else if (hex[i] A hex[i] F) nibble hex[i] - A 10; else if (hex[i] a hex[i] f) nibble hex[i] - a 10; else return -1; // 非法字符 if (i % 2 0) { out[len] nibble 4; } else { out[len] | nibble; } } return len; }注意该函数返回实际解析字节数而非原字符串长度。若输入0791返回2若输入07 91同样返回2——这对调试AT指令返回的混乱响应极其友好。4.2 字段提取状态机跳过未知字段的安全策略PDU协议允许扩展字段pdu_decode.c不假设固定长度而是根据smsc_len、tp_ra_len等字段动态计算偏移// pdu_decode.c int pdu_decode_submit(const uint8_t *pdu, int pdu_len, pdu_submit_t *out) { int offset 0; // Step 1: SMSC if (pdu_len 1) return PDU_ERR_TRUNCATED; out-smsc_len pdu[offset]; if (out-smsc_len 0) { if (pdu_len offset 1) return PDU_ERR_TRUNCATED; out-smsc_type pdu[offset]; int smsc_bytes (out-smsc_len 1) / 2; // 半字节转字节 if (pdu_len offset smsc_bytes) return PDU_ERR_TRUNCATED; memcpy(out-smsc_addr, pdu offset, smsc_bytes); offset smsc_bytes; } // Step 2: TP-MR... // 后续字段同理每步检查剩余长度 }这种“检查-移动-再检查”的模式让解码器在遇到损坏PDU时不会崩溃而是返回明确错误码。4.3 内容还原7bit/UCS2的智能识别pdu_decode.c不依赖编码标志位TP-DCS字段常被省略而是根据tp_udl和pdu_len推断编码if (tp_udl * 8 (pdu_len - offset) * 7) { // 可能是7bit检查是否能整除 if ((tp_udl * 7) % 8 0) { gsm7_unpack(pdu offset, (tp_udl * 7 6) / 8, out-tp_ud_text, sizeof(out-tp_ud_text)); } else { // 尝试UCS2字节数应为偶数 if (tp_udl % 2 0 tp_udl pdu_len - offset) { ucs2_to_utf8(pdu offset, tp_udl, out-tp_ud_text, sizeof(out-tp_ud_text)); } } }这种启发式判断虽不100%准确但在真实Modem场景中覆盖95%以上情况——毕竟设备厂商也很少严格遵守所有DCS字段。5. 避坑指南那些让我熬夜改了六版才跑通的血泪经验PDU协议看着简单实操全是坑。以下是我在某物联网项目中踩过的5个典型问题每个都附带复现方法和修复方案5.1 现象ATCMGSxxx返回CMS ERROR: 302原因TP-UDL字段值错误。常见于7bit编码时把字符数当字节数填如ABC填3实际应填3但你好填2就错——UCS2下tp_udl必须是4。解决在pdu_encode_submit()末尾加断言assert(tp_udl expected_udl);expected_udl按编码类型分别计算7bit(strlen(text)*76)/8UCS2strlen(text)*2。5.2 现象解码出的手机号末尾多出F或0原因BCD转换时未正确处理奇数位地址。例如138123456710位补零成13812345670012位但倒序BCD应为311832547600若漏掉最后00则得3118325476解码后变138123456少一位。解决bcd_encode()函数中补零逻辑必须保证strlen(tmp)为偶数且循环i2时检查i1 len单数位用0xF0填充。5.3 现象中文短信显示为乱码如ä½ å¥½原因输入文本是UTF-8编码但代码误当Latin-1处理。pdu_encode.c中is_gsm7_text()函数若未正确识别UTF-8多字节序列会错误启用7bit编码。解决在is_gsm7_text()开头添加UTF-8校验if ((c 0xC0) 0x80) continue; // skip UTF-8 continuation并确保text参数是合法UTF-8。5.4 现象时间戳显示为0000-00-00 00:00:00原因gmtime()返回NULL传入负时间戳或time_t溢出。嵌入式平台time_t常为32位2038年后失效。解决增加空指针检查struct tm *tm gmtime(t); if (!tm) { memset(scts, 0, 7); return; }并用time(NULL)替代硬编码时间。5.5 现象编译报错undefined reference to memcpy原因裸机环境未链接libc而utils.c直接调用memcpy。解决在utils.c顶部添加轻量级memcpy实现void *my_memcpy(void *dest, const void *src, size_t n) { uint8_t *d (uint8_t*)dest; const uint8_t *s (const uint8_t*)src; for (size_t i 0; i n; i) d[i] s[i]; return dest; }并在所有memcpy调用处替换为my_memcpy。6. 二次开发实战给PDU demo加上Modem AT指令胶水层这个demo的价值不仅在于编解码更在于它能无缝接入真实硬件。我在某智能电表项目中把它改造为AT指令驱动模块核心改动仅3处6.1 扩展pdu.h定义AT指令交互结构新增at_modem_t结构体封装串口操作和状态// pdu.h 新增 typedef struct { int (*send_cmd)(const char *cmd, char *resp, int resp_size, int timeout_ms); int (*wait_ok)(char *resp, int size, int timeout_ms); void *priv; // 串口句柄等私有数据 } at_modem_t; int pdu_send_over_at(at_modem_t *modem, const char *smsc, const char *dest, const char *text);6.2 实现pdu_send_over_at编解码AT指令流水线// pdu_encode.c 新增 int pdu_send_over_at(at_modem_t *modem, const char *smsc, const char *dest, const char *text) { uint8_t pdu_buf[512]; int pdu_len pdu_encode_submit(smsc, dest, text, pdu_buf, sizeof(pdu_buf)); if (pdu_len 0) return pdu_len; // 步骤1切换PDU模式 char resp[128]; modem-send_cmd(ATCMGF0, resp, sizeof(resp), 1000); modem-wait_ok(resp, sizeof(resp), 1000); // 步骤2发送CMGS指令长度为PDU字节数的2倍因HEX编码 char cmd[64]; snprintf(cmd, sizeof(cmd), ATCMGS%d, pdu_len); modem-send_cmd(cmd, resp, sizeof(resp), 1000); // 步骤3发送PDU HEX字符串带\r结尾 char hex_pdu[1024]; for (int i 0; i pdu_len; i) { sprintf(hex_pdu i*2, %02X, pdu_buf[i]); } strcat(hex_pdu, \r); modem-send_cmd(hex_pdu, resp, sizeof(resp), 5000); // 等待Modem响应 // 步骤4检查响应是否含CMGS:成功或CMS ERROR:失败 if (strstr(resp, CMGS:)) { return 0; // 成功 } else if (strstr(resp, CMS ERROR:)) { return -2; } return -1; }6.3 适配不同串口用函数指针解耦硬件modem-send_cmd可以指向不同实现Linux平台serial_send_cmd()调用write()系统调用STM32平台hal_uart_send_cmd()调用HAL_UART_Transmit()测试环境mock_send_cmd()写入内存缓冲区供单元测试这样业务层代码只需调用pdu_send_over_at(my_modem, ...)完全不关心底层是USB转串口还是RS485。从那以后我每次对接新Modem都先用这个demo生成标准PDU再用逻辑分析仪抓取AT指令波形对比pdu_encode.c输出——只要PDU字节一致问题一定出在串口配置波特率、流控或AT指令时序上。这套方法帮我避开了90%的“Modem不响应”类问题。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑