news 2026/10/3 8:08:42

STM32+Air780E+OLED:按键发送中文短信与PDU编码实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32+Air780E+OLED:按键发送中文短信与PDU编码实战

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

按键发短信这件事,听起来像是功能机时代的老古董,但在工业现场、远程告警、老人看护、农业大棚这些场景里,它依然是最朴素也最可靠的一种通信手段。我这次要做的,就是拿一块 STM32 最小系统板,配上一片 Air780E 的 4G Cat.1 模组,再加一块 0.96 寸的 OLED 屏,实现“按一下键,发一条中文短信,屏幕上同步显示发送状态”的完整闭环。核心关键词就四个:STM32、Air780E、OLED、AT 指令,而真正让大多数人卡住的,是中文短信背后的 PDU 编码。

先说清楚这个项目能干什么。你手头有一块 STM32F103C8T6 或者类似的板子,接上 Air780E 模组,再挂一块 I2C 的 OLED,按下按键之后,模组把一条预设好的中文短信发到指定手机号,OLED 上实时显示“正在发送”“发送成功”或者“发送失败”。它解决的是“设备侧主动告警”的问题——不需要联网服务器,不需要云平台,一条短信直达。适合谁看?适合已经会点 STM32 基础、能点灯能串口打印、想往物联网通信方向迈一步的朋友;也适合做毕设、做工业报警器、做远程抄表的工程师拿来当参考。

为什么选 Air780E 而不是别的模组?这是有讲究的。市面上常见的 4G 模组里,Air780E 是 Cat.1 制式,功耗和成本都比 Cat.4 低,而且它原生支持 AT 指令,串口透传逻辑简单,中文短信走 PDU 模式也很成熟。相比之下,某些模组虽然便宜,但 AT 指令集不完整,发中文短信时容易踩坑。OLED 选 0.96 寸 I2C 版本,是因为它只要两根线,不占用太多 IO,STM32F103C8T6 这种引脚紧张的芯片最合适。整个方案的核心思路就是:STM32 当大脑,负责按键扫描、状态机调度、OLED 刷新;Air780E 当嘴巴,负责把 PDU 编码后的短信发出去;OLED 当脸面,负责把过程可视化。

这里有个关键决策点:短信内容怎么组织?直接发 ASCII 英文最简单,但标题要求中文,就必须用 PDU 模式。PDU 是“Protocol Data Unit”的缩写,中文短信在 GSM 协议里不能直接以明文发送,必须经过 UCS2 编码再拼成 PDU 串。很多人第一次接触 PDU 会觉得头大,其实拆开看就是几段固定格式加一串十六进制。我后面会详细讲怎么算。

另一个决策点是按键触发方式。我试过轮询和外部中断两种,最后选了轮询加软件消抖。原因很简单:外部中断在按键抖动时容易误触发,而轮询放在主循环里,配合 20ms 的消抖延时,稳定得多。OLED 刷新也不能放在中断里,I2C 时序对时间敏感,中断里刷屏容易导致通信失败。

整体硬件连接是这样的:STM32 的 USART2 接 Air780E 的串口,波特率 115200;I2C1 接 OLED,地址通常是 0x78;按键接在 PA0,下拉输入,按下为高电平。电源部分要注意,Air780E 在发射瞬间电流能冲到 2A,所以供电必须给足,最好单独用一路 3.8V 的 DC-DC,不要直接从 STM32 的 3.3V LDO 上取电,否则一发短信就复位。

2. 核心细节解析与实操要点

2.1 Air780E 的 AT 指令交互逻辑

Air780E 上电之后,串口会输出一堆启动日志,这时候不要急着发指令,等它稳定下来,先发一个AT,看它回不回OK。这是最基本的握手。如果连AT都不回,先查波特率,Air780E 默认是 115200,但有些固件版本是 9600,我遇到过两次,折腾了半天才发现是波特率不对。

