Appendix · story

Work log: PD CC1, sensor calibration, power domains

A long Saturday-night session that closed several open holes and identified the next round.

The night started on the USB-PD CC1 hang from the previous handoff and ended with a fresh rk_power_domain driver and a recalibrated rk_tsadc. Nine driver-side commits, one rolled-back experiment, four partial → working promotions in the components matrix, and one one-line rc.conf fix.

2026-05-02 evening

USB-PD on CC1 finally negotiates 9 V / 3 A. The handoff from earlier in the day had 036fecc fusb302: keep sink Rd on both CC pins deployed but PD was still failing because the RK818 PMIC IRQ enforcement path was re-arming SWITCH2_EN immediately after fusb302(4) cleared it via prepare_typec_sink. 544bd86 rk818: preserve prepared Type-C sink path added a rk818_typec_sink_prepared flag so the IRQ-driven enforce path uses the wider mask (OTG | SWITCH2) once prepare has run, locking DCDC_EN at 0x3f for the rest of the boot. Live capture from CC1 attach:

rk818_pmu0: rk818[typec-sink]: prepared Type-C sink path (DCDC_EN 0x7f->0x3f)
rk818_pmu0: rk818[irq]: enforced sink mode (mask=0xc0, DCDC_EN 0xff->0x3f)
fusb3020: negotiated PD Rev3 (partner=3, SW1=0x45 C3=0x05)
fusb3020: Source_Capabilities: 4 PDOs (5 V/2.4 A, 9 V/3 A, 15 V/3 A, 20 V/2.25 A)
fusb3020: Sending Request for PDO 2 (RDO=0x2104b12c)
fusb3020: TX_SUCCESS in 7 ms
fusb3020: PD negotiated: 9000 mV / 3000 mA
rk818_pmu0: rk818: USB input current limit set to 3000 mA (idx 11, USB_CTRL=0xcb)

Both CC orientations now negotiate. Essay 16 grew a second WarStory (“CC1 silence and the prepared-sink contract”) covering the full bring-up arc — power-supply coordination layer, PMIC IRQ migration, sink-prepare contract, and the IRQ enforce mask preservation.

2026-05-02 late evening — bench validation pass

Ran mise run bench:phone --with-actuators to see what survived the night’s deploys. The results split cleanly:

StepStatusNote
vibrator-rumble ● working new gpio_vibrator(4) path; you felt the 5 s rumble
sensors ● working post-fixes; see below
sgm3140-status + -pulse ● working three 250 ms strobes verified by eye
modem-status ● working after mise run modem:power:phone
spi-flash ○ blocked rk_spi RDID skew, see below
WiFi fallback matrix ◐ partial known SDIO IRQ wedge at 8 MiB host→phone

Three concrete fixes landed from this pass:

  • sgm3140 overlay didn’t reach /dev/led/flash / /dev/led/torch because sys/modules/dtb/rockchip/Makefile never listed sgm3140.dtso in its DTSO= set. b7ec39b dtb: build sgm3140 overlay as part of the rockchip dtb modules patches the Makefile so buildkernel emits sgm3140.dtbo. Phone’s /boot/loader.conf fdt_overlays line was extended from bcm-hostwake to bcm-hostwake,sgm3140. After redeploy: /dev/led/flash, /dev/led/torch, and the bench’s flash + torch + strobe pulses all work end to end.
  • bwfm runtime knobs were missing because mise run build-kernel doesn’t rebuild .ko modules and bwfm_sdio.ko was stale. mise run build-module bwfm_sdio + manual deploy gave us dev.bwfm_sdio.0.poll_fallback_hz and irq_watchdog_hz live. The fallback matrix can now run; it does run; it shows the same “host SDIO IRQ stays flat under sustained transfer” pattern that essay 15 already calls out.
  • The phone was running an Apr-22 dtb. The compass node, the &tsadc { status = "okay" } re-enable, and several other recent dts changes only existed in the May-02 build. Deploying the fresh dtb unblocked rk_tsadc attach and let magnetometer(4) even reach device_attach.
2026-05-02 late evening — magnetometer

The AF8133J ack’d over i2c only after deasserting its active-low reset GPIO (<&gpio1 RK_PA1 GPIO_ACTIVE_LOW>) — verified by hand with gpioctl before writing any driver code. The driver had no reset handling at all.

31c69f6 magnetometer: deassert reset-gpio before first I2C read added the gpio_pin_get_by_ofw_property("reset-gpios") lookup + a 10 ms low/high pulse, but the polarity was backwards: gpio_pin_set_active(true) drives an ACTIVE_LOW pin low (asserted = in reset). My initial pulse ended in the asserted state, so the chip stayed held in reset. 3b6cb7b magnetometer: fix reset-gpio polarity in attach pulse swapped the calls. After:

magnetometer0: af8133j detected (reg[0x01]=0x00 reg[0x00]=0x5e)
$ sysctl dev.magnetometer.0
dev.magnetometer.0.x_raw: -1553
dev.magnetometer.0.y_raw: -211
dev.magnetometer.0.z_raw: 992
dev.magnetometer.0.chip_variant: af8133j

Promoted to ● working in the components matrix.

2026-05-02 late evening — rk_tsadc trim

The TSADC reads about 30 °C low at idle on the PinePhone Pro (raw code ~466 maps to −3 °C via the rk3399_code_table, but the chip is clearly warmer). The original driver had a TODO to “wire a tsadc-trim cell from rk_efuse”; investigation tonight confirmed Linux’s RK3399 thermal driver applies no per-chip trim and the standard RK3399 efuse binding has no such cell. So we made the offset a writable + tunable knob instead.

7613729 rk_tsadc: make trim_offset_mc writable + tunable changed dev.rk_tsadc.0.trim_offset_mc from CTLFLAG_RD to CTLFLAG_RW and added a TUNABLE_INT_FETCH("hw.rk_tsadc.trim_offset_mc") so the loader can set it. Phone’s /boot/loader.conf now has hw.rk_tsadc.trim_offset_mc=-30000. Idle temps:

$ sysctl dev.rk_tsadc.0.cpu_temp dev.rk_tsadc.0.gpu_temp
dev.rk_tsadc.0.cpu_temp: 33750   # 33.75 °C, was 5.55 °C uncalibrated
dev.rk_tsadc.0.gpu_temp: 29444   # 29.44 °C, was 0.62 °C uncalibrated

Both looked sensible at the time. Later correction, 2026-05-05: the root cause was not trim. Linux’s RK3399 path reports 1024 - tsadc_q; our driver had been interpreting the native q value directly. ede7969 rk_tsadc: derive RK3399 q-select code in software fixed that transform in software, and the phone now runs with trim_offset_mc=0. Keep this entry as the false trail that led to the right comparison.

2026-05-03 early morning — components matrix sweep

Audited the matrix against tonight’s evidence and promoted four more rows. Each had a working driver doing the kernel-side job; “partial” was actually about userspace policy that lives outside the driver. Fresh notes on each per-component page spell out what specifically remains for nice integration:

  • Vibrator — driver works; needs a feedbackd-shaped daemon to map semantic events (“notification”, “ringtone”) to FF_RUMBLE parameters.
  • MPU-6500 — driver works with IRQ-driven cache (irq_count=7516089 after 18 min uptime); the userland phone-orientation sysctl poller ships and drives sway/Hyprland rotation. In-kernel EV_ABS would be cleaner long-term.
  • STK3311 — driver works with thresholded near/far IRQ; userland phone-proximity script ships (commented-out in compositor configs). Needs ALS curve fit for backlight scaling.
  • RK3399 eFuse — driver reads all five leakage cells + cpu_id + wafer_info live. The TSADC trim consumer turned out not to exist; CPU/GPU DVFS would be the next plausible consumer if FreeBSD ever grows Rockchip cpufreq glue.

Matrix totals after the sweep: ● working 32 / ◐ partial 7 / ○ blocked 0 / · not started 6.

Also softened essays 14 and 15 + component-rk-vop so the modeset-lock and GPU-stress wedges are flagged as unconfirmed on #141+ rather than asserted as known-broken — neither has reproduced in casual use since the rk_vop follow-up arc landed ( 21b88a8 rk_vop: ack only fired interrupts; restore wait_for_vblanks , 36d4794 rk_vop: latch page-flip events until FS_INTR , 3f72b0e rk_vop: roll back unproven vblank latch ).

