【FPGA】VGA显示文字、彩条、图片——基于DE2-115

前言

本文内容:

  1. 深入了解VGA协议,理解不同显示模式下的VGA控制时序参数(行频、场频、水平/垂直同步时钟周期、显示后沿/前沿等概念和计算方式);

  2. 通过Verilog编程,在至少2种显示模式下(640480@60Hz,1024768@75Hz)分别实现以下VGA显示,并对照VGA协议信号做时序分析:1)屏幕上显示彩色条纹;2)显示自定义的汉字字符(姓名-学号);3)输出一幅彩色图像。

  3. 在Verilog代码中,将行、场同步信号中,故意分别加入一定 ms延时(用delay命令),观察会出现什么现象。


一、VGA概述

1.1 简述

图像显示设备在日常生活中随处可见,例如家庭电视机、计算机显示屏幕等,这些设备之所以能够显示我们需要的数据图像信息,归功于视频传输接口。常见的视频传输接口有三种:VGA 接口、 DVI 接口和 HDMI 接口,目前的显示设备都配有这三种视频传输接口。

三类视频接口的发展历程为 VGA → DVI → HDMI 。其中 VGA 接口出现最早,只能传输模拟图像信号; 随后出现的 DVI 接口又分为三类: DVI-A 、 DVI-D 、 DVI-I ,分别可传输纯模拟图像信号、纯数字图像信号和兼容模拟、数字图像信号;最后的 HDMI 在传输数字图像信号的基础上又可以传输音频信号。

VGA,英文全称“ Video Graphics Array ”,译为视频图形阵列,是一种使用模拟信号进行视频传输的标准协议,由 IBM 公司于 1987 年推出,因其分辨率高、显示速度快、颜色丰富等优点,广泛应用于彩色显示器领域。由于 VGA 接口体积较大,与追求小巧便携的笔记本电脑背道而驰,在笔记本电脑领域,VGA 接口已被逐渐淘汰,但对于体积较大的台式机,这种情况并未发生,虽然 VGA 标准在当前个人电脑市场中已经过时,但因其在显示标准中的重要性和良好的兼容性,VGA 仍然是最多制造商所共同支持的一个标准,个人电脑在加载自己独特驱动程序之前,都必须支持 VGA 的标准。
早期的 CRT 显示器只能接收模拟信号,不能接收数字信号,计算机内部显卡将数字信号转换成模拟信号,通过 VGA 接口传给 VGA 显示器,虽然现如今许多种类的显示器可以直接接收数字信号,但为了兼容显卡的 VGA 接口,大都支持 VGA 标准。

在这里插入图片描述

VGA 接口中以针式引出信号线的称为公头,以孔式引出信号线的称为母头。在计算机VGA 显示器上一般引出母头接口,使用两头均为公头的 VGA 连接线将计算机与 VGA显示器连接起来,两者图像传输时,使用的是 VGA 图像传输标准,该标准的具体内容在后面博文会详细说明。VGA 公头、母头接口和 VGA 连接线。在这里插入图片描述

图2 VGA接口(左边为母头,右边为公头)

在这里插入图片描述

图3 VGA连接线

1.2 管脚定义

下面,我们结合 VGA 接口引脚图和各引脚定义表格,对 VGA 接口各引脚做一下简单介绍。

