文章目录


本节主要来完成Modelsim联合Matlab进行FPGA图像仿真。

参考咸鱼FPGA的方法进行学习,很便捷。

我这里主要根据之前介绍的独立采用Modelsim仿真的方法,以及运用Matlab实现图像和文本之间转换的方法进行彩色图像转灰度的仿真测试。

在学习本节之前,需要掌握如何独立采用Modelsim进行仿真,能更便利的进行图像的仿真验证,不用担心quartus代码不可综合情况的出现。还需要掌握Matlab实现图像与txt文本之间的转换。这些之前博文中已经写过,这里不再赘述。

一、Modelsim联合Matlab进行FPGA图像仿真的步骤

如下是咸鱼FPGA给出的联合仿真原理图,清晰明了。

对步骤的分析如下:
1、先采用Matlab来对原图进行处理,来生成txt文档(post_img.txt)。——img_data_gen.m
2、通过Modelsim建立工程,为测试项目top_tb。——top_tb.v。其中待测试模块为top.v(内部包含图像处理模块、图像产生模块(相当于VGA模块))。
3、对于测试模块中top_tb,我们可以进行仿真波形的查看。另外通过测试文件中的代码,我们能将VGA数据逐像素的写入到post_img.txt文档中。
4、基于post_img.txt文档,用Matlab处理,将txt文档转换成图片的形式。——— img_data_show.m
5、此时,就完成了matlab和modelsim的联合FPGA图像处理仿真。

二、具体实现方法

2.1 新建一个Modelsim项目并编写测试代码

首先新建一个project,将项目名设为top_tb,语言为verilog。——该top_tb为tb测试文件。
测试文件中主要包括:

  • 信号的定义以及图像尺寸的声明
  • 模块的例化
  • 产生时钟激励和复位激励
  • 将图像数据转变为txt文本
  • 将最终的VGA数据逐像素的写入到txt文档中

2.2 新建v文件来编写待测试代码

在project的基础上,新建source v文件进行待测试代码的编写。
这里的待测试代码是一个彩色图像进行灰度转换的项目。包含三个模块:顶层模块,图像产生模块(相当于VGA模块),灰度转换图像处理模块

2.3 建成项目后,我们采用matlab生成txt文本文件

%                 彩色图片转为txt文本,格式为24bit的hex数据
clc;
clear all;
%--------------------------------------------------------------------------
pre_img = imread('pre_img.jpg');   %读取原图图片文件
[ROW,COL,N] = size(pre_img);       %获得图片尺寸[宽度,长度,维度]
RGB_ij = uint64(zeros(ROW,COL));   %定义32位宽的RGB变量
%--------------------------------------------------------------------------
fid = fopen('pre_img.txt','w');    %打开txt文档,使得后续数据的写入该文档
for i = 1:ROW
    for j = 1:COL
        R = double(pre_img(i,j,1));
        G = double(pre_img(i,j,2));
        B = double(pre_img(i,j,3));
        %-------------------------------------
        RGB          = R*(2^16) + G*(2^8) + B;
        RGB_ij(i,j)  = RGB;
        RGB_hex      = dec2hex(RGB);
        %-------------------------------------
        fprintf(fid,'%s\n',RGB_hex); %将字符打印到txt文件
    end
end
fclose(fid);

将生成的txt文本文件放入到与测试项目同一文件夹中。

2.4 编译仿真并进行波形分析

此时我们有了待测试的文件以及测试文件,和图像数据txt文档。即可Cmopile,成功后进行Simulate,选中我们的top_tb工程即可。


添加波形并进行查看:
我们放大前面部分进行验证。img_data表示原图转换成txt文档的数据,我们可以将其与pre_img.txt文档中的数据进行比对。可看到第一个是e4915f,第二个数据de8b59,以此类推……
VGA_data是我们经过灰度处理后的数据,在VGA_de有效的时候,将其逐像素的写入到了post_img_txt中。

2.5 通过Matlab将txt文档转换成图像

将生成的post_img_txt放到img_data_show.m同一文件夹下,进行txt文档与图像之间的转换。

