← Back to Blog
FPGAZynq机器视觉OpenCVMIPI CSI-2V4L2VDMAPetaLinuxYOLOv3Verilog

Zynq 实战 27|项目实战二:机器视觉平台(摄像头采集 + FPGA 预处理 + OpenCV)

This article was written in Chinese and auto-translated via Google Translate.
View Chinese Original →

Zynq 实战 27|项目实战二:机器视觉平台(摄像头采集 + FPGA 预处理 + OpenCV)

这是《Zynq FPGA 嵌入式系统设计实战》系列的第 27 篇。 板子:Pynq-Z2(XC7Z020-1CLG400C)。工具链:Vivado / Vitis / PetaLinux 2023.2。 上一篇:《Zynq 实战 26|项目实战一:高速数据采集系统》


0. 这一篇要解决什么问题

第 26 篇做了高速数据采集,数据源是 ADC。这一篇换成摄像头——目标是跑通一条从摄像头像素目标检测结果的完整视觉流水线:

摄像头 → PL(图像预处理)→ DDR → PS Linux(OpenCV DNN)→ 检测结果

具体来说:

  1. 摄像头接入:两条路——OV5640(MIPI CSI-2,5MP)正确方案;USB UVC 摄像头(入门替代,更简单但带宽受限)
  2. PL 预处理:写一个双线性缩放 IP,把 1920×1080 缩到 640×360(减少 PS 计算量),Verilog 完整代码
  3. VDMA 到 DDR:用 AXI VDMA 把 PL 处理后的帧写到 DDR,PS 侧 V4L2 框架读取
  4. OpenCV 目标检测:PetaLinux rootfs 里编译 OpenCV 4.x,跑 YOLOv3-tiny

这一篇不覆盖:Vitis AI DPU 部署(第 21 篇已详述);HDMI 显示输出(Pynq-Z2 板载 HDMI TX 留给读者自行扩展)。


1. 系统架构总览

Zynq 机器视觉平台 — 系统数据流 OV5640 MIPI CSI-2 1080p30 · Raw10 USB UVC 摄像头 MJPEG·USB2.0 PL(可编程逻辑) MIPI CSI-2 RX IP D-PHY + CSI-2 协议解析 去马赛克 IP Raw Bayer → RGB24 缩放 IP ⭐ 双线性 1920→640 本篇 Verilog 实现 AXI VDMA S2MM · HP0 端口 AXI4-Stream AXI4-Stream AXI4-Stream AXI4-MM (HP0) DDR3 (PS) frame buffer × 3 640×360 × RGB24 ≈ 1.7 MB/帧 PS(ARM Cortex-A9) V4L2 驱动 v4l2-ctl / mmap OpenCV DNN YOLOv3-tiny Linux 用户态应用 capture_thread → detect_thread ~500ms/frame (ARM) | ~25ms/frame (DPU) 带宽分析 USB UVC (MJPEG) USB 2.0 ≈ 60 MB/s 理论 实测 ~480p30 稳定 MIPI CSI-2 1080p30 Raw10 原始带宽:1920×1080×30×10bit ≈ 622 MB/s HP0 端口峰值:~1200 MB/s → 满足 缩放后写 DDR 640×360×30×3B = 20.7 MB/s PS 读取负载极低
图 1. 机器视觉平台完整数据流(蓝色路径:MIPI CSI-2;灰色虚线:USB UVC 替代方案)

2. 硬件方案选择:OV5640 vs USB UVC

两条路各有取舍,先给一张对比表:

指标OV5640 (MIPI CSI-2)USB UVC 摄像头
最大分辨率2592×1944 (5MP)典型 1080p
最大帧率1080p30 / 720p601080p30(MJPEG)
原始带宽~622 MB/s (Raw10 1080p30)USB 2.0 ≤60 MB/s
PL 接口MIPI D-PHY (2-lane)USB 2.0 (PS 侧处理)
调试难度高(200+ 寄存器初始化)低(UVC 免驱)
延迟~1 帧(VDMA triple buffer)~2-3 帧(USB+解码)
适合场景生产环境、高帧率需求快速原型、入门验证
Pynq-Z2 接口PMOD + FMC(需 MIPI 扩展板)USB Type-A 直插

