Building a USB 1.1 Controller on FPGA: Hands-On Guide to HID Keyboard Integration
Developing your own USB controller on an FPGA demands a solid grasp of the protocol and hardware quirks. This guide walks through implementing a USB 1.1 controller that connects a keyboard, covering key modules, device initialization, and HID descriptor handling.
Hardware Features of USB 1.1
Unlike USB 2.0, USB 1.1 uses just two digital lines (D+ and D-) that run in differential mode, letting you hook them straight to FPGA pins. It has two speeds: Low Speed (1.5 Mbps) and Full Speed (12 Mbps). The device signals its speed by pulling D- high for Low Speed or D+ high for Full Speed during connection.
Key hardware considerations:
- Data lines need pull-down resistors to ground on the host side.
- Protect FPGA inputs from 5V spikes, say with 3.3V Zener diodes.
- For testing, grab boards like the STM32 BluePill to emulate HID devices.
USB Controller Module Architecture
The controller breaks down into interconnected modules, each tackling a specific job in the USB protocol stack.
- M_DATA — Core module for sending and receiving data.
- M_CRC16_USB — Computes CRC16 checksums for data packets.
- M_GET_PACKET — Handles data reception: sends IN token, receives packet, checks CRC, and sends ACK.
- M_RECEIVE_MODULE — Data receive module, including:
* M_GET_DATA — Reads raw data from the bus.
* M_MEMORY_BUF_CRC16 — Buffer that calculates CRC16 on the fly.
* M_BUF_RETRANSLATOR — Forwards data from buffer only after successful CRC check.
- M_SEND_PACKET — Manages data transmission: OUT token, data send, and ACK wait.
- M_TRANSMIT_MODULE — Transmits data, tokens, Start of Frame (SOF) packets, and handshake packets.
- M_CRC_5 — Calculates CRC5 for tokens, SOF, and handshakes.
- M_SEND_DATA — Sends data packets with CRC16.
- M_SEND_TOKEN — Sends tokens, SOF, and handshakes (all with CRC5).
- M_SEND_END_OF_PACKET — Generates End of Packet (EOP) signal.
- M_DATA_TRANSFER — Physical layer for bit transmission on the bus.
- M_SOF_SENDER — Periodically sends SOF packets for clock sync.
- M_MAIN_AUTOMATA — Main state machine controlling the controller.
- M_USB_INIT — Handles USB device initialization.
- M_TRANSACTION — Processes control transfers.
- M_HID_ANALYZER — Parses HID descriptors to understand data formats.
- MEMORY_BUF (4 instances) — Buffers for HID analysis data.
Device Initialization Process
Initialization kicks off by detecting speed from D+ and D- states. Then the main state machine (M_MAIN_AUTOMATA) runs a precise sequence.
always @(posedge clk)
begin
if (rst)
begin
State_main <= S_IDLE;
SOF_En <= 1'b0;
FullSpeedConnect <= 1'b0;
// ... reset other signals
end
else
begin
case (State_main)
S_IDLE:
// Wait for connection and speed detection
if (Dm & !Dp) // Low Speed
begin
FullSpeedConnect <= 1'b0;
ResetTime <= 15_000; // 10 ms at 1.5 MHz
// ... set Low Speed timers
State_main <= S_POWER_RISE;
end
else if(Dp & !Dm) // Full Speed
begin
FullSpeedConnect <= 1'b1;
ResetTime <= 120_000; // 10 ms at 12 MHz
// ... set Full Speed timers
State_main <= S_POWER_RISE;
end
S_POWER_RISE:
// Wait 100 ms for power stabilization
if (WaiteCount == PowerRiseTime || SKIP_POWER_RISE)
begin
WaiteCount <= 0;
State_main <= S_USB_RESET;
end
S_USB_RESET:
// Device reset (0 on D+ and D- for 10 ms)
if (WaiteCount == ResetTime || SKIP_POWER_RISE)
begin
WaiteCount <= 0;
State_main <= S_INIT_SOF_SENDER;
end
S_INIT_SOF_SENDER:
// Start SOF packets right after reset
if (Eof1) State_main <= S_WAITE_SOF;
else SOF_En <= 1'b1;
S_WAITE_SOF:
if (!Eof1) State_main <= S_USB_RESET_RECOWERY;
S_USB_RESET_RECOWERY:
// Recovery after reset (10 ms)
if (WaiteCount == ResetTime || SKIP_POWER_RISE)
begin
WaiteCount <= 0;
State_main <= S_USB_INIT;
end
S_USB_INIT:
// Run control transfer sequence
if (InitComplite)
begin
State_main <= S_REQUEST;
end
else if (InitFail)
State_main <= S_FAIL;
// ... polling states (S_REQUEST, S_WAIT, etc.)
endcase
end
end
Critical tip: Start sending SOF (Start of Frame) packets immediately after the reset phase (S_USB_RESET), not during recovery. SOF keeps the device in sync with the host. Full Speed uses a frame number with CRC5; Low Speed just sends EOP (two zero cycles on both lines). Delaying SOF can break compatibility with some devices.
HID Descriptor Parsing and Device Polling
Once initialization wraps up—including assigning a non-zero address—the controller shifts to polling (S_REQUEST). The M_HID_ANALYZER module digs into descriptors fetched via control transfers to figure out data packet formats and sizes. For a keyboard, it pulls report count, size, and IDs.
Typical HID polling loop:
- Send IN token to device address and Endpoint 1.
- Receive data packet.
- Verify CRC16.
- Send ACK if good.
- Extract payload (like key scan codes), skipping leading header bytes.
- Wait the polling interval (e.g., 24 ms) before next poll.
If the device sends NAK (Not Acknowledged), retry later. No response or errors? Fall back to wait or re-init.
Key Takeaways
- Timely SOF packets: Fire off Start of Frame right after reset—vital for reliable HID operation.
- FPGA input protection: Shield data lines from 5V with proper circuitry, as USB mixes 3.3V/5V levels.
- Descriptor parsing: HID won't work without breaking down descriptors to map data structures.
- Error handling: Gracefully manage NAKs, STALLs, and timeouts for robust links.
- Modular design: Breaking into modules eases debugging and reuse across projects.
— Editorial Team
No comments yet.