ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32+Air780E+OLED短信发送实战:PDU编码与AT状态机

2026/10/2 17:29:16 拓冰建站 浏览量
STM32+Air780E+OLED短信发送实战:PDU编码与AT状态机 1. 从按键触发到短信落地这条链路到底难在哪很多人第一次做STM32加4G模块发短信的项目脑子里想的是一条直线按键按下、MCU读IO、串口发AT指令、短信出去。真动手才发现这条链路上埋着好几个坑而且每个坑都能让你卡上大半天。我前后做过三四个类似的项目从最早的SIM800C到后来的Air780E每次都会在某个环节上重新踩一遍。这篇就把STM32F103加Air780E、配一块0.96寸OLED做状态显示的完整方案拆开讲重点放在中文短信的PDU编码、AT指令的时序控制、以及OLED状态机怎么设计才不卡主循环。先说清楚这个项目适合谁看。如果你已经会用STM32的GPIO和UART能点亮OLED但对4G模块的AT指令流程不熟或者发英文短信没问题、一发中文就乱码那这篇基本能覆盖你的需求。如果你连串口中断都没写过建议先把UART收发跑通再回来。整个方案的核心关键词就几个STM32、Air780E、OLED、AT指令、PDU。其中PDU是中文短信的关键也是最多人栽跟头的地方。为什么选Air780E而不是更常见的SIM800C或者EC20系列Air780E是Cat.1模块支持VoLTE功耗和体积都比传统2G模块友好而且现在2G网络在很多地方已经退网了继续用SIM800C属于给自己找麻烦。Air780E走的是标准AT指令集和SIM800C有大量重合但细节上有差异尤其是短信相关的指令返回格式和PDU长度处理。OLED这边我用的0.96寸SSD1306I2C接口主要用来显示待机发送中发送成功发送失败这几个状态以及信号强度和运营商信息。整条链路的难点我总结成三个第一按键消抖和触发逻辑要稳不能按一下发三条第二AT指令的发送和等待回复必须带超时和重试不能死等第三中文短信必须走PDU模式UCS2编码长度计算和分段都要自己处理。下面按实际开发顺序展开。2. 硬件连接与供电Air780E不是随便接个3.3V就能跑2.1 引脚分配与串口选择先列一下我实际用的连接方式主控是STM32F103C8T6OLED用I2C1Air780E用USART2。具体分配如下功能STM32引脚外设备注OLED SCLPB6I2C1_SCL4.7k上拉OLED SDAPB7I2C1_SDA4.7k上拉Air780E TXPA3USART2_RX模块TX接MCU RXAir780E RXPA2USART2_TX模块RX接MCU TX按键PA0GPIO输入下拉按下为高模块PWRKEYPB0GPIO输出控制开机模块RESETPB1GPIO输出备用这里有个容易忽略的点Air780E的PWRKEY不是给个高电平就开机而是需要拉低一段时间再释放。根据模块手册典型操作是拉低至少500ms然后释放模块才会启动。我一开始直接给高电平模块毫无反应查了半天以为是串口问题其实是开机时序没对。2.2 供电才是最大的坑Air780E在发射瞬间的电流峰值可以到2A左右虽然平均电流不大但那个瞬时峰值如果电源扛不住模块会直接复位或者注册不上网络。我最初用STM32板子上的3.3V LDO给模块供电结果模块能开机、能识别SIM卡但一注册网络就掉线串口返回一堆乱码。后来换成独立的DC-DC降压模块输入5V输出3.8V电流能力3A以上问题立刻消失。注意Air780E的供电电压典型值是3.8V不是3.3V。虽然3.3V也能工作但射频性能会打折扣信号弱的地方容易注册失败。建议单独一路3.8V供电并且在模块电源脚旁边并一个1000uF以上的电解电容加一个0.1uF陶瓷电容用来吸收发射瞬间的电流冲击。另外模块的GND必须和STM32共地这个不用多说但实际接线时因为用了独立电源很容易忘记把地连在一起。我就干过这事串口死活收不到数据最后发现是地没共。2.3 OLED的I2C上拉电阻0.96寸SSD1306模块大多数自带4.7k上拉电阻但有些廉价模块省掉了。如果你发现OLED时亮时不亮或者I2C扫描不到地址先拿万用表量一下SCL和SDA对VCC的阻值。没有上拉的话在STM32这边补两个4.7k电阻到3.3V。I2C速率我设在400kHz实测这个速率下SSD1306刷新一屏大概十几毫秒足够用了。如果线比较长降到100kHz更稳。3. AT指令交互框架别在主循环里死等3.1 为什么不能用阻塞式发送新手最容易写出的代码是这样的按键按下调用一个SendSMS函数里面依次发送AT、等待OK、发送ATCMGF、等待OK、发送ATCMGS、等待、发送PDU数据、等待OK。整个过程用HAL_Delay死等。这样写功能能跑通但问题是一旦某个环节模块没回复整个程序就卡死了OLED也不刷新按键也没反应。我的做法是把AT交互做成一个状态机放在主循环里轮询配合串口中断接收。具体来说定义一个发送缓冲区和接收缓冲区接收用中断或者DMA每收到一个字节就检查是否包含预期的结束标志比如OK\r\n或者ERROR\r\n。主循环里维护一个状态变量根据当前状态决定下一步发什么指令。3.2 状态机的状态划分我把整个发短信流程拆成这几个状态IDLE空闲等待按键触发CHECK_MODULE发送AT确认模块在线SET_TEXT_MODE发送ATCMGF0设置为PDU模式SET_CHARSET发送ATCSCSUCS2设置字符集SEND_CMGS发送ATCMGS长度等待提示SEND_PDU发送PDU数据以CtrlZ0x1A结尾WAIT_RESULT等待CMGS返回或者ERRORDONE更新OLED状态回到IDLE每个状态都有一个超时计数器比如CHECK_MODULE状态等3秒没收到OK就重试重试3次失败就跳到错误处理OLED显示模块无响应。这样即使模块抽风程序也不会卡死。3.3 串口接收的缓冲策略接收缓冲区我开了512字节因为PDU模式下模块返回的内容可能比较长尤其是收到短信或者查询信息的时候。接收中断里做两件事把字节存入环形缓冲区同时检查是否匹配到结束标志。匹配用简单的字符串查找就行不需要多复杂。这里有个细节Air780E返回的结束标志有时候是\r\nOK\r\n有时候是\r\nCMGS: 12\r\n\r\nOK\r\n。我的处理方式是只要检测到OK\r\n或者ERROR\r\n就认为一个完整响应结束把缓冲区内容交给状态机解析。解析的时候用strstr找关键字比如找CMGS确认短信发送成功找ERROR确认失败。// 简化的状态机轮询伪代码 void AT_Process(void) { switch(at_state) { case IDLE: if (key_pressed) { at_state CHECK_MODULE; at_timeout HAL_GetTick(); UART_SendString(AT\r\n); } break; case CHECK_MODULE: if (strstr(rx_buffer, OK)) { at_state SET_TEXT_MODE; UART_SendString(ATCMGF0\r\n); ClearRxBuffer(); } else if (HAL_GetTick() - at_timeout 3000) { retry_count; if (retry_count 3) at_state ERROR_HANDLE; else UART_SendString(AT\r\n); } break; // ... 后续状态类似 } }这个框架跑起来之后OLED刷新和按键扫描完全不受影响因为AT交互只是主循环里的一个分支每次执行很快返回。4. 中文短信的PDU编码为什么你的中文发出去是乱码4.1 PDU模式与Text模式的本质区别短信发送有两种模式Text模式和PDU模式。Text模式看起来简单直接发字符串就行但它对中文的支持极差因为Text模式默认走GSM 7-bit编码中文根本不在这个字符集里。你发你好出去对方收到的可能是一堆问号或者乱码。PDU模式则是把整个短信内容按照协议打包成十六进制字符串支持UCS2编码中文就是两个字节一个字符稳定可靠。所以结论很明确发中文必须用PDU模式。ATCMGF0就是切到PDU模式ATCMGF1是Text模式。很多人卡在中文乱码上就是因为用了Text模式。4.2 PDU串的组成结构一条完整的PDU串由这几部分组成短信中心号码长度、短信中心号码、PDU类型、目标号码长度、目标号码、协议标识、数据编码方案、有效期、用户数据长度、用户数据。听起来复杂但实际编码的时候大部分字段是固定的。以发送你好到手机号13800138000为例短信中心号码假设是8613800100500。我来拆解一下短信中心号码08 91 68 31 08 00 10 05 00。08是长度91表示国际格式后面是号码的BCD反转。PDU类型11表示发送短信。目标号码0D 91 68 31 08 00 10 38 00。0D是长度13位91国际格式号码BCD反转。协议标识00。数据编码08表示UCS2编码。有效期00表示默认。用户数据长度04因为你好的UCS2编码是4个字节。用户数据4F60 597D这是你和好的Unicode码。把这些拼起来就是完整的PDU串。实际发送时ATCMGS后面的长度参数是PDU串去掉短信中心号码之后的长度以字节为单位但要注意是十六进制字符串长度的一半。4.3 号码BCD反转的坑目标号码和短信中心号码都需要做BCD反转。什么叫BCD反转比如号码13800138000先补成偶数位13800138000已经是11位前面补0变成013800138000然后两两交换01 38 00 13 80 00变成10 83 00 31 08 00。最后前面加上长度和格式标识。这个反转逻辑写错的话短信要么发不出去要么发到错误的号码。我建议单独写一个函数处理并且用已知号码测试验证。// 号码BCD反转示例 void PhoneToBCD(char *phone, char *out) { int len strlen(phone); int i; // 补F使长度为偶数 if (len % 2 ! 0) { phone[len] F; phone[len1] \0; len; } for (i 0; i len; i 2) { out[i] phone[i1]; out[i1] phone[i]; } out[len] \0; }4.4 UCS2编码的转换中文转UCS2其实就是把每个汉字的Unicode码用十六进制表示。STM32上可以用一个简单的查表或者直接取GB2312转Unicode但更省事的办法是预先在PC上把要发送的中文转成UCS2字符串直接存在代码里。如果中文内容是固定的几条比如报警正常测试那就提前转好省去在MCU上做编码转换的麻烦。如果中文是动态的比如从传感器读数值拼字符串那就需要实现一个GB2312到UCS2的转换表。这个表比较大STM32F103的Flash够放但代码量会增加。我的建议是尽量把中文内容固定化动态部分用数字和英文代替。提示Air780E支持ATCSMP设置短信参数其中ATCSMP17,167,0,8中的最后一个参数8就是UCS2编码。设置好这个之后PDU串里的数据编码方案也要对应写08两边要一致。5. OLED状态显示的设计别让刷新拖慢主循环5.1 状态信息的取舍OLED只有128x64像素能显示的内容有限。我最终确定显示这几行第一行显示模块状态INITREADYSENDINGOKFAIL第二行显示信号强度CSQ值第三行显示运营商名称或者短信发送计数。刷新频率不用太高200ms一次足够了因为状态变化本身就不频繁。这里有个设计决策OLED刷新用全屏刷新还是局部刷新SSD1306支持页地址和列地址设置可以只刷新变化的部分。但实现起来麻烦而且200ms全屏刷新对主循环的影响很小I2C 400kHz下全屏1024字节大概20ms所以我直接用全屏刷新代码简单可靠。5.2 状态更新的时机状态更新不能放在AT状态机里面直接调用OLED刷新因为那样会增加状态机的执行时间。我的做法是状态机只更新一个全局变量oled_state主循环里单独有一个OLED刷新任务检查oled_state是否变化变化了才刷新。这样状态机和显示解耦逻辑清晰。// 状态机里只改标志 oled_state STATE_SENDING; // 主循环里刷新 if (oled_state ! last_oled_state) { OLED_ShowString(0, 0, state_str[oled_state]); last_oled_state oled_state; }5.3 信号强度查询Air780E用ATCSQ查询信号强度返回格式是CSQ: 20,99这样的。第一个数字是信号强度范围0到31越大越好99表示未知。我把它映射成百分比显示在OLED上20大概对应65%左右。这个查询不用太频繁30秒一次就行放在主循环的定时任务里。6. 按键触发逻辑一次按下只发一条6.1 硬件消抖与软件消抖按键我用的是最普通的轻触开关按下时PA0读到高电平。硬件上并了一个0.1uF电容做简单消抖但软件消抖还是必须的。我的做法是检测到高电平后延时20ms再检测一次仍然是高电平才认为是有效按下。释放也要检测确保一次按下只触发一次。6.2 发送中的按键屏蔽短信发送过程可能持续几秒到十几秒取决于网络这期间如果用户又按了按键不能重复触发。我在状态机里加了一个判断只有IDLE状态下按键才有效其他状态忽略按键。同时OLED显示SENDING用户能看到正在发送中。6.3 长按与短按的区分如果项目需要还可以区分长按和短按。比如短按发送预设短信长按进入配置模式。实现方式是记录按下持续时间释放时判断。这个不是必须的但加上之后交互更丰富。我一般会留一个长按3秒重启模块的功能方便现场调试。7. 实测中遇到的几个典型问题与排查过程7.1 模块开机后串口无响应第一次调试时给模块上电、拉PWRKEY然后发AT串口毫无反应。排查过程先确认串口线TX/RX没接反用示波器看STM32的TX脚有没有波形有波形说明MCU在发。然后量模块的VCC发现只有3.3V而且用的是LDO电流能力不足。换成3.8V/3A的DC-DC后模块正常启动串口返回RDY。7.2 中文短信发送成功但对方收到乱码这个问题的根源是ATCSMP的参数没设对。我一开始只设了ATCMGF0没有设ATCSMP模块用了默认的编码方案导致UCS2数据被当成7-bit解析。加上ATCSMP17,167,0,8之后中文正常显示。另外要注意ATCSCSUCS2也要设否则模块可能对PDU串里的字符集理解有偏差。7.3 短信发送返回ERRORERROR的原因有很多常见的有PDU串长度计算错误、目标号码格式不对、短信中心号码不对、SIM卡欠费。排查时先把PDU串拿到PC上用串口助手手动发一遍确认PDU串本身没问题。如果PC上能发成功那就是STM32代码里的字符串处理有问题重点检查长度参数和结尾的CtrlZ有没有正确发送。7.4 OLED显示闪烁OLED闪烁通常是因为刷新太频繁或者I2C通信不稳定。我把刷新周期从50ms改成200ms后明显改善。另外检查I2C上拉电阻如果阻值太大比如10k波形上升沿会变缓导致通信错误。换成4.7k后稳定。8. 代码组织与后续扩展思路整个工程我分成这几个文件main.c负责主循环和任务调度at_command.c负责AT状态机pdu.c负责PDU编码oled.c负责显示key.c负责按键。这样分下来每个文件职责清晰改起来不容易出错。后续如果要扩展几个方向比较实用一是增加接收短信功能解析CMT返回的PDU串提取发送方号码和内容二是把发送结果通过串口打印出来方便调试三是增加多个预设短信按键循环切换四是把信号强度和注册状态定时上报到OLED做成一个简单的状态监控终端。我在实际使用中发现Air780E的AT指令响应速度和网络环境关系很大信号好的时候一两秒就返回信号差的时候可能要十秒以上。所以超时时间不能设太短我一般设15秒重试2次。另外模块在注册网络期间会主动上报很多URC信息比如CREG、CGREG这些信息如果不需要可以忽略但接收缓冲区要够大不然会覆盖掉有用的响应。最后分享一个小技巧调试AT指令的时候把STM32收到的所有原始数据通过另一个串口转发到PC上打印出来比单步调试直观得多。你能清楚看到模块到底回了什么是没回、回错了、还是回了一半。这个习惯帮我省了大量猜测的时间。