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寸SSD1306,I2C接口,四针脚。这个屏几乎是STM32项目的标配了,驱动简单、显示效果清晰、功耗低。我用的是HAL库驱动,配合u8g2图形库来显示中文和英文混合内容。这里有个细节:u8g2库虽然强大,但移植到STM32上需要配置好I2C的底层读写函数,否则会出现花屏或者不亮的情况。后面我会详细讲这个坑怎么填。
整个系统的数据流是这样的:按键按下 -> STM32检测到电平变化 -> 通过UART向Air780E发送AT指令 -> Air780E执行驻网、短信编码、发送 -> STM32解析Air780E返回的响应 -> 根据响应更新OLED显示状态。这个流程看似简单,但每一步都有不少细节需要注意,尤其是AT指令的时序和PDU编码的计算。
2. 硬件选型与连接细节
2.1 核心器件清单与选型理由
先列一下我实际用到的物料清单,以及为什么选这些型号。
| 器件 | 型号 | 数量 | 选型理由 |
|---|---|---|---|
| 主控 | STM32F103C8T6 | 1 | 资源充足,HAL库成熟,价格低 |
| 4G模组 | Air780E | 1 | Cat.1全网通,AT指令丰富,支持PDU短信 |
| 显示屏 | 0.96寸SSD1306 OLED | 1 | I2C接口,驱动简单,显示清晰 |
| 按键 | 轻触按键 | 1 | 普通6x6x5mm即可 |
| 电平转换 | TXS0108E或分压电阻 | 1 | Air780E是1.8V电平,STM32是3.3V |
| 电源 | 5V/2A适配器 | 1 | Air780E峰值电流可达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 RX
- STM32 PA3 (USART2_RX) -> 电平转换 -> Air780E TX
- STM32 PB6 (I2C1_SCL) -> OLED SCL
- STM32 PB7 (I2C1_SDA) -> OLED SDA
- STM32 PA0 -> 按键一端,按键另一端接GND
- OLED VCC -> 3.3V,GND -> GND
- Air780E VCC -> 5V,GND -> GND
注意:Air780E的供电一定要足够。我一开始用STM32板子上的3.3V给Air780E供电,结果模组一驻网就重启。后来换成独立的5V/2A电源,问题解决。Air780E在发射信号瞬间电流会冲到2A左右,所以电源的瞬态响应要好,最好在VCC和GND之间并一个1000uF的电解电容。
按键这块,我用的是PA0,配置为上拉输入,按键按下时拉低。为什么不用外部中断?因为短信发送本身是一个耗时操作,用轮询方式反而更简单可控。我在主循环里每50ms检测一次按键状态,加上软件消抖,实测很稳。
OLED的I2C地址是0x78(7位地址)或0x3C(8位地址左移一位)。如果你用HAL库的I2C函数,注意地址要左移一位。我一开始没注意,屏幕一直不亮,查了半天才发现是地址问题。
3. AT指令与PDU编码核心解析
3.1 Air780E短信发送的AT指令流程
Air780E发送短信的AT指令流程和大多数4G模组类似,但有几个细节不一样。我先把完整的指令序列列出来,然后逐条解释。
AT # 测试模组是否响应 AT+CPIN? # 查询SIM卡状态 AT+CSQ # 查询信号质量 AT+CREG? # 查询网络注册状态 AT+CMGF=0 # 设置为PDU模式 AT+CMGS=<长度> # 发送短信,<长度>是PDU数据的长度 > <PDU数据> # 输入PDU数据,以Ctrl+Z结束这里最关键的是AT+CMGF=0,它把短信模式设置为PDU模式。为什么不用Text模式?因为Text模式不支持中文,只有PDU模式才能发送中文短信。PDU模式的全称是Protocol Data Unit,它把短信内容按照一定的格式编码成十六进制字符串,然后通过AT指令发送出去。
AT+CMGS=<长度>中的长度是指PDU数据的长度,不包括短信中心号码的长度。这个长度计算很容易出错,后面我会详细讲。
3.2 PDU编码原理与中文短信编码计算
PDU编码是這個项目最核心的技术点,也是最容易出错的地方。我先用一个实际例子来说明。
假设我要发送的短信内容是“你好”,目标手机号是“13800138000”,短信中心号码是“+8613800210500”。PDU编码的步骤如下:
第一步,编码短信中心号码。短信中心号码的PDU格式是:长度 + 类型 + 号码。对于“+8613800210500”,去掉“+”号,得到“8613800210500”,共13位。长度字节是(13+1)/2 + 1 = 8,即0x08。类型是0x91(表示国际号码)。号码需要两两交换,变成“68310820050100”,最后补“F”凑成偶数位,得到“68310820050100F0”。所以短信中心号码的PDU是:089168310820050100F0。
第二步,编码目标手机号。目标号码是“13800138000”,共11位。长度字节是(11+1)/2 = 6,即0x06。类型是0x81(表示国内号码)。号码两两交换,变成“3108108300F0”。所以目标号码的PDU是:06813108108300F0。
第三步,编码短信内容。中文短信需要用UCS2编码,也就是把每个中文字符转换成Unicode码点,再转成十六进制。“你”的Unicode是4F60,“好”的Unicode是597D。所以短信内容的十六进制是“4F60597D”,共4个字节。
第四步,组装PDU数据。完整的PDU数据包括:短信中心号码 + 第一字节(TPDU类型)+ 目标号码 + 协议标识 + 编码方式 + 有效期 + 短信内容长度 + 短信内容。
- 短信中心号码:
089168310820050100F0 - TPDU类型:
11(表示发送短信,包含有效期) - 目标号码:
06813108108300F0 - 协议标识:
00 - 编码方式:
08(UCS2编码) - 有效期:
00(默认) - 短信内容长度:
04(4个字节) - 短信内容:
4F60597D
所以完整的PDU数据是:089168310820050100F01106813108108300F0000800044F60597D
这个PDU数据的长度是:短信中心号码部分(16个字符)+ TPDU部分(2个字符)+ 目标号码部分(14个字符)+ 协议标识(2个字符)+ 编码方式(2个字符)+ 有效期(2个字符)+ 短信内容长度(2个字符)+ 短信内容(8个字符)= 48个字符,即24个字节。所以AT+CMGS=24。
注意:这个长度计算很容易出错。我建议在代码里写一个函数,自动计算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("AT+CMGF=0"); Air780E_WaitResponse("OK", 2000); // 发送短信 sprintf(cmd, "AT+CMGS=%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); // Ctrl+Z // 等待发送结果 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的RX,RX要接TX,不要接反了。我一开始就是TX接TX,结果发出去的指令模组根本收不到。
第二,检查波特率是否匹配。Air780E默认是115200,如果你改过模组的波特率,STM32这边也要同步改。可以用AT+IPR?查询当前波特率。
第三,检查电源是否足够。Air780E在驻网时电流会突然增大,如果电源供电不足,模组会重启,导致AT指令无响应。我建议用独立的5V/2A电源,并在VCC和GND之间并一个1000uF的电解电容。
第四,检查SIM卡是否插好。如果SIM卡没插好,AT+CPIN?会返回ERROR。这时候要重新插拔SIM卡,确保接触良好。
5.2 中文短信发送失败或乱码
中文短信发送失败的原因通常有两个:一是PDU编码错误,二是短信中心号码不对。
PDU编码错误最常见的是长度计算错误。AT+CMGS=<长度>中的长度是PDU数据的字节数,不包括短信中心号码的长度。我一开始把短信中心号码的长度也算进去了,结果一直返回ERROR。后来写了一个自动计算长度的函数,问题解决。
短信中心号码不对也会导致发送失败。你可以用AT+CSCA?查询当前设置的短信中心号码。如果不对,可以用AT+CSCA="+8613800210500"重新设置。注意,短信中心号码要加上国家码,比如中国是+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编码逻辑是通用的。