pico-serial control protocol (version 1)¶
For host apps (the D-Modem app, a desktop tool) that want more than a plain
serial port gives: the board's role, every modem line, and settings. The
C definitions are in src/core/protocol.h, and every value of them in the protocol reference.
Finding it¶
The USB device has a vendor interface:
- class 0xFF, subclass 0x50, protocol 0x01;
- named "pico-serial control";
- with no endpoints.
Every request is a vendor control request with the interface as
recipient, and wIndex set to that interface's number (2 in firmware 0.1).
| bmRequestType | Direction |
|---|---|
0xC1 |
IN (device → host) |
0x41 |
OUT (host → device), wLength = 0 |
An unknown request or an out-of-range value stalls.
Requests¶
| bRequest | Name | Dir | wValue | Data |
|---|---|---|---|---|
| 1 | GET_INFO | IN | 0 | ps_info_t, 14 bytes |
| 2 | SET_MODE | OUT | 0 auto, 1 DCE, 2 DTE | — (saved in flash) |
| 3 | SET_OUTPUTS | OUT | mask << 8 \| value |
— (not saved) |
| 4 | GET_INPUTS | IN | 0 | ps_inputs_t, 5 bytes |
| 5 | SET_FLOW | OUT | 0 auto, 1 on, 2 off | — (saved) |
| 6 | GET_STATS | IN | 0 | ps_stats_t, 32 bytes |
| 7 | CLEAR_STATS | OUT | 0 | — |
| 8 | SET_BAUD_MODE | OUT | 0 auto, 1 host, 2 learn | — (saved) |
| 9 | AUTOBAUD_RESTART | OUT | 0 | — (no effect unless learning is in use) |
| 10 | BT_PAIRING | OUT | seconds (0 closes) | — W board only: open the Bluetooth pairing window |
| 11 | BT_FORGET | OUT | 0 | — W board only: forget every bonded phone, then open the window |
All multi-byte fields are little-endian.
Modem line bits¶
| Bit | Line |
|---|---|
0x01 |
DTR |
0x02 |
RTS |
0x04 |
DCD |
0x08 |
DSR |
0x10 |
RI |
0x20 |
CTS |
1 means asserted (ON).
As DCE: the board drives DCD, DSR, RI and CTS, and reads the computer's DTR and RTS.
As DTE: it drives DTR and RTS, and reads DCD, DSR, RI and CTS.
SET_OUTPUTS overrides the default mapping (SPEC.md §5.3–5.4) for the
lines in mask. Example: wValue = 0x1010 raises RI for a ring. RI has no
default source, so it needs SET_OUTPUTS. An override of the flow line (CTS
as DCE, RTS as DTE) is still held off while the board's buffer to the host
is ¾ full.
Baud rate¶
Where the UART's speed comes from (SET_BAUD_MODE):
| wValue | Mode | As DCE | As DTE |
|---|---|---|---|
| 0 | auto (default) | learned | the host's |
| 1 | host | the host's | the host's |
| 2 | learn | learned | learned |
"The host's" is the speed in the last CDC SET_LINE_CODING. "Learned" is measured from the far end's characters: see README.md. Data bits, parity and stop bits are always the host's.
While learning, received bytes are held back. Once the speed is known
(GET_INFO flag 0x08), the characters it was learned from are sent on, and
the UART switches to it. GET_INFO's baud is the speed the UART runs at,
learned or not. AUTOBAUD_RESTART starts learning again.
Structures¶
ps_info_t (14 bytes):
| Offset | Size | Field |
|---|---|---|
| 0 | 1 | protocol (1) |
| 1 | 3 | firmware major, minor, patch |
| 4 | 1 | board: 0 hood, 1 W, 2 Pico bench rig |
| 5 | 1 | mode setting: 0 auto, 1 DCE, 2 DTE |
| 6 | 1 | role now: 0 none, 1 DCE, 2 DTE |
| 7 | 1 | flow setting |
| 8 | 1 | flags: 0x01 last line coding adjusted, 0x02 baud above 250 kbit/s, 0x04 CTS flow control active, 0x08 baud learned, 0x10 learning the baud now |
| 9 | 4 | baud the UART runs at now |
| 13 | 1 | baud mode setting: 0 auto, 1 host, 2 learn |
Fields may be added at the end in later firmware without changing the protocol version. Ask for at least the length you understand and read what comes back.
ps_inputs_t (5 bytes):
| Offset | Field |
|---|---|
| 0 | role |
| 1 | inputs: lines read from the far end |
| 2 | outputs: lines driven now |
| 3 | detect: 0x01 chip A STATUS (computer present), 0x02 chip B INVALID (modem present), 0x04 chip A's always-on receiver on pin 2 |
| 4 | host lines: DTR/RTS as last set over CDC |
ps_stats_t (8 × u32):
- bytes to RS-232;
- bytes from RS-232;
- framing, parity, overrun and break counts;
- bytes dropped (received while the host wasn't reading and the buffer was full);
- role changes.
Also on the CDC interface¶
The CDC serial port sends standard SERIAL_STATE notifications on its interrupt endpoint, so apps whose USB stack reads them need nothing from this protocol for the common lines: - as DCE: DSR = the computer's DTR; DCD = a computer is attached; - as DTE: the modem's DCD, DSR and RI; - both roles: break, framing, parity and overrun events.
Bluetooth (W board)¶
The W board offers the same controls over BLE, in the pico-serial
control service, 50530001-5C6F-4E2B-9A6B-1D2C0E5F7A10. Its
characteristics share that UUID with the second field changed (5053000N).
Every one needs an encrypted (bonded) link. Writes other than line coding
are refused (Write Not Permitted) unless this BLE link owns the RS-232
port.
| UUID | Name | Access | Content |
|---|---|---|---|
50530002 |
Line coding | read, write | baud (u32), data bits, parity (0 none, 1 odd, 2 even), stop bits (1, 2): 7 bytes. Kept for this link and used whenever it owns the port |
50530003 |
Outputs | read, write | write: mask, value (as SET_OUTPUTS); read: override mask, lines driven now |
50530004 |
Inputs | read, notify | ps_inputs_t; notified when it changes |
50530005 |
Mode | read, write | write: the setting (as SET_MODE); read: setting, role now |
50530006 |
Break | write | u16 ms; 0 ends it, 0xFFFF until 0 |
50530007 |
Flow | read, write | as SET_FLOW |
50530008 |
Credits | read, notify | u16: free bytes in the buffer to the DB25. Notified when it moves by 256 or more, and when it empties |
50530009 |
Info | read | ps_info_t |
Data goes through the Nordic UART Service (6E400001-B5A3-F393-E0A9-E50E24DCCA9E):
write to RX 6E400002…, subscribe to TX 6E400003…. A BLE write can't be
held back, so an app sending more than the buffer holds (4 KB) should
watch Credits.
Classic Bluetooth (SPP) carries data only; there are no controls on it.
Line stick (pico-serial Line)¶
The phone-line stick (SPEC-L.md, -DPS_BOARD=line) is a
different device: a softmodem's line interface, for a host driver such as
D-Modem's dmodemd. It has none of the above. Its definitions are in
src/core/line_proto.h; this section is enough to
write the host's driver. It is shaped like Smart Link's SmartUSB56 (the
ST7554, which frontend/dmodemd/usb_line.cpp drives), so that driver is the model.
Finding it¶
- USB
1209:0002(a pid.codes test PID until a real one is assigned), product "pico-serial Line". No kernel driver claims it: open it with libusb. - Windows binds WinUSB to both interfaces by itself (no INF, no
Zadig): the stick is USB 2.1 with a BOS descriptor whose Microsoft OS 2.0
platform capability names vendor request
0x4D. Windows reads the descriptor set with it (bmRequestType0xC0,wIndex7). The set gives each interface the compatible IDWINUSBand aDeviceInterfaceGUID:{0463C853-BB62-4057-9E76-D14B0E3ACAFD}for the line (D-Modem's, as itsdmodem-usb.infgives it), the Pico SDK's{bc7398c1-73cd-4cb7-98b8-913a8fca7bf6}for the reset interface. - Interface 0: class
0xFF, subclass0x4C, protocol0x01, "pico-serial Line". (Interface 1 is Raspberry Pi's reset interface, forpicotool.) - Alternate setting 0 has no endpoints. Alternate setting 1 has:
| Endpoint | Type | Max packet | |
|---|---|---|---|
0x01 OUT |
isochronous, asynchronous, every 1 ms | 40 bytes | the modem's samples to the line |
0x82 IN |
isochronous, asynchronous, every 1 ms | 40 bytes | the line's samples |
0x83 IN |
interrupt, every 4 ms | 8 bytes | lp_event_t when something changed |
Samples¶
- S16, little-endian, mono, at the rate set (9600 Hz after reset: the rate D-Modem's V.90 needs; any of the Si3056's: 7200, 8000, 8229, 8400, 9000, 9600, 10286, 12000, 13714, 16000).
SET_INTERFACEto alternate setting 1 starts both directions; back to 0 stops them. Both FIFOs are emptied on each change. Samples run on hook too, so ring and caller-ID detection can work on them (with the line monitor on,SET_MONITOR).- IN: each packet carries the samples the codec gave since the last one (9 or 10 a millisecond at 9600 Hz; up to 20 if a packet was missed). Its length is the stick's clock: count samples, not milliseconds.
- OUT: send, per millisecond, as many samples as the last IN packet brought (the SmartUSB56 driver does exactly this: each OUT transfer goes with the lengths of an IN transfer just completed). The stick plays them at the codec's clock after a 32-sample head start (about 3 ms); if it runs dry it plays silence, counts an underrun and waits for its head start again. More than its 1024-sample FIFO holds is dropped and counted as an overrun (as are line samples the host isn't taking).
- The line's delay through the stick: the head start, plus the Si3056's filters (12 samples each way).
Requests¶
Vendor control requests; recipient the interface (wIndex = 0) or the
device (as the SmartUSB56's driver sends them): bmRequestType 0xC1
or 0xC0 IN, 0x41 or 0x40 OUT (wLength 0). An unknown request or a
bad value stalls.
| bRequest | Name | Dir | wValue | |
|---|---|---|---|---|
0x01 |
GET_INFO | IN | 0 | lp_info_t, 12 bytes |
0x02 |
GET_STATUS | IN | 0 | lp_status_t, 20 bytes (reads four DAA registers: ~1 ms) |
0x10 |
SET_RATE | OUT | Hz | one of the rates above; set it before starting the samples |
0x11 |
SET_HOOK | OUT | 1 off hook, 0 on hook | the OFHK pin: takes effect at once |
0x12 |
HOOK_FLASH | OUT | ms, 1–2000 | on hook for that long, then off hook again (no-op on hook) |
0x13 |
SET_MONITOR | OUT | 1 on, 0 off | the on-hook line monitor (ONHM), for caller ID |
0x20 |
DAA_READ | IN | register 0–127 | 1 byte: a Si3056/Si3018 register |
0x21 |
DAA_WRITE | OUT | register << 8 \| value |
a register; register 5 keeps OHE set (the hook stays the pin's) and its ONHM bit sets the monitor |
0x30 |
RESET | OUT | 0 | the DAA reset and brought up again, on hook (~150 ms) |
0x31 |
REBOOT_UF2 | OUT | 0 | into the RP2040's UF2 loader, 50 ms after the status stage |
0x4D |
MS_OS_20 | IN | 0 | (device, wIndex 7) the Microsoft OS 2.0 descriptor set, for Windows |
Pulse dialling is the host's: it sends SET_HOOK 0 and 1 for each
break and make, as dmodemd does with the SmartUSB56's hook GPIO. Each
takes effect when the request arrives (well within a millisecond of the
control transfer); there is no device-side pulse timer.
Country settings are the host's too: after bring-up the line side has
the Si3056's reset values (FCC: 600 Ω, the US's DC termination). Write the
country's registers 16, 26, 30, 31 and the gains (Si3056 datasheet Table 15,
"Country Specific Register Settings") with DAA_WRITE.
Unplugging (or the host deconfiguring the stick) puts the line on hook. The watchdog does too if the firmware stalls.
Structures¶
All little-endian, packed.
lp_info_t (12 bytes): version (1), fw_major, fw_minor, fw_patch,
rate (u16, Hz), max_samples (20, per packet), state (below),
reg11 (line-side ID in bits 7:4: 1 = Si3018, 3 = Si3019; system-side
revision in 3:0), reg13 (line-side revision in bits 5:2), 2 reserved.
lp_status_t (20 bytes): reg12 (FDT bit 6: the link across the barrier
works; LCS[4:0], the loop current), reg28 (LCS2), reg29 (LVS, the line
voltage), reg5 (RDTN, RDTP, ONHM, RDT, OHE, OH), int_src (register 4's
bits since the last status), state, underruns (u16), overruns (u16),
samples_in (u32: to the host), samples_out (u32: played),
ring_edges, 1 reserved.
lp_event_t (8 bytes, on 0x83 whenever it changes, and once when the
samples start): state, ring_edges (the ring detector's changes, mod
256: each one is a pulse for the modem's ring cadence check, as the
SmartUSB56's ring GPIO changes are), int_src (register 4 since the last
event: 0x80 ring, 0x40 receive overload, 0x20 the link lost, 0x08
the line dropped out, 0x01 polarity reversal), seq (+1 each event),
underruns, overruns (u16 each).
state bits: 0x01 LINK (FDT), 0x02 OFF_HOOK, 0x04 MONITOR, 0x08
STREAMING, 0x10 RINGING (the ring detector active now), 0x20 FLASH.
Inside the stick¶
The Si3056 is the serial port's master (serial mode 0) on a 48 MHz MCLK
from the RP2040's USB PLL (PLL1 N = 125, M = 256 → 98.304 MHz); the RP2040
is its slave in PIO (src/line/si3056.pio), every 16-bit word through
src/core/daa_frame.c from the PIO's interrupt. Register accesses ride in
secondary frames, asked for by the FC pin (16-bit mode, SB set at
bring-up). The hook is the OFHK pin (OHE set), the ring detector the RGDT
pin, the DAA's interrupt the AOUT/INT pin.
WB stick (pico-serial WB)¶
The wideband bare-pair stick (SPEC-WB.md, -DPS_BOARD=wb)
is the Line stick's sibling for bare copper pairs: a TLV320AIC3204 codec
behind a line transformer, streaming the pair's samples at 96 kHz for a
wideband modem on the host (D-Modem's dmodemd). Its USB device is the Line
stick's (the same interface layout, endpoints, sample framing and
Microsoft OS 2.0 descriptors), with its own requests: no hook, ring or
caller ID. The definitions are in src/core/wb_proto.h.
Finding it¶
- USB
1209:0003(a pid.codes test PID, like the Line stick's1209:0002, until a real one is assigned), product "pico-serial WB".GET_INFO'sboardis 4. No kernel driver claims it: open it with libusb. - Windows binds WinUSB to both interfaces by itself, as for the Line
stick: the same BOS descriptor, vendor request
0x4D(wIndex7), and D-Modem'sDeviceInterfaceGUID{0463C853-BB62-4057-9E76-D14B0E3ACAFD}for the line interface. - Interface 0: class
0xFF, subclass0x57('W'), protocol0x01, "pico-serial WB". (Interface 1 is Raspberry Pi's reset interface.) - Alternate setting 0 has no endpoints. Alternate setting 1 has:
| Endpoint | Type | Max packet | |
|---|---|---|---|
0x01 OUT |
isochronous, asynchronous, every 1 ms | 480 bytes | the modem's samples to the line |
0x82 IN |
isochronous, asynchronous, every 1 ms | 480 bytes | the line's samples |
0x83 IN |
interrupt, every 4 ms | 8 bytes | wp_event_t when something changed |
Samples¶
- S16, little-endian, mono, at the rate set: 96 000 Hz after reset; 48 000 and 192 000 Hz for tests. 192 bytes a millisecond each way at 96 kHz, 384 at 192 kHz; a packet holds up to 240 samples (192 kHz with 25% to catch up).
- IN carries one of the codec's two ADC channels (
SET_INPUT): the hybrid (the left ADC, default: the line minus the replica of our own transmit, so the far end's signal with ours 10–20 dB down) or the line (the right ADC: the pair's voltage itself, ours included, inverted). The other channel's level is inGET_STATUS. One channel keeps the packets at SPEC-WB's sizes; the second is for measuring the hybrid, which the peaks do without streaming it. - OUT: one channel; the stick drives the line differentially from it (the left DAC's two outputs: HPL = s, HPR = −s).
SET_INTERFACEto alternate setting 1 starts both directions, back to 0 stops them; both FIFOs are emptied on each change. As on the Line stick, each IN packet carries the samples the codec gave since the last one, so its length is the stick's clock (the codec's PLL on the stick's 12 MHz crystal): count samples, not milliseconds, and send per millisecond as many OUT samples as the last IN packet brought. The stick plays them after a 3 ms head start; dry, it plays silence, counts an underrun and waits for its head start again. More than its 4096-sample FIFO holds is dropped and counted as an overrun (as are line samples the host isn't taking).- The line's delay through the stick: the head start, a third of a millisecond of DMA blocks each way, and the codec's filters.
Requests¶
Vendor control requests; recipient the interface (wIndex low byte 0)
or the device: bmRequestType 0xC1 or 0xC0 IN, 0x41 or 0x40 OUT
(wLength 0). An unknown request or a bad value stalls.
| bRequest | Name | Dir | wValue | |
|---|---|---|---|---|
0x01 |
GET_INFO | IN | 0 | wp_info_t, 16 bytes |
0x02 |
GET_STATUS | IN | 0 | wp_status_t, 28 bytes: the line's level (peaks) and the codec's flags since the last status |
0x10 |
SET_RATE | OUT | kHz: 48, 96 or 192 | the codec reset and brought up at that rate (about 100 ms), the settings below kept |
0x11 |
SET_CARRIER | OUT | 0 off, 1 on, 2 auto | the LINK LED: the modem's carrier; auto (the default) lights it while the samples stream |
0x12 |
SET_TX_GAIN | OUT | int16, 0.5 dB steps, −127..48 | the DAC's digital volume (codec page 0 register 65), soft-stepped; 0 after reset |
0x13 |
SET_RX_GAIN | OUT | 0..95, 0.5 dB steps | the hybrid MicPGA's gain (page 1 register 59); 0 after reset |
0x14 |
SET_INPUT | OUT | 0 hybrid, 1 line | which ADC channel IN carries |
0x15 |
SET_LOOPBACK | OUT | 0 off, 1 firmware, 2 codec | see below |
0x20 |
CODEC_READ | IN | register 0–127; wIndex = page << 8 |
1 byte: a codec register |
0x21 |
CODEC_WRITE | OUT | register << 8 \| value; wIndex = page << 8 |
a codec register, 1–127 (the page select is the firmware's) |
0x30 |
RESET | OUT | 0 | the codec reset and brought up again, rate and settings kept |
0x31 |
REBOOT_UF2 | OUT | 0 | into the RP2040's UF2 loader, 50 ms after the status stage |
0x4D |
MS_OS_20 | IN | 0 | (device, wIndex 7) the Microsoft OS 2.0 descriptor set, for Windows |
Loopbacks: firmware (1) sends the host's OUT samples straight back
on IN, at the codec's clock, instead of the line's: the USB path and the
host's driver without the line. Codec (2) feeds the codec's ADCs to its
DAC (page 0 register 29, D4): the stick repeats what it hears onto the
pair, a remote loopback for the stick at the other end. The hybrid
residue goes round too: with a poorly matched replica and a high
SET_RX_GAIN it can ring, so keep the gain low for it.
Levels: at 0 dBFS (and SET_TX_GAIN 0) each driver swings 0.5 Vrms,
1.0 Vrms between them; through 2 × 60.4 Ω into a 120 Ω pair (a 1:1
transformer) that is 0.5 Vrms on the line, +3.2 dBm for a full-scale sine,
and about +4.8 dBm where the drivers clip (AVDD 1.77 V). A multicarrier
signal backed off ~11 dB for its peaks averages about −8 dBm. The hybrid
input reaches 0 dBFS at 0.5 Vrms (line minus replica, SET_RX_GAIN 0:
the PGA's +6 dB for a differential pair on 10 kΩ), the line input at
1.0 Vrms (20 kΩ: our own full-scale transmit reads −6 dBFS there).
Structures¶
All little-endian, packed.
wp_info_t (16 bytes): version (1), fw_major, fw_minor, fw_patch,
rate (u32, Hz), max_samples (u16, 240), state, board (4: the WB
stick), input, loopback, tx_gain (i8, 0.5 dB), rx_gain (u8, 0.5 dB).
wp_status_t (28 bytes): peak_hybrid, peak_line, peak_tx (u16 each:
the largest |sample| since the last status, 32768 = full scale, of the left
ADC, the right ADC and the host's samples played), ovf (page 0 register
42 since the last status: 0x80/0x40 left/right DAC overflow,
0x08/0x04 left/right ADC overflow), oc (register 44's over current:
0x80 HPL, 0x40 HPR), adc_flags (register 36 now: 0x40/0x04
left/right ADC up, 0x80/0x08 their PGAs at gain), dac_flags
(register 37 now: 0x80 left DAC up, 0x20 HPL, 0x02 HPR), state,
1 reserved, underruns, overruns (u16), samples_in, samples_out
(u32), resyncs (u16: the I2S slave lost its place and started again),
2 reserved.
wp_event_t (8 bytes, on 0x83 whenever it changes, and once when the
samples start): state, seq (+1 each event), ovf, oc (as in the
status, since the last event: the codec's INT1 on MFP5 brings them),
underruns, overruns (u16 each).
state bits: 0x01 CODEC (it came up and its clocks run), 0x02 CARRIER
(the LINK LED is lit), 0x04 LOOPBACK, 0x08 STREAMING.
Inside the stick¶
The codec is the I2S master on the RP2040's 12 MHz crystal (GPOUT0 on GPIO21, MCLK): its PLL makes 12 × 8.1920 = 98.304 MHz (P = 1, R = 1, J = 8, D = 1920), NDAC = NADC = MDAC = MADC = 4, and per rate:
| Rate | DOSR = AOSR | Filter | DAC block | ADC block | BCLK = DAC_CLK / N |
|---|---|---|---|---|---|
| 48 kHz | 128 | A | PRB_P1 | PRB_R1 | N = 8: 3.072 MHz |
| 96 kHz | 64 | B | PRB_P7 | PRB_R7 | N = 4: 6.144 MHz |
| 192 kHz | 32 | C | PRB_P17 | PRB_R13 | N = 2: 12.288 MHz |
Every rate keeps DOSR × fs = 6.144 MHz and the modulators at 6.144 MHz
(SLAA557's limits: 2.8–6.2 MHz, 6.758 MHz), MDAC × OSR / 32 at or above
each block's resource class, and 64 BCLKs a frame (I2S, 32-bit words). The
RP2040 is the slave in PIO (src/wb/i2s.pio: the codec's bit in as BCLK
rises, ours out just after, a whole BCLK of setup), its words moved by
DMA in blocks of 32 frames and handed to src/core/wb_audio.c from the
DMA's interrupt. If WCLK isn't where 32 bits each side put it, the PIO
stops and the firmware starts it over (resyncs). The bring-up and the
analog setup (A-LDO 1.77 V, common mode 0.9 V, drivers from AVDD, the
hybrid in the MicPGAs) are in src/core/wb_codec.c; registers over I2C1
(address 0x18).