% txt文本中24bit的数据还原为彩色图片
clear all;
clc;
%--------------------------------------------------------------------------
ROW = 480;  %宽度
COL = 640;  %长度
N   = 3;    %维度
post_img = uint8(zeros(ROW,COL,N));
%--------------------------------------------------------------------------
fid = fopen('post_img.txt','r'); %打开post_img.txt文档,并向里面写数据
for i = 1:ROW
    for j = 1:COL
        value = fscanf(fid,'%s',1); 
        %------------------------------------------
        post_img(i,j,1) = uint8(hex2dec(value(1:2)));
        post_img(i,j,2) = uint8(hex2dec(value(3:4)));
        post_img(i,j,3) = uint8(hex2dec(value(5:6)));  
    end 
end
fclose(fid);
%--------------------------------------------------------------------------
pre_img = imread('pre_img.jpg'); %处理前面的图像进行展示比对
subplot(121);imshow(pre_img), title('处理前');
subplot(122);imshow(post_img),title('处理后');
%--------------------------------------------------------------------------
imwrite(post_img,'处理后的图片.jpg');   %保存处理后的图片输出为jpg格式

至此,经过咸鱼FPGA大佬的资料学习,成功完成了Modelsim和Matlab的联合仿真。本例程的灰度化是采用RGB分量转灰度的方式进行的,根据上面步骤可尝试进行Ycbcr转灰度或者图像翻转多种图像处理操作等操作。

三、matlab出错点

matlab 出现 “文件标识符无效。使用 fopen 生成有效的文件标识符。”主要有以下两个原因:

1、可能是路径或者文件名错了(我这里是路径错误,路径修改后可正常运行)
2、可能是在c盘下的目录,因为没有管理员权限所以报错。

四、采用该方法实现Ycbcr的灰度转换

我这里根据上面的学习,对Ycbcr灰度转换进行仿真验证,在此之前需要掌握RGB到Ycbcr灰度转换的流水线技术

4.1 搭建FPGA图像处理工程

该部分我们首先搭建FPGA进行灰度转换处理的工程。
框图如下:


根据上面的学习,逐模块的进行代码编写即可。

4.2 图像生成模块代码