握手成功之后,要依次配置几个关键参数。第一,AT+CMGF=0,把短信模式设成 PDU 模式。注意,AT+CMGF=1是文本模式,只能发英文,发中文会乱码或者直接失败。第二,AT+CSCS="UCS2",设置字符集为 UCS2,这一步很多人会漏掉,结果 PDU 串拼出来是对的,但模组解析出错。第三,AT+CSMP=17,167,0,8,设置短信参数,其中最后一个参数 8 表示 UCS2 编码。这个指令里的 17 是 FO 参数,167 是 VP 有效期,0 是 PID,8 是 DCS。DCS 设为 8 就是告诉模组“我要用 UCS2 发中文”。

发短信的指令是AT+CMGS=<length>,这里的 length 是 PDU 串去掉开头两个字符后的长度,也就是 TPDU 的长度,不是整个 PDU 串的长度。这个细节坑了无数人。举个例子,如果你的 PDU 串是0011000D916831...,那么 length 就是总长度除以 2 再减去 1。因为 PDU 串是十六进制字符串,两个字符代表一个字节,而AT+CMGS要的是字节数,且不包含 SMSC 地址部分。

发完AT+CMGS=<length>之后,模组会回一个>提示符,这时候才能把 PDU 串发进去,最后以Ctrl+Z(0x1A)结尾。注意,Ctrl+Z是十六进制的 0x1A,不是字符串“Ctrl+Z”。我在代码里用USART_SendData(USART2, 0x1A)直接发这个字节。如果发的是Ctrl+C(0x1B),就是取消发送。

2.2 中文短信的 PDU 编码计算

PDU 编码是這個项目里最硬核的部分。我把它拆成四段来看:SMSC 地址段、TPDU 首字节、目标号码段、短信内容段。

SMSC 地址段通常写00,表示使用模组默认的短信中心号码。如果你要指定,就写07加号码长度加91加号码。我一般直接用00,省事。

TPDU 首字节是11或01,11表示有状态报告,01表示没有。我选11,因为 OLED 上要显示发送状态,有状态报告更靠谱。

目标号码段稍微复杂。假设手机号是13800138000,先算长度:11 位,十六进制是0B。然后加91表示国际格式。接着把号码两两颠倒:13 80 01 38 00 00变成31 08 10 83 00 F0。注意最后如果奇数位,补F。所以号码段就是0B91加3108108300F0。

短信内容段是 UCS2 编码。每个中文字符对应一个 Unicode 码点,比如“你”是4F60,“好”是597D。把每个字符的 Unicode 码点转成十六进制字符串,拼起来就是内容段。内容长度是字符数乘以 2,再转成十六进制。比如“你好”两个字,长度是 4 个字节,十六进制04。

把这些拼起来,完整的 PDU 串就是:00+11+00+0B91+3108108300F0+00+08+04+4F60597D。等等,这里有个00和08是什么?00是 TP-PID,08是 TP-DCS,表示 UCS2。这两个字节不能漏。

所以完整 PDU 是:0011000B913108108300F00008044F60597D。长度计算:去掉开头的00,剩下11000B913108108300F00008044F60597D,数一下字符数,是 38 个字符,除以 2 得 19 字节。所以AT+CMGS=19。

我在代码里写了一个pdu_encode函数,输入手机号和中文内容,输出 PDU 串和长度。这个函数我调了整整一个下午才跑通,主要坑在号码颠倒和长度计算上。

2.3 OLED 状态显示的设计

OLED 用 I2C 驱动,我选的是 SSD1306 芯片,0.96 寸,128x64 分辨率。驱动库我用的是自己精简过的 HAL 库版本,没有用现成的 u8g2,因为 u8g2 占 Flash 太大,STM32F103C8T6 只有 64KB Flash,塞进去有点紧张。

显示内容分三行:第一行显示“短信发送”,第二行显示当前状态,比如“正在发送...”“发送成功”“发送失败”,第三行显示重试次数或者错误码。状态切换的时候,我只刷新变化的那一行,不全屏刷新,这样 I2C 占用时间短,不会阻塞主循环。

