1. 项目缘起与整体设计思路按键一按短信就发到手机上OLED屏幕上还能实时看到发送状态——这个需求听起来简单但真要做出来涉及的东西比想象中多。STM32负责逻辑控制和界面显示Air780E负责蜂窝网络通信两者通过串口用AT指令对话短信内容还得经过PDU编码才能带上中文。这套组合在工业现场、远程报警、农业监测这些场景里非常实用因为很多地方没有WiFi覆盖但蜂窝网络信号是稳定的。我最初接触这个需求是帮一个做温室大棚的朋友做的。他想在棚里放几个紧急按钮工人发现异常情况按一下他手机立刻收到中文短信同时棚内的小OLED屏上显示“已发送”或“发送失败”。这个场景对成本敏感对稳定性要求高STM32F103C8T6加Air780E加0.96寸OLED整套硬件成本控制在五十元以内非常适合小批量部署。这个项目的核心难点不在单个模块而在“串联”STM32要同时处理按键扫描、OLED刷新、串口收发、AT指令状态机、PDU编码任何一个环节卡住整个流程就断了。我见过不少人单独调通了OLED也单独调通了Air780E发英文短信但把中文短信和状态显示整合到一起就各种问题。所以这篇内容我会从整体架构讲起把每个环节的坑都摊开来说。适合谁参考有STM32基础、会用HAL库或标准库、想快速把蜂窝通信集成到项目里的开发者。如果你完全没碰过串口和AT指令建议先把UART收发和状态机的基本概念过一遍否则后面PDU编码部分会比较吃力。最终实现的效果上电后OLED显示“系统就绪”按下按键STM32通过串口向Air780E发送一系列AT指令完成短信内容的中文PDU编码、发送、状态查询OLED同步显示“正在发送”“发送成功”或“发送失败错误码XX”。整个过程不需要电脑干预脱机运行。2. 硬件选型与接线要点2.1 为什么选STM32F103C8T6和Air780ESTM32F103C8T6这颗芯片不用多介绍72MHz主频、64KB Flash、20KB RAM对于这个项目来说绰绰有余。选它主要三个原因一是资料多遇到问题容易搜到答案二是价格便宜核心板十块钱左右三是外设够用两个USART一个接Air780E一个留给调试打印、一个I2C接OLED、若干GPIO接按键和状态灯。Air780E是合宙出的Cat.1模块支持LTE-FDD和LTE-TDD走的是4G网络但功耗和成本比Cat.4低不少。它最大的好处是AT指令集完善中文短信、TCP透传、HTTP请求都支持而且官方文档写得比较清楚。相比SIM800C那种2G模块Air780E在信号覆盖和未来可用性上更有保障。实测下来Air780E在室内靠窗位置信号强度一般在-70dBm到-85dBm之间发短信响应时间在3到8秒。OLED选的是0.96寸I2C接口的SSD1306分辨率128x64足够显示四行中文。这里要注意市面上有些0.9寸OLED虽然也是I2C但驱动芯片可能是SSD1306的兼容型号初始化序列略有差异买的时候最好确认一下。我踩过一次坑同一份代码在0.96寸上正常换0.9寸就花屏后来发现是对比度设置寄存器不兼容。2.2 接线方案与电源注意事项接线本身不复杂但有几个地方容易出问题。先看接线表STM32引脚连接目标说明PA9 (USART1_TX)Air780E_RX调试串口也可用于AT指令PA10 (USART1_RX)Air780E_TX接收模块返回数据PB6 (I2C1_SCL)OLED_SCLI2C时钟线PB7 (I2C1_SDA)OLED_SDAI2C数据线PB0按键一端按键另一端接GNDPB1状态LED串联1K电阻到GND3.3VOLED_VCCOLED供电5VAir780E_VCC模块供电必须5VGND共地所有模块共地注意Air780E的供电必须是5V而且峰值电流能到2A。如果你用STM32板子上的5V引脚供电一定要确认板子上的LDO能扛住。我一开始用AMS1117-5.0供电发短信瞬间模块重启后来换了一个2A的DC-DC模块才稳定。建议单独给Air780E供5V或者在VCC和GND之间并一个1000uF的电解电容缓冲。另外Air780E的串口电平是3.3V和STM32的USART直接连没问题不需要电平转换。但如果你用的是5V的STM32板子比如某些最小系统板带5V稳压要确认USART引脚是不是3.3V输出否则可能烧模块的RX脚。按键部分我用的是外部下拉加内部上拉的方式实际上直接启用STM32内部上拉、按键接GND就行省一个电阻。但要注意按键抖动后面软件部分会讲怎么处理。3. AT指令与PDU编码核心解析3.1 Air780E发短信的AT指令流程Air780E发一条中文短信不是一条指令就能搞定的需要按顺序执行多条AT指令。我整理了一个最小可用流程AT // 测试模块是否响应 ATCPIN? // 查询SIM卡状态返回READY才能继续 ATCSQ // 查询信号质量建议大于10 ATCMGF0 // 设置为PDU模式发中文必须用PDU ATCMGS长度 // 发送短信参数是PDU串的长度 PDU串 // 收到后输入PDU数据 CtrlZ // 十六进制0x1A表示结束这里的关键是ATCMGF0设置为PDU模式。很多人习惯用文本模式ATCMGF1但文本模式不支持中文发出去会是乱码或者直接失败。PDU模式虽然麻烦但通用性强英文中文都能发。ATCMGS后面的长度参数是PDU串的长度注意这个长度是“PDU串的字符数除以2”因为PDU串是十六进制表示的两个字符代表一个字节。比如PDU串有86个字符长度参数就是43。这个参数算错模块会返回ERROR。3.2 PDU编码原理与中文短信构造PDU编码是整个过程里最绕的部分但理解了结构就不难。一条PDU短信由几个部分组成短信中心号码格式是91 号码长度 号码补F对齐。比如8613800250500去掉号是13位长度是13的十六进制0D号码两两分组补F变成68310820050500F0前面加91最终是9168310820050500F0。如果你不知道当地短信中心号码可以先用手机发一条短信然后在模块里用ATCSCA?查询。短信首字节11表示普通短信、有状态报告01表示普通短信、无状态报告。我一般用11这样能收到状态报告确认发送成功。目标号码和短信中心号码类似91 长度 号码补F。比如手机号13800138000长度0B号码变成3108003800F0前面加91。协议标识00表示普通GSM短信。编码方式08表示UCS2编码中文00表示7bit编码英文。发中文必须用08。时间戳7个字节格式是“年月日时分秒时区”每个字节是十进制数转十六进制。比如2024年3月15日14时30分00秒时区32中国是8加32后是40十六进制28时间戳就是42313514300028。这个时间戳可以随便填模块不会校验但格式必须对。用户数据长度中文短信的字符数乘以2再转十六进制。比如“你好”两个字长度是4十六进制04。用户数据中文转UCS2编码。每个汉字对应两个字节比如“你”是4F60“好”是597D拼起来就是4F60597D。把这些拼起来一条发往13800138000、内容为“你好”的PDU串就是9168310820050500F011000B913108003800F0000842313514300028044F60597D看起来很长但拆开看就是上面那几个部分依次拼接。我在代码里写了一个pdu_encode函数输入目标号码和中文内容输出PDU串和长度后面会给出具体实现。提示短信中心号码每个地区不同移动、联通、电信也不一样。最稳妥的办法是插卡后发一条英文短信用ATCMGS发出去然后读ATCSCA?拿到中心号码。或者直接问运营商客服。4. 软件架构与关键代码实现4.1 整体状态机设计这个项目最怕的就是“阻塞式”写法发一条短信程序卡在等待模块响应上OLED不刷新了按键也不响应了。所以必须用状态机加非阻塞的方式。我把整个流程拆成几个状态STATE_IDLE空闲OLED显示“系统就绪”等待按键STATE_SEND_SMS按键触发开始发送短信流程STATE_WAIT_RESP等待模块返回超时重试STATE_SUCCESS发送成功OLED显示“发送成功”STATE_FAIL发送失败OLED显示错误码主循环里用HAL_GetTick()做超时判断每个状态停留时间不超过设定值超时就跳转到失败状态。这样即使模块没响应系统也不会卡死。按键扫描用10ms定时器中断检测到按下后置一个标志位主循环里判断标志位再进入发送流程。按键消抖用计数法连续检测到低电平超过3次30ms才认为是有效按下。4.2 OLED显示驱动与中文字库OLED用HAL库的I2C驱动初始化序列参考SSD1306数据手册。这里不展开讲I2C底层重点说中文字库。0.96寸OLED显示中文需要字库支持。我用的是PCtoLCD2002取模软件设置阴码、逐列式、顺向生成16x16点阵。每个汉字32个字节存到一个数组里。比如“系”字的点阵数据const unsigned char HZ_Xi[] { 0x10,0x20,0x3F,0xC0,0x20,0x40,0x20,0x40,0x3F,0xC0,0x20,0x40,0x20,0x40,0x20,0x40, 0x3F,0xC0,0x20,0x40,0x20,0x40,0x20,0x40,0x3F,0xC0,0x20,0x40,0x00,0x00,0x00,0x00 };显示的时候先设置页地址和列地址然后逐字节写入。注意SSD1306的显存是按页组织的每页8行128列。显示一个16x16汉字要占两页、16列。我封装了一个OLED_ShowChinese(uint8_t x, uint8_t y, const uint8_t *hz)函数x是列坐标0到127y是页坐标0到7内部自动处理跨页写入。注意I2C速率建议设在400kHz太快了OLED可能不响应太慢了刷新会闪。我试过1MHz0.96寸的屏直接花屏降到400kHz就正常了。4.3 串口收发与AT指令解析串口用USART1波特率115200开启接收中断。Air780E默认波特率是115200如果你买的是二手模块可能被改过可以用ATIPR115200恢复。接收中断里把数据存到一个环形缓冲区主循环里解析。解析AT响应时不要用strstr去匹配整个缓冲区因为模块返回的数据可能分多次到达。我的做法是维护一个接收状态检测到\r\n就认为一行结束然后逐行判断。比如等待OK响应代码逻辑是if (strstr((char*)rx_line, OK) ! NULL) { at_state AT_OK; } else if (strstr((char*)rx_line, ERROR) ! NULL) { at_state AT_ERROR; }发送AT指令后启动一个超时定时器比如5秒。5秒内没收到预期响应就重试一次再失败就报错。4.4 PDU编码函数实现PDU编码函数是整个项目的核心我把它拆成几个小函数// 号码编码8613800138000 - 91683108003800F0 void encode_number(const char *number, char *out) { // 去掉号计算长度两两交换补F } // 中文转UCS2每个汉字转两个字节 void chinese_to_ucs2(const char *text, char *out) { // 遍历UTF-8或GBK编码的汉字查表或计算UCS2码 } // 时间戳生成 void encode_timestamp(char *out) { // 获取当前时间格式化为PDU时间戳 } // 主编码函数 void pdu_encode(const char *center, const char *target, const char *text, char *pdu, int *len) { // 拼接各部分 }中文转UCS2这里有个坑如果你的代码文件是UTF-8编码一个汉字占3个字节需要先转成UCS2。我用的方法是查表把常用汉字和对应的UCS2码存到一个数组里用二分查找。如果汉字数量不多比如只发固定几条短信直接硬编码也行。时间戳部分我用的是STM32的RTC如果没有RTC可以写死一个固定时间模块不会校验。但建议还是加上RTC方便调试。4.5 完整发送流程代码把上面的模块串起来主发送函数大概长这样void send_sms_task(void) { switch (sms_state) { case STATE_IDLE: if (key_pressed) { OLED_Clear(); OLED_ShowChinese(0, 0, HZ_Zheng); OLED_ShowChinese(16, 0, HZ_Zai); OLED_ShowChinese(32, 0, HZ_Fa); OLED_ShowChinese(48, 0, HZ_Song); sms_state STATE_SEND_SMS; step 0; } break; case STATE_SEND_SMS: switch (step) { case 0: uart_send(ATCMGF0\r\n); step 1; timeout HAL_GetTick(); break; case 1: if (at_ok) { pdu_encode(CENTER_NUM, TARGET_NUM, 你好, pdu_buf, pdu_len); sprintf(cmd, ATCMGS%d\r\n, pdu_len); uart_send(cmd); step 2; timeout HAL_GetTick(); } break; case 2: if (at_prompt) { // 收到 uart_send(pdu_buf); uart_send_byte(0x1A); step 3; timeout HAL_GetTick(); } break; case 3: if (at_ok) { sms_state STATE_SUCCESS; } else if (at_error) { sms_state STATE_FAIL; } break; } if (HAL_GetTick() - timeout 5000) { sms_state STATE_FAIL; } break; case STATE_SUCCESS: OLED_Clear(); OLED_ShowChinese(0, 0, HZ_Fa); OLED_ShowChinese(16, 0, HZ_Song); OLED_ShowChinese(32, 0, HZ_Cheng); OLED_ShowChinese(48, 0, HZ_Gong); HAL_Delay(2000); sms_state STATE_IDLE; break; case STATE_FAIL: OLED_Clear(); OLED_ShowChinese(0, 0, HZ_Fa); OLED_ShowChinese(16, 0, HZ_Song); OLED_ShowChinese(32, 0, HZ_Shi); OLED_ShowChinese(48, 0, HZ_Bai); HAL_Delay(2000); sms_state STATE_IDLE; break; } }这段代码是简化版实际项目中还要处理重试、错误码显示、按键长按等功能。但核心逻辑就是这样状态机驱动非阻塞超时保护。5. 常见问题与排查技巧实录5.1 模块不响应AT指令这是最常见的问题表现是发送AT后没有任何返回。排查顺序检查供电用万用表量Air780E的VCC和GND确认是5V。如果电压低于4.5V模块可能启动不了。发短信瞬间电压跌落超过0.5V说明电源功率不够需要换DC-DC或加大电容。检查串口接线TX和RX是否交叉连接STM32的TX接模块的RXSTM32的RX接模块的TX。我见过有人直连TX-TX调了半天。检查波特率Air780E默认115200但有些批次是9600。可以尝试用9600发送AT如果响应了再用ATIPR115200改回来。检查SIM卡卡是否插好是否欠费用ATCPIN?查询返回CPIN: READY才正常。如果返回CME ERROR: 10说明没检测到卡重新插拔。检查模块是否启动Air780E上电后状态灯会闪烁。如果灯不亮可能是模块损坏或供电不足。5.2 短信发送失败返回ERRORATCMGS返回ERROR常见原因PDU串长度算错长度参数是PDU串字符数除以2不是字节数。比如PDU串有86个字符长度是43。算错了模块直接拒绝。短信中心号码错误不同地区、不同运营商的中心号码不同。用ATCSCA?查询如果返回空需要手动设置ATCSCA8613800250500。目标号码格式错误PDU里的号码必须是91开头号码两两交换补F。比如13800138000编码后是3108003800F0前面加91。中文编码错误UCS2编码时汉字转码不对模块会返回ERROR。建议先用英文短信测试确认流程通了再换中文。5.3 OLED显示花屏或闪烁I2C速率过高降到400kHz或100kHz试试。初始化序列不对不同批次的SSD1306初始化参数略有差异特别是对比度设置。可以尝试调整0x81命令后的参数值。电源干扰OLED和Air780E共用3.3V时模块发短信瞬间电流突变可能导致OLED复位。建议OLED单独用LDO供电或者在VCC和GND之间并一个100uF电容。显存写入越界显示汉字时x坐标加16不能超过128y坐标加2不能超过8。越界会导致花屏。5.4 按键误触发抖动加10ms到30ms的软件消抖或者并在按键两端并一个0.1uF电容。中断优先级如果按键用中断优先级不要设太高否则可能打断串口接收。长按处理如果按键长按会连续触发多次发送。可以在发送期间禁用按键或者检测按键释放后再允许下一次触发。5.5 常见问题速查表现象可能原因解决方法模块不响应AT供电不足、接线错误、波特率不对量电压、交叉TX/RX、试9600返回ERRORPDU长度错、中心号码错、编码错检查长度计算、查询CSCA、先用英文测试OLED花屏I2C太快、初始化不对、电源干扰降速、调初始化、加电容按键误触发抖动、长按、中断优先级加消抖、禁用按键、调优先级发送成功但收不到号码错、短信中心错、运营商拦截核对号码、查CSCA、换卡测试模块频繁重启电源功率不够换DC-DC、加大电容实操心得调试的时候先把Air780E用USB转TTL接到电脑上用串口助手手动发AT指令确认模块和SIM卡都正常。然后再接到STM32上用代码控制。这样能把硬件问题和软件问题分开省很多时间。6. 项目扩展与优化方向这套基础框架跑通后可以往几个方向扩展。一是增加多个按键对应不同的短信内容比如“温度过高”“湿度异常”“设备故障”OLED上显示对应的状态。二是加入传感器比如DHT11测温度湿度超过阈值自动发短信不需要人工按键。三是把短信改成TCP透传数据直接上传到服务器适合数据量大的场景。代码层面可以把PDU编码做成一个独立的模块支持任意长度中文短信。还可以加入短信发送队列按键触发后先入队后台慢慢发避免频繁按键导致状态机混乱。OLED显示也可以优化比如加一个进度条显示发送进度。我在实际使用中发现Air780E在信号弱的地方发送失败率比较高可以加一个重试机制失败后等待3秒重发最多重试3次。如果3次都失败OLED显示“信号弱请检查天线”。天线部分Air780E自带PCB天线如果装在金属壳里信号会差很多建议外接一根棒状天线成本几块钱效果提升明显。最后分享一个小技巧如果你批量生产每块板子的短信中心号码可能不同可以在代码里加一个自动获取中心号码的逻辑上电后发ATCSCA?把返回的号码存到Flash里下次直接用。这样就不用手动改代码了。