4. 色彩校正模块RTL实现:白平衡、Gamma校正与色彩矩阵变换
好,咱们进入正题。色彩校正这个模块,说白了就是让摄像头看到的世界,跟人眼看到的一样真实。我在项目中调试过不少ISP pipeline,发现很多工程师容易把这三个子模块混在一起调,结果越调越乱。其实它们分工很明确:白平衡负责“色温”,Gamma负责“亮度感知”,色彩矩阵负责“颜色还原”。
今天我就带你把这三大块RTL实现拆开揉碎,一个一个讲清楚。
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
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
4.4 整体流水线整合
三个模块串起来,就是一条三级流水线:白平衡 → Gamma校正 → 色彩矩阵变换。每个模块内部还有2-3级流水,整体延迟大概8-10个时钟周期。
我个人习惯在模块之间加valid-ready握手信号,这样每个模块可以独立暂停,方便调试。你想想看,如果某个模块需要多等一拍,没有握手信号,整条流水线就乱了。
4.5 设计要点总结
- 定点数格式统一: 白平衡用Q8.8,Gamma用整数查表,色彩矩阵用Q1.8。混用时要做好位宽匹配,否则数据对不上。
- 饱和截断是必须的: 任何乘加运算后都要做饱和处理,防止溢出。我见过有人偷懒直接截断,结果高亮区域出现彩色条纹。
- 流水线深度要平衡: 三个模块的延迟尽量一致,否则需要插入FIFO做同步。我个人习惯让每个模块延迟相同,省去同步逻辑。
- 系数更新要平滑: 白平衡增益和矩阵系数不要帧间突变,否则画面会闪烁。可以在帧消隐期更新,或者做渐变切换。
好了,色彩校正的RTL实现就讲到这里。这三个模块单独看都不复杂,但组合在一起,要考虑的细节就多了。尤其是时序收敛和面积优化,需要根据具体工艺库做调整。下次遇到色彩偏色的问题,你知道该查哪个模块了吧?