To create a complete video display pipeline in the Zynq FPGA that outputs 720p60 HDMI video.
We are building a custom “graphics card” inside the programmable logic:
┌─────────────┐ ┌──────────────────┐ ┌──────────┐ ┌──────┐
│ AXI VDMA │───►│ axis_to_video │───►│ rgb2hdmi │───►│ HDMI │
│(Framebuffer)│ │ (custom RTL) │ │(Digilent)│ │ Out │
└─────────────┘ └──────────────────┘ └──────────┘ └──────┘
▲ ▲
DDR RAM VTC Timing
DRM writes pixels → VDMA streams → sync with VTC → encode → display
Key Features:
Note: This step covers Vivado hardware design only. Linux driver configuration is Step 8.
Before starting:
We need two external components: a custom Verilog module for timing synchronization and the Digilent IP library for HDMI encoding.
1. Download Custom RTL: Download the helper module that bridges standard AXI-Stream video to the native video timing signals. This is a small module that is tailored to the specific requirements of the project, there is also a Xilinx IP core that can be used, but it is much larger and more complex.
axis_to_video.v2. Clone the Digilent Vivado Library:
The PYNQ-Z2 HDMI port uses TMDS signaling, which requires a specialized encoder. Xilinx doesn’t provide native TMDS IP for Zynq-7000, so we use Digilent’s open-source rgb2hdmi encoder.
# Navigate to a directory for third-party IP (outside your project)
cd ~/xilinx_ip # Or C:\xilinx_ip on Windows
git clone https://github.com/Digilent/vivado-library.git
3. Add Repository to Vivado:
vivado-library folder you just clonedVerification: You should see a message in the Tcl Console:
INFO: Repository 'vivado-library' added.
INFO: Found 1 IP(s) and 0 interfaces and 0 design(s) in '.../vivado-library/ip'
The IP catalog should now include RGB to DVI Video Encoder (Digilent).
The PS needs high-bandwidth access to DDR RAM for video data and additional clocks for video processing.
1. Open Block Design:
system_design.bd)2. Enable High-Performance Port:
3. Enable PL Fabric Clocks:
4. Enable Interrupts:
5. Apply Changes:
We’ll add the IP blocks that form the video pipeline.
1. Add Clocking Wizard:
Add IP:
Configure:
clk_out1 → pixel_clkclk_out2 → serial_clk2. Add AXI VDMA:
Add IP:
Configure:
3. Add Video Timing Controller (VTC):
Add IP:
Configure:
4. Add axis_to_video Custom Module:
This module bridges the AXI-Stream from VDMA to parallel video signals synchronized with VTC timing.
axis_to_video.vConfigure axis_to_video:
The module has parameters with defaults:
No GUI configuration needed if using default parameters.
5. Add RGB to DVI Video Encoder (Digilent):
Add IP:
Configure:
false (Active Low reset)Video pipelines are sensitive to clock domains. Each signal must be on the correct clock. Follow carefully.
Connect Clocking Wizard Input:
clk_in1 → Zynq PS FCLK_CLK0 (100 MHz)resetn → Processor System Reset peripheral_aresetn[0]Run Connection Automation:
100 MHz Clock Domain (AXI Control):
Connect Zynq PS FCLK_CLK0 to this should be done already by Connection Automation:
s_axi_lite_aclk (control register access)m_axi_mm2s_aclk (DDR memory read clock)s_axi_aclk (control registers)M_AXI_GP0_ACLK (already connected via automation)S_AXI_HP0_ACLK (HP port clock)74.25 MHz Clock Domain (Pixel Clock):
Connect Clocking Wizard pixel_clk (74.25 MHz) to:
m_axis_mm2s_aclk (AXI-Stream output clock)clk (generates timing at pixel rate)video_clkPixelClk371.25 MHz Clock Domain (Serial Clock):
Connect Clocking Wizard serial_clk (371.25 MHz) to:
SerialClk2. Add Reset Controller for Video Domain:
The 74.25MHz domain needs its own reset controller.
slowest_sync_clk ← pixel_clk (from Clocking Wizard)ext_reset_in ← PS FCLK_RESET0_Ndcm_locked ← locked (from Clocking Wizard)3. Reset Connections (74.25 MHz Domain):
Connect peripheral_aresetn from this new Reset block to:
resetnresetnaRst_n (if present)4. LED Status Wiring:
locked pin on Clocking Wizard.clk_locked.5. Interrupt Connection (Critical for Linux):
mm2s_introut (VDMA) → IRQ_F2P (Zynq PS)
IRQ_F2P is a bus, connect it to the first bit [0:0] or use an AXI Concat block if you have multiple interrupts. For this single connection, direct wire usually works and defaults to ID 61 (29 + 32).Vivado automation often fails here. Perform these manually:
A. VDMA to Axis_to_Video: A. VDMA to Axis_to_Video:
M_AXIS_MM2S directly to axis_to_video s_axis.
(Vivado should automatically group the TDATA, TVALID, TREADY, etc. signals)B. VTC to Axis_to_Video (Video Timing):
vtiming_out interface.axis_to_video:
active_video → vtc_active_videohsync_out → vtc_hsyncvsync_out → vtc_vsyncf_sync_out → vtc_fsyncC. Axis_to_Video to RGB2HDMI:
RGB interface.axis_to_video outputs to rgb2hdmi inputs:
vid_active_video → vid_pVDEvid_data → vid_pDatavid_hsync → vid_pHSyncvid_vsync → vid_pVSyncHDMI Output:
Make the HDMI TMDS signals external:
TMDS (differential pair bus)TMDS → Make ExternalTMDS_0_clk_n, TMDS_0_clk_p, etc.TMDS_0 (the bus) in the diagram.hdmi_txhdmi_tx_clk_p, etc.).Debug Signal (optional but recommended):
Expose the PLL lock status to an LED:
locked outputlocked → Make Externalclk_lockedBefore generating bitstream:
Expected Validation Messages:
The abstract hdmi_tx port must be mapped to actual FPGA pins on the PYNQ-Z2 board.
1. Create Constraints File:
hdmi_pinout.xdc2. Add Pin Constraints:
Open hdmi_pinout.xdc and add the following:
# HDMI TX Signals
set_property -dict { PACKAGE_PIN L16 IOSTANDARD TMDS_33 } [get_ports { hdmi_tx_clk_p }];
set_property -dict { PACKAGE_PIN L17 IOSTANDARD TMDS_33 } [get_ports { hdmi_tx_clk_n }];
set_property -dict { PACKAGE_PIN K17 IOSTANDARD TMDS_33 } [get_ports { hdmi_tx_data_p[0] }];
set_property -dict { PACKAGE_PIN K18 IOSTANDARD TMDS_33 } [get_ports { hdmi_tx_data_n[0] }];
set_property -dict { PACKAGE_PIN K19 IOSTANDARD TMDS_33 } [get_ports { hdmi_tx_data_p[1] }];
set_property -dict { PACKAGE_PIN J19 IOSTANDARD TMDS_33 } [get_ports { hdmi_tx_data_n[1] }];
set_property -dict { PACKAGE_PIN J18 IOSTANDARD TMDS_33 } [get_ports { hdmi_tx_data_p[2] }];
set_property -dict { PACKAGE_PIN H18 IOSTANDARD TMDS_33 } [get_ports { hdmi_tx_data_n[2] }];
# Debug LED (LD0) - Maps to Clock Lock status
set_property -dict { PACKAGE_PIN R14 IOSTANDARD LVCMOS33 } [get_ports { clk_locked }];
Save the file.
Now we build the design and create the hardware description for PetaLinux.
Generate Bitstream:
Expected Duration: 10-30 minutes depending on your machine
Build Progress:
Check for Errors:
After completion, check the Messages panel:
Export Hardware:
Once bitstream generation succeeds:
<petalinux_project>/project-spec/hw-description/system.xsa (or system_design_wrapper.xsa)The XSA file contains:
You can test the bitstream before configuring Linux.
Program FPGA via JTAG:
.bit file: <project>/impl_1/system_design_wrapper.bit (this should automatically be selected)Expected Behavior:
✅ LED0 (LD0) turns ON: PLL is locked, clocks are stable
❌ LED0 off or flickering: Clock generation issue - check power supply or PLL config
❓ HDMI monitor shows “No Signal”: Expected - VDMA not configured yet (needs Linux driver)
At this stage: The hardware is programmed but idle. The VDMA has no framebuffer address configured, so no video data flows. This is normal and will be fixed in Step 8 when we configure the DRM driver.
You’ve created a complete video output pipeline in the Zynq FPGA:
✅ AXI VDMA - Fetches framebuffer pixels from DDR RAM
✅ axis_to_video - Synchronizes AXI-Stream with video timing, strips alpha channel
✅ Video Timing Controller - Generates 720p60 sync signals
✅ rgb2hdmi - Encodes RGB to HDMI TMDS for physical output
✅ Clock generation - Produces 74.25 MHz pixel clock and 371.25 MHz serial clock
✅ Pin constraints - Maps signals to physical PYNQ-Z2 HDMI connector
Linux DRM → DDR RAM ← VDMA → axis_to_video + VTC → rgb2hdmi → HDMI Monitor
(framebuffer) (read) (sync) (encode) (display)
system.xsa - Hardware description exported to PetaLinuxsystem_design_wrapper.bit - FPGA bitstreamStep 8: Linux Configuration (Software Side)
Now that the hardware exists, we need to configure PetaLinux to:
CONFIG_DRM_XLNX_PL_DISP=y)xlnx_dummy_connector for display mode reporting/dev/fb0 and /dev/dri/card0 appearmodetest to show test patternsThe hardware is ready. Next step: teach Linux how to use it!
Problem: Bitstream generation fails with timing violations
Solution:
Problem: Can’t find rgb2hdmi IP in catalog
Solution:
ip/ subdirectoryProblem: Port names don’t match constraints
Solution:
# List all external ports
report_ports
# Or in Tcl console:
get_ports *hdmi*
# Update constraint file with actual names
Problem: Design validation shows unconnected ports
Solution:
Problem: Address Editor shows overlapping addresses
Solution:
For detailed software configuration, see Step 8 and the main [README.md](../README.m
create_clock -period 10.000 -name fclk0 [get_pins processing_system7_0/inst/PS7_i/FCLKCLK[0]]
set_clock_groups -asynchronous
-group [get_clocks fclk0]
-group [get_clocks pixel_clk]
-group [get_clocks serial_clk]
**Key Points:**
- **TMDS_33:** I/O standard for HDMI differential signaling at 3.3V
- **clk_locked LED:** If this LED is solid ON after programming, clocks are stable
- **Timing constraints:** Help Vivado optimize the design to meet timing requirements
- **Asynchronous clock groups:** Tell Vivado these clocks are unrelated (prevents false path analysis)
**3. Verify Port Names Match:**
If your external port names differ (e.g., `TMDS_0_clk_p` instead of `hdmi_tx_clk_p`), update the constraint file accordingly:
```tcl
# Check actual port names in the block design
# Flow Navigator → Open Elaborated Design → I/O Ports window
Or use generic wildcards:
set_property -dict {PACKAGE_PIN L16 IOSTANDARD TMDS_33} [get_ports {*clk_p}]
Before we start fighting with Linux drivers, we must verify the “Digital Physics” of our design.
1. The “Heartbeat” Check (LED Debugging)
We want physical proof that our 74.25MHz/371MHz clocks are stable.
2. Timing Closure Analysis
The HDMI serializer runs at 371.25 MHz. This is fast for the Zynq-7000 fabric.
You have physically wired a Video Card inside your chip.
Next Step: In Step 8, we will return to PetaLinux to enable the xlnx-drm drivers, which will detect this pipeline and expose it to Debian as /dev/dri/card0 (a standard graphics card).
The video pipeline is wired. Now we need to teach Linux how to drive it.