STM32+SIM900A实现短信远程控制:AT指令解析与工程实践

STM32+SIM900A实现短信远程控制:AT指令解析与工程实践 简介本资源是一套基于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控制可直接编译烧录运行是掌握STM32GSM模块协同开发、文本协议解析与嵌入式通信实战的优质参考工程。 先把场景摆出来你做一个设备摆在没有人值守的地方比如乡下的农业大棚、临时仓库、或者一台户外广告屏控制箱。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通知或者程序周期性地用ATCMGL查询未读短信。解析内容把短信正文从AT指令返回的一堆字符串里提取出来。手机号码、时间、正文这三段是最关键的信息。识别指令STM32端预设若干个指令字符串比如#LEDON、#LEDOFF、#STATUS。拿收到的短信正文做匹配匹配上了就去执行对应的GPIO操作或读取传感器。执行动作对STM32来说指令最终落地无非就是控制引脚电平、读取ADC、操作外设。这一层跟普通单片机逻辑没有区别。回复结果通过ATCMGS回复一条短信给发送者内容是“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引脚。如果手头有示波器可以看模块在ATCSQ命令后发射瞬间的电压跌落情况凡是跌到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的RXSTM32的RX接SIM900A的TXGND共地。有些开发板上有DB9接口或者MAX232芯片不能直接用来接SIM900A。这个我在第一次调的时候搞反了串口助手看不到模块的回复查了半天发现是电平不匹配。天线是另一个容易忽略的地方。SIM900A必须插GSM天线否则信号强度会非常差甚至无法注册网络。天线接口一般是IPX插座把天线卡扣按上去听到“咔哒”声就算到位。如果你的设备放在金属壳里天线最好引到外壳外面而且天线周围不要有大面积覆铜和金属屏蔽罩。SIM卡方面SIM900A的卡座支持1.8V和3V的SIM卡插卡方向有防呆设计但经常有人把卡插反。还有一个常见问题是卡座上有一个“卡到位检测”开关如果卡没完全推进去模块会一直报找不到SIM卡。上电后用ATCPIN?命令就能看到SIM卡状态返回CPIN: READY说明卡识别正常。2.3 上电时序与开机检测SIM900A不是一通电就自动开机的。VBAT通电之后PWRKEY引脚需要被拉低至少500ms以上模块才会启动。因此硬件设计上如果直接用STM32的GPIO去控制PWRKEY要注意电平匹配并且开机GPIO要设置为推挽输出拉低一段时间再释放。启动之后模块需要几秒钟时间搜索网络。可以通过ATCREG?查询网络注册状态返回CREG: 0,1或CREG: 0,5表示已注册上网络。还有ATCSQ可以查询信号强度返回的数值范围是0到31一般大于10就说明信号可用。我习惯在STM32上电后先发一个AT判断模块是否在线。如果模块没有回应就认为它还没开机程序执行PWRKEY拉低1秒去触发开机然后每隔200ms发一次AT直到返回OK。这个开机握手逻辑看起来简单但对后面稳定跑短信识别非常重要因为如果模块没完全启动就发ATCMGL命令会直接丢在缓冲区里得不到响应。3. AT指令与短信收发机制文本模式、PDU模式和中文编码3.1 从ATCMGF说起两种短信模式怎么选SIM900A支持两种短信模式文本模式Text Mode和PDU模式。用ATCMGF1切换到文本模式用ATCMGF0切换到PDU模式。很多教程推荐直接用PDU模式因为PDU能处理中文和长短信传输的内容是二进制编码后的十六进制字符串。但如果你的短信指令是用英文/ASCII字符设计的那么文本模式已经够用而且代码里做字符串匹配非常方便。文本模式下短信正文直接用可读ASCII字符发送。比如ATCMGS13800138000回车之后模块返回此时输入短信内容最后以十六进制0x1A也就是ASCII的CtrlZ作为结束标志。短息发出后模块返回CMGS: index以及OK。接收短信时文本模式的返回数据自带可读文本。执行ATCMGLREC UNREAD模块会返回类似CMGL: 1,REC UNREAD,13800138000,,24/03/10,14:30:2532 #LEDON注意最后一行是短信正文前面的引号里分别是索引、状态、号码、时间。STM32解析的时候只需要找到最后一个换行后的一行数据那就是正文。如果你的指令是中文比如“打开灯”“查询状态”文本模式就需要处理UCS2编码。SIM900A通过ATCSCSUCS2切换字符集此后短信内容就要用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设置文本模式ATCMGF10为PDU模式设置字符集ATCSCSUCS2可选中文场景使用新短信主动上报ATCNMI2,1,0,0,0收到新短信时上报CMTI查询网络注册ATCREG?返回0,1或0,5表示已注册查询信号强度ATCSQ返回值为信号等级读未读短信ATCMGLREC UNREAD返回未读短信列表读指定短信ATCMGRindex按短信索引读取删除短信ATCMGDindex0表示删除全部已读发送短信ATCMGS号码之后输入内容0x1A结束读短信和删短信要成对处理。如果只读不删短信会越积越多存储满了之后模块可能无法接收新短信。我之前在调试时遇到“为什么收不到新短信”的问题深入查才发现SIM900A的短信存储满了ATCMGL返回老数据ATCNMI虽然收到了CMTI提示但新短信根本没有空间写入。所以程序里应该在成功读取一条短信并完成指令识别后马上执行ATCMGDindex把这条短信删掉。如果短信内容格式错误也建议删除避免垃圾短信持续占用存储空间。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;发完ATCMGLREC 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, ATCMGS\%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。假设你执行了ATCMGF1没执行ATCSCSUCS2然后直接发ATCMGS138...再输入中文内容模块会把中文按GB2312编码解读。但SIM900A本身对中文支持不统一最后可能发送成功但对方收到的是乱码。标准做法是几条指令组合使用ATCMGF1 ATCSCSUCS2 ATCSMP17,167,0,8其中ATCSMP17,167,0,8是设置短信格式最后一个8表示UCS2编码。由于第三条命令在不同固件版本下表现不一致需要以实际模块手册为准。如果你的中文指令本身是固定写死在单片机里的那我建议直接在上位机把中文转成UCS2十六进制字符串再烧到程序里这样最稳。5.3 发短信成功但收不到SIM卡与信号问题排除供电和编码问题后如果模块回复CMGS: 35,OK说明网络已经接受短信发送请求但对方没收到那大概率是信号差或者对方手机拦截了GSM短信。ATCSQ返回数值最好在15以上低于10发短信时经常出现“网络超时但后来又发出去好几条”的诡异现象。还有一次我查了很久最后发现是天线的IPX座没扣紧信号强度一直在CSQ: 5附近徘徊。把天线重新插到位信号直接到CSQ: 21。所以接到新模块第一件事就是先查CSQ不要急着跑业务逻辑。另外SIM卡不要用停机或欠费卡否则能查询到网络但发送总失败。调试时直接把自己的手机卡插进去能简化很多问题。6. 从示例工程到真实产品还能怎么改6.1 增加白名单与安全校验短信远程控制有个天然的安全问题只要知道设备号码和指令格式任何人都能发短信控制你的设备。这在真实部署中是不能接受的。所以至少要加一个简单的白名单机制。SIM900A在读取短信时AT返回里带有发送方号码。STM32端解析后把号码和预设白名单做比较如果不在白名单内直接回复“抱歉无权限”并删除短信不执行任何控制动作。白名单可以存放在STM32内部Flash里比如预留一段存储区保存多个号码字符串。这样产品出厂后用户通过特定指令添加手机号。更稳妥的做法是增加指令加密比如每条指令后面附带校验码但短信通道本身延迟高、内容不可控加密逻辑要设计得足够简单实用。6.2 多条指令并发与长短信分片实际使用中用户可能在一条短信里写多个指令比如“LEDONLEDOFF”或者短信被手机切分成两条连续短信发送。处理起来其实不复杂STM32读取短信正文后先做分隔符拆分比如按分段然后逐条执行。但要注意短信分片后两条短信到达SIM900A的索引不同程序需要把同一发送方的相邻短信内容拼接后再判断。长短信分片不推荐在嵌入式端做复杂重组因为GSM短信单条上限是140个字节PDU模式下能容纳的汉字有限。大多数远程控制指令都很短把指令设计得精简一点比写一个完整的长短信重组器要省事得多。6.3 低功耗待机与唤醒这个项目的完整形态如果要做成电池供电那就绕不开SIM900A的功耗问题。模块在正常待机时电流也有毫安级发送短信时几百毫安。如果设备长期不通信可以让模块进入休眠模式。SIM900A常用的低功耗指令是ATCFUN0模块会关闭射频功能电流降到很低。需要收发短信时用ATCFUN1重新打开。或者用ATCSCLK1使能慢时钟模式。在STM32端可以把SIM900A的某根GPIO接成唤醒输入或者通过定时器周期性唤醒模块检查未读短信。不过说句实在话SIM900A天生就不是为超低功耗设计的。如果你要做电池供电的野外设备更合适的选择是带NB-IoT的模块比如BC26待机电流低一个数量级。但在“快速验证短信控制逻辑”这个阶段SIM900A仍然是便宜且稳妥的方案。回到一开始那个需求。短信远程控制这个方向放在今天看好像不够时髦但它在很多特定场景里依然是无缝替代方案。这个STM32SIM900A工程最值得学习的不是某条AT指令而是“串口数据怎么解析、AT响应怎么组成状态机、短信内容怎么从一堆字符串里安全提取出来”这整套思路。把这个思路吃透换个4G模块、5G模块也只是换一套AT指令表整体架构完全可以复用。我自己做这片小工程时最大的感受是先把串口助手把模块单独调通再连STM32不要一上来就焊死一起调不然问题很难定位。顺序对了整个项目会顺利很多。本文还有配套的精品资源点击获取