第03课: 帧缓冲

64×48像素帧缓冲存储器,写像素读回验证

🏆 写像素读回正确 ✅ Verilator仿真验证通过

📖 核心概念

💡 关键思路:本课的核心是帧缓冲概念——帧缓冲是存储一帧图像数据的内存。每个像素用4位表示16种颜色

💻 Verilog设计代码

设计模块源码——这是你真正要理解的硬件逻辑:

// 第03课: 帧缓冲 - 写像素读回正确 // 第3课: 帧缓冲 - 写像素读回正确 module framebuffer ( input wire clk, input wire rst_n, input wire [15:0] write_addr, input wire [3:0] write_data, input wire write_en, input wire [15:0] read_addr, output reg [3:0] read_data, output reg valid ); // 64x48 = 3072 nibbles (4-bit pixels, 16 colors) reg [3:0] mem [0:3071]; integer i; initial begin for (i = 0; i < 3072; i = i + 1) mem[i] = 4'h0; end always @(posedge clk or negedge rst_n) begin if (!rst_n) begin read_data <= 0; valid <= 0; read_data <= 0; valid <= 0; end else begin if (write_en && write_addr < 3072) mem[write_addr] <= write_data; if (read_addr < 3072) begin read_data <= mem[read_addr]; valid <= 1; end else begin read_data <= 0; valid <= 0; end end end endmodule

🧪 测试平台(Testbench)

testbench = 你的"手柄+屏幕",模拟输入、验证输出:

/* verilator lint_off WIDTHEXPAND */ /* verilator lint_off WIDTHTRUNC */ /* verilator lint_off UNOPTFLAT */ /* verilator lint_off WIDTHEXPAND */ /* verilator lint_off WIDTHTRUNC */ /* verilator lint_off UNOPTFLAT */ module tb; reg clk, rst_n; reg [15:0] write_addr, read_addr; reg [3:0] write_data; reg write_en; wire [3:0] read_data; wire valid; framebuffer uut ( .clk(clk), .rst_n(rst_n), .write_addr(write_addr), .write_data(write_data), .write_en(write_en), .read_addr(read_addr), .read_data(read_data), .valid(valid) ); always clk = #10 ~clk; integer i, errors; initial begin $dumpfile("sim.vcd"); $dumpvars(0, tb); clk = 0; rst_n = 0; write_en = 0; write_addr = 0; write_data = 0; read_addr = 0; repeat(5) @(posedge clk); rst_n = 1; $display("=== 帧缓冲仿真 ==="); $display("写像素读回正确"); $display(""); // Test 1: Write and read back individual pixels $display("--- 测试1: 写入读回 ---"); errors = 0; for (i = 0; i < 16; i = i + 1) begin write_addr = i * 10; write_data = i[3:0]; write_en = 1; @(posedge clk); end write_en = 0; repeat(2) @(posedge clk); for (i = 0; i < 16; i = i + 1) begin read_addr = i * 10; @(posedge clk); @(posedge clk); if (read_data !== i[3:0]) begin $display(" ❌ 地址%0d: 写入%0d 读回%0d", i*10, i, read_data); errors = errors + 1; end end if (errors == 0) $display(" ✅ 16个像素写入读回全部正确"); else $display(" ❌ 有%0d个错误", errors); // Test 2: Fill pattern $display(""); $display("--- 测试2: 填充测试 ---"); for (i = 0; i < 64; i = i + 1) begin write_addr = i; write_data = i[3:0] + 4'h1; write_en = 1; @(posedge clk); end write_en = 0; repeat(2) @(posedge clk); errors = 0; for (i = 0; i < 64; i = i + 1) begin read_addr = i; @(posedge clk); @(posedge clk); if (read_data !== (i[3:0] + 4'h1)) errors = errors + 1; end if (errors == 0) $display(" ✅ 64像素填充写入读回正确"); else $display(" ❌ 填充测试有%0d个错误", errors); // Test 3: Overwrite test $display(""); $display("--- 测试3: 覆盖写入 ---"); write_addr = 100; write_data = 4'hA; write_en = 1; @(posedge clk); write_data = 4'h5; @(posedge clk); write_en = 0; repeat(2) @(posedge clk); read_addr = 100; @(posedge clk); @(posedge clk); if (read_data == 4'h5) $display(" ✅ 覆盖写入正确(旧值=A, 新值=5)"); else $display(" ❌ 覆盖写入错误(期望5, 实际%0d)", read_data); // Test 4: Valid signal $display(""); $display("--- 测试4: 有效信号 ---"); read_addr = 0; @(posedge clk); @(posedge clk); if (valid) $display(" ✅ 有效地址valid=1"); else $display(" ❌ 有效地址valid应为1"); // Test 5: Boundary $display(""); $display("--- 测试5: 边界地址 ---"); write_addr = 3071; write_data = 4'hF; write_en = 1; @(posedge clk); write_en = 0; repeat(2) @(posedge clk); read_addr = 3071; @(posedge clk); @(posedge clk); if (read_data == 4'hF) $display(" ✅ 最高地址3071读写正确"); else $display(" ❌ 最高地址读写错误(期望F, 实际%0h)", read_data); $display(""); $display("✅ 写像素读回正确验证通过!"); $display("🏆 成就解锁: 写像素读回正确!"); $finish; end endmodule

