Skip to content

Verilog · Chapter 4.5 · Lexical Conventions

String Handling in Verilog

Verilog has no first-class string type. A string literal is really just packed ASCII bytes stored in a reg vector, eight bits per character, arranged most significant byte first, and an undersized destination silently loses the leading characters. This lesson covers the literal syntax, the escape sequences you can put inside a string, and how to compute the reg width needed to hold a given number of characters. It also walks through the format specifiers used by display and related system tasks, the rules for comparing and concatenating strings at the bit-vector level, and the silent-truncation trap that catches beginners. Finally it points out where SystemVerilog's dedicated string type changes this model and makes text handling far easier.

Foundation14 min readVerilogStringsASCIISyntax

Chapter 4 · Page 4.5 · Lexical Conventions

The Storage Model — Strings Are Reg Vectors

Pure Verilog (IEEE 1364) has no string type. What you write as a quoted string literal is a packed ASCII byte sequence — one 8-bit byte per character, packed MSB-first into a reg vector. The compiler computes the literal's width from the character count (×8) and either matches it to your declared reg width or truncates / zero-extends to fit.

storage-model.v — a string is a reg vector
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
reg [8*5-1:0] greet;          // 40-bit reg, room for 5 ASCII chars
initial greet = "Hello";      // packed: 'H' 'e' 'l' 'l' 'o' → 40'h_48_65_6C_6C_6F
 
reg [55:0] tag;               // 56 bits = 7 characters
initial tag = "VLSI-MR";      // 'V' 'L' 'S' 'I' '-' 'M' 'R'

There is no length field, no terminator. The string ends at the reg's MSB; padding (when the reg is wider than the literal) is zero bytes on the left.

The Literal Syntax

Per IEEE 1364-2005 §3.6:

  • A string literal is enclosed in double quotes "...". Single quotes are reserved for sized-literal bases.
  • A literal must fit on one source line — there is no multi-line string. Use \n to embed a newline, or concatenate via {} at the reg level.
  • A literal may not contain an unescaped " (use \") or an unescaped backslash at end of line.
syntax.v — one-line, double-quoted, escapes for the rest
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
"Hello, world"                       // ✅ standard
"Path: C:\\design\\rtl"              // ✅ backslash escaped
"He said \"go\""                     // ✅ embedded double-quote escaped
"line one\nline two"                 // ✅ embedded newline via escape
"line one
 line two"                            // ❌ illegal — literal multi-line

Escape Sequences

EscapeASCIIMeaning
\n0x0ANewline (LF)
\t0x09Horizontal tab
\r0x0DCarriage return
\\0x5CLiteral backslash
\"0x22Literal double-quote
\v0x0BVertical tab
\f0x0CForm feed
\a0x07Bell (alert)
\dddvariesOctal escape — 1 to 3 octal digits, value 000–377
%%0x25Literal % (in $display format strings only)
escapes.v — common escape patterns
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
reg [8*12-1:0] s_line   = "first\nsecond";       // 'f' 'i' 'r' 's' 't' 0x0A 's' 'e' 'c' 'o' 'n' 'd'
reg [8*8-1:0]  s_quote  = "say \"hi\"";          // s a y ' ' " h i "
reg [8*4-1:0]  s_octal  = "\101\102BC";          // \101 = 'A', \102 = 'B' → "ABBC"
reg [8*3-1:0]  s_back   = "C:\\";                // 'C' ':' '\\'

\% is not a Verilog string escape — it is a format-string token for $display / $write / $sformat. Inside a regular string literal, % is just the byte 0x25. Inside a format string, %% produces a literal %.

Octal Escapes (\ddd)

Three-digit octal escapes are the IEEE-blessed way to embed an arbitrary byte:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
"\012"     // 0x0A — same as \n
"\015"     // 0x0D — same as \r
"\033"     // 0x1B — ESC (terminal-control prefix; useful in $display)
"\377"     // 0xFF — maximum byte value

Octal escapes accept 1 to 3 octal digits. The lexer reads digits greedily until either three digits are consumed or a non-octal character is seen. Decimal and hex escapes (\xff, \d255) are not part of IEEE 1364 — only octal.

The Reg-Width Calculation

Sizing a reg for a known string is just N_chars * 8:

reg-width.v — sizing the destination
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 5 chars × 8 bits = 40 bits
reg [39:0] mode = "READY";
 
