ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32+Air780E 4G模组发送中文短信:PDU编码、AT指令与OLED显示实战

STM32+Air780E 4G模组发送中文短信:PDU编码、AT指令与OLED显示实战

手里同时玩着STM32和合宙Air780E的朋友,大概率都动过这个念头:能不能让单片机直接把中文短信发出去,再用OLED屏幕把状态清清楚楚显示出来?这个项目做的就是这件事。整体链路不复杂:按一下按键,STM32通过串口指挥Air780E,把一条中文短信通过4G网络发到指定手机号,0.96寸OLED屏幕上实时显示信号强度、网络注册状态和发送结果。适合物联网报警、设备状态上报、远程通知这类场景,也是学习“MCU+4G模组”协作方式的一个很典型的小项目。无论你是刚点亮OLED的HAL库新手,还是正在做毕设选型,这套流程都能直接拿来改。

1. 项目整体设计与方案选型

1.1 按键、短信、OLED组成的完整闭环

把需求拆开看,这个项目本质上是一套“远程告警/通知设备”的前端交互链路。按下按键后,设备通过4G网络把预设好的中文内容以短信形式发给目标手机号,同时在本地OLED上把当前状态直观地展示给操作者。这里有个特别容易被忽略的点:中文短信和英文短信的编码方式完全不同。

英文短信可以用7bit编码,在TEXT模式下直接发;中文短信必须走UCS2编码,也就是PDU模式。很多人第一次做短信项目就栽在这里——AT+CMGF=1打开TEXT模式,然后直接发中文,结果对方收到一串问号“???”。这个项目表面上是“按键+模组+屏幕”,真正的难点其实集中在PDU编码和AT指令时序两条线上。

我从一开始就把功能拆成了三个模块:STM32本地逻辑(按键检测、OLED显示、状态机)、Air780E通信层(AT指令封装、串口收发)、PDU编码层(把中文转成符合GSM规范的十六进制报文)。三个模块分开设计和调试,最后再合并,排错效率会高很多。

1.2 为什么采用Air780E做4G通信

Air780E是合宙推出的4G Cat.1模组,全网通、国产、价格低,一颗芯片大概十几块钱,开发板也就三四十。相比NB-IoT,Cat.1在短信、语音、移动性方面更完整;相比即将退网的2G/3G方案,Cat.1在目前的移动网络环境下更稳妥。

具体到这个项目,我选择Air780E还有几个实操层面的理由:

  • 支持标准AT指令,不需要额外协议栈,STM32通过UART发文本命令就能控制模组拨号、发短信
  • 工作电压范围3.4~4.3V,典型值3.8V,锂电池或稳压电源都能供电
  • 待机功耗不高,适合电池供电的告警设备
  • 合宙官方提供非常详细的AT指令手册和参考设计,周边电路照着手册画就行

当然也有别的路线,比如直接用Air780E跑LuatOS脚本,用Lua写业务逻辑,彻底不要STM32。但如果你手里已经有STM32的外设资源——按键、OLED、传感器等,让Air780E只干通信的活,职责更清晰,代码也更好维护。这个项目的取舍就是:STM32做主控,4G模组做通信外设。

2. 硬件清单与电路连接细节

2.1 器件清单与供电方案

项目硬件非常精简,我列一份实测通过的清单:

器件型号/规格数量说明
主控STM32F103C8T6(蓝丸)1HAL库开发,其他型号也能通用
4G模组合宙Air780E核心板1板载SIM卡槽、天线、电平转换
OLED0.96寸 I2C SSD130614针或7针版本均可
按键轻触开关2用于确认和触发发送
SIM卡移动/联通/电信1必须开通短信功能
电源5V/2A适配器 + 稳压1注意模组发射瞬间的电流尖峰

