Files
mestre/audio-architecture.md
Joakim 2526d5b087 feat: M3 complete — WiFi, BT keyboard and mouse
Bluetooth (RTL8723D USB combo chip):
- GPIO pin 357 (active low) enables WiFi/BT hardware via x6200_gpio_set()
- bluez5_utils 5.64 (downgraded from 5.79 — HID input plugin broken in 5.79)
- rtl8723d_config.bin added to overlay (missing from linux-firmware package)
- S45wifi-bt: GPIO enable + modprobe btusb/uhid/hidp at boot
- S85bt-keyboard: auto-connect loop with scan+connect every 20s

WiFi (RTL8723DU):
- out-of-tree lwfinger/rtw88 driver (RTW88_8723DU not in kernel 6.1 mainline)
- linux-firmware RTL_RTW88 for rtw88/rtw8723d_fw.bin
- regulatory.db for cfg80211
- wpa_supplicant with multi-network config in /etc/wpa_supplicant.conf
- S46wifi: wpa_supplicant + udhcpc at boot

Key findings:
- RTL8723D USB WiFi (0bda:d723) requires out-of-tree rtw88 on kernel 6.1
- BT and WiFi share same USB device, both need GPIO 357 = 0 to power on
- bluez5 5.79 HID input plugin not linked into bluetoothd (build system bug)
2026-05-09 21:02:53 +02:00

256 lines
11 KiB
Markdown

