4. Vitis Vision库与加速器基础

在 Kria 平台上做机器视觉,图像预处理往往是最先遇到性能瓶颈的环节。CPU 上跑 OpenCV 的 GaussianBlur、resize、cvtColor 虽然开发快,但在 1080p@60fps 的多路输入场景下很快会吃满 ARM 核。Vitis Vision 库的价值就在于:它把常用图像处理函数以 HLS(高层次综合)可综合的形式提供出来,让同一套算法语义既能跑在 PS 端做功能验证,又能通过 Vitis HLS 综合成 PL 端的硬件加速器,最终以流水线方式在 FPGA 逻辑中并行处理像素流。

Vitis Vision 的函数结构与两种调用形态

Vitis Vision 库(xf::cv 命名空间)的函数在接口上有一个关键设计:它同时支持“软件参考实现”和“硬件综合实现”。以高斯滤波为例,函数原型大致是:

void GaussianBlur(xf::cv::Mat<TYPE, ROWS, COLS, NPC>& src,
                  xf::cv::Mat<TYPE, ROWS, COLS, NPC>& dst,
                  float sigma);

这里的 xf::cv::Mat 不是 OpenCV 的 cv::Mat,而是为 HLS 设计的定长矩阵模板。模板参数中 TYPE 是像素数据类型(如 XF_8UC1、XF_8UC3),ROWS/COLS 是图像尺寸,NPC(Number of Pixels per Clock)表示每个时钟周期并行处理的像素数,可取 1、2、4、8。NPC 直接决定数据位宽和吞吐:NPC=1 时每周期处理 1 个像素,NPC=8 时每周期处理 8 个像素,理论吞吐提升 8 倍,但消耗的 DSP 和 BRAM 也相应增加。

在 PS 端调用时,这些函数就是普通的 C++ 函数,编译后由 ARM 核执行,用于算法正确性验证;在 PL 端,同样的函数被 Vitis HLS 综合成 RTL,通过 AXI4-Stream 接口与外部数据流对接。这种“一份代码、两种形态”的结构,是 Vitis Vision 区别于普通 OpenCV 移植的核心。

PL 端加速器生成流程

把一个 Vitis Vision 函数变成 Kria 上可调用的加速器,通常走以下路径。首先编写 HLS 顶层函数,内部调用 xf::cv:: 函数,并用 #pragma HLS INTERFACE 指定接口类型。典型的顶层会使用 AXI4-Stream 输入输出,配合 hls::stream 做流式处理:

void preprocess_top(hls::stream<ap_axiu<24,1,1,1>>& src_stream,
                    hls::stream<ap_axiu<24,1,1,1>>& dst_stream,
                    int rows, int cols) {
#pragma HLS INTERFACE axis port=src_stream
#pragma HLS INTERFACE axis port=dst_stream
#pragma HLS INTERFACE s_axilite port=rows
#pragma HLS INTERFACE s_axilite port=cols
#pragma HLS INTERFACE s_axilite port=return
    xf::cv::Mat<XF_8UC3, HEIGHT, WIDTH, NPC> src_mat(rows, cols);
    xf::cv::Mat<XF_8UC3, HEIGHT, WIDTH, NPC> dst_mat(rows, cols);
#pragma HLS DATAFLOW
    // 流接口转 Mat
    // 调用 Vitis Vision 函数
    // Mat 转流接口
}

综合时,Vitis HLS 会报告三类关键指标:Latency(处理一帧所需时钟数)、Initiation Interval(II,流水线启动间隔)、以及资源占用(LUT、FF、DSP、BRAM)。理想情况下,数据流函数在 DATAFLOW 指令下形成流水线,II 接近 1,即每个时钟周期都能接收新像素。若 II 远大于 1,说明存在数据依赖或内存访问冲突,需要调整 NPC 或拆分循环。

综合通过后,在 Vitis 中创建 Platform 工程,把生成的 .xo 文件加入 Linker,与 Kria 的 base platform(如 kv260_custom_platform)链接,生成 .xclbin。这个 .xclbin 就是 PL 端的“固件”,运行时由 XRT 加载到 FPGA 的对应区域。

图像预处理映射到 PL 的典型链路

工业现场常见的预处理链路是:去噪 → 缩放 → 色彩转换。在 PL 端,这三步可以串成一条 AXI4-Stream 流水线,每级之间用 hls::stream 连接,形成背压式数据流。下图展示了一条典型的映射关系:

Vitis Vision 预处理链路在 Kria PL 端的映射 PS (ARM) OpenCV 参考验证 DDR 帧缓存 1080p BGR 帧 AXI4-Stream PL 可编程逻辑(xclbin) ① 去噪 GaussianBlur sigma=1.5, 3×3 NPC=2, II≈1 stream ② 缩放 resize 1920×1080 → 640×640 双线性插值 stream ③ 色彩转换 cvtColor BGR → RGB 或 BGR → GRAY AXI4-Stream 输出帧 640×640 RGB 资源与性能参考(示例估算) DSP: ~45 BRAM: ~18 LUT: ~12K Latency: ~1.2ms/帧 NPC=2 时吞吐约 2 pixel/clock,@250MHz 约 500M pixel/s 三级流水线由 #pragma HLS DATAFLOW 驱动,级间背压自动传递

图中三级之间用 AXI4-Stream 连接,每级内部由 Vitis Vision 函数实现。需要注意的是,resize 的缩放比例会改变后续级的数据量,因此下游的 NPC 和行缓存深度需要按输出尺寸重新计算。例如输入 1920×1080、输出 640×640,缩放后每行像素数减少,行缓存 BRAM 需求也随之下降,但插值计算本身会引入额外的乘加运算,DSP 占用可能上升。

参数选择与常见陷阱

实际配置时,NPC 的选择需要权衡吞吐和资源。以 1080p@60fps 为例,总像素率约 124.4M pixel/s。若 PL 时钟为 250MHz,NPC=1 时理论吞吐 250M pixel/s,看似够用,但流水线中每级都有 II 和延迟,实际有效吞吐会打折扣。NPC=2 可提供约 500M pixel/s 的余量,适合多路复用或更高帧率。NPC=4 以上则要关注 AXI4-Stream 位宽是否匹配(NPC=4、8bit 三通道时数据位宽为 96bit),以及 DDR 带宽是否成为瓶颈。

另一个常见问题是图像边界处理。Vitis Vision 的滤波函数默认对边界做复制或零填充,若应用对边缘敏感(如工业测量),需要在 HLS 顶层显式处理边界像素,或预留边界区域。此外,xf::cv::Mat 的尺寸是编译期常量,运行时不能动态改变,因此若输入分辨率可变,需要实例化多个不同尺寸的加速器,或在 PS 端先做裁剪/填充到固定尺寸。

在 Kria KV260 上,PL 端可用的 BRAM 约 144 块(36Kb 每块),DSP 约 1248 个。一条三级预处理链路通常占用 10%~20% 的 DSP 和 15%~25% 的 BRAM,剩余资源可留给后续的推理加速器或其它视觉任务。建议先用 Vitis HLS 的 C 仿真验证功能,再用 C/RTL 协同仿真确认时序,最后上板用 XRT 的 xrt::run 做端到端吞吐测试。

把预处理映射到 PL 之后,PS 端只需负责图像采集、DMA 搬运和结果后处理,ARM 核的负载会显著下降。下一阶段就可以在这条流水线之后接入量化后的神经网络加速器,形成完整的“采集—预处理—推理—输出”硬件链路。