Appendix · story

Work log: USB-C source role and host-mode bring-up

FUSB302 can be guarded before sourcing VBUS; DWC3 can now be forced into host mode for controlled hub tests.

2026-05-07 — source mode needed a battery guard

The USB-C hub test started from a bad assumption: enabling Type-C source role is not just a CC experiment. On the PinePhone Pro it also asks the RK818 to source 5 V onto VBUS, and that can pull the battery down hard while the phone is already hot or low.

628e536 fusb302: poll source CC attach state moved the FUSB302 source path away from the sink toggle engine and into an explicit source-side comparator poll: advertise default Rp on both CC pins, sample CC1/CC2, and expose the result as dev.fusb302.0.source_cc1 / source_cc2. Kernel #196 proved the unsafe half first: dev.rk818_pmu.0.rk818_source_role entered source mode and VBUS rose, but the phone was discharging and the FUSB302 did not yet report a clean Rd attach.

The next fix was a guard. f754656 typec: guard source role on battery voltage added dev.fusb302.0.source_min_voltage_mv, defaulting to 3600 mV, and asks the shared power_supply layer for the cached RK818 battery voltage before enabling source mode. Two follow-up commits fixed the shape of that guard: c3ed085 rk818: avoid live i2c in voltage callback made the RK818 voltage callback use the cached battery sample instead of doing live I2C from the sysctl path, and ebd8587 fusb302: return non-retry error for source guard returned EPERM instead of EAGAIN so sysctl(8) did not retry in a tight loop.

◐ partial The guard is proven. With the phone around 3570 mV, raising source_min_voltage_mv to 3700 made sysctl dev.fusb302.0.source_role=1 fail immediately with Operation not permitted; dev.fusb302.0.source_role stayed 0 and dev.rk818_pmu.0.rk818_source_role stayed 0.

2026-05-07 — hub-attached boot exposed a FUSB302 attach race

The first guarded kernel still found a boot-time failure mode when the hub was already attached. The controller node appeared under iicbus3, but fusb3020 returned from attach before creating its sysctls:

fusb3020: Reset failed: 2
fusb3020: Initialization failed: 2
device_attach: fusb3020 attach returned 2

That left the phone in DWC3 gadget mode with no Type-C controls at all. 4e5d139 fusb302: retry transient i2c transfer failures added short retries around the FUSB302 I2C register read/write helpers. Kernel #201 then booted with the hub attached and attached the controller normally:

fusb3020: FUSB302 version 8 rev 1
fusb3020: Cable detected: CC1 (normal), VBUS=yes, CC1_lvl=1
dev.fusb302.0.orientation: 1
dev.fusb302.0.vbus_present: 1
dev.fusb302.0.typec_current_ma: 500

The same boot also made the remaining gap obvious. The hub still talked to the phone as a USB gadget partner:

snps_dwc3_fdt0: Configured for device mode
ugen4.1: <DWC3 Gadget Root HUB> at usbus4
2026-05-07 — DWC3 needs host-mode experiments without losing the default gadget lifeline

The PinePhone Pro DTS keeps &usbdrd_dwc3_0 at dr_mode = "peripheral" so the day-to-day ssh pinephone USB-Ethernet path survives. That is correct for normal development, but it prevents a hub from enumerating even if the Type-C controller and RK818 are asked to act as source.

16a9f5e dwc3: add host-mode boot override added a narrow boot-time override:

hw.dwc3.force_host=1

The default remains 0, and kernel #202 proved that default did not regress the gadget path:

hw.dwc3.force_host: 0
snps_dwc3_fdt0: Configured for device mode
snps_dwc3_fdt0: DWC3 gadget attached (device mode)

A one-boot nextboot -e hw.dwc3.force_host=1 test was then attempted with the hub attached. That boot did not return over WiFi and did not produce a post-boot serial login in the first capture window. That was recorded as a negative receipt at the time. The 2026-05-08 follow-up proved the hang was an observation problem, not a code problem: the forced-host kernel actually completes attach, but the first run was captured too short and assumed dead.

2026-05-08 — forced host mode actually works, and the hub really enumerates

The starting point was the suspicion that the prior hw.dwc3.force_host=1 boot had hung. The first job was to make the kernel produce a forensic receipt no matter how the boot ended.

