第3章 传感器与模组:CMOS传感器工作原理、Sensor驱动基础、模组标定简介
好,我们进入第三章。前面两章我们把ISP的全局架构和色彩基础捋了一遍。从这章开始,要真正碰硬件了——传感器。说白了,没有传感器这个“眼睛”,后面算法再牛也是白搭。
我个人习惯,讲传感器之前,先让大家明白一个事儿:ISP处理的是数字信号,但传感器输出的是模拟信号。这个转换过程,就是本章的核心。
3.1 CMOS传感器工作原理
现在市面上99%的手机、相机、车载摄像头,用的都是CMOS传感器。CCD基本退出了消费市场,只在一些工业、科研领域还能见到。
CMOS怎么工作的?我尽量用大白话讲。
每个像素点,本质上就是一个光电二极管。光子打进来,产生电子-空穴对。电子被收集,形成电压。这个电压大小,和光照强度成正比。
但这里有个关键问题:怎么把每个像素的电压读出来?
CMOS用的是“行选+列选”的寻址方式。每一行有一个行选信号,每一列有一个列选信号。要读某个像素,就打开它所在的行和列,把电压送到列总线上去。
嗯,这里要注意:同一行的像素是共享一条行选线的,同一列的像素是共享一条列总线的。所以读出的时候,是一行一行地读。
核心流程:
- 光子入射 → 光电转换 → 电子积累
- 行选通 → 像素复位 → 读出复位电压
- 曝光结束 → 读出信号电压
- 相关双采样(CDS) → 消除复位噪声
- 模数转换(ADC) → 输出数字码值
我在项目中遇到过一个问题:某款传感器在低光下噪点特别多。查了半天,发现是CDS时序没配好。复位电压和信号电压的采样点间隔太短,噪声没消干净。后来把时序调了一下,信噪比直接提升了3dB。
3.2 Sensor驱动基础
传感器不是上电就能用的。你得通过I2C或SPI总线,给它写寄存器,配置工作模式。
驱动要做哪些事?我列个清单:
| 配置项 | 说明 | 典型值 |
|---|---|---|
| PLL配置 | 设置内部时钟频率 | 根据sensor datasheet计算 |
| 曝光时间 | 控制每帧的积分时间 | 1/30s ~ 1/10000s |
| 增益 | 模拟增益和数字增益 | 1x ~ 16x |
| 帧率 | 每秒输出多少帧 | 30fps / 60fps |
| 输出格式 | RAW8 / RAW10 / RAW12 | RAW10最常见 |
| 窗口大小 | 有效像素区域 | 1920x1080 |
| 镜像/翻转 | 水平/垂直翻转 | 根据模组安装方向 |
驱动初始化的一般步骤:
- 上电,等待电源稳定(一般需要1-5ms)
- 拉高复位引脚,等待传感器内部初始化完成
- 通过I2C写入PLL配置,设定主时钟
- 配置输出格式、分辨率、帧率
- 配置曝光和增益的初始值
- 拉高STANDBY引脚,让传感器开始输出数据
- 等待VSYNC信号,确认第一帧到来
给你看一段简化的驱动代码,配置一个OV5640传感器:
// OV5640 初始化序列(简化版)
static const struct regval_list ov5640_init_regs[] = {
{0x3103, 0x11}, // 系统时钟使能
{0x3008, 0x82}, // 软件复位
{0x3008, 0x42}, // 退出复位
{0x3103, 0x03}, // 时钟分频配置
{0x3034, 0x1A}, // PLL配置
{0x3035, 0x21}, // PLL倍频
{0x3036, 0x46}, // PLL分频
{0x3037, 0x08}, // 输出时钟分频
{0x3808, 0x07}, // 输出宽度高8位
{0x3809, 0x80}, // 输出宽度低8位 (1920)
{0x380A, 0x04}, // 输出高度高8位
{0x380B, 0x38}, // 输出高度低8位 (1080)
{0x3630, 0x36}, // 模拟电路配置
{0x3631, 0x0E},
{0x3632, 0xE2},
{0x3633, 0x23},
{0x3621, 0xE0},
{0x3704, 0xA0},
{0x3703, 0x5A},
{0x3715, 0x78},
{0x3717, 0x01},
{0x3731, 0x12},
{0x3708, 0x64},
{0x3705, 0x1A},
{0x3A02, 0x03}, // AEC配置
{0x3A03, 0xD8},
{0x3A08, 0x01},
{0x3A09, 0x27},
{0x3A0A, 0x00},
{0x3A0B, 0xF6},
{0x3A0E, 0x03},
{0x3A0D, 0x04},
{0x5180, 0xFF}, // AWB配置
{0x5181, 0xF2},
{0x5182, 0x00},
{0x5183, 0x14},
{0x5184, 0x25},
{0x5185, 0x24},
{0x5186, 0x09},
{0x5187, 0x09},
{0x5188, 0x09},
{0x5189, 0x75},
{0x518A, 0x54},
{0x518B, 0xE0},
{0x518C, 0xB2},
{0x518D, 0x42},
{0x518E, 0x3D},
{0x518F, 0x56},
{0x5190, 0x46},
{0x5191, 0xF8},
{0x5192, 0x04},
{0x5193, 0x70},
{0x5194, 0xF0},
{0x5195, 0xF0},
{0x5196, 0x03},
{0x5197, 0x01},
{0x5198, 0x04},
{0x5199, 0x12},
{0x519A, 0x04},
{0x519B, 0x00},
{0x519C, 0x06},
{0x519D, 0x82},
{0x519E, 0x38},
{0x5381, 0x1E}, // 色彩矩阵配置
{0x5382, 0x5B},
{0x5383, 0x08},
{0x5384, 0x0A},
{0x5385, 0x7E},
{0x5386, 0x88},
{0x5387, 0x7C},
{0x5388, 0x6C},
{0x5389, 0x10},
{0x538A, 0x01},
{0x538B, 0x98},
{0x5300, 0x08}, // 降噪配置
{0x5301, 0x30},
{0x5302, 0x10},
{0x5303, 0x00},
{0x5304, 0x08},
{0x5305, 0x30},
{0x5306, 0x08},
{0x5307, 0x16},
{0x5309, 0x08},
{0x530A, 0x30},
{0x530B, 0x04},
{0x530C, 0x06},
{0x3008, 0x02}, // 启动输出
};
这段代码看着长,其实逻辑很简单:就是往寄存器里写一堆值。但每个值什么意思,得对着datasheet看。我刚开始做驱动时,最头疼的就是datasheet里那些“Reserved”位——写错了传感器就不干活。
避坑指南:
我曾经因为I2C速率没配好,导致传感器初始化失败。传感器对I2C时序有要求,一般不超过400kHz。有些传感器还要求SCL高电平时间大于某个值。如果初始化失败,先查I2C波形,别急着怀疑寄存器写错了。
3.3 模组标定简介
传感器本身有差异,镜头有差异,组装也有差异。所以每个模组出厂前,都得标定。
标定做什么?说白了,就是测量模组的各种“缺陷”,然后算出补偿参数。这些参数会烧录到模组的OTP(一次性可编程)存储器里,或者存在系统端的校准文件中。
常见的标定项目:
| 标定项目 | 测量内容 | 补偿方式 |
|---|---|---|
| 暗电流标定 | 无光照时的像素输出 | 减去暗电流值 |
| 镜头阴影标定 | 四角亮度衰减 | LSC增益补偿 |
| 坏点标定 | 死点、亮点、热噪点 | 坏点替换/插值 |
| 白平衡标定 | R/G/B通道增益比 | AWB增益矩阵 |
| 色彩标定 | 色彩还原准确性 | CCM色彩校正矩阵 |
标定流程一般是这样的:
- 搭建标定环境(积分球、标准光源、色卡)
- 采集多帧RAW图
- 计算各像素的响应特性
- 生成补偿参数
- 验证标定效果
我印象最深的一次,是某个项目在量产时发现,同一批模组的暗电流差异很大。查来查去,发现是模组厂在组装时,传感器和镜头的间距有微小偏差,导致进光量不一致。后来在标定流程里加了一个“暗电流归一化”步骤,问题才解决。
小技巧:
标定环境很重要。积分球的均匀性、光源的色温稳定性、暗室的漏光情况,都会影响标定精度。我建议标定前先测一下环境,确保光源色温偏差在±50K以内,照度均匀性在95%以上。
下面这张图,展示了传感器到ISP的完整数据链路:
这张图把整个链路串起来了。你想想看,从光子打到像素上,到ISP拿到RAW图,中间经历了多少环节?每个环节都可能引入噪声和失真。标定,就是把这些“不完美”给量化出来,然后补偿掉。