1.BMP介绍
BMP(Bitmap)是一种用于存储位图图像的文件格式,广泛应用于 Windows 操作系统中。BMP 文件可以存储高质量的图像数据,包括颜色深度较高的图片,同时支持无压缩或可选的简单压缩方式。
BMP格式:
文件头(14 字节):
- 文件类型:2 字节,通常为 
BM。 - 文件大小:4 字节,整个 BMP 文件的大小。
 - 保留字段:4 字节,保留字段,值为 0。
 - 像素数据偏移量:4 字节,图像数据开始的位置。
 
信息头(40 字节,常见的 BITMAPINFOHEADER):
- 头大小:4 字节,信息头的大小。
 - 图像宽度:4 字节,以像素为单位。
 - 图像高度:4 字节,以像素为单位(正数表示自下而上,负数表示自上而下)。
 - 颜色平面数:2 字节,通常为 1。
 - 每像素位数:2 字节,表示颜色深度,常见值为 24。
 - 压缩方式:4 字节,通常为 0 表示无压缩。
 - 图像大小:4 字节,图像数据的大小,压缩时有效。
 - 水平分辨率:4 字节,水平方向像素每米数。
 - 垂直分辨率:4 字节,垂直方向像素每米数。
 - 使用颜色数:4 字节,调色板中使用的颜色数。
 - 重要颜色数:4 字节,调色板中重要的颜色数。
 
图像数据:包含每个像素的颜色值,对于 24 位 BMP,每个像素由 3 个字节组成,分别表示红色、绿色和蓝色。
示例:
 
Tip:
- BMP的54个字节的文件头都是小端模式,即36_00_03_00实际的十六进制是(30036)h = (196,662)d = (256(宽)* 256 (高)*3+对齐字节+信息头)
 - BMP图像的每行需要是4的倍数(即对齐),如果不够需要填充0,如上图256%4 = 0,所以196,662 = (256(宽)* 256 (高)*3(字节)+0+54)
 - BMP中图像数据的存储是从左下开始的,即坐标(width:0,height:height)开始存贮的,而matlab中imread函数或者软件打开bmp查看数值时(如上图)是从左下读的,所以不用考虑存储的位置
 
2.BMP转VGA时序
首先,在verilog中是无法直接读取bmp图片的,需要将bmp通过脚本将其转为.dat文件,然后使用$readmemh函数去读取到数组中后,根据模拟的时钟信号将数据发出,下面是转bmp图片转dat文件的matlab代码:
注意:以下所有代码都是针对仿真的时候使用,且都需要替换文件名和路径!!!
clear;%从工作区中删除项目释放内存
clc;
close all;%关闭所有窗口
%main
% 1.读取图片数据
img = imread('1.bmp');
% imread函数两个参数:文件路径和读取格式
imtool(img,[]);
[h, w, ~] = size(img);      % 获取图像高度、宽度
fid = fopen('image_data.dat', 'w');
% 遍历图像数据,将其保存为十六进制形式
for i = h:-1:1              % 从底部开始
    for j = 1:w
        pixel_val = img(i,j,:);  % 获取每个像素的RGB值
        r = pixel_val(1);        % 红色通道
        g = pixel_val(2);        % 绿色通道
        b = pixel_val(3);        % 蓝色通道
        fprintf(fid, '%02X%02X%02X\n', r, g, b);  % 输出为 6 位十六进制,RGB 顺序
    end
end
 