5a1cb12 dwc3: log host-attach state unconditionally made three changes to the host attach path in src/sys/dev/usb/controller/dwc3/dwc3.c:

  • The U-Boot register snapshot (PRE-RESET: GCTL=… GUSB2PHYCFG=…) used to fire only in device mode. It now fires for both paths.
  • A new POST-HOST-CONFIG: GCTL=… GUSB2PHYCFG=… GUSB3PIPECTL=… GUCTL=… GUCTL1=… line prints right after snps_dwc3_configure_host() and snps_dwc3_do_quirks(), so the register values handed to xHCI are visible.
  • snps_dwc3_attach_xhci() now ends with one of two symmetric lines: Configured for host mode (xHCI attached) on success, or Host mode attach failed: %d on error. The per-step prints already inside attach_xhci() give the cause.

Kernel #203 (built and deployed via mise run kernel:deploy:phone on the eMMC root) booted clean in default gadget mode with the new PRE-RESET line firing, no regressions on USB Ethernet, and a healthy Configured for device mode receipt.

A one-shot nextboot -e hw.dwc3.force_host=1; reboot then produced the host-mode receipt that was missing from the prior session:

snps_dwc3_fdt0: forcing host mode via hw.dwc3.force_host
snps_dwc3_fdt0: PRE-RESET: GCTL=0x30c12004 GUSB2PHYCFG=0x40101408
                GUSB3PIPECTL=0x010c0002 DCTL=0x00f00000 DSTS=0x00d2069c
                DCFG=0x00080804
snps_dwc3_fdt0: POST-HOST-CONFIG: GCTL=0x30c11004 GUSB2PHYCFG=0x00101408
                GUSB3PIPECTL=0x010a0002 GUCTL=0x02004010 GUCTL1=0x1004018a
snps_dwc3_fdt0: 64 bytes context size, 32-bit DMA
snps_dwc3_fdt0: xECP capabilities <LEGACY,PROTO,PROTO,DEBUG>
usbus4: trying to attach
snps_dwc3_fdt0: Configured for host mode (xHCI attached)
usbus4: 5.0Gbps Super Speed USB v3.0
ugen4.1: <Synopsys XHCI root HUB> at usbus4
uhub4: <Synopsys XHCI root HUB, class 9/0, rev 3.00/1.00, addr 1>

Decode: the GCTL PRTCAPDIR field (bits 13:12) moved from 0b10 (device, U-Boot’s leftover) to 0b01 (host) in the post-config dump. xHCI then attached at SuperSpeed and the Synopsys root hub came up. This is the controller half of the test that the prior session had no evidence for.

That left one open question: would the hub actually enumerate when the phone is also a Type-C source? The earlier work had landed the FUSB302 source-role path and the RK818 5 V boost gate, but the live hub case was untested.

With battery at 3697 mV (above the 3600 mV source_min_voltage_mv guard) and an Anker USB-C hub plugged in:

# sudo sysctl dev.fusb302.0.source_role=1
dev.fusb302.0.source_role: 0 -> 1

dmesg:
fusb3020: source mode: polling as DFP, advertising default Rp on CC1/CC2
rk818_pmu0: rk818: type-C role -> source (5V→VBUS) (DCDC_EN=0xff)
fusb3020: source attach: CC1=Ra CC2=Rd, advertising default Rp

CC1=Ra (cable VCONN sense) and CC2=Rd (sink) on a USB-C orientation 2 attach. The hub then came up the way a real USB host expects:

ugen4.2: <VIA Labs, Inc. USB2.0 Hub>            class 9/0   2.10/90.91
uhub5: 5 ports with 4 removable, self powered, MTT enabled
ugen4.3: <ASIX AX88179A>                        idVendor=0x0b95 idProduct=0x1790
ugen4.4: <Norelsys NS1081>                      umass0, SCSI over Bulk-Only
ugen4.5: <CHICONY USB Keyboard>                 hkbd0 via hidbus0
ugen4.6: <AnkerInnovations Limited Anker USB-C Hub Device>

So FreeBSD on the PinePhone Pro now enumerates a full downstream USB tree — a USB 2.0 hub IC, a Gigabit-Ethernet adapter, a USB mass-storage bridge, a HID keyboard, and the hub’s own management interface — over its USB-C port, with the phone supplying VBUS as a Type-C source.

