☰
STM32与Air780E实现中文短信发送:AT指令与PDU编码实战
2026/10/3 3:21:28 网站建设 项目流程

1. 项目缘起与整体方案拆解

按键一按,短信就发到手机上,同时OLED屏幕上还能看到当前状态——这个需求听起来简单,但真要做出来,涉及的东西比想象中多。STM32负责逻辑控制和界面显示,Air780E负责蜂窝网络通信,两者通过串口用AT指令交互,短信内容还得经过PDU编码才能发中文。这套组合在物联网报警、远程设备状态上报、老人紧急呼叫器等场景里非常实用,成本也压得住,整套硬件算下来不到五十块钱。

我最初做这个项目是因为一个做养殖场的朋友,他的鱼塘增氧机需要一套远程报警装置,断网断WiFi的情况下还能把报警短信发出去。WiFi方案直接排除,蜂窝网络是唯一选择。选Air780E是因为它支持Cat.1,功耗和成本比Cat.4模块低不少,而且AT指令集兼容性好,资料也全。STM32这边用F103C8T6就够,资源绰绰有余,OLED用0.96寸I2C接口的SSD1306,显示个状态信息完全没问题。

整个系统的数据流是这样的:按键触发外部中断,STM32检测到后先更新OLED显示“正在发送”,然后通过UART向Air780E发送一系列AT指令,完成短信的PDU编码、发送、确认,最后根据模块返回的结果更新OLED显示“发送成功”或“发送失败”。这里面最核心的技术点有三个:AT指令的时序控制、中文短信的PDU编码、以及OLED的多级状态刷新。

为什么不用Air780E自带的透传模式?因为透传模式下你没法精确知道短信到底发出去没有,模块返回的确认信息会被透传模式吞掉。用AT指令虽然麻烦一点,但每一步都有明确的响应,可靠性高得多。而且PDU编码虽然看起来复杂,但理解了它的结构之后,用C语言实现也就是几十行代码的事。

注意:Air780E的固件版本不同,AT指令集可能有细微差异。建议先用USB转TTL模块连电脑,用串口助手把所有指令跑一遍,确认响应格式后再写代码。

2. 硬件选型与接线要点

2.1 核心器件清单与选型理由

先列一下我实际用的物料,都是市面上容易买到的:

器件型号数量备注
主控STM32F103C8T6最小系统板1蓝色药丸板即可
通信模块Air780E开发板1注意买带SIM卡座的版本
显示屏0.96寸OLED SSD1306 I2C14针接口,VCC/GND/SCL/SDA
按键6x6mm轻触开关1接在PA0上,用外部中断
电源AMS1117-3.3 + 5V输入1Air780E峰值电流能到2A,LDO要选大电流的
SIM卡移动/联通/电信4G卡1注意要开通短信功能

Air780E的工作电压是3.3V到4.2V,典型值3.8V。我一开始直接用STM32板子上的3.3V给它供电,结果模块注册网络的时候电流一冲,STM32直接复位了。后来换成独立的AMS1117-3.3,输入端并了一个1000uF的电解电容和一个0.1uF的陶瓷电容,问题才解决。这个坑很典型,蜂窝模块的瞬态电流远比你想象的大,电源设计绝对不能省。

OLED这边没什么好说的,I2C接口就四根线,VCC接3.3V,GND接地,SCL接STM32的PB6,SDA接PB7。注意SSD1306的I2C地址通常是0x78(写地址),有些模块是0x7A,用之前先用I2C扫描程序确认一下。

2.2 串口连接与电平匹配

STM32和Air780E之间用UART通信,我选的是USART2,TX接PA2,RX接PA3。Air780E的串口电平是3.3V,和STM32直接对接没问题,不需要电平转换。但有一点要注意:Air780E的串口默认波特率是115200,但有些固件版本可能是9600,上电后用AT指令确认一下,如果是9600就发AT+IPR=115200改过来。

按键接在PA0上,配置成下拉输入,上升沿触发外部中断。为什么用中断而不是轮询?因为轮询会占用主循环的时间,而且按键抖动处理起来麻烦。用外部中断配合简单的软件消抖(在中断服务函数里延时20ms再读一次电平),既省CPU又可靠。

