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

19 篇文章 34 订阅
18 篇文章 4 订阅

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中有这里几个层次的文档:

  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*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.

  • 3
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
以下是一个简单的FIFO转AXI-StreamVerilog代码: ``` module fifo_to_axi_stream ( input clk, input rst, input [31:0] data_in, input wr_en, output [31:0] data_out, output axi_tvalid, output axi_tlast, output axi_tready ); // AXI-Stream interface signals reg [31:0] axi_data; reg axi_tvalid; reg axi_tlast; wire axi_tready; // FIFO signals reg [31:0] fifo [0:7]; reg [2:0] rd_ptr = 0; reg [2:0] wr_ptr = 0; reg [2:0] count = 0; reg full = 0; reg empty = 1; // Write data to FIFO always @(posedge clk) begin if (rst) begin wr_ptr <= 0; count <= 0; full <= 0; empty <= 1; end else if (wr_en && !full) begin fifo[wr_ptr] <= data_in; wr_ptr <= wr_ptr + 1; count <= count + 1; full <= (count == 8); empty <= 0; end end // Read data from FIFO and send to AXI-Stream always @(posedge clk) begin if (rst) begin rd_ptr <= 0; axi_data <= 0; axi_tvalid <= 0; axi_tlast <= 0; end else if (!empty && axi_tready) begin axi_data <= fifo[rd_ptr]; rd_ptr <= rd_ptr + 1; count <= count - 1; full <= 0; empty <= (count == 0); axi_tvalid <= 1; axi_tlast <= (count == 1); end else begin axi_tvalid <= 0; axi_tlast <= 0; end end // Assign output signals assign data_out = axi_data; assign axi_tready = 1; endmodule ``` 在这个模块中,我们使用了一个8个元素的FIFO,以将输入数据从一个时钟域传递到另一个时钟域。输入数据通过`data_in`信号传递,并且在`wr_en`信号为高电平时写入FIFO。输出数据通过`data_out`信号传递,以便在AXI-Stream时钟域中使用。 AXI-Stream接口由三个信号组成:数据信号(axi_data),有效标志(axi_tvalid)和末尾标志(axi_tlast)。在本例中,我们使用一个简单的流水线协议,其中我们在每个时钟周期中传输一个32位数据。有效标志表示当前传输的数据是否有效,末尾标志表示当前传输的数据是否为流的最后一个数据。在本例中,我们使用FIFO中的数据计数来确定何时传输最后一个数据。 请注意,我们使用了同步复位信号(rst)来确保在系统启动时所有信号都处于已知状态。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小何的芯像石头

谢谢你嘞,建议用用我的链接

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值