惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
aimingoo的专栏
aimingoo的专栏
爱范儿
爱范儿
D
Docker
I
InfoQ
Microsoft Security Blog
Microsoft Security Blog
G
Google Developers Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Vercel News
Vercel News
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
Tailwind CSS Blog
D
DataBreaches.Net
月光博客
月光博客
N
Netflix TechBlog - Medium
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
V
Visual Studio Blog
MyScale Blog
MyScale Blog
B
Blog
阮一峰的网络日志
阮一峰的网络日志
L
LangChain Blog
Recent Announcements
Recent Announcements
Microsoft Azure Blog
Microsoft Azure Blog
WordPress大学
WordPress大学

h-jr.me

Tinymist 的 Typst 无用声明检测 一路顺风 经验贴|末九17%上岸南大计院 Frama-C WP Tutorial | 函数契约 SOP 和 POS Verilog 基础语法 Wasm 二进制结构 我和 Typst 的 2024 计算机网络 TCP 大实验 2024 年冬天吃的第一个亏从换电脑开始 我学编程原神 Rust 记三天数模国赛 C 编译体系
Verilog 打砖块游戏
2025-07-05 · via h-jr.me

Verilog 打砖块游戏

2025-07-05 · 2 分钟 · hongjr03

以 Breakout 为例分析 FPGA 设计

代码来源:https://www.fpga4fun.com/BreakoutGame.html


项目结构

从网站上下载下来的代码很简洁,只有四个文件:

.

├── breakout_playfield.v

├── breakout_videogen.v

├── breakout.ucf

└── breakout.v

1 directory, 4 files

其中:

  • breakout.v: 硬件接口层(VGA、LED、音频)
  • breakout_videogen.v: 显示控制层(时序、渲染)
  • breakout_playfield.v: 游戏逻辑层(碰撞、状态)

breakout.ucf 则是约束文件,它负责引脚映射。

breakout.v

breakout.v 开始。

// (c) KNJN LLC 2024 for fpga4fun.com

////////////////////////////////////////////////////////////////////////

module breakout(

input clk,

output VGA_HS, VGA_VS, VGA_R, VGA_G, VGA_B,

output [1:0] LED,

output audioR, audioL

);

wire [9:0] PaddleX; // paddle position

// if you have a way to control the paddle, make sure to update PaddleX here

assign PaddleX = 10'd900; // othersise this line puts the paddle off-screen to run the game in demo mode

wire DrawArea, hSync, vSync, red, green, blue, Collision, BrickHit;

breakout_videogen myVideoGen(

.clk(clk),

.PaddleX(PaddleX),

.DrawArea(DrawArea), .hSync(hSync), .vSync(vSync), .red(red), .green(green), .blue(blue),

.Collision(Collision), .BrickHit(BrickHit)

);

assign VGA_R = DrawArea & red;

assign VGA_G = DrawArea & green;

assign VGA_B = DrawArea & blue;

assign VGA_HS = ~hSync;

assign VGA_VS = ~vSync;

assign LED = {BrickHit, Collision & ~BrickHit};

reg [15:0] audio; always @(posedge clk) audio <= audio + Collision + BrickHit;

assign audioR = audio[15];

assign audioL = audio[15];

endmodule

开头先是连接 VGA 输出信号(RGB、同步信号),然后处理球拍位置。虽然但是,这个球拍在这个程序里是一个写死的常量,如果有控制球拍的逻辑还要另外跟它信号连上。

然后和 breakout_videogen.v 有关的代码,和传视频信号有关的,放到后面再看。

breakout_videogen.v

// (c) KNJN LLC 2024 for fpga4fun.com

////////////////////////////////////////////////////////////////////////

module breakout_videogen(

input clk,

input [9:0] PaddleX,

output reg DrawArea, hSync, vSync,

output red, green, blue,

output reg Collision, BrickHit

);

//localparam ballspeed = 2; // ball moves 4 pixels per frame

localparam ballspeed = 3; // ball moves 8 pixels per frame

reg [9:0] ballX = 100; // initial ball position

reg [8:0] ballY = 300;

reg ball_dirX, ball_dirY;

////////////////////////////////////////////////////////////////////////

parameter hDrawArea = 640;

parameter hSyncPorch = 16;

parameter hSyncLen = 96;

parameter hFrameSize = 800;

parameter vDrawArea = 480;

parameter vSyncPorch = 10;

parameter vSyncLen = 2;

parameter vFrameSize = 525;

reg [9:0] CounterX;

reg [8:0] CounterY;

always @(posedge clk) CounterX <= (CounterX==hFrameSize-1) ? 10'd0 : CounterX+10'd1;

always @(posedge clk) if(CounterX==hFrameSize-1) CounterY <= (CounterY==vFrameSize-1) ? 9'd0 : CounterY+9'd1;

always @(posedge clk) DrawArea <= (CounterX<hDrawArea) & (CounterY<vDrawArea);

always @(posedge clk) hSync <= (CounterX>=hDrawArea+hSyncPorch) & (CounterX<hDrawArea+hSyncPorch+hSyncLen);