Three caveats land on the next-up list, not the today list:

  • SuperSpeed lanes are silent. The Synopsys root advertises 5.0 Gbps but every downstream device enumerates as USB 2.0 HIGH (480 Mbps). rk_typec_phy does not yet swap the SS+/SS- lanes for orientation, so the hub’s USB 3 endpoints have nowhere to talk.
  • AX88179 has no driver loaded. axge(4) is upstream-clean but not in the kernel config; the AX88179 enumerates as a generic USB device, not as a ueX network interface. Adding device axge (and urndis/if_ure if relevant) is the obvious next pass.
  • umass0 on this hub is its empty SD-card-reader slot. da0/da1 attach failed with MODE SENSE errors and Mode page 8 missing, consistent with the slot having no card. A real USB stick or SSD on the hub would still attach as da0 through the same path.

The source-mode load is also real. Battery dropped from 3731 mV to 3595 mV inside about a minute of source_role=1 while the hub was attached and powered. The 3600 mV guard remains the right floor; the correct way to extend a host-mode bench is a wall charger feeding the hub’s PD-in port, not the phone’s battery.

hw.dwc3.force_host=1 is still a development override, not a runtime role switch. Replacing it requires a real Type-C role-switch bridge that re-roles the controller when fusb302(4) reports a state change, plus the rk_typec_phy DP/SS lane work.

● working on USB-C host-mode enumeration with downstream USB 2 devices, with the phone as Type-C source. ▸ next on SS lanes (orientation), axge(4) for the AX88179, and the dynamic role switch that retires the boot override.

2026-05-08 (later) — SuperSpeed lanes, same hub, 5.0 Gbps

The 2026-05-08 morning entry left two specific items on the next-up list: the rk_typec_phy SS lane swap, and axge(4) for the AX88179. Both landed the same day.

PINEPHONE_PRO now compiles axge, axe, ure, and miibus ( 5589d8a PINEPHONE_PRO: add axge / axe / ure for downstream USB Ethernet ). The change is one block in the kernel config; the drivers themselves are upstream-clean.

The rk_typec_phy work was the bigger piece. The previous behavior hard-coded the PHY into USB-2-only mode and PMA pin-assignment D/F (four lanes DP, no USB-3), regardless of cable orientation. That is why every downstream device on the hub enumerated as USB 2.0 HIGH even though the Synopsys root advertised SuperSpeed: the SS lanes were parked.

1dbe486 rk_typec_phy: add tunable USB-3 SuperSpeed bring-up adds two boot tunables:

hw.rk_typec_phy.usb3_enable=1   # route SS lanes through the PHY
hw.rk_typec_phy.flip=0          # CC1 orientation: TX=lane 0, RX=lane 1
hw.rk_typec_phy.flip=1          # CC2 orientation: TX=lane 3, RX=lane 2

Both default to 0 so default boots remain USB-2-only and the gadget lifeline never regresses. With usb3_enable=1 the driver:

  1. Sets the GRF flip bit (GRF_USB3PHY_CON0 bit 0, rockchip,typec-conn-dir per upstream rk3399.dtsi).
  2. Runs the existing 24M / PLL config.
  3. Configures TX on lane 0 + RX on lane 1 (or 3 + 2 for flip=1).
  4. Skips the PMA_LANE_CFG = PIN_ASSIGN_D_F and DP_MODE_CTL = DP_MODE_ENTER_A2 writes that were parking the controller in DP-A2 mode.
  5. After PMA reports ready, clears USB3PHY_CON0_USB2_ONLY and USB3OTG_CON1_U3_DIS so xHCI sees a real SS port.

Helpers rk_typec_phy_cfg_tx_lane() / rk_typec_phy_cfg_rx_lane() / rk_typec_phy_set_typec_flip() parameterize the lane register tuples that were inlined for lane 0 / lane 1 only.

Kernel #205 proved the default path first: with both tunables at 0, the phone booted the same gadget-mode dmesg as #203, USB-Ethernet came back up, the new PRE-RESET dump fired, no regression.

The bench then ran the full chain:

sudo nextboot \
  -e 'hw.dwc3.force_host=1' \
  -e 'hw.rk_typec_phy.usb3_enable=1' \
  -e 'hw.rk_typec_phy.flip=1'
sudo reboot
# after boot, with battery low:
sudo sysctl dev.fusb302.0.source_min_voltage_mv=3400
sudo sysctl dev.fusb302.0.source_role=1