实操心得:PA0同时也是STM32的WKUP引脚,如果你用到了待机唤醒功能,要注意别冲突。我一般把按键放在PA1上,避开这个坑。

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

3.1 Air780E短信相关AT指令速查

Air780E发短信的AT指令流程不算复杂,但顺序不能乱。下面是我实测可用的指令序列:

AT // 测试模块是否在线 AT+CPIN? // 查询SIM卡状态,返回+CPIN: READY表示正常 AT+CSQ // 查询信号质量,第一个参数大于10基本可用 AT+CMGF=0 // 设置为PDU模式,发中文必须用PDU AT+CMGS=<length> // 发送短信,length是PDU数据的长度(不含中心号码) > <PDU数据> // 收到>提示后输入PDU数据 // 最后发送Ctrl+Z(0x1A)触发发送

这里有几个关键点。AT+CMGF=0是必须的,因为Text模式不支持中文。AT+CMGS后面的长度参数是PDU数据的字节数,不是字符数,算错了模块会返回ERROR。PDU数据输入完后要发0x1A(也就是Ctrl+Z),不是回车,这个很容易搞错。

模块返回+CMGS: <mr>表示发送成功,mr是消息参考号。如果返回+CMS ERROR: <code>就是失败了,常见的错误码有:

错误码含义排查方向
10SIM卡未插入检查卡座接触
13短信内存满删除旧短信
21目标号码格式错误检查号码位数
38网络未注册等待注册或检查天线
500未知错误重启模块试试

3.2 PDU编码原理与C语言实现

PDU编码是中文短信的核心难点。它的结构是这样的:

[短信中心号码长度][短信中心号码][PDU类型][目标号码长度][目标号码][协议标识][编码方式][有效期][用户数据长度][用户数据]

短信中心号码一般不用自己填,模块会自动从SIM卡读取。目标号码需要做半字节交换,比如号码13800138000,前面补上86(中国区号),变成8613800138000,然后两两交换:683108103800F0。注意最后如果是奇数位,补F凑成偶数。

用户数据部分,中文用UCS2编码,每个字符占2个字节。比如“你好”的Unicode是4F60597D,直接按大端序排列就行。用户数据长度是Unicode字节数,不是字符数。

下面是我用的PDU编码函数核心逻辑:

// 将字符串转换为UCS2编码的PDU用户数据 // 输入:src-源字符串(UTF-8或GBK),dst-输出缓冲区 // 返回:UCS2字节数 int str_to_ucs2(const char *src, uint8_t *dst) { int len = 0; // 这里假设src是GBK编码,需要查表转Unicode // 实际项目中我用了一个小的GBK-Unicode映射表 while (*src) { uint16_t unicode = gbk_to_unicode(src); dst[len++] = (unicode >> 8) & 0xFF; dst[len++] = unicode & 0xFF; src += 2; // GBK汉字占2字节 } return len; }

如果你不想自己维护GBK-Unicode映射表,有个取巧的办法:把要发送的中文短信预先转成Unicode码点数组,直接硬编码在程序里。对于固定内容的报警短信,这样做最省事。

PDU数据的组装我用了一个缓冲区,按顺序拼接各个字段。最后计算总长度时,注意AT+CMGS后面的长度参数是“PDU类型+目标号码+协议标识+编码方式+有效期+用户数据长度+用户数据”的总字节数,不包括短信中心号码部分。

注意:不同运营商对短信中心号码的处理方式略有差异。移动的短信中心号码通常是+8613800XXX500,联通是+8613010XXX500。如果你自己拼接PDU时包含了短信中心号码,AT+CMGS的长度参数要相应调整。我建议直接用AT+CSCA?查询模块自动获取的号码,省得自己算。

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

4.1 主程序状态机设计

整个程序我用了一个简单的状态机来管理,避免在中断里做太多事情:

