Fixed and dynamic arrays

Size an array from the transaction it holds, grow it without losing data, and know what happens at the edges of its bounds.

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

A read scoreboard keeps the expected data of each burst in logic [31:0] exp_beat [16], because the first version of the spec allowed 16 beats. When the test starts issuing 32-beat bursts, the writes to beats 16 to 31 are silently ignored. The checker loops with foreach (exp_beat[i]), so it compares only the first 16 beats of every burst and reports a pass. A DUT bug that corrupts the second half of long bursts goes unnoticed for a month.

A 32-beat burst checked against a 16-element array
012345rvalidbeat141516171819rdata0e0f1011ff13checkedcorrupt beat, never compared

Beats 16 and above have no element to land in, so the checker never looks at them.

Fixed arrays have a size set at compile time

A fixed-size array gets its number of elements from the declaration, before the simulation starts. logic [31:0] exp_beat [16]; always has exactly 16 elements, numbered 0 to 15.

Fixed arrays suit counts that the spec fixes for good: 4 byte lanes, 8 DMA channels or 32 interrupt lines. $size(exp_beat) returns the count, which is safer than repeating the number 16 in a loop.

Going past the end does not stop the simulation. The standard says a write to an index outside the array is ignored, and a read returns the element type's default value, which is x for logic and 0 for int. A burst longer than the array therefore loses data without a single message.

Dynamic arrays are sized at run time

A dynamic array is declared with empty brackets, as in logic [31:0] exp_beat [];. It starts with no elements at all, so exp_beat.size() returns 0 until you allocate it.

The constructor new[n] allocates n elements, each set to the element type's default value. For a read burst, the request tells you the size: on an AXI-style bus the length field ARLEN holds the number of beats minus 1, so you allocate new[arlen + 1].

StatementSize afterwardsContents
exp_beat = new[4];4four x elements
exp_beat = new[6](exp_beat);6the old 4 elements, then 2 x elements
exp_beat = new[2](exp_beat);2the first 2 old elements
exp_beat = new[3];3three x elements; the old data is gone
exp_beat.delete();0nothing

Allocating, growing, shrinking and emptying logic [31:0] exp_beat [].

Once allocated, a dynamic array behaves like a fixed one. You index it, loop over it with foreach, and size() tells you how many elements it holds.

Growing without losing data

The second form of the constructor, new[n](old), allocates n elements and copies in as many old elements as fit. Without the argument in parentheses, the new array starts from default values and the old contents are lost.

The two forms differ by a few characters, so the mistake is easy to make and hard to see. A monitor that appends each beat with new[size() + 1] keeps only the last beat it received, and every earlier beat reads as x.

Grow without copying

// append one beat
exp_beat = new[exp_beat.size() + 1];
exp_beat[exp_beat.size() - 1] = data;

Each append throws away the beats collected so far.

Grow and copy

// append one beat
exp_beat = new[exp_beat.size() + 1](exp_beat);
exp_beat[exp_beat.size() - 1] = data;

Keeps every earlier beat and adds the new one at the end.

Growing one element at a time copies the whole array on every append, so the work grows with the square of the length. When you collect items one by one, a queue is the better tool, as the next lesson shows.

Copying an array copies the elements

Assigning one array to another copies every element. After snapshot = exp_beat;, the two arrays are separate, so changing snapshot[0] leaves exp_beat[0] as it was.

This differs from many software languages, where an assignment makes a second name for the same list. In SystemVerilog, a dynamic array on the left of an assignment is resized to match the one on the right.

Check every beat

Size the expected-data array from the request, and count the beats that actually arrive. At the last beat, the count must equal exp_beat.size(), and any beat that arrives after that is an error of its own.

Check the index before you use it as well. A beat number from the DUT can be wrong, and reading past the end returns a default value that may happen to match the data.

The last-beat flag gives a second check. On an AXI-style bus, rlast must be set on beat len - 1 and on no other beat. An early or missing rlast is an error even when every data value matches.

Choosing between fixed and dynamic

Use a fixed array when the count is part of the hardware and never changes during a test, such as the 4 byte lanes of a 32-bit bus. Its size documents the design, and the compiler can check constant indexes against it.

Use a dynamic array when each transaction brings its own count. Sizing a fixed array for the largest burst wastes memory: 1,000 outstanding reads with room for 256 beats each reserve 256,000 elements, even when most bursts carry 4 beats.

A dynamic array also keeps the size honest. exp_beat.size() always equals the burst length you allocated, so a loop over it covers exactly the beats that the request promised.

Try it

The fixed array silently dropped beats 4 and 5, so reading beat 5 returns x. The dynamic array was sized from the burst length and holds all 6 beats; growing it with new[8](exp_beat) kept them and added two x elements.

Changing the copy left the original alone. Now delete (exp_beat) from the new[8] line and predict the next display line before you run it.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: fixed: $size=4, beat 5 reads xxxxxxxx new[6]: size=6, beat 0=xxxxxxxx new[8](exp_beat): beat 5=00000105, beat 7=xxxxxxxx after copy: exp_beat[0]=00000100 snapshot[0]=0000dead new[2] without copy: beat 0=xxxxxxxx after delete: size=0

Check yourself

Predict the outputPredict the exact output.
module tb;
  logic [7:0] a [];
  initial begin
    a = new[2];
    a[0] = 8'h11;
    a[1] = 8'h22;
    a = new[3](a);
    $display("%0d %h %h", a.size(), a[1], a[2]);
  end
endmodule
Conceptexp_beat already holds 4 beats. Then exp_beat = new[8]; runs. What happens to the 4 beats?
ConceptA checker writes beat 20 into logic [31:0] exp [16] and later reads exp[20]. What does the standard say happens?
Read the waveformThe scoreboard allocates exp = new[arlen] when the read request arrives. Which beat arrives with no expected value to compare against?
One read burst with ARLEN = 3
012345arvalidarlen3rvalidbeat0123rlast

ARLEN holds the number of beats minus 1.

Spot the bugEach option is the body of task automatic append(logic [31:0] v), which must add v to the end of the dynamic array beats and keep every earlier element. Which option loses data?

Remember

  • A fixed array has a compile-time size; a dynamic array starts empty and is sized with new[n] at run time.
  • Out-of-range writes are ignored and out-of-range reads return the default value, with no error.
  • new[n](old) keeps the old elements; plain new[n] discards them.
  • Assigning one array to another copies every element; the two arrays are then separate.
  • Size burst storage from the request, and count the beats that actually arrive.

Practice