The UVM memory allocation manager, uvm_mem_mam, hands out exclusive blocks of memory, called regions, from one address space, in the same way that malloc() and free() hand out memory in C. A test asks it for a region of a given size, gets back a uvm_mem_region that is guaranteed not to overlap any other region in use, uses it, and releases it when it is done. It exists so that testbench code that needs buffers, such as DMA transfers, never has to track addresses by hand.

It is optional. A testbench with a handful of fixed buffers can simply use fixed addresses. The manager earns its place when buffers are allocated dynamically and randomly, by several agents, sequences or tests that must not step on each other.

9 min read | Intermediate

What You'll Learn

  • Explain what the memory allocation manager does and what it does not do
  • Create a manager, then request, use, reserve and release regions
  • Constrain where regions are placed, for example aligned to a cache line
  • Recognise the situations where the manager is worth using

The Problem It Solves

Take a DMA engine. A test configures a transfer by writing a source address, a destination address and a length into registers, and the engine copies data through system memory. To test it properly, the addresses should be random, so that over many runs the engine meets buffers at every alignment and position. But random addresses can collide: if the destination buffer overlaps the source buffer, the copy corrupts its own input, and the test fails for a reason that has nothing to do with the DUT.

With several DMA channels running at once, or a firmware image occupying part of the memory, the problem grows. Every sequence would need to know every other sequence's buffers. A shared allocator solves this once: every buffer comes from the same manager, so by construction no two live regions overlap.

How It Works

Mam regions

One uvm_mem_mam instance manages one contiguous address space, from offset 0 to a configured end offset. It remembers every region that is currently allocated. A new request is placed at a random offset where it fits inside the space and does not overlap an existing region. A region stays reserved until it is explicitly released.

The manager is pure bookkeeping. It never drives the DUT, and it never holds data. If a uvm_mem from the register model is attached to it, the regions can also read and write that memory, but the actual accesses are performed by the usual register and memory machinery.

Syntax


// Describe the managed space
class uvm_mem_mam_cfg;
  rand int unsigned             n_bytes;      // bytes in each memory location
  rand bit [63:0]               end_offset;   // last managed location
  rand uvm_mem_mam::alloc_mode_e mode;        // GREEDY (the only implemented mode)
  rand uvm_mem_mam::locality_e   locality;    // BROAD or NEARBY
endclass

// The manager
function new(string name, uvm_mem_mam_cfg cfg, uvm_mem mem = null);
function uvm_mem_region request_region(int unsigned n_bytes, uvm_mem_mam_policy alloc = null, ...);
function uvm_mem_region reserve_region(bit [63:0] start_offset, int unsigned n_bytes, ...);
function void           release_region(uvm_mem_region region);
function void           release_all_regions();
function string         convert2string();

// A region
function bit [63:0] get_start_offset();
function bit [63:0] get_end_offset();
function int unsigned get_len();             // number of locations
task write(output uvm_status_e status, input uvm_reg_addr_t offset, input uvm_reg_data_t value, ...);
task read (output uvm_status_e status, input uvm_reg_addr_t offset, output uvm_reg_data_t value, ...);
function void release_region();

Two units are easy to confuse. Sizes passed to request_region() and reserve_region() are in bytes. Offsets, such as a region's start offset, are in memory locations, and n_bytes in the configuration says how many bytes each location holds. A region of 256 bytes in a 4-byte-wide memory therefore occupies 64 locations.

BROAD locality scatters new regions randomly across the whole space, which is usually what you want for stress. NEARBY places new regions next to existing ones, which packs the memory tightly. The THRIFTY allocation mode, which would reuse released space, is documented as not yet implemented, so use GREEDY.

Example: A DMA Test

The system memory is a uvm_mem called dram in the register model: 64K locations of 32 bits, so 256 KB. The first 64 locations hold boot code the DMA must not overwrite.

Creating the manager