`timescale 1 ns/1 ns
//该模块用来生成图像数据,根据此数据进行后续的图像算法处理
module img_gen
//========================< 参数 >==========================================
//					  640x480 @60Hz 25Mhz
#(
parameter H_ADDR			= 640					      ,	//行有效数据	  
parameter H_SYNC			= 96				 	      ,	//行同步  
parameter H_BACK			= 48					      ,	//行显示后沿
parameter H_TOTAL			= H_ADDR+H_SYNC+H_BACK	,	//行扫描周期784
parameter V_ADDR			= 480				      	,	//场有效数据	  
parameter V_SYNC			= 2					   	,	//场同步	
parameter V_BACK			= 33					      ,  //场显示后沿
parameter V_TOTAL			= V_ADDR+V_SYNC+V_BACK		//场扫描周期515
)
//========================< 端口 >==========================================
(
//系统全局信号
input	wire			       	clk						,	//时钟
input	wire				      rst_n					,	   //复位,低电平有效
//img output ----------------------------------------
output	wire				   img_hsync				,	//img行同步信号
output	wire				   img_vsync				,	//img场同步信号
output	reg  	[23:0]		img_data				   ,	//img原图像数据信号
output	reg					img_de						//img数据有效指示信号
);
//========================< 信号 >==========================================
reg	  	[15:0]			 	cnt_h				   	; //行有效数据计数器
wire					      	add_cnt_h				; //计数器使能信号
wire						      end_cnt_h				;
reg	  	[15:0]		 		cnt_v				   	;
wire					      	add_cnt_v				;
wire					      	end_cnt_v				;
//---------------------------------------------------
reg  	[23:0] 				   ram [H_ADDR*V_ADDR-1:0]	;
reg 	[31:0] 			    	i				 	   	;
//==========================================================================
//==	行、场计数
//==========================================================================
always @(posedge clk or negedge rst_n) begin
	if(!rst_n)
		cnt_h <= 0;
	else if(add_cnt_h) begin
		if(end_cnt_h)
			cnt_h <= 0;
		else
			cnt_h <= cnt_h + 1;
	end
end

assign add_cnt_h = 1;
assign end_cnt_h = add_cnt_h && cnt_h==H_TOTAL-1;//计数到783清零,说明一行像素计数完成
//---------------------------------------------------
always @(posedge clk or negedge rst_n) begin 
	if(!rst_n)
		cnt_v <= 0;
	else if(add_cnt_v) begin
		if(end_cnt_v)
			cnt_v <= 0;
		else
			cnt_v <= cnt_v + 1;
	end
end

assign add_cnt_v = end_cnt_h; //每计数完一行,即可实现一场的+1.
assign end_cnt_v = add_cnt_v && cnt_v==V_TOTAL-1; //计数到515清零,说明一帧图像完成
//==========================================================================
//==	输出
//==========================================================================
//行场同步信号
//---------------------------------------------------
assign img_hsync = (cnt_h <= H_SYNC - 1) ? 0 : 1;
assign img_vsync = (cnt_v <= V_SYNC - 1) ? 0 : 1;

//数据有效信号,相当于数据输出使能
//行:144-784;场:35- 515
assign img_req = (cnt_h >= H_SYNC + H_BACK - 1) && (cnt_h < H_SYNC + H_BACK + H_ADDR - 1) &&
				 (cnt_v >= V_SYNC + V_BACK	  ) && (cnt_v < V_SYNC + V_BACK + V_ADDR    );

//读取txt文件到数组中,格式为16进制(相当于往存储器中存数据)
//---------------------------------------------------
//如果是用quartus进行rtl查看,就需要将这里引去,因为不可综合。
initial begin
	$readmemh("pre_img.txt", ram);
end

//当数据有效的时候,从ram中将数据逐像素的输出,输出后供后面图像处理模块使用
//---------------------------------------------------
always@(posedge clk or negedge rst_n) begin
	if(!rst_n)begin
		img_data <= 24'd0;
		i <= 0;
	end
	else if(img_req) begin
		img_data <= ram[i];
		i <= i + 1;
	end
	else if(i==H_ADDR*V_ADDR) begin //全部像素读完后进行清零。
		img_data <= 24'd0;
		i <= 0;
	end
end

//数据使能延迟一拍(读数据需要花费1个clk,因此延迟一拍) 
always @(posedge clk) begin 
	img_de <= img_req;
end

endmodule

4.3 图像处理模块——Ycbcr灰度转换

//RGB分量转Gray灰度图
module RGB_Gray
//========================< 端口 >==========================================
(
input   wire				clk						,	//时钟
input   wire				rst_n					   ,	//复位
//原图 ----------------------------------------------
input   wire				RGB_hsync				,
input   wire				RGB_vsync				,
input   wire    [23:0]  RGB_data				   ,
input   wire				RGB_de					,
//灰度转换图 ----------------------------------------------
output  wire				gray_hsync			  	, //这里的行场同步信号也就是最终的VGA行场同步信号。
output  wire				gray_vsync				, //需要根据实际消耗的时钟进行延迟
output  wire	[23:0]	gray_data				,
output  wire				gray_de
);

wire [7:0]  R0,G0,B0;

reg [15:0]  R1,G1,B1;
reg [15:0]  R2,G2,B2;
reg [15:0]  R3,G3,B3;

reg [16:0] Y1,Cb1,Cr1;
reg [23:0]  Y2,Cb2,Cr2;

reg [7:0]  RGB_de_r     ;  
reg [7:0]  RGB_hsync_r  ;
reg [7:0]  RGB_vsync_r  ;

//将24位RGB分成三分量

assign R0 = RGB_data[23:16];
assign G0 = RGB_data[15:8];
assign B0 = RGB_data[7:0];

//=============根据RGB转Ycbcr的公式进行计算(三级流水线)==================
//---------------------------------------------------
//clk 1 第一级流水线完成所有的乘法计算

always @(posedge clk or negedge rst_n) begin
    if(!rst_n)begin
        {R1,G1,B1} <= {16'd0, 16'd0, 16'd0};
        {R2,G2,B2} <= {16'd0, 16'd0, 16'd0};
        {R3,G3,B3} <= {16'd0, 16'd0, 16'd0};
    end
    else begin
        {R1,G1,B1} <= { {R0 * 16'd77},  {G0 * 16'd150}, {B0 * 16'd29 } };
        {R2,G2,B2} <= { {R0 * 16'd43},  {G0 * 16'd85},  {B0 * 16'd128} };
        {R3,G3,B3} <= { {R0 * 16'd128}, {G0 * 16'd107}, {B0 * 16'd21 } };
    end
end
//---------------------------------------------------
//clk 2 第二级流水线完成所有的加减法计算

always @(posedge clk or negedge rst_n) begin
    if(!rst_n)begin
        Y1  <= 16'd0;
        Cb1 <= 16'd0;
        Cr1 <= 16'd0;
    end
    else begin
        Y1  <= R1 + G1 + B1;
        Cb1 <= B2 - R2 - G2 + 16'd32768; //128扩大256倍
        Cr1 <= R3 - G3 - B3 + 16'd32768; //128扩大256倍
    end
end
//---------------------------------------------------
//clk 3 第三级流水线完成所有的移位计算(缩小256倍) 右移8位

always @(posedge clk or negedge rst_n) begin
    if(!rst_n)begin
        Y2  <= 8'd0;
        Cb2 <= 8'd0;
        Cr2 <= 8'd0;
    end
    else begin
	     Y2  <= Y1 >> 8;  
        Cb2 <= Cb1>> 8;
        Cr2 <= Cr1>> 8;
    end
end

// 取YcbCr三分量中的Y分量,将其赋值给RGB888通道即可。
assign gray_data = {Y2[7:0],Y2[7:0],Y2[7:0]}; 

//=================== 信号同步==================================
//为确保图像能正常显示,要保持数据与数据使能和行场有效信号同步
//前面我们花费了三个clk来计算,因此延迟三拍


always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        RGB_de_r    <= 3'b0;
        RGB_hsync_r <= 3'b0;
        RGB_vsync_r <= 3'b0;
    end
    else begin  
        RGB_de_r    <= {RGB_de_r[1:0],    RGB_de};
        RGB_hsync_r <= {RGB_hsync_r[1:0], RGB_hsync};
        RGB_vsync_r <= {RGB_vsync_r[1:0], RGB_vsync};
    end
end


assign gray_de    = RGB_de_r[2];
assign gray_hsync = RGB_hsync_r[2];
assign gray_vsync = RGB_vsync_r[2];

endmodule

4.4 顶层top模块

`timescale 1 ns/1 ns

