完善IMX415驱动回调函数
1.v4l2子设备操作集
1.1 v4l2_subdev_pad_ops
(1)imx415_enum_mbus_code:
枚举(List/Enumerate)该传感器支持的媒体总线格式编码(Media Bus Pixel Codes)。当应用层或上游驱动(如 ISP、MIPI CSI-2 接收器)询问“这个摄像头支持哪些图像输出格式”时,就会调用这个函数。
假设supported_modes为:
| 索引 (index) | 分辨率 (width x height) | 总线格式 (bus_fmt) | 最大帧率 (max_fps) | 寄存器配置表 |
| 0 | 3840 x 2160 (4K) | RAW 10-bit | 30 fps | &imx415_4k_raw10_regs |
| 1 | 3840 x 2160 (4K) | RAW 12-bit | 20 fps | &imx415_4k_raw12_regs |
| 2 | 1920 x 1080 (1080P) | RAW 10-bit | 60 fps | &imx415_1080p_raw10_regs |
static int imx415_enum_mbus_code(struct v4l2_subdev *sd, struct v4l2_subdev_state *sd_state, struct v4l2_subdev_mbus_code_enum *code) { struct imx415 *imx415 = to_imx415(sd); if (code->index >= imx415->cfg_num) return -EINVAL; code->code = imx415->supported_modes[code->index].bus_fmt; return 0; }参数:
struct v4l2_subdev *sd:指向当前 V4L2 子设备的结构体指针,代表当前的 IMX415 传感器实体。 struct v4l2_subdev_state *sd_state:子设备的状态(在较新的内核中用于管理 pad、stream 的配置和格式),在这个具体的实现中暂时没有被直接使用。 struct v4l2_subdev_mbus_code_enum *code:这是一个输入/输出型的结构体。 输入:code->index(框架或用户空间传入的索引号,从 0 开始递增) 输出:code->code(驱动需要填入该索引对应的媒体总线编码,例MEDIA_BUS_FMT_SRGGB10_1X10 )。struct imx415 *imx415 = to_imx415(sd);static inline struct imx415 *to_imx415(struct v4l2_subdev *_sd) { return container_of(_sd, struct imx415, sd); }作用:通过 V4L2 的通用子设备指针sd,反向推导出 IMX415 驱动特有的私有数据结构体指针imx415。
inline关键字在 C/C++ 中用于定义内联函数,其主要目的是通过将函数代码直接插入到调用位置来减少函数调用的开销,从而提高运行效率。内联函数适合短小、频繁调用的函数,相比宏更安全,但可能会增大代码体积。
code->code = imx415->supported_modes[code->index].bus_fmt;作用:如果索引合法,驱动就会从它内部维护的模式数组supported_modes中,根据当前的index找到对应的模式,并将其中的媒体总线格式bus_fmt(例如 RAW10, RAW12 等 Bayer 格式编码)赋值给code->code。
问:为什么不用一个 switch case 直接返回,而是要去查 supported_modes 表?
答:如果后续产品升级,要增加对 HDR 模式或不同像素格式(比如从 RAW10 升级到 RAW12)的支持,我们只需要在supported_modes数组里增加一项,并修改cfg_num的大小。这个enum_mbus_code函数本身一行代码都不用改! 这种将“业务逻辑”与“数据”分离的方式,是内核驱动低耦合设计的典范。
(2)imx415_enum_frame_sizes
根据指定的媒体总线编码(图像格式),枚举该图像传感器所支持的图像分辨率(帧大小/Frame Sizes)。当上层应用或 ISP 想要知道“当传感器输出某种格式时,支持哪些分辨率(如 4K, 1080P)”时,就会调用此函数。
static int imx415_enum_frame_sizes(struct v4l2_subdev *sd, struct v4l2_subdev_state *sd_state, struct v4l2_subdev_frame_size_enum *fse) { struct imx415 *imx415 = to_imx415(sd); if (fse->index >= imx415->cfg_num) return -EINVAL; if (fse->code != imx415->supported_modes[fse->index].bus_fmt) return -EINVAL; fse->min_width = imx415->supported_modes[fse->index].width; fse->max_width = imx415->supported_modes[fse->index].width; fse->max_height = imx415->supported_modes[fse->index].height; fse->min_height = imx415->supported_modes[fse->index].height; return 0; }struct v4l2_subdev_frame_size_enum *fse:这是核心的输入/输出结构体。 输入:fse->index(分辨率索引)和 fse->code(特定的媒体总线编码,即图像格式)。 输出:fse->min_width, fse->max_width, fse->min_height, fse->max_height(用于描述支持的分辨率范围或固定分辨率)。enum_mbus_code是格式枚举,enum_frame_sizes是分辨率枚举,比如系统说:“我已经决定要用 RAW12 格式了 (fse->code = RAW12),请问在这个格式下,你支持哪些分辨率?”
驱动中的函数只会求出一个格式下的分辨率,但是上层是index++的while循环的,所以是枚举。
(3)imx415_get_fmt
获取(读取)传感器当前的图像格式、分辨率等配置信息。当上层应用(如抓图软件)或 ISP 驱动想要知道“摄像头现在正处于什么工作模式(如 4K 还是 1080P?RAW10 还是 RAW12?)”时,就会调用这个函数。
static int imx415_get_fmt(struct v4l2_subdev *sd, struct v4l2_subdev_state *sd_state, struct v4l2_subdev_format *fmt) { struct imx415 *imx415 = to_imx415(sd); const struct imx415_mode *mode = imx415->cur_mode; mutex_lock(&imx415->mutex); if (fmt->which == V4L2_SUBDEV_FORMAT_TRY) { #ifdef CONFIG_VIDEO_V4L2_SUBDEV_API fmt->format = *v4l2_subdev_get_try_format(sd, sd_state, fmt->pad); #else mutex_unlock(&imx415->mutex); return -ENOTTY; #endif } else { fmt->format.width = mode->width; fmt->format.height = mode->height; fmt->format.code = mode->bus_fmt; fmt->format.field = V4L2_FIELD_NONE; if (fmt->pad < PAD_MAX && mode->hdr_mode != NO_HDR) fmt->reserved[0] = mode->vc[fmt->pad]; else fmt->reserved[0] = mode->vc[PAD0]; } mutex_unlock(&imx415->mutex); return 0; }fmt和mode的关系与区别:
mode是驱动内部维护的硬件参数集,它直接映射了图像传感器(如 IMX415)的物理硬件极限。 摄像头芯片需要改写内部成百上千个寄存器才能改变一次工作状态。因此,驱动开发者会把这些状态打包成一个个固定的mode。
fmt是 V4L2框架标准定义的通用结构体(struct v4l2_subdev_format),它是摄像头对外的“共同语言”。
| 特性 | fmt (格式) | mode (模式) |
| 本质 | V4L2 框架层面的标准数据结构 | 驱动内部自定义的硬件配置集合 |
| 可见性 | 对外公开(应用程序和系统直接可见) | 对内隐藏(一般只在驱动源码内部使用) |
| 关注点 | 图像长宽是多少?数据打包格式是什么? | 硬件寄存器怎么配?时钟跑多快?支持多少帧率? |
| 灵活性 | 理论上可以传入任意分辨率进行协商测试 | 只有固定的几个(如 4K@30, 1080P@60) |
| 举例 | 3840x2160,MEDIA_BUS_FMT_SRGGB10_1X10 | imx415_mode_4k_30fps(含上百行寄存器参数) |
(4)imx415_set_fmt
设置传感器的图像格式与分辨率。当用户层或 ISP 驱动说“我要把摄像头改成 1080P 分辨率”时,就会调用此函数。驱动在这里不仅要完成fmt到mode的匹配,还要动态计算并更新一大堆与时序(Blanking)和时钟(Pixel Rate)相关的控制参数。
static int imx415_set_fmt(struct v4l2_subdev *sd, struct v4l2_subdev_state *sd_state, struct v4l2_subdev_format *fmt) { struct imx415 *imx415 = to_imx415(sd); const struct imx415_mode *mode; s64 h_blank, vblank_def, vblank_min; u64 pixel_rate = 0; u8 lanes = imx415->bus_cfg.bus.mipi_csi2.num_data_lanes; mutex_lock(&imx415->mutex); mode = imx415_find_best_fit(imx415, fmt); fmt->format.code = mode->bus_fmt; fmt->format.width = mode->width; fmt->format.height = mode->height; fmt->format.field = V4L2_FIELD_NONE; if (fmt->which == V4L2_SUBDEV_FORMAT_TRY) { #ifdef CONFIG_VIDEO_V4L2_SUBDEV_API *v4l2_subdev_get_try_format(sd, sd_state, fmt->pad) = fmt->format; #else mutex_unlock(&imx415->mutex); return -ENOTTY; #endif } else { imx415_change_mode(imx415, mode); h_blank = mode->hts_def - mode->width; __v4l2_ctrl_modify_range(imx415->hblank, h_blank, h_blank, 1, h_blank); vblank_def = mode->vts_def - mode->height; /* VMAX >= (PIX_VWIDTH / 2) + 46 = height + 46 */ vblank_min = (mode->height + 46) - mode->height; __v4l2_ctrl_modify_range(imx415->vblank, vblank_min, IMX415_VTS_MAX - mode->height, 1, vblank_def); __v4l2_ctrl_s_ctrl(imx415->vblank, vblank_def); __v4l2_ctrl_s_ctrl(imx415->link_freq, mode->mipi_freq_idx); pixel_rate = (u32)link_freq_items[mode->mipi_freq_idx] / mode->bpp * 2 * lanes; __v4l2_ctrl_s_ctrl_int64(imx415->pixel_rate, pixel_rate); } dev_info(&imx415->client->dev, "%s: mode->mipi_freq_idx(%d)", __func__, mode->mipi_freq_idx); mutex_unlock(&imx415->mutex); return 0; }上面的get_fmt是根据mode获取fmt,现在的set_fmt就是根据用户传入的fmt来设置mode。
这段代码做的事情可以总结为:
imx415_set_fmt表面上是在“设置一个长宽格式”,其实在 ACTIVE 分支下,它在幕后帮硬件拉动了一整条多米诺骨牌:
改格式:把用户要的分辨率对齐成硬件支持的
mode。算时序:重新计算这一档模式下的水平消隐、垂直消隐限制,防止硬件时序冲突。
调时钟:根据当前的物理带宽、通道数和图像位深,算出精准的MIPI 链路频率和像素速率并通知系统。
(5)imx415_get_selection
向系统报告传感器的“裁剪/窗口(Crop/Selection)”边界信息。当上层应用或 ISP 想要知道“这个摄像头当前输出的图像是从多大的原始像素区域中裁剪出来的,它的起点坐标和宽高是多少”时,就会调用这个函数。
为什么需要这个函数?
你可能买到的 IMX415 摄像头号称是4K(3840x2160)或者是1080P(1920x1080)的。但实际上,IMX415 芯片内部的物理像素矩阵为了留出边缘光屏蔽区和对齐缓冲,它的全尺寸其实是3864x2192(或者 1080P 模式下硬件输出1944x1096)。
如果直接把 3864x2192 的图送给后端,由于长宽不是标准的 16:9 比例,普通的显示器或标准的 4K/1080P ISP 编码器就无法处理。
因此,驱动必须告诉系统:如何从硬件输出的略大尺寸中,居中裁剪出标准的 4K 或 1080P 图像。
static int imx415_get_selection(struct v4l2_subdev *sd, struct v4l2_subdev_state *sd_state, struct v4l2_subdev_selection *sel) { struct imx415 *imx415 = to_imx415(sd); if (sel->target == V4L2_SEL_TGT_CROP_BOUNDS) { if (imx415->cur_mode->width == 3864) { sel->r.left = CROP_START(imx415->cur_mode->width, DST_WIDTH_3840); sel->r.width = DST_WIDTH_3840; sel->r.top = CROP_START(imx415->cur_mode->height, DST_HEIGHT_2160); sel->r.height = DST_HEIGHT_2160; } else if (imx415->cur_mode->width == 1944) { sel->r.left = CROP_START(imx415->cur_mode->width, DST_WIDTH_1920); sel->r.width = DST_WIDTH_1920; sel->r.top = CROP_START(imx415->cur_mode->height, DST_HEIGHT_1080); sel->r.height = DST_HEIGHT_1080; } else { sel->r.left = CROP_START(imx415->cur_mode->width, imx415->cur_mode->width); sel->r.width = imx415->cur_mode->width; sel->r.top = CROP_START(imx415->cur_mode->height, imx415->cur_mode->height); sel->r.height = imx415->cur_mode->height; } return 0; } return -EINVAL; }背景:当传感器硬件实际吐出
3864宽度的图像时。裁剪行为:驱动要求最终对外输出的有效宽度
sel->r.width为3840,有效高度sel->r.height为2160(标准的 4K UHD)。起点坐标计算:
CROP_START通常是一个宏,用来计算居中裁剪的起始 X 坐标(left)和 Y 坐标(top)。其数学公式通常为:
这意味着,告诉 ISP 驱动:“请从第 $(12, 16)$ 个像素开始,切出一个 $3840 \times 2160$ 的标准 4K 矩形”。
1.2 v4l2_subdev_video_ops
static const struct v4l2_subdev_video_ops imx415_video_ops = { .s_stream = imx415_s_stream, .g_frame_interval = imx415_g_frame_interval, };此操作集负责视频流相关操作,最主要的就是开关视频流。
imx415_s_stream
static int imx415_s_stream(struct v4l2_subdev *sd, int on) { struct imx415 *imx415 = to_imx415(sd); struct i2c_client *client = imx415->client; int ret = 0; dev_info(&imx415->client->dev, "s_stream: %d. %dx%d, hdr: %d, bpp: %d\n", on, imx415->cur_mode->width, imx415->cur_mode->height, imx415->cur_mode->hdr_mode, imx415->cur_mode->bpp); mutex_lock(&imx415->mutex); on = !!on; if (on == imx415->streaming) goto unlock_and_return; if (on) { if (imx415->is_thunderboot && rkisp_tb_get_state() == RKISP_TB_NG) { imx415->is_thunderboot = false; __imx415_power_on(imx415); } ret = pm_runtime_get_sync(&client->dev); if (ret < 0) { pm_runtime_put_noidle(&client->dev); goto unlock_and_return; } ret = __imx415_start_stream(imx415); if (ret) { v4l2_err(sd, "start stream failed while write regs\n"); pm_runtime_put(&client->dev); goto unlock_and_return; } } else { __imx415_stop_stream(imx415); pm_runtime_put(&client->dev); } imx415->streaming = on; unlock_and_return: mutex_unlock(&imx415->mutex); return ret; }对于正常的开流启动:先做pm_runtime_get_sync(&client->dev)进行上电设备唤醒,再做__imx415_start_stream(imx415)进行启动流。
__imx415_start_stream(imx415)代码如下:
作用:完成 IMX415 开流前的寄存器初始化、控制参数同步、HDR 初始曝光配置,最后退出待机状态,让传感器开始输出图像。
static int __imx415_start_stream(struct imx415 *imx415) { int ret; if (!imx415->is_thunderboot) { ret = imx415_write_array(imx415->client, imx415->cur_mode->global_reg_list); if (ret) return ret; ret = imx415_write_array(imx415->client, imx415->cur_mode->reg_list); if (ret) return ret; } imx415_get_pclk_and_tline(imx415); /* In case these controls are set before streaming */ ret = __v4l2_ctrl_handler_setup(&imx415->ctrl_handler); if (ret) return ret; if (imx415->has_init_exp && imx415->cur_mode->hdr_mode != NO_HDR) { imx415->rhs1_old = IMX415_RHS1_DEFAULT; imx415->rhs2_old = IMX415_RHS2_DEFAULT; ret = imx415_ioctl(&imx415->subdev, PREISP_CMD_SET_HDRAE_EXP, &imx415->init_hdrae_exp); if (ret) { dev_err(&imx415->client->dev, "init exp fail in hdr mode\n"); return ret; } } return imx415_write_reg(imx415->client, IMX415_REG_CTRL_MODE, IMX415_REG_VALUE_08BIT, 0); }函数执行顺序:
判断是否为 Thunderboot │ ├─ 普通启动 │ ├─ 写全局初始化寄存器 │ └─ 写当前分辨率/帧率模式寄存器 │ └─ Thunderboot └─ 跳过重复初始化 │ 计算像素时钟和单行曝光时间 │ 同步 V4L2 控件到传感器寄存器 │ 如果是 HDR 模式,写入初始 HDR 曝光参数 │ 写 CTRL_MODE = 0,退出待机,开始输出图像(1)写全局初始化寄存器和模式寄存器
(2)计算像素时钟频率和单行曝光时间
计算出当前模式下的——pclk(像素时钟频率)和tline(传输一行图像所需的时间)。
static void imx415_get_pclk_and_tline(struct imx415 *imx415) { const struct imx415_mode *mode = imx415->cur_mode; imx415->pclk = (u32)div_u64((u64)mode->hts_def * mode->vts_def * mode->max_fps.denominator, mode->max_fps.numerator); imx415->tline = (u32)div_u64((u64)mode->hts_def * 1000000000, imx415->pclk); }在看公式前,必须先了解以下传感器时序名词:
hts_def (Horizontal Total Size):水平总尺寸。指 Sensor 内部读出一行数据物理上所需的总像素时钟数(包含有效像素和水平消隐区)。
vts_def (Vertical Total Size):垂直总尺寸。指 Sensor 读出一帧数据物理上所需的总行数(包含有效行和垂直消隐区)。
max_fps (Max Frames Per Second):该模式下的最大帧率。它由两个整数组成:numerator(分子)和 denominator(分母)。例如 30 fps 通常表示为 30/1,即分子 30,分母 1。
公式1(计算pclk):
传感器吐出一帧完整的图像,物理上需要扫描的总像素点数就是:
。
如果一秒钟要吐出 $FPS$ 帧图像,那么一秒钟内 Sensor 物理上总共要扫描处理的像素点数就是:
。
pclk的本质就是像素时钟频率(单位:Hz),即“一秒钟内处理多少个像素点”。驱动通过这个乘法反向推算出当前分辨率下的物理像素时钟。
公式2(计算tline):
既然
pclk代表一秒钟(即 $10^{9}$ 纳秒)内可以跑多少个像素时钟。那么跑完一行所需的像素时钟数(
hts_def)需要消耗多少纳秒呢?答案显而易见:用一行的总时钟数
hts_def除以一秒钟的总时钟数pclk,就能得到传输一行图像所需的绝对时间(单位:纳秒 ns)。
为什么要大费周折算这两个值?
答:
这两个值算出来后,并不会直接写进 IMX415 的寄存器,而是提供给 Linux 内核及后端的 AE(自动曝光)算法使用的。
精准控制曝光时间:
在用户层或者 AE 算法看来,曝光时间是以“微秒”或“毫秒”为单位的。但 IMX415 芯片非常“笨”,它内部的曝光控制寄存器只认“行数(Lines)”。
当系统算法说:“我想曝光 10ms (10,000,000ns)”。
驱动就会去计算:
曝光寄存器应填写的行数 = 1000000ns / tline
有了
tline这个严丝合缝的“时间面额”,驱动才能把上层的时间单位精准转换为底层的硬件行数寄存器值。
(3)同步V4L2控件
目的:
用户可能在开流之前就调用了:设置曝光,设置增益,设置垂直消隐等
但那时传感器可能:
- 尚未上电;
- 尚未完成寄存器初始化;
- 处于 runtime suspend;
- 还没有进入可写寄存器的状态。
因此这些控制值虽然已经保存在 V4L2 控件框架中,却不一定已经真正写入传感器。
__v4l2_ctrl_handler_setup()会遍历imx415->ctrl_handler中注册的所有控制项,并调用控制的回调函数:
static const struct v4l2_ctrl_ops imx415_ctrl_ops = { .s_ctrl = imx415_set_ctrl, };调用关系大概为:
__v4l2_ctrl_handler_setup() │ ├─ exposure 控件 │ └─ imx415_set_ctrl() │ └─ 写曝光寄存器 │ ├─ analogue gain 控件 │ └─ imx415_set_ctrl() │ └─ 写增益寄存器 │ ├─ vblank 控件 │ └─ imx415_set_ctrl() │ └─ 写 VMAX │ └─ test pattern 控件 └─ imx415_set_ctrl() └─ 写测试图寄存器(4)判断是否需要写入 HDR 初始曝光
首先判断是否需要写入HDR初始曝光:
if (imx415->has_init_exp && imx415->cur_mode->hdr_mode != NO_HDR) {如果需要则初始化HDR曝光边界:
imx415->rhs1_old = IMX415_RHS1_DEFAULT; imx415->rhs2_old = IMX415_RHS2_DEFAULT;(5)通过私有 ioctl 写入 HDR AE 参数
(6)写控制模式寄存器正式开流
return imx415_write_reg(imx415->client, IMX415_REG_CTRL_MODE, IMX415_REG_VALUE_08BIT, 0);1.3 v4l2_subdev_core_ops
static const struct v4l2_subdev_core_ops imx415_core_ops = { .s_power = imx415_s_power, .ioctl = imx415_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl32 = imx415_compat_ioctl32, #endif };(1)imx415_s_power
应用层接口上电/下电,最终还是调用__imx415_power_on()电路真正接通电源;
