Skip to content

UVM RAL · Chapter 1 · Register Specification & CSR Thinking

Address Offsets & the Register Map

A register model that knows a register's fields but not its address cannot reach anything: the fields say what the bits mean, but the offset is what lets the testbench actually touch them. A register lives at an offset relative to the block's base address, and the sum, base plus offset, is the bus address a transaction must carry. The register map is the object that records every register's offset and turns an abstract register write into a concrete address on the bus. This page explains byte addressing, so a block of 32-bit registers steps by four, why registers are aligned to their width, and how to build a map and add registers to it. It ends with the wrong-offset bug where an access lands cleanly on the neighbouring register and the test passes while verifying the wrong thing.

Foundation11 min readUVM RALAddress MapOffsetuvm_reg_mapAlignmentadd_reg

Chapter 1 · Section 1.2 · Register Specification & CSR Thinking

1. Why Should I Learn This?

The fields tell you what a register means; the offset tells you where it is. Both halves have to be right, and the offset half fails in a uniquely nasty way: a wrong offset does not usually error out, it just reaches a different register — one that also reads and writes — so the transaction completes and the test is satisfied while verifying something entirely unintended. There is no exception, no UVM_NOT_OK, no red flag; just a green suite checking the wrong address.

Learning how offsets and maps work is what lets you avoid that silent failure, reason about why a register block is laid out the way it is, and build or read a uvm_reg_map with confidence. It is also the foundation for everything addressing-related later — multiple maps, hierarchical blocks, byte enables, endianness — so getting the base-plus-offset mental model solid now pays off through the rest of the track.

2. Industry Story — the copy-paste that shifted a block

An engineer builds the register map for a new block by copy-pasting the previous block's map and editing the register names and offsets. Most edits are correct, but one add_reg call keeps the pasted offset 0x0C when the new register belongs at 0x10. Both 0x0C and 0x10 are real, writable registers in the block, so nothing errors: every write/read to the mis-mapped register completes cleanly against its neighbour.

The suite goes green. The mis-mapped register — an interrupt-enable — is never actually verified, because every access to it is really hitting the adjacent status register, which happens to accept writes. Months later a firmware engineer discovers interrupts cannot be enabled on that block in silicon: the enable writes are going to the wrong address in the real driver too, but that is a software bug that RAL was supposed to make impossible to ship by verifying the register interface — and RAL did not catch it, because the model's map was wrong in the exact same direction. The post-mortem lesson: an offset error is invisible when the wrong address is also a valid register, so the map must be verified against the spec directly, not merely exercised.

3. Concept — base, offset, and the map that adds them

A register's bus address is a simple sum, but each part earns its place:

  • Base address — where the block sits in the system's address space (say 0x4000_0000). Set once per map. In a unit-level testbench the base is often 0x0, because you are addressing the block in isolation.
  • Offset — the register's byte address within the block, from the spec (0x00, 0x04, 0x08, …). This is what add_reg records.
  • Addressbase + offset, the value that actually goes on the bus.

Two facts the spec assumes you already know:

  • Byte addressing. On a byte-addressed bus, consecutive 32-bit (4-byte) registers sit at offsets 0x00, 0x04, 0x08, … — they step by the register width in bytes, not by one. Stepping by one would pack four registers into the space of one and overlap them.
  • Alignment. A register is aligned to its size — a 4-byte register at a multiple-of-4 offset — so a single bus transfer of the bus width lands exactly on it, with no split access and no ambiguity about which bytes belong to which register.

A block can also carry more than one map when it is reachable from more than one bus (a fast path and a debug path, say); each map gives the same registers addresses in its own address space. Here is a small block's map, drawn as the address space it defines:

Peripheral register map: ID at 0x10, INT_STATUS at 0x0C, INT_EN at 0x08, STATUS at 0x04, CTRL at 0x00, each a 4-byte registerA peripheral register map (offsets from block base)A peripheral register map (offsets from block base)HIGHHIGHLOWLOWID @ 0x10RO — device / version id0x10INT_STATUS @ 0x0CW1C — pending interrupts0x0CINT_EN @ 0x08RW — interrupt enables0x08STATUS @ 0x04RO — busy / done status0x04CTRL @ 0x00RW — enable, mode0x00
Figure 1 — a small peripheral register map. Five 32-bit registers at byte offsets 0x00 through 0x10, each stepping by 4 because the bus is byte-addressed and the registers are 4 bytes wide. The address a transaction carries is base + offset; in a unit testbench the base is often 0x0, so the offset is the address. Note the offsets are contiguous and 4-aligned — a wrong offset that is still a multiple of 4 lands cleanly on a neighbour, which is exactly why offset bugs hide.

4. Mental Model — the map is the register's street address

5. Working Example — building the map with create_map and add_reg

