15、摄像头驱动开发:QNX Camera框架、Android Camera HAL3实现、ISP图像处理、摄像头标定与预览流控制

摄像头驱动,在智能座舱里绝对是个硬骨头。我这些年调过的摄像头模组,少说也有几十款了。从最初的V4L2到QNX的Camera框架,再到Android的Camera HAL3,每一层都有它的脾气。今天咱们就把这块彻底捋清楚。

15.1 QNX Camera框架:底层基石

QNX的Camera框架,说白了就是一套基于资源管理器的驱动模型。它不像Linux那样用V4L2,而是走自己的路——io-pkt + cam_driver 的架构。

我个人习惯把QNX Camera框架分成三层:

  • 应用层:调用Camera API,比如 camera_open()camera_start_preview()
  • 服务层:Camera Service,负责管理多个摄像头实例
  • 驱动层:实际的硬件控制,包括MIPI CSI、I2C配置、ISP初始化

这里有个关键点——QNX的驱动是用户态进程。你想想看,这意味着什么?驱动崩了不会导致整个系统挂掉。我在项目中遇到过好几次,摄像头驱动在调试阶段频繁崩溃,但中控屏的导航和音乐播放完全不受影响。这在安全关键系统里太重要了。

核心数据结构: QNX Camera驱动中,最核心的是 cam_dev_t 结构体。它包含了设备节点、缓冲区队列、状态机等信息。每个摄像头对应一个这样的结构体。
// QNX Camera驱动核心结构体示例
typedef struct {
    char                    dev_name[32];       // 设备名,如 /dev/cam0
    uint32_t                cam_id;             // 摄像头ID
    cam_state_t             state;              // 当前状态
    cam_buffer_queue_t      buf_queue;          // 缓冲区队列
    mipi_csi_config_t       csi_config;         // MIPI CSI配置
    i2c_handle_t            i2c_hdl;            // I2C控制句柄
    isp_handle_t            isp_hdl;            // ISP处理句柄
    pthread_mutex_t         lock;               // 互斥锁
} cam_dev_t;

15.2 Android Camera HAL3:桥接的艺术

Android的Camera HAL3,是连接Framework和底层驱动的桥梁。它的设计哲学是——让Framework完全控制数据流

HAL3的核心接口是 camera3_device_t。你需要实现它的所有操作函数,包括 initialize()configure_streams()process_capture_request() 等。

嗯,这里要注意:configure_streams() 是性能的关键。Framework会在这里告诉你需要哪些数据流——预览流、拍照流、视频流等等。你需要根据这些流的需求,配置底层的ISP和MIPI接口。

我曾经踩过一个坑:某个项目里,预览流要求30fps,拍照流要求全分辨率。我一开始没注意,直接把两个流都配成了全分辨率。结果预览帧率掉到了15fps,画面卡得不行。后来才意识到,预览流应该用较小的分辨率,或者开启ISP的缩放功能

避坑指南:configure_streams() 中,一定要检查所有流的格式和分辨率是否匹配。如果某个流不支持,尽早返回错误,别等到 process_capture_request() 时才报错。
// HAL3 configure_streams 实现片段
int my_camera_configure_streams(
    const struct camera3_device *device,
    camera3_stream_configuration_t *stream_list)
{
    // 检查流数量
    if (stream_list->num_streams > MAX_STREAMS) {
        ALOGE("Too many streams: %d", stream_list->num_streams);
        return -EINVAL;
    }

    // 遍历所有流,检查格式和分辨率
    for (uint32_t i = 0; i < stream_list->num_streams; i++) {
        camera3_stream_t *stream = stream_list->streams[i];

        // 检查分辨率是否支持
        if (!is_resolution_supported(stream->width, stream->height)) {
            ALOGE("Unsupported resolution: %dx%d",
                  stream->width, stream->height);
            return -EINVAL;
        }

        // 检查格式是否支持
        if (stream->format != HAL_PIXEL_FORMAT_YCbCr_420_888 &&
            stream->format != HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED) {
            ALOGE("Unsupported format: %d", stream->format);
            return -EINVAL;
        }
    }

    // 配置底层ISP和MIPI
    return configure_hardware(stream_list);
}

15.3 ISP图像处理:从RAW到YUV

ISP(图像信号处理器)是摄像头驱动的灵魂。它负责把传感器输出的RAW数据,转换成我们肉眼能看的YUV或RGB图像。

ISP的处理流水线,我习惯把它画成下面这张图:

ISP图像处理流水线 RAW输入 黑电平校正 去马赛克 白平衡 颜色校正 Gamma校正 YUV输出 预览/编码 ISP处理流水线:从传感器RAW数据到最终可显示的YUV图像

每个ISP模块都有对应的参数需要调节。比如黑电平校正,需要根据传感器的暗电流特性设置偏移量。白平衡则需要根据当前光照环境动态调整R/G/B增益。

我在项目中遇到过最头疼的问题,就是ISP参数调优。同一颗传感器,装在A车上效果很好,换到B车上就偏色。后来发现是B车的挡风玻璃贴了隔热膜,影响了红外截止滤镜的效果。最后我们不得不在ISP里加了一个场景自适应白平衡算法

