Zynq 实战 13|VDMA + VTC + HDMI:搭一条 1080p 视频流水线
Zynq 实战 13|VDMA + VTC + HDMI:搭一条 1080p 视频流水线
这是《Zynq FPGA 嵌入式系统设计实战》系列的第 13 篇。 板子:Pynq-Z2(XC7Z020)。工具链:Vivado / Vitis / PetaLinux 2023.2。 上一篇:《Zynq 实战 12|AXI DMA 引擎驱动》
0. 这一篇要解决什么问题
上一篇用 AXI DMA 把 PL 的任意数据流搬进 DDR。视频是 AXI-Stream 数据流的一个特殊子集:它有固定帧率、固定分辨率、严格的行列时序——普通 AXI DMA 管不了帧切换和显示时序,需要专用的 AXI VDMA(Video DMA) 和 VTC(Video Timing Controller)。
本篇做完之后,你会有:
- 一条跑通 1920×1080@60Hz 的完整视频流水线,从 PL 内部的 Test Pattern Generator(TPG)输出到 HDMI 显示器
- 清楚 VDMA 的两种工作模式和三帧缓冲配置
- 能用裸机寄存器操作或 Linux 控制 VDMA 帧切换
本文不覆盖摄像头输入(那是 14、15 篇的内容),不覆盖 H.264 编码——只做显示输出这条路。
1. 整体 Block Design 拓扑
完整 Block Design 里的 IP 清单:
| IP | 版本 | 功能 |
|---|---|---|
| AXI4-Stream Test Pattern Generator | v8.2 | 生成彩条 / 渐变 / 棋盘格测试图案 |
| AXI Video DMA | v6.3 | 视频帧在 AXI-Stream 和 DDR 之间搬运 |
| AXI4-Stream Subset Converter | v1.1 | 可选:做 AXI-Stream 位宽转换或颜色格式调整 |
| Video Timing Controller | v6.2 | 产生 HSYNC / VSYNC / DE / 行列计数 |
| HDMI 1.4/2.0 Tx Subsystem | 或 RGB to DVI IP | TMDS 编码输出 |
| MMCM(Clocking Wizard) | v6.0 | 生成 148.5 MHz pixel clock |
2. 带宽核算:为什么要用 HP 端口
1080p60 RGB24 的原始带宽:
1920 × 1080 × 60 Hz × 3 bytes = 373,248,000 B/s ≈ 356 MB/s
VDMA 在 Triple Buffer(三帧缓冲)模式下,需要:
- S2MM 写通道:写入当前捕获帧(373 MB/s,若有摄像头)
- MM2S 读通道:读出当前显示帧(373 MB/s)
两个方向加起来:~746 MB/s。
即使只做显示(无摄像头输入),MM2S 读带宽 373 MB/s 也接近 GP0 端口(AXI-GP0 理论值约 600 MB/s,实际负载下更低)的极限。
结论:视频流水线必须用 HP 端口(S_AXI_HP0 或 S_AXI_HP2),不能用 GP 端口。
HP0 实测能跑约 800 MB/s(见第 12 篇),单方向 373 MB/s 占用率约 47%,有余量。
3. 1080p60 时序参数:VTC 的每一个数字
VTC 需要精确的时序参数才能输出符合 HDMI 标准的 1920×1080@60Hz 信号。这些数字来自 VESA/CEA 标准:
| 参数 | 水平(H)值 | 垂直(V)值 | 说明 |
|---|---|---|---|
| Active(有效像素/行) | 1920 | 1080 | 实际画面 |
| Front Porch(前廊) | 88 | 4 | 有效区结束到同步脉冲前 |
| Sync Width(同步脉冲) | 44 | 5 | HSYNC / VSYNC 高脉冲宽度 |
| Back Porch(后廊) | 148 | 36 | 同步脉冲结束到有效区开始 |
| Total(总长) | 2200 | 1125 | 1920+88+44+148 / 1080+4+5+36 |
| Pixel Clock | 148.5 MHz | — | 2200 × 1125 × 60 = 148.5 MHz |
🚧 避坑:CEA-861 标准里 1080p60 的 pixel clock 是 148.5 MHz,不是 148 MHz 也不是 150 MHz。Zynq 的 MMCM 能精确锁到 148.5 MHz(通过合理设置 CLKFBOUT_MULT_F 和 CLKOUT_DIVIDE)。如果你用 150 MHz,显示器大概率能显示但画面会有轻微抖动,某些 HDMI 接收器会拒绝信号。Clocking Wizard 里设置目标频率 148.5 MHz,Vivado 会自动算分频参数。
Clocking Wizard 参数(PL 输入时钟 200 MHz):
CLKIN1_PERIOD = 5.000 # 200 MHz 输入
CLKFBOUT_MULT_F = 7.425 # VCO = 200 * 7.425 = 1485 MHz
CLKOUT0_DIVIDE_F = 10.0 # 1485 / 10 = 148.5 MHz
CLKOUT1_DIVIDE = 2 # 1485 / 2 = 742.5 MHz(TMDS 5× 时钟,用于 HDMI 序列化)
在 VTC 寄存器里设置(裸机,偏移见 PG016):
/* VTC 基地址(Vivado Address Editor 里的分配值) */
#define VTC_BASE 0x43C20000
/* VTC 寄存器偏移 */
#define VTC_CTL 0x000 /* Control */
#define VTC_GASIZE 0x060 /* Generator Active Size:低16位=行有效像素-1,高16位=帧有效行-1 */
#define VTC_GFENC 0x068 /* Generator Frame Encoding(field/frame)*/
#define VTC_GHSIZE 0x070 /* H total size */
#define VTC_GVSIZE 0x074 /* V total size */
#define VTC_GHSYNC 0x078 /* H sync 起始/结束 */
#define VTC_GVSYNC 0x07C /* V sync 起始/结束 */
#define VTC_GHBPST 0x080 /* H back porch 起始 */
#define VTC_GVBPST 0x084 /* V back porch 起始 */
/* 1920×1080@60Hz 参数写入 */
void vtc_config_1080p60(void __iomem *vtc_base)
{
/* 先停止 VTC */
iowrite32(0x00000000, vtc_base + VTC_CTL);
/* Active area: 1920 cols, 1080 rows(均为值-1) */
iowrite32((1079 << 16) | 1919, vtc_base + VTC_GASIZE);
/* Total: H=2200, V=1125 */
iowrite32(2199, vtc_base + VTC_GHSIZE);
iowrite32(1124, vtc_base + VTC_GVSIZE);
/* Hsync: 起始列 2008 (1920+88), 结束列 2052 (2008+44) */
iowrite32((2051 << 16) | 2008, vtc_base + VTC_GHSYNC);
/* Vsync: 起始行 1084 (1080+4), 结束行 1089 (1084+5) */
iowrite32((1088 << 16) | 1084, vtc_base + VTC_GVSYNC);
/* Back porch: H 从列 2052 到 2199(实际是 active 从 2200-148=2052 开始) */
iowrite32(2052, vtc_base + VTC_GHBPST);
iowrite32(1089, vtc_base + VTC_GVBPST);
/* 启动 VTC,生成器模式 */
iowrite32(0x00000003, vtc_base + VTC_CTL); /* GEN_ENABLE | ENABLE */
}
4. VDMA 配置详解
4.1 IP 配置参数
在 Vivado 里双击 AXI VDMA IP:
| 配置项 | 设置值 | 说明 |
|---|---|---|
| Enable Read Channel | ✅ | MM2S(DDR → PL,用于显示输出) |
| Enable Write Channel | ✅(如需摄像头输入) | S2MM(PL → DDR) |
| Frame Buffers | 3 | Triple Buffer,避免撕裂 |
| Memory Map Data Width | 64 | 对应 HP0 64-bit 接口 |
| Stream Data Width | 32 | RGB24 每像素 3 字节,对齐到 32-bit(1 字节浪费,或用 24-bit 流) |
| Max Frame Store | 3 | 最大帧缓冲数 |
| Line Buffer Depth | 2048 | 行缓冲深度,1920 像素以上必须 ≥ 2048 |
| Video Format | RGB | 与 TPG 输出格式一致 |
🚧 避坑:Line Buffer Depth 必须 ≥ 水平分辨率。如果你把它设成 1024 然后跑 1920 列,VDMA 会在行末尾丢数据,症状是画面上出现规律性水平撕裂条纹,但 dmesg 里没有任何报错。
4.2 Free-running 模式 vs Frame-locked 模式
Free-running(自由运行):
VDMA 持续循环读取 DDR 帧缓冲,不和 VSYNC 同步。这是最简单的配置:
- 帧地址寄存器(
MM2S_Start_Address_1/2/3)在启动时写一次,之后 VDMA 自己循环 - PS 端可以在 VDMA 读完一帧后更新下一帧的 DDR 内容,但不精确同步
- 适合静态图像显示、单帧更新频率 < 30 fps 的场景
- 风险:撕裂(VDMA 读到一半,PS 已经写新数据)
Frame-locked(帧同步):
VDMA 用 VTC 的 VSYNC 信号作为帧切换信号,只在 VSYNC 期间切换帧地址:
- 需要把 VTC 的
vtiming_out接到 VDMA 的s_axis_vid_tvalid时序输入 - VDMA 的
PARKPTR寄存器指向当前显示帧;PS 端向空闲帧写数据,写完后切PARKPTR - 适合需要平滑显示的场景(视频播放、UI 渲染)
实际推荐:绝大多数有显示需求的场景,用 Frame-locked + Triple Buffer(3 帧:显示、备用、写入,三路轮换),完全消除撕裂。
5. VDMA 寄存器操作(裸机 + Linux 两种方式)
5.1 裸机寄存器直接写(Vitis / Standalone)
VDMA MM2S 通道关键寄存器(偏移来自 PG020):
| 偏移 | 名称 | 写入值 | 说明 |
|---|---|---|---|
0x00 | MM2S_VDMACR | 0x00010003 | RS=1(运行),Circular_Park=1,FSYNC=1 |
0x18 | MM2S_PARK_PTR_REG | 0x00000000 | RdFramePtr=0(从帧0开始) |
0x28 | MM2S_START_ADDR_1 | 0x10000000 | 帧0 DDR 地址 |
0x2C | MM2S_START_ADDR_2 | 0x10800000 | 帧1 DDR 地址(偏移 8MB) |
0x30 | MM2S_START_ADDR_3 | 0x11000000 | 帧2 DDR 地址(偏移 16MB) |
0x58 | MM2S_HSIZE | 1920 × 3 = 5760 | 每行字节数 |
0x5C | MM2S_FRMDLY_STRIDE | 5760 | stride = hsize(无对齐 padding) |
0x60 | MM2S_VSIZE | 1080 | 帧行数,写此寄存器触发传输开始 |
/*
* vdma_start_display.c — 裸机启动 VDMA MM2S(Vitis Standalone)
*
* 假设:
* - VDMA 基地址 0x43000000
* - Frame Buffer 0 在 DDR 0x10000000(16MB 对齐)
* - 1920×1080 RGB24,stride = 5760 bytes/line
*/
#include "xaxivdma.h"
#include "xparameters.h"
#define VDMA_ID XPAR_AXI_VDMA_0_DEVICE_ID
#define FB0_ADDR 0x10000000UL
#define FB1_ADDR 0x10780000UL /* 1920*1080*3 = 6,220,800 ≈ 0x5F1000 → 对齐 0x600000 */
#define FB2_ADDR 0x10F00000UL
#define H_PIXELS 1920
#define V_LINES 1080
#define BYTES_PER_PIXEL 3
#define STRIDE (H_PIXELS * BYTES_PER_PIXEL) /* 5760 */
int vdma_start_display(void)
{
XAxiVdma vdma;
XAxiVdma_Config *cfg;
XAxiVdma_DmaSetup rd_cfg;
int ret;
cfg = XAxiVdma_LookupConfig(VDMA_ID);
if (!cfg) return XST_FAILURE;
ret = XAxiVdma_CfgInitialize(&vdma, cfg, cfg->BaseAddress);
if (ret != XST_SUCCESS) return ret;
/* 配置 MM2S(读通道 → 显示输出) */
rd_cfg.VertSizeInput = V_LINES;
rd_cfg.HoriSizeInput = STRIDE;
rd_cfg.Stride = STRIDE;
rd_cfg.FrameDelay = 0;
rd_cfg.EnableCircularBuf = 1; /* 循环帧缓冲 */
rd_cfg.EnableSync = 1; /* 帧同步模式 */
rd_cfg.PointNum = 0;
rd_cfg.EnableFrameCounter = 0;
rd_cfg.FixedFrameStoreAddr = 0;
rd_cfg.FrameStoreStartAddr[0] = FB0_ADDR;
rd_cfg.FrameStoreStartAddr[1] = FB1_ADDR;
rd_cfg.FrameStoreStartAddr[2] = FB2_ADDR;
ret = XAxiVdma_DmaConfig(&vdma, XAXIVDMA_READ, &rd_cfg);
if (ret != XST_SUCCESS) return ret;
ret = XAxiVdma_DmaSetBufferAddr(&vdma, XAXIVDMA_READ, rd_cfg.FrameStoreStartAddr);
if (ret != XST_SUCCESS) return ret;
ret = XAxiVdma_DmaStart(&vdma, XAXIVDMA_READ);
if (ret != XST_SUCCESS) return ret;
xil_printf("VDMA MM2S 已启动,Triple Buffer 模式\r\n");
return XST_SUCCESS;
}
帧切换(显示下一帧,裸机):
/* 把 FB1 填满新内容后,切换到 FB1 显示 */
void vdma_switch_to_frame(XAxiVdma *vdma, int frame_idx)
{
/*
* PARKPTR 寄存器低 5 位 = RdFramePtr,VDMA 在下一个 VSYNC 时切到此帧
* 非 Circular 模式下需要重写 HSIZE/VSIZE 触发新帧
*/
XAxiVdma_StartParking(vdma, frame_idx, XAXIVDMA_READ);
}
5.2 Linux 内核:通过 V4L2 或直接 MMIO
PetaLinux 2023.2 里有 xilinx-vdma 驱动,但 V4L2 子系统的完整支持需要额外的 xlnx_v_vdma_proc 设备驱动链。对于直接写 VDMA 帧地址的简单需求,用 /dev/mem 调试或 UIO(见第 11 篇 UIO 方法)最简单:
/* linux_vdma_ctrl.c — 通过 /dev/mem 控制 VDMA(仅调试,生产用 UIO 或内核驱动) */
#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#define VDMA_BASE 0x43000000UL
#define MAP_SIZE 0x10000
/* MM2S 寄存器偏移 */
#define MM2S_VDMACR 0x00
#define MM2S_PARK_PTR 0x18
#define MM2S_START_1 0x28
#define MM2S_START_2 0x2C
#define MM2S_START_3 0x30
#define MM2S_HSIZE 0x58
#define MM2S_STRIDE 0x5C
#define MM2S_VSIZE 0x60 /* 写这个触发帧传输 */
#define FB0 0x10000000UL
#define FB1 0x10780000UL
#define FB2 0x10F00000UL
int main(void)
{
int fd = open("/dev/mem", O_RDWR | O_SYNC);
void *base = mmap(NULL, MAP_SIZE, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, VDMA_BASE);
#define WR(off, val) (*(volatile uint32_t*)((uint8_t*)base+(off)) = (val))
/* 停止 VDMA */
WR(MM2S_VDMACR, 0x00000000);
usleep(1000);
/* 帧地址 */
WR(MM2S_START_1, (uint32_t)FB0);
WR(MM2S_START_2, (uint32_t)FB1);
WR(MM2S_START_3, (uint32_t)FB2);
/* 帧尺寸:stride 和 hsize(字节) */
WR(MM2S_HSIZE, 1920 * 3); /* 5760 */
WR(MM2S_STRIDE, 1920 * 3); /* 5760 */
/* 启动(RS=1,Circular,GenLockSrc=0) */
WR(MM2S_VDMACR, 0x00010003);
/* 写 VSIZE 触发 → 开始传输 */
WR(MM2S_VSIZE, 1080);
printf("VDMA MM2S 已启动\n");
munmap(base, MAP_SIZE);
close(fd);
return 0;
}
6. 常见故障排查
6.1 画面撕裂(Tearing)
症状:显示画面出现水平分界线,上下两部分属于不同帧的内容。
根因:使用 Free-running 模式,VDMA 读取帧缓冲时 PS 同时在写同一块 DDR。
修复:
- 改用 Frame-locked 模式(
FSYNCbit 设为 1) - 使用 Triple Buffer(3 帧),确保 VDMA 读、PS 写、备用三帧互不冲突
- 确认 VTC 的 VSYNC 已正确连接到 VDMA 的同步输入
6.2 画面偏移(Image Shift / Pixel Drift)
症状:画面整体向右或向下偏移若干像素,有时伴随边缘绿色或黑色条纹。
根因 A:stride(行步进)配置错误。stride 是每行的 字节数(含对齐 padding),不是像素数。RGB24 + 1920 列时 stride = 5760,如果误填成 1920 会导致 VDMA 读行地址偏移。
根因 B:帧缓冲 DDR 起始地址不是 AXI DMA 要求的对齐倍数(通常要求 4 字节或 64 字节对齐,Zynq 上至少 4 字节对齐)。
🚧 避坑:VDMA 的
MM2S_HSIZE填的是字节数(bytes per line),不是像素数(pixels per line)。RGB24 是1920 × 3 = 5760。填错这个值是最常见的画面偏移原因。
6.3 帧缓冲数量不足
症状:视频播放时偶发闪烁,或高速场景(如滚动字幕)出现撕裂。
根因:双缓冲(2 帧)在 PS 写帧速度比 VDMA 读帧速度慢时,无法保证总有一帧可读且完整。
修复:改用三帧(Frame Buffers = 3),VDMA 的 FRMSTORE 寄存器设为 3,DDR 里预分配 3 块帧内存。
6.4 Genlock 相关问题
Genlock 用于多路 VDMA 之间的帧同步(例如捕获通道和显示通道同步):
| Genlock 模式 | 配置 | 适用场景 |
|---|---|---|
| Dynamic Genlock | S2MM 为 Master,MM2S 为 Slave | 摄像头输入同步显示,帧率由输入决定 |
| Static Genlock | 两通道独立固定帧率 | 静态图像循环显示 |
| Disabled | 无同步 | 只有显示,无输入 |
常见 Genlock 问题:如果 MM2S 帧率(由 pixel clock 决定)和 S2MM 帧率(由摄像头时序决定)不完全匹配(哪怕差 1 ppm),长时间运行后会发生帧跳(drop 或 repeat)。解决方案是给 Clocking Wizard 加 MMCM 的动态相位调整,或接受偶发帧跳(对大多数应用可接受)。
7. HDMI Tx 子系统配置要点
Pynq-Z2 上的 HDMI 输出路径有两种常见方案:
方案 A:rgb2dvi IP(免费,简单)
来自 Digilent 的 IP(在 Vivado IP Catalog 或 GitHub 上获取),把 RGB 并行视频 + 时序信号转成 TMDS 编码输出,走 HDMI 连接器。Pynq-Z2 板子上的 HDMI Tx 连接器接的就是 PL IO,可以直接用 rgb2dvi。
方案 B:Xilinx HDMI 1.4/2.0 Tx Subsystem(需要 license)
功能完整,支持 HDCP、音频嵌入,但需要付费 license。个人项目和学习用 rgb2dvi 足够。
rgb2dvi IP 连线:
VTC 的 vtiming_out ──→ rgb2dvi 的 vid_io_in_timing
AXI-Stream 视频数据 ──→ AXI4-Stream to Video Out IP ──→ rgb2dvi 的 vid_io_in (RGB 24-bit)
Pixel Clock(148.5M)──→ rgb2dvi 的 PixelClk
TMDS_Clk_p/n ──→ HDMI 连接器差分时钟引脚
TMDS_Data_p/n[2:0] ──→ HDMI 连接器 3 路差分数据引脚
XDC 引脚约束(Pynq-Z2 HDMI Tx,来自官方 Master XDC):
# HDMI TX(来自 Pynq-Z2 Master XDC)
set_property -dict {PACKAGE_PIN L17 IOSTANDARD TMDS_33} [get_ports {hdmi_tx_clk_p}]
set_property -dict {PACKAGE_PIN L18 IOSTANDARD TMDS_33} [get_ports {hdmi_tx_clk_n}]
set_property -dict {PACKAGE_PIN K17 IOSTANDARD TMDS_33} [get_ports {hdmi_tx_data_p[2]}]
set_property -dict {PACKAGE_PIN K18 IOSTANDARD TMDS_33} [get_ports {hdmi_tx_data_n[2]}]
set_property -dict {PACKAGE_PIN G17 IOSTANDARD TMDS_33} [get_ports {hdmi_tx_data_p[1]}]
set_property -dict {PACKAGE_PIN H17 IOSTANDARD TMDS_33} [get_ports {hdmi_tx_data_n[1]}]
set_property -dict {PACKAGE_PIN D19 IOSTANDARD TMDS_33} [get_ports {hdmi_tx_data_p[0]}]
set_property -dict {PACKAGE_PIN D20 IOSTANDARD TMDS_33} [get_ports {hdmi_tx_data_n[0]}]
🚧 避坑:
rgb2dviIP 的kGenerateSerialClk参数默认是true(内部生成 5× 串行时钟),如果你想用外部 MMCM 生成的 5× 时钟(更稳定),需要把它改为false并连接SerialClk。用内部生成时,pixel clock 抖动略大,某些严格的显示器(特别是 4K 显示器做降频输入)可能拒绝信号。
8. 资源占用参考
以下数据来自 Vivado 2023.2,Pynq-Z2(XC7Z020),实现后:
| IP 模块 | LUT | FF | BRAM | DSP |
|---|---|---|---|---|
| AXI VDMA v6.3(读写均开,3帧) | 2,847 | 3,912 | 4 | 0 |
| VTC v6.2 | 284 | 312 | 0 | 0 |
| TPG v8.2(1080p) | 1,156 | 1,024 | 2 | 2 |
| rgb2dvi(TMDS 编码) | 98 | 64 | 0 | 0 |
| AXI SmartConnect + Interconnect | 1,245 | 1,087 | 0 | 0 |
| 合计 | 5,630 | 6,399 | 6 | 2 |
| XC7Z020 可用 | 53,200 | 106,400 | 140 | 220 |
| 占用率 | 10.6% | 6.0% | 4.3% | 0.9% |
资源占用不多,还有充足空间加 HLS 算法或 DSP 模块。
9. 本篇 Checklist
- Pixel clock 锁到 148.5 MHz(不是 148 或 150),Clocking Wizard 里验证 locked 信号
- VTC H total = 2200,V total = 1125(CEA-861 标准值)
- VDMA Line Buffer Depth ≥ 2048(≥ 水平分辨率)
- VDMA
MM2S_HSIZE填字节数(1920 × 3 = 5760),不是像素数 - Frame Buffers = 3(Triple Buffer),DDR 里预留 3 × 6.2 MB ≈ 18.7 MB
- HP0 端口 Data Width = 64-bit,确认 VDMA 接的是 HP 端口不是 GP 端口
- 用 Frame-locked 模式避免撕裂,VTC VSYNC 正确接到 VDMA 同步输入
10. 下一篇预告
下一篇 《Zynq 实战 14|AXI-Stream 实战:高速 ADC 采集与流式 DSP》 会深入 AXI4-Stream 协议本身:
- 自己写一个 AXI4-Stream Master IP(产生 sin 波样本,模拟 ADC 数据流)
- 接 FIFO + AXI DMA S2MM 进 DDR
- 分析 100 Msps × 16-bit = 200 MB/s 的可行性,背压(backpressure)测试
- 在流上串一个 FIR 滤波器 IP,做实时流式 DSP
参考资料
| 文档号 | 名称 | 用途 |
|---|---|---|
| PG020 | AXI Video DMA v6.3 Product Guide | VDMA 寄存器映射(MM2S/S2MM 偏移)、Genlock 配置、Frame Buffer 地址格式 |
| PG016 | Video Timing Controller v6.2 Product Guide | VTC 寄存器详解,1080p/720p/VGA 标准时序参数表 |
| PG278 | AXI4-Stream to Video Out v4.0 Product Guide | Stream → 并行视频信号的时序转换,连接 VTC 的方式 |
| CEA-861 | A DTV Profile for Uncompressed High Speed Digital Interfaces | 1080p60 标准时序(H total 2200 / V total 1125 / pixel clock 148.5 MHz)权威来源 |
| UG585 | Zynq-7000 SoC TRM | HP 端口带宽规格(第 9 章),PS DDR 地址映射 |
| Digilent GitHub | Digilent/vivado-library / ip / rgb2dvi | rgb2dvi IP 源码和例程,Pynq-Z2 HDMI Tx 连接示例 |
这是《Zynq FPGA 嵌入式系统设计实战》系列第 13 篇。 如果你在 VTC 时序参数、VDMA 撕裂或 Pixel Clock 锁相上遇到了问题,欢迎留言。