ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

008、Camera HAL 与 Linux Kernel 的边界

008、Camera HAL 与 Linux Kernel 的边界

008、Camera HAL 与 Linux Kernel 的边界

写这个题目前,我脑子里先蹦出来一件往事。去年调一块老平台,HAL层一开流,预览画面绿油油,像是蒙了一层滤镜。驱动同事说HAL传下去的buffer格式不对,HAL同事说驱动把pixel format解析错了。两边各自拿着打印,吵了三天。最后我拉了个v4l2 trace,发现驱动在VIDIOC_S_FMT里把bytesperline改成了比width*2更大的值,而HAL层一厢情愿地按自己的stride去填数据。谁都没错,但数据就是在边界上错位了。从那之后我养成了个习惯:看Camera问题,第一件事先问“这个字段到底是HAL该填,还是kernel该填,还是两边都要认同一个值”。这就是Camera HAL和Linux Kernel的边界问题,比大多数想象中要微妙得多。

边界不是一条线,是一堆交错的接口

教科书喜欢说HAL在用户态,Kernel驱动在内核态,中间是系统调用。这话对,但没实际用处。真正干起活来,边界是几个具体的“握手点”:

  • 设备节点:HAL打开/dev/video*,这是第一个握手。
  • 控制通道:通过ioctl发各种VIDIOC_*命令。
  • 数据通道:mmap出来的buffer,或者dma_buf导入导出。
  • 事件通道:HAL在poll()上等,内核驱动完成帧事件后唤醒。
  • 同步机制:sync_file、dma_fence,用于和ISP、GPU、Display协作。
  • 电源域:HAL通过media controller打开entity,然后内核管理时钟和电源域。

每个握手点都可能出错,而最烦人的是错法还不一样。控制通道出错,多半是ioctl返回值没检查,或者结构体里的reserved字段没清零。数据通道出错,那就是颜色错乱、花屏、buffer overflow这种肉眼可见的灾难。事件通道出错,通常是HAL等不到POLLIN,然后莫名其妙超时。同步机制出错,那就是死锁,整个Pipeline卡死,连log都刷不干净。

我在代码里见过最坑的写法,是HAL层为了“性能”跳过VIDIOC_QUERYBUF,直接拿自己算的offset去mmap。驱动换了版本,mmap的语义跟着变,offset 对不上了,结果摄像头一开流就崩。崩溃地址在libc,谁也想不到是上层少调了一个ioctl。这就是典型的边界不清晰:HAL以为自己在和内核驱动“直接对话”,实际上中间隔着V4L2框架,你不按规则走,框架就让你以最惨烈的方式清醒。

V4L2是边界上的“收费站”,但不是唯一的

V4L2确实是最常见的Camerabuffer传输框架,但千万不要以为内核只管V4L2。现代CAMERA流水线里,HAL要打交道的还有:

  • media controller:用来拓扑配置,比如让某个sensor和ISP的某个entity连接起来。
  • camera sensor / isp / isp_subdev:这些v4l2_subdev节点,HAL通常不直接read它们,但会通过media-ctl或者直接ioctl(VIDIOC_SUBDEV_S_FMT)设置。
  • i2c / spi:sensor寄存器配置,有些HAL会自己写i2c设备节点,虽然我现在强烈不推荐这么做。
  • gpio / regulator / clk:用来控制reset、电源、MCLK。这些往往在开机阶段由内核驱动做,但有的平台偷懒,让HAL去导/sys/class/regulator。这种设计是妥妥的边界模糊,出了问题你会同时挨硬件和内核驱动两方的骂。

有一次我们排查摄像头无法唤醒,最后发现是HAL层直接写了一个GPIO口去拉sensor的reset,而那个GPIO在设备树里已经被另一个外设申请走了。系统没崩,但sensor的I2C就是不回应,因为reset被别的设备拉住了。你说这是HAL的错还是内核的错?站在边界上,两边都有责任。正确的做法是:HAL只负责通过V4L2/media framework的接口去控制sensor状态,具体的GPIO、电压这些活儿,交给内核的v4l2_subdev_core_ops里的s_power回调去干。越过这条线写硬件,就是自己给自己埋雷。

buffer路径上最容易打架的地方