module top
//========================< 参数 >==========================================
//将parameter常量传给调用实例
#(
	parameter 		 			  IMG_H = 640				, //图像长度
	parameter 		 			  IMG_W = 480		 		 //图像宽度
)
//========================< 端口 >==========================================
//全局时钟和复位
(
	input    wire               clk                	, 	//50MHZ时钟
	input    wire               rst_n              	, 	//低电平复位

//VGA 端口
	output	wire				   VGA_hsync				,	//VGA行同步
	output	wire			    	VGA_vsync				,	//VGA场同步
	output   wire    [23:0]    VGA_data			   	,	//数据
	output	wire				   VGA_de						//数据有效
);
//========================信号 >==========================================
	wire                        img_hsync				;
	wire                        img_vsync				;
	wire    [23:0]              img_data				;
	wire                        img_de					;
	
//========================< 模块实例化 >===================================
// 图像数据产生模块  (VGA模块)                 
img_gen
#(
	.H_ADDR					   (IMG_H				),	//图像长度(行有效数据)
	.V_ADDR				   	(IMG_W				)	//图像宽度(场有效数据)
)
u_img_gen
(
    .clk                    (clk             ), 
    .rst_n                  (rst_n			 	),

    .img_hsync              (img_hsync       ),  //img输出行同步
    .img_vsync              (img_vsync       ),  //img输出场同步
    .img_data               (img_data        ),  //img输出数据,该数据是原图的像素数据
	 .img_de				       (img_de				)	//img输出数据有效信号
);
//==========================================================================
//==            			RGB分量转Gray灰度图
//==========================================================================
RGB_Gray u_RGB_Gray
(
    .clk                    (clk                    ),
    .rst_n                  (rst_n                  ),
    //原图 ------------------------------------------
    .RGB_hsync              (img_hsync              ),
    .RGB_vsync              (img_vsync              ),
    .RGB_data               (img_data               ),
	 .RGB_de                 (img_de                 ),
    //灰度图 ----------------------------------------
    .gray_hsync             (VGA_hsync              ),
    .gray_vsync             (VGA_vsync              ),
    .gray_data              (VGA_data               ),  //灰度处理后的图像像素数据
	 .gray_de                (VGA_de                 )
);

