Shift Operators
<<, >>, <<<, >>> — logical vs arithmetic right shift, signed behavior, sign extension.
Module 4 · Page 4.6
The One Operator That Actually Has a Trap
If you are verifying a DSP core, a fixed-point multiplier, or any design that divides signed values by powers of two, shift operators become genuinely important. The arithmetic right shift (>>>) is what makes signed_val >>> 1 equivalent to dividing by 2 while preserving the sign. The logical right shift (>>) shifts in zeros from the left — which is correct for unsigned values but gives the wrong answer for negative signed values.
The concrete failure: -8 >> 1 gives +120 on an 8-bit signed variable (the MSB shifts in as 0, the negative number becomes large positive). -8 >>> 1 gives -4, which is the correct arithmetic result. Both operations succeed silently. No error, no warning — just a wrong number feeding the rest of your datapath.
Left shifts have no such distinction — both << and <<< always shift in zeros on the right. They are functionally identical. The double-angle form <<< exists for symmetry with the arithmetic right shift, but adds nothing different.
Four Operators, Two Distinctions
| Operator | Name | Fill direction | Fill value | Use for |
|---|---|---|---|---|
<< | Logical left shift | Fills right (LSB side) | Always 0 | Multiply by 2^n, bit-mask generation |
<<< | Arithmetic left shift | Fills right (LSB side) | Always 0 — identical to << | Signed multiply by 2^n (same result as <<) |
>> | Logical right shift | Fills left (MSB side) | Always 0 | Unsigned divide by 2^n, extract upper bits |
>>> | Arithmetic right shift | Fills left (MSB side) | Sign bit for signed types, 0 for unsigned | Signed divide by 2^n — preserves sign |
How Arithmetic Right Shift Works
When you right-shift a signed negative number, the vacated MSB positions need to be filled. Logically shifting in 0s turns the number positive — which is arithmetically wrong. Arithmetically shifting in copies of the sign bit (1 for negative, 0 for positive) preserves the sign and gives the correct floor-division-by-power-of-two result.
- << — Logical Left — Shifts bits toward MSB, fills 0s on the right. Equivalent to multiplying by 2. Bits shifted out of the MSB are lost. Works identically to <<<.
- >> — Logical Right — Shifts bits toward LSB, fills 0s on the left. Correct for unsigned divide-by-2. Wrong for signed negative — turns negative into large positive.
- >>> — Arithmetic Right — Fills with the sign bit (MSB). For negative signed values, fills 1s. For positive or unsigned, fills 0s. This is what you want for signed division.
- <<< — Arithmetic Left — Fills 0s on the right — identical to << in all cases. Exists for language symmetry. Safe to use either form for left shifts.
Syntax & Type Rules
// General syntax: operand shift_op shift_amount
result = value << n; // logical left — n can be an expression
result = value >> n; // logical right
result = value <<< n; // arith left — same as <<
result = value >>> n; // arith right — sign-extends if value is signed
// ── Unsigned examples (logic [7:0]) ──────────────────────────────
logic [7:0] u = 8'b0001_0000; // 16
u << 2; // 8'b0100_0000 = 64 (×4)
u >> 2; // 8'b0000_0100 = 4 (÷4, zero-fill)
u >>> 2; // 8'b0000_0100 = 4 (same — unsigned)
// ── Signed examples (logic signed [7:0]) ─────────────────────────
logic signed [7:0] s = -8; // 8'b1111_1000
s >> 2; // 8'b0011_1110 = +62 WRONG: shifted in 0s
s >>> 2; // 8'b1111_1110 = -2 CORRECT: shifted in sign bit (1)
// ── Shift amount rules ───────────────────────────────────────────
// Shift amount is always treated as UNSIGNED
// If shift amount >= operand width, result is 0 (or all sign bits for >>>)
logic [7:0] v = 8'hFF;
v >> 8; // 8'h00 — shifted out entirely
v >> 100; // 8'h00 — same
// ── Variable shift (generates barrel shifter in synthesis) ───────
logic [3:0] shift_amt;
result = data << shift_amt; // shift_amt computed at runtimeType Determines >>> Behavior
| Declaration | Type | >>> behavior |
|---|---|---|
logic [7:0] v | Unsigned 4-state | Zero-fill — same as >> |
logic signed [7:0] v | Signed 4-state | Sign-extend — fills MSB copies |
int v | Signed 2-state 32-bit | Sign-extend |
bit [7:0] v | Unsigned 2-state | Zero-fill |
shortint v | Signed 2-state 16-bit | Sign-extend |
$signed(v) >>> n | Cast to signed context | Sign-extend regardless of original declaration |
Step-by-Step Visual Evaluation
Left Shifts — Bit Movement Diagram
Starting value: 8'b0001_1010 = 26
| Operation | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | Decimal |
|---|---|---|---|---|---|---|---|---|---|
| Original | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 26 |
<< 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 0 | 52 (×2) |
<< 2 | 0 | 1 | 1 | 0 | 1 | 0 | 0 | 0 | 104 (×4) |
<< 3 | 1 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 208 (×8, fits in 8 bits) |
<< 4 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 160 — MSB of original lost |
Green cells = zeros shifted in from the right.
Right Shifts — Logical vs Arithmetic on -8
logic signed [7:0] s = -8 → binary 8'b1111_1000. Watch what fills in from the left:
| Operation | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | Value | Correct? |
|---|---|---|---|---|---|---|---|---|---|---|
| Original (-8) | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | -8 | — |
>> 1 (logical) | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | +124 | ❌ Wrong |
>>> 1 (arithmetic) | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | -4 | ✅ Correct (-8/2) |
>> 2 (logical) | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | +62 | ❌ Wrong |
>>> 2 (arithmetic) | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | -2 | ✅ Correct (-8/4) |
Green = zero fill (logical). Orange = sign-bit fill (arithmetic).
Shift Amount Effects
| Shift amount | Effect on 8'hFF >> n | Effect on signed -8 >>> n |
|---|---|---|
| 0 | 8'hFF (unchanged) | -8 (unchanged) |
| 1 | 8'h7F = 127 | -4 |
| 4 | 8'h0F = 15 | -1 (all sign bits) |
| 7 | 8'h01 = 1 | -1 (still all sign bits) |
| 8 | 8'h00 = 0 (fully shifted out) | -1 (sign bit fills entire result) |
| > 8 | 8'h00 = 0 | -1 for negative, 0 for positive |
Code Examples — From Basics to Production
Example 1 — Beginner: All Four Operators
module tb_shift_basic;
logic [7:0] u_val = 8'd16; // unsigned 16 = 0001_0000
logic signed [7:0] s_neg = -8; // signed -8 = 1111_1000
logic signed [7:0] s_pos = 8'd16; // signed +16 = 0001_0000
initial begin
// ── Left shifts — zero fills right, identical for << and <<< ──
$display("u_val << 2 = %0d (16×4=64)", u_val << 2); // 64
$display("u_val <<< 2 = %0d (same)", u_val <<< 2); // 64
// ── Logical right — zero fills left ───────────────────────────
$display("u_val >> 2 = %0d (16÷4=4)", u_val >> 2); // 4
$display("s_neg >> 2 = %0d (WRONG!)", s_neg >> 2); // +62
// ── Arithmetic right — sign fills left ────────────────────────
$display("s_neg >>> 2 = %0d (-8÷4=-2)", s_neg >>> 2); // -2
$display("s_pos >>> 2 = %0d (16÷4=4)", s_pos >>> 2); // 4 (positive: 0 fills)
$display("u_val >>> 2 = %0d (unsigned: 0 fill)", u_val >>> 2); // 4
$finish;
end
endmoduleExpected output:
u_val << 2 = 64 (16×4=64)
u_val <<< 2 = 64 (same)
u_val >> 2 = 4 (16÷4=4)
s_neg >> 2 = 62 (WRONG!)
s_neg >>> 2 = -2 (-8÷4=-2)
s_pos >>> 2 = 4 (16÷4=4)
u_val >>> 2 = 4 (unsigned: 0 fill)Example 2 — Intermediate: Address Calculation and Mask Generation
module tb_shift_address;
logic [31:0] base_addr = 32'h1000_0000;
logic [31:0] word_addr, byte_mask, field_mask;
int index;
initial begin
// ── Word-aligned address from byte index ──────────────────────
// AXI/APB: word addresses are byte_addr >> 2
// Byte address from word index: word_idx << 2
for (int i = 0; i < 4; i++) begin
word_addr = base_addr + (32'(i) << 2);
$display("Word[%0d] addr = 0x%08h", i, word_addr);
end
// 0x10000000, 0x10000004, 0x10000008, 0x1000000C
// ── Generate single-bit mask at position n ────────────────────
for (int bit_pos = 0; bit_pos < 4; bit_pos++) begin
byte_mask = 32'h1 << bit_pos;
$display("Bit mask [%0d] = 0x%08h (%032b)",
bit_pos, byte_mask, byte_mask);
end
// ── Extract byte N from 32-bit word ──────────────────────────
logic [31:0] data = 32'hAABBCCDD;
for (int byte_n = 0; byte_n < 4; byte_n++) begin
logic [7:0] extracted = (data >> (byte_n * 8)) & 8'hFF;
$display("Byte[%0d] = 0x%02h", byte_n, extracted);
end
// Byte[0]=0xDD, Byte[1]=0xCC, Byte[2]=0xBB, Byte[3]=0xAA
$finish;
end
endmoduleExample 3 — Verification-Oriented: Fixed-Point Arithmetic Checker
// DSP core verification: right-shift with rounding
// DUT computes: (A × B) >>> SCALE, with half-up rounding
class DspScoreboard;
localparam int SCALE = 8; // right-shift amount
function int expected_result(input int a, b);
longint product = longint'(a) * longint'(b);
longint half = 1 << (SCALE - 1); // 0.5 in fixed-point
longint rounded = (product + half) >>> SCALE; // arithmetic shift
return int'(rounded);
endfunction
function void check(input int a, b, dut_out);
int exp = expected_result(a, b);
if (exp !== dut_out)
$error("DSP FAIL: %0d * %0d >> %0d = exp %0d, got %0d",
a, b, SCALE, exp, dut_out);
else
$display("DSP OK: %0d * %0d >> %0d = %0d", a, b, SCALE, exp);
endfunction
endclass
module tb_dsp;
DspScoreboard sb;
initial begin
sb = new();
sb.check(100, 200, 78); // 100×200=20000, 20000>>8 ≈ 78
sb.check(-100, 200, -78); // -100×200=-20000, -20000>>8 ≈ -78
$finish;
end
endmoduleExample 4 — Corner Case: >>> on Unsigned Type — No Sign Extension
module tb_unsigned_arith_shift;
logic [7:0] u_val = 8'hF8; // unsigned — MSB = 1, but not signed
logic signed [7:0] s_val = 8'hF8; // signed — same bits = -8
initial begin
$display("u_val = %0d (unsigned 248)", u_val);
$display("s_val = %0d (signed -8)", s_val);
// >>> on UNSIGNED — no sign extension, fills 0
$display("u_val >>> 2 = %0d (zero-fill: 248/4=62)", u_val >>> 2);
// >>> on SIGNED — sign extension, fills 1s
$display("s_val >>> 2 = %0d (sign-fill: -8/4=-2)", s_val >>> 2);
// If you WANT signed behavior on an unsigned variable — cast first
$display("$signed(u_val) >>> 2 = %0d", $signed(u_val) >>> 2);
// Now sign-extends: -8/4 = -2
$finish;
end
endmoduleExpected output:
u_val = 248 (unsigned 248)
s_val = -8 (signed -8)
u_val >>> 2 = 62 (zero-fill: 248/4=62)
s_val >>> 2 = -2 (sign-fill: -8/4=-2)
$signed(u_val) >>> 2 = -2Waveform & Simulation Thinking
Result Width — Does Shifting Change the Width?
In SystemVerilog, shift operators do not change the width of the result. The result is the same width as the left operand (the value being shifted). Bits that shift off the edge are simply lost — there is no automatic widening. This is important when left-shifting a value that might overflow.
logic [7:0] a = 8'hFF;
logic [15:0] wide_result;
logic [7:0] narrow_result;
// Result is same width as LHS — truncated to 8 bits
narrow_result = a << 1; // 8'h1FE & 8'hFF mask = 8'hFE — MSB lost
// Widen BEFORE shifting to preserve all bits
wide_result = {8'h00, a} << 1; // concat to 16 bits first: 16'h01FE
// Or equivalently:
wide_result = 16'(a) << 1; // explicit width cast
$display("narrow: 0x%0h", narrow_result); // 0xfe
$display("wide: 0x%0h", wide_result); // 0x1feSynthesis — Constant vs Variable Shift
| Shift type | Hardware generated | Area / Timing impact |
|---|---|---|
Constant shift (val << 3) | Pure wiring — no gates at all | Zero area, zero delay — just rewire the bits |
Variable shift (val << n) | Barrel shifter — multiplexer tree | Significant area and timing — scales with log2(width) MUX levels |
Arithmetic right (>>>) constant | Wiring + sign replication | Near-zero area — replicate MSB N times |
| Arithmetic right variable | Barrel shifter with sign-aware MUX | Slightly larger than logical barrel shifter |
Where You'll Use These in Real Projects
// ── 1. Single-bit mask generation — error injection ───────────────
function logic [31:0] bit_mask(input int pos);
return 32'h1 << pos;
endfunction
// ── 2. Protocol field packing — build from named fields ───────────
function logic [31:0] pack_axi_ctrl(
input logic [1:0] burst_type,
input logic [2:0] burst_len,
input logic write
);
return ({29'h0, burst_type} << 16)
| ({29'h0, burst_len} << 8)
| {31'h0, write};
endfunction
// ── 3. Byte lane enable from address ──────────────────────────────
function logic [3:0] byte_enables(input logic [31:0] addr, input int size);
logic [3:0] mask = (4'h1 << size) - 4'h1; // size bytes → N-bit mask
return mask << addr[1:0]; // align to byte offset
endfunction
// ── 4. Scoreboard: extract field from DUT response ────────────────
function logic [7:0] extract_status(input logic [31:0] reg_val);
return (reg_val >> 16) & 8'hFF; // bits [23:16]
endfunction
// ── 5. Constraint: aligned address generation ─────────────────────
class AlignedTxn;
rand logic [31:0] addr;
rand logic [1:0] size; // 0=byte, 1=halfword, 2=word
constraint c_align {
addr[1:0] == 2'b00; // word-aligned
addr inside {[32'h1000:32'h1FFF]};
}
endclassCommon Bugs & How to Debug Them
Bug 1 — Logical >> on Signed Value: Wrong Arithmetic Result
// DUT computes signed accumulator / 4
// Scoreboard model uses wrong shift
int accumulator = -100;
// BUGGY: logical right shift fills 0s — makes negative → large positive
int result_wrong = accumulator >> 2;
$display("Wrong: %0d", result_wrong); // 1073741799 — completely wrong
// CORRECT: arithmetic right shift preserves sign
int result_right = accumulator >>> 2;
$display("Right: %0d", result_right); // -25 — correct (-100/4)Bug 2 — Left Shift Overflow: Result Truncated to Operand Width
logic [7:0] val = 8'hC0; // 1100_0000
// BUGGY: expecting 0x300 but result truncated to 8 bits
logic [7:0] result = val << 2;
$display("result = 0x%0h", result); // 0x00 — top bits lost!
// CORRECT: widen before shifting
logic [9:0] result_wide = {2'b00, val} << 2;
$display("result_wide = 0x%0h", result_wide); // 0x300 — correctBug 3 — >>> on logic [N:0] — No Sign Extension Happens
logic [7:0] data = 8'hF8; // MSB=1 but type is UNSIGNED
// BUGGY: engineer expects sign extension because "MSB is 1"
// But logic [7:0] is unsigned — >>> fills 0s, not the MSB
logic [7:0] wrong = data >>> 2;
$display("Wrong: %0d", wrong); // 62 — zero filled, not sign extended
// CORRECT option 1: declare as signed
logic signed [7:0] s_data = 8'hF8; // same bits = -8
logic [7:0] correct1 = s_data >>> 2;
$display("Correct1: %0d", correct1); // -2
// CORRECT option 2: cast at the shift expression
logic [7:0] correct2 = $signed(data) >>> 2;
$display("Correct2: %0d", correct2); // -2Interview Questions
Beginner Level
Q1: What is the difference between >> and >>> in SystemVerilog?>> is logical right shift — always fills vacated MSB positions with 0. >>> is arithmetic right shift — fills with the sign bit for signed types (preserving the sign), and with 0 for unsigned types. For positive values and unsigned types, they produce the same result. The difference only appears with signed negative values. Q2: What does <<< do differently from <<? Nothing. Both logical and arithmetic left shift always fill vacated right-side (LSB) positions with 0, regardless of the operand's sign. <<< exists for symmetry with the four-operator set but produces identical results to <<. Either form is correct; teams typically standardize on one or the other in their coding guidelines.
Intermediate Level
Q3: A signed 8-bit variable holds -8 (8'b1111_1000). What is the result of >> 2 vs >>> 2?>> 2 (logical): fills 0s from left → 8'b0011_1110 = +62. Wrong for signed arithmetic.>>> 2 (arithmetic): fills sign bit (1) from left → 8'b1111_1110 = -2. Correct — -8 divided by 4 is -2. Q4: What happens when the shift amount equals or exceeds the operand width? For logical shifts (<<, >>) and arithmetic left: the result is 0 — all bits shift out. For arithmetic right shift (>>>) on a signed type: the result is all copies of the sign bit — all 1s for negative, all 0s for positive. The shift amount is always treated as unsigned, so a negative shift amount would be treated as a very large positive shift, producing 0 or all-sign-bits.
Experienced Engineer Level
Q5: A scoreboard models a DUT that performs signed division by powers of two. The DUT uses arithmetic right shift but the model uses integer division (/). For -7 ÷ 4, do they produce the same result? No. -7 >>> 2 = -2 (floor division: ⌊-7/4⌋ = ⌊-1.75⌋ = -2). -7 / 4 = -1 (truncation toward zero: -1.75 truncates to -1). Arithmetic right shift performs floor division, not truncation-toward-zero. They agree for exact powers of two (e.g., -8/4=-2), but diverge for non-exact negative values. A scoreboard that uses / to model a DUT that uses >>> will report false mismatches on these edge cases. Always model the DUT's actual shift behavior in the reference model.
Best Practices & Coding Guidelines
- Declare signed types explicitly — For signed data, use int, shortint, or logic signed [N:0]. Never rely on a high MSB alone — the type determines >>> behavior.
- Widen before shifting left — If the left shift result needs to be wider than the operand, widen first: 16'(narrow) << n or concatenate zeros. Shifting a narrow variable silently discards high bits.
- Use >>> only for signed arithmetic — Use >>> exclusively for signed division-by-power-of-two. For unsigned operations, >> is clearer and communicates intent.
- Match model to DUT implementation — If the DUT uses arithmetic shift, your scoreboard reference model must also use arithmetic shift — not integer division, which truncates differently for negative non-exact values.
| Task | Correct approach | Common mistake |
|---|---|---|
| Multiply unsigned by 2^n | val << n | Forgetting result is same width — overflow silently |
| Divide unsigned by 2^n | val >> n | Using >>> — works but wrong operator semantically |
| Divide signed by 2^n | signed_val >>> n | Using >> — wrong result for negative values |
| Generate bit mask at position n | 32'h1 << n | 1 << n — 1 is 32-bit int but expression context may truncate |
| Extract upper byte from 32-bit | (val >> 24) & 8'hFF | Forgetting the AND mask — implicit sign-extension can corrupt |
| Left shift wider result needed | 16'(val) << n | val << n — result stays at original width |
Summary
Four shift operators, but the real decision you make is between two: >> and >>>. Left shifts (<< and <<<) are identical. Right shifts diverge only for signed negative values.
>>>only sign-extends when the operand is a signed type. Applying>>>tologic [N:0]produces the same zero-fill result as>>. The operator does not override the declaration. Use$signed()to cast if needed.- Arithmetic right shift is floor division, not truncation. For negative non-exact values,
x >>> ngives a different result thanx / (1 << n). Your scoreboard reference model must match the DUT's implementation exactly. - Left shifts do not widen the result. The result is always the same width as the left operand. Bits shifted past the MSB are lost. Widen before shifting when the full result is needed.