4. 亮度测量标准与规范
好,咱们进入实战环节。前面讲了I2C协议和硬件接线,现在要真刀真枪地写代码了。BH1750这个传感器,说白了就是个光强度转数字的小芯片。你给它发个指令,它就把环境光照强度通过I2C总线吐出来。
我个人习惯,拿到一个新传感器,先不急着写业务逻辑。先写个最简单的demo,确保能读到原始数据。这一步走稳了,后面才不慌。
4.1 准备工作:安装必要的库
在树莓派或Jetson Nano上,我们主要用两个库:smbus2 和 time。time是Python自带的,smbus2需要装一下。
sudo pip3 install smbus2
嗯,这里要注意。如果你用的是Python虚拟环境,记得先激活环境再装。我在项目中遇到过有人直接在系统级Python里装,结果虚拟环境里找不到库,折腾了半天。
4.2 核心代码:读取光照强度
下面这段代码,是我最常用的BH1750读取模板。它包含了初始化、单次测量和连续测量三种模式。
import smbus2
import time
# BH1750 I2C地址(ADDR引脚接GND时为0x23,接VCC时为0x5C)
BH1750_ADDR = 0x23
# 指令定义
CMD_POWER_ON = 0x01
CMD_RESET = 0x07
CMD_CONT_H_RES_MODE = 0x10 # 连续高分辨率模式
CMD_ONCE_H_RES_MODE = 0x20 # 单次高分辨率模式
class BH1750:
def __init__(self, bus=1, addr=BH1750_ADDR):
self.bus = smbus2.SMBus(bus)
self.addr = addr
self._power_on()
self._reset()
def _power_on(self):
"""上电"""
self.bus.write_byte(self.addr, CMD_POWER_ON)
time.sleep(0.01)
def _reset(self):
"""复位"""
self.bus.write_byte(self.addr, CMD_RESET)
time.sleep(0.01)
def set_mode(self, mode=CMD_CONT_H_RES_MODE):
"""设置测量模式"""
self.bus.write_byte(self.addr, mode)
time.sleep(0.18) # 等待首次测量完成
def read_light(self):
"""读取光照强度(单位:lux)"""
data = self.bus.read_word_data(self.addr, 0x00)
# 注意:BH1750返回的是大端序,需要转换
raw = ((data & 0xFF) << 8) | ((data >> 8) & 0xFF)
lux = raw / 1.2
return lux
# 使用示例
if __name__ == '__main__':
sensor = BH1750()
sensor.set_mode(CMD_CONT_H_RES_MODE)
try:
while True:
lux = sensor.read_light()
print(f"当前光照强度: {lux:.2f} lux")
time.sleep(1)
except KeyboardInterrupt:
print("采集结束")
关键点解析:
- 地址选择:ADDR引脚接地时地址是0x23,接VCC时是0x5C。我建议你焊板子时直接接地,省得记两个地址。
- 字节序转换:BH1750返回的数据是大端序(高字节在前),而树莓派的CPU是小端序。所以代码里做了移位处理。这个坑我踩过,第一次读出来数值完全不对,后来查手册才发现。
- 除1.2:这是BH1750的转换系数,手册里写死的。高分辨率模式下,原始值除以1.2就是lux值。
4.3 三种测量模式对比
BH1750提供了好几种测量模式,但实际项目中常用的就三种。我整理了个表格,方便你对比选择。
| 模式 | 指令 | 分辨率 | 测量时间 | 适用场景 |
|---|---|---|---|---|
| 连续高分辨率 | 0x10 | 1 lux | 120 ms | 一般室内照明监测 |
| 连续低分辨率 | 0x13 | 4 lux | 16 ms | 快速扫描、户外强光 |
| 单次高分辨率 | 0x20 | 1 lux | 120 ms | 低功耗、电池供电设备 |
你想想看,如果你的设备是插电的,用连续模式最省事。如果是电池供电的,比如便携式亮度计,那就用单次模式,测完就休眠,能省不少电。
4.4 调试技巧:常见问题与排查
代码写好了,一跑发现读不到数据,或者数据明显不对。别急,我总结了几条排查路径。
我曾经遇到过的问题:
- I2C设备找不到:先跑
sudo i2cdetect -y 1,看看0x23或0x5C地址上有没有设备。如果没有,八成是接线问题或地址搞错了。 - 读出来全是0:检查一下是不是忘记调用
set_mode()了。BH1750上电后默认是掉电状态,不发指令它不会干活。 - 数值跳变剧烈:看看传感器窗口有没有被遮挡,或者是不是有快速闪烁的光源(比如LED灯)。BH1750对50Hz/60Hz的工频闪烁有抑制,但高频PWM调光可能会干扰它。
- 数值偏大或偏小:检查一下字节序转换对不对。我刚开始写的时候,直接用了
read_word_data()的返回值,没做大小端转换,结果读出来的lux值大了256倍。
我的调试小习惯:
在代码里加几个打印点,把原始数据、转换后的数据都打出来。比如:
data = self.bus.read_word_data(self.addr, 0x00)
print(f"原始数据: {data:#06x}") # 打印十六进制
raw = ((data & 0xFF) << 8) | ((data >> 8) & 0xFF)
print(f"转换后: {raw:#06x}")
lux = raw / 1.2
print(f"lux值: {lux:.2f}")
这样一眼就能看出是硬件问题还是软件问题。
4.5 知识结构图:BH1750数据采集流程
下面这张图,把整个数据采集的流程串起来了。从硬件初始化到最终拿到lux值,每一步都标清楚了。
4.6 进阶:异常处理与重试机制
在实际项目中,传感器偶尔会抽风。比如I2C总线被其他设备占用,或者传感器内部测量还没完成。所以,健壮的代码一定要加异常处理。
def read_light_safe(self, retries=3):
"""带重试的安全读取"""
for attempt in range(retries):
try:
data = self.bus.read_word_data(self.addr, 0x00)
raw = ((data & 0xFF) << 8) | ((data >> 8) & 0xFF)
lux = raw / 1.2
return lux
except OSError as e:
print(f"读取失败 (尝试 {attempt+1}/{retries}): {e}")
time.sleep(0.1)
raise RuntimeError("BH1750 读取失败,已重试 {} 次".format(retries))
这段代码加了3次重试。如果连续3次都读不到数据,就抛出异常,让上层逻辑处理。我在做智能照明项目时,就靠这个机制扛住了偶尔的I2C总线冲突。
4.7 数据验证:用已知光源校准
代码跑通了,读出来的数值准不准?我建议你用已知的光源做个快速验证。
- 暗室(关灯):应该读到 0~10 lux。如果读到几十甚至上百,说明有漏光或者传感器被遮挡不严。
- 办公室照明(日光灯):一般在 300~500 lux。如果差太多,检查一下传感器窗口有没有脏污。
- 户外阴天:大约 1000~2000 lux。如果读到上万,看看是不是直射太阳光了。
- 手机闪光灯近距离照射:可以到 10000 lux 以上。注意别长时间照,传感器会饱和。
总结一下:
BH1750的数据采集,核心就三步:发指令、等测量、读数据。代码本身不复杂,但字节序、地址选择、模式配置这三个细节容易出错。把基础demo跑通了,后面做亮度闭环控制就有了可靠的数据源。