A 5-week journey through ACPI, HDA, I2C, I2S, and an undocumented chip to get stereo sound working on a laptop where Linux only played through one speaker.
Update (April 12, 2026): the daemon described below had a subtle flaw I only caught after shipping — every sound on the left speaker was missing its first ~100–200 ms. The full story of finding and fixing it is now in Part II: The day I got tired of guessing.
The Chuwi CoreBook X is an AMD Ryzen 5 7430U laptop with a peculiar audio defect on Linux: only the right speaker works. The left speaker is completely silent. It works on Windows — but only with Chuwi's OEM driver. A clean Windows install also fails.
This affects every Linux user of this laptop. Reports exist on Chuwi forums, kernel bugzilla, and the SOF (Sound Open Firmware) tracker.
The first step was understanding the laptop's audio architecture. Dumping the HDA codec info:
$ cat /proc/asound/card1/codec#0
Codec: Conexant SN6180
Vendor Id: 0x14f120d1
Subsystem Id: 0x27821221
The Conexant SN6180 is the main HDA codec. It has an integrated Class-D amplifier that drives the right speaker through pin 0x17. This works perfectly on Linux.
But the left speaker isn't connected to the SN6180. So what is it connected to?
Searching through the ACPI tables (the firmware description of the hardware), we found a mysterious device:
Scope (_SB.I2CB)
{
Device (CHIP)
{
Name (_HID, "AWDZ8298")
// I2C addresses: 0x34 and 0x35 at 400kHz
// GPIO: pin 121 (reset)
}
}
AWDZ8298 — an AWINIC device on AMD's I2C bus. After research, we identified it: an AWINIC AW88298, a Class-D Smart PA amplifier. It's a chip specialized in amplifying audio for small speakers, with thermal protection, gain control, and an integrated boost converter.
The chip is alive and responds:
$ sudo i2cget -y 0 0x34 0x00 w
0x5218 # Byte-swap → 0x1852 = AW88298 PID ✓
The AW88298 uses 16-bit big-endian registers, but i2cget returns bytes in little-endian order (SMBus protocol). Every read needs mental byte-swapping: 0x5218 is really 0x1852.
Reading the status register:
$ sudo i2cget -y 0 0x34 0x01 w
0x2000 # → BE 0x0020 → bit5 NOCLKS = 1
NOCLKS — "No clock". The chip is powered on but receives no audio signal. It needs an I2S (Inter-IC Sound) clock from somewhere, and nobody is providing it.
Our first hypothesis was that the AMD Audio Co-Processor (ACP) should provide I2S to the AW88298. The ACP is an independent audio processor inside the AMD SoC that can handle I2S and PDM interfaces.
We filed issues on the SOF and kernel trackers. The response from AMD (Vijendar Mukunda, audio team engineer) was clear:
"There is no role of ACP IP here. It's purely HDA stack use case where I2S based amplifiers are connected to HDA codec."
AMD said the ACP wasn't involved and that this was a "pure HDA stack case", similar to how Cirrus Logic connects CS35L41 amplifiers to Realtek codecs.
The second lead was the snd-hda-scodec-cs35l41 driver, which solves an analogous problem: external amplifiers connected to Realtek HDA codecs. The kernel architecture for this is elegant:
But there's a fundamental difference: Realtek codecs have documented dedicated I2S output pins. The Conexant SN6180 has nothing like that in its public documentation. This made us doubt AMD's answer.
We read the ACP_I2S_PIN_CONFIG register directly from the ACP's memory-mapped registers:
pin_config = read_mmio(0xFCD81400)
# Result: 0 → I2S disabled in ACP
PIN_CONFIG = 0 — I2S disabled in the ACP. The ACP was completely off. AMD was right: the ACP doesn't participate.
But then... where does the I2S come from?
We wrote a kernel module (aw88298_test.ko) that does exactly three things:
We loaded it while playing audio through the right speaker:
$ speaker-test -D hw:1,0 -c 2 -l 0 &
$ sudo insmod aw88298_test.ko
And in dmesg:
aw88298-test: Chip ID verified: 0x1852 (AW88298)
aw88298-test: [INITIAL] PLL=unlocked CLKS=no NOCLKS=no
aw88298-test: PLL locked and I2S clock present!
aw88298-test: *** TEST RESULT: I2S clock PRESENT ***
The PLL locked. There IS an I2S clock. It comes from somewhere — and it's not the ACP.
To confirm it was a real clock and not electrical noise, we changed the AW88298's I2S configuration:
# Change from 32-bit/64fs to 16-bit/32fs
$ sudo i2cset -y 0 0x34 0x06 0x0814 w
# Result: PLL loses lock → SYSST = 0x0000
Changing the format broke the PLL. Restoring the original format (32-bit, 64fs, 48kHz) locked it again. The clock is real and has specific parameters.
Despite the PLL being locked, the amplifier's current sense registers read zero:
$ sudo i2cget -y 0 0x34 0x15 w # ISNDAT (current)
0x0000
$ sudo i2cget -y 0 0x34 0x16 w # VSNDAT (voltage)
0x0000
Clock present, but no data. The I2S bus has BCLK and WS (Word Select), but the SDATA line is silent.
We went back to the HDA codec dump and looked at it with fresh eyes:
Node 0x10 [Audio Output] — DAC for headphones
Node 0x11 [Audio Output] — DAC for right speaker
Node 0x22 [Audio Output] — Extra DAC, stream=0, UNASSIGNED
Node 0x23 [Audio Output] — Extra DAC, pin 0x26 (not connected)
Node 0x1d [Pin Complex] — Configured as [N/A], DISABLED
Connection: 1
0x22 ← Connected to DAC 0x22!
Four DACs. The SN6180 has four digital-to-analog converters, not two. And pin 0x1d — which was disabled with [N/A] configuration — is connected to DAC 0x22.
This is not normal for a simple HDA codec. Two extra DACs and two extra pins suggest the chip was designed for exactly this scenario: an I2S output to an external amplifier.
# 1. Assign the active audio stream to DAC 0x22
$ sudo hda-verb /dev/snd/hwC1D0 0x22 0x706 0x50
# 2. Set the stream format
$ sudo hda-verb /dev/snd/hwC1D0 0x22 0x200 0x11
# 3. Enable pin 0x1d as output
$ sudo hda-verb /dev/snd/hwC1D0 0x1d 0x707 0x40
# 4. Enable EAPD on pin 0x1d
$ sudo hda-verb /dev/snd/hwC1D0 0x1d 0x70c 0x02
"It's playing really loud!"
The left speaker came alive. After 5 weeks of investigation, 4 open issues, communication with AMD engineers, ACPI register analysis, HDA codec dumps, kernel driver reading, and hundreds of I2C commands... the left speaker was playing.
The first sound was mono — both speakers played both channels. The trick for real stereo was elegant:
In HDA, each DAC has a channel parameter within the stream. If a stereo stream has channel 0 (left) and channel 1 (right), we can tell each DAC to take only one:
# DAC 0x11 (right speaker): take channel 1 only
$ sudo hda-verb /dev/snd/hwC1D0 0x11 0x706 0x51 # stream 5, channel 1
# DAC 0x22 (left speaker): take channel 0 only
$ sudo hda-verb /dev/snd/hwC1D0 0x22 0x706 0x50 # stream 5, channel 0
The AW88298 also has its own channel selector (I2SCTRL register, CHSEL=01 = Left), so it only reproduces the left channel from I2S. Perfect stereo.
With YouTube music, the left speaker was choppy on peaks. The dropouts were synchronized with the music — when there was a bass hit or drum kick, the sound would briefly cut out.
Reading the AW88298's interrupt register:
$ sudo i2cget -y 0 0x34 0x02 w
0x9543 # → BE 0x4395
# CLIPIS = 1 — Clipping detected!
# UVLIS = 1 — Under-voltage!
The amplifier was clipping on signal peaks and the boost converter couldn't maintain voltage. The root cause: HAGCE = 0 — the Hardware Automatic Gain Control was disabled.
HAGC is an automatic gain control system that dynamically reduces volume when the signal approaches the amplifier's limits. Without it, peaks simply get clipped.
# Enable HAGC and increase boost current limit
$ sudo i2cset -y 0 0x34 0x05 0x6B00 w
# HAGCE=1, BST_IPEAK=11 (4.25A), HMUTE=0
The choppy audio disappeared completely.
Two completely different audio paths:
The AW88298 is controlled via I2C (separate bus from audio). Audio travels via I2S from the HDA codec.
| Action | Verb | Param | Description |
|---|---|---|---|
| Stream → DAC 0x22 | 0x706 | <stream_id>0 | Assign stream, channel 0 (left) |
| Stream → DAC 0x11 | 0x706 | <stream_id>1 | Assign stream, channel 1 (right) |
| Format DAC 0x22 | 0x200 | <format> | Copy format from active stream |
| Pin 0x1d output | 0x707 | 0x40 | Enable pin as output |
| Pin 0x1d EAPD | 0x70c | 0x02 | Enable output amplifier on pin |
| Register | Address | Value | Description |
|---|---|---|---|
| SYSCTRL | 0x04 | 0x3040 | SPK_GAIN=AV14, I2SEN=1, PWDN=0, AMPPD=0 |
| SYSCTRL2 | 0x05 | 0x006B | HAGCE=1, BST_IPEAK=11 (4.25A), HMUTE=0 |
| I2SCTRL | 0x06 | 0x14E8 | Philips I2S, 32-bit, 64fs, 48kHz, Left ch. |
Note on endianness: the AW88298 uses big-endian. With i2cset/i2cget (SMBus, little-endian), bytes are reversed. The value 0x3040 is written as i2cset -y 0 0x34 0x04 0x4030 w.
The definitive fix is ~460 lines of new kernel code:
We add component binding support to the Conexant driver (which never had it — this is a first). The fixup for the Chuwi CoreBook X (subsystem ID 0x2782:0x1221):
AWDZ8298An I2C driver that:
AWDZ8298component_add)1. AMD was right — but their answer was so terse we almost dismissed it. "Pure HDA stack case" is technically correct, but explains nothing. It took us weeks to prove it.
2. Public documentation lies by omission — the SN6180 doesn't publicly document its I2S capability. But the hardware is there: 4 DACs, I2S pins, all wired on the PCB.
3. The test module was key — without it, we'd still be debating whether I2S comes from ACP or the codec. 30 lines of C code resolved the question in 3 seconds.
4. HAGC is mandatory — without hardware automatic gain control, the amplifier clips on signal peaks. This isn't obvious until you play real music (test tones don't have peaks).
5. The kernel's component binding is elegant — the framework already exists for exactly this case. We just needed to adapt it to Conexant (which had never needed it before).
hda-verb + i2cset. Works, survives reboots, correct stereo. The daemon monitors for AW88298 power loss (e.g. after suspend) and re-initializes automatically.After the daemon shipped I hit a subtle problem: every sound on the left speaker was losing its first ~100–200 ms. Continuous music was fine, but dialogue, notifications, and short clips were all choppy at startup. It turned out the AW88298 was intentionally idling its boost converter during digital silence and taking a moment to re-engage when signal returned. The fix was a single read-modify-write on an undocumented boost-mode register.
That whole debugging story — including the passive register sampler that surfaced the root cause, and a few lessons I took away from it — is in Part II: The day I got tired of guessing.