传感器 · 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。

OLED 显示 164.2 lux 状态 NORMAL
室内普通灯光:164.2 lux,状态 NORMAL
OLED 显示 2361.7 lux 状态 BRIGHT 亮度条走满
强光照射:2361.7 lux,BRIGHT,亮度条走满
155.8实测最低 lux(室内)
2361.7实测最高 lux(摄像灯)
1 秒刷新一次
0x23实测识别地址

实测光照变化

下表是现场观察到的大致范围,属于现象记录,不是标定数据。不同灯具、角度和距离差别很大。

测试场景观察到的 luxOLED 状态
遮住 BH1750明显下降随数值下降
办公室普通灯光几十 luxNORMAL
室内近距离灯光155.8 — 178.3 luxNORMAL
灯光直接照射715.8 luxBRIGHT
摄像灯强光2361.7 luxBRIGHT,亮度条走满

编译烧录记录

PlatformIO 终端显示 Verified OK 与 SUCCESS
PlatformIO 上传输出:** 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
未验证的环境不当成支持。本项目没有在 Arduino IDE、Keil、STM32CubeIDE 下编译或验证过,所以不提供这些环境的支持说明。下载区的 .ino 文件同理,已标注「未实测」。

What it does

这个模块干什么

BH1750 就是给单片机装一只眼睛,专门看现在有多亮。手遮住它,数值掉下去;手电筒照过去,数值明显涨上来。单片机拿到这个数值,就能自己决定要不要开灯、要不要把屏幕调暗。

它和光敏电阻最大的区别是:直接输出 lux,有明确物理单位,不用接 ADC 再自己换算,同型号之间一致性也好得多。

BH1750光敏电阻 LDR
输出数字,单位就是 lux电阻值,要接 ADC 自己换算
单位有明确物理单位只能比大小
光谱接近人眼感觉,红外影响很小对红外也有反应
一致性同型号差异小个体差异大,基本要逐个校准
接线I2C 两根信号线一路模拟量 + 分压电阻

对比依据来自数据手册对 BH1750 特性的描述(人眼响应、红外影响小、±20% 误差)与光敏电阻的常规特性,由我们整理成表。资料参考

Wiring

接线

本项目实测 BH1750 和 OLED 共用同一组 I2C,一共只要 4 根线。下图接法实机跑通。

STM32F103C8T6 + BH1750 + 0.96 OLED 简化接线图
信号BH1750STM32F103C8T6OLED
电源VCC3.3VVCC / VDD
地GNDGNDGND
时钟SCLPB8SCL / SCK
数据SDAPB9SDA
地址ADDR——
按模块丝印接线,不要照着照片数孔位。这不是空话:市面上两种常见 BH1750 模块板芯片相同但排针顺序不同——GY-30 是 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。

PlatformIO 工程 ZIP 含 platformio.ini、src/main.cpp、README、接线图。已去掉本机编译路径,解压即可用。89 KB
下载 ZIP
已编译固件 bin 本页代码编译出的 firmware.bin,可直接烧录到 STM32F103C8T6。33 KB
下载 BIN
Arduino IDE 版 .ino 未实测 与上面同一份代码,只改了扩展名。未在 Arduino IDE 下实际编译验证,供参考。用它需要先装 STM32 官方核心和两个 Adafruit 库。6 KB
下载 INO

bin 怎么烧进去

这块板子不能用网页烧录器。网页烧录(WebSerial)目前主要面向 ESP32 / ESP8266 这类自带 USB 串口下载的芯片。STM32F103C8T6 本项目用的是 ST-Link + SWD,浏览器接触不到 SWD 接口,所以本页不提供在线烧录入口——提供了也烧不进去。

用下面任一方式烧录 bin:

方式工具说明
ST-Link(本项目用的)STM32CubeProgrammer选 ST-LINK,起始地址填 0x08000000,选 bin 文件后 Download
ST-Link 命令行st-flashst-flash write firmware.bin 0x08000000
USB 转串口STM32CubeProgrammer / FlyMcuBOOT0 拉高复位进 ISP 模式,走串口下载
不要照抄 COM 号。视频和截图里出现的串口号是我们这台电脑的,你的电脑多半不一样。查当前串口:pio device list,找 CP2102 / CP210x / CH340 对应的那个。

第一次使用

  1. 按上面接线图接好 4 根线:3.3V、GND、PB8、PB9
  2. 下载 ZIP,解压,用 VS Code + PlatformIO 打开这个文件夹
  3. 等依赖库自动装完,点 Build,看到 SUCCESS
  4. 接上 ST-Link,点 Upload,看到 ** Verified OK **
  5. 看 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:38OLED 界面近景
0:48下一步与资料下载

Reference

实用参数与资料

资料参考 这一节来自芯片数据手册和公开资料,不是我们实测的结论。只留会影响你怎么接线、选型、写代码的内容。

最需要提前知道的一条:量程上限

数据寄存器是 16 位,最大 65535。默认设置下能读到的最高值是 65535 / 1.2 = 54612 lx。