// Mnemonic: each ASCII char = 1 byte = 8 bits, MSB-first packing
// "READY" → 'R' 'E' 'A' 'D' 'Y' → 40'h_52_45_41_44_59
 
// 12 chars × 8 bits = 96 bits
reg [95:0] tag_long = "VLSI-MR-RTL ";

Best practice: use the [8*N-1:0] form to make the character count obvious:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
reg [8*5  - 1:0] mode    = "READY";       // 5 chars
reg [8*12 - 1:0] long    = "VLSI-MR-RTL ";  // 12 chars
reg [8*64 - 1:0] line_buf;                // a 64-character line buffer

The Silent-Truncation Trap

If the destination reg is narrower than the literal, Verilog drops the leftmost (high-order, first-character) bytes. This is the canonical Verilog string bug.

truncation.v — the leftmost-byte loss
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
reg [8*4-1:0] short;         // 4 chars = 32 bits
initial short = "Hello";     // 5 chars = 40 bits
                             // High 8 bits (the 'H') are silently dropped
                             // short ends up holding "ello"
 
reg [8*8-1:0] long_buf;
initial long_buf = "Hi";     // 2 chars = 16 bits
                             // Zero-pads the upper 48 bits
                             // long_buf = 64'h_0000_0000_0000_4869

The asymmetry — leftmost characters lost on truncation, zero-padding on the upper bits on extension — catches everyone the first time. Lint rules often flag the truncation as a width mismatch warning, but production code routinely silences width warnings, so the bug ships.

Format Specifiers in $display / $write / $sformat

Format strings are a separate parsing pass — % followed by a code consumes one argument:

