Vivado & the PYNQ-Z2
Installing Vivado, the PYNQ-Z2 board and its parts, creating a project, the XDC constraint file, and the flow from synthesis to bitstream.
1. From text to wires
Up to now every design in this course has lived in a simulator. A simulator only needs to know what your circuit does. To put it on a chip, a tool must decide which of the chip's tens of thousands of look-up tables (LUTs) and flip-flops implement each piece of logic, where they sit on the silicon, and which programmable wires connect them. For AMD (formerly Xilinx) FPGAs that tool is Vivado. Its final output is a bitstream (a .bit file): a few megabytes of configuration bits that set every LUT's truth table, every flip-flop's options and every routing switch on the chip.
This chapter is the practical bridge. We tour the PYNQ-Z2 board, install Vivado, create a project, write the constraint file that ties port names to physical pins, and follow one small design through every step of the flow — reading the reports that tell you whether the result can be trusted. Chapter 14 then builds the first real design on the board, and Chapter 15 explains the timing numbers you meet here.
2. A tour of the PYNQ-Z2
The PYNQ-Z2 (made by TUL) is built around a Zynq-7000 XC7Z020, part number XC7Z020-1CLG400C. A Zynq is two things in one package: a processing system (PS) — a dual-core ARM Cortex-A9 running at 650 MHz on this board, with 512 MB of DDR3 memory — and programmable logic (PL), an Artix-7-class FPGA fabric. Parts I–VI of this course use only the PL; Part VII connects the two halves.
| Feature | Connected to | Used in this course |
|---|---|---|
| 125 MHz clock | PL pin H16 | every design: clk |
| 4 green LEDs LD0–3 | PL (R14, P14, N16, M14) | Ch. 13, 14 onwards |
| 2 RGB LEDs LD4, LD5 | PL (3 pins each) | this chapter |
| 4 buttons, 2 switches | PL | Ch. 14 onwards |
| 2 PMOD connectors | PL, 8 signals each | UART adapter, sensors |
| Arduino / Raspberry Pi headers | PL (mostly) | optional add-ons |
| HDMI in / out | PL | video projects |
| Audio codec | PL (I2S + I2C) | optional |
| USB-UART + JTAG (micro-USB) | PS UART / JTAG chain | programming, Linux console |
| Ethernet, USB host, micro-SD | PS | PYNQ Linux (Part VII) |
| 512 MB DDR3 | PS memory controller | Part VII (DMA) |
Two board details save hours of confusion. First, the boot jumper chooses where the Zynq loads its configuration at power-on. Set it to JTAG for the PL-only labs in Parts V–VI: the chip waits, and Vivado programs it over the micro-USB cable. Set it to SD to boot the PYNQ Linux image from the micro-SD card (Part VII). Second, the micro-USB serial port is wired to the PS, not the PL. A UART built in HDL cannot reach it directly; it needs a 3.3 V USB–serial adapter on a PMOD pin, or a path through the ARM processor.
LVCMOS33). The FPGA pins are not 5 V tolerant. Arduino shields designed for 5 V logic, or a 5 V USB–serial adapter on a PMOD, can damage the Zynq. Check the voltage of anything you plug in.3. Installing Vivado
Vivado is free for small and mid-sized devices. The no-cost edition (called Vivado ML Standard in recent releases, formerly WebPACK) includes the XC7Z020, so you need no licence for anything in this course. A few practical facts:
- Operating system: Linux or Windows only. On a Mac, run Vivado inside a Linux or Windows virtual machine (practical mainly on Intel Macs), or use a separate PC. Check the release notes for the exact supported distributions.
- Disk space: tens of gigabytes even when you install only the 7-series devices. Deselect the device families you do not need (UltraScale, Versal) to keep it down.
- Version: for PL-only designs any recent version works. Once you build overlays for the PYNQ Linux image (Part VII), use the version that matches your image: Vivado 2024.1 for PYNQ v3.1, 2022.1 for PYNQ v3.0.
- Download: from the AMD website, with a free AMD account. The web installer downloads only what you select.
- Cable drivers: on Linux, the JTAG cable drivers are installed by a separate script that ships with Vivado and needs root rights. Without them the Hardware Manager will not see the board.
4. Creating a project
Vivado can be driven two ways. In project mode the graphical interface keeps track of your sources, runs and reports in a project directory. In non-project mode you write a short Tcl script that runs each step explicitly (Section 9). Beginners usually start with project mode; the essential choices are the same.
- Start Vivado and create a new project. Give it a name and a path without spaces (spaces in paths are a classic cause of obscure tool errors).
- Choose RTL Project: you supply HDL sources. You may tick the option not to add sources yet.
- Choose the target: either the part
xc7z020clg400-1directly (filter by family Zynq-7000, package clg400, speed grade −1), or — if you have installed the TUL PYNQ-Z2 board files — the PYNQ-Z2 board, which selects the same part and also knows the board's interfaces for block designs in Part VII. - Set the project's target language (Verilog or VHDL). It only affects generated files; you can mix both kinds of sources.
- Add your design sources (
.vor.vhd), your simulation sources (testbenches, optional), and a constraints file (.xdc). Mark the top-level module as top.
The part number encodes everything the tools need: xc7z020 is the device, clg400 the 400-ball package (that is why pins have names like H16: row H, column 16 of the ball grid), and -1 the speed grade, the slowest of the family. Timing analysis uses delays for that speed grade, so a bitstream that meets timing for -1 works on every chip sold as -1.
5. A first design: switches, buttons and a colour cycle
Our example is deliberately simple but touches both kinds of logic. The four green LEDs show the two switches and two buttons directly — pure wires, no clock. The RGB LED LD4 cycles through its colours, driven by the top three bits of a free-running counter. Because RGB LEDs are dazzling at full current, the colour is only switched on one clock cycle in sixteen: a 7.8 MHz pulse that the eye averages to a gentle 6 % brightness. (This is the simplest form of pulse-width modulation.)
module sw_rgb #(
parameter CW = 27 // counter width: 2^27 cycles = 1.07 s at 125 MHz
) (
input wire clk, // 125 MHz, pin H16
input wire [1:0] sw, // SW0, SW1
input wire [1:0] btn, // BTN0, BTN1
output wire [3:0] led, // LD0-LD3
output wire [2:0] rgb // LD4: rgb[2]=R, rgb[1]=G, rgb[0]=B
);
// Purely combinational: switches and buttons straight to the green LEDs
assign led = {btn, sw};
// Free-running counter. The top three bits choose a colour (0..7),
// so the colour changes every 2^(CW-3) cycles.
reg [CW-1:0] count = 0;
always @(posedge clk)
count <= count + 1'b1;
// The RGB LED is very bright: light it only 1 cycle in 16 (6.25 % duty)
wire bright = (count[3:0] == 4'd0);
assign rgb = count[CW-1:CW-3] & {3{bright}};
endmoduleThe counter width CW is a parameter so that the testbench can shrink it to 8 bits: one full colour cycle then takes 256 clock cycles. The testbench checks all sixteen switch/button combinations, counts how many cycles the RGB LED is lit (2 per colour × 7 colours = 14) and checks that all seven non-black colours appear. Both languages print the same PASS at 2064 ns.
`timescale 1ns/1ps
module sw_rgb_tb;
reg clk = 0;
reg [1:0] sw = 2'b00, btn = 2'b00;
wire [3:0] led;
wire [2:0] rgb;
integer i, lit = 0, errors = 0;
reg [7:0] seen = 8'b0; // which colours appeared
// CW = 8: the colour changes every 32 cycles instead of every 0.13 s
sw_rgb #(.CW(8)) dut (.clk(clk), .sw(sw), .btn(btn), .led(led), .rgb(rgb));
always #4 clk = ~clk; // 125 MHz
initial begin
// 1. Green LEDs follow {btn, sw} for all 16 input combinations
for (i = 0; i < 16; i = i + 1) begin
{btn, sw} = i;
#1;
if (led !== i) errors = errors + 1;
end
// 2. One full counter period: 256 cycles
for (i = 0; i < 256; i = i + 1) begin
@(negedge clk);
if (rgb != 3'b000) begin
lit = lit + 1;
seen[rgb] = 1'b1;
end
end
if (errors == 0 && lit == 14 && seen == 8'b1111_1110)
$display("PASS: LEDs follow inputs, RGB lit %0d of 256 cycles, 7 colours", lit);
else
$display("FAIL: errors=%0d lit=%0d seen=%b", errors, lit, seen);
$finish;
end
endmodule# Verilog with Icarus iverilog -g2012 -o tb sw_rgb.v sw_rgb_tb.v && vvp tb # VHDL with GHDL ghdl -a --std=08 sw_rgb.vhd sw_rgb_tb.vhd ghdl -e --std=08 sw_rgb_tb && ghdl -r --std=08 sw_rgb_tb
6. The XDC constraint file
The HDL says there is an input called sw[0]. Nothing in it says that this input is ball M20 of the package. That is the job of the Xilinx Design Constraints (XDC) file. XDC is a set of Tcl commands, based on the industry-standard SDC format. Here is the complete file for sw_rgb:
## sw_rgb.xdc -- PYNQ-Z2 pin constraints (every user I/O here is 3.3 V)
## Clock: pin H16, 125 MHz = 8 ns period, 50 % duty (rises at 0, falls at 4)
set_property -dict { PACKAGE_PIN H16 IOSTANDARD LVCMOS33 } [get_ports clk]
create_clock -name sys_clk -period 8.000 -waveform {0.000 4.000} [get_ports clk]
## Slide switches SW0, SW1 -- bus bits are written name[index], in braces
set_property -dict { PACKAGE_PIN M20 IOSTANDARD LVCMOS33 } [get_ports {sw[0]}]
set_property -dict { PACKAGE_PIN M19 IOSTANDARD LVCMOS33 } [get_ports {sw[1]}]
## Push-buttons BTN0, BTN1
set_property -dict { PACKAGE_PIN D19 IOSTANDARD LVCMOS33 } [get_ports {btn[0]}]
set_property -dict { PACKAGE_PIN D20 IOSTANDARD LVCMOS33 } [get_ports {btn[1]}]
## Green LEDs LD0-LD3
set_property -dict { PACKAGE_PIN R14 IOSTANDARD LVCMOS33 } [get_ports {led[0]}]
set_property -dict { PACKAGE_PIN P14 IOSTANDARD LVCMOS33 } [get_ports {led[1]}]
set_property -dict { PACKAGE_PIN N16 IOSTANDARD LVCMOS33 } [get_ports {led[2]}]
set_property -dict { PACKAGE_PIN M14 IOSTANDARD LVCMOS33 } [get_ports {led[3]}]
## RGB LED LD4: rgb[0] = blue, rgb[1] = green, rgb[2] = red
set_property -dict { PACKAGE_PIN L15 IOSTANDARD LVCMOS33 } [get_ports {rgb[0]}]
set_property -dict { PACKAGE_PIN G17 IOSTANDARD LVCMOS33 } [get_ports {rgb[1]}]
set_property -dict { PACKAGE_PIN N15 IOSTANDARD LVCMOS33 } [get_ports {rgb[2]}]Read one line from the inside out:
get_ports {sw[0]}finds the top-level port object calledsw[0]. Bus bits are namedname[index]in both languages (VHDL'ssw(0)also becomessw[0]). The braces stop Tcl from treating[0]as a command to execute.get_ports led[*]with a wildcard would match all four bits.PACKAGE_PIN M20places that port on ball M20, which the board designers wired to switch SW0.IOSTANDARD LVCMOS33configures the I/O buffer for 3.3 V CMOS levels, matching the bank voltage on the board.set_property -dict { ... }sets several properties at once; two separateset_propertylines would be equivalent.
The clock line is different in kind. create_clock -period 8.000 does not configure any hardware; it tells the timing analyser that a clock with an 8 ns period (125 MHz) enters on port clk, rising at 0 ns and falling at 4 ns. Every path between flip-flops on that clock will then be checked against 8 ns. Without it Vivado still builds the design, but it cannot tell you whether it is fast enough — the timing summary reports the clock as unconstrained.
led and the XDC says LED, or you copied a master XDC for the whole board and left in pins your design does not have, Vivado warns that get_ports matched nothing and ignores the line. Then the real port is left unplaced, and bitstream generation stops with an error about unconstrained I/O. Read the critical warnings; they name the offending port.7. What each step of the flow does
Elaboration reads the HDL, resolves parameters/generics and the module hierarchy, and builds a generic netlist of adders, multiplexers, registers and comparators. Width mismatches, missing signals and accidental latches show up here. Vivado's RTL schematic view draws this netlist — a good way to check that the hardware you imagined is the hardware you described.
Synthesis maps that generic netlist onto the real resources of the 7-series fabric: 6-input LUTs, flip-flops, carry chains for adders and counters, block RAM, DSP48E1 slices for multipliers. It also optimises: constant logic disappears, duplicate registers merge, and logic that drives no output is removed. (If a whole block of yours vanishes from the utilisation report, it is usually because nothing used its output.)
Implementation has three main steps. opt_design cleans up the synthesised netlist. place_design assigns every LUT and flip-flop to a specific site on the die, trying to keep connected cells close. route_design chooses the programmable wires between them. Only after routing are the wire delays known exactly, so only the post-route timing report is final.
Bitstream generation runs design-rule checks (DRC) — every port placed, every I/O standard set, no combinational loops — and writes the .bit file. For a 7020 it is about 4 MB regardless of how much of the chip you use, because every configuration bit is written.
| sw_rgb resource | Expected | Why |
|---|---|---|
| Flip-flops | 27 | the 27-bit counter |
| LUTs | a few | the increment lives mostly in the carry chain; plus the rgb gating |
| CARRY4 | about 7 | 27 bits / 4 bits per carry block, rounded up |
| Bonded IOB | 12 | 1 clk + 2 sw + 2 btn + 4 led + 3 rgb |
| BUFG | 1 | the clock goes onto a global clock buffer |
Predicting the utilisation before you look is a powerful habit. If the report disagrees wildly with your prediction — 0 flip-flops, or thousands — you have misunderstood something about your own design.
8. Reading the reports
A bitstream is only as trustworthy as the reports behind it. After implementation, look at three things, in this order.
1. Messages. Errors stop the flow; critical warnings do not, but they usually mean the result is not what you intended: a constraint that matched no port, a port with no pin, a combinational loop. Ordinary warnings are worth a skim (for example, “signal is unused” or “latch inferred”).
2. Timing summary. For every clock path the tool computes the slack: how much earlier than required the data arrives. Positive slack means the path is fast enough. The summary reports:
| Number | Meaning | Must be |
|---|---|---|
| WNS | worst negative slack: setup slack of the worst path | ≥ 0 |
| TNS | total negative slack: sum of the negative setup slacks | = 0 |
| WHS | worst hold slack | ≥ 0 |
| THS | total hold slack (sum of negative hold slacks) | = 0 |
| Unconstrained | paths not covered by any clock | none you care about |
Design Timing Summary | WNS(ns) | TNS(ns) | TNS Failing Endpoints | WHS(ns) | THS(ns) | THS Failing Endpoints | | 4.871 | 0.000 | 0 | 0.172 | 0.000 | 0 | All user specified timing constraints are met.
That estimate (for the illustrative numbers above) is only a rough guide: the tools optimised for 8 ns, and a tighter constraint would change placement. Chapter 15 derives setup and hold slack properly and shows what to do when WNS is negative.
3. Utilisation. How much of each resource the design uses. On the XC7Z020 the totals are 53 200 LUTs, 106 400 flip-flops, 140 block RAMs of 36 Kb and 220 DSP48E1 slices. A design that fits on paper can still be hard to place and route when one resource is nearly full, because the placer has no freedom left. The program below parses an excerpt of a report_utilization text report — the same parsing you would use to track utilisation across builds — and plots it against the device totals.
Click Run to execute the Python code
Code will be executed with Python 3 on the server
9. The batch flow: one Tcl script
Everything the graphical interface does is a Tcl command underneath. In non-project mode you call those commands yourself, which makes the build reproducible, scriptable and easy to keep in version control. The whole flow for sw_rgb fits in a page:
# build.tcl -- non-project batch flow. Run with: # vivado -mode batch -source build.tcl set top sw_rgb set part xc7z020clg400-1 set out ./build file mkdir $out # 1. Read sources (use read_vhdl for .vhd files) and constraints read_verilog sw_rgb.v # read_vhdl sw_rgb.vhd read_xdc sw_rgb.xdc # 2. Synthesis: elaborate the HDL and map it to LUTs, flip-flops, carry chains synth_design -top $top -part $part write_checkpoint -force $out/post_synth.dcp report_utilization -file $out/util_synth.rpt # 3. Implementation: optimise, place, route opt_design place_design route_design write_checkpoint -force $out/post_route.dcp # 4. Reports to read before trusting the bitstream report_timing_summary -file $out/timing.rpt report_utilization -file $out/util_route.rpt report_drc -file $out/drc.rpt # 5. Bitstream write_bitstream -force $out/$top.bit
The checkpoints (.dcp) save the design after each stage; open_checkpoint build/post_route.dcp in the Vivado GUI lets you look at the placed and routed result, including the device view and the timing paths, without rebuilding. A design of this size builds in a minute or two on a laptop; most of that time is fixed start-up and device-loading overhead.
- Put
sw_rgb.v(or.vhd, then swap theread_*line),sw_rgb.xdcandbuild.tclin one directory. - Run
vivado -mode batch -source build.tclfrom a terminal where Vivado's settings script has been sourced (on Linux, thesettings64.shin the installation directory). - Open
build/timing.rpt: check that all constraints are met. Openbuild/util_route.rptand compare with the table in Section 7. - Boot jumper on JTAG, micro-USB connected, board on. In Vivado open the Hardware Manager, connect to the target, and program the device with
build/sw_rgb.bit. - Flip SW0/SW1 and press BTN0/BTN1: LD0–LD3 follow instantly. LD4 steps through its seven colours, about 0.13 s each, with a dark gap where the code is 000.
The bitstream is lost when power is removed: in JTAG mode the configuration lives only in the FPGA's SRAM cells. That is normal for SRAM-based FPGAs, and exactly what you want while developing.
Practice Problems
go on button BTN2 (pin L20) as a 3.3 V CMOS input, and explain why a port without these lines stops bitstream generation.sw_rgb the RGB LED is lit only when count[3:0] == 0. (a) What is the duty cycle and the frequency of that brightness pulse? (b) With CW = 27, how long does each of the eight colours last?uart_tx to the PL design and constrains it to the pin of the board's micro-USB UART, expecting to see characters in a terminal. Why can't this work, and what are two correct alternatives?