0
点赞
收藏
分享

微信扫一扫

Verilog读取BMP图片并接入AXI-Stream仿真附DocNav的拙劣使用指南

Verilog读取BMP图片并接入AXI-Stream仿真附DocNav的拙劣使用指南

BMP文件格式解析(带颜色表)及Verilog的AXI-Stream接入仿真(二):

在本文中,你将能看见:

  • BMP文件解析后,粗糙的Verilog仿真搭建
  • AXI-Stream验证IP
  • 拙劣的DocNav使用指南

文章目录


在上一节中,笔者解析了BMP文件格式,并用Verilog进行了初步的读取。在这节中,会将上一节读取的图像数据转换成AXI-Stream以便后续图像处理。本文也会给出现在很多技术公众号的“素材”来源

AXI-Stream视频流规定

第一步需要做的是明白视频流在AXI-Stream中是怎么规定的,

DocNav的拙劣讲解

在这里直接打开安装Vivado时“附送”的文档工具:DocNav(笔者高分屏稍微有点BUG):

直接打开Design Hub View:

可以看到xilinx家很直白地就把他家的器件和工具链做了一个简明的分类。然后我们要做图片、视频流转AXIS,所以直接跳到FPGA Design-> Video Design:

就可以看到我们需要的资料基本都在这了,这里我们直接打开第一个Design Guide就可以了,总的来说DocNav中有这里几个层次的文档:

  1. tutorial 入门文档,当中不少已经被各大公众号搬运了,更为甚至还翻译出书了
  2. user guide:针对器件、ip的文档,搬运这里的,就可能会被冠以“高手”的称号
  3. Overview、Product Guide:产品文档,好的图都能从这里找!
  4. Reference Guide、Reference 、Reference Manual:细节文档,本身也是手册性质的,这些一般见于以工作的大佬的博客中。

其他的文档基本就要再单独分析了,所以很欢迎各位靠这个软件变成一个技术高超的FPGAer博主!(doge

打开UG934(就是刚刚的第一个Design Guide):

EOL(End of Line)/ SOF(Start of Frame)时序

在介绍AXI-Stream的时候就讲过视频流了,在AXIS中使用tlast来表示视频流的一行结束,我们用这个文档中的时序图来看看具体的时序控制:

这里需要注意到:

  1. 在EOL被断言(拉高)后,VALID是需要在下一个时刻置低的,这一点后面仿真的时候也会验证。
  2. 无论是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*iBmpHight+j] = pixel_data[23:16];
                    rBmpData_ch2[i*iBmpHight+j] = pixel_data[15:8];
                    rBmpData_ch3[i*iBmpHight+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_tlast = (w_cnt ==  width-1) ? 1'b1: 1'b0 ;

s_axis_tdata地址计算

这里直接assign一个寻址就可以了:

assign pixel_axi_data = {rBmpData_ch1[h_cnt*height + w_cnt],rBmpData_ch2[h_cnt*height + w_cnt] , rBmpData_ch3[h_cnt*height + 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.

欢迎关注同名wx

举报

相关推荐

0 条评论