完善IMX415驱动回调函数 1.v4l2子设备操作集1.1 v4l2_subdev_pad_ops1imx415_enum_mbus_code:枚举List/Enumerate该传感器支持的媒体总线格式编码Media Bus Pixel Codes。当应用层或上游驱动如 ISP、MIPI CSI-2 接收器询问“这个摄像头支持哪些图像输出格式”时就会调用这个函数。假设supported_modes为索引 (index)分辨率 (width x height)总线格式 (bus_fmt)最大帧率 (max_fps)寄存器配置表03840 x 2160 (4K)RAW 10-bit30 fpsimx415_4k_raw10_regs13840 x 2160 (4K)RAW 12-bit20 fpsimx415_4k_raw12_regs21920 x 1080 (1080P)RAW 10-bit60 fpsimx415_1080p_raw10_regsstatic 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函数本身一行代码都不用改 这种将“业务逻辑”与“数据”分离的方式是内核驱动低耦合设计的典范。2imx415_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循环的所以是枚举。3imx415_get_fmt获取读取传感器当前的图像格式、分辨率等配置信息。当上层应用如抓图软件或 ISP 驱动想要知道“摄像头现在正处于什么工作模式如 4K 还是 1080PRAW10 还是 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 框架层面的标准数据结构驱动内部自定义的硬件配置集合可见性对外公开应用程序和系统直接可见对内隐藏一般只在驱动源码内部使用关注点图像长宽是多少数据打包格式是什么硬件寄存器怎么配时钟跑多快支持多少帧率灵活性理论上可以传入任意分辨率进行协商测试只有固定的几个如 4K30, 1080P60举例3840x2160,MEDIA_BUS_FMT_SRGGB10_1X10imx415_mode_4k_30fps含上百行寄存器参数4imx415_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 链路频率和像素速率并通知系统。5imx415_get_selection向系统报告传感器的“裁剪/窗口Crop/Selection”边界信息。当上层应用或 ISP 想要知道“这个摄像头当前输出的图像是从多大的原始像素区域中裁剪出来的它的起点坐标和宽高是多少”时就会调用这个函数。为什么需要这个函数你可能买到的 IMX415 摄像头号称是4K3840x2160或者是1080P1920x1080的。但实际上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_opsstatic const struct v4l2_subdev_video_ops imx415_video_ops { .s_stream imx415_s_stream, .g_frame_interval imx415_g_frame_interval, };此操作集负责视频流相关操作最主要的就是开关视频流。imx415_s_streamstatic 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_opsstatic 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()电路真正接通电源