![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/a669aee5a12343f9807b1bbae8fe3199.png

图4 VGA接口引脚图
引脚1:红基色;引脚2:绿基色;引脚3:蓝基色;
引脚6:红色地;引脚7:绿色地;引脚8:蓝色地;引脚10:数字地
引脚11:地址码0;引脚12:地址码1;引脚4:地址码2;引脚15:地址码3
引脚13:行同步;引脚14:场同步
引脚5:自测试(各厂家定义不同);引脚9:保留(各厂家定义不同)

      由图4可知,VGA 接口共有 15 个引脚,分为 3 排,每排各 5 个, 按照自上而下、从左向右的顺序排列。其中第一排的引脚 1 、 2 、 3 和第三排的引脚 13 、 14 最为重要。
      VGA 使用工业界通用的 RGB 色彩模式作为色彩显示标准,这种色彩显示标准是根据三原色中红色、绿色、蓝色所占比例多少及三原色之间的相互叠加得到各式各样的颜色。引脚 1 红基色 (RED) 、引脚 2 绿基色 (GREEN) 、引脚 3 蓝基色 (BLUE) 就是 VGA 接口中负责传输三原色的传输通道。要注意的是,这 3 个引脚传输的是模拟信号。
      引脚 13 行同步信号 (HSYNC) 、引脚 14 场同步信号 (VSYNC) ,这两个信号,是在 VGA显示图像时,负责同步图像色彩信息的同步信号。在后面博文中,我们会对这两个信号进行详细讲解。
      引脚 5 、 9 :这两个引脚分别是 VGA 接口的自测试和预留接口,不过不同生产厂家对这两个接口定义不同,在接线时,两引脚可悬空不接。
      引脚 4 、 11 、 12 、 15 :这四个是 VGA 接口的地址码,可以悬空不接。
      引脚 6 、 7 、 8 、 10 :这四个引脚接地,无需解释。

1.3 VGA显示原理

VGA 显示器显示图像,并不是直接让图像在显示器上显示出来,而是采用扫描的方式,将构成图像的像素点,在行同步信号和场同步信号的同步下,按照从上到下、由左到右的顺序扫描到显示屏上。VGA 显示器扫描方式,具体见图 1 。
在这里插入图片描述

结合

  结合 VGA 显示器扫描方式示意图,我们简要说明一下 VGA 显示器的扫描规律。
   (1) 在行、场同步信号的同步作用下,扫描坐标定位到左上角第一个像素点坐标;
   (2) 自左上角 ( 第一行 ) 第一个像素点坐标,逐个像素点向右扫描 ( 图中第一个水平方向箭头) ;
   (3) 扫描到第一行最后一个数据,一行图像扫描完成,进行图像消隐,扫描坐标自第一行行尾转移到第二行行首( 图中第一条虚线 ) ;
   (4) 重复若干次扫描至最后一行行尾,一帧图像扫描完成,进行图像消隐,扫描坐标跳转回到左上角第一行行首( 图中对角线箭头 ) ,开始下一帧图像的扫描。

   在扫描的过程中会对每一个像素点进行单独赋值,使每个像素点显示对应色彩信息,
  当一帧图像扫描结束后,开始下一帧图像的扫描,循环往复,当扫描速度足够快,加之人眼的视觉暂留特性,我们会看到一幅完整的图片,而不是一个个闪烁的像素点。这就是VGA 显示的原理。

1.4 VGA时序标准

      为了适应匹配不同厂家的 VGA 显示器, VGA 视频传输接口有自己的一套 VGA 时序标准,只有遵循 VGA 的时序标准,才能正确的进行图像信息的显示。在这里我们以 VESA VGA 时序标准为例,为大家讲解一下 VGA 时序标准,具体见图 2 。
在这里插入图片描述

      由 VESA VGA 时序标准图可知, VGA 时序由两部分构成,行同步时序与场同步时序,为了方便大家理解,我们将行同步时序与场同步时序分开讲解。
     (1) 行同步时序,具体见图 3 。

在这里插入图片描述

图 3 行同步时序图

      图中 Video 代表传输的图像信息, HSync 表示行同步信号。 HSync 自上升沿起到下一个上升沿止为一个完整周期,我们称之为行扫描周期。
      一个完整的行扫描周期,包含 6 部分: Sync (同步)、 Back Porch (后沿)、 Left Border(左边框)、 Addressable Video (有效图像)、 Right Border (右边框)、 Front Porch(前沿),这 6 部分的基本单位是 pixel (像素),即一个像素时钟周期。在一个完整的行扫描周期中,Video 图像信息在 HSync 行同步信号的同步下完成一行图像的扫描显示,Video 图像信息只有在 “Addressable” Video (有效图像)阶段,图像信息有效,其他阶段图像信息无效。
      HSync 行同步信号在 Sync (同步)阶段,维持高电平,其他阶段均保持低电平,在下一个行扫描周期的 Sync (同步)阶段, HSync 行扫描信号会再次拉高,其他阶段拉低,周而复始。
      (2) 场同步时序,具体见图 4 。

在这里插入图片描述

图 4 场同步时序图

      理解了行同步时序,场同步时序就更容易理解了,两者相类似,如图所示,图中Video 代表传输的图像信息, VSync 表示场同步信号VSync 自上升沿起到下一个上升沿止为一个完整周期,我们称之为场扫描周期
      一个完整的场扫描周期,也包含 6 部分: Sync (同步)、 Back Porch (后沿)、 Top Border(上边框)、 “Addressable” Video (有效图像)、 Bottom Border (底边框)、 Front Porch(前沿),与行同步信号不同的是,这 6 部分的基本单位是 line (行),即一个完整的行扫描周期。
       在一个完整的场扫描周期中,Video 图像信息在 HSync (行同步信号)VSync (场同步信号)的共同作用下完成一帧图像的显示,Video 图像信息只有在 “Addressable” Video(有效图像)阶段,图像信息有效,其他阶段图像信息无效。VSync 行同步信号在 Sync (同步)阶段维持高电平,其他阶段均保持低电平,完成一个场扫描周期后,进入下一帧图像的扫描。
       综上所述,将行同步时序图与场同步时序图结合起来就构成了 VGA 时序图,具体见图 5。

在这里插入图片描述

图 5 VGA时序图

图中的红色区域表示在一个完整的行扫描周期中,Video 图像信息只在此区域有效,黄色区域表示在一个完整的场扫描周期中,Video 图像信息只在此区域有效,两者相交的橙色区域,就是 VGA 图像的最终显示区域

1.5 VGA 显示模式及相关参数

行同步时序可分为 6 个阶段,对于这 6 个阶段的参数是有严格定义的,参数配置不正确,VGA 不能正常显示。 VGA 显示器可支持多种分辨率,不同分辨率对应个阶段的参数是不同的,常用 VGA 分辨率时序参数,具体见图 6 。
在这里插入图片描述
       下面我们以经典 VGA 显示模式 640x480@60 为例,为读者讲解一下 VGA 显示的相关参数。

       (1) 显示模式: 640x480@60
              640x480 是指 VGA 的分辨率, 640 是指有效显示图像每一行有 640 个像素点, 480 是指每一帧图像有 480 行, 640 * 480 = 307200 ≈ 300000 ,每一帧图片包含约 30 万个像素点,之前某品牌手机广告上所说的 30 万像素指的就是这个; @60 是指 VGA 显示图像的刷新频率,60 就是指 VGA 显示器每秒刷新图像 60 次,即每秒钟需要显示 60 帧图像。

       (2) 时钟 (MHz) : 25.175MHz
              这是 VGA 显示的工作时钟,像素点扫描频率

       (3) 行同步信号时序 ( 像素 ) 、场同步信号时序 ( 行数 )
              行同步信号时序分为 6 段, Sync (同步)、 Back Porch (后沿)、 Left Border (左边框)、 “Addressable” Video (有效图像)、 Right Border (右边框)、 Front Porch (前沿),这 6 段构成一个行扫描周期,单位为像素时钟周期。
              同步阶段,参数为 96,指在行时序的同步阶段,行同步信号需要保持 96 个像素时钟周期的高电平, 其他几个阶段与此相似。
              场同步信号时序与其类似,只是单位不再是像素时钟周期,而是一个完整的行扫描周期,在此不再赘述。
              在这里,我们看回图 6,由图可知,即使 VGA 显示分辨率相同,但刷新频率不同的话,相关参数也存在差异,如 640x480@60 、 640x480@75 ,这两个显示模式虽然具有相同的分辨率,但是 640x480@75 的刷新频率更快,所以像素时钟更快,时序参数也有区别。

       下面我们以显示模式 640x480@60 、 640x480@75 为例,学习一下时钟频率的计算方法。

       行扫描周期 * 场扫描周期 * 刷新频率 = 时钟频率
       640x480@60:
       行扫描周期:800( 像素 ) ,场扫描周期: 525( 行扫描周期 ) 刷新频率: 60Hz
       800 * 525 * 60 = 25,200,000 ≈ 25.175MHz(误差忽略不计)
       640x480@75:
       行扫描周期:840( 像素 ) 场扫描周期: 500( 行扫描周期 ) 刷新频率: 75Hz
       840 * 500 * 75 = 31,500,000 = 31.5MHz
       在计算时钟频率时,大家要谨记一点,要使用行扫描周期场扫描周期的参数进行计算,不能使用有效图像的参数进行计算,虽然在有效图像外的其他阶段图像信息均无效,但图像无效阶段的扫描也花费了扫描时间。
       以上就是对 VGA 显示标准中分辨率相关参数的讲解,在编写 VGA 驱动时,我们要根据 VGA 显示模式的不同调整相关参数,只有这样 VGA 图像才能正常显示。

二、VGA显示自定义的汉字字符

笔主整篇文章中使用的板子都是 EP4CE115F29C7,使用其他板子问题不大,但引脚绑定不太一样,可自行对应板子查找更改。

2.1 点阵汉字生成

字模的提取可通过字符取模软件来实现,在这里我们使用取模软件“PCtoLCD2002”来获取汉字 李菊芳-632109160602 的字模。如下图所示:
在这里插入图片描述

打开之后会发现软件中的字体、字宽和字高都是无法设置的,这个时候点击菜单栏的“模式”,选择“字符模式”。
切换到字符模式后,就可以设置字体、字宽和字高了。字宽和字高的值越高,显示在LCD屏上的字符就越大,但是代码也需要做相应的修改。这里将字体选择默认的“宋体”,字宽和字高设置成“32”,然后在下方文本框中输入汉字,如下图所示:

在这里插入图片描述

2.2 生成BMP文件

然后点击文件-另存为,把图片保存为BMP图片,再点击文件-打开,切换到图形模式,把保存的BMP图片打开得到整体的字符

在这里插入图片描述

2.3 生成txt文件

再点击选项按如下参数设置

在配置界面中,当鼠标悬浮在各配置选项上时,软件会自动提示当前配置的含义。需要注意的是左下角“每行显示数据”是以字节(Byte)为单位的,而一个字节的数据为8个bit,即可以表示一行点阵中的8个像素点。由于图中的点阵每行为304个像素点,所以需要304/8=38个Byte的数据来表示一行,因此将“每行显示数据—点阵”处设置为38
配置字模选项完成后,点击“生成字模”,即可得到汉字所对应的点阵数据。
在这里插入图片描述
最后点击生成字符并保存字符为文本文件
在这里插入图片描述

最后得到字符如下
在这里插入图片描述

把得到的字符在verilog里面使用即可
在这里插入图片描述

2.4 实现效果

姓名+学号 居中显示:

在这里插入图片描述

三、VGA显示条纹

3.1 实现流程

时钟分频模块
640x480像素的VGA协议所需时钟频率25MHZ,使用clk IP核进行时钟分频

根据当前行地址判断需要显示的颜色即可。

输出颜色竖条

// 状态输出逻辑,根据不同的状态输出不同的RGB数据
always @( * ) begin
    case ( states_current )
        //彩条
        states_1 : begin
            if ( addr_h == 0 ) begin
                rgb_data = black;
            end
            else if ( addr_h >0 && addr_h <81 ) begin
                rgb_data = red;
            end
            else if ( addr_h >80 && addr_h <161 ) begin
                rgb_data = orange;
                // rgb_data = red;
            end
            else if ( addr_h >160 && addr_h <241 ) begin
                rgb_data = yellow;
            end
            else if ( addr_h >240 && addr_h <321 ) begin
                rgb_data = green;
            end
            else if ( addr_h >320 && addr_h <401 ) begin
                rgb_data = blue;
            end
            else if ( addr_h >400 && addr_h <481 ) begin
                rgb_data = indigo;
            end
            else if ( addr_h >480 && addr_h <561 ) begin
                rgb_data = purple;
            end
            else if ( addr_h >560 && addr_h <641 ) begin
                rgb_data = white;
            end
            else begin
                rgb_data = black;
            end
            
        end

3.2 实现效果

在这里插入图片描述

四、VGA输出一幅彩色图像

在前面的学习中了解到图像的格式有多种,例如JPEG,BMP,PNG,JPG等,图像的位数也有单色、16色、256色、4096色、16位真彩色、24位真彩色、32位真彩色在这里插入图片描述
这几种。
VGA的驱动程序显示的格式为RGB565,我们先找到一张需要显示的彩色图片,经过处理,将该图片转化为ROM可以存储的格式,然后VGA驱动程序从ROM中读取数据,输出到VGA显示屏显示。尽量选一张小的图片,因为ROM存储空间有限。

4.1 bmp图片转hex文件

使用BMP2Mif软件将bmp格式图片转换为mif文件

在这里插入图片描述

转换后的.mif文件:
在这里插入图片描述

4.2 引入ROM ip核

新建Quartus工程,产生ROM IP核,将生成的mif文件保存在ROM中
双击选择ROM:1-PORT
在这里插入图片描述

更改设置,words大小设置要大于图片大小(50x49x24=58800< 65536),然后next
在这里插入图片描述

取消勾选q
在这里插入图片描述

如果是16位位图,就加载HEX文件,然后next

这里因为我们转换的是24位位图,所以在ROM里要引入 .mif 文件。(16位位图不用管)
在这里插入图片描述

勾选上输出 inst,方便例化,然后finish

在这里插入图片描述

这里同时要调用前面的pll ip核生成一个25mHz的时钟。

4.3 代码实现

data_drive.v文件里,从ikun_rom取出图片数据。
在这里插入图片描述
其他 .v 文件与前文一致。

4.4 实现效果

640*480分辨率显示下:彩色图像显示在正中间

在这里插入图片描述

五、代码

5.1 时钟分频

分别使用640×480 60HZ和800×600 72HZ,对应时钟分别为25M和50M,需要使用PLL进行分频 时钟频率 = 行帧长 × 列帧长 * 刷新率

640 ×480 60HZ对应时钟频率= 800 ×525 × 60 = 25.2M

ip核里面找到ALTPLL
在这里插入图片描述

基础时钟选择50M
在这里插入图片描述

取消勾选输出使能
在这里插入图片描述

c0默认输出50M即可, c1分频到25M,如需其他时钟频率可以自己进行设置
在这里插入图片描述
勾选如下选项后finish
在这里插入图片描述

5.2 vga驱动模块

vga.drive.v

// /*
module vga_dirve (
        input			wire						clk,            //系统时钟    
        input			wire						rst_n,          //复位
        input			wire		[ 15:0 ]		rgb_data,       //RGB--565,即pixel_data[15:11]控制R、pixel_data[10:5]控制G、pixel_data[4:0]控制B
        
        output			wire							vga_clk,    //vga时钟 25M

        output			reg							h_sync,     //行同步信号
        output			reg							v_sync,     //场同步信号

        output			reg		[ 11:0 ]				addr_h, //行地址
        output			reg		[ 11:0 ]				addr_v,  //列地址
        
        output			wire		[ 4:0 ]				rgb_r,  //红基色
        output			wire		[ 5:0 ]				rgb_g,  //绿基色
        output			wire		[ 4:0 ]				rgb_b  //蓝基色
);


// 定义VGA信号的参数,基于640x480 60Hz的VGA模式
// 640 * 480 60HZ
localparam	 H_FRONT = 16; // 行同步前沿信号周期长
localparam	 H_SYNC  = 96; // 行同步信号周期长
localparam	 H_BLACK = 48; // 行同步后沿信号周期长
localparam	 H_ACT   = 640; // 行显示周期长
localparam	 V_FRONT = 11; // 场同步前沿信号周期长
localparam	 V_SYNC  = 2; // 场同步信号周期长
localparam	 V_BLACK = 31; // 场同步后沿信号周期长
localparam	 V_ACT   = 480; // 场显示周期长

// 800 * 600 72HZ
// localparam	 H_FRONT = 40; // 行同步前沿信号周期长
// localparam	 H_SYNC  = 120; // 行同步信号周期长
// localparam	 H_BLACK = 88; // 行同步后沿信号周期长
// localparam	 H_ACT   = 800; // 行显示周期长
// localparam	 V_FRONT = 37; // 场同步前沿信号周期长
// localparam	 V_SYNC  = 6; // 场同步信号周期长
// localparam	 V_BLACK = 23; // 场同步后沿信号周期长
// localparam	 V_ACT   = 600; // 场显示周期长


// 计算总的行和场周期
localparam	H_TOTAL = H_FRONT + H_SYNC + H_BLACK + H_ACT; // 行周期 16+96+48+640 = 800
localparam	V_TOTAL = V_FRONT + V_SYNC + V_BLACK + V_ACT; // 列周期 11+2+6+31+480 = 512
reg			[ 11:0 ]			cnt_h			; // 行计数器 0-799
reg			[ 11:0 ]			cnt_v			; // 场计数器 0-524
reg			[ 15:0 ]			rgb			; // 对应显示颜色值

// 对应计数器开始、结束、计数信号
wire							flag_enable_cnt_h			;
wire							flag_clear_cnt_h			;
wire							flag_enable_cnt_v			;
wire							flag_clear_cnt_v			;
wire							flag_add_cnt_v  			;
wire							valid_area      			;


// 25M时钟 行周期*场周期*刷新率 = 800 * 525 * 60
wire							clk_25			;
// 50M时钟 1040 * 666 * 72
wire							clk_50			;
//PLL
pll	pll_inst (
	.areset ( ~rst_n ),
	.inclk0 ( clk ),
	.c0 ( clk_50 ), //50M
	.c1 ( clk_25 ) //25M
);

//根据不同分配率选择不同频率时钟
assign vga_clk = clk_25;


// 行计数
always @( posedge vga_clk or negedge rst_n ) begin
    if ( !rst_n ) begin
        cnt_h <= 0;
    end
    else if ( flag_enable_cnt_h ) begin
        if ( flag_clear_cnt_h ) begin
            cnt_h <= 0;
        end
        else begin
            cnt_h <= cnt_h + 1;
        end
    end
    else begin
        cnt_h <= 0;
    end
end
assign flag_enable_cnt_h = 1;
assign flag_clear_cnt_h  = cnt_h == H_TOTAL - 1;



// 行同步信号
always @( posedge vga_clk or negedge rst_n ) begin
    if ( !rst_n ) begin
        h_sync <= 1;
    end
    else if ( cnt_h == H_SYNC - 1 ) begin // 同步周期时为1
        h_sync <= 0;
    end
    else if ( flag_clear_cnt_h ) begin // 其余为0
        h_sync <= 1;
    end
    else begin
        h_sync <= h_sync;
    end
end


// 场计数
always @( posedge vga_clk or negedge rst_n ) begin
    if ( !rst_n ) begin
        cnt_v <= 0;
    end
    else if ( flag_enable_cnt_v ) begin
        if ( flag_clear_cnt_v ) begin
            cnt_v <= 0;
        end
        else if ( flag_add_cnt_v ) begin
            cnt_v <= cnt_v + 1;
        end
        else begin
            cnt_v <= cnt_v;
        end
    end
    else begin
        cnt_v <= 0;
    end
end



assign flag_enable_cnt_v = flag_enable_cnt_h;
assign flag_clear_cnt_v  = cnt_v == V_TOTAL - 1;
assign flag_add_cnt_v    = flag_clear_cnt_h;



// 场同步信号
always @( posedge vga_clk or negedge rst_n ) begin
    if ( !rst_n ) begin
        v_sync <= 1;
    end
    else if ( cnt_v == V_SYNC - 1 ) begin
        v_sync <= 0;
    end
        else if ( flag_clear_cnt_v ) begin
        v_sync <= 1;
        end
    else begin
        v_sync <= v_sync;
    end
end

// 对应有效区域行地址 1-640
always @( posedge vga_clk or negedge rst_n ) begin
    if ( !rst_n ) begin
        addr_h <= 0;
    end
    else if ( valid_area ) begin
        addr_h <= cnt_h - H_SYNC - H_BLACK + 1;
    end
    else begin
        addr_h <= 0;
    end
end
// 对应有效区域列地址 1-480
always @( posedge vga_clk or negedge rst_n ) begin
    if ( !rst_n ) begin
        addr_v <= 0;
    end
    else if ( valid_area ) begin
        addr_v <= cnt_v -V_SYNC - V_BLACK + 1;
    end
    else begin
        addr_v <= 0;
    end
end
// 有效显示区域
assign valid_area = cnt_h >= H_SYNC + H_BLACK && cnt_h <= H_SYNC + H_BLACK + H_ACT && cnt_v >= V_SYNC + V_BLACK && cnt_v <= V_SYNC + V_BLACK + V_ACT;


// 显示颜色
always @( posedge vga_clk or negedge rst_n ) begin
    if ( !rst_n ) begin
        rgb <= 16'h0;
    end
    else if ( valid_area ) begin
        rgb <= rgb_data;
    end
    else begin
        rgb <= 16'b0;
    end
end
assign rgb_r = rgb[ 15:11 ];
assign rgb_g = rgb[ 10:5 ];
assign rgb_b = rgb[ 4:0 ];
endmodule // vga_dirve
// */

5.3 显示数据生成模块

data_drive.v


module data_drive (
    input			wire						vga_clk,      // VGA时钟输入
    input			wire						rst_n,        // 复位信号,低电平有效
    input			wire		[ 11:0 ]		addr_h,       // 水平地址输入
    input			wire		[ 11:0 ]		addr_v,       // 垂直地址输入
    input			wire		[ 2:0 ]		 key,          // 三个按键输入

    output			reg		[ 15:0 ]				rgb_data      // 输出的RGB数据

);

// 定义一些颜色的16位表示
localparam	red    = 16'd63488;
localparam	orange = 16'd64384;
localparam	yellow = 16'd65472;
localparam	green  = 16'd1024;
localparam	blue   = 16'd31;
localparam	indigo = 16'd18448;
localparam	purple = 16'd32784;
localparam	white  = 16'd65503;
localparam	black  = 16'd0;


//显示的名字
// 存储显示字符的每一行数据
// reg [ 383:0 ] char_line[ 64:0 ];

//李菊芳-632109160602  -16
//16行,每行152个bit
// reg [ 152:0 ] char_line[ 15:0 ];

//李菊芳-632109160602 ——32
//32*3+16*13 = 304 304/8 = 38
reg [ 303:0 ] char_line[ 31:0 ];

// 定义显示状态的参数
localparam	states_1 = 1; // 彩条
localparam	states_2 = 2; // 字符
localparam	states_3 = 3; // 图片

// 图片的尺寸参数
// parameter	height = 78; // 图片高度
// parameter	width  = 128; // 图片宽度

//ikun2
parameter	height = 52; // 图片高度
parameter	width  = 52; // 图片宽度

// 当前状态和下一个状态的寄存器
reg			[ 2:0 ]			states_current			; // 当前状态
reg			[ 2:0 ]			states_next			    ; // 下个状态

// ROM的地址寄存器和数据输出
reg			[ 13:0 ]		rom_address				; // ROM地址
// wire		[ 15:0 ]		rom_data				; // 图片数据
wire		[ 23:0 ]		rom_data				; // 显示彩色图片数据


// 状态机的标志位
wire							flag_enable_out1			; // 文字有效区域标志
wire							flag_enable_out2			; // 图片有效区域标志
wire							flag_clear_rom_address		; // ROM地址清零标志
wire							flag_begin_h			    ; // 图片显示行开始标志
wire							flag_begin_v			    ; // 图片显示列开始标志

// 状态转移逻辑
always @( posedge vga_clk or negedge rst_n ) begin
    if ( !rst_n ) begin
        states_current <= states_1;// 复位时设置初始状态为彩条
    end
    else begin
        states_current <= states_next;// 否则转移到下一个状态
    end
end

// 状态判断逻辑,根据按键输入更新下一个状态
always @( posedge vga_clk or negedge rst_n ) begin
    if ( !rst_n ) begin
        states_next <= states_1;
    end
    else if ( key[ 0 ] ) begin
        states_next <= states_1;
    end
        else if ( key[ 1 ] ) begin
        states_next <= states_2;
        end
        else if ( key[ 2 ] ) begin
        states_next <= states_3;
        end
    else begin
        states_next <= states_next;
    end
end

// 状态输出逻辑,根据不同的状态输出不同的RGB数据
always @( * ) begin
    case ( states_current )
        //彩条
        states_1 : begin
            if ( addr_h == 0 ) begin
                rgb_data = black;
            end
            else if ( addr_h >0 && addr_h <81 ) begin
                rgb_data = red;
            end
            else if ( addr_h >80 && addr_h <161 ) begin
                // rgb_data = orange;
                rgb_data = red;
            end
            else if ( addr_h >160 && addr_h <241 ) begin
                rgb_data = yellow;
            end
            else if ( addr_h >240 && addr_h <321 ) begin
                rgb_data = green;
            end
            else if ( addr_h >320 && addr_h <401 ) begin
                rgb_data = blue;
            end
            else if ( addr_h >400 && addr_h <481 ) begin
                rgb_data = indigo;
            end
            else if ( addr_h >480 && addr_h <561 ) begin
                rgb_data = purple;
            end
            else if ( addr_h >560 && addr_h <641 ) begin
                rgb_data = white;
            end
            else begin
                rgb_data = black;
            end
            
        end
        //字符
        states_2 : begin
            if ( flag_enable_out1 ) begin
                //480*640
                // rgb_data = char_line[ addr_v-208 ][ 532 - addr_h ]? white:black;
                rgb_data = char_line[ addr_v-224 ][ 472 - addr_h ]? white:black;
            end
            else begin
                rgb_data = black;
            end
        end
        //图片
        states_3 : begin
            if ( flag_enable_out2 ) begin
                rgb_data = rom_data;
            end
            else begin
                rgb_data = black;
            end
            
        end
        default: begin
            case ( addr_h )
                0 : rgb_data      = black;
                1 : rgb_data      = red;
                81 : rgb_data     = orange;
                161: rgb_data     = yellow;
                241: rgb_data     = green;
                321: rgb_data     = blue;
                401: rgb_data     = indigo;
                481: rgb_data     = purple;
                561: rgb_data     = white;
                default: rgb_data = rgb_data;
            endcase
        end
    endcase
end

//李骏飞的居中显示参数
//32*3+16*13 = 304 304/8 = 38
// 根据当前状态和地址范围设置标志位
parameter ljf_width = 304;  // 字符数据的宽度
parameter ljf_height = 32;  // 字符数据的高度
assign flag_enable_out1 = states_current == states_2 && 
                           addr_h >= (640 - ljf_width) / 2 && 
                           addr_h <  ((640 - ljf_width) / 2) + ljf_width && 
                           addr_v >= (480 - ljf_height) / 2 && 
                           addr_v <  ((480 - ljf_height) / 2) + ljf_height;

// assign flag_begin_h     = addr_h > ( ( 640 - width ) / 2 ) && addr_h < ( ( 640 - width ) / 2 ) + width + 1;
// assign flag_begin_v     = addr_v > ( ( 480 - height )/2 ) && addr_v <( ( 480 - height )/2 ) + height + 1;
assign flag_begin_h     = addr_h >= ( ( 640 - width ) / 2 ) && addr_h < ( ( 640 - width ) / 2 ) + width ;
assign flag_begin_v     = addr_v >= ( ( 480 - height )/2 ) && addr_v <( ( 480 - height )/2 ) + height ;
assign flag_enable_out2 = states_current == states_3 && flag_begin_h && flag_begin_v;

//ROM地址计数器
always @( posedge vga_clk or negedge rst_n ) begin
    if ( !rst_n ) begin
        rom_address <= 0;// 复位时清零ROM地址
    end
    else if ( flag_clear_rom_address ) begin //计数满清零
        rom_address <= 0;
    end
    else if ( flag_enable_out2 ) begin  //在有效区域内+1
        rom_address <= rom_address + 1;
    end
    else begin  //无效区域保持
        rom_address <= rom_address;
    end
end
assign flag_clear_rom_address = rom_address == height * width - 1 || states_current != states_3;

// 初始化显示文字的逻辑
always@( posedge vga_clk or negedge rst_n ) begin
    if ( !rst_n ) begin
        //李菊芳-632109160602 ——32
        //32*3+16*13 = 304 304/8 = 38
        char_line[0] =  304'h0000000000000000000000000000000000000000000000000000000000000000000000000000;
        char_line[1] =  304'h0000000000000000000000000000000000000000000000000000000000000000000000000000;
        char_line[2] =  304'h0003800000101000002008000000000000000000000000000000000000000000000000000000;
        char_line[3] =  304'h0003C000001C1C0000380E000000000000000000000000000000000000000000000000000000;
        char_line[4] =  304'h000380100018180000300C000000000000000000000000000000000000000000000000000000;
        char_line[5] =  304'h000380380018181800300C300000000000000000000000000000000000000000000000000000;
        char_line[6] =  304'h3FFFFFFC3FFFFFFC1FFFFFF8000001E007C007E0008003C007C0008001E003C001E003C007E0;
        char_line[7] =  304'h180FE0000098180000300C000000061818600838018006201820018006180620061806200838;
        char_line[8] =  304'h001FF00000D8180000330C0000000C18303010181F800C3030101F800C180C300C180C301018;
        char_line[9] =  304'h003FB80000F8180000318C00000008183018200C01801818301801800818181808181818200C;
        char_line[10] = 304'h 007B9C00019000200030C800000018003018200C01801818600801801800181818001818200C;
        char_line[11] = 304'h 00F39E0001FFFFF000006000000010003018300C01801808600C01801000180810001808300C;
        char_line[12] = 304'h 01E38F800300003000004030000010000018300C0180300C600C01801000300C1000300C300C;
        char_line[13] = 304'h 03C387F0020300301FFFFFF8000030000018000C0180300C600C01803000300C3000300C000C;
        char_line[14] = 304'h 07838DFE0483083000060000000033E0003000180180300C600C018033E0300C33E0300C0018;
        char_line[15] = 304'h 1FFFFEF808431C300006000000003630006000180180300C600C01803630300C3630300C0018;
        char_line[16] = 304'h 38401F3010631830000C00007FFE381803C000300180300C701C01803818300C3818300C0030;
        char_line[17] = 304'h 60003C0020633030000C008000003808007000600180300C302C01803808300C3808300C0060;
        char_line[18] = 304'h 0001F00000232330000FFFC00000300C001800C00180300C186C0180300C300C300C300C00C0;
        char_line[19] = 304'h 0001E0000FFFFFB0000C00C00000300C000801800180300C0F8C0180300C300C300C300C0180;
        char_line[20] = 304'h 0001E010000F0030000C01800000300C000C03000180300C000C0180300C300C300C300C0300;
        char_line[21] = 304'h 0001C038000B8030001801800000300C000C02000180300C00180180300C300C300C300C0200;
        char_line[22] = 304'h 7FFFFFFC001B6030001801800000300C300C04040180180800180180300C1808300C18080404;
        char_line[23] = 304'h 3801C00000333830003001800000180C300C08040180181800100180180C1818180C18180804;
        char_line[24] = 304'h 0001C00000631C20003001800000180830081004018018183030018018081818180818181004;
        char_line[25] = 304'h 0001C00000C30C200060030000000C183018200C01800C30306001800C180C300C180C30200C;
        char_line[26] = 304'h 0001C0000183046000C0030000000E3018303FF803C0062030C003C00E3006200E3006203FF8;
        char_line[27] = 304'h 0001C000020300600180C300000003E007C03FF81FF803C00F801FF803E003C003E003C03FF8;
        char_line[28] = 304'h 003FC0000C030FE002003E000000000000000000000000000000000000000000000000000000;
        char_line[29] = 304'h 0007C000300303C00C001E000000000000000000000000000000000000000000000000000000;
        char_line[30] = 304'h 0003800000020080300008000000000000000000000000000000000000000000000000000000;
        char_line[31] = 304'h 0000000000000000000000000000000000000000000000000000000000000000000000000000;


    end
end

// /*
//ikun
// ROM实例化,根据地址输出数据
ikun_rom	ikun_rom_inst (
.address ( rom_address ),
.clock ( vga_clk ),
.q ( rom_data )
);
// */



endmodule // data_drive


5.4 按键消抖模块

key_debounce.v

module key_debounce(
    input 	wire	clk,        // 时钟信号输入
    input 	wire 	rst_n,      // 复位信号,低电平有效
    input 	wire 	key,        // 按键输入

    output 	reg 	flag,       // 抖动标志,0表示抖动中,1表示抖动结束
    output 	reg	key_value  // 按键稳定后的值
);

// 定义参数MAX_NUM,用于设置消抖计数的最大值
parameter MAX_NUM = 20'd1_000_000;

// 内部信号声明
reg [19:0] delay_cnt;  // 用于消抖的计数器,足够大以覆盖抖动时间
reg key_reg;            // 上一次按键的状态

// 第一个always块:处理按键抖动
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        key_reg <= 1;        // 复位时,将上一次按键状态设为高电平
        delay_cnt <= 0;      // 复位时,清零计数器
    end
    else begin
        key_reg <= key;      // 非复位时,更新上一次按键状态
        // 当上一次按键状态与当前按键状态不一致时,认为是抖动开始,重置计数器
        if(key_reg != key) begin
            delay_cnt <= MAX_NUM;
        end
        else begin
            // 如果按键状态一致,开始计数,直到计数器减到0
            if(delay_cnt > 0)
                delay_cnt <= delay_cnt - 1;
            else
                delay_cnt <= 0;
        end
    end
end

// 第二个always块:在按键稳定后输出按键值和标志
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        flag <= 0;            // 复位时,抖动标志设为0
        key_value <= 1;       // 复位时,按键值设为高电平
    end
    else begin
        // 当计数器的值减到1时,认为按键已经稳定,更新抖动标志和按键值
        if(delay_cnt == 1) begin
            flag <= 1;
            key_value <= key;
        end
        else begin
            // 如果计数器还没减到1,保持抖动标志为0,按键值不变
            flag <= 0;
            key_value <= key_value;
        end
    end
end

endmodule

六、整体实现效果(加入按键)

加入按键之后的实现效果
在这里插入图片描述


总结

  • VGA显示,难点在于显示驱动模块,行场同步时序的代码编写,可以先写计数器和大模块,一些内部信号与标志信号可以逐步完善;其次就是数据显示中显示区域代码的编写。

  • VGA显示,显示彩条部分还可以改进,可以定义不同彩条的显示区域。显示文字主要就是显示位置的确定与汉字点阵的生成,汉字点阵还是费了一些功夫,最开始实在没找到一个比较合适的转换工具和方法,后来在朋友的帮助指点下终于可以了。显示图片一定要注意24位位图的话在ROM里面引入的是 .mif 文件,16位位图的话就引入hex文件。

  • 代码或哪一部分有问题欢迎留言指正。

参考

VGA显示接口简介

基于FPGA的VGA显示彩条、字符、图片

【FPGA实验】基于DE2-115平台的VGA显示

FPGA VGA显示协议

相关推荐

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-05-26 04:06:14       98 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-05-26 04:06:14       106 阅读
  3. 在Django里面运行非项目文件

    2024-05-26 04:06:14       87 阅读
  4. Python语言-面向对象

    2024-05-26 04:06:14       96 阅读

热门阅读

  1. c++11 auto类型推导的使用建议和注意事项

    2024-05-26 04:06:14       40 阅读
  2. HTML5

    2024-05-26 04:06:14       35 阅读
  3. Android-okhttp调接口传参简单举例

    2024-05-26 04:06:14       33 阅读
  4. qt里的QPieSeries饼图

    2024-05-26 04:06:14       32 阅读
  5. Python 点云裁剪

    2024-05-26 04:06:14       37 阅读
  6. 汇编实现流水灯

    2024-05-26 04:06:14       33 阅读
  7. 入门Kotlin的学习之路经验总结

    2024-05-26 04:06:14       35 阅读
  8. icloud如何高效利用

    2024-05-26 04:06:14       32 阅读
  9. NDIS小端口驱动(六)

    2024-05-26 04:06:14       28 阅读
  10. unity 常用工具和代码

    2024-05-26 04:06:14       32 阅读
  11. docker image prune -f 命令什么用途

    2024-05-26 04:06:14       32 阅读