From 6de229f824482fc7ddefee168113ec6ad4ee5155 Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 08:47:40 -0500 Subject: [PATCH 1/4] mfd: macsmc: warn the SMC of a sleep after the debug link's cut On a debug cable, the sleep driver cold-resets the debug port's controller in the SMC's noirq suspend, to end the KIS link. The SMC takes that as a cable pulled and plugged back. It had been warned of the sleep (MBSE = slpw) at .prepare, and it woke the AP right after the commit for USB-C_plug or acdetach. On a J700, 223 of 223 sleeps that sent the reset woke 0.15 to 0.27 s after the commit, with the RTC alarm 10 to 19 s away, though the outbox had been quiet for 500 ms before the AP idle notice. All 154 sleeps that sent no reset ran to their alarm. macOS never resets a port controller around a sleep, and writes nothing to the SMC then but MBSE and the AP power notices. So when the debug link is up at .prepare, warn the SMC in the noirq suspend instead, after the cut and its events, and leave macOS's shortest measured gap, 770 ms, before slpa. A sleep without a debug cable is unchanged. Signed-off-by: Diljit Singh --- drivers/mfd/macsmc-test.c | 25 ++++++++++++++ drivers/mfd/macsmc.c | 61 ++++++++++++++++++++++++--------- drivers/soc/apple/sleep.c | 25 ++++++++++---- include/linux/mfd/macsmc.h | 2 ++ include/linux/soc/apple/sleep.h | 8 +++++ 5 files changed, 98 insertions(+), 23 deletions(-) diff --git a/drivers/mfd/macsmc-test.c b/drivers/mfd/macsmc-test.c index 16d6fdae7c12e..f88f385d70649 100644 --- a/drivers/mfd/macsmc-test.c +++ b/drivers/mfd/macsmc-test.c @@ -246,6 +246,30 @@ static void macsmc_sleep_order_test(struct kunit *test) KUNIT_EXPECT_EQ(test, ctx->seen[6], SMC_KEY(waka)); } +static void macsmc_test_outbox_poll(struct apple_smc *smc, const char *after, + unsigned int min_ms, unsigned int quiet_ms) +{ +} + +static void macsmc_sleep_warn_late_test(struct kunit *test) +{ + struct macsmc_test_context *ctx = test->priv; + + kunit_activate_static_stub(test, apple_smc_ap_power, macsmc_test_ap_power); + kunit_activate_static_stub(test, apple_smc_outbox_poll, macsmc_test_outbox_poll); + ctx->result = 8; + + /* A sleep that cuts the debug link: slpw comes at the noirq suspend, before slpa. */ + ctx->smc.sleep_warn_late = true; + KUNIT_EXPECT_EQ(test, apple_smc_sleep_enter(&ctx->smc), 0); + KUNIT_EXPECT_FALSE(test, ctx->smc.sleep_warn_late); + KUNIT_EXPECT_TRUE(test, ctx->smc.sleep_warned); + KUNIT_ASSERT_EQ(test, ctx->seen_count, 3U); + KUNIT_EXPECT_EQ(test, ctx->seen[0], SMC_KEY(slpw)); + KUNIT_EXPECT_EQ(test, ctx->seen[1], SMC_KEY(slpa)); + KUNIT_EXPECT_EQ(test, ctx->seen[2], (u32)MACSMC_TEST_AP_IDLE); +} + static void macsmc_sleep_refused_test(struct kunit *test) { struct macsmc_test_context *ctx = test->priv; @@ -343,6 +367,7 @@ static struct kunit_case macsmc_parser_cases[] = { KUNIT_CASE(macsmc_sram_value_test), KUNIT_CASE(macsmc_key_info_test), KUNIT_CASE(macsmc_sleep_order_test), + KUNIT_CASE(macsmc_sleep_warn_late_test), KUNIT_CASE(macsmc_sleep_refused_test), KUNIT_CASE(macsmc_notice_test), KUNIT_CASE(macsmc_lid_wake_test), diff --git a/drivers/mfd/macsmc.c b/drivers/mfd/macsmc.c index 801718b9b24d4..ce25a0c53d79c 100644 --- a/drivers/mfd/macsmc.c +++ b/drivers/mfd/macsmc.c @@ -672,19 +672,21 @@ MODULE_DEVICE_TABLE(of, apple_smc_of_match); * from their letters. * * SMC.OutboxNotEmpty wakes the SoC while anything waits in the outbox. An event that arrives - * during the sleep's entry (the lid, a key) is left there, so the sleep returns at once. The - * debug link's cut fills it too, and the SMC counts what it reports after the notice as wake - * reasons, so the cut comes before the notice. + * during the sleep's entry (the lid, a key) is left there, so the sleep returns at once. + * + * The debug link's cut, on a debug cable only, makes the SMC see a cable pulled and plugged + * back. With the cut after slpw, the SMC woke the AP right after the commit, for USB-C_plug + * or acdetach, in every such sleep (J700, 223 of 223), even with its outbox quiet for 500 ms + * before the notice. A sleep that cuts the link warns the SMC only after the cut. */ /* The gaps macOS leaves before the AP idle notice and after it. */ #define SMC_SLEEP_ARM_MS 6 #define SMC_SLEEP_IDLE_MS 30 +/* The shortest gap macOS left between slpw and slpa, in two traced sleeps. */ +#define SMC_SLEEP_WARN_MS 770 -/* - * After the debug link's cut. Its port and charger events keep coming for over a second, - * and any that lands after the AP idle notice wakes the SoC at once. - */ +/* After the debug link's cut. Its port and charger events keep coming for over a second. */ #define SMC_OUTBOX_QUIET_MS 500 #define SMC_OUTBOX_SETTLE_MS 5000 @@ -864,7 +866,11 @@ static int apple_smc_prepare(struct device *dev) { struct apple_smc *smc = dev_get_drvdata(dev); - if (pm_suspend_via_firmware()) + if (!pm_suspend_via_firmware()) + return 0; + if (apple_sleep_debug_link_up()) + smc->sleep_warn_late = true; + else apple_smc_sleep_warn(smc); return 0; @@ -875,19 +881,27 @@ static void apple_smc_complete(struct device *dev) { struct apple_smc *smc = dev_get_drvdata(dev); + smc->sleep_warn_late = false; if (smc->sleep_warned) { apple_smc_write_mbse(smc, SMC_KEY(waka)); smc->sleep_warned = false; } } -/* The events go to their handlers as usual, through the RTKit worker. */ -static void apple_smc_outbox_settle(struct apple_smc *smc) +/* + * For at least @min_ms, then until the outbox is quiet for @quiet_ms. The events go to their + * handlers as usual, through the RTKit worker. + */ +static void apple_smc_outbox_poll(struct apple_smc *smc, const char *after, + unsigned int min_ms, unsigned int quiet_ms) { unsigned int ms, quiet = 0, count = 0, last = 0; int ret; - for (ms = 0; ms < SMC_OUTBOX_SETTLE_MS && quiet < SMC_OUTBOX_QUIET_MS; ms++) { + if (IS_ENABLED(CONFIG_MFD_MACSMC_KUNIT_TEST)) + KUNIT_STATIC_STUB_REDIRECT(apple_smc_outbox_poll, smc, after, min_ms, quiet_ms); + + for (ms = 0; ms < SMC_OUTBOX_SETTLE_MS && (ms < min_ms || quiet < quiet_ms); ms++) { ret = apple_rtkit_poll(smc->rtk); if (ret > 0) { count += ret; @@ -898,8 +912,23 @@ static void apple_smc_outbox_settle(struct apple_smc *smc) } usleep_range(1000, 1200); } - dev_dbg(smc->dev, "debug link cut: %u messages, the last at %u ms, %s after %u ms\n", - count, last, quiet >= SMC_OUTBOX_QUIET_MS ? "quiet" : "still busy", ms); + /* A debug cable only. */ + dev_info(smc->dev, "%s: %u messages in %u ms, the last at %u ms%s\n", after, count, ms, + last, ms >= SMC_OUTBOX_SETTLE_MS ? ", still busy" : ""); +} + +/* The noirq suspend's part, polled: the cut, a late warning, the commit's notices. */ +static int apple_smc_sleep_enter(struct apple_smc *smc) +{ + if (apple_sleep_debug_link_cut()) + apple_smc_outbox_poll(smc, "debug link cut", 0, SMC_OUTBOX_QUIET_MS); + if (smc->sleep_warn_late) { + smc->sleep_warn_late = false; + apple_smc_sleep_warn(smc); + apple_smc_outbox_poll(smc, "late sleep warning", SMC_SLEEP_WARN_MS, 0); + } + + return apple_smc_sleep_commit(smc); } static int apple_smc_suspend_noirq(struct device *dev) @@ -907,13 +936,11 @@ static int apple_smc_suspend_noirq(struct device *dev) struct apple_smc *smc = dev_get_drvdata(dev); int ret; - if (!smc->sleep_warned) + if (!smc->sleep_warned && !smc->sleep_warn_late) return 0; apple_smc_set_polled(smc, true); - if (apple_sleep_debug_link_cut()) - apple_smc_outbox_settle(smc); - ret = apple_smc_sleep_commit(smc); + ret = apple_smc_sleep_enter(smc); apple_smc_set_polled(smc, false); return ret; diff --git a/drivers/soc/apple/sleep.c b/drivers/soc/apple/sleep.c index 3838cedf471ef..63929af5d714e 100644 --- a/drivers/soc/apple/sleep.c +++ b/drivers/soc/apple/sleep.c @@ -1859,13 +1859,19 @@ static int sleep_hpm_wait_contract(struct apple_sleep *s) return -ETIMEDOUT; } +/* A live debug link holds the debug switch domain on. */ +static bool sleep_debug_link_up(struct apple_sleep *s) +{ + return s->hpm_spmi && + FIELD_GET(PS_ACTUAL, readl(s->ps_aon + s->soc->ps_aon_debug_switch)); +} + /* * 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, and so does the partner's new contract. - * tipd rearms the link after a refusal. + * cold reset. To the SMC that's a cable pulled: it records the port's events as wake + * reasons once it has been warned of the sleep, and wakes the AP right after the commit. + * So the SMC's driver warns it only after this. tipd rearms the link after a refusal. */ static bool apple_sleep_usb_down(struct apple_sleep *s) { @@ -1877,8 +1883,7 @@ static bool apple_sleep_usb_down(struct apple_sleep *s) return false; s->usb_down_ran = true; - if (!s->hpm_spmi || - !FIELD_GET(PS_ACTUAL, readl(s->ps_aon + s->soc->ps_aon_debug_switch))) + if (!sleep_debug_link_up(s)) return false; dev_info(s->dev, "debug link: cutting it for the sleep\n"); @@ -1918,6 +1923,14 @@ bool apple_sleep_debug_link_cut(void) } EXPORT_SYMBOL_GPL(apple_sleep_debug_link_cut); +bool apple_sleep_debug_link_up(void) +{ + struct apple_sleep *s = apple_sleep; + + return s && !s->usb_down_ran && sleep_debug_link_up(s); +} +EXPORT_SYMBOL_GPL(apple_sleep_debug_link_up); + /* * macOS takes the wake for a lid closing during the sleep as a dark one: every driver wakes, * the display stays off, and the root domain sleeps again as soon as it sees the lid closed. diff --git a/include/linux/mfd/macsmc.h b/include/linux/mfd/macsmc.h index bf8781ba4c52e..d0cc7ae5f504a 100644 --- a/include/linux/mfd/macsmc.h +++ b/include/linux/mfd/macsmc.h @@ -116,6 +116,8 @@ struct apple_smc { bool ap_notified; /* Told of a coming sleep, and not yet of its end. */ bool sleep_warned; + /* To be told at the noirq suspend, after the debug link's cut. */ + bool sleep_warn_late; /* In a noirq phase: replies are polled for. */ bool polled; /* MBSW at the last wake: what the SMC had woken the AP for. */ diff --git a/include/linux/soc/apple/sleep.h b/include/linux/soc/apple/sleep.h index 1766f865b64d2..d7b6207601124 100644 --- a/include/linux/soc/apple/sleep.h +++ b/include/linux/soc/apple/sleep.h @@ -108,6 +108,9 @@ bool apple_sleep_debug_link_cut_peek(struct device *dev); */ bool apple_sleep_debug_link_cut(void); +/* This sleep will end the debug link: for the SMC's driver, which warns the SMC after it. */ +bool apple_sleep_debug_link_up(void); + /* * From the SMC's driver, in its noirq resume: the SMC woke the AP for the lid alone, and the * lid reads closed. If nothing else woke the SoC, the PM core goes back to sleep once every @@ -227,6 +230,11 @@ static inline bool apple_sleep_debug_link_cut(void) return false; } +static inline bool apple_sleep_debug_link_up(void) +{ + return false; +} + static inline void apple_sleep_lid_closed_wake(void) { } From f77883831ea5f1a1b4fbcdd4aaa7418105435170 Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 08:54:20 -0500 Subject: [PATCH 2/4] input: macsmc-input: after a dark wake, stay up only for a lid that opened A dark wake for a closed lid sleeps again, and .prepare refuses that sleep if the lid reads open by then: the user opened it. A dark wake can also come with the lid open, for the debug link's cut on a debug cable. The lid reads open at .prepare there too, and the sleep would be refused for a lid that never moved. Note at the dark wake whether the lid was closed, and refuse only then. A failed read keeps the old behaviour. Signed-off-by: Diljit Singh --- drivers/input/misc/macsmc-input.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/drivers/input/misc/macsmc-input.c b/drivers/input/misc/macsmc-input.c index fb3fe3fdb6920..109af27bbb11b 100644 --- a/drivers/input/misc/macsmc-input.c +++ b/drivers/input/misc/macsmc-input.c @@ -270,10 +270,14 @@ static int macsmc_input_pm_prepare(struct device *dev) static void macsmc_input_pm_complete(struct device *dev) { struct macsmc_input *smcin = dev_get_drvdata(dev); + u8 lid; - /* Still armed: a lid opened or a button pressed before the next .prepare wakes. */ + /* + * Still armed: a lid opened or a button pressed before the next .prepare wakes. A dark + * wake with the lid already open (for the debug link's cut) has no lid to open. + */ if (pm_dark_wake()) { - smcin->dark = true; + smcin->dark = apple_smc_read_u8(smcin->smc, SMC_KEY(MSLD), &lid) < 0 || lid; return; } smcin->wakeup_mode = false; From e6c213aabf2fd53a711d47b091757254a9c74194 Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 08:54:20 -0500 Subject: [PATCH 3/4] usb: typec: tipd: cd321x: rearm the debug link only at a full wake A dark wake sleeps again with user space still frozen. Rearming the debug link in its .complete brought KIS back, and the next entry cut it again, so a wake for the cut's own events would come again. Keep the rearm pending until a wake that isn't dark. The setup after the cold reset still runs. Signed-off-by: Diljit Singh --- drivers/usb/typec/tipd/core.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/drivers/usb/typec/tipd/core.c b/drivers/usb/typec/tipd/core.c index 99cd947f790cb..0f581faf056e5 100644 --- a/drivers/usb/typec/tipd/core.c +++ b/drivers/usb/typec/tipd/core.c @@ -1720,10 +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); - /* A link still down from an earlier sleep isn't cut again, so it stays pending. */ + /* + * A link still down from an earlier sleep isn't cut again, so it stays pending. A dark + * wake sleeps again at once: the link stays down for it, and the full wake rearms it. + */ if (apple_sleep_debug_link_was_cut(tps->dev)) cd321x->rearm_pending = true; - if (cd321x->rearm_pending) { + if (cd321x->rearm_pending && !pm_dark_wake()) { cd321x->rearm_tries = 0; cd321x_rearm_debug_link(cd321x); } From f48b3e2ed08494560b22915bc6f0306fdbb94c5f Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 08:54:20 -0500 Subject: [PATCH 4/4] soc: apple: sleep, mfd: macsmc: take a wake for the debug link's cut as a dark wake If the SMC still wakes the AP for the cut's own events, sleep again through the PM core's dark wake, as for a lid closing. That needs all three: only the SMC's AIC sources woke the SoC, the wake record has nothing but acattach, acdetach or USB-C_plug, and this sleep sent the reset. The link then stays down, so the next entry doesn't cut it, and runs to its alarm like the 154 J700 sleeps that sent no reset. Any other source or reason gives a normal wake. Without a debug cable there is no cut, so a charger plugged in during the sleep wakes the machine as before. Signed-off-by: Diljit Singh --- drivers/mfd/macsmc-test.c | 14 ++++++++++++++ drivers/mfd/macsmc.c | 18 ++++++++++++++++++ drivers/soc/apple/sleep.c | 16 ++++++++++++++++ include/linux/soc/apple/sleep.h | 10 ++++++++++ 4 files changed, 58 insertions(+) diff --git a/drivers/mfd/macsmc-test.c b/drivers/mfd/macsmc-test.c index f88f385d70649..d6ccf8a3970a2 100644 --- a/drivers/mfd/macsmc-test.c +++ b/drivers/mfd/macsmc-test.c @@ -356,6 +356,19 @@ static void macsmc_wake_reason_test(struct kunit *test) KUNIT_EXPECT_STREQ(test, names, "wifibt USB-C_plug"); } +static void macsmc_port_reasons_test(struct kunit *test) +{ + /* The debug link's cut: USB-C_plug, acdetach, or both. */ + KUNIT_EXPECT_TRUE(test, apple_smc_port_reasons_only(BIT_ULL(42))); + KUNIT_EXPECT_TRUE(test, apple_smc_port_reasons_only(BIT_ULL(41) | BIT_ULL(42))); + /* Anything else, with it or alone, is a real wake. */ + KUNIT_EXPECT_FALSE(test, apple_smc_port_reasons_only(0)); + KUNIT_EXPECT_FALSE(test, apple_smc_port_reasons_only(BIT_ULL(42) | BIT_ULL(0))); + KUNIT_EXPECT_FALSE(test, apple_smc_port_reasons_only(BIT_ULL(42) | BIT_ULL(10))); + KUNIT_EXPECT_FALSE(test, apple_smc_port_reasons_only(BIT_ULL(23))); + KUNIT_EXPECT_FALSE(test, apple_smc_port_reasons_only(BIT_ULL(43))); +} + static struct kunit_case macsmc_parser_cases[] = { KUNIT_CASE(macsmc_key_info_lock_test), KUNIT_CASE(macsmc_key_index_test), @@ -372,6 +385,7 @@ static struct kunit_case macsmc_parser_cases[] = { KUNIT_CASE(macsmc_notice_test), KUNIT_CASE(macsmc_lid_wake_test), KUNIT_CASE(macsmc_wake_reason_test), + KUNIT_CASE(macsmc_port_reasons_test), {} }; diff --git a/drivers/mfd/macsmc.c b/drivers/mfd/macsmc.c index ce25a0c53d79c..1d0e4d58b68a6 100644 --- a/drivers/mfd/macsmc.c +++ b/drivers/mfd/macsmc.c @@ -851,12 +851,30 @@ static void apple_smc_lid_wake(struct apple_smc *smc) apple_sleep_lid_closed_wake(); } +/* Only what a cable pulled and plugged back reports: what the debug link's cut makes. */ +static bool apple_smc_port_reasons_only(u64 record) +{ + static const char * const names[] = { "acattach", "acdetach", "USB-C_plug" }; + u64 port = 0; + int i, bit; + + for (i = 0; i < ARRAY_SIZE(names); i++) { + bit = apple_smc_wake_reason_bit(names[i]); + if (bit >= 0) + port |= BIT_ULL(bit); + } + + return record && !(record & ~port); +} + static void apple_smc_sleep_wake(struct apple_smc *smc) { apple_smc_notice_on(smc); apple_smc_write_mbse(smc, SMC_KEY(waka)); smc->sleep_warned = false; apple_smc_lid_wake(smc); + if (apple_smc_port_reasons_only(smc->wake_record)) + apple_sleep_debug_link_cut_wake(); /* A dark wake isn't one: with wakeup_count in use, it would abort the sleep after it. */ if (smc->wake_record && !pm_dark_wake()) pm_wakeup_event(smc->dev, 0); diff --git a/drivers/soc/apple/sleep.c b/drivers/soc/apple/sleep.c index 63929af5d714e..91ccd2386e44c 100644 --- a/drivers/soc/apple/sleep.c +++ b/drivers/soc/apple/sleep.c @@ -1946,6 +1946,22 @@ void apple_sleep_lid_closed_wake(void) } EXPORT_SYMBOL_GPL(apple_sleep_lid_closed_wake); +/* + * The SMC woke the AP for the port alone, and this sleep cut the debug link: the cut's own + * events. Sleep again. The link stays down until tipd rearms it at a full wake, so the next + * entry doesn't cut it. + */ +void apple_sleep_debug_link_cut_wake(void) +{ + struct apple_sleep *s = apple_sleep; + + if (!s || !s->smc_woke_alone || !READ_ONCE(sleep_debug_link_cut)) + return; + dev_info(s->dev, "debug link: its cut woke the sleep: dark wake\n"); + pm_set_dark_wake(); +} +EXPORT_SYMBOL_GPL(apple_sleep_debug_link_cut_wake); + /* * The radio's link and the engine that starts it, in macOS's order, measured on one * sleep: the link into L2, the engine stopped, PERST# asserted, the PHY's clocks off. diff --git a/include/linux/soc/apple/sleep.h b/include/linux/soc/apple/sleep.h index d7b6207601124..301d955cfe678 100644 --- a/include/linux/soc/apple/sleep.h +++ b/include/linux/soc/apple/sleep.h @@ -118,6 +118,12 @@ bool apple_sleep_debug_link_up(void); */ void apple_sleep_lid_closed_wake(void); +/* + * From the SMC's driver, in its noirq resume: the SMC woke the AP for the port alone. If + * this sleep cut the debug link and nothing else woke the SoC, it's a dark wake. + */ +void apple_sleep_debug_link_cut_wake(void); + /* The radio's host has its port back after a real sleep, so the next commit may stop it. */ void apple_sleep_radio_port_up(void); @@ -239,6 +245,10 @@ static inline void apple_sleep_lid_closed_wake(void) { } +static inline void apple_sleep_debug_link_cut_wake(void) +{ +} + static inline void apple_sleep_radio_port_up(void) { }