
做嵌入式这行最怕的就是设备出问题人不在现场。去年我在捣鼓一个园区设备报警装置时被折腾得够呛设备异常数据能采到可值班手机就是收不到提醒后来我干脆做了这个“STM32Air780E按键发送中文短信OLED状态显示”的小项目。整套系统的思路很直接STM32负责按键检测和状态管理Air780E作为4G通信模组通过串口AT指令下发中文短信OLED屏实时显示待机、选择模板、发送中、成功/失败等状态。这个方案非常适合毕业设计、机房报警、农业大棚预警、无人值守设备通知等场景也可以作为物联网项目的短信下发底座。本文会把方案选型、底层驱动、PDU编码、状态机设计和实际踩坑记录全部摊开讲清楚想复现的可以直接抄作业。1. 项目定位与整体方案设计1.1 这套系统到底解决了什么问题先想清楚需求在很多场景下设备端的告警并不需要走复杂的物联网平台只需要把一句中文短信发到值班人员的手机上。比如温度超限、门磁被打开、设备离线、料仓缺料这类事件用按键触发一条预置好的中文短信比开发完整App、接入云平台要快得多也稳定得多。这里最大的痛点是中文。很多入门教程只让你用串口发英文明文一旦涉及中文就要处理编码、PDU报文格式、AT指令参数计算等多个环节。而用Air780E这类4G Cat.1模组配合STM32本质上就是把“发短信”封装成一个串口外设功能MCU端只需要组织好AT指令和数据帧就行。OLED的作用也很关键没有屏幕的话你根本不知道模组有没有注册网络、短信有没有发送成功调试起来两眼一抹黑。有了状态显示现场人员也能一眼看出系统当前在干什么。我的建议是做之前先列出几个问题要发几条预置短信手机号是固定写死还是可配置按键是直接发送还是先选择模板再确认这些决定了状态机的复杂度和代码结构。我这次按“三条预置模板 按键选择 确认发送”来设计既满足演示效果又不会把状态机搞得太复杂。1.2 为什么选Air780E而不是2G或NB-IoT很多老玩家第一反应是SIM800C/SIM900A毕竟当年GPRS模块是标配。但2G网络正在逐步清频退网很多地区的2G信号已经弱到没法稳定收发短信了。NB-IoT模组虽然覆盖好、功耗低但短信能力在部分运营商网络下支持并不理想而且AT指令生态相对封闭。相比之下Air780E这类Cat.1模组是当前相当省心的选择基于4G LTE网络覆盖成熟资费也不贵串口AT指令用法和传统GPRS模块一脉相承迁移成本很低。Air780E还有一个好处是合宙生态完善官方资料、原理图、核心板都很容易买到。我用的是Air780E核心板自带USB转串口、电源电路和天线接口拿到手插上SIM卡就能用。如果自己画底板集成模组就得特别注意电平匹配和射频天线走线新手不建议一上来就贴片集成。选择Air780E的另一个原因是它的串口AT响应很规整指令超时和错误码都比较明确方便MCU做状态判断。比如发送短信后返回CMGS: 序号和OK失败时返回ERROR或CME ERROR:这对OLED状态显示非常友好。相比之下有些老模块的异常返回格式五花八门解析起来很痛苦。1.3 硬件组成与接线规划我用的硬件清单如下器件型号/规格作用主控STM32F103C8T6最小系统板按键扫描、状态机、OLED驱动、AT指令组织4G模组Air780E核心板Cat.1短信收发、网络注册显示屏0.96寸I2C OLEDSSD1306128x64显示菜单、状态、发送结果按键轻触开关3个上翻、下翻/选择、确认/发送电源5V USB供电板载稳压至3.3V系统供电注意模组峰值电流接线这块最容易翻车也最好提前列个表STM32引脚外设引脚说明PA9USART1_TXAir780E主串口RX交叉连接PA10USART1_RXAir780E主串口TX交叉连接GNDGND必须共地PB6OLED SCLI2C时钟PB7OLED SDAI2C数据PA0 / PA1 / PA2按键K1/K2/K3另一端接GND内部上拉3.3VOLED VCC、Air780E核心板逻辑参考电源Air780E核心板本身是5V USB供电但串口逻辑电平是3.3V和STM32的3.3V电平可以直接对接。如果模组是1.8V IO规格的版本就一定要加电平转换否则串口通信会时好时坏。还有一个容易忽略的点SIM卡一定要插到位触点接触不良会导致CPIN: NOT INSERTED这部分我在后面排查章节会详细说。2. 驱动层怎么做OLED显示、按键、串口2.1 OLEDSSD1306能显示中文的关键点SSD1306本身不带中文字库这是很多新手第一次用OLED显示中文就卡住的地方。LCD1602自带ASCII字库OLED不一样每一个汉字都要自己准备点阵数据。常见的做法是用PCtoLCD2002软件取模把需要的汉字生成16x16的点阵数组然后通过I2C写GRAM。取模时要选“列行式”还是“逐行式”必须和你代码里的显示函数匹配否则字会左右分裂或者上下颠倒。我习惯建一个oled_font.c文件把用到的中文做成一维数组按顺序编号。显示函数大致是这样void OLED_ShowChinese(uint8_t x, uint8_t y, uint8_t index, uint8_t size) { uint8_t i; // 以16x16汉字为例一列两字节共16列 for (i 0; i 16; i) { OLED_DrawDot(x i, y, font_hanzi[index][i * 2]); OLED_DrawDot(x i, y 1, font_hanzi[index][i * 2 1]); } // 根据需要继续绘制下半部分或调整页地址 }初始化和ASCII显示可以直接用现成HAL库代码但要注意几个要点SSD1306 I2C地址通常是0x3C有些模块是0x3D可以用I2C扫描确认I2C总线上最好有上拉电阻很多现成模块已经内置如果自己飞线记得加4.7k上拉初始化序列里必须要打开电荷泵0x8D, 0x14否则屏幕不亮。这部分代码网上一抓一大把关键是别把0x3C左移一位搞错在HAL里要用0x3C 1作为设备地址。2.2 独立按键用中断还是轮询按键这部分我见过太多人一上来就写外部中断然后在中断服务函数里延时消抖结果把系统卡死。我的观点是裸机小项目里按键完全可以用定时器轮询扫描10ms扫一次用状态机消抖效果稳定又不会阻塞主循环。简单来说读到的电平认为稳定后才算一次有效按键具体逻辑可以这样写uint8_t Key_Scan(void) { static uint8_t state 0; uint8_t press KEY_NONE; uint8_t level KEY_GET_LEVEL(); switch (state) { case 0: // 空闲 if (level PRESS) state 1; break; case 1: // 第一次确认按下 if (level PRESS) { press KEY_SHORT; state 2; } else state 0; break; case 2: // 等待释放 if (level RELEASE) state 0; break; default: state 0; break; } return press; }这个状态机的好处是天然消抖按键至少维持20ms以上才产生有效事件。如果还想做长按功能在状态2里加一个计时变量超过800ms就算长按。另外按键引脚要配置成内部上拉输入按下时接地为低电平省掉外部上拉电阻。注意不要把按键接在复用功能引脚上比如PA9、PA10这种串口引脚否则初始化串口后按键状态会乱。2.3 串口对接Air780E的接线与参数STM32和Air780E之间走的是标准UART我用USART1波特率1152008位数据、无校验、1位停止位。模组的AT指令都是\r\n结尾MCU发送指令时也要补\r\n这是新手最容易漏的细节——只发\n或者只发字符串不加回车模组根本不理会。接收侧我强烈建议用中断加环形缓冲区不要在串口中断里做复杂解析而是在主循环里从环形缓冲区取数据、逐条匹配响应。Air780E的响应有时候分成多帧到达比如先收到\r\n再收到如果每收到一个字节就急着处理很容易判断错误。用环形缓冲区把数据攒起来再按标志字符串匹配稳很多。串口初始化代码用STM32CubeMX生成即可记得把中断优先级设合理别和OLED的I2C中断抢资源。我实测下来模组刚上电时会有几秒初始化时间这个时候发AT指令可能没有响应所以MCU开机后先延时2到3秒再开始和模组握手。3. 短信发送核心Air780E的初始化与PDU编码3.1 上电初始化时序AT、CPIN、CSQ、CREG、CMGFAir780E上电后不能上来就发短信需要先做一套基础检测。我的初始化流程是这样的AT // 测试通信正常回OK ATE0 // 关闭指令回显减少串口数据干扰 ATCPIN? // 查询SIM卡状态期望返回 CPIN: READY ATCSQ // 查询信号质量例如 CSQ: 18,99 ATCREG? // 查询网络注册状态期望返回 CREG: 0,1 或 0,5 ATCMGF0 // 设置短信格式为PDU模式其中CSQ返回的第一个数字是信号强度范围0到31数值越大越好一般大于10才能比较可靠地发短信。如果是99说明根本没有信号常见原因是天线没接好或SIM卡欠费。CREG返回值里1表示已注册本地网络5表示已注册漫游网络这两个都算正常。如果返回2或3就要检查SIM卡和天线。这里有个隐藏问题ATCMGF0在部分固件里默认就是PDU模式但显式设置一次没坏处。发送中文短信必须用PDU模式Text模式对中文支持很弱尤其是通过UART交互时容易乱码。我建议初始化完成后先发一条英文测试短信或查询指令验证链路再进入正式发送流程。3.2 PDU模式与中文编码原理PDU模式本质上是一串十六进制字符串里面包含了短信中心地址、目标号码、编码方式、用户内容等信息。中文短信用的是UCS2编码也就是每个汉字占2个字节在PDU字符串里表现为4个十六进制字符。比如“温度过高”这4个字UCS2编码就是6E295EA68FC79AD8。在实际开发中我不建议手算UCS2码太容易出错。我的做法是先用电脑工具生成好再硬编码到STM32里。Python一行命令就能搞定python3 -c print(温度过高.encode(utf-16-be).hex())输出6e295ea68fc79ad8直接粘贴到代码里的const char *content_hex就行。如果项目里要做动态编辑短信内容那就需要在MCU端维护GB2312到UCS2的转换表代码量会大很多。对于按键触发固定模板这种需求预生成UCS2是最省事、最可靠的做法。3.3 组装PDU报文逐字段拆解以手机号13800138000、内容“温度过高”为例组装一条完整的PDU。先把手机号加上86国家码变成8613800138000共13位奇数位需要补F凑成偶数然后每两位交换顺序得到683108108300F0。这条短信的PDU字符串为0011000D91683108108300F0000800086E295EA68FC79AD8逐段拆开看字段值含义0000短信中心长度00表示使用SIM卡默认短信中心1111SMS-SUBMIT类型带状态报告请求0000消息参考号MR0D0D目标号码长度13位数字9191号码格式91表示国际格式86683108108300F0地址目标号码8613800138000反序编码0000协议标识PID0808编码方案DCS08表示UCS20000有效期VP0808用户数据长度UDL8字节6E295EA68FC79AD8UDUCS2编码的中文内容这一步是最容易出错的。手机号反序编码很多人写不对我的建议是写一个小函数处理而不是手动拼字符串。目标号码长度字段是十进制的13写成十六进制就是0D。UDL是字节数不是汉字个数4个汉字是8写成08。这些细节一个错了模组要么直接报ERROR要么短信发出去是乱码。3.4 STM32端发送函数与应答判断组装好PDU字符串后发送流程是这样的先计算ATCMGS后面的长度。这个长度是PDU十六进制字符串转换成字节后的总字节数减1也就是去掉第一个短信中心长度字节后的长度。如果不懂原理直接按这个公式在代码里算uint16_t total_bytes strlen(pdu_hex) / 2; uint16_t cmgs_len total_bytes - 1; sprintf(cmd, ATCMGS%d\r\n, cmgs_len);把ATCMGS23\r\n发给模组等待模组返回提示符。收到后立即发送PDU字符串不加回车不空格最后发送十六进制0x1A表示结束。等待模组返回CMGS: 序号和OK或者返回ERROR。我用的是USART_SendString加USART_WaitResponse配合超时的思路。等待提示符时不能简单用strstr找因为模组返回的是\r\n带一个空格需要在环形缓冲区里做非精确匹配。发送0x1A时也要小心有些串口助手里0x1A是特殊字符但MCU直接用数组发送没这问题。等待响应时我给了5秒超时超时就认定发送失败OLED显示失败状态。这里再加一个细节如果PDU里短信中心留空首字节00发送失败可以改成显式填写短信中心号码。中国移动的短信中心PDU前缀是0891683108200005F0这是经过反序编码的不同地区可能不同建议直接咨询运营商。在实际项目中我遇到过SIM卡默认短信中心和PDU格式不兼容的情况显式填短信中心后问题就解决了。4. 按键操作流程与OLED状态联动4.1 用状态机管理“选模板-确认-发送-反馈”整机操作流程要设计得足够简单否则现场人员不会用。我把界面逻辑分成六个状态待机、模板选择、确认发送、发送中、发送成功、发送失败。按键就三个K1向上切换模板、K2确认/发送、K3返回。状态机主体大概是这样的typedef enum { ST_STANDBY, ST_TEMPLATE_SELECT, ST_CONFIRM, ST_SENDING, ST_OK, ST_FAIL } app_state_t; void App_StateMachine(void) { uint8_t key Key_Scan(); switch (app_state) { case ST_STANDBY: if (key KEY_K2) app_state ST_TEMPLATE_SELECT; break; case ST_TEMPLATE_SELECT: if (key KEY_K1) { template_index (template_index 1) % 3; } if (key KEY_K2) app_state ST_CONFIRM; if (key KEY_K3) app_state ST_STANDBY; break; case ST_CONFIRM: if (key KEY_K2) { Start_AT_Send(); app_state ST_SENDING; } if (key KEY_K3) app_state ST_STANDBY; break; case ST_SENDING: if (send_finished) { app_state send_result ? ST_OK : ST_FAIL; } break; case ST_OK: case ST_FAIL: // 停留3秒后自动回待机 if (status_timeout) app_state ST_STANDBY; break; } }为什么一定要加“确认发送”这一步因为短信发送是有资费成本的按键误触一次就发一条很烦人。三步操作虽然多一步但能避免大量误发。我在待机界面上还会显示当前信号强度信号差时直接提示“信号弱”让用户心里有数。4.2 按键驱动与应用逻辑解耦很多项目写到最后按键扫描函数里直接调用短信发送函数看着能跑实际上代码耦合严重后面要加功能、改界面牵一发动全身。我的做法是把按键事件抽象出来Key_Scan()只负责返回KEY_NONE/KEY_K1/KEY_K2/KEY_K3至于按下去做什么由上层状态机决定。这样设计的好处是后面把按键换成触摸、改成矩阵键盘只需要改Key_Scan()内部实现上层逻辑完全不用动。同样Air780E的AT发送函数也只暴露一个Start_AT_Send()接口内部自己维护PDU组装、串口发送、超时判断上层关心的只是send_finished和send_result两个标志。这种分层在写毕业设计和实际产品时都很重要。毕业设计答辩时老师会问“如果按键连发怎么办”“如果模组无响应怎么办”如果代码本身就是分层结构回答起来会很清晰按键有消抖状态机模组有超时判断状态机有超时回待机逻辑这些都是设计好的容错机制。4.3 OLED界面刷新策略OLED刷新如果漫无目的地全屏清会闪得厉害尤其发送中状态需要持续显示动态效果。我的策略是把屏幕分成三个区域顶部状态栏、中部内容区、底部提示栏。状态栏常显“信号格SIM状态”内容区只刷菜单或结果底部提示栏显示当前按键操作。发送中状态的“动画”我用两个方式实现一是“SENDING”文字闪烁每隔300ms反色显示一次让用户知道系统在干活二是在发送成功或失败后全屏显示大号“成功”或“失败”停留3秒再回待机。局部刷新用OLED_ClearArea(x, y, w, h)只清理改动的矩形区域然后重新画对应内容。这样屏幕视觉上干净稳定不会像老式计算器一样闪个不停。状态栏的信号显示我做了个简单的柱状图CSQ大于12显示满格8到12显示三格4到7显示两格低于4显示一格。这个细节看起来不起眼但在实际使用中非常有用放在楼宇里测试时信号多少一目了然省得拿AT指令一点点查。5. 实测中常见的坑与排查手册5.1 OLED不亮、花屏的快速定位OLED不亮先别怀疑屏坏了按顺序排查供电是不是3.3VSDA和SCL有没有接反I2C地址是0x3C还是0x3D初始化序列有没有开电荷泵。我遇到过最奇葩的情况是模块背面有焊锡短路导致SDA一直被拉低整个I2C总线卡死。另外用HAL库时I2C地址要左移一位很多人直接传0x3C结果通信失败。花屏通常和电源质量有关。STM32板子和OLED共用同一个3.3V供电如果电源纹波大或者电流不够OLED显示就会出现雪花点、字符残缺。解决方法是给OLED供电并一个大一点的电容比如100uF同时在I2C线上确认上拉电阻是否到位。还有一点OLED的初始化序列必须在主循环跑起来之前完成不要把它放在被频繁执行的函数里重复初始化否则屏幕会周期性闪烁甚至变花。5.2 短信发不出去或者乱码的排查短信发不出去十有八九是下面几个原因PDU长度计算错误、短信中心为空导致运营商不接收、SIM卡没有网络注册成功、目标号码编码错误。PDU长度错误的最典型表现是模组收到ATCMGSxx后不回而是直接回ERROR。这时候把PDU字符串用电脑端串口助手手动发送一遍比对结果就能确认是不是MCU组帧的问题。发出去但内容是乱码几乎可以肯定是UCS2编码和DCS字段不匹配。检查一下PDU里DCS是不是08用户数据段是不是每个汉字对了4个十六进制字符。还有一个常见坑如果短信内容里混了英文和中文UDL按字节算容易出错很多项目里英文用了ASCII编码、中文用了UCS2DCS却统一写成08结果整条短信解析失败。我的建议是短信内容要么全中文、要么全英文DCS固定为08最省心。5.3 串口AT无响应或响应乱码串口无响应前先做最小验证拿USB转TTL直接接Air780E在电脑串口助手上手动敲AT、回车看能不能回OK。如果能问题就在STM32到模组的连接上。检查串口TX/RX有没有交叉GND有没有共地波特率是不是一致。Air780E上电后前几秒是不理人的MCU开机后延时2到3秒再发AT否则第一条指令必定丢失。响应乱码多半是波特率不匹配或者地线接触不良。我遇到过用劣质杜邦线导致信号畸变现象是能收到OK但里面夹着乱码字节换短线、重新插拔后恢复正常。如果模组的固件版本不同部分指令的返回格式会有细微差异比如有的版本ATCSQ返回CSQ: 18,99有的版本多几个空格匹配关键词时建议只匹配CSQ不要匹配完整字符串。5.4 按键抖动、误触发和状态乱跳按键问题最隐蔽的是引脚悬空。STM32内部上拉在大部分情况下够用但如果按键线比较长或者环境电磁干扰大建议外部再加一个10k上拉电阻到3.3V并且并一个0.1uF电容滤掉高频毛刺。软件消抖方面不要在按键扫描函数里用HAL_Delay(20)那样会阻塞串口接收和OLED刷新。用我前面写的状态机消抖放在10ms定时器中断里扫描效果远好于简单延时。状态乱跳还有一种情况是按键释放检测不严谨。比如手一抖一次按击被拆成两次短按状态机从“确认发送”直接跳回“待机”。我的解决办法是状态2里加释放确认没有检测到释放就不产生下一次按键事件。长按功能也要谨慎如果和短按共用中键最好等释放后再判断否则容易误触发。5.5 常见问题速查表现象可能原因处理办法OLED完全不亮I2C地址错误、电荷泵未开启、SDA/SCL接反先跑I2C扫描确认地址后补初始化电荷泵OLED花屏电源纹波大、I2C上拉缺失并100uF电容检查4.7k上拉AT无响应串口TX/RX接反、没共地、波特率不对USB转TTL单独验证模组再查接线CPIN返回ERRORSIM卡没插好、卡槽氧化重新插卡检查卡座弹片短信发不出PDU长度错误、网络未注册电脑串口助手手动组帧测试收到的短信乱码DCS字段和实际编码不匹配检查DCS是否为08UDL按字节算按键误触发引脚悬空、消抖不彻底外部上拉电容用状态机消抖这几个月做下来我个人最大的体会是这种STM32加4G模组的小项目调试顺序非常重要。先把OLED点亮再把按键扫描跑顺然后用USB转TTL单独把Air780E的AT流程摸透最后才把它们接到一起。很多人的问题出在一开始就把所有外设堆在一起一旦出错根本分不清是屏幕问题、按键问题还是模组问题。串口这个环节尤其建议先脱离STM32单独验证等PC串口助手能稳定发出中文短信后再移植到MCU代码里成功率会高很多。这套骨架后面也很容易扩展比如加个GPS模块上报位置、加个温湿度传感器把实时数据拼进短信内容、做一个多级联系人轮流发送都是在现有状态机上加分支的事。