The morning state was misleading. The RT5640 driver exposed
pcm0: <rt5640-sound> (play/rec), but a raw read from /dev/dsp0
either blocked, returned all-zero samples, or wedged on close before the
bench could collect post-capture counters. The first real fix was
teardown ordering: RK I2S RX had to stop before RT5640 ADC power dropped,
otherwise the controller could still be consuming clocks from a codec path
that had just been powered off.
Once teardown stopped being the visible failure, the next tests moved down
to clocks and trigger semantics. 5852817 rt5640: expose DMIC capture tuning knobs exposed the
RT5640 soft knobs that made the bench falsifiable:
dev.rt5640.0.dmic_clk_div
dev.rt5640.0.dmic_edge
dev.rt5640.0.adc_dig_volume
dev.rt5640.0.dmic1_asrc
dev.rt5640.0.adc_asrc
dev.rt5640.0.i2s1_refThose knobs ruled out the obvious codec-only guesses. PostmarketOS’
ADC Capture Volume = 80, Megi/Linux divider index 4, both DMIC
rising-edge bits, and the DMIC/ADC/I2S ASRC switches could all be written
and read back, but they did not turn the capture into intelligible audio.
The key Linux delta was in the Rockchip controller. Linux starts TX and RX
together for capture because the RK3399 I2S block is a shared serial
engine. FreeBSD was starting only RX for a capture-only stream. 636743a rk_i2s: start TX with capture like Linux changed rk_i2s_dai_trigger() to start both directions
for one-way streams and keep the shared engine running until neither
playback nor capture is active.
That produced nonzero DMIC samples, but it exposed a second kernel bug:
rt5640_dai_trigger() was still doing sleeping I2C reads while the sound
channel held a non-sleepable lock. The serial console showed the exact
path:
dsp_close -> chn_abort -> audio_soc_chan_trigger -> rt5640_dai_trigger 174d61f rt5640: avoid sleeping I2C in trigger made the RT5640 trigger callback sleep-free and
left capture-route power in the init path. That is not the final battery
policy, but it is the correct lock boundary: future start/stop power
gating has to run from a taskqueue, not directly from PCM trigger context.
◐ partial At this point the kernel could record nonzero samples and close cleanly, but full-duplex monitor tests still tripped the next RK I2S sequencing issue.
The first TSADC soft governor was intentionally conservative: sample from
a callout, run policy on taskqueue_thread, cap Panfrost through
dev.panfrost.0.gpu_max_auto_mhz, and cap CPU by writing
dev.cpu.0.freq / dev.cpu.4.freq. It worked well enough to keep a
glmark2 run alive, but it left one bad ownership boundary: powerd
could immediately raise the CPU frequency again because both paths were
ordinary user-priority cpufreq requests.
The local guard closed that race by stopping powerd during warm/hot
policy. That was acceptable as a bench harness, but not as the final phone
posture. The FreeBSD cpufreq core appears to have the right interface:
thermal drivers can call CPUFREQ_SET(..., CPUFREQ_PRIO_KERN) and later
restore with CPUFREQ_SET(..., NULL, CPUFREQ_PRIO_KERN). Kernel #191
tried that path in rk_tsadc.
◐ partial The soft policy shape is now the one we want
for the phone, but the first implementation was unsafe. #191 reached
root mount, then sdhci_rockchip0-slot0 repeatedly timed out reading
ufs/FreeBSD_Install. Restoring kernel.prev (#190) booted the same
eMMC cleanly. The cpufreq-priority integration is therefore backed out;
the shipping path remains GPU max-auto caps plus dev.cpu.N.freq caps
with phone_thermal_guard yielding powerd. Hardware comparator IRQs
and TSHUT are still off by default until the native-q alarm semantics are
proven on a recovery-attached bench.
The rollback kernel (#192) booted cleanly after the cpufreq-priority
path was removed. Its first mise run thermal:phone -- soft-test also
caught a userland guard bug: the guard sampled the GPU max-auto baseline
after the kernel had already applied the warm 400 MHz cap, so cooldown
left Panfrost capped. The guard now refreshes its baseline only while
policy is normal, marks active thermal episodes explicitly, and the
soft-test checks that GPU max-auto returns to the pre-test value.
The thermal-policy gap after the cpufreq rollback was not the soft
governor; it was whether the RK3399 comparator could be trusted before
arming any interrupt-backed policy. 0497253 thermal: add runtime tsadc irq bench added a
runtime bench instead of another boot-time trap: choose one channel,
program one trip, clear pending state, enable one comparator bit, and
auto-disarm on the first matching IRQ. It also refuses to run while
TSHUT is armed.
Kernel #193 made the failure precise. In the old software q-select
mode, code-domain trips did not fire, while native-q trips had the wrong
high-temperature sense. That matched the earlier storm/no-fire evidence
without leaving the phone in a bad state.
e4017e7 thermal: enable tsadc hardware qsel then enabled RK3399 hardware Q-select
(TSADCV3_AUTO_Q_SEL_EN) by default. Kernel #195 booted with
q_sel_enabled=1, auto_con=51, tshut_enabled=0, and clean
temperature readout. a856481 thermal: tighten tsadc irq bench accounting tightened the bench so only
the armed channel counts as a bench fire and native-q cases are treated
as diagnostic.
◐ partial mise run thermal:phone -- irq-bench now
passes the guarded comparator predicate: code-domain nofire/fire cases
work on both CPU and GPU channels, the native-q cases remain the negative
control, and the final state is INT_EN=0, irq_enabled=0,
tshut_enabled=0, irq_bench_last_error=0. That closes comparator IRQ
semantics for a guarded software path. Hardware TSHUT still needs a
separate recovery-attached proof before it can be enabled at boot.
The speakerphone-style test was intentionally simple:
dd if=/dev/dsp0 | dd of=/dev/dsp0That found a controller bug independent of the RT5640 route. Once
FreeBSD started TX and RX together like Linux, the old START path could
clear one FIFO while the shared serial engine was already running. The
symptom was I2S_CLR_TXC stuck at 1; playback returned EIO; the
phone needed another reboot before the next test.
Linux does not clear FIFOs on START in this mode. It drops I2S_XFER,
waits briefly, then clears both FIFOs after the last stream stops.
7367f66 rk_i2s: clear FIFOs after shared engine stop moved the FreeBSD overlay to that rule.
Kernel #189 proved the controller side:
playback-only: exit 0
capture-only: exit 0
full-duplex: TX/RX frame counters advanced
I2S_XFER: 0
I2S_CLR: 0
clear_timeouts: 0It also proved the board-level loudspeaker path again. The PinePhone Pro
loudspeaker is not RT5640 SPO; it is HPOL/HPOR through the external
AW8737/simple-audio-amplifier path. With mixer vol raised and
mixer speaker=0, the user heard the generated tone through the phone’s
speaker and dev.simpleamp.0.enabled followed playback start/stop.
● working RK I2S playback, capture, and full-duplex PCM transport now return cleanly. The remaining audio work is route policy, gain, headset, earpiece, and modem call integration.
The last false negative was not in the kernel. Repeated mic-to-speaker tests produced feedback or static but no voice. The RT5640 and I2S counters were live, which made it look like another codec routing mistake. The actual board condition was simpler: PinePhone Pro privacy switch #3 was off, which physically disables the onboard microphones.
After enabling that switch, the bench swept the remaining live knobs. The setting that produced the first intelligible internal-mic playback was:
dev.rt5640.0.dmic_clk_div: 3
dev.rt5640.0.dmic_edge: 3
dev.rt5640.0.adc_dig_volume: 32
dev.rt5640.0.regs.dmic: 47200 # 0xb860
dev.rt5640.0.regs.adc_dig_vol: 8224 # 0x2020 0624070 rt5640: default DMIC to verified internal mic mode made that the boot default and documented the
privacy-switch trap. Kernel #190 then rebooted with the new default and
confirmed it without manual sysctls:
FreeBSD PinePhonePro-FreeBSD 15.0-STABLE #190
dev.rt5640.0.dmic_clk_div: 3
dev.rt5640.0.dmic_edge: 3
dev.rt5640.0.adc_dig_volume: 32
dev.rt5640.0.regs.dmic: 47200
dev.rt5640.0.regs.adc_dig_vol: 8224The final bounded proof muted the speaker during capture, recorded
962,560 bytes from /dev/dsp0, then played the raw sample back through
the loudspeaker. The kernel returned to idle:
dev.i2s.0.regs.xfer: 0
dev.i2s.0.regs.clr: 0
dev.i2s.0.tx_stats.clear_timeouts: 0
dev.i2s.0.rx_stats.clear_timeouts: 0● working Internal DMIC capture is confirmed on the PinePhone Pro. It is not a finished phone audio stack yet: headset mic, HP detect, earpiece, echo/gain policy, and EG25-G call routing still need their own receipts.