前言
本篇基于合宙Air780E 4G模组,搭配STM32F103C8T6单片机,实现按键触发发送中文短信功能。按下PB12按键,STM32通过串口向Air780E发送AT指令,采用PDU模式UCS2编码发送中文短信,OLED屏幕实时展示整个流程的工作状态。
重要说明:程序中只保存目标手机号,手机号不会在OLED屏幕上显示;模组返回SEND_OK仅代表短信已经提交运营商网络,不等于对方手机一定收到短信,最终接收情况以目标手机实际收到为准。
一、整体实现功能流程
整体业务逻辑:
- PB12按键按下,STM32完成按键消抖,识别按键按下事件
- STM32通过USART1串口和Air780E模组进行AT指令交互
- 依次检测SIM卡就绪状态、蜂窝网络注册状态
- 组装UCS2编码的PDU短信数据包,包含目标手机号与中文短信内容“测试”
- Air780E模组将短信提交给移动运营商网络,等待模组返回
+CMGS以及OK应答 - OLED屏幕展示短信发送成功/失败状态
简单概括:按键提供发送触发信号,STM32做逻辑控制、PDU编码,Air780E负责对接移动网络发送短信,OLED负责反馈运行状态。

二、硬件接线说明
⚠️注意:本文使用的是支持5V输入的Air780E开发板;裸模组VBAT引脚严禁接5V,裸模组供电范围3.3‑4.3V,串口IO电平为1.8V,还需要PWRKEY引脚完成开机,使用裸模组务必阅读官方硬件手册。
STM32 USART1固定引脚:PA9为TX发送、PA10为RX接收。模组开发板丝印TX接STM32的PA9,开发板RX接STM32的PA10,接线一定要区分模组丝印、STM32引脚、裸模组引脚三者,更换载板务必核对原理图。
| 模块或器件引脚 | STM32F103C8T6 引脚 | 功能说明 |
|---|---|---|
| Air780E开发板VCC | 5V | Air780E开发板供电(仅适配支持5V的载板) |
| Air780E开发板TX | PA9 | USART1发送 |
| Air780E开发板RX | PA10 | USART1接收 |
| Air780E开发板GND | GND | 单片机和模组共地,必须连接 |
| 按键KEY信号端 | PB12 | 按键输入,开启STM32内部上拉 |
| 按键另一端 | GND | 按键按下时PB12拉低 |
| OLED SCL | PB8 | 软件I2C时钟线 |
| OLED SDA | PB9 | 软件I2C数据线 |
| OLED VCC/GND | 5V/GND | OLED供电,与单片机共地 |
硬件关键点:共地非常重要! 如果STM32和Air780E不共地,串口通信会出现乱码、完全无应答问题。
三、工作原理讲解
Air780E属于蜂窝4G通信模组。STM32单片机本身不能直接连接运营商移动网络,依靠USART1串口下发AT指令控制Air780E模组。 模组依靠SIM卡完成运营商身份认证,搭配天线,在网络正常的情况下把短信提交给移动网络。
整体信号流向: 按键PB12 → STM32单片机 → USART1串口AT指令 → Air780E模组(SIM卡+天线)→ 运营商移动网络 → 目标接收手机 同时STM32驱动OLED,实时打印每一步工作状态。
这里有两套反馈信号,不要混淆:
- 串口AT反馈:模组通过串口返回OK、ERROR、CMS ERROR等应答,STM32可以立刻读取解析,SEND_OK就是来自这个反馈,代表短信已经交给运营商。
- 手机实际接收:短信经由运营商网络投递到目标手机,会存在网络延迟,本项目没有实现短信送达报告。
四、串口AT指令通信原理
串口参数配置:8位数据,无校验位,1停止位(8N1)。 程序优先以115200波特探测Air780E,若无应答,切换9600波特尝试通信。Air780E出厂默认波特率115200,串口不支持自适应波特率。
AT指令以\r回车作为命令结束标记,最简单的交互示例:
STM32发送: AT\r
模组回复: \r\nOK\r\n
工程使用USART1接收中断,接收到的字节存入环形缓冲区,主循环读取缓冲区解析应答内容,判断OK、错误码。短信发送过程是阻塞式,发送期间OLED显示SENDING...,等待AT交互完成后刷新最终状态。
本项目用到核心AT指令:
| AT指令 | 项目内作用 | 预期正常返回 |
|---|---|---|
| AT | 检测串口链路是否通畅 | OK |
| ATE0 | 关闭命令回显,简化应答解析 | OK |
| AT+CPIN? | 查询SIM卡状态 | +CPIN: READY |
| AT+CREG? | 查询电路域网络注册状态,发送短信以此为判断依据 | +CREG: 0,1 |
| AT+CEREG? | 查询分组域注册,仅用于故障诊断 | 返回注册状态码 |
| AT+CSQ | 获取信号强度,用于调试排查 | 返回RSSI信号编码值 |
| AT+CMGF=0 | 设置PDU短信编码模式 | OK |
| AT+CMGS=<长度> | 提交PDU短信 | 返回>提示符,后续输出+CMGS与OK |
注意:
+CSQ返回数值不能直接当成信号百分比;AT+CREG?和AT+CEREG?不能互相替代,短信发送判断只用CREG。
五、中文短信PDU编码原理
普通ASCII编码无法直接发送中文,本项目使用UCS2编码,DCS设置为0x08。 测试两个汉字编码转换:
| 汉字 | Unicode码点 | PDU内十六进制 |
|---|---|---|
| 测 | U+6D4B | 6D 4B |
| 试 | U+8BD5 | 8B D5 |
短信内容“测试”对应数据6D4B8BD5,一共4字节,用户数据长度填写04。
PDU数据包字段简单解析
00 | 11 | 00 | 0D | 91 | [目的号码编码] | 00 | 08 | A7 | 04 | 6D 4B 8B D5
00:短信中心地址长度,不计入AT+CMGS的长度参数11:短信提交类型,携带有效期00:消息参考号0D:代表目的号码是13位国际号码(国内手机号前面补86)91:代表国际号码格式- 目的号码编码:手机号数字两两翻转,奇数位末尾补F
00:协议标识08:DCS,代表UCS2中文编码A7:短信有效期24小时04:短信内容字节数6D4B8BD5:“测试”UCS2编码
重点:PDU最开头
00不算入AT+CMGS的长度,例如完整PDU一共20字节,代码中填写AT+CMGS=19。模组输出>提示符之后,发送PDU字符串,最后发送Ctrl+Z(0x1A)结束输入。
六、按键逻辑
PB12开启STM32内部上拉:松开按键引脚为高电平,按下接地引脚为低电平。 按键机械触点会抖动,代码做20ms软件消抖处理;程序只识别按键按下边沿,按住按键不会重复发送短信,松开之后再次按下才会重新触发发送流程。
发送短信之前会依次校验:串口AT通信正常、回显关闭成功、SIM就绪、网络注册成功、PDU模式设置成功。任意一步失败直接停止执行,OLED输出对应错误信息。短信发送函数执行期间不会响应新按键。
七、OLED屏幕状态说明
OLED屏幕只打印提示信息与运行状态,不会显示目标手机号。
| OLED显示内容 | 含义说明 |
|---|---|
| READY - PRESS KEY | 单片机初始化完成,等待按键;不等于模组网络就绪 |
| SENDING... | 正在执行AT查询,提交短信 |
| SEND OK | 模组返回+CMGS以及OK,短信提交网络成功 |
| NO MODEM RESPONSE | AT探测阶段完全收不到模组串口数据 |
| BAD AT RESPONSE | 收到串口数据,但是没有合法OK应答 |
| SIM NOT READY | AT+CPIN?查询SIM卡未就绪 |
| NETWORK NOT READY | 蜂窝网络没有注册成功 |
| NO SMS PROMPT | AT+CMGS指令没有收到>输入提示符 |
| SMS SEND FAILED | 超时或者模组返回发送错误 |
| CMS ERROR: n | 短信业务错误码,需要查阅Air780E官方手册排查 |
NETWORK NOT READY界面同时会打印CREG、EPS状态,NA代表读取不到有效数据。
八、源代码插
这里包含Air780E串口驱动、main主函数。
工程文件结构参考:
├─HARDWARE
│ ├─KEY
│ │ └─KEY.c/h
│ ├─AIR780E
│ │ └─AIR780E.c/h
│ └─OLED
│ └─OLED.c/h
└─USER
└─main.c
📝下方为完整源代码:
main.c代码
/* STM32F103C8T6 + AIR780E SMS teaching demonstration.
* Tested breakout wiring: board TX -> PA9 (USART1_TX),
* board RX -> PA10 (USART1_RX), KEY -> PB12 (active low).
* OLED SCL -> PB8, OLED SDA -> PB9. */
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "KEY.h"
#include "AIR780E.H"
#define TARGET_PHONE "19981770821"
#define TEST_TEXT_UCS2 "6D4B8BD5"
static void Demo_ShowStatus(const char *status, AIR780E_Result result);
static const char *Demo_ResultText(AIR780E_Result result);
int main(void)
{
u8 was_pressed = 0u;
u8 pressed;
AIR780E_Result result;
delay_init();
NVIC_Configuration();
OLED_Init();
OLED_ColorTurn(0u);
OLED_DisplayTurn(0u);
KEY_Init();
AIR780E_Init();
Demo_ShowStatus("READY - PRESS KEY", AIR780E_OK);
while (1)
{
pressed = ((KEY_ReadState() & KEY1_PRESSED) != 0u) ? 1u : 0u;
if (pressed != 0u && was_pressed == 0u) {
Demo_ShowStatus("SENDING...", AIR780E_OK);
result = AIR780E_SendSMS(TARGET_PHONE, TEST_TEXT_UCS2);
Demo_ShowStatus(Demo_ResultText(result), result);
}
was_pressed = pressed;
delay_ms(20u);
}
}
static const char *Demo_ResultText(AIR780E_Result result)
{
switch (result) {
case AIR780E_OK: return "SEND OK";
case AIR780E_NO_RESPONSE: return "NO MODEM RESPONSE";
case AIR780E_BAD_AT_RESPONSE: return "BAD AT RESPONSE";
case AIR780E_SIM_NOT_READY: return "SIM NOT READY";
case AIR780E_NETWORK_NOT_READY: return "NETWORK NOT READY";
case AIR780E_ECHO_CONFIG_ERROR: return "ECHO SET FAILED";
case AIR780E_PDU_MODE_ERROR: return "PDU MODE FAILED";
case AIR780E_CONFIG_ERROR: return "CONFIG FAILED";
case AIR780E_CHARSET_ERROR: return "CHARSET FAILED";
case AIR780E_SMS_PARAM_ERROR: return "SMS PARAM FAILED";
case AIR780E_NO_PROMPT: return "NO SMS PROMPT";
case AIR780E_SEND_ERROR: return "SMS SEND FAILED";
default: return "INVALID ARGUMENT";
}
}
static void Demo_ShowStatus(const char *status, AIR780E_Result result)
{
OLED_Clear();
OLED_ShowString(0u, 0u, (u8 *)"AIR780E SMS DEMO", 12u);
OLED_ShowString(0u, 13u, (u8 *)"KEY PB12 TO SEND", 12u);
OLED_ShowString(0u, 26u, (u8 *)status, 12u);
if (result == AIR780E_NO_RESPONSE || result == AIR780E_BAD_AT_RESPONSE) {
OLED_ShowString(0u, 39u, (u8 *)"UART1 PA9/PA10", 12u);
} else if ((result == AIR780E_SEND_ERROR || result == AIR780E_NO_PROMPT) &&
AIR780E_GetLastCMSError() != 0xFFFFu) {
OLED_ShowString(0u, 39u, (u8 *)"CMS ERROR: ", 12u);
OLED_ShowNum(66u, 39u, AIR780E_GetLastCMSError(), 3u, 12u);
} else if (result == AIR780E_NETWORK_NOT_READY) {
OLED_ShowString(0u, 39u, (u8 *)"CREG: ", 12u);
if (AIR780E_GetLastNetworkStatus() == 0xFFu) {
OLED_ShowString(36u, 39u, (u8 *)"NA", 12u);
} else {
OLED_ShowNum(36u, 39u, AIR780E_GetLastNetworkStatus(), 2u, 12u);
}
OLED_ShowString(66u, 39u, (u8 *)"EPS:", 12u);
if (AIR780E_GetLastEPSStatus() == 0xFFu) {
OLED_ShowString(96u, 39u, (u8 *)"NA", 12u);
} else {
OLED_ShowNum(96u, 39u, AIR780E_GetLastEPSStatus(), 2u, 12u);
}
}
if (result == AIR780E_NO_RESPONSE) {
OLED_ShowString(0u, 52u, (u8 *)"CHECK AT/BAUD", 12u);
} else if (result == AIR780E_BAD_AT_RESPONSE) {
OLED_ShowString(0u, 52u, (u8 *)"CHECK AT FIRMWARE", 12u);
} else {
OLED_ShowString(0u, 52u, (u8 *)"RELEASE TO RETRY", 12u);
}
OLED_Refresh();
}
AIR780E.C代码
/*
* Air780E AT 短信驱动。文件编码:UTF-8。
* USART1 中断将收到的字节放入环形缓冲区;主循环负责解析 AT 应答,
* 查询 SIM 与网络状态,并构造 UCS2 编码的短信 PDU。
*/
#include "AIR780E.H"
#include "delay.h"
#include "misc.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_usart.h"
#include <string.h>
#define AIR780E_DEFAULT_BAUD 115200u
#define AIR780E_REFERENCE_BAUD 9600u
#define AIR780E_RX_BUFFER_SIZE 256u
#define AIR780E_RESPONSE_SIZE 160u
#define AIR780E_PDU_BUFFER_SIZE 320u
#define AIR780E_AT_TIMEOUT_MS 1500u
#define AIR780E_SMS_TIMEOUT_MS 60000u
#define AIR780E_NETWORK_POLL_ATTEMPTS 40u
#define AIR780E_NETWORK_POLL_DELAY_MS 1500u
static volatile uint8_t air_rx_buffer[AIR780E_RX_BUFFER_SIZE];
static volatile uint16_t air_rx_head;
static volatile uint16_t air_rx_tail;
static volatile uint16_t air_probe_rx_count;
static volatile uint8_t air_probe_active;
static uint32_t air_baud_rate = AIR780E_DEFAULT_BAUD;
static uint16_t air_last_cms_error = 0xFFFFu;
static char air_last_response[AIR780E_RESPONSE_SIZE];
static uint8_t air_last_network_status = 0xFFu;
static uint8_t air_last_eps_status = 0xFFu;
static uint8_t air_last_signal_rssi = 0xFFu;
/* 从“+CMS ERROR:”后提取十进制错误码;无数字时返回 0xFFFF。 */
static uint16_t AIR780E_ParseErrorNumber(const char *value)
{
uint16_t number = 0u;
uint8_t has_digit = 0u;
while (*value == ' ') {
value++;
}
while (*value >= '0' && *value <= '9') {
has_digit = 1u;
number = (uint16_t)(number * 10u + (uint16_t)(*value - '0'));
value++;
}
return (has_digit != 0u) ? number : 0xFFFFu;
}
/* 从 +CREG/+CEREG 应答中读取注册状态,0xFF 表示解析失败。 */
static uint8_t AIR780E_ParseNetworkStatus(const char *response,
const char *prefix)
{
const char *value = response;
uint8_t status;
while ((value = strstr(value, prefix)) != 0) {
value += strlen(prefix);
while (*value == ' ') {
value++;
}
while (*value >= '0' && *value <= '9') {
value++;
}
while (*value == ' ') {
value++;
}
if (*value != ',') {
continue;
}
value++;
while (*value == ' ') {
value++;
}
if (*value < '0' || *value > '9') {
continue;
}
status = 0u;
while (*value >= '0' && *value <= '9') {
status = (uint8_t)(status * 10u + (uint8_t)(*value - '0'));
value++;
}
if (status <= 11u) {
return status;
}
}
return 0xFFu;
}
/* 解析 +CSQ 返回的 RSSI 原始值,不在此处换算为 dBm。 */
static uint8_t AIR780E_ParseSignalRSSI(const char *response)
{
const char *value = strstr(response, "+CSQ:");
uint8_t rssi = 0u;
if (value == 0) {
return 0xFFu;
}
value += 5;
while (*value == ' ') {
value++;
}
if (*value < '0' || *value > '9') {
return 0xFFu;
}
while (*value >= '0' && *value <= '9') {
rssi = (uint8_t)(rssi * 10u + (uint8_t)(*value - '0'));
value++;
}
return (rssi <= 31u || rssi == 99u) ? rssi : 0xFFu;
}
/* 判断网络状态是否属于已注册状态。 */
static int AIR780E_IsRegistered(uint8_t status)
{
return (status == 1u || status == 5u || status == 6u || status == 7u ||
status == 9u || status == 10u);
}
/* 重新配置 USART1 波特率,发送和接收均使用 8N1。 */
static void AIR780E_SetBaud(uint32_t baud_rate)
{
USART_InitTypeDef serial;
USART_Cmd(USART1, DISABLE);
USART_StructInit(&serial);
serial.USART_BaudRate = baud_rate;
serial.USART_WordLength = USART_WordLength_8b;
serial.USART_StopBits = USART_StopBits_1;
serial.USART_Parity = USART_Parity_No;
serial.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
serial.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART1, &serial);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
USART_Cmd(USART1, ENABLE);
air_baud_rate = baud_rate;
}
/* 发新命令前丢弃旧应答,避免上一条命令的数据影响本次解析。 */
static void AIR780E_ClearInput(void)
{
NVIC_DisableIRQ(USART1_IRQn);
air_rx_tail = air_rx_head;
NVIC_EnableIRQ(USART1_IRQn);
}
/* 从中断写入的环形缓冲区读取一个字节。 */
static int AIR780E_ReadByte(uint8_t *value)
{
if (air_rx_tail == air_rx_head) {
return 0;
}
*value = air_rx_buffer[air_rx_tail];
air_rx_tail = (uint16_t)((air_rx_tail + 1u) % AIR780E_RX_BUFFER_SIZE);
return 1;
}
/* 等待发送数据寄存器空闲后写入一个字节。 */
static void AIR780E_WriteByte(uint8_t value)
{
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET) {
}
USART_SendData(USART1, value);
}
static void AIR780E_WriteString(const char *value)
{
while (*value != '\0') {
AIR780E_WriteByte((uint8_t)*value++);
}
}
/*
* 等待 AT 命令的完整结果:成功返回 1,ERROR 或超时返回 0。
* required 非空时,必须同时看到指定字段和最终 OK;prompt 非零时等待“>”。
* 收到 +CMS ERROR 时保留数值,供 OLED 显示和故障排查。
*/
static int AIR780E_WaitResponse(const char *required, uint32_t timeout_ms,
uint8_t prompt)
{
char response[AIR780E_RESPONSE_SIZE];
uint16_t length = 0u;
uint32_t waited = 0u;
uint8_t value;
uint8_t found_required = (required == 0) ? 1u : 0u;
char *cms_error;
response[0] = '\0';
air_last_response[0] = '\0';
while (waited < timeout_ms) {
if (AIR780E_ReadByte(&value) == 0) {
delay_ms(1u);
waited++;
continue;
}
if (prompt != 0u && value == (uint8_t)'>') {
return 1;
}
if (length >= AIR780E_RESPONSE_SIZE - 1u) {
memmove(response, response + 1, AIR780E_RESPONSE_SIZE - 2u);
length = AIR780E_RESPONSE_SIZE - 2u;
}
response[length++] = (char)value;
response[length] = '\0';
if (required != 0 && strstr(response, required) != 0) {
found_required = 1u;
}
cms_error = strstr(response, "+CMS ERROR:");
if (cms_error != 0 && strchr(cms_error, '\r') != 0) {
air_last_cms_error = AIR780E_ParseErrorNumber(cms_error + 11);
return 0;
}
if (strstr(response, "\r\nERROR\r\n") != 0 ||
strstr(response, "+CME ERROR:") != 0) {
return 0;
}
if (prompt == 0u && strstr(response, "\r\nOK\r\n") != 0) {
memcpy(air_last_response, response, (size_t)length + 1u);
return (found_required != 0u) ? 1 : 0;
}
}
return 0;
}
/* 清空旧输入、发送 AT 命令并等待应答。 */
static int AIR780E_Command(const char *command, const char *required,
uint32_t timeout_ms)
{
AIR780E_ClearInput();
AIR780E_WriteString(command);
return AIR780E_WaitResponse(required, timeout_ms, 0u);
}
/* 查询电路域、分组域的网络注册状态及信号强度。 */
static void AIR780E_UpdateNetworkDiagnostics(void)
{
if (AIR780E_Command("AT+CREG?\r", "+CREG:",
AIR780E_AT_TIMEOUT_MS) != 0) {
air_last_network_status = AIR780E_ParseNetworkStatus(air_last_response,
"+CREG:");
}
if (AIR780E_Command("AT+CEREG?\r", "+CEREG:",
AIR780E_AT_TIMEOUT_MS) != 0) {
air_last_eps_status = AIR780E_ParseNetworkStatus(air_last_response,
"+CEREG:");
}
if (AIR780E_Command("AT+CSQ\r", "+CSQ:",
AIR780E_AT_TIMEOUT_MS) != 0) {
air_last_signal_rssi = AIR780E_ParseSignalRSSI(air_last_response);
}
}
/*
* 按参考工程先确认 +CREG 注册成功,再发短信。
* 查询失败或尚未注册时不提交短信,避免无网络时直接出现 CMS 331。
*/
static int AIR780E_WaitForNetwork(void)
{
uint8_t attempt;
air_last_network_status = 0xFFu;
for (attempt = 0u; attempt < AIR780E_NETWORK_POLL_ATTEMPTS; attempt++) {
if (AIR780E_Command("AT+CREG?\r", "+CREG:",
AIR780E_AT_TIMEOUT_MS) != 0) {
air_last_network_status = AIR780E_ParseNetworkStatus(
air_last_response, "+CREG:");
if (AIR780E_IsRegistered(air_last_network_status) != 0) {
return 1;
}
if (air_last_network_status == 3u ||
air_last_network_status == 8u ||
air_last_network_status == 11u) {
return 0;
}
}
delay_ms(AIR780E_NETWORK_POLL_DELAY_MS);
}
return 0;
}
static int AIR780E_IsHex(char value)
{
return ((value >= '0' && value <= '9') ||
(value >= 'A' && value <= 'F') ||
(value >= 'a' && value <= 'f'));
}
/* 将一个字节转成两个十六进制字符,追加到 PDU 字符串。 */
static void AIR780E_AppendHexByte(char *pdu, uint16_t *length, uint8_t value)
{
static const char digits[] = "0123456789ABCDEF";
pdu[(*length)++] = digits[value >> 4];
pdu[(*length)++] = digits[value & 0x0Fu];
}
/* 初始化 USART1 GPIO 和 RXNE 接收中断。 */
void AIR780E_Init(void)
{
GPIO_InitTypeDef gpio;
NVIC_InitTypeDef interrupt;
air_rx_head = 0u;
air_rx_tail = 0u;
air_probe_rx_count = 0u;
air_probe_active = 0u;
air_last_cms_error = 0xFFFFu;
air_last_response[0] = '\0';
air_last_network_status = 0xFFu;
air_last_eps_status = 0xFFu;
air_last_signal_rssi = 0xFFu;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO |
RCC_APB2Periph_USART1, ENABLE);
/* 本开发板丝印 TX 接 PA9、丝印 RX 接 PA10,与参考工程一致。 */
gpio.GPIO_Pin = GPIO_Pin_9;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
gpio.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &gpio);
gpio.GPIO_Pin = GPIO_Pin_10;
gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &gpio);
AIR780E_SetBaud(AIR780E_DEFAULT_BAUD);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
interrupt.NVIC_IRQChannel = USART1_IRQn;
interrupt.NVIC_IRQChannelPreemptionPriority = 2u;
interrupt.NVIC_IRQChannelSubPriority = 0u;
interrupt.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&interrupt);
}
uint16_t AIR780E_GetLastCMSError(void)
{
return air_last_cms_error;
}
uint8_t AIR780E_GetLastNetworkStatus(void)
{
return air_last_network_status;
}
uint8_t AIR780E_GetLastEPSStatus(void)
{
return air_last_eps_status;
}
uint8_t AIR780E_GetLastSignalRSSI(void)
{
return air_last_signal_rssi;
}
/* 探测模组、检查 SIM 和网络、组装 PDU,并等待短信提交结果。 */
AIR780E_Result AIR780E_SendSMS(const char *phone, const char *ucs2_hex)
{
char command[20];
char destination[18];
char pdu[AIR780E_PDU_BUFFER_SIZE];
uint16_t index;
uint16_t body_length;
uint16_t destination_length;
uint16_t pdu_length;
uint16_t tpdu_length;
uint8_t international;
uint8_t attempt;
if (phone == 0 || ucs2_hex == 0) {
return AIR780E_INVALID_ARGUMENT;
}
air_last_cms_error = 0xFFFFu;
for (index = 0u; phone[index] != '\0'; index++) {
if (index >= 15u || phone[index] < '0' || phone[index] > '9') {
return AIR780E_INVALID_ARGUMENT;
}
}
if (index == 0u) {
return AIR780E_INVALID_ARGUMENT;
}
body_length = 0u;
while (ucs2_hex[body_length] != '\0') {
if (body_length >= 280u || AIR780E_IsHex(ucs2_hex[body_length]) == 0) {
return AIR780E_INVALID_ARGUMENT;
}
body_length++;
}
if (body_length == 0u || (body_length % 4u) != 0u) {
return AIR780E_INVALID_ARGUMENT;
}
/* 低功耗固件可能丢掉唤醒时的前几条命令,因此重复探测两种波特率。 */
air_probe_rx_count = 0u;
air_probe_active = 1u;
for (attempt = 0u; attempt < 12u; attempt++) {
if (AIR780E_Command("AT\r", 0, 1000u) != 0) {
break;
}
if ((attempt % 3u) == 2u) {
AIR780E_SetBaud(air_baud_rate == AIR780E_DEFAULT_BAUD
? AIR780E_REFERENCE_BAUD : AIR780E_DEFAULT_BAUD);
}
delay_ms(250u);
}
air_probe_active = 0u;
if (attempt == 12u) {
return (air_probe_rx_count == 0u)
? AIR780E_NO_RESPONSE : AIR780E_BAD_AT_RESPONSE;
}
if (AIR780E_Command("ATE0\r", 0, AIR780E_AT_TIMEOUT_MS) == 0) {
return AIR780E_ECHO_CONFIG_ERROR;
}
for (attempt = 0u; attempt < 10u; attempt++) {
if (AIR780E_Command("AT+CPIN?\r", "+CPIN: READY", 3000u) != 0) {
break;
}
delay_ms(1000u);
}
if (attempt == 10u) {
return AIR780E_SIM_NOT_READY;
}
air_last_eps_status = 0xFFu;
air_last_signal_rssi = 0xFFu;
AIR780E_UpdateNetworkDiagnostics();
if (AIR780E_WaitForNetwork() == 0) {
AIR780E_UpdateNetworkDiagnostics();
return AIR780E_NETWORK_NOT_READY;
}
if (AIR780E_Command("AT+CMGF=0\r", 0, AIR780E_AT_TIMEOUT_MS) == 0) {
return AIR780E_PDU_MODE_ERROR;
}
/* PDU 模式明确指定 UCS2 编码;国内 11 位手机号按 +86 国际格式编码。 */
international = (index == 11u && phone[0] == '1') ? 1u : 0u;
destination_length = index;
if (international != 0u) {
destination[0] = '8';
destination[1] = '6';
memcpy(destination + 2, phone, destination_length);
destination_length += 2u;
} else {
memcpy(destination, phone, destination_length);
}
pdu_length = 0u;
AIR780E_AppendHexByte(pdu, &pdu_length, 0x00u); /* 使用 SIM 卡保存的短信中心号码 */
AIR780E_AppendHexByte(pdu, &pdu_length, 0x11u); /* SMS-SUBMIT,包含相对有效期 */
AIR780E_AppendHexByte(pdu, &pdu_length, 0x00u); /* 短信消息参考号 */
AIR780E_AppendHexByte(pdu, &pdu_length, (uint8_t)destination_length);
AIR780E_AppendHexByte(pdu, &pdu_length,
(international != 0u) ? 0x91u : 0x81u);
for (index = 0u; index < destination_length; index += 2u) {
uint8_t low = (uint8_t)(destination[index] - '0');
uint8_t high = (index + 1u < destination_length)
? (uint8_t)(destination[index + 1u] - '0') : 0x0Fu;
AIR780E_AppendHexByte(pdu, &pdu_length,
(uint8_t)(low | (uint8_t)(high << 4)));
}
AIR780E_AppendHexByte(pdu, &pdu_length, 0x00u); /* 协议标识 PID */
AIR780E_AppendHexByte(pdu, &pdu_length, 0x08u); /* 数据编码 DCS:UCS2 */
AIR780E_AppendHexByte(pdu, &pdu_length, 0xA7u); /* 短信有效期:24 小时 */
AIR780E_AppendHexByte(pdu, &pdu_length, (uint8_t)(body_length / 2u));
if (pdu_length + body_length >= AIR780E_PDU_BUFFER_SIZE) {
return AIR780E_INVALID_ARGUMENT;
}
memcpy(pdu + pdu_length, ucs2_hex, body_length);
pdu_length = (uint16_t)(pdu_length + body_length);
pdu[pdu_length] = '\0';
tpdu_length = (uint16_t)(pdu_length / 2u - 1u); /* CMGS 长度不计短信中心长度字节 */
memcpy(command, "AT+CMGS=", 8u);
index = 8u;
if (tpdu_length >= 100u) {
command[index++] = (char)('0' + tpdu_length / 100u);
}
if (tpdu_length >= 10u) {
command[index++] = (char)('0' + (tpdu_length / 10u) % 10u);
}
command[index++] = (char)('0' + tpdu_length % 10u);
command[index++] = '\r';
command[index] = '\0';
AIR780E_ClearInput();
AIR780E_WriteString(command);
if (AIR780E_WaitResponse(0, 5000u, 1u) == 0) {
return AIR780E_NO_PROMPT;
}
AIR780E_WriteString(pdu);
AIR780E_WriteByte(0x1Au);
/* 提交到移动网络可能耗时较长,等待 +CMGS 与最终 OK。 */
if (AIR780E_WaitResponse("+CMGS:", AIR780E_SMS_TIMEOUT_MS, 0u) == 0) {
if (air_last_cms_error == 331u) {
AIR780E_UpdateNetworkDiagnostics();
}
return AIR780E_SEND_ERROR;
}
return AIR780E_OK;
}
/* USART1 接收中断:读出字节并写入环形缓冲区;AT 解析留在主循环。 */
void USART1_IRQHandler(void)
{
uint16_t next;
uint16_t status = USART1->SR;
uint8_t value = (uint8_t)USART1->DR;
if ((status & USART_SR_RXNE) == 0u) {
return;
}
if (air_probe_active != 0u && air_probe_rx_count != 0xFFFFu) {
air_probe_rx_count++;
}
next = (uint16_t)((air_rx_head + 1u) % AIR780E_RX_BUFFER_SIZE);
if (next != air_rx_tail) {
air_rx_buffer[air_rx_head] = value;
air_rx_head = next;
}
}
AIR780E.H代码
#ifndef AIR780E_H
#define AIR780E_H
/*
* Air780E 短信驱动接口。文件编码:UTF-8。
* 仅在主循环调用发送函数;发送过程会等待 AT 应答和短信提交结果。
*/
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* 本开发板按已验证的参考工程接线:
* 开发板 TX -> STM32 PA9(USART1 发送端);
* 开发板 RX -> STM32 PA10(USART1 接收端)。
* 这里的 TX/RX 是开发板端子丝印,不能直接套用裸模组的管脚方向。
* USART1 使用 8N1,依次尝试 115200 和 9600 波特率。
*/
typedef enum {
AIR780E_OK = 0, /* 模组返回 +CMGS 和最终 OK */
AIR780E_INVALID_ARGUMENT, /* 手机号或 UCS2 十六进制内容无效 */
AIR780E_NO_RESPONSE, /* 探测阶段没有收到任何串口字节 */
AIR780E_BAD_AT_RESPONSE, /* 收到字节,但未得到有效 AT 应答 */
AIR780E_SIM_NOT_READY, /* SIM 卡未就绪 */
AIR780E_CONFIG_ERROR, /* 预留:通用配置错误 */
AIR780E_NO_PROMPT, /* AT+CMGS 后未收到输入提示符 */
AIR780E_SEND_ERROR, /* 短信提交失败或等待超时 */
AIR780E_ECHO_CONFIG_ERROR, /* 关闭命令回显失败 */
AIR780E_PDU_MODE_ERROR, /* 设置 PDU 模式失败 */
AIR780E_CHARSET_ERROR, /* 预留:字符集配置错误 */
AIR780E_SMS_PARAM_ERROR, /* 预留:短信参数配置错误 */
AIR780E_NETWORK_NOT_READY /* 网络尚未注册 */
} AIR780E_Result;
/* 初始化 PA9、PA10、USART1 及接收中断;调用前需初始化延时模块。 */
void AIR780E_Init(void);
/*
* phone 为纯数字手机号;国内 11 位手机号码会自动加 86 国际区号。
* ucs2_hex 为 UTF-16BE 码元的十六进制字符串,例如“测试”为“6D4B8BD5”。
* 函数会阻塞等待模组结果,不能从中断服务函数中调用。
*/
AIR780E_Result AIR780E_SendSMS(const char *phone, const char *ucs2_hex);
/* 查询最近一次 AT 诊断;0xFFFF 或 0xFF 表示没有取得对应数据。 */
uint16_t AIR780E_GetLastCMSError(void);
uint8_t AIR780E_GetLastNetworkStatus(void);
uint8_t AIR780E_GetLastEPSStatus(void);
uint8_t AIR780E_GetLastSignalRSSI(void);
#ifdef __cplusplus
}
#endif
#endif
九、实物演示章节
9.1 实物硬件清单
- STM32F103C8T6最小系统板 ×1
- Air780E开发板(支持5V输入版本) ×1
- SIM卡(4G可用,开通短信业务) ×1
- 4G天线(接Air780E) ×1
- 0.96寸I2C OLED屏幕 ×1
- 轻触按键1个、杜邦线若干
9.2 演示操作步骤
- 按照硬件接线表完成接线,确认所有GND共地,天线接好Air780E,插入可用SIM卡。
- 将编译完成的程序下载进STM32单片机,上电。
- OLED屏幕打印
READY - PRESS KEY,代表单片机初始化完成。等待Air780E模组开机、搜网注册。 - 按下PB12按键,可以看到屏幕切换到
SENDING...,此时STM32执行整套AT交互,查询SIM、网络,组装PDU发送短信。 - 如果一切正常,屏幕输出
SEND OK;等待数秒,查看目标手机,接收内容为中文“测试”短信。 - 如果报错,根据OLED提示的错误信息进行故障排查。

9.3 演示中常见现象
- OLED显示SEND_OK,但是手机收不到短信:模组仅完成向网络提交,运营商存在延迟;检查SIM卡是否关闭短信功能、是否欠费。
- NO MODEM RESPONSE:排查TX/RX接线是否交叉、共地、波特率、模组是否正常开机。
- SIM NOT READY:SIM卡插反、SIM卡损坏,模组识别失败。
- NETWORK NOT READY:天线没有接好,信号差,SIM卡不支持4G网络,所在位置无4G信号。

十、故障排查思路
按照顺序定位问题,由底层到上层逐级排查:
- 串口链路阶段:确认模组已经开机,全部器件共地,TX/RX接线,波特率匹配,观察是否可以收到AT指令应答。对应OLED报错:
NO MODEM RESPONSE、BAD AT RESPONSE。 - SIM卡与网络注册阶段:检查SIM卡、天线,观察
CPIN、CREG、CSQ返回结果,确认网络注册成功。对应OLED报错:SIM NOT READY、NETWORK NOT READY。 - 短信提交阶段:确认AT+CMGS是否收到
>提示符,查看有没有+CMGS、OK应答;出现+CMS ERROR记录错误编号,查阅官方AT手册。对应OLED报错:NO SMS PROMPT、SMS SEND FAILED。
供电提醒:Air780E射频发射瞬间电流大,电源供电能力不足会导致模组反复重启,AT通信断断续续。
十一、总结
STM32F103单片机检测按键,通过USART1串口发送一系列AT指令,采用PDU模式UCS2编码组装中文短信数据包,交由Air780E 4G模组对接移动网络发送短信,OLED实时展示各个步骤运行状态。
重要区分:模组返回SEND_OK代表短信已经提交运营商,不等于目标手机100%收到短信,实际接收以手机为准。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2602_95576488/article/details/166600635




