CH376 USB Host
The FPGC has two WCH CH376T USB host controllers on the board.
Each chip provides a single USB type-A host port and exposes its
state machine to the FPGA over an 8-wire SPI bus. The two
controllers are wired to SPI2 and SPI3 on the FPGA side.
The CH376 is an unusual peripheral by FPGC standards: rather than exposing a raw USB transceiver, it implements the entire USB host stack — enumeration, control transfers, mass-storage class support, HID class support, and even a built-in FAT12/16/32 filesystem driver — in firmware on the chip. The host MCU (or in our case the FPGC's CPU) talks to it over a small command-byte protocol that hides USB's complexity entirely.
The FPGC uses the CH376 exclusively as a generic USB host controller — primarily for USB HID keyboards. The chip's built-in FAT filesystem and mass-storage commands are unused; the FPGC has its own SPI flash and SD card slot with BRFS for storage.
Bus shape
Each CH376 chip presents the same set of pins to the FPGA:
| Pin | Role |
|---|---|
CS |
chip select (active low) |
CLK |
serial clock |
SI |
host → chip (MOSI) |
SO |
chip → host (MISO) |
INT |
falling-edge interrupt to host |
RST |
active-low reset |
Internally each CH376 also has its own 12 MHz crystal and the on-chip USB transceiver lines that lead to the type-A connector. The chip handles VBUS switching for the connected device through a small load switch on the PCB.
The SPI clock can run up to roughly 12 MHz on the CH376; the FPGC runs both ports at the same divider as the slow-side SPI peripherals.
INT is wired to dedicated FPGA GPIO pins, readable via MMIO
(FPGC_CH376_TOP_NINT and FPGC_CH376_BOT_NINT). The chip
asserts INT (active low) whenever an asynchronous event happens
(device plugged in, device unplugged, transfer complete). The driver
acknowledges by reading the chip's interrupt status register.
Protocol shape
CH376 communication is a stream of command bytes punctuated by small data payloads. A typical exchange looks like this:
- The host pulls
CSlow. - The host writes one command byte (the opcode).
- The host writes any required parameter bytes for that command.
- The host clocks
0x00to read back any response bytes. - The host raises
CS.
There is no concept of a 6-byte command frame as on SD or SPI flash — every CH376 instruction has its own length defined by the opcode itself. A minimum 1.5 µs inter-command delay is required after sending the command byte before sending/receiving data.
Useful commands
The USB host keyboard driver uses the following subset:
Initialisation & diagnostics
| Opcode | Mnemonic | Purpose |
|---|---|---|
0x01 |
GET_IC_VER |
Returns chip firmware version (sanity check) |
0x05 |
RESET_ALL |
Soft reset (wait ~35 ms for completion) |
0x06 |
CHECK_EXIST |
Loopback test — send a byte, get its bitwise NOT back |
0x15 |
SET_USB_MODE |
Switch between disabled, host, host+SOF, bus-reset |
0x04 |
SET_USB_SPEED |
Select full-speed (12 Mbps) or low-speed (1.5 Mbps) |
Connection detection
| Opcode | Mnemonic | Purpose |
|---|---|---|
0x16 |
TEST_CONNECT |
Query connection status (connected/disconnected/ready) |
0x0A |
GET_DEV_RATE |
Check if connected device is low-speed |
0x22 |
GET_STATUS |
Read and clear the chip's interrupt status |
USB transactions
| Opcode | Mnemonic | Purpose |
|---|---|---|
0x27 |
RD_USB_DATA0 |
Read received data from chip's USB buffer |
0x2C |
WR_HOST_DATA |
Write data to chip's USB transmit buffer |
0x4E |
ISSUE_TKN_X |
Issue a USB token (IN/OUT/SETUP) with explicit toggle |
0x4F |
ISSUE_TOKEN |
Issue a USB token (simpler, toggle managed by chip) |
Simplified enumeration
| Opcode | Mnemonic | Purpose |
|---|---|---|
0x45 |
SET_ADDRESS |
Set USB device address (simplified SET_ADDRESS) |
0x46 |
GET_DESCR |
Get device/config descriptor (simplified control xfer) |
0x49 |
SET_CONFIG |
Set USB configuration (simplified SET_CONFIGURATION) |
0x13 |
SET_USB_ADDR |
Tell CH376 which device address to talk to |
Toggle & retry control
| Opcode | Mnemonic | Purpose |
|---|---|---|
0x0B |
SET_RETRY |
Configure NAK retry behavior and timeout retries |
0x1C |
SET_ENDP6 |
Set RX data toggle (DATA0/DATA1 synchronisation) |
0x1D |
SET_ENDP7 |
Set TX data toggle |
USB modes
The CH376 supports several modes, selected via SET_USB_MODE:
| Mode | Code | Description |
|---|---|---|
| 0 | 0x00 |
Disabled (default after reset) |
| 4 | 0x04 |
USB host, disabled (no auto-detect) |
| 5 | 0x05 |
USB host, enabled — auto-detects connect/disconnect |
| 6 | 0x06 |
USB host, enabled + generates SOF packets |
| 7 | 0x07 |
USB host, bus reset — holds USB bus in reset state |
The driver initialises each port in mode 5 (host enabled, no
SOF). In this mode the CH376 automatically detects device
connect/disconnect and asserts INT to notify the CPU — no polling
required.
During device enumeration, the driver briefly enters mode 7 (bus reset) to reset the USB bus, then switches to mode 6 (host with SOF) to generate the Start-Of-Frame packets required by most USB devices. The device stays in mode 6 until disconnected.
Interrupts
The CH376 asserts its INT pin (active low) for asynchronous
events. The host reads GET_STATUS to determine the cause and
clear the interrupt:
| Status byte | Name | Meaning |
|---|---|---|
0x14 |
USB_INT_SUCCESS |
USB transaction completed successfully |
0x15 |
USB_INT_CONNECT |
A device was plugged in |
0x16 |
USB_INT_DISCONNECT |
The connected device was unplugged |
0x17 |
USB_INT_BUF_OVER |
Buffer overflow on a read |
0x18 |
USB_INT_USB_READY |
Device initialised (address assigned) |
For failed transactions, bits 3-0 encode the USB response:
0x2A— device returned NAK (normal for HID polling when no new data is available)0x2E— device returned STALL
The BDOS driver checks the INT pin in the main loop. When
asserted, it reads the status to handle connect/disconnect. During
keyboard polling (timer ISR, every 10 ms), failed IN transactions
return NAK status and are silently ignored.
Keyboard path
USB keyboards use the HID boot protocol: an 8-byte report describing the modifier keys (bit-mapped byte 0) and up to six simultaneously pressed key codes (bytes 2–7; byte 1 is reserved).
Enumeration
When the CH376 asserts INT with status USB_INT_CONNECT, the
driver enumerates the device:
- Bus reset — enter mode 7, hold for 50 ms, switch to mode 6.
- Speed detection —
GET_DEV_RATEdetermines full/low speed. - Device descriptor —
GET_DESCRretrieves the 18-byte device descriptor. - Set address —
SET_ADDRESSassigns address 1 to the device. - Set configuration —
SET_CONFIGactivates configuration 1. - Config descriptor — manual control transfer reads the config descriptor (up to 64 bytes) and parses it for an HID boot keyboard interface and its interrupt IN endpoint.
- SET_PROTOCOL — class-specific control transfer forces boot protocol mode (guaranteed 8-byte reports).
- SET_IDLE(0) — tells the device to only send reports when key state changes.
- SET_RETRY — configures the CH376 for immediate NAK notification (no infinite retry), so the polling ISR never blocks.
Polling
A hardware timer fires every 10 ms and calls the keyboard poll
function. This issues an IN token to the device's interrupt endpoint
using ISSUE_TKN_X with a 10 ms timeout. Three outcomes:
USB_INT_SUCCESS— new report available. The 8-byte report is read viaRD_USB_DATA0, translated from HID keycodes to the FPGC's internal event format, and pushed into a 64-entry FIFO. Key repeat is handled in software.- NAK (
0x2A) — no new data; the ISR returns immediately. - Error / timeout — returned as a negative status; the main loop handles reconnection.
Disconnect
When the INT pin asserts with status USB_INT_DISCONNECT, the
driver clears the keyboard state (FIFO, key bitmap, repeat state).
No chip reset is needed — the CH376 stays in mode 5 and will
generate a new USB_INT_CONNECT when a device is plugged in again.
Two ports
Both ports are fully independent — each has its own CH376 chip, its
own 12 MHz crystal, its own VBUS switch, and its own SPI controller
on the FPGA side. Software addresses them as CH376_SPI_TOP
(SPI_USB_0) and CH376_SPI_BOTTOM (SPI_USB_1).
Today the keyboard runs on the bottom port (CH376_SPI_BOTTOM).
The top port is wired and electrically validated but no driver code
binds to it yet.