Skip to content
VLSI Mentor

USB · Module 15

Keyboards on USB

A key matrix produces a set; the boot report has six slots. The mechanism that reconciles them has its own reserved code — and the chapter where the verification transaction stops being the pins.

Chapter 15.1 built the schedule and said nothing about what the report contains. This chapter is the canonical interrupt device, and it contains a hardware problem that is much more interesting than it first appears.

A key matrix produces a set of pressed keys. The boot keyboard report is eight fixed bytes. Those two facts do not fit together, and reconciling them is a design decision with a name, a reserved code, and a failure mode users can feel.

1. Why a Keyboard Is an Interrupt Device at All

A keyboard produces a few bytes a second at most. It could use Bulk, and Chapter 10.3 would give it far more bandwidth than it needs.

It uses Interrupt because bandwidth is not what it needs. Chapter 10.4 §2: an interrupt endpoint buys a bound on the gap between questions, and a keystroke that arrives 200 ms late is a keystroke the user notices.

A keyboard is the clearest case in USB of a device that needs a guarantee about when and almost nothing about how much.

And Chapter 10.3 §2's grid is why it cannot have both. A reservation is capped, so a guarantee about timing is bought with a small share of capacity — which for a keyboard is an excellent trade and for a storage device would be a disaster.

2. The Boot Report Is Eight Fixed Bytes

Fixed, in both senses: always eight, and always the same layout.

ByteContent
0modifier bitmap — one bit each for the left and right Ctrl, Shift, Alt and GUI keys
1reserved, always 0x00
2–7six key slots, each a HID usage code, 0x00 meaning empty

Two structural decisions in that table are the whole chapter.

The modifiers are a bitmap and the keys are a list. Eight modifiers fit in eight bits because there are exactly eight of them and they are known in advance. There are far more than eight ordinary keys, so they cannot be a bitmap in one byte — and a bitmap over every possible usage code would be 32 bytes.

So ordinary keys are represented by value, not by position — and a list of values needs a length, which a fixed-size report does not have. The empty slots carry 0x00 instead.

3. Rollover: What Happens When Seven Keys Are Down

The obvious answer is to report the first six and drop the seventh. The specification does something else, and the difference matters.

When more than six ordinary keys are down, all six slots are filled with 0x01 — the HID Usage Table's ErrorRollOver.

Not six real keys and a dropped one. Six copies of an error code.

Why discard the information you have? Because a partial list is indistinguishable from a complete one. Six real usage codes mean exactly these six keys are down; if they could also mean at least these six and possibly others, the host could never act on a full report with confidence.

The report says either exactly what is pressed, or that it cannot say. There is no third state, and inventing one would make every full report ambiguous.

And it is recoverable. Release one key and the next report is an ordinary six-key list; nothing latches. §6's rollover output exists to make the condition visible to firmware, not to remember it.

4. The Hardware, Before Any Language

Select up to six set bits from an N-bit vector, map each to a usage code, and detect the overflow.

ElementPurpose
A population count over the matrixis the limit exceeded?
A bounded fill of six slotsthe selection
An index that stops at 6the bound
A usage base, added to the bit indexbit position → usage code
A 0x01 broadcast on overflow§3's rule
Registers holding the last report§5's hold

Three design decisions, each a mutation in §9:

The overflow test is a count, not a fill failure. Fill six slots and see whether any keys are left over gives the same answer and is the wrong shape — it computes the partial list you must then throw away, and a design that has computed it is a design that might transmit it.

The usage code is BASE + index, and the base is a parameter. The mapping from a matrix position to a HID usage is a property of the keyboard, not of the encoder.

And the report is held, not recomputed. §5.

5. The Report Is Held Between Samples

The matrix changes continuously; the report changes only when sampled.

A keyboard scans its matrix on its own periodic tick — Chapter 15.1 §1's reason for a device to count frames at all. Between one sample and the next, the report must not change, because the host may poll at any moment and must receive a consistent report rather than a half-updated one.