2026-05-03 early morning — rk_spi RDID skew (open)

Investigated why mx25l(4) reads JEDEC RDID as 25 70 18 instead of GD25LQ128’s expected c8 60 18. The capacity byte (0x18) is correct; the leading two bytes are wrong but stable across reboots and clocks. Eliminated:

  • Clock speed (10 MHz vs 1 MHz: same bytes)
  • CPHA / clock phase (mode 0 vs mode 1: same bytes, with spibus correctly reporting mode 1)
  • Stale mx25l JEDEC table (gd25lq128 is already in our patch as 0xc8 0x6018)

Diff against Linux’s drivers/spi/spi-rockchip.c showed FreeBSD’s rk_spi never writes CTRLR1 (the per-transfer RX frame count register). Linux writes CTRLR1 = (frame_count - 1) before every transfer. CTRLR1 left at default 0 means “expect 1 RX frame”, which fits the symptom: only the last RX byte arrives intact.

f0768d4 rk_spi: write CTRLR1 frame count before each transfer tried adding the CTRLR1 write inside rk_spi_xfer_buf bracketed by rk_spi_enable_chip(0) / rk_spi_enable_chip(1). Patch built, deployed — and the kernel hung silently after the loader’s “Autoboot in 1 seconds” menu (CRU hack means kernel boot output never reaches serial, only the EFI framebuffer). Reverted in 18eeb81 Revert "rk_spi: write CTRLR1 frame count before each transfer" .

Likely failure mode: enable_chip(0) mid-transfer, after set_cs(true) had already been called, left the controller in a bad state. Linux’s order is disable → write CTRLR0 + CTRLR1 + BAUDR → enable → assert CS → fill TX FIFO; our wrapper-style insertion happened between “assert CS” and “fill TX FIFO”. The corrected next-session plan is to refactor rk_spi_transfer to do the CTRLR1 write before enable_chip + CS-assert, matching Linux’s order. The known-bad kernel is preserved as /boot/kernel/kernel.bad-rk_spi on the SD for forensics.

GD25LQ128 stays ◐ partial until that refactor lands.

2026-05-03 early morning — rk_power_domain (sysctl-only)

First-cut port of drivers/pmdomain/rockchip/pm-domains.c for the RK3399. FreeBSD has no power-domain framework at all — no pwr_domain_if.m, no consumer parsing in simplebus, no peer driver expects power-domains = <&power N> to do anything. Rather than build the whole framework tonight, the new driver takes a sysctl-first shape: it implements the actual register sequencing (bus-idle handshake before power-off, power-on before idle release, mirroring Linux’s rockchip_pmu_set_idle_request() + rockchip_do_pmu_set_power_domain()) and exposes every one of the 27 RK3399 domains under dev.rk_power_domain.0.<name>.{power,idle}_state (RW) plus _status (read-only mirror).

Verified live with a safe round-trip on the RGA domain (no driver, no consumer, has full pwr+idle handshake):

$ sysctl dev.rk_power_domain.0.rga.power_state_status
dev.rk_power_domain.0.rga.power_state_status: 1     # boot state: on
$ sudo sysctl dev.rk_power_domain.0.rga.power_state=0
dev.rk_power_domain.0.rga.power_state: 1 -> 0
$ sysctl dev.rk_power_domain.0.rga.{power,idle}_state_status
dev.rk_power_domain.0.rga.power_state_status: 0     # off, idle latched
dev.rk_power_domain.0.rga.idle_state_status: 1
$ sudo sysctl dev.rk_power_domain.0.rga.power_state=1
dev.rk_power_domain.0.rga.power_state: 0 -> 1
$ sysctl dev.rk_power_domain.0.rga.{power,idle}_state_status
dev.rk_power_domain.0.rga.power_state_status: 1     # back on, un-idled
dev.rk_power_domain.0.rga.idle_state_status: 0

All 27 domains start pwr=1, idle=0 — U-Boot really does leave everything on.

