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.
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.
| Element | Bits of the word | Value when lanes = 32'hDEADBEEF |
|---|---|---|
lanes[3] | 31:24 | 8'hDE |
lanes[2] | 23:16 | 8'hAD |
lanes[1] | 15:8 | 8'hBE |
lanes[0] | 7:0 | 8'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.
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]=xxCheck yourself
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 andw[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 frombase; the width must be constant.- An out-of-range read of a
logicarray returnsx.
Practice
- Byte-lane extraction Implement · Easy