The report is a snapshot, and a snapshot that changes under the reader is not a snapshot.

This is also why §6's design has no combinational path from key_matrix to the report outputs. Every output is registered, and the register updates only on sample — so the host's poll and the device's scan are decoupled, which they must be, because Chapter 15.1 §1 established that their clocks are independent.

§9's K3 removes the hold, and the symptom is exactly what the decoupling exists to prevent.

A block diagram of a USB keyboard's report path. A key matrix feeds a population counter and a bounded slot filler. The population counter decides whether more than six ordinary keys are down; if so, the overflow path fills all six slots with the ErrorRollOver code, and otherwise the bounded filler places up to six usage codes derived from the matrix bit positions. A separate modifier bitmap path bypasses the slot logic entirely and cannot overflow. A sample pulse, generated by the keyboard's own scan tick, captures the selected slots and the modifier byte into the eight-byte report register. The host's poll reads that register, and the two rhythms are independent.Key matrixN bits, changingModifier bits8 — cannot overflowCount keysmore than six?Fill 6 slotsbounded selectAll six = 0x01ErrorRollOverReport register8 bytes, heldHost pollreads the snapshotScan tickdevice's own clock> 6≤ 6overridebyte 0sample8 bytes12
Figure 1 — the two independent rhythms of a keyboard. The scan tick produces a snapshot; the poll consumes whichever snapshot is current. Neither waits for the other, which is why the report must be registered rather than computed on demand.

6. Verilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// hid_kbd_report  —  SIMPLIFIED SYNTHESIZABLE TEACHING RTL
//
// Models sections 3 to 5: bounded slot selection with the rollover rule, and
// a report held between samples.
//
// NOT MODELLED: matrix scanning and debounce (a keyboard problem, not a USB
// one); ghosting detection (section 3's callout); the HID report descriptor
// that declares this layout; the interrupt endpoint that transmits it
// (Chapter 10.4); and the usage-code table itself -- the mapping here is a
// linear BASE+index, which a real keyboard replaces with a lookup.
// ─────────────────────────────────────────────────────────────────────────
module hid_kbd_report #(
  parameter N_KEYS     = 32,
  parameter USAGE_BASE = 8'h04
)(
  input  wire              clk,
  input  wire              rst_n,
  input  wire              bus_reset,
  input  wire              sample,          // the device's own scan tick
  input  wire [N_KEYS-1:0] key_matrix,
  input  wire [7:0]        modifiers,
  output wire [7:0]        rpt_modifiers,
  output wire [7:0]        rpt_reserved,
  output wire [7:0]        rpt_key0, rpt_key1, rpt_key2,
  output wire [7:0]        rpt_key3, rpt_key4, rpt_key5,
  output wire              rollover
);
  // HID Usage Table: 0x00 = no key in this slot, 0x01 = ErrorRollOver.
  localparam [7:0] USAGE_NONE     = 8'h00;
  localparam [7:0] USAGE_ROLLOVER = 8'h01;
  localparam       MAX_SLOTS      = 6;

  reg [7:0] slot [0:MAX_SLOTS-1];
  reg [7:0] mods_r;
  reg       roll_r;

  assign rpt_modifiers = mods_r;
  assign rpt_reserved  = 8'h00;        // byte 1 is reserved, always zero
  assign rpt_key0 = slot[0];  assign rpt_key1 = slot[1];
  assign rpt_key2 = slot[2];  assign rpt_key3 = slot[3];
  assign rpt_key4 = slot[4];  assign rpt_key5 = slot[5];
  assign rollover = roll_r;

  // Section 4: the overflow test is a COUNT, not a failed fill.
  integer ci;
  reg [7:0] nkeys;
  always @(*) begin
    nkeys = 8'd0;
    for (ci = 0; ci < N_KEYS; ci = ci + 1)
      if (key_matrix[ci]) nkeys = nkeys + 8'd1;
  end

  integer si, fi;
  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (si = 0; si < MAX_SLOTS; si = si + 1) slot[si] <= USAGE_NONE;
      mods_r <= 8'h00;
      roll_r <= 1'b0;
    end else if (bus_reset) begin
      for (si = 0; si < MAX_SLOTS; si = si + 1) slot[si] <= USAGE_NONE;
      mods_r <= 8'h00;
      roll_r <= 1'b0;
    end else if (sample) begin
      // Section 2: modifiers are a bitmap and never roll over.
      mods_r <= modifiers;
      if (nkeys > MAX_SLOTS) begin
        // Section 3: EVERY slot carries ErrorRollOver, not a partial list.
        for (si = 0; si < MAX_SLOTS; si = si + 1) slot[si] <= USAGE_ROLLOVER;
        roll_r <= 1'b1;
      end else begin
        roll_r <= 1'b0;
        fi = 0;
        for (si = 0; si < MAX_SLOTS; si = si + 1) slot[si] <= USAGE_NONE;
        for (ci = 0; ci < N_KEYS; ci = ci + 1) begin
          if (key_matrix[ci] && (fi < MAX_SLOTS)) begin
            slot[fi] <= USAGE_BASE + ci[7:0];
            fi = fi + 1;
          end
        end
      end
    end
    // Section 5: no else -- the report is HELD between samples.
  end
