第19课: NES精灵

精灵图案ROM+坐标映射+水平/垂直翻转

🏆 8×8精灵渲染 ✅ Verilator仿真验证通过

📖 核心概念

💡 关键思路:本课的核心是精灵图案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输出会拖慢仿真速度,验证通过后可以减少打印频率