Appendix · story

Why the rk818 says "discharging" with PD plugged in

USB present, CHRG_EN set, FSM frozen at state 0 — resolved: DCDC_EN.OTG was forcing 5 V back onto VBUS

USB-PD negotiation is solid (see essay 16 and the USB-C / PD verification recipe). The fusb302 reaches Transition_Sink → Ready, calls into rk818_charger_set_input_current_ma() ( ef6035e fusb302+rk818: apply PD-negotiated input current to the charger ), and the rk818’s USB_CTRL_REG[3:0] gets the 3000 mA index. Every visible bit says “we are charging.” Net battery current says “we are not.”

This page collects the evidence and a ranked plan for figuring out why the rk818 charger FSM stays in state 0 (discharging) with a clean PD contract on VBUS.

What we observed

A clean 9 V / 3 A negotiation on a fresh boot, captured with the post-PD register dump added in d4e3c83 rk818: decode SUP_STS FSM + dump USB_CTRL in charger status :

PD negotiated: 9000 mV / 3000 mA
rk818: USB input current limit set to 3000 mA (idx 11, USB_CTRL=0x4b)
rk818: charger status: SUP_STS=0x8e (fsm=discharging, bat_exist=1) \
       USB_CTRL=0x4b C1=0xe3 (chrg_en=1, ilim_idx=0x3) \
       C2=0x4a C3=0x0e
hw.acpi.battery.rate: -235        # mA out of the battery
hw.acpi.battery.life: 4           # %

▸ symptom

With a 9 V / 3 A PD source plugged in and CHRG_EN asserted in CHRG_CTRL_REG1, the rk818’s charger state machine does not advance out of state 0 (discharging). Battery net-discharges at ~235 mA exactly as it would with no charger. Plugging or unplugging the PD cable does not change SUP_STS[6:4].

Decoding the dump

The bit decodings below come from src/sys/dev/iicbus/pmic/rockchip/rk818_battery.c:107 (existing battery-poll FSM table) and src/sys/dev/iicbus/pmic/rockchip/rk818_battery.h for the register addresses (SUP_STS_REG=0xA0, USB_CTRL_REG=0xA1, CHRG_CTRL_REG1=0xA3, CHRG_CTRL_REG2=0xA4, CHRG_CTRL_REG3=0xA5).

SUP_STS = 0x8e (1000_1110)

bitsvaluemeaning
71battery present
6:40charger FSM = discharging (0=discharging, 1=dead-charge, 2=trickle, 3=CC-CV, 4=termination)
3:00xeunknown nibble — probably USB/DC presence + fault/online flags

USB_CTRL = 0x4b (0100_1011)

bitsvaluemeaning
70USB_VLIM_EN (?) — not asserted
61USB_ILIM_EN (?) — input current limit enabled
5:40reserved / VLIM-select
3:00xb = 11USB_ILIM_SEL index 11 → 3000 mA, per the table at rk818_battery.c:430

(The existing USB-C / PD recipe section 6 currently labels bit 7 as USB_ILIM_EN. That is wrong — 0x4b has bit 6 set, not bit 7. The bit 7 vs bit 6 split between USB_VLIM_EN and USB_ILIM_EN is one of the things we need to nail down against Megi’s driver.)

CHRG_CTRL_REG1 = 0xe3 (1110_0011)

bitsvaluemeaning
71CHRG_EN asserted
6:40b110charge max-V select (likely 4.30 V — needs datasheet confirmation)
3:00x3charge current bucket index (~1500–1600 mA, matches DT default of 1.5 A)

CHRG_CTRL_REG2 = 0x4a

bitsvaluemeaning
7:60b01termination current = 150 mA (matches DT)
5:00x0atimer / other charge-loop settings

CHRG_CTRL_REG3 = 0x0e — charge timeout enables (init writes this; the PD-side current-limit path re-asserts it at rk818_battery.c:473).

So: charger-enable asserted, input limit programmed, valid charge current and termination — and the FSM is parked at state 0.

Hypotheses, ranked — what we tried and ruled out

The original investigation enumerated six theories before we had the live-poke infrastructure to A/B test them. None of them turned out to be the answer; the real cause appears as the seventh entry below. They’re preserved here because the path is the interesting part — and because the work to falsify them (decoding SUP_STS[3:0], naming the INT_STS family, confirming USB_VLIM_EN’s position, etc.) was independently necessary infrastructure.

▸ hypothesis 1

USB_VLIM_EN (bit 7 of USB_CTRL_REG) is not set, and the chip refuses to recognise VBUS as a valid charging source until it is.ruled out.

The theory was that the chip distinguishes “VBUS is electrically present” from “VBUS is a sane voltage I am willing to charge from,” and that we were missing the latter enable. f8e26a1 rk818: live debug — periodic dump, peek/poke sysctls, set USB_VLIM_EN added an init-time write that asserts USB_VLIM_EN; USB_CTRL then reads back 0xcb (bits 7, 6, 3, 1, 0 set) instead of 0x4b. The FSM did not move. SUP_STS stayed 0x86, CHG_STS stayed 0 (no-charging), and hw.acpi.battery.rate stayed negative. Bit assignment confirmed correct against the datasheet; bit not load-bearing for this fault.

▸ hypothesis 2

CHRG_CTRL_REG1.CHRG_EN (bit 7) is necessary but not sufficient — CHRG_CTRL_REG3 has gating bits we don’t understand.ruled out.

We poked every reasonable bit pattern into CHRG_CTRL_REG3 from userland once dbg_reg.poke landed ( f8e26a1 rk818: live debug — periodic dump, peek/poke sysctls, set USB_VLIM_EN ). No combination unstuck the FSM. The register doesn’t gate “start the charger loop” — it’s timeout config, exactly as Honeyguide’s init implies.

▸ hypothesis 3

Charger protection FSM tripped on something — over-temp, over-current, or weird VBUS — and a latched fault blocks charging.ruled out.

After c11afd2 rk818: correct INT_STS register addresses + decoded bit names corrected the INT_STS register addresses (they had been inheriting the RK808 layout — wrong for RK818) and re-decoded all the bit names from the RK818-1 datasheet V1.0 pp. 70–77, no fault bits were ever set in either INT_STS1 or INT_STS2 across the whole investigation. The chip was not in a latched-fault state. It just refused to charge.

▸ hypothesis 4

Battery voltage too low — the chip is stuck in a special low-voltage path that requires different setup.ruled out.

The FSM reads as CHG_STS=0 (no-charging), not 1 (dead-charge). Battery voltage was around 3.6 V for most of the investigation — well above any plausible dead-charge threshold. Reading the datasheet later confirmed: CHG_STS=0 is unambiguous “the charger is not running,” not “the charger is in a special low-voltage mode.”

▸ hypothesis 5

SUP_STS bits 3:0 carry a fault code we haven’t decoded.partially correct, but not the answer.

SUP_STS[3:0] is meaningful — c11afd2 decoded it from the datasheet as BAT_EXS, CHG_OK, USB_EFF, BAT_OK (or similar — see the commit). The decode showed USB_EFF=0 (“USB is not an effective charging input”) even with VBUS clearly present at 9 V. That was the smoking gun. But the cause of USB_EFF=0 was not in the INT_STS family or in any latched fault — it was in DCDC_EN_REG, an entirely different register. So decoding SUP_STS[3:0] told us what was wrong, not why.

▸ hypothesis 6

A “charger online” interrupt latch needs to be read and cleared first.ruled out.

Once INT_STS1 / INT_STS2 were correctly addressed and decoded ( c11afd2 rk818: correct INT_STS register addresses + decoded bit names ), nothing was ever pending. The chip was not waiting on an unacknowledged event.

▸ hypothesis 7

DCDC_EN_REG (0x23) defaults to 0xff at boot — bit 7 (OTG_EN) pushes 5 V from the chip’s own boost back onto VBUS, which is also being driven by the PD source. The chip’s USB sense block sees the conflict and latches SUP_STS.USB_EFF=0.confirmed; this was the answer.

This hypothesis didn’t exist on the original list. It surfaced once the live dbg_reg.peek infrastructure ( f8e26a1 rk818: live debug — periodic dump, peek/poke sysctls, set USB_VLIM_EN ) made it cheap to scan all of the rk818’s enable registers and notice that DCDC_EN_REG was at 0xff — every rail enabled, including the OTG 5V boost. With a PD source already sourcing VBUS in sink mode, that bit creates a back-drive conflict on the VBUS rail that the chip flags as a USB fault.