endmodule

Contract. Purpose: turn a key matrix into a boot keyboard report. Inputs: the matrix, the modifier bits, a sample pulse, two resets. Outputs: the eight report bytes and a rollover indication. State: six usage bytes, the modifier byte, one flag — 56 flip-flops. Hardware: a population counter, a bounded priority fill, and the report register. Reset: every slot to 0x00, modifiers to 0x00, rollover clear — on hard reset and bus reset. Priority: reset beats sample; rollover beats the fill. Boundary: 0, 6 and 7 keys are the cases that matter. Assumptions: sample is one cycle; USAGE_BASE + N_KEYS − 1 does not exceed 0xFF. Omissions: in the header. Invariants: §10.

7. SystemVerilog

The same hardware, with two changes that are not cosmetic: the report becomes a type, and the selection is separated from the capture.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
package hid_kbd_pkg;
  localparam logic [7:0] USAGE_NONE     = 8'h00;
  localparam logic [7:0] USAGE_ROLLOVER = 8'h01;
  localparam int unsigned MAX_SLOTS     = 6;

  // The report IS eight bytes with a fixed layout. Declaring it once means
  // the layout cannot drift between the block that builds it and the block
  // that transmits it -- which eight separate ports cannot guarantee.
  typedef struct packed {
    logic [7:0] key5, key4, key3, key2, key1, key0;
    logic [7:0] reserved;
    logic [7:0] modifiers;
  } kbd_report_t;
endpackage

module hid_kbd_report_sv
  import hid_kbd_pkg::*;