OLED 初始化的时候要注意,0.9 寸和 0.96 寸的 I2C 地址可能不一样,0.96 寸通常是 0x78,0.9 寸有些是 0x7A。我手里这块 0.96 寸的,地址是 0x78。如果你买的是 0.9 寸,先扫 I2C 地址确认一下。另外,OLED 的供电最好是 3.3V,有些模块标称 5V 兼容,但长期跑 5V 容易烧。

显示汉字需要取模。我用的是 PCtoLCD2002 软件,取模方式选“阴码+逐列式+顺向”,每个汉字 16x16 像素,占 32 个字节。把取模数据放到一个数组里,显示的时候按列写入。这里有个小技巧:把常用的状态文字提前取好模,比如“正在发送”“发送成功”“发送失败”,存成常量数组,用的时候直接索引,不用动态取模。

3. 实操过程与核心环节实现

3.1 硬件连接与供电检查

先把硬件接起来。STM32F103C8T6 的 PA2 接 Air780E 的 RXD,PA3 接 TXD,GND 共地。OLED 的 SCL 接 PB6,SDA 接 PB7,VCC 接 3.3V,GND 共地。按键一端接 PA0,另一端接 3.3V,PA0 配置成下拉输入。Air780E 的 VCC 接 3.8V,这个 3.8V 我用的是单独的 DC-DC 模块,输入 5V,输出 3.8V,最大电流 3A。

供电检查这一步不能省。我第一版直接拿 STM32 板子上的 3.3V 给 Air780E 供电,结果一按按键发短信,STM32 就复位,OLED 黑屏。后来用示波器看,Air780E 发射瞬间把 3.3V 拉到了 2.1V,STM32 的 BOR 复位了。换成独立 3.8V 供电之后,问题消失。所以记住:Air780E 的供电必须独立,且电流能力不低于 2A。

串口线也要注意,Air780E 的串口是 1.8V 电平还是 3.3V 电平?我这款是 3.3V 电平,可以直接接 STM32。如果你买的是 1.8V 版本,中间要加电平转换。不确定的话,先看模组丝印或者问卖家。

3.2 STM32 端代码框架

代码分四个模块:串口驱动、OLED 驱动、按键扫描、PDU 编码。串口驱动用 HAL 库的HAL_UART_Receive_IT开中断接收,把模组返回的数据存到一个环形缓冲区里。主循环里解析缓冲区,找OK、ERROR、+CMGS:这些关键字。

OLED 驱动我写了三个函数:OLED_Init、OLED_ShowString、OLED_Refresh。OLED_ShowString支持 ASCII 和汉字,汉字用索引查表。OLED_Refresh把显存刷到屏幕,我用的页地址模式,每次刷一页,8 页刷完。

按键扫描放在SysTick中断里,每 10ms 扫一次,连续 3 次读到高电平才确认按下。确认按下后,置一个标志位key_pressed,主循环检测到这个标志位就启动发送流程。

发送流程是一个状态机:IDLE->SEND_AT->WAIT_OK->SEND_CMGF->WAIT_OK->SEND_CMGS->WAIT_PROMPT->SEND_PDU->WAIT_RESULT->DONE。每个状态等模组回复,超时 3 秒就跳到ERROR状态,OLED 显示“发送失败”,然后重试一次。

3.3 PDU 编码函数的实现细节

pdu_encode函数的输入是手机号字符串和中文内容字符串,输出是 PDU 串和长度。我一步步写:

第一步,处理手机号。把手机号的每个字符转成数字,然后两两交换。如果长度是奇数,最后补F。前面加0B91,其中0B是长度,91是国际格式。

第二步,处理内容。把每个中文字符的 Unicode 码点取出来,转成 4 位十六进制字符串,拼起来。内容长度是字符数乘以 2,转成 2 位十六进制。

第三步,拼 TPDU。11+00+ 号码段 +00+08+ 内容长度 + 内容段。

第四步,拼 SMSC。00+ TPDU。

第五步,算长度。PDU 串总字符数除以 2,再减去 1,就是AT+CMGS的参数。

