前言
本文是为数电课编写的实验指导,介绍了如何为 Wujian100 添加一个简单的 VGA 显示模块。作为实验指导,本文只讲解大概思路,没有展示完整代码。
- 基础实验:为 Wujian100 添加外设
- wujian100 相关文章:哈猪猪的博客
实验目的
- 熟悉 VGA 的显示原理。
- 进一步学习软硬件开发流程。
实验器材
Basys3, Vivado 2022.2,VGA 连接线,VGA 显示器。
实验原理
VGA 显示原理
VGA(Video Graphics Array)是一种模拟视频信号接口,它通过传输红、绿、蓝三个模拟信号以及行同步信号(HSYNC)和场同步信号(VSYNC)来实现图像显示。其中 HSYNC 和 VSYNC 信号是用来同步画面扫描的。Basys3 支持 VGA 输出,其中红绿蓝每种颜色 4bits。
标准的 VGA 分辨模式是 640×480@60Hz,代表屏幕每行有 640个像素以及每帧有 480 行,每秒钟显示 60 帧图像。
在实际使用中,每行像素点的扫描是按照时序进行的,从左往右,从上往下,扫描顺序如下图所示:
当 HSYNC 信号的高电平来临时,系统会从左到右逐个扫描每个像素点,并在每行结束时产生一个负脉冲,即行消隐时间。当 VSYNC 信号的高电平来临时,系统会从屏幕的上部开始往下扫描每行像素点,并在整个画面结束时产生一个负脉冲,即场消隐时间。这样,通过对 HSYNC 和 VSYNC 信号的控制,就可以完成整个画面的扫描和显示。HSYNC,VSYNC 时序示意图如下所示:
由图我们可以得知:在 VGA 标准输出中,显示一行信号需要 96+48+640+16=800 个像素点时间,显示一帧图像需要 2+33+480+10=525 行时间,即 525×800= 420k 个像素点时间。每秒显示 60 帧需要约 25M 像素时间。
VGA 显示模块设计
Basys3 支持 4bits 位深的 RGB 输出。它使用了一个电阻网络来实现数字信号到模拟信号的转换,信号引脚以及电路示意图如下所示。
由图可知,12bits 的颜色信号加上 HS-水平同步信号和 VS-垂直同步信号——我们需要一个 VGA 驱动器按照正确的时序来生成这 14bits FPGA 信号。
我们还需要一个帧缓存,用于存储一帧图像的像素信息,同时我们还能够通过系统总线用 CPU 来读写帧缓存的信息,这样就能用软件来控制图像显示。
因此在本实验中,我们将要设计并实现的 VGA 显示模块 My_ram_vga,它的结构如下所示:
它包含两个模块:my_dual_ram
和 vga_driver
。my_dual_ram
是用真双口 RAM 构造的 64KB 帧缓存,一端挂载到 AHB 总线上可被 CPU 读写,一端的 BRAM_PORTB 由驱动 vga_driver
读取像素信息,以生成 14 位 VGA 显示信号:vga_data
, vga_vs
, vga_hs
。
模块整体挂载到 AHB-Lite 系统总线上,因此 CPU 可以读写帧缓存中的像素信息。此外,由于 640×480@60Hz 的 VGA 时钟频率为 25MHz,不同于总线频率,因此我们需要为 vga_driver
提供 25Mhz 的 vga_clk
。
帧缓存设计
我们用 Basys3 上的 BRAM 资源实现一个能存储一帧图像的帧缓存。
Wujian100 SoC 上原本有1个 IRAM 和 3个 DRAM ,每块大小均为 64KB,且需要 Basys3 的 16 个 BRAM 资源构建。然而,由于 Basys3 板载资源只有 50个 BRAM,因此在实验一之前,我们为了适应 Basys3 有限资源对 SoC 作了裁剪,去掉了第三个 DRAM。
现在,我们还需要去掉一个 DRAM 以建立一块 64KB 的帧缓存,让 VGA 驱动器读取像素信息并输出 RGB 以及同步信号,实现显示。
64KB 的帧缓存能放下多大的图像呢?VGA 的标准输出是 640×480@60Hz,简单计算可知我们的帧缓存是放不下 12bits 的像素的。因此,我们可以将一帧图像的大小设为 320×240,再由 VGA 驱动控制映射到 640×480 显示器上。同时,使每个像素只支持 4bits 色彩,从而压缩帧大小,还方便对齐。
上图是帧缓存 my_dual_ram
的结构图。接口中 BRAM_PORTB_0
与 vga_driver
相连,其它都是 AHB-Lite 信号线。
- Block Memory Generator:我们利用此 IP 创建一个真双口 RAM,支持双口的读写。PORT A 和 PORT B 都是 native interface,这种接口描述自然、便于理解,方便我们写
vga_driver
的读数据逻辑。(实际上vga_driver
不需要写帧缓存,因此一个简单双口 RAM 就可以。选择真双口 RAM是由于此应用场景下 IP 设置的限制。) - AXI BRAM Controller:Block Memory Generator 不支持两个端口采用不同的接口协议,即 PORT_B 是 native 而 PORT_A 是 AXI 协议。因此我们需要 AXI BRAM Controller 来进行转接控制。
- AHB-Lite to AXI Bridge:将 SoC 的系统总线 AHB-Lite 转接为 AXI 协议,这样 CPU 就能够通过总线最终藉由 PORT_A 读写帧缓存了。
VGA 驱动设计
vga_driver
受 vga_clk
驱动,通过 BRAM_PORTB
读取像素信息,生成 14 位 VGA 显示信号:vga_data
, vga_vs
, vga_hs。
简单起见,我们设帧缓存中的每个像素包括红色 1bit,绿色 2bits,蓝色 1bit;帧缓存位宽与总线一致,均为 32bits。因此我们可以定义像素数据的存储格式为:
wujian100 uses little end
----------------------------------------------------------------------------
| 31 - 28 | 27 - 24 | 23 - 20 | 19 - 16 | 15 - 12 | 11 - 8 | 7 - 4 | 3 - 0 |
| PIXEL_7 | PIXEL_6 | PIXEL_5 | PIXEL_4 | PIXEL_3 | PIXEL_2 | PIXEL_1 | PIXEL_0 |
----------------------------------------------------------------------------
| 3 2 1 0 |
| red green_1 green_0 blue |
bits with XXX_0 contain 4-bit color value for pixel (N) and XXX_1 for (N + 1)
vga_driver
主要有两个任务:
- 生成 2bits 行场同步信号
vga_hs
,vga_vs
。由 VGA 显示原理,我们可以通过设计行、列的计数器实现。 - 生成当前像素的 12bits RGB 信号:
- 由当前像素位置映射到 320×240 位置,从而得到像素信息对应帧缓存的读地址,请求读写。由于 BRAM 会在读地址变化的下一时钟周期给出数据,因此需要提前改变读地址。
- 从读取的 32bit 数据中选取对应像素数据,再将一个像素的 4bits 颜色数据映射为 12bits 颜色信号。
实验步骤
硬件部分
添加时钟
实验一之前,我们已经帮大家分频得到了 20MHz 的总线时钟,利用的是 IP 核 clk_wiz。
现在我们需要 Re-customize IP,使其输出第二个时钟 25MHz,作为 vga_clk
。如何实例化并引出时钟信号,参见 wujian100_open_fpga_top.v
中我们已经实例化的 u_clk_wiz_0
。
删除 DRAM
我们选择去掉第二块 DRAM。为了更快捷地实现 DRAM 增减,我们可以模仿 dummy 模块的编写方式,构造一个空的 DRAM_empty 模块。它和原本 DRAM 模块的接口一致,不过没有内部逻辑,且输出都拉低为0。再用 DRAM_empty 替代原 DRAM 模块。这样不需要在整个系统中理清信号和逻辑,就能快捷地实现模块的增减。
DMA 模块也是通过这种方式剪裁的,同学们可以模仿实现。
构造帧缓存
和实验一流程类似,我们首先创建 Block Design,命名为 my_dual_ram
。
添加 IP:Block Memory Generator。设置如下:
- Basic:
- Mode:BRAM Controller,因为我们希望
vga_driver
通过简单的信号线而非总线来读写帧缓存。 - Memory Type:True Dual Port RAM。
- PortA:将会由总线控制。
- Write Width: 32
- Read Width: 32
- Write Depth: 16384
- PortB:将会由 VGA 驱动控制。保持默认。
- Other Options:取消勾选 Enable Safety Circuit。
添加 AHB-Lite to AXI Bridge 以及 AXI BRAM Controller,分别对两者进行合适的设置并连线。引出转接桥 AHB 信号线和 BRAM_PORTB 到顶层。
设置 BRAM 的地址为:0x4002_0000~0x4002_FFFF,替代 dummy1。
最终的架构图应与实验原理中的一致。
最后同实验一,打包生成 wrapper。
编写 VGA 驱动
根据实验原理编写 vga_driver
。
事实上 1bit 红色,2 bits 绿色,1bit 蓝色所得到的 16 色调色盘并不是现实中最常见的 16 种颜色,显示效果也不是很好。同学们可以自己设计或参考游戏机种的调色盘,也可以自己设计数据存储格式,显示分辨率等。
完成 VGA 显示模块
新建顶层模块 my_ram_vga
,以整合 my_dual_ram
, vga_driver
以及 vga_clk
。整合方式同实验一。最后替代 main_dummy_top1
。修改约束文件。
注意这次信号线更多,并且有输入也有输出,要小心连线。
完成后下载到 Basys3。
软件部分
软件部分我们提供了一个 vga 显示自定义图片的示例,供大家参考。图片大小设定为 200×200,后文的代码都仅支持该大小的图片。
进入项目目录:sdk\projects\Labs\Lab2\
转换图片为 C 风格数组
我们首先需要准备一张 200×200 的图片,可以用系统自带画图软件裁剪、重新调整大小为 200×200。
接着,我们需要将图片色彩量化为 16 色,再转换为 C 风格数组:每个数组元素都是 32bits 无符号数,并按前文所述的格式以小端法存储像素信息。这样,我们就可以通过向真双口 RAM 所在地址写这些数据,将图片数据存入帧缓存,最后由 VGA 驱动读取显示图片。
我们为大家编写了一个将图片转换为 C 风格数组的 Python 脚本:pictures\pic_to_c_array.py
。运行并拷贝结果。
CDK
进入 CDK 项目目录:vga\
将 python 脚本结果粘贴到 picture_data.h
。
在 vga.c
中我们为大家编写了两个函数:flush_canvas()
和 display_picture()
。分别用于刷写屏幕为指定颜色,以及展示 200×200 图片。
由于我们在实验初又删除了一个 DRAM,因此还需要修改链接脚本 sdk\board\wujian100_open_evb\gcc_csky.ld
中 SRAM 的地址空间为 0x20000000-0x20010000
。
连接 Basys3 到 VGA 显示器。打开 CDK 项目,编译并通过调试模式运行,我们就可以在显示器上看到北京大学校徽了:
同学们可以尝试更多图片,也可以改进代码以支持不同大小的图片,或是自定义调色盘提升显示效果。
更多图片: