USB · Module 17
Microframe Scheduling (125 µs)
High speed divides every frame into eight 125 µs microframes — and the frame number does not advance eight times faster. Two nested moduli, and the mutation a range-constrained VHDL type caught that the others could not.
Chapter 17.1 built one time base: a 1 ms frame, numbered modulo 2048, its boundary derived by counting controller clocks. Every service requirement in Modules 14–16 was denominated in those frames.
High speed keeps that frame and puts eight scheduling opportunities inside it.
The obvious guess about what that means to the hardware is wrong, and it is wrong in a way that produces working silicon which fails subtly. The frame number does not advance eight times faster. It advances once per millisecond — exactly as in 17.1 — while a separate three-bit index walks beneath it.
This chapter builds that nested structure, and its central property is one sentence: for eight consecutive scheduling opportunities, the number the SOF carries is the same value.
1. Eight Per Millisecond
The relationship is stated directly in the Linux USB core, in the documentation of how interrupt endpoints express their intervals:
* Note that High Speed and SuperSpeed(+) interrupt endpoints use a logarithmic
* encoding of the endpoint interval, and express polling intervals in
* microframes (eight per millisecond) rather than in frames (one per
* millisecond)."Microframes (eight per millisecond) rather than in frames (one per millisecond)." That is the whole arithmetic: 1 ms ÷ 8 = 125 µs, and Chapter 15.4 already relied on it when it decoded bInterval as 2^(bInterval−1) microframes.
What changes for the scheduler is the rate of opportunity, not the numbering. Eight times as many moments at which work can be placed; the same frame number stamped on all eight.
| Full speed | High speed | |
|---|---|---|
| Scheduling opportunity | every 1 ms | every 125 µs |
| Opportunities per frame | 1 | 8 |
| Frame number advances | once per opportunity | once per eight |
bInterval units | frames | microframes |
2. The Number Is the Same Eight Times
This is the property that catches people, so it is worth stating it in the form a device actually experiences.
A high-speed device receives eight SOF packets per millisecond. All eight carry the same 11-bit frame number. The number changes on the ninth.
So the microframe index is not transmitted in that field. A device that needs to know which of the eight it is in counts SOFs between frame-number changes — the index is derived, not received.
3. Two Moduli, Nested — Not One Faster Counter
The structure follows directly, and the phrasing matters:
controller clocks --> microframe (125 us) --> frame (1 ms)
index counts 0..7 number advances ONCE per 8A frame boundary is not a separate event from a microframe boundary. It is the microframe boundary that rolls the index over — always also a microframe boundary, never instead of one. Deriving it that way rather than running a second independent counter is what keeps the two moduli from drifting apart, and it is why this chapter's RTL computes at_fbound from at_ubound rather than counting to 8000 separately.
Six time concepts are now in play. Chapter 17.1 §3 named four; this chapter adds two:
| Term | What it is | Who defines it |
|---|---|---|
| controller clock | the RTL clock | the implementation |
| microframe | a 125 µs interval | USB |
| frame | a 1 ms interval — eight microframes | USB |
| frame number | the 11-bit value the SOF carries | USB |
| microframe index | 0–7 within the frame, derived not received | USB structure, local state |
| frame counter | what software reads back | the host controller |
uframe_tick and frame_tick are both controller-domain pulses, and every waveform in this chapter is in that domain.
4. The Hardware, Before Any Language
State retained: the frame number, the microframe index, the position within the current microframe, and a pending SOF request.
On each controller clock while operating: the position advances.
When the position has run a whole microframe: the position restarts, uframe_tick pulses, and a SOF is requested — because a SOF is transmitted in every microframe, carrying the same number eight times over.
If the index is below 7: it advances, and that is all. This is not a frame boundary.
If the index is 7: it rolls to 0, frame_tick pulses in addition to uframe_tick, and the frame number advances — modulo 2048.
When the port is not operating: both protocol quantities are held — the number and the index, because a device is tracking both — and only the local position is parked.
On reset or bus reset: both restart at zero.
The missed-SOF report is per microframe. A boundary arriving with the previous SOF un-acked renews the request and reports the loss, exactly as in 17.1 §4 — but eight times as often, which is why the report matters more here.
5. Verilog
The RTL contract
- What it models: the high-speed time base — eight 125 µs microframes nested inside each 1 ms frame.
- Why it exists: because the scheduling opportunity is now the microframe (§1) while the frame number still advances once per millisecond (§2).
- Inputs:
enable,sof_ack,bus_reset. - Authoritative state:
frame_randuf_r— both protocol quantities;pos_r— a local measurement. - Derived state:
at_ubound, andat_fboundderived from it rather than counted separately. - Outputs:
sof_frame_no,uframe,uframe_pos,uframe_tick,frame_tick,sof_req,frame_wrapped,sof_missed. - Hardware implied: one
POS_Wcounter, one 3-bit counter, one 11-bit counter, two comparators, five flags. - Reset: asynchronous active-low
rst_n;bus_resetsynchronous and equivalent, and both restart the number and the index. - Priority:
!enableoutranks the boundary, which outranks the ordinary count. A frame boundary is a microframe boundary that also rolls the index. - Latency: one cycle; every output is registered.
- Boundary behaviour: the index wraps at 8, the number at 2048 — two nested moduli.
- Collision behaviour: a boundary with an un-acked SOF renews the request and pulses
sof_missed, eight times per frame rather than once. - Assumptions:
TICKS_PER_UFRAMEcontroller clocks equal one 125 µs microframe. - Omissions: no PHY, no packet engine, no split transactions, no transaction translator — those are Module 18's.
- What DV should verify: that the frame number is unchanged across eight consecutive microframe boundaries; that every index 0–7 is visited; that a frame boundary lands exactly where the index rolls to zero; that both quantities survive a disable and restart on a bus reset.
// usb_uframe_timer -- the high-speed time base: eight 125 us microframes
// nested inside each 1 ms frame.
//
// The structure is two moduli, not one faster counter:
//
// controller clocks --> microframe (125 us) --> frame (1 ms)
// counts 0..7 advances ONCE per 8
//
// The subtlety that catches almost everyone: the frame NUMBER does not
// advance eight times faster at high speed. It advances once per
// millisecond, exactly as at full speed, while a three-bit index walks
// beneath it. So for EIGHT consecutive scheduling opportunities the number
// the SOF carries is the SAME value, and a device or controller that
// identifies a service opportunity by frame number alone has eight ways to
// be wrong.
//
// `sof_frame_no` is what the SOF token carries. `uframe` is the controller's
// own index and is NOT transmitted in that field -- a device tracks it by
// counting SOFs between frame-number changes.
module usb_uframe_timer #(
parameter integer TICKS_PER_UFRAME = 6000, // controller clocks per 125 us
parameter integer FRAME_W = 11,
parameter integer POS_W = 16
) (
input wire clk,
input wire rst_n,
input wire bus_reset,
input wire enable,
input wire sof_ack,
output wire [FRAME_W-1:0] sof_frame_no, // the value the SOF carries
output wire [2:0] uframe, // 0..7, NOT on the wire
output wire [POS_W-1:0] uframe_pos,
output wire uframe_tick, // every 125 us
output wire frame_tick, // every 1 ms (uframe 7 -> 0)
output wire sof_req,
output wire frame_wrapped,
output wire sof_missed
);
localparam [FRAME_W-1:0] FRAME_MAX = {FRAME_W{1'b1}}; // 2047
localparam [2:0] UF_MAX = 3'd7;
localparam [POS_W-1:0] POS_LAST = TICKS_PER_UFRAME - 1;
reg [FRAME_W-1:0] frame_r;
reg [2:0] uf_r;
reg [POS_W-1:0] pos_r;
reg uftick_r, ftick_r, wrap_r, sofreq_r, missed_r;
assign sof_frame_no = frame_r;
assign uframe = uf_r;
assign uframe_pos = pos_r;
assign uframe_tick = uftick_r;
assign frame_tick = ftick_r;
assign sof_req = sofreq_r;
assign frame_wrapped = wrap_r;
assign sof_missed = missed_r;
wire at_ubound = enable && (pos_r == POS_LAST);
// The FRAME boundary is the microframe boundary that rolls the index over.
// It is a derived condition, not a second counter -- deriving it keeps the
// two moduli from drifting apart, which they can if each is counted alone.
wire at_fbound = at_ubound && (uf_r == UF_MAX);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
frame_r <= {FRAME_W{1'b0}}; uf_r <= 3'd0; pos_r <= {POS_W{1'b0}};
uftick_r <= 1'b0; ftick_r <= 1'b0; wrap_r <= 1'b0;
sofreq_r <= 1'b0; missed_r <= 1'b0;
end else if (bus_reset) begin
frame_r <= {FRAME_W{1'b0}}; uf_r <= 3'd0; pos_r <= {POS_W{1'b0}};
uftick_r <= 1'b0; ftick_r <= 1'b0; wrap_r <= 1'b0;
sofreq_r <= 1'b0; missed_r <= 1'b0;
end else begin
uftick_r <= 1'b0;
ftick_r <= 1'b0;
wrap_r <= 1'b0;
missed_r <= 1'b0;
if (sof_ack) sofreq_r <= 1'b0;
if (!enable) begin
// Hold BOTH protocol quantities -- the frame number and the
// microframe index are each part of the schedule the device is
// tracking. Park only the local position.
pos_r <= {POS_W{1'b0}};
end else if (at_ubound) begin
pos_r <= {POS_W{1'b0}};
uftick_r <= 1'b1;
// A SOF is transmitted in EVERY microframe, so the request is
// renewed eight times per frame -- carrying the same number each
// time. The missed-SOF report is per microframe accordingly.
if (sofreq_r && !sof_ack) missed_r <= 1'b1;
sofreq_r <= 1'b1;
if (uf_r == UF_MAX) begin
uf_r <= 3'd0;
ftick_r <= 1'b1;
// The frame number advances HERE -- once per eight microframes.
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
uf_r <= uf_r + 3'd1;
end
end else begin
pos_r <= pos_r + {{(POS_W-1){1'b0}}, 1'b1};
end
end
end
endmoduleTwo details are worth naming.
at_fbound is derived from at_ubound, not counted separately. A second counter running to 8000 controller clocks would be a second opinion about when a millisecond has elapsed, and two opinions drift. Deriving the frame boundary as the microframe boundary that rolls the index makes drift structurally impossible.
Both protocol quantities are held when the port is disabled. Chapter 17.1 held one; here there are two, and a device tracking its position within a frame needs both. Only pos_r — the local measurement — is parked.
6. SystemVerilog
package usb_uframe_pkg;
// What this microframe boundary is. The three cases are NESTED moduli, not
// three independent events, and naming them keeps that visible: a frame
// boundary is always also a microframe boundary, never instead of one.
typedef enum logic [1:0] {
B_NONE, // an ordinary controller clock inside the microframe
B_UFRAME, // a 125 us boundary that does NOT roll the index over
B_FRAME // the 125 us boundary that rolls 7 -> 0: also a 1 ms boundary
} bnd_e;
endpackage
module usb_uframe_timer_sv
import usb_uframe_pkg::*;
#(
parameter int unsigned TICKS_PER_UFRAME = 6000,
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] sof_frame_no,
output logic [2:0] uframe,
output logic [POS_W-1:0] uframe_pos,
output logic uframe_tick,
output logic frame_tick,
output logic sof_req,
output logic frame_wrapped,
output logic sof_missed
);
initial begin
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_UFRAME < 2)
$fatal(1, "TICKS_PER_UFRAME=%0d leaves no room for a microframe",
TICKS_PER_UFRAME);
if ((TICKS_PER_UFRAME - 1) >= (1 << POS_W))
$fatal(1, "POS_W=%0d cannot represent a microframe of %0d clocks",
POS_W, TICKS_PER_UFRAME);
end
localparam logic [FRAME_W-1:0] FRAME_MAX = '1; // 2047
localparam logic [2:0] UF_MAX = 3'd7;
localparam logic [POS_W-1:0] POS_LAST = POS_W'(TICKS_PER_UFRAME - 1);
bnd_e bnd;
always_comb begin
if (!enable || (uframe_pos != POS_LAST)) bnd = B_NONE;
else if (uframe == UF_MAX) bnd = B_FRAME;
else bnd = B_UFRAME;
end
// A microframe boundary is EITHER kind. Deriving it from the enum rather
// than recomputing the comparison keeps the two from drifting apart.
wire at_ubound = (bnd != B_NONE);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n || bus_reset) begin
sof_frame_no <= '0; uframe <= '0; uframe_pos <= '0;
uframe_tick <= 1'b0; frame_tick <= 1'b0; frame_wrapped <= 1'b0;
sof_req <= 1'b0; sof_missed <= 1'b0;
end else begin
uframe_tick <= 1'b0;
frame_tick <= 1'b0;
frame_wrapped <= 1'b0;
sof_missed <= 1'b0;
if (sof_ack) sof_req <= 1'b0;
if (!at_ubound) begin
// Hold BOTH protocol quantities when disabled; park only the local
// position. A disabled port must not resume mid-microframe.
if (!enable) uframe_pos <= '0;
else uframe_pos <= uframe_pos + 1'b1;
end else begin
uframe_pos <= '0;
uframe_tick <= 1'b1;
// A SOF is transmitted in EVERY microframe, carrying the same number
// eight times over, so the request is renewed per microframe.
if (sof_req && !sof_ack) sof_missed <= 1'b1;
sof_req <= 1'b1;
unique case (bnd)
B_UFRAME: begin
uframe <= uframe + 3'd1;
end
B_FRAME: begin
uframe <= '0;
frame_tick <= 1'b1;
// The frame number advances HERE -- once per eight microframes.
if (sof_frame_no == FRAME_MAX) begin
sof_frame_no <= '0;
frame_wrapped <= 1'b1;
end else begin
sof_frame_no <= sof_frame_no + 1'b1;
end
end
B_NONE: ; // unreachable here; stated so the case is exhaustive
endcase
end
end
end
endmodulebnd_e names the nesting rather than describing it. B_UFRAME and B_FRAME are both microframe boundaries; the enumeration makes that a fact about the type rather than a comment, and at_ubound is then derived from the enum — (bnd != B_NONE) — instead of recomputing the comparison. Two expressions that must agree have become one.
7. VHDL
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package usb_uframe_pkg is
-- What this microframe boundary is. The three cases are NESTED moduli, not
-- three independent events: a frame boundary is always also a microframe
-- boundary, never instead of one.
type bnd_t is (B_NONE, B_UFRAME, B_FRAME);
end package;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb_uframe_pkg.all;
entity usb_uframe_timer_vhdl is
generic (
TICKS_PER_UFRAME : positive := 6000;
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;
sof_frame_no : out unsigned(FRAME_W-1 downto 0);
uframe : out unsigned(2 downto 0);
uframe_pos : out unsigned(POS_W-1 downto 0);
uframe_tick : out std_logic;
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_uframe_timer_vhdl is
constant FRAME_MAX : unsigned(FRAME_W-1 downto 0) := (others => '1');
constant UF_MAX : unsigned(2 downto 0) := to_unsigned(7, 3);
constant POS_LAST : unsigned(POS_W-1 downto 0) :=
to_unsigned(TICKS_PER_UFRAME - 1, POS_W);
signal frame_r : unsigned(FRAME_W-1 downto 0) := (others => '0');
-- The index is range-constrained: a value outside 0..7 is an error the
-- simulator reports rather than a silent wrap. Chapter 16.5 measured what
-- that buys when a mutation pushes a counter past its limit.
signal uf_r : integer range 0 to 7 := 0;
signal pos_r : unsigned(POS_W-1 downto 0) := (others => '0');
signal uftick_r, ftick_r, wrap_r, sofreq_r, missed_r : std_logic := '0';
signal bnd : bnd_t;
signal at_ubound : boolean;
begin
assert FRAME_W = 11
report "the USB 2.0 SOF frame-number field is 11 bits" severity failure;
assert TICKS_PER_UFRAME >= 2
report "TICKS_PER_UFRAME leaves no room for a microframe" severity failure;
assert TICKS_PER_UFRAME - 1 < 2**POS_W
report "POS_W cannot represent a microframe of this many clocks"
severity failure;
sof_frame_no <= frame_r;
uframe <= to_unsigned(uf_r, 3);
uframe_pos <= pos_r;
uframe_tick <= uftick_r;
frame_tick <= ftick_r;
sof_req <= sofreq_r;
frame_wrapped <= wrap_r;
sof_missed <= missed_r;
bnd <= B_NONE when enable = '0' or pos_r /= POS_LAST else
B_FRAME when uf_r = 7 else
B_UFRAME;
at_ubound <= bnd /= B_NONE;
process (clk, rst_n)
begin
if rst_n = '0' then
frame_r <= (others => '0'); uf_r <= 0; pos_r <= (others => '0');
uftick_r <= '0'; ftick_r <= '0'; wrap_r <= '0';
sofreq_r <= '0'; missed_r <= '0';
elsif rising_edge(clk) then
if bus_reset = '1' then
frame_r <= (others => '0'); uf_r <= 0; pos_r <= (others => '0');
uftick_r <= '0'; ftick_r <= '0'; wrap_r <= '0';
sofreq_r <= '0'; missed_r <= '0';
else
uftick_r <= '0';
ftick_r <= '0';
wrap_r <= '0';
missed_r <= '0';
if sof_ack = '1' then
sofreq_r <= '0';
end if;
if not at_ubound then
-- Hold BOTH protocol quantities when disabled; park only the local
-- position. A disabled port must not resume mid-microframe.
if enable = '0' then
pos_r <= (others => '0');
else
pos_r <= pos_r + 1;
end if;
else
pos_r <= (others => '0');
uftick_r <= '1';
-- A SOF is transmitted in EVERY microframe, carrying the same
-- number eight times over, so the request is renewed per
-- microframe and so is the missed-SOF report.
if sofreq_r = '1' and sof_ack = '0' then
missed_r <= '1';
end if;
sofreq_r <= '1';
case bnd is
when B_UFRAME =>
uf_r <= uf_r + 1;
when B_FRAME =>
uf_r <= 0;
ftick_r <= '1';
-- The frame number advances HERE: once per eight microframes.
if frame_r = FRAME_MAX then
frame_r <= (others => '0');
wrap_r <= '1';
else
frame_r <= frame_r + 1;
end if;
when B_NONE =>
null; -- unreachable here; stated so the case is exhaustive
end case;
end if;
end if;
end if;
end process;
end architecture;uf_r is integer range 0 to 7, and that is the consequential line. §11 measures what it does when a mutation pushes the index past its limit: the simulation stops, naming the signal and the line, where the other two languages wrap silently. Chapter 16.5 found the same property on a different counter, and finding it twice in two modules is what makes it a property of the language rather than a coincidence.
8. Comparing the Three
| Concern | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| The nesting | at_fbound derived from at_ubound | bnd_e — the enum is the nesting | bnd_t, no encoding |
| The microframe index | reg [2:0], wraps silently | logic [2:0], same | integer range 0 to 7 — range-checked |
| Illegal parameterisation | undetected | three $fatal guards | three assert ... severity failure |
| Case exhaustiveness | not checked | unique — but see §18 | enforced by the type |
9. The Testbenches
The model keeps ONE absolute microframe count and derives both quantities by division:
check(sof_frame_no === FRAME_W'((ubounds/UF_MOD) % FRAME_MOD), "sof_frame_no");
check(uframe === 3'(ubounds % UF_MOD), "uframe");A nesting error cannot be reproduced by a division of one total. A frame number advancing per microframe, an index failing to roll over, a frame boundary in the wrong place — each breaks the relationship between the two quantities, and the model derives both from a single number that has no such relationship to break.
The directed sequence is built around §2's property:
| Scenario | What it pins down |
|---|---|
| one microframe boundary | it is not a frame boundary; the index advances and the number does not |
| a whole frame's worth | the number is unchanged across the microframes |
| a counted run | eight microframe ticks per frame tick, exactly |
| three frames | every index 0–7 is visited |
| every frame boundary | lands exactly where the index rolls to zero |
enable low | both quantities held, position parked |
bus_reset at a non-zero index | both restart — see below |
| 2048 frames × 8 | the nested wrap, directed |
| 4000 randomised acks | independently drawn |
10. Mutation Testing — Across All Three Languages
| ID | Mutation | Verilog | SystemVerilog | VHDL | Killed |
|---|---|---|---|---|---|
| — | baseline, no mutation | 0 | 0 | 0 | — |
| U1 | the frame number advances every microframe | 146281 | 146281 | 146281 | ✅ all three |
| U2 | the index wraps one late (0…8) | 146281 | 146281 | FATAL | ✅ all three — see §11 |
| U3 | every microframe tick is also a frame tick | 15298 | 15298 | 15298 | ✅ all three |
| U4 | the index is never reset at a frame boundary | 146178 | 146178 | 146178 | ✅ all three |
| U5 | the frame boundary is placed at index 0 | 146281 | 146281 | 146281 | ✅ all three |
| U6 | bus_reset does not clear the index | 99993 | see note | 99993 | ✅ after §9's fix |
U6's SystemVerilog cell is a tooling limit, not a missing result. Verilog and VHDL both measure 99 993, against 0 before §9's stimulus fix. The SystemVerilog run of the same mutation, with the same stimulus, had not completed after fourteen minutes of CPU when this chapter was finalised — against ten seconds for Verilog and about a minute for VHDL.
That 80× spread is worth recording as a tooling observation rather than hidden. The three benches drive identical directed stimulus and the baseline runs are comparable — the SystemVerilog baseline is in fact the fastest of the three at about a second. It is only the failing path that diverges, and only under Icarus: a mutation producing roughly a hundred thousand failures costs seconds in one language front-end and more than a quarter of an hour in another, on the same simulator and the same machine.
The cell says "see note" because an unmeasured figure is an invented figure. Chapter 16.5 §20 declined to invent a count for a fatal VHDL run for exactly this reason, and the discipline is the same one: report what the tool actually produced.
U1 is the chapter's thesis measured. Advancing the frame number eight times too fast costs 146 281 failures — the largest count in Module 17 — because it breaks the relationship between the two quantities on every single microframe, not at a boundary.
U3's much smaller 15 298 is worth reading. Asserting frame_tick on every microframe leaves both counters correct; only the tick is wrong. The quantities are right and the event is wrong — which is exactly the kind of defect that survives a bench checking values but not edges.
11. The Mutation the Type System Caught
U2 does not produce a failure count in VHDL. It stops the simulation:
** Fatal: 335ns+0: value 8 outside of INTEGER range 0 to 7 for signal UF_R
> uf_vhdl_U2.vhd:123
|
123 | uf_r <= uf_r + 1;
| ^^^^^^^^At 335 nanoseconds, naming the signal, the line and the illegal value. The Verilog and SystemVerilog versions wrap a 3-bit counter from 7 to 0 silently and produce 146 281 checker failures much later, downstream of the corruption.
12. The Waveform
Eight microframes, one frame number
9 cyclesThis waveform is in the controller clock domain and one column is one microframe boundary, not one clock. The underlying simulation ran two controller clocks per microframe; the columns here are the boundaries themselves, because the boundaries are what a scheduler sees.
Columns 0 through 7 are the chapter. Eight distinct scheduling opportunities, eight SOF packets, one frame number. A controller that keyed its schedule on sof_frame_no alone would place work in some one of those eight — and would appear to work.
13. Assertions
// A1. THE property of this chapter: the frame number changes ONLY at a
// frame boundary -- never at an ordinary microframe boundary.
property p_number_only_at_frame;
@(posedge clk) disable iff (!rst_n || bus_reset)
!frame_tick |=> $stable(sof_frame_no);
endproperty
a_number_only_at_frame: assert property (p_number_only_at_frame);
// A2. SAFETY: a frame boundary is ALWAYS also a microframe boundary. The
// nesting stated as an implication, so a design that produced one
// without the other would fail immediately.
property p_frame_implies_uframe;
@(posedge clk) disable iff (!rst_n)
frame_tick |-> uframe_tick;
endproperty
a_frame_implies_uframe: assert property (p_frame_implies_uframe);
// A3. SAFETY: a frame boundary lands exactly where the index rolls to 0.
property p_frame_at_index_zero;
@(posedge clk) disable iff (!rst_n || bus_reset)
frame_tick |-> (uframe == 3'd0);
endproperty
a_frame_at_index_zero: assert property (p_frame_at_index_zero);
// A4. BOUNDEDNESS -- the property VHDL's subtype enforces for free.
property p_index_in_range;
@(posedge clk) disable iff (!rst_n)
(uframe <= 3'd7);
endproperty
a_index_in_range: assert property (p_index_in_range);
// A5. COUNTING: exactly eight microframe ticks between frame ticks.
// Written against the bench's own tick count, not the DUT's index.
property p_eight_per_frame;
@(posedge clk) disable iff (!rst_n || bus_reset)
frame_tick |-> (uticks_since_last_frame == 8);
endproperty
a_eight_per_frame: assert property (p_eight_per_frame);
// A6. PROGRESS: an enabled port eventually produces a frame boundary. A
// timer that ticks microframes for ever without ever rolling the index
// satisfies A1, A2, A3 and A4 perfectly -- and is mutation U4.
property p_eventually_frames;
@(posedge clk) disable iff (!rst_n || bus_reset)
enable |-> ##[1:(8*TICKS_PER_UFRAME)] frame_tick;
endproperty
a_eventually_frames: assert property (p_eventually_frames);Assertion contracts
| Claim | Safety / progress | Vacuity risk | How non-vacuity is established | |
|---|---|---|---|---|
| A1 | the number changes only at a frame boundary | safety | none — most cycles are not boundaries | holds constantly |
| A2 | a frame boundary is also a microframe boundary | safety | moderate — needs frame boundaries | 2183 frame ticks measured |
| A3 | a frame boundary lands at index 0 | safety | moderate — same antecedent | same |
| A4 | the index stays in 0–7 | safety | none — no antecedent | holds every cycle |
| A5 | exactly eight microframes per frame | safety | moderate | 2183 frame ticks |
| A6 | an enabled port eventually frames | progress | low | enable is high for almost the whole run |
A6 is the only progress property and it is the one that catches U4. An index that never resets never reaches 7 again, so frame_tick never fires again — and A1, A2, A3 and A4 all continue to hold perfectly, because a design that stops producing boundaries violates no property phrased as when a boundary occurs…. Chapter 17.1 §14 made the same argument about a timer that stops; this is the same failure one level up.
A4 is exactly what VHDL's integer range 0 to 7 provides without being written (§11) — and in the Verilog and SystemVerilog it must be written, because a 3-bit vector makes the property trivially true while the intent is that the index never attempt to exceed 7.
14. Verification: Still Not a UVM Problem
Chapter 17.1 §15 declined UVM for a counter and promised it would arrive in 17.3. This chapter is still a counter — two of them — and the same argument applies unchanged.
The input space is three bits and two parameters. The interesting scenarios are the nesting property, the index walk, the reset-from-non-zero case §9 found, and the nested wrap. Every one of them is a directed test, and §9 is the evidence that the directed tests are where the value was: the randomised phase contributed nothing to U6 and the fix was two lines of directed stimulus.
Where the scenario space genuinely begins is the next chapter, where multiple flows with different service requirements compete for one of these opportunities and the question becomes which one should have been chosen, and did the losers survive.
15. Debugging: the Device That Polls at the Right Rate and Still Jitters
A high-speed interrupt device requests a 125 µs interval. Measured over a second, it is polled almost exactly 8000 times — the correct rate. But the interval between consecutive polls varies from under 125 µs to nearly 1 ms, and an application sampling the device sees jitter it cannot explain.
The rate is right and the spacing is wrong, which is a very specific signature. A controller placing eight polls per millisecond at arbitrary microframes within each millisecond produces exactly this: the right count, the wrong distribution.
And §2 named the cause before any instrument is attached. A scheduler that keys its periodic placement on the frame number rather than on the microframe index has eight positions to choose from and no reason to prefer one, so the poll lands wherever the frame's work happened to leave room. Correct rate, uncontrolled phase.
The chain, from the outside in:
Analyser — measure the interval distribution, not the count. "8000 polls per second" is consistent with both correct and broken behaviour. The histogram of gaps distinguishes them immediately.
Analyser — which microframe do the polls land in? Count SOFs since the last frame-number change to recover the index (§2: it is not transmitted). A device polled at a consistent index is correctly scheduled; one scattered across indices is not.
Controller — is the schedule indexed by microframe at all? If the scheduling structure has one entry per frame rather than eight, the controller cannot express which microframe, and no amount of tuning will fix it. That is an architecture problem, not a parameter.
RTL — the first divergence. Compare the bench's independent (ubounds mod 8) against the design's uframe at each boundary. The first boundary where they differ is the bug — and in practice it is U1 (the number advancing per microframe), U4 (the index never rolling) or U5 (the frame boundary in the wrong place).
16. Common Misconceptions
"High speed just runs the frame counter eight times faster." The frame number advances once per millisecond at both speeds (§2). Mutation U1 is exactly this belief and costs 146 281 failures.
"The SOF tells the device which microframe it is." The frame-number field is the same across all eight (§2); the index is derived by counting SOFs.
"A microframe is a small frame." It is a scheduling opportunity inside a frame. Eight of them share one frame number, and bInterval is denominated in them at high speed (§1).
"Frame boundaries and microframe boundaries are separate events." A frame boundary is a microframe boundary — the one that rolls the index (§3). Treating them as independent invites two counters that drift.
"A disabled port should restart its microframe index." Both quantities are protocol state and both are held (§4). Only the local position is parked.
"If the poll rate is right, the scheduling is right." §15: the right rate with an uncontrolled index produces up to 875 µs of jitter on a 125 µs request.
"Testing reset once is enough." §9: a reset applied only from an already-clear state proves nothing about clearing.
17. Exercises
1. A controller runs at 60 MHz. Compute TICKS_PER_UFRAME and TICKS_PER_FRAME, and confirm the second is eight times the first. Then determine what happens if a designer parameterises both independently and they disagree by one.
2. §2 says the microframe index is derived by counting SOFs between frame-number changes. Write the device-side logic that recovers it, and determine what it does after a missed SOF.
3. Re-run mutation U6 against the original stimulus (one bus reset, at index 0) and against the fixed stimulus, and explain the difference in terms of observability rather than coverage.
4. VHDL's integer range 0 to 7 caught U2 (§11). Add an equivalent run-time check to the SystemVerilog without changing the design's behaviour, and say what it costs and when it fires relative to the checker.
5. Write an SVA property that catches U5 — the frame boundary placed at index 0 instead of 7 — without referring to the design's uframe. State what external reference it needs.
6. §15's device is polled at the right rate with the wrong phase. Design the smallest change to a frame-indexed schedule that makes the microframe position controllable, and say what it costs in storage.
18. Summary
High speed puts eight 125 µs scheduling opportunities inside each 1 ms frame (§1), and the Linux core states the relationship directly: microframes (eight per millisecond) rather than in frames (one per millisecond).
The frame number does not advance eight times faster (§2). It advances once per millisecond, so eight consecutive SOFs carry the same value, and the microframe index is derived by counting them rather than received. A controller that identifies an opportunity by frame number alone has eight ways to be wrong, and the symptom is jitter rather than failure (§15).
A frame boundary is the microframe boundary that rolls the index (§3) — derived, not counted separately, because two independent counters for one millisecond will eventually disagree.
All three HDL implementations were simulated (§19), and six mutations died in all three (§10). U1 — advancing the number per microframe — costs 146 281 failures, the largest count in the module.
U2 does not produce a count in VHDL at all (§11). integer range 0 to 7 stops the simulation at 335 ns naming the signal and the line, where the other two wrap silently. This is the second module in which a range-constrained subtype has caught a counter defect the other languages could only observe as a downstream symptom — which makes it a language property rather than a coincidence.
And U6 first survived entirely (§9), because the whole run applied one bus reset and the index happened to be zero when it did. A clearing operation tested only from an already-clear state proves nothing. Two lines of directed stimulus took it from 0 to 99 993, measured identically in Verilog and VHDL.
19. Tooling, Honestly
| Language | Design | Testbench | Analysed / compiled | Simulated | Mutations |
|---|---|---|---|---|---|
| Verilog-2005 | usb_uframe_timer | uf_v_tb.v | ✅ Icarus -g2005 | ✅ 0 errors | ✅ all six |
| SystemVerilog | usb_uframe_timer_sv | uf_sv_tb.sv | ✅ Icarus -g2012 | ✅ 0 errors | ✅ all six |
| VHDL-2008 | usb_uframe_timer_vhdl | uf_vhdl_tb.vhd | ✅ nvc 1.23.0 | ✅ 0 errors | ✅ all six |
| SVA (§13) | — | — | ❌ unsupported by Icarus | ❌ | — |
Two cells in §10's table are not numbers, and both are honest rather than missing. U2's VHDL result is a fatal range violation (§11), recorded as FATAL rather than invented as a count. U6's SystemVerilog result is a tooling limit: the same mutation and stimulus measure 99 993 in ten seconds under Verilog and about a minute under nvc, and had not completed after fourteen minutes of CPU under Icarus SystemVerilog. An unmeasured figure is an invented figure, so the cell says so.
The SystemVerilog bench is generated from the Verilog bench's scenario list, so the two drive identical stimulus — deliberate for a time base, and the reason their counts agree exactly. The VHDL bench uses uniform and diverges only in the randomised phase.
20. What Comes Next
Two chapters have now built time: a frame, and eight microframes inside it. Every one of them is an opportunity — a moment at which the host may place work on the bus.
Nothing so far decides what to place.
Chapter 17.3 is that decision, and it is the module's centre. Several flows are waiting, each with a different requirement: an isochronous endpoint holding a reservation that 16.4 admitted, an interrupt endpoint whose 15.4 deadline is approaching, a control transfer that must never be starved, and bulk traffic that will take whatever is left.
They cannot all go first. The chapter takes that apart into the decisions it actually is — what is pending, what is eligible, what does policy require, what does the budget permit, and who wins — and shows that the most common scheduler defect is treating any two of those as the same question.
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
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.
- Related topic
Host Scheduling Algorithm
Pending, eligible and granted are three different things. An arbiter verified exhaustively over 262144 points — and the reference-model coupling that made four invariant mutations die by a single check each.
- Related topic
Bandwidth Allocation
A scheduler has two numbers for every transaction — what it expects the work to cost and what it actually cost — and using one for both keeps a wrong ledger while making correct decisions.
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.
