回顾第六章的全景图,三个亮点各有自己的「手术位置」,且都落在官方例程的外围自研区域,不碰加密 IP:
| 亮点 | 改造落点 | 依赖的前置知识 | 改动量 |
|---|---|---|---|
| A 单缓冲消融 | sd_card_bmp 的 next_buf_lut / disp_buf_idx | 第二、三章双缓冲 | 极小(几行) |
| B OSD 三级流水 | vout_data 输出前,加一个 OSD 模块 | 第五章读侧、时序 | 中(新模块) |
| C 定点淡入淡出 | Read FIFO 与 HDMI 之间 | 第二、五章 + alpha 融合 | 中(新模块) |
三者都「小而值钱」:改动集中在自研模块,不动官方已验证的 SD/SDRAM/HDMI 主链路,基础分稳,加分项可控。
这是三个亮点里最轻量、性价比最高的一个——核心就是「把双缓冲故意退化成单缓冲,对照观察画面撕裂」,用实测数据证明双缓冲的必要性。
改造点只有一处:sd_card_bmp 里选缓冲的函数和切换逻辑。给模块加一个 SINGLE_BUF 参数,为真时读写永远指向 buffer0:
function [1:0] next_buf_lut;
input [1:0] cur_disp_buf;
input valid_now;
begin
if (SINGLE_BUF)
next_buf_lut = 2'd0; // 消融:永远写 buffer0
else if (!valid_now)
next_buf_lut = 2'd0;
else if (cur_disp_buf == 2'd0)
next_buf_lut = 2'd1;
else
next_buf_lut = 2'd0;
end
endfunction
提交切换处同样加一行:
disp_buf_idx <= SINGLE_BUF ? 2'd0 : pending_buf_idx; // 单缓冲:显示永远 buffer0
单缓冲模式下,写侧正在往 buffer0 灌新图,读侧同时从 buffer0 取旧图,读写直接冲突——半帧新图半帧旧图的「撕裂」就出来了。顶层把这个参数接到一个拨码开关上,现场就能一键切换对照。
write_buf_idx/disp_buf_idx 和帧边界,双缓冲模式下两个索引在 VSYNC 时刻才交换,单缓冲模式则永远相等。这组对照就是论文第 7 章「消融实验」的核心素材。OSD(On-Screen Display)在画面上叠加文字,比如显示「图片编号 / 播放模式 / 实时帧率」。它是三个亮点里观感最直观、答辩最好讲的一个。
官方扩展项把 OSD 描述得简单(「文字叠加」),但论文里要拆成三级流水写才显深度:
三级流水的 RTL 骨架:
// 第 1 级:坐标命中 + 字模相对地址
always @(posedge clk) begin
osd_hit <= (x >= X0) && (x < X0 + CHAR_W * N) && (y >= Y0) && (y < Y0 + 16);
char_x <= (x - X0) >> 3; // 字符列号(8 像素宽)
char_y <= (y - Y0) >> 4; // 字符行号(16 像素高)
dot_x <= x[2:0]; // 字模内列偏移
dot_y <= y[3:0]; // 字模内行偏移
end
// 第 2 级:字模 ROM 查找(BRAM,1 拍读延迟)
always @(posedge clk) begin
rom_addr <= {char_code[char_y], dot_y}; // {字符 ASCII, 行号} = 字模行地址
end
// 第 3 级:像素仲裁
always @(posedge clk) begin
dot_on <= font_row[~dot_x]; // bit 命中
vout <= osd_hit ? (dot_on ? fg : bg) : video_pixel;
end
字模点阵(8×16 的 ASCII 字体)不是手写的,用 Python 生成再初始化 BRAM:
# 用 PIL 把字符渲染成 8x16 点阵,导出 verilog 初始化文件
from PIL import Image, ImageDraw, ImageFont
font = ImageFont.truetype("consola.ttf", 16)
for ch in range(32, 127):
img = Image.new("1", (8, 16), 0)
ImageDraw.Draw(img).text((0, 0), chr(ch), font=font, fill=1)
rows = [sum((img.getpixel((px, py)) & 1) << (7 - px) for px in range(8)) for py in range(16)]
print(f"8'h{ch:02X}: font_row = 16'b" + ",".join(f"{r:02X}" for r in rows) + ";")
video_delay 之后,直接用延迟后的 de_r 驱动一个 x/y 计数器:de 有效时 x 自增,到行尾 x 清零、y 加一。这样 x/y 天然和像素流对齐,不需要额外的时序同步。转场特效是官方扩展项里最「提分」的一项。淡入淡出的数学本质是一句 alpha 混合:
Y = (1 − α)·A + α·B A = 旧图,B = 新图,α ∈ [0, 1]
硬件上不能做浮点,用 8bit 定点 α∈[0,255] 化简成单乘 + 减 + 移位:
// Shapiro 化简:单次乘法 + 减法 + 8bit 右移(纯连线截位,0 周期开销)
assign blend_r = ((alpha * (new_r - old_r)) >> 8) + old_r;
assign blend_g = ((alpha * (new_g - old_g)) >> 8) + old_g;
assign blend_b = ((alpha * (new_b - old_b)) >> 8) + old_b;
α 的更新必须放在场消隐期——如果扫描中途改 α,同一帧上半截和下半截混合比例不一样,会看到明显的「渐变线」。所以 α 步进挂在场同步边沿:
always @(posedge clk) begin
if (vsync_edge) begin
if (fading)
alpha <= (alpha > 8'd248) ? 8'd255 : alpha + 8'd8; // 每帧 +8,约 32 帧过渡
else
alpha <= 8'd0;
end
end
old_r 和 new_r 要同时拿到——旧图在 buffer0、新图在 buffer1,读侧本来只读一个 buffer。完整实现需要读侧能同时访问两个缓冲(双端口读 / 分时复用读端口),这会改动读状态机的带宽分配。建议先做「α 逐帧步进 + 从单 buffer 取像素」的简化版跑通数学,再升级到双 buffer 混合。把三个亮点放回第六章的全景图,它们的插入位置一目了然:
sd_card_bmp 里加一个 SINGLE_BUF 参数开关;video_delay 之后、vout_data 输出之前,吃延迟后的 de 和像素;它们的共同点:都在读侧、都在加密 IP 外围、都用「打拍」对齐时序。这也正是官方例程留好的接口——数据链路的每一环都有明确的输入输出,你要做的就是在正确的节点上「接线」。