Verification, Toolchain & Project 1 Kickoff

Rodolfo Azevedo

Institute of Computing, University of Campinas (UNICAMP), Brazil

rodolfo.azevedo@unicamp.br

http://www.ic.unicamp.br/~rodolfo/mo801

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Goal of this class

Module 2, Class 4: testing the full core, compiling real programs for it, and getting started.

Today closes the loop: M02A01-A03 gave you the ISA, the datapath, and the FSM. Now we test it end to end, run compiled C programs on it (not just hand-written instructions), and turn this into Project 1's milestones.

At the end of this class, you should be able to:

  • Test the complete RV32I core with an end-to-end simulation testbench that loads a hex file and checks register state.
  • Compile and link a C program for RV32I using riscv-none-elf-gcc with the appropriate flags and linker script.
  • Convert a compiled binary to the $readmemh hex format for BRAM initialization.
  • Plan your Project 1 implementation using the provided milestones and the testbench infrastructure.
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Binary → hex conversion for $readmemh

$readmemh expects a hex file where each line is one memory word (for a 32-bit memory, 4 bytes per line). objcopy -O verilog generates a Verilog-style hex file, but it uses byte addresses as keys — not word indices — which confuses Yosys BRAM initialization.

The correct pipeline:

# 1. Compile to ELF
riscv-none-elf-gcc -march=rv32i -mabi=ilp32 -T link.ld main.c crt0.S -o main.elf

# 2. Extract binary (raw bytes, no ELF headers)
riscv-none-elf-objcopy -O binary main.elf main.bin

# 3. Convert to word-indexed hex (custom script)
python3 bin2hex.py main.bin main.hex   # 4 bytes per line, little-endian

# 4. Load in simulation AND synthesis
initial begin
  if (INIT_FILE != "") $readmemh(INIT_FILE, mem);
end

The bin2hex.py script reads 4 bytes at a time (little-endian) and writes one hex word per line. Why not xxd or od? They produce byte-addressed output or wrong byte order.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Loading programs in Project 1: synthesis-time initialization

Project 1 has no UART yet — there is no way to send bytes to the board at runtime. Programs are loaded by initializing the instruction BRAM at synthesis time:

// In the testbench or top-level, before simulation/synthesis:
initial $readmemh("program.hex", imem.mem);

For hardware: Yosys reads the $readmemh call during synthesis and generates a BRAM initialized with the program contents. Changing the program means re-running make load (~2 minutes).

This is acceptable for Project 1 because test programs are short and you are re-synthesizing anyway to fix control FSM bugs. The simulation loop (make sim) is always instant.

Project 2 fixes this: a UART bootloader lets you send programs over serial in seconds with no re-synthesis. To support that, the instruction memory must be a writable SRAM from the start — design it that way now so Project 2 can reuse the same memory module without changes.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

From single instructions to programs

  • M01A03's directed tests checked one instruction at a time (load_instruction, step_one_instruction, check_result). A program is a sequence of instructions in instruction memory, executed by repeatedly stepping the FSM through FETCH until some termination condition.
  • Two ways to get a program into your testbench:
    1. Hand-assembled: a handful of 32'h... words, as in M01A03 - good for targeted FSM/datapath bugs.
    2. Compiled from C: a real toolchain (riscv-gnu-toolchain or an online assembler) produces a binary; you load it into instruction_mem and run it - good for "does this actually work as a computer."
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

ebreak as a termination convention

// main.c
int main(void) {
    int a = 5, b = 7;
    int sum = a + b;
    __asm__ volatile ("ebreak");
    return sum;
}
  • ebreak (a single, fixed-encoding instruction, 0x00100073) is normally a debugger breakpoint trap. For Project 1, repurpose it as "the program is done": when the FSM fetches and decodes ebreak, stop the simulation (or halt the FSM) instead of trapping to an exception handler.
  • In your testbench:
always_ff @(posedge clk)
    if (state == DECODE && instr == 32'h00100073) begin
        $display("Program finished. x10 (a0) = %0d", dut.regfile[10]);
        $finish;
    end
  • This single convention turns "run this C program" into "run until ebreak, then inspect registers" - no need to implement RV32I's full exception/trap mechanism for this course.
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Compiling C to RV32I

  • riscv-gnu-toolchain: a full GCC cross-compiler targeting RV32I. Typical invocation:
riscv64-unknown-elf-gcc -march=rv32i -mabi=ilp32 -nostdlib -nostartfiles \
    -Wl,-Ttext=0x0 -o main.elf main.c
riscv64-unknown-elf-objcopy -O binary main.elf main.bin
  • -nostdlib -nostartfiles: no C standard library, no _start/crt0 - your core has no OS, no syscalls, nothing to link against. Programs must be entirely self-contained (no printf, no malloc).
  • -march=rv32i: restrict codegen to the base ISA - no M extension (no hardware mul/div; GCC will emit library calls for *// unless told otherwise, which won't link). Stick to +, -, comparisons, bitwise ops, and array/pointer access in your test programs for Project 1.
  • An online assembler (e.g., the one linked from Project 1) is a lighter-weight alternative for small hand-written .s test programs - no toolchain installation needed.
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Loading a program into the testbench

module cpu_tb;
    logic clk = 0, rst;
    cpu dut (.clk(clk), .rst(rst));

    initial forever #5 clk = ~clk;

    initial begin
        $readmemh("main.hex", dut.imem.mem);   // load instruction memory

        rst = 1; @(posedge clk); @(posedge clk); rst = 0;

        wait (program_done);    // set by the ebreak check above
        $display("x10 = %0d (expected 12)", dut.regfile[10]);
        $finish;
    end
endmodule
  • $readmemh reads a file of hex words (one per line) into a memory array - convert your .bin to this format with objcopy/xxd/a small script.
  • dut.imem.mem - direct hierarchical access into the design-under-test's internal signals. Verilator allows this for testbenches even though it wouldn't be "real hardware" - extremely useful for loading memories and inspecting internal state (like regfile) without dedicated ports.
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Testing strategy: exhaustive vs. sampled

When verifying a hardware module against a reference, you must choose how many outputs to check:

Strategy Checks Risk When to use
Sampled First N outputs, or random subset May miss bugs in specific output positions Fast smoke test
Exhaustive Every single output element Catches all bugs Final verification

In this course: for the KWS inference kernel (4000 output elements × 8 test vectors = 32,000 values), we check all of them against TFLite Micro's output. This caught a pipeline timing bug that only manifested in elements 17–23 of the second output row — a sampled test would have missed it.

Rule: for correctness verification, always check exhaustively. Sampling is for performance debugging.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Verification strategy for Project 1

Building on M01A03, a layered strategy:

  1. Per-instruction directed tests: one short program per instruction (or small group), each checking a specific register result - covers the cycle-count table from M02A03.
  2. Small integer programs: loops, conditionals, simple arithmetic (sums, factorials, array max) - exercises combinations of instructions and realistic control flow (BEQ/BLT + JAL for loops).
  3. Cross-check against a reference: if you have access to a software RV32I simulator (e.g., Spike, or even a simple Python interpreter), compare your core's final register state against the reference for the same program - catches subtle decode/datapath bugs that pass individual instruction tests but fail in combination.
  4. Waveform debugging (GTKWave): for anything that fails step 2/3, trace state, PC, IR, and the relevant registers cycle-by-cycle.
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Synthesis & timing on the Tang Nano 9K

  • Once simulation passes, run the M01A04 flow: Yosys -> nextpnr-himbaechel -> gowin_pack -> openFPGALoader.
  • Resource check: a minimal RV32I multicycle core (register file, ALU, control FSM, small instruction/data memories) should comfortably fit within the Tang Nano 9K's ~8,640 LUTs / ~6,480 FFs - but measure it, since this is your baseline for Projects 2 and 3 (see Design Space & Alternatives, §5).
  • Timing: nextpnr reports the maximum clock frequency the routed design meets. A correct-but-unconstrained design might report e.g. 60 MHz - more than enough headroom below the 27 MHz onboard oscillator, but worth understanding why a number comes out the way it does (long combinational paths through the ALU and immediate-generation logic are common culprits).
  • Observable execution: drive the 6 onboard LEDs from, e.g., the low bits of x10 after ebreak - a simple, visible "it worked" signal without needing UART (that's Project 2).
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Commit hygiene: one commit per validated stage

Each project stage should be committed only after it passes its verification:

# Bad: one giant commit mixing broken states
git commit -m "add everything"

# Good: one commit per validated milestone
git commit -m "Stage 1: fetch/decode working — all I-type tests pass"
git commit -m "Stage 2: ALU complete — arithmetic tests pass"
git commit -m "Stage 3: branches working — branch test suite passes"

Why this matters:

  • git bisect can find the exact commit that broke something
  • Your commit history is a record of what was working when
  • Partial work in progress goes in a branch, not main

Project requirement: each project submission must have at least one commit per stage in the rubric. Commit messages must describe what was verified, not just what was changed.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Debug by bisection

When a synthesis anomaly appears (unexpectedly high LUT count, timing failure, wrong behavior), do not debug the whole system at once. Bisect:

  1. Isolate the suspect module: synthesize it alone with yosys -p "synth_gowin -top suspect -json out.json" suspect.sv
  2. Compare against expectation: if stat shows 2000 LUTs for a module that should fit in 2 BRAMs, the module itself is the problem — not integration
  3. Remove features one by one: comment out the async reset, switch to a single-port memory, etc., re-synthesize after each change until the anomaly disappears
  4. The removed feature is the culprit

Example from the reference implementation: a memory module used 16,089 LUT4 instead of ~4 BSRAM blocks. Bisection revealed two causes: (1) async reset in the write process, (2) three memories written in a single if/else chain (see BRAM pitfalls slide).

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Project 1: milestones

A suggested week-by-week breakdown across Weeks 3-6:

Week Milestone
3 Register file + ALU modules, each with their own Verilator testbench (M01A03's per-module pattern)
4 Control FSM + datapath wiring for R/I-type instructions; directed tests for ADD/ADDI/etc. pass in simulation
5 Add branches, loads, stores; small integer programs (loops, conditionals) pass; ebreak termination works
6 Synthesize, place & route, run on the Tang Nano 9K; resource/timing report; documentation
  • This is a suggestion, not a requirement - pairs working ahead can start exploring Project 2's bus/peripherals early (with instructor input on whether to extend this core or branch a copy).
  • Security: identify one security vulnerability in your design and describe how you would mitigate it (see Security corner below).
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Wrapping up Module 2

You now have everything needed for Project 1: the ISA (M02A01), the datapath (M02A02), the control FSM (M02A03), and how to verify, compile for, and run it on real hardware (today).

The slides that follow are optional enrichment content — covered if time permits or assigned as self-study. They provide important context for secure and energy-efficient processor design but are not required for Project 1.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Security corner: writable IMEM without protection (Optional)

Your Project 1 design has a writable instruction memory — any SW instruction to the IMEM address range can overwrite program code. This is code injection by design.

  • In a real system, this is the attack surface that buffer overflows exploit: the attacker writes machine code into memory and redirects execution to it.
  • Your processor has no mechanism to distinguish "code" from "data" at the hardware level — it executes whatever is at the address in PC.

The missing defense: Physical Memory Protection (PMP)

RISC-V defines PMP (Physical Memory Protection) registers that enforce access rules per memory region:

┌─────────────────────┐
│  0x0000 – 0x0FFF    │  IMEM  → PMP: execute-only (no write after boot)
│  0x1000 – 0x1FFF    │  DMEM  → PMP: read/write (no execute)
│  0x2000 – 0x2FFF    │  I/O   → PMP: read/write (no execute)
└─────────────────────┘
  • With PMP, a store to the IMEM range would raise an access fault exception instead of silently overwriting instructions.
  • Your Project 1 core does not implement PMP — this is an intentional simplification. But you should be aware of the risk.

Bare-metal C without an OS

Without an operating system, your C programs also lack:

Protection What it does Your system
Stack canaries Detect buffer overflows before return Not present
ASLR Randomize memory layout each run Not possible (fixed memory map)
W⊕X (NX bit) Memory is writable or executable, never both Not enforced
MMU / virtual memory Isolate processes from each other Not present

Project 1 security question: "Your IMEM is writable. Describe a scenario where this could be exploited and one hardware change that would prevent it."

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Energy corner: multicycle vs single-cycle power (Optional)

A single-cycle processor activates every datapath component on every cycle — the ALU, memory, register file, and all muxes are always switching, even for a NOP.

A multicycle processor only activates the components needed for the current state:

State Active components Idle components
FETCH IMEM, PC, ALU (PC+4) DMEM, register write port
DECODE Register file read, immediate decode IMEM, DMEM, ALU
EXECUTE ALU IMEM, DMEM
MEMORY DMEM IMEM, ALU
  • Fewer active components per cycle → lower activity factor () → less dynamic power.
  • But: more cycles per instruction → the clock runs longer for the same work.
  • Net effect: multicycle usually wins on energy per instruction (power × time), even if it loses on raw throughput.

Clock gating — the key optimization

In a production design, you would gate the clock to idle modules: if the ALU is not needed in the MEMORY state, its clock input is held low → zero dynamic power.

  • Your Project 1 does not implement clock gating — but the FSM structure already enables it. The control signals you designed are exactly the enable signals a clock-gating cell would use.
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Energy corner: RISC-V and energy-efficient ISA design (Optional)

RISC-V's fixed 32-bit instruction width has an energy cost: fetching 4 bytes per instruction, even when a simpler encoding (like ARM Thumb's 16-bit) would suffice.

The RV32C (Compressed) extension addresses this: 16-bit encodings for the most common instructions reduce instruction memory bandwidth by ~25-30%, which directly reduces:

  • IMEM read energy (fewer bits switched on the memory bus).
  • Cache energy (more instructions per cache line).
ISA variant Avg instruction size Relative fetch energy
RV32I 4.0 bytes 1.00×
RV32IC ~3.0 bytes ~0.75×
ARM Thumb-2 ~3.2 bytes ~0.80×

Your Project 1 implements RV32I without the C extension — but in a battery-powered product, adding RV32C would be one of the first optimizations.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Reproducibility note: testing as documentation

Your Verilator testbenches are executable specifications of your processor:

  • test_addi.sv documents that ADDI x1, x0, 42 stores 42 in x1 after exactly 4 cycles.
  • If someone modifies the FSM and breaks ADDI, the test fails — the specification is enforced, not just written.

GitHub Classroom CI integration

Your project repository can run Verilator tests on every push:

# .github/workflows/test.yml
name: Verilator tests
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: make -C tb test
  • Every commit gets a green checkmark (tests pass) or red X (something broke).
  • The instructor sees test results without downloading and building your code.
  • Your commit history becomes a verifiable record of when each instruction started working.
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Next module

Module 3 - HW/SW Interfaces (Weeks 7-9): extending your core with a custom MUL instruction, a memory-mapped bus, and UART/timer/GPIO peripherals - Project 2.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0