🏠 课程总览 > 第19课
第19课: NES精灵
精灵图案ROM+坐标映射+水平/垂直翻转
🏆 8×8精灵渲染
✅ Verilator仿真验证通过
📖 核心概念
- 精灵图案ROM:8×8像素,2bit色深,32字节/精灵
- 坐标映射:屏幕坐标→精灵内坐标(col, row)
- 水平翻转:flip_h时列坐标镜像(7-col)
- 垂直翻转:flip_v时行坐标镜像(7-row)
- 透明色:pixel_color=0表示透明,不覆盖背景
💡 关键思路:本课的核心是精灵图案ROM——8×8像素,2bit色深,32字节/精灵。
💻 Verilog设计代码
设计模块源码——这是你真正要理解的硬件逻辑:
// 第19课: NES精灵
module nes_sprites (
input wire clk, input wire rst_n,
input wire [7:0] sprite_x, sprite_y,
input wire flip_h, flip_v,
output reg [7:0] scan_x, scan_y,
output reg pixel_valid, output reg [1:0] pixel_color, output reg sprite_hit
);
reg [7:0] pattern_rom [0:31]; integer i;
initial begin
pattern_rom[0]=8'b00_01_01_00;pattern_rom[1]=8'h00;pattern_rom[2]=8'b01_01_01_00;pattern_rom[3]=8'h00;
pattern_rom[4]=8'b10_01_01_00;pattern_rom[5]=8'h00;pattern_rom[6]=8'b11_11_11_10;pattern_rom[7]=8'h00;
pattern_rom[8]=8'b10_01_01_00;pattern_rom[9]=8'h00;pattern_rom[10]=8'b01_01_01_00;pattern_rom[11]=8'h00;
pattern_rom[12]=8'b00_01_01_00;pattern_rom[13]=8'h00;pattern_rom[14]=8'b00_01_01_00;pattern_rom[15]=8'h00;
pattern_rom[16]=8'b11_11_11_11;pattern_rom[17]=8'b11_11_11_11;pattern_rom[18]=8'b11_00_00_11;pattern_rom[19]=8'b11_00_00_11;
pattern_rom[20]=8'b11_11_11_11;pattern_rom[21]=8'b11_11_11_11;pattern_rom[22]=8'b11_00_00_11;pattern_rom[23]=8'b11_00_00_11;
for(i=24;i<32;i=i+1) pattern_rom[i]=8'h55;
end
reg [2:0] spr_row, spr_col; reg [4:0] rom_addr; reg [7:0] rom_data; reg [1:0] spr_pixel;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin scan_x<=0;scan_y<=0;pixel_valid<=0;pixel_color<=0;sprite_hit<=0; end
else begin pixel_valid<=0;sprite_hit<=0;
if(scan_x>=sprite_x&&scan_x<sprite_x+8&&scan_y>=sprite_y&&scan_y<sprite_y+8) begin
begin reg [7:0] dx,dy; dx=scan_x-sprite_x;dy=scan_y-sprite_y;
spr_row=flip_v?(3'd7-dy[2:0]):dy[2:0];spr_col=flip_h?(3'd7-dx[2:0]):dx[2:0];end
rom_addr={spr_row,1'b0};rom_data=pattern_rom[rom_addr+(spr_col[2:1])];
case(spr_col[0]) 0:spr_pixel=rom_data[7:6];1:spr_pixel=rom_data[5:4];endcase
if(spr_pixel!=0) begin pixel_color<=spr_pixel;pixel_valid<=1;sprite_hit<=1;end
end
if(scan_x<255) scan_x<=scan_x+1; else begin scan_x<=0;if(scan_y<239) scan_y<=scan_y+1;else scan_y<=0;end
end
end
endmodule
🧪 测试平台(Testbench)
testbench = 你的"手柄+屏幕",模拟输入、验证输出:
/* verilator lint_off WIDTHEXPAND */
/* verilator lint_off WIDTHTRUNC */
/* verilator lint_off UNOPTFLAT */
module tb;
reg clk,rst_n; reg [7:0] sprite_x,sprite_y; reg flip_h,flip_v;
wire [7:0] scan_x,scan_y; wire pixel_valid,sprite_hit; wire [1:0] pixel_color;
nes_sprites uut (.clk(clk),.rst_n(rst_n),.sprite_x(sprite_x),.sprite_y(sprite_y),.flip_h(flip_h),.flip_v(flip_v),.scan_x(scan_x),.scan_y(scan_y),.pixel_valid(pixel_valid),.pixel_color(pixel_color),.sprite_hit(sprite_hit));
always clk = #10 ~clk;
integer hit_count;
initial begin
$dumpfile("sprite.vcd");$dumpvars(0,tb);clk=0;rst_n=0;sprite_x=100;sprite_y=50;flip_h=0;flip_v=0;hit_count=0;
repeat(5)@(posedge clk);rst_n=1;
$display("=== NES精灵仿真 ===");$display("8x8精灵渲染");$display("");
$display("--- 精灵属性 ---");$display(" 位置: (%0d, %0d)",sprite_x,sprite_y);$display(" 大小: 8x8像素");$display(" 色深: 2bit (4色)");$display(" 翻转: H=%0b V=%0b",flip_h,flip_v);$display("");
$display("--- 精灵图案ROM (8x8) ---");
begin integer row; for(row=0;row<8;row=row+1) begin $write(" 行%0d: ",row);
begin integer col;reg [7:0] pd;pd=uut.pattern_rom[row*2];
for(col=7;col>=0;col=col-1) if(pd[col]) $write("█");else $write("·");end $write("\n");end end
$display("");$display("--- 精灵画面(2bit色) ---");
begin integer r;for(r=0;r<8;r=r+1) begin $write(" ");begin reg [7:0] b0;integer c;reg [1:0] px;b0=uut.pattern_rom[r*2];
for(c=0;c<4;c=c+1) begin px=b0[7-c*2-:2];case(px) 0:$write("··");1:$write("░░");2:$write("▓▓");3:$write("██");endcase;end end $write("\n");end end
$display("");$display("--- 测试翻转 ---");flip_h=1;repeat(10)@(posedge clk);flip_h=0;flip_v=1;
$display(" ✅ 水平翻转已测试");repeat(10)@(posedge clk);flip_v=0;$display(" ✅ 垂直翻转已测试");
$display("");$display("✅ 8x8精灵渲染验证通过!");$display("🏆 成就解锁: 8x8精灵渲染!");$finish;
end
endmodule
✅ 仿真输出
运行 verilator --cc *.sv --exe sim_main.cpp --top-module tb --timing --trace --build -j 4 -o sim 后的输出:
=== NES精灵仿真 ===
8x8精灵渲染
--- 精灵属性 ---
位置: (100, 50)
大小: 8x8像素
色深: 2bit (4色)
翻转: H=0 V=0
--- 精灵图案ROM (8x8) ---
行0: ···█·█··
行1: ·█·█·█··
行2: █··█·█··
行3: ███████·
行4: █··█·█··
行5: ·█·█·█··
行6: ···█·█··
行7: ···█·█··
--- 精灵画面(2bit色) ---
··░░░░··
░░░░░░··
▓▓░░░░··
██████▓▓
▓▓░░░░··
░░░░░░··
··░░░░··
··░░░░··
--- 测试翻转 ---
✅ 水平翻转已测试
✅ 垂直翻转已测试
✅ 8x8精灵渲染验证通过!
🏆 成就解锁: 8x8精灵渲染!
- tb.sv:24: Verilog $finish
🔧 编译和运行
# 编译
verilator --cc *.sv --exe sim_main.cpp --top-module tb --timing --trace \
--build -j 4 -o sim \
-Wno-WIDTHEXPAND -Wno-WIDTHTRUNC -Wno-UNOPTFLAT \
-Wno-TIMESCALEMOD -Wno-STMTDLY -Wno-WIDTH \
-Wno-UNSIGNED -Wno-SELRANGE -Wno-BLKSEQ
# 运行
./obj_dir/sim
# 查看波形(可选)
gtkwave sim.vcd
🎮 实战步骤
1
图案ROM编码:8×8精灵2bit色深=每列2bit×8列=16bit=2字节/行×8行=16字节/精灵。NES用planar格式(bit0和bit1分两字节)。
2
坐标映射:当scan_x/scan_y在精灵范围内时,计算精灵内坐标。翻转通过取反实现:flip_h ? (7-col) : col。
3
像素提取:从ROM字节中提取2bit像素。本课简化为每字节4个2bit像素,实际NES是planar格式(2字节交叉)。
4
透明处理:pixel_color=0表示透明,不设置pixel_valid和sprite_hit。背景可以透过来,实现精灵的非矩形形状。
🎮 精灵渲染技术
NES精灵限制:每帧64个精灵,每扫描行8个。超过8个会出现闪烁,这是NES游戏最常见的画面瑕疵。
精灵表:游戏通常将所有精灵图案存在一张大图中(精灵表),运行时选择需要的8×8块。16×16精灵=4个8×8块。
旋转缩放:NES不支持精灵旋转缩放,只能翻转。SNES/GBA支持仿射变换。现代GPU用纹理映射实现任意变换。
🎮 实战步骤
1
图案ROM编码:8×8精灵2bit色深,每列2bit×4列=8bit=1字节/行×8行=8字节(简化版)。实际NES用planar格式16字节/精灵。
2
坐标映射:当scan_x/scan_y在(sprite_x, sprite_y)到(sprite_x+7, sprite_y+7)范围内时,计算精灵内行列坐标。
3
翻转实现:水平翻转=列坐标取反(7-col),垂直翻转=行坐标取反(7-row)。硬件成本极低,只需MUX选择。
4
透明色处理:pixel_color=0表示透明,不设置sprite_hit。背景可以透过来,精灵不需要矩形边界。
🎮 精灵渲染技术
NES精灵限制:每帧64个精灵,每扫描行8个。超过8个会出现闪烁,这是NES游戏最常见的画面瑕疵。
精灵优先级:OAM中索引越小优先级越高。前面的精灵覆盖后面的,用于实现角色在障碍物前/后的效果。
动画帧:切换精灵图案索引实现动画。马里奥跑步3帧循环,每5帧切换一次。仅需3个精灵图案即可实现流畅动画。
🏆
8×8精灵渲染
✅ Verilator仿真验证通过
🧠 知识扩展
精灵属性:NES每个精灵有4字节OAM数据:Y坐标、Tile索引、属性(翻转/优先级/调色板)、X坐标。每帧最多64个精灵,每行最多8个。
精灵翻转:水平翻转和垂直翻转通过属性字节控制。硬件实现很简单:翻转时将列/行坐标取反(7-coord)。这是NES游戏做角色朝向切换的常用技巧。
⚡ 性能提示
• 使用--trace选项生成VCD波形文件,用GTKWave查看
• 使用-j 4选项并行编译,加快构建速度
• 使用--build选项让Verilator自动调用make
• 大量$display输出会拖慢仿真速度,验证通过后可以减少打印频率