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 0 to len()-1.
  • Indexing a single position (s[i]) yields a byte — 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
endmodule
tool   = 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:

MethodDirectionNotes
atoi()string → intReads leading decimal digits; stops at the first non-digit.
atohex()string → intInterprets characters as hexadecimal.
atobin() / atooct()string → intBinary and octal variants.
itoa(i)int → stringWrites the decimal text into the string, in place.
hextoa(i)int → stringWrites 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
endmodule
field   = 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
endmodule

Building 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
endmodule
line = 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

OperationExampleResult
Lengths.len()Character count (int)
Indexs[0]Byte / ASCII code
Substrings.substr(0, 3)Chars 0..3 inclusive
Concatenate{a, b}Joined string
To integers.atoi()Parsed int
From integers.itoa(n)Writes into s
Uppercases.toupper()New uppercased string
Format$sformatf("x=%0d", n)Formatted string

Practical tips

  1. Prefer string over reg [N:0] for any text you print or compare — no width bookkeeping, no accidental truncation.
  2. Remember substr bounds are inclusive on both ends; off-by-one errors here are the most common string bug.
  3. Methods that start with ato return a value; itoa/hextoa/putc mutate in place and return nothing.
  4. Use $sformatf to 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.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Compile and simulate this in the browser — no install. Edit the code and re-run instantly.

Open in Verification Lab

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
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD