曾经在紫光展锐做过几年的camera驱动,经历过从2013 年最初的几人团队,每人独当一面,负责很多的模块的粗放,到后面的逐步的精细化,设计部门按照内核驱动,hal驱动,tuning效果,3A,效果算法等展开,个人和团组只需专注于某一层面的模块或者算法的反复迭代。现在虽然离开展锐已经数年,对其中软硬件开发过程中的映像还是比较深刻,对开发的规范执行和架构和feature实现的优化选择尤为感慨,这里做个对当时情景的分享回顾,让大家对手机里面的camera架构有个了解。
以上是当时展锐 camera系统的软硬件大致架构。最底下是展锐的isp 硬件系统和外部sensor。其上是内核层的各类对应驱动。再上面是内核层之上,hal层之下的hal 分层驱动,最上就是安卓自带的cameraserver,负责在camera的app 和 hal之间来调用传递帧和各类参量。
HW 层
HW这个对应的层级是硬件层,主要是芯片里面的isp 硬件系统,相机后处理模块cpp,csi /mipi,外部的各类典型的sensor。
这里isp 硬件系统包含了online 处理部分的DCAM 和offline的isp,不包括mipi csi controller和dphy,展锐对于isp硬件的定义和行业稍有差异,业内把所有从sensor 进入的mipi dphy 到最终输出到DDR buffer 之前的所有关于图像流处理的硬件系统都称之为isp,如上图虚线框内的这部分。
online 部分的DCAM 硬件接收从mipi csi controller 过来的数据流,实现预览录像和拍照的数据分流,还有其他的pdaf,vch 虚拟通道的解包分流等。预览拍照通路接受sensor过来的全尺寸图像,变换为最终输出的图像流的各种尺寸。在通路内部增加了不同的效果处理模块,online 主要关注raw 域的效果处理,这些模块会在后续展开。dcam 分流出来的通路最终会把图像帧吐到驱动设定的各个内部轮转的buffer内,给offline的isp 硬件做输入。
这里一个DCAM 硬件对应一个mipi csi controller 过来的数据流,也就是一个正在运行的sensor,如果是多个sensor 同时运行,那就有多个DCAM 接入到mipi csi controller 后面在运行,经典的isp 系统一般有两个主要的online的DCAM,支持大尺寸的sensor,1~2 个小的DCAM支持小尺寸的sensor。
offline部分的isp硬件只有1个,它接收各个DCAM送来的图像帧,在各路流的30fps的基础上做分时的预览/拍照/录像的帧的效果处理,最终输出YUV420 图像。一般来说,一个33ms内分成4个slot,按照预览/拍照/录像/拍照 这样的顺序做分时处理,之所以这样设计isp,是因为针对大尺寸的拍照图片,在online 的处理上带宽不容易满足,容易产生瓶颈导致堵塞整条通路。做成这种offline 处理后,将图像在宽度方向上按照2592 的最大切片宽度做分割,这样64M 或者上亿分辨率的大图像,在宽度方向上切割成几个slice切片,针对每个slice 做分别处理,在输出设置上做准确的配置,使isp输出在每个行buffer上对应的切片位置,最终得出完整的一帧处理后的大图像,绕开了超级大图带来带宽瓶颈。唯一的不足就是拍照牺牲了实时性。需要几帧的时间来完成大图的isp 处理。
对于预览和录像来说,没有这种切片需求,1个slot 内基本都能处理完毕。如果在运行期间没有拍照,那isp会调度把拍照这部分的带宽让出来优先处理正在运行的预览录像。
这样多个online 部分的DCAM 硬件,加上单个的isp组合,基本实现了各种场景尤其是双摄的典型应用了,同时可以降低zsl 拍照时的运行带宽。只有zsl 拍照时刻,DCAM 的拍照通道的大图才会被offline部分的isp硬件处理,而且是分割切片处理了各个slice。
cpp 这部分是相机的后处理模块,主要是关于图像的旋转和拉伸,拉伸主要用于在拍照过程中需要的输出320*240的thubmbnail 图,这样方便和大图一起合成最终的jpeg图,这样在PC 上能被正确的看到小图标。旋转主要是90,180,270 ,mirror,flip这类简单的翻转。
csi/phy 是关于mipi csi controller和dphy,cphy 这类,展锐有自己实现的这类模块,实现国产化的替代。csi/phy 也是对应一个sensor 一路,一般的isp 硬件系统有至少3路这样的mipi csi controller和phy。
sensor 就是手机上的各类sensor,包括前后摄像头,双摄的第2颗辅助摄像头,其他应用场景的摄像头等,都是用i2c来写入配置,产生mipi 总线上的图像流输出到ISP系统
kenerl 驱动层
对应于硬件上的结构设计, 在kernel 内部驱动也是做了相应的匹配设计。
展锐isp 的驱动在最早的版本上是实现了V4L2 框架的设计,后续为了方便支持双摄等场景,剥离了V4L2 ,采用了简单的类V4L2 接口的misc 设备节点,在对应V4L2 ioctl 命令的基础上增加了BIND/UNBIND 摄像头指定的dcam id这样的操作接口。这样不用大改user到HAL 这层的驱动,就能沿用上之前的框架。
isp 驱动最上层是dcam_core 这层接口,实现输入图像流尺寸,格式,输出各路图像的尺寸,格式,帧buffer的配置,最终启动streamon, 通过dequeue,queue 实现各路图像帧的完成上传和回收排队再次等待接收图像帧。或者streamoff 停止图像流的接收等,这一层完全是逻辑相关的,不涉及到任何的硬件操作。实现了对上的cmr_grab 的适配,对下的dcam_drv 和isp_drv 的调配。
dcam_core下面有dcam drv 和isp drv 这两种驱动模块,分别处理online 和offline 硬件部分,dcam drv主要是关于online的输入图像的接口模块的cap_top和各个输出通路的寄存器的配置,各类中断的响应,输出buffer的完成接收,给到dcam_core,上传给cmr_grab到和上层使用完毕,压回dcam_core到dcam drv 。还有关于各个效果模块的分别参数配置,3A信息帧的配置输出和回收配入,通道sof 等中断信息的发出。isp drv 是关于offline 分时调度的配置和大图slice 切割的配置,中间还是dcam 剩余的效果处理,比如full RGB域,YUV域的效果模块配置。
类似的cpp 也是这样分为cpp_core和cpp_drv 这样的分层处理,cpp_core是逻辑层的接口,主要识别是拉伸还是旋转的配置启动。在cpp_drv里面实现了具体的拉伸还是旋转的寄存器配置和完成通知。
sensor 驱动抽象了所有sensor的供电,配置clk,配置i2c 地址,传输配置参数这类共同的操作接口,还有就是mipi phy和controller的初始化装载和卸载。user 层sensor驱动只需要将具体的配置表(寄存器和值这样)和相关的reset,powerdown有效配置传入,kernel就能正确的驱动。
除此之外,kernel 还有你关于各个模块内需要的上下电接口pw,clk 配置接口等。
需要说明的是,展锐当时实现在了一个族的系列片最小的修改量,来支持不同系列的芯片项目,这样沿用一套完全相同的驱动框架,通过反复的软件迭代来保证项目的稳定。,如下图这样,将有不同的差异部分的操作抽象出来,实现在xxx_drv层的调用。
HAL层驱动
kernel 之上是user层的驱动,也就是HAL分层驱动。除了HAL 使用了安卓原生的c++之外,其他都是用标准c写的,增加了可移植性,同样的一套code,从HAL层以下都能方便的移植到其他的linux系统上去。
HAL层code 最早支持HAL1.0,中间也曾短暂搞过2.0,最终在展锐那会是HAL3.0 的框架,1.0,2.0 基本不再维护和使用。这部分code 是对安卓框架,尤其是java app 这边的功能支持,同时也要调用到sprd_oem 层里面。
sprd_oem 是综合场景的实现层,实现了最终各种场景的顶层设计。它可以调用其下各个模块来实现hal层需要的接口。
sprd_oem层下面的各个模块不能互相调用,只能由sprd_oem来调用实现完整流程。这模块提供接口或者回调函数指针,来实现往下的调用和往sprd_oem上层的信息传递。
cmr_preview 模块 是实现预览或者录像通路的完整配置和数据帧的流转。其中包含了设置输出帧的格式,大小,ion 管理的buffer 地址,输入帧的格式,大小等。
cmr_snapshot 模块是实现拍照通路的完整配置和数据帧的流转, 其中包含了设置输出帧的格式,大小,ion 管理的buffer 地址,输入帧的格式,大小等。 它的大小图生成和转化成合成的jpeg 输出的内存的分割由sprd_oem调用cmr_mem 来实现。
cmr_mem 是管理拍照的内存分割,如果按照拍照时需要各种类型的内存完全平铺来申请,那内存会耗的非常厉害,这里采用了优化的合并复用,来减少内存的开销。
cmr_grab 是HAL 分层驱动的最底层,是适配kernel 层的dcam core,来实现图像帧,统计帧的抓取,sof 各帧时间戳的获取等。
关于ISP效果类的配置和统计信息,cmr_grab 将会通过判断接收类型分发到isp mw层去。isp mw 会把3A 信息再传递给isp 3A alg,结合app这边给出的配置来决定3A的调配运行。同时isp tune 这层也会把app 这边给出来的配置通过isp mw ,cmr_grab ,下发到内核中配置硬件,实现效果的动态配置。
cmr_sensor 是关于各种支持sensor的驱动配置,具体的mipi类型,供电参数,powerdown,reset,供电电压,帧率,寄存器配置表等,通过内核sensor 驱动实现了最终的sensor的配置。同时isp 这边需要的3A 统计部分也有对sensor 这边的动态配置。
cmr_sensor 里面的sensor 配置和 isp tune的sensor 效果是一一对应的,展锐把sensor的驱动放到了cmr_sensor 层,把sensor对应的数据流ISP 调校放到了 isp tune,通过项目的编译xml 文件来生成对应的可配置。而且展锐还做了了按需生成项目需要sensor库和tuning库,防止了无关的sensor库和tuning库带来的尺寸开销和运行时遍历sensor带来的性能问题。
这部分可以参考 展锐平台的camera sensor驱动代码设计解析
关于图像帧的流转
对帧的流转,从dcam的online 到isp的offline,然后和hal层的传递回传,这里大致介绍下。
展锐的所有需要的应用需要的图像帧buffer 是在HAL分层驱动里面由ion 来申请的,然后通过QUEUE 这个ioctl 传入,在kernel转换成物理地址,刷入到camera IOMMU的一级页表中去。streamon 以后不再动态改动这个页表,在streamoff 的时候才会撤除。
内核需要的从dcam 输出传递到isp 输入的帧是内核层分配的,在streamon 前根据格式和尺寸预先分配好,然后也是变换成物理地址以及设置页表的camera 专用IOMMU中去。
具体流程这样,dcam 在sof中断到来的时候设置下一帧的buffer到寄存器中去,这帧地址在下一个sof到来才会真正的生效,是下一帧的图像存在这帧的buffer,同时当前帧buffer 地址截接收生效。streamon后 的第一帧buffer 不需要sof,需要强写一个立即生效位,硬件就把当前帧写入buffer。
如图,dcam 后面一共有4片buffer,A2 这片已经收完整帧,然后给isp 去做输入。下一帧buffer A3 在之前A2帧的sof 已经设置好,在A3的sof 触发时生效,等待硬件刷入整帧数据,刷完后也是和A2一样,在A2 后面输入给isp,后面的A0,A1 也是同样处理。A2这帧的buffer如果isp 处理完,那就把它还给DCAM,排队到A1后面,依次等待dcam 的使用和输出。这样形成online 到offline的内循环处理。中间如果系统发生瓶颈问题,所有帧都给了isp,但是都没有处理完成,为了防止dcam online 的误踩有效图像,破坏已有数据帧,给DCAM 一个reseve帧,这个帧只是空转,不会传递给isp。等isp 有帧返回给dcam 了,再接着使用这些有效帧循环。
isp 这边的帧是HAL 分层驱动传递过来的,也是类似的思路。ISP 只有dcam 给过来帧和HAL 给输出帧buffer都有了,才会做offline的转换,否则啥都不干,因为没有输入输出buffer可用。当前图上 ISP 的0 buffer已经完成,上传给HAL,HAL 传递到app 去处理。1 号buffer 接着对应下一帧dcam 的输入处理,1号buffer如果处理完,也是上传到HAL,然后app,后面2号也是同样。app 处理完的3 号buffer当前回传给isp,ISP会把它放到2之后依次处理,这样形成isp 这边的外循环。
这就是展锐当时的camera 内部的软硬件系统架构。