☰
STM32+Air780E实战:按键触发中文短信发送与OLED状态显示
2026/10/3 7:10:24 网站建设 项目流程

1. 项目缘起与整体设计思路

按键发短信这件事,听起来像是二十年前的玩法,但在嵌入式圈子里,它一直是个非常经典的练手项目。原因很简单:它把GPIO输入检测、I2C显示驱动、UART串口通信、AT指令交互、PDU编码这几个嵌入式开发中最常见的知识点串成了一条完整的链路。你把这套东西跑通一遍,基本就摸清了单片机跟通信模组打交道的核心套路。

我这次用的是STM32F103C8T6加上Air780E模组。Air780E是合宙推出的一款Cat.1通信模组,支持4G全网通,走的是UART串口跟主控通信,用标准AT指令控制。相比早期的2G模组,它的网络覆盖和稳定性都好不少,而且价格也不贵,拿来练手非常合适。OLED用的是0.96寸的SSD1306,I2C接口,四根线就能点亮,显示状态信息足够用了。

整个项目的功能目标很明确:按下一个按键,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共地

按键另一端接GND,PA0配置为上拉输入,按下时读到低电平。10kΩ上拉电阻可以省掉,因为STM32内部有上拉,但如果你发现按键抖动严重,外接一个10kΩ上拉会更稳。

Air780E的UART波特率默认是115200,STM32的USART1也配置成115200、8位数据位、1位停止位、无校验。这里有个细节:Air780E的TX引脚输出是1.8V电平,而STM32的RX引脚识别高电平阈值是0.7×VDD=2.31V(VDD=3.3V时)。严格来说1.8V不够,但实测下来大多数情况下STM32能识别到,如果你遇到通信不稳定,加一个电平转换模块更保险。

提示:接线完成后,先不要急着写代码。用USB转TTL模块直接连接Air780E,在电脑串口助手里发送“AT”测试模组是否正常响应。这一步能帮你排除硬件问题,省去后面大量调试时间。

3. AT指令与PDU编码核心解析

3.1 Air780E短信相关AT指令流程

Air780E发送短信的AT指令流程不算复杂,但顺序不能乱。我把它拆成几个阶段:

第一阶段:基础检查

AT // 测试模组是否响应 AT+CPIN? // 查询SIM卡状态 AT+CSQ // 查询信号质量 AT+CREG? // 查询网络注册状态

AT+CPIN?返回+CPIN: READY说明SIM卡正常。AT+CSQ返回的第一个数字是信号强度,范围0-31,越大越好,低于10基本没法用。AT+CREG?返回+CREG: 0,1或+CREG: 0,5都表示已注册上网络。

第二阶段:配置短信参数

AT+CMGF=0 // 设置为PDU模式 AT+CSCS="GSM" // 字符集设置为GSM AT+CSMP=17,167,0,8 // 设置短信参数

这里重点说AT+CMGF=0。短信有两种模式:Text模式和PDU模式。Text模式看起来简单,但发中文会乱码,因为Text模式默认只支持ASCII字符。PDU模式虽然编码麻烦,但支持中文、长短信、状态报告等完整功能。所以做中文短信,PDU模式是唯一选择。

AT+CSMP=17,167,0,8这个参数里,最后一个“8”表示短信编码方式为UCS2(即Unicode),这是中文短信必须的设置。17表示短信的默认编码格式,167是协议标识,0是编码方式。

第三阶段:发送短信

AT+CMGS=<长度> // 长度是PDU串去掉中心号码后的字符数 > <PDU串> // 收到“>”提示后输入PDU串 <Ctrl+Z> // 发送

模组返回+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[i+1]; dst[i+1] = src[i]; } }

中文转UCS2需要一张Unicode映射表,或者用GB2312到Unicode的转换表。我图省事,直接在代码里硬编码了常用汉字的Unicode值,因为项目里短信内容就那么几条固定的。如果你需要发任意中文,建议移植一个GB2312到Unicode的转换表,大概几百KB的Flash空间。

注意:PDU串的长度计算要准确。AT+CMGS=后面的长度参数是PDU串中除去短信中心号码部分后的字符数(两个十六进制字符算一个字节)。算错了模组会返回ERROR,而且不告诉你具体哪里错了,非常难排查。

4. 软件架构与关键代码实现

4.1 整体软件流程设计

软件部分我分成四个模块:OLED显示驱动、按键检测、AT指令收发、PDU编码。主循环的逻辑很直接:

  1. 初始化OLED、USART1、I2C1、GPIO
  2. 显示“系统初始化中”
  3. 依次发送AT指令检查模组状态
  4. 显示“就绪,等待按键”
  5. 检测按键是否按下
  6. 按下后执行发送流程,OLED同步更新状态
  7. 发送完成后回到等待状态

这个流程里,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地址通常是0x78(8位地址)或0x3C(7位地址),HAL库用的是左移一位的8位地址,所以填0x78。

第二阶段:串口通信测试

用USB转TTL连接Air780E,在电脑上确认模组能正常响应AT指令。然后把Air780E接到STM32上,写一个透传程序,STM32收到什么就转发到电脑串口,电脑发什么就转发给Air780E。这样你能在电脑上看到模组的回复,确认STM32和模组之间的通信正常。

第三阶段:AT指令流程验证

在透传模式下,手动在电脑上发送AT指令,走一遍完整的短信发送流程。确认PDU串正确、模组能成功发出短信。这一步过了,说明你的PDU编码逻辑是对的。

第四阶段:整合代码

把OLED显示、按键检测、AT指令收发整合到一起,加上状态机逻辑。这个阶段主要调试的是状态切换和超时处理。

5.2 实测记录与参数调整

我在实测中遇到几个问题,记录一下:

第一个问题是模组注册网络慢。第一次上电后,AT+CREG?返回+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编码设置AT+CSMP最后参数为8

5.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, "AT+CMGS=%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的供电范围)。

AT+CPIN?返回ERROR

说明SIM卡没识别到。检查卡座是否接触良好,SIM卡是否插反,卡是否欠费。物联网卡有时候需要先激活才能用,联系运营商确认一下。

AT+CSQ返回的信号值很低

信号值低于10的话,短信大概率发不出去。换个位置,或者接一根外置天线。Air780E的板载天线效果一般,如果项目对信号要求高,建议换成外置棒状天线。

6.2 PDU编码类问题

短信发送后对方收到乱码

99%是编码方式没设对。确认AT+CSMP=17,167,0,8这条指令执行了,最后的“8”就是UCS2编码。另外确认PDU串里的编码方式字节也是“08”。

AT+CMGS返回ERROR

最常见的原因是PDU长度算错了。长度参数是PDU串中用户数据部分的字节数,不是整个PDU串的长度。比如你的PDU串是0011000D9168310801380000AA044F60597D,那么用户数据是4F60597D,长度是4个字节,AT+CMGS=4。注意这里说的是“字节数”,但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指令的响应格式可能有细微差异。如果发现手册上的指令跟实际返回不一致,先查一下固件版本(AT+CGMR),然后找对应版本的AT指令手册。合宙的官网有各个版本的文档,别拿旧手册调新固件。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询