USB · Module 26
AXI Interfaces to USB
AXI forbids a burst from crossing a 4 KB address boundary, and the reason this bug reaches production is that many interconnects quietly split the burst for you — so the engine that breaks the rule works on the board you develop on and corrupts memory on the board you ship.
Chapter 26.2 turned a byte count into packets. This chapter turns the same byte count into bus transactions, and it has exactly one rule that matters.
1. A USB Controller Is Two AXI Ports With Opposite Requirements
| Port | What goes through it | What it wants |
|---|---|---|
| slave | register reads and writes | latency — a driver is spinning on a status bit |
| master | packet data, by DMA | throughput — nobody waits on an individual beat |
Those are different ports for a reason.
2. The 4 KB Rule
An AXI burst must not cross a 4 KB address boundary. Not "should not": the protocol forbids it, because 4 KB is the smallest page an interconnect is allowed to decode differently, and a burst crossing one could be half in one slave and half in another.
A DMA buffer is whatever address the operating system handed you. So:
addr 0x1000, 256 beats of 4 bytes -> one burst. Fits.
addr 0x1F00, 256 beats of 4 bytes -> MUST BE SPLIT:
64 beats to 0x2000,
then 192 from there.The same 1024 bytes, aligned and not
3. And This Is Why It Is So Easy to Ship Broken
Many interconnects tolerate a crossing burst. Some split it for you. Some route the whole thing on the first address and corrupt memory. Some assert an error signal nobody has wired up.
4. The Other Limit
AXI4 allows at most 256 beats in an INCR burst, so a long transfer is split by the beat count as well as by the boundary. Both limits apply at once and a burst is the smaller of three things:
what is LEFT the descriptor does not go on for ever
the 4 KB BOUNDARY the protocol forbids crossing it
MAX_BEATS AXI4 INCR allows at most 256Counting the two kinds of split separately is worth the extra counter: a run of boundary splits says the buffer is badly aligned, and a run of length splits says it is simply long. Those lead to different conversations with whoever allocates the buffers.
5. What We Are Building
usb_axi_burst takes an address and a beat count and emits the burst sequence that covers it:
| Output | What it is |
|---|---|
baddr | this burst's start address |
blen | AXI AWLEN — the beat count minus one |
bsize | AXI AWSIZE — log2(bytes per beat), derived from DATA_W |
bbeats | the same count, unencoded, for the testbench to check |
n_split_bound / n_split_len | which limit cut each burst short |
and refuses three things outright: a zero-beat descriptor, an unaligned start address, and a second descriptor while one is running.
6. Verilog-2005 Implementation
// usb_axi_burst -- the one AXI rule a DMA engine has to obey and the one it
// is easiest to get away with breaking.
//
// A USB CONTROLLER IS TWO AXI PORTS WITH OPPOSITE REQUIREMENTS
//
// the SLAVE port registers. A handful of 32-bit accesses, and every
// one of them is a driver spinning on a status bit.
// It wants LATENCY.
//
// the MASTER port the DMA engine moving packet data. Kilobytes at a
// time, and nobody is waiting on any individual beat.
// It wants THROUGHPUT.
//
// Those are different ports for a reason. Putting both behind one bridge
// makes the register reads queue behind a 256-beat burst, and a driver that
// polls a status register then sees a millisecond of latency on an access
// that should take tens of nanoseconds.
//
// THE 4 KB RULE
//
// An AXI burst must not cross a 4 KB address boundary. Not "should not":
// the protocol forbids it, because 4 KB is the smallest page an interconnect
// is allowed to decode differently, and a burst that crossed one could be
// half in one slave and half in another.
//
// A DMA buffer is whatever address the operating system gave you. So:
//
// addr 0x1000, 256 beats of 4 bytes -> one burst. Fits.
// addr 0x1F00, 256 beats of 4 bytes -> MUST BE SPLIT:
// 64 beats to 0x2000, then 192.
//
// AND THIS IS WHY IT IS SO EASY TO SHIP BROKEN
//
// Many interconnects tolerate a crossing burst. Some split it for you, some
// route the whole thing on the first address and corrupt memory, some assert
// an error nobody has wired up. So an engine that ignores the rule works
// perfectly on the SoC it was developed on, and fails on the next one -- and
// the failure is memory corruption at an address that has nothing to do with
// USB.
//
// It is not a bug you find by testing. It is a bug you find by
// obeying the rule.
//
// THE OTHER LIMIT
//
// AXI4 allows at most 256 beats in an INCR burst, so a long transfer is
// split by the beat count as well as by the boundary. Both limits apply at
// once and the burst is the smaller of the three: what is left, what fits
// before the boundary, and what the protocol allows.
module usb_axi_burst #(
parameter integer ADDR_W = 32,
parameter integer DATA_W = 32,
parameter integer BOUNDARY = 4096, // the AXI rule. 4 KB, always.
parameter integer MAX_BEATS = 256 // AXI4 INCR limit
) (
input wire clk,
input wire rst_n,
input wire start,
input wire [ADDR_W-1:0] addr, // where the buffer begins
input wire [15:0] beats, // how many beats it is
input wire burst_ack, // the fabric accepted this burst
input wire eot,
output wire busy,
output wire bv, // a burst is offered this cycle
output wire [ADDR_W-1:0] baddr,
output wire [7:0] blen, // AXI AWLEN: beats - 1
output wire [2:0] bsize, // AXI AWSIZE: log2(bytes per beat)
output wire [15:0] bbeats, // the same count, not encoded
output wire [15:0] bindex, // 0-based burst number
output wire [15:0] remaining,
output wire done_pulse,
output wire err_pulse,
output wire [1:0] err_code,
output reg [31:0] n_start,
output reg [31:0] n_burst,
output reg [31:0] n_split_bound, // split because of the 4 KB rule
output reg [31:0] n_split_len, // split because of the beat limit
output reg [31:0] n_done,
output reg [31:0] n_unaligned,
output reg [31:0] n_zero,
output reg [31:0] n_restart
);
localparam [1:0] E_NONE = 2'd0,
E_UNALIGNED = 2'd1, // the address is not beat-aligned
E_ZERO = 2'd2, // a descriptor of no beats
E_RESTART = 2'd3; // a new one while a burst is running
// Bytes per beat, and the AXI size encoding of it. Derived from DATA_W
// rather than written as a constant, because the two must never drift:
// an AWSIZE that disagrees with the data width is a burst the fabric will
// happily accept and misinterpret.
localparam integer BPB = DATA_W / 8;
localparam [2:0] BSIZE = (BPB == 1) ? 3'd0 :
(BPB == 2) ? 3'd1 :
(BPB == 4) ? 3'd2 :
(BPB == 8) ? 3'd3 :
(BPB == 16) ? 3'd4 :
(BPB == 32) ? 3'd5 :
(BPB == 64) ? 3'd6 : 3'd7;
localparam [15:0] BND16 = BOUNDARY;
localparam [15:0] MAXB16 = MAX_BEATS;
localparam [15:0] BPB16 = BPB;
reg [ADDR_W-1:0] addr_r;
reg [15:0] rem_r, idx_r;
reg busy_r, dn_r, er_r;
reg [1:0] ec_r;
// ---- How many beats this burst may carry. ----
//
// The smaller of three limits, and all three are real:
//
// what is left the descriptor does not go on for ever
// the 4 KB boundary the protocol forbids crossing it
// MAX_BEATS AXI4 INCR allows at most 256
//
// Dropping any one of them produces a burst that the fabric may accept
// and misroute, which is the worst kind of wrong: no error anywhere.
function [15:0] beats_to_boundary;
input [ADDR_W-1:0] a;
reg [15:0] off;
begin
off = a[15:0] % BND16;
beats_to_boundary = (BND16 - off) / BPB16;
end
endfunction
function [15:0] this_burst;
input [ADDR_W-1:0] a;
input [15:0] r;
reg [15:0] lim_b, lim_l;
begin
lim_b = beats_to_boundary(a);
lim_l = MAXB16;
this_burst = r;
if (lim_b < this_burst) this_burst = lim_b;
if (lim_l < this_burst) this_burst = lim_l;
end
endfunction
wire [15:0] tb = this_burst(addr_r, rem_r);
assign busy = busy_r;
assign bv = busy_r;
assign baddr = addr_r;
// AXI encodes the beat count as length MINUS ONE. A burst of one beat has
// AWLEN = 0, and there is no encoding for zero beats -- which is why a
// descriptor of no beats is rejected rather than issued.
assign blen = busy_r ? (tb[7:0] - 8'd1) : 8'd0;
assign bsize = BSIZE;
assign bbeats = busy_r ? tb : 16'd0;
assign bindex = idx_r;
assign remaining = rem_r;
assign done_pulse = dn_r;
assign err_pulse = er_r;
assign err_code = ec_r;
reg [ADDR_W-1:0] addr_n;
reg [15:0] rem_n, idx_n;
reg busy_n, dn_n, er_n;
reg [1:0] ec_n;
reg st_n, bu_n, sb_n, sl_n, dn_c, un_n, zr_n, rs_n;
always @* begin
addr_n = addr_r;
rem_n = rem_r;
idx_n = idx_r;
busy_n = busy_r;
dn_n = 1'b0; er_n = 1'b0; ec_n = E_NONE;
st_n = 1'b0; bu_n = 1'b0; sb_n = 1'b0; sl_n = 1'b0;
dn_c = 1'b0; un_n = 1'b0; zr_n = 1'b0; rs_n = 1'b0;
if (eot) begin
// Nothing: the counters are the report.
end else if (start) begin
if (busy_r) begin
er_n = 1'b1; ec_n = E_RESTART;
rs_n = 1'b1;
end else if (beats == 16'd0) begin
// AXI has no encoding for a zero-beat burst. A descriptor of no
// beats is a software bug and issuing AWLEN = 0xFF for it -- which
// is what `beats - 1` produces -- writes 256 beats of rubbish.
er_n = 1'b1; ec_n = E_ZERO;
zr_n = 1'b1;
end else if ((addr[15:0] % BPB16) != 16'd0) begin
// An INCR burst of this size must start on a beat boundary. An
// unaligned start is legal AXI only with the narrow-transfer rules,
// which a DMA engine has no reason to use and every reason to avoid.
er_n = 1'b1; ec_n = E_UNALIGNED;
un_n = 1'b1;
end else begin
addr_n = addr;
rem_n = beats;
idx_n = 16'd0;
busy_n = 1'b1;
st_n = 1'b1;
end
end else if (burst_ack) begin
if (busy_r) begin
bu_n = 1'b1;
// Which limit cut this burst short is worth counting separately: a
// run of boundary splits says the buffer is badly aligned, and a run
// of length splits says it is simply long. They lead to different
// conversations with whoever allocates the buffers.
if (tb < rem_r) begin
if (tb == beats_to_boundary(addr_r)) sb_n = 1'b1;
else sl_n = 1'b1;
end
addr_n = addr_r + {{(ADDR_W-16){1'b0}}, tb} * BPB;
rem_n = rem_r - tb;
idx_n = idx_r + 16'd1;
if (rem_n == 16'd0) begin
busy_n = 1'b0;
dn_n = 1'b1;
dn_c = 1'b1;
end
end
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
addr_r <= {ADDR_W{1'b0}};
rem_r <= 16'd0;
idx_r <= 16'd0;
busy_r <= 1'b0;
dn_r <= 1'b0; er_r <= 1'b0; ec_r <= E_NONE;
n_start <= 32'd0;
n_burst <= 32'd0;
n_split_bound <= 32'd0;
n_split_len <= 32'd0;
n_done <= 32'd0;
n_unaligned <= 32'd0;
n_zero <= 32'd0;
n_restart <= 32'd0;
end else begin
addr_r <= addr_n;
rem_r <= rem_n;
idx_r <= idx_n;
busy_r <= busy_n;
dn_r <= dn_n; er_r <= er_n; ec_r <= ec_n;
if (st_n) n_start <= n_start + 32'd1;
if (bu_n) n_burst <= n_burst + 32'd1;
if (sb_n) n_split_bound <= n_split_bound + 32'd1;
if (sl_n) n_split_len <= n_split_len + 32'd1;
if (dn_c) n_done <= n_done + 32'd1;
if (un_n) n_unaligned <= n_unaligned + 32'd1;
if (zr_n) n_zero <= n_zero + 32'd1;
if (rs_n) n_restart <= n_restart + 32'd1;
end
end
endmodule7. SystemVerilog Implementation
// usb_axi_burst -- the one AXI rule a DMA engine has to obey and the one it
// is easiest to get away with breaking.
//
// A USB CONTROLLER IS TWO AXI PORTS WITH OPPOSITE REQUIREMENTS
//
// the SLAVE port registers. A handful of 32-bit accesses, and every
// one of them is a driver spinning on a status bit.
// It wants LATENCY.
//
// the MASTER port the DMA engine moving packet data. Kilobytes at a
// time, and nobody is waiting on any individual beat.
// It wants THROUGHPUT.
//
// Those are different ports for a reason. Putting both behind one bridge
// makes the register reads queue behind a 256-beat burst, and a driver that
// polls a status register then sees a millisecond of latency on an access
// that should take tens of nanoseconds.
//
// THE 4 KB RULE
//
// An AXI burst must not cross a 4 KB address boundary. Not "should not":
// the protocol forbids it, because 4 KB is the smallest page an interconnect
// is allowed to decode differently, and a burst that crossed one could be
// half in one slave and half in another.
//
// A DMA buffer is whatever address the operating system gave you. So:
//
// addr 0x1000, 256 beats of 4 bytes -> one burst. Fits.
// addr 0x1F00, 256 beats of 4 bytes -> MUST BE SPLIT:
// 64 beats to 0x2000, then 192.
//
// AND THIS IS WHY IT IS SO EASY TO SHIP BROKEN
//
// Many interconnects tolerate a crossing burst. Some split it for you, some
// route the whole thing on the first address and corrupt memory, some assert
// an error nobody has wired up. So an engine that ignores the rule works
// perfectly on the SoC it was developed on, and fails on the next one -- and
// the failure is memory corruption at an address that has nothing to do with
// USB.
//
// It is not a bug you find by testing. It is a bug you find by
// obeying the rule.
//
// THE OTHER LIMIT
//
// AXI4 allows at most 256 beats in an INCR burst, so a long transfer is
// split by the beat count as well as by the boundary. Both limits apply at
// once and the burst is the smaller of the three: what is left, what fits
// before the boundary, and what the protocol allows.
package usb_axi_pkg;
typedef enum logic [1:0] {
E_NONE = 2'd0,
E_UNALIGNED = 2'd1, // the address is not beat-aligned
E_ZERO = 2'd2, // a descriptor of no beats
E_RESTART = 2'd3 // a new one while a burst is running
} axi_err_e;
endpackage
module usb_axi_burst
import usb_axi_pkg::*;
#(
parameter int ADDR_W = 32,
parameter int DATA_W = 32,
parameter int BOUNDARY = 4096, // the AXI rule. 4 KB, always.
parameter int MAX_BEATS = 256 // AXI4 INCR limit
) (
input logic clk,
input logic rst_n,
input logic start,
input logic [ADDR_W-1:0] addr, // where the buffer begins
input logic [15:0] beats, // how many beats it is
input logic burst_ack, // the fabric accepted this burst
input logic eot,
output logic busy,
output logic bv, // a burst is offered this cycle
output logic [ADDR_W-1:0] baddr,
output logic [7:0] blen, // AXI AWLEN: beats - 1
output logic [2:0] bsize, // AXI AWSIZE: log2(bytes per beat)
output logic [15:0] bbeats, // the same count, not encoded
output logic [15:0] bindex, // 0-based burst number
output logic [15:0] remaining,
output logic done_pulse,
output logic err_pulse,
output axi_err_e err_code,
output logic [31:0] n_start,
output logic [31:0] n_burst,
output logic [31:0] n_split_bound, // split because of the 4 KB rule
output logic [31:0] n_split_len, // split because of the beat limit
output logic [31:0] n_done,
output logic [31:0] n_unaligned,
output logic [31:0] n_zero,
output logic [31:0] n_restart
);
// Bytes per beat, and the AXI size encoding of it. Derived from DATA_W
// rather than written as a constant, because the two must never drift:
// an AWSIZE that disagrees with the data width is a burst the fabric will
// happily accept and misinterpret.
localparam int BPB = DATA_W / 8;
localparam [2:0] BSIZE = (BPB == 1) ? 3'd0 :
(BPB == 2) ? 3'd1 :
(BPB == 4) ? 3'd2 :
(BPB == 8) ? 3'd3 :
(BPB == 16) ? 3'd4 :
(BPB == 32) ? 3'd5 :
(BPB == 64) ? 3'd6 : 3'd7;
localparam logic [15:0] BND16 = 16'(BOUNDARY);
localparam logic [15:0] MAXB16 = 16'(MAX_BEATS);
localparam logic [15:0] BPB16 = 16'(BPB);
logic [ADDR_W-1:0] addr_r;
logic [15:0] rem_r, idx_r;
logic busy_r, dn_r, er_r;
axi_err_e ec_r;
// ---- How many beats this burst may carry. ----
//
// The smaller of three limits, and all three are real:
//
// what is left the descriptor does not go on for ever
// the 4 KB boundary the protocol forbids crossing it
// MAX_BEATS AXI4 INCR allows at most 256
//
// Dropping any one of them produces a burst that the fabric may accept
// and misroute, which is the worst kind of wrong: no error anywhere.
function automatic logic [15:0] beats_to_boundary(input logic [ADDR_W-1:0] a);
logic [15:0] off;
begin
off = a[15:0] % BND16;
beats_to_boundary = (BND16 - off) / BPB16;
end
endfunction
function automatic logic [15:0] this_burst(input logic [ADDR_W-1:0] a,
input logic [15:0] r);
logic [15:0] lim_b, lim_l;
begin
lim_b = beats_to_boundary(a);
lim_l = MAXB16;
this_burst = r;
if (lim_b < this_burst) this_burst = lim_b;
if (lim_l < this_burst) this_burst = lim_l;
end
endfunction
logic [15:0] tb;
assign tb = this_burst(addr_r, rem_r);
assign busy = busy_r;
assign bv = busy_r;
assign baddr = addr_r;
// AXI encodes the beat count as length MINUS ONE. A burst of one beat has
// AWLEN = 0, and there is no encoding for zero beats -- which is why a
// descriptor of no beats is rejected rather than issued.
assign blen = busy_r ? (tb[7:0] - 8'd1) : 8'd0;
assign bsize = BSIZE;
assign bbeats = busy_r ? tb : 16'd0;
assign bindex = idx_r;
assign remaining = rem_r;
assign done_pulse = dn_r;
assign err_pulse = er_r;
assign err_code = ec_r;
logic [ADDR_W-1:0] addr_n;
logic [15:0] rem_n, idx_n;
logic busy_n, dn_n, er_n;
axi_err_e ec_n;
logic st_n, bu_n, sb_n, sl_n, dn_c, un_n, zr_n, rs_n;
always_comb begin
addr_n = addr_r;
rem_n = rem_r;
idx_n = idx_r;
busy_n = busy_r;
dn_n = 1'b0; er_n = 1'b0; ec_n = E_NONE;
st_n = 1'b0; bu_n = 1'b0; sb_n = 1'b0; sl_n = 1'b0;
dn_c = 1'b0; un_n = 1'b0; zr_n = 1'b0; rs_n = 1'b0;
if (eot) begin
// Nothing: the counters are the report.
end else if (start) begin
if (busy_r) begin
er_n = 1'b1; ec_n = E_RESTART;
rs_n = 1'b1;
end else if (beats == 16'd0) begin
// AXI has no encoding for a zero-beat burst. A descriptor of no
// beats is a software bug and issuing AWLEN = 0xFF for it -- which
// is what `beats - 1` produces -- writes 256 beats of rubbish.
er_n = 1'b1; ec_n = E_ZERO;
zr_n = 1'b1;
end else if ((addr[15:0] % BPB16) != 16'd0) begin
// An INCR burst of this size must start on a beat boundary. An
// unaligned start is legal AXI only with the narrow-transfer rules,
// which a DMA engine has no reason to use and every reason to avoid.
er_n = 1'b1; ec_n = E_UNALIGNED;
un_n = 1'b1;
end else begin
addr_n = addr;
rem_n = beats;
idx_n = 16'd0;
busy_n = 1'b1;
st_n = 1'b1;
end
end else if (burst_ack) begin
if (busy_r) begin
bu_n = 1'b1;
// Which limit cut this burst short is worth counting separately: a
// run of boundary splits says the buffer is badly aligned, and a run
// of length splits says it is simply long. They lead to different
// conversations with whoever allocates the buffers.
if (tb < rem_r) begin
if (tb == beats_to_boundary(addr_r)) sb_n = 1'b1;
else sl_n = 1'b1;
end
addr_n = addr_r + ADDR_W'({{(ADDR_W-16){1'b0}}, tb} * BPB);
rem_n = rem_r - tb;
idx_n = idx_r + 16'd1;
if (rem_n == 16'd0) begin
busy_n = 1'b0;
dn_n = 1'b1;
dn_c = 1'b1;
end
end
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
addr_r <= '0;
rem_r <= 16'd0;
idx_r <= 16'd0;
busy_r <= 1'b0;
dn_r <= 1'b0; er_r <= 1'b0; ec_r <= E_NONE;
n_start <= 32'd0;
n_burst <= 32'd0;
n_split_bound <= 32'd0;
n_split_len <= 32'd0;
n_done <= 32'd0;
n_unaligned <= 32'd0;
n_zero <= 32'd0;
n_restart <= 32'd0;
end else begin
addr_r <= addr_n;
rem_r <= rem_n;
idx_r <= idx_n;
busy_r <= busy_n;
dn_r <= dn_n; er_r <= er_n; ec_r <= ec_n;
if (st_n) n_start <= n_start + 32'd1;
if (bu_n) n_burst <= n_burst + 32'd1;
if (sb_n) n_split_bound <= n_split_bound + 32'd1;
if (sl_n) n_split_len <= n_split_len + 32'd1;
if (dn_c) n_done <= n_done + 32'd1;
if (un_n) n_unaligned <= n_unaligned + 32'd1;
if (zr_n) n_zero <= n_zero + 32'd1;
if (rs_n) n_restart <= n_restart + 32'd1;
end
end
endmodule8. VHDL-2008 Implementation
-- usb_axi_burst -- the one AXI rule a DMA engine has to obey and the one it
-- is easiest to get away with breaking.
--
-- A USB CONTROLLER IS TWO AXI PORTS WITH OPPOSITE REQUIREMENTS
--
-- the SLAVE port registers. A handful of 32-bit accesses, and every
-- one of them is a driver spinning on a status bit.
-- It wants LATENCY.
--
-- the MASTER port the DMA engine moving packet data. Kilobytes at a
-- time, and nobody is waiting on any individual beat.
-- It wants THROUGHPUT.
--
-- Those are different ports for a reason. Putting both behind one bridge
-- makes the register reads queue behind a 256-beat burst, and a driver that
-- polls a status register then sees a millisecond of latency on an access
-- that should take tens of nanoseconds.
--
-- THE 4 KB RULE
--
-- An AXI burst must not cross a 4 KB address boundary. Not "should not": the
-- protocol forbids it, because 4 KB is the smallest page an interconnect is
-- allowed to decode differently, and a burst that crossed one could be half
-- in one slave and half in another.
--
-- A DMA buffer is whatever address the operating system gave you. So:
--
-- addr 0x1000, 256 beats of 4 bytes -> one burst. Fits.
-- addr 0x1F00, 256 beats of 4 bytes -> MUST BE SPLIT:
-- 64 beats to 0x2000, then 192.
--
-- AND THIS IS WHY IT IS SO EASY TO SHIP BROKEN
--
-- Many interconnects tolerate a crossing burst. Some split it for you, some
-- route the whole thing on the first address and corrupt memory, some assert
-- an error nobody has wired up. So an engine that ignores the rule works
-- perfectly on the SoC it was developed on, and fails on the next one -- and
-- the failure is memory corruption at an address that has nothing to do with
-- USB.
--
-- It is not a bug you find by testing. It is a bug you find by
-- obeying the rule.
--
-- THE OTHER LIMIT
--
-- AXI4 allows at most 256 beats in an INCR burst, so a long transfer is split
-- by the beat count as well as by the boundary. Both limits apply at once and
-- the burst is the smaller of the three: what is left, what fits before the
-- boundary, and what the protocol allows.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package usb_axi_pkg is
constant E_NONE : std_logic_vector(1 downto 0) := "00";
constant E_UNALIGNED : std_logic_vector(1 downto 0) := "01";
constant E_ZERO : std_logic_vector(1 downto 0) := "10";
constant E_RESTART : std_logic_vector(1 downto 0) := "11";
end package;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb_axi_pkg.all;
entity usb_axi_burst is
generic (
ADDR_W : integer := 32;
DATA_W : integer := 32;
BOUNDARY : integer := 4096; -- the AXI rule. 4 KB, always.
MAX_BEATS : integer := 256 -- AXI4 INCR limit
);
port (
clk : in std_logic;
rst_n : in std_logic;
start : in std_logic;
addr : in unsigned(ADDR_W-1 downto 0);
beats : in unsigned(15 downto 0);
burst_ack : in std_logic;
eot : in std_logic;
busy : out std_logic;
bv : out std_logic;
baddr : out unsigned(ADDR_W-1 downto 0);
blen : out unsigned(7 downto 0); -- AXI AWLEN: beats - 1
bsize : out unsigned(2 downto 0); -- AXI AWSIZE
bbeats : out unsigned(15 downto 0);
bindex : out unsigned(15 downto 0);
remaining : out unsigned(15 downto 0);
done_pulse : out std_logic;
err_pulse : out std_logic;
err_code : out std_logic_vector(1 downto 0);
n_start : out unsigned(31 downto 0);
n_burst : out unsigned(31 downto 0);
n_split_bound : out unsigned(31 downto 0);
n_split_len : out unsigned(31 downto 0);
n_done : out unsigned(31 downto 0);
n_unaligned : out unsigned(31 downto 0);
n_zero : out unsigned(31 downto 0);
n_restart : out unsigned(31 downto 0)
);
end entity;
architecture rtl of usb_axi_burst is
-- Bytes per beat, and the AXI size encoding of it. Derived from DATA_W
-- rather than written as a constant, because the two must never drift: an
-- AWSIZE that disagrees with the data width is a burst the fabric will
-- happily accept and misinterpret.
constant BPB : integer := DATA_W / 8;
function size_enc (b : integer) return unsigned is
begin
case b is
when 1 => return to_unsigned(0, 3);
when 2 => return to_unsigned(1, 3);
when 4 => return to_unsigned(2, 3);
when 8 => return to_unsigned(3, 3);
when 16 => return to_unsigned(4, 3);
when 32 => return to_unsigned(5, 3);
when 64 => return to_unsigned(6, 3);
when others => return to_unsigned(7, 3);
end case;
end function;
signal addr_r : unsigned(ADDR_W-1 downto 0) := (others => '0');
signal rem_r, idx_r : unsigned(15 downto 0) := (others => '0');
signal busy_r, dn_r, er_r : std_logic := '0';
signal ec_r : std_logic_vector(1 downto 0) := E_NONE;
signal st_c, bu_c, sb_c, sl_c : unsigned(31 downto 0) := (others => '0');
signal dn_c, un_c, zr_c, rs_c : unsigned(31 downto 0) := (others => '0');
signal tb : unsigned(15 downto 0) := (others => '0');
-- ---- How many beats this burst may carry. ----
--
-- The smaller of three limits, and all three are real:
--
-- what is left the descriptor does not go on for ever
-- the 4 KB boundary the protocol forbids crossing it
-- MAX_BEATS AXI4 INCR allows at most 256
--
-- Dropping any one of them produces a burst that the fabric may accept and
-- misroute, which is the worst kind of wrong: no error anywhere.
function beats_to_boundary (a : unsigned) return unsigned is
variable off : unsigned(15 downto 0);
begin
off := a(15 downto 0) mod to_unsigned(BOUNDARY, 16);
return (to_unsigned(BOUNDARY, 16) - off) / to_unsigned(BPB, 16);
end function;
function this_burst (a : unsigned; r : unsigned(15 downto 0))
return unsigned is
variable v : unsigned(15 downto 0);
variable b : unsigned(15 downto 0);
begin
v := r;
b := beats_to_boundary(a);
if b < v then v := b; end if;
if to_unsigned(MAX_BEATS, 16) < v then v := to_unsigned(MAX_BEATS, 16); end if;
return v;
end function;
begin
tb <= this_burst(addr_r, rem_r);
busy <= busy_r;
bv <= busy_r;
baddr <= addr_r;
-- AXI encodes the beat count as length MINUS ONE. A burst of one beat has
-- AWLEN = 0, and there is no encoding for zero beats -- which is why a
-- descriptor of no beats is rejected rather than issued.
blen <= (tb(7 downto 0) - 1) when busy_r = '1' else (others => '0');
bsize <= size_enc(BPB);
bbeats <= tb when busy_r = '1' else (others => '0');
bindex <= idx_r;
remaining <= rem_r;
done_pulse <= dn_r;
err_pulse <= er_r;
err_code <= ec_r;
n_start <= st_c;
n_burst <= bu_c;
n_split_bound <= sb_c;
n_split_len <= sl_c;
n_done <= dn_c;
n_unaligned <= un_c;
n_zero <= zr_c;
n_restart <= rs_c;
process (clk, rst_n)
variable addr_v : unsigned(ADDR_W-1 downto 0);
variable rem_v, idx_v : unsigned(15 downto 0);
variable busy_v, dn_v, er_v : std_logic;
variable ec_v : std_logic_vector(1 downto 0);
variable tb_v : unsigned(15 downto 0);
begin
if rst_n = '0' then
addr_r <= (others => '0');
rem_r <= (others => '0');
idx_r <= (others => '0');
busy_r <= '0';
dn_r <= '0'; er_r <= '0'; ec_r <= E_NONE;
st_c <= (others => '0'); bu_c <= (others => '0');
sb_c <= (others => '0'); sl_c <= (others => '0');
dn_c <= (others => '0'); un_c <= (others => '0');
zr_c <= (others => '0'); rs_c <= (others => '0');
elsif rising_edge(clk) then
addr_v := addr_r;
rem_v := rem_r;
idx_v := idx_r;
busy_v := busy_r;
dn_v := '0'; er_v := '0'; ec_v := E_NONE;
tb_v := this_burst(addr_r, rem_r);
if eot = '1' then
-- Nothing: the counters are the report.
null;
elsif start = '1' then
if busy_r = '1' then
er_v := '1'; ec_v := E_RESTART;
rs_c <= rs_c + 1;
elsif beats = 0 then
-- AXI has no encoding for a zero-beat burst. A descriptor of no
-- beats is a software bug and issuing AWLEN = 0xFF for it -- which
-- is what `beats - 1` produces -- writes 256 beats of rubbish.
er_v := '1'; ec_v := E_ZERO;
zr_c <= zr_c + 1;
elsif (addr(15 downto 0) mod to_unsigned(BPB, 16)) /= 0 then
-- An INCR burst of this size must start on a beat boundary. An
-- unaligned start is legal AXI only with the narrow-transfer rules,
-- which a DMA engine has no reason to use and every reason to
-- avoid.
er_v := '1'; ec_v := E_UNALIGNED;
un_c <= un_c + 1;
else
addr_v := addr;
rem_v := beats;
idx_v := (others => '0');
busy_v := '1';
st_c <= st_c + 1;
end if;
elsif burst_ack = '1' then
if busy_r = '1' then
bu_c <= bu_c + 1;
-- Which limit cut this burst short is worth counting separately: a
-- run of boundary splits says the buffer is badly aligned, and a
-- run of length splits says it is simply long. They lead to
-- different conversations with whoever allocates the buffers.
if tb_v < rem_v then
if tb_v = beats_to_boundary(addr_r) then
sb_c <= sb_c + 1;
else
sl_c <= sl_c + 1;
end if;
end if;
addr_v := addr_v + resize(tb_v * to_unsigned(BPB, 16), ADDR_W);
rem_v := rem_v - tb_v;
idx_v := idx_v + 1;
if rem_v = 0 then
busy_v := '0';
dn_v := '1';
dn_c <= dn_c + 1;
end if;
end if;
end if;
addr_r <= addr_v;
rem_r <= rem_v;
idx_r <= idx_v;
busy_r <= busy_v;
dn_r <= dn_v; er_r <= er_v; ec_r <= ec_v;
end if;
end process;
end architecture;9. Seeing a Burst Split
256 beats from 0x1F00: the first burst ends exactly on the boundary
usb_axi_burst — the 4 KB boundary split
10 cyclesAnd the split that has nothing to do with alignment:
An aligned transfer, split by the beat limit alone
usb_axi_burst — length splits, not boundary splits
10 cycles10. The Testbenches
This is one of the few blocks where the specification states the acceptance criteria directly, so the testbench asserts properties rather than a table of expected outputs:
| # | Property |
|---|---|
| 1 | no burst crosses a BOUNDARY-byte address boundary |
| 2 | no burst exceeds MAX_BEATS beats |
| 3 | every burst carries at least one beat |
| 4 | AWLEN is the beat count minus one, every time |
| 5 | consecutive bursts are contiguous — no gap, no overlap |
| 6 | the beats delivered sum to exactly the beats requested |
Property 1 is the one this chapter is about. Property 6 is the one that catches a splitter which obeys property 1 by dropping data, which is the obvious way to make property 1 pass.
The exhaustive claim sweeps the two numbers the arithmetic depends on, as a product:
every aligned offset in a boundary window (64)
x every beat count from 1 to two windows (128)
= 8192 transfers, every burst of every one checkedVerilog-2005 testbench
`timescale 1ns/1ps
// Testbench for usb_axi_burst.
//
// THE CHECKS ARE PROTOCOL PROPERTIES, NOT EXPECTED VALUES
//
// A burst splitter is one of the few blocks where the specification states
// the acceptance criteria directly, so the testbench asserts those rather
// than a table of expected outputs:
//
// 1. no burst crosses a BOUNDARY-byte address boundary
// 2. no burst exceeds MAX_BEATS beats
// 3. every burst carries at least one beat
// 4. AWLEN is the beat count MINUS ONE, every time
// 5. consecutive bursts are contiguous -- no gap, no overlap
// 6. the beats delivered sum to exactly the beats requested
//
// Property 1 is the one this chapter is about and property 6 is the one that
// catches a splitter which obeys property 1 by dropping data.
//
// THE EXHAUSTIVE CLAIM
//
// The splitting arithmetic depends on two numbers: where in the boundary
// window the buffer starts, and how many beats it is. So both are swept, all
// the way across, as a product:
//
// every aligned offset in a boundary window (64)
// x every beat count from 1 to 2 windows' worth (128)
// = 8192 transfers, every burst of every one checked
//
// The DUT is instantiated with a SCALED boundary so that product is
// tractable. A second instance carries the real 4096/256 values and is
// driven by the directed cases, because a parameter that is never changed is
// a constant with extra steps -- chapter 25.4's retry budget again.
module tb_ab_v;
localparam integer ADDR_W = 32;
localparam integer DATA_W = 32;
localparam integer BPB = DATA_W / 8;
// the scaled instance, for the exhaustive sweep
localparam integer BND_S = 256;
localparam integer MAXB_S = 16;
// the real instance, for the directed cases
localparam integer BND_R = 4096;
localparam integer MAXB_R = 256;
localparam [1:0] E_NONE = 2'd0, E_UNALIGNED = 2'd1, E_ZERO = 2'd2,
E_RESTART = 2'd3;
reg clk = 1'b0, rst_n = 1'b0;
reg start = 1'b0, burst_ack = 1'b0, eot = 1'b0;
reg [ADDR_W-1:0] addr = {ADDR_W{1'b0}};
reg [15:0] beats = 16'd0;
wire busy, bv, done_pulse, err_pulse;
wire [ADDR_W-1:0] baddr;
wire [7:0] blen;
wire [2:0] bsize;
wire [15:0] bbeats, bindex, remaining;
wire [1:0] err_code;
wire [31:0] n_start, n_burst, n_split_bound, n_split_len, n_done,
n_unaligned, n_zero, n_restart;
usb_axi_burst #(.ADDR_W(ADDR_W), .DATA_W(DATA_W),
.BOUNDARY(BND_S), .MAX_BEATS(MAXB_S)) dut (
.clk(clk), .rst_n(rst_n),
.start(start), .addr(addr), .beats(beats), .burst_ack(burst_ack),
.eot(eot),
.busy(busy), .bv(bv), .baddr(baddr), .blen(blen), .bsize(bsize),
.bbeats(bbeats), .bindex(bindex), .remaining(remaining),
.done_pulse(done_pulse), .err_pulse(err_pulse), .err_code(err_code),
.n_start(n_start), .n_burst(n_burst), .n_split_bound(n_split_bound),
.n_split_len(n_split_len), .n_done(n_done),
.n_unaligned(n_unaligned), .n_zero(n_zero), .n_restart(n_restart)
);
// ---- The same design with the REAL AXI numbers. ----
//
// Driven by the same stimulus and checked against the same properties. A
// splitter whose boundary is hard-coded to whatever the test used passes
// everything above and corrupts memory on the first real SoC.
wire busy4, bv4, done4;
wire [ADDR_W-1:0] baddr4;
wire [7:0] blen4;
wire [15:0] bbeats4, bindex4;
usb_axi_burst #(.ADDR_W(ADDR_W), .DATA_W(DATA_W),
.BOUNDARY(BND_R), .MAX_BEATS(MAXB_R)) dut4k (
.clk(clk), .rst_n(rst_n),
.start(start), .addr(addr), .beats(beats), .burst_ack(burst_ack),
.eot(eot),
.busy(busy4), .bv(bv4), .baddr(baddr4), .blen(blen4), .bsize(),
.bbeats(bbeats4), .bindex(bindex4), .remaining(),
.done_pulse(done4), .err_pulse(), .err_code(),
.n_start(), .n_burst(), .n_split_bound(), .n_split_len(), .n_done(),
.n_unaligned(), .n_zero(), .n_restart()
);
always #5 clk = ~clk;
integer errors = 0, checks = 0, steps = 0;
integer k;
// reach: aligned offset (64) x beat count (128)
reg [0:0] reach [0:8191];
integer n_reach;
task ck;
input [255:0] nm;
input [31:0] got, exp;
begin
checks = checks + 1;
if (got !== exp) begin
errors = errors + 1;
if (errors < 25)
$display("FAIL t=%0t step=%0d %0s got=%0d exp=%0d",
$time, steps, nm, got, exp);
end
end
endtask
task fail; input [255:0] nm;
begin
errors = errors + 1;
if (errors < 25) $display("FAIL t=%0t step=%0d %0s", $time, steps, nm);
end
endtask
task tickc;
begin
@(posedge clk); #1;
steps = steps + 1;
end
endtask
// ---- The six properties, checked on the burst being offered. ----
task check_burst;
input [ADDR_W-1:0] a;
input [15:0] nb;
input [7:0] ln;
input integer bnd;
input integer maxb;
input [255:0] who;
reg [31:0] off;
begin
// 3. at least one beat
checks = checks + 1;
if (nb == 16'd0) begin
errors = errors + 1;
if (errors < 25) $display("FAIL %0s: a zero-beat burst", who);
end
// 2. the AXI4 INCR limit
checks = checks + 1;
if (nb > maxb[15:0]) begin
errors = errors + 1;
if (errors < 25)
$display("FAIL %0s: %0d beats exceeds the %0d-beat limit",
who, nb, maxb);
end
// 4. AWLEN is beats minus one
checks = checks + 1;
if (ln !== (nb[7:0] - 8'd1)) begin
errors = errors + 1;
if (errors < 25)
$display("FAIL %0s: AWLEN %0d for %0d beats", who, ln, nb);
end
// 1. THE RULE. The burst must not cross the boundary.
off = a % bnd;
checks = checks + 1;
if ((off + nb * BPB) > bnd[31:0]) begin
errors = errors + 1;
if (errors < 25)
$display("FAIL %0s: burst at 0x%0h (offset %0d) of %0d beats crosses the %0d-byte boundary",
who, a, off, nb, bnd);
end
end
endtask
// Run one whole transfer, checking every burst and the tiling.
task run_xfer;
input [ADDR_W-1:0] a0;
input [15:0] nb;
integer seen;
reg [ADDR_W-1:0] expect_addr;
integer guard;
integer max_ok;
reg stuck;
begin
start = 1'b1; burst_ack = 1'b0; eot = 1'b0; addr = a0; beats = nb;
tickc;
start = 1'b0;
seen = 0;
expect_addr = a0;
guard = 0;
// ---- PROPERTY 7: a bound on the NUMBER of bursts. ----
//
// Each limit can only cut a transfer so many times. Within one
// boundary window the beat limit gives ceil(beats / MAX_BEATS) bursts,
// and the transfer spans a known number of windows -- so a correct
// splitter cannot need more than the sum of the two.
//
// It is a real property and it is also what keeps the suite usable: a
// mutation that computes the distance to the boundary wrongly emits
// ONE-BEAT bursts, which is correct-looking, terminating, and turns an
// eight-second run into a ten-minute one. Bounding the loop at 4096
// catches a design that hangs; bounding it at what the limits allow
// catches a design that crawls.
max_ok = ((nb + MAXB_S - 1) / MAXB_S)
+ (((a0 % BND_S) + nb * BPB + BND_S - 1) / BND_S) + 1;
stuck = 1'b0;
while (busy && (guard < max_ok) && !stuck) begin
guard = guard + 1;
check_burst(baddr, bbeats, blen, BND_S, MAXB_S, "scaled");
// ---- FAIL FAST on a design that cannot progress. ----
//
// A zero-beat burst is not just wrong, it is unadvanceable: the
// address never moves and the remaining count never falls, so the
// loop below would run its full guard of 4096 for every one of the
// 8192 transfers. One mutation then reports NINETY-SIX MILLION
// failures and the other six are invisible beside it -- and the run
// takes minutes instead of seconds.
//
// A mutation score is only comparable if no mutation is allowed to
// spin. Bound the loop AND leave it on the first unadvanceable
// burst.
if (bbeats == 16'd0) stuck = 1'b1;
// The 4 KB instance needs fewer splits for the same transfer, so it
// finishes first and its outputs stop meaning anything. Checking a
// burst that is not being offered is checking the idle value of a
// bus, which is a property of the reset state and not of the
// splitter.
if (busy4)
check_burst(baddr4, bbeats4, blen4, BND_R, MAXB_R, "real 4K");
// 5. contiguous: no gap and no overlap between consecutive bursts
checks = checks + 1;
if (baddr !== expect_addr) begin
errors = errors + 1;
if (errors < 25)
$display("FAIL burst %0d at 0x%0h, expected 0x%0h",
bindex, baddr, expect_addr);
end
expect_addr = baddr + bbeats * BPB;
seen = seen + bbeats;
burst_ack = 1'b1;
tickc;
burst_ack = 1'b0;
end
// 6. THE TILING. Everything asked for was delivered, once.
checks = checks + 1;
if (!stuck && (seen != nb)) begin
errors = errors + 1;
if (errors < 25)
$display("FAIL addr=0x%0h beats=%0d: delivered %0d", a0, nb, seen);
end
checks = checks + 1;
if (guard >= max_ok) begin
errors = errors + 1;
if (errors < 25)
$display("FAIL addr=0x%0h beats=%0d took %0d bursts; the limits allow at most %0d",
a0, nb, guard, max_ok - 1);
end
if (stuck) fail("a zero-beat burst: the transfer cannot progress");
// ---- Leave the DUT clean for the next case. ----
//
// A transfer that was abandoned leaves the design BUSY, and every
// subsequent descriptor is then refused as a restart -- so one failing
// case reports itself 8192 more times under a different name, and the
// real first failure is buried. Resetting here costs nothing on a
// passing run, where the design is already idle.
if (busy) reset_all;
end
endtask
task reset_all;
begin
start = 1'b0; burst_ack = 1'b0; eot = 1'b0;
rst_n = 1'b0;
@(posedge clk); #1;
rst_n = 1'b1;
@(negedge clk);
end
endtask
integer off, nb, i, w, base;
integer max_bursts, tot_bursts;
integer n_unal, n_zero_cases, n_rst;
initial begin
for (k = 0; k < 8192; k = k + 1) reach[k] = 1'b0;
max_bursts = 0; tot_bursts = 0;
n_unal = 0; n_zero_cases = 0; n_rst = 0;
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(negedge clk);
// ================= PHASE 1 -- every offset x every length ============
//
// 64 aligned offsets across one boundary window, and every beat count
// from 1 to two windows' worth. The bursts of every one of the 8192
// transfers are checked against all six properties.
for (off = 0; off < BND_S; off = off + BPB)
for (nb = 1; nb <= 128; nb = nb + 1) begin
run_xfer(32'h0001_0000 + off[31:0], nb[15:0]);
reach[(off / BPB) * 128 + (nb - 1)] = 1'b1;
if (n_burst - base > max_bursts) max_bursts = n_burst - base;
base = n_burst;
end
// ================= PHASE 2 -- the worked example, at 4 KB ============
//
// The case from the header, driven against the instance with the real
// AXI numbers: a buffer 256 bytes below a 4 KB boundary, 256 beats of 4
// bytes. It must come out as 64 beats and then 192, and the first burst
// must end exactly on the boundary.
reset_all;
start = 1'b1; addr = 32'h0000_1F00; beats = 16'd256; eot = 1'b0;
tickc;
start = 1'b0;
if (bbeats4 !== 16'd64) begin
errors = errors + 1;
$display("FAIL 0x1F00: first burst is %0d beats, expected 64", bbeats4);
end
if ((baddr4 + bbeats4 * BPB) !== 32'h0000_2000) begin
errors = errors + 1;
$display("FAIL 0x1F00: the first burst does not end on the boundary");
end
burst_ack = 1'b1; tickc; burst_ack = 1'b0;
if (baddr4 !== 32'h0000_2000) begin
errors = errors + 1;
$display("FAIL 0x1F00: the second burst starts at 0x%0h", baddr4);
end
if (bbeats4 !== 16'd192) begin
errors = errors + 1;
$display("FAIL 0x1F00: second burst is %0d beats, expected 192",
bbeats4);
end
burst_ack = 1'b1; tickc; burst_ack = 1'b0;
if (busy4) begin
errors = errors + 1;
$display("FAIL 0x1F00: the transfer did not finish in two bursts");
end
// ...and a buffer that IS 4 KB aligned needs no split at all.
reset_all;
start = 1'b1; addr = 32'h0000_2000; beats = 16'd256;
tickc;
start = 1'b0;
if (bbeats4 !== 16'd256) begin
errors = errors + 1;
$display("FAIL aligned 256 beats split into %0d", bbeats4);
end
burst_ack = 1'b1; tickc; burst_ack = 1'b0;
if (busy4) begin
errors = errors + 1;
$display("FAIL an aligned 256-beat transfer took more than one burst");
end
// ================= PHASE 3 -- the two limits are DIFFERENT ===========
//
// A long, perfectly aligned transfer is split by the beat limit and not
// by the boundary. Counting them together loses the distinction between
// "this buffer is badly aligned" and "this buffer is simply large",
// which are different conversations with whoever allocates them.
reset_all;
base = n_split_bound;
k = n_split_len;
run_xfer(32'h0002_0000, 16'd64); // aligned; 64 beats, MAXB_S = 16
if (n_split_len - k != 3) begin
errors = errors + 1;
$display("FAIL aligned long transfer: %0d length splits, expected 3",
n_split_len - k);
end
if (n_split_bound != base) begin
errors = errors + 1;
$display("FAIL aligned long transfer produced a boundary split");
end
// ...and a short transfer that straddles a boundary is split by the
// boundary and not by the length.
base = n_split_bound;
k = n_split_len;
run_xfer(32'h0002_00F8, 16'd4); // 8 bytes below a 256-byte boundary
if (n_split_bound - base != 1) begin
errors = errors + 1;
$display("FAIL straddling transfer: %0d boundary splits, expected 1",
n_split_bound - base);
end
if (n_split_len != k) begin
errors = errors + 1;
$display("FAIL straddling transfer produced a length split");
end
// ================= PHASE 4 -- the rejections, EXHAUSTIVELY ============
//
// Three things must be refused, and each one is refused for a reason that
// is quantified over something: every misaligned offset, every address,
// every point in a running transfer. The first version of this phase drove
// one case of each and the two mutations that accept them died on THREE
// and FOUR checks -- correct, and one phase reordering from zero.
//
// An INCR burst of this size must start on a beat boundary, so every
// non-zero offset within a beat is a rejection: BPB - 1 of them, at four
// different base addresses.
n_unal = 0;
for (i = 0; i < 4; i = i + 1)
for (off = 1; off < BPB; off = off + 1) begin
reset_all;
base = n_unaligned;
start = 1'b1; eot = 1'b0;
addr = 32'h0003_0000 + (i[31:0] << 8) + off[31:0];
beats = 16'd8;
tickc;
start = 1'b0;
if (n_unaligned != base + 1) begin
errors = errors + 1;
$display("FAIL offset %0d at base %0d was accepted", off, i);
end
// ...and nothing started: no burst is offered and no address latched.
if (busy) begin
errors = errors + 1;
$display("FAIL offset %0d started a transfer", off);
end
if (bv) begin
errors = errors + 1;
$display("FAIL offset %0d offered a burst", off);
end
if (err_code !== E_UNALIGNED) begin
errors = errors + 1;
$display("FAIL offset %0d reported code %0d", off, err_code);
end
n_unal = n_unal + 1;
end
if (n_unal != 4 * (BPB - 1)) begin
errors = errors + 1;
$display("FAIL unaligned cases %0d, expected %0d", n_unal, 4 * (BPB - 1));
end
// ...and a zero-beat descriptor, at every one of those base addresses.
// AXI has no encoding for it: `beats - 1` is 0xFFFF, so accepting it
// issues 256 beats of whatever was in the buffer.
n_zero_cases = 0;
for (i = 0; i < 4; i = i + 1) begin
reset_all;
base = n_zero;
start = 1'b1; eot = 1'b0;
addr = 32'h0004_0000 + (i[31:0] << 12);
beats = 16'd0;
tickc;
start = 1'b0;
if (n_zero != base + 1) begin
errors = errors + 1;
$display("FAIL a zero-beat descriptor at base %0d was accepted", i);
end
if (busy || bv) begin
errors = errors + 1;
$display("FAIL a zero-beat descriptor started a transfer");
end
if (err_code !== E_ZERO) begin
errors = errors + 1;
$display("FAIL zero-beat reported code %0d", err_code);
end
// ...and an aligned, non-zero descriptor at the same address IS
// accepted, so the rejection is about the beat count and not the
// address.
start = 1'b1; beats = 16'd8;
tickc;
start = 1'b0;
if (!busy) begin
errors = errors + 1;
$display("FAIL a valid descriptor at base %0d was refused", i);
end
burst_ack = 1'b1; tickc; burst_ack = 1'b0;
n_zero_cases = n_zero_cases + 1;
end
if (n_zero_cases != 4) begin
errors = errors + 1;
$display("FAIL zero-beat cases %0d, expected 4", n_zero_cases);
end
// ...and a restart, at every burst index of a multi-burst transfer. The
// running descriptor must be untouched: its address, its remaining count
// and its burst index all survive.
n_rst = 0;
for (i = 0; i < 4; i = i + 1) begin
reset_all;
start = 1'b1; eot = 1'b0;
addr = 32'h0005_0000; beats = 16'd64; // 4 bursts at MAXB_S = 16
tickc;
start = 1'b0;
for (nb = 0; nb < i; nb = nb + 1) begin
burst_ack = 1'b1; tickc; burst_ack = 1'b0;
end
base = n_restart;
w = baddr;
nb = remaining;
start = 1'b1; addr = 32'h0006_0000; beats = 16'd8;
tickc;
start = 1'b0;
if (n_restart != base + 1) begin
errors = errors + 1;
$display("FAIL a restart at burst %0d was not reported", i);
end
if (baddr !== w[31:0]) begin
errors = errors + 1;
$display("FAIL a restart at burst %0d moved the address", i);
end
if (remaining !== nb[15:0]) begin
errors = errors + 1;
$display("FAIL a restart at burst %0d changed the remaining count", i);
end
if (bindex !== i[15:0]) begin
errors = errors + 1;
$display("FAIL a restart at burst %0d moved the burst index", i);
end
// ...and the original transfer still completes. BOUNDED: an
// unbounded `while (busy)` in a testbench is an infinite loop the
// moment a mutation stops the design advancing, and the run never
// reaches the summary that would have told you which mutation.
off = 0;
while (busy && (off < 4096)) begin
burst_ack = 1'b1; tickc; burst_ack = 1'b0;
off = off + 1;
end
if (busy) begin
errors = errors + 1;
if (errors < 25)
$display("FAIL the transfer after a restart at burst %0d never finished", i);
reset_all;
end
n_rst = n_rst + 1;
end
if (n_rst != 4) begin
errors = errors + 1;
$display("FAIL restart cases %0d, expected 4", n_rst);
end
// The random phase is switchable, because a mutation score is only
// interesting once it is DECOMPOSED. The directed phases already reach
// every situation the exhaustiveness proof requires, so nothing in that
// claim depends on it.
`ifndef DIRECTED_ONLY
// ================= PHASE 5 -- random =================================
reset_all;
for (i = 0; i < 4000; i = i + 1) begin
w = ($unsigned($random) % 4096) & 32'hFFFF_FFFC; // aligned
run_xfer(32'h0010_0000 + w[31:0],
($unsigned($random) % 400) + 1);
end
`endif
// ================= the exhaustiveness proof ==========================
n_reach = 0;
for (k = 0; k < 8192; k = k + 1) n_reach = n_reach + reach[k];
if (n_reach != 8192) begin
errors = errors + 1;
$display("FAIL offset x length reach %0d/8192", n_reach);
end
$display("steps=%0d checks=%0d reach=%0d/8192 errors=%0d",
steps, checks, n_reach, errors);
// The counters below cover the random phase only: each directed phase
// resets the DUT so that its measurement starts from a known state, and
// a counter that spans a reset is a counter nobody can interpret.
$display("[random phase] start=%0d burst=%0d done=%0d",
n_start, n_burst, n_done);
$display("[random phase] split_boundary=%0d split_length=%0d",
n_split_bound, n_split_len);
$display("%0s: %0d errors in %0d checks",
(errors == 0) ? "PASS" : "FAIL", errors, checks);
$finish;
end
endmoduleSystemVerilog testbench
`timescale 1ns/1ps
// Testbench for usb_axi_burst.
//
// THE CHECKS ARE PROTOCOL PROPERTIES, NOT EXPECTED VALUES
//
// A burst splitter is one of the few blocks where the specification states
// the acceptance criteria directly, so the testbench asserts those rather
// than a table of expected outputs:
//
// 1. no burst crosses a BOUNDARY-byte address boundary
// 2. no burst exceeds MAX_BEATS beats
// 3. every burst carries at least one beat
// 4. AWLEN is the beat count MINUS ONE, every time
// 5. consecutive bursts are contiguous -- no gap, no overlap
// 6. the beats delivered sum to exactly the beats requested
//
// Property 1 is the one this chapter is about and property 6 is the one that
// catches a splitter which obeys property 1 by dropping data.
//
// THE EXHAUSTIVE CLAIM
//
// The splitting arithmetic depends on two numbers: where in the boundary
// window the buffer starts, and how many beats it is. So both are swept, all
// the way across, as a product:
//
// every aligned offset in a boundary window (64)
// x every beat count from 1 to 2 windows' worth (128)
// = 8192 transfers, every burst of every one checked
//
// The DUT is instantiated with a SCALED boundary so that product is
// tractable. A second instance carries the real 4096/256 values and is
// driven by the directed cases, because a parameter that is never changed is
// a constant with extra steps -- chapter 25.4's retry budget again.
module tb_ab_sv;
import usb_axi_pkg::*;
localparam int ADDR_W = 32;
localparam int DATA_W = 32;
localparam int BPB = DATA_W / 8;
// the scaled instance, for the exhaustive sweep
localparam int BND_S = 256;
localparam int MAXB_S = 16;
// the real instance, for the directed cases
localparam int BND_R = 4096;
localparam int MAXB_R = 256;
logic clk = 1'b0, rst_n = 1'b0;
logic start = 1'b0, burst_ack = 1'b0, eot = 1'b0;
logic [ADDR_W-1:0] addr = '0;
logic [15:0] beats = 16'd0;
logic busy, bv, done_pulse, err_pulse;
logic [ADDR_W-1:0] baddr;
logic [7:0] blen;
logic [2:0] bsize;
logic [15:0] bbeats, bindex, remaining;
axi_err_e err_code;
logic [31:0] n_start, n_burst, n_split_bound, n_split_len, n_done,
n_unaligned, n_zero, n_restart;
usb_axi_burst #(.ADDR_W(ADDR_W), .DATA_W(DATA_W),
.BOUNDARY(BND_S), .MAX_BEATS(MAXB_S)) dut (
.clk(clk), .rst_n(rst_n),
.start(start), .addr(addr), .beats(beats), .burst_ack(burst_ack),
.eot(eot),
.busy(busy), .bv(bv), .baddr(baddr), .blen(blen), .bsize(bsize),
.bbeats(bbeats), .bindex(bindex), .remaining(remaining),
.done_pulse(done_pulse), .err_pulse(err_pulse), .err_code(err_code),
.n_start(n_start), .n_burst(n_burst), .n_split_bound(n_split_bound),
.n_split_len(n_split_len), .n_done(n_done),
.n_unaligned(n_unaligned), .n_zero(n_zero), .n_restart(n_restart)
);
// ---- The same design with the REAL AXI numbers. ----
//
// Driven by the same stimulus and checked against the same properties. A
// splitter whose boundary is hard-coded to whatever the test used passes
// everything above and corrupts memory on the first real SoC.
logic busy4, bv4, done4;
logic [ADDR_W-1:0] baddr4;
logic [7:0] blen4;
logic [15:0] bbeats4, bindex4;
usb_axi_burst #(.ADDR_W(ADDR_W), .DATA_W(DATA_W),
.BOUNDARY(BND_R), .MAX_BEATS(MAXB_R)) dut4k (
.clk(clk), .rst_n(rst_n),
.start(start), .addr(addr), .beats(beats), .burst_ack(burst_ack),
.eot(eot),
.busy(busy4), .bv(bv4), .baddr(baddr4), .blen(blen4), .bsize(),
.bbeats(bbeats4), .bindex(bindex4), .remaining(),
.done_pulse(done4), .err_pulse(), .err_code(),
.n_start(), .n_burst(), .n_split_bound(), .n_split_len(), .n_done(),
.n_unaligned(), .n_zero(), .n_restart()
);
always #5 clk = ~clk;
int errors = 0, checks = 0, steps = 0;
int k;
// reach: aligned offset (64) x beat count (128)
bit reach [8192];
int n_reach;
task automatic ck(string nm, int unsigned got, int unsigned exp);
checks++;
if (got !== exp) begin
errors++;
if (errors < 25)
$display("FAIL t=%0t step=%0d %0s got=%0d exp=%0d",
$time, steps, nm, got, exp);
end
endtask
task automatic fail(string nm);
errors++;
if (errors < 25) $display("FAIL t=%0t step=%0d %0s", $time, steps, nm);
endtask
task automatic tickc;
begin
@(posedge clk); #1;
steps = steps + 1;
end
endtask
// ---- The six properties, checked on the burst being offered. ----
task automatic check_burst(logic [ADDR_W-1:0] a, logic [15:0] nb,
logic [7:0] ln, int bnd, int maxb, string who);
logic [31:0] off;
begin
// 3. at least one beat
checks = checks + 1;
if (nb == 16'd0) begin
errors = errors + 1;
if (errors < 25) $display("FAIL %0s: a zero-beat burst", who);
end
// 2. the AXI4 INCR limit
checks = checks + 1;
if (nb > 16'(maxb)) begin
errors = errors + 1;
if (errors < 25)
$display("FAIL %0s: %0d beats exceeds the %0d-beat limit",
who, nb, maxb);
end
// 4. AWLEN is beats minus one
checks = checks + 1;
if (ln !== (nb[7:0] - 8'd1)) begin
errors = errors + 1;
if (errors < 25)
$display("FAIL %0s: AWLEN %0d for %0d beats", who, ln, nb);
end
// 1. THE RULE. The burst must not cross the boundary.
off = a % 32'(bnd);
checks = checks + 1;
if ((off + 32'(nb) * BPB) > 32'(bnd)) begin
errors = errors + 1;
if (errors < 25)
$display("FAIL %0s: burst at 0x%0h (offset %0d) of %0d beats crosses the %0d-byte boundary",
who, a, off, nb, bnd);
end
end
endtask
// Run one whole transfer, checking every burst and the tiling.
task run_xfer;
input [ADDR_W-1:0] a0;
input [15:0] nb;
integer seen;
reg [ADDR_W-1:0] expect_addr;
integer guard;
int max_ok;
reg stuck;
begin
start = 1'b1; burst_ack = 1'b0; eot = 1'b0; addr = a0; beats = nb;
tickc;
start = 1'b0;
seen = 0;
expect_addr = a0;
guard = 0;
// ---- PROPERTY 7: a bound on the NUMBER of bursts. ----
//
// Each limit can only cut a transfer so many times. Within one
// boundary window the beat limit gives ceil(beats / MAX_BEATS) bursts,
// and the transfer spans a known number of windows -- so a correct
// splitter cannot need more than the sum of the two.
//
// It is a real property and it is also what keeps the suite usable: a
// mutation that computes the distance to the boundary wrongly emits
// ONE-BEAT bursts, which is correct-looking, terminating, and turns an
// eight-second run into a ten-minute one. Bounding the loop at 4096
// catches a design that hangs; bounding it at what the limits allow
// catches a design that crawls.
max_ok = ((int'(nb) + MAXB_S - 1) / MAXB_S)
+ (((int'(a0) % BND_S) + int'(nb) * BPB + BND_S - 1) / BND_S) + 1;
stuck = 1'b0;
while (busy && (guard < max_ok) && !stuck) begin
guard = guard + 1;
check_burst(baddr, bbeats, blen, BND_S, MAXB_S, "scaled");
// ---- FAIL FAST on a design that cannot progress. ----
//
// A zero-beat burst is not just wrong, it is unadvanceable: the
// address never moves and the remaining count never falls, so the
// loop below would run its full guard of 4096 for every one of the
// 8192 transfers. One mutation then reports NINETY-SIX MILLION
// failures and the other six are invisible beside it -- and the run
// takes minutes instead of seconds.
//
// A mutation score is only comparable if no mutation is allowed to
// spin. Bound the loop AND leave it on the first unadvanceable
// burst.
if (bbeats == 16'd0) stuck = 1'b1;
// The 4 KB instance needs fewer splits for the same transfer, so it
// finishes first and its outputs stop meaning anything. Checking a
// burst that is not being offered is checking the idle value of a
// bus, which is a property of the reset state and not of the
// splitter.
if (busy4)
check_burst(baddr4, bbeats4, blen4, BND_R, MAXB_R, "real 4K");
// 5. contiguous: no gap and no overlap between consecutive bursts
checks = checks + 1;
if (baddr !== expect_addr) begin
errors = errors + 1;
if (errors < 25)
$display("FAIL burst %0d at 0x%0h, expected 0x%0h",
bindex, baddr, expect_addr);
end
expect_addr = baddr + ADDR_W'(32'(bbeats) * BPB);
seen = seen + int'(bbeats);
burst_ack = 1'b1;
tickc;
burst_ack = 1'b0;
end
// 6. THE TILING. Everything asked for was delivered, once.
checks = checks + 1;
if (!stuck && (seen != nb)) begin
errors = errors + 1;
if (errors < 25)
$display("FAIL addr=0x%0h beats=%0d: delivered %0d", a0, nb, seen);
end
checks = checks + 1;
if (guard >= max_ok) begin
errors = errors + 1;
if (errors < 25)
$display("FAIL addr=0x%0h beats=%0d took %0d bursts; the limits allow at most %0d",
a0, nb, guard, max_ok - 1);
end
if (stuck) fail("a zero-beat burst: the transfer cannot progress");
// ---- Leave the DUT clean for the next case. ----
//
// A transfer that was abandoned leaves the design BUSY, and every
// subsequent descriptor is then refused as a restart -- so one failing
// case reports itself 8192 more times under a different name, and the
// real first failure is buried. Resetting here costs nothing on a
// passing run, where the design is already idle.
if (busy) reset_all;
end
endtask
task automatic reset_all;
begin
start = 1'b0; burst_ack = 1'b0; eot = 1'b0;
rst_n = 1'b0;
@(posedge clk); #1;
rst_n = 1'b1;
@(negedge clk);
end
endtask
int off, nb, i, w, base;
int max_bursts, tot_bursts;
integer n_unal, n_zero_cases, n_rst;
initial begin
foreach (reach[q]) reach[q] = 1'b0;
max_bursts = 0; tot_bursts = 0;
n_unal = 0; n_zero_cases = 0; n_rst = 0;
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(negedge clk);
// ================= PHASE 1 -- every offset x every length ============
//
// 64 aligned offsets across one boundary window, and every beat count
// from 1 to two windows' worth. The bursts of every one of the 8192
// transfers are checked against all six properties.
for (off = 0; off < BND_S; off = off + BPB)
for (nb = 1; nb <= 128; nb = nb + 1) begin
run_xfer(32'h0001_0000 + 32'(off), 16'(nb));
reach[(off / BPB) * 128 + (nb - 1)] = 1'b1;
if (int'(n_burst) - base > max_bursts) max_bursts = int'(n_burst) - base;
base = int'(n_burst);
end
// ================= PHASE 2 -- the worked example, at 4 KB ============
//
// The case from the header, driven against the instance with the real
// AXI numbers: a buffer 256 bytes below a 4 KB boundary, 256 beats of 4
// bytes. It must come out as 64 beats and then 192, and the first burst
// must end exactly on the boundary.
reset_all;
start = 1'b1; addr = 32'h0000_1F00; beats = 16'd256; eot = 1'b0;
tickc;
start = 1'b0;
if (bbeats4 !== 16'd64) begin
errors = errors + 1;
$display("FAIL 0x1F00: first burst is %0d beats, expected 64", bbeats4);
end
if ((baddr4 + bbeats4 * BPB) !== 32'h0000_2000) begin
errors = errors + 1;
$display("FAIL 0x1F00: the first burst does not end on the boundary");
end
burst_ack = 1'b1; tickc; burst_ack = 1'b0;
if (baddr4 !== 32'h0000_2000) begin
errors = errors + 1;
$display("FAIL 0x1F00: the second burst starts at 0x%0h", baddr4);
end
if (bbeats4 !== 16'd192) begin
errors = errors + 1;
$display("FAIL 0x1F00: second burst is %0d beats, expected 192",
bbeats4);
end
burst_ack = 1'b1; tickc; burst_ack = 1'b0;
if (busy4) begin
errors = errors + 1;
$display("FAIL 0x1F00: the transfer did not finish in two bursts");
end
// ...and a buffer that IS 4 KB aligned needs no split at all.
reset_all;
start = 1'b1; addr = 32'h0000_2000; beats = 16'd256;
tickc;
start = 1'b0;
if (bbeats4 !== 16'd256) begin
errors = errors + 1;
$display("FAIL aligned 256 beats split into %0d", bbeats4);
end
burst_ack = 1'b1; tickc; burst_ack = 1'b0;
if (busy4) begin
errors = errors + 1;
$display("FAIL an aligned 256-beat transfer took more than one burst");
end
// ================= PHASE 3 -- the two limits are DIFFERENT ===========
//
// A long, perfectly aligned transfer is split by the beat limit and not
// by the boundary. Counting them together loses the distinction between
// "this buffer is badly aligned" and "this buffer is simply large",
// which are different conversations with whoever allocates them.
reset_all;
base = int'(n_split_bound);
k = int'(n_split_len);
run_xfer(32'h0002_0000, 16'd64); // aligned; 64 beats, MAXB_S = 16
if (int'(n_split_len) - k != 3) begin
errors = errors + 1;
$display("FAIL aligned long transfer: %0d length splits, expected 3",
int'(n_split_len) - k);
end
if (int'(n_split_bound) != base) begin
errors = errors + 1;
$display("FAIL aligned long transfer produced a boundary split");
end
// ...and a short transfer that straddles a boundary is split by the
// boundary and not by the length.
base = int'(n_split_bound);
k = int'(n_split_len);
run_xfer(32'h0002_00F8, 16'd4); // 8 bytes below a 256-byte boundary
if (int'(n_split_bound) - base != 1) begin
errors = errors + 1;
$display("FAIL straddling transfer: %0d boundary splits, expected 1",
int'(n_split_bound) - base);
end
if (int'(n_split_len) != k) begin
errors = errors + 1;
$display("FAIL straddling transfer produced a length split");
end
// ================= PHASE 4 -- the rejections, EXHAUSTIVELY ============
//
// Three things must be refused, and each one is refused for a reason that
// is quantified over something: every misaligned offset, every address,
// every point in a running transfer. The first version of this phase drove
// one case of each and the two mutations that accept them died on THREE
// and FOUR checks -- correct, and one phase reordering from zero.
//
// An INCR burst of this size must start on a beat boundary, so every
// non-zero offset within a beat is a rejection: BPB - 1 of them, at four
// different base addresses.
n_unal = 0;
for (i = 0; i < 4; i = i + 1)
for (off = 1; off < BPB; off = off + 1) begin
reset_all;
base = int'(n_unaligned);
start = 1'b1; eot = 1'b0;
addr = 32'h0003_0000 + 32'(i << 8) + 32'(off);
beats = 16'd8;
tickc;
start = 1'b0;
if (int'(n_unaligned) != base + 1) begin
errors = errors + 1;
$display("FAIL offset %0d at base %0d was accepted", off, i);
end
// ...and nothing started: no burst is offered and no address latched.
if (busy) begin
errors = errors + 1;
$display("FAIL offset %0d started a transfer", off);
end
if (bv) begin
errors = errors + 1;
$display("FAIL offset %0d offered a burst", off);
end
if (err_code !== E_UNALIGNED) begin
errors = errors + 1;
$display("FAIL offset %0d reported code %0d", off, err_code);
end
n_unal = n_unal + 1;
end
if (n_unal != 4 * (BPB - 1)) begin
errors = errors + 1;
$display("FAIL unaligned cases %0d, expected %0d", n_unal, 4 * (BPB - 1));
end
// ...and a zero-beat descriptor, at every one of those base addresses.
// AXI has no encoding for it: `beats - 1` is 0xFFFF, so accepting it
// issues 256 beats of whatever was in the buffer.
n_zero_cases = 0;
for (i = 0; i < 4; i = i + 1) begin
reset_all;
base = int'(n_zero);
start = 1'b1; eot = 1'b0;
addr = 32'h0004_0000 + 32'(i << 12);
beats = 16'd0;
tickc;
start = 1'b0;
if (int'(n_zero) != base + 1) begin
errors = errors + 1;
$display("FAIL a zero-beat descriptor at base %0d was accepted", i);
end
if (busy || bv) begin
errors = errors + 1;
$display("FAIL a zero-beat descriptor started a transfer");
end
if (err_code !== E_ZERO) begin
errors = errors + 1;
$display("FAIL zero-beat reported code %0d", err_code);
end
// ...and an aligned, non-zero descriptor at the same address IS
// accepted, so the rejection is about the beat count and not the
// address.
start = 1'b1; beats = 16'd8;
tickc;
start = 1'b0;
if (!busy) begin
errors = errors + 1;
$display("FAIL a valid descriptor at base %0d was refused", i);
end
burst_ack = 1'b1; tickc; burst_ack = 1'b0;
n_zero_cases = n_zero_cases + 1;
end
if (n_zero_cases != 4) begin
errors = errors + 1;
$display("FAIL zero-beat cases %0d, expected 4", n_zero_cases);
end
// ...and a restart, at every burst index of a multi-burst transfer. The
// running descriptor must be untouched: its address, its remaining count
// and its burst index all survive.
n_rst = 0;
for (i = 0; i < 4; i = i + 1) begin
reset_all;
start = 1'b1; eot = 1'b0;
addr = 32'h0005_0000; beats = 16'd64; // 4 bursts at MAXB_S = 16
tickc;
start = 1'b0;
for (nb = 0; nb < i; nb = nb + 1) begin
burst_ack = 1'b1; tickc; burst_ack = 1'b0;
end
base = int'(n_restart);
w = int'(baddr);
nb = int'(remaining);
start = 1'b1; addr = 32'h0006_0000; beats = 16'd8;
tickc;
start = 1'b0;
if (int'(n_restart) != base + 1) begin
errors = errors + 1;
$display("FAIL a restart at burst %0d was not reported", i);
end
if (baddr !== 32'(w)) begin
errors = errors + 1;
$display("FAIL a restart at burst %0d moved the address", i);
end
if (remaining !== 16'(nb)) begin
errors = errors + 1;
$display("FAIL a restart at burst %0d changed the remaining count", i);
end
if (bindex !== 16'(i)) begin
errors = errors + 1;
$display("FAIL a restart at burst %0d moved the burst index", i);
end
// ...and the original transfer still completes. BOUNDED: an
// unbounded `while (busy)` in a testbench is an infinite loop the
// moment a mutation stops the design advancing, and the run never
// reaches the summary that would have told you which mutation.
off = 0;
while (busy && (off < 4096)) begin
burst_ack = 1'b1; tickc; burst_ack = 1'b0;
off = off + 1;
end
if (busy) begin
errors = errors + 1;
if (errors < 25)
$display("FAIL the transfer after a restart at burst %0d never finished", i);
reset_all;
end
n_rst = n_rst + 1;
end
if (n_rst != 4) begin
errors = errors + 1;
$display("FAIL restart cases %0d, expected 4", n_rst);
end
// The random phase is switchable, because a mutation score is only
// interesting once it is DECOMPOSED. The directed phases already reach
// every situation the exhaustiveness proof requires, so nothing in that
// claim depends on it.
`ifndef DIRECTED_ONLY
// ================= PHASE 5 -- random =================================
reset_all;
for (i = 0; i < 4000; i = i + 1) begin
w = int'(($unsigned($random) % 4096) & 32'hFFFF_FFFC); // aligned
run_xfer(32'h0010_0000 + 32'(w),
16'(($unsigned($random) % 400) + 1));
end
`endif
// ================= the exhaustiveness proof ==========================
n_reach = 0;
for (k = 0; k < 8192; k = k + 1) n_reach = n_reach + reach[k];
if (n_reach != 8192) begin
errors = errors + 1;
$display("FAIL offset x length reach %0d/8192", n_reach);
end
$display("steps=%0d checks=%0d reach=%0d/8192 errors=%0d",
steps, checks, n_reach, errors);
// The counters below cover the random phase only: each directed phase
// resets the DUT so that its measurement starts from a known state, and
// a counter that spans a reset is a counter nobody can interpret.
$display("[random phase] start=%0d burst=%0d done=%0d",
n_start, n_burst, n_done);
$display("[random phase] split_boundary=%0d split_length=%0d",
n_split_bound, n_split_len);
$display("%0s: %0d errors in %0d checks",
(errors == 0) ? "PASS" : "FAIL", errors, checks);
$finish;
end
endmoduleVHDL-2008 testbench
-- Testbench for usb_axi_burst (VHDL-2008).
--
-- THE CHECKS ARE PROTOCOL PROPERTIES, NOT EXPECTED VALUES
--
-- A burst splitter is one of the few blocks where the specification states
-- the acceptance criteria directly, so the testbench asserts those rather
-- than a table of expected outputs:
--
-- 1. no burst crosses a BOUNDARY-byte address boundary
-- 2. no burst exceeds MAX_BEATS beats
-- 3. every burst carries at least one beat
-- 4. AWLEN is the beat count MINUS ONE, every time
-- 5. consecutive bursts are contiguous -- no gap, no overlap
-- 6. the beats delivered sum to exactly the beats requested
--
-- Property 1 is the one this chapter is about and property 6 is the one that
-- catches a splitter which obeys property 1 by dropping data.
--
-- THE EXHAUSTIVE CLAIM
--
-- every aligned offset in a boundary window (64)
-- x every beat count from 1 to 2 windows' worth (128)
-- = 8192 transfers, every burst of every one checked
--
-- The DUT is instantiated with a SCALED boundary so that product is
-- tractable. A second instance carries the real 4096/256 values and is driven
-- by the directed cases, because a parameter that is never changed is a
-- constant with extra steps.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.usb_axi_pkg.all;
entity tb_ab_vhdl is
end entity;
architecture sim of tb_ab_vhdl is
constant ADDR_W : integer := 32;
constant DATA_W : integer := 32;
constant BPB : integer := DATA_W / 8;
constant BND_S : integer := 256; -- the scaled instance
constant MAXB_S : integer := 16;
constant BND_R : integer := 4096; -- the real one
constant MAXB_R : integer := 256;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal start : std_logic := '0';
signal burst_ack : std_logic := '0';
signal eot : std_logic := '0';
signal addr : unsigned(ADDR_W-1 downto 0) := (others => '0');
signal beats : unsigned(15 downto 0) := (others => '0');
signal busy_s, bv_s, done_s, err_s : std_logic;
signal baddr_s : unsigned(ADDR_W-1 downto 0);
signal blen_s : unsigned(7 downto 0);
signal bsize_s : unsigned(2 downto 0);
signal bbeats_s, bindex_s, rem_s : unsigned(15 downto 0);
signal ec_s : std_logic_vector(1 downto 0);
signal n_start_s, n_burst_s, n_sb_s, n_sl_s : unsigned(31 downto 0);
signal n_done_s, n_un_s, n_zr_s, n_rs_s : unsigned(31 downto 0);
signal busy4, bv4, done4, err4 : std_logic;
signal baddr4 : unsigned(ADDR_W-1 downto 0);
signal blen4 : unsigned(7 downto 0);
signal bsize4 : unsigned(2 downto 0);
signal bbeats4, bindex4, rem4 : unsigned(15 downto 0);
signal ec4 : std_logic_vector(1 downto 0);
signal d1, d2, d3, d4, d5, d6, d7, d8 : unsigned(31 downto 0);
signal done : boolean := false;
begin
clk <= not clk after 5 ns when not done else '0';
dut : entity work.usb_axi_burst
generic map (ADDR_W => ADDR_W, DATA_W => DATA_W,
BOUNDARY => BND_S, MAX_BEATS => MAXB_S)
port map (
clk => clk, rst_n => rst_n,
start => start, addr => addr, beats => beats,
burst_ack => burst_ack, eot => eot,
busy => busy_s, bv => bv_s, baddr => baddr_s, blen => blen_s,
bsize => bsize_s, bbeats => bbeats_s, bindex => bindex_s,
remaining => rem_s, done_pulse => done_s, err_pulse => err_s,
err_code => ec_s,
n_start => n_start_s, n_burst => n_burst_s, n_split_bound => n_sb_s,
n_split_len => n_sl_s, n_done => n_done_s, n_unaligned => n_un_s,
n_zero => n_zr_s, n_restart => n_rs_s
);
-- ---- The same design with the REAL AXI numbers. ----
--
-- Driven by the same stimulus and checked against the same properties. A
-- splitter whose boundary is hard-coded to whatever the test used passes
-- everything above and corrupts memory on the first real SoC.
dut4k : entity work.usb_axi_burst
generic map (ADDR_W => ADDR_W, DATA_W => DATA_W,
BOUNDARY => BND_R, MAX_BEATS => MAXB_R)
port map (
clk => clk, rst_n => rst_n,
start => start, addr => addr, beats => beats,
burst_ack => burst_ack, eot => eot,
busy => busy4, bv => bv4, baddr => baddr4, blen => blen4,
bsize => bsize4, bbeats => bbeats4, bindex => bindex4,
remaining => rem4, done_pulse => done4, err_pulse => err4,
err_code => ec4,
n_start => d1, n_burst => d2, n_split_bound => d3, n_split_len => d4,
n_done => d5, n_unaligned => d6, n_zero => d7, n_restart => d8
);
stim : process
variable errors, checks, steps : integer := 0;
type int_array is array (natural range <>) of integer;
variable reach : int_array(0 to 8191) := (others => 0);
variable n_reach : integer := 0;
variable base, kk, w_v : integer := 0;
variable n_unal, n_zero_cases, n_rst, wv, rv, gv : integer := 0;
variable ln : line;
-- A deterministic LFSR, so a rerun reproduces exactly the same traffic.
variable lfsr : unsigned(31 downto 0) := x"AC10FEED";
impure function rnd_nat return integer is
begin
lfsr := lfsr(30 downto 0) &
(lfsr(31) xor lfsr(21) xor lfsr(1) xor lfsr(0));
return to_integer(lfsr(29 downto 0));
end function;
procedure fail (nm : string) is
begin
errors := errors + 1;
if errors < 25 then
write(ln, string'("FAIL step=") & integer'image(steps) & " " & nm);
writeline(output, ln);
end if;
end procedure;
procedure tickc is
begin
wait until rising_edge(clk);
wait for 1 ns;
steps := steps + 1;
end procedure;
-- ---- The six properties, checked on the burst being offered. ----
procedure check_burst (a : unsigned(ADDR_W-1 downto 0);
nb : unsigned(15 downto 0);
ln_v : unsigned(7 downto 0);
bnd, maxb : integer; who : string) is
variable off : integer;
begin
-- 3. at least one beat
checks := checks + 1;
if nb = 0 then fail(who & ": a zero-beat burst"); end if;
-- 2. the AXI4 INCR limit
checks := checks + 1;
if to_integer(nb) > maxb then
fail(who & ": burst exceeds the beat limit");
end if;
-- 4. AWLEN is beats minus one
checks := checks + 1;
if ln_v /= (nb(7 downto 0) - 1) then
fail(who & ": AWLEN is not beats minus one");
end if;
-- 1. THE RULE. The burst must not cross the boundary.
off := to_integer(a) mod bnd;
checks := checks + 1;
if (off + to_integer(nb) * BPB) > bnd then
fail(who & ": the burst crosses the boundary");
end if;
end procedure;
procedure reset_all is
begin
start <= '0'; burst_ack <= '0'; eot <= '0';
rst_n <= '0';
wait until rising_edge(clk);
wait for 1 ns;
rst_n <= '1';
wait for 1 ns;
end procedure;
-- Run one whole transfer, checking every burst and the tiling.
procedure run_xfer (a0 : unsigned(ADDR_W-1 downto 0);
nb : unsigned(15 downto 0)) is
variable seen : integer := 0;
variable exp_a : unsigned(ADDR_W-1 downto 0);
variable guard : integer := 0;
variable max_ok : integer := 0;
variable stuck : boolean := false;
begin
start <= '1'; burst_ack <= '0'; eot <= '0';
addr <= a0; beats <= nb;
wait for 0 ns;
tickc;
start <= '0';
seen := 0;
exp_a := a0;
guard := 0;
-- ---- PROPERTY 7: a bound on the NUMBER of bursts. ----
--
-- Each limit can only cut a transfer so many times. Within one boundary
-- window the beat limit gives ceil(beats / MAX_BEATS) bursts, and the
-- transfer spans a known number of windows -- so a correct splitter
-- cannot need more than the sum of the two.
--
-- It is a real property and it is also what keeps the suite usable: a
-- mutation that computes the distance to the boundary wrongly emits
-- ONE-BEAT bursts, which is correct-looking, terminating, and turns a
-- two-second run into a two-minute one.
max_ok := ((to_integer(nb) + MAXB_S - 1) / MAXB_S)
+ (((to_integer(a0) mod BND_S) + to_integer(nb) * BPB
+ BND_S - 1) / BND_S) + 1;
stuck := false;
while busy_s = '1' and guard < max_ok and not stuck loop
guard := guard + 1;
check_burst(baddr_s, bbeats_s, blen_s, BND_S, MAXB_S, "scaled");
-- ---- FAIL FAST on a design that cannot progress. ----
--
-- A zero-beat burst is not just wrong, it is unadvanceable: the
-- address never moves and the remaining count never falls, so this
-- loop would run its full guard of 4096 for every one of the 8192
-- transfers. One mutation then reports ninety-six MILLION failures
-- and the other six are invisible beside it.
--
-- A mutation score is only comparable if no mutation is allowed to
-- spin.
if bbeats_s = 0 then stuck := true; end if;
-- The 4 KB instance needs fewer splits for the same transfer, so it
-- finishes first and its outputs stop meaning anything. Checking a
-- burst that is not being offered is checking the idle value of a
-- bus.
if busy4 = '1' then
check_burst(baddr4, bbeats4, blen4, BND_R, MAXB_R, "real 4K");
end if;
-- 5. contiguous: no gap and no overlap between consecutive bursts
checks := checks + 1;
if baddr_s /= exp_a then fail("bursts are not contiguous"); end if;
exp_a := baddr_s + resize(bbeats_s * to_unsigned(BPB, 16), ADDR_W);
seen := seen + to_integer(bbeats_s);
burst_ack <= '1';
wait for 0 ns;
tickc;
burst_ack <= '0';
wait for 0 ns;
end loop;
-- 6. THE TILING. Everything asked for was delivered, once.
checks := checks + 1;
if not stuck and seen /= to_integer(nb) then
fail("the bursts do not tile the range");
end if;
checks := checks + 1;
if guard >= max_ok then
fail("the transfer took more bursts than the limits allow");
end if;
if stuck then fail("a zero-beat burst: the transfer cannot progress"); end if;
-- ---- Leave the DUT clean for the next case. ----
--
-- A transfer that was abandoned leaves the design BUSY, and every
-- subsequent descriptor is then refused as a restart -- so one failing
-- case reports itself thousands more times under a different name.
if busy_s = '1' then reset_all; end if;
end procedure;
begin
wait until rising_edge(clk);
wait until rising_edge(clk);
wait until rising_edge(clk);
rst_n <= '1';
wait for 1 ns;
-- ================= PHASE 1 -- every offset x every length ============
for oi in 0 to (BND_S / BPB) - 1 loop
for nb in 1 to 128 loop
run_xfer(to_unsigned(16#10000# + oi * BPB, ADDR_W),
to_unsigned(nb, 16));
reach(oi * 128 + (nb - 1)) := 1;
end loop;
end loop;
-- ================= PHASE 2 -- the worked example, at 4 KB ============
reset_all;
start <= '1'; addr <= to_unsigned(16#1F00#, ADDR_W);
beats <= to_unsigned(256, 16); eot <= '0';
wait for 0 ns;
tickc;
start <= '0';
if bbeats4 /= 64 then fail("0x1F00: first burst is not 64 beats"); end if;
if (baddr4 + resize(bbeats4 * to_unsigned(BPB, 16), ADDR_W))
/= to_unsigned(16#2000#, ADDR_W) then
fail("0x1F00: the first burst does not end on the boundary");
end if;
burst_ack <= '1'; wait for 0 ns; tickc; burst_ack <= '0'; wait for 0 ns;
if baddr4 /= to_unsigned(16#2000#, ADDR_W) then
fail("0x1F00: the second burst does not start on the boundary");
end if;
if bbeats4 /= 192 then fail("0x1F00: second burst is not 192 beats"); end if;
burst_ack <= '1'; wait for 0 ns; tickc; burst_ack <= '0'; wait for 0 ns;
if busy4 = '1' then
fail("0x1F00: the transfer did not finish in two bursts");
end if;
-- ...and a buffer that IS 4 KB aligned needs no split at all.
reset_all;
start <= '1'; addr <= to_unsigned(16#2000#, ADDR_W);
beats <= to_unsigned(256, 16);
wait for 0 ns;
tickc;
start <= '0';
if bbeats4 /= 256 then fail("an aligned 256-beat transfer was split"); end if;
burst_ack <= '1'; wait for 0 ns; tickc; burst_ack <= '0'; wait for 0 ns;
if busy4 = '1' then fail("an aligned transfer took more than one burst"); end if;
-- ================= PHASE 3 -- the two limits are DIFFERENT ===========
reset_all;
base := to_integer(n_sb_s);
kk := to_integer(n_sl_s);
run_xfer(to_unsigned(16#20000#, ADDR_W), to_unsigned(64, 16));
if to_integer(n_sl_s) - kk /= 3 then
fail("aligned long transfer: wrong number of length splits");
end if;
if to_integer(n_sb_s) /= base then
fail("aligned long transfer produced a boundary split");
end if;
base := to_integer(n_sb_s);
kk := to_integer(n_sl_s);
run_xfer(to_unsigned(16#200F8#, ADDR_W), to_unsigned(4, 16));
if to_integer(n_sb_s) - base /= 1 then
fail("straddling transfer: wrong number of boundary splits");
end if;
if to_integer(n_sl_s) /= kk then
fail("straddling transfer produced a length split");
end if;
-- ================= PHASE 4 -- the rejections, EXHAUSTIVELY ============
--
-- Three things must be refused, and each one is refused for a reason that
-- is quantified over something. The first version of this phase drove one
-- case of each and the two mutations that accept them died on THREE and
-- FOUR checks -- correct, and one phase reordering from zero.
n_unal := 0;
for i in 0 to 3 loop
for off in 1 to BPB-1 loop
reset_all;
base := to_integer(n_un_s);
start <= '1'; eot <= '0';
addr <= to_unsigned(16#30000# + i * 256 + off, ADDR_W);
beats <= to_unsigned(8, 16);
wait for 0 ns;
tickc;
start <= '0';
if to_integer(n_un_s) /= base + 1 then
fail("an unaligned address was accepted");
end if;
-- ...and nothing started: no burst is offered and no address latched.
if busy_s = '1' or bv_s = '1' then
fail("an unaligned address started a transfer");
end if;
if ec_s /= E_UNALIGNED then fail("wrong code for unaligned"); end if;
n_unal := n_unal + 1;
end loop;
end loop;
if n_unal /= 4 * (BPB - 1) then
errors := errors + 1;
write(ln, string'("FAIL unaligned cases ") & integer'image(n_unal));
writeline(output, ln);
end if;
-- ...and a zero-beat descriptor, at every one of those base addresses.
-- AXI has no encoding for it: `beats - 1` is 0xFFFF, so accepting it
-- issues 256 beats of whatever was in the buffer.
n_zero_cases := 0;
for i in 0 to 3 loop
reset_all;
base := to_integer(n_zr_s);
start <= '1'; eot <= '0';
addr <= to_unsigned(16#40000# + i * 4096, ADDR_W);
beats <= to_unsigned(0, 16);
wait for 0 ns;
tickc;
start <= '0';
if to_integer(n_zr_s) /= base + 1 then
fail("a zero-beat descriptor was accepted");
end if;
if busy_s = '1' or bv_s = '1' then
fail("a zero-beat descriptor started a transfer");
end if;
if ec_s /= E_ZERO then fail("wrong code for zero beats"); end if;
-- ...and an aligned, non-zero descriptor at the same address IS
-- accepted, so the rejection is about the beat count and not the
-- address.
start <= '1'; beats <= to_unsigned(8, 16);
wait for 0 ns;
tickc;
start <= '0';
if busy_s /= '1' then fail("a valid descriptor was refused"); end if;
burst_ack <= '1'; wait for 0 ns; tickc; burst_ack <= '0'; wait for 0 ns;
n_zero_cases := n_zero_cases + 1;
end loop;
if n_zero_cases /= 4 then
errors := errors + 1;
write(ln, string'("FAIL zero-beat cases ") & integer'image(n_zero_cases));
writeline(output, ln);
end if;
-- ...and a restart, at every burst index of a multi-burst transfer. The
-- running descriptor must be untouched.
n_rst := 0;
for i in 0 to 3 loop
reset_all;
start <= '1'; eot <= '0';
addr <= to_unsigned(16#50000#, ADDR_W);
beats <= to_unsigned(64, 16);
wait for 0 ns;
tickc;
start <= '0';
for nb in 0 to i-1 loop
burst_ack <= '1'; wait for 0 ns; tickc; burst_ack <= '0'; wait for 0 ns;
end loop;
base := to_integer(n_rs_s);
wv := to_integer(baddr_s);
rv := to_integer(rem_s);
start <= '1'; addr <= to_unsigned(16#60000#, ADDR_W);
beats <= to_unsigned(8, 16);
wait for 0 ns;
tickc;
start <= '0';
if to_integer(n_rs_s) /= base + 1 then
fail("a restart was not reported");
end if;
if to_integer(baddr_s) /= wv then fail("a restart moved the address"); end if;
if to_integer(rem_s) /= rv then fail("a restart changed the remaining count"); end if;
if to_integer(bindex_s) /= i then fail("a restart moved the burst index"); end if;
-- BOUNDED: an unbounded `while busy` in a testbench is an infinite loop
-- the moment a mutation stops the design advancing, and the run never
-- reaches the summary that would have told you which mutation.
gv := 0;
while busy_s = '1' and gv < 4096 loop
burst_ack <= '1'; wait for 0 ns; tickc; burst_ack <= '0'; wait for 0 ns;
gv := gv + 1;
end loop;
if busy_s = '1' then
fail("the transfer after a restart never finished");
reset_all;
end if;
n_rst := n_rst + 1;
end loop;
if n_rst /= 4 then
errors := errors + 1;
write(ln, string'("FAIL restart cases ") & integer'image(n_rst));
writeline(output, ln);
end if;
-- ================= PHASE 5 -- random =================================
reset_all;
for i in 0 to 3999 loop
w_v := (rnd_nat mod 4096) / BPB * BPB; -- aligned
run_xfer(to_unsigned(16#100000# + w_v, ADDR_W),
to_unsigned((rnd_nat mod 400) + 1, 16));
end loop;
-- ================= the exhaustiveness proof ==========================
n_reach := 0;
for k in 0 to 8191 loop n_reach := n_reach + reach(k); end loop;
if n_reach /= 8192 then
errors := errors + 1;
write(ln, string'("FAIL offset x length reach ")
& integer'image(n_reach) & "/8192");
writeline(output, ln);
end if;
write(ln, string'("steps=") & integer'image(steps)
& " checks=" & integer'image(checks)
& " reach=" & integer'image(n_reach) & "/8192"
& " errors=" & integer'image(errors));
writeline(output, ln);
write(ln, string'("[random phase] start=")
& integer'image(to_integer(n_start_s))
& " burst=" & integer'image(to_integer(n_burst_s))
& " done=" & integer'image(to_integer(n_done_s)));
writeline(output, ln);
write(ln, string'("[random phase] split_boundary=")
& integer'image(to_integer(n_sb_s))
& " split_length=" & integer'image(to_integer(n_sl_s)));
writeline(output, ln);
if errors = 0 then
write(ln, string'("PASS: 0 errors in ") & integer'image(checks)
& " checks");
else
write(ln, string'("FAIL: ") & integer'image(errors) & " errors in " &
integer'image(checks) & " checks");
end if;
writeline(output, ln);
done <= true;
wait;
end process;
end architecture;11. Exhaustive Verification
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| (offset × beat count) reached | 8192 / 8192 | 8192 / 8192 | 8192 / 8192 |
| unaligned rejections swept | 12 / 12 | 12 / 12 | 12 / 12 |
| zero-beat rejections swept | 4 / 4 | 4 / 4 | 4 / 4 |
| restart positions swept | 4 / 4 | 4 / 4 | 4 / 4 |
| Steps | 106615 | 106615 | 105840 |
| Checks executed | 553104 | 553104 | 548877 |
| bursts issued (random phase) | 54378 | 54378 | 53603 |
| boundary splits / length splits | 12576 / 37802 | 12576 / 37802 | 12404 / 37199 |
| Result | PASS | PASS | PASS |
Every burst of every one of the 8192 transfers is checked against all seven properties, and against both the scaled instance and the one carrying the real 4096-byte boundary and 256-beat limit.
12. Mutation Testing
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| X3 | AWLEN carries the beat count, not count − 1 | 108587 | 108587 | 107724 |
| X5 | the address advances by beats instead of bytes | 93537 | 93537 | 92813 |
| X2 | the 256-beat limit is dropped | 27355 | 27355 | 27121 |
| X4 | the distance to the boundary is the offset, not the remainder | 21811 | 21811 | 21618 |
| X1 | the 4 KB boundary limit is dropped | 20315 | 20315 | 20019 |
| X7 | an unaligned start address is accepted | 48 | 48 | 36 |
| X6 | a zero-beat descriptor is accepted | 12 | 12 | 12 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages.
X1 — the mutation this chapter exists for — is nearly the lowest score in the table, and that is worth sitting with. Dropping the boundary limit does not corrupt a single byte inside the model: the bursts still tile the range exactly, still carry the right beats, still advance contiguously. Five of the six properties still hold. Only property 1 fails, once per crossing burst.
Directed against random
| # | All phases | Directed only | Random |
|---|---|---|---|
| X1 | 20315 | 7686 | 12629 |
| X2 | 27355 | 11606 | 15749 |
| X3 | 108587 | 48184 | 60403 |
| X4 | 21811 | 9884 | 11927 |
| X5 | 93537 | 41096 | 52441 |
| X6 | 12 | 12 | 0 |
| X7 | 48 | 48 | 0 |
Every one of the seven is killed by directed stimulus alone — that is the column that matters, and no mutation depends on the random phase to be caught.
For five of the seven the random phase nonetheless contributes the larger share, and the ratios are remarkably flat: roughly 40:60 directed to random for X1 through X5. That is what a well-mixed suite looks like. The two exceptions are X6 and X7, where the random contribution is exactly zero — the rejection paths, which no correct driver ever exercises.
13. Two Testbench Defects the Mutations Exposed
Neither of these is a design bug. Both are testbench bugs that only a mutant could reveal, and both are worth more than the mutation scores they distorted.
X4 reported ninety-six million failures. The mutation computes the distance to the boundary as the offset into the window rather than the distance remaining in it — which at an aligned address yields zero beats. A zero-beat burst is not merely wrong, it is unadvanceable: the address never moves and the remaining count never falls.
The drain loop was bounded at 4096 iterations, so it did not hang. It spun 4096 times for each of 8192 transfers instead, and one mutation's score buried the other six.
And an unbounded while (busy) in a directed phase was an outright hang. The restart phase drove a transfer to completion with while (busy) ack; — which under X4, at an aligned address, never terminates. The run never reached the summary that would have said which mutation was responsible.
An unbounded wait in a testbench is an infinite loop
the moment a mutation stops the design advancing.
Every wait gets a bound, and exceeding the bound is a
REPORTED FAILURE rather than a silent exit.14. Debugging Walkthrough: The Corruption That Moves When You Rebuild
The report. A camera bridge streams for hours on the development board and corrupts a filesystem on the production board within minutes. The corrupted bytes are in a completely unrelated memory region.
Step 1 — is it USB? Nothing in the USB stack reports an error. The transfers complete, the CRCs pass, the endpoint counters are clean.
Step 2 — where is the corruption? At an address about 3 KB above a buffer the USB driver owns. Sometimes 1 KB. It moves when the kernel is rebuilt, which is what makes it look like a memory-allocator bug.
Step 3 — what is 3 KB above a USB buffer? Whatever the allocator handed out next. The distance varies because the offset of the USB buffer within its page varies, and that offset is decided by an allocator whose behaviour changes with the kernel build.
Step 4 — so the write is going past the end of the buffer by a page boundary's worth. Instrument the AXI master. Every burst that crosses a 4 KB boundary is being routed on its first address — the whole burst lands in the first page, and the part that belonged in the second page overwrites whatever follows.
Step 5 — why the development board is fine. Its interconnect splits crossing bursts internally. It is not required to, it is not documented as doing so, and nobody knew it did.
Step 6 — why it took hours versus minutes. The development board's buffers happened to be page-aligned more often, because a different allocator was in use. The bug rate is the rate at which the allocator returns a buffer positioned so that a burst crosses.
15. UVM: A Protocol Monitor That Checks the Bus, Not the Bytes
// An AXI scoreboard normally answers "did the right data arrive at the right
// address". That question cannot see the bug in section 14, because the right
// data DID arrive -- at the address the interconnect chose to put it.
//
// This monitor answers a different question: is every transaction LEGAL. It
// never looks at write data at all.
class axi_aw_item extends uvm_sequence_item;
`uvm_object_utils(axi_aw_item)
rand bit [31:0] awaddr;
rand bit [7:0] awlen; // beats MINUS ONE
rand bit [2:0] awsize; // log2(bytes per beat)
rand bit [3:0] awid;
function new(string name = "axi_aw_item"); super.new(name); endfunction
function int unsigned beats(); return int'(awlen) + 1; endfunction
function int unsigned bytes_per_beat(); return 1 << awsize; endfunction
function int unsigned span(); return beats() * bytes_per_beat(); endfunction
endclass
// ---------------------------------------------------------------------
// The checker. Four rules, and the first is the one this chapter is about.
// ---------------------------------------------------------------------
class axi_protocol_monitor extends uvm_subscriber #(axi_aw_item);
`uvm_component_utils(axi_protocol_monitor)
localparam int BOUNDARY = 4096;
localparam int MAX_BEATS = 256;
int unsigned n_aw, n_cross, n_overlong, n_misaligned;
int unsigned n_by_id [int]; // outstanding per ID, for rule 4
function new(string name, uvm_component parent); super.new(name, parent);
endfunction
function void write(axi_aw_item t);
int unsigned off = t.awaddr % BOUNDARY;
n_aw++;
// ---- RULE 1: no burst crosses a 4 KB boundary. ----
//
// Checked on the ADDRESS CHANNEL alone, before any data exists. It needs
// no model of what the transfer is for, which is exactly why it catches a
// defect that every data-comparison scoreboard passes.
if (off + t.span() > BOUNDARY) begin
`uvm_error("AXI/4K",
$sformatf("burst at 0x%08h (offset %0d) of %0d beats x %0d bytes crosses a 4 KB boundary by %0d bytes",
t.awaddr, off, t.beats(), t.bytes_per_beat(),
off + t.span() - BOUNDARY))
n_cross++;
end
// ---- RULE 2: an INCR burst is at most 256 beats. ----
if (t.beats() > MAX_BEATS) begin
`uvm_error("AXI/LEN",
$sformatf("burst of %0d beats exceeds the AXI4 INCR limit", t.beats()))
n_overlong++;
end
// ---- RULE 3: the start address is aligned to the transfer size. ----
//
// Unaligned INCR is legal AXI only under the narrow-transfer rules, which
// a DMA engine has no reason to use and every reason to avoid: the byte
// lanes stop lining up with the data and the fabric will not tell you.
if ((t.awaddr % t.bytes_per_beat()) != 0) begin
`uvm_error("AXI/ALIGN",
$sformatf("burst at 0x%08h is not aligned to its %0d-byte beat size",
t.awaddr, t.bytes_per_beat()))
n_misaligned++;
end
n_by_id[t.awid]++;
endfunction
// ---- RULE 4: a descriptor chain that assumes ordering must use ONE ID. ----
//
// AXI guarantees ordering only WITHIN an ID. Two outstanding writes with
// different IDs may complete in either order, so a DMA engine that writes a
// descriptor's data and then its ownership bit under different IDs can have
// the ownership bit land first -- and software then reads a descriptor the
// hardware has not finished writing.
//
// It is not a rule the bus can check transaction by transaction, so it is
// checked as a property of the run: a stream that used more than one ID has
// given up ordering, and the report says so rather than leaving it implied.
function void report_phase(uvm_phase phase);
super.report_phase(phase);
`uvm_info("AXI",
$sformatf("%0d write bursts | %0d crossings, %0d over-long, %0d misaligned",
n_aw, n_cross, n_overlong, n_misaligned), UVM_LOW)
if (n_by_id.size() > 1)
`uvm_warning("AXI/IDS",
$sformatf("%0d distinct AWIDs were used: AXI orders transactions only WITHIN an ID, so nothing here is ordered with respect to anything else",
n_by_id.size()))
endfunction
endclass16. Common Misconceptions
"The interconnect will split a crossing burst." Some do. The protocol does not require it, and the ones that do not corrupt memory.
"It works on our SoC, so the engine is right." It works on your interconnect. That is a different claim.
"A crossing burst would produce an error." Only if something asserts one and something else is listening. Usually neither.
"One AXI port is enough." Register reads then queue behind 256-beat bursts, and the driver appears to hang under load.
"AWLEN is the beat count." It is the count minus one. There is no encoding for zero beats, which is why zero is refused.
"AWSIZE can be set once and forgotten." It must agree with the data width. A mismatch places bytes in the wrong lanes and reports nothing.
"Splitting is only about the boundary." Two limits apply at once, and counting them together loses the distinction between a misaligned buffer and a large one.
"AXI is ordered." Only within an ID. Across IDs it is not, and a descriptor chain that assumes otherwise breaks intermittently.
"A low mutation score means a weak check." X1 scores 20315 and is the whole chapter: it violates one property and satisfies the other five.
17. Exercises
1. A buffer at 0x3F80 is transferred as 64 beats of 8 bytes. Give the burst sequence, and the sequence if the beat size were 4 bytes instead.
2. Derive the bound ceil(beats / MAX_BEATS) + windows + 1 used as property 7, and say what a splitter would have to do to exceed it.
3. X1 drops the boundary limit and still satisfies properties 2 to 6. Construct a testbench check that models only data movement and show that it passes the mutant.
4. X6 and X7 have a random contribution of exactly zero. Explain why, and say what it implies about verifying error paths generally.
5. The design refuses a second descriptor while one is running. Argue for the alternative — queueing it — and say what the queue costs in state and in failure modes.
6. Two writes are issued under different AWIDs: a descriptor's data and its ownership bit. Construct the completion order that corrupts, and give two fixes.
7. Add support for WRAP bursts. Which of the seven properties change, which do not, and what new one is needed?
18. Summary
| Idea | Why it matters |
|---|---|
| Two AXI ports, opposite requirements | registers want latency, DMA wants throughput |
| A burst must not cross a 4 KB boundary | the protocol forbids it; some interconnects paper over it |
| Tolerant fabrics are worse than strict ones | the bug is invisible where it is written |
| Two limits apply at once | the boundary and the 256-beat maximum |
| Count the split reasons separately | misaligned and large are different conversations |
AWLEN is the count minus one | and zero beats has no encoding at all |
AWSIZE is derived from the data width | a mismatch misplaces bytes and reports nothing |
| AXI orders only within an ID | a descriptor chain across IDs is not ordered |
| Check the protocol, not the data | X1 satisfies five of six properties |
| Error paths have zero random coverage | correct stimulus never produces them |
| Bound every wait, and report the bound | an unbounded while (busy) hangs on the first mutant |
| Reset after an abandoned case | or one failure reports itself 8191 more times |
| 8192 offset × length transfers, 2 instances | 7 mutations, all killed in 3 languages |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o ab_v.out ab_v.v ab_v_tb.v && ./ab_v.out |
| SystemVerilog | iverilog -g2012 -o ab_sv.out ab_sv.sv ab_sv_tb.sv && ./ab_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a ab_vhdl.vhd ab_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_ab_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_ab_vhdl |
| One mutation | iverilog -g2005 -DMUT_X1 -o mm ab_v_mut.v ab_v_tb.v && ./mm |
| Directed only | iverilog -g2005 -DDIRECTED_ONLY -o mm ab_v_mut.v ab_v_tb.v && ./mm |
All three implementations pass with 0 errors: every aligned offset in a boundary window crossed with every beat count up to two windows — 8192 transfers — with all seven protocol properties checked on every burst of every one, against both a scaled instance and one carrying the real 4096-byte boundary and 256-beat limit, and every one of the seven mutations killed by directed stimulus alone.
Chapter 26.4 — USB Interrupt Integration is about the one wire that tells software any of this happened. Its central defect is a single line of RTL: the hardware sets a status bit on the same cycle the driver writes 1 to clear it, and if the clear wins that interrupt is not delayed — it is gone.
Continue learning
Related tutorials
- Related topic
USB Controllers on SoC
DWC2, MUSB and xHCI differ in a hundred mechanical ways that do not matter and one architectural way that does — whether endpoints own their packet buffers or share a pool.
- Related topic
DMA Integration
A descriptor has a byte count and the wire has packets, and the rule that converts one to the other is not ceil(length / packet size) — the version that is hangs on exactly the buffer sizes everybody uses.
- Related topic
USB Interrupt Integration
Hardware sets a status bit on the same cycle the driver writes 1 to clear it, and if the clear wins that interrupt is not delayed — it is gone, along with every event behind it.
- Related topic
Firmware Interaction
A register interface looks like memory and is not — reading can change it, writing 1 can clear it, one register can apply another, and none of that is anything a compiler knows.
Standards & specifications
- Governing standard
- USB-IF (Universal Serial Bus Specification)(opens USB Implementers Forum (USB-IF) in a new tab)
Defines the USB bus — its electrical signalling, connectors, packet and transaction model, device framework and the descriptors a device must expose — together with the device-class specifications layered on it. It does not define host-controller register interfaces (xHCI and EHCI are separate documents) nor any operating system's driver architecture.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the USB curriculum.
