尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32+Air780E 4G短信报警系统:PDU编码与OLED状态显示实战

STM32+Air780E 4G短信报警系统:PDU编码与OLED状态显示实战 1. 项目缘起与整体方案拆解按键一按短信就发到手机上OLED屏幕上还能实时看到发送状态——这个需求听起来简单但真要从零搭出来涉及的知识面其实挺杂的STM32的GPIO和中断、I2C驱动的OLED显示、Air780E的AT指令交互、中文字符的PDU编码还有串口通信的稳定性处理。我当初做这个项目是因为手头有个场景需要在没有网络覆盖的环境下做远程告警WiFi和蓝牙都够不着最后选了4G Cat.1模组这条路。Air780E是合宙推出的一款Cat.1 bis模组性价比很高支持全网通走的是AT指令控制。它和STM32之间通过UART串口通信STM32发AT指令模组执行后返回结果。整个系统的核心逻辑就是按键触发中断STM32通过串口向Air780E发送一系列AT指令完成短信的编码和发送同时把每一步的状态显示在OLED上。这个项目适合谁呢如果你已经玩过STM32的基础例程比如点灯、串口收发、I2C驱动屏幕那这个项目正好是一个综合练习。它不涉及太复杂的算法但对通信协议的理解、状态机的设计、字符串处理能力都有一定要求。如果你是完全零基础建议先把STM32的串口和I2C搞明白再来看这个。整个方案的核心模块可以拆成四块按键输入检测、OLED状态显示、Air780E串口通信、中文短信PDU编码。这四块各自独立又互相配合下面我逐块拆开讲。2. 硬件选型与接线要点2.1 主控与模组的搭配逻辑STM32我选的是F103C8T6也就是大家常说的“最小系统板”。原因很简单这个项目对算力要求不高F103的72MHz主频、64KB Flash、20KB RAM完全够用而且资料多、踩坑少。如果你手头有F401或者其他型号也完全可以代码移植量不大。Air780E的供电需要注意它的峰值电流可以到2A左右所以不能用STM32板子上的3.3V LDO直接给它供电。我一开始就是图省事从板子上取电结果模组一注册网络就重启查了半天才发现是供电不足。后来单独用了一个DC-DC降压模块输入5V输出3.8V电流能力至少2A问题就解决了。串口连接上Air780E的主串口默认波特率是115200我用的是STM32的USART2TX接模组的RXRX接模组的TX交叉连接。另外模组的PWRKEY引脚需要拉低一段时间来开机我直接用了一个GPIO控制上电后拉低1.5秒再拉高模组就启动。2.2 OLED的I2C接线与地址确认OLED我用的是0.96寸的SSD1306I2C接口4针VCC、GND、SCL、SDA。接在STM32的I2C1上PB6是SCLPB7是SDA。这里有个坑要注意市面上很多0.96寸OLED模块的I2C地址是0x788位地址或者0x3C7位地址取决于模块背后的电阻配置。我手上这块是0x78但HAL库的I2C函数用的是7位地址所以代码里要写0x3C。如果你不确定自己的OLED地址可以写一个简单的I2C扫描程序遍历0x00到0x7F看哪个地址有应答。这个方法很实用尤其是当你换了一块不同厂家的屏幕时。模块引脚STM32引脚备注OLEDSCLPB6I2C1时钟OLEDSDAPB7I2C1数据Air780ETXPA3USART2_RXAir780ERXPA2USART2_TXAir780EPWRKEYPA1开机控制按键一端PA0外部中断按键另一端GND下拉触发按键我接在PA0上配置为下降沿触发的外部中断内部上拉。这样按键按下时引脚被拉低触发中断。加一个10ms的软件消抖避免误触发。3. Air780E的AT指令通信机制3.1 AT指令的基本交互流程Air780E的AT指令遵循标准的3GPP规范每条指令以AT开头以\r\n结尾。模组收到后会返回OK表示成功或者ERROR表示失败。有些指令还会返回中间信息比如查询信号质量会返回CSQ: 20,99这样的数据。发送短信的AT指令流程大致是这样的AT // 测试模组是否响应 ATCPIN? // 查询SIM卡状态 ATCSQ // 查询信号质量 ATCMGF0 // 设置为PDU模式 ATCMGS长度 // 发送短信后面跟PDU数据这里的关键是ATCMGF0它把短信模式设为PDU模式。为什么不用文本模式ATCMGF1因为文本模式不支持中文只能发ASCII字符。要发中文必须用PDU模式把中文按照GSM 03.38规范编码成十六进制字符串。3.2 串口接收的状态机设计串口通信最怕的就是数据收不全或者收多了。Air780E返回的数据长度不固定有时候一条指令的响应分几次到达有时候多条响应粘在一起。我一开始用简单的HAL_UART_Receive阻塞接收结果经常丢数据。后来改成了中断接收加环形缓冲区的方式。每收到一个字节就存进缓冲区主循环里检查缓冲区里有没有完整的响应。判断完整的依据是看有没有收到\r\n结尾的标志或者超时一定时间没有新数据。具体实现上我定义了一个uart_rx_buffer数组和一个rx_index变量。串口中断里把收到的字节存入缓冲区rx_index自增。主循环里检测rx_index是否大于0如果大于0就说明有数据然后分析缓冲区内容。分析完后清空缓冲区和rx_index。注意串口中断里不要做耗时操作只负责存数据。数据分析放在主循环里做避免中断嵌套导致的问题。3.3 指令发送与响应的超时处理每条AT指令发出后都要等模组响应。如果模组没响应不能一直死等要设一个超时时间。我的做法是发完指令后启动一个软件定时器比如5秒。在5秒内如果收到了预期的响应比如OK就继续下一步如果超时了就重发或者报错。这个超时机制很重要。实际测试中模组注册网络、发送短信这些操作耗时不确定有时候快有时候慢。如果不设超时程序卡死在一个地方整个系统就挂了。我用的超时时间是普通AT指令5秒短信发送15秒。短信发送涉及网络交互时间要留够。如果15秒还没返回基本可以判断是发送失败了。4. 中文短信的PDU编码实现4.1 PDU编码的基本原理PDUProtocol Data Unit是短信在网络上传输的格式。一条短信的PDU串包含了很多信息短信中心号码、目标号码、编码方式、时间戳、短信内容等。对于中文短信内容部分需要用UCS2编码也就是把每个中文字符转成2字节的Unicode码再转成十六进制字符串。举个例子汉字“你”的Unicode码是0x4F60在PDU串里就写成4F60。汉字“好”是0x597D写成597D。所以“你好”的PDU内容部分就是4F60597D。完整的PDU串还要在前面加上短信中心号码、目标号码等信息。短信中心号码的编码比较特殊需要做奇偶位交换。比如号码8613800138000去掉后是8613800138000在前面补86变成8613800138000然后每两位交换最后加上长度和类型标识。4.2 目标号码的编码处理目标号码的编码规则和短信中心号码类似但更简单一些。假设要发给13800138000先确定长度11位转成十六进制是0B。然后每两位交换31 08 10 83 00 F0最后如果长度是奇数补一个F。我在代码里写了一个函数encode_phone_number输入是字符串形式的号码输出是编码后的十六进制字符串。这个函数处理了奇偶位交换和奇数长度补F的逻辑。void encode_phone_number(const char *number, char *output) { int len strlen(number); int i; for (i 0; i len; i 2) { if (i 1 len) { sprintf(output strlen(output), %02X%02X, number[i1] - 0, number[i] - 0); } else { sprintf(output strlen(output), F%02X, number[i] - 0); } } }这段代码的逻辑是每次取两个字符交换顺序后转成十六进制。如果只剩一个字符就在前面补F。比如13800138000处理后就变成了3108108300F0。4.3 中文内容的UCS2编码中文内容的编码需要把每个汉字转成Unicode码。在STM32上字符串通常是UTF-8或者GBK编码的。如果你的编译器用的是UTF-8那一个汉字占3个字节如果是GBK占2个字节。不管哪种都需要先转成Unicode码。我用的方法是查表法。把常用的汉字和对应的Unicode码做成一个数组发送时查表获取。这个方法简单可靠但只适合固定内容的短信。如果要发任意中文就需要完整的Unicode转换表那会占用大量Flash空间。对于这个项目短信内容是固定的比如“按键触发报警”这六个字。我直接在代码里写死了对应的Unicode码const char *sms_content 4F60597D89E652A88B66; // 你好触发报警如果你需要发动态内容建议在PC端先把中文转成UCS2码然后通过串口传给STM32。这样STM32只负责拼接PDU串不负责编码转换负担小很多。4.4 完整PDU串的拼接把各部分拼在一起就得到了完整的PDU串。格式是这样的00 // 短信中心号码长度00表示用默认的 0B // 目标号码长度 91 // 目标号码类型 3108108300F0 // 目标号码编码 00 // 协议标识 00 // 编码方式00表示UCS2 AA // 有效期 4F60597D89E652A88B66 // 短信内容拼接完成后计算整个PDU串的长度不包括短信中心号码部分然后发送ATCMGS长度等模组返回后再把PDU串发过去最后发CtrlZ0x1A表示结束。提示PDU串的长度是十六进制字符串的长度除以2因为每两个十六进制字符代表一个字节。比如4F60597D是8个字符实际是4个字节。5. OLED状态显示的设计与实现5.1 SSD1306的驱动移植OLED我用的是SSD1306驱动芯片0.96寸128x64分辨率。HAL库的I2C驱动OLED网上有很多现成的代码我是在GitHub上找了一个轻量级的驱动只有两个文件oled.c和oled.h。移植的时候主要改I2C的句柄和地址。驱动里最核心的函数是OLED_WriteCmd和OLED_WriteData分别用来写命令和写数据。初始化的时候需要发送一系列命令来配置屏幕的对比度、扫描方向、显示模式等。这些命令在数据手册里都有但不需要全部理解直接抄现成的初始化序列就行。显示字符的函数是OLED_ShowString它把ASCII字符的字模数据从数组里取出来写到屏幕的GDDRAM里。每个字符占8x16个像素128x64的屏幕一行能显示16个字符一共4行。5.2 状态信息的布局设计屏幕上要显示的信息不多但需要清晰。我分了四行第一行模组状态比如“Air780E OK”或者“Air780E FAIL”第二行SIM卡状态比如“SIM Ready”或者“SIM Error”第三行信号质量比如“CSQ: 20”第四行发送状态比如“Sending...”或者“Send OK”这样的布局让操作者一眼就能看出问题出在哪。如果模组没响应第一行就会显示FAIL如果SIM卡没插好第二行会报错如果信号太差第三行会显示CSQ值很低。刷新策略上我没有用全屏刷新而是只更新变化的部分。全屏刷新会导致屏幕闪烁而且I2C传输数据量大影响主循环的实时性。我的做法是每次状态变化时只更新对应的那一行。比如发送状态从“Sending...”变成“Send OK”只重写第四行。5.3 中文字符的显示处理OLED显示中文比显示英文麻烦因为中文字模占用的空间大。一个16x16的中文字模需要32个字节而一个8x16的英文字模只需要16个字节。如果要在屏幕上显示中文需要先取模然后把数据写到屏幕的对应位置。我用的取模软件是PCtoLCD2002设置成“阴码、逐列式、顺向”生成的数组直接放到代码里。显示的时候调用OLED_ShowChinese函数传入中文字模数组和显示位置。不过这个项目里OLED主要显示状态信息用英文就够了。中文显示只在调试阶段用过正式版里没用到。如果你需要显示中文建议把常用的几个汉字取模后存到Flash里不要动态生成。显示内容字体大小占用像素刷新频率模组状态8x16128x16状态变化时SIM状态8x16128x16状态变化时信号质量8x16128x16每10秒发送状态8x16128x16状态变化时6. 按键中断与主循环的配合6.1 外部中断的配置按键接在PA0上配置为下降沿触发。在CubeMX里把PA0设为GPIO_EXTI0模式选“External Interrupt Mode with Falling edge trigger detection”然后使能NVIC中断。中断服务函数里我设置了一个标志位key_pressed然后清除中断标志。主循环里检测这个标志位如果为1就执行发送短信的流程执行完后把标志位清零。这里有个细节中断里不要做耗时操作也不要调用HAL_Delay。我见过有人在中断里直接发AT指令结果串口数据错乱。中断里只设标志位所有实际工作都在主循环里做。6.2 软件消抖的实现机械按键按下时会有抖动通常持续5到20毫秒。如果不消抖一次按下可能会触发多次中断。我的做法是在中断里设置标志位后主循环里先延时20毫秒再检测按键引脚的电平。如果还是低电平就确认是有效按下如果变成了高电平就认为是抖动忽略这次触发。if (key_pressed) { HAL_Delay(20); if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET) { // 确认按下执行发送流程 send_sms(); } key_pressed 0; }这个方法简单有效实测下来很少出现误触发。如果你想要更可靠的消抖可以用定时器做状态机消抖但代码会复杂一些。6.3 发送流程的状态机设计发送短信不是一步完成的需要经过多个步骤检查模组、检查SIM卡、检查信号、设置PDU模式、发送短信、等待结果。这些步骤不能一股脑全塞在一起否则出错时很难定位。我设计了一个简单的状态机用enum定义各个状态typedef enum { STATE_IDLE, STATE_CHECK_MODULE, STATE_CHECK_SIM, STATE_CHECK_SIGNAL, STATE_SET_PDU, STATE_SEND_SMS, STATE_WAIT_RESULT, STATE_DONE, STATE_ERROR } sms_state_t;主循环里根据当前状态执行对应的操作操作完成后切换到下一个状态。如果某一步出错就跳到STATE_ERROR在OLED上显示错误信息。这个状态机的好处是逻辑清晰每一步的职责明确。调试的时候我可以在OLED上显示当前状态一眼就能看出卡在哪一步。7. 常见问题与排查技巧实录7.1 模组无响应或返回ERROR这是最常见的问题可能的原因有好几个。首先检查供电Air780E的峰值电流很大如果电源带不动模组会反复重启。用万用表量一下模组供电引脚正常应该在3.8V左右如果低于3.5V就有问题。其次检查串口接线TX和RX有没有接反。我遇到过好几次TX接TXRX接RX结果怎么都不通。正确的接法是TX接RXRX接TX。还有波特率Air780E默认是115200但有些固件版本可能是9600。如果不确定可以发AT试试看有没有返回OK。如果没有换个波特率再试。现象可能原因排查方法无任何返回供电不足测量模组供电电压无任何返回串口接反交换TX和RX无任何返回波特率不对尝试9600和115200返回ERRORSIM卡未插好重新插拔SIM卡返回ERROR信号太差查询CSQ值返回ERRORPDU格式错误检查PDU串长度和内容7.2 短信发送失败但模组返回OK有时候模组返回OK但短信并没有发出去。这种情况通常是PDU串有问题。检查PDU串的长度是否正确长度是十六进制字符串长度除以2。如果长度算错了模组会接受指令但不会发送短信。另外检查目标号码的编码特别是奇数长度的号码最后一位要补F。我一开始漏了补F结果号码变成1380013800少了一位短信自然发不出去。还有一个坑是短信中心号码。如果PDU串里短信中心号码长度写00表示用SIM卡里存储的默认号码。但如果SIM卡里没有存短信中心号码发送就会失败。这时候需要在PDU串里显式指定短信中心号码。7.3 OLED不显示或显示乱码OLED不显示先检查I2C地址。用I2C扫描程序确认地址是0x3C还是0x78。如果地址不对屏幕不会有任何反应。如果显示乱码通常是初始化序列不对。不同的SSD1306模块初始化命令可能略有差异。我遇到过一块屏幕用标准初始化序列显示正常但换了一块同型号的屏幕就花屏了。后来发现是对比度设置不同调整0x81命令的参数后就好了。还有I2C速率的问题。有些OLED模块对I2C速率敏感400kHz可能太快降到100kHz就正常了。在CubeMX里可以配置I2C的时钟速率。7.4 按键触发不灵敏或连击按键触发不灵敏通常是消抖时间太短。机械按键的抖动时间可能长达20毫秒如果消抖只延时5毫秒可能还在抖动期内就检测了导致误判。把消抖时间加到20到30毫秒试试。连击的问题可能是中断标志没有正确清除。在中断服务函数里一定要清除对应的中断标志位否则中断会反复触发。另外如果按键引脚没有上拉悬空时电平不确定也会导致误触发。配置内部上拉或者外接一个10k的上拉电阻。实操心得调试按键的时候我习惯在中断里翻转一个LED这样能直观地看到中断触发了多少次。如果按一次LED闪多次就说明消抖没做好。8. 代码结构与关键实现细节8.1 工程目录的组织方式整个工程我分了几个模块main.c负责主循环和状态机air780e.c封装AT指令的发送和接收oled.c负责显示pdu.c负责PDU编码key.c负责按键处理。每个模块有对应的.h文件对外暴露接口函数。这样的组织方式让代码清晰很多。调试的时候如果短信发不出去我只用看air780e.c和pdu.c如果屏幕不亮只看oled.c。不用在一个几千行的main.c里翻来翻去。8.2 AT指令发送函数的封装我封装了一个air780e_send_cmd函数输入是指令字符串、期望的响应、超时时间返回是成功或失败。函数内部负责发送指令、等待响应、超时处理。int air780e_send_cmd(const char *cmd, const char *expect, uint32_t timeout) { uart_clear_buffer(); HAL_UART_Transmit(huart2, (uint8_t *)cmd, strlen(cmd), 1000); HAL_UART_Transmit(huart2, (uint8_t *)\r\n, 2, 100); uint32_t start HAL_GetTick(); while (HAL_GetTick() - start timeout) { if (strstr(uart_rx_buffer, expect) ! NULL) { return 0; // 成功 } if (strstr(uart_rx_buffer, ERROR) ! NULL) { return -1; // 失败 } } return -2; // 超时 }这个函数是整个通信模块的核心所有AT指令都通过它发送。uart_clear_buffer清空接收缓冲区避免上次的残留数据干扰。发送后轮询缓冲区看有没有期望的响应。8.3 PDU编码函数的实现PDU编码我写了一个pdu_encode函数输入是目标号码和短信内容输出是完整的PDU串。函数内部先编码号码再编码内容最后拼接。int pdu_encode(const char *number, const char *content, char *pdu_out) { char encoded_number[32] {0}; char encoded_content[256] {0}; encode_phone_number(number, encoded_number); encode_ucs2(content, encoded_content); int number_len strlen(number); int content_len strlen(encoded_content) / 2; sprintf(pdu_out, 00%02X91%s0000AA%s, number_len, encoded_number, encoded_content); return strlen(encoded_content) / 2; }返回值是短信内容的字节数这个值要传给ATCMGS指令。注意ATCMGS的长度参数是PDU串中除去短信中心号码部分的长度不是整个PDU串的长度。8.4 主循环的调度逻辑主循环里做三件事检测按键标志、执行状态机、刷新OLED。状态机的执行不是每轮都跑而是根据当前状态决定要不要执行。比如在STATE_IDLE状态下只有按键触发后才切换到STATE_CHECK_MODULE。while (1) { if (key_pressed) { HAL_Delay(20); if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET) { sms_state STATE_CHECK_MODULE; } key_pressed 0; } switch (sms_state) { case STATE_CHECK_MODULE: // 发送AT指令检查模组 break; case STATE_CHECK_SIM: // 检查SIM卡 break; // ... 其他状态 } oled_refresh(); }这个结构简单明了每个状态只做一件事做完就切换。调试的时候我可以在每个状态里加一句OLED显示这样就能看到状态机的流转过程。9. 实际测试与优化经验9.1 发送成功率的测试数据我在不同信号环境下做了测试记录了一些数据信号质量(CSQ)发送次数成功次数成功率20-315050100%10-19504896%5-9504080%0-4501530%CSQ值越高信号越好20以上基本没问题。10到19偶尔会失败但重发一次就能成功。5以下就很不稳定了建议换个位置再试。基于这个数据我在代码里加了一个重发机制如果发送失败自动重试两次。重试间隔3秒。这样即使信号不太好也能提高成功率。9.2 功耗优化的考虑这个项目如果要用电池供电功耗是个问题。Air780E在发送短信时电流能到2A待机时也有几毫安。STM32和OLED的功耗相对小一些但加起来也有十几毫安。我的优化做法是不发送的时候让Air780E进入休眠模式用ATCSCLK2指令。STM32也进入低功耗模式用按键中断唤醒。OLED在不需要显示的时候关闭显示用0xAE命令。这样整体待机电流能降到1mA以下用一节18650电池能撑好几天。不过休眠后模组需要重新注册网络发送短信的延迟会增加几秒。这个取舍要看具体需求。9.3 代码稳定性的改进一开始我的代码经常跑飞后来发现是串口缓冲区溢出。Air780E返回的数据有时候很长比如查询短信列表时返回的数据可能几百个字节。我的缓冲区只开了128字节不够用。后来把缓冲区加到512字节问题就解决了。另外在串口中断里加了溢出检测如果缓冲区满了就丢弃新数据并置一个错误标志。主循环里检测到这个标志就清空缓冲区重新开始。还有一个稳定性问题是看门狗。我在主循环里加了HAL_IWDG_Refresh如果程序卡死超过一定时间看门狗就会复位。这个功能在长时间运行的项目里很有必要。实操心得调试串口通信的时候我习惯用一个USB转TTL模块并联在STM32和Air780E之间的TX线上用串口助手抓包。这样能看到STM32发了什么模组回了什么比在代码里加打印方便多了。10. 项目扩展与功能升级方向10.1 支持动态短信内容现在的短信内容是写死的如果要发动态内容比如温度值、传感器数据就需要在运行时生成PDU串。我的思路是在PC端写一个转换工具把中文转成UCS2码然后通过串口传给STM32。STM32只负责拼接PDU串不负责编码转换。或者用STM32的Flash存一个常用汉字表只支持有限的汉字。这个方法适合内容变化不大的场景比如固定几个报警信息。10.2 增加电话拨打功能Air780E也支持拨打电话AT指令是ATD号码;。如果要增加这个功能可以在按键上做区分短按发短信长按打电话。OLED上显示拨号状态。不过打电话涉及语音通道Air780E需要外接麦克风和扬声器。硬件上要增加音频电路软件上要配置音频通道。这个扩展的复杂度比发短信高不少。10.3 接入云平台Air780E支持MQTT和HTTP可以把数据直接传到云平台。如果要远程查看设备状态这个功能很实用。不过云平台需要配置服务器地址、设备ID、鉴权信息等比发短信复杂。我的建议是先把短信功能做稳定再考虑云平台。短信的好处是不依赖服务器只要手机有信号就能收到可靠性比云平台高。10.4 多按键多号码管理现在的代码只支持一个按键发一个号码。如果要支持多个按键发不同号码可以扩展状态机每个按键对应一个号码。号码存在Flash里通过串口指令修改。这个扩展的难点在于号码管理。要设计一套简单的协议通过串口或者按键来添加、删除、修改号码。如果号码不多可以直接写死在代码里用宏定义区分。这个项目我从开始做到稳定运行前前后后花了大概两周时间。大部分时间不是在写代码而是在调试通信和排查各种奇怪的问题。最深的体会是串口通信一定要加超时和重试PDU编码一定要仔细核对长度和格式OLED显示一定要做局部刷新。这三点做好了项目就稳了一大半。
返回列表