Repository navigation
usb: typec: tipd: put an empty CD321x port to sleep, as macOS - #296
Closed
nekolyanich wants to merge 1 commit into
Closed
nekolyanich wants to merge 1 commit into
nekolyanich wants to merge 1 commit into
Conversation
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
force-pushed
the
pr/cd321x-unplug-sleep
branch
from
October 14, 2026 04:36
f734674 to
d1c7924
Compare
nekolyanich
marked this pull request as ready for review
October 14, 2026 04:56
This was referenced Oct 14, 2026
This was referenced Oct 24, 2026
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_statustrace events):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:
At boot, the MagSafe port's controller (hpm5, nothing plugged in) gets the same:
0x50 = 0x2000,0x50 = 0,SSPS 2in one trace,0x50 = 0x2000,SSPS 2in the other (2 of 2). It is the only empty controller traced atboot: 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 thedisplay 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 0after theSSPS 0and once at boot. Our controller rejectedit 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():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;
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;
tipd_cd321x_datasets a newport_sleepflag). Nothing was traced for theSN201202x, 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 stateunknown (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, 0to 0x50 whileswitch_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 cableremoved 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.
freezable tipd workqueue), in two sessions:
(
macsmc-aconline), 1 of 1. The Dell over Thunderbolt unplugged and plugged again: back 3 of 3. PipeWireaudio played on the Dell and on the TV: 0 "Capabilities changed" refusals. One known atcphy WARNING,
atc.c:2681inatcphy_mux_set()called from the cable work (cd321x_update_work()), on 1 of the 3 Dellreplugs; the same WARNING from the same path also fired in an earlier session without the CD321x sleep;
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()andcd321x_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.