引言
CMOS 图像传感器存在暗电流和固定模式噪声(FPN),传统模拟域 CDS 电路难以完全抑制,数字域单次暗帧减易受随机噪声干扰。本文利用传感器读出链路前端的暗区参考行,提出一种多帧累加平均 + 乒乓 RAM 双缓冲 的实时暗电流抑制架构。核心思路是:在暗区对每个像素位置独立累加 16 帧暗像素,取平均后生成高精度暗基准;通过前台/后台双 RAM 乒乓切换,使暗基准的后台刷新与有效区读取并行工作,无帧间停顿;最终在有效像素区执行饱和减法输出。以下按照原始 Verilog 代码从上到下的书写顺序,逐步说明各模块的实现逻辑。
设计推导
为什么代码会写成下面这个顺序?每一段逻辑都是在前一段的基础上解决一个新遇到的问题。下面按照从现象到方案、从方案到问题、从问题到再方案的思路,推导出完整的实现步骤。
推导 1:先确定暗行在哪里 → Step 1
RAW 图像传感器输出的数据,前几行是暗像素(暗区参考行),后面才是有效图像。要消除 FPN,第一步必须区分出当前到来的行是暗行还是有效行 ,否则不知道哪些像素该用来生成暗基准、哪些该用来输出。这就是 Step 1 的 dark_phase 状态机。
推导 2:暗行要按像素位置存 RAM → Step 2
确定了"这是暗行"之后,还要把暗像素存进 RAM 里留着用。存 RAM 需要知道地址------存到第几个位置?传感器一行有 204 个像素,需要一个列地址计数器逐个索引过去,这就是 Step 2 的 col_addr。
推导 3:单帧暗行不够干净,要多帧累加 → Step 3 + Step 4
用一帧暗行做 FPN 消除为什么不行?因为单帧里的随机噪声还没被平均掉,暗基准本身就带噪声,消完反而会引入新噪声。解决办法是多帧累加取平均 。那就要知道"现在是第几帧",所以需要一个帧计数器 frame_cnt,以及累加 RAM acc_g1/b/r/g2。累加 16 帧后右移 4 位就是平均值。
推导 4:多帧累加时 RAM 读写会冲突 → Step 5 + Step 6
这是整个设计最关键的问题。数据是源源不断来的:新的一帧暗行正在往累加 RAM 里写,而上一轮累加完的结果又需要读出来取平均、写成新的暗基准。这两个动作同时发生在一个时钟域里,如果用同一组 RAM,地址会打架、数据会冲突。
解决办法是乒乓双缓冲:把 RAM 分成前台和后台两组。当前台在被"读暗基准供有效像素减法"时,后台同时在"写新累加完的暗基准"。16 帧累加完成时,翻转一次开关,前后台角色立刻交换,无缝衔接------没有任何停顿周期。
这就是 Step 5 的 buf_swap_pending 切换标志和 Step 6 的乒乓双 RAM 写入逻辑。
推导 5:有效像素到来时怎么减暗 → Step 7 + Step 8 + Step 9
暗基准存好了,接下来处理有效像素:从 RAM 里读暗基准 → 与原始像素对齐 → 相减输出。
读 RAM 有一个时钟周期的延迟,所以原始像素数据 pre_data 也要打一拍才能和读出来的暗基准 dark_val_r 对齐,这就是 Step 7(读暗基准)+ Step 8(数据打拍)。最后对齐后做饱和减法输出,这就是 Step 9。
饱和减法的额外考量:暗基准是 16 帧平均值,个别像素的瞬时值可能因噪声波动略低于基准值,此时如果直接截断为 0 会有偏差,强制归零更准确。
推导 6:步骤顺序为什么就是代码顺序
把上面的推导串起来,就是一条问题驱动的单向链:
要消 FPN → 先找暗行在哪(Step 1)
→ 暗行存 RAM 要地址(Step 2)
→ 单帧不够干净要多帧累加(Step 3 + Step 4)
→ 多帧累加遇到 RAM 读写冲突 → 乒乓(Step 5 + Step 6)
→ 有效像素来的时候读基准对齐相减输出(Step 7 + Step 8 + Step 9)
原始 Verilog 代码恰好就是按这个推导顺序从上到下写的------先写 dark_phase,再写 col_addr,再写 frame_cnt,再写累加 RAM,再写乒乓切换,最后写输出。不存在任何一步顺序颠倒,因为逻辑上就不允许颠倒:没有 dark_phase 就不知道该不该累加,没有累加结果就写不出暗基准 RAM,没有暗基准 RAM 就没法减法输出。
下面的 Step 1 ~ Step 9 每一步即原始代码中对应那一段原样照搬。
实现步骤
Step 1:判定当前处于暗区还是有效区
利用传感器前端的暗区参考行(前 FPN_REDU=4 行)作为暗像素提取源。复位后默认为暗区;扫描完第 3 行(最后一行暗行)时切换为有效区;扫描到帧末行时回到暗区。每帧循环。
reg dark_phase;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
dark_phase <= 1'b1;
else if((norow == NOR_MAX-1 && unpack_cksum))
dark_phase <= 1'b1;
else if(norow == FPN_REDU-1 && unpack_cksum)
dark_phase <= 1'b0;
end
dark_phase = 1 时当前行是暗像素,仅用于累加,不作为最终输出;
dark_phase = 0 时是有效像素区,需要输出减暗后的图像。
Step 2:生成像素列地址
reg [DATA_WIDTH-1:0] col_addr;
always @(posedge clk) begin
if(!rst_n)
col_addr <= 10'b0;
else if(pre_de)
col_addr <= (col_addr == WR_IW-1'b1) ? 0 : col_addr + 1'b1;
else
col_addr <= 10'b0;
end
col_addr 在 pre_de 有效时从 0 递增到 203(WR_IW-1),pre_de 无效时清零。它作为像素水平索引,驱动后续所有 RAM 的地址端口,实现对每个像素位置的独立处理。
Step 3:帧计数器 + 首帧标志
reg [3:0] frame_cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
frame_cnt <= 4'd0;
else if ((norow == NOR_MAX-1) && unpack_cksum)
frame_cnt <= (frame_cnt == AVG_FRAMES-1) ? 4'd0 : frame_cnt + 1'b1;
end
wire first_frame = (frame_cnt == 4'd0);
每完成一帧(norow == NOR_MAX-1 && unpack_cksum)计数器加 1,范围 0~15,形成 16 帧累加窗口。first_frame 为新一轮累加的首帧标志,用于初始化累加器,避免沿用旧值。
Step 4:四通道独立累加
每个 Bayer 通道各配一组 13 位分布式 RAM,按 norow[1:0] 低两位做通道分离,仅在暗区累加 16 帧数据。这是整个模块的核心。
(* ram_style = "distributed" *) reg [12:0] acc_g1 [WR_IW-1:0];
(* ram_style = "distributed" *) reg [12:0] acc_b [WR_IW-1:0];
(* ram_style = "distributed" *) reg [12:0] acc_r [WR_IW-1:0];
(* ram_style = "distributed" *) reg [12:0] acc_g2 [WR_IW-1:0];
always @(posedge clk) begin
if (dark_phase && pre_de) begin
case (norow[1:0])
2'b00: acc_g1[col_addr] <= first_frame ? {3'd0, pre_data} : acc_g1[col_addr] + pre_data;
2'b01: acc_b [col_addr] <= first_frame ? {3'd0, pre_data} : acc_b [col_addr] + pre_data;
2'b10: acc_r [col_addr] <= first_frame ? {3'd0, pre_data} : acc_r [col_addr] + pre_data;
2'b11: acc_g2[col_addr] <= first_frame ? {3'd0, pre_data} : acc_g2[col_addr] + pre_data;
endcase
end
end
-
通道分离 :
norow[1:0]=00→G1、01→B、10→R、11→G2,四个通道的暗电流特性独立,分开累加避免跨通道误差。 -
首帧处理 :
first_frame时直接赋值{3'd0, pre_data}初始化累加器;后续帧执行acc_x + pre_data累加。 -
13 位宽度:10 位原始数据最大值 1023,16 帧累加最大值 1023×16 =163681023×16=16368,需 14 位;这里取 13 位近似,FPGA 资源可省一个 LUT。
Step 5:乒乓缓冲切换标志
16 帧累加完成时,需要将新基准从后台 RAM 切换到前台,这由 buf_swap_pending 的翻转来协调。紧随累加逻辑之后,为下一步的乒乓写入做准备。
reg buf_swap_pending;
wire frame_end = (frame_cnt == AVG_FRAMES-1)
&& (norow == NOR_MAX-1) && (unpack_cksum);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
buf_swap_pending <= 1'b0;
else if (frame_end && buf_swap_pending == 0)
buf_swap_pending <= 1'b1;
else if (frame_end && buf_swap_pending)
buf_swap_pending <= 1'b0;
end
-
frame_end:16 帧末脉冲,在frame_cnt=15且当前帧末时产生。 -
buf_swap_pending:每次frame_end翻转一次,控制前台/后台 RAM 的读写归属
Step 6:平均值计算与后台 RAM 写入
累加窗口完成后,在第 16 帧的有效区将累加值右移 4 位取平均,写入后台 RAM。这一段包含了 RAM 声明、写入条件和实际写入 always 三块内容。
写入触发信号 buf_sel 与地址遍历计数器 avg_cnt:
wire buf_sel = (frame_cnt == AVG_FRAMES-1) && !dark_phase;
reg [7:0] avg_cnt;
always @(posedge clk) begin
if(!rst_n)
avg_cnt <= 'b0;
else if(first_frame)
avg_cnt <= 'b0;
else if(buf_sel)
avg_cnt <= (avg_cnt == WR_IW) ? avg_cnt : avg_cnt + 1'b1;
else
avg_cnt <= 'b0;
end
乒乓双 RAM 声明(每个通道两组,共 8 组 9 位分布式 RAM):
(* ram_style = "distributed" *) reg [9:0] dark_ram_g1 [WR_IW:0];
(* ram_style = "distributed" *) reg [9:0] dark_ram_b [WR_IW:0];
(* ram_style = "distributed" *) reg [9:0] dark_ram_r [WR_IW:0];
(* ram_style = "distributed" *) reg [9:0] dark_ram_g2 [WR_IW:0];
(* ram_style = "distributed" *) reg [9:0] dark_ram1_g1 [WR_IW:0];
(* ram_style = "distributed" *) reg [9:0] dark_ram1_b [WR_IW:0];
(* ram_style = "distributed" *) reg [9:0] dark_ram1_r [WR_IW:0];
(* ram_style = "distributed" *) reg [9:0] dark_ram1_g2 [WR_IW:0];
后台写入 always(右移 4 位取平均):
always @(posedge clk) begin
if (buf_sel && avg_cnt < WR_IW) begin
if (buf_swap_pending == 1'b0) begin
dark_ram1_g1[avg_cnt] <= acc_g1[avg_cnt][AVG_SHIFT+8:AVG_SHIFT];
dark_ram1_b [avg_cnt] <= acc_b [avg_cnt][AVG_SHIFT+8:AVG_SHIFT];
dark_ram1_r [avg_cnt] <= acc_r [avg_cnt][AVG_SHIFT+8:AVG_SHIFT];
dark_ram1_g2[avg_cnt] <= acc_g2[avg_cnt][AVG_SHIFT+8:AVG_SHIFT];
end
else begin
dark_ram_g1 [avg_cnt] <= acc_g1[avg_cnt][AVG_SHIFT+8:AVG_SHIFT];
dark_ram_b [avg_cnt] <= acc_b [avg_cnt][AVG_SHIFT+8:AVG_SHIFT];
dark_ram_r [avg_cnt] <= acc_r [avg_cnt][AVG_SHIFT+8:AVG_SHIFT];
dark_ram_g2 [avg_cnt] <= acc_g2[avg_cnt][AVG_SHIFT+8:AVG_SHIFT];
end
end
end
平均值计算:取累加值的 [AVG_SHIFT+8 : AVG_SHIFT] 位,等价于右移 4 位除以 16。9 位存储宽度可覆盖 1023/16×16 = 1023。
乒乓机制:
-
buf_swap_pending=0→ 前台读dark_ram_*,后台写dark_ram1_* -
buf_swap_pending=1→ 前台读dark_ram1_*,后台写dark_ram_*
Step 7:有效区读取暗基准值
在有效区按 Bayer 通道类型,从前台 RAM 中读取对应像素位置的暗基准值,供后续减法使用。
reg [DATA_WIDTH-1:0] dark_val_r;
always @(posedge clk) begin
if (pre_de && !dark_phase) begin
case (norow[1:0])
2'b00: dark_val_r <= buf_swap_pending ? dark_ram1_g1[col_addr] : dark_ram_g1[col_addr];
2'b01: dark_val_r <= buf_swap_pending ? dark_ram1_b [col_addr] : dark_ram_b [col_addr];
2'b10: dark_val_r <= buf_swap_pending ? dark_ram1_r [col_addr] : dark_ram_r [col_addr];
2'b11: dark_val_r <= buf_swap_pending ? dark_ram1_g2[col_addr] : dark_ram_g2[col_addr];
endcase
end
end
buf_swap_pending 决定读哪组 RAM,实现前后台切换。
Step 8:原始数据打拍对齐
暗基准 RAM 的读取有 1 拍延迟,需要对原始像素数据 pre_data 和有效信号 pre_de 各打一拍,使其与暗基准读取时序对齐后才能相减。
reg [DATA_WIDTH-1:0] pre_data_r;
reg pre_de_r1;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
pre_data_r <= 'd0;
pre_de_r1 <= 1'b0;
end
else begin
pre_data_r <= pre_data;
pre_de_r1 <= pre_de;
end
end
Step 9:饱和减法输出
暗区直接透传,有效区执行饱和减法 post_data = max(pre_data_r - dark_val_r, 0)。这是整个流水线的最终输出段。
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
post_data <= 'd0;
post_de <= 1'b0;
end
else if (dark_phase) begin
post_data <= pre_data_r;
post_de <= pre_de_r1;
end
else if (pre_de_r1) begin
post_data <= (pre_data_r >= dark_val_r) ? (pre_data_r - dark_val_r) : 'd0;
post_de <= 1'b1;
end
else begin
post_data <= 'd0;
post_de <= 1'b0;
end
end
-
饱和减法:暗基准是 16 帧平均值,个别像素瞬时值可能略低于基准(噪声波动),强制归零避免截断误差。
-
核心路径延迟 :暗基准读取 1 拍 + 输出打拍 1 拍 = 2 个时钟周期。
┌──────────── 每帧前 4 行(暗区) ────────────┐
│ norow[1:0] → Bayer通道 → acc_x[col_addr] │
│ first_frame首帧初始化,后续15帧累加 │
└──────────────────────────────────────────────┘
│
▼
┌──────────── 每帧后 812 行(有效区) ────────┐
│ 路径A: 读前台dark_ram → dark_val_r │
│ 路径B: pre_data打一拍 → pre_data_r │
│ A/B对齐 → 饱和减法 → post_data │
└──────────────────────────────────────────────┘
│
▼ 16帧累计完成
┌──────────── 第16帧有效区后台刷新 ──────────┐
│ acc_x[col] 右移4位 → 写入后台 dark_ram1_* │
│ frame_end 触发 buf_swap_pending 翻转 │
│ 前后台角色交换,下一帧立即使用新基准 │
└──────────────────────────────────────────────┘
完整代码
`timescale 1ns / 1ps
module Image_Sensor_r#(
parameter WR_IW = 204 ,
parameter NOR_MAX = 816 ,
parameter FPN_REDU = 4 ,
parameter AVG_FRAMES = 16 ,
parameter AVG_SHIFT = 4 ,
parameter DATA_WIDTH = 'd10
)(
input clk ,
input gclk_0 ,
input rst_n ,
input [DATA_WIDTH-1:0] norow ,
input fifo_rst ,
input unpack_cksum ,
input [DATA_WIDTH-1:0] pre_data ,
input pre_de ,
output reg [DATA_WIDTH-1:0] post_data ,
output reg post_de
);
reg dark_phase;
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
dark_phase <= 1'b1;
else if((norow == NOR_MAX-1 && unpack_cksum))
dark_phase <= 1'b1;
else if(norow == FPN_REDU-1 && unpack_cksum)
dark_phase <= 1'b0;
end
reg [DATA_WIDTH-1:0] col_addr ;
always @(posedge clk ) begin
if(!rst_n)col_addr <=10'b0;
else if(pre_de) col_addr <=(col_addr ==WR_IW-1'b1)?0:col_addr +1'b1;
else col_addr <=10'b0;
end
// ============================================================
// 3. 帧结束脉冲 + 帧计数器
// ============================================================
reg [3:0] frame_cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
frame_cnt <= 4'd0;
else if ((norow == NOR_MAX-1) && unpack_cksum)
frame_cnt <= (frame_cnt == AVG_FRAMES-1) ? 4'd0 : frame_cnt + 1'b1;
end
wire first_frame = (frame_cnt == 4'd0);
// ============================================================
// 5. 多帧累加(13 位)
// ============================================================
(* ram_style = "distributed" *) reg [12:0] acc_g1 [WR_IW-1:0];
(* ram_style = "distributed" *) reg [12:0] acc_b [WR_IW-1:0];
(* ram_style = "distributed" *) reg [12:0] acc_r [WR_IW-1:0];
(* ram_style = "distributed" *) reg [12:0] acc_g2 [WR_IW-1:0];
always @(posedge clk) begin
if (dark_phase && pre_de) begin
case (norow[1:0])
2'b00: acc_g1[col_addr] <= first_frame ? {3'd0, pre_data} : acc_g1[col_addr] + pre_data;
2'b01: acc_b [col_addr] <= first_frame ? {3'd0, pre_data} : acc_b [col_addr] + pre_data;
2'b10: acc_r [col_addr] <= first_frame ? {3'd0, pre_data} : acc_r [col_addr] + pre_data;
2'b11: acc_g2[col_addr] <= first_frame ? {3'd0, pre_data} : acc_g2[col_addr] + pre_data;
endcase
end
end
// ============================================================
// 5. 帧切换标志
// ============================================================
reg buf_swap_pending ;
wire frame_end = (frame_cnt == AVG_FRAMES-1)&&(norow == NOR_MAX-1) && (unpack_cksum);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
buf_swap_pending <= 1'b0;
else if (frame_end&&buf_swap_pending==0)
buf_swap_pending <= 1'b1;
else if (frame_end && buf_swap_pending)
buf_swap_pending <= 1'b0;
end
// ============================================================
// 6. 乒乓暗基准 RAM(9 位存,够用)
// ============================================================
(* ram_style = "distributed" *) reg [9:0] dark_ram_g1 [WR_IW:0];
(* ram_style = "distributed" *) reg [9:0] dark_ram_b [WR_IW:0];
(* ram_style = "distributed" *) reg [9:0] dark_ram_r [WR_IW:0];
(* ram_style = "distributed" *) reg [9:0] dark_ram_g2 [WR_IW:0];
(* ram_style = "distributed" *) reg [9:0] dark_ram1_g1 [WR_IW:0];
(* ram_style = "distributed" *) reg [9:0] dark_ram1_b [WR_IW:0];
(* ram_style = "distributed" *) reg [9:0] dark_ram1_r [WR_IW:0];
(* ram_style = "distributed" *) reg [9:0] dark_ram1_g2 [WR_IW:0];
// 写入后台 RAM:取累加值高 9 位
wire buf_sel =(frame_cnt == AVG_FRAMES-1)&& !dark_phase ;
reg [7:0] avg_cnt ;
always @(posedge clk ) begin
if(!rst_n)avg_cnt<='b0;
else if(first_frame)avg_cnt<='b0;
else if(buf_sel)avg_cnt<=avg_cnt==WR_IW?avg_cnt:avg_cnt+1'b1;
else avg_cnt<='b0;
end
always @(posedge clk) begin
if (buf_sel&&avg_cnt<WR_IW) begin
if (buf_swap_pending == 1'b0) begin
dark_ram1_g1[avg_cnt] <= acc_g1[avg_cnt][AVG_SHIFT+8:AVG_SHIFT];
dark_ram1_b [avg_cnt] <= acc_b [avg_cnt][AVG_SHIFT+8:AVG_SHIFT];
dark_ram1_r [avg_cnt] <= acc_r [avg_cnt][AVG_SHIFT+8:AVG_SHIFT];
dark_ram1_g2[avg_cnt] <= acc_g2[avg_cnt][AVG_SHIFT+8:AVG_SHIFT];
end
else begin
dark_ram_g1 [avg_cnt] <= acc_g1[avg_cnt][AVG_SHIFT+8:AVG_SHIFT];
dark_ram_b [avg_cnt] <= acc_b [avg_cnt][AVG_SHIFT+8:AVG_SHIFT];
dark_ram_r [avg_cnt] <= acc_r [avg_cnt][AVG_SHIFT+8:AVG_SHIFT];
dark_ram_g2 [avg_cnt] <= acc_g2[avg_cnt][AVG_SHIFT+8:AVG_SHIFT];
end
end
end
// ============================================================
// 9. 读前台暗基准
// ============================================================
reg [DATA_WIDTH-1:0] dark_val_r;
always @(posedge clk) begin
if (pre_de && !dark_phase) begin
case (norow[1:0])
2'b00: dark_val_r <= buf_swap_pending ? dark_ram1_g1[col_addr] : dark_ram_g1[col_addr];
2'b01: dark_val_r <= buf_swap_pending ? dark_ram1_b [col_addr] : dark_ram_b [col_addr];
2'b10: dark_val_r <= buf_swap_pending ? dark_ram1_r [col_addr] : dark_ram_r [col_addr];
2'b11: dark_val_r <= buf_swap_pending ? dark_ram1_g2[col_addr] : dark_ram_g2[col_addr];
endcase
end
end
// ============================================================
// 10. 打拍 + 饱和减法输出
// ============================================================
reg [DATA_WIDTH-1:0] pre_data_r ;
reg pre_de_r1 ;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
pre_data_r <= 'd0;
pre_de_r1 <= 1'b0;
end
else begin
pre_data_r <= pre_data;
pre_de_r1 <= pre_de;
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
post_data <= 'd0;
post_de <= 1'b0;
end
else if (dark_phase) begin
post_data <= pre_data_r;
post_de <= pre_de_r1;
end
else if (pre_de_r1) begin
post_data <= (pre_data_r >= dark_val_r) ? (pre_data_r - dark_val_r): 'd0;
post_de <= 1'b1;
end
else begin
post_data <= 'd0;
post_de <= 1'b0;
end
end
endmodule