014、曝光时间与增益联动控制:AE 算法到 Sensor Register 的映射实现
014、曝光时间与增益联动控制:AE 算法到 Sensor Register 的映射实现
一、一个让我熬夜三天的 Bug
去年做某款 50M 主摄的 AE 调试,客户反馈暗光下预览画面忽明忽暗,像呼吸灯一样闪烁。抓了 log 看,AE 算法算出的 target exposure 在 30ms 到 60ms 之间来回跳,但 sensor 的 register 读回来却显示 exposure 根本没变——gain 倒是跟着算法在变。这就怪了,算法明明发了新参数,sensor 为什么没执行?
查了两天,最后发现是 AE 算法输出的 exposure time 和 gain 组合,在映射到 sensor register 时,被驱动层的一个 rounding 逻辑给“吞”了。具体来说,sensor 的 exposure line 单位是 line count,而算法用的是毫秒。当 line time 是 20us 时,30ms 对应 1500 lines,60ms 对应 3000 lines。但驱动里有个限制:exposure line 必须是 4 的倍数(某些 sensor 的硬件限制)。1500 不是 4 的倍数,被 round down 到 1496,而 3000 是 4 的倍数,直接写进去了。结果就是:算法以为曝光时间变了,实际 sensor 只变了 4 lines(约 80us)的差异,gain 却跟着算法大幅调整,画面自然闪烁。
这个坑让我意识到:AE 算法和 sensor 硬件之间的映射,不是简单的乘除关系,而是一个需要精心设计的联动控制逻辑。今天就把这块的实战经验掰开揉碎讲清楚。
二、AE 算法的“理想国”与 Sensor 的“现实墙”
AE 算法通常工作在“理想世界”里——它假设曝光时间可以连续变化,增益可以任意取值,两者组合可以精确达到目标亮度。但 sensor 硬件是“现实世界”的产物,有各种限制:
- 曝光时间粒度:sensor 的 exposure 通常以 line 为单位,line time 由 PCLK 和帧率决定。比如 30fps 下 line time 约 33.33us,那么 exposure 只能以 33.33us 的整数倍变化。算法说“我要 45.5ms”,sensor 只能给 45.33ms 或 45.66ms。
- 增益步长:模拟增益(AGC)通常有 0.1dB 或 0.125dB 的步长,数字增益(DGC)步长更细但会引入噪声。算法说“我要 2.37x”,sensor 可能只能给 2.375x 或 2.3125x。
- 寄存器写入时序:exposure 和 gain 的寄存器写入有严格的 timing 要求——必须在 VBlank 期间写入,否则会导致 frame 撕裂。有些 sensor 甚至要求 exposure 和 gain 必须在同一帧内同时更新,否则画面会闪。
这些限制意味着:AE 算法输出的 (exposure_time, gain) 对,必须经过一个“映射层”才能变成 sensor 能理解的 (line_count, gain_code) 对。这个映射层如果设计不好,轻则精度损失,重则画面闪烁、帧率不稳。
三、映射层的核心设计:从“浮点”到“整数”的战争
3.1 曝光时间的映射:别用浮点除法
很多新手会这样写:
// 别这样写!浮点除法慢,还容易精度问题floatline_time_us=33.33f;uint16_texposure_lines=(uint16_t)(exposure_time_ms*1000.0f/line_time_us+0.5f);这里踩过坑:浮点除法在嵌入式环境里很慢,而且 line_time_us 本身可能是个近似值(比如 33.33 其实是 1000/30 的近似)。更致命的是,当 exposure_time_ms 很大时,浮点精度不够会导致 rounding 错误。
正确的做法是用定点数:
// 推荐:用 Q16.16 定点数,或者直接用整数运算// 假设 line_time_us 是 33.33,我们把它放大 100 倍变成 3333// exposure_time_ms 也放大 100 倍变成微秒uint32_tline_time_x100=3333;// 33.33us * 100uint32_texposure_us=exposure_time_ms*1000;uint16_texposure_lines=(exposure_us*100+line_time_x100/2)/line_time_x100;// 注意:这里 *100 是为了对齐 line_time_x100 的精度但这样还不够,因为 sensor 可能要求 exposure_lines 是某个对齐值(比如 4 的倍数)。所以还要加一个对齐函数:
// 对齐到 4 的倍数,向上取整(宁可多曝光一点,别少曝光)uint16_talign_exposure_lines(uint16_tlines,uint16_talignment){return(lines+alignment-1)&~(alignment-1);}为什么向上取整?因为如果向下取整,实际曝光时间比算法要求的短,画面会偏暗,AE 会继续增加 gain,导致噪声放大。向上取整虽然可能过曝一点点,但 AE 的闭环控制会很快收敛回来。
3.2 增益的映射:模拟 vs 数字的博弈
增益映射比曝光时间更复杂,因为涉及模拟增益和数字增益的分配策略。常见的策略有:
- 先模拟后数字:优先用模拟增益,模拟增益不够了再用数字增益。这是最常用的策略,因为模拟增益噪声小。
- 混合策略:在某个阈值内只用模拟,超过阈值后模拟和数字按比例分配。比如模拟增益到 4x 后,再增加增益时模拟和数字各承担一半。
映射函数的核心是查表,因为 sensor 的增益码和实际增益值通常不是线性关系。比如某 sensor 的模拟增益码 0x00 对应 1x,0x01 对应 1.125x,0x02 对应 1.25x……这种非线性关系必须用 lookup table。
// 增益码到实际增益的查找表(假设有 16 个档位)constfloatgain_table[16]={1.0f,1.125f,1.25f,1.5f,1.75f,2.0f,2.5f,3.0f,3.5f,4.0f,5.0f,6.0f,8.0f,10.0f,12.0f,16.0f};// 从目标增益找最近的增益码uint8_tfind_gain_code(floattarget_gain){// 这里踩过坑:直接用二分查找,但要注意边界uint8_tlow=0,high=15;while(low<high){uint8_tmid=(low+high)/2;if(gain_table[mid]<target_gain){low=mid+1;}else{high=mid;}}// 返回最接近的码值,但要注意:如果 target_gain 比 table[0] 还小,返回 0// 如果比 table[15] 还大,返回 15(但这种情况应该由 AE 算法避免)returnlow;}但这里有个细节:增益码的查找不能只看绝对值,还要考虑步长。比如 target_gain 是 1.2x,table 里 1.125x 和 1.25x 的误差分别是 0.075 和 0.05,按理说应该选 1.25x。但如果你知道 sensor 的模拟增益在 1.25x 时噪声比 1.125x 大很多,那可能宁愿选 1.125x 然后让数字增益补一点。这就是经验了。
3.3 联动控制:曝光时间和增益的“跷跷板”
AE 算法通常输出一个 target exposure(单位:lux·sec),然后由映射层决定如何拆分成 exposure time 和 gain。但映射层有更大的自由度——它可以在一定范围内调整两者的比例,只要总 exposure 不变。
比如 target exposure 是 0.1 lux·sec,可以拆成:
- 方案 A:exposure time = 50ms, gain = 2x
- 方案 B:exposure time = 100ms, gain = 1x
方案 A 帧率高但噪声大,方案 B 帧率低但画质好。映射层需要根据当前场景(运动/静止)和用户设置(优先画质/优先帧率)来做决策。
这个决策逻辑通常放在 AE 算法和映射层之间,我称之为“策略层”。策略层会输出一个“偏好曝光时间”和“偏好增益范围”,映射层在这个范围内做精确映射。
// 策略层输出的偏好参数typedefstruct{uint16_tpref_exposure_min;// 偏好最小曝光时间(ms)uint16_tpref_exposure_max;// 偏好最大曝光时间(ms)floatpref_gain_min;// 偏好最小增益floatpref_gain_max;// 偏好最大增益}ae_strategy_t;// 映射层根据策略和 target exposure 计算实际参数voidae_map_target(floattarget_exposure,ae_strategy_t*strategy,uint16_t*out_exposure_lines,uint8_t*out_gain_code){// 先尝试用偏好范围内的最大曝光时间uint16_texposure_ms=strategy->pref_exposure_max;floatrequired_gain=target_exposure/(exposure_ms/1000.0f);// 如果增益超出偏好范围,减少曝光时间while(required_gain>strategy->pref_gain_max&&exposure_ms>strategy->pref_exposure_min){exposure_ms-=10;// 步长 10ms,可以更精细required_gain=target_exposure/(exposure_ms/1000.0f);}// 如果增益还是太大,说明 target exposure 太高,需要 AE 算法重新调整if(required_gain>strategy->pref_gain_max){// 这里踩过坑:直接 clamp gain 会导致画面亮度不够// 正确做法是通知 AE 算法降低 target exposurerequired_gain=strategy->pref_gain_max;}// 最后做精确映射*out_exposure_lines=exposure_ms_to_lines(exposure_ms);*out_gain_code=gain_to_code(required_gain);}四、寄存器写入的时序陷阱
映射完参数后,最后一步是写入 sensor 寄存器。这里有个常见的坑:exposure 和 gain 的写入顺序和时机。
很多 sensor 要求 exposure 和 gain 必须在同一帧的 VBlank 期间写入,而且写入顺序有讲究。比如某 sensor 的 datasheet 写的是:先写 gain,再写 exposure,否则 exposure 会使用旧的 gain 值计算。这种细节 datasheet 里往往藏在某个角落,不仔细看就漏了。
更坑的是,有些 sensor 的 exposure 寄存器是 double buffered——写入后不会立即生效,而是等到下一帧的 VBlank 才 latch。而 gain 寄存器是 immediate 生效的。如果你先写 gain 再写 exposure,gain 会立即改变当前帧的亮度,导致当前帧出现亮度跳变。
正确的做法是:在 VBlank 中断里,先写 exposure(因为它有 double buffer,不会立即生效),再写 gain(让它和下一帧的 exposure 同步生效)。或者更保险的做法是:在 VBlank 开始前准备好所有参数,VBlank 期间一次性写入。
// VBlank 中断处理函数voidvsync_isr(void){// 这里踩过坑:如果 exposure 和 gain 分开写,中间被其他中断打断就完了// 所以要用原子操作,或者关中断__disable_irq();// 先写 exposure(double buffered,不会立即生效)i2c_write_sensor(REG_EXPOSURE_H,(new_exposure>>8)&0xFF);i2c_write_sensor(REG_EXPOSURE_L,new_exposure&0xFF);// 再写 gain(立即生效,但会和下一帧的 exposure 同步)i2c_write_sensor(REG_GAIN,new_gain);__enable_irq();}五、实战中的“暗坑”与经验
5.1 曝光时间与帧率的矛盾
当 exposure time 接近 frame time 时,sensor 的 VBlank 会变得很小,导致没有足够时间写入寄存器。比如 30fps 下 frame time 是 33.33ms,如果 exposure time 设成 32ms,VBlank 只有 1.33ms。如果 I2C 速度是 400kHz,写两个寄存器需要约 50us,看起来够用。但如果你还要写其他寄存器(比如 gain、白平衡、LSC 等),时间就不够了。
解决方案:在 AE 策略层限制最大 exposure time 为 frame time 的 90%,留出足够的 VBlank 时间。或者使用更快的 I2C 速度(1MHz)和 DMA 传输。
5.2 增益切换的“咔嗒”声
某些 sensor 在切换增益时,如果增益码变化太大,会导致画面出现瞬间的亮度跳变,看起来像“咔嗒”一声。这是因为 sensor 内部的 PGA(可编程增益放大器)在切换时有一个 settling time。
解决方案:增益变化做平滑处理。比如当前增益码是 0x05,目标增益码是 0x0A,不要一步到位,而是分 3-5 步,每帧增加 1-2 个码值。这样虽然收敛慢一点,但画面过渡自然。
5.3 不同 sensor 的“脾气”
每个 sensor 的映射逻辑都不一样。比如 Sony IMX 系列喜欢用 line count 做 exposure,而 Samsung ISOCELL 系列喜欢用微秒。有些 sensor 的 gain 码是 10-bit,有些是 12-bit。甚至同一个 sensor 的不同模式(比如 4:3 和 16:9)下,line time 都可能不同。
我的做法是:为每个 sensor 写一个独立的映射层,把 sensor 相关的参数(line time、gain table、对齐要求、写入时序)封装成一个结构体,通过回调函数注入到通用 AE 框架中。这样换 sensor 时只需要改参数,不用改逻辑。
六、个人经验性建议
永远不要相信 AE 算法输出的浮点值。在映射层里,所有计算都用定点数,精度至少到 1/1000。浮点误差在多次迭代后会累积成可见的闪烁。
映射层要有“回退”机制。当 sensor 无法精确匹配算法要求时(比如 exposure 对齐后偏差太大),要能通知 AE 算法重新调整 target。不要自己偷偷 clamp,否则 AE 的闭环控制会失效。
调试时多打 log。把算法输出的 target、映射后的 line count 和 gain code、实际读回的 sensor 值都打出来。我见过太多“算法以为写了,sensor 没收到”的 case。
写一个 sensor 模拟器。在 PC 上模拟 sensor 的寄存器行为,包括 line time、gain table、对齐限制等。这样可以在没有硬件的情况下调试映射逻辑,省去大量烧录时间。
最后,也是最关键的:多看 datasheet 的“Application Notes”部分。sensor 厂商通常会在那里写一些“隐藏”的限制,比如“exposure 和 gain 必须在同一帧写入,且 gain 必须在 exposure 之后写入”。这些细节决定了你的映射层是否稳定。
曝光时间与增益的联动控制,说难不难,说简单也不简单。它不像 AE 算法那样需要复杂的数学模型,但每一个细节都可能成为你调试路上的绊脚石。希望这篇文章能帮你少踩几个坑。