# X6200 Audio Architecture
A detailed technical account of how audio works on the Xiegu X6200,
reverse-engineered during Mestre development (May 2026).
## Overview
The X6200 has two distinct audio sources and one audio output path.
Understanding the distinction between them is essential for any software
that wants to produce sound on this radio.
```
┌─────────────────────────────────────────────────────────┐
│ X6200 Hardware │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ RF Front │ │ Allwinner│ │
│ │ End │ │ R16 │ (SoC / Application CPU) │
│ │ + FPGA │ │ │ │
│ └────┬─────┘ └────┬─────┘ │
│ │ RX audio │ DAC audio │
│ │ (analog) │ (digital → analog via codec) │
│ └──────┬────────┘ │
│ │ │
│ ┌────▼─────┐ │
│ │ Audio │ ← voice_rec bit selects source │
│ │ MUX │ (controlled via I2C to MCU) │
│ └────┬─────┘ │
│ │ │
│ ┌────▼─────┐ │
│ │ PA │ ← Power Amplifier │
│ │ (Class D)│ enabled by rxvol_set > 0 │
│ └────┬─────┘ │
│ │ │
│ ┌────▼─────┐ │
│ │ Speaker │ │
│ └──────────┘ │
└─────────────────────────────────────────────────────────┘
```
## The Two Audio Sources
### Source 1: RX-Radio Audio (default)
The RF front-end and FPGA process the received RF signal and produce
an analog audio output — the classic "radio receiver sound". This is
the default audio source at power-on. It is completely independent of
the SoC and ALSA; it bypasses the codec entirely and is fed directly
into the audio MUX.
This is why you hear receiver noise immediately when the X6200 boots,
even before Linux has fully started.
### Source 2: SoC DAC Audio
The Allwinner R16 SoC contains an integrated audio codec
(`sun8i-a33-audio`) which can produce digital audio, converted to
analog via the DAC. This is the source used by software running on
the SoC — piHPSDR, aplay, PulseAudio, etc.
This source goes through the standard Linux ALSA stack:
```
Application (piHPSDR, aplay)
→ PulseAudio (mixing, resampling)
→ ALSA (hw:0,0 / pcmC0D0p)
→ sun8i-a33-audio codec driver
→ DAC (analog output)
→ Audio MUX
→ PA
→ Speaker
```
## The Audio MUX — The Critical Control Point
A hardware multiplexer inside the MCU/FPGA selects which source
reaches the power amplifier and speaker. This MUX is controlled
via I2C, **not** via ALSA or any Linux kernel interface.
### I2C Protocol
The X6200 MCU sits on I2C bus 0, address `0x72`. All hardware control
uses the X6200Control library protocol: a 6-byte write transaction
consisting of a 2-byte register address followed by a 4-byte value.
The audio MUX is controlled by the `voice_rec` bit in the
`sple_atue_trx` register (command index 12, address `0x0030`):
| voice_rec bit | Audio source heard in speaker |
|---------------|-------------------------------|
| 0 (default) | RX-radio (RF receiver) |
| 1 | SoC DAC (software audio) |
**To switch to SoC DAC audio:**
```bash
i2ctransfer -y 0 w6@0x72 0x00 0x30 0x08 0x00 0x00 0x00
```
**To switch back to RX-radio audio:**
```bash
i2ctransfer -y 0 w6@0x72 0x00 0x30 0x00 0x00 0x00 0x00
```
## The Power Amplifier — The Other Critical Control Point
The speaker PA (power amplifier) is not always on. It is enabled
as a side effect of setting the speaker volume via `x6200_control_rxvol_set()`.
When `rxvol = 0` (the default after `x6200_control_init()`), the PA
is effectively off and nothing is heard regardless of the MUX position
or ALSA configuration.
This is why setting the correct mixer levels alone is not sufficient —
the MCU must be told to activate the PA via the I2C control protocol.
## The ALSA Codec — Clock Dependency
The `sun8i-a33-audio` codec requires its master clock (`pll-audio`,
22.5792 MHz) to be running for DAC output to work. The kernel's
clock framework only enables this clock when a consumer holds an
active reference — i.e., when an application has the PCM device open.
**This is the most subtle requirement:** if no process holds
`/dev/snd/pcmC0D0p` open, the audio clocks are disabled and the
codec produces no output, even if the ALSA mixer is correctly
configured and voice_rec is set to DAC mode.
PulseAudio, running in system mode, holds the PCM device open
continuously. This is what keeps the clocks running at all times
and makes reliable audio possible.
```
PulseAudio running → pcmC0D0p open → pll-audio enabled → codec active
```
Without PulseAudio (or equivalent), every `aplay` invocation would
need to wait for the codec to stabilise after the clock starts —
and the X6200 MCU may reset the MUX during this window.
## The ALSA Mixer — Required Configuration
The codec's internal mixer must be configured correctly for audio
to flow from the DAC to the analog output. The critical controls are:
| numid | Name | Required value |
|-------|-----------------------------------|----------------|
| 2 | AIF1 DA0 Playback Volume | 160, 160 |
| 7 | Headphone Playback Volume | 58 |
| 8 | Headphone Playback Switch | on, on |
| 20 | AIF1 Data Digital ADC Cap. Switch | on, on |
| 26 | AIF1 DA0 Stereo Playback Route | 3, 3 (Mix Mono)|
| 28 | AIF1 Slot 0 Digital DAC PB Switch | on, on |
| 31 | DAC Playback Switch | off, off |
Note that `DAC Playback Switch` (numid=31) must be **off** — this is
counterintuitive but correct. The signal path goes through AIF1,
not the direct DAC path.
These values are saved in `/var/lib/alsa/asound.state` and restored
at boot via `alsactl restore` (init script `S50alsa`).
## Complete Initialisation Sequence
The following sequence must be performed in this exact order at boot:
### Step 1: ALSA mixer state (S50alsa)
```bash
alsactl restore
```
Restores the correct mixer levels from `/var/lib/alsa/asound.state`.
### Step 2: PulseAudio (S50pulseaudio)
```bash
pulseaudio --system --daemonize --disallow-exit --exit-idle-time=-1
```
Starts PulseAudio in system mode. This opens `pcmC0D0p` and keeps
the ALSA clocks running continuously.
### Step 3: X6200 hardware init (S70mestre-audio)
```python
lib.x6200_control_init() # Establishes I2C state, sends all_cmd
lib.x6200_control_spmode_set(False) # Speaker mode (not headphone)
lib.x6200_control_rxvol_set(50) # Enables PA, sets volume (0-100)
```
`x6200_control_init()` sends a full command packet to the MCU via I2C,
establishing all register values. **It sets rxvol=0** (PA off) as
its default. `rxvol_set(50)` must follow immediately to enable the PA.
### Step 4: Switch audio MUX to SoC DAC (S70mestre-audio)
```bash
i2ctransfer -y 0 w6@0x72 0x00 0x30 0x08 0x00 0x00 0x00
```
Sets the `voice_rec` bit, switching the speaker from RX-radio audio
to SoC DAC audio. From this point, any audio played via PulseAudio
or directly to `hw:0,0` (44100 Hz, stereo, S16_LE) will be heard
in the speaker.
## Mestre's Implementation
Mestre's init scripts implement this sequence:
```
S50alsa → alsactl restore
S50pulseaudio → pulseaudio --system
S70mestre-audio → x6200_control_init + rxvol_set + voice_rec switch
S96keepalive → I2C PMU keepalive (prevents automatic power-off)
```
The audio MUX switch command (`voice_rec=on`) is permanent until the
next `x6200_control_init()` call — it does not need to be repeated.
However, the PMU keepalive (separate from audio) must run continuously
or the radio will power off after approximately 180 seconds.
## Volume Control
Speaker volume is controlled via `x6200_control_rxvol_set(vol)` where
`vol` is 0-100. This sends an I2C command to the MCU which adjusts
an analog gain stage. **It is not reflected in the ALSA mixer** and
cannot be read or set via `amixer`.
The ALSA mixer `Headphone Playback Volume` (numid=7) and
`AIF1 DA0 Playback Volume` (numid=2) affect the digital gain
in the codec but are set to fixed values at boot and not used
for runtime volume control in Mestre.
## Known Quirks
**rxvol_set defaults to 0 after init.** Every call to
`x6200_control_init()` resets the speaker volume to 0 (PA off).
Always call `rxvol_set(N)` immediately after `init()`.
**voice_rec is reset by x6200_control_init().** If `init()` is called
again (e.g., after a crash), the MUX reverts to RX-radio mode and
the full init sequence must be repeated.
**PulseAudio must start before mestre-audio.** If PulseAudio is not
running when the audio MUX is switched, the codec clocks may not be
active and the switch may have no audible effect.
**44100 Hz stereo S16_LE** is the correct format for direct hardware
playback (`hw:0,0`). PulseAudio handles resampling from other rates.
## References
- [gdyuldin/X6200Control](https://github.com/gdyuldin/X6200Control) —
I2C control library source, including the `sple_atue_trx` register
layout and `voice_rec` bit definition.
- [gdyuldin/x6200_gui](https://github.com/gdyuldin/x6200_gui) —
Reference implementation of the full audio init sequence
(`audio_play_en()`, `audio_init()`, `radio_init()`).
- [AetherRadio/X6100Control](https://github.com/AetherRadio/X6100Control) —
X6100 control library (protocol-compatible with X6200), used to
understand the register map before X6200Control source was available.
- Allwinner R16 / sun8i-a33 audio driver: `sound/soc/sunxi/` in the
Linux kernel tree. The codec is `sun8i-codec` with an AIF1 I2S
interface to the SoC's audio PLL.