Reassembling a struct's fields into a flat byte stream for a serial link, or splitting a received byte stream back into a struct's fields, normally means writing the shifting and masking by hand, field by field. SystemVerilog's streaming operators, {>>{...}} and {<<{...}}, do this packing and unpacking in a single expression instead.

8 min read | Intermediate Level

What You'll Learn

  • How {>>{...}} and {<<{...}} pack a group of values into a single bit stream
  • What the optional slice size actually reorders, and why >> ignores it entirely
  • How the same operators run in reverse to unpack a stream back into variables
  • A realistic design and verification use: converting a struct to and from a byte queue

Packing Values Into a Stream

A streaming concatenation looks like an ordinary concatenation, but with a stream operator and an optional slice size right after the opening brace:


module tb;
  initial begin
    int j = { "A", "B", "C", "D" };   // packs to 32'h41424344

    bit [31:0] s1 = { >>      { j } };  // same bit order, unchanged
    bit [31:0] s2 = { << byte { j } };  // byte order reversed
    bit [31:0] s3 = { << 16   { j } };  // 16-bit halves swapped

    $display("j  = 0x%0h", j);
    $display("s1 = 0x%0h", s1);
    $display("s2 = 0x%0h", s2);
    $display("s3 = 0x%0h", s3);
  end
endmodule

Output

Output
j  = 0x41424344
s1 = 0x41424344
s2 = 0x44434241
s3 = 0x43444142

The stream operator, >> or <<, controls the direction blocks are streamed in. >> streams left to right, which is the same order the bits already have - so it simply ignores whatever slice size is given.

<< streams right to left instead. It slices the value into blocks of the given size, starting from the right-most bit, and then reverses the order of those blocks - without reversing the bits inside each block.

That is exactly what s2 and s3 show. { << byte {j} } slices j into four 8-bit (one-byte) blocks and reverses their order, which is the standard way to flip between big-endian and little-endian byte order. { << 16 {j} } instead slices it into two 16-bit blocks and swaps those.

If no slice size is given at all, it defaults to 1 bit - so a bare {<<{expr}} reverses the individual bits of expr, not whole bytes or words.

How the streaming operators and slice sizes reorder the bytes of a 32-bit value

Unpacking a Stream Back Into Variables

Putting a streaming concatenation on the left-hand side of an assignment runs the same operation in reverse - it unpacks a stream into one or more variables:


module tb;
  initial begin
    bit [31:0] stream = 32'h41424344;
    byte a, b, c, d;

    { >> { a, b, c, d } } = stream;

    $display("a = 0x%0h  b = 0x%0h  c = 0x%0h  d = 0x%0h", a, b, c, d);
  end
endmodule

Output

Output
a = 0x41  b = 0x42  c = 0x43  d = 0x44

The source on the right supplies the bits, consumed from its most significant end first, and the variables on the left are filled in the order they are listed. If the source has more bits than the targets need, the extra low-order bits are simply left unread. If it has fewer bits than the targets need, that is an error - there is nothing left to fill the remaining variable with.

Packing a Struct Into a Byte Queue

Streaming operators work on more than just integers. A struct, array, or queue can be streamed too, which is what makes them useful for modeling a packet on a byte-wide channel:


module tb;
  typedef struct packed {
    byte addr;
    byte code;
    byte payload;
  } pkt_t;

  initial begin
    byte channel [$];   // the byte-wide channel, modeled as a queue
    byte pkt_bytes [$];
    pkt_t p, rx;
    byte rx_bytes [$];

    // Send: pack the struct's 3 bytes into a queue, then append that to the channel
    p = '{addr: 8'hA0, code: 8'h3C, payload: 8'hFF};
    pkt_bytes = { >> byte { p } };
    channel   = { channel, pkt_bytes };

    $display("p        = 0x%0h", p);
    $display("channel  = 0x%0h 0x%0h 0x%0h", channel[0], channel[1], channel[2]);

    // Receive: pop 3 bytes back off the channel and unpack them into a struct
    rx_bytes = { channel[0], channel[1], channel[2] };
    { >> byte { rx } } = rx_bytes;

    $display("rx.addr    = 0x%0h", rx.addr);
    $display("rx.code    = 0x%0h", rx.code);
    $display("rx.payload = 0x%0h", rx.payload);
  end
endmodule

Output

Output
p        = 0xa03cff
channel  = 0xa0 0x3c 0xff
rx.addr    = 0xa0
rx.code    = 0x3c
rx.payload = 0xff

The sender's struct fields are streamed out byte by byte, in declaration order, without the sender ever writing out the individual shifts and masks for addr, code, and payload separately. The receiver does the same operation in reverse, reconstructing the struct's fields directly from the raw bytes pulled off the channel.

The same conversion is equally useful on the design side, for a serializer or deserializer that packs a wide internal bus into one byte at a time for a narrow external interface, and on the verification side, for a driver or monitor that converts between a readable transaction struct and the raw bytes actually seen on a pin-level bus.

Common Beginner Mistakes

Mistake #1: Assuming >> reorders data the same way << does

{ >> N {expr} } and { >> {expr} } produce the exact same result for any slice size N, because left-to-right streaming never reorders anything.

Only << actually uses the slice size to reorder blocks. Reaching for >> out of habit and expecting a reordering effect produces silently unchanged data instead of an error.

Mistake #2: Forgetting the default slice size is 1 bit, not 1 byte

{ << {expr} } with no slice size reverses individual bits, which is rarely what's intended when the goal is byte or word reordering.

Write { << byte {expr} } or an explicit bit count like { << 8 {expr} } whenever the reordering should happen at byte or word granularity instead of bit granularity.

Mistake #3: Unpacking into a target with too few bits available

If the right-hand stream supplies fewer bits than the left-hand variables need, unpacking is an error, not a zero-fill.

Zero-filling only happens the other way around, when a streaming concatenation is packed into a fixed-size target that is wider than the stream: the stream is left-aligned and the remaining low-order bits are zero-filled.

Quick Check

Q: For int j = 32'h11_22_33_44;, what does { << 16 {j} } produce?

Show Answer

32'h3344_1122. j splits into two 16-bit blocks, 'h1122 and 'h3344, and << reverses their order - giving 'h3344 followed by 'h1122 - without touching the bit order inside either block.

Practice Exercise

Challenge: Byte-Swap a 32-bit Value

Write a one-line expression that reverses the byte order of a 32-bit variable data, so that 32'h12_34_56_78 becomes 32'h78_56_34_12.

Hint

This is exactly the big-endian/little-endian conversion shown earlier - it just needs the right slice size for a byte.

Solution

module tb;
  initial begin
    bit [31:0] data    = 32'h12345678;
    bit [31:0] swapped = { << byte { data } };

    $display("data    = 0x%0h", data);
    $display("swapped = 0x%0h", swapped);
  end
endmodule

Output

Output
data    = 0x12345678
swapped = 0x78563412

Slicing into byte-sized blocks and streaming them right to left reverses the four bytes while leaving the bits inside each byte untouched - exactly a byte-swap.

Summary

In this tutorial, you learned:

  • {>>{...}} and {<<{...}} pack their contents into a single bit stream, with an optional slice size controlling how it gets reordered
  • >> always streams left to right and ignores the slice size; only << reorders blocks, right to left
  • The default slice size is 1 bit, so byte or word reordering needs an explicit slice size like byte or 16
  • The same operators on the left-hand side of an assignment unpack a stream back into one or more variables
  • Structs, arrays, and queues can all be streamed, which makes these operators a natural fit for converting a struct-typed packet to and from a raw byte channel
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5