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的驱动是用户态进程。你想想看,这意味着什么?驱动崩了不会导致整个系统挂掉。我在项目中遇到过好几次,摄像头驱动在调试阶段频繁崩溃,但中控屏的导航和音乐播放完全不受影响。这在安全关键系统里太重要了。
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模块都有对应的参数需要调节。比如黑电平校正,需要根据传感器的暗电流特性设置偏移量。白平衡则需要根据当前光照环境动态调整R/G/B增益。
我在项目中遇到过最头疼的问题,就是ISP参数调优。同一颗传感器,装在A车上效果很好,换到B车上就偏色。后来发现是B车的挡风玻璃贴了隔热膜,影响了红外截止滤镜的效果。最后我们不得不在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下,预览流的典型流程是:
- 申请缓冲区:从驱动层申请一组DMA缓冲区
- 启动采集:配置MIPI接口,开始接收传感器数据
- ISP处理:每个帧到达后,经过ISP流水线处理
- 回调通知:处理完成后,通知应用层取走数据
- 显示:应用层把数据送到显示驱动,最终显示在屏幕上
这里有个性能关键点——缓冲区管理。我建议使用双缓冲或三缓冲机制。双缓冲时,一个缓冲区用于ISP处理,另一个用于显示。三缓冲则多一个缓冲区用于采集,可以更好地应对帧率波动。
在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