ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

STM32+Air780E实战:按键触发中文短信与OLED状态显示

STM32+Air780E实战:按键触发中文短信与OLED状态显示 1. 从按一下发一条短信说起这个项目到底在解决什么问题很多人第一次接触蜂窝通信模块都是从发短信这个最朴素的需求开始的。原因很简单短信不依赖公网IP、不需要服务器中转、不挑接收端设备一条AT指令下去对方手机就能收到。对于做远程告警、设备状态上报、无人值守节点的人来说短信几乎是成本最低、链路最短的通信方式。但真动手做的时候问题就来了。市面上大量教程停留在用串口助手手动敲AT指令发英文短信的阶段一旦换成单片机自动发送、一旦内容变成中文、一旦还要在本地屏幕上看到发送状态能直接抄的完整方案就很少了。这个项目要解决的正是这三个一旦叠加之后的工程落地问题STM32作为主控Air780E作为4G Cat.1通信模组通过按键触发自动发送一条中文短信同时在OLED屏幕上实时显示当前状态。拆开来看它涉及四个相对独立又必须串起来的技术点。第一是STM32与Air780E的串口通信包括电平匹配、波特率协商、AT指令收发与超时处理第二是中文短信的PDU编码这是整个项目里最容易翻车的地方因为中文不能像英文那样直接塞进AT指令的引号里第三是OLED的状态显示用I2C或SPI驱动一块0.96寸屏把正在发送发送成功发送失败这些状态可视化第四是按键触发与状态机设计保证按键不抖动、发送不阻塞、异常能恢复。适合谁来参考这份内容如果你已经能点亮STM32的LED、能用串口打印调试信息那这篇基本可以照着做下来。如果你连HAL库的串口收发都还没跑通建议先把HAL_UART_Transmit和HAL_UART_Receive_IT这两个函数吃透再回来。整个项目的硬件成本大概在六十到一百元之间一块STM32F103C8T6最小系统板、一个Air780E模组或开发板、一块0.96寸OLED、一个按键、若干杜邦线仅此而已。我先把最终效果说清楚方便你判断要不要继续往下看上电后OLED显示Ready按下按键屏幕依次显示Sending...模组完成网络注册和短信发送后屏幕显示Send OK或Send Fail同时串口打印完整的AT交互日志。整个过程不需要电脑介入断电重启后依然可用。2. 硬件连接与供电那些看起来能跑其实跑不起来的细节2.1 Air780E的供电不是接3.3V就行Air780E是合宙的Cat.1模组工作电压典型值3.3V到4.2V但这里有个巨大的坑它在发射瞬间的峰值电流可以冲到2A左右。很多人用STM32最小系统板上的3.3V LDO给模组供电结果就是模组能注册上网络但一发短信就重启或者干脆连网络都注册不上串口返回一堆乱码。正确的做法是给Air780E单独供电。我实测下来最稳的方案是用一块能输出3A以上的DC-DC降压模块输入接5V比如USB供电输出调到3.8V左右给模组。STM32自己用最小系统板上的3.3V两者共地即可。如果你用的是合宙的Air780E开发板板载已经带了电源管理直接USB供电就行但要注意USB线不能太细劣质线材的压降同样会导致发射失败。提示判断供电是否够用的土办法——在模组电源引脚附近并一个1000uF的电解电容加一个0.1uF的陶瓷电容如果加了之后发送成功率明显提升说明原来的供电确实偏弱。2.2 串口交叉连接与电平确认STM32和Air780E之间走的是UART。Air780E的主串口默认波特率是1152008位数据位、1位停止位、无校验。连接方式是交叉STM32的TX接模组的RXSTM32的RX接模组的TX。这里要注意Air780E的IO电平是3.3V和STM32F103的3.3V IO可以直接对接不需要电平转换。如果你用的是5V的STM32板子比如某些老款那就必须加电平转换否则长期运行会损伤模组。我建议在STM32的TX和模组RX之间串一个100欧姆的电阻虽然理论上不需要但实测能有效抑制上电瞬间的电流冲击导致的通信异常。这个电阻不影响正常通信属于加了没坏处的保险措施。2.3 OLED的I2C接线与地址确认0.96寸OLED常见的有SSD1306和SH1106两种驱动芯片前者I2C地址通常是0x787位地址0x3C后者是0x7A。很多人买到的模块丝印不写清楚接上去不亮就以为是代码问题其实是地址不对。判断方法很简单用STM32的硬件I2C扫描一遍总线或者干脆用软件I2C逐个地址试。接线只有四根VCC、GND、SCL、SDA。VCC接3.3VSCL和SDA分别接STM32的I2C引脚同时各接一个4.7K的上拉电阻到3.3V。有些模块板载已经带了上拉那就不要再外加否则上拉过强反而会导致通信失败。这一点在0.9寸和0.96寸OLED上表现尤其明显0.9寸的部分模块对I2C上拉非常敏感这是热词里0.9寸oled对i2c兼容问题的真实来源。2.4 按键的硬件消抖按键一端接GPIO另一端接GNDGPIO配置为上拉输入。硬件上并一个0.1uF电容到地能省掉一部分软件消抖的麻烦。但软件消抖仍然要做我后面会讲具体做法。3. 中文短信为什么不能直接发PDU编码的来龙去脉3.1 从一条失败的AT指令说起如果你按照英文短信的写法直接发ATCMGS13800138000 你好设备告警大概率会失败或者收到乱码。原因是短信在底层有两种模式Text模式和PDU模式。Text模式只对ASCII字符友好中文必须走PDU模式。PDU模式把整条短信包括接收方号码、短信内容、编码方式、时间戳等打包成一串十六进制字符串再通过AT指令发送。3.2 PDU串的结构拆解一条完整的PDU串由这几部分组成短信中心号码长度与号码、PDU类型、接收方号码长度与号码、协议标识、编码方式、有效期、用户数据长度、用户数据。对于中文短信编码方式字段是08表示UCS2编码也就是每个中文字符占2个字节。我拿你好举例。这两个字的Unicode码点是4F60和597DUCS2编码后就是4F60597D共4个字节。用户数据长度字段要填的是字节数即04。所以用户数据部分就是044F60597D。接收方号码的处理也有讲究。假设号码是13800138000共11位。PDU里号码不是直接写数字而是要做奇偶交换把号码两两分组然后交换每组的位置。13800138000分组后是13 80 01 38 00 0最后补一个F凑成偶数位变成13 80 01 38 00 0F交换每组后是31 08 10 83 00 F0。号码长度字段填0B11的十六进制。把这些拼起来再补上短信中心号码等前缀才是完整的PDU串。手工算一次你就明白为什么很多人在这里翻车——任何一个字段算错短信就发不出去而且模组返回的错误码往往很模糊。3.3 用代码自动生成PDU串手工算不现实必须用C代码生成。核心逻辑分三步第一步把中文字符串转成UCS2字节数组第二步把接收方号码做奇偶交换第三步按PDU格式拼接并计算各字段长度。// 将UTF-8或GBK中文字符串转为UCS2字节数组假设源为GBK // 实际项目中建议统一用UTF-8再查表转UCS2 void str_to_ucs2(const char *src, uint8_t *dst, uint16_t *len) { uint16_t i 0, j 0; while (src[i]) { uint16_t code; if ((uint8_t)src[i] 0x80) { code src[i]; i 1; } else { // GBK双字节转Unicode需查表此处省略表 code gbk_to_unicode((uint8_t)src[i], (uint8_t)src[i1]); i 2; } dst[j] code 8; dst[j] code 0xFF; } *len j; }号码奇偶交换的代码更简单void swap_number(const char *num, char *out, uint8_t *len) { uint8_t n strlen(num); *len n; uint8_t i; for (i 0; i n; i 2) { if (i 1 n) { out[i] num[i1]; out[i1] num[i]; } else { out[i] F; out[i1] num[i]; } } out[i] \0; }拼接PDU串时用户数据长度字段要填的是UCS2字节数不是字符数。这一点我踩过坑一开始按字符数填结果短信内容被截断只发出前一半。3.4 发送PDU短信的AT指令序列完整流程是这样的ATCMGF0 // 设置为PDU模式 ATCMGS长度 // 长度是PDU串去掉短信中心号码后的字节数 PDU串CtrlZ // 0x1A结束模组返回CMGS: mr表示发送成功mr是消息参考号。如果返回ERROR或CMS ERROR: code就要根据错误码排查。常见的错误码500表示未知错误多半是PDU格式问题331表示网络未注册10表示网络超时。注意ATCMGS后面的长度参数是十六进制字符串的字符数除以2再减去短信中心号码部分的长度不是PDU串总长度。这个细节几乎所有教程都写得含糊我第一次做的时候在这里卡了整整一个下午。4. STM32侧的串口收发与状态机设计4.1 为什么不能用阻塞式发送最直观的写法是发一条AT指令然后HAL_Delay(1000)等模组返回。这种写法在发短信场景下会出大问题模组注册网络可能要十几秒发送短信可能要五到十秒如果全程阻塞按键就完全没响应OLED也没法刷新用户体验极差。正确的做法是用中断接收加环形缓冲区主循环里跑一个状态机。串口每收到一个字节就进中断把数据塞进环形缓冲区主循环定期从缓冲区取数据解析模组返回状态机根据当前状态决定下一步发什么指令。4.2 环形缓冲区的实现要点环形缓冲区要处理两个指针写指针在中断里移动读指针在主循环里移动。关键是要保证读写指针的原子性。在Cortex-M3上对32位变量的读写是原子的所以用uint16_t或uint32_t做索引就不需要关中断。但如果缓冲区大小超过65535就得用32位索引。#define UART_BUF_SIZE 512 static uint8_t uart_buf[UART_BUF_SIZE]; static volatile uint16_t uart_wr 0; static volatile uint16_t uart_rd 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { uart_buf[uart_wr] rx_byte; uart_wr (uart_wr 1) % UART_BUF_SIZE; HAL_UART_Receive_IT(huart, rx_byte, 1); } uint16_t uart_available(void) { return (uart_wr - uart_rd UART_BUF_SIZE) % UART_BUF_SIZE; }每次只收一个字节再重新开启中断虽然效率不高但在115200波特率下完全够用而且逻辑最简单不容易出错。4.3 状态机的状态划分我把整个发送流程拆成这几个状态IDLE空闲、CHECK_MODULE检查模组在线、CHECK_SIM检查SIM卡、CHECK_NET检查网络注册、SET_PDU_MODE设置PDU模式、SEND_SMS发送短信、WAIT_RESULT等待结果、DONE完成。每个状态发一条AT指令收到预期响应就跳到下一个状态超时就报错。状态机的好处是每一步都可控、可观测。OLED上显示的状态其实就是当前状态机的状态名调试时一眼就能看出卡在哪一步。4.4 超时与重试机制每个状态都要设超时一般AT指令响应超时设2秒网络注册超时设30秒。超时后不要立刻报错先重试两次两次都失败才进入错误状态。实测下来模组刚上电时第一次AT指令经常没响应重试一次就好了这是模组的正常启动行为不是故障。5. OLED状态显示让调试不再靠猜5.1 显示内容的规划OLED只有128x64像素能显示的内容有限。我规划了四行第一行显示模组状态MOD:OK或MOD:FAIL第二行显示网络状态NET:OK或NET:NO第三行显示发送状态SMS:IDLE/SMS:SEND/SMS:OK/SMS:ERR第四行显示一个计数器记录成功发送的条数。这样的布局好处是即使不看串口光看屏幕就能判断问题出在哪一层。模组没起来第一行就是FAILSIM卡没插好第二行就是NO发送失败第三行就是ERR。5.2 HAL库驱动OLED的关键点用HAL库的硬件I2C驱动SSD1306最容易卡在HAL_I2C_Master_Transmit的返回值上。如果返回HAL_ERROR先检查地址对不对再检查上拉电阻。我遇到过一种情况地址对、上拉也有但就是不通最后发现是I2C时钟频率设成了400KHz而那块老模块只支持100KHz。把频率降到100KHz就正常了。#define OLED_ADDR 0x78 void oled_write_cmd(uint8_t cmd) { uint8_t buf[2] {0x00, cmd}; HAL_I2C_Master_Transmit(hi2c1, OLED_ADDR, buf, 2, 100); } void oled_write_data(uint8_t *data, uint16_t len) { uint8_t buf[129]; buf[0] 0x40; memcpy(buf 1, data, len); HAL_I2C_Master_Transmit(hi2c1, OLED_ADDR, buf, len 1, 100); }显示汉字需要取模。用PC端取模软件生成字模数组每个16x16汉字占32字节。如果不想取模也可以只显示英文和数字状态信息用英文完全够用这样能省掉大量字库空间。5.3 刷新策略不要每帧全刷OLED全屏刷新一次要传1024字节在100KHz I2C下大概需要100ms。如果主循环里每轮都全刷会严重拖慢状态机响应。我的做法是只在状态变化时刷新对应行其他时候不动。SSD1306支持页寻址可以只更新某几页这样刷新一行的耗时降到20ms左右。6. 按键处理与整体联调中的真实坑6.1 按键消抖与长按识别按键用20ms定时器扫描连续两次读到按下才认为是有效按下。同时我加了一个发送中禁止再次触发的逻辑如果当前状态机不在IDLE按键直接忽略。这个逻辑看似简单但少了它就会出现连按导致多条短信重复发送的问题话费就是这么烧掉的。6.2 联调顺序建议不要一上来就把所有代码写完再调。我的建议是按这个顺序分步验证第一步只跑串口用串口助手确认STM32能收到Air780E的启动信息第二步加上OLED确认能显示固定文字第三步加上按键确认按键能改变屏幕内容第四步把AT指令流程跑通先用英文短信验证链路第五步换成PDU中文短信。每一步都验证通过再进下一步出问题时排查范围就小很多。6.3 几个高频故障的排查表现象可能原因排查方法模组无响应供电不足或TX/RX接反测模组供电电压交换TX/RX返回乱码波特率不匹配确认双方都是115200网络注册失败天线未接或SIM卡欠费检查天线用手机确认卡状态短信返回ERRORPDU串格式错误用在线PDU工具对比生成的串OLED不亮I2C地址错误或上拉缺失扫描I2C地址检查上拉发送成功但对方收不到号码格式错误确认号码做了奇偶交换6.4 一个容易被忽略的细节短信中心号码PDU串开头的短信中心号码很多人直接抄教程里的固定值但不同运营商的短信中心号码是不同的。正确做法是先发ATCSCA?查询模组里存储的短信中心号码再把它编进PDU串。如果查询返回空说明SIM卡没写好需要手动设置。这一步不做短信可能发出去但对方永远收不到。7. 关于这套方案后续能怎么扩展这套框架跑通之后扩展空间其实很大。把按键换成定时器触发就变成了定时上报把固定短信内容换成传感器读数就变成了环境监测告警把OLED换成更大尺寸的屏就能显示更多信息。Air780E本身还支持MQTT和HTTP如果短信只是兜底方案完全可以主用网络上报、失败时降级到短信。我个人在实际操作中的体会是这类单片机加通信模组的项目百分之七十的时间花在供电和通信链路的稳定性上真正写业务逻辑的时间不到百分之三十。所以别急着写代码先把电源、天线、串口这三样东西用示波器和串口助手确认到位后面会顺很多。另外PDU编码这块建议单独写一个测试函数用已知的输入输出对拍确认无误后再集成到主流程里否则一旦出错你根本分不清是编码问题还是通信问题。
返回列表