为 Wujian100 添加 VGA 显示模块

前言

本文是为数电课编写的实验指导,介绍了如何为 Wujian100 添加一个简单的 VGA 显示模块。作为实验指导,本文只讲解大概思路,没有展示完整代码。

实验目的

  1. 熟悉 VGA 的显示原理。
  2. 进一步学习软硬件开发流程。

实验器材

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_ramvga_drivermy_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_0vga_driver 相连,其它都是 AHB-Lite 信号线。

  1. Block Memory Generator:我们利用此 IP 创建一个真双口 RAM,支持双口的读写。PORT A 和 PORT B 都是 native interface,这种接口描述自然、便于理解,方便我们写 vga_driver 的读数据逻辑。(实际上 vga_driver 不需要写帧缓存,因此一个简单双口 RAM 就可以。选择真双口 RAM是由于此应用场景下 IP 设置的限制。)
  2. AXI BRAM Controller:Block Memory Generator 不支持两个端口采用不同的接口协议,即 PORT_B 是 native 而 PORT_A 是 AXI 协议。因此我们需要 AXI BRAM Controller 来进行转接控制。
  3. AHB-Lite to AXI Bridge:将 SoC 的系统总线 AHB-Lite 转接为 AXI 协议,这样 CPU 就能够通过总线最终藉由 PORT_A 读写帧缓存了。

VGA 驱动设计

vga_drivervga_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 主要有两个任务:

  1. 生成 2bits 行场同步信号 vga_hsvga_vs。由 VGA 显示原理,我们可以通过设计行、列的计数器实现。
  2. 生成当前像素的 12bits RGB 信号:
  3. 由当前像素位置映射到 320×240 位置,从而得到像素信息对应帧缓存的读地址,请求读写。由于 BRAM 会在读地址变化的下一时钟周期给出数据,因此需要提前改变读地址。
  4. 从读取的 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_ramvga_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 项目,编译并通过调试模式运行,我们就可以在显示器上看到北京大学校徽了:

同学们可以尝试更多图片,也可以改进代码以支持不同大小的图片,或是自定义调色盘提升显示效果。

更多图片:

文章作者:哈猪猪
文章链接:https://hazhuzhu.com/fpga/add-vga-module-for-wujian100.html
许可协议: CC BY-NC-SA 4.0
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