22 KiB
EMBS runbook - Vivado -> Vitis HLS -> Vitis -> board
Personal quick-reference for the assessment. Compiled from the EMBS docs at https://iangray001.github.io/embs/. Running example: the Game of Life (Practical 3b), because that is the one you already have working.
Read this first: three corrections to the step order
You listed eleven steps. Eight are right. Two are in the wrong place, and one was ambiguous. Everything below follows the corrected order.
1. "Application which uses the HLS component" cannot come before "wiring the
HLS IP into the block diagram." The driver (xtoplevel.h), the base addresses
(xparameters.h) and the XToplevel_* functions are all generated from the
exported XSA. Until the IP is in the block design, a bitstream is built and the
hardware is re-exported, those files do not exist. Writing the app first means
writing against nothing.
2. "Simulating this" is not a separate step, and the docs only cover one kind. See the note below.
3. You build the bitstream twice, and they are different things. The first (step 2) validates the base system and is what creates the platform. The second (step 7) is the real one, containing your IP. Expect to throw the first away.
The corrected order
| # | Step | Where |
|---|---|---|
| 1 | Create the block design | P1a |
| 2 | Build the base bitstream | P1a |
| 3 | Export hardware, create the Platform, first app | P1a |
| 4 | Write the HLS component | P3a |
| 5 | Write the testbench, run C Simulation | P3a |
| 6 | Wire the IP into the block design | P4 |
| 7 | Build the bitstream again, re-export the XSA | P4 |
| 8 | Re-read the XSA, then write the app that drives the IP | P4 |
| 9 | Program the board and run | P1a / P4 |
| 10 | Test it out | P4 / Software API |
About "simulating this"
Three different things get called simulation, and the docs only document the first:
- HLS C Simulation - runs your testbench as ordinary C++ in software. This is step 5, and it is the only simulation the practicals use. It checks logic, not speed or hardware size.
- C/RTL co-simulation - re-runs the testbench against the generated RTL. More thorough, much slower. Not covered anywhere in the EMBS docs.
- Vivado behavioural simulation - simulates the whole block design. Not covered anywhere in the EMBS docs.
So: do step 5 properly, and treat the FPGA as the only other test rig you have.
Part 0 - Once per session
On the CS servers (or your own machine) Vivado and HLS need the licence server and the tools on the PATH:
echo 'export LM_LICENSE_FILE=2100@cslm0.its.york.ac.uk' >> ~/.bashrc
echo 'export PATH=/opt/york/cs/net/xilinx_vivado-2025.1-x86_64-1/2025.1/Vivado/bin:$PATH' >> ~/.bashrc
Reopen the terminal. Check vivado -version says 2025.1, not 2020.x.
Verify the licence is visible: Help -> Manage License -> Manage License Search
Paths should show LM_LICENSE_FILE = 2100@cslm0.its.york.ac.uk.
The three tools, in one line each:
- Vivado - builds the hardware design (the bitfile) and programs the FPGA.
- Vitis - builds software for the ARM cores, and loads both.
- Vitis HLS - turns C/C++ into a custom IP core you can drop into Vivado.
Never put spaces in a Vivado project path, a Vitis workspace, or a project name. It genuinely breaks the tools later.
Part 1 - Base hardware and platform (Practical 1a)
1.1 New project
- Vivado -> Create Project.
- Name and location (no spaces).
- RTL Project + "Do not specify sources at this time".
- Next past Constraints.
- Default Part page -> Boards -> Vendor
digilentinc.com, NameZybo Z7-10. First time only: Refresh to pull the board catalogue, then click the download arrow in the Status column to install the board files. - Select Zybo Z7-10 -> Next -> Finish.
1.2 Block design
- Flow Navigator -> Create Block Design (default name
design_1is fine). - Add IP ->
ZYNQ7 Processing System. - Switch the centre panel to Board -> double-click 4 LEDs -> OK.
Then 4 Buttons -> OK. (This drops in an
axi_gpiocore.) - Green banner -> Run Connection Automation -> OK.
This adds a Processor System Reset, an AXI Interconnect, and wires the PS
master
M_AXI_GP0through to the GPIO slaveS_AXI. - Green banner -> Run Block Automation -> OK. This attaches DDR and the fixed I/O, and sets clock speeds.
- Toolbar -> Regenerate Layout to tidy the diagram.
1.3 Wrapper and bitstream
- Sources panel -> right-click
design_1.bd-> Create HDL Wrapper -> "Let Vivado manage wrapper and auto-update" -> OK. - A critical warning about
PCW_UIPARAM_DDR_DQS_TO_CLK_DELAY_*is expected and ignorable. Any other critical warning is not. - Flow Navigator -> Generate Bitstream -> Yes to run synthesis and implementation. Minutes for this design; much longer later.
- Progress: Design Runs tab at the bottom.
Worth a look once, for the assessment: Open Implemented Design -> Device view, Report Utilization, Open Synthesized Design -> Schematic.
1.4 Program the FPGA (base design)
- Flow Navigator -> Open Hardware Manager -> Open Target -> Open New Target.
- Right-click
xc7z010-> Program Device. - Bitstream should already be
design_1_wrapper.bit-> Program.
The green DONE LED lights, and nothing else happens - the ARM cores have no software yet.
1.5 Platform, then first application
- Vivado -> File -> Export -> Export Hardware -> Next -> Include
bitstream -> Next -> Finish. This produces
design_1_wrapper.xsa. - Tools -> Launch Vitis IDE -> OK.
- Vitis -> File -> Set Workspace (e.g.
~/vitis_workspace, no spaces). - File -> New Component -> Platform. Name it e.g.
basicIO_platform. - Browse to the exported
design_1_wrapper.xsa. - Defaults are correct:
standaloneandps7_cortexa9_0-> Next -> Finish. - View -> Examples -> Embedded Software Examples -> Hello World -> Create
Application Component From Template. Pick your platform, then the
standalone_ps7_cortexa9_0domain.
Vitis project types, because they get confused:
- Platform - the exported hardware plus drivers. Comes from Vivado.
- System - generated; drivers/frameworks your apps sit on ("the OS").
- Application - your code.
You must make a new System whenever the hardware interface changes.
Part 2 - The HLS component and its testbench (Practical 3a/3b)
2.1 New HLS component
- Vitis -> File -> New Component -> HLS.
- Name it descriptively (not
hls_component). - Configuration File -> Next. Add Source Files -> Next.
- Part =
xc7z010clg400-1. Next -> Next -> Finish. - Add your sources (
gameoflife.cpp,gameoflife.h). - Settings -> hls_config.cfg -> under C Synthesis sources set
top =
game_of_life_step.
Your existing hls_config.cfg already has the right shape:
part=xc7z010clg400-1
[hls]
flow_target=vivado
package.output.format=ip_catalog
package.output.syn=false
tb.file=testbench.cpp
syn.file=gameoflife.cpp
syn.file=gameoflife.h
syn.top=game_of_life_step
2.2 The interface template
Two interfaces, and they do different jobs:
- AXI Slave (
s_axilite) - the ARM uses it to start/stop the core and to pass a few scalars. - AXI Master (
m_axi) - the core uses it to read/write main memory directly. Only needed if you are moving a lot of data.
The Game of Life packs its whole 10x10 grid into 4 x 32-bit words, so it needs only the slave interface:
#pragma HLS INTERFACE s_axilite port = in_words bundle = AXILiteS
#pragma HLS INTERFACE s_axilite port = out_words bundle = AXILiteS
#pragma HLS INTERFACE s_axilite port = return bundle = AXILiteS
If you do use an AXI Master, the template is:
#pragma HLS INTERFACE m_axi port=ram offset=slave bundle=MAXI
#pragma HLS INTERFACE s_axilite port=ram bundle=AXILiteS
#pragma HLS INTERFACE s_axilite port=return bundle=AXILiteS register
Trap. The knowledge base uses
registeron the slave ports; Practical 3a's template omits it. The docs are explicit that theport=returnpragma is what bundles start / stop / done / reset into a control register: "If you do not include this pragma, then HLS will generate simple wires for these signals instead and your IP core will not be controllable from the ARM cores directly." Include it, or you get an IP you cannot start.
Trap. For an
m_axiport the datatype must be 32-bit (uint32), because RAM is 32 bits wide. Returninguint100 toplevel(uint100 *ram)makes HLS generate 100 wires that you cannot wire up in Vivado.
2.3 Testbench and C Simulation
- Right-click Test Bench -> New Test Bench File ->
testbench.cpp. - Settings -> hls_config.cfg -> Testbench Sources -> Add File and add it. (Creating the file is not enough - this is the step people miss.)
- Flow panel -> C Simulation -> Run.
Good testbench habits, from the docs: return 0 on success, 1 on failure, check against pre-calculated values, and for a design like the Game of Life run several generations in sequence so you know state carries over correctly. Edge behaviour (wrap-around or not) is your choice - but decide, and test it.
2.4 Synthesis and tuning
Flow panel -> C Synthesis -> Run. Then read the report:
- Timing Estimate - at 100 MHz the target period is 10 ns. The 3a design estimates ~7.3 ns, so it meets timing.
- Performance and Resource Estimates - overall latency and interval, then per-loop. This is why loops are labelled in EMBS code.
- Utilisation - LUTs, FFs, DSPs, BRAMs. Click the % toggle to see it as a fraction of the device. The Z-7010 has 60 BRAMs, 36 kbit each.
The 3a worked example is the clearest demonstration of the trade-off:
| Change | Latency | Notes |
|---|---|---|
Baseline (with PIPELINE off deliberately) |
910 | |
PIPELINE on both loops |
521 | Interval 1; trip counts 100 and 99 |
UNROLL both loops |
~420 | ~7300 LUTs - 3x the size; HLS used 4 BRAMs instead of 1 |
+ INLINE on both functions |
366 | Saved hardware by scheduling across functions |
+ ARRAY_PARTITION type=complete on the array, UNROLL the read loop |
115 | BRAMs down, LUTs even slightly down |
Inspect with C Synthesis -> Reports -> Schedule Viewer and the Analysis perspective. Columns are clock states; things in the same column happen in parallel. A successfully unrolled design looks vertical; a stuck one looks horizontal.
BRAMs only have two access ports, so an array in BRAM caps your parallelism
at 2. ARRAY_PARTITION with type=complete moves it into registers. For a
multidimensional array, set dim=0 so all dimensions are partitioned.
Game of Life targets (P3b): latency under 300 cycles; ~200-250 is reachable; the author has one at 43 cycles.
Useful directives, from the knowledge base: PIPELINE, UNROLL, INLINE,
ARRAY_PARTITION, ARRAY_MAP, ALLOCATION, LATENCY, LOOP_FLATTEN,
LOOP_TRIPCOUNT, DATAFLOW, RESOURCE, BIND_STORAGE.
Part 3 - Put the IP into the hardware (Practical 4)
3.1 Export the IP
In Vitis HLS -> Flow Navigator -> Package:
- output.file = something sensible (e.g.
practical4) - output.format =
ip_catalog - Run
3.2 Add it to Vivado
- Vivado -> open your Block Design.
- Window -> IP Catalog.
- Flow Navigator -> Settings -> IP -> Repository -> +.
- Select your HLS project directory -> Select. Vivado scans and reports an IP was added -> OK -> OK.
- In the Block Design -> Add IP -> your core is listed under the Display Name you set (or "Toplevel" if you forgot).
3.3 Wire it up
Only if your IP has an m_axi port (it needs main memory):
- Double-click the Zynq block -> PS-PL Configuration -> expand HP Slave AXI Interface -> tick S AXI HP0 -> OK. A new port appears.
- Run Connection Automation -> tick S_AXI_HP0 -> it offers to connect it
to your IP's
m_axi.
Always:
- In the same Connection Automation dialog, tick
s_axi_AXILiteS-> it offers to connect toM_AXI_GP0on the processing system -> OK.
Result: your slave port goes through processing_system7_0_axi_periph to the PS,
and (if you have one) your master port goes through axi_mem_intercon into the
Zynq.
The Game of Life as written has no
m_axiport, so it needs no S AXI HP0 - just the GP0 path. Don't tick HP0 unless you actually have a master interface.
Tracing rule to remember: the Zynq's M_AXI must be traceable to every
S_AXI in your design, and any IP's M_AXI must be traceable to S_AXI_HP0 on
the Zynq. Vivado may use an AXI SmartConnect instead of an AXI Interconnect -
that is fine.
If the AXI structure comes out wrong: delete all the AXI Interconnect blocks and re-run Connection Automation.
3.4 Rebuild and re-export
- Save the block design.
- Generate Bitstream.
- File -> Export -> Export Hardware -> Include bitstream -> overwrite the existing XSA.
Part 4 - Application, programming, testing
4.1 Re-read the XSA, then write the app
Order matters here - this is the step that was out of place in your list.
- In Vitis, open your platform's
vitis-comp.json-> Switch / re-read XSA -> point at the new XSA. - Under drivers, your IP now appears (e.g.
toplevel). - File -> New Component -> Application, on the
design_1_wrapperplatform, letting it create a new System. Required whenever the hardware interface changes.
4.2 Driving the IP from the ARM
The core principle, from the Software API page: you never "pass data" to HLS. The data is in shared main memory; the ARM just tells the core where to look.
#include <stdio.h>
#include "xil_printf.h"
#include "xparameters.h"
#include "xtoplevel.h"
#include "xil_cache.h"
u32 shared[1000];
int main() {
XToplevel hls;
Xil_DCacheDisable();
for (int i = 0; i < 100; i++) shared[i] = i;
shared[0] = 8000;
XToplevel_Initialize(&hls, XPAR_TOPLEVEL_0_BASEADDR);
XToplevel_Set_ram(&hls, (u32) shared); // where the core should look
XToplevel_Start(&hls);
while (!XToplevel_IsDone(&hls));
printf("arg2 = %lu\narg3 = %lu\n",
XToplevel_Get_arg2(&hls), XToplevel_Get_arg3(&hls));
return 0;
}
Always set the RAM offset. XToplevel_Set_ram is what makes ram[0] inside
HLS mean "start of my shared array" rather than "address 0 of DDR".
Getter/setter names change with direction. If the core only reads an argument
you get XToplevel_Set_arg1 / XToplevel_Get_arg1; if it reads and writes
you get XToplevel_Set_arg1_V_i / XToplevel_Get_arg1_V_o. Check the generated
xtoplevel.h rather than guessing.
4.3 Caches - the silent wrong-answer bug
The ARM's caches are invisible to your IP, and HLS's writes do not invalidate them. You must handle it explicitly:
#include <xil_cache.h>
// after writing shared data, before starting the core
Xil_DCacheFlush();
// or: Xil_DCacheFlushRange((INTPTR) shared, sizeof(shared));
XToplevel_Start(&hls);
while (!XToplevel_IsDone(&hls));
// before reading results back
Xil_DCacheInvalidate();
// or: Xil_DCacheInvalidateRange((INTPTR) shared, sizeof(shared));
Xil_DCacheDisable() also works and is simpler - just slower.
4.4 Fix the bitstream path before you iterate
Do this once, before you start changing HLS code. Vitis does not reliably refresh its own copy of the bitfile, so it can silently program old hardware.
- Application -> Settings -> launch.json.
- Bitstream File is probably
${workspaceFolder}/<project>/_ide/bitstream/design_1_wrapper.bit. - Change it to Vivado's real output:
<vivado project>/<project>.runs/impl_1/design_1_wrapper.bit.
4.5 Program and run
From Vitis, hitting Run does both halves: programs the PL with the bitstream, and loads the ARM executable into DDR. That is why "upload to hardware" is really two uploads.
4.6 Test it out
- Serial output on a lab PC:
screen /dev/ttyUSB1 115200(try ttyUSB0/2 if that fails). Exit with Ctrl-A, release, then k, then y. Full settings: 115200 8N1, no flow control. - Do not use the SDK/Vitis built-in console - it is line-buffered and gets slow with lots of data.
- For single keypresses use
XUartPs_IsReceiveData/XUartPs_RecvByte/XUartPs_SendBytefromxuartps_hw.h, becausescanfblocks until a newline. - Timing measurements:
XTime_GetTime, withCOUNTS_PER_SECOND, fromxiltimer.h. Put ausleep(1000)first or the timer reads 0. - Debug your own IP: add a
versionslave argument set to a constant (e.g. 42). IfXToplevel_Get_version()returns 42, the slave interface and the whole build/export chain are good. Then add amodeargument and switch between normal operation and small AXI read/write test cases - that isolates a broken master interface from a broken algorithm.
The loop: when you change your HLS
This is the sequence you will actually live in. The docs list it in two places (Practical 4 and the knowledge base) and it is the same list:
- Re-run synthesis in HLS.
- Re-export the IP core (Package -> Run).
- In Vivado, it should say "IP Catalog is out-of-date". If not: IP Status -> Re-Run Report, then Refresh IP Catalog.
- In the IP Status panel, select your IP -> Upgrade Selected.
- Generate Output Products -> Generate.
- Generate Bitstream.
- Export Hardware (include bitstream).
- In Vitis, platform -> Settings -> vitis-comp.json -> Switch / re-read XSA.
- Reprogram and run.
- If you changed the interface, regenerate the System and move the application into it.
Step 8 is not optional even with the launch.json fix. Re-reading the XSA is what updates the hardware description, the drivers and xparameters.h. The launch.json change only fixes which bitfile gets programmed.
Symptom of getting this wrong: software hangs talking to your core, or the core returns stale/nonsense results, straight after a change that "should" have worked. Compare modification times:
<workspace>/<project>/_ide/bitstream/design_1_wrapper.bit <- Vitis' copy
<vivado project>/<project>.runs/impl_1/design_1_wrapper.bit <- what Vivado built
If the first is older, Vitis is running old hardware.
Quick reference
| I want to... | Where |
|---|---|
| Set the HLS top function | HLS: Settings -> hls_config.cfg -> C Synthesis sources -> top |
| Register a testbench | HLS: Settings -> hls_config.cfg -> Testbench Sources -> Add File |
| Run the testbench | HLS: Flow -> C Simulation -> Run |
| See latency / II | HLS: C Synthesis -> Reports -> Synthesis |
| See what runs in parallel | HLS: C Synthesis -> Reports -> Schedule Viewer, and the Analysis perspective |
| Add a directive | HLS: View -> HLS Directives (or the two-star button) |
| Package the IP | HLS: Flow Navigator -> Package -> ip_catalog -> Run |
| Register the IP repo | Vivado: Settings -> IP -> Repository -> + |
| Enable DDR access | Vivado: Zynq block -> PS-PL Configuration -> HP Slave AXI -> S AXI HP0 |
| Auto-wire AXI | Vivado: green banner -> Run Connection Automation |
| Export hardware | Vivado: File -> Export -> Export Hardware -> Include bitstream |
| Re-read hardware in Vitis | Platform -> Settings -> vitis-comp.json -> Switch / re-read XSA |
| Fix which bitfile is programmed | Application -> Settings -> launch.json -> Bitstream File |
| Import a driver example | Platform -> vitis-comp.json -> ... -> Board Support Package -> drivers -> Import Examples |
| Measure code time | XTime_GetTime from xiltimer.h |
Traps that cost hours
- The
port=returnpragma withregister. Without it your IP has no control register and cannot be started from the ARM. - Forgetting to add the testbench to
hls_config.cfg. Creating the file is not enough. - Forgetting
XToplevel_Set_ram. Your core writes over arbitrary memory. - Caches. Flush before the core starts, invalidate before reading results.
- Not re-reading the XSA. Stale drivers and stale
xparameters.h. - Vitis programming an old bitfile. Fix the launch.json path once.
- Spaces in paths or project names.
- A
uint32-vs-wide datatype on anm_axiport. 100 wires you cannot route. - Running out of disk. Check
quota -s; aim for ~500 MB free.~/.cacheis the usual culprit. If the tools misbehave after filling the disk, delete.Xilinxand.Xilfrom your home directory. - Not making a new System after changing the interface. The application will not see the new driver.
VLAB notes (you are off campus)
Everything above works unchanged, with three differences:
- Program the FPGA via Remote Server rather than a local target:
Hostname
localhost, Port12345(whatever you passed tovlab.py -l). In Vivado: Open Hardware Manager -> Open Target -> Open New Target -> Remote Server. In Vitis: Settings ->launch.json-> Target Connection -> new. - Serial - you do not need
screen /dev/ttyUSB*. The VLAB client is the serial terminal. - No buttons, no LEDs, no live HDMI. You cannot observe anything except over
the UART. Design for that: this is exactly why the Game of Life testbench and
the
version/modedebug arguments matter so much more on the VLAB.
Remember vlab.py must run on the machine running the Xilinx tools, and on
a shared server you need your own port (-l 20041) because 12345 collides.
Where each part came from
| Topic | Page |
|---|---|
| Project, block design, bitstream, platform, first app | docs/practicals/practical-1a |
| GPIO, UART calculator, extending the hardware | docs/practicals/practical-1b |
| HLS project, testbench, synthesis, directives | docs/practicals/practical-3a |
| Game of Life spec and targets | docs/practicals/practical-3b |
| Exporting IP, wiring AXI, iteration loop, Collatz | docs/practicals/practical-4 |
| Sharing memory, caches, memcpy, timers, UART, Ethernet | docs/resourcestips/software-api |
| Terms, directives, AXI interfaces, loop optimisations | docs/resourcestips/hls-knowledge-base |
| Debug interfaces and debug modes for your own IP | docs/resourcestips/debugging |
| Serial terminal settings | docs/resourcestips/uart |
| Disk quota and cleaning projects | docs/resourcestips/disk-usage |
| VLAB remote target and serial | docs/setup/vlab, docs/setup/vlab-quickstart |