
简介这是一份基于STM32F103C8T6的多节点环境检测系统完整工程资源适用于嵌入式学习者和单片机开发者可帮助理解无线传感网络、数据采集与远程报警联动机制。系统支持多个检测节点同时采集温湿度、空气质量等环境数据通过HC-05无线发送至基站基站结合SGP30、MQ-135等传感器判断异常并借助SIM800C模块向手机发送短信提醒。资源包共452个文件约11.16MB以c、h源码文件为主包含大量模块化代码与可编译工程另有hex固件、uvprojx工程配置和OLED显示相关文件便于直接烧录与二次开发。目前已有1834人学习适合作为课程设计、毕业设计或竞赛项目的参考方案可快速搭建环境检测与告警系统原型。1. 为什么 STM32 环境检测系统要把传感器全堆在节点上做多节点环境监测最容易被低估的不是采集而是通信和异常判定。这套基于 STM32F103C8T6 的方案把 DHT11、SGP30、MQ-135、OLED 和蓝牙模块放在每个采集节点基站端再挂 SIM800C 做短信告警正好覆盖了“多点采集—无线汇聚—远程报警”的完整闭环。适合那些刚把 STM32 标准库工程跑通、想用真实项目把所有外设串起来的开发者也适合作为毕业设计的骨架工程。文件包里能看到 BKM32.axf、BMP280.axf 和 keilkilll.bat说明作者是用 Keil MDK 建的标准库工程不是 CubeMX 生成的 HAL 版本原始工程结构和寄存器操作习惯都保留得比较完整这对想读源码的人来说反而更好入手。2. 节点端传感器选型与 DHT11、SGP30、MQ-135 的读取逻辑2.1 DHT11 单总线时序的 STM32 实现要点这套系统的每个节点都以 STM32F103C8T6 为核心DHT11 负责温湿度采集。DHT11 用的是单总线协议只有一根数据线时序要求比 I2C 严格得多。经典坑是主机拉低总线后要在 20-40us 内释放随后读取从机响应的低电平时间——如果 GPIO 配置成推挽输出后没有及时切回输入模式读到的永远是高电平。标准库写法通常是把某个引脚初始化为开漏输出并外接上拉电阻这样读和写可以复用同一个引脚。#define DHT11_PORT GPIOB #define DHT11_PIN GPIO_Pin_8 void DHT11_Start(void) { GPIO_SetBits(DHT11_PORT, DHT11_PIN); GPIO_ResetBits(DHT11_PORT, DHT11_PIN); delay_us(18000); // 主机拉低超过18ms触发DHT11响应 GPIO_SetBits(DHT11_PORT, DHT11_PIN); delay_us(30); } uint8_t DHT11_ReadByte(void) { uint8_t i, data 0; for (i 0; i 8; i) { while (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) RESET); delay_us(40); if (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) SET) { data (data 1) | 0x01; } else { data (data 1) | 0x00; } while (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) SET); } return data; }GPIO_ReadInputDataBit读取前必须确认端口时钟已使能且引脚模式为输入。delay_us(40)的准确度直接影响数据位“0”和“1”的判定——DHT11 用高电平持续时长表达位值26-28us 是“0”70us 左右是“1”所以在 40us 处采样是折中方案。如果系统里开了多个定时器中断建议把 DHT11 读取放到主循环而非中断里否则中断嵌套会造成时序漂移。2.2 SGP30 与 MQ-135 的互补关系SGP30 是 I2C 接口的 VOC 气体传感器输出 eCO2 和 TVOC 两个指标内部自带算法校准但首次上电需要运行约 12 小时的基线漂移补偿否则读数会持续走高。初次使用要调用sgp30_probe()读取传感器 ID确认 I2C 地址是 0x58。MQ-135 则完全不同它是模拟输出型气敏电阻通过 ADC 采样电压来估算空气质量精度不高但对甲醛、氨气等还原性气体响应快。把这俩放一起是合理的——SGP30 看长期趋势MQ-135 看瞬时突变。ADC 采样 MQ-135 时有个细节STM32F103 的 ADC 输入阻抗不够高直接接 MQ-135 的模拟输出会导致采样值偏低。常见做法是加一个电压跟随器或者至少把采样时间设置为 239.5 周期让内部采样电容充分充电。ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) RESET); adc_value ADC_GetConversionValue(ADC1);ADC_SampleTime_239Cycles5是关键参数。MQ-135 的内阻变化范围从几 kΩ 到几百 kΩ采样时间太短会导致采样电容上的电压还没稳定就被锁存。ADC_RegularChannelConfig里的第二个参数ADC_Channel_0是 PA0 引脚别和 SGP30 的 I2C 引脚默认 PB6/PB7搞混。2.3 OLED 显示与传感器数据刷新策略OLED 在这个项目里承担的是节点本地显示功能用的是 SSD1306 驱动芯片。很多人把数据刷新放在主循环里每秒更新一次温湿度、VOC 数值但 OLED 是全屏刷新的话I2C 通信一次要传 1024 字节128×64每 8 行一页。如果刷得太快会干扰 SGP30 的 I2C 读取——两者共用同一组 I2C 外设SGP30 的单次测量指令也需要数百毫秒才返回结果。我建议的刷新策略是OLED 每秒刷新一次SGP30 每 2 秒读一次MQ-135 的 ADC 每 500ms 读一次。刷新 OLED 前加一个while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY))判断确保 I2C 总线空闲。数据展示上把温度、湿度、eCO2、TVOC 分四行显示每一行用固定宽度格式化避免数字位数变化导致显示错位。3. 多节点组网与 HC-05 蓝牙模块的串口透传机制3.1 节点与基站之间的三种组网方式对比这套系统架构是“多个采集节点 一个基站”但不同场景下节点和基站之间的物理连接方式可以不同。HC-05 蓝牙模块最常见的用法是主从配对模式基站端设为主机节点端设为从机开机后自动配对连接。这种方式的限制是主从一对一多个节点就需要基站扩展多个串口或者让节点轮流连接基站。对 STM32F103C8T6 来说串口资源有限USART1/2/3所以实际工程里更常见的是把 HC-05 配成广播模式基站轮询各个节点的数据。另一种方式是节点主动上报每个节点定时通过 HC-05 发送数据帧基站收到后解析来源 ID。这种方式实现简单但数据帧碰撞概率高节点越多越明显。第三种则是用 RS485 总线做有线组网稳定性最高但失去了无线部署的灵活性。3.2 自定义帧格式与串口 DMA 接收数据帧格式是整个通信链路里最值得花时间设计的部分。参考这个项目的工程结构帧格式可以这样定义帧头节点ID数据长度温湿度eCO2TVOCADC值校验帧尾0xAA1字节1字节4字节2字节2字节2字节1字节0x55节点 ID 用来区分不同位置的采集点比如客厅是 0x01、卧室是 0x02。数据长度字段表示从节点 ID 到校验位之前的字节数便于接收端做边界判定。校验用累加和即可对室内环境监测这种低速、低数据量应用CRC16 不必需但也不嫌多。接收端用串口空闲中断 DMA 是效率最好的方案。基站端的 STM32 通过 DMA 把串口数据搬到环形缓冲区串口空闲中断触发一帧数据接收完成标志。这样 CPU 不需要逐字节处理可以腾出手来做 OLED 显示和短信报警逻辑。void USART3_IRQHandler(void) { if (USART_GetITStatus(USART3, USART_IT_IDLE) ! RESET) { DMA_Cmd(DMA1_Channel3, DISABLE); data_len 64 - DMA_GetCurrDataCounter(DMA1_Channel3); DMA_SetCurrDataCounter(DMA1_Channel3, 64); DMA_Cmd(DMA1_Channel3, ENABLE); frame_ready 1; USART_ReceiveData(USART3); } }DMA1_Channel3是 USART3 的接收通道DMA_GetCurrDataCounter返回的是缓冲区剩余计数用总长度减去它就是实际收到的字节数。注意 DMA 接收缓冲要在初始化时清零frame_ready标志位在主循环里被清零否则同一帧数据会被重复处理。串口波特率建议设为 9600 或 19200HC-05 在 115200 下误码率会明显升高尤其是节点用锂电池供电时。3.3 HC-05 的 AT 指令配置方法HC-05 蓝牙模块出厂默认是 AT 模式上电后按住模块上的按键再通电会进入 AT 指令模式此时波特率固定为 38400。可以用 USB 转 TTL 模块连接通过串口助手发指令完成模块配置。关键配置项有三个主从模式、配对密码和配对码。基站端主机配置指令ATROLE1 // 设置为主模式 ATCMODE0 // 固定地址连接模式 ATBIND98D3:31:FD0A12 // 绑定节点端蓝牙地址 ATUART9600,0,0 // 设置串口波特率9600停止位1无校验节点端从机配置指令ATROLE0 // 设置为从模式 ATPSWD1234 // 设置配对密码 ATUART9600,0,0ATCMODE0和ATBIND配合使用能让基站只连接指定的节点蓝牙避免在蓝牙搜索列表里配错设备。如果节点数量超过两个基站端需要用多个 HC-05或者改用 ESP8266 做 TCP 组网。这个项目用的是 HC-05 说明作者控制了节点数量方案更偏向课程设计和毕业设计场景。4. 基站异常判定与 SIM800C 短信报警的完整实现4.1 异常阈值的分层设计“当基站检测到数据异常时给手机发短信”是这套系统的核心功能但“异常”的定义要仔细想。如果只是简单地判断温度超过 30°C 就报警传感器抖动和瞬时噪声会频繁触发告警。合理的做法是分两级告警阈值和确认时长。温度连续 5 次采样每次间隔 1 秒都超过阈值才确认进入异常状态然后触发短信发送。对于空气质量的判定SGP30 的 eCO2 建议阈值设为 1000ppmTVOC 设为 2.5mg/m³MQ-135 的 ADC 值因为不同批次传感器的基线电阻不同最好经过一次标定。标定方法是在洁净空气环境下读取 ADC 值作为基准然后把当前 ADC 值与基准值的差值作为异常判断依据。这个思路在老工程里通常被忽略导致设备换一个地方就误报。typedef struct { float temp_max; float humi_min; uint16_t eco2_max; uint16_t tvoc_max; uint16_t mq135_adc_max; } Alarm_Threshold; void Check_Environment_Abnormal(Env_Data_t* current, Alarm_Threshold* thr) { static uint8_t abnormal_count 0; uint8_t abnormal_flag 0; if (current-temperature thr-temp_max || current-humidity thr-humi_min || current-eco2 thr-eco2_max || current-tvoc thr-tvoc_max || current-mq135_adc thr-mq135_adc_max) { abnormal_flag 1; } if (abnormal_flag) { abnormal_count; if (abnormal_count 5) { SIM800C_Send_Alarm_SMS(current); abnormal_count 0; } } else { abnormal_count 0; } }abnormal_count是静态变量这个函数每次被调用都会累加连续异常次数。注意这里用了“连续 5 次”而不是“10 秒内任意 5 次”两者的区别在于前者能过滤掉瞬时毛刺但也会漏掉那种每隔 4 秒抖动一次的异常数据。实际部署时可以根据现场环境调整阈值比如厨房的温湿度波动本来就大阈值就要放宽一些。4.2 SIM800C 的 AT 指令流程与状态机设计SIM800C 是全频段 GSM/GPRS 模块通过串口用 AT 指令驱动。发送短信的标准流程是先发ATCMGF1切换到文本模式再发ATCMGS手机号等模块返回提示符后输入短信内容最后发送 0x1ACtrlZ结束。这里面最容易出问题的是等待提示符的超时处理SIM800C 在信号差的时候响应会延迟直接发短信内容会导致指令丢失。工程上要把短信发送封装成一个状态机避免主循环被阻塞。SIM800C 发送一条短信大约需要 2-3 秒如果基站同时要轮询多个节点数据阻塞式发送会让其他节点的数据缓存溢出。typedef enum { SMS_IDLE, SMS_SET_FORMAT, SMS_SET_PHONE, SMS_WAIT_PROMPT, SMS_SEND_CONTENT, SMS_WAIT_SEND_DONE } SMS_State; void SIM800C_Send_SMS_Task(void) { switch (sms_state) { case SMS_IDLE: if (alarm_triggered) { UART2_SendString(ATCMGF1\r\n); sms_state SMS_SET_FORMAT; } break; case SMS_SET_FORMAT: if (Check_AT_Response(OK)) { UART2_SendString(ATCMGS\13800138000\\r\n); sms_state SMS_SET_PHONE; } else { sms_state SMS_IDLE; } break; case SMS_SET_PHONE: if (Check_AT_Response()) { char content[64]; sprintf(content, Alarm! Temp:%.1f RH:%.1f%%, temp, humi); UART2_SendString(content); UART2_SendChar(0x1A); sms_state SMS_WAIT_SEND_DONE; } break; case SMS_WAIT_SEND_DONE: if (Check_AT_Response(OK)) { alarm_triggered 0; sms_state SMS_IDLE; } break; } }Check_AT_Response需要在串口接收中断里维护一个接收缓冲区并在收到完整响应后置标志位。要注意 SIM800C 在发送完短信后会返回CMGS: 序号再返回OK所以检查OK时要做模糊匹配不能直接比较字符串。5. 环境检测系统的移植、工程整理与调试技巧拿到这组工程文件后直接用 Keil 打开 BKM32.uvproj 或其他工程文件之前要把keilkilll.bat先跑一遍。这个批处理脚本的作用是清理 Keil 工程生成的中间文件包括 .o、.axf、.crf、.uvguix 里的用户配置等。常见做法是双击运行一次或者用命令行执行它会删除当前目录及子目录下所有编译产物保留 .c、.h、.uvproj 等源文件。如果系统之前用不同版本的 Keil 打开过工程可能会有 .uvguix 后缀的界面布局残留文件这些删掉并不影响编译。如果涉及同时使用 BMP280 气压传感器的数据要检查是 I2C 还是 SPI 接口。BMP280 的 I2C 地址是 0x76 或 0x77取决于 SDO 引脚的电平如果和 SGP30 共用 I2C 总线初始化顺序很重要先初始化 SGP30再初始化 BMP280因为这两颗芯片的初始化命令不同一旦 SDA 线上的设备地址冲突设备树和驱动代码都会变得难以排查。项目中提到的cc936.c、cc949.c等文件是与 FatFS 文件系统编码有关的转换表。cc936.c是简体中文 GBK 编码转换表cc949.c是韩文编码。如果你发现在某些移植版本中编译报错找不到cc936.c只需要在 FatFS 的配置头文件ffconf.h里选择需要的代码页并把对应的编码转换文件加入工程即可。这类文件体积较大占用的编译时间不少如果项目不需要中文文件名可以禁用相关宏定义或者不加入编译路径。调试这套系统时推荐先不用无线模块直接用 USB 转 TTL 连接节点和基站做串口联调。先确认节点端串口打印的数据正确再把 HC-05 接上——这里有个关键点HC-05 的 RXD 和 TXD 要交叉连接且模块的 RXD 不能直接接 STM32 的 3.3V 逻辑输出否则长期运行可能损坏模块建议加一个电平匹配电路或电阻分压。SIM800C 的供电要求也常被低估它的瞬间峰值电流可达 2A如果用 USB 供电短信发送时会掉电重启。要给 SIM800C 单独接一个 5V/2A 的电源或者用锂电池加升压模块供电。最后讲一个实用技巧给每个节点定义一个拨码开关配置的地址位把地址读取函数放在上电初始化阶段存入全局变量。这样同一套代码烧进不同的节点板不需要重新编译就能区分节点位置。这比在每个节点源码里改宏定义高效得多尤其是在部署了 4、5 个节点之后才发现要改地址的时候能省掉大量重复烧录的时间。本文还有配套的精品资源点击获取