回到最开始的绿屏问题。buffer在HAL和Kernel之间传递,绕不开几个关键字段:

  • bytesused:这帧实际用了多少字节。有的驱动在处理partial frame时会改这个值,而HAL如果傻乎乎地按widthheightbpp去读,就会把末尾的垃圾数据当有效像素。我见过HAL层有人直接写死bytesused = width * height * 2,完全无视驱动填的值。结果驱动因为要对齐256字节,实际填的比这大了一点,而HAL拿到的是旧数据——因为bytesused没更新,HAL还在消费上一帧的缓冲区。这种问题极其隐蔽,计数器看起来在走,但画面是delay的。
  • field:隔行扫描那个字段。现在大部分sensor都是逐行扫描,这个值填V4L2_FIELD_NONE就对了。但有些老驱动会填V4L2_FIELD_ANY,HAL如果没校验,直接丢给ISP,ISP那边就毛了。
  • timestamp:这是大坑。HAL和内核用的时钟基准可能不一样。有的驱动用ktime_get_boottime(),有的用getnstimeofday()。HAL侧如果用CLOCK_MONOTONIC去对比时间戳,就会飘。更麻烦的是跨v4l2 buffer 和 sync_fence 的时间戳,需要厂家统一基准,否则帧率统计都是扯淡。

我写代码的习惯是:HAL层在任何VIDIOC_DQBUF之后,第一时间打印pixelformat、bytesused、reserved2、timestamp这四个字段,打三帧再关掉。这不是浪费性能,这是在边界上做“入境检查”。很多问题看一眼这几个值就知道是谁不负责任。

关于buffer的cache一致性,也值得单独聊。内核驱动在把sensor DMA过来的数据写到mmap buffer之前,会做dma_sync_single_for_cpu()。如果HAL层用CPU读这个buffer,没问题。但如果HAL是丢给GPU/ISP硬件去读的,就麻烦大了。HAL必须保证自己不再用CPU去碰这块内存,否则cache里的数据没刷新,硬件拿到的是脏的。有个平台,HAL为了做颜色转换,在GPU读之前先用NEON指令把buffer里的YUV转成了NV12,结果因为cache没有clean,GPU读到的还是旧数据,画面上全是马赛克。那是在用ion/dmabuf的时候踩的,后来老老实实加了DMA_BUF_IOCTL_SYNC,问题才消失。所以,border线上还有一堆和DMA_BUF相关的ioctl,别嫌烦,那是和内核同步“我这边访问完了,你那边可以读了”的必备仪式。

时间戳和metadata,谁说了算

我在很多方案里看到,sensor的曝光、增益、白平衡这些3A参数,是HAL通过V4L2的VIDIOC_S_EXT_CTRLS写下去的。这个动作本身没问题,问题在于驱动什么时候生效。有的sensor是写寄存器立即生效,有的sensor是等下一次VSYNC才生效。HAL的3A算法通常需要知道参数生效的确切帧时机,否则算出来的AE/AG/AWB全错。这种情况下,HAL需要靠V4L2_EVENT_FRAME_SYNC或者vsync timestamp来对齐。而内核驱动有义务在填buffer时,把vsync_ioctl里的vb2_v4l2_buffer.sequence和timestamp保持一致。

我见过一个驱动,sequence在VIDIOC_DQBUF后居然是乱序的。排查到最后,是驱动在sensor温度过高的时候丢帧,但没有把V4L2_BUF_FLAG_ERROR置位。HAL侧看到sequence跳变,还以为丢了帧,实际是驱动没处理这种情况。这个锅必须内核背。反过来,HAL侧也有很多骚操作,比如直接调用VIDIOC_STREAMON然后不等POLL就去VIDIOC_DQBUF,直接返回EAGAIN。那完全是HAL对V4L2状态机不理解。STREAMON之后必须要等到底层驱动把第一个buffer放进去,poll会等到。你硬抢,就只能一次次吃EAGAIN。写代码别和内核“抢跑”。

电源管理那儿的边界暗坑

