Verilog读取BMP图片并接入AXI-Stream仿真附DocNav的拙劣使用指南
BMP文件格式解析(带颜色表)及Verilog的AXI-Stream接入仿真(二):
在本文中,你将能看见:
- BMP文件解析后,粗糙的Verilog仿真搭建
- AXI-Stream验证IP
- 拙劣的DocNav使用指南
在上一节中,笔者解析了BMP文件格式,并用Verilog进行了初步的读取。在这节中,会将上一节读取的图像数据转换成AXI-Stream以便后续图像处理。本文也会给出现在很多技术公众号的“素材”来源
blog历史版本:
- 20220421:created
- 20220425:
- error fixed:图片存储索引错误
- 代码表述错误:修正tuser代码段复制错误
AXI-Stream视频流规定
第一步需要做的是明白视频流在AXI-Stream中是怎么规定的,
DocNav的拙劣讲解
在这里直接打开安装Vivado时“附送”的文档工具:DocNav(笔者高分屏稍微有点BUG):
直接打开Design Hub View:
可以看到xilinx家很直白地就把他家的器件和工具链做了一个简明的分类。然后我们要做图片、视频流转AXIS,所以直接跳到FPGA Design-> Video Design:
就可以看到我们需要的资料基本都在这了,这里我们直接打开第一个Design Guide就可以了,总的来说DocNav中有这里几个层次的文档:
- tutorial 入门文档,当中不少已经被各大公众号搬运了,更为甚至还翻译出书了
- user guide:针对器件、ip的文档,搬运这里的,就可能会被冠以“高手”的称号
- Overview、Product Guide:产品文档,好的图都能从这里找!
- Reference Guide、Reference 、Reference Manual:细节文档,本身也是手册性质的,这些一般见于以工作的大佬的博客中。
其他的文档基本就要再单独分析了,所以很欢迎各位靠这个软件变成一个技术高超的FPGAer博主!(doge
打开UG934(就是刚刚的第一个Design Guide):
EOL(End of Line)/ SOF(Start of Frame)时序
在介绍AXI-Stream的时候就讲过视频流了,在AXIS中使用tlast来表示视频流的一行结束,我们用这个文档中的时序图来看看具体的时序控制:
这里需要注意到:
- 在EOL被断言(拉高)后,VALID是需要在下一个时刻置低的,这一点后面仿真的时候也会验证。
- 无论是EOL还是SOF,拉高时数据通道依然是有效的,这个后面在算法处理的时候会使用到。
像素格式在数据通道的排布
这里直接看手册:
当一个时钟采样一个像素的时候:
后面还有一个时钟采样两个像素或多个像素的,这里就不提了,我们只要这里的RGBA就可以开展工作了。
Verilog读取BMP文件改造
思路是先用寄存器组代替fwrite,后面再用计数器往外读
fwrite改reg组
在上一节中,我们使用$fwrite将读取到的像素数据打到txt中:
在这里,我们使用4个reg组去把像素值保存下来备用:
reg [7:0] rBmpData_ch1 [0:total_size-1];
reg [7:0] rBmpData_ch2 [0:total_size-1];
reg [7:0] rBmpData_ch3 [0:total_size-1];
reg [7:0] rBmpData_ch4 [0:total_size-1];
......
1:begin
for (i = 0; i<iBmpHight; i=i+1) begin // 原序读(左下到右上)
// for (i = iBmpHight-1; i>=0; i=i-1) // 反序读(左上到右下)
for (j = 0; j< iBmpWidth; j=j+1)begin
iIndex = (i*iBmpWidth + j + iDataStartIndex*8)/8;
index_1 =~((i*iBmpWidth + j + iDataStartIndex*8)%8);
pixel_idx = rBmpData[iIndex][index_1*1 +: 1];
pixel_data = rBmpColor[pixel_idx];
rBmpData_ch1[i*iBmpWidth+j] = pixel_data[23:16];
rBmpData_ch2[i*iBmpWidth+j] = pixel_data[15:8];
rBmpData_ch3[i*iBmpWidth+j] = pixel_data[ 7:0];
end
end
end
这样图片读取完,数据就都在这三组寄存器组中了,可以将其中一组用$writememh做数据验证:
引入AXIS的验证IP(VIP)
在这里引入一个AXI的验证IP,以便后续仿真方便,在IP catalog中,搜索AXIS:
这里选择PASS THROUGH 然后把数据位宽改一下,然后打开tlast和tuser通道:
通过这种方式可以检验我们的代码握手和各个通道是否正确
手写一个AXIS的握手
在这里需要着重控制的信号线如下:
wire s_axis_tready; //输入
wire s_axis_tlast;
wire s_axis_tuser;
wire [23:0] s_axis_tdata;
reg s_axis_tvalid;
s_axis_tvalid控制
在AXI协议中,valid是应该自主控制的,所以我们这里直接给1就行了 吗?,我们在这里先直接给1:
然后就会在仿真中发现错误:
其中,报错信息代号能从ARM官网查询:AXI4-Stream-protocol-assertion-descriptions
接下来给他加一个初值再加一个初始延时:
reg s_axis_tvalid ;
reg [5:0] rst_cnt;
always @(posedge clk_i or negedge rst_n_i) begin
if(~rst_n_i)begin
s_axis_tvalid <= 1'b0;
end
else if(s_axis_tlast) begin
s_axis_tvalid <= 1'b0;
end
else if(rst_cnt == 6'd30) begin
s_axis_tvalid <= 1'b1;
end
else begin
s_axis_tvalid <= s_axis_tvalid;
end
end
always @(posedge clk_i or negedge rst_n_i) begin
if(~rst_n_i)begin
rst_cnt <= 6'd0;
end
else if(rst_cnt == 6'd30) begin
rst_cnt <= rst_cnt;
end
else begin
rst_cnt <= rst_cnt + 1'b1;
end
end
error就没有了,当然这里也会跟reset的控制相关,这个大家可以再探索一下.
s_axis_tlast 控制
这里我们使用一个计数器来写,当然这里的命名规范并不好,大家看看就好:
reg [$clog2(width)-1 :0] w_cnt;
always @(posedge clk_i or negedge rst_n_i) begin
if(~rst_n_i)begin
w_cnt <= 'b0;
end
else if(s_axis_tvalid && s_axis_tready && w_cnt < width-1 ) begin
w_cnt <= w_cnt + 1'b1;
end
else if(w_cnt == width-1) begin
w_cnt <= 'b0;
end
else begin
w_cnt <= w_cnt;
end
end
assign s_axis_tlast = (w_cnt == width-1) ? 1'b1: 1'b0 ;
s_axis_tuser 控制
这里再加一个行计数器,二者是0的时候断言s_axis_tuser:
reg [$clog2(height)-1 :0] h_cnt;
always @(posedge clk_i or negedge rst_n_i) begin
if(~rst_n_i)begin
h_cnt <= 'b0;
end
else if(s_axis_tlast) begin
h_cnt <= h_cnt + 1'b1;
end
else if(h_cnt == height-1) begin
h_cnt <= 'b0;
end
else begin
h_cnt <= h_cnt;
end
end
assign s_axis_tuser = (h_cnt == 0 && w_cnt == 0) ? 1'b1: 1'b0 ;
s_axis_tdata地址计算
这里直接assign一个寻址就可以了:
assign pixel_axi_data = {rBmpData_ch1[h_cnt*width + w_cnt],rBmpData_ch2[h_cnt*width + w_cnt] , rBmpData_ch3[h_cnt*width + w_cnt]};
把tb封装一下
最后我们稍微引线一下:
axi4stream_vip_0 m_axi4stream_vip_0 (
.aclk(clk_i), // input wire aclk
.aresetn(rst_n_i), // input wire aresetn
.s_axis_tvalid(s_axis_tvalid), // input wire [0 : 0] s_axis_tvalid
.s_axis_tready(s_axis_tready), // output wire [0 : 0] s_axis_tready
.s_axis_tdata(pixel_axi_data), // input wire [23 : 0] s_axis_tdata
.s_axis_tlast(s_axis_tlast), // input wire [0 : 0] s_axis_tlast
.s_axis_tuser(s_axis_tuser), // input wire [0 : 0] s_axis_tuser
.m_axis_tvalid(m_axis_tvalid), // output wire [0 : 0] m_axis_tvalid
.m_axis_tready(m_axis_tready), // input wire [0 : 0] m_axis_tready
.m_axis_tdata(m_axis_tdata), // output wire [23 : 0] m_axis_tdata
.m_axis_tlast(m_axis_tlast), // output wire [0 : 0] m_axis_tlast
.m_axis_tuser(m_axis_tuser) // output wire [0 : 0] m_axis_tuser
);
这里我们把主端用端口封出去,再稍微拓展一下:
module bmp_tb#( parameter data_out = "./b.txt",
parameter bmp_path = "./a.bmp",
parameter height = 3840,
parameter width = 2160
)(
input clk_i,
input rst_n_i,
// AXIS
output [0:0] m_axis_tvalid, // output wire [0 : 0] m_axis_tvalid
input [0:0] m_axis_tready, // input wire [0 : 0] m_axis_tready
output [23:0] m_axis_tdata, // output wire [7 : 0] m_axis_tdata
output [0:0] m_axis_tlast, // output wire [0 : 0] m_axis_tlast
output [0:0] m_axis_tuser // output wire [0 : 0] m_axis_tuser
);
以后的调用就比较简单了,这里生成一个tb去测试这个tb,依然非常简单:
module bmp_axi_tb_top();
// Parameters
localparam data_out = "./test_ch.txt";
localparam bmp_path = "./test.bmp";
localparam height = 256;
localparam width = 256;
// Ports
reg clk_i = 0;
reg rst_n_i = 1;
// AXI-Stream Ports
wire [0:0] m_axis_tvalid;
reg [0:0] m_axis_tready=1'b0;
wire [23:0] m_axis_tdata;
wire [0:0] m_axis_tlast;
wire [0:0] m_axis_tuser;
bmp_tb #(
.data_out(data_out ),
.bmp_path(bmp_path ),
.height(height ),
.width (width )
)
bmp_tb_dut (
.clk_i (clk_i ),
.rst_n_i (rst_n_i ),
.m_axis_tvalid (m_axis_tvalid ),
.m_axis_tready (m_axis_tready ),
.m_axis_tdata (m_axis_tdata ),
.m_axis_tlast (m_axis_tlast ),
.m_axis_tuser ( m_axis_tuser)
);
initial begin
begin
#10 rst_n_i = 1'b0;
#200 rst_n_i = 1'b1;
#495 m_axis_tready = 1'b1;
#500000000;
$finish;
end
end
always begin
#5 clk_i = ! clk_i ;
end
endmodule
仿真结果
将原来的信号全干掉,找到刚刚的那个ip,把ip全线拉出来:
具体的波形长这样:
检查一下边角数据:
结语
通过这个仿真,我们就可以将图片导入到verilog中进行处理了,下一节中我们会将我们处理完的AXIS流再保存为bmp文件,这样的话我们就可以完成整个FPGA图像处理验证平台的搭建了。
that all,thanks.