Skip to content
8 changes: 6 additions & 2 deletions drivers/input/misc/macsmc-input.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
39 changes: 39 additions & 0 deletions drivers/mfd/macsmc-test.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -332,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),
Expand All @@ -343,10 +380,12 @@ 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),
KUNIT_CASE(macsmc_wake_reason_test),
KUNIT_CASE(macsmc_port_reasons_test),
{}
};

Expand Down
79 changes: 62 additions & 17 deletions drivers/mfd/macsmc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -849,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);
Expand All @@ -864,7 +884,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;
Expand All @@ -875,19 +899,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;
Expand All @@ -898,22 +930,35 @@ 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)
{
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;
Expand Down
41 changes: 35 additions & 6 deletions drivers/soc/apple/sleep.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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");
Expand Down Expand Up @@ -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.
Expand All @@ -1933,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.
Expand Down
7 changes: 5 additions & 2 deletions drivers/usb/typec/tipd/core.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
2 changes: 2 additions & 0 deletions include/linux/mfd/macsmc.h
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
18 changes: 18 additions & 0 deletions include/linux/soc/apple/sleep.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,22 @@ 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
* device has resumed, as macOS does from a dark wake.
*/
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);

Expand Down Expand Up @@ -227,10 +236,19 @@ 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)
{
}

static inline void apple_sleep_debug_link_cut_wake(void)
{
}

static inline void apple_sleep_radio_port_up(void)
{
}
Expand Down
Loading