Back to Home

USB controller on FPGA: development for HID devices using the example of a keyboard

The article is dedicated to the development of USB 1.1 controller based on FPGA for connecting HID devices such as a keyboard. It covers modular architecture in Verilog, device initialization process, importance of timely SOF packet transmission and analysis of HID descriptors.

Creating USB controller on FPGA: from theory to a working keyboard
Advertisement 728x90

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:

Google AdInline article slot
  • 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.

Google AdInline article slot

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

Google AdInline article slot

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

Advertisement 728x90

Read Next