供电是整个项目最容易翻车的地方。Air780E在射频发射瞬间的峰值电流能到1A以上,如果用电脑USB口直接给模组供电,发射瞬间电压跌落后模组就会重启,表现就是“发送短信时设备突然断电”。我的做法是:5V适配器进来后分两路,一路经过AMS1117-3.3给STM32和OLED,另一路单独经过一颗MP2315降压到3.8V给Air780E。如果手头没有降压模块,至少要在模组VBAT引脚并一个470uF电解电容加一个100nF陶瓷电容,能缓解大部分瞬间压降。

2.2 STM32与Air780E串口对接方法

STM32和Air780E之间走UART,我用的USART1(PA9/PA10)。Air780E核心板的AT串口电平也是3.3V,跟STM32可以直接对接,不需要外置电平转换。接线对应关系如下:

  • STM32 PA9 (TX) → Air780E RX
  • STM32 PA10 (RX) → Air780E TX
  • STM32 GND → Air780E GND

这里有两个必须提醒的点。

第一,发送和接收交叉连接:STM32的TX接模组的RX,很多新手第一次会把TX接TX,结果AT指令发过去石沉大海。第二,两边必须共地。我调试时遇到过“AT发过去完全没反应”的问题,最后发现是GND没接到一起。串口通信参考地不一致,信号电平就是悬空的,模组当然收不到数据。

如果你买的是Air780E裸模组而不是核心板,那还要注意UART电平匹配问题。裸模组的串口电平是1.8V/3.3V规格,需要加电平转换电路才能和STM32直连。买核心板可以省掉这个麻烦,板子上已经做好了电平转换。

2.3 按键电路设计:从原理到防抖思路

按键用了两个:KEY1和KEY2。KEY1用来进入发送确认界面、切换预设收件人,KEY2负责触发发送。电路结构是标准的“按键接GPIO到GND,内部上拉”:

PA0 --- KEY1 --- GND PA1 --- KEY2 --- GND

STM32F103内部自带上拉电阻,代码里把GPIO配置成输入上拉模式,按键按下时引脚被拉低,读到低电平就表示按下。如果板子质量一般,或者你希望信号更稳定,可以外接10kΩ电阻到3.3V。

关于按键的硬件消抖,我想强调一点:不要随便在按键两端并联大电容。大电容确实能消除机械抖动,但会让长按检测变得很难受,因为电容充放电改变了电平跳变的时间。软件消抖完全够用,后面代码部分我会给一套可靠的时间窗口消抖方案。

2.4 OLED屏幕的I2C连接要点

0.96寸OLED用的几乎全是SSD1306控制芯片,I2C地址默认是0x3C(BS0接地)或0x3D(BS0接VCC)。我用的是I2C1(PB6/PB7),在STM32CubeMX里配置为快速模式400kbps。

接线对应关系:

  • OLED SCL → PB6 (I2C1_SCL)
  • OLED SDA → PB7 (I2C1_SDA)
  • OLED VCC → 3.3V
  • OLED GND → GND

OLED的VCC接3.3V,不要接到5V上,部分板子没有稳压芯片会直接烧掉。如果OLED有RST引脚,建议接到MCU的一个GPIO上,初始化前先拉低再拉高做一次硬件复位,能省掉很多莫名其妙的显示故障。

3. 中文短信的PDU编码:从AT指令到字节拆解

3.1 为什么TEXT模式发不了中文

Air780E的AT指令里,短信模式由AT+CMGF控制:

  • AT+CMGF=1,TEXT模式,适合发纯ASCII内容
  • AT+CMGF=0,PDU模式,所有内容打包成十六进制字符串传输

TEXT模式发不了中文的根源在编码上。TEXT模式下短信内容按GSM 7bit或8bit编码传输,而汉字需要16bit的UCS2编码。只有切到PDU模式,通过DCS字段指定08(UCS2),才能把“你好”编码成4F60597D这样的字节序列发出去。

所以结论很明确:要做中文短信,必须用PDU模式。

3.2 一个完整PDU报文的逐段拆解

