简介面向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图像信号处理器的绑定方式决定了你不能像单片机裸机那样直接读寄存器出图。做这套东西的典型场景是产品上用了一颗国产或日系 sensorRK3566 的 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、ISPRK3566 的 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_sensor1a { 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; port0 { reg 0; csi2_dphy0_input: endpoint0 { 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_codefmt-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 IDv4l2-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 这两个最磨人的参数开发顺利希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?