Skip to content

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.

This page describes the protocol and behaviour. The two main use cases on the FPGC today are USB keyboards (HID class) and USB mass storage (the FAT layer is bypassed in favour of raw block reads).

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 into the FPGA's interrupt controller. The chip asserts it whenever an asynchronous event happens (device plugged in, transfer complete, descriptor available). 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:

  1. The host pulls CS low.
  2. The host writes one command byte (the opcode).
  3. The host writes any required parameter bytes for that command.
  4. The host clocks 0xFF to read back any response bytes.
  5. 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. Opcodes occupy the range 0x01..0x6F and are documented in the WCH datasheet.

Useful commands

Only a small subset is needed for the workloads the FPGC runs:

Opcode Mnemonic Purpose
0x01 GET_IC_VER Returns chip firmware version (sanity check on init)
0x02 SET_BAUDRATE Reconfigures the UART side (unused — we use SPI)
0x05 RESET_ALL Soft reset
0x06 CHECK_EXIST Loopback test for bus integrity
0x15 SET_USB_MODE Switches between device, host, and host-with-SOF
0x22 GET_STATUS Reads/clears the chip's interrupt status
0x27 RD_USB_DATA0 Streams the chip's receive buffer
0x2C WR_HOST_DATA Loads bytes into the chip's transmit buffer
0x45 ISSUE_TKN_X Issues a host-side USB token (IN/OUT/SETUP)
0x4E DISK_CONNECT Probes for a connected mass-storage device
0x53 DISK_READ Reads N 512-byte sectors from the device
0x54 DISK_RD_GO Continues a multi-sector read
0x55 DISK_WRITE Writes N 512-byte sectors
0x56 DISK_WR_GO Continues a multi-sector write

The HID-keyboard path uses a different sub-command set (Set Report, Get Report, periodic IN polling) layered on top of ISSUE_TKN_X.

USB modes

The CH376 supports three modes, selected via SET_USB_MODE:

  • Device mode — chip pretends to be a USB peripheral. Unused on the FPGC.
  • Host mode — chip acts as a USB host but does not generate Start-Of-Frame packets automatically.
  • Host with SOF — chip acts as a USB host and generates SOFs. Required for any class that depends on USB framing (which is most of them, including HID).

The driver puts each port into "host with SOF" during init.

Interrupts

After the host issues a command that triggers a USB transaction (e.g. ISSUE_TKN_X), the chip processes it asynchronously and raises its INT line when the result is available. The host reads the chip's interrupt status register (GET_STATUS) to determine the outcome:

Status Meaning
USB_INT_SUCCESS Transaction succeeded
USB_INT_CONNECT A device was plugged in
USB_INT_DISCONNECT The connected device was unplugged
USB_INT_BUF_OVER Buffer overflow on a read
USB_INT_DISK_READ A disk read returned data ready to be drained
USB_INT_DISK_WRITE The chip is ready for the next data block
USB_INT_USB_SUSPEND Bus went into suspend
0x2F NAK (re-issue the token, common for HID polling)

Reading the status register also clears the interrupt for that event, freeing the chip to raise the next one.

Keyboard path

USB keyboards use the HID boot protocol: an 8-byte report describing the modifier keys (bit-mapped) and up to six simultaneously pressed key codes. The FPGC polls the keyboard once per frame interval by issuing an IN token and, on USB_INT_SUCCESS, draining the report out of the chip's receive buffer with RD_USB_DATA0. The decoded report is then translated from HID usage codes to the FPGC's internal key event format and posted into a per-port FIFO that the shell and user programs consume.

Repeated NAKs (0x2F) are normal and just mean the keyboard has nothing new to report; the driver simply waits for the next polling window.

Disk path

USB mass-storage devices (flash drives, external SSDs in USB enclosures) also speak the bulk-transfer SCSI subset on top of USB, but the CH376's firmware abstracts that away. From the host's perspective the chip exposes a block device with three operations:

  • DISK_CONNECT to probe for a device,
  • DISK_READ / DISK_RD_GO to stream sectors out,
  • DISK_WRITE / DISK_WR_GO to stream sectors in.

Sectors are 512 bytes, the same size as on an SD card. Multi-sector operations alternate between DISK_RD_GO (read more) or DISK_WR_GO (the chip has accepted the previous sector — give me the next) callbacks until the requested count is reached.

The CH376 will also expose its built-in FAT driver for filesystem-aware accesses, but the FPGC bypasses that layer entirely: the on-board filesystem is BRFS, not FAT, and the chip's FAT firmware would only get in the way.

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 through SPI2 (USB Host 1) and SPI3 (USB Host 2). Today the keyboard runs on USB Host 1; USB Host 2 is wired and electrically validated but no driver code binds to it yet.