传感器 · BH1750 · I2C 光照
BH1750 光照传感器 STM32F103 接线与代码
让单片机知道"现在环境有多亮"。本页是 STM32F103C8T6 + BH1750 + 0.96 OLED 的完整实机验证记录,含接线图、完整代码、源码 ZIP 和已编译固件下载,全部实测通过。
Quick Facts
- 芯片:BH1750FVI(I2C 接口,直接输出 lux)
- 地址:0x23(ADDR 悬空或接地)
- 供电:3.3V,与主控共地
- 接线:SCL→PB8,SDA→PB9,共 4 根线
- 配套:STM32F103C8T6 + 0.96 OLED(0x3C)
- 实测:155 — 2361 lux,NORMAL / BRIGHT 正常切换
Verified
实机验证结果
本项目实测 下面的照片全部是我们自己拍的实机画面,不是渲染图。更新于 2026-08-14。
实测光照变化
下表是现场观察到的大致范围,属于现象记录,不是标定数据。不同灯具、角度和距离差别很大。
| 测试场景 | 观察到的 lux | OLED 状态 |
|---|---|---|
| 遮住 BH1750 | 明显下降 | 随数值下降 |
| 办公室普通灯光 | 几十 lux | NORMAL |
| 室内近距离灯光 | 155.8 — 178.3 lux | NORMAL |
| 灯光直接照射 | 715.8 lux | BRIGHT |
| 摄像灯强光 | 2361.7 lux | BRIGHT,亮度条走满 |
编译烧录记录
** Verified OK ** → [SUCCESS] Took 55.93 seconds| 项目 | 本项目实际使用 |
|---|---|
| 主控 | STM32F103C8T6(BluePill) |
| 显示 | 0.96 寸 OLED,SSD1306,128×64,地址 0x3C |
| 开发工具 | VS Code + PlatformIO |
| 框架 | Arduino / STM32duino |
| 烧录 | ST-Link |
| 串口 | 115200 |
| 依赖库 | Adafruit SSD1306、Adafruit GFX Library |
.ino 文件同理,已标注「未实测」。
What it does
这个模块干什么
BH1750 就是给单片机装一只眼睛,专门看现在有多亮。手遮住它,数值掉下去;手电筒照过去,数值明显涨上来。单片机拿到这个数值,就能自己决定要不要开灯、要不要把屏幕调暗。
它和光敏电阻最大的区别是:直接输出 lux,有明确物理单位,不用接 ADC 再自己换算,同型号之间一致性也好得多。
| BH1750 | 光敏电阻 LDR | |
|---|---|---|
| 输出 | 数字,单位就是 lux | 电阻值,要接 ADC 自己换算 |
| 单位 | 有明确物理单位 | 只能比大小 |
| 光谱 | 接近人眼感觉,红外影响很小 | 对红外也有反应 |
| 一致性 | 同型号差异小 | 个体差异大,基本要逐个校准 |
| 接线 | I2C 两根信号线 | 一路模拟量 + 分压电阻 |
对比依据来自数据手册对 BH1750 特性的描述(人眼响应、红外影响小、±20% 误差)与光敏电阻的常规特性,由我们整理成表。资料参考
Wiring
接线
本项目实测 BH1750 和 OLED 共用同一组 I2C,一共只要 4 根线。下图接法实机跑通。
| 信号 | BH1750 | STM32F103C8T6 | OLED |
|---|---|---|---|
| 电源 | VCC | 3.3V | VCC / VDD |
| 地 | GND | GND | GND |
| 时钟 | SCL | PB8 | SCL / SCK |
| 数据 | SDA | PB9 | SDA |
| 地址 | ADDR | — | — |
GND · ADDR · SDA · SCL · VCC,GY-302 是 ADDR · SDA · SCL · GND · VCC。同样插在第 1 脚,一个是 GND,另一个是 ADDR。本项目实测用的是 4 脚版本,丝印为
VCC GND SCL SDA。
| 要点 | 说明 |
|---|---|
| 供电 3.3V | 本项目实测用 3.3V。BH1750 芯片耐压 3.6V,多数模块板带稳压可接 5V,但和 3.3V 的 STM32 一起用时直接 3.3V 最省事 |
| 必须共地 | 所有模块 GND 接在一起,否则 I2C 会莫名失败 |
| 地址不冲突 | BH1750 是 0x23,OLED 是 0x3C,可以共用一组 I2C。ADDR 接高电平会变成 0x5C,程序就读不到了 |
| PA13 / PA14 | 留给 ST-Link 调试,不要占用 |
Code
完整代码
本项目实测 下面是实际烧录进板子、拍摄视频时运行的那一份,一字未改。环境:VS Code + PlatformIO + Arduino(STM32duino)。
关键处说明
1. 把 I2C 指到 PB8 / PB9。STM32duino 默认的 I2C 引脚不是 PB8/PB9,不手动指定就读不到任何东西。
2. 初始化三条指令按顺序发:0x01 上电 → 0x07 复位 → 0x10 连续高分辨率测量,然后等 180 ms。Reset 必须在 Power On 之后,掉电状态下发无效,这是手册明确写的顺序要求。delay(180) 取的是手册给的最大测量时间(典型值是 120 ms),这样最慢的批次也不会读到空数据。
3. 换算:每次读 2 个字节拼成 16 位整数,再除以 1.2。1.2 是数据手册给的换算系数典型值。
4. 状态阈值是我们自己定的:< 20 为 DARK,20 — 200 为 NORMAL,> 200 为 BRIGHT。底部亮度条按 0 — 1000 lux 映射。
/*
* ==================== FIRMWARE VERSION ====================
* Project: 01_BH1750
* Board: STM32F103C8T6 / BluePill
* Current firmware: BH1750 UI 1.1
* Version tag on OLED/Serial: BH1750 UI 1.1
* Base stable firmware: main_v4_实测通过_20260814.cpp
* Date: 2026-08-14
*
* UI 1.1 changes:
* - Larger lux value for video recording
* - Simple animated sun icon
* - Lux bar
* - DARK / NORMAL / BRIGHT status
*
* Save rule:
* After this version is confirmed by real hardware test, copy this file to:
* 固件/稳定版本/main_ui_v1_1_大字体图标动画_实测通过_20260814.cpp
* ==========================================================
*/
#include <Arduino.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Wire.h>
constexpr uint8_t OLED_ADDR = 0x3C;
constexpr uint8_t BH1750_ADDR = 0x23;
constexpr int SCREEN_WIDTH = 128;
constexpr int SCREEN_HEIGHT = 64;
constexpr char FW_VERSION[] = "BH1750 UI 1.1";
constexpr uint8_t BAR_X = 6;
constexpr uint8_t BAR_Y = 54;
constexpr uint8_t BAR_W = 78;
constexpr uint8_t BAR_H = 7;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
bool sunFrame = false;
bool bh1750Write(uint8_t command) {
Wire.beginTransmission(BH1750_ADDR);
Wire.write(command);
return Wire.endTransmission() == 0;
}
bool i2cDeviceFound(uint8_t address) {
Wire.beginTransmission(address);
return Wire.endTransmission() == 0;
}
bool bh1750Init() {
if (!bh1750Write(0x01)) {
return false;
}
delay(10);
if (!bh1750Write(0x07)) {
return false;
}
delay(10);
if (!bh1750Write(0x10)) {
return false;
}
delay(180);
return true;
}
bool bh1750ReadLux(float &lux) {
Wire.requestFrom(BH1750_ADDR, static_cast<uint8_t>(2));
if (Wire.available() < 2) {
bh1750Write(0x10);
delay(180);
return false;
}
uint16_t raw = static_cast<uint16_t>(Wire.read()) << 8;
raw |= Wire.read();
lux = raw / 1.2f;
return true;
}
void showStatus(const char *line1, const char *line2) {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println(FW_VERSION);
display.println();
display.println(line1);
display.println(line2);
display.display();
}
const char *luxStatus(float lux) {
if (lux < 20.0f) {
return "DARK";
}
if (lux > 200.0f) {
return "BRIGHT";
}
return "NORMAL";
}
void drawSunIcon(int16_t x, int16_t y, bool frame) {
display.drawCircle(x, y, 3, SSD1306_WHITE);
if (frame) {
display.drawPixel(x, y - 7, SSD1306_WHITE);
display.drawPixel(x, y + 7, SSD1306_WHITE);
display.drawPixel(x - 7, y, SSD1306_WHITE);
display.drawPixel(x + 7, y, SSD1306_WHITE);
} else {
display.drawLine(x - 5, y - 5, x - 7, y - 7, SSD1306_WHITE);
display.drawLine(x + 5, y - 5, x + 7, y - 7, SSD1306_WHITE);
display.drawLine(x - 5, y + 5, x - 7, y + 7, SSD1306_WHITE);
display.drawLine(x + 5, y + 5, x + 7, y + 7, SSD1306_WHITE);
}
}
void drawLuxBar(float lux) {
float clamped = lux;
if (clamped < 0.0f) {
clamped = 0.0f;
}
if (clamped > 1000.0f) {
clamped = 1000.0f;
}
uint8_t fillW = static_cast<uint8_t>((clamped / 1000.0f) * (BAR_W - 2));
display.drawRect(BAR_X, BAR_Y, BAR_W, BAR_H, SSD1306_WHITE);
display.fillRect(BAR_X + 1, BAR_Y + 1, fillW, BAR_H - 2, SSD1306_WHITE);
}
void showLux(float lux) {
const char *status = luxStatus(lux);
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
drawSunIcon(10, 8, sunFrame);
display.setCursor(22, 2);
display.println("BH1750");
display.setTextSize(2);
display.setCursor(18, 20);
display.println(lux, 1);
display.setTextSize(1);
display.setCursor(88, 28);
display.println("lux");
drawLuxBar(lux);
display.setCursor(90, 53);
display.println(status);
display.display();
sunFrame = !sunFrame;
}
void showBootScreen() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println(FW_VERSION);
display.println();
display.println("OLED UI 1.1");
display.println("PB8 SCL PB9 SDA");
display.println("Booting...");
display.display();
}
void setup() {
Serial.begin(115200);
delay(300);
Wire.setSCL(PB8);
Wire.setSDA(PB9);
Wire.begin();
Wire.setClock(100000);
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
Serial.println("OLED init failed");
while (true) {
delay(1000);
}
}
showBootScreen();
Serial.println();
Serial.println(FW_VERSION);
Serial.println("OLED UI 1.1");
delay(1200);
showStatus("OLED OK", "BH1750 init...");
Serial.print("OLED 0x3C: ");
Serial.println(i2cDeviceFound(OLED_ADDR) ? "found" : "not found");
Serial.print("BH1750 0x23: ");
Serial.println(i2cDeviceFound(BH1750_ADDR) ? "found" : "not found");
if (!bh1750Init()) {
showStatus("BH1750 failed", "Check wiring");
Serial.println("BH1750 init failed");
} else {
showStatus("BH1750 ready", "PB8/PB9 I2C");
Serial.println("BH1750 ready");
}
}
void loop() {
float lux = 0.0f;
if (bh1750ReadLux(lux)) {
showLux(lux);
Serial.print("Lux:");
Serial.println(lux, 1);
} else {
showStatus("Read failed", "Check PB8/PB9");
Serial.println("BH1750 read failed");
}
delay(1000);
}
库依赖:Adafruit SSD1306 和 Adafruit GFX Library,用 PlatformIO 打开工程会按 platformio.ini 自动下载,不用手动安装。
Download
下载
下面三个文件都可以直接点击下载。推荐用第一个 ZIP,解压后 VS Code 打开就能 Build。
bin 怎么烧进去
用下面任一方式烧录 bin:
| 方式 | 工具 | 说明 |
|---|---|---|
| ST-Link(本项目用的) | STM32CubeProgrammer | 选 ST-LINK,起始地址填 0x08000000,选 bin 文件后 Download |
| ST-Link 命令行 | st-flash | st-flash write firmware.bin 0x08000000 |
| USB 转串口 | STM32CubeProgrammer / FlyMcu | BOOT0 拉高复位进 ISP 模式,走串口下载 |
pio device list,找 CP2102 / CP210x / CH340 对应的那个。
第一次使用
- 按上面接线图接好 4 根线:3.3V、GND、PB8、PB9
- 下载 ZIP,解压,用 VS Code + PlatformIO 打开这个文件夹
- 等依赖库自动装完,点 Build,看到 SUCCESS
- 接上 ST-Link,点 Upload,看到
** Verified OK ** - 看 OLED:先是启动画面,然后是 lux 数值。用手遮一下、手电筒照一下,数值应该跟着变
Demo
视频演示
本项目实测 58 秒,全部是实机画面,没有演示动画。
| 时间 | 内容 |
|---|---|
| 0:00 | 实测结果先看,OLED 显示 715.8 lux / BRIGHT |
| 0:07 | 打光后 lux 从 155 上升到 715,状态切到 BRIGHT |
| 0:15 | 强光 2361 lux,亮度条走满 |
| 0:21 | 接线只要 4 根线 |
| 0:33 | 烧录 Verified OK |
| 0:38 | OLED 界面近景 |
| 0:48 | 下一步与资料下载 |
Reference
实用参数与资料
资料参考 这一节来自芯片数据手册和公开资料,不是我们实测的结论。只留会影响你怎么接线、选型、写代码的内容。
最需要提前知道的一条:量程上限
数据寄存器是 16 位,最大 65535。默认设置下能读到的最高值是 65535 / 1.2 = 54612 lx。
| 配置 | 可测上限 | 适用 |
|---|---|---|
| 高分辨率 + MTreg 69(默认) | 约 54612 lx | 室内绰绰有余 |
| 高分辨率模式 2 + MTreg 69 | 约 27306 lx | 精度翻倍,量程减半 |
| MTreg 调到最小 31 | 约 121557 lx | 能覆盖直射阳光 |
| MTreg 调到最大 254 | 约 14838 lx | 暗环境,量程明显缩小 |
0x11)的读数要再除以 2。它的分辨率是 0.5 lx,换算是 raw / 1.2 / 2。用现成库不用管,自己手写驱动很容易忘,结果读数正好偏大一倍。
六种测量模式
| 指令 | 模式 | 分辨率 | 测量时间 |
|---|---|---|---|
0x10 | 连续 · 高分辨率(本项目用这个) | 1 lx | 典型 120 ms,最大 180 ms |
0x11 | 连续 · 高分辨率 2 | 0.5 lx | 典型 120 ms,最大 180 ms |
0x13 | 连续 · 低分辨率 | 4 lx | 典型 16 ms,最大 24 ms |
0x20 | 单次 · 高分辨率 | 1 lx | 典型 120 ms,最大 180 ms |
0x21 | 单次 · 高分辨率 2 | 0.5 lx | 典型 120 ms,最大 180 ms |
0x23 | 单次 · 低分辨率 | 4 lx | 典型 16 ms,最大 24 ms |
连续 vs 单次:连续模式测完自动接着测,读的时候直接拿最新值;单次模式测完就自动掉电,下次要用得重新发指令。电池供电的产品用单次模式更省电,一直插着电用连续模式最省事。
怎么选模式
| 场景 | 模式 | MTreg | 为什么 |
|---|---|---|---|
| 室内演示 / 学习 / 一直插电 | 0x10 | 默认 69 | 最省事,本项目就是这个 |
| 电池供电,几分钟测一次 | 0x20 | 默认 69 | 测完自动掉电,平时几乎不耗电 |
| 户外 / 可能见到直射阳光 | 0x10 | 调到 31 | 量程拉到约 121557 lx,避免顶到头 |
| 只需快速判断亮/暗 | 0x13 | 默认 69 | 16 ms 出结果,4 lx 精度够用 |
读取节奏:别读得比测量还快
连续模式下传感器按自己的节奏一轮一轮测,你读得再快也只是把同一个值取出来第二遍。高分辨率一轮要 120 — 180 ms,读取间隔要大于这个数,否则会看到"数值好几次都不变",误以为传感器卡住了。
本项目实测 我们主循环是 delay(1000),一秒读一次,远大于 180 ms,不会踩到这个问题。要加快刷新,最快也别低于 200 ms。
为什么读数没有被灯光闪烁搅乱
市电供电的灯具亮度会以 100 Hz 起伏,很多光传感器因此读数跳动。BH1750 高分辨率模式积分时间约 120 ms,正好覆盖十几个闪烁周期,等于自动做了平均,所以读数天然就稳。反过来说,改用低分辨率模式(16 ms)只盖住一个多周期,读数就可能开始跳。
用阈值做开关,一定要加迟滞
只用一个阈值判断亮暗,光照在临界点附近轻微波动时输出会疯狂来回跳。标准做法是用两个阈值拉开距离:低于 40 才开灯,高于 60 才关灯,中间保持不变。
lux 数值对照:多少算亮
| 环境 | 大致 lux | 本项目实测对应 |
|---|---|---|
| 晴朗满月夜 | 0.25 — 1 lx | — |
| 昏暗走廊 / 应急照明 | 20 — 50 lx | 代码里 < 20 判为 DARK |
| 家庭客厅照明 | 50 — 150 lx | — |
| 办公室照明 | 300 — 500 lx | 我们室内实测 155 — 178 lx,比标准办公室略暗 |
| 电视演播室灯光 | 约 1000 lx | 我们打光实测 715 lx,加强后 2361 lx |
| 阴天室外 | 1000 — 5000 lx | 2361 lx 已接近阴天室外水平 |
| 正午直射阳光 | 32000 — 100000 lx | 超出本项目测试范围,且会饱和 |
照度是"这个位置有多亮",不是"灯有多亮"。同一盏灯,传感器离得近读数就高,离得远就低。所以阈值必须在传感器最终安装的那个位置标定,桌面上试出来的数字,模块挪到墙上就不成立了。
基本参数
| 参数 | 数值 | 来源 |
|---|---|---|
| 芯片型号 | BH1750FVI(ROHM 罗姆) | 数据手册 |
| 芯片供电 | 2.4 — 3.6 V | 数据手册 |
| 模块板供电 | GY-30 / GY-302 自带稳压,标称 3 — 5 V | 厂家模块资料 |
| 工作电流 | 约 120 µA;掉电模式几乎为 0 | 数据手册 |
| I2C 地址 | ADDR 接地或悬空 0x23;接高 0x5C | 数据手册 |
| 测量范围 | 1 — 65535 lx | 数据手册 |
| 测量误差 | 约 ±20% | 数据手册 |
| 光谱峰值 | 约 560 nm,接近人眼最敏感波长 | 数据手册 |
| 工作温度 | −40 — +85 ℃ | 数据手册 |
| 换算系数 | 典型值 1.2,原始 16 位数据 ÷ 1.2 = lux | 数据手册 |
装进外壳时会遇到的问题
| 问题 | 说明 |
|---|---|
| 光窗不能挡 | 传感器正面小方窗是进光口,被外壳、排线、胶带压住就读不准,装配要留开口 |
| 透光罩会衰减 | 加亚克力或玻璃罩后读数整体偏低,加了罩子必须重新标阈值,不能沿用裸测数字 |
| 视场角有限 | 它测的是正面来的光,朝向不同同一房间读数能差很多,安装角度要固定 |
| 自家光源会干扰 | 板上 LED、OLED 反光可能被读进去,布局时让传感器背对自家光源 |
以上为光学传感器的通用工程实践,本项目在面包板上裸测,没有做过外壳实验。资料参考
Troubleshooting
常见问题排查
下面是根据手册工作原理整理的常见原因。标注「实测」的是我们自己遇到过的,其余是资料推断。
串口扫描不到 BH1750,或 OLED 一直显示 BH1750 failed
按这个顺序查:
- 先看有没有共地。BH1750、OLED、STM32 的 GND 必须接在一起,只接了电源正极不接地,I2C 一定不通。
- 确认 SCL 接 PB8、SDA 接 PB9。这两根接反是最常见的错误,程序不会报错,就是读不到。
- 看模块丝印,不要照着照片数孔位。GY-30 和 GY-302 的第 1 脚一个是 GND、一个是 ADDR,插反了供电就错了。
- 确认地址是 0x23。ADDR 脚被接到 3.3V 的话地址会变成
0x5C,程序里写死的 0x23 就找不到设备。ADDR 悬空或接 GND 都是 0x23。 - 确认供电是 3.3V。本项目实测用 3.3V。
程序启动时会往串口打印地址检测结果,接 115200 波特率就能看到:
OLED 0x3C: found
BH1750 0x23: found
BH1750 ready
读数一直不变,好像卡住了
先确认是不是真的卡住。两种情况都会让数值看起来不动:
- 读得太快。连续模式一轮测量要 120 — 180 ms,读取间隔小于这个数,拿到的就是上一轮的同一个值。把间隔加大到 200 ms 以上再看。
- 实测 环境光本来就没变。我们拍视频时数值长时间停在 715.8 不动,一度以为画面卡了,其实是光源稳定加上 120 ms 积分自带平均的结果。用手遮一下、拿手电筒照一下,数值立刻变化就说明是正常的。
读数顶到最大值不动
是饱和,不是坏了。默认设置下上限是 54612 lx,直射阳光超过 100000 lx,直接就顶死了。
处理:改用低分辨率模式,或者把 MTreg 调到最小 31,量程可以拉到约 121557 lx。详见上面「量程上限」一节。
OLED 上数字显示不出来 / 显示乱码
先确认 OLED 地址是 0x3C。常见的 0.96 寸 SSD1306 大多是 0x3C,少数是 0x3D。
如果 OLED 完全不亮,看串口是不是打印了 OLED init failed——那是 display.begin() 失败,一般还是接线或地址问题。
数值和手机测光 App 对不上
这是正常的,不是坏了。三个原因:
- BH1750 本身测量误差约 ±20%
- 不同传感器的光谱响应曲线不一样,同一个场景读数本来就会差,差多少还取决于光源是白炽灯、卤素还是 LED
- 照度跟位置和朝向有关,手机和模块不在同一个位置、不朝同一个方向,读数当然不同
BH1750 给的是稳定、可重复、单位明确的相对量,做阈值判断和趋势观察完全够用;要绝对标定必须用专业仪器针对具体光源校准。
References
资料来源
本页信息来源声明
| 内容 | 来源 |
|---|---|
| 接线、地址、代码、lux 实测值、编译烧录记录、视频 | 本项目实测 2026-08-14 实机测试 |
| 芯片参数、六种模式、指令集、MTreg、量程上限计算 | 资料参考 ROHM 数据手册与开源库文档 |
| GY-30 / GY-302 引脚差异、模块板供电 | 资料参考 厂家与经销商模块资料 |
| lux 环境对照值 | 资料参考 公开照度水平参考资料,为典型范围 |
| 外壳装配注意事项 | 资料参考 光学传感器通用工程实践,本项目未做外壳实验 |
| SKU | 待补充 |
本页所有「资料参考」内容均由我们重新整理表述,未直接复制原文。参数以你手上模块的实际批次和卖家说明为准。