Ready-to-run snippets

Code Examples

Ready-to-run Verilog, SystemVerilog, and UVM examples. Click any to open it live in the IDE.

← All examples

Self-Checking AHB Testbench (find the bug)

Verilog Bus Protocols

Random bursts, a byte-level model, a scoreboard and protocol checks find a byte-lane bug in an AHB-Lite memory slave.

Open in IDE
design.v
// An AHB-Lite memory slave: 1 KB of RAM, byte/halfword/word transfers. It has a bug.
//   WAIT_NONSEQ  wait states for a NONSEQ transfer (the first beat of a burst)
//   WAIT_SEQ     wait states for a SEQ transfer (the following beats)
//   WAIT_WRITES  0: writes never wait (the slave buffers them), 1: writes wait like reads
//   RO_BASE      addresses from RO_BASE upwards are read-only: a write gets an ERROR response
module ahb_mem #(
  parameter WAIT_NONSEQ = 0,
  parameter WAIT_SEQ    = 0,
  parameter WAIT_WRITES = 1,
  parameter RO_BASE     = 10'h300
) (
  input             HCLK,
  input             HRESETn,
  input             HSEL,
  input      [31:0] HADDR,
  input      [1:0]  HTRANS,
  input             HWRITE,
  input      [2:0]  HSIZE,
  input      [31:0] HWDATA,
  input             HREADY,
  output            HREADYOUT,
  output            HRESP,
  output     [31:0] HRDATA
);
  reg [31:0] mem [0:255];                // 256 words = 1 KB
  integer    i;

  initial begin
    for (i = 0; i < 256; i = i + 1)
      mem[i] = 32'h0;
  end

  // Address phase signals, registered for the data phase
  reg        d_valid;                    // a NONSEQ or SEQ transfer is in its data phase
  reg [9:0]  d_addr;
  reg        d_write;
  reg [2:0]  d_size;
  reg [3:0]  wait_left;                  // wait states still to insert
  reg        err1;                       // first cycle of an ERROR response
  reg        err2;                       // second cycle of an ERROR response

  wire start = HREADY && HSEL && HTRANS[1];        // NONSEQ (10) or SEQ (11) accepted
  wire bad   = start && HWRITE && (HADDR[9:0] >= RO_BASE);

  always @(posedge HCLK) begin
    if (!HRESETn) begin
      d_valid   <= 1'b0;
      wait_left <= 4'd0;
      err1      <= 1'b0;
      err2      <= 1'b0;
    end
    else begin
      err2 <= err1;
      if (err1) begin
        err1    <= 1'b0;                 // second cycle of the ERROR response follows
        d_valid <= 1'b0;
      end
      else if (HREADY) begin
        // A new address phase completes: capture it
        d_valid   <= start && !bad;
        d_addr    <= HADDR[9:0];
        d_write   <= HWRITE;
        d_size    <= HSIZE;
        if (!start || bad || (HWRITE && !WAIT_WRITES))
          wait_left <= 4'd0;
        else if (HTRANS == 2'b10)
          wait_left <= WAIT_NONSEQ;
        else
          wait_left <= WAIT_SEQ;
        err1      <= bad;
      end
      else if (wait_left != 4'd0) begin
        wait_left <= wait_left - 4'd1;
      end
    end
  end

  // The data phase completes when no wait states are left
  assign HREADYOUT = err1 ? 1'b0 : (wait_left == 4'd0);
  assign HRESP     = err1 || err2;

  // Byte lanes for the write: little-endian, lane = address bits [1:0]
  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) :
                                          4'b1111;

  always @(posedge HCLK) begin
    if (d_valid && d_write && HREADYOUT) begin
      if (lanes[0])
        mem[d_addr[9:2]][7:0]   <= HWDATA[7:0];
      if (lanes[1])
        mem[d_addr[9:2]][15:8]  <= HWDATA[15:8];
      if (lanes[2])
        mem[d_addr[9:2]][23:16] <= HWDATA[23:16];
      if (lanes[3])
        mem[d_addr[9:2]][31:24] <= HWDATA[31:24];
    end
  end

  // Read data: the whole word, in the cycle the transfer completes.
  // The master picks the active byte lanes.
  assign HRDATA = (d_valid && !d_write && HREADYOUT) ? mem[d_addr[9:2]] : 32'h0;
