Skip to content

SystemVerilog · Module 2

String Type & Methods

len, substr, toupper, compare, atoi, itoa, $sformatf — message generation patterns.

Module 2 · Page 2.4

More Than Just $display Arguments

Before SystemVerilog, the only way to work with text in simulation was through system task format strings — write the string to $display and that was the end of it. You couldn't store a string, manipulate it, compare it, or pass it between functions. SystemVerilog's string type changes all of that. It's a first-class data type with built-in methods, operator support, and direct integration with $sformatf.

In real verification work, strings show up constantly: UVM component names (get_full_name()), test names from the command line (+UVM_TESTNAME), register field names in RAL models, log file generation, CSV output for coverage analysis, and scoreboard mismatch messages that show exactly what was expected and what was received.

The key properties to internalize before using strings: dynamic length (grows and shrinks automatically), initializes to "" (empty, not null), indexing gives you a byte (the ASCII code of the character, not the character itself), and comparison operators are case-sensitive.

How the string Type Works

A string variable holds an ordered sequence of bytes (characters). The length is tracked automatically — no null terminator, no fixed buffer size. When you assign a longer string, it grows. When you assign a shorter one, it shrinks. Memory management is completely automatic.

Concatenation uses the same curly-brace syntax as bit vectors: {"Hello", " ", "World"} = "Hello World". Comparison uses the standard relational operators (==, !=, <, >) with lexicographic ordering. The built-in methods cover the most common string operations — length, substring, case conversion, character access, and type conversion from/to numeric types.

Dynamic length

Grows and shrinks automatically. No fixed buffer. No overflow. Initializes to "" (empty). len() returns current character count.

Indexing → byte

s[i] returns the ASCII code of character i as a byte. Use substr(i,i) for a single-char string. Index is 0-based.

Concatenation with {}

Same syntax as bit concatenation: {"first", "second"}. Or use $sformatf for formatted strings with values.

Comparison operators

==, !=, <, > work lexicographically. Case-sensitive: "ABC" ≠ "abc". Use icompare() for case-insensitive.

Complete Method Reference

SystemVerilog — String Type Syntax & Methods
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Declaration ──────────────────────────────────────────────────
string s      = "Hello";         // initialized
string empty;                    // = "" (empty, not null)
string path   = "/top/dut/fifo";
 
// ── Concatenation ─────────────────────────────────────────────────
string full   = {s, " World"};  // "Hello World"
string padded = {"[", s, "]"};   // "[Hello]"
 