flip=0 produced an attach where the FUSB302 reported orientation: 2 (CC2) — i.e. the cable was actually CC2-oriented but the PHY had been told flip=0. Hub came up but only on USB-2 because the SS lanes were configured for the wrong physical lane pair. A second reboot with flip=1 matched the cable, and usbconfig produced the receipt:

ugen4.1: <Synopsys XHCI root HUB>            SUPER 5.0Gbps
ugen4.2: <USB3.0 Hub VIA Labs, Inc.>         SUPER 5.0Gbps
ugen4.3: <AX88179 Gigabit Ethernet ...>      SUPER 5.0Gbps  pwr=ON 46mA
ugen4.4: <NS1081 Norelsys>                   SUPER 5.0Gbps  pwr=ON 38mA
ugen4.5: <USB2.0 Hub VIA Labs, Inc.>         HIGH 480Mbps
ugen4.6: <KU-0833 Keyboard Chicony ...>      LOW 1.5Mbps
ugen4.7: <Anker USB-C Hub Device>            HIGH 480Mbps

dmesg:
miibus0: <MII bus> on axge0
ue0: <USB Ethernet> on axge0
hkbd0: <CHICONY USB Keyboard> on hidbus0
kbd3 at hkbd0

The same Anker hub is now exposing both branches at the right speed: a SuperSpeed USB-3 hub with the AX88179 and the NS1081 SATA-bridge as real 5.0 Gbps devices, and the USB-2 hub IC carrying the keyboard as a USB-2 LOW-speed HID. ue0 is the AX88179 wired up by axge(4); it showed no carrier only because no Ethernet cable was plugged into the adapter’s RJ45.

Two failure modes are worth recording so the next bench session can recognize them:

  • Wrong flip value, hub on USB-2 only. The dmesg path looks identical to the 2026-05-08 morning result: <Synopsys XHCI root HUB> at SUPER, then a USB-2 hub IC at HIGH, no SS device. If dev.fusb302.0.orientation reads 2 (CC2) and the boot tunable is flip=0, that is the situation. Reverse the tunable.
  • source_min_voltage_mv guard refuses. With the hub on the upstream side and no wall power, the phone drains while in source mode (-300 to -500 mA). The 3600 mV guard is still the right floor for unsupervised use; the bench had to drop it to 3400 mV to keep going while battery was already at 3494 mV. Plug the hub’s PD-in port into a wall charger before any extended SuperSpeed bench.

What is still open after this entry:

  • rk_typec_phy reads flip from a boot tunable instead of querying fusb302(4) for the live orientation. A reseat that flips the cable silently re-breaks SS until the next reboot. The right shape is a notifier from fusb302 into a phy re-init path.
  • hw.dwc3.force_host=1 is still a boot tunable for the same reason: no runtime role switch.
  • The umass0 device probe still fails (MODE SENSE errors, Mode page 8 missing) — that’s the empty SD-card slot inside the hub, not a regression. A real USB stick or SSD would attach as da0 through the same path.
  • DisplayPort alt-mode: PD VDM (Discover Identity / SVIDs / Modes / Enter Mode), TCPHY DP lane reconfig, and a FreeBSD cdn_dp DRM bridge are still unbuilt. The SS-lane work just landed the phy groundwork; DP needs an entirely separate phy mode.

● working on USB-C host mode + Type-C source role + SuperSpeed enumeration + ue0 from axge, all on the same Anker hub in a single bench, with kernel #205.

2026-05-08 (later) — first PD VDMs on the wire (Discover Identity sent, partner silent)

The 2026-05-08 host-mode work landed every prerequisite for talking USB-PD VDMs to a partner: DWC3 force_host ( 16a9f5e dwc3: add host-mode boot override ), host-attach diagnostics ( 5a1cb12 dwc3: log host-attach state unconditionally ), axge(4) ( 5589d8a PINEPHONE_PRO: add axge / axe / ure for downstream USB Ethernet ), and rk_typec_phy SS lane swap ( 1dbe486 rk_typec_phy: add tunable USB-3 SuperSpeed bring-up ). Source-role attach + downstream USB-3 enumeration was repeatedly demonstrated ( 1b4004e site: usb-c host mode and source role enumerated a real hub , da9b079 site: SuperSpeed lanes light up, axge brings AX88179 up as ue0 ). The next layer up is sending Vendor-Defined Messages to ask the partner what alt-modes it supports — Discover Identity, Discover SVIDs.

