
1. 项目缘起与整体设计思路按键发短信这件事听起来像是二十年前的玩法但在嵌入式圈子里它一直是个非常经典的练手项目。原因很简单它把GPIO输入检测、I2C显示驱动、UART串口通信、AT指令交互、PDU编码这几个嵌入式开发中最常见的知识点串成了一条完整的链路。你把这套东西跑通一遍基本就摸清了单片机跟通信模组打交道的核心套路。我这次用的是STM32F103C8T6加上Air780E模组。Air780E是合宙推出的一款Cat.1通信模组支持4G全网通走的是UART串口跟主控通信用标准AT指令控制。相比早期的2G模组它的网络覆盖和稳定性都好不少而且价格也不贵拿来练手非常合适。OLED用的是0.96寸的SSD1306I2C接口四根线就能点亮显示状态信息足够用了。整个项目的功能目标很明确按下一个按键STM32通过AT指令控制Air780E发送一条中文短信到指定手机号同时OLED屏幕上实时显示当前的操作状态——比如“正在初始化”“正在发送”“发送成功”或者“发送失败”。这个过程中中文短信的PDU编码是最容易踩坑的地方后面我会详细拆解。为什么选这个方案而不是其他我考虑过几个点。第一Air780E的AT指令集比较规范文档也齐全遇到问题容易排查。第二STM32F103C8T6是最经典的入门芯片资料多、成本低HAL库开发效率高。第三OLED用I2C而不是SPI接线更简单虽然刷新率低一些但显示几行状态文字完全够用。第四按键直接用GPIO轮询加软件消抖没必要上中断因为发短信本身是个低频操作不需要毫秒级响应。这个项目适合谁如果你已经点亮过LED、用过UART串口打印调试信息那就可以直接上手。如果你连I2C和UART都还没碰过建议先把这两个外设玩明白再来看这个项目不然调试起来会比较痛苦。注意Air780E需要插入有效的SIM卡才能正常工作建议使用已激活的物联网卡或普通手机卡确保卡内有余额或流量套餐支持短信功能。2. 硬件选型与接线方案详解2.1 核心器件清单与选型理由先把物料清单列清楚方便你对照准备器件型号数量备注主控芯片STM32F103C8T61最小系统板即可通信模组Air780E1合宙Cat.1模组显示屏0.96寸OLED SSD13061I2C接口4针按键轻触按键16x6mm常规款电阻10kΩ1按键上拉用电阻1kΩ1限流保护电容100μF1电源滤波电源5V/2A1给模组供电Air780E对供电的要求比较高峰值电流可以到2A左右。如果你直接用STM32板子上的3.3V或者5V引脚给它供电大概率会出现模组反复重启或者注册网络失败的情况。我的做法是单独用一个5V/2A的电源适配器给Air780E供电STM32用另一路5V供电两边共地就行。OLED选0.96寸I2C版本是因为它只需要两根信号线SCL和SDA加上VCC和GND一共四根线。市面上有些0.9寸的OLED对I2C时序兼容性不太好我实测过几款0.96寸的SSD1306最稳定驱动库也最成熟。2.2 接线方案与注意事项接线这块我整理了一个对照表照着接就行STM32引脚连接目标说明PA9 (USART1_TX)Air780E RX串口发送PA10 (USART1_RX)Air780E TX串口接收PB6 (I2C1_SCL)OLED SCLI2C时钟PB7 (I2C1_SDA)OLED SDAI2C数据PA0按键一端按键输入3.3VOLED VCC显示屏供电GNDOLED GND / 按键另一端 / 模组GND共地按键另一端接GNDPA0配置为上拉输入按下时读到低电平。10kΩ上拉电阻可以省掉因为STM32内部有上拉但如果你发现按键抖动严重外接一个10kΩ上拉会更稳。Air780E的UART波特率默认是115200STM32的USART1也配置成115200、8位数据位、1位停止位、无校验。这里有个细节Air780E的TX引脚输出是1.8V电平而STM32的RX引脚识别高电平阈值是0.7×VDD2.31VVDD3.3V时。严格来说1.8V不够但实测下来大多数情况下STM32能识别到如果你遇到通信不稳定加一个电平转换模块更保险。提示接线完成后先不要急着写代码。用USB转TTL模块直接连接Air780E在电脑串口助手里发送“AT”测试模组是否正常响应。这一步能帮你排除硬件问题省去后面大量调试时间。3. AT指令与PDU编码核心解析3.1 Air780E短信相关AT指令流程Air780E发送短信的AT指令流程不算复杂但顺序不能乱。我把它拆成几个阶段第一阶段基础检查AT // 测试模组是否响应 ATCPIN? // 查询SIM卡状态 ATCSQ // 查询信号质量 ATCREG? // 查询网络注册状态ATCPIN?返回CPIN: READY说明SIM卡正常。ATCSQ返回的第一个数字是信号强度范围0-31越大越好低于10基本没法用。ATCREG?返回CREG: 0,1或CREG: 0,5都表示已注册上网络。第二阶段配置短信参数ATCMGF0 // 设置为PDU模式 ATCSCSGSM // 字符集设置为GSM ATCSMP17,167,0,8 // 设置短信参数这里重点说ATCMGF0。短信有两种模式Text模式和PDU模式。Text模式看起来简单但发中文会乱码因为Text模式默认只支持ASCII字符。PDU模式虽然编码麻烦但支持中文、长短信、状态报告等完整功能。所以做中文短信PDU模式是唯一选择。ATCSMP17,167,0,8这个参数里最后一个“8”表示短信编码方式为UCS2即Unicode这是中文短信必须的设置。17表示短信的默认编码格式167是协议标识0是编码方式。第三阶段发送短信ATCMGS长度 // 长度是PDU串去掉中心号码后的字符数 PDU串 // 收到“”提示后输入PDU串 CtrlZ // 发送模组返回CMGS: 消息参考号表示发送成功返回ERROR就是失败了。3.2 中文短信PDU编码原理与实操PDU编码是整个项目里最容易卡住的地方。我一开始也在这上面折腾了很久后来把逻辑理清楚了就简单了。PDU串的结构是这样的00 // 短信中心号码长度用00表示使用默认 11 // 短信中心号码类型 00 // 目标号码长度 91 // 目标号码类型91表示国际号码 目标号码 // 经过半字节交换的号码 00 // 协议标识 08 // 编码方式08表示UCS2 有效期 // 一般用AA 短信长度 // 用户数据的字节数 用户数据 // UCS2编码的中文内容目标号码的处理有个“半字节交换”的规则。比如手机号是13800138000先在前面加“86”中国区号变成8613800138000然后每两个数字交换位置68 31 08 10 83 00 00最后如果位数是奇数就补“F”。这个交换规则是PDU协议规定的不交换的话号码就错了。中文内容的UCS2编码就是把每个汉字转成4位十六进制的Unicode码。比如“你好”两个字的Unicode是4F60597D直接拼上去就行。在STM32上实现这个编码我写了一个函数输入手机号和中文内容输出完整的PDU串。核心逻辑是// 将单个字符转换为十六进制字符串 void char2hex(char c, char *hex) { if(c 0 c 9) { hex[0] 0 (c - 0) / 10; hex[1] 0 (c - 0) % 10; } else if(c A c F) { hex[0] 0 (c - A 10) / 10; hex[1] 0 (c - A 10) % 10; } } // 半字节交换 void swap_nibbles(char *src, char *dst, int len) { for(int i 0; i len; i 2) { dst[i] src[i1]; dst[i1] src[i]; } }中文转UCS2需要一张Unicode映射表或者用GB2312到Unicode的转换表。我图省事直接在代码里硬编码了常用汉字的Unicode值因为项目里短信内容就那么几条固定的。如果你需要发任意中文建议移植一个GB2312到Unicode的转换表大概几百KB的Flash空间。注意PDU串的长度计算要准确。ATCMGS后面的长度参数是PDU串中除去短信中心号码部分后的字符数两个十六进制字符算一个字节。算错了模组会返回ERROR而且不告诉你具体哪里错了非常难排查。4. 软件架构与关键代码实现4.1 整体软件流程设计软件部分我分成四个模块OLED显示驱动、按键检测、AT指令收发、PDU编码。主循环的逻辑很直接初始化OLED、USART1、I2C1、GPIO显示“系统初始化中”依次发送AT指令检查模组状态显示“就绪等待按键”检测按键是否按下按下后执行发送流程OLED同步更新状态发送完成后回到等待状态这个流程里AT指令的收发是同步阻塞的。也就是说发一条指令后等模组回复收到回复或者超时了再发下一条。这样做的好处是逻辑简单不容易出错。缺点是发送过程中按键没响应但发短信本来就是个几秒钟的操作用户不会在意。4.2 OLED状态显示驱动实现OLED我用的是HAL库加软件I2C的方式。为什么不用硬件I2C因为STM32F103的硬件I2C有历史遗留的稳定性问题虽然新版HAL库改善了很多但软件I2C更可控引脚随便选移植也方便。软件I2C的核心就是控制SCL和SDA两根线的电平#define OLED_SCL_PIN GPIO_PIN_6 #define OLED_SCL_PORT GPIOB #define OLED_SDA_PIN GPIO_PIN_7 #define OLED_SDA_PORT GPIOB void I2C_Start(void) { SDA_HIGH(); SCL_HIGH(); delay_us(2); SDA_LOW(); delay_us(2); SCL_LOW(); delay_us(2); } void I2C_WriteByte(uint8_t data) { for(int i 0; i 8; i) { if(data 0x80) SDA_HIGH(); else SDA_LOW(); SCL_HIGH(); delay_us(2); SCL_LOW(); delay_us(2); data 1; } }OLED显示状态信息我定义了四个状态IDLE就绪、SENDING发送中、SUCCESS成功、FAIL失败。每个状态对应屏幕上不同的显示内容。SSD1306的显存是128x64位我用的8x16字体一行能显示16个字符总共能显示4行。显示“发送成功”的时候我还会在第二行显示发送时间戳方便确认操作记录。时间戳用STM32的SysTick计数换算成秒数虽然不精确但够用了。4.3 按键检测与消抖处理按键检测看起来简单但实际做的时候有几个坑。第一个是抖动机械按键按下和松开的时候会有几十毫秒的电平抖动不处理的话一次按下会被识别成多次。第二个是长按如果用户按住不放代码会一直触发发送。我的处理方式是检测到低电平后延时20ms再检测一次如果还是低电平就确认按下。然后等待按键松开松开后再延时20ms确认。这样一次按下只触发一次操作。uint8_t Key_Scan(void) { if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET) { HAL_Delay(20); if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET) { while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET); HAL_Delay(20); return 1; } } return 0; }这个函数放在主循环里轮询返回1表示有一次有效的按键按下。注意HAL_Delay在中断里不能用但主循环里没问题。4.4 AT指令收发与超时处理AT指令的收发我封装了两个函数一个发送指令一个等待预期回复。void AT_SendCmd(char *cmd) { HAL_UART_Transmit(huart1, (uint8_t *)cmd, strlen(cmd), 1000); HAL_UART_Transmit(huart1, (uint8_t *)\r\n, 2, 100); } uint8_t AT_WaitResponse(char *expected, uint32_t timeout) { uint8_t buf[256]; uint32_t start HAL_GetTick(); uint16_t idx 0; while((HAL_GetTick() - start) timeout) { if(HAL_UART_Receive(huart1, buf[idx], 1, 10) HAL_OK) { idx; if(idx sizeof(buf) - 1) break; buf[idx] \0; if(strstr((char *)buf, expected) ! NULL) { return 1; } } } return 0; }这里有个细节HAL_UART_Receive的超时设成10ms这样每次循环不会卡太久。整个等待过程有个总超时比如发送短信我给30秒因为网络不好的时候模组可能需要更长时间。提示Air780E的回复有时候会分多次到达比如先回一个\r\n再回OK\r\n。所以接收缓冲区要足够大而且要用strstr做子串匹配不能要求完全相等。5. 实操过程与调试记录5.1 分阶段调试步骤我强烈建议不要一次性把代码写完再调试而是分阶段来每个阶段验证通过再进入下一个。第一阶段OLED点亮先写一个最简单的OLED测试程序在屏幕上显示“Hello”。如果显示正常说明I2C通信没问题。如果花屏或者不亮检查接线和I2C地址。SSD1306的I2C地址通常是0x788位地址或0x3C7位地址HAL库用的是左移一位的8位地址所以填0x78。第二阶段串口通信测试用USB转TTL连接Air780E在电脑上确认模组能正常响应AT指令。然后把Air780E接到STM32上写一个透传程序STM32收到什么就转发到电脑串口电脑发什么就转发给Air780E。这样你能在电脑上看到模组的回复确认STM32和模组之间的通信正常。第三阶段AT指令流程验证在透传模式下手动在电脑上发送AT指令走一遍完整的短信发送流程。确认PDU串正确、模组能成功发出短信。这一步过了说明你的PDU编码逻辑是对的。第四阶段整合代码把OLED显示、按键检测、AT指令收发整合到一起加上状态机逻辑。这个阶段主要调试的是状态切换和超时处理。5.2 实测记录与参数调整我在实测中遇到几个问题记录一下第一个问题是模组注册网络慢。第一次上电后ATCREG?返回CREG: 0,2正在搜索网络持续了大概15秒才变成CREG: 0,1。所以初始化代码里要加一个循环等待每2秒查一次最多等30秒。第二个问题是短信发送成功后模组返回的CMGS: 15里的数字是消息参考号不是错误码。我一开始以为15是错误查了手册才知道是正常的。第三个问题是OLED在发送过程中刷新太频繁导致闪烁。后来改成只在状态变化时刷新而不是每次循环都刷新就稳定了。问题现象原因解决方法模组反复重启供电不足单独5V/2A供电AT指令无回复TX/RX接反交换TX和RX接线短信发送ERRORPDU长度算错重新计算CMGS参数OLED花屏I2C地址错误改为0x78按键触发多次抖动未处理加20ms延时消抖中文显示乱码未用UCS2编码设置ATCSMP最后参数为85.3 完整发送流程的代码逻辑把整个发送流程串起来主循环里大概是这样while(1) { if(Key_Scan()) { OLED_ShowString(0, 0, Sending...); // 检查模组状态 if(!AT_CheckModule()) { OLED_ShowString(0, 2, Module Error); continue; } // 构建PDU串 BuildPDU(13800138000, 测试短信, pdu_buf); // 发送短信 char cmd[32]; sprintf(cmd, ATCMGS%d, pdu_len); AT_SendCmd(cmd); if(AT_WaitResponse(, 5000)) { HAL_UART_Transmit(huart1, (uint8_t *)pdu_buf, strlen(pdu_buf), 5000); HAL_UART_Transmit(huart1, (uint8_t *)\x1A, 1, 1000); if(AT_WaitResponse(CMGS, 30000)) { OLED_ShowString(0, 2, Send OK); } else { OLED_ShowString(0, 2, Send Fail); } } HAL_Delay(3000); OLED_Clear(); OLED_ShowString(0, 0, Ready); } }这段代码里BuildPDU函数负责把手机号和中文内容编码成PDU串pdu_len是计算出来的长度。发送完成后延时3秒再回到就绪状态让用户能看到结果。6. 常见问题排查与避坑经验6.1 AT指令交互类问题模组不回复任何指令先检查波特率。Air780E默认115200但有些批次可能是9600。如果不确定可以尝试几个常见波特率。再检查TX/RX是否接反这个错误太常见了我至少犯过三次。最后检查供电用万用表量一下模组的VCC引脚确保在3.8V到4.2V之间Air780E的供电范围。ATCPIN?返回ERROR说明SIM卡没识别到。检查卡座是否接触良好SIM卡是否插反卡是否欠费。物联网卡有时候需要先激活才能用联系运营商确认一下。ATCSQ返回的信号值很低信号值低于10的话短信大概率发不出去。换个位置或者接一根外置天线。Air780E的板载天线效果一般如果项目对信号要求高建议换成外置棒状天线。6.2 PDU编码类问题短信发送后对方收到乱码99%是编码方式没设对。确认ATCSMP17,167,0,8这条指令执行了最后的“8”就是UCS2编码。另外确认PDU串里的编码方式字节也是“08”。ATCMGS返回ERROR最常见的原因是PDU长度算错了。长度参数是PDU串中用户数据部分的字节数不是整个PDU串的长度。比如你的PDU串是0011000D9168310801380000AA044F60597D那么用户数据是4F60597D长度是4个字节ATCMGS4。注意这里说的是“字节数”但PDU串里每个字节是用两个十六进制字符表示的所以实际字符数是8。中文短信内容超过70个字UCS2编码下一条短信最多70个字符每个汉字算一个字符。超过70个字需要拆分长短信PDU协议里有UDH用户数据头来处理。这个比较复杂建议先确保单条短信能发成功再研究长短信。6.3 硬件与显示类问题OLED显示内容闪烁不要在每次循环里都调用OLED刷新函数。SSD1306的显存写入需要时间频繁刷新会导致屏幕闪烁。正确的做法是只在状态变化时刷新或者用双缓冲机制。按键按下没反应先确认按键接线是否正确用万用表量一下按下时PA0是否接地。如果接线没问题检查代码里GPIO初始化是否正确配置为上拉输入。HAL库里的配置是GPIO_MODE_INPUT加GPIO_PULLUP。STM32和Air780E通信不稳定如果用的是杜邦线连接线太长或者接触不良都会导致通信错误。尽量用短一点的线或者直接焊在板子上。另外Air780E的TX输出是1.8V电平如果STM32识别不稳定加一个TXS0108E电平转换模块。提示调试的时候在STM32的UART接收中断里加一个LED翻转每收到一个字节翻转一次。这样你能直观地看到模组有没有回复数据比看串口打印快得多。7. 项目扩展与个人实操体会这个项目跑通之后可以往几个方向扩展。第一个是加一个RTC时钟模块在短信内容里带上时间戳这样每条短信都有时间记录。第二个是加一个温湿度传感器按键后把当前环境数据一起发出去做成一个简易的远程环境监测终端。第三个是把按键换成定时器触发每隔一小时自动发送一次状态短信做成一个定时上报设备。我在实际做这个项目的时候最大的体会是PDU编码一定要单独测试。不要把它跟AT指令流程混在一起调那样出了问题你根本不知道是编码错了还是指令流程错了。我的做法是在电脑上用Python写一个小脚本先把PDU串生成出来用串口助手手动发给模组确认能发出正确的中文短信后再把这段逻辑移植到STM32上。这样能省掉大量在单片机上反复烧录调试的时间。另一个体会是供电一定要重视。我一开始用STM32板子上的5V引脚给Air780E供电模组一注册网络就重启折腾了半天才想到是电流不够。后来单独用一个5V/2A的电源问题立刻消失。嵌入式项目里电源问题引起的故障至少占三成遇到莫名其妙的现象先查供电。最后分享一个小技巧Air780E的固件版本不同AT指令的响应格式可能有细微差异。如果发现手册上的指令跟实际返回不一致先查一下固件版本ATCGMR然后找对应版本的AT指令手册。合宙的官网有各个版本的文档别拿旧手册调新固件。