// ── $sformatf: format a string (returns string) ───────────────────
string msg = $sformatf("TID=%0d addr=0x%08h", 42, 32'hCAFE);
 
// ── len() — character count ───────────────────────────────────────
int n = s.len();              // 5 for "Hello"
 
// ── getc() / putc() — character access by index ──────────────────
byte ch  = s.getc(0);         // 72 = ASCII 'H'
s.putc(0, 8'h68);              // replaces s[0] with 'h': "hello"
 
// ── substr(i, j) — extract substring [i..j] inclusive ─────────────
string sub = s.substr(1, 3);  // "ell" (from "Hello")
string ch_s = s.substr(0, 0); // "H" — single character as string
 
// ── toupper() / tolower() — case conversion ───────────────────────
string up  = s.toupper();      // "HELLO"
string low = s.tolower();      // "hello"
 
// ── compare() / icompare() — comparison ──────────────────────────
int cmp = s.compare("Hello");  // 0 = equal, <0 = less, >0 = greater
int icmp = s.icompare("HELLO"); // 0 — case-insensitive equal
 
// ── atoi() / atohex() / atoreal() — string → number ──────────────
string num_s = "42";
int    num   = num_s.atoi();    // 42 (decimal)
string hex_s = "FF";
int    hex   = hex_s.atohex(); // 255 (hexadecimal)
string rl_s  = "3.14";
real   rl    = rl_s.atoreal(); // 3.14
 
// ── itoa() / hextoa() / realtoa() — number → string ──────────────
string s_int;
s_int.itoa(255);               // s_int = "255"
s_int.hextoa(255);             // s_int = "ff"
string s_real;
s_real.realtoa(3.14);          // s_real = "3.14"
 
// ── Comparison operators (lexicographic, case-sensitive) ──────────
"abc" == "abc"    // 1
"abc" != "ABC"    // 1 — case-sensitive
"abc" <  "abd"    // 1 — lexicographic: c < d
"B"   <  "a"      // 1 — 'B'=66, 'a'=97, uppercase < lowercase in ASCII
MethodSignatureReturnsExample
len()int len()Character count"Hello".len() = 5
getc(i)byte getc(int i)ASCII byte at index i"Hi".getc(0) = 72
putc(i,c)void putc(int i, byte c)void — modifies in placeReplace char at position i
substr(i,j)string substr(int i, j)Substring from i to j inclusive"Hello".substr(1,3) = "ell"
toupper()string toupper()Uppercase copy"abc".toupper() = "ABC"
tolower()string tolower()Lowercase copy"ABC".tolower() = "abc"
compare(s)int compare(string s)0/negative/positive0 = equal, case-sensitive
icompare(s)int icompare(string s)0/negative/positive0 = equal, case-insensitive
atoi()int atoi()Decimal integer"42".atoi() = 42
atohex()int atohex()Hex integer"FF".atohex() = 255
atobin()int atobin()Binary integer"1010".atobin() = 10
atoreal()real atoreal()Real (floating point)"3.14".atoreal() = 3.14
itoa(n)void itoa(int n)void — modifies strings.itoa(255) → s="255"
hextoa(n)void hextoa(int n)void — modifies strings.hextoa(255) → s="ff"
realtoa(r)void realtoa(real r)void — modifies strings.realtoa(3.14) → s="3.14"

Visual — String Indexing, Substring, and Case

String Indexing and Character Positions

String: s = "VLSI"

IndexCharacters.getc(i)s[i]s.substr(i,i)
0V86 (ASCII)86 (byte)"V" (string)
1L7676"L"
2S8383"S"
3I7373"I"
s.len()= 4 (total characters)

Comparison Behavior

ExpressionResultReason
"abc" == "abc"1Identical strings
"abc" == "ABC"0Case-sensitive: 'a'(97) ≠ 'A'(65)
"abc".icompare("ABC")0Case-insensitive: equal
"abc" < "abd"1Lexicographic: 'c'(99) below 'd'(100)
"B" < "a"1ASCII: 'B'=66 below 'a'=97 — every uppercase letter sorts before every lowercase one
"10" < "9"1Lexicographic, not numeric: '1'(49) below '9'(57)
"10".atoi() > "9".atoi()1Numeric comparison: 10 above 9

Code Examples — Basic Operations to UVM Log Generation

Example 1 — Beginner: All String Methods

Example 1 — String Methods Reference
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_string_basics;
 
  string s = "SystemVerilog";
  string result;
  int    n;
 
  initial begin
 
    // ── Length ────────────────────────────────────────────────────
    $display("len = %0d", s.len());              // 13
 
    // ── Substring ─────────────────────────────────────────────────
    $display("[0,5]  = %s", s.substr(0, 5));    // System
    $display("[6,12] = %s", s.substr(6, 12));   // Verilog
 
    // ── Case conversion ────────────────────────────────────────────
    $display("upper = %s", s.toupper());        // SYSTEMVERILOG
    $display("lower = %s", s.tolower());        // systemverilog
 
    // ── getc / putc ────────────────────────────────────────────────
    $display("s[0] = %0d (ASCII)", s.getc(0));   // 83 = 'S'
    s.putc(0, "s");                              // replace 'S' with 's'
    $display("after putc: %s", s);               // systemVerilog
    s = "SystemVerilog";                          // reset
 
    // ── compare / icompare ─────────────────────────────────────────
    $display("compare(same): %0d",     s.compare("SystemVerilog"));  // 0
    $display("compare(diff case): %0d", s.compare("systemverilog"));  // non-zero
    $display("icompare(same): %0d",    s.icompare("SYSTEMVERILOG")); // 0
 
    // ── $sformatf — build formatted strings ───────────────────────
    string msg = $sformatf("TXN[%0d]: addr=0x%08h data=0x%04h",
                           42, 32'hA000_1234, 16'hABCD);
    $display("%s", msg);
 
    // ── Numeric conversions ────────────────────────────────────────
    string ns = "255";
    $display("atoi:   %0d", ns.atoi());    // 255
    ns = "FF";
    $display("atohex: %0d", ns.atohex()); // 255
    ns = "1010";
    $display("atobin: %0d", ns.atobin()); // 10
 
    string out;
    out.itoa(255);   $display("itoa:   %s", out);   // 255
    out.hextoa(255); $display("hextoa: %s", out);   // ff
 
    $finish;
  end
 
endmodule

Example 2 — Intermediate: Parsing and Building Protocol Messages

Example 2 — String Parsing and Message Building
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_string_parsing;
 
  // Build a structured log entry string
  function automatic string format_txn(
    input int           tid,
    input logic [31:0] addr,
    input logic [31:0] data,
    input bit           is_write
  );
    string op = is_write ? "WR" : "RD";
    return $sformatf("[%s] TID=%03d ADDR=0x%08h DATA=0x%08h",
                     op, tid, addr, data);
  endfunction
 
  // Check if a component path contains a specific substring
  function automatic bit path_contains(string full_path, sub);
    // Manual substring search — find sub in full_path
    int sub_len  = sub.len();
    int path_len = full_path.len();
    for (int i = 0; i <= path_len - sub_len; i++) begin
      if (full_path.substr(i, i + sub_len - 1) == sub)
        return 1;
    end
    return 0;
  endfunction
 
  // Extract test name from command-line style argument "test=my_test"
  function automatic string parse_arg(string arg);
    for (int i = 0; i < arg.len(); i++) begin
      if (arg.getc(i) == "=")
        return arg.substr(i+1, arg.len()-1);
    end
    return arg;   // no '=' found, return original
  endfunction
 
  initial begin
    $display("%s", format_txn(7, 32'hA000_0100, 32'hCAFE_BABE, 1));
    $display("%s", format_txn(8, 32'hB000_0200, 32'h1234_5678, 0));
 
    $display("path has 'fifo': %0b", path_contains("/top/dut/fifo/ctrl", "fifo"));
    $display("path has 'sram': %0b", path_contains("/top/dut/fifo/ctrl", "sram"));
 
    $display("test name: %s", parse_arg("test=axi_burst_test"));
 
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[WR] TID=007 ADDR=0xA0000100 DATA=0xCAFEBABE
[RD] TID=008 ADDR=0xB0000200 DATA=0x12345678
path has 'fifo': 1
path has 'sram': 0
test name: axi_burst_test

Example 3 — Verification: UVM-Style Error Reporter

Example 3 — UVM-Style Message Generation
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class axi_scoreboard;
 
  string  comp_name  = "axi_sb";
  int     check_cnt  = 0;
  int     fail_cnt   = 0;
 
  function automatic string make_banner(string title);
    string line = "================================";
    return {"\n", line, "\n  ", title, "\n", line};
  endfunction
 
  task automatic check(
    input logic [31:0] exp, got,
    input string        context_str = ""
  );
    string loc;
    check_cnt++;
 
    // Build location tag from component and context
    loc = (context_str == "") ?
          $sformatf("[%s]", comp_name) :
          $sformatf("[%s::%s]", comp_name, context_str);
 
    if (^got === 1'bX) begin
      $error("%s X-VALUE: got contains X — DUT not reset?", loc);
      fail_cnt++;
    end else if (exp !== got) begin
      $error("%s MISMATCH: expected=0x%08h got=0x%08h", loc, exp, got);
      fail_cnt++;
    end else
      $display("%s PASS: 0x%08h", loc, got);
  endtask
 
  function void report();
    string status = (fail_cnt == 0) ? "PASSED" : "FAILED";
    $display("%s", make_banner($sformatf("%s Test %s",
             comp_name.toupper(), status)));
    $display("  Checks: %0d  Fail: %0d", check_cnt, fail_cnt);
  endfunction
 
endclass
 
module tb_sb_demo;
  initial begin
    axi_scoreboard sb = new();
    sb.check(32'hAABB, 32'hAABB, "beat0");
    sb.check(32'hCCDD, 32'hCCEE, "beat1");
    sb.report();
    $finish;
  end
endmodule

Example 4 — Corner Case: Indexing, Empty String, Numeric Parsing

Example 4 — Edge Cases
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_string_corners;
 
  initial begin
 
    // ── s[i] vs getc(i) vs substr(i,i) ───────────────────────────
    string s = "ABC";
    $display("s[0]         = %0d (byte/ASCII)",  s[0]);           // 65
    $display("s.getc(0)    = %0d (byte/ASCII)",  s.getc(0));       // 65
    $display("s.substr(0,0)= %s (string char)", s.substr(0,0));  // A
 
    // ── Empty string ─────────────────────────────────────────────
    string empty;
    $display("empty.len()  = %0d", empty.len());  // 0
    $display("empty == '' : %0b", empty == "");   // 1
 
    // ── Lexicographic vs numeric comparison ───────────────────────
    $display("'10' < '9' (string): %0b",   "10" < "9");             // 1 — trap!
    $display("10 > 9 (integer):    %0b",   "10".atoi() > "9".atoi()); // 1 — correct
 
    // ── $sformatf vs itoa ─────────────────────────────────────────
    string s1 = $sformatf("%0d", 42);    // "42" — formatted
    string s2; s2.itoa(42);              // "42" — same result
    $display("sformatf: '%s'  itoa: '%s'", s1, s2);  // same
 
    // ── Out-of-range index: tool-dependent behavior ───────────────
    // s.getc(100) on "ABC" — most tools return 0 or error
    // Always guard: if (i < s.len()) before accessing
 
    // ── Concatenation with {} ─────────────────────────────────────
    string prefix = "AXI";
    string suffix; suffix.itoa(3);
    string full = {prefix, "_", suffix};   // "AXI_3"
    $display("concat: %s", full);
 
    $finish;
  end
 
endmodule

Simulation Behavior — Storage, Scope, and $sformatf

How the Simulator Stores Strings

Internally, the simulator maintains a dynamic character buffer for each string variable. Assignment copies the content, not a reference — s2 = s1 gives s2 its own independent copy. Modifying s2 does not affect s1. Strings are value types, not reference types — unlike class handles in SV.

$sformatf vs $sformat

Two ways to build formatted strings: $sformatf returns the formatted string as a function return value — cleaner and more flexible. $sformat(var, format, args) writes the result into var as a task — the older Verilog-style form. In modern SV, always use $sformatf. You can assign it directly, pass it to a function, or use it inline in a $display call.

OperationBehaviorNote
s2 = s1Deep copy — independent storageStrings are value types
s = ""Clears string to emptylen() becomes 0
s[i]Returns byte (ASCII code)NOT a single-char string
Out-of-bounds indexTool-dependent: 0 or an errorGuard with if (i < s.len())
$sformatfReturns string (function)Preferred in SV
$sformat(var,...)Writes to var (task)Legacy Verilog style

Where Strings Show Up in Real Verification

Verification Patterns Using string
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 1. UVM COMPONENT NAMING ───────────────────────────────────────
// All UVM components have a string name and hierarchical path
// get_full_name() → "/top/env/axi_agent/driver"
// Filtering by path prefix/substring is common in debug
 
// ── 2. SCOREBOARD MISMATCH MESSAGES ──────────────────────────────
function automatic string mismatch_msg(
  string field, logic[31:0] exp, got);
  return $sformatf("%s: expected=0x%08h got=0x%08h diff=0x%08h",
                   field, exp, got, exp ^ got);
endfunction
 
// ── 3. PLUSARG / TEST CONFIGURATION ──────────────────────────────
string test_name = "default_test";
initial begin
  if ($value$plusargs("TEST=%s", test_name))
    $display("Running test: %s", test_name);
end
 
// ── 4. REGISTER FIELD NAMES IN RAL ───────────────────────────────
string field_names [8] = '{"STATUS", "CTRL", "IRQ", "MASK",
                            "DATA0", "DATA1", "ADDR", "ID"};
// Access by name: field_names[reg_index].toupper() for display
 
// ── 5. ASSOCIATIVE ARRAY WITH STRING KEYS ────────────────────────
int    opcode_hits [string];   // count by opcode name
opcode_hits["AXI_WRITE"]++;
opcode_hits["AXI_READ"]++;
// foreach (opcode_hits[name])
//   $display("%-15s: %0d hits", name, opcode_hits[name]);
 
// ── 6. CSV REPORT GENERATION ─────────────────────────────────────
function automatic string to_csv(string name, int val, pct);
  return $sformatf("%s,%0d,%0d%%", name, val, pct);
endfunction
// Write to file: $fdisplay(fd, to_csv("AXI_WR", 1234, 75));

The Synthesis Boundary — Where string Stops

Nothing else on this page matters as much as this section, because it is the one that decides whether the material can be applied where it will do damage.

Treat string as a simulation and testbench construct. It is not portable synthesizable RTL.

The reason is structural rather than a tool limitation to be worked around. A string's defining property is dynamic storage: its length changes at run time, and the simulator maintains a per-variable buffer to hold it. There is no hardware equivalent of a variable-length object — a bus has a fixed number of wires and a register a fixed number of flops. A synthesis tool has nothing to build.

That is a statement about portable RTL, and it is worth being precise rather than absolute. Some tools accept string literals in restricted, elaboration-time positions — a parameter holding a file name for $readmemh, a tool attribute, a $display inside code that never reaches synthesis. Those are constants consumed during elaboration, not signals. Relying on any of them is a portability decision to make deliberately and document, not a default to assume.

What to use instead

When hardware genuinely needs fixed textual data — a state tag in a debug register, an ASCII magic number in a header, a four-character ID field — use a packed vector of fixed width. A string literal assigned to one is a compile-time constant with a known width, which is an entirely different construct from a string variable:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── SIMULATION ONLY ────────────────────────────────────────────────
string state_name = "IDLE";        // dynamic length, simulator-managed
$display("state = %s", state_name);
 
// ── SYNTHESIZABLE EQUIVALENT ───────────────────────────────────────
// A string LITERAL assigned to a packed vector is a sized constant.
// "IDLE" is four bytes -> 32 bits, MSB-first: 'I' 'D' 'L' 'E'.
localparam logic [31:0] TAG_IDLE = "IDLE";   // 32'h49444C45
localparam logic [31:0] TAG_BUSY = "BUSY";   // 32'h42555359
 
logic [31:0] tag_q;
always_ff @(posedge clk or negedge rst_n)
  if      (!rst_n)   tag_q <= TAG_IDLE;
  else if (start)    tag_q <= TAG_BUSY;
  else if (done)     tag_q <= TAG_IDLE;

Two details worth knowing about that conversion. A string literal assigned to a narrower packed vector is truncated from the left, keeping the rightmost characters — so a five-character literal in a 32-bit parameter silently loses its first character. And a literal assigned to a wider vector is zero-padded on the left, which is why "IDLE" in a logic [63:0] is 64'h0000_0000_4944_4C45 rather than left-justified.

For anything longer than a few characters, a packed byte array (see Fixed-Size Arrays) indexed by position, or a small ROM initialised with $readmemh, is the right structure. The pattern to avoid is trying to make a variable-length thing exist in hardware.

The boundary in one table

ConstructSimulationSynthesisNote
string s; (variable)Dynamic length — no hardware equivalent
s.len(), substr(), atoi()Methods on a dynamic object
$sformatf, $displayStripped before synthesis; never on a decision path
localparam logic [31:0] T = "IDLE";A sized constant, not a string variable
logic [7:0] msg [0:15]; initialised from a file⚠️ tool-dependentA memory; $readmemh support for synthesis varies

Bugs Engineers Hit With Strings

Bug 1 — s[i] Returns Byte, Not Character String

Bug 1 — Indexing Returns byte, Not String
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
string s = "PASS";
 
// BUGGY: expecting a string comparison, getting a byte comparison
if (s[0] == "P")              // s[0]=80 (byte), "P"=80 in ASCII — actually works!
  $display("starts with P");  // BUT: this is byte vs byte comparison, not string vs string
 
// The real trap: trying to use s[i] as a string fails type check
string first = s[0];           // COMPILE ERROR: cannot assign byte to string
 
// CORRECT: use substr for single-character string access
string first_ok = s.substr(0, 0);  // "P" — proper single-char string
if (first_ok == "P")
  $display("correct string comparison");

Bug 2 — Case-Sensitive == Misses Matching Strings

Bug 2 — Case-Sensitive String Comparison
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
string test_name;
$value$plusargs("TEST=%s", test_name);   // user types: +TEST=AXI_TEST
 
// BUGGY: case-sensitive comparison — fails if user typed "axi_test" or "Axi_Test"
if (test_name == "AXI_TEST")
  $display("running AXI test");      // only matches exact "AXI_TEST"
 
// CORRECT option 1: normalize to uppercase before comparison
if (test_name.toupper() == "AXI_TEST")
  $display("running AXI test");      // matches AXI_TEST, axi_test, Axi_Test
 
// CORRECT option 2: use icompare() for case-insensitive
if (test_name.icompare("AXI_TEST") == 0)
  $display("running AXI test");

Bug 3 — Lexicographic Ordering Breaks Version-Number Comparison

Bug 3 — Numeric Strings Sort Wrongly Lexicographically
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
string test_ids [$] = '{"test_9", "test_10", "test_2"};
test_ids.sort();
$display("Sorted: %p", test_ids);
// Prints: "test_10", "test_2", "test_9"
// Lexicographic: '1'(49) < '2'(50) < '9'(57) → 10 sorts before 2!
 
// For numeric sort of test names, extract the number and sort by int
function automatic int get_num(string name);
  // Find the underscore and get everything after it
  for (int i = 0; i < name.len(); i++)
    if (name.getc(i) == "_")
      return name.substr(i+1, name.len()-1).atoi();
  return 0;
endfunction
// Now sort by get_num(): test_2, test_9, test_10 — correct

Bug 4 — Forgetting That itoa() Modifies the String In-Place

Bug 4 — itoa() Is a void Task, Not a Function
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// BUGGY: treating itoa like a function that returns a string
string s;
if (s.itoa(42) == "42")   // COMPILE ERROR: itoa returns void, not string
  $display("match");
 
// CORRECT: call itoa, then use the string
s.itoa(42);              // modifies s to "42"
if (s == "42")
  $display("match");    // works
 
// PREFERRED: use $sformatf instead — cleaner, works anywhere in an expression
string s2 = $sformatf("%0d", 42);   // "42" — directly usable in expression
if (s2 == "42") $display("match");

Proving It — Value Semantics, Mutation, and the Comparison Traps

The string type's surprises are all consequences of two facts: it is a value with dynamic storage, and its comparisons are textual. Both are checkable.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// string_semantics_proof.sv
//
// Self-checking proof of: copy-by-value assignment, which methods mutate,
// indexing returning a byte, and lexicographic vs numeric comparison.
module string_semantics_proof;
 
  int errors = 0;
 
  task automatic chk_s(string name, string got, string exp);
    if (got != exp) begin
      errors++;
      $display("FAIL %-46s got=\"%s\" exp=\"%s\"", name, got, exp);
    end else
      $display("pass %-46s = \"%s\"", name, got);
  endtask
 
  task automatic chk_i(string name, int got, int exp);
    if (got != exp) begin
      errors++;
      $display("FAIL %-46s got=%0d exp=%0d", name, got, exp);
    end else
      $display("pass %-46s = %0d", name, got);
  endtask
 
  initial begin
    // ================================================================
    // 1. ASSIGNMENT COPIES THE CONTENT, NOT A REFERENCE
    // ================================================================
    begin
      string s1 = "hello";
      string s2;
      s2 = s1;                       // an independent copy
      s2 = {s2, " world"};           // mutate the copy
      chk_s("s1 after s2 was modified", s1, "hello");
      chk_s("s2",                       s2, "hello world");
    end
 
    // ================================================================
    // 2. NO NULL TERMINATOR, NO FIXED BUFFER, EMPTY IS ""
    // ================================================================
    begin
      string e;                      // default-initialised
      chk_s("default string is empty", e,       "");
      chk_i("empty len() is 0",        e.len(), 0);
 
      e = "abc";
      chk_i("len() is the character count", e.len(), 3);
      e = {e, "defghij"};            // grows, no overflow to guard
      chk_i("len() after growth",           e.len(), 10);
    end
 
    // ================================================================
    // 3. INDEXING RETURNS A byte, NOT A ONE-CHARACTER STRING
    // ================================================================
    begin
      string s = "Hello";
      byte   b;
      string one;
 
      b   = s[0];                    // a byte: the ASCII code
      one = s.substr(0, 0);          // a one-character STRING
 
      chk_i("s[0] is a byte (ASCII 'H' = 72)", int'(b), 72);
      chk_s("s.substr(0,0) is a string",       one,     "H");
      chk_i("last index is len()-1",  int'(s[s.len()-1]), 111);  // 'o'
    end
 
    // ================================================================
    // 4. WHICH METHODS MUTATE
    // ================================================================
    begin
      string s = "abc";
      string up;
 
      up = s.toupper();              // RETURNS a copy
      chk_s("toupper() returns a copy", up, "ABC");
      chk_s("...original unchanged",    s,  "abc");
 
      s.itoa(42);                    // MUTATES in place, returns nothing
      chk_s("itoa() replaced the string", s, "42");
      chk_i("atoi() reads back the value", s.atoi(), 42);
    end
 
    // ================================================================
    // 5. COMPARISON IS TEXTUAL - lexicographic, case-sensitive
    // ================================================================
    begin
      chk_i("\"10\" < \"9\"   is TRUE (lexicographic)", int'("10" < "9"),  1);
      chk_i("10 < 9        is FALSE (numeric)",         int'(10 < 9),      0);
      chk_i("\"10\".atoi() > \"9\".atoi()",             int'("10".atoi() > "9".atoi()), 1);
 
      chk_i("\"RESET\" == \"reset\"  is FALSE",  int'(string'("RESET") == "reset"), 0);
      chk_i("case-normalised comparison is TRUE",
            int'(string'("RESET").tolower() == "reset"), 1);
 
      chk_i("\"Zebra\" < \"apple\"  is TRUE ('Z'=90 < 'a'=97)",
            int'("Zebra" < "apple"), 1);
    end
 
    if (errors == 0) $display("\nstring_semantics_proof: ALL CHECKS PASSED");
    else             $display("\nstring_semantics_proof: %0d FAILURES", errors);
    $finish;
  end
endmodule

Section 5's first two checks are the pair worth keeping side by side: "10" < "9" is true and 10 < 9 is false. Same digits, opposite answers, because one comparison is about text and the other about number.

A string variable is a simulator-managed dynamic object with methods and no hardware equivalent, while a string literal assigned to a packed vector is a fixed-width compile-time constant that synthesises, truncating from the left when narrow and zero-padding on the left when wide.string s = "IDLE";variable, dynamic lengthlocalparam [31:0] T ="IDLE";literal, fixed widthSimulator-managedbufferlen, substr, atoiCompile-time constant32'h49444C45Simulation onlyno hardware equivalentSynthesizableleft-truncate, left-pad12
Figure 1 - a dynamic string against a packed vector, and where the boundary falls. A string variable is a simulator-managed object whose length changes at run time, supporting methods such as len and substr, and it has no hardware equivalent. A string literal assigned to a packed vector is a different construct entirely: a fixed-width compile-time constant that synthesises like any other parameter, truncated from the left if the vector is too narrow and zero-padded on the left if it is wider.
5

A scoreboard compared formatted strings and passed on an all-X result

COMPARED-TEXT-NOT-VALUES
Buggy Code & Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: the comparison was built out of the message-formatting code,
//         because the message was written first and the check reused it.
class reg_scoreboard extends uvm_scoreboard;
 
  function void check(logic [31:0] got, logic [31:0] exp);
    string got_s = $sformatf("%0d", got);
    string exp_s = $sformatf("%0d", exp);
 
    if (got_s == exp_s)                        // ❌ compares TEXT
      `uvm_info("SB", $sformatf("match: %s", got_s), UVM_HIGH)
    else
      `uvm_error("SB", $sformatf("mismatch: got %s exp %s", got_s, exp_s))
  endfunction
 
endclass
 
// %0d renders an all-X value as "0". So when the DUT drives X and the
// expected value is 0, the two strings are identical and the check passes:
//   got = 32'hxxxx_xxxx  ->  "0"
//   exp = 32'd0          ->  "0"
//   "0" == "0"           ->  MATCH
 
// ✅ FIX: compare the values. Use the formatting only for the message.
function void check(logic [31:0] got, logic [31:0] exp);
  if (got === exp)                             // ✅ compares VALUES
    `uvm_info("SB", $sformatf("match: %0h", got), UVM_HIGH)
  else
    `uvm_error("SB", $sformatf("mismatch: got %0h exp %0h", got, exp))
endfunction
Symptom

A register scoreboard reported clean while a configuration register read back as all-X on one access path. The failure was found by a directed test written months later that happened to print the raw value.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
transaction 8,214   register CFG_CTRL
  got  = 32'hxxxx_xxxx
  exp  = 32'h0000_0000
  scoreboard: MATCH
 
the same data, printed two ways:
  $display("%0d", got)  ->  0          <-- what the scoreboard compared
  $display("%0h", got)  ->  xxxxxxxx   <-- what was actually there

The scoreboard had been running for four months. Its match rate was 100%, and the register in question was read on every test.

Root Cause

The check compared formatted text instead of values, and %0d renders an unknown value as 0.

That single formatting behaviour is the whole bug. $sformatf("%0d", 32'hxxxx_xxxx) produces the string "0", indistinguishable from the rendering of a genuine zero. The comparison was then perfectly correct — the two strings really were equal — and reported a match on data that was entirely unknown.

The general defect is broader than the X case, and it is worth stating in full: converting values to text before comparing them inserts every formatting decision into the verdict. Field width, leading zeros, signed versus unsigned rendering, and the treatment of X and Z all become part of what "equal" means. Two different values can produce identical strings; one value can produce different strings under different format specifiers.

How it came to be written is the ordinary way. The error message was written first, using $sformatf — which is exactly right, because a message is for a human. The check was then built from the same strings because they were already there. The reuse looks like tidiness and is the defect.

Two things kept it alive. A 100% match rate looks like a passing scoreboard, so nothing prompted an investigation. And %0d is the natural format for a register value in a log, so the formatting choice that caused it was never suspicious.

Fix

Compare values; format only for the message. The two concerns run in opposite directions and should not share code.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 1. The comparison uses ===, which never returns X and reports an
//    unknown value as the mismatch it is.
if (got === exp) ...
 
// 2. Screen for X explicitly, so an all-X read is reported as its own
//    failure rather than folded into a value mismatch. They are
//    different bugs and deserve different messages.
if ($isunknown(got))
  `uvm_error("SB", $sformatf("DUT drove X on %s: %0h", reg_name, got))
else if (got !== exp)
  `uvm_error("SB", $sformatf("mismatch on %s: got %0h exp %0h",
                             reg_name, got, exp))
 
// 3. Format with %0h rather than %0d anywhere an X could appear. Hex
//    renders X as x; decimal renders it as 0.

Three habits, and the second is the one that generalises furthest:

  • Format for humans, compare for machines. $sformatf builds messages. It never appears on the path that decides pass or fail.
  • Never reuse the message expression as the check expression. They have opposite requirements — a message should be readable and lossy, a check should be exact and lossless — and code that satisfies one will quietly fail the other.
  • Use %0h while debugging anything that might be unknown. %0d renders X as 0 and Z as 0, which is the single most effective way to hide the state you are looking for. This is the same trap as an uninitialised integer counter printing as zero — see Arithmetic Operators.

Interview Questions

Best Practices & Coding Guidelines

Use $sformatf for dynamic messages

Any message with embedded values should use $sformatf. It's cleaner than concatenating itoa() calls and supports all format specifiers natively.

Guard index access

Always check i < s.len() before s[i] or s.getc(i). Out-of-bounds behavior is tool-dependent and not standardized.

Normalize for comparison

When comparing user input, test names, or command-line arguments: normalize to uppercase with toupper() or use icompare(). Never assume the user typed the right case.

Use itoa sparingly — prefer $sformatf

itoa(), hextoa() etc. are void tasks that modify in-place. $sformatf returns a string and works in expressions. It's more flexible and clearer in intent.

TaskPreferredAvoid / Watch Out
Build formatted string$sformatf("fmt", val)s.itoa(val) — void, modifies in-place
Get single characters.substr(i, i) (string) or s.getc(i) (byte)s[i] — returns byte, not string
Case-insensitive compares.icompare(t) == 0s == t — case-sensitive
Numeric sort of string numbersSort by atoi() valueString sort() — lexicographic order
Check non-empty strings.len() > 0 or s != ""No implicit bool conversion in SV

Summary

The string type handles dynamic text in testbenches, UVM message generation, test configuration, and CSV report output. The full method set covers everything you need for practical verification work. Three things to drill into muscle memory: s[i] gives you a byte (ASCII), not a string; == is case-sensitive; and numeric strings don't sort numerically with sort(). For building messages, $sformatf is almost always the right tool.

  • string is dynamic — no fixed buffer, no overflow. Initializes to "". Assignment is a deep copy.
  • s[i] returns byte (ASCII code), not a string. Use substr(i,i) for single-char strings.
  • == is case-sensitive. Use icompare() or normalize with toupper() when case doesn't matter.
  • String sort is lexicographic, so "10" sorts before "2". For numeric strings, sort by atoi() value instead.
  • $sformatf is the best tool for building dynamic strings. It returns a string directly, supports all format specifiers, and works inline in any expression.

What a string is not. Fixed-Size Arrays covers packed versus unpacked dimensions and the packed vectors that are the synthesizable alternative for fixed text. Dynamic Arrays and Queues are the other dynamically-sized types, and share string's simulation-only status for the same reason. Integer Types for the byte that indexing returns.

Where strings meet comparison and formatting. Relational & Equality Operators — the === that should be doing the comparison in Debug Lab 5, and why %0d rendering X as 0 is the same class of hazard as != returning X. Arithmetic Operators for the int versus integer counter that prints as zero for the same reason. For building formatted text in UVM reporting, UVM Verbosity Control.

References.

  • IEEE 1800 (SystemVerilog) — the string data type and its built-in methods (len, substr, getc, putc, toupper, tolower, compare, icompare, atoi, atohex, itoa and the rest) are defined in the data-types clause. The properties this page relies on are specified there: a string is dynamically sized, has no null terminator, initialises to the empty string, assignment copies the value, and indexing yields a byte. The rules for converting between string literals and packed vectors — left-truncation when narrower, left zero-padding when wider — are in the same clause.
  • IEEE 1364.1 (Verilog RTL Synthesis) — the synthesizable subset, which contains no dynamically-sized types.

Requirement versus practice. The type's semantics above are language requirements. The synthesis boundary is engineering practice with a structural reason: IEEE 1800 defines the type without saying what any tool must synthesize, and a dynamically-sized object has no hardware realisation, so treating string as simulation-only is the portable position rather than a rule the standard states. Individual tools may accept string literals in restricted elaboration-time positions; relying on that is a portability decision to document, not a default. The recommendations — compare values rather than formatted text, use %0h when X is possible, and pad numeric text that will be sorted — are likewise practice.

Part of SystemVerilog Fundamentals·Data Types·Lesson 8 of 53

View program

Continue learning