Skip to content

usb: typec: tipd: put an empty CD321x port to sleep, as macOS - #2

Closed
nekolyanich wants to merge 1 commit into
aurora-wipfrom
pr/cd321x-unplug-sleep
Closed

nekolyanich wants to merge 1 commit into
aurora-wipfrom
pr/cd321x-unplug-sleep

Conversation

@nekolyanich

Copy link
Copy Markdown
Owner

Draft: the code went through several review rounds; native tests run tonight (see Testing), and I'll update this PR with them.

On an M2 Pro (j416s), replugs of a USB-C to HDMI adapter in DP alt mode often never reach the display: natively
on our tree, 26 of 56 over three days (10 of 24, 5 of 8, 11 of 24). In the run whose controller I traced, the
lost plugs got no VBUS (no USB, no display, the adapter's LED stays off) and alternated with good ones.

What the controller shows. I traced the CD321x of that port (4 cycles: 2 good, 2 lost) (cd321x_irq/tps6598x_status/
cd321x_data_status trace events):

moment pp_5v0 pp_cable PD data_status
unplug after a plug that failed off off – empty
next plug (works) off → out off → fault → out yes USB2/3, then DP, then HPD
unplug after a session with DP off in – DATA_CONNECTION | USB_DATA_ROLE | 0x80000000
next plug (lost) stays off stays in never USB2/3, no DP

After a DP session the port keeps its cable power path "in" and the session's data status. The next plug never
turns on 5 V. Unplugging that dead plug clears the state, hence the alternation.

What macOS does. I traced macOS 13's I2C traffic to the same controller under the m1n1 hypervisor, with
the same TV and adapter. On a disconnect:

W 0x50 = 0x00002000                    8 of 8
W DATA1 = 0x02, CMD1 = 'SSPS'          8 of 8
W 0x50 = 0x00002000                    5 of 8 (in one of them after a further interrupt)
W 0x50 = 0x00000000                    5 of 8
R 0x20 (SYSTEM_POWER_STATE) = 0x02     3 of 3 (the first trace didn't decode reads)

At boot, the MagSafe port's controller (hpm5, nothing plugged in) gets the same: 0x50 = 0x2000, 0x50 = 0,
SSPS 2 in one trace, 0x50 = 0x2000, SSPS 2 in the other (2 of 2). It is the only empty controller traced at
boot: the USB-C ports traced had plugs (hpm1, hpm2), and hpm0 wasn't in the traces.

The port then stays in power state 2 while nothing is plugged in. On the next connect, as soon as STATUS shows
the plug, macOS sends SSPS 0 (and reads back 0), before the display gets HPD. 5 of 5 replugs reached the
display under macOS. We have no documentation for register 0x50; it is 6 bytes long, and macOS reads and writes 4.
macOS also writes 0x2000 to it after each DATA_STATUS read with HPD_IRQ (bit 14) set (11 of 11 in the second
trace), but also with no HPD_IRQ pending (at the unplugs, and at boot to the MagSafe controller). So the patch names the
register and the bit by number only, with what was seen. macOS also sends a USBw 0 after the SSPS 0 and once at boot. Our controller rejected
it in 19 of 19 tries, and the fix works without it, so it is left out.

The change. The driver remembers the port's power state (unknown after probe, awake, asleep). In
cd321x_update_work():

  • when it finds no plug and the port isn't known to be asleep, before cd321x_pm_complete(): 0x50 = 0x2000,
    SSPS 2, 0x50 = 0x2000, 0x50 = 0. That covers a disconnect, a disconnect retried after the safe-mode switch
    failed, and a port found empty at probe: the MagSafe controller, as macOS does at boot, and also an empty USB-C
    port, which macOS was not seen doing. At probe this uses the unplug order of the 0x50 writes; macOS's boot
    order differs slightly;
  • STATUS is read again right before SSPS 2, and a plug present skips the sleep: state 2 was never seen entered
    with a partner attached. So two cases leave the port awake, as before this change: an unplug and replug
    inside one 500 ms debounce, and a plug landing between the cable work's snapshot and that STATUS read;
  • before a new partner is registered: SSPS 0;
  • any failure leaves the port's state unknown, so the next pass tries again (one extra read);
  • only the CD321x does any of this (tipd_cd321x_data sets a new port_sleep flag). Nothing was traced for the
    SN201202x, which shares the cable work, so it keeps its behaviour.

The code always sends all four steps on a disconnect (the tested HACK did too). Both use switch_power_state(),
which reads the state before SSPS (macOS doesn't) and after it (as macOS does), and skips a state already set,
so a plug present at probe sees no extra command. A failure only warns; it doesn't start a revalidation
retry, because the cable state has already been applied and the next event still reaches the port. Probe
(SSPS 0) and suspend are unchanged. What macOS does at resume isn't traced; resume makes the port's state
unknown (the controller may be re-initialised, PTCH), so the revalidation pass puts an empty port to sleep again.
For a port already in state 2 that pass still writes 0x2000, 0x2000, 0 to 0x50 while switch_power_state()
skips the SSPS: one more such write sequence per resume per empty port, harmless as far as we can tell (macOS
writes 0x50 often). The calls run inside the cable work, so they follow the
debounce and cd321x_pm_can_update(): nothing is sent while the port is prepared for suspend, and a cable
removed during sleep is put to sleep when the resume revalidation applies.

Testing: MacBook Pro 16" M2 Pro (j416s), a TV on a USB-C to HDMI adapter on one port, a Dell over Thunderbolt on another.

  • Under the m1n1 hypervisor, guided replugs of the TV's adapter, the same sequence behind a module parameter on one kernel: 4 of 16 replugs lost without it, 0 of 24 with it (8 + 16).
  • This commit (and the same change on our tree), cold boots with the TV and the Dell attached: 22 of 22 boots across the review rounds came up with the TV's audio playing and no Dell refusals; with the final version (4 boots) the empty MagSafe controller is put to sleep once at probe, as macOS does at boot.
  • Natively: tonight (a 16-replug run, MagSafe charging after boot and after a replug, the Dell unplugged 3 times, a USB-C charger, fast replugs). I'll fill these in here.

Your aurora-silicon#295 also changes the disconnect path in tipd/core.c. This applies to aurora-wip as is; if aurora-silicon#295 lands first, I'll rebase on it.

Not tested: other Macs and SoCs, other firmware, other adapters and sinks, and other partners on any port:
chargers (charging through the port), USB-only devices, docks, USB4/TBT. The sleep runs on every CD321x
disconnect and on every empty controller at probe (the MagSafe one included), not only after DP: MagSafe charging
after boot is to be tested. Also untested: a fast replug (the port stays awake, as before this patch), and resume.

🤖 Generated with Claude Code

On an M2 Pro (j416s), replugs of a USB-C to HDMI adapter in DP alt mode
often never reached the display: 26 of 56 natively, over three days. In
a trace of the controller's status (4 cycles), after the unplug of a
session that had DP it reported the cable power path (PP_CABLE) "in" and
kept the session's data status (DATA_CONNECTION, USB_DATA_ROLE); the
next plug got no VBUS, never turned on 5 V and never reached PD (2 of
2). Unplugging that failed plug cleared the state, so the next one
worked.

macOS, traced under the m1n1 hypervisor with the same TV and adapter,
writes 0x2000 to register 0x50 and sends SSPS 2 on every disconnect (8
of 8 in two traces), and after it writes 0x2000 and 0 to 0x50 in 5 of 8.
The port then reads back system power state 2 and stays in it while
unplugged (3 of 3, the trace that decodes reads). On the next connect,
as soon as STATUS shows the plug, macOS sends SSPS 0, before the display
gets HPD. At boot it puts the MagSafe port's controller, which has
nothing plugged in, into state 2 as well (2 of 2); no empty USB-C port
was traced at boot. We have no documentation for register 0x50; it is 6
bytes long, and macOS also writes 0x2000 to it after each DATA_STATUS
HPD_IRQ (11 of 11), so the bit is named by number only.

Do the same from the cable work: whenever it finds no plug and the port
is not known to be asleep (a disconnect, also a retried one, or an empty
port at probe), write 0x2000 to 0x50, SSPS 2, 0x2000 and 0 to 0x50 (at
probe too, where macOS's order differs slightly); before a new partner
is registered, SSPS 0. STATUS is read again right before the sleep, and
a plug present skips it: state 2 was never seen entered with a partner
attached. So a plug within the debounce of an unplug, or one landing
while the cable work runs, leaves the port awake, as before this change.
Both go through switch_power_state(), which also reads the state before
SSPS (macOS doesn't) and skips a state already set. A failure only warns
and leaves the port state unknown, as does a resume, so the next pass
tries again. Only the CD321x does this (tipd_cd321x_data sets
port_sleep): nothing was traced for the SN201202x, which shares the
cable work, so it keeps its behaviour.

Testing: TBD

Assisted-by: Claude:claude-opus-5-5 checkpatch
Signed-off-by: nekolyanich <me@nekolyanich.com>
@nekolyanich

Copy link
Copy Markdown
Owner Author

Published as draft: aurora-silicon#296

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant