news 2026/9/16 16:15:33

STM32环境监测实战:20元开发板搭建温湿度烟雾报警系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32环境监测实战:20元开发板搭建温湿度烟雾报警系统

简介:面向物联网与嵌入式初学者的家庭环境监测系统完整工程,基于STM32F103C8T6,结合DHT11温湿度传感器、MQ-2烟雾传感器与0.9寸OLED屏,实现数据采集、实时显示与阈值报警。适合毕业设计、课程实训或智能家居入门,覆盖GPIO、ADC、定时器、I2C等典型外设应用。压缩包共231个文件,约5.32MB,以C源文件、H头文件、Keil工程文件及启动汇编为主,此外还包含编译生成的hex、map、crf等辅助文件,便于直接打开工程烧录验证。资源中还保留了J-Link调试日志,其中可看到USB连接失败的报错记录,对排查下载器连接问题有一定参考价值。目前已有3758人学习下载。借助这套资料,读者不仅能获得可运行的温湿度与烟雾监测代码,还能学习外设驱动封装、阈值判断与报警逻辑,理解从传感器采样到显示输出的完整开发流程。

1. 一块 20 元开发板搭家庭环境监测的完整链路

把 STM32F103C8T6、DHT11、0.9 寸 OLED 和 MQ-2 拼在一起,很多人第一反应是“这不过是个毕业设计”。但真正动手后你会发现,这块 20 元出头的国产 ARM Cortex-M3 最小系统板上,最考验人的反而不是写逻辑,而是怎么在 3.3V 供电、有限 GPIO 和不断抖动的传感器时序之间找到一个稳定平衡点。DHT11 要 18ms 以上的总线占用才能读到温湿度,MQ-2 的模拟电压会随预热时间缓慢漂移,蜂鸣器一响还会把板载稳压器的纹波拉高——这些问题堆在一起,恰好构成了一条从配环境到调参、从看波形到做联动的完整嵌入式训练路径。

这篇文章按“硬件连接 → 温湿度采集 → OLED 显示 → 烟雾检测与告警联动 → 参数校准与验证”的顺序推进,每一步都能直接抄板、烧录、看现象。目标读者是正在做物联网课设或入门比赛的人,也适合已经写过点 Arduino、想切换到 STM32 原生开发、顺便搞清 HAL 库底层配置的工程师。文章里所有代码均在 STM32F103C8T6 最小系统板上以 HAL 库方式验证,时钟配置为 72MHz 主频、8MHz 外部晶振,GPIO 多数为推挽输出或浮空输入,具体参数说明跟着代码走。

2. 硬件连接:别急着接线,先理清供电和引脚分配的边界

2.1 模块供电的 3.3V 与 5V 之争

DHT11 和 MQ-2 的常见模块上通常都带有板载上拉电阻和电源指示灯,但它们的推荐工作电压并不一致。DHT11 传感器本身工作电压是 3.3V 至 5.5V,而 MQ-2 模块上的比较器 LM393 需要 5V 才能保证比较阈值稳定;0.9 寸 OLED 的 SSD1306 驱动芯片则明确支持 3.3V,接 5V 反而有损坏风险。初次做这个项目的人最容易犯的错,是把所有模块的 VCC 全部接到 STM32F103C8T6 板载的 3.3V 输出上,结果 MQ-2 加热丝电流不足,传感器输出始终是低电平。

正确的接法是:板子通过 USB 或 ST-Link 供电时,5V 和 3.3V 两个引脚都已经有稳定输出,DHT11 和 OLED 接 3.3V,MQ-2 模块接 5V。STM32F103C8T6 的 GPIO 全部兼容 5V 输入(数据手册标注为 FT 引脚),所以 MQ-2 模块的数字输出 DO 可以直接连到 PA1 上,不需要电平转换。蜂鸣器模块如果是低电平触发的有源蜂鸣器,同样接 5V 供电,控制脚由 PA2 输出。

// 引脚分配总表(以标准最小系统板为准) // PA0 -> DHT11 DATA // PA1 -> MQ-2 DO(数字输出) // PA2 -> 蜂鸣器控制脚(低电平触发) // PB8 -> OLED SCL // PB9 -> OLED SDA // GND -> 所有模块共地

引脚说明:PA0 配置为开漏输出加外部上拉,是为了模拟 DHT11 需要的单总线时序;PA1 配置为浮空输入即可,因为 MQ-2 模块上的 LM393 已经输出明确的 0V 或 3.3V 逻辑电平;PB8 和 PB9 是 STM32F103C8T6 的 I2C1 引脚,硬件上直接复用。蜂鸣器接 5V 是因为有源蜂鸣器内部有振荡电路,5V 驱动声压更足,而 STM32 的推挽输出拉到 3.3V 也能触发,但声音会偏小。

2.2 蜂鸣器控制脚为什么不能直接推挽驱动

蜂鸣器模块上虽然自带了一个 S8550 三极管做驱动,但很多卖家为了省成本,模块上并没有加续流二极管。当蜂鸣器是感性负载时,断电瞬间会产生反电动势,如果直接让 STM32 的 PA2 推挽输出去关断,这个反向尖峰可能会灌进 GPIO,长期运行容易导致引脚内部钳位二极管老化。

常见做法是让 STM32 的 PA2 只控制一个外部 NPN 三极管(如 S8050)的基极,三极管集电极接蜂鸣器负极,蜂鸣器正极接 5V,同时在蜂鸣器两端反向并联一个 1N4148 二极管。这样 STM32 的 GPIO 只提供毫安级的基极电流,关断时反电动势被二极管吸收,MCU 本身得到保护。如果手头没有外部三极管,也可以直接用模块上的低电平触发引脚,但至少要在 PA2 上串联一个 1kΩ 电阻限流。

3. 用 HAL 库驱动 DHT11 温湿度传感器:时序逻辑比代码更关键

3.1 DHT11 的 40 位数据帧究竟怎么读

DHT11 单总线协议是理解整个系统的分水岭,它没有时钟线,完全靠主机拉低总线发起一次握手、然后从机连续输出高低电平来表示 0 和 1。一次完整读取需要这几个步骤:主机拉低总线至少 18ms,然后释放并拉高 20 至 40μs,接下来从机响应一个 80μs 的低电平和一个 80μs 的高电平,之后开始输出 40 位数据,每位数据都是 50μs 的低电平加上 26 至 28μs(代表 0)或 70μs(代表 1)的高电平。

这里最容易踩的坑是延时精度。HAL 库的 HAL_Delay() 基于 SysTick 中断实现,最小分辨率是 1ms,完全无法用于微秒级时序。因此在读取 DHT11 时,需要使用 DWT 或 SysTick 的 CYCCNT 寄存器来做微秒延时。

// 使用 DWT 实现微秒级延时,适用于 DHT11 时序 static void DWT_Delay_Init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; } static void DWT_Delay_us(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000); while ((DWT->CYCCNT - start) < ticks); } // 读取单个位:高电平持续时间长则为 1,短则为 0 uint8_t DHT11_ReadBit(void) { while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET); DWT_Delay_us(40); if (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) { while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET); return 1; } else { return 0; } }

延时说明:DWT_Delay_Init 使能了内核调试单元的周期计数器,DWT->CYCCNT 在每个时钟周期加一,因此 ticks 是用 SystemCoreClock(72MHz)换算出的周期数,DHT11_ReadBit 里的 40μs 延时是为了避开位起始的低电平窗口,再判断此刻总线电平是 0 还是 1,逻辑上等效于采样高电平宽度。这种方法比 for 循环空转精确得多,而且可移植到任何 Cortex-M3/M4 内核。

3.2 读取结果的数据校验

DHT11 返回的 40 位数据中,前 16 位是湿度整数和小数部分,中间 16 位是温度整数和小数部分,最后 8 位是校验和。DHT11 的温度小数位默认读出 0,湿度小数位可能有值,因此拼接时要把整数和小数分开处理。

typedef struct { uint8_t humi_int; uint8_t humi_dec; uint8_t temp_int; uint8_t temp_dec; uint8_t checksum; } DHT11_Data; uint8_t DHT11_Read(DHT11_Data *data) { uint8_t buffer[5] = {0}; HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_RESET); DWT_Delay_us(20000); // 主机拉低至少 18ms HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_SET); DWT_Delay_us(30); // 等待从机响应低电平 if (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) return 1; while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET); while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET); for (int i = 0; i < 40; i++) { buffer[i / 8] <<= 1; if (DHT11_ReadBit()) buffer[i / 8] |= 1; } if ((uint8_t)(buffer[0] + buffer[1] + buffer[2] + buffer[3]) != buffer[4]) { return 2; // 校验失败 } >// I2C 地址扫描函数,用于确认 OLED 实际地址 void OLED_ScanAddress(void) { for (uint8_t addr = 0x30; addr < 0x80; addr++) { if (HAL_I2C_IsDeviceReady(&hi2c1, addr, 1, 100) == HAL_OK) { printf("Found device at 0x%02X\r\n", addr); } } }