endmodule
testbench.v
// Testbench: verify an AHB-Lite memory slave with random bursts, a byte-level
// reference model (scoreboard) and protocol checks
module tb;
  reg         HCLK    = 0;
  reg         HRESETn = 0;
  wire [31:0] HADDR;
  wire [1:0]  HTRANS;
  wire        HWRITE;
  wire [2:0]  HSIZE;
  wire [2:0]  HBURST;
  wire [31:0] HWDATA;
  wire [31:0] HRDATA;
  wire        HREADY;
  wire        HRESP;

  // HTRANS encodings
  localparam IDLE = 2'b00, BUSY = 2'b01, NONSEQ = 2'b10, SEQ = 2'b11;
  // HBURST encodings
  localparam SINGLE = 3'b000, INCR = 3'b001, WRAP4 = 3'b010, INCR4 = 3'b011,
             WRAP8  = 3'b100, INCR8 = 3'b101, WRAP16 = 3'b110, INCR16 = 3'b111;
  // HSIZE encodings
  localparam BYTE = 3'b000, HALF = 3'b001, WORD = 3'b010;

  // The command list: one entry per address phase
  reg  [1:0]  c_trans [0:255];
  reg  [31:0] c_addr  [0:255];
  reg         c_write [0:255];
  reg  [2:0]  c_size  [0:255];
  reg  [2:0]  c_burst [0:255];
  reg  [31:0] c_wdata [0:255];
  integer     n  = 0;                    // number of commands
  integer     ap = 0;                    // command in the address phase
  integer     dp = -1;                   // command in the data phase (-1: none)
  integer     k;
  reg         skip = 0;                  // drive IDLE: the rest of a burst was cancelled
  reg         cancel_on_error = 1;

  // Append one address phase to the list
  task add(input [1:0] t, input w, input [31:0] a, input [2:0] s, input [2:0] b, input [31:0] d);
    begin
      c_trans[n] = t;
      c_write[n] = w;
      c_addr[n]  = a;
      c_size[n]  = s;
      c_burst[n] = b;
      c_wdata[n] = d << (8 * a[1:0]);    // put narrow data on its byte lanes
      n = n + 1;
    end
  endtask

  // Append a whole burst. beats = 1 for SINGLE; INCR needs an explicit length.
  task burst(input w, input [2:0] b, input [31:0] a0, input [2:0] s, input integer beats, input [31:0] d0);
    integer    i;
    reg [31:0] a;
    reg [31:0] bytes;
    reg [31:0] lo;
    begin
      bytes = 1 << s;
      a = a0;
      for (i = 0; i < beats; i = i + 1) begin
        add(i == 0 ? NONSEQ : SEQ, w, a, s, b, d0 + i);
        if (b == WRAP4 || b == WRAP8 || b == WRAP16) begin
          lo = a0 & ~(bytes * beats - 1);              // wrap boundary
          a  = lo + ((a + bytes - lo) % (bytes * beats));
        end
        else begin
          a = a + bytes;
        end
      end
    end
  endtask

  // Insert a BUSY cycle before the next beat (address and control of the next beat)
  task busy_next(input w, input [31:0] a, input [2:0] s, input [2:0] b);
    begin
      add(BUSY, w, a, s, b, 32'h0);
    end
  endtask

  // ---- Reference model and scoreboard ----
  reg [7:0] model [0:1023];              // one entry per byte of the slave
  integer   errors = 0;
  integer   beats  = 0;
  integer   seed   = 7;

  // Called when a data phase completes: update the model or compare the read data
  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

  // ---- Protocol checks, sampled on every rising edge ----
  reg        p_ready = 1;
  reg [1:0]  p_trans = IDLE;
  reg [31:0] p_addr;
  reg        p_write;
  reg [2:0]  p_size;
  reg [2:0]  p_burst;
  reg        p_resp  = 0;
  reg        p_dwrite = 0;               // a write is in its data phase
  reg [31:0] p_wdata;
  reg [31:0] last_addr;                  // address of the last accepted NONSEQ/SEQ beat
  reg [31:0] next;
  reg [31:0] span;

  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
      if (HREADY && HTRANS[1])
        last_addr = HADDR;
      if (HREADY)
        p_dwrite <= HTRANS[1] && HWRITE;
      p_ready <= HREADY;
      p_trans <= HTRANS;
      p_addr  <= HADDR;
      p_write <= HWRITE;
      p_size  <= HSIZE;
      p_burst <= HBURST;
      p_resp  <= HRESP;
      p_wdata <= HWDATA;
    end
  end

  // ---- Random stimulus: singles, INCR4 and WRAP4 bursts of random size ----
  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

  // Address phase outputs
  assign HTRANS = (HRESETn && ap < n && !skip) ? c_trans[ap] : IDLE;
  assign HADDR  = (ap < n) ? c_addr[ap]  : (n > 0 ? c_addr[n-1]  : 32'h0);
  assign HWRITE = (ap < n) ? c_write[ap] : (n > 0 ? c_write[n-1] : 1'b0);
  assign HSIZE  = (ap < n) ? c_size[ap]  : (n > 0 ? c_size[n-1]  : 3'b0);
  assign HBURST = (ap < n) ? c_burst[ap] : (n > 0 ? c_burst[n-1] : 3'b0);
  // Data phase output
  assign HWDATA = (dp >= 0 && c_write[dp]) ? c_wdata[dp] : 32'h0;

  always @(posedge HCLK) begin
    if (HRESETn) begin
      if (HREADY) begin
        if (dp >= 0 && c_trans[dp][1])
          check_beat(dp);
        if (skip) begin
          dp   <= -1;
          skip <= 0;
        end
        else begin
          dp <= (ap < n) ? ap : -1;
          if (ap < n)
            ap <= ap + 1;
        end
      end
      else if (HRESP && cancel_on_error && ap < n && c_trans[ap] != NONSEQ) begin
        // First cycle of an ERROR: cancel the rest of this burst
        skip <= 1;
        k = ap;
        while (k < n && c_trans[k] != NONSEQ)
          k = k + 1;
        ap <= k;
      end
    end
  end

  ahb_mem #(
    .WAIT_NONSEQ (2),
    .WAIT_SEQ    (1),
    .WAIT_WRITES (1)
  ) mem (
    .HCLK      (HCLK),
    .HRESETn   (HRESETn),
    .HSEL      (1'b1),
    .HADDR     (HADDR),
    .HTRANS    (HTRANS),
    .HWRITE    (HWRITE),
    .HSIZE     (HSIZE),
    .HWDATA    (HWDATA),
    .HREADY    (HREADY),
    .HREADYOUT (HREADY),
    .HRESP     (HRESP),
    .HRDATA    (HRDATA)
  );

  always #5 HCLK = ~HCLK;

  initial begin
    $dumpfile("dump.vcd");
    $dumpvars(0, tb);
    for (k = 0; k < 1024; k = k + 1)
      model[k] = 8'h00;                  // the slave's RAM starts at zero
    random_list(60);
    #22 HRESETn = 1;
    wait (ap == n && dp == -1);
    if (errors == 0)
      $display("PASS: %0d beats checked, no errors", beats);
    else
      $display("FAIL: %0d errors in %0d beats", errors, beats);
    $finish;
  end
endmodule
Admin mode — edit and delete controls are visible on each row. Use the IDE to add new snippets via File → Save as Snippet.