图像流AXI-Stream生成BMP文件
BMP文件格式解析(带颜色表)及Verilog的AXI-Stream接入仿真(三):
在本文中,你将能看见:
- 将图像AXI-Stream流转换成BMP文件格式流输出
- 此模块为可综合设计!后续或许可以通过这种方式走pcie把图像回传或存在SD卡中。
- 此模块处理为固定图像大小,想根据AXIS流调整大小的可以自行改改
- btw,上一篇教程出了一些问题,建议查看最新勘误版
在完成这篇后,基于FPGA的图像处理验证平台就搭建完了,可以肆意在这个IO中间加各种算法,然后丢回Matlab做验证了。
实现思路
在实现上,由于bmp除去文件头后也只是把图像流数据按顺序放而已,所以这里
- 先用一个fifo缓存图像数据
- 写一个状态机控制按顺序输出文件头和数据。
- 注意fifo的读写和AXIS之间的握手和控制逻辑。因为看起来fifo是暂存数据的,但可预见fifo应该是有可能周期性空的,因为在每行的结束后tlast都是让valid拉低一个周期,这一小个周期在行多了之后一点会抵消文件头的大小。
生成缓存fifo
声明:
- 命名并不规范
- 可以用原语(xpm)或其他同类型IP生成,这里不多赘述。
在IP catalog的搜索框中写fifo,选FIFO Generator:
具体按下面设置:
这里注意必须选择First Word Fall Through
选25位是因为,在数据结构上是1tuser + 2*8 data,选择把帧开始标志也丢进fifo可以避免错帧。
总体端口
生成的BMP文件依然以AXIS格式输出,在tb中再以二进制格式写进文件:
module axis2bmp#(
parameter PIC_HEIGHT = 1080,
parameter PIC_WIDTH = 1920
)(
// global signal
input clk_i, // clock
input rst_n_i, // reset
// axi stream (slave) interface signal -> pixel data
input [23:0] s_axis_video_tdata, // DATA
input [0:0] s_axis_video_tvalid, // VALID
output [0:0] s_axis_video_tready, // READY
input [0:0] s_axis_video_tuser, // SOF
input [0:0] s_axis_video_tlast, // EOL
// axi stream (master) interface signal -> bmp
output reg [23:0] m_axis_video_tdata, // DATA
output [ 0:0] m_axis_video_tvalid, // VALID
input [ 0:0] m_axis_video_tready, // (meaningless)
output [ 0:0] m_axis_video_tlast // end of file stream
);
slave端为图像数据,master端为输出BMP文件流,这里需要注意master流中并不处理反压问题(即没有ready信号,懒得加fifo)
fifo接口逻辑
// image pixel fifo dw=24, BRAM cap=512
wire [24:0] bmp_header_din;
wire [0:0] bmp_header_wr;
wire [0:0] bmp_header_full;
wire [0:0] bmp_header_empty;
wire [0:0] bmp_header_rd;
wire [24:0] bmp_header_dout;
bmp_header bmp_header_inst (
.clk(clk_i), // input wire clk
.srst(~rst_n_i), // input wire srst
.din(bmp_header_din), // input wire [23 : 0] din
.wr_en(bmp_header_wr), // input wire wr_en
.rd_en(bmp_header_rd), // input wire rd_en
.dout(bmp_header_dout), // output wire [23 : 0] dout
.full(bmp_header_full), // output wire full
.empty(bmp_header_empty) // output wire empty
);
// pixel fifo assignment
assign bmp_header_din = {s_axis_video_tuser,s_axis_video_tdata};
assign s_axis_video_tready = ~bmp_header_full;
assign bmp_header_wr = s_axis_video_tready && s_axis_video_tvalid;
fifo的读使能放到后面再讲,这里先处理好数据进来就可以了
文件流处理状态机
经典三板斧,不展开
包头数据准备
需要搬回第一篇中的BMP文件格式,由于是输出,所以我们就不考虑调色板了:
这里先用一些localparam存起来,(这里考虑大小不变)
//--------------------------写BMP状态机------------------------
// local parameter
localparam [15:0] bfType = 16'h4d42;
localparam [31:0] bfReserved = 32'h0000_0000;
localparam [31:0] biSizeImage = PIC_HEIGHT * PIC_WIDTH * 3;
localparam [31:0] biSizeImage_cnt = PIC_HEIGHT * PIC_WIDTH;
localparam [31:0] bfOffset = 32'd54;
localparam [31:0] bfSize = biSizeImage + bfOffset;
localparam [31:0] biSize = 32'h28;
localparam [31:0] biWidth = PIC_WIDTH;
localparam [31:0] biHeight = PIC_HEIGHT;
localparam [15:0] biPlanes = 16'd1;
localparam [15:0] biBitCount = 16'd24;
localparam [31:0] biCompression = 32'd0;
localparam [127:0] biUseless = 128'd0;
localparam CNT_PIXEL = $clog2(PIC_HEIGHT*PIC_WIDTH);
转移状态
//转移状态
localparam S_WAIT = 3'b001 ; // 等待SOF标记
localparam S_WRITE_HEADER = 3'b010 ; // 写BMP包头
localparam S_WRITE_DATA = 3'b100 ; // 写BMP数据
状态转移变量
//状态转移变量
reg [2:0] state, n_state; // 状态寄存器
reg [4:0] header_cnt; // 包头计数器
reg [CNT_PIXEL-1:0] pixel_cnt; // 像素计数器
wire frame_start = bmp_header_dout[24]; // SOF flag
wire write_header_done = (header_cnt == 5'd17); // 18 -1 -> 18*3
wire write_pixel_done = (pixel_cnt == biSizeImage_cnt -1'b1);
这里需要注意 : 两个状态只由计数器指定跳转
状态转移
//状态机初始化
always @ (posedge clk_i) begin
if(~rst_n_i)
state <= S_WAIT;
else
state <= n_state;
end
状态机 状态转移
always @ (*) begin
case(state)
S_WAIT :
if(frame_start)
n_state = S_WRITE_HEADER;
else
n_state = S_WAIT;
S_WRITE_HEADER:
if(write_header_done)
n_state = S_WRITE_DATA;
else
n_state = S_WRITE_HEADER;
S_WRITE_DATA:
if(write_pixel_done)
n_state = S_WAIT;
else
n_state = S_WRITE_DATA;
default:
n_state = S_WAIT;
endcase
end
写BMP包头 处理逻辑
这里直接按照文件格式,用计数器怼进去进行:
always @(posedge clk_i or negedge rst_n_i) begin
if (~rst_n_i)
header_cnt <= 5'd0;
else if(state == S_WRITE_HEADER && header_cnt < 5'd17)
header_cnt <= header_cnt + 1'd1;
else
header_cnt <= 5'd0;
end
在数据上,可参考(注意数据以小端输出):
case (header_cnt)
5'd0 :
m_axis_video_tdata = {bfSize[0+:8], bfType};
5'd1 :
m_axis_video_tdata = bfSize[8+:24];
5'd2 :
m_axis_video_tdata = bfReserved[0 +:24];
5'd3 :
m_axis_video_tdata = {bfOffset[0+:16],bfReserved[24+:8]};
5'd4 :
m_axis_video_tdata = {biSize[0+:8], bfOffset[16+:16]};
5'd5 :
m_axis_video_tdata = biSize[8+:24];
5'd6 :
m_axis_video_tdata = biWidth[0+:24];
5'd7 :
m_axis_video_tdata = {biHeight[0+:16],biWidth[24+:8]};
5'd8 :
m_axis_video_tdata = {biPlanes[0+:8],biHeight[16+:16]};
5'd9 :
m_axis_video_tdata = {biBitCount[0+:16],biPlanes[8+:8]};
5'd10 :
m_axis_video_tdata = biCompression[0+:24];
5'd11 :
m_axis_video_tdata = {biSizeImage[0+:16],biCompression[24+:8]};
5'd12 :
m_axis_video_tdata = {biUseless[0+:8], biSizeImage[16+:16]};
5'd13 :
m_axis_video_tdata = biUseless[8+:24];
5'd14 :
m_axis_video_tdata = biUseless[32+:24];
5'd15 :
m_axis_video_tdata = biUseless[56+:24];
5'd16 :
m_axis_video_tdata = biUseless[80+:24];
5'd17 :
m_axis_video_tdata = biUseless[104+:24];
default:
m_axis_video_tdata = 24'heeeeee;
endcase
其中+:和-:语法简介可以翻看笔者之前的文章或自行百度。这里这样写是为了看位宽方便一点
写图像数据
这里直接放开fifo数据就可以了,注意握手逻辑:
计数器逻辑:
always @(posedge clk_i or negedge rst_n_i) begin
if (~rst_n_i)
pixel_cnt <= 'd0;
else if(state == S_WRITE_DATA && pixel_cnt < biSizeImage_cnt-1) begin
if(bmp_header_empty)
pixel_cnt <= pixel_cnt;
else
pixel_cnt <= pixel_cnt + 1'd1;
end
else
pixel_cnt <= 'd0;
end
数据放行:
assign bmp_header_rd = ((state == S_WRITE_DATA) && ~bmp_header_empty)
|| ((state == S_WAIT) && ~frame_start);
………………
else if(state == S_WRITE_DATA) begin
m_axis_video_tdata = bmp_header_dout;
end
………………
这里上下两块合成一个组合逻辑就变成m_axis_video_tdata的完整控制逻辑了
仿真tb编写
这里由前面的积累就比较简单,直接将上一节生成的AXIS输入例化即可:
定义localparam
// Parameters
localparam data_out = "./a_ch.txt";
localparam bmp_path = "./test.bmp";
localparam bmp_path_out = "./test_out.bmp";
localparam height = 1080;
localparam width = 1920;
这里如果输入输出不一样可以在tb中分开定义
简单引个线
// AXI-Stream Ports
wire [0:0] m_axis_tvalid;
wire [0:0] m_axis_tready;
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)
);
wire [23:0] m_axis_video_tdata; //这才是想要的信号
wire [ 0:0] m_axis_video_tvalid;
wire [ 0:0] m_axis_video_tlast;
axis2bmp
#(
.PIC_HEIGHT(height ),
.PIC_WIDTH (width )
)
axis2bmp_dut (
.clk_i (clk_i ),
.rst_n_i (rst_n_i ),
.s_axis_video_tdata (m_axis_tdata ),
.s_axis_video_tvalid (m_axis_tvalid ),
.s_axis_video_tready (m_axis_tready ),
.s_axis_video_tuser (m_axis_tuser ),
.s_axis_video_tlast (m_axis_tlast ),
.m_axis_video_tdata (m_axis_video_tdata ),
.m_axis_video_tvalid (m_axis_video_tvalid ),
.m_axis_video_tready (1'b1 ),
.m_axis_video_tlast ( m_axis_video_tlast)
);
文件流写入
这里直接用$fwrite一个一个字节写进去就可以了:
integer iBmpFileId;
initial begin
begin
iBmpFileId = $fopen(bmp_path_out,"wb");
#10 rst_n_i = 1'b0;
#200 rst_n_i = 1'b1;
while (!m_axis_video_tlast) begin
@(negedge clk_i) ;
if(m_axis_video_tvalid==1'b1)begin
$fwrite(iBmpFileId, "%c", m_axis_video_tdata[7-:8]) ;
$fwrite(iBmpFileId, "%c", m_axis_video_tdata[15-:8]) ;
$fwrite(iBmpFileId, "%c", m_axis_video_tdata[23-:8]);
end
end
$fclose(iBmpFileId);
#2000;
$finish;
end
end
然后在sim_x-> behav -> xsim下面放好图像就可以了
这个时候 如果跟着当年教程做的话,你肯定很会好奇
上一篇教程出错啦哈哈哈哈哈哈哈哈哈哈,不过如果你是最近才看这系列的话就已经被修正了,如果有兴趣可以去公众号的"错误文章"这个tag下找一下经典错文。这里分享一下排错过程,原图是这样的
根据上面的步骤后,得到的图像是这样的:
很显然就是读入到reg的时候索引错了,看样子应该就能排查出原来写入的:
rBmpData_ch3[i*iBmpHight+j] = pixel_data[ 7:0];
这里应该按行来存储,即:
rBmpData_ch3[i*iBmpWidth+j] = pixel_data[ 7:0];
把相关的逻辑清理完之后图像就正常了!
如果复现有问题的话
一般应该是文件头的逻辑出现错误,这里推荐直接开那些查看Hex文件的软件进行对比: 一个一个对比就好了,至于计数器错误的话笔者就没办法了
结语
这个验证平台终于做完咯,谢谢大家!
后面会先从一些图像处理的小部件开始整起(填充,行缓冲器等)讲起,再开始整相关算法。
thanks
|