1. 方案背景:为什么选STM32+Air780E做按键发短信
之前一直在用手机直接发短信,但有些固定场景——比如门岗告警、设备自检结果上报、远程通知——需要设备自己把信息发出去。一开始考虑过4G Cat.1模块加串口透传的方案,对比下来Air780E最合适:模块本身支持AT指令,封装小,价格便宜,而且短信和TCP/UDP都能跑。STM32负责按键交互和OLED显示,Air780E专职通信,两个角色分工很明确。
这个项目做下来其实就是一条链路:按键事件 → 状态刷新 → 串口AT指令 → 短信发出。做完之后,你可以把按键替换成传感器触发,把短信内容改成报警信息,整个框架直接平移。所以这篇不只是讲“按键发短信”,更关键的是这套交互流程和AT指令时序控制。
适合看这篇的人有两类:一是正在做STM32+4G模块类项目、被AT指令卡住的朋友;二是想把“本地设备事件”通过短信或网络上报出去的嵌入式开发者。硬件成本很低,主控加模块加屏幕,整体下来也就几十块钱。
2. 系统整体设计与模块划分
2.1 功能需求拆解
标题里写的是“按键发送中文短信+OLED状态显示”,拆开看其实有四个独立需求:
- 按键输入:带消抖,单次触发,不给系统带来误判
- 短信内容管理:中英文混合,编码方式要处理好,否则发出去是乱码
- 4G模块控制:Air780E的串口AT指令交互,上电初始化,然后拨号发短信
- 状态显示:OLED实时显示当前短信内容、发送进度、结果
四个需求之间是串行关系,但每个环节都有独立的“坑”。比如很多人以为短信发出去就行,结果用PDU模式下中文编码没处理好,对面收到全是问号。还有按键消抖不做,一次按下触发三次发送,短信费直接翻倍。这些都是实际做下来才会发现的细节。
2.2 硬件组成与选型理由
我用的是普中STM32F103ZET6开发板,Air780E通过串口2连接,按键接在GPIO上,OLED用I2C接口。具体引脚分配看下面的接线表:
| 模块 | STM32引脚 | 说明 |
|---|---|---|
| Air780E TX | PA2(USART2_TX) | MCU发数据到模块 |
| Air780E RX | PA3(USART2_RX) | 模块发数据到MCU |
| OLED SCL | PB8 | I2C1时钟 |
| OLED SDA | PB9 | I2C1数据 |
| 按键KEY1 | PA0 | 短按发送短信 |
| 按键KEY2 | PA1 | 短按切换短信内容 |
选串口2而不是串口1是有讲究的。串口1经常被用来做调试输出,你在调试的时候需要看log,如果把Air780E挂在串口1上,log和AT指令数据会混在一起,排查问题非常痛苦。串口2留给模块,串口1专门输出调试信息,两者互不干扰。
OLED因为只用来显示状态,I2C就够了,省引脚。如果想显示大量中文内容,可以考虑SPI接口的OLED,刷新速度快一些,但I2C在这个项目里完全够用,界面又不是动画。
Air780E的供电要注意一点,模块峰值电流在2A左右,不能用STM32开发板的3.3V直接供电,最好用独立稳压电源或者开发板上的5V转3.3V电路带载能力更强的接口。我实际测试直接用开发板3.3V供电会出现模块频繁重启,换成外接3.3V稳压模块后稳定多了。
2.3 程序架构思路
代码我按“分层”方式组织,方便后续灵活改:
// 分层结构 app_key.c // 按键扫描、消抖、事件上报 app_sms_content.c // 短信内容管理、中文编码 app_air780e.c // Air780E AT指令交互、状态机 app_oled.c // OLED显示刷新 main.c // 任务调度关键的地方在于:按键只上报“事件”,不直接去操作Air780E。中间加一个简单的事件标志位,主循环检测到事件后再调用对应模块。这样做的好处是以后改成传感器触发时,只需要在传感器回调里设置同一个事件标志,其他逻辑完全不动。
3. 按键输入与消抖处理
3.1 硬件电路与GPIO配置
按键电路很简单,一个IO口接按键到GND,内部上拉。STM32的PA0和PA1都支持外部中断,但我没有用中断,而是用的定时器轮询扫描。原因后面说。
GPIO配置代码:
GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);注意内部上拉要选对,否则按键按下时IO电平不确定,消抖程序怎么写都没用。按下时读到的电平是低,释放时是高,这个逻辑要在后面扫描函数里用对。
3.2 为什么选择轮询而不是外部中断
外部中断看起来很美好,按下立刻响应,但实际用下来问题不少:按键抖动会导致中断反复触发,需要额外的软硬件消抖;如果按键按下时主循环正在处理AT指令,中断服务函数里做不了复杂操作,只能置标志位,那和轮询的效果其实差不多。更麻烦的是,中断里做延时消抖会阻塞系统,尤其在Air780E回数据的时候,一个过滤延时就可能丢掉AT指令响应。
所以我用了5ms定时器轮询的方式。核心流程:
- 定时器每5ms触发一次
- 检测IO电平,连续多次读到稳定低电平才认为是有效按下
- 检测到按下后等待释放
- 释放后上报按键事件
为了防止“长按连续触发”或者“一次按下多次上报”,我在释放之后加了一个小延时保护。这个保护时间设成200ms,意思是你按下再释放后200ms内,即使按键又被按下也不会触发第二次。实际体验下来既不卡手也不会误触。
uint8_t key_scan(GPIO_TypeDef* port, uint16_t pin) { static uint8_t stable_cnt = 0; static uint8_t key_released = 1; if (HAL_GPIO_ReadPin(port, pin) == GPIO_PIN_RESET) { if (stable_cnt < 3) { stable_cnt++; } else { if (key_released) { key_released = 0; // 上报一次按键事件 key_event_set(pin); } } } else { stable_cnt = 0; key_released = 1; } return 0; }这里的“连续3次读到低电平算是有效按下”配合5ms定时器,相当于15ms的软件消抖,对于机械按键来说足够了。如果你想更保险,可以把稳定次数调到5,也就是25ms,但我测试下来3次就很稳,超过反而会让快速连按失效。
3.3 按键事件与短信内容联动
两个按键各司其职:KEY1是发送键,KEY2是切换短信内容键。这里我做了一个小设计:KEY2每按一次,屏幕上显示的内容循环切换,当前选中的内容才会被发送。
#define SMS_CONTENT_NUM 3 const char* sms_content_table[SMS_CONTENT_NUM] = { "设备运行正常,定时巡检报告。", "设备告警:温度异常,请尽快处理。", "位置信息已更新,请确认收到。" }; static uint8_t current_content_idx = 0; void sms_content_switch(void) { current_content_idx++; if (current_content_idx >= SMS_CONTENT_NUM) { current_content_idx = 0; } } const char* sms_content_get(void) { return sms_content_table[current_content_idx]; }发送前OLED上会显示当前待发送的完整内容,确认无误再按键发送。这个交互在调试阶段帮我省了很多短信费——一开始没做显示,经常发错内容,等发现的时候短信套餐已经没了。
4. 中文短信编码与PDU格式详解
4.1 为什么中文短信需要PDU编码
Air780E支持两种短信发送格式:Text模式和PDU模式。Text模式只能发ASCII字符,中文内容必须用PDU格式。PDU格式的核心就是把中文转成UCS2编码,每条短信最大长度70个汉字(140字节)。
如果把中文直接用GBK编码发出去,某些模块支持,但兼容性很差,尤其在跨运营商的情况下很容易乱码。UCS2是通用的,所有支持中文短信的GSM模块都认得。
4.2 UCS2编码代码实现
UCS2编码本质上是把每个汉字转成16位的Unicode码,两个字节表示一个字。STM32端需要将UTF-8字符串转成UCS2字节流。如果你用的是STM32标准库,字符串常量默认是UTF-8,所以需要做一次转换。
我写了一个简易转换函数:
// UTF-8编码的文字,转换为UCS2字节序(大端) // 返回UCS2字节数 uint16_t utf8_to_ucs2(const uint8_t* utf8_str, uint8_t* ucs2_buf) { uint16_t ucs2_len = 0; while (*utf8_str != 0) { uint8_t c = *utf8_str; uint16_t code = 0; if (c < 0x80) { // ASCII直接映射 code = c; utf8_str += 1; } else if ((c & 0xE0) == 0xC0) { // 2字节UTF-8编码 code = ((c & 0x1F) << 6) | (utf8_str[1] & 0x3F); utf8_str += 2; } else if ((c & 0xF0) == 0xE0) { // 3字节UTF-8编码 code = ((c & 0x0F) << 12) | ((utf8_str[1] & 0x3F) << 6) | (utf8_str[2] & 0x3F); utf8_str += 3; } else { // 中文UTF-8基本都是3字节,其他情况先跳过 utf8_str += 1; continue; } ucs2_buf[ucs2_len++] = (code >> 8) & 0xFF; ucs2_buf[ucs2_len++] = code & 0xFF; } return ucs2_len; }转换完以后,UC2字节流要拼到AT指令里。“设备运行正常”这几个字的UCS2编码经过转换后,看起来是一串十六进制,比如“8BBE 5907 8FD0 884C 6B63 5E38”,每个汉字对应两个字节。
注意:这里的转换只支持单字符UTF-8在1~3字节内的情况,如果字符串里有特殊符号或生僻字,需要扩展解析逻辑。我实际测试下来,常用汉字和标点符号都没问题。
4.3 完整PDU格式组装
PDU格式的短信中心地址、发送号码、用户数据长度都有固定格式。以“+8613800100500”这样的短信中心号码为例,PDU组装逻辑如下:
uint16_t pdu_build(uint8_t* pdu_buf, const char* phone_number, const uint8_t* ucs2_data, uint16_t ucs2_len) { uint16_t idx = 0; uint8_t i; // 短信中心号码部分,这里固定填入+8613800100500的PDU编码 // 具体过程:+8613800100500去掉+号,偶数位和奇数位交换,前面加长度 pdu_buf[idx++] = 0x08; // 短信中心号码长度 pdu_buf[idx++] = 0x91; // 国际格式 // 86 13 80 00 10 05 00 -> 交换后为 68 31 08 00 01 50 00 static const uint8_t sc_addr[] = {0x68, 0x31, 0x08, 0x00, 0x01, 0x50, 0x00}; for (i = 0; i < sizeof(sc_addr); i++) { pdu_buf[idx++] = sc_addr[i]; } // 短信类型和参数 pdu_buf[idx++] = 0x11; // 短信提交,TP-UDHI等参数关闭 pdu_buf[idx++] = 0x00; // 消息参考值,一般填0 // 目标号码长度和类型 uint8_t phone_len = strlen(phone_number); pdu_buf[idx++] = phone_len; // 号码长度,不含+号 pdu_buf[idx++] = 0x91; // 国际格式 pdu_buf[idx++] = phone_len; // 实际号码长度 // 号码也做奇偶交换 for (i = 0; i < phone_len; i++) { uint8_t num = phone_number[i] - '0'; if (i & 1) { // 奇数为放到高四位 pdu_buf[idx - 1] |= (num << 4); } else { pdu_buf[idx] = num; idx++; } } if (phone_len & 1) { pdu_buf[idx++] = 0xF0; // 奇数长度末尾补F } // 协议标识、编码方式、有效期 pdu_buf[idx++] = 0x00; // TP-PID pdu_buf[idx++] = 0x08; // TP-DCS为UCS2编码 pdu_buf[idx++] = 0xAA; // TP-VP有效期 // 用户数据长度 pdu_buf[idx++] = ucs2_len; // 用户数据 for (i = 0; i < ucs2_len; i++) { pdu_buf[idx++] = ucs2_data[i]; } return idx; }这里有几个容易踩坑的点:
- 短信中心号码是固定的,但不同运营商、不同地区可能不一样。我这边的号码是+8613800100500,如果你不确定自己的短信中心号码,可以用AT+CSCA?查询。
- 电话号码的长度计算不要带“+”号,但格式类型写0x91表示国际格式。
- 奇数长度的电话号码要在末尾补0xF0,否则PDU格式解析会错位。
4.4 发送AT指令的完整流程
编码完后,拼接AT指令发送:
uint8_t pdu_sms_send(const char* phone, const char* utf8_content) { uint8_t pdu_buf[256]; uint8_t ucs2_buf[256]; uint16_t ucs2_len = utf8_to_ucs2((const uint8_t*)utf8_content, ucs2_buf); uint16_t pdu_len = pdu_build(pdu_buf, phone, ucs2_buf, ucs2_len); // AT+CMGS=<长度>,然后回车等待模块返回 '>' sprintf(at_cmd, "AT+CMGS=%d\r", pdu_len); uart2_send_string(at_cmd); // 等待 '>' 提示符 if (!wait_for_prompt(">", 2000)) { return 0; } // 发送PDU数据,最后加0x1A结束符 uart2_send_bytes(pdu_buf, pdu_len); uart2_send_byte(0x1A); // 等待模块返回OK或ERROR if (wait_for_response("OK", 5000)) { return 1; } return 0; }这个流程最关键的一步是必须等待模块返回“>”提示符后再发PDU数据。很多人直接AT+CMGS发完后立刻跟上PDU数据,模块还没准备好,数据丢了,然后短信没发出去,返回ERROR。还有人在PDU数据末尾忘记加0x1A(Ctrl-Z),模块等不到结束符就一直挂着,直到超时。
5. Air780E模块初始化与串口AT指令交互
5.1 模块上电时序
Air780E有个上电时序的要求:VBAT上电后需要等一段时间,模块内部启动完成后才能接受AT指令。我实测下来,从VBAT供电到能稳定响应AT指令,大约需要2~3秒。如果上电立即发AT指令,大概率收到的是空响应或者乱码。
我做了个简单的倒计时流程:
void air780e_power_on(void) { // 拉高PWRKEY引脚一段时间,触发开机 HAL_GPIO_WritePin(AIR780E_PWRKEY_GPIO_Port, AIR780E_PWRKEY_Pin, GPIO_PIN_RESET); HAL_Delay(800); HAL_GPIO_WritePin(AIR780E_PWRKEY_GPIO_Port, AIR780E_PWRKEY_Pin, GPIO_PIN_SET); // 等待模块就绪 HAL_Delay(3000); // 发送AT测试 uart2_send_string("AT\r\n"); wait_for_response("OK", 1000); }正常运行时,我会在系统初始化阶段调用这个函数,保证后续AT指令都能得到正常响应。
5.2 基础AT指令设置
开机后需要做一轮基础配置,手机的短信模式要切成PDU模式,关闭回声,设置短信存储位置为SIM卡或模块内部。这些指令不多,但顺序不能乱:
void air780e_init(void) { // 关闭回声 uart2_send_string("ATE0\r\n"); wait_for_response("OK", 1000); // 设置短信格式为PDU uart2_send_string("AT+CMGF=0\r\n"); wait_for_response("OK", 1000); // 设置短信存储位置为SIM卡 uart2_send_string("AT+CPMS=\"SM\",\"SM\",\"SM\"\r\n"); wait_for_response("OK", 1000); // 查询短信中心 uart2_send_string("AT+CSCA?\r\n"); wait_for_response("OK", 1000); }这里AT+CMGF=0要特别强调:Text模式是1,PDU模式是0。如果不切模式,后面你用AT+CMGS发送PDU指令,模块会当成普通文本处理,编码格式错乱,发出去全是乱码。
还有个容易被忽略的点:Air780E默认波特率是115200,有些模块默认是9600或者自动波特率。如果你用的是自动波特率版本,上电后第一次发AT指令需要使用特殊格式让它自动识别。我用的是固定115200版本,省去这个麻烦,串口初始化直接按115200配置。
5.3 串口接收与状态机
AT指令交互的核心是串口接收。我用了DMA接收+空闲中断的方式,配合一个环形缓冲区,然后在主循环里处理。DMA+空闲中断的好处是CPU占用小,模块一有数据就能自动收完,不用逐字节轮询等待。
接收缓存到了之后,在解析函数里做关键字匹配:
typedef struct { uint8_t buffer[512]; uint16_t len; uint16_t rx_state; // 0=空闲 1=接收中 } Uart2Rx_t; Uart2Rx_t uart2_rx; // 在主循环中轮询 void at_response_handle(void) { if (uart2_rx.len > 0) { // 在buffer里查找是否含有关键字 if (strstr((char*)uart2_rx.buffer, "OK") != NULL) { at_result = AT_RESULT_OK; } else if (strstr((char*)uart2_rx.buffer, "ERROR") != NULL) { at_result = AT_RESULT_ERROR; } else if (strstr((char*)uart2_rx.buffer, ">") != NULL) { at_result = AT_RESULT_PROMPT; } uart2_rx.len = 0; memset(uart2_rx.buffer, 0, sizeof(uart2_rx.buffer)); } }我这个状态机写得很简略,实际项目里AT指令交互往往是多步状态联动:发送AT指令A后要等OK,再发送AT指令B,再等某个特定响应。初学者很容易犯的一个错是在主循环里用HAL_Delay等待响应,这样会把整个系统卡死。按键扫描和OLED刷新全停了,一旦模块响应慢了半拍,系统就像死机一样。
正确做法是拆成“发送指令→设置超时→处理响应”的流程,不要让等待占满整个主循环。
5.4 超时重试机制
我在项目里做了一个通用的AT指令发送函数:
uint8_t at_send_command_wait(const char* cmd, const char* expect, uint16_t timeout_ms) { uint16_t elapsed = 0; at_result = AT_RESULT_NONE; uart2_send_string(cmd); while (elapsed < timeout_ms) { at_response_handle(); if (at_result == AT_RESULT_OK && expect == NULL) { return 1; } if (at_result == AT_RESULT_OK && strcmp(expect, "OK") == 0) { return 1; } if (at_result == AT_RESULT_ERROR) { return 0; } HAL_Delay(10); elapsed += 10; } return 0; }之所以要加超时,是因为模块偶尔会响应慢,尤其SIM卡注册网络的时候,可能几秒钟没有响应。如果你用无限等待,整个系统就卡在那了。设置超时后,即使模块没响应,系统也能继续跑,下次按键还能重新触发发送。
6. OLED状态显示与中文取模实现
6.1 驱动库选型与显示内容规划
OLED我用的0.96寸SSD1306,I2C接口。驱动上直接用了开源的SSD1306驱动,做了简单的移植。显示内容规划三段区域:
- 顶部:当前模块状态(初始化中/待发送/发送中/发送成功/发送失败)
- 中部:短信内容预览,最多显示两行,每行8个汉字
- 底部:当前短信内容序号和剩余短信条数
布局上建议用坐标定位,把屏幕横向分成128像素宽的列。OLED的SSD1306一行最多显示16×16点阵的汉字8个,所以短信内容要自动换行。我写了个简单换行函数:每次计算当前行写完多少个字,超过8个自动换到下一行,最多显示两行。
6.2 中文显示的核心:字库取模
这是整个OLED显示部分最容易被卡住的地方。SSD1306本身不带字库,你要显示中文,必须把汉字的点阵数据烧进代码里。
我用的是16×16点阵,每个汉字需要32字节的数据。取模软件用的是PCtoLCD2002,设置要点:
- 取模方式:逐行取模,高位在前
- 点阵大小:16×16
- 反色:关闭,因为白底黑字是常规显示
- 字节顺序:横向取模,每行2个字节
比如“设”字的取模数据是一串类似这样的字节数组:
static const uint8_t font_set[32] = { 0x00, 0x00, 0x7F, 0xFC, ... };显示函数的核心逻辑是把每个字节展开成像素点:
void oled_show_chinese(uint8_t x, uint8_t y, const uint8_t* font_data) { for (uint8_t row = 0; row < 16; row++) { for (uint8_t col = 0; col < 16; col++) { uint8_t byte_index = row * 2 + (col / 8); uint8_t bit_index = 7 - (col % 8); if (font_data[byte_index] & (1 << bit_index)) { oled_draw_point(x + col, y + row, 1); } } } }如果你在取模软件里选的“逐列取模”或者“倒序”,显示出来就是镜像或者乱码。这是OLED中文显示最常见的问题,没有之一。
6.3 状态显示与发送流程联动
OLED显示和AT指令发送流程要同步,我用了一个简单的枚举变量作为“状态机”,每次状态变化就刷新屏幕:
typedef enum { SMS_STATE_IDLE, SMS_STATE_SENDING, SMS_STATE_SUCCESS, SMS_STATE_FAIL } SmsState_t;发送时流程是:
- 屏幕显示“正在发送...”
- 调用AT指令发送函数,LED指示灯同步亮起
- 等待响应期间,屏幕每隔200ms刷新一串动态点,提示用户系统没死机
- 返回OK后显示“发送成功”,返回ERROR或超时显示“发送失败:请检查信号”
这个动态提示非常重要,因为AT指令等待可能需要几秒钟,如果屏幕一直静止,用户会以为设备卡死了。加个简单的动画,体验会好很多。
6.4 信号强度与网络注册状态显示
Air780E还支持查询信号强度指令AT+CSQ,返回值的范围是0~31,99表示无信号。我把这个查询也放到了初始化流程里,并且在OLED顶部显示当前信号强度等级。
显示格式很简单,就是“信号:XX%”。这样在调试的时候能直观看到模块是不是已经注册上网络,省得每次都要拿串口工具去手动敲指令查。
void oled_show_signal(uint8_t percent) { char buf[16]; sprintf(buf, "信号:%d%%", percent); oled_show_string(0, 0, buf); }这里要强调一下:AT+CSQ返回值不是百分比,是RSSI等级值,需要自己做转换。实测等级值22以上基本是满格,10以下信号很弱,短信发送容易失败。
7. 实测效果与故障排查记录
7.1 完整发送流程演示
硬件连接好,上电后屏幕先显示“系统初始化中...”,同时Air780E开始开机AT握手。我用的是外接电源给模块供电,STM32的串口2收到模块的“RDY”提示后,开始发送初始化指令序列。
整个过程实测时间线大概是:
| 时序 | 操作 | 预计耗时 |
|---|---|---|
| 上电 | 模块开机,AT响应 | 2~3秒 |
| 初始化 | ATE0/CMGF/CPMS/CSCA | 1秒 |
| 按键发送 | 按下KEY1 | 立即触发 |
| AT+CMGS | 等待提示符 | 0.5秒 |
| 发送PDU | 等待OK | 1~2秒 |
| 完成 | OLED显示成功 | 总体5秒内完成 |
按键按下到对面手机收到短信,完整链路大概4~5秒。这个速度在4G Cat.1模块里算正常,不是短信通道的问题,是AT指令交互和PDU组装消耗的时间。
7.2 踩坑记录一:中文乱码
第一个坑就是中文乱码。一开始我用Text模式发送,短信内容直接是UTF-8字符串,“设备运行正常”发出去,对面收到的是“璁惧澶囪繍琛屾甯?”。后来换成PDU模式也不能直接用UTF-8的字节流,必须转成UCS2。这个坑很多人踩,原因在于嵌入式里字符串默认是UTF-8或者GBK,而PDU只认UCS2,中间的转换步骤不能省略。
7.3 踩坑记录二:按键误触发送多条
第二个坑是按键消抖没做好。第一次测试时,按一下KEY1,结果对面收到三条一模一样的短信。排查发现是按键没有消抖,机械按键的抖动被当成了多次按下。加上消抖逻辑后再测,按多少次都只发一条,问题解决。
这里我想多说一句:按键消抖不是只在机械按键上需要,如果你以后用继电器或者开关量信号触发发送,一样要处理。信号边沿的抖动是物理世界绕不开的现象,软件上的处理成本很低,值得每个项目都做掉。
7.4 踩坑记录三:模块供电不足
第三个坑就是供电。Air780E的峰值电流确实不小,直接用开发板的3.3V供电,开机没问题,但是一发送短信就重启。后来用5V/2A的电源转3.3V给模块单独供电,问题消失。排查的时候用示波器抓了VBAT波形,发现发送瞬间电压掉到了3.1V以下,超出了模块工作电压范围。
7.5 踩坑记录四:OLED不亮或者花屏
OLED不亮的原因多半是I2C地址不对。SSD1306的I2C地址默认是0x78或0x7A,取决于SA0引脚电平。有的模块用的是0x3C,有的用0x3D,你得自己试。我在代码里专门做了地址扫描,初始化时尝试两个地址,读到ACK的就是正确的。
另外OLED的花屏,大多是因为刷新和数据传输不同步。如果你在显示中途有重置或者断电,SSD1306内部显存状态会乱。解决方法是初始化时发一次清屏指令,把所有GDDRAM清零。
8. 扩展思路:从按键发短信到设备远程报警系统
做完这个项目后,你会发现核心链路其实是“事件检测→编码→AT指令发送→状态反馈”。按键只是事件来源的一种,短信也只是输出方式的一种。我后续把它改造成了环境监测设备的远程报警,传感器检测到温湿度超限就自动发短信到管理员手机,OLED上显示当前环境参数和报警状态。
改造的部分只有几点:
- 把按键触发改成传感器触发,设置好阈值判断逻辑
- 短信内容改成格式化的传感器数据
- 增加多目标号码支持,用手机号轮询的方式发给多个管理员
Air780E还支持TCP/UDP透传,这意味着同样的框架可以走MQTT上报或者HTTP POST到服务器。我试过用Air780E的TCP透传功能把设备数据发送到自己的服务器,AT指令流程类似,只是把AT+CMGS换成了AT+TCPSEND之类的指令。如果你有更复杂的物联网需求,这个板子完全能撑起来。
最后分享一个调试技巧:AT指令调试的时候,STM32的串口1接USB转TTL到电脑,串口2接Air780E。电脑上打开串口助手监听串口1的log,同时在代码里把发送和接收到的AT指令都打印出来。这样你能实时看到模块和MCU之间到底发生了什么,排查定位的效率比盲调高太多。
这个项目整体难度适中,核心难点在PDU编码和AT指令时序,但只要理清了流程,一次点亮没有太大问题。如果哪里卡住了,可以按我文章里的排查顺序一步步来,基本都能解决。