Packed vs unpacked arrays

Read packed and unpacked dimensions from a declaration, and predict which index holds which byte of a word.

Lesson
1.2
Time
20 min
Runs on
Your browser
Check
5 questions

A bridge test fails on every write: the DUT stores 32'hEFBEADDE where the test sent 32'hDEADBEEF. The bytes arrive most significant first, and the testbench stores byte i into element lanes[i] of a packed array. In a packed array, index 0 is the least significant byte, so the first byte to arrive lands at the bottom of the word.

Bytes stored with lanes[i] = rx_byte
012345rx_validi0123rx_bytedeadbeeflanesxxxxxxdexxxxaddexxbeaddeefbeaddeexpected 32'hdeadbeef

Each byte fills the next lane up from bit 0, so the word comes out byte-reversed.

Dimensions before and after the name

An array declaration can have dimensions on both sides of the variable name, and the side decides how the bits are stored.

Dimensions before the name are packed, which means the whole array is one contiguous vector of bits. logic [3:0][7:0] lanes; is a single 32-bit value, split into four 8-bit fields.

Dimensions after the name are unpacked, which means the array is a collection of separate elements. logic [7:0] rx [0:3]; holds four bytes, named rx[0] to rx[3].

The C-style size in logic [7:0] rx [4]; means the same as [0:3]. That short form is only allowed for unpacked dimensions, so packed dimensions always use a range such as [3:0].

Which index holds the most significant byte

In a packed array, the left index of the declared range is the most significant. For logic [3:0][7:0] lanes, lanes[3] holds bits 31 to 24 and lanes[0] holds bits 7 to 0.

ElementBits of the wordValue when lanes = 32'hDEADBEEF
lanes[3]31:248'hDE
lanes[2]23:168'hAD
lanes[1]15:88'hBE
lanes[0]7:08'hEF

A descending packed range puts index 0 at the least significant end.

Declare the range ascending, as in logic [0:3][7:0] w, and the order flips so that w[0] is the most significant byte. Both forms are legal, so pick one style per project and keep to it.

Unpacked indexes carry no significance at all, because rx[0] is simply the first element. Trouble starts when you copy between the two kinds and assume that index 0 means the same thing in both.

Copy by matching index

// rx[0] arrived first and is the MSB
foreach (rx[i])
  lanes[i] = rx[i];
// lanes == 32'hefbeadde

Byte order reversed, because index 0 of a packed array is the least significant byte.

Map arrival order to significance

// rx[0] arrived first and is the MSB
foreach (rx[i])
  lanes[3-i] = rx[i];
// lanes == 32'hdeadbeef

The first byte lands in lanes[3], bits 31 to 24, as the protocol requires.

What each kind can do

A packed array behaves like a plain vector: you can add to it, compare it with ==, shift it, and assign it to a logic [31:0] without a cast.

An unpacked array is not a number, so you read and write it one element at a time or loop over it with foreach. Memories, FIFOs and lists of samples are natural unpacked arrays.

Reading an unpacked array out of range does not stop the simulation. Instead, a read of rx[4] returns the element type's default value, which is 8'hxx for a logic element, so a bad index looks like bad data.

Two system functions answer size questions about these arrays. $bits(lanes) returns the total number of bits, 32, and $size(rx) returns the number of elements in a dimension, 4.

Memories use both kinds at once

A memory model is the most common mix of the two kinds. logic [31:0] mem [0:1023]; is an unpacked array of 1,024 elements, each a packed 32-bit word, for 4 KB of storage in total.

When you index it, the unpacked index comes first and the packed index second. mem[5] is word 5, and mem[5][15:8] is bits 15 to 8 of that word, which is its byte 1.

The split matches the hardware: you load and store whole words by address, so the address is the unpacked index. Each word is then taken apart into bytes and bits, so the word itself is packed.

A packed word can have two packed dimensions of its own. With logic [3:0][7:0] mem [0:1023];, the expression mem[5][1] selects byte 1 of word 5 directly, with no bit ranges to get wrong.

Slicing a vector with +:

A part-select such as word[15:8] picks a fixed range of bits, and both of its bounds must be constants. When the position changes at run time, you need an indexed part-select instead.

word[base +: 8] selects 8 bits, starting at bit base and counting up, while word[base -: 8] starts at base and counts down. The width must be a constant, but base can be any expression.

So word[k*8 +: 8] extracts byte k, with byte 0 at the least significant end, and that single line replaces a four-way case statement.

The tempting alternative, word[k*8+7 : k*8], does not compile, even though its two bounds always differ by exactly 7. The +: form exists for this situation.

Watch the top end, though. If base is 12 in a 16-bit logic vector, word[base +: 8] asks for bits 19 to 12, and the four bits above 15 come back as x. Check the index range before you slice, or the unknown bits will travel into your scoreboard.

Try it

The first copy reverses the word, as in the opening story. The second maps the first byte to lanes[3] and gives the intended value.

The fixed and indexed part-selects pick the same 8 bits. The last line reads past the end of rx and gets xx. Try idx = -1 and predict the result before you run it.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: lanes[i] = rx[i] : efbeadde lanes[3-i] = rx[i] : deadbeef word[15:8]=be word[8 +: 8]=be bits=32 elements=4 rx[4]=xx

Check yourself

Predict the outputPredict the exact output. Note the direction of the packed range.
module tb;
  logic [0:3][7:0] w;
  initial begin
    w = 32'h11223344;
    $display("%h %h", w[0], w[3]);
  end
endmodule
ConceptWhich declaration gives one 32-bit value that you can add to with +?
ConceptIn the indexed part-select word[base +: 8], what must be a constant?
Read the waveformA receiver writes each arriving byte with lanes[count] = rx_byte, where lanes is logic [3:0][7:0] and count starts at 0. What does lanes hold after cycle 3?
Receiver filling a packed word
01234rx_validcount01234rx_byte12345678last byte

One byte arrives per cycle while rx_valid is 1.

Spot the bugEach option is the body of function automatic logic [7:0] get_byte(logic [31:0] w, int k). It must return byte k, where byte 0 is bits 7:0. Which option is wrong?

Remember

  • Dimensions before the name are packed and form one vector; dimensions after the name are unpacked and form separate elements.
  • In logic [3:0][7:0] w, w[3] is the most significant byte and w[0] the least.
  • Unpacked index 0 means first, not least significant, so map the order yourself when you copy between the two.
  • w[base +: 8] takes 8 bits upward from base; the width must be constant.
  • An out-of-range read of a logic array returns x.

Practice