Skip to content
VLSI Mentor

USB · Module 26

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.

Chapter 26.1 was about where the packet buffers live. This one is about what fills and empties them, and about a piece of arithmetic that looks like a rounding-up and is not.

1. A Descriptor Has a Length. The Wire Has Packets.

Software hands the controller a buffer address and a byte count. The controller has to turn that into a sequence of packets, none longer than the endpoint's maximum packet size, and the receiver has to be able to tell when the sequence has ended.

The obvious rule is ceil(L / M) packets. The correct rule is:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   n_full = L / M          full packets
   last   = L % M          then ONE more packet of this length

   total  = n_full + 1     always. For every L, including zero.

and last may be zero, in which case that final packet is a zero-length packet and it is not optional.

2. Why the Zero-Length Packet Is Not Optional

The receiver knows a transfer has ended when it sees a packet shorter than the maximum. That is the only in-band terminator there is.

So if the byte count happens to be an exact multiple of the packet size, every packet is full, nothing is short, and the receiver has no way to know the transfer finished. It waits for more.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   L = 63   ->  one 63-byte packet.            Short: done.
   L = 64   ->  one 64-byte packet + a ZLP.    The ZLP is the terminator.
   L = 65   ->  one 64 + one 1-byte packet.    Short: done.
   L = 0    ->  a single ZLP.                  A real transfer.

Three byte counts, three different sequences

A sequence diagram between a device and a host for three transfers over a 64-byte endpoint. A 63-byte transfer is one short packet which terminates it. A 64-byte transfer is one full packet, which does not terminate it, followed by a zero-length packet which does. A 65-byte transfer is one full packet followed by a one-byte short packet which terminates it.The packet sequence for 63, 64 and 65 bytes over a 64-byte endpointDeviceHostL = 63: DATA[63] —short, so doneL = 64: DATA[64] —full, NOT doneL = 64: DATA[0] —the ZLP terminatesitL = 65: DATA[64] —full, NOT doneL = 65: DATA[1] —short, so done
The terminator is whichever packet is shorter than the maximum. At 65 bytes the one-byte packet does that job for free; at 64 there is nothing short, so a zero-length packet has to be added. The total is n_full + 1 in all three cases, which is the formulation that has no exception.

3. And in the Other Direction, a Short Packet Ends It Early

On a receive, the host may simply send less than the descriptor asked for. The first short packet terminates the transfer and the difference is the residual:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   residual = length requested - bytes actually moved

So both endings are successes, and they mean different things to the layer above:

CompletionWhat happenedWhat the driver does next
D_LENGTHthe full count movedissue the next descriptor
D_SHORTthe peer ended it earlylook at the residual first

Collapsing them into one "done" bit throws away the only information the driver needs to decide which.

4. Babble Is Not a Short Read

The opposite case is a real error. If the peer sends more than the packet can hold, the extra bytes went somewhere — past the end of the buffer the driver allocated.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   fewer bytes than asked for   ->  a residual. Normal.
   more bytes than a packet     ->  BABBLE. A buffer overrun.

The two must not be reported the same way, and an engine that treats both as "length mismatch" makes a memory-corruption bug look like a routine short read.

5. What We Are Building

usb_dma_xfer takes a descriptor and drives the packet sequence, in either direction:

OutputWhat it is for
pkt_totaln_full + 1, computed once when the descriptor arrives
pkt_len / pkt_is_zlpthis packet's length; zero means the terminator
moved / residualhow much actually moved, and how much did not
done_codeD_LENGTH or D_SHORT — which ending it was
err_codebabble, a restart, or an acknowledgement with nothing outstanding

