FPGA实战项目1------坦克大战
根据模块化 思想,可将此任务简单的进行模块拆分:
系统原理,模块划分,硬件架构,算法支持,Verilog实现框架
一,系统总体原理
1. 核心设计思想
硬件并行处理: 利用 FPGA 的并行特性,同时处理多个坦克移动、子弹飞行、碰撞检测等逻辑。
时序clk驱动: 所有模块在统一时钟域下同步运行,通过状态机和计数器实现精准时序控制。
**像素级渲染显示:**基于 VGA 接口,将游戏元素(坦克、子弹、得分)渲染为像素信号,驱动显示器显示。
2. 系统架构
(系统时钟模块,输入模块,游戏状态模块,我方坦克,敌方坦克群,子弹管理,得分模块,VGA渲染模块(坐标转为像素),VGA接口,显示器连接)
二,核心模块详细设计
具体的讲解均体现在随文Verilog代码中!
1. 输入模块(Input Controller)
功能
处理用户输入(按键 ),输出我方坦克控制信号(移动方向、射击指令),包含按键消抖。
需要注意的是:按键消抖模块,一般的设计部分都必须要有属于固定部分。
该模块的主要功能是对机械按键输入的信号进行消抖处理,去除按键在按下和释放瞬间产生的抖动信号,输出稳定的按键状态。同时,检测按键的按下和释放动作,分别产生单周期的脉冲信号,方便后续模块根据这些脉冲信号执行相应的操作。
算法
采用计数器计时的方法。当检测到按键输入信号与当前存储的按键状态不一致时,计数器开始计数。如果在计数过程中按键状态再次发生变化,计数器清零重新开始计数。当计数器达到预设值(对应约 20ms 的消抖时间)时,认为按键状态稳定,更新存储的按键状态。
状态
计数器状态:包括计数中、计数完成(达到 20ms)和清零状态。
按键状态:存储当前按键的稳定状态(按下或释放)。
逻辑
在每个时钟上升沿检查复位信号和按键状态。复位时,将所有状态初始化为默认值。正常工作时,根据按键状态的变化控制计数器的计数,并在合适的时机更新按键状态和产生脉冲信号。
原理
机械按键在按下和释放时会产生短暂的抖动,这些抖动信号可能会被误判为多次按键操作。通过设置一个固定的消抖时间(20ms),可以避开抖动期,确保只有当按键状态稳定一段时间后才被认为是有效的按键操作。
代码如下:
//此模块为按键消抖模块。确保用户按键信号稳定
module key_debounce(
input wire clk,
input wire rst_n,
input wire key_in, // 原始按键输入信号,由于机械特性,该信号可能存在抖动
output wire key_out, // 消抖后的稳定按键输出信号
output reg key_pulse_down, // 按键按下时产生的单周期脉冲信号,可用于触发特定操作
output reg key_pulse_up // 按键释放时产生的单周期脉冲信号,可用于停止特定操作
);
// 20位计数器,用于计时消抖时间。在50MHz时钟频率下,计数到1000000对应约20ms
reg [19:0] cnt;
// 存储当前经过消抖处理后的按键状态
reg key_reg;
// 存储上一个时钟周期的按键状态,用于检测按键状态的变化
reg key_prev;
// 时序逻辑,在时钟上升沿或复位信号下降沿触发;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// 复位时,计数器清零,按键状态寄存器和上一状态寄存器置为高电平
cnt <= 0;
key_reg <= 1;
key_prev <= 1;
// 按键按下和释放脉冲信号清零
key_pulse_down <= 0;
key_pulse_up <= 0;
end
else begin
// 保存上一个时钟周期的按键状态,随时更新,在CLK到来时,先更新PREV,确保状态存储
key_prev <= key_reg;
if (key_in != key_reg) begin // 检测到按键状态发生变化,原信号是肯定能采集到的;
// 计数器开始计数
cnt <= cnt + 1;
if (cnt == 20'd1000000) begin // 经过约20ms的消抖时间
// 更新按键状态寄存器
key_reg <= key_in;
// 计数器清零
cnt <= 0;
end
end
else begin
cnt <= 0;
end
// 检测按键按下的下降沿,出现上升沿,未进行亚稳态消除操作(可进行打拍)
key_pulse_down <= key_prev & (!key_reg);
// 检测按键释放的上升沿,出现上升沿
key_pulse_up <= (!key_prev) & key_reg;
end
end
// 消抖后的按键输出信号等于按键状态寄存器的值
assign key_out = key_reg;
endmodule
相应的仿真测试文件代码如下所示:
`timescale 1ns / 1ps
module tb_key_debounce;
// 定义信号
reg clk;
reg rst_n;
reg key_in;
wire key_out;
wire key_pulse_down;
wire key_pulse_up;
// 实例化按键消抖模块
key_debounce uut (
.clk(clk),
.rst_n(rst_n),
.key_in(key_in),
.key_out(key_out),
.key_pulse_down(key_pulse_down),
.key_pulse_up(key_pulse_up)
);
// 生成时钟信号
initial begin
clk = 0;
forever #10 clk = ~clk; // 50MHz时钟,周期20ns
end
// 测试序列
initial begin
// 初始化信号
rst_n = 0;
key_in = 1;
#20; // 保持复位20ns
rst_n = 1;
// 模拟按键抖动
#100;
key_in = 0;
#10; key_in = 1;
#10; key_in = 0;
#10; key_in = 1;
#10; key_in = 0;
#20000000; // 等待消抖时间
// 模拟按键释放抖动
key_in = 1;
#10; key_in = 0;
#10; key_in = 1;
#10; key_in = 0;
#10; key_in = 1;
#20000000; // 等待消抖时间
#1000
$finish;
end
endmodule
仿真效果如图:
2. 我方坦克模块(player_tank.v)
功能
该模块负责控制我方坦克的移动和射击 。根据输入的移动方向控制信号,在一定的速度控制下,更新坦克的坐标位置。同时,根据射击请求信号,触发子弹发射。
算法
使用计数器控制坦克的移动速度,每 16 个时钟周期坦克移动一次。根据移动方向控制信号的不同编码,更新坦克的 x 或 y 坐标。在移动过程中,进行边界检查,确保坦克不会移出屏幕范围。
状态
坦克位置状态:通过 x 和 y 坐标表示坦克在屏幕上的位置。
速度计数器状态:控制坦克移动的时间间隔。
射击状态:根据射击请求信号决定是否发射子弹。
逻辑
在每个时钟上升沿检查复位信号和输入信号。复位时,将坦克位置和射击状态初始化为默认值。正常工作时,计数器不断计数,当计数达到 15 时,根据移动方向控制信号更新坦克位置,然后计数器清零。对于射击请求信号,直接将其传递给发射使能信号。
原理
通过计数器实现坦克移动速度的控制,避免坦克移动过快。边界检查可以保证游戏的合理性和稳定性,防止坦克移出屏幕范围。射击请求信号的传递实现了坦克的射击功能。
代码如下:
//我方坦克模型
module player_tank(
input wire clk,
input wire rst_n, // 将坦克的状态恢复到初始位置
input wire [2:0] move_dir, // 移动方向控制信号
input wire fire_req, // 射击请求信号,单周期脉冲,触发坦克射击
output reg [9:0] x, // 坦克的x坐标,10位宽度可表示较大范围的屏幕位置
output reg [9:0] y, // 坦克的y坐标,10位宽度可表示较大范围的屏幕位置
output reg fire_en // 子弹发射使能信号,单周期脉冲,用于触发子弹发射模块
);
// 4位计数器,用于控制坦克的移动速度,每16个时钟周期移动一次
reg [3:0] speed_cnt;
// 时序逻辑,在时钟上升沿或复位信号下降沿触发
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// 复位时,坦克初始位置设置在屏幕底部中央
x <= 10'd320;
y <= 10'd450;
// 发射使能信号清零
fire_en <= 0;
end
else begin
// 每来一个clk,速度计数器加1
speed_cnt <= speed_cnt + 1;
if (speed_cnt == 4'd15) begin // 当计数满16个时钟周期,进行移动方向的判别
case (move_dir)
3'b001: y <= (y > 8) ? y - 1 : y; // 向上移动,同时进行边界检查
3'b010: y <= (y < 472) ? y + 1 : y; // 向下移动,同时进行边界检查
3'b100: x <= (x > 8) ? x - 1 : x; // 向左移动,同时进行边界检查
3'b101: x <= (x < 632) ? x + 1 : x; // 向右移动,同时进行边界检查
endcase
// 速度计数器清零
speed_cnt <= 0;
end
// 射击处理,将射击请求信号直接传递给发射使能信号
fire_en <= fire_req;
end
end
endmodule
注意到在代码中,有左右移动的指令: 3'b001: y <= (y > 8) ? y - 1 : y;
这是一个条件赋值语句,运用了三元运算符 ? :。其语法形式为 condition ? expression1 : expression2,也就是当 condition 为真时,表达式的值是 expression1;反之则是 expression2。
综合起来,3'b001: y <= (y > 8) ? y - 1 : y; 表示当 move_dir 为 3'b001(向上移动)时,若坦克的 y 坐标大于 8,就将 y 坐标减 1,让坦克向上移动;若 y 坐标小于等于 8,就维持 y 坐标不变,以此避免坦克移出屏幕顶部。
边界值 8 是人为设定的一个常量,其目的是防止坦克移出屏幕范围。因为坦克自身有一定的大小,为了保证坦克不会完全移出屏幕,所以设定了一个安全边界。8 这个值是根据坦克的大小和屏幕分辨率来确定的,以确保坦克在移动到屏幕边缘时能够正确停止。
人眼具有视觉暂留特性,对于一定频率内的连续变化,能够感知到的是平滑的运动。只要坦克的移动速度不是极快,在每 16 个时钟周期移动一个单位的情况下,人眼通常会将其视为连续的移动,而不会察觉到明显的停顿或瞬移。
3. 敌方坦克模块(enemy_tank.v)
功能
该模块实现敌方坦克的随机移动和自动射击 功能。敌方坦克会在一定时间间隔内随机改变移动方向,并以一定的概率进行射击。
算法
使用计数器控制敌方坦克改变移动方向的时间间隔,每 50 个时钟周期随机选择一个新的移动方向。同时,使用随机数生成器,每 1000 个时钟周期有 1% 的概率触发射击。在移动过程中,进行边界检查,确保敌方坦克不会移出屏幕范围。
状态
敌方坦克位置状态:通过 x 和 y 坐标表示敌方坦克在屏幕上的位置。
方向计数器状态:控制敌方坦克改变移动方向的时间间隔。
移动方向状态:记录敌方坦克当前的移动方向。
射击状态:根据随机数决定是否发射子弹。
逻辑
在每个时钟上升沿检查复位信号和进行相应操作。复位时,将敌方坦克位置、方向计数器、移动方向和射击状态初始化为默认值。正常工作时,方向计数器不断计数,当计数达到 50 时,随机改变移动方向并清零计数器。同时,根据随机数判断是否进行射击。
原理
通过计数器和随机数生成器实现敌方坦克的随机移动和射击。边界检查保证了敌方坦克在屏幕范围内活动,增加了游戏的合理性。
代码如下:
//地方坦克模型
module enemy_tank(
input wire clk,
input wire rst_n,
output reg [9:0] x,
output reg [9:0] y,
output reg enemy_fire // 敌方坦克射击信号,单周期脉冲,触发敌方子弹发射
);
// 用于控制敌方坦克改变移动方向的计数器
reg [6:0] cnt_dir;
// 敌方坦克的移动方向,0-上,1-下,2-左,3-右
reg [2:0] dir;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// 复位时,敌方坦克初始位置设置在屏幕顶部中央
x <= 10'd320;
y <= 10'd30;
// 方向计数器清零
cnt_dir <= 0;
// 初始方向设置为向下
dir <= 1;
// 射击信号清零
enemy_fire <= 0;
end
else begin
cnt_dir <= cnt_dir + 1;
if (cnt_dir == 7'd50) begin // 每50个时钟周期
// 随机改变移动方向
dir <= $urandom_range(3, 0);
// 方向计数器清零
cnt_dir <= 0;
end
// 按方向移动
case (dir)
0: y <= (y > 8) ? y - 1 : y; // 向上移动,同时进行边界检查
1: y <= (y < 472) ? y + 1 : y; // 向下移动,同时进行边界检查
2: x <= (x > 8) ? x - 1 : x; // 向左移动,同时进行边界检查
3: x <= (x < 632) ? x + 1 : x; // 向右移动,同时进行边界检查
endcase
// 随机射击逻辑,每1000个时钟周期有1%的概率射击
if ($urandom_range(999, 0) == 0) begin
enemy_fire <= 1;
end
else begin
enemy_fire <= 0;
end
end
end
endmodule
4. 子弹管理模块(bullet_manager.v)
功能
该模块负责管理我方和敌方发射的子弹,包括子弹的发射、飞行和碰撞检测。 根据坦克的发射信号,设置子弹的初始位置和有效标志,控制子弹的飞行方向,并检测子弹是否超出屏幕范围。同时,提供碰撞检测函数,用于判断子弹是否与坦克发生碰撞。
算法
当接收到我方或敌方坦克的发射信号时,设置子弹的初始位置和有效标志。在子弹有效时,根据子弹的类型(我方或敌方)控制其飞行方向(向上或向下)。当子弹超出屏幕范围时,将其有效标志清零。碰撞检测通过判断子弹的坐标是否在坦克的矩形范围内来实现。
状态
子弹位置状态:通过 x 和 y 坐标表示子弹在屏幕上的位置。
子弹有效状态:标志子弹是否处于飞行状态。
子弹类型状态:区分子弹是我方还是敌方发射的。
逻辑
在每个时钟上升沿检查复位信号和发射信号。复位时,将子弹的所有状态初始化为默认值。当接收到发射信号时,初始化子弹状态。在子弹飞行过程中,根据其类型更新位置,并检查是否超出屏幕范围。碰撞检测函数在需要时被调用,判断子弹与坦克是否碰撞。
原理
通过对发射信号的响应和对子弹位置的更新,实现子弹的发射和飞行。碰撞检测的原理是基于坐标范围的判断,确保只有当子弹进入坦克的一定范围内才判定为碰撞。
代码如下:
//子弹模型管理
module bullet_manager(
input wire clk,
input wire rst_n,
input wire [9:0] player_x, // 我方坦克的x坐标,用于确定我方子弹的发射初始位置
input wire [9:0] player_y, // 我方坦克的y坐标,用于确定我方子弹的发射初始位置
input wire [9:0] enemy_x, // 敌方坦克的x坐标,用于确定敌方子弹的发射初始位置
input wire [9:0] enemy_y, // 敌方坦克的y坐标,用于确定敌方子弹的发射初始位置
input wire fire_en, // 我方坦克发射使能信号,触发我方子弹发射
input wire enemy_fire, // 敌方坦克发射信号,触发敌方子弹发射
output reg [9:0] bullet_x, // 子弹的x坐标,输出给其他模块用于显示和碰撞检测
output reg [9:0] bullet_y, // 子弹的y坐标,输出给其他模块用于显示和碰撞检测
output reg bullet_valid, // 子弹有效标志,1表示子弹处于飞行状态
output reg bullet_is_enemy // 子弹是否为敌方子弹标志,1表示敌方子弹
);
// 替代结构体的寄存器,子弹封装打包包括位置坐标,有效状态,敌我子弹
reg [9:0] bullet_x_reg;
reg [9:0] bullet_y_reg;
reg bullet_valid_reg;
reg bullet_is_enemy_reg;
// 时序逻辑,在时钟上升沿或复位信号下降沿触发
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// 复位时,子弹坐标清零,有效标志清零,敌方子弹标志清零
bullet_x_reg <= 0;
bullet_y_reg <= 0;
bullet_valid_reg <= 0;
bullet_is_enemy_reg <= 0;
end else begin
if (fire_en) begin // 我方坦克发射子弹
// 子弹初始位置设置在我方坦克中央
bullet_x_reg <= player_x + 8;
bullet_y_reg <= player_y;
// 子弹有效标志置1
bullet_valid_reg <= 1;
// 子弹为我方子弹,标志置0
bullet_is_enemy_reg <= 0;
end else if (enemy_fire) begin // 敌方坦克发射子弹
// 子弹初始位置设置在敌方坦克中央
bullet_x_reg <= enemy_x + 8;
bullet_y_reg <= enemy_y;
// 子弹有效标志置1
bullet_valid_reg <= 1;
// 子弹为敌方子弹,标志置1
bullet_is_enemy_reg <= 1;
end
if (bullet_valid_reg) begin
if (bullet_is_enemy_reg) begin
// 敌方子弹向下飞行,y坐标加5
bullet_y_reg <= (bullet_y_reg < 480) ? bullet_y_reg + 5 : 0;
if (bullet_y_reg == 0) bullet_valid_reg <= 0; // 子弹超出屏幕范围,有效标志清零
end else begin
// 我方子弹向上飞行,y坐标减5
bullet_y_reg <= (bullet_y_reg > 0) ? bullet_y_reg - 5 : 0;
if (bullet_y_reg == 0) bullet_valid_reg <= 0; // 子弹超出屏幕范围,有效标志清零
end
end
end
// 将寄存器的值输出
bullet_x <= bullet_x_reg;
bullet_y <= bullet_y_reg;
bullet_valid <= bullet_valid_reg;
bullet_is_enemy <= bullet_is_enemy_reg;
end
// 碰撞检测函数,判断子弹是否与坦克发生碰撞
function reg collision_detect(
input [9:0] bx, // 子弹的x坐标
input [9:0] by, // 子弹的y坐标
input [9:0] tx, // 坦克的x坐标
input [9:0] ty // 坦克的y坐标
);
// 通过判断子弹坐标是否在坦克矩形范围内来检测碰撞
collision_detect = (bx >= tx - 8 && bx <= tx + 8 &&
by >= ty - 8 && by <= ty + 8);
endfunction
endmodule
5. 得分模块(score_module.v)
功能
该模块根据子弹击中敌方坦克或我方坦克的情况更新玩家的得分。当我方子弹击中敌方坦克时,得分增加;当敌方子弹击中我方坦克时,得分减少。
算法
在每个时钟上升沿检查复位信号和击中信号。复位时,将得分清零。当检测到子弹击中敌方坦克信号时,得分加 10;当检测到子弹击中我方坦克信号时,得分减 5,但得分最低为 0。
状态
得分状态:通过 16 位寄存器存储玩家的得分。
逻辑
在每个时钟上升沿,首先检查复位信号。如果复位信号有效,将得分清零。然后检查击中信号,根据不同的击中情况更新得分。
原理
根据游戏规则,通过对击中信号的判断,实现得分的增加或减少,为玩家提供游戏成绩的反馈。
代码如下:
//得分模块管理
module score_module(
input wire clk,
input wire rst_n, // 复位信号,低电平有效,将得分清零
input wire bullet_hit_enemy, // 子弹击中敌方坦克信号,高电平有效
input wire bullet_hit_player, // 子弹击中我方坦克信号,高电平有效
output reg [15:0] score // 玩家的得分,16位寄存器存储
);
// 时序逻辑,在时钟上升沿或复位信号下降沿触发
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// 复位时,得分清零
score <= 0;
end else begin
if (bullet_hit_enemy) begin
// 子弹击中敌方坦克,得分加10
score <= score + 10;
end else if (bullet_hit_player) begin
// 子弹击中我方坦克,得分减5,最低为0
score <= (score > 5) ? score - 5 : 0;
end
end
end
endmodule
6. VGA 显示模块(vga_controller.v)
关于VGA需要补充的一点:在代码中的计数器原理!
功能
该模块生成 VGA 时序信号(行同步信号和场同步信号),并根据坦克和子弹的位置信息,在 VGA 屏幕上绘制游戏画面 。同时,根据不同的元素(我方坦克、敌方坦克、子弹)设置不同的颜色。
算法
使用行计数器和场计数器生成 VGA 时序信号。在行计数器和场计数器的计数过程中,根据特定的范围设置行同步信号和场同步信号的电平。在有效显示区域内,根据坦克和子弹的坐标判断当前像素点是否属于某个元素,如果是,则设置相应的 RGB 颜色;否则,设置为黑色。在消隐期,将 RGB 颜色信号清零。
状态
行计数器状态:记录当前行的位置。
场计数器状态:记录当前场的位置。
同步信号状态:行同步信号和场同步信号的电平。
RGB 颜色状态:当前像素点的颜色。
逻辑
在每个时钟上升沿检查复位信号和进行相应操作。复位时,将所有状态初始化为默认值。正常工作时,行计数器和场计数器不断计数,根据计数结果生成同步信号和判断是否处于有效显示区域。在有效显示区域内,根据元素的位置信息设置 RGB 颜色。
原理
VGA 显示的原理是通过行同步信号和场同步信号同步显示器的扫描过程,在有效显示区域内,根据 RGB 颜色信号控制每个像素点的颜色,从而实现图像的显示。
假设 704 个像素的行时序和 512 行的场时序配置如下:
行时序:
有效显示区域:640 个像素
行同步脉冲:48 个像素
后沿:8 个像素
前沿:8 个像素
总计:640 + 48 + 8 + 8 = 704 个像素
场时序:
有效显示行数:480 行
场同步脉冲:2 行
后沿:20 行
前沿:10 行
总计:480 + 2 + 20 + 10 = 512 行
代码如下:
//VGA显示控制模块
module vga_controller(
input wire clk, // 25MHz像素时钟,为VGA时序提供基准,确保图像正确显示
input wire rst_n, // 复位信号,低电平有效,将VGA状态初始化
input wire [9:0] player_x, // 我方坦克的x坐标,用于在VGA屏幕上绘制我方坦克
input wire [9:0] player_y, // 我方坦克的y坐标,用于在VGA屏幕上绘制我方坦克
input wire [9:0] enemy_x, // 敌方坦克的x坐标,用于在VGA屏幕上绘制敌方坦克
input wire [9:0] enemy_y, // 敌方坦克的y坐标,用于在VGA屏幕上绘制敌方坦克
input wire enemy_alive, // 敌方坦克存活标志,1表示存活
input wire [9:0] bullet_x, // 子弹的x坐标,用于在VGA屏幕上绘制子弹
input wire [9:0] bullet_y, // 子弹的y坐标,用于在VGA屏幕上绘制子弹
input wire bullet_valid, // 子弹有效标志,1表示子弹处于飞行状态
input wire bullet_is_enemy, // 子弹是否为敌方子弹标志,1表示敌方子弹
input wire [15:0] score, // 玩家的得分
output reg hsync, // VGA行同步信号,用于同步显示器的行扫描
output reg vsync, // VGA场同步信号,用于同步显示器的场扫描
output reg [2:0] rgb // RGB颜色信号,简化为3位,控制显示器的像素颜色
);
// 行计数器,用于生成行同步信号和确定当前行位置
reg [9:0] hcnt;
// 场计数器,用于生成场同步信号和确定当前场位置
reg [9:0] vcnt;
// 时序逻辑,在时钟上升沿或复位信号下降沿触发
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// 复位时,行计数器和场计数器清零
hcnt <= 0;
vcnt <= 0;
// 行同步信号和场同步信号置高电平
hsync <= 1;
vsync <= 1;
// RGB颜色信号清零
rgb <= 0;
end else begin
// 行计数器加1
hcnt <= hcnt + 1;
if (hcnt == 10'd704) begin // 一行结束
// 行计数器清零
hcnt <= 0;
// 场计数器加1
vcnt <= vcnt + 1;
if (vcnt == 10'd512) begin // 一场结束
// 场计数器清零
vcnt <= 0;
end
end
// 行同步信号生成
if (hcnt >= 10'd656 && hcnt < 10'd704) begin
hsync <= 0;
end
else begin
hsync <= 1;
end
// 场同步信号生成
if (vcnt >= 10'd490 && vcnt < 10'd492) begin
vsync <= 0;
end else begin
vsync <= 1;
end
// 有效显示区域判断
if (hcnt < 10'd640 && vcnt < 10'd480) begin
// 绘制我方坦克
if (hcnt >= player_x && hcnt < player_x + 16 &&
vcnt >= player_y && vcnt < player_y + 16) begin
// 我方坦克显示为白色
rgb <= 3'b111;
end
// 绘制敌方坦克
else if (enemy_alive && hcnt >= enemy_x && hcnt < enemy_x + 16 &&
vcnt >= enemy_y && vcnt < enemy_y + 16) begin
// 敌方坦克显示为红色
rgb <= 3'b001;
end
// 绘制子弹
else if (bullet_valid && hcnt >= bullet_x && hcnt < bullet_x + 4 &&
vcnt >= bullet_y && vcnt < bullet_y + 4) begin
if (bullet_is_enemy) begin
// 敌方子弹显示为蓝色
rgb <= 3'b010;
end else begin
// 我方子弹显示为绿色
rgb <= 3'b100;
end
end
else begin
// 其他区域显示为黑色
rgb <= 3'b000;
end
end else begin
// 消隐期无显示,RGB颜色信号清零
rgb <= 0;
end
end
end
endmodule
7. 顶层模块
功能
该模块是整个坦克大战游戏系统的顶层模块,将各个子模块连接起来,实现游戏的整体功能。包括按键消抖、坦克控制、子弹管理、得分计算和 VGA 显示等功能的集成。
算法
通过实例化各个子模块,并将它们的输入输出信号进行连接,实现模块之间的通信和协同工作。根据按键输入信号,控制我方坦克的移动和射击;敌方坦克自动进行随机移动和射击;子弹管理模块处理子弹的发射和飞行;得分模块根据子弹击中情况更新得分;VGA 显示模块将游戏画面显示在显示器上。
状态
整个游戏系统的状态由各个子模块的状态共同决定,包括按键状态、坦克位置状态、子弹状态、得分状态等。
逻辑
在顶层模块中,首先对按键输入信号进行消抖处理,生成稳定的按键脉冲信号。然后根据按键脉冲信号生成移动方向控制信号,控制我方坦克的移动和射击。敌方坦克独立进行随机移动和射击。子弹管理模块根据坦克的发射信号处理子弹的发射和飞行,并进行碰撞检测。得分模块根据碰撞检测结果更新得分。最后,VGA 显示模块根据坦克、子弹的位置和得分信息,将游戏画面显示在显示器上。
原理
通过模块化设计,将游戏的不同功能拆分成多个子模块,每个子模块负责特定的任务。顶层模块将这些子模块连接起来,通过信号的传递和处理,实现整个游戏系统的功能。
代码如下:
//顶层模块
module tank_war_top(
input wire clk_50m, // 开发板50MHz晶振时钟
input wire rst_n, // 全局复位(低有效)
input wire [3:0] keys, // 方向按键(上/下/左/右)
input wire fire_key, // 射击按键
output wire hsync, // VGA行同步信号
output wire vsync, // VGA场同步信号
output wire [2:0] rgb // RGB颜色信号(3位简化)
);
// ================== 1. 时钟处理(50MHz→25MHz分频)==================
reg clk_25m;
clk_25m instance_name
(
// Clock out ports
.clk_out1(clk_25m), // output clk_out1
// Status and control signals
.reset(rst_n), // input reset
.locked(), // output locked
// Clock in ports
.clk_in1(clk_50m)); // input clk_in1
// ================== 2. 按键消抖模块实例化 ==================
wire [3:0] key_pulse_down; // 方向键按下脉冲
wire fire_pulse_down; // 射击键按下脉冲
// 方向键消抖(4个按键,使用generate循环实例化)
genvar i;
generate
for(i=0; i<4; i=i+1) begin: dir_key_debounce
key_debounce u_dir_key_debounce(
.clk(clk_50m),
.rst_n(rst_n),
.key_in(keys[i]),
.key_out(), // 消抖后电平信号(未使用,悬空)
.key_pulse_down(key_pulse_down[i]), // 按下脉冲
.key_pulse_up() // 释放脉冲(未使用,悬空)
);
end
endgenerate
// 射击键消抖(单独实例化)
key_debounce u_fire_key_debounce(
.clk(clk_50m),
.rst_n(rst_n),
.key_in(fire_key),
.key_out(), // 消抖后电平信号(未使用)
.key_pulse_down(fire_pulse_down), // 射击按下脉冲
.key_pulse_up() // 释放脉冲(未使用)
);
// ================== 3. 移动方向生成 ==================
reg [2:0] move_dir; // 001-上,010-下,100-左,101-右
always @(*) begin
move_dir = 3'b000;
if(key_pulse_down[0]) move_dir = 3'b001; // 上
if(key_pulse_down[1]) move_dir = 3'b010; // 下
if(key_pulse_down[2]) move_dir = 3'b100; // 左
if(key_pulse_down[3]) move_dir = 3'b101; // 右
end
// ================== 4. 我方坦克模块实例化 ==================
wire [9:0] player_x, player_y;
wire fire_en; // 我方坦克射击使能
player_tank u_player_tank(
.clk(clk_50m),
.rst_n(rst_n),
.move_dir(move_dir),
.fire_req(fire_pulse_down), // 射击请求来自射击键消抖脉冲
.x(player_x),
.y(player_y),
.fire_en(fire_en)
);
// ================== 5. 敌方坦克模块实例化 ==================
wire [9:0] enemy_x, enemy_y;
wire enemy_fire; // 敌方坦克射击信号
enemy_tank u_enemy_tank(
.clk(clk_50m),
.rst_n(rst_n),
.x(enemy_x),
.y(enemy_y),
.enemy_fire(enemy_fire)
);
// ================== 6. 子弹管理模块实例化 ==================
wire [9:0] bullet_x, bullet_y;
wire bullet_valid, bullet_is_enemy;
bullet_manager u_bullet_manager(
.clk(clk_50m),
.rst_n(rst_n),
.player_x(player_x),
.player_y(player_y),
.enemy_x(enemy_x),
.enemy_y(enemy_y),
.fire_en(fire_en), // 我方发射使能
.enemy_fire(enemy_fire), // 敌方发射信号
.bullet_x(bullet_x),
.bullet_y(bullet_y),
.bullet_valid(bullet_valid),
.bullet_is_enemy(bullet_is_enemy)
);
// ================== 7. 碰撞检测与得分模块 ==================
wire bullet_hit_enemy, bullet_hit_player;
// 我方子弹击中敌方坦克(子弹有效+我方子弹+碰撞检测)
assign bullet_hit_enemy = bullet_valid && !bullet_is_enemy &&
u_bullet_manager.collision_detect(bullet_x, bullet_y, enemy_x, enemy_y);
// 敌方子弹击中我方坦克(子弹有效+敌方子弹+碰撞检测)
assign bullet_hit_player = bullet_valid && bullet_is_enemy &&
u_bullet_manager.collision_detect(bullet_x, bullet_y, player_x, player_y);
wire [15:0] score;
score_module u_score_module(
.clk(clk_50m),
.rst_n(rst_n),
.bullet_hit_enemy(bullet_hit_enemy),
.bullet_hit_player(bullet_hit_player),
.score(score)
);
// ================== 8. VGA显示模块实例化(25MHz时钟)==================
wire enemy_alive;
assign enemy_alive = 1'b1; // 简化处理,敌方坦克始终存活(可扩展为生命值系统)
vga_controller u_vga_controller(
.clk(clk_25m),
.rst_n(rst_n),
.player_x(player_x),
.player_y(player_y),
.enemy_x(enemy_x),
.enemy_y(enemy_y),
.enemy_alive(enemy_alive),
.bullet_x(bullet_x),
.bullet_y(bullet_y),
.bullet_valid(bullet_valid),
.bullet_is_enemy(bullet_is_enemy),
.score(score),
.hsync(hsync),
.vsync(vsync),
.rgb(rgb)
);
endmodule
三,最终效果
1. 与开发板连接的约束文件如下所示
# 系统时钟与复位
set_property PACKAGE_PIN W19 [get_ports clk_50m] ;# 50MHz有源晶振
set_property IOSTANDARD LVCMOS33 [get_ports clk_50m]
set_property PACKAGE_PIN B21 [get_ports rst_n] ;# 复位信号(拨码开关SW[0],闭合时低电平)
set_property IOSTANDARD LVCMOS33 [get_ports rst_n]
# 方向按键(轻触按键S[0]-S[3],按下低电平有效)
set_property PACKAGE_PIN D21 [get_ports {keys[0]}] ;# 上
set_property PACKAGE_PIN E21 [get_ports {keys[1]}] ;# 下
set_property PACKAGE_PIN G22 [get_ports {keys[2]}] ;# 左
set_property PACKAGE_PIN G21 [get_ports {keys[3]}] ;# 右
set_property IOSTANDARD LVCMOS33 [get_ports {keys[3:0]}]
# 射击按键(轻触按键S[4],按下低电平有效)
set_property PACKAGE_PIN B22 [get_ports fire_key]
set_property IOSTANDARD LVCMOS33 [get_ports fire_key]
# VGA显示接口(使用GPIO0扩展接口,单端信号)
set_property PACKAGE_PIN C18 [get_ports hsync] ;# 行同步
set_property PACKAGE_PIN C19 [get_ports vsync] ;# 场同步
set_property PACKAGE_PIN E19 [get_ports {rgb[0]}] ;# 红色(最低位)
set_property IOSTANDARD LVCMOS33 [get_ports {rgb[0]}] ;# 修改此处,将rgb[0]的IO标准改为LVCMOS33
set_property PACKAGE_PIN D19 [get_ports {rgb[1]}] ;# 绿色
set_property PACKAGE_PIN F19 [get_ports {rgb[2]}] ;# 蓝色(最高位)
set_property IOSTANDARD LVCMOS33 [get_ports {hsync vsync rgb[2:1]}]
# 未使用的信号(保留注释,避免误分配)
# set_property PACKAGE_PIN ... [get_ports {LED[*] UART_*}] ;# 其他外设可根据需要分配
2. 改进与发展
我们可以改变显示路径,使其在TFT显示屏上显示,并不非要通过电脑显示屏,
因为通常情况下,开发板通过 HDMI 线连接笔记本电脑,是无法仅用笔记本电脑显示的。因为笔记本电脑的 HDMI 接口一般是输出接口,只能将笔记本电脑的画面输出到外部显示设备,而不能接收外部设备的信号来在笔记本电脑屏幕上显示。
想在笔记本电脑上显示开发板的内容,一种可能的方法是使用带有视频输入功能的采集卡。将开发板通过 HDMI 线连接到采集卡的 HDMI 输入接口,然后将采集卡通过 USB 等接口连接到笔记本电脑,再在笔记本电脑上安装采集卡对应的软件,通过软件来显示和录制开发板输出的视频信号。
同时更加详细的改进,可以增设多个坦克,优化坦克的显示设置等。同时还可以进行设置多个小游戏,通过开发板的按钮选择进入不同的游戏界面等。
详看下回分解!