简介:本资源是一套基于STM32F10x系列单片机与SIM900A GSM模块实现短信指令识别与自动回复的完整嵌入式项目工程,面向嵌入式初学者及物联网实践开发者,解决远程控制场景下低功耗、文本交互式设备管理问题。压缩包共90个文件,含8个核心C源码(如main.c、GSM.c、USART_IO.c)、6个头文件(如GSM.h、stm32f10x_conf.h)、多组编译中间文件(.o/.d/.dep)及调试配置(.uv2、.opt、.sct),整体大小2.08MB,结构清晰体现Keil MDK标准工程组织方式。已有211人学习下载,资源附带readme.txt说明与GPS信息回复等典型用例,代码涵盖AT指令收发、短信内容解析(关键词匹配)、串口通信容错处理及硬件响应逻辑(如GPIO控制),可直接编译烧录运行,是掌握STM32+GSM模块协同开发、文本协议解析与嵌入式通信实战的优质参考工程。 先把场景摆出来:你做一个设备,摆在没有人值守的地方,比如乡下的农业大棚、临时仓库、或者一台户外广告屏控制箱。WiFi覆盖不稳,4G还要插流量卡、交月租,但你只需要偶尔发一条短信,查一下状态、开个继电器、让设备重启一次。这种情况下,GSM短信是最简单直接的路子。我自己用STM32配SIM900A做过一个短信远程控制的小工程,核心功能就是读取收到的短信,解析里面写好的指令,执行动作,再自动回一条短信告诉用户“执行成功”或者“当前状态是什么”。标题里说的“可识别短信指令并回复用户”,做出来就是这个效果。
这类项目网上散落着很多代码片段,但大多是“能跑但说不清为什么”。这篇文章把我实际调通的完整思路写出来,包括为什么选SIM900A、短信中文编码怎么处理、AT指令应该怎么排、STM32端串口数据该怎么接,以及几个我踩过之后印象深刻的坑。适合刚接触单片机连GSM模块的开发者,也适合想用短信做远程控制但不知道怎么下手的同学。
1. 为什么用短信做远程控制:这个项目的真实需求与选型逻辑
1.1 短信比WiFi、蓝牙、4G更合适的地方
在做技术选型的时候,很多人第一反应是WiFi模块,觉得用MQTT、HTTP走云平台很“现代”。但实际项目里,WiFi有一个绕不开的问题:设备所在的网络环境不一定友好。有些厂房角落、地下空间、农村鸡舍,要么没有WiFi,要么网络信号弱到连路由器都管理不了。就算有WiFi,还有一个不是程序员能控制的问题:路由器重启、密码变更、DHCP租期变化,设备就可能掉线,而远程控制场景恰恰容不得“设备自己掉线”。
蓝牙的问题更明显,通信距离基本就是十几米,想实现真正的远程控制,几乎不可行。
4G模块确实性能强,但成本和门槛都在:需要SIM卡有流量套餐,需要一个云服务器或者至少一个公网IP,需要处理TCP长连接、心跳保活、断线重连。对一个简单的“收到短信、执行指令、回复短信”需求来说,这套体系太重了。
短信的底层是GSM网络,只要手机有信号的地方基本就能用,不需要额外配置IP,不需要服务器。设备端只需要在收到短信时解析内容,按约定好的指令做事,再原路回一条短信。它的缺点是不能实时双向长连接、有短信延迟,但对“远程控制”这种低频操作来说,完全够用。
1.2 SIM900A为什么是这个项目里最省事的方案
当前市面上真正便宜又容易买到资料的GSM模块,SIM900A是绕不过去的一个。它工作在GSM/GPRS频段,支持普通的短信收发,也支持GPRS数据业务。从这个项目的实际诉求出发,我们根本用不到GPRS,只用它最基础的短信功能就够了。
SIM900A最大的好处是串口控制。STM32通过一个UART接口向模块发AT指令,模块做的事情就是把AT指令转成GSM网络动作。短信收发、读取、删除、信号查询,全部有标准AT指令对应。这意味着硬件上不需要额外搭建复杂的基带电路,软件上也不需要懂GSM协议栈,只要会串口收发和字符串解析,就能把这个模块驱动起来。
相比SIM7600、Air724UG这些后来的模块,SIM900A的优势是资料特别多,十年前的论坛帖子、示例代码一抓一大把。虽然它的封装大、功耗高、只能2G网络,但在“我需要一个能通短信的老实模块”这个场景下,它反而最不容易卡住。项目中如果对体积、功耗、网络频段没有强制要求,用SIM900A可以把大部分精力集中在“解析短信、执行指令、回复短信”这一条主链路上。
1.3 项目整体功能拆解
这个工程拆开来看,其实就五个环节:接收短信、解析内容、识别指令、执行动作、回复结果。
- 接收短信:SIM900A在收到新短信后会通过串口主动上报一条
+CMTI: "SM",index通知,或者程序周期性地用AT+CMGL查询未读短信。 - 解析内容:把短信正文从AT指令返回的一堆字符串里提取出来。手机号码、时间、正文这三段是最关键的信息。
- 识别指令:STM32端预设若干个指令字符串,比如
#LEDON、#LEDOFF、#STATUS。拿收到的短信正文做匹配,匹配上了就去执行对应的GPIO操作或读取传感器。 - 执行动作:对STM32来说,指令最终落地无非就是控制引脚电平、读取ADC、操作外设。这一层跟普通单片机逻辑没有区别。
- 回复结果:通过
AT+CMGS回复一条短信给发送者,内容是“LED ON”或者“当前温度多少”,让用户知道设备接收到了指令并且状态已经变化。
硬件上,STM32与SIM900A用串口连接,SIM900A插上手机卡,接上天线,供电做好,整个系统就具备了这个功能闭环。下面先从硬件说起,因为我在这个环节吃过不少亏。
2. 硬件接线与上电前的几个关键点:SIM900A不是随便接电就能跑的
2.1 模块供电是最大的坑:峰值电流能到2A
先说结论:SIM900A的VBAT供电范围是3.2V到4.8V,典型值是4.0V左右,但在射频发射瞬间电流会突然拉到接近2A。很多第一次用这个模块的人,直接拿STM32开发板上的3.3V或者USB的5V去给模块供电,结果就是模块启动到一半自动掉电、反复重启,或者一打电话、一发短信就复位。
有人问,USB口不是标称5V/500mA吗?把5V接到SIM900A的VBAT上好像也超过4.8V了,而且电流不够。少数模块板上带了稳压电路,可以对USB口供电,但性能很勉强。我的做法是单独用一个DC-DC降压模块,输入电压建议9V或者12V,输出调到4.0V至4.2V之间,并且滤波电容要加足。至少并联一个100uF电解电容和一个0.1uF陶瓷电容,尽量靠近模块的VBAT引脚。如果手头有示波器,可以看模块在AT+CSQ命令后发射瞬间的电压跌落情况,凡是跌到3.3V以下,基本就要重新设计供电了。
STM32主板和SIM900A模块建议分开供电,共地即可。STM32的电源用自己的5V/3.3V来源,SIM900A的电源用独立的DC-DC。这样做不仅避免射频大电流干扰单片机的ADC基准,排查问题的时候也更容易定位是供电还是通信的问题。
2.2 串口电平匹配、天线和SIM卡细节
SIM900A的UART引脚是LVTTL电平,也就是3.3V逻辑。STM32的串口也是3.3V电平,所以两者可以直接连接。但如果你用的是带TTL转USB模块的调试板,或者板子上的电平是5V的,就必须加电平转换,否则长期运行会烧模块引脚。
连接上,STM32的TX接SIM900A的RX,STM32的RX接SIM900A的TX,GND共地。有些开发板上有DB9接口或者MAX232芯片,不能直接用来接SIM900A。这个我在第一次调的时候搞反了,串口助手看不到模块的回复,查了半天发现是电平不匹配。
天线是另一个容易忽略的地方。SIM900A必须插GSM天线,否则信号强度会非常差,甚至无法注册网络。天线接口一般是IPX插座,把天线卡扣按上去听到“咔哒”声就算到位。如果你的设备放在金属壳里,天线最好引到外壳外面,而且天线周围不要有大面积覆铜和金属屏蔽罩。
SIM卡方面,SIM900A的卡座支持1.8V和3V的SIM卡,插卡方向有防呆设计,但经常有人把卡插反。还有一个常见问题是卡座上有一个“卡到位检测”开关,如果卡没完全推进去,模块会一直报找不到SIM卡。上电后用AT+CPIN?命令就能看到SIM卡状态,返回+CPIN: READY说明卡识别正常。
2.3 上电时序与开机检测
SIM900A不是一通电就自动开机的。VBAT通电之后,PWRKEY引脚需要被拉低至少500ms以上,模块才会启动。因此硬件设计上,如果直接用STM32的GPIO去控制PWRKEY,要注意电平匹配,并且开机GPIO要设置为推挽输出,拉低一段时间再释放。
启动之后,模块需要几秒钟时间搜索网络。可以通过AT+CREG?查询网络注册状态,返回+CREG: 0,1或+CREG: 0,5表示已注册上网络。还有AT+CSQ可以查询信号强度,返回的数值范围是0到31,一般大于10就说明信号可用。
我习惯在STM32上电后先发一个AT,判断模块是否在线。如果模块没有回应,就认为它还没开机,程序执行PWRKEY拉低1秒去触发开机,然后每隔200ms发一次AT,直到返回OK。这个开机握手逻辑看起来简单,但对后面稳定跑短信识别非常重要,因为如果模块没完全启动就发AT+CMGL,命令会直接丢在缓冲区里得不到响应。
3. AT指令与短信收发机制:文本模式、PDU模式和中文编码
3.1 从AT+CMGF说起:两种短信模式怎么选
SIM900A支持两种短信模式:文本模式(Text Mode)和PDU模式。用AT+CMGF=1切换到文本模式,用AT+CMGF=0切换到PDU模式。很多教程推荐直接用PDU模式,因为PDU能处理中文和长短信,传输的内容是二进制编码后的十六进制字符串。但如果你的短信指令是用英文/ASCII字符设计的,那么文本模式已经够用,而且代码里做字符串匹配非常方便。
文本模式下,短信正文直接用可读ASCII字符发送。比如AT+CMGS="13800138000"回车之后,模块返回>,此时输入短信内容,最后以十六进制0x1A(也就是ASCII的Ctrl+Z)作为结束标志。短息发出后,模块返回+CMGS: <index>以及OK。
接收短信时,文本模式的返回数据自带可读文本。执行:
AT+CMGL="REC UNREAD"模块会返回类似:
+CMGL: 1,"REC UNREAD","13800138000","","24/03/10,14:30:25+32" #LEDON注意最后一行是短信正文,前面的引号里分别是索引、状态、号码、时间。STM32解析的时候,只需要找到最后一个换行后的一行数据,那就是正文。
如果你的指令是中文,比如“打开灯”“查询状态”,文本模式就需要处理UCS2编码。SIM900A通过AT+CSCS="UCS2"切换字符集,此后短信内容就要用Unicode十六进制字符串发送和接收。这样处理起来并不比PDU模式简单,所以我更建议把指令定义成英文或者数字,比如#DDON,把底层编码的复杂度直接绕开。
3.2 PDU模式下的UCS2编码与中文短信解析
如果项目确实需要中文短信内容作为指令,那还是要正视PDU模式。PDU模式的好处是模块的字符集统一用UCS2编码,无论中文还是英文,短信内容都以十六进制Unicode表示,不依赖模块内部字符集的设置。
一条中文短信“打开灯”,在PDU模式下,短信正文对应的UCS2编码是十六进制字符串:625389E7006F。其中6253是“打”,89E7是“开”,006F是“灯”。STM32端要做的就是把收到的短信内容中,每四个十六进制字符识别成一个Unicode字符,然后和自己预设的指令编码表做匹配。
实际开发中,我一般不在单片机端做中文输入。最省事的办法是上位机或者测试脚本里把中文指令转换成UCS2十六进制字符串,然后烧录进STM32的指令表。比如:
// “打开灯”的UCS2编码 const char *cmd_open_light = "625389E7006F"; // “查询状态”的UCS2编码 const char *cmd_query_status = "67E58BE172600001";这样STM32不需要内置中文字库,只需要做十六进制字符串的比较。缺点是在手机上编辑发送的指令必须是中文,收到的命令需要经过模块的UCS2解码,短信内容显示出来才是正常中文。对用户来说无感,开发者多写一层转换而已。
3.3 完整AT指令序列:读短信、删短信、发短信
我把最常用的AT指令整理成一张表,方便开发的时候查看。
| 用途 | 指令 | 说明 |
|---|---|---|
| 模块在线检测 | AT | 返回OK |
| 设置文本模式 | AT+CMGF=1 | 0为PDU模式 |
| 设置字符集 | AT+CSCS="UCS2" | 可选,中文场景使用 |
| 新短信主动上报 | AT+CNMI=2,1,0,0,0 | 收到新短信时上报+CMTI |
| 查询网络注册 | AT+CREG? | 返回0,1或0,5表示已注册 |
| 查询信号强度 | AT+CSQ | 返回值为信号等级 |
| 读未读短信 | AT+CMGL="REC UNREAD" | 返回未读短信列表 |
| 读指定短信 | AT+CMGR=<index> | 按短信索引读取 |
| 删除短信 | AT+CMGD=<index> | 0表示删除全部已读 |
| 发送短信 | AT+CMGS="号码" | 之后输入内容,0x1A结束 |
读短信和删短信要成对处理。如果只读不删,短信会越积越多,存储满了之后模块可能无法接收新短信。我之前在调试时遇到“为什么收不到新短信”的问题,深入查才发现SIM900A的短信存储满了,AT+CMGL返回老数据,AT+CNMI虽然收到了+CMTI提示,但新短信根本没有空间写入。
所以程序里应该在成功读取一条短信并完成指令识别后,马上执行AT+CMGD=<index>把这条短信删掉。如果短信内容格式错误,也建议删除,避免垃圾短信持续占用存储空间。
4. STM32端怎么处理AT响应:串口接收与状态机设计
4.1 不定长串口数据接收:中断+DMA的常见做法
SIM900A和STM32之间的通信本质上就是串口收发字符串。难点在于AT指令的返回是不定长的,有的命令只回OK,有的命令会回一大段短信列表。如果只用简单的HAL_UART_Receive阻塞接收,模块回复还没发完,程序就可能超时。
我目前用得比较顺手的方式是“串口空闲中断+DMA接收”。开启串口DMA接收,数据到达时自动存进缓冲区,一帧数据发送完,串口总线变成空闲电平,空闲中断触发,此时缓冲区里的就是一条完整的AT响应数据。这种方式对CPU占用极小,而且不会丢字节。
代码思路(HAL库,已简化):
#define BUF_SIZE 1024 uint8_t sim900a_rx_buf[BUF_SIZE]; uint8_t sim900a_dma_buf[BUF_SIZE]; void SIM900A_UART_Init(void) { HAL_UARTEx_ReceiveToIdle_DMA(&huart2, sim900a_dma_buf, BUF_SIZE); __HAL_DMA_DISABLE_IT(&hdma_uart2_rx, DMA_IT_HT); }空闲中断回调里,把DMA缓冲区里的数据复制到业务缓冲区,并清空DMA接收计数:
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart == &huart2) { memcpy(sim900a_rx_buf, sim900a_dma_buf, Size); sim900a_data_len = Size; // 通知业务层处理AT响应 SIM900A_ParseData(sim900a_rx_buf, sim900a_data_len); // 重新启动下一次接收 HAL_UARTEx_ReceiveToIdle_DMA(&huart2, sim900a_dma_buf, BUF_SIZE); __HAL_DMA_DISABLE_IT(&hdma_uart2_rx, DMA_IT_HT); } }如果你用的芯片没有空闲中断,也可以用逐字节接收中断,把每个字节放进环形缓冲区,后面统一解析。区别不大,关键是把“收数据”和“解析数据”两个过程分开,不要让业务逻辑卡在串口底层。
4.2 用状态机解析AT返回,而不是硬等
SIM900A的串口数据可能是分片到达的,如果程序只在某一时刻判断一次“缓冲区里有没有OK”,很容易漏掉后半段数据。所以我习惯把AT响应解析写成一个状态机,或者至少做到“收到完整一帧后再统一处理”。
一个简化的解析流程是:
- 判断缓冲区中是否包含
\r\n,如果没有,说明数据不完整,继续等待。 - 如果包含,则把这行内容提取出来。
- 判断行首前缀:
- 以
OK开头,表示当前指令执行完成。 - 以
ERROR开头,表示指令执行失败。 - 以
+CMTI开头,表示有新短信,后面跟的是"SM",<index>。 - 以
+CMGL开头,表示短信数据开始,后续跟着短信正文。
- 以
在STM32的裸机程序里,我一般用一个全局状态变量记录当前正在进行的AT操作。比如:
typedef enum { AT_IDLE, AT_WAIT_CMGL_LIST, AT_WAIT_CMGS_DATA, } at_op_state_t;发完AT+CMGL="REC UNREAD"后,状态置为AT_WAIT_CMGL_LIST,然后在串口回调里解析返回。等读到OK的时候,表示本次读短信操作结束,可以决定下一轮是执行指令还是继续发下一条AT指令。
这种设计的好处是,程序不需要在发AT指令后疯狂干等,而是通过串口中断驱动逐步推进,既稳定又不会阻塞其它传感器或者外设的轮询逻辑。
4.3 指令识别与执行:字符串匹配和消息回复
当程序从+CMGL返回里提取出短信正文后,最直接的方式就是和预设指令比较。比如我用的是#开头的内容作为指令:
if (strncmp(sms_content, "#LEDON", strlen("#LEDON")) == 0) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); SIM900A_SendSMS(sms_phone, "LED ON"); } else if (strncmp(sms_content, "#LEDOFF", strlen("#LEDOFF")) == 0) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); SIM900A_SendSMS(sms_phone, "LED OFF"); } else if (strncmp(sms_content, "#STATUS", strlen("#STATUS")) == 0) { uint16_t adc = ReadADC(); SIM900A_SendSMS(sms_phone, "CURRENT STATUS: ..."); }这里要注意,从AT返回的短信正文末尾通常会带一个\r或者\n,比较前最好先做一次尾部清理。如果短信内容包含了前缀信息(比如UCS2模式下正文是十六进制字符串),要记得先把正文转换成可比较的格式。
回复短信的发送函数,核心代码如下:
void SIM900A_SendSMS(const char *phone, const char *msg) { char cmd[64]; sprintf(cmd, "AT+CMGS=\"%s\"\r", phone); SIM900A_SendString(cmd); // 等待模块返回 '>' HAL_Delay(100); SIM900A_SendString(msg); // 发送结束符 0x1A uint8_t end_char = 0x1A; SIM900A_SendByte(&end_char, 1); }等待>这一步不能省。模块只有在返回>后才会接受短信内容,直接秒发容易把短信内容当成AT命令处理。我一般会写一个SIM900A_WaitForChar('>'),超时时间设2秒,确保模块已经进入内容输入态。
5. 调试时最容易翻车的几个地方:我的踩坑记录
5.1 供电不足导致的模块反复重启
这个项目我第一次联调时,直接用USB转串口的5V给SIM900A供电,结果模块一开机就重启,串口助手间歇性打印RDY,有时候还会报UNDERVOLTAGE警告。用万用表量VBAT,空闲时4.5V,一旦模块发射搜索网络,电压瞬间跌到2.9V。这算是SIM900A最典型的故障现象。
后来换了独立的DC-DC模块,把输入12V降到4.2V,同时加了470uF的电解电容储能,问题才彻底消失。这里提醒一句:不要只看模块静态电流,短信收发是突发射频操作,瞬间电流远比平均电流大。电容也不能太大,太大容易拖慢上电时序,但至少要有100uF级别。
如果电路板已经画完了,没有预留大电容,可以在模块附近用飞线焊接一个电容。实测中,我在VBAT和GND之间焊了一个470uF/10V的电解电容,模块的稳定性立刻上了一个台阶。
5.2 中文短信回复总是乱码?编码表的问题
用文本模式发中文时,必须把字符集设置成UCS2。假设你执行了AT+CMGF=1,没执行AT+CSCS="UCS2",然后直接发AT+CMGS="138...",再输入中文内容,模块会把中文按GB2312编码解读。但SIM900A本身对中文支持不统一,最后可能发送成功,但对方收到的是乱码。
标准做法是几条指令组合使用:
AT+CMGF=1 AT+CSCS="UCS2" AT+CSMP=17,167,0,8其中AT+CSMP=17,167,0,8是设置短信格式,最后一个8表示UCS2编码。由于第三条命令在不同固件版本下表现不一致,需要以实际模块手册为准。如果你的中文指令本身是固定写死在单片机里的,那我建议直接在上位机把中文转成UCS2十六进制字符串,再烧到程序里,这样最稳。
5.3 发短信成功但收不到?SIM卡与信号问题
排除供电和编码问题后,如果模块回复+CMGS: 35,OK,说明网络已经接受短信发送请求,但对方没收到,那大概率是信号差或者对方手机拦截了GSM短信。AT+CSQ返回数值最好在15以上,低于10,发短信时经常出现“网络超时但后来又发出去好几条”的诡异现象。
还有一次我查了很久,最后发现是天线的IPX座没扣紧,信号强度一直在+CSQ: 5附近徘徊。把天线重新插到位,信号直接到+CSQ: 21。所以接到新模块,第一件事就是先查+CSQ,不要急着跑业务逻辑。
另外,SIM卡不要用停机或欠费卡,否则能查询到网络但发送总失败。调试时直接把自己的手机卡插进去,能简化很多问题。
6. 从示例工程到真实产品:还能怎么改
6.1 增加白名单与安全校验
短信远程控制有个天然的安全问题:只要知道设备号码和指令格式,任何人都能发短信控制你的设备。这在真实部署中是不能接受的。所以至少要加一个简单的白名单机制。
SIM900A在读取短信时,AT返回里带有发送方号码。STM32端解析后,把号码和预设白名单做比较,如果不在白名单内,直接回复“抱歉,无权限”并删除短信,不执行任何控制动作。
白名单可以存放在STM32内部Flash里,比如预留一段存储区保存多个号码字符串。这样产品出厂后,用户通过特定指令添加手机号。更稳妥的做法是增加指令加密,比如每条指令后面附带校验码,但短信通道本身延迟高、内容不可控,加密逻辑要设计得足够简单实用。
6.2 多条指令并发与长短信分片
实际使用中,用户可能在一条短信里写多个指令,比如“+LEDON+LEDOFF”,或者短信被手机切分成两条连续短信发送。处理起来其实不复杂:STM32读取短信正文后,先做分隔符拆分,比如按+分段,然后逐条执行。但要注意,短信分片后,两条短信到达SIM900A的索引不同,程序需要把同一发送方的相邻短信内容拼接后再判断。
长短信分片不推荐在嵌入式端做复杂重组,因为GSM短信单条上限是140个字节,PDU模式下能容纳的汉字有限。大多数远程控制指令都很短,把指令设计得精简一点,比写一个完整的长短信重组器要省事得多。
6.3 低功耗待机与唤醒
这个项目的完整形态如果要做成电池供电,那就绕不开SIM900A的功耗问题。模块在正常待机时电流也有毫安级,发送短信时几百毫安。如果设备长期不通信,可以让模块进入休眠模式。
SIM900A常用的低功耗指令是AT+CFUN=0,模块会关闭射频功能,电流降到很低。需要收发短信时,用AT+CFUN=1重新打开。或者用AT+CSCLK=1使能慢时钟模式。在STM32端,可以把SIM900A的某根GPIO接成唤醒输入,或者通过定时器周期性唤醒模块检查未读短信。
不过说句实在话,SIM900A天生就不是为超低功耗设计的。如果你要做电池供电的野外设备,更合适的选择是带NB-IoT的模块,比如BC26,待机电流低一个数量级。但在“快速验证短信控制逻辑”这个阶段,SIM900A仍然是便宜且稳妥的方案。
回到一开始那个需求。短信远程控制这个方向,放在今天看好像不够时髦,但它在很多特定场景里依然是无缝替代方案。这个STM32+SIM900A工程最值得学习的不是某条AT指令,而是“串口数据怎么解析、AT响应怎么组成状态机、短信内容怎么从一堆字符串里安全提取出来”这整套思路。把这个思路吃透,换个4G模块、5G模块也只是换一套AT指令表,整体架构完全可以复用。我自己做这片小工程时最大的感受是:先把串口助手把模块单独调通,再连STM32,不要一上来就焊死一起调,不然问题很难定位。顺序对了,整个项目会顺利很多。
本文还有配套的精品资源,点击获取