我举个例子,手机号13800138000,内容“你好”。手机号处理:13 80 01 38 00 00-> 交换 ->31 08 10 83 00 F0,前面加0B91,得到0B913108108300F0。内容处理:“你”是4F60,“好”是597D,拼起来4F60597D,长度04。TPDU 是11+00+0B913108108300F0+00+08+04+4F60597D。完整 PDU 是00+11000B913108108300F00008044F60597D。长度:去掉开头00,剩下 38 个字符,除以 2 得 19。所以发AT+CMGS=19。

这个函数我封装成void pdu_encode(char *phone, char *content, char *pdu_out, int *len_out),在 STM32 上跑没问题,因为字符串不长,栈空间够用。

3.4 发送流程的代码实现

主循环里检测到key_pressed标志后,先清标志,OLED 显示“正在发送...”,然后进入状态机。状态机用switch-case实现,每个 case 里发对应的 AT 指令,然后等回复。

发AT的时候,我直接调HAL_UART_Transmit发字符串AT\r\n,然后等 500ms,检查缓冲区里有没有OK。如果有,进入下一步;如果没有,重发一次,再没有就报错。

发AT+CMGF=0和AT+CSCS="UCS2"类似,都是发指令等OK。

发AT+CMGS=19之后,等模组回>。这个>是提示符,不是OK。我在缓冲区里找>字符,找到之后发 PDU 串,最后发0x1A。

发完 PDU 之后,模组会回+CMGS: <mr>和OK,表示发送成功。如果回ERROR或者+CMS ERROR,就是失败。我在缓冲区里找+CMGS:和OK,找到就显示“发送成功”,否则显示“发送失败”。

这里有个细节:模组返回的数据可能分多次到达,所以缓冲区要够大,我设了 256 字节。解析的时候用strstr找关键字,不要用strcmp,因为返回的数据前后可能有其他字符。

3.5 OLED 显示刷新与状态同步

OLED 刷新我放在主循环里,每 100ms 刷一次。状态变量sms_state有四个值:STATE_IDLE、STATE_SENDING、STATE_SUCCESS、STATE_FAIL。根据这个变量决定显示什么文字。

显示“正在发送...”的时候,我加了一个小动画,每 500ms 切换一次“.”的数量,从“.”到“..”到“...”,这样用户知道系统没死机。这个动画用SysTick计数实现,不占用额外定时器。

显示“发送成功”之后,保持 3 秒,然后自动回到STATE_IDLE,OLED 显示“按按键发送短信”。显示“发送失败”之后,保持 5 秒,然后回到STATE_IDLE,同时允许用户再按一次重发。

OLED 的显存我用了 1024 字节的数组,OLED_Refresh的时候按页写入。I2C 速率设成 400kHz,刷一屏大概 20ms,不影响主循环。

4. 常见问题与排查技巧实录

4.1 模组不回复 AT 指令

这是最常见的问题。先查波特率,Air780E 默认 115200,但有些固件是 9600。如果波特率对,查串口线有没有接反,TX 接 RX,RX 接 TX。如果线也对,查供电,用万用表量模组 VCC 是不是 3.8V,发射瞬间会不会掉。我遇到过供电不足导致模组反复重启,串口一直输出启动日志,根本不进 AT 模式。换独立供电之后解决。

还有一个坑:Air780E 上电后需要几秒钟初始化,如果你上电就发AT,它可能还没准备好。我在代码里加了 5 秒延时,等模组稳定后再发AT。

4.2 中文短信发送失败

如果模组回ERROR,先检查AT+CMGF=0和AT+CSCS="UCS2"有没有设对。然后检查 PDU 串的长度计算,AT+CMGS的参数是不是 TPDU 长度。我踩过的坑是长度算成了整个 PDU 串的长度,结果模组一直回ERROR。后来用串口助手手动发,对比长度,才发现问题。

另一个坑是Ctrl+Z发成了字符串。在代码里,HAL_UART_Transmit发的是字节数组,0x1A是一个字节,不是两个字符1和A。如果你用printf发"0x1A",那就错了。

还有,短信中心号码如果设错,也会失败。我一般用00让模组用默认的,如果默认的不对,就手动设AT+CSCA="+8613800100500",这个号码各地不同,问运营商。

