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值,每一步都标清楚了。

BH1750 数据采集流程 硬件初始化 I2C总线检测 发送测量指令 等待测量完成 读取原始数据 转换计算 lux 值 关键检查点: • 地址:0x23 / 0x5C • 接线:VCC/GND/SDA/SCL • 上拉电阻:4.7kΩ 常见错误: • 忘记调用 set_mode() • 字节序未转换 调试工具: • i2cdetect 检测地址 • 打印原始十六进制数据

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跑通了,后面做亮度闭环控制就有了可靠的数据源。