# EMBS runbook - Vivado -> Vitis HLS -> Vitis -> board Personal quick-reference for the assessment. Compiled from the EMBS docs at . 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: ```bash 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 1. Vivado -> **Create Project**. 2. Name and location (**no spaces**). 3. **RTL Project** + "Do not specify sources at this time". 4. Next past Constraints. 5. Default Part page -> **Boards** -> Vendor `digilentinc.com`, Name `Zybo 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. 6. Select **Zybo Z7-10** -> Next -> Finish. ### 1.2 Block design 1. Flow Navigator -> **Create Block Design** (default name `design_1` is fine). 2. **Add IP** -> `ZYNQ7 Processing System`. 3. Switch the centre panel to **Board** -> double-click **4 LEDs** -> OK. Then **4 Buttons** -> OK. (This drops in an `axi_gpio` core.) 4. Green banner -> **Run Connection Automation** -> OK. This adds a *Processor System Reset*, an *AXI Interconnect*, and wires the PS master `M_AXI_GP0` through to the GPIO slave `S_AXI`. 5. Green banner -> **Run Block Automation** -> OK. This attaches DDR and the fixed I/O, and sets clock speeds. 6. Toolbar -> **Regenerate Layout** to tidy the diagram. ### 1.3 Wrapper and bitstream 1. **Sources** panel -> right-click `design_1.bd` -> **Create HDL Wrapper** -> "Let Vivado manage wrapper and auto-update" -> OK. 2. A critical warning about `PCW_UIPARAM_DDR_DQS_TO_CLK_DELAY_*` is **expected and ignorable**. Any *other* critical warning is not. 3. Flow Navigator -> **Generate Bitstream** -> Yes to run synthesis and implementation. Minutes for this design; much longer later. 4. 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) 1. Flow Navigator -> **Open Hardware Manager** -> **Open Target** -> **Open New Target**. 2. Right-click `xc7z010` -> **Program Device**. 3. 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 1. Vivado -> **File -> Export -> Export Hardware** -> Next -> **Include bitstream** -> Next -> Finish. This produces `design_1_wrapper.xsa`. 2. **Tools -> Launch Vitis IDE** -> OK. 3. Vitis -> **File -> Set Workspace** (e.g. `~/vitis_workspace`, no spaces). 4. **File -> New Component -> Platform**. Name it e.g. `basicIO_platform`. 5. Browse to the exported **`design_1_wrapper.xsa`**. 6. Defaults are correct: `standalone` and `ps7_cortexa9_0` -> Next -> Finish. 7. **View -> Examples -> Embedded Software Examples -> Hello World -> Create Application Component From Template**. Pick your platform, then the `standalone_ps7_cortexa9_0` domain. 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 1. Vitis -> **File -> New Component -> HLS**. 2. Name it descriptively (not `hls_component`). 3. Configuration File -> Next. Add Source Files -> Next. 4. **Part = `xc7z010clg400-1`**. Next -> Next -> Finish. 5. Add your sources (`gameoflife.cpp`, `gameoflife.h`). 6. **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**: ```c #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: ```c #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 `register` on the slave ports; Practical 3a's > template omits it. The docs are explicit that the `port=return` pragma 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_axi` port the datatype must be **32-bit** (`uint32`), because > RAM is 32 bits wide. Returning `uint100 toplevel(uint100 *ram)` makes HLS > generate 100 wires that you cannot wire up in Vivado. ### 2.3 Testbench and C Simulation 1. Right-click **Test Bench** -> **New Test Bench File** -> `testbench.cpp`. 2. **Settings -> hls_config.cfg -> Testbench Sources -> Add File** and add it. (Creating the file is *not* enough - this is the step people miss.) 3. 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 1. Vivado -> open your Block Design. 2. **Window -> IP Catalog**. 3. Flow Navigator -> **Settings** -> **IP** -> **Repository** -> **+**. 4. Select your **HLS project directory** -> Select. Vivado scans and reports an IP was added -> OK -> OK. 5. 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): 1. Double-click the **Zynq** block -> **PS-PL Configuration** -> expand **HP Slave AXI Interface** -> tick **S AXI HP0** -> OK. A new port appears. 2. **Run Connection Automation** -> tick **S_AXI_HP0** -> it offers to connect it to your IP's `m_axi`. **Always:** 3. In the same Connection Automation dialog, tick **`s_axi_AXILiteS`** -> it offers to connect to **`M_AXI_GP0`** on 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_axi` port**, 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 1. Save the block design. 2. **Generate Bitstream**. 3. **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.** 1. In Vitis, open your **platform**'s `vitis-comp.json` -> **Switch / re-read XSA** -> point at the new XSA. 2. Under **drivers**, your IP now appears (e.g. `toplevel`). 3. **File -> New Component -> Application**, on the `design_1_wrapper` platform, **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. ```c #include #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: ```c #include // 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**. 1. Application -> **Settings -> launch.json**. 2. **Bitstream File** is probably `${workspaceFolder}//_ide/bitstream/design_1_wrapper.bit`. 3. Change it to Vivado's real output: `/.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_SendByte` from `xuartps_hw.h`, because `scanf` blocks until a newline. - **Timing** measurements: `XTime_GetTime`, with `COUNTS_PER_SECOND`, from `xiltimer.h`. Put a `usleep(1000)` first or the timer reads 0. - **Debug your own IP**: add a `version` slave argument set to a constant (e.g. 42). If `XToplevel_Get_version()` returns 42, the slave interface and the whole build/export chain are good. Then add a `mode` argument 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: 1. Re-run **synthesis** in HLS. 2. **Re-export** the IP core (Package -> Run). 3. In Vivado, it should say **"IP Catalog is out-of-date"**. If not: **IP Status** -> **Re-Run Report**, then **Refresh IP Catalog**. 4. In the **IP Status** panel, select your IP -> **Upgrade Selected**. 5. **Generate Output Products** -> **Generate**. 6. **Generate Bitstream**. 7. **Export Hardware** (include bitstream). 8. In Vitis, platform -> **Settings -> vitis-comp.json -> Switch / re-read XSA**. 9. Reprogram and run. 10. 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: ``` //_ide/bitstream/design_1_wrapper.bit <- Vitis' copy /.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 1. **The `port=return` pragma with `register`.** Without it your IP has no control register and cannot be started from the ARM. 2. **Forgetting to add the testbench to `hls_config.cfg`.** Creating the file is not enough. 3. **Forgetting `XToplevel_Set_ram`.** Your core writes over arbitrary memory. 4. **Caches.** Flush before the core starts, invalidate before reading results. 5. **Not re-reading the XSA.** Stale drivers and stale `xparameters.h`. 6. **Vitis programming an old bitfile.** Fix the launch.json path once. 7. **Spaces in paths or project names.** 8. **A `uint32`-vs-wide datatype on an `m_axi` port.** 100 wires you cannot route. 9. **Running out of disk.** Check `quota -s`; aim for ~500 MB free. `~/.cache` is the usual culprit. If the tools misbehave after filling the disk, delete `.Xilinx` and `.Xil` from your home directory. 10. **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`, Port `12345` (whatever you passed to `vlab.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`/`mode` debug 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` |