034、影像虚拟化资源管理——高通骁龙平台的虚拟机间ISP共享与QoS保障机制
去年年底在帮一家车厂做8155平台的智能座舱方案时,碰到一个特别诡异的案子。客户报障说,倒车影像偶尔会卡顿,而且不是那种帧率掉到20fps的卡顿,是那种画面突然停顿一下、然后跳变的感觉,像极了丢帧。我们一开始怀疑是摄像头驱动的问题,查了三天,把CSI2的时序、MIPI的lane分配、甚至线束的屏蔽层都翻了个底朝天,毫无头绪。后来把日志拉出来,发现一个关键线索——卡顿发生的时刻,正好是仪表盘上的导航地图在做3D渲染切换的瞬间。这就对上了,问题出在虚拟化层,而不是物理链路。
这代8155的ISP是支持硬件虚拟化的,也就是说,一个物理ISP可以被切成多个虚拟ISP实例,分别给仪表系统(QNX)和座舱系统(Android)用。但问题在于,高通默认的虚拟化方案里,ISP的带宽和算力分配是静态的,按比例切好就固定了。我们当时给仪表侧分了60%的ISP资源,给座舱侧分了40%。结果导航3D渲染一跑起来,座舱侧的GPU和ISP争抢DDR带宽,ISP的吞吐量瞬间掉到阈值以下,虚拟ISP实例就开始丢帧。而仪表侧虽然只用了60%的资源,但因为它的虚拟ISP实例优先级高,反而没受影响。这事的本质是,静态分区在动态负载面前就是纸糊的墙。
高通在骁龙平台上解决这个问题的核心机制叫“ISP QoS Token Bucket”,说白了就是一个令牌桶算法,但实现细节相当讲究。每个虚拟ISP实例都有一个独立的令牌桶,桶的容量和填充速率由系统侧的TZ(TrustZone)固件在启动时配置。关键点在于,这个配置不是死的,它允许运行时的动态调整,但调整的入口不在Linux内核里,而在一个叫“Camera Resource Manager”的微核服务里。这个微核跑在TZ的信任域里,普通内核驱动只能通过SMC调用去请求调整,不能直接改寄存器。这里踩过一个坑——我们一开始想当然地在Android内核里直接写ISP的QoS寄存器,结果一写就触发安全异常,整个相机服务直接crash。后来才明白,高通的虚拟化安全模型里,ISP的QoS寄存器是TZ独占的,非安全世界只能通过请求-授权的方式间接控制。
实际调优的时候,光理解令牌桶还不够,得搞清楚令牌桶的“突发容量”和“持续速率”分别对应什么。持续速率决定了虚拟ISP实例的保底带宽,突发容量决定了它能瞬间借用的额外带宽。我们当时把仪表侧的突发容量调得很大,想着反正它平时用不满,不如让座舱侧在导航渲染时能借点带宽。结果发现,令牌桶的借用机制有个隐藏约束——它只能借用同属一个“QoS域”的空闲令牌,而8155上仪表和座舱默认分属不同的QoS域。跨域借用需要额外配置一个“共享池”,这个共享池的大小在PIL(Peripheral Image Loader)加载ISP固件时就得定好,运行期改不了。所以正确的做法是,在启动阶段就把共享池预留出来,而不是等出问题了再想办法。
代码层面,高通提供了一个叫cam_sync_qos_request的接口,但别指望它开箱即用。这个接口的入参是一个结构体,里面有个qos_priority字段,取值范围是0到255。我们一开始按直觉把座舱侧的优先级设成200,仪表侧设成50,想着高优先级肯定先保障。结果完全反了——高通的QoS语义里,数值越小优先级越高,0是最高,255是最低。这个反直觉的设计坑了不少人,我们后来在代码注释里专门写了警示。另外,cam_sync_qos_request是异步的,它只是把请求挂到队列里,真正的生效要等TZ侧的回调确认。如果你在请求后立刻去读ISP的带宽统计寄存器,读到的还是旧值,必须等一个CAM_SYNC_QOS_ACK事件。这里有个调试技巧,可以在TZ侧开一个trace点,用atrace抓SMC调用的时间戳,能精确看到请求到确认的延迟,正常应该在微秒级,如果超过100微秒,基本可以断定TZ侧有别的安全服务在抢占CPU。
再往深一层说,虚拟化ISP共享的瓶颈往往不在ISP本身,而在它下游的“图像处理管线”的DDR带宽。高通把ISP的输出直接写到DDR的特定区域,然后由各自的虚拟机去读取。如果两个虚拟机的DDR访问模式是“交错突发”的,比如仪表侧每写一行就切到座舱侧,那DDR的bank冲突会非常严重,实际带宽可能只有理论值的60%。解决这个问题要靠“DDR QoS Port”的配置,在8155上叫DDR_QOS_PORT_ISP0和DDR_QOS_PORT_ISP1,这两个端口可以设置不同的仲裁权重。我们最后是把仪表侧的端口权重设成3:1,同时把座舱侧的端口突发长度从256字节降到128字节,这样虽然单次传输小了,但减少了bank冲突,整体吞吐反而提升了15%。这个参数在文档里写得模棱两可,只说“建议按场景调整”,实际上就是让你自己试。
还有一个容易忽略的点是“虚拟ISP实例的时钟域”。高通允许每个虚拟ISP实例跑在不同的时钟频率上,但频率切换是有代价的——需要停掉整个ISP的流水线,等PLL重新锁定,这个时间大概在200微秒左右。如果你在座舱侧频繁调整时钟频率,比如根据场景自动切换高帧率模式,那每次切换都会让仪表侧的虚拟ISP实例也跟着停一下。我们当时就遇到过,座舱侧切到60fps模式时,仪表侧的倒车影像会闪一下黑屏。解决办法是,把座舱侧的时钟频率切换策略改成“渐变式”,先升到中间频率,稳定后再升到目标频率,避免直接跳变。这个策略在驱动里用cam_isp_set_clock_ramp接口实现,但要注意,这个接口在QNX侧和Android侧的实现不完全一样,QNX侧需要自己维护一个状态机。
最后说点个人经验。做这类虚拟化资源调优,别一上来就钻寄存器细节,先花半天时间把高通的“Camera Virtualization Overview”文档里的架构图吃透,特别是那个“QoS Domain”和“Resource Pool”的关系图。然后,一定要在项目早期就验证跨域共享池的配置,别等到联调阶段才想起来。还有,TZ侧的日志默认是关闭的,记得在tzbsp的配置里打开CAM_QOS_DEBUG,否则出了问题你只能靠猜。另外,如果你们用的是非高通平台,比如瑞芯微或者安霸,它们的虚拟化方案思路完全不同,瑞芯微的RKISP是软虚拟化,靠驱动层的调度器做时间片轮转,没有硬件级的QoS保障,所以别把高通这套经验直接套过去。做影像系统架构,平台差异永远是第一位的,方案可以借鉴,但底层机制必须重新理解。