Part IArchitectural Foundations

Lab --- The Open-Source HDL Workflow

August 3, 2026·23 min read·beginner

Chapter 11 used Chisel and Verilator to design and simulate a 32-bit ALU. That workflow covers the write-simulate-debug cycle but leaves out two important steps in the digital design flow: synthesis…

Chapter 11 used Chisel and Verilator to design and simulate a 32-bit ALU. That workflow covers the write-simulate-debug cycle but leaves out two important steps in the digital design flow: synthesis (translating RTL to gates) and event-driven simulation (running a Verilog testbench with full timing). This lab chapter fills those gaps by introducing the open-source HDL workflow that the reader will use for the rest of the book’s projects and labs.

The workflow has five tools. Yosys reads Verilog, optimizes the logic, and maps it to a gate-level netlist. Icarus Verilog (iverilog) provides event-driven simulation with full Verilog timing semantics. Verilator provides fast cycle-accurate simulation for regression testing. Surfer displays the waveforms. Netlistsvg renders the synthesized netlist as a schematic diagram. Together, these five tools give the reader a complete, cross-platform, zero-cost alternative to the commercial EDA suites (Synopsys, Cadence, Siemens) that cost hundreds of thousands of dollars per seat.

01.Setup and Installation

macOS (Homebrew)

macOS setup via Homebrew

Bash
# Yosys (synthesis)
brew install yosys
# Icarus Verilog (event-driven simulation)
brew install icarus-verilog
# Verilator (cycle-accurate simulation)
brew install verilator
# Surfer (waveform viewer)
brew install --cask surfer
# Netlistsvg (netlist visualization, requires Node.js)
brew install node
npm install -g netlistsvg

Verify macOS installations

Bash
yosys -V # Yosys 0.4x
iverilog -V # Icarus Verilog version 1x.x
verilator --version # Verilator 5.x
surfer --version
netlistsvg --version # should print a version number

Linux (Arch as canonical)

Arch Linux setup

Bash
sudo pacman -S yosys iverilog verilator nodejs npm
yay -S surfer
npm install -g netlistsvg

For Debian/Ubuntu:

Debian/Ubuntu alternative

Bash
sudo apt install yosys iverilog verilator nodejs npm
npm install -g netlistsvg
# Surfer: download AppImage from the Surfer GitHub releases

For Fedora:

Fedora alternative

Bash
sudo dnf install yosys iverilog verilator nodejs npm
npm install -g netlistsvg
# Surfer: same as Debian instructions

Yosys and Icarus Verilog have limited native Windows support. The recommended path is ArchWSL under WSL2:

Windows setup via WSL2 (ArchWSL)

Bash
# Inside the ArchWSL terminal:
sudo pacman -S yosys iverilog verilator nodejs npm
yay -S surfer
npm install -g netlistsvg

Surfer also has a native Windows binary. Download it from the Surfer GitHub releases page and run it on the Windows side. Pass the VCD path from within WSL2 using the /mnt/c/… mapping or copy the trace file to the Windows filesystem.

02.Lab Exercise 1: Event-Driven Simulation with Icarus Verilog

The goal of this exercise is to write a small Verilog design, write a testbench, simulate with Icarus Verilog, and view the waveform in Surfer.

The design: a 4-bit ripple-carry adder

A 4-bit ripple-carry adder in Verilog

Verilog
module rca4 ( input [3:0] a, b, input cin, output [3:0] sum, output cout ); wire [4:0] carry; assign carry[0] = cin; genvar i; generate for (i = 0; i < 4; i = i + 1) begin : fa assign sum[i] = a[i] ^ b[i] ^ carry[i]; assign carry[i+1] = (a[i] & b[i]) | (a[i] & carry[i]) | (b[i] & carry[i]); end endgenerate assign cout = carry[4]; endmodule

The generate for loop unrolls at elaboration time, producing four full-adder stages connected by the carry chain. This is structural Verilog: each iteration describes one piece of hardware that exists simultaneously with all the others.

The testbench

Testbench for the 4-bit adder

