ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F103驱动BH1750数字光强度传感器完整实现指南

STM32F103驱动BH1750数字光强度传感器完整实现指南 前阵子做一台上位机控制的环境监测设备需要实时采集环境光照数据板子用的是STM32F103C8T6传感器挑来挑去最后定了BH1750。这颗I2C接口的数字光强度传感器在低成本光照采集方案里几乎是默认选项模块几块钱内部自带16位ADC和光电二极管直接输出lux值不需要外部运放也不需要自己算曲线。如果你正在用STM32F103驱动BH1750或者打算在智能照明、室内光控、植物补光这类项目里加环境光传感器这篇文章应该能帮你把硬件接线、协议时序、驱动代码和调试坑一次讲清楚。下面所有代码和接线方式都是实测通过的但我也会把测试过程中遇到的几个隐蔽问题一并写出来方便你对照排查。1. 选型复盘为什么这颗传感器成了环境光采集的默认答案1.1 关键参数与实测边界先聊选型。市面上常见的光照采集方案有光敏电阻、光敏二极管、TSL2561、OPT3001、BH1750这些。光敏电阻便宜但输出是模拟量需要ADC配合而且不同批次阻值离散性大只能测个大概趋势没法给出准确的lux数值。TSL2561是老牌数字光照传感器精度也不差但封装偏小手工焊接比较考验焊工而且库文件维护没有BH1750这么普及。OPT3001性能更好价格也更高对大部分DIY和工业控制场景来说是性能溢出。BH1750的优势很直接ROHM出品I2C接口量程1到65535 lux分辨率最低能做到0.5 lux内置光电二极管加ADC输出已经是数字量MCU直接读就行。我实测下来的边界是在暗室环境下能稳定读出个位数lux的变化手机闪光灯直射时会到上万lux且不饱和这个范围覆盖绝大多数室内外光照场景。耗电方面连续高分辨率测量模式时典型电流0.2mA左右单次测量模式更低用电池供电的设备也能接受。1.2 与同价位方案的实际取舍实际项目里我曾对比过模块化光敏电阻板和BH1750模块同样测一个办公室窗边位置光敏电阻板配合STM32内部ADC只能判断亮了暗了而BH1750能读出具体是多少lux比如“早上10点窗边是8234 lux下午3点变成1900 lux”。这个差距在做智能窗帘、自动补光控制时非常关键因为阈值判断依赖的是具体数值而不是模糊的范围。还有一点容易被忽略BH1750模块普遍带电压转换电路很多卖家设计的模块可以直接接3.3V或5V供电I2C电平会被模块上的LDO和电平转换电路处理过接STM32F103不用再额外担心3.3V和5V逻辑电平兼容问题。当然如果自己画板子直接贴BH1750芯片供电就必须严格控制在3.3VI2C上拉也要按3.3V处理。2. 硬件接线与I2C总线准备2.1 引脚定义与最小系统连接BH1750模块上是6个引脚实际只用其中4个VCC、GND、SCL、SDA另外ADDR和DVI两个引脚需要额外注意。我用的STM32F103C8T6最小系统板接线如下表BH1750引脚功能接到STM32F103VCC电源正3.3VGND电源地GNDSCLI2C时钟PB6I2C1_SCLSDAI2C数据PB7I2C1_SDAADDR从机地址选择GND接地DVI未使用悬空PB6和PB7是STM32F103的I2C1默认引脚这样选的好处是如果用CubeMX生成代码直接勾选I2C1就能自动把这两个引脚配置好。如果板子上这两个脚被占用了也可以用软件I2C任意接两个GPIO后面会讲软件模拟的方式。2.2 上拉电阻、供电与地址确定I2C总线工作在开漏模式必须有上拉电阻才能输出高电平。大多数成品BH1750模块上已经板载了4.7k或10k上拉电阻插上杜邦线就能用自己画板子时记得在SCL和SDA上分别加一个4.7k电阻到3.3V不要漏。ADDR引脚的接法决定I2C从机地址这个必须明确。ADDR接地时7位地址是0x23换算成8位读写地址为0x46读操作是0x47ADDR接VCC时7位地址是0x5C8位地址是0xB8。我的模块默认把ADDR引脚引出短接GND最省事。如果你同时接两个BH1750可以通过给ADDR分配不同电平来区分两个传感器这样一条I2C总线上就能挂两个光强度计。提示不要将ADDR引脚悬空。悬空状态下部分模块的地址电平不稳定可能出现这次读到0x23、下次读到0x5C的现象排查起来非常浪费时间。供电也多说一句虽然部分模块标注支持3V到5V但BH1750本体工作电压是3.3V左右如果模块上带LDO接5V供电没问题如果是不带LDO的裸板接5V可能会烧芯片。买模块时看一眼描述或者直接量一下VCC引脚的电容耐压拿不准就统一用3.3V供电。3. BH1750协议细节指令、模式与测量时序3.1 指令集与操作码速查BH1750的指令非常简单主机通过I2C向传感器写入一个字节的操作码来下发命令不需要寄存器地址。常用的操作码整理成下表操作码命令含义0x00掉电模式0x01上电模式等待测量指令0x02重置寄存器0x10连续H分辨率模式分辨率1 lux测量时间约120ms0x11连续H分辨率模式2分辨率0.5 lux0x13连续L分辨率模式分辨率4 lux测量时间约16ms0x20一次H分辨率模式0x21一次H分辨率模式20x23一次L分辨率模式看到这里你会发现这个传感器没有复杂的寄存器映射表核心交互就是下发模式命令、等待测量完成、读取数据。这也是它容易上手的原因。3.2 连续模式与一次模式的选择连续H分辨率模式0x10是最常用的传感器会每隔约120ms自动更新一次测量结果主机任何时刻发起读操作都能拿到最新数据。适合做实时监测类项目比如智能灯光调节环境光一变数据立刻跟着变。一次模式0x20、0x21、0x23则每发一次命令只测一次测量完成后传感器自动进入掉电状态适合低功耗应用。比如电池供电的温湿度光照记录仪每10分钟醒过来发一条0x23命令等16ms读一次数据然后又睡过去功耗能压得很低。我个人的习惯是开发调试阶段用连续H分辨率模式方便随时观察数据变化正式做低功耗产品时切换成一次L分辨率模式配合外部定时唤醒MCU。3.3 测量时间、数据读取与灵敏度调整测量时间直接影响数据有效性H分辨率模式典型测量时间是120ms最大到180msL分辨率模式典型16ms最大24ms。代码里发完测量命令后至少要延时超过这个时间再读数据否则读到的是上一次测量的旧值甚至全零。数据读取是标准的I2C读操作一次读2个字节高位在前。原始寄存器值是一个无符号16位整数换算成光照度的公式是// 连续H分辨率模式操作码0x10 float lux (float)((buff[0] 8) | buff[1]) / 1.2f; // 连续H分辨率模式2操作码0x11 float lux (float)((buff[0] 8) | buff[1]) / 2.4f; // 连续L分辨率模式操作码0x13 float lux (float)((buff[0] 8) | buff[1]) / 1.2f;为什么H模式要除以1.2这是ROHM手册里给定的常数和内部积分时间、光电二极管灵敏度相关。H模式2的分辨率是0.5 lux但最大量程会缩小到约32640 lux除以2.4才能还原真实照度。BH1750还支持通过写测量时间寄存器MTreg调整灵敏度操作码是0x40加上高5位、0x60加上低3位默认值是69。调高MTreg可以提升低光环境下的分辨率代价是测量时间变长。多数项目不需要动它保持默认即可。4. 基于STM32F103的驱动实现从初始化到读lux值4.1 基于HAL库的I2C外设启动我这次用的是STM32CubeMX生成工程MCU选STM32F103C8T6I2C1配置成标准模式时钟速度100kHz。BH1750不是高速设备100kHz完全够把速率调太高反而容易在长线上出现信号质量问题。关键初始化配置如下void MX_I2C1_Init(void) { hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 100000; hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 0; hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 0; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(hi2c1) ! HAL_OK) { Error_Handler(); } }GPIO方面PB6和PB7由CubeMX自动配置为开漏输出模式并使能内部上拉。使用HAL库时要注意HAL_I2C_Master_Transmit函数内部已经封装了起始、停止、ACK等时序处理不用像操作标准库寄存器那样手动清标志位。4.2 写命令与读光强的核心代码BH1750的驱动核心就两个操作向传感器写入命令字、从传感器读取2字节测量数据。HAL库版本代码如下#include bh1750.h #include main.h extern I2C_HandleTypeDef hi2c1; #define BH1750_I2C hi2c1 #define BH1750_ADDR 0x46 // ADDR接地8位地址0x23 1 #define BH1750_POWER_ON 0x01 #define BH1750_CMD_CONT_H 0x10 // 连续高分辨率模式精度1 lux #define BH1750_CMD_RESET 0x02 void BH1750_WriteCmd(uint8_t cmd) { HAL_I2C_Master_Transmit(BH1750_I2C, BH1750_ADDR, cmd, 1, 100); } void BH1750_ReadData(uint8_t *buff, uint8_t len) { HAL_I2C_Master_Receive(BH1750_I2C, BH1750_ADDR, buff, len, 100); } float BH1750_GetLux(void) { uint8_t buff[2] {0}; uint16_t raw 0; float lux 0.0f; // 先上电再启动连续H分辨率测量 BH1750_WriteCmd(BH1750_POWER_ON); HAL_Delay(10); BH1750_WriteCmd(BH1750_CMD_CONT_H); HAL_Delay(180); // 等待测量完成留出余量 BH1750_ReadData(buff, 2); raw (buff[0] 8) | buff[1]; lux (float)raw / 1.2f; return lux; }如果你在FreeRTOS环境里用这段代码HAL_Delay要换成osDelay或者vTaskDelay避免阻塞任务调度。另外连续模式下第二次调用BH1750_GetLux时其实可以直接跳过重新下发0x10命令只等待180ms再读数据但我习惯保留上电和模式命令保证传感器状态始终确定。4.3 软件I2C兜底方案与适用场景STM32F103的硬件I2C外设因为兼容性问题和部分库实现不够健壮偶尔会出现总线卡死的现象最典型的症状是I2C通信时SCL正常翻转但SDA拉不低或者HAL_I2C_Master_Transmit一直返回超时错误。这个问题在F1系列上尤为常见网上争论也最多。如果遇到这种情况我的建议是直接换成软件模拟I2C一了百了。软件I2C的原理很简单用两个GPIO分别模拟SCL时钟和SDA数据按时序翻转电平。BH1750是慢速从机GPIO模拟完全够用。核心代码片段如下#define SCL_GPIO_PORT GPIOB #define SCL_PIN GPIO_PIN_6 #define SDA_GPIO_PORT GPIOB #define SDA_PIN GPIO_PIN_7 #define SCL_H() HAL_GPIO_WritePin(SCL_GPIO_PORT, SCL_PIN, GPIO_PIN_SET) #define SCL_L() HAL_GPIO_WritePin(SCL_GPIO_PORT, SCL_PIN, GPIO_PIN_RESET) #define SDA_H() HAL_GPIO_WritePin(SDA_GPIO_PORT, SDA_PIN, GPIO_PIN_SET) #define SDA_L() HAL_GPIO_WritePin(SDA_GPIO_PORT, SDA_PIN, GPIO_PIN_RESET) #define SDA_READ() HAL_GPIO_ReadPin(SDA_GPIO_PORT, SDA_PIN) void I2C_Start(void) { SDA_H(); SCL_H(); delay_us(5); SDA_L(); delay_us(5); SCL_L(); } void I2C_Stop(void) { SDA_L(); SCL_H(); delay_us(5); SDA_H(); delay_us(5); } uint8_t I2C_SendByte(uint8_t byte) { uint8_t i; for (i 0; i 8; i) { if (byte 0x80) SDA_H(); else SDA_L(); byte 1; SCL_H(); delay_us(5); SCL_L(); delay_us(5); } // 接收ACK SDA_H(); // 释放SDA SCL_H(); delay_us(5); uint8_t ack SDA_READ(); SCL_L(); delay_us(5); return ack; // 返回从机应答状态 } uint8_t I2C_RecvByte(void) { uint8_t i, byte 0; SDA_H(); for (i 0; i 8; i) { byte 1; SCL_H(); delay_us(5); if (SDA_READ()) byte | 0x01; SCL_L(); delay_us(5); } // 主机发送ACK准备接收下一字节 SDA_L(); SCL_H(); delay_us(5); SCL_L(); SDA_H(); return byte; }用这套软件I2CBH1750发送地址和数据的流程就完全可控任何GPIO都能用调试起来也很直观。不过GPIO模拟的速度肯定不如硬件I2C但在100kHz这个量级上BH1750完全不受影响。5. 实测数据与调试过程中踩过的隐蔽坑5.1 真实环境下的数据表现程序跑通后我用手机上的照度计App和几组已知光源做了对比测试数据如下表测试环境手机照度AppBH1750读数室内LED灯正常照明约340 lux约352 lux无窗户的暗室约20 lux约23 lux手机闪光灯近距离直射约12500 lux约11800 lux阳光透过窗户直射桌面约9800 lux约8900 lux整体趋势和绝对值都对得上误差主要来自传感器光谱响应和App标定差异实际项目里用这个精度做灯光控制和阈值报警完全没问题。5.2 坑一硬件I2C总线卡死与复位策略我在调试时遇到过一次很典型的硬件I2C卡死程序跑一段时间后HAL_I2C_Master_Transmit一直返回HAL_BUSYSCL和SDA被某个状态卡住导致整个传感器读取流程停摆。排查过程是先量波形用示波器看SCL有连续脉冲但SDA一直低电平说明总线状态机已经混乱。解决办法有两个方向。第一在每次通信前尝试复位置位硬件I2Cvoid I2C1_Bus_Reset(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_I2C_DISABLE(hi2c1); // 把PB6、PB7临时配置为普通开漏输出手动拉出几个时钟周期 GPIO_InitStruct.Pin GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET); for (int i 0; i 10; i) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); HAL_Delay(1); } MX_I2C1_Init(); }第二也是我更推荐的直接把通信层改成软件I2C彻底绕开F1硬件I2C这个老毛病。BH1750的数据量很小软件I2C不会成为性能瓶颈。5.3 坑二读回数据全零或固定值不变这个坑出现在第一次上板测试时我发完0x10命令后只等了50ms就去读数据结果读回来两个字节全是0。后来看手册才发现H分辨率模式需要最长180ms才能完成一次测量延时不足就相当于数据还没准备好读回来的当然是空数据。把延时调整到180ms后数据立刻正常了。另外注意一种情况如果模块一直在连续H分辨率模式运行你在循环里每次读数据前都重新发0x10命令会把内部测量周期不断重置导致读数始终停留在0。正确做法是初始化时发一次0x10之后循环里只需要等待180ms再读不需要反复写模式命令。5.4 坑三ADDR引脚悬空导致的地址漂移有次客户反馈说他的板子上BH1750读数时有时无查到最后发现是模块上的ADDR引脚什么也没接。BH1750的ADDR输入虽然是逻辑电平但悬空时读到的电位不稳定偶尔会被识别成高电平导致I2C地址在0x23和0x5C之间跳变通信自然时好时坏。解决方式就是明确把ADDR接到GND或VCC不要悬空。如果已经发生了地址漂移可以在初始化时做一次地址探测先尝试向0x46发上电命令再尝试0xB8两个地址只有其中一个能收到ACK这样可以动态识别当前传感器的实际地址。5.5 坑四换算公式与MTreg的配合问题如果你的代码里设置过MTreg就不能再用固定的除以1.2。当MTreg从默认值69改成其他值时测量结果的换算关系要乘以一个比例系数实际lux 原始寄存器值 / 1.2 x 69 / MTreg。比如把MTreg改成138实际lux 原始寄存器值 / 2.4。很多人在低光环境下为了提高灵敏度改了MTreg却忘了同步修改换算公式导致读数整体偏小一倍。我的建议是除非项目对低光照度有特殊要求否则不修改MTreg全部用默认值这样换算公式最简单也最不容易出错。6. 驱动代码的跨平台移植思路与长期运行经验6.1 移植到其他MCU平台的关键点如果后面要把这套驱动移植到ESP32、GD32或者STM32G4系列上核心逻辑不用动只需要把底层I2C读写函数替换成对应平台API。比如ESP-IDF环境下I2C读WriteCmd函数会变成// ESP32示例仅示意 static esp_err_t bh1750_write_cmd(uint8_t cmd) { i2c_cmd_handle_t cmd_handle i2c_cmd_link_create(); i2c_master_start(cmd_handle); i2c_master_write_byte(cmd_handle, (BH1750_ADDR_7BIT 1) | I2C_MASTER_WRITE, true); i2c_master_write_byte(cmd_handle, cmd, true); i2c_master_stop(cmd_handle); esp_err_t ret i2c_master_cmd_begin(I2C_PORT, cmd_handle, pdMS_TO_TICKS(100)); i2c_cmd_link_delete(cmd_handle); return ret; }驱动层的核心就是地址、指令、测量时间这三个常量只要这三点保持一致上层调用完全不用改。6.2 数据滤波与多次采样BH1750本身输出的数据已经比较稳定但日光灯和LED驱动电源存在频闪实测读数在小范围内会有几十lux的跳动。如果需要平滑的数据可以做一个简单的滑动平均滤波取最近5次采样的平均值作为有效值#define SAMPLE_COUNT 5 float lux_history[SAMPLE_COUNT] {0}; uint8_t sample_index 0; float lux_sum 0.0f; uint8_t sample_ready 0; float BH1750_GetLuxSmoothed(void) { float lux BH1750_GetLux(); lux_sum - lux_history[sample_index]; lux_history[sample_index] lux; lux_sum lux; sample_index (sample_index 1) % SAMPLE_COUNT; if (sample_index 0) sample_ready 1; return (sample_ready) ? (lux_sum / SAMPLE_COUNT) : lux; }这个简单滤波器适合光照缓慢变化的场景响应时间大概在1秒左右已经够用了。6.3 长期运行稳定性说明这套驱动在我这个项目里已经连续运行了几个月24小时不断电没有出现数据漂移或者通信丢失的问题。除了有一次因为接线松动导致SDA接触不良读到的数据时好时坏其他时间都稳定输出。硬件上建议把模块和MCU之间的杜邦线尽量缩短或者直接用排针和短跳线连接减少接触电阻和电磁干扰。我个人的习惯是驱动代码里所有延时都用宏定义或常量管理比如BH1750_MEASURE_TIME_H_MODE定义为180方便以后调参读写函数增加返回值判断一旦HAL_I2C_Master_Transmit返回非HAL_OK就执行一次I2C总线复位再重试。这样即使碰到偶发总线异常程序也能自动恢复不会死等。最后再分享一个小技巧如果是在不太明亮的房间里调试没有专业照度计可以直接拿手机下载一个照度App作为参考虽然绝对值不一定很准但用来验证BH1750读数的数量级和变化趋势完全够用。我就是靠这个方法完成初步校准的后续需要精确标定时再考虑用标准光源或专业照度计。
返回列表