此处只说明图像放大,不涉及图像缩小,因为大多数图像处理场景下图像缩小的需求可以直接使用像素点抽取的方式实现,而且在缩小之后还涉及到与需求关联紧密的图像拼接,很难有通用化的设计。
图像放大有以下关键设计:
以下设计场景是将输入图像内指定范围的图像子块放大至与输入图像相同的分辩率。
图像放大的计算参数包括:
令输入图像分辩率为 W × H W\times H W×H,由 (x0, y0) 和 z 计算得到全部计算参数,不同的插值算法有不同的参数计算。
双线性插值,使用包围当前输出像素点(绿点)的 4 个输入像素点(红点)进行插值。
一般情况:
特殊情况,输出像素点与输入像素点的左上角点重合。
即输出像素点与某个输入像素点重合情况下的特殊处理,可以选择 4 个输入像素点中任一点作为重合点,但是由于图像坐标系由左上角开始,因此与左上角点重合比较便于计算。
x b e g i n ≤ x 0 x b e g i n = f l o o r ( x 0 ) x_{begin}\leq x_0\\ x_{begin}=floor(x_0) xbegin≤x0xbegin=floor(x0)
y b e g i n ≤ y 0 y b e g i n = f l o o r ( y 0 ) y_{begin}\leq y_{0}\\ y_{begin}=floor(y_0) ybegin≤y0ybegin=floor(y0)
输出图像的列坐标最大值为 x 0 + ( W − 1 ) × z x_0+(W-1)\times z x0+(W−1)×z
输出图像的行坐标最大值为 y 0 + ( H − 1 ) × z y_0+(H-1)\times z y0+(H−1)×z
x e n d > x 0 + ( W − 1 ) × z x e n d = f l o o r ( x 0 + ( W − 1 ) × z + 1 ) x_{end}>x_0+(W-1)\times z\\ x_{end}=floor(x_0+(W-1)\times z+1) xend>x0+(W−1)×zxend=floor(x0+(W−1)×z+1)
y e n d > y 0 + ( H − 1 ) × z y e n d = f l o o r ( y 0 + ( H − 1 ) × z + 1 ) y_{end}>y_0+(H-1)\times z\\ y_{end}=floor(y_0+(H-1)\times z+1) yend>y0+(H−1)×zyend=floor(y0+(H−1)×z+1)
设输出像素点在输出图像中的坐标值为 (X, Y),该点对应的输入图像的坐标为:
( x 0 + X × z , y 0 + Y × z ) (x_0+X\times z, y_0+Y\times z) (x0+X×z,y0+Y×z)
则计算该输出像素点的输入图像的 4 个插值点的坐标为:
x 00 = x 10 = f l o o r ( x 0 + X × z ) x 01 = x 11 = f l o o r ( x 0 + X × z + 1 ) y 00 = y 01 = f l o o r ( y 0 + Y × z ) y 10 = y 11 = f l o o r ( y 0 + Y × z + 1 ) x00=x10=floor(x_0+X\times z)\\ x01=x11=floor(x_0+X\times z+1)\\ y00=y01=floor(y_0+Y\times z)\\ y10=y11=floor(y_0+Y\times z+1) x00=x10=floor(x0+X×z)x01=x11=floor(x0+X×z+1)y00=y01=floor(y0+Y×z)y10=y11=floor(y0+Y×z+1)
双立方插值,使用包围当前输出像素点(绿点)的 16 个输入像素点(红点)进行插值。
一般情况:
特殊情况,输出像素点与输入像素点的第 2 行第 2 列的像素点重合。
即输出像素点与某个输入像素点重合情况下的特殊处理,可以选择个红框上 4 个输入像素点任一点作为重合点,但是由于图像坐标系由左上角开始,因此与红框的左上角点重合比较便于计算。
x b e g i n ≤ ( x 0 − 1 ) x b e g i n = f l o o r ( x 0 − 1 ) x_{begin}\leq (x_0-1)\\ x_{begin}=floor(x_0-1) xbegin≤(x0−1)xbegin=floor(x0−1)
y b e g i n ≤ ( y 0 − 1 ) y b e g i n = f l o o r ( y 0 − 1 ) y_{begin}\leq (y_{0}-1)\\ y_{begin}=floor(y_0-1) ybegin≤(y0−1)ybegin=floor(y0−1)
x e n d > x 0 + ( W − 1 ) × z + 1 x e n d = f l o o r ( x 0 + ( W − 1 ) × z + 1 + 1 ) x_{end}>x_0+(W-1)\times z+1\\ x_{end}=floor(x_0+(W-1)\times z+1+1) xend>x0+(W−1)×z+1xend=floor(x0+(W−1)×z+1+1)
y e n d > y 0 + ( H − 1 ) × z + 1 y e n d = f l o o r ( y 0 + ( H − 1 ) × z + 1 + 1 ) y_{end}>y_0+(H-1)\times z+1\\ y_{end}=floor(y_0+(H-1)\times z+1+1) yend>y0+(H−1)×z+1yend=floor(y0+(H−1)×z+1+1)
设输出像素点在输出图像中的坐标值为 (X, Y),该点对应的输入图像的坐标为:
( x 0 + X × z , y 0 + Y × z ) (x_0+X\times z, y_0+Y\times z) (x0+X×z,y0+Y×z)
则计算该输出像素点的输入图像的 16 个插值点的坐标为:
x 00 = x 10 = x 20 = x 30 = f l o o r ( x 0 + X × z − 1 ) x 01 = x 11 = x 21 = x 31 = f l o o r ( x 0 + X × z ) x 02 = x 12 = x 22 = x 32 = f l o o r ( x 0 + X × z + 1 ) x 03 = x 13 = x 23 = x 33 = f l o o r ( x 0 + X × z + 2 ) y 00 = y 01 = y 02 = y 03 = f l o o r ( y 0 + Y × z − 1 ) y 10 = y 11 = y 12 = y 13 = f l o o r ( y 0 + Y × z ) y 20 = y 21 = y 22 = y 23 = f l o o r ( y 0 + Y × z + 1 ) y 30 = y 31 = y 32 = y 33 = f l o o r ( y 0 + Y × z + 2 ) x00=x10=x20=x30=floor(x_0+X\times z-1)\\ x01=x11=x21=x31=floor(x_0+X\times z)\\ x02=x12=x22=x32=floor(x_0+X\times z+1)\\ x03=x13=x23=x33=floor(x_0+X\times z+2)\\ y00=y01=y02=y03=floor(y_0+Y\times z-1)\\ y10=y11=y12=y13=floor(y_0+Y\times z)\\ y20=y21=y22=y23=floor(y_0+Y\times z+1)\\ y30=y31=y32=y33=floor(y_0+Y\times z+2) x00=x10=x20=x30=floor(x0+X×z−1)x01=x11=x21=x31=floor(x0+X×z)x02=x12=x22=x32=floor(x0+X×z+1)x03=x13=x23=x33=floor(x0+X×z+2)y00=y01=y02=y03=floor(y0+Y×z−1)y10=y11=y12=y13=floor(y0+Y×z)y20=y21=y22=y23=floor(y0+Y×z+1)y30=y31=y32=y33=floor(y0+Y×z+2)
在 FPGA 实现中,每个时钟周期计算产生 1 个输出像素点的情况下,多个输出像素点的计算可能使用同一组输入像素点进行插值,而每个时钟周期都有新的输入像素点进入,因此需要对输入数据进行缓冲。
而且在参数设置的放大倍数较大的情况下,用于放大的图像子块较小,只需要少量的输入数据进行插值计算,导致输入数据占用时长远小于输出数据占用时长,因此需要控制上游模块暂停数据输入,防止由于缓冲空间不足,导致用于插值计算的输入数据被新的输入数据冲走。
数据缓冲分为 3 个阶段:
阶段 1:将输入图像数据送入二维缓冲产生与插值计算相同格式的并行数据。双线性插值为 2 × 2 2\times 2 2×2,双立方插值为 4 × 4 4\times 4 4×4。
阶段 2:将二维缓冲输出的并行数据存入 FIFO,根据其行列坐标值选择插值范围内的并行数据写入 FIFO。
阶段 3:将 FIFO 内的数据按行读出,并依次循环交替写入 2 个 Block RAM,实现 ping/pong切换。使用 RAM 而不用 FIFO 的原因在于这 1 行输入像素点数据可能多次用于多行输出像素点的插值计算,需要反复多次读取;用 ping/pong 切换的原因在于可以实现 1 个 RAM 读出数据用于插值计算的同时,另 1 个 RAM 可以写入 FIFO 读出的下 1 行输入图像数据。
双立方插值,即 OpenCV 中 resize 函数的 INTER_CUBIC 插值算法。
插值系数 W 的计算公式:
W ( x ) = { ( a + 2 ) × ∣ x ∣ 3 − ( a + 3 ) × ∣ x ∣ 2 + 1 ∣ x ∣ ≤ 1 a × ∣ x ∣ 3 − 5 × a × ∣ x ∣ 2 + 8 × a × ∣ x ∣ − 4 × a 1 < ∣ x ∣ < 2 0 e l s e W(x)=\begin{cases} (a+2)\times \lvert x\rvert^3-(a+3)\times\lvert x\rvert^2+1&\lvert x\rvert\leq1\\ a\times \lvert x\rvert^3-5\times a\times\lvert x\rvert^2+8\times a\times\lvert x\rvert-4\times a&1<\lvert x\rvert <2\\ 0&else \end{cases} W(x)=⎩⎪⎨⎪⎧(a+2)×∣x∣3−(a+3)×∣x∣2+1a×∣x∣3−5×a×∣x∣2+8×a×∣x∣−4×a0∣x∣≤11<∣x∣<2else
上式中 a 值取 -0.5。
设输出像素点的坐标为 (x, y),用于计算的插值像素点 (xi, yi) 取其邻近的 4 × 4 4\times 4 4×4 个输入像素点。
根据坐标转换部分的说明:
插值计算公式如下:
f ( x , y ) = ∑ i = 0 3 ∑ j = 0 3 f ( x i , y i ) × W ( x − x i ) × W ( y − y i ) f(x,y)=\sum^3_{i=0}\sum^3_{j=0}f(x_i,y_i)\times W(x-x_i)\times W(y-y_i) f(x,y)=i=0∑3j=0∑3f(xi,yi)×W(x−xi)×W(y−yi)
完整的设计分为 4 个步骤:
二维缓冲例化代码,二维缓冲输出的并行数据、fv、lv、x和y相当于下个步骤的数据输入:
wire [7:0] buf_11;
wire [7:0] buf_12;
wire [7:0] buf_13;
wire [7:0] buf_14;
wire [7:0] buf_21;
wire [7:0] buf_22;
wire [7:0] buf_23;
wire [7:0] buf_24;
wire [7:0] buf_31;
wire [7:0] buf_32;
wire [7:0] buf_33;
wire [7:0] buf_34;
wire [7:0] buf_41;
wire [7:0] buf_42;
wire [7:0] buf_43;
wire [7:0] buf_44;
(*keep = "TRUE"*) wire buf_fv;
(*keep = "TRUE"*) wire buf_lv;
(*keep = "TRUE"*) wire [15:0] buf_x;
(*keep = "TRUE"*) wire [15:0] buf_y;
buf_5x5_zoom buf_5x5_u (
.clk(clk), // input wire clk
.rst(rst), // input wire [0 : 0] rst
.in_pix(in_pix), // input wire [7 : 0] in_pix
.in_lv(in_lv), // input wire [0 : 0] in_lv
.out_pix00(), // output wire [7 : 0] out_pix00
.out_pix01(), // output wire [7 : 0] out_pix01
.out_pix02(), // output wire [7 : 0] out_pix02
.out_pix03(), // output wire [7 : 0] out_pix03
.out_pix04(), // output wire [7 : 0] out_pix04
.out_pix10(), // output wire [7 : 0] out_pix10
.out_pix11(buf_11), // output wire [7 : 0] out_pix11
.out_pix12(buf_12), // output wire [7 : 0] out_pix12
.out_pix13(buf_13), // output wire [7 : 0] out_pix13
.out_pix14(buf_14), // output wire [7 : 0] out_pix14
.out_pix20(), // output wire [7 : 0] out_pix20
.out_pix21(buf_21), // output wire [7 : 0] out_pix21
.out_pix22(buf_22), // output wire [7 : 0] out_pix22
.out_pix23(buf_23), // output wire [7 : 0] out_pix23
.out_pix24(buf_24), // output wire [7 : 0] out_pix24
.out_pix30(), // output wire [7 : 0] out_pix30
.out_pix31(buf_31), // output wire [7 : 0] out_pix31
.out_pix32(buf_32), // output wire [7 : 0] out_pix32
.out_pix33(buf_33), // output wire [7 : 0] out_pix33
.out_pix34(buf_34), // output wire [7 : 0] out_pix34
.out_pix40(), // output wire [7 : 0] out_pix40
.out_pix41(buf_41), // output wire [7 : 0] out_pix41
.out_pix42(buf_42), // output wire [7 : 0] out_pix42
.out_pix43(buf_43), // output wire [7 : 0] out_pix43
.out_pix44(buf_44), // output wire [7 : 0] out_pix44
.out_x(buf_x), // output wire [15 : 0] out_col
.out_y(buf_y), // output wire [15 : 0] out_row
.out_fv(buf_fv), // output wire [0 : 0] out_fv
.out_lv(buf_lv) // output wire [0 : 0] out_lv
);
二维缓冲输出的 fv 用于第 1 次参数传递,将端口参数送至 FIFO 写接口,参考更新办法见前述的参数更新流程:
//更新FIFO缓冲的参数
always @(posedge clk) begin
if (rst == 1'b1) begin
z <= 32'hFFFF_FFFF;
x0 <= 32'hFFFF_FFFF;
y0 <= 32'hFFFF_FFFF;
x_begin <= 16'hFFFF;
y_begin <= 16'hFFFF;
x_end <= 16'hFFFF;
y_end <= 16'hFFFF;
col_len <= 16'hFFFF;
row_len <= 16'hFFFF;
end
else begin
case ({buf_fv_d1, buf_fv, param_valid})
{1'b1, 1'b0, 1'b1}: begin
//fv下降沿,且参数有效,则更新参数值
z <= param_z;
x0 <= param_x0;
y0 <= param_y0;
x_begin <= param_x_begin;
y_begin <= param_y_begin;
x_end <= param_x_end;
y_end <= param_y_end;
col_len <= param_col_len;
row_len <= param_row_len;
end
default: begin
//保持
z <= z;
x0 <= x0;
y0 <= y0;
x_begin <= x_begin;
y_begin <= y_begin;
x_end <= x_end;
y_end <= y_end;
col_len <= col_len;
row_len <= row_len;
end
endcase
end
end
用状态机控制截取输入图像数据写入 FIFO。
注意下方代码的 state_en 寄存器,用于控制算法使能,在算法非使能情况下直接输出二维缓冲的结果,数据不写入 FIFO。
always @(posedge clk) begin
if (rst == 1'b1) begin
state_fifo_wr <= FIFO_WR_WAIT;//复位后等待fv下降沿才开始工作
fifo_wr_en <= 1'b0;
fifo_din <= {128{1'b1}};
end
else begin
case (state_fifo_wr)
FIFO_WR_WAIT: begin
//在fv下降沿检查算法使能状态,用于启动状态机
case ({buf_fv_d1, buf_fv, state_en})
{1'b1, 1'b0, ENABLED}: begin
state_fifo_wr <= FIFO_WR_DATA;
end
default: begin
state_fifo_wr <= state_fifo_wr;
end
endcase
fifo_wr_en <= 1'b0;
fifo_din <= {128{1'b1}};
end
FIFO_WR_DATA: begin
if ((buf_x >= x_begin) && (buf_x <= x_end) && (buf_y >= y_begin) && (buf_y <= y_end)) begin
//二维缓冲输出的并行数据坐标在截取范围内
fifo_wr_en <= buf_fv & buf_lv;//二缓缓冲输出并行数据有效
end
else begin
fifo_wr_en <= 1'b0;
end
//FIFO写入数据为并行数据拼接
fifo_din <= {buf_11, buf_12, buf_13, buf_14,
buf_21, buf_22, buf_23, buf_24,
buf_31, buf_32, buf_33, buf_34,
buf_41, buf_42, buf_43, buf_44};
case ({buf_x, buf_y, buf_fv, buf_lv})
{x_end, y_end, 1'b1, 1'b1}: begin
//有效范围内的最后1个像素点从二维缓冲输出
state_fifo_wr <= FIFO_WR_WAIT;
end
default: begin
//状态保持
state_fifo_wr <= state_fifo_wr;
end
endcase
end
default: begin
state_fifo_wr <= FIFO_WR_WAIT;
fifo_wr_en <= 1'b0;
fifo_din <= {128{1'b1}};
end
endcase
end
end
FIFO 读出状态机如下,在从 FIFO 读出 1 帧数据之前先寄存 FIFO 写接口的参数,完成第 2 次参数传递,再根据 Block RAM 的可写状态切换数据写入的 RAM,直到 1 帧图像在 FIFO 内缓冲的数据全部读出。
always @(posedge clk) begin
if (rst == 1'b1) begin
fifo_rd_en <= 1'b0;
state_fifo_rd <= FIFO_RD_PARAM;
end
else begin
case (state_fifo_rd)
FIFO_RD_PARAM: begin
//FIFO非空,表示新一帧图像数据已进入,更新参数
if (fifo_empty == 1'b0) begin
state_fifo_rd <= FIFO_RD_LINE_WAIT_PING;
end
else begin
state_fifo_rd <= state_fifo_rd;
end
fifo_rd_en <= 1'b0;
end
FIFO_RD_LINE_WAIT_PING: begin
//等待FIFO内装入1行数据量sg_col_len,且ping可写
if ((fifo_data_count >= bram_col_len) && (bram_state_ping == 1'b0)) begin
fifo_rd_en <= 1'b1;
state_fifo_rd <= FIFO_RD_LINE_PING;
end
else begin
fifo_rd_en <= 1'b0;
state_fifo_rd <= state_fifo_rd;
end
end
FIFO_RD_LINE_PING: begin
//状态保持sg_col_len个时钟周期,从FIFO内读出1行数据写入ping
//在当前状态下fifo_rd_en保持有效
case (cnt_bram_col_len)
bram_col_len: begin
//完成sg_col_len个时钟周期计数,1行数据读出完成
fifo_rd_en <= 1'b0;
case (cnt_bram_row_len)
bram_row_len: begin
//完成sg_row_len行的数据读出,即1帧图像读出完成,状态机复位
state_fifo_rd <= FIFO_RD_PARAM;
end
default: begin
//未完成1帧图像读出,接下来读出下1行数据,切换至pong
state_fifo_rd <= FIFO_RD_LINE_WAIT_PONG;
end
endcase
end
default: begin
fifo_rd_en <= 1'b1;
state_fifo_rd <= state_fifo_rd;
end
endcase
end
FIFO_RD_LINE_WAIT_PONG: begin
//等待FIFO内装入1行数据量sg_col_len,且pong可写
if ((fifo_data_count >= bram_col_len) && (bram_state_pong == 1'b0)) begin
fifo_rd_en <= 1'b1;
state_fifo_rd <= FIFO_RD_LINE_PONG;
end
else begin
fifo_rd_en <= 1'b0;
state_fifo_rd <= state_fifo_rd;
end
end
FIFO_RD_LINE_PONG: begin
//状态保持bram_col_len个时钟周期,从FIFO内读出1行数据写入pong
//在当前状态下fifo_rd_en保持有效
case (cnt_bram_col_len)
bram_col_len: begin
//完成sg_col_len个时钟周期计数,1行数据读出完成
fifo_rd_en <= 1'b0;
case (cnt_bram_row_len)
bram_row_len: begin
//完成bram_row_len行的数据读出,即1帧图像读出完成,状态机复位
state_fifo_rd <= FIFO_RD_PARAM;
end
default: begin
//未完成1帧图像读出,接下来读出下1行数据,切换至ping
state_fifo_rd <= FIFO_RD_LINE_WAIT_PING;
end
endcase
end
default: begin
fifo_rd_en <= 1'b1;
state_fifo_rd <= state_fifo_rd;
end
endcase
end
default: begin
fifo_rd_en <= 1'b0;
state_fifo_rd <= FIFO_RD_PARAM;
end
endcase
end
end
//FIFO读出1帧数据之前,更新bram写参数
always @(posedge clk) begin
if (rst == 1'b1) begin
{bram_z, bram_x0, bram_y0, bram_x_begin, bram_y_begin, bram_x_end, bram_y_end, bram_col_len, bram_row_len} <= {192{1'b1}};
end
else begin
case (state_fifo_rd)
FIFO_RD_PARAM: begin
{bram_z, bram_x0, bram_y0, bram_x_begin, bram_y_begin, bram_x_end, bram_y_end, bram_col_len, bram_row_len} <= {z, x0, y0, x_begin, y_begin, x_end, y_end, col_len, row_len};
end
default: begin
//保持
{bram_z, bram_x0, bram_y0, bram_x_begin, bram_y_begin, bram_x_end, bram_y_end, bram_col_len, bram_row_len} <= {bram_z, bram_x0, bram_y0, bram_x_begin, bram_y_begin, bram_x_end, bram_y_end, bram_col_len, bram_row_len};
end
endcase
end
end
Block RAM 定义为双口 RAM,a 口专用于写入,b 口专用于读出。
注意:RAM 的写读地址与输入像素点列坐标一致,方便 sysgen 根据列坐标读出用于插值的数据。
Block RAM 写入流程由 FIFO 读出状态机控制:
//例化FIFO读出后保存行数据的bram
//用ping/pong切换的方式,实现1个bram写入的过程中另1个bram可以读出
reg wea_ping = 1'b0;
reg wea_pong = 1'b0;
reg [127:0] dina_ping = {128{1'b1}};
reg [127:0] dina_pong = {128{1'b1}};
reg [9:0] addra_ping = 10'h3FF;
reg [9:0] addra_pong = 10'h3FF;
//port b保持读状态,用addrb更新读出数值
wire [9:0] addrb_ping;
wire [9:0] addrb_pong;
wire [127:0] doutb_ping;
wire [127:0] doutb_pong;
//port a专用于写入
//port b专用于读出
bram_zoom bram_ping
(
.clka(clk), // input wire clka
.wea(wea_ping), // input wire [0 : 0] wea
.addra(addra_ping), // input wire [9 : 0] addra
.dina(dina_ping), // input wire [127 : 0] dina
.douta(), // output wire [127 : 0] douta
.clkb(clk), // input wire clkb
.web(1'b0), // input wire [0 : 0] web
.addrb(addrb_ping), // input wire [9 : 0] addrb
.dinb(128'd0), // input wire [127 : 0] dinb
.doutb(doutb_ping) // output wire [127 : 0] doutb
);
bram_zoom bram_pong
(
.clka(clk), // input wire clka
.wea(wea_pong), // input wire [0 : 0] wea
.addra(addra_pong), // input wire [9 : 0] addra
.dina(dina_pong), // input wire [127 : 0] dina
.douta(), // output wire [127 : 0] douta
.clkb(clk), // input wire clkb
.web(1'b0), // input wire [0 : 0] web
.addrb(addrb_pong), // input wire [9 : 0] addrb
.dinb(128'd0), // input wire [127 : 0] dinb
.doutb(doutb_pong) // output wire [127 : 0] doutb
);
//bram写接口,写地址即输入像素点坐标
always @(posedge clk) begin
case (state_fifo_rd)
FIFO_RD_LINE_PING: begin
//FIFO数据写入ping
wea_ping <= 1'b1;
dina_ping <= fifo_dout;
addra_ping <= addra_ping+10'd1;
end
default: begin
wea_ping <= 1'b0;
dina_ping <= 128'd0;
addra_ping <= bram_x_begin[9:0]-10'd1;//用于在FIFO_RD_LINE_PING首个时钟周期+1后得到bram_x_begin
end
endcase
end
always @(posedge clk) begin
case (state_fifo_rd)
FIFO_RD_LINE_PONG: begin
//FIFO数据写入pong
wea_pong <= 1'b1;
dina_pong <= fifo_dout;
addra_pong <= addra_pong+10'd1;
end
default: begin
wea_pong <= 1'b0;
dina_pong <= 128'd0;
addra_pong <= bram_x_begin[9:0]-10'd1;//用于在FIFO_RD_LINE_PONG首个时钟周期+1后得到bram_x_begin
end
endcase
end
Block RAM 的读出状态机,用输入 sysgen 模块的 lv 信号控制 sysgen 的计算流程,并且进行第 3 次参数传递,将参数供 sysgen 计算使用。
注意下方代码中 backpressure_in 信号的处理,即收到下游模块送入的反压信号时,Block RAM 读出状态机将停止工作。
//bram数据读出控制
//来自于sysgen的输入图像坐标请求和对应的输出图像坐标
(*keep = "TRUE"*) wire coord_en;//坐标有效
(*keep = "TRUE"*) wire [15:0] coord_x;//输出图像列坐标
(*keep = "TRUE"*) wire [15:0] coord_y;//输出图像行坐标
(*keep = "TRUE"*) wire [15:0] coord_req_x;//请求输入图像的列坐标,用于bram读地址
(*keep = "TRUE"*) wire [15:0] coord_req_y;//请求输入图像的行坐标,切换bram ping/pong
//bram读地址直连sysgen的请求输入图像列坐标
assign addrb_ping = coord_req_x[9:0];
assign addrb_pong = coord_req_x[9:0];
//coord_en延迟1个时钟周期,用于同步bram读出数据,即比coord_req_x延迟1个时钟周期
reg coord_en_d1 = 1'b0;
always @(posedge clk) begin
coord_en_d1 <= coord_en;
end
//coord_y延迟1个时钟周期,用于判断输出1帧结果
reg [15:0] coord_y_d1 = 16'hFFFF;
always @(posedge clk) begin
coord_y_d1 <= coord_y;
end
//coord_req_y延迟1个时钟周期,用于查看请求输入图像的行坐标的变化,判断切换bram ping/pong
reg [15:0] coord_req_y_d1 = 16'hFFFF;
always @(posedge clk) begin
coord_req_y_d1 <= coord_req_y;
end
//从bram写流程中取出的参数,用于sysgen计算,完成1帧插值计算,从ping中读出首行插值数据之前寄存
(*keep = "TRUE"*) reg [31:0] sg_z = 32'hFFFF_FFFF;
(*keep = "TRUE"*) reg [31:0] sg_x0 = 32'hFFFF_FFFF;
(*keep = "TRUE"*) reg [31:0] sg_y0 = 32'hFFFF_FFFF;
(*keep = "TRUE"*) reg [15:0] sg_y_begin = 16'hFFFF;
(*keep = "TRUE"*) reg [15:0] sg_y_end = 16'hFFFF;
//bram读状态机计数
reg [15:0] cnt_bram_rd = 16'd1;//计数范围
//bram读状态机,由bram内数据状态和sysgen坐标请求状态控制
localparam BRAM_RD_WAIT_PING = 6'b000001;
localparam BRAM_RD_PING = 6'b000010;
localparam BRAM_RD_HOLD_PING = 6'b000100;
localparam BRAM_RD_WAIT_PONG = 6'b001000;
localparam BRAM_RD_PONG = 6'b010000;
localparam BRAM_RD_HOLD_PONG = 6'b100000;
(*keep = "TRUE"*) reg [5:0] state_bram_rd = BRAM_RD_WAIT_PING;
always @(posedge clk) begin
if (rst == 1'b1) begin
state_bram_rd <= BRAM_RD_WAIT_PING;
end
else begin
case (state_bram_rd)
BRAM_RD_WAIT_PING: begin
//等待ping可读,且无反压输入
if ((bram_state_ping == 1'b1) && (backpressure_in == 1'b0)) begin
state_bram_rd <= BRAM_RD_PING;
end
else begin
state_bram_rd <= state_bram_rd;
end
end
BRAM_RD_PING: begin
//保持COLS个时钟周期,生成输入sysgen的lv,长度为COLS,启动sysgen计算
case (cnt_bram_rd)
COLS: begin
state_bram_rd <= BRAM_RD_HOLD_PING;
end
default: begin
state_bram_rd <= state_bram_rd;
end
endcase
end
BRAM_RD_HOLD_PING: begin
//前1个状态生成的lv送出全部的的坐标请求
case ({coord_en_d1, coord_en})
{1'b1, 1'b0}: begin
//完成1行坐标请求
if ((coord_y == 16'd0) && (coord_y_d1 != 16'd0)) begin
//完成一帧图像最后1行的请求
//状态机复位
state_bram_rd <= BRAM_RD_WAIT_PING;
end
/*
由于浮点计算误差,可能导致一帧数据计算过程中,读取bram的次数多于row_width,从FIFO中多读出下帧的若干行
因此必须通过ori_y有效值限制切换
ori_y值小于row_begin,表示起点行误差,在ori_y>ori_y_d1情况下发现ori_y值小于等于row_begin,表示ori_y_d1小于row_begin,则不切换
ori_y值大于row_end,表示终点行误差,在ori_y>ori_y_d1情况下发现ori_y值大于row_end,不切换
*/
else if ((coord_req_y > coord_req_y_d1) && (coord_req_y <= sg_y_end) && (coord_req_y > sg_y_begin)) begin
//计算输入的原始图像坐标已切换至下一行
state_bram_rd <= BRAM_RD_WAIT_PONG;
end
else begin
//不切换bram,仍使用ping
state_bram_rd <= BRAM_RD_WAIT_PING;
end
end
endcase
end
BRAM_RD_WAIT_PONG: begin
//等待pong可读,且无反压输入
if ((bram_state_pong == 1'b1) && (backpressure_in == 1'b0)) begin
state_bram_rd <= BRAM_RD_PONG;
end
else begin
state_bram_rd <= state_bram_rd;
end
end
BRAM_RD_PONG: begin
//保持COLS个时钟周期,生成输入sysgen的lv,启动sysgen计算
case (cnt_bram_rd)
COLS: begin
state_bram_rd <= BRAM_RD_HOLD_PONG;
end
default: begin
state_bram_rd <= state_bram_rd;
end
endcase
end
BRAM_RD_HOLD_PONG: begin
//等待sysgen完成1行数据请求
case ({coord_en_d1, coord_en})
{1'b1, 1'b0}: begin
//完成1行坐标请求
if ((coord_y == 16'd0) && (coord_y_d1 != 16'd0)) begin
//完成一帧图像最后1行的请求
//状态机复位
state_bram_rd <= BRAM_RD_WAIT_PING;
end
/*
由于浮点计算误差,可能导致一帧数据计算过程中,读取bram的次数多于row_width,从FIFO中多读出下帧的若干行
因此必须通过ori_y有效值限制切换
ori_y值小于row_begin,表示起点行误差,在ori_y>ori_y_d1情况下发现ori_y值小于等于row_begin,表示ori_y_d1小于row_begin,则不切换
ori_y值大于row_end,表示终点行误差,在ori_y>ori_y_d1情况下发现ori_y值大于row_end,不切换
*/
else if ((coord_req_y > coord_req_y_d1) && (coord_req_y <= sg_y_end) && (coord_req_y > sg_y_begin)) begin
//计算输入的原始图像坐标已切换至下一行
state_bram_rd <= BRAM_RD_WAIT_PING;
end
else begin
//不切换bram,仍使用pong
state_bram_rd <= BRAM_RD_WAIT_PONG;
end
end
endcase
end
default: begin
state_bram_rd <= BRAM_RD_WAIT_PING;
end
endcase
end
end
//更新sysgen参数
always @(posedge clk) begin
if (rst == 1'b1) begin
sg_z <= 32'hFFFF_FFFF;
sg_x0 <= 32'hFFFF_FFFF;
sg_y0 <= 32'hFFFF_FFFF;
sg_y_begin <= 16'hFFFF;
sg_y_end <= 16'hFFFF;
end
else begin
case ({state_bram_rd, bram_state_ping, backpressure_in, coord_x, coord_y})
{BRAM_RD_WAIT_PING, 1'b1, 1'b0, 16'd0, 16'd0}: begin
//1帧数据首次进入BRAM_RD_PING状态时用bram参数更新sg参数
sg_z <= bram_z;
sg_x0 <= bram_x0;
sg_y0 <= bram_y0;
sg_y_begin <= bram_y_begin;
sg_y_end <= bram_y_end;
end
default: begin
//保持
sg_z <= sg_z;
sg_x0 <= sg_x0;
sg_y0 <= sg_y0;
sg_y_begin <= sg_y_begin;
sg_y_end <= sg_y_end;
end
endcase
end
end
Block RAM 的可写可读状态根据前述状态机的控制:
always @(posedge clk) begin
if (rst == 1'b1) begin
bram_state_ping <= 1'b0;//默认为可写状态
end
else begin
case (bram_state_ping)
1'b0: begin
//见bram写接口控制
case ({wea_ping, addra_ping})
{1'b1, bram_x_end[9:0]}: begin
//下个时钟周期完成1行内最后1个像素点写入
bram_state_ping <= 1'b1;
end
default: begin
//保持
bram_state_ping <= bram_state_ping;
end
endcase
end
1'b1: begin
//即state_bram_rd在BRAM_RD_HOLD_PING状态下的转移条件
case (state_bram_rd)
BRAM_RD_HOLD_PING: begin
//前1个状态生成的lv送出全部的的坐标请求
case ({coord_en_d1, coord_en})
{1'b1, 1'b0}: begin
//完成1行坐标请求
if ((coord_y == 16'd0) && (coord_y_d1 != 16'd0)) begin
//完成一帧图像最后1行的请求
//状态机复位
bram_state_ping <= 1'b0;
end
else if ((coord_req_y > coord_req_y_d1) && (coord_req_y <= sg_y_end) && (coord_req_y > sg_y_begin)) begin
//计算输入的原始图像坐标已切换至下一行
bram_state_ping <= 1'b0;
end
else begin
//不切换bram,仍使用ping
//保持
bram_state_ping <= bram_state_ping;
end
end
endcase
end
default: begin
//保持
bram_state_ping <= bram_state_ping;
end
endcase
end
endcase
end
end
always @(posedge clk) begin
if (rst == 1'b1) begin
bram_state_pong <= 1'b0;//默认为可写状态
end
else begin
case (bram_state_pong)
1'b0: begin
//见bram写接口控制
case ({wea_pong, addra_pong})
{1'b1, bram_x_end[9:0]}: begin
//下个时钟周期完成1行内最后1个像素点写入
bram_state_pong <= 1'b1;
end
default: begin
//保持
bram_state_pong <= bram_state_pong;
end
endcase
end
1'b1: begin
//即state_bram_rd在BRAM_RD_HOLD_PONG状态下的转移条件
case (state_bram_rd)
BRAM_RD_HOLD_PONG: begin
//前1个状态生成的lv送出全部的的坐标请求
case ({coord_en_d1, coord_en})
{1'b1, 1'b0}: begin
//完成1行坐标请求
if ((coord_y == 16'd0) && (coord_y_d1 != 16'd0)) begin
//完成一帧图像最后1行的请求
//状态机复位
bram_state_pong <= 1'b0;
end
else if ((coord_req_y > coord_req_y_d1) && (coord_req_y <= sg_y_end) && (coord_req_y > sg_y_begin)) begin
//计算输入的原始图像坐标已切换至下一行
bram_state_pong <= 1'b0;
end
else begin
//不切换bram,仍使用pong
//保持
bram_state_pong <= bram_state_pong;
end
end
endcase
end
default: begin
//保持
bram_state_pong <= bram_state_pong;
end
endcase
end
endcase
end
end
模块的反压控制除了向数据流上游输出反压以外,还负责将下游的反压信号向上游传递:
//在数据流上级模块中将根据反压停止数据送出状态机
parameter BACKPRESSURE_THRESHOLD = 12'd768;
reg backpressure_out = 1'b0;
always @(posedge clk) begin
if (fifo_data_count >= BACKPRESSURE_THRESHOLD) begin
backpressure_out <= 1'b1;
end
else begin
backpressure_out <= backpressure_in;//向上游传递反压
end
end
sysgen 与 Verilog 的配合方法在于 sysgen 向 Verilog 请求数据,Verilog 根据请求数据的坐标向 sysgen 送入用于插值的数据。
Verilog 在 Block RAM 读状态机中产生向 sysgen 输入的 in_lv,其持续的时间即输出图像的列数目,sysgen 计算产生其输出像素点的坐标 (coord_x, coord_y),以及该像素点插值计算的输入像素点的坐标 (coord_req_x, coord_req_y),计算方法与前述的坐标转换一致。
插值计算过程全部使用浮点数,在输出之前转化为无符号整数输出。
插值计算的实现过程与双立方插值计算一致。
先进行坐标计算:
最后将 Block RAM 读入的并行数据与插值系数进行乘加完成插值计算。
由于数据接口比较复杂,不方便在 sysgen 环境下仿真,于是实机试验。
试验图片来源于视频截图:https://www.youku.com/
输入原始图像:
参数 z 值为 0.5 的放大图像:
参数 z 值为 0.3 的放大图像: