Skip to content

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

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

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

Conversation

@nekolyanich

@nekolyanich nekolyanich commented Oct 13, 2026 •

Copy link
Copy Markdown

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, on our downstream tree with the same change (modules 8cde43ad: our tree with this change plus a
    freezable tipd workqueue), in two sessions:
    • 2026-10-13: MagSafe, whose controller is put to sleep at probe, unplugged and plugged again: back on AC
      (macsmc-ac online), 1 of 1. The Dell over Thunderbolt unplugged and plugged again: back 3 of 3. PipeWire
      audio played on the Dell and on the TV: 0 "Capabilities changed" refusals. One known atcphy WARNING,
      atc.c:2681 in atcphy_mux_set() called from the cable work (cd321x_update_work()), on 1 of the 3 Dell
      replugs; the same WARNING from the same path also fired in an earlier session without the CD321x sleep;
    • 2026-10-14: 16 guided replugs of the TV's adapter (20 s unplugged each): 16 of 16 reached the display, 4.6 s
      after the plug each time, 0 refusals. 3 fast replugs (unplugged and back within about half a second): 3 of 3
      came back, the display reconnected 5-6 s later. A USB-C charger in the port the TV had just used negotiated a
      power contract (its power supply went online, the partner was registered, the SMC logged power events at the
      plug). The Mac was also powered through another port all along (the m1n1 debug cable) and the battery was
      already full, so this shows the contract, not charging. No kernel WARNING, BUG or Oops in this session.

Rebased onto aurora-wip after #293, which reorders the disconnect path in the cable work (the DRM disconnect now comes before the Thunderbolt, role and mux steps) and changes tps6598x_disconnect() and cd321x_remove(); the sleep on the no-plug exit and the wake before partner registration are unchanged; the tests above ran on the previous base (bb41967), the rebased commit also booted twice under the hypervisor with both displays, audio playing, and the MagSafe controller put to sleep at probe.

Not tested: other Macs and SoCs, other firmware, other adapters and sinks, and other partners on any port:
USB-only devices, docks. The sleep runs on every CD321x
disconnect and on every empty controller at probe (the MagSafe one included), not only after DP. Also untested:
resume.

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: MacBook Pro 16" M2 Pro (j416s), DCP firmware 13.5, a TV on a
USB-C to HDMI adapter and a Dell over Thunderbolt. Under the m1n1
hypervisor, guided replugs of the TV's adapter with the same sequence
behind a module parameter on one kernel: 4 of 16 lost without it, 0 of
24 with it. Cold boots with both displays: 22 of 22 clean across the
review rounds of this change (4 of them with this version, which put the
empty MagSafe controller to sleep once at probe). Natively, on a
downstream tree with the same change, in two sessions. In the first,
MagSafe unplugged and replugged after the probe-time sleep was back on
AC (1 of 1); the Dell over Thunderbolt unplugged and replugged came back
3 of 3; audio played on both without a refusal; one known atcphy WARNING
(atc.c:2681 in atcphy_mux_set(), called from the cable work) fired on a
Dell replug; the same WARNING from the same path also fired in an
earlier session without this change. In the second, 16 of 16 replugs of
the TV's adapter reached the display (0 lost, 0 refusals), 3 of 3 fast
replugs (back within about half a second) came back, and a USB-C charger
plugged into the same port after an unplug negotiated a power contract
(its power supply went online; the Mac was also powered through another
port, and the battery was already full); no kernel warning.

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

Copy link
Copy Markdown
Author

Superseded by #425: the same change rebased onto integration/aurora (aurora-wip is retired), with the port woken at the first pass that sees the plug and S0 restored at remove, tested on the PR commit.

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