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.
| Byte | Content |
|---|---|
| 0 | modifier bitmap — one bit each for the left and right Ctrl, Shift, Alt and GUI keys |
| 1 | reserved, always 0x00 |
| 2–7 | six 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'sErrorRollOver.
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.
| Element | Purpose |
|---|---|
| A population count over the matrix | is the limit exceeded? |
| A bounded fill of six slots | the selection |
| An index that stops at 6 | the bound |
| A usage base, added to the bit index | bit 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.
6. Verilog
// ─────────────────────────────────────────────────────────────────────────
// 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
endmoduleContract. 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.
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
endmodulerpt_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
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
| Concern | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| Report as an object | eight separate ports | struct packed — one type | array type + separate ports |
| Reset the whole report | loop + separate byte | rpt_q <= '0 | loop + separate signal |
| Population count | explicit loop | $countones | explicit loop, ranged type |
| Slot storage | reg [7:0] slot [0:5] | packed [5:0][7:0] | slot_array_t |
| Comb/seq separation | one always block | always_comb + always_ff | two named processes |
| Bound on the count | none | none | natural range 0 to N_KEYS |
| Whole-array assignment | per element | per field | slots_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:
check(k0 === nth_usage(0), "slot 0 usage");
check(k1 === nth_usage(1), "slot 1 usage");
// …one check per slotThe SystemVerilog bench compares the whole report against an independent model and adds 400 randomised matrices:
// 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
endfunctionThe VHDL bench uses the same independent-scan model with assert:
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.
| Mutation | Verilog TB | SystemVerilog TB |
|---|---|---|
| K1 rollover never detected | 10 errors | 810 errors |
| K2 usage code off by one | 39 errors | 13 errors |
| K3 report decays between samples | 1 error | 1 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.
// 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 component | What it would own here |
|---|---|
| Sequence item | a 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} |
| Sequence | scenarios: a rollover entered and left; keys added one at a time to the boundary; modifiers varied independently of keys |
| Driver | converts a key set into the matrix bits and one sample pulse — the only component that knows the pin encoding |
| Monitor | observes 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 |
| Scoreboard | compares reports, and separately tracks whether the rollover transitions were seen |
| Coverage | key count (0,1,5,6,7,>7) × modifier count (0,1,>1) × rollover transition |
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?
| Layer | Observation | Tool |
|---|---|---|
| System | input stops above N simultaneous keys | the user |
| Host stack | which protocol — boot or report | host driver state |
| USB-visible | 0x01 in all six slots | protocol analyser |
| Device | rollover asserted, key count > 6 | firmware log or ILA |
| RTL | the population count at the sample | first 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
Related tutorials
- Related topic
Mice on USB
A relative report cannot be resent, so the accumulator must saturate rather than wrap and must be cleared by the act of being read — and a real signedness bug the testbench caught on its first check.
- Related topic
The Transfer-Types Question
Guaranteed bandwidth and guaranteed delivery are the same resource spent two different ways — isochronous has no retries because every slot is already promised, not because somebody skimped.
- Related topic
The Polling Model
The host runs a periodic schedule the device cannot see, so device hardware must count frames rather than trust a period — built in Verilog, SystemVerilog and VHDL with a schedule model that disagrees independently.
- Related topic
Interrupt Latency Requirements
bInterval is a request, not a contract, and it bounds one term of seven. A latency monitor in three HDLs, and the measurement showing 3000 randomised steps could not reach two of its own boundaries.
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.
