From e76a5dd3012062c95db8cb6b7b0ec861217b5c03 Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Sun, 8 Nov 2026 22:15:18 -0500 Subject: [PATCH 1/3] usb: typec: tipd: rearm the debug link until it takes after a sleep The sleep controller ends the debug port's link with a cold reset of its port controller, and tipd rearms it in .complete with the boot loader's sequence. On the J700 a rearm can be refused: the controller rejects one of the commands. The sequence then stopped, so the controller stayed in its "DBMa" debug mode, unlocked, and the Debug USB didn't come back for the rest of the boot. Wait for the mode register after entering debug mode, as the boot loader does, before enabling the debug VDM. Leave debug mode and lock again after a failed step too. Repeat a failed rearm every 250 ms for up to 10 s from a delayed work, and keep it pending across a sleep that comes first, since a link that is down isn't cut again. Signed-off-by: Diljit Singh --- drivers/usb/typec/tipd/core.c | 133 ++++++++++++++++++++++++------ drivers/usb/typec/tipd/tps6598x.h | 4 + 2 files changed, 114 insertions(+), 23 deletions(-) diff --git a/drivers/usb/typec/tipd/core.c b/drivers/usb/typec/tipd/core.c index a5d7b5b03fd8a..b26a41597c9f7 100644 --- a/drivers/usb/typec/tipd/core.c +++ b/drivers/usb/typec/tipd/core.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -1576,9 +1577,10 @@ static void cd321x_prepare(struct tps6598x *tps) cd321x_pm_prepare(&cd321x->pm); cd321x->state_valid = false; } - /* Gate scheduling first; neither worker may be drained under its mutex. */ + /* Gate scheduling first; no worker may be drained under its mutex. */ cancel_delayed_work_sync(&cd321x->resume_work); cancel_delayed_work_sync(&cd321x->update_work); + cancel_delayed_work_sync(&cd321x->rearm_work); /* As macOS does a second or so before the commit, measured on the same controller. */ if (pm_suspend_via_firmware() && device_property_read_bool(tps->dev, "apple,sleep-ssps")) { @@ -1593,37 +1595,114 @@ static void cd321x_prepare(struct tps6598x *tps) } } +/* A try every 250 ms for 10 s */ +#define CD321X_REARM_TRIES 40 +#define CD321X_REARM_RETRY_MS 250 + +static int cd321x_debug_cmd(struct tps6598x *tps, const char *cmd, size_t len, const u8 *in) +{ + u8 out = 0; + int ret; + + ret = tps6598x_exec_cmd(tps, cmd, len, in, 1, &out); + return ret ?: (out & 0xf ? -EIO : 0); +} + +/* The firmware rejects a command for the mode it is switching to until it has switched. */ +static int cd321x_wait_dbma(struct tps6598x *tps, bool on) +{ + u32 mode; + int ret, err; + + err = read_poll_timeout(tps6598x_read32, ret, + ret || (!memcmp(&mode, modes[TPS_MODE_DBMA], 4) == on), + 1000, 100000, false, tps, TPS_REG_MODE, &mode); + return ret ?: err; +} + /* - * A sleep cut this port's debug link. Rearm it the way a boot loader does: unlock with - * the board's key, debug mode on, enable the debug VDM, debug mode off, lock. + * The boot loader's sequence: unlock with the board's key, debug mode on, enable the debug + * VDM, debug mode off, lock. Debug mode is left and the lock set again after a failure too. */ -static void cd321x_rearm_debug_link(struct tps6598x *tps) +static int cd321x_arm_debug_link(struct tps6598x *tps, const char *key, const char **step) { static const u8 vdm[] = { 0x06, 0x46, 0x82, 0x01 }; static const u8 no_key[4]; - const char *key; - u8 on = 1, off = 0, out = 0; + u8 on = 1, off = 0; + int ret, err; + + *step = "unlock"; + ret = cd321x_debug_cmd(tps, "LOCK", sizeof(no_key), (const u8 *)key); + if (ret) + return ret; + + *step = "debug mode"; + ret = cd321x_debug_cmd(tps, "DBMa", 1, &on); + if (!ret) + ret = cd321x_wait_dbma(tps, true); + if (!ret) { + *step = "debug VDM"; + ret = cd321x_debug_cmd(tps, "DVEn", sizeof(vdm), vdm); + } + + err = cd321x_debug_cmd(tps, "DBMa", 1, &off); + if (!err) + err = cd321x_wait_dbma(tps, false); + if (err && !ret) { + *step = "leaving debug mode"; + ret = err; + } + err = cd321x_debug_cmd(tps, "LOCK", sizeof(no_key), no_key); + if (err && !ret) { + *step = "lock"; + ret = err; + } + return ret; +} + +/* + * A sleep cut this port's debug link. Right after the wake the controller can reject the + * sequence, so a failed try is repeated until the link is back. + */ +static void cd321x_rearm_debug_link(struct cd321x *cd321x) +{ + struct tps6598x *tps = &cd321x->tps; + const char *key, *step; int ret; + lockdep_assert_held(&tps->lock); if (device_property_read_string(tps->dev, "apple,debug-lock-key", &key) || - strlen(key) != sizeof(no_key) || !apple_sleep_debug_link_was_cut(tps->dev)) + strlen(key) != 4) { + cd321x->rearm_pending = false; return; + } - ret = tps6598x_exec_cmd(tps, "LOCK", sizeof(no_key), (const u8 *)key, 1, &out); - if (!ret && !(out & 0xf)) - ret = tps6598x_exec_cmd(tps, "DBMa", 1, &on, 1, &out); - if (!ret && !(out & 0xf)) - ret = tps6598x_exec_cmd(tps, "DVEn", sizeof(vdm), vdm, 1, &out); - if (!ret && (out & 0xf)) - ret = -EIO; - if (!ret) - ret = tps6598x_exec_cmd(tps, "DBMa", 1, &off, 0, NULL); - if (!ret) - ret = tps6598x_exec_cmd(tps, "LOCK", sizeof(no_key), no_key, 0, NULL); - if (ret) - dev_warn(tps->dev, "debug link not rearmed: %d\n", ret); - else - dev_dbg(tps->dev, "debug link rearmed\n"); + ret = cd321x_arm_debug_link(tps, key, &step); + cd321x->rearm_tries++; + if (!ret) { + if (cd321x->rearm_tries > 1) + dev_info(tps->dev, "debug link rearmed, try %u\n", cd321x->rearm_tries); + cd321x->rearm_pending = false; + return; + } + if (cd321x->rearm_tries < CD321X_REARM_TRIES) { + mod_delayed_work(system_freezable_wq, &cd321x->rearm_work, + msecs_to_jiffies(CD321X_REARM_RETRY_MS)); + return; + } + dev_warn(tps->dev, "debug link not rearmed after %u tries: %s: %d\n", + cd321x->rearm_tries, step, ret); + cd321x->rearm_pending = false; +} + +static void cd321x_rearm_work(struct work_struct *work) +{ + struct cd321x *cd321x = container_of(to_delayed_work(work), struct cd321x, rearm_work); + + guard(mutex)(&cd321x->tps.lock); + if (cd321x->rearm_pending && cd321x->pm.phase != CD321X_PM_PREPARED && + cd321x->pm.phase != CD321X_PM_REMOVED) + cd321x_rearm_debug_link(cd321x); } static void cd321x_complete(struct tps6598x *tps) @@ -1641,7 +1720,13 @@ static void cd321x_complete(struct tps6598x *tps) } if (pm_suspend_via_firmware() && device_property_read_bool(tps->dev, "apple,sleep-ssps")) tps->data->switch_power_state(tps, TPS_SYSTEM_POWER_STATE_S0); - cd321x_rearm_debug_link(tps); + /* A link still down from an earlier sleep isn't cut again, so it stays pending. */ + if (apple_sleep_debug_link_was_cut(tps->dev)) + cd321x->rearm_pending = true; + if (cd321x->rearm_pending) { + cd321x->rearm_tries = 0; + cd321x_rearm_debug_link(cd321x); + } if (cd321x_pm_resume(&cd321x->pm)) mod_delayed_work(system_freezable_wq, &cd321x->resume_work, 0); } @@ -2259,6 +2344,7 @@ VISIBLE_IF_KUNIT void cd321x_stop_work(struct cd321x *cd321x) } cancel_delayed_work_sync(&cd321x->resume_work); cancel_delayed_work_sync(&cd321x->update_work); + cancel_delayed_work_sync(&cd321x->rearm_work); } EXPORT_SYMBOL_IF_KUNIT(cd321x_stop_work); @@ -2375,6 +2461,7 @@ VISIBLE_IF_KUNIT void cd321x_init_work(struct cd321x *cd321x) { INIT_DELAYED_WORK(&cd321x->update_work, cd321x_update_work); INIT_DELAYED_WORK(&cd321x->resume_work, cd321x_resume_work); + INIT_DELAYED_WORK(&cd321x->rearm_work, cd321x_rearm_work); INIT_WORK(&cd321x->tbt_attach_work, cd321x_tbt_attach_work); atomic_set(&cd321x->link_event, 0); atomic_set(&cd321x->ready_event, 0); diff --git a/drivers/usb/typec/tipd/tps6598x.h b/drivers/usb/typec/tipd/tps6598x.h index f0dfd0a3b1b77..c45c29b8190da 100644 --- a/drivers/usb/typec/tipd/tps6598x.h +++ b/drivers/usb/typec/tipd/tps6598x.h @@ -470,12 +470,16 @@ struct cd321x { bool display_route_active; /* The sleep's debug link cut reset this controller: its setup is expected. */ bool reset_by_cut; + /* The debug link is still to be rearmed after a cut, and the tries so far */ + bool rearm_pending; + unsigned int rearm_tries; bool dp_sink_reported; bool dp_irq_hpd; struct cd321x_status update_status; struct delayed_work update_work; struct delayed_work resume_work; + struct delayed_work rearm_work; struct cd321x_pm_state pm; struct usb_pd_identity cur_partner_identity; bool cur_partner_is_pd; From d44fd3565fbbbb464475a1dbb0617636b30f62e8 Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 00:02:22 -0500 Subject: [PATCH 2/3] soc: apple: t8140-sleep: wait for the debug port's partner after the cut The debug link is cut before a sleep with a cold reset of its port controller. The partner takes that as an unplug, attaches again and renegotiates its power contract. On the J700 that failed in about one sleep in 25: the controller saw nothing attached while the partner kept no contract, tipd's rearm was refused, and no VDM from the partner got through until the cable was replugged. After the reset, poll the controller's power status for a PD contract, for up to 5 s, and log how long it took. If none comes, reset it again, up to three times, then go on as before. Signed-off-by: Diljit Singh --- drivers/soc/apple/t8140-sleep.c | 63 ++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/drivers/soc/apple/t8140-sleep.c b/drivers/soc/apple/t8140-sleep.c index d9e16f936593f..d885143cad2f4 100644 --- a/drivers/soc/apple/t8140-sleep.c +++ b/drivers/soc/apple/t8140-sleep.c @@ -181,6 +181,12 @@ struct t8140_s2r_mailbox { #define HPM_CMD_COLD_RESET 0x64696147 #define HPM_SSPS_S3 3 #define HPM_RESET_MS 1000 +#define HPM_PAGE_POWER_STATUS 0x3f +#define HPM_POWER_CONNECTED BIT(0) +#define HPM_POWER_TYPEC_MODE GENMASK(3, 2) +#define HPM_POWER_TYPEC_PD 3 +#define HPM_CONTRACT_MS 5000 +#define HPM_RESET_TRIES 3 /* A PMU register write on the sleep's SPMI bus */ #define SPMI_CMD_WRITE (BIT(15) | (0xe << 8) | 0x30) @@ -1736,18 +1742,46 @@ static int t8140_hpm_sleep(struct t8140_sleep *s) return ret; } +/* + * The partner takes the reset as an unplug, attaches again and renegotiates its contract. + * Left to finish across the commit, that sometimes failed: neither side had a contract or + * saw the other, and no VDM got through until a replug. Returns the ms it took. + */ +static int t8140_hpm_wait_contract(struct t8140_sleep *s) +{ + bool wake = true; + u32 val = 0; + int ms, ret; + + for (ms = 0; ms < HPM_CONTRACT_MS; ms += 10) { + ret = wake ? t8140_hpm_cmd(s, SPMI_OP_WAKEUP, 0, NULL, NULL) : 0; + if (!ret) + ret = t8140_hpm_select(s, HPM_PAGE_POWER_STATUS); + if (!ret) + ret = t8140_hpm_cmd(s, SPMI_OP_EXT_READ(4), HPM_REG_DATA, NULL, &val); + if (!ret && (val & HPM_POWER_CONNECTED) && + FIELD_GET(HPM_POWER_TYPEC_MODE, val) == HPM_POWER_TYPEC_PD) + return ms; + wake = ret; + mdelay(10); + } + + return -ETIMEDOUT; +} + /* * The debug link stays up with the ATC0 domains off, and it holds the debug domains * active, which macOS has idle. The only known way to end it is the port controller's * cold reset. To the SMC that's a cable pulled: it queues the port's events and records * them as wake reasons. So it has to come before the SMC's AP idle notice, not after, or - * SMC.OutboxNotEmpty is up at the commit. tipd rearms the link after a refusal. + * SMC.OutboxNotEmpty is up at the commit, and so does the partner's new contract. + * tipd rearms the link after a refusal. */ static bool t8140_sleep_usb_down(struct t8140_sleep *s) { u32 cmd = HPM_CMD_COLD_RESET; u32 enable; - int ret; + int i, ret, ms; if (s->usb_down_ran) return false; @@ -1759,16 +1793,27 @@ static bool t8140_sleep_usb_down(struct t8140_sleep *s) dev_info(s->dev, "debug link: cutting it for the sleep\n"); enable = readl(s->hpm_spmi + SPMI_NOTIFY_ENABLE); writel(enable & ~SPMI_NOTIFY_REPLY, s->hpm_spmi + SPMI_NOTIFY_ENABLE); - ret = t8140_hpm_cmd(s, SPMI_OP_WAKEUP, 0, NULL, NULL); - if (!ret) - ret = t8140_hpm_select(s, HPM_PAGE_CMD1); - if (!ret) - ret = t8140_hpm_cmd(s, SPMI_OP_EXT_WRITE(4), HPM_REG_DATA, &cmd, NULL); - if (!ret) { + /* A reset that the partner doesn't come back from is done again. */ + for (i = 1; i <= HPM_RESET_TRIES; i++) { + ret = t8140_hpm_cmd(s, SPMI_OP_WAKEUP, 0, NULL, NULL); + if (!ret) + ret = t8140_hpm_select(s, HPM_PAGE_CMD1); + if (!ret) + ret = t8140_hpm_cmd(s, SPMI_OP_EXT_WRITE(4), HPM_REG_DATA, &cmd, NULL); + if (ret) + break; WRITE_ONCE(t8140_debug_link_cut, true); mdelay(HPM_RESET_MS); - ret = t8140_hpm_sleep(s); + ms = t8140_hpm_wait_contract(s); + if (ms >= 0) { + dev_info(s->dev, "debug link: contract back %d ms after reset %d\n", + HPM_RESET_MS + ms, i); + break; + } + dev_warn(s->dev, "debug link: no contract with the partner after reset %d\n", i); } + if (!ret) + ret = t8140_hpm_sleep(s); writel(1, s->hpm_spmi + SPMI_NOTIFY_ACK); writel(enable, s->hpm_spmi + SPMI_NOTIFY_ENABLE); s->usb_down_ret = ret; From 707249105b765dfafe47aded29c6423f98a96249 Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 04:59:23 -0500 Subject: [PATCH 3/3] selftests: apple-dcp: follow the debug-link rearm work in cd321x probe cd321x_init_work() now also sets up rearm_work, so the probe fixtures' cd321x needs the member, as it has update_work and resume_work. Without it typec-probe and probe-mux-pm-dispatch no longer compile. Signed-off-by: Diljit Singh --- tools/testing/selftests/drivers/gpu/apple-dcp/pm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py index 9c59dda578c79..943c2725bdb73 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py @@ -497,7 +497,7 @@ def function(text, name): mocks = r''' struct cd321x { struct tps6598x tps; - struct work update_work,resume_work; + struct work update_work,resume_work,rearm_work; int link_event,ready_event,restore_event; struct cd321x_pm_state pm; void *mux,*tbt_switch,*port_altmode_dp,*port_altmode_tbt;