4. 色彩校正模块RTL实现:白平衡、Gamma校正与色彩矩阵变换

好,咱们进入正题。色彩校正这个模块,说白了就是让摄像头看到的世界,跟人眼看到的一样真实。我在项目中调试过不少ISP pipeline,发现很多工程师容易把这三个子模块混在一起调,结果越调越乱。其实它们分工很明确:白平衡负责“色温”,Gamma负责“亮度感知”,色彩矩阵负责“颜色还原”。

今天我就带你把这三大块RTL实现拆开揉碎,一个一个讲清楚。

核心思路: 白平衡是乘系数,Gamma是查表映射,色彩矩阵是乘加运算。三个模块串联起来,就是一条完整的色彩校正流水线。

4.1 白平衡模块RTL设计

白平衡要解决什么问题?你想想看,同一张白纸,在日光灯下偏蓝,在白炽灯下偏黄。人眼会自动校正,但传感器不会。所以我们要给R、G、B三个通道分别乘上不同的增益系数,让白色区域恢复成纯白。

我个人习惯把白平衡模块分成两步:增益计算和增益应用。增益计算通常由软件完成,RTL只负责应用。但有些场景下,RTL也需要做简单的统计,比如计算当前帧的R/G、B/G比值。

4.1.1 模块接口与架构

信号名 方向 位宽 说明
clk 输入 1 像素时钟
rst_n 输入 1 异步复位,低有效
pixel_in 输入 30 输入像素,每通道10bit
gain_r 输入 16 R通道增益,Q8.8格式
gain_g 输入 16 G通道增益,Q8.8格式
gain_b 输入 16 B通道增益,Q8.8格式
pixel_out 输出 30 校正后像素
valid 输出 1 数据有效标志

这里要注意,增益系数我用的是Q8.8定点格式。为什么不用浮点?因为硬件实现定点乘法比浮点简单太多,面积和功耗都小一个量级。

4.1.2 核心代码实现

module white_balance (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [29:0] pixel_in,   // {R[9:0], G[9:0], B[9:0]}
    input  wire [15:0] gain_r,     // Q8.8
    input  wire [15:0] gain_g,     // Q8.8
    input  wire [15:0] gain_b,     // Q8.8
    output reg  [29:0] pixel_out,
    output reg         valid
);

    // 拆包
    wire [9:0] r_in = pixel_in[29:20];
    wire [9:0] g_in = pixel_in[19:10];
    wire [9:0] b_in = pixel_in[ 9: 0];

    // 乘法结果,18bit = 10bit * 8bit(Q8.8的整数部分)
    reg [17:0] r_mult, g_mult, b_mult;

    // 截断到10bit
    reg [9:0] r_out, g_out, b_out;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            r_mult <= 0;
            g_mult <= 0;
            b_mult <= 0;
            pixel_out <= 0;
            valid <= 0;
        end else begin
            // 第一级流水:乘法
            r_mult <= r_in * gain_r[15:8];  // 取整数部分
            g_mult <= g_in * gain_g[15:8];
            b_mult <= b_in * gain_b[15:8];

            // 第二级流水:截断与输出
            r_out <= (r_mult[17:8] > 10'h3FF) ? 10'h3FF : r_mult[17:8];
            g_out <= (g_mult[17:8] > 10'h3FF) ? 10'h3FF : g_mult[17:8];
            b_out <= (b_mult[17:8] > 10'h3FF) ? 10'h3FF : b_mult[17:8];

            pixel_out <= {r_out, g_out, b_out};
            valid <= 1'b1;
        end
    end

endmodule
我曾经踩过的坑: 乘法结果直接截断,没有做饱和处理。结果高亮区域出现颜色反转,白平衡越调越奇怪。记住,任何乘法后都要做饱和截断,防止溢出导致数据翻转。

4.2 Gamma校正模块RTL设计

Gamma校正,说白了就是把人眼对亮度的非线性感知,映射到线性空间。人眼对暗部细节更敏感,所以我们要把暗部拉开,亮部压缩。标准Gamma值一般是2.2,但不同显示设备可能不一样。

RTL实现Gamma校正,最直接的方法就是查表法。提前算好输入到输出的映射关系,存到ROM里,输入像素直接当地址去查。

4.2.1 查表法实现

10bit输入对应1024个地址,每个地址存10bit输出,总共也就1Kb的ROM。这点面积,对于现代芯片来说几乎可以忽略不计。

module gamma_correction (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [9:0]  pixel_in,
    output reg  [9:0]  pixel_out,
    output reg         valid
);

    // Gamma LUT,这里用case语句模拟ROM
    // 实际项目中会例化同步RAM或ROM
    reg [9:0] lut [0:1023];

    initial begin
        // 预计算Gamma 2.2映射表
        for (int i = 0; i < 1024; i++) begin
            lut[i] = $rtoi(1023 * (($itor(i) / 1023.0) ** (1.0/2.2)));
        end
    end

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            pixel_out <= 0;
            valid <= 0;
        end else begin
            pixel_out <= lut[pixel_in];
            valid <= 1'b1;
        end
    end

endmodule
小技巧: 如果芯片面积紧张,可以用分段线性插值代替全表。把1024个点压缩成32段,每段用斜率+截距计算。面积能省80%,精度损失肉眼几乎看不出来。

4.2.2 流水线设计

Gamma校正本身只有一级查表,但放在整个色彩校正流水线里,要注意时序。我一般会在Gamma前后各加一级寄存器,把关键路径打断。

// 三级流水线示例
always @(posedge clk) begin
    stage1 <= pixel_in;      // 输入寄存
    stage2 <= lut[stage1];   // 查表
    pixel_out <= stage2;     // 输出寄存
end

嗯,这里要注意,如果查表用的同步RAM,读操作本身就要一个时钟周期,所以实际是四级流水。设计时一定要看综合报告,别想当然。

4.3 色彩矩阵变换模块RTL设计

色彩矩阵变换,就是把RGB三个通道通过一个3x3矩阵,重新组合成新的RGB。为什么要这么做?因为传感器的色彩滤镜和真实人眼感知有偏差,需要通过矩阵来校正。

公式很简单:

R_out = M00 * R_in + M01 * G_in + M02 * B_in
G_out = M10 * R_in + M11 * G_in + M12 * B_in
B_out = M20 * R_in + M21 * G_in + M22 * B_in

每个系数都是定点数,我习惯用Q1.8格式,范围在-2到+2之间,精度0.0039,足够用了。

4.3.1 乘加树结构

三个通道同时计算,需要9个乘法器和6个加法器。如果直接写,综合器会帮你优化,但手动安排好流水线,能跑更高频率。

module color_matrix (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [9:0]  r_in, g_in, b_in,
    input  wire [8:0]  coeff [0:8],  // 9个系数,Q1.8格式
    output reg  [9:0]  r_out, g_out, b_out,
    output reg         valid
);

    // 第一级:乘法
    reg [17:0] m00, m01, m02;
    reg [17:0] m10, m11, m12;
    reg [17:0] m20, m21, m22;

    // 第二级:加法
    reg [18:0] r_sum, g_sum, b_sum;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            {m00, m01, m02} <= 0;
            {m10, m11, m12} <= 0;
            {m20, m21, m22} <= 0;
            {r_sum, g_sum, b_sum} <= 0;
            {r_out, g_out, b_out} <= 0;
            valid <= 0;
        end else begin
            // 第一级流水
            m00 <= r_in * coeff[0];
            m01 <= g_in * coeff[1];
            m02 <= b_in * coeff[2];
            m10 <= r_in * coeff[3];
            m11 <= g_in * coeff[4];
            m12 <= b_in * coeff[5];
            m20 <= r_in * coeff[6];
            m21 <= g_in * coeff[7];
            m22 <= b_in * coeff[8];

            // 第二级流水:加法 + 饱和截断
            r_sum <= {1'b0, m00} + {1'b0, m01} + {1'b0, m02};
            g_sum <= {1'b0, m10} + {1'b0, m11} + {1'b0, m12};
            b_sum <= {1'b0, m20} + {1'b0, m21} + {1'b0, m22};

            // 第三级流水:截断到10bit
            r_out <= (r_sum[18:8] > 10'h3FF) ? 10'h3FF : 
                     (r_sum[18] ? 10'h000 : r_sum[17:8]);
            g_out <= (g_sum[18:8] > 10'h3FF) ? 10'h3FF : 
                     (g_sum[18] ? 10'h000 : g_sum[17:8]);
            b_out <= (b_sum[18:8] > 10'h3FF) ? 10'h3FF : 
                     (b_sum[18] ? 10'h000 : b_sum[17:8]);

            valid <= 1'b1;
        end
    end

endmodule
注意符号位: 系数可能是负数,所以乘法器要用有符号乘法。Verilog里用$signed()做类型转换,否则负数会变成很大的正数,结果全错。

4.4 整体流水线整合

三个模块串起来,就是一条三级流水线:白平衡 → Gamma校正 → 色彩矩阵变换。每个模块内部还有2-3级流水,整体延迟大概8-10个时钟周期。

我个人习惯在模块之间加valid-ready握手信号,这样每个模块可以独立暂停,方便调试。你想想看,如果某个模块需要多等一拍,没有握手信号,整条流水线就乱了。

色彩校正模块流水线架构 RAW像素 白平衡 R×Gain_R G×Gain_G / B×Gain_B Gamma校正 LUT查表 γ=2.2 色彩矩阵 3×3乘加 M×RGB 校正后RGB 流水线总延迟:8~10个时钟周期 每级内部2~3级寄存器,模块间valid-ready握手

4.5 设计要点总结

  • 定点数格式统一: 白平衡用Q8.8,Gamma用整数查表,色彩矩阵用Q1.8。混用时要做好位宽匹配,否则数据对不上。
  • 饱和截断是必须的: 任何乘加运算后都要做饱和处理,防止溢出。我见过有人偷懒直接截断,结果高亮区域出现彩色条纹。
  • 流水线深度要平衡: 三个模块的延迟尽量一致,否则需要插入FIFO做同步。我个人习惯让每个模块延迟相同,省去同步逻辑。
  • 系数更新要平滑: 白平衡增益和矩阵系数不要帧间突变,否则画面会闪烁。可以在帧消隐期更新,或者做渐变切换。
调试经验: 仿真时先给白平衡系数全设为1.0,Gamma用线性表,矩阵设为单位矩阵。确认通路没问题后,再逐个模块使能。这样定位问题快很多。

好了,色彩校正的RTL实现就讲到这里。这三个模块单独看都不复杂,但组合在一起,要考虑的细节就多了。尤其是时序收敛和面积优化,需要根据具体工艺库做调整。下次遇到色彩偏色的问题,你知道该查哪个模块了吧?