数字逻辑设计大程实验报告
1 设计说明
我们小组基于 Vivado 工程与 SWORD 开发板设计了一款单人游戏。游戏的灵感源自于几何冲刺,玩家需要使用改变重力的形式躲避前方生成的不规则物块,玩家通过使用SWORD开发板上的按钮进行对象的移动,并完成游戏的开始与暂停。
开始界面
状态开关拨到00,进入游戏初始画面,并播放我们的主题曲(节选自maimaidx2024开始界面音乐)
游戏中
状态开关拨到01,进入游戏。玩家通过切换一个游戏开关的状态,实现史蒂夫的加速度的上下切换,从而躲避从屏幕右侧随机生成的障碍物。切换加速度方向时,蜂鸣器会播放对应的音效(上下音效不同)。为了实现玩家向右移动的视觉效果,我们在玩家左侧通过粒子效果生成了拖尾,拖尾方向与玩家速度方向相反。我们把躲避的障碍物数量定义为游戏的分数,并用七段数码管实现了十进制分数的显示,分数家有五条生命,在屏幕左下角用心来显示。每发生一次碰撞,生命值减1,并进入一秒钟的无敌状态,在无敌状态期间,蜂鸣器会播放一段对应的无敌状态音乐。
游戏结束
当玩家生命值减为0,游戏结束,画面暂停,玩家拖尾逐渐消失,并在屏幕中央出现GAME OVER图像。此时玩家通过波动状态开关,可以重新进入开始界面,开始新一轮的游玩。
整体设计思路
我们采用 Top-Down 设计思路,由 top 模块来组织连接其他子模块,其中主要的功能模块有 map(地图模块), VGA,beep(蜂鸣器),game_logic(游戏逻辑),使用 IP 核来存储图片等数据。主要实现了图形显示与贴图,障碍物随机化,游戏人物移动,蜂鸣器等功能。
2 VGA设计思路
VGA模块主要用于处理游戏的显示状态与更新。
2.1 vga_ctrl 模块
此模块助教已提供。接收像素点的色彩信息($Din$),输出当前像素点的坐标($row,col$)和 $VGA$ 有关的变量($R,G,B,HS,VS$)
// -----------------------------------------------------------------------------
// VGA 控制模块 vga_ctrl
// 输入:
// clk - VGA 时钟信号(25 MHz)
// rst - 异步复位信号,高有效
// Din - 输入像素数据(12位,格式为 bbbb_gggg_rrrr)
// 输出:
// row - 像素 RAM 行地址(9位,480 行)
// col - 像素 RAM 列地址(10位,640 列)
// rdn - 读像素 RAM 使能(低有效)
// R,G,B - VGA 红、绿、蓝三色输出(各 4 位)
// HS - VGA 行同步信号
// VS - VGA 场同步信号
// -----------------------------------------------------------------------------
module vga_ctrl(
input clk, // vga clk = 25 MHz
input rst,
input [11:0]Din, // bbbb_gggg_rrrr, pixel
output reg [8:0]row, // pixel ram row address, 480 (512) lines
output reg [9:0]col, // pixel ram col address, 640 (1024) pixels
output reg rdn, // read pixel RAM(active_low)
output reg [3:0]R, G, B, // red, green, blue colors
output reg HS, VS // horizontal and vertical synchronization
);
// h_count: VGA horizontal counter (0~799)
reg [9:0] h_count; // VGA horizontal counter (0~799): pixels
initial h_count = 10'h0;
always @ (posedge clk) begin
if (rst) h_count <= 10'h0;
else if (h_count == 10'd799)
h_count <= 10'h0;
else h_count <= h_count + 10'h1;
end
// v_count: VGA vertical counter (0~524)
reg [9:0] v_count; // VGA vertical counter (0~524): pixel
initial v_count = 10'h0;
always @ (posedge clk or posedge rst) begin
if (rst) v_count <= 10'h0;
else if (h_count == 10'd799) begin
if (v_count == 10'd524) v_count <= 10'h0;
else v_count <= v_count + 10'h1;
end
end
// signals, will be latched for outputs
wire [9:0] row_addr = v_count - 10'd35; // pixel ram row addr
wire [9:0] col_addr = h_count - 10'd143; // pixel ram col addr
wire h_sync = (h_count > 10'd95); // 96 -> 799
wire v_sync = (v_count > 10'd1); // 2 -> 524
wire read = (h_count > 10'd142) && // 143 -> 782
(h_count < 10'd783) && // 640 pixels
(v_count > 10'd34) && // 35 -> 514
(v_count < 10'd525); // 480 lines
// vga signals
always @ (posedge clk) begin
row <= row_addr[8:0]; // pixel ram row address
col <= col_addr; // pixel ram col address
rdn <= ~read; // read pixel (active low)
HS <= h_sync; // horizontal synchronization
VS <= v_sync; // vertical synchronization
R <= rdn ? 4'h0 : Din[3:0]; // 3-bit red
G <= rdn ? 4'h0 : Din[7:4]; // 3-bit green
B <= rdn ? 4'h0 : Din[11:8]; // 2-bit blue
end
endmodule
2.3 vga_screen_pic 模块
输出当前像素点的色彩信息并提供给 vga_ctrl 模块
2.3.1 IP 核的生成
此步骤将图片转换为符合格式的 $.coe$ 文件。我们采用了python与matlab两种语言实现原始图片(raw image)转换为$coe$文件以进行进一步处理,该过程可以处理各种后缀的图片。
获取原始图片,例如开始界面:
StartImage.png

利用python将图片处理成合适的分辨率大小
DPIadjust.py
from PIL import Image
# 打开图片
image = Image.open("StartImage.png")
width = 40
height = 40
# 直接调整分辨率(DPI)
image.info['dpi'] = (width,height) # 修改为72DPI
rgb_image = image.convert("RGB")
low_res_image= rgb_image.resize((width, height), Image.Resampling.LANCZOS)
# 保存
low_res_image.save("StartImage.jpg", quality=100)
利用$matlib$将图片处理成$.coe$文件
ImgtoCoe.m
clear;
clc;
% 读取图片(替换为图片路径)
image_array = imread('your_image.png');
% 检查图片尺寸
[height, width, ~] = size(image_array);
if height ~= 40 || width ~= 40 %(定义图片合法大小)
error('图片尺寸不是40x40,请检查!');
end
% 提取 RGB 分量
red = image_array(:,:,1); % R 通道 (uint8)
green = image_array(:,:,2); % G 通道 (uint8)
blue = image_array(:,:,3); % B 通道 (uint8)
% 转换为 uint32 并展开成一维向量(按行扫描顺序)
r = uint32(reshape(red', 1, [])); % 转置后展开
g = uint32(reshape(green', 1, []));
b = uint32(reshape(blue', 1, []));
% 初始化 RGB 数据(12-bit RGB444 格式)
rgb = zeros(1, 1600, 'uint32'); % 40x40=1600像素
% 将 RGB888 转换为 RGB444(每个通道取高4位)
for i = 1:1600
r_4bit = bitshift(r(i), -4); % 取R通道高4位
g_4bit = bitshift(g(i), -4); % 取G通道高4位
b_4bit = bitshift(b(i), -4); % 取B通道高4位
% 合并为12-bit RGB (R[3:0]G[3:0]B[3:0])
rgb(i) = bitor(bitshift(r_4bit, 8), bitor(bitshift(g_4bit, 4), b_4bit));
end
% 写入 COE 文件,这里改成对应的文件名字
fid = fopen('output.coe', 'w');
% COE 文件头
fprintf(fid, 'MEMORY_INITIALIZATION_RADIX=16;\n');
fprintf(fid, 'MEMORY_INITIALIZATION_VECTOR=\n');
% 写入像素数据(16进制)
for i = 1:1600
if i == 1600
fprintf(fid, '%03x;', rgb(i)); % 最后一个数据加分号
else
fprintf(fid, '%03x,\n', rgb(i)); % 其他数据加逗号和换行
end
end
fclose(fid);
disp('40x40 COE 文件生成成功!');
2.3.2 IP 核的调用
模块接口以及变量定义与初始化,包含游戏对象常量,拖尾效果常量,HEART常量:
module vga_screen_pic(
input wire [9:0] pix_x,
input wire [8:0] pix_y,
input wire clk,
input wire [1:0] gamemode,
input wire [8:0] player_y,//玩家x固定
input wire [2:0] heart, //一共有5条命
//障碍物
input logic [9:0] [1:0] obstacle_class,
input logic [9:0] [9:0] obstacle_x_game_left,
input logic [9:0] [9:0] obstacle_x_game_right,
input logic [9:0] [8:0] obstacle_y_game_up,
input logic [9:0] [8:0] obstacle_y_game_down,
// Trail effect inputs(拖尾轨迹)
input logic [40:0] [9:0] trail_x,
input logic [40:0] [8:0] trail_y,
input logic [40:0] [3:0] trail_life,
output reg [11:0] rgb //这里是bgr的依次输出
);
//参量说明
// Game object constants (游戏对象常量)
parameter PLAYER_X = 160,
PLAYER_SIZE = 40,
GAMEOVER_X = 220,
GAMEOVER_Y = 140,
UPPER_BOUND = 20,
LOWER_BOUND = 460;
parameter DEFAULT_COLOR = 12'h000,
COLOR_INITIAL = 12'h0F0,
COLOR_INGAME = 12'hFFF,
COLOR_PAUSED = 12'hFF0,
COLOR_ENDED = 12'hFFF,
COLOR_OBSTACLE = 12'hFA0,
COLOR_PLAYER = 12'h00F;
// Trail effect constants (拖尾效果常量)
parameter TRAIL_SIZE = 4, // Trail particle size
TRAIL_BASE_COLOR = 12'h44F, // Base trail color (darker blue)
TRAIL_FADE_LEVELS = 10; // Number of fade levels
parameter H_PIC = 10'd200, // over图片宽度 (Game Over image width/height for square)
SCREEN_W_PIC = 10'd640, // VGA 宽度 (VGA width)
PLAYER_PIC = 10'd40; // Player image size
//HEART参量
parameter HEART_SIZE = 10'd18, // Heart图片的宽度
// 第一张图片的位置
HEART_Y = 10'd460,
HEART_X = 10'd0,
MAX_HEART = 5; // 最大心形数量
parameter UNIT_SIZE = 30;
// --- 新增:定义一个将RGB转换为BGR的宏 ---
`define RGB_TO_BGR(color) {color[3:0], color[7:4], color[11:8]}
ROM模块,包含:玩家,开始界面,结束界面,背景界面,心形界面,四种不同贴图的障碍物
// 玩家
StevePlayer player_rom (
.clka(clk), // input wire clka
.addra(pic_romaddrPlayer), // input wire [10 : 0] addra
.douta(player_out_data) // output wire [11 : 0] douta
);
// 开始界面
start game_start (
.clka(clk), // input wire clka
.addra(pic_romaddrStart), // input wire [18 : 0] addra
.douta(game_start_data) // output wire [11 : 0] douta
);
// 结束界面
game_over game_over_rom (
.clka(clk),
.addra(pic_romaddrOver),
.douta(game_over_data)
);
// 背景界面
background background_rom (
.clka(clk), // input wire clka
.addra(pic_romaddrBackground), // input wire [18 : 0] addra
.douta(background_data) // output wire [11 : 0] douta
);
// 心形界面
Heart heart_rom (
.clka(clk),
.addra(pic_romaddrHeart),
.douta(heart_data)
);
// 四种不同类型的障碍物
black black_rom (
.clka(clk),
.addra(pic_romaddrBlack), // 连接地址寄存器
.douta(black_data) // 连接到数据线而非直接连rgb
);
skeleton skeleton_rom (
.clka(clk),
.addra(pic_romaddrSkeleton),
.douta(skeleton_data)
);
crepper crepper_rom (
.clka(clk),
.addra(pic_romaddrCrepper),
.douta(crepper_data)
);
zomber zomber_rom (
.clka(clk),
.addra(pic_romaddrZomber),
.douta(zomber_data)
);
计算rom地址
always_comb begin
pic_romaddrBackground = (pix_y >= UPPER_BOUND && pix_y < LOWER_BOUND) ? pix_x + (pix_y - UPPER_BOUND) * SCREEN_W_PIC : 0; // In-game background ROM address
pic_romaddrStart = pix_x + pix_y * SCREEN_W_PIC;
pic_romaddrPlayer = (pix_x >= PLAYER_X && pix_x < PLAYER_X + PLAYER_SIZE &&
pix_y >= player_y && pix_y < player_y + PLAYER_SIZE) ?
(pix_x - PLAYER_X) + (pix_y - player_y) * PLAYER_PIC : 0;
// Default to 0 if out of bounds
pic_romaddrOver = (pix_x >= GAMEOVER_X && pix_x < GAMEOVER_X + H_PIC &&
pix_y >= GAMEOVER_Y && pix_y < GAMEOVER_Y + H_PIC) ?
(pix_x - GAMEOVER_X) + (pix_y - GAMEOVER_Y) * H_PIC : 0;
// Default to 0 if out of bounds
pic_romaddrHeart = 0;
for (int h = 0; h < MAX_HEART; h++) begin
if (pix_y >= HEART_Y && pix_y < HEART_Y + HEART_SIZE &&
pix_x >= HEART_X + h*HEART_SIZE && pix_x < HEART_X + (h+1)*HEART_SIZE && h < heart) begin
pic_romaddrHeart = (pix_x - (HEART_X + h*HEART_SIZE)) + (pix_y - HEART_Y) * HEART_SIZE;
end
end
// 计算障碍物的ROM地址
pic_romaddrBlack = 0;
pic_romaddrSkeleton = 0;
pic_romaddrCrepper = 0;
pic_romaddrZomber = 0;
for (int j = 0; j < 10; j = j + 1) begin
if (pix_x >= obstacle_x_game_left[j] && pix_x < obstacle_x_game_right[j] &&
pix_y >= obstacle_y_game_up[j] && pix_y < obstacle_y_game_down[j]) begin
// 计算障碍物内的相对坐标
automatic logic [9:0] rel_x = pix_x - obstacle_x_game_left[j];
automatic logic [8:0] rel_y = pix_y - obstacle_y_game_up[j];
// 缩放到单元格内坐标(0-29)
automatic logic [4:0] unit_x = rel_x % UNIT_SIZE;
automatic logic [4:0] unit_y = rel_y % UNIT_SIZE;
// 根据障碍物类型计算ROM地址
case (obstacle_class[j])
2'd0: pic_romaddrBlack = unit_x + unit_y * UNIT_SIZE; // 小黑
2'd1: pic_romaddrSkeleton = unit_x + unit_y * UNIT_SIZE; // 小白
2'd2: pic_romaddrCrepper = unit_x + unit_y * UNIT_SIZE; // 苦力怕
2'd3: pic_romaddrZomber = unit_x + unit_y * UNIT_SIZE; // 僵尸
endcase
break; // 找到一个障碍物后停止搜索
end
end
end
拖尾效果计算
always_comb begin
trail_hit = 1'b0;
trail_alpha = 4'd0;
trail_idx = 0;
// Check all trail particles to see if current pixel hits any
for (integer i = 0; i < 41; i = i + 1) begin
// Use center as reference, so calculate left/top and right/bottom
if (trail_life[i] > 0 &&
pix_x >= (trail_x[i] - TRAIL_SIZE/2) && pix_x < (trail_x[i] + (TRAIL_SIZE+1)/2) &&
pix_y >= (trail_y[i] - TRAIL_SIZE/2) && pix_y < (trail_y[i] + (TRAIL_SIZE+1)/2)) begin
trail_hit = 1'b1;
trail_alpha = trail_life[i]; // Use life as alpha intensity
trail_idx = i;
break; // Use first hit trail (highest priority)
end
end
end
基于拖尾的生命值的颜色计算,距离玩家越远,拖尾生命值越低,显示效果越暗
always_comb begin
case (trail_alpha)
4'd10: trail_color = 12'hFDD; // Brightest trail (white)
4'd9: trail_color = 12'hEEF; // Very bright (light blue-white)
4'd8: trail_color = 12'hDDF; // Bright (light blue)
4'd7: trail_color = 12'hCCF; // Medium-bright (medium light blue)
4'd6: trail_color = 12'hBBE; // Medium (medium blue)
4'd5: trail_color = 12'hAAD; // Medium-dim (darker blue)
4'd4: trail_color = 12'h99C; // Dim (dark blue)
4'd3: trail_color = 12'h88B; // Very dim (very dark blue)
4'd2: trail_color = 12'h77A; // Almost invisible (extremely dark blue)
4'd1: trail_color = 12'h669; // Barely visible (near black)
default: trail_color = 12'h000; // Invisible
endcase
end
像素类型状态信号说明
reg [3:0] pixel_state; // Changed from [2:0] to [3:0]
integer i;
//确定当前像素的状态
//0: Border (边界)
//1: Obstacle (障碍物)
//2: player(史蒂夫)
//3: Game Over image (游戏结束图片)
//4: Game Over background (游戏结束背景)
//5: In-game background (游戏内背景)
//6: 初始画面
//7: Paused screen (暂停画面)
//8: Trail particle (拖尾粒子) - New state
//9: Heart (心形图标)
//10: 障碍物-小黑 obstacle_class = 2'd0
//11: 障碍物-小白 obstacle_class = 2'd1
//12: 障碍物-苦力怕 obstacle_class = 2'd2
//13: 障碍物-僵尸 obstacle_class = 2'd3
判断像素状态
always_comb begin
pixel_state = 4'd0; // Default to background (默认为背景)
if (gamemode == 2'b00) begin
pixel_state = 4'd6; //(初始画面)
end
else begin
if(heart != 0) begin
for (int h = 0; h < MAX_HEART; h++) begin
if (pix_y >= HEART_Y && pix_y < HEART_Y + HEART_SIZE &&
pix_x >= HEART_X + h*HEART_SIZE && pix_x < HEART_X + (h+1)*HEART_SIZE &&
h < heart) begin
pixel_state = 4'd9; // Heart状态
break;
end
end
end
// 如果不是heart,再判断其他
if (pixel_state != 4'd9) begin
if (gamemode == 2'b01) begin
if (pix_y <= UPPER_BOUND || pix_y >= LOWER_BOUND) begin
pixel_state = 4'd0; // Border (边界)
end
else if (pix_x >= PLAYER_X && pix_x < PLAYER_X + PLAYER_SIZE &&
pix_y >= player_y && pix_y < player_y + PLAYER_SIZE)
begin
pixel_state = 4'd2; // Player (玩家)
end
else begin
logic is_obstacle;
is_obstacle = 1'b0;
for (i = 0; i < 10; i = i + 1) begin
// 使用width和height参数计算障碍物边界
if (pix_x >= obstacle_x_game_left[i] && pix_x < obstacle_x_game_right[i]&&
pix_y >= obstacle_y_game_up[i] && pix_y < obstacle_y_game_down[i])
begin
// 根据obstacle_class设置对应的状态
case (obstacle_class[i])
2'd0: pixel_state = 4'd10; // 小黑
2'd1: pixel_state = 4'd11; // 小白
2'd2: pixel_state = 4'd12; // 苦力怕
2'd3: pixel_state = 4'd13; // 僵尸
default: pixel_state = 4'd1; // 默认障碍物
endcase
is_obstacle = 1'b1;
break;
end
end
if (!is_obstacle) begin
if (trail_hit) pixel_state = 4'd8; // Trail particle
else pixel_state = 4'd5; // In-game background
end
end
end
else if (gamemode == 2'b11) begin
// Game Over图片
if (pix_x >= GAMEOVER_X && pix_x < GAMEOVER_X + H_PIC &&
pix_y >= GAMEOVER_Y && pix_y < GAMEOVER_Y + H_PIC) begin
pixel_state = 4'd3;
end
// 边界
else if (pix_y <= UPPER_BOUND || pix_y >= LOWER_BOUND) begin
pixel_state = 4'd0;
end
// 玩家
else if (pix_x >= PLAYER_X && pix_x < PLAYER_X + PLAYER_SIZE &&
pix_y >= player_y && pix_y < player_y + PLAYER_SIZE) begin
pixel_state = 4'd2;
end
// 障碍物和背景
else begin
logic is_obstacle;
is_obstacle = 1'b0;
for (i = 0; i < 10; i = i + 1) begin
// 使用width和height参数计算障碍物边界
if (pix_x >= obstacle_x_game_left[i] && pix_x < obstacle_x_game_right[i]&&
pix_y >= obstacle_y_game_up[i] && pix_y < obstacle_y_game_down[i]) begin
// 根据obstacle_class设置对应的状态
case (obstacle_class[i])
2'd0: pixel_state = 4'd10; // 小黑
2'd1: pixel_state = 4'd11; // 小白
2'd2: pixel_state = 4'd12; // 苦力怕
2'd3: pixel_state = 4'd13; // 僵尸
default: pixel_state = 4'd1; // 默认障碍物
endcase
is_obstacle = 1'b1;
break;
end
end
if (!is_obstacle) begin
if (trail_hit) pixel_state = 4'd8; // Trail particle
else pixel_state = 4'd4; // Game over background
end
end
end //end gamemode 2'b11
else if (gamemode == 2'b10) pixel_state = 4'd7;
else begin
pixel_state = 4'd0; //黑色背景
end
end
end
end //end pixel_state detection
修改RGB输出部分,处理不同类型障碍物的颜色
always_comb begin
// Default to black
rgb = `RGB_TO_BGR(DEFAULT_COLOR); // 使用宏
case (gamemode)
2'b00: begin // 初始游戏模式
rgb = `RGB_TO_BGR(game_start_data); // 使用宏
end
2'b01, 2'b11: begin // 游戏进行模式和游戏结束模式
case (pixel_state)
4'd0: rgb = `RGB_TO_BGR(DEFAULT_COLOR); // Border (边界) or Default (默认)
4'd1: rgb = `RGB_TO_BGR(COLOR_OBSTACLE); // 普通障碍物
4'd2: rgb = `RGB_TO_BGR(player_out_data); // Player (玩家)
4'd3: rgb = `RGB_TO_BGR(game_over_data); // 游戏结束图片
4'd4: rgb = `RGB_TO_BGR(background_data); // 游戏结束背景
4'd5: rgb = `RGB_TO_BGR(background_data); // In-game background (游戏内背景)
4'd8: rgb = `RGB_TO_BGR(trail_color); // 轨迹粒子
4'd9: rgb = `RGB_TO_BGR(heart_data); // 心形图标
// 障碍物类型对应ROM数据
4'd10: rgb = `RGB_TO_BGR(black_data); // 小黑 - 使用ROM数据
4'd11: rgb = `RGB_TO_BGR(skeleton_data); // 小白 - 使用ROM数据
4'd12: rgb = `RGB_TO_BGR(crepper_data); // 苦力怕 - 使用ROM数据
4'd13: rgb = `RGB_TO_BGR(zomber_data); // 僵尸 - 使用ROM数据
default: rgb = `RGB_TO_BGR(DEFAULT_COLOR);
endcase
end
2'b10: begin //暂停模式
if (pixel_state == 4'd9)
rgb = `RGB_TO_BGR(heart_data); // 显示心形图标
else
rgb = `RGB_TO_BGR(COLOR_PAUSED);
end
default: rgb = `RGB_TO_BGR(DEFAULT_COLOR);
endcase
end
endmodule
3 蜂鸣器设计思路
3.1 实现原理
我们采用无源蜂鸣器,内部不带震荡源,因此需要PWM方波驱动其发声。PWM方波的频率决定声音的音调,而PWM方波的占空比决定声音的响度。所以只需产生不同频率和占空比的PWM方波去驱动无源蜂鸣器,就能让无源蜂鸣器发出想要的声音序列。
3.2.1 top_beep 模块
我们设计了三种beep状态,分别为$beep_gamestart$,$beep_gameover$,$beep_gaming$,归总入top中。
module top_beep(
input wire clk,
input [1:0] gamemode,
input wire sw,
output reg beep
);
wire beep_start;
wire beep_over;
wire beep_player;
initial begin
beep = 1'b0;
end
beep_gamestart bp_gs(.clk(clk), .gamemode(gamemode), .beep(beep_start));
beep_gameover bp_go(.clk(clk), .gamemode(gamemode), .beep(beep_over));
beep_gaming bp_gi(.clk(clk), .gamemode(gamemode), .sw(sw), .beep(beep_player));
always @(posedge clk) begin
if (gamemode == 2'b00) begin
beep = beep_start; // 游戏待开始状态,beep 为 game_start
end
else if (gamemode == 2'b11) begin
beep = beep_over; // 游戏结束状态,beep 为 game_over
end
else if (gamemode == 2'b01) begin
beep = beep_player; // 游戏进行状态,beep 为 player 操作反馈
end
else begin
beep = 1'b0; // 其他状态静音
end
end
endmodule
3.2.2 beep_gamestart与beep_gameover模块
$clk$ 频率为 $100MHz$ ,周期为 $10ns$,该音乐的 bpm 为 $120$ ,即每分钟$120$拍。以 1 个 16 分音符的长度为单位时间,即 $1/8=0.125s=125ms$ ,则一单位时间等价于$12500000$个clk周期。根据每个音的音调换算成clk周期,例如$A4$音,$f=440Hz$,音波周期$T=1/440=2272727ns=227273$个$clk$周期。
beep_gamestart.v
module beep_gamestart(
input clk,
input [1:0] gamemode, // 游戏状态变量
output reg beep
);
reg rst;
reg [23:0] cnt; //cnt用来计时,记录125ms的时间,这里4/4拍的音乐,每 125ms 产生一个音符
reg [5:0] cnt_125ms; // 125ms个数计数,用来数经过了几拍
reg [19:0] freq_cnt; // 音调频率计数
reg [19:0] freq_data; // 音调频率
wire [19:0] duty_data; // 占空比
initial begin // 初始化所有 reg 信号
rst = 1'b0;
beep = 1'b0;
cnt = 24'b0;
freq_cnt = 20'b0;
cnt_125ms = 6'b0;
freq_data = 20'b0;
end
parameter TIME_125ms = 24'd12499999, // 125 ms
A4 = 19'd227273, // 440 Hz
A14= 19'd214519, // 466.16Hz(A#4)
G4 = 19'd255101, // 392Hz
D4 = 19'd378787, // 264Hz
E4 = 19'd303030, // 330Hz
F4 = 19'd286532, // 349Hz
C4 = 19'd381678; // 262Hz
选择占空比为 $50%$% 的PWM方波,并根据游戏状态信号调整 $rst$信号:
assign duty_data = freq_data >> 1'b1;
always @(*) begin
if (gamemode == 2'b00) begin // 游戏开始页面
rst = 1'b0; // rst 为 0 时,声波正常产生
end
else begin
rst = 1'b1; // rst 为 1 时,声波不产生
end
end
$cnt,cnt_125ms,freq_cnt,beep$ 的调整 :
//cnt用来计时,记录125ms的时间,这里4/4拍的音乐,每 125ms 产生一个音符
always @(posedge clk or posedge rst) begin
if (rst) begin
cnt <= 24'd0;end
else if (cnt == TIME_125ms) begin // 每当 cnt 达到 TIME_125ms 即每过 1 个单位时间,该变量重置为 0 重新开始计数
cnt <= 24'd0;end
else begin
cnt <= cnt + 1'b1;end
end
// cnt_125ms 用来计数 125ms 的个数
always @(posedge clk or posedge rst) begin
if (rst) begin
cnt_125ms <= 6'd0;end
else if (cnt == TIME_125ms && cnt_125ms == 7'd64) begin // 音乐播放结束时,重置为 0 ,实现循环播放音乐
cnt_125ms <= 6'd0;end
else if (cnt == TIME_125ms) begin // 每过 1 个时间单位,变量值加 1
cnt_125ms <= cnt_125ms + 1'b1;
end
end
//调整freq_cnt
always @(posedge clk or posedge rst) begin
if (rst) begin
freq_cnt <= 19'd0;end
else if (freq_cnt >= freq_data || cnt == TIME_125ms) begin // 当频率计数信号大于此时的声音频率,或每当过 1 个板载时钟周期时,该值重置为 0
c<= 19'd0;end
else begin
freq_cnt <= freq_cnt + 1'b1;
end
end
//调整beep
always @(posedge clk or posedge rst) begin
if (rst) begin
beep <= 1'b0;
end else if (freq_cnt > duty_data) begin // 当频率计数信号大于占空比时,使 PWM 为 1,实现 50% 的占空比
beep <= 1'b1;
end else begin
beep <= 1'b0;
end
end
调整freq_data实现乐谱写入
always @(posedge clk or posedge rst) begin
if (rst) begin
freq_data <= 19'd0;
end else begin
case (cnt_125ms) // 根据不同的时间段,为该变量赋值不同的频率值
7'd0: freq_data <= D4;
7'd1: freq_data <= E4;
7'd2: freq_data <= F4;
7'd3: freq_data <= G4;
7'd4: freq_data <= A4;
7'd5: freq_data <= A4;
7'd6: freq_data <= A4;
--snip--
7'd54: freq_data <= G4;
7'd55: freq_data <= G4;
7'd56: freq_data <= G4;
7'd57: freq_data <= G4;
default: freq_data <= 19'd0;
endcase
end
end
endmodule
结束音乐和开始音乐的设计思路同理
3.2.3 beep_gaming模块的相关设计
根据音效类型设置音符持续时间
always @(*) begin
if (sound_type == 2'b10) begin
current_duration = CRASH_NOTE_DURATION;//碰撞音效持续时间
end else begin
current_duration = MOVE_NOTE_DURATION;//移动音效持续时间
end
end
检测信号变化并控制音符播放
always @(posedge clk or posedge rst) begin
if (rst) begin
sw_prev <= 1'b0;
crash_prev <= 2'b00;
sound_active <= 1'b0;
note_cnt <= 24'd0;
note_index <= 4'd0;
total_notes <= 4'd0;
sound_type <= 2'b00;
end else begin
sw_prev <= sw;
crash_prev <= crash;
// 优先检测碰撞信号
if (crash == 2'b11 && crash_prev != 2'b11) begin
// 碰撞发生,播放失落音效
sound_active <= 1'b1;
note_cnt <= 24'd0;
note_index <= 4'd0;
sound_type <= 2'b10; // 碰撞音效
total_notes <= 4'd10; // 10个音符,总共1秒
end
// 只有在没有碰撞音效播放时才响应玩家操作
else if (crash == 2'b00 && sw != sw_prev && !sound_active) begin
sound_active <= 1'b1;
note_cnt <= 24'd0;
note_index <= 4'd0;
if (sw) begin
// 上移音效
sound_type <= 2'b00;
total_notes <= 4'd4;
end else begin
// 下移音效
sound_type <= 2'b01;
total_notes <= 4'd4;
end
end
// 音符时间控制和切换
else if (sound_active) begin
if (note_cnt >= current_duration) begin
note_cnt <= 24'd0;
if (note_index >= total_notes - 1) begin
// 所有音符播放完毕
sound_active <= 1'b0;
note_index <= 4'd0;
end else begin
// 切换到下一个音符
note_index <= note_index + 1'b1;
end
end else begin
note_cnt <= note_cnt + 1'b1;
end
end
end
end
根据当前音符索引和音效类型选择不同播放频率
always @(posedge clk or posedge rst) begin
if (rst) begin
freq_data <= 20'd0;
end else if (sound_active) begin
case (sound_type)
2'b00: begin // 上移音效
case (note_index)
4'd0: freq_data <= C5; // 523Hz
4'd1: freq_data <= E5; // 659Hz
4'd2: freq_data <= G5; // 784Hz
4'd3: freq_data <= C6; // 1047Hz
default: freq_data <= C5;
endcase
end
2'b01: begin // 下移音效
case (note_index)
4'd0: freq_data <= G4; // 392Hz
4'd1: freq_data <= E4; // 330Hz
4'd2: freq_data <= C4; // 262Hz
4'd3: freq_data <= A3; // 220Hz
default: freq_data <= G4;
endcase
end
2'b10: begin // 碰撞音效
case (note_index)
4'd0: freq_data <= F4; // 349Hz
4'd1: freq_data <= D4; // 294Hz
4'd2: freq_data <= AS3; // 245Hz
4'd3: freq_data <= G3; // 196Hz
4'd4: freq_data <= F3; // 175Hz
4'd5: freq_data <= D3; // 147Hz
4'd6: freq_data <= AS2; // 122Hz
4'd7: freq_data <= G2; // 98Hz
4'd8: freq_data <= G2; // 87Hz
4'd9: freq_data <= G2; // 73Hz
default: freq_data <= F4;
endcase
end
default: freq_data <= 20'd0;
endcase
end else begin
freq_data <= 20'd0;
end
end
4 game_logic设计思路
4.1 ·参数常量设置
module game_logic(
input wire rst_n,
input wire clk, // 60Hz frame clock
input [2:0] sw,
input logic [9:0] [9:0] obstacle_x_left,
input logic [9:0] [9:0] obstacle_x_right,
input logic [9:0] [8:0] obstacle_y_up,
input logic [9:0] [8:0] obstacle_y_down,
output reg [1:0] gamemode,
output reg [8:0] player_y,
output wire [2:0] heart,
output reg [1:0] crash,
// Trail effect outputs
output reg [40:0] [9:0] trail_x,
output reg [40:0] [8:0] trail_y,
output reg [40:0] [3:0] trail_life
);
wire sw_n = ~sw[0]; // Player control switch
reg [8:0] velocity;
reg [1:0] crash;
reg velocity_direction; // 0 for up, 1 for down
// Heart system variables
reg [2:0] heart_reg;
reg [8:0] safe_time_counter; // Counter for safe time after collision
wire in_safe_time = (safe_time_counter > 0);
// Game constants
parameter UPPER_BOUND = 20;
parameter LOWER_BOUND = 460;
parameter PLAYER_SIZE = 40;
parameter PLAYER_X_LEFT = 160;
parameter PLAYER_X_RIGHT= 200;
parameter MAX_VELOCITY = 10;
parameter ACCELERATION = 1;
// Heart system constants
parameter INITIAL_HEARTS = 5;
parameter SAFE_TIME_DURATION = 60; // 1 seconds at 60Hz
// Trail constants
parameter TRAIL_COUNT = 41;
parameter TRAIL_SPAWN_X = PLAYER_X_LEFT - 8;
parameter TRAIL_HORIZONTAL_SPEED = 4;
parameter TRAIL_MAX_LIFE_CENTER = 10;
parameter TRAIL_MAX_LIFE_INNER = 8;
parameter TRAIL_MAX_LIFE_OUTER = 6;
parameter SPAWN_DELAY = 3; // 新增生成粒子的延迟参数
parameter TAIL_SIZE = 4; // 新增拖尾大小参数
// Trail generation variables
reg [2:0] trail_timer; // Timer for trail generation
reg [4:0] trail_write_index; // Index for writing new trails
reg [3:0] spawn_timer; // Timer for controlling particle spawn rate
// Boundary collision flags
wire hit_upper_bound = (player_y <= UPPER_BOUND);
wire hit_lower_bound = (player_y >= LOWER_BOUND - PLAYER_SIZE);
wire hit_boundary = hit_upper_bound || hit_lower_bound;
// Heart output assignment
assign heart = heart_reg;
// gamemode logic
always_comb begin
if (heart_reg == 0) begin
gamemode = 2'b11; // Game over when no hearts left
end else begin
gamemode = sw[2:1];
end
end
4.2 游戏模式切换
当生命值为0时切换游戏模式
// gamemode logic
always_comb begin
if (heart_reg == 0) begin
gamemode = 2'b11; // Game over when no hearts left
end else begin
gamemode = sw[2:1];
end
end
4.3 玩家状态
player速度与方向的控制,当遇到边界时,速度强制设置为0,方向不变直至出现改变方向操作。然后根据当前速度,方向以及前一刻y坐标计算出player当前y坐标。
// Enhanced velocity and direction logic with boundary handling
wire [8:0] velocity_next = (gamemode == 2'b01) ? (
// If hitting boundary and trying to move into it, set velocity to 0
(hit_upper_bound && velocity_direction == 0) ? 9'd0 :
(hit_lower_bound && velocity_direction == 1) ? 9'd0 :
// Normal velocity calculation
(sw_n == velocity_direction) ?
((velocity + ACCELERATION > MAX_VELOCITY) ? MAX_VELOCITY : velocity + ACCELERATION) :
((velocity < ACCELERATION) ? (ACCELERATION - velocity) : velocity - ACCELERATION)//设置最大阈值与速度绝对值
) : velocity;//竖直方向上做匀变速运动,ACCELERATION为常量
wire velocity_direction_next = (gamemode == 2'b01) ? (
// If hitting boundary, don't change direction unless switching control
(hit_boundary && sw_n != velocity_direction) ? ~velocity_direction :
// Normal direction logic
(sw_n == velocity_direction) ? velocity_direction :
((velocity < ACCELERATION) ? ~velocity_direction : velocity_direction)
) : velocity_direction;
// Player position logic - simplified since velocity is now properly controlled
wire [8:0] player_y_calc = velocity_direction_next ? player_y + velocity_next : player_y - velocity_next;
wire [8:0] player_y_next = (gamemode == 2'b01) ? (
(player_y_calc < UPPER_BOUND) ? UPPER_BOUND :
(player_y_calc > LOWER_BOUND - PLAYER_SIZE) ? (LOWER_BOUND - PLAYER_SIZE) :
player_y_calc
) : player_y;
4.4 粒子轨迹效果
4.4.1 游戏初始化
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
player_y <= (LOWER_BOUND + UPPER_BOUND - PLAYER_SIZE) / 2;
velocity <= 0;
crash <= 2'b00;
velocity_direction <= 0;
trail_timer <= 0;
trail_write_index <= 0;
spawn_timer <= 0;
heart_reg <= INITIAL_HEARTS;
safe_time_counter <= 0;
// Initialize trail points
for (integer i = 0; i < TRAIL_COUNT; i = i + 1) begin
trail_x[i] <= 10'd0;
trail_y[i] <= 9'd0;
trail_life[i] <= 4'd0;
end
end else if (sw[2:1] == 2'b00) begin // Reset game
player_y <= (LOWER_BOUND + UPPER_BOUND - PLAYER_SIZE) / 2;
velocity <= 0;
crash <= 2'b00;
velocity_direction <= 0;
trail_timer <= 0;
trail_write_index <= 0;
spawn_timer <= 0;
heart_reg <= INITIAL_HEARTS;
safe_time_counter <= 0;
//拖尾轨迹初始化
for (integer i = 0; i < TRAIL_COUNT; i = i + 1) begin
trail_x[i] <= 10'd0;
trail_y[i] <= 9'd0;
trail_life[i] <= 4'd0;
end
end
4.4.2更新游戏状态
无敌帧状态:为避免多次连续碰撞,需要在一次碰撞后加入无敌帧。拖尾轨迹状态:x方向上,与玩家同步移动;y方向上,与玩家相反方向移动。每一个拖尾粒子都存在独立的生命值,并且进行计数,根据不同生命值显示颜色,以此达成渐变效果。
else if (heart_reg > 0) begin // Normal game logic (only when hearts > 0)
player_y <= player_y_next;
velocity <= velocity_next;
velocity_direction <= velocity_direction_next;
// Update safe time counter(无敌帧计数)
if (safe_time_counter > 0) begin
safe_time_counter <= safe_time_counter - 1;
end
// Update existing trail points
for (integer i = 0; i < TRAIL_COUNT; i = i + 1) begin
if (trail_life[i] > 0) begin
// Move trail point horizontally (向左移动)
trail_x[i] <= trail_x[i] - TRAIL_HORIZONTAL_SPEED;
// Apply subtle vertical movement based on ACTUAL velocity and direction
if (gamemode == 2'b01 && velocity > 1 && !hit_boundary) begin
if (velocity_direction == 0) begin
// Player moving up, trail moves down slightly
trail_y[i] <= trail_y[i] + (velocity >> 2);
end else begin
// Player moving down, trail moves up slightly
if (trail_y[i] >= (velocity >> 2)) begin
trail_y[i] <= trail_y[i] - (velocity >> 2);
end else begin
trail_y[i] <= 0;
end
end
end
// Decrease life counter(拖尾粒子生命值计数)
trail_life[i] <= trail_life[i] - 1;
// Remove trail points that go off screen or die(移除无效粒子)
if (trail_x[i] < 10 || trail_life[i] == 1) begin
trail_life[i] <= 4'd0;
end
end
end
4.4.3 trail生成逻辑
初始化拖尾轨迹
if (gamemode == 2'b01) begin
trail_timer <= trail_timer + 1;
if (trail_timer >= 2) begin
trail_timer <= 0;
// 每次生成5个拖尾点
// 检查是否有足够的空间生成5个点
if (trail_write_index + 5 <= TRAIL_COUNT) begin
// 生成5个均匀分布的拖尾点
for (integer j = 0; j < 5; j = j + 1) begin
trail_x[trail_write_index + j] <= TRAIL_SPAWN_X;
// 计算均匀分布的y坐标
// 玩家方块高度为PLAYER_SIZE,分成5等份,对应5个拖尾点
trail_y[trail_write_index + j] <= player_y + (j * (PLAYER_SIZE / 4)) + TAIL_SIZE;
// 根据位置设置不同的生命值
if (j == 2) begin // 中心点
trail_life[trail_write_index + j] <= TRAIL_MAX_LIFE_CENTER;
end else if (j == 1 || j == 3) begin // 内侧点
trail_life[trail_write_index + j] <= TRAIL_MAX_LIFE_INNER;
end else begin // 外侧点
trail_life[trail_write_index + j] <= TRAIL_MAX_LIFE_OUTER;
end
end
// 更新写入索引,每次增加5
trail_write_index <= trail_write_index + 5;
end else begin
// 如果剩余空间不足5个,则重置到开头
trail_write_index <= 0;
// 生成5个均匀分布的拖尾点
for (integer j = 0; j < 5; j = j + 1) begin
trail_x[j] <= TRAIL_SPAWN_X;
// 计算均匀分布的y坐标
trail_y[j] <= player_y + (j * (PLAYER_SIZE / 4)) + TAIL_SIZE;
// 根据位置设置不同的生命值
if (j == 2) begin // 中心点
trail_life[j] <= TRAIL_MAX_LIFE_CENTER;
end else if (j == 1 || j == 3) begin // 内侧点
trail_life[j] <= TRAIL_MAX_LIFE_INNER;
end else begin // 外侧点
trail_life[j] <= TRAIL_MAX_LIFE_OUTER;
end
end
trail_write_index <= 5;
end
end
end
4.4.4 碰撞检测模块
当处于正在游戏状态(gamemode 01)并且不处于无敌帧状态下检测
// Collision detection logic - only in gamemode 01 and when not in safe time
if (gamemode == 2'b01 && !in_safe_time) begin
crash <= 2'b00; // Assume no collision initially
for (integer k = 0; k < 10; k = k + 1) begin
// AABB collision detection algorithm
if ( (PLAYER_X_RIGHT > obstacle_x_left[k]) &&
(PLAYER_X_LEFT < obstacle_x_right[k]) &&
(player_y + PLAYER_SIZE > obstacle_y_up[k]) &&
(player_y < obstacle_y_down[k]) )
begin
// Collision detected - reduce heart and start safe time
if (heart_reg > 1) begin
heart_reg <= heart_reg - 1;
safe_time_counter <= SAFE_TIME_DURATION;
end else begin
heart_reg <= 0; // Game over
end
crash <= 2'b11; // Set crash state for this frame
end
end
end
end else begin
// Game over state (heart_reg == 0), still update existing trail points but don't spawn new ones
for (integer i = 0; i < TRAIL_COUNT; i = i + 1) begin
if (trail_life[i] > 0) begin
// Continue moving existing trail points
trail_x[i] <= trail_x[i] - TRAIL_HORIZONTAL_SPEED;
trail_life[i] <= trail_life[i] - 1;
// Remove trail points that go off screen or die
if (trail_x[i] < 10 || trail_life[i] == 1) begin
trail_life[i] <= 4'd0;
end
end
end
end
end
5 map模块
该模块主要实现地图的状态的模拟,障碍物大小与位置的随机化。
5.1 参数常量设置
5.1.1 基础参数定义
module map(
input wire rst_n,
input wire clk, // Input clock (60Hz frame clock)
input wire [1:0] gamemode,
output wire [13:0] score,
output logic [9:0] [1:0] obstacle_class,
output logic [9:0] [9:0] obstacle_x_left,
output logic [9:0] [9:0] obstacle_x_right,
output logic [9:0] [8:0] obstacle_y_up,
output logic [9:0] [8:0] obstacle_y_down
);
//================================================================
// Parameters Definition
//================================================================
localparam NUM_OBSTACLES = 10;
localparam SCREEN_WIDTH = 640;
localparam UPPER_BOUND = 20;
localparam LOWER_BOUND = 460;
localparam PLAY_AREA_HEIGHT = LOWER_BOUND - UPPER_BOUND;
// Obstacle Parameters
localparam UNIT_LENGTH = 30;
localparam SCROLL_SPEED = 4;
localparam MIN_OBSTACLE_WIDTH = 20;
localparam MAX_OBSTACLE_WIDTH = 80;
localparam MIN_OBSTACLE_HEIGHT = 20;
localparam MAX_OBSTACLE_HEIGHT = 150;
localparam MIN_GAP_DIFFICULTY = 80;
localparam MAX_GAP_DIFFICULTY = 180;
localparam PLAYER_SIZE_Y = 40;
5.1.2 对障碍物相关数据进行初始化
这里重点说明40%的障碍物被选择在地图边界以保证地图难度。
// Boundary bias parameters
localparam BOUNDARY_PREFERENCE_THRESHOLD = 8'd102; // 40% probability to select boundary (102/255 ≈ 40%)
localparam UPPER_BOUNDARY_ZONE_SIZE = 60; // Upper boundary zone size
localparam LOWER_BOUNDARY_ZONE_SIZE = 60; // Lower boundary zone size
// Obstacle removal boundary - ensures obstacle is fully off screen before removal
localparam DELETE_BOUNDARY = -100; // Removal boundary, ensures obstacle is fully off screen
5.1.2 内部信号定义
//================================================================
// Internal Signal Definitions
//================================================================
reg [NUM_OBSTACLES-1:0] active;
// Use signed X position to prevent overflow
reg signed [11:0] pos_x [0:NUM_OBSTACLES-1]; // 12-bit signed X position, range -2048 to 2047
reg [8:0] pos_y [0:NUM_OBSTACLES-1];
reg [6:0] width [0:NUM_OBSTACLES-1];
reg [7:0] height [0:NUM_OBSTACLES-1];
reg signed [11:0] next_spawn_x; // Next spawn X position
reg [1:0] gamemode_prev;
// Registered outputs
reg [9:0] [9:0] obstacle_x_left_reg;
reg [9:0] [9:0] obstacle_x_right_reg;
reg [9:0] [8:0] obstacle_y_up_reg;
reg [9:0] [8:0] obstacle_y_down_reg;
// Score register
reg [13:0] score_reg; // 14 bits, enough for 0~9999
5.2 随机化系统
以下是随机数生成与分布逻辑说明
5.2.1 随机数生成机制
我们的map.sv 模块采用了多组 LFSR(线性反馈移位寄存器)和混合熵源来生成高质量的伪随机数。主要相关代码如下:
// 随机数寄存器
reg [31:0] rng1, rng2, rng3;
reg [23:0] rng4;
reg [15:0] chaos_counter;
reg [31:0] feedback_shift;
reg [7:0] noise_accumulator;
// 每帧更新
rng1 <= {rng1[30:0], rng1[31] ^ rng1[21] ^ rng1[1] ^ rng1[0]};
rng2 <= {rng2[30:0], rng2[31] ^ rng2[27] ^ rng2[5] ^ rng2[3]};
rng3 <= {rng3[30:0], rng3[31] ^ rng3[25] ^ rng3[7] ^ rng3[2]};
rng4 <= {rng4[22:0], rng4[23] ^ rng4[18] ^ rng4[12] ^ rng4[6]};
chaos_counter <= chaos_counter + ((rng1[7:0] & 8'h0F) | 8'h01);
feedback_shift <= {feedback_shift[30:0], (rng1[15] ^ rng2[7] ^ rng3[23] ^ rng4[11] ^ chaos_counter[3])};
noise_accumulator <= noise_accumulator + rng1[7:0] + rng2[15:8] + rng3[23:16] + rng4[7:0] + chaos_counter[7:0];
这些寄存器的值在每个时钟周期都会更新,保证了随机数的复杂性和不可预测性。
最终通过如下函数混合所有熵源,得到主随机数:
function automatic [31:0] get_chaos_random;
input [4:0] counter;
begin
get_chaos_random = rng1 ^ rng2 ^ rng3 ^ {rng4, rng4[7:0]} ^
feedback_shift ^ {noise_accumulator, noise_accumulator,
noise_accumulator, noise_accumulator} ^
({counter, counter, counter, counter, counter, counter, 2'b0} <<
(chaos_counter[3:0] % 16)) ^
(chaos_counter * 16'hACE1);
end
endfunction
5.2.2 障碍物Y坐标分布与区域覆盖
障碍物的Y坐标分布采用了分区和动态概率机制,确保边界和中间区域都能被覆盖,且分布均匀。
动态边界概率
根据当前障碍物生成的统计数据,动态调整边界生成概率:
// 计算当前边界障碍物比例
if (total_count > 0) begin
boundary_ratio = (boundary_count * 8'd100) / total_count;
end else begin
boundary_ratio = 8'd0;
end
// 动态调整边界概率
if (boundary_ratio < 8'd35) begin
boundary_preference = BOUNDARY_PREFERENCE_THRESHOLD + 8'd51; // 增加到约60%
end else if (boundary_ratio > 8'd50) begin
boundary_preference = BOUNDARY_PREFERENCE_THRESHOLD - 8'd25; // 降低到约30%
end else begin
boundary_preference = BOUNDARY_PREFERENCE_THRESHOLD; // 默认40%
end
// 随机决定是否生成在边界
use_boundary_generation = (chaos_rng[7:0] < boundary_preference);
区域强制覆盖机制
将Y轴分为8个分区,每次生成障碍物时,记录其覆盖的分区:
selected_zone = ((new_y_pos - UPPER_BOUND) * 8) / (LOWER_BOUND - UPPER_BOUND - new_height);
if (selected_zone <= 7) begin
force_coverage_map[selected_zone] <= 1'b1;
end
每16次生成(counter[3:0]==4’b1111)时,如果最上或最下分区未被覆盖,则强制生成边界障碍物:
if (counter[3:0] == 4'b1111) begin
if (!coverage_map[0] || !coverage_map[7]) begin
use_boundary_generation = 1'b1;
end
end
Y坐标分布代码片段
障碍物Y坐标的最终分布逻辑如下:
if (use_boundary_generation) begin
// 边界区
use_upper_boundary = chaos_rng[8];
if (use_upper_boundary) begin
// 上边界
boundary_offset = (chaos_rng[23:16] ^ noise_accumulator) % UPPER_BOUNDARY_ZONE_SIZE;
result_y = UPPER_BOUND + boundary_offset;
end else begin
// 下边界
boundary_offset = (chaos_rng[15:8] ^ noise_accumulator) % LOWER_BOUNDARY_ZONE_SIZE;
result_y = max_y_pos - boundary_offset;
if (result_y < UPPER_BOUND) result_y = UPPER_BOUND;
end
end else begin
// 中间区
middle_area_start = UPPER_BOUND + UPPER_BOUNDARY_ZONE_SIZE;
middle_area_end = max_y_pos - LOWER_BOUNDARY_ZONE_SIZE;
if (middle_area_end > middle_area_start) begin
boundary_offset = (chaos_rng[31:24] ^ chaos_rng[15:8] ^ noise_accumulator) %
(middle_area_end - middle_area_start);
result_y = middle_area_start + boundary_offset;
end else begin
boundary_offset = (chaos_rng[23:16] ^ noise_accumulator) % (max_y_pos - UPPER_BOUND);
result_y = UPPER_BOUND + boundary_offset;
end
end
5.2.3 总结
- 通过多级LFSR和混合熵源生成高质量的随机数,保证障碍物参数的不可预测性。
- 通过动态概率和分区强制覆盖机制,确保Y轴所有区域(尤其是边界)都能被障碍物覆盖,避免出现长时间无障碍物的“死区”。
- 这种设计既保证了游戏的随机性和挑战性,又保证了游戏体验的公平和完整性。
5.3 主状态与障碍物逻辑
//================================================================
// Main state machine and obstacle logic
//================================================================
// Disappeared obstacle counter
reg [3:0] disappear_count;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
next_spawn_x <= SCREEN_WIDTH + MIN_GAP_DIFFICULTY;
gamemode_prev <= 2'b00;
boundary_generation_count <= 8'b0;
total_generation_count <= 8'b0;
score_reg <= 14'd0;
disappear_count <= 4'd0; // 初始化计数器
for (integer i = 0; i < NUM_OBSTACLES; i++) begin
active[i] <= 1'b0;
pos_x[i] <= SCREEN_WIDTH + 100;
pos_y[i] <= UPPER_BOUND;
width[i] <= MIN_OBSTACLE_WIDTH;
height[i] <= MIN_OBSTACLE_HEIGHT;
end
end else begin
gamemode_prev <= gamemode;
if (gamemode == 2'b00) begin
// Reset state in idle mode
for (integer i = 0; i < NUM_OBSTACLES; i++) begin
active[i] <= 1'b0;
pos_x[i] <= SCREEN_WIDTH + 100;
end
next_spawn_x <= SCREEN_WIDTH + MIN_GAP_DIFFICULTY;
force_coverage_map <= 8'b0;
boundary_generation_count <= 8'b0;
total_generation_count <= 8'b0;
score_reg <= 14'd0;
disappear_count <= 4'd0;
end
else if (gamemode == 2'b01) begin
if (gamemode_prev == 2'b00) begin
for (integer i = 0; i < NUM_OBSTACLES; i++) begin
active[i] <= 1'b0;
pos_x[i] <= SCREEN_WIDTH + 100;
end
next_spawn_x <= SCREEN_WIDTH + MIN_GAP_DIFFICULTY;
force_coverage_map <= 8'b0;
boundary_generation_count <= 8'b0;
total_generation_count <= 8'b0;
score_reg <= 14'd0;
disappear_count <= 4'd0;
end
// Move all active obstacles
for (integer i = 0; i < NUM_OBSTACLES; i++) begin
if (active[i]) begin
pos_x[i] <= pos_x[i] - SCROLL_SPEED;
end
end
next_spawn_x <= next_spawn_x - SCROLL_SPEED;
// 重置消失计数器
disappear_count <= 4'd0;
// Remove obstacles that are off screen and count them
for (integer i = 0; i < NUM_OBSTACLES; i++) begin
if (active[i] && (pos_x[i] + $signed({5'b0, width[i]}) < DELETE_BOUNDARY)) begin
active[i] <= 1'b0;
disappear_count <= disappear_count + 1'b1;
end
end
// Score accumulation in next clock cycle (will be handled by the register update)
if (score_reg + disappear_count > 14'd9999)
score_reg <= 14'd9999;
else
score_reg <= score_reg + disappear_count;
5.3.2 生成障碍物
随机化生成障碍物后需判断是否为边界障碍物并更新数据与防止溢出。
if (next_spawn_x <= SCREEN_WIDTH) begin
for (integer i = 0; i < NUM_OBSTACLES; i++) begin
if (!active[i]) begin
reg [31:0] chaos_random;
reg [7:0] new_width, new_height;
reg [8:0] new_y_pos;
reg [7:0] gap_size;
reg [2:0] selected_zone;
reg is_boundary_obstacle;
// Generate random numbers
chaos_random = get_chaos_random(coverage_counter);
new_width = get_random_width(chaos_random);
new_height = get_random_height(chaos_random);
// Use enhanced boundary Y algorithm
new_y_pos = get_enhanced_boundary_y(chaos_random, new_height,
coverage_counter, last_zone, force_coverage_map,
boundary_generation_count, total_generation_count);
gap_size = get_random_gap(chaos_random);
// Check if this is a boundary obstacle
is_boundary_obstacle = (new_y_pos <= (UPPER_BOUND + UPPER_BOUNDARY_ZONE_SIZE)) ||
(new_y_pos >= (LOWER_BOUND - new_height - LOWER_BOUNDARY_ZONE_SIZE));
// Update statistics
total_generation_count <= total_generation_count + 1;
if (is_boundary_obstacle) begin
boundary_generation_count <= boundary_generation_count + 1;
end
// Prevent overflow
if (total_generation_count == 8'hFF) begin
total_generation_count <= 8'd100;
boundary_generation_count <= (boundary_generation_count > 8'd100) ?
8'd40 : (boundary_generation_count * 8'd100) / 8'hFF;
end
5.3.3 地图状态更新
// Update coverage map
selected_zone = ((new_y_pos - UPPER_BOUND) * 8) / (LOWER_BOUND - UPPER_BOUND - new_height);
if (selected_zone <= 7) begin
force_coverage_map[selected_zone] <= 1'b1;
end
last_zone <= selected_zone;
// Every 32 obstacles, reset coverage map
if (coverage_counter == 5'b11111) begin
force_coverage_map <= 8'b0;
end
active[i] <= 1'b1;
pos_x[i] <= SCREEN_WIDTH;
pos_y[i] <= new_y_pos;
width[i] <= new_width[6:0];
height[i] <= new_height;
next_spawn_x <= SCREEN_WIDTH + gap_size;
break;
end
end
end
end
end
end
5.4 障碍物输出逻辑
//================================================================
// Output logic
//================================================================
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (integer k = 0; k < NUM_OBSTACLES; k++) begin
obstacle_x_left_reg[k] <= 10'd700;
obstacle_x_right_reg[k] <= 10'd700;
obstacle_y_up_reg[k] <= 9'd500;
obstacle_y_down_reg[k] <= 9'd500;
end
end else begin
for (integer k = 0; k < NUM_OBSTACLES; k++) begin
// Only output obstacles that are active and within screen
if (active[k] && pos_x[k] >= 0 && pos_x[k] < SCREEN_WIDTH) begin
obstacle_x_left_reg[k] <= 10'(pos_x[k]);
obstacle_x_right_reg[k] <= 10'(pos_x[k] + $signed({5'b0, width[k]}));
obstacle_y_up_reg[k] <= pos_y[k];
obstacle_y_down_reg[k] <= 9'(pos_y[k] + height[k]);
end else begin
obstacle_x_left_reg[k] <= 10'd700;
obstacle_x_right_reg[k] <= 10'd700;
obstacle_y_up_reg[k] <= 9'd500;
obstacle_y_down_reg[k] <= 9'd500;
end
end
end
end
assign obstacle_x_left = obstacle_x_left_reg;
assign obstacle_x_right = obstacle_x_right_reg;
assign obstacle_y_up = obstacle_y_up_reg;
assign obstacle_y_down = obstacle_y_down_reg;
// Output score
assign score = score_reg;
assign obstacle_class[0] = 2'b00; // Example class based on Y position
assign obstacle_class[1] = 2'b10;
assign obstacle_class[2] = 2'b01;
assign obstacle_class[3] = 2'b11;
assign obstacle_class[4] = 2'b10;
assign obstacle_class[5] = 2'b00;
assign obstacle_class[6] = 2'b01;
assign obstacle_class[7] = 2'b11;
assign obstacle_class[8] = 2'b10;
assign obstacle_class[9] = 2'b01;
6 top模块
我们在top模块中增加了抗锯齿功能,并使其支持拖尾效果。
module top(
input wire clk, // Main input clock (e.g., 100MHz)
input wire RST_n, // On-board reset button (active-low)
input wire [2:0] sw, // Switches for game control
output wire [3:0] R, // VGA Red output
output wire [3:0] G, // VGA Green output
output wire [3:0] B, // VGA Blue output
output wire HS, // VGA Horizontal Sync
output wire VS, // VGA Vertical Sync
output wire beep,
output wire [3:0] AN,
output wire [7:0] SEGMENT,
output wire [1:0] gamemode_led
);
6.1 内部信号初始化
// --- Internal Signals ---
wire rst_n_debounced; // Debounced active-low reset signal
wire clk_25mhz; // 25MHz clock for VGA pixel timing
wire clk_60hz; // 60Hz clock for game logic timing
wire score_rst; // Reset signal for score display
wire [13:0] score;
wire [3:0] bcd3, bcd2, bcd1, bcd0; // BCD outputs for score display
wire [1:0] gamemode;
wire [8:0] player_y;
wire [2:0] heart_game; // 游戏逻辑时钟域的心脏数量
wire [1:0] crash;
// Trail effect signals from game logic
wire [40:0] [9:0] trail_x_game;
wire [40:0] [8:0] trail_y_game;
wire [40:0] [3:0] trail_life_game;
// 游戏逻辑时钟域的障碍物数据
logic [9:0] [1:0] obstacle_class; // 障碍物类别(双缓冲)
logic [9:0] [9:0] obstacle_x_game_left;
logic [9:0] [9:0] obstacle_x_game_right;
logic [9:0] [8:0] obstacle_y_game_up;
logic [9:0] [8:0] obstacle_y_game_down;
// VGA时钟域的障碍物数据(双缓冲)
logic [9:0] [9:0] obstacle_x_left_vga;
logic [9:0] [9:0] obstacle_x_right_vga;
logic [9:0] [8:0] obstacle_y_up_vga;
logic [9:0] [8:0] obstacle_y_down_vga;
logic [8:0] player_y_vga;
logic [1:0] gamemode_vga;
logic [2:0] heart_vga; // VGA时钟域的心脏数量(双缓冲)
// VGA时钟域的拖尾数据(双缓冲)
logic [40:0] [9:0] trail_x_vga;
logic [40:0] [8:0] trail_y_vga;
logic [40:0] [3:0] trail_life_vga;
// VGA signals
wire [9:0] pix_x;
wire [8:0] pix_y;
wire [11:0] vga_data_out; // 12-bit color data from screen generator
// --- Debouncer ---
assign rst_n_debounced = RST_n;
// --- Clock Generation ---
// Generate 25MHz clock for VGA from main clock (assuming 100MHz input)
reg [1:0] clk_div_25m;
always_ff @(posedge clk or negedge rst_n_debounced) begin
if (!rst_n_debounced) clk_div_25m <= 2'b0;
else clk_div_25m <= clk_div_25m + 1;
end
assign clk_25mhz = clk_div_25m[1];
// Generate 60Hz clock for game logic
clkdiv_60hz u_clkdiv_60hz(.clk(clk), .rst_n(rst_n_debounced), .clk_60hz(clk_60hz));
6.2 抗锯齿效果实现
// --- 关键修复:时钟域同步器(增加拖尾数据同步)---
// 将游戏逻辑数据同步到VGA时钟域,避免锯齿问题
always_ff @(posedge clk_25mhz or negedge rst_n_debounced) begin
if (!rst_n_debounced) begin
// 复位时初始化障碍物数据
for (integer i = 0; i < 10; i++) begin
obstacle_x_left_vga[i] <= 10'd700;
obstacle_x_right_vga[i] <= 10'd700;
obstacle_y_up_vga[i] <= 9'd500;
obstacle_y_down_vga[i] <= 9'd500;
end
player_y_vga <= 9'd240;
gamemode_vga <= 2'b00;
heart_vga <= 3'd5; // 初始化心脏数量
// 复位时初始化拖尾数据
for (integer i = 0; i < 41; i++) begin
trail_x_vga[i] <= 10'd0;
trail_y_vga[i] <= 9'd0;
trail_life_vga[i] <= 4'd0;
end
end else begin
// 在垂直同步信号(VS)有效时更新显示数据
// 这样可以确保VGA在绘制下一帧时使用一套完整且稳定的数据
if (!VS) begin // 在垂直消隐期间更新数据
// 同步障碍物和玩家数据
obstacle_x_left_vga <= obstacle_x_game_left;
obstacle_x_right_vga <= obstacle_x_game_right;
obstacle_y_up_vga <= obstacle_y_game_up;
obstacle_y_down_vga <= obstacle_y_game_down;
player_y_vga <= player_y;
gamemode_vga <= gamemode;
heart_vga <= heart_game; // 同步心脏数量
// 同步拖尾数据
trail_x_vga <= trail_x_game;
trail_y_vga <= trail_y_game;
trail_life_vga <= trail_life_game;
end
// 否则,保持当前帧的数据不变
end
end
6.3 各类模块汇总
包含game_logic,map,VGA,beep等模块。
// --- Game Logic Module (Enhanced with Trail Effect and Heart System) ---
game_logic u_game_logic (
.rst_n(rst_n_debounced),
.sw(sw),
.clk(clk_60hz), // 使用60Hz时钟
.obstacle_x_left(obstacle_x_game_left),
.obstacle_x_right(obstacle_x_game_right),
.obstacle_y_up(obstacle_y_game_up),
.obstacle_y_down(obstacle_y_game_down),
.gamemode(gamemode),
.player_y(player_y),
.heart(heart_game), // 连接心脏数量输出
.crash(crash),
// Trail effect outputs
.trail_x(trail_x_game),
.trail_y(trail_y_game),
.trail_life(trail_life_game)
);
// --- Map Generation Module ---
map u_map (
.rst_n(rst_n_debounced),
.clk(clk_60hz), // 使用60Hz时钟
.gamemode(gamemode),
.score(score),
.obstacle_class(obstacle_class), // 传递障碍物类别
.obstacle_x_left(obstacle_x_game_left),
.obstacle_x_right(obstacle_x_game_right),
.obstacle_y_up(obstacle_y_game_up),
.obstacle_y_down(obstacle_y_game_down)
);
// --- VGA Screen Picture Generator (Enhanced with Trail Effect) ---
vga_screen_pic u_vga_screen_pic(
.pix_x(pix_x),
.pix_y(pix_y),
.clk(clk),
.gamemode(gamemode_vga), // 使用VGA时钟域的同步数据
.player_y(player_y_vga), // 使用VGA时钟域的同步数据
.heart(heart_vga), // 传递心脏数量给VGA显示模块
.obstacle_class(obstacle_class), // 传递障碍物类别
.obstacle_x_game_left(obstacle_x_left_vga),
.obstacle_x_game_right(obstacle_x_right_vga),
.obstacle_y_game_up(obstacle_y_up_vga),
.obstacle_y_game_down(obstacle_y_down_vga),
// Trail effect inputs
.trail_x(trail_x_vga),
.trail_y(trail_y_vga),
.trail_life(trail_life_vga),
.rgb(vga_data_out)
);
// --- VGA Controller ---
vga_ctrl u_vga_ctrl(
.clk(clk_25mhz),
.rst(~rst_n_debounced), // vga_ctrl often uses an active-high reset
.Din(vga_data_out),
.row(pix_y),
.col(pix_x),
.R(R),
.G(G),
.B(B),
.HS(HS),
.VS(VS)
);
// --- Other Peripherals ---
assign gamemode_led = score[1:0];
assign heart = heart_vga; // 输出心脏数量(使用VGA时钟域同步后的数据)
assign score_rst = (gamemode == 2'b00); // Reset score when in initial state
BinToBCD bcd_instance (
.bin(score),
.bcd3(bcd3),
.bcd2(bcd2),
.bcd1(bcd1),
.bcd0(bcd0)
);
DisplayNumber d1(.clk(clk), .RST(score_rst), .Hexs({bcd3, bcd2, bcd1, bcd0}),
.Points(4'b0000), .LES(4'b0000), .Segment(SEGMENT), .AN(AN));
top_beep u_top_beep(
.clk(clk),
.gamemode(gamemode),
.sw(sw[0]),
.crash(crash),
.beep(beep)
);
7 仿真与调试过程分析
我们主要通过下板验证我们的代码,这里主要讲述VGA模块的仿真实现。在使用vga显示时,常常要利用一个模块生成rgb的值,传入vga,以下将介绍如何对生成rgb的模块进行仿真,检验rgb生成的逻辑。
7.1 仿真流程
我们的目标是在不实际上板的情况下,通过仿真验证 vga_screen_pic 模块生成的画面是否正确。核心思路是“扫描”屏幕上的每一个像素点,记录其颜色值,最后将这些颜色值利用python组合成一张图片。
7.1.1 仿真代码
//实例化和定义接口省略
integer f;
initial f = $fopen("screen_pixels.txt", "w");
initial begin
//初始化参数省略
for (pix_y = 0; pix_y < 480; pix_y = pix_y + 1) begin
for (pix_x = 0; pix_x < 640; pix_x = pix_x + 1) begin
#1;
$fwrite(f, "%d %d %h %h %h\n", pix_y, pix_x, rgb[11:8], rgb[7:4], rgb[3:0]); //注意这里的rgb的高低位是rgb还是bgr
end
end
$fclose(f);
$stop;
end
endmodule
7.1.2 修改路径
将上面的$fopen("screen_pixels.txt", "w");的screen_pixels.txt改成改成本地文件夹下文件的一个绝对路径,如C:/Users/simu/screen_pixels.txt,实测发现不需要有原来的文件,仿真时会新建。(注意这里如果windows的路径是\,请使用/)
7.1.3 vivado仿真
使用vivado进行仿真,注意下面示例界面的继续的按钮,如果仿真没有完成(弹到仿真文件的$stop,请点击“继续仿真”)

7.1.4 使用python生成图片
在txt文件对应的文件夹下,让ai创建一个python文件,用于生成图片。
#配置python环境
pip install pillow #也可使用conda
from PIL import Image
WIDTH, HEIGHT = 640, 480
img = Image.new("RGB", (WIDTH, HEIGHT), "black")
pixels = img.load()
with open("screen_pixels.txt") as f:
for line in f:
row, col, r, g, b = line.strip().split() #注意这里的rgb还是bgr
row = int(row)
col = int(col)
r = int(r, 16) * 17
g = int(g, 16) * 17
b = int(b, 16) * 17
if 0 <= col < WIDTH and 0 <= row < HEIGHT:
pixels[col, row] = (r, g, b)
img.save("screen_output.png")
print("图片已保存为 screen_output.png")
7.1.5 代码运行
运行python代码,查看结果,下面是一个示例结果
![]()
7.2 具体思路
- 编写Testbench:创建一个专门用于测试
vga_screen_pic的仿真文件(例如tb_vga.sv)。 - 模拟像素扫描: 在Testbench中,使用嵌套循环遍历所有像素坐标,即
pix_y从 0 到 479,pix_x从 0 到 639。 - 提供输入: 为
vga_screen_pic模块提供必要的输入,如gamemode,player_y等,以模拟特定的游戏场景。 - 记录像素颜色: 对于每一个像素坐标 (
pix_x,pix_y),Testbench会记录下vga_screen_pic模块输出的rgb颜色值。 - 导出数据到文件: 使用Verilog的系统任务 (
$fopen,$fwrite,$fclose),将每个像素的坐标和颜色值写入一个文本文件(例如screen_pixels.txt)。文件格式通常为:行坐标 列坐标 B G R。(注意vga_screen_pic.sv的写入的是bgr还是rgb,需与python代码对应) - 运行仿真: 在Vivado等仿真工具中运行此Testbench。(注意如果默认仿真时间不足,请点击继续仿真,直到结束,这里结束时会跳转到仿真代码的
$finish)仿真结束后,你将得到路径里的screen_pixels.txt文件。 - 生成图片: 使用一个简单的Python脚本(需安装Pillow库),读取
screen_pixels.txt文件。python脚本会创建一个640x480的空白图片,并根据文件中的数据填充每一个像素的颜色。 - 验证结果: 查看生成的图片(例如
screen_output.png),即可直观地判断你的vga_screen_pic模块逻辑是否正确
8 下板检验
开始界面:

游戏过程:

结束界面:

分数显示:

9 Debug与实验心得
9.1数组问题
我们最初使用了一个高达200位的数据存储并在模块间传递障碍物的x坐标,共10个障碍物,每个障碍物20位。但是在下板过程中发现结果不对,推测极有可能是这个过于宽的数据导致了下板失败。
解决方法
我们决定采用数组来存这个10*20的信号。但是Verilog并不支持在模块接口中使用数组,所以我们采用了SystemVerilog。
$SystemVerilog$简介
SystemVerilog 是 Verilog 的超集,完全兼容 Verilog 的语法,并在此基础上扩展了许多新特性。
- Verilog 兼容性:所有 Verilog 代码都可以直接在 SystemVerilog 中使用。
- 数组支持:SystemVerilog 增强了对数组的支持,允许在模块端口、参数和变量中直接使用多维数组,极大地方便了复杂数据结构的表达和传递。
- always_ff 和 always_comb:引入了
always_ff(专用于时序逻辑)和always_comb(专用于组合逻辑)块,语义更清晰,能帮助避免常见的综合和仿真陷阱。 - logic 变量:新增
logic类型,既可用于综合也可用于仿真,避免了reg和wire的混淆,适合大多数信号声明。 - 混用 .sv 和 .v 文件:在同一个工程中,可以同时使用
.sv(SystemVerilog)和.v(Verilog)文件,便于逐步迁移和兼容旧代码。
9.2锯齿问题
我们最初下板时,障碍物出现了锯齿问题。

这是因为我们的障碍物(图中蓝色)在向右移动,它的坐标值以wire形式输出,以60Hz的频率改变的。VGA扫描信号扫到障碍物下方时,障碍物的坐标发生了变化,导致障碍物上下呈现出““脱节””的状态。
解决方法
在top文件中,障碍物的坐标不要直接以wire形式接入vga相关模块,而是在垂直消隐时存入reg,并且把这个reg接入vga相关模块。
module top(
//...
);
// 游戏逻辑时钟域的障碍物数据
// 这个变量用于接受某个模块的output
logic [9:0] [9:0] obstacle_x_game_left;
// VGA时钟域的障碍物数据
// 这个用于vga相关模块的input
logic [9:0] [9:0] obstacle_x_left_vga;
// --- 关键:时钟域同步器 ---
// 将游戏逻辑数据同步到VGA时钟域,避免锯齿问题
always_ff @(posedge clk_25mhz or negedge rst_n_debounced) begin
if(reset) begin
//...
end else begin
// 在垂直同步信号(VS)有效时更新显示数据
// 这样可以确保VGA在绘制下一帧时使用一套完整且稳定的数据
if (!VS) begin // 在垂直消隐期间更新数据
obstacle_x_left_vga <= obstacle_x_game_left;
//...
end
end
end
//...
endmodule
9.3 ROM地址生成
rom地址生成需要根据相对位置进行生成,开始使用的是绝对坐标计算得到rom的地址,出现了图像滚动/图像rgb不定态的问题。
解决办法:使用正负判断以及确定基准点计算相对坐标。
//正负(即超出rom显示图片边界判断),相对位置
pic_romaddrOver = (pix_x >= GAMEOVER_X && pix_x < GAMEOVER_X + H_PIC &&
pix_y >= GAMEOVER_Y && pix_y < GAMEOVER_Y + H_PIC) ?
(pix_x - GAMEOVER_X) + (pix_y - GAMEOVER_Y) * H_PIC : 0; // Default to 0 if out bounds
9.4 state的引入
vga要使用rom加载的图像类型很多,一开始使用的是直接判断生成rgb的逻辑,这样使得代码复杂,并且拓展性不强。
解决办法:这里引入了state变量,先根据gamemode以及障碍物等参数给state赋值,借助state对rgb进行赋值。后续利用state确实具有很强的拓展性。
9.5 debug不定态
Q:我遇到了txt文件里的rgb是x? A: 再次进行仿真,在vivado的scope加入仿真模块,查看对应坐标的不定态原因,可能是一个一个input的不定态导致的输出不定态
Tip:vivado波形界面改变pix_x或者pix_y是radix使用unsigned integer可以查看十进制坐标

参考资料:https://wintermelonc.github.io/WintermelonC_Docs/zju/basic_courses/digital_logic_design/lab/final/