当生成.dat文件之后,就可以直接将时序数据转换成VGA/HDMI 时序,如下所示:
module SIM_Generate_Data
#(
    parameter           P_CLK_PERIOD        = 5      ,    // 5ns
    parameter           P_RST_DURATION      = 50     ,    // 100ns
    parameter           P_H_MAX_CNT         = 800    ,    // 8bit
    parameter           P_H_SYNC            = 96     ,   
    parameter           P_H_FRONT           = 144    ,   //96  40 8
    parameter           P_V_MAX_CNT         = 525    ,   // 8bit
    parameter           P_V_SYNC            = 35     ,   //2  25  8
    parameter           P_V_FRONT           = 144    , 
    parameter           P_SCREEN_WIDTH      = 640    ,   //96  40 8 640 8 8  = 800
    parameter           P_SCREEN_HEIGHT     = 480    ,   //2  25  8 480 8 2  = 525
    parameter           P_IMAGE_WIDTH       = 256    ,
    parameter           P_IMAGE_HEIGHT      = 256    ,
    parameter           P_BACKFROUND_COLOR  = 24'h000000
)
(   
    output  wire            o_clk           ,
    output  wire            o_rst_n         ,
    output  wire    [7:0]   o_r_channel     ,
    output  wire    [7:0]   o_g_channel     ,
    output  wire    [7:0]   o_b_channel     ,
    output  reg             o_hsync         ,
    output  reg             o_vsync    
);
    
localparam      P_H_IMG_START = P_H_FRONT+P_SCREEN_WIDTH/2-P_IMAGE_WIDTH/2-1;
localparam      P_H_IMG_END   = P_H_FRONT+P_SCREEN_WIDTH/2+P_IMAGE_WIDTH/2-1;
localparam      P_V_IMG_START = P_V_FRONT+P_SCREEN_HEIGHT/2-P_IMAGE_HEIGHT/2-1;
localparam      P_V_IMG_END   = P_V_FRONT+P_SCREEN_HEIGHT/2+P_IMAGE_HEIGHT/2-1;
    
initial begin
    r_clk   = 1'b0;
    r_rst_n = 1'b0;
    #(P_RST_DURATION)r_rst_n = 1'b1;
end
    
initial begin
    $readmemh("../../../../../sim_data/image_data.dat", r_image_data);
    $display("Tip\:last image data read from file: %h",r_image_data[P_IMAGE_WIDTH*P_IMAGE_HEIGHT-1]);
end
    
always #(P_CLK_PERIOD/2) r_clk = ~r_clk;
reg                 r_clk           ;
reg                 r_rst_n         ;
reg     [23:0]      r_image_data  [P_IMAGE_WIDTH*P_IMAGE_HEIGHT-1:0];
reg     [15:0]      r_width_cnt     ;
reg     [15:0]      r_height_cnt    ;
reg     [7:0]       r_r_channel     ;
reg     [7:0]       r_g_channel     ;
reg     [7:0]       r_b_channel     ;
reg                 r_img_hsync     ;
reg                 r_img_vsync     ;
reg     [15:0]      r_h_cnt         ;
reg     [15:0]      r_v_cnt         ;
reg     [15:0]      r_total_cnt     ;
reg     [7:0]       or_r_channel    ;
reg     [7:0]       or_g_channel    ;
reg     [7:0]       or_b_channel    ;
reg     [7:0]       or_r_channel_1d ;
reg     [7:0]       or_g_channel_1d ;
reg     [7:0]       or_b_channel_1d ;
assign  o_clk       =   r_clk        ;
assign  o_rst_n     =   r_rst_n      ;
assign  o_r_channel =   or_r_channel_1d;
assign  o_g_channel =   or_g_channel_1d;
assign  o_b_channel =   or_b_channel_1d;
always @(posedge r_clk) begin
    if (!r_rst_n) begin
        o_hsync <= 1'b0;
        o_vsync <= 1'b0;
    end else begin
        o_hsync <= r_hsync;
        o_vsync <= r_vsync;
    end
end
//gererate r_hsync,r_vsync
always @(posedge r_clk,negedge r_rst_n) begin
    if(!r_rst_n)
        r_h_cnt <= 16'd0;
    else if(r_h_cnt == P_H_MAX_CNT-1) 
        r_h_cnt <= 16'd0;
    else
        r_h_cnt <= r_h_cnt + 16'd1;
end
always @(posedge r_clk,negedge r_rst_n) begin
    if(!r_rst_n)
        r_v_cnt <= 16'd0;
    else if(r_v_cnt == P_V_MAX_CNT-1 && r_h_cnt == P_H_MAX_CNT-1) 
        r_v_cnt <= 16'd0;
    else if(r_h_cnt == P_H_MAX_CNT-1)
        r_v_cnt <= r_v_cnt + 16'd1;
    else
        r_v_cnt <= r_v_cnt;
