SystemVerilog String Type and Methods: A Practical Guide
In Verilog, text handling meant juggling fixed-width reg vectors and remembering that eight bits held one character. SystemVerilog adds a dedicated string data type: a dynamically sized, built-in type that grows and shrinks on its own and ships with a toolbox of methods for measuring, slicing, converting, and formatting text. This makes testbench messages, file parsing, and scoreboard logging far cleaner. This tutorial walks through the string type and its most-used methods with examples you can run yourself.
What the string type is
A systemverilog string is an ordered collection of bytes, each holding one ASCII character. Unlike a packed vector, you never declare a width. Declare one, assign a literal, and the type manages storage automatically. An unassigned string is the empty string "", not x.
- Characters are indexed from
0tolen()-1. - Indexing a single position (
s[i]) yields abyte— the character’s ASCII code. - Strings are compared, concatenated, and replicated with the same operators (
==,{,},{n{}}) you already use on vectors.
Length, indexing, substrings
The three operations you reach for most are len(), direct indexing, and substr(). len() returns the character count as an int. substr(i, j) returns the characters from position i through j inclusive — so substr(0, 3) returns four characters, not three. Concatenation builds a new string from pieces using braces.
module tb;
string tool;
string msg;
int width;
initial begin
tool = "ChipXpert";
width = tool.len(); // number of characters
$display("tool = %s", tool);
$display("length = %0d", width);
// Concatenation reuses the { , } syntax of packed values
msg = {"Welcome to ", tool, " labs"};
$display("msg = %s", msg);
// Index one character: tool[i] is a byte holding the ASCII code
$display("tool[0] = %0d (as char: %c)", tool[0], tool[0]);
$display("last = %c", tool[width-1]);
// substr(i, j) extracts characters i..j inclusive
$display("substr(0,3) = %s", tool.substr(0, 3));
$display("substr(4,8) = %s", tool.substr(4, 8));
$finish;
end
endmoduletool = ChipXpert length = 9 msg = Welcome to ChipXpert labs tool[0] = 67 (as char: C) last = t substr(0,3) = Chip substr(4,8) = Xpert /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp3c26ifnr/basics.sv:24: $finish called at 0 (1s)
Notice substr(0, 3) returns Chip (positions 0, 1, 2, 3) and tool[0] prints 67, the ASCII code for C. The %c format specifier renders that same byte as a character.
Converting between strings and numbers
Parsing text into numbers and back is everyday work when reading stimulus files or building register names. The conversion methods come in pairs:
| Method | Direction | Notes |
|---|---|---|
atoi() | string → int | Reads leading decimal digits; stops at the first non-digit. |
atohex() | string → int | Interprets characters as hexadecimal. |
atobin() / atooct() | string → int | Binary and octal variants. |
itoa(i) | int → string | Writes the decimal text into the string, in place. |
hextoa(i) | int → string | Writes the hexadecimal text, in place. |
The ato* methods return a value you assign, while itoa() and its siblings modify the string they are called on rather than returning a result.
module tb;
string field;
string num_txt;
string rebuilt;
int value;
initial begin
field = "speed=250";
// Pull the numeric part out with substr, then convert with atoi()
num_txt = field.substr(6, 8);
value = num_txt.atoi();
$display("field = %s", field);
$display("num_txt = %s", num_txt);
$display("value = %0d", value);
$display("doubled = %0d", value * 2);
// itoa() writes an integer into a string (modifies in place)
rebuilt.itoa(value * 2);
$display("rebuilt = %s (len %0d)", rebuilt, rebuilt.len());
// atohex() reads hex digits; handy for parsing register dumps
num_txt = "1a5";
value = num_txt.atohex();
$display("atohex(1a5) = %0d", value);
$finish;
end
endmodulefield = speed=250 num_txt = 250 value = 250 doubled = 500 rebuilt = 500 (len 3) atohex(1a5) = 421 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpu8hbgcfn/convert.sv:26: $finish called at 0 (1s)
A common trap: because itoa() mutates in place, writing s = t.itoa(5) is wrong — the method does not return the string. Call t.itoa(5) on its own line, then use t.
Case conversion and comparison
SystemVerilog also defines toupper() and tolower(), which return a new string with the alphabetic characters converted, leaving the original untouched. For ordering, compare() works like C’s strcmp (returns negative, zero, or positive), and icompare() does the same while ignoring case. Single characters are read with getc(i) and overwritten with putc(i, c).
These methods are part of IEEE 1800, but tool support varies — some simulators (including older Icarus Verilog builds) do not implement toupper, getc, putc, or compare. The block below is shown for reference rather than executed here:
module tb;
string name;
string loud;
initial begin
name = "ChipXpert";
loud = name.toupper(); // returns "CHIPXPERT"; name unchanged
$display("%s -> %s", name, loud);
$display("lower = %s", name.tolower());
// getc reads a byte, putc overwrites one position
$display("getc(0) = %c", name.getc(0));
name.putc(0, "c");
$display("after putc = %s", name);
// compare: 0 means equal; icompare ignores case
$display("compare = %0d", name.compare("cHIPXPERT"));
$display("icompare = %0d", name.icompare("CHIPXPERT"));
$finish;
end
endmoduleBuilding strings with $sformatf
The single most useful tool for readable testbenches is $sformatf. It takes the same format specifiers as $display but returns the formatted text as a string instead of printing it. That returned value can be stored, concatenated, passed to a function, or handed to a UVM reporting macro. This is how you build dynamic labels, register names, and log messages.
module tb;
string line;
int id;
logic [7:0] addr;
initial begin
id = 7;
addr = 8'hA5;
// $sformatf returns a formatted string instead of printing it
line = $sformatf("packet id=%0d addr=0x%02h", id, addr);
$display("line = %s", line);
$display("len = %0d", line.len());
// Build messages incrementally for logging or UVM-style reports
for (int i = 0; i < 3; i++) begin
string tag;
tag = $sformatf("[ch%0d]", i);
$display("%s processed at %0t", tag, $time);
end
// Width, zero-pad, and radix controls work just like $display
$display("%s", $sformatf("bin=%08b dec=%4d hex=%h", addr, id, addr));
$finish;
end
endmoduleline = packet id=7 addr=0xa5 len = 21 [ch0] processed at 0 [ch1] processed at 0 [ch2] processed at 0 bin=10100101 dec= 7 hex=a5 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpik2kiqna/format.sv:24: $finish called at 0 (1s)
The format specifiers behave exactly as in $display: %0d prints a decimal with no leading padding, %02h pads hex to two digits, and %08b pads binary to eight. Because $sformatf hands back a value, you can nest it directly inside another $display call, as the last line shows.
Quick reference
| Operation | Example | Result |
|---|---|---|
| Length | s.len() | Character count (int) |
| Index | s[0] | Byte / ASCII code |
| Substring | s.substr(0, 3) | Chars 0..3 inclusive |
| Concatenate | {a, b} | Joined string |
| To integer | s.atoi() | Parsed int |
| From integer | s.itoa(n) | Writes into s |
| Uppercase | s.toupper() | New uppercased string |
| Format | $sformatf("x=%0d", n) | Formatted string |
Practical tips
- Prefer
stringoverreg [N:0]for any text you print or compare — no width bookkeeping, no accidental truncation. - Remember
substrbounds are inclusive on both ends; off-by-one errors here are the most common string bug. - Methods that start with
atoreturn a value;itoa/hextoa/putcmutate in place and return nothing. - Use
$sformatfto assemble messages once, then reuse the string — cheaper and clearer than repeated inline formatting.
Quick check
How many characters does "data".substr(0, 2) return, and what are they?
Three characters: dat. The bounds are inclusive, so positions 0, 1, and 2 are returned.
What is the difference between atoi() and itoa()?
atoi() parses a string and returns an integer, so you assign its result. itoa(n) takes an integer and writes its decimal text into the string it is called on, modifying it in place and returning nothing.
Why use $sformatf instead of $display?
$display prints immediately and returns nothing. $sformatf returns the formatted text as a string, so you can store it, concatenate it, or pass it to a function or reporting macro.
If s = "7F", what does s.atohex() return?
127. The characters are interpreted as a hexadecimal number (0x7F = 127 decimal), unlike atoi() which would read them as decimal and stop at the non-digit.
Share your question in comments or talk to our mentor team for batch guidance.
Compile and simulate this in the browser — no install. Edit the code and re-run instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses