FPGA实战项目1——坦克大战

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 等接口连接到笔记本电脑,再在笔记本电脑上安装采集卡对应的软件,通过软件来显示和录制开发板输出的视频信号。

同时更加详细的改进,可以增设多个坦克,优化坦克的显示设置等。同时还可以进行设置多个小游戏,通过开发板的按钮选择进入不同的游戏界面等。

详看下回分解!


相关推荐
董厂长19 分钟前
LLM :Function Call、MCP协议与A2A协议
网络·人工智能·深度学习·llm
Elastic 中国社区官方博客22 分钟前
Elasticsearch:我们如何在全球范围内实现支付基础设施的现代化?
大数据·人工智能·elasticsearch·搜索引擎·全文检索·可用性测试
A旧城以西25 分钟前
MySQL----数据库的操作
java·开发语言·数据库·sql·学习·mysql
HUIBUR科技28 分钟前
人工智能与智能合约:如何用AI优化区块链技术中的合约执行?
人工智能·ai·智能合约
当当狸1 小时前
当当狸智能天文望远镜 TW2 | 用科技触摸星辰,让探索触手可及
人工智能·科技·内容运营
geneculture1 小时前
金融的本质是智融、融资的实质是融智、投资的关键是投智,颠覆传统金融学的物质资本中心论,构建了以智力资本为核心的新范式
大数据·人工智能·算法·金融·系统工程融智学
Green1Leaves1 小时前
从零开始学习人工智能(Python高级教程)Day6-Python3 正则表达式
python·学习·正则表达式
极小狐1 小时前
极狐Gitlab 如何创建并使用子群组?
数据库·人工智能·git·机器学习·gitlab
计算机小手3 小时前
全格式文档转 Markdown 工具,Docker 一键部署,支持 API 调用
经验分享·开源软件
hzj66 小时前
Sentinel学习
分布式·学习·sentinel