下面用实际例子把PDU报文拆开。假设短信中心号码(SMSC)是+8613800200500,目标号码13800138000,短信内容“你好”。

“你好”的UCS2编码为:你=4F60,好=597D,所以内容是4F60597D。

完整PDU报文字节序列如下:

07 91 68 31 08 20 00 50 F0 // SMSC字段 11 00 // PDU类型 + 消息参考号 0D 91 68 31 08 10 83 00 F0 // 目标号码 00 08 00 04 // PID + DCS + VP + UDL 4F 60 59 7D // 用户数据“你好”

逐段解释:

  • 07:短信中心地址长度为7字节
  • 91:短信中心地址类型,91表示国际格式(号码前隐去了“+”号)
  • 683108200050F0:短信中心号码+8613800200500的BCD码,规则是“去掉+号,末尾奇数位补F,然后每两位数字交换顺序”。“8613800200500”补F后变成“8613800200500F”,两两交换后就是“68 31 08 20 00 50 F0”
  • 11:PDU类型,表示SMS-SUBMIT(发送短信),可以理解成“这是一条由终端发往短信中心的提交报文”
  • 00:消息参考号MR,终端自行分配,单条短信填00即可
  • 0D:目标号码长度为13位,这里用的是国际格式,所以目标号码要含国家码86,即“8613800138000”
  • 91:目标号码类型,国际格式
  • 683108108300F0:目标号码“8613800138000”的BCD编码,规则同上
  • 00:协议标识PID,标准短信填00
  • 08:数据编码方案DCS,08表示UCS2(这个字节决定中文能不能正确显示)
  • 00:有效期VP,00表示使用短信中心默认有效期
  • 04:用户数据长度UDL,单位是字节,“你好”共4字节
  • 4F60597D:UCS2编码后的短信内容

计算AT+CMGS参数:整条PDU总共28字节,SMSC字段占9字节,TPDU长度为28-9=19字节。所以发送指令为:

AT+CMGS=19

这里提醒一下:AT+CMGS的length参数,我在Air780E上实测填TPDU长度(不含SMSC)是没问题的,但不同固件版本可能略有差异。拿到模组后用串口助手先单独测一遍,填错会返回ERROR,也很好排查。

3.3 PC端生成UCS2十六进制串的快捷方法

实际项目里,短信内容往往要改,不能每次都用计算器手搓。最简单的方式是在PC上用Python先把中文转成UCS2十六进制字符串:

# 在电脑上运行,把中文短信内容转成UCS2 Hex串 def gbk_text_to_ucs2_hex(text): return text.encode('utf-16-be').hex().upper() print(gbk_text_to_ucs2_hex("你好")) # 输出 4F60597D print(gbk_text_to_ucs2_hex("温度过高")) # 输出 6E295F66 8FC7 9AD8

把输出的十六进制字符串按字节拆开,固化到STM32的const数组里,发送时直接调用。这个方案的优点是MCU端不需要集成庞大的GB2312到Unicode映射表,省了很多Flash空间。

4. STM32端代码实现:状态机与驱动封装

4.1 SSD1306显示驱动:无需U8g2的轻量写法

0.96寸OLED驱动芯片是SSD1306,I2C地址默认0x3C。很多人一上来就用U8g2图形库,功能强大但移植代码量偏大。这个项目只需要显示字符、数字和几个预设的中文状态词,自己封装一个轻量驱动完全够用。

SSD1306驱动核心是三步:初始化、设置页地址和列地址、连续写显示数据。初始化序列是一段固定配置,网上到处都有标准模板;显示部分把128x64分辨率的显存映射成8页,每页128字节,一个字节表示一列8个像素。我封装的核心接口:

void OLED_Init(void); // 初始化 void OLED_Clear(void); // 清屏 void OLED_ShowString(uint8_t x, uint8_t y, char* str); // 显示ASCII字符串 void OLED_ShowChinese(uint8_t x, uint8_t y, const uint8_t* data); // 显示16x16中文 void OLED_ShowNum(uint8_t x, uint8_t y, uint16_t num); // 显示数字

中文显示用取模软件PCtoLCD2002把“收件人、发送中、成功、失败”等字符取成16x16的点阵数组,存在Flash里。取模时选“横向取模”和“字节倒序”,这个要和驱动里的读字节方向保持一致,否则显示出来是镜像或者乱码。

OLED上我设计了三屏状态:待机屏显示“信号:xx”和网络注册状态;确认屏显示收件人号码;发送屏显示“发送中...”动态状态,完成后显示“成功”或“失败+错误码”。

4.2 按键检测:定时扫描、消抖与长短按

按键我选了“定时器扫描+软件消抖”的方案,没有用外部中断。原因是两个按键需要支持短按和长按的区分,定时器轮询能统一处理,代码结构也更简单。

void Key_Scan(void) // 每5ms调用一次 { static uint8_t stable_cnt[2] = {0}; static uint8_t last_level[2] = {1, 1}; // 上一次稳定电平 uint8_t level[2]; level[0] = HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin); level[1] = HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin); for (int i = 0; i < 2; i++) { if (level[i] != last_level[i]) { stable_cnt[i]++; if (stable_cnt[i] >= 4) { // 连续4次扫描一致,约20ms消抖窗口 last_level[i] = level[i]; stable_cnt[i] = 0; if (level[i] == 0) { Key_PressRecord(i); // 记录按下时间戳 } else { Key_ReleaseReport(i); // 释放时上报短按或长按 } } } else { stable_cnt[i] = 0; } } }

消抖的核心逻辑是“连续读到多次相同电平才认为状态稳定”,比简单delay(10ms)后直接判断可靠。因为delay期间如果又出现抖动,会被误判成一次新的按键。

长按短按的区分,我采用“释放时判断”的策略:按下时只记录时间戳,不立即触发任何动作;释放时根据按下时间超过200ms上报“短按”,超过1s上报“长按”。这个设计的坑我之前踩过:在按下瞬间就判断短按,结果长按释放时也会触发一次短按,导致每次长按都先执行了短按功能。改成释放时上报后,长短按互不干扰。

4.3 串口AT指令收发与响应解析

Air780E默认波特率115200,我用USART1的DMA接收加空闲中断(IDLE)来接收不定长的AT响应,这样MCU可以腾出时间处理OLED和按键,不用死等在串口上。

AT指令发送封装得很薄:

void Air780E_SendCmd(char* cmd, uint16_t timeout_ms); uint8_t Air780E_WaitForPrompt(char token, uint16_t timeout_ms); uint8_t Air780E_WaitForResponse(char* expect, uint16_t timeout_ms);

所有AT指令统一以\r\n结尾,用HAL_UART_Transmit发送。接收端把DMA收到的数据放进环形缓冲,解析时查找“OK”“ERROR”“>”这些关键词。

Air780E上电后不能立刻收AT指令,要等模组完成网络注册。我的初始化时序是:

  1. AT → 测通信,等待OK
  2. AT+CPIN? → 查询SIM卡,期望返回READY
  3. AT+CSQ → 查信号强度,返回值范围0~31,数值越大信号越好
  4. AT+CREG? → 查网络注册状态,返回1或5表示已注册
  5. AT+CMGF=0 → 切换到PDU模式
  6. AT+CSCA="+8613800200500" → 设置短信中心号码

这个流程全部跑通后,OLED才从“初始化中...”跳转到待机屏,避免用户按下按键时模组还在注册网络导致发送失败。

4.4 短信发送主流程与状态机

主流程我写成一个状态机,四个核心状态:

typedef enum { STATE_IDLE, // 待机 STATE_SEND_CONFIRM, // 确认发送 STATE_SENDING, // 发送中 STATE_DONE // 成功/失败展示 } AppState;

按键触发流程是:待机屏短按KEY1进入确认屏,OLED显示“发给 13800138000”;确认屏短按KEY2触发发送,OLED切到“发送中...”,同时模组开始进入AT交互;收到模组返回+CMGS就置成功,收到ERROR/CMS ERROR就显示错误码。

发送核心代码示意:

void App_SendSMS(void) { // PDU内容,预先按3.2节生成 const uint8_t pdu_full[] = { 0x07,0x91,0x68,0x31,0x08,0x20,0x00,0x50,0xF0, // SMSC 0x11,0x00, 0x0D,0x91,0x68,0x31,0x08,0x10,0x83,0x00,0xF0, // 目标号码 0x00,0x08,0x00,0x04, // PID+DCS+VP+UDL 0x4F,0x60,0x59,0x7D // “你好” }; uint8_t smsc_len = 9; uint16_t tpdu_len = sizeof(pdu_full) - smsc_len; // 19 char cmd[32]; snprintf(cmd, sizeof(cmd), "AT+CMGS=%d\r\n", tpdu_len); USART_SendString(cmd); if (Air780E_WaitForPrompt('>', 2000) == 0) { // 出现 > 后,发送不含SMSC字段的PDU内容,并以0x1A结束 USART_SendBytes(pdu_full + smsc_len, tpdu_len); USART_SendByte(0x1A); if (Air780E_WaitForResponse("+CMGS", 10000) == 0) { OLED_ShowStatus("发送成功"); } else { OLED_ShowStatus("发送失败"); } } else { OLED_ShowStatus("未收到确认"); } }

这段代码里有两个容易踩坑的点。第一,发送0x1A(Ctrl+Z)后,模组才开始真正发送短信,等待时间不要设太短,网络拥塞时10秒都算紧张。第二,如果OLED一直停在“发送中...”,大概率是模组已经返回错误响应但代码没有正确识别,建议把最终的AT响应全文在OLED上滚动显示出来,这样排查很直观。

5. 调试实录:常见问题与排查方法

5.1 OLED不亮、花屏的针对性处理

OLED不亮,我每次调试都按三步走:

第一,确认VCC和GND。0.96寸OLED的VCC接3.3V,部分板子没有稳压,接5V会烧。第二,确认I2C地址。用一段I2C扫描程序扫一下总线,看返回值是0x3C还是0x3D,驱动初始化里的地址必须和实际一致。第三,确认初始化序列有没有执行到。在OLED_Init前后加串口打印,避免代码流程卡在别的地方。

花屏比较麻烦,十有八九是I2C时序问题。把I2C从400kbps降到100kbps试一下,如果花屏消失,说明总线过长或电平边沿太陡。另外,OLED的RST引脚如果用MCU控制,初始化前记得先拉低再拉高;如果RST悬空,一些型号的SSD1306会有概率上电花屏。

还有一个容易忽略的干扰源:OLED和Air780E共用3.3V电源时,模组发射瞬间会把3.3V拉低,导致OLED花屏。给模组单独供电或者加强滤波电容能明显改善。

5.2 AT指令无响应与串口乱码排查

AT指令无响应,按概率从高到低排查:

  • 模组没开机。Air780E核心板的PWRKEY需要触发才能开机,最简单的办法是拔插电源。开机后观察板载NETLIGHT信号灯,如果有呼吸闪烁,说明模组基本工作正常
  • TX/RX接反。STM32的TX接模组的RX,交叉连接,不要相同对接
  • 波特率不对。默认是115200,如果模组固件被配置过其他波特率,串口助手收到的就是乱码
  • 没共地。两个设备之间GND没有连在一起

我调试时的习惯是:先用USB转TTL把Air780E单独接到电脑串口助手,手动敲一遍完整的AT流程,模组本身没问题后再接STM32。这样能极大缩小排查范围。很多朋友的问题是“STM32发的AT模组没反应”,结果用串口助手一测,模组本身就是好的,问题全在接线和波特率上。

5.3 短信发不出去的几个隐蔽原因

短信发送失败,我整理成一张速查表:

现象根本原因处理方法
AT+CMGS=xx返回ERRORPDU长度参数不对或报文格式错误核对TPDU长度,检查PDU内容字节数
发送后+CMS ERROR: 302SIM卡未注册或短信中心未设置执行AT+CSCA设置中心号码,确认CREG=1
返回OK但对方没收到号码BCD编码错误或信号太弱核验DA字段,看CSQ值是否低于10
对方收到乱码DCS字段不是08,或内容不是UCS2编码PDU里DCS必须为08,内容必须是UTF-16BE
一直停在发送中网络拥塞,模组超时未完成拉长等待时间,检查天线连接

两个特别隐蔽的坑:

第一,SIM卡欠费或者没开通短信业务,模组照样能注册网络、信号满格,但短信就是发不出去,返回的信息可能是OK但对方收不到,或者直接+CMS ERROR。遇到这种问题先换张正常手机卡验证。

第二,短信中心号码在不同地区有差异。中国移动在大多数省份是+8613800200500,但个别地区不同。用AT+CSCA?查询当前配置,跟运营商客服确认归属地的正确号码。

5.4 按键重复触发与长按短按冲突

按键的问题,我调试中遇到两大类。

一类是按下一次触发了两三次。原因是消抖时间太短或者没有对“释放”抖动做处理。我用的方案是5ms扫描、连续4次采样一致才算稳定,相当于20ms消抖窗口,同时对按下和释放两个边沿都做同样的滤波,松手瞬间的弹跳就不会造成重复触发。

另一类是长按和短按冲突。我前面提到过,一定不要在按下瞬间就判断短按,要等释放时根据按下时长来上报。按下时记录毫秒级时间戳,释放时计算差值,短于200ms算短按,超过1s算长按。中间还可以用OLED实时显示“请按住...”,提示用户当前是长按状态,体验更清晰。

6. 项目扩展方向与个人体会

6.1 把固定短信升级成动态传感数据

这个项目跑通后,最自然的扩展是把固定短信内容改成动态数据。比如加上DHT11温湿度传感器,按键发送时把温度、湿度拼进短信内容。

动态内容的UCS2编码绕不开“字符串转Unicode”的问题。一种做法是在MCU端只处理数字、字母和常用标点,因为这些字符的UCS2编码就是0x0030到0x0039、0x0041到0x005A这种固定区间,拼成字节毫无难度。中文部分如果必须要动态拼接,就只能集成GB2312到Unicode的映射表,工作量比较大,我建议用“预设模板+填充动态数字”的方式,绕开中文动态编码。

6.2 后续可以继续探索的方向

除了传感数据,还可以加RTC定时发送。配合DS3231或者STM32内部RTC,每天固定时间自动发一条状态报告,按键变成手动触发的补充入口。这个改动量不大,但应用场景一下子宽了很多,适合设备巡检、老人看护提醒等。

OLED界面也可以做成一个完整的菜单系统。目前只是状态显示,你可以用两个按键做个光标上下移动的小菜单,切换收件人、预览发送内容、查看信号历史记录。Air780E返回的CSQ值、IMEI、网络注册状态都可以展示出来,整台设备就变成一个带屏幕的短信终端,给长辈用也直观。

最后说一点实操体会。做这个项目最大的收获不是“短信终于发出去了”这个结果,而是真正理解了PDU编码背后的字节逻辑。以前用手机发短信,从来没想过一条“你好”在空口传输时要变成4F60597D这样的数据。当你亲手把每一个字节算出来、发出去,看到对方手机上收到正确中文短信的那一刻,那种掌控感是串口助手随便发发命令完全比不了的。现在这套“按键+4G模组+状态屏”的骨架,我已经直接复用到另外两个项目上了,每次改改PDU内容和状态机逻辑就能交付,开发效率提升很明显。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进