The driver is in src/sys/arm64/rockchip/rk_power_domain.c ( 227e58b rk_power_domain: sysctl-only RK3399 PMU power-domain controller + Makefile fixups 42dbab2 files.arm64: fix hunk count after rk_power_domain addition + 84175f0 rk_power_domain: use OF_decode_addr instead of fdtbus_bs_tag ). Promoted from · not started to ● working in the matrix; the per-component page spells out the four-step plan to plug it into a real framework so peer drivers can request domains via DT.

2026-05-03 — last micro-fix

WiFi DHCP race fix: the previous handoff and tonight both hit a state where wlan0 associated to the AP (UP, RUNNING, ssid Ether, bssid …, AES-CCM ucast) but had no inet line. Root cause is the ifconfig_wlan0="WPA SYNCDHCP" + background_dhclient_wlan0="NO" combo: bwfm SDIO completes WPA association asynchronously, after rc.d/bwfm_sdio has created the wlan0 interface, and SYNCDHCP times out (~30 s) before WPA finishes.

4e6ffe4 overlay/rc.conf: switch wlan0 to background DHCP switches overlay/etc/rc.conf to WPA DHCP + background_dhclient_wlan0="YES". Live tonight. Boot doesn’t wait on DHCP; the lease lands a few seconds after login. ue0’s static inet stays SYNCDHCP-style (it boots ready synchronously).

Remaining gaps — prioritized

What’s left, ranked by bang-for-buck given where we are now:

Near-term, bounded (1–2 sessions each)

  1. rk_spi CTRLR1 refactor. Move the CTRLR1 = len-1 write to before enable_chip + CS-assert in rk_spi_transfer, matching Linux’s order. Should fix the JEDEC RDID skew and unblock /dev/flash0. Then rewrite tools/debug-spi-flash-phone.sh to use dd instead of the nonexistent flashctl(8). Detail in project_spi_flash_rdid_skew.md memory note.
  2. Re-verify modeset-lock + GPU-stress on #144. Run scripts/wedge-repro and either close the WarStory in essay 8 or capture a current trace. Site already says “needs re-verification” — actually doing it gets us out of “unconfirmed” purgatory.

Medium scope (2–4 sessions)

  1. WiFi SDIO IRQ enablement. Essay 15 has the full plan: track sdio_intr_enabled in DWMMC softc, clear CLKENA_LP while SDIO IRQ is armed, issue update-clock command after CLKENA change. The transfer matrix is the falsifier. Highest user-facing impact of anything left; current state is “associates and gets a lease, but bwfm_irq=0 for the entire transfer and 8 MiB host→phone wedges.”
  2. Power-domain framework. Define pwr_domain_if.m (modeled on clk_if.m / hwreset_if.m), teach simplebus to parse power-domains = <&power N>, add request-counting, then have tonight’s rk_power_domain become a real consumer. Reusable across the entire Rockchip family. Lays the groundwork for…
  3. Suspend-to-RAM. Needs the framework above + CPU cluster power-down + DDR self-refresh + RK818 PMIC suspend. Real workstream but starts unlocking actual phone-as-phone use.

Bigger / interesting

  1. Rockchip IOMMU. Needs an ARM platform IOMMU framework first (FreeBSD has none — iommu(9) today is x86 + POWER). Multi-session, real architectural work, reusable upstream.
  2. USB-C DisplayPort alt-mode. VDM (DiscoverIdentity / DiscoverSVIDs / EnterMode for SVID 0xff01) in fusb302(4) + tcphy0_dp wired into a DRM bridge. Linux drives DP-Alt out of the PPP’s USB-C port today — silicon supports it, FreeBSD just doesn’t. Multi-session.
  3. OTG / host mode. Re-implement source-role state machine in fusb302(4) (was scaffolded in e0ad0ad fusb302 + rk818: source-role / OTG path scaffold , rolled back in 6cb29ab usb-c: roll back post-wednesday role scaffold ) + DWC3 device→host mode-switch. Blocked on FreeBSD’s lack of extcon framework; Linux uses extcon notifications to drive the DWC3 mode swap.

Userspace polish (small but proves the kernel work)

Done as of this session — for the record