endmodule

4.5 测试tb文件

`timescale 1ns/1ns  //时间精度
`define    Clock 20 //时钟周期

module top_tb;
//========================< 参数 >==========================================
parameter                   IMG_H = 640         	;   //图像长度
parameter                   IMG_W = 480         	;   //图像高度
//========================< 信号 >==========================================
reg                         clk                     ; //时钟,50Mhz
reg                         rst_n                   ; //复位,低电平有效
wire                        VGA_hsync               ; //VGA行同步
wire                        VGA_vsync               ; //VGA场同步
wire    [23:0]              VGA_data                ; //数据
wire                        VGA_de                  ; //数据有效
//==========================================================================
//==    模块例化
//==========================================================================
top
#(
    .IMG_H                  (IMG_H                  ),
    .IMG_W                  (IMG_W                  )
)
u_top
(
    .clk                    (clk                    ),
    .rst_n                  (rst_n                  ),
    .VGA_hsync              (VGA_hsync              ),
    .VGA_vsync              (VGA_vsync              ),
    .VGA_data               (VGA_data               ),
    .VGA_de                 (VGA_de                 )
);
//==========================================================================
//==    产生时钟激励和复位激励
//==========================================================================
initial begin
    clk = 1;
    forever
        #(`Clock/2) clk = ~clk;
end

initial begin
    rst_n = 0; #(`Clock*20+1);
    rst_n = 1;
end
//==========================================================================
//==    图像数据转变为txt文本
//==========================================================================
//打开post_img.txt文件
//---------------------------------------------------
integer post_img_txt;

initial begin
    post_img_txt = $fopen("post_img.txt");
end

//像素写入到txt中
//---------------------------------------------------
reg [20:0] pixel_cnt; //640*480 = 307200 ,对这些像素逐个写入

always @(posedge clk) begin
    if(!rst_n) begin
        pixel_cnt <= 0;
    end
    else if(VGA_de) begin
        pixel_cnt = pixel_cnt + 1;
        $fdisplay(post_img_txt,"%h",VGA_data);
		
        if(pixel_cnt == IMG_H*IMG_W)
            $stop;
    end
end

endmodule

有了这几个文件即可建成modelism项目,后进行编译仿真。得到波形以及运行的时间。最重要的得到了处理后的图像数据,将其放到matlab文件夹中,使用img_data_show.m程度进行txt文档到图像的转换即可。

这就是我们采用verilog编写图像处理程序,然后结合matlab得到的最终仿真波形和效果图,验证了算法的正确性,便于上板验证。


黄色线处起就是在一行一行的处理图像数据,最终完成一帧图像的处理,具体的数据分析可根据公式计算验证。

五、仿真调试中出现的问题

1、在Ycbcr公式编写正确的情况下,却不能正确计算R1,G1……
这里主要是位数出现了错误:
比如当我们进行乘法的时候,两个八位相乘是16位。加减法的时候仍然是16位。
2、在计算的时候Y2也错误
同样还是位数的问题,当十进制显示的时候都是0我们不好观察,将数据均设置成二进制,就能清楚的看到数据是有的,但是位宽不正确,导致右移8位,也就是取高8位时候都是0.因此要特别注意位数的问题。


如下是针对Ycbcr仿真的结果,我们可以根据公式进行局部数据的计算。
例如:
首先读入24位数据,根据我们三分量的分配,看是否正确。

//将24位RGB分成三分量

assign R0 = RGB_data[23:16];
assign G0 = RGB_data[15:8];
assign B0 = RGB_data[7:0];

当输入第一个RGB888时,像素值是111001001001000101011111
高八位11100100 = 228 正确 ——R0
中八位 1001 0001 = 145 正确——G0
低八位 01011111 = 95 正确 —— B0
R1 = 77 * R0 = 77 ……逐个计算G1、B1、Y1等,验证均正确。