Verilator is a cycle-accurate SystemVerilog simulator that compiles your design to C++, then runs it natively — far faster than event-driven simulators for large designs.
# Compile DUT + SV testbench into an executable
verilator --binary --trace -Wall \
--top-module adder_tb \
adder.sv adder_tb.sv \
-o sim_adder
# Run simulation
./obj_dir/sim_adder
# Open waveform
gtkwave sim.vcd
--binary: generate Verilator's own main(), compile and link an executable. It implies --timing, which is what makes #10 delays and always-block clocks work.--binary you get C++ sources only — and nothing to run.SystemVerilog (tb_foo.sv) |
C++ harness (tb_foo.cpp) |
|
|---|---|---|
| What it is | a module with no ports | an ordinary C++ main() |
| Time | #10 delays, always clock |
you toggle the clock and call eval() |
| Parameters | set at DUT instantiation | -GNAME=value on the command line |
| Verilator | --binary |
--cc --exe --build |
| Good at | self-contained checks | file I/O, reference models, thousands of vectors |
Reach for SystemVerilog first. One language, less ceremony, and enough for anything self-contained — a decoder, an FSM, a counter.
Verilator turns your module into a C++ class: every port becomes a public member.
#include "Vdecoder2to4.h"
#include "verilated.h"
int main(int argc, char **argv) {
Verilated::commandArgs(argc, argv);
Vdecoder2to4 *dut = new Vdecoder2to4;
for (int s = 0; s < 4; s++) {
dut->sel = s; // drive an input
dut->eval(); // recompute outputs — nothing happens without this
printf("%s sel=%d y=%d\n",
dut->y == (1 << s) ? "[PASS]" : "[FAIL]", s, (int)dut->y);
}
delete dut;
return 0;
}
eval() is the whole model: change inputs, call it, read outputs.clk low, eval(), set it high, eval(). That loop is time.The KWS kernel and the accelerator are where the C++ style pays off:
None of that is comfortable in a SystemVerilog initial block.
Rule of thumb: if the testbench only needs to poke signals and check them, write SV. If it needs to compute what the right answer is, write C++.
ifndef SYNTHESIS and ifdef SIMULATIONTwo standard patterns for code that must NOT be synthesized:
Pattern 1: `ifndef SYNTHESIS — RTL assertions embedded in the module
// Inside a regular synthesizable module:
`ifndef SYNTHESIS
always_ff @(posedge clk)
assert (wptr < DEPTH) else $fatal(1, "FIFO write pointer overflow");
`endif
Yosys defines SYNTHESIS automatically during synthesis. Verilator does not define it by default.
Pattern 2: `ifdef SIMULATION — debug ports that only exist in testbench builds
module riscv_core (
input logic clk, rst_n,
// ... normal ports ...
`ifdef SIMULATION
output logic [31:0] dbg_pc, // expose PC for testbench inspection
output logic [31:0] dbg_reg_data
`endif
);
Pass -DSIMULATION to Verilator (--compiler gcc -DSIMULATION) to enable.
Verilator is stricter than Yosys. Common warnings and how to handle them:
| Warning | Meaning | Suppress when... |
|---|---|---|
UNUSEDSIGNAL |
Signal declared but never read | Debug signals only needed in sim |
WIDTHEXPAND |
Expression auto-widened | Intentional zero-extension |
WIDTHTRUNC |
Expression auto-truncated | Taking only the low bits deliberately |
SYNCASYNCNET |
Net crosses sync/async boundary | After a known synchronizer |
Inline suppression (document the reason):
/* verilator lint_off UNUSEDSIGNAL */
logic [31:0] dbg_pc; // exposed via SIMULATION port only
/* verilator lint_on UNUSEDSIGNAL */
Rule: suppress only when the warning is a known false positive, and always add a comment explaining why.
Verilator compiles synthesizable SV to a C++ class. A C++ testbench can drive any signal cycle by cycle:
#include "Vuart_tx.h" // generated by Verilator
#include "verilated.h"
int main() {
auto dut = std::make_unique<Vuart_tx>();
// Send byte 0xA5 at 115200 baud (27 MHz → 234 clocks/bit)
dut->txd_in = 0xA5;
dut->send = 1;
tick(dut); // rising edge: latch
dut->send = 0;
// Sample the TX line bit by bit
for (int bit = 0; bit < 10; bit++) { // 1 start + 8 data + 1 stop
for (int i = 0; i < 234; i++) tick(dut);
std::cout << "bit " << bit << " = " << (int)dut->tx << "\n";
}
}
This bit-banging approach lets you test a UART peripheral without a real serial port — just count clock cycles and check the output signal.
After running simulation with $dumpvars:
gtkwave sim.vcd.What to look for:
rst deassert cleanly after two cycles?count start at 0 and increment by 1 every cycle?x (red/unknown)? That means an undriven signal.GTKWave is how you debug Project 1 when a simulation assertion fires — zoom in to the failing cycle and trace back through state, PC, IR, and control signals.
Each step produces human-readable output — read it:
WNS: ... — negative means a timing violation.Real designs span multiple .sv files. A minimal Makefile:
TOP = top
SRC = $(wildcard src/*.sv)
DEVICE = GW1NR-9C
all: bitstream.fs
netlist.json: $(SRC)
yosys -p "read_verilog -sv $(SRC); synth_gowin -top $(TOP) -json $@"
pnr.json: netlist.json pins.cst
nextpnr-himbaechel --device $(DEVICE) --json $< --cst pins.cst --write $@ --freq 27
bitstream.fs: pnr.json
gowin_pack $< -o $@
load: bitstream.fs
openFPGALoader -b tangnano9k $<
sim: $(SRC) tb/$(TOP)_tb.sv
verilator --cc --exe --build -j4 --top-module $(TOP)_tb $(SRC) tb/$(TOP)_tb.sv -o sim_$(TOP)
./obj_dir/sim_$(TOP)
make sim runs simulation. make load synthesizes, P&Rs, packs, and loads in one step.
The Tang Nano 9K uses the Gowin GW1NR-9C — a small FPGA with these on-chip resources:
| Resource | Count | Description |
|---|---|---|
| LUT4 | 8,640 | 4-input lookup tables (combinational logic) |
| Flip-flop (DFF) | 6,480 | D flip-flops (registered state) |
| Shadow registers | 17,280 | Additional registers (config/scan use) |
| BSRAM | 26 blocks | 18 Kbit each → 468 Kbit total on-chip SRAM |
| DSP (MULT18X18) | 20 blocks | 18×18 signed multipliers, used for Zmmul/accelerator |
| PSRAM | 64 Mbit | External pseudo-SRAM (off-chip, via dedicated pins) |
| PLL (rPLL) | 2 | Phase-locked loops for clock synthesis |
| ADC | 1 | 8-channel 12-bit (not used in this course) |
Yosys maps your RTL to these primitives automatically. The synthesis report (stat command) shows how many of each are used — always check before submitting a project stage.
Every signal in your top-level module must be mapped to a physical pin of the FPGA package. This is done via a .cst (constraint) file:
// hw/constraints/tangnano9k.cst
IO_LOC "clk" 52; // logical name → pin number
IO_LOC "rst_n" 4;
IO_LOC "led[0]" 10;
IO_LOC "uart_tx" 17;
IO_PORT "clk" PULL_MODE=UP DRIVE=4;
Key fields:
IO_LOC: maps a port name to the package pin number (from the board schematic)IO_TYPE: electrical standard — LVCMOS33 (3.3V logic, default), LVCMOS18, LVDS, etc.PULL_MODE: UP / DOWN / NONE / KEEPER — internal resistor to avoid floating inputsDRIVE: output current strength in mA (4, 8, 16, 24) — higher drive for long traces or LEDsExample: Tang Nano 9K's button is connected to a pin that floats when not pressed → must use PULL_MODE=UP (active-low button reads 1 when released, 0 when pressed).
The GW1NR-9C includes 2 rPLL (reconfigurable PLL) blocks. A PLL multiplies and divides a reference clock to generate a precise output frequency:
at 27 MHz, the multicycle RV32I core has a timing slack of several nanoseconds — we are well below the Fmax. Using a PLL to run at, say, 50 MHz would require re-analyzing all timing paths and potentially adding pipeline stages.
The instantiation template is in the Gowin IP catalog; the Makefile would add --add-file pll.v to the nextpnr command.
rPLL primitiveYosys can't infer a PLL from behavioral RTL — it's dedicated hard-IP silicon, not LUTs/FFs. You instantiate the primitive directly; synth_gowin recognizes rPLL from its Gowin cell library and hands it untouched to nextpnr-himbaechel for placement.
// pll.v — plain Verilog (not SV), hand-written or generator-produced
module Gowin_rPLL (clkout, clkin);
output clkout;
input clkin;
rPLL rpll_inst (
.CLKOUT(clkout), .CLKOUTP(), .CLKOUTD(), .CLKOUTD3(), .LOCK(),
.CLKIN(clkin), .CLKFB(1'b0), .RESET(1'b0), .RESET_P(1'b0),
.FBDSEL(6'b0), .IDSEL(6'b0), .ODSEL(6'b0),
.PSDA(4'b0), .DUTYDA(4'b0), .FDLY(4'b0)
);
defparam rpll_inst.FCLKIN = "27";
defparam rpll_inst.IDIV_SEL = 2; // input divider: ÷(IDIV_SEL+1) = ÷3
defparam rpll_inst.FBDIV_SEL = 3; // feedback divider: ×(FBDIV_SEL+1) = ×4
defparam rpll_inst.ODIV_SEL = 8; // from {2,4,8,16,...} — keeps the VCO in range
endmodule
CLKOUT = CLKIN × (FBDIV_SEL+1) / (IDIV_SEL+1) → 27 × 4 / 3 = 36 MHz, exact.
ODIV_SEL only takes values from a restricted set, chosen so the internal VCO lands in its valid frequency band — get it wrong and the PLL never locks. Don't hand-pick these values. Apicula ships a generator that resolves them for you:
python3 -m apycula.gowin_pll -d GW1NR-9C -if 27 -of 36 # flags vary by version — check --help
# → writes Gowin_rPLL.v with IDIV_SEL / FBDIV_SEL / ODIV_SEL already resolved
wire clk_36, pll_lock;
Gowin_rPLL pll_inst (.clkin(clk_27), .clkout(clk_36));
assign rst_n = pll_lock; // hold the design in reset until the PLL locks
$(SRC) in the Makefile — extend the wildcard to src/*.sv src/*.v, since pll.v is plain Verilog.LOCK. A flip-flop sampling CLKOUT before it stabilizes starts in an undefined state — the same failure mode as the glitch attacks from the security corner.When you openFPGALoader -b tangnano9k bitstream.fs:
For development: SRAM (default) — fast iteration, and no wear.
Lab 4 (traffic light FSM, M01A04) is due this class. The testbench for Lab 4 should:
{red, yellow, green} for each state.Lab 2 (toolchain setup + Tang Nano 9K bring-up) is also due this class. Your submission should include the make load output showing successful bitstream loading and a photo/video of the LED blink exercise from M01A03.
A hardware trojan is malicious logic inserted into a design — by a rogue designer, a compromised IP core, or even a tampered synthesis tool. The trojan remains dormant until a rare trigger condition activates it.
| Technique | Example |
|---|---|
| Rare trigger | Activates only after 2³² clock cycles |
| Cheat code | Specific input sequence on an unused port |
| Analog | Modifies transistor sizing — invisible in RTL |
A glitch attack injects a voltage or clock perturbation to make a flip-flop sample the wrong value — causing the FSM to skip a state.
DECODE state and jumps to WRITEBACK could write garbage to the register file.default case in the FSM's always_comb that asserts an error signal or forces a reset.In Project 1, your FSM's
defaultcase is not just good practice — it is your first line of defense against fault injection.
Two sources of power dissipation in CMOS:
Dynamic power — every time a gate output toggles (0→1 or 1→0), it charges or discharges a capacitor:
Static power — leakage current flows even when nothing toggles. In modern processes (≤28 nm) this can be 30-50% of total power. In the Tang Nano 9K's 55 nm process, it is smaller but not negligible.
| Design choice | Energy effect |
|---|---|
| Unused outputs left floating | Oscillate → waste switching energy |
| Clock running to idle modules | Gates toggle on every edge even if output is unused |
| One-hot vs binary state encoding | One-hot: more flip-flops, but fewer toggle per transition |
default assignments in always_comb |
Prevent glitches from incomplete sensitivity — fewer spurious toggles |
After synthesis, Yosys reports the number of LUTs, flip-flops, and estimated net count. More nets ≈ more capacitance ≈ more dynamic power. Watch this number grow across Projects 1→2→3.
In Lab 3 and Lab 4, you can feel the energy cost directly: the Tang Nano 9K draws more current (warmer USB connector) when your design has high switching activity.
Your synthesis results depend on the exact version of every tool. A Yosys update can change LUT count by 10-15%.
oss-cad-suite-2026-08-01.yosys --version, nextpnr-himbaechel --version, verilator --version..gitignore for build artifacts (*.fs, *.json, *.asc) — only source HDL and constraints go into version control.Two students with the same source and the same toolchain version must get the same bitstream. If they don't, something is wrong.
Module 2 — RISC-V Architecture: the ISA your datapath must implement, the multicycle execution model, and the control FSM that ties everything together. Project 1 begins.