fusb302(4) was sink-only PD before this session; the audit at docs/superpowers/notes/2026-05-08-fusb302-audit.md catalogued the existing message infrastructure and where new VDM hooks slot in. M1 added them in eight small commits:

Bench setup for the M1 done predicate test:

  • Anker USB-C hub with nothing in its PD-in port — the hub only becomes UFP toward the phone when bus-powered (anything in PD-in keeps it DFP and refuses to be a sink for our source role; both a desktop USB-C cable and a pure USB-C PD wall charger were tried, both kept the hub stuck in DFP).
  • Phone in default boot mode — no force_host needed; PD VDMs flow on the CC pins independent of DWC3.
  • Battery temporarily allowed to source below the 3600 mV guard via dev.fusb302.0.source_min_voltage_mv=3200.
sudo sysctl dev.fusb302.0.source_role=1
# wait ~3s for source attach
sudo sysctl dev.fusb302.0.vdm_discover=1

Result on kernel #216:

fusb3020: source attach: CC1=Ra CC2=Rd, advertising default Rp
fusb3020: vdm_discover triggered (v=1 orientation=2 vdm_pending=0
                                  source_role=1 vbus_present=1)
fusb3020: TX[type=0x0f ndo=1 id=0 hdr=0x11af]
                fifo(15): 12 12 12 13 86 af 11 01 80 00 ff ff 14 fe a1
fusb3020: TX regs: pre SW1=0xd6 ST0=0x81 ST1=0x28 |
         fifo_err=0 post-FIFO ST0=0x81 ST1=0x20 | TXSTART_err=0
         -> timeout in 53 ms (INT=0x00 INTA=0x00)

dev.fusb302.0.dp_svid_seen: 0
dev.fusb302.0.dp_partner_svids:

Decode of the actual TX FIFO bytes proves the constructor is correct:

FieldWire bytes (LSB-first)DecodedExpected
PACKSYM86header(2) + data(4) = 6
PD headeraf 11 → 0x11afmsgtype=0x0f (VDM), DR=DFP, PR=Source, Rev3, ndo=1
VDM Header VDO01 80 00 ff → 0xff008001SVID=0xff00 (PD_VDM_SID), VDM_TYPE=Structured, CMD_TYPE=REQ, CMD=0x01 (DISCOVER_IDENTITY)

Every bit on the wire matches USB-PD r3.1 §6.4.4. The phone is now sending Discover Identity correctly.

The hub does not respond. INT=0x00 INTA=0x00 after 53 ms means the chip never saw a GoodCRC from the partner. The most likely cause is that USB-PD r3.0 partners typically require a Source/Sink contract (Source_Capabilities → Request → Accept → PSReady) to be established before they accept VDMs. Today’s fusb302(4) source path enables VBUS via the RK818 boost and advertises Rp, but does not yet send Source_Capabilities — that’s PE_SRC_* policy-engine work and explicitly out of M1’s scope.

So M1 ends at this status:

◐ partial The plan’s done predicate (dp_svid_seen=1 with 0xff01 in the SVID list) was not reached because the hub didn’t respond to the VDM. The plan’s M1 abort predicate (“If the hub never replies to Discover Identity, abort”) was written for the case where the dock truly doesn’t do DP — that’s not what this is. The wire-level VDM is provably correct; the missing piece is partner-contract negotiation, which the design doc lists as M2 work.

What lands cleanly:

  • PD_DATA_VENDOR + Structured VDM Header constants and decode helpers.
  • Role-parameterised fusb302_build_header(), _pd_send_with_role(), _pd_send() wrapper, _vdm_send_structured() builder.
  • VDM RX dispatch + fusb302_handle_vdm_response() (Discover Identity ACK chains Discover SVIDs; Discover SVIDs ACK populates dp_partner_svids[] and sets dp_svid_seen if 0xff01 appears).
  • dev.fusb302.0.vdm_discover admin sysctl trigger + dp_svid_seen
    • dp_partner_svids (hex CSV) read sysctls.

What M2 picks up:

  • Source-side PE: send Source_Capabilities, accept partner Request, send Accept + PSReady to establish a contract. Then the hub will respond to Discover Identity.
  • Once that’s working, finish the DP alt-mode chain: Discover Modes, Enter Mode, DP Status, DP Configure.