Skip to content
VLSI Mentor

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:

ModuleWhat it asked of the schedule
14 — Bulkwhatever is left over, with no timing guarantee
15 — Interrupta bound on the gap — at most bInterval frames between polls
16 — Isochronousa 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#define USB_PID_SOF			0xa5

It 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 update

frame_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:

TermWhat it isWho defines it
controller clockthe RTL clock this logic runs onthe implementation
framea 1 ms protocol intervalUSB
frame numberthe 11-bit value the SOF carriesUSB
frame counterwhat software reads backthe 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.

A sequence diagram of how a USB frame boundary is produced and used. The controller clock drives a position counter inside the frame timer. After the configured number of clocks the timer reaches the frame boundary and emits a one-cycle frame tick, which is an internal controller-domain pulse rather than a signal on the bus. The timer simultaneously advances the eleven-bit frame number modulo two thousand and forty-eight and raises a start-of-frame request, which is a level held until taken. The packet engine takes that request, acknowledges it, and transmits a start-of-frame token carrying the current frame number onto the bus, where the device receives it. Meanwhile the same frame tick is delivered to the scheduler, which treats it as a new service opportunity and as the point at which its per-frame budget is refreshed. The device uses the received frame number to advance its own frame-derived state, such as an interrupt endpoint's polling schedule or an isochronous endpoint's feedback measurement window.Where a frame comes from, and what rides in itController clockFrame timerPacket engineDeviceTICKS_PER_FRAMEclocksposition reaches thelimit — boundaryframe_no + 1 (mod2048); frame_tickpulsessof_req — a LEVEL,held until takensof_ackSOF token (PID 0xa5)carrying frame_noadvance frame-derived state (poll schedule, feedback window)advanceframe-derived state(poll schedule,…frame_tick also = anew schedulingopportunity
Figure 1 — one frame, from the controller clock that derives its boundary to the work placed inside it. The frame number crosses to the device on the wire; everything above the dashed line is internal.

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_W counter, one 11-bit counter, one comparator, four flags.
  • Reset: asynchronous active-low rst_n; bus_reset synchronous and equivalent, and both restart the frame number.
  • Priority: !enable outranks the boundary, which outranks the ordinary count.
  • Latency: every output is registered; frame_tick is 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; an sof_ack in the boundary cycle is not a miss.
  • Assumptions: TICKS_PER_FRAME controller clocks equal one 1 ms frame; sof_ack is 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_FRAME clocks; that the frame number wraps at 2048 and reports it; that sof_req is a level and frame_tick a pulse; that a disabled port holds the number and parks the position.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

Three 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endmodule

tstep_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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

ConcernVerilogSystemVerilogVHDL
The three casesif/else if chain, enable as an outer guardtypedef enum + unique casetype tstep_t, no encoding
Where enable is testedoutside the boundary testinside the case selectioninside the case selection
Illegal parameterisationundetectedthree $fatal guardsthree assert ... severity failure
Counter incrementwidth-matched concatenation+ 1'b1+ 1, defined by the type
Case exhaustivenessnot checkedunique — but see §20enforced 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
      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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // 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
  endtask

Both 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:

ScenarioWhat it pins down
clocks before the boundaryno tick until the frame has fully elapsed
the boundary clocktick, number advances, SOF requested
the following clockframe_tick is a pulse; sof_req is a level
sof_ackclears the request
a boundary with the SOF un-ackedsof_missed, and the request renewed for the new frame
sof_ack in the boundary cyclenot a miss
enable low for three framesnumber held, position parked, no boundaries
bus_resetnumber restarted, request cleared
2048 framesthe 11-bit protocol wrap, directed
4000 randomised acksindependently 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  REACH: boundaries=2553 wraps=1 sof_missed=24 ack-on-boundary=1

10. Mutation Testing — Across All Three Languages

IDMutationVerilogSystemVerilogVHDLKilled
—baseline, no mutation000—
F1the frame-number modulus is off by one401240124012✅ all three
F2the explicit wrap is removed111✅ all three — see §12
F3the frame is one controller clock too long475484754847594✅ all three
F4sof_req is a pulse instead of a level358535853568✅ all three
F5a missed SOF is never reported252520✅ all three
F6bus_reset does not restart numbering203882038820388✅ all three
F7the enable guard is removed116116116✅ 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
      if (!enable) begin              // Verilog: an OUTER guard
        pos_r <= 0;                   //   ...which dominates regardless
      end else if (at_boundary) begin
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    if      (!enable)               tstep = T_HELD;    // SV: INSIDE the
    else if (frame_pos == POS_LAST) tstep = T_BOUNDARY; //   case selection

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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 cycles
A waveform of the frame time base over twelve controller clocks, with a deliberately shortened frame of four clocks. The position counter advances one, two, three and then returns to zero at the boundary in cycle three, where the frame tick pulses for a single cycle, the frame number advances from zero to one, and a start-of-frame request is asserted. In cycle four the packet engine acknowledges, and the request drops. The position advances again and the next boundary falls in cycle seven, advancing the frame number to two and asserting a new start-of-frame request. This time no acknowledgement arrives, and the request remains asserted through cycles eight, nine and ten, demonstrating that it is a level held until taken rather than a pulse. In cycle eleven the following boundary arrives with that request still outstanding: the frame number advances to three, the request is renewed for the new frame, and the missed-start-of-frame output pulses to report that the previous frame's packet was never sent. Every signal in this diagram is in the controller clock domain and none of it depicts USB wire signalling.boundary — tick pulses, number advancesboundary — tick pulses,number advancesack taken — request dropsack taken — request dropsrequest HELD, not a pulserequest HELD, not a pulseboundary with SOF unsent — reportedboundary with SOF unsent —reportedcycle01234567891011frame_pos123012301230frame_tickframe_no000111122223sof_reqsof_acksof_missedt0t1t2t3t4t5t6t7t8t9t10t11
Figure 2 — twelve controller clocks with a four-clock frame, taken from the simulator. The frame length is shortened so the structure is readable; the 11-bit protocol modulus is unchanged.

This 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // 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

ClaimSafety / progressVacuity riskHow non-vacuity is established
A1frame_tick is a pulsesafetylow2553 boundaries measured
A2the number changes only at a boundarysafetynone in practice — most cycles are non-boundaryholds constantly
A3the number advances modulo 2048safetymoderate — no boundaries makes it vacuouspaired with A5
A4sof_req is a levelsafetymoderate — needs an un-acked request24 missed SOFs and many held cycles
A5an enabled port eventually ticksprogresslowenable is high for almost the whole run
A6a disabled port is frozensafetymoderate — needs enable lowone 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

LanguageDesignTestbenchAnalysed / compiledSimulatedMutations
Verilog-2005usb_frame_timerft_v_tb.v✅ Icarus -g2005✅ 0 errors✅ all seven
SystemVerilogusb_frame_timer_svft_sv_tb.sv✅ Icarus -g2012✅ 0 errors✅ all seven
VHDL-2008usb_frame_timer_vhdlft_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

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.