end
always @(posedge r_clk,negedge r_rst_n) begin
    if(!r_rst_n)
        r_img_hsync <= 1'b0;
    else if(r_h_cnt < P_H_SYNC)
        r_img_hsync <= 1'b1;
    else
        r_img_hsync <= 1'b0;
end
always @(posedge r_clk , negedge r_rst_n) begin
    if(!r_rst_n)begin
        r_vsync <= 1'b0;
    end else if(r_v_cnt >P_V_IMG_START && r_v_cnt <= P_V_IMG_END)begin
        r_vsync <= 1'b1;
    end else begin
        r_vsync <= 1'b0;
    end
end
always @(posedge r_clk,negedge r_rst_n) begin
    if(!r_rst_n)
        r_img_vsync <= 1'b0;
    else if(r_v_cnt < P_V_SYNC)
        r_img_vsync <= 1'b1;
    else
        r_img_vsync <= 1'b0;
end
always @(posedge r_clk,negedge r_rst_n) begin
    if(!r_rst_n)begin
        r_width_cnt <= 16'd0;
        r_height_cnt <= 16'd0;
        r_r_channel <= 8'd0;
        r_g_channel <= 8'd0;
        r_b_channel <= 8'd0;
    end else if(r_h_cnt > P_H_IMG_START && r_h_cnt <= P_H_IMG_END && r_v_cnt > P_V_IMG_START && r_v_cnt <= P_V_IMG_END)begin  //show image data
        r_r_channel <= r_image_data[r_width_cnt+r_height_cnt*P_IMAGE_WIDTH][23:16];
        r_g_channel <= r_image_data[r_width_cnt+r_height_cnt*P_IMAGE_WIDTH][15:8];
        r_b_channel <= r_image_data[r_width_cnt+r_height_cnt*P_IMAGE_WIDTH][7:0];
        r_width_cnt <= r_width_cnt + 16'd1;
        if(r_h_cnt == P_H_IMG_END )
            r_height_cnt = r_height_cnt + 16'd1;
        else
            r_height_cnt = r_height_cnt;
    end else if(r_h_cnt > P_H_FRONT-1 && r_h_cnt < (P_H_FRONT+P_SCREEN_WIDTH) && r_v_cnt > P_V_FRONT-1 && r_v_cnt < (P_V_FRONT+P_SCREEN_HEIGHT))begin  //show background color)begin
        r_r_channel <= P_BACKFROUND_COLOR[23:16];
        r_g_channel <= P_BACKFROUND_COLOR[15:8];
        r_b_channel <= P_BACKFROUND_COLOR[7:0];
    end
    else begin
        r_width_cnt <= 16'd0;
        r_r_channel <= 8'd0;
        r_g_channel <= 8'd0;
        r_b_channel <= 8'd0;
    end
end
//actully generated image data output
always @(posedge r_clk,negedge r_rst_n) begin
    if(!r_rst_n)begin
        or_r_channel <= 8'd0;
        or_g_channel <= 8'd0;
        or_b_channel <= 8'd0;
        r_hsync <= 1'b0;
    end else if(r_h_cnt > P_H_IMG_START && r_h_cnt <= P_H_IMG_END && r_v_cnt > P_V_IMG_START && r_v_cnt <= P_V_IMG_END)begin  //show image data
        r_hsync <= 1'b1;
        or_r_channel <= r_image_data[r_width_cnt+r_height_cnt*P_IMAGE_WIDTH][23:16];
        or_g_channel <= r_image_data[r_width_cnt+r_height_cnt*P_IMAGE_WIDTH][15:8];
        or_b_channel <= r_image_data[r_width_cnt+r_height_cnt*P_IMAGE_WIDTH][7:0];
    end else begin
        r_hsync <= 1'b0;
        or_r_channel <= 8'd0;
        or_g_channel <= 8'd0;
        or_b_channel <= 8'd0;
    end
