USB · Module 17
Frame Scheduling (1 ms)
The 1 ms frame is the unit of scheduling opportunity, and its number is an 11-bit protocol field living inside a counter of some other width — with the mutation that was equivalent in one language only.
Module 16 finished by naming what every chapter in it had quietly assumed: the slot exists. A feedback loop, a frame assembler, gap arithmetic, an admission decision and a concealment stage all began after the host had agreed to carry the stream and had placed it somewhere in time.
Module 17 is where that placement happens, and the whole module rests on one structure that has to be built before anything can be scheduled into it: the frame.
This chapter builds the 1 ms frame time base. It is the smallest RTL in the module and the one everything else depends on, and it contains a distinction that causes more real defects than its size suggests — the difference between a protocol-defined number and the counter that happens to hold it.
1. A Frame Is a Unit of Opportunity, Not a Unit of Data
The word frame has been used loosely since Module 15. Make it precise.
A frame is a 1 ms interval of bus time, delimited by the host transmitting a Start-of-Frame packet. It is not a quantity of data, not a transaction, and not a period any device controls. It is the repeating opportunity into which the host places work.
Everything Modules 14–16 built expressed its requirements in these units:
| Module | What it asked of the schedule |
|---|---|
| 14 — Bulk | whatever is left over, with no timing guarantee |
| 15 — Interrupt | a bound on the gap — at most bInterval frames between polls |
| 16 — Isochronous | a reserved slot in every service interval, admitted in advance |
Three different requirements, one resource, and the frame is the unit all three are denominated in. That is why the frame time base comes first: a scheduler cannot honour "at most 8 frames" or "once per interval" until something is counting frames correctly.
2. The SOF Is a Packet, and the Frame Number Rides In It
The Start-of-Frame is a real token on the wire with its own packet identifier, which the Linux host-controller header lists alongside every other USB PID:
#define USB_PID_SOF 0xa5It carries an 11-bit frame number, and that width is the first thing worth fixing in mind, because it is the source of §12's and §13's findings. Eleven bits means the number counts 0 … 2047 and then returns to 0. It is a protocol field, not a convenience.
3. Protocol Time Is Not an RTL Clock
The second distinction, and the one this chapter's RTL exists to make concrete.
A frame is 1 ms. A controller clock is one clock. Nothing in the hardware makes those the same thing, and a design that treats a frame boundary as "a clock edge" has skipped the only part of the problem that is actually hardware.
The boundary is derived, by counting controller clocks:
controller clock (e.g. 48 MHz)
↓ count TICKS_PER_FRAME of them
frame boundary
↓ qualify: is the port operating?
frame_tick — a one-cycle CONTROLLER-DOMAIN pulse
↓
scheduler state updateframe_tick in this module is an abstraction of a qualified USB scheduling boundary, not the USB wire clock and not the SOF packet. Every waveform in Module 17 is in the controller domain and says so.
Four time concepts are now in play and they must not be blurred:
| Term | What it is | Who defines it |
|---|---|---|
| controller clock | the RTL clock this logic runs on | the implementation |
| frame | a 1 ms protocol interval | USB |
| frame number | the 11-bit value the SOF carries | USB |
| frame counter | what software reads back | the host controller |
And two more arrive in Chapter 17.2 — the microframe and the service interval — which is precisely why this chapter separates them now rather than later.
4. The Hardware, Before Any Language
State retained: the frame number, the position within the current frame, and a pending SOF request.
On reset or bus reset: everything clears. A bus reset restarts frame numbering — the device's frame-derived state (15.1's poll schedule, 16.1's feedback window) restarts with it, so continuing the old count would leave the two ends numbering frames differently.
On each controller clock while the port is operating: the position advances.
When the position has run the whole frame: the position restarts, frame_tick pulses for one cycle, a SOF is requested, and the frame number advances — modulo 2048, the protocol's modulus.
When the port is not operating: the frame number is held — it is protocol state — and the position is parked at zero — it is a local measurement that must not resume mid-frame.
The SOF request is a level, not a pulse. The packet engine may be busy; a pulse would be lost. It is held until sof_ack.
And the collision that matters: a frame boundary arriving while the previous SOF is still un-acked. The new request overwrites the old — a stale SOF names a frame that has already ended, and transmitting it would announce the wrong number — but the loss is reported on sof_missed rather than discarded silently. Module 16 made the same argument about abandoned video frames: something was lost is information the layer above needs.
5. Verilog
The RTL contract
- What it models: the 1 ms frame time base of a USB 2.0 host controller.
- Why it exists: because the frame is the unit every service requirement in Modules 14–16 is denominated in (§1), and the boundary must be derived from a controller clock rather than assumed (§3).
- Inputs:
enable(the port is operating),sof_ack(the packet engine took the SOF),bus_reset. - Authoritative state:
frame_r(protocol state),pos_r(local measurement),sofreq_r. - Derived state:
at_boundary— combinational, not stored. - Outputs:
frame_no,frame_pos,frame_tick,sof_req,frame_wrapped,sof_missed. - Hardware implied: one
POS_Wcounter, one 11-bit counter, one comparator, four flags. - Reset: asynchronous active-low
rst_n;bus_resetsynchronous and equivalent, and both restart the frame number. - Priority:
!enableoutranks the boundary, which outranks the ordinary count. - Latency: every output is registered;
frame_tickis visible the cycle after the boundary condition holds. - Boundary behaviour: the frame number wraps at 2048, the protocol modulus.
- Collision behaviour: a boundary with an un-acked SOF renews the request and pulses
sof_missed; ansof_ackin the boundary cycle is not a miss. - Assumptions:
TICKS_PER_FRAMEcontroller clocks equal one 1 ms frame;sof_ackis a single-cycle pulse. - Omissions: no PHY, no clock recovery, no packet engine, no frame-adjust register, no host-controller-visible counter beyond 11 bits.
- What DV should verify: that the boundary lands after exactly
TICKS_PER_FRAMEclocks; that the frame number wraps at 2048 and reports it; thatsof_reqis a level andframe_ticka pulse; that a disabled port holds the number and parks the position.
// usb_frame_timer -- the 1 ms frame time base of a USB 2.0 host controller.
//
// TIME DOMAINS. This module is the place where they are reconciled, so it is
// worth being exact about which is which:
//
// * the CONTROLLER CLOCK is `clk`, an ordinary RTL clock;
// * a FRAME is a 1 ms protocol interval, and it is DERIVED here by counting
// TICKS_PER_FRAME controller clocks;
// * `frame_tick` is a one-cycle CONTROLLER-DOMAIN pulse marking that
// boundary. It is an abstraction of a qualified USB scheduling boundary,
// NOT the USB wire clock and NOT the SOF packet itself;
// * `sof_req` asks the packet engine to transmit a Start-of-Frame. The
// packet takes real bus time, which is why `sof_ack` exists and why a
// frame boundary arriving before that ack is an error worth reporting.
//
// The frame NUMBER is 11 bits because that is the width of the field the SOF
// token carries, so it wraps at 2048 rather than at any convenient power of
// the counter's own width. That distinction -- a protocol-defined modulus
// living inside a counter whose natural width is something else -- is the
// single most common defect in this block.
module usb_frame_timer #(
parameter integer TICKS_PER_FRAME = 48000, // controller clocks per 1 ms
parameter integer FRAME_W = 11, // SOF frame-number field width
parameter integer POS_W = 16 // position-within-frame width
) (
input wire clk,
input wire rst_n,
input wire bus_reset,
input wire enable, // the port is in an operating state
input wire sof_ack, // the packet engine took the SOF
output wire [FRAME_W-1:0] frame_no, // the value the SOF token carries
output wire [POS_W-1:0] frame_pos, // controller clocks into this frame
output wire frame_tick, // 1 cycle at each frame boundary
output wire sof_req, // held until sof_ack
output wire frame_wrapped,// 1 cycle when frame_no wraps to 0
output wire sof_missed // a boundary arrived with SOF unsent
);
localparam [FRAME_W-1:0] FRAME_MAX = {FRAME_W{1'b1}}; // 2047
localparam [POS_W-1:0] POS_LAST = TICKS_PER_FRAME - 1;
reg [FRAME_W-1:0] frame_r;
reg [POS_W-1:0] pos_r;
reg tick_r, wrap_r, sofreq_r, missed_r;
assign frame_no = frame_r;
assign frame_pos = pos_r;
assign frame_tick = tick_r;
assign sof_req = sofreq_r;
assign frame_wrapped = wrap_r;
assign sof_missed = missed_r;
// The boundary is where the position counter has run the whole frame.
// Comparing against TICKS_PER_FRAME rather than TICKS_PER_FRAME-1 makes
// every frame one controller clock too long -- a 20 ppm error at 48 MHz
// that accumulates into a visible frame-rate drift. See mutation F3.
wire at_boundary = enable && (pos_r == POS_LAST);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
frame_r <= {FRAME_W{1'b0}}; pos_r <= {POS_W{1'b0}};
tick_r <= 1'b0; wrap_r <= 1'b0; sofreq_r <= 1'b0; missed_r <= 1'b0;
end else if (bus_reset) begin
// A bus reset restarts frame numbering. The device's frame-derived
// state (Chapter 15.1's poll schedule, Chapter 16.1's feedback window)
// is restarted with it, so continuing the old count would leave the
// two ends numbering frames differently.
frame_r <= {FRAME_W{1'b0}}; pos_r <= {POS_W{1'b0}};
tick_r <= 1'b0; wrap_r <= 1'b0; sofreq_r <= 1'b0; missed_r <= 1'b0;
end else begin
tick_r <= 1'b0;
wrap_r <= 1'b0;
missed_r <= 1'b0;
// The SOF request is a LEVEL held until the packet engine takes it,
// not a pulse. A pulse would be lost whenever the engine was busy.
if (sof_ack) sofreq_r <= 1'b0;
if (!enable) begin
// Not operating: hold the frame number, restart the position. The
// frame number is protocol state and must survive; the position is
// a local measurement and must not resume mid-frame.
pos_r <= {POS_W{1'b0}};
end else if (at_boundary) begin
pos_r <= {POS_W{1'b0}};
tick_r <= 1'b1;
// COLLISION: the boundary and an un-acked SOF. The new request
// overwrites the old one -- the stale SOF is for a frame that has
// already ended and transmitting it now would announce the wrong
// number -- but the loss is REPORTED rather than silent.
if (sofreq_r && !sof_ack) missed_r <= 1'b1;
sofreq_r <= 1'b1;
// The 11-bit protocol modulus, not the counter's natural width.
if (frame_r == FRAME_MAX) begin
frame_r <= {FRAME_W{1'b0}};
wrap_r <= 1'b1;
end else begin
frame_r <= frame_r + {{(FRAME_W-1){1'b0}}, 1'b1};
end
end else begin
pos_r <= pos_r + {{(POS_W-1){1'b0}}, 1'b1};
end
end
end
endmoduleThree details are worth naming.
FRAME_MAX is {FRAME_W{1'b1}}, and the explicit wrap is written out anyway. §13 measures exactly how much that redundancy is worth — and the answer is surprising.
pos_r and frame_r are treated differently when the port is disabled. One is held, one is parked. That asymmetry is the whole content of protocol state versus local measurement, and it is the kind of decision that reads as arbitrary until the reason is stated: a device resuming mid-frame would be counting a frame that never finished.
sof_req is assigned in two places — cleared on sof_ack, set at the boundary — and the boundary assignment comes second, so a simultaneous ack and boundary leaves the request set. That ordering is the collision policy, and §9's note explains why it is stated deliberately rather than inherited from the order the lines happen to appear in.
6. SystemVerilog
Same hardware, with the three cases named and the parameterisation guarded.
package usb_frame_pkg;
// What this controller clock does to the frame time base. Naming the four
// cases makes them exhaustive and mutually exclusive; in the Verilog they
// are the positions of an if/else chain and the exclusivity is positional.
typedef enum logic [1:0] {
T_HELD, // the port is not operating: hold the number, park position
T_COUNT, // an ordinary clock inside the frame
T_BOUNDARY // the frame has fully elapsed
} tstep_e;
endpackage
module usb_frame_timer_sv
import usb_frame_pkg::*;
#(
parameter int unsigned TICKS_PER_FRAME = 48000,
parameter int unsigned FRAME_W = 11,
parameter int unsigned POS_W = 16
) (
input logic clk,
input logic rst_n,
input logic bus_reset,
input logic enable,
input logic sof_ack,
output logic [FRAME_W-1:0] frame_no,
output logic [POS_W-1:0] frame_pos,
output logic frame_tick,
output logic sof_req,
output logic frame_wrapped,
output logic sof_missed
);
// Elaboration guards. Each of these produces a design that runs and is
// wrong rather than one that fails loudly.
initial begin
if (TICKS_PER_FRAME < 2)
$fatal(1, "TICKS_PER_FRAME=%0d leaves no room for a frame", TICKS_PER_FRAME);
if (FRAME_W != 11)
$fatal(1, "FRAME_W=%0d: the USB 2.0 SOF frame-number field is 11 bits",
FRAME_W);
if ((TICKS_PER_FRAME - 1) >= (1 << POS_W))
$fatal(1, "POS_W=%0d cannot represent a frame of %0d clocks",
POS_W, TICKS_PER_FRAME);
end
localparam logic [FRAME_W-1:0] FRAME_MAX = '1; // 2047
localparam logic [POS_W-1:0] POS_LAST = POS_W'(TICKS_PER_FRAME - 1);
tstep_e tstep;
always_comb begin
if (!enable) tstep = T_HELD;
else if (frame_pos == POS_LAST) tstep = T_BOUNDARY;
else tstep = T_COUNT;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n || bus_reset) begin
// A bus reset restarts frame numbering: the device's frame-derived
// state restarts with it, so continuing the old count would leave the
// two ends numbering frames differently.
frame_no <= '0; frame_pos <= '0;
frame_tick <= 1'b0; sof_req <= 1'b0;
frame_wrapped <= 1'b0; sof_missed <= 1'b0;
end else begin
frame_tick <= 1'b0;
frame_wrapped <= 1'b0;
sof_missed <= 1'b0;
// sof_req is a LEVEL held until the packet engine takes it. A pulse
// would be lost whenever the engine happened to be busy.
if (sof_ack) sof_req <= 1'b0;
unique case (tstep)
T_HELD: begin
// Hold the frame number -- it is protocol state. Park the position
// -- it is a local measurement that must not resume mid-frame.
frame_pos <= '0;
end
T_COUNT: begin
frame_pos <= frame_pos + 1'b1;
end
T_BOUNDARY: begin
frame_pos <= '0;
frame_tick <= 1'b1;
// COLLISION: the boundary and an un-acked SOF. The new request
// overwrites the old -- the stale SOF names a frame that has
// ended -- but the loss is REPORTED rather than silent.
if (sof_req && !sof_ack) sof_missed <= 1'b1;
sof_req <= 1'b1;
// The 11-bit PROTOCOL modulus, not the counter's natural width.
if (frame_no == FRAME_MAX) begin
frame_no <= '0;
frame_wrapped <= 1'b1;
end else begin
frame_no <= frame_no + 1'b1;
end
end
endcase
end
end
endmoduletstep_e makes the three cases exhaustive and mutually exclusive by construction, which §12 turns out to depend on: the Verilog's outer if (!enable) guard and the SystemVerilog's T_HELD case look equivalent and place the enable decision in structurally different positions.
The FRAME_W != 11 guard is the one to argue for. It refuses to elaborate a design whose frame-number field is not the protocol's width — which is not a parameterisation anyone should be free to make, because §2 established that 11 bits is normative. A parameter that must have exactly one value is documentation until something enforces it.
7. VHDL
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package usb_frame_pkg is
-- What this controller clock does to the frame time base. A distinct type
-- with no numeric encoding: the case below must be exhaustive and cannot
-- be compared against an integer.
type tstep_t is (T_HELD, T_COUNT, T_BOUNDARY);
end package;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb_frame_pkg.all;
entity usb_frame_timer_vhdl is
generic (
TICKS_PER_FRAME : positive := 48000;
FRAME_W : positive := 11;
POS_W : positive := 16
);
port (
clk : in std_logic;
rst_n : in std_logic;
bus_reset : in std_logic;
enable : in std_logic;
sof_ack : in std_logic;
frame_no : out unsigned(FRAME_W-1 downto 0);
frame_pos : out unsigned(POS_W-1 downto 0);
frame_tick : out std_logic;
sof_req : out std_logic;
frame_wrapped : out std_logic;
sof_missed : out std_logic
);
end entity;
architecture rtl of usb_frame_timer_vhdl is
constant FRAME_MAX : unsigned(FRAME_W-1 downto 0) := (others => '1');
constant POS_LAST : unsigned(POS_W-1 downto 0) :=
to_unsigned(TICKS_PER_FRAME - 1, POS_W);
signal frame_r : unsigned(FRAME_W-1 downto 0) := (others => '0');
signal pos_r : unsigned(POS_W-1 downto 0) := (others => '0');
signal tick_r : std_logic := '0';
signal wrap_r : std_logic := '0';
signal sofreq_r : std_logic := '0';
signal missed_r : std_logic := '0';
signal tstep : tstep_t;
begin
assert TICKS_PER_FRAME >= 2
report "TICKS_PER_FRAME leaves no room for a frame" severity failure;
assert FRAME_W = 11
report "the USB 2.0 SOF frame-number field is 11 bits" severity failure;
assert TICKS_PER_FRAME - 1 < 2**POS_W
report "POS_W cannot represent a frame of this many clocks" severity failure;
frame_no <= frame_r;
frame_pos <= pos_r;
frame_tick <= tick_r;
sof_req <= sofreq_r;
frame_wrapped <= wrap_r;
sof_missed <= missed_r;
tstep <= T_HELD when enable = '0' else
T_BOUNDARY when pos_r = POS_LAST else
T_COUNT;
process (clk, rst_n)
begin
if rst_n = '0' then
frame_r <= (others => '0'); pos_r <= (others => '0');
tick_r <= '0'; sofreq_r <= '0'; wrap_r <= '0'; missed_r <= '0';
elsif rising_edge(clk) then
if bus_reset = '1' then
-- A bus reset restarts frame numbering: the device's frame-derived
-- state restarts with it, so continuing the old count would leave
-- the two ends numbering frames differently.
frame_r <= (others => '0'); pos_r <= (others => '0');
tick_r <= '0'; sofreq_r <= '0'; wrap_r <= '0'; missed_r <= '0';
else
tick_r <= '0';
wrap_r <= '0';
missed_r <= '0';
-- sof_req is a LEVEL held until the packet engine takes it; a pulse
-- would be lost whenever the engine happened to be busy.
if sof_ack = '1' then
sofreq_r <= '0';
end if;
case tstep is
when T_HELD =>
-- Hold the frame number (protocol state); park the position
-- (a local measurement that must not resume mid-frame).
pos_r <= (others => '0');
when T_COUNT =>
pos_r <= pos_r + 1;
when T_BOUNDARY =>
pos_r <= (others => '0');
tick_r <= '1';
-- COLLISION: the boundary and an un-acked SOF. The new request
-- overwrites the old -- the stale SOF names a frame that has
-- ended -- but the loss is REPORTED, not silent.
if sofreq_r = '1' and sof_ack = '0' then
missed_r <= '1';
end if;
sofreq_r <= '1';
-- The 11-bit PROTOCOL modulus, not the counter's natural width.
if frame_r = FRAME_MAX then
frame_r <= (others => '0');
wrap_r <= '1';
else
frame_r <= frame_r + 1;
end if;
end case;
end if;
end if;
end process;
end architecture;tstep_t has no numeric encoding, so the case must be exhaustive and cannot be compared against an integer — the same property 16.3 and 16.5 relied on.
The three assert ... severity failure statements are concurrent, evaluated as part of the architecture rather than as a process, and they are the VHDL counterpart of §6's $fatal guards.
pos_r + 1 and frame_r + 1 need no width plumbing, because numeric_std defines addition of an unsigned and a natural. The Verilog writes {{(POS_W-1){1'b0}}, 1'b1} to match the counter's width exactly; the VHDL lets the type carry it.
8. Comparing the Three
| Concern | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| The three cases | if/else if chain, enable as an outer guard | typedef enum + unique case | type tstep_t, no encoding |
Where enable is tested | outside the boundary test | inside the case selection | inside the case selection |
| Illegal parameterisation | undetected | three $fatal guards | three assert ... severity failure |
| Counter increment | width-matched concatenation | + 1'b1 | + 1, defined by the type |
| Case exhaustiveness | not checked | unique — but see §20 | enforced by the type |
The second row is not a stylistic difference, and §12 is the measurement that proves it.
9. The Testbenches
The model does not use the design's method. The design compares a position counter against a limit and resets it. Every bench instead counts boundaries and derives the frame number by modulo division, and derives the position from an absolute clock count:
if (enable == 0) clks_in_frame = 0;
else if (clks_in_frame == TPF-1) begin
boundaries = boundaries + 1; // count boundaries
clks_in_frame = 0;
end else clks_in_frame = clks_in_frame + 1;
...
check(frame_no === FRAME_W'(boundaries % FRAME_MOD), "frame_no");Neither derivation can reproduce a compare-and-reset off-by-one, which is what makes mutation F3 — a frame one controller clock too long — cost 47 548 failures rather than passing unnoticed.
A testbench defect, found by the first-divergence rule. The first run failed from the second cycle onward with the position always one ahead of the model. The cause was not in the design:
FAIL: frame_pos (pos=2 | exp pos=1, t=27000)The clock edge that releases reset is itself a counted controller clock. The DUT advanced on it; the model had not started counting. The fix is to hold the port disabled across that edge, so both start from a defined position — and it is stated in the bench rather than patched silently, because a bench that silently compensates for a one-cycle offset will hide a real one later.
A second bench defect, same class. Three directed checks then failed while the model and DUT agreed throughout — the checks were aimed at the wrong cycle, because they hardcoded loop counts from an arbitrary starting position. The fix is an align_to_boundary helper:
// Directed tests that count cycles must start from a known position;
// hardcoding counts from an arbitrary point is how the first version of
// this bench mis-aimed three of its own checks.
task align_to_boundary;
begin
step(1'b0);
while (!frame_tick) step(1'b0);
end
endtaskBoth defects were in the verification code, both were found by reading the first divergence rather than the last failure, and neither would have been found by adding stimulus. Module 15 found a bench that truncated its own messages; 16.2 found one whose stimulus described a device that does not exist. Verification code is executable engineering logic and fails in the same ways design code does.
The directed sequence covers the boundaries §4 identifies:
| Scenario | What it pins down |
|---|---|
| clocks before the boundary | no tick until the frame has fully elapsed |
| the boundary clock | tick, number advances, SOF requested |
| the following clock | frame_tick is a pulse; sof_req is a level |
sof_ack | clears the request |
| a boundary with the SOF un-acked | sof_missed, and the request renewed for the new frame |
sof_ack in the boundary cycle | not a miss |
enable low for three frames | number held, position parked, no boundaries |
bus_reset | number restarted, request cleared |
| 2048 frames | the 11-bit protocol wrap, directed |
| 4000 randomised acks | independently drawn |
The wrap is directed, not random. It is one transition out of 2048, and no random ack pattern targets it. The bench walks the counter all the way round:
REACH: boundaries=2553 wraps=1 sof_missed=24 ack-on-boundary=110. Mutation Testing — Across All Three Languages
| ID | Mutation | Verilog | SystemVerilog | VHDL | Killed |
|---|---|---|---|---|---|
| — | baseline, no mutation | 0 | 0 | 0 | — |
| F1 | the frame-number modulus is off by one | 4012 | 4012 | 4012 | ✅ all three |
| F2 | the explicit wrap is removed | 1 | 1 | 1 | ✅ all three — see §12 |
| F3 | the frame is one controller clock too long | 47548 | 47548 | 47594 | ✅ all three |
| F4 | sof_req is a pulse instead of a level | 3585 | 3585 | 3568 | ✅ all three |
| F5 | a missed SOF is never reported | 25 | 25 | 20 | ✅ all three |
| F6 | bus_reset does not restart numbering | 20388 | 20388 | 20388 | ✅ all three |
| F7 | the enable guard is removed | 116 | 116 | 116 | ✅ all three — see §11 |
F3 is the largest at ~47 500 and for the reason §9 gives: a frame one clock too long shifts every subsequent boundary, so one defect misaligns the entire run rather than one frame.
F6 costs 20 388 because a bus reset that leaves the frame number running desynchronises the two ends permanently — the failure mode 16.2 called loss of synchronisation, here applied to time rather than to frames.
11. The Mutation That Was Equivalent in One Language Only
F7 first measured 0 in Verilog and 116 in SystemVerilog and VHDL. A survivor in exactly one language is either a real semantic difference or a mutation that did not land where it was meant to, and §50 requires finding out which.
It was the second, and the reason is visible in five lines. The two designs test enable in structurally different positions:
if (!enable) begin // Verilog: an OUTER guard
pos_r <= 0; // ...which dominates regardless
end else if (at_boundary) begin if (!enable) tstep = T_HELD; // SV: INSIDE the
else if (frame_pos == POS_LAST) tstep = T_BOUNDARY; // case selectionThe Verilog mutation removed enable from at_boundary — and the outer if (!enable) branch takes priority no matter what at_boundary says, so the mutant was behaviourally identical to the original. A genuinely equivalent mutation. The SystemVerilog and VHDL mutations removed the guard itself, which is live.
Placing the Verilog mutation on the guard instead gave 116 — exact agreement across all three.
12. The Mutation That Barely Died, and Why That Is a Coincidence
F2 removes the explicit wrap and costs exactly one failure, in every language. That is close enough to a survivor to demand the same investigation.
The single failure is not the frame number. It is the flag:
FAIL: frame_wrapped (fno=0 pos=0 tick=1 sof=1 wrap=0 miss=0, t=164517000)The frame number wraps correctly anyway — and the reason is a width coincidence, demonstrated directly rather than argued:
11-bit counter at 2047, +1 -> 0 (natural width == modulus)
16-bit counter at 2047, +1 -> 2048 (natural width != modulus)frame_r is exactly 11 bits, so its natural overflow lands precisely on the protocol modulus. The explicit if (frame_r == FRAME_MAX) is therefore redundant for the value and load-bearing only for frame_wrapped.
13. The Waveform
Boundaries, a level-held SOF request, and a missed SOF
12 cyclesThis waveform is in the controller clock domain. frame_tick is the qualified internal pulse of §3, not an edge on D+/D−; sof_req is a request to a packet engine, not the SOF packet. Reading it as bus timing would suggest a frame is four clocks long, which is exactly the conflation §3 exists to prevent.
Cycle 3 against cycle 8 is the level-versus-pulse distinction. frame_tick is high for one cycle; sof_req stays high across four. Mutation F4 makes the request a pulse and costs 3585 failures — because a packet engine that is busy for even one cycle then never sees it.
Cycle 11 is the collision from §4. The boundary arrives, the previous SOF was never taken, the request is renewed for the new frame, and the loss is reported rather than discarded.
14. Assertions
// A1. SAFETY: frame_tick is a one-cycle pulse. A level here would make
// every downstream scheduler charge the same boundary repeatedly.
property p_tick_is_pulse;
@(posedge clk) disable iff (!rst_n || bus_reset)
frame_tick |=> !frame_tick;
endproperty
a_tick_is_pulse: assert property (p_tick_is_pulse);
// A2. SAFETY: the frame number changes ONLY at a boundary.
property p_number_stable_between;
@(posedge clk) disable iff (!rst_n || bus_reset)
!frame_tick |=> $stable(frame_no);
endproperty
a_number_stable_between: assert property (p_number_stable_between);
// A3. SAFETY, and the protocol modulus stated directly: the number always
// advances by exactly one, modulo 2048. Written against an observed
// boundary rather than against the DUT's own wrap decision, which is
// what mutations F1 and F2 corrupt.
property p_modulus;
@(posedge clk) disable iff (!rst_n || bus_reset)
frame_tick |-> (frame_no == ((($past(frame_no)) + 1) % 2048));
endproperty
a_modulus: assert property (p_modulus);
// A4. SAFETY: sof_req is a LEVEL -- once asserted it stays asserted until
// an ack or a boundary. This is mutation F4's inverse.
property p_req_is_level;
@(posedge clk) disable iff (!rst_n || bus_reset)
(sof_req && !sof_ack && !frame_tick) |=> sof_req;
endproperty
a_req_is_level: assert property (p_req_is_level);
// A5. PROGRESS: an enabled port eventually produces a boundary. A timer
// that never ticks satisfies A1, A2, A3 and A4 perfectly.
property p_eventually_ticks;
@(posedge clk) disable iff (!rst_n || bus_reset)
enable |-> ##[1:TICKS_PER_FRAME] frame_tick;
endproperty
a_eventually_ticks: assert property (p_eventually_ticks);
// A6. SAFETY: a disabled port produces no boundaries and holds the number.
property p_disabled_is_frozen;
@(posedge clk) disable iff (!rst_n || bus_reset)
!enable |=> !frame_tick && $stable(frame_no);
endproperty
a_disabled_is_frozen: assert property (p_disabled_is_frozen);Assertion contracts
| Claim | Safety / progress | Vacuity risk | How non-vacuity is established | |
|---|---|---|---|---|
| A1 | frame_tick is a pulse | safety | low | 2553 boundaries measured |
| A2 | the number changes only at a boundary | safety | none in practice — most cycles are non-boundary | holds constantly |
| A3 | the number advances modulo 2048 | safety | moderate — no boundaries makes it vacuous | paired with A5 |
| A4 | sof_req is a level | safety | moderate — needs an un-acked request | 24 missed SOFs and many held cycles |
| A5 | an enabled port eventually ticks | progress | low | enable is high for almost the whole run |
| A6 | a disabled port is frozen | safety | moderate — needs enable low | one directed three-frame disabled window |
A5 is the only progress property, and §47's argument applies exactly. A timer whose at_boundary never became true — TICKS_PER_FRAME mis-parameterised, the comparison inverted, the enable stuck low — would satisfy A1, A2, A3, A4 and A6 perfectly and would stop every scheduler in Module 17 dead. Safety properties cannot distinguish a correct timer from one that has stopped.
A3 is written against $past(frame_no) and an observed frame_tick, not against the design's own wrap comparison. §37's principle: a property phrased in terms of the decision under test cannot detect a design that is wrong about that decision, and F1 and F2 corrupt exactly that comparison.
15. Verification: Why This Block Does Not Need UVM
Chapter 16.2 adopted UVM for frame reassembly; 16.4 declined it for admission arithmetic. This block is firmly in the second category, and the reason is worth stating because Module 17 will change it.
The input space is three bits and a parameter. enable, sof_ack, bus_reset. There are no transactions, no ordering, no error classes, and the only interesting sequences — a boundary with an un-acked SOF, an ack in the boundary cycle, a disabled window — are four directed tests, not a stimulus distribution.
A UVM environment here would randomise over a space that a dozen directed cases already cover exhaustively. §26's warning about fake completeness applies to verification environments as much as to RTL.
Where UVM arrives in this module is Chapter 17.3, where multiple flows with different service requirements compete for one opportunity and the interesting question becomes which candidate should have been selected, and did the loser survive. That is a scenario space; this is a counter.
16. Debugging: the Device That Drifts Out of Its Polling Schedule
A high-speed interrupt device with an 8 ms interval works correctly for minutes at a time, then begins missing polls. A bus analyser shows SOF packets arriving normally. The device's own frame counter and the host's disagree by a growing amount. Re-enumerating fixes it, for another few minutes.
The symptom names the class before anything is instrumented. A growing disagreement between two counters that should advance together is an accumulating rate error, not a lost event — the same reasoning 16.1 §17 applied to a clicking headset, and the same arithmetic works here.
Convert the drift rate into a defect. If the counters diverge by one frame every N frames, the frame period is wrong by 1/N. A frame one controller clock too long at 48 MHz and 48 000 clocks per frame is a 1-in-48 000 error — about 21 ppm, which is one frame of drift every 48 seconds. Mutation F3 is exactly that defect, and it is the first thing to check.
The chain, from the outside in:
Analyser — are SOFs arriving at 1 ms? Measure the interval, not the presence. SOFs "arriving normally" in the symptom description means present, which is not the same as on time.
Analyser — do the frame numbers advance by one? A number that skips is a different defect from a number that is late. Skipping points at sof_missed (§4) — the packet engine not taking requests — rather than at the time base.
Controller — does sof_missed ever pulse? If it does, the time base is correct and the packet engine is the fault. The two failures look identical on the wire and are distinguished immediately by one internal signal, which is why it is an output rather than an internal condition.
RTL — the first divergence. Compare the bench's independent boundary count against the design's frame_no at each boundary. The first boundary where they differ is the bug, and in practice it is F3 (a comparison against the wrong limit), F1 (the wrong modulus) or F6 (a bus reset that did not restart numbering).
17. Common Misconceptions
"A frame is a quantity of data." It is an interval of bus time — a repeating opportunity (§1). What fits inside it is Chapter 17.4's subject.
"A frame boundary is a clock edge." It is derived by counting controller clocks (§3). frame_tick is a qualified controller-domain pulse, not the USB wire clock.
"The frame counter wraps at 2048." The SOF field is 11 bits. The host controller's counter is implementation-specific, from 256 to 65536 frames (§2) — and a driver assuming 2048 is wrong on most controllers.
"The explicit wrap is redundant, so it can be removed." It is redundant only because the counter happens to be exactly 11 bits (§12). Widen the counter and it becomes essential.
"sof_req can be a pulse — the packet engine is always ready." Mutation F4 costs 3585 failures (§10). A packet engine busy for one cycle loses the request.
"A bus reset should preserve the frame number so the device stays in step." The opposite: the device restarts its frame-derived state, so preserving the host's count guarantees disagreement (§4).
"If every safety property passes, the timer works." A timer that never ticks passes five of the six properties in §14.
18. Exercises
1. A controller runs at 60 MHz. Compute TICKS_PER_FRAME and the drift in ppm if the comparison is against TICKS_PER_FRAME rather than TICKS_PER_FRAME − 1. Convert that into frames of drift per minute.
2. Widen frame_r to 16 bits while keeping the 11-bit protocol modulus, then re-run mutation F2. Predict the failure count before running it, using §12's argument.
3. The design holds the frame number and parks the position when enable is low. Construct the argument for the opposite choice on each — holding the position, or restarting the number — and say what breaks in Chapter 15.1's device under each.
4. Add a host-controller-visible frame counter of HC_FRAME_W bits alongside the 11-bit protocol number, as §2 describes. Decide whether it is a separate counter or a widening of the existing one, and justify the answer from §12.
5. Write an SVA property that catches F3 (a frame one clock too long) without referring to TICKS_PER_FRAME. If you conclude it cannot be done, say what external reference the property needs.
6. §11 found that F7 was equivalent in the Verilog because a dominating outer guard made the mutated expression dead. Find another expression in this chapter's Verilog that is dominated by an outer condition, and say whether mutating it would be detectable.
7. The bench's align_to_boundary fixed three mis-aimed checks (§9). Determine whether those three checks would have been caught by the randomised phase, and explain what that says about directed tests that hardcode cycle counts.
19. Summary
The frame is the unit of scheduling opportunity (§1), and the three service requirements Modules 14–16 established are all denominated in it — which is why the time base must exist before any scheduler can be built.
The SOF carries an 11-bit frame number (§2), and that is normative. The host controller's own frame counter is not: the kernel documents it as implementation-specific, from 256 to 65536 frames. Two counters, one protocol field, and only one of them is fixed.
The frame boundary is derived by counting controller clocks (§3), and frame_tick is a qualified controller-domain pulse — not the wire clock, not the SOF packet. Four distinct time concepts are now in play and 17.2 adds two more.
The SOF request is a level and the frame tick is a pulse (§4), and a boundary arriving with the previous SOF unsent reports the loss rather than discarding it silently.
All three HDL implementations were simulated (§20), and seven mutations died in all three (§10) — but two of them required investigation rather than recording. F7 was a genuinely equivalent mutation in the Verilog (§11), because it landed behind a dominating if (!enable) guard that the SystemVerilog and VHDL express as a case selection; moving it to the guard gave exact agreement at 116.
And F2 dies by exactly one failure, on a flag (§12). The frame number wraps correctly without the explicit wrap because an 11-bit counter's natural overflow lands on the 11-bit protocol modulus — a coincidence, demonstrated directly, that disappears the moment the counter is widened. §2 established that real controllers do widen it.
20. Tooling, Honestly
| Language | Design | Testbench | Analysed / compiled | Simulated | Mutations |
|---|---|---|---|---|---|
| Verilog-2005 | usb_frame_timer | ft_v_tb.v | ✅ Icarus -g2005 | ✅ 0 errors | ✅ all seven |
| SystemVerilog | usb_frame_timer_sv | ft_sv_tb.sv | ✅ Icarus -g2012 | ✅ 0 errors | ✅ all seven |
| VHDL-2008 | usb_frame_timer_vhdl | ft_vhdl_tb.vhd | ✅ nvc 1.23.0 | ✅ 0 errors | ✅ all seven |
| SVA (§14) | — | — | ❌ unsupported by Icarus | ❌ | — |
unique case | — | — | ✅ accepted | ⚠️ quality ignored | — |
The SystemVerilog bench is generated from the Verilog bench's scenario list rather than written independently, so the two drive identical stimulus. That is deliberate for a time base — it makes the cross-HDL comparison a clean equivalence check — but it means the two benches are not independent of one another, and §10's near-identical counts demonstrate design equivalence rather than stimulus diversity. The VHDL bench uses uniform and therefore diverges in the randomised phase, which is where its counts differ.
21. What Comes Next
This chapter built one time base: a 1 ms frame, numbered modulo 2048, with a boundary derived from a controller clock.
Chapter 17.2 adds a second one inside it. High-speed USB divides every frame into eight 125 µs microframes, and the relationship between the two is not a simple substitution — the frame number does not advance eight times faster. It advances once per millisecond, exactly as here, while a separate three-bit counter walks 0 through 7 beneath it.
That creates a structure with two nested moduli and one subtlety that catches almost everyone: for eight consecutive scheduling opportunities, the frame number on the wire is the same value. What distinguishes them is not the number the SOF carries — and a device or controller that identifies a service opportunity by frame number alone at high speed has eight ways to be wrong.
Browse the full path on the USB tutorials index.
Continue learning
Related tutorials
- Related topic
Host-Side Scheduling
A USB transaction cannot be stopped once its token goes out, so the scheduler must ask whether it will finish before it starts — and the periodic reserve exists to protect bulk traffic, not to limit isochronous.
- Related topic
The Scheduling Question
Periodic traffic is placed first because that is how admission control's promise is kept — and bulk is round-robin because a rotating pointer is the only fairness mechanism in the entire schedule.
- Related topic
Mice on USB
A relative report cannot be resent, so the accumulator must saturate rather than wrap and must be cleared by the act of being read — and a real signedness bug the testbench caught on its first check.
- Related topic
Audio over Isochronous
Isochronous gives up the retry and the NAK, so a device cannot slow the host down — it can only report the rate it needs. The feedback accumulator in three HDLs, and the clamp that hid a defect from eight of nine chances to catch it.
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.
