
1. 项目缘起与整体方案拆解按键发短信听起来像是功能机时代的老古董玩法但放在嵌入式开发里它其实是一个把人机交互、串口通信、无线通信协议、显示驱动串起来的综合性练手项目。我最初做这个STM32加Air780E的组合是因为手头有一批工业现场的设备需要做远程告警——设备本身没有联网能力但现场有4G信号覆盖最省事的办法就是让MCU通过串口控制4G模组把告警信息以短信形式发出去。后来发现这个架构太适合拿来做教学和原型验证了于是就有了今天这篇东西。这个项目要干的事情很明确STM32作为主控负责读取按键输入、驱动OLED显示当前状态、通过串口向Air780E发送AT指令Air780E作为4G通信模组负责驻网、建立短信通道、把中文内容以PDU格式编码后发送出去。OLED在这里的角色是状态监视器让你不用接串口调试助手就能知道模组当前是在找网、已注册、正在发送还是发送成功。整套东西的成本大概在六十到八十块钱之间STM32F103C8T6最小系统板十来块Air780E核心板三十多0.96寸OLED十来块再加上几个按键和杜邦线就能跑起来。适合看这篇内容的人我大致分三类。第一类是刚学完STM32的GPIO和串口想找个综合项目练手的同学这个项目涉及的外设不算多但链路完整能把UART中断收发、状态机设计、I2C驱动OLED这些知识点串起来。第二类是做物联网终端原型的工程师需要快速验证4G短信通道的可行性这套代码可以直接拿去做告警模块的雏形。第三类是对AT指令和PDU编码好奇的开发者中文短信的PDU编码规则确实有点绕但搞明白之后你会发现它设计得挺巧妙。方案选型上我做了几个关键取舍这里展开说一下背后的逻辑。主控选STM32F103C8T6而不是更便宜的合泰或者新唐原因很简单——HAL库生态成熟串口空闲中断加DMA的收发方案有大量现成参考OLED的I2C驱动也有现成库可以移植开发效率高。Air780E选的是合宙的模组支持LTE Cat.1短信和语音都支持AT指令集兼容性不错关键是文档齐全PDU编码的示例给得清楚。OLED用0.96寸SSD1306I2C接口四根线搞定比SPI版本省引脚刷新率虽然低一点但显示状态信息完全够用。整个系统的数据流是这样的按键按下触发外部中断STM32在中断里做软件消抖后置位一个标志主循环检测到标志后启动短信发送流程。发送流程分几步走——先检查模组是否已注册到网络然后配置短信模式为PDU模式接着把中文内容编码成PDU字符串通过串口发给模组最后等待模组返回发送结果。OLED在每个阶段刷新不同的状态文字让你一眼就能看出当前卡在哪一步。这个流程看起来简单但实际调试的时候坑不少后面我会逐个拆开讲。注意Air780E的供电一定要给足峰值电流能到2A用USB口直接供电经常会出现模组反复重启的情况建议单独用一路3.7V锂电池或者5V/2A的电源适配器供电。2. 硬件连接与核心细节解析2.1 引脚分配与电气连接要点硬件连接这块我先把引脚分配表列出来然后解释为什么这么分。STM32F103C8T6的USART1接Air780E的主串口PA9做TX、PA10做RX波特率固定115200。这里有个细节要注意Air780E的串口电平是1.8V还是3.3V取决于模组版本我手上这块是3.3V电平的可以直接和STM32的USART1对接。如果你买到的是1.8V版本中间必须加电平转换电路否则通信会不稳定甚至烧毁模组引脚。OLED用I2C1接口PB6做SCL、PB7做SDA上拉电阻用4.7K接到3.3V。SSD1306的I2C地址通常是0x78或0x7A具体取决于模块上电阻的焊接位置用I2C扫描程序扫一下就能确认。按键我用了三个分别接在PA0、PA1、PA2上配置成下拉输入按键另一端接3.3V按下时产生上升沿触发外部中断。PA0同时是WKUP引脚但这里我们不用它的唤醒功能只当普通中断用。功能模块STM32引脚模组引脚备注串口TXPA9RXD交叉连接串口RXPA10TXD交叉连接OLED SCLPB6SCL4.7K上拉OLED SDAPB7SDA4.7K上拉按键1PA0—发送短信按键2PA1—切换显示页面按键3PA2—复位模组模组供电—VBAT3.7V~4.2V模组地GNDGND共地供电部分我要单独强调一下。Air780E在发射瞬间的电流峰值可以达到2A平均电流在待机时大概3mA但发射时平均也有几百毫安。如果你用STM32板子上的3.3V LDO给模组供电大概率会失败因为LDO的电流输出能力不够而且模组发射时会把电源拉低导致STM32复位。正确的做法是给模组单独供3.7V到4.2V的锂电池或者大电流的5V转3.8V DCDCSTM32和OLED用另一路3.3V供电两边共地就行。2.2 Air780E的启动时序与串口初始化Air780E上电之后不是立刻就能收AT指令的它有一个启动过程。VBAT上电后模组的PWRKEY引脚需要拉低至少500ms再拉高模组才会真正开机。我一开始不知道这个上电后直接发AT指令结果一直没回应后来查了硬件手册才发现要控制PWRKEY。如果你买的Air780E核心板已经带了自动开机电路那上电就会自动启动不需要手动控制PWRKEY但启动完成仍然需要几秒钟时间。启动完成的标志是模组主动通过串口输出一串信息通常是“RDY”开头然后会有一些版本号和状态信息。我的做法是在STM32初始化完串口之后先延时3秒等模组启动然后发一条“AT”指令如果500ms内收到“OK”就认为模组就绪否则重试最多重试5次。这个握手过程很重要跳过的话后面发短信指令会全部失败。串口初始化用HAL库配置成115200、8位数据位、1位停止位、无校验、无硬件流控。接收我用的是空闲中断加DMA的方式这样不用一个字节一个字节地进中断效率高很多。具体做法是开启USART1的DMA接收设置一个256字节的缓冲区同时使能空闲中断。每当一帧数据接收完毕空闲中断触发在中断里计算接收到的数据长度置位一个标志主循环里处理。这个方案比轮询方式可靠得多尤其是在接收模组返回的长字符串时。// 串口空闲中断回调示例 void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart-Instance USART1) { rx_len Size; rx_flag 1; // 重新启动DMA接收 HAL_UARTEx_ReceiveToIdle_DMA(huart1, rx_buf, RX_BUF_SIZE); } }2.3 OLED显示驱动的移植与状态页面设计OLED驱动我直接移植了网上常见的SSD1306驱动但做了两处修改。第一处是把I2C的写函数改成HAL库的阻塞式发送因为状态显示对刷新速度要求不高阻塞式反而更稳定。第二处是增加了中文字库因为要显示“发送中”“发送成功”这些中文状态需要用到汉字取模。取模用PCtoLCD2002软件设置成阴码、逐列式、顺向生成的字模数组直接放到代码里。显示页面我设计了三个。页面一显示模组状态包括“驻网状态”“信号强度”“短信模式”三行信息。页面二显示最近一次发送的结果包括“发送时间”“目标号码”“发送结果”。页面三显示按键操作提示和系统运行时间。按键2用来切换页面按键1触发发送按键3复位模组。这样设计的好处是信息分层不会一屏塞太多内容导致看不清。实操心得SSD1306的I2C通信速率不要设太高我试过400KHz有时候会花屏降到100KHz就非常稳定。另外OLED的初始化延时不能省尤其是上电后的VCC稳定时间至少给100ms。3. 软件架构与核心流程实现3.1 主循环状态机设计整个软件的核心是一个状态机我把它分成六个状态IDLE、CHECK_NET、SET_PDU_MODE、ENCODE_PDU、SEND_SMS、WAIT_RESULT。IDLE状态等待按键触发CHECK_NET状态发“ATCREG?”查询网络注册状态SET_PDU_MODE发“ATCMGF0”设置PDU模式ENCODE_PDU在MCU内部完成中文到PDU的编码SEND_SMS发“ATCMGS”指令并传入PDU字符串WAIT_RESULT等待模组返回“CMGS”和“OK”。状态机的驱动靠一个定时器每100ms进一次状态处理函数根据当前状态执行对应操作。这样做的好处是逻辑清晰每个状态只干一件事调试的时候看OLED显示的状态编号就知道卡在哪一步。比如显示“S2”就说明在CHECK_NET状态如果一直停在这里那就是网络注册有问题查天线、查SIM卡、查信号覆盖就行。typedef enum { STATE_IDLE 0, STATE_CHECK_NET, STATE_SET_PDU_MODE, STATE_ENCODE_PDU, STATE_SEND_SMS, STATE_WAIT_RESULT } sms_state_t; void sms_state_machine(void) { switch (current_state) { case STATE_IDLE: if (key1_pressed) { key1_pressed 0; current_state STATE_CHECK_NET; oled_show_status(S1, Checking net...); } break; case STATE_CHECK_NET: if (send_at_and_wait(ATCREG?, 0,1, 2000) SUCCESS) { current_state STATE_SET_PDU_MODE; oled_show_status(S2, Net OK); } else { retry_count; if (retry_count 5) { current_state STATE_IDLE; oled_show_status(ERR, Net fail); } } break; // 后续状态省略 } }3.2 中文短信的PDU编码原理与实现PDU编码是中文短信发送里最绕的一块我尽量用大白话把它讲清楚。短信在GSM网络里传输的时候内容不是直接传文本而是编码成一串十六进制的数据。英文用7位编码中文用UCS2编码也就是把每个汉字当成一个16位的Unicode字符来处理。PDU串的结构分三部分短信中心号码、目标号码、短信内容。每部分都有固定的格式和长度字段。短信中心号码的处理比较特殊先要在号码前加“91”然后做奇偶位交换。比如短信中心号码是“8613800100500”去掉加号变成“8613800100500”前面加“91”变成“918613800100500”然后从“86”开始两两交换变成“681318000500F0”最后补“F”凑成偶数位。目标号码同理加“91”后做奇偶交换。短信内容的UCS2编码就是把每个汉字转成Unicode码点比如“你好”的Unicode是“4F60597D”直接拼上去就行。整个PDU串拼好之后前面还要加一个长度字段表示除去短信中心号码之外的部分有多长。这个长度是按字节算的不是按字符算。我写了一个编码函数输入目标号码和中文内容输出完整的PDU字符串。调试的时候可以用在线PDU编码工具对照确保自己的编码结果一致。void encode_pdu(char *target, char *content, char *pdu_out) { char sca[] 918613800100500; // 短信中心号码 char pdu_body[512] {0}; char temp[8]; // 目标号码处理 strcat(pdu_body, 1100); // 目标号码长度和类型 // ... 奇偶交换逻辑 // 短信内容UCS2编码 strcat(pdu_body, 0008); // UCS2编码标识 // ... 汉字转Unicode // 计算长度并拼接 sprintf(pdu_out, %02X%s, strlen(pdu_body)/2, pdu_body); }3.3 AT指令收发与超时重试机制AT指令的收发我封装了两个函数一个用于发送不需要等待特定响应的指令一个用于发送并等待特定响应。等待响应的函数带超时参数超时时间根据指令类型不同而不同。比如“AT”指令超时500ms“ATCREG?”超时2秒“ATCMGS”超时10秒因为要等网络侧响应。重试机制是这样的每条关键指令最多重试3次每次重试之间间隔1秒。如果3次都失败就认为这条指令彻底失败状态机跳到错误处理分支OLED显示错误码。错误码我定义了几个E1表示串口无响应E2表示网络未注册E3表示PDU模式设置失败E4表示短信发送失败。这样你看到OLED上的错误码就知道该查哪个方向。int send_at_and_wait(char *cmd, char *expect, uint32_t timeout_ms) { for (int retry 0; retry 3; retry) { uart_send_string(cmd); uart_send_string(\r\n); uint32_t start HAL_GetTick(); while (HAL_GetTick() - start timeout_ms) { if (rx_flag) { rx_flag 0; if (strstr((char*)rx_buf, expect) ! NULL) { return SUCCESS; } if (strstr((char*)rx_buf, ERROR) ! NULL) { break; } } } HAL_Delay(1000); } return FAIL; }注意发“ATCMGS”之后模组会返回一个“”提示符这时候才能把PDU串发过去最后还要发一个CtrlZ0x1A表示结束。这个时序不能乱提前发PDU或者忘了发CtrlZ都会导致发送失败。4. 实操过程与关键环节记录4.1 从零搭建的完整步骤第一步是硬件焊接和连接。先把STM32最小系统板插到面包板上然后把OLED的VCC接3.3V、GND接GND、SCL接PB6、SDA接PB7。按键一端接3.3V另一端分别接PA0、PA1、PA2同时在PA0到GND之间接一个10K下拉电阻。Air780E核心板单独用锂电池供电串口线交叉连接到STM32的PA9和PA10注意共地。第二步是STM32工程配置。用STM32CubeMX生成初始化代码开启USART1异步模式、I2C1、三个GPIO外部中断。USART1的DMA接收要手动在代码里配置CubeMX里只能配基本参数。时钟树配置成72MHz主频APB1给36MHzAPB2给72MHz。生成代码后把OLED驱动和PDU编码函数加进去。第三步是Air780E的初始配置。上电后等3秒发“AT”确认通信正常然后发“ATCPIN?”确认SIM卡识别正常发“ATCSQ”查看信号强度发“ATCREG?”确认网络注册状态。这几个指令都通过之后再发“ATCMGF0”设置PDU模式。这一套流程我写成了一个初始化函数上电自动执行OLED上会依次显示每一步的结果。第四步是联调发送。按按键1观察OLED状态变化。正常流程是S1到S5依次跳转最后显示“Send OK”。如果卡在某一步根据状态编号排查。我第一次调试的时候卡在S3也就是设置PDU模式失败后来发现是模组还没注册到网络就发了CMGF指令加了一个CREG检查之后就正常了。4.2 信号强度与网络注册的实测数据我在不同位置做了几组测试记录信号强度和注册状态的关系。CSQ返回值范围是0到310表示信号极差31表示信号极好。实测下来CSQ低于10的时候短信发送成功率明显下降有时候要重试三四次才能发出去。CSQ在15到25之间是比较理想的范围发送基本一次成功。测试位置CSQ值CREG状态发送成功率窗边220,110/10室内中间140,18/10地下室入口80,14/10地下室深处30,20/10CREG返回“0,1”表示已注册到本地网络“0,2”表示正在搜索网络“0,0”表示未注册。如果一直停在“0,2”说明信号太差或者天线没接好。Air780E的天线接口是IPEX一代我用的是配套的FPC天线贴在设备外壳内侧。如果你用弹簧天线效果会差一些建议尽量用FPC或者棒状天线。4.3 发送耗时与优化空间一条中文短信从按键触发到OLED显示发送成功我实测的平均耗时在8到12秒之间。这个时间主要花在几个地方网络注册检查大概1秒PDU模式设置0.5秒PDU编码在MCU内部完成只要几毫秒ATCMGS发送和等待网络侧响应大概5到8秒最后等待OK返回1到2秒。如果信号好整体可以压缩到6秒左右信号差的话可能超过15秒。优化空间有几个方向。一是把网络注册检查做成周期性的后台任务不用每次发送前都查一遍这样可以省掉1秒。二是PDU编码可以预先算好如果发送内容固定的话直接把PDU串存在数组里省掉编码时间。三是如果对实时性要求高可以考虑用TCP透传代替短信但那就需要服务器端配合了复杂度会上升不少。实操心得ATCMGS的等待时间不要设太短我一开始设了5秒结果信号稍差就超时失败。后来改成15秒成功率明显提升。但也不能无限等超过15秒基本就是网络侧出问题了重试比干等更有效。5. 常见问题与排查技巧实录5.1 模组无响应与串口通信故障模组无响应是最常见的问题表现是发“AT”之后收不到“OK”。排查顺序是这样的先用万用表量模组的VBAT电压确认在3.7V到4.2V之间然后量PWRKEY引脚确认开机时有一个拉低再拉高的脉冲接着用示波器或者逻辑分析仪看STM32的TX引脚有没有数据发出最后检查串口线是不是交叉连接的TX接RXD、RX接TXD。我遇到过一次模组无响应查了半天发现是串口线接成了直连TX对TX、RX对RX当然收不到数据。还有一次是波特率设错了Air780E默认是115200我设成了9600发出去的全是乱码。另外要注意有些Air780E核心板上的串口引脚标的是“TXD”和“RXD”但那是模组侧的命名连接到STM32时要交叉。5.2 PDU编码错误导致发送失败PDU编码错误的表现是模组返回“CMS ERROR: 304”或者直接返回“ERROR”。304错误码表示无效的PDU参数通常是长度字段算错了或者号码格式不对。排查方法是把生成的PDU串和在线工具的结果逐字节对比重点看长度字段、短信中心号码的奇偶交换、目标号码的奇偶交换这三处。我踩过的一个坑是短信中心号码写错了。不同运营商的短信中心号码不一样移动是“8613800XXX500”联通是“8613010XXX500”电信是“8613800XXX500”但后面几位不同。最稳妥的办法是把SIM卡插到手机上在短信设置里查一下短信中心号码然后照着填。填错的话PDU串发出去也会返回OK但短信实际发不到对方这个隐蔽性很强要注意。5.3 OLED显示异常与I2C冲突OLED显示异常主要有三种表现完全不亮、花屏、部分区域不刷新。完全不亮先查供电和I2C地址用I2C扫描程序确认地址是0x78还是0x7A。花屏通常是I2C速率太高或者上拉电阻太大把速率降到100KHz、上拉换成4.7K试试。部分区域不刷新可能是显存更新逻辑有问题检查一下页地址和列地址的设置范围。I2C冲突发生在同时使用多个I2C设备的时候。我这个项目里OLED独占I2C1没有其他设备所以没遇到冲突。但如果你要加其他I2C传感器注意地址不能冲突SSD1306的0x78和很多传感器的地址是错开的一般没问题。如果实在冲突可以用软件I2C把OLED挂到其他GPIO上。问题现象可能原因排查方法解决方案模组无响应供电不足量VBAT电压单独供电加大电容模组无响应串口接反检查TX/RX交叉连接返回304错误PDU长度错误对比在线工具重新计算长度字段返回304错误号码格式错误检查奇偶交换修正交换逻辑OLED花屏I2C速率高降低速率改成100KHzOLED不亮地址错误I2C扫描改驱动里的地址5.4 短信发送成功但对方收不到这个问题最隐蔽因为模组返回了“CMGS: XX”和“OK”看起来一切正常但对方就是收不到短信。原因通常是短信中心号码配置错误或者对方号码格式不对。排查方法是先给自己发一条确认自己的号码能收到然后再试其他号码。如果自己收不到那就是短信中心号码的问题如果自己能收到但别人收不到那可能是对方号码的运营商有问题。还有一个可能是短信被拦截了。有些手机的安全软件会把陌生号码发来的短信归到垃圾箱这个不是技术问题但调试的时候容易误判。建议调试阶段先用自己常用的号码做测试确认链路通了再换其他号码。注意Air780E在PDU模式下目标号码前面不需要加“86”直接写“13800138000”就行编码函数里会自动处理。如果你加了“86”编码出来的PDU串会多出几位导致发送失败。6. 项目扩展与个人体会这套东西跑通之后扩展方向其实挺多的。最直接的是把按键触发改成传感器触发比如接一个DHT11温湿度传感器温度超过阈值自动发短信告警。STM32的ADC采集外部电压超过范围也发短信。这样就从手动发送变成了自动告警实用性提升一个档次。另一个方向是增加短信接收功能。Air780E支持接收短信模组收到短信后会通过串口输出“CMTI”提示STM32解析这个提示后发“ATCMGR”读取短信内容再解码PDU串还原出中文。这样就可以做远程控制发一条“开灯”的短信STM32收到后控制继电器动作。接收的PDU解码比编码稍微复杂一点但原理是相通的。OLED显示也可以做得更丰富。现在只显示了状态文字其实还可以显示信号强度柱状图、发送计数、模组温度等信息。SSD1306的显存是128x64像素够画不少东西了。我后面打算加一个简单的图标显示发送成功显示对勾失败显示叉号这样一眼就能看出结果。最后分享一个我在调试过程中总结的小技巧把每次发送的PDU串和模组返回的完整响应都通过串口打印出来存到数组里出错的时候可以回看。我一开始没做这个出了问题只能靠猜后来加了日志功能排查效率高了很多。日志不用存太多保留最近五条就行用环形缓冲区实现不占多少内存。这个项目我从开始做到稳定运行大概花了三个周末的时间其中大部分时间花在PDU编码的调试和网络注册的稳定性优化上。硬件连接其实很简单软件逻辑也不复杂但细节特别多每一个细节没注意到都可能导致发送失败。如果你也在做类似的东西我的建议是先把AT指令手动调通用串口调试助手一条一条发确认每条指令的响应都符合预期然后再写代码自动化。手动调通之后再写代码你会发现顺利很多。