ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32+Air780E 4G短信发送与OLED显示实战

STM32+Air780E 4G短信发送与OLED显示实战 1. 项目缘起与整体方案拆解按键一按短信就发到手机上而且还能在OLED小屏上看到当前状态——这个需求听起来简单但真做起来涉及的东西比想象中多。STM32负责逻辑控制和界面显示Air780E负责蜂窝通信两者通过串口用AT指令对话短信内容还得经过PDU编码才能发中文。这套组合在物联网报警、远程通知、设备状态上报等场景里非常实用成本也压得住整套硬件算下来不到五十块。我最初做这个项目是因为手头有个设备需要把告警信息推给值班人员但现场没有WiFi布线也麻烦用4G模块发短信是最省事的方案。Air780E是移柯推出的Cat.1模块支持LTE-FDD和LTE-TDD功耗和成本都比Cat.4低不少而且AT指令集兼容性做得不错上手门槛低。STM32这边我选的是F103C8T6蓝色药丸板资源够用资料铺天盖地出了问题好查。整个系统的数据流是这样的按键触发外部中断STM32检测到之后先通过串口给Air780E发AT指令确认模块注册到网络了然后切换到短信文本模式设置编码为PDU模式把中文短信内容编码成PDU格式的十六进制字符串再通过ATCMGS指令发送出去。发送结果通过串口返回给STM32STM32解析后更新OLED上的状态显示。OLED用的是0.96寸I2C接口的SSD1306驱动简单显示内容清晰。注意Air780E的串口电平是1.8VSTM32的串口是3.3V直接连会烧模块。必须加电平转换电路或者用模块自带的电平转换底板。我第一版就是忘了这事模块直接冒烟了。方案选型上为什么不用SIM800C因为SIM800C是2G模块现在很多地方2G基站已经退网了信号覆盖越来越差。Air780E支持4G Cat.1覆盖好而且功耗更低。为什么不用ESP32直接发短信ESP32没有蜂窝通信能力还得外挂模块多此一举。STM32Air780E的组合分工明确STM32管逻辑和显示Air780E管通信各司其职。PDU编码是绕不开的坎。中文短信不能用Text模式发因为Text模式只支持ASCII字符。PDU模式把短信内容编码成十六进制字符串支持Unicode字符集中文、英文、数字都能发。PDU编码的规则稍微有点绕但理解了之后就是套公式的事。后面我会详细拆解PDU编码的计算过程。OLED显示这块我用的u8g2库虽然比直接写SSD1306寄存器方便但u8g2的RAM占用不小F103C8T6只有20KB RAM得注意别把栈撑爆了。如果RAM紧张可以直接用SSD1306的驱动库自己写显示函数占用会小很多。2. 硬件连接与关键细节2.1 物料清单与接线方案先把手头的家伙什列一下免得做到一半发现少东西。物料型号/规格数量备注主控板STM32F103C8T6最小系统板1蓝色药丸板4G模块Air780E1带电平转换底板显示屏0.96寸OLED SSD1306 I2C14针接口按键轻触按键1接PA0外部中断电源5V/2A1模块发射时电流峰值可达2A电平转换TXS0108E或分压电阻13.3V转1.8V杜邦线若干-建议用硅胶线接线方案如下STM32的USART2_TXPA2接电平转换后接Air780E的RXDSTM32的USART2_RXPA3接电平转换后接Air780E的TXDSTM32的PB6接OLED的SCLPB7接OLED的SDASTM32的PA0接按键一端按键另一端接地PA0配置为上拉输入Air780E的PWRKEY接STM32的PB0用于控制模块开关机共地共地共地重要的事情说三遍提示Air780E的PWRKEY引脚需要拉低至少500ms才能开机松开后模块才会启动。我一开始只拉低了100ms模块死活不开机查了半天手册才发现时序不对。2.2 电源设计的坑Air780E在发射瞬间的电流峰值能到2A如果电源跟不上模块会反复重启串口返回一堆乱码。我一开始用USB口供电结果模块一发射就掉电OLED也跟着闪。后来换了个5V/2A的独立电源问题解决。电源部分建议加一个大电容比如470uF的电解电容并联在模块的VCC和GND之间用来缓冲发射瞬间的电流冲击。另外模块的VCC走线要粗别用细杜邦线压降太大。OLED的供电从STM32的3.3V取就行电流很小几十毫安。但要注意如果STM32的3.3V稳压器是从5V转过来的而5V又被Air780E拉垮了OLED也会跟着闪。所以最好给Air780E单独供电或者至少加个大电容。2.3 电平转换的必要性Air780E的串口电平是1.8VSTM32的是3.3V。如果直接连STM32的TX输出3.3V高电平会超过Air780E的RX耐压值长期工作会损坏模块。我第一版就是直接连的模块用了两天就挂了串口再也回不了数据。电平转换有两种方案一是用专用的电平转换芯片比如TXS0108E双向自动转换方便二是用分压电阻STM32的TX串一个1k电阻再并一个2k电阻到地分压后得到1.1V左右虽然不到1.8V但Air780E的RX高电平阈值是0.7*VCC1.26V1.1V有点悬建议用1k1.5k的分压得到1.32V稳妥一些。Air780E的TX输出1.8VSTM32的RX高电平阈值是0.7*3.3V2.31V1.8V不够所以Air780E的TX到STM32的RX也需要电平转换。可以用一个NPN三极管做电平转换或者用TXS0108E。我图省事直接用了TXS0108E八通道双向接两路就行。3. AT指令与PDU编码核心解析3.1 Air780E的AT指令流程Air780E的AT指令集和SIM800C、SIM7600系列基本兼容常用的指令就那么几条。发送中文短信的完整流程如下AT # 测试模块是否响应 ATCPIN? # 查询SIM卡状态 ATCREG? # 查询网络注册状态 ATCMGF0 # 设置为PDU模式 ATCMGSlength # 发送短信length是PDU字符串的长度 PDU字符串 # 输入PDU字符串以CtrlZ结束每一步都要等模块返回OK或者预期响应才能进行下一步。我试过连续发送指令不等响应结果模块直接卡死只能断电重启。ATCPIN?返回CPIN: READY表示SIM卡正常。ATCREG?返回CREG: 0,1表示已经注册到网络如果是0,2表示正在搜索0,0表示未注册。如果一直注册不上检查天线是否接好SIM卡是否欠费。ATCMGF0设置为PDU模式返回OK。ATCMGF1是Text模式只能发英文。ATCMGF2是PDU模式但用Text模式发送不常用。ATCMGS 中的length是PDU字符串的长度不是短信内容的长度。这个长度是TPDU的字节数不包括SMSC地址部分的长度。计算错了会返回ERROR。3.2 PDU编码的详细计算PDU编码是短信发送的核心难点我花了整整一个下午才搞明白。PDU字符串的格式如下SMSC地址 TPDUSMSC地址是短信中心号码一般用8613800100500编码后是0891683108100005F0。这个号码每个地区可能不一样但大多数情况下用默认的就行。如果发送失败可以问运营商要当地的短信中心号码。TPDU部分包含以下字段TP-MTI短信类型01表示发送TP-VP有效期一般用AA表示相对格式TP-MR消息参考号随便填00TP-DA目标号码即接收短信的手机号TP-PID协议标识一般用00TP-DCS数据编码方案08表示UnicodeTP-UDL用户数据长度TP-UD用户数据即短信内容以发送“你好”到手机号13800138000为例计算过程如下目标号码13800138000去掉号长度11位奇偶校验后编码为长度0B11位 号码31 08 31 08 00 F0每两位交换最后补FTP-DA字段0B 91 31 08 31 08 00 F0其中91表示国际格式0B是号码长度。短信内容“你好”的Unicode编码你4F60 好597DUnicode编码后是4F60597D共4个字节。TP-UDL 4 * 2 08因为Unicode每个字符2字节4字节对应8个十六进制字符TP-UD 4F60597D完整的TPDU01 00 0B 91 31 08 31 08 00 F0 00 08 08 4F 60 59 7D加上SMSC地址08 91 68 31 08 10 00 05 F0 01 00 0B 91 31 08 31 08 00 F0 00 08 08 4F 60 59 7D这个字符串的长度是34个十六进制字符即17字节。所以ATCMGS17。注意PDU字符串的长度是字节数不是字符数。34个十六进制字符是17字节所以ATCMGS17。我一开始填了34模块返回ERROR查了半天才发现是字节数。3.3 用代码实现PDU编码手动算一次可以但每次发短信都手动算不现实。我用C语言写了一个PDU编码函数输入手机号和短信内容输出PDU字符串。#include string.h #include stdio.h // 将Unicode字符串转换为PDU格式的十六进制字符串 void unicode_to_pdu(const char *unicode_str, char *pdu_str) { // unicode_str格式如4F60597D // pdu_str输出如4F60597D strcpy(pdu_str, unicode_str); } // 将手机号编码为PDU格式 void phone_to_pdu(const char *phone, char *pdu_phone) { int len strlen(phone); int i; char temp[4]; // 长度字节 sprintf(pdu_phone, %02X, len); pdu_phone 2; // 号码编码 for (i 0; i len; i 2) { if (i 1 len) { sprintf(temp, %c%c, phone[i1], phone[i]); } else { sprintf(temp, F%c, phone[i]); } strcat(pdu_phone, temp); } } // 生成完整的PDU字符串 void generate_pdu(const char *phone, const char *unicode_content, char *pdu) { char pdu_phone[32]; char smsc[] 0891683108100005F0; char tpdu_header[] 01000B91; char tpdu_footer[] 000808; phone_to_pdu(phone, pdu_phone); sprintf(pdu, %s%s%s%s%s, smsc, tpdu_header, pdu_phone, tpdu_footer, unicode_content); }这个代码只是示意实际使用中还需要处理字符串拼接、内存分配等问题。更稳妥的做法是用一个缓冲区逐步拼接。中文转Unicode可以用在线工具或者用STM32的GBK字库转换。如果短信内容是固定的可以提前算好Unicode编码直接写在代码里。如果需要动态生成就得在STM32上实现GBK到Unicode的转换这会占用不少Flash空间。我实际项目中短信内容就几条固定的告警信息所以提前算好了Unicode编码用查表的方式实现省事又省空间。4. OLED显示与状态机设计4.1 OLED驱动与显示内容OLED用的是0.96寸SSD1306I2C接口分辨率128x64。我用的u8g2库虽然占用大一点但显示汉字方便自带字库。u8g2的初始化u8g2_t u8g2; u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2, U8G2_R0, u8x8_byte_sw_i2c, u8x8_gpio_and_delay_stm32); u8g2_InitDisplay(u8g2); u8g2_SetPowerSave(u8g2, 0); u8g2_ClearBuffer(u8g2);显示内容分三行第一行模块状态如“模块就绪”、“注册中”、“已注册”第二行短信状态如“发送中”、“发送成功”、“发送失败”第三行信号强度如“信号: 23”刷新频率不用太高500ms一次就行太频繁会闪。提示u8g2的I2C可以用硬件I2C也可以用软件模拟。硬件I2C速度快但STM32的硬件I2C有坑容易卡死。我用的软件模拟稳定但速度慢一点刷一屏大概20ms够用了。4.2 状态机设计整个系统用一个状态机来管理状态定义如下typedef enum { STATE_IDLE, // 空闲 STATE_CHECK_MODULE, // 检查模块 STATE_CHECK_SIM, // 检查SIM卡 STATE_CHECK_NET, // 检查网络 STATE_SEND_SMS, // 发送短信 STATE_WAIT_RESULT, // 等待结果 STATE_SUCCESS, // 发送成功 STATE_FAIL // 发送失败 } SystemState;按键触发后状态从IDLE切换到CHECK_MODULE然后依次检查模块、SIM卡、网络都正常后进入SEND_SMS发送PDU字符串等待模块返回CMGS: xx表示发送成功返回ERROR表示失败。每个状态都有超时机制比如检查网络超过30秒还没注册上就跳到FAIL状态OLED显示“网络注册失败”。状态切换时OLED同步更新显示内容。比如进入CHECK_NET状态OLED显示“注册中...”进入SUCCESS状态OLED显示“发送成功”。4.3 按键消抖与中断处理按键接在PA0配置为下降沿触发的外部中断。但机械按键有抖动一次按下可能会触发多次中断。我在中断服务函数里加了简单的消抖void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { static uint32_t last_time 0; uint32_t now HAL_GetTick(); if (now - last_time 200) { // 200ms消抖 last_time now; key_pressed 1; // 设置标志位 } EXTI_ClearITPendingBit(EXTI_Line0); } }中断里只设置标志位具体处理放在主循环里避免在中断里做耗时操作。注意HAL_GetTick()在中断里调用是安全的它只是读取一个全局变量。但如果在中断里调用HAL_Delay()会卡死因为HAL_Delay()依赖SysTick中断而中断优先级配置不当会导致死锁。5. 常见问题与排查实录5.1 模块不响应AT指令这是最常见的问题原因可能有以下几种串口接线错误TX和RX接反了或者电平转换没接好波特率不对Air780E默认波特率是115200STM32也要设成115200模块没开机PWRKEY时序不对或者电源电流不够串口被占用STM32的串口初始化没成功排查步骤先用USB转TTL直接连Air780E用串口助手发AT看有没有OK返回。如果有说明模块没问题问题在STM32这边。如果没有检查模块供电和PWRKEY时序。我遇到过一次模块返回一堆乱码查了半天发现是波特率设成了9600改成115200就好了。5.2 短信发送失败短信发送失败的原因比较多常见的有问题现象可能原因解决方法返回ERRORPDU字符串格式错误检查长度和编码返回CMS ERROR: 500短信中心号码错误联系运营商获取正确的号码返回CMS ERROR: 331信号弱检查天线换个位置返回CMS ERROR: 332网络未注册等待注册或检查SIM卡无返回模块卡死断电重启PDU字符串格式错误是最常见的尤其是长度计算错误。我写了一个小工具在电脑上先验证PDU字符串确认无误后再写到STM32代码里。5.3 OLED显示异常OLED显示异常通常有这几种花屏I2C通信不稳定检查上拉电阻SCL和SDA都要接4.7k上拉不亮供电问题或者初始化失败显示内容错位u8g2的坐标设置错误闪烁刷新频率太高或者电源不稳我遇到过OLED和Air780E共用3.3V模块发射时OLED闪一下后来给OLED单独加了一个100uF的电容问题解决。5.4 电源问题导致模块重启Air780E发射时电流峰值2A如果电源跟不上模块会重启串口返回一堆乱码OLED也会闪。解决方法用独立电源给Air780E供电至少5V/2A在模块VCC和GND之间并一个470uF电解电容电源走线要粗别用细杜邦线如果和STM32共用电源STM32的稳压器要能扛住2A的瞬时电流我一开始用USB口供电模块一发射就重启换了独立电源后稳定运行。6. 实操心得与优化建议6.1 调试顺序很重要别一上来就写完整代码分步调试先调通OLED显示能显示静态文字再调通串口能收到Air780E的AT响应然后调通按键中断能触发状态切换最后调通PDU编码和短信发送每一步都确认无误后再进行下一步否则出了问题很难定位。6.2 用串口打印调试信息STM32的USART1接USB转TTL打印调试信息到电脑串口助手。比如printf(State: %d\n, current_state); printf(AT Response: %s\n, rx_buffer); printf(PDU: %s\n, pdu_string);这样能清楚地看到程序运行到哪一步AT指令返回了什么PDU字符串对不对。6.3 短信内容提前编码如果短信内容是固定的提前在电脑上算好Unicode编码直接写在代码里。比如“设备告警”的Unicode是8BBE59074E8B8B66直接存成字符串省去在STM32上做编码转换的麻烦。6.4 加看门狗STM32的独立看门狗IWDG一定要开防止程序跑飞。Air780E的AT指令交互有时候会卡住如果程序卡在等待响应的地方看门狗能复位STM32重新开始。IWDG_Init(IWDG_Prescaler_64, 4095); // 约1秒超时 IWDG_ReloadCounter(); // 主循环里喂狗6.5 信号强度查询ATCSQ可以查询信号强度返回CSQ: 23,023表示信号强度范围0-31越大越好。OLED上显示信号强度能直观看到当前网络质量。如果信号强度低于10短信发送可能会失败这时候可以提示用户换个位置。6.6 短信发送成功后的处理短信发送成功后模块返回CMGS: xxxx是消息参考号。STM32解析到这个响应后把状态切换到SUCCESSOLED显示“发送成功”然后延时2秒回到IDLE状态等待下一次按键。如果发送失败OLED显示“发送失败”同样延时后回到IDLE。6.7 低功耗优化如果设备是电池供电低功耗很重要。Air780E支持PSM和eDRX省电模式可以在不需要发送短信的时候让模块进入休眠。STM32也可以进入Stop模式按键中断唤醒。不过低功耗和实时性有冲突看具体需求。如果设备是常电供电就不用折腾低功耗了。6.8 天线选择Air780E的天线接口是IPEX可以用配套的棒状天线或者FPC天线。天线摆放位置对信号影响很大尽量远离金属物体别贴在PCB的覆铜区域。我试过用FPC天线贴在塑料外壳内侧信号强度比棒状天线低5个dB左右但胜在美观。如果对信号要求高还是用外置棒状天线。7. 代码结构与关键实现7.1 主循环框架int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); // 调试串口 MX_USART2_UART_Init(); // Air780E串口 MX_I2C1_Init(); // OLED OLED_Init(); Air780E_Init(); SystemState state STATE_IDLE; while (1) { IWDG_ReloadCounter(); switch (state) { case STATE_IDLE: if (key_pressed) { key_pressed 0; state STATE_CHECK_MODULE; } break; case STATE_CHECK_MODULE: if (Air780E_CheckModule()) { state STATE_CHECK_SIM; } else { state STATE_FAIL; } break; case STATE_CHECK_SIM: if (Air780E_CheckSIM()) { state STATE_CHECK_NET; } else { state STATE_FAIL; } break; case STATE_CHECK_NET: if (Air780E_CheckNet()) { state STATE_SEND_SMS; } else { state STATE_FAIL; } break; case STATE_SEND_SMS: Air780E_SendSMS(); state STATE_WAIT_RESULT; break; case STATE_WAIT_RESULT: if (Air780E_GetResult()) { state STATE_SUCCESS; } else { state STATE_FAIL; } break; case STATE_SUCCESS: OLED_ShowString(0, 2, 发送成功); HAL_Delay(2000); state STATE_IDLE; break; case STATE_FAIL: OLED_ShowString(0, 2, 发送失败); HAL_Delay(2000); state STATE_IDLE; break; } OLED_Refresh(); } }这个框架清晰明了每个状态只做一件事出了问题容易定位。7.2 串口接收中断Air780E的响应通过串口返回用中断接收存到缓冲区里。#define RX_BUFFER_SIZE 256 uint8_t rx_buffer[RX_BUFFER_SIZE]; uint16_t rx_index 0; void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART2); if (rx_index RX_BUFFER_SIZE - 1) { rx_buffer[rx_index] data; rx_buffer[rx_index] \0; } USART_ClearITPendingBit(USART2, USART_IT_RXNE); } }主循环里检查rx_buffer里有没有预期的响应比如“OK”、“CMGS:”等。7.3 PDU编码的完整实现void GeneratePDU(const char *phone, const char *unicode_hex, char *pdu_out) { char smsc[] 0891683108100005F0; char tpdu_header[] 01000B91; char phone_pdu[32]; char tpdu_footer[] 000808; // 手机号编码 int len strlen(phone); sprintf(phone_pdu, %02X, len); for (int i 0; i len; i 2) { if (i 1 len) { sprintf(phone_pdu strlen(phone_pdu), %c%c, phone[i1], phone[i]); } else { sprintf(phone_pdu strlen(phone_pdu), F%c, phone[i]); } } // 拼接PDU sprintf(pdu_out, %s%s%s%s%s, smsc, tpdu_header, phone_pdu, tpdu_footer, unicode_hex); }这个函数输入手机号和Unicode十六进制字符串输出完整的PDU字符串。Unicode十六进制字符串可以提前算好也可以动态生成。7.4 OLED显示函数void OLED_ShowStatus(SystemState state) { u8g2_ClearBuffer(u8g2); u8g2_SetFont(u8g2, u8g2_font_wqy12_t_gb2312); switch (state) { case STATE_IDLE: u8g2_DrawUTF8(u8g2, 0, 12, 状态: 空闲); break; case STATE_CHECK_MODULE: u8g2_DrawUTF8(u8g2, 0, 12, 状态: 检查模块); break; case STATE_CHECK_SIM: u8g2_DrawUTF8(u8g2, 0, 12, 状态: 检查SIM); break; case STATE_CHECK_NET: u8g2_DrawUTF8(u8g2, 0, 12, 状态: 注册网络); break; case STATE_SEND_SMS: u8g2_DrawUTF8(u8g2, 0, 12, 状态: 发送短信); break; case STATE_SUCCESS: u8g2_DrawUTF8(u8g2, 0, 12, 状态: 发送成功); break; case STATE_FAIL: u8g2_DrawUTF8(u8g2, 0, 12, 状态: 发送失败); break; } u8g2_SendBuffer(u8g2); }u8g2的中文字体用wqy12显示12像素高的汉字清晰度够用。8. 项目扩展与实用建议这套基础框架搭好之后可以扩展出很多实用功能。比如加一个DHT11温湿度传感器定时采集环境数据超过阈值就自动发短信告警。或者加一个继电器收到特定短信后控制继电器开关实现远程控制。短信内容也可以动态生成比如把温度值拼接到短信里“当前温度35度超过阈值”。这需要在STM32上做数字转字符串再转Unicode稍微麻烦一点但原理是一样的。如果觉得短信费用高可以改用Air780E的MQTT功能把数据推到服务器服务器再决定要不要发短信。Air780E支持MQTT over TCPAT指令也有一套但比短信复杂一些适合有网络服务的场景。天线方面如果设备装在金属外壳里一定要把天线引出来否则信号会被屏蔽。我试过把模块和天线都放在铁盒子里信号强度直接掉到0短信根本发不出去。最后说一个实际踩过的坑Air780E的固件版本不同AT指令的响应可能略有差异。我手头有两个批次的模块一个返回“OK”另一个返回“\r\nOK\r\n”解析的时候要把\r\n去掉。建议在代码里做字符串匹配时忽略\r\n只匹配关键字。这个项目整体难度不大但细节多尤其是PDU编码和电源设计容易翻车。把这两块搞定了剩下的就是按部就班的活。我前后花了大概一周的业余时间从选型到调试完成中间烧了一个模块换了两次电源方案最后稳定运行了三个月没出问题。
返回列表