AMBA AHB 2.0 VIP in SystemVerilog UVM: Bursts, Constraints, Coverage and Tests

AHB is where AMBA stops being a toy. Unlike APB, an AHB transfer is pipelined, comes in bursts of up to sixteen beats, wraps at power-of-two boundaries, can be stalled by the target and interrupted by the initiator, and may be answered with four different responses. A verification IP for it has to generate legal traffic under all of those rules, which makes it a good case study in constrained-random transaction modelling. This post walks through the AHB 2.0 UVM VIP on GitHub: what it supports, how the transaction constraints encode the protocol, what the sequence library and test list look like, what the coverage model measures, and one real bug in the constraints that has been sitting in the repository since 2015. If you want the gentler introduction to the same testbench structure, start with the APB VIP walkthrough.

AHB 2.0, AHB-Lite and AHB5

The AHB family has three generations that matter, and a VIP written for one is not automatically usable on another.

VersionSpecWhat it has
AHB 2.0AMBA 2 (IHI 0011)Multiple masters with an arbiter (HBUSREQ, HGRANT), SPLIT and RETRY responses, 2-bit HRESP
AHB-LiteAMBA 3 (IHI 0033)Single master, no arbitration, only OKAY and ERROR, 1-bit HRESP
AHB5AMBA 5 (IHI 0033B)Adds exclusive transfers (HEXCL, HEXOKAY), extended memory types, secure transfers (HNONSEC), multiple-layer support

This VIP targets AHB 2.0 in a single-master, single-slave configuration, so it exercises the transfer pipeline, bursts, wait states and responses, but not arbitration between masters. That is the useful subset for most block-level work, and it is also exactly the subset that AHB-Lite kept.

What the VIP supports

  • Single master and single slave, with a separate reset agent.
  • All eight burst types: SINGLE, INCR, WRAP4, INCR4, WRAP8, INCR8, WRAP16, INCR16.
  • Transfer sizes from byte to word, with addresses aligned to the size.
  • BUSY cycles inserted by the master mid-burst and HREADY wait states inserted by the slave.
  • OKAY and ERROR responses, with RETRY and SPLIT defined in the types but not driven.
  • Functional coverage on burst type, size, direction, response and their crosses.

ahb2_uvm_tb/
├── ahb_env/           ahb_env, env_config, ahb_coverage, ahb_vseqr, ahb_vseqs, top.sv
├── ahb_master_agent/  agent, config, driver, monitor, sequencer, sequence library, ahb_mxtn
├── ahb_slave_agent/   agent, config, driver, monitor, sequencer, sequences, ahb_sxtn
├── ahb_test/          base, reset, crt, incrx, wrapx, incrbusy, err tests, tb_defs.svh
├── reset_agent/       agent, driver, sequencer, sequences
├── rtl/               ahb_intf.sv
└── sim/               Makefile, run.pl, ahb_inc.f, testcases.txt, wave and radix scripts

Signals and transfer types

SignalWidthDescription
HCLK, HRESETn1Bus clock and active-low reset
HADDR32Address
HTRANS2Transfer type: IDLE, BUSY, NONSEQ, SEQ
HWRITE1Direction
HSIZE3Transfer size, BYTE to 1024-bit
HBURST3Burst type
HWDATA, HRDATA32Write and read data
HREADY1Slave ready; low inserts a wait state
HRESP2OKAY, ERROR, RETRY, SPLIT
HTRANS is the signal that makes AHB pipelined. The first beat of a burst is NONSEQ, later beats are SEQ, the master can insert BUSY beats when it is not ready to supply or accept data, and IDLE means no transfer. Address and control for beat N are on the bus during the data phase of beat N-1. The enumerations live in tb_defs.svh and are used by transactions, drivers, monitors and coverage alike:

typedef enum bit [1:0] {IDLE, BUSY, NONSEQ, SEQ}                              transfer_t;
typedef enum bit       {READ, WRITE}                                          rw_t;
typedef enum bit [2:0] {SINGLE, INCR, WRAP4, INCR4, WRAP8, INCR8, WRAP16, INCR16} burst_t;
typedef enum bit [2:0] {BYTE, HALFWORD, WORD, WORDx2, WORDx4, WORDx8, WORDx16, WORDx32} size_t;
typedef enum bit [1:0] {OKAY, ERROR, RETRY, SPLIT}                            resp_t;

