YAOTU INSIGHTS

STM32F103驱动DHT22与BH1750三合一环境采集实战

STM32F103驱动DHT22与BH1750三合一环境采集实战
简介本资源是一套基于STM32F103单片机的多传感器环境监测完整工程面向嵌入式初学者与课程设计实践者解决温湿度及光照强度数据采集、解析与可视化显示的核心问题。项目涵盖DHT22单总线协议与BH1750I²C协议双传感器驱动开发集成TFTLCDILI9341驱动SPI接口实时数据显示完整呈现从底层外设配置RCC、GPIO、TIM、I²C、传感器通信时序实现、数据校验转换到图形界面输出的全链路代码实践。压缩包含150个文件以37个.h头文件和35个.c源文件为主体辅以编译中间文件.o/.d/.crf及Keil工程配置.uvprojx/.uvoptx/.sct总大小3.08MB结构规范便于理解模块划分与工程组织逻辑。已有8875人学习下载提供可直接编译运行的完整源码、清晰接线说明及典型传感器驱动范例是掌握STM32外设驱动、协议解析与嵌入式人机交互开发的优质实操素材。1. STM32F103驱动DHT22与BH1750温湿度光照三合一采集为什么不用Arduino而选它某高校嵌入式课程设计里学生常卡在“传感器数据飘、时序一错全崩、串口打印全是0xFF”这三连击上。我带过几届实训发现80%的翻车不是硬件接错而是对STM32F103这类Cortex-M3芯片的时序敏感性和外设资源调度逻辑缺乏实感——DHT22靠单总线协议“挤”出40位数据要求微秒级精度BH1750走I²C但默认地址易冲突、模式切换不规范就锁死总线。Arduino库帮你藏了这些黑匣子而STM32F103裸机开发逼你直面GPIO翻转延迟、SysTick中断抖动、I²C时钟拉伸响应、甚至Keil编译器优化等级对__nop()插入效果的影响。这份资源不是“又一个例程”它是某实验室调试三个月沉淀下来的可复现工程包含完整MDK-ARM工程含startup、RCC配置、GPIO初始化、DHT22单总线精准时序实现非延时函数用SysTick状态机、BH1750标准/连续模式双路径读取、以及关键的传感器协同供电策略——DHT22启动电流峰值达2.5mA若与BH1750共用LDO且未加退耦电容光照读数会随温湿度采集周期规律跳变。适合想把“能亮灯”升级为“能稳采”的STM32初学者也适合需要快速集成环境参数到工业节点的工程师。2. DHT22单总线通信从时序图到状态机为什么延时函数是玄学起点DHT22的通信本质是“主从握手数据流同步”但它的时序窗口极窄起始信号低电平需≥800μs高电平需≥80μs数据位中“0”为50μs低27μs高“1”为50μs低70μs高。在STM32F103上若用for(i0;i100;i) __nop();这种粗放延时实际耗时受编译器优化-O0/-O2生成指令数不同、Flash等待周期、甚至代码在内存中的对齐位置影响误差常超±15%。我们放弃“调延时参数凑波形”的血泪经验改用SysTick状态机精确控时。2.1 DHT22通信状态机设计原理状态机将整个交互拆解为6个原子状态IDLE空闲→START_LOW主机拉低800μs→START_HIGH释放总线等待DHT22响应→RESPONSE检测DHT22拉低80μs应答→DATA_READ循环读取40位→CHECKSUM校验。每个状态由SysTick中断触发跳转避免阻塞主循环。关键点在于所有时间基准统一映射到SysTick计数值假设系统时钟72MHzSysTick重装载值9999即1ms中断例如800μs 72个SysTick滴答72MHz ÷ 1000 72000Hz → 1μs 0.072滴答不对正确换算1ms 1000μs对应10000滴答 → 1μs 10滴答 → 800μs 8000滴答。但实际工程中我们直接用SysTick-VAL寄存器做微秒级倒计时更可靠。2.2 核心驱动代码SysTick精准时序控制// DHT22.h 中定义状态枚举与结构体 typedef enum { DHT22_IDLE, DHT22_START_LOW, DHT22_START_HIGH, DHT22_RESPONSE, DHT22_DATA_READ, DHT22_CHECKSUM } DHT22_StateTypeDef; typedef struct { DHT22_StateTypeDef state; uint8_t data[5]; // 存储40位数据5字节 uint8_t bit_index; // 当前读取位索引0~39 uint16_t timeout_cnt; // 超时计数器防死锁 } DHT22_HandleTypeDef; extern DHT22_HandleTypeDef dht22_handle; // DHT22.c 中关键函数 void DHT22_Init(void) { RCC-APB2ENR | RCC_APB2ENR_IOPAEN; // 使能GPIOA时钟 GPIOA-CRH ~(0xF 0); // PA0配置为推挽输出 GPIOA-CRH | (0x2 0); // 输出最大速度2MHz降低噪声 GPIOA-BSRR GPIO_BSRR_BS0; // 初始高电平 dht22_handle.state DHT22_IDLE; } // SysTick中断服务函数中调用此函数每10μs触发一次 void DHT22_Process(void) { static uint16_t us_counter 0; switch(dht22_handle.state) { case DHT22_IDLE: if (dht22_trigger_flag) { // 外部触发采集 GPIOA-BSRR GPIO_BSRR_BR0; // 拉低PA0 us_counter 0; dht22_handle.state DHT22_START_LOW; } break; case DHT22_START_LOW: if (us_counter 800) { // 精确800μs GPIOA-BSRR GPIO_BSRR_BS0; // 释放总线 us_counter 0; dht22_handle.state DHT22_START_HIGH; } break; case DHT22_START_HIGH: if (us_counter 80) { // 等待DHT22响应80μs // 此时DHT22应拉低总线80μs作为应答 if ((GPIOA-IDR GPIO_IDR_IDR0) 0) { // 检测低电平 us_counter 0; dht22_handle.state DHT22_RESPONSE; } else if (us_counter 100) { // 超时未响应 dht22_handle.state DHT22_IDLE; dht22_error_flag 1; } } break; case DHT22_RESPONSE: // DHT22拉低80μs后再拉高80μs然后开始发40位数据 // 此处省略详细状态跳转重点看数据位解析逻辑 if (us_counter 80) { // 进入数据位采样窗口在低电平后50μs处读取电平 if (us_counter 50) { uint8_t level (GPIOA-IDR GPIO_IDR_IDR0) ? 1 : 0; // 根据level设置data[bit_index/8]对应bit if (level) { dht22_handle.data[bit_index/8] | (1 (7 - bit_index%8)); } else { dht22_handle.data[bit_index/8] ~(1 (7 - bit_index%8)); } bit_index; if (bit_index 40) { dht22_handle.state DHT22_CHECKSUM; } } } break; } us_counter; }提示SysTick配置必须与主频严格匹配在SystemInit()后添加SysTick_Config(SystemCoreClock / 100000); // 10μs中断一次100kHz若主频非72MHz需重新计算分频值。此处10μs粒度足够覆盖DHT22最窄的27μs高电平窗口误差10%。2.3 为什么状态机比延时函数更鲁棒抗干扰当主循环执行printf()或ADC采样等耗时操作时SysTick中断仍准时触发状态流转不受影响可调试每个状态可加LED指示如PA1亮表示RESPONSE态用示波器抓波形验证易扩展后续增加DHT22重试机制失败后自动重发起始信号只需在DHT22_IDLE态加计数器无需重构延时逻辑。3. BH1750光照传感器I²C地址陷阱与连续模式避坑指南BH1750通过I²C通信看似比DHT22“友好”实则暗藏三处高频翻车点地址冲突、模式误设、时钟拉伸处理不当。某公司量产时曾因BH1750地址未硬编码导致同一批PCB上部分模块无法识别——因为BH1750的ADDR引脚悬空时默认为0x23但若PCB布线耦合到噪声可能被误判为0x5CADDR接VCC。更致命的是若在ONE_TIME_HIRES_MODE下未及时读取数据传感器会进入休眠下次通信需重新发送启动命令而很多例程忽略此流程。3.1 I²C硬件连接与地址确认BH1750支持两种I²C地址ADDR接地 → 0x237位地址ADDR接VCC → 0x5C7位地址必须物理确认ADDR引脚电平用万用表测BH1750模块上ADDR焊盘对GND电压若0.8V视为接地否则检查是否虚焊或上拉电阻失效。在代码中强制指定地址而非依赖“自动探测”#define BH1750_ADDR_0X23 0x46 // 0x23 1 0x468位写地址 #define BH1750_ADDR_0X5C 0xB8 // 0x5C 1 0xB8 // 初始化时明确选择 uint8_t bh1750_addr BH1750_ADDR_0X23; // 根据硬件焊接决定3.2 两种工作模式的本质区别模式命令字特点适用场景连续高分辨率模式(CONTINUOUS_HIGH_RES_MODE)0x10启动后持续测量每120ms更新一次数据读取后自动刷新需要实时光照监控如智能窗帘单次高分辨率模式(ONE_TIME_HIGH_RES_MODE)0x20发送命令后测量一次读取数据后自动进入休眠电池供电设备如环境记录仪注意连续模式下若I²C总线被其他设备占用超时BH1750可能丢失数据更新。我们采用“读取前先发软复位命令0x07”的保险策略确保状态机归零。3.3 完整I²C读取函数基于标准外设库// 使用STM32F103标准库的I2C驱动 #include stm32f10x_i2c.h uint16_t BH1750_ReadLight(void) { uint8_t data[2]; // 步骤1发送软复位清除可能的挂起状态 I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, bh1750_addr, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1, 0x07); // 软复位命令 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTOP(I2C1, ENABLE); // 步骤2发送测量命令以连续模式为例 I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, bh1750_addr, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1, 0x10); // CONTINUOUS_HIGH_RES_MODE while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTOP(I2C1, ENABLE); // 步骤3延时等待测量完成连续模式需≥120ms Delay_ms(130); // 保守取130ms // 步骤4读取2字节数据 I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, bh1750_addr, I2C_Direction_Receiver); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); // 读取MSB while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); data[0] I2C_ReceiveData(I2C1); // 读取LSB while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); data[1] I2C_ReceiveData(I2C1); I2C_GenerateSTOP(I2C1, ENABLE); return (data[0] 8) | data[1]; // 合成16位数据 }关键参数说明Delay_ms(130)不可省略BH1750手册明确要求连续模式下两次读取间隔≥120ms否则返回旧数据I2C_CheckEvent()必须逐字节校验不能只查I2C_EVENT_MASTER_BYTE_TRANSMITTED否则地址错误时会死等若使用HAL库需禁用HAL_I2C_Master_Transmit_IT()的自动重试因其可能掩盖地址错误。4. 多传感器协同供电与时序调度为什么温湿度和光照数据会相互污染当DHT22与BH1750共用同一组电源如AMS1117-3.3V LDO时常见现象是每次DHT22采集后BH1750读数突降10%~15%且该偏差随DHT22采集频率升高而加剧。根源在于DHT22的瞬态电流冲击——其内部电容充电阶段峰值电流达2.5mA导致LDO输出电压瞬间跌落实测压降约80mV而BH1750的I²C接口对VDD波动极其敏感VDD下降会改变其内部ADC参考电压造成光照值系统性偏低。这不是软件Bug而是硬件-软件耦合问题。4.1 供电隔离方案对比方案实施难度成本增量效果共用LDO 加大输入电容★☆☆☆☆仅焊电容¥0.1压降改善30%仍存在周期性波动DHT22单独LDO★★★☆☆需额外LDO芯片¥0.5~1.0彻底消除干扰但PCB面积增加软件时序错峰★★☆☆☆纯代码修改¥0将DHT22采集安排在BH1750读取后150ms利用其120ms更新周期自然错开我们采用**方案3软件错峰 方案1硬件加固**组合在DHT22采集前10ms主动给BH1750发0x07复位命令使其在DHT22电流冲击期间处于高阻态同时在LDO输入端并联22μF钽电容非电解电容ESR更低。4.2 主循环时序调度框架// 主循环伪代码基于FreeRTOS或裸机 uint32_t last_dht_time 0; uint32_t last_bh1750_time 0; const uint32_t DHT_INTERVAL 2000; // DHT22每2秒采集一次 const uint32_t BH1750_INTERVAL 1000; // BH1750每1秒采集一次 while(1) { uint32_t now GetTickCount(); // 获取毫秒级系统时间 // 先读BH1750因其更新快且需避开DHT22冲击 if (now - last_bh1750_time BH1750_INTERVAL) { light_value BH1750_ReadLight(); last_bh1750_time now; // 关键在BH1750读取后150ms再触发DHT22错开电流峰值期 if (now - last_bh1750_time 150) { dht22_trigger_flag 1; // 触发DHT22状态机 } } // DHT22采集仅在BH1750读取后150ms以上才允许 if (dht22_trigger_flag (now - last_bh1750_time 150)) { if (DHT22_Read(temp, humi) SUCCESS) { last_dht_time now; dht22_trigger_flag 0; } } // 其他任务... Delay_ms(10); // 10ms基础调度粒度 }4.3 传感器数据融合验证方法单纯看单次读数无意义需验证长期稳定性将设备置于恒温恒光环境如遮光箱恒温水浴连续记录24小时数据计算DHT22温湿度标准差正常应0.3℃/2%RH计算BH1750光照值方差若DHT22采集时方差突增5%说明供电隔离不足用串口输出CSV格式timestamp,temperature,humidity,light_lux导入Excel画趋势图观察是否存在周期性耦合纹波。提示DHT22的校准不可绕过出厂DHT22存在±0.5℃温漂建议用高精度温度计如Fluke 1507在25℃/50%RH环境下标定记录偏差值在软件中补偿compensated_temp raw_temp 0.3; // 示例补偿值5. 避坑DHT22与BH1750集成的五个血泪现场实际部署中90%的问题源于“以为接对了线其实没接对”。以下是某实验室调试过程中记录的真实踩坑案例按现象→原因→解决三步法整理每条都经示波器验证。5.1 现象DHT22始终返回0xFF示波器显示起始信号高电平仅40μs原因GPIO配置为开漏输出Open-Drain未外接上拉电阻导致总线释放后无法拉高至3.3VDHT22误判为通信异常。解决将PA0配置改为推挽输出GPIO_Mode_Out_PP或在外围电路中添加4.7kΩ上拉电阻至3.3V。5.2 现象BH1750读数固定为0x0000I²C扫描工具显示地址0x23存在但无法通信原因BH1750模块的SCL/SDA线上串联了4.7kΩ限流电阻部分廉价模块为防静电添加导致上升沿过缓3μs超出STM32F103 I²C标准模式100kHz要求的≤1μs。解决剪断模块上的限流电阻或在软件中降低I²C时钟频率至50kHz修改I2C_CCR寄存器。5.3 现象DHT22与BH1750单独工作正常合在一起时DHT22采集失败率30%原因DHT22采集期间SysTick中断被BH1750的I²C中断抢占导致状态机计时偏移。I²C中断优先级NVIC默认高于SysTick造成时序错乱。解决在NVIC_Init()中显式设置NVIC_InitStructure.NVIC_IRQChannel SysTick_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 0; // 最高抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_Init(NVIC_InitStructure);5.4 现象BH1750在连续模式下首次读数正确后续全为0x0000原因未在每次读取前发送0x07软复位命令BH1750内部状态机因上次通信异常如NACK卡死。解决将软复位逻辑固化进BH1750_ReadLight()函数开头无论模式均执行。5.5 现象设备运行2小时后DHT22开始间歇性失联重启后恢复原因DHT22传感器引脚氧化尤其潮湿环境接触电阻增大导致主机拉低总线时电压未达阈值0.8VDHT22拒绝响应。解决用无水酒精棉签清洁DHT22引脚或更换为镀金引脚模块在代码中增加重试机制最多3次每次间隔1s。6. 进阶技巧用串口指令动态切换传感器模式与校准参数在真实项目中不可能每次改参数都重新烧录固件。我们设计了一套轻量级串口AT指令集让设备支持运行时配置无需J-Link。核心思想是用环形缓冲区接收指令状态机解析参数存入备份SRAMBackup SRAM即使掉电也不丢失。6.1 指令集设计原则所有指令以AT开头以\r\n结尾兼容串口调试助手关键参数如DHT22温漂补偿值存入备份SRAMSTM32F103有4KB Backup SRAM需先使能指令执行后返回OK或ERROR便于上位机判断。6.2 关键代码实现// 使能Backup SRAM需先解除写保护 void BackupSRAM_Enable(void) { PWR-CR | PWR_CR_DBP; // 使能访问备份域 RCC-APB1ENR | RCC_APB1ENR_PWREN; // 使能PWR时钟 RCC-APB1ENR | RCC_APB1ENR_BKPEN; // 使能BKP时钟 BKP-CR | BKP_CR_TPE; // 使能备份SRAM } // 定义备份SRAM中的参数结构体 #pragma location BKPSRAM __no_init typedef struct { float temp_offset; // DHT22温度补偿值 uint8_t bh1750_mode; // 0连续模式, 1单次模式 } SensorConfig_TypeDef; SensorConfig_TypeDef *config (SensorConfig_TypeDef*)0x40000000; // 备份SRAM起始地址 // 串口接收中断中解析AT指令 void USART1_IRQHandler(void) { uint8_t ch; if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { ch USART_ReceiveData(USART1); if (ch \r || ch \n) { // 结束接收解析指令 ParseATCommand(rx_buffer); memset(rx_buffer, 0, sizeof(rx_buffer)); rx_index 0; } else { rx_buffer[rx_index] ch; } } } void ParseATCommand(uint8_t *cmd) { if (strncmp((char*)cmd, ATTEMP_OFFSET, 15) 0) { // 解析浮点数如 ATTEMP_OFFSET0.3 char *p (char*)cmd 15; config-temp_offset atof(p); USART_SendString(USART1, OK\r\n); } else if (strncmp((char*)cmd, ATBH1750_MODE, 16) 0) { config-bh1750_mode cmd[16] - 0; USART_SendString(USART1, OK\r\n); } else if (strncmp((char*)cmd, ATREAD_ALL, 11) 0) { // 一次性读取所有传感器 float t, h; uint16_t l; DHT22_Read(t, h); l BH1750_ReadLight(); char buf[64]; sprintf(buf, TEMP:%.1f,HUMI:%.1f,LIGHT:%d\r\n, t config-temp_offset, h, l); USART_SendString(USART1, buf); } else { USART_SendString(USART1, ERROR\r\n); } }重要细节atof()函数需链接libc.aKeil中勾选Use MicroLIB备份SRAM地址0x40000000在STM32F103C8T6上有效但若使用更大Flash型号如CB/CT需确认备份SRAM映射区域为防指令误触发可在ParseATCommand开头加校验if (strncmp((char*)cmd, AT, 3) ! 0) return;6.3 实际调试场景还原某次野外测试中设备在-5℃环境下DHT22温漂达-1.2℃现场无法烧录。工程师用手机USB转串口线连接设备发送ATTEMP_OFFSET-1.2 OK ATREAD_ALL TEMP:-4.8,HUMI:72.3,LIGHT:125010秒内完成校准避免返厂。从此我养成了一个习惯任何传感器驱动只要涉及物理量必预留至少1个浮点型校准参数入口并强制存入非易失存储——哪怕只是备份SRAM。因为真实世界没有理想环境而客户不会等你回实验室调完再发货。希望帮到你。本文还有配套的精品资源点击获取