简介:这份资源面向嵌入式开发初学者与需要快速搭建光照采集方案的工程师,围绕STM32F103微控制器驱动BH1750光照强度传感器(GY-30模块)展开,解决环境光测量与TFTLCD实时显示的实际问题。压缩包共140个文件,约4.53MB,以37个h头文件、34个c源文件为核心,配合16个o目标文件、16个d依赖文件及crf、lst、map等编译中间产物,另有uvprojx工程文件、sct分散加载文件、hex与axf可执行文件,以及pdf说明和bat脚本,构成一套可直接编译下载的完整Keil工程。内容覆盖I²C总线配置、BH1750一次测量与连续高分辨率模式切换、光照数据字节解析与Lux换算,以及TFTLCD初始化、坐标设置与界面布局等环节。目前已有4833人学习下载,适合希望打通微控制器编程、传感器驱动、总线通信与图形显示全链路的开发者参考实践。
1. 从一颗“便宜到离谱”的光照芯片说起
GY-30 这个模块,很多做嵌入式的人都买过:某宝上不到五块钱,板子上焊一颗 BH1750FVI,再加一个 3.3V 稳压和电平转换,插上 I2C 就能读数。但真正把它接到 STM32F103 上跑通、并且读出来的数字能信,中间踩的坑比想象中多。我见过太多人卡在“读出来永远是 0 或者 65535”这一步,然后怀疑模块坏了、怀疑焊接虚了,最后发现是地址搞错或者时序没等够。
这篇笔记就围绕 STM32F103 驱动 BH1750(GY-30 模块)这件事,把选型理由、I2C 配置、命令时序、数据换算、常见翻车点一次讲透。适合两类人:一是刚上手 STM32 标准库或 HAL 库、想找个靠谱传感器练手的;二是已经在用 BH1750,但读数不稳、量程不对、想搞清楚内部机制的人。读完你应该能自己写出一份可复现的驱动,并且知道每个参数为什么这么设。
2. BH1750 与 GY-30:先搞清楚你手里这块板子到底是什么
2.1 BH1750FVI 芯片本身的关键参数
BH1750FVI 是罗姆(ROHM)推出的一款数字式环境光传感器,核心是一个接近人眼响应曲线(峰值约 560nm)的光电二极管,配合 16 位 ADC 输出勒克斯(lx)值。它最大的好处是“直接出物理量”,不需要你自己做对数拟合或查表,省掉了光敏电阻那套标定流程。
几个必须记住的硬指标:
| 参数 | 典型值 | 说明 |
|---|---|---|
| 供电电压 | 2.4V ~ 3.6V | 芯片本体,不是模块 |
| 接口 | I2C,最高 400kHz | 标准模式 100k 也完全够用 |
| 默认地址 | 0x23(7 位) | ADDR 引脚接高时变 0x5C |
| 测量范围 | 1 ~ 65535 lx | 分辨率随测量时间变化 |
| 最小分辨率 | 0.11 lx(高分辨率模式) | 对应 16 位满量程 |
| 工作电流 | 约 0.12mA | 连续测量时 |
这里有个容易混淆的点:芯片供电是 2.4~3.6V,但 GY-30 模块通常标“3~5V 供电”。原因是模块上带了一颗稳压芯片和 I2C 电平转换电路,所以你可以直接接 5V。但如果你是从芯片裸片自己搭电路,千万别接 5V,会烧。
2.2 GY-30 模块上多出来的那几个元件
GY-30 模块的典型电路包含:
- 一颗 3.3V LDO(常见型号如 XC6206 或类似),把输入的 3~5V 降到 3.3V 给 BH1750 供电;
- 一颗双路电平转换 MOS 管或专用电平转换芯片,把 SDA/SCL 从 5V 域转到 3.3V 域;
- 上拉电阻,通常在 4.7kΩ 到 10kΩ 之间;
- ADDR 引脚默认下拉到 GND,所以地址是 0x23。
这意味着:如果你用 STM32F103 的 3.3V 系统,模块的 VCC 接 3.3V 或 5V 都能工作,但接 5V 时模块内部 LDO 会发热一点点,问题不大。真正要注意的是,有些廉价模块为了省成本,电平转换电路偷工减料,导致 3.3V 下 I2C 波形上升沿很缓,高速通信时容易出错。我一般建议:如果主控是 3.3V,直接给模块供 3.3V,绕过电平转换,波形最干净。
2.3 为什么选 BH1750 而不是光敏电阻或 OPT3001
光敏电阻便宜,但输出是模拟电阻值,非线性严重,每颗一致性差,要做产品得逐颗标定,产线成本反而高。OPT3001 精度更好、带中断,但价格是 BH1750 的好几倍,而且封装小,手工焊接困难。BH1750 处在“性能够用、价格极低、焊接友好”的甜点区,适合大多数室内光照监测场景,比如自动调光、农业补光、屏幕亮度调节。
它的局限也要清楚:响应曲线虽然接近人眼,但在红外段仍有少量响应,强红外源附近会偏大;另外它没有中断输出,只能轮询,低功耗场景下不如带中断的型号省电。
3. 用 STM32F103 的 I2C 外设把 BH1750 跑起来
3.1 硬件连接与上拉电阻的取舍
以 STM32F103C8T6 最小系统板为例,推荐连接:
| STM32 引脚 | GY-30 引脚 | 说明 |
|---|---|---|
| 3.3V | VCC | 3.3V 供电,波形最干净 |
| GND | GND | 共地 |
| PB6 | SCL | I2C1 时钟 |
| PB7 | SDA | I2C1 数据 |
| 不接 | ADDR | 默认 0x23 |
上拉电阻:GY-30 模块自带上拉,通常不需要额外加。但如果你用长杜邦线(超过 20cm),或者总线上挂了多个从机,建议在 STM32 侧再并一颗 4.7kΩ 到 3.3V,改善上升沿。注意不要并太多,否则总线电容增大,反而拖慢边沿。
提示:如果你用的是 PB6/PB7 以外的引脚做 I2C,记得在初始化时把 GPIO 模式设为“复用开漏输出”,并且使能对应的 AFIO 时钟。标准库和 HAL 库的写法不同,下面以 HAL 库为例。
3.2 用 CubeMX 配置 I2C1 的关键参数
在 CubeMX 里选 I2C1,模式选 I2C,速度模式选 Standard Mode 或 Fast Mode。BH1750 支持 400kHz,但为了稳定,我一般先用 100kHz 跑通,再往上调。关键参数:
- Clock Speed:100000 Hz
- Duty Cycle:2:1(Fast Mode 下才需要关心)
- Analog Filter:Enable
- Digital Filter:Enable,系数 0
生成代码后,I2C 初始化部分大致如下(HAL 库):
/* I2C1 init function generated by CubeMX */ static void MX_I2C1_Init(void) { hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 100000; // 100kHz,先求稳 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(); } }逻辑说明:ClockSpeed 设 100000 是保守值,BH1750 手册标称支持 400k,但廉价模块的电平转换电路在 400k 下容易丢数据。DutyCycle 在标准模式下无效,填默认即可。NoStretchMode 必须 Disable,因为 BH1750 在测量期间会拉低 SCL 做时钟拉伸,如果禁止拉伸,通信会直接失败。
3.3 BH1750 的命令集与上电时序
BH1750 的操作很简单,就是往地址 0x23 写一个字节命令。常用命令:
| 命令字节 | 含义 |
|---|---|
| 0x00 | 断电 |
| 0x01 | 上电 |
| 0x07 | 复位 |
| 0x10 | 连续高分辨率模式,1lx 分辨率,测量时间约 120ms |
| 0x11 | 连续高分辨率模式2,0.5lx 分辨率,约 120ms |
| 0x13 | 连续低分辨率模式,4lx 分辨率,约 16ms |
| 0x20 | 单次高分辨率模式,测量完自动断电 |
| 0x21 | 单次高分辨率模式2 |
| 0x23 | 单次低分辨率模式 |
上电后的正确流程是:先发 0x01(上电),等待至少 10ms,再发测量命令。如果你用单次模式,每次测量前都要发一次命令,然后等测量时间过去再读。连续模式下,发一次命令后可以周期性读,但要注意读的时候不要打断正在进行的测量。
一个常见的翻车点是:上电后直接发 0x10,然后立刻读,结果读到 0。原因是芯片还没完成第一次转换。正确做法是发完测量命令后,延时至少 120ms(高分辨率模式)再读。
3.4 读数据的完整代码与换算
下面是一段可复现的读取函数,基于 HAL 库:
#include "bh1750.h" #define BH1750_ADDR (0x23 << 1) // HAL 库需要 8 位地址,左移一位 /* 写一个命令字节 */ HAL_StatusTypeDef BH1750_WriteCmd(uint8_t cmd) { return HAL_I2C_Master_Transmit(&hi2c1, BH1750_ADDR, &cmd, 1, 100); } /* 读取光照值,返回 lx */ float BH1750_ReadLight(void) { uint8_t buf[2] = {0}; uint16_t raw = 0; float lux = 0.0f; // 发单次高分辨率测量命令 if (BH1750_WriteCmd(0x20) != HAL_OK) { return -1.0f; // 通信失败 } // 等待测量完成,高分辨率模式典型 120ms,留余量到 180ms HAL_Delay(180); // 读两个字节,BH1750 输出是高字节在前 if (HAL_I2C_Master_Receive(&hi2c1, BH1750_ADDR, buf, 2, 100) != HAL_OK) { return -1.0f; } raw = (uint16_t)((buf[0] << 8) | buf[1]); lux = raw / 1.2f; // 高分辨率模式下,换算系数是 1.2 return lux; }逻辑说明:HAL 库的 I2C 地址参数是 8 位格式,所以 7 位地址 0x23 要左移一位变成 0x46。发送命令用 Master_Transmit,接收用 Master_Receive。数据高字节在前,拼成 16 位整数后除以 1.2 得到 lx。这个 1.2 是芯片手册给出的典型值,实际芯片之间会有几个百分点的差异,高精度场景需要标定。
参数说明:延时 180ms 是保守值,如果你用低分辨率模式(0x23),延时可以缩短到 30ms 左右。超时参数 100ms 是 I2C 传输超时,不是测量等待时间,别搞混。
4. 避坑与排查:BH1750 读数异常的 5 个真实原因
4.1 读出来永远是 0 或 65535
现象:串口打印 raw 值,要么全 0,要么全 0xFFFF。
原因:I2C 通信根本没成功,但 HAL 库返回了 HAL_OK。这种情况多半是地址错了,或者 SDA/SCL 接反。还有一种可能是模块供电不足,LDO 没起来。
解决:先用逻辑分析仪或示波器看 SDA/SCL 有没有波形。如果没有,检查引脚配置和上拉。如果有波形但从机不响应,用 I2C 扫描代码确认地址。扫描代码如下:
void I2C_Scan(void) { for (uint8_t addr = 1; addr < 128; addr++) { if (HAL_I2C_IsDeviceReady(&hi2c1, addr << 1, 2, 10) == HAL_OK) { printf("Found device at 0x%02X\n", addr); } } }如果扫到 0x23,说明地址对;扫不到,检查硬件。
4.2 读数明显偏大或偏小
现象:在室内读出来 5000 lx,实际手机测光 App 显示 300 lx。
原因:换算系数用错。高分辨率模式(0x10/0x20)除以 1.2,高分辨率模式2(0x11/0x21)除以 2.4,低分辨率模式(0x13/0x23)除以 0.48。很多人不管什么模式都除以 1.2,结果低分辨率模式下读数偏小四倍多。
解决:确认你发的命令字节,然后对照手册选系数。另外,如果模块上盖了透明塑料壳,会有额外衰减,需要乘以一个补偿系数,通常在 1.1~1.3 之间。
4.3 连续读取时数据偶尔跳变
现象:大部分读数正常,偶尔蹦出一个极大或极小值。
原因:读的时候正好赶上芯片在更新数据寄存器,读到了半新半旧的值。BH1750 在连续模式下,内部寄存器更新和 I2C 读取是异步的。
解决:连续模式下,两次读取之间至少间隔测量时间(高分辨率 120ms)。或者改用单次模式,每次读之前重新发命令,虽然慢一点,但数据一致性最好。我一般做产品用单次模式,做快速原型用连续模式加软件滤波。
4.4 I2C 总线死锁,SCL 被拉低
现象:程序跑一段时间后,I2C 通信全部失败,SCL 一直低电平。
原因:主机在从机拉低 SCL 做时钟拉伸时复位,或者总线受到干扰,从机状态机卡死。
解决:加 I2C 总线恢复函数,在初始化前手动发送 9 个时钟脉冲,让从机释放总线。STM32 的 HAL 库没有现成函数,需要自己用 GPIO 模拟:
void I2C_BusRecovery(void) { GPIO_InitTypeDef gpio = {0}; // 把 SCL 和 SDA 临时切成普通开漏输出 gpio.Pin = GPIO_PIN_6 | GPIO_PIN_7; gpio.Mode = GPIO_MODE_OUTPUT_OD; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &gpio); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET); for (int i = 0; i < 9; 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); } // 补一个停止条件 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET); HAL_Delay(1); // 恢复 I2C 复用功能 MX_I2C1_Init(); }这个函数在 I2C 初始化之前调用,能解决大部分死锁。
4.5 用 5V 供电时通信不稳定
现象:模块接 5V 时,I2C 时好时坏;接 3.3V 就正常。
原因:模块上的电平转换电路在 5V 供电时,3.3V 侧的上拉仍然接到 3.3V,但转换 MOS 管的体二极管可能导致漏电,波形畸变。
解决:主控是 3.3V 时,直接给模块供 3.3V。如果必须用 5V(比如模块离主控很远,3.3V 压降大),在 SDA/SCL 上串一颗 100Ω 电阻限流,并确认模块的电平转换芯片支持 400kHz 以上。
5. 进阶:把 BH1750 做成低功耗光照节点的一个具体技巧
如果你要做电池供电的光照节点,BH1750 的轮询模式其实很费电,因为每次测量都要 120ms 且芯片持续上电。一个实用的技巧是:用单次低分辨率模式(0x23),测量时间只有 16ms,分辨率 4lx,对大多数“天黑开灯”场景完全够用。测完之后立刻发 0x00 断电命令,芯片电流降到 0.01μA 级别。
具体流程:
- STM32 平时在 Stop 模式,RTC 定时唤醒(比如每 10 秒一次);
- 唤醒后给 BH1750 上电(如果模块 VCC 由 GPIO 控制,直接拉高);
- 发 0x01 上电,延时 10ms;
- 发 0x23 单次低分辨率测量,延时 20ms;
- 读 2 字节,换算:lux = raw / 0.48;
- 发 0x00 断电;
- 判断 lux 是否超过阈值,决定是否触发后续动作;
- STM32 重新进入 Stop 模式。
这样平均电流可以压到几十微安,一颗 2000mAh 的电池能撑一年以上。注意:如果你用 GPIO 控制模块 VCC,要确认 GPIO 的驱动电流足够模块 LDO 启动,通常需要加一颗 MOS 管做开关。
验证方法:用万用表电流档串在电池和模块之间,看平均电流。如果发现待机电流偏大,检查 STM32 的未用引脚是否配置为模拟输入,避免浮空漏电。
我自己在这个方案上踩过的最大坑是:忘了在断电前把 I2C 引脚切成模拟输入,结果 SDA/SCL 上的上拉电阻通过模块内部电路漏电,待机电流多了 200μA。后来养成习惯,每次进 Stop 模式前把所有 I2C 引脚设为模拟输入,问题才解决。希望帮到你。
本文还有配套的精品资源,点击获取