An AHB slave has many more ways to fail than an APB slave. Wait states, bursts, wrapping addresses and narrow transfers on byte lanes all multiply the number of cases. Directed tests cannot cover them all, so AHB verification relies on random bursts, a reference model that works at byte level, and protocol checks that run on every clock edge. This article builds that testbench in plain Verilog and uses it to find a byte-lane bug in the memory slave from Designing an AHB-Lite Memory Slave.

9 min read | Intermediate

What You'll Learn

  • Generate random AHB bursts of mixed types and sizes
  • Check narrow transfers with a byte-level reference model
  • Write AHB protocol checks for wait states, bursts and ERROR responses
  • Map the testbench onto a UVM AHB agent and onto formal properties

The Testbench

The pieces are the same as for APB in Verifying an APB Slave: stimulus, a master, a model, a scoreboard and protocol checks. What changes is the detail. The master is the pipelined command-list engine from AHB Transfers and Pipelining, the slave inserts two wait states on the first beat of a burst and one on each following beat, and every check has to understand byte lanes.

Random Bursts

The stimulus builds a list of 60 bursts. Each one is a SINGLE, an INCR4 or a WRAP4, of bytes, halfwords or words, at a random aligned address. The first half are all writes, so that the memory holds interesting data, and the rest are random writes and reads:


