【FPGA】VGA显示文字、彩条、图片——基于DE2-115
通过Verilog编程,在至少2种显示模式下(640*480@60Hz,1024*768@75Hz)分别实现以下VGA显示,并对照VGA协议信号做时序分析:1)屏幕上显示彩色条纹;2)显示自定义的汉字字符(姓名-学号);3)输出一幅彩色图像。
文章目录
前言
本文内容:
-
深入了解VGA协议,理解不同显示模式下的VGA控制时序参数(行频、场频、水平/垂直同步时钟周期、显示后沿/前沿等概念和计算方式);
-
通过Verilog编程,在至少2种显示模式下(640480@60Hz,1024768@75Hz)分别实现以下VGA显示,并对照VGA协议信号做时序分析:1)屏幕上显示彩色条纹;2)显示自定义的汉字字符(姓名-学号);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 连接线。
1.2 管脚定义
下面,我们结合 VGA 接口引脚图和各引脚定义表格,对 VGA 接口各引脚做一下简单介绍。
由图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 。
图中 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 。
理解了行同步时序,场同步时序就更容易理解了,两者相类似,如图所示,图中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。
图中的红色区域表示在一个完整的行扫描周期中,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文件。
-
代码或哪一部分有问题欢迎留言指正。
参考
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)