注意: ISP参数不是调一次就完事的。温度变化、光照变化、甚至摄像头模组的个体差异,都会影响图像质量。建议在驱动中实现一个 自动校准机制,定期检查图像质量并调整参数。

15.4 摄像头标定:让图像更真实

摄像头标定,说白了就是让摄像头知道「这个世界是什么样的」。它包括两部分:

  • 几何标定:矫正镜头畸变,确定焦距、主点位置
  • 颜色标定:矫正颜色偏差,让图像颜色更真实

几何标定通常用棋盘格。你拿着棋盘格在摄像头前摆各种姿势,然后算法会计算出镜头的畸变参数。我建议至少拍20张不同角度的棋盘格照片,覆盖画面的各个区域。

颜色标定则用色卡。比如X-Rite的ColorChecker,上面有24个标准色块。通过对比摄像头拍到的颜色和标准颜色,可以计算出颜色校正矩阵。

标定类型 工具 输出参数 我的经验
几何标定 棋盘格(如9x6) 焦距、畸变系数、主点 棋盘格要平整,不要有褶皱
颜色标定 色卡(如ColorChecker) 颜色校正矩阵、Gamma表 光照要均匀,避免反光
白平衡标定 灰卡 R/G/B增益 不同色温下都要标定

标定参数怎么存?我习惯把它们放在一个JSON文件里,驱动启动时加载。这样换摄像头模组时,只需要换标定文件,不用重新编译驱动。

// 标定参数JSON示例
{
    "camera_id": "OV5640_001",
    "calibration_date": "2024-03-15",
    "geometric": {
        "fx": 1234.56,
        "fy": 1235.78,
        "cx": 640.0,
        "cy": 480.0,
        "distortion": [0.12, -0.34, 0.01, 0.02, 0.05]
    },
    "color": {
        "ccm": [
            [1.02, -0.03, 0.01],
            [-0.02, 1.05, -0.03],
            [0.01, -0.04, 1.03]
        ],
        "gamma": [0, 0.1, 0.2, ...]
    }
}

15.5 预览流控制:从驱动到屏幕

预览流控制,是摄像头驱动里最直观的部分。用户按下「预览」按钮,摄像头开始采集数据,经过ISP处理,最终显示在屏幕上。

在QNX下,预览流的典型流程是:

  1. 申请缓冲区:从驱动层申请一组DMA缓冲区
  2. 启动采集:配置MIPI接口,开始接收传感器数据
  3. ISP处理:每个帧到达后,经过ISP流水线处理
  4. 回调通知:处理完成后,通知应用层取走数据
  5. 显示:应用层把数据送到显示驱动,最终显示在屏幕上

这里有个性能关键点——缓冲区管理。我建议使用双缓冲或三缓冲机制。双缓冲时,一个缓冲区用于ISP处理,另一个用于显示。三缓冲则多一个缓冲区用于采集,可以更好地应对帧率波动。

性能优化技巧: 如果预览帧率上不去,先检查MIPI的时钟配置。很多时候不是ISP不够快,而是MIPI的时钟没配好,导致数据传输带宽不足。

在Android HAL3中,预览流控制是通过 process_capture_request() 实现的。Framework会下发一个请求,里面包含了输出缓冲区的信息。你需要把ISP处理完的数据填到这些缓冲区里。

// HAL3 process_capture_request 实现片段
int my_camera_process_capture_request(
    const struct camera3_device *device,
    camera3_capture_request_t *request)
{
    // 获取输出缓冲区
    const camera3_stream_buffer_t *output_buffers = request->output_buffers;
    uint32_t num_buffers = request->num_output_buffers;

    // 遍历所有输出缓冲区
    for (uint32_t i = 0; i < num_buffers; i++) {
        camera3_stream_buffer_t *buf = &output_buffers[i];

        // 等待ISP处理完成
        wait_for_isp_done();

        // 把ISP输出数据拷贝到缓冲区
        memcpy(buf->buffer, isp_output, buf->stream->width *
               buf->stream->height * 3 / 2);  // YUV420格式

        // 标记缓冲区为已填充
        buf->status = CAMERA3_BUFFER_STATUS_OK;
    }

    // 通知Framework处理完成
    camera3_callback_ops->process_capture_result(device, request);

    return 0;
}

预览流控制还有一个容易被忽略的点——帧同步。在多摄像头系统中,比如环视系统,需要保证所有摄像头的帧是同步的。我建议使用硬件触发信号,让所有摄像头同时开始采集。软件同步的话,误差会累积,时间长了画面就对不齐了。

好了,摄像头驱动这块的内容就这些。从QNX的底层框架,到Android的HAL3实现,再到ISP处理和标定,每一步都有它的门道。实际开发中,你可能会遇到各种各样的问题——图像偏色、帧率不稳、标定不准。别急,一个一个排查,总能找到根因。


公众号:蓝海资料掘金营,微信deep3321