Repository navigation
Lesson 8 The USB stack
How the FM-1 appears to a computer as a MIDI device and a USB microphone, written from scratch at the register level, with no USB library and no interrupts. Files: firmware/src/usb.c (1,078 lines), firmware/hal/fm1_usb.h, firmware/src/midi_uart.c (the TRS jack), and the tests tests/host/uac_test.c and tests/host/trs_test.c.
This is an optional track, inherited mostly from the Felucca and X0X projects. You don't need it to write an app like FM1-GM, but it's an excellent way to learn how USB really works underneath an operating system's driver.
If you've only used USB from the host side, here is the device's view.
- The host is in charge. The device never speaks first. The host sends tokens: SETUP, OUT (host to device data), IN ("send me data if you have any"). If the device has nothing ready for an IN, it answers NAK and the host asks again later.
- Endpoints are numbered mailboxes. Endpoint 0 (EP0) is the control endpoint every device has; the host uses it to ask what the device is and to configure it. Other endpoints carry the actual data. FM1-GM uses EP1 (MIDI, both directions) and EP4 (audio, IN only).
- Transfer types: control (EP0, request and response), bulk (reliable, whenever there's bandwidth: MIDI uses this), isochronous (guaranteed slot every 1 ms frame, no retries: audio uses this), interrupt (small, polled).
-
Enumeration: when you plug a device in, the host resets the bus, assigns an address (
SET_ADDRESS), reads descriptors (GET_DESCRIPTOR) that describe the device, then picks a configuration (SET_CONFIGURATION). After that the data endpoints are live. - Classes: if the descriptors say "I am a USB Audio Class device with a MIDI Streaming interface", every operating system already has a driver. That's what class compliant means, and why the FM-1 needs no driver install.
USB full speed sends a Start Of Frame (SOF) every 1 ms, with an 11-bit frame number. FM1-GM uses that to tell whether a host is really there.
Read the header comment of usb.c. The key decision:
All SIE traffic happens in usb_poll(), called from the TIMER5 ISR at 2 kHz, so INDEX is never shared and no USB IRQ is needed.
The USB controller here is a MUSB-style core (the SIE, serial interface engine). Its endpoint registers are reached through a shared INDEX register: you write the endpoint number to INDEX, then read or write that endpoint's control/status registers. If two contexts touched USB, one could change INDEX between the other's two accesses. Instead of locking, the whole driver runs from one place: usb_poll(), called every 0.5 ms from the timer interrupt (Stage 1, section 5.3). One context, no locks, no USB interrupt at all.
The cost is up to 0.5 ms of extra latency, irrelevant for MIDI. The benefit is a driver you can reason about.
A note on stale comments, again. The header says MIDI in goes to a ring "the audio ISR drains". As Stage 1 found, it's actually drained by the UI in the main loop. It also mentions console.c, editor.c and announce.c, which are files from the ancestor projects that aren't in this repository. Read inherited comments with care.
Every SIE register access is a small handshake through fm1_usb.h:
static void sie_wr(uint32_t r, uint32_t v)
{
uint32_t n = 20000;
if (!fm1_usb_sie_on()) return;
fm1_usb_sie_wr_start(r, v);
while (!fm1_usb_sie_done() && --n)
;
if (n) usb.timeouts = 0; // consecutive failures only
else if (++usb.timeouts > 50u) usb.up = 0; // SIE dead (no clock?): stop polling it
}Notice the bounded wait: every busy-wait has a counter, and 50 consecutive timeouts mark USB as down rather than hanging the timer interrupt forever. Then the main loop's usb_retry() tries usb_start() once a second, so plugging in a cable after powering on still works. Never write an unbounded wait on hardware.
The descriptors are const uint8_t arrays near the top of the file, written as raw bytes with a comment per line. DEV_DESC is the device descriptor (18 bytes: USB version, max packet size for EP0, vendor id 0x1209 and product id from pid.codes, string indexes). CFG_DESC is the configuration descriptor and everything under it:
configuration
interface 0: Audio Control (required by the class; just headers and terminals)
interface 1: MIDI Streaming (EP1 OUT and EP1 IN, bulk, 64 bytes; one MIDI IN jack, one OUT jack)
interface 2: Audio Streaming (alt 0: no bandwidth; alt 1: EP4 IN, isochronous, 16-bit stereo 44.1 kHz)
[interfaces 3-4: CDC serial, only if FELUCCA_CDC]
The #if FELUCCA_UAC and #if FELUCCA_CDC blocks splice optional functions in and out, and CFG_LEN is computed to match. One line catches mistakes at compile time:
typedef char cfg_len_ok[sizeof CFG_DESC == CFG_LEN ? 1 : -1];If the array's size doesn't match the length written inside it, the array type has size −1 and the build fails. That's the pre-C11 version of _Static_assert, which the file also uses elsewhere. Hand-written binary tables deserve compile-time checks.
Another small detail: bcdDevice (the device version) changes when the audio or CDC function is added, because "hosts cache descriptors per version". Windows in particular remembers a device's descriptors by vendor, product and version; change the interfaces without changing the version and you get confusing behaviour.
The audio streaming interface has two alternate settings. Alt 0 has no endpoint, so it reserves no bandwidth. When an app starts recording, the host selects alt 1 with SET_INTERFACE, which reserves the isochronous slot. That's how USB audio devices stay out of the bus's way when nobody listens.
ep0_service() handles the control endpoint. A control transfer is a SETUP packet (8 bytes: request type, request, value, index, length), optionally a data stage, then a status stage. The function reads the setup bytes and dispatches with one switch on bmRequestType << 8 | bRequest:
| Code | Request | What the device does |
|---|---|---|
0x0005 |
SET_ADDRESS | remember it; apply it after the status stage (pend_addr) |
0x8006 |
GET_DESCRIPTOR | send the descriptor, at most wLength bytes |
0x0009 |
SET_CONFIGURATION | configure EP1 (and EP4); config 0 turns them off |
0x8008 |
GET_CONFIGURATION | send 1 byte |
0x010B |
SET_INTERFACE | alt 1 on the audio interface starts the stream |
0x0201 |
CLEAR_FEATURE (halt) | reset an endpoint's data toggle |
0x2201, 0xA281..3
|
audio SET_CUR / GET_CUR/MIN/MAX | sampling rate: always 44,100 |
| anything else | STALL: "I don't support that" |
Three points worth understanding:
-
SET_ADDRESS is delayed. The device must finish the status stage at the old address (0) before switching. Getting this wrong is the classic first-USB-driver bug. Here,
pend_addris written toFADDRat the start of the nextep0_service()call. -
Long responses go out in 64-byte chunks (
e0_send,e0_chunk). If the response is shorter than the host asked for and an exact multiple of 64, a zero-length packet must follow to tell the host it's finished (e0_zlp). Another classic. - STALL is a normal answer. Unknown requests are stalled, and hosts handle that fine. Supporting only what you need keeps the driver small.
The goto ack; / goto stall; at the end of the switch are a common and readable C idiom for shared exit paths.
USB-MIDI doesn't send raw MIDI bytes. Every message is a 4-byte event packet:
byte 0: cable number (high nibble) | Code Index Number, CIN (low nibble)
bytes 1-3: the MIDI bytes, padded with 0
The CIN says what kind of message it is, mostly the status's high nibble (0x9 note on, 0xB control change…), plus special codes for SysEx: 4 = SysEx starts or continues (3 bytes), 5/6/7 = SysEx ends with 1/2/3 bytes. That's why the whole firmware passes MIDI around as uint32_t pkt and pulls out st = (pkt >> 8) & 0xFF etc. (you saw this in midi_in() in Stage 5).
For each 4-byte packet in the received buffer:
- SysEx CINs (4–7): feed the bytes one at a time to
sysex_byte(). - Channel messages (CIN 8–E) and real-time bytes (clock, start, stop: CIN F with a byte ≥
0xF8): push the packet intomidi_in_q, the SPSC ring from Stage 1, if there's room.
sysex_byte() assembles SysEx messages. Short ones are compared with two fixed commands: F0 22 24 35 7D F7 asks to reboot into the chip's update mode, F0 22 24 35 7F F7 starts a firmware update (Lesson 9). Longer ones are collected into sx_frame (up to 640 bytes) and handed to the main loop with a flag, sx_ready, for the update protocol and the song editor link (Lesson 10). Only one frame is buffered; a frame arriving while one waits is dropped and the host retries. One buffer plus a ready flag is the simplest handoff between contexts when the producer can afford to drop.
Every poll, if the previous IN packet has been taken, build a new one of up to 16 events (64 bytes):
-
SysEx first, from
sx_out_q, so a reply to the editor is never interleaved with notes. - Then MIDI out from
midi_out_q, filled by the sequencer in the audio interrupt (seq_midi_out, Stage 3), but only when no SysEx is being queued (sx_busy).
ota_wire_send() is the reverse of the CIN rules: it chops an F0 … F7 message into 3-byte packets with CIN 4, ending with CIN 5, 6 or 7. If the queue is full it waits, but at most 200 ms and only while USB is configured, feeding the watchdog meanwhile (ota_idle()). Bounded again.
In usb_poll(), a bus reset (iu & 0x04) sets the address to 0, unconfigures, throws away pending MIDI out (mo_r = mo_w) and pending SysEx, and resets the endpoint-0 state. A new host shouldn't receive note-offs meant for the last one. On a reset, drop anything that belonged to the old session.
The comments record a hardware surprise: the SOF-pending flag keeps firing with the cable out, so it can't tell you a host is present. Instead, every 10 ms the driver reads the frame number; if it hasn't moved for 100 ms, the device considers itself suspended (unplugged or host asleep), and the UI stops showing "USB" (and plat_battery() stops claiming USB power). Verify your assumptions about hardware signals; write down the ones that turned out false.
This is the most interesting part of the file, and the problem is universal in audio: the FM-1 makes samples at the rate of its own crystal, measured at about 44,145 Hz, while the computer takes 44,100 samples a second by its clock (44 frames in most 1 ms packets, 45 in every tenth: uac.acc += 100; if >= 1000 → 45). The two never agree exactly, so a buffer between them slowly fills up or drains.
The comment explains the two options:
- Felucca's way: send 44 or 45 frames per packet as the buffer needs. The host then sees a device at 44,145 Hz. A recorder doesn't mind, but an app passing the input straight to its output has two clocks and drops or repeats samples every few seconds.
- X0X's way (used here): resample the FM-1's audio so that exactly 44,100 frames a second go out, and steer the resampling ratio so the buffer stays at a target fill. To the host, the FM-1 runs on the host's own clock.
The pieces:
-
The ring:
ua_ring[2048], frames packed as 16-bit L | R << 16. Producer: the audio interrupt (uac_tap, called right after each render inrender_block). Consumer:uac_service()in the timer interrupt. Power-of-two size, masked indexes (& (UA_N - 1u)). -
The resampler in
uac_tap(): for each input frame, produce as many output frames as the step says, using cubic Hermite interpolation over the last four frames.rs_stepslightly above 1 means "consume input a bit faster than output". -
The control loop in
uac_window(): every 32 renders (about 185 ms), take the lowest fill seen (measured just before a block goes in, the ring's low point), compare it to the targetUA_MID= 200 frames, and adjust:
err = (float)(win_min - UA_MID);
uac.rs_trim += err * 1.0e-6f; // integral: learns the clocks' drift
uac.rs_step = 1.0f + uac.rs_trim + err * 1.0e-5f; // proportional on topThat's a PI controller (proportional + integral), the same thing that keeps a thermostat or a cruise control on target. The integral term learns the constant difference between the two clocks; the proportional term corrects short-term error. Taking the minimum over a window instead of the instantaneous fill ignores the sawtooth from rendering in 256-frame blocks.
-
Starting and stopping: hosts often select alt 1 long before they actually read, and a recorder can stop reading without going back to alt 0. So the ring is only fed while packets are really being taken (
flowing), and is primed with 460 frames of silence when the stream (re)starts, so there's a cushion. - Underruns and overruns are counted, not hidden: an empty ring repeats the last frame (a tiny glitch rather than a click to zero); a full one drops the frame.
tests/host/uac_test.c runs exactly this code against a simulated host: a 1 kHz sine at a deliberately wrong input rate (44,145–44,180 Hz), 40 simulated seconds, and checks that after settling the output is 1 kHz by the host's clock, clean, with no short packets, and the ring's low point near 200. That's how you test a control loop: simulate the plant (the two clocks), run it, check it settles.
The isochronous endpoint also has a double buffer (ep4tx[2]): the DMA may still be reading the last packet while the next is being written.
Classic 5-pin (here 3.5 mm TRS) MIDI is a 31,250 baud serial line. The UART receives bytes by DMA into a 128-byte ring, and uart_midi_poll(), in the same timer-interrupt context as usb_poll(), parses them and pushes USB-MIDI packets into the same midi_in_q. The rest of the firmware can't tell USB from TRS. Both producers run in one context, so the ring still has a single producer.
The parser um_byte() is a compact model of the serial MIDI byte stream:
- Bytes ≥
0xF8are real-time messages: they can appear anywhere, even between the data bytes of another message, and don't disturb it. Clock, start, continue and stop are passed on. - Other bytes with the top bit set are status bytes: remember it (unless it's a system message), and how many data bytes it needs (1 for program change and channel pressure, 2 for the rest).
- Data bytes fill the message; when complete, it's emitted. The status is kept, so the next data bytes form another message with the same status: running status, which MIDI senders use to save bandwidth (and which inspired Stage 2's "same length" bit).
- SysEx is ignored here, so a TRS cable can't trigger the update-mode command.
If the DMA writes faster than the poll reads and laps the reader, uart_midi_take() skips to the newest 128 bytes and resets the parser state, so it resynchronises on the next status byte instead of misreading data as a message. tests/host/trs_test.c feeds byte sequences (with running status, real-time interruptions, SysEx) and checks the packets that come out.
- Run a whole driver from one context, and you need no locks.
- Bound every hardware wait; degrade to "off" and retry later.
- Descriptors are data; check their lengths at compile time; bump the version when they change.
- EP0 is a small state machine: delay SET_ADDRESS, send zero-length packets when needed, STALL what you don't support.
- Represent MIDI internally as USB-MIDI 4-byte packets; it's fixed-size and easy to queue.
- On bus reset, drop the old session's queued data.
- Two clocks always drift. Resample and steer the buffer fill with a PI controller; test it in simulation.
- A serial MIDI parser must handle running status, real-time bytes anywhere, and resync after data loss.
- Decode this USB-MIDI packet sequence into MIDI messages:
09 90 3C 64,0B B0 07 50,04 F0 7D 46,07 47 01 F7. - Why must
SET_ADDRESStake effect only after the status stage? What would the host see if the device switched immediately? - A host asks for 64 bytes of a 128-byte descriptor (
wLength= 64). Does the device send a zero-length packet after it? What if the descriptor were exactly 64 bytes andwLengthwere 255? - In
uac_window(), the proportional gain is 10⁻⁵ and the integral gain 10⁻⁶ per window. What happens if you make the integral gain 100 times bigger? (Think: overshoot and oscillation.) Try it inuac_test.c. - Feed
um_byte()by hand:90 3C 64 F8 3E 64 C0 05. Which packets come out, in order? - Why does the driver read only
INTRTX1/INTRRX1and skip the*2registers outside the update loader?
Next: Lesson 9, the firmware installer.
← Stage 7: The sound engine · Contents · Lesson 9: The firmware installer →