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)
This commit is contained in:
255
audio-architecture.md
Normal file
255
audio-architecture.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user