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.

systemverilog
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.

systemverilog
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.

diagram
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
systemverilog
// 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.