✅ 仿真输出

运行 verilator --cc *.sv --exe sim_main.cpp --top-module tb --timing --trace --build -j 4 -o sim 后的输出:

=== 帧缓冲仿真 === 写像素读回正确 --- 测试1: 写入读回 --- ✅ 16个像素写入读回全部正确 --- 测试2: 填充测试 --- ✅ 64像素填充写入读回正确 --- 测试3: 覆盖写入 --- ✅ 覆盖写入正确(旧值=A, 新值=5) --- 测试4: 有效信号 --- ✅ 有效地址valid=1 --- 测试5: 边界地址 --- ✅ 最高地址3071读写正确 ✅ 写像素读回正确验证通过! 🏆 成就解锁: 写像素读回正确! - tb.sv:97: 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-BLKLOOPINIT # 运行 ./obj_dir/sim # 查看波形(可选) gtkwave sim.vcd

🎮 实战步骤

1
存储结构:3072×4bit = 12288bit = 1536字节。每个地址存储一个4位像素值(0-15=16种颜色)
2
写操作:write_en=1时,将write_data写入write_addr对应的存储单元。写操作在时钟上升沿生效
3
读操作:read_addr在时钟上升沿采样,下一个周期输出对应数据。valid信号表示读地址有效
4
覆盖写入:对同一地址连续写两次,后写的值覆盖先写的值。这是RAM的基本特性

🎮 游戏开发知识

双缓冲:实际VGA系统中常用双缓冲:一个缓冲用于显示,另一个用于绘制。两缓冲交替切换避免撕裂

BRAM利用:FPGA的Block RAM适合实现帧缓冲。Xilinx 7系列每个BRAM为36Kbit,可存储6×6像素

带宽计算:640×480@60Hz需要18.4MB/s带宽。如果像素为16位色,需要36.8MB/s

🏆
写像素读回正确
✅ Verilator仿真验证通过

🧠 知识扩展

双缓冲:实际VGA系统中常用双缓冲:一个缓冲用于显示,另一个用于绘制。两缓冲交替切换避免撕裂

BRAM利用:FPGA的Block RAM适合实现帧缓冲。Xilinx 7系列每个BRAM为36Kbit,可存储6×6像素

带宽计算:640×480@60Hz需要18.4MB/s带宽。如果像素为16位色,需要36.8MB/s

⚡ 性能提示

• 使用--trace选项生成VCD波形文件,用GTKWave查看

• 使用-j 4选项并行编译,加快构建速度

• 使用--build选项让Verilator自动调用make

• 大量$display输出会拖慢仿真速度,验证通过后可以减少打印频率