Skip to content
VLSI Mentor

USB · Module 29

USB in Embedded Devices

On a microcontroller the controller is a peripheral, and the protocol can be perfectly correct while the device goes deaf. Everything turns on one question — who owns this buffer right now — answered by one bit per buffer and exhausted over 132 transitions in three languages.

The fifth case study, and the first one where USB is not the product. A flash drive, a webcam, an audio interface and a phone are all things that are USB devices. A sensor node with a USB port is a thing that happens to have a USB port, and the controller inside it is one peripheral among a dozen — sharing a bus, a clock tree, a reset tree, an interrupt controller and a few kilobytes of RAM with everything else on the chip.

1. The Protocol Can Be Right And The Product Still Fails

Here is a failure that is genuinely common, and that no amount of staring at the specification will explain.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    SYMPTOM   The device enumerates. The host sees the right descriptors, the
              right endpoints, the right class. Then it goes deaf: every OUT
              transfer is NAKed forever, or worse, every packet is accepted
              and discarded. A bus analyser shows nothing wrong with the
              PACKETS -- they are well-formed, correctly addressed, correctly
              CRCed, and correctly handshaken.

The protocol is being spoken correctly. The product does not work. The reason is always in the same place: the controller and the firmware disagree about the state of something they share, and the wire cannot show you a disagreement between two things that are both on the device side of it.

Where the controller sits, and where firmware meets it

A stack showing the layers of an embedded USB device from the connector upward. At the bottom, the USB connector and cable. Above it the PHY, which is analog and has its own clock domain. Above that the serial interface engine, which handles packet framing, CRC checking and handshakes. Above that the endpoint buffer ownership layer, highlighted as the subject of this chapter, which is where the controller and the firmware meet. Above that the register file that firmware reads and writes. Above that the firmware class driver, and at the top the application.An embedded USB device, bottom to topApplication firmwareSensor reads, command handling, product behaviourSensor reads, command handling, product behaviourClass driver and descriptorsEnumeration, control transfers, class requestsEnumeration, control transfers, class requestsThe register seamOwnership, lengths, toggles. Two agents write here.Ownership, lengths, toggles. Two agents write here.Serial interface engineFraming, PID decode, CRC16, handshake generationFraming, PID decode, CRC16, handshake generationPHYDifferential driver, receiver, its own clock domainDifferential driver, receiver, its own clock domainConnector and cableWhere chapters 29.1 to 29.4 did their workWhere chapters 29.1 to 29.4 did their work
Read upward. Each band is a different kind of engineering with a different failure mode, and only one boundary in the stack is crossed by two independent agents at once: the one highlighted. Everything above it runs on a CPU; everything below it runs on a clock.

2. Four Resets, And They Are Not The Same Reset

Before the ownership question, a distinction that causes more embedded USB bugs than any single line of RTL. The word "reset" names at least four different events in a USB device, and they clear different things.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    POWER-ON / SoC RESET
        The chip comes up. Nothing is configured, no firmware has run, there
        are no descriptors, there is no address. Everything is cleared,
        firmware configuration included, because there is no firmware
        configuration yet.

    USB BUS RESET
        The host drove SE0 on the data lines for long enough to mean it. The
        device must return to its DEFAULT state: address zero, unconfigured,
        and -- the part people forget -- every endpoint's data toggle back to
        DATA0. The chip is not reset. The CPU keeps running. Firmware's
        variables are all still there.

    ENDPOINT RESET (clearing a halt)
        The host sent CLEAR_FEATURE(ENDPOINT_HALT) for one endpoint. That
        endpoint's data toggle goes back to DATA0. Nothing else in the device
        is affected, and no other endpoint is affected.

    SOFTWARE RESET
        Firmware decided to re-initialise the controller, usually because it
        got lost. Whatever the datasheet says it clears, which is not
        necessarily any of the above.

Four reset sources, and the state each one is allowed to touch

A diagram of four reset sources in an embedded USB device, one per row, each read left to right as event, scope and survivors. A power-on reset clears everything including the configuration, and nothing survives because nothing has been configured yet. A USB bus reset, caused by the host driving SE0, clears the buffer ownership, the pointers and the data toggle, and the endpoint configuration survives. A host request to clear an endpoint halt returns only that endpoint's data toggle to DATA0, and the buffers and every other endpoint survive. A firmware software reset clears whatever the datasheet says it clears, and nothing about it can be assumed.Power-on resetthe chip comes upEverythingstate and configNothing survivesthere is nothing yetUSB bus resethost drove SE0Data and togglebuffers, ptrs, DATA0Config survivesep_en, max packetClear haltone host requestToggle onlyback to DATA0Buffers surviveand other endpointsSoftware resetfirmware gave upDatasheet onlyread it carefullyUnspecifiedassume nothingclearsleavesclearsleavesclearsleavesclearsleaves12
One row per reset, read left to right: the event, what it returns to default, and what it must leave alone. The second row is the one that causes shipping bugs in both directions — a scope one line too long clears firmware's configuration on every suspend, and one line too short leaves a stale data toggle and a device that acknowledges everything while accepting nothing.

The design in this chapter takes rst_n and usb_reset as two separate inputs, clears different things from each, and makes the endpoint-halt case a firmware register write. That is not an abundance of caution. It is the minimum number of reset scopes a USB endpoint can be built with and still be correct.

3. Who Owns This Buffer

Now the central question. A packet arrives. The engine has checked its CRC and wants to hand it over. Firmware is in the middle of copying the previous packet out of memory. What happens?

There are only three possible answers, and two of them are wrong.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    OVERWRITE      put the new packet where the old one is. Firmware is
                   reading a buffer whose contents change under it. Silent
                   data corruption, and no error is reported anywhere.

    DROP           throw the new packet away. Correct on the wire only if the
                   device also NAKs it, so the host retries. If the device
                   ACKs and drops, the data is gone and nobody knows.

    HAVE A SECOND  put it somewhere else. The endpoint has two buffers, the
    BUFFER         hardware fills one while firmware drains the other, and
                   the device only has to NAK when BOTH are busy.

The third is what "double buffering", "ping-pong" and "dual-bank" all name. It exists for one reason and it is worth stating precisely, because it is also the reason the verification in this chapter is shaped the way it is:

So the state is: two buffers, and one bit per buffer saying who owns it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    full[i] == 0     the HARDWARE owns buffer i. It may write into it.
    full[i] == 1     FIRMWARE owns buffer i. It has a packet to read, and the
                     hardware must not touch it.

One bit, one owner, no second copy. The moment there are two places that record whether a buffer is busy, they can disagree, and the bug is not findable from the wire.

Two pointers say which buffer each side is looking at:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    hw_ptr    the buffer the next accepted packet goes into
    fw_ptr    the buffer firmware reads next

And the ordering guarantee — packets come out in the order they went in — is what those two pointers are for. They each advance by exactly one, for their own reason, and nothing else moves them.

The hardware side: where a packet goes

A block diagram of the hardware side of a double-buffered USB OUT endpoint, read left to right. The serial interface engine presents a packet as a commit pulse, a length and a PID toggle bit. A toggle check decides whether the packet is new data or a retransmission of one already accepted, and an accepted packet sets one of the two ownership bits. The hardware pointer sits above those bits and selects both which ownership bit is set and which of the two length registers the packet's length is written into. When both ownership bits are set, the NAK output to the right of them asserts and the engine must stall the host.Enginecommit, len, PIDToggle checknew, or a resend?hw_ptrwhich buffer nextfull[1:0]one bit per bufferlen0, len1the length rides inhw_nakboth busy: stallpacketacceptwhich bitwhich regboth12
Read left to right. The toggle check is the only place a packet can be turned away as a duplicate; the hardware pointer decides both which ownership bit is set and which length register is written; and the two ownership bits are the only state the firmware side also touches. NAK is drawn as a result on the right rather than as a return arrow to the engine, so that every line on this path runs one way.

The firmware side: what the CPU sees

A block diagram of the firmware side of a double-buffered USB OUT endpoint. On the left, the two ownership bits, the two captured lengths and the firmware read pointer all feed a register file in the middle, which exposes four registers named CTRL, STAT, LEN and ACK. From the register file an interrupt goes to the CPU when either buffer is owned by firmware, and the register values go to firmware, which reads the length and then writes the acknowledge register to release the buffer.full[1:0]the same two bitslen0, len1the captured lengthsfw_ptrwhich one is nextRegister fileCTRL STAT LEN ACKirqto the CPUFirmwareread LEN, then ACKACK writereleases, advancesSTATLENselectsany fullreadswrite 112
The same two ownership bits, read from the other direction. Everything firmware can observe arrives through four registers, and everything firmware can do to the endpoint is a write to one of them. The acknowledge write is the return path: it clears the ownership bit and advances the read pointer, which is why the read pointer is an input to the register file rather than something firmware sets.

4. The Register Map Is The Contract, And One Bit Of It Is Unusual

Firmware sees four registers. Three of them are ordinary. The fourth is where the interesting decision lives.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    ADDR  NAME   ACCESS       CONTENTS
    ----  -----  -----------  --------------------------------------------
    0     CTRL   see below    bit 0  ep_en   endpoint enabled
                              bit 1  dtog    the data toggle
    1     STAT   read-only    bits 1:0  full     who owns each buffer
                              bit  2    fw_ptr   which one firmware reads
                              bit  3    hw_ptr   which one hardware fills
                              bit  4    overrun  sticky: a packet was lost
                              bit  5    badack   sticky: an illegal release
    2     LEN    read-only    the length of the buffer at fw_ptr, in bytes
    3     ACK    write-1      release the buffer at fw_ptr

CTRL bit 0 is an ordinary read-write configuration bit: firmware owns it, the hardware never changes it, and reading it back gives what was written.

CTRL bit 1 is not. It is a write-1-to-toggle bit: writing a one flips it, writing a zero leaves it alone. And it has to be, for a reason that is the clearest example in this chapter of a hardware decision forced by a software problem.

There is a second, quieter decision in that table. ACK releases the buffer at fw_ptr, not a buffer firmware names. The alternative — a bit per buffer, write one to clear — lets firmware release buffer 1 before buffer 0, and the ordering guarantee is gone. Putting the ordering in hardware costs nothing and removes a way for firmware to be wrong.

And the third: releasing a buffer firmware does not own is an illegal operation, and the hardware's response to it is to do nothing at all and set a sticky flag. In particular fw_ptr must not move. If it does, one stray write desynchronises the two pointers permanently: firmware then reads the buffer the hardware is filling, forever, and the device is broken until the next bus reset. That is mutation M6.

5. The Design (Verilog-2005)

The whole hardware contract, written down before any code, because three languages have to implement the same thing:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    PURPOSE      hold the ownership state of a two-buffer USB OUT endpoint,
                 and expose it to firmware as four registers

    INPUTS       clk, rst_n                 the chip's clock and reset
                 usb_reset                  one cycle, from the SE0 detector
                 rx_commit, rx_pid_odd      a packet whose CRC checked out
                 rx_len                     its length, 0..MAXPKT
                 rx_abort                   a packet that did not
                 fw_we, fw_addr, fw_wdata   the peripheral bus

    OUTPUTS      hw_nak                     the engine must NAK the next OUT
                 hw_buf_sel                 where the next packet goes
                 hw_dup                     one cycle: that was a resend
                 fw_rdata                   the register read data
                 irq                        level, to the interrupt controller
                 five counters               observation only

    AUTHORITATIVE STATE
                 full[1:0]   one ownership bit per buffer
                 len0, len1  the captured length of each buffer
                 hw_ptr      which buffer hardware fills next
                 fw_ptr      which buffer firmware reads next
                 dtog        the data toggle this endpoint EXPECTS next
                 ep_en       firmware's configuration bit
                 two sticky error flags

    DERIVED, WITH NO STATE OF THEIR OWN
                 hw_nak = not ep_en, or both buffers owned by firmware
                 irq    = ep_en and either buffer owned by firmware
                 the four register read values

    RESET SCOPES
                 rst_n      everything, ep_en and the counters included
                 usb_reset  the data state and the toggle; NOT ep_en, NOT
                            the counters

    PRIORITY, SAME CYCLE
                 rst_n            beats everything
                 usb_reset        beats every firmware write and every packet
                 a hardware set and a firmware release COMPOSE
                 a hardware toggle flip and a firmware toggle flip COMPOSE
                 an accept into the buffer being released is impossible while
                 the engine honours hw_nak, and is asserted to be so

    LATENCY      one clock. Every output is a function of registered state,
                 so the effect of a packet is visible the cycle after it.

    BOUNDARIES   rx_len == 0        a real packet. Takes a buffer.
                 rx_len == MAXPKT   the width boundary of the length register
                 both buffers full  hw_nak, and a commit anyway is an overrun
                 release with nothing owned  a no-op, and fw_ptr must not move

    ASSUMPTIONS  the engine honours hw_nak
                 rx_len is already validated against MAXPKT upstream
                 usb_reset is a single-cycle pulse

    OMISSIONS    the packet RAM, the PHY, the engine, CRC16, endpoint 0,
                 DMA, the bus protocol, IN endpoints, isochronous endpoints
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  usbep_pingpong -- the buffer-ownership block of a double-buffered
//  ("ping-pong") USB OUT endpoint in a microcontroller-class device
//  controller. This is the hardware/firmware seam: the registers and
//  the ownership state that a USB controller and the firmware running
//  on the same chip use to hand packets to each other.
//
//  CLASSIFICATION: simplified synthesisable teaching RTL.
//  It is NOT a USB device controller. There is no PHY, no serial
//  interface engine, no bit unstuffing, no CRC16, no packet RAM, no
//  endpoint 0 control transfer machine and no DMA. Everything upstream
//  of it is reduced to three pins -- rx_commit, rx_pid_odd, rx_len --
//  and everything downstream of it is reduced to a four-register bus.
//  What is left is the part firmware actually races with.
// =====================================================================
module usbep_pingpong #(
  // Endpoint maximum packet size in bytes. 64 is the full-speed bulk
  // maximum. LENW must hold 0..MAXPKT INCLUSIVE, so 64 needs 7 bits.
  parameter integer MAXPKT = 64,
  parameter integer LENW   = 7
) (
  input  wire            clk,
  // Power-on / SoC reset. Clears everything, firmware configuration
  // included, because after it there is no firmware state to honour.
  input  wire            rst_n,

  // ---- USB bus reset: a DIFFERENT reset, one cycle wide ----------
  // The host has driven SE0 for long enough that the device must return
  // to its default state. This clears the endpoint's DATA PATH state
  // and its data toggle, and deliberately does NOT clear ep_en.
  input  wire            usb_reset,

  // ---- serial interface engine side ------------------------------
  // One cycle, asserted at the end of a packet whose CRC checked out.
  input  wire            rx_commit,
  // The toggle bit of the received PID: 0 for DATA0, 1 for DATA1.
  input  wire            rx_pid_odd,
  // Payload length in bytes. ZERO IS LEGAL -- a zero-length packet is
  // a real packet that terminates a short transfer.
  input  wire [LENW-1:0] rx_len,
  // One cycle: the packet in progress is bad (CRC error, babble,
  // timeout). Nothing is stored, nothing advances, and because the
  // device sends no handshake the host will retry with the SAME PID.
  input  wire            rx_abort,

  // No buffer is free, so the engine must NAK the next OUT token.
  output wire            hw_nak,
  // Which of the two buffers the next accepted packet lands in.
  output wire            hw_buf_sel,
  // One cycle: the packet just committed was a retransmission of one
  // already accepted. It is ACKed on the wire and discarded here.
  output wire            hw_dup,

  // ---- firmware (peripheral bus) side ----------------------------
  input  wire            fw_we,
  input  wire [1:0]      fw_addr,
  input  wire [15:0]     fw_wdata,
  output wire [15:0]     fw_rdata,

  // Level-sensitive interrupt request to the CPU. NOT the USB
  // interrupt TRANSFER type -- an unrelated use of the word.
  output wire            irq,

  // ---- observation counters (not part of the contract) -----------
  output wire [15:0]     n_accept,
  output wire [15:0]     n_dup,
  output wire [15:0]     n_abort,
  output wire [15:0]     n_overrun,
  output wire [15:0]     n_badack
);

  // Register map.
  localparam [1:0] A_CTRL = 2'd0,  // RW ep_en; write-1-to-TOGGLE dtog
                   A_STAT = 2'd1,  // read-only ownership + sticky errors
                   A_LEN  = 2'd2,  // read-only length of the buffer at fw_ptr
                   A_ACK  = 2'd3;  // write-1 releases the buffer at fw_ptr

  // ---- authoritative state ---------------------------------------
  // full[i] == 1 means FIRMWARE owns buffer i. full[i] == 0 means the
  // hardware owns it. There is exactly one ownership bit per buffer and
  // no second copy of it anywhere.
  reg  [1:0]      full;
  reg  [LENW-1:0] len0, len1;
  reg             hw_ptr, fw_ptr;
  reg             dtog;        // the data toggle this endpoint EXPECTS next
  reg             ep_en;
  reg             sticky_ovr, sticky_bad;
  reg  [15:0]     c_acc, c_dup, c_abt, c_ovr, c_bad;

  // ---- decode the firmware access --------------------------------
  wire fw_ctrl = fw_we & (fw_addr == A_CTRL);
  wire fw_ack  = fw_we & (fw_addr == A_ACK) & fw_wdata[0];

  // Write-1-to-TOGGLE, not read-modify-write. See the chapter: hardware
  // writes this same bit, so firmware must be able to express "flip it"
  // rather than "make it this value".
  wire fw_dtog = fw_ctrl & fw_wdata[1];

  // ---- classify the committed packet -----------------------------
  wire both_full = full[0] & full[1];

  // A commit is only looked at while the endpoint is enabled.
  wire cmt       = rx_commit & ep_en;
  // The PID matched what we expect, so this is new data.
  wire cmt_new   = cmt & (rx_pid_odd == dtog);
  // The PID did not match: the host missed our handshake and resent.
  // ACK it on the wire, store nothing, do not move the toggle.
  wire cmt_dup   = cmt & (rx_pid_odd != dtog);

  // New data with somewhere to put it.
  wire accept    = cmt_new & ~both_full;
  // New data with nowhere to put it. hw_nak should have stopped this,
  // so reaching it means the engine broke the contract.
  wire overrun   = cmt_new &  both_full;

  // A release of a buffer the firmware does not own is illegal: it is
  // a no-op, and in particular fw_ptr MUST NOT move.
  wire ack_ok    = fw_ack &  full[fw_ptr];
  wire ack_bad   = fw_ack & ~full[fw_ptr];

  // Set and clear masks for the ownership bits. They are applied in ONE
  // expression so that a set and a clear in the same cycle both land.
  wire [1:0] set_mask = accept ? (hw_ptr ? 2'b10 : 2'b01) : 2'b00;
  wire [1:0] clr_mask = ack_ok ? (fw_ptr ? 2'b10 : 2'b01) : 2'b00;

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      full       <= 2'b00;
      len0       <= {LENW{1'b0}};
      len1       <= {LENW{1'b0}};
      hw_ptr     <= 1'b0;
      fw_ptr     <= 1'b0;
      dtog       <= 1'b0;
      ep_en      <= 1'b0;
      sticky_ovr <= 1'b0;
      sticky_bad <= 1'b0;
      c_acc      <= 16'd0;
      c_dup      <= 16'd0;
      c_abt      <= 16'd0;
      c_ovr      <= 16'd0;
      c_bad      <= 16'd0;
    end else if (usb_reset) begin
      // A USB bus reset returns the endpoint to its default data state.
      // The buffers are handed back to the hardware, the pointers go to
      // zero, and THE DATA TOGGLE GOES TO DATA0. ep_en survives: the
      // endpoint still exists in the descriptor, and the counters
      // survive because they are diagnostics, not state.
      full       <= 2'b00;
      len0       <= {LENW{1'b0}};
      len1       <= {LENW{1'b0}};
      hw_ptr     <= 1'b0;
      fw_ptr     <= 1'b0;
      dtog       <= 1'b0;
      sticky_ovr <= 1'b0;
      sticky_bad <= 1'b0;
    end else begin
      // One expression, so a hardware set and a firmware release in the
      // same cycle compose instead of one overwriting the other.
      full <= (full | set_mask) & ~clr_mask;

      if (accept) begin
        if (hw_ptr) len1 <= rx_len;
        else        len0 <= rx_len;
        hw_ptr <= ~hw_ptr;
        c_acc  <= c_acc + 16'd1;
      end

      if (ack_ok)  fw_ptr <= ~fw_ptr;

      // The toggle is a DELTA from two sources. Hardware flips it on an
      // accepted packet; firmware flips it by writing 1. Both in the
      // same cycle means two flips, which is no change -- and that is
      // the correct answer, not a race.
      dtog <= dtog ^ accept ^ fw_dtog;

      if (fw_ctrl)              ep_en <= fw_wdata[0];
      if (overrun)              sticky_ovr <= 1'b1;
      if (ack_bad)              sticky_bad <= 1'b1;

      if (cmt_dup)              c_dup <= c_dup + 16'd1;
      if (rx_abort)             c_abt <= c_abt + 16'd1;
      if (overrun)              c_ovr <= c_ovr + 16'd1;
      if (ack_bad)              c_bad <= c_bad + 16'd1;
    end
  end

  // ---- derived outputs -------------------------------------------
  // A disabled endpoint NAKs everything: it has no buffers to offer.
  assign hw_nak     = ~ep_en | both_full;
  assign hw_buf_sel = hw_ptr;
  assign hw_dup     = cmt_dup;
  assign irq        = ep_en & (full[0] | full[1]);

  wire [LENW-1:0] len_cur = fw_ptr ? len1 : len0;

  assign fw_rdata =
      (fw_addr == A_CTRL) ? {14'd0, dtog, ep_en}                                :
      (fw_addr == A_STAT) ? {10'd0, sticky_bad, sticky_ovr, hw_ptr, fw_ptr, full} :
      (fw_addr == A_LEN ) ? {{(16-LENW){1'b0}}, len_cur}                        :
                            16'd0;

  assign n_accept  = c_acc;
  assign n_dup     = c_dup;
  assign n_abort   = c_abt;
  assign n_overrun = c_ovr;
  assign n_badack  = c_bad;

endmodule

6. What The Machine Actually Does

Three traces, each answering one question. In all three, a value shown in a cycle is sampled by the rising edge at the end of that cycle, and the registered result appears in the cycle after it. That convention is stated because every ambiguity in a waveform of a synchronous design is an ambiguity about which edge did the work.

Why two buffers, in one picture

Three packets arriving faster than firmware services them

A waveform of three packets arriving at a double-buffered USB OUT endpoint faster than firmware drains them. The first packet of eight bytes is committed at cycle one and buffer zero becomes owned by firmware at cycle two, raising the interrupt. A second packet of twelve bytes is committed at cycle three and buffer one becomes owned at cycle four, at which point both ownership bits are set and the NAK output asserts. Firmware releases one buffer at cycle five, the NAK clears at cycle six, and a third packet of twenty bytes is accepted immediately. Firmware releases the remaining buffers at cycles nine and eleven, reading the lengths eight, twelve and twenty in the order they arrived.fillingfillingNAKNAKoverlappedoverlappeddraineddrainedboth busy: NAK the hostboth busy: NAK the hostone released, room againone released, room again8, 12, 20 -- in order8, 12, 20 -- in orderclkrx_commitrx_len--812--20--------ACK writefull[1:0]000001111011110100hw_buf_selhw_nakirqLEN00881212122012t0t1t2t3t4t5t6t7t8
The argument for double buffering, in the region where it matters. Two packets arrive back to back; at cycle 3 both buffers are owned by firmware and the engine is told to NAK — with a single buffer that would have happened a packet earlier. Firmware releases at 3, 6 and 7 and reads 8, 12 and 20, the order they arrived. The last cell repays a second look: with nothing owned, LEN holds a residue.

Two things in that trace are worth naming.

The first is cycle 6. A packet is accepted on the cycle immediately after the release that made room for it — the engine saw hw_nak low and committed with no gap. That is the double buffer working: firmware's lateness cost the host one NAKed token, not a re-enumeration.

The second is hw_buf_sel. It alternates, and it is not a decoration: it is what the packet-writing side of a real controller uses to pick a base address in the endpoint RAM. If it advanced on an aborted packet it would step past a buffer the ownership bits say is free, and the two would be out of step from then on. Mutation M5.

The collision the two sides cannot avoid

A packet arriving on the same cycle firmware releases a buffer

A waveform showing a packet commit and a firmware buffer release occurring on the same clock cycle. Before the collision, buffer zero is owned by firmware holding a five byte packet, and the hardware pointer is at buffer one. On the collision cycle a nine byte packet is committed and the acknowledge register is written. On the next cycle the ownership mask has changed from binary zero one to binary one zero: buffer one is now owned by firmware and buffer zero has been returned to the hardware. Firmware releases the remaining buffer two cycles later, after which nothing is owned and the length register holds a stale value from the earlier packet.buffer 0 ownedbuffer 0ownedcollisioncollisionbuffer 1 ownedbuffer 1 ownedemptyemptya commit and a release, one cyclea commit and a release, onecycleboth landed: 01 became 10both landed: 01 became 10nothing owned: LEN is stale, not wrongnothing owned: LEN isstale, not wrongclkrx_commitrx_len--9----------ACK writefull[1:0]01011010100000hw_buf_selirqLEN5599955t0t1t2t3t4t5t6
Both events land. The ownership mask goes from 01 to 10 in one edge — bit 1 set by the hardware, bit 0 cleared by firmware — because the update is written as a single expression rather than two competing branches. The last two cycles show the one quantity in this design that is not defined: with nothing owned, LEN holds a stale residue, and that is a fact about the contract rather than a bug.

The toggle, a retransmission, and a bus reset

This is the trace that explains the failure at the top of the chapter. Every packet in it is a DATA0 packet — the host never changes PID — and the device accepts the first, discards the second and accepts the third.

Three identical DATA0 packets, and why the middle one is discarded

A waveform of three identical DATA0 packets arriving at a USB endpoint. The first is accepted at cycle one and the expected data toggle advances to one. The second, still DATA0, does not match the expected toggle, so the duplicate output pulses at cycle three, the duplicate counter increments, and no buffer is taken. A USB bus reset at cycle five returns the expected toggle to zero and hands both buffers back to the hardware. The third packet, again DATA0, now matches and is accepted, taking the accept counter to two.first packetfirst packetthe resendthe resendbus resetbus resetback in stepback in stepaccepted: now expecting DATA1accepted: now expectingDATA1DATA0 again: a resend, ACK and dropDATA0 again: a resend, ACKand dropbus reset: expectation back to DATA0bus reset: expectation backto DATA0the same packet is new data againthe same packet is new dataagainclkrx_commitrx_pid_oddrx_len--4--4------6--usb_resetdtoghw_dupirqn_accept001111112t0t1t2t3t4t5t6t7t8
The device expects DATA0, accepts, and moves its expectation to DATA1. The host's next packet is DATA0 again, which can only mean the host never saw the handshake: it is a retransmission, so it is acknowledged on the wire and thrown away here. The bus reset at cycle 5 returns the expectation to DATA0, and the identical packet at cycle 7 is new data again. A bus reset that skipped the toggle would leave the device stuck discarding every packet the host sent.

7. The Testbench (Verilog)

Five phases, and the first four have to pass on their own. The reference model is a cycle-accurate mirror of the contract, stepped from the input pins at each edge and from its own prior state — it never reads a DUT output, so it can disagree.

One decision about the checker is worth stating before the code, because it changed what the bench is able to catch. Nothing is checked by hierarchical reference. Every comparison goes through the ports, which for the firmware side means every comparison goes through the four-register map. The register map is therefore under test rather than assumed, and a design whose internal state is right but whose STAT bit order is wrong fails.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    PHASE 1  STATE SWEEP   12 reachable ownership states x 11 events,
                           exhaustively, each state CONSTRUCTED and then
                           PROVED before the event is applied
    PHASE 2  BOUNDARY      11 named scenarios, one per edge worth naming
    PHASE 3  ORDER         96 engine offers against a firmware model that is
                           deliberately, periodically late
    PHASE 4  PARITY        the same event with the buffers relabelled: 66
                           paired runs
    PHASE 5  RANDOM        supplementary, and audited for what it reaches
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  tb_usbep_pingpong -- Verilog-2005 testbench for usbep_pingpong.
//
//  Everything is checked against an INDEPENDENT cycle-accurate reference
//  model held in the ref_* variables. The model is advanced from the
//  inputs presented at each clock edge and from its own prior state. It
//  never reads a DUT output, so it is capable of disagreeing.
//
//  Nothing is checked by hierarchical reference either. Every comparison
//  goes through the ports -- which means the four-register firmware map
//  is itself under test, not just the internal state.
//
//  PHASES
//    1 STATE SWEEP  exhaustive: 12 reachable ownership states x 11 events
//    2 BOUNDARY     the named edges, one scenario each
//    3 ORDER        packets must come out in the order they went in
//    4 PARITY       relabelling the two buffers changes nothing but labels
//    5 RANDOM       supplementary; phases 1-4 must pass without it
// =====================================================================
`timescale 1ns/1ps

module tb_usbep_pingpong;

  localparam integer MAXPKT = 64;
  localparam integer LENW   = 7;

  reg              clk = 1'b0;
  reg              rst_n;
  reg              usb_reset;
  reg              rx_commit, rx_pid_odd, rx_abort;
  reg  [LENW-1:0]  rx_len;
  reg              fw_we;
  reg  [1:0]       fw_addr;
  reg  [15:0]      fw_wdata;

  wire             hw_nak, hw_buf_sel, hw_dup, irq;
  wire [15:0]      fw_rdata;
  wire [15:0]      n_accept, n_dup, n_abort, n_overrun, n_badack;

  usbep_pingpong #(.MAXPKT(MAXPKT), .LENW(LENW)) dut (
    .clk(clk), .rst_n(rst_n), .usb_reset(usb_reset),
    .rx_commit(rx_commit), .rx_pid_odd(rx_pid_odd),
    .rx_len(rx_len), .rx_abort(rx_abort),
    .hw_nak(hw_nak), .hw_buf_sel(hw_buf_sel), .hw_dup(hw_dup),
    .fw_we(fw_we), .fw_addr(fw_addr), .fw_wdata(fw_wdata),
    .fw_rdata(fw_rdata), .irq(irq),
    .n_accept(n_accept), .n_dup(n_dup), .n_abort(n_abort),
    .n_overrun(n_overrun), .n_badack(n_badack)
  );

  always #5 clk = ~clk;

  // ---- the independent reference model ---------------------------
  reg  [1:0]      r_full;
  reg  [LENW-1:0] r_len0, r_len1;
  reg             r_hw, r_fw, r_dtog, r_en, r_ovr, r_bad;
  reg  [15:0]     r_acc, r_dup, r_abt, r_ovc, r_bdc;
  // what the model says hw_dup should be DURING the present cycle
  reg             r_hwdup;

  // ---- bookkeeping ------------------------------------------------
  integer chk_dir, chk_rnd, err;
  integer in_random;
  // measured reachability, not assumed
  integer m_accept, m_dup, m_abort, m_ovr, m_bad, m_bothfull, m_nak,
          m_zlp, m_maxpkt, m_dtograce, m_setclr, m_setupfail;

  integer i, j, k, t, e, o, p, g;

  task bump;                       // one comparison happened
    begin
      if (in_random) chk_rnd = chk_rnd + 1;
      else           chk_dir = chk_dir + 1;
    end
  endtask

  task ck;                         // compare one 32-bit quantity
    input [255:0] what;
    input [31:0]  got;
    input [31:0]  exp;
    begin
      bump;
      if (got !== exp) begin
        err = err + 1;
        if (err <= 60)
          $display("  ** %0s: got %0d expected %0d  (t=%0t)", what, got, exp, $time);
      end
    end
  endtask

  // Advance the reference model using the input pins as they stand at
  // this clock edge. Called immediately after @(posedge clk).
  task ref_step;
    reg both, ctl, dtg, cmt, cnew, cdup, acc, ovr, aok, abad;
    reg hw_old, fw_old;
    begin
      hw_old = r_hw;
      fw_old = r_fw;
      if (!rst_n) begin
        r_full = 2'b00; r_len0 = 0; r_len1 = 0;
        r_hw = 0; r_fw = 0; r_dtog = 0; r_en = 0;
        r_ovr = 0; r_bad = 0;
        r_acc = 0; r_dup = 0; r_abt = 0; r_ovc = 0; r_bdc = 0;
      end else if (usb_reset) begin
        r_full = 2'b00; r_len0 = 0; r_len1 = 0;
        r_hw = 0; r_fw = 0; r_dtog = 0;
        r_ovr = 0; r_bad = 0;
        // ep_en and the counters deliberately survive
      end else begin
        both = r_full[0] & r_full[1];
        ctl  = fw_we && (fw_addr == 2'd0);
        dtg  = ctl && fw_wdata[1];
        cmt  = rx_commit && r_en;
        cnew = cmt && (rx_pid_odd == r_dtog);
        cdup = cmt && (rx_pid_odd != r_dtog);
        acc  = cnew && !both;
        ovr  = cnew &&  both;
        aok  = fw_we && (fw_addr == 2'd3) && fw_wdata[0] &&  r_full[fw_old];
        abad = fw_we && (fw_addr == 2'd3) && fw_wdata[0] && !r_full[fw_old];

        if (acc) begin
          r_full[hw_old] = 1'b1;
          if (hw_old) r_len1 = rx_len; else r_len0 = rx_len;
          r_hw  = ~hw_old;
          r_acc = r_acc + 1;
        end
        if (aok) begin
          r_full[fw_old] = 1'b0;
          r_fw = ~fw_old;
        end
        r_dtog = r_dtog ^ acc ^ dtg;
        if (ctl)  r_en  = fw_wdata[0];
        if (ovr)  begin r_ovr = 1'b1; r_ovc = r_ovc + 1; end
        if (abad) begin r_bad = 1'b1; r_bdc = r_bdc + 1; end
        if (cdup)     r_dup = r_dup + 1;
        if (rx_abort) r_abt = r_abt + 1;

        // measured event tallies
        if (acc)  m_accept = m_accept + 1;
        if (cdup) m_dup    = m_dup    + 1;
        if (ovr)  m_ovr    = m_ovr    + 1;
        if (abad) m_bad    = m_bad    + 1;
        if (rx_abort) m_abort = m_abort + 1;
        if (acc && rx_len == 0)      m_zlp    = m_zlp    + 1;
        if (acc && rx_len == MAXPKT) m_maxpkt = m_maxpkt + 1;
        if (acc && dtg)              m_dtograce = m_dtograce + 1;
        if (acc && aok)              m_setclr   = m_setclr   + 1;
      end
    end
  endtask

  // What hw_dup should be right now, from the reference state and pins.
  function ref_dup;
    input dummy;
    begin
      ref_dup = rx_commit && r_en && (rx_pid_odd !== r_dtog);
    end
  endfunction

  // Compare the DUT against the model through the ports only. Sweeps
  // fw_addr with fw_we low, which has no side effect.
  task cmp;
    reg [15:0] exp;
    begin
      if (r_full[0] & r_full[1]) m_bothfull = m_bothfull + 1;
      if (hw_nak)                m_nak      = m_nak      + 1;

      fw_addr = 2'd0; #1;
      ck("CTRL", {16'd0, fw_rdata}, {16'd0, 14'd0, r_dtog, r_en});
      fw_addr = 2'd1; #1;
      exp = {10'd0, r_bad, r_ovr, r_hw, r_fw, r_full};
      ck("STAT", {16'd0, fw_rdata}, {16'd0, exp});
      fw_addr = 2'd2; #1;
      exp = {{(16-LENW){1'b0}}, (r_fw ? r_len1 : r_len0)};
      ck("LEN",  {16'd0, fw_rdata}, {16'd0, exp});
      fw_addr = 2'd3; #1;
      ck("ACKRD", {16'd0, fw_rdata}, 32'd0);

      ck("hw_nak",     {31'd0, hw_nak},     {31'd0, (~r_en | (r_full[0] & r_full[1]))});
      ck("hw_buf_sel", {31'd0, hw_buf_sel}, {31'd0, r_hw});
      ck("irq",        {31'd0, irq},        {31'd0, (r_en & (r_full[0] | r_full[1]))});
      ck("n_accept",   {16'd0, n_accept},   {16'd0, r_acc});
      ck("n_dup",      {16'd0, n_dup},      {16'd0, r_dup});
      ck("n_abort",    {16'd0, n_abort},    {16'd0, r_abt});
      ck("n_overrun",  {16'd0, n_overrun},  {16'd0, r_ovc});
      ck("n_badack",   {16'd0, n_badack},   {16'd0, r_bdc});
      fw_addr = 2'd0;
    end
  endtask

  // One cycle. Inputs must already be driven. Checks hw_dup before the
  // edge, advances the model at the edge, compares after it, then
  // releases the one-cycle inputs.
  task step;
    begin
      #1;
      ck("hw_dup", {31'd0, hw_dup}, {31'd0, ref_dup(1'b0)});
      @(posedge clk);
      ref_step;
      #1;
      cmp;
      rx_commit = 0; rx_abort = 0; fw_we = 0; usb_reset = 0;
      rx_len = 0; fw_wdata = 0;
    end
  endtask

  task idle;  begin step; end endtask

  task wr;                                 // a firmware register write
    input [1:0]  a;
    input [15:0] d;
    begin fw_we = 1; fw_addr = a; fw_wdata = d; step; end
  endtask

  task do_ack;   begin wr(2'd3, 16'h0001); end endtask
  task do_dtog;  begin wr(2'd0, {14'd0, 1'b1, r_en}); end endtask
  task set_en;   input v; begin wr(2'd0, {15'd0, v}); end endtask

  task commit;                             // an accepted or duplicate packet
    input match;
    input [LENW-1:0] len;
    begin
      rx_commit  = 1;
      rx_pid_odd = match ? r_dtog : ~r_dtog;
      rx_len     = len;
      step;
    end
  endtask

  task abort_pkt; begin rx_abort = 1; step; end endtask

  task hard_reset;
    begin
      rst_n = 0; usb_reset = 0;
      rx_commit = 0; rx_pid_odd = 0; rx_len = 0; rx_abort = 0;
      fw_we = 0; fw_addr = 0; fw_wdata = 0;
      repeat (3) begin @(posedge clk); ref_step; end
      #1; rst_n = 1;
      @(posedge clk); ref_step; #1; cmp;
    end
  endtask

  task bus_reset; begin usb_reset = 1; step; end endtask

  // -----------------------------------------------------------------
  //  Build one of the 12 reachable (occupancy, base pointer, toggle)
  //  states, then PROVE it was built. A setup that silently fails must
  //  not turn into a passing test of the wrong state.
  // -----------------------------------------------------------------
  task setup_state;
    input integer occ;      // 0, 1 or 2 buffers owned by firmware
    input integer bp;       // base pointer: where fw_ptr sits
    input integer tog;      // the expected data toggle
    reg [15:0] stat, ctrl;
    integer want_full, want_hw;
    begin
      hard_reset;
      set_en(1'b1);
      bus_reset;                        // a clean, known data state
      if (bp == 1) begin                // rotate both pointers by one
        commit(1'b1, 7'd3); do_ack;
      end
      if (occ >= 1) commit(1'b1, 7'd5);
      if (occ >= 2) commit(1'b1, 7'd9);
      if (r_dtog !== tog[0]) do_dtog;

      // --- prove it ---
      want_full = (occ == 0) ? 0 :
                  (occ == 1) ? (bp ? 2 : 1) : 3;
      want_hw   = (occ == 1) ? (bp ? 0 : 1) : bp;
      fw_addr = 2'd1; #1; stat = fw_rdata;
      fw_addr = 2'd0; #1; ctrl = fw_rdata;
      fw_addr = 2'd0;
      bump; if (stat[1:0] !== want_full[1:0]) begin
        err = err + 1; m_setupfail = m_setupfail + 1;
        $display("  ** setup full: occ=%0d bp=%0d got %b want %b", occ, bp, stat[1:0], want_full[1:0]);
      end
      bump; if (stat[2] !== bp[0]) begin
        err = err + 1; m_setupfail = m_setupfail + 1;
        $display("  ** setup fw_ptr: occ=%0d bp=%0d got %b", occ, bp, stat[2]);
      end
      bump; if (stat[3] !== want_hw[0]) begin
        err = err + 1; m_setupfail = m_setupfail + 1;
        $display("  ** setup hw_ptr: occ=%0d bp=%0d got %b want %b", occ, bp, stat[3], want_hw[0]);
      end
      bump; if (ctrl[1] !== tog[0]) begin
        err = err + 1; m_setupfail = m_setupfail + 1;
        $display("  ** setup dtog: occ=%0d bp=%0d tog=%0d got %b", occ, bp, tog, ctrl[1]);
      end
    end
  endtask

  // Apply event number `ev`. One cycle, except where the event is
  // deliberately two things at once.
  task apply_event;
    input integer ev;
    begin
      case (ev)
        0:  idle;
        1:  do_ack;
        2:  abort_pkt;
        3:  commit(1'b1, 7'd0);                       // ZLP
        4:  commit(1'b1, 7'd1);
        5:  commit(1'b1, MAXPKT[LENW-1:0]);           // the width boundary
        6:  commit(1'b0, 7'd13);                      // retransmission
        7:  begin rx_commit=1; rx_pid_odd=r_dtog;  rx_len=7'd7;
                  fw_we=1; fw_addr=2'd3; fw_wdata=16'h0001; step; end
        8:  begin rx_commit=1; rx_pid_odd=~r_dtog; rx_len=7'd13;
                  fw_we=1; fw_addr=2'd3; fw_wdata=16'h0001; step; end
        9:  begin rx_abort=1; fw_we=1; fw_addr=2'd3; fw_wdata=16'h0001; step; end
        10: bus_reset;
        default: idle;
      endcase
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 3: order preservation
  // -----------------------------------------------------------------
  integer q_len [0:255];       // what the engine handed in
  integer q_wr, q_rd;
  integer svc_pat [0:7];
  integer ord_pkts, ord_naked, ord_read, ord_bothfull;
  reg [15:0] rl;

  task phase_order;
    integer n, c, want, drained;
    begin
      // How many packets firmware drains per engine offer. The AVERAGE is
      // exactly one, so the endpoint neither starves nor runs away -- but
      // the RUNS OF ZERO are the point. Without them the occupancy never
      // exceeds one buffer, the second buffer is never used, and the whole
      // mechanism under test is never entered.
      svc_pat[0]=0; svc_pat[1]=0; svc_pat[2]=2; svc_pat[3]=1;
      svc_pat[4]=0; svc_pat[5]=2; svc_pat[6]=1; svc_pat[7]=2;
      hard_reset; set_en(1'b1); bus_reset;
      q_wr = 0; q_rd = 0; ord_pkts = 0; ord_naked = 0; ord_read = 0;
      ord_bothfull = 0;
      for (n = 0; n < 96; n = n + 1) begin
        // the engine offers a packet every iteration, and honours hw_nak
        if (hw_nak) begin
          ord_naked = ord_naked + 1;
          idle;
        end else begin
          q_len[q_wr] = (n * 7) % (MAXPKT + 1);
          q_wr = q_wr + 1;
          ord_pkts = ord_pkts + 1;
          commit(1'b1, ((n * 7) % (MAXPKT + 1)));
        end
        if (hw_nak) ord_bothfull = ord_bothfull + 1;
        // firmware drains svc_pat[] packets this iteration -- sometimes
        // none, which is what backs the endpoint up
        for (c = 0; c < svc_pat[n % 8]; c = c + 1) begin
          if (irq && q_rd < q_wr) begin
            fw_addr = 2'd2; #1; rl = fw_rdata; fw_addr = 2'd0;
            want = q_len[q_rd]; q_rd = q_rd + 1; ord_read = ord_read + 1;
            ck("order", {16'd0, rl}, want);
            do_ack;
          end else if (irq) begin
            // more releases than commits: the pointers have desynchronised
            bump; err = err + 1; do_ack;
          end else idle;
        end
      end
      // drain what is left
      drained = 0;
      while (irq && drained < 8 && q_rd < q_wr) begin
        fw_addr = 2'd2; #1; rl = fw_rdata; fw_addr = 2'd0;
        want = q_len[q_rd]; q_rd = q_rd + 1; ord_read = ord_read + 1;
        ck("order-drain", {16'd0, rl}, want);
        do_ack;
        drained = drained + 1;
      end
      bump; if (ord_read !== ord_pkts) begin
        err = err + 1;
        $display("  ** order: committed %0d, read back %0d", ord_pkts, ord_read);
      end
      // The scenario must have HAPPENED. A phase that backs the endpoint
      // up zero times has tested the ordering of a one-deep queue.
      bump; if (ord_naked < 4) begin
        err = err + 1;
        $display("  ** order: backpressure never reached -- %0d NAKed offers", ord_naked);
      end
      bump; if (ord_bothfull < 8) begin
        err = err + 1;
        $display("  ** order: both buffers full only %0d times", ord_bothfull);
      end
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 4: buffer-parity relabelling
  //
  //  Run the same event on the same occupancy with the base pointer at
  //  0 and at 1. Everything the firmware can observe must be identical
  //  except the three things that ARE buffer labels -- the ownership
  //  mask and the two pointers -- which must be exchanged.
  // -----------------------------------------------------------------
  reg [15:0] par_stat [0:1];
  reg [15:0] par_ctrl [0:1];
  reg [15:0] par_len  [0:1];
  reg [2:0]  par_out  [0:1];
  integer par_pairs, par_len_skipped, par_reset_cases;

  task phase_parity;
    integer occ_i, ev_i, tg_i, side;
    begin
      par_pairs = 0; par_len_skipped = 0; par_reset_cases = 0;
      for (occ_i = 0; occ_i <= 2; occ_i = occ_i + 1)
      for (tg_i  = 0; tg_i  <= 1; tg_i  = tg_i  + 1)
      for (ev_i  = 0; ev_i  <= 10; ev_i = ev_i + 1) begin
        for (side = 0; side <= 1; side = side + 1) begin
          setup_state(occ_i, side, tg_i);
          apply_event(ev_i);
          fw_addr = 2'd1; #1; par_stat[side] = fw_rdata;
          fw_addr = 2'd0; #1; par_ctrl[side] = fw_rdata;
          fw_addr = 2'd2; #1; par_len[side]  = fw_rdata;
          fw_addr = 2'd0;
          par_out[side] = {hw_nak, irq, hw_buf_sel};
        end
        par_pairs = par_pairs + 1;
        // ---- the label-free observations must match exactly ----
        ck("par-ctrl", {16'd0, par_ctrl[0]}, {16'd0, par_ctrl[1]});
        ck("par-nak",  {31'd0, par_out[0][2]}, {31'd0, par_out[1][2]});
        ck("par-irq",  {31'd0, par_out[0][1]}, {31'd0, par_out[1][1]});
        ck("par-sticky", {16'd0, par_stat[0][5:4]}, {16'd0, par_stat[1][5:4]});
        // Occupancy is label-free, so the two sides must agree on whether
        // firmware owns anything at all -- and LEN is only DEFINED when it
        // does. Comparing it when nothing is owned compares two different
        // stale residues and reports a difference that is not one.
        ck("par-own", {31'd0, par_out[0][1]}, {31'd0, par_out[1][1]});
        if (par_out[0][1] && par_out[1][1])
          ck("par-len", {16'd0, par_len[0]}, {16'd0, par_len[1]});
        else
          par_len_skipped = par_len_skipped + 1;
        // ---- and the labels themselves must be the other way round ----
        ck("par-full-swap", {30'd0, par_stat[0][1:0]},
                            {30'd0, par_stat[1][0], par_stat[1][1]});
        if (ev_i != 10) begin
          ck("par-fwptr-inv", {31'd0, par_stat[0][2]}, {31'd0, ~par_stat[1][2]});
          ck("par-hwptr-inv", {31'd0, par_stat[0][3]}, {31'd0, ~par_stat[1][3]});
          ck("par-bufsel-inv",{31'd0, par_out[0][0]},  {31'd0, ~par_out[1][0]});
        end else begin
          // THE ONE EXCEPTION, and it is required rather than tolerated: a
          // USB bus reset is the only operation that names an absolute
          // buffer, because after it the host and the device have to agree
          // on which buffer is next and zero is the only value both can
          // assume. So the relabelling collapses instead of inverting.
          par_reset_cases = par_reset_cases + 1;
          ck("par-rst-fwptr",  {16'd0, par_stat[0][2], par_stat[1][2]}, 32'd0);
          ck("par-rst-hwptr",  {16'd0, par_stat[0][3], par_stat[1][3]}, 32'd0);
          ck("par-rst-bufsel", {16'd0, par_out[0][0],  par_out[1][0]},  32'd0);
        end
      end
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 2: the named boundaries
  // -----------------------------------------------------------------
  reg [15:0] s1, s2;

  task phase_boundary;
    begin
      // B1 an accepted packet and a firmware toggle-write in the SAME
      //    cycle: two flips, so the toggle must not move.
      hard_reset; set_en(1'b1); bus_reset;
      rx_commit=1; rx_pid_odd=r_dtog; rx_len=7'd4;
      fw_we=1; fw_addr=2'd0; fw_wdata={14'd0, 1'b1, 1'b1};
      step;
      fw_addr=2'd0; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B1 dtog unchanged", {31'd0, s1[1]}, 32'd0);
      ck("B1 packet still taken", {16'd0, n_accept}, 32'd1);

      // B2 MAXPKT reads back intact: a length register one bit too
      //    narrow turns 64 into 0.
      hard_reset; set_en(1'b1); bus_reset;
      commit(1'b1, MAXPKT[LENW-1:0]);
      fw_addr=2'd2; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B2 MAXPKT length", {16'd0, s1}, MAXPKT);

      // B3 a zero-length packet is a PACKET: it takes a buffer and
      //    raises the interrupt.
      hard_reset; set_en(1'b1); bus_reset;
      commit(1'b1, 7'd0);
      ck("B3 zlp irq",    {31'd0, irq}, 32'd1);
      ck("B3 zlp accept", {16'd0, n_accept}, 32'd1);
      fw_addr=2'd2; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B3 zlp length", {16'd0, s1}, 32'd0);

      // B4 an overrun must DROP the new packet, not overwrite a buffer
      //    firmware still owns.
      hard_reset; set_en(1'b1); bus_reset;
      commit(1'b1, 7'd11); commit(1'b1, 7'd22);
      ck("B4 nak asserted", {31'd0, hw_nak}, 32'd1);
      rx_commit=1; rx_pid_odd=r_dtog; rx_len=7'd33; step;   // engine misbehaves
      ck("B4 overrun counted", {16'd0, n_overrun}, 32'd1);
      ck("B4 accept unchanged", {16'd0, n_accept}, 32'd2);
      fw_addr=2'd2; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B4 first length intact", {16'd0, s1}, 32'd11);
      do_ack;
      fw_addr=2'd2; #1; s2 = fw_rdata; fw_addr=2'd0;
      ck("B4 second length intact", {16'd0, s2}, 32'd22);

      // B5 releasing a buffer firmware does not own must not move the
      //    read pointer. If it does, the two pointers desynchronise and
      //    the endpoint never recovers.
      hard_reset; set_en(1'b1); bus_reset;
      do_ack;
      ck("B5 badack counted", {16'd0, n_badack}, 32'd1);
      fw_addr=2'd1; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B5 fw_ptr still 0", {31'd0, s1[2]}, 32'd0);
      commit(1'b1, 7'd17);
      fw_addr=2'd2; #1; s2 = fw_rdata; fw_addr=2'd0;
      ck("B5 packet readable", {16'd0, s2}, 32'd17);

      // B6 an aborted packet advances nothing and flips nothing, so the
      //    host's retry with the SAME PID is still new data.
      hard_reset; set_en(1'b1); bus_reset;
      abort_pkt;
      fw_addr=2'd1; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B6 no buffer taken", {16'd0, s1[1:0]}, 32'd0);
      ck("B6 hw_ptr still 0",  {31'd0, s1[3]},   32'd0);
      fw_addr=2'd0; #1; s2 = fw_rdata; fw_addr=2'd0;
      ck("B6 dtog still 0",    {31'd0, s2[1]},   32'd0);
      commit(1'b1, 7'd8);
      ck("B6 retry accepted",  {16'd0, n_accept}, 32'd1);

      // B7 back-to-back packets on consecutive cycles.
      hard_reset; set_en(1'b1); bus_reset;
      rx_commit=1; rx_pid_odd=r_dtog; rx_len=7'd2;
      #1; @(posedge clk); ref_step; #1;
      rx_commit=1; rx_pid_odd=r_dtog; rx_len=7'd3;
      #1; @(posedge clk); ref_step; #1;
      rx_commit=0; cmp;
      ck("B7 both taken", {16'd0, n_accept}, 32'd2);
      fw_addr=2'd1; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B7 both full",  {16'd0, s1[1:0]}, 32'd3);

      // B8 a USB bus reset clears the data state and the toggle, and
      //    leaves the endpoint ENABLED. Clearing ep_en here is a real
      //    bug: the device enumerates and then goes deaf.
      hard_reset; set_en(1'b1);
      commit(1'b1, 7'd9);                    // dtog is now 1
      fw_addr=2'd0; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B8 dtog is 1 first", {31'd0, s1[1]}, 32'd1);
      bus_reset;
      fw_addr=2'd0; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B8 ep_en survives", {31'd0, s1[0]}, 32'd1);
      ck("B8 dtog cleared",   {31'd0, s1[1]}, 32'd0);
      fw_addr=2'd1; #1; s2 = fw_rdata; fw_addr=2'd0;
      ck("B8 buffers returned", {16'd0, s2[1:0]}, 32'd0);
      ck("B8 pointers zeroed",  {16'd0, s2[3:2]}, 32'd0);
      // and the host's first packet after a reset is DATA0
      rx_commit=1; rx_pid_odd=1'b0; rx_len=7'd6; step;
      ck("B8 DATA0 accepted", {16'd0, n_accept}, 32'd2);

      // B9 disabling the endpoint hides the interrupt and NAKs, but does
      //    not destroy what firmware already owns.
      hard_reset; set_en(1'b1); bus_reset;
      commit(1'b1, 7'd21);
      set_en(1'b0);
      ck("B9 irq gone",   {31'd0, irq},    32'd0);
      ck("B9 nak forced", {31'd0, hw_nak}, 32'd1);
      fw_addr=2'd1; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B9 buffer kept", {16'd0, s1[1:0]}, 32'd1);
      set_en(1'b1);
      ck("B9 irq returns", {31'd0, irq}, 32'd1);
      fw_addr=2'd2; #1; s2 = fw_rdata; fw_addr=2'd0;
      ck("B9 length kept", {16'd0, s2}, 32'd21);

      // B10 a commit arriving while the endpoint is disabled is not a
      //     duplicate and not an overrun -- it is nothing at all.
      hard_reset; bus_reset;
      rx_commit=1; rx_pid_odd=1'b1; rx_len=7'd5; step;
      ck("B10 nothing counted", {16'd0, n_accept + n_dup + n_overrun}, 32'd0);

      // B11 a release and a bus reset in the same cycle: the reset wins
      //     and the buffer is returned to hardware either way.
      hard_reset; set_en(1'b1); bus_reset;
      commit(1'b1, 7'd12);
      usb_reset=1; fw_we=1; fw_addr=2'd3; fw_wdata=16'h0001; step;
      fw_addr=2'd1; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B11 buffers clear",  {16'd0, s1[1:0]}, 32'd0);
      ck("B11 fw_ptr zeroed",  {31'd0, s1[2]},   32'd0);
      ck("B11 no bad ack",     {16'd0, n_badack}, 32'd0);
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 1: the exhaustive state sweep
  // -----------------------------------------------------------------
  task phase_sweep;
    integer occ_i, bp_i, tg_i, ev_i;
    begin
      for (occ_i = 0; occ_i <= 2; occ_i = occ_i + 1)
      for (bp_i  = 0; bp_i  <= 1; bp_i  = bp_i  + 1)
      for (tg_i  = 0; tg_i  <= 1; tg_i  = tg_i  + 1)
      for (ev_i  = 0; ev_i  <= 10; ev_i = ev_i + 1) begin
        setup_state(occ_i, bp_i, tg_i);
        cmp;                       // the state we start from
        apply_event(ev_i);         // cmp happens inside step
        idle;                      // and one cycle later
      end
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 5: random, with the firmware latency deliberately spread
  //  wide enough that both buffers are actually used.
  // -----------------------------------------------------------------
  task phase_random;
    integer n, r, lat, c;
    begin
      in_random = 1;
      hard_reset; set_en(1'b1); bus_reset;
      for (n = 0; n < 4000; n = n + 1) begin
        r = {$random} % 100;
        if (r < 55) begin
          // the engine offers a packet; it honours hw_nak most of the
          // time and breaks the rule occasionally so the overrun path
          // is exercised at all
          if (!hw_nak || (({$random} % 100) < 3)) begin
            rx_commit  = 1;
            rx_pid_odd = (({$random} % 100) < 12) ? ~r_dtog : r_dtog;
            rx_len     = ({$random} % (MAXPKT + 1));
            step;
          end else idle;
        end else if (r < 62) begin
          abort_pkt;
        end else if (r < 78) begin
          // firmware services -- sometimes when there is nothing there
          do_ack;
        end else if (r < 85) begin
          // A packet arriving on the SAME CYCLE as a firmware release.
          // The two sides are independent agents, so this happens in real
          // hardware -- and a random phase that issues one action per
          // cycle can never produce it.
          // and it only EXISTS when firmware owns one buffer and the
          // hardware has the other, so the branch is steered at that
          // window rather than hoping to land in it
          if (irq && !hw_nak) begin
            rx_commit  = 1;
            rx_pid_odd = r_dtog;
            rx_len     = ({$random} % (MAXPKT + 1));
            fw_we = 1; fw_addr = 2'd3; fw_wdata = 16'h0001;
            step;
          end else idle;
        end else if (r < 89) begin
          // a toggle write landing on the SAME CYCLE as an accepted
          // packet. Two flips compose to none, and that is the answer a
          // read-modify-write register cannot give.
          rx_commit  = 1;
          rx_pid_odd = r_dtog;
          rx_len     = ({$random} % (MAXPKT + 1));
          fw_we = 1; fw_addr = 2'd0; fw_wdata = {14'd0, 1'b1, 1'b1};
          step;
        end else if (r < 92) begin
          do_dtog;
        end else if (r < 95) begin
          bus_reset;
        end else if (r < 97) begin
          set_en(({$random} % 100) < 20 ? 1'b0 : 1'b1);
        end else begin
          lat = {$random} % 5;
          for (c = 0; c <= lat; c = c + 1) idle;
        end
      end
      set_en(1'b1);
      in_random = 0;
    end
  endtask

  initial begin
    chk_dir = 0; chk_rnd = 0; err = 0; in_random = 0;
    m_accept=0; m_dup=0; m_abort=0; m_ovr=0; m_bad=0; m_bothfull=0;
    m_nak=0; m_zlp=0; m_maxpkt=0; m_dtograce=0; m_setclr=0; m_setupfail=0;

    phase_sweep;
    $display("  phase 1 state sweep     : %0d checks, %0d errors", chk_dir, err);
    phase_boundary;
    $display("  phase 2 boundary        : %0d checks, %0d errors", chk_dir, err);
    phase_order;
    $display("  phase 3 order           : %0d checks, %0d errors  (%0d packets, %0d NAKed offers, %0d both-full)",
             chk_dir, err, ord_pkts, ord_naked, ord_bothfull);
    phase_parity;
    $display("  phase 4 parity          : %0d checks, %0d errors  (%0d pairs, %0d reset exceptions, %0d LEN undefined)",
             chk_dir, err, par_pairs, par_reset_cases, par_len_skipped);
    $display("  ---- DIRECTED-ONLY TOTAL: %0d checks, %0d errors ----", chk_dir, err);
    phase_random;

    $display("");
    $display("  measured reachability (all phases)");
    $display("    accepted packets ....... %0d", m_accept);
    $display("    of which zero-length ... %0d", m_zlp);
    $display("    of which MAXPKT ........ %0d", m_maxpkt);
    $display("    retransmissions ........ %0d", m_dup);
    $display("    aborted packets ........ %0d", m_abort);
    $display("    overruns ............... %0d", m_ovr);
    $display("    illegal releases ....... %0d", m_bad);
    $display("    accept+toggle-write .... %0d", m_dtograce);
    $display("    accept+release ......... %0d", m_setclr);
    $display("    cycles with both full .. %0d", m_bothfull);
    $display("    cycles asserting NAK ... %0d", m_nak);
    $display("    setup failures ......... %0d", m_setupfail);
    $display("");
    $display("  directed checks .......... %0d", chk_dir);
    $display("  random checks ............ %0d", chk_rnd);
    $display("  TOTAL checks ............. %0d", chk_dir + chk_rnd);
    $display("  ERRORS ................... %0d", err);
    if (err == 0) $display("  PASS");
    else          $display("  FAIL");
    $finish;
  end

endmodule

8. The Measurement

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
                        VERILOG      SYSTEMVERILOG      VHDL-2008
    phase 1 sweep        14,850          14,850          14,850
    phase 2 boundary     15,549          15,549          15,549
    phase 3 order        18,170          18,170          18,170
    phase 4 parity       30,356          30,356          30,356
    ---- DIRECTED        30,356          30,356          30,356
    errors                    0               0               0

    phase 5 random       54,768          55,080          55,197
    TOTAL                85,124          85,436          85,553
    errors                    0               0               0

The cumulative directed figures are identical to the digit in all three languages, phase by phase, which is what "the same directed stimulus" has to mean if it is going to mean anything. The random columns differ because $random, $urandom_range and ieee.math_real.uniform are three different generators.

The exhaustive part, and where its denominator comes from

The sweep claims to cover 12 reachable ownership states. Twelve is a derived number, not a chosen one, and the derivation matters because a wrong denominator turns a coverage claim into a slogan.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    RAW STATE SPACE     full[1:0]  4 values
                        hw_ptr     2
                        fw_ptr     2
                        dtog       2
                        --------------------
                        32 combinations

Most of those cannot happen. The two pointers are not independent of the ownership bits, because each one advances for exactly one reason:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    full == 00   nothing is owned, so the next buffer to be filled and the
                 next to be read are the same one:  hw_ptr == fw_ptr    2 states
    full == 01   firmware owns buffer 0, so it reads buffer 0 next, and the
                 hardware must be pointing at the other one              1 state
    full == 10   the mirror of that                                      1 state
    full == 11   both owned; the next release and the next fill are both
                 the oldest buffer:  hw_ptr == fw_ptr                    2 states
    ------------------------------------------------------------------------
                                                            6 ownership states
                                                          x 2 toggle values
                                                          = 12

The remaining 20 of the 32 are unreachable, and asserting anything about them would be asserting something about a state the design cannot be in.

The eleven events are the complete set of things that can happen to the endpoint in one cycle, including the three deliberate coincidences:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    E0   nothing
    E1   firmware releases
    E2   the engine aborts a packet
    E3   a matching packet, length 0          -- the ZLP boundary
    E4   a matching packet, length 1
    E5   a matching packet, length MAXPKT     -- the width boundary
    E6   a non-matching packet                -- a retransmission
    E7   a matching packet AND a release, same cycle
    E8   a retransmission AND a release, same cycle
    E9   an abort AND a release, same cycle
    E10  a USB bus reset

12 x 11 = 132 transitions, each one entered from a state that was built and verified, and each one compared before the event, on the event, and one cycle after it. The phase's 14,850 checks are much larger than 132 x 13 because every cycle of every construction sequence is also compared — 264 constructions, each of which is itself a small verified scenario.

The relational part, and the one exception it has

Phase 4 runs each of 66 (occupancy, toggle, event) combinations twice: once with the buffers labelled one way round and once the other. The claim is that relabelling the two buffers changes nothing a learner would call behaviour:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    MUST BE IDENTICAL       CTRL, LEN, hw_nak, irq, the sticky flags
    MUST BE EXCHANGED       the two ownership bits
    MUST BE INVERTED        fw_ptr, hw_ptr, hw_buf_sel

That is a statement about two executions, so — as in 29.4 — it cannot be an assertion. It is a testbench structure, and it catches the class of bug where buffer 0 works and buffer 1 does not.

It has exactly one exception, and the exception is required rather than tolerated:

What the random phase actually reached

An iteration count is not a result. These are the outcomes the whole run produced, counted by the bench rather than assumed:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    accepted packets .......... 1,349
      of which zero-length ....    33
      of which MAXPKT .........    29
    retransmissions ...........   147
    aborted packets ...........   335
    overruns (engine ignored
      hw_nak on purpose) ......   165
    illegal releases ..........   141
    accept + toggle-write, one
      cycle ...................    50
    accept + release, one cycle    83
    cycles with both buffers
      owned by firmware ....... 2,882
    cycles asserting hw_nak ... 3,383
    setups that failed to build
      the requested state .....     0

Two of those rows exist because the numbers under them started at zero.

9. SystemVerilog

The same contract, with the types doing work. Two of them earn their place and one of them changes what a mutation can be.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    addr_e      the register map as an enum rather than four localparams, so
                an address the map does not define is a visible mistake
    ep_data_t   a packed struct holding EXACTLY the fields a USB bus reset
                returns to default -- every one of which resets to zero
    always_ff   one sequential block, and a tool that will complain if it
                ever infers latches from it

The struct is the interesting one. Because every field in it resets to zero, the entire bus-reset behaviour is one assignment:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    end else if (usb_reset) begin
      d <= '0;

ep_en is deliberately not a member. It is declared alongside the struct, not inside it, and the reason is the whole argument: a reset list that is one line too long clears firmware's configuration, and here there is no list to get wrong. Mutation M4 — a bus reset that also clears ep_en — cannot be injected into this version by deleting or adding a single line inside the reset branch. It has to be injected by adding ep_en to a branch that otherwise names nothing.

And mutation M3 — a bus reset that forgets the toggle — cannot be injected at all without abandoning the idiom. There is no line to remove. To build that bug in the SystemVerilog version you have to replace the single assignment with seven field assignments and leave one out, which is exactly the shape of code the struct exists to avoid writing.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  usbep_pingpong (SystemVerilog) -- the same hardware contract as the
//  Verilog-2005 module, expressed with the types that make the intent
//  checkable: an enum for the register map, a struct for the ownership
//  state, always_ff for the one sequential block, and explicit widths
//  everywhere a number is captured.
//
//  CLASSIFICATION: simplified synthesisable teaching RTL. Identical
//  behavioural contract to usbep_pingpong.v -- same ports, same reset
//  semantics, same same-cycle priorities, same latency.
// =====================================================================
module usbep_pingpong_sv #(
  parameter int MAXPKT = 64,
  parameter int LENW   = 7
) (
  input  logic            clk,
  input  logic            rst_n,
  input  logic            usb_reset,

  input  logic            rx_commit,
  input  logic            rx_pid_odd,
  input  logic [LENW-1:0] rx_len,
  input  logic            rx_abort,

  output logic            hw_nak,
  output logic            hw_buf_sel,
  output logic            hw_dup,

  input  logic            fw_we,
  input  logic [1:0]      fw_addr,
  input  logic [15:0]     fw_wdata,
  output logic [15:0]     fw_rdata,

  output logic            irq,

  output logic [15:0]     n_accept,
  output logic [15:0]     n_dup,
  output logic [15:0]     n_abort,
  output logic [15:0]     n_overrun,
  output logic [15:0]     n_badack
);

  // The register map as a type rather than four localparams, so a write
  // to an address the map does not define is a visible mistake.
  typedef enum logic [1:0] {
    A_CTRL = 2'd0,   // RW ep_en; write-1-to-TOGGLE the data toggle
    A_STAT = 2'd1,   // read-only
    A_LEN  = 2'd2,   // read-only
    A_ACK  = 2'd3    // write-1 releases the buffer at fw_ptr
  } addr_e;

  // Everything a USB bus reset returns to its default value, gathered in
  // one type so the reset list cannot drift away from the state list.
  typedef struct packed {
    logic [1:0]      full;     // 1 = firmware owns this buffer
    logic [LENW-1:0] len0;
    logic [LENW-1:0] len1;
    logic            hw_ptr;
    logic            fw_ptr;
    logic            dtog;     // the data toggle this endpoint EXPECTS
    logic            ovr;
    logic            bad;
  } ep_data_t;

  // Every field above resets to zero, which is why they are gathered in
  // one struct: the whole reset is one assignment and cannot drift.
  localparam int EP_DATA_W = 2 + LENW + LENW + 5;

  ep_data_t   d;
  logic       ep_en;                 // NOT in ep_data_t: survives bus reset
  logic [15:0] c_acc, c_dup, c_abt, c_ovr, c_bad;

  // ---- decode ----------------------------------------------------
  logic fw_ctrl, fw_ack, fw_dtog;
  logic both_full, cmt, cmt_new, cmt_dup, accept, overrun, ack_ok, ack_bad;
  logic [1:0] set_mask, clr_mask;
  // The buffer each side is pointing at, selected rather than indexed.
  // Indexing a packed-struct field with a variable is legal SystemVerilog
  // but Icarus 13.0 will not elaborate it, so the mux is written out.
  logic own_at_fw, own_at_hw;

  assign fw_ctrl   = fw_we && (addr_e'(fw_addr) == A_CTRL);
  assign fw_ack    = fw_we && (addr_e'(fw_addr) == A_ACK) && fw_wdata[0];
  assign fw_dtog   = fw_ctrl && fw_wdata[1];

  assign both_full = d.full[0] && d.full[1];
  assign cmt       = rx_commit && ep_en;
  assign cmt_new   = cmt && (rx_pid_odd == d.dtog);
  assign cmt_dup   = cmt && (rx_pid_odd != d.dtog);
  assign accept    = cmt_new && !both_full;
  assign overrun   = cmt_new &&  both_full;
  assign own_at_fw = d.fw_ptr ? d.full[1] : d.full[0];
  assign own_at_hw = d.hw_ptr ? d.full[1] : d.full[0];
  assign ack_ok    = fw_ack &&  own_at_fw;
  assign ack_bad   = fw_ack && !own_at_fw;

  assign set_mask  = accept ? (d.hw_ptr ? 2'b10 : 2'b01) : 2'b00;
  assign clr_mask  = ack_ok ? (d.fw_ptr ? 2'b10 : 2'b01) : 2'b00;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      d     <= '0;
      ep_en <= 1'b0;
      c_acc <= '0; c_dup <= '0; c_abt <= '0; c_ovr <= '0; c_bad <= '0;
    end else if (usb_reset) begin
      // One assignment, from one constant, of exactly the fields that are
      // DATA state. ep_en is not in the struct, so it cannot be cleared
      // here by accident -- which is the mistake this typing prevents.
      d <= '0;
    end else begin
      // A hardware set and a firmware release in the same cycle compose.
      d.full <= (d.full | set_mask) & ~clr_mask;

      if (accept) begin
        if (d.hw_ptr) d.len1 <= rx_len;
        else          d.len0 <= rx_len;
        d.hw_ptr <= ~d.hw_ptr;
        c_acc    <= c_acc + 16'd1;
      end

      if (ack_ok) d.fw_ptr <= ~d.fw_ptr;

      // A delta from two sources, so both land.
      d.dtog <= d.dtog ^ accept ^ fw_dtog;

      if (fw_ctrl)  ep_en <= fw_wdata[0];
      if (overrun)  begin d.ovr <= 1'b1; c_ovr <= c_ovr + 16'd1; end
      if (ack_bad)  begin d.bad <= 1'b1; c_bad <= c_bad + 16'd1; end
      if (cmt_dup)  c_dup <= c_dup + 16'd1;
      if (rx_abort) c_abt <= c_abt + 16'd1;
    end
  end

  // ---- derived ---------------------------------------------------
  logic [LENW-1:0] len_cur;
  assign len_cur    = d.fw_ptr ? d.len1 : d.len0;

  assign hw_nak     = !ep_en || both_full;
  assign hw_buf_sel = d.hw_ptr;
  assign hw_dup     = cmt_dup;
  assign irq        = ep_en && (d.full[0] || d.full[1]);

  always_comb begin
    unique case (addr_e'(fw_addr))
      A_CTRL: fw_rdata = {14'd0, d.dtog, ep_en};
      A_STAT: fw_rdata = {10'd0, d.bad, d.ovr, d.hw_ptr, d.fw_ptr, d.full};
      A_LEN:  fw_rdata = {{(16-LENW){1'b0}}, len_cur};
      A_ACK:  fw_rdata = 16'd0;
    endcase
  end

  assign n_accept  = c_acc;
  assign n_dup     = c_dup;
  assign n_abort   = c_abt;
  assign n_overrun = c_ovr;
  assign n_badack  = c_bad;

`ifdef SVA_ON
  // ---------------------------------------------------------------
  //  Concurrent assertions. Icarus Verilog 13.0 rejects SVA outright,
  //  so these are compiled only where a tool supports them; under
  //  Icarus each is enforced by the procedural check named beside it.
  // ---------------------------------------------------------------

  // SAFETY. A packet is never written into a buffer firmware still owns.
  // This is the one that keeps received data from being silently lost.
  property p_no_overwrite;
    @(posedge clk) disable iff (!rst_n)
      accept |-> !own_at_hw;
  endproperty
  a_no_overwrite: assert property (p_no_overwrite);

  // SAFETY. While the engine is told to NAK, no packet can be taken.
  property p_nak_is_honoured;
    @(posedge clk) disable iff (!rst_n)
      hw_nak |-> !accept;
  endproperty
  a_nak_is_honoured: assert property (p_nak_is_honoured);

  // CONSISTENCY. The two derived outputs are functions of the ownership
  // bits and the enable, with no state of their own.
  property p_outputs_are_derived;
    @(posedge clk) disable iff (!rst_n)
      (irq    == (ep_en && (d.full[0] || d.full[1]))) &&
      (hw_nak == (!ep_en || (d.full[0] && d.full[1])));
  endproperty
  a_outputs_are_derived: assert property (p_outputs_are_derived);

  // BOUNDS. A captured length never exceeds the endpoint's maximum. A
  // length register one bit too narrow fails this by wrapping.
  property p_length_in_range;
    @(posedge clk) disable iff (!rst_n)
      (d.len0 <= MAXPKT) && (d.len1 <= MAXPKT);
  endproperty
  a_length_in_range: assert property (p_length_in_range);

  // ORDERING. Each pointer moves by exactly one position, and only for
  // its own reason. A pointer that jumps has desynchronised the two
  // sides permanently.
  property p_ptrs_step_once;
    @(posedge clk) disable iff (!rst_n)
      ##1 (d.hw_ptr != $past(d.hw_ptr)) |-> $past(accept) || $past(usb_reset);
  endproperty
  a_ptrs_step_once: assert property (p_ptrs_step_once);

  property p_fw_ptr_needs_ownership;
    @(posedge clk) disable iff (!rst_n)
      ##1 (d.fw_ptr != $past(d.fw_ptr)) |-> $past(ack_ok) || $past(usb_reset);
  endproperty
  a_fw_ptr_needs_ownership: assert property (p_fw_ptr_needs_ownership);

  // STABILITY. While firmware owns the buffer at fw_ptr and has not
  // released it, the length it will read does not change underneath it.
  property p_len_stable_while_owned;
    @(posedge clk) disable iff (!rst_n)
      (own_at_fw && !ack_ok && !usb_reset) |=> $stable(len_cur);
  endproperty
  a_len_stable_while_owned: assert property (p_len_stable_while_owned);

  // RESET. A USB bus reset returns the data state to default on the next
  // edge and leaves the configuration alone. The second conjunct is the
  // one that fails when a designer puts ep_en in the reset list.
  property p_bus_reset_scope;
    @(posedge clk) disable iff (!rst_n)
      usb_reset |=> (d == {EP_DATA_W{1'b0}}) && (ep_en == $past(ep_en));
  endproperty
  a_bus_reset_scope: assert property (p_bus_reset_scope);

  // TOGGLE. Three separate obligations on one bit.
  property p_toggle_flips_on_accept;
    @(posedge clk) disable iff (!rst_n)
      (accept && !fw_dtog && !usb_reset) |=> d.dtog == !$past(d.dtog);
  endproperty
  a_toggle_flips_on_accept: assert property (p_toggle_flips_on_accept);

  property p_toggle_holds_on_dup_or_abort;
    @(posedge clk) disable iff (!rst_n)
      ((cmt_dup || rx_abort) && !accept && !fw_dtog && !usb_reset)
        |=> $stable(d.dtog);
  endproperty
  a_toggle_holds: assert property (p_toggle_holds_on_dup_or_abort);

  // The write-1-to-toggle decision, stated as a property: two flips in
  // one cycle compose to none. A read-modify-write register cannot
  // satisfy this.
  property p_toggle_composes;
    @(posedge clk) disable iff (!rst_n)
      (accept && fw_dtog && !usb_reset) |=> $stable(d.dtog);
  endproperty
  a_toggle_composes: assert property (p_toggle_composes);

  // PROGRESS. A buffer firmware owns does not stay owned forever if
  // firmware keeps releasing. Stated under an explicit assumption,
  // because without one it is simply false -- firmware may never run.
  property p_release_makes_progress;
    @(posedge clk) disable iff (!rst_n)
      (d.full != 2'b00) && ack_ok |=> (d.full != $past(d.full));
  endproperty
  a_release_makes_progress: assert property (p_release_makes_progress);

  // COVER, so that the safety properties above cannot pass vacuously by
  // the design never reaching the interesting states at all.
  c_both_full:    cover property (@(posedge clk) both_full);
  c_overrun:      cover property (@(posedge clk) overrun);
  c_zlp:          cover property (@(posedge clk) accept && rx_len == '0);
  c_maxpkt:       cover property (@(posedge clk) accept && rx_len == MAXPKT);
  c_toggle_race:  cover property (@(posedge clk) accept && fw_dtog);
  c_set_and_clr:  cover property (@(posedge clk) accept && ack_ok);
  c_dup:          cover property (@(posedge clk) cmt_dup);
  c_bad_release:  cover property (@(posedge clk) ack_bad);
`endif

endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  tb_usbep_pingpong_sv -- SystemVerilog testbench for usbep_pingpong_sv.
//
//  Phases 1-4 present the SAME directed stimulus, in the same order, as
//  the Verilog-2005 bench, so their check counts must agree to the digit.
//  Phase 5 uses its own randomisation and is not expected to agree.
//
//  Everything is checked against an INDEPENDENT cycle-accurate reference
//  model held in the ref_* variables. The model is advanced from the
//  inputs presented at each clock edge and from its own prior state. It
//  never reads a DUT output, so it is capable of disagreeing.
//
//  Nothing is checked by hierarchical reference either. Every comparison
//  goes through the ports -- which means the four-register firmware map
//  is itself under test, not just the internal state.
//
//  PHASES
//    1 STATE SWEEP  exhaustive: 12 reachable ownership states x 11 events
//    2 BOUNDARY     the named edges, one scenario each
//    3 ORDER        packets must come out in the order they went in
//    4 PARITY       relabelling the two buffers changes nothing but labels
//    5 RANDOM       supplementary; phases 1-4 must pass without it
// =====================================================================
`timescale 1ns/1ps

module tb_usbep_pingpong_sv;

  localparam int MAXPKT = 64;
  localparam int LENW   = 7;

  logic            clk = 1'b0;
  logic            rst_n;
  logic            usb_reset;
  logic            rx_commit, rx_pid_odd, rx_abort;
  logic [LENW-1:0] rx_len;
  logic            fw_we;
  logic [1:0]      fw_addr;
  logic [15:0]     fw_wdata;

  wire             hw_nak, hw_buf_sel, hw_dup, irq;
  wire [15:0]      fw_rdata;
  wire [15:0]      n_accept, n_dup, n_abort, n_overrun, n_badack;

  usbep_pingpong_sv #(.MAXPKT(MAXPKT), .LENW(LENW)) dut (
    .clk(clk), .rst_n(rst_n), .usb_reset(usb_reset),
    .rx_commit(rx_commit), .rx_pid_odd(rx_pid_odd),
    .rx_len(rx_len), .rx_abort(rx_abort),
    .hw_nak(hw_nak), .hw_buf_sel(hw_buf_sel), .hw_dup(hw_dup),
    .fw_we(fw_we), .fw_addr(fw_addr), .fw_wdata(fw_wdata),
    .fw_rdata(fw_rdata), .irq(irq),
    .n_accept(n_accept), .n_dup(n_dup), .n_abort(n_abort),
    .n_overrun(n_overrun), .n_badack(n_badack)
  );

  always #5 clk = ~clk;

  // ---- the independent reference model ---------------------------
  // The independent reference model. Separate variables rather than one
  // struct: Icarus 13.0 supports no unpacked struct, and a packed one
  // cannot be indexed by a variable, which rm_full has to be.
  logic [1:0]      rm_full;
  logic [LENW-1:0] rm_len0, rm_len1;
  logic            rm_hw, rm_fw, rm_dtog, rm_en, rm_ovr, rm_bad;
  logic [15:0]     rm_acc, rm_dup, rm_abt, rm_ovc, rm_bdc;
  // what the model says hw_dup should be DURING the present cycle
  // hw_dup is a pulse, checked in the cycle it appears

  // ---- bookkeeping ------------------------------------------------
  int  chk_dir, chk_rnd, err;
  bit  in_random;
  // measured reachability, not assumed
  int  meas_accept, meas_dup, meas_abort, meas_ovr, meas_bad, meas_bothfull,
       meas_nak, meas_zlp, meas_maxpkt, meas_dtograce, meas_setclr,
       meas_setupfail;


  task bump;                       // one comparison happened
    begin
      if (in_random) chk_rnd = chk_rnd + 1;
      else           chk_dir = chk_dir + 1;
    end
  endtask

  task ck(string what, logic [31:0] got, logic [31:0] exp);
    begin
      bump;
      if (got !== exp) begin
        err = err + 1;
        if (err <= 60)
          $display("  ** %s: got %0d expected %0d  (t=%0t)", what, got, exp, $time);
      end
    end
  endtask

  // Advance the reference model using the input pins as they stand at
  // this clock edge. Called immediately after @(posedge clk).
  task ref_step;
    logic both, ctl, dtg, cmt, cnew, cdup, acc, ovr, aok, abad;
    logic hw_old, fw_old;
    begin
      hw_old = rm_hw;
      fw_old = rm_fw;
      if (!rst_n) begin
        rm_full = 2'b00; rm_len0 = 0; rm_len1 = 0;
        rm_hw = 0; rm_fw = 0; rm_dtog = 0; rm_en = 0;
        rm_ovr = 0; rm_bad = 0;
        rm_acc = 0; rm_dup = 0; rm_abt = 0; rm_ovc = 0; rm_bdc = 0;
      end else if (usb_reset) begin
        rm_full = 2'b00; rm_len0 = 0; rm_len1 = 0;
        rm_hw = 0; rm_fw = 0; rm_dtog = 0;
        rm_ovr = 0; rm_bad = 0;
        // ep_en and the counters deliberately survive
      end else begin
        both = rm_full[0] & rm_full[1];
        ctl  = fw_we && (fw_addr == 2'd0);
        dtg  = ctl && fw_wdata[1];
        cmt  = rx_commit && rm_en;
        cnew = cmt && (rx_pid_odd == rm_dtog);
        cdup = cmt && (rx_pid_odd != rm_dtog);
        acc  = cnew && !both;
        ovr  = cnew &&  both;
        aok  = fw_we && (fw_addr == 2'd3) && fw_wdata[0] &&  rm_full[fw_old];
        abad = fw_we && (fw_addr == 2'd3) && fw_wdata[0] && !rm_full[fw_old];

        if (acc) begin
          rm_full[hw_old] = 1'b1;
          if (hw_old) rm_len1 = rx_len; else rm_len0 = rx_len;
          rm_hw  = ~hw_old;
          rm_acc = rm_acc + 1;
        end
        if (aok) begin
          rm_full[fw_old] = 1'b0;
          rm_fw = ~fw_old;
        end
        rm_dtog = rm_dtog ^ acc ^ dtg;
        if (ctl)  rm_en  = fw_wdata[0];
        if (ovr)  begin rm_ovr = 1'b1; rm_ovc = rm_ovc + 1; end
        if (abad) begin rm_bad = 1'b1; rm_bdc = rm_bdc + 1; end
        if (cdup)     rm_dup = rm_dup + 1;
        if (rx_abort) rm_abt = rm_abt + 1;

        // measured event tallies
        if (acc)  meas_accept = meas_accept + 1;
        if (cdup) meas_dup    = meas_dup    + 1;
        if (ovr)  meas_ovr    = meas_ovr    + 1;
        if (abad) meas_bad    = meas_bad    + 1;
        if (rx_abort) meas_abort = meas_abort + 1;
        if (acc && rx_len == 0)      meas_zlp    = meas_zlp    + 1;
        if (acc && rx_len == MAXPKT) meas_maxpkt = meas_maxpkt + 1;
        if (acc && dtg)              meas_dtograce = meas_dtograce + 1;
        if (acc && aok)              meas_setclr   = meas_setclr   + 1;
      end
    end
  endtask

  // What hw_dup should be right now, from the reference state and pins.
  function logic ref_dup();
    return rx_commit && rm_en && (rx_pid_odd !== rm_dtog);
  endfunction

  // Compare the DUT against the model through the ports only. Sweeps
  // fw_addr with fw_we low, which has no side effect.
  task cmp;
    logic [15:0] exp;
    begin
      if (rm_full[0] & rm_full[1]) meas_bothfull = meas_bothfull + 1;
      if (hw_nak)                meas_nak      = meas_nak      + 1;

      fw_addr = 2'd0; #1;
      ck("CTRL", {16'd0, fw_rdata}, {16'd0, 14'd0, rm_dtog, rm_en});
      fw_addr = 2'd1; #1;
      exp = {10'd0, rm_bad, rm_ovr, rm_hw, rm_fw, rm_full};
      ck("STAT", {16'd0, fw_rdata}, {16'd0, exp});
      fw_addr = 2'd2; #1;
      exp = {{(16-LENW){1'b0}}, (rm_fw ? rm_len1 : rm_len0)};
      ck("LEN",  {16'd0, fw_rdata}, {16'd0, exp});
      fw_addr = 2'd3; #1;
      ck("ACKRD", {16'd0, fw_rdata}, 32'd0);

      ck("hw_nak",     {31'd0, hw_nak},     {31'd0, (~rm_en | (rm_full[0] & rm_full[1]))});
      ck("hw_buf_sel", {31'd0, hw_buf_sel}, {31'd0, rm_hw});
      ck("irq",        {31'd0, irq},        {31'd0, (rm_en & (rm_full[0] | rm_full[1]))});
      ck("n_accept",   {16'd0, n_accept},   {16'd0, rm_acc});
      ck("n_dup",      {16'd0, n_dup},      {16'd0, rm_dup});
      ck("n_abort",    {16'd0, n_abort},    {16'd0, rm_abt});
      ck("n_overrun",  {16'd0, n_overrun},  {16'd0, rm_ovc});
      ck("n_badack",   {16'd0, n_badack},   {16'd0, rm_bdc});
      fw_addr = 2'd0;
    end
  endtask

  // One cycle. Inputs must already be driven. Checks hw_dup before the
  // edge, advances the model at the edge, compares after it, then
  // releases the one-cycle inputs.
  task step;
    begin
      #1;
      ck("hw_dup", {31'd0, hw_dup}, {31'd0, ref_dup()});
      @(posedge clk);
      ref_step;
      #1;
      cmp;
      rx_commit = 0; rx_abort = 0; fw_we = 0; usb_reset = 0;
      rx_len = 0; fw_wdata = 0;
    end
  endtask

  task idle; step; endtask

  task wr(logic [1:0] a, logic [15:0] d);   // a firmware register write
    fw_we = 1; fw_addr = a; fw_wdata = d; step;
  endtask

  task do_ack;  wr(2'd3, 16'h0001); endtask
  task do_dtog; wr(2'd0, {14'd0, 1'b1, rm_en}); endtask
  task set_en(logic v); wr(2'd0, {15'd0, v}); endtask

  task commit(logic match, logic [LENW-1:0] len);  // accepted or duplicate
    rx_commit  = 1;
    rx_pid_odd = match ? rm_dtog : ~rm_dtog;
    rx_len     = len;
    step;
  endtask

  task abort_pkt; rx_abort = 1; step; endtask

  task hard_reset;
    begin
      rst_n = 0; usb_reset = 0;
      rx_commit = 0; rx_pid_odd = 0; rx_len = 0; rx_abort = 0;
      fw_we = 0; fw_addr = 0; fw_wdata = 0;
      repeat (3) begin @(posedge clk); ref_step; end
      #1; rst_n = 1;
      @(posedge clk); ref_step; #1; cmp;
    end
  endtask

  task bus_reset; usb_reset = 1; step; endtask

  // -----------------------------------------------------------------
  //  Build one of the 12 reachable (occupancy, base pointer, toggle)
  //  states, then PROVE it was built. A setup that silently fails must
  //  not turn into a passing test of the wrong state.
  // -----------------------------------------------------------------
  task setup_state(int occ, int bp, int tog);
    logic [15:0] stat, ctrl;
    int want_full, want_hw;
    begin
      hard_reset;
      set_en(1'b1);
      bus_reset;                        // a clean, known data state
      if (bp == 1) begin                // rotate both pointers by one
        commit(1'b1, 7'd3); do_ack;
      end
      if (occ >= 1) commit(1'b1, 7'd5);
      if (occ >= 2) commit(1'b1, 7'd9);
      if (rm_dtog !== tog[0]) do_dtog;

      // --- prove it ---
      want_full = (occ == 0) ? 0 :
                  (occ == 1) ? (bp ? 2 : 1) : 3;
      want_hw   = (occ == 1) ? (bp ? 0 : 1) : bp;
      fw_addr = 2'd1; #1; stat = fw_rdata;
      fw_addr = 2'd0; #1; ctrl = fw_rdata;
      fw_addr = 2'd0;
      bump; if (stat[1:0] !== want_full[1:0]) begin
        err = err + 1; meas_setupfail = meas_setupfail + 1;
        $display("  ** setup full: occ=%0d bp=%0d got %b want %b", occ, bp, stat[1:0], want_full[1:0]);
      end
      bump; if (stat[2] !== bp[0]) begin
        err = err + 1; meas_setupfail = meas_setupfail + 1;
        $display("  ** setup fw_ptr: occ=%0d bp=%0d got %b", occ, bp, stat[2]);
      end
      bump; if (stat[3] !== want_hw[0]) begin
        err = err + 1; meas_setupfail = meas_setupfail + 1;
        $display("  ** setup hw_ptr: occ=%0d bp=%0d got %b want %b", occ, bp, stat[3], want_hw[0]);
      end
      bump; if (ctrl[1] !== tog[0]) begin
        err = err + 1; meas_setupfail = meas_setupfail + 1;
        $display("  ** setup dtog: occ=%0d bp=%0d tog=%0d got %b", occ, bp, tog, ctrl[1]);
      end
    end
  endtask

  // Apply event number `ev`. One cycle, except where the event is
  // deliberately two things at once.
  task apply_event(int ev);
    begin
      case (ev)
        0:  idle;
        1:  do_ack;
        2:  abort_pkt;
        3:  commit(1'b1, 7'd0);                       // ZLP
        4:  commit(1'b1, 7'd1);
        5:  commit(1'b1, LENW'(MAXPKT));           // the width boundary
        6:  commit(1'b0, 7'd13);                      // retransmission
        7:  begin rx_commit=1; rx_pid_odd=rm_dtog;  rx_len=7'd7;
                  fw_we=1; fw_addr=2'd3; fw_wdata=16'h0001; step; end
        8:  begin rx_commit=1; rx_pid_odd=~rm_dtog; rx_len=7'd13;
                  fw_we=1; fw_addr=2'd3; fw_wdata=16'h0001; step; end
        9:  begin rx_abort=1; fw_we=1; fw_addr=2'd3; fw_wdata=16'h0001; step; end
        10: bus_reset;
        default: idle;
      endcase
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 3: order preservation
  // -----------------------------------------------------------------
  int  q_len [256];            // what the engine handed in
  int  q_wr, q_rd;
  int  svc_pat [8];
  int  ord_pkts, ord_naked, ord_read, ord_bothfull;
  logic [15:0] rl;

  task phase_order;
    int n, c, want, drained;
    begin
      // How many packets firmware drains per engine offer. The AVERAGE is
      // exactly one, so the endpoint neither starves nor runs away -- but
      // the RUNS OF ZERO are the point. Without them the occupancy never
      // exceeds one buffer, the second buffer is never used, and the whole
      // mechanism under test is never entered.
      svc_pat[0]=0; svc_pat[1]=0; svc_pat[2]=2; svc_pat[3]=1;
      svc_pat[4]=0; svc_pat[5]=2; svc_pat[6]=1; svc_pat[7]=2;
      hard_reset; set_en(1'b1); bus_reset;
      q_wr = 0; q_rd = 0; ord_pkts = 0; ord_naked = 0; ord_read = 0;
      ord_bothfull = 0;
      for (n = 0; n < 96; n = n + 1) begin
        // the engine offers a packet every iteration, and honours hw_nak
        if (hw_nak) begin
          ord_naked = ord_naked + 1;
          idle;
        end else begin
          q_len[q_wr] = (n * 7) % (MAXPKT + 1);
          q_wr = q_wr + 1;
          ord_pkts = ord_pkts + 1;
          commit(1'b1, LENW'((n * 7) % (MAXPKT + 1)));
        end
        if (hw_nak) ord_bothfull = ord_bothfull + 1;
        // firmware drains svc_pat[] packets this iteration -- sometimes
        // none, which is what backs the endpoint up
        for (c = 0; c < svc_pat[n % 8]; c = c + 1) begin
          if (irq && q_rd < q_wr) begin
            fw_addr = 2'd2; #1; rl = fw_rdata; fw_addr = 2'd0;
            want = q_len[q_rd]; q_rd = q_rd + 1; ord_read = ord_read + 1;
            ck("order", {16'd0, rl}, want);
            do_ack;
          end else if (irq) begin
            // more releases than commits: the pointers have desynchronised
            bump; err = err + 1; do_ack;
          end else idle;
        end
      end
      // drain what is left
      drained = 0;
      while (irq && drained < 8 && q_rd < q_wr) begin
        fw_addr = 2'd2; #1; rl = fw_rdata; fw_addr = 2'd0;
        want = q_len[q_rd]; q_rd = q_rd + 1; ord_read = ord_read + 1;
        ck("order-drain", {16'd0, rl}, want);
        do_ack;
        drained = drained + 1;
      end
      bump; if (ord_read !== ord_pkts) begin
        err = err + 1;
        $display("  ** order: committed %0d, read back %0d", ord_pkts, ord_read);
      end
      // The scenario must have HAPPENED. A phase that backs the endpoint
      // up zero times has tested the ordering of a one-deep queue.
      bump; if (ord_naked < 4) begin
        err = err + 1;
        $display("  ** order: backpressure never reached -- %0d NAKed offers", ord_naked);
      end
      bump; if (ord_bothfull < 8) begin
        err = err + 1;
        $display("  ** order: both buffers full only %0d times", ord_bothfull);
      end
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 4: buffer-parity relabelling
  //
  //  Run the same event on the same occupancy with the base pointer at
  //  0 and at 1. Everything the firmware can observe must be identical
  //  except the three things that ARE buffer labels -- the ownership
  //  mask and the two pointers -- which must be exchanged.
  // -----------------------------------------------------------------
  logic [15:0] par_stat [2];
  logic [15:0] par_ctrl [2];
  logic [15:0] par_len  [2];
  logic [2:0]  par_out  [2];
  int  par_pairs, par_len_skipped, par_reset_cases;

  task phase_parity;
    int occ_i, ev_i, tg_i, side;
    begin
      par_pairs = 0; par_len_skipped = 0; par_reset_cases = 0;
      for (occ_i = 0; occ_i <= 2; occ_i = occ_i + 1)
      for (tg_i  = 0; tg_i  <= 1; tg_i  = tg_i  + 1)
      for (ev_i  = 0; ev_i  <= 10; ev_i = ev_i + 1) begin
        for (side = 0; side <= 1; side = side + 1) begin
          setup_state(occ_i, side, tg_i);
          apply_event(ev_i);
          fw_addr = 2'd1; #1; par_stat[side] = fw_rdata;
          fw_addr = 2'd0; #1; par_ctrl[side] = fw_rdata;
          fw_addr = 2'd2; #1; par_len[side]  = fw_rdata;
          fw_addr = 2'd0;
          par_out[side] = {hw_nak, irq, hw_buf_sel};
        end
        par_pairs = par_pairs + 1;
        // ---- the label-free observations must match exactly ----
        ck("par-ctrl", {16'd0, par_ctrl[0]}, {16'd0, par_ctrl[1]});
        ck("par-nak",  {31'd0, par_out[0][2]}, {31'd0, par_out[1][2]});
        ck("par-irq",  {31'd0, par_out[0][1]}, {31'd0, par_out[1][1]});
        ck("par-sticky", {16'd0, par_stat[0][5:4]}, {16'd0, par_stat[1][5:4]});
        // Occupancy is label-free, so the two sides must agree on whether
        // firmware owns anything at all -- and LEN is only DEFINED when it
        // does. Comparing it when nothing is owned compares two different
        // stale residues and reports a difference that is not one.
        ck("par-own", {31'd0, par_out[0][1]}, {31'd0, par_out[1][1]});
        if (par_out[0][1] && par_out[1][1])
          ck("par-len", {16'd0, par_len[0]}, {16'd0, par_len[1]});
        else
          par_len_skipped = par_len_skipped + 1;
        // ---- and the labels themselves must be the other way round ----
        ck("par-full-swap", {30'd0, par_stat[0][1:0]},
                            {30'd0, par_stat[1][0], par_stat[1][1]});
        if (ev_i != 10) begin
          ck("par-fwptr-inv", {31'd0, par_stat[0][2]}, {31'd0, ~par_stat[1][2]});
          ck("par-hwptr-inv", {31'd0, par_stat[0][3]}, {31'd0, ~par_stat[1][3]});
          ck("par-bufsel-inv",{31'd0, par_out[0][0]},  {31'd0, ~par_out[1][0]});
        end else begin
          // THE ONE EXCEPTION, and it is required rather than tolerated: a
          // USB bus reset is the only operation that names an absolute
          // buffer, because after it the host and the device have to agree
          // on which buffer is next and zero is the only value both can
          // assume. So the relabelling collapses instead of inverting.
          par_reset_cases = par_reset_cases + 1;
          ck("par-rst-fwptr",  {16'd0, par_stat[0][2], par_stat[1][2]}, 32'd0);
          ck("par-rst-hwptr",  {16'd0, par_stat[0][3], par_stat[1][3]}, 32'd0);
          ck("par-rst-bufsel", {16'd0, par_out[0][0],  par_out[1][0]},  32'd0);
        end
      end
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 2: the named boundaries
  // -----------------------------------------------------------------
  logic [15:0] s1, s2;

  task phase_boundary;
    begin
      // B1 an accepted packet and a firmware toggle-write in the SAME
      //    cycle: two flips, so the toggle must not move.
      hard_reset; set_en(1'b1); bus_reset;
      rx_commit=1; rx_pid_odd=rm_dtog; rx_len=7'd4;
      fw_we=1; fw_addr=2'd0; fw_wdata={14'd0, 1'b1, 1'b1};
      step;
      fw_addr=2'd0; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B1 dtog unchanged", {31'd0, s1[1]}, 32'd0);
      ck("B1 packet still taken", {16'd0, n_accept}, 32'd1);

      // B2 MAXPKT reads back intact: a length register one bit too
      //    narrow turns 64 into 0.
      hard_reset; set_en(1'b1); bus_reset;
      commit(1'b1, LENW'(MAXPKT));
      fw_addr=2'd2; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B2 MAXPKT length", {16'd0, s1}, MAXPKT);

      // B3 a zero-length packet is a PACKET: it takes a buffer and
      //    raises the interrupt.
      hard_reset; set_en(1'b1); bus_reset;
      commit(1'b1, 7'd0);
      ck("B3 zlp irq",    {31'd0, irq}, 32'd1);
      ck("B3 zlp accept", {16'd0, n_accept}, 32'd1);
      fw_addr=2'd2; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B3 zlp length", {16'd0, s1}, 32'd0);

      // B4 an overrun must DROP the new packet, not overwrite a buffer
      //    firmware still owns.
      hard_reset; set_en(1'b1); bus_reset;
      commit(1'b1, 7'd11); commit(1'b1, 7'd22);
      ck("B4 nak asserted", {31'd0, hw_nak}, 32'd1);
      rx_commit=1; rx_pid_odd=rm_dtog; rx_len=7'd33; step;   // engine misbehaves
      ck("B4 overrun counted", {16'd0, n_overrun}, 32'd1);
      ck("B4 accept unchanged", {16'd0, n_accept}, 32'd2);
      fw_addr=2'd2; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B4 first length intact", {16'd0, s1}, 32'd11);
      do_ack;
      fw_addr=2'd2; #1; s2 = fw_rdata; fw_addr=2'd0;
      ck("B4 second length intact", {16'd0, s2}, 32'd22);

      // B5 releasing a buffer firmware does not own must not move the
      //    read pointer. If it does, the two pointers desynchronise and
      //    the endpoint never recovers.
      hard_reset; set_en(1'b1); bus_reset;
      do_ack;
      ck("B5 badack counted", {16'd0, n_badack}, 32'd1);
      fw_addr=2'd1; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B5 fw_ptr still 0", {31'd0, s1[2]}, 32'd0);
      commit(1'b1, 7'd17);
      fw_addr=2'd2; #1; s2 = fw_rdata; fw_addr=2'd0;
      ck("B5 packet readable", {16'd0, s2}, 32'd17);

      // B6 an aborted packet advances nothing and flips nothing, so the
      //    host's retry with the SAME PID is still new data.
      hard_reset; set_en(1'b1); bus_reset;
      abort_pkt;
      fw_addr=2'd1; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B6 no buffer taken", {16'd0, s1[1:0]}, 32'd0);
      ck("B6 hw_ptr still 0",  {31'd0, s1[3]},   32'd0);
      fw_addr=2'd0; #1; s2 = fw_rdata; fw_addr=2'd0;
      ck("B6 dtog still 0",    {31'd0, s2[1]},   32'd0);
      commit(1'b1, 7'd8);
      ck("B6 retry accepted",  {16'd0, n_accept}, 32'd1);

      // B7 back-to-back packets on consecutive cycles.
      hard_reset; set_en(1'b1); bus_reset;
      rx_commit=1; rx_pid_odd=rm_dtog; rx_len=7'd2;
      #1; @(posedge clk); ref_step; #1;
      rx_commit=1; rx_pid_odd=rm_dtog; rx_len=7'd3;
      #1; @(posedge clk); ref_step; #1;
      rx_commit=0; cmp;
      ck("B7 both taken", {16'd0, n_accept}, 32'd2);
      fw_addr=2'd1; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B7 both full",  {16'd0, s1[1:0]}, 32'd3);

      // B8 a USB bus reset clears the data state and the toggle, and
      //    leaves the endpoint ENABLED. Clearing ep_en here is a real
      //    bug: the device enumerates and then goes deaf.
      hard_reset; set_en(1'b1);
      commit(1'b1, 7'd9);                    // dtog is now 1
      fw_addr=2'd0; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B8 dtog is 1 first", {31'd0, s1[1]}, 32'd1);
      bus_reset;
      fw_addr=2'd0; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B8 ep_en survives", {31'd0, s1[0]}, 32'd1);
      ck("B8 dtog cleared",   {31'd0, s1[1]}, 32'd0);
      fw_addr=2'd1; #1; s2 = fw_rdata; fw_addr=2'd0;
      ck("B8 buffers returned", {16'd0, s2[1:0]}, 32'd0);
      ck("B8 pointers zeroed",  {16'd0, s2[3:2]}, 32'd0);
      // and the host's first packet after a reset is DATA0
      rx_commit=1; rx_pid_odd=1'b0; rx_len=7'd6; step;
      ck("B8 DATA0 accepted", {16'd0, n_accept}, 32'd2);

      // B9 disabling the endpoint hides the interrupt and NAKs, but does
      //    not destroy what firmware already owns.
      hard_reset; set_en(1'b1); bus_reset;
      commit(1'b1, 7'd21);
      set_en(1'b0);
      ck("B9 irq gone",   {31'd0, irq},    32'd0);
      ck("B9 nak forced", {31'd0, hw_nak}, 32'd1);
      fw_addr=2'd1; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B9 buffer kept", {16'd0, s1[1:0]}, 32'd1);
      set_en(1'b1);
      ck("B9 irq returns", {31'd0, irq}, 32'd1);
      fw_addr=2'd2; #1; s2 = fw_rdata; fw_addr=2'd0;
      ck("B9 length kept", {16'd0, s2}, 32'd21);

      // B10 a commit arriving while the endpoint is disabled is not a
      //     duplicate and not an overrun -- it is nothing at all.
      hard_reset; bus_reset;
      rx_commit=1; rx_pid_odd=1'b1; rx_len=7'd5; step;
      ck("B10 nothing counted", {16'd0, n_accept + n_dup + n_overrun}, 32'd0);

      // B11 a release and a bus reset in the same cycle: the reset wins
      //     and the buffer is returned to hardware either way.
      hard_reset; set_en(1'b1); bus_reset;
      commit(1'b1, 7'd12);
      usb_reset=1; fw_we=1; fw_addr=2'd3; fw_wdata=16'h0001; step;
      fw_addr=2'd1; #1; s1 = fw_rdata; fw_addr=2'd0;
      ck("B11 buffers clear",  {16'd0, s1[1:0]}, 32'd0);
      ck("B11 fw_ptr zeroed",  {31'd0, s1[2]},   32'd0);
      ck("B11 no bad ack",     {16'd0, n_badack}, 32'd0);
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 1: the exhaustive state sweep
  // -----------------------------------------------------------------
  task phase_sweep;
    int occ_i, bp_i, tg_i, ev_i;
    begin
      for (occ_i = 0; occ_i <= 2; occ_i = occ_i + 1)
      for (bp_i  = 0; bp_i  <= 1; bp_i  = bp_i  + 1)
      for (tg_i  = 0; tg_i  <= 1; tg_i  = tg_i  + 1)
      for (ev_i  = 0; ev_i  <= 10; ev_i = ev_i + 1) begin
        setup_state(occ_i, bp_i, tg_i);
        cmp;                       // the state we start from
        apply_event(ev_i);         // cmp happens inside step
        idle;                      // and one cycle later
      end
    end
  endtask

  // -----------------------------------------------------------------
  //  PHASE 5: random, with the firmware latency deliberately spread
  //  wide enough that both buffers are actually used.
  // -----------------------------------------------------------------
  task phase_random;
    int n, r, lat, c;
    begin
      in_random = 1;
      hard_reset; set_en(1'b1); bus_reset;
      for (n = 0; n < 4000; n = n + 1) begin
        r = $urandom_range(99);
        if (r < 55) begin
          // the engine offers a packet; it honours hw_nak most of the
          // time and breaks the rule occasionally so the overrun path
          // is exercised at all
          if (!hw_nak || (($urandom_range(99)) < 3)) begin
            rx_commit  = 1;
            rx_pid_odd = (($urandom_range(99)) < 12) ? ~rm_dtog : rm_dtog;
            rx_len     = LENW'($urandom_range(MAXPKT));
            step;
          end else idle;
        end else if (r < 62) begin
          abort_pkt;
        end else if (r < 78) begin
          // firmware services -- sometimes when there is nothing there
          do_ack;
        end else if (r < 85) begin
          // A packet arriving on the SAME CYCLE as a firmware release.
          // The two sides are independent agents, so this happens in real
          // hardware -- and a random phase that issues one action per
          // cycle can never produce it.
          // and it only EXISTS when firmware owns one buffer and the
          // hardware has the other, so the branch is steered at that
          // window rather than hoping to land in it
          if (irq && !hw_nak) begin
            rx_commit  = 1;
            rx_pid_odd = rm_dtog;
            rx_len     = LENW'($urandom_range(MAXPKT));
            fw_we = 1; fw_addr = 2'd3; fw_wdata = 16'h0001;
            step;
          end else idle;
        end else if (r < 89) begin
          // a toggle write landing on the SAME CYCLE as an accepted
          // packet. Two flips compose to none, and that is the answer a
          // read-modify-write register cannot give.
          rx_commit  = 1;
          rx_pid_odd = rm_dtog;
          rx_len     = LENW'($urandom_range(MAXPKT));
          fw_we = 1; fw_addr = 2'd0; fw_wdata = {14'd0, 1'b1, 1'b1};
          step;
        end else if (r < 92) begin
          do_dtog;
        end else if (r < 95) begin
          bus_reset;
        end else if (r < 97) begin
          set_en(($urandom_range(99)) < 20 ? 1'b0 : 1'b1);
        end else begin
          lat = $urandom_range(4);
          for (c = 0; c <= lat; c = c + 1) idle;
        end
      end
      set_en(1'b1);
      in_random = 0;
    end
  endtask

  initial begin
    chk_dir = 0; chk_rnd = 0; err = 0; in_random = 0;
    meas_accept=0; meas_dup=0; meas_abort=0; meas_ovr=0; meas_bad=0; meas_bothfull=0;
    meas_nak=0; meas_zlp=0; meas_maxpkt=0; meas_dtograce=0; meas_setclr=0; meas_setupfail=0;

    phase_sweep;
    $display("  phase 1 state sweep     : %0d checks, %0d errors", chk_dir, err);
    phase_boundary;
    $display("  phase 2 boundary        : %0d checks, %0d errors", chk_dir, err);
    phase_order;
    $display("  phase 3 order           : %0d checks, %0d errors  (%0d packets, %0d NAKed offers, %0d both-full)",
             chk_dir, err, ord_pkts, ord_naked, ord_bothfull);
    phase_parity;
    $display("  phase 4 parity          : %0d checks, %0d errors  (%0d pairs, %0d reset exceptions, %0d LEN undefined)",
             chk_dir, err, par_pairs, par_reset_cases, par_len_skipped);
    $display("  ---- DIRECTED-ONLY TOTAL: %0d checks, %0d errors ----", chk_dir, err);
    phase_random;

    $display("");
    $display("  measured reachability (all phases)");
    $display("    accepted packets ....... %0d", meas_accept);
    $display("    of which zero-length ... %0d", meas_zlp);
    $display("    of which MAXPKT ........ %0d", meas_maxpkt);
    $display("    retransmissions ........ %0d", meas_dup);
    $display("    aborted packets ........ %0d", meas_abort);
    $display("    overruns ............... %0d", meas_ovr);
    $display("    illegal releases ....... %0d", meas_bad);
    $display("    accept+toggle-write .... %0d", meas_dtograce);
    $display("    accept+release ......... %0d", meas_setclr);
    $display("    cycles with both full .. %0d", meas_bothfull);
    $display("    cycles asserting NAK ... %0d", meas_nak);
    $display("    setup failures ......... %0d", meas_setupfail);
    $display("");
    $display("  directed checks .......... %0d", chk_dir);
    $display("  random checks ............ %0d", chk_rnd);
    $display("  TOTAL checks ............. %0d", chk_dir + chk_rnd);
    $display("  ERRORS ................... %0d", err);
    if (err == 0) $display("  PASS");
    else          $display("  FAIL");
    $finish;
  end

endmodule

10. VHDL-2008

The same contract again. VHDL's contribution is that it will not let a numeric quantity and a collection of bits be the same thing without a written conversion, and the conversion boundary is precisely where this design's width mistake lives.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
      when A_LEN =>
        v(LENW - 1 downto 0) := std_logic_vector(len_cur);

len_cur is unsigned; v is std_logic_vector. The cast is there because the register file is bits and the length is a number, and writing it down forces the author to have an opinion about the width. Mutation M9 narrows the length register by one bit so that a 64-byte packet reports zero, and in the VHDL version it has to be written as an explicit reconstruction:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
          if hw_ptr = '1' then len1_r <= '0' & rx_len(LENW-2 downto 0);

which is visibly a truncation. In Verilog the same mutation is a part-select that looks almost like the original line. Neither language stops you making the mistake; one of them makes it easier to see in review.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  usbep_pingpong (VHDL-2008) -- the same hardware contract as the
--  Verilog-2005 and SystemVerilog modules. Same ports, same reset
--  scopes, same same-cycle priorities, same latency.
--
--  CLASSIFICATION: simplified synthesisable teaching RTL.
--
--  The one thing VHDL insists on that the other two do not: every
--  numeric quantity says whether it is a number or a collection of bits,
--  and every conversion between the two is written down. The length
--  register is `unsigned`, the register file is `std_logic_vector`, and
--  the boundary between them is explicit -- which is exactly where the
--  width mistake in this design would live.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity usbep_pingpong is
  generic (
    MAXPKT : natural := 64;
    LENW   : natural := 7           -- must hold 0 .. MAXPKT INCLUSIVE
  );
  port (
    clk        : in  std_logic;
    rst_n      : in  std_logic;
    -- A USB bus reset: one cycle, and NOT the same thing as rst_n.
    usb_reset  : in  std_logic;

    -- serial interface engine side
    rx_commit  : in  std_logic;
    rx_pid_odd : in  std_logic;
    rx_len     : in  unsigned(LENW - 1 downto 0);
    rx_abort   : in  std_logic;

    hw_nak     : out std_logic;
    hw_buf_sel : out std_logic;
    hw_dup     : out std_logic;

    -- firmware (peripheral bus) side
    fw_we      : in  std_logic;
    fw_addr    : in  unsigned(1 downto 0);
    fw_wdata   : in  std_logic_vector(15 downto 0);
    fw_rdata   : out std_logic_vector(15 downto 0);

    irq        : out std_logic;

    n_accept   : out unsigned(15 downto 0);
    n_dup      : out unsigned(15 downto 0);
    n_abort    : out unsigned(15 downto 0);
    n_overrun  : out unsigned(15 downto 0);
    n_badack   : out unsigned(15 downto 0)
  );
end entity usbep_pingpong;

architecture rtl of usbep_pingpong is

  constant A_CTRL : unsigned(1 downto 0) := "00";
  constant A_STAT : unsigned(1 downto 0) := "01";
  constant A_LEN  : unsigned(1 downto 0) := "10";
  constant A_ACK  : unsigned(1 downto 0) := "11";

  -- authoritative state
  signal full_r  : std_logic_vector(1 downto 0);
  signal len0_r  : unsigned(LENW - 1 downto 0);
  signal len1_r  : unsigned(LENW - 1 downto 0);
  signal hw_ptr  : std_logic;
  signal fw_ptr  : std_logic;
  signal dtog    : std_logic;
  signal ep_en   : std_logic;
  signal ovr_r   : std_logic;
  signal bad_r   : std_logic;
  signal c_acc   : unsigned(15 downto 0);
  signal c_dup   : unsigned(15 downto 0);
  signal c_abt   : unsigned(15 downto 0);
  signal c_ovr   : unsigned(15 downto 0);
  signal c_bad   : unsigned(15 downto 0);

  -- derived
  signal both_full : std_logic;
  signal fw_ctrl   : std_logic;
  signal fw_ack    : std_logic;
  signal fw_dtog   : std_logic;
  signal cmt       : std_logic;
  signal cmt_new   : std_logic;
  signal cmt_dup   : std_logic;
  signal accept_s  : std_logic;
  signal overrun_s : std_logic;
  signal own_at_fw : std_logic;
  signal own_at_hw : std_logic;
  signal ack_ok    : std_logic;
  signal ack_bad   : std_logic;
  signal set_mask  : std_logic_vector(1 downto 0);
  signal clr_mask  : std_logic_vector(1 downto 0);
  signal len_cur   : unsigned(LENW - 1 downto 0);

  -- Pick the bit a one-bit pointer selects. Written once so the
  -- selection cannot be spelled two different ways in two places.
  function pick (v : std_logic_vector(1 downto 0); s : std_logic)
    return std_logic is
  begin
    if s = '1' then return v(1); else return v(0); end if;
  end function pick;

begin

  both_full <= full_r(0) and full_r(1);

  fw_ctrl <= '1' when (fw_we = '1' and fw_addr = A_CTRL) else '0';
  fw_ack  <= '1' when (fw_we = '1' and fw_addr = A_ACK and fw_wdata(0) = '1')
             else '0';
  fw_dtog <= fw_ctrl and fw_wdata(1);

  cmt     <= rx_commit and ep_en;
  cmt_new <= '1' when (cmt = '1' and rx_pid_odd = dtog)  else '0';
  cmt_dup <= '1' when (cmt = '1' and rx_pid_odd /= dtog) else '0';

  accept_s  <= cmt_new and (not both_full);
  overrun_s <= cmt_new and both_full;

  own_at_fw <= pick(full_r, fw_ptr);
  own_at_hw <= pick(full_r, hw_ptr);
  ack_ok    <= fw_ack and own_at_fw;
  ack_bad   <= fw_ack and (not own_at_fw);

  set_mask <= "10" when (accept_s = '1' and hw_ptr = '1') else
              "01" when (accept_s = '1')                  else "00";
  clr_mask <= "10" when (ack_ok   = '1' and fw_ptr = '1') else
              "01" when (ack_ok   = '1')                  else "00";

  seq : process (clk, rst_n)
  begin
    if rst_n = '0' then
      full_r <= "00";
      len0_r <= (others => '0');
      len1_r <= (others => '0');
      hw_ptr <= '0';
      fw_ptr <= '0';
      dtog   <= '0';
      ep_en  <= '0';
      ovr_r  <= '0';
      bad_r  <= '0';
      c_acc  <= (others => '0');
      c_dup  <= (others => '0');
      c_abt  <= (others => '0');
      c_ovr  <= (others => '0');
      c_bad  <= (others => '0');
    elsif rising_edge(clk) then
      if usb_reset = '1' then
        -- The data state, and only the data state. ep_en is absent from
        -- this list on purpose, and so are the counters.
        full_r <= "00";
        len0_r <= (others => '0');
        len1_r <= (others => '0');
        hw_ptr <= '0';
        fw_ptr <= '0';
        dtog   <= '0';
        ovr_r  <= '0';
        bad_r  <= '0';
      else
        -- One assignment, so a hardware set and a firmware release in the
        -- same cycle compose instead of one losing to the other.
        full_r <= (full_r or set_mask) and (not clr_mask);

        if accept_s = '1' then
          if hw_ptr = '1' then len1_r <= rx_len;
          else                 len0_r <= rx_len;
          end if;
          hw_ptr <= not hw_ptr;
          c_acc  <= c_acc + 1;
        end if;

        if ack_ok = '1' then
          fw_ptr <= not fw_ptr;
        end if;

        -- A delta from two independent sources.
        dtog <= dtog xor accept_s xor fw_dtog;

        if fw_ctrl = '1' then
          ep_en <= fw_wdata(0);
        end if;
        if overrun_s = '1' then
          ovr_r <= '1';
          c_ovr <= c_ovr + 1;
        end if;
        if ack_bad = '1' then
          bad_r <= '1';
          c_bad <= c_bad + 1;
        end if;
        if cmt_dup = '1' then
          c_dup <= c_dup + 1;
        end if;
        if rx_abort = '1' then
          c_abt <= c_abt + 1;
        end if;
      end if;
    end if;
  end process seq;

  len_cur <= len1_r when fw_ptr = '1' else len0_r;

  hw_nak     <= (not ep_en) or both_full;
  hw_buf_sel <= hw_ptr;
  hw_dup     <= cmt_dup;
  irq        <= ep_en and (full_r(0) or full_r(1));

  rdmux : process (fw_addr, dtog, ep_en, bad_r, ovr_r, hw_ptr, fw_ptr,
                   full_r, len_cur)
    variable v : std_logic_vector(15 downto 0);
  begin
    v := (others => '0');
    case fw_addr is
      when A_CTRL =>
        v(0) := ep_en;
        v(1) := dtog;
      when A_STAT =>
        v(1 downto 0) := full_r;
        v(2)          := fw_ptr;
        v(3)          := hw_ptr;
        v(4)          := ovr_r;
        v(5)          := bad_r;
      when A_LEN =>
        v(LENW - 1 downto 0) := std_logic_vector(len_cur);
      when others =>
        v := (others => '0');
    end case;
    fw_rdata <= v;
  end process rdmux;

  n_accept  <= c_acc;
  n_dup     <= c_dup;
  n_abort   <= c_abt;
  n_overrun <= c_ovr;
  n_badack  <= c_bad;

end architecture rtl;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  tb_usbep_pingpong -- VHDL-2008 testbench for usbep_pingpong.
--
--  Phases 1-4 present the SAME directed stimulus, in the same order, as
--  the Verilog-2005 and SystemVerilog benches, so their directed check
--  counts must agree to the digit. Phase 5 uses its own generator and is
--  not expected to agree.
--
--  The reference model lives in process VARIABLES rather than signals.
--  That is not a stylistic choice: a variable updates immediately, which
--  is what a model stepped inside the same process as the stimulus needs,
--  and it also makes a second driver impossible.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;

entity tb_usbep_pingpong is
end entity tb_usbep_pingpong;

architecture sim of tb_usbep_pingpong is

  constant MAXPKT : natural := 64;
  constant LENW   : natural := 7;
  constant HALF   : time    := 10 ns;

  signal clk        : std_logic := '0';
  signal rst_n      : std_logic := '0';
  signal usb_reset  : std_logic := '0';
  signal rx_commit  : std_logic := '0';
  signal rx_pid_odd : std_logic := '0';
  signal rx_len     : unsigned(LENW - 1 downto 0) := (others => '0');
  signal rx_abort   : std_logic := '0';
  signal fw_we      : std_logic := '0';
  signal fw_addr    : unsigned(1 downto 0) := "00";
  signal fw_wdata   : std_logic_vector(15 downto 0) := (others => '0');

  signal hw_nak     : std_logic;
  signal hw_buf_sel : std_logic;
  signal hw_dup     : std_logic;
  signal fw_rdata   : std_logic_vector(15 downto 0);
  signal irq        : std_logic;
  signal n_accept   : unsigned(15 downto 0);
  signal n_dup      : unsigned(15 downto 0);
  signal n_abort    : unsigned(15 downto 0);
  signal n_overrun  : unsigned(15 downto 0);
  signal n_badack   : unsigned(15 downto 0);

  signal done : boolean := false;

  function pick (v : std_logic_vector(1 downto 0); s : std_logic)
    return std_logic is
  begin
    if s = '1' then return v(1); else return v(0); end if;
  end function pick;

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

begin

  dut : entity work.usbep_pingpong
    generic map (MAXPKT => MAXPKT, LENW => LENW)
    port map (
      clk => clk, rst_n => rst_n, usb_reset => usb_reset,
      rx_commit => rx_commit, rx_pid_odd => rx_pid_odd,
      rx_len => rx_len, rx_abort => rx_abort,
      hw_nak => hw_nak, hw_buf_sel => hw_buf_sel, hw_dup => hw_dup,
      fw_we => fw_we, fw_addr => fw_addr, fw_wdata => fw_wdata,
      fw_rdata => fw_rdata, irq => irq,
      n_accept => n_accept, n_dup => n_dup, n_abort => n_abort,
      n_overrun => n_overrun, n_badack => n_badack
    );

  clkgen : process
  begin
    while not done loop
      clk <= '0'; wait for HALF;
      clk <= '1'; wait for HALF;
    end loop;
    wait;
  end process clkgen;

  stim : process
    -- ---- bookkeeping ----
    variable chk_dir, chk_rnd, errs : natural := 0;
    variable in_random              : boolean := false;
    variable shown                  : natural := 0;

    -- ---- the independent reference model ----
    variable rm_full : std_logic_vector(1 downto 0) := "00";
    variable rm_len0 : unsigned(LENW - 1 downto 0) := (others => '0');
    variable rm_len1 : unsigned(LENW - 1 downto 0) := (others => '0');
    variable rm_hw   : std_logic := '0';
    variable rm_fw   : std_logic := '0';
    variable rm_dtog : std_logic := '0';
    variable rm_en   : std_logic := '0';
    variable rm_ovr  : std_logic := '0';
    variable rm_bad  : std_logic := '0';
    variable rm_acc, rm_dup, rm_abt, rm_ovc, rm_bdc : natural := 0;

    -- ---- measured reachability ----
    variable meas_accept, meas_dup, meas_abort, meas_ovr, meas_bad : natural := 0;
    variable meas_bothfull, meas_nak, meas_zlp, meas_maxpkt        : natural := 0;
    variable meas_dtograce, meas_setclr, meas_setupfail             : natural := 0;

    -- ---- phase bookkeeping ----
    variable q_len : integer_vector(0 to 255) := (others => 0);
    variable q_wr, q_rd : natural := 0;
    variable ord_pkts, ord_naked, ord_read, ord_bothfull : natural := 0;
    variable par_pairs, par_len_skipped, par_reset_cases : natural := 0;
    variable par_stat, par_ctrl, par_len : integer_vector(0 to 1) := (0, 0);
    variable par_nak, par_irq, par_sel   : integer_vector(0 to 1) := (0, 0);
    variable seed1 : positive := 981_173;
    variable seed2 : positive := 27_961;

    procedure bump is
    begin
      if in_random then chk_rnd := chk_rnd + 1;
      else              chk_dir := chk_dir + 1;
      end if;
    end procedure bump;

    procedure ck (what : string; got : integer; exp : integer) is
    begin
      bump;
      if got /= exp then
        errs := errs + 1;
        if shown < 60 then
          shown := shown + 1;
          report "  ** " & what & ": got " & integer'image(got) &
                 " expected " & integer'image(exp) severity warning;
        end if;
      end if;
    end procedure ck;

    -- Advance the model from the input pins as they stand at this edge.
    procedure ref_step is
      variable both, ctl, dtg, cm, cnew, cdup, acc, ovr, aok, abad : boolean;
      variable hw_old, fw_old : std_logic;
    begin
      hw_old := rm_hw;
      fw_old := rm_fw;
      if rst_n = '0' then
        rm_full := "00";
        rm_len0 := (others => '0'); rm_len1 := (others => '0');
        rm_hw := '0'; rm_fw := '0'; rm_dtog := '0'; rm_en := '0';
        rm_ovr := '0'; rm_bad := '0';
        rm_acc := 0; rm_dup := 0; rm_abt := 0; rm_ovc := 0; rm_bdc := 0;
      elsif usb_reset = '1' then
        rm_full := "00";
        rm_len0 := (others => '0'); rm_len1 := (others => '0');
        rm_hw := '0'; rm_fw := '0'; rm_dtog := '0';
        rm_ovr := '0'; rm_bad := '0';
      else
        both := (rm_full(0) = '1') and (rm_full(1) = '1');
        ctl  := (fw_we = '1') and (fw_addr = "00");
        dtg  := ctl and (fw_wdata(1) = '1');
        cm   := (rx_commit = '1') and (rm_en = '1');
        cnew := cm and (rx_pid_odd = rm_dtog);
        cdup := cm and (rx_pid_odd /= rm_dtog);
        acc  := cnew and not both;
        ovr  := cnew and both;
        aok  := (fw_we = '1') and (fw_addr = "11") and (fw_wdata(0) = '1')
                and (pick(rm_full, fw_old) = '1');
        abad := (fw_we = '1') and (fw_addr = "11") and (fw_wdata(0) = '1')
                and (pick(rm_full, fw_old) = '0');

        if acc then
          if hw_old = '1' then rm_full(1) := '1'; rm_len1 := rx_len;
          else                 rm_full(0) := '1'; rm_len0 := rx_len;
          end if;
          rm_hw  := not hw_old;
          rm_acc := rm_acc + 1;
        end if;
        if aok then
          if fw_old = '1' then rm_full(1) := '0'; else rm_full(0) := '0'; end if;
          rm_fw := not fw_old;
        end if;
        if acc /= dtg then rm_dtog := not rm_dtog; end if;
        if ctl then rm_en := fw_wdata(0); end if;
        if ovr  then rm_ovr := '1'; rm_ovc := rm_ovc + 1; end if;
        if abad then rm_bad := '1'; rm_bdc := rm_bdc + 1; end if;
        if cdup then rm_dup := rm_dup + 1; end if;
        if rx_abort = '1' then rm_abt := rm_abt + 1; end if;

        if acc  then meas_accept := meas_accept + 1; end if;
        if cdup then meas_dup    := meas_dup    + 1; end if;
        if ovr  then meas_ovr    := meas_ovr    + 1; end if;
        if abad then meas_bad    := meas_bad    + 1; end if;
        if rx_abort = '1' then meas_abort := meas_abort + 1; end if;
        if acc and rx_len = 0 then meas_zlp := meas_zlp + 1; end if;
        if acc and to_integer(rx_len) = MAXPKT then
          meas_maxpkt := meas_maxpkt + 1;
        end if;
        if acc and dtg then meas_dtograce := meas_dtograce + 1; end if;
        if acc and aok then meas_setclr   := meas_setclr   + 1; end if;
      end if;
    end procedure ref_step;

    -- Everything is compared through the ports. Sweeping fw_addr with
    -- fw_we low has no side effect.
    procedure cmp is
      variable exp : integer;
    begin
      if (rm_full(0) = '1') and (rm_full(1) = '1') then
        meas_bothfull := meas_bothfull + 1;
      end if;
      if hw_nak = '1' then meas_nak := meas_nak + 1; end if;

      fw_addr <= "00"; wait for 1 ns;
      ck("CTRL", to_integer(unsigned(fw_rdata)),
         b2i(rm_en) + 2 * b2i(rm_dtog));
      fw_addr <= "01"; wait for 1 ns;
      exp := to_integer(unsigned(rm_full)) + 4 * b2i(rm_fw) + 8 * b2i(rm_hw)
             + 16 * b2i(rm_ovr) + 32 * b2i(rm_bad);
      ck("STAT", to_integer(unsigned(fw_rdata)), exp);
      fw_addr <= "10"; wait for 1 ns;
      if rm_fw = '1' then exp := to_integer(rm_len1);
      else                exp := to_integer(rm_len0);
      end if;
      ck("LEN", to_integer(unsigned(fw_rdata)), exp);
      fw_addr <= "11"; wait for 1 ns;
      ck("ACKRD", to_integer(unsigned(fw_rdata)), 0);

      if (rm_en = '0') or ((rm_full(0) = '1') and (rm_full(1) = '1')) then
        ck("hw_nak", b2i(hw_nak), 1);
      else
        ck("hw_nak", b2i(hw_nak), 0);
      end if;
      ck("hw_buf_sel", b2i(hw_buf_sel), b2i(rm_hw));
      if (rm_en = '1') and ((rm_full(0) = '1') or (rm_full(1) = '1')) then
        ck("irq", b2i(irq), 1);
      else
        ck("irq", b2i(irq), 0);
      end if;
      ck("n_accept",  to_integer(n_accept),  rm_acc);
      ck("n_dup",     to_integer(n_dup),     rm_dup);
      ck("n_abort",   to_integer(n_abort),   rm_abt);
      ck("n_overrun", to_integer(n_overrun), rm_ovc);
      ck("n_badack",  to_integer(n_badack),  rm_bdc);
      fw_addr <= "00";
    end procedure cmp;

    procedure step is
      variable want_dup : integer;
    begin
      wait for 1 ns;
      if (rx_commit = '1') and (rm_en = '1') and (rx_pid_odd /= rm_dtog) then
        want_dup := 1;
      else
        want_dup := 0;
      end if;
      ck("hw_dup", b2i(hw_dup), want_dup);
      wait until rising_edge(clk);
      ref_step;
      wait for 1 ns;
      cmp;
      rx_commit <= '0'; rx_abort <= '0'; fw_we <= '0'; usb_reset <= '0';
      rx_len    <= (others => '0');
      fw_wdata  <= (others => '0');
    end procedure step;

    procedure idle is begin step; end procedure idle;

    procedure wr (a : unsigned(1 downto 0); d : std_logic_vector(15 downto 0)) is
    begin
      fw_we <= '1'; fw_addr <= a; fw_wdata <= d; step;
    end procedure wr;

    procedure do_ack is
    begin
      wr("11", x"0001");
    end procedure do_ack;

    procedure do_dtog is
      variable d : std_logic_vector(15 downto 0) := (others => '0');
    begin
      d(1) := '1'; d(0) := rm_en;
      wr("00", d);
    end procedure do_dtog;

    procedure set_en (v : std_logic) is
      variable d : std_logic_vector(15 downto 0) := (others => '0');
    begin
      d(0) := v;
      wr("00", d);
    end procedure set_en;

    procedure commit (match : boolean; ln : natural) is
    begin
      rx_commit <= '1';
      if match then rx_pid_odd <= rm_dtog; else rx_pid_odd <= not rm_dtog; end if;
      rx_len <= to_unsigned(ln, LENW);
      step;
    end procedure commit;

    procedure abort_pkt is begin rx_abort <= '1'; step; end procedure abort_pkt;

    procedure bus_reset is begin usb_reset <= '1'; step; end procedure bus_reset;

    procedure hard_reset is
    begin
      rst_n     <= '0'; usb_reset <= '0';
      rx_commit <= '0'; rx_pid_odd <= '0';
      rx_len    <= (others => '0'); rx_abort <= '0';
      fw_we     <= '0'; fw_addr <= "00"; fw_wdata <= (others => '0');
      for i in 0 to 2 loop
        wait until rising_edge(clk);
        ref_step;
      end loop;
      wait for 1 ns;
      rst_n <= '1';
      wait until rising_edge(clk);
      ref_step;
      wait for 1 ns;
      cmp;
    end procedure hard_reset;

    -- Build one of the 12 reachable (occupancy, base pointer, toggle)
    -- states, then PROVE it was built.
    procedure setup_state (occ : natural; bp : natural; tog : natural) is
      variable stat, ctrl : integer;
      variable want_full, want_hw : integer;
    begin
      hard_reset;
      set_en('1');
      bus_reset;
      if bp = 1 then
        commit(true, 3);
        do_ack;
      end if;
      if occ >= 1 then commit(true, 5); end if;
      if occ >= 2 then commit(true, 9); end if;
      if b2i(rm_dtog) /= tog then do_dtog; end if;

      if occ = 0 then
        want_full := 0;
      elsif occ = 1 then
        if bp = 1 then want_full := 2; else want_full := 1; end if;
      else
        want_full := 3;
      end if;
      if occ = 1 then
        if bp = 1 then want_hw := 0; else want_hw := 1; end if;
      else
        want_hw := bp;
      end if;

      fw_addr <= "01"; wait for 1 ns; stat := to_integer(unsigned(fw_rdata));
      fw_addr <= "00"; wait for 1 ns; ctrl := to_integer(unsigned(fw_rdata));
      fw_addr <= "00";
      bump;
      if (stat mod 4) /= want_full then
        errs := errs + 1; meas_setupfail := meas_setupfail + 1;
        report "  ** setup full" severity warning;
      end if;
      bump;
      if ((stat / 4) mod 2) /= bp then
        errs := errs + 1; meas_setupfail := meas_setupfail + 1;
        report "  ** setup fw_ptr" severity warning;
      end if;
      bump;
      if ((stat / 8) mod 2) /= want_hw then
        errs := errs + 1; meas_setupfail := meas_setupfail + 1;
        report "  ** setup hw_ptr" severity warning;
      end if;
      bump;
      if ((ctrl / 2) mod 2) /= tog then
        errs := errs + 1; meas_setupfail := meas_setupfail + 1;
        report "  ** setup dtog" severity warning;
      end if;
    end procedure setup_state;

    procedure apply_event (ev : natural) is
      variable d : std_logic_vector(15 downto 0) := (others => '0');
    begin
      case ev is
        when 0 => idle;
        when 1 => do_ack;
        when 2 => abort_pkt;
        when 3 => commit(true, 0);
        when 4 => commit(true, 1);
        when 5 => commit(true, MAXPKT);
        when 6 => commit(false, 13);
        when 7 =>
          rx_commit <= '1'; rx_pid_odd <= rm_dtog;
          rx_len    <= to_unsigned(7, LENW);
          fw_we     <= '1'; fw_addr <= "11"; fw_wdata <= x"0001";
          step;
        when 8 =>
          rx_commit <= '1'; rx_pid_odd <= not rm_dtog;
          rx_len    <= to_unsigned(13, LENW);
          fw_we     <= '1'; fw_addr <= "11"; fw_wdata <= x"0001";
          step;
        when 9 =>
          rx_abort <= '1';
          fw_we    <= '1'; fw_addr <= "11"; fw_wdata <= x"0001";
          step;
        when 10 => bus_reset;
        when others => idle;
      end case;
    end procedure apply_event;

    -- ---- PHASE 1 ----
    procedure phase_sweep is
    begin
      for occ_i in 0 to 2 loop
        for bp_i in 0 to 1 loop
          for tg_i in 0 to 1 loop
            for ev_i in 0 to 10 loop
              setup_state(occ_i, bp_i, tg_i);
              cmp;
              apply_event(ev_i);
              idle;
            end loop;
          end loop;
        end loop;
      end loop;
    end procedure phase_sweep;

    -- ---- PHASE 2 ----
    procedure phase_boundary is
      variable s1, s2 : integer;
      variable d      : std_logic_vector(15 downto 0) := (others => '0');
    begin
      -- B1 an accepted packet and a firmware toggle write in one cycle
      hard_reset; set_en('1'); bus_reset;
      rx_commit <= '1'; rx_pid_odd <= rm_dtog; rx_len <= to_unsigned(4, LENW);
      d := (others => '0'); d(1) := '1'; d(0) := '1';
      fw_we <= '1'; fw_addr <= "00"; fw_wdata <= d;
      step;
      fw_addr <= "00"; wait for 1 ns; s1 := to_integer(unsigned(fw_rdata));
      ck("B1 dtog unchanged", (s1 / 2) mod 2, 0);
      ck("B1 packet still taken", to_integer(n_accept), 1);

      -- B2 MAXPKT reads back intact
      hard_reset; set_en('1'); bus_reset;
      commit(true, MAXPKT);
      fw_addr <= "10"; wait for 1 ns; s1 := to_integer(unsigned(fw_rdata));
      fw_addr <= "00";
      ck("B2 MAXPKT length", s1, MAXPKT);

      -- B3 a zero-length packet is a packet
      hard_reset; set_en('1'); bus_reset;
      commit(true, 0);
      ck("B3 zlp irq", b2i(irq), 1);
      ck("B3 zlp accept", to_integer(n_accept), 1);
      fw_addr <= "10"; wait for 1 ns; s1 := to_integer(unsigned(fw_rdata));
      fw_addr <= "00";
      ck("B3 zlp length", s1, 0);

      -- B4 an overrun drops the packet; it must not overwrite
      hard_reset; set_en('1'); bus_reset;
      commit(true, 11); commit(true, 22);
      ck("B4 nak asserted", b2i(hw_nak), 1);
      rx_commit <= '1'; rx_pid_odd <= rm_dtog; rx_len <= to_unsigned(33, LENW);
      step;
      ck("B4 overrun counted", to_integer(n_overrun), 1);
      ck("B4 accept unchanged", to_integer(n_accept), 2);
      fw_addr <= "10"; wait for 1 ns; s1 := to_integer(unsigned(fw_rdata));
      fw_addr <= "00";
      ck("B4 first length intact", s1, 11);
      do_ack;
      fw_addr <= "10"; wait for 1 ns; s2 := to_integer(unsigned(fw_rdata));
      fw_addr <= "00";
      ck("B4 second length intact", s2, 22);

      -- B5 an illegal release must not move the read pointer
      hard_reset; set_en('1'); bus_reset;
      do_ack;
      ck("B5 badack counted", to_integer(n_badack), 1);
      fw_addr <= "01"; wait for 1 ns; s1 := to_integer(unsigned(fw_rdata));
      fw_addr <= "00";
      ck("B5 fw_ptr still 0", (s1 / 4) mod 2, 0);
      commit(true, 17);
      fw_addr <= "10"; wait for 1 ns; s2 := to_integer(unsigned(fw_rdata));
      fw_addr <= "00";
      ck("B5 packet readable", s2, 17);

      -- B6 an aborted packet advances nothing and flips nothing
      hard_reset; set_en('1'); bus_reset;
      abort_pkt;
      fw_addr <= "01"; wait for 1 ns; s1 := to_integer(unsigned(fw_rdata));
      fw_addr <= "00";
      ck("B6 no buffer taken", s1 mod 4, 0);
      ck("B6 hw_ptr still 0", (s1 / 8) mod 2, 0);
      fw_addr <= "00"; wait for 1 ns; s2 := to_integer(unsigned(fw_rdata));
      ck("B6 dtog still 0", (s2 / 2) mod 2, 0);
      commit(true, 8);
      ck("B6 retry accepted", to_integer(n_accept), 1);

      -- B7 back-to-back packets on consecutive cycles
      hard_reset; set_en('1'); bus_reset;
      rx_commit <= '1'; rx_pid_odd <= rm_dtog; rx_len <= to_unsigned(2, LENW);
      wait for 1 ns; wait until rising_edge(clk); ref_step; wait for 1 ns;
      rx_commit <= '1'; rx_pid_odd <= rm_dtog; rx_len <= to_unsigned(3, LENW);
      wait for 1 ns; wait until rising_edge(clk); ref_step; wait for 1 ns;
      rx_commit <= '0'; rx_len <= (others => '0');
      cmp;
      ck("B7 both taken", to_integer(n_accept), 2);
      fw_addr <= "01"; wait for 1 ns; s1 := to_integer(unsigned(fw_rdata));
      fw_addr <= "00";
      ck("B7 both full", s1 mod 4, 3);

      -- B8 a bus reset clears the data state and leaves ep_en alone
      hard_reset; set_en('1');
      commit(true, 9);
      fw_addr <= "00"; wait for 1 ns; s1 := to_integer(unsigned(fw_rdata));
      ck("B8 dtog is 1 first", (s1 / 2) mod 2, 1);
      bus_reset;
      fw_addr <= "00"; wait for 1 ns; s1 := to_integer(unsigned(fw_rdata));
      ck("B8 ep_en survives", s1 mod 2, 1);
      ck("B8 dtog cleared", (s1 / 2) mod 2, 0);
      fw_addr <= "01"; wait for 1 ns; s2 := to_integer(unsigned(fw_rdata));
      fw_addr <= "00";
      ck("B8 buffers returned", s2 mod 4, 0);
      ck("B8 pointers zeroed", (s2 / 4) mod 4, 0);
      rx_commit <= '1'; rx_pid_odd <= '0'; rx_len <= to_unsigned(6, LENW);
      step;
      ck("B8 DATA0 accepted", to_integer(n_accept), 2);

      -- B9 disabling the endpoint hides the interrupt, keeps the data
      hard_reset; set_en('1'); bus_reset;
      commit(true, 21);
      set_en('0');
      ck("B9 irq gone", b2i(irq), 0);
      ck("B9 nak forced", b2i(hw_nak), 1);
      fw_addr <= "01"; wait for 1 ns; s1 := to_integer(unsigned(fw_rdata));
      fw_addr <= "00";
      ck("B9 buffer kept", s1 mod 4, 1);
      set_en('1');
      ck("B9 irq returns", b2i(irq), 1);
      fw_addr <= "10"; wait for 1 ns; s2 := to_integer(unsigned(fw_rdata));
      fw_addr <= "00";
      ck("B9 length kept", s2, 21);

      -- B10 a commit into a disabled endpoint is nothing at all
      hard_reset; bus_reset;
      rx_commit <= '1'; rx_pid_odd <= '1'; rx_len <= to_unsigned(5, LENW);
      step;
      ck("B10 nothing counted",
         to_integer(n_accept) + to_integer(n_dup) + to_integer(n_overrun), 0);

      -- B11 a release and a bus reset in the same cycle: reset wins
      hard_reset; set_en('1'); bus_reset;
      commit(true, 12);
      usb_reset <= '1';
      fw_we <= '1'; fw_addr <= "11"; fw_wdata <= x"0001";
      step;
      fw_addr <= "01"; wait for 1 ns; s1 := to_integer(unsigned(fw_rdata));
      fw_addr <= "00";
      ck("B11 buffers clear", s1 mod 4, 0);
      ck("B11 fw_ptr zeroed", (s1 / 4) mod 2, 0);
      ck("B11 no bad ack", to_integer(n_badack), 0);
    end procedure phase_boundary;

    -- ---- PHASE 3 ----
    procedure phase_order is
      variable svc  : integer_vector(0 to 7) := (0, 0, 2, 1, 0, 2, 1, 2);
      variable want, drained, rl : integer;
    begin
      hard_reset; set_en('1'); bus_reset;
      q_wr := 0; q_rd := 0;
      ord_pkts := 0; ord_naked := 0; ord_read := 0; ord_bothfull := 0;
      for n in 0 to 95 loop
        if hw_nak = '1' then
          ord_naked := ord_naked + 1;
          idle;
        else
          q_len(q_wr) := (n * 7) mod (MAXPKT + 1);
          q_wr := q_wr + 1;
          ord_pkts := ord_pkts + 1;
          commit(true, (n * 7) mod (MAXPKT + 1));
        end if;
        if hw_nak = '1' then ord_bothfull := ord_bothfull + 1; end if;
        for c in 1 to svc(n mod 8) loop
          if (irq = '1') and (q_rd < q_wr) then
            fw_addr <= "10"; wait for 1 ns; rl := to_integer(unsigned(fw_rdata));
            fw_addr <= "00";
            want := q_len(q_rd); q_rd := q_rd + 1; ord_read := ord_read + 1;
            ck("order", rl, want);
            do_ack;
          elsif irq = '1' then
            -- more releases than commits: the pointers have desynchronised
            bump; errs := errs + 1; do_ack;
          else
            idle;
          end if;
        end loop;
      end loop;
      drained := 0;
      while (irq = '1') and (drained < 8) and (q_rd < q_wr) loop
        fw_addr <= "10"; wait for 1 ns; rl := to_integer(unsigned(fw_rdata));
        fw_addr <= "00";
        want := q_len(q_rd); q_rd := q_rd + 1; ord_read := ord_read + 1;
        ck("order-drain", rl, want);
        do_ack;
        drained := drained + 1;
      end loop;
      ck("order count", ord_read, ord_pkts);
      bump;
      if ord_naked < 4 then
        errs := errs + 1;
        report "  ** order: backpressure never reached" severity warning;
      end if;
      bump;
      if ord_bothfull < 8 then
        errs := errs + 1;
        report "  ** order: both buffers rarely full" severity warning;
      end if;
    end procedure phase_order;

    -- ---- PHASE 4 ----
    procedure phase_parity is
    begin
      par_pairs := 0; par_len_skipped := 0; par_reset_cases := 0;
      for occ_i in 0 to 2 loop
        for tg_i in 0 to 1 loop
          for ev_i in 0 to 10 loop
            for side in 0 to 1 loop
              setup_state(occ_i, side, tg_i);
              apply_event(ev_i);
              fw_addr <= "01"; wait for 1 ns;
              par_stat(side) := to_integer(unsigned(fw_rdata));
              fw_addr <= "00"; wait for 1 ns;
              par_ctrl(side) := to_integer(unsigned(fw_rdata));
              fw_addr <= "10"; wait for 1 ns;
              par_len(side)  := to_integer(unsigned(fw_rdata));
              fw_addr <= "00";
              par_nak(side) := b2i(hw_nak);
              par_irq(side) := b2i(irq);
              par_sel(side) := b2i(hw_buf_sel);
            end loop;
            par_pairs := par_pairs + 1;
            ck("par-ctrl", par_ctrl(0), par_ctrl(1));
            ck("par-nak",  par_nak(0),  par_nak(1));
            ck("par-irq",  par_irq(0),  par_irq(1));
            ck("par-sticky", (par_stat(0) / 16) mod 4, (par_stat(1) / 16) mod 4);
            ck("par-own", par_irq(0), par_irq(1));
            if (par_irq(0) = 1) and (par_irq(1) = 1) then
              ck("par-len", par_len(0), par_len(1));
            else
              par_len_skipped := par_len_skipped + 1;
            end if;
            -- the ownership mask must be the other way round
            ck("par-full-swap", par_stat(0) mod 4,
               ((par_stat(1) mod 4) / 2) + 2 * ((par_stat(1) mod 4) mod 2));
            if ev_i /= 10 then
              ck("par-fwptr-inv",  (par_stat(0) / 4) mod 2,
                 1 - ((par_stat(1) / 4) mod 2));
              ck("par-hwptr-inv",  (par_stat(0) / 8) mod 2,
                 1 - ((par_stat(1) / 8) mod 2));
              ck("par-bufsel-inv", par_sel(0), 1 - par_sel(1));
            else
              par_reset_cases := par_reset_cases + 1;
              ck("par-rst-fwptr",
                 ((par_stat(0) / 4) mod 2) + ((par_stat(1) / 4) mod 2), 0);
              ck("par-rst-hwptr",
                 ((par_stat(0) / 8) mod 2) + ((par_stat(1) / 8) mod 2), 0);
              ck("par-rst-bufsel", par_sel(0) + par_sel(1), 0);
            end if;
          end loop;
        end loop;
      end loop;
    end procedure phase_parity;

    -- ---- PHASE 5 ----
    impure function rnd (n : positive) return natural is
      variable x : real;
    begin
      uniform(seed1, seed2, x);
      return natural(real(n - 1) * x);
    end function rnd;

    procedure phase_random is
      variable r, lat : natural;
      variable d      : std_logic_vector(15 downto 0) := (others => '0');
    begin
      in_random := true;
      hard_reset; set_en('1'); bus_reset;
      for n in 0 to 3999 loop
        r := rnd(100);
        if r < 55 then
          if (hw_nak = '0') or (rnd(100) < 3) then
            rx_commit <= '1';
            if rnd(100) < 12 then rx_pid_odd <= not rm_dtog;
            else                  rx_pid_odd <= rm_dtog;
            end if;
            rx_len <= to_unsigned(rnd(MAXPKT + 1), LENW);
            step;
          else
            idle;
          end if;
        elsif r < 62 then
          abort_pkt;
        elsif r < 78 then
          do_ack;
        elsif r < 85 then
          -- A packet arriving on the SAME CYCLE as a firmware release.
          -- The two sides are independent agents, so this happens in real
          -- hardware -- and a random phase that issues one action per
          -- cycle can never produce it.
          -- and it only EXISTS when firmware owns one buffer and the
          -- hardware has the other, so the branch is steered at that
          -- window rather than hoping to land in it
          if (irq = '1') and (hw_nak = '0') then
            rx_commit <= '1'; rx_pid_odd <= rm_dtog;
            rx_len    <= to_unsigned(rnd(MAXPKT + 1), LENW);
            fw_we <= '1'; fw_addr <= "11"; fw_wdata <= x"0001";
            step;
          else
            idle;
          end if;
        elsif r < 89 then
          rx_commit <= '1'; rx_pid_odd <= rm_dtog;
          rx_len    <= to_unsigned(rnd(MAXPKT + 1), LENW);
          d := (others => '0'); d(1) := '1'; d(0) := '1';
          fw_we <= '1'; fw_addr <= "00"; fw_wdata <= d;
          step;
        elsif r < 92 then
          do_dtog;
        elsif r < 95 then
          bus_reset;
        elsif r < 97 then
          if rnd(100) < 20 then set_en('0'); else set_en('1'); end if;
        else
          lat := rnd(5);
          for c in 0 to lat loop idle; end loop;
        end if;
      end loop;
      set_en('1');
      in_random := false;
    end procedure phase_random;

  begin
    phase_sweep;
    report "  phase 1 state sweep     : " & integer'image(chk_dir) &
           " checks, " & integer'image(errs) & " errors";
    phase_boundary;
    report "  phase 2 boundary        : " & integer'image(chk_dir) &
           " checks, " & integer'image(errs) & " errors";
    phase_order;
    report "  phase 3 order           : " & integer'image(chk_dir) &
           " checks, " & integer'image(errs) & " errors  (" &
           integer'image(ord_pkts) & " packets, " &
           integer'image(ord_naked) & " NAKed offers, " &
           integer'image(ord_bothfull) & " both-full)";
    phase_parity;
    report "  phase 4 parity          : " & integer'image(chk_dir) &
           " checks, " & integer'image(errs) & " errors  (" &
           integer'image(par_pairs) & " pairs, " &
           integer'image(par_reset_cases) & " reset exceptions, " &
           integer'image(par_len_skipped) & " LEN undefined)";
    report "  ---- DIRECTED-ONLY TOTAL: " & integer'image(chk_dir) &
           " checks, " & integer'image(errs) & " errors ----";
    phase_random;

    report "  measured reachability (all phases)";
    report "    accepted packets ....... " & integer'image(meas_accept);
    report "    of which zero-length ... " & integer'image(meas_zlp);
    report "    of which MAXPKT ........ " & integer'image(meas_maxpkt);
    report "    retransmissions ........ " & integer'image(meas_dup);
    report "    aborted packets ........ " & integer'image(meas_abort);
    report "    overruns ............... " & integer'image(meas_ovr);
    report "    illegal releases ....... " & integer'image(meas_bad);
    report "    accept+toggle-write .... " & integer'image(meas_dtograce);
    report "    accept+release ......... " & integer'image(meas_setclr);
    report "    cycles with both full .. " & integer'image(meas_bothfull);
    report "    cycles asserting NAK ... " & integer'image(meas_nak);
    report "    setup failures ......... " & integer'image(meas_setupfail);
    report "  directed checks .......... " & integer'image(chk_dir);
    report "  random checks ............ " & integer'image(chk_rnd);
    report "  TOTAL checks ............. " & integer'image(chk_dir + chk_rnd);
    report "  ERRORS ................... " & integer'image(errs);
    if errs = 0 then report "  PASS"; else report "  FAIL" severity failure; end if;
    done <= true;
    wait;
  end process stim;

end architecture sim;

11. Assertions

Twelve properties, in six categories, written as SVA for a tool that supports concurrent assertions. Icarus Verilog 13.0 rejects them outright, so each is listed with the procedural check that enforces it in the runs above — and the pairing is not decorative: every one of the named checks below is a check that actually executes and is counted in the 30,356.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    CATEGORY      PROPERTY                        ENFORCED BY
    ------------  ------------------------------  --------------------------
    safety        p_no_overwrite                  the cmp STAT + LEN checks,
                                                  every cycle of every phase
    safety        p_nak_is_honoured               boundary B4, plus the
                                                  n_overrun comparison
    consistency   p_outputs_are_derived           the cmp hw_nak + irq checks
    bounds        p_length_in_range               the cmp LEN check, and B2
    ordering      p_ptrs_step_once                the cmp STAT check, which
                                                  compares both pointers
    ordering      p_fw_ptr_needs_ownership        boundary B5
    stability     p_len_stable_while_owned        the cmp LEN check across
                                                  the idle cycle of every
                                                  one of the 132 transitions
    reset         p_bus_reset_scope               boundary B8, and event E10
                                                  in all 12 states
    toggle        p_toggle_flips_on_accept        the cmp CTRL check
    toggle        p_toggle_holds_on_dup_or_abort  events E2 and E6, 12 states
    toggle        p_toggle_composes               boundary B1 -- and ONLY B1
    progress      p_release_makes_progress        phase 3, 84 packets

The assertion source is in the SystemVerilog module above, behind SVA_ON. Two of the twelve are worth reading closely.

12. Where UVM Fits

For this block the trade genuinely flips, and for a reason that is specific rather than general: there are two agents, not one. The engine and the firmware are independent initiators writing to shared state, and every bug in section 13 that a directed bench nearly missed was a bug in the region where their timing overlaps.

A directed bench reaches those regions by hand-building each coincidence. That works — boundary scenario B1 is exactly such a hand-build — and the cost is that the coincidences you did not think of are simply absent. Two sequencers running concurrently produce them by construction.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  The item is an ARCHITECTURAL EVENT, not a bundle of pins. There is no
//  field here for rx_commit, because "a packet arrived" is the event and
//  the pulse is how it is delivered.
//
//  ILLUSTRATIVE. This environment is not compiled or run in this
//  chapter: Icarus Verilog cannot run UVM, and the measurements in
//  sections 8 and 14 come from the procedural benches above.
// =====================================================================
typedef enum {
  ENG_PACKET,      // a packet whose CRC checked out
  ENG_ABORT,       // a packet whose CRC did not
  FW_SERVICE,      // read LEN, then release
  FW_TOGGLE,       // write 1 to CTRL bit 1
  FW_ENABLE,       // write CTRL bit 0
  BUS_RESET        // the SE0 detector fired
} ep_event_e;

class ep_txn extends uvm_sequence_item;
  `uvm_object_utils(ep_txn)

  rand ep_event_e   kind;
  rand int unsigned len;          // meaningful for ENG_PACKET
  rand bit          pid_matches;  // 0 makes it a retransmission
  rand int unsigned delay;        // cycles before this event

  // ---- constraints that put the stimulus where the mechanism is ----

  // The length distribution is deliberately NOT uniform. Uniform over
  // 0..64 spends 1.5% of its packets on the two boundaries that matter
  // and the rest in a middle that behaves identically throughout.
  constraint c_len {
    kind == ENG_PACKET -> len dist {
      0                :/ 15,     // the ZLP -- a real packet
      1                :/ 10,
      [2 : MAXPKT-1]   :/ 45,
      MAXPKT           :/ 30      // the width boundary
    };
  }

  // A retransmission is rare on a healthy link and is the entire reason
  // the toggle exists, so it is weighted up rather than left to chance.
  constraint c_pid { pid_matches dist { 1 :/ 85, 0 :/ 15 }; }

  // The firmware service delay is the axis that decides whether the
  // SECOND BUFFER IS EVER USED. A model that always services promptly
  // tests a one-deep queue. The tail here is what produces backpressure.
  constraint c_delay {
    kind == FW_SERVICE -> delay dist {
      0        :/ 30,
      [1 : 3]  :/ 40,
      [4 : 12] :/ 30              // longer than the packet interval
    };
  }
endclass

// =====================================================================
//  TWO agents. This is the structural claim of the section: the engine
//  and firmware are separate initiators, and a single sequencer cannot
//  express "these two things happened on the same cycle" because it is
//  the thing that serialises them.
// =====================================================================
class ep_env extends uvm_env;
  `uvm_component_utils(ep_env)

  eng_agent  eng;      // drives rx_commit / rx_pid_odd / rx_len / rx_abort
  fw_agent   fw;       // drives the peripheral bus
  ep_model   model;    // the ownership reference model
  ep_sb      sb;       // the scoreboard
  ep_cov     cov;      // coverage
endclass

// =====================================================================
//  The reference model is a TWO-DEEP QUEUE plus a toggle, and it is
//  deliberately not a copy of the RTL's structure: it has no pointers.
//  A model that reproduced hw_ptr and fw_ptr could reproduce a pointer
//  bug along with them.
// =====================================================================
class ep_model extends uvm_component;
  `uvm_component_utils(ep_model)

  int unsigned q[$];          // lengths, oldest first. At most two.
  bit          expect_dtog;   // the PID this endpoint expects next
  bit          enabled;

  function void engine_packet(int unsigned len, bit pid);
    if (!enabled) return;                  // a disabled endpoint sees nothing
    if (pid != expect_dtog) return;        // a resend: ACKed, not stored
    if (q.size() == 2) begin               // hw_nak should have prevented this
      `uvm_error("EP", "overrun: engine committed while both buffers were full")
      return;
    end
    q.push_back(len);
    expect_dtog = ~expect_dtog;
  endfunction

  function void firmware_service();
    if (q.size() == 0) begin
      // legal for firmware to try, illegal for the endpoint to move on it
      `uvm_info("EP", "release with nothing owned", UVM_HIGH)
      return;
    end
    void'(q.pop_front());
  endfunction

  function void bus_reset();
    q.delete();
    expect_dtog = 1'b0;        // the part that makes the device deaf if missed
    // enabled deliberately survives
  endfunction

  function bit nak();  return !enabled || q.size() == 2; endfunction
  function bit irq();  return  enabled && q.size() > 0;  endfunction
endclass

// =====================================================================
//  Coverage that answers questions rather than counting bins. The first
//  cross IS the 132-transition space the directed sweep exhausts, so it
//  has a known denominator and a known target.
// =====================================================================
class ep_cov extends uvm_subscriber #(ep_txn);
  `uvm_component_utils(ep_cov)

  bit [1:0]    occ;        // 0, 1 or 2 buffers owned
  ep_event_e   ev;
  bit          tog;
  bit [1:0]    lat_bucket; // 0 = prompt, 1 = a few cycles, 2 = late

  covergroup cg;
    // the state x event space, with the unreachable occupancy 3 excluded
    cp_occ : coverpoint occ { bins n[] = {0, 1, 2}; }
    cp_ev  : coverpoint ev;
    cp_tog : coverpoint tog;
    x_transition : cross cp_occ, cp_ev, cp_tog;

    // Does firmware ever run late ENOUGH? Without this cross the
    // regression can report full transition coverage while never having
    // put two packets in the endpoint at once.
    cp_lat : coverpoint lat_bucket { bins b[] = {0, 1, 2}; }
    x_backpressure : cross cp_occ, cp_lat {
      // the only bins that prove the second buffer was used
      bins engaged = binsof(cp_occ) intersect {2};
    }
  endgroup
endclass

// =====================================================================
//  Error injection, restricted to faults that belong to THIS block.
// =====================================================================
class ep_fault_seq extends uvm_sequence #(ep_txn);
  `uvm_object_utils(ep_fault_seq)

  // 1  the engine ignores hw_nak            -> overrun, sticky flag
  // 2  firmware releases with nothing owned -> no-op, fw_ptr must not move
  // 3  a bus reset with a buffer still owned-> the buffer is reclaimed
  // 4  firmware disables while a buffer is owned -> irq drops, data kept
  // 5  a retransmission arriving when both buffers are full
  //
  // Not injected here, because they belong to other blocks: a CRC error
  // (the engine's), a babble (the engine's), a bus-powered brown-out
  // (the power path's), a descriptor error (firmware's).
endclass

13. Mutation Testing

Twelve mutations, each a plausible single mistake, each generated by a script that asserts its replacement applied — a mutation that silently failed to generate produces a zero that reads exactly like a survivor.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    MUT       V-ALL     V-DIR     S-ALL     S-DIR     H-ALL     H-DIR
    BASE          0         0         0         0         0         0
    M1        16115        24     11777        24     15812        24
    M2        15605      1694     18340      1694     15404      1694
    M3        20385        29     21301        29     23365        29
    M4        31143      8984     31408      8984     30981      8984
    M5        24975       150     24348       150     23908       150
    M6        18877        85     18894        85     19250        85
    M7        18526         2     20854         2     19369         2
    M8         3093       648      3052       648      2990       648
    M9           62        17       102        17       39         17
    M10          53         2       126         2       89          2
    M11       22402       239     23470       239     22702       239
    M12       22091       324     21834       324     21205       324
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    M1   a firmware release is LOST when a hardware set lands in the same
         cycle -- two branches instead of one expression
    M2   a zero-length packet is treated as no packet at all
    M3   a USB bus reset does not clear the data toggle
    M4   a USB bus reset also clears the endpoint enable
    M5   the write pointer advances on an aborted packet as well
    M6   an illegal release still advances the read pointer
    M7   a firmware toggle write OVERRIDES the hardware flip instead of
         composing with it -- a read-modify-write register, in effect
    M8   NAK is asserted when EITHER buffer is busy, so the second buffer
         is never used
    M9   the length register is one bit too narrow
    M10  the interrupt ignores the endpoint enable
    M11  an overrun overwrites a buffer firmware still owns
    M12  a retransmission is stored instead of discarded

BASE reads zero in all six columns, and every DIRECTED column is identical across all three languages — twelve mutations, three languages, thirty-six measurements, and the directed score depends only on the mutation. That is the result the identical-stimulus claim in section 8 is worth something for.

The ALL columns are not expected to match and do not: they include three different random generators, and the spread between the smallest and largest ALL for a single mutation reaches 1.6x (M1: 11,777 to 16,115). An ALL column is a fact about the stimulus at least as much as about the design.

The two thin scores, and what they mean

M7 and M10 each score 2. Both are worth looking at rather than congratulating.

M10 — an interrupt that ignores the endpoint enable — scores 2 for the mirror reason: the only state in which it differs is "a buffer is owned by firmware while the endpoint is disabled", and only boundary scenario B9 constructs it. On a real device that state is reached every time firmware disables an endpoint to reconfigure it, and the symptom is an interrupt storm into an ISR whose endpoint is not there.

Both scores being 2 is a coincidence, and worth checking rather than assuming, because identical scores from different mutations are the signature of a duplicate pair. Here they are not: the failing checks differ (CTRL and B1 for M7; irq and B9 for M10), the mechanisms are unrelated, and the totals differ by a factor of 350.

The one that correctness cannot see

Survivors, equivalents, duplicates

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    survivors              none
    equivalent mutants     none
    duplicate mutants      none
    retargeted             none

That is worth stating plainly rather than dressing up: unlike 29.3, where a mutation scored zero in all six columns because sign extension before a left shift was dead code at the modelled width, and unlike 29.4, where two nominally different mutations turned out to be the same defect, this set produced no equivalences. Each of the twelve alters a different architectural decision, and the check-name breakdown confirms it — no two mutations fail the same set of named checks.

The near miss is the M7/M10 score collision above, which looks like a duplicate pair and is not. Investigating it cost five minutes and is the only reason that sentence can be written down.

14. Debugging It On A Real Board

The failure at the top of the chapter — enumerates, then goes deaf — has at least five causes, and the useful skill is not "look at the waveform". It is knowing which piece of evidence can distinguish them, because most of the evidence available on an embedded board cannot distinguish any of them.

Which evidence answers which question

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    EVIDENCE                    ANSWERS                        CANNOT ANSWER
    -------------------------   ----------------------------   ---------------
    USB protocol analyser       Is the host sending? Is the    Anything about
                                device NAKing, ACKing or       WHY the device
                                silent? Which PID?             answered that way

    controller STAT register    Who owns each buffer, where    What firmware
    read from a debugger        both pointers are, whether     believes
                                the sticky flags are set

    firmware log / counters     What firmware believes, and    Whether the
                                whether its ISR ran            hardware agrees

    interrupt status            Whether irq was asserted and   Whether the ISR
                                whether it was taken           released anything

    a scope on the D lines      Whether the PHY is driving     Which layer
                                at all, and at what rate       above it is wrong

    clock and reset state       Whether the controller is      Anything about
                                clocked and out of reset       protocol state

    power / VBUS                Whether the device is          Anything at all
                                enumerating for a reason       about data flow
                                unrelated to data

Two rows of that table are the ones people skip and then spend a day on. Reading STAT from a debugger while the device is stuck is nearly free and answers the ownership question directly. And comparing it to what firmware believes is the whole diagnosis, because a disagreement between those two is the failure by definition.

The ladder, in the order that costs least

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    1  Is the host even asking?
       Analyser: OUT tokens present?
         no  -> not this block. Enumeration, addressing, or the host side.
         yes -> continue.

    2  What is the device answering?
         NAK forever   -> buffers are not being released.        go to 3
         ACK, no data  -> packets are being accepted and
                          discarded: a toggle mismatch.          go to 5
         nothing       -> the engine or the PHY, not this block.

    3  Read STAT.
         full == 11, irq high        -> firmware is not servicing. Is the
                                        interrupt enabled? Did the ISR run?
                                        Does the ISR write ACK?
         full == 11, irq low         -> ep_en is clear. Firmware disabled the
                                        endpoint, or a bus reset cleared it
                                        (mutation M4).
         badack sticky set           -> firmware released a buffer it did not
                                        own. If fw_ptr also moved, the two
                                        pointers are desynchronised and every
                                        read from now on is the wrong buffer
                                        (mutation M6).
         overrun sticky set          -> the engine committed while NAKed. The
                                        engine ignored hw_nak, or hw_nak is
                                        computed from the wrong thing.

    4  Compare fw_ptr with the buffer firmware THINKS it is reading.
       If they differ, stop looking at the hardware. One stray write did this,
       and the sticky flag in step 3 says when.

    5  Read CTRL bit 1 immediately after a bus reset. It must be zero.
       If it is one, every packet the host sends will look like a
       retransmission: acknowledged, discarded, forever (mutation M3).

First divergence, applied to this block

The general rule from the earlier chapters — find the first event after which the expected and observed architectural state differ — is unusually cheap to apply here, because the expected state is a two-entry queue and the observed state is one register read.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    EXPECTED, from the analyser log alone:
        for each OUT packet the host sent and the device ACKed:
            if its PID matches the expected toggle   -> push its length
            else                                     -> nothing changes
        for each release firmware logged:
            pop

    OBSERVED, from a STAT trace:
        the occupancy, the two pointers, the toggle

    The first cycle where the two disagree is the defect. Everything after it
    is a consequence, including every symptom that is easier to see.

The reason this works at all is that the model has no internal structure to mispredict. It is a queue and a bit. A model that reproduced hw_ptr and fw_ptr could reproduce a pointer bug alongside the design and agree with it all the way to the symptom — which is why the UVM reference model in section 12 has no pointers either.

15. Four Mistakes That Ship

Each of these is a wrong mental model first, an implementation bug second, and an observable failure third. That order is the useful one, because the fix is at the first step.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    MODEL   "a reset is a reset"
    BUG     one reset input, used for both the chip and the bus
    SHIPS   AS  a device that re-enumerates correctly and then loses firmware's
                configuration every time the host resets the bus -- which some
                hosts do on every suspend/resume cycle
    CAUGHT  BY  a property with two conjuncts: what the reset clears AND what
                it must not. Mutation M4, 8,984 directed failures.

    MODEL   "a zero-length packet is nothing"
    BUG     the accept condition is guarded on a non-zero length
    SHIPS   AS  transfers that hang at exactly the sizes that are a multiple of
                the maximum packet size, because the terminating ZLP is the one
                packet the device swallows. Intermittent, size-dependent, and
                reproducible only with the right file.
    CAUGHT  BY  a boundary scenario that treats length zero as a packet.
                Mutation M2, 1,694 directed failures.

    MODEL   "firmware can read-modify-write any register"
    BUG     the data toggle is an ordinary read-write field
    SHIPS   AS  a rare lost or duplicated packet under load, at a rate that
                depends on how long firmware's bus takes -- so it disappears
                when you add instrumentation
    CAUGHT  BY  a same-cycle scenario, and by nothing else. Mutation M7,
                2 directed failures, both from one scenario.

    MODEL   "double buffering is an optimisation, so it cannot be wrong"
    BUG     NAK asserted when either buffer is busy
    SHIPS   AS  a device that passes every functional test and misses its
                throughput target by half, discovered during system
                integration by somebody who is not looking at this block
    CAUGHT  BY  a property that states what NAK MEANS, or a coverage bin that
                requires occupancy two. Mutation M8, 648 directed failures,
                all of them the same check.

16. What This Does Not Cover

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    NOT MODELLED                      WHY IT IS OUT OF SCOPE
    -------------------------------   ------------------------------------
    the packet RAM and its address    this block owns the OWNERSHIP of the
    generation                        buffers, not their contents. A real
                                      controller's hw_buf_sel picks a base
                                      address; that adder is not a decision.
    the serial interface engine:      the producer of rx_commit. 29.1 and
    bit unstuffing, NRZI, CRC16,      29.2 work at that layer.
    PID decode, handshake generation
    endpoint 0 and control transfers  a different state machine with a
                                      different shape -- setup, data, status
    IN endpoints                      the mirror image, and NOT symmetric:
                                      firmware fills, hardware drains, and
                                      the interesting race moves to the
                                      other side of the same registers
    isochronous endpoints             no handshake, so no toggle and no
                                      retransmission; 29.2 and 29.3 build
                                      that shape
    DMA and descriptors               who owns a buffer is the same question
                                      one level up, with a memory system in
                                      between. Deliberately left whole.
    suspend, resume, remote wakeup     a power state machine that gates the
                                      clock this block runs on
    the peripheral bus protocol       fw_we / fw_addr / fw_wdata stand in for
                                      AHB-Lite, APB or a proprietary bus
    clock domain crossing             this block is entirely in ONE domain.
                                      See the note below -- that is a
                                      modelling choice, not a claim.

17. Exercises

Nine, in the order they build on each other. The first three need no tools.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    1  TRACE
       Start from full == 01, hw_ptr == 1, fw_ptr == 0, dtog == 1.
       Apply, one cycle each:
           a matching packet of length 0
           a firmware ACK
           an aborted packet
           a non-matching packet of length 9
           a USB bus reset
       Write down full, hw_ptr, fw_ptr, dtog, hw_nak and irq after each.
       Then say which of the five events changed the data toggle, and why
       the other four did not.

    2  DENOMINATOR
       Section 8 derives 12 reachable ownership states from a raw space of
       32. Now do it for a FOUR-buffer endpoint: how many of the raw
       2^4 x 4 x 4 x 2 combinations are reachable, and what is the argument?
       Then say what happens to the 11-event sweep, and whether exhausting
       the product is still the right plan.

    3  REGISTER SEMANTICS
       CTRL bit 1 is write-1-to-toggle. Suppose instead the endpoint had a
       pair of write-only action bits: SET_DTOG and CLR_DTOG, where writing
       1 to either performs that action. Is that race-free against a
       hardware flip? Give a trace, and say what firmware can express with
       this scheme that it cannot express with write-1-to-toggle, and
       whether it should be allowed to.

    4  VERILOG
       Add a THIRD buffer. Keep the register map: ACK still releases the
       buffer at fw_ptr, STAT still reports the ownership bits. Decide
       first what hw_nak means, then what the pointers are, then write it.
       Say what changed about the reachable state space before you simulate.

    5  SYSTEMVERILOG
       Implement the same three-buffer contract, and keep the property that
       the bus-reset behaviour is one assignment. Then answer: does the
       struct still make mutation M3 impossible to inject in one line?

    6  VHDL
       Implement the three-buffer contract. The ownership bits are now
       std_logic_vector(2 downto 0) and the pointers are no longer one bit,
       so pick() is no longer a two-way choice. Write the conversion
       explicitly and say what the range of the pointer type is and why.

    7  TESTBENCH
       Extend phase 3 so that the firmware model's service pattern is a
       PARAMETER, and sweep it from "drains two per offer" to "drains none
       for eight offers in a row". Plot NAKed offers against the pattern.
       Where is the knee, and what does its position tell you about the
       buffer count?

    8  SVA
       Write a progress property for the three-buffer version that is not
       vacuous: "a buffer firmware owns is eventually released" is false
       without an assumption, and "it is released when firmware releases it"
       is a tautology. State the assumption explicitly in the property.

    9  UVM
       The environment in section 12 has two sequencers so that coincidences
       happen by construction. Write the virtual sequence that makes a
       specific coincidence happen ON DEMAND -- an accepted packet and a
       toggle write on the same cycle -- and then say why you still want the
       unconstrained version running alongside it.

    10 MUTATION
       For each of the twelve mutations in section 13, predict WHICH of the
       thirteen per-cycle comparisons fails first. Then check three of your
       predictions against the named-check breakdown. The interesting cases
       are M5, M11 and M12, which all begin by failing STAT.

    11 DEBUG
       A device NAKs forever. STAT reads full == 11, irq == 1, both sticky
       flags clear. Firmware's log says its ISR ran 4,213 times and wrote
       ACK 4,213 times. Name two hardware faults and one firmware fault
       consistent with every one of those observations, and the single
       additional reading that separates them.

18. The Interview Answer

"A USB device enumerates correctly and then accepts nothing. Where do you look, and what would you have done in the RTL to make that question easier?"

The first half is a two-step narrowing and it should take one sentence. Look at whether the device is NAKing or ACKing. Those are different bugs: NAKing forever means buffers are not being released, and ACKing while nothing arrives means packets are being accepted and discarded — which on a bulk endpoint means the data toggle is wrong, and the commonest reason for that is a bus reset that did not clear it.

That last point is the one worth volunteering, because it is the distinction the question is really about. A USB bus reset is not a chip reset. The host drove SE0; the CPU never stopped; firmware's variables are all still there. What the bus reset must do is return the endpoint's data state to default, including the data toggle to DATA0, and what it must not do is clear the configuration firmware wrote. Get the scope one line too long and the device loses its configuration on every suspend; one line too short and it accepts nothing while acknowledging everything, which is the symptom in the question.

The second half is the better half of the answer. Three things in the RTL make the question cheap:

One owner bit per buffer, and no second copy of it. The failure mode of shared state is disagreement, and disagreement needs two records to disagree. If the ownership is one bit that both sides read and each side changes for exactly one reason, then reading that bit is the diagnosis.

Sticky flags for the illegal operations. An engine that commits while NAKed, and firmware that releases a buffer it does not own, are both contract violations by somebody else. They cost one flip-flop each, they are set at the instant the violation happens, and they survive until read — which is the difference between knowing what happened and inferring it from a symptom that appears minutes later under load.

Deltas rather than values wherever both sides write. The data toggle is written by the hardware on every accepted packet and by firmware when the host clears a halt. Making it a read-modify-write field means firmware's write loses any hardware flip that lands between its read and its write, and no amount of care in firmware closes that window. Making it write-1-to-toggle means firmware can only say "flip", and two flips in one cycle compose to none — which is the right answer rather than a race resolved by whoever won.

If there is time for one more, the verification half: the mechanism that makes an endpoint double-buffered only exists when firmware is late, so a testbench whose firmware model services promptly has tested a single-buffered endpoint and will report thousands of passes while doing it. The bench has to make firmware late on purpose, and then it has to count how often both buffers were full — because the difference between "we tested backpressure" and "we believe we tested backpressure" is a number that somebody has to print.

19. What Carries Forward

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    THE MECHANISM
    o  on an embedded part the controller is a PERIPHERAL: the interesting
       boundary moved from the wire to a handful of registers
    o  one ownership bit per buffer, one owner, no second copy
    o  two pointers, each advancing by one, each for exactly one reason
    o  double buffering exists only in the region where firmware is late,
       and that is also the only region where it can be tested
    o  a USB bus reset is not a chip reset and not an endpoint reset: three
       scopes, and the reset list is wrong in two different directions
    o  the data toggle distinguishes a lost handshake from a lost packet
       using one bit and no timers -- and a retransmission is ACKED and
       discarded, which looks wrong until you ask what the host is saying
    o  an aborted packet flips nothing and advances nothing, because the
       host will resend with the SAME PID
    o  a field both sides write must be a DELTA, not a value

    THE RESULT
    o  132 transitions -- 12 reachable ownership states x 11 events --
       exhausted in three languages with identical directed counts
    o  the 12 comes from 32 raw combinations minus 20 that the pointer
       rules make unreachable, and the toggle is only a free axis because
       firmware can flip it without moving a pointer
    o  relabelling the two buffers changes nothing but labels, over 66
       paired runs, with exactly one exception: the bus reset, which is the
       one operation that names an absolute buffer

    THE METHOD
    o  check through the PORTS, so the register map is under test rather
       than assumed
    o  build a state, then PROVE you built it -- 264 constructions, four
       checks each, and a setup that fails silently is a coverage fiction
    o  a MEASURED ZERO is the cheapest bug report available: two reachability
       holes here were found by a counter, not by a failing check
    o  a new random branch inserted after a wider range test is DEAD CODE,
       and the tally that did not move is the only thing that says so
    o  a coincidence between two independent agents is not produced by a
       bench that drives one agent per cycle, however long it runs
    o  a same-cycle design decision is tested by one same-cycle scenario --
       delete it and the mutation survives 30,000 checks, measured
    o  a THROUGHPUT bug has no incorrect output to find; it has to be
       written down as a property or measured as a number
    o  no equivalent mutants here, and saying so is worth as much as
       finding one -- but identical scores from two mutations still have to
       be investigated before they can be called a coincidence
    o  a reference model with no pointers cannot reproduce a pointer bug
       and agree with the design all the way to the symptom
    o  a tool limitation's workaround is sometimes the design you should
       have written: own_at_fw names a concept the design already had
    o  a single-clock model establishes that a protocol is correct GIVEN
       correctly delivered events, and nothing about whether they are

The last case study drops the host out of the picture almost entirely. On an FPGA development board the USB connector usually does not reach the FPGA at all: it reaches a bridge chip, and what arrives on the pins is a byte stream with two active-low flags and a turnaround cycle — which is a different problem wearing USB's name.

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.