STM32+Air780E按键发送中文短信:PDU编码与OLED状态显示实战

发布时间:2026/9/28 2:10:26
STM32+Air780E按键发送中文短信:PDU编码与OLED状态显示实战 做过这玩意儿的人都知道市面上绝大多数“STM32发短信”的教程还停留在SIM800L这种2G模块上。可当你真的把设备放到地下室、郊区甚至只是隔着几堵墙的仓库2G信号瞬间教做人。我今年做一套环境监测箱子客户要求异常时直接把告警短信发到手机上屏幕还得实时显示发送状态。一开始我也差点走上SIM800L的老路后来换成合宙Air780E这颗Cat.1 4G模块用STM32F103C8T6驱动加一块0.96寸OLED按键触发终于把“STM32Air780E实现按键发送中文短信OLED状态显示”这套方案稳定跑通。这篇文章就是把我在这个项目里从选型、接线、PDU编码、状态机到AT指令时序的完整思路写出来尤其是中文短信编码这个最容易翻车的地方我会掰开揉碎讲清楚给后面要做类似告警设备的朋友一份能直接抄作业的参考。1. 从项目需求到方案定型Air780E在这里解决了什么问题1.1 为什么短信模块非用Cat.1不可如果你只是在学校实验室里做个demoSIM800L确实便宜二十来块钱串口透传AT指令发短信网上教程一抓一大把。但一旦放到实际项目里2G模块的问题就非常现实国内2G网络正在加速退网基站越来越少基站信号覆盖质量也在下降。我实测过同一位置SIM800L的信号格数只有一格而Air780E能稳定注册上4G网络CSQ值在20以上。再加上2G网络对短信中心号码的兼容问题经常出现“ATCMGS发送成功但对方收不到”这种玄学故障排查成本极高。Air780E是合宙出的Cat.1模块所谓Cat.1就是LTE网络里专门为物联网场景设计的低功耗、低成本类别带宽不大但足够跑短信、MQTT、TCP这类小流量业务。它和SIM800L一样走串口AT指令移植成本低但网络可靠性完全不在一个级别。而且Air780E的固件里对PDU短信的支持比较完整中文UCS2编码发出去基本不会出现乱码。对我来说最舒服的一点是它跟STM32之间就是标准的UART通信不需要折腾USB、不需要跑协议栈一个串口就能搞定。1.2 系统架构一块STM32、一颗4G模块、一块OLED这套系统整体架构不复杂核心就三个角色主控STM32F103C8T6负责按键检测、PDU报文构造、串口收发、OLED状态刷新。通信Air780E通过串口接收STM32下发的AT指令和数据完成短信发送。显示0.96寸SSD1306 OLED屏I2C接口显示当前状态待机、发送中、成功、失败。为什么用STM32F103而不是ESP32或者直接让Air780E自己跑脚本因为Air780E虽然是模组但它同时支持Lua开发理论上可以不用STM32直接发短信。不过我这里还要接一堆传感器、做本地逻辑判断后续还要扩展按键、蜂鸣器之类的交互主控放在STM32上更符合我整个项目的设计习惯。STM32F103的价格、资料、生态都太成熟了HAL库或者标准库随便写上手门槛低。1.3 硬件接线表与串口分配我使用的串口分配是这样的模块/器件STM32引脚说明Air780E TXDPA10 (USART1_RX)模块发送STM32接收Air780E RXDPA9 (USART1_TX)STM32发送模块接收Air780E GNDGND共地OLED SDAPB7软件I2C数据线OLED SCLPB6软件I2C时钟线按键KEY1PA1按下触发发送内部上拉低电平有效OLED VCC/GND3.3V/GNDSSD1306供电这里有个细节Air780E的串口电平是1.8V逻辑但实际和STM32的3.3V串口直接相连时很多项目也都能正常工作因为模块内部有电平兼容设计。但我个人建议稳妥起见RXD线上串一个1k电阻做限流TXD直接接STM32的RX没太大问题。我这次是直接对接跑通的没加电平转换芯片你的板子如果对稳定性要求极高可以加一个TXS0108E之类的电平转换。OLED我选了软件I2C没有用STM32的硬件I2C。原因很实在STM32F1系列硬件I2C的兼容性处理在一些老库版本里表现一般软件I2C反而稳定而且OLED刷新频率不高软件模拟完全够用。初始化SSD1306之后先清屏显示一版“SYSTEM READY”确认屏幕和I2C时序没问题再往下走。2. 电源和上电时序最容易埋雷的两处细节很多新手在做这类项目时第一反应是“找一根USB线直接供电”。USB线能带动STM32、OLED但Air780E这种4G Cat.1模块在发射时电流非常大。Air780E的规格书里写着峰值电流可以到0.5A以上如果在信号弱的地方模块会自动加大发射功率瞬时电流更高。我实测过用普通电脑USB口供电模块一发起网络注册或者发短信电压瞬间被拉低模块直接掉电重启表现就是串口一会儿通一会儿不通OLED花屏按键按下去毫无反应。2.1 4G模块的瞬时电流远比想象中大正确的供电方式是外接5V/2A的电源适配器经过一颗低压差稳压器或者DC-DC降压到4V左右单独给Air780E的VBAT供电。STM32和OLED则走3.3V那一路两块电物理上分开不要共用一条细长的杜邦线。模块VBAT引脚旁边要加大电容我放了一个470uF电解电容并联一个100nF陶瓷电容这个组合对付GSM/LTE的突发电流很有效。如果你板子空间允许最好按合宙硬件手册推荐的电路来画VBAT入口放一个大电容再加一颗TVS管防浪涌。开发期用面包板飞线的朋友至少也要保证供电线尽量短、尽量粗不要用那种又细又长的杜邦线否则模块一发射线损压降就够你喝一壶的。2.2 上电等待与网络注册判断Air780E上电之后并不是立刻就能发短信它内部要完成开机自检、SIM卡读取、网络搜索、小区注册这一整套流程。我这个项目里STM32上电后先等2秒再往串口发“AT\r\n”看模块有没有回“OK”。如果没回就再等1秒再发最多重试5次。等模块能回AT之后还要发“ATCPIN?”查SIM卡状态直到返回“CPIN: READY”才能进入待机状态。OLED上则显示“SIM READY”或者“CSQ: xx”让我知道模块已经就绪。这里有个经验千万不要在模块还没注册网络时就去发“ATCMGS”大概率返回ERROR。即使模块回了OK也不代表网络侧的短信中心已经准备好接收保险的做法是发完“ATCSQ”确认信号值再发“ATCEREG?”确认注册状态为0或10未注册1已注册。2.3 OLED驱动初始化与显示自检OLED我在上电后先做一次完整初始化关显示、设置时钟分频、设置多路复用、设置显示偏移、开启显示。SSD1306的初始化序列网上到处都是我用的是自己精简过的一版只保留必要的命令实测稳定。初始化完清屏显示“AIR780E TEST”两秒再显示当前CSQ值。如果你用的OLED是四针I2C版本注意焊接和插线很容易因为接触不良导致地址读不到。我调试时遇到过屏幕亮一半、显示乱码的情况排查到最后是SDA线接触不良重新插紧后一切正常。项目里建议在初始化后做一次“清屏 显示固定字符”的自检如果屏没起来哪怕后面代码写得再好你也看不见状态。3. 中文短信的核心PDU编码原理与STM32实现这部分是整个项目里最有技术含量、也是网上教程说得最含糊的地方。很多人卡在“ATCMGS”发中文短信这一步发出去的全是乱码或者直接报错。问题基本都出在PDU编码上。3.1 为什么“直接发中文”发不出去SMS短信有两种编码模式Text模式ATCMGF1和PDU模式ATCMGF0。Text模式下你直接通过串口发一串ASCII字符模块就按默认编码帮忙封装成短信。这种模式处理英文数字非常方便但中文不行因为短信的默认字符集是GSM 7-bit里面根本没有汉字的位置。要发中文必须走PDU模式而PDU报文里承载中文文本的方式是UCS2编码也就是把每个汉字转成两个字节的Unicode码然后用十六进制字符串形式拼进PDU报文。比如“温”的Unicode是0x6E29“度”是0x5EA6“温度”的UCS2十六进制就是“6E295EA6”。所以你要做的第一件事就是把想发的短信内容从中文转成UCS2十六进制串。这就是整个项目最核心的编码逻辑。3.2 手工拆解一条完整PDU短信我们直接拆一条真实的PDU报文。假设短信中心号码SMSC8613800210500收件人手机号13812345678短信内容“温度告警”首先看SMSC部分。短信中心的号码不是直接写ASCII而是用BCD码颠倒字节序后表示。8613800210500去掉“”后是“8613800210500”一共13位是奇数末尾补一个F凑成14位“8613800210500F”。然后按两位一组颠倒顺序86 - 6813 - 3180 - 0802 - 2010 - 0150 - 050F - F0。于是得到“683108200105F0”。前面再加一个字节表示SMSC信息长度这段BCD数据是7个字节加上前面的地址类型0x91总共8个字节所以长度填0x08地址类型用0x91表示国际格式号码里有86。最终SMSC部分是“0891683108200105F0”。如果你所在地区短信中心号码不含区号类型就填0x81。然后是TPDU部分。我构造的是一个SMS-SUBMIT报文字段取值解释MTI11SMS-SUBMIT不要求回执无有效期字段MR00消息参考号随便填DA长度0B收件人号码共11位DA类型81国内号码格式DA编码3118325476F813812345678按BCD颠倒13-3181-1823-3245-5467-768F-F8PID00普通短信DCS08关键表示UCS2编码中文短信必须用这个UDL08消息内容共8个字节4个汉字 x 2字节UD6E295EA6544A8B66“温度告警”的UCS2十六进制把这串连起来完整的PDU报文就是0891683108200105F011000B813118325476F80008046E295EA6544A8B66等一下这里UD我写的是“6E295EA6544A8B66”核对一下温6E29度5EA6告544A警8B66。拼起来就是“6E295EA6544A8B66”。UDL是8字节所以ATCMGS后面填的字节数是怎么算的是TPDU的总长度从MTI那个“11”开始往后数11、00、0B、81、31、18、32、54、76、F8、00、08、08、6E、29、5E、A6、54、4A、8B、66一共21个字节。等等我重新数一下上面写UDL08那UD内容就是6E 29 5E A6 54 4A 8B 66八个字节MTI(1)MR(1)DA长度(1)DA类型(1)DA号码(6)PID(1)DCS(1)UDL(1)UD(8)21。所以ATCMGS21。之前网上很多文章把SMSC的长度也算进去那是错的CMGS后面的数字是TPDU的字节数不含SMSC部分。这条PDU对应的AT命令流程是ATCMGF0 ATCMGS21 0891683108200105F011000B813118325476F80008046E295EA6544A8B660x1A模块返回“”提示符后把PDU字符串发过去最后发0x1A十六进制的CtrlZ表示结束。如果中途想取消发0x1B。3.3 STM32端编码实现方案固定内容与查表STM32本身并不直接支持把GB2312/GBK字符串转成UCS2因为单片机里存的字符串字面量在Keil里通常就是GBK编码。我这里有三种方案按复杂度排序方案一短信内容固定时直接在PC上先把内容转成UCS2十六进制串存到代码里当常量。比如“温度告警”就存成char pdu[] 0891683108200105F011000B813118325476F80008046E295EA6544A8B66。简单粗暴适合内容固定、不需要动态组装的场景。方案二内容包含动态数据比如温度值时做一张“常用汉字GBK到Unicode”的映射表在STM32里查表转换。这个表比较大一个汉字对应两个Unicode字节常用汉字六七百个就能覆盖大多数告警文案。查表逻辑就是从字符串里读出GBK双字节算出索引去查UCS2码然后拼进PDU。方案三如果动态拼接的字符都是ASCII数字、字母、符号中文字段固定那可以只把中文字段用心跳常量ASCII字符单独转成Unicode再加前缀00。比如“温度25.3度告警”就处理成“6E29 5EA6 0025 002E 0033 5EA6 544A 8B66”。UCS2里ASCII字符的高字节是00所以字母和数字直接用ASCII码加上“00”前缀就行这个方案实现起来很轻松。我这里实际项目用的是方案三因为告警文案模板固定变化的只有数值所以PDU构造函数只需要处理数字和字母转UCS2不用带全量汉字表。核心构造代码长这样只贴关键逻辑void BuildPduString(char *pdu, uint8_t *phone, uint16_t temp) { uint8_t i; uint8_t len 0; // 1. SMSC: 固定为0891683108200105F0这里写成宏 strcat(pdu, SMSC_STR); // 2. TPDU固定头 strcat(pdu, 11000B813118325476F80008); // 3. 计算UDL温度数值如果按“xx.x”格式是5个字符加上模板里的汉字 // 温度内容为 6E295EA6 xx.x的UCS2 544A8B66 // 具体字节数动态算 // 4. 温度数值转UCS2 char tempUcs2[8]; snprintf(tempUcs2, sizeof(tempUcs2), 00%02X00%02X, temp 8, temp 0xFF); strcat(pdu, tempUcs2); }这个函数里有个关键点ATCMGS后面的长度是根据UDL动态算出来的不能写死。我建议单独写一个GetPduTpduLen()函数数一下实际拼出来的TPDU十六进制字符串长度除以2就是CMGS参数。3.4 几个最容易编码错误的点SCA、DA、UDL编码错误是中文短信发不出去的最大来源我踩过的坑有四个一是短信中心号补F漏了。奇数长度的号码不补FBCD编码就全错位。判断规则号码长度是奇数末尾补一个F是偶数不用补。二是DA长度算错。DA长度是收件人号码的位数不是BAI编码后的字节数。国内手机号11位填0B如果号码带86长度是13位填0D同时DA类型要改成0x91。三是UDL算错。UDL是UCS2字节数不是汉字个数。4个汉字是8写成04就等着收乱码吧。四是ATCMGS参数混入SMSC。CMGS参数是TPDU字节数SMSC那一段不算。我见过不少人的代码把整条PDU字符串长度除以2直接填进去模块有时候也能发出去但部分固件会把SMSC再套进地址域里导致短信中心解析出错。所以最稳的还是按TPDU算。如果你调试时收到的返回是ERROR先把这几项全部检查一遍八成能修好。4. 按键触发与OLED状态机让发送过程可视化硬件和编码搞定了接下来是软件框架。这个项目里按键、OLED、串口三者要配合好不能互相卡死。4.1 按键检测中断标志位主循环处理按键我挂在PA1上内部上拉低电平有效。用外部中断EXTI1检测下降沿中断里只做一件事置位send_request标志位具体发送逻辑全部放主循环。这是嵌入式里非常经典的“中断置标志、主循环干活”模式。为什么不让中断里直接发AT指令因为ATCMGS的流程是发命令、等提示符、再发PDU、等回复中间是串口收发和延时整体耗时要好几秒。在中断里干这种事轻则阻塞其他任务重则栈溢出。按键抖动会触发多次中断所以主循环发现标志位后先延时20ms再读一次引脚电平确认确实按下再清标志位并进入发送流程。4.2 四态状态机设计OLED显示的状态我用一个枚举表示typedef enum { ST_IDLE, // 待机 ST_SENDING, // 正在发送 ST_SUCCESS, // 发送成功 ST_FAIL // 发送失败 } SendState_t;主循环每隔100ms刷新一次OLED对应状态的画面。待机时显示“SYSTEM READY”和CSQ值按键按下后切到ST_SENDING显示“SENDING...”发送完成根据串口返回结果置ST_SUCCESS或ST_FAILOLED显示“TX OK”或“TX ERR”持续3秒后自动回到ST_IDLE。状态机最大的作用是隔离了“什么时候做什么事”和“现在屏幕上显示什么”的联系。即使发送流程里加了重试逻辑OLED也只是跟着状态走不会因为AT指令的时序而卡画面。这里有个小技巧ST_SUCCESS/ST_FAIL停留的3秒计数不要用HAL_Delay()硬等否则按键会失灵。我用一个tick变量主循环每次累计到300次每次10ms才切回ST_IDLE用户期间按按键也不会被忽略因为按键标志位是在主循环开头就检查的。4.3 OLED刷新与串口发送的协作方式OLED的I2C刷新如果频率太高会占用大量CPU时间影响串口的接收。我用的是软件I2C一次全屏刷新大概几毫秒但主循环里我限制每100ms才刷新一次状态区而不是每轮循环都刷。这样串口中断里接收AT返回的数据不会丢。发送PDU流程我用了一个简单的阻塞状态机状态A发“ATCMGS21\r\n”等待模块返回“”状态B收到“”后发PDU字符串再发0x1A状态C等待“CMGS: xx”或“OK”判断成功等待“ERROR”判断失败这个等待过程不能无限等我设了5秒超时超时算失败。为什么用串口中断接收而不是查询方式因为AT返回的数据可能在任意时间过来查询方式容易在发送PDU字符串的过程中漏掉“”提示符。我的串口接收是中断环形缓冲区主循环轮询缓冲区里有没有目标字符串。5. AT指令时序从模块初始化到短信发送PDU编码正确只解决了一半问题另一半是AT指令的执行时序。Air780E的AT指令规范跟传统GSM模块大体兼容但有几个细节不同我专门把跑通的指令序列列出来。5.1 初始化AT流程查卡、查信号、注册网络上电后的初始化顺序是AT ATE0 ATCPIN? ATCSQ ATCEREG?第一条“AT”是握手确认模块串口通信正常。返回“OK”后关掉回显减少后面解析返回数据时碰到无关字符的麻烦。“ATCPIN?”返回“CPIN: READY”表示SIM卡识别正常如果这里返回“CME ERROR: SIM not inserted”检查卡槽接触和SIM卡方向。“ATCSQ”返回类似“CSQ: 20,99”第一个数字是信号值范围0~31越高越好。我这个项目判断标准是第一次查询小于8时再查一次连续两次小于8就OLED显示“SIGNAL LOW”但不会阻止发送因为极端情况下短信还是能发出去的。“ATCEREG?”返回“CEREG: 0,1”第二个参数为1表示已注册到网络。这个状态有时候上电后要等几秒甚至十几秒所以我的主循环里会轮询这个指令直到注册成功才把状态切到ST_IDLE。5.2 发送PDU的完整指令序列发送短信时的完整交互如下我用CR表示回车0x1A表示CtrlZTX: ATCMGF0CR RX: OK TX: ATCMGS21CR RX: TX: 0891683108200105F011000B813118325476F80008046E295EA6544A8B660x1A RX: CMGS: 146 RX: OK“ATCMGF0”设置PDU模式。Air780E的固件默认可能已经在PDU模式但显式设置一遍更保险因为有些模块重新上电后会恢复默认Text模式。“ATCMGS21”后面的数字是TPDU字节数我前面已经算过21就是“温度告警”那条短信的正确长度。模块回“”之后必须一次性把PDU字符串发送完结尾紧跟0x1A不要在PDU字符串和0x1A之间加回车。如果你加回车模块会把0x0D当作PDU的一部分解析然后报错。这里踩过一次我在PDU字符串后面习惯性加了“\r\n”结果模块回ERROR排查了半天才意识到ATCMGS的输入模式特殊除了最终0x1A中间不能有别的控制字符。成功返回“CMGS: xxx”和“OK”这个xxx是消息参考号可以忽略。如果返回“ERROR”通常问题在PDU内容本身或者模块网络侧拒绝前者优先检查编码。5.3 返回码解读、超时与重试策略我总结的常见返回码含义返回内容含义处理动作OK上一条指令成功继续下一步CMGS: 数字短信已提交成功置ST_SUCCESSERROR指令失败或PDU不合法检查PDU编码置ST_FAILCME ERROR: xx模块内部错误如SIM未就绪重新走初始化流程无响应模块死机或供电异常复位模块重试策略上我只对网络侧原因造成的失败做重试比如“CME ERROR: 302”网络拒绝。如果是因为PDU编码错误造成的ERROR重试一万次也是白搭。所以我在发送失败后会把OLED切到FAIL状态并且我有一段临时的调试逻辑把要发送的PDU串通过USB虚拟串口打到PC上人工检查一遍长度和十六进制内容。这里分享一个排查技巧如果你不确定模块到底收到的是什么把“ATE0”先不要关保持回显看模块回显的内容是否和你发送的一致。回显中一旦出现乱码或多出来的字符说明串口波特率或者接线有问题而不是编码问题。6. 实测中绕不开的三个坑和排查思路最后这部分我把实际调试验证过程中遇到的三个典型问题完整复盘一遍。这些都是“教科书里不会写但只要你做实物就一定会碰上”的坑。6.1 OLED花屏电源纹波和I2C时序双重影响现象按键按下后OLED屏幕偶尔显示雪花有时候甚至整个屏幕变暗要复位才能恢复。第一次碰到花屏我以为是SSD1306的初始化命令写错了反复核对初始化序列没问题。后来用手指按了一下屏幕边缘的电源线发现花屏概率明显变大这才意识到是电源问题。Air780E一发射整个3.3V母线被拉低OLED的VCC出现纹波SSD1306内部状态机紊乱于是开始花屏。解决思路分两层第一给OLED的VCC和GND之间加一颗100nF和一颗10uF电容过滤高频和低频纹波第二在软件上做异常恢复SSD1306有一个“63h, 0x00”命令可以关闭内部升压电路但更实用的是屏幕花屏后重新初始化一遍。我在主循环里加了一个屏幕自检计数如果超过500ms没有状态刷新就视为显示异常重新走一遍SSD1306初始化并清屏。另外软件I2C的时序也要检查。如果你把I2C的SCL频率推得太高SSD1306偶尔也会显示错乱。我的软件I2C延时控制在2~5us实测稳定。6.2 发送返回ERROR短信中心编码错在哪现象ATCMGS发出去了“”也等到了PDU字符串也发了但模块返回ERROR并且OLED稳定显示发送失败。这个坑几乎每个做中文短信的人都会踩。我最开始用网上某个教程里的短信中心编码直接套“0891683108200105F0”。但问题是不同运营商的短信中心号码不一样不同地区的移动、联通、电信号码也完全不同。我现在用的短信中心编码是上海移动的如果你在广东直接套用就发不出去。解决办法是查你自己的短信中心号码。普通手机拨号界面输入*#*#4636#*#*或者询问运营商客服拿到短信中心号码后用前面讲的BCD编码规则重新算一遍“0891”开头的SMSC字段。要注意有些地区短信中心号码是8613800xxx500的格式但个别省市前面不是86开头这时地址类型就要从0x91改成0x81。还有一个很容易被忽略的细节如果你的短信中心号码不止13位比如带了一些特殊长号码SCA长度也要跟着变。SCA长度是“0x91 号码BCD字节数”不是固定8。6.3 模块随机重启供电不足的经典症状现象模块能正常初始化能发一两条短信但发到第三条的时候串口突然失去响应OLED状态卡在SENDING过几秒又恢复然后显示的不是成功而是系统重新点亮的画面。这个现象最先让我怀疑是程序死循环后来发现是Air780E自己重启了。罪魁祸首还是供电。模块长时间满载发射后瞬时电流把电源电压拉低到模块最低工作电压以下触发了欠压保护模块自动关机再启动。我在电源上做了两个改进后彻底解决把USB供电换成了独立5V/2A适配器VBAT引脚上的电容从100uF加到470uF并在PCB走线上把模块电源路径加宽。如果你在面包板上调试别偷懒直接用面包板电源轨面包板内部铜皮电阻太大压降很吓人。我后来是单独焊了一块小转接板用粗短线把电源引到模块。还有一点Air780E的VBAT推荐电压是3.8V左右不要直接拿5V怼上去模块虽然内部有电源管理但长期超压会影响寿命。我用的是一颗RT9013 LDO降到3.8V效果稳定。开发阶段如果你手头只有AMS1117-3.3勉强能用但模块发射时压降会偏大可能出现间歇性重启。除了电源还要检查模块的复位引脚。Air780E的RESET引脚如果悬空在某些电气噪声环境下会被误触发。我在量产板上把RESET引脚通过10k电阻上拉到高电平并用100nF电容接地滤波再没出现过随机重启。最后再补充一个跟OLED状态显示相关的实用经验把STM32的调试串口USB虚拟串口那路和Air780E的AT串口分开。我一开始图省事把打印日志和AT指令混在同一个串口输出结果日志一打PDU字符串就被冲断了。后来调试串口用PA2/PA3AT串口用PA9/PA10两边互不干扰排查问题效率高了很多。这是做这个项目时体会最深的一点——硬件上的“隔离”看似多占资源却能让软件调试的复杂度直线下降。如果你也在搞类似的项目先把电源、串口分配、PDU编码这三关过了基本就成功一大半了。