#(
  parameter int unsigned N_KEYS     = 32,
  parameter logic [7:0]  USAGE_BASE = 8'h04
)(
  input  logic              clk,
  input  logic              rst_n,
  input  logic              bus_reset,
  input  logic              sample,
  input  logic [N_KEYS-1:0] key_matrix,
  input  logic [7:0]        modifiers,
  output kbd_report_t       report_o,
  output logic              rollover
);
  kbd_report_t rpt_q;
  logic        roll_q;
  assign report_o = rpt_q;
  assign rollover = roll_q;

  // $countones states the intent; the Verilog loop states the method.
  logic [$clog2(N_KEYS+1)-1:0] nkeys;
  always_comb nkeys = $countones(key_matrix);

  logic over_limit;
  assign over_limit = (nkeys > MAX_SLOTS);

  // ── COMBINATIONAL SELECTION, separated from sequential capture ──────────
  // The six slot values are a pure function of the matrix. Splitting them out
  // makes the selection reviewable and reusable independently of WHEN it is
  // sampled -- the Verilog version blends the two into one always block.
  logic [MAX_SLOTS-1:0][7:0] slot_next;
  always_comb begin
    int unsigned fi;
    fi = 0;
    for (int unsigned s = 0; s < MAX_SLOTS; s++) slot_next[s] = USAGE_NONE;
    if (over_limit) begin
      for (int unsigned s = 0; s < MAX_SLOTS; s++) slot_next[s] = USAGE_ROLLOVER;
    end else begin
      for (int unsigned ci = 0; ci < N_KEYS; ci++) begin
        if (key_matrix[ci] && (fi < MAX_SLOTS)) begin
          slot_next[fi] = USAGE_BASE + 8'(ci);
          fi = fi + 1;
        end
      end
    end
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      rpt_q <= '0; roll_q <= 1'b0;
    end else if (bus_reset) begin
      rpt_q <= '0; roll_q <= 1'b0;
    end else if (sample) begin
      rpt_q.modifiers <= modifiers;
      rpt_q.reserved  <= USAGE_NONE;
      rpt_q.key0 <= slot_next[0];  rpt_q.key1 <= slot_next[1];
      rpt_q.key2 <= slot_next[2];  rpt_q.key3 <= slot_next[3];
      rpt_q.key4 <= slot_next[4];  rpt_q.key5 <= slot_next[5];
      roll_q     <= over_limit;
    end
    // Section 5: no else -- the report is held.
  end
endmodule

rpt_q <= '0 resets the entire eight-byte report in one assignment, and cannot miss a field. The Verilog version resets six slots in a loop and the modifier byte separately — two places to forget one.

8. VHDL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package hid_kbd_pkg is
  constant USAGE_NONE     : std_logic_vector(7 downto 0) := x"00";
  constant USAGE_ROLLOVER : std_logic_vector(7 downto 0) := x"01";
  constant MAX_SLOTS      : natural := 6;

  -- VHDL has no packed struct, so the six slots become an array type. The
  -- report's byte layout is then the port list plus this type -- less
  -- self-describing than the SystemVerilog struct, and still one object.
  type slot_array_t is array (0 to MAX_SLOTS-1) of std_logic_vector(7 downto 0);
end package hid_kbd_pkg;

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.hid_kbd_pkg.all;

entity hid_kbd_report is
  generic (
    N_KEYS     : positive := 32;
    USAGE_BASE : natural  := 16#04#
  );
  port (
    clk           : in  std_logic;
    rst_n         : in  std_logic;
    bus_reset     : in  std_logic;
    sample        : in  std_logic;
    key_matrix    : in  std_logic_vector(N_KEYS-1 downto 0);
    modifiers     : in  std_logic_vector(7 downto 0);
    rpt_modifiers : out std_logic_vector(7 downto 0);
    rpt_reserved  : out std_logic_vector(7 downto 0);
    rpt_slots     : out slot_array_t;
    rollover      : out std_logic
  );
end entity hid_kbd_report;

architecture rtl of hid_kbd_report is
  signal slots_q    : slot_array_t;
  signal mods_q     : std_logic_vector(7 downto 0);
  signal roll_q     : std_logic;
  signal nkeys      : natural range 0 to N_KEYS;
  signal over_limit : std_logic;
  signal slot_next  : slot_array_t;
