STM32+Air780E+OLED:按键触发中文短信发送终端实战
1. 项目缘起与整体方案拆解按键一按短信发出OLED屏幕上实时滚动着“发送中”“发送成功”的状态——这个场景听起来像是某个工业设备的报警通知模块或者是一个远程数据采集终端的核心交互逻辑。我最近刚把一个类似的项目从零跑通用的是STM32F103C8T6作为主控搭配Air780E这款4G Cat.1模组外加一块0.96寸的SSD1306 OLED屏。整个项目最核心的难点不在于硬件连接而在于如何通过AT指令把中文短信发出去以及如何让OLED在有限的刷新率下稳定显示状态信息。先说说这个项目到底能干什么。简单讲它就是一个“按键触发式的中文短信发送终端”。你按下一个物理按键STM32通过串口向Air780E发送AT指令Air780E负责驻网、建立连接、编码短信内容最终把一条包含中文的短信发送到指定手机号。与此同时OLED屏幕上会显示当前的操作状态是正在发送、发送成功还是发送失败。这个方案适合谁呢如果你正在做毕业设计、工业报警器、远程数据上报终端或者单纯想入门4G通信模组和OLED显示这个项目都是一个非常好的练手选择。为什么选Air780E而不是其他模组市面上常见的4G模组有移远的EC20、合宙的Air724UG、Air780E等。Air780E是合宙推出的一款Cat.1模组性价比极高支持全网通AT指令集兼容性好而且官方文档和社区资源非常丰富。更重要的是它支持PDU模式发送中文短信这是本项目的关键技术点。相比之下EC20价格偏高Air724UG虽然也支持中文短信但Air780E的功耗和封装更适合小型化设备。STM32这边我选的是F103C8T6也就是大家常说的“蓝板”资源足够、资料多、价格便宜HAL库开发也很成熟。OLED屏用的是0.96寸SSD1306I2C接口四针脚。这个屏几乎是STM32项目的标配了驱动简单、显示效果清晰、功耗低。我用的是HAL库驱动配合u8g2图形库来显示中文和英文混合内容。这里有个细节u8g2库虽然强大但移植到STM32上需要配置好I2C的底层读写函数否则会出现花屏或者不亮的情况。后面我会详细讲这个坑怎么填。整个系统的数据流是这样的按键按下 - STM32检测到电平变化 - 通过UART向Air780E发送AT指令 - Air780E执行驻网、短信编码、发送 - STM32解析Air780E返回的响应 - 根据响应更新OLED显示状态。这个流程看似简单但每一步都有不少细节需要注意尤其是AT指令的时序和PDU编码的计算。2. 硬件选型与连接细节2.1 核心器件清单与选型理由先列一下我实际用到的物料清单以及为什么选这些型号。器件型号数量选型理由主控STM32F103C8T61资源充足HAL库成熟价格低4G模组Air780E1Cat.1全网通AT指令丰富支持PDU短信显示屏0.96寸SSD1306 OLED1I2C接口驱动简单显示清晰按键轻触按键1普通6x6x5mm即可电平转换TXS0108E或分压电阻1Air780E是1.8V电平STM32是3.3V电源5V/2A适配器1Air780E峰值电流可达2A这里重点说一下电平转换的问题。Air780E的UART接口电平是1.8V而STM32F103的UART是3.3V。如果直接连接长期工作可能会损坏Air780E的IO口。我一开始图省事直接连了结果模组偶尔会死机后来加了TXS0108E电平转换芯片才稳定下来。如果你手头没有电平转换芯片也可以用两个电阻分压STM32的TX接1k电阻到Air780E的RX再在Air780E的RX和GND之间接一个2k电阻这样3.3V分压后大约是1.8V左右。但分压方案在高速波特率下波形会变差建议还是用专用芯片。2.2 接线方案与注意事项接线这块我踩过坑所以说得细一点。STM32和Air780E之间用UART通信我选的是USART2波特率115200。为什么选115200因为Air780E默认波特率是115200而且这个速率在STM32F103上很稳定不容易出错。如果你用9600虽然更稳但发送长短信时等待时间会明显变长。具体接线如下STM32 PA2 (USART2_TX) - 电平转换 - Air780E RXSTM32 PA3 (USART2_RX) - 电平转换 - Air780E TXSTM32 PB6 (I2C1_SCL) - OLED SCLSTM32 PB7 (I2C1_SDA) - OLED SDASTM32 PA0 - 按键一端按键另一端接GNDOLED VCC - 3.3VGND - GNDAir780E VCC - 5VGND - GND注意Air780E的供电一定要足够。我一开始用STM32板子上的3.3V给Air780E供电结果模组一驻网就重启。后来换成独立的5V/2A电源问题解决。Air780E在发射信号瞬间电流会冲到2A左右所以电源的瞬态响应要好最好在VCC和GND之间并一个1000uF的电解电容。按键这块我用的是PA0配置为上拉输入按键按下时拉低。为什么不用外部中断因为短信发送本身是一个耗时操作用轮询方式反而更简单可控。我在主循环里每50ms检测一次按键状态加上软件消抖实测很稳。OLED的I2C地址是0x787位地址或0x3C8位地址左移一位。如果你用HAL库的I2C函数注意地址要左移一位。我一开始没注意屏幕一直不亮查了半天才发现是地址问题。3. AT指令与PDU编码核心解析3.1 Air780E短信发送的AT指令流程Air780E发送短信的AT指令流程和大多数4G模组类似但有几个细节不一样。我先把完整的指令序列列出来然后逐条解释。AT # 测试模组是否响应 ATCPIN? # 查询SIM卡状态 ATCSQ # 查询信号质量 ATCREG? # 查询网络注册状态 ATCMGF0 # 设置为PDU模式 ATCMGS长度 # 发送短信长度是PDU数据的长度 PDU数据 # 输入PDU数据以CtrlZ结束这里最关键的是ATCMGF0它把短信模式设置为PDU模式。为什么不用Text模式因为Text模式不支持中文只有PDU模式才能发送中文短信。PDU模式的全称是Protocol Data Unit它把短信内容按照一定的格式编码成十六进制字符串然后通过AT指令发送出去。ATCMGS长度中的长度是指PDU数据的长度不包括短信中心号码的长度。这个长度计算很容易出错后面我会详细讲。3.2 PDU编码原理与中文短信编码计算PDU编码是這個项目最核心的技术点也是最容易出错的地方。我先用一个实际例子来说明。假设我要发送的短信内容是“你好”目标手机号是“13800138000”短信中心号码是“8613800210500”。PDU编码的步骤如下第一步编码短信中心号码。短信中心号码的PDU格式是长度 类型 号码。对于“8613800210500”去掉“”号得到“8613800210500”共13位。长度字节是(131)/2 1 8即0x08。类型是0x91表示国际号码。号码需要两两交换变成“68310820050100”最后补“F”凑成偶数位得到“68310820050100F0”。所以短信中心号码的PDU是089168310820050100F0。第二步编码目标手机号。目标号码是“13800138000”共11位。长度字节是(111)/2 6即0x06。类型是0x81表示国内号码。号码两两交换变成“3108108300F0”。所以目标号码的PDU是06813108108300F0。第三步编码短信内容。中文短信需要用UCS2编码也就是把每个中文字符转换成Unicode码点再转成十六进制。“你”的Unicode是4F60“好”的Unicode是597D。所以短信内容的十六进制是“4F60597D”共4个字节。第四步组装PDU数据。完整的PDU数据包括短信中心号码 第一字节TPDU类型 目标号码 协议标识 编码方式 有效期 短信内容长度 短信内容。短信中心号码089168310820050100F0TPDU类型11表示发送短信包含有效期目标号码06813108108300F0协议标识00编码方式08UCS2编码有效期00默认短信内容长度044个字节短信内容4F60597D所以完整的PDU数据是089168310820050100F01106813108108300F0000800044F60597D这个PDU数据的长度是短信中心号码部分16个字符 TPDU部分2个字符 目标号码部分14个字符 协议标识2个字符 编码方式2个字符 有效期2个字符 短信内容长度2个字符 短信内容8个字符 48个字符即24个字节。所以ATCMGS24。注意这个长度计算很容易出错。我建议在代码里写一个函数自动计算PDU长度。不要手动算否则很容易发不出去。3.3 STM32端AT指令发送与解析代码实现在STM32端我用HAL库的UART发送和接收函数来实现AT指令的交互。核心代码如下// 发送AT指令 void Air780E_SendCmd(char *cmd) { HAL_UART_Transmit(huart2, (uint8_t *)cmd, strlen(cmd), 1000); HAL_UART_Transmit(huart2, (uint8_t *)\r\n, 2, 1000); } // 等待特定响应 uint8_t Air780E_WaitResponse(char *expected, uint32_t timeout) { uint8_t buffer[256]; uint32_t start HAL_GetTick(); uint16_t idx 0; while (HAL_GetTick() - start timeout) { if (HAL_UART_Receive(huart2, buffer[idx], 1, 10) HAL_OK) { idx; if (idx 256) idx 0; if (strstr((char *)buffer, expected) ! NULL) { return 1; } } } return 0; }这段代码的逻辑很简单发送指令后不断接收串口数据直到收到预期的响应或者超时。这里有个细节HAL_UART_Receive的超时我设的是10ms这样不会阻塞太久。buffer的大小是256字节足够容纳大多数AT响应。发送短信的完整函数如下void SendChineseSMS(char *phone, char *content) { char pdu[512]; char cmd[64]; uint8_t pdu_len; // 生成PDU数据 pdu_len GeneratePDU(phone, content, pdu); // 设置PDU模式 Air780E_SendCmd(ATCMGF0); Air780E_WaitResponse(OK, 2000); // 发送短信 sprintf(cmd, ATCMGS%d, pdu_len); Air780E_SendCmd(cmd); Air780E_WaitResponse(, 5000); // 发送PDU数据 HAL_UART_Transmit(huart2, (uint8_t *)pdu, strlen(pdu), 5000); HAL_UART_Transmit(huart2, (uint8_t *)\x1A, 1, 1000); // CtrlZ // 等待发送结果 if (Air780E_WaitResponse(CMGS:, 30000)) { OLED_ShowString(0, 4, Send OK); } else { OLED_ShowString(0, 4, Send Fail); } }这里GeneratePDU函数负责把手机号和短信内容转换成PDU格式的十六进制字符串。这个函数的实现比较长核心是UCS2编码和长度计算。我建议你把这个函数单独放在一个文件里方便调试。4. OLED显示驱动与状态更新4.1 SSD1306驱动移植与u8g2配置OLED这块我用的是u8g2库因为它对中文显示的支持非常好而且自带了很多字体。移植u8g2到STM32上需要做两件事一是提供I2C的底层读写函数二是配置u8g2的初始化参数。I2C底层函数我用的是HAL库的HAL_I2C_Mem_Write和HAL_I2C_Mem_Read。具体实现如下uint8_t u8g2_byte_cb(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) { switch (msg) { case U8X8_MSG_BYTE_SEND: HAL_I2C_Master_Transmit(hi2c1, 0x78, (uint8_t *)arg_ptr, arg_int, 100); break; case U8X8_MSG_BYTE_INIT: break; case U8X8_MSG_BYTE_SET_DC: break; case U8X8_MSG_BYTE_START_TRANSFER: break; case U8X8_MSG_BYTE_END_TRANSFER: break; default: return 0; } return 1; }这里0x78是OLED的I2C地址。注意HAL库的I2C地址需要左移一位所以实际写的是0x78而不是0x3C。u8g2的初始化代码如下u8g2_t u8g2; u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2, U8G2_R0, u8g2_byte_cb, u8g2_gpio_and_delay_cb); u8g2_InitDisplay(u8g2); u8g2_SetPowerSave(u8g2, 0); u8g2_ClearBuffer(u8g2); u8g2_SetFont(u8g2, u8g2_font_wqy12_t_chinese1); u8g2_DrawUTF8(u8g2, 0, 12, 系统就绪); u8g2_SendBuffer(u8g2);这里用的是u8g2_font_wqy12_t_chinese1字体它支持常用中文字符。如果你要显示更多汉字可以换成u8g2_font_wqy16_t_chinese2但字体会大一些显示的内容会少一些。4.2 状态显示逻辑与刷新策略OLED的刷新策略很重要因为I2C的速率有限频繁刷新会导致屏幕闪烁。我的做法是只在状态发生变化时才刷新屏幕而不是每次循环都刷新。状态机设计如下状态显示内容触发条件IDLE“等待按键”系统初始化完成SENDING“发送中...”按键按下开始发送SUCCESS“发送成功”收到CMGS响应FAIL“发送失败”超时或收到ERROR状态切换的代码如下typedef enum { STATE_IDLE, STATE_SENDING, STATE_SUCCESS, STATE_FAIL } SystemState; SystemState currentState STATE_IDLE; void UpdateOLED(SystemState state) { u8g2_ClearBuffer(u8g2); switch (state) { case STATE_IDLE: u8g2_DrawUTF8(u8g2, 0, 12, 等待按键); break; case STATE_SENDING: 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_SendBuffer是一个阻塞操作刷新128x64的屏幕大约需要20-30ms。如果你在发送短信的过程中频繁调用这个函数可能会影响AT指令的接收。所以我的建议是在发送短信前更新一次状态发送完成后再更新一次中间不要频繁刷新。5. 常见问题与排查技巧实录5.1 AT指令无响应或返回ERROR这是最常见的问题我遇到过好几次。排查思路如下第一检查串口接线是否正确。STM32的TX要接Air780E的RXRX要接TX不要接反了。我一开始就是TX接TX结果发出去的指令模组根本收不到。第二检查波特率是否匹配。Air780E默认是115200如果你改过模组的波特率STM32这边也要同步改。可以用ATIPR?查询当前波特率。第三检查电源是否足够。Air780E在驻网时电流会突然增大如果电源供电不足模组会重启导致AT指令无响应。我建议用独立的5V/2A电源并在VCC和GND之间并一个1000uF的电解电容。第四检查SIM卡是否插好。如果SIM卡没插好ATCPIN?会返回ERROR。这时候要重新插拔SIM卡确保接触良好。5.2 中文短信发送失败或乱码中文短信发送失败的原因通常有两个一是PDU编码错误二是短信中心号码不对。PDU编码错误最常见的是长度计算错误。ATCMGS长度中的长度是PDU数据的字节数不包括短信中心号码的长度。我一开始把短信中心号码的长度也算进去了结果一直返回ERROR。后来写了一个自动计算长度的函数问题解决。短信中心号码不对也会导致发送失败。你可以用ATCSCA?查询当前设置的短信中心号码。如果不对可以用ATCSCA8613800210500重新设置。注意短信中心号码要加上国家码比如中国是86。5.3 OLED不亮或花屏OLED不亮的原因通常有三个一是I2C地址不对二是接线松动三是初始化序列不对。I2C地址方面SSD1306的7位地址是0x3C但HAL库需要左移一位所以实际写的是0x78。如果你用的是软件I2C地址就是0x3C。我一开始用硬件I2C地址写的是0x3C结果屏幕一直不亮后来改成0x78就好了。接线松动也是常见问题。I2C的SCL和SDA线如果接触不良屏幕会花屏或者不亮。我建议用杜邦线的时候要插紧或者直接焊上去。初始化序列不对也会导致花屏。u8g2库的初始化序列是经过验证的但如果你自己写初始化代码要注意SSD1306的初始化命令顺序。我建议直接用u8g2库省事又稳定。5.4 按键抖动导致误触发按键抖动是机械按键的固有特性如果不处理按一次可能会触发多次发送。我的做法是在检测到按键按下后延时20ms再检测一次如果还是按下状态才认为是有效按键。代码如下if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET) { HAL_Delay(20); if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET) { // 有效按键 while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET); // 等待按键释放 return 1; } } return 0;这段代码的逻辑是检测到低电平后延时20ms再检测如果还是低电平就确认按键按下。然后等待按键释放避免重复触发。6. 实操心得与进阶建议这个项目我从开始到跑通大概花了三天时间其中大部分时间都花在PDU编码和OLED驱动上。有几个心得值得分享。第一PDU编码一定要写单元测试。我在PC上写了一个Python脚本用来验证PDU编码的正确性。每次修改编码函数后先用Python脚本跑一遍确认无误后再移植到STM32上。这样可以节省大量调试时间。第二AT指令的响应解析要留足超时时间。Air780E在驻网和发送短信时响应时间可能会超过10秒。我一开始把超时设成5秒结果经常误判为失败。后来改成30秒问题解决。第三OLED的刷新频率不要太高。I2C的速率有限频繁刷新会导致屏幕闪烁而且会占用CPU时间。我的做法是只在状态变化时刷新平时保持静态显示。第四电源设计要留余量。Air780E的峰值电流可达2A如果你的电源只能提供1A模组在发射信号时会重启。我建议用5V/2A的电源并在VCC和GND之间并一个1000uF的电解电容。第五如果你要发送长短信需要注意PDU的长度限制。一条短信最多140字节超过这个长度需要拆分多条发送。Air780E支持长短信自动拆分但需要在PDU中设置好UDH用户数据头。这部分比较复杂建议先从短短信开始调试。这个项目后续还可以扩展比如增加多个按键每个按键对应不同的短信内容或者增加一个温度传感器当温度超过阈值时自动发送报警短信还可以把发送记录保存到Flash中方便后续查询。这些扩展都不难核心的AT指令和PDU编码逻辑是通用的。