星河单片机头像
关注
STM32+Air780E实现按键发送中文短信(OLED状态显示)封面图

STM32+Air780E实现按键发送中文短信(OLED状态显示)

前言

本篇基于合宙Air780E 4G模组,搭配STM32F103C8T6单片机,实现按键触发发送中文短信功能。按下PB12按键,STM32通过串口向Air780E发送AT指令,采用PDU模式UCS2编码发送中文短信,OLED屏幕实时展示整个流程的工作状态。

重要说明:程序中只保存目标手机号,手机号不会在OLED屏幕上显示;模组返回SEND_OK仅代表短信已经提交运营商网络,不等于对方手机一定收到短信,最终接收情况以目标手机实际收到为准。

视频讲解:STM32+Air780E实现按键发送中文短信(OLED状态显示)文章配图https://www.bilibili.com/video/BV14taN63Ezr/?share_source=copy_web&vd_source=91b967b41a17a5686913521545a8c3fb

一、整体实现功能流程

整体业务逻辑:

  1. PB12按键按下,STM32完成按键消抖,识别按键按下事件
  2. STM32通过USART1串口和Air780E模组进行AT指令交互
  3. 依次检测SIM卡就绪状态、蜂窝网络注册状态
  4. 组装UCS2编码的PDU短信数据包,包含目标手机号与中文短信内容“测试”
  5. Air780E模组将短信提交给移动运营商网络,等待模组返回+CMGS以及OK应答
  6. 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开发板VCC5VAir780E开发板供电(仅适配支持5V的载板)
Air780E开发板TXPA9USART1发送
Air780E开发板RXPA10USART1接收
Air780E开发板GNDGND单片机和模组共地,必须连接
按键KEY信号端PB12按键输入,开启STM32内部上拉
按键另一端GND按键按下时PB12拉低
OLED SCLPB8软件I2C时钟线
OLED SDAPB9软件I2C数据线
OLED VCC/GND5V/GNDOLED供电,与单片机共地

硬件关键点:共地非常重要! 如果STM32和Air780E不共地,串口通信会出现乱码、完全无应答问题。

三、工作原理讲解

Air780E属于蜂窝4G通信模组。STM32单片机本身不能直接连接运营商移动网络,依靠USART1串口下发AT指令控制Air780E模组。 模组依靠SIM卡完成运营商身份认证,搭配天线,在网络正常的情况下把短信提交给移动网络。

整体信号流向: 按键PB12 → STM32单片机 → USART1串口AT指令 → Air780E模组(SIM卡+天线)→ 运营商移动网络 → 目标接收手机 同时STM32驱动OLED,实时打印每一步工作状态。

这里有两套反馈信号,不要混淆:

  1. 串口AT反馈:模组通过串口返回OK、ERROR、CMS ERROR等应答,STM32可以立刻读取解析,SEND_OK就是来自这个反馈,代表短信已经交给运营商。
  2. 手机实际接收:短信经由运营商网络投递到目标手机,会存在网络延迟,本项目没有实现短信送达报告。

四、串口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+6D4B6D 4B
试U+8BD58B 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 RESPONSEAT探测阶段完全收不到模组串口数据
BAD AT RESPONSE收到串口数据,但是没有合法OK应答
SIM NOT READYAT+CPIN?查询SIM卡未就绪
NETWORK NOT READY蜂窝网络没有注册成功
NO SMS PROMPTAT+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 实物硬件清单

  1. STM32F103C8T6最小系统板 ×1
  2. Air780E开发板(支持5V输入版本) ×1
  3. SIM卡(4G可用,开通短信业务) ×1
  4. 4G天线(接Air780E) ×1
  5. 0.96寸I2C OLED屏幕 ×1
  6. 轻触按键1个、杜邦线若干

9.2 演示操作步骤

  1. 按照硬件接线表完成接线,确认所有GND共地,天线接好Air780E,插入可用SIM卡。
  2. 将编译完成的程序下载进STM32单片机,上电。
  3. OLED屏幕打印READY - PRESS KEY,代表单片机初始化完成。等待Air780E模组开机、搜网注册。
  4. 按下PB12按键,可以看到屏幕切换到SENDING...,此时STM32执行整套AT交互,查询SIM、网络,组装PDU发送短信。
  5. 如果一切正常,屏幕输出SEND OK;等待数秒,查看目标手机,接收内容为中文“测试”短信。
  6. 如果报错,根据OLED提示的错误信息进行故障排查。

9.3 演示中常见现象

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

十、故障排查思路

按照顺序定位问题,由底层到上层逐级排查:

  1. 串口链路阶段:确认模组已经开机,全部器件共地,TX/RX接线,波特率匹配,观察是否可以收到AT指令应答。对应OLED报错:NO MODEM RESPONSE、BAD AT RESPONSE。
  2. SIM卡与网络注册阶段:检查SIM卡、天线,观察CPIN、CREG、CSQ返回结果,确认网络注册成功。对应OLED报错:SIM NOT READY、NETWORK NOT READY。
  3. 短信提交阶段:确认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

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--