这块屏幕的刷新思路是基于 1KB 显存(128×32 像素,每像素 1 位),往显存写数据时先按页(8 像素一行)组织,写完整个显存后通过 I2C 一次性送入屏幕。显示字符串时要先做 ASCII 字模取模,取模方式为纵向取模、字节倒序,因为 SSD1306 的显存排列是从左上角开始、纵向跨越 8 页。

4.2 显示温湿度数值的格式化输出

在嵌入式场景中,直接调用 sprintf 会把 stdio 库的完整格式化逻辑拉进来,代码体积增加不少。0.9 寸 OLED 只有 32 像素高,显示两行文本比较合适:第一行显示湿度,第二行显示温度。为了速度,可以自己写一个整数转字符串的简单函数,或者用有限精度的 snprintf。

char line1[16]; char line2[16]; // 将温湿度按一位小数格式化为字符串 snprintf(line1, sizeof(line1), "Humi:%d.%d%%", dht.humi_int, dht.humi_dec); snprintf(line2, sizeof(line2), "Temp:%d.%d C", dht.temp_int, dht.temp_dec); OLED_Clear(); OLED_ShowString(0, 0, line1, 8); OLED_ShowString(0, 16, line2, 8); OLED_Refresh();

显示注意事项:每次刷新前先调 OLED_Clear 清空显存,否则上次残留的“Humi:”前缀会覆盖在新字符串前,造成叠字;OLED_ShowString 的第三个参数是字模在闪存中的起始地址,显示完要 OLED_Refresh 才会真正把显存通过 I2C 刷到屏上。由于 I2C 速率设为 400kHz,整屏刷新约需 30ms,肉眼感觉不到闪烁,但不要在 OLED_Refresh 期间去读 DHT11,否则 I2C 时序可能被拉长,传感器读数出错。

5. 烟雾传感器 MQ-2 读取与蜂鸣器联动告警

5.1 MQ-2 的数字输出与模拟输出的选择

MQ-2 模块上其实有两个输出引脚:AO 是模拟电压输出,DO 是数字电平输出。DO 引脚旁边通常有一个蓝色电位器,用来调节比较器阈值——顺时针旋转会提高触发阈值,逆时针调低,也就是说模块上电后,LED 指示灯亮起时 DO 输出低电平,表示当前烟雾浓度已经超过预设阈值。

项目中标题写的是“烟雾传感器”,但很多手头只有模块的人只接了 DO,这样系统只能告诉你“超标了”还是“没超标”,看不到浓度变化的趋势曲线。若 MCU 还有空闲 ADC 通道,把 AO 接到 PA3,配合 STM32 的 12 位 ADC 连续采样,可以在 OLED 上显示一个 0 至 4095 的浓度百分比条,同时用 DO 作为硬阈值快速触发蜂鸣器。这样双通道并用的做法在真实环境监测里更常见。

// 使用 ADC1 通道 4 读取 MQ-2 的 AO 引脚 HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 100); uint16_t smoke_adc = HAL_ADC_GetValue(&hadc1); // 映射到 0-100 浓度百分比 uint8_t smoke_percent = (smoke_adc * 100) / 4095; // 判断数字输出引脚,低电平表示烟雾超标 uint8_t smoke_alarm = (HAL_GPIO_ReadPin(MQ2_GPIO_Port, MQ2_Pin) == GPIO_PIN_RESET); if (smoke_alarm) { HAL_GPIO_WritePin(BUZZER_Port, BUZZER_Pin, GPIO_PIN_RESET); } else { HAL_GPIO_WritePin(BUZZER_Port, BUZZER_Pin, GPIO_PIN_SET); }

ADC 参数说明:STM32F103C8T6 的 ADC 是 12 位分辨率,转换结果范围 0 至 4095,对应 0V 至 3.3V(注意 AO 接 5V 供电时输出高电平可达 4V 左右,已经超过 3.3V 量程,建议把 MQ-2 供电改接 3.3V,并接受灵敏度略微下降的代价;或者用电阻分压后再进 PA3)。PollForConversion 设置 100ms 超时,如果转换未完成则返回超时错误码,实际项目中可把这段放在定时器中断里做周期性采样。