end
always @(posedge r_clk,negedge r_rst_n) begin
    if(!r_rst_n)begin
        or_r_channel_1d <= 8'd0;
        or_g_channel_1d <= 8'd0;
        or_b_channel_1d <= 8'd0;
    end else begin
        or_r_channel_1d <= or_r_channel;
        or_g_channel_1d <= or_g_channel;
        or_b_channel_1d <= or_b_channel;
    end
end
always @(posedge r_clk,negedge r_rst_n) begin
    if(!r_rst_n)begin
        r_total_cnt <= 16'd0;
    end else if(!r_vsync)
        r_total_cnt <= 16'd0;
    else if(r_hsync && r_vsync)
        r_total_cnt <= r_total_cnt + 16'd1;
    else
        r_total_cnt <= r_total_cnt;
end
endmodule
 
3.将VGA时序转成BMP 图片
在转的时候,同时生成了.dat文件,如果生成的bmp不对,可以查看对应的.dat与原.dat文件的差异,也可以直接比较生成的BMP文件差异。

module SIM_output_image_TB
#(
    parameter                   P_DATA_WIDTH = 8    ,
    parameter                   P_IMG_WIDTH  = 256  ,
    parameter                   P_IMG_HEIGHT = 256
)
(
    input                       i_clk    ,
    input                       i_rst_n  ,
    input                       i_h_sync ,
    input                       i_v_sync ,
    input  [P_DATA_WIDTH-1:0]   i_data  
);
// 定义输入信号
reg                             ri_h_sync       ,
                                ri_v_sync       ;
reg         [P_DATA_WIDTH-1:0]  ri_data         ;
wire                            w_valid_signal  ;
assign w_valid_signal = ri_h_sync && ri_v_sync  ;
always @(posedge i_clk or negedge i_rst_n) begin
    if (!i_rst_n) begin
        ri_h_sync <= 'd0;
        ri_v_sync <= 'd0;
        ri_data   <= 'd0;
    end else begin
        ri_h_sync <= i_h_sync;
        ri_v_sync <= i_v_sync;
        ri_data   <= i_data  ;
    end
end
/*--------------大端模式转小端模式----------------*/
//bmp采取的是小端模式
function [31:0] big_convert_little_endian(
    input   [31:0] fi_data 
);
begin
    big_convert_little_endian[7:0]   = fi_data[31:24] ;
    big_convert_little_endian[15:8]  = fi_data[23:16] ;
    big_convert_little_endian[23:16] = fi_data[15:8]  ;
    big_convert_little_endian[31:24] = fi_data[7:0]   ;
end
endfunction
/*--------------宽度4字节对齐----------------*/
/*
在 BMP 文件中,图像数据的每一行必须是 4 字节对齐的,这意味着每行的数据大小必须是 4 的倍数。如果图像的每行像素数据没有达到 4 字节的倍数,就需要在每行的末尾添加一些填充字节,使其达到 4 字节的倍数
*/
function [31:0] f_image_width_align(
    input [31:0] fi_image_width
);
    if(fi_image_width>0)begin
        f_image_width_align = fi_image_width + (4-((fi_image_width * 3) % 4))%4;
    end else
        $display("image_width is less than or equal to zero!");
endfunction
/*--------------bmp文件写入----------------*/
task bmp_file_write(
    input   integer  ti_fp,
    input   [23:0]   ti_image_data
);
    begin
        $fwrite(ti_fp, "%c%c%c", ti_image_data[7:0],ti_image_data[15:8],ti_image_data[23:16]);
    end
endtask
/*--------------bmp文件关闭----------------*/
task bmp_file_close(
    input   integer  fp
);
    begin
        $fclose(fp);
        $display("BMP file already closed!");
    end
endtask
/*--------------bmp文件末尾补0----------------*/
task bmp_paddings_zero(
    input   integer  ti_fp      ,
    input   integer  ti_zero_num
);
    integer i;
begin
    if(ti_zero_num>0)begin
        for (i=0;i<ti_zero_num;i=i+1) begin
            $fwrite(ti_fp,"%c",0);  // 填充字节
        end
    end