class dma_env extends uvm_env;
  `uvm_component_utils(dma_env)
  dma_reg_block m_ral;                     // contains the uvm_mem called dram
  uvm_mem_mam   m_mam;
  // Rest of the code (constructor, agents)

  virtual function void build_phase(uvm_phase phase);
    uvm_mem_mam_cfg cfg = new;
    super.build_phase(phase);
    cfg.n_bytes    = 4;                    // 32-bit locations
    cfg.end_offset = 64*1024 - 1;          // last valid location
    cfg.mode       = uvm_mem_mam::GREEDY;
    cfg.locality   = uvm_mem_mam::BROAD;
    m_mam = new("dram_mam", cfg, m_ral.dram);   // attach the memory so regions can read/write it

    void'(m_mam.reserve_region(0, 64*4));       // boot code: 64 locations, never handed out
  endfunction
endclass

reserve_region() creates a fixed region at a chosen offset. It returns null if the area is outside the managed space or overlaps an existing region. Reserving regions like this punches permanent holes in the space, here for the boot code.

Allocating buffers in a sequence

The sequence asks the manager for a source and a destination buffer, fills the source, programs the DMA registers with the buffers' bus addresses, and checks the destination afterwards:


class dma_copy_seq extends uvm_sequence #(bus_item);
  `uvm_object_utils(dma_copy_seq)
  uvm_mem_mam     m_mam;                   // set by the test
  dma_reg_block   m_ral;
  // Rest of the code (constructor)

  virtual task body();
    uvm_mem_region src, dst;
    uvm_status_e   status;
    uvm_reg_data_t rd;
    int unsigned   bytes = 256;                     // transfer size in bytes
    int unsigned   words = bytes / 4;               // in 32-bit locations

    src = m_mam.request_region(bytes);
    dst = m_mam.request_region(bytes);
    if (src == null || dst == null) `uvm_fatal("MAM", "No room for the DMA buffers")

    // Fill the source buffer
    for (int i = 0; i < words; i++)
      src.write(status, i, $urandom, .parent(this));

    // Program the DMA with the buffers' bus addresses
    m_ral.src_addr.write(status, m_ral.dram.get_address(src.get_start_offset()), .parent(this));
    m_ral.dst_addr.write(status, m_ral.dram.get_address(dst.get_start_offset()), .parent(this));
    m_ral.length.write  (status, bytes, .parent(this));
    m_ral.ctrl.write    (status, 32'h1, .parent(this));     // start

    // ... wait for the done interrupt or status bit ...

    for (int i = 0; i < words; i++) begin
      src.read(status, i, rd, .parent(this));
      // compare against dst.read(...) at the same offset
    end

    m_mam.release_region(src);                       // give the space back
    m_mam.release_region(dst);
  endtask
endclass

The sequence never calls a random function for an address and never checks for overlap. request_region() returned two regions that are guaranteed apart, in different places on every run. region.write(status, i, ...) addresses word i of the region, so the code doesn't care where in the memory the region ended up.

The bus addresses come from the memory itself: get_address() converts a location offset into the physical address through the register map, which includes the memory's base address in the system.

Quick Check

Q: The test runs thousands of copies without releasing any region. What eventually happens, and how does the sequence find out?

Show Answer

The space fills up. request_region() returns null when no gap is large enough, and the sequence's check for null reports it. The cause is a region leak, so every path out of a sequence, including error paths, must release what it requested.

Controlling Where Regions Go

A region's start offset is chosen by randomizing a uvm_mem_mam_policy object. The manager's default policy only keeps regions inside the space and clear of one another. Real hardware often has more demanding rules: DMA buffers aligned to a cache line, a descriptor table that must sit inside one 4 KB page, or buffers restricted to a particular half of the memory. Extend the policy with constraints on start_offset:


class aligned_policy extends uvm_mem_mam_policy;
  int unsigned align = 16;                         // in locations: 16 x 4 bytes = 64-byte cache line
  constraint c_align { start_offset % align == 0; }
endclass

// In a sequence
aligned_policy pol = new;
uvm_mem_region buf = m_mam.request_region(1024, pol);   // 1024 bytes, cache-line aligned

The policy also carries min_offset, max_offset and in_use, the managed range and the regions already allocated, so a constraint or pre_randomize() can depend on them. If the constraints cannot be satisfied, for example an alignment that no free gap can honour, request_region() returns null, just as when the memory is full.

Real-World Uses

  • DMA and scatter-gather engines. Source buffers, destination buffers and descriptor tables are allocated per transfer, with random sizes and alignments, and released when the transfer completes. Chained descriptors work the same way: each descriptor and its buffers is another region.
  • Several masters sharing one memory. Two DMA channels and a CPU model all request from the same manager, so their buffers cannot collide even though nothing coordinates their sequences.
  • Memory with fixed contents. Reserving the area holding a firmware image, a boot ROM or memory-mapped peripherals keeps random buffers out of it.
  • Network and storage controllers. Receive and transmit rings and packet buffers are allocated and freed continuously, which is exactly the allocation pattern the manager models.
  • Memory-leak checks. At the end of the test, convert2string() lists every region still allocated, so a testbench that forgot to release a buffer can be caught instead of silently shrinking the available space.

Call release_all_regions() at the start of each test if tests share one environment and run back to back, so one test's leftovers don't affect the next.

When Not to Use It

If a test uses a small, fixed set of buffers, plain constants are simpler and easier to read. The same goes for a design with a single master and one buffer at a time: there is nothing to collide with. The manager also won't help with data checking. It tracks where buffers are, not what they hold, so a scoreboard or the register model's memory access still has to verify the contents.

Common Mistakes

Mistake #1: Ignoring a null region

A request that cannot be satisfied returns null, not an error. Using the handle without checking it ends in a null object access on the next line. Check every return value.

Mistake #2: Mixing bytes and locations

Sizes are in bytes and offsets are in locations. Passing a size in words, or treating a start offset as a byte address, shifts every buffer. Convert with get_address() or the n_bytes of the configuration.

Mistake #3: Forgetting to release regions

Regions stay reserved until released, so a leak slowly fills the space and later requests start to fail. Release on every exit path, and print convert2string() at the end of the test.

Mistake #4: Several managers over the same memory

Each manager knows only its own regions. Two managers on one memory can hand out overlapping buffers. Create one manager per memory and share the handle through the environment or a configuration object.

Summary

In this article, you learned:

  • uvm_mem_mam allocates non-overlapping regions from one address space, like malloc() and free(), and does no DUT access itself
  • request_region() picks a random free place, reserve_region() fixes one, and release_region() returns it
  • Sizes are in bytes and offsets are in memory locations
  • An extended uvm_mem_mam_policy adds alignment or placement rules
  • It pays off with dynamic, random buffers from several agents, such as DMA tests, and isn't needed for a few fixed buffers
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5