直射阳光超过 100000 lx,是这个上限的两倍。把模块直接拿到太阳底下,读数会顶到头不动,看起来像坏了,其实是饱和了。室内怎么用都碰不到这个上限(本项目实测最高才 2361 lux),但只要做户外产品就必须处理。
配置可测上限适用
高分辨率 + MTreg 69(默认)约 54612 lx室内绰绰有余
高分辨率模式 2 + MTreg 69约 27306 lx精度翻倍,量程减半
MTreg 调到最小 31约 121557 lx能覆盖直射阳光
MTreg 调到最大 254约 14838 lx暗环境,量程明显缩小
还有个容易漏的坑:高分辨率模式 2(0x11)的读数要再除以 2。它的分辨率是 0.5 lx,换算是 raw / 1.2 / 2。用现成库不用管,自己手写驱动很容易忘,结果读数正好偏大一倍。

六种测量模式

指令模式分辨率测量时间
0x10连续 · 高分辨率(本项目用这个)1 lx典型 120 ms,最大 180 ms
0x11连续 · 高分辨率 20.5 lx典型 120 ms,最大 180 ms
0x13连续 · 低分辨率4 lx典型 16 ms,最大 24 ms
0x20单次 · 高分辨率1 lx典型 120 ms,最大 180 ms
0x21单次 · 高分辨率 20.5 lx典型 120 ms,最大 180 ms
0x23单次 · 低分辨率4 lx典型 16 ms,最大 24 ms

连续 vs 单次:连续模式测完自动接着测,读的时候直接拿最新值;单次模式测完就自动掉电,下次要用得重新发指令。电池供电的产品用单次模式更省电,一直插着电用连续模式最省事。

怎么选模式

场景模式MTreg为什么
室内演示 / 学习 / 一直插电0x10默认 69最省事,本项目就是这个
电池供电,几分钟测一次0x20默认 69测完自动掉电,平时几乎不耗电
户外 / 可能见到直射阳光0x10调到 31量程拉到约 121557 lx,避免顶到头
只需快速判断亮/暗0x13默认 6916 ms 出结果,4 lx 精度够用

读取节奏:别读得比测量还快

连续模式下传感器按自己的节奏一轮一轮测,你读得再快也只是把同一个值取出来第二遍。高分辨率一轮要 120 — 180 ms,读取间隔要大于这个数,否则会看到"数值好几次都不变",误以为传感器卡住了。

本项目实测 我们主循环是 delay(1000),一秒读一次,远大于 180 ms,不会踩到这个问题。要加快刷新,最快也别低于 200 ms。

为什么读数没有被灯光闪烁搅乱

市电供电的灯具亮度会以 100 Hz 起伏,很多光传感器因此读数跳动。BH1750 高分辨率模式积分时间约 120 ms,正好覆盖十几个闪烁周期,等于自动做了平均,所以读数天然就稳。反过来说,改用低分辨率模式(16 ms)只盖住一个多周期,读数就可能开始跳。

用阈值做开关,一定要加迟滞

只用一个阈值判断亮暗,光照在临界点附近轻微波动时输出会疯狂来回跳。标准做法是用两个阈值拉开距离:低于 40 才开灯,高于 60 才关灯,中间保持不变。

本项目实测 我们代码里的 DARK / NORMAL / BRIGHT 是纯显示用的,没做迟滞,因为只用来在屏幕上标个字,跳一下无所谓。但拿它去控制继电器或灯,就必须加迟滞,否则临界光照下会听到继电器一直哒哒响。

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 lx2361 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数据手册
±20% 是传感器本身的测量误差,不是代码问题。所以 BH1750 适合判断"变亮了 / 变暗了 / 现在处在哪一档",不适合当标准照度计用。要做绝对标定得用专业仪器,而且必须针对具体光源单独校准——不同型号的光传感器测同一场景,读数差别可能相当大。

装进外壳时会遇到的问题

问题说明
光窗不能挡传感器正面小方窗是进光口,被外壳、排线、胶带压住就读不准,装配要留开口
透光罩会衰减加亚克力或玻璃罩后读数整体偏低,加了罩子必须重新标阈值,不能沿用裸测数字
视场角有限它测的是正面来的光,朝向不同同一房间读数能差很多,安装角度要固定
自家光源会干扰板上 LED、OLED 反光可能被读进去,布局时让传感器背对自家光源

以上为光学传感器的通用工程实践,本项目在面包板上裸测,没有做过外壳实验。资料参考

Troubleshooting

常见问题排查

下面是根据手册工作原理整理的常见原因。标注「实测」的是我们自己遇到过的,其余是资料推断。

串口扫描不到 BH1750,或 OLED 一直显示 BH1750 failed

按这个顺序查:

  1. 先看有没有共地。BH1750、OLED、STM32 的 GND 必须接在一起,只接了电源正极不接地,I2C 一定不通。
  2. 确认 SCL 接 PB8、SDA 接 PB9。这两根接反是最常见的错误,程序不会报错,就是读不到。
  3. 看模块丝印,不要照着照片数孔位。GY-30 和 GY-302 的第 1 脚一个是 GND、一个是 ADDR,插反了供电就错了。
  4. 确认地址是 0x23。ADDR 脚被接到 3.3V 的话地址会变成 0x5C,程序里写死的 0x23 就找不到设备。ADDR 悬空或接 GND 都是 0x23。
  5. 确认供电是 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待补充

本页所有「资料参考」内容均由我们重新整理表述,未直接复制原文。参数以你手上模块的实际批次和卖家说明为准。