SpecifierMeaning
%d / %0dDecimal (signed). %0d drops leading spaces.
%h / %0hHexadecimal. %0h drops leading zeros.
%b / %0bBinary.
%o / %0oOctal.
%cOne ASCII character (low 8 bits of the argument).
%sPrint the argument as a string — interprets each byte of the argument as ASCII.
%tTime, using the $timeformat setup.
%mHierarchical name of the calling scope (no argument consumed).
%%Literal %.
format.v — typical $display lines
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$display("MODE = %s, count = %0d, addr = %h", mode_reg, count, addr);
$display("State entered at t=%t",            $time);
$display("%m: assertion failed");
$display("%c", 8'h41);                       // prints "A"

%s reads its argument as bytes — eight bits at a time, MSB-first, stopping at the argument's width. If the argument is 64'h_0000_0000_4869_0000, %s prints "\0\0\0\0Hi\0\0" — note the leading and trailing NUL bytes, which most terminals render as blanks. Right-justify before printing if you want clean output.

String Comparison

Equality at the reg-vector level works only when both operands are the same width. If they differ, the narrower operand is zero-extended on its high bits — and zero bytes don't match the printable bytes of the wider string.

compare.v — width matters
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
reg [8*5-1:0] a = "READY";          // 40 bits
reg [8*5-1:0] b = "READY";          // 40 bits
if (a == b)  /* ✅ true */;
 
reg [8*6-1:0] c = "READY ";          // 48 bits — trailing space
if (a == c)  /* ❌ false — width mismatch, a zero-pads upper byte */;
 
// Right pattern: declare a fixed name-width and assign with explicit padding
reg [8*6-1:0] a6 = "READY ";         // 48 bits, padded
if (a6 == c) /* ✅ true */;

In real code, fix the width once (a "string slot" with the largest expected name plus padding) and assign everything against that fixed width. Comparison is just a bit-vector equality on the slot.

String Concatenation

Verilog string concatenation is bit-level — the {} operator joins reg vectors, not character strings. The widths have to add up exactly.

concat.v — bit-level concatenation
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
reg [8*3-1:0] hi = "Hi ";            // 24 bits
reg [8*5-1:0] who = "World";          // 40 bits
reg [8*8-1:0] msg;                    // 64 bits
initial msg = {hi, who};              // 24 + 40 = 64 ✅ → "Hi World"

If the widths don't add up to the destination, you'll get a width-mismatch warning and silent truncation / zero-extension. There is no + operator for strings in Verilog (that's a SystemVerilog string-type feature).

Common Mistakes

Five Verilog-string bugs that ship.

  1. Undersized reg truncates the high-order characters. reg [31:0] m = "Hello"; drops the 'H'. Always size with [8*N-1:0].
  2. Zero-padding on the upper bits. A 64-bit reg holding "Hi" has 48 zero bits on top. %s prints those as NUL bytes — usually invisible on terminals but visible in pipe-captured output.
  3. Comparison across mismatched widths. "READY" == "READY " is false at the bit level. Fix the slot width once and pad everything explicitly.
  4. Forgetting \ doubling. "C:\design" is wrong — \d is parsed by the lexer (and the \d here is not a defined escape, so behaviour is implementation-defined). Write "C:\\design".
  5. Treating %s as null-terminated. %s prints the argument's full byte width, including NUL padding bytes. There is no string-termination convention in pure Verilog.

Verilog Strings vs SystemVerilog string

PropertyPure Verilog (IEEE 1364)SystemVerilog string (IEEE 1800)
Typereg [N*8-1:0] (bit vector)string (first-class dynamic type)
LengthFixed at declaration (reg width)Dynamic — .len() returns it
ComparisonBit-vector ==, width-sensitive== operates on characters; widths irrelevant
Concatenation{a, b} — must match destination widths1 + s2 — produces a new dynamic string
SubstringManual bit-slicings.substr(i, j)
Find / replaceManualBuilt-in methods
MemoryFixed at declarationAllocates dynamically per assignment
Use in RTLConstant ROM if synthesised; usually debug onlyNot synthesisable — testbench/UVM only

If you're writing SystemVerilog testbenches, prefer the string type. In pure Verilog (.v files, IEEE 1364), the packed-ASCII reg-vector model is the only option — and it works fine for $display formatting and small fixed-width name slots, which is 95% of what RTL string handling needs.

Worked Example — Opcode Mnemonic Decode

A tiny instruction-decode block that converts a 4-bit opcode into a 4-character mnemonic for $display-style debug, illustrating all the string-handling rules at once.

rtl/opcode_decoder.v — opcode → 4-char mnemonic
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module opcode_decoder (
    input  wire [3:0]      opcode,
    output reg  [8*4-1:0]  mnemonic    // 4 chars × 8 bits = 32 bits
);
 
    always @(*) begin
        case (opcode)
            4'h0:    mnemonic = "NOP ";    // padded to 4 chars
            4'h1:    mnemonic = "ADD ";
            4'h2:    mnemonic = "SUB ";
            4'h3:    mnemonic = "AND ";
            4'h4:    mnemonic = "OR  ";    // padded to 4 chars
            4'h5:    mnemonic = "XOR ";
            4'h6:    mnemonic = "LD  ";
            4'h7:    mnemonic = "ST  ";
            default: mnemonic = "??? ";
        endcase
    end
 
endmodule

What's right about this:

  • Every literal is exactly 4 characters → 32 bits → matches [8*4-1:0]. No truncation, no padding surprises.
  • The default arm prevents a latch and gives $display an unmistakable failure marker ("??? ") for unhandled opcodes.
  • Comparison against this output (e.g. if (mnemonic == "ADD ")) works because every value is the same 32-bit width.

Debug usage in a testbench:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
initial begin
    $display("Cycle %0d : opcode=%h → %s", $time, dut.opcode, dut.mnemonic);
end

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Cycle 100 : opcode=1 → ADD
Cycle 110 : opcode=3 → AND
Cycle 120 : opcode=f → ???

Interview Insights

Summary

Verilog has no string type — strings are packed ASCII bytes (8 bits per character, MSB-first) stored in reg vectors. The literal syntax ("...", IEEE 1364 §3.6) is single-line with \-escapes for newline / tab / quote / backslash / octal byte. The destination reg width determines the result: too narrow drops the leftmost characters; too wide zero-pads the upper bits. $display format specifiers (%s, %d, %h, %c, %t, %m) read their argument at the reg-vector level — %s walks the bit vector eight bits at a time. Equality (==) is bit-level and width-sensitive, so fix a slot width once and pad everything to it. SystemVerilog's string type changes the model — first-class type, dynamic length, character-level operators — and is the right choice for testbench code, but pure Verilog's packed-ASCII model handles the small fixed-width name slots that RTL debug code needs. The next sub-topic covers identifier declaration — the lexer rules for naming reg, wire, module, and friends.

Beat coverage notes: all required beats present. ⏱ Timing Observation, 🧠 Simulator Scheduling Insight omitted — strings are pure lexical/value rules; no timing or scheduling angle. The 🏗 Synthesis Concern is the constant-ROM behaviour of string literals in RTL and the silent leftmost-character truncation.