Verilog
`timescale 1ns / 1ps module rca4_tb; reg [3:0] a, b; reg cin; wire [3:0] sum; wire cout; rca4 dut (.a(a), .b(b), .cin(cin), .sum(sum), .cout(cout)); initial begin $dumpfile("rca4.vcd"); $dumpvars(0, rca4_tb); // Test vector 1: 5 + 3 = 8 a = 4'd5; b = 4'd3; cin = 0; #10; // Test vector 2: 15 + 1 = 16 (overflow) a = 4'd15; b = 4'd1; cin = 0; #10; // Test vector 3: 7 + 8 + 1 = 16 (carry-in) a = 4'd7; b = 4'd8; cin = 1; #10; // Test vector 4: 0 + 0 + 0 = 0 a = 4'd0; b = 4'd0; cin = 0; #10; $finish; end // Self-checking reg [4:0] expected; always @(*) begin expected = a + b + cin; #1; if ({cout, sum} !== expected) $display("FAIL: %d + %d + %d = %d, got %d", a, b, cin, expected, {cout, sum}); end endmodule

Running the simulation

Compile and simulate with Icarus Verilog

Bash
# Compile the design and testbench
iverilog -o rca4_sim rca4.v rca4_tb.v
# Run the simulation (produces rca4.vcd)
vvp rca4_sim
# Open the waveform
surfer rca4.vcd

In Surfer, add signals a, b, cin, sum, and cout to the waveform display. Verify that the sum and carry-out match the expected values at each time step.

03.Lab Exercise 2: Synthesis with Yosys

The goal of this exercise is to synthesize the 4-bit adder with Yosys and inspect the resulting gate-level netlist.

Running synthesis

Synthesize with Yosys

Bash
yosys -p "
read_verilog rca4.v;
synth -top rca4;
write_json rca4.json;
stat
"

The synth command performs technology-independent optimization (constant propagation, dead-code elimination, logic minimization). The write_json command exports the netlist in Yosys’s JSON format. The stat command prints a summary of the cell counts.

Viewing the netlist

Generate an SVG schematic from the netlist

Bash
netlistsvg rca4.json -o rca4.svg

Open rca4.svg in a web browser or image viewer. The schematic shows the gate-level structure of the adder: XOR gates for the sum bits, AND-OR trees for the carry chain, and the connections between them. Compare this with the structural Verilog source. The synthesis tool may have optimized or restructured some of the logic, but the functional equivalence is guaranteed.

Gate-level simulation

To verify that the synthesized netlist produces the same outputs as the RTL, export the netlist as Verilog and simulate it with the same testbench:

Gate-level simulation with Icarus Verilog

Bash
# Export Verilog netlist from Yosys
yosys -p "
read_verilog rca4.v;
synth -top rca4;
write_verilog rca4_gates.v
"
# Simulate the gate-level netlist
iverilog -o rca4_gate_sim rca4_gates.v rca4_tb.v
vvp rca4_gate_sim

If the testbench reports no failures, the gate-level netlist is functionally equivalent to the RTL. This is the same equivalence check (at a basic level) that ASIC design teams run after synthesis and after place-and-route.

04.Lab Exercise 3: Cycle-Accurate Simulation with Verilator

Icarus Verilog is an event-driven simulator: it re-evaluates signals whenever their inputs change, which faithfully models Verilog timing semantics (including delays and glitches) but runs slowly on large designs. Verilator takes a different approach. It compiles the RTL into a C++ class that evaluates the circuit once per clock cycle, ignoring intra-cycle glitches. The result is dramatically faster simulation at the cost of losing sub-cycle timing information.

For regression testing of processors and other large designs, Verilator is the standard choice in the open-source hardware community. The RISC-V Rocket Chip, BOOM, and many Linux-capable SoC designs use Verilator as their primary simulation platform.

A simple C++ testbench for Verilator

Verilator C++ testbench for the 4-bit adder

C++
#include "Vrca4.h" #include "verilated.h" #include "verilated_vcd_c.h" #include <cstdio> int main(int argc, char **argv) { Verilated::commandArgs(argc, argv); Verilated::traceEverOn(true); Vrca4 *dut = new Vrca4; VerilatedVcdC *vcd = new VerilatedVcdC; dut->trace(vcd, 99); vcd->open("rca4_verilator.vcd"); int time = 0; int errors = 0; // Test all 4-bit input combinations for (int a = 0; a < 16; a++) { for (int b = 0; b < 16; b++) { for (int cin = 0; cin < 2; cin++) { dut->a = a; dut->b = b; dut->cin = cin; dut->eval(); vcd->dump(time++); int expected = a + b + cin; int got = (dut->cout << 4) | dut->sum; if (got != expected) { printf("FAIL: %d+%d+%d=%d, " "got %d\n", a, b, cin, expected, got); errors++; } } } } vcd->close(); delete dut; printf("Tests: 512, Errors: %d\n", errors); return errors ? 1 : 0; }

Building and running with Verilator

Build and run the Verilator testbench

Bash
# Compile the design into C++
verilator --cc rca4.v --trace --exe tb_rca4.cpp
# Build the executable
make -C obj_dir -f Vrca4.mk
# Run the simulation
./obj_dir/Vrca4
# View the trace
surfer rca4_verilator.vcd

The Verilator simulation tests all 512 input combinations (16×16×216 \times 16 \times 2) in a fraction of a second. Icarus Verilog also finishes this sweep quickly, because 512 vectors through a handful of gates is a trivial workload for either engine. The event-driven engine’s higher per-evaluation overhead becomes the limiting factor only once the design grows to a full CPU core and the vector count grows with it.

05.Lab Exercise 4: Putting It All Together

The final exercise applies the full workflow to a slightly larger design: the 32-bit ALU from Chapter 11, this time written in Verilog rather than Chisel.

ALU in Verilog

32-bit ALU in Verilog (abbreviated)

Verilog
module alu32 ( input [31:0] a, b, input [3:0] op, input [4:0] shamt, output reg [31:0] result, output zero ); always @(*) begin case (op) 4'b0000: result = a + b; 4'b0001: result = a - b; 4'b0010: result = a & b; 4'b0011: result = a | b; 4'b0100: result = a ^ b; 4'b0101: result = ~(a | b); 4'b0110: result = ($signed(a) < $signed(b)) ? 32'd1 : 32'd0; 4'b0111: result = (a < b) ? 32'd1 : 32'd0; 4'b1000: result = a << shamt; 4'b1001: result = $signed(a) >>> shamt; default: result = 32'd0; endcase end assign zero = (result == 32'd0); endmodule

Workflow steps

The exercise asks the reader to perform each step of the workflow:

Step 1: Simulate with Icarus Verilog. Write a Verilog testbench that tests at least five operations. Compile with iverilog, run with vvp, and inspect the VCD in Surfer.

Step 2: Simulate with Verilator. Write a C++ testbench (or use ChiselTest if the Chisel version from Chapter 11 is preferred). Run all 10 operations with multiple input vectors. Verify zero errors.

Step 3: Synthesize with Yosys. Run synth -top alu32 in Yosys. Record the cell counts from stat. Export the JSON netlist and generate an SVG schematic with Netlistsvg. Identify the adder, the mux tree, and the shifter in the schematic.

Step 4: Gate-level simulation. Export the synthesized Verilog netlist and simulate it with the same testbench. Confirm functional equivalence.

This four-step workflow (RTL simulation, cycle-accurate regression, synthesis, gate-level verification) is the backbone of every open-source hardware project. The rest of the book’s projects and labs follow this exact pattern, scaling up to pipelined CPUs, cache controllers, and multicore interconnects.

06.Tool Summary

Table 1. Open-source HDL workflow tool summary.

ToolRoleKey command
YosysSynthesisyosys -p "read_verilog f.v; synth; write_json f.json; stat"
Icarus VerilogEvent-driven simiverilog -o sim f.v tb.v && vvp sim
VerilatorCycle-accurate simverilator --cc f.v --trace --exe tb.cpp
SurferWaveform viewersurfer trace.vcd
NetlistsvgNetlist viewernetlistsvg f.json -o f.svg

07.Understanding Yosys Internals

Yosys processes a design through a pipeline of internal passes. Each pass transforms the design representation. Understanding the major passes helps the reader interpret synthesis reports and diagnose unexpected results.

Parsing. The read_verilog command parses Verilog source into Yosys’s internal abstract syntax tree (AST). SystemVerilog support is partial and requires the -sv flag. VHDL support is available through the ghdl plugin (ghdl --synth followed by read_verilog).

Elaboration. Yosys resolves parameters, unrolls generate loops, and flattens the module hierarchy (if the user requests -flatten). After elaboration, every module instance is a concrete collection of cells and wires with fixed widths.

Technology-independent optimization. The opt family of passes runs constant propagation (opt_const), dead-code elimination (opt_clean), and logic simplification (opt_reduce). These passes remove unused signals, fold constant muxes, and simplify Boolean expressions without knowing anything about the target technology.

Technology mapping. For FPGA targets, the synth_ice40, synth_ecp5, or synth_xilinx commands map the optimized logic to the target’s lookup tables (LUTs), flip-flops, and block RAMs. For ASIC targets, the abc pass (which invokes the ABC logic synthesis engine from UC Berkeley) maps logic to cells from a Liberty (.lib) standard-cell library.

Output. The mapped netlist is written in the requested format: write_verilog for Verilog, write_json for JSON (consumed by Netlistsvg and other tools), or write_blif / write_edif for other downstream tools.

Table 2. Common Yosys commands and their roles.

CommandPurpose
read_verilogParse Verilog source
synthRun the full generic synthesis flow
synth_ice40Synthesize for Lattice iCE40 FPGAs
optTechnology-independent optimization
abcTechnology mapping via the ABC engine
statPrint cell and wire count statistics
write_verilogExport gate-level Verilog
write_jsonExport JSON netlist for Netlistsvg
showRender a GraphViz diagram (requires xdot)

08.Makefile for the Workflow

As the number of source files and tool invocations grows, typing commands by hand becomes tedious and error-prone. A Makefile automates the workflow and ensures that each step uses the correct flags and file paths.

Makefile for the open-source HDL workflow

Makefile
DESIGN = rca4 TOP = rca4 SOURCES = $(DESIGN).v TB_IV = $(DESIGN)_tb.v TB_VL = tb_$(DESIGN).cpp # Icarus Verilog simulation .PHONY: sim-iv sim-iv: $(DESIGN).vcd $(DESIGN).vcd: $(SOURCES) $(TB_IV) iverilog -o $(DESIGN)_iv $(SOURCES) $(TB_IV) vvp $(DESIGN)_iv # Verilator simulation .PHONY: sim-vl sim-vl: obj_dir/V$(TOP) ./obj_dir/V$(TOP) obj_dir/V$(TOP): $(SOURCES) $(TB_VL) verilator --cc $(SOURCES) --trace \ --exe $(TB_VL) --top-module $(TOP) make -C obj_dir -f V$(TOP).mk # Yosys synthesis .PHONY: synth synth: $(DESIGN).json $(DESIGN).json: $(SOURCES) yosys -p "read_verilog $(SOURCES); \ synth -top $(TOP); \ write_json $(DESIGN).json; stat" # Netlist visualization $(DESIGN).svg: $(DESIGN).json netlistsvg $(DESIGN).json -o $(DESIGN).svg # Gate-level simulation .PHONY: gate-sim gate-sim: $(DESIGN)_gates.v $(TB_IV) iverilog -o $(DESIGN)_gate_sim \ $(DESIGN)_gates.v $(TB_IV) vvp $(DESIGN)_gate_sim $(DESIGN)_gates.v: $(SOURCES) yosys -p "read_verilog $(SOURCES); \ synth -top $(TOP); \ write_verilog $(DESIGN)_gates.v" # View waveform .PHONY: wave wave: $(DESIGN).vcd surfer $(DESIGN).vcd # Clean .PHONY: clean clean: rm -rf obj_dir *.vcd *.vvp *_iv *_sim \ *.json *.svg *_gates.v

With this Makefile in place, the entire workflow reduces to a few short commands: make sim-iv for event-driven simulation, make sim-vl for Verilator regression, make synth for synthesis, make gate-sim for gate-level equivalence testing, and make wave to open Surfer. The reader should adapt this Makefile for every lab and project chapter that follows.

09.Comparing Event-Driven and Cycle-Accurate Simulation

The choice between Icarus Verilog and Verilator is not a matter of quality. It is a matter of purpose.

Event-driven simulation (Icarus Verilog) faithfully models the Verilog timing semantics. Every signal change triggers re-evaluation of every expression that depends on it. Time advances in discrete events, and the simulator can model propagation delays, setup/hold checks (with appropriate annotations), and intra-cycle glitches. The cost is speed: an event-driven simulator may run 10 to 100 times slower than a cycle-accurate simulator for the same design.

Cycle-accurate simulation (Verilator) evaluates the entire design once per clock cycle. It does not model delays or glitches. The output of every combinational path is the steady-state value, computed as if the combinational logic settles instantaneously. This abstraction matches the RTL designer’s mental model (the output is correct at the end of every clock cycle) and enables much higher simulation throughput.

For the projects in this book, Verilator is the default simulation engine. Icarus Verilog is used when the reader needs to inspect intra-cycle behavior or when a testbench relies on Verilog timing constructs (#delay, @(posedge ...) with specific timing) that Verilator does not support.

10.Debugging Strategies

When a test fails, the open-source workflow provides several approaches for locating the bug.

Waveform inspection. The first step is always to open the VCD or FST trace in Surfer and add the signals of interest. Look for the clock cycle where the actual output diverges from the expected output. Then trace backward through the combinational logic by adding intermediate signals to the waveform. Most bugs are visible as a signal that stays at an unexpected constant value (an unconnected wire, a stuck mux select) or that changes one cycle too early or too late (a missing register, an off-by-one in a counter).

Print statements. In an Icarus Verilog testbench, $display and $monitor print signal values to the console at specific simulation times. In a Verilator C++ testbench, printf serves the same role. Print statements are crude but fast, and they are often the quickest path to a diagnosis when the waveform has thousands of signals and the failing cycle is buried deep in the trace.

Assertions. SystemVerilog’s assert keyword (supported in both Icarus Verilog and Verilator with the --assert flag) checks a condition at simulation time and reports an error if it fails. Placing assertions at key points in the design (the output of every mux, the carry chain of every adder, the state register of every FSM) turns silent data corruption into an immediate, located failure.

Gate-level versus RTL comparison. If the RTL simulation passes but the gate-level simulation fails, the bug is in the synthesis mapping (rare with Yosys for combinational logic, more common when latches are inferred). If both fail identically, the bug is in the RTL. If neither fails but the waveform looks wrong, the bug is in the testbench.

11.Looking Ahead

This lab chapter completes Part I of the book. The reader now has the full set of foundational tools and concepts: the computing stack and the layers of abstraction (Chapter 1), technology trends and the quantitative principles of design (Chapter 2), number systems and encodings (Chapter 3), Boolean algebra and combinational logic (Chapter 4), digital building blocks (Chapter 5), sequential logic and timing (Chapter 6), integer and floating-point arithmetic (Chapters 7 and 8), the physics of CMOS and technology scaling (Chapter 9), hardware description languages (Chapter 10), the 32-bit ALU built in Chisel (Chapter 11), and the open-source HDL workflow (this chapter).

Part II turns to instruction set architectures: the programmer-visible contract between software and hardware. The first concept chapter (Chapter 13) opens with the anatomy of an instruction and the design decisions that distinguish one ISA family from another. The HDL and simulation skills from Chapters 10 through 12 return in full force in Part III, where the reader builds a single-cycle and then a pipelined RISC-V CPU.

12.Worked Examples

13.Exercises

Book mode
computer-architecturearchitectural-foundations
Was this helpful?