🖼️ 第4课:VGA图像

ROM存储位图 + 坐标映射像素

🏆 成就:像素画家 ✅ Verilator验证

🖼️
像素画家
Verilator仿真显示一个位图

🧩 位图原理

把图像像素颜色存进ROM,用坐标作为地址读取对应颜色。我们用8×8笑脸图案演示。

Verilogvga_bitmap.v核心
reg [7:0] rom [0:7]; // 8行×8位
initial begin
    rom[0]=8'b00111100; rom[1]=8'b01111110;
    rom[2]=8'b01011010; rom[3]=8'b01111110;
    rom[4]=8'b01111110; rom[5]=8'b01011010;
    rom[6]=8'b01100110; rom[7]=8'b00111100;
end
wire [2:0] img_x=(hcount-POS_X)>>3;
wire [2:0] img_y=(vcount-POS_Y)>>3;
wire pixel_on=rom[img_y][7-img_x];
assign rgb=video_on?(in_image?(pixel_on?3'b110:3'b000):3'b011):3'b000;

📐 ROM寻址详解

8×8笑脸ROM寻址: img_y = 行号(0~7), img_x = 列号(0~7) ROM行 = img_y, ROM位 = 7 - img_x (最高位=最左) 例:像素(3,2) → rom[2][7-3] = rom[2][4] rom[2] = 01011010 ^ 第4位=1 → 该像素点亮 放大显示(SCALE=8): 屏幕坐标 → 逻辑像素 → ROM地址 → 像素颜色 (288,208) → (0,0) → rom[0][7] = 0 → 黑 (296,208) → (1,0) → rom[0][6] = 0 → 黑 (304,208) → (2,0) → rom[0][5] = 1 → 黄 ... 居中计算: POS_X = (640 - 8×8) / 2 = 288 POS_Y = (480 - 8×8) / 2 = 208

🧪 Verilator完整测试台

SystemVerilogvga_bitmap_tb.sv
module vga_bitmap_tb;
    logic clk,rst_n; logic hsync,vsync; logic [2:0] rgb;
    vga_bitmap uut(.*);
    initial clk=0; always #20 clk=~clk;

    int yellow_count, cyan_count;
    int frame_count=0; logic vsync_prev;
    always @(posedge clk) begin
        vsync_prev<=vsync;
        if(uut.video_on) case(rgb)
            3'b110: yellow_count++;
            3'b011: cyan_count++;
        endcase
        if(vsync&&!vsync_prev) begin
            frame_count++;
            if(frame_count==2) begin
                $display("========== 位图验证 ==========");
                $display("笑脸黄色像素: %0d", yellow_count);
                $display("背景青色像素: %0d", cyan_count);
                $display("总可见像素: %0d (640×480=%0d)",
                    yellow_count+cyan_count, 640*480);
                if(yellow_count>0 && cyan_count>0)
                    $display("✓ 笑脸图案和背景均可见!");
                $display("==============================");
                $finish;
            end
        end
    end
    initial begin rst_n=0;#100;rst_n=1;end
    initial #100_000_000 $finish;
endmodule

📊 ROM容量计算

图案尺寸位深ROM容量备注
8×81位8字节最简演示
8×83位24字节8色图案
16×163位96字节经典精灵
32×324位512字节中等精灵
64×648位4KB大角色
256×2248位56KB整屏背景

💡 FPGA中的ROM:小ROM直接用Verilog reg数组综合,工具自动映射到LUT。大ROM使用Block RAM (BRAM)。

🔬 多色ROM位图详解

1位ROM只能表示"亮/暗"。要显示多色图案,每个像素需要更多位。下面是3位色ROM的实现方式:

Verilog3位色ROM图案实现
// 3位色ROM:每个像素3位(R,G,B)
// 8×8像素 = 64个3位值
reg [2:0] color_rom [0:63];

initial begin
    // 蘑菇图案(红白相间)
    color_rom[0+:8]  = '{3'b000,3'b000,3'b100,3'b100,3'b100,3'b100,3'b000,3'b000};
    color_rom[8+:8]  = '{3'b000,3'b100,3'b100,3'b100,3'b100,3'b100,3'b100,3'b000};
    color_rom[16+:8] = '{3'b000,3'b100,3'b111,3'b100,3'b100,3'b111,3'b100,3'b000};
    color_rom[24+:8] = '{3'b100,3'b111,3'b100,3'b111,3'b111,3'b100,3'b111,3'b100};
    color_rom[32+:8] = '{3'b100,3'b111,3'b100,3'b111,3'b111,3'b100,3'b111,3'b100};
    color_rom[40+:8] = '{3'b000,3'b100,3'b111,3'b111,3'b111,3'b111,3'b100,3'b000};
    color_rom[48+:8] = '{3'b000,3'b100,3'b100,3'b110,3'b110,3'b100,3'b100,3'b000};
    color_rom[56+:8] = '{3'b000,3'b000,3'b110,3'b110,3'b110,3'b110,3'b000,3'b000};
end

// 地址计算
wire [5:0] rom_addr = {img_y, img_x}; // 6位: y[2:0] & x[2:0]
wire [2:0] pixel_rgb = color_rom[rom_addr];

🎯 实战挑战

挑战1:设计一个16×16的心形图案

挑战2:实现多图案切换(按按钮切换显示不同图案)

挑战3:用ROM显示数字0~9(为Pong计分板做准备)

💡 设计你自己的图案

1. 在方格纸上画出8×8图案

2. 把每个格子转换为1(亮)或0(暗)

3. 每行8位组成一个字节

4. 修改rom[0]~rom[7]的值

5. 重新编译仿真看你的图案!

🛠️ 编译运行

Bash编译运行步骤
verilator --binary -j 0 --trace \
    vga_sync.v vga_bitmap.v vga_bitmap_tb.sv
./obj_dir/Vvga_bitmap_tb

# 预期输出:
# ========== 位图验证 ==========
# 笑脸黄色像素: ...
# 背景青色像素: ...
# 总可见像素: 307200 (640×480=307200)
# ✓ 笑脸图案和背景均可见!
# ==============================

💡 FPGA中的ROM:小ROM(几十字节)用Verilog reg数组综合,工具自动映射到LUT。大ROM(几KB+)使用Block RAM (BRAM)。Verilator对这两种都支持。

🧠 概念检查清单

✅ ROM如何存储位图数据?

✅ 如何用屏幕坐标计算ROM地址?

✅ 为什么要放大(SCALE)?小图案在大屏幕上会怎样?

✅ 1位ROM和3位ROM的区别是什么?

✅ 如何让图案在屏幕上居中显示?

✅ FPGA中ROM用什么实现?LUT vs BRAM?

🔮 下一课预告

下一课我们让图案动起来!精灵(Sprite)= 位置寄存器 + 图案ROM。通过每帧更新位置,实现弹跳移动效果。这是游戏开发的核心概念!

🐛 常见问题排查

Q: 图案不显示? 检查POS_X和POS_Y是否在640×480范围内。

Q: 图案位置偏移? 确认SCALE和坐标计算是否正确:img_x = (hcount-POS_X) >> $clog2(SCALE)

Q: 图案变形? 检查ROM位序——最高位对应最左像素。

Q: Verilator报错? 确保vga_sync.v在同一目录下。

⚠️ ROM初始化陷阱

Verilog的initial块在仿真中有效,但综合到真实FPGA时行为不同!大多数FPGA工具链会忽略initial块。实际部署时需要:①使用复位逻辑初始化 ②使用$readmemh从文件加载 ③使用BRAM的初始化属性。Verilator仿真中initial完全OK,所以本课代码可以直接跑。

📐 ROM寻址详解

8×8笑脸ROM寻址: img_y=行号(0~7), img_x=列号(0~7) 地址: ROM行=img_y, ROM位=7-img_x 例: 像素(3,2)→rom[2][4] rom[2]=01011010 → 第4位=1→点亮 放大显示(SCALE=8): 屏幕坐标→逻辑像素→ROM地址→像素颜色 (288,208)→(0,0)→rom[0][7]=0→黑 (304,208)→(2,0)→rom[0][5]=1→黄

📊 ROM容量计算

图案尺寸位深ROM容量
8×81位8字节
16×163位96字节
32×324位512字节
64×648位4KB
256×2248位56KB

💡 FPGA中的ROM:小ROM直接用Verilog的reg数组综合,工具自动映射到LUT。大ROM需要Block RAM (BRAM)。

💡 设计你自己的图案

1. 在方格纸上画8×8图案

2. 每格转换为1(亮)或0(暗)

3. 每行8位组成一个字节

4. 修改rom[0]~rom[7]的值

5. 重新编译仿真看你的图案!

🧪 Verilator完整测试台

SystemVerilogvga_bitmap_tb.sv
module vga_bitmap_tb;
    logic clk,rst_n; logic hsync,vsync; logic [2:0] rgb;
    vga_bitmap uut(.*);
    initial clk=0; always #20 clk=~clk;

    int yellow_count, cyan_count;
    int frame_count=0; logic vsync_prev;
    always @(posedge clk) begin
        vsync_prev<=vsync;
        if(uut.video_on) case(rgb)
            3'b110: yellow_count++;
            3'b011: cyan_count++;
        endcase
        if(vsync&&!vsync_prev) begin
            frame_count++;
            if(frame_count==2) begin
                $display("========== 位图验证 ==========");
                $display("笑脸黄色像素: %0d",yellow_count);
                $display("背景青色像素: %0d",cyan_count);
                $display("总可见像素: %0d",yellow_count+cyan_count);
                if(yellow_count>0&&cyan_count>0)
                    $display("✓ 笑脸图案和背景均可见!");
                $display("==============================");
                $finish;
            end
        end
    end
    initial begin rst_n=0;#100;rst_n=1;end
    initial #100_000_000 $finish;
endmodule