从 SPEC 到上板:在 Zynq-7020 上用 RTL 手写一个 YOLO 卷积加速器(完整教程)

本文把一个完整的「mini-YOLO(YOLOv2 变体)FPGA 加速器」教学案例从算法原理 → 定点量化 → RTL 前端设计 → UVM 验证 → Zynq 上板 → 时序优化与踩坑全流程串起来。所有代码在正点原子领航者 Zynq-7020(XC7Z020CLG400-2)上跑通,UVM 回归 10 层全部 mism=0 / maxdiff=0。

全文配套源码目录:E:\fpga\FPGA_projects\YOLO\Yolo\repro\。


目录

  1. 项目概览
  2. [算法原理与 Python 参考链](#算法原理与 Python 参考链)
  3. 定点量化与测试向量生成
  4. [RTL 架构设计详解](#RTL 架构设计详解)
  5. [UVM 验证环境搭建](#UVM 验证环境搭建)
  6. [Zynq 上板指南](#Zynq 上板指南)
  7. 时序优化与踩坑实录

一、项目概览

1.1 这个项目做了什么

把一个 9 层卷积的 mini-YOLO 网络(输入 416×416×3 BGR,输出 13×13×125 检测张量,VOC 20 类)从浮点 C 参考实现,一步步搬到 Zynq-7020 PL 端:

复制代码
416×416×3 BGR  ──►  conv1~conv9  ──►  13×13×125  ──►  anchor/NMS 解码
   (float)            (int16)             (int16)
层 in_ch out_ch 核 激活 池化 输出尺寸
conv1 3 16 3×3 --- 2×2 max+Leaky 208×208
conv2 16 32 3×3 --- 2×2 max+Leaky 104×104
conv3 32 64 3×3 --- 2×2 max+Leaky 52×52
conv4 64 128 3×3 --- 2×2 max+Leaky 26×26
conv5 128 256 3×3 --- 2×2 max+Leaky 13×13
conv6 256 512 3×3 --- --- 13×13
pool6 512 512 --- Leaky 2×2 stride1 13×13
conv7 512 1024 3×3 Leaky --- 13×13
conv8 1024 1024 3×3 Leaky --- 13×13
conv9 1024 125 1×1 --- --- 13×13

输出 125 = 5 anchors × 25(tx,ty,tw,th,obj + 20 类)。

整个工程的分工:

  • 算法侧(Python/numpy,禁用 MATLAB):复现浮点前向、做 int16 量化、生成 UVM/上板共用的测试向量与参考输出。
  • 硬件侧(Verilog) :conv_core.v 主控状态机 + pe.v 9-tap 3 级流水 MAC×16 + axi_wrapper.v AXI4 包装。
  • 验证侧(SystemVerilog/UVM 1.2) :conv_if.sv DDR 模型 + driver/scoreboard,10 层全量回归。
  • 上板侧(Vivado 2019.2 + Vitis):Zynq BD + PS C 程序 + SD 卡权重/输入。

1.2 你需要的环境

组件 版本 用途
板卡 正点原子领航者 Zynq-7020(XC7Z020CLG400-2) PL 实现
Vivado / Vitis 2019.2 综合、实现、PS 软件工程
QuestaSim / ModelSim 10.7(UVM 1.2) RTL 功能仿真
Python 3.8+(numpy) 算法参考链 + 量化 + 测试向量
操作系统 Windows 10/11 全流程本机

1.3 仓库目录速览

复制代码
repro/
├── docs/                    # SPEC / 测试计划 / 仿真图 / 交付报告
├── rtl/                     # RTL 源码
│   ├── conv_core.v          #   主控状态机 + 数据通路(~1200 行)
│   ├── pe.v                 #   9-tap 3 级流水 MAC×16
│   ├── axi_wrapper.v        #   AXI4-Lite 配置 + AXI4 Master
│   └── pool_ram.v           #   池化行缓存(LUTRAM)
├── sw/                      # Python 参考链
│   ├── minyolo_net.py       #   浮点前向
│   ├── quantize.py          #   int16 量化 + 导出
│   ├── dump_testdata.py     #   UVM 测试向量
│   └── gen_board_files.py   #   上板 SD 文件
├── uvm/tb/                  # UVM 环境
│   ├── conv_if.sv           #   virtual interface + DDR 模型
│   ├── conv_pkg.sv          #   driver / scoreboard
│   ├── tb_top.sv
│   ├── run_full.do
│   └── testdata/<layer>/    #   10 层测试向量
└── board/
    ├── vivado/              # synth.tcl / zynq_bd.tcl
    ├── vitis/src/           # main.c / axi_drv.h
    └── sd/                  # weights/inputs/refs + layers.json

1.4 验证结果一览

复制代码
[PASS] conv1  bytes=1384448 mism=0 maxdiff=0
[PASS] conv2  bytes=692224  mism=0 maxdiff=0
[PASS] conv3  bytes=346112  mism=0 maxdiff=0
[PASS] conv4  bytes=173056  mism=0 maxdiff=0
[PASS] conv5  bytes=86528   mism=0 maxdiff=0
[PASS] conv6  bytes=173056  mism=0 maxdiff=0
[PASS] conv7  bytes=346112  mism=0 maxdiff=0
[PASS] conv8  bytes=346112  mism=0 maxdiff=0
[PASS] conv9  bytes=43264   mism=0 maxdiff=0
[PASS] pool6  bytes=173056  mism=0 maxdiff=0
RESULT: ALL PASS  (layers=10 pass=10 fail=0)

二、算法原理与 Python 参考链

这一章把 sw/minyolo_net.py 完整贴出来,每一段都解释"为什么这么写、硬件上对应什么"。

2.1 完整源码(minyolo_net.py,99 行)

python 复制代码
# -*- coding: utf-8 -*-
"""
minYOLO 网络前向(numpy 实现)
严格按 HLS C 源码 minYOLO.cpp 移植:
  - conv1-6: 卷积+bias(无激活),随后 2x2 maxpool(融合 LeakyReLU 0.1)
  - conv6 后: pad6(复制第11行/列的特殊padding)-> slowpool6(2x2 max stride1 + LeakyReLU)
  - conv7/8: 卷积+bias+LeakyReLU(无pool)
  - conv9: 1x1 卷积,只累加、不加 bias(与 C 源码一致)
输入: x (3,416,416) float32, 通道序为 BGR, 数值 [0,1]
输出: (125,13,13)
"""
import numpy as np


def im2col(x, kh, kw):
    """x: (C,H,W) -> cols: (C*kh*kw, H*W),zero padding 保持尺寸"""
    C, H, W = x.shape
    pad = kh // 2
    xp = np.pad(x, ((0, 0), (pad, pad), (pad, pad)), mode="constant")
    cols = np.empty((C, kh, kw, H, W), dtype=np.float32)
    for dy in range(kh):
        for dx in range(kw):
            cols[:, dy, dx] = xp[:, dy:dy + H, dx:dx + W]
    return cols.reshape(C * kh * kw, H * W)


def conv(x, w, b=None, leaky=False):
    """3x3 卷积(same/zero-pad),可选 bias 与 LeakyReLU(0.1)
    w: (K,C,3,3)"""
    C, H, W = x.shape
    K = w.shape[0]
    cols = im2col(x, 3, 3)                # (C*9, H*W)
    wm = w.reshape(K, C * 9)              # (K, C*9)
    out = (wm @ cols).reshape(K, H, W)
    if b is not None:
        out += b.reshape(K, 1, 1)
    if leaky:
        out = np.where(out > 0, out, out * 0.1)
    return out


def maxpool_leaky(x):
    """2x2 maxpool, stride 2, 融合 LeakyReLU(0.1)"""
    C, H, W = x.shape
    a = x[:, 0::2, 0::2]
    b = x[:, 0::2, 1::2]
    c = x[:, 1::2, 0::2]
    d = x[:, 1::2, 1::2]
    m = np.maximum(np.maximum(a, b), np.maximum(c, d))
    return np.where(m > 0, m, m * 0.1)


def pad6(x):
    """conv6 输出 13x13 -> 14x14 的特殊 padding(与 C 源码逐位一致)"""
    C = x.shape[0]
    out = np.zeros((C, 14, 14), dtype=np.float32)
    out[:, 0:13, 0:13] = x
    out[:, 0:13, 13] = x[:, :, 11]     # 右列 = 原第 11 列
    out[:, 13, 0:13] = x[:, 11, :]     # 下行 = 原第 11 行
    out[:, 13, 13] = x[:, 11, 11]     # 右下角
    return out


def slowpool6(x):
    """14x14 -> 13x13 的 2x2 maxpool(stride 1)+ LeakyReLU(0.1)"""
    C = x.shape[0]
    m = np.maximum.reduce(
        [x[:, i:i + 13, j:j + 13] for i in range(2) for j in range(2)])
    return np.where(m > 0, m, m * 0.1)


def conv9_1x1(x, w):
    """1x1 卷积: out[k] = sum_c in[c]*w[k][c],不加 bias(与 C 源码一致)"""
    C, H, W = x.shape
    K = w.shape[0]
    out = (w @ x.reshape(C, H * W)).reshape(K, H, W)
    return out


def minyolo_forward(x, W):
    """x: (3,416,416) BGR [0,1]; W: 权重 dict -> (125,13,13)"""
    o = conv(x, W["conv1_w"], W["conv1_b"])
    o = maxpool_leaky(o)                      # 16,208,208
    o = conv(o, W["conv2_w"], W["conv2_b"])
    o = maxpool_leaky(o)                      # 32,104,104
    o = conv(o, W["conv3_w"], W["conv3_b"])
    o = maxpool_leaky(o)                      # 64,52,52
    o = conv(o, W["conv4_w"], W["conv4_b"])
    o = maxpool_leaky(o)                      # 128,26,26
    o = conv(o, W["conv5_w"], W["conv5_b"])
    o = maxpool_leaky(o)                      # 256,13,13
    o = conv(o, W["conv6_w"], W["conv6_b"])   # 512,13,13
    o = pad6(o)                               # 512,14,14
    o = slowpool6(o)                          # 512,13,13
    o = conv(o, W["conv7_w"], W["conv7_b"], leaky=True)   # 1024,13,13
    o = conv(o, W["conv8_w"], W["conv8_b"], leaky=True)   # 1024,13,13
    o = conv9_1x1(o, W["conv9_w"])            # 125,13,13
    return o

2.2 逐段解析

im2col:把卷积展开成矩阵乘

python 复制代码
def im2col(x, kh, kw):
    C, H, W = x.shape
    pad = kh // 2
    xp = np.pad(x, ((0,0),(pad,pad),(pad,pad)), mode="constant")
    ...
  • pad = kh//2 = 1,3×3 卷积上下左右各补 1 个 0,保持输出尺寸不变(same padding);
  • cols[:, dy, dx] = xp[:, dy:dy+H, dx:dx+W]:对每个 tap (dy,dx),把整个 feature map 平移后切片,得到 (C,kh,kw,H,W);
  • 最后 reshape 成 (C*kh*kw, H*W),每一列就是一个像素的 9 个采样点。

硬件对应 :RTL 里没有真做 im2col,而是用行缓冲 lb0~lb3 + 像素延迟链 shA/shB/shC 实时拼出这 9 个采样点(见第四章)。

conv:矩阵乘 + bias + Leaky

python 复制代码
cols = im2col(x, 3, 3)      # (C*9, H*W)
wm   = w.reshape(K, C*9)    # (K, C*9)
out  = (wm @ cols).reshape(K, H, W)
  • wm @ cols 就是标准 GEMM:每个输出通道 k,对所有输入通道 c 和所有 tap (dy,dx) 求 Σ x[c,h+dy-1,w+dx-1]*w[k,c,dy,dx];
  • bias 直接 broadcast 加到每个像素;
  • LeakyReLU:负值 ×0.1,正值不变。

硬件对应 :pe.v 里 9 个 16×16 乘法 → 3 路部分和 → 48bit 累加,就是这个 wm @ cols 的硬件展开。

maxpool_leaky:切片取 2×2

python 复制代码
a = x[:, 0::2, 0::2]   # 偶数行偶数列
b = x[:, 0::2, 1::2]   # 偶数行奇数列
c = x[:, 1::2, 0::2]   # 奇数行偶数列
d = x[:, 1::2, 1::2]   # 奇数行奇数列
m = np.maximum(np.maximum(a,b), np.maximum(c,d))
  • 用 ::2 切片把 feature map 拆成四个 2×2 象限,逐元素 max;
  • 结果再做 LeakyReLU。

硬件对应 :conv_core.v 池化模块 max4(d_prev_p1, d_prev_p0, d_cur_p1, d_cur_p0),双组行缓存存"上一行/当前行",每个输出像素取 4 个采样点做 max。

pad6:最邪门的一层

python 复制代码
out[:, 0:13, 0:13] = x
out[:, 0:13, 13] = x[:, :, 11]   # 右列 = 原第 11 列
out[:, 13, 0:13] = x[:, 11, :]   # 下行 = 原第 11 行
out[:, 13, 13]   = x[:, 11, 11]
  • 13×13 → 14×14,但补的那一行/列不是 0,而是复制原 feature map 的第 11 行/列;
  • 这是从 C 源码 minYOLO.cpp 逐位抄过来的,不要"聪明地"改成 0 padding------改了就和参考输出对不上。

硬件对应 :拆成独立的 pool_only 硬件层(pool6),RTL 输入布局要按 [pix][ch] 整块读(v6.4 修过这个错位 bug)。

slowpool6:stride=1 的池化

python 复制代码
m = np.maximum.reduce(
    [x[:, i:i+13, j:j+13] for i in range(2) for j in range(2)])
  • 14×14 → 13×13,2×2 窗口但 stride=1(不是 2);
  • 四个切片 i,j ∈ {0,1} 各是 13×13,逐元素 max。

硬件对应 :RTL 里 cfg1.bit5 = pstride1 = 1 进入这个分支。

conv9_1x1:无 bias

python 复制代码
out = (w @ x.reshape(C, H*W)).reshape(K, H, W)
  • 1×1 卷积没有空间求和,就是 out[k,h,w] = Σ_c in[c,h,w]*w[k,c];
  • 不加 bias(C 源码就是这么写的)。

硬件对应:kernel=0 时 PE 9 个 tap 里只有中心 tap w11 接权重,其余 8 个 tap 必须接 0(v6.2 修过这个污染 bug)。

顶层串联

输入 416×416×3 → 5 次 stride2 池化后 13×13 → conv6/pad6/slowpool6 保持 13×13 → conv7/8/9 不池化 → 输出 13×13×125。


三、定点量化与测试向量生成

这一章把 sw/quantize.py 核心函数和 sw/dump_testdata.py 完整贴出来,讲清楚 int16 量化数学和 UVM 测试向量怎么一键生成。

3.1 量化参数与层表

python 复制代码
AQ_IMG = 11         # 输入图像量化: a16 = round(x * 2^11), x in [0,1]
                    # 像素 0..255 -> 0..2047

# 硬件层表(conv6 拆为 conv6 + pool6(POOL_ONLY) 两硬件层)
LAYERS = [
    dict(name="conv1", in_ch=3,    out_ch=16,   h=416, w=416, k=3, bias=1, conv_act=0, pool=1, pstride=2, pact=1),
    dict(name="conv2", in_ch=16,   out_ch=32,   h=208, w=208, k=3, bias=1, conv_act=0, pool=1, pstride=2, pact=1),
    dict(name="conv3", in_ch=32,   out_ch=64,   h=104, w=104, k=3, bias=1, conv_act=0, pool=1, pstride=2, pact=1),
    dict(name="conv4", in_ch=64,   out_ch=128,  h=52,  w=52,  k=3, bias=1, conv_act=0, pool=1, pstride=2, pact=1),
    dict(name="conv5", in_ch=128,  out_ch=256,  h=26,  w=26,  k=3, bias=1, conv_act=0, pool=1, pstride=2, pact=1),
    dict(name="conv6", in_ch=256,  out_ch=512,  h=13,  w=13,  k=3, bias=1, conv_act=0, pool=0, pstride=2, pact=0),
    dict(name="pool6", in_ch=512,  out_ch=512,  h=14,  w=14,  k=1, bias=0, conv_act=0, pool=1, pstride=1, pact=1, pool_only=1),
    dict(name="conv7", in_ch=512,  out_ch=1024, h=13,  w=13,  k=3, bias=1, conv_act=1, pool=0, pstride=2, pact=0),
    dict(name="conv8", in_ch=1024, out_ch=1024, h=13,  w=13,  k=3, bias=1, conv_act=1, pool=0, pstride=2, pact=0),
    dict(name="conv9", in_ch=1024, out_ch=125,  h=13,  w=13,  k=1, bias=0, conv_act=0, pool=0, pstride=2, pact=0),
]

解析:

  • 每层的 bias/conv_act/pool/pstride/pact 直接对应 RTL cfg1 的位域;
  • pool_only=1 表示这一层硬件只做池化(pool6),不做卷积;
  • conv9 k=1,硬件 kernel=0,只接中心 tap。

3.2 饱和与 Leaky 定点近似

python 复制代码
def sat16(x):
    return np.clip(x, -32768, 32767).astype(np.int16)

def leaky16(x):
    """x int16 -> int16,负值 ×0.1 定点近似(与硬件一致)"""
    x = x.astype(np.int32)
    neg = (x * 6554) >> 16
    return np.where(x < 0, neg, x).astype(np.int16)

解析:

  • sat16:int16 溢出截断,硬件 quantize48 函数里就是这个逻辑;
  • leaky16:0.1 ≈ 6554/65536,负值乘 6554 再右移 16 位,和 RTL leaky16_v 完全一致。

3.3 定点卷积核心

python 复制代码
def quant_conv(w_f, b_f, a16, WQ, SHIFT, bias_en, AQ_IN):
    """int64 精确累加,模拟 DSP48E1 48bit 累加"""
    w16 = np.round(w_f * (2 ** WQ)).astype(np.int32)   # (K,C,kh,kw)
    K, C, kh, kw = w16.shape
    H, W = a16.shape[1], a16.shape[2]

    pad = kh // 2
    ap = np.pad(a16.astype(np.int32), ((0,0),(pad,pad),(pad,pad)), mode="constant")
    cols = np.empty((C, kh, kw, H, W), dtype=np.int32)
    for dy in range(kh):
        for dx in range(kw):
            cols[:, dy, dx] = ap[:, dy:dy+H, dx:dx+W]
    cols = cols.reshape(C*kh*kw, H*W).astype(np.float64)
    wm = w16.reshape(K, C*kh*kw).astype(np.float64)
    acc = (wm @ cols).astype(np.int64)                # int64 精确

    if bias_en:
        acc += np.round(b_f * (2 ** (WQ + AQ_IN))).astype(np.int64).reshape(K, 1)

    out = (acc + (1 << (SHIFT - 1))) >> SHIFT          # 四舍五入右移
    return sat16(out.reshape(K, H, W)), w16

解析:

  • w16 = round(w * 2^WQ):权重按每层 WQ 缩放,WQ 由 floor(log2(32767/wmax)) 决定;
  • 用 float64 承载 int32 矩阵乘再 cast int64,是为了在 Python 端拿到和硬件完全一致的精确累加结果(numpy int32 会溢出);
  • acc += round(b * 2^(WQ+AQ_IN)):bias 缩放到和累加器同尺度;
  • (acc + 2^(SHIFT-1)) >> SHIFT:四舍五入右移(加半个 LSB 再截断)。

3.4 dump_testdata.py:生成 UVM 测试向量(完整)

python 复制代码
# -*- coding: utf-8 -*-
"""
UVM 测试向量生成:独立重算量化链,逐层导出
  uvm/testdata/<layer>/{input.bin, weights.bin, bias.bin, expected.bin, cfg.json}
"""
import os, sys, json
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from PIL import Image
from minyolo_net import conv, maxpool_leaky, pad6, slowpool6, conv9_1x1
from quantize import (LAYERS, sat16, leaky16, quant_conv, quant_pool_only,
                      AQ_IMG, NPZ, IMG, hw_layout_w, hw_layout_out)

REPRO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TD = os.path.join(REPRO, "uvm", "testdata")
W = dict(np.load(NPZ))

def reg_cfg(L, cfg):
    kern = 1 if L["k"] == 3 else 0
    cfg1 = (kern | (L["bias"] << 1) | (L["conv_act"] << 2) | (L["pool"] << 4)
            | ((1 if L["pstride"] == 1 else 0) << 5) | (L["pact"] << 7))
    if L.get("pool_only"):
        cfg1 |= (1 << 8)
    return {"c_in": L["in_ch"], "c_out": L["out_ch"], "h": L["h"], "w": L["w"],
            "cfg0": (L["in_ch"] << 16) | L["out_ch"],
            "cfg1": cfg1,
            "cfg2": ((L["h"] - 1) << 9) | (L["w"] - 1),
            "a_shift": cfg.get("a_shift", 0),
            "w_offset": cfg.get("w_offset", -1),
            "b_offset": cfg.get("b_offset", -1)}

def main():
    os.makedirs(TD, exist_ok=True)
    im = Image.open(IMG).convert("RGB")
    rgb = np.asarray(im, dtype=np.uint8)
    x = np.zeros((3, 416, 416), dtype=np.float32)
    for c in range(3):
        x[2-c] = rgb[:, :, c].astype(np.float32) / 255.0   # RGB -> BGR

    a16 = sat16(np.round(x * (2 ** AQ_IMG)))
    aq_in = AQ_IMG
    w_off = b_off = 0
    for L in LAYERS:
        nm = L["name"]
        a_in = a16.copy()
        if L.get("pool_only"):
            # pool6: 13x13 -> 14x14 pad6
            C = a16.shape[0]
            ap = np.zeros((C, 14, 14), dtype=a16.dtype)
            ap[:, 0:13, 0:13] = a16
            ap[:, 0:13, 13] = a16[:, :, 11]
            ap[:, 13, 0:13] = a16[:, 11, :]
            ap[:, 13, 13]   = a16[:, 11, 11]
            out16 = quant_pool_only(ap, L["pstride"], L["pact"])
            cfg = dict(a_shift=0, w_offset=-1, b_offset=-1)
            # pool_only 输入硬件布局 [pix][ch]
            Cc, Hp, Wp = ap.shape
            inp_hw = np.zeros((Cc//16, Hp*Wp, 16), dtype=ap.dtype)
            for b in range(Cc//16):
                inp_hw[b] = ap[b*16:(b+1)*16].reshape(16, -1).T.reshape(Hp*Wp, 16)
            save_layer(L, inp_hw, out16, None, None, cfg)
        else:
            w_f = W["conv%s_w" % nm[-1]]
            b_f = W["conv%s_b" % nm[-1]]
            wmax = np.abs(w_f).max()
            WQ = int(np.floor(np.log2(32767.0/wmax))) if wmax > 0 else 15
            out16, w16 = quant_conv(w_f, b_f, a_in, WQ,
                                    max(WQ+aq_in, 1), bool(L["bias"]), aq_in)
            b32 = np.round(b_f * (2**(WQ+aq_in))).astype(np.int32) if L["bias"] else None
            if L["conv_act"] == 1: out16 = leaky16(out16)
            if L["pool"]:           out16 = quant_pool_only(out16, L["pstride"], L["pact"])
            cfg = dict(a_shift=max(WQ+aq_in,1), w_offset=w_off,
                       b_offset=b_off if L["bias"] else -1)
            save_layer(L, a_in, out16, w16, b32, cfg)
            w_off += hw_layout_w(w16).size * 2
            if L["bias"]: b_off += b32.size * 4
        a16 = out16

def save_layer(L, a_in, expected, w16, b32, cfg):
    d = os.path.join(TD, L["name"])
    os.makedirs(d, exist_ok=True)
    a_in.astype("<i2").tofile(os.path.join(d, "input.bin"))
    hw_layout_out(expected).tofile(os.path.join(d, "expected.bin"))
    if w16 is not None:
        hw_layout_w(w16).astype("<i2").tofile(os.path.join(d, "weights.bin"))
    if b32 is not None:
        b32.astype("<i4").tofile(os.path.join(d, "bias.bin"))
    with open(os.path.join(d, "cfg.json"), "w") as f:
        json.dump(reg_cfg(L, cfg), f, indent=1)

if __name__ == "__main__":
    main()

3.5 关键设计点

cfg0 位域编码(v6.7 修复后)

python 复制代码
"cfg0": (L["in_ch"] << 16) | L["out_ch"]
  • 高 16 位 c_in,低 16 位 c_out;
  • v6.7 之前这里写的是 (c_in<<16)|(c_out<<8),和 RTL 解码 cfg0[15:0]=c_out 不一致,上板必挂。

cfg1 位域

python 复制代码
cfg1 = (kern | (bias<<1) | (conv_act<<2) | (pool<<4)
      | (pstride1<<5) | (pact<<7))
if pool_only: cfg1 |= (1<<8)

和 RTL 位域一一对应。

硬件布局 hw_layout_w

权重从 (K,C,3,3) 重排成 (K//16, C, 16, 9)------16 个输出通道一组,硬件一拍能搬一组权重到 PE。

pool_only 输入布局

python 复制代码
inp_hw[b] = ap[b*16:(b+1)*16].reshape(16,-1).T.reshape(Hp*Wp, 16)
  • 卷积层输入是 [ch][pix](RTL 按通道步进读);
  • pool_only 层输入是 [pix][ch](RTL 按像素整块读 16 通道);
  • v6.4 修过这个错位------旧版统一用 [ch][pix],pool6 完全读错。

3.6 运行

bash 复制代码
cd repro/sw
python dump_testdata.py

输出 uvm/testdata/<layer>/ 下 5 个文件:cfg.json / input.bin / weights.bin / bias.bin / expected.bin。


四、RTL 架构设计详解

这一章把 pe.v、pool_ram.v 完整源码,以及 conv_core.v 的核心模块全部贴出来。

4.1 配置解码

verilog 复制代码
localparam CB = 16;   // 每块输入通道数(= PE 数)

// cfg0: [31:16]=c_in, [15:0]=c_out
wire [15:0] c_in  = cfg0[31:16];
wire [15:0] c_out = cfg0[15:0];   // v6.7: 16bit,c_out 最大 1024,8bit 会溢出

// cfg1 位域
wire        kernel   = cfg1[0];   // 1=3x3, 0=1x1
wire        bias_en  = cfg1[1];
wire        conv_act = cfg1[2];   // 卷积后接 Leaky
wire        pool_en  = cfg1[4];
wire        pool_stride = cfg1[5]; // 1=stride1 (slowpool6), 0=stride2
wire        pool_act = cfg1[7];
wire        pool_only = cfg1[8];  // 纯池化层 (pool6)

// cfg2: [17:9]=h-1, [8:0]=w-1
wire [8:0]  H = cfg2[17:9] + 9'd1;
wire [8:0]  W = cfg2[8:0] + 9'd1;

// 输出尺寸
wire [8:0]  Hp = pool_stride ? (H - 9'd1) : (H >> 1);
wire [8:0]  Wp = pool_stride ? (W - 9'd1) : (W >> 1);
wire [8:0]  Hout = pool_en ? Hp : H;
wire [8:0]  Wout = pool_en ? Wp : W;

// 块数
wire [15:0] nblk = (c_out + 15) >> 4;    // 输出通道块数
wire [15:0] ncb  = (c_in + CB - 1) >> 4;  // 输入通道块数

解析 :所有配置全部是 wire 组合解码,不占寄存器------这是"一层一配置"设计的关键。

4.2 主状态机 10 态

verilog 复制代码
localparam S_IDLE=0, S_LOCK=1, S_WRSETUP=2, S_BIAS=3, S_WLOAD=4,
           S_STREAM=5, S_STREAM_SETUP=6, S_REDUCE=7, S_POOL=8, S_DONE=9;
reg [3:0] state;

always @(posedge core_clk or negedge core_rst_n) begin
    if (!core_rst_n) begin
        state <= S_IDLE; busy <= 0; done <= 0;
    end else case (state)
        S_IDLE:   if (start) state <= S_LOCK;
        S_LOCK:   state <= S_WRSETUP;
        S_WRSETUP: state <= (nblk_done) ? S_DONE : S_BIAS;
        S_BIAS:   if (bias_done) state <= S_WLOAD;
        S_WLOAD:  if (w_done)    state <= S_STREAM_SETUP;
        S_STREAM_SETUP: state <= S_STREAM;
        S_STREAM: if (pix_done) state <= (ncb>1) ? S_REDUCE : S_WRSETUP;
        S_REDUCE: if (red_done) state <= S_WRSETUP;
        S_DONE:   begin done <= 1; state <= S_IDLE; end
    endcase
end

解析:

  • nblk_done:所有输出通道块跑完 → S_DONE;
  • ncb=1(单输入块,绿路径):S_STREAM 直接 → S_WRSETUP;
  • ncb>1(多输入块,红路径):S_STREAM → S_REDUCE → S_WRSETUP。

4.3 行缓冲:4 个 1D 分布式 RAM

verilog 复制代码
localparam LW = 420;
(* ram_style="distributed" *) reg [15:0] lb0 [0:LW-1];
(* ram_style="distributed" *) reg [15:0] lb1 [0:LW-1];
(* ram_style="distributed" *) reg [15:0] lb2 [0:LW-1];
(* ram_style="distributed" *) reg [15:0] lb3 [0:LW-1];

wire [1:0] sl0 = (r_in + 2) % 4;   // 上上行
wire [1:0] sl1 = (r_in + 3) % 4;   // 上一行
wire [1:0] sl2 = r_in % 4;         // 当前行(写)

wire [15:0] lb0_rd = lb0[ch_sub_r*LW + col_x];
wire [15:0] lb1_rd = lb1[ch_sub_r*LW + col_x];
wire [15:0] lb2_rd = lb2[ch_sub_r*LW + col_x];
wire [15:0] lb3_rd = lb3[ch_sub_r*LW + col_x];
wire [15:0] lb_rdA = (sl0==0) ? lb0_rd : (sl0==1) ? lb1_rd
                   : (sl0==2) ? lb2_rd : lb3_rd;
wire [15:0] lb_rdB = (sl1==0) ? lb0_rd : (sl1==1) ? lb1_rd
                   : (sl1==2) ? lb2_rd : lb3_rd;

// 行缓冲写(独立无复位 always)
always @(posedge core_clk) begin
    if (lb_consume) begin
        case (r_in % 4)
            2'd0: lb0[ch_sub_r*LW + col_x] <= rd_hold[rd_hcnt*16 +: 16];
            2'd1: lb1[ch_sub_r*LW + col_x] <= rd_hold[rd_hcnt*16 +: 16];
            2'd2: lb2[ch_sub_r*LW + col_x] <= rd_hold[rd_hcnt*16 +: 16];
            default: lb3[ch_sub_r*LW + col_x] <= rd_hold[rd_hcnt*16 +: 16];
        endcase
    end
end

解析:

  • v6.4 之前是 3D 数组,Vivado 报 Synth 8-5856 not supported;
  • 改成 4 个独立 1D RAM 后正确推 LUTRAM;
  • 写 always 无异步复位:满足 Vivado RAM 推断规则。

4.4 9-tap 延迟链

verilog 复制代码
reg [15:0] shA [0:31];
reg [15:0] shB [0:31];
reg [15:0] shC [0:31];

always @(posedge core_clk or negedge core_rst_n) begin
    if (!core_rst_n) begin
        for (int i = 0; i < 32; i++) begin
            shA[i] <= 0; shB[i] <= 0; shC[i] <= 0;
        end
    end else if (lb_consume) begin
        shA[0] <= lb_rdA;
        shB[0] <= lb_rdB;
        shC[0] <= rd_hold[rd_hcnt*16 +: 16];
        for (int i = 1; i < 32; i++) begin
            shA[i] <= shA[i-1];
            shB[i] <= shB[i-1];
            shC[i] <= shC[i-1];
        end
    end
end

wire [15:0] w00 = shA[2*blk_sz_r-1];
wire [15:0] w01 = shA[  blk_sz_r-1];
wire [15:0] w02 = lb_rdA;
wire [15:0] w10 = shB[2*blk_sz_r-1];
wire [15:0] w11 = shB[  blk_sz_r-1];
wire [15:0] w12 = lb_rdB;
wire [15:0] w20 = shC[2*blk_sz_r-1];
wire [15:0] w21 = shC[  blk_sz_r-1];
wire [15:0] w22 = rd_hold[rd_hcnt*16 +: 16];

解析:这就是 im2col 的硬件实时展开------Python 先把整个 feature map 展开,硬件边读边拼。

4.5 PE 阵列例化(16 个并行)

verilog 复制代码
wire pix_ok = (r_in >= 2) && (r_in <= H+1) && (col_x >= 2) && (col_x <= W+1) && !pool_only;

wire finalize_c = pe_pipe[2] && !pool_only && (ncb==1) && (ch_sub_p2 == blk_sz_r-1);
wire ps_cap     = pe_pipe[2] && !pool_only && (ncb>1) && (ch_sub_p2 == blk_sz_r-1);
wire pe_en  = (state==S_STREAM) && rd_hold_vld && pix_ok && !ps_wr_active && !ps_cap;
wire pe_clr = pe_en && (ch_sub_r == 0);

wire [47:0] acc [0:15];
genvar g;
generate
    for (g = 0; g < 16; g++) begin: pe_arr
        pe u_pe (
            .clk(core_clk), .rst_n(core_rst_n),
            .en(pe_en), .clr(pe_clr),
            .a00(w00), .a01(w01), .a02(w02),
            .a10(w10), .a11(w11), .a12(w12),
            .a20(w20), .a21(w21), .a22(w22),
            .w00(wreg[g*9+0]), .w01(wreg[g*9+1]), .w02(wreg[g*9+2]),
            .w10(wreg[g*9+3]), .w11(wreg[g*9+4]), .w12(wreg[g*9+5]),
            .w20(wreg[g*9+6]), .w21(wreg[g*9+7]), .w22(wreg[g*9+8]),
            .acc(acc[g])
        );
    end
endgenerate

4.6 量化三级流水

verilog 复制代码
reg [15:0] q16 [0:15];
reg [15:0] act16 [0:15];
reg fin_q, fin_q2, fin_q3;

always @(posedge core_clk or negedge core_rst_n) begin
    if (!core_rst_n) begin
        fin_q <= 0; fin_q2 <= 0; fin_q3 <= 0;
        for (int k = 0; k < 16; k++) begin
            q16[k] <= 0; act16[k] <= 0;
        end
    end else begin
        fin_q  <= finalize_c || red_quant;
        fin_q2 <= fin_q && !red_quant;
        fin_q3 <= fin_q2 && !red_quant;

        if (fin_q && !red_quant_q && (ncb == 1)) begin
            for (int k = 0; k < 16; k++)
                q16[k] <= quantize48(acc[k] +
                    (bias_en ? {{16{breg[k][31]}},breg[k]} : 48'd0),
                    a_shift);
        end
        if (fin_q2) begin
            for (int k = 0; k < 16; k++)
                act16[k] <= (conv_act == 1) ? leaky16_v(q16[k]) : q16[k];
        end
    end
end

4.7 池化双组行缓存

verilog 复制代码
reg pp_grp;
reg pool_cap_q, pool_cap_q2, pool_cap_q3, pool_cap_q4;

always @(posedge core_clk or negedge core_rst_n)
    if (!core_rst_n) begin
        pool_cap_q <= 0; pool_cap_q2 <= 0; pool_cap_q3 <= 0; pool_cap_q4 <= 0;
    end else begin
        pool_cap_q  <= pool_cap;
        pool_cap_q2 <= pool_cap_q;
        pool_cap_q3 <= pool_cap_q2;
        pool_cap_q4 <= pool_cap_q3;
    end

wire pp_wr0 = pool_cap_q && (pp_grp==1'b0 || (pool_stride==1'b0 && !pcy[0]));
wire pp_wr1 = pool_cap_q && (pp_grp==1'b1 || (pool_stride==1'b0 && !pcy[0]));

解析 :原来用 pprev <= pcur 整行复制要 13440 个 FF;改成双组 RAM + 行指针后 1 个 FF 搞定。

4.8 写通道:16ch 打包成 64-bit

verilog 复制代码
reg [15:0] ogrp [0:15];
reg ogrp_vld0, ogrp_vld1;
reg wsel;
reg [3:0] wph;

always @(posedge core_clk or negedge core_rst_n) begin
    if (!core_rst_n) begin
        ogrp_vld0 <= 0; ogrp_vld1 <= 0;
        wsel <= 0; wph <= 0; wr_valid <= 0; wr_cnt <= 0;
    end else begin
        wr_valid <= 0;
        if (state == S_WRSETUP) begin
            ogrp_vld0 <= 0; ogrp_vld1 <= 0; wsel <= 0; wph <= 0;
        end

        if (ogrp_ld_conv || ogrp_ld_pool) begin
            if (!ogrp_vld0) begin
                for (int k = 0; k < 16; k++)
                    ogrp[k] <= ogrp_ld_conv ? act16[k] : pout_act[k];
                ogrp_vld0 <= 1;
            end else if (!ogrp_vld1) begin
                ogrp_vld1 <= 1; wsel <= ~wsel;
                for (int k = 0; k < 16; k++)
                    ogrp[k] <= ogrp_ld_conv ? act16[k] : pout_act[k];
            end
        end

        if (ogrp_vld0 && !wph[2]) begin
            case (wph[1:0])
                2'd0: wr_data <= {ogrp[3], ogrp[2], ogrp[1], ogrp[0]};
                2'd1: wr_data <= {ogrp[7], ogrp[6], ogrp[5], ogrp[4]};
                2'd2: wr_data <= {ogrp[11], ogrp[10], ogrp[9], ogrp[8]};
                2'd3: wr_data <= {ogrp[15], ogrp[14], ogrp[13], ogrp[12]};
            endcase
        end
    end
end

解析:一个像素 16 个 int16 = 32 字节,64-bit 总线写 4 拍。双缓冲让下一像素在写当前像素时就填充,不 bubble。

4.9 pe.v:完整 76 行

verilog 复制代码
`timescale 1ns/1ps
module pe (
    input  wire        clk,
    input  wire        rst_n,
    input  wire        en,
    input  wire        clr,
    input  wire [15:0] a00, a01, a02,
    input  wire [15:0] a10, a11, a12,
    input  wire [15:0] a20, a21, a22,
    input  wire [15:0] w00, w01, w02,
    input  wire [15:0] w10, w11, w12,
    input  wire [15:0] w20, w21, w22,
    output wire [47:0] acc
);
    reg [31:0] p0, p1, p2, p3, p4, p5, p6, p7, p8;
    reg [32:0] s0, s1, s2;
    reg [47:0] acc_r;
    reg [2:0]  en_q, clr_q;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            p0<=0; p1<=0; p2<=0; p3<=0; p4<=0;
            p5<=0; p6<=0; p7<=0; p8<=0; en_q<=0;
        end else begin
            if (en) begin
                p0 <= $signed(w00) * $signed(a00);
                p1 <= $signed(w01) * $signed(a01);
                p2 <= $signed(w02) * $signed(a02);
                p3 <= $signed(w10) * $signed(a10);
                p4 <= $signed(w11) * $signed(a11);
                p5 <= $signed(w12) * $signed(a12);
                p6 <= $signed(w20) * $signed(a20);
                p7 <= $signed(w21) * $signed(a21);
                p8 <= $signed(w22) * $signed(a22);
            end
            en_q <= {en_q[1:0], en};
        end
    end

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin s0<=0; s1<=0; s2<=0; clr_q<=0; end
        else begin
            if (en_q[0]) begin
                s0 <= $signed(p0) + $signed(p1) + $signed(p2);
                s1 <= $signed(p3) + $signed(p4) + $signed(p5);
                s2 <= $signed(p6) + $signed(p7) + $signed(p8);
            end
            clr_q <= {clr_q[1:0], clr};
        end
    end

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) acc_r <= 0;
        else if (en_q[1]) begin
            if (clr_q[1])
                acc_r <= {{15{s0[32]}},s0} + {{15{s1[32]}},s1} + {{15{s2[32]}},s2};
            else
                acc_r <= acc_r + {{15{s0[32]}},s0} + {{15{s1[32]}},s1} + {{15{s2[32]}},s2};
        end
    end
    assign acc = acc_r;
endmodule

4.10 pool_ram.v:完整 27 行

verilog 复制代码
module pool_ram #(
    parameter AW = 10,
    parameter DW = 16
) (
    input                    clk,
    input                    we,
    input  [AW-1:0]          wa,
    input  [DW-1:0]          wd,
    input  [AW-1:0]          ra,
    output wire [DW-1:0]     rd
);
    (* ram_style = "distributed" *) reg [DW-1:0] mem [0:(1<<AW)-1];
    always @(posedge clk) begin
        if (we) mem[wa] <= wd;
    end
    assign rd = mem[ra];
endmodule

五、UVM 验证环境搭建

5.1 tb_top.sv:完整 44 行

systemverilog 复制代码
`timescale 1ns/1ps
module tb_top;
    import uvm_pkg::*;
    import conv_pkg::*;

    logic clk = 0;
    always #5 clk = ~clk;           // 100MHz

    conv_if #(.AW(32), .DW(64)) u_if (.clk(clk));

    conv_core #(.AW(32), .DW(64)) u_core (
        .core_clk(clk), .core_rst_n(u_if.rst_n),
        .start(u_if.start),
        .cfg0(u_if.cfg0), .cfg1(u_if.cfg1), .cfg2(u_if.cfg2),
        .in_base(u_if.in_base), .out_base(u_if.out_base),
        .ps_base(u_if.ps_base),
        .w_base(u_if.w_base), .b_base(u_if.b_base),
        .a_shift(u_if.a_shift),
        .busy(u_if.busy), .done(u_if.done),
        .rd_req(u_if.rd_req), .rd_addr(u_if.rd_addr), .rd_len(u_if.rd_len),
        .rd_ack(u_if.rd_ack), .rd_ready(u_if.rd_ready),
        .rd_valid(u_if.rd_valid), .rd_data(u_if.rd_data),
        .wr_req(u_if.wr_req), .wr_addr(u_if.wr_addr), .wr_len(u_if.wr_len),
        .wr_ack(u_if.wr_ack), .wr_ready(u_if.wr_ready),
        .wr_valid(u_if.wr_valid), .wr_data(u_if.wr_data)
    );

    initial begin
        uvm_config_db#(virtual conv_if)::set(null, "*", "vif", u_if);
        run_test("conv_base_test");
    end

    initial begin
        #(64'd10000000000);
        $display("TB_TIMEOUT @%0t", $time);
        $finish;
    end
endmodule

5.2 run_full.do:一条命令跑全量回归

tcl 复制代码
onerror { resume }
vlib work
vmap work work
vmap uvm E:/fpga/questasim/uvm-1.2

vlog -sv -L uvm +incdir+E:/fpga/questasim/uvm-1.2/src \
     conv_if.sv conv_pkg.sv tb_top.sv \
     ../../rtl/conv_core.v ../../rtl/pe.v ../../rtl/pool_ram.v

vsim -c -L uvm -sv_lib E:/fpga/questasim/uvm-1.2/win64/uvm_dpi \
     -suppress 3829 +L=10 \
     -do "run -all; quit" work.tb_top

PowerShell 跑回归:

powershell 复制代码
cd repro\uvm\tb
Get-Process | ?{ $_.ProcessName -match "vsim|vlog" } | Stop-Process -Force
if (Test-Path work) { Remove-Item work -Recurse -Force }
vsim -c -do "do run_full.do" > transcript.txt 2>&1

5.3 conv_if.sv:DDR 模型 + BFM

systemverilog 复制代码
interface conv_if #(parameter AW=32, parameter DW=64) (input logic clk);
    logic        rst_n, start;
    logic [31:0] cfg0, cfg1, cfg2, in_base, out_base, w_base, b_base, ps_base;
    logic [7:0]  a_shift;
    logic        busy, done;

    logic        rd_req, rd_ack, rd_valid;
    logic [AW-1:0]  rd_addr;
    logic [19:0]    rd_len;
    logic [DW-1:0]  rd_data;

    logic        wr_req, wr_ack, wr_valid;
    logic [AW-1:0]  wr_addr;
    logic [19:0]    wr_len;
    logic [DW-1:0]  wr_data;

    // DDR:64M 个 64-bit 字(=512MB 可寻址空间)
    reg [63:0] dram [0:8388607];

    function automatic [7:0] rbyte(input int a);
        rbyte = dram[a >> 3][((a & 7)*8) +: 8];
    endfunction

    task automatic wbyte(input int a, input [7:0] d);
        dram[a >> 3] = (dram[a>>3] & ~(64'hFF << ((a&7)*8)))
                     | (64'(d) << ((a&7)*8));
    endtask

    task automatic load_file(input string fname, input int base, input int nbytes);
        int fp = $fopen(fname, "rb");
        int n = $fread(dram, fp, base >> 3, nbytes >> 3);
        $fclose(fp);
    endtask

    always @(posedge clk) begin
        rd_ack <= rd_req;
        if (rd_req) begin
            rd_valid <= 1;
            rd_data  <= dram[rd_addr >> 3];
        end
    end
endinterface

5.4 conv_seq_item

systemverilog 复制代码
class conv_seq_item extends uvm_sequence_item;
    `uvm_object_utils(conv_seq_item)
    string layer;
    string data_dir;
    int    c_in, c_out, h, w;
    int    cfg0, cfg1, cfg2;
    int    a_shift;
    int    w_offset, b_offset;
    int    in_bytes, w_bytes, b_bytes, out_bytes;
    bit    pool_only;
    function new(string name = "conv_seq_item");
        super.new(name);
    endfunction
endclass

5.5 conv_driver

systemverilog 复制代码
class conv_driver extends uvm_driver #(conv_seq_item);
    `uvm_component_utils(conv_driver)
    virtual conv_if vif;
    uvm_analysis_port #(conv_seq_item) ap;

    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        if (!uvm_config_db#(virtual conv_if)::get(this, "", "vif", vif))
            `uvm_fatal("NOVIF", "no vif")
        ap = new("ap", this);
    endfunction

    task run_phase(uvm_phase phase);
        conv_seq_item item;
        forever begin
            seq_item_port.get_next_item(item);
            drive_item(item);
            seq_item_port.item_done();
            ap.write(item);
        end
    endtask

    task drive_item(conv_seq_item item);
        vif.load_file({item.data_dir,"/",item.layer,"/input.bin"},
                      32'h000000, item.in_bytes);
        if (item.w_offset >= 0)
            vif.load_file({item.data_dir,"/",item.layer,"/weights.bin"},
                          32'h400000+item.w_offset, item.w_bytes);
        if (item.b_bytes > 0)
            vif.load_file({item.data_dir,"/",item.layer,"/bias.bin"},
                          32'h2800000+item.b_offset, item.b_bytes);

        vif.in_base  = 32'h200000;
        vif.out_base = 32'h2900000;
        vif.ps_base  = 32'h2C00000;
        vif.w_base   = 32'h400000 + item.w_offset;
        vif.b_base   = 32'h2800000 + item.b_offset;
        vif.cfg0 = item.cfg0; vif.cfg1 = item.cfg1; vif.cfg2 = item.cfg2;
        vif.a_shift = item.a_shift;

        @(posedge vif.clk); vif.start = 1;
        @(posedge vif.clk); vif.start = 0;
        wait (vif.done);
        repeat (8) @(posedge vif.clk);
    endtask
endclass

5.6 conv_scoreboard

systemverilog 复制代码
class conv_scoreboard extends uvm_scoreboard;
    `uvm_component_utils(conv_scoreboard)
    virtual conv_if vif;
    uvm_analysis_imp #(conv_seq_item, conv_scoreboard) ap_imp;
    int total_layers, pass_layers, fail_layers;

    function void write(conv_seq_item item);
        compare_layer(item);
    endfunction

    task compare_layer(conv_seq_item item);
        reg [7:0] exp_mem [0:4194303];
        int fp, n, i, mism, maxdiff;
        reg [15:0] e16, g16;
        int sd;

        fp = $fopen({item.data_dir,"/",item.layer,"/expected.bin"}, "rb");
        n = $fread(exp_mem, fp, 0, item.out_bytes);
        $fclose(fp);

        mism = 0;
        for (i = 0; i < item.out_bytes; i++) begin
            if (vif.rbyte(32'h2900000 + i) !== exp_mem[i])
                mism = mism + 1;
        end

        maxdiff = 0;
        for (i = 0; i < item.out_bytes/2; i++) begin
            e16 = {exp_mem[i*2+1], exp_mem[i*2]};
            g16 = {vif.rbyte(32'h2900000+i*2+1), vif.rbyte(32'h2900000+i*2)};
            sd = $signed(e16) - $signed(g16);
            if (sd < 0) sd = -sd;
            if (sd > maxdiff) maxdiff = sd;
        end

        total_layers++;
        if (mism == 0) begin
            pass_layers++;
            `uvm_info("SCB", $sformatf("[PASS] %0s bytes=%0d mism=0 maxdiff=%0d",
                       item.layer, item.out_bytes, maxdiff), UVM_LOW)
        end else begin
            fail_layers++;
            `uvm_error("SCB", $sformatf("[FAIL] %0s mism=%0d maxdiff=%0d",
                       item.layer, mism, maxdiff));
        end
    endtask

    function void report_phase(uvm_phase phase);
        `uvm_info("SCB", $sformatf("SUMMARY layers=%0d pass=%0d fail=%0d",
                   total_layers, pass_layers, fail_layers), UVM_NONE)
    endfunction
endclass

5.7 30 个测试点(企业级分解)

维度 编号 测试点
功能 F1~F8 绿路径/红路径/池化/1x1/pool_only/bias 开关/边界尺寸
接口 I1~I6 AXI 握手/背压/突发长度/越界/未对齐地址
量化 Q1~Q3 饱和/移位/Leaky 近似
异常 E1~E3 非法 cfg/超时/DDR 越界
时序 T1~T3 多驱动/组合环/跨时钟域
覆盖率 C1~C3 状态机/分支/翻转
上板 B1~B4 PS 配置/DMA/SD 卡/UART

5.8 回归结果

复制代码
[PASS] conv1  bytes=1384448 mism=0 maxdiff=0
[PASS] conv2  bytes=692224  mism=0 maxdiff=0
[PASS] conv3  bytes=346112  mism=0 maxdiff=0
[PASS] conv4  bytes=173056  mism=0 maxdiff=0
[PASS] conv5  bytes=86528   mism=0 maxdiff=0
[PASS] conv6  bytes=43264   mism=0 maxdiff=0
[PASS] pool6  bytes=173056  mism=0 maxdiff=0
[PASS] conv7  bytes=173056  mism=0 maxdiff=0
[PASS] conv8  bytes=173056  mism=0 maxdiff=0
[PASS] conv9  bytes=43264   mism=0 maxdiff=0
SUMMARY layers=10 pass=10 fail=0

六、Zynq 上板指南

6.1 硬件连接

  • 板卡:正点原子领航者 Zynq-7020(XC7Z020CLG400-2)
  • PL 时钟:FCLK_CLK0 = 100MHz(PS PLL 输出)
  • DDR:1GB DDR3,PS 端直接寻址
  • SD 卡:FAT32,放 weights/inputs/refs/layers.json
  • UART1:115200,打印检测结果

6.2 Vivado BD 结构

复制代码
Zynq PS7 (DDR3/UART1/FCLK_CLK0 100MHz/M_AXI_GP0)
        │
        ▼
     AXI Interconnect
        │
        ├── S_AXI(AXI4-Lite) ──► axi_wrapper.cfg 寄存器组
        │
        └── M00_AXI(AXI4 Master)──► axi_wrapper.rd/wr master ──► PS DDR
tcl 复制代码
set_property -dict [list \
  CONFIG.PCW_FPGA0_PERIPHERAL_FREQMHZ {100} \
  CONFIG.PCW_UART1_PERIPHERAL_ENABLE {1} \
  CONFIG.PCW_UIPARAM_DDR_DQS_TO_CLK_DELAY_0 {0.400} \
] [get_bd_cells ps7_0]

connect_bd_net [get_bd_pins ps7/FCLK_CLK0] [get_bd_pins axi_ic/ACLK]
connect_bd_net [get_bd_pins ps7/FCLK_CLK0] [get_bd_pins conv_core_0/S_AXI_ACLK]

6.3 寄存器映射(PS 侧)

地址 名称 说明
0x00 ctrl bit0=start, bit1=软复位
0x04 status bit0=busy, bit1=done(写 1 清)
0x08 cfg0 (c_in<<16)|c_out
0x0C cfg1 bit0=kern3, bit1=bias, bit2=conv_act, bit4=pool, bit5=pstride1, bit7=pact, bit8=pool_only
0x10 cfg2 (h-1)<<9 | (w-1)
0x14 a_shift 每层右移位数
0x18 in_base 输入 DDR 地址
0x1C out_base 输出 DDR 地址
0x20 ps_base 部分和 DDR 地址
0x24 w_base 权重 DDR 地址
0x28 b_base bias DDR 地址

AXI_BASE = 0x43C00000。

6.4 DDR 地址布局

区 地址 说明
输入 0x02000000 当前层输入 int16
权重 0x00400000 + w_offset 当前层权重
bias 0x02800000 + b_offset 当前层 bias
输出 0x02900000 当前层输出
部分和 0x02C00000 多块归约暂存

6.5 Vitis C 程序流程

c 复制代码
// 1. 从 SD 卡加载 layers.json + 所有权重到 DDR
load_sd_to_ddr();

// 2. 逐层跑
for (int layer = 0; layer < 10; layer++) {
    axi_wr(cfg0, layers[layer].cfg0);
    axi_wr(cfg1, layers[layer].cfg1);
    axi_wr(cfg2, layers[layer].cfg2);
    axi_wr(a_shift, layers[layer].a_shift);
    axi_wr(in_base,  INPUT_DDR);
    axi_wr(out_base, OUTPUT_DDR);
    axi_wr(ps_base,  PS_DDR);
    axi_wr(w_base,   WEIGHT_DDR + layers[layer].w_off);
    axi_wr(b_base,   BIAS_DDR   + layers[layer].b_off);

    axi_wr(ctrl, 0x1);
    while (!(axi_rd(status) & 0x2));
    axi_wr(status, 0x2);

    swap_in_out();
}

// 3. 解码 + NMS
yolodecode(output, 13, 13);
uart_print_boxes();

6.6 SD 卡布局

复制代码
SD:/
├── layers.json
├── weights/
│   ├── conv1_w.bin / conv1_b.bin
│   ├── conv2_w.bin ...
│   └── conv9_w.bin
├── inputs/
│   └── input_416.bin
└── refs/
    └── conv1_out.bin ... conv9_out.bin

由 sw/gen_board_files.py 一键生成。

6.7 16GB 内存跑 Vivado 综合的坑

Vivado 2019.2 在 16GB 物理内存上跑到 Cross Boundary and Area Optimization 阶段会爆。不动 C 盘的解法:

复制代码
设置 → 系统 → 存储 → 高级存储设置 → 虚拟内存 → 更改
→ 取消"自动管理" → 选 E: → 自定义大小 16384~16384 → 设置 → 确定 → 重启

重启后跑 board/vivado/synth16g.tcl 即可产出 utilization/timing 报告。


七、时序优化与踩坑实录

这个工程从 v6.1 一路修到 v6.7,UVM 跑了 90+ 轮仿真。这一章把最有教学价值的 bug 全部摊开。

7.1 流水/时序优化总览

# 优化措施 实现
1 PE 3 级流水(乘→部分和→累加) pe.v
2 块级寄存器化:blk_sz_r / m_tgt 每块锁存一次 conv_core
3 9-tap 像素延迟链 32 级数组替代多维 RAM 索引 conv_core
4 量化 3 级流水:fin_q → fin_q2 → fin_q3 conv_core
5 池化 4 级流水:pool_cap_q → q2 → q3 → q4 conv_core
6 池化双组 BRAM + pp_grp 行指针轮转 conv_core
7 写通道双缓冲 ogrp_vld0/1 + 4 拍×4 输出 conv_core
8 ps 写与输出写分时复用通道 conv_core
9 rd_hold 4 拍消费 + rd_hold_vld 背压 conv_core
10 AXI 突发长度寄存器化 axi_wrapper

关键路径估算(100MHz / 10ns 预算):

路径 估算 余量
lb 组合读 → 4:1 → 32:1 延迟链 mux → PE 输入 ~4-5ns ✅
quantize48(48bit 加法+饱和) ~3-4ns ✅
leaky16_v(16×16 乘+比较) ~2.5-3ns ✅
max4 比较树 / AXI WLAST ~1-1.5ns ✅

最差路径 ~5ns,余量 >50%。

7.2 Bug #1:ps_cap / EOS 同拍竞争

现象:conv3 最后一个像素的部分和没写进 DDR,S_REDUCE 读回来是 0,输出整体错。

根因 :S_STREAM 末尾,最后像素 ps_cap 拉高的那拍,状态机正好转移到 S_REDUCE,ps 写请求被同拍吞掉。

修复 :加 ps_pix_cnt >= PIXC 门控 + last_ps 中间态,让最后一条 ps 写落盘后再跳转。

7.3 Bug #2:wr_len_cap 缺失

现象 :ps 写还在进行,EOS 拍更新了 wr_len_r,写完成判断用了新长度,把正在写的 ps 区当成完成,提前结束。

根因 :写通道完成判断用的是实时 wr_len_r,而不是 wr_req 被接受那拍捕获的值。

修复:

verilog 复制代码
if (wr_req) wr_len_cap <= wr_len_r;
// 完成判断统一用 wr_len_cap

7.4 Bug #3:wr_req 电平持续

现象:写完成后写通道立刻又接受一个旧地址,污染 ps 区。

根因 :wr_req 是电平信号,写完成后它还在,写通道误以为又来了一笔。

修复 :wr_ack 后清 wr_req,改成脉冲。

7.5 Bug #4:ps_cap_soon 捕获竞争

现象:新像素 word0 在 ps_cap 前一拍被提前捕获,ch0 丢失,acc 跨像素运行。

根因:ps_cap 前一拍就该阻断新像素 word0 捕获,但代码没做。

修复 :加 ps_cap_soon(ps_cap 前一拍)阻断捕获。

7.6 Bug #5:conv3 输出整体前移 4 像素

现象 :out[pix+4] == expected[pix],首写地址 0x2900080(应是 0x2900000)。

根因 :conv_if 写通道同拍竞争------ps 写最后一条 wr_valid_q 与输出写 wr_req 接受同拍,wr_cnt last-wins 被留在 16,输出写起点偏移 128B = 4 像素。

修复:

systemverilog 复制代码
wire wr_accept_cyc = (wr_req && !wr_pend_q && !wr_active)
                  || (!wr_active && wr_pend_q);
if (!wr_accept_cyc) wr_cnt <= wr_cnt + 1;

修完 10 层全 PASS。

7.7 多驱动修复(11 个信号)

Vivado 综合遇到 multi-driven 会忽略一个驱动,QuestaSim 允许多驱动掩盖问题,导致综合后网表行为错误。

修过的 11 个信号:m_cnt / ch_sub_r / pix_cnt / n_cnt / rd_hcnt / rd_hold_vld / wr_cnt / ogrp_vld0 / ogrp_vld1 / wsel / wph。

原则:一个信号只由一个 always 块驱动。

7.8 综合内存爆炸

现象:16GB 机器跑到 Cross Boundary 阶段内存耗尽。

根因 :conv_core.v 三个大型多维数组在 Vivado 2019.2 报 Synth 8-5856 not supported,走寄存器展开灾难路径。

修法:

原结构 改成
lb 三维 [CB][4][LW] 4 个独立 1D 分布式 RAM lb0...lb3
ppram 三维 430Kbit 双组 generate 实例化 32 个 pool_ram(LUTRAM)
wreg 三维 [CB][16][9] 单维 [CB*16*9]
9 tap 直接多维索引 32 级像素延迟链 shA/shB/shC
pprev<=pcur 整行复制 pp_grp 行指针翻转(1 FF)

改完后 Synthesize 9:57 完成,peak 15033MB,0 multi-driven / 0 RAM 错误。

7.9 上板 cfg0 编码 bug

现象:UVM 10 层全过,但上板模式 A/B 必挂。

根因 :gen_board_files.py 用 (c_out<<8) 编码 cfg0,而 RTL 解码 cfg0[15:0]=c_out------上板 10 层配置全错。

修复:

python 复制代码
# 改前:cfg0 = (c_in << 16) | (c_out << 8)
# 改后:
cfg0 = (c_in << 16) | c_out

修完后三方(RTL / UVM / 上板)cfg0 完全一致。

7.10 提频瓶颈(备查)

当前 100MHz 无风险,若以后要提频:

  1. w00=shA[2*blk_sz_r-1] 等 6 个 32:1 可变索引 mux------>150MHz 需固定索引或加打拍;
  2. pool_ram LUTRAM 组合读------提频需改 BRAM 同步读并重排 q3/q4;
  3. quantize48 的 48bit 加法------可拆两级加法树。

7.11 收尾经验

整个工程从 SPEC 到上板走过的路:

复制代码
SPEC.md → RTL v6.1 → UVM run61 → 90+ 轮排障 → v6.7 全过
→ Vivado 综合(16GB + E盘 pagefile)→ Zynq BD → Vitis → SD 上板

最有价值的 5 条经验:

  1. 先写 Python 定点 golden,再写 RTL------不然 bug 根本定位不到;
  2. UVM 比对要 mism=0,不要"误差范围内"------量化对不上一定是 RTL 错;
  3. 多驱动是隐形杀手------QuestaSim 不报错,Vivado 综合后网表行为错;
  4. 综合内存爆炸先查多维数组------Vivado 2019.2 对某些 3D array 不支持,会默默展开成 FF;
  5. 上板配置和 UVM 配置必须同源------一个位域写错,上板必挂,UVM 还查不出来。
相关推荐
FPGA小徐1 小时前
Vivado 按键消抖与复位模块代码修复详解:KEY_TOP、key、rst_gen 完整实战
fpga开发
炎龙铠甲会合体3 小时前
AXI2ICACHE— AXI4-Full 四路组相联指令 Cache 设计与 UVM 验证
fpga开发·面试·芯片验证
bingcheby3 小时前
FMQL15/20/45 (复旦微青龙系列 PSOC)新手使用说明
fpga开发
漫漫长远4 小时前
低功耗UPF介绍(一)
笔记·嵌入式硬件·学习·fpga开发
自小吃多16 小时前
【Verilog/FPGA】存储器(数组)
嵌入式硬件·fpga开发
szxinmai主板定制专家16 小时前
RK3588+FPGA异构架构|高速数据采集+边缘AI落地应用全解析
人工智能·嵌入式硬件·fpga开发·架构·zynq
明德扬21 小时前
FPGA MIPI 多路视频聚合方案
fpga开发·mipi视频拼接·mipi多路拼接·mipi协议·mipi视频拼接方案·fpga mipi方案·mipi csi
XMAIPC_Robot21 小时前
为什么大型储能需要边缘 AI 协调控制器?RK3588+FPGA 高速采集方案
人工智能·嵌入式硬件·fpga开发·arm+fpga·rk3588+fpga·协调控制器
szxinmai主板定制专家21 小时前
RK3576+CODESYS+RK182X+FPGA异构架构:半导体精密设备一体化控制器方案
人工智能·fpga开发·架构·rk3576+codesys