这一章在干嘛?

前六章是「理解官方例程」,这一章是「在官方例程上长出属于自己的东西」——这正是竞赛区分度所在。三大亮点不是凭空发明,而是精准地插在前六章已经讲清的链路节点上:消融实验改在双缓冲索引处,OSD 插在 vout_data 输出前,淡入淡出挂在 Read FIFO 与 HDMI 之间。看完这一章,你就知道「读懂」和「拿分」之间还差哪一步。

三大亮点怎么插进现有链路

亮点 A:单缓冲消融实验

亮点 B:OSD 三级流水

亮点 C:定点淡入淡出

三个亮点的整合落点

三大亮点怎么插进现有链路

回顾第六章的全景图,三个亮点各有自己的「手术位置」,且都落在官方例程的外围自研区域,不碰加密 IP:

亮点改造落点依赖的前置知识改动量
A 单缓冲消融sd_card_bmpnext_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 描述得简单(「文字叠加」),但论文里要拆成三级流水写才显深度:

  1. 坐标命中级:判断当前像素 (x,y) 是否落在文字区域,算出字符行列号和字模内偏移;
  2. 字模生成级:查 ASCII 8×16 点阵 ROM,取出当前字符、当前行的 8bit 字模;
  3. 像素混合级:字模 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_rnew_r同时 拿到——旧图在 buffer0、新图在 buffer1,读侧本来只读一个 buffer。完整实现需要读侧能同时访问两个缓冲(双端口读 / 分时复用读端口),这会改动读状态机的带宽分配。建议先做「α 逐帧步进 + 从单 buffer 取像素」的简化版跑通数学,再升级到双 buffer 混合。

三个亮点的整合落点

把三个亮点放回第六章的全景图,它们的插入位置一目了然:

  1. 消融实验——不新增模块,只在 sd_card_bmp 里加一个 SINGLE_BUF 参数开关;
  2. OSD——插在 video_delay 之后、vout_data 输出之前,吃延迟后的 de 和像素;
  3. 淡入淡出——插在 Read FIFO 之后、HDMI 编码之前,同样吃读侧像素流。

它们的共同点:都在读侧、都在加密 IP 外围、都用「打拍」对齐时序。这也正是官方例程留好的接口——数据链路的每一环都有明确的输入输出,你要做的就是在正确的节点上「接线」。

动手的顺序建议

先做 A(消融实验,几分钟改完,立刻有对照数据),再做 B(OSD,观感最直接),最后做 C(淡入淡出,先简化版)。每完成一个就烧板验证一次,不要三个一起上——出了问题不知道是哪个模块的锅。