Skip to content

Lab 3 — Combinational Logic: Decoder on the Tang Nano 9K

Goal

Implement a 2-to-4 decoder in SystemVerilog, verify it exhaustively with a Verilator testbench, and deploy it to the Tang Nano 9K so that two buttons control which of four LEDs lights up. By the end you will have a concrete feel for the write–simulate–synthesize–deploy loop on real combinational hardware.

What you need

  • Lab 2 completed (OSS CAD Suite installed, board detected, blink working)
  • The Tang Nano 9K connected via USB

What is a decoder?

A decoder takes N binary inputs and activates exactly one of 2^N outputs. The 2-to-4 case maps a 2-bit select signal to a 4-bit one-hot output:

sel[1:0] y[3:0]
2'b00 4'b0001
2'b01 4'b0010
2'b10 4'b0100
2'b11 4'b1000

Decoders are everywhere in digital design: address decoders in memory maps, instruction decoders in CPUs, and one-hot FSM output encoders. You will use exactly this pattern in Project 2 when your bus needs to route an address to the right peripheral.

Step 1 — Write the module

hw/rtl/bringup/decoder2to4.sv already has the module interface in your repository — sel[1:0] in, y[3:0] out — with a TODO where the body goes.

Activity 1

Implement the body: y should have exactly one bit set, at position sel.

Before simulating, answer: what are the values of y when sel = 2'b10?

Hint — variable part-select

A clean way to write this: an always_comb block that clears y to all zeros, then does y[sel] = 1'b1. That last line is a variable part-select — indexing a vector with another signal rather than a constant. Yosys handles this correctly for a vector this small. The default assignment before it is what lets the tool (and you) confirm there are no unintended latches.

Step 2 — Simulate with Verilator

Before touching the board, prove the design is correct in simulation. Simulation is free — it catches bugs in seconds rather than after a slow synthesis run.

hw/sim/bringup/tb_decoder2to4.sv is partly written for you: sel=0 and sel=1 are worked examples, checking that y has exactly one bit set, at the right position.

Activity 2

Add the other two cases (sel=2, sel=3), following the exact shape of the two that are already there.

The file will not let you skip this: it counts how many cases actually ran, and calls $fatal if it is fewer than 4 — a testbench that quietly tested less than the whole space would be worse than no testbench at all.

Run it:

cd hw
make sim-decoder2to4

Expected output:

1
2
3
4
5
6
[PASS] sel=0 y=0001
[PASS] sel=1 y=0010
[PASS] sel=2 y=0100
[PASS] sel=3 y=1000

~~~ALL TESTS PASSED~~~