6. Verilog-2005 Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// usb_dma_xfer -- the arithmetic that decides where a USB transfer ends, and
// the two ways a DMA engine gets it wrong.
//
// A DESCRIPTOR HAS A LENGTH. THE WIRE HAS PACKETS.
//
// Software hands the controller a buffer and a byte count. The controller
// has to turn that into a sequence of packets, none longer than the
// endpoint's maximum packet size, and the host has to be able to tell when
// the sequence has ended.
//
// The rule is not "send ceil(L/M) packets". It is:
//
//     n_full = L / M          full packets
//     last   = L % M          then ONE more packet of this length
//
// and `last` may be ZERO, in which case that final packet is a
// ZERO-LENGTH PACKET and it is not optional.
//
// WHY THE ZLP IS NOT OPTIONAL
//
// The receiver knows a transfer has ended when it sees a packet SHORTER
// than the maximum. That is the only in-band terminator there is. So if the
// byte count happens to be an exact multiple of the packet size, every
// packet is full, nothing is short, and the receiver has no way to know the
// transfer finished -- it waits for more.
//
//     L = 63   ->  one 63-byte packet.           Short: done.
//     L = 64   ->  one 64-byte packet + a ZLP.   The ZLP is the terminator.
//     L = 65   ->  one 64 + one 1-byte packet.   Short: done.
//     L = 0    ->  a single ZLP.                 A real transfer.
//
// The total is n_full + 1 packets, always, for every L including zero. An
// engine that emits ceil(L/M) is correct for every length except the exact
// multiples -- which is to say it works in testing and hangs on the one
// buffer size everybody actually uses, because everybody rounds buffers up
// to a power of two.
//
// AND IN THE OTHER DIRECTION, A SHORT PACKET ENDS IT EARLY
//
// On a receive, the host may simply send less than the descriptor asked
// for. The first short packet terminates the transfer and the difference is
// the RESIDUAL:
//
//     residual = length requested - bytes actually moved
//
// A residual is NOT an error. It is the normal way a variable-length
// response arrives, and an engine that reports it as an under-run produces
// one false error per short read -- which on a bulk pipe is most of them.
//
// THE COMPLETION CODE SAYS WHICH OF THE TWO HAPPENED
//
// Both endings are successes and they mean different things to the layer
// above, so they are reported separately: D_LENGTH means the full count
// moved, D_SHORT means the peer ended it early and there is a residual.
// Collapsing them into one "done" bit throws away the only information the
// driver needs to decide whether to issue another transfer.
module usb_dma_xfer #(
  parameter integer MAXP = 64      // endpoint maximum packet size
) (
  input  wire        clk,
  input  wire        rst_n,

  input  wire        start,        // a descriptor is being handed over
  input  wire [15:0] len,          // its byte count
  input  wire        dir,          // 0 = TX (device sends), 1 = RX (receives)

  input  wire        pkt_ack,      // the packet on the wire completed
  input  wire [7:0]  rx_len,       // RX only: how many bytes actually arrived
  input  wire        eot,

  output wire        busy,
  output wire        pkt_valid,    // a packet is offered this cycle
  output wire [7:0]  pkt_len,      // its length; 0 means a ZLP
  output wire        pkt_is_zlp,
  output wire [15:0] pkt_index,    // 0-based position in the sequence
  output wire [15:0] pkt_total,    // how many there will be: n_full + 1
  output wire [15:0] moved,
  output wire [15:0] residual,
  output wire        done_pulse,
  output wire [1:0]  done_code,
  output wire        err_pulse,
  output wire [1:0]  err_code,

  output reg [31:0] n_start,
  output reg [31:0] n_pkt,
  output reg [31:0] n_zlp,
  output reg [31:0] n_done_len,
  output reg [31:0] n_done_short,
  output reg [31:0] n_restart,
  output reg [31:0] n_ack_idle,
  output reg [31:0] n_babble
);

  // Named, not bare bits. `dir_r` appears in three conditions and a bare
  // `if (dir_r)` reads as "if direction", which is not a question. The two
  // values behave completely differently -- one of them can end a transfer
  // early -- so they get names.
  localparam DIR_TX = 1'b0,   // the device sends
             DIR_RX = 1'b1;   // the device receives

  localparam [1:0] D_NONE   = 2'd0,
                   D_LENGTH = 2'd1,   // the full count moved
                   D_SHORT  = 2'd2;   // the peer ended it early; see residual

  localparam [1:0] E_NONE    = 2'd0,
                   E_RESTART = 2'd1,  // a new descriptor while one is running
                   E_ACKIDLE = 2'd2,  // a packet completed with none offered
                   E_BABBLE  = 2'd3;  // the peer sent more than was asked for

  // Widths derived from the parameter's own type. A packet length needs
  // eight bits because MAXP is 64 and the count runs 0..MAXP inclusive --
  // not because 64 happens to fit in seven.
  localparam [15:0] MAXP16 = MAXP;
  localparam [7:0]  MAXP8  = MAXP;

  reg [15:0] len_r;       // the descriptor's byte count
  reg [15:0] idx_r;       // which packet we are on
  reg [15:0] tot_r;       // how many there will be
  reg [15:0] moved_r;
  reg        busy_r, dir_r;
  reg        dn_r, er_r;
  reg [1:0]  dc_r, ec_r;

  // ---- The sequence, computed once when the descriptor arrives. ----
  //
  // n_full + 1, for every length including zero. Writing it as ceil(L/M) is
  // correct for every length that is NOT an exact multiple of the packet
  // size, which is why it survives testing.
  function [15:0] total_pkts;
    input [15:0] l;
    begin
      total_pkts = (l / MAXP16) + 16'd1;
    end
  endfunction

  // The length of packet `i` of a transfer of `l` bytes: full until the
  // last, and the last is the remainder -- which is zero exactly when the
  // ZLP is required.
  function [7:0] pkt_len_of;
    input [15:0] l;
    input [15:0] i;
    begin
      if (i < (l / MAXP16)) pkt_len_of = MAXP8;
      else                  pkt_len_of = (l % MAXP16) & 16'h00FF;
    end
  endfunction

  assign busy       = busy_r;
  assign pkt_valid  = busy_r;
  assign pkt_len    = pkt_len_of(len_r, idx_r);
  assign pkt_is_zlp = busy_r && (pkt_len_of(len_r, idx_r) == 8'd0);
  assign pkt_index  = idx_r;
  assign pkt_total  = tot_r;
  assign moved      = moved_r;
  // A residual is what was asked for and did not arrive. It is meaningful
  // only once the transfer has ended, and it is not an error.
  assign residual   = len_r - moved_r;
  assign done_pulse = dn_r;
  assign done_code  = dc_r;
  assign err_pulse  = er_r;
  assign err_code   = ec_r;

  reg [15:0] len_n, idx_n, tot_n, moved_n;
  reg        busy_n, dir_n, dn_n, er_n;
  reg [1:0]  dc_n, ec_n;
  reg        st_n, pk_n, zl_n, dl_n, ds_n, rs_n, ai_n, bb_n;
  reg [7:0]  this_len;
  reg [15:0] take;

  always @* begin
    len_n   = len_r;
    idx_n   = idx_r;
    tot_n   = tot_r;
    moved_n = moved_r;
    busy_n  = busy_r;
    dir_n   = dir_r;
    dn_n    = 1'b0; er_n = 1'b0;
    dc_n    = D_NONE; ec_n = E_NONE;
    st_n = 1'b0; pk_n = 1'b0; zl_n = 1'b0; dl_n = 1'b0;
    ds_n = 1'b0; rs_n = 1'b0; ai_n = 1'b0; bb_n = 1'b0;
    this_len = pkt_len_of(len_r, idx_r);

    if (eot) begin
      // Nothing: the counters are the report.
    end else if (start) begin
      if (busy_r) begin
        // A second descriptor while one is still running. The hardware can
        // only track one, so the new one would silently replace the old and
        // the driver would wait for a completion that never comes.
        er_n = 1'b1; ec_n = E_RESTART;
        rs_n = 1'b1;
      end else begin
        len_n   = len;
        idx_n   = 16'd0;
        tot_n   = total_pkts(len);
        moved_n = 16'd0;
        busy_n  = 1'b1;
        dir_n   = dir;
        st_n    = 1'b1;
      end
    end else if (pkt_ack) begin
      if (!busy_r) begin
        // A packet completed with nothing offered. On real hardware this is
        // a controller and a driver that disagree about whose turn it is.
        er_n = 1'b1; ec_n = E_ACKIDLE;
        ai_n = 1'b1;
      end else begin
        pk_n = 1'b1;
        if (this_len == 8'd0) zl_n = 1'b1;

        // ---- How many bytes actually moved. ----
        //
        // On TX it is what we offered. On RX it is what arrived, which is
        // the peer's choice and may be less.
        take = (dir_r == DIR_RX) ? {8'd0, rx_len} : {8'd0, this_len};

        if ((dir_r == DIR_RX) && ({8'd0, rx_len} > {8'd0, this_len})) begin
          // The peer sent more than the packet could hold. This is babble,
          // and it is a real error rather than a short read: the extra bytes
          // went somewhere.
          er_n = 1'b1; ec_n = E_BABBLE;
          bb_n = 1'b1;
          take = {8'd0, this_len};
        end

        moved_n = moved_r + take;

        if ((dir_r == DIR_RX) && (take < {8'd0, this_len})) begin
          // ---- A SHORT packet on receive ends the transfer. ----
          //
          // Not an error. The peer had less to send than we asked for, which
          // is how every variable-length response arrives. The difference is
          // the residual and the driver needs it.
          busy_n = 1'b0;
          dn_n   = 1'b1; dc_n = D_SHORT;
          ds_n   = 1'b1;
        end else if (idx_r + 16'd1 >= tot_r) begin
          // ---- The sequence is complete. ----
          //
          // Including the case where the last packet was a ZLP, which is a
          // packet like any other and has to be acknowledged before the
          // transfer is done.
          busy_n = 1'b0;
          dn_n   = 1'b1; dc_n = D_LENGTH;
          dl_n   = 1'b1;
        end else begin
          idx_n = idx_r + 16'd1;
        end
      end
    end
  end

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      len_r   <= 16'd0;
      idx_r   <= 16'd0;
      tot_r   <= 16'd0;
      moved_r <= 16'd0;
      busy_r  <= 1'b0;
      dir_r   <= DIR_TX;
      dn_r    <= 1'b0; er_r <= 1'b0;
      dc_r    <= D_NONE; ec_r <= E_NONE;
      n_start      <= 32'd0;
      n_pkt        <= 32'd0;
      n_zlp        <= 32'd0;
      n_done_len   <= 32'd0;
      n_done_short <= 32'd0;
      n_restart    <= 32'd0;
      n_ack_idle   <= 32'd0;
      n_babble     <= 32'd0;
    end else begin
      len_r   <= len_n;
      idx_r   <= idx_n;
      tot_r   <= tot_n;
      moved_r <= moved_n;
      busy_r  <= busy_n;
      dir_r   <= dir_n;
      dn_r    <= dn_n; er_r <= er_n;
      dc_r    <= dc_n; ec_r <= ec_n;

      if (st_n) n_start      <= n_start      + 32'd1;
      if (pk_n) n_pkt        <= n_pkt        + 32'd1;
      if (zl_n) n_zlp        <= n_zlp        + 32'd1;
      if (dl_n) n_done_len   <= n_done_len   + 32'd1;
      if (ds_n) n_done_short <= n_done_short + 32'd1;
      if (rs_n) n_restart    <= n_restart    + 32'd1;
      if (ai_n) n_ack_idle   <= n_ack_idle   + 32'd1;
      if (bb_n) n_babble     <= n_babble     + 32'd1;
    end
  end
endmodule

7. SystemVerilog Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// usb_dma_xfer -- the arithmetic that decides where a USB transfer ends, and
// the two ways a DMA engine gets it wrong.
//
// A DESCRIPTOR HAS A LENGTH. THE WIRE HAS PACKETS.
//
// Software hands the controller a buffer and a byte count. The controller
// has to turn that into a sequence of packets, none longer than the
// endpoint's maximum packet size, and the host has to be able to tell when
// the sequence has ended.
//
// The rule is not "send ceil(L/M) packets". It is:
//
//     n_full = L / M          full packets
//     last   = L % M          then ONE more packet of this length
//
// and `last` may be ZERO, in which case that final packet is a
// ZERO-LENGTH PACKET and it is not optional.
//
// WHY THE ZLP IS NOT OPTIONAL
//
// The receiver knows a transfer has ended when it sees a packet SHORTER
// than the maximum. That is the only in-band terminator there is. So if the
// byte count happens to be an exact multiple of the packet size, every
// packet is full, nothing is short, and the receiver has no way to know the
// transfer finished -- it waits for more.
//
//     L = 63   ->  one 63-byte packet.           Short: done.
//     L = 64   ->  one 64-byte packet + a ZLP.   The ZLP is the terminator.
//     L = 65   ->  one 64 + one 1-byte packet.   Short: done.
//     L = 0    ->  a single ZLP.                 A real transfer.
//
// The total is n_full + 1 packets, always, for every L including zero. An
// engine that emits ceil(L/M) is correct for every length except the exact
// multiples -- which is to say it works in testing and hangs on the one
// buffer size everybody actually uses, because everybody rounds buffers up
// to a power of two.
//
// AND IN THE OTHER DIRECTION, A SHORT PACKET ENDS IT EARLY
//
// On a receive, the host may simply send less than the descriptor asked
// for. The first short packet terminates the transfer and the difference is
// the RESIDUAL:
//
//     residual = length requested - bytes actually moved
//
// A residual is NOT an error. It is the normal way a variable-length
// response arrives, and an engine that reports it as an under-run produces
// one false error per short read -- which on a bulk pipe is most of them.
//
// THE COMPLETION CODE SAYS WHICH OF THE TWO HAPPENED
//
// Both endings are successes and they mean different things to the layer
// above, so they are reported separately: D_LENGTH means the full count
// moved, D_SHORT means the peer ended it early and there is a residual.
// Collapsing them into one "done" bit throws away the only information the
// driver needs to decide whether to issue another transfer.
package usb_dma_pkg;
  typedef enum logic {
    DIR_TX = 1'b0,    // the device sends
    DIR_RX = 1'b1     // the device receives
  } xfer_dir_e;

  // Both endings are SUCCESSES and they mean different things to the layer
  // above, so they are separate values rather than one "done" bit.
  typedef enum logic [1:0] {
    D_NONE   = 2'd0,
    D_LENGTH = 2'd1,  // the full count moved
    D_SHORT  = 2'd2   // the peer ended it early; see residual
  } done_code_e;

  typedef enum logic [1:0] {
    E_NONE    = 2'd0,
    E_RESTART = 2'd1, // a new descriptor while one is running
    E_ACKIDLE = 2'd2, // a packet completed with none offered
    E_BABBLE  = 2'd3  // the peer sent more than was asked for
  } dma_err_e;
endpackage

module usb_dma_xfer
  import usb_dma_pkg::*;
 #(
  parameter int MAXP = 64          // endpoint maximum packet size
) (
  input  logic        clk,
  input  logic        rst_n,

  input  logic        start,       // a descriptor is being handed over
  input  logic [15:0] len,         // its byte count
  input  xfer_dir_e   dir,

  input  logic        pkt_ack,     // the packet on the wire completed
  input  logic [7:0]  rx_len,      // RX only: how many bytes actually arrived
  input  logic        eot,

  output logic        busy,
  output logic        pkt_valid,   // a packet is offered this cycle
  output logic [7:0]  pkt_len,     // its length; 0 means a ZLP
  output logic        pkt_is_zlp,
  output logic [15:0] pkt_index,   // 0-based position in the sequence
  output logic [15:0] pkt_total,   // how many there will be: n_full + 1
  output logic [15:0] moved,
  output logic [15:0] residual,
  output logic        done_pulse,
  output done_code_e  done_code,
  output logic        err_pulse,
  output dma_err_e    err_code,

  output logic [31:0] n_start,
  output logic [31:0] n_pkt,
  output logic [31:0] n_zlp,
  output logic [31:0] n_done_len,
  output logic [31:0] n_done_short,
  output logic [31:0] n_restart,
  output logic [31:0] n_ack_idle,
  output logic [31:0] n_babble
);

  // Widths derived from the parameter's own type. A packet length needs
  // eight bits because MAXP is 64 and the count runs 0..MAXP inclusive --
  // not because 64 happens to fit in seven.
  localparam logic [15:0] MAXP16 = 16'(MAXP);
  localparam logic [7:0]  MAXP8  = 8'(MAXP);

  logic [15:0] len_r;     // the descriptor's byte count
  logic [15:0] idx_r;     // which packet we are on
  logic [15:0] tot_r;     // how many there will be
  logic [15:0] moved_r;
  logic        busy_r;
  xfer_dir_e   dir_r;
  logic        dn_r, er_r;
  done_code_e  dc_r;
  dma_err_e    ec_r;

  // ---- The sequence, computed once when the descriptor arrives. ----
  //
  // n_full + 1, for every length including zero. Writing it as ceil(L/M) is
  // correct for every length that is NOT an exact multiple of the packet
  // size, which is why it survives testing.
  function automatic logic [15:0] total_pkts(input logic [15:0] l);
    begin
      total_pkts = (l / MAXP16) + 16'd1;
    end
  endfunction

  // The length of packet `i` of a transfer of `l` bytes: full until the
  // last, and the last is the remainder -- which is zero exactly when the
  // ZLP is required.
  function automatic logic [7:0] pkt_len_of(input logic [15:0] l,
                                            input logic [15:0] i);
    begin
      if (i < (l / MAXP16)) pkt_len_of = MAXP8;
      else                  pkt_len_of = (l % MAXP16) & 16'h00FF;
    end
  endfunction

  assign busy       = busy_r;
  assign pkt_valid  = busy_r;
  assign pkt_len    = pkt_len_of(len_r, idx_r);
  assign pkt_is_zlp = busy_r && (pkt_len_of(len_r, idx_r) == 8'd0);
  assign pkt_index  = idx_r;
  assign pkt_total  = tot_r;
  assign moved      = moved_r;
  // A residual is what was asked for and did not arrive. It is meaningful
  // only once the transfer has ended, and it is not an error.
  assign residual   = len_r - moved_r;
  assign done_pulse = dn_r;
  assign done_code  = dc_r;
  assign err_pulse  = er_r;
  assign err_code   = ec_r;

  logic [15:0] len_n, idx_n, tot_n, moved_n;
  logic        busy_n, dn_n, er_n;
  xfer_dir_e   dir_n;
  done_code_e  dc_n;
  dma_err_e    ec_n;
  logic        st_n, pk_n, zl_n, dl_n, ds_n, rs_n, ai_n, bb_n;
  logic [7:0]  this_len;
  logic [15:0] take;

  always_comb begin
    len_n   = len_r;
    idx_n   = idx_r;
    tot_n   = tot_r;
    moved_n = moved_r;
    busy_n  = busy_r;
    dir_n   = dir_r;
    dn_n    = 1'b0; er_n = 1'b0;
    dc_n    = D_NONE; ec_n = E_NONE;
    st_n = 1'b0; pk_n = 1'b0; zl_n = 1'b0; dl_n = 1'b0;
    ds_n = 1'b0; rs_n = 1'b0; ai_n = 1'b0; bb_n = 1'b0;
    this_len = pkt_len_of(len_r, idx_r);

    if (eot) begin
      // Nothing: the counters are the report.
    end else if (start) begin
      if (busy_r) begin
        // A second descriptor while one is still running. The hardware can
        // only track one, so the new one would silently replace the old and
        // the driver would wait for a completion that never comes.
        er_n = 1'b1; ec_n = E_RESTART;
        rs_n = 1'b1;
      end else begin
        len_n   = len;
        idx_n   = 16'd0;
        tot_n   = total_pkts(len);
        moved_n = 16'd0;
        busy_n  = 1'b1;
        dir_n   = dir;
        st_n    = 1'b1;
      end
    end else if (pkt_ack) begin
      if (!busy_r) begin
        // A packet completed with nothing offered. On real hardware this is
        // a controller and a driver that disagree about whose turn it is.
        er_n = 1'b1; ec_n = E_ACKIDLE;
        ai_n = 1'b1;
      end else begin
        pk_n = 1'b1;
        if (this_len == 8'd0) zl_n = 1'b1;

        // ---- How many bytes actually moved. ----
        //
        // On TX it is what we offered. On RX it is what arrived, which is
        // the peer's choice and may be less.
        take = (dir_r == DIR_RX) ? {8'd0, rx_len} : {8'd0, this_len};

        if ((dir_r == DIR_RX) && ({8'd0, rx_len} > {8'd0, this_len})) begin
          // The peer sent more than the packet could hold. This is babble,
          // and it is a real error rather than a short read: the extra bytes
          // went somewhere.
          er_n = 1'b1; ec_n = E_BABBLE;
          bb_n = 1'b1;
          take = {8'd0, this_len};
        end

        moved_n = moved_r + take;

        if ((dir_r == DIR_RX) && (take < {8'd0, this_len})) begin
          // ---- A SHORT packet on receive ends the transfer. ----
          //
          // Not an error. The peer had less to send than we asked for, which
          // is how every variable-length response arrives. The difference is
          // the residual and the driver needs it.
          busy_n = 1'b0;
          dn_n   = 1'b1; dc_n = D_SHORT;
          ds_n   = 1'b1;
        end else if (idx_r + 16'd1 >= tot_r) begin
          // ---- The sequence is complete. ----
          //
          // Including the case where the last packet was a ZLP, which is a
          // packet like any other and has to be acknowledged before the
          // transfer is done.
          busy_n = 1'b0;
          dn_n   = 1'b1; dc_n = D_LENGTH;
          dl_n   = 1'b1;
        end else begin
          idx_n = idx_r + 16'd1;
        end
      end
    end
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      len_r   <= 16'd0;
      idx_r   <= 16'd0;
      tot_r   <= 16'd0;
      moved_r <= 16'd0;
      busy_r  <= 1'b0;
      dir_r   <= DIR_TX;
      dn_r    <= 1'b0; er_r <= 1'b0;
      dc_r    <= D_NONE; ec_r <= E_NONE;
      n_start      <= 32'd0;
      n_pkt        <= 32'd0;
      n_zlp        <= 32'd0;
      n_done_len   <= 32'd0;
      n_done_short <= 32'd0;
      n_restart    <= 32'd0;
      n_ack_idle   <= 32'd0;
      n_babble     <= 32'd0;
    end else begin
      len_r   <= len_n;
      idx_r   <= idx_n;
      tot_r   <= tot_n;
      moved_r <= moved_n;
      busy_r  <= busy_n;
      dir_r   <= dir_n;
      dn_r    <= dn_n; er_r <= er_n;
      dc_r    <= dc_n; ec_r <= ec_n;

      if (st_n) n_start      <= n_start      + 32'd1;
      if (pk_n) n_pkt        <= n_pkt        + 32'd1;
      if (zl_n) n_zlp        <= n_zlp        + 32'd1;
      if (dl_n) n_done_len   <= n_done_len   + 32'd1;
      if (ds_n) n_done_short <= n_done_short + 32'd1;
      if (rs_n) n_restart    <= n_restart    + 32'd1;
      if (ai_n) n_ack_idle   <= n_ack_idle   + 32'd1;
      if (bb_n) n_babble     <= n_babble     + 32'd1;
    end
  end
endmodule

8. VHDL-2008 Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- usb_dma_xfer -- the arithmetic that decides where a USB transfer ends, and
-- the two ways a DMA engine gets it wrong.
--
-- A DESCRIPTOR HAS A LENGTH. THE WIRE HAS PACKETS.
--
-- Software hands the controller a buffer and a byte count. The controller
-- has to turn that into a sequence of packets, none longer than the
-- endpoint's maximum packet size, and the host has to be able to tell when
-- the sequence has ended.
--
-- The rule is not "send ceil(L/M) packets". It is:
--
--     n_full = L / M          full packets
--     last   = L mod M        then ONE more packet of this length
--
-- and `last` may be ZERO, in which case that final packet is a ZERO-LENGTH
-- PACKET and it is not optional.
--
-- WHY THE ZLP IS NOT OPTIONAL
--
-- The receiver knows a transfer has ended when it sees a packet SHORTER than
-- the maximum. That is the only in-band terminator there is. So if the byte
-- count happens to be an exact multiple of the packet size, every packet is
-- full, nothing is short, and the receiver has no way to know the transfer
-- finished -- it waits for more.
--
--     L = 63   ->  one 63-byte packet.           Short: done.
--     L = 64   ->  one 64-byte packet + a ZLP.   The ZLP is the terminator.
--     L = 65   ->  one 64 + one 1-byte packet.   Short: done.
--     L = 0    ->  a single ZLP.                 A real transfer.
--
-- The total is n_full + 1 packets, always, for every L including zero. An
-- engine that emits ceil(L/M) is correct for every length except the exact
-- multiples -- which is to say it works in testing and hangs on the one
-- buffer size everybody actually uses, because everybody rounds buffers up
-- to a power of two.
--
-- AND IN THE OTHER DIRECTION, A SHORT PACKET ENDS IT EARLY
--
-- On a receive, the host may simply send less than the descriptor asked for.
-- The first short packet terminates the transfer and the difference is the
-- RESIDUAL:
--
--     residual = length requested - bytes actually moved
--
-- A residual is NOT an error. It is the normal way a variable-length
-- response arrives, and an engine that reports it as an under-run produces
-- one false error per short read -- which on a bulk pipe is most of them.
--
-- THE COMPLETION CODE SAYS WHICH OF THE TWO HAPPENED
--
-- Both endings are successes and they mean different things to the layer
-- above, so they are reported separately: D_LENGTH means the full count
-- moved, D_SHORT means the peer ended it early and there is a residual.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package usb_dma_pkg is
  constant DIR_TX : std_logic := '0';   -- the device sends
  constant DIR_RX : std_logic := '1';   -- the device receives

  constant D_NONE   : std_logic_vector(1 downto 0) := "00";
  constant D_LENGTH : std_logic_vector(1 downto 0) := "01";
  constant D_SHORT  : std_logic_vector(1 downto 0) := "10";

  constant E_NONE    : std_logic_vector(1 downto 0) := "00";
  constant E_RESTART : std_logic_vector(1 downto 0) := "01";
  constant E_ACKIDLE : std_logic_vector(1 downto 0) := "10";
  constant E_BABBLE  : 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_dma_pkg.all;

entity usb_dma_xfer is
  generic (
    MAXP : integer := 64          -- endpoint maximum packet size
  );
  port (
    clk     : in std_logic;
    rst_n   : in std_logic;

    start   : in std_logic;       -- a descriptor is being handed over
    len     : in unsigned(15 downto 0);
    dir     : in std_logic;       -- DIR_TX or DIR_RX

    pkt_ack : in std_logic;       -- the packet on the wire completed
    rx_len  : in unsigned(7 downto 0);
    eot     : in std_logic;

    busy       : out std_logic;
    pkt_valid  : out std_logic;
    pkt_len    : out unsigned(7 downto 0);   -- 0 means a ZLP
    pkt_is_zlp : out std_logic;
    pkt_index  : out unsigned(15 downto 0);
    pkt_total  : out unsigned(15 downto 0);  -- n_full + 1
    moved      : out unsigned(15 downto 0);
    residual   : out unsigned(15 downto 0);
    done_pulse : out std_logic;
    done_code  : out std_logic_vector(1 downto 0);
    err_pulse  : out std_logic;
    err_code   : out std_logic_vector(1 downto 0);

    n_start      : out unsigned(31 downto 0);
    n_pkt        : out unsigned(31 downto 0);
    n_zlp        : out unsigned(31 downto 0);
    n_done_len   : out unsigned(31 downto 0);
    n_done_short : out unsigned(31 downto 0);
    n_restart    : out unsigned(31 downto 0);
    n_ack_idle   : out unsigned(31 downto 0);
    n_babble     : out unsigned(31 downto 0)
  );
end entity;

architecture rtl of usb_dma_xfer is
  signal len_r, idx_r, tot_r, moved_r : unsigned(15 downto 0)
    := (others => '0');
  signal busy_r, dir_r, dn_r, er_r : std_logic := '0';
  signal dc_r : std_logic_vector(1 downto 0) := D_NONE;
  signal ec_r : std_logic_vector(1 downto 0) := E_NONE;

  signal st_c, pk_c, zl_c, dl_c : unsigned(31 downto 0) := (others => '0');
  signal ds_c, rs_c, ai_c, bb_c : unsigned(31 downto 0) := (others => '0');

  -- ---- The sequence, computed once when the descriptor arrives. ----
  --
  -- n_full + 1, for every length including zero. Writing it as ceil(L/M) is
  -- correct for every length that is NOT an exact multiple of the packet
  -- size, which is why it survives testing.
  function total_pkts (l : unsigned(15 downto 0)) return unsigned is
  begin
    return (l / to_unsigned(MAXP, 16)) + 1;
  end function;

  -- The length of packet `i` of a transfer of `l` bytes: full until the
  -- last, and the last is the remainder -- which is zero exactly when the
  -- ZLP is required.
  function pkt_len_of (l, i : unsigned(15 downto 0)) return unsigned is
    variable rem16 : unsigned(15 downto 0);
  begin
    if i < (l / to_unsigned(MAXP, 16)) then
      return to_unsigned(MAXP, 8);
    else
      rem16 := l mod to_unsigned(MAXP, 16);
      return rem16(7 downto 0);
    end if;
  end function;
begin
  busy       <= busy_r;
  pkt_valid  <= busy_r;
  pkt_len    <= pkt_len_of(len_r, idx_r);
  pkt_is_zlp <= '1' when (busy_r = '1') and (pkt_len_of(len_r, idx_r) = 0)
                else '0';
  pkt_index  <= idx_r;
  pkt_total  <= tot_r;
  moved      <= moved_r;
  -- A residual is what was asked for and did not arrive. It is meaningful
  -- only once the transfer has ended, and it is not an error.
  residual   <= len_r - moved_r;
  done_pulse <= dn_r;
  done_code  <= dc_r;
  err_pulse  <= er_r;
  err_code   <= ec_r;

  n_start      <= st_c;
  n_pkt        <= pk_c;
  n_zlp        <= zl_c;
  n_done_len   <= dl_c;
  n_done_short <= ds_c;
  n_restart    <= rs_c;
  n_ack_idle   <= ai_c;
  n_babble     <= bb_c;

  process (clk, rst_n)
    variable len_v, idx_v, tot_v, moved_v : unsigned(15 downto 0);
    variable busy_v, dir_v, dn_v, er_v    : std_logic;
    variable dc_v, ec_v                   : std_logic_vector(1 downto 0);
    variable this_len : unsigned(7 downto 0);
    variable take     : unsigned(15 downto 0);
  begin
    if rst_n = '0' then
      len_r   <= (others => '0');
      idx_r   <= (others => '0');
      tot_r   <= (others => '0');
      moved_r <= (others => '0');
      busy_r  <= '0';
      dir_r   <= DIR_TX;
      dn_r    <= '0'; er_r <= '0';
      dc_r    <= D_NONE; ec_r <= E_NONE;
      st_c <= (others => '0'); pk_c <= (others => '0');
      zl_c <= (others => '0'); dl_c <= (others => '0');
      ds_c <= (others => '0'); rs_c <= (others => '0');
      ai_c <= (others => '0'); bb_c <= (others => '0');
    elsif rising_edge(clk) then
      len_v   := len_r;
      idx_v   := idx_r;
      tot_v   := tot_r;
      moved_v := moved_r;
      busy_v  := busy_r;
      dir_v   := dir_r;
      dn_v := '0'; er_v := '0';
      dc_v := D_NONE; ec_v := E_NONE;
      this_len := pkt_len_of(len_r, idx_r);

      if eot = '1' then
        -- Nothing: the counters are the report.
        null;
      elsif start = '1' then
        if busy_r = '1' then
          -- A second descriptor while one is still running. The hardware can
          -- only track one, so the new one would silently replace the old
          -- and the driver would wait for a completion that never comes.
          er_v := '1'; ec_v := E_RESTART;
          rs_c <= rs_c + 1;
        else
          len_v   := len;
          idx_v   := (others => '0');
          tot_v   := total_pkts(len);
          moved_v := (others => '0');
          busy_v  := '1';
          dir_v   := dir;
          st_c    <= st_c + 1;
        end if;
      elsif pkt_ack = '1' then
        if busy_r = '0' then
          -- A packet completed with nothing offered. On real hardware this
          -- is a controller and a driver that disagree about whose turn it
          -- is.
          er_v := '1'; ec_v := E_ACKIDLE;
          ai_c <= ai_c + 1;
        else
          pk_c <= pk_c + 1;
          if this_len = 0 then zl_c <= zl_c + 1; end if;

          -- ---- How many bytes actually moved. ----
          --
          -- On TX it is what we offered. On RX it is what arrived, which is
          -- the peer's choice and may be less.
          if dir_r = DIR_RX then
            take := resize(rx_len, 16);
          else
            take := resize(this_len, 16);
          end if;

          if dir_r = DIR_RX and rx_len > this_len then
            -- The peer sent more than the packet could hold. This is babble,
            -- and it is a real error rather than a short read: the extra
            -- bytes went somewhere.
            er_v := '1'; ec_v := E_BABBLE;
            bb_c <= bb_c + 1;
            take := resize(this_len, 16);
          end if;

          moved_v := moved_v + take;

          if dir_r = DIR_RX and take < resize(this_len, 16) then
            -- ---- A SHORT packet on receive ends the transfer. ----
            --
            -- Not an error. The peer had less to send than we asked for,
            -- which is how every variable-length response arrives.
            busy_v := '0';
            dn_v   := '1'; dc_v := D_SHORT;
            ds_c   <= ds_c + 1;
          elsif (idx_v + 1) >= tot_v then
            -- ---- The sequence is complete. ----
            --
            -- Including the case where the last packet was a ZLP, which is a
            -- packet like any other and has to be acknowledged before the
            -- transfer is done.
            busy_v := '0';
            dn_v   := '1'; dc_v := D_LENGTH;
            dl_c   <= dl_c + 1;
          else
            idx_v := idx_v + 1;
          end if;
        end if;
      end if;

      len_r   <= len_v;
      idx_r   <= idx_v;
      tot_r   <= tot_v;
      moved_r <= moved_v;
      busy_r  <= busy_v;
      dir_r   <= dir_v;
      dn_r    <= dn_v; er_r <= er_v;
      dc_r    <= dc_v; ec_r <= ec_v;
    end if;
  end process;
end architecture;

9. Seeing the Terminator Appear

64 bytes over a 64-byte endpoint: two packets, and the second is empty

usb_dma_xfer — the zero-length terminator

10 cycles
A ten-cycle waveform. A descriptor of 64 bytes is started on a 64-byte endpoint; the packet total reads 2. The first packet offered is 64 bytes and is acknowledged, moving 64 bytes. The second packet offered has length zero and the zero-length flag is high; acknowledging it completes the transfer with the code LENGTH and a residual of zero.total = 2 for a 64-byte transfertotal = 2 for a 64-bytetransferthe second packet is a ZLPthe second packet is a ZLPdone only after the ZLP is ackeddone only after the ZLP isackedclkstartlen64646464646464646464pkt_total0222222222pkt_len064640000000pkt_is_zlppkt_ackmoved00064646464646464done_code00000LENGTHLENGTHLENGTHLENGTHLENGTHt0t1t2t3t4t5t6t7t8t9
The total is computed when the descriptor arrives and reads 2, not 1. The first packet is full, which does not end the transfer; the second carries no bytes at all and does. The transfer is not complete until the zero-length packet has been acknowledged like any other.

And the other ending, with its residual:

A receive that the peer ends early

usb_dma_xfer — a short packet and a residual

10 cycles
A ten-cycle waveform. A 200-byte receive descriptor is started and the packet total reads four. The first packet arrives with 64 bytes and the moved count reaches 64. The second arrives with only 20 bytes, which is shorter than the 64-byte maximum; the moved count reaches 84, the transfer completes with the code SHORT, the residual reads 116, and the error pulse stays low throughout.one full packet: 64 of 200 movedone full packet: 64 of 200moved20 bytes: short, so the transfer ends20 bytes: short, so thetransfer endsresidual 116, and NO errorresidual 116, and NO errorclkstartlen200200200200200200200200200200pkt_total0444444444pkt_ackrx_len0646420202020202020moved006464848484848484residual0200136136116116116116116116done_code0000SHORTSHORTSHORTSHORTSHORTSHORTerr_pulset0t1t2t3t4t5t6t7t8t9
A 200-byte descriptor expects four packets. The peer sends 64, then 20 — which is shorter than the maximum, so the transfer is over. 84 bytes moved and 116 did not, and the completion says SHORT rather than raising an error, because nothing went wrong.

10. The Testbenches

The oracle is a shadow model written from sections 1 to 4 rather than from the RTL, re-derived every cycle and compared against every output — twenty-one checks per cycle.

The exhaustive claim is over every descriptor length, because the interesting values of that one number are the ones where the arithmetic changes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   every L in 0 .. 1024, in BOTH directions
       -> 2050 complete transfers, every packet of every one
          compared against an expectation computed from the rule

   and for every L in 0 .. 256, a short packet injected at
   EVERY position in the sequence
       -> 640 residual cases, because the arithmetic depends on
          where the peer stopped

Verilog-2005 testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
// Testbench for usb_dma_xfer.
//
// The oracle is a shadow model written from the chapter's rules rather than
// from the RTL, re-derived every cycle and compared against every output.
//
// THE EXHAUSTIVE CLAIM IS OVER EVERY DESCRIPTOR LENGTH
//
// The packet sequence is a function of one number, and the interesting
// values of that number are the ones where the arithmetic changes: the exact
// multiples of the maximum packet size, where the zero-length terminator
// becomes necessary. Testing "a short one, a long one and one in the middle"
// is exactly the test that misses them.
//
//     every L in 0 .. 1024, in both directions
//     -> 2050 complete transfers, every packet of every one checked
//
// and separately, for every L in 0 .. 256, a short packet injected at EVERY
// position in the sequence, because the residual arithmetic depends on where
// the peer stopped.
module tb_dx_v;

  localparam integer MAXP  = 64;
  localparam integer L_MAX = 1024;
  localparam integer S_MAX = 256;

  localparam [1:0] D_NONE = 2'd0, D_LENGTH = 2'd1, D_SHORT = 2'd2;
  localparam [1:0] E_NONE = 2'd0, E_RESTART = 2'd1, E_ACKIDLE = 2'd2,
                   E_BABBLE = 2'd3;

  reg         clk = 1'b0, rst_n = 1'b0;
  reg         start = 1'b0, dir = 1'b0, pkt_ack = 1'b0, eot = 1'b0;
  reg  [15:0] len = 16'd0;
  reg  [7:0]  rx_len = 8'd0;

  wire        busy, pkt_valid, pkt_is_zlp, done_pulse, err_pulse;
  wire [7:0]  pkt_len;
  wire [15:0] pkt_index, pkt_total, moved, residual;
  wire [1:0]  done_code, err_code;
  wire [31:0] n_start, n_pkt, n_zlp, n_done_len, n_done_short,
              n_restart, n_ack_idle, n_babble;

  usb_dma_xfer #(.MAXP(MAXP)) dut (
    .clk(clk), .rst_n(rst_n),
    .start(start), .len(len), .dir(dir),
    .pkt_ack(pkt_ack), .rx_len(rx_len), .eot(eot),
    .busy(busy), .pkt_valid(pkt_valid), .pkt_len(pkt_len),
    .pkt_is_zlp(pkt_is_zlp), .pkt_index(pkt_index), .pkt_total(pkt_total),
    .moved(moved), .residual(residual),
    .done_pulse(done_pulse), .done_code(done_code),
    .err_pulse(err_pulse), .err_code(err_code),
    .n_start(n_start), .n_pkt(n_pkt), .n_zlp(n_zlp),
    .n_done_len(n_done_len), .n_done_short(n_done_short),
    .n_restart(n_restart), .n_ack_idle(n_ack_idle), .n_babble(n_babble)
  );

  always #5 clk = ~clk;

  // ---------------- the shadow model ----------------
  reg [15:0] m_len, m_idx, m_tot, m_moved;
  reg        m_busy, m_dir, m_dn, m_er;
  reg [1:0]  m_dc, m_ec;
  reg [31:0] c_start, c_pkt, c_zlp, c_dlen, c_dshort, c_rs, c_ai, c_bb;

  integer errors = 0, checks = 0, steps = 0;
  integer k;

  // The independent expectation, written from the chapter rather than from
  // the design: n_full + 1 packets, for every length including zero.
  function [15:0] exp_total; input [15:0] l;
    begin exp_total = (l / MAXP) + 16'd1; end
  endfunction

  function [7:0] exp_plen; input [15:0] l; input [15:0] i;
    begin
      if (i < (l / MAXP)) exp_plen = MAXP;
      else                exp_plen = (l % MAXP);
    end
  endfunction

  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 model_step;
    reg [7:0]  this_len;
    reg [15:0] take;
    begin
      m_dn = 1'b0; m_er = 1'b0; m_dc = D_NONE; m_ec = E_NONE;
      this_len = exp_plen(m_len, m_idx);

      if (eot) begin
        // nothing
      end else if (start) begin
        if (m_busy) begin
          m_er = 1'b1; m_ec = E_RESTART; c_rs = c_rs + 1;
        end else begin
          m_len = len; m_idx = 16'd0; m_tot = exp_total(len);
          m_moved = 16'd0; m_busy = 1'b1; m_dir = dir;
          c_start = c_start + 1;
        end
      end else if (pkt_ack) begin
        if (!m_busy) begin
          m_er = 1'b1; m_ec = E_ACKIDLE; c_ai = c_ai + 1;
        end else begin
          c_pkt = c_pkt + 1;
          if (this_len == 8'd0) c_zlp = c_zlp + 1;
          take = m_dir ? {8'd0, rx_len} : {8'd0, this_len};
          if (m_dir && ({8'd0, rx_len} > {8'd0, this_len})) begin
            m_er = 1'b1; m_ec = E_BABBLE; c_bb = c_bb + 1;
            take = {8'd0, this_len};
          end
          m_moved = m_moved + take;
          if (m_dir && (take < {8'd0, this_len})) begin
            // A SHORT packet on receive ends the transfer. Not an error.
            m_busy = 1'b0; m_dn = 1'b1; m_dc = D_SHORT;
            c_dshort = c_dshort + 1;
          end else if (m_idx + 16'd1 >= m_tot) begin
            m_busy = 1'b0; m_dn = 1'b1; m_dc = D_LENGTH;
            c_dlen = c_dlen + 1;
          end else begin
            m_idx = m_idx + 16'd1;
          end
        end
      end
    end
  endtask

  task check_out;
    begin
      ck("busy",       {31'd0, busy},      {31'd0, m_busy});
      ck("pkt_valid",  {31'd0, pkt_valid}, {31'd0, m_busy});
      ck("pkt_len",    {24'd0, pkt_len},   {24'd0, exp_plen(m_len, m_idx)});
      ck("pkt_is_zlp", {31'd0, pkt_is_zlp},
         {31'd0, m_busy && (exp_plen(m_len, m_idx) == 8'd0)});
      ck("pkt_index",  {16'd0, pkt_index}, {16'd0, m_idx});
      ck("pkt_total",  {16'd0, pkt_total}, {16'd0, m_tot});
      ck("moved",      {16'd0, moved},     {16'd0, m_moved});
      ck("residual",   {16'd0, residual},  {16'd0, m_len - m_moved});
      ck("done_pulse", {31'd0, done_pulse},{31'd0, m_dn});
      ck("done_code",  {30'd0, done_code}, {30'd0, m_dc});
      ck("err_pulse",  {31'd0, err_pulse}, {31'd0, m_er});
      ck("err_code",   {30'd0, err_code},  {30'd0, m_ec});
      ck("n_start",      n_start,      c_start);
      ck("n_pkt",        n_pkt,        c_pkt);
      ck("n_zlp",        n_zlp,        c_zlp);
      ck("n_done_len",   n_done_len,   c_dlen);
      ck("n_done_short", n_done_short, c_dshort);
      ck("n_restart",    n_restart,    c_rs);
      ck("n_ack_idle",   n_ack_idle,   c_ai);
      ck("n_babble",     n_babble,     c_bb);
      // ---- the structural invariant ----
      //
      // Every transfer ends exactly once, by length or by a short packet.
      // If these ever drift apart, a completion has been produced with no
      // transfer behind it -- which is a driver waiting on an event that
      // already fired, or worse, one that fired twice.
      ck("completions", n_done_len + n_done_short, c_dlen + c_dshort);
    end
  endtask

  task step;
    begin
      model_step;
      @(posedge clk);
      #1;
      steps = steps + 1;
      check_out;
    end
  endtask

  task do_start; input [15:0] l; input d;
    begin
      start = 1'b1; pkt_ack = 1'b0; eot = 1'b0; len = l; dir = d;
      step;
      start = 1'b0;
    end
  endtask

  task do_ack; input [7:0] rl;
    begin
      start = 1'b0; pkt_ack = 1'b1; eot = 1'b0; rx_len = rl;
      step;
      pkt_ack = 1'b0;
    end
  endtask

  task idle;
    begin
      start = 1'b0; pkt_ack = 1'b0; eot = 1'b0;
      step;
    end
  endtask

  task reset_all;
    begin
      start = 1'b0; pkt_ack = 1'b0; eot = 1'b0;
      rst_n = 1'b0;
      @(posedge clk); #1;
      rst_n = 1'b1;
      m_len = 16'd0; m_idx = 16'd0; m_tot = 16'd0; m_moved = 16'd0;
      m_busy = 1'b0; m_dir = 1'b0; m_dn = 1'b0; m_er = 1'b0;
      m_dc = D_NONE; m_ec = E_NONE;
      c_start=0; c_pkt=0; c_zlp=0; c_dlen=0; c_dshort=0;
      c_rs=0; c_ai=0; c_bb=0;
      @(negedge clk);
    end
  endtask

  integer L, d, i, sp, w;
  integer n_zlp_cases, n_short_cases, base_zlp, base_dlen, base_dshort;
  integer n_bab_cases, n_rst_cases;
  integer seen_total, seen_pkts;

  initial begin
    n_zlp_cases = 0; n_short_cases = 0;
    n_bab_cases = 0; n_rst_cases = 0;

    repeat (3) @(posedge clk);
    rst_n = 1'b1;
    @(negedge clk);
    reset_all;

    // ================= PHASE 1 -- EVERY length, both directions ==========
    //
    // The whole packet sequence is driven and every packet's length, index
    // and total is compared against an expectation computed from the
    // chapter's rule rather than from the design.
    for (d = 0; d < 2; d = d + 1)
      for (L = 0; L <= L_MAX; L = L + 1) begin
        do_start(L[15:0], d[0]);
        if (pkt_total !== exp_total(L[15:0])) begin
          errors = errors + 1;
          if (errors < 25)
            $display("FAIL L=%0d total %0d expected %0d", L, pkt_total,
                     exp_total(L[15:0]));
        end
        base_dlen = c_dlen;
        seen_pkts = 0;
        // acknowledge every packet the engine offers, at its full length so
        // nothing terminates early
        // 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. A correct sequence is n_full + 1 packets, so twice
        // that plus a margin is a bound no correct design can reach.
        while (m_busy && (seen_pkts < 2 * exp_total(L[15:0]) + 8)) begin
          if (exp_plen(m_len, m_idx) == 8'd0) n_zlp_cases = n_zlp_cases + 1;
          seen_pkts = seen_pkts + 1;
          do_ack(exp_plen(m_len, m_idx));
        end
        if (m_busy) begin
          errors = errors + 1;
          if (errors < 25)
            $display("FAIL L=%0d never completed after %0d packets", L, seen_pkts);
          reset_all;
        end
        if (seen_pkts != exp_total(L[15:0])) begin
          errors = errors + 1;
          if (errors < 25)
            $display("FAIL L=%0d emitted %0d packets, expected %0d", L,
                     seen_pkts, exp_total(L[15:0]));
        end
        if (c_dlen != base_dlen + 1) begin
          errors = errors + 1;
          $display("FAIL L=%0d did not complete by length", L);
        end
        if (m_moved != L[15:0]) begin
          errors = errors + 1;
          $display("FAIL L=%0d moved %0d bytes", L, m_moved);
        end
      end
    // Every exact multiple of MAXP needs a ZLP, and there are L_MAX/MAXP + 1
    // of them in each direction. If that count is wrong the sweep did not
    // reach the cases it exists for.
    if (n_zlp_cases != 2 * ((L_MAX / MAXP) + 1)) begin
      errors = errors + 1;
      $display("FAIL saw %0d zero-length packets, expected %0d",
               n_zlp_cases, 2 * ((L_MAX / MAXP) + 1));
    end

    // ================= PHASE 2 -- the ZLP boundary, named ================
    //
    // The three lengths either side of one multiple, spelled out, because
    // this is the case the whole chapter is about.
    reset_all;
    do_start(16'd63, 1'b0);
    if (pkt_total !== 16'd1 || pkt_len !== 8'd63) begin
      errors = errors + 1;
      $display("FAIL L=63: total=%0d len=%0d, expected 1 and 63",
               pkt_total, pkt_len);
    end
    do_ack(8'd63);
    do_start(16'd64, 1'b0);
    if (pkt_total !== 16'd2) begin
      errors = errors + 1;
      $display("FAIL L=64: total=%0d, expected 2 (a full packet and a ZLP)",
               pkt_total);
    end
    do_ack(8'd64);
    if (!pkt_is_zlp || (pkt_len !== 8'd0)) begin
      errors = errors + 1;
      $display("FAIL L=64: the second packet is not a ZLP (len=%0d)", pkt_len);
    end
    do_ack(8'd0);
    do_start(16'd65, 1'b0);
    if (pkt_total !== 16'd2) begin
      errors = errors + 1;
      $display("FAIL L=65: total=%0d, expected 2", pkt_total);
    end
    do_ack(8'd64);
    if (pkt_len !== 8'd1) begin
      errors = errors + 1;
      $display("FAIL L=65: the second packet is %0d bytes, expected 1",
               pkt_len);
    end
    do_ack(8'd1);
    // ...and a zero-length transfer is one ZLP, not nothing at all.
    do_start(16'd0, 1'b0);
    if (pkt_total !== 16'd1 || !pkt_is_zlp) begin
      errors = errors + 1;
      $display("FAIL L=0: total=%0d zlp=%0b, expected 1 and a ZLP",
               pkt_total, pkt_is_zlp);
    end
    do_ack(8'd0);

    // ================= PHASE 3 -- a short packet at EVERY position =======
    //
    // On receive, the peer may stop early at any packet. The residual
    // arithmetic depends on where, so every position is driven for every
    // length up to S_MAX.
    for (L = 0; L <= S_MAX; L = L + 1)
      for (sp = 0; sp < exp_total(L[15:0]); sp = sp + 1) begin
        reset_all;
        do_start(L[15:0], 1'b1);
        base_dshort = c_dshort;
        seen_total = 0;
        for (i = 0; i < sp; i = i + 1) begin
          seen_total = seen_total + exp_plen(L[15:0], i[15:0]);
          do_ack(exp_plen(L[15:0], i[15:0]));
        end
        // the short one: one byte fewer than the engine offered, or skip
        // this case when the offered packet is already zero-length
        if (exp_plen(L[15:0], sp[15:0]) != 8'd0) begin
          seen_total = seen_total + exp_plen(L[15:0], sp[15:0]) - 1;
          do_ack(exp_plen(L[15:0], sp[15:0]) - 8'd1);
          n_short_cases = n_short_cases + 1;
          if (c_dshort != base_dshort + 1) begin
            errors = errors + 1;
            $display("FAIL L=%0d short at %0d did not complete SHORT", L, sp);
          end
          if (done_code !== D_SHORT) begin
            errors = errors + 1;
            $display("FAIL L=%0d short at %0d code=%0d", L, sp, done_code);
          end
          // ---- THE arithmetic. ----
          if (residual !== (L[15:0] - seen_total[15:0])) begin
            errors = errors + 1;
            if (errors < 25)
              $display("FAIL L=%0d short at %0d: residual %0d expected %0d",
                       L, sp, residual, L - seen_total);
          end
          if (err_pulse !== 1'b0) begin
            errors = errors + 1;
            $display("FAIL a short packet raised an error: it is not one");
          end
        end
      end

    // ================= PHASE 4 -- babble, at EVERY position ==============
    //
    // A short packet is the peer choosing to send less. More than the packet
    // can hold is the peer sending bytes that went somewhere, and the two
    // must not be reported the same way.
    //
    // The first version of this phase drove one babble case and the mutation
    // that accepts babble silently died on FIVE checks. A property with one
    // instance is a property that is nearly untested, so every packet
    // position of a multi-packet transfer is driven, at four different
    // over-lengths.
    n_bab_cases = 0;
    for (L = 0; L < 4; L = L + 1)            // over-length index
      for (sp = 0; sp < 4; sp = sp + 1) begin
        reset_all;
        do_start(16'd256, 1'b1);
        for (i = 0; i < sp; i = i + 1) do_ack(8'd64);
        base_dshort = c_dshort;
        k = c_bb;
        // 65, 100, 200, 255 -- all longer than the 64-byte packet
        do_ack((L == 0) ? 8'd65 : (L == 1) ? 8'd100 :
               (L == 2) ? 8'd200 : 8'd255);
        if (c_bb != k + 1) begin
          errors = errors + 1;
          $display("FAIL babble at position %0d, len case %0d not reported",
                   sp, L);
        end
        // ...and only MAXP bytes are counted as moved, whatever arrived.
        if (m_moved != (sp + 1) * MAXP) begin
          errors = errors + 1;
          $display("FAIL babble at %0d moved %0d, expected %0d",
                   sp, m_moved, (sp + 1) * MAXP);
        end
        // ...and it is NOT reported as a short read, because it is not one.
        if (c_dshort != base_dshort) begin
          errors = errors + 1;
          $display("FAIL babble at %0d was reported as a short packet", sp);
        end
        n_bab_cases = n_bab_cases + 1;
      end
    if (n_bab_cases != 16) begin
      errors = errors + 1;
      $display("FAIL babble cases %0d, expected 16", n_bab_cases);
    end

    // ================= PHASE 5 -- a restart, at EVERY position ============
    //
    // A second descriptor arriving while one is running. The hardware can
    // only track one, so the new one would silently replace the old and the
    // driver would wait for a completion that never comes.
    //
    // Driven at every position in the sequence and with three different
    // replacement lengths, for the same reason as phase 4: the mutation that
    // accepts the replacement died on 39 checks when this was one case.
    n_rst_cases = 0;
    for (L = 0; L < 3; L = L + 1)
      for (sp = 0; sp < 4; sp = sp + 1) begin
        reset_all;
        do_start(16'd256, 1'b0);
        for (i = 0; i < sp; i = i + 1) do_ack(8'd64);
        k = c_rs;
        do_start((L == 0) ? 16'd64 : (L == 1) ? 16'd512 : 16'd1, 1'b1);
        if (c_rs != k + 1) begin
          errors = errors + 1;
          $display("FAIL restart at position %0d was not reported", sp);
        end
        // THE check: the running descriptor is untouched. Its length, its
        // direction, its position and its total all survive.
        if (m_len != 16'd256) begin
          errors = errors + 1;
          $display("FAIL restart at %0d replaced the length: %0d", sp, m_len);
        end
        if (m_idx != sp[15:0]) begin
          errors = errors + 1;
          $display("FAIL restart at %0d moved the position to %0d", sp, m_idx);
        end
        if (m_tot != 16'd5) begin
          errors = errors + 1;
          $display("FAIL restart at %0d changed the total to %0d", sp, m_tot);
        end
        if (m_dir != 1'b0) begin
          errors = errors + 1;
          $display("FAIL restart at %0d flipped the direction", sp);
        end
        // ...and the original transfer still completes normally.
        w = 0;
        while (m_busy && (w < 64)) begin
          do_ack(exp_plen(m_len, m_idx));
          w = w + 1;
        end
        if (m_busy) begin
          errors = errors + 1;
          if (errors < 25)
            $display("FAIL the transfer after a restart at %0d never finished", sp);
          reset_all;
        end
        if (m_busy == 1'b0 && m_moved != 16'd256) begin
          errors = errors + 1;
          $display("FAIL after a restart at %0d the transfer moved %0d",
                   sp, m_moved);
        end
        n_rst_cases = n_rst_cases + 1;
      end
    if (n_rst_cases != 12) begin
      errors = errors + 1;
      $display("FAIL restart cases %0d, expected 12", n_rst_cases);
    end

    // ...and an acknowledgement with nothing outstanding, which is the other
    // half of the same disagreement between controller and driver.
    reset_all;
    k = c_ai;
    do_ack(8'd64);
    if (c_ai != k + 1) begin
      errors = errors + 1;
      $display("FAIL an idle ack was not reported");
    end

    // The random phase is switchable, because a mutation score is only
    // interesting once it is DECOMPOSED. Phases 1 to 5 already sweep every
    // length and every short-packet position, so nothing in the exhaustive
    // claim depends on it.
`ifndef DIRECTED_ONLY
    // ================= PHASE 6 -- random =================================
    reset_all;
    for (i = 0; i < 20000; i = i + 1) begin
      w = $unsigned($random) % 100;
      if (!m_busy) begin
        if (w < 90) do_start($unsigned($random) % 600, $unsigned($random) % 2);
        else if (w < 95) do_ack($unsigned($random) % 80);
        else idle;
      end else begin
        if (w < 88) begin
          // Mostly full-length acks, with a realistic fraction of short ones
          // and a small fraction of babble. Leaving babble out of the mix
          // means the one error that is NOT a short read is exercised by a
          // single directed case, and the check that separates them is under
          // no load at all.
          sp = $unsigned($random) % 100;
          if (sp < 12)
            do_ack((exp_plen(m_len, m_idx) == 8'd0) ? 8'd0
                   : (exp_plen(m_len, m_idx) - 8'd1));
          else if (sp < 15 && m_dir)
            do_ack(8'd200);                 // more than any packet can hold
          else
            do_ack(exp_plen(m_len, m_idx));
        end else if (w < 94) begin
          do_start($unsigned($random) % 600, $unsigned($random) % 2);
        end else idle;
      end
    end

`endif

    $display("steps=%0d checks=%0d errors=%0d", steps, checks, errors);
    $display("lengths swept=%0d (x2 directions)  ZLP cases=%0d  short cases=%0d",
             L_MAX + 1, n_zlp_cases, n_short_cases);
    $display("start=%0d pkt=%0d zlp=%0d done_len=%0d done_short=%0d",
             n_start, n_pkt, n_zlp, n_done_len, n_done_short);
    $display("restart=%0d ack_idle=%0d babble=%0d",
             n_restart, n_ack_idle, n_babble);
    $display("%0s: %0d errors in %0d checks",
             (errors == 0) ? "PASS" : "FAIL", errors, checks);
    $finish;
  end
endmodule

SystemVerilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
// Testbench for usb_dma_xfer.
//
// The oracle is a shadow model written from the chapter's rules rather than
// from the RTL, re-derived every cycle and compared against every output.
//
// THE EXHAUSTIVE CLAIM IS OVER EVERY DESCRIPTOR LENGTH
//
// The packet sequence is a function of one number, and the interesting
// values of that number are the ones where the arithmetic changes: the exact
// multiples of the maximum packet size, where the zero-length terminator
// becomes necessary. Testing "a short one, a long one and one in the middle"
// is exactly the test that misses them.
//
//     every L in 0 .. 1024, in both directions
//     -> 2050 complete transfers, every packet of every one checked
//
// and separately, for every L in 0 .. 256, a short packet injected at EVERY
// position in the sequence, because the residual arithmetic depends on where
// the peer stopped.
module tb_dx_sv;
  import usb_dma_pkg::*;

  localparam int MAXP  = 64;
  localparam int L_MAX = 1024;
  localparam int S_MAX = 256;

  logic        clk = 1'b0, rst_n = 1'b0;
  logic        start = 1'b0, pkt_ack = 1'b0, eot = 1'b0;
  xfer_dir_e   dir = DIR_TX;
  logic [15:0] len = 16'd0;
  logic [7:0]  rx_len = 8'd0;

  logic        busy, pkt_valid, pkt_is_zlp, done_pulse, err_pulse;
  logic [7:0]  pkt_len;
  logic [15:0] pkt_index, pkt_total, moved, residual;
  done_code_e  done_code;
  dma_err_e    err_code;
  logic [31:0] n_start, n_pkt, n_zlp, n_done_len, n_done_short,
               n_restart, n_ack_idle, n_babble;

  usb_dma_xfer #(.MAXP(MAXP)) dut (
    .clk(clk), .rst_n(rst_n),
    .start(start), .len(len), .dir(dir),
    .pkt_ack(pkt_ack), .rx_len(rx_len), .eot(eot),
    .busy(busy), .pkt_valid(pkt_valid), .pkt_len(pkt_len),
    .pkt_is_zlp(pkt_is_zlp), .pkt_index(pkt_index), .pkt_total(pkt_total),
    .moved(moved), .residual(residual),
    .done_pulse(done_pulse), .done_code(done_code),
    .err_pulse(err_pulse), .err_code(err_code),
    .n_start(n_start), .n_pkt(n_pkt), .n_zlp(n_zlp),
    .n_done_len(n_done_len), .n_done_short(n_done_short),
    .n_restart(n_restart), .n_ack_idle(n_ack_idle), .n_babble(n_babble)
  );

  always #5 clk = ~clk;

  // ---------------- the shadow model ----------------
  logic [15:0] m_len, m_idx, m_tot, m_moved;
  logic        m_busy, m_dn, m_er;
  xfer_dir_e   m_dir;
  done_code_e  m_dc;
  dma_err_e    m_ec;
  int unsigned c_start, c_pkt, c_zlp, c_dlen, c_dshort, c_rs, c_ai, c_bb;

  int errors = 0, checks = 0, steps = 0;
  int k;

  // The independent expectation, written from the chapter rather than from
  // the design: n_full + 1 packets, for every length including zero.
  function automatic logic [15:0] exp_total(input logic [15:0] l);
    begin exp_total = (l / 16'(MAXP)) + 16'd1; end
  endfunction

  function automatic logic [7:0] exp_plen(input logic [15:0] l,
                                          input logic [15:0] i);
    begin
      if (i < (l / 16'(MAXP))) exp_plen = 8'(MAXP);
      else                     exp_plen = 8'(l % 16'(MAXP));
    end
  endfunction

  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 model_step;
    logic [7:0]  this_len;
    logic [15:0] take;
    begin
      m_dn = 1'b0; m_er = 1'b0; m_dc = D_NONE; m_ec = E_NONE;
      this_len = exp_plen(m_len, m_idx);

      if (eot) begin
        // nothing
      end else if (start) begin
        if (m_busy) begin
          m_er = 1'b1; m_ec = E_RESTART; c_rs = c_rs + 1;
        end else begin
          m_len = len; m_idx = 16'd0; m_tot = exp_total(len);
          m_moved = 16'd0; m_busy = 1'b1; m_dir = dir;
          c_start = c_start + 1;
        end
      end else if (pkt_ack) begin
        if (!m_busy) begin
          m_er = 1'b1; m_ec = E_ACKIDLE; c_ai = c_ai + 1;
        end else begin
          c_pkt = c_pkt + 1;
          if (this_len == 8'd0) c_zlp = c_zlp + 1;
          take = (m_dir == DIR_RX) ? {8'd0, rx_len} : {8'd0, this_len};
          if ((m_dir == DIR_RX) && ({8'd0, rx_len} > {8'd0, this_len})) begin
            m_er = 1'b1; m_ec = E_BABBLE; c_bb = c_bb + 1;
            take = {8'd0, this_len};
          end
          m_moved = m_moved + take;
          if ((m_dir == DIR_RX) && (take < {8'd0, this_len})) begin
            // A SHORT packet on receive ends the transfer. Not an error.
            m_busy = 1'b0; m_dn = 1'b1; m_dc = D_SHORT;
            c_dshort = c_dshort + 1;
          end else if (m_idx + 16'd1 >= m_tot) begin
            m_busy = 1'b0; m_dn = 1'b1; m_dc = D_LENGTH;
            c_dlen = c_dlen + 1;
          end else begin
            m_idx = m_idx + 16'd1;
          end
        end
      end
    end
  endtask

  task automatic check_out;
    begin
      ck("busy",       {31'd0, busy},      {31'd0, m_busy});
      ck("pkt_valid",  {31'd0, pkt_valid}, {31'd0, m_busy});
      ck("pkt_len",    {24'd0, pkt_len},   {24'd0, exp_plen(m_len, m_idx)});
      ck("pkt_is_zlp", {31'd0, pkt_is_zlp},
         {31'd0, m_busy && (exp_plen(m_len, m_idx) == 8'd0)});
      ck("pkt_index",  {16'd0, pkt_index}, {16'd0, m_idx});
      ck("pkt_total",  {16'd0, pkt_total}, {16'd0, m_tot});
      ck("moved",      {16'd0, moved},     {16'd0, m_moved});
      ck("residual",   {16'd0, residual},  {16'd0, m_len - m_moved});
      ck("done_pulse", {31'd0, done_pulse},{31'd0, m_dn});
      ck("done_code",  done_code, m_dc);
      ck("err_pulse",  {31'd0, err_pulse}, {31'd0, m_er});
      ck("err_code",   err_code, m_ec);
      ck("n_start",      n_start,      c_start);
      ck("n_pkt",        n_pkt,        c_pkt);
      ck("n_zlp",        n_zlp,        c_zlp);
      ck("n_done_len",   n_done_len,   c_dlen);
      ck("n_done_short", n_done_short, c_dshort);
      ck("n_restart",    n_restart,    c_rs);
      ck("n_ack_idle",   n_ack_idle,   c_ai);
      ck("n_babble",     n_babble,     c_bb);
      // ---- the structural invariant ----
      //
      // Every transfer ends exactly once, by length or by a short packet.
      // If these ever drift apart, a completion has been produced with no
      // transfer behind it -- which is a driver waiting on an event that
      // already fired, or worse, one that fired twice.
      ck("completions", n_done_len + n_done_short, c_dlen + c_dshort);
    end
  endtask

  task automatic step;
    begin
      model_step;
      @(posedge clk);
      #1;
      steps = steps + 1;
      check_out;
    end
  endtask

  task automatic do_start(logic [15:0] l, xfer_dir_e d);
    begin
      start = 1'b1; pkt_ack = 1'b0; eot = 1'b0; len = l; dir = d;
      step;
      start = 1'b0;
    end
  endtask

  task automatic do_ack(logic [7:0] rl);
    begin
      start = 1'b0; pkt_ack = 1'b1; eot = 1'b0; rx_len = rl;
      step;
      pkt_ack = 1'b0;
    end
  endtask

  task automatic idle;
    begin
      start = 1'b0; pkt_ack = 1'b0; eot = 1'b0;
      step;
    end
  endtask

  task automatic reset_all;
    begin
      start = 1'b0; pkt_ack = 1'b0; eot = 1'b0;
      rst_n = 1'b0;
      @(posedge clk); #1;
      rst_n = 1'b1;
      m_len = 16'd0; m_idx = 16'd0; m_tot = 16'd0; m_moved = 16'd0;
      m_busy = 1'b0; m_dir = DIR_TX; m_dn = 1'b0; m_er = 1'b0;
      m_dc = D_NONE; m_ec = E_NONE;
      c_start=0; c_pkt=0; c_zlp=0; c_dlen=0; c_dshort=0;
      c_rs=0; c_ai=0; c_bb=0;
      @(negedge clk);
    end
  endtask

  integer L, d, i, sp, w;
  integer n_zlp_cases, n_short_cases, base_zlp, base_dlen, base_dshort;
  integer n_bab_cases, n_rst_cases;
  integer seen_total, seen_pkts;

  initial begin
    n_zlp_cases = 0; n_short_cases = 0;
    n_bab_cases = 0; n_rst_cases = 0;

    repeat (3) @(posedge clk);
    rst_n = 1'b1;
    @(negedge clk);
    reset_all;

    // ================= PHASE 1 -- EVERY length, both directions ==========
    //
    // The whole packet sequence is driven and every packet's length, index
    // and total is compared against an expectation computed from the
    // chapter's rule rather than from the design.
    for (d = 0; d < 2; d = d + 1)
      for (L = 0; L <= L_MAX; L = L + 1) begin
        do_start(16'(L), xfer_dir_e'(d[0]));
        if (pkt_total !== exp_total(16'(L))) begin
          errors = errors + 1;
          if (errors < 25)
            $display("FAIL L=%0d total %0d expected %0d", L, pkt_total,
                     exp_total(16'(L)));
        end
        base_dlen = c_dlen;
        seen_pkts = 0;
        // acknowledge every packet the engine offers, at its full length so
        // nothing terminates early
        // 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. A correct sequence is n_full + 1 packets, so twice
        // that plus a margin is a bound no correct design can reach.
        while (m_busy && (seen_pkts < 2 * int'(exp_total(16'(L))) + 8)) begin
          if (exp_plen(m_len, m_idx) == 8'd0) n_zlp_cases = n_zlp_cases + 1;
          seen_pkts = seen_pkts + 1;
          do_ack(exp_plen(m_len, m_idx));
        end
        if (m_busy) begin
          errors = errors + 1;
          if (errors < 25)
            $display("FAIL L=%0d never completed after %0d packets", L, seen_pkts);
          reset_all;
        end
        if (seen_pkts != exp_total(16'(L))) begin
          errors = errors + 1;
          if (errors < 25)
            $display("FAIL L=%0d emitted %0d packets, expected %0d", L,
                     seen_pkts, exp_total(16'(L)));
        end
        if (c_dlen != base_dlen + 1) begin
          errors = errors + 1;
          $display("FAIL L=%0d did not complete by length", L);
        end
        if (m_moved != 16'(L)) begin
          errors = errors + 1;
          $display("FAIL L=%0d moved %0d bytes", L, m_moved);
        end
      end
    // Every exact multiple of MAXP needs a ZLP, and there are L_MAX/MAXP + 1
    // of them in each direction. If that count is wrong the sweep did not
    // reach the cases it exists for.
    if (n_zlp_cases != 2 * ((L_MAX / MAXP) + 1)) begin
      errors = errors + 1;
      $display("FAIL saw %0d zero-length packets, expected %0d",
               n_zlp_cases, 2 * ((L_MAX / MAXP) + 1));
    end

    // ================= PHASE 2 -- the ZLP boundary, named ================
    //
    // The three lengths either side of one multiple, spelled out, because
    // this is the case the whole chapter is about.
    reset_all;
    do_start(16'd63, DIR_TX);
    if (pkt_total !== 16'd1 || pkt_len !== 8'd63) begin
      errors = errors + 1;
      $display("FAIL L=63: total=%0d len=%0d, expected 1 and 63",
               pkt_total, pkt_len);
    end
    do_ack(8'd63);
    do_start(16'd64, DIR_TX);
    if (pkt_total !== 16'd2) begin
      errors = errors + 1;
      $display("FAIL L=64: total=%0d, expected 2 (a full packet and a ZLP)",
               pkt_total);
    end
    do_ack(8'd64);
    if (!pkt_is_zlp || (pkt_len !== 8'd0)) begin
      errors = errors + 1;
      $display("FAIL L=64: the second packet is not a ZLP (len=%0d)", pkt_len);
    end
    do_ack(8'd0);
    do_start(16'd65, DIR_TX);
    if (pkt_total !== 16'd2) begin
      errors = errors + 1;
      $display("FAIL L=65: total=%0d, expected 2", pkt_total);
    end
    do_ack(8'd64);
    if (pkt_len !== 8'd1) begin
      errors = errors + 1;
      $display("FAIL L=65: the second packet is %0d bytes, expected 1",
               pkt_len);
    end
    do_ack(8'd1);
    // ...and a zero-length transfer is one ZLP, not nothing at all.
    do_start(16'd0, DIR_TX);
    if (pkt_total !== 16'd1 || !pkt_is_zlp) begin
      errors = errors + 1;
      $display("FAIL L=0: total=%0d zlp=%0b, expected 1 and a ZLP",
               pkt_total, pkt_is_zlp);
    end
    do_ack(8'd0);

    // ================= PHASE 3 -- a short packet at EVERY position =======
    //
    // On receive, the peer may stop early at any packet. The residual
    // arithmetic depends on where, so every position is driven for every
    // length up to S_MAX.
    for (L = 0; L <= S_MAX; L = L + 1)
      for (sp = 0; sp < exp_total(16'(L)); sp = sp + 1) begin
        reset_all;
        do_start(16'(L), DIR_RX);
        base_dshort = int'(c_dshort);
        seen_total = 0;
        for (i = 0; i < sp; i = i + 1) begin
          seen_total = seen_total + int'(exp_plen(16'(L), 16'(i)));
          do_ack(exp_plen(16'(L), 16'(i)));
        end
        // the short one: one byte fewer than the engine offered, or skip
        // this case when the offered packet is already zero-length
        if (exp_plen(16'(L), 16'(sp)) != 8'd0) begin
          seen_total = seen_total + int'(exp_plen(16'(L), 16'(sp))) - 1;
          do_ack(exp_plen(16'(L), 16'(sp)) - 8'd1);
          n_short_cases = n_short_cases + 1;
          if (c_dshort != base_dshort + 1) begin
            errors = errors + 1;
            $display("FAIL L=%0d short at %0d did not complete SHORT", L, sp);
          end
          if (done_code !== D_SHORT) begin
            errors = errors + 1;
            $display("FAIL L=%0d short at %0d code=%0d", L, sp, done_code);
          end
          // ---- THE arithmetic. ----
          if (residual !== (16'(L) - 16'(seen_total))) begin
            errors = errors + 1;
            if (errors < 25)
              $display("FAIL L=%0d short at %0d: residual %0d expected %0d",
                       L, sp, residual, L - seen_total);
          end
          if (err_pulse !== 1'b0) begin
            errors = errors + 1;
            $display("FAIL a short packet raised an error: it is not one");
          end
        end
      end

    // ================= PHASE 4 -- babble, at EVERY position ==============
    //
    // A short packet is the peer choosing to send less. More than the packet
    // can hold is the peer sending bytes that went somewhere, and the two
    // must not be reported the same way.
    //
    // The first version of this phase drove one babble case and the mutation
    // that accepts babble silently died on FIVE checks. A property with one
    // instance is a property that is nearly untested, so every packet
    // position of a multi-packet transfer is driven, at four different
    // over-lengths.
    n_bab_cases = 0;
    for (L = 0; L < 4; L = L + 1)            // over-length index
      for (sp = 0; sp < 4; sp = sp + 1) begin
        reset_all;
        do_start(16'd256, DIR_RX);
        for (i = 0; i < sp; i = i + 1) do_ack(8'd64);
        base_dshort = int'(c_dshort);
        k = int'(c_bb);
        // 65, 100, 200, 255 -- all longer than the 64-byte packet
        do_ack((L == 0) ? 8'd65 : (L == 1) ? 8'd100 :
               (L == 2) ? 8'd200 : 8'd255);
        if (int'(c_bb) != k + 1) begin
          errors = errors + 1;
          $display("FAIL babble at position %0d, len case %0d not reported",
                   sp, L);
        end
        // ...and only MAXP bytes are counted as moved, whatever arrived.
        if (m_moved != 16'((sp + 1) * MAXP)) begin
          errors = errors + 1;
          $display("FAIL babble at %0d moved %0d, expected %0d",
                   sp, m_moved, (sp + 1) * MAXP);
        end
        // ...and it is NOT reported as a short read, because it is not one.
        if (int'(c_dshort) != base_dshort) begin
          errors = errors + 1;
          $display("FAIL babble at %0d was reported as a short packet", sp);
        end
        n_bab_cases = n_bab_cases + 1;
      end
    if (n_bab_cases != 16) begin
      errors = errors + 1;
      $display("FAIL babble cases %0d, expected 16", n_bab_cases);
    end

    // ================= PHASE 5 -- a restart, at EVERY position ============
    //
    // A second descriptor arriving while one is running. The hardware can
    // only track one, so the new one would silently replace the old and the
    // driver would wait for a completion that never comes.
    //
    // Driven at every position in the sequence and with three different
    // replacement lengths, for the same reason as phase 4: the mutation that
    // accepts the replacement died on 39 checks when this was one case.
    n_rst_cases = 0;
    for (L = 0; L < 3; L = L + 1)
      for (sp = 0; sp < 4; sp = sp + 1) begin
        reset_all;
        do_start(16'd256, DIR_TX);
        for (i = 0; i < sp; i = i + 1) do_ack(8'd64);
        k = int'(c_rs);
        do_start((L == 0) ? 16'd64 : (L == 1) ? 16'd512 : 16'd1, DIR_RX);
        if (int'(c_rs) != k + 1) begin
          errors = errors + 1;
          $display("FAIL restart at position %0d was not reported", sp);
        end
        // THE check: the running descriptor is untouched. Its length, its
        // direction, its position and its total all survive.
        if (m_len != 16'd256) begin
          errors = errors + 1;
          $display("FAIL restart at %0d replaced the length: %0d", sp, m_len);
        end
        if (m_idx != 16'(sp)) begin
          errors = errors + 1;
          $display("FAIL restart at %0d moved the position to %0d", sp, m_idx);
        end
        if (m_tot != 16'd5) begin
          errors = errors + 1;
          $display("FAIL restart at %0d changed the total to %0d", sp, m_tot);
        end
        if (m_dir != DIR_TX) begin
          errors = errors + 1;
          $display("FAIL restart at %0d flipped the direction", sp);
        end
        // ...and the original transfer still completes normally.
        w = 0;
        while (m_busy && (w < 64)) begin
          do_ack(exp_plen(m_len, m_idx));
          w = w + 1;
        end
        if (m_busy) begin
          errors = errors + 1;
          if (errors < 25)
            $display("FAIL the transfer after a restart at %0d never finished", sp);
          reset_all;
        end
        if (m_busy == 1'b0 && m_moved != 16'd256) begin
          errors = errors + 1;
          $display("FAIL after a restart at %0d the transfer moved %0d",
                   sp, m_moved);
        end
        n_rst_cases = n_rst_cases + 1;
      end
    if (n_rst_cases != 12) begin
      errors = errors + 1;
      $display("FAIL restart cases %0d, expected 12", n_rst_cases);
    end

    // ...and an acknowledgement with nothing outstanding, which is the other
    // half of the same disagreement between controller and driver.
    reset_all;
    k = int'(c_ai);
    do_ack(8'd64);
    if (int'(c_ai) != k + 1) begin
      errors = errors + 1;
      $display("FAIL an idle ack was not reported");
    end

    // The random phase is switchable, because a mutation score is only
    // interesting once it is DECOMPOSED. Phases 1 to 5 already sweep every
    // length and every short-packet position, so nothing in the exhaustive
    // claim depends on it.
`ifndef DIRECTED_ONLY
    // ================= PHASE 6 -- random =================================
    reset_all;
    for (i = 0; i < 20000; i = i + 1) begin
      w = $unsigned($random) % 100;
      if (!m_busy) begin
        if (w < 90) do_start(16'($unsigned($random) % 600), xfer_dir_e'($unsigned($random) % 2));
        else if (w < 95) do_ack(8'($unsigned($random) % 80));
        else idle;
      end else begin
        if (w < 88) begin
          // Mostly full-length acks, with a realistic fraction of short ones
          // and a small fraction of babble. Leaving babble out of the mix
          // means the one error that is NOT a short read is exercised by a
          // single directed case, and the check that separates them is under
          // no load at all.
          sp = $unsigned($random) % 100;
          if (sp < 12)
            do_ack((exp_plen(m_len, m_idx) == 8'd0) ? 8'd0
                   : (exp_plen(m_len, m_idx) - 8'd1));
          else if (sp < 15 && (m_dir == DIR_RX))
            do_ack(8'd200);                 // more than any packet can hold
          else
            do_ack(exp_plen(m_len, m_idx));
        end else if (w < 94) begin
          do_start(16'($unsigned($random) % 600), xfer_dir_e'($unsigned($random) % 2));
        end else idle;
      end
    end

`endif

    $display("steps=%0d checks=%0d errors=%0d", steps, checks, errors);
    $display("lengths swept=%0d (x2 directions)  ZLP cases=%0d  short cases=%0d",
             L_MAX + 1, n_zlp_cases, n_short_cases);
    $display("start=%0d pkt=%0d zlp=%0d done_len=%0d done_short=%0d",
             n_start, n_pkt, n_zlp, n_done_len, n_done_short);
    $display("restart=%0d ack_idle=%0d babble=%0d",
             n_restart, n_ack_idle, n_babble);
    $display("%0s: %0d errors in %0d checks",
             (errors == 0) ? "PASS" : "FAIL", errors, checks);
    $finish;
  end
endmodule

VHDL-2008 testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- Testbench for usb_dma_xfer (VHDL-2008).
--
-- The oracle is a shadow model held in process variables and written from the
-- chapter's rules rather than from the RTL, re-derived every cycle and
-- compared against every output.
--
-- THE EXHAUSTIVE CLAIM IS OVER EVERY DESCRIPTOR LENGTH
--
-- The packet sequence is a function of one number, and the interesting values
-- of that number are the ones where the arithmetic changes: the exact
-- multiples of the maximum packet size, where the zero-length terminator
-- becomes necessary. Testing "a short one, a long one and one in the middle"
-- is exactly the test that misses them.
--
--     every L in 0 .. 1024, in both directions
--     -> 2050 complete transfers, every packet of every one checked
--
-- and separately, for every L in 0 .. 256, a short packet injected at EVERY
-- position in the sequence, because the residual arithmetic depends on where
-- the peer stopped.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.usb_dma_pkg.all;

entity tb_dx_vhdl is
end entity;

architecture sim of tb_dx_vhdl is
  constant MAXP  : integer := 64;
  constant L_MAX : integer := 1024;
  constant S_MAX : integer := 256;

  signal clk     : std_logic := '0';
  signal rst_n   : std_logic := '0';
  signal start   : std_logic := '0';
  signal dir     : std_logic := DIR_TX;
  signal pkt_ack : std_logic := '0';
  signal eot     : std_logic := '0';
  signal len     : unsigned(15 downto 0) := (others => '0');
  signal rx_len  : unsigned(7 downto 0)  := (others => '0');

  signal busy_s, pkt_valid_s, pkt_is_zlp_s : std_logic;
  signal done_pulse_s, err_pulse_s         : std_logic;
  signal pkt_len_s : unsigned(7 downto 0);
  signal pkt_index_s, pkt_total_s, moved_s, residual_s : unsigned(15 downto 0);
  signal done_code_s, err_code_s : std_logic_vector(1 downto 0);
  signal n_start_s, n_pkt_s, n_zlp_s, n_dlen_s : unsigned(31 downto 0);
  signal n_dshort_s, n_rs_s, n_ai_s, n_bb_s    : unsigned(31 downto 0);

  signal done : boolean := false;
begin
  clk <= not clk after 5 ns when not done else '0';

  dut : entity work.usb_dma_xfer
    generic map (MAXP => MAXP)
    port map (
      clk => clk, rst_n => rst_n,
      start => start, len => len, dir => dir,
      pkt_ack => pkt_ack, rx_len => rx_len, eot => eot,
      busy => busy_s, pkt_valid => pkt_valid_s, pkt_len => pkt_len_s,
      pkt_is_zlp => pkt_is_zlp_s, pkt_index => pkt_index_s,
      pkt_total => pkt_total_s, moved => moved_s, residual => residual_s,
      done_pulse => done_pulse_s, done_code => done_code_s,
      err_pulse => err_pulse_s, err_code => err_code_s,
      n_start => n_start_s, n_pkt => n_pkt_s, n_zlp => n_zlp_s,
      n_done_len => n_dlen_s, n_done_short => n_dshort_s,
      n_restart => n_rs_s, n_ack_idle => n_ai_s, n_babble => n_bb_s
    );

  stim : process
    -- ---------------- the shadow model ----------------
    variable m_len, m_idx, m_tot, m_moved : unsigned(15 downto 0)
      := (others => '0');
    variable m_busy, m_dir, m_dn, m_er : std_logic := '0';
    variable m_dc : std_logic_vector(1 downto 0) := D_NONE;
    variable m_ec : std_logic_vector(1 downto 0) := E_NONE;
    variable c_start, c_pkt, c_zlp, c_dlen : integer := 0;
    variable c_dshort, c_rs, c_ai, c_bb    : integer := 0;

    variable errors, checks, steps : integer := 0;
    variable n_zlp_cases, n_short_cases : integer := 0;
    variable n_bab_cases, n_rst_cases : integer := 0;
    variable base_dlen, base_dshort, seen_total, seen_pkts : integer := 0;
    variable kk, w_v, sp_v, gw : integer := 0;
    variable ln : line;

    -- A deterministic LFSR, so a rerun reproduces exactly the same traffic.
    variable lfsr : unsigned(31 downto 0) := x"D1CE0FF5";

    impure function rnd_nat return integer is
    begin
      lfsr := lfsr(30 downto 0) &
              (lfsr(31) xor lfsr(21) xor lfsr(1) xor lfsr(0));
      -- Only the low 30 bits: a full 32-bit unsigned does not fit in VHDL's
      -- INTEGER, and to_integer aborts the run rather than wrapping.
      return to_integer(lfsr(29 downto 0));
    end function;

    -- The independent expectation, written from the chapter rather than from
    -- the design: n_full + 1 packets, for every length including zero.
    function exp_total (l : integer) return integer is
    begin
      return (l / MAXP) + 1;
    end function;

    function exp_plen (l, i : integer) return integer is
    begin
      if i < (l / MAXP) then return MAXP; else return l mod MAXP; end if;
    end function;

    procedure ck (nm : string; got, exp : integer) is
    begin
      checks := checks + 1;
      if got /= exp then
        errors := errors + 1;
        if errors < 25 then
          write(ln, string'("FAIL step=") & integer'image(steps) & " " & nm
                    & " got=" & integer'image(got)
                    & " exp=" & integer'image(exp));
          writeline(output, ln);
        end if;
      end if;
    end procedure;

    function sl2i (s : std_logic) return integer is
    begin
      if s = '1' then return 1; else return 0; end if;
    end function;

    procedure model_step is
      variable this_len : integer;
      variable take     : integer;
    begin
      m_dn := '0'; m_er := '0'; m_dc := D_NONE; m_ec := E_NONE;
      this_len := exp_plen(to_integer(m_len), to_integer(m_idx));

      if eot = '1' then
        null;
      elsif start = '1' then
        if m_busy = '1' then
          m_er := '1'; m_ec := E_RESTART; c_rs := c_rs + 1;
        else
          m_len := len; m_idx := (others => '0');
          m_tot := to_unsigned(exp_total(to_integer(len)), 16);
          m_moved := (others => '0'); m_busy := '1'; m_dir := dir;
          c_start := c_start + 1;
        end if;
      elsif pkt_ack = '1' then
        if m_busy = '0' then
          m_er := '1'; m_ec := E_ACKIDLE; c_ai := c_ai + 1;
        else
          c_pkt := c_pkt + 1;
          if this_len = 0 then c_zlp := c_zlp + 1; end if;
          if m_dir = DIR_RX then
            take := to_integer(rx_len);
          else
            take := this_len;
          end if;
          if m_dir = DIR_RX and to_integer(rx_len) > this_len then
            m_er := '1'; m_ec := E_BABBLE; c_bb := c_bb + 1;
            take := this_len;
          end if;
          m_moved := m_moved + to_unsigned(take, 16);
          if m_dir = DIR_RX and take < this_len then
            -- A SHORT packet on receive ends the transfer. Not an error.
            m_busy := '0'; m_dn := '1'; m_dc := D_SHORT;
            c_dshort := c_dshort + 1;
          elsif (m_idx + 1) >= m_tot then
            m_busy := '0'; m_dn := '1'; m_dc := D_LENGTH;
            c_dlen := c_dlen + 1;
          else
            m_idx := m_idx + 1;
          end if;
        end if;
      end if;
    end procedure;

    procedure check_out is
    begin
      ck("busy",       sl2i(busy_s),      sl2i(m_busy));
      ck("pkt_valid",  sl2i(pkt_valid_s), sl2i(m_busy));
      ck("pkt_len",    to_integer(pkt_len_s),
                       exp_plen(to_integer(m_len), to_integer(m_idx)));
      if m_busy = '1' and
         exp_plen(to_integer(m_len), to_integer(m_idx)) = 0 then
        ck("pkt_is_zlp", sl2i(pkt_is_zlp_s), 1);
      else
        ck("pkt_is_zlp", sl2i(pkt_is_zlp_s), 0);
      end if;
      ck("pkt_index",  to_integer(pkt_index_s), to_integer(m_idx));
      ck("pkt_total",  to_integer(pkt_total_s), to_integer(m_tot));
      ck("moved",      to_integer(moved_s),     to_integer(m_moved));
      ck("residual",   to_integer(residual_s),
                       to_integer(m_len) - to_integer(m_moved));
      ck("done_pulse", sl2i(done_pulse_s), sl2i(m_dn));
      ck("done_code",  to_integer(unsigned(done_code_s)),
                       to_integer(unsigned(m_dc)));
      ck("err_pulse",  sl2i(err_pulse_s), sl2i(m_er));
      ck("err_code",   to_integer(unsigned(err_code_s)),
                       to_integer(unsigned(m_ec)));
      ck("n_start",      to_integer(n_start_s),  c_start);
      ck("n_pkt",        to_integer(n_pkt_s),    c_pkt);
      ck("n_zlp",        to_integer(n_zlp_s),    c_zlp);
      ck("n_done_len",   to_integer(n_dlen_s),   c_dlen);
      ck("n_done_short", to_integer(n_dshort_s), c_dshort);
      ck("n_restart",    to_integer(n_rs_s),     c_rs);
      ck("n_ack_idle",   to_integer(n_ai_s),     c_ai);
      ck("n_babble",     to_integer(n_bb_s),     c_bb);
      -- ---- the structural invariant ----
      --
      -- Every transfer ends exactly once, by length or by a short packet.
      ck("completions", to_integer(n_dlen_s) + to_integer(n_dshort_s),
                        c_dlen + c_dshort);
    end procedure;

    procedure step is
    begin
      model_step;
      wait until rising_edge(clk);
      wait for 1 ns;
      steps := steps + 1;
      check_out;
    end procedure;

    procedure do_start (l : integer; d : std_logic) is
    begin
      start <= '1'; pkt_ack <= '0'; eot <= '0';
      len <= to_unsigned(l, 16); dir <= d;
      wait for 0 ns;
      step;
      start <= '0';
    end procedure;

    procedure do_ack (rl : integer) is
    begin
      start <= '0'; pkt_ack <= '1'; eot <= '0';
      rx_len <= to_unsigned(rl, 8);
      wait for 0 ns;
      step;
      pkt_ack <= '0';
    end procedure;

    procedure idle is
    begin
      start <= '0'; pkt_ack <= '0'; eot <= '0';
      wait for 0 ns;
      step;
    end procedure;

    procedure reset_all is
    begin
      start <= '0'; pkt_ack <= '0'; eot <= '0';
      rst_n <= '0';
      wait until rising_edge(clk);
      wait for 1 ns;
      rst_n <= '1';
      m_len := (others => '0'); m_idx := (others => '0');
      m_tot := (others => '0'); m_moved := (others => '0');
      m_busy := '0'; m_dir := DIR_TX; m_dn := '0'; m_er := '0';
      m_dc := D_NONE; m_ec := E_NONE;
      c_start := 0; c_pkt := 0; c_zlp := 0; c_dlen := 0;
      c_dshort := 0; c_rs := 0; c_ai := 0; c_bb := 0;
      wait for 1 ns;
    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;
    reset_all;

    -- ================= PHASE 1 -- EVERY length, both directions ==========
    for d in 0 to 1 loop
      for L in 0 to L_MAX loop
        if d = 1 then do_start(L, DIR_RX); else do_start(L, DIR_TX); end if;
        if to_integer(pkt_total_s) /= exp_total(L) then
          errors := errors + 1;
          if errors < 25 then
            write(ln, string'("FAIL L=") & integer'image(L) & " total "
                      & integer'image(to_integer(pkt_total_s))
                      & " expected " & integer'image(exp_total(L)));
            writeline(output, ln);
          end if;
        end if;
        base_dlen := c_dlen;
        seen_pkts := 0;
        -- 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.
        while m_busy = '1' and seen_pkts < 2 * exp_total(L) + 8 loop
          if exp_plen(to_integer(m_len), to_integer(m_idx)) = 0 then
            n_zlp_cases := n_zlp_cases + 1;
          end if;
          seen_pkts := seen_pkts + 1;
          do_ack(exp_plen(to_integer(m_len), to_integer(m_idx)));
        end loop;
        if m_busy = '1' then
          errors := errors + 1;
          write(ln, string'("FAIL L=") & integer'image(L)
                    & " never completed");
          writeline(output, ln);
          reset_all;
        end if;
        if seen_pkts /= exp_total(L) then
          errors := errors + 1;
          if errors < 25 then
            write(ln, string'("FAIL L=") & integer'image(L) & " emitted "
                      & integer'image(seen_pkts) & " packets");
            writeline(output, ln);
          end if;
        end if;
        if c_dlen /= base_dlen + 1 then
          errors := errors + 1;
          write(ln, string'("FAIL L=") & integer'image(L)
                    & " did not complete by length");
          writeline(output, ln);
        end if;
        if to_integer(m_moved) /= L then
          errors := errors + 1;
          write(ln, string'("FAIL L=") & integer'image(L) & " moved "
                    & integer'image(to_integer(m_moved)));
          writeline(output, ln);
        end if;
      end loop;
    end loop;
    -- Every exact multiple of MAXP needs a ZLP, and there are L_MAX/MAXP + 1
    -- of them in each direction.
    if n_zlp_cases /= 2 * ((L_MAX / MAXP) + 1) then
      errors := errors + 1;
      write(ln, string'("FAIL saw ") & integer'image(n_zlp_cases)
                & " zero-length packets, expected "
                & integer'image(2 * ((L_MAX / MAXP) + 1)));
      writeline(output, ln);
    end if;

    -- ================= PHASE 2 -- the ZLP boundary, named ================
    reset_all;
    do_start(63, DIR_TX);
    if to_integer(pkt_total_s) /= 1 or to_integer(pkt_len_s) /= 63 then
      errors := errors + 1;
      write(ln, string'("FAIL L=63 sequence")); writeline(output, ln);
    end if;
    do_ack(63);
    do_start(64, DIR_TX);
    if to_integer(pkt_total_s) /= 2 then
      errors := errors + 1;
      write(ln, string'("FAIL L=64 total, expected 2 (a packet and a ZLP)"));
      writeline(output, ln);
    end if;
    do_ack(64);
    if pkt_is_zlp_s /= '1' or to_integer(pkt_len_s) /= 0 then
      errors := errors + 1;
      write(ln, string'("FAIL L=64 second packet is not a ZLP"));
      writeline(output, ln);
    end if;
    do_ack(0);
    do_start(65, DIR_TX);
    if to_integer(pkt_total_s) /= 2 then
      errors := errors + 1;
      write(ln, string'("FAIL L=65 total")); writeline(output, ln);
    end if;
    do_ack(64);
    if to_integer(pkt_len_s) /= 1 then
      errors := errors + 1;
      write(ln, string'("FAIL L=65 second packet length"));
      writeline(output, ln);
    end if;
    do_ack(1);
    -- ...and a zero-length transfer is one ZLP, not nothing at all.
    do_start(0, DIR_TX);
    if to_integer(pkt_total_s) /= 1 or pkt_is_zlp_s /= '1' then
      errors := errors + 1;
      write(ln, string'("FAIL L=0 is not a single ZLP"));
      writeline(output, ln);
    end if;
    do_ack(0);

    -- ================= PHASE 3 -- a short packet at EVERY position =======
    for L in 0 to S_MAX loop
      for sp in 0 to exp_total(L)-1 loop
        reset_all;
        do_start(L, DIR_RX);
        base_dshort := c_dshort;
        seen_total := 0;
        for i in 0 to sp-1 loop
          seen_total := seen_total + exp_plen(L, i);
          do_ack(exp_plen(L, i));
        end loop;
        if exp_plen(L, sp) /= 0 then
          seen_total := seen_total + exp_plen(L, sp) - 1;
          do_ack(exp_plen(L, sp) - 1);
          n_short_cases := n_short_cases + 1;
          if c_dshort /= base_dshort + 1 then
            errors := errors + 1;
            write(ln, string'("FAIL short did not complete SHORT"));
            writeline(output, ln);
          end if;
          if done_code_s /= D_SHORT then
            errors := errors + 1;
            write(ln, string'("FAIL short completion code"));
            writeline(output, ln);
          end if;
          -- ---- THE arithmetic. ----
          if to_integer(residual_s) /= L - seen_total then
            errors := errors + 1;
            if errors < 25 then
              write(ln, string'("FAIL L=") & integer'image(L) & " short at "
                        & integer'image(sp) & ": residual "
                        & integer'image(to_integer(residual_s))
                        & " expected " & integer'image(L - seen_total));
              writeline(output, ln);
            end if;
          end if;
          if err_pulse_s /= '0' then
            errors := errors + 1;
            write(ln, string'("FAIL a short packet raised an error"));
            writeline(output, ln);
          end if;
        end if;
      end loop;
    end loop;

    -- ================= PHASE 4 -- babble, at EVERY position ==============
    --
    -- A short packet is the peer choosing to send less. More than the packet
    -- can hold is the peer sending bytes that went somewhere, and the two must
    -- not be reported the same way.
    --
    -- The first version of this phase drove one babble case and the mutation
    -- that accepts babble silently died on FIVE checks. Every packet position
    -- of a multi-packet transfer is driven, at four different over-lengths.
    n_bab_cases := 0;
    for Lv in 0 to 3 loop
      for sp in 0 to 3 loop
        reset_all;
        do_start(256, DIR_RX);
        for i in 0 to sp-1 loop do_ack(64); end loop;
        base_dshort := c_dshort;
        kk := c_bb;
        if    Lv = 0 then do_ack(65);
        elsif Lv = 1 then do_ack(100);
        elsif Lv = 2 then do_ack(200);
        else              do_ack(255);
        end if;
        if c_bb /= kk + 1 then
          errors := errors + 1;
          write(ln, string'("FAIL babble not reported at position ")
                    & integer'image(sp));
          writeline(output, ln);
        end if;
        -- ...and only MAXP bytes are counted as moved, whatever arrived.
        if to_integer(m_moved) /= (sp + 1) * MAXP then
          errors := errors + 1;
          write(ln, string'("FAIL babble moved the wrong count"));
          writeline(output, ln);
        end if;
        -- ...and it is NOT reported as a short read, because it is not one.
        if c_dshort /= base_dshort then
          errors := errors + 1;
          write(ln, string'("FAIL babble was reported as a short packet"));
          writeline(output, ln);
        end if;
        n_bab_cases := n_bab_cases + 1;
      end loop;
    end loop;
    if n_bab_cases /= 16 then
      errors := errors + 1;
      write(ln, string'("FAIL babble cases ") & integer'image(n_bab_cases));
      writeline(output, ln);
    end if;

    -- ================= PHASE 5 -- a restart, at EVERY position ============
    --
    -- A second descriptor arriving while one is running. The hardware can only
    -- track one, so the new one would silently replace the old and the driver
    -- would wait for a completion that never comes.
    n_rst_cases := 0;
    for Lv in 0 to 2 loop
      for sp in 0 to 3 loop
        reset_all;
        do_start(256, DIR_TX);
        for i in 0 to sp-1 loop do_ack(64); end loop;
        kk := c_rs;
        if    Lv = 0 then do_start(64, DIR_RX);
        elsif Lv = 1 then do_start(512, DIR_RX);
        else              do_start(1, DIR_RX);
        end if;
        if c_rs /= kk + 1 then
          errors := errors + 1;
          write(ln, string'("FAIL restart not reported at position ")
                    & integer'image(sp));
          writeline(output, ln);
        end if;
        -- THE check: the running descriptor is untouched.
        if to_integer(m_len) /= 256 then
          errors := errors + 1;
          write(ln, string'("FAIL restart replaced the length"));
          writeline(output, ln);
        end if;
        if to_integer(m_idx) /= sp then
          errors := errors + 1;
          write(ln, string'("FAIL restart moved the position"));
          writeline(output, ln);
        end if;
        if to_integer(m_tot) /= 5 then
          errors := errors + 1;
          write(ln, string'("FAIL restart changed the total"));
          writeline(output, ln);
        end if;
        if m_dir /= DIR_TX then
          errors := errors + 1;
          write(ln, string'("FAIL restart flipped the direction"));
          writeline(output, ln);
        end if;
        -- ...and the original transfer still completes normally.
        gw := 0;
        while m_busy = '1' and gw < 64 loop
          do_ack(exp_plen(to_integer(m_len), to_integer(m_idx)));
          gw := gw + 1;
        end loop;
        if m_busy = '1' then
          errors := errors + 1;
          write(ln, string'("FAIL the transfer after a restart never finished"));
          writeline(output, ln);
          reset_all;
        end if;
        if m_busy = '0' and to_integer(m_moved) /= 256 then
          errors := errors + 1;
          write(ln, string'("FAIL the transfer did not complete after a restart"));
          writeline(output, ln);
        end if;
        n_rst_cases := n_rst_cases + 1;
      end loop;
    end loop;
    if n_rst_cases /= 12 then
      errors := errors + 1;
      write(ln, string'("FAIL restart cases ") & integer'image(n_rst_cases));
      writeline(output, ln);
    end if;

    -- ...and an acknowledgement with nothing outstanding, which is the other
    -- half of the same disagreement between controller and driver.
    reset_all;
    kk := c_ai;
    do_ack(64);
    if c_ai /= kk + 1 then
      errors := errors + 1;
      write(ln, string'("FAIL an idle ack was not reported"));
      writeline(output, ln);
    end if;

    -- ================= PHASE 6 -- random =================================
    reset_all;
    for i in 0 to 19999 loop
      w_v := rnd_nat mod 100;
      if m_busy = '0' then
        if w_v < 90 then
          if (rnd_nat mod 2) = 1 then do_start(rnd_nat mod 600, DIR_RX);
          else                        do_start(rnd_nat mod 600, DIR_TX); end if;
        elsif w_v < 95 then do_ack(rnd_nat mod 80);
        else idle; end if;
      else
        if w_v < 88 then
          sp_v := rnd_nat mod 100;
          if sp_v < 12 then
            if exp_plen(to_integer(m_len), to_integer(m_idx)) = 0 then
              do_ack(0);
            else
              do_ack(exp_plen(to_integer(m_len), to_integer(m_idx)) - 1);
            end if;
          elsif sp_v < 15 and m_dir = DIR_RX then
            do_ack(200);
          else
            do_ack(exp_plen(to_integer(m_len), to_integer(m_idx)));
          end if;
        elsif w_v < 94 then
          if (rnd_nat mod 2) = 1 then do_start(rnd_nat mod 600, DIR_RX);
          else                        do_start(rnd_nat mod 600, DIR_TX); end if;
        else idle; end if;
      end if;
    end loop;

    write(ln, string'("steps=") & integer'image(steps)
              & " checks=" & integer'image(checks)
              & " errors=" & integer'image(errors));
    writeline(output, ln);
    write(ln, string'("lengths swept=") & integer'image(L_MAX + 1)
              & " (x2 directions)  ZLP cases=" & integer'image(n_zlp_cases)
              & "  short cases=" & integer'image(n_short_cases));
    writeline(output, ln);
    write(ln, string'("start=") & integer'image(to_integer(n_start_s))
              & " pkt=" & integer'image(to_integer(n_pkt_s))
              & " zlp=" & integer'image(to_integer(n_zlp_s))
              & " done_len=" & integer'image(to_integer(n_dlen_s))
              & " done_short=" & integer'image(to_integer(n_dshort_s)));
    writeline(output, ln);
    write(ln, string'("restart=") & integer'image(to_integer(n_rs_s))
              & " ack_idle=" & integer'image(to_integer(n_ai_s))
              & " babble=" & integer'image(to_integer(n_bb_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

MeasureVerilogSystemVerilogVHDL
descriptor lengths swept1025 × 21025 × 21025 × 2
zero-length-terminator cases34 / 3434 / 3434 / 34
short-packet positions640 / 640640 / 640640 / 640
babble positions × over-lengths16 / 1616 / 1616 / 16
restart positions × lengths12 / 1212 / 1212 / 12
Steps415784157841578
Checks executed873138873138873138
packets driven144181441813991
zero-length packets474752
completed by length / short2466 / 7492466 / 7492425 / 738
restarts / idle acks / babble994 / 182 / 177994 / 182 / 1771203 / 270 / 195
ResultPASSPASSPASS

2050 complete transfers, every packet of every one compared against a length computed from the rule in section 1 rather than from the design — and 640 more transfers with the peer stopping at every possible point.

12. Mutation Testing

#MutationVerilogSysVerVHDL
W4a short packet does not end the transfer215577215577218640
W7a second descriptor silently replaces a running one200201200201206527
W1ceil(L/M) instead of n_full + 1195252195252172609
W2the last packet is emitted at full length117024117024116657
W5the residual is always zero373003730037331
W6babble is accepted as data216822168221777
W3a short packet is ALSO reported as an error341834183396
—unmutated baseline000

All seven die in all three languages.

W3 is two orders of magnitude below the rest, and it is the one most likely to ship. It does not break the transfer. Every byte still arrives, every completion still fires, the residual is still right — the only difference is an extra error report on each short read. So it dies only on the err_pulse and err_code checks of the 749 short completions, and nowhere else.

Directed against random

#All phasesDirected onlyRandom
W119525259454135798
W21170244681370211
W3341819201498
W42155774309211268
W5373002009417206
W6216828021602
W7200201578199623

Every mutation is killed by directed stimulus alone, and W3 is the one case where the directed half is larger than the random half — because the exhaustive length sweep drives 2050 transfers that each end cleanly, and every one of them is an opportunity to notice a spurious error. The random phase, which mixes short reads in at 12%, produces fewer.

W6 and W7 were 5 and 39 before their phases were made exhaustive. Babble was driven once and a restart once; making both sweep every packet position — 16 babble cases and 12 restart cases — took them to 80 and 578. It is the same lesson as 26.1's fairness phase: a property with one instance is a property that is nearly untested.

13. Debugging Walkthrough: The Transfer That Works Until You Round the Buffer Up

The report. A data-logger firmware works for eighteen months. A routine change replaces a 1000-byte staging buffer with 1024 bytes "for alignment". The device now hangs on the first transfer after boot, on some hosts.

Step 1 — what changed? The buffer size, and nothing else. The data is identical, the endpoint is identical, the descriptor chain is identical.

Step 2 — where does it hang? The device has sent all 1024 bytes. The host is not asking for more and not completing the transfer. Both sides are waiting.

Step 3 — count the packets. 1024 bytes over a 64-byte endpoint is 16 packets, all full. Nothing is short.

Step 4 — so nothing terminated the transfer. The host requested 2048 bytes (its own buffer size), received 1024 in 16 full packets, and is still waiting for either 1024 more bytes or a short packet. The device thinks it is finished. Neither will move.

Step 5 — why 1000 bytes worked. 1000 is 15 full packets and one of 40 bytes. The 40-byte packet is short, so it terminated the transfer for free. The old buffer size was hiding the bug.

Step 6 — why "on some hosts". A host that requested exactly 1024 bytes has its own length-based completion and does not need the terminator. One that requested more does. So the failure depends on the host's buffer size, which is why it reproduced on two machines out of five.

The fix. One line in the DMA engine: total = n_full + 1, unconditionally.

14. UVM: A Descriptor Item That Carries Its Own Expected Sequence

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The point of this item is the function at the bottom. A sequence item that
// only carries a length makes the scoreboard responsible for working out what
// the packet sequence should be -- and the scoreboard will do it the same way
// the RTL does, because whoever writes it reads the RTL.
//
// Computing the expected sequence HERE, from the rule, means the scoreboard
// compares against an independent derivation rather than a reimplementation.
class usb_dma_desc extends uvm_sequence_item;
  `uvm_object_utils(usb_dma_desc)

  rand int unsigned len;
  rand bit          is_rx;
  rand int unsigned maxp;

  function new(string name = "usb_dma_desc"); super.new(name); endfunction

  // ---- The distribution includes the EXACT MULTIPLES, deliberately. ----
  //
  // A uniform draw over 0..4095 hits a multiple of 64 about 1.5% of the
  // time, which is enough to find this eventually and not enough to find it
  // in a nightly. They are weighted up because they are the entire failure
  // mode.
  constraint c_len {
    maxp inside {8, 16, 32, 64, 512};
    len inside {[0:4096]};
    // one in four descriptors is an exact multiple of the packet size
    (len % maxp == 0) dist { 1 := 1 };
  }
  constraint c_mult { solve maxp before len; }

  // ---- The expected packet sequence, from the RULE. ----
  //
  // n_full + 1, always. Returning a queue rather than a count so the
  // scoreboard can compare LENGTHS in order, not just how many there were --
  // a design that emits the right number of packets with the last one at full
  // length is a design that overruns the host's buffer.
  function void expected_packets(ref int unsigned q[$]);
    int unsigned n_full = len / maxp;
    q.delete();
    for (int i = 0; i < n_full; i++) q.push_back(maxp);
    q.push_back(len % maxp);        // ZERO when len is an exact multiple
  endfunction

  function bit needs_zlp(); return (len % maxp) == 0; endfunction
endclass


// ---------------------------------------------------------------------
// The scoreboard. Two queues and one comparison, and the comparison is of
// LENGTHS IN ORDER rather than of a total.
// ---------------------------------------------------------------------
class usb_dma_sb extends uvm_scoreboard;
  `uvm_component_utils(usb_dma_sb)

  `uvm_analysis_imp_decl(_desc)
  `uvm_analysis_imp_decl(_pkt)

  uvm_analysis_imp_desc #(usb_dma_desc, usb_dma_sb) desc_ap;
  uvm_analysis_imp_pkt  #(usb_pkt_item, usb_dma_sb) pkt_ap;

  int unsigned expect_q[$];
  usb_dma_desc cur;
  int unsigned moved, n_desc, n_pkt, n_zlp, n_short_done, n_len_done;

  function new(string name, uvm_component parent);
    super.new(name, parent);
    desc_ap = new("desc_ap", this);
    pkt_ap  = new("pkt_ap", this);
  endfunction

  function void write_desc(usb_dma_desc d);
    if (cur != null && expect_q.size() > 0)
      `uvm_error("DMA/RESTART",
        $sformatf("a new descriptor arrived with %0d packets still expected",
                  expect_q.size()))
    cur = d;
    moved = 0;
    d.expected_packets(expect_q);
    n_desc++;
    // The count is asserted here, before any packet arrives, because it is a
    // property of the LENGTH alone -- and getting it from the design would
    // make the whole comparison circular.
    if (expect_q.size() != (d.len / d.maxp) + 1)
      `uvm_fatal("DMA/MODEL", "the expected-sequence function is wrong")
  endfunction

  function void write_pkt(usb_pkt_item p);
    int unsigned want;
    n_pkt++;
    if (expect_q.size() == 0) begin
      `uvm_error("DMA/EXTRA", "a packet arrived with none expected")
      return;
    end
    want = expect_q.pop_front();

    // ---- Length, in order. Not a running total. ----
    //
    // A design that sends the right number of bytes in the wrong packet
    // boundaries passes a total comparison and fails on the wire, because the
    // receiver's terminator is a packet LENGTH and not a byte count.
    if (p.len != want && !(cur.is_rx && p.len < want))
      `uvm_error("DMA/PKTLEN",
        $sformatf("packet %0d is %0d bytes, expected %0d", n_pkt, p.len, want))

    if (want == 0) begin
      n_zlp++;
      if (!cur.needs_zlp())
        `uvm_error("DMA/ZLP",
          "a zero-length packet was sent for a length that does not need one")
    end

    moved += p.len;

    // ---- On receive, a short packet ends it. Not an error. ----
    if (cur.is_rx && p.len < want) begin
      n_short_done++;
      `uvm_info("DMA/SHORT",
        $sformatf("transfer ended early: %0d of %0d bytes, residual %0d",
                  moved, cur.len, cur.len - moved), UVM_HIGH)
      expect_q.delete();
      cur = null;
    end else if (expect_q.size() == 0) begin
      n_len_done++;
      if (moved != cur.len)
        `uvm_error("DMA/MOVED",
          $sformatf("the sequence completed having moved %0d of %0d bytes",
                    moved, cur.len))
      cur = null;
    end
  endfunction

  // ---- The check that only the end of the test can make. ----
  //
  // A descriptor whose sequence never completed is a driver blocked for ever
  // on a transfer the device thinks it finished -- which is section 13's bug,
  // and it produces no error of its own anywhere.
  function void check_phase(uvm_phase phase);
    super.check_phase(phase);
    if (cur != null)
      `uvm_error("DMA/UNTERMINATED",
        $sformatf("a descriptor of %0d bytes never completed: %0d packets still expected. On the wire this is a hang, not an error.",
                  cur.len, expect_q.size()))
  endfunction

  function void report_phase(uvm_phase phase);
    super.report_phase(phase);
    `uvm_info("DMA",
      $sformatf("%0d descriptors, %0d packets, %0d ZLPs | completed by length %0d, short %0d",
                n_desc, n_pkt, n_zlp, n_len_done, n_short_done),
      UVM_LOW)
    // A run that produced no zero-length packets never tested the terminator,
    // whatever else it did. Stated as a coverage failure rather than left for
    // a reader to notice.
    if (n_zlp == 0)
      `uvm_error("DMA/NO_ZLP",
        "no zero-length packet in the whole run: the exact-multiple case was never generated")
  endfunction
endclass

15. Common Misconceptions

"The packet count is ceil(L/M)." It is n_full + 1. The two agree except on the exact multiples, which are the sizes everybody uses.

"A zero-length packet is a degenerate case." It is the terminator. Without it the receiver waits for ever.

"L = 0 means nothing is sent." It means one zero-length packet is sent. It is a real transfer with a real completion.

"A residual is an under-run." It is how every variable-length response arrives. Reporting it as an error produces one false error per short read.

"Short and complete are both just 'done'." They mean different things to the driver, which is why the completion carries a code.

"Fewer bytes and more bytes are both length mismatches." Fewer is normal. More is a buffer overrun.

"The DMA completion is the transfer completion." The DMA engine finishes when the bytes are in the FIFO. The transfer finishes when the peer has taken them.

"Testing a few lengths covers the arithmetic." The failure set is exactly the multiples of the packet size, which no hand-picked sample contains.

16. Exercises

1. A 4096-byte buffer is sent over a 512-byte endpoint. Give the packet sequence under both rules and say what the receiver does in each case.

2. ZLP cases = 34 for a sweep of 0..1024 over a 64-byte endpoint. Derive it, then give the number for a 512-byte endpoint.

3. W3 scores 3418 against W4's 215577 and is the more likely to ship. Explain both facts from what each mutation does to the transfer.

4. On receive, the design treats rx_len < pkt_len as a completion and rx_len > pkt_len as babble. Say what it should do with rx_len > pkt_len on the LAST packet of a transfer, and whether the current design is right.

5. The residual is len - moved and is meaningful only after completion. Construct the reading a driver would get if it sampled it mid-transfer, and say what the design should do about it.

6. A ZLP is one packet on the wire and carries no data. Work out what it costs in bus time at high speed, and whether a transfer-size policy could avoid ever needing one.

7. The UVM scoreboard fails a run with no zero-length packets. Write the constraint that guarantees one, then say why the check should stay anyway.

17. Summary

IdeaWhy it matters
A descriptor has a length; the wire has packetsand the conversion has one exception everybody hits
total = n_full + 1, alwaysceil(L/M) is right except on the exact multiples
A short packet is the only terminatorso an all-full sequence never ends
An exact multiple needs a ZLPand those are the sizes that survive code review
L = 0 is one ZLP, not nothinga real transfer with a real completion
A residual is not an errorit is how variable-length responses arrive
Two endings, two codesthe driver needs to know which
Babble is not a short readthe extra bytes overran somebody's buffer
Compare packet lengths in ordera right total with wrong boundaries fails on the wire
A low mutation score can be the shipped bugW3 breaks nothing and fills the log
Fail fast on an unadvanceable designor one mutation reports 96 million and hides the rest
2050 transfers, 34 ZLP cases, 640 short positions7 mutations, all killed in 3 languages

Tooling

StepCommand
Verilog-2005iverilog -g2005 -o dx_v.out dx_v.v dx_v_tb.v && ./dx_v.out
SystemVerilogiverilog -g2012 -o dx_sv.out dx_sv.sv dx_sv_tb.sv && ./dx_sv.out
VHDL-2008 analysenvc --std=2008 -a dx_vhdl.vhd dx_vhdl_tb.vhd
VHDL-2008 elaboratenvc --std=2008 -e tb_dx_vhdl
VHDL-2008 runnvc --std=2008 -r tb_dx_vhdl
One mutationiverilog -g2005 -DMUT_W1 -o mm dx_v_mut.v dx_v_tb.v && ./mm
Directed onlyiverilog -g2005 -DDIRECTED_ONLY -o mm dx_v_mut.v dx_v_tb.v && ./mm

All three implementations pass with 0 errors: every descriptor length from 0 to 1024 driven in both directions with every packet compared against the rule, all 34 zero-length-terminator cases reached and counted, a short packet injected at all 640 positions, babble and restart swept over every packet position, and every one of the seven mutations killed by directed stimulus alone.


Chapter 26.3 — AXI Interfaces to USB takes the DMA engine's bytes and puts them on a bus. Its central rule is the one AXI states outright and the one it is easiest to get away with breaking: a burst must not cross a 4 KB address boundary. Many interconnects tolerate a burst that does, which is why an engine that ignores the rule works perfectly on the SoC it was written on and corrupts memory on the next one.

Continue learning

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.