简介:RK ISP 驱动代码包,面向嵌入式Linux下Rockchip图像信号处理器(ISP)的驱动开发与移植场景,适合内核驱动工程师和学习V4L2框架的开发者。资源以rk-isp11为例,重点展示设备树匹配使用的of_device_id,并基于device/media/platform/rk-cif/cif_cif10_v4l2.c进行讲解,可帮助理解ISP与Camera子系统在同一驱动框架下的协作方式。压缩包共含17个文件,以7个C源文件、8个头文件为主,配套1个Makefile与1个Kconfig,代码量约96KB,各模块包括平台初始化、图像源注册、v4l2-subdev对接以及寄存器定义等,结构清晰便于按模块精读。现有1763人学习下载。读者可直接获得整套rkisp11驱动源码,结合V4L2与子设备模型,可快速定位设备树匹配、图像数据通路及平台驱动接口,为后续适配新Sensor或调试ISP管线提供具体参考。
1. rkisp驱动代码不是单个文件:先把它当一套ISP媒体栈看待
做Rockchip平台相机方案的人,迟早会打开一个叫rkisp的驱动代码目录,刚开始容易以为里面就一个.c文件,顶多几百行。实际翻进去会发现,drivers/media/platform/rockchip/isp/下躺着十来个源文件,probe入口、ISP主控、MIPI CSI2接收、resizer、stats统计回传、params参数下发各自分工,背后还依赖内核的v4l2_subdev和media controller框架。这套代码直接影响三个视频节点能不能出帧、画面黑不黑、颜色对不对。适合两种人:一是要移植或调试Rockchip ISP的新手,二是想把数据流和buffer时序彻底理清、以后能自己改帧率改分辨率的熟手。先把它当一套驱动栈看,后面才不会迷路。
2. rkisp驱动代码的三层骨架:platform driver、v4l2_subdev与video设备
2.1 从probe入口看rkisp的注册顺序
拿到源码后先别急着搜寄存器操作,先把probe流程看明白。不同SDK的目录名可能是isp、isp1或isp2,但结构基本一致,核心文件大致是这么分工的:
| 文件 | 职责 |
|---|---|
| rkisp_dev.c | 平台驱动入口、v4l2_device注册、media device初始化 |
| rkisp_isp.c | ISP主控子设备,负责sensor数据流控制和link配置 |
| rkisp_csi.c | MIPI CSI2接收子设备,完成时序同步与虚拟通道解析 |
| rkisp_resizer.c | 缩放通路,负责mainpath/selfpath输出分辨率调整 |
| rkisp_stream.c | 三个视频通路的stream状态机与buffer管理 |
| rkisp_stats.c | ISP统计信息上报,给3A算法用 |
| rkisp_params.c | 3A参数回灌,AWB/AE等参数的向下传递 |
probe那段代码是整个驱动的总开关,注册顺序直接决定你开机后看到的设备节点顺序。示意代码大致长这样:
// rkisp_dev.c 的 probe 流程(示意) static int rkisp_plat_drv_probe(struct platform_device *pdev) { struct rkisp_device *isp = kzalloc(sizeof(*isp), GFP_KERNEL); /* 1. media device 必须先注册,后续 link 建立在它之上 */ media_device_init(&isp->media_dev, "rkisp"); /* 2. v4l2_device 是所有 subdev 和 video 节点的父容器 */ v4l2_device_register(&pdev->dev, &isp->v4l2_dev); /* 3. 依次注册 ISP 主控、MIPI CSI2、resizer 子设备 */ rkisp_register_isp_subdev(isp); rkisp_register_csi_subdev(isp); rkisp_register_resizer_subdev(isp); /* 4. 注册 mainpath/selfpath/dmapath 三个 video 节点 */ rkisp_register_stream_video(isp); /* 5. 注册 stats 与 params 两个辅助节点 */ rkisp_register_stats_video(isp); rkisp_register_params_video(isp); /* 6. media link 必须最后建,link 依赖前面已注册的 entity */ rkisp_create_media_links(isp); return 0; }这里register顺序不是随手写的。v4l2_device必须先于subdev和video节点存在,media link又必须放到最后,因为link引用的entity句柄要等所有子设备注册完才能拿到。stats节点要先做event上报初始化,params节点则要先准备默认参数缓冲,否则应用层一打开节点就会读到空参数组。你调试时发现设备节点顺序和板子文档对不上,多半是SDK改过probe顺序,不是什么玄学。
2.2 MainPath、SelfPath、DmaPath:三个视频节点各自干嘛
rkisp驱动代码里最常被问的就是“为什么有两个video0/video1/video2”。这三条通路不是复制粘贴的关系,用途差别很大:
| 通路 | 特点 | 典型用途 |
|---|---|---|
| MainPath | 完整ISP处理链,支持缩放、裁剪、3A统计 | 主预览、拍照、录像 |
| SelfPath | 带宽较小,独立参数配置 | 画中画、双路预览、低分辨率第二路 |
| DmaPath | 相对接近DMA裸数据通路 | 调试、特殊数据采集场景 |
MainPath能拿到的格式和分辨率范围最大,SelfPath在多数SDK里宽度受限,DmaPath则不一定经过完整ISP链路,出图颜色可能是raw状态。应用层一般只开MainPath,需要第二路稳定输出时才考虑SelfPath。调试时如果发现SelfPath帧率一直上不去,先看带宽配置而不是怀疑驱动有bug。
2.3 media controller链路:sensor到video之间不是直连
rkisp驱动里最常见的新手误区,是以为sensor输出直接进ISP的video节点。实际在media controller框架下,链路是分段建立的:
sensor subdev -> mipi csi2 subdev -> isp subdev -> resizer subdev -> mainpath video
每个subdev都有自己的pad,相邻pad之间必须有media link相连,并且在应用层打开video节点之前,这条链路要处于enable状态。硬件上焊了线,不代表驱动里默认就通了;链路没建好,STREAMON会直接报EPIPE。所以拿到一份新SDK,第一步一定是打开media-ctl -p看实体列表:
media-ctl -d /dev/media0 -p输出里会列出所有entity,比如rockchip-mipi-csi2、rkisp-isp-subdev、rkisp_mainpath,它们之间的连接关系用->表示。链路是否enable,决定了你后续open节点和stream on能不能成功。这一步看不懂,后面所有排查都容易变成瞎试。
3. 让rkisp驱动代码跑起来:Kconfig、设备树与开机证据
3.1 把驱动编进内核而不是编成模块
rkisp驱动代码在大部分SDK里默认是模块化编译,但我刚上手时吃了一次亏:insmod顺序错了,sensor模块先加载,结果匹配不到后加载的csi2实体,probe直接defer,日志里只有一条不痛不痒的deferred probe提示。后来我拿到新板子一律先编成=y,少一层加载顺序的烦恼。
make ARCH=arm64 rockchip_defconfig make menuconfig # 进入 Device Drivers > Multimedia support > V4L platform devices # 找到 Rockchip ISP driver,按 Y 编入内核 grep CONFIG_VIDEO_ROCKCHIP_ISP .config这里有个细节:menuconfig里勾选的是CONFIG_VIDEO_ROCKCHIP_ISP,而sensor、csi2、isp是互相依赖的,编成=y时内核会在启动阶段按probe顺序自动处理依赖。编成=m时,加载顺序要自己控制,常见做法是写一个modules.order或者在rcS里按sensor、csi2、isp的顺序依次modprobe。少一个模块,后面所有节点都是空的。
3.2 设备树里匹配什么
rkisp驱动代码能起来,设备树节点至少要提供compatible、reg、interrupts、clocks这几样。compatible要和驱动里的of_match_table对得上,reg是寄存器基地址,interrupts是ISP中断号,clocks少了任何一个,probe都可能挂在clock_get阶段。
&rkisp { status = "okay"; compatible = "rockchip,rk3568-rkisp"; /* 示例值,以你的SDK为准 */ reg = <0x0 0xfdff0000 0x0 0x10000>; /* 示例值,以板级dts为准 */ interrupts = <GIC_SPI 62 IRQ_TYPE_LEVEL_HIGH>; clocks = <&cru ACLK_ISP>, <&cru CLK_ISP>; clock-names = "aclk_isp", "hclk_isp"; power-domains = <&power RK3568_PD_VI>; };写设备树最容易翻车的是clocks和power-domains。时钟名必须在驱动代码里能找到对应,顺序不一致没关系,因为驱动按clock-names字符串查找。power-domain没就绪时,probe会返回-EPROBE_DEFER,内核日志里出现deferred probe字样。遇到这种情况,先查父级power-domain的status是不是okay,而不是急着改驱动代码。
3.3 开机后怎么确认驱动真的起来了
驱动probe成功不代表链路就是好的,开机后第一步先看内核日志和设备节点是否存在:
dmesg | grep -i rkisp ls -l /dev/video0 /dev/video1 /dev/video2 v4l2-ctl --list-devices media-ctl -d /dev/media0 -pdmesg里能看到rkisp的probe成功打印,通常包含rkisp driver probe success或类似的字样。ls能看到video节点存在,说明video_register_device执行过了。v4l2-ctl --list-devices会按驱动名分组列出设备:
rkisp (platform:fdff0000.rkisp): /dev/video0 /dev/video1 /dev/video2media-ctl -p是判断链路是否完整的关键。输出里如果只有rkisp自己的entity,没有sensor或csi2实体,说明上游链路压根没注册进来,问题不在isp驱动,而在sensor那边。如果entity齐全但link状态是[0]而不是[1],说明链路没enable,先用media-ctl手动建一下链接再继续。
4. 顺一遍rkisp数据流:stream on、buffer与stats/params
4.1 打开节点先定格式:S_FMT后要读回bytesperline
rkisp的video节点多数是multi-plane接口,格式协商要用V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,这一点和普通USB camera不一样。打开节点后先做S_FMT,但不要盲目假设你设置的宽度就是驱动最终对齐后的值。
int fd = open("/dev/video0", O_RDWR); struct v4l2_format fmt = {0}; fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; fmt.fmt.pix_mp.width = 1920; fmt.fmt.pix_mp.height = 1080; fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_NV12; fmt.fmt.pix_mp.field = V4L2_FIELD_NONE; fmt.fmt.pix_mp.num_planes = 1; if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) { perror("S_FMT"); return -1; } /* 驱动可能为了16字节对齐,把 bytesperline 改了 */ printf("bytesperline=%u sizeimage=%u\n", fmt.fmt.pix_mp.plane_fmt[0].bytesperline, fmt.fmt.pix_mp.plane_fmt[0].sizeimage);驱动在对齐方面非常死板,尤其NV12这类格式,宽度不是16的倍数时,bytesperline会被强制往上对齐。你如果按192010803/2去算sizeimage,大概率比驱动实际要求的buffer小,后面QBUF会报EINVAL。正确姿势是S_FMT后把plane_fmt里的bytesperline和sizeimage原样读出来,后续申请buffer全按这个值来。
4.2 REQBUFS、QBUF、STREAMON的顺序一个都不能乱
rkisp的stream状态机对调用顺序很敏感,最常见的崩溃就是没建media link就直接REQBUFS,或者QBUF前没做QUERYBUF。标准流顺序应该这样走:
struct v4l2_requestbuffers req = {0}; req.count = 4; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; req.memory = V4L2_MEMORY_MMAP; ioctl(fd, VIDIOC_REQBUFS, &req); struct v4l2_buffer buf = {0}; struct v4l2_plane planes[1] = {0}; buf.type = req.type; buf.memory = V4L2_MEMORY_MMAP; buf.index = 0; buf.length = 1; buf.m.planes = planes; /* QUERYBUF 拿到 mmap 的 offset 和长度 */ ioctl(fd, VIDIOC_QUERYBUF, &buf); void *map = mmap(NULL, planes[0].length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, planes[0].m.mem_offset); /* 入队,之后才能 STREAMON */ ioctl(fd, VIDIOC_QBUF, &buf); ioctl(fd, VIDIOC_STREAMON, &req.type);REQBUFS的count建议先按4个申请,太少容易在帧率波动时丢帧,太多则浪费内存。QUERYBUF那一步很多人偷懒跳过,直接拿固定地址mmap,结果驱动写了内核分配的另一块内存,画面永远花。QBUF是入队归还buffer给驱动的动作,STREAMON必须在至少一个buffer入队之后执行,否则驱动没有可用的内存写入目标,会卡在等待队列里。
4.3 stats与params:rkisp不同于普通USB camera的关键之处
rkisp驱动代码里,最不好理解的部分就是stats和params两个辅助节点。普通USB camera只要打开节点就能出图,rkisp不一样:ISP处理完一帧后会把统计信息通过stats节点上报给应用层,应用层算完3A参数再通过params节点写回驱动。这个闭环如果没跑起来,画面永远是默认参数下的结果,暗光下会黑到你怀疑人生。
/* 打开 stats 节点,通常是 /dev/video1 */ int fd_stats = open("/dev/video1", O_RDWR); struct v4l2_event_subscription sub = {0}; sub.type = V4L2_EVENT_ISP_STATS; /* 以SDK头文件定义为准 */ ioctl(fd_stats, VIDIOC_SUBSCRIBE_EVENT, &sub); /* 打开 params 节点,通常是 /dev/video2 */ int fd_params = open("/dev/video2", O_RDWR); struct rkisp_params_cfg cfg; memset(&cfg, 0, sizeof(cfg)); ioctl(fd_params, RKISP_CMD_SET_PARAMS, &cfg);stats事件是ISP中断触发后由驱动上报的,订阅动作一定要在STREAMON之前完成,否则前几帧的统计信息会被丢掉,AE会在一段时间内不收敛。params下发的结构体在不同SDK里差异很大,驱动代码里通常有RKISP_API_VERSION之类的宏来约束版本,应用层和内核态结构体版本不一致时,参数会被错误解析,表现就是画面颜色诡异,AWB怎么调都偏色。常见做法是先在应用层把params结构体完整清零,只设需要的模块位,别一上来就填满。
5. rkisp驱动踩坑记录:media链路、格式对齐与事件订阅
rkisp这套驱动代码,说实话调试起来最折磨人的不是算法复杂,而是那些链路和buffer层面的隐性约束。我整理了几条自己翻过车的记录,每一行都是实际烧过时间的地方。
5.1 链路类问题:sensor找不见、STREAMON报EPIPE
现象:media-ctl -d /dev/media0 -p输出里只有rkisp自己的entity,sensor完全看不到,像是被凭空吞掉了。
原因:sensor驱动没有成功probe。最常见是i2c地址不对,或者sensor供电的regulator没在dts里打开,sensor芯片根本没上电,读不到chip id,probe直接返回-ENODEV。
解决:先用dmesg | grep -i sensor看有没有sensor注册失败的打印,再检查i2c总线:i2cdetect -y <bus>,如果能扫到地址说明硬件OK,问题在dts或驱动匹配表;扫不到就顺着供电和时钟排查,别一上来就怀疑rkisp驱动代码有问题。
现象:media-ctl -p里链路都有了,但应用层调用STREAMON直接返回EPIPE,连个多余日志都没有。
原因:media link没有被enable。链路存在是两码事,链路状态是[0]还是[1]才决定数据能不能流过去。尤其在换了sensor或改了dts之后,某些SDK的link默认状态会被重置。
解决:手动把链路建起来:
media-ctl -d /dev/media0 -l "'sensor 0-0036':0 -> 'rockchip-mipi-csi2':0 [1]" media-ctl -d /dev/media0 -l "'rockchip-mipi-csi2':0 -> 'rkisp-isp-subdev':0 [1]"entity名字必须和media-ctl -p里打印的一字不差,粘贴复制最保险。链路enable后如果还报EPIPE,再去查resizer和mainpath之间的link,rkisp的链路不止一段。
5.2 格式与buffer类问题:EINVAL、sizeimage对不上
现象:QBUF报EINVAL,或者打开了节点但一入队就失败,日志里没有任何内核错误,看起来像是驱动故意不给人用。
原因:S_FMT返回后,驱动把bytesperline和sizeimage改了,你没读回来,直接按自己计算的数值去申请buffer。NV12在非16对齐宽度下非常容易触发这个问题,还有一个隐藏坑是multi-plane接口的buf.length要赋成plane数量,而不是buffer大小。
解决:S_FMT之后强制把plane_fmt[0].bytesperline和sizeimage打印出来,所有buffer申请都用驱动给的值。如果发现驱动把宽度从1920对齐到了1920但bytesperline从1920变成了2048,说明内部有额外对齐,不要试图用V4L2_PIX_FMT把格式改成RGB去绕过,绕不过去的。
现象:v4l2-ctl抓帧成功,文件大小看起来正常,但播放出来画面整体偏移,或者后半部分出现条纹。
原因:sizeimage比实际数据需要的空间大,驱动在buffer尾部留了padding。应用层把整个sizeimage当作有效数据拿去解码,解码器把padding当成了数据。
解决:解析帧数据时,每帧有效行数按height算,但每行长度要用bytesperline,不能用宽度乘像素字节数。如果bytesperline是2048而宽度是1920,NV12下UV平面的起始地址也要基于bytesperline计算,否则色度信息全偏。
5.3 事件与参数类问题:黑屏、stats事件丢失
现象:mainpath出帧正常,帧率稳定,但画面全黑,曝光值完全不动,感觉ISP根本没在做自动曝光。
原因:params通道没有任何数据下发,ISP部分模块处于未初始化状态,或者stats事件没有被消费,3A闭环根本没建立。很多调试板子只打开了mainpath,忽略了与ISP配套的stats和params节点。
解决:至少用空结构体走一次RKISP_CMD_SET_PARAMS,再订阅stats事件。如果条件允许,先接一支基础3A库跑通闭环,再换裸驱动调试。一味只看mainpath出黑帧,容易误判成sensor问题,浪费半天时间。
现象:stats事件订阅了,但read返回超时,或者事件频率远低于帧率,AE反应迟钝。
原因:订阅发生在STREAMON之后,前几帧统计已经丢了;另一种可能是在多个fd上混用了stats和params节点,不同实例的上下文错位。
解决:严格按open stats -> SUBSCRIBE_EVENT -> REQBUFS -> STREAMON的顺序初始化,并且stats和mainpath要用同一个rkisp实例下的节点,不同videoX之间不要混搭。把订阅时机提前到STREAMON之前,问题基本就消失了。
6. 我排查rkisp问题的固定动作:media拓扑存档与ioctl回放
接手一块新的Rockchip板子,我不急着改任何代码,先把环境状态固化下来,再动一行驱动代码。第一件事是dump一份media拓扑存档:
v4l2-ctl --list-devices > /tmp/devices_$(date +%m%d).txt media-ctl -d /dev/media0 -p > /tmp/media0_$(date +%m%d).txt dmesg | grep -iE "rkisp|csi2|sensor" > /tmp/isp_dmesg.txt这三个文件就是一个可对比的基线。后面改sensor dts、更新驱动代码、换内核,都能用diff /tmp/media0_旧.txt /tmp/media0_新.txt快速看出链路变化。很多rkisp问题是被SDK更新悄悄改掉了entity命名或link默认状态,对比存档比翻git log快得多。
第二件事是抓一帧原始数据验证基本通路:
v4l2-ctl -d /dev/video0 \ --set-fmt-video=width=1920,height=1080,pixelformat=NV12 \ --stream-mmap --stream-count=1 --stream-to=/tmp/frame.raw ls -l /tmp/frame.raw文件大小如果和S_FMT后返回的sizeimage一致,说明格式协商正常;如果不一致,基本就是链路里某一段没对齐。这一步跑通了,才能继续谈颜色、曝光和3A。
第三件事是把应用层的调用顺序固定成一个回放脚本,每次改驱动后按相同顺序跑一遍。从open开始,到subscribe event、S_FMT、REQBUFS、QUERYBUF、mmap、QBUF、STREAMON,最后再RKISP_CMD_SET_PARAMS投递一次空参数。顺序不能乱,乱一次就是一次翻车。
从那以后,我拿到任何一块新板子,第一件事就是先把media拓扑dump一份存档,再动驱动代码。这个动作救过我很多次,也省下了好几个排查到深夜的下午。希望帮到你。
本文还有配套的精品资源,点击获取