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