3. I2C/SPI 控制总线驱动:传感器寄存器读写、时序分析、Linux 内核 I2C 子系统、驱动框架编写
好,我们进入第三章。这一章聊的是控制总线——I2C 和 SPI。
你想想看,图像传感器再厉害,也得有人给它下指令。曝光时间、增益、帧率、输出格式……这些参数都藏在传感器的寄存器里。怎么读写?靠的就是 I2C 或 SPI 总线。
说白了,控制总线就是嵌入式系统和传感器之间的“对话通道”。通道不通,后面所有调优都是空谈。我在项目中遇到过好几次,图像出问题,查了半天发现是 I2C 时序不对,寄存器根本没写进去。
3.1 传感器寄存器读写:从原理到实战
传感器的寄存器,本质就是一堆 8 位或 16 位的存储单元。每个寄存器有一个地址,里面存着配置值。
读写流程其实很简单:
- 写操作:主机发送设备地址 + 写标志 → 寄存器地址 → 要写入的数据。
- 读操作:主机发送设备地址 + 写标志 → 寄存器地址 → 重启条件 → 设备地址 + 读标志 → 读取数据。
关键点:很多传感器支持“连续读写”。你只需要发送起始地址,然后连续发时钟,传感器会自动递增地址。批量配置时效率高很多。
我个人的习惯是,拿到一个新传感器,先用逻辑分析仪抓一下官方驱动板的读写波形。确认地址、数据格式、字节顺序(大端/小端)都对得上,再开始写驱动。这一步能省后面大量调试时间。
3.2 时序分析:为什么你的 I2C 不通?
时序问题,是控制总线最常见的坑。尤其是 I2C,两根线(SCL、SDA)要满足严格的时序要求。
| 参数 | 标准模式 (100kHz) | 快速模式 (400kHz) | 说明 |
|---|---|---|---|
| SCL 时钟频率 | ≤ 100 kHz | ≤ 400 kHz | 由主机决定 |
| 起始条件保持时间 | ≥ 4.0 μs | ≥ 0.6 μs | SCL 高电平时 SDA 下降 |
| 数据建立时间 | ≥ 250 ns | ≥ 100 ns | SCL 上升沿前数据稳定 |
| 数据保持时间 | ≥ 0 ns | ≥ 0 ns | SCL 下降沿后数据保持 |
| 停止条件建立时间 | ≥ 4.0 μs | ≥ 0.6 μs | SCL 高电平时 SDA 上升 |
我曾经在一个项目里,I2C 总线长度超过 30cm,上拉电阻用了 4.7kΩ,结果在 400kHz 下波形严重畸变。后来换成 2.2kΩ 上拉,问题解决。嗯,这里要注意:总线电容和上拉电阻要匹配。长线或从设备多时,适当减小上拉电阻。
避坑指南:我曾经遇到过传感器在高温下 I2C 通信失败。查了半天,发现是 PCB 走线太长,信号反射导致建立时间不足。最后在 SDA 和 SCL 上各串了一个 22Ω 的电阻,问题解决。所以,时序问题不光是软件的事,硬件设计也很关键。
3.3 Linux 内核 I2C 子系统:架构与核心数据结构
Linux 内核的 I2C 子系统,设计得很清晰。它分三层:
- I2C 核心层:提供统一的接口,管理适配器和设备。
- I2C 适配器层:对应具体的硬件控制器(如 SoC 的 I2C 控制器)。
- I2C 设备驱动层:我们写的就是这一层。
核心数据结构就三个:
struct i2c_adapter:代表一个 I2C 控制器。struct i2c_client:代表一个 I2C 从设备(传感器)。struct i2c_driver:代表设备驱动。
为什么说这个架构好?因为它把硬件细节隔离了。你写传感器驱动时,根本不用管底层怎么发时钟、怎么处理中断。直接用 i2c_transfer() 或 i2c_smbus_read_byte_data() 就行。
个人经验:我建议新手先用 i2c_smbus_* 系列函数。它们封装了常见的读写模式,代码更简洁。等需要优化性能时,再改用 i2c_transfer() 直接操作消息队列。
3.4 驱动框架编写:从零开始写一个传感器驱动
好,我们动手写一个。假设传感器是 OV5640,I2C 地址 0x3C(7 位地址 0x1E)。
驱动的基本骨架:
#include <linux/i2c.h>
#include <linux/module.h>
static int ov5640_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
// 1. 初始化传感器
// 2. 注册 V4L2 子设备
// 3. 创建 sysfs 接口(可选)
return 0;
}
static int ov5640_remove(struct i2c_client *client)
{
// 反初始化
return 0;
}
static const struct i2c_device_id ov5640_id[] = {
{ "ov5640", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, ov5640_id);
static struct i2c_driver ov5640_driver = {
.driver = {
.name = "ov5640",
},
.probe = ov5640_probe,
.remove = ov5640_remove,
.id_table = ov5640_id,
};
module_i2c_driver(ov5640_driver);
MODULE_LICENSE("GPL");
寄存器读写函数,我习惯这样封装:
static int ov5640_write_reg(struct i2c_client *client,
u16 reg, u8 val)
{
u8 buf[3];
buf[0] = reg >> 8; // 高字节
buf[1] = reg & 0xFF; // 低字节
buf[2] = val;
return i2c_master_send(client, buf, 3);
}
static int ov5640_read_reg(struct i2c_client *client,
u16 reg, u8 *val)
{
u8 reg_buf[2];
reg_buf[0] = reg >> 8;
reg_buf[1] = reg & 0xFF;
struct i2c_msg msg[2] = {
{
.addr = client->addr,
.flags = 0,
.len = 2,
.buf = reg_buf,
},
{
.addr = client->addr,
.flags = I2C_M_RD,
.len = 1,
.buf = val,
},
};
return i2c_transfer(client->adapter, msg, 2);
}
注意:OV5640 的寄存器地址是 16 位的,所以要先发高字节再发低字节。不同传感器的地址格式可能不同,一定要看 datasheet。
驱动写好后,怎么和设备树匹配?在设备树里这样写:
&i2c2 {
status = "okay";
clock-frequency = <400000>;
ov5640: camera@3c {
compatible = "ovti,ov5640";
reg = <0x3c>;
// 其他属性:电源、复位、时钟等
};
};
内核启动时,I2C 核心会根据 compatible 属性和 id_table 匹配,自动调用 probe 函数。
3.5 知识体系总览
这一章的内容,我用一张图帮你理清脉络:
3.6 调试技巧与常见问题
驱动写完了,怎么验证?我一般用这几个方法:
- i2cdetect:扫描总线,确认设备地址是否正确。
- i2cget / i2cset:手动读写寄存器,验证通信。
- 逻辑分析仪:抓波形,看时序是否满足要求。
- 内核调试打印:在驱动里加
dev_dbg(),跟踪每次读写的结果。
小技巧:我习惯在 probe 函数里先读传感器的 ID 寄存器。如果读到的值跟 datasheet 对不上,直接返回 -ENODEV。这样能快速判断 I2C 通信是否正常。
常见问题:
- 设备地址不对:7 位地址和 8 位地址搞混。I2C 设备地址通常是 7 位,但很多 datasheet 给的是 8 位(左移一位后的值)。
- 时序不满足:时钟频率太高,或上拉电阻不合适。
- 寄存器地址格式错误:16 位地址和 8 位地址混用。
- 设备树匹配失败:compatible 字符串和 id_table 不一致。
嗯,这一章的内容就到这里。控制总线驱动是传感器驱动的基石,把 I2C/SPI 搞透,后面写任何传感器驱动都会顺手很多。