这一篇在干嘛?
这是官方 DEMO 里最有「游戏味」的一个实验:FPGA 开发板外接一台 VGA 显示器和一把 PS2 键盘,左边画出 10×10 的五子棋棋盘,右边是提示框。两名玩家轮流用键盘上的 W、A、S、D 移动光标、按落子键下棋,任何一方先连成五颗棋子,游戏就停止并在提示框中宣布获胜方,按开发板上的复位键即可开新局。整个游戏逻辑全部用硬件描述语言实现,没有一颗单片机参与。
实验目标与原理
实验目的
这是一个基于 FPGA 的 VGA 游戏,利用 FPGA 开发板及外接 VGA 显示器、PS2 键盘,实现基本的游戏动作逻辑及胜方界面。
在 FPGA 上只有一个复位键,其他操作都在 PS2 键盘上进行并在 VGA 上显示。VGA 的布局是:左边由 10×10 格的棋盘构成,右边由一个提示框组成,同时为了界面好看,给整个边界着了颜色。当任意一方组成五颗棋子后操作停止,并在提示框中显示获胜方。
功能要求
- 通过 PS2 键盘的 W、A、S、D 控制光标的移动方位,选择所要下棋的位置;
- 确定后按落子键下棋子(功能要求一节写的是 U 键,实测环节用的是 ENTER 键,上板时以实际下载验证为准);
- 当双方任意一方满足五颗棋子成串后,游戏结束并给出提示;
- 结束后按下 FPGA 上的复位键重新开始新的一局。
VGA 显示原理速览
VGA 是模拟信号的「逐行扫描」显示方式:显示器上的电子束(对应现在的像素时钟)从屏幕左上角开始,一行一行地向右扫描、向下翻页。为了告诉显示器「这一行到哪里该回头」「这一帧到哪里该重来」,VGA 接口里专门有行同步 HS 和场同步 VS 两个信号,它们各自按严格的时序拉低/拉高。
以本文使用的 640×480 分辨率为例,一行并不是只扫 640 个像素:把行同步脉冲、同步后的消隐期都算上,一行实际对应 800 个像素时钟(hcnt 从 0 计到 800),其中前 96 个计数期间 HS 保持低电平;一帧也不是只扫 480 行,而是 525 行(vcnt 从 0 计到 525),其中前 2 行期间 VS 保持低电平。像素时钟越快,刷新越快——本实验用 25MHz 的像素时钟,正好对应常见的 60Hz 刷新率。
为什么需要锁相环?
开发板晶振给 FPGA 的时钟是 50MHz,而 VGA 640×480@60Hz 需要 25MHz 的像素时钟驱动。所以顶层用一个锁相环(PLL)把 50MHz 二分频出 25MHz,专门供 VGA 模块使用——这就是原文总述里「锁相环倍频供 VGA 显示」的由来(50→25 实为分频,工程里习惯把 PLL 的时钟变换统称为倍频/分频处理)。
系统总体设计
系统设计框图

系统方案比较
使用 FPGA 实现设计只需使用 Vivado 软件设计好电路,然后将整个电路下再到 FPGA 中即可;而使用单片机实现设计方案比较复杂。经过综合比较,本实验选择使用 FPGA 实现系统的设计。
各模块电路设计
顶层文件

总述:
- 锁相环:FPGA 时钟是 50MHz 的频率,而 VGA 640×480 需要 25MHz 的频率进行驱动,所以选择了一个锁相环进行倍频供 VGA 显示。
- KEY 模块:PS2 键盘与 FPGA 通信,发送相关指令给 FPGA,FPGA 接收到后给与一定的处理送入 VGA 对其进行操作。为了键盘和 VGA 同步,故选用同一个时钟。
- VGA 模块:此程序的核心便是 VGA 模块。它集成了显示的设置、键盘数据的处理、相应命令的执行;同时在游戏结束后,返回给键盘一条命令让其停止发送数据,使界面停留在最后操作上。
键盘模块

本模块采用通用的 PS2 键盘接口协议,对 FPGA 发送命令控制其操作,PS2 接口如图 3.2-1。

图 3.2-1

PS2 时序图图 3.2-2 (b)键盘接受时序
(a)键盘发送时序
从 PS/2 向 PC 机发送一个字节:
- 检测时钟线电平,如果时钟线为低,则延时 50μs;
- 检测判断时钟信号是否为高,为高,则向下执行,为低,则转到 (1);
- 检测数据线是否为高,如果为高则继续执行,如果为低,则放弃发送(此时 PC 机在向 PS/2 设备发送数据,所以 PS/2 设备要转移到接收程序处接收数据);
- 延时 20μs(如果此时正在发送起始位,则应延时 40μs);
- 输出起始位(0)到数据线上。这里要注意的是:在送出每一位后都要检测时钟线,以确保 PC 机没有抑制 PS/2 设备,如果有则中止发送;
- 输出 8 个数据位到数据线上;
- 输出校验位;
- 输出停止位(1);
- 延时 30μs(如果在发送停止位时释放时钟信号则应延时 50μs)。
通过以下步骤可发送单个位:
- 准备数据位(将需要发送的数据位放到数据线上);
- 延时 20μs;
- 把时钟线拉低;
- 延时 40μs;
- 释放时钟线;
- 延时 20μs。
说明:此程序只需要 PS2 向 FPGA 发送字节,故只给出发送字节的操作方法。
初学者可以这样理解 PS2 协议:键盘和 FPGA 之间有两根线——时钟线 ps2clk 和数据线 ps2date。键盘是「主动方」,每按一个键,键盘自己产生时钟脉冲,并在时钟的节拍下把 11 位帧(1 位起始位 0 + 8 位数据位 + 1 位校验位 + 1 位停止位 1)一位一位地放到数据线上;FPGA 只要在键盘时钟的下降沿采样数据线,就能把键值完整收下来。这也是为什么 ps2clk、ps2date 两个引脚约束里都配了上拉(PULLTYPE = PULLUP)——PS2 总线是开漏式的,空闲时靠上拉保持高电平。
VGA 显示模块

如图 3.3-2。CLK 接收锁相环倍频出来的 25MHz 频率进行驱动,key-signeal 接收键盘发送的指令,rst 复位操作,ting-flag 游戏结束命令。
VGA 显示时序程序:
always@(posedge CLK)
begin
if(hcnt==800) hcnt<=0;
else hcnt<=hcnt+1;
end
always@(posedge CLK)
begin
if(hcnt<96) HS<=0;
else HS<=1;
end
always@(posedge CLK)
begin
if(hcnt==800)
begin
vcnt<=vcnt+1;
if(vcnt==525) vcnt<=0;
end
end
always@(posedge CLK)
begin
if(vcnt<2) VS<=0;
else VS<=1;
end
/////逐行拆解这段时序代码:
- 第一个 always 块:像素计数器 hcnt 每 25MHz 时钟加 1,计到 800 归零,对应「扫完一行」;
- 第二个 always 块:hcnt 小于 96 时 HS 输出低电平,其余时间输出高电平——这就是宽度为 96 个像素时钟的行同步脉冲;
- 第三个 always 块:每当 hcnt 计满一行,行计数器 vcnt 加 1,计到 525 归零,对应「扫完一帧」;
- 第四个 always 块:vcnt 小于 2 时 VS 输出低电平,即宽度为 2 行的场同步脉冲。
有了 HS/VS 这套「节拍器」,剩下的工作就是把(hcnt, vcnt)当前落在哪个坐标翻译成颜色:落在棋盘区域内就根据棋子存储矩阵输出黑/白色,落在提示框区域就输出提示信息对应的颜色,其余区域输出边界色。游戏胜负判断则是每次落子后检查该点横、竖、两条斜线四个方向上是否有连续五颗同色棋子。
仿真与实测落子键不一致
「实验仿真」一节只要求根据描述语言进行仿真测试、分析波形;而功能要求里写的是按 U 键落子,下载测试环节实际是按 ENTER 键落子。上板前请以工程源码中的键值译码为准,确认落子键后再生效操作,避免「下了棋没反应」的困惑。
引脚分配
管脚约束如下(原文分两段给出,均一字不差保留):
set_pin_assignment { B } { LOCATION = E13; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { CLK } { LOCATION = R7; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { G } { LOCATION = N6; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { HS } { LOCATION = E7; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }set_pin_assignment { R } { LOCATION = D5; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { VS } { LOCATION = F7; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { ps2clk } { LOCATION = M16; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { ps2date } { LOCATION = K16; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }
set_pin_assignment { rst } { LOCATION = A2; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; }各信号的含义:R、G、B 是 VGA 的三基色模拟通道驱动脚(E13/N6/D5),HS(E7)、VS(F7)是行、场同步,ps2clk(M16)、ps2date(K16)接 PS2 键盘接口,rst(A2)是板上的复位键。
上板流程
- 将 VGA 线一端插在开发板 VGA 接口上,另一端连接显示器;
- PS2 键盘插在 KX_SD_PS2_VGA 模块 PS2 接口上;
- 编译工程并下载程序到 FPGA。
下载程序之后,可以看到 VGA 显示器上显示棋盘格,通过 W、A、D、S 切换方向,ENTER 键落子。
现象与总结



上板后可以看到:VGA 左侧是 10×10 的棋盘格,右侧是提示框;光标随 WASD 移动,落子后棋子出现在对应格子里;一方连成五子后操作停止,提示框显示获胜方;按复位键 A2 可重新开局。
这个实验把 FPGA 设计里最常见的三件事串在了一起:时钟管理(PLL 出 25MHz 像素时钟)、外设协议(PS2 键盘时序接收)、显示时序(VGA 行场扫描)。理解了「用计数器生成 HS/VS、再用坐标查表出颜色」这套思路,就可以改造它画出任意点阵图案甚至更强的游戏界面。
自测
自测
- 本实验为什么需要一个锁相环?输入输出时钟频率各是多少?
- VGA 行同步脉冲 HS 在 hcnt 处于什么范围时为低电平?宽度是多少个像素时钟?
- PS2 键盘向 FPGA 发送一个字节时,帧结构由哪几部分组成?FPGA 应该在时钟的什么沿采样数据线?
- 为什么 ps2clk、ps2date、rst 这几个引脚的约束里都配置了 PULLTYPE = PULLUP?
- 若要判断五子棋胜负,落子后需要在哪几个方向上检查连续同色棋子?