Linux drives that bit dynamically — its otg5v / RK818_ID_OTG_SWITCH regulator + Type-C role-change notifier toggles DCDC_EN[7] on/off as the port flips between source and sink roles. FreeBSD’s PD driver is sink-only for now; init just unconditionally clears it. The later Linux parity audit found that our original DCDC_EN[7:6] mask was too broad: SWITCH2_EN is a separate regulator, not part of Type-C VBUS policy ( 4d190f3 rk818: decouple SWITCH2 from Type-C OTG ).

The actual answer: DCDC_EN.OTG stuck on

▸ breakthrough

With the dbg_reg.poke sysctl from f8e26a1 rk818: live debug — periodic dump, peek/poke sysctls, set USB_VLIM_EN , we could clear DCDC_EN[7] from userland without a rebuild and watch the chip’s state in real time. The first poke flipped everything:

# before
SUP_STS=0x86 (chg=no-chrg, usb_eff=0, bat_exs=1)
hw.acpi.battery.rate=-290     # mA out of the battery
DCDC_EN_REG=0xff

# poke
sysctl dev.rk818.0.dbg_reg.poke=0x23,0x7f

# after
SUP_STS=0xb7 (chg=CC-CV, usb_eff=1, bat_exs=1, ...)
hw.acpi.battery.rate=+242     # mA into the battery
DCDC_EN_REG=0x7f

Battery voltage rose ~250 mV in 5 seconds. The FSM walked straight from discharging into CC-CV charging. USB_EFF flipped 0 → 1 the instant OTG_EN cleared.

▸ fix

Clear DCDC_EN_REG[7] (OTG_EN) unconditionally during rk818 charger init ( 8da75ba rk818: clear OTG/SWITCH2 in DCDC_EN — battery now charges from PD , narrowed by 4d190f3 rk818: decouple SWITCH2 from Type-C OTG ). FreeBSD’s USB-PD driver is sink-only for now; we have no business driving 5 V back onto VBUS, and the chip’s USB sense block can’t tolerate it being driven from both sides. Linux’s drivers/regulator/rk808-regulator.c exposes otg5v and a Type-C role-change notifier flips it on for source mode; we can mirror that path when we add source-role support, but until then the right default is “off.”

The supporting infrastructure that made this find possible:

▸ lesson

The investigation enumerated six hypotheses before the actual answer surfaced. That isn’t a failure of the hypotheses — it’s a feature of an opaque PMIC: the six theories were the reasonable theories given what we knew, and falsifying each one required infrastructure (INT_STS decoded, SUP_STS[3:0] named, USB_VLIM_EN confirmed) that we needed regardless. The actual root cause was in a register class (DCDC_EN) we hadn’t even been looking at.

The generalisable lesson is the value of the live dbg_reg.poke sysctl. Until f8e26a1 rk818: live debug — periodic dump, peek/poke sysctls, set USB_VLIM_EN , every hypothesis cost a kernel rebuild + reboot

  • re-establish-PD cycle to test — call it 10 minutes per bit toggle, and that’s on the build host being warm. With dbg_reg.poke, testing a register change costs a single sysctl write and a dmesg | grep — a couple of seconds. That turned what would have been a multi-session hypothesis-elimination grind into a single-session “scan the registers, spot the anomaly, poke the bits” find.

For any opaque programmable peripheral where the bit semantics aren’t fully understood: build the live-poke harness first, before the proper fix. The harness is throwaway and the fix is the deliverable, but the harness is what makes finding the fix tractable.

Reproducing the find from a running phone

The same poke that fixed the bug is still useful as a diagnostic on any suspect rk818 — confirm the chip is in the conflict state, then clear it in-place. If clearing DCDC_EN[7] doesn’t move USB_EFF 0→1, the fault is not the OTG conflict and the older hypotheses come back into play.

╞═ confirm DCDC_EN.OTG isn't sneaking back on ═╡
# read DCDC_EN (0x23)
ssh pinephone 'sudo sysctl dev.rk818.0.dbg_reg.peek=0x23'

# read SUP_STS — usb_eff and chg fields are decoded in dmesg
ssh pinephone 'sudo dmesg | grep "rk818: charger status" | tail -1'

# if DCDC_EN reads with bit 7 set and usb_eff=0, clear only OTG_EN:
ssh pinephone 'sudo sysctl dev.rk818.0.dbg_reg.poke=0x23,0x7f'

# observe SUP_STS flipping to chg=CC-CV, usb_eff=1, and battery rate going positive
ssh pinephone 'sysctl hw.acpi.battery.rate hw.acpi.battery.life'

Where the fix lives