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.
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
\nto 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.
"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-lineEscape Sequences
| Escape | ASCII | Meaning |
|---|---|---|
\n | 0x0A | Newline (LF) |
\t | 0x09 | Horizontal tab |
\r | 0x0D | Carriage return |
\\ | 0x5C | Literal backslash |
\" | 0x22 | Literal double-quote |
\v | 0x0B | Vertical tab |
\f | 0x0C | Form feed |
\a | 0x07 | Bell (alert) |
\ddd | varies | Octal escape — 1 to 3 octal digits, value 000–377 |
%% | 0x25 | Literal % (in $display format strings only) |
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:
"\012" // 0x0A — same as \n
"\015" // 0x0D — same as \r
"\033" // 0x1B — ESC (terminal-control prefix; useful in $display)
"\377" // 0xFF — maximum byte valueOctal 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:
// 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:
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 bufferThe 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.
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_4869The 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:
| Specifier | Meaning |
|---|---|
%d / %0d | Decimal (signed). %0d drops leading spaces. |
%h / %0h | Hexadecimal. %0h drops leading zeros. |
%b / %0b | Binary. |
%o / %0o | Octal. |
%c | One ASCII character (low 8 bits of the argument). |
%s | Print the argument as a string — interprets each byte of the argument as ASCII. |
%t | Time, using the $timeformat setup. |
%m | Hierarchical name of the calling scope (no argument consumed). |
%% | Literal %. |
$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.
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.
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.
- Undersized reg truncates the high-order characters.
reg [31:0] m = "Hello";drops the'H'. Always size with[8*N-1:0]. - Zero-padding on the upper bits. A 64-bit reg holding
"Hi"has 48 zero bits on top.%sprints those as NUL bytes — usually invisible on terminals but visible in pipe-captured output. - Comparison across mismatched widths.
"READY" == "READY "is false at the bit level. Fix the slot width once and pad everything explicitly. - Forgetting
\doubling."C:\design"is wrong —\dis parsed by the lexer (and the\dhere is not a defined escape, so behaviour is implementation-defined). Write"C:\\design". - Treating
%sas null-terminated.%sprints the argument's full byte width, including NUL padding bytes. There is no string-termination convention in pure Verilog.
Verilog Strings vs SystemVerilog string
| Property | Pure Verilog (IEEE 1364) | SystemVerilog string (IEEE 1800) |
|---|---|---|
| Type | reg [N*8-1:0] (bit vector) | string (first-class dynamic type) |
| Length | Fixed at declaration (reg width) | Dynamic — .len() returns it |
| Comparison | Bit-vector ==, width-sensitive | == operates on characters; widths irrelevant |
| Concatenation | {a, b} — must match destination width | s1 + s2 — produces a new dynamic string |
| Substring | Manual bit-slicing | s.substr(i, j) |
| Find / replace | Manual | Built-in methods |
| Memory | Fixed at declaration | Allocates dynamically per assignment |
| Use in RTL | Constant ROM if synthesised; usually debug only | Not 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.
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
endmoduleWhat's right about this:
- Every literal is exactly 4 characters → 32 bits → matches
[8*4-1:0]. No truncation, no padding surprises. - The
defaultarm prevents a latch and gives$displayan 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:
initial begin
$display("Cycle %0d : opcode=%h → %s", $time, dut.opcode, dut.mnemonic);
endOutput:
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.
Related Tutorials
- Lexical Conventions — Chapter 4 overview; the parent layer.
- Number Representation — Chapter 4.4; the literal-sizing discipline that strings inherit.
- Operator Usage — Chapter 4.3; the
{}concatenation operator used for string assembly. - RTL Designing — Chapter 3; where string-via-
$displaydebug fits into the RTL workflow.