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.
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
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.
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
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.
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.
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:
hw_ptr the buffer the next accepted packet goes into
fw_ptr the buffer firmware reads nextAnd 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
The firmware side: what the CPU sees
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.
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_ptrCTRL 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:
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// =====================================================================
// 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;
endmodule6. 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
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
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
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.
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// =====================================================================
// 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
endmodule8. The Measurement
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 0The 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.
RAW STATE SPACE full[1:0] 4 values
hw_ptr 2
fw_ptr 2
dtog 2
--------------------
32 combinationsMost of those cannot happen. The two pointers are not independent of the ownership bits, because each one advances for exactly one reason:
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
= 12The 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:
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 reset12 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:
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_selThat 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:
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 ..... 0Two 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.
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 itThe struct is the interesting one. Because every field in it resets to zero, the entire bus-reset behaviour is one assignment:
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.
// =====================================================================
// 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// =====================================================================
// 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
endmodule10. 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.
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:
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.
-- =====================================================================
-- 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;-- =====================================================================
-- 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.
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 packetsThe 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.
// =====================================================================
// 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).
endclass13. 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.
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 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 discardedBASE 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
survivors none
equivalent mutants none
duplicate mutants none
retargeted noneThat 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
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 dataTwo 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
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.
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.
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
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.
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
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 areThe 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
Related tutorials
- Related topic
Endpoint Logic
A lost ACK and a lost data packet look identical to the host, so it resends the same bytes — and the data toggle is the only thing that tells a device a retransmission from new data.
- Related topic
USB Webcams
UVC streams video over isochronous transfers, which have no retries. With only a frame-ID bit and an end-of-frame flag for framing, exactly 1 packet loss in P is detectable — 6% for a 16-packet frame, and under 1% for a real one.
- Related topic
FIFO Architecture
An endpoint FIFO stores packets, not bytes — a zero-length packet carries nothing and must still occupy a buffer, because it is the only thing that terminates a transfer ending on a packet boundary.
- Related topic
Endpoint Problems
A NAK is not an error and a STALL is not a NAK — one is flow control working, one is firmware refusing permanently, and a monitor that treats them alike either floods the log or misses the endpoint that has stopped.
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.
