4、I2C/SPI寄存器读写驱动:Linux内核I2C子系统框架、I2C设备驱动编写(probe/remove/ioctl)、SPI设备驱动编写、寄存器读写封装与调试。
各位同学,欢迎来到第四章。
这一章,我们聊聊传感器驱动里最核心的活儿——寄存器读写。
你想想看,图像传感器本质上就是个复杂的模拟前端加数字控制逻辑。你要让它工作,就得通过I2C或SPI总线,往它肚子里塞一堆配置参数。曝光时间、增益、帧率、输出格式……全得靠寄存器来搞定。
我刚开始做传感器驱动时,觉得I2C读写不就是调个函数吗?后来发现,坑全在细节里。总线时序不对、设备地址搞错、寄存器地址长度没对齐……随便一个都能让你折腾半天。
好,咱们直接进入正题。
4.1 Linux内核I2C子系统框架
Linux内核的I2C子系统,说白了就是三层结构:
- I2C核心层:管理总线、设备、驱动的注册与匹配。
- I2C总线驱动层:处理硬件时序,比如i2c-imx、i2c-tegra这些。
- I2C设备驱动层:咱们要写的部分,负责和具体传感器通信。
我个人习惯把I2C子系统想象成一个“中介”。你不需要关心底层怎么拉高拉低SCL/SDA线,只需要告诉核心层:我要读哪个设备、哪个寄存器、读几个字节。剩下的,核心层帮你搞定。
这里有个关键数据结构——i2c_client。它代表一个挂在I2C总线上的设备。驱动里所有操作,都围绕这个client展开。
struct i2c_client {
unsigned short addr; // 7位或10位设备地址
char name[I2C_NAME_SIZE];
struct i2c_adapter *adapter; // 所属的I2C适配器
// ... 其他成员
};
4.2 I2C设备驱动编写:probe/remove/ioctl
一个标准的I2C设备驱动,核心就三个回调函数:probe、remove、ioctl。
probe函数:设备匹配成功时调用。在这里做初始化、分配资源、注册设备节点。
static int sensor_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
struct sensor_dev *sdev;
int ret;
// 分配私有数据结构
sdev = devm_kzalloc(&client->dev, sizeof(*sdev), GFP_KERNEL);
if (!sdev)
return -ENOMEM;
sdev->client = client;
i2c_set_clientdata(client, sdev);
// 初始化互斥锁
mutex_init(&sdev->lock);
// 注册字符设备或V4L2设备
// ... 这里省略具体注册代码
dev_info(&client->dev, "sensor probe success\n");
return 0;
}
remove函数:设备移除时调用。释放资源、注销设备。
static int sensor_remove(struct i2c_client *client)
{
struct sensor_dev *sdev = i2c_get_clientdata(client);
// 注销设备
// ... 清理工作
mutex_destroy(&sdev->lock);
dev_info(&client->dev, "sensor removed\n");
return 0;
}
ioctl函数:用户空间通过设备节点发命令。比如设置曝光时间、读取传感器温度。
static long sensor_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct sensor_dev *sdev = file->private_data;
int ret = 0;
switch (cmd) {
case SENSOR_IOC_SET_EXPOSURE:
// 从arg解析曝光值,写入寄存器
ret = sensor_write_reg(sdev, REG_EXPOSURE, (u16)arg);
break;
case SENSOR_IOC_GET_TEMP:
// 读取温度寄存器
ret = sensor_read_reg(sdev, REG_TEMP, &temp);
if (ret == 0)
ret = copy_to_user((void __user *)arg, &temp, sizeof(temp));
break;
default:
return -ENOTTY;
}
return ret;
}
_IOC_DIR(cmd)检查,花不了几行代码。
4.3 SPI设备驱动编写
SPI和I2C最大的区别是什么?
SPI是四线制(MOSI、MISO、SCLK、CS),全双工,速度更快。图像传感器里,很多高帧率、高分辨率的型号都用SPI来配置寄存器,因为I2C的100kHz/400kHz速度有时候跟不上。
SPI设备驱动的框架和I2C类似,但通信方式不同。核心结构体是spi_device。
static int sensor_spi_probe(struct spi_device *spi)
{
struct sensor_dev *sdev;
// 设置SPI模式、时钟频率
spi->mode = SPI_MODE_0;
spi->max_speed_hz = 10 * 1000 * 1000; // 10MHz
spi_setup(spi);
sdev = devm_kzalloc(&spi->dev, sizeof(*sdev), GFP_KERNEL);
if (!sdev)
return -ENOMEM;
sdev->spi = spi;
spi_set_drvdata(spi, sdev);
// 后续初始化...
return 0;
}
SPI读写寄存器时,我习惯封装一个通用函数。因为很多传感器要求先发地址,再发数据,或者地址和数据一起发。
static int sensor_spi_write_reg(struct sensor_dev *sdev, u16 reg, u8 val)
{
u8 tx_buf[3];
struct spi_transfer t = {
.tx_buf = tx_buf,
.len = 3,
};
struct spi_message m;
tx_buf[0] = (reg >> 8) & 0xFF; // 寄存器高字节
tx_buf[1] = reg & 0xFF; // 寄存器低字节
tx_buf[2] = val; // 要写入的数据
spi_message_init(&m);
spi_message_add_tail(&t, &m);
return spi_sync(sdev->spi, &m);
}
4.4 寄存器读写封装与调试
这部分,我觉得是驱动开发里最体现功力的地方。
好的封装,能让上层代码完全不知道底层是I2C还是SPI。我一般会定义一组抽象接口:
struct sensor_reg_ops {
int (*read)(struct sensor_dev *sdev, u16 reg, u8 *val);
int (*write)(struct sensor_dev *sdev, u16 reg, u8 val);
int (*read_multi)(struct sensor_dev *sdev, u16 reg, u8 *buf, int len);
int (*write_multi)(struct sensor_dev *sdev, u16 reg, u8 *buf, int len);
};
然后在probe时根据总线类型,挂载不同的实现。这样,上层配置算法调用sdev->ops->write(sdev, 0x3000, 0x42)就行了,不用关心底层细节。
调试技巧,嗯,这里我得好好说说。
寄存器读写出问题,最常见的现象就是传感器没反应,或者图像异常。我一般按这个顺序排查:
- 确认设备地址:用i2cdetect或spidev_test工具扫一下,看设备是否应答。
- 检查时序:用示波器或逻辑分析仪抓波形。SCL频率对不对?数据是否在时钟上升沿稳定?
- 验证寄存器地址:有些传感器寄存器地址是16位的,有些是8位的。写错了地址,读回来的全是0xFF或0x00。
- 加打印:在读写函数里加dev_dbg,打印每次操作的寄存器地址和值。配合动态调试,可以精确定位问题。
最后,分享一个我曾经踩过的坑。
有一次调试一款SPI接口的传感器,读寄存器总是返回0。折腾了两天,换芯片、换线、换逻辑分析仪……最后发现,传感器要求寄存器地址发送时,最高位要置1表示读操作。数据手册里写的是“Bit15: R/W#”,我愣是没注意。从那以后,我读任何数据手册,都会先把“Register Access”章节翻三遍。
好了,这一章的内容就到这里。
I2C和SPI的寄存器读写,是传感器驱动的基石。你把这个基础打牢了,后面写V4L2驱动、写控制算法,都会顺手很多。
下一章,我们聊聊V4L2框架和视频数据流。到时候见。