If you haven't read part I, start there. The short version: five weeks drilling down through Linux's audio stack until I discovered that the left channel goes through an AWINIC AW88298 chip with no mainline driver, connected via I2S to an undocumented output on the Conexant SN6180 codec. I built a daemon that configured it and the speaker worked. Just not quite right.
When the daemon was up and running, the left speaker finally made sound. Left and right came out of their respective sides, stereo balance worked, levels were reasonable. One detail was missing: the first 100–200 ms of every sound were choppy. Or just plain missing.
The home test was brutal. With continuous music — put on an album and let it play — the left was perfect. But open a video with dialogue, where there are silences and voices come back; trigger a short system notification; start that classic "now left channel, now right channel" test video everyone uses for stereo checks. In all those cases, the first fragment of sound on the left arrived late and wrong.
The human brain catches those artifacts immediately. A driver that drops the beginning of every word in a video is, in practice, a broken driver.
My main hypothesis was: "the AW88298 goes into protection and takes time to recover." Based on that, I spent days trying:
SPK_GAIN between AV7 and AV20HAGCE (Hardware AGC)BST_IPEAK across different levelsINPLEV=-6dB as headroom marginSYSINT in the polling loop to "clear" transient flagsEvery one of those ideas had a manual-based justification. None of them fixed the symptom. Some made it worse. One of them — periodically reading SYSINT — turned out to be actively counterproductive, because that register is read-clear: reading it wipes the interrupt history. I had spent hours of debugging destroying the evidence I was trying to examine.
The daemon worked "well enough" for continuous music, so I left it at that. But deep down I knew the problem was still there.
After hours of fighting with no progress, I forced myself to stop and restate the problem from scratch. The unknown was no longer the HDA routing — I had that figured out. What I didn't know was what the AW88298 does when it transitions from digital silence to a signal while clocks are live. And the aw882xx PID 1852 datasheet (courtesy of the headers published by dianjixz) explicitly documented the relevant state observables: BSTS (boost start-up finished), SWS/WDS (amplifier switching status), PLL, clock, over-current, over-voltage. All in SYSST, read-only, non-destructive.
I had spent weeks reading the datasheet looking for things to write and had never once stopped to think about what I could read. SYSST, VBAT, PVDD, TEMP, ISNDAT, VSNDAT were a live snapshot of the chip's actual state. I just needed to take that snapshot every few milliseconds and see which bits changed at the exact moment of the glitch.
I wrote a script in five minutes. A bash loop reading six registers over I2C, printing only lines where something changed so we wouldn't drown in text, millisecond-precision timestamps. Zero intervention: the chip had no idea it was being watched.
while true; do
sysst=$(read_be 0x01)
pvdd=$(read_be 0x14)
vbat=$(read_be 0x12)
temp=$(read_be 0x13)
# ...
if [[ "$sig" != "$prev" ]]; then
printf "T+%s SYSST=%s PVDD=%dmV ...\n" "$(ts)" "$sysst" "$pvdd_mv"
prev="$sig"
fi
done
I stopped the daemon, looped a L/R test video, launched the sampler, and the pattern showed up on the very first run:
The WDS (amplifier switching) and BSTS (boost start-up finished) bits were flickering in sync with the audio. When the audio was digital silence — not clocks stopped, not a closed stream, silence inside a live stream — the chip was shutting down its switching stage and its boost converter. When the signal came back, it had to re-engage them. Those 100–200 ms were the time the boost took to come back up to operating level.
It wasn't a bug. It wasn't a tripped protection. It wasn't PipeWire suspending anything. It was the chip doing to the audio exactly what its firmware told it to do.
BSTCTRL2 and Smart Boost 2The relevant register was somewhere I had never looked: BSTCTRL2 (0x61). Hardware readout:
The public documentation for the aw882xx (PID 1852, published by dianjixz/aw882xx) describes the boost modes like this:
BST_MODE |
Behavior |
|---|---|
000 |
Transparent — boost in bypass |
001 |
Force Boost — boost is always active |
101 |
Smart Boost 1 |
others (incl. 110) |
Smart Boost 2 — boost shuts down when audio level drops below a threshold and reactivates when it rises |
Read that again: "boost shuts down when audio level drops below a threshold and reactivates when it rises." That text, written years ago in a datasheet for a chip designed to maximize efficiency, described exactly what my sampler was seeing. The chip shipped with the most aggressive power-saving mode enabled, one that cut its power stage the moment audio went quiet.
It makes complete sense: the AW88298 is designed primarily for products where audio is continuous — music, sustained voice, doorbells. In those cases, shutting the boost during silences saves battery. On a laptop, where audio starts and stops a hundred times a day — an email notification, the blip of an incoming message, a single word from the video you're watching over lunch — that energy saving turns into a glitch at the start of every event.
i2csetChanging BST_MODE to 001 (Force Boost) without touching the other fields in the register is a read-modify-write:
# Current value: 0x6673 BE
# Clear bits [14:12]: 0x6673 & 0x8FFF = 0x0673
# Set bit 12: 0x0673 | 0x1000 = 0x1673
# Little-endian on the bus: 0x7316
sudo i2cset -y 0 0x34 0x61 0x7316 w
One command. One register. The glitches vanished the instant I pressed Enter. I ran the L/R test again, played a dialogue video again, triggered system notifications again. Clean in every case.
I ran the sampler again out of curiosity. BSTS stayed at 1 the whole time. WDS too. The chip was no longer shutting down.
With Smart Boost 2 out of the picture, a second thing changed: the left speaker's volume ceiling.
For weeks I had been convinced the left couldn't match the right at maximum volume. I had tried pushing the DAC amp up to 0x4a (same value as the right) and always hit distortion or glitches well before that, around 0x39. I assumed it was a physical limitation of the speaker or the boost converter.
Wrong. The ceiling was being imposed by Smart Boost 2 dropping out under load. With Force Boost active, we retuned:
INPLEV=-6dB. We were using it as headroom against boost dropout. Without that dropout, the headroom is unnecessary.0x40 — 10 steps below the right side's maximum, which compensates for the different analog gain chains between the two chips.max(0, right - 10). A single slider, two speakers, for the first time in this laptop's life under Linux.SYSINT is read-clear. Don't poll it. End of PSA.aw882xx register headers with documentation of the boost modes, because nadimkobeissi had documented the HDA side-codec pattern for Legion/AW88399, because a handful of kernel maintainers responded patiently on issues that had been open for weeks. Every piece of public documentation I wrote in part I and in this very post is my small contribution to making sure the next person who searches for AW88298 Linux doesn't spend five weeks finding what they need.The daemon is running on cortez (my Chuwi CoreBook X). With systemctl enable aw88298 it survives reboots; the left speaker comes up in the first second after login, sounds clean, matches the right at maximum volume, and responds to the system volume slider in a unified way.
The next step is moving all of this into a kernel patch — side-codec init sequence, Force Boost in the probe, volume mirror as an ALSA kcontrol — and submitting it to LKML. That will be the subject of part III.
If you have the same laptop, the daemon and diagnostic scripts are in my repo: github.com/pacomont/chuwi-corebook-x-left-speaker. Any feedback is welcome, especially if you have an internal AW88298 datasheet with more complete documentation on the boost modes and silence detection thresholds — there are things in bits 11 and 13 of SYSST that I still don't know the names of.