BH1750 的基本信息
- 工作电压:3V - 5V
- I2C 地址:默认 0x23 或 0x5C(具体取决于地址引脚配置)
- 测量范围:1 - 65535 Lux(高分辨率模式)
- 精度:高分辨率模式 1 Lux,低分辨率模式 4 Lux
- 响应时间:高分辨率模式 120ms,低分辨率模式 16ms
- 工作模式:
- 连续高分辨率模式:每次读取数据,分辨率为 1 Lux,周期为 120ms
- 一次性高分辨率模式:用户触发读取一次数据,分辨率为 1 Lux,周期为 120ms
- 连续低分辨率模式:每次读取数据,分辨率为 4 Lux,周期为 16ms
- 关机模式:节省功耗时,传感器关闭
BH1750 数据转换
BH1750 返回的光照值是一个 16 位无符号整数,表示传感器测量的光照强度。其高 8 位和低 8 位分别传输。
-
I2C 数据:大端格式传输的。即高位字节在前,低位字节在后。
- 高字节(16 位数据的高 8 位)
- 低字节(16 位数据的低 8 位)
数据转换
BH1750 输出的原始数据需要进行字节序转换(因为 BH1750 是大端格式,RK3568是小端,然后计算实际的 Lux 值。
-
Lux 计算公式:
lux = (0x03 << 8) | 0xE8 = 0x03E8 = 1000 Lux
模式选择和数据更新
- 高分辨率模式:数据的更新周期为 120ms,每次读取返回 1 Lux 精度的值。
- 低分辨率模式:数据的更新周期为 16ms,每次读取返回 4 Lux 精度的值。
BH1750 驱动全流程
1. 确定硬件接口
BH1750 通过 I2C 接口 与主机通信,主要寄存器:
- 0x23 或 0x5C(I2C 地址)
- 命令寄存器(常用命令):
0x10
:连续高分辨率模式(1lx 分辨率,120ms)0x20
:一次性高分辨率模式(1lx 分辨率,120ms)0x13
:连续低分辨率模式(4lx 分辨率,16ms)
2. 实现 BH1750 驱动
(1) BH1750设备结构体
#define BH1750_ADDR 0x23 // BH1750 I2C 地址
#define BH1750_POWER_ON 0x01 // 开启 BH1750
#define BH1750_CONT_H_RES_MODE 0x10 // 连续高分辨率模式
// 设备结构体
struct bh1750_dev {
struct i2c_client *client;
struct cdev cdev;
dev_t devt;
struct class *class;
};
(2) I2C 读写函数
// 发送命令到 BH1750
static int bh1750_write_cmd(struct i2c_client *client, u8 cmd)
{
return i2c_smbus_write_byte(client, cmd);
}
// 读取光照值(16bit)
static int bh1750_read_lux(struct i2c_client *client)
{
int lux = i2c_smbus_read_word_data(client, BH1750_CONT_H_RES_MODE);
if (lux < 0)
return lux;
// BH1750 数据是 Big-Endian 需要交换字节序
return ((lux & 0xFF) << 8) | (lux >> 8);
}
(3) 字符设备 read() 实现
static ssize_t bh1750_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
struct bh1750_dev *dev = filp->private_data;
int lux;
char kbuf[6]; // 存放字符串
lux = bh1750_read_lux(dev->client);
if (lux < 0)
return lux;
snprintf(kbuf, sizeof(kbuf), "%d\n", lux);
if (copy_to_user(buf, kbuf, strlen(kbuf)))
return -EFAULT;
return strlen(kbuf);
}
(4) 文件操作集
static const struct file_operations bh1750_fops = {
.owner = THIS_MODULE,
.read = bh1750_read,
.open = simple_open,
};
(5) Probe函数
static int bh1750_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
struct bh1750_dev *dev;
int ret;
dev = devm_kzalloc(&client->dev, sizeof(struct bh1750_dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
dev->client = client;
i2c_set_clientdata(client, dev);
// 发送开机指令
ret = bh1750_write_cmd(client, BH1750_POWER_ON);
if (ret < 0) {
dev_err(&client->dev, "Failed to power on BH1750\n");
return ret;
}
// 注册字符设备
ret = alloc_chrdev_region(&dev->devt, 0, 1, "bh1750");
if (ret < 0)
return ret;
cdev_init(&dev->cdev, &bh1750_fops);
ret = cdev_add(&dev->cdev, dev->devt, 1);
if (ret < 0)
goto unregister_chrdev;
dev->class = class_create(THIS_MODULE, "bh1750");
device_create(dev->class, NULL, dev->devt, NULL, "bh1750");
return 0;
unregister_chrdev:
unregister_chrdev_region(dev->devt, 1);
return ret;
}
(6) Remove函数
static int bh1750_remove(struct i2c_client *client)
{
struct bh1750_dev *dev = i2c_get_clientdata(client);
device_destroy(dev->class, dev->devt);
class_destroy(dev->class);
cdev_del(&dev->cdev);
unregister_chrdev_region(dev->devt, 1);
return 0;
}
(7) I2C 设备 ID 及驱动结构体
static const struct i2c_device_id bh1750_id[] = {
{ "bh1750", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, bh1750_id);
static struct i2c_driver bh1750_driver = {
.driver = {
.name = "bh1750",
},
.probe = bh1750_probe,
.remove = bh1750_remove,
.id_table = bh1750_id,
};
3. 动态编译、加载驱动
(1) 添加到 Makefile
obj-m += bh1750.o
(2) 编译驱动
make modules
make modules
命令会编译内核源代码树中的所有内核模块,并生成 .ko
文件
(3) 加载驱动
insmod bh1750.ko
(4) 创建设备节点
mknod /dev/bh1750 c 240 0 # (假设主设备号 240)
(5) 修改设备树
对于 BH1750 光照传感器这样的 I2C 设备,你需要在设备树中指定它的 I2C 地址、引脚、驱动信息等内容,确保内核能够识别并正确加载相应的驱动。
BH1750 设备树修改流程
1. 修改设备树文件
在 arch/arm64/boot/dts/
目录下,
&i2c1 {
status = "okay";
bh1750@23 {
compatible = "bh1750";
reg = <0x23>; // BH1750 默认的 I2C 地址
status = "okay";
};
};
2. 编译设备树
修改完成后,需要重新编译设备树,并将新的设备树文件加载到系统中。
make dtbs # 编译设备树文件
将编译生成的 .dtb
文件放置到NFS,并确保启动时加载新的设备树(bootagers)。
3. 驱动和设备树的关联
设备树中的 compatible
字段会与驱动程序的 of_match_table
进行匹配,以便加载相应的驱动程序。
为什么 I2C 不能直接使用中断读取数据
-
同步性与轮询机制:
- I2C 是同步通信协议,这意味着数据的发送和接收是由时钟信号(SCL)同步的,数据传输是由主设备控制的,从设备只能响应主设备的请求。
- 因为从设备在 I2C 协议中没有主动推送数据的机制,不能像 SPI 或 UART 等协议一样,主动向主设备发送数据并触发中断。换句话说,从设备不能在没有主设备请求的情况下主动中断并发送数据。
-
中断与异步通信的区别:
- 中断 是一种 异步 通信机制,它不依赖于特定的时序或控制信号,设备在某个事件发生时可以通过中断机制通知处理器进行相应操作。
- 然而,I2C 设备无法主动发起中断,因为它的工作模式是 主设备驱动的同步请求-响应模型。只有当主设备发出读取命令时,从设备才会返回数据。因此,I2C 通信的模式与中断式的异步通信是完全不兼容的。
可优化点
- 如何减少 CPU 开销? → 可以使用 I2C DMA 模式批量读取数据,避免 CPU 轮询。
- 如何让 BH1750 更省电? → 进入
0x00
关机模式,只有在需要时才POWER_ON
。 - 如果数据不稳定,如何优化? → 读取多次数据,滤波处理数据。