This is the punch-list that came out of auditing five subsystems against their Linux mainline (and where applicable OpenBSD) references on 2026-04-30, after the WiFi stability arc landed and we picked our heads up. The audit lists what each driver does, what the reference implementation does, and what is most likely the cause of the next user-visible flake. Recommendations are actionable: file, line, register, bit. Every item is falsifiable on the next bench session.
The site already has subsystem essays for the surface symptoms — this page is the diff between what we ship and what the canonical drivers do.
SDIO IRQ delivery — DWMMC + bwfm
The current bench reality (2026-04-29 logs): WiFi associates, completes a 2 MiB
transfer with cmd53_err=0, but dev.rockchip_dwmmc.0.sdio_intrs stays at 0.
The driver only survives because bwfm_sdio_poll_callout ticks at 100 Hz and
enqueues the task regardless of whether the chip raised an IRQ.
Reference path (Linux mainline)
drivers/mmc/host/dw_mmc.c:
dw_mci_prepare_sdio_irq()is called first on arm. It clearsSDMMC_CLKEN_LOW_PWRinCLKENA, issuesCMD_UPD_CLK | CMD_PRV_DAT_WAITto commit the clock change, and setsDW_MMC_CARD_NO_LOW_PWRso laterdw_mci_setup_bus()calls don’t re-arm clock-gating.__dw_mci_enable_sdio_irq()then setsSDMMC_INT_SDIO(host->sdio_irq)(BIT(16) on Rockchip, sincedw_mmc-rockchip.csetshost->sdio_irq = 8) inINTMASK, underhost->irq_lock.dw_mci_interrupt()seespending & SDMMC_INT_SDIO, acks it viaRINTSTS, disables the bit in INTMASK again, then callssdio_signal_irq(host->mmc). The core’s IRQ thread re-arms after the function handler returns.
The order is CLKENA(LP=0) + UPD_CLK → INTMASK |= SDIO → CCCR INT_ENABLE = 0x03.
Our path
patches/sys/dev/mmc/host/dwmmc.c.patch::dwmmc_cam_sdio_intr (the
function added by 916fe51 Add SDIO IRQ support for bwfm + extended by 5c4b836 Keep DWMMC clock running for SDIO IRQ )
used to do INTMASK first, then CLKENA + UPD_CLK — the reverse of
Linux. The current overlay arms the handler state first, commits
CLKENA with low-power mode cleared, clears stale RINTSTS.SDIO, and
only then unmasks INTMASK.SDIO. The bench predicate is still
dev.rockchip_dwmmc.0.sdio_intrs > 0 during a bounded transfer.
dwmmc_intr() SDIO branch acks RINTSTS but does not clear the SDIO
bit in INTMASK on fire, leaving it permanently armed. dwmmc_attach()
installs an initial INTMASK that does not include SDMMC_INTMASK_SDIO;
any code path that re-issues that mask silently disarms us.
src/sys/dev/bwfm/bwfm_sdio.c:
bwfm_sdio_intr(line 1453) istaskqueue_enqueueonly. Correct.bwfm_sdio_poll_callout(line 1473) re-arms unconditionally — even whensc_irq_count > 0. The polling path therefore masks the very bug we’re trying to find: every successful transfer is ambiguous between “IRQ delivered” and “callout fired.”
Top divergences ranked by “could explain sdio_intrs=0”
-
DAT[1] pinctrl pull configuration. SDIO function-1 IRQ is signaled by the card pulling DAT[1] low while in 4-bit mode. If our DTS pinctrl for the SDIO node leaves DAT[1] without a pull-up (or with the wrong drive strength), the card asserts low but the controller front-end sees nothing — exactly matching the symptom (CMD53 holds the line driven during a transfer, so polling works; between transfers the line floats and the IRQ is lost). This is the single highest-likelihood root cause.
-
INTMASK / CLKENA write order. Linux is explicit that the clock must be ungated before INTMASK_SDIO is set; otherwise an initial spurious DAT[1] edge during clock startup can be latched and the real IRQ lost.
-
No mask-on-fire in
dwmmc_intrSDIO branch. Linux disables INTMASK_SDIO in the handler and re-enables aftersdio_signal_irqreturns. We don’t, which means a card holding DAT[1] low through our function handler will create an IRQ storm — but right now no IRQ ever fires in the first place, so we haven’t hit it.
Recommendations (priority order)
- Audit the
sdmmc0/sdio0pinctrl entries in our PPP DTS for explicitbias-pull-upon DAT[0..3] and CMD. Cross-reference Rockchiprockchip,rk3399.dtsiand the postmarketOS PPP DTS. If DAT[1] has no pull, no software fix indwmmc.cwill help. - Reorder
dwmmc_cam_sdio_intrto write CLKENA (clear LP) + UPD_CLK busy-poll before INTMASK. After UPD_CLK and before INTMASK, also clear any latched stale state withWRITE4(SDMMC_RINTSTS, SDMMC_INTMASK_SDIO). - Add a single capture point in
sdiob_claim_irq: dumpINTMASK / MINTSTS / RINTSTS / GRF pull register for the SDIO padsimmediately after theMMC_SIM_SDIO_INTR(...,true)call and before the CCCR0x00 → 0x03write. The next bench session can then say in one log entry whether the host bit is set, the GPIO is pulled, and whether the card has already raised the line. - Mirror Linux’s mask-on-fire in
dwmmc_intrSDIO branch once IRQs are flowing. - Stop
bwfm_sdio_poll_calloutoncesc_irq_count > 0so logs stop conflating IRQ and poll-driven progress.
Strange code
bwfm_sdio.c:1554-1556ORs inSDPCMD_INTSTATUS_HMB_FRAME_INDwheneverNAKHANDLEDis set — copied verbatim from OpenBSD. Combined with the L1569 “consume fn2 frames without FRAME_IND” branch, the poll task always finds RX frames once TX is pending, which is exactly what lets the polling path pretend to work.- The
dump_statesysctl is only on-demand. The interesting moment is between the host arm and the card arm; that’s the snapshot we don’t yet capture.
bwfm — TX, NVRAM, scan, events
Things still OpenBSD-shaped
bwfm.c:3241bwfm_rx_event_cbusesTAILQ_FIRST(&ic->ic_vaps);bwfm_transmit(bwfm.c:2366) andic->ic_curchanaccesses at 1692 and 2675 also assume a singleton VAP. Works on PPP today (single STA), fails silently the moment we add a monitor or AP VAP. Comment, don’t fix yet.bwfm_sdio.c:1736hardcodesbcdc->priority = 0with a “TODO classify priority” comment. EAPOL goes in the same fifo as bulk data. The driver already extracts the ethertype at line 1743 (it logs EAPOL). Two-line patch:if (ethertype == ETHERTYPE_PAE) priority = 7;— preserves WPA reauth under sustained TX.
NVRAM padding (likely root cause of intermittent firmware weirdness)
bwfm_sdio.c:1057-1075 — after stripping trailing NULs we do
nvlen += 2, then roundup(nvlen, 4). Linux’s
brcmf_fw_nvram_strip() does roundup(nvram_len + 1, 4) — exactly
one trailing NUL plus alignment fill. We always add one more NUL
than Linux. On any input whose stripped length is ≡ 3 (mod 4) the
token offset shifts by one byte and the firmware-side CRC of the NVRAM
block mismatches the count we encode in the trailer.
The fix is one-character: change nvlen += 2 to nvlen += 1 at
bwfm_sdio.c:1060, and adjust the nvlen - 2 reference at L1068.
Scan, credits, events
- Scan version probe.
bwfm.c:2702tries v2 first, falls back to v0. Linux brcmfmac on BCM43455 unconditionally uses v0. We burn one failed iovar per scan. Add a chip-id shortcut: whensc->sc_chip.ch_chip == BRCM_CC_4345_CHIP_ID, forcesc_scan_ver = 0before the iovar probe atbwfm.c:2009. - Control-frame credit reservation.
bwfm_sdio.c:1629(bwfm_sdio_tx_ok) uses bare(tx_max - tx_seq). Linux’sdata_ok()reservesTXCTL_CREDITS=2whenctrl_frame_statis set, so a control frame can always preempt a bulk burst. Without that, sustained TX can lock out fwvar set/get for the full burst, surfacing as transientset_varfailures during heavy traffic. Themsleep(... hz)atbwfm.c:1374then times out at 1 s — a much shorter window than Linux’s 2 s. - Event subscriptions. Recent commit
1475c7aHandle successful bwfm link events addedBWFM_E_LINK. We do not subscribe to or handleBWFM_E_NDIS_LINK,BWFM_E_ROAM,BWFM_E_PSK_SUP, orBWFM_E_FIFO_CREDIT_MAP. None of those are required for STA association on PPP, but ROAM / NDIS_LINK are how Linux keeps net80211-equivalent state coherent through firmware-initiated transitions. Likely cause of some subset of “associated but no DHCP” reconnect symptoms.
Strange code / band-aids
bwfm_sdio.c:1649-1654“repairing uninitialized txq” inbwfm_sdio_txctl. Attach already callsmbufq_init(...,BWFM_TXQLEN)(per1594fe4Initialize bwfm tx queue before deferred init ). This branch should be dead. It still fires. Find the zero-er before deleting; the device_printf is the only signal we have that something is re-zeroing the softc post-attach.bwfm_sdio.c:2205sc->sc_tx_seq = 0xff;without comment. Works (initial credit window of 1) but reads as a sentinel. OpenBSD just leaves it 0. Add a comment or set to 0.bwfm_sdio.c:2057-2099post-HT firmware dump is an unconditional device_printf block. Gate behindbwfm_debugor remove.bwfm_sdio.c:1336blindly assignstx_maxfrom firmware. Linux clampsif ((u8)(tx_seq_max - bus->tx_seq) > 0x40) tx_seq_max = bus->tx_seq + 0x40;to defend against rolled credit windows from a misbehaving firmware.
rk_vop — the modeset-lock wedge
Open WarStory in essay 08. The audit found three structurally suspect items in the same code path; two of them match the wedge symptoms exactly.
Reference path (Linux mainline)
drivers/gpu/drm/rockchip/rockchip_drm_fb.c mode_config helpers
point at the stock drm_atomic_helper_commit_tail_rpm, which runs:
modeset_disables → modeset_enables → commit_planes(ACTIVE_ONLY) →
fake_vblank → commit_hw_done → wait_for_vblanks →
cleanup_planes.
rockchip_drm_vop.c::vop_crtc_atomic_flush (1618-1685) latches the
event into vop->event under event_lock, calls
drm_crtc_vblank_get, then vop_cfg_done. The IRQ handler
(vop_isr 1393-1455) on FS_INTR calls drm_crtc_handle_vblank,
then vop_handle_vblank (2234-2246) drains vop->event with
drm_crtc_send_vblank_event and calls drm_crtc_vblank_put.
The point is that the event lives on the driver between flush and
vblank IRQ — never on crtc->state once flush has run.
Our path
src/sys/dev/drm/rockchip/rk_drm.c:147-165 — custom
rk_drm_atomic_commit_tail that does modeset_disables → commit_planes
→ modeset_enables → fake_vblank → commit_hw_done → cleanup_planes.
The comment at 157-162 explicitly says wait_for_vblanks was removed
because “the wait can deadlock when a GPU reset parks the scheduler
thread while a DRM commit is in flight” — that diagnosis predates the
panfrost reset rework ( 3378d3d Queue Panfrost reset before scheduler stop , cf2dd90 Fix Panfrost reset fence signaling context ).
src/sys/dev/drm/rockchip/rk_vop.c:428-444 — rk_crtc_atomic_begin
sends crtc->state->event immediately and nulls it.
rk_crtc_atomic_flush (446-481) re-reads crtc->state->event; if
begin already drained it, flush no-ops. Both paths exist; the prior
git history (per the agent’s read of revert chains) shows them fighting
across multiple revisions.
rk_vop.c:269-290 — rk_vop_intr reads INTR_STATUS0, then writes
~0 to INTR_CLEAR0 (line 280) — clears every pending bit in the
status register, including bits that were about to assert. On a
shared interrupt line a frame-completion that arrives between the
read and the write is silently lost.
Top divergences ranked by “could explain the wedge”
drm_atomic_helper_wait_for_vblanksremoved. Without this,cleanup_planesruns synchronously in the same commit ascommit_hw_done. A later commit’swait_for_dependenciesthen sees acommit->flip_donethat nobody completes. Exact symptom:[CRTC:33:crtc-0] flip_done timed outon the wedge screenshot.- Shared-IRQ ack writes
~0.rk_vop.c:280clobbers any in-flight FS_INTR by clearing the entire status register. Frame completions that race the ack are lost — so vblank counter advances stop, the CRTC commit chain stalls. - Double-event drain. Both
atomic_beginandatomic_flushhandle the event, in different ways, with novop->eventdriver-side latch.vblank_getis taken in flush (line 470) but never balanced by avblank_put(the IRQ never sees the event because the IRQ doesn’t drainvop->event— there is novop->event). Avblank_getwithout a matchingvblank_putis exactly the kind of imbalance that surfaces as “flip_done never completes” under sustained load. vblank_getcalled withevent_lockheld.rk_vop.c:465takesevent_lockthen 470 callsdrm_crtc_vblank_get, which takesvbl_lock. The IRQ path takesvbl_lockand may signal a waiter onevent_lock. Classic A-B / B-A.
Recommendations (priority order)
- Match Linux’s
vop->eventpattern. Addvop_eventto softc. Inrk_crtc_atomic_flushonly: underevent_lock,WARN_ON(vop_event != NULL); WARN_ON(drm_crtc_vblank_get(crtc) != 0); vop_event = state->event; state->event = NULL. Gutrk_crtc_atomic_begin’s event handling to a no-op. Inrk_vop_intrafterdrm_crtc_handle_vblank, call a newrk_vop_handle_vblank(sc)that underevent_lockdoesdrm_crtc_send_vblank_event(crtc, vop_event); drm_crtc_vblank_put(crtc); vop_event = NULL. - Restore
drm_atomic_helper_wait_for_vblanksinrk_drm_atomic_commit_tailbeforecleanup_planes. The original panfrost-reset deadlock concern is mooted by the post-3378d3dQueue Panfrost reset before scheduler stop reset path (deferred to taskqueue, fence signaled before scheduler stop). - Fix the shared-IRQ ack: change
INTR_CLEAR0 = ~0toINTR_CLEAR0 = status, and gate the handler withif ((status & INTR_STATUS0_FS_INTR) == 0) return;(returnFILTER_STRAYif converted to a filter). - Add a per-commit ringbuffer sysctl for
scripts/wedge-reproto bisect the remaining cases when (1)-(3) land. Capture per commit:(timestamp, crtc_seq, atomic_begin_event_ptr, atomic_flush_event_ptr, vop_event_ptr, fs_intr_count, vbl_counter, curthread). - Drop the U-Boot-preserve early-return in
rk_crtc_atomic_enable(rk_vop.c:502-514) oncefirst_modeset_doneis set. Today every Hyprland output reconfig goes through the short-circuit and skips timing reprogramming.
Strange code
rk_vop.c:289device_printffrom interrupt context on every unhandled status bit. Under shared-IRQ floods this serializes on the console lock and looks exactly like a wedge. Replace with a counter or rate-limit.rk_vop.c:480re-enablesINTR_EN0_FS_INTRon every flush. The bit is sticky.rk_plane.c:278panic("unknown lb_mode, dst_w %d", dst_w)— any plane wider than 3840 panics the kernel. Latent footgun.rk_drm.c:140-145uses non-blockingdrm_atomic_helper_commitpaired with a custom synchronous commit_tail that dropswait_for_vblanks. That hybrid is the structural source of the lock warnings.
fusb302 + rk818 — Hard_Reset and OTG
USB-PD sink works on a fresh boot. Two open issues: Hard_Reset lands in
PE_DISABLED and stays there until manually reattached; no source-role
path for future host-mode / OTG.
Linux Hard_Reset path
drivers/usb/typec/tcpm/tcpm.c:
_tcpm_pd_hard_reset(6801) →HARD_RESET_START(5827) →SNK_HARD_RESET_SINK_OFF(5877) → waitPD_T_SAFE_0V = 650 ms→SNK_HARD_RESET_SINK_ON(5900) →SNK_STARTUP.fusb302.c::fusb302_pd_reset()(1425) writesFUSB302_RESET = RESET_PD(BIT(1)) on every received Hard_Reset to flush the chip’s protocol layer.
Linux does not have PE_DISABLED. Recovery is a timed sequence;
the BC_LVL signal is not used as a “drop everything” trigger.
Our equivalent
fusb302.c:1329-1343 HARDRST handler does TX/RX flush, zeros message-id
counters and source-PDO state, jumps directly to PE_SNK_STARTUP. It
does not write FUSB302_RESET = RESET_PD, and it does not wait
PD_T_SAFE_0V for the source’s VBUS to settle. Verified at the cited
lines.
fusb302.c:1296-1306 BC_LVL=0 watchdog — if BC_LVL_MASK is zero for
500 ms while VBUS is present, drop to
PE_DISABLED. Verified. This is the root cause of the post-Hard_Reset wedge. After Hard_Reset the source briefly drops Rp before re-applying; the watchdog sees BC_LVL=0
- VBUS=1 and slams us to
PE_DISABLED— from which the only recovery is the manualdev.fusb302.0.reattach=1sysctl.
fusb302.c:1496-1509 PE_SNK_HARD_RESET (the case where we send
Hard_Reset, not receive) does DELAY(5000) — that is 5 ms, not 5 µs
(FreeBSD DELAY() is microseconds). Linux’s
PD_T_PS_HARD_RESET = 30 ms is six times longer; we may transition to
PE_SNK_STARTUP before the chip has finished BMC-encoding the Hard_Reset
preamble.
Linux source-role → rk818 OTG path
PPP DTS:
otg_switch: OTG_SWITCHregulator inside rk818 (rk3399-pinephone-pro.dts:676), backed bydrivers/regulator/rk808-regulator.c::RK818_ID_OTG_SWITCH=RK818_DCDC_EN_REG, BIT(7).fusb0.vbus-supply = <&otg_switch>(line 883). Parent isboost_otg: DCDC_BOOST(line 664, DCDC_EN bit 5).
Source attach: TCPM enters SRC_STARTUP → tcpm_set_vbus(port, true)
→ fusb302::tcpm_set_vbus (drivers/usb/typec/tcpm/fusb302.c:759) →
regulator_enable(chip->vbus) → rk808-regulator core flips
DCDC_EN_REG bit 7. Bit 6 (SWITCH2) is unrelated — it’s a generic
switched output (RK818_ID_SWITCH2), not OTG. Our
DCDC_EN_OTG_MASK = OTG | SWITCH2 (rk818_battery.h:54) bundles
something that shouldn’t be bundled.
Recommendations (priority order)
- Fix Hard_Reset recovery. In the
INTA.HARDRSThandler atfusb302.c:1329:- Write
FUSB302_RESET = RESET_PD(BIT(1)) immediately. - Reset
bc_lvl_zero_ticks = 0so the BC_LVL watchdog doesn’t fire on the post-Hard_Reset CC blip. - Add an explicit
PE_SNK_HARD_RESET_RECOVERYstate with aPD_T_SAFE_0V = 650 msdeadline; only enterPE_SNK_STARTUPafter that.
- Write
- Skip BC_LVL=0 watchdog for ~1 s after HARDRST. Or bump from
500 ms to 1000 ms+. Today this is the actual cause of the
PE_DISABLEDwedge — a timing-band mismatch between two correct policies. - Auto-reattach as a backstop. On entry to
PE_DISABLEDafter HARDRST, schedule a 2-second callout that re-runs the equivalent ofdev.fusb302.0.reattach=1. Keep the sysctl for debug. - Drop SWITCH2 from
DCDC_EN_OTG_MASK. It’s an unrelated rail. KeepDCDC_EN_OTG = BIT(7)only. - Source-role state machine when we tackle OTG. Add
PE_SRC_*states; on PR_Swap from sink to us-as-source, callrk818_charger_set_source_role(true)(already exists atrk818_battery.c:710) fromSRC_TRANSITION_TO_DEFAULT. - Decouple fusb302 ↔ rk818 from the global-singleton call by
exposing an eventhandler (
EVENTHANDLER_DECLARE(typec_role_change, ...)) so PineTab2’s rk817 can register the same listener. - Fix
PE_SNK_HARD_RESETsend delay atfusb302.c:1507: bumpDELAY(5000)(5 ms) to apause()of 30 ms to matchPD_T_PS_HARD_RESET.
Other small bugs
PD_T_SENDER_RESP_MS = 100(line 377) — PD spec is 24-30 ms (Linux uses 60 ms). Acceptable for sink-only, fix when adding source.- Missing timers:
PD_T_SINK_REQUEST(100 ms),PD_T_PS_HARD_RESET(30 ms),PD_T_SAFE_0V(650 ms). The last is the one that matters today. - Register addresses
0x40-0x43/0x3C-0x3Fwere corrected (c11afd2rk818: correct INT_STS register addresses + decoded bit names ) and now match Linux.
rt5640 — locking, capture path, rk_tsadc
Lock-order reversal
rt5640.c:865-907 rt5640_attach runs in newbus attach context;
iicbus child-attach already holds the iicbus lock when it calls our
attach. Attach calls rt5640_init(sc) (line 899) which fires ~50
register writes via rt5640_write → iicdev_writeto(IIC_WAIT) →
iicbus_request_bus → re-acquires the iicbus mutex. Reversal.
Linux avoids this with regmap: every regmap_update_bits performs its
own bus arbitration, and codec init runs in the ASoC probe taskqueue —
not under a held bus lock.
The right fix is the Linux pattern: in rt5640_attach, replace
inline rt5640_init(sc) with taskqueue_enqueue(taskqueue_thread, &sc->init_task).
Drop RT5640_LOCK around single iicdev_writeto calls (which already
serialize on the iicbus lock); keep it only for read-modify-write
sequences in rt5640_modify.
Microphone capture (DAPM walk)
The PinePhone Pro internal mic is DMIC1 on IN1P (per the
project_rt5640_mic_plan.md memory + PPP DTS
realtek,dmic1-data-pin = <1>). Our rt5640reg.h is missing every
register needed for capture — STO_ADC_MIXER (0x27),
REC_L1/2_MIXER (0x3b/0x3c), REC_R1/2_MIXER (0x3d/0x3e),
ADC_DIG_VOL (0x1c), IN1_IN2 (0x0d), INL_INR_VOL (0x0f),
DMIC (0x75). Add these defs.
DMIC1 init sequence (append to rt5640_init):
/* PWR_ADC_L|R, then PWR_ADC_SF (stereo filter) */
rt5640_modify(sc, RT5640_PWR_DIG1, (1<<2)|(1<<1), (1<<2)|(1<<1));
rt5640_modify(sc, RT5640_PWR_DIG2, (1<<15), (1<<15));
/* DMIC1 enable, IN1P data pin per PPP DTS, divider index 3 */
rt5640_write(sc, 0x75 /*DMIC*/, (1<<15) | (1<<11) | (3<<5));
/* Stereo ADC mixer: ADC2 = DMIC1, unmute L2/R2, mute L1/R1 */
rt5640_write(sc, 0x27 /*STO_ADC_MIXER*/, 0x4040);
/* ADC digital volume ~0 dB */
rt5640_write(sc, 0x1c /*ADC_DIG_VOL*/, 0x2f2f);
For analog headset mic on IN2P (gated on jack-detect later): power
BST2 + MICBIAS1 in PWR_ANLG2, set BST2 gain in IN1_IN2, unmute
the BST2 input in REC_L2_MIXER / REC_R2_MIXER, switch
STO_ADC_MIXER to analog (0x2020).
mixer_setrecsrc is unwired
rt5640.c:487-492 returns 0 unconditionally. Wire it to flip
STO_ADC_MIXER between DMIC and analog so userland can pick the input.
rt5640_dai_trigger is a no-op for PCMDIR_REC
rt5640.c:802-819. With capture enabled, REC must gate PWR_DIG1 ADC
bits and PWR_ANLG2 BST/MB bits on/off so we don’t burn current when
no stream is open. Mirror Linux’s DAPM transitions.
Strange code
rt5640.c:172rt5640_write(sc, RT5640_GCTL1, 0x3f41)— address 0xfa, value0x3f41is unsourced. Linux’srt5640_reg_init()table does not touch 0xfa. Likely vendor BSP cargo-cult. Drop or annotate.- Many single-write paths hold
RT5640_LOCKaround oneiicdev_writeto— pointless because iicbus already serializes. Keep the lock only for multi-write sequences.
rk_tsadc — superseded by the 2026-05-01 audit
This section was written before src/sys/arm64/rockchip/rk_tsadc.c
landed. The follow-up
candidate driver audit
corrected one important detail: rk3399_tsadc_data uses v3
initialize/control hooks, but v2 callbacks for data, alarm, and
shutdown registers. The current driver should therefore mirror
drivers/thermal/rockchip_thermal.c this way:
- Read the per-chip
tsadc_trimcell from the efuse node. - Use the v2 data/trip register block:
TSADCV2_DATA(chn) = 0x20 + chn*4,TSADCV2_COMP_INT(chn) = 0x30 + chn*4, andTSADCV2_COMP_SHUT(chn) = 0x40 + chn*4. TSHUT_LOW_ACTIVEpolarity (the Honeyguide DTS had it set to HIGH, which is the false-shutdown root cause — the GPIO asserts at boot and the SoC immediately gates power).TSADCV3_AUTO_PERIOD = 1875.- RK3399S has the same TSADC IP and OTP layout as RK3399; no PPP-specific differences.
The old note’s generic v3 COMP_SHUT = 0x10c + chn*4 advice is wrong
for RK3399.
Cross-cutting priority list
These are the items most likely to convert “mostly works” into “works”:
- ◐ partial rk_vop:
wait_for_vblanks, thevop->eventdriver-side latch, and the shared-IRQ ack fix have code in the overlay. Bench still needs ascripts/wedge-reprosoak to prove the modeset-lock wedge is gone. - ◐ partial fusb302: Hard_Reset recovery code is
present for both received and locally-sent resets; bench still needs
to prove neither direction falls back to
PE_DISABLED. - ◐ partial bwfm_sdio: NVRAM trailing-NUL count, EAPOL priority, scan-version shortcut, control-credit reservation, and TX credit-window clamp are in the overlay. Remaining work is hardware soak, not first-code.
- ◐ partial bwfm_sdio + dwmmc: pinctrl audit on
SDIO DAT[1] pull, then bench the INTMASK / CLKENA reorder patch,
then add a capture point in
sdiob_claim_irqonly ifsdio_intrs=0persists. - ● working rt5640: DMIC1 has an audible
div3/edge3/adc32bench setting; analog headset capture still needs a receipt. - ◐ partial fusb302 / rk818: type-C role-change eventhandler and OTG rail sequencing are scaffolded; DWC3 host-mode bridge is next once a USB-C accessory can be tested.
Each item has a verified file:line and a falsifiable next-bench
predicate. The wedge-repro instrumentation ( 279739c scripts/wedge-repro: deterministic load generator for the modeset-lock wedge ,
e22bae6 drm: gate WARN_ON kdb_backtrace+panic behind sysctls for wedge debugging ) is the lever for #1; the existing
debug:wifi:transfer harness is the lever for #3 and #4.
Caveats
This audit was produced by reading our drivers against Linux mainline
and OpenBSD HEAD on 2026-04-30. Two corrections were made during
review of the raw findings: (a) DELAY(5000) is 5 ms, not 5 µs as the
initial draft stated; (b) the txq-repair band-aid uses BWFM_TXQLEN,
not the hard-coded 256 initially flagged. Other claims have been
spot-checked against the cited file:line locations; readers should
verify before patching.
Reproduction notes for each subsystem live in the existing recipe appendices (finishing plan, USB-C / PD verification, GPU debugging).