Architecture

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#e0f2fe', 'primaryTextColor': '#0f172a', 'primaryBorderColor': '#0066cc', 'lineColor': '#475569', 'secondaryColor': '#f8fafc'}}}%%
flowchart TB
    subgraph TEST["ahb_test"]
        TC[Test cases]
        VSEQ[Virtual sequences]
    end

    subgraph ENV["ahb_env"]
        VSEQR([Virtual sequencer])
        subgraph MAGT["ahb_master_agent"]
            MDRV[Driver]
            MMON[Monitor]
            MSEQR([Sequencer])
        end

        subgraph SAGT["ahb_slave_agent"]
            SDRV[Slave driver]
            SMON[Slave monitor]
            SSEQR([Sequencer])
        end

        COV[Coverage subscriber]
    end

    subgraph RAGT["reset_agent"]
        RST[Reset driver]
    end

    IF{{ahb_intf}}

    TC --> VSEQ --> VSEQR
    VSEQR --> MSEQR --> MDRV
    VSEQR --> SSEQR --> SDRV
    MDRV -->|address, control, write data| IF
    IF -->|read data, HREADY, HRESP| MDRV
    SDRV <--> IF
    MMON -.-> COV
    RST -->|HRESETn| IF

    style IF fill:#d1fae5,stroke:#10b981,stroke-width:2px
    style COV fill:#fef3c7,stroke:#f59e0b,stroke-width:2px

The master agent and slave agent each have a sequencer, and a virtual sequencer in the environment coordinates them. A test starts a virtual sequence, which starts a master sequence and a slave sequence together, so the slave's wait-state and response behaviour is stimulus in its own right rather than a fixed model. The reset agent lets tests assert HRESETn in the middle of a burst, which is what ahb_reset_test does.

The transaction: a burst is one item

The master transaction represents a whole burst, not a beat. address, trans_type, write_data and busy are dynamic arrays whose sizes are decided by the burst type, and the constraints are where the protocol lives.


class ahb_mxtn extends uvm_sequence_item;
  rand bit          reset;
  rand transfer_t   trans_type[];
  rand bit [31:0]   address[];
  rand size_t       trans_size;
  rand burst_t      burst_mode;
  rand rw_t         read_write;
  rand bit [31:0]   write_data[];
  rand bit          busy[];
  rand int          no_of_busy;

  bit               ready;
  resp_t            response;
  bit [31:0]        read_data;

  // Beat count follows the burst type
  constraint addr {
    if (burst_mode == SINGLE)                       address.size == 1;
    if (burst_mode == INCR)                         address.size < (1024 / (2 ** trans_size));
    if (burst_mode == WRAP4  || burst_mode == INCR4)  address.size == 4;
    if (burst_mode == WRAP8  || burst_mode == INCR8)  address.size == 8;
    if (burst_mode == WRAP16 || burst_mode == INCR16) address.size == 16;
  }
  constraint min_size_limit { address.size > 0; }

  // A burst must not cross a 1 KB address boundary
  constraint kb_boundry {
    if (burst_mode == INCR)
      address[0][10:0] <= (1024 - address.size * (2 ** trans_size));
    if (burst_mode == WRAP4 || burst_mode == INCR4)
      address[0][10:0] <= (1024 - 4 * (2 ** trans_size));
    if (burst_mode == WRAP8 || burst_mode == INCR8)
      address[0][10:0] <= (1024 - 8 * (2 ** trans_size));
    if (burst_mode == WRAP16 || burst_mode == INCR16)
      address[0][10:0] <= (1024 - 16 * (2 ** trans_size));
  }

  // Every beat is aligned to the transfer size
  constraint word_boundary {
    if (trans_size == HALFWORD) foreach (address[i]) address[i][0]   == 1'b0;
    if (trans_size == WORD)     foreach (address[i]) address[i][1:0] == 2'b0;
  }
  // ... busy placement, data array sizing
endclass

Three protocol rules are encoded here, and each is a classic AHB interview question:

  • Beat count follows HBURST. Fixed-length bursts have 4, 8 or 16 beats. INCR is unbounded by the spec, so the model bounds it by the next rule.
  • No burst crosses a 1 KB boundary. AHB decoders work on 1 KB granularity, so a burst that crossed the line could straddle two slaves. The constraint places the start address so that the whole burst fits below the boundary. This is the rule most hand-written stimulus gets wrong.
  • Addresses are aligned to HSIZE. A halfword transfer has bit 0 clear, a word transfer has bits 1:0 clear, and so on. Wrapping bursts wrap within the aligned block of beats * size bytes, which is why alignment and the beat count interact.

The bug that has been there since 2015

The version in the repository writes the INCR bound as address.size < (1024/(2^trans_size)). In SystemVerilog ^ is XOR, not exponentiation; power is . With trans_size ranging 0 to 7, 2 ^ trans_size evaluates to 2, 3, 0, 1, 6, 7, 4, 5, so for WORD transfers (trans_size = 2) the expression divides by zero and the constraint is silently unsatisfiable or, on some solvers, dropped. The other constraints in the same file use correctly, which is how the typo survived: the fixed-length bursts, which every test exercises, were fine, and only unbounded INCR bursts of word size were affected. The listing above shows the corrected form. The lesson is the usual one: a constraint that never fails randomization is not evidence that it is right. Add a coverpoint on the thing you constrained.

Sequences and the test list

The master sequence library builds up from a base class, each sequence randomizing one burst with an inline constraint that selects the corner of interest:


