Part 1 · Language Foundations · Intermediate
Strings, Reals & Special Types
Dynamic strings and their methods, real/shortreal in testbench math, chandle for DPI, the event type, and $sformatf idioms.
The string type
SystemVerilog string is a dynamic, variable-length byte sequence — unlike Verilog's old trick of packing ASCII into a fixed-width reg vector, it grows and shrinks at runtime, compares with ==/< lexicographically, concatenates with {a, b}, and indexes per character with s[i] (each element is a byte). Strings are everywhere in a testbench — component names, file paths, plusargs, log messages — so the method set is worth knowing cold.
string s = "AXI4_master";
string t;
$display("%0d", s.len()); // 11
$display("%s", s.toupper()); // AXI4_MASTER
$display("%s", s.substr(0, 3)); // "AXI4" (start, END index — inclusive)
$display("%0d", s.getc(0)); // 65 ('A')
t = s; t.putc(0, "a"); // value copy — s unchanged
// Number ↔ string conversions
int n = "42".atoi(); // 42 (also atohex, atooct, atobin)
real r = "3.14".atoreal(); // 3.14
t.itoa(255); // t = "255"
t.hextoa(255); // t = "ff"
// Building names and parsing — daily idioms
string nm = $sformatf("mst_%0d", 3); // "mst_3"
if (s.substr(0, 2) == "AXI") $display("AXI-family agent");Two semantics points trip people up: substr(i, j) takes start and end indexes, both inclusive — not start and length like most languages — and strings are value types : assignment copies the contents, so there is no aliasing and no null handle, and an unassigned string is simply empty (len() == 0).
real, shortreal, and TB math
real is an IEEE-754 double (64-bit) and shortreal a float (32-bit). They are testbench-only in practice — not synthesizable — and earn their keep in bandwidth/latency statistics, analog-ish reference models, and timing math with realtime. The conversion rules matter: assigning real to an integer rounds to the nearest (2.5 → 3), while $rtoi truncates (2.5 → 2); $itor goes the other way. Integer division stays integer — a/b with two ints drops the remainder before any real context sees it, so cast one operand first. And never compare reals with == after arithmetic; compare against an epsilon.
int bytes_moved = 4096;
realtime t_start, t_end;
t_start = $realtime;
// ... run traffic ...
t_end = $realtime;
real dur_ns = (t_end - t_start) / 1ns; // realtime → real ns
real bw = real'(bytes_moved) / dur_ns; // cast FIRST, then divide
$display("throughput %.2f bytes/ns", bw);
// Rounding family
int a = int'(2.5); // 3 (cast rounds)
int b = $rtoi(2.5); // 2 (truncates)
real c = $itor(7) / 2; // 3.5
// Epsilon compare — never == on computed reals
real exp_bw = 1.6;
if ((bw > exp_bw - 0.001) && (bw < exp_bw + 0.001))
$display("bandwidth within tolerance");chandle, event, and $sformatf idioms
chandle is an opaque pointer-sized handle for DPI-C : C code returns a void* (a C model instance, a file handle, a socket) and SystemVerilog stores and passes it back without ever dereferencing it — the only legal operations are assignment and comparison with null. This is how a C reference model keeps per-instance state across DPI calls. event is the named synchronization primitive: trigger with ->ev, wait with @(ev) or wait(ev.triggered) — the latter avoids the classic same-timestep race where the trigger fires just before the @ starts waiting. Events are variables: they can be passed, copied (aliasing the same underlying event), and compared.
Legend: [TYPE]
SPECIAL TYPES — WHO OWNS WHAT [TYPE]
SystemVerilog side C side (DPI)
────────────────── ────────────
chandle model; void* make_model(void);
│ import "DPI-C" function ... creates state, returns ptr
│
├── model = make_model(); ────► malloc'd C object
├── step(model, txn); ────► uses ptr, updates state
└── free_model(model); ────► free()
(SV never dereferences — opaque)
event sync:
producer: -> done_ev;
consumer: @(done_ev); races if trigger precedes @
consumer: wait(done_ev.triggered); safe within same timestep// chandle + DPI: stateful C reference model
import "DPI-C" function chandle model_new(int cfg);
import "DPI-C" function int model_step(chandle m, int din);
import "DPI-C" function void model_free(chandle m);
chandle m = model_new(32'h10);
int exp = model_step(m, txn.data);
model_free(m);
// $sformatf idioms — building strings without $display
string msg = $sformatf("[%0t] %s: addr=%08h data=%08h resp=%s",
$time, agent_name, t.addr, t.data, t.resp.name());
string path = $sformatf("env.agent[%0d].monitor", idx);
$display("%s", msg);Format specifier quick rules
%0d / %0h strip leading zeros and spaces — use them in messages; bare %d pads to the type's full width.
%s on an enum prints garbage — call .name(); %p pretty-prints structs, queues, and arrays whole.
%t with $timeformat controls time rendering once, globally — set it in the top module.
$sformatf returns the string; $sformat writes into a string argument — the former composes better.
Key takeaways
string is dynamic and value-typed, with substr(start, end-inclusive) and a full atoi/itoa conversion family.
real→int casting rounds while $rtoi truncates; cast before dividing and compare reals with an epsilon.
chandle carries opaque C pointers across DPI — store, pass back, compare to null; never interpret.
wait(ev.triggered) beats @(ev) when trigger and wait can land in the same timestep.
Common pitfalls
Treating substr like (start, length) — the second argument is the inclusive end index.
Computing a ratio of two ints then assigning to real — the truncation already happened; cast an operand first.
Printing enums with %d or %s on the raw value instead of .name() — unreadable logs in every triage session.
Using @(event) for producer/consumer sync in zero-delay code — the same-timestep trigger is missed; use .triggered.