begin

  rpt_modifiers <= mods_q;
  rpt_reserved  <= USAGE_NONE;
  rpt_slots     <= slots_q;
  rollover      <= roll_q;

  -- The count's RANGE is part of its type. Neither other language states the
  -- bound; here an out-of-range value is a run-time error rather than a
  -- silently truncated one.
  count_keys : process (key_matrix)
    variable n : natural;
  begin
    n := 0;
    for i in key_matrix'range loop
      if key_matrix(i) = '1' then
        n := n + 1;
      end if;
    end loop;
    nkeys <= n;
  end process count_keys;

  over_limit <= '1' when nkeys > MAX_SLOTS else '0';

  select_slots : process (key_matrix, over_limit)
    variable fi : natural;
  begin
    for s in 0 to MAX_SLOTS-1 loop
      slot_next(s) <= USAGE_NONE;
    end loop;
    if over_limit = '1' then
      for s in 0 to MAX_SLOTS-1 loop
        slot_next(s) <= USAGE_ROLLOVER;
      end loop;
    else
      fi := 0;
      for ci in 0 to N_KEYS-1 loop
        if key_matrix(ci) = '1' and fi < MAX_SLOTS then
          slot_next(fi) <= std_logic_vector(to_unsigned(USAGE_BASE + ci, 8));
          fi := fi + 1;
        end if;
      end loop;
    end if;
  end process select_slots;

  capture : process (clk, rst_n)
  begin
    if rst_n = '0' then
      for s in 0 to MAX_SLOTS-1 loop
        slots_q(s) <= USAGE_NONE;
      end loop;
      mods_q <= (others => '0');
      roll_q <= '0';
    elsif rising_edge(clk) then
      if bus_reset = '1' then
        for s in 0 to MAX_SLOTS-1 loop
          slots_q(s) <= USAGE_NONE;
        end loop;
        mods_q <= (others => '0');
        roll_q <= '0';
      elsif sample = '1' then
        mods_q  <= modifiers;
        slots_q <= slot_next;      -- a whole-array assignment
        roll_q  <= over_limit;
      end if;
      -- Section 5: no else -- the report is held.
    end if;
  end process capture;

end architecture rtl;

9. Comparing the Three

ConcernVerilogSystemVerilogVHDL
Report as an objecteight separate portsstruct packed — one typearray type + separate ports
Reset the whole reportloop + separate byterpt_q <= '0loop + separate signal
Population countexplicit loop$countonesexplicit loop, ranged type
Slot storagereg [7:0] slot [0:5]packed [5:0][7:0]slot_array_t
Comb/seq separationone always blockalways_comb + always_fftwo named processes
Bound on the countnonenonenatural range 0 to N_KEYS
Whole-array assignmentper elementper fieldslots_q <= slot_next

The most useful row is the last two. VHDL bounds the count in its type — a value outside 0 to N_KEYS is an error rather than a wrap — and assigns the whole slot array in one statement, which neither of the others does as naturally.

And the most consequential is the second. rpt_q <= '0' resets eight bytes and cannot miss one; the other two reset the slots and the modifier byte in separate statements, which is two opportunities to forget one on a later edit.

10. The Testbenches, and What They Found

All three apply the same scenario: reset; zero keys; one key; eight modifiers with no keys; the boundary at 5, 6 and 7; recovery below the limit; the hold between samples; non-contiguous keys; all keys; a bus reset; and an exhaustive sweep of key counts 0 through 8.

The Verilog bench checks each slot individually:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
        check(k0 === nth_usage(0), "slot 0 usage");
        check(k1 === nth_usage(1), "slot 1 usage");
        // …one check per slot

The SystemVerilog bench compares the whole report against an independent model and adds 400 randomised matrices:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // For each slot it performs its OWN independent scan for the (n+1)-th
  // lowest set bit. The DUT fills slots in a single pass with a running
  // index; this model never has a running index at all, so a fill-order or
  // index-advance error in the DUT cannot be reproduced here.
  function automatic logic [7:0] nth_set_usage(input logic [N_KEYS-1:0] km,
                                               input int unsigned nth);
    int unsigned seen;
    begin
      seen = 0;
      for (int unsigned i = 0; i < N_KEYS; i++) begin
        if (km[i]) begin
          if (seen == nth) return BASE + 8'(i);
          seen = seen + 1;
        end
      end
      return USAGE_NONE;
    end
  endfunction

