第三章 Sensor驱动开发:I2C/SPI通信协议、sensor寄存器配置、V4L2 subdev注册、sensor控制接口实现(曝光、增益、帧率)
好,我们直接进入正题。Sensor驱动,说白了就是让摄像头芯片听话。你得告诉它怎么曝光、怎么放大信号、多久出一帧画面。而这些指令,全靠I2C或SPI总线传过去。今天我就把这块掰开了讲清楚。
3.1 I2C/SPI通信协议:选对路子,少走弯路
先说说通信协议。Sensor和主控之间,最常见的就两种:I2C和SPI。我个人习惯,能走I2C就不走SPI,除非sensor寄存器太多或者帧率要求极高。
I2C的特点:两根线(SCL、SDA),地址7位或10位,速度一般400kHz或1MHz。我遇到过不少新手,上来就把I2C时钟配成3.4MHz,结果sensor根本不响应。为什么?因为很多老款sensor只支持标准模式(100kHz)或快速模式(400kHz)。
核心要点:I2C通信时,一定要先发设备地址(7位左移1位,最后一位是读写位),再发寄存器地址,最后发数据。每次传输后都要检查ACK信号。
SPI的特点:四根线(SCLK、MOSI、MISO、CS),全双工,速度可以跑到几十MHz。适合寄存器多、需要批量配置的场景。但要注意,SPI的时序模式(CPOL、CPHA)必须和sensor手册完全一致,否则数据全是乱的。
我的经验:调试SPI时,先用逻辑分析仪抓波形。我曾经花了两天时间查一个sensor初始化失败的问题,最后发现是CS片选信号拉低后没等足够时间就发数据了。sensor手册上写的是CS拉低后至少等1us,我代码里只等了500ns。
3.2 Sensor寄存器配置:从手册到代码
拿到一款新sensor,第一件事就是看寄存器手册。别急着写代码,先把关键寄存器梳理清楚。我一般会列个表格:
| 寄存器地址 | 寄存器名称 | 默认值 | 功能描述 |
|---|---|---|---|
| 0x0100 | MODE_SELECT | 0x00 | 软件复位/进入待机模式 |
| 0x0202 | EXPOSURE_H | 0x00 | 曝光时间高8位 |
| 0x0203 | EXPOSURE_L | 0x00 | 曝光时间低8位 |
| 0x0204 | GAIN_H | 0x00 | 增益高8位 |
| 0x0205 | GAIN_L | 0x00 | 增益低8位 |
| 0x0300 | FRAME_LENGTH_H | 0x0E | 帧长高8位(决定帧率) |
| 0x0301 | FRAME_LENGTH_L | 0x10 | 帧长低8位 |
配置sensor时,我建议按这个顺序来:
- 先软件复位(写0x0103为0x01)
- 等待至少10ms,让sensor内部稳定
- 配置PLL和时钟分频
- 配置输出格式(RAW8、RAW10、YUV等)
- 配置分辨率(窗口裁剪、binning等)
- 配置帧率(通过帧长和行时间计算)
- 最后退出待机模式(写0x0100为0x01)
注意:很多sensor在待机模式下才能修改关键寄存器。如果你在streaming状态下写曝光值,可能不会生效。我曾经踩过这个坑,sensor手册里写得很清楚,但我没仔细看,结果曝光死活调不动。
3.3 V4L2 subdev注册:让内核认识你的sensor
在Linux内核里,sensor被抽象成一个V4L2 subdev。注册过程其实不复杂,但有几个关键点要注意。
先看核心代码框架:
static struct v4l2_subdev_ops sensor_subdev_ops = {
.core = &sensor_core_ops,
.video = &sensor_video_ops,
.pad = &sensor_pad_ops,
};
static int sensor_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct sensor_dev *sensor;
struct v4l2_subdev *sd;
sensor = devm_kzalloc(&client->dev, sizeof(*sensor), GFP_KERNEL);
if (!sensor)
return -ENOMEM;
sd = &sensor->subdev;
v4l2_i2c_subdev_init(sd, client, &sensor_subdev_ops);
// 注册到V4L2框架
ret = v4l2_async_register_subdev(sd);
if (ret < 0) {
dev_err(&client->dev, "failed to register subdev\n");
return ret;
}
return 0;
}
这里有个细节:v4l2_i2c_subdev_init会自动把i2c_client和subdev关联起来。你后面读写寄存器时,直接用i2c_smbus_write_byte_data或i2c_smbus_read_byte_data就行。
我的习惯:在probe函数里,我会先读sensor的chip ID寄存器,确认硬件连接正确。比如:
chip_id = i2c_smbus_read_word_data(client, 0x0000);
if (chip_id != 0x1234) {
dev_err(&client->dev, "chip ID mismatch: 0x%04x\n", chip_id);
return -ENODEV;
}
这一步能帮你快速定位硬件问题,省得后面调试时抓瞎。
3.4 Sensor控制接口实现:曝光、增益、帧率
这部分是驱动开发的核心。用户空间的ISP框架(比如Exynos的FIMC-IS)会通过V4L2的控制接口来调节sensor参数。你需要实现s_s_ctrl回调函数。
先看控制ID的定义:
static const struct v4l2_ctrl_ops sensor_ctrl_ops = {
.s_ctrl = sensor_s_ctrl,
};
static int sensor_init_controls(struct sensor_dev *sensor)
{
struct v4l2_ctrl_handler *hdl = &sensor->ctrl_handler;
v4l2_ctrl_handler_init(hdl, 3);
v4l2_ctrl_new_std(hdl, &sensor_ctrl_ops,
V4L2_CID_EXPOSURE_ABSOLUTE,
0, 0xFFFF, 1, 0x0100);
v4l2_ctrl_new_std(hdl, &sensor_ctrl_ops,
V4L2_CID_GAIN,
0, 0xFF, 1, 0x40);
v4l2_ctrl_new_std(hdl, &sensor_ctrl_ops,
V4L2_CID_PIXEL_RATE,
0, 100000000, 1, 72000000);
sensor->subdev.ctrl_handler = hdl;
return 0;
}
然后是实现控制逻辑:
static int sensor_s_ctrl(struct v4l2_ctrl *ctrl)
{
struct sensor_dev *sensor = container_of(ctrl->handler,
struct sensor_dev, ctrl_handler);
u16 exposure, gain;
u32 frame_length;
switch (ctrl->id) {
case V4L2_CID_EXPOSURE_ABSOLUTE:
exposure = ctrl->val;
// 写曝光寄存器
sensor_write(sensor, 0x0202, (exposure >> 8) & 0xFF);
sensor_write(sensor, 0x0203, exposure & 0xFF);
break;
case V4L2_CID_GAIN:
gain = ctrl->val;
// 写增益寄存器
sensor_write(sensor, 0x0204, (gain >> 8) & 0xFF);
sensor_write(sensor, 0x0205, gain & 0xFF);
break;
case V4L2_CID_PIXEL_RATE:
// 根据像素时钟计算帧长
frame_length = ctrl->val / sensor->line_time;
sensor_write(sensor, 0x0300, (frame_length >> 8) & 0xFF);
sensor_write(sensor, 0x0301, frame_length & 0xFF);
break;
}
return 0;
}
关键点:曝光和增益的调节顺序很重要。我建议先调曝光,再调增益。因为增益会放大噪声,如果先调增益再调曝光,画面噪点会很明显。另外,帧率和曝光是耦合的——曝光时间不能超过帧周期。你想想看,如果帧率是30fps(33ms一帧),曝光设成50ms,那画面就乱套了。
3.5 避坑指南:我踩过的那些雷
最后分享几个实战中容易出问题的地方:
- I2C地址不对:sensor手册上给的地址可能是7位值,但Linux驱动里要用8位值(左移1位)。我见过有人直接拿7位地址去读写,结果设备永远找不到。
- 寄存器写入顺序:有些sensor要求先写高8位再写低8位,有些则相反。一定要看手册里的时序图。
- 帧率计算:帧率 = 像素时钟 / (帧长 × 行长度)。其中行长度包括有效像素和blanking时间。我刚开始做时,忘了算blanking,算出来的帧率差了20%。
- 多sensor冲突:如果系统里有多个sensor共用一个I2C总线,记得检查地址是否冲突。我曾经把两个sensor设成同一个地址,结果一个初始化成功,另一个死活不行。
我的调试技巧:在sensor驱动里加调试打印,每次写寄存器时都打印出来。比如:
dev_dbg(&client->dev, "write reg 0x%04x = 0x%02x\n", reg, val);
配合逻辑分析仪,你能快速定位是驱动没写对,还是硬件没响应。
好了,这一章的内容就到这里。Sensor驱动开发,说白了就是三件事:把数据发过去、把参数配好、把接口暴露出来。下一章我们会讲MIPI CSI-2接口的配置和调试,那是sensor和ISP之间的高速公路,跑不通的话前面全白干。