5.2 蜂鸣器节奏与误报抑制

蜂鸣器联动不能只做布尔判断,否则人在厨房炒个菜,油烟一飘就响个不停,系统会被当成狼来了。常见做法是加一个“连续超标 N 次才告警”的计数窗口,同时让蜂鸣器以 1Hz 频率间歇鸣叫,而不是长鸣。

uint8_t alarm_count = 0; // 主循环周期约 200ms 执行一次 void Smoke_Task(void) { if (smoke_alarm) { if (alarm_count < 5) alarm_count++; if (alarm_count >= 5) { // 500ms 响,500ms 停 if (HAL_GetTick() % 1000 < 500) { HAL_GPIO_WritePin(BUZZER_Port, BUZZER_Pin, GPIO_PIN_RESET); } else { HAL_GPIO_WritePin(BUZZER_Port, BUZZER_Pin, GPIO_PIN_SET); } } } else { alarm_count = 0; HAL_GPIO_WritePin(BUZZER_Port, BUZZER_Pin, GPIO_PIN_SET); } }

逻辑解释:alarm_count 是连续检测到烟雾超过阈值的次数,达到 5 次(约 1 秒)才触发告警,避免瞬时干扰。HAL_GetTick() 返回系统上电以来的毫秒数,用取模 1000 判断当前处于鸣叫窗口还是静音窗口。如果直接让蜂鸣器长鸣,不仅刺耳,还会让用户因为烦躁而干脆关掉电源。想要不同告警等级,可以改成火警两短一长、烟雾一短一长的节奏。

6. 调参与验证:把传感器数据校准到可信再谈智能化

6.1 DHT11 与标准温湿度计对比的偏移修正

DHT11 的精度本来就只有 ±2℃ 和 ±5% RH,在小空间里连续工作半小时后,PCB 上的走线发热可能导致测到的温度比环境实际高 1 至 2℃。常见的校准方法是把系统放在通风处,旁边放一个标准温湿度计,等待 10 分钟后记录两组数据,计算固定偏移量,然后在代码里做加法修正。

// 温度偏移修正示例:实测标准温度为 25.0℃,DHT11 读数为 26.3℃ // 则 int8_t temp_offset = 26 - 25 = 1,后续显示时减去 1 int8_t temp_offset = 1; int8_t display_temp = dht.temp_int - temp_offset; // 如果偏移量为负,同样可以直接相加

如果 DHT11 读数波动剧烈,不要先怀疑算法——优先检查 PA0 到传感器之间的杜邦线长度,线长超过 20cm 时总线电容会明显拉长电平跳变沿,导致 0/1 误判,此时把上拉电阻从 10kΩ 换成 4.7kΩ 能直接改善波形。另外,传感器探头不要贴近 STM32 芯片的背面部,那里是发热区。

6.2 MQ-2 预热漂移与阈值自校准

MQ-2 是半导体气敏传感器,内部加热丝需要持续通电才能保持敏感层温度,初次上电的 1 至 3 分钟里,输出电压会持续上升最后趋于稳定。很多人在上电后马上测试报警功能,发现一用打火机靠近就报,拿走之后却要等很久才恢复,原因不是传感器坏,而是加热丝尚未达到工作温度区间,敏感层表面吸附的气体分子还在慢慢脱附。

一个可靠的自校准流程是:MCU 上电后先等 60 秒,然后连续读取 10 次 MQ-2 的数字输出和模拟值作为基准。假设环境是洁净空气,将这 10 次的平均值作为阈值基准线,之后任何时刻读到的值超过基准线 1.2 倍,就判定为烟雾事件。这样做还有个额外好处,就是同一块板子换到不同海拔或不同环境,都能自动适应本底浓度,而不是依赖模块上那颗电位器的出厂位置。

// 上电自校准:取前 10 次读数的平均值作为基准 uint16_t baseline = 0; for (uint8_t i = 0; i < 10; i++) { HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 100); baseline += HAL_ADC_GetValue(&hadc1); HAL_Delay(100); } baseline /= 10; // 动态告警阈值 uint16_t alarm_threshold = baseline * 1.2;

