Bluetooth A2DP plays music to a speaker (essay 12). That’s audio leaving the phone over the air. This essay is about audio leaving the phone through the phone — earpiece, loudspeaker, headphone jack — all three wired to the same Realtek RT5640 codec on i2c1, taking I2S samples from the SoC’s I2S0 controller. RT5640 → speaker amp → cone. That’s the whole topology and it isn’t working.
The user has been consistent about this. Mid-session: “should it be out the speakers? because no.” And later, after hours of reading sound subsystem code: “no audio can we fix the display by chance?” That’s the honest tone here. The chip hardware is configured correctly. The data pipeline isn’t delivering samples. The speaker is silent.
What works
Almost every layer below “samples in the FIFO” is verified.
- RT5640 attaches at I2C 0x38, chip ID 0x6231 (
71fe3bfaudio: add RT5640 codec driver for PinePhone Pro ,147cd75audio: enable i2c1 bus and add rt5640 + sound to kernel config ). - All RT5640 registers configure correctly, verified by readback: right power state, right routing (DAC → output mixer → HP amp), right ADDA_CLK1 (
5abd6f5rt5640: fix ADDA_CLK1 (0x0000 for 256fs MCLK), set HP_VOL to 0dB ). - MCLK enabled.
b7a0829rt5640: enable MCLK (SCLK_I2S_8CH_OUT) clock gate — was gated off, codec had no clock turned theclk_i2soutgate on;0632d48clk_gate: implement get_gate (fixes "unimplemented" sysctl) rk_i2s: skip clock disable/set/enable if already at correct frequency added the missingget_gatemethod sosysctlcould report it. - BCLK and LRCK toggle on the scope. CKR is MDIV=4, TSD=64, RSD=64 → BCLK 3.072 MHz, LRCK 48 kHz. Exactly what 16-bit stereo at 48 kHz wants.
- I2S0’s TX interrupt fires, the ISR reads from
play_buf, writes 32-bit words into I2S_TXDR. 124 bytes per interrupt.
Codec configured, clocks running, frame sync ticking, ISR firing, FIFO being written. By any chip-side measurement this should produce audio.
What’s broken
The samples being written to the FIFO are zero. Every one of them.
rk_i2s TX[0]: ready=2048 level=0 readyptr=0 play_ptr=0 bufsize=192000 has_nonzero=0 (checked 64 bytes)
rk_i2s TX[0]: wrote 124 bytes, last=0x00000000 first_nonzero=0x00000000 found_nz=0
rk_i2s TX WARNING: 50 consecutive all-zero FIFO writes! Buffer data is lost somewhere.
That instrumentation is from src/sys/arm64/rockchip/rk_i2s.c:410. 7eff5a0 audio: add debug logging to trace zero-buffer issue added it. The ISR scans the first 64 bytes of the ready area for any non-zero sample and finds none. sndbuf_getready() reports 2048 bytes ready — the PCM framework believes there is data to send — but every byte in the ready area is 0x00.
The ISR isn’t broken. The FIFO write path isn’t broken. The buffer bookkeeping says we should have audio. The contents say we don’t. Something between userspace write(/dev/dsp0, …) and the hardware play buffer is silently filling with zeros.
What we tried
[WAR STORY]
Convince ourselves the chip side is fine
▸ symptom
mpg123 /tmp/song.mp3 runs, mixer levels look right, /dev/dsp0 accepts writes. Speaker silent.
▸ hypothesis 1
RT5640 misconfigured. Dumped every register, compared byte-for-byte against the Linux ALSA RT5640 init. Match. 5abd6f5 rt5640: fix ADDA_CLK1 (0x0000 for 256fs MCLK), set HP_VOL to 0dB fixed the one bit we had wrong (ADDA_CLK1); dfa399d rt5640: use MCLK directly (U-Boot configures it), remove PLL-from-BCLK ripped out the PLL-from-BCLK path because U-Boot already configures MCLK pass-through.
▸ hypothesis 2
MCLK isn’t reaching the codec. Scope says 12.288 MHz at the MCLK pin. Hardware is alive.
▸ hypothesis 3
The I2S driver isn’t writing to the FIFO. Logging added in ecc7e55 rk_i2s: add data flow logging to TX interrupt and d7cb545 rk_i2s: add debug logging to trigger, interrupt handler, and setup_intr shows the ISR fires, play_ptr advances 124 bytes per interrupt. The chip-side data path is doing its job.
▸ hypothesis 4
The codec can’t play what we’re sending — wrong format. Forced test data into the FIFO from the trigger: sine ( 7d9ddc7 rk_i2s: TEST — write sine wave directly to FIFO in trigger to test hardware path ), sawtooth ( 700acd0 rk_i2s: TEST — integer sawtooth pattern to FIFO ), 500 ms square wave ( 832e067 rk_i2s: TEST — 500ms square wave directly to FIFO to test hardware path ). None produced audio. 65fb052 rk_i2s: remove FIFO test (crashes in trigger context) reverted the test infrastructure (trigger-context writes crashed under edge cases). Inconclusive — either the speaker amp is off, or codec routing is broken in a way that survives register-dump comparison. Unproven.
That closes off the chip, clocks, I2S driver, and codec config to the limit of what we can check without a logic analyzer on the I2S data line. What’s left is the layer above: why are the bytes zero by the time the ISR sees them?
The current theory: vchan is feeding zeros
FreeBSD’s PCM subsystem inserts a virtual-channel mixer (vchan) between /dev/dsp0 and the hardware channel. Userspace opens a vchan client; vchan mixes its inputs down to a single hardware channel; the rk_i2s ISR drains that.
This is the live theory because everything below it is known-good and the symptom is exactly what a non-running vchan would produce. play_buf is allocated with M_ZERO. If nobody writes into it, but the read pointer advances (because the ISR consumes bytes — even zeros — every interrupt), the bookkeeping looks healthy while the actual audio data is missing. That matches the trace.
Where to dig next
- Add prints to
sys/dev/sound/pcm/vchan.caroundvchan_mix_s16— does it run at all, is its client list non-empty when audio should be playing, where does it write its output? - Compare to
snd_hda. Intel HDA is the reference implementation; its hardware-channel hand-off is whataudio_socshould match. Read both side by side. - Bypass vchan entirely. The PCM framework supports opening the hardware channel directly. If audio plays that way, the bug is vchan. If not, it’s between the hardware channel and the ISR — possibly
audio_dai_get_ptrreturn convention, possibly buffer-format negotiation that silently degrades to silence on a mismatch. - Read the Allwinner H6 and Rockchip RK3328
audio_socports — both work on FreeBSD. Find what they do that we don’t.
Smaller smells, not blockers
rt5640_dai_inittakes the RT5640 mutex inside the I2C bus’s Giant path. Lock-order reversal. Doesn’t crash, but real.- The simple-amp speaker node in our DT has no
VCC-supply.simpleamplogsfailed to set sysclk for aux nodebecause it doesn’t implementset_sysclk. Both benign, both fix-worthy.
What this essay deliberately doesn’t have
There is no <Fix> block. No <Breakthrough>. No green pill. The chip is configured, the clocks run, the FIFO drains, and the user can’t hear anything from their phone. We know roughly where the bug lives — somewhere in the PCM vchan-to-hardware-channel handoff — but not enough to fix it. The next session opens with prints in vchan.c and a fresh boot.
▸ lesson
Confirming the layers below the bug is not the same as fixing the bug. Every register, every clock gate, every FIFO write was verified. None of it produced audio. The bug lives in a layer of FreeBSD nobody on this project has read deeply yet, and the path forward is not more chip-side instrumentation — it’s reading sys/dev/sound/pcm/ until we understand it.