SystemVerilog Basics

Chapter 3 of 16

Running Your First Simulation: EDA Playground, Waveforms, and $finish

Actually run a SystemVerilog testbench: set up EDA Playground, understand $timescale and its more modern replacement, timeunit/timeprecision, learn why simulation never ends without $finish, dump and read a waveform with $dumpfile/$dumpvars, and get a complete, copy-pasteable example that works end to end.

The last two chapters covered the "design vs. verification" mental model and the raw syntax needed to read code. But reading isn't running — and HDL is one of the things you learn fastest by actually simulating it. This chapter is entirely about the mechanics of getting a simulation to actually execute: where to run code, how to make it stop, and how to look at what happened.

Where to run code: EDA Playground

You don't need to install a simulator to follow this track. EDA Playground runs one in the browser, for free, with no setup:

  1. Create a free account (or use the "Continue as guest" option).
  2. On the left, you'll see two panels: Testbench and (optionally) Design. For everything in this track, one file in the Testbench panel is enough — there's no separate DUT file to manage.
  3. Under Tools & Simulators, pick a simulator. Any of the free ones (e.g. Icarus Verilog) can run almost every example in this track — nothing here needs a paid license. Two call-outs, so a later surprise doesn't read as a new problem: the UVM track needs a UVM-capable simulator instead (Icarus doesn't support the UVM class library, called out explicitly there), and this track's own clocking block section (ch13) needs a simulator with full SystemVerilog support instead of Icarus (Icarus doesn't implement clocking blocks at all — confirmed directly, not assumed — called out explicitly there too, with a specific alternative to pick).
  4. Check Open EPWave after run so the waveform viewer opens automatically once simulation finishes.
  5. Paste code into the Testbench panel and click Run.

That's the whole workflow: paste, run, look at the log and the waveform. Every example from here on is written to be pasted directly into that Testbench panel and run as-is.

`timescale: what a "time unit" actually is

Earlier chapters used #10 without ever pinning down what "10" means. `timescale answers that, and it's normally the very first line of a file:

`timescale 1ns/1ps
  • The first number (1ns) is the time unit — what #10 actually means: 10 of that unit, i.e. 10ns here.
  • The second number (1ps) is the time precision — the smallest time increment the simulator tracks internally, used for rounding.

If you omit `timescale, most simulators fall back to a tool-specific default — which is exactly why it's worth setting explicitly rather than leaving it to chance. Every example below starts with `timescale 1ns/1ps, which is a common, sensible default for this kind of teaching example.

timeunit/timeprecision: the more modern way to write this

`timescale is a compiler directive inherited from Verilog: it behaves more like a text macro, applying to every module compiled after that line, up until the next `timescale shows up. If several files with different `timescale settings get compiled together, it's easy for some module to silently end up with the wrong time unit — a classic gotcha.

SystemVerilog offers a more modern alternative: the timeunit/timeprecision keywords, declared directly inside a module/interface/program/package, scoped precisely to that block — unaffected by compile order or other files:

module tb;
  timeunit      1ns;
  timeprecision 1ps;
 
  // ... same effect as `timescale 1ns/1ps, but scoped to just this module
endmodule

There's also a compact single-line form with identical meaning:

module tb;
  timeunit 1ns/1ps;
  // ...
endmodule

`timescale is still everywhere — plenty of existing code and tutorials use it, and later examples in this track keep using it for brevity. But new code should generally prefer timeunit/timeprecision: the semantics are identical (first value is the time unit, second is the time precision), just expressed with a keyword form that's scoped clearly and doesn't depend on compile order, avoiding the "who silently changed this in which file" problem `timescale can create — and it's what most UVM codebases actually use today.

A quick note on program, since it just showed up in that list: every testbench in this track is written inside a plain module, the same construct used for a DUT. SystemVerilog also has a program block — same idea as module, but historically meant specifically for testbench code, with its own scheduling region (it runs after all module-based logic settles for that time step, mainly to sidestep certain race conditions between testbench and DUT). It still shows up in legacy code, but the class-based, virtual interface-driven style this track builds toward (and UVM itself) is written in plain modules, so program isn't used here — worth being able to recognize it if it shows up in someone else's code, not something to reach for yourself.

$finish: why your simulation never seems to end

