The PL is built with Vivado for part xc7z020clg400-1. The flow lives in pl/top/: a block
design generated by Tcl (axi_interconnect_map.tcl), the SystemVerilog top
(satlink_zynq7020.sv), ADI's HDL library for axi_ad9361 and axi_dmac, and one constraint
file per board. Full detail is in pl/top/BUILD.md.
Prerequisites#
| Requirement | Detail |
|---|---|
| Vivado | 2024.1 (2024.1.2 validated on hardware; 2023.2 also validated). pl/top/vivado-env.sh is the only place that names a version |
| Memory | Synthesis peaks around 7 GB. Use a disk swap file, not zram, if the machine is short |
| Time | About 35 to 45 minutes for a full build on 16 cores with VIVADO_JOBS=4 |
. pl/top/vivado-env.sh # sources Vivado and exports REQUIRED_VIVADO_VERSION
# override with VIVADO_VERSION=2023.2 or VIVADO_ROOT=<path>
export VIVADO_JOBS=4 # 2 on a 4-core / 8 GB machineThe one-command build#
cd pl/top
systemd-run --user --collect --unit=satlink-build bash build-noila.sh
journalctl --user -u satlink-build -fbuild-noila.sh runs the whole flow for the fishball board: the fast guards, the block design,
a forced re-synthesis, implementation, the timing gate, then the bitstream and the .xsa.
Artefacts land in pl/top/build/.
Other scripts in pl/top/:
| Script | Purpose |
|---|---|
build-noila.sh | Full production build, no debug core |
build-ila.sh | Full build inserting an ILA on every net marked (* mark_debug = "true" *) (none today, so it produces a debug-free bitstream) |
resume-ila.sh, build-from-synth.sh | Resume from a checkpointed synthesis |
check-timing.sh | The timing gate, called by every build script |
deploy-sd.sh | Convert the .bit to system_top.bin and copy it to the SD card (bitstream only) |
Read the directory before launching anything long: the tool you need usually exists.
Step by step#
cd pl/top
make guards # lints (fatal) — a few seconds
make area # area ceilings per module — about 10 minutes
make synth XDC=constraints/zynqsdr_fishball.xdc
cd build && vivado -mode batch -nolog -nojournal \
-source ../scripts/run_impl.tcl -tclargs satlink_zynq7020
vivado -mode batch -nolog -nojournal \
-source ../scripts/run_bit.tcl -tclargs satlink_zynq7020 satlink_zynq7020Call the implementation Tcl directly: make impl depends on synth. Set FORCE_RESYNTH=1
whenever the bitstream must provably match the sources (the build scripts do).
Constraints: one file per board#
| Board | XDC= |
|---|---|
| fishball (development) | constraints/zynqsdr_fishball.xdc |
| LibreSDR / zynqsdr rev5 (product) | constraints/zynqsdr_rev5.xdc |
Each board file has a <base>_timing.xdc companion, added automatically, holding the clock
groups and the timing exceptions used only in implementation.
The guards#
Checks that can fail, run before the long flow:
| Guard | Catches |
|---|---|
make guards → pl/tools/lint_inert_regs.py | CSR registers written and read back, but consumed by nothing |
make guards → pl/tools/lint_phantom_bits.py | Register bits software writes that no RTL decodes |
make guards → ps/tools/lint_unused_profile_fields.py | Profile fields nothing reads (a known list with tickets and dates is allowed) |
make area | Per-module LUT/BRAM/DSP ceilings (pl/top/scripts/area_budget.tsv); known breaches are listed with a ticket, new ones fail |
run_synth.tcl | Fails on RAM inferred as flip-flops (Synth 8-4767), except allow-listed RAMs |
check-timing.sh | Reads the routed timing report and refuses to write a bitstream with negative slack |
pl/tools/read_cdc_report.py | Clock-domain crossings without exceptions beyond the allowlist |
Keep the synthesis critical-warning count at zero: a dismissed "harmless" critical warning once hid unconstrained clock crossings and disabled Vivado's synthesis cache.
Checking a change before a full build#
An out-of-context synthesis of the affected module takes minutes and shows area and timing surprises:
synth_design -mode out_of_context -top <module> -part xc7z020clg400-1
report_utilization
report_timing_summaryThe DSP budget (220 DSP48E1) is the historical constraint of this design; the RRC filters fit
because they fold the linear-phase symmetry into the DSP pre-adders. See pl/top/DSP.md.
Simulation#
The cocotb testbenches under pl/sim/ run with Verilator:
. .venv/bin/activate
cd pl && make -C sim -f $PWD/tools/verilator.mk TEST=<name> PL_ROOT=$PWD simA passing simulation does not guarantee synthesis: Vivado rejects constructs Verilator accepts
(input logic under default_nettype none, data-dependent while loops).
Debug builds (ILA)#
scripts/run_ila.tcl runs between synthesis and implementation, probes every net carrying
(* mark_debug = "true" *), and writes its constraints into a dedicated build/debug_nets.xdc.
To capture:
- build with
build-ila.shand deploy withdeploy-sd.sh, which copies the.ltxproduced by that implementation; - open the hardware manager, select the FPGA device (
xc7z020_1, notarm_dap_0, which reports "0 ILAs found" without error), and load that.ltx; - trigger on the event rather than immediately: an immediate trigger on a bursty signal can land in a gap and show nothing.
Pitfalls: a .ltx older than the .bit describes another circuit; debug constraints must never
be saved into the active constraint set (a phantom ILA would then ride into every production
build); an ILA can only trigger on a running clock and will not tell you when its clock is dead.
After the build#
Produce and deploy a release with the new bitstream (see
Releases and Deployment), or, for a PL-only experiment, deploy-sd.sh. The
.xsa's internal bitstream is named satlink_zynq7020.bit.