Identity
| Part | Realtek RT5640 (also marketed as ALC5640) |
| Role | I2S audio codec — DAC/ADC, mixers, output drivers, MICBIAS, jack-detect |
| Bus / address | i2c1 addr 0x1c (chip ID 0x6231) |
| GPIO / IRQ | HP-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 |
| Datasheet | Realtek RT5640 datasheet (community archive) |
| Pine64 wiki | PinePhone Pro hardware |
| Schematic | sheets 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
- Our tree:
src/sys/dev/iicbus/rt5640.c(952 lines) — codec driver. Register definitions inrt5640reg.h. - Linux mainline:
sound/soc/codecs/rt5640.c
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:
EnableSeq.conf:Stereo ADC1 Mux = ADC,Stereo ADC2 Mux = DMIC1,ADC Capture Volume = 80,ADC Capture Switch = on,Mono ADC Capture Switch = off,ADC Boost Gain = 0, and all RECMIX analog mic switches off by default.HiFi.conf/VoiceCall.conf:MicenablesStereo ADC MIXL/R ADC2and leaves the internal microphone on the digital-mic path;HeadsetenablesRECMIXL/R BST2plusStereo ADC MIXL/R ADC1.- The historical postmarketOS loader issue was not a different mixer
recipe: ALSA simply needed to find
/usr/share/alsa/ucm2/conf.d/simple-card/PinePhonePro.conf.
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
- Tune internal DMIC gain for normal speech now that the default
(
dmic_clk_div=3,dmic_edge=3,adc_dig_volume=32) has a durable capture/playback receipt. - Bench-verify the analog headset-mic chain —
rt5640_apply_recsrcnow powers BST4 (IN2P boost), MICBIAS1, and the L/R RECMIX rails, sets the BST2 gain field, unmutes BST2 inREC_L2/R2_MIXER, and routesSTO_ADC_MIXERto ADC1, but no one has spoken into a headset mic yet. Falsifiers in the new analog subsection of the parity verification. - Consume HP-detect on GPIO4_D4 and switch routing between HPO (loudspeaker) and HP jack on insertion.
- Drop the unsourced
rt5640_write(sc, RT5640_GCTL1, 0x3f41)at line 172, or annotate where it came from — Linux’s reg-init table doesn’t touch 0xfa.
Parity verification
The deferred-init refactor (cross-driver audit §rt5640) is observable from boot dmesg without instrumentation:
- Pre-fix dmesg: the
rt5640_initbody’s readbackdevice_printfs (“RT5640 readback: SPK_VOL=…”) interleave withrt5640<N>: <Realtek RT5640 Audio CODEC>and the iicbus child-attach line, all at the same monotonic timestamp — init is running synchronously insidert5640_attachwhile the parent still holds the iicbus lock. - Post-fix dmesg: the device-attach line lands at attach time,
then a few hundred ms later (when
taskqueue_threadpicks the task up) the readback printfs print as a separate burst. The gap is the signal that init moved out of the locked attach context. - Falsifier: if
WITNESSis enabled and dmesg showslock order reversalinvolving the iicbus mutex during attach, the refactor did not take. (Reference path: bwfm-style kernel built withoptions WITNESS+options WITNESS_SKIPSPIN.) - Userland regression check:
cat /tmp/sine.raw > /dev/dsp0must still produce audible output through the AW8737 → loudspeaker path. The taskqueue runs the same register sequence; only the calling context changes.
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).
- dmesg readback: the post-init burst should show
DMIC=0xb860 STO_ADC_MIX=0x4040 ADC_DIG_VOL=0x2020. IfDMICreads back as0x0000, the I2C write didn’t take and the ADC has no data-pin source. IfSTO_ADC_MIXreads0x4040but the capture is silent, the analog L1/R1 paths are correctly muted but ADC2 is not being clocked — usually aPWR_DIG1ADC L/R bit that didn’t stick or a DMIC clock divider mismatch. - mixer:
mixer rec.volshould accept reads/writes (currently hardware writes are wired throughrt5640_apply_recsrc; volume on the rec dev itself is plumbed by the audio_soc framework).mixer rec=micselects DMIC1;mixer rec=lineselects the analog IN2P headset path. - live sysctls: the running driver exposes
dev.rt5640.0.dmic_enabled, not the older draft namedmic1_enabled. The I2S side exposesdev.i2s.0.rx_stats.*anddev.i2s.0.regs.*after the RX-safety patch; read these before and immediately after a failed capture attempt. Use the named RT5640 controlsdev.rt5640.0.sysclk_rate,dev.rt5640.0.dmic_clk_div,dev.rt5640.0.dmic_data_pin,dev.rt5640.0.dmic_edge,dev.rt5640.0.dmic1_asrc,dev.rt5640.0.adc_asrc,dev.rt5640.0.i2s1_ref,dev.rt5640.0.adc_dig_volume, anddev.rt5640.0.adc_boost_gainfor live DMIC experiments; Current internal-mic default isdmic_clk_div=3,dmic_edge=3, andadc_dig_volume=32.dev.rt5640.0.dmic_clk_divis the Linux divider index field, not the divisor (0..5map to divisors{2,3,4,6,8,12}).dev.rt5640.0.dmic_data_pinfollows Linux’s internal values (0 = IN1P,1 = GPIO3).dev.rt5640.0.dmic_edgeis a bitfield: bit 0 samples the left DMIC channel on the rising edge, bit 1 samples the right channel on the rising edge;0means both falling. The ASRC knobs writeASRC_1.DMIC_1_M_ASYN,ASRC_2.ADC_M_ASYN, andASRC_2.I2S1_R_D_ENwithout changing the default.adc_dig_volume=80matches postmarketOS HiFi’sADC Capture Volume;adc_boost_gain=0matches postmarketOS’ADC Boost Gain.dev.rt5640.0.regs.gpio_ctrl1should always read with GP2 set to DMIC1_SCL; GP3 is only switched to DMIC1_SDA for the non-default GPIO3 data path. Do not hammerdev.rt5640.0.regs.dmicdirectly in a loop, because the first raw divider sweep re-enumerated USB before it printed a useful result. - bench capture: first confirm the PinePhone Pro microphone privacy
switch (#3) is enabled. With
mixer rec=micand the phone exposed to a known sound source (e.g. tap the mic, voice into the handset),dd if=/dev/dsp0 of=/tmp/cap.raw bs=4096 count=128 conv=syncshould produce non-silent samples. Round-trip test:oggenc /tmp/cap.raw -r 48000 -B 16 -C 2 -R -o /tmp/cap.ogg && play /tmp/cap.oggfrom a remote box. - Falsifier — USB disappears: capture start wedges hard enough that
USB Ethernet drops before any samples are written. That is below the
RT5640 route: audit
rk_i2s_dai_trigger, RX FIFO clear, RX interrupt enable, andI2S_XFERdirection bits before changing DMIC gain/divider. - Falsifier — close wedge: a 4096-byte
ddprints successful transfer but the shell never reaches the next command. That is teardown ordering: stop RK I2S RX before dropping RT5640 ADC power, otherwise the clock consumer can still be active while the provider disappears. - Falsifier — silence: capture is all-zero. First check privacy switch
#3; if the onboard microphone is physically disabled, this looks like a
codec or clock bug. If the switch is enabled, either the ADC isn’t
powered (
PWR_DIG1bits 1/2 not set),STO_ADC_MIXERselected the wrong source (checkADC_2_SRCfield), GP2 is still GPIO instead of DMIC clock, the IN1P/GPIO3 data-pin assumption is wrong, or the DMIC clock divider/edge/asynchronous clocking is wrong. Index4is Linux’s calculated choice for the observed 12.288 MHz sysclk, but on this bench it captured static while index3plusdmic_edge=3andadc_dig_volume=32was the first intelligible setting.adc_dig_volume=80anddmic1_asrc=adc_asrc=i2s1_ref=1have been falsified as sufficient fixes.dev.rt5640.0.dmic_data_pin=1has already falsified the GPIO3-vs-IN1P mux as the only problem: it also returned all-zero frames with clean close. - Falsifier — saturated: capture is clipped at
0x7fff/0x8001. BST gain too high — reduceIN1_IN2boost field. - Falsifier — clipped/distorted: ADC digital volume too high —
reduce
ADC_DIG_VOLfrom0x2f(47) down to0x1f(31) or lower. - Power gating: do not do I2C directly from
rt5640_dai_trigger.dsp_close → chn_abort → audio_soc_chan_trigger → rt5640_dai_triggerruns while the sound channel owns a non-sleepable lock; sleeping iniicdev_readfromtrips the kernel assertion shown on the serial console. Current safe behavior leaves ADC/filter power on after init. A future battery pass can move start/stop power gating into a taskqueue.
Analog headset mic on IN2P
Bench predicate (after build + boot, with a TRRS headset plugged in to the 3.5 mm jack):
mixer rec=line(orsysctl dev.rt5640.0.recsrc=1) flips the recsrc state machine to the headset path.sysctl dev.rt5640.0.recsrcreturns1.sysctl dev.rt5640.0.bst2_gainreturns5by default.sysctl dev.rt5640.0.regs.pwr_anlg2has bits 12 (PWR_BST4→ IN2P boost) and 11 (PWR_MB1→ MICBIAS1) set.sysctl dev.rt5640.0.regs.pwr_mixerhas bits 11 and 10 set (PWR_RM_L|R).sysctl dev.rt5640.0.regs.rec_l2_mixerclears bit 4 (M_BST4_RM_L = 0→ BST2 unmuted into RECMIXL); same forrec_r2_mixer.sysctl dev.rt5640.0.regs.in3_in4exposes the BST2 gain field at bits [10:8].- Capture:
dd if=/dev/dsp0 of=/tmp/headset-mic.raw bs=4096 count=128 conv=syncwhile speaking into the headset mic — the file should contain audible audio afteroggenc -r 48000 -B 16 -C 2 -R. - Linux cross-check: same
arecordagainst the analog input on a Linux PinePhone Pro image — signal levels should be qualitatively similar at the same gain step.
Falsifiers:
- All-zero capture →
PWR_BST4(bit 12 ofPWR_ANLG2) didn’t take, orREC_L2/R2_MIXERBST2 input still muted (bit 4), orSTO_ADC_MIXERis still routing DMIC (0x4040instead of0x3020). - Saturated/clipped capture →
bst2_gaintoo high; drop to 3-4. - DC-offset only (no AC response to sound) → MICBIAS1 not powered
(
PWR_ANLG2bit 11 clear), so the mic capsule isn’t biased — the pin sits at the codec’s DC reference and just floats. - Right channel silent, left works → asymmetric BST2 unmute or
PWR_RM_Rnot set; checkrec_r2_mixerbit 4 andpwr_mixerbit 10.
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.
Related
- On-device audio — the bring-up arc, including the TLV-polarity bug that hid every other fix.
- Cross-driver audit —
rt5640section, lock and capture-path findings. - Component: rk_i2s — feeds samples into the codec.
- Component: AW8737 — external amplifier on HPO.