简介:面向RK3566平台Linux内核驱动开发者,提供MIPI-Camera相机驱动从编写到调试的完整参考。资源围绕RGBD相机与多款常见Sensor(如gc2053、gc2093、s5k33d、sc2310)展开,覆盖数据通路配置、AE曝光策略与帧率控制等关键环节,适合有C语言和基础驱动知识、希望快速上手RK平台相机调试的读者。压缩包共25个文件,其中18个C源文件构成驱动主体,另有7个txt说明文件记录修改历程与验证结论,整体仅220KB,结构紧凑便于查阅。已有917人学习浏览。从目录来看,内容不止单一驱动,还包含不同Sensor型号的适配版本、60fps与30fps等帧率模式下的AE调试验证,以及按“官方/在用/自研”划分的多分支目录,能直观呈现驱动演化与排错思路,对学习驱动分层、Sensor注册和V4L2集成有实际帮助。
1. 基于RK3566的MIPI-Camera内核驱动开发,先把“调时序”这事想清楚
接手RK3566平台做MIPI-Camera内核驱动之前,我建议你先接受一个事实:这个方向 80% 的时间不在写 C 代码,而是在对着 datasheet 和设备树调时序。RK3566 的 MIPI-CSI 通道数量、和 ISP(图像信号处理器)的绑定方式,决定了你不能像单片机裸机那样直接读寄存器出图。做这套东西的典型场景是:产品上用了一颗国产或日系 sensor,RK3566 的 BSP 内核没有对应驱动,需要你照着 sensor 手册写一个 v4l2_subdev 驱动,再配合 Rockchip 的 media framework 把数据通路串起来。适合谁来干?至少得看得懂 i2c 时序图、分得清 mclk 和 pclk、对 Linux 设备模型不陌生的内核驱动工程师。新手能跟步骤把最小系统跑起来,熟手能在方案选型阶段就避开 lane 映射和时钟频率这两个最大的坑。
2. 摸清 RK3566 的 MIPI-CSI 硬件链路和内核里现成的骨架
2.1 MIPI-CSI 链路的四层分工:sensor、D-PHY、CSI-Host、ISP
RK3566 的 MIPI-Camera 数据通路,物理上可以拆成四段:sensor 端负责把光信号变成 RAW 图像数据,通过 MIPI CSI-2 协议以 lane(数据通道)发送出去;RK3566 片内的 D-PHY 负责接收高速差分信号,做串并转换;CSI-Host 把 PHY 拿到的包解析成 frame 格式;之后数据进入 ISP 做去噪、色彩校正和缩放,最后汇入 V4L2 的 video 节点。你在驱动开发中碰到的绝大多数“点不亮”问题,都出在物理层:lane 映射错、时钟频率不一致、上电时序不对,根本没有走到 CSI-Host 那一步。
内核目录里,drivers/media/platform/rockchip/ 下已经放好了 mipi_dphy、csi_host 和 isp 的驱动,这些是 Rockchip BSP 内核长期维护的,基本不用大改。你真正要写的,是 sensor 这一端的设备驱动。Sensor 挂在 I2C 总线上,是一个标准的 i2c_client,但它同时又注册成一个 v4l2_subdev,对外暴露 set_fmt、s_stream 这类回调,让 media framework 能指挥它。理解这层抽象关系,比急着抄代码重要得多。
2.2 内核里现成的媒体控制器框架,RK3566 是怎么串起来的
RK3566 BSP 内核(常见 4.19 或 5.10 分支)对摄像头部分采用的是 media controller 框架。也就是说,不是像老平台那样直接 open /dev/video0 就能出图,你得先通过 media-ctl 工具把 sensor 的 pad、D-PHY 的 pad、CSI-Host 的 pad 之间的 link 全部建立好,sensor → D-PHY → CSI-Host → ISP → video 节点这条路才算通。
在驱动开发阶段,你要确认几件事:第一,你自己的 sensor subdev 驱动有没有正确注册到 media device 上;第二,sensor 的 pad 类型有没有声明成 MEDIA_PAD_FL_SOURCE;第三,v4l2_subdev 的 set_fmt 回调里,mbus_code 是不是和 sensor 实际输出的编码一致。RK3566 的 rkisp 驱动默认接收 V4L2_MBUS_FMT_SRGGB10_1X10 这类 RAW 格式,如果你在 sensor 驱动里写成 YUYV8_2X8,链路建立会失败,或者抓出来的图像色彩一团乱。
先用系统里已有的抓图工具验证链路通没通,比一上来就写代码更高效。我一般会在驱动没写之前,先看看 BSP 里有没有同类 sensor 的参考驱动。RK3566 的 SDK 里通常带 OV 系列和索尼系列的 sensor 驱动源码,它们的 probe 函数和 s_stream 实现套路高度一致。你只需要把自己的 sensor 手册里那几个关键寄存器——芯片 ID、曝光寄存器、增益寄存器、输出分辨率配置——替换进去,大概率能跑通。这也是做这个平台最常见的起步方式。
3. 把一条 MIPI lane 的设备和时序写对:从设备树开始
3.1 最小传感器设备树片段:clocks、GPIO、供电三件套
设备树是 sensor 驱动的地基。RK3566 的硬件设计中,sensor 通常挂在某个 I2C 控制器(比如 i2c3)上,外部需要一个 24M 或 27M 的参考时钟(mclk),一个复位引脚,一个 power down 引脚。下面是一个最小可用的设备树片段,基于任意一款常见 sensor 的接入形式:
&i2c3 { status = "okay"; cam_sensor: mv_sensor@1a { compatible = "vendor,mv-sensor"; reg = <0x1a>; pinctrl-names = "default"; pinctrl-0 = <&cam_sensor_mclk_pins &cam_sensor_reset_pins>; clocks = <&cru SCLK_CAM_SENSOR>; clock-names = "xvclk"; assigned-clocks = <&cru SCLK_CAM_SENSOR>; assigned-clock-rates = <24000000>; reset-gpios = <&gpio3 RK_PA0 GPIO_ACTIVE_LOW>; pwdn-gpios = <&gpio3 RK_PA1 GPIO_ACTIVE_LOW>; rockchip,camera-module-index = <0>; rockchip,camera-module-facing = "back"; rockchip,camera-module-name = "mv_sensor"; rockchip,camera-module-lens-name = "default-lens"; port { mv_sensor_out: endpoint { remote-endpoint = <&csi2_dphy0_input>; >&csi2_dphy0 { status = "okay"; ports { #address-cells = <1>; #size-cells = <0>; port@0 { reg = <0>; csi2_dphy0_input: endpoint@0 { reg = <0>; remote-endpoint = <&mv_sensor_out>; ># drivers/media/i2c/Makefile obj-$(CONFIG_VIDEO_MV_SENSOR) += mv_sensor.oconfig VIDEO_MV_SENSOR tristate "MV Sensor support" depends on VIDEO_DEV && I2C help Support for the MV series CMOS image sensor.注意这里 CONFIG_VIDEO_MV_SENSOR 编译成模块(m)还是编进内核(y)。调试阶段建议编成模块,这样每次改代码只需要 make modules 然后 insmod,不用整机重启。但要提前确认 RK3566 用的根文件系统支持 module 加载,并且 sensor 用到的符号(比如 v4l2_subdev_init)在内核里是 EXPORT 的,否则会 insmod 失败。一般情况下 i2c 子系统这些符号都是导出的,问题不大。
4.2 i2c_driver 的 probe 骨架:获取时钟、复位、寄存器初始化
下面是一个最小可用的 i2c_driver 骨架,我按常规 sensor 驱动的写法展开。核心是在 probe 里完成三件事:拿 i2c_client 的私有数据、初始化 v4l2_subdev、把 power 相关的 GPIO 拉到位。
#include <linux/module.h> #include <linux/i2c.h> #include <linux/clk.h> #include <linux/gpio/consumer.h> #include <media/v4l2-subdev.h> #include <media/v4l2-mediabus.h> struct mv_sensor { struct i2c_client *client; struct v4l2_subdev sd; struct clk *xvclk; struct gpio_desc *reset_gpio; struct gpio_desc *pwdn_gpio; u32 mbus_code; }; static inline struct mv_sensor *to_mv_sensor(struct v4l2_subdev *sd) { return container_of(sd, struct mv_sensor, sd); } static int mv_sensor_read(struct mv_sensor *sensor, u16 reg, u8 *val) { struct i2c_client *client = sensor->client; struct i2c_msg msgs[2]; u8 buf[2] = { reg >> 8, reg & 0xff }; int ret; msgs[0].addr = client->addr; msgs[0].flags = 0; msgs[0].len = 2; msgs[0].buf = buf; msgs[1].addr = client->addr; msgs[1].flags = I2C_M_RD; msgs[1].len = 1; msgs[1].buf = val; ret = i2c_transfer(client->adapter, msgs, 2); if (ret < 0) return ret; return 0; } static int mv_sensor_write(struct mv_sensor *sensor, u16 reg, u8 val) { struct i2c_client *client = sensor->client; u8 buf[3] = { reg >> 8, reg & 0xff, val }; int ret; ret = i2c_master_send(client, buf, 3); if (ret < 0) return ret; return 0; } static int mv_sensor_probe(struct i2c_client *client) { struct mv_sensor *sensor; struct device *dev = &client->dev; int ret; sensor = devm_kzalloc(dev, sizeof(*sensor), GFP_KERNEL); if (!sensor) return -ENOMEM; sensor->client = client; /* 时钟:开启前先确认频率 */ sensor->xvclk = devm_clk_get(dev, "xvclk"); if (IS_ERR(sensor->xvclk)) { dev_err(dev, "failed to get xvclk\n"); return PTR_ERR(sensor->xvclk); } ret = clk_prepare_enable(sensor->xvclk); if (ret) return ret; sensor->reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH); if (IS_ERR(sensor->reset_gpio)) return PTR_ERR(sensor->reset_gpio); sensor->pwdn_gpio = devm_gpiod_get(dev, "pwdn", GPIOD_OUT_LOW); if (IS_ERR(sensor->pwdn_gpio)) return PTR_ERR(sensor->pwdn_gpio); /* 时序:先复位,后释放,等待 sensor 内部 PLL 稳定 */ gpiod_set_value_cansleep(sensor->reset_gpio, 1); msleep(10); gpiod_set_value_cansleep(sensor->reset_gpio, 0); msleep(20); /* 在这里读 sensor ID,确认 i2c 通路正常 */ v4l2_i2c_subdev_init(&sensor->sd, client, &mv_sensor_ops); return 0; } static const struct i2c_device_id mv_sensor_id[] = { { "mv_sensor", 0 }, { } }; MODULE_DEVICE_TABLE(i2c, mv_sensor_id); static const struct of_device_id mv_sensor_of_match[] = { { .compatible = "vendor,mv-sensor" }, { } }; MODULE_DEVICE_TABLE(of, mv_sensor_of_match); static struct i2c_driver mv_sensor_i2c_driver = { .probe = mv_sensor_probe, .id_table = mv_sensor_id, .driver = { .name = "mv_sensor", .of_match_table = mv_sensor_of_match, }, }; module_i2c_driver(mv_sensor_i2c_driver);这里参数的讲究不少。i2c 地址的读写分两步或直接组合传输,关键在于 sensor 寄存器是 16 位还是 8 位寻址;上面代码按 16 位寄存器地址写,如果换成 8 位寻址的传感器,buf 长度和索引全要改,否则后面的读到的全是你发的寄存器地址本身,排查起来非常头疼。devm_kzalloc 的好处是出错自动释放,减少内存管理类问题,这也是我在 RK3566 平台上的习惯。msleep 的时长不是随便写的,10ms 和 20ms 来自 sensor 手册上电时序要求里 t_rtm 和 t_stable 的典型值,你换成别的时长得按手册来,宁长勿短。
4.3 v4l2_subdev 回调:从 set_fmt 到 s_stream 出流
probe 只是把设备树里的硬件资源拿到手,真正让图像从 sensor 里流出来,要看 v4l2_subdev 的 core ops。我一般至少实现 set_fmt、get_fmt 和 s_stream 三个回调,下面用代码说明关键部分。
static int mv_sensor_set_fmt(struct v4l2_subdev *sd, struct v4l2_subdev_state *sd_state, struct v4l2_subdev_format *fmt) { struct mv_sensor *sensor = to_mv_sensor(sd); /* * 这里只做合法性校验和记录。 * 真正的寄存器配置放到 s_stream 里统一做。 */ if (fmt->format.code != sensor->mbus_code) fmt->format.code = sensor->mbus_code; if (fmt->format.width != 1280 || fmt->format.height != 720) return -EINVAL; return 0; } static int mv_sensor_s_stream(struct v4l2_subdev *sd, int enable) { struct mv_sensor *sensor = to_mv_sensor(sd); int ret = 0; if (enable) { /* * 先写流控寄存器,再写分辨率、曝光、增益。 * 最后用 streaming 寄存器把数据发出。 */ mv_sensor_write(sensor, 0x0100, 0x01); usleep_range(1000, 2000); } else { mv_sensor_write(sensor, 0x0100, 0x00); } return ret; }set_fmt 里有个常见的坑:内核的 v4l2_subdev_call 调用链会先走到这里,如果你的驱动没有 initial 回调去初始化 mbus_code,fmt->format.code 可能是一个非法值。所以在 probe 阶段就把 sensor->mbus_code 固定成 sensor 输出的真实格式,比如 10 bit RAW。s_stream 里可以做得更细,分成“预流寄存器组”和“流寄存器组”两段;我在实际项目里会把 sensor 手册给出的推荐寄存器表做成一个 const 数组,在 s_stream(enable) 时循环写入。这样代码可读性高,也方便后期针对不同模组换寄存器表,而不是改代码逻辑。
5. 避坑:RK3566 MIPI 摄像头驱动最常见的 5 个翻车点
5.1 现象:probe 成功但抓图全是黑的,链路却提示正常
这是新手第一个会撞到的问题。i2c 能读回 sensor ID,v4l2-ctl 设置格式也成功,/dev/video0 能打开,拍出来却是一片黑。原因大概率是 sensor 根本没有输出有效数据,或者是输出数据被 dphy 层的错误 lane 映射吃掉了。排查的路线是:先用示波器量 mclk 有没有波形,再量 MIPI 数据 lane 上有没有高速差分信号摆动。如果 sensor 没出 MIPI 时钟,回头看 sensor 的 stream 寄存器有没有写进去。如果 sensor 有输出但 dphy 端报错,去对照设备树里的># 抓取 dphy 初始化日志 dmesg -n 8 dmesg | grep -i "csi\|dphy\|isp\|mv_sensor" # 跑 v4l2 兼容性检查 v4l2-compliance -d /dev/video0 -s
用 perf 看中断频率是个容易被忽略的技巧。MIPI sensor 通常通过 GPIO 或者内部信号触发帧中断,你可以在驱动的中断处理函数里做一个计数器,然后在应用层定时读取 /sys 节点,确认中断频率和你设置的帧率一致。如果发现中断频率漂移,或者有时一秒钟少了几次,多半是 vblank 设置不稳,回来改 blanking。这套方法本质上是用数据说话,替你把“图像看起来偶尔卡”这种模糊体验,变成可量化的帧计数。
回到开头那句话,RK3566 的 mipi-camera 驱动开发,真正难的不是 C 语言语法,也不是 Linux 设备模型,而是对硬件手册的耐心和对日志的敏感。我自己的习惯是每次改完寄存器表,先记录 diff 再上板,出问题第一件事查 dmesg 而不是怀疑编译器。这样反复几轮下来,踩坑点基本都能沉淀成团队的 check list。希望你在这条路上比我更早摸清 lane 映射和 blanking 这两个最磨人的参数,开发顺利,希望帮到你。
本文还有配套的精品资源,点击获取