3. Sensor驱动核心:I2C/SPI控制接口,寄存器读写实战
好,咱们进入正题。Sensor驱动里最基础、也最核心的活儿,就是通过I2C或SPI去读写寄存器。说白了,你写的驱动能不能让Sensor正常干活,全看这一步稳不稳。
我个人习惯,拿到一块新的Sensor模组,第一件事不是看数据手册里的那些高大上的算法,而是先把I2C/SPI的读写调通。为什么?因为这是你和Sensor对话的唯一通道。通道不通,后面全是白搭。
3.1 为什么是I2C和SPI?
Camera Sensor的控制接口,主流就两种:I2C和SPI。你可能会问,为什么不用UART?嗯,UART虽然简单,但速度慢,而且多设备挂载不方便。Sensor的控制数据量不大,但对时序要求高,I2C和SPI正好合适。
| 特性 | I2C | SPI |
|---|---|---|
| 引脚数 | 2线(SCL, SDA) | 4线(SCLK, MOSI, MISO, CS) |
| 速度 | 标准100kHz~400kHz,快速可达1MHz | 通常10MHz~50MHz |
| 多设备 | 地址寻址,支持多设备 | 片选CS,一主多从 |
| 适用场景 | 寄存器配置、参数读取 | 高速配置、固件下载 |
我在项目中遇到过,有些Sensor为了省成本,只给了I2C接口。但像索尼IMX系列的高端Sensor,往往同时支持I2C和SPI。我建议,如果硬件设计允许,优先用SPI做配置,速度更快,调试也更方便。
3.2 寄存器读写的基础套路
不管I2C还是SPI,读写寄存器的套路其实差不多。核心就是:地址 + 数据。
你想想看,Sensor内部有成千上万个寄存器,每个寄存器都有一个唯一的地址。你要做的就是:
- 告诉Sensor你要访问哪个地址
- 然后读或写这个地址里的数据
就这么简单。但实际写代码时,坑往往出在细节上。
3.2.1 I2C读写流程
I2C的读写,标准流程是这样的:
// 写寄存器
1. 发送起始信号
2. 发送设备地址 + 写位(0)
3. 等待ACK
4. 发送寄存器地址(高字节)
5. 等待ACK
6. 发送寄存器地址(低字节)
7. 等待ACK
8. 发送数据
9. 等待ACK
10. 发送停止信号
// 读寄存器
1. 发送起始信号
2. 发送设备地址 + 写位(0)
3. 等待ACK
4. 发送寄存器地址(高字节)
5. 等待ACK
6. 发送寄存器地址(低字节)
7. 等待ACK
8. 发送重复起始信号
9. 发送设备地址 + 读位(1)
10. 等待ACK
11. 读取数据
12. 发送NACK
13. 发送停止信号
嗯,这里要注意:很多Sensor的寄存器地址是16位的,需要分高字节和低字节发送。我刚开始做驱动时,就犯过这个错——只发了低字节,结果读出来的数据全是乱的。
关键点:I2C读操作时,需要先发一个写操作来设置寄存器地址,然后再发一个重复起始信号切换为读模式。这个重复起始信号不能省略,否则Sensor会懵掉。
3.2.2 SPI读写流程
SPI就简单多了,没有地址的概念,全靠片选信号区分设备。读写流程:
// SPI写寄存器
1. 拉低CS(片选)
2. 发送命令字节(通常写命令为0x02)
3. 发送寄存器地址(16位,分两次发送)
4. 发送数据
5. 拉高CS
// SPI读寄存器
1. 拉低CS
2. 发送命令字节(通常读命令为0x03)
3. 发送寄存器地址(16位,分两次发送)
4. 读取数据
5. 拉高CS
SPI的好处是,读写可以同时进行。你在发送地址的同时,MISO线上可能已经在返回数据了。但要注意,很多Sensor的SPI模式是模式0(CPOL=0, CPHA=0),数据在时钟上升沿采样。如果搞错了模式,读出来的数据全是0xFF或者0x00。
避坑指南:我曾经在一个项目里,SPI读寄存器总是返回0xFF。查了两天,最后发现是Sensor的SPI模式是模式3(CPOL=1, CPHA=1),而我代码里配的是模式0。改了一行代码,问题解决。所以,拿到Sensor后,第一件事就是确认SPI模式。
3.3 实战:写一个通用的I2C读写函数
好了,理论说完了,咱们直接上代码。我习惯把I2C读写封装成两个函数,这样上层调用起来特别清爽。
#include <linux/i2c.h>
#include <linux/delay.h>
#define SENSOR_I2C_ADDR 0x6C // 假设Sensor的I2C地址
// 写寄存器:16位地址,8位数据
static int sensor_write_reg(struct i2c_client *client, u16 reg, u8 val)
{
int ret;
u8 buf[3];
buf[0] = (reg >> 8) & 0xFF; // 寄存器地址高字节
buf[1] = reg & 0xFF; // 寄存器地址低字节
buf[2] = val; // 要写入的数据
ret = i2c_master_send(client, buf, 3);
if (ret < 0) {
dev_err(&client->dev, "写寄存器失败: reg=0x%04x, val=0x%02x\n", reg, val);
return ret;
}
// 有些Sensor需要一点延时才能接受下一次操作
usleep_range(100, 200);
return 0;
}
// 读寄存器:16位地址,返回8位数据
static int sensor_read_reg(struct i2c_client *client, u16 reg, u8 *val)
{
int ret;
u8 addr_buf[2];
addr_buf[0] = (reg >> 8) & 0xFF;
addr_buf[1] = reg & 0xFF;
// 先发送寄存器地址
ret = i2c_master_send(client, addr_buf, 2);
if (ret < 0) {
dev_err(&client->dev, "发送寄存器地址失败: reg=0x%04x\n", reg);
return ret;
}
// 再读取数据
ret = i2c_master_recv(client, val, 1);
if (ret < 0) {
dev_err(&client->dev, "读取寄存器失败: reg=0x%04x\n", reg);
return ret;
}
return 0;
}
你看,代码其实不长。但有几个细节我特别想强调:
- 地址顺序:先高字节后低字节,这是行业惯例。但有些Sensor是反的,先低后高。一定要看数据手册确认。
- 延时:写操作后加一个usleep,不是所有Sensor都需要,但加上更保险。我遇到过一些Sensor,连续写太快会丢数据。
- 错误处理:每个i2c操作都要检查返回值。别偷懒,否则出了问题你都不知道是硬件没连上还是代码写错了。
3.4 实战:SPI读写函数
SPI的读写函数,套路也差不多。但要注意,SPI的读写通常是一次性完成的。
#include <linux/spi/spi.h>
#define SPI_CMD_WRITE 0x02
#define SPI_CMD_READ 0x03
// SPI写寄存器
static int sensor_spi_write_reg(struct spi_device *spi, u16 reg, u8 val)
{
int ret;
u8 tx_buf[4];
tx_buf[0] = SPI_CMD_WRITE; // 写命令
tx_buf[1] = (reg >> 8) & 0xFF; // 地址高字节
tx_buf[2] = reg & 0xFF; // 地址低字节
tx_buf[3] = val; // 数据
ret = spi_write(spi, tx_buf, 4);
if (ret < 0) {
dev_err(&spi->dev, "SPI写失败: reg=0x%04x\n", reg);
return ret;
}
return 0;
}
// SPI读寄存器
static int sensor_spi_read_reg(struct spi_device *spi, u16 reg, u8 *val)
{
int ret;
u8 tx_buf[4] = {0};
u8 rx_buf[4] = {0};
tx_buf[0] = SPI_CMD_READ; // 读命令
tx_buf[1] = (reg >> 8) & 0xFF;
tx_buf[2] = reg & 0xFF;
tx_buf[3] = 0; // dummy字节,用于产生时钟
// SPI读写同时进行
ret = spi_write_then_read(spi, tx_buf, 4, rx_buf, 4);
if (ret < 0) {
dev_err(&spi->dev, "SPI读失败: reg=0x%04x\n", reg);
return ret;
}
// 注意:读到的数据在rx_buf[3]里,因为前3个字节是命令和地址
*val = rx_buf[3];
return 0;
}
小技巧:SPI读操作时,发送的dummy字节(0x00)不能省略。因为SPI是同步通信,你不发数据,时钟就不会产生,Sensor也就不会把数据推出来。我见过有人把dummy字节写成0xFF,也能用,但0x00更规范。
3.5 调试技巧:如何验证读写是否正确?
代码写完了,怎么验证?我一般会这么做:
- 读芯片ID寄存器:几乎所有Sensor都有一个芯片ID寄存器(比如地址0x0000或0x0001),里面存着一个固定值。如果能正确读出来,说明I2C/SPI通路是通的。
- 写一个已知寄存器再读回来:比如写0xAA到某个测试寄存器,再读回来看看是不是0xAA。注意,有些寄存器是只读的,写进去也没用。
- 用逻辑分析仪抓波形:这是最靠谱的方法。看看SCL/SDA或者SCLK/MOSI/MISO的时序对不对。我遇到过硬件上SDA上拉电阻没焊,导致通信时好时坏,用逻辑分析仪一眼就看出来了。
嗯,说到逻辑分析仪,我建议每个做驱动的工程师都备一个。便宜的几十块钱,但能帮你省下几天的调试时间。
3.6 常见问题与避坑
最后,我总结几个我踩过的坑,希望能帮你少走弯路:
- I2C地址不对:Sensor的I2C地址通常是7位,但Linux内核里用的是8位地址(左移一位)。别搞混了。
- 寄存器地址长度:有些Sensor用8位地址,有些用16位。一定要看数据手册确认。
- 时序问题:I2C的时钟频率不能太高,否则Sensor跟不上。我遇到过把I2C频率设到400kHz,结果Sensor死活不响应,降到100kHz就好了。
- SPI的CS控制:有些Sensor要求CS在每次传输后必须拉高一段时间,否则会进入错误状态。这个时间在数据手册里通常叫"CS de-assert time"。
好了,这一章的内容就到这里。说白了,I2C/SPI的寄存器读写就是Sensor驱动的"敲门砖"。这块搞定了,后面配置Sensor的曝光、增益、帧率什么的,就只是往寄存器里填数据的事了。
下一章,咱们会聊聊如何通过I2C/SPI配置Sensor的上电序列和初始化流程。到时候,你会看到这些读写函数是怎么被串起来用的。