ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32+SIM900A短信远程控制实战:AT指令与串口通信详解

STM32+SIM900A短信远程控制实战:AT指令与串口通信详解 简介本资源是一套基于STM32F10x系列单片机与SIM900A GSM模块实现短信指令识别与自动回复的完整嵌入式项目工程面向嵌入式初学者及物联网实践开发者解决远程控制、简易IoT终端响应等典型应用场景问题。压缩包共90个文件包含核心C源码8个.c、头文件6个.h、编译中间文件11个.o、10个.d、Keil工程配置与链接脚本如.uv2、.sct、.axf等以及README说明文档和硬件驱动模块如GSM.H、USART.H、DS18B20.c等整体大小为2.08MB。已有211人学习下载。读者可直接导入Keil MDK环境编译运行获得可复用的AT指令解析框架、短信读取/发送逻辑、关键词匹配机制及多传感器如DHT21、DS18B20联动示例代码特别适合掌握UART通信、GSM模块调试与嵌入式文本处理能力的进阶实践。 上个月收拾工位翻出一块吃灰很久的SIM900A模块想起之前给老家大棚做的一套短信远程控制装置。今天把核心逻辑抽出来聊聊。这个项目的核心就一件事STM32通过串口连接SIM900A让模块在收到短信后触发通知单片机读取短信内容、解析出预定义指令再自动回复一条短信告诉用户执行结果。整个链路看起来简单真正动手做的时候供电、AT指令时序、串口缓冲处理、短信内容解析这些环节每一步都能让人怀疑人生。这篇东西适合手里已经有STM32开发板、想用最低成本实现“短信远程控制”的开发者不管你是做毕设、竞赛还是个人DIY都可以直接照着抄。1. 项目整体设计与方案选型1.1 要解决的真实问题短信远程指令交互这个项目要解决的是没有互联网环境下的远程控制问题。比如大冬天的蔬菜大棚没有WiFi也没有网线但人的手机有信号那就让设备插一张卡通过短信来控制卷帘电机、水泵或者灯光。用户发一条“LED ON”到设备里的SIM卡STM32收到后解析出“LED ON”这个指令控制GPIO把继电器或者灯打开再回复一条“LED ON OK”给用户。整个过程看起来像聊天实际上是一条完整的指令闭环。和局域网通信不一样短信通信天然有延迟和不确定性。一公里的距离和一百公里的距离延迟差不太多但网络拥塞、信号弱、短信中心排队这些因素都会导致短信几分钟后才到达。所以设计的时候不能指望像TCP一样确认机制而是要把状态机、超时、重试和日志这些都考虑进去。这也正是这个项目最值得写出来的地方不是AT指令怎么发而是怎么在不可靠的链路上把“收到短信-解析-执行-回复”这个闭环做稳定。1.2 为什么选SIM900A而不是4G模块或WiFi我选SIM900A有几个原因。第一是便宜二手模块十几块到三十几块就能拿下核心板和模块加起来不到五十块钱。第二是资料多SIM900A在物联网学习圈子里几乎是标配AT指令集文档到处都是踩坑经验也能搜到一堆。第三是功耗和体积控制得不错待机电流在1mA级别做电池供电的控制器也扛得住。但必须说清楚SIM900A是2G模块现在很多地区的2G网络信号已经肉眼可见地变弱了甚至有些地方已经关闭了2G基站。所以你要先确认自己所在的区域还有GSM信号再决定用不用这个方案。如果项目要长期稳定运行更建议换成4G Cat.1模块比如Air724UG、EC800M这些硬件设计思路类似AT指令也基本兼容核心的短信收发、读取、删除逻辑可以直接平移过去。这个项目我建议定位成学习型产品或者短期限场景使用不要拿去给客户做长期生产交付。1.3 系统架构与数据流整个系统可以拆成三条链路。第一条是SIM900A和基站之间走的是GSM无线信号不用我们管。第二条是SIM900A和STM32之间走的是UART串口这是开发者需要处理的全部内容。第三条是STM32和外部设备之间比如LED、继电器、传感器通过GPIO或I2C/SPI去控制。短信数据在内部的流动顺序是这样的用户手机发送短信到SIM卡。SIM900A检测到新短信通过串口主动上报一行字符串CMTI: SM,2里面那个2就是短信存储在SIM卡里的位置。STM32收到这行通知后等待几百毫秒发送ATCMGR2去读取这条短信。SIM900A返回短信内容包含发送者手机号、时间、短信正文。STM32解析出手机号和正文匹配指令表执行对应动作。STM32发送ATCMGS手机号模块返回提示符后发送回复内容最后发送十六进制1A结束。用户手机收到回复短信。整个过程里STM32虽然看起来是在“收发短信”但本质上它就是和SIM900A聊AT指令。SIM900A屏蔽了GSM协议栈让你用简单的字符串指令就能操作短信、电话、GPRS这些功能。理解了这个架构后面写代码就顺了。2. 硬件准备与电路连接2.1 需要的物料清单硬件部分不算复杂但有些坑必须先排掉。STM32最小系统板我用的STM32F103C8T6也就是常见的“蓝板”。HAL库和标准库都可以后面代码我用HAL库演示。SIM900A模块注意买带天线接口和SIM卡槽的完整模块不要买裸片自己焊GPRS射频电路非常痛苦。SIM卡一张能正常收发短信的SIM卡最好是老一点的卡。某些新出的物联网卡默认关闭短信功能需要找运营商确认。天线GSM天线有些模块随机附带没有就单独买一根。电源这个最关键。SIM900A峰值电流能到2A建议准备一个5V、额定电流2A以上的电源适配器或者用12V电源加DC-DC降压到5V。电平转换模块SIM900A的UART电平是2.8VSTM32是3.3V稳妥点加一个3.3V转3.3V的隔离不现实但可以用两颗电阻分压加一颗三极管或者直接买一个TTL电平转换模块。如果只是测试很多开发板可以直连但不推荐长期用。USB转TTL模块用于先调试SIM900A比如CH340或者CP2102的模块。杜邦线若干、LED及限流电阻。物料清单里最容易被忽略的是天线。SIM900A如果没有接天线或者天线没接好模块很可能一直注册不上网络你会看到指示灯慢闪ATCREG?的返回值一直是0或2。这时候不用怀疑代码先检查天线。2.2 供电设计最容易翻车的点SIM900A的供电不是随便找一个5V就能解决的。模块开机瞬间电流尖峰很大官方手册推荐电源要能提供2A的峰值电流。如果你用电脑USB口供电大概率会出现两种情况一种是模块一直重启指示灯亮一下就灭另一种是打电话或者发短信的瞬间系统电压被拉低STM32也跟着复位。我的做法是单独给SIM900A一路电源不跟STM32共用。STM32用USB或者AMS1117的3.3V供电SIM900A用5V/2A电源适配器供电。然后在SIM900A的电源输入端并联一颗1000uF的电解电容和一颗0.1uF的陶瓷电容电解电容用来扛峰值电流陶瓷电容用来滤高频噪声。这个组合我测下来很稳模块发短信时电压跌落幅度可以控制在可接受范围。还有一点SIM900A的开机方式并不是一上电就启动它是一个电平触发开机引脚低电平持续一段时间才能开机。有些模块板上已经做了自动开机电路但有些没有。如果是纯模块需要把PWRKEY引脚拉低1秒以上再放开。这个细节很容易忽略很多人接好线发现模块不亮其实就是没开机而不是没供电。2.3 串口电平与STM32接线SIM900A的串口是TTL电平但实际电平是2.8V。STM32的串口是高电平3.3V两者电压差不大很多教程直接TXD接RXD、RXD接TXD也能跑通。但为了可靠还是建议做个电平转换。最简单的办法是用模块板载的电路有的SIM900A模块板子上已经集成了电平转换你只要注意看模块丝印是“TTL”还是“RS232”。如果是TTL直接和STM32连接风险不大如果标着RS232必须加MAX232转换。接线表参考SIM900A引脚接STM32引脚说明TXDPA10 (USART1_RX)SIM900A发送到STM32RXDPA9 (USART1_TX)STM32发送到SIM900AGNDGND必须共地PWRKEY可接一个GPIO或手动按键低电平开机5V5V电源给模块供电注意串口要交叉连接这一点新手经常搞反。调试时如果模块收不到AT指令先检查TXD/RXD是不是交叉接的。还有一个容易踩的坑是共用电源地线没接好串口通信会出现乱码或者完全没响应。2.4 上电启动确认连接好硬件后先不要急着接STM32我建议先用USB转TTL模块单独调试SIM900A。USB转TTL的TXD接SIM900A的RXDUSB转TTL的RXD接SIM900A的TXD然后打开串口助手。SIM900A上电后指示灯会开始闪。模块启动大概需要几秒钟等指示灯规律闪烁后在串口助手里发送AT如果一切正常模块会回复OK。如果发送AT没有反应先检查波特率。很多SIM900A模块默认是115200也有不少出厂是9600两种都试一下。还有模块支持自动波特率检测第一次发送AT时模块内部会去测算波特率你看到串口可能没有回复再发一次AT就OK了。这时候说明模块是活的就可以接STM32了。3. SIM900A基础配置与AT指令实操3.1 用USB转TTL手工验证模块用串口助手调试是后面排查问题的基础我每次做这个项目都会先把AT指令在串口助手里完整跑一遍确认模块没问题再写单片机代码。这样可以把问题分离硬件问题在硬件阶段暴露软件问题在软件阶段解决。串口助手打开后波特率选115200发送新行那个勾选上也就是每条指令回车换行。先发AT看是否能返回OK。然后发ATCGMM模块会返回型号信息比如SIMCOM_SIM900A。接着用ATCPIN?查询SIM卡状态返回READY说明SIM卡识别正常。这里有个小经验SIM900A的AT指令是不区分大小写的但很多命令后面不能带空格比如ATCMGF1写成ATCMGF 1可能就会报ERROR。另外每条指令以回车换行结尾在串口助手里的“发送新行”其实就是加\r\n。这一点后面STM32代码里发字符串也要注意。3.2 设置短信模式和接收通知要让模块在收到短信后主动通知单片机需要设置两样东西。第一条是短信模式命令是ATCMGF1。等号后面的1表示文本模式0表示PDU模式。文本模式下收发英文、数字都很简单PDU模式可以收发中文但编码复杂度高。项目初期先用文本模式跑通整个流程后续需要中文再切换PDU。第二条是短信通知命令是ATCNMI2,1,0,0,0。这条命令是告诉SIM900A有新短信到达时直接通过串口上报CMTI: SM,位置。如果不设置模块收到短信后只会默默存起来不会主动告诉你。ATCNMI后面五个参数的含义网上有很多说明实际做短信控制只需要记住这个组合就行它是最常用的配置。设置完这两条后用另一台手机给SIM卡发一条hello串口助手应该会看到CMTI: SM,1这个1就是短信的存储位置。然后发送ATCMGR1模块会返回类似CMGR: REC READ,8613800138000,,24/03/18,10:33:1132 hello OK手机号和短信内容都出来了。测试完记得发ATCMGD1删除这条短信不然SIM卡存满后新短信会被拒收。3.3 中文短信与PDU模式的取舍如果你只是做指令控制我建议短信内容全用英文比如LED ON、LED OFF。这样文本模式完全够用代码逻辑很简单。但如果你的场景必须支持中文比如用户发“开灯”来开灯你就得切换到PDU模式。PDU模式是文本模式之外的另一种短信编码方式。它的核心思路是把短信内容转成UCS2编码然后按一个复杂的格式拼装起来。比如“开灯”的UCS2编码是5F00 706F那么发送时AT指令会变成ATCMGF0 ATCMGS长度 0011000D9168...地址编码...0008A70C5F00706F这个长度不是短信内容的字符数而是一串十六进制数据的长度计算起来很繁琐。实际项目里更常见的做法是用现成的PDU编码库或者自己写一个Unicode转UCS2的函数。不过我用过的经验是PDU模式下模块返回的数据结构更复杂解析短信内容时也要先把UCS2转回中文处理起来比文本模式麻烦不少。所以这个项目先不展开PDU细节等文本模式跑通了后续再根据需求升级。3.4 AT指令速查表调试阶段最常用的AT指令我整理成了一张表随时可以查指令作用典型返回AT测试模块是否响应OKATCGMM查询模块型号SIMCOM_SIM900AATCPIN?查询SIM卡状态CPIN: READYATCREG?查询网络注册状态CREG: 0,1ATCSQ查询信号强度CSQ: 18,0ATCMGF1切换到文本模式OKATCNMI2,1,0,0,0新短信主动上报OKATCMGRN读取第N条短信短信内容 OKATCMGS手机号发送短信返回 后输入内容ATCMGDN删除第N条短信OKATCMGD1,4删除全部短信OK当你用ATCMGS发送短信时模块返回的是而不是OK。这个像大于号一样的字符是提示符意思是“编辑器已打开请输内容”。你输入短信内容后必须发送十六进制1A作为结束符短信才会真正发送出去。如果发新的AT指令或者超时模块可能放弃发送返回ERROR。4. STM32端核心代码实现4.1 UART接收与状态判断我用的STM32F103C8T6HAL库串口1接SIM900A。串口初始化的部分就不贴了重点说接收逻辑。SIM900A传给STM32的数据不是固定长度有CMTI通知有CMGR返回的整条短信内容。最简单的方式是用串口中断逐字节接收同时用超时方式判断一帧数据是否接收完成。实现思路是串口每收到一个字节就存进缓冲区同时更新一个计数器。在主循环里不停检查这个计数器如果距离上次收到字节的时间超过了50毫秒就认为是这一帧接收完了可以开始处理。这个超时值不是拍脑袋定的因为SIM900A的返回内容在不同网络状况下会有变化50毫秒足够让一帧内容完整倒出来也不会让主循环等太久。代码框架大致如下#define RX_BUFFER_SIZE 512 uint8_t sim900A_rx_buffer[RX_BUFFER_SIZE]; uint8_t sim900A_rx_byte; volatile uint16_t rx_index 0; volatile uint32_t last_rx_tick 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { if (rx_index RX_BUFFER_SIZE) { sim900A_rx_buffer[rx_index] sim900A_rx_byte; } last_rx_tick HAL_GetTick(); HAL_UART_Receive_IT(huart1, sim900A_rx_byte, 1); } }主循环里判断一帧数据结束if (rx_index 0 (HAL_GetTick() - last_rx_tick) 50) { // 此时 sim900A_rx_buffer 中存了一整帧返回 process_sim900A_frame(sim900A_rx_buffer, rx_index); rx_index 0; }注意缓冲区大小要设足够大短信内容加上AT指令返回头信息最多可能二三百字节512字节够用。如果收到的数据超过缓冲区要处理溢出最简单是清零后重新接收否则可能把下一帧覆盖掉。4.2 解析短信内容与手机号从串口拿到的一整帧数据里最核心的是CMGR:开头的那段内容。解析步骤分成两步先找手机号再找短信正文。实际情况里CMGR:返回的第一行通常是CMGR: REC READ,8613800138000,,24/03/18,10:33:1132手机号在两个双引号之间。用C库函数strchr找第一个双引号再找下一个双引号就能把这段字符串抠出来。不过有些运营商返回的格式会多一个空格或者逗号建议先打印原始数据不要拿假设去套。短信正文在第二行。常见格式是状态行后面跟着\r\n然后正文内容最后又有\r\nOK\r\n。所以可以寻找状态行结束后的第一个非换行字符开始到OK之前结束。我写了一个简易函数做这件事void parse_sms_content(uint8_t *frame, char *phone, char *content, uint16_t *content_len) { char *p strstr((char*)frame, CMGR:); if (p NULL) return; // 提取手机号找第一个双引号 char *start strchr(p, ); if (start NULL) return; start; char *end strchr(start, ); if (end NULL) return; int phone_len (int)(end - start); if (phone_len 20) phone_len 20; memcpy(phone, start, phone_len); phone[phone_len] 0; // 提取正文找状态行后的换行 char *line_end strstr(end, \r\n); if (line_end NULL) return; line_end 2; // 跳过换行 char *ok_str strstr(line_end, OK); if (ok_str NULL) ok_str line_end strlen((char*)line_end); *content_len (uint16_t)(ok_str - line_end); if (*content_len 127) *content_len 127; memcpy(content, line_end, *content_len); content[*content_len] 0; }这里要特别提醒不要用strstr(frame, OK)去找正文结尾因为短信内容本身可能包含“OK”这两个字母比如用户发了“OK”作为指令。更好的做法是先定位到\r\n再从第二行开始截取到缓冲区末尾。实际项目中我会在读取短信时就明确要求模块返回ATCMGR...后以OK结尾但解析时仍然不能只靠OK来判断。4.3 指令识别与执行短信内容提取出来后要跟预设指令表做匹配。指令匹配建议用strncmp并且忽略大小写。C库没有现成的不区分大小写比较函数可以自己写一个或者把内容统一转成小写再比较。我的指令表是这样设计的指令文本动作LED ONGPIOB12拉高点亮LEDLED OFFGPIOB12拉低熄灭LEDSTATUS回复当前LED状态HELP回复支持的所有指令比如void handle_sms_command(const char *cmd, const char *phone) { if (strcmp(cmd, LED ON) 0) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET); send_sms(phone, LED ON OK); } else if (strcmp(cmd, LED OFF) 0) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET); send_sms(phone, LED OFF OK); } else if (strcmp(cmd, STATUS) 0) { if (is_led_on) send_sms(phone, LED IS ON); else send_sms(phone, LED IS OFF); } else { send_sms(phone, UNKNOWN CMD. TRY: LED ON, LED OFF, STATUS); } }有一点要注意收到的短信内容末尾可能带换行或者多余空格要先把首尾空白字符清理掉再做匹配否则“LED ON\n”就匹配不上。我的做法是写一个trim_string函数去掉\r、\n、空格和制表符。4.4 自动回复短信的实现回复短信是整个项目最接近“和人互动”的一步。发送流程简单但时序错了就发不出去。正确做法是这样的发送ATCMGS手机号加回车换行。等待SIM900A返回字符不要立刻发内容。发送短信内容。发送0x1A作为结束符。等待模块返回CMGS: xxx加OK。如果第2步不等直接发内容模块会把内容当成AT指令解析大概率返回ERROR。如果第4步结束符发成\r\n也不会成功。这个0x1A是CtrlZ的ASCII码必须是单字节十六进制不是字符串。串口发送函数我封装成这样void send_sms(const char *phone, const char *msg) { char cmd[128]; snprintf(cmd, sizeof(cmd), ATCMGS\%s\\r\n, phone); uart_send_string(cmd); if (wait_for_prompt(, 3000) 0) { uart_send_string(msg); uint8_t ctrl_z 0x1A; HAL_UART_Transmit(huart1, ctrl_z, 1, 1000); } }wait_for_prompt函数是轮询串口接收缓冲区里有没有字符。因为SIM900A提示符可能紧跟着ATCMGS指令的ECHO返回所以我给这个等待加超时避免卡死在等待里。这里还要注意HAL库的HAL_UART_Transmit是阻塞发送如果SIM900A回数据很慢接收中断和发送同时进行可能会丢数据。我的做法是发送指令前把接收缓冲区的索引清零发送后再开启接收中断尽量错开收发时间。4.5 防止主循环卡死状态机架构如果只是在小Demo里跑用一个阻塞式流程也能实现收到CMTI延时读短信解析回复。但这样的流程很脆弱因为SIM900A返回的时机不确定一个HAL_Delay可能就挡住了其他事件。比如你在等ATCMGS的提示符时新短信又来了串口缓冲区被新通知覆盖整个流程就乱了。所以我建议用状态机来管理短信处理流程。大致状态可以分成IDLE空闲等待CMTI通知。CMTI_RECEIVED收到新短信通知已解析出短信位置等待读取。READ_SMS发送ATCMGRN等待返回完整内容。PARSE_SMS解析短信内容执行指令。SEND_REPLY发送ATCMGS等待发送内容等待CMGS返回。DELETE_SMS删除当前短信回到IDLE。主循环里按状态机切换每次只在串口有完整帧时处理一帧没有帧时就轮询其他任务。这样做的好处是系统不会因为短信处理而阻塞其他工作比如按键扫描、传感器采集都可以同时进行。项目后期如果需要同时控制多路继电器或者上报温湿度状态机扩展起来也容易。5. 实际测试与常见问题排查5.1 全流程测试实录我调通这个项目之后的测试流程是这样的先把SIM900A接好天线、插好卡上电后等模块注册网络。然后用串口助手指令ATCREG?确认网络注册成功信号强度用ATCSQ看返回值第一个数字最好大于10我曾经遇到过CSQ: 0,0说明天线没接好或者模块在室内收不到信号。确认网络正常后我用另一台手机给设备卡发了一条LED ON。大概一两秒后串口打印出这样一行CMTI: SM,2紧接着代码自动发送ATCMGR2然后模块返回了发件人手机号和内容。解析成功后GPIOB12变成高电平LED点亮同时串口能看到发出的ATCMGS流程。最后用户手机收到回复短信内容是LED ON OK。整个过程从发短信到收到回复耗时大约3到5秒取决于短信中心的处理速度。这里有一个经验SIM900A的回复不是实时的尤其是发送短信时从发出0x1A到收到CMGS: 215有时候要等好几秒。如果超时设得太短会误判为发送失败。建议发送短信的超时时间至少10秒等待提示符的超时3秒就够了。5.2 常见问题速查表现象可能原因解决办法发送AT没有OK接线错、波特率错、模块未开机检查TXD/RXD交叉连接切换波特率确认PWRKEY已拉低指示灯常亮不闪模块未开机检查供电检查PWRKEY开机时序ATCREG?返回0或2未注册网络检查SIM卡是否插好天线是否接好确认当地有2G信号收不到CMTI没执行ATCNMI或SIM卡存满执行ATCNMI2,1,0,0,0删除旧短信读取短信返回ERROR短信位置不存在或已删除根据CMTI里的位置读取读完立即删除短信内容中文乱码文本模式不支持中文改用PDU模式或使用英文/拼音指令发短信时卡在等待之前AT指令未发送完成发送ATCMGS前清空串口缓冲确认模块返回提示符再发内容模块自动重启电源供电不足换2A电源增加大电容高增益天线干扰单片机天线离MCU太近拉长天线走线增加屏蔽或拉开距离排查问题时尽量用串口助手直连SIM900A确认模块本身没问题再接到STM32上。我最常用的排查步骤是先AT再查SIM卡再查网络注册再设短信模式再手测收发一条短信。哪一步出了问题就停在哪一步基本能定位到硬件还是软件。5.3 我踩过的一些坑第一个坑是供电。项目刚开始我用的是STM32板的USB供电额外引了5V给SIM900A结果模块一注册网络或者发短信电流一拉大电压跌落STM32直接复位。后来我把SIM900A单独接上5V/3A电源适配器并在模块近端加了1000uF电容问题才算解决。第二个坑是不等提示符。第一次写发送短信代码时我在发完ATCMGS后直接发短信内容结果模块始终返回ERROR。查了好久才发现SIM900A必须在收到字符后才会进入内容输入模式你发得太快它还没准备好内容就丢了。解决办法很简单加一个等待提示符的程序而且等待前要清空接收缓冲区防止上一次残留的返回数据干扰判断。第三个坑是CMTI和CMGR返回的内容同时进入缓冲区。手机如果连续发两条短信SIM900A可能会在读取第一条的过程中又上报第二条的通知导致解析错位。我的处理方式是在状态机里加了一个互斥标志读取短信时新来的CMTI先缓存等当前流程完成再处理。如果不用状态机至少要在代码里加一个“busy”标志避免串口数据交叉把缓冲区冲乱。第四个坑是中文回复。一开始我想让设备回复“开灯成功”结果模块回复出去是乱码因为文本模式根本不支持中文。后来我把所有指令和回复都改成英文缩写比如LED ON OK、UNKNOWN CMD省掉了PDU编码的麻烦。如果确实需要中文再走PDU模式但那是另一个工程量。最后再分享一个小技巧。SIM900A的启动时间很慢从开机到能执行AT指令短则2秒长则7到8秒。如果STM32刚刚上电就立刻初始化串口并发送AT指令模块可能还没准备好初始化就会失败。我的做法是在程序启动后先延时3秒再开始和模块通信。甚至在调ATCNMI设置的时候如果首次失败隔1秒重试一次直到成功为止。这样做看起来不够优雅但在实际不稳定的通信环境里非常管用。本文还有配套的精品资源点击获取
返回列表