STM32环境监测站DIY:从DHT11、MQ-2到ADC与单总线驱动实战
简介本资源是一套完整的基于STM32的多参数环境监测系统开发套件面向嵌入式初学者、课程设计学生及物联网实践开发者解决温湿度、烟雾浓度与光照强度同步采集与处理的核心需求。压缩包共303个文件含45个C源文件实现DHT11单总线驱动、MQ-2与光敏电阻ADC采样、数据滤波与标定逻辑、47个头文件定义传感器接口与结构体、69个编译中间文件.o及6个工程备份文件.bak辅以Keil MDK工程配置.uvprojx/.uvoptx和调试输出.axf/.map整体18.1MB结构清晰、模块解耦便于理解外设配置与传感器协同逻辑。已有9614人学习下载资源提供可直接编译运行的完整工程涵盖HAL库初始化、时序精准控制、模拟信号校准、多传感器数据融合等关键实现细节并附带原理图PDF助力读者快速掌握STM32环境感知类项目的软硬件协同开发全流程。1. 项目缘起为什么需要自己动手做一个环境检测仪最近在整理工作室翻出来一堆之前做项目剩下的传感器模块像DHT11、MQ-2、光敏电阻这些都是些经典又便宜的“电子积木”。看着它们我就琢磨与其让它们在角落里吃灰不如组合起来做个实实在在能用的东西。正好手边还有几块STM32的开发板一个集成了温湿度、烟雾浓度和光照强度检测的“桌面环境监测站”的想法就成型了。这个项目听起来不复杂但麻雀虽小五脏俱全。它几乎涵盖了嵌入式开发入门到进阶的多个核心环节GPIO的输入输出驱动DHT11、ADC模数转换读取MQ-2和光敏电阻、定时器精准延时、串口通信调试以及如何将分散的传感器数据整合成一个可读的系统。对于刚接触STM32的朋友来说跟着做一遍远比只看教程收获大对于有经验的朋友也能从中回顾一些底层驱动的细节和系统整合的思路。更重要的是做完之后你得到的是一个可以摆在桌上、实时告诉你环境状况的小设备这种成就感是看代码无法比拟的。接下来我会从硬件连接、核心驱动原理、代码实现、调试心得这几个方面把整个项目的构建过程掰开揉碎了讲清楚。文末我也会提供完整的原理图使用嘉立创EDA绘制和源程序文件基于STM32标准外设库你可以直接拿去复现或作为自己项目的起点。2. 硬件选型与电路设计为什么是它们仨选择DHT11、MQ-2和光敏电阻这个组合是经过成本和实用性权衡的。它们价格低廉、资料丰富、接口简单非常适合用于原型验证和DIY项目。2.1 传感器核心特性与工作原理DHT11温湿度传感器这是一个数字式传感器通过单总线协议与MCU通信。它内部集成了一个电阻式感湿元件和一个NTC测温元件以及一个8位单片机做模数转换和协议处理。对我们开发者而言好处是无需外接ADC只需要一个GPIO口按照严格的时序要求读写即可。但要注意它的测量响应较慢约2秒一次湿度精度为±5%RH温度精度为±2°C适用于对精度要求不高的场合。MQ-2烟雾传感器这是一个模拟量输出的半导体气敏传感器。它的核心是一个二氧化锡SnO2敏感层当接触到可燃气体如液化气、丙烷、氢气或烟雾时其电导率会发生变化从而改变其与负载电阻分压后的输出电压。这个电压是模拟量所以我们需要用到STM32的ADC功能将其转换为数字值。MQ-2需要预热通常通电20-30分钟后读数才稳定且输出电压与环境气体浓度大致呈对数关系通常需要进行校准。光敏电阻这是一个最简单的模拟量传感器其电阻值随光照强度增强而减小。我们通过一个固定电阻如10KΩ与其串联构成分压电路。光照变化导致光敏电阻阻值变化从而改变中间节点的电压这个电压同样通过ADC读取。它的响应是非线性的但对于区分“亮”、“暗”、“中等亮度”这种级别已经足够。2.2 电路连接详解与原理图思路三个传感器与STM32最小系统板的连接是项目的硬件基础。下图清晰地展示了连接关系flowchart TD subgraph A [STM32 最小系统板] direction LR PC1[GPIO Pin] PA0[ADC Channel 0] PA1[ADC Channel 1] UART_TX[UART TX] UART_RX[UART RX] end subgraph B [传感器模块] DHT11_Sensor[DHT11br温湿度传感器] MQ2_Sensor[MQ-2br烟雾传感器] LDR_Sensor[光敏电阻模块] end PC1 -- 单总线数据线 -- DHT11_Sensor PA0 -- ADC读取模拟电压 -- MQ2_Sensor PA1 -- ADC读取模拟电压 -- LDR_Sensor UART_TX -- 发送数据至电脑 -- PC[上位机/串口助手] UART_RX -- 接收电脑指令 -- PC具体接线说明以STM32F103C8T6为例DHT11VCC- 开发板3.3V注意有些模块兼容5V但STM32 GPIO是3.3V电平建议统一用3.3V供电GND- 开发板GNDDATA- 开发板PC1可任选一个GPIO这里以PC1为例MQ-2模块VCC- 开发板5VMQ-2加热丝需要5V供电才能正常工作GND- 开发板GNDAO模拟输出 - 开发板PA0对应ADC1的通道0光敏电阻模块VCC- 开发板3.3VGND- 开发板GNDAO模拟输出 - 开发板PA1对应ADC1的通道1串口通信用于调试输出USART1_TX(PA9) - USB转TTL模块的RXUSART1_RX(PA10) - USB转TTL模块的TXGND- USB转TTL模块的GND注意MQ-2模块的DO数字输出引脚在这个项目中我们用不到它是通过一个电位器设定阈值后输出的高低电平我们采用ADC读取AO是为了获得更连续的浓度信息。另外为ADC通道PA0, PA1增加一个0.1uF的滤波电容到地可以有效抑制高频干扰让读数更稳定。原理图设计要点在绘制原理图如使用嘉立创EDA时除了正确的连线还要注意电源去耦在每个传感器的VCC引脚附近放置一个0.1uF的陶瓷电容到GND尤其是在MQ-2模块附近因为其工作时电流会有波动。上拉电阻DHT11的数据线通常需要在靠近MCU一端接一个4.7KΩ - 10KΩ的上拉电阻到3.3V以确保总线空闲时为高电平。很多模块已经内置了这个电阻但原理图上最好体现出来如果模块没有则需要自己添加。ADC输入保护可以在PA0和PA1上串联一个100Ω的电阻并配合一个钳位二极管如BAT54S到3.3V和GND防止意外高压损坏ADC引脚。对于DIY项目如果接线确认无误可以省略但养成好习惯很重要。3. 软件驱动解析从时序到数据硬件搭好只是骨架让传感器“活”起来的核心是软件驱动。这部分我们分传感器逐个击破。3.1 DHT11单总线驱动与时间的精准对话DHT11的通信协议是典型的单总线协议对时序要求极其严格。MCU作为主机控制整个通信过程。通信流程拆解主机启动信号MCU将数据线拉低至少18毫秒ms然后拉高20-40微秒µs随后释放总线设置为输入模式等待DHT11响应。从机响应信号DHT11检测到起始信号后会先将总线拉低80µs再拉高80µs表示准备发送数据。数据传输随后DHT11开始发送40位数据8位湿度整数8位湿度小数8位温度整数8位温度小数8位校验和。每一位数据都以一个50µs的低电平起始位开始随后的高电平持续时间决定数据位是0还是126-28µs的高电平表示位0。70µs的高电平表示位1。数据解析MCU需要在起始低电平后等待一个短暂延时如30-40µs再去采样总线电平以此判断是0还是1。代码实现关键基于标准库// 1. 引脚初始化 void DHT11_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(DHT11_GPIO_CLK, ENABLE); GPIO_InitStructure.GPIO_Pin DHT11_GPIO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; // 初始化为推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStructure); GPIO_SetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN); // 先拉高 } // 2. 核心读取函数 uint8_t DHT11_Read_Byte(void) { uint8_t i, data 0; for (i 0; i 8; i) { data 1; // 左移先接收高位 while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) RESET); // 等待50us低电平起始位结束 Delay_us(40); // 延时40us后采样避开临界点 if (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) SET) { data | 0x01; // 读到1 while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) SET); // 等待高电平结束 } // 如果读到0此时总线已经是低电平下一位的起始位直接进入下一轮循环 } return data; } // 3. 读取温湿度 uint8_t DHT11_Read_Data(uint8_t *temp, uint8_t *humi) { uint8_t buf[5]; uint8_t i; // ... 发送起始信号 ... if (DHT11_Check() 0) { // 检查响应信号 for (i 0; i 5; i) { buf[i] DHT11_Read_Byte(); } if (buf[0] buf[1] buf[2] buf[3] buf[4]) { // 校验和验证 *humi buf[0]; *temp buf[2]; return 1; // 成功 } } return 0; // 失败 }避坑心得DHT11最怕时序不准。Delay_us()函数必须用定时器实现精确微秒延时不能使用简单的for循环因为编译器优化和中断会影响其准确性。我使用的是SysTick定时器来实现。另外两次读取间隔要大于2秒否则DHT11可能不响应。3.2 ADC读取MQ-2与光敏电阻将模拟世界数字化MQ-2和光敏电阻的输出都是模拟电压我们需要STM32的ADC将其转换为数字值。这里采用ADC1的多通道扫描模式配合DMA可以高效、自动地连续采集多个通道的数据不占用CPU资源。ADC配置步骤初始化ADC时钟和GPIO使能ADC1和GPIOA的时钟将PA0、PA1配置为模拟输入模式。配置ADC参数ADC_Mode独立模式。ADC_ScanConvModeENABLE扫描模式。ADC_ContinuousConvModeENABLE连续转换。ADC_ExternalTrigConv软件触发。ADC_DataAlign右对齐。ADC_NbrOfChannel通道数这里是2。配置规则通道使用ADC_RegularChannelConfig函数按顺序PA0, PA1添加通道并设置采样时间如ADC_SampleTime_55Cycles5。配置DMA因为是多通道连续转换数据会源源不断产生使用DMA是标准做法。使能DMA1时钟。配置DMA通道ADC1对应DMA1_Channel1。设置外设地址为ADC1的DR寄存器地址内存地址为我们定义的数组如uint16_t ADC_ConvertedValue[2]数据方向为外设到内存内存地址自增。启用DMA校准ADC启动转换。代码片段示例uint16_t ADC_ConvertedValue[2]; // 存放通道0和通道1的结果 void ADC1_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; ADC_InitTypeDef ADC_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; // 1. 开启时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); // 2. 配置PA0, PA1为模拟输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 配置DMA DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)ADC_ConvertedValue; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize 2; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; // 循环模式 DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel1, DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); // 4. 配置ADC ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode ENABLE; ADC_InitStructure.ADC_ContinuousConvMode ENABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 2; ADC_Init(ADC1, ADC_InitStructure); // 配置规则通道顺序和采样时间 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); // 5. 使能ADC DMA校准启动 ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); }配置完成后ADC_ConvertedValue[0]和ADC_ConvertedValue[1]就会自动被DMA更新为PA0和PA1的ADC原始值0-4095对应0-3.3V。数据换算电压值 (ADC原始值 / 4095) * 3.3VMQ-2浓度估算这是一个难点因为其输出与浓度是非线性的。通常的做法是在洁净空气中读取一个基准值ADC_air。在实际测量中计算ratio ADC_gas / ADC_air。根据传感器数据手册中的曲线图或经验公式通过ratio估算ppm浓度。对于项目演示我们常常直接用ADC原始值或电压值来相对表示“浓度变化”。光照强度估算同样非线性。可以设定几个阈值比如ADC值 3000为“黑暗”1000 ADC值 3000为“昏暗”ADC值 1000为“明亮”。更精确的做法需要对照照度计进行标定。3.3 系统整合与数据输出有了各个传感器的数据我们需要一个主循环来协调它们并通过串口将数据发送到电脑端显示。主程序逻辑int main(void) { uint8_t temperature, humidity; uint16_t smoke_adc, light_adc; float smoke_voltage, light_voltage; SystemInit(); Delay_Init(); // 初始化SysTick延时 USART1_Init(115200); // 初始化串口115200波特率 DHT11_Init(); ADC1_DMA_Init(); printf(Environment Monitor Started\r\n); while(1) { // 1. 读取DHT11每2秒以上读一次 if(DHT11_Read_Data(temperature, humidity)) { printf(Temp: %d C, Humi: %d %%\r\n, temperature, humidity); } else { printf(DHT11 Read Failed!\r\n); } // 2. 获取ADC值DMA自动更新直接读取数组 smoke_adc ADC_ConvertedValue[0]; light_adc ADC_ConvertedValue[1]; smoke_voltage (float)smoke_adc / 4095 * 3.3; light_voltage (float)light_adc / 4095 * 3.3; printf(Smoke ADC: %d (Voltage: %.2f V)\r\n, smoke_adc, smoke_voltage); printf(Light ADC: %d (Voltage: %.2f V)\r\n, light_adc, light_voltage); printf(-------------------\r\n); Delay_ms(2500); // 延时2.5秒兼顾DHT11的读取间隔 } }为了让printf重定向到串口你需要重写fputc函数int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); return ch; }4. 调试与优化让数据更可靠项目联调阶段总会遇到各种问题这里分享几个最常见的坑和解决办法。4.1 DHT11读取失败排查现象串口一直打印“DHT11 Read Failed!”。检查接线这是第一步也是最容易出错的一步。确保VCC、GND、DATA线没有接错或虚焊。用万用表量一下DHT11的VCC和GND之间是不是3.3V。检查上拉电阻如果模块没有内置上拉电阻必须在数据线上加一个4.7KΩ-10KΩ的上拉电阻到3.3V。检查延时函数这是重中之重。DHT11对微秒级延时非常敏感。确保你的Delay_us()函数是准确的。可以用逻辑分析仪或者示波器抓取DATA线的波形对照数据手册的时序图看启动信号、响应信号和数据位的高低电平持续时间是否在允许范围内。我最初用循环实现的延时就不准后来换用SysTick定时器才稳定。检查电源干扰尝试在DHT11的VCC和GND之间并联一个10uF的电解电容稳定电源。检查代码逻辑确认在发送起始信号后是否及时将GPIO模式从输出切换为输入以释放总线等待DHT11响应。4.2 ADC读数跳动大现象串口输出的ADC值尤其是光敏电阻在光照不变时最后几位数字不停跳动。软件滤波这是最常用的方法。不要只取一次ADC值而是连续采样多次比如16次或32次然后排序取中值或者直接求平均值。DMA配合定时器触发可以很方便地实现定期采集一组数据然后处理。#define SAMPLE_TIMES 32 uint16_t get_filtered_adc_value(uint8_t channel_index) { uint32_t sum 0; for(int i0; iSAMPLE_TIMES; i) { sum ADC_ConvertedValue[channel_index]; Delay_ms(1); // 简单延时或等待下一次DMA更新 } return (uint16_t)(sum / SAMPLE_TIMES); }硬件滤波如前所述在ADC输入引脚对地加一个0.1uF的电容。如果信号线较长可以考虑使用屏蔽线。参考电压稳定确保给STM32的VDDA模拟电源和VSSA模拟地引脚提供了干净、稳定的电源最好通过磁珠或电感与数字电源隔离并加上去耦电容。避开噪声源让ADC走线远离晶振、数字开关信号线等噪声源。4.3 MQ-2数据解读与校准现象不知道ADC值代表什么浓度。预热MQ-2通电后需要至少20分钟预热才能稳定。刚上电的读数没有参考价值。获取基准值在确认洁净、通风良好的环境中读取稳定的ADC值作为ADC_air。这个值会因传感器个体差异、环境温湿度而不同。定性判断对于报警应用可以采取相对值法。例如设定一个阈值Threshold ADC_air * 1.5当实时ADC值持续超过该阈值一段时间则判定为有烟雾/可燃气体。这种方法简单有效避免了复杂的ppm换算。定量估算进阶如果需要粗略估算浓度需要查找该型号MQ-2的灵敏度特性曲线图。通常数据手册会给出在不同气体浓度下的传感器电阻比值Rs/R0曲线。你需要先计算出当前传感器的R0在洁净空气中的电阻然后根据实时ADC值计算Rs再查表或拟合公式估算浓度。这个过程比较繁琐多数DIY项目用定性判断足矣。4.4 系统功耗与实时性考量这是一个简单的轮询系统主循环中延时2.5秒。在低功耗应用场景下这是不可接受的。使用定时器中断可以配置一个定时器每2.5秒产生一次中断在中断服务函数中设置一个标志位。主循环只检查这个标志位标志位有效时才去读取DHT11和打印数据。这样CPU大部分时间可以进入低功耗的睡眠模式。使用RTOS如FreeRTOS为每个传感器创建一个任务。例如创建一个低优先级任务每2.5秒读取一次DHT11ADC由DMA自动更新另一个任务定期去处理ADC数组并计算。这样程序结构更清晰也便于扩展比如增加OLED显示任务、网络上传任务等。5. 功能扩展与项目进阶思路基础功能实现后这个项目还有很多可以玩和升级的地方。5.1 本地显示脱离电脑的独立设备添加一个OLED显示屏如SSD1306驱动的0.96寸OLED可以实时显示所有数据让设备真正独立。硬件通过I2C或SPI接口连接OLED到STM32。软件移植一个OLED驱动库如u8g2或Adafruit_SSD1306的C语言版本。在主循环中将格式化好的字符串显示到OLED上替代或补充串口打印。5.2 阈值报警从监测到预警增加声光报警功能当烟雾浓度或温度超过设定阈值时自动报警。硬件增加一个LED和一个蜂鸣器连接到STM32的GPIO。软件在主循环的数据处理部分加入判断逻辑。例如if(smoke_adc SMOKE_THRESHOLD) { GPIO_SetBits(BUZZER_GPIO_PORT, BUZZER_PIN); // 蜂鸣器响 GPIO_SetBits(LED_GPIO_PORT, LED_PIN); // LED亮 } else { GPIO_ResetBits(BUZZER_GPIO_PORT, BUZZER_PIN); GPIO_ResetBits(LED_GPIO_PORT, LED_PIN); }可以设置一个报警延迟和消抖避免误报。5.3 数据上传接入物联网通过ESP8266/ESP32 WiFi模块或NB-IoT模块将数据上传到云平台如阿里云、腾讯云、ThingsBoard等实现远程监控。硬件添加一个串口WiFi模块如ESP-01S连接到STM32的另一个串口USART2。软件STM32通过AT指令集控制ESP8266连接路由器。使用MQTT或HTTP协议将传感器数据打包成JSON格式发送到云平台。云平台可以配置数据可视化仪表盘和报警规则。进阶使用带有网络功能的MCU如STM32LWIP或直接使用ESP32可以简化硬件设计。5.4 外壳与电源完成产品化最后一步使用3D打印或亚克力板为你的环境监测仪制作一个外壳不仅能保护电路也更美观。电源方面可以从USB供电改为锂电池供电并加入充电管理电路如TP4056实现便携和长期监测。这个基于STM32的多传感器环境检测项目从硬件焊接、软件驱动到系统调试完整地走完了一个嵌入式产品原型开发的全流程。过程中遇到的时序、ADC、数据融合等问题都是嵌入式开发中的经典问题。希望这份详细的拆解能帮助你不仅成功复现这个项目更能理解其背后的原理并激发你更多的创意和扩展。本文还有配套的精品资源点击获取