A simulation doesn't stop on its own just because your initial block reaches its last line — if there's nothing telling the simulator to stop, it will sit there waiting for more events (or the tool's own runtime limit kicks in). $finish is the system task that tells the simulator "stop now":

initial begin
  // ... stimulus and checks ...
  $finish;
end

Forgetting $finish is the single most common reason a beginner's first simulation "hangs" or produces no output — the simulation either times out on EDA Playground's server-side limit or just never reaches the point where it would print a summary. Every runnable example in this track ends its top-level initial block with $finish.

$dumpfile / $dumpvars: recording a waveform

$display prints values at the instant you ask, but to see how a signal's value changes over time, you need a waveform. Two system tasks turn that on:

initial begin
  $dumpfile("waves.vcd");   // the output file EPWave will open
  $dumpvars(0, tb);         // dump every signal, starting from module "tb", recursing into everything inside it
end
  • $dumpfile("waves.vcd") names the file the waveform gets written to (EDA Playground's EPWave viewer picks this up automatically — you don't need to open the file yourself).
  • $dumpvars(0, tb) starts recording. The first argument is how many levels deep to recurse (0 means "all levels," i.e. everything nested inside tb, however deep); the second is which module to start from.

Place this pair near the top of your top-level initial block, before you start driving signals — anything that happens before $dumpvars runs won't show up in the waveform.

Reading the waveform

Once a simulation with $dumpfile/$dumpvars finishes, EPWave opens showing:

  • A signal list on the left — click a signal name to add it to the waveform view (or it may already be added, depending on the viewer's default).
  • A timeline across the top, in the units `timescale specified.
  • Colored waveform traces for each signal — for a single-bit signal, a low line is 0, a high line is 1, and (for 4-state signals) a distinct color/pattern usually marks x.
  • A cursor you can click and drag along the timeline to read a specific signal's value at that instant, and a zoom control to expand a busy region or collapse a long idle stretch.

The most common first move: drag the cursor to just after you change an input, and check whether the output actually updated the way you expected.

A complete, runnable example

Here's mux2 from chapter 1, wrapped in an actual testbench with everything above wired in — paste this directly into EDA Playground's Testbench panel and run it as-is:

`timescale 1ns/1ps
 
module mux2 (
  input  logic sel,
  input  logic a,
  input  logic b,
  output logic y
);
  always_comb begin
    y = sel ? b : a;
  end
endmodule
 
module tb;
  logic sel, a, b, y;
 
  mux2 dut (.sel(sel), .a(a), .b(b), .y(y));
 
  initial begin
    $dumpfile("waves.vcd");
    $dumpvars(0, tb);
 
    sel = 1'b0; a = 1'b1; b = 1'b0;
    #10;
    $display("t=%0t sel=%0b a=%0b b=%0b y=%0b", $time, sel, a, b, y);
 
    sel = 1'b1;
    #10;
    $display("t=%0t sel=%0b a=%0b b=%0b y=%0b", $time, sel, a, b, y);
 
    $finish;
  end
endmodule

Running this should print two lines (y following a when sel=0, then following b when sel=1) and open a waveform with four traces (sel, a, b, y) that you can inspect directly. This is also the template worth reusing whenever you want to try out a snippet from a later chapter that isn't already wrapped in a runnable module: drop the snippet's declarations and logic into a tb module's body (or its own class/task, called from initial), add $dumpfile/$dumpvars if you want a waveform, and end with $finish.

Summary

  • EDA Playground runs a real simulator in the browser — paste code into the Testbench panel, pick a free simulator (e.g. Icarus Verilog), and click Run.
  • `timescale <unit>/<precision> (usually the first line of a file) defines what #10-style delays actually mean and how finely the simulator tracks time.
  • timeunit/timeprecision mean the same thing but are scoped to a single module/interface/program/package and don't depend on compile order — the more modern, recommended alternative to `timescale.
  • $finish stops the simulation — forgetting it is the most common reason a first simulation appears to hang or print nothing.
  • $dumpfile("name.vcd") + $dumpvars(0, top_module), placed early in the top-level initial block, record a waveform you can inspect after the run.
  • A waveform viewer shows a signal list, a timeline, and colored traces; drag the cursor to read a specific signal's value at a specific time.
  • Any later chapter's bare code snippet can be made runnable by dropping it into a module tb; ... endmodule shell with an initial block ending in $finish, following the pattern in this chapter's full example.

A simulation seems to just hang forever with no output. What's the most likely cause?

What does `timescale 1ns/1ps mean?

What's the main advantage of timeunit/timeprecision keywords inside a module over the `timescale compiler directive?

Which system task tells the simulator to stop running? (with the leading $, lowercase)