简介:这份资源是面向嵌入式初学者与物联网开发者的STM32温湿度采集报警系统完整工程包,围绕环境温湿度实时监测与超限报警展开,可应用于农业大棚、仓储监控、智能家居等场景。包内共204个文件,以52个c源文件与43个h头文件为核心,涵盖GPIO、ADC、定时器及串口驱动代码,另含30个d依赖文件、30个o目标文件、28个crf编译记录及uvprojx、uvoptx等Keil工程配置,压缩包约3.29MB,工程结构完整可直接编译。项目涉及DHT系列传感器的单总线通信时序解析、ADC模拟量采集、中断服务程序触发蜂鸣器或LED报警,以及WiFi、蓝牙模块的无线告警扩展思路,代码注释与模块划分便于理解硬件交互流程。目前已有4799人学习下载,适合希望掌握STM32外设驱动、传感器通信与报警机制设计的学习者参考实践。
1. 从一份 STM32 温湿度采集报警系统源码说起:它到底能解决什么问题
如果你手头正好有一份 STM32 温湿度采集报警系统的源代码,或者正准备从零搭一套,先别急着烧录。这类项目在 STM32 入门到进阶的过渡阶段出现频率极高,原因很直接:它同时踩中了传感器驱动、串口通信、阈值判断、报警输出四条主线,任何一条没打通,系统就是块砖。我见过太多人把 DHT11 接到 PA0 就开始写 while 循环,结果读出来全是 0xFF,最后怀疑人生。
这套系统要解决的核心问题很朴素:让一颗 STM32 周期性读取环境温湿度,在超出设定范围时触发声光报警,同时把数据通过串口或屏幕呈现出来。适合谁?电子类专业做课设的学生、刚转嵌入式的软件工程师、需要快速验证传感器方案的硬件从业者。它不追求高精度工业级采集,但足够让你把 GPIO、定时器、UART、单总线协议这些基础外设串成一条完整的链路。源代码的价值不在于代码本身多优雅,而在于它提供了一个可运行的参照系,让你知道每一步该看什么、改什么、验证什么。
2. 温湿度采集报警系统的硬件选型与最小系统搭建
2.1 传感器选型:DHT11、DHT22 还是 SHT30
选型决定了后续代码的复杂度和精度天花板。DHT11 最便宜,单总线协议,湿度精度 ±5%RH,温度 ±2℃,采样周期不能低于 1 秒,适合对精度无要求的场景。DHT22 贵一点,精度提升到 ±2%RH 和 ±0.5℃,量程也更宽。SHT30 走 I2C,精度 ±1.5%RH 和 ±0.2℃,响应快,但需要额外的 I2C 驱动和上拉电阻。
我一般会这样建议:如果只是做课设或验证逻辑,DHT11 足够,因为它的单总线时序能让你深刻理解微秒级延时的重要性。如果要做实际环境监测,直接上 SHT30,省去单总线时序调试的玄学问题。注意 DHT11 对供电电压敏感,3.3V 下长线传输容易丢包,建议传感器供电脚就近加 100nF 去耦电容。
2.2 最小系统与报警输出电路
STM32 最小系统包括晶振、复位、BOOT 配置和电源滤波。报警输出通常用蜂鸣器加 LED,蜂鸣器分有源和无源,有源接 GPIO 直接响,无源需要 PWM 驱动。我习惯用无源蜂鸣器加 S8050 三极管驱动,GPIO 输出 PWM 控制音调,这样能区分“温度超限”和“湿度超限”两种报警音。
LED 限流电阻按 1kΩ 到 2kΩ 选,太小亮度高但电流大,太大亮度不足。如果系统要接继电器控制外部设备,务必加光耦隔离,否则继电器吸合瞬间的反向电动势可能直接打穿 GPIO。这一点在实验室里翻车的人不少,血泪经验就是:任何感性负载,隔离先行。
2.3 接线表与电源注意事项
| 模块 | 引脚 | STM32 引脚 | 备注 |
|---|---|---|---|
| DHT11 | VCC | 3.3V | 加 100nF 去耦 |
| DHT11 | DATA | PA0 | 需 4.7kΩ 上拉 |
| DHT11 | GND | GND | 共地 |
| 蜂鸣器 | 基极 | PB5 | 经 1kΩ 到 S8050 |
| LED 红 | 阳极 | PC13 | 经 1kΩ 到 GND |
| LED 绿 | 阳极 | PC14 | 经 1kΩ 到 GND |
| UART1 | TX | PA9 | 接 USB-TTL RX |
| UART1 | RX | PA10 | 接 USB-TTL TX |
电源部分,如果直接用 ST-Link 的 3.3V 给整个板子供电,蜂鸣器响的时候电流会瞬间拉高,可能导致 STM32 复位。常见做法是外部供 5V,板载 LDO 转 3.3V,或者至少给蜂鸣器单独供电。这个坑我在早期项目里踩过,现象是蜂鸣器一响,串口就断,查了半天以为是串口干扰,其实是电源跌落。
3. 从源码拆解:DHT11 驱动、定时器与串口上报的实现
3.1 DHT11 单总线时序的代码实现
DHT11 的通信过程分三步:主机发开始信号、DHT11 响应、数据传输。关键在于微秒级延时,HAL 库的 HAL_Delay 是毫秒级,不能用。我一般用 DWT 或者定时器做微秒延时。
// 微秒延时,基于 DWT 计数器 void delay_us(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000); while ((DWT->CYCCNT - start) < ticks); } // 读取 DHT11 一个字节 uint8_t DHT11_ReadByte(void) { uint8_t i, byte = 0; for (i = 0; i < 8; i++) { while (!HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN)); // 等待 50us 低电平结束 delay_us(40); // 延时 40us 判断高电平持续时间 byte <<= 1; if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN)) byte |= 1; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN)); // 等待剩余高电平结束 } return byte; }逻辑说明:DHT11 用高电平持续时间区分 0 和 1,26-28us 表示 0,70us 表示 1。延时 40us 后采样,如果还是高电平就是 1,否则是 0。参数上,DWT 需要先在初始化时使能,CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;。注意 DHT11 上电后需要 1 秒稳定时间,第一次读取前先延时 1 秒,否则读出来全是 0。
3.2 定时器中断里做周期采集与阈值判断
采集周期用定时器中断实现,比如 TIM2 每 2 秒触发一次。中断里置标志位,主循环检测标志位后执行读取,避免在中断里做耗时操作。
volatile uint8_t read_flag = 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { read_flag = 1; } } // 主循环 while (1) { if (read_flag) { read_flag = 0; if (DHT11_ReadData(&temp, &humi) == 0) { if (temp > TEMP_HIGH || humi > HUMI_HIGH) { Alarm_On(ALARM_BOTH); } else { Alarm_Off(); } printf("Temp:%d C, Humi:%d %%RH\r\n", temp, humi); } } }参数说明:TIM2 预分频器设为 7199,自动重装载值 19999,系统时钟 72MHz 时,定时周期 = (7199+1)*(19999+1)/72000000 = 2 秒。阈值 TEMP_HIGH 和 HUMI_HIGH 建议用宏定义放在头文件,方便修改。注意 printf 重定向到 UART 需要实现fputc,否则串口没输出。
3.3 串口重定向与上位机数据格式
串口输出用printf最方便,但需要重定向。在 Keil 里勾选 Use MicroLIB,然后实现:
int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); return ch; }数据格式我一般用Temp:25 C, Humi:60 %RH这种纯文本,方便串口助手直接看。如果要接上位机画曲线,可以改成 JSON 或 CSV,比如{"t":25,"h":60}。注意串口波特率设 115200,太高在长线上容易误码,太低刷屏慢。如果发现串口输出乱码,先查波特率和时钟配置,八成是 HSE 没起振或者 PLL 倍频算错了。
4. 报警逻辑与阈值配置:别让蜂鸣器变成噪声源
4.1 回差判断与报警状态机
最简单的报警逻辑是“超限就响,不超就停”,但这样在阈值附近会频繁启停,蜂鸣器咔咔响,继电器也容易坏。我一般加回差,比如温度超过 30℃ 报警,低于 28℃ 才解除。湿度超过 70% 报警,低于 65% 解除。
#define TEMP_HIGH 30 #define TEMP_LOW 28 #define HUMI_HIGH 70 #define HUMI_LOW 65 typedef enum { NORMAL, ALARM_TEMP, ALARM_HUMI, ALARM_BOTH } AlarmState; AlarmState state = NORMAL; void Check_Alarm(int temp, int humi) { switch (state) { case NORMAL: if (temp > TEMP_HIGH && humi > HUMI_HIGH) state = ALARM_BOTH; else if (temp > TEMP_HIGH) state = ALARM_TEMP; else if (humi > HUMI_HIGH) state = ALARM_HUMI; break; case ALARM_TEMP: if (temp < TEMP_LOW) state = NORMAL; break; case ALARM_HUMI: if (humi < HUMI_LOW) state = NORMAL; break; case ALARM_BOTH: if (temp < TEMP_LOW && humi < HUMI_LOW) state = NORMAL; break; } }逻辑说明:状态机保证报警后不会因为数值微小波动就解除,必须降到回差下限才恢复。参数上,回差一般取 2-5 个单位,太小没效果,太大响应迟钝。注意如果传感器读数本身跳动大,先做滑动平均滤波,比如连续读 5 次去掉最大最小值再平均。
4.2 蜂鸣器驱动与 LED 指示
有源蜂鸣器直接 GPIO 置高就响,无源的需要 PWM。我习惯用 TIM3 的 CH1 输出 2kHz 方波驱动无源蜂鸣器,不同报警状态用不同频率,比如温度报警 2kHz,湿度报警 1kHz,两者都超就交替响。
void Alarm_On(AlarmState s) { switch (s) { case ALARM_TEMP: __HAL_TIM_SET_AUTORELOAD(&htim3, 35999); // 2kHz HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); HAL_GPIO_WritePin(LED_RED_PORT, LED_RED_PIN, GPIO_PIN_RESET); break; case ALARM_HUMI: __HAL_TIM_SET_AUTORELOAD(&htim3, 71999); // 1kHz HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); HAL_GPIO_WritePin(LED_GREEN_PORT, LED_GREEN_PIN, GPIO_PIN_RESET); break; default: HAL_TIM_PWM_Stop(&htim3, TIM_CHANNEL_1); HAL_GPIO_WritePin(LED_RED_PORT, LED_RED_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(LED_GREEN_PORT, LED_GREEN_PIN, GPIO_PIN_SET); break; } }参数说明:系统时钟 72MHz,预分频 0 时,ARR=35999 对应 2kHz,ARR=71999 对应 1kHz。LED 低电平点亮,所以报警时置 RESET。注意 PWM 启动后如果不停,蜂鸣器会一直响,状态切换时先 Stop 再改 ARR,否则可能出现占空比异常。
5. 避坑与排查:源码跑不通时先查这五个地方
5.1 现象:DHT11 读出来全是 0 或 255
原因:单总线时序不对,或者上拉电阻没接。DHT11 的数据线必须接 4.7kΩ 上拉到 3.3V,否则总线拉不高。另外微秒延时不准也会导致采样点偏移。
解决:用示波器看 DATA 线波形,正常应该有 80us 低电平开始信号,然后 80us 低电平响应,接着 40 个数据位。如果没有波形,查 GPIO 是否配置为开漏输出,或者延时函数是否被编译器优化掉。把 delay_us 加volatile修饰或者用__NOP()填充。
5.2 现象:串口输出乱码
原因:波特率不匹配、时钟配置错误、或者 TX/RX 接反。常见的是外部晶振没起振,系统默认跑 HSI,导致波特率偏差。
解决:先确认SystemCoreClock的值,在 main 里打印出来看是不是 72000000。如果是 8000000,说明 HSE 没起振,查晶振负载电容和焊接。另外 USB-TTL 模块的 TX 接 STM32 的 RX,RX 接 TX,别接反。
5.3 现象:蜂鸣器一响系统就复位
原因:电源电流不足,蜂鸣器启动瞬间拉低电压。或者蜂鸣器驱动三极管基极电阻太小,GPIO 灌电流超限。
解决:给蜂鸣器单独供电,或者加大电源滤波电容。基极电阻从 1kΩ 换成 4.7kΩ,减小 GPIO 电流。如果用的是有源蜂鸣器,直接接 GPIO 可能电流超标,必须加三极管。
5.4 现象:定时器中断进不去
原因:中断优先级配置错误,或者 NVIC 没使能。HAL 库中HAL_TIM_Base_Start_IT才会开中断,只调HAL_TIM_Base_Start不会进回调。
解决:检查HAL_NVIC_SetPriority和HAL_NVIC_EnableIRQ是否调用,确认HAL_TIM_Base_Start_IT(&htim2)在 main 里执行。如果用了 FreeRTOS,注意中断优先级不能高于configMAX_SYSCALL_INTERRUPT_PRIORITY。
5.5 现象:阈值改了但报警不动作
原因:宏定义改了但没重新编译,或者变量被优化。还有一种情况是传感器读数没更新,read_flag 一直被清零。
解决:改宏定义后全编译一次,别只点 Build。在报警判断前加串口打印,确认 temp 和 humi 的值是否在变。如果不变,查 DHT11 读取函数返回值,可能是校验和错误被丢弃了。
6. 进阶技巧:把采集数据用起来,而不只是响个蜂鸣器
6.1 滑动平均滤波与数据校准
DHT11 的读数跳动大,直接用来判断阈值容易误报。我一般做 5 点滑动平均,把最近 5 次读数存数组,每次新数据进来替换最旧的,然后求平均。这样能平滑掉大部分随机噪声。
#define FILTER_LEN 5 int temp_buf[FILTER_LEN] = {0}; int humi_buf[FILTER_LEN] = {0}; uint8_t buf_idx = 0; int Filter_Avg(int *buf) { int sum = 0; for (int i = 0; i < FILTER_LEN; i++) sum += buf[i]; return sum / FILTER_LEN; } // 读取后存入 temp_buf[buf_idx] = temp; humi_buf[buf_idx] = humi; buf_idx = (buf_idx + 1) % FILTER_LEN; int temp_avg = Filter_Avg(temp_buf); int humi_avg = Filter_Avg(humi_buf);参数说明:FILTER_LEN 越大越平滑,但响应越慢。5 是一个折中值,对应 2 秒采集周期就是 10 秒的窗口。如果发现滤波后数据滞后严重,降到 3。注意数组初始化要填合理值,否则前几次平均会被 0 拉低。
6.2 用 OLED 或 LCD 本地显示
串口看数据需要电脑,实际部署时加个 0.96 寸 OLED 更直观。I2C 接口的 SSD1306 驱动成熟,移植方便。显示内容分三行:第一行温度,第二行湿度,第三行报警状态。刷新率不用太高,500ms 一次足够,否则 I2C 占用太多时间。
void OLED_Refresh(int temp, int humi, AlarmState s) { char buf[20]; sprintf(buf, "Temp: %d C", temp); OLED_ShowString(0, 0, (uint8_t *)buf); sprintf(buf, "Humi: %d %%RH", humi); OLED_ShowString(0, 2, (uint8_t *)buf); switch (s) { case NORMAL: OLED_ShowString(0, 4, (uint8_t *)"Status: OK"); break; case ALARM_TEMP: OLED_ShowString(0, 4, (uint8_t *)"Status: TEMP!"); break; case ALARM_HUMI: OLED_ShowString(0, 4, (uint8_t *)"Status: HUMI!"); break; case ALARM_BOTH: OLED_ShowString(0, 4, (uint8_t *)"Status: BOTH!"); break; } }注意 OLED 的 I2C 地址通常是 0x78 或 0x7A,取决于 SA0 引脚。如果屏幕不亮,先扫 I2C 地址,确认接线。另外 OLED 初始化需要延时,上电后等 100ms 再发命令。
6.3 数据上报到巴法云或其他物联网平台
如果想让数据上云,STM32 加 ESP8266 走 AT 指令是最省事的方案。巴法云支持 MQTT,ESP8266 烧 AT 固件后,STM32 通过串口发 AT 指令连接。关键指令序列:AT+CWMODE=1设 STA 模式,AT+CWJAP="SSID","PASS"连 WiFi,AT+MQTTUSERCFG配置 MQTT 参数,AT+MQTTCONN连接,AT+MQTTPUB发数据。
void ESP8266_SendData(int temp, int humi) { char cmd[64]; sprintf(cmd, "AT+MQTTPUB=0,\"/topic\",\"temp:%d,humi:%d\",0,0\r\n", temp, humi); HAL_UART_Transmit(&huart2, (uint8_t *)cmd, strlen(cmd), 1000); }参数说明:topic 要跟巴法云控制台创建的主题一致,数据格式用 JSON 或纯文本都行。注意 ESP8266 的波特率默认 115200,但 AT 固件版本不同可能不一样,先发AT确认。如果连不上 WiFi,查 SSID 和密码有没有特殊字符,AT 指令里需要转义。
6.4 一个我常犯的错误
早期做这套系统时,我总想把所有功能塞进一个 while 循环,结果 DHT11 读取阻塞 20ms,串口打印又阻塞,OLED 刷新再阻塞,整个系统响应迟钝,按键按下去半天没反应。后来改成定时器分时调度:2 秒读传感器,500ms 刷 OLED,100ms 扫按键,主循环只做状态机切换。这样每个任务互不干扰,CPU 占用率也降下来了。
如果你也在做类似的项目,记住一句话:阻塞式延时是嵌入式系统的头号敌人。能用状态机就用状态机,能用中断就用中断。希望帮到你。
本文还有配套的精品资源,点击获取