拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32与SIM800C实战:AT命令、串口异步接收与GPRS应用开发

简介面向嵌入式与物联网开发者STM32F103驱动SIM800C模块的HAL库工程适合需要快速实现短信收发、语音通话、GPRS数据通信和蓝牙交互的场景代码基于UART串口通信完整覆盖AT指令的构建、发送与响应解析可直接适配STM32F1系列控制器。资源共1680个文件主要是870个.h头文件与730个.c源文件并包含uvprojx/uvoptx工程配置、启动文件、hex固件及调试配置等压缩包仅10.96MB目录组织清晰方便对照阅读和二次开发。目前已有803人学习/下载适合正在学习HAL库、串口中断、AT指令集或物联网无线通信终端开发的工程师与学生作为参考。工程内不仅实现了短信收发ATCMGF/CMGS/CMGR、拨号与挂断ATD/ATH、GPRS网络接入及TCP/IP收发还提供蓝牙配对连接与数据透传示例同时包含电源管理、超时重试和异常恢复等实用处理代码注释详尽是构建STM32通信终端的良好起点。1. STM32F103 最小系统 SIM800C先认清它是 AT 语音机而不是传感器用 STM32F103 最小系统板点 OLED、读 DHT11几行 HAL 代码就能收工轮到 SIM800C 时这套经验会失效。SIM800C 内部跑着完整的 GSM/GPRS 协议栈和蓝牙协议栈主控不读寄存器只通过串口一行行发 AT 文本命令。更反直觉的是模块会主动上报事件新短信到达时随时插入一条CMTI打乱原本线性的收发流程。下面按落地顺序展开先讲硬件与电平再搭 HAL 串口的异步接收框架然后短信电话、GPRS TCP 会话最后落在蓝牙调试与整机自检上。适合手里只有一块 STM32F103 最小系统板、想做短信告警或低吞吐量数据上报的工程师也适合从标准库 V3.5 迁移到 HAL 后想重建串口接收模型的人。核心认知只有一个SIM800C 不是外设是一台等文本命令的“AT 语音机”。2. 硬件接线与 HAL 串口异步接收框架SIM800C 的响应永远不定长2.1 引脚接线与电平边界先给出一张最小接线表主串口用 USART2把 PA2/PA3 留给模块避免和调试打印的 USART1 抢线。STM32F103 引脚方向SIM800C 引脚说明PA2USART2_TX→SIM800_RXDF103 输出 3.3VSIM800C 是 2.8V TTL 逻辑建议串 510Ω1kΩ 电阻PA3USART2_RX←SIM800_TXD2.8V 输出对 F103 的输入高电平阈值 2.31V 有余量可直连GND—GND两端必须共地这是所有怪问题的头号来源5V/2A 电源→VBAT 或 5V 输入GSM 突发发射时模块瞬时电流接近 2A不能用 3.3V LDO 供电PB0推挽输出→PWRKEY多数模块上电即开机也有模块需要拉低 PWRKEY 约 500ms 完成开机电源是所有新手最容易栽的地方。STMF103 最小系统板上的 AMS1117-3.3V 只能撑几十毫安级别的负载SIM800C 一入网发射电压就被拉垮表现是ATCSQ永远返回 99或者模块反复重启。常见做法是模块用独立的 5V/2A 适配器或锂电池供电主控板单独供电只在 GND 处单点相接。信号线暂且不管先把地接好、把电源喂饱再谈 AT 命令。还有一个细节SIM800C 的串口电平是 2.8V不是 5V。F103 的 TX 输出 3.3V高电平略超规格但绝大多数模块能接受稳妥做法是在 TX 线上串一个 510Ω 到 1kΩ 的电阻限流如果项目要过认证或批量生产建议加 TXS0102 这类电平转换芯片。SIM800_TXD 到 PA3 这段可以直接连因为 2.8V 高于 F103 识别高电平的最低 2.31V可靠性足够。2.2 CubeMX 串口配置115200 8N1 开中断在 STM32CubeMX 里选 USART2置为 Asynchronous波特率 1152008 位数据、无校验、1 停止位。NVIC 设置里勾选 USART2 global interrupt。CubeMX 生成的代码不需要大改只需要在main()里确认调用了MX_USART2_UART_Init()。一个容易被忽略的点SIM800C 出厂波特率不一定是 115200很多模块默认 9600 或 115200取决于固件版本和出厂配置。上电后先用 USB-TTL 转串口单独连模块一个个波特率试过去看到AT回OK才算找到正确速率。正常情况下模块上电后会有几秒初始化等板载 NETLIGHT 灯开始慢闪再发命令。F1 系列里 F103C8T6 只有 3 个 USARTF105/F107 有 5 个这套工程只占一个串口换型号时只需改串口句柄和中断回调应用层代码不用动。2.3 环形缓冲区HAL 串口逐字节接收的正确姿势SIM800C 响应不定长ATCMGL可以一次回几百字节新短信 URC 只有十几个字节阻塞式HAL_UART_Receive等固定长度必然卡死。所以串口接收层要用环形缓冲区配合中断每收到一个字节先存起来主循环再按行取走。// sim_rx.cSIM800C 串口接收环形缓冲 #define SIM_RX_SIZE 512 static uint8_t sim_rx_buf[SIM_RX_SIZE]; static volatile uint16_t sim_rx_head 0; static volatile uint16_t sim_rx_tail 0; static uint8_t sim_rx_byte; // 中断回调逐字节写入环形缓冲 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART2) { sim_rx_buf[sim_rx_head] sim_rx_byte; sim_rx_head (sim_rx_head 1) % SIM_RX_SIZE; HAL_UART_Receive_IT(huart2, sim_rx_byte, 1); } } // 主循环或任务里取一行完整响应返回行长度无整行返回 0 int sim_get_line(char *line, int max_len) { int len 0; while (sim_rx_head ! sim_rx_tail) { uint8_t c sim_rx_buf[sim_rx_tail]; sim_rx_tail (sim_rx_tail 1) % SIM_RX_SIZE; if (c \n len 0) { line[len] \0; return len; } if (len max_len - 1) line[len] c; } return 0; }这段代码的核心是HAL_UART_Receive_IT的一次性挂接和重新挂接。每次收到一个字节进入中断回调把字节写入环形缓冲后立刻再调用一次HAL_UART_Receive_IT准备收下一个字节。sim_rx_head在中断里写sim_rx_tail在主循环里读两者都在中断和主循环之间共享必须加volatile。缓冲区 512 字节对 GPRS 场景足够如果频繁用ATCMGLALL拉全量短信建议加大到 2048并在入缓冲时加满判断。sim_get_line以\n作为行结束符自动忽略\r这样应用层拿到的就是OK、ERROR、CMTI: SM,3这类干净文本。注意 AT 命令的响应不一定只有一行比如ATCLCC会回多行调用方要做“收集所有行直到出现 OK”的逻辑而不是取到一行就返回。提示如果工程里跑了 FreeRTOS这套环形缓冲不用改把sim_get_line和后续状态解析放进独立的 SIM800C 任务即可裸机工程就在主循环里轮询。2.4 异步事件与状态机CMTI 为什么不请自来SIM800C 会随时产生 URC即模块主动上报的消息。最常见的场景是开启新短信指示后来短信时模块会在任何命令间隙插入一行CMTI: SM,3。如果此时程序正堵在HAL_UART_Receive等一个固定长度的响应这行 URC 要么被吞掉要么被当成响应的一部分短信永远读不出来。所以应用层不能只写“发一条命令等一条响应”的线性代码要维护一个状态机发送命令前记录当前期望的关键字收到行后先判断是不是 URC是 URC 就走异步处理分支不是 URC 再和当前期望匹配。后面第 3 章会给出行解析函数这里先记住一个原则任何等待响应的循环里都要能跳过非目标行不能因为一行CMTI就认为命令失败了。3. 短信与电话文本模式、呼叫命令与一个能用的发送函数3.1 开机自检命令链每块新模块上电后先按顺序跑一遍下面这张表。这五条命令能覆盖 90% 的“模块不工作”问题。命令典型返回说明ATOK模块活着、串口波特率正确ATE0OK关闭命令回显让后续解析更干净ATCPIN?CPIN: READYSIM 卡未插或方向反了会返回 ERRORATCSQCSQ: 22,0第一个数是信号等级 0~3199 表示天线未接或射频异常ATCREG?CREG: 0,1第二个数 1 表示已注册本地网3 表示被拒绝ATCSQ返回的 22 不是百分比而是一个对数映射等级。22 以上基本是满信号12 以下就要检查天线和供电。如果ATCSQ一直返回 99别急着查软件先看天线座是否拧紧、模块的射频天线是否被金属外壳罩住。SIM 卡接触不良也常见表现为ATCPIN?返回 ERROR重新插拔能解决大半。3.2 文本模式短信收发0x1A 才是正文结束符文本模式下收发短信有三条规则。第一ATCMGF1切到文本模式否则ATCMGS会走 PDU 模式命令格式完全不同。第二ATCMGS号码输入回车后模块会回提示符这个提示符不带回车换行所以用sim_get_line按行等待永远等不到。第三短信正文必须以 0x1AASCII 的 Ctrl-Z结束模块收到 0x1A 才真正提交短信然后回OK或ERROR。下面这段代码是短信发送的最小实现依赖前文的sim_get_line和环形缓冲// 等待模块发出 提示符该字符不带回车换行必须按字节等 static int sim_wait_prompt(uint32_t timeout_ms) { uint32_t start HAL_GetTick(); while (HAL_GetTick() - start timeout_ms) { if (sim_rx_head ! sim_rx_tail) { uint8_t c sim_rx_buf[sim_rx_tail]; sim_rx_tail (sim_rx_tail 1) % SIM_RX_SIZE; if (c ) return 0; } } return -1; } // 发送命令并等待响应子串遇到 ERROR 提前失败 static int sim_expect(const char *cmd, const char *resp, uint32_t timeout_ms) { char line[160]; if (cmd ! NULL cmd[0] ! \0) { HAL_UART_Transmit(huart2, (uint8_t *)cmd, strlen(cmd), 200); HAL_UART_Transmit(huart2, (uint8_t *)\r, 1, 200); } uint32_t start HAL_GetTick(); while (HAL_GetTick() - start timeout_ms) { if (sim_get_line(line, sizeof(line)) 0) { if (strstr(line, resp) ! NULL) return 0; if (strstr(line, ERROR) ! NULL) return -1; } } return -2; } // 发送一条文本短信 static int sim_send_sms(const char *number, const char *text) { char cmd[64]; if (sim_expect(ATCMGF1, OK, 1000) ! 0) return -1; // 文本模式切换失败 snprintf(cmd, sizeof(cmd), ATCMGS\%s\, number); HAL_UART_Transmit(huart2, (uint8_t *)cmd, strlen(cmd), 200); HAL_UART_Transmit(huart2, (uint8_t *)\r, 1, 200); if (sim_wait_prompt(5000) ! 0) return -2; // 模块没回 HAL_UART_Transmit(huart2, (uint8_t *)text, strlen(text), 300); uint8_t ctrl_z 0x1A; // 短信正文结束符 HAL_UART_Transmit(huart2, ctrl_z, 1, 200); if (sim_expect(, OK, 10000) ! 0) return -3; // 短信提交失败 return 0; }代码里最值得注意的两个细节sim_wait_prompt是字节级读取它不经过sim_get_line因为后面没有换行sim_expect的第一个参数如果传空串就只等待不发送这样能复用同一套超时和行解析逻辑。0x1A是 ASCII 的 Ctrl-Z不要用\r或\n替代否则短信正文会停留在编辑态模块既不回 OK 也不回 ERROR。短信接收方面读取用ATCMGLALL或ATCMGRindex删除用ATCMGDindex。主动接收新短信必须先开ATCNMI2,1开启后模块会在新短信到达时上报CMTI: SM,index。程序拿到 index 后再用ATCMGRindex取内容。读取后要及时删除SIM 卡存储区满了之后新短信会写不进去。下面是 URC 行解析函数放在主循环的取行逻辑里// 收到一行识别 URC 并返回事件类型 int sim_process_line(const char *line) { if (strncmp(line, CMTI:, 6) 0) { int index 0; sscanf(line, CMTI: \SM\,%d, index); new_sms_index index; // 通知主状态机去读短信 return 1; } if (strcmp(line, RING) 0) { // 来电主程序执行 ATCLCC 查询主叫号码 return 2; } return 0; }用strncmp而不是strstr判断前缀是为了避免普通响应里恰好包含同样文本时误触发。sscanf的格式串里%d前要写清楚引号和逗号解析失败时返回 0。3.3 电话功能ATD 后面的分号不能省打电话的命令本身很简单ATD13800138000;。分号必须跟在号码最后面表示这是语音呼叫没有分号模块会按数据呼叫处理行为完全不同。发完ATD...后模块回OK之后主叫方接听前不会再有额外状态上报。挂断用ATH。来电检测需要先执行ATCLIP1开启主叫号码显示来电时模块上报CLIP: 13800138000,129,,0同时上报一行RING。如果在通话中想查询当前呼叫状态ATCLCC会返回一行或多行CLCC: 1,0,0,0,0,13800138000,129其中第二个字段是呼叫方向第三个字段是呼叫状态。工程上外呼一般由按键或 Modbus 寄存器触发主控发完ATD后起一个软件定时器到时间就发ATH不要用阻塞延时等通话结束。3.4 状态机不要写成 sleep短信、电话和 GPRS 共用同一个串口任何长时间 sleep 都会造成 URC 丢失。常见做法是维护一个状态变量和一张“当前等待关键字”的表主循环每轮只做三件事从环形缓冲取行、用sim_process_line判断是不是 URC、不是则和当前期望关键字做匹配。FreeRTOS 移植过来也一样把这三步放进一个独立任务其它任务通过队列发命令索引。4. GPRS 数据业务APN 配置、TCP 会话与异步行处理4.1 APN 与 PDP 上下文先激活再接服务器GPRS 的完整会话不是一条命令而是一组命令。先配置 PDP 上下文再附着网络再激活上下文最后才能建 TCP 连接。国内卡常见默认 APN中国移动cmnet、联通3gnet、电信ctnet。企业卡和物联网卡要用卡商提供的专用 APN用户名密码可能也要填。命令典型返回说明ATCGDCONT1,IP,cmnetOK配置 PDP 上下文APN 拼错会在这里埋雷ATCGATT1OK附着 GPRS 网络ATCGACT1,1OK激活 PDP 上下文失败多为 APN 或卡没开数据套餐ATCIPSTARTTCP,120.55.8.9,8080CONNECT OK建立 TCP 连接ATCIPSEND进入发送窗口等待提示符业务数据 0x1ASEND OK数据帧已交给远端服务器ATCIPCLOSECLOSE OK关闭本次活动连接ATCIPSHUTSHUT OK关闭移动场景释放 PDP激活失败先查ATCGACT?看当前上下文状态返回 0 说明没激活成功。另一个容易忽略的点整个会话只有一个活动 socketSIM800C 的 TCP/IP 应用同一时刻只能跑一条 TCP 连接。业务设计必须串行发完一帧等SEND OK再发下一帧不能开两个线程同时发。4.2 发一帧 TCP 数据的完整函数下面这段代码直接复用第 3 章的sim_expect和sim_wait_prompt完成一次完整的 TCP 数据发送。// sim_tcp.c建链、发一帧数据、等待确认 static int sim_tcp_send(const char *host, uint16_t port, const uint8_t *data, uint16_t len) { char cmd[96]; snprintf(cmd, sizeof(cmd), ATCIPSTART\TCP\,\%s\,%u, host, port); if (sim_expect(cmd, CONNECT OK, 15000) ! 0) return -1; // 建链失败检查 IP 端口和网络 if (sim_wait_prompt(2000) ! 0) // CIPSEND 的 也没有换行 return -2; HAL_UART_Transmit(huart2, data, len, 500); uint8_t end 0x1A; // 通知模块这一帧结束 HAL_UART_Transmit(huart2, end, 1, 200); if (sim_expect(, SEND OK, 10000) ! 0) return -3; // 发送失败需要重建连接 return 0; }这段代码的逻辑说明sim_expect(cmd, CONNECT OK, 15000)负责发送建链命令超时 15 秒是因为 GPRS 建链受网络影响很大可能要等几秒才回CONNECT OK如果中途收到ERRORsim_expect会提前返回。ATCIPSEND的提示符同样没有换行所以复用sim_wait_prompt。数据长度建议控制在 1024 字节以内超过 MTU 时模块虽然能内部封包但长包失败率会明显上升实际工程里把业务帧拆成小块更稳。发送后如果等到ERROR不要直接重发先把连接关掉执行ATCIPCLOSE必要时再执行ATCIPSHUT重建整个 TCP/IP 场景。sim_expect(, SEND OK, 10000)在等待期间如果收到CMTI这类 URC 行会正常跳过继续等不会误判失败。等待响应的循环里能忽略非目标行这是第 2 章说的异步模型带来的直接好处。如果CIPSTART里写的是域名而不是 IP部分固件可能解析失败。遇到这种情况先换成对端公网 IP 测试能连上说明域名解析链路有问题再查ATCDNSCFG的 DNS 配置。4.3 下行数据 IPD 的粘包处理TCP 对端主动下发的数据模块会以IPD,len:data的行格式上报例如IPD,12:hello world!。解析时先找IPD,前缀取出逗号后面的长度再从数据区切出对应字节。注意 payload 可能很长模块会把数据和后续 URC 拼接在一起不能假设“一行就是一个完整包”。处理粘包的办法是维护一个累积区先解析出 len如果当前行剩余字节不够 len就把剩余数据暂存继续等环形缓冲里的后续字节直到凑满 len。解析完成后检查缓冲区里是否还有剩余内容再做下一轮。这块逻辑和串口协议解析是同一套路建议独立成sim_ipd_handler不要在sim_get_line里直接处理否则行边界和包边界会互相污染。5. SIM800C 蓝牙通信的验证技巧ATCLAC、SPP 会话与整机自检5.1 先确认固件支持哪些命令SIM800C 的蓝牙命令在不同固件上子集不完全一样验证前先执行ATCLAC模块会把支持的全部命令列出来。看到以BT开头的条目就说明固件带蓝牙协议栈。这一步值得认真做因为很多模块出厂固件精简过蓝牙功能凭记忆敲命令只会得到一片 ERROR。5.2 打开蓝牙并建立 SPP 会话蓝牙在 SIM800C 上默认为关闭状态需要先执行ATBTPOWER1打开。如果这条命令返回 ERROR去ATCLAC的输出里找蓝牙相关的实际命令名。打开后手机安装蓝牙串口助手按模块标签上的 PIN 配对常见是 0000 或 1234。配对成功后手机与模块之间的 SPP 通道就建立了。这里要分清架构SPP 数据进入模块后是否需要主 UART 转发取决于固件对蓝牙数据的路由方式。我一般把蓝牙当近场调试口而不是业务主链路STM32 继续走主 UART 发 AT手机端发的内容被当作远端调试指令注入模块蓝牙是否透传、透传模式下怎么退出以手册为准。不要同时从蓝牙和主 UART 发起 AT两个口抢命令会让模块响应变得不可预期。5.3 整机自检序列一小时排除硬件问题拿到新模块后按顺序执行下面这张表能快速判断问题出在哪一层。步骤命令预期失败先查1ATOK串口交叉、波特率2ATE0OK—3ATCPIN?CPIN: READYSIM 卡方向、金属触点4ATCSQ信号等级 ≥ 12天线、供电99 是天线或射频异常5ATCREG?注册成功欠费、卡未开通网络6ATCMGF1OK固件异常7ATCIPSTARTCONNECT OK对端 IP 端口、APN 配置最后补一个具体技巧如果调试时用 DAP 下载失败先把 SIM800C 供电断开再确认最小系统板的 BOOT0 处于 Flash 启动档位。大多数情况下是模块瞬态电流把共地电源拖垮而不是程序问题另一类是把 BOOT0 拨到了系统存储区启动重新拨回 Flash 档即可。自检时每一条命令都盯两秒回显别同时挂着串口助手和单片机抢发否则你看到的乱码多半来自自己。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门