本文把一个完整的「mini-YOLO(YOLOv2 变体)FPGA 加速器」教学案例从算法原理 → 定点量化 → RTL 前端设计 → UVM 验证 → Zynq 上板 → 时序优化与踩坑全流程串起来。所有代码在正点原子领航者 Zynq-7020(XC7Z020CLG400-2)上跑通,UVM 回归 10 层全部
mism=0 / maxdiff=0。全文配套源码目录:
E:\fpga\FPGA_projects\YOLO\Yolo\repro\。
目录
- 项目概览
- [算法原理与 Python 参考链](#算法原理与 Python 参考链)
- 定点量化与测试向量生成
- [RTL 架构设计详解](#RTL 架构设计详解)
- [UVM 验证环境搭建](#UVM 验证环境搭建)
- [Zynq 上板指南](#Zynq 上板指南)
- 时序优化与踩坑实录
一、项目概览
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.v9-tap 3 级流水 MAC×16 +axi_wrapper.vAXI4 包装。 - 验证侧(SystemVerilog/UVM 1.2) :
conv_if.svDDR 模型 + 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直接对应 RTLcfg1的位域; 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 位,和 RTLleaky16_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 无风险,若以后要提频:
w00=shA[2*blk_sz_r-1]等 6 个 32:1 可变索引 mux------>150MHz 需固定索引或加打拍;- pool_ram LUTRAM 组合读------提频需改 BRAM 同步读并重排 q3/q4;
- quantize48 的 48bit 加法------可拆两级加法树。
7.11 收尾经验
整个工程从 SPEC 到上板走过的路:
SPEC.md → RTL v6.1 → UVM run61 → 90+ 轮排障 → v6.7 全过
→ Vivado 综合(16GB + E盘 pagefile)→ Zynq BD → Vitis → SD 上板
最有价值的 5 条经验:
- 先写 Python 定点 golden,再写 RTL------不然 bug 根本定位不到;
- UVM 比对要 mism=0,不要"误差范围内"------量化对不上一定是 RTL 错;
- 多驱动是隐形杀手------QuestaSim 不报错,Vivado 综合后网表行为错;
- 综合内存爆炸先查多维数组------Vivado 2019.2 对某些 3D array 不支持,会默默展开成 FF;
- 上板配置和 UVM 配置必须同源------一个位域写错,上板必挂,UVM 还查不出来。