Skip to content

UVM RAL · Chapter 12 · Advanced RAL

Multi-Bus SoC RAL

In a real SoC the same registers are often reachable from more than one bus master. A peripheral might be accessed by the CPU over an APB fabric and by a DMA engine or debug controller over an AXI fabric, at possibly different addresses and always through different adapters. RAL models this with multiple address maps over one register model. You build one model with one set of registers and one mirror, then give it several maps, each a distinct view with its own base offsets, endianness, adapter, and sequencer onto the same shared registers. Two ideas carry it: the registers and mirror are shared, and every access must specify which map, since the same register has a different address and adapter on each bus. Because the mirror is shared, a write on one bus is visible from the other. This page breaks the bug where an access omits the map and goes out the default, wrong bus.

Foundation12 min readUVM RALmulti-busSoCaddress mapsper-map adapter

Chapter 12 · Section 12.7 · Advanced RAL

1. Why Should I Learn This?

Real SoCs expose registers on multiple buses, and modelling that correctly means one register model with per-bus maps and adapters — and specifying which map on every access. Knowing that the registers/mirror are shared across maps (so a write on one bus is visible on the other) and that an access without a map silently uses the default (wrong) bus is what keeps SoC-scale register accesses going out the intended fabric.

Learning multi-bus SoC RAL closes the advanced chapter by extending maps (Chapter 3) and adapters (Chapter 5) to SoC scale — it is how a single register model serves a CPU path and a DMA/debug path over one shared set of registers.

2. Industry Story — the CPU test that drove the DMA bus

A peripheral's registers are reachable two ways: from the CPU over APB and from a DMA/debug controller over AXI, modelled as one register model with an APB map and an AXI map (each with its own adapter). A CPU-path test writes a config register — but the write() was issued without specifying the map, so it fell back to the default map (AXI). The write went out the AXI fabric, not APB.

The test thought it was exercising the CPU path, but it silently drove the DMA/debug path — wrong address view, wrong adapter, wrong bus. The register did get written (via AXI), so it did not obviously fail, but the CPU-path verification the test intended never happened, and any APB-specific behaviour (APB timing, APB-only access restrictions) went untested. It surfaced later as a CPU-path bug that 'passed' in regression — because regression had been driving AXI. The fix was to specify the map on the access (.map(apb_map)) so it went out the intended bus. The post-mortem lesson: in a multi-bus model, an access must specify which map (which bus path), because the same register has a different address and adapter per bus — an access without a map falls back to the default map and silently goes out the wrong bus, so it verifies a path you did not intend while the intended path goes untested; always specify the map on a multi-map access.

3. Concept — one model, per-bus maps and adapters, specify the map per access

Multi-bus RAL is one register model reached by several per-bus maps. The essentials:

  • One model, many maps. Build one register model (one set of registers, one shared mirror). Add multiple address maps — one per bus master/fabric. The registers are not duplicated; each map is a view/path onto them.
  • Each map is a per-bus view with its own adapter. A map carries its own base offsets, endianness (Chapter 3), and its own adapter and sequencer (Chapter 5) — so the APB map reaches the registers at APB addresses via the APB adapter, the AXI map at AXI addresses via the AXI adapter.
  • Specify the map per access. Because the same register has a different address and adapter on each bus, an access must say which map: reg.write(status, value, .map(apb_map)). Omitting it uses the default map.
  • The mirror is shared (understand it). Since the registers/mirror are shared, an access via one bus updates the mirror seen by the other — a write via APB is visible via AXI. This is correct (one register), but must be understood when reasoning across buses.
  • The bug is a missing/wrong map. No .map() (or the wrong one) falls back to the default -> the access goes out the wrong bus (wrong address/adapter), or coverage/prediction is mis-attributed.

Here is one register model reached by two per-bus maps with their own adapters:

One register model with an APB map and an AXI map, each with its own adapter, both reaching the shared registersAPB pathAXI pathwhich bus?APB map (view)APB base/endian + APBadapter+sequencerONE register modelshared registers + sharedmirrorAXI map (view)AXI base/endian + AXIadapter+sequencerspecify the map peraccess.map(apb_map) vs.map(axi_map) — omitting ->DEFAULT (wrong bus)12
Figure 1 — multi-bus SoC RAL: ONE register model, MANY maps. The registers and their mirror are SHARED (one set of hardware). The APB map is one view (APB base offsets, endianness) reaching them through the APB adapter+sequencer; the AXI map is another view through the AXI adapter+sequencer. An access must specify WHICH map (.map(apb_map) vs .map(axi_map)) because the same register has a different address/adapter per bus. Omitting the map falls back to the DEFAULT map -> the access goes out the WRONG bus. A write via one bus updates the SHARED mirror seen by the other — correct, since it is the same register.

4. Mental Model — the registers are one building with two entrances; you must say which door

5. Working Example — one model, per-bus maps, map-specified accesses

Build one model with an APB map and an AXI map (each its own adapter), and specify the map per access:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ONE register model, TWO per-bus maps — each its OWN base offsets and its OWN adapter/sequencer.
class periph_reg_block extends uvm_reg_block;
  uvm_reg_map apb_map, axi_map;
  virtual function void build();
    // Shared registers built ONCE (one set of hardware, one mirror) ...
    apb_map = create_map("apb_map", 'h0000, 4, UVM_LITTLE_ENDIAN);   // CPU/APB view (its base/endian)
    axi_map = create_map("axi_map", 'h8000, 4, UVM_LITTLE_ENDIAN);   // DMA/AXI view (different base)
    apb_map.add_reg(status, 'h0004);   // same register, APB address
    axi_map.add_reg(status, 'h0004);   // same register, AXI address (view onto the SAME reg)
  endfunction
endclass
// Connect each map to ITS OWN adapter + sequencer (Ch5):
apb_map.set_sequencer(apb_sqr, apb_adapter);
axi_map.set_sequencer(axi_sqr, axi_adapter);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SPECIFY THE MAP per access — the register is shared, but the BUS PATH is per-map.
uvm_status_e s;
status.write(s, 32'hCAFE, .map(apb_map));   // goes out the CPU/APB path (APB address + APB adapter)
status.read (s, v,        .map(axi_map));   // reads via the DMA/AXI path (AXI address + AXI adapter)
// Omitting .map() would use the DEFAULT map -> possibly the WRONG bus (next section).
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SHARED mirror: a write via one bus is visible via the other, because it is the SAME register.
status.write(s, 32'hABCD, .map(apb_map));   // write via APB
status.read (s, v,        .map(axi_map));   // read via AXI -> v == 0xABCD (same register, shared mirror)
// This is CORRECT — the two maps are routes to ONE register, not two independent registers.

Specifying the map sends each access out the intended bus; the shared mirror makes a write via one bus visible via the other (correct, one register). Omitting the map — the bug of the next section — silently drives the default bus.

6. Debugging Session — an access that omits the map and drives the wrong bus

1

An access without a specified map falls back to the default map, so it silently goes out the wrong bus, verifying a path the test did not intend while the intended path goes untested

SPECIFY THE MAP PER ACCESS
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A CPU-path test writes a config register but does NOT specify the map (default map is axi_map):
cfg_reg.write(s, 32'h0000_0001);   // BUG: no .map() -> falls back to the DEFAULT map (AXI), not APB
// The test INTENDS the CPU/APB path, but the write silently goes out the AXI (DMA/debug) fabric:
// wrong address view, wrong adapter, wrong bus. The APB path the test exists to verify is NOT exercised.
Symptom

The test passes — the register does get written (via AXI) and reads back correctly — so nothing obviously fails. But the CPU/APB path the test was written to verify was never exercised: the access silently went out the AXI fabric via the AXI adapter. Any APB-specific behaviour (APB timing, APB-only access rules, APB error responses) goes untested, and a CPU-path bug can 'pass' regression because regression was driving AXI. The tell is subtle — the test succeeds while verifying the wrong path — and is visible only if you check which bus the access actually drove (which adapter/sequencer saw the transaction).

Root Cause

In a multi-bus model, an access must specify which map (which bus path), because the same register has a different address and adapter on each bus. Here the write() omitted the .map() argument, so RAL fell back to the model's default map — which was the AXI map — and drove the access out the AXI fabric (AXI address view, AXI adapter, AXI sequencer). The test intended the APB path (the CPU path), but because it did not name the map, it silently exercised the default (AXI) path instead. The register itself is shared, so the write succeeded (via AXI) and the shared mirror updated, which is why the test passed — it verified a path, just not the intended one. This is not a register, adapter, or mirror bug (all are correct); it is a map-selection bug specific to multi-bus models: the access defaulted to the wrong bus because the map was unspecified. The consequence is silent mis-verification — the intended (APB) path, and its bus-specific behaviour, went untested while the test reported success.

Fix

Specify the map on the access: cfg_reg.write(s, value, .map(apb_map)), so it goes out the intended CPU/APB path (APB address + APB adapter). Audit multi-bus accesses to ensure each names its map, and consider verifying which adapter/sequencer saw each transaction at bring-up so a default-map fallback is caught. The rule the bug teaches: in a multi-bus model an access must specify which map (which bus path), because the same register has a different address and adapter per bus — an access without a .map() falls back to the default map and silently goes out the wrong bus, verifying a path you did not intend while the intended path (and its bus-specific behaviour) goes untested. The tell: a multi-bus test that passes but drove the wrong fabric — check which map/adapter actually carried the access, and name the map on every multi-bus access.

7. Common Mistakes

  • Omitting the map on a multi-bus access. It falls back to the default map and silently drives the wrong bus — specify .map(...) on every multi-bus access.
  • Assuming the intended bus is the default. The default map may be a different bus than you mean — name the map rather than relying on the default.
  • Thinking the maps are separate register copies. One model, many maps — the registers and mirror are shared; a write via one bus is visible via the other (correct).
  • Duplicating the registers per bus. Build one register model and add per-bus maps; do not create separate register sets per fabric.
  • Not verifying which bus an access drove. A passing test can be driving the wrong fabric — check which adapter/sequencer saw the transaction.

8. Industry Best Practices

  • Model one register set, many per-bus maps. One model (shared registers/mirror); a map per bus master, each with its own base/endianness and its own adapter/sequencer.
  • Specify the map on every multi-bus access. Name the bus path (.map(apb_map) / .map(axi_map)) — never rely on the default.
  • Understand the shared mirror is cross-bus. A write via one bus is visible via another because it is the same register — reason about buses as routes to shared registers.
  • Verify the driven bus at bring-up. Confirm which adapter/sequencer carried an access so a default-map fallback (wrong bus) is caught.
  • Exercise each bus path deliberately. Bus-specific behaviour (timing, access rules, error responses) is only tested if you use that map.

9. Interview / Review Questions

10. Key Takeaways

  • In an SoC the same registers are reachable from multiple buses (CPU via APB, DMA/debug via AXI) at possibly different addresses and through different adapters — modelled as one register model with multiple address maps.
  • One model, many maps: the registers and mirror are shared (one set of hardware); each map is a per-bus view/path with its own base offsets, endianness, and its own adapter + sequencer (extends Ch3 maps + Ch5 adapters to SoC scale).
  • An access must specify which map (.map(apb_map) / .map(axi_map)), because the same register has a different address and adapter per bus — the register names which register, the map names which bus path.
  • The signature bug is an access with no .map(): it falls back to the default map and silently goes out the wrong bus — the test passes (the shared register is written) while the intended path and its bus-specific behaviour go untested (silent mis-verification).
  • The shared mirror is cross-bus: a write via one bus is visible via the other because it is the same register — this is correct, not cross-talk; reason about buses as routes to shared registers, always name the map, and verify which fabric an access drove.