The decoder is purely combinational, so there is no clock to generate: set sel, wait a delay (#1) for the output to settle, and check y.

Where to start

You already have a worked example: sim/bringup/tb_blink.sv from Lab 2. Copy its shape — the module tb_...; with no ports, the DUT instantiation, the check task, the $dumpfile/$dumpvars pair, and the $finish-on-success / $fatal-on-failure ending. Yours is simpler: no clock, no reset, four cases.

Why $fatal and not just $display? $finish always exits 0, so a testbench that only printed FAIL would still let make report success. Ending in $fatal makes a broken design fail the build.

The C++ alternative

The same test can be written as a C++ harness (tb_decoder2to4.cpp, run with make simcc-decoder2to4) — Lab 2's tb_blink.cpp is the worked example. It is not the right tool here: a four-case combinational check needs nothing that SystemVerilog cannot do. You will want the C++ style later, when a testbench has to read real data files or compare against a reference model written in C.

Step 3 — Add a top-level and deploy to the board

The decoder module is generic. To deploy it you need a top-level that connects it to the physical pins of the Tang Nano 9K — hw/rtl/bringup/decoder2to4_top.sv in your repository, again with the interface given and a TODO for the body:

1
2
3
4
5
module decoder2to4_top (
    input  logic       sys_rst_n,  // S1 button, used here as sel[0]
    input  logic       btn2_n,     // S2 button, used here as sel[1]
    output logic [5:0] led
);

Activity 3

Wire the decoder in: build sel[1:0] from the two buttons, instantiate decoder2to4, and drive led[3:0] from its output. Note: both the buttons and the LEDs are active-low on the Tang Nano 9K — a button reads 0 when pressed, and a LED needs a 0 to turn on — so you are inverting twice, once on the way in and once on the way out.

Why sys_rst_n and btn2_n, not a 2-bit btn[1:0]?

Every design in this course shares one .cst per board (see hw/README.md's "board's vocabulary" section) — sys_rst_n and btn2_n are simply the two spare buttons' names in that shared vocabulary, S1 and S2. decoder2to4 has no clock or reset, so nothing stops this design from reading those same two pins as plain inputs instead. This is also why the top-level has its own file, separate from decoder2to4.sv: the generic decoder stays reusable, and only the top adapts to this board's specific pins.

Synthesize, place-and-route, and program the board:

1
2
3
cd hw
make decoder2to4_top
make decoder2to4_top-program

Underneath, that runs the same three tools as Lab 2, reading both source files together:

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

Verify on the board: pressing each button combination should light exactly one LED. Try all four combinations and confirm the one-hot behavior.

make decoder2to4 (without _top) will not synthesize for hardware

Running the plain module through the full flow fails place-and-route with ERROR: Unconstrained IO: sel/y are not in the board's .cst, on purpose — a generic module should not know about physical pins. make decoder2to4-synth alone (just the synthesis step) works fine, and simulation (make sim-decoder2to4) never touches the .cst at all. Only decoder2to4_top — the file that gives sel/y a board-specific meaning — goes all the way to a bitstream.

Step 4 — Extend it (challenge)

Activity 4

Change the design to a 3-to-8 decoder: sel[2:0] selecting among y[7:0]. If you only have 2 physical buttons, drive the third select bit from a slow clock divider so it toggles automatically, letting you observe all 8 outputs over time.

Update the testbench to cover all 8 cases (loop from 0 to 7).

On the board, you only have 6 LEDs — tie the top 2 outputs to unused signals or leave them unconnected. Observe that exactly one of the 6 visible LEDs lights at a time for the lower 6 states.

Hint

The module change is minimal: widen the ports to logic [2:0] sel and logic [7:0] y — the body y[sel] = 1'b1 stays identical. The testbench loop upper bound changes from 3 to 7. For the board, you only observe led[5:0] driven by ~y[5:0]; connect y[7:6] to open signals or leave them unwired in the top-level.

Step 5 — Think about it

Activity 5

A decoder is often the first stage of address decoding for memory-mapped peripherals. In Project 2, your bus will use a decoder to route a memory address to the right peripheral register.

Sketch (on paper or in a text file) a 2-to-4 decoder used as an address decoder: inputs are addr[1:0], outputs are chip-select signals cs_uart, cs_timer, cs_gpio, cs_cmac. Which address maps to which peripheral?

There is no single correct answer — this is your first sketch of the Project 2 memory map. Think about which peripheral is accessed most often and whether address ordering matters for the software layer.

Checklist

  • decoder2to4.sv implemented; make sim-decoder2to4 reports ~~~ALL TESTS PASSED~~~ (all 4 cases).
  • decoder2to4_top.sv implemented; make decoder2to4_top builds and make decoder2to4_top-program deploys — button presses light exactly one LED.
  • 3-to-8 decoder variant working in simulation.
  • Address-decoder sketch done.
  • EQUIPE.md updated; work committed and pushed.

Summary

You implemented a decoder in SystemVerilog, verified it with an exhaustive testbench, and deployed it to real hardware. The y[sel] = 1'b1 idiom (variable part-select) is a compact way to encode one-hot outputs — you will see the same pattern in the FSM output logic in Lab 4, and in the bus address decoder in Project 2.