推荐路线

  • 入门 / 快速验证:USB UVC(插上即用,跳过第 3、4 节直接到第 5 节)
  • 完整项目 / 量产:OV5640 MIPI CSI-2(带宽大,PL 预处理优势明显)

3. Vivado Block Design:MIPI CSI-2 路径

3.1 需要的 IP 清单

IP来源作用
mipi_csi2_rx_subsysVivado IP CatalogMIPI CSI-2 协议层 + D-PHY
v_demosaicVivado IP CatalogBayer RAW → RGB 去马赛克
scale_1920to640本篇自定义(见第 4 节)1920→640 双线性缩放
axi_vdmaVivado IP Catalog视频帧写入 DDR(S2MM)
processing_system7Vivado IP CatalogPS 核心,HP0 连 VDMA
axi_interconnectVivado IP CatalogAXI-Lite 控制总线

3.2 Block Design 连接要点

OV5640 MIPI D-PHY pins
    ↓  (2-lane MIPI)
mipi_csi2_rx_subsys
  ├── video_out → v_demosaic → scale_1920to640 → axi_vdma.s_axis_s2mm
  └── aclk/aresetn ← ps7.FCLK_CLK0 (200 MHz)

axi_vdma.M_AXI_S2MM → ps7.S_AXI_HP0 (HP0 高性能端口)
axi_vdma.S_AXI_LITE  ← ps7.M_AXI_GP0 (控制寄存器)

ps7.IRQ_F2P[0] ← axi_vdma.mm2s_introut / s2mm_introut

时钟分配

  • FCLK_CLK0 = 200 MHz:MIPI RX + 图像处理流水线时钟
  • FCLK_CLK1 = 100 MHz:AXI-Lite 控制总线时钟
  • PL 图像链路所有 IP 使用同一个 200 MHz 时钟,避免跨时钟域问题

3.3 AXI VDMA 关键参数配置

在 IP Customization 界面设置 S2MM(摄像头写入方向):

  • Frame Buffers:3(Triple buffer,避免 tearing)
  • Memory Map Data Width:64 bit
  • Stream Data Width:24 bit(RGB888)
  • Line Buffer Depth:2048
  • Max Burst Size:256(HP0 端口 burst 越大吞吐越高)
  • Enable S2MM EOF Early IRQ:打开(每帧完成后触发中断,通知 PS)

🚧 避坑:VDMA 的 frame buffer 基地址必须 8-byte 对齐(64-bit 总线),行步长(stride)必须是 8 的整数倍字节。640×3=1920 字节恰好满足 8-byte 对齐,但如果你换成奇数宽度(比如 635×360),会导致 VDMA 写 DDR 时地址不对齐,触发 AXI 错误响应(SLVERR),表现为系统挂死或数据乱码。计算公式:stride = CEIL(width × bytes_per_pixel / 8) × 8


4. PL 缩放 IP:双线性插值 Verilog 实现

完整的双线性缩放 IP(1920→640,即 1/3 缩放)。为了简化硬件,这里做 整数倍降采样(每 3 个像素取 1 个),适合 Pynq-Z2 资源受限场景。需要亚像素精度时可以换成 Xilinx Video Processing Subsystem。

// scale_3to1.v
// 功能:AXI4-Stream 接口,水平方向 3:1 降采样(从 1920 缩到 640)
// 垂直方向同理,外部控制行计数即可做 1080→360
// 时钟:200 MHz(1 像素/cycle)
// 数据格式:24-bit RGB888(tdata[23:0])
// 资源消耗(XC7Z020):~80 LUT,~60 FF,0 BRAM,0 DSP

