Skip to content

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
// ── 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) < 'd'(100)
"B" < "a"1ASCII: 'B'=66 < 'a'=97 (uppercase < lowercase)
"10" < "9"1Lexicographic, not numeric: '1'(49) < '9'(57)
"10".atoi() > "9".atoi()1Numeric comparison: 10 > 9

Code Examples — Basic Operations to UVM Log Generation

Example 1 — Beginner: All String Methods

Example 1 — String Methods Reference
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
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
[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
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
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 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
// ── 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));

Bugs Engineers Hit With Strings

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

Bug 1 — Indexing Returns byte, Not String
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
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 "10" < "9" When Comparing Version Numbers

Bug 3 — Numeric Strings Sort Wrongly Lexicographically
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
// 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");

Interview Questions

Beginner Level

Q1: What does s[0] return for a string variable? How do you get a single-character string?s[0] returns a byte containing the ASCII code of the first character — not a string. For s = "Hello", s[0] returns 72 (ASCII code of 'H'). To get a single-character string, use s.substr(0, 0) which returns "H". You can also use s.getc(0) to explicitly get the byte, making the intent clear. Q2: How do you build a formatted string with integer and hex values in SystemVerilog? Use $sformatf: string msg = $sformatf("TID=%0d addr=0x%08h", tid, addr);. It works exactly like $display but returns the formatted string instead of printing it. All format specifiers work: %0d, %h, %b, %f, %s, etc. Alternatively, use {str1, str2} concatenation for joining pre-built strings.

Intermediate Level

Q3: Is string comparison in SV case-sensitive? How do you do a case-insensitive comparison? Yes, the == operator on strings is case-sensitive — "ABC" ≠ "abc". For case-insensitive comparison, two options: (1) normalize both strings first with toupper() or tolower() before comparing, or (2) use the built-in icompare() method which returns 0 for case-insensitive equal. icompare() is cleaner and more explicit about intent.

Experienced Engineer Level

Q4: A queue of string test names is sorted using the built-in sort() method. The expected order is test_1, test_2, test_10, but the actual order is test_1, test_10, test_2. Why? String sort() uses lexicographic ordering — it compares character by character. '1' (ASCII 49) < '2' (ASCII 50), so "test_10" sorts before "test_2" because at position 6, '1' < '2'. This is the same behavior as sorting strings in any language without natural sort support. Fix: extract the numeric suffix with substr() and atoi(), then sort by the resulting integer: q.sort() with (item.substr(5, item.len()-1).atoi()).

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 — "10" < "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.