简介:本资源是一套基于STM32F103的SIM800C模块HAL库驱动工程,面向嵌入式开发者和物联网学习者,覆盖短信收发、拨打接听电话、GPRS联网及蓝牙透传四个核心功能。工程以HAL库为标准,包含UART初始化与中断接收、AT指令封装与响应解析、GPRS TCP/IP链路建立、蓝牙配对连接等完整代码框架,适合需要快速集成2G通信或学习AT指令控制的开发场景。包内共有1680个文件,其中以C语言源码为主,含870个.h头文件、730个.c源文件,以及uvprojx、uvoptx、scvd、hex等Keil工程与烧录文件,整体压缩包约10.96MB,目录按模块划分便于检索。该资源已有803人学习下载,源码注释较详细,可直接在STM32F1系列上移植或二次开发。
1. 为什么是SIM800C:从STM32F103 UART到可上线的通信链路
做物联网设备时最常遇到的情况是:现场没有WiFi,NB-IoT模组成本又压不下来,LoRa还得自己维护网关。翻出库存里的SIM800C,一块STM32F103最小系统板加上一张物联网卡,就能把短信、语音、GPRS和蓝牙四件事一次做完,这也是很多工业采集器、车载终端、远程报警器的原型方案。这个项目就是基于STM32F103的HAL库,对SIM800C做完整驱动,覆盖短信收发、拨打与接听电话、GPRS建链、蓝牙SPP通信。很多人卡住的点反而不是AT指令,而是UART电平、供电和上电时序——这三件事没闹明白,模块永远只回乱码或干脆没反应。本文按硬件连接、指令交互、GPRS建链、蓝牙与排错的顺序拆开讲,所有代码都是可抄作业的HAL库写法。
2. HAL库UART初始化与SIM800C硬件连接:电平、供电与上电时序
SIM800C本质是一个带完整协议栈的串口外设,所有业务都通过UART交换AT指令完成。所以驱动它的第一步不是写短信函数,而是把物理链路和HAL库串口配置做扎实。很多移植案例跑不起来,问题不在代码,而在模块供电瞬间掉压导致反复重启,或者STM32F103与模块之间的电平定义没对齐。
2.1 引脚连接与电平匹配方案
SIM800C的UART标准电平不是3.3V而是2.8V CMOS电平,但实际上可以直接和STM32F103的3.3V UART相连。STM32F103的GPIO是5V容忍输入,接收模块TXD输出的高电平接近VBAT(约4V)时不会烧引脚;反过来STM32F103发送给模块RXD的3.3V高电平,也落在SIM800C的VIH范围(约0.7×VBAT)之上。
直接连是能工作的,但工程上我建议加一级串联电阻做保护。下面是三种常见接法对比:
| 方案 | 接法 | 可靠性 | 适用场景 |
|---|---|---|---|
| 直连 | 同电压域直接交叉连接 | 中 | 手工验证、临时测试 |
| 串联电阻 | STM32 TXD串1K电阻到SIM800C RXD | 高 | 量产主板,防插拔损伤 |
| 电平转换 | TXS0108或三极管双向转换 | 最高 | SIM800C独立供电、共地不牢的场合 |
SIM800C的RXD对毛刺比较敏感,串电阻能抑制STM32F103引脚切换时的过冲。推荐方案是TXD串1K电阻,RXD串220欧电阻,两边共地。另外注意UART是交叉连接:STM32F103的PA2(USART2_TX)接SIM800C的RXD,PA3(USART2_RX)接SIM800C的TXD。使用STM32CubeMX生成工程时,在Pinout中将USART2配置为异步模式即可,PA2和PA3会自动分配。
2.2 PWRKEY时序与供电设计
SIM800C不是上电就立刻工作的。模块的PWRKEY引脚需要被拉低至少1.2秒才能开机,典型做法是用一个NPN三极管或NMOS管受控于STM32F103的一个普通GPIO。直接拿MCU引脚拉PWRKEY在很多板子上也能用,但模块开机瞬间电流变化大,推荐三极管方案更稳。
供电是SIM800C项目里最容易翻车的地方。模块在GPRS发射时峰值电流可以到2A,如果稳压器输出能力不足,VBAT会瞬间跌落,模块直接掉电重启。常见的AMS1117-3.3不行,至少要用能输出2A以上的降压芯片,比如MP2303或MIC29302,输入电容建议470uF电解电容加0.1uF陶瓷电容并联。
上电时序的完整顺序是:先给VBAT上电,等待VBAT稳定到3.4V-4.4V之后,再把PWRKEY拉低1200ms释放。参考时序代码如下:
void sim800c_power_on(void) { HAL_GPIO_WritePin(PWRKEY_PORT, PWRKEY_PIN, GPIO_PIN_RESET); // 拉低PWRKEY HAL_Delay(1500); // 保持1.5秒,超过手册要求的1.2秒 HAL_GPIO_WritePin(PWRKEY_PORT, PWRKEY_PIN, GPIO_PIN_SET); // 释放,模块开始开机 HAL_Delay(3000); // 等待模块软件启动 }逻辑说明:拉低PWRKEY的动作在SIM800C内部被识别为开机触发信号,1.2秒是下限,很多模组厂家出厂资料里建议拉1秒到2秒之间。代码里用1.5秒留出余量。释放后模块内部固件开始初始化,此时不应立刻发AT指令,等3秒更稳妥。
2.3 中断接收:从单字节回调到环形缓冲区
AT指令的响应是变长字符串,无法用固定长度的HAL_UART_Receive完成解析。推荐做法是用HAL_UART_Receive_IT每次接收一个字节,在回调函数里把数据存入缓冲区,同时启用串口空闲中断(IDLE)来感知一条完整响应的结束。
启用空闲中断的常用做法是在初始化后追加一行__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE);,并在中断回调中判断__HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE)。因为HAL库默认不处理空闲中断,必须自己在USART2_IRQHandler里做一次清标志位动作:
void USART2_IRQHandler(void) { HAL_UART_IRQHandler(&huart2); // HAL库处理接收中断 if (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE) != RESET) { __HAL_UART_CLEAR_IDLEFLAG(&huart2); // 清空闲标志 rx_line_done = 1; // 通知解析层一条数据已经收完 } }逻辑说明:空闲中断触发条件是串口在一帧数据结束后持续一个字节时间内没有新数据,这正好对应SIM800C的AT响应结束。rx_line_done标志位供主循环查询,避免在中断里做字符串解析拖慢系统。缓冲区建议设计成环形队列,因为GPRS收到的TCP数据可能长达成百上千字节,单字节固定数组很容易覆盖。
3. AT指令控制层:短信收发、拨号挂断与响应解析
硬件链路通了之后,模拟器里所做的就是构造字符串、发送、等待响应、解析响应。AT指令本身不复杂,但指令间的时序、超时和响应分类决定整个驱动是否可靠。
3.1 发送一条AT命令的完整流程:超时、重试与响应分类
SIM800C的响应分为三种:OK表示成功、ERROR表示失败、+XXX:开头的事件上报属于异步通知。发送指令前要确保模块处于命令态而非数据态,GPRS透传之后尤其要注意。标准做法是维护一张指令等待表,每次发送后等待匹配的响应字符串,超时则重试。
uint8_t sim800c_send_at(const char *cmd, const char *expect, uint32_t timeout_ms) { uint8_t retry = 3; while (retry--) { sim800c_uart_send_string(cmd); // 发送AT指令字符串 uint32_t tick = HAL_GetTick(); while (HAL_GetTick() - tick < timeout_ms) { if (strstr(rx_buf, expect) != NULL) // 在接收缓冲区里查找期望响应 { return 1; } if (strstr(rx_buf, "ERROR") != NULL) { break; } } HAL_Delay(100); } return 0; }逻辑说明:函数发送指令后轮询接收缓冲区,用strstr匹配期望字符串,遇到ERROR直接跳出进入重试。这个函数是所有上层功能的基础。参数里expect不像"OK"这样简单,发送短信时要匹配">"提示符而不是OK。
重试次数设为3,间隔100ms,这是考虑到SIM800C在信号较差时注册网络和指令响应都会变慢。最好不要无限重试,否则GPRS断网恢复时会把指令队列阻塞住。
3.2 TEXT模式短信:CMGS的提示符陷阱
接收短信用TEXT模式,先AT+CMGF=1切到TEXT,再AT+CMGL="ALL"读取全部短信。发送短信时需要特别注意AT+CMGS的响应:模块返回的不是OK,而是>提示符,表示等待输入短信内容,最后必须以十六进制的0x1A(Ctrl+Z)作为结束符。
void sim800c_send_sms(const char *phone, const char *text) { char cmd[128]; snprintf(cmd, sizeof(cmd), "AT+CMGS=\"%s\"\r\n", phone); sim800c_uart_send_string(cmd); // 发送电话号码 wait_for_char('>'); // 等待模块输出'>'提示符 sim800c_uart_send_string(text); // 发送短信正文(ASCII字符) sim800c_uart_send_byte(0x1A); // 发送结束符Ctrl+Z wait_for_string("OK"); // 等待发送成功 }逻辑说明:AT+CMGS后跟目标号码,短信正文不能带\r\n换行,否则会提前结束输入。0x1A是GSM协议里的短信结束标志,不能直接用\r替代。TEXT模式只支持ASCII和GSM基础字符集,中文短信需要切到PDU模式并把中文转成UCS2编码,那是另一套流程,量产产品建议直接用PDU模式统一处理。
短信接收解析相对简单:AT+CMGL="ALL"返回的每一条以+CMGL:开头,后面依次是序号、状态、号码、日期和正文,用逗号分割字符串即可。需要注意的是读取后主动用AT+CMGD=<index>删除已读短信,防止SIM卡存储区写满。
3.3 拨号、接听与挂断:从ATD到+CLCC
拨打电话用ATD<号码>;,注意结尾分号表示语音呼叫而不是数据呼叫。挂断用ATH。接听则依赖模块主动上报的RING通知,STM32F103在解析到RING字符串后发送ATA接听。
常见的误用是ATD后面忘记加分号,此时SIM800C认为是要建立数据连接,行为完全不是预期的。监听来电状态可以轮询AT+CLCC主动查询当前呼叫列表,返回+CLCC: 1,0,0,0,0,"号码",129这样的格式,第二个字段0表示呼叫状态处于拨号中。高位的是第几个字段代表什么,建议参考模块手册的呼叫状态表。
if (strstr(rx_buf, "RING") != NULL) { sim800c_send_at("ATA\r\n", "OK", 1000); // 自动接听 }逻辑说明:处理来电的关键是识别RING前导符。RING每5秒上报一次,如果第一次没接起来,第二次RING到达时要避免重复发送ATA,需要在代码里加一个呼叫状态标志位。
4. GPRS拨号与TCP链路:从APN配置到HTTP GET
GPRS是SIM800C系列最有价值的功能,它让设备可以走TCP/IP协议上报数据到服务器。整个流程比短信复杂的地方在于:需要先激活移动数据场景,拿到本地IP,才能建立TCP连接。
4.1 GPRS承载流程:CSTT、CIICR、CIFSR
GPRS的建立顺序是固定的,缺一步后面都会失败。这里用一张表把链路建立阶段和典型返回列出:
| 阶段 | AT指令 | 作用 | 典型返回 |
|---|---|---|---|
| 设置APN | AT+CSTT="CMNET" | 配置接入点 | OK |
| 激活移动场景 | AT+CIICR | 拨号附着GPRS网络 | OK |
| 获取本地IP | AT+CIFSR | 返回模块分配的IP | 10.x.x.x |
| 建立TCP连接 | AT+CIPSTART="TCP","x.x.x.x",port | 连接服务器 | CONNECT OK |
| 发送数据 | AT+CIPSEND | 进入数据输入态 | > |
AT+CSTT的APN必须和SIM卡运营商匹配,移动物联网卡通常是CMNET,联通是UNINET,电信则是CTNET。如果APN配置错了,AT+CIICR会一直返回ERROR或PDP DEACT。AT+CIFSR返回的IP通常是运营商的私网地址,这是正常的,说明PDP上下文已激活。
4.2 非透传TCP:CIPSTART + CIPSEND + 0x1A
GPRS发送数据和发送短信一样,AT+CIPSEND之后模块返回>,输入数据完毕后再用十六进制0x1A结束发送,并等待SEND OK返回。下面这段代码展示了一次完整TCP发送:
uint8_t sim800c_tcp_send(const char *data, uint16_t len) { if (sim800c_send_at("AT+CIPSEND\r\n", ">", 2000) == 0) { return 0; // 模块没返回'>',可能TCP连接已断开 } sim800c_uart_send_buffer((uint8_t *)data, len); // 发送应用数据 sim800c_uart_send_byte(0x1A); // 数据结束符 return sim800c_send_at("", "SEND OK", 3000); // 等待模块确认发送完成 }逻辑说明:这段代码里的sim800c_send_at("", "SEND OK", 3000)第二个参数不是发空指令,此时发空字符串只是让其进入轮询模式。实际业务中SEND OK其实不需要发任何指令,直接把上一次的收发留到接收处理逻辑里即可。更稳妥的做法是单独等待SEND OK字符串,而不是复用send_at。
TCP连接的最大问题是链路会因信号弱或服务器超时断开,断开后模块会有CLOSED通知。驱动里必须在收到CLOSED后自动走一遍AT+CIPSTART重新建链,否则后续AT+CIPSEND永远返回ERROR。
4.3 用裸HTTP报文做GET,避开HTTPCLIENT的参数歧义
SIM800C不同固件对AT+HTTPCLIENT的参数定义不一致,为了兼容性,更可控的做法是直接用TCP裸报文发HTTP请求。连接上服务器的80端口后,把HTTP报文作为AT+CIPSEND的数据发出去,服务器返回的HTTP/1.1 200 OK会原样从串口回来。
char http_get[] = "GET /api/device/status?sn=1001 HTTP/1.1\r\n" "Host: 120.25.xxx.xxx\r\n" "Connection: close\r\n" "\r\n"; sim800c_tcp_send(http_get, strlen(http_get));逻辑说明:报文的Host头必须填写服务器IP或域名,Connection: close让服务器在响应后主动断开TCP,省去一条AT+CIPCLOSE指令。返回的数据里会包含HTTP头部和正文,嵌入式端解析时从\r\n\r\n之后取JSON或纯文本即可。
完整的HTTP响应可能超过2KB,而SIM800C的UART接收缓冲区通常只有几KB。因此在实际项目里要把缓冲区做大(至少4KB),并且在解析时不要一次性strstr整包,而是用一个状态标记记录是否已经找到头部结束符,再逐步读取正文。这就是为什么前面章节用环形缓冲区而不是定长数组的原因。
5. 蓝牙SPP、SYNC指示与异常恢复
5.1 SIM800C蓝牙与HC-05的差异
SIM800C内置经典蓝牙3.0,和常见的HC-05模块不同,它不只是一个串口透传外设,而是通过AT+BTPOWER、AT+BTSTATUS等指令管理蓝牙协议栈。调试时最容易踩的坑是:插上HC-05用手机能直接搜到,而SIM800C的蓝牙默认是关闭的,必须先发送AT+BTPOWER=1打开蓝牙电源,再用手机发起配对。配对完成后,模块与手机之间的数据通道依然走的是SPP协议,手机端需要专门支持蓝牙串口的调试工具才能收发数据。
蓝牙数据收发和UART是同一通道:手机发来的数据会直接出现在SIM800C的串口输出中,因此RING解析逻辑要兼容蓝牙数据帧。如果之前通信的波特率参数被改过,蓝牙连上了但手机端收不到数据,优先检查模块的UART波特率设置,而不是蓝牙连接状态。不同批次固件指令差异较大,代码中建议用宏统一管理蓝牙开关指令,方便针对固件版本调整。
5.2 SYNC状态灯、PWRKEY异常与看门狗兜底
判断模块是否真正进入工作状态,不能只靠发送AT等OK,因为偶尔一次OK不代表模块已经注册上网络。SYNC引脚连接一个LED可以通过闪烁频率判断模块状态,这是现场排错最直接的手段:
| SYNC引脚行为 | 模块状态 |
|---|---|
| 64ms亮/800ms灭(慢闪) | 正在搜索网络 |
| 64ms亮/3000ms灭(快闪间隔) | 待机,已注册网络 |
| 常亮 | 通话中或GPRS数据传输中 |
上电后长时间慢闪无法转入待机,说明天线未接或SIM卡没识别到。此时用AT+CSQ查信号质量,返回+CSQ: 99,99表示无信号;返回+CSQ: 10,0以上数值越大信号越好。信号正常但无法注册网络,优先检查天线馈线焊接和SIM卡座的接触弹片。
异常恢复的兜底方案是软件看门狗加硬件复位引脚:当连续多次AT发送无响应时,先尝试用PWRKEY拉低1.5秒软复位,若软复位后仍无OK响应,则触发STM32F103的系统复位。使用HAL库自带的独立看门狗即可,超时时间设置为3秒,主循环里每轮AT交互完成后喂狗一次。注意不要在任何等待阻塞循环里喂狗,否则看门狗会失去意义。
本文还有配套的精品资源,点击获取