参数怎么调:alarm_threshold 的 1.2 倍系数适合家庭厨房环境,若放在灰尘较大的木材加工间,建议提高到 1.5;若做的是酒精、油漆这类挥发性气体检测,MQ-2 本身对乙醇、甲烷都有响应,可以把系数放低到 1.1,同时增加 6.2 节提到的“连续 5 次超标”确认窗口,避免检测到香水、酒精湿巾这类瞬时挥发物时误响。

6.3 整机验证步骤

所有代码都烧好之后的验证顺序建议固定下来:第一步,用 ST-Link 连接板子,打开串口助手,波特率 115200,确认串口能周期性打印温湿度和烟雾 ADC 原始值,这一步能定位传感器接线和时序问题;第二步,对着 DHT11 呵一口气,观察串口输出湿度是否上升、温度是否微升,响应时间通常在 2 秒左右;第三步,用打火机不点火、靠近 MQ-2 放气,观察烟雾浓度值是否有台阶式上升,蜂鸣器是否在约 1 秒后开始间歇鸣叫;第四步,用棉签蘸少量酒精靠近 MQ-2,由于 MQ-2 对酒精同样敏感,应看到与烟雾类似的反应,但停止靠近后报警应在 5 秒内解除。

若 DHT11 长时间返回校验错误,用示波器挂 PA0 看波形,正常数据位的高电平应分别是 26μs 和 70μs 两簇;如果整个波形有重影,大概率是 DWT_Delay_us 的时钟频率算错,检查 SystemCoreClock 变量是否为 72MHz。OLED 屏不亮时,先用 I2C 扫描程序确认地址,如果地址是 0x3C 或 0x3D(7 位模式),说明模块使用了不同 I2C 地址映射,需要把驱动里的地址改为对应值。系统长时间运行后 OLED 出现花屏,多半是 I2C 总线被 DHT11 读取时的关闭外部中断抢占过长,可以在 OLED_Refresh 前临时关闭中断或改用 DMA 方式发送显存数据。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 16:14:25

毕业论文写作利器paperxie:AI辅助全流程解决方案

1. 毕业论文写作痛点与解决方案作为一名经历过毕业论文"洗礼"的过来人&#xff0c;我深知从选题到答辩的每个环节都充满挑战。记得当年我为了格式调整熬到凌晨三点&#xff0c;查重报告出来时手都在发抖。这种经历促使我深入研究了paperxie这款专为学术写作设计的智能…

作者头像 李华
网站建设 2026/9/16 16:13:55

PLC与伺服系统在高速贴标机中的协同控制方案

1. 项目背景与需求解析贴标机作为包装产线的核心设备&#xff0c;其自动化程度直接影响整线效率。在最近一个饮料产线改造项目中&#xff0c;客户要求实现每分钟120瓶的贴标速度&#xff0c;同时需兼容3种不同瓶型和5种标签规格。这要求PLC不仅要高速处理传感器信号&#xff0c…

作者头像 李华
网站建设 2026/9/16 16:13:25

TypeScript工程化基建:基于Nx与semantic-release的技能模块化实践

1. 项目概述&#xff1a;一个被严重低估的 TypeScript 工程化能力基座“agent-skills”这个名称乍看像某个 AI 智能体&#xff08;Agent&#xff09;的技能插件库&#xff0c;但结合热搜词agent-skills, TypeScript, node, Nx, semantic-release&#xff0c;再叠加大量围绕Type…

作者头像 李华
网站建设 2026/9/16 16:12:51

MBA论文写作工具对比:千笔AI与SpeedAI评测

1. 项目概述&#xff1a;MBA论文写作工具对比评测最近在MBA学术圈里&#xff0c;论文写作辅助工具突然成了热门话题。作为一名经历过三次论文重写的MBA毕业生&#xff0c;我深刻理解那种被导师打回重写的痛苦。今天要对比评测的两款AI写作工具——千笔AI和SpeedAI&#xff0c;都…

作者头像 李华
网站建设 2026/9/16 16:11:57

鸿蒙与Flutter跨端开发中的复杂JSON解析实践

1. 项目概述作为一名长期在移动端开发领域摸爬滚打的老兵&#xff0c;我最近在鸿蒙生态与Flutter跨端开发中遇到了一个经典难题——复杂JSON数据的解析与处理。这看似基础的操作&#xff0c;在实际业务场景中往往会演变成令人头疼的"数据迷宫"。JSON作为现代应用开发…

作者头像 李华