The PinePhone Pro talks to its USB-C port through an ON Semi FUSB302 controller on DTS &i2c4 at 7-bit address 0x22. FreeBSD logs that as shifted addr 0x44 on iicbus3. The chip implements the BMC PHY USB-PD wants — 600 kHz biphase mark coding on the CC line — and surfaces a register set for orientation detection, CC measurement, FIFO-based message TX/RX, and interrupt status. Without a driver, the phone is a 5 V / 500 mA dumb sink: any PD source on the other end advertises 9 V, 12 V, 15 V, 20 V profiles into a void, and the rk818 PMIC pulls whatever charging current the source provides at default USB Type-C levels. With Hyprland up and pkg install running and the battery already low, that’s not enough — the phone browns out.
This essay is the bring-up of an in-tree fusb302(4) driver: from a minimal cable-detect-only attachment, through a full USB-PD r3.0 sink policy engine, through a regression where the Request transmitted but Accept never came back, to a repeatable 9 V / 3 A negotiation across reboots.
What works today
fusb302(4)attaches from DTS&i2c4addr0x22; FreeBSD dmesg prints shifted addr0x44oniicbus3(device_idmatches FUSB302BMPX).- BMC PHY enables in sink-only role with retry / auto-soft-reset / auto-hardreset configured.
- CC orientation detection is reliable:
dev.fusb302.0.orientationreports1or2for cable-up vs cable-down;dev.fusb302.0.vbus_presenttracks plug events. - IRQ-driven worker: the chip’s interrupt resource drives the policy-engine kproc on actual chip events instead of a 100 Hz wall clock (
5ca6317fusb302: switch from 10ms polling to IRQ-driven worker ). Source_Capabilitiesdecode works — the driver parses Fixed Supply PDOs, picks the highest-power one at or belowdev.fusb302.0.max_voltage_mv(default 9000), and builds a Request RDO.- The TX path frames SOP,
PACKSYM | byte_count, header LSB-first, NDO×4 data bytes, JAM_CRC, EOP, TXOFF, TXON, all in one I2C burst.c3809d5fusb302: actually trigger transmission with CONTROL0.TX_START was the missing piece — TXOFF/TXON wasn’t actually firing the transmitter until that commit added theCONTROL0.TX_STARTwrite.045efa2fusb302: event-driven TX completion + per-TX retry program + COLLISION unmask made TX completion event-driven (TX_SENT / RETRY_FAIL / HARD_RESET / COLLISION) instead of speculative. - SPECREV is dynamic: GoodCRC defaults to Rev3 and downgrades only when the partner answers Rev2 (
ee8daadfusb302: dynamic SPECREV, programmed MEASURE/POWER, rx_msgid tracking +a8b584dfusb302: default GoodCRC to Rev3 (downgrade only on Rev2 partner) ). The 65 W source on the bench negotiates Rev3 cleanly; older sources still get Rev2 framing on the wire. - Standard PD timers (
tSenderResponse30 ms,tPSTransition550 ms,tNoResponse5 s) run fromcallout(9)rather than off state-age polling (12a63fefusb302: PD timers via callout instead of state-age polling ). The kproc no longer needs to wake just to age timers. dev.fusb302.0.reattach=1drops theRdpull-down for 200 ms and re-initialises the chip (94b42f7fusb302: re-detect orientation on BC_LVL drop, add reattach sysctl +ce438befusb302: reattach now drops Rd for 200ms before re-init ). A source that’s stuck in Type-C “attached” sees the sink go away and re-issuesSource_Capabilitiesfrom scratch — this is the recovery for a wedged contract that doesn’t need a physical unplug.- The rk818 charger driver gained
rk818_charger_set_input_current_ma()(ef6035efusb302+rk818: apply PD-negotiated input current to the charger ), called from the sink policy engine onTransition_Sink → Readyso the negotiated current actually changes the input limit. - The rk818 actually charges the battery now (
8da75bark818: clear OTG/SWITCH2 in DCDC_EN — battery now charges from PD ).DCDC_EN_REG[7](OTG_EN) defaulted on at boot — the chip’s own 5 V boost was back-driving VBUS while the PD source was sourcing it, latchingSUP_STS.USB_EFF=0and parking the charger FSM in no-charging. Clearing that bit during init flipped the FSM straight into CC-CV;hw.acpi.battery.ratewent from-290 mAto+242 mAand battery voltage rose ~250 mV in 5 seconds. A later Linux parity pass narrowed the mask so unrelatedSWITCH2_ENstays out of Type-C role policy (4d190f3rk818: decouple SWITCH2 from Type-C OTG ).
Three sysctls go non-zero on a successful contract:
$ sysctl dev.fusb302.0
dev.fusb302.0.vbus_present: 1
dev.fusb302.0.orientation: 1
dev.fusb302.0.negotiated_voltage_mv: 9000
dev.fusb302.0.negotiated_current_ma: 3000
dev.fusb302.0.max_voltage_mv: 9000
The rk818 PMIC’s input current limit goes from default to 3000 mA on the same transition, so pkg install plus Hyprland plus GPU stress no longer brown the PMIC out under sustained load. With 8da75ba rk818: clear OTG/SWITCH2 in DCDC_EN — battery now charges from PD in place, the battery is also genuinely charging from the PD source rather than sitting on the input rail while the chip refused to commit.
The bring-up arc
[WAR STORY]
From cable-detect to repeatable negotiation
▸ symptom
Plug a 65 W PD source into the phone. Battery indicator stays at 500 mA charge rate. PMIC brownout under any sustained load — pkg install, Hyprland startup, the GPU touching anything. Serial captures show POR resets.
▸ hypothesis 1
Stand up a minimal driver that just detects the cable orientation and reports it through sysctl. 10e8b0a fusb302: minimal USB Type-C port controller driver attaches the chip at 0x22, dumps device_id, configures the CC pull-downs (PDWN_CC1 / PDWN_CC2), and reads back STATUS0.BC_LVL to identify which CC pin saw Rp. That much works on the first try — dev.fusb302.0.orientation updates when the cable flips. But VBUS still defaults to 5 V because the chip never does anything past detection.
▸ hypothesis 2
Build the full sink policy engine. 88f04f4 fusb302: add USB-PD sink state machine is the big one — 1 000 lines added to fusb302.c. Adds the FUSB302 register map (CONTROL3, MASKA, MASKB, FIFO at 0x43, SLICE), USB-PD r3.1 message framing (16-bit headers, Fixed Supply PDO parser, Request RDO builder), the BMC PHY enable sequence (AUTO_CRC, PD r2.0 spec rev, sink-only role, retry config), the TX/RX FIFO paths, and the sink policy engine state machine following USB-PD r3.1 §8.3.3.3:
Disabled → Startup → Discovery → Wait_for_Capabilities
→ Evaluate_Capability → Select_Capability
→ Transition_Sink → ReadyPlus standard timers (tSenderResponse 30 ms, tPSTransition 550 ms, tNoResponse 5 s) and a 100 Hz polled kproc that ticks the SM, drains RX, ages timers. Untested at commit time — the phone wedged with a brownout-induced kernel panic before the kernel built with this could be deployed.
▸ hypothesis 3
Once deployed, the chip wasn’t seeing the cable at all in PE_DISABLED. 6bf3223 fusb302: drop PDWN on inactive CC line post-attach dropped the PDWN on the inactive CC line after attach so the active line could actually drive — the CC pulldowns were fighting the source’s Rp. Cable detection became reliable.
▸ hypothesis 4
RX FIFO drain wasn’t working — interrupts fired but the policy engine didn’t see Source_Capabilities. 33f0e7a fusb302: log chip status periodically + drain RX when FIFO non-empty logs chip status periodically and drains the RX FIFO whenever it’s non-empty regardless of which interrupt bit fired. 0d6a355 fusb302: accumulate interrupt bits across diagnostic dump window accumulates interrupt bits across the diagnostic dump window so a fast-clearing interrupt doesn’t get missed. 7fb5992 fusb302: clear CONTROL0.M_INT_MASK so interrupt latches actually fire clears CONTROL0.M_INT_MASK so interrupt latches actually fire.
That gets Source_Capabilities decoded.
▸ hypothesis 5
The Request transmits but the source ignores it. Watching with a logic probe: TX FIFO is being filled correctly, header looks right, but the BMC line never goes active. c3809d5 fusb302: actually trigger transmission with CONTROL0.TX_START finds it: TXOFF/TXON sets up the transmit window but the actual trigger is CONTROL0.TX_START, which we weren’t writing. One bit. Linux’s fusb302.c doesn’t write it (the TXON token in the FIFO is enough on the part variant they target); on this part, it isn’t. After this commit the source replies with Accept and then PS_RDY.
▸ breakthrough
First successful negotiation. dev.fusb302.0.negotiated_voltage_mv = 9000, negotiated_current_ma = 3000. ef6035e fusb302+rk818: apply PD-negotiated input current to the charger wires this into the rk818 charger via the new rk818_charger_set_input_current_ma() entry point, so the PMIC’s input limit actually rises to match what was negotiated. PD fast-charge confirmed.
▸ hypothesis 6
Robustness — silent timeouts shouldn’t escalate. 809d9cf fusb302: stop blasting hard resets; advertise PD r3.0 GoodCRC stops blasting hard resets when tSenderResponse expires; advertises PD r3.0 in our GoodCRC so sources don’t reject our framing. a9121e3 fusb302: forgive Request/PS_RDY timeouts; stop spurious hard resets further forgives Request / PS_RDY timeouts — keep the link, retry the request, don’t burn the policy engine to the ground on every blip. a4f63d3 fusb302: retry cable detection while in PE_DISABLED retries cable detection while in PE_DISABLED so a flaky plug-in isn’t terminal. 2d3fc5f fusb302: flush TX FIFO before queuing each PD message flushes the TX FIFO before queuing each PD message so leftover bytes from a prior failed transmission don’t corrupt the next frame.
▸ hypothesis 7
Negotiation became flaky again across reboots: Request transmitted, no Accept, and a chip dump showed CONTROL3 immediately reading back the wrong value after we wrote it. The bits we set in BMC PHY enable weren’t sticking. ee8daad fusb302: dynamic SPECREV, programmed MEASURE/POWER, rx_msgid tracking + a8b584d fusb302: default GoodCRC to Rev3 (downgrade only on Rev2 partner) made SPECREV dynamic — the chip was advertising Rev2 in our GoodCRC against a Rev3 source, and the mismatch was producing GoodCRC frames the source silently dropped. Default to Rev3 in our GoodCRC; only downgrade when the partner answers Rev2. That cleared the silent-drop case.
▸ hypothesis 8
TX completion was timing-fragile — the policy engine assumed the message had landed once TX_START was asserted and didn’t wait for a real chip event. 045efa2 fusb302: event-driven TX completion + per-TX retry program + COLLISION unmask moved TX completion to the IRQ path: handle TX_SENT, RETRY_FAIL, HARD_RESET, and COLLISION as discrete events, with a per-TX retry program. 5ca6317 fusb302: switch from 10ms polling to IRQ-driven worker meanwhile replaced the 100 Hz polling kproc with an interrupt-driven worker, so the policy engine wakes on actual chip events rather than every 10 ms. 12a63fe fusb302: PD timers via callout instead of state-age polling moved PD timers off state-age polling onto callout(9) so the kproc no longer needs to wake just to check elapsed time.
▸ breakthrough
Repeatable negotiation across reboots. The live capture at the top of this essay is what the serial console now shows on a fresh plug-in: 4-PDO source, RDO 0x2104b12c for PDO 2 (9 V / 3 A), Select_Capability → Transition_Sink → Ready, rk818 input limit raised to 3000 mA. Both dev.fusb302.0.negotiated_voltage_mv and negotiated_current_ma populate without intervention.
▸ fix
What made it click was four changes that each addressed a different layer of the stack:
callout(9)for PD timers (12a63fefusb302: PD timers via callout instead of state-age polling ) —tSenderResponseand friends fire on the kernel timer wheel. State-age polling can’t time anything tighter than the kproc wake interval, and the kproc wake interval was 10 ms (post5ca6317fusb302: switch from 10ms polling to IRQ-driven worker ), which is the same order of magnitude as the timer being measured.- Dynamic SPECREV with downgrade-on-Rev2 (
ee8daadfusb302: dynamic SPECREV, programmed MEASURE/POWER, rx_msgid tracking +a8b584dfusb302: default GoodCRC to Rev3 (downgrade only on Rev2 partner) ) — default our GoodCRC to PD r3.0, only fall back to Rev2 if the partner answers Rev2. The bench source is Rev3; advertising Rev2 made it silently drop our GoodCRC and re-sendSource_CapabilitiesuntiltNoResponsekilled the link. - IRQ-driven TX completion (
045efa2fusb302: event-driven TX completion + per-TX retry program + COLLISION unmask ) — the policy engine waits forTX_SENT/RETRY_FAIL/COLLISIONrather than assumingTX_STARTmeans done. With the polled path, aRETRY_FAILcould happen in the gap between the SM advancing and the next poll, and the SM would proceed with the wrong assumption that the message had landed. reattachsysctl withRddrop (94b42f7fusb302: re-detect orientation on BC_LVL drop, add reattach sysctl +ce438befusb302: reattach now drops Rd for 200ms before re-init ) — when a source is stuck in Type-Cattachedstate and won’t re-issueSource_Capabilities, droppingRdfor 200 ms forces a Type-Cdetached → attachedcycle on the source side. This is the explicit recovery handle when negotiation has wedged but the cable hasn’t been touched.
▸ lesson
The intermittent regression wasn’t a single bug — it was four loosely-coupled fragilities that each contributed a window of failure. State-age polling lost time precision; static Rev2 SPECREV silently mismatched a Rev3 source; speculative TX completion lost RETRY_FAIL events; and there was no recovery path short of unplugging the cable. None of these in isolation fully broke negotiation — but collectively they made the success path narrow enough to be unreliable. The general principle: event-driven where the protocol is event-driven, callout-timed where the protocol specifies wall-clock deadlines, and always provide a software-side recovery handle for state machines you don’t fully control.
[WAR STORY]
CC1 silence and the prepared-sink contract
▸ symptom
Two days after the bring-up arc landed, two new failures appeared. (a) Warm reboots sometimes failed FUSB attach with Reset failed: 2. (b) When attach succeeded, PD on CC1 decoded Source_Capabilities cleanly but every Request TX timed out with TXSTART_err=0 -> timeout, SW1=0x45. CC2 still negotiated. The 8da75ba “clear OTG and SWITCH2 unconditionally during rk818 init” change had bought charging on the bench, but the unconditional bit-bash was widening the boot mask further than it should and racing the FUSB probe.
▸ hypothesis 1
Narrow the boot mask. Linux treats SWITCH2_EN as a separate regulator the RK818 sub-PMIC needs on during certain transitions, not as the Type-C VBUS switch. 4d190f3 rk818: decouple SWITCH2 from Type-C OTG drops SWITCH2_EN from the boot-time clear and removes the bogus comment in rk818_battery.h that conflated the two. 8eac8e7 site: correct RK818 OTG switch docs fixes the same conflation in the appendix and HARDWARE notes. FUSB attach reliability comes back. PD Request TX on CC1 still fails.
▸ hypothesis 2
Manual register poking shows PD Request TX succeeds the moment DCDC_EN is forced from 0x7f (OTG clear, SWITCH2 on) to 0x3f (both clear). So the failure isn’t FUSB-side at all — RK818’s DCDC topology was somehow back-pressuring CC1 BMC signaling. But FUSB needed SWITCH2 on during its own reset/probe (otherwise the I2C Reset NACK’d). The two requirements were directly opposite — and timed: SWITCH2 had to be on during probe and off before PD TX.
▸ hypothesis 3
The order matters. Hold SWITCH2 high until FUSB has finished probing, then have FUSB tell RK818 it’s safe to drop it once the sink path is wired up. That requires a contract between the two drivers — but FUSB shouldn’t know about RK818 directly, and the polling-thread coupling that ef6035e fusb302+rk818: apply PD-negotiated input current to the charger shipped was already a code smell.
6469f2e power: add charger coordination layer adds dev/power_supply/ — a minimal coordination layer modelled on Linux’s power_supply framework but stripped to what we actually need. Two function pointers (set_input_current_limit, set_typec_source_role), a notifier (power_supply_changed), and string-keyed lookup. RK818 calls power_supply_register("rk818", &ops, cookie) at attach. FUSB calls power_supply_set_input_current_limit("rk818", mA) and a new power_supply_prepare_typec_sink("rk818") without ever naming the PMIC’s symbols. 2036021 power: fix files.arm64 patch hunk wires it into sys/conf/files.arm64. ccd3862 rk818: publish charger after lock init publishes the charger only after the rwlock init so the first IRQ doesn’t race against an uninitialised lock.
06348c6 fusb302: prepare sink path after TCPC attach adds the prepare_typec_sink callback: FUSB calls it after Reset + device_id succeeds but before the first PD TX. 9041600 rk818: hold SWITCH2 through TCPC probe makes RK818 force SWITCH2 on during early charger init so the next warm reboot reliably passes FUSB probe. d51baa8 fusb302: settle non-PD Type-C sources handles the orthogonal case of legacy 5 V walls: settle to a stable Type-C current advertisement instead of looping on PD discovery against a non-PD source.
▸ hypothesis 4
With the prepare contract in place, PD Request TX on CC1 is still timing out. Comparing register state to Linux: on attach, the Linux FUSB driver leaves Rd enabled on both CC pins and changes only the polarity / measure / TX selection in SWITCHES0. Our post-attach write was floating the inactive CC line. 036fecc fusb302: keep sink Rd on both CC pins keeps PDWN1 | PDWN2 set after attach and only swaps MEAS_CC1 vs MEAS_CC2. CC1 PD now negotiates at the protocol level on the bench.
▸ hypothesis 5
Still flaky in the field. Boot logs after 036fecc revealed the last layer:
rk818_pmu0: rk818[typec-sink]: prepared Type-C sink path (DCDC_EN 0x7f->0x3f)
fusb3020: IRQ mode (irq 84)
rk818_pmu0: rk818[irq]: cleared OTG in sink mode (DCDC_EN 0xff->0x7f)FUSB had cleared SWITCH2 via prepare_typec_sink, but the RK818 PMIC IRQ handler — newly introduced in d1b3a0b rk818: drive charger updates from PMIC IRQ , which moves charger updates off the 5 Hz polling thread onto the chip’s INT pin (with parent-IRQ plumbing in aa76411 rk8xx: fix IRQ patch hunk count + 6399b97 rk8xx: use insertion hunks for IRQ patch + 65a65a1 rk8xx: carry PMIC IRQ parent as overlay and a tunable gate in 6351db4 rk8xx: gate PMIC IRQ behind tunable ) — immediately re-armed SWITCH2 because its own enforce path only masked OTG. The contract worked exactly once, then the IRQ undid it on the very next interrupt.
544bd86 rk818: preserve prepared Type-C sink path tracks a rk818_typec_sink_prepared flag inside the PMIC. Once FUSB has called prepare_typec_sink, the sink-enforce path masks DCDC_EN_PPP_SINK_MASK (OTG | SWITCH2) instead of DCDC_EN_OTG_MASK. The flag resets on source-role transitions so a future sink attach re-prepares cleanly.
▸ breakthrough
Live capture from #138, CC1 attach (the configuration that previously failed every time):
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 regs: ... TXSTART_err=0 -> TX_SUCCESS in 7 ms (INT=0x53 INTA=0x04)
fusb3020: PD negotiated: 9000 mV / 3000 mA
rk818_pmu0: rk818: USB input current limit set to 3000 mA (idx 11, USB_CTRL=0xcb)PD now negotiates 9 V / 3 A on both CC1 and CC2 from a fresh boot. The 0xff -> 0x3f enforce line in the second printf is the proof that the sink-prepared mask is sticking across IRQs.
▸ fix
Five moving parts, each addressing a different layer:
- Coordination layer (
6469f2epower: add charger coordination layer +2036021power: fix files.arm64 patch hunk +ccd3862rk818: publish charger after lock init ) —dev/power_supply/decouples FUSB from RK818. Two function pointers, a notifier, string-keyed lookup. No driver-to-driver symbol dependencies. - PMIC IRQ migration (
d1b3a0brk818: drive charger updates from PMIC IRQ +aa76411rk8xx: fix IRQ patch hunk count +6399b97rk8xx: use insertion hunks for IRQ patch +65a65a1rk8xx: carry PMIC IRQ parent as overlay +6351db4rk8xx: gate PMIC IRQ behind tunable ) — charger state updates fire on the RK818 INT pin instead of a 5 Hz polling thread. Tunable gates the IRQ path until the rk8xx parent’s IRQ resource passes through cleanly. - Prepare-sink contract (
06348c6fusb302: prepare sink path after TCPC attach +9041600rk818: hold SWITCH2 through TCPC probe +4d190f3rk818: decouple SWITCH2 from Type-C OTG ) — RK818 holds SWITCH2 high through TCPC probe and the boot mask only clears OTG; FUSB callsprepare_typec_sinkafterdevice_idbefore the first PD TX, and that call drops both OTG and SWITCH2. - Maintain the prepared state (
544bd86rk818: preserve prepared Type-C sink path ) — once prepare has run, the IRQ-driven enforce path uses the wider mask so it doesn’t undo the prepare on the next interrupt. Reset on source-role transitions so a future sink attach re-prepares. - CC1 BMC signalling (
036feccfusb302: keep sink Rd on both CC pins ) — keepRdon both CC pins after attach (Linux pattern), changing onlyMEAS_CCx. Independent of the RK818 chain but required to make CC1 PD work at all.
d51baa8 fusb302: settle non-PD Type-C sources sits alongside this stack: it teaches FUSB to settle non-PD walls without burning cycles on PD discovery.
▸ lesson
A two-driver contract needs to survive both directions of edge-triggered state change. The first version of the contract was a single notify (“FUSB tells RK818 it’s a sink”), which RK818 honored once and then the PMIC’s own IRQ-driven re-enforcement promptly reverted. The fix wasn’t a bigger callback or a deeper notifier — it was making RK818 remember it had been prepared, so its own IRQ path would honor the wider mask going forward. Cross-driver coordination layers need both an action verb and a sticky state, not just one or the other. The corollary: when you migrate a subsystem from polling to IRQ, audit every “the polling thread eventually fixes this” assumption before merging.
What’s still open
- IRQ wiring is now in place (
5ca6317fusb302: switch from 10ms polling to IRQ-driven worker ), but the chip’s GPIO interrupt resource still allocates through the i2c bus child path — the same plumbing fix that helped goodix. Worth confirming nothing fragile is left. - Source role / source-capabilities advertisement is scaffolded but unproven. The FUSB302 side has
PE_SRC_*states and arolesysctl, and rk818 listens for type-C role-change events to flip BOOST_OTG / OTG_SWITCH. The missing piece is bench validation with an OTG accessory and then the DWC3 host-mode bridge. - Hard-reset recovery now has code for both directions: received Hard_Reset goes through
PE_SNK_HARD_RESET_RECOVERYwithPD_T_SAFE_0V_MS = 650, and locally-sent Hard_Reset waitsPD_T_PS_HARD_RESET_MS = 30before returning to startup. Bench still needs to trigger both and prove we never fall intoPE_DISABLED. - Type-C DisplayPort alt-mode is not on our roadmap yet but the silicon supports it — Linux drives DP-Alt out of the PPP’s USB-C port today. Bringing it up on FreeBSD would mean adding VDM (DiscoverIdentity / DiscoverSVIDs / EnterMode for SVID
0xff01) to the policy engine, plus wiringtcphy0_dpinto a DRM bridge so the DP lanes route through the Type-C mux. Not small, but not silicon-blocked.
The battery now actually charges from PD, end-to-end. The earlier “rk818 charge-current bump” follow-up turned out to be a misdiagnosis: the charge-current target was never the cap. The chip was refusing to charge at all because DCDC_EN_REG.OTG_EN defaulted on at boot, pushing the chip’s own 5 V boost back onto VBUS while the PD source was sourcing it — SUP_STS.USB_EFF latched to 0 (“USB fault”) and the FSM stayed in no-charging. Clearing that bit during rk818 init ( 8da75ba rk818: clear OTG/SWITCH2 in DCDC_EN — battery now charges from PD ) flipped the FSM straight into CC-CV; battery voltage rose ~250 mV in 5 seconds. The shape of that fix evolved: 4d190f3 rk818: decouple SWITCH2 from Type-C OTG narrowed the boot mask back to OTG-only because SWITCH2_EN is a separate regulator (FUSB needs it on through probe), and the second war story above replaces the unconditional clear with a per-state contract — RK818 holds SWITCH2 through TCPC probe, FUSB calls prepare_typec_sink after device_id, and a sink-prepared flag makes the IRQ-driven enforce path honor that wider mask forever after. Full hypothesis-by-hypothesis walkthrough in appendix: rk818 not actually charging.
For verification recipes, BC_LVL decoding, and how to use reattach to wake a stuck source, see appendix: USB-C / PD verification.