精灵概念 + 位置寄存器 + 边界弹跳
🏆 成就:精灵驯兽师 ✅ Verilator验证
精灵是可以在屏幕上移动的独立图案。每个VSYNC帧更新位置,碰到边界反弹。
reg [9:0] sprite_x, sprite_y;
reg signed [1:0] vel_x, vel_y;
// 每帧更新
always @(posedge clk) if(vsync_fall) begin
sprite_x <= sprite_x + vel_x;
sprite_y <= sprite_y + vel_y;
if(sprite_x<=0 || sprite_x>=632) vel_x<=-vel_x;
if(sprite_y<=0 || sprite_y>=472) vel_y<=-vel_y;
end
wire in_sprite=(hcount>=sprite_x)&&(hcount=sprite_y)&&(vcount module vga_sprite_tb;
logic clk,rst_n; logic hsync,vsync; logic [2:0] rgb;
vga_sprite uut(.*);
initial clk=0; always #20 clk=~clk;
int frame_count=0; logic vsync_prev;
logic [9:0] last_x,last_y; int bounce_count;
always @(posedge clk) begin
vsync_prev<=vsync;
if(vsync&&!vsync_prev) begin
frame_count++;
if(frame_count>=2) begin
if(uut.sprite_x!=last_x || uut.sprite_y!=last_y)
$display(" 帧%0d: pos=(%0d,%0d)",
frame_count,uut.sprite_x,uut.sprite_y);
if(uut.sprite_x<=1 || uut.sprite_x>=632 ||
uut.sprite_y<=1 || uut.sprite_y>=472) begin
bounce_count++;
$display(" ↗ 反弹!pos=(%0d,%0d) 次数=%0d",
uut.sprite_x,uut.sprite_y,bounce_count);
end
last_x=uut.sprite_x; last_y=uut.sprite_y;
end
if(bounce_count>=4) begin
$display("精灵弹跳验证完成! 反弹=%0d ✓",bounce_count);
$finish;
end
end
end
initial begin rst_n=0;#100;rst_n=1;end
initial #500_000_000 $finish;
endmodulereg [9:0] trail_x[0:3], trail_y[0:3];
always @(posedge clk) if(vsync_fall) begin
trail_x[3]<=trail_x[2]; trail_x[2]<=trail_x[1];
trail_x[1]<=trail_x[0]; trail_x[0]<=sprite_x;
// Y同理
end
// 渲染:越旧的拖尾颜色越暗
wire in_trail3 = (hcount>=trail_x[3])&&(hcount<trail_x[3]+8)&&
(vcount>=trail_y[3])&&(vcount<trail_y[3]+8);
// ... in_trail2, in_trail1 类似
wire [2:0] sprite_rgb = in_sprite ? 3'b100 :
in_trail0 ? 3'b100 :
in_trail1 ? 3'b100 :
in_trail2 ? 3'b100 : 3'b000;FPGA的天然优势就是并行!CPU渲染100个精灵需要循环100次,FPGA可以同时判断100个精灵的位置——零额外开销。
// 4个精灵的位置和颜色
reg [9:0] sprite_x [0:3];
reg [9:0] sprite_y [0:3];
reg [2:0] sprite_color [0:3]; // 每个精灵不同颜色
// 并行判断所有精灵 — 零额外延迟!
wire [3:0] in_sprite;
genvar g;
generate for(g=0;g<4;g++) begin
assign in_sprite[g] = (hcount >= sprite_x[g]) &&
(hcount < sprite_x[g] + 8) &&
(vcount >= sprite_y[g]) &&
(vcount < sprite_y[g] + 8);
end endgenerate
// 优先级:精灵0 > 精灵1 > 精灵2 > 精灵3
wire [2:0] final_color = in_sprite[0] ? sprite_color[0] :
in_sprite[1] ? sprite_color[1] :
in_sprite[2] ? sprite_color[2] :
in_sprite[3] ? sprite_color[3] :
3'b011; // 背景色1. 固定速度:每帧移动1像素 → 60像素/秒
2. 加速速度:每帧移动2像素 → 120像素/秒
3. 小数速度:每3帧移动1像素 → 20像素/秒(用计数器实现)
4. 变速运动:每帧加速度变化 → 模拟重力效果
挑战1:添加第二个精灵(蓝色),两个同时弹跳
挑战2:实现穿越模式(从对面出来而不是反弹)
挑战3:用ROM笑脸替换纯色方块
verilator --binary -j 0 --trace \
vga_sync.v vga_sprite.v vga_sprite_tb.sv
./obj_dir/Vvga_sprite_tb
# 预期输出:
# 帧2: pos=(101,101)
# 帧3: pos=(102,102)
# ...
# ↗ 反弹!pos=(632,xxx) 次数=1
# ↗ 反弹!pos=(xxx,472) 次数=2
# 精灵弹跳验证完成! 反弹=4 ✓
# 提示:仿真可能需要较长时间等待精灵弹到边界
# 可以调整初始位置更靠近边界加速测试✅ 精灵的三个核心属性是什么?
✅ 为什么用VSYNC信号触发位置更新?
✅ 边界反弹的检测条件怎么写?
✅ 如何判断当前像素属于哪个精灵?
✅ 为什么FPGA天然适合多精灵渲染?
✅ 精灵速度控制有哪些方法?
下一课我们将用精灵技术实现经典Pong游戏!球体弹跳+挡板碰撞+自动AI+计分系统——你的第一个完整FPGA游戏!
reg [9:0] trail_x[0:3], trail_y[0:3];
always @(posedge clk) if(vsync_fall) begin
trail_x[3]<=trail_x[2]; trail_x[2]<=trail_x[1];
trail_x[1]<=trail_x[0]; trail_x[0]<=sprite_x;
// Y方向同理
end1. 多精灵:FPGA天然并行!复制位置寄存器即可
2. 速度控制:速度寄存器或"每N帧移动1像素"
3. 精灵层叠:用优先级决定重叠时谁在上面
4. 把纯色方块替换为ROM笑脸(第4课内容)
module vga_sprite_tb;
logic clk,rst_n; logic hsync,vsync; logic [2:0] rgb;
vga_sprite uut(.*);
initial clk=0; always #20 clk=~clk;
int frame_count=0; logic vsync_prev;
logic [9:0] last_x,last_y; int bounce_count;
always @(posedge clk) begin
vsync_prev<=vsync;
if(vsync&&!vsync_prev) begin
frame_count++;
if(frame_count>=2) begin
if(uut.sprite_x!=last_x||uut.sprite_y!=last_y)
$display("帧%0d: pos=(%0d,%0d)",frame_count,uut.sprite_x,uut.sprite_y);
if(uut.sprite_x<=1||uut.sprite_x>=632||
uut.sprite_y<=1||uut.sprite_y>=472) begin
bounce_count++;
$display("↗ 反弹! pos=(%0d,%0d) 次数=%0d",uut.sprite_x,uut.sprite_y,bounce_count);
end
last_x=uut.sprite_x; last_y=uut.sprite_y;
end
if(bounce_count>=4) begin
$display("精灵弹跳验证完成! 反弹=%0d ✓",bounce_count);
$finish;
end
end
end
initial begin rst_n=0;#100;rst_n=1;end
initial #500_000_000 $finish;
endmodule