The VHDL bench uses the same independent-scan model with assert:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
        chk(rollover = '0', "six or fewer keys must not set rollover");
        for s in 0 to MAX_SLOTS-1 loop
          chk(rpt_slots(s) = nth_set_usage(key_matrix, s),
              "slot " & integer'image(s) & " usage");
        end loop;

11. Mutation Testing

Three mutations, applied identically to the Verilog and the SystemVerilog.

MutationVerilog TBSystemVerilog TB
K1 rollover never detected10 errors810 errors
K2 usage code off by one39 errors13 errors
K3 report decays between samples1 error1 error

All three caught in both languages. And the two rows in the middle disagree in opposite directions, which is the finding.

12. Assertions

Published for an SVA-capable simulator; Icarus does not support concurrent assertions, so these were reviewed, not executed.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// R1 — SAFETY. Rollover and a real usage code are mutually exclusive: a
//   rollover report carries 0x01 in every slot, never a partial list.
//   Bug caught: section 11's K1, and any "fill six then flag" design that
//   leaves real keys visible alongside the error code.
//   Vacuity: antecedent is `rollover`, a DUT output -- see R2.
property p_rollover_is_total;
  @(posedge clk) disable iff (!rst_n)
    rollover |-> (report_o.key0 == USAGE_ROLLOVER)
              && (report_o.key1 == USAGE_ROLLOVER)
              && (report_o.key2 == USAGE_ROLLOVER)
              && (report_o.key3 == USAGE_ROLLOVER)
              && (report_o.key4 == USAGE_ROLLOVER)
              && (report_o.key5 == USAGE_ROLLOVER);
endproperty
a_rollover_is_total: assert property (p_rollover_is_total);

// R2 — SAFETY, and R1's vacuity answer. The condition is stated over the
//   INPUT, so a DUT that never asserts rollover cannot make it disappear.
property p_rollover_iff_over_limit;
  @(posedge clk) disable iff (!rst_n || bus_reset)
    sample |=> (rollover == ($countones($past(key_matrix)) > MAX_SLOTS));
endproperty
a_rollover_iff_over_limit: assert property (p_rollover_iff_over_limit);

// M1 — SAFETY. Modifiers never roll over and never occupy a key slot.
//   Section 2's asymmetry, asserted.
property p_modifiers_independent;
  @(posedge clk) disable iff (!rst_n || bus_reset)
    (sample && ($countones(key_matrix) == 0)) |=>
      (report_o.key0 == USAGE_NONE) && (report_o.modifiers == $past(modifiers));
endproperty
a_modifiers_independent: assert property (p_modifiers_independent);

// B1 — SAFETY. Byte 1 is reserved and always zero.
property p_reserved_zero;
  @(posedge clk) disable iff (!rst_n) report_o.reserved == 8'h00;
endproperty
a_reserved_zero: assert property (p_reserved_zero);

// H1 — SAFETY. Section 5's hold: without a sample, the report does not move.
//   Section 11's K3.
property p_held_between_samples;
  @(posedge clk) disable iff (!rst_n || bus_reset)
    (!sample) |=> $stable(report_o);
endproperty
a_held_between_samples: assert property (p_held_between_samples);

// COVER — the boundary must actually be crossed in both directions, or the
// rollover properties are decoration.
c_enter_rollover: cover property (@(posedge clk) !rollover ##1 rollover);
c_leave_rollover: cover property (@(posedge clk)  rollover ##1 !rollover);

R1 and R2 are a deliberate pair. R1's antecedent is rollover — a DUT output — so a design that never rolls over satisfies it vacuously, which is exactly §11's K1. R2 states the condition over key_matrix, an input, so no DUT behaviour can erase it. R1 says what a rollover report looks like; R2 says when one must happen.

And the two covers are not optional. Without c_enter_rollover and c_leave_rollover, a stimulus set that never exceeds six keys leaves every rollover property trivially true — the properties would be correct and the verification worthless.

13. Verification: Where UVM Starts to Earn Its Place

Chapter 15.1 §12 rejected UVM because the transaction and the pins were the same thing. Here they are not, and the difference is worth stating precisely.

The pins are: a 32-bit matrix, an 8-bit modifier vector, and a sample pulse.

The thing verification should reason about is: this set of keys is now down. A set, not a bit vector — because the report's correctness depends on the set's cardinality (§3's limit) and its members' ordering (§4's fill), and neither is visible in a bit vector without interpreting it.

UVM componentWhat it would own here
Sequence itema key set — a list of key indices plus a modifier mask. Not a bit vector, because the interesting constraints are size() == 6, size() == 7, size() inside {0,1}
Sequencescenarios: a rollover entered and left; keys added one at a time to the boundary; modifiers varied independently of keys
Driverconverts a key set into the matrix bits and one sample pulse — the only component that knows the pin encoding
Monitorobserves the report bytes and reconstructs the key set the device believes is down — independently, by decoding usage codes back to indices
Reference model§10's per-slot independent scan, at the set level: given a key set, predict the report
Scoreboardcompares reports, and separately tracks whether the rollover transitions were seen
Coveragekey count (0,1,5,6,7,>7) × modifier count (0,1,>1) × rollover transition
A sequence diagram of a keyboard crossing the rollover boundary. With five keys held, the device's scan tick samples the matrix and the report carries five usage codes and one empty slot. A sixth key is pressed; the next sample produces a report with six usage codes and no empty slots, which is still an ordinary report because six is the limit rather than an overflow. A seventh key is pressed; the next sample produces a report in which all six slots carry the ErrorRollOver code and no real key information is present at all. One key is then released, bringing the total back to six, and the following sample produces an ordinary six-key report again, because the rollover condition is recomputed from scratch each time and nothing latches.Six keys, then seven, then six againKeys heldDeviceHost5 keys downreport: 5 usages +one empty slota 6th key — stillwithin the limitreport: 6 usages, noempty slota 7th key — nowhereto put itcount > 6 → discardthe partial listreport: 0x01 in ALLSIX slots"the device cannotsay what is pressed"one key released —back to 6report: 6 usagesagain — nothinglatched
Figure 2 — a rollover entered and left. Note that the sixth key is still an ordinary report and the seventh replaces the whole thing; and that releasing one key restores a normal report immediately, because nothing latches.

14. Debugging: the Keyboard That Types Nothing Under Four Fingers

A game controller built as a HID keyboard works for normal typing. When the user holds several keys at once — as a game requires — all input stops, and resumes the moment a key is released. No error appears in any log.

What does stops and resumes tell you? That it is not a crash and not a disconnection. The device is still reporting, and what it is reporting is being interpreted as nothing.

What does a report of six 0x01 codes mean to a host? §3: I cannot say what is pressed. A correct host presses no keys in response — which from the user's side is indistinguishable from the keyboard having stopped.

So is the device broken? Possibly not at all. Six keys is the boot report's limit, and §3's callout is the first thing to establish: which report is in use?

LayerObservationTool
Systeminput stops above N simultaneous keysthe user
Host stackwhich protocol — boot or reporthost driver state
USB-visible0x01 in all six slotsprotocol analyser
Devicerollover asserted, key count > 6firmware log or ILA
RTLthe population count at the samplefirst divergence

The decisive observation is the third row, and it splits the investigation in two:

If the analyser shows six 0x01 codes, the device is behaving correctly and the fault is a configuration one — the host is using the boot protocol, or the device only implements it. The fix is a report descriptor supporting more simultaneous keys, and a host that uses it.

If the analyser shows fewer than six real keys and rollover anyway, the device is over-reporting — and §3's callout names the likely cause: matrix ghosting, where a diodeless matrix cannot distinguish certain three-key combinations and the firmware correctly refuses to guess.

And if it shows six real keys with the seventh silently missing? That is the bug §3 exists to prevent — a partial list presented as a complete one. The host has no way to know it is incomplete, so the symptom would be a wrong keystroke rather than none, which is worse and much harder to attribute.

The signature to keep: input that stops entirely above a key count, and resumes on release, is a rollover report being interpreted correctly — and the question is not why the device failed but which of two independent limits it hit.

15. Common Misconceptions

16. Exercises

Trace. With USAGE_BASE = 0x04 and bits 3, 7 and 31 set, write all eight report bytes. Then set bit 0 as well, and again with three more bits set.

Verilog. Add a ghost_suspect input which, when asserted, forces the rollover behaviour regardless of the key count. Where in §6's priority chain does it belong, and why not simply OR it into nkeys > MAX_SLOTS?

SystemVerilog. Replace the linear USAGE_BASE + index mapping with a lookup table parameterised at elaboration. What type expresses it, and what build-time check should accompany it?

VHDL. Implement the same lookup-table variant. Which of VHDL's type features make the table's bounds a compile-time property rather than a convention?

Testbench. §10's Verilog bench checks slots individually and the SystemVerilog bench compares the aggregate. Write a Verilog check that does both — compares the aggregate and then reports the first differing slot — and state what it costs.

SVA. §12's R1 is vacuous when rollover never asserts. Write a property that catches a DUT which asserts rollover too often — at six keys rather than seven — and explain why R2 alone does not.

Mutation. Predict which of §10's stimulus phases catches a mutation that fills slots from the highest set bit downward. Then decide whether either bench would localise it, and what would have to change to make it obvious.

UVM. Design the sequence-item constraint set that guarantees the rollover boundary is crossed in both directions at least once in 50 items, without simply hard-coding the two cases.

Debug. A keyboard produces correct reports for any five keys but wrong usage codes when key 31 is among them. Using §14's layer table, name the first divergence and the most likely cause.

Architecture. A 104-key keyboard needs N_KEYS = 104. Compare §6's combinational population count against a pipelined adder tree: gate count, timing closure, and what changes in the verification.

17. Summary

A keyboard is the clearest case in USB of a device that needs a guarantee about when and almost nothing about how much — which is why it is an interrupt device despite producing a few bytes a second.

The boot report is eight fixed bytes: a modifier bitmap, a reserved zero, and six key slots. The two encodings are the chapter's structural fact — modifiers cannot overflow by construction and ordinary keys can — and that asymmetry is the layout's reason, not an accident of it.

When more than six ordinary keys are down, all six slots carry 0x01 — not six real keys and a dropped one.

The report says either exactly what is pressed, or that it cannot say. A partial list would make every full report ambiguous.

And two independent limits produce the same report: six slots in the protocol, and matrix ghosting in the hardware, where refusing to guess is the correct behaviour.

The report is a registered snapshot, because the device's scan and the host's poll are on independent clocks and a snapshot that changes under its reader is not one.

§11 applied three mutations to the Verilog and the SystemVerilog. All three caught in both, and the two interesting rows disagree in opposite directions:

Removing rollover cost the SystemVerilog bench 810 failures against the Verilog bench's 10 — because of 400 randomised matrices, not because of the language. The usage off-by-one cost the Verilog bench 39 against SystemVerilog's 13 — because it checks six slots separately where the other compares one struct.

Failure count and diagnostic content are independent axes, and neither language caused either difference.

§13 is where UVM starts to earn its place, and the reason is precise: the pins are a bit vector and the transaction is a set — because correctness depends on the set's cardinality and its members' order, neither of which is visible without interpretation. And the monitor must decode the report rather than re-encode the matrix, or the scoreboard compares one thing derived twice instead of two things derived differently.

Tooling, honestly: Verilog and SystemVerilog compiled and simulated clean; VHDL reviewed, not run. Two Icarus limitations shaped the code — no automatic inside always_ff, and a crash on SystemVerilog queues — and both times the workaround was better than what it replaced.

18. What Comes Next

A keyboard reports state: which keys are down right now, and a report lost is a report immediately superseded.

Chapter 15.3 reports something else entirely. A mouse sends relative motion — how far it moved since the last report — and a relative value that is lost is gone, because the next report describes a different interval.

That single difference changes the hardware completely: the accumulator must survive between reports, must saturate rather than wrap, and must be cleared by the act of being read — which introduces a race between the device's motion and the host's poll that a state report simply does not have.

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.