摄像头有一个经典问题:设备休眠后唤醒,sensor状态丢失。HAL层有时候为了省事,在resume后不管三七二十一再调一次VIDIOC_S_FMT。你以为这能恢复格式?驱动可能根本不会重新应用sensor setting,因为它觉得FMT没变,直接返回了0。这又是个边界定义问题:HAL觉得“我重新设置一次格式,你驱动就该重新配置一遍”,内核驱动觉得“我这里有个state,你设的值和上次相同,我就认为不需要重新配置”。两边都没错,但结果就是sensor黑屏。这是驱动开发里常见的“状态同步”问题。解决办法是HAL层不要用VIDIOC_S_FMT去恢复初始化,而是走VIDIOC_STREAMOFF再VIDIOC_STREAMON,或者明确调VIDIOC_SUBDEV_S_POWER(如果有的话)。这种细节,只有被坑过的人才喜欢在代码注释里写个大叹号。

还有一次,我们遇到sensor突然下了电,HAL这边poll一直等到超时。最终发现内核驱动在runtime suspend回调里,顺手把V4L2的video节点给关了。因为那个平台的power domain绑的是sensor的电源域,一超时自动省电。HAL没有申请pm_runtime_get_sync()之类的东西,或者说得更准确一点,HAL不该去直接碰电源,而是应该保证在整个streamon之后保持子设备处于pipeline开启状态。内核驱动这边也有责任,它应该在STREAMON的ioctl里做pm_runtime_get,在STREAMOFF里做put。如果你遇到驱动没干这件事,去骂驱动工程师,然后把这个问题写进review checklist里。

边界问题排查三板斧

一是用v4l2-compliance系统性地跑一遍。这个东西是内核v4l2 framework自带的,它能告诉你驱动对每个ioctl的响应是否符合规范。很多HAL侧的奇怪现象,根源是驱动没有遵守规范,v4l2-compliance会一针见血地报出来。比如某个VIDIOC_REQBUFS的count限制不合法,v4l2-compliance会告诉你“expected 1, got 0”。别急着写代码,先跑一轮。

二是加打印,但是别乱加。在HAL层的QBUF和DQBUF里加上打印,打印出index、bytesused、flags和timestamp。一行一个,连续拍10帧,然后把log丢给驱动的人。他们看到第一眼就知道你那边有没有做奇怪的操作。如果两边的打印对得上,问题就在更底层。

三是看设备树和media topology。HAL的错误不一定是HAL的代码逻辑,很可能是驱动注册出来的entity顺序不对。比如sensor的v4l2 subdev节点注册时名字给了/dev/v4l-subdev0,而HAL假设/dev/v4l-subdev0是ISP。这种“路径假设”问题和宿命论一样,代码根本没法自我纠正。你需要用media-ctl -p看一张真实的拓扑图,对照着HAL里的枚举逻辑检查。

最后,给新人们几句不中听的

别在自己的代码里写“根据kernel版本”的条件编译。一旦你写了这行,就说明你对边界根本没有清晰的把握。内核接口是驱动提供的,HAL只需要按照V4L2规范去调用,版本兼容是上游需要管的,你判断不了那么多。把版本判断扔给CMake,不如信任官方头文件。

别在HAL层直接操作寄存器。不管你觉得自己多牛,看到的sensor datasheet多权威,那都是内核驱动该干的活。你手痒写一句devmem进去,之后别人看代码会被你气死。如果驱动实现有问题,该驱动去改驱动,HAL别越俎代庖。

还有一个经验:调试和vendor的摄像头驱动问题时,永远在HAL侧保留一份“不经过3A、不经过ISP tuning,直接dump YUV数据”的旁路开关。因为边界问题最大的麻烦就是“黑盒”,你不知道数据在哪个阶段被改坏了。有了旁路,你可以在HAL拿到内存buffer的时候直接保存原始数据,然后开个工具如RawViewer或者Python脚本解析。一旦你能确定“驱动给的YUV裸数据是对的”,那么问题就在HAL后处理,反之则在kernel。这个习惯救过我很多次命。

边界这破事儿,说不清是“谁的地盘”,但出了问题永远是“谁都不认账”。维护得好的系统,其实是靠框架纪律撑着的——HAL不做内核的事,内核也不暴露不该暴露的接口。你守住这条线,Camera pipeline就能跑得丝滑。你越线一次,就得用一个周末去填坑。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进