4. 单通道3x3卷积模块
任务目标
设计一个单通道3x3卷积RTL模块,接收图像像素流,输出卷积结果流。模块采用行缓冲结构实现3x3窗口滑动,完成乘累加运算后输出单像素卷积结果。
接口定义
clk : 时钟
rst_n : 异步复位,低有效
pixel_in : 输入像素,8bit无符号
pixel_valid : 输入有效标志
row_start : 新一行开始标志(第一个像素时拉高一个周期)
conv_out : 卷积结果,16bit有符号
out_valid : 输出有效标志
卷积核与参数
使用固定3x3卷积核(可综合为常量):
+1 +0 -1
+2 +0 -2
+1 +0 -1 (Sobel垂直边缘检测核)
输入图像宽度可配置,通过参数 IMG_WIDTH 指定,默认64像素。像素为8bit无符号,卷积结果为有符号16bit(最大范围:255×8 = 2040,16bit足够)。
关键完整代码
module conv3x3_single_ch #(
parameter IMG_WIDTH = 64
)(
input wire clk,
input wire rst_n,
input wire [7:0] pixel_in,
input wire pixel_valid,
input wire row_start,
output reg [15:0] conv_out,
output reg out_valid
);
// 行缓冲:存储前两行像素
reg [7:0] line_buf [0:1][0:IMG_WIDTH-1];
// 3x3窗口寄存器
reg [7:0] win [0:2][0:2];
// 行缓冲写地址
reg [$clog2(IMG_WIDTH)-1:0] wr_addr;
reg [$clog2(IMG_WIDTH)-1:0] rd_addr;
// 行计数与像素计数
reg [1:0] row_cnt;
reg is_valid_row; // 当前行是否为有效数据行(第3行起)
// 写地址控制
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_addr <= 0;
row_cnt <= 0;
end else if (pixel_valid) begin
if (row_start) begin
wr_addr <= 0;
if (row_cnt == 2'd2)
row_cnt <= 2'd2; // 保持最大2
else
row_cnt <= row_cnt + 1'b1;
end else begin
wr_addr <= wr_addr + 1'b1;
end
end
end
// 写入行缓冲(交替写入两行)
always @(posedge clk) begin
if (pixel_valid) begin
if (row_cnt == 2'd0)
line_buf[0][wr_addr] <= pixel_in;
else if (row_cnt == 2'd1)
line_buf[1][wr_addr] <= pixel_in;
// row_cnt==2时,当前行直接进入窗口,不写缓冲
end
end
// 读地址(滞后写地址一拍,用于窗口移位)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
rd_addr <= 0;
else if (pixel_valid) begin
if (row_start)
rd_addr <= 0;
else
rd_addr <= rd_addr + 1'b1;
end
end
// 窗口更新
wire [7:0] buf0_data = line_buf[0][rd_addr];
wire [7:0] buf1_data = line_buf[1][rd_addr];
always @(posedge clk) begin
if (pixel_valid) begin
// 第0行数据
win[0][0] <= win[0][1];
win[0][1] <= win[0][2];
win[0][2] <= (row_cnt == 2'd0) ? pixel_in : buf0_data;
// 第1行数据
win[1][0] <= win[1][1];
win[1][1] <= win[1][2];
win[1][2] <= (row_cnt == 2'd1) ? pixel_in : buf1_data;
// 第2行数据(当前输入行)
win[2][0] <= win[2][1];
win[2][1] <= win[2][2];
win[2][2] <= pixel_in;
end
end
// 有效输出标志:第3行起且列位置≥2
reg [1:0] valid_delay;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
valid_delay <= 2'd0;
is_valid_row <= 1'b0;
end else if (pixel_valid) begin
if (row_start) begin
if (row_cnt == 2'd2)
is_valid_row <= 1'b1;
else if (row_cnt == 2'd0)
is_valid_row <= 1'b0;
end
// 列位置判断:rd_addr>=2 且当前行有效
if (is_valid_row && (rd_addr >= 2))
valid_delay <= 2'd3; // 等待窗口完全填充
else if (valid_delay != 2'd0)
valid_delay <= valid_delay - 1'b1;
end
end
// 卷积计算
wire signed [15:0] p00 = $signed({8'b0, win[0][0]});
wire signed [15:0] p01 = $signed({8'b0, win[0][1]});
wire signed [15:0] p02 = $signed({8'b0, win[0][2]});
wire signed [15:0] p10 = $signed({8'b0, win[1][0]});
wire signed [15:0] p11 = $signed({8'b0, win[1][1]});
wire signed [15:0] p12 = $signed({8'b0, win[1][2]});
wire signed [15:0] p20 = $signed({8'b0, win[2][0]});
wire signed [15:0] p21 = $signed({8'b0, win[2][1]});
wire signed [15:0] p22 = $signed({8'b0, win[2][2]});
wire signed [15:0] sum = p00 - p02 + (p10 << 1) - (p12 << 1) + p20 - p22;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
conv_out <= 16'd0;
out_valid <= 1'b0;
end else begin
conv_out <= sum;
out_valid <= (valid_delay == 2'd1) ? 1'b1 : 1'b0;
end
end
endmodule
运行与验证
Testbench验证要点:
- 输入一个8×8测试图像(需满足
IMG_WIDTH=8重新例化),按行逐像素输入,每行第一个像素拉高row_start。 - 前两行输入时
out_valid保持低电平;从第3行第3个像素起,out_valid拉高并输出对应卷积值。 - 对比手工计算的Sobel结果,例如中心像素周围全为常数100时,输出应为0(平坦区域梯度为0)。
通过条件:仿真波形中 out_valid 时序正确,且输出值与手工计算一致。典型测试:输入全0图像,输出恒为0;输入垂直边缘图像(左半0,右半255),边缘列输出非零值。
仿真命令(Vivado/XSim)
# 在Vivado Tcl控制台或终端执行
xvlog conv3x3_single_ch.v tb_conv3x3.v
xelab tb_conv3x3 -debug typical
xsim tb_conv3x3 -runall
常见问题
- 输出延迟:从输入到输出共3个时钟周期延迟(窗口填充2拍+计算1拍),属于正常流水延迟。
- 边界处理:本设计不输出图像边界像素(前2行和前2列),如需边界填充需额外逻辑。
- 行缓冲深度:参数
IMG_WIDTH必须与实际图像宽度一致,否则窗口数据错位。