end
endtask
/*--------------建立bmp文件----------------*/
task bmp_file_create(
    input   integer         ti_image_width       ,
    input   integer         ti_image_height      ,
    output  reg     [31:0]  to_real_row_width ,
    output  integer         o_fp  
);
    reg [7:0]   bmp_header [53:0]; // bmp文件头
    reg [31:0]  binary_file_size ; // 文件大小
    reg [31:0]  little_endian_width,little_endian_height; // 宽度转为小端模式
    integer     i;
    begin
        to_real_row_width = f_image_width_align(ti_image_width); // 真实行宽
        $display("image_width = %03d,real_image_width = %03d",ti_image_width,to_real_row_width);
        little_endian_width  =  big_convert_little_endian(to_real_row_width); // 宽度转为小端模式
        little_endian_height =  big_convert_little_endian(ti_image_height);   // 宽度转为小端模式
        binary_file_size = big_convert_little_endian(to_real_row_width*3*ti_image_height + 54);
        /*--------------bmp文件头(14bytes)---------------*/
        //标识符:2 字节 ("BM")
        bmp_header[0]   = 8'h42;
        bmp_header[1]   = 8'h4d;
        // 文件大小:4 字节
        bmp_header[2]   = binary_file_size[31:24]; //因为前面大端转小端的已经转了,所以这里直接赋值
        bmp_header[3]   = binary_file_size[23:16];
        bmp_header[4]   = binary_file_size[15:8] ;
        bmp_header[5]   = binary_file_size[7:0]  ;
        // 保留位:4 字节(通常为 0)
        bmp_header[6]   = 8'h00;
        bmp_header[7]   = 8'h00;
        bmp_header[8]   = 8'h00;
        bmp_header[9]   = 8'h00;
        // 像素数据的偏移量:4 字节
        bmp_header[10]  = 8'h36;
        bmp_header[11]  = 8'h00;
        bmp_header[12]  = 8'h00;
        bmp_header[13]  = 8'h00;
        /*--------------bmp信息头(40bytes)---------------*/
        //信息头大小:4 字节(通常为 40)
        bmp_header[14]  = 8'h28;
        bmp_header[15]  = 8'h00;
        bmp_header[16]  = 8'h00;
        bmp_header[17]  = 8'h00;
        // 图像宽度:4 字节
        bmp_header[18]  = little_endian_width[31:24];
        bmp_header[19]  = little_endian_width[23:16];
        bmp_header[20]  = little_endian_width[15:8] ;
        bmp_header[21]  = little_endian_width[7:0]  ;
        // 图像高度:4 字节
        bmp_header[22]  = little_endian_height[31:24];
        bmp_header[23]  = little_endian_height[23:16];
        bmp_header[24]  = little_endian_height[15:8] ;
        bmp_header[25]  = little_endian_height[7:0]  ;
        // 颜色平面数:2 字节(必须为 1)
        bmp_header[26]  = 8'h01;
        bmp_header[27]  = 8'h0;
        // 每像素位数:2 字节(例如 24 位表示 RGB)
        bmp_header[28]  = 8'h18;
        bmp_header[29]  = 8'h0;
        // 压缩方式:4 字节(0 表示不压缩)
        bmp_header[30]  = 8'h0;
        bmp_header[31]  = 8'h0;
        bmp_header[32]  = 8'h0;
        bmp_header[33]  = 8'h0;
        // 图像数据大小:4 字节
        bmp_header[34]  = 8'h0;
        bmp_header[35]  = 8'h0;
        bmp_header[36]  = 8'h0;
        bmp_header[37]  = 8'h0;
        // 水平方向分辨率:4 字节
        bmp_header[38]  = 8'h0;
        bmp_header[39]  = 8'h0;
        bmp_header[40]  = 8'h0;
        bmp_header[41]  = 8'h0;
        // 垂直方向分辨率:4 字节
        bmp_header[42]  = 8'h0;
        bmp_header[43]  = 8'h0;
        bmp_header[44]  = 8'h0;
        bmp_header[45]  = 8'h0;
        // 使用的颜色数:4 字节(0 表示使用所有颜色)
        bmp_header[46]  = 8'h0;
        bmp_header[47]  = 8'h0;
        bmp_header[48]  = 8'h0;
        bmp_header[49]  = 8'h0;
        // 重要的颜色数:4 字节(0 表示所有颜色都重要)
        bmp_header[50]  = 8'h0;  
        bmp_header[51]  = 8'h0;
        bmp_header[52]  = 8'h0;
        bmp_header[53]  = 8'h0;
        //打开文件
        o_fp = $fopen("../../../../../sim_data/result.bmp", "w");
        if(o_fp!= 0)begin
            $display("width:%d,height:%d",ti_image_width,ti_image_height);
            for (i = 0;i <= 53 ;i = i + 1) begin
                $fwrite(o_fp,"%c",bmp_header[i]);
            end
        end else
            $display("Error opening result.bmp file!\n");
    end
