Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations35
    • SystemVerilog Data Types
      • 4-State vs 2-State Types
      • Nets vs Variables
      • Integer Types & Casting
      • Enums, Structs & Unions
      • X/Z Propagation & Pessimism
      • Strings, Reals & Special Types
    • Arrays: Packed, Dynamic, Queues & Associative
    • Procedural Code & Scheduling
    • Operators, Expressions & Width Rules
    • Packages, Typedefs & Compilation
    • Interfaces, Modports & Clocking Blocks
  • Object-Oriented SystemVerilog for Verification35
  • Constraint Randomization — Interview-Critical Deep Dive66
  • SystemVerilog Assertions — Interview-Critical Deep Dive63
  • Functional Coverage in SystemVerilog28
  • Testbench Architecture in SystemVerilog38
  • Advanced Language & Integration30
  • Senior & Interview Preparation34
  • Principal-Level Verification28
Loading lesson

‹SystemVerilog Data Types
  1. Topics
  2. SystemVerilog
  3. SystemVerilog Language Foundations
  4. SystemVerilog Data Types
  5. Strings, Reals & Special Types

Part 1 · Language Foundations · Intermediate

Strings, Reals & Special Types

Dynamic strings, real-number math, opaque chandle handles, events, and message formatting.

On this page

    Why this matters

    A bandwidth report said 409 MB/s when the link moved 409.6. Two ints divided before the result reached a real. Strings, reals and events each have a rule like this that the compiler never warns about.

    After this lesson you can: use string methods and $sformatf for messages, do real-number math without losing the fraction, and wait for an event without a same-timestep race.

    Strings are dynamic values

    A SystemVerilog string is a variable-length byte sequence that grows and shrinks at runtime. It compares lexicographically, concatenates with braces, and indexes per character, with each element a byte.

    Two semantics points trip people up. substr(i, j) takes start and end indexes, both inclusive, not start and length. Assignment copies the contents, so strings are value types with no aliasing and no null handle.

    Expression

    Value

    s.len()

    11 for s = "AXI4_master"

    s.toupper()

    "AXI4_MASTER"

    s.substr(0, 3)

    "AXI4"

    s.getc(0)

    65, the code for A

    "42".atoi()

    42, with atohex, atooct, and atobin beside it

    Real math stays in the testbench

    real is an IEEE-754 double and shortreal a float, both testbench-only in practice. They earn their keep in bandwidth statistics, reference models, and timing math with realtime.

    Conversions need care: assigning real to integer rounds to nearest while $rtoi truncates, and integer division drops the remainder before any real context sees it, so cast one operand first. Never compare computed reals with ==; compare against an epsilon.

    Handles, events, and messages

    chandle is an opaque pointer-sized handle used to hold a pointer that C code returned. SystemVerilog only stores it, passes it back and compares it with null. It only matters once you call C from SystemVerilog, which DPI-C covers.

    event is the named synchronization primitive: trigger with ->ev and wait with @(ev) or wait(ev.triggered). The latter wins when trigger and wait can land in the same timestep, because the triggered state persists while a bare @ that starts waiting too late misses the pulse.

    Event Scheduling explains why the same-timestep case is a race. The bench below uses fork ... join, which runs the two blocks inside it at the same time and waits for both; Part 2 teaches it properly.

    Build messages with $sformatf, which returns the string, and print enums with .name(). Bare %d pads to the full type width, so %0d and %0h strip the padding in log lines.

    Testbench

    Predict first s holds "AXI4_master". What does s.substr(0, 3) return?

    "AXI4". The second argument is the inclusive end index, not a length, so indexes 0 to 3 are four characters.

    String methods and conversions, realtime bandwidth math with rounding rules, and the race-free event wait.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    module tb_strings;
      string s = "AXI4_master";
      string t;
      int errors = 0;
    
      initial begin
        if (s.len() != 11) errors++;
        if (s.toupper() != "AXI4_MASTER") errors++;
        if (s.substr(0, 3) != "AXI4") errors++;   // end index, inclusive
        if (s.getc(0) != 65) errors++;
        t = s;
        t.putc(0, 97);                            // value copy: s keeps its A
        if (t != "aXI4_master") errors++;
        if (s != "AXI4_master") errors++;
        t = "42";
        if (t.atoi() != 42) errors++;
        t = "";
        t.itoa(255);
        if (t != "255") errors++;
        t.hextoa(255);
        if (t != "ff") errors++;
        if ($sformatf("mst_%0d", 3) != "mst_3") errors++;
        if (s.substr(0, 2) != "AXI") errors++;
        if (errors != 0) $fatal(1, "FAIL: string checks saw %0d unexpected results", errors);
        $display("PASS: len, case, substr, getc, putc, atoi, itoa and sformatf all agreed");
        $finish;
      end
    endmodule
    Terminal
    PASS: len, case, substr, getc, putc, atoi, itoa and sformatf all agreed
    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    module tb_realtime;
      timeunit 1ns;
      timeprecision 1ps;
      realtime t_start, t_end, dur;
      real bw, exp_bw = 409.6;
      int bytes_moved = 4096;
      int errors = 0;
    
      initial begin
        t_start = $realtime;
        #10;
        t_end = $realtime;
        dur = t_end - t_start;              // already in ns from the timeunit
        if (dur != 10.0) errors++;
        bw = real'(bytes_moved) / dur;      // cast first, then divide
        if ((bw < exp_bw - 0.001) || (bw > exp_bw + 0.001)) errors++;
        if (int'(2.5) !== 3) errors++;      // static cast rounds
        if ($rtoi(2.5) !== 2) errors++;     // rtoi truncates
        if ($itor(7) / 2 != 3.5) errors++;
        if (errors != 0) $fatal(1, "FAIL: real-math checks saw %0d unexpected results", errors);
        $display("PASS: realtime measured 10 ns, bandwidth matched, rounding rules held");
        $finish;
      end
    endmodule
    Terminal
    PASS: realtime measured 10 ns, bandwidth matched, rounding rules held
    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    module tb_event;
      timeunit 1ns; timeprecision 1ps;
      event done_ev;
      bit seen = 1'b0;
      int errors = 0;
    
      initial begin
        fork
          begin
            wait (done_ev.triggered);        // safe even if the trigger already fired
            seen = 1'b1;
          end
          begin
            -> done_ev;
          end
        join
        #1;
        if (seen !== 1'b1) errors++;
        if (errors != 0) $fatal(1, "FAIL: event wait saw %0d unexpected results", errors);
        $display("PASS: triggered wait caught the same-timestep event");
        $finish;
      end
    endmodule
    Terminal
    PASS: triggered wait caught the same-timestep event

    Common pitfall

    Treating substr like start-plus-length reads one character too many or too few, and computing a ratio of two ints before assigning to real truncates before the cast can help. Cast an operand first and compare reals with an epsilon.

    Interview

    “When does @(ev) miss and wait(ev.triggered) catch?” When producer and consumer run in the same timestep with zero delay, the trigger can fire before the @ starts waiting. The triggered state persists, so the wait form is race-free there.

    Check yourself Two strings a and b hold the same text, and the code changes b.putc(0, 97). Does a change?

    No. Assigning a string copies its contents, so strings have no aliasing and no null value, unlike class handles.

    Check yourself A testbench computes bw as real'(bytes) / dur with dur measured at 10 ns for 4096 bytes. Why cast before dividing?

    Two ints would divide as integers and drop the fraction first. The cast lifts the division into reals so the fractional bandwidth survives.

    Remember

    • substr(i, j) takes two inclusive indexes, not a start and a length.

    • Cast one operand to real before dividing; real to int rounds, $rtoi truncates.

    • wait(ev.triggered) catches a trigger in the same timestep; @(ev) can miss it.

    Rule of thumb: strings copy by value; reals stay in the testbench; events need triggered when timing is close.

    How this was checked

    Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. Icarus 13 cannot run the string bench (no toupper) or the event bench (no .triggered); the real-math bench runs on both.

    chandle is described only; DPI-C, which uses it, is run in its own lesson.

    Related topics

    • Next topic: Arrays

    Previous

    12 / 363

    Next: Arrays: Packed, Dynamic, Queues & Associative

    Up next

    Up nextArrays: Packed, Dynamic, Queues & Associative

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • 4-State vs 2-State Types
    • Nets vs Variables
    • Integer Types & Casting