Appendix · reference

Realtek RT5640 (audio codec)

I2C-controlled stereo codec driving loudspeaker (HPO + AW8737), earpiece (SPO), and the headphone jack

Identity

PartRealtek RT5640 (also marketed as ALC5640)
RoleI2S audio codec — DAC/ADC, mixers, output drivers, MICBIAS, jack-detect
Bus / addressi2c1 addr 0x1c (chip ID 0x6231)
GPIO / IRQHP-detect on GPIO4_D4 (DTS has simple-audio-card,hp-det-gpio and codec hp-det-gpios; local policy pending); MCLK from RK3399 clk_i2sout
DatasheetRealtek RT5640 datasheet (community archive)
Pine64 wikiPinePhone Pro hardware
Schematicsheets 6–8 (codec, DMIC wiring, headphone jack)

Status — ◐ partial

Loudspeaker playback works end-to-end on every boot via the HPO → AW8737 amplifier → cone path: 4ce7a60 rt5640: fix DAC1_DIG_VOL polarity (0x0000 was -65.6dB) + OUT MIX regs fixed the TLV polarity bug on DAC1_DIG_VOL that was attenuating both channels by 65 dB; 3b23d2f audio: drive simple amplifier enable GPIO and be6f2f1 audio: include GPIO definitions for simple amp made the external speaker amp auto-enable on PCMTRIG_START. The FreeBSD PCM device registers as pcm0: <rt5640-sound> (play/rec), and internal microphone capture now has a confirmed bench receipt. The 2026-05-06 bring-up moved the failure from “read never returns” to “nonzero samples but no usable voice” through several layers. Kernel #182 fixed capture teardown by stopping RK I2S RX before RT5640 ADC power-down. Kernel #186 falsified the soft codec knobs as sufficient fixes: postmarketOS’ ADC Capture Volume = 80, Megi/Linux divider index 4, both DMIC rising edge bits, and the DMIC/ADC/I2S ASRC bits all wrote correctly but did not make the audio intelligible by themselves. Kernel #187 matched Linux’s Rockchip I2S trigger semantics and started TX+RX together, which finally returned nonzero DMIC samples. Kernel #188 removed sleeping I2C transactions from rt5640_dai_trigger, avoiding the PCM-channel-lock assertion. Kernel #189 moved FIFO clears to the final RK I2S STOP path and proved playback, capture, and full-duplex monitor close with CLR=0 and no clear timeouts.

The board-level catch was the PinePhone Pro microphone privacy switch: switch #3 gates the onboard microphones. With it off, the DMIC route can produce floating/static-looking data even though the codec and I2S counters look live. With that switch enabled, the first intelligible internal-mic setting was divider index 3, both channels on the rising edge, and ADC digital volume 32. The boot default now programs DMIC = 0xb860 (DMIC1_EN | IN1P | divider index 3 | rising-edge L/R), STO_ADC_MIXER = 0x4040 (ADC_2_SRC = DMIC1, analog L1/R1 muted), and ADC_DIG_VOL = 0x2020. Kernel #190 verified those defaults after reboot, recorded 962,560 bytes from /dev/dsp0, played them back through the loudspeaker, and left I2S_XFER=0, I2S_CLR=0, and both FIFO clear-timeout counters at zero. rt5640_mixer_setrecsrc (and the matching dev.rt5640.0.recsrc sysctl) flip the full analog capture chain in one place — PWR_ANLG2.PWR_BST4, PWR_ANLG2.PWR_MB1, PWR_MIXER.PWR_RM_L|R, IN3_IN4 BST2 gain, REC_L2/R2_MIXER BST2 unmute, STO_ADC_MIXER route — and a dev.rt5640.0.bst2_gain sysctl exposes the boost step (0–7) so the bench can tune gain without recompiling. Speakerphone is still not a finished phone profile: the loudspeaker side works and full-duplex PCM transport now runs, but echo, gain, earpiece selection, and modem call routing still need bench receipts. Earpiece (SPO) and jack-detect routing remain unimplemented. The DTS already names GPIO4_D4 for HP detect; the missing piece is consuming it in the local codec/audio policy and switching HPO routing safely. The lock-order reversal called out in the cross-driver audit — rt5640_attach issuing ~50 register writes under the iicbus child-attach lock — was fixed by deferring rt5640_init to a taskqueue worker; see the parity verification section below.

