STM32+Air780E 4G模组发送中文短信:PDU编码、AT指令与OLED显示实战 手里同时玩着STM32和合宙Air780E的朋友大概率都动过这个念头能不能让单片机直接把中文短信发出去再用OLED屏幕把状态清清楚楚显示出来这个项目做的就是这件事。整体链路不复杂按一下按键STM32通过串口指挥Air780E把一条中文短信通过4G网络发到指定手机号0.96寸OLED屏幕上实时显示信号强度、网络注册状态和发送结果。适合物联网报警、设备状态上报、远程通知这类场景也是学习“MCU4G模组”协作方式的一个很典型的小项目。无论你是刚点亮OLED的HAL库新手还是正在做毕设选型这套流程都能直接拿来改。1. 项目整体设计与方案选型1.1 按键、短信、OLED组成的完整闭环把需求拆开看这个项目本质上是一套“远程告警/通知设备”的前端交互链路。按下按键后设备通过4G网络把预设好的中文内容以短信形式发给目标手机号同时在本地OLED上把当前状态直观地展示给操作者。这里有个特别容易被忽略的点中文短信和英文短信的编码方式完全不同。英文短信可以用7bit编码在TEXT模式下直接发中文短信必须走UCS2编码也就是PDU模式。很多人第一次做短信项目就栽在这里——ATCMGF1打开TEXT模式然后直接发中文结果对方收到一串问号“???”。这个项目表面上是“按键模组屏幕”真正的难点其实集中在PDU编码和AT指令时序两条线上。我从一开始就把功能拆成了三个模块STM32本地逻辑按键检测、OLED显示、状态机、Air780E通信层AT指令封装、串口收发、PDU编码层把中文转成符合GSM规范的十六进制报文。三个模块分开设计和调试最后再合并排错效率会高很多。1.2 为什么采用Air780E做4G通信Air780E是合宙推出的4G Cat.1模组全网通、国产、价格低一颗芯片大概十几块钱开发板也就三四十。相比NB-IoTCat.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我用的USART1PA9/PA10。Air780E核心板的AT串口电平也是3.3V跟STM32可以直接对接不需要外置电平转换。接线对应关系如下STM32 PA9 (TX) → Air780E RXSTM32 PA10 (RX) → Air780E TXSTM32 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 --- GNDSTM32F103内部自带上拉电阻代码里把GPIO配置成输入上拉模式按键按下时引脚被拉低读到低电平就表示按下。如果板子质量一般或者你希望信号更稳定可以外接10kΩ电阻到3.3V。关于按键的硬件消抖我想强调一点不要随便在按键两端并联大电容。大电容确实能消除机械抖动但会让长按检测变得很难受因为电容充放电改变了电平跳变的时间。软件消抖完全够用后面代码部分我会给一套可靠的时间窗口消抖方案。2.4 OLED屏幕的I2C连接要点0.96寸OLED用的几乎全是SSD1306控制芯片I2C地址默认是0x3CBS0接地或0x3DBS0接VCC。我用的是I2C1PB6/PB7在STM32CubeMX里配置为快速模式400kbps。接线对应关系OLED SCL → PB6 (I2C1_SCL)OLED SDA → PB7 (I2C1_SDA)OLED VCC → 3.3VOLED GND → GNDOLED的VCC接3.3V不要接到5V上部分板子没有稳压芯片会直接烧掉。如果OLED有RST引脚建议接到MCU的一个GPIO上初始化前先拉低再拉高做一次硬件复位能省掉很多莫名其妙的显示故障。3. 中文短信的PDU编码从AT指令到字节拆解3.1 为什么TEXT模式发不了中文Air780E的AT指令里短信模式由ATCMGF控制ATCMGF1TEXT模式适合发纯ASCII内容ATCMGF0PDU模式所有内容打包成十六进制字符串传输TEXT模式发不了中文的根源在编码上。TEXT模式下短信内容按GSM 7bit或8bit编码传输而汉字需要16bit的UCS2编码。只有切到PDU模式通过DCS字段指定08UCS2才能把“你好”编码成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”11PDU类型表示SMS-SUBMIT发送短信可以理解成“这是一条由终端发往短信中心的提交报文”00消息参考号MR终端自行分配单条短信填00即可0D目标号码长度为13位这里用的是国际格式所以目标号码要含国家码86即“8613800138000”91目标号码类型国际格式683108108300F0目标号码“8613800138000”的BCD编码规则同上00协议标识PID标准短信填0008数据编码方案DCS08表示UCS2这个字节决定中文能不能正确显示00有效期VP00表示使用短信中心默认有效期04用户数据长度UDL单位是字节“你好”共4字节4F60597DUCS2编码后的短信内容计算ATCMGS参数整条PDU总共28字节SMSC字段占9字节TPDU长度为28-919字节。所以发送指令为ATCMGS19这里提醒一下ATCMGS的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驱动芯片是SSD1306I2C地址默认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指令要等模组完成网络注册。我的初始化时序是AT → 测通信等待OKATCPIN? → 查询SIM卡期望返回READYATCSQ → 查信号强度返回值范围0~31数值越大信号越好ATCREG? → 查网络注册状态返回1或5表示已注册ATCMGF0 → 切换到PDU模式ATCSCA8613800200500 → 设置短信中心号码这个流程全部跑通后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, // PIDDCSVPUDL 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), ATCMGS%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(未收到确认); } }这段代码里有两个容易踩坑的点。第一发送0x1ACtrlZ后模组才开始真正发送短信等待时间不要设太短网络拥塞时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 短信发不出去的几个隐蔽原因短信发送失败我整理成一张速查表现象根本原因处理方法ATCMGSxx返回ERRORPDU长度参数不对或报文格式错误核对TPDU长度检查PDU内容字节数发送后CMS ERROR: 302SIM卡未注册或短信中心未设置执行ATCSCA设置中心号码确认CREG1返回OK但对方没收到号码BCD编码错误或信号太弱核验DA字段看CSQ值是否低于10对方收到乱码DCS字段不是08或内容不是UCS2编码PDU里DCS必须为08内容必须是UTF-16BE一直停在发送中网络拥塞模组超时未完成拉长等待时间检查天线连接两个特别隐蔽的坑第一SIM卡欠费或者没开通短信业务模组照样能注册网络、信号满格但短信就是发不出去返回的信息可能是OK但对方收不到或者直接CMS ERROR。遇到这种问题先换张正常手机卡验证。第二短信中心号码在不同地区有差异。中国移动在大多数省份是8613800200500但个别地区不同。用ATCSCA?查询当前配置跟运营商客服确认归属地的正确号码。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内容和状态机逻辑就能交付开发效率提升很明显。