ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

STM32F103同时驱动DHT22与BH1750:单总线与I2C实战

STM32F103同时驱动DHT22与BH1750:单总线与I2C实战 简介面向STM32F103嵌入式开发学习者该资料提供基于DHT22温湿度与BH1750光照强度传感器的完整驱动方案覆盖单总线时序解析、I2C通信、数据校验与数值转换并最终在TFTLCD显示屏上呈现适合需要实践传感器接口和LCD显示的开发者参考。压缩包共150个文件、3.08MB以37个h头文件和35个c源文件为核心配合uvprojx/uvoptx工程文件构成可直接编译的Keil项目同时包含hex/axf可执行文件便于烧录验证以及o/crf中间文件和htm、txt等说明文档目录结构清晰便于按模块查阅。目前已有8865人学习下载适合作为STM32外设驱动课程设计或毕业设计的参考资料。资料内附空气温湿度与光照强度相关源码与接线说明可对照理解DHT22的脉冲捕获和BH1750的I2C读取流程并借助ILI9341驱动库实现屏幕上实时显示有助于贯通硬件配置、协议解析与显示编程全链路。1. 一块F103同时驱动两个传感器时序敏感型与寄存器型驱动的实战组合DHT22和BH1750是环境数据采集场景里非常经典的一对传感器前者走单总线协议、对时序要求苛刻后者走I2C总线、以寄存器操作和数据换算为核心。把它们放在同一块STM32F103上驱动意味着要同时处理两种完全不同的通信风格这对刚接触嵌入式传感开发的人而言是一个能一次性摸清两种总线协议的实战组合。这套资源面向的是想把手写驱动能力补齐的开发者而不是只想抄个现成mian函数的初学者。很多初次尝试的人在DHT22这一关就会卡住单总线的位时序要求到达微妙级精度稍不留神就读回全0xFF或者偶发跳变的数据而BH1750看起来简单却容易在I2C地址、寄存器配置或数据掩码上翻车。这份资料把所有关键代码放在了一起。从GPIO初始化、微秒级延时到两个传感器的完整驱动函数再到主函数中的轮询逻辑基本覆盖了从裸芯片到稳定输出温湿度和光照数值的整条链路也顺带解决了传感器驱动学习里最费时间的部分——查手册、调时序、试错。2. 硬件连接与工程搭建从引脚分配到时钟配置2.1 引脚规划与电源设计DHT22的DATA引脚适合接在支持外部中断的引脚上这里选PA0它不仅具备GPIO输入输出切换能力还挂在EXTI0线上后续如果想改成中断方式捕获传感器响应信号硬件上不需要再做任何挪动。BH1750的SCL和SDA接到PB6、PB7这两个引脚在F103上对应I2C1的默认映射即使选择用普通GPIO软件模拟I2C也方便随时切换成硬件I2C方式。引脚整体规划为PA0接DHT22数据线PB6接BH1750的SCLPB7接BH1750的SDA电源统一使用3.3V。电源部分需要在传感器电源引脚旁边放置0.1uF去耦电容DHT22的数据线通常建议加上一个4.7k到10k的上拉电阻。BH1750如果使用模块大多已经板载了上拉电阻但使用裸芯片时就需要自查。上拉电阻过小会导致信号边沿变缓过大则可能让电平翻转不彻底尤其是长线传输时问题更明显。常见的做法是选4.7k作为默认值。2.2 工程模板与目录结构工程基础选用标准的库函数版本相比寄存器操作库函数版本在代码可读性和二次开发上手速度上都有明显优势。整个工程目录结构按功能模块拆分每个驱动文件对应一颗外部设备Project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── delay.h │ │ └── gpio.h │ └── Src/ │ ├── main.c │ ├── delay.c │ └── gpio.c ├── Drivers/ │ └── Sensor/ │ ├── dht22.c │ ├── dht22.h │ ├── bh1750.c │ └── bh1750.h └── Project.uvprojx传感器驱动独立放在Drivers/Sensor目录下不与应用层代码混在一起。这样在更换MCU平台或者把DHT22换成DHT11、把BH1750换成光敏电阻分压采集方案时只需要替换对应驱动文件主程序里的调用接口不变这种模块隔离在工程后期维护中会省很多事。2.3 时钟、GPIO与微秒级延时初始化STM32F103在默认状态下使用内部8MHz RC振荡器实际项目里几乎都会改为外部晶振并倍频到72MHz的系统主频。系统时钟配置完成后要分别使能GPIOA、GPIOB和AFIO时钟。DHT22的PA0在读取阶段需要作为浮空输入在发送启动信号阶段要作为推挽输出所以初始化代码里先按推挽输出模式配置。BH1750的两个引脚P B6、PB7在软件模拟I2C场景下需要配置为开漏输出并配合外部上拉电阻工作。void GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; /* 使能GPIOA、GPIOB和AFIO时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); /* PA0: DHT22 DATA初始化为推挽输出速度50MHz */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); /* PB6: I2C_SCL, PB7: I2C_SDA开漏输出需要外部上拉 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_SetBits(GPIOB, GPIO_Pin_6 | GPIO_Pin_7); }GPIO初始化里有两个容易忽视的细节。PA0用了50MHz的翻转速度这是为了保证DHT22启动信号的下降沿足够陡峭单总线协议对边沿时间有明确要求边沿太缓会导致传感器无法正确识别起始信号。PB6和PB7配置为开漏输出时置1和置0分别对应释放总线和拉低总线这是模拟I2C的基础。初始化结束前把两个引脚拉高保证总线空闲状态。延时函数是整个DHT22驱动的基石不能用DWT之外的方式解决。SysTick的tick中断在忙等场景下难以做到微秒级精确控制DWT的CYCCNT寄存器可以直接读取CPU周期数在72MHz主频下每个周期约13.9纳秒。实现微秒延时的核心是记录起始计数值然后循环等待目标差值。void Delay_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } void Delay_Us(uint32_t us) { uint32_t startTick DWT-CYCCNT; uint32_t waitTicks us * 72; uint32_t currentTick; while (1) { currentTick DWT-CYCCNT; if (currentTick - startTick waitTicks) { break; } } }Delay_Us(1)对应72个CPU周期这是DHT22时序操作的最小时间单位。这里使用无符号整型差值计算绕过了CYCCNT回绕的问题。如果主频抬高到72MHz以上waitTicks的计算系数也要同步调整。DWT方案的好处是不占用定时器资源也不依赖中断优先级设置读取时序时不会被其他中断扰乱。3. DHT22驱动核心单总线时序与数据帧解析3.1 单总线协议与启动时序DHT22的数据格式由40个二进制位组成按从高到低的顺序排列为16位湿度整数部分、16位湿度小数部分、16位温度整数部分、16位温度小数部分最后是8位校验和。校验和的算法是前四个字节相加取低8位与收到的校验字节比较。实际工程里湿度整数和小数直接拼合计算温度则要单独检查最高位是否为1为1时表示零下温度需要对数据做补码转换。启动一次通信由主机端发起典型流程是把数据线拉低至少1毫秒再释放传感器收到启动信号后会回应一个80微秒的低电平响应随后拉高80微秒然后开始连续发送40位数据。每次读取周期中每一位数据都由50微秒的低电平起始信号加上高电平持续时间构成高电平持续26到28微秒表示逻辑0持续70微秒表示逻辑1。uint8_t DHT22_Start(void) { uint8_t retry 0; /* 拉低至少1ms作为启动信号 */ GPIO_ResetBits(GPIOA, GPIO_Pin_0); Delay_Us(1000); /* 释放总线切换为输入模式 */ GPIO_SetBits(GPIOA, GPIO_Pin_0); DHT22_Pin_Mode_Input(); /* 等待传感器响应信号80us低电平 */ while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) Bit_SET) { if (retry 100) { return 1; } Delay_Us(1); } retry 0; while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) Bit_RESET) { if (retry 100) { return 1; } Delay_Us(1); } return 0; }这里有一个关键细节发送启动信号时必须先确保PA0处于推挽输出模式释放总线前切换为浮空输入模式因为后续所有数据位的读取都依赖输入模式。等待响应信号时使用了两段轮询循环第一段等待传感器把总线拉低第二段等待传感器释放总线两段都带有超时保护防止传感器不存在或损坏时程序陷入死循环。3.2 读位实现与数据帧组装判断逻辑0和逻辑1没有直接测量高电平总时长而是采用采样点判定法。每一位数据以50微秒低电平为起始在低电平结束后延时40微秒再读取一次引脚电平。如果此时引脚为高说明高电平持续时间超过了40微秒判定为逻辑1否则为逻辑0。采样点法的好处是不需要等待完整高电平周期结束整个数据帧的读取速度会明显快于逐一计时的方式也减少了定时器计时的累积误差。uint8_t DHT22_Read_Bit(void) { uint8_t bitValue 0; while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) Bit_SET); while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) Bit_RESET); Delay_Us(40); if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) Bit_SET) { bitValue 1; } while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) Bit_SET); return bitValue; }读位函数内部的三个轮询循环分别对应位起始低电平的结束等待、50微秒低电平的完整等待以及位结束后高电平的等待。这些等待都不能省否则下一次读取会错位。延时40微秒的位置决定了判定点如果把延时改成50微秒读到的值可能会全部偏向逻辑1这是实践中很容易出现的偏差源。读取完整数据帧时用一个无符号字符数组记录5个字节然后逐位填充。校验失败时返回一个明确错误码而不是把错误数据交给上层。DHT22数据更新的最小间隔是2秒读取频率高于这个值会拿到重复数据或触发传感器不响应。uint8_t DHT22_Read_Data(uint8_t data[5]) { uint8_t i, j; if (DHT22_Start() ! 0) { return 1; } for (j 0; j 5; j) { data[j] 0; for (i 0; i 8; i) { data[j] 1; data[j] | DHT22_Read_Bit(); } } /* 校验和验证 */ if ((uint8_t)(data[0] data[1] data[2] data[3]) ! data[4]) { return 2; } return 0; }3.3 温湿度换算与负温度处理DHT22的原始数据是16位无符号数。湿度换算相对简单直接把湿度整数与小数字节拼接后的值除以10得到带一位小数的百分比相对湿度。温度需要单独看最高位最高位为1表示零下温度实际值等于原始数据去除符号位后的补码取反加一再除以10。很多人在这一步直接把原始值当作有符号短整型强转结果零下温度显示成几千的乱码。float DHT22_Get_Temperature(uint8_t data[5]) { uint16_t rawTemp ((uint16_t)data[2] 8) | data[3]; float temp; if (rawTemp 0x8000) { rawTemp 0x7FFF; temp (float)(~rawTemp 1) / 10.0f; } else { temp (float)rawTemp / 10.0f; } return temp; }DHT22分辨率是0.1精度为正负0.5摄氏度量程覆盖零下40到零上80摄氏度。实际值可以转成浮点数直接使用或者放大十倍转为整数发送给串口或屏幕减少浮点运算在部分低端MCU上的性能开销。4. BH1750驱动核心I2C寄存器配置与光照强度合成4.1 设备地址与工作模式选择BH1750的I2C地址由ADDR引脚的电平决定。ADDR接低电平时地址为0x23接高电平时地址为0x5C使用7位地址表示时需要左移一位得到8位写地址。初始化阶段建议直接发送一次掉电命令再切换到连续测量模式保证传感器状态可预期。这里采用连续高分辨率模式寄存器值为0x10测量时间约120毫秒分辨率约为1勒克斯。另一种可选方案是连续高分辨率模式2寄存器值为0x20分辨率提升到0.5勒克斯适用于低照度场景但量程上限会相应减半。如果项目只需要一次性读取或者读取频率很低可以选择一次高分辨率模式。测量时间的差异在高频轮询环境下会影响响应节奏实际使用时优先推荐连续高分辨率模式兼顾精度、量程和实时性。4.2 软件模拟I2C时序函数与主设备读写软件模拟I2C可以避免硬件I2C外设的配置与中断适配问题代码便于移植对理解I2C协议本身也有帮助。起始信号定义为主设备在SCL高电平期间将SDA从高拉低结束信号定义为主设备在SCL高电平期间将SDA从低拉高。数据传输时SDA的电平变化必须在SCL低电平期间完成SCL高电平期间保持稳定这是I2C的基本约定。void I2C_Start(void) { SDA_HIGH(); SCL_HIGH(); Delay_Us(5); SDA_LOW(); Delay_Us(5); SCL_LOW(); } void I2C_Stop(void) { SDA_LOW(); SCL_HIGH(); Delay_Us(5); SDA_HIGH(); Delay_Us(5); } uint8_t I2C_Write_Byte(uint8_t byte) { uint8_t i, ack; for (i 0; i 8; i) { if (byte 0x80) { SDA_HIGH(); } else { SDA_LOW(); } byte 1; Delay_Us(2); SCL_HIGH(); Delay_Us(5); SCL_LOW(); } /* 释放SDA读取从设备ACK */ SDA_INPUT_MODE(); SCL_HIGH(); Delay_Us(5); ack SDA_READ(); SCL_LOW(); SDA_OUTPUT_MODE(); return ack; }I2C_Write_Byte每发送一个字节后都释放SDA线让从设备有机会在第九个时钟周期拉低SDA作为应答。如果设备地址错误引脚接触不良或从设备处于异常状态这一步就会读到高电平主设备需要使用超时计数处理否则模拟I2C的读写函数会因为等待ACK而永久卡死。每次操作之间延时2到5微秒这个尺度是针对72MHz主频和软件GPIO操作速度设置的改到其他主频时需要重新实验微调。读取BH1750测量结果时需要先发送设备地址加读标志位然后连续读取两个字节。第一个字节是光照强度的高8位第二个字节是低8位。读取第二个字节后主设备需要发送非应答信号终止后续数据传输。uint8_t I2C_Read_Byte(uint8_t ack) { uint8_t i, byte 0; SDA_INPUT_MODE(); for (i 0; i 8; i) { byte 1; SCL_HIGH(); Delay_Us(5); if (SDA_READ()) { byte | 0x01; } SCL_LOW(); Delay_Us(5); } SDA_OUTPUT_MODE(); if (ack) { SDA_LOW(); } else { SDA_HIGH(); } SCL_HIGH(); Delay_Us(5); SCL_LOW(); SDA_HIGH(); return byte; }读字节函数的ack参数控制的是主机对从设备的应答行为。读最后一个字节前传入0表示主机非应答这是I2C协议里相当重要的一步漏掉这个动作会导致后续通信错乱。软件模拟I2C的所有时序函数都在同一驱动文件里后续更换MCU时只需要调整GPIO相关宏定义和延时时间。4.3 光照强度数据合成与输出范围BH1750返回的16位原始值需要右移4位再除以1.2才能换算为实际的勒克斯数值。这个换算公式来自芯片数据手册这里的1.2对应连续高分辨率模式的修正系数模式2时系数会不同。光照强度的量程取决于当前模式连续高分辨率模式量程上限约为65535勒克斯宽动态范围模式可以覆盖到100000勒克斯以上但分辨率和精度相对较低。float BH1750_Get_Lux(void) { uint8_t buf[2]; uint16_t rawValue; float lux; I2C_Start(); I2C_Write_Byte(BH1750_ADDR_W); I2C_Write_Byte(0x10); /* 连续高分辨率模式 */ I2C_Stop(); /* 等待测量完成 */ Delay_Us(180000); I2C_Start(); I2C_Write_Byte(BH1750_ADDR_R); buf[0] I2C_Read_Byte(1); buf[1] I2C_Read_Byte(0); I2C_Stop(); rawValue ((uint16_t)buf[0] 8) | buf[1]; lux (float)(rawValue 4) / 1.2f; return lux; }先发送测量模式命令再等待约180毫秒然后重新发起I2C通信读取结果。这里需要特别注意的是每次读取前都必须重新发送地址和寄存器命令BH1750不会像EEPROM那样记住上一次的寄存器配置。光照强度在典型室内环境下通常在100到1000勒克斯之间直射阳光下可以达到数万勒克斯算法判断时需要预留充足的动态范围。5. 避坑与排查DHT22和BH1750调试中的典型问题5.1 现象DHT22读回全0xFF或者数值在正常值与0xFF之间随机跳变原因发送启动信号后主机没有正确切换到输入模式导致后续读取的引脚电平一直保持高电平读到全1另一个常见原因是在读位循环中没有做足够的超时保护一旦传感器未给出响应程序会不断在紧等循环中空转最终读到错误的电平组合。解决每次GPIO_SetBits释放总线之后立即切换PA0模式为浮空输入严格顺序不能颠倒。建议在DHT22_Start函数的两个等待循环中加入超时计数超时值设为100微秒级超过即返回错误码。从实际调试经验看把模式切换放在一个独立宏里封装可以避免遗漏。5.2 现象DHT22温度偶尔跳变湿度也有轻微波动原因读取40位数据期间发生了中断响应比如SysTick中断、串口中断或定时器中断介入单总线的位时序被打断后采样点判定就会出错。DHT22对时序的要求是位级别微秒精度中断延迟超过几十微秒就会破坏数据的完整性。解决在DHT22_Read_Data外层关中断读取完成后再恢复。使用__disable_irq()和__enable_irq()包裹整个读取过程而不是只包住一个读位函数。需要留意的是关闭中断时间大约在几毫秒量级在这个量级内关中断不会影响系统的整体实时性。若项目里有更高优先级的硬实时需求可以把DHT22读取放在RTOS的专用线程里并使用互斥锁。5.3 现象BH1750发送地址后收不到ACK原因地址错误最为常见。默认模块地址是0x23但部分模块的ADDR引脚默认被拉高实际地址变为0x5C。另一个原因是开漏输出配置后没有正常释放总线SDA一直处于低电平状态主设备永远无法与被控设备正常握手。解决先用逻辑分析仪或示波器观察SDA在发送第一个地址字节后第九个时钟周期的电平变化。如果没有硬件调试工具可以直接尝试把代码中的设备地址改成0x5C重新编译测试排除焊接或模块设计问题。同时确认PB7引脚的外部上拉电阻是否焊好。5.4 现象光照强度数值在正常和0之间切换信号线上有异常波形原因I2C信号线上的干扰通常来自电源。BH1750对电源噪声比DHT22更敏感特别是在线路较长或电源纹波较大的情况下I2C总线的上升沿会被噪声破坏导致读写错误。解决两个传感器与F103之间使用尽量短的双绞线电源线和信号线分开走线传感器的VCC和GND引脚就近放置10uF电解电容和0.1uF陶瓷电容形成低阻抗供电回路。如果仍不稳定需要将I2C速率降低软件模拟I2C的延时时间加倍后再观察。5.5 现象数据手册明明支持零下温度代码里却显示出一个很大的正数原因DHT22的原始温度数据带符号位在零下温度时最高位为1。直接把两个字节拼成无符号短整型再除10无法正确表示负数。解决按本章3.3节的做法先判断符号位再对去掉符号位的数据做补码转换最后加负号输出。建议在调试阶段把原始数据和换算后的数据同时打印出来方便确认是哪一步出了问题。6. 二次开发多路传感器数据帧整合与低功耗改造方向6.1 统一数据帧结构把两个传感器结果打包输出实际项目中很少只做两路传感器采集大概率会串口或无线模块把数据上报给上位机。两个驱动接口已经独立接下来的改造重点是把读取结果统一封装成一条标准数据帧。可以为温湿度、光照强度分别定义结构体再由一个调度函数负责按固定周期读取并组包。typedef struct { float temperature; float humidity; uint8_t dht22Error; } EnvData_t; typedef struct { float lux; uint8_t bh1750Error; } LightData_t; void Sensor_Collect(EnvData_t *env, LightData_t *light) { uint8_t dhtBuf[5]; env-dht22Error DHT22_Read_Data(dhtBuf); if (env-dht22Error 0) { env-humidity DHT22_Get_Humidity(dhtBuf); env-temperature DHT22_Get_Temperature(dhtBuf); } light-bh1750Error 0; light-lux BH1750_Get_Lux(); }在传感器出错时保留上一次有效值和错误码上位机可以据此判断数据是否可信而不是显示一个异常值。这种约定在很多采集系统的实际调试阶段能大幅减少排查时间。6.2 低功耗改造从连续测量到按需唤醒F103带两个传感器的常供电模式功耗并不高但如果是电池供电的节点设备就要考虑降低平均功耗。BH1750支持掉电模式发送0x00指令后进入待机状态需要测量时再发送连续测量命令测完立即回到掉电模式。DHT22没有掉电指令功耗受读取频率影响把采样周期从1秒拉长到5秒以上对功耗的影响非常直接。更进一步的方案是把主控从睡眠模式中唤醒再执行传感器读取DHT22的启动信号本身可以作为一段可控的GPIO输出操作读取完毕后主控重新进入睡眠。这个方案需要处理唤醒时间与传感器上电稳定时间的配合以我踩过坑的经验最稳妥的做法是唤醒后先等待100毫秒再启动DHT22测量不要毫无理由地信任第一次测量结果。6.3 移植到其他MCU驱动隔离与抽象层设计本次工程的驱动文件已经做了初步的硬件依赖分离。DHT22驱动里PA0引脚的置位与复位、GPIO模式切换被封装为宏定义BH1750的I2C时序操作也集中在几个宏中。移植到其他系列MCU时只需要修改宏定义和延时函数适配算法层不需要改动。#define DHT22_GPIO_PORT GPIOA #define DHT22_GPIO_PIN GPIO_Pin_0 #define DHT22_Pin_Mode_Output() { \ GPIO_InitTypeDef GPIO_InitStructure; \ GPIO_InitStructure.GPIO_Pin DHT22_GPIO_PIN; \ GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; \ GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; \ GPIO_Init(DHT22_GPIO_PORT, GPIO_InitStructure); \ }如果项目要频繁移植到不同平台可以先设计一个简单的传感器抽象接口包含Init、Read、Deinit三个函数指针业务层只面向这个接口编程。这样后续接入其它传感器时新增一个驱动文件即可。移植完成后务必把延时函数、引脚映射、上拉电阻三个点重新走一遍避免出现硬件层的隐性不匹配。6.4 验证方法日志输出与数据正确性交叉检查完成改造后需要一套验证流程推荐的方式是F103串口把原始值和换算值按固定格式打印到串口工具或日志面板重点关注几个异常时段。DHT22的温湿度可以用水银温度计或其它自备的基准传感器放在同一环境里参照对比BH1750可以在不同光照环境下观察数值量级是否合理开灯时数值上升用手遮挡时数值下降这种动态响应验证比看绝对数值更可靠。数据稳定后再检查长时间运行是否有偶发错误码如果每100次采样出现一两次错误不要急着改算法先看供电和线材接触。从那以后我每次给新板子调传感器都会强制走一遍完整的时序核对和日志验证流程确认硬件接线与软件时序都符合数据手册的规范才继续往下改。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表