Skip to content

Lab 2 - Toolchain & FPGA Bring-up

Goal

By the end of this lab, you should be able to: write a tiny SystemVerilog design, simulate it, synthesize it for the Tang Nano 9K, and see it run on real hardware (an LED blinking is enough).

0. Get your repository

Your pair's repository already contains everything this lab needs: setup.sh, the blink.sv interface, the board pin constraints, a Makefile driving the whole flow, and a worked Verilator testbench. Clone it and follow its README.md — this page explains why each step exists, the repository's README is the literal walkthrough.

1. Install the OSS CAD Suite

Running ./setup.sh at the root of your repository downloads everything below into tools/ for you. If you would rather install it by hand, download a release of the OSS CAD Suite for your platform. It bundles everything we need:

  • Yosys - synthesis.
  • nextpnr-himbaechel (Gowin backend) - place and route for the GW1NR-9 (Tang Nano 9K's FPGA).
  • Verilator - fast SystemVerilog simulation for testbenches.
  • GTKWave - waveform viewer.
  • openFPGALoader - bitstream upload over USB.

Add the suite's bin directory to your PATH:

export PATH="/path/to/oss-cad-suite/bin:$PATH"

The suite also ships an environment.sh/activate script that does this for you - but that script also reorders several other things (its own bundled Python among them), which can quietly break unrelated Python-based tools you install later in the course (e.g. the optional LiteX tutorials need litex, a Python package, importable from python3 - sourcing the full script can shadow that with the suite's own bundled Python instead). Prefer the plain PATH export above - it's all any command in this course actually needs from the suite.

2. Connect the board

Connect the Tang Nano 9K via USB. Verify openFPGALoader can detect it:

openFPGALoader --detect

It should list one device with the GW1NR-9C part. Seeing nothing is the most common way to get stuck in this lab, and the cause depends on your system.

Linux, and inside WSL2 — udev rules

Non-root users cannot open the USB device until the udev rules are installed:

1
2
3
4
sudo curl -fsSL -o /etc/udev/rules.d/99-openfpgaloader.rules \
    https://raw.githubusercontent.com/trabucayre/openFPGALoader/master/99-openfpgaloader.rules
sudo udevadm control --reload-rules
sudo udevadm trigger

Unplug and replug the board afterwards. If it still fails, add yourself to plugdev (sudo usermod -aG plugdev $USER) and log out and back in.

Windows + WSL2 — the board is not there at all

WSL2 has no access to USB devices by default. Installing the toolchain inside WSL is not enough: you must forward the device in from Windows with usbipd-win.

winget install --interactive --exact dorssel.usbipd-win

Then, in an administrator PowerShell:

usbipd list                        # find the board's BUSID
usbipd bind --busid <BUSID>        # once per device

Attaching is needed every time you replug the board, and does not need administrator:

usbipd attach --wsl --busid <BUSID>

Keep a WSL terminal open while doing this, or the WSL VM shuts down and the device detaches. Check with lsusb inside WSL — and you still need the udev rules above.

If this is more friction than you want to live with for a whole semester, a native Linux install or dual boot will save you time.

hw/rtl/bringup/blink.sv in your repository gives you the module interface — parameters, ports, and a placeholder body. The implementation is yours: a counter fed by sys_clk that flips the LEDs at BLINK_HZ. Keep the port names as they are; hw/constraints/tangnano9k.cst maps them onto physical pins.

The placeholder already builds, so run the flow once before writing any logic — it proves your toolchain works, and the LEDs staying off is the expected result:

1
2
3
cd hw
make blink            # synth + place & route + pack
make blink-program    # upload to the board

Behind those two targets are three tools, each consuming the previous one's output:

1
2
3
4
5
yosys -p "read_verilog -sv blink.sv; synth_gowin -top blink -json blink.json"
nextpnr-himbaechel --json blink.json --write blink_routed.json \
    --device GW1NR-LV9QN88PC6/I5 --vopt family=GW1N-9C --vopt cst=tangnano9k.cst
gowin_pack -d GW1N-9C -o blink.fs blink_routed.json
openFPGALoader -b tangnano9k blink.fs

Read the Makefile — you should be able to point at the line that runs each of these. make blink-synth, make blink-pnr and make blink-pack stop after each stage, which is how you find out which one is complaining.

Never write the board's flash

make blink-program loads the bitstream into the FPGA's SRAM, so it is gone after a power cycle. That is the only mode we use: the on-board flash takes a limited number of write cycles and these boards are reused across semesters.

4. Simulate with Verilator

This course uses two testbench styles, and blink is given in both so you can compare them on the same design before writing your own:

1
2
3
cd hw
make sim-blink                              # sim/bringup/tb_blink.sv  - SystemVerilog
make simcc-blink VFLAGS=-GCLK_FREQ_HZ=1000  # sim/bringup/tb_blink.cpp - C++ harness

Both run the same four checks and fail the build if your blink is wrong, so make itself tells you whether the design is right.

A SystemVerilog testbench is a module with no ports: it instantiates the design under test, drives it from an initial block, generates the clock with an always block, and overrides the DUT's parameters where it instantiates it. Reach for this first — it keeps everything in one language, and it is all a self-contained design needs. This is the style you will use in Lab 3.

A C++ harness compiles the design into a C++ class you drive from main(): every port becomes a member, eval() recomputes the outputs, and toggling the clock yourself is what advances time. More ceremony, but the testbench is an ordinary C++ program — it can read real data files, call a reference implementation written in C and compare against it, or push thousands of vectors. That is what the KWS kernel and accelerator labs need, which is why you meet it now.

Both write a waveform; open whichever you just ran:

gtkwave build/tangnano9k/obj_dir_blink/blink.vcd      # from sim-blink
gtkwave build/tangnano9k/obj_dir_blink_cc/blink.vcd   # from simcc-blink

Note the parameter override. Blinking at 1 Hz from a 27 MHz clock is 27 million cycles per blink — far too slow to simulate. The SystemVerilog testbench sets CLK_FREQ_HZ where it instantiates the DUT; the C++ one cannot reach into a parameter list, so it takes VFLAGS=-GCLK_FREQ_HZ=1000 on the command line instead. Either way, this is why blink.sv takes the frequency as a parameter rather than hardcoding it.

Reading both files is part of this lab. They are commented as tutorials, and every testbench in this course is one of those two shapes.

Checklist

  • OSS CAD Suite installed and on PATH.
  • Board detected by openFPGALoader --detect (udev rules on Linux; usbipd on WSL2).
  • make blink produces a bitstream and make blink-program loads it.
  • Your blink.sv implementation actually blinks the LEDs on the board.
  • make sim-blink and make simcc-blink both report ~~~ALL TESTS PASSED~~~.
  • You have read both tb_blink.sv and tb_blink.cpp and can say what each line does.
  • You opened the resulting waveform in GTKWave.
  • EQUIPE.md filled in; work committed and pushed.