task random_list(input integer count);
  integer    i;
  integer    kind;
  reg [2:0]  sz;
  reg [31:0] a;
  reg        w;
  begin
    for (i = 0; i < count; i = i + 1) begin
      kind = {$random(seed)} % 3;
      sz   = {$random(seed)} % 3;                   // byte, halfword or word
      a    = ({$random(seed)} % 'h2C0) & ~((1 << sz) - 1);
      w    = (i < count / 2) ? 1'b1 : ($random(seed) & 1);
      if (kind == 0)
        burst(w, SINGLE, a, sz, 1, $random(seed));
      else if (kind == 1)
        burst(w, INCR4, a, sz, 4, $random(seed));
      else
        burst(w, WRAP4, a, sz, 4, $random(seed));
    end
  end
endtask

The address is masked to the transfer size, so every beat is aligned, and kept below 0x2C0 so that no burst reaches the read-only region or crosses the 1 KB boundary. Constraining the stimulus to legal traffic is part of the job: a random test that breaks the protocol only finds bugs in itself.

A Byte-Level Model and Scoreboard

A word-wide model cannot check a byte write properly, because the other three bytes of the word must not change. The model is therefore one entry per byte. When a data phase completes, the testbench calls check_beat, which visits the four byte lanes and handles only the active ones:


reg [7:0] model [0:1023];                // one entry per byte of the slave

task check_beat(input integer c);
  integer b;
  reg [9:0] a;
  begin
    beats = beats + 1;
    for (b = 0; b < 4; b = b + 1) begin
      a = (c_addr[c][9:0] & ~10'd3) + b;
      if (a >= c_addr[c][9:0] && a < c_addr[c][9:0] + (1 << c_size[c])) begin    // active byte lane
        if (c_write[c])
          model[a] = c_wdata[c][8*b +: 8];
        else if (HRDATA[8*b +: 8] !== model[a]) begin
          $display("%0t  MISMATCH read 0x%h byte %0d: got 0x%h, expected 0x%h",
                   $time, c_addr[c][15:0], b, HRDATA[8*b +: 8], model[a]);
          errors = errors + 1;
        end
      end
    end
  end
endtask

A lane is active when its byte address lies between the transfer address and the address plus the transfer size. For a halfword at 0x232, that is lanes 2 and 3. Writes update the model only on those lanes; reads compare only those lanes, since the slave may return anything on the others.

Quick Check

Q: Why are the model updates done when the data phase completes rather than when the address is accepted?

Show Answer

Because the write data only exists in the data phase, and a transfer is not finished until HREADY is high. A transfer that ends with ERROR must not update the model at all, and that is only known at the end of the data phase.

Protocol Checks

The checker samples the bus on every rising edge and compares it with the previous edge. Each check is a rule from the earlier articles:


always @(posedge HCLK) begin
  if (HRESETn) begin
    // While a transfer waits, the master holds its address and control
    // (after the first ERROR cycle it may change them)
    if (!p_ready && !p_resp && p_trans[1] && (HTRANS != p_trans || HADDR != p_addr || HWRITE != p_write ||
                                              HSIZE != p_size || HBURST != p_burst)) begin
      $display("%0t  PROTOCOL: address or control changed during a wait state", $time);
      errors = errors + 1;
    end
    // ... and its write data
    if (!p_ready && p_dwrite && HWDATA != p_wdata) begin
      $display("%0t  PROTOCOL: HWDATA changed during a wait state", $time);
      errors = errors + 1;
    end
    // Transfers are aligned to their size
    if (HTRANS[1] && (HADDR & ((1 << HSIZE) - 1)) != 0) begin
      $display("%0t  PROTOCOL: unaligned address 0x%h", $time, HADDR);
      errors = errors + 1;
    end
    // A SEQ beat continues from the previous beat (incrementing or wrapping)
    if (HREADY && HTRANS == SEQ) begin
      span = (1 << HSIZE) * ((HBURST == WRAP4) ? 4 : (HBURST == WRAP8) ? 8 : 16);
      next = last_addr + (1 << HSIZE);
      if (HBURST == WRAP4 || HBURST == WRAP8 || HBURST == WRAP16)
        next = (last_addr & ~(span - 1)) | (next & (span - 1));
      if (HADDR != next) begin
        $display("%0t  PROTOCOL: SEQ address 0x%h, expected 0x%h", $time, HADDR, next);
        errors = errors + 1;
      end
    end
    // An ERROR response takes two cycles: HREADY low, then HREADY high
    if (p_resp && !p_ready && !(HRESP && HREADY)) begin
      $display("%0t  PROTOCOL: ERROR response not completed in its second cycle", $time);
      errors = errors + 1;
    end
    // ... then remember this edge in the p_ registers and last_addr
  end
end

The first three checks and the SEQ check watch the master; the ERROR check watches the slave. The hold check deliberately ignores IDLE and BUSY in the previous cycle, because the protocol lets a master change those during a wait (IDLE to NONSEQ, BUSY to SEQ). A full checker adds more rules in the same style: IDLE and BUSY get a zero-wait OKAY, HRESP is OKAY during wait states, a fixed-length burst ends with SEQ, and no incrementing burst crosses 1 KB.

Finding the Bug

The slave in the example has a one-line bug. With the correct slave, the test passes:

Output
PASS: 186 beats checked, no errors

With the buggy one, it fails:

Output
3165  MISMATCH read 0x0230 byte 2: got 0x00, expected 0xf2
3165  MISMATCH read 0x0230 byte 3: got 0x00, expected 0xa2
3325  MISMATCH read 0x0260 byte 2: got 0x00, expected 0xb5
3325  MISMATCH read 0x0260 byte 3: got 0x00, expected 0xee
...
FAIL: 17 errors in 186 beats

Every early mismatch is in bytes 2 and 3, the upper halfword, and the slave returned zero there: those bytes were never written. That points at halfword writes with address bit 1 set. The lane decode in the slave has the two halfword cases swapped:


wire [3:0] lanes = (d_size == 3'b000) ? (4'b0001 << d_addr[1:0]) :
                   (d_size == 3'b001) ? (d_addr[1] ? 4'b0011 : 4'b1100) :   // swapped
                                        4'b1111;

A halfword write to an address such as 0x232 writes lanes 1:0 instead of 3:2, copying the inactive lanes of HWDATA into the wrong bytes. No word-only test would ever see it, and neither would a test that writes and reads the same halfword, because the read side is correct.

Quick Check

Q: The protocol checker reported nothing for the buggy slave. Does that mean it is useless here?

Show Answer

No. The bug is in data storage, which only the scoreboard can see. The protocol checker shows that the traffic was legal, so the mismatches really are the slave's fault and not a broken master.

From Here to UVM and Formal

In UVM, the command-list engine becomes a driver fed by sequence items, the random burst task becomes a constrained-random sequence, the protocol checks move into the monitor or into SystemVerilog assertions bound to the interface, and the byte model becomes a scoreboard or a register model. The UVM AHB-Lite Master Agent articles build the agent.

Protocol rules are also a natural fit for formal verification, which proves a property such as "address and control are stable while HREADY is low" for every possible input sequence instead of the ones a random test happens to hit. The Simple Bus Slave article shows the method on a smaller bus.

Running It in ChipVerify Lab

The example contains the buggy slave and this testbench. Run it, read the first mismatches, and find the bug yourself before looking at the lane decode.

Common Mistakes

Mistake #1: Comparing whole words for narrow reads

A byte read only promises one valid byte. Comparing all 32 bits of HRDATA reports false mismatches on the inactive lanes.

Mistake #2: Random stimulus that breaks the protocol

Unaligned addresses or bursts across 1 KB make the slave misbehave legally. Constrain the stimulus to legal traffic, and let the protocol checker prove it stays legal.

Mistake #3: A checker that forgets the exceptions

Flagging every change during a wait state rejects legal IDLE-to-NONSEQ and BUSY-to-SEQ switches and the changes after an ERROR. Encode the exceptions from the specification, not a simplified rule.

Practice Exercise

Challenge: Fix It, Then Change the Seed

Fix the lane decode and confirm that the test passes. Then put the bug back and change seed from 7 to 21. Does the test still find it?

Hint

The fix swaps the two constants in the halfword case back. A different seed produces a different list of 60 bursts.

Solution

(d_size == 3'b001) ? (d_addr[1] ? 4'b1100 : 4'b0011) :

With the fix, seed 7 prints PASS: 186 beats checked, no errors. With the bug and seed 21, the list has 156 beats and the test prints FAIL: 20 errors in 156 beats; with the fix and seed 21 it prints PASS: 156 beats checked, no errors. A regression runs many seeds so that a bug which one list happens to miss is caught by another.

Open the ready-made example in the lab and run the simulation.

Summary

  • AHB verification needs random bursts of mixed types and sizes, because the corner cases multiply.
  • A byte-level model updates and compares only the active lanes of each beat, when its data phase completes.
  • Protocol checks encode the specification's rules, including its exceptions, and run on every edge.
  • The scoreboard finds data bugs; the protocol checker proves the traffic was legal.
  • The same structure becomes a UVM agent with assertions, and the protocol rules can be proven formally.
Did you find what you were looking for?

Quiz

No quiz questions available for this article.