typedef enum { STATE_IDLE, // 空闲,显示待机界面 STATE_SENDING, // 正在发送短信 STATE_SUCCESS, // 发送成功 STATE_FAILED, // 发送失败 STATE_WAIT_ACK // 等待模块响应 } SystemState; SystemState g_state = STATE_IDLE; uint32_t g_state_timer = 0;

按键中断里只做一件事:把g_state从STATE_IDLE改成STATE_SENDING,然后置一个标志位。主循环检测到状态变化后,才开始执行AT指令流程。这样做的好处是中断服务函数极短,不会影响其他中断的响应。

OLED刷新也放在主循环里,根据当前状态显示不同内容。我定义了三个显示页面:

  • 待机页:显示“系统就绪”和信号强度
  • 发送页:显示“正在发送...”和一个动态省略号
  • 结果页:显示“发送成功”或“发送失败”加错误码

4.2 AT指令发送与响应解析

发送AT指令的函数我封装成了阻塞式,带超时机制:

// 发送AT指令并等待预期响应 // cmd: 要发送的指令 // expect: 期望的响应关键字 // timeout: 超时时间(毫秒) // 返回:0-成功,-1-超时,-2-收到ERROR int at_send_wait(const char *cmd, const char *expect, uint32_t timeout) { uart_send_string(cmd); uart_send_string("\r\n"); uint32_t start = get_tick(); while (get_tick() - start < timeout) { if (uart_rx_contains(expect)) { uart_clear_rx_buffer(); return 0; } if (uart_rx_contains("ERROR")) { uart_clear_rx_buffer(); return -2; } } return -1; }

串口接收我用的是DMA+空闲中断的方式,这样不会丢数据。接收缓冲区开512字节,对于AT指令的响应足够了。解析响应时,我用了简单的字符串查找,没有上复杂的解析器,因为Air780E的响应格式很固定。

发送短信的完整流程封装成了一个函数:

int send_sms(const char *phone, const char *content) { char pdu_buf[256]; int pdu_len; // 1. 检查模块状态 if (at_send_wait("AT", "OK", 1000) != 0) return -1; // 2. 设置PDU模式 if (at_send_wait("AT+CMGF=0", "OK", 1000) != 0) return -2; // 3. 生成PDU数据 pdu_len = build_pdu(phone, content, pdu_buf); if (pdu_len < 0) return -3; // 4. 发送短信 char cmgs_cmd[32]; sprintf(cmgs_cmd, "AT+CMGS=%d", pdu_len); if (at_send_wait(cmgs_cmd, ">", 2000) != 0) return -4; // 5. 发送PDU数据 uart_send_string(pdu_buf); uart_send_byte(0x1A); // Ctrl+Z // 6. 等待发送结果 if (at_send_wait("", "+CMGS:", 10000) == 0) { return 0; // 成功 } return -5; // 失败 }

4.3 OLED显示驱动与界面设计

OLED我用的是HAL库的硬件I2C,速度配置成400kHz。SSD1306的初始化序列比较长,但都是标准流程,网上随便找个例程都能用。我重点说一下界面设计。

0.96寸OLED只有128x64像素,能显示的信息有限。我用了8x16的ASCII字体和16x16的汉字字体。汉字取模用的是PCtoLCD2002,设置成“阴码、逐列式、顺向”,这样和SSD1306的显存格式匹配。

显示刷新我做了个简单的双缓冲机制:先在内存里画好一帧,再一次性写入OLED。这样刷新时不会看到闪烁。内存占用是128*8=1024字节,F103C8T6的20KB RAM完全扛得住。

uint8_t oled_buffer[8][128]; // 显存缓冲区 void oled_refresh(void) { for (uint8_t page = 0; page < 8; page++) { oled_set_cursor(page, 0); i2c_write_data(oled_buffer[page], 128); } }

状态显示我用了不同的图标和文字组合。待机时显示一个天线图标和信号强度值,发送中显示一个旋转的箭头,成功显示对勾,失败显示叉号。这些图标都是16x16的,用取模软件生成。

实操心得:OLED的I2C通信容易被中断打断,导致显示错乱。我的做法是在刷新OLED时关掉全局中断,刷完再打开。虽然会短暂影响实时性,但OLED刷新一次只要几毫秒,对按键响应的影响可以忽略。

5. 常见问题与排查实录

5.1 模块不响应AT指令

这是最常见的问题,表现是发什么指令都没反应。排查顺序是这样的:

先确认波特率。Air780E上电后默认波特率是115200,但如果你之前改过没保存,下次上电可能还是旧值。用串口助手以9600和115200分别试一下,发AT看有没有OK返回。

再确认电源。用万用表量模块的VCC引脚,发送指令时电压不能低于3.3V。如果掉到3.0V以下,就是供电不足,换大电流LDO或者并个大电容。

最后确认串口线序。TX接RX,RX接TX,这个不用多说,但新手经常搞反。Air780E开发板上一般有丝印标注,仔细看一下。

5.2 短信发送失败错误码解析

模块返回+CMS ERROR时,错误码是关键线索。我整理了一个速查表:

错误码可能原因解决方法
10SIM卡未识别重新插拔,用橡皮擦擦触点
13短信存储满发AT+CMGD=1,4删除所有短信
21号码格式错误检查是否加了86前缀
38网络未注册检查天线,等待AT+CREG?返回1或5
50编码错误检查PDU数据长度和格式
500模块内部错误重启模块,检查固件版本

我遇到最多的是错误码38,原因是天线没接好。Air780E对天线比较敏感,用那种几块钱的FPC天线就行,但一定要插紧。信号强度用AT+CSQ查询,第一个值在10以上才能稳定发短信,低于5基本没戏。

5.3 OLED显示异常排查

OLED的问题一般分三类:完全不亮、显示乱码、显示偏移。

完全不亮先查供电,VCC要有3.3V,GND要共地。然后查I2C地址,用扫描程序确认地址是0x78还是0x7A。最后查初始化序列,SSD1306上电后需要延时100ms再发初始化指令,太快了不行。

显示乱码通常是取模方式不对。PCtoLCD2002里设置成“阴码、逐列式、顺向”,和SSD1306的显存组织方式匹配。如果你用的是“逐行式”,显示出来就是乱的。

显示偏移一般是页地址设置错了。SSD1306的显存分8页,每页8行像素。设置页地址用0xB0+page,列地址低4位用0x00+(col&0x0F),高4位用0x10+(col>>4)。这三个指令的顺序不能乱。

5.4 按键抖动与误触发

机械按键的抖动时间一般在5ms到20ms之间。我在中断服务函数里做了20ms的延时消抖,但这样会阻塞其他中断。更好的做法是用定时器做消抖:中断里启动一个20ms的单次定时器,定时器到期后再读按键电平,如果还是按下状态才认为是有效触发。

还有一种情况是按键按下后连续触发多次。这是因为中断标志没清除干净。STM32的外部中断在进入服务函数后会自动清除标志,但如果你用的是HAL库的HAL_GPIO_EXTI_Callback,需要在回调函数里手动调用__HAL_GPIO_EXTI_CLEAR_IT清除标志。

避坑技巧:如果你发现按键偶尔会触发两次,先检查是不是长按导致的。可以在发送完短信后加一个2秒的“冷却期”,期间忽略所有按键输入。

6. 项目扩展与优化方向

这套基础框架搭好之后,能扩展的方向很多。我目前正在做的一个改进是加入短信接收功能,用AT+CMGR读取未读短信,解析PDU后显示在OLED上。这样就能实现双向通信,远程查询设备状态。

另一个方向是降低功耗。Air780E支持PSM和eDRX省电模式,在不需要发送短信的时候让模块进入休眠,需要时用STM32的GPIO唤醒。实测下来,加上PSM之后待机电流能从15mA降到2mA以下,用电池供电能撑好几个月。

显示方面,0.96寸OLED确实有点小,我试过换成1.3寸的SH1106,分辨率一样是128x64,但可视面积大不少,驱动代码几乎不用改,只需要把初始化序列里的几行改一下就行。如果你要做产品化,建议直接上1.3寸的。

最后说一下代码结构。我现在的工程是用STM32CubeMX生成的HAL库框架,外设初始化都是自动生成的,我只需要在main.c里加业务逻辑。AT指令解析和PDU编码我单独放在了air780e.c和pdu.c里,OLED驱动在oled.c里,这样模块化之后移植到其他项目很方便。整个工程编译下来Flash占用大概30KB,RAM占用8KB左右,F103C8T6的64KB Flash和20KB RAM还有不少余量,后续加功能完全够用。

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

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

立即咨询