🏠 课程总览 > 第03课
第03课: 帧缓冲
64×48像素帧缓冲存储器,写像素读回验证
🏆 写像素读回正确
✅ Verilator仿真验证通过
📖 核心概念
- 帧缓冲概念:帧缓冲是存储一帧图像数据的内存。每个像素用4位表示16种颜色
- 双端口访问:CPU写像素+VGA读像素同时进行,需要时序控制避免冲突
- 地址映射:64×48=3072个像素,地址0-3071。像素(x,y)的地址=y×64+x
💡 关键思路:本课的核心是帧缓冲概念——帧缓冲是存储一帧图像数据的内存。每个像素用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输出会拖慢仿真速度,验证通过后可以减少打印频率