Bit-synchronized multichannel I2S audio cluster for active speaker systems
A scalable multichannel audio platform built on Luckfox Pico MAX (Rockchip RV1106) single-board computers. Multiple nodes share I2S clocks via physical wiring, guaranteeing bit-perfect synchronization across all channels — critical for multi-way active crossover systems.
- Bit-synchronized output — all nodes share BCK/LRCLK/MCLK via hardwire, ensuring zero sample drift between channels
- Scalable — up to 16 slave nodes (128 channels), limited only by HQPlayer's channel count
- 8 channels per node — 4x stereo I2S data lines (SDO0–SDO3)
- PCM up to 32bit/192kHz guaranteed (8ch per node)
- PCM up to 32bit/384kHz in 4ch mode (per node, not fully tested)
- DSD up to DSD256 native playback with automatic PCM/DSD switching
- Flexible clocking — external oscillators (EXT mode) or built-in RV1106 PLL (PLL mode). The on-chip PLL delivers surprisingly high audio quality — some audiophile experts prefer it over expensive external audio clocks
- Dual clock domain — 44.1kHz and 48kHz families with automatic GPIO-based oscillator switching
- Bit-perfect — no resampling, no mixing, direct DMA-to-I2S path
- Network Audio — HQPlayer NAA protocol over Ethernet
In a multi-way active speaker system, each frequency band (tweeter, midrange, woofer, subwoofer) is driven by a separate DAC channel. Any timing offset between channels causes:
- Phase errors at crossover frequencies
- Comb filtering artifacts
- Degraded stereo imaging
This cluster guarantees zero inter-channel timing error because all nodes share the same physical I2S clock signals. The DACs on every node see identical BCK/LRCLK edges simultaneously.
┌──────────────────────────────────────────────────┐
│ HQPlayer (PC/Mac) │
│ Multichannel output: 2/4/6/8ch per NAA endpoint │
└────────┬───────────────────────┬─────────────────┘
│ TCP/IP │ TCP/IP
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Node 0 (MASTER) │ │ Node 1 (SLAVE) │ ...up to 16 nodes
│ Luckfox Pico │ │ Luckfox Pico │
│ MAX │ │ MAX │
│ │ │ │
│ NAA ─► ALSA │ │ NAA ─► ALSA │
│ │ │ │ │ │
│ I2S DMA │ │ I2S DMA │
│ ┌──────────┐ │ │ ┌──────────┐ │
│ │SDO0..SDO3│ │ │ │SDO0..SDO3│ │
│ │BCK LRCLK │───┼─────┼──►│BCK LRCLK │ │
│ │MCLK │───┼─────┼──►│MCLK │ │
│ └──────────┘ │ │ └──────────┘ │
└──────────────────┘ └──────────────────┘
│ │
8ch to DAC(s) 8ch to DAC(s)
| Signal | GPIO | Pin | Direction (Master) | Direction (Slave) |
|---|---|---|---|---|
| BCK (Bit Clock) | GPIO2_A0 | 24 | Output | Input |
| LRCLK (Frame Clock) | GPIO2_A1 | 25 | Output | Input |
| MCLK (Master Clock) | GPIO2_A2 | 26 | Output | Input |
| SDO0 (Data ch0-1) | GPIO2_A4 | 21 | Output | Output |
| SDO1 (Data ch2-3) | GPIO2_A7 | 34 | Output | Output |
| SDO2 (Data ch4-5) | GPIO2_A6 | 29 | Output | Output |
| SDO3 (Data ch6-7) | GPIO2_A3 | 27 | Output | Output |
| Signal | GPIO | Pin | Function |
|---|---|---|---|
| MUTE | GPIO1_D3 | 10 | Mute output (active high) |
| MUTE_INV | GPIO3_C6 | 22 | Inverted mute (active low) |
| DSD_ON | GPIO1_D0 | 19 | DSD mode enable |
| MCLK_SEL | GPIO1_D1 | 20 | Clock domain select (44.1/48kHz) |
| FREQ_INV | GPIO1_C5 | 6 | Inverted clock domain select |
| HI_RES | GPIO1_C6 | 5 | Hi-Res indicator (active at 352.8/384 kHz) |
| HW_SYNC | GPIO1_C7 | 4 | Start sync wire: master OUT → slave IN |
Connect master → all slaves with short wires (< 5cm recommended):
Master Slave(s)
────── ────────
BCK (GPIO2_A0, pin 24) ──────► BCK (GPIO2_A0, pin 24)
LRCLK (GPIO2_A1, pin 25) ──────► LRCLK (GPIO2_A1, pin 25)
MCLK (GPIO2_A2, pin 26) ──────► MCLK (GPIO2_A2, pin 26)
HW_SYNC (GPIO1_C7, pin 4) ──────► HW_SYNC (GPIO1_C7, pin 4)
GND ─────────────────────────── GND
Important:
- All three clock lines (BCK, LRCLK, MCLK) must be connected
- HW_SYNC wire triggers bit-exact simultaneous start across all nodes — connect master pin 4 to all slaves pin 4
- Common GND is mandatory
- Keep wires short and equal length to minimize skew
- Data lines (SDO0–SDO3) go to each node's own DAC — they are NOT shared between nodes
- MCLK_SEL and DSD_ON may optionally be shared if all nodes use the same DAC configuration
In EXT mode, the master node receives MCLK from an external oscillator or DAC clock output. Two oscillators are used:
| Oscillator | Frequency (512fs) | Frequency (1024fs) | Clock Domain |
|---|---|---|---|
| OSC_49M | 24.576 MHz | 49.152 MHz | 48kHz family |
| OSC_45M | 22.5792 MHz | 45.1584 MHz | 44.1kHz family |
GPIO1_D1 (MCLK_SEL) selects between them via a gpio-mux-clock. The driver switches automatically based on the sample rate.
- HQPlayer Desktop or Embedded
- Ethernet network connecting all nodes and HQPlayer host
- Luckfox Pico MAX boards flashed with this firmware
HQPlayer host OS: Linux is strongly recommended. Windows versions of HQPlayer produce bursty network traffic that can cause DMA underruns on the RV1106's single-core CPU, especially at sample rates above 192 kHz.
build.shThe firmware image is produced in buildroot/output/images/.
Connect to any node via SSH:
ssh root@<node-ip>Default password: multisync
Each node must be configured as either master or slave using the configuration script:
/opt/configure.sh <clock_source> <mclk_multiplier> <role>| Parameter | Values | Description |
|---|---|---|
| clock_source | ext / pll |
ext = external oscillator, pll = internal PLL |
| mclk_multiplier | 512 / 1024 |
MCLK-to-sample-rate ratio |
| role | master / slave |
Clock master or slave |
2-node stereo bi-amping (external 512fs clock):
# Node 0 — master (tweeters + midrange)
/opt/configure.sh ext 512 master
# Node 1 — slave (woofers + subwoofers)
/opt/configure.sh ext 512 slave4-node system with 1024fs clock:
# Node 0 — master
/opt/configure.sh ext 1024 master
# Nodes 1, 2, 3 — slaves
/opt/configure.sh ext 1024 slave2-node system with internal PLL (no external oscillators needed):
# Node 0 — master (PLL generates MCLK internally)
/opt/configure.sh pll 1024 master
# Node 1 — slave (receives MCLK/BCK/LRCLK from master via wires)
/opt/configure.sh pll 1024 slaveSingle standalone node (no slaves):
/opt/configure.sh pll 1024 masterNote: In PLL mode, the master generates MCLK from its internal PLL. Slave nodes still work — they receive all clocks (MCLK, BCK, LRCLK) from the master via the same physical wiring as in EXT mode. The only difference is the clock source on the master side.
The script:
- Writes clock mode and MCLK multiplier to
/etc/i2s.conf - Sets the hostname (
masterorslave) - Links the appropriate networkaudiod startup script
- Configures Chrony time synchronization (master serves NTP, slave syncs from master)
- Flashes the correct Device Tree Blob to NAND
- Reboots the node
Each node appears as a separate NAA endpoint on the network. Configure HQPlayer's multichannel output:
- Backend: Combo
- Device: Select each node's NAA endpoint
- Channels per endpoint: 2, 4, 6, or 8 (depending on your speaker configuration)
- Buffer time: 1 ms
- Clock mode: time
| HQPlayer Setting | Value | Notes |
|---|---|---|
| Backend | Combo | Combines multiple NAA endpoints |
| Channels | 2 / 4 / 6 / 8 | Per endpoint |
| Buffer | 1 ms | Critical — see note below |
| Clock | time | Network-based synchronization |
Why 1 ms buffer? In Combo/Network mode HQPlayer sends both nodes an absolute timestamp "start playing at time T". A large buffer (e.g. 100 ms) means T is set 100 ms in the future — during that window any clock drift between nodes accumulates and causes a desynchronized start. With 1 ms, the window is nearly zero: nodes either start together or HQPlayer immediately detects a miss and retries. Once playback begins, hardware I2S synchronization (shared BCK/LRCLK wires) takes over and is cycle-accurate regardless of buffer size.
Combo backend maps consecutive channel pairs across NAA endpoints. For example, with 3 nodes at 8ch each:
| Channels | Node 0 | Node 1 | Node 2 |
|---|---|---|---|
| 1–8 | SDO0–SDO3 | — | — |
| 9–16 | — | SDO0–SDO3 | — |
| 17–24 | — | — | SDO0–SDO3 |
| Mode | Channels/Node | Sample Rate | Bitrate/Node | Status |
|---|---|---|---|---|
| PCM 8ch | 8 | up to 192 kHz | 49 Mbps | Tested, stable |
| PCM 4ch | 4 | up to 384 kHz | 49 Mbps | Tested, stable |
| PCM 6ch | 6 | 384 kHz | 74 Mbps | Tested, stable (Linux HQPlayer required) |
| DSD 4ch | 4 | DSD64–DSD256 | — | Not tested (driver supports it) |
Network bandwidth limit: Each node uses 100 Mbit Ethernet. Stable multi-node sync requires ~50% headroom for TCP overhead and retransmissions. Configurations exceeding ~50 Mbps per node may gradually desync. Single-node operation is not affected (8ch/384kHz works fine standalone).
The I2S output pin drive strength directly affects MCLK signal quality. Higher drive strength causes switching noise that couples back to the MCLK input on the RV1106, creating visible jitter on an oscilloscope.
Use the drv_strength.sh script to adjust drive strength at runtime without reflashing:
/opt/drv_strength.sh show # Display current levels
/opt/drv_strength.sh 0 # Minimum drive (cleanest MCLK)
/opt/drv_strength.sh 7 # Maximum drive (strongest output)| Level | MCLK Quality | Signal Strength | Recommended Use |
|---|---|---|---|
| 0 | Best | Weakest | Short wires (< 5cm) |
| 2–3 | Good | Moderate | Typical setup |
| 7 | Worst (jitter) | Strongest | Default (not recommended for EXT mode) |
The script controls BCK, LRCLK, and SDO0–SDO3 output pins. MCLK (input) and SDI0 (input) are not affected. The selected level is saved to /etc/i2s_drv_level and automatically applied at boot.
All controls are accessible at /sys/devices/platform/ffae0000.i2s/:
| Attribute | Access | Description |
|---|---|---|
mute |
R/W | Hardware mute (0/1) |
mclk_multiplier |
R/W | 512 or 1024 |
pcm_channel_swap |
R/W | Swap L/R channels in PCM mode |
dsd_physical_swap |
R/W | Swap DSD data pins |
dsd_sample_swap |
R/W | DSD sample swap (noise fix) |
freq_domain_invert |
R/W | Invert frequency domain GPIO |
pinctrl_state |
R/W | Switch pins: default / idle |
tx_reset |
W | Trigger I2S TX reset |
postmute_delay_ms |
R/W | Mute hold time after start (ms) |
frame_counter |
R | Monotonic frame counter |
drift_ppb |
R/W | Clock drift correction (ppb) |
pointer_offset |
R/W | DMA pointer offset (frames) |
Nodes use chrony for precise time synchronization:
- Master syncs to internet NTP servers with very slow, jitter-free slewing (
maxslewrate 10 ppm, polling every 4–68 minutes) - Slave tracks master over local network with tight polling (every 4–32 seconds)
- Before each playback session, the slave forces a precise time step (
chronyc makestep) to snap to master's clock - During playback, only smooth frequency adjustments — no time jumps
ext_tree/
├── board/luckfox/
│ ├── dts_max/ # Device Tree sources
│ │ ├── rv1106.dtsi # Base SoC definition
│ │ ├── rv1106-pinctrl.dtsi # Pin multiplexing
│ │ ├── rockchip-pinconf.dtsi # Pin electrical config
│ │ ├── rv1106_512_ext-ipc.dtsi # 512fs master config
│ │ ├── rv1106_512_ext_full-ipc.dtsi # 512fs slave config
│ │ ├── rv1106_ext-ipc.dtsi # 1024fs master config
│ │ ├── rv1106_ext_full-ipc.dtsi# 1024fs slave config
│ │ ├── rv1106_pll-ipc.dtsi # PLL mode config
│ │ └── *.dts # Top-level device trees
│ ├── rootfs_overlay/
│ │ └── etc/
│ │ ├── init.d/ # Boot scripts
│ │ ├── networkaudiod/ # NAA start/stop hooks
│ │ ├── chrony.conf.master # NTP server config
│ │ └── chrony.conf.slave # NTP client config
│ └── scripts/
│ └── post-build.sh # DTB packaging
├── configs/
│ └── luckfox_pico_max_defconfig # Buildroot config
└── patches/
└── linux_rv1106.patch # Kernel driver modifications
- HQPlayer sends multichannel audio over TCP/IP to NAA daemons on each node
- networkaudiod (NAA) receives audio and writes to the ALSA device via
snd_pcm_writei() - ALSA/DMA transfers audio data to the I2S peripheral's TX FIFO
- I2S hardware serializes the data to SDO0–SDO3, clocked by shared BCK/LRCLK
- Since all nodes share the same physical clock signals, their data outputs are bit-synchronized
The I2S driver automatically handles:
- Muting during format changes (PCM ↔ DSD)
- Clock domain switching (44.1kHz ↔ 48kHz)
- DSD mode GPIO signaling
- Master start delay for slave synchronization
- Pin isolation in idle state
This project is licensed under the GNU General Public License v2.0 — see the LICENSE file for details.
The kernel driver modifications are derived from the Rockchip I2S/TDM driver and are distributed under GPL-2.0 as required by the Linux kernel licensing terms.