3. I2C/SPI控制总线:Sensor寄存器读写机制、时序要求、Linux内核I2C驱动框架
做Sensor驱动移植,说白了就是跟寄存器打交道。而跟寄存器通信,绕不开两条总线——I2C和SPI。我刚开始接触这行时,总觉得不就是读写几个寄存器嘛,能有多复杂?直到有一次调一颗OV的Sensor,死活读不到ID,折腾了两天才发现是I2C时序上有个上拉电阻没焊好。嗯,从那以后我再也不敢小看总线这块了。
3.1 Sensor寄存器读写机制
Sensor内部有一堆寄存器,用来控制曝光、增益、帧率、输出格式等等。我们通过总线往这些寄存器里写值,或者读出现有的值,就能配置Sensor的工作状态。
读写机制其实很简单:
- 写操作:主机发送设备地址 + 寄存器地址 + 数据
- 读操作:主机发送设备地址 + 寄存器地址,然后重新发起读操作,从设备返回数据
这里有个坑——寄存器地址长度。有的Sensor用8位地址,有的用16位。我遇到过一颗Sensor,datasheet上写的是16位地址,但实际读出来全是0xFF。后来发现它其实支持8位和16位两种模式,需要通过某个特殊寄存器切换。你说坑不坑?
关键点:读写Sensor寄存器时,务必确认以下几点:
- 设备地址(7位还是10位)
- 寄存器地址宽度(8位还是16位)
- 数据宽度(通常是8位,但也有16位的)
- 是否需要页选(Page Select)机制
3.2 I2C时序要求
I2C是Sensor最常用的控制总线。它只有两根线——SCL(时钟)和SDA(数据)。但别小看这两根线,时序要求很严格。
标准的I2C时序参数包括:
| 参数 | 标准模式(100kHz) | 快速模式(400kHz) | 高速模式(3.4MHz) |
|---|---|---|---|
| SCL时钟频率 | ≤100kHz | ≤400kHz | ≤3.4MHz |
| 起始条件保持时间 | ≥4.0μs | ≥0.6μs | ≥0.16μs |
| 数据建立时间 | ≥250ns | ≥100ns | ≥10ns |
| SCL低电平时间 | ≥4.7μs | ≥1.3μs | ≥0.32μs |
| SCL高电平时间 | ≥4.0μs | ≥0.6μs | ≥0.12μs |
我个人习惯在调试时先用100kHz跑通,再慢慢往上提频率。为什么?因为高频下信号反射、串扰的问题会更明显。我曾经在一个项目里,Sensor离主控有10cm的排线,400kHz下通信时好时坏,降到200kHz就稳了。后来加了上拉电阻和串联电阻才搞定。
调试技巧:用示波器抓I2C波形时,注意看SCL和SDA的上升沿和下降沿是否陡峭。如果边沿太缓,说明上拉电阻太大或者总线电容太大。一般4.7kΩ上拉电阻是个不错的起点。
3.3 SPI时序要求
SPI比I2C快得多,适合需要大量读写寄存器的场景。它用四根线:SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。
SPI有四种模式,由时钟极性(CPOL)和时钟相位(CPHA)决定:
| 模式 | CPOL | CPHA | 说明 |
|---|---|---|---|
| 模式0 | 0 | 0 | 空闲时SCLK为低,数据在上升沿采样 |
| 模式1 | 0 | 1 | 空闲时SCLK为低,数据在下降沿采样 |
| 模式2 | 1 | 0 | 空闲时SCLK为高,数据在下降沿采样 |
| 模式3 | 1 | 1 | 空闲时SCLK为高,数据在上升沿采样 |
Sensor最常用的是模式0和模式3。你想想看,如果模式搞错了,读回来的数据全是乱的。我建议在驱动初始化时,先读一下Sensor的ID寄存器,确认通信正常后再做后续配置。
注意:SPI的片选信号(CS)在每次传输前后都要拉低再拉高。有些Sensor要求CS在整帧配置期间保持低电平,否则会复位内部状态机。这个一定要看datasheet的时序图。
3.4 Linux内核I2C驱动框架
在Linux下写Sensor驱动,我们通常不会直接操作I2C控制器寄存器。内核已经封装好了I2C核心框架,我们只需要调用接口就行。
核心的数据结构就两个:
struct i2c_client:代表一个I2C设备,包含设备地址、适配器等信息struct i2c_driver:代表I2C设备驱动,包含probe、remove等回调函数
典型的驱动注册流程是这样的:
// 1. 定义i2c_driver结构体
static struct i2c_driver sensor_i2c_driver = {
.driver = {
.name = "my_sensor",
.of_match_table = sensor_of_match,
},
.probe = sensor_probe,
.remove = sensor_remove,
.id_table = sensor_id_table,
};
// 2. 在probe函数中初始化Sensor
static int sensor_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
// 保存client指针
struct sensor_dev *sensor = devm_kzalloc(&client->dev,
sizeof(*sensor),
GFP_KERNEL);
sensor->client = client;
i2c_set_clientdata(client, sensor);
// 读取Sensor ID确认通信
u8 chip_id = i2c_smbus_read_byte_data(client, 0x00);
if (chip_id != EXPECTED_ID) {
dev_err(&client->dev, "Sensor ID mismatch\n");
return -ENODEV;
}
// 注册V4L2子设备
// ...
return 0;
}
// 3. 注册驱动
module_i2c_driver(sensor_i2c_driver);
读写寄存器的常用API:
i2c_smbus_read_byte_data(client, reg):读一个字节i2c_smbus_write_byte_data(client, reg, val):写一个字节i2c_smbus_read_i2c_block_data(client, reg, len, buf):连续读多个字节i2c_smbus_write_i2c_block_data(client, reg, len, buf):连续写多个字节
这里有个经验——批量读写比单字节读写快得多。我在配置Sensor的寄存器序列时,会把连续的寄存器合并成一次批量操作。比如配置一组增益寄存器,用i2c_smbus_write_i2c_block_data一次写完,比逐个写快10倍以上。
避坑指南:我曾经遇到一个问题——用i2c_smbus_read_byte_data读寄存器,返回值总是0xFF。查了半天发现是I2C控制器不支持SMBus的快速读写命令。后来改用i2c_transfer直接构造I2C消息才解决。所以,如果你的平台比较特殊,建议直接用i2c_transfer,它是最底层的接口,兼容性最好。
3.5 知识体系总览
下面这张图把本章的核心逻辑串起来了。从物理层的时序,到驱动层的框架,再到应用层的读写机制,一条线下来就清晰了。
嗯,这一章的内容就这些。I2C和SPI是Sensor驱动的基本功,时序搞清楚了,驱动框架用熟了,后面移植Sensor就是水到渠成的事。
公众号:蓝海资料掘金营,微信deep3321