STM32F103驱动DHT22与BH1750的工程级实践方案
发布时间:2026/10/20 13:45:25 锦皓数字建站

简介本资源是一套基于STM32F103单片机的嵌入式环境监测项目完整工程面向嵌入式初学者与课程实践者解决温湿度及光照强度多传感器协同采集、解析与可视化显示的核心问题适用于智能农业、环境监控等教学与原型开发场景。压缩包共150个文件含37个头文件h定义外设接口与数据结构、35个源文件c实现DHT22单总线驱动、BH1750的I2C通信、TFTLCD显示逻辑及系统时钟配置等核心功能另有编译中间文件o/d/crf与Keil工程配置uvprojx、uvoptx、sct等整体3.08MB结构规范便于理解STM32标准外设库工程组织方式。已有8875人学习下载资源附带可直接编译运行的完整代码、清晰接线说明及LCD数据显示效果涵盖脉冲时序捕获、I2C读写时序处理、校验位验证、光照数值单位换算等关键实现细节是掌握传感器驱动与嵌入式外设协同开发的优质实践范例。1. 为什么用 STM32F103 驱动 DHT22 BH1750 不是“接上线就能读数”——一套能过答辩、能上电、能稳定跑 72 小时的嵌入式传感方案很多同学拿到毕设题目“基于 STM32 的环境监测系统”第一反应是DHT22 有现成库BH1750 是 I²CHAL 库点几下就生成了烧进去一读数据不就完事了结果调试三天DHT22 偶尔返回 0x8000校验失败BH1750 连续读 10 次有 3 次超时串口打印全是乱码或跳变值答辩时老师一问“你这温湿度连续 5 分钟没刷新是传感器坏了还是你的时序没对上”当场卡壳。这不是玄学是典型嵌入式传感落地的“三重断层”硬件信号完整性没验证、协议时序没抠到微秒级、软件状态机没覆盖异常分支。本方案不是教你怎么复制粘贴例程而是还原一个真实项目负责人从选型确认、信号实测、驱动重写、多传感器协同、到长期稳定性压测的完整闭环。它面向两类人一是需要交一份结构完整、参数可查、现象可复现、答辩能讲清“为什么这么写”的课程设计/毕业设计二是手头已有开发板但总在传感器通信环节翻车、急需一份带波形截图、带寄存器配置依据、带掉电恢复逻辑的工程级参考。整套资源包含 Keil MDK 工程含全部 .c/.h、逻辑分析仪实测时序图.csv .png、传感器数据校准表、以及一份 12 页的《多源传感系统验收 checklist》——不是说明书是交付物。2. 硬件接口与协议层设计DHT22 单总线时序死磕与 BH1750 I²C 地址冲突规避2.1 DHT22 单总线协议为什么标准 HAL_Delay_us() 在 F103 上根本不可靠DHT22 的通信本质是单总线One-Wire协议但和 DS18B20 不同它没有 ROM 搜索机制全靠严格的高低电平持续时间定义状态。关键时序点有 4 个主机拉低 ≥800μs 启动信号 → 释放总线并延时 20–40μs → DHT22 拉低 80μs 响应 → 再拉高 80μs 表示准备就绪。问题来了STM32F103 使用 SysTick 或 HAL_Delay_us() 实现微秒级延时在 72MHz 主频下HAL_Delay_us(20)实际耗时可能在 18–25μs 波动而 DHT22 对响应窗口容忍度极低±5μs。更致命的是HAL_Delay_us() 依赖 SysTick 中断若中断被禁用如进入临界区延时直接失效。我们放弃所有软件延时改用GPIO 输出比较 定时器输入捕获双模控制// 初始化 DHT22 GPIO推挽输出上拉 GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // 启动信号手动控制 GPIO用 TIM2 CH1 输出精确 850μs 低电平 __HAL_RCC_TIM2_CLK_ENABLE(); TIM_HandleTypeDef htim2; htim2.Instance TIM2; htim2.Init.Prescaler 72 - 1; // 1MHz 计数频率72MHz / 72 htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 849; // 850μs0~849 共 850 个计数 htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_Base_Init(htim2); HAL_TIM_Base_Start(htim2); // 拉低 PA0 并启动定时器 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); __HAL_TIM_SET_COUNTER(htim2, 0); __HAL_TIM_ENABLE(htim2); // 等待定时器溢出自动拉高 while (__HAL_TIM_GET_FLAG(htim2, TIM_FLAG_UPDATE) RESET); __HAL_TIM_CLEAR_FLAG(htim2, TIM_FLAG_UPDATE); HAL_TIM_Base_Stop(htim2);提示这段代码不是为了炫技而是解决一个真实痛点——当系统开启 FreeRTOS 或大量中断服务时软件延时完全失准。用硬件定时器输出固定脉宽把时序控制权从 CPU 移交给外设是工业级传感器驱动的底线做法。2.2 BH1750 地址配置陷阱为什么你的 I²C 扫描总显示 “No Device”BH1750 支持两种地址模式ADDR 引脚接地为0x23接 VCC 为0x5C。但实际电路中若使用 4.7kΩ 上拉电阻且 VCC3.3VADDR 引脚电平易受 PCB 走线容抗影响导致上电时识别为浮空态I²C 总线扫描失败。某高校课程设计中12 块板子有 3 块始终无法识别 BH1750最后发现是 ADDR 焊盘离电源地平面太近形成分布电容使引脚等效电压落在 0.8–2.0V 的不确定区。解决方案分三级硬件层在 ADDR 引脚并联 100nF 陶瓷电容到 GND提供明确低电平锚点驱动层初始化前强制执行两次地址探测先发 0x23再发 0x5C避免因上电时序导致的地址锁死协议层启用 BH1750 的“一次测量模式”0x20而非连续模式0x10防止 I²C 总线被长期占用影响 DHT22 唤醒。// I²C 地址自适应探测函数返回有效地址或 0xFF uint8_t bh1750_detect_address(I2C_HandleTypeDef *hi2c) { uint8_t addr_list[2] {0x23, 0x5C}; for (int i 0; i 2; i) { if (HAL_I2C_IsDeviceReady(hi2c, addr_list[i] 1, 3, 10) HAL_OK) { return addr_list[i]; } } return 0xFF; } // 初始化 BH1750带重试 uint8_t bh1750_init(I2C_HandleTypeDef *hi2c) { uint8_t addr bh1750_detect_address(hi2c); if (addr 0xFF) return 1; uint8_t cmd 0x20; // 一次测量模式1lx 分辨率 HAL_I2C_Master_Transmit(hi2c, (addr 1), cmd, 1, 100); HAL_Delay(180); // 等待转换完成最大 120ms 60ms 余量 return 0; }2.3 多传感器共存干扰DHT22 与 BH1750 的电源噪声耦合实测DHT22 在响应阶段会瞬间吸入 2.5mA 电流造成 VDD 纹波而 BH1750 对电源噪声极其敏感其内部 ADC 参考电压直连 VDD。我们在某实验室用示波器实测当 DHT22 开始传输数据时VDD 出现 80mV 峰峰值振荡持续 5ms此时 BH1750 读数偏差达 ±15%。这不是理论推测是真实波形截图见资源包power_noise_dht22_burst.png。应对策略不是加电容那么简单PCB 层面DHT22 电源走线单独打孔经 10μF 钽电容 100nF 陶瓷电容滤波后接入软件层面在 DHT22 通信完成后的 10ms 内禁止任何 I²C 操作包括 BH1750 读取调度层面将 DHT22 采集周期设为 2sBH1750 设为 1.5s错开峰值电流时刻。注意这个 10ms 禁止窗口不是拍脑袋定的。我们用逻辑分析仪抓取 DHT22 总线释放时刻上升沿到 VDD 恢复稳定纹波 5mV的时间实测均值为 8.3ms向上取整为 10ms留足安全裕量。3. 软件架构与状态机实现从裸机轮询到可扩展传感框架3.1 三层状态机设计分离“物理交互”、“数据解析”、“业务逻辑”很多初学者把 DHT22 读取写成一个大函数拉低→等待→读 40bit→校验→转 float→打印。一旦 BH1750 加进来代码立刻变成意大利面条。本方案采用物理层 / 解析层 / 应用层三层解耦物理层dht22_hal.c只负责 GPIO 电平控制、定时器触发、原始 bit 流捕获不涉及任何数据含义解析层dht22_parser.c接收 40bit 数组执行 CRC 校验、整数/小数分离、温度补偿DHT22 在 0–50℃ 外需查表修正输出struct dht22_data应用层sensor_mgr.c统一管理所有传感器生命周期定义采集策略如“DHT22 每 2s 一次BH1750 每 1.5s 一次两者错峰”提供sensor_get_temperature()等统一接口。这种分层让新增传感器如后续加 BMP280 气压只需实现新的xxx_hal.c和xxx_parser.csensor_mgr.c无需修改。3.2 DHT22 bit 流捕获用输入捕获替代 while 循环 polling传统做法用while(HAL_GPIO_ReadPin() GPIO_PIN_SET)等待上升沿但 F103 主频 72MHzwhile 循环本身就有 3–4 个周期抖动无法满足 DHT22 的 5μs 精度要求。我们改用TIM3 输入捕获将 DHT22 数据线接入 PA6TIM3_CH1配置为上升沿下降沿双触发// TIM3 输入捕获初始化PA6 __HAL_RCC_TIM3_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_6; GPIO_InitStruct.Mode GPIO_MODE_AF_INPUT; GPIO_InitStruct.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); htim3.Instance TIM3; htim3.Init.Prescaler 72 - 1; // 1MHz 计数 htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 0xFFFF; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_IC_Init(htim3); // 配置通道 1 为输入捕获双边沿 TIM_IC_InitTypeDef sConfigIC {0}; sConfigIC.ICPolarity TIM_INPUTCHANNELPOLARITY_BOTHEDGE; sConfigIC.ICSelection TIM_ICSELECTION_DIRECTTI; sConfigIC.ICPrescaler TIM_ICPSC_DIV1; sConfigIC.ICFilter 0; HAL_TIM_IC_ConfigChannel(htim3, sConfigIC, TIM_CHANNEL_1); HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_1); // 开启中断在HAL_TIM_IC_CaptureCallback()中记录每次边沿的计数值相邻值相减即得高/低电平宽度单位μs。40 个 bit 全部捕获完成后再统一解析——这比实时判断快得多且不受中断延迟影响。3.3 传感器协同调度基于滴答定时器的非阻塞任务队列不使用 RTOS但要实现多传感器定时采集必须避免HAL_Delay()阻塞。我们基于 SysTick 实现一个轻量级任务调度器// 任务结构体 typedef struct { void (*func)(void); // 任务函数指针 uint32_t period_ms; // 执行周期ms uint32_t last_exec_ms; // 上次执行时刻SysTick 计数 uint8_t enable; // 使能标志 } sensor_task_t; // 全局任务数组支持 5 个传感器 sensor_task_t g_sensor_tasks[5] { {.func dht22_read_task, .period_ms 2000, .enable 1}, {.func bh1750_read_task, .period_ms 1500, .enable 1}, // ... 其他任务 }; // SysTick 中断回调每 1ms 触发 void SysTick_Handler(void) { HAL_IncTick(); static uint32_t tick_count 0; tick_count; for (int i 0; i 5; i) { if (g_sensor_tasks[i].enable (tick_count - g_sensor_tasks[i].last_exec_ms) g_sensor_tasks[i].period_ms) { g_sensor_tasks[i].func(); g_sensor_tasks[i].last_exec_ms tick_count; } } }提示这个调度器看似简单但解决了两个关键问题一是避免HAL_Delay()导致的 CPU 空转二是让 DHT22 和 BH1750 的采集时刻天然错开2000ms vs 1500ms最小公倍数 6000ms即每 6 秒才重合一次大幅降低电源耦合概率。4. 关键避坑指南DHT22 与 BH1750 集成中最容易翻车的 5 个点4.1 现象DHT22 连续返回 0x8000-3276.8℃或 0x00000℃原因DHT22 的 40bit 数据中第 16–31bit 是湿度整数部分第 32–40bit 是温度小数部分。若 bit 流捕获错位 1bit整个数据帧解析全错CRC 必然失败但部分开发者忽略校验直接强转导致整数溢出。解决在dht22_parser.c中强制加入 CRC 校验分支校验失败时返回错误码DHT22_ERR_CRC上层应用必须处理该错误禁止默认值填充。4.2 现象BH1750 读数跳变剧烈如 120lx → 850lx → 30lx原因BH1750 的测量范围受MEASUREMENT_TIME寄存器影响。默认值为 0x00120ms 转换时间但若 I²C 通信受干扰导致该寄存器被意外写入 0xFF则转换时间变为 16ms灵敏度暴跌微弱光照下读数归零。解决每次读取前先写入0x00重置测量时间再发0x20启动转换形成“写配置 → 启动 → 延时 → 读取”四步闭环。4.3 现象系统运行 2 小时后 DHT22 完全无响应复位后恢复原因DHT22 在高温高湿环境80%RH下内部结露导致信号线漏电拉低能力下降。此时主机释放总线后DHT22 无法及时拉高主机误判为“无响应”反复重试直至耗尽资源。解决在dht22_hal.c中增加“软复位”逻辑若连续 3 次启动失败强制将 PA0 设置为输入模式 100ms再切回推挽输出利用内部上拉电阻强行抬高总线模拟硬件复位。4.4 现象串口打印的温湿度值正常但通过 UART 发送给 PC 时数据错乱原因DHT22 返回的float类型数据如 25.6℃经sprintf(buf, %.1f, temp)转字符串时若未分配足够缓冲区如只开 8 字节%.1f可能写入 7 字符-25.6\0导致栈溢出覆盖相邻变量。解决为每个传感器数据分配独立缓冲区char dht22_str[16]并在sprintf后显式添加\0截断dht22_str[15] \0;。4.5 现象多块板子中仅某一块 BH1750 始终读数为 0原因BH1750 的 I²C SDA/SCL 线在 PCB 上过长10cm且未包地形成天线效应拾取 MCU 时钟辐射噪声导致 ACK 信号被干扰。示波器实测该板 SCL 线存在 20MHz 谐波。解决在 BH1750 的 SDA/SCL 线靠近芯片端各串联一个 33Ω 电阻阻抗匹配并在两线之间跨接 10pF 电容滤除高频共模噪声——这是硬件整改的最小代价方案。5. 验收与答辩实战如何用 3 张图、2 个表、1 份日志证明你的系统“真能用”5.1 验收三要素可复现、可量化、可追溯很多同学答辩时只说“我测了数据是对的”但老师追问“怎么证明”就哑火。合格的嵌入式系统验收必须满足三个硬指标验收维度具体要求本方案交付物可复现在任意一块同型号开发板上按文档步骤操作100% 复现相同现象提供hardware_setup_guide.pdf含跳线设置、供电要求、电容位置标注可量化所有性能参数有实测数据支撑非理论值test_report.pdf包含 72 小时连续运行日志每 5 分钟采样、温湿度误差柱状图、BH1750 线性度拟合曲线R²≥0.998可追溯任意一行代码改动都能对应到具体问题或需求变更Git 提交记录按功能模块分类feat/dht22-timing, fix/bh1750-addr-detect提示答辩时不要一上来就演示“现在读数是 25.3℃”而是打开test_report.pdf翻到第 3 页的“72 小时稳定性测试”表格指着其中一行说“老师您看这是第 36 小时的数据温度 25.3℃湿度 48.2%与第 1 小时基准值偏差 0.5℃证明我们的电源滤波和时序控制是有效的。”5.2 答辩必问问题预演与应答话术Q1为什么不用 DHT11 而用 DHT22成本差多少→ 回答模板“DHT11 精度是 ±2℃/±5%RHDHT22 是 ±0.5℃/±2%RH。在课程设计‘环境监测’场景下若用于教室温控反馈0.5℃ 精度差异直接影响 PID 控制器的积分项累积速度。成本上 DHT22 单颗 2.3 元DHT11 1.1 元但节省的后期校准工时远超此差价。我们实测过用 DHT11 在 25℃ 下连续 10 次读数标准差为 1.2℃DHT22 为 0.18℃。”Q2BH1750 的光照单位是 lx但你们报告里写了‘相对照度’为什么→ 回答模板“因为 BH1750 的出厂校准系数存在 ±15% 偏差且受安装角度、外壳透光率影响。我们不做绝对标定那需要专业积分球而是定义‘相对照度’以正午晴天窗台读数为 100%其他场景按比例折算。这样既规避了硬件标定难题又保证了趋势判断的可靠性——比如窗帘关闭时读数从 100% 降到 12%这个变化量比绝对值 3200lx 更有意义。”Q3如果 DHT22 和 BH1750 同时故障系统怎么告警→ 回答模板“我们在sensor_mgr.c中实现了‘健康心跳’机制每个传感器任务执行后更新全局sensor_health_t结构体中的last_success_ms。主循环每 5 秒检查一次若任一传感器last_success_ms超过 30 秒未更新则触发蜂鸣器报警并通过 UART 发送ERR: DHT22_TIMEOUT字符串。这不是理论设计是真实压测结果——我们故意拔掉 DHT22 排针系统在 32 秒后准确报警。”5.3 一份让老师眼前一亮的“答辩附加页”别只交代码和报告附上一页 A4 纸大小的《传感器系统健康看板》用最简方式呈现核心指标指标当前值合格阈值状态说明DHT22 通信成功率99.8%≥95%✅连续 1000 次采集失败 2 次BH1750 读数稳定性σ0.7lx≤2.0lx✅100 次连续读数标准差电源纹波VDD12mVpp≤50mVpp✅示波器实测带载 50mA系统平均功耗8.3mA≤15mA✅电池供电可持续 120 小时这张表背后是 3 天的实测数据但它让老师 10 秒内抓住系统质量的关键证据。从那以后我每次做嵌入式项目都强制走一遍“验收三要素”自查能不能在别人板子上复现有没有数字支撑改动能不能回溯这比写一百行注释都管用。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。