这一章在干嘛?
前六章是「理解官方例程」,这一章是「在官方例程上长出属于自己的东西」——这正是竞赛区分度所在。三大亮点不是凭空发明,而是精准地插在前六章已经讲清的链路节点上:消融实验改在双缓冲索引处,OSD 插在
vout_data输出前,淡入淡出挂在 Read FIFO 与 HDMI 之间。看完这一章,你就知道「读懂」和「拿分」之间还差哪一步。
三大亮点怎么插进现有链路
回顾第六章的全景图,三个亮点各有自己的「手术位置」,且都落在官方例程的外围自研区域,不碰加密 IP:
| 亮点 | 改造落点 | 依赖的前置知识 | 改动量 |
|---|---|---|---|
| A 单缓冲消融 | sd_card_bmp 的 next_buf_lut / disp_buf_idx | 第二、三章双缓冲 | 极小(几行) |
| B OSD 三级流水 | vout_data 输出前,加一个 OSD 模块 | 第五章读侧、时序 | 中(新模块) |
| C 定点淡入淡出 | Read FIFO 与 HDMI 之间 | 第二、五章 + alpha 融合 | 中(新模块) |
三者都「小而值钱」:改动集中在自研模块,不动官方已验证的 SD/SDRAM/HDMI 主链路,基础分稳,加分项可控。
亮点 A:单缓冲消融实验
这是三个亮点里最轻量、性价比最高的一个——核心就是「把双缓冲故意退化成单缓冲,对照观察画面撕裂」,用实测数据证明双缓冲的必要性。
改造点只有一处: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 章「消融实验」的核心素材。
亮点 B:OSD 三级流水
OSD(On-Screen Display)在画面上叠加文字,比如显示「图片编号 / 播放模式 / 实时帧率」。它是三个亮点里观感最直观、答辩最好讲的一个。
官方扩展项把 OSD 描述得简单(「文字叠加」),但论文里要拆成三级流水写才显深度:
- 坐标命中级:判断当前像素 (x,y) 是否落在文字区域,算出字符行列号和字模内偏移;
- 字模生成级:查 ASCII 8×16 点阵 ROM,取出当前字符、当前行的 8bit 字模;
- 像素混合级:字模 bit 命中则输出前景色,否则输出背景色或透传视频像素。
三级流水的 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) + ";")
像素坐标从哪来?
OSD 插在
video_delay之后,直接用延迟后的de_r驱动一个 x/y 计数器:de有效时 x 自增,到行尾 x 清零、y 加一。这样 x/y 天然和像素流对齐,不需要额外的时序同步。
亮点 C:定点淡入淡出
转场特效是官方扩展项里最「提分」的一项。淡入淡出的数学本质是一句 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参数开关; - OSD——插在
video_delay之后、vout_data输出之前,吃延迟后的de和像素; - 淡入淡出——插在 Read FIFO 之后、HDMI 编码之前,同样吃读侧像素流。
它们的共同点:都在读侧、都在加密 IP 外围、都用「打拍」对齐时序。这也正是官方例程留好的接口——数据链路的每一环都有明确的输入输出,你要做的就是在正确的节点上「接线」。
动手的顺序建议
先做 A(消融实验,几分钟改完,立刻有对照数据),再做 B(OSD,观感最直接),最后做 C(淡入淡出,先简化版)。每完成一个就烧板验证一次,不要三个一起上——出了问题不知道是哪个模块的锅。
单缓冲消融实验的核心改动是什么?
给 sd_card_bmp 加 SINGLE_BUF 参数,让 next_buf_lut 和 disp_buf_idx 恒指向 buffer0,读写强制竞争制造撕裂
OSD 三级流水是哪三级?
坐标命中级(算字符行列号+字模偏移)、字模生成级(查 8×16 点阵 ROM)、像素混合级(bit 命中选前景/背景色)
字模点阵为什么用 Python 生成而不是手写?
用 PIL 渲染 ASCII 字符成 8×16 点阵,批量导出 BRAM 初始化文件,省去手敲几百个十六进制的苦工
淡入淡出的硬件化简公式是什么、为什么省 DSP?
V = [α·(V1−V2)]>>8 + V2,单次乘法+减法+移位,比两次乘加省一半 DSP
α 为什么必须在场消隐期更新?
扫描中途改 α 会导致同一帧上下混合比例不一致,出现「渐变线」,所以挂 VSYNC 边沿逐帧步进