基于ROM的VGA图像显示(触边弹跳特效)

发布时间 2023-07-31 20:49:52作者: jasonlee~

第39章、基于ROM的VGA图像显示(触边弹跳特效)

【1 理论】

【弹跳特效实现方法】

问题1:图片是如何运动起来的?

//rd_en:ROM读使能
assign rd_en = (( (pix_x >= (((H_VALID - H_PIC)/2) - 1'b1))
							 && (pix_x < (((H_VALID - H_PIC)/2) + H_PIC - 1'b1)))
							 &&((pix_y >= ((V_VALID - W_PIC)/2))
							 && ((pix_y < (((V_VALID - W_PIC)/2) + W_PIC)))));
//pic_valid:图片数据有效信号
always@(posedge vga_clk or negedge sys_rst_n)
	if(sys_rst_n == 1'b0)
		pic_valid <= 1'b1;
	else
		pic_valid <= rd_en;
//pix_data_out:输出VGA显示图像数据
assign pix_data_out = (pic_valid == 1'b1) ? pic_data : pix_data;
//rom_addr:读ROM地址
always@(posedge vga_clk or negedge sys_rst_n)
	if(sys_rst_n == 1'b0)
		rom_addr <= 14'd0;
	else if(rom_addr == (PIC_SIZE - 1'b1))
		rom_addr <= 14'd0;
	else if(rd_en == 1'b1)
		rom_addr <= rom_addr + 1'b1;

//------------- rom_pic_inst -------------// 
rom_pic rom_pic_inst
(
.address (rom_addr ), //输入读ROM地址,14bit
.clock   (vga_clk ), //输入读时钟,vga_clk,频率25MHz,1bit
.rden    (rd_en ), //输入读使能,1bit
.q       (pic_data ) //输出读数据,16bit
);

由上述代码可以看出,ROM的读使能信号rd_en和图像数据有效信号pic_valid的约束条件都是固定参数,这使它们只能在固定显示区域有效。假如我们每次在一帧图像显示完成后对图像的显示区域重新定义,是否就可以实现显示图片显示位置的变化

(1)在每一帧图像显示完成后只改变水平方向的rd_en信号的显示范围,这就可以实现图片在水平方向显示位置的改变。若每次显示位置只向左或向右移动若干个相同的像素点个数,加之人眼的视觉暂留效果,就可以实现显示图片在水平方向上的左右移动。

(2)若每次显示位置只向上或向下移动若干个像素点个数,加之人眼的视觉暂留效果,就可以实现显示图片在竖直方向上的竖直上下移动。

要注意的是,左右或者上下移动的像素点个数的多少表示图片移动的快慢,数值越大移动也快,这个数值不固定,可自行设置,但不建议设置太大,数值设置太大图片移动会显的不太流畅,本实验工程设置移动数值为1个像素点,读者也可自行设置参数观察实验效果。

问题二:运动方向如何实现与水平方向45度夹角?

假使水平方向和竖直方向的移动像素点个数相同,根据勾股定理,图片移动的方向就实现了与水平方向45度夹角。当然读者也可以根据自己喜好,通过更改水平方向和竖直方向的移动像素点个数使图片按照其他方向移动。

问题3:怎样实现图片触框反弹?

以水平方向图片移动为例。在图片的第一帧图像显示中,图片的最左边一列像素点与显示区域左边框重合,每刷新一帧图像,图片向右移动若干个像素点;当图片的最右边的一列像素点与显示区域右边框重合,在下一帧图像刷新时,图片开始向反方向移动,这就实现了显示图片水平方向的触框反弹特效。同理,竖直方向也是如此。

【2 实战】

以十色等宽彩条做背景,将存储于ROM中的图片显示在VGA显示器上,图片显示初始位置为有效显示区域左上角,图片沿着与水平方向45度夹角的方向,向右下角运动,运动过程中,当图片边沿触及显示区域边沿,图片向反方向运动,实现图片弹跳特效。图片分辨率为100*100,VGA显示模式为640x480@60。

【图像数据生成模块】

设计本模块的目的是产生VGA彩条背景像素点色彩信息、读出ROM存储的图片数据并实现弹跳特效。

第一部分:彩条背景色彩信息(pix_data)波形图绘制思路

根据输入像素点坐标(pix_x,pix_y),在有效显示区域,将pix_x计数范围十等分,在不同的计数部分给pix_data赋值对应的色彩信息,因为采用时序逻辑的赋值方式,pix_data滞后pix_x、pix_y信号一个时钟周期。信号波形图如下:

第二部分:ROM读使能(rd_en)、ROM地址(rom_addr)波形图绘制思路

显示的的图片数据是事先写入ROM,ROM为调用IP核生成,写入照片分辨率为100*100。要想将写入ROM的图片读取出来,使能信号和地址信号必不可少,所以模块内部要声明ROM读使能信号(rd_en)和ROM地址(rom_addr)信号。

在图片显示区域拉高使能信号,将要读取数据地址写入ROM地址端口,读取地址对应图像数据,但有一点要注意,自ROM读取的数据是滞后使能信号和地址信号一个时钟周期的,比如,当使能信号为高电平,地址写入为999,但与地址999同步输出的数据为地址998的数据,所以ROM读使能信号(rd_en)和ROM地址(rom_addr)信号均要超前图片显示区域一个时钟周期,信号波形图绘制如下:

第三部分:图片显示有效信号(pic_valid)、待显示图像数据(pic_data_out)波形图绘制思路

1.由于ROM读使能信号(rd_en)超前图片显示区域一个时钟周期,可利用此使能信号 延迟一个时钟周期生成图片显示有效信号(pic_valid);

2.在有效信号为高电平时,将自ROM读出的图片数据赋值给待显示图像数据(pic_data_out),覆盖彩条背景。信号波形图如下:

第四部分:水平移动变量(x_move)、竖直移动变量(y_move)、水平移动标志(x_flag)、竖直移动标志(y_flag)波形图绘制思路

(1)图片移动:改变图片显示区域(pic_valid)+视觉暂留。以图片显示水平移动为例,要想实现显示图片的水平移动,就要改变图片有效信号pic_valid的水平有效区域。

(2)图片有效信号pic_valid是由ROM读使能信号rd_en打一拍得到,所以归根到底就是改变ROM读使能信号rd_en的水平有效区域

(3)实现图片的移动:声明一个变量x_move作为ROM读使能信号rd_en的水平方向约束条件,x_move初值为0,表示图片初始显示位置在显示器最左边;每完成一帧图像的显示,x_move信号自加1,表示移动速度为1个像素点;设图片每行像素点个数为H_PIC = 100,当pix_x信号在计数范围(x_move ≤ pix_x <x_move + H_PIC)(pix_x在图片显示区域),约束条件pix_y信号不变时,ROM读使能信号rd_en信号有效(读出图片数据)。同理,可声明竖直移动变量y_move,移动速度与水平移动变量x_move相同,实现显示图片竖直向右移动。将两者结合,可实现显示图片与水平方向45度夹角右下移动。

(4)实现图片的触框反弹:声明两个移动满标志信号水平移动标志(x_flag)竖直移动标志(y_flag)拉高表示移到边框,持续高电平x_move减到0)。水平移动标志信号x_flag,作用是控制水平方向上显示图片的移动,当x_flag信号为低电平时,每完成一帧图像的显示,x_move信号自加1,实现图片的向右移动;当图片的最右边与显示屏右边框重合时, x_flag信号赋值高电平,每完成一帧图像的显示,x_move信号自减1,实现图片的向左移动;当图片最左边与显示器最左边重合,x_flag信号变为低电平,每完成一帧图像的显示,x_move信号自加1,实现图片的向右移动。周而复始,实现了显示图片水平方向触框回弹的效果。同理,可以使用竖直移动标志信号y_flag实现竖直方向上的显示图片触框回弹效果。两者结合,实现显示图片在显示器显示区域内触框反弹特效。

module vga_pic(
    input  wire        vga_clk   ,
    input  wire        vga_rst_n ,
    input  wire [11:0] pix_x     ,
    input  wire [11:0] pix_y     ,  //

    output wire [15:0] pix_data_out  //pix_data_out即背景和图片的信息,rgb565模式共16位
);     

reg x_flag ;
reg x_move ;
reg y_flag ;
reg y_move ;

wire ONE,TWO,THREE,FOUR,FIVE,SIX,SEVEN,EIGHT,NINE,TEN ;
//-----------------------------------------------------------------------------------//
//------------------------------模块1:输出背景信息(pic_data)------------------------//
//-----------------------------------------------------------------------------------//
/*-----------------------------① 定义背景、图片参数---------------------------*/
//定义背景有效显示区域(640*480)
parameter   H_VALID  = 10'd640   ,
            V_VALID  = 10'd480   ;
//定义图片显示区域长宽值、像素点个数
parameter   H_PIC    = 10'd100   ,
            W_PIC    = 10'd100   ,
            PIC_SIZE = 14'd10000 ;
//定义背景颜色  
parameter   RED      = 16'hF800  ,
            ORANGE   = 16'hFC00  ,
            YELLOW   = 16'hFFE0  ,
            GREEN    = 16'h07E0  ,
            CYAN     = 16'h07FF  ,
            BLUE     = 16'h001F  ,
            PURPPLE  = 16'hF81F  ,
            BLACK    = 16'h0000  ,
            WHITE    = 16'hFFFF  ,
            GRAY     = 16'hD69A  ;

/*--------------------------------② 将背景分区域编号----------------------------------*/
//图片有效显示区域即为vga信号有效区域,用 pix_x 在 800 的周期内找到对应的 640 即可
assign  ONE   = ((pix_x >= 0           ) && (pix_x < H_VALID/10   ));
assign  TWO   = ((pix_x >= H_VALID/10  ) && (pix_x < H_VALID/10*2 ));
assign  THREE = ((pix_x >= H_VALID/10*2) && (pix_x < H_VALID/10*3 ));
assign  FOUR  = ((pix_x >= H_VALID/10*3) && (pix_x < H_VALID/10*4 ));
assign  FIVE  = ((pix_x >= H_VALID/10*4) && (pix_x < H_VALID/10*5 ));
assign  SIX   = ((pix_x >= H_VALID/10*5) && (pix_x < H_VALID/10*6 ));
assign  SEVEN = ((pix_x >= H_VALID/10*6) && (pix_x < H_VALID/10*7 ));
assign  EIGHT = ((pix_x >= H_VALID/10*7) && (pix_x < H_VALID/10*8 ));
assign  NINE  = ((pix_x >= H_VALID/10*8) && (pix_x < H_VALID/10*9 ));
assign  TEN   = ((pix_x >= H_VALID/10*9) && (pix_x < H_VALID      ));
/*---------------------区域赋值:将10个区域进行相应10种颜色赋值,输出pix_data--------------------*/
wire [3:0] pic_area;
reg [15:0] pix_data ;
//背景信息生成
always@(posedge vga_clk or negedge vga_rst_n)begin
    if(!vga_rst_n) 
        pix_data <= 15'b0 ;
    else begin
        case(pic_area)
            ONE     : pix_data <= RED     ;
            TWO     : pix_data <= ORANGE  ;
            THREE   : pix_data <= YELLOW  ;
            FOUR    : pix_data <= GREEN   ;
            FIVE    : pix_data <= CYAN    ;
            SIX     : pix_data <= BLUE    ;
            SEVEN   : pix_data <= PURPPLE ;
            EIGHT   : pix_data <= BLACK   ;
            NINE    : pix_data <= WHITE   ;
            TEN     : pix_data <= GRAY    ;
            default : pix_data <= 15'b0   ;
        endcase
    end
end 

//----------------------------------------------------------------------------------------------------------------------//
//------------------------------模块2:生成rom IP核需要的使能和地址信号,进而输出图片信息q(pic_data)------------------------//
//--------------------------------------------------------------------------------------------------------
always@(posedge vga_clk or negedge vga_rst_n)begin
    if(vga_rst_n == 1'b0)
        x_flag <= 1'b0;
    else if(x_move == 10'd0)
        x_flag <= 1'b0;
    else if((x_move == (H_VALID - H_PIC - 1'b1))//x_move = 10'b539,移动值的最大值
            && (pix_x == (H_VALID - 1'b1))
            && (pix_y == (V_VALID - 1'b1)))//到达临界值
        x_flag <= 1'b1;
end 

//x_move:图片横向移动量
always@(posedge vga_clk or negedge vga_rst_n)begin
    if(vga_rst_n == 1'b0)
        x_move <= 10'd0;
    else if(x_flag == 1'b0)
        x_move <= x_move + 1'b1;
    else if(x_flag == 1'b1)
        x_move <= x_move - 1'b1;//x_move信号不是在加就是在减
end

//y_flag:图片上下移动标志
always@(posedge vga_clk or negedge vga_rst_n)begin
    if(vga_rst_n == 1'b0)
        y_flag <= 1'b0;
    else if(y_move == 0)
        y_flag <= 1'b0;
    else if((y_move == (V_VALID - W_PIC - 1'b1))//y_move = 10'b379,移动值的最大值
            && (pix_x == (H_VALID - 1'b1))
            && (pix_y == (V_VALID - 1'b1)))
        y_flag <= 1'b1;
end

//y_move:图片纵向移动量
always@(posedge vga_clk or negedge vga_rst_n)begin
    if(vga_rst_n == 1'b0)
        y_move <= 10'd0;
    else if(y_flag == 1'b0)
        y_move <= y_move + 1'b1;
    else if(y_flag == 1'b1)
        y_move <= y_move - 1'b1;
end

//rd_en:ROM 读使能
assign rd_en = (((pix_x >= (x_move)) && (pix_x < (x_move + H_PIC)))       //此时图片被扫描出来
                && ((pix_y >= (y_move)) && ((pix_y < (y_move + W_PIC)))));

//②、rom_addr:读ROM地址,相当于1个计数器
reg [13:0] rom_addr ;		//rom中存储了0-9999共计10000个地址值,14位
always@(posedge vga_clk or negedge vga_rst_n)begin
    if(vga_rst_n == 1'b0)
        rom_addr <= 14'd0;
    else if(rom_addr == (PIC_SIZE - 1'b1))
        rom_addr <= 14'd0;
    else if(rd_en == 1'b1)
        rom_addr <= rom_addr + 1'b1;
    // else 
    //     rom_addr <= rom_addr ;
end

//③、rom输出q,即为图片数据pic_data
wire [15:0] pic_data ;
rom_pic	rom_pic_inst01 (
    .address ( rom_addr  ),
    .clock   ( vga_clk   ),
    .rden    ( rd_en     ),
    .q       ( pic_data  )//在生成ROM IP核时设置输出 q 延迟 使能信号rd_en 1个时钟周期。为了解决这一问题,将rd_en提前1个时钟周期进行抵消
);

//-------------------------------------------------------------------------------------------//
//------------------------------模块3:输出图像信息(pix_data_out)----------------------------//
//-------------------------------------------------------------------------------------------//
 //pic_valid:图片数据有效信号,有效时将rom中读取的图片数据覆盖到背景数据,生成图像数据 pix_data_out
reg pic_valid ;
always@(posedge vga_clk or negedge vga_rst_n)begin
    if(vga_rst_n == 1'b0)
        pic_valid <= 1'b1;
    else
        pic_valid <= rd_en;
end

//从rom读出的图片数据
assign pix_data_out = (pic_valid == 1'b1) ? pic_data : pix_data;


endmodule 

【时钟生成模块(clk_gen)】

module clk_gen(
    input   inclk0  ,
    input   arset   ,
    output  c0      ,
    output  locked  
);

clk_25	clk_25_inst01 (
	.areset ( arset  ),
	.inclk0 ( inclk0 ),
	.c0     ( c0     ),
	.locked ( locked )
	);
endmodule 

【vga控制模块(vga_ctrl)】

module vga_ctrl(
    input          vga_clk    , 
    input          vga_rst_n  ,
    input  [15:0]  pix_data_out   ,//pix_data即显示图像的信息,rgb565模式共16位
    
    output [9:0]   pix_x      ,//680*640,10位才包含
    output [9:0]   pix_y      ,
    output         hsync      ,
    output         vsync      ,
    // output         rgb_valid  ,
    output [15:0]  pic_rgb      //rgb565,共16位
);
/*----------------------------参数定义----------------------------------*/
//定义行、场信号节点参数:同步、后沿、左边框、有效数据、右边框、前沿、总周期
parameter   H_SYNC   = 10'd96   , //行同步 
            H_BACK   = 10'd40   , //行时序后沿
            H_LEFT   = 10'd8    , //行时序左边框
            H_VALID  = 10'd640  , //行有效数据
            H_RIGHT  = 10'd8    , //行时序右边框
            H_FRONT  = 10'd8    , //行时序前沿
            H_TOTAL  = 10'd800  ; //行扫描周期,一个周期800个像素点
parameter   V_SYNC   = 10'd2    , //场同步
            V_BACK   = 10'd25   , //场时序后沿
            V_TOP    = 10'd8    , //场时序上边框
            V_VALID  = 10'd480  , //场有效数据
            V_BOTTOM = 10'd8    , //场时序下边框
            V_FRONT  = 10'd2    , //场时序前沿
            V_TOTAL  = 10'd525  ; //场扫描周期,一个周期525

/*----------------------------行场计数器:h_cnt、v_cnt----------------------------------*/
//行、场计数器(h_cnt、v_cnt):用来生成行、场同步信号,行计数器h_cnt,计数(0-799),共10位
reg [9:0] h_cnt ;
always@(posedge vga_clk or negedge vga_rst_n)begin
    if(!vga_rst_n)
        h_cnt <= 10'b0 ;
    else if(h_cnt == H_TOTAL-1'b1)
        h_cnt <= 10'b0 ;
    else 
        h_cnt <= h_cnt + 10'b1 ;
end 
reg [9:0] v_cnt ;
always@(posedge vga_clk or negedge vga_rst_n)begin
    if(!vga_rst_n)
        v_cnt <= 10'b0 ;
    else if((v_cnt == V_TOTAL-1'b1) && (h_cnt == H_TOTAL - 1'b1))//计数到整幅图片的最后一个像素
        v_cnt <= 10'b0 ;
    else if(h_cnt==H_TOTAL - 1'b1)//每当行计数满则向下一行
        v_cnt <= v_cnt + 10'b1 ;
    else 
        v_cnt <= v_cnt ;
end 

/*----------------------------1、行场同步信号(hsync、vsync):由计数器确定行、场同步信号hsync、vsync----------------------*/
//行、场同步信号(hsync、vsync):只有行、场同步信号为高,其余皆为低 ,表明行场扫描信号的过程开始
wire hysnc ;
wire vysnc ;
assign hsync = (h_cnt >= 0) && (h_cnt < 96) ;
assign vsync = (v_cnt >= 0) && (v_cnt < 2)  ; 

/*----------------------------2、区域有效标志信号(rgb_valid):计数器确定,680*480,底边的大小----------------------------------*/
wire rgb_valid ;
assign rgb_valid =  (((h_cnt >= H_SYNC + H_BACK + H_LEFT )&&(h_cnt < H_SYNC + H_BACK + H_LEFT + H_VALID)) 
                    &&((v_cnt >= V_SYNC + V_BACK + V_TOP)&&(v_cnt < V_SYNC + V_BACK + V_BOTTOM + V_VALID)));

//h_valid 从低电平向高电平跳变后,显示器开始采集rgb[15:0]端口上的值对应输出的模拟量,故在h_valid变高之前,
//将坐标值(pic_x,pic_y)的值送入vga_pic端口。当h_valid跳变时,第0个像素的数据就送到rgb[15:0]端口。
//设定一个pix_data_req用来控制坐标型号的产生。则pix_data_req在(143<= h_cnt <= 782)且 (35<= v_cnt <=514)范围内有效
/*-----------------------------3、色彩请求信号(pix_data_req):计数器确定,在行扫描信号上超前pic_valid信号一个时钟----------------------*/
wire pix_data_req ;
assign  pix_data_req = (((h_cnt >= H_SYNC + H_BACK + H_LEFT - 1'b1 )&&(h_cnt < H_SYNC + H_BACK + H_LEFT + H_VALID - 1'b1)) 
                        &&((v_cnt >= V_SYNC + V_BACK + V_TOP)&&(v_cnt < V_SYNC + V_BACK + V_BOTTOM + V_VALID)));

/*-----------------------------4、像素点坐标:请求信号、计数器、显示区域确定。坐标即距有效显示区域边界的值-----------------------------*/
assign pix_x = (pix_data_req == 1'b1)?(h_cnt - H_SYNC - H_BACK - H_LEFT -1'b1):10'h3ff ;//行场计数器的值计算出相应像素点的坐标位置
assign pix_y = (pix_data_req == 1'b1)?(v_cnt - V_SYNC - V_BACK - V_TOP)       :10'h3ff ;//10'h3ff=1023,即最大值

/*-----------------------------5、输出pic_rgb:将vga_pic模块生成的图形样式(pix_data)赋值给pic_rgb,并显示-----------------------------*/
assign pic_rgb = (rgb_valid == 1'b1)?pix_data_out : 16'b0 ;


endmodule

【顶层模块(top_vga_rom_picjump)】

module top_vga_rom_picjump(
    input         sys_clk   ,
    input         sys_rst_n ,
    output        hsync     ,
    output        vsync     ,
    output [15:0] rgb       
);

    wire vga_clk   ;
    wire locked    ;
    wire pix_x     ; 
    wire pix_y     ; 
    wire pix_data  ; 
    wire vga_rst_n ;

assign vga_rst_n = (sys_rst_n && locked);

//25MHZ时钟生成模块
clk_gen clk_gen_inst01(
    . inclk0 (sys_clk   ) ,
    . arset  (~sys_rst_n) ,
    . c0     (vga_clk   ) ,
    . locked (locked    ) 
);

//图像样式生成模块
vga_pic vga_pic_inst01(
    . vga_clk       (vga_clk      ) ,
    . vga_rst_n     (vga_rst_n    ) ,
    . pix_x         (pix_x        ) ,
    . pix_y         (pix_y        ) ,
    
    . pix_data_out  (pix_data_out )
); 

//vga时序控制模块,输出像素点坐标,并根据图像样式输出行场信号
// wire rgb_valid ;
vga_ctrl vga_ctrl_inst01(
    . vga_clk      (vga_clk      ), 
    . vga_rst_n    (vga_rst_n    ),
    . pix_data_out (pix_data_out ),//pix_data即显示图像的信息,rgb565模式共16位
    
    . pix_x      (pix_x    ),//680*640,10位才包含
    . pix_y      (pix_y    ),
    . hsync      (hsync    ),
    . vsync      (vsync    ),
    . pic_rgb    (rgb      ) 
);

endmodule