Driver

The driver is a hand-port of the Linux ASoC codec, restricted to the playback paths that PinePhone Pro actually uses. Linux’s regmap + DAPM widget topology is collapsed into linear register writes in rt5640_init(); the original implementation called this directly from rt5640_attach and triggered the lock-order reversal, because every rt5640_write re-acquires the iicbus mutex inside an attach context that already holds it. The fix (per the cross-driver audit) defers rt5640_init to a taskqueue_thread worker so the register sequence runs outside the attach lock — matching ASoC’s probe path on Linux.

The PinePhone Pro topology is unusual: the loudspeaker hangs off HPOL/HPOR through an external simple-audio-amplifier (AW8737), not the codec’s SPO pins. SPO drives the earpiece. Every “speaker silent” symptom in essay 13 trace back to that asymmetry being invisible from the datasheet — only the schematic shows it.

Linux handles the same topology through ASoC + ALSA UCM. The PinePhone Pro UCM profiles expose Earpiece on SPOL, Speaker on the HPO path plus Internal Speaker Switch, Mic on the ADC2 / digital-mic path, and Headset on the RECMIX BST2 / ADC1 path. The FreeBSD driver has hard-coded register paths for the same physical routes, but it does not yet have an ALSA-UCM-equivalent policy layer for call profiles, jack switching, or speakerphone echo/volume tuning.

The current postmarketOS/Pine64 UCM files are useful because they keep the same topology honest rather than inventing a second route:

Megi’s 2026 kernel tree matches that story in the DTS: the sound card is named PinePhonePro, routes "DMIC1", "Internal Microphone", gives the codec realtek,dmic1-data-pin = <1>, and supplies MCLK from SCLK_I2S_8CH_OUT. In the RT5640 driver, set_dmic_clk() derives the DMIC divider from sysclk / ADDA prediv through rl6231_calc_dmic_clk(). With FreeBSD’s observed 12.288 MHz sysclk and ADDA prediv 1, that algorithm chooses divider index 4 (divisor 8, 1.536 MHz). The same driver has ASRC widgets for DMIC1/ADC/I2S, but is_using_asrc() returns 0 in the fetched Megi source, so ASRC is a live falsifier, not a Linux default.

One 2026-05-06 browser-media failure was not an RT5640 kernel wedge. With Firefox using its packaged PulseAudio path, YouTube left PulseAudio’s OSS thread pegged while Sway and Panfrost stayed reachable. The phone image now sets Firefox cubeb to oss in mobile-config-firefox and disables PulseAudio autospawn via the user’s PulseAudio client config. That does not make YouTube cheap on the RK3399S — the follow-up run still saturated Firefox media/content CPUs — but it keeps the test on the FreeBSD PCM/RT5640 path and avoids a misleading PulseAudio spin.

Open work

Parity verification

The deferred-init refactor (cross-driver audit §rt5640) is observable from boot dmesg without instrumentation:

Reference: Linux’s sound/soc/codecs/rt5640.c calls devm_regmap_init_i2c and lets the regmap mutex serialize all register access; ASoC core itself runs codec probe in its own taskqueue, never under a held parent-bus lock. Our taskqueue-deferred rt5640_init mirrors that pattern with the locking primitives FreeBSD already provides.

Capture path (DMIC1 + analog headset)

The DMIC1 init block at the end of rt5640_init writes are observable both via dmesg readbacks and userland mixer state. Reference: Linux’s sound/soc/codecs/rt5640.c rt5640_dapm_routes documents the canonical capture-side widget chain (DMIC1 → Stereo ADC MIXL/R (ADC2 input) → Stereo ADC L/R → IF1 ADC1 → I2S TX).

Analog headset mic on IN2P

Bench predicate (after build + boot, with a TRRS headset plugged in to the 3.5 mm jack):

Falsifiers:

Naming caveat that bit us during this bring-up: Linux mainline’s DAPM widget called “BST2” maps to register field PWR_BST4 (bit 12 of PWR_ANLG2) and the REC mute is at SFT 4 (M_BST4_RM_L), not the bit positions the chip’s datasheet-style register field names suggest. The IN2 Boost gain control likewise lives in IN3_IN4 (0x0e), not IN1_IN2. The 2026-04-30 audit brief had all three of these wrong; cross-checked here against sound/soc/codecs/rt5640.h and the rt5640_dapm_widgets table.