The block builds its registers (from 1.1) and then places them in a map. The map is where base, offset, bus width, and endianness are set, and where each register is added at its spec offset:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class periph_reg_block extends uvm_reg_block;
  `uvm_object_utils(periph_reg_block)
  rand ctrl_reg   ctrl;      // 0x00
  rand status_reg status;    // 0x04
  rand int_en_reg int_en;    // 0x08
  rand int_st_reg int_status;// 0x0C
       id_reg     id;        // 0x10
 
  function new(string name = "periph_reg_block");
    super.new(name, UVM_NO_COVERAGE);
  endfunction
 
  virtual function void build();
    // Create + build each register (elided: create(), configure(this), build()).
    ctrl = ctrl_reg::type_id::create("ctrl"); ctrl.configure(this); ctrl.build();
    // ... status, int_en, int_status, id likewise ...
 
    // The map: base address, bytes-per-bus-word, endianness.
    default_map = create_map("default_map", .base_addr('h0),
                             .n_bytes(4), .endian(UVM_LITTLE_ENDIAN));
 
    // Place each register at its SPEC offset. 4-byte regs step by 4 (byte-addressed).
    default_map.add_reg(ctrl,       .offset('h00), .rights("RW"));
    default_map.add_reg(status,     .offset('h04), .rights("RO"));
    default_map.add_reg(int_en,     .offset('h08), .rights("RW"));
    default_map.add_reg(int_status, .offset('h0C), .rights("RW"));
    default_map.add_reg(id,         .offset('h10), .rights("RO"));
 
    lock_model();   // freeze: offsets resolve to addresses now
  endfunction
endclass

At run time the sum is automatic — the test names a register, the map supplies the address:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// base_addr is 0x0 here, so address == offset.
periph.int_en.write(status, 32'h1);   // map drives a bus write to 0x08
periph.int_en.read (status, got);     // map drives a bus read  from 0x08, auto-checked
// If int_en had been added at 0x0C by mistake, BOTH lines would hit int_status
// instead — and complete without error. That is the next section.

The n_bytes(4) says the bus moves four bytes per word, which is why 32-bit registers are one transfer apart at four-byte offsets; lock_model() freezes the layout so addresses are fixed before any access runs.

6. Debugging Session — the offset that hits the neighbour

1

A register added at the wrong offset lands on a valid neighbour, so every access completes and the register is never really verified

WRONG OFFSET
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Spec: INT_EN is at offset 0x08. A copy-paste left it at 0x0C (INT_STATUS's slot).
default_map.add_reg(int_en, .offset('h0C), .rights("RW"));   // BUG: should be 'h08
// Now every int_en access is driven to address 0x0C — INT_STATUS's address.
periph.int_en.write(status, 32'h1);   // completes: writes 0x0C, not 0x08
periph.int_en.read (status, got);     // completes: reads  0x0C — no error raised
Symptom

Nothing looks wrong. The interrupt-enable test passes: writes complete, reads complete, and because INT_STATUS at 0x0C is a real writable register, the mirror often reconciles too. Meanwhile INT_EN at 0x08 is never touched by any test — its bits are never exercised, and if the RTL enable logic is broken, no register test can see it. The bug surfaces only when someone checks interrupts end-to-end and finds they never fire, or when a careful reviewer notices two registers claim overlapping behaviour. There is no error message pointing at the offset, which is what makes it expensive.

Root Cause

The register's recorded offset does not match the spec. Because the wrong offset (0x0C) is also a valid, writable register, the bus transaction succeeds — a wrong-but-valid address produces no bounce. This is the defining hazard of offset errors: unlike a wrong field width, which frequently mismatches on read-back, a wrong offset that lands on an accepting neighbour completes silently, so the suite stays green while INT_EN goes entirely unverified and INT_STATUS gets scribbled on by tests that think they are elsewhere.

Fix

Correct the offset to the spec value: default_map.add_reg(int_en, .offset('h08), .rights("RW"));. Then guard against the whole class of bug two ways. First, add a register-map check — the built-in bit-bash / address sequences (Chapter 7) walk the map and catch overlaps and unreachable registers by reading back a register's own identity, not just completing a transfer. Second, in review, verify each add_reg offset against the spec cell directly and confirm the offsets are contiguous and aligned — a duplicated or skipped offset is visible by inspection. The rule the bug teaches: a completing access proves the address is a register, never that it is the right one; verify the map against the spec, do not merely exercise it.

7. Common Mistakes

  • Stepping offsets by 1 instead of by the register width. On a byte-addressed bus, 32-bit registers sit at 0x00, 0x04, 0x08 — stepping by one overlaps them.
  • Copy-pasting a map and missing an offset edit. The classic source of a wrong-but-valid offset; every pasted add_reg must be re-checked against the new spec.
  • Misaligned registers. A 4-byte register at a non-multiple-of-4 offset is almost always a transcription error and can force split bus accesses.
  • Assuming a completing access means the right register. A wrong offset on a valid neighbour completes silently — exercise is not verification.
  • Forgetting lock_model(), so offsets never resolve to addresses.

8. Industry Best Practices

  • Run the built-in map/address sequences. Bit-bash and hw-reset sequences walk the map and catch overlaps, gaps, and unreachable registers that a hand-written test would miss.
  • Verify offsets against the spec in review. Each add_reg offset traces to a spec cell; confirm contiguity and alignment at a glance.
  • Prefer generated maps for large blocks. IP-XACT/RALF generation (Chapter 13) removes copy-paste offset errors entirely — the single biggest source of map bugs.
  • Keep base out of register offsets. Set the base once on the map; never bake it into individual add_reg offsets, or a base change breaks every register.
  • Model multiple maps when the block has multiple buses. Do not force two access paths through one map; give each its own.

9. Interview / Review Questions

10. Key Takeaways

  • A register's bus address is base + offset; the offset is the register's byte address within the block, straight from the spec.
  • On a byte-addressed bus, N-byte registers step by N — 32-bit registers sit at 0x00, 0x04, 0x08 — and are aligned to their size so one transfer lands exactly on them.
  • The uvm_reg_map records offsets (add_reg) and turns reg.write() into a real address; a block can carry multiple maps when reached from multiple buses.
  • A wrong offset that lands on a valid neighbour completes silently — the most dangerous map bug, because exercise is not verification.
  • Verify the map against the spec (traceable, contiguous, aligned offsets) and run address-walking sequences; never trust that a completing access hit the intended register.