endtask
/*--------------建立输出dot文件指针,方便后续写入数据----------------*/
task output_create_dot_file(
    output      integer to_fp
);
begin
    to_fp = $fopen("../../../../../sim_data/output.dat", "w"); 
    if (to_fp == 0)
        $display("Error creating dot file!\n");
    else
        $display("Dot file created successfully!\n");
end
endtask
/*--------------根据.dat文件指针,持续写入数据----------------*/
task output_write_dot_file(
    input   integer     ti_fp,
    input   [23:0]      ti_data 
);
begin
    if(ti_fp!= 0) begin
        $fwrite(ti_fp,"%04X\n",ti_data);
        $display("Expected uppercase hex output: %04X", ti_data);
    end
    else
        $display("Error writing dot file!\n");
end
endtask
/*--------------根据.dat文件指针,关闭----------------*/
task output_close_dot_file(
    input   integer     ti_fp
);
begin
    if (ti_fp != 0) begin
        $display("Closing dot file: %d", ti_fp);
        $fclose(ti_fp);
    end else begin
        $display("Error closing dot file: Invalid file pointer");
    end
end
endtask
// 定义输出信号
integer bmp_file    ; // 文件指针
integer dot_file    ; // 文件指针
integer real_width  ;
integer w, h        ; // 当前像素坐标
integer i, j        ; // 循环变量
// 存储像素数据
reg [23:0] image_data [0:P_IMG_WIDTH*P_IMG_HEIGHT-1]; // 24位色,每个像素3字节
// 初始化仿真信号
initial begin
    i = 0;
    j = 0;
    bmp_file_create(P_IMG_WIDTH, P_IMG_HEIGHT, real_width, bmp_file); // 创建BMP文件,写入bmp文件头
    output_create_dot_file(dot_file); // 创建输出文件
end
// 仿真结束时写入图像数据
always @(posedge i_clk or negedge i_rst_n) begin
    if (!i_rst_n) begin
        w <= 0;
        h <= 0;  // 从左下角开始填充
    end else if (w_valid_signal) begin
        // 存储像素数据,8位输入拼接为24位RGB数据
        image_data[h * P_IMG_WIDTH + w] <= {ri_data, ri_data, ri_data};
        $display("h:%04d, w:%04d, data:%04X", h, w, {ri_data, ri_data, ri_data});
        output_write_dot_file(dot_file,{ri_data, ri_data, ri_data});
        // 坐标递增
        if (w == P_IMG_WIDTH - 1) begin
            w <= 0;
            if (h == P_IMG_HEIGHT - 1) begin
                h <= 0;
                output_close_dot_file(dot_file);  // 关闭输出的对比文件文件
                // 当图像数据全部写入时,输出 BMP 文件
                for (i = 0; i < P_IMG_HEIGHT; i = i + 1) begin
                    for (j = 0; j < P_IMG_WIDTH; j = j + 1) begin
                        bmp_file_write(bmp_file, image_data[i * P_IMG_WIDTH + j]);
                        // 行填充部分 0
                        bmp_paddings_zero(bmp_file, real_width - P_IMG_WIDTH);
                    end
                end
                bmp_file_close(bmp_file);
                
                #10; 
                $finish;  // 仿真结束
            end else
                h <= h + 1;  // 填充下一行
        end else 
            w <= w + 1;  // 填充下一列
    end
end
endmodule
                


















