
简介面向 STM32 开发者的 BH1750 光照强度传感器库函数压缩包主要用于 GY-30 模块的环境光照精确测量提供完整的 I2C 通信驱动实现可直接降低从零编写底层驱动的时间成本。资源包共包含 3 个文件其中有头文件用于接口声明与宏定义C 源码文件用于函数实现另配一个 7z 压缩文件便于部署整体仅 6KB非常轻量适合快速移植到各类 STM32 工程。库函数覆盖初始化、连续/一次性测量模式切换、Lux 数据读取与错误检查等核心流程开发者只需调用对应 API 即可获得光照强度数值。该资源尤其适合嵌入式入门者理解 BH1750 的 I2C 时序也可用于智能家居、农业监测、工业自动化等需要环境光感知的快速原型开发。目前已有 3376 人学习下载是学习 STM32 传感器驱动和 I2C 通信的实用参考。1. 为什么选BH1750/GY-30光照采集的第一课做智能台灯、智能窗帘、鱼缸补光灯这类项目最基础的需求就是“知道环境光现在有多亮”。早年我用光敏电阻加ADC的方案便宜是便宜一个电阻加一个电容成本几毛钱但实际调试时真的会怀疑人生同型号电阻不同批次差异大温度一变读数就飘输出曲线非线性低照度段想做得细腻一点光标定表就能写满一页。后来项目里统一换成BH1750搭配GY-30模块这个决定帮我省掉了大把调试时间。BH1750是罗姆推出的一颗数字环境光传感器测量范围1到65535勒克斯内部集成了光电二极管、积分放大器和16位ADC直接通过I2C对外输出数字量。对STM32来说它就是一个小I2C从设备两根线就能通信读出来的16位原始值经过简单换算就是光照强度。这也是为什么你在各大论坛搜STM32光照传感器排在前面的方案基本全是这一套组合。这篇文章不打算只甩一份代码让你复制而是把选型理由、硬件接线、I2C协议、库函数设计、实测校准整个链路讲明白。刚学完STM32基础外设准备做毕设、或者打算在量产项目里引入光照检测的工程师都可以直接按这篇文章的步骤操作重点是把底层原理讲透踩过的坑一并说清楚。1.1 光照传感器选型BH1750凭什么成为标配我手头这几类光照采集方案都试过简单对比一下优缺点立现光敏电阻成本极低但一致性差、非线性严重受温度影响大需要自己搭放大电路和标定表。TSL2561当年的经典款性能确实强但早已停产市面上大量拆机片一不小心就买到翻新或有暗病的。OPT3001、MAX44009指标优秀很多高端产品在用但单颗价格偏高中文资料也少对新手不友好。BH1750量程宽、价格便宜、I2C输出、模块化成熟买一块GY-30模块几块钱就能跑起来。BH1750内部结构其实很有意思芯片里有一个光电二极管把光信号转成电流再经过积分放大、ADC转换最后通过I2C输出。对MCU来说完全不需要关心模拟前端打开通信接口就能拿数据。这颗芯片的供电范围是2.4V到3.6V而市面上的GY-30模块通常已经帮你布局好上拉电阻或者简单稳压电路所以也可以直接兼容5V供电输入。1.2 GY-30模块上藏着哪些“基础设施”很多教程习惯把GY-30当成黑盒子但我觉得新手还是应该拆开看一眼。模块背面除了BH1750芯片一般还有两颗4.7k欧姆上拉电阻把SDA和SCL拉到VCC。这个设计意味着接STM32的IO口时不用额外配置内部上拉省掉不少麻烦。模块上的ADDR引脚同样关键它是BH1750的地址选择脚接地或者悬空时从机地址是0x23接高电平时地址变成0x5C。绝大多数GY-30模块出厂时已经把ADDR拉低了所以大家默认都是0x23。如果你自己画PCB用裸片这个引脚一定不能悬空不处理不然后期排查地址问题非常痛苦。还有一个容易忽略的点不同店铺卖的GY-30模块电路并不完全一样有的带稳压有的不带。买回来先看背面丝印或者直接问卖家5V是否安全。我见过有人把5V直接灌进没有稳压的模块芯片当场冒烟的那个教训一直记到现在。2. 硬件接线与I2C基础从地址0x23说起2.1 GY-30引脚定义与STM32接线表GY-30模块最常用的有5个引脚VCC、GND、SCL、SDA、ADDR。接线本身没什么悬念真正需要操心的是电平匹配和引脚的复用冲突。GY-30引脚功能推荐接法VCC电源正极3.3V或5V确认模块是否带稳压GND电源地GNDSCLI2C时钟线PB6软件I2C可随意SDAI2C数据线PB7软件I2C可随意ADDR地址选择接地默认从机地址0x23我用的是STM32F103C8T6软件I2C方式SCL和SDA接PB6、PB7。选软件模拟方式的好处是引脚几乎不受限制换板子只需要改宏定义。如果你用硬件I2C就要避开和其他外设的复用冲突比如PB6、PB7在部分板卡上可能和定时器或者串口调试功能冲突配置前先看原理图。2.2 I2C时序基础起始、停止、应答,一眼就能看懂I2C总线像一条只有两根线的公共走廊所有设备都挂在上面。SCL是时钟线SDA是数据线两根线都是开漏结构靠上拉电阻维持高电平。任何设备要发言先把总线拉低开始通信。三条核心时序规则记住就够了起始条件SCL保持高电平时SDA从高电平跳变到低电平表示总线开始通信。停止条件SCL保持高电平时SDA从低电平跳变到高电平表示通信结束。数据有效性SCL高电平期间SDA上的电平必须保持稳定只有在SCL低电平期间SDA才允许发生跳变。应答位更像快递签收。主机每发完一个字节会释放SDA从设备如果正常接收就把SDA拉低一个时钟周期表示“我收到了”如果没拉低说明从设备不在线或者忙主机该考虑重新初始化或者查接线了。这段时序原理理解了后边库函数里每一个delay、每一次拉高拉低你都能看得明明白白而不是机械复制。2.3 从机地址0x23的来龙去脉BH1750的7位从机地址由ADDR引脚决定。ADDR接地时地址是0x23接高电平时是0x5C。这里有个新手最容易踩的坑I2C总线上实际发送的地址字节并不是0x23本身而是“7位地址左移一位再加上读写方向位”。所以初始化发送地址时写操作要用0x460x23 1读操作要用0x470x46或上1。很多人在网上找的例程里看到0x46、0x47疑惑为什么不直接用0x23其实就是这个原因。如果你在代码里发0x23当从机地址设备根本不会应答读回来的数据永远是0xFF或者0x00。我调试过帮别人看的工程折腾半天最后就是这里错了软件上看不出任何明显问题用逻辑分析仪一抓波形就露馅。3. 指令集与测量模式先把状态机摸透3.1 常用指令字节与测量模式表BH1750的操作逻辑很简单主机先发从机写地址再发一个指令字节芯片就会进入对应状态。常用的指令如下指令字节功能适用场景0x00掉电模式低功耗休眠0x01上电模式等待测量命令0x07复位重置数据寄存器0x10连续高分辨率模式实时性要求高分辨率1 lx0x11连续高分辨率模式2光线较暗时提高分辨率0x13连续低分辨率模式快速扫描分辨率4 lx0x20一次高分辨率模式单次测量后自动掉电0x23一次低分辨率模式电池供电、低频采集选模式的时候要根据实际场景来。普通室内环境连续高分辨率模式0x10就足够用夜间小夜灯那种低照度场景可以考虑高分辨率模式2如果做的是农业大棚这种需要高频采样的设备低分辨率模式虽然精度低一点但转换时间短数据更新速度高一截。3.2 连续测量还是单次测量功耗与实时性的博弈连续测量模式下芯片会按照设定的分辨率定期更新数据寄存器MCU随时启动读操作都能拿到最新值。适合智能台灯这种需要实时响应的场景MCU读取无脑不需要额外状态管理。单次测量模式则更适合电池供电的采集设备。发一次测量指令后芯片完成转换并保存数据然后自动进入掉电状态等待下一次测量。从功耗角度这比连续模式节能不少代价是每次读数据前都要启动一次测量代码里要手动加延时等待转换完成。转换时间这里一定要说清楚高分辨率模式大约120ms低分辨率模式大约16ms。我见过有人连续模式下发完初始化马上读数据读到的全是同一个旧值还以为芯片坏了。其实只要测量还没完成寄存器里的数据就是上一次的转换结果这一点在产品逻辑设计时要留足余量。3.3 测量时间寄存器进阶灵敏度调节BH1750还支持通过测量时间寄存器来调整灵敏度指令前缀是0x40和0x60。默认测量时间是69毫秒级别对应灵敏度系数1数据换算时除以1.2得到勒克斯。如果你调高了测量时间灵敏度会提升可以测到更暗的环境光但同时转换时间变长高光照下还可能提前饱和。这个功能在透明外壳产品或者需要极低照度检测的场景里很有用。普通项目用默认即可不必一上来就改寄存器免得换算关系自己都绕晕。4. 库函数落地从GPIO模拟I2C到lux输出4.1 为什么我优先用软件I2CSTM32F1系列的硬件I2C外设在社区里名声一直不太好网上有大量“硬件I2C卡死”的吐槽。虽然有相当一部分是配置问题但对新手来说排查成本实在太高好不容易跑起来的工程改一个中断优先级就可能复现死锁。软件I2C用GPIO模拟时序不依赖芯片外设Bug换板子也灵活出问题还能用逻辑分析仪直接量波形。光照传感器是低速设备软件模拟完全跑得动我没理由不选它。当然如果你用的是G0、L4、F4这些新系列硬件I2C已经相当稳定又需要低功耗中断唤醒那走硬件I2C加中断是更合理的选择。关键是知道自己为什么选而不是盲目抄。4.2 GPIO配置与底层I2C时序函数用标准库写的话GPIO配置很简单。注意要配置成开漏输出配合模块上的上拉电阻才能实现正确的线与逻辑。下面这段是基础配置#define I2C_SCL_PIN GPIO_Pin_6 #define I2C_SDA_PIN GPIO_Pin_7 #define I2C_GPIO_PORT GPIOB void I2C_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin I2C_SCL_PIN | I2C_SDA_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; // 开漏输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(I2C_GPIO_PORT, GPIO_InitStructure); GPIO_SetBits(I2C_GPIO_PORT, I2C_SCL_PIN | I2C_SDA_PIN); }底层时序函数里SDA是需要双向切换的。发送数据时配置成输出模式读取从设备应答和数据时要切成输入模式。我用标准库这样处理void I2C_SDA_IN(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin I2C_SDA_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(I2C_GPIO_PORT, GPIO_InitStructure); } void I2C_SDA_OUT(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin I2C_SDA_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(I2C_GPIO_PORT, GPIO_InitStructure); }起始、停止、发送字节、接收字节这四个函数是软件I2C的基石。起始条件就是“SCL高电平时拉低SDA”停止条件相反void I2C_Start(void) { I2C_SDA_OUT(); I2C_SDA_H(); I2C_SCL_H(); delay_us(5); I2C_SDA_L(); delay_us(5); I2C_SCL_L(); } void I2C_Stop(void) { I2C_SDA_OUT(); I2C_SDA_L(); I2C_SCL_H(); delay_us(5); I2C_SDA_H(); delay_us(5); }发送一个字节的完整过程是循环8次每次把最高位移到SDA上然后拉高SCL让从设备采样再拉低SCL为下一位做准备。8位发完释放SDA拉高SCL读从设备的应答位uint8_t I2C_SendByte(uint8_t data) { uint8_t i; uint8_t ack; I2C_SDA_OUT(); for (i 0; i 8; i) { if (data 0x80) I2C_SDA_H(); else I2C_SDA_L(); data 1; I2C_SCL_H(); delay_us(2); I2C_SCL_L(); delay_us(2); } I2C_SDA_IN(); // 释放SDA让从设备控制应答位 I2C_SCL_H(); delay_us(2); ack I2C_SDA_READ(); // 0表示从设备应答1表示无应答 I2C_SCL_L(); I2C_SDA_OUT(); return ack; }接收一个字节则是把过程反过来先切换SDA为输入然后SCL拉高一个周期读取SDA电平放进变量里uint8_t I2C_RecvByte(void) { uint8_t i, data 0; I2C_SDA_IN(); for (i 0; i 8; i) { I2C_SCL_H(); delay_us(2); data 1; if (I2C_SDA_READ()) data | 0x01; I2C_SCL_L(); delay_us(2); } return data; }延时函数的时长取决于你的主频和优化等级。用SysTick做一个简单的delay_usI2C时钟控制在100kHz到400kHz之间都行。这些底层函数背后其实都是在严格执行I2C协议只是把硬件控制器的活交给GPIO来做而已。4.3 BH1750驱动层初始化、写指令、读光强底层I2C就绪之后BH1750的驱动就水到渠成。我把驱动拆成三个函数初始化、写指令、读数据各自职责单一也方便上层调用。#define BH1750_WRITE_ADDR 0x46 // 0x23 1 #define BH1750_READ_ADDR 0x47 // (0x23 1) | 0x01 #define BH1750_POWER_ON 0x01 #define BH1750_POWER_DOWN 0x00 #define BH1750_RESET 0x07 #define BH1750_CONT_H_RES 0x10 #define BH1750_ONE_H_RES 0x20 void BH1750_Init(void) { I2C_GPIO_Config(); I2C_Start(); I2C_SendByte(BH1750_WRITE_ADDR); I2C_SendByte(BH1750_POWER_ON); I2C_Stop(); } void BH1750_WriteCmd(uint8_t cmd) { I2C_Start(); I2C_SendByte(BH1750_WRITE_ADDR); I2C_SendByte(cmd); I2C_Stop(); } uint16_t BH1750_ReadLuxRaw(void) { uint8_t buf[2]; uint16_t raw; I2C_Start(); I2C_SendByte(BH1750_READ_ADDR); buf[0] I2C_RecvByte(); I2C_SendAck(1); // 第一个字节后发ACK buf[1] I2C_RecvByte(); I2C_SendAck(0); // 最后一个字节后发NACK I2C_Stop(); raw ((uint16_t)buf[0] 8) | buf[1]; return raw; }驱动在使用时先在主循环里发一次单次测量指令等120ms以上再去读原始值。这里有两个小陷阱要提醒第一I2C_SendByte返回的ack值建议在调试时打印出来看一眼如果一直是1说明从设备无应答赶紧查接线和地址别一头扎进算法里找问题。第二读取数据时分别对应接收两个字节。I2C标准规定收到倒数第一个字节时主机要回ACK收到最后一个字节时主机要回NACK然后拉停止信号。NACK的作用是通知从设备“后面不用再发了”如果这里处理不对从设备可能一直占住总线。4.4 数据换算从16位原始值到勒克斯BH1750返回的16位原始值并不是直接的勒克斯数值要根据分辨率系数换算。默认测量时间下高分辨率模式的换算关系是float lux (float)raw / 1.2f;这个系数由芯片内部积分时间和灵敏度决定。如果你是低分辨率模式分辨率是4 lx/LSB换算就变成float lux (float)raw / 1.2f;别疑惑为什么一样低分辨率模式内部已经把原始值做了4倍缩放处理。真实情况是不同模式的LSB差异数据手册里写得很清楚我这里给的说法是基于常用默认配置。如果调了测量时间寄存器那换算系数还要跟着变。我建议在工程里把“原始值”和“勒克斯”分开存储原始值用来排查问题换算后的浮点值用来做业务逻辑。5. 实测校准与误码排查别让读数骗了你5.1 实测一组数据验证稳定性传感器驱动写完后我习惯先接一个简单的串口打印工程每秒读一次数据放在不同光照环境下观察。实测下来在白天靠窗位置的读数大约1800到2300勒克斯室内普通LED灯下约300到400勒克斯用手完全遮住传感器则掉到20勒克斯以下。这里有一个容易忽略的现象紧贴窗口和离窗半米读数差异非常大这恰恰说明传感器灵敏度和动态范围是正常的。如果数值纹丝不动或者变化极其微小问题大概率出在你用东西挡住了模块的感光窗口。GY-30模块顶部那个小圆孔或者长方形凹槽就是感光面很多开发者在面包板上插线时杜邦线或者外壳边缘刚好挡住了它读数自然不对。5.2 常见故障与完整排查链路我在群里帮人排查过很多次光照采集问题把高发故障整理成一张表操作顺序也是按优先级排的现象可能原因排查操作读回0xFFFF或0x0000接线错误、地址错误、上拉缺失用逻辑分析仪抓波形确认SCL/SDA有无数据发送地址后无应答从机地址不对、ADDR悬空万用表测SDA电平确认模块供电数值一直不变转换时间不够、死锁拉长读取间隔重新上电复位数据跳变剧烈线缆过长、接触不良、供电不稳缩短杜邦线加100nF去耦电容烧录报no target found调试器连接问题与传感器无关检查ST-Link接线和复位电路如果遇到烧录时报“no stm32 target found”这种错误先冷静一下这八成是你的JTAG/SWD线松了或者开发板进入了低功耗模式跟BH1750一点关系都没有。别在这个时候去改传感器代码越改越乱。还有一个经验如果连续读取时数据偶尔跳一个比较大的值我倾向于加均值滤波。具体做法是连续读5次去掉最大值和最小值剩下的取平均。这么做对光照这种缓变物理量特别有效而且代码就那么几行收益非常明显。5.3 产品化的几点经验从原型到产品光照传感器的位置选择至少要考虑三件事感光面不能被外壳遮挡要留透明窗要尽量远离发热元件避免红外干扰导致读数偏高如果产品放在户外还得考虑防水透光膜的选型普通透明亚克力对紫外线的阻隔会影响实际测量。在软件层面我建议把传感器的读写封装成一个独立模块对外只暴露“获取当前光照强度”这一个接口。后续无论换MCU还是改硬件I2C都只动底层那几个函数上层业务代码完全不用改。如果是做台灯这种带闭环控制的可以把采集频率放到100ms一次配合PWM调光动态响应才跟得上。我个人在实际项目里的做法是软件I2C加100毫秒定时采集五次滑窗平均后对外提供一个回调事件。这个结构不大不小不管是集成到RTOS还是裸机状态机里都方便。最后再分享一个小技巧调试I2C设备时手里有一根逻辑分析仪比什么都强。SDA、SCL、GND三个夹子一夹波形一目了然比对着串口打印猜数据要高效得多。我当年要是早学会看波形可能少熬好几个大夜。本文还有配套的精品资源点击获取