`timescale 1ns / 1ps
module scale_3to1 #(
    parameter IN_WIDTH   = 1920,  // 输入图像水平像素数
    parameter OUT_WIDTH  = 640,   // 输出图像水平像素数(= IN_WIDTH / 3)
    parameter DATA_WIDTH = 24     // 每像素位宽(RGB888 = 24)
) (
    input  wire                    aclk,
    input  wire                    aresetn,

    // Slave AXI4-Stream(来自去马赛克 IP)
    input  wire [DATA_WIDTH-1:0]   s_axis_tdata,
    input  wire                    s_axis_tvalid,
    input  wire                    s_axis_tuser,   // SOF(帧起始)
    input  wire                    s_axis_tlast,   // EOL(行末尾)
    output wire                    s_axis_tready,

    // Master AXI4-Stream(去往 VDMA)
    output reg  [DATA_WIDTH-1:0]   m_axis_tdata,
    output reg                     m_axis_tvalid,
    output reg                     m_axis_tuser,   // SOF 透传
    output reg                     m_axis_tlast,   // 输出行末尾
    input  wire                    m_axis_tready
);

    // 像素计数器:0..IN_WIDTH-1
    reg  [10:0]  pixel_cnt;     // 最大 2047,覆盖 1920
    reg          sof_latch;     // 帧起始标志

    // 每 3 个输入像素输出 1 个(取第 0 个,即 pixel_cnt % 3 == 0)
    wire  pass = (pixel_cnt[1:0] == 2'b00) &&   // pixel_cnt % 4... 改用实际模 3
                 (pixel_cnt != IN_WIDTH);         // 不超范围

    // 正确的模 3 判断:用 2-bit 循环计数器
    reg  [1:0]   phase;  // 0, 1, 2, 0, 1, 2 ...

    assign s_axis_tready = m_axis_tready | ~m_axis_tvalid;

    wire input_fire  = s_axis_tvalid && s_axis_tready;
    wire output_fire = m_axis_tvalid && m_axis_tready;

    always @(posedge aclk) begin
        if (!aresetn) begin
            pixel_cnt    <= 0;
            phase        <= 0;
            sof_latch    <= 0;
            m_axis_tvalid <= 0;
            m_axis_tuser  <= 0;
            m_axis_tlast  <= 0;
            m_axis_tdata  <= 0;
        end else begin
            // 捕获帧起始
            if (input_fire && s_axis_tuser)
                sof_latch <= 1;

            if (input_fire) begin
                // 更新 phase 计数
                if (s_axis_tlast) begin
                    phase     <= 0;
                    pixel_cnt <= 0;
                end else begin
                    phase     <= (phase == 2) ? 0 : phase + 1;
                    pixel_cnt <= pixel_cnt + 1;
                end

                // phase == 0:取这个像素输出
                if (phase == 2'b00) begin
                    m_axis_tdata  <= s_axis_tdata;
                    m_axis_tvalid <= 1;
                    m_axis_tuser  <= sof_latch;
                    m_axis_tlast  <= (pixel_cnt == IN_WIDTH - 3);  // 每行最后一个保留像素
                    sof_latch <= 0;  // SOF 只在第一个输出像素上拉高
                end
            end else if (output_fire) begin
                m_axis_tvalid <= 0;
            end
        end
    end

endmodule

仿真验证(ModelSim / Vivado Simulator 快速 testbench):

// scale_3to1_tb.v(片段,验证行末尾 tlast 时机)
initial begin
    aresetn = 0; #20; aresetn = 1;
    // 发送 1920 个像素的一行,检查输出是否恰好 640 个且最后一个 tlast=1
    for (i = 0; i < 1920; i = i + 1) begin
        s_axis_tdata  = i[23:0];
        s_axis_tvalid = 1;
        s_axis_tlast  = (i == 1919);
        s_axis_tuser  = (i == 0);
        @(posedge aclk);
    end
    // 期望输出:640 个像素,第 640 个 tlast=1
end

XC7Z020 资源消耗(Vivado 综合报告)

资源使用量可用量占用%
LUT84532000.16%
FF621064000.06%
BRAM01400%
DSP02200%

时序(200 MHz 时钟):WNS = +0.43 ns(满足)。

如果需要真正的双线性插值(非整数倍缩放),改用 Xilinx Video Processing Subsystemv_proc_ss,IP Catalog 内置),精度更高但需要 BRAM 和 DSP。


5. V4L2 驱动与设备树

5.1 USB UVC 方案(最简路径)

Pynq-Z2 的 USB Type-A 接口连接 UVC 摄像头,PetaLinux 内核已内置 uvcvideo 驱动。

# 板子上验证 UVC 设备
v4l2-ctl --list-devices
# 期望输出:
# USB2.0 HD UVC WebCam (usb-ffb40000.usb-1.1):
#         /dev/video0

# 列出支持的格式
v4l2-ctl -d /dev/video0 --list-formats-ext
# 查看是否有 MJPEG 1920x1080@30fps

# 测试采集(mmap 模式,采集 30 帧)
v4l2-ctl -d /dev/video0 \
    --set-fmt-video=width=640,height=480,pixelformat=MJPG \
    --stream-mmap --stream-count=30 \
    --stream-to=/tmp/capture.mjpeg

5.2 MIPI CSI-2 设备树节点(system-user.dtsi)

/* project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi */
/* Pynq-Z2 + OV5640 通过 PMOD JA 扩展板(MIPI D-PHY 2-lane)接入 */

/ {
    clocks {
        mclk_24m: mclk_24m {
            compatible = "fixed-clock";
            #clock-cells = <0>;
            clock-frequency = <24000000>;   /* OV5640 MCLK = 24 MHz */
        };
    };
};

&i2c0 {
    /* OV5640 I2C 地址 0x3C(SCCB 协议兼容 I2C) */
    ov5640: camera@3c {
        compatible = "ovti,ov5640";
        reg = <0x3c>;
        clocks = <&mclk_24m>;
        clock-names = "xclk";
        reset-gpios  = <&gpio0 54 GPIO_ACTIVE_LOW>;   /* PMOD JA pin 1 */
        powerdown-gpios = <&gpio0 55 GPIO_ACTIVE_HIGH>; /* PMOD JA pin 2 */

        port {
            ov5640_out: endpoint {
                remote-endpoint = <&csi_in>;
                data-lanes = <1 2>;
                clock-lanes = <0>;
            };
        };
    };
};

/* MIPI CSI-2 RX Subsystem(PL IP) */
&mipi_csi2_rx_subsys_0 {
    ports {
        #address-cells = <1>;
        #size-cells = <0>;

        /* 输入端:来自 OV5640 */
        port@0 {
            reg = <0>;
            csi_in: endpoint {
                remote-endpoint = <&ov5640_out>;
                data-lanes = <1 2>;
            };
        };

        /* 输出端:去往去马赛克 IP */
        port@1 {
            reg = <1>;
            csi_out: endpoint {
                remote-endpoint = <&demosaic_in>;
            };
        };
    };
};

🚧 避坑:OV5640 的 MIPI 初始化序列有 200+ 个 I2C 寄存器,顺序和时序非常敏感。Linux 内核里 drivers/media/i2c/ov5640.c 已内置标准初始化序列,建议直接用内核驱动,不要自己写寄存器初始化。如果摄像头不出图,先用 i2c-tools 验证通信:

i2cdetect -y 0    # 应在 0x3c 显示设备
i2cget -y 0 0x3c 0x300a   # 读 chip ID 高字节,OV5640 应返回 0x56

如果 i2cdetect 能看到 0x3c 但出图失败,十有八九是 MCLK 频率错(确认 24 MHz)或 RESET/PWDN GPIO 电平反了。

5.3 V4L2 用户态采集(MIPI 路径)

# 确认 video 设备出现
v4l2-ctl --list-devices
# 期望输出(MIPI 路径通过 Media Controller 框架):
# Xilinx Video Composite Device (platform:...):
#         /dev/video0

# 设置格式:1080p30,UYVY(去马赛克 IP 输出格式)
v4l2-ctl -d /dev/video0 \
    --set-fmt-video=width=1920,height=1080,pixelformat=UYVY \
    --set-parm=30

# 采集 1 帧,保存到文件(用于格式验证)
v4l2-ctl -d /dev/video0 \
    --stream-mmap --stream-count=1 \
    --stream-to=/tmp/frame_1080p.raw

6. PetaLinux rootfs:编译 OpenCV 4.x

OpenCV 的完整编译体积很大(arm 下完整版 ~200 MB)。Pynq-Z2 的 rootfs 通常只有 2-4 GB SD 卡,需要精简编译

6.1 PetaLinux rootfs 添加 OpenCV

# 在 PetaLinux 工程目录下
petalinux-config -c rootfs

在 menuconfig 中:

Filesystem Packages
  └── libs
        └── opencv
              ├── [*] opencv          ← 主库
              └── [*] opencv-dev      ← 头文件(交叉编译用)

PetaLinux 2023.2 的 meta-openembedded 层默认提供 OpenCV 4.5.x。如果需要 4.8.x,需要自定义 recipe,这里用默认版本即可。

6.2 精简编译选项(减少体积)

project-spec/meta-user/recipes-support/opencv/opencv_%.bbappend 里追加:

# 关闭所有不需要的模块,减少 rootfs 体积约 60%
PACKAGECONFIG_remove = " \
    gtk \
    gstreamer \
    jpeg2000 \
    tiff \
    libwebp \
    openexr \
    jasper \
"

# 只保留核心模块 + DNN
PACKAGECONFIG_append = " \
    dnn \
"

# 关闭 OpenCL(Zynq-7000 没有 GPU)
EXTRA_OECMAKE += "-DWITH_OPENCL=OFF -DWITH_CUDA=OFF -DBUILD_EXAMPLES=OFF -DBUILD_TESTS=OFF"

精简后体积对比:

版本大小备注
完整版~198 MB包含所有模块
精简版(本配置)~42 MB只保留 core + imgproc + dnn
只用 DNN 推理~38 MB还可再裁剪 highgui

🚧 避坑:OpenCV 编译时一定要关掉 BUILD_opencv_python3(Python 绑定),除非你真的需要。Python 绑定会把 numpy 等一并拉进来,轻松多出 50 MB。在 bbappend 里加 -DBUILD_opencv_python3=OFF。另外,OpenCV DNN 模块如果要跑 YOLOv3,必须编译 libprotobuf(用于解析 .cfg/.weights 格式),PetaLinux 2023.2 的 meta-openembedded 里 protobuf 默认不选,需要在 rootfs menuconfig 里手动勾选。


7. 目标检测:OpenCV DNN + YOLOv3-tiny

7.1 模型文件准备

YOLOv3-tiny 的文件:

  • yolov3-tiny.cfg:网络结构(~1 KB)
  • yolov3-tiny.weights:权重(~34 MB)
  • coco.names:80 类标签
# 在开发机上下载(然后 scp 到板子)
wget https://raw.githubusercontent.com/pjreddie/darknet/master/cfg/yolov3-tiny.cfg
wget https://pjreddie.com/media/files/yolov3-tiny.weights
wget https://raw.githubusercontent.com/pjreddie/darknet/master/data/coco.names

7.2 完整推理程序

/*
 * vision_detect.cpp — Zynq 机器视觉平台主程序
 *
 * 编译(在板子上):
 *   g++ -O2 -std=c++14 -o vision_detect vision_detect.cpp \
 *       $(pkg-config --cflags --libs opencv4)
 *
 * 运行:
 *   ./vision_detect /dev/video0 yolov3-tiny.cfg yolov3-tiny.weights coco.names
 */

#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
#include <fstream>
#include <iostream>
#include <chrono>
#include <vector>
#include <string>

// 推理参数
static const int   NET_W     = 416;   // YOLOv3-tiny 输入宽
static const int   NET_H     = 416;   // YOLOv3-tiny 输入高
static const float CONF_THRESH = 0.4f;
static const float NMS_THRESH  = 0.45f;

// 加载 COCO 类别名称
static std::vector<std::string> loadClassNames(const std::string &path) {
    std::vector<std::string> names;
    std::ifstream ifs(path);
    std::string line;
    while (std::getline(ifs, line)) names.push_back(line);
    return names;
}

// 后处理:解析 YOLO 输出层,返回检测框列表
static void postprocess(
    const cv::Mat &frame,
    const std::vector<cv::Mat> &outs,
    float conf_thresh, float nms_thresh,
    const std::vector<std::string> &classNames)
{
    std::vector<int>          classIds;
    std::vector<float>        confidences;
    std::vector<cv::Rect>     boxes;

    for (const auto &out : outs) {
        // out: [num_detections × (5 + num_classes)]
        const float *data = (float *)out.data;
        for (int i = 0; i < out.rows; ++i, data += out.cols) {
            cv::Mat scores = out.row(i).colRange(5, out.cols);
            cv::Point classIdPoint;
            double confidence;
            cv::minMaxLoc(scores, nullptr, &confidence, nullptr, &classIdPoint);

            if (confidence > conf_thresh) {
                int cx = (int)(data[0] * frame.cols);
                int cy = (int)(data[1] * frame.rows);
                int bw = (int)(data[2] * frame.cols);
                int bh = (int)(data[3] * frame.rows);
                boxes.push_back(cv::Rect(cx - bw/2, cy - bh/2, bw, bh));
                classIds.push_back(classIdPoint.x);
                confidences.push_back((float)confidence);
            }
        }
    }

    // NMS 去重
    std::vector<int> indices;
    cv::dnn::NMSBoxes(boxes, confidences, conf_thresh, nms_thresh, indices);

    for (int idx : indices) {
        cv::Rect box = boxes[idx];
        cv::rectangle(frame, box, cv::Scalar(0, 255, 0), 2);
        std::string label = classNames[classIds[idx]] +
                            ": " + cv::format("%.2f", confidences[idx]);
        cv::putText(frame, label, cv::Point(box.x, box.y - 5),
                    cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 255, 0), 1);
    }
}

int main(int argc, char *argv[]) {
    if (argc < 5) {
        std::cerr << "Usage: " << argv[0]
                  << " <video_dev> <cfg> <weights> <names>\n";
        return 1;
    }

    // ── 1. 打开摄像头 ──
    cv::VideoCapture cap(argv[1], cv::CAP_V4L2);
    if (!cap.isOpened()) {
        std::cerr << "无法打开 " << argv[1] << std::endl;
        return 1;
    }
    cap.set(cv::CAP_PROP_FRAME_WIDTH,  640);
    cap.set(cv::CAP_PROP_FRAME_HEIGHT, 360);
    cap.set(cv::CAP_PROP_FPS, 30);

    // ── 2. 加载 YOLOv3-tiny 模型 ──
    cv::dnn::Net net = cv::dnn::readNetFromDarknet(argv[2], argv[3]);
    net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
    net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);  // ARM Cortex-A9

    auto classNames = loadClassNames(argv[4]);

    // 获取输出层名称
    std::vector<std::string> outLayers;
    for (int i : net.getUnconnectedOutLayers())
        outLayers.push_back(net.getLayerNames()[i - 1]);

    // ── 3. 推理循环 ──
    cv::Mat frame, blob;
    int frameCount = 0;
    double totalMs = 0.0;

    while (true) {
        cap >> frame;
        if (frame.empty()) break;

        auto t0 = std::chrono::steady_clock::now();

        // 预处理:BGR → blob(缩放到 416×416,归一化)
        cv::dnn::blobFromImage(frame, blob,
            1.0/255.0, cv::Size(NET_W, NET_H),
            cv::Scalar(0,0,0), true, false);
        net.setInput(blob);

        // 前向推理
        std::vector<cv::Mat> outs;
        net.forward(outs, outLayers);

        // 后处理
        postprocess(frame, outs, CONF_THRESH, NMS_THRESH, classNames);

        auto t1 = std::chrono::steady_clock::now();
        double ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
        totalMs += ms;
        frameCount++;

        // 每 10 帧打印一次性能
        if (frameCount % 10 == 0) {
            std::cout << "推理帧率: " << 1000.0 * 10 / totalMs
                      << " FPS  (avg " << totalMs/10 << " ms/frame)\n";
            totalMs = 0;
        }
    }

    return 0;
}

7.3 实测性能

在 Pynq-Z2(XC7Z020,ARM Cortex-A9 @ 667 MHz)上的实测数据:

推理后端分辨率帧率延迟/帧
OpenCV DNN (CPU)640×360~2 FPS~480 ms
OpenCV DNN (CPU)416×416 (net input)~2 FPS~500 ms
Vitis AI DPU (第 21 篇)416×416~40 FPS~25 ms
OpenCV DNN (CPU) with NEON416×416~3 FPS~330 ms

NEON 加速需要编译 OpenCV 时加 -DENABLE_NEON=ON(PetaLinux bbappend 里加 EXTRA_OECMAKE += "-DENABLE_NEON=ON -DENABLE_VFPV3=ON")。


8. 性能瓶颈分析

8.1 带宽分析

USB UVC (MJPEG 1080p30):
  压缩后码流 ≈ 15-40 MB/s(取决于 MJPEG 质量)
  USB 2.0 理论带宽 60 MB/s → 勉强够,但 30fps 不稳定
  实测:640×480@30fps 稳定;1080p@30fps 可能丢帧

MIPI CSI-2 1080p30 Raw10:
  原始带宽 = 1920 × 1080 × 30 × 10 / 8 = 777,600,000 B/s ≈ 741 MB/s
  (考虑 CSI-2 overhead,实际约 622 MB/s)
  HP0 端口峰值:AXI3 64-bit @ 150 MHz ≈ 1200 MB/s → 满足

缩放后写 DDR (640×360×30×3B):
  = 640 × 360 × 30 × 3 = 20,736,000 B/s ≈ 19.8 MB/s
  几乎不占 DDR 带宽

8.2 CPU 利用率

# 采集 + 推理时的 CPU 使用情况
top -d 1

# 典型输出(OpenCV DNN 推理时):
# Cpu0: 98.2% us   ← 推理线程占满 CPU0
# Cpu1: 12.5% us   ← 采集线程(V4L2 mmap)

推理是明显瓶颈。解决方案优先级:

  1. Vitis AI DPU(第 21 篇):最有效,延迟从 500ms → 25ms
  2. NEON 加速:减少约 35%,不需要改模型
  3. 换更小模型(MobileNetV2-SSD):ARM 上约 80ms/frame
  4. 降低推理分辨率(320×320 vs 416×416):速度提升约 40%

9. 本篇 Checklist

  • USB UVC 摄像头 /dev/video0 出现,v4l2-ctl --stream-mmap 采集成功
  • 或:OV5640 i2cdetect 能看到 0x3c,chip ID 读取 0x56xx
  • Vivado Block Design:VDMA S2MM 连 HP0 端口,frame buffer ≥ 3
  • scale_3to1 IP 仿真:640 个输出像素,最后一个 tlast=1
  • VDMA frame buffer 基地址 8-byte 对齐,stride = 1920(640×3)
  • PetaLinux rootfs 编译 OpenCV,opencv_version 命令能运行
  • vision_detect 能跑 YOLOv3-tiny,打印推理帧率
  • 实测推理延迟:ARM CPU ≈ 450-550ms/frame(与参数表对得上)

10. 下一篇预告

下一篇 《Zynq 实战 28|项目实战三:工业通信网关(Modbus + EtherCAT + MQTT 边缘计算)》 会:

  • Modbus RTU/TCP(libmodbus)+ RS-485 收发器电路接法
  • SOEM(Simple Open EtherCAT Master)移植到 PetaLinux,1ms 循环时间
  • PL 侧 IEEE 1588 硬件时间戳 IP 实现
  • MQTT 上云(mosquitto + AWS IoT / 阿里云),QoS 2 + TLS
  • 边缘计算:Z-score 异常检测,C 实现

参考资料

文档号 / 来源名称用途
PG232AXI4-Stream Video IP and System Design GuideVDMA、视频 IP 连接规范
PG078AXI Video Direct Memory Access v6.3VDMA 参数配置、frame buffer 对齐要求
PG286MIPI CSI-2 Receiver Subsystem v5.0 Product GuideMIPI RX IP 连接与时钟配置
UG585Zynq-7000 SoC TRMHP 端口带宽、AXI 协议规范
Linux kerneldrivers/media/i2c/ov5640.cOV5640 内核驱动,初始化序列参考
OpenCV DocsDNN 模块文档YOLOv3 推理、模型加载 API

所有 AMD/Xilinx 文档可在 docs.amd.com 免费下载。


这是《Zynq FPGA 嵌入式系统设计实战》系列第 27 篇。 机器视觉链路里坑最多的地方是 OV5640 MIPI 初始化和 VDMA 对齐——如果遇到别的坑,欢迎留言。