UVM RAL · Chapter 8 · Memories
Burst Access
Memories are usually accessed in runs, so RAL provides burst access through burst read and burst write to move a contiguous block of locations in a single call instead of one access per location. This matters for two reasons. It can map to a real bus burst of one address phase and many data beats, which is faster and matches how DMA engines and caches actually hit a memory. And it is addressed simply: the values array is written contiguously from a start offset, and the array length is the burst length. Whether it becomes a true protocol burst depends on the adapter and bus, since an adapter without burst support may split it into single accesses. The correctness rule is that a burst must stay within the memory's range, or its tail beats overrun the last location and corrupt the neighbouring region.
Foundation12 min readUVM RALburst_readburst_writeburstoverrun
Chapter 8 · Section 8.3 · Memories
1. Why Should I Learn This?
Real memory traffic is bursty — DMA, caches, and streaming engines move contiguous blocks, not isolated words — so verifying a memory the way it will actually be used means bursting, and burst_read/burst_write are how you do it in RAL. Understanding how a burst is addressed (contiguous from a start index, length = array length), how it maps or degrades to the bus, and the rule that it must stay in range, is what lets you use bursts for both speed and realism without introducing overruns.
Learning burst access — its addressing, its adapter dependence, and its range discipline — builds directly on uvm_mem (8.1) and memory placement (8.2), and it is how you exercise memories realistically before the large-memory strategies of 8.5.
2. Industry Story — the burst that ran off the end
A team uses burst_write to load descriptor blocks into a descriptor RAM. Most loads target the start of the RAM and work perfectly. One test loads a block near the end of the RAM — starting a handful of locations before the last — with a full-size block. The block was longer than the number of locations remaining, so the burst ran past the RAM's last location, and the extra beats wrote into the next region of the address map — a control register block sitting just after the RAM (8.2).
The descriptor RAM 'load' appeared to succeed, but it had silently overwritten several control registers with descriptor data, mis-configuring the block. The failure showed up downstream as corrupted control state with no obvious cause — the RAM write 'passed,' the control registers were 'never written,' and only the end-of-RAM load triggered it, so it looked intermittent and took days to pin to a burst overrun. The root issue was never checking that the burst's length fit within the locations remaining from its start index. The post-mortem lesson: a burst runs contiguously from its start index for its whole length, so a burst that starts near the end of a memory and is longer than the remaining locations overruns the memory's last address and spills into the neighbouring region — always check that start + length stays within the memory's size (8.2).
3. Concept — contiguous from a start index, length = array length, stay in range
Burst access moves a contiguous run of locations in one call:
- Addressing is contiguous from a start index.
burst_write(status, start, values)writesvalues[0]atstart,values[1]atstart + 1, and so on;burst_read(status, start, values)fillsvaluesfrom the run beginning atstart. The length of the data array is the burst length — N values means N locations,startthroughstart + N - 1. - It maps to the bus through the adapter. With a burst-capable bus and adapter, one address phase carries N data beats — the efficiency and realism win. If bursts are unsupported, UVM may split the burst into single accesses: still correct, but no speed-up (so a 'slow burst' is often a serialized one).
- It must stay within the memory's range. Because the run is contiguous for its whole length, the last location touched is
start + N - 1. If that exceeds the memory's last index (size - 1), the burst overruns — the tail beats address past the memory, into whatever the map places next (8.2). The rule:start + N <= size.
Here is a burst write as one address plus N contiguous beats, contrasted with N single writes:
4. Mental Model — a burst is a run with a start and a length that must fit
5. Working Example — a burst write with a fit check
Burst-write a contiguous block, guarding that the run stays within the memory:
// burst_write moves a contiguous block: values[0] at start, values[1] at start+1, ...
// The data array length IS the burst length. Check the run fits BEFORE issuing it.
task load_block(uvm_mem mem, int start, uvm_reg_data_t values[]);
uvm_status_e s;
// FIT CHECK: last location touched is start + values.size() - 1; it must be <= size-1.
if (start + values.size() > mem.get_size()) begin
`uvm_error("BURST", $sformatf("burst overruns memory: start %0d + len %0d > size %0d",
start, values.size(), mem.get_size()))
return;
end
mem.burst_write(s, start, values); // one addressed run of values.size() beats
endtask// A start-at-zero load always fits; the danger is a load near the END of the memory:
uvm_reg_data_t block[] = new[64]; // 64-beat burst
load_block(buf_mem, 0, block); // OK: 0 + 64 = 64 <= size
load_block(buf_mem, size-8, block); // CAUGHT: (size-8) + 64 overruns — the fit check fails cleanly
// Without the fit check, that second call would spill 56 beats PAST the memory into the next region (8.2).// Burst vs single is the ADAPTER's call. If burst_write is 'slow', the adapter likely serialized it:
// one address + N beats needs a burst-capable bus AND an adapter written for bursts; otherwise UVM
// splits into N singles — still correct, just not faster. That is a performance question, not a bug.The fit check (start + N <= size) is the one guard a burst needs for correctness; without it, a burst near the end of the memory overruns into the neighbouring region, which the next section demonstrates.
6. Debugging Session — a burst that overruns the memory's end
A burst runs contiguously for its whole length; starting near the end with a length past the last location overruns into the neighbouring region and corrupts it
BURST OVERRUN// Load a full-size descriptor block near the END of the RAM, with no fit check:
uvm_reg_data_t block[] = new[64]; // 64-beat block
// RAM has 'size' locations; this starts only 8 before the end:
buf_mem.burst_write(s, buf_mem.get_size() - 8, block); // BUG: (size-8)+64 runs 56 PAST the last index
// The first 8 beats land in the RAM; the remaining 56 spill into the NEXT region of the map (8.2)
// -- a control register block -- silently overwriting control state with descriptor data.Most descriptor loads work perfectly — they target the start or middle of the RAM and fit comfortably. The one load that starts near the end of the RAM 'succeeds' too (no error from the burst), but afterwards the block is mysteriously mis-configured: control registers that follow the RAM in the address map hold descriptor-looking garbage. The RAM write reported success and the control registers were never explicitly written, so nothing points at the RAM load; and only the end-of-RAM load triggers it, so it looks intermittent. Tracing the corrupted register values back to descriptor data — and noticing they sit just past the RAM's end — is what finally reveals a burst that ran off the end.
A burst runs contiguously from its start index for its whole length: burst_write(start, values) writes start through start + values.size() - 1, with no knowledge of the memory's boundary. Here the start was size - 8 and the block was 64 beats, so the run needed locations size - 8 through size + 55 — but the memory only has locations up to size - 1. The first 8 beats stayed inside the RAM; the remaining 56 addressed past the memory's last location, into the addresses the map assigns to the next item — a control register block placed right after the RAM (8.2). Those beats wrote descriptor data over control registers. It passed silently because a burst does not check its own bounds and each individual beat is a valid bus write to some address; nothing enforced that start + length stays within size. Start-at-zero and mid-RAM loads never overran, which is why only the end-of-RAM load exposed the bug — a dynamic overrun of the memory's range, distinct from a static placement overlap (8.2) but landing in the same neighbour.
Guard every burst with a fit check: before issuing, verify start + values.size() <= mem.get_size(), and fail cleanly (or clamp/split deliberately) if not — so an end-of-memory load that would overrun is caught with a clear message instead of silently corrupting the neighbour. For loads that legitimately must handle variable start positions, compute the maximum block that fits from the start index, or reject starts too close to the end. The rule the bug teaches: a burst addresses contiguously for its whole length with no boundary awareness, so start + length must be verified against the memory's size before every burst; a burst that starts near the end and is longer than the locations remaining overruns into the next region (8.2). The tell in the wild is a neighbour region corrupted with memory-looking data after an end-of-memory burst — that is an overrun, and the fix is a fit check, not a change to the memory or the neighbour.
7. Common Mistakes
- Not checking that a burst fits.
start + lengthmust be<= size; a burst near the end with a length past the last location overruns into the next region (8.2). - Assuming a burst is always a protocol burst. Whether it becomes one address + N beats or splits into singles is the adapter/bus decision — a slow burst is often a serialized one, not a bug.
- Testing bursts only from index 0. Start-at-zero bursts always fit and never reveal overruns — burst near the end to expose them.
- Mismatching the data array length to the intended run. The array length is the burst length; a wrong-size array writes/reads the wrong number of locations.
- Reading a neighbour corruption as unrelated. Memory-looking data in a following region after an end-of-memory burst is an overrun, not a separate bug.
8. Industry Best Practices
- Fit-check every burst. Verify
start + length <= sizebefore issuing; fail cleanly on overrun rather than corrupting the neighbour. - Burst to exercise realism, not just speed. Real traffic is bursty (DMA, caches); bursting hits the memory the way it will actually be used.
- Test bursts at the memory's boundaries. Start-at-end and full-length bursts surface overruns that start-at-zero bursts hide (8.5).
- Treat burst-vs-single as a performance question. If a burst serializes, look at the adapter and bus burst support; correctness is unaffected.
- Match the data array length to the intended range deliberately. The array size sets the burst length — size it to the block you mean to move.
9. Interview / Review Questions
10. Key Takeaways
- Burst access (
burst_read/burst_write) moves a contiguous block of locations in one call —values[0]atstart,values[1]atstart + 1, … — with the data array length as the burst length; it is both faster and how real (DMA/cache) traffic hits a memory. - Whether a burst becomes a true bus burst (one address + N beats) or splits into singles is the adapter/bus decision — a slow burst is usually a serialized one, a performance matter, not a correctness bug.
- A burst has no boundary awareness: it writes
startthroughstart + N - 1, so the correctness rule isstart + N <= size— the run must stay within the memory. - The signature bug is a burst overrun: starting near the end with a length past the last location runs the tail beats into the next region (8.2) and silently corrupts a neighbour.
- Guard with a fit check before every burst and test bursts at the memory's boundaries (8.5); a neighbour holding memory-looking data after an end-of-memory burst is an overrun — a dynamic range violation, distinct from a static placement overlap (8.2).