class ahb_wrapx_mseq extends ahb_mbase_seq;
  `uvm_object_utils(ahb_wrapx_mseq)
  task body();
    req = ahb_mxtn::type_id::create("req");
    start_item(req);
    assert(req.randomize() with {
      burst_mode inside {WRAP4, WRAP8, WRAP16};
    });
    finish_item(req);
  endtask
endclass

Tests select sequences through the virtual sequencer, and sim/testcases.txt is the regression list:

TestExercises
ahb_reset_testReset asserted mid-transfer; driver and monitor recover
ahb_crt_testFully constrained-random bursts, all types and sizes
ahb_incrx_testINCR4, INCR8, INCR16 fixed-length increments
ahb_wrapx_testWRAP4, WRAP8, WRAP16 wrapping bursts
ahb_incrbusy_testIncrements with master BUSY beats inserted
ahb_err_testSlave returns ERROR; master terminates the burst

Coverage: what the VIP measures

The coverage subscriber samples every monitored master transaction. The crosses are the interesting part, because a burst type that only ever appeared with one size is a hole that a per-signal coverpoint would hide.


covergroup ahb_cg;
  option.per_instance = 1;
  RST:   coverpoint ahb_xtn.reset;
  WR:    coverpoint ahb_xtn.read_write;
  TRANS: coverpoint ahb_xtn.trans_type[0];
  SIZE:  coverpoint ahb_xtn.trans_size { bins s[] = {[BYTE:WORD]}; }
  BURST: coverpoint ahb_xtn.burst_mode;
  ADDR:  coverpoint ahb_xtn.address[0] { option.auto_bin_max = 32; }
  RESP:  coverpoint ahb_xtn.response   { bins rsp[] = {OKAY, ERROR}; }
  RDY:   coverpoint ahb_xtn.ready;
  WRxSIZE:       cross WR, SIZE;
  BURSTxSIZE:    cross BURST, SIZE;
  WRxBURST:      cross WR, BURST;
  WRxBURSTxSIZE: cross WR, BURST, SIZE;
endgroup

What it does not measure is as informative as what it does. There is no coverpoint for the 1 KB boundary case, so the constraint bug above could never have been caught by coverage. There is no bin for RETRY or SPLIT, which is honest, since the slave never drives them. And HPROT is commented out in the transaction, so protection attributes are neither driven nor covered. A coverage model is a statement of what you claim to have verified; reading it this way is a habit worth building.

Running it

The sim directory carries a Makefile with per-test targets and a Perl runner that loops over testcases.txt, both written for QuestaSim:


cd sim
make run_test3                    # one test, coverage saved and reported as HTML
perl run.pl                       # every test in testcases.txt, random seed each

Each target compiles with vlog, runs vsim -c -coverage -sv_seed random +UVM_TESTNAME=, and calls vcover report -html. The UVM quick start explains the switches if the flow is new to you.

What I would change today

  • Fix the ^ typo and add a coverpoint that bins address[0][10:0] near the 1 KB boundary so the fix is observable.
  • Add a scoreboard. The master monitor and slave monitor both reconstruct the same beats; comparing them is a twenty-line uvm_scoreboard and makes the VIP self-checking instead of coverage-only.
  • Drive RETRY and SPLIT from the slave sequence, or delete them from the enum. A response type that exists in the types but never on the wire is a false promise to the next user.
  • Protocol assertions in the interface. AHB has crisp rules that are easy to assert: control signals stable while HREADY is low, SEQ only after NONSEQ or SEQ, no BUSY on a SINGLE, and the 1 KB rule itself. An interface with assertions catches driver bugs at the source instead of in a scoreboard mismatch three beats later.
  • Move to AHB5 signal names if the VIP will meet modern RTL. The transfer pipeline is unchanged, so the driver and monitor survive; the interface and the response handling are what grow.

Key takeaways

  • An AHB burst is one transaction, and the protocol rules become constraints: beat count from HBURST, no 1 KB boundary crossing, alignment to HSIZE.
  • ^ is XOR in SystemVerilog. Power is **. A constraint with a typo can pass every regression for years if nothing covers the case it governs.
  • Master and slave are both agents with sequencers, coordinated by a virtual sequencer, so wait states and responses are stimulus you control.
  • Read a coverage model for what it omits. Here: boundary cases, RETRY, SPLIT and HPROT.
  • AHB 2.0's single-master subset is what AHB-Lite standardized and what AHB5 extends. A VIP built on that subset ports forward with an interface change.

Source: ahb2_uvm_tb on GitHub. Protocol references: AMBA AHB Protocol Specification (Arm IHI 0033), AMBA 2 Specification (IHI 0011).

Author
Mayur Kubavat
DV engineer working on SoC verification. Writes here about UVM, PCIe, SystemVerilog, and the everyday craft of getting designs to tape-out.

Comments (0)

Leave a Comment