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)
256 lines
11 KiB
Markdown
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.
|