4.3 OLED 不显示或显示乱码

OLED 不显示,先查 I2C 地址。用HAL_I2C_IsDeviceReady扫一下,地址 0x78 或 0x7A。如果地址对但不显示,查初始化序列,SSD1306 的初始化命令不能少,特别是0xAE(关显示)、0xD5(时钟分频)、0xA8(复用率)、0x8D(电荷泵)这几个。电荷泵不打开,屏幕不亮。

显示乱码通常是取模方式不对。PCtoLCD2002 里选“阴码+逐列式+顺向”,如果选成“阳码”或者“逆向”,显示就是乱的。另外,汉字取模是 16x16,ASCII 是 8x16,显示的时候坐标要算对,不然会重叠。

4.4 按键误触发

按键误触发一般是消抖没做好。我在SysTick里每 10ms 扫一次,连续 3 次读到高电平才确认,这样 30ms 内的抖动都滤掉了。如果还误触发,加一个 100ms 的冷却时间,发完短信之后 100ms 内不响应按键。

还有一种情况是按键接的上拉或下拉不对。我用的下拉输入,按键接 3.3V,按下为高。如果你接 GND,就要配上拉输入,按下为低。配置错了,按键一直触发或者一直不触发。

4.5 常见问题速查表

问题现象可能原因排查方法解决方案
模组不回 AT波特率不对换 9600 试改代码波特率
模组不回 AT供电不足量 VCC 电压独立 3.8V 供电
发中文失败PDU 长度错手动算长度用 TPDU 长度
发中文失败字符集不对查 CSCS设 UCS2
OLED 不亮电荷泵没开查初始化加 0x8D 命令
OLED 乱码取模方式错查取模设置阴码逐列顺向
按键误触发消抖不够加延时30ms 消抖
发送后复位电源跌落示波器看独立供电

4.6 实操心得与避坑技巧

第一个心得:PDU 编码不要手算,写个函数在 PC 上先验证。我用 Python 写了一个小脚本,输入手机号和内容,输出 PDU 串和长度,跟 STM32 上的函数对比,确保一致。这样调试的时候心里有底。

第二个心得:串口调试助手是你的好朋友。先用串口助手手动发 AT 指令,确认模组能发中文短信,再把流程搬到 STM32 上。这样能把硬件问题和代码问题分开。

第三个心得:OLED 显示状态的时候,不要显示太多文字,三行足够。第一行标题,第二行状态,第三行错误码。错误码用数字,比如E01表示模组无响应,E02表示 PDU 错误,E03表示发送失败。这样排查的时候一目了然。

第四个心得:Air780E 的固件版本不同,AT 指令集可能有差异。我遇到过一款固件不支持AT+CSMP的第四个参数,后来换成AT+CMGF=0加AT+CSCS="UCS2"就够了。所以如果指令报错,先查固件手册。

第五个心得:STM32 的串口接收中断里不要做太多事,只把数据存进缓冲区,解析放在主循环。中断里做strstr会阻塞太久,导致丢数据。

第六个心得:如果项目要长期运行,加一个看门狗。我在主循环里喂IWDG,防止模组死机导致程序卡死。模组死机的时候,STM32 可以复位模组,通过控制模组的 RESET 引脚。

这个项目我从画板子到调通,前前后后花了大概一周时间,其中 PDU 编码占了一半。现在回头看,最难的不是技术本身,而是把每个细节都抠清楚。短信发送成功那一刻,OLED 上跳出“发送成功”四个字,那种成就感还是挺实在的。如果你也在做类似的东西,希望这些经验能帮你少走点弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 8:08:29

Oracle性能优化实战:SGA调整与SQL改写要点解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 8:08:10

DRV8818PWPR+STM32L073RZ工业步进驱动硬核实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 8:07:39

RocketMQ 5.x架构解析:NameServer与新增组件详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 8:07:22

金蝶云苍穹插件开发:加载数据实战详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 8:07:00

智能车电路组开源项目解析:从原理图到整机调试的硬件设计指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 8:06:26

SMMU深度解析:设备DMA内存保护与地址翻译机制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华