always @(posedge clk) vSync <= (CounterY>=vDrawArea+vSyncPorch) & (CounterY<vDrawArea+vSyncPorch+vSyncLen);

////////////////////////////////////////////////////////////////////////

wire DrawBall, DrawBorder, DrawPaddle, DrawBrick, BrickHit_now, BrickHit_acq;

reg RestoreBrickwall = 1'b1;

reg MoveBall;

breakout_playfield #(hDrawArea, vDrawArea) game(

.clk(clk),

.PaddleX(PaddleX),

.CounterX(MoveBall ? ballX + {6'h00, {4{CounterX[0]}}} : CounterX),

.CounterY(MoveBall ? ballY + {5'h00, {4{CounterX[1]}}} : CounterY),

.ballX(ballX),

.ballY(ballY),

.DrawBall(DrawBall), .DrawBorder(DrawBorder), .DrawPaddle(DrawPaddle), .DrawBrick(DrawBrick),

.BrickHit_now(BrickHit_now), .BrickHit_acq(BrickHit_acq), .RestoreBrickwall(RestoreBrickwall)

);

// we are going to update the ball position during offscreen timing

wire FrameTick = (CounterX==hFrameSize-1) & (CounterY==vDrawArea-1);

always @(posedge clk) MoveBall <= MoveBall ? ~&CounterX[ballspeed+2:0] : FrameTick;

wire BounceableOject = DrawBorder | DrawPaddle | DrawBrick;

reg [3:0] HBC; always @(posedge clk) HBC <= {BounceableOject, HBC[3:1]}; // record the ball corners hits in HBC (HotBallCorner)

wire [15:0] updateDirX = 16'b01101101_10110110; // and update the ball direction if needed

wire [15:0] updateDirY = 16'b01111001_10011110;

always @(posedge clk) if(MoveBall & CounterX[2:0]==3'h5 & updateDirX[HBC]) ball_dirX <= (~HBC[0] & HBC[1]) | (~HBC[2] & HBC[3]);

always @(posedge clk) if(MoveBall & CounterX[2:0]==3'h5 & updateDirY[HBC]) ball_dirY <= (~HBC[0] & ~HBC[1]) | ( HBC[2] & HBC[3]);

always @(posedge clk) if(MoveBall & CounterX[2:0]==3'h7) ballX <= ballX + {{9{ball_dirX}}, 1'b1}; // and then the ball position

always @(posedge clk) if(MoveBall & CounterX[2:0]==3'h7) ballY <= ballY + {{8{ball_dirY}}, 1'b1};

// then get stats on ball collisions and brick hits

reg [2:0] BHA; always @(posedge clk) BHA <= {DrawBrick, BHA[2:1]};

assign BrickHit_now = MoveBall & CounterX[2] & BHA[0];

always @(posedge clk) if(FrameTick) BrickHit<=1'b0; else if(BrickHit_now) BrickHit<=1'b1;

reg [7:0] BrickHit_count=0; always @(posedge clk) BrickHit_count <= RestoreBrickwall ? 8'h00 : BrickHit_count + BrickHit_acq;

always @(posedge clk) RestoreBrickwall <= RestoreBrickwall ? ~FrameTick : (BrickHit_count==19*7) & ballY[8];

always @(posedge clk) if(FrameTick) Collision<=1'b0; else if(MoveBall & CounterX[2] & HBC[1]) Collision<=1'b1;

wire DrawAll = DrawBall | DrawBorder | DrawPaddle | DrawBrick;

assign red = DrawBrick;

assign green = DrawAll;

assign blue = DrawAll;

endmodule

查了一下,breakout_videogen.v 实现标准 VGA 640x480@60Hz 时序,即以下规范:

  • 水平时序:
    • 可见区域:640 像素
    • 前肩(Front Porch):16 像素
    • 同步脉冲:96 像素
    • 后肩(Back Porch):48 像素
    • 总计:800 像素
  • 垂直时序:
    • 可见区域:480 行
    • 前肩:10 行
    • 同步脉冲:2 行
    • 后肩:33 行
    • 总计:525 行

因为老 CRT 显示器是用电子束从左到右、从上到下逐行扫描的,扫描完一行需要时间回到下一行开始需要返回时间,所以需要同步脉冲来调整时序。好吧这个不重要。

接着通过两个计数器生成像素坐标:

  • CounterX: 水平像素计数(0-799),对应寄存器长度是 210−12^10-1,大于 800800
  • CounterY: 垂直行计数(0-524),对应寄存器长度是 29−12^9-1,实际上不够存,不知道这里为什么这么设计;
  • DrawArea: 标识当前像素是否在可显示区域内。

总计 800×525=420,000800×525 = 420,000 个时钟周期完成一帧。

球的运动用 X 和 Y 两个分解方向表示,运动状态机由 MoveBall 控制。MoveBall 的逻辑是:当为真时检查~&CounterX[ballspeed+2:0](不全为 1 时继续),当为假时等待 FrameTick 重新启动,这里 +2 是控制球移动频率。也就是说:

  • 当 MoveBall=0 时,等待 FrameTick(帧结束)信号来启动移动
  • 当 MoveBall=1 时,检查 ~&CounterX[ballspeed+2:0],当 CounterX 的低 (ballspeed+3) 位不全为 1 时继续移动。ballspeed=3 时,检查 CounterX[5:0],意味着每 64 个时钟周期球移动一次。

碰撞检测通过 HBC(Hot Ball Corner)机制实现,HBC <= {BounceableOject, HBC[3:1]}进行右移操作,记录连续 4 个时钟周期的碰撞历史状态。系统使用两个 16 位查找表updateDirXupdateDirY,根据 HBC 的 4 位值索引决定是否更新球的方向,在时钟周期 5 更新方向,时钟周期 7 更新位置,避免了复杂的碰撞计算。

砖块碰撞通过 BHA 记录砖块碰撞历史,BrickHit_now检测当前击中状态,BrickHit作为帧级标志在每帧开始清零、有碰撞时置 1。BrickHit_count计数器跟踪总击中砖块数(19×7=133 块),当全部击中且球到达顶部时RestoreBrickwall重置砖墙。Collision标志类似地在每帧清零,球移动时检测到碰撞则置 1。

breakout_playfield.v

// (c) KNJN LLC 2024 for fpga4fun.com

/////////////////////////////////////////////////////////////////

module breakout_playfield(

input clk,

input [9:0] CounterX, ballX, PaddleX,

input [8:0] CounterY, ballY,

output reg DrawBall, DrawBorder, DrawPaddle,

output DrawBrick,

input BrickHit_now, RestoreBrickwall,

output BrickHit_acq

);

parameter hDrawArea = 640;

parameter vDrawArea = 480;

always @(posedge clk) DrawBall <= (CounterX>=ballX) && (CounterX<ballX+10'd16) && (CounterY>=ballY) && (CounterY<ballY+9'd16);

always @(posedge clk) DrawBorder <= (CounterX[9:2]==0) || (CounterX[9:2]==hDrawArea/4-1) || (CounterY[8:2]==0) || (CounterY[8:2]==vDrawArea/4-1);

always @(posedge clk) DrawPaddle <= (CounterX>=PaddleX) && (CounterX<=PaddleX+10'd64) && (CounterY>=vDrawArea-9'd46) && (CounterY<vDrawArea-9'd30);

// the brickwall starts at coordinates (16,48)

wire [9:0] BrickXo = CounterX-10'd16; wire [4:0] BrickX_H = BrickXo[9:5]; wire [4:0] BrickX_L = BrickXo[4:0];

wire [8:0] BrickYo = CounterY- 9'd48; wire [4:0] BrickY_H = BrickYo[8:4]; wire [3:0] BrickY_L = BrickYo[3:0];

wire [9:0] BrickA = {BrickY_H, BrickX_H}; // and is organized as a 32 x 32 matrix

reg [1023:0] RAMbrickwall; // in this blockram

reg BrickPresent, BrickHit_nowR;

always @(posedge clk)

begin

if(BrickHit_now | RestoreBrickwall) RAMbrickwall[BrickA] <= RestoreBrickwall ? BrickX_H<19 & BrickY_H<7 : 1'b0; // 19 x 7 brickwall

BrickPresent <= RAMbrickwall[BrickA];

BrickHit_nowR <= BrickHit_now;

end

assign BrickHit_acq = BrickPresent & BrickHit_nowR;

reg BrickBody; always @(posedge clk) BrickBody <= |BrickY_L[3:1] && |BrickX_L[4:1]; // leave two pixels between each brick

assign DrawBrick = BrickPresent & BrickBody;

endmodule

这个模块负责游戏场景中各个对象的绘制判断。球的绘制区域是 16×16 像素,通过比较当前扫描位置与球坐标确定;边框绘制使用位移操作CounterX[9:2]CounterY[8:2],实际上是将坐标除以 4,在 640×480 显示区域的边缘绘制 4 像素宽的边框;挡板绘制在屏幕底部,宽度 64 像素,位置在距离底部 30-46 像素的区域,跟随 PaddleX 水平移动。所有这些绘制判断都通过比较当前扫描坐标与对象坐标范围来实现。

砖墙从坐标 (16,48) 开始,通过坐标变换BrickXoBrickYo计算相对位置,然后分别提取高位和低位部分。BrickX_HBrickY_H作为 32×32 矩阵的索引,组成 10 位地址BrickA访问 1024 位的块 RAMRAMbrickwall。砖块的实际排列是 19×7 的矩阵,在RestoreBrickwall信号时恢复,在BrickHit_now信号时清除对应位置的砖块。BrickBody通过检查坐标的低位部分BrickY_L[3:1]BrickX_L[4:1]是否非零来在砖块间留出 2 像素的间隙,最终的DrawBrick信号需要同时满足砖块存在和在砖块主体区域的条件。BrickHit_acq信号在砖块存在且检测到击中时产生,用于游戏逻辑的砖块计数。

featured