diff --git a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml index a1dfa81a65014..071ba0f3a6fed 100644 --- a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml +++ b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml @@ -114,6 +114,13 @@ properties: resets: maxItems: 1 + apple,hold-rx-in-sleep: + description: + Messages from this coprocessor need not wake the system. The OS may + leave them in the receive FIFO while the system sleeps and handle + them at resume. + type: boolean + power-domains: minItems: 1 maxItems: 2 diff --git a/Documentation/devicetree/bindings/power/apple,pmgr-pwrstate.yaml b/Documentation/devicetree/bindings/power/apple,pmgr-pwrstate.yaml index 919e53113fd2a..6ba123fbeba95 100644 --- a/Documentation/devicetree/bindings/power/apple,pmgr-pwrstate.yaml +++ b/Documentation/devicetree/bindings/power/apple,pmgr-pwrstate.yaml @@ -22,6 +22,7 @@ allOf: then: properties: apple,firmware-owned: false + apple,sleep-retained: false apple,always-on: false apple,min-state: false apple,force-disable: false @@ -106,6 +107,13 @@ properties: coprocessors' CPU domains. type: boolean + apple,sleep-retained: + description: + A coprocessor keeps running in this domain across system sleep with no + system-sleep callbacks of its own. The OS keeps the domain on during + system sleep. Runtime power management is unaffected. + type: boolean + apple,firmware-owned: description: The boot loader hands over active hardware whose power domain remains diff --git a/arch/arm64/boot/dts/apple/t8132-j713-gpu.dtsi b/arch/arm64/boot/dts/apple/t8132-j713-gpu.dtsi index 7cddbaf1e5b56..bd6bf689e84e7 100644 --- a/arch/arm64/boot/dts/apple/t8132-j713-gpu.dtsi +++ b/arch/arm64/boot/dts/apple/t8132-j713-gpu.dtsi @@ -80,6 +80,8 @@ /* ASCWrap v6 contains the same mailbox aperture used by ASC v4. */ compatible = "apple,t8132-asc-mailbox", "apple,asc-mailbox-v4"; reg = <0x3 0x02608000 0x0 0x4000>; + /* Idle ring-service kicks need not wake the system. */ + apple,hold-rx-in-sleep; interrupt-parent = <&aic>; interrupts = , , @@ -95,6 +97,8 @@ &ps_gfx { status = "okay"; power-domains = <&gfx_tvm>; + /* The G16 firmware stays live across suspend-to-idle. */ + apple,sleep-retained; }; /* The existing report provider owns readiness for shared PMP commands. */ diff --git a/drivers/iommu/apple-dart.c b/drivers/iommu/apple-dart.c index fbe1ba33089b4..81fd469a6c5c3 100644 --- a/drivers/iommu/apple-dart.c +++ b/drivers/iommu/apple-dart.c @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -311,6 +312,8 @@ struct apple_dart { struct apple_dart_dcp *dcp; struct apple_dart_dcp *frontend[2]; bool preserve_display; + /* Retained radio/display state left in place across suspend-to-idle. */ + bool s2idle_kept; /* Resources reachable by a retained domain outlive device removal. */ bool retained; bool retained_mtp; @@ -4718,10 +4721,23 @@ static const struct apple_dart_hw apple_dart_hw_t8110 = { .ttbr_count = 1, }; +static void apple_dart_save_state(struct apple_dart *dart) +{ + unsigned int sid, idx; + + for (sid = 0; sid < dart->num_streams; sid++) { + dart->save_tcr[sid] = apple_dart_readl(dart, + DART_TCR(dart, sid)); + for (idx = 0; idx < dart->hw->ttbr_count; idx++) + dart->save_ttbr[sid][idx] = + apple_dart_readl(dart, + DART_TTBR(dart, sid, idx)); + } +} + static __maybe_unused int apple_dart_suspend(struct device *dev) { struct apple_dart *dart = dev_get_drvdata(dev); - unsigned int sid, idx; dart->tunnel_state_restored = false; if (READ_ONCE(dart->commands_gated)) @@ -4733,9 +4749,19 @@ static __maybe_unused int apple_dart_suspend(struct device *dev) */ if (dart->cold_ane) return 0; - /* Neither retained radio nor display has a qualified suspend boundary. */ - if (dart->managed_sids || dart->preserve_display || apple_dart_has_borrowed(dart)) - return -EBUSY; + /* + * Retained radio and display DMA has no suspend boundary of its own. + * Suspend-to-idle leaves it running in place: these DARTs have no + * power domain here, and the DCP keeps the display domains on. The + * registers are saved only for resume to check. + */ + if (dart->managed_sids || dart->preserve_display || apple_dart_has_borrowed(dart)) { + if (pm_suspend_target_state != PM_SUSPEND_TO_IDLE) + return -EBUSY; + apple_dart_save_state(dart); + dart->s2idle_kept = true; + return 0; + } /* The tunneled DART has no state to save when its domain stays on. */ if (dart->power_retained || dart->retained_mtp) return 0; @@ -4744,15 +4770,7 @@ static __maybe_unused int apple_dart_suspend(struct device *dev) if (dart->locked) return 0; - for (sid = 0; sid < dart->num_streams; sid++) { - dart->save_tcr[sid] = apple_dart_readl(dart, - DART_TCR(dart, sid)); - for (idx = 0; idx < dart->hw->ttbr_count; idx++) - dart->save_ttbr[sid][idx] = - apple_dart_readl(dart, - DART_TTBR(dart, sid, idx)); - } - + apple_dart_save_state(dart); return 0; } @@ -4816,6 +4834,28 @@ static int apple_dart_invalidate_each_stream(struct apple_dart *dart) return 0; } +/* + * Resume of a DART whose live DMA stayed in place through suspend-to-idle. It + * cannot be restored from the saved copy. If the translations changed, the + * consumers' DMA is no longer what they set up: refuse their further use + * through the same paths as a translation fault before failing resume, since + * a resume error does not stop the PM core from resuming them. + */ +static int apple_dart_resume_retained(struct apple_dart *dart) +{ + if (!dart->s2idle_kept) + return -EBUSY; + dart->s2idle_kept = false; + if (apple_dart_state_retained(dart)) + return 0; + + dev_err(dart->dev, "retained DMA state lost in suspend-to-idle\n"); + if (dart->managed_sids) + apple_dart_managed_quarantine(dart); + apple_dart_quarantine_borrowed(dart); + return -EIO; +} + static __maybe_unused int apple_dart_resume(struct device *dev) { struct apple_dart *dart = dev_get_drvdata(dev); @@ -4826,7 +4866,7 @@ static __maybe_unused int apple_dart_resume(struct device *dev) if (dart->cold_ane) return 0; if (dart->managed_sids || dart->preserve_display || apple_dart_has_borrowed(dart)) - return -EBUSY; + return apple_dart_resume_retained(dart); /* PCIe-C restored these mappings before starting its downstream link. */ if (dart->tunnel_state_restored) { diff --git a/drivers/pci/controller/pcie-apple.c b/drivers/pci/controller/pcie-apple.c index 9ad89bba8415c..614b2577ad258 100644 --- a/drivers/pci/controller/pcie-apple.c +++ b/drivers/pci/controller/pcie-apple.c @@ -3638,8 +3638,12 @@ static int apple_pcie_suspend_noirq(struct device *dev) { struct apple_pcie *pcie = apple_pcie_lookup(dev); + /* + * The managed radio has no suspend boundary of its own; suspend-to-idle + * leaves it in place like any other internal port (no power domain). + */ if (pcie->managed_sids) - return -EBUSY; + return pm_suspend_target_state == PM_SUSPEND_TO_IDLE ? 0 : -EBUSY; if (!pcie->hw->tunneled) return 0; pcie->reset_on_resume = false; diff --git a/drivers/pmdomain/apple/pmgr-pwrstate.c b/drivers/pmdomain/apple/pmgr-pwrstate.c index a0350f831b6f2..5be4731606d9a 100644 --- a/drivers/pmdomain/apple/pmgr-pwrstate.c +++ b/drivers/pmdomain/apple/pmgr-pwrstate.c @@ -54,6 +54,8 @@ struct apple_pmgr_ps { bool externally_clocked; bool system_sleep; bool firmware_owned; + /* apple,sleep-retained: kept on while the system sleeps */ + bool sleep_retained; /* Never enable automatic power management (apple,no-auto-pm). */ bool no_auto_pm; bool inherited_on; @@ -263,6 +265,9 @@ static int apple_pmgr_ps_power_off(struct generic_pm_domain *genpd) return 0; if (ps->inherited_on) return -EBUSY; + /* A live coprocessor keeps this domain through system sleep. */ + if (ps->sleep_retained && ps->system_sleep) + return -EBUSY; ret = apple_pmgr_ps_set(genpd, APPLE_PMGR_PS_PWRGATE, false); if (ret) @@ -524,6 +529,7 @@ static int apple_pmgr_ps_probe(struct platform_device *pdev) * vote and never read, reset or change the hardware state. */ ps->firmware_owned = of_property_read_bool(node, "apple,firmware-owned"); + ps->sleep_retained = of_property_read_bool(node, "apple,sleep-retained"); /* * Only T8132 hands over inherited-on domains with this contract. Older * M3 device trees carry the property with another meaning; keep them @@ -531,7 +537,8 @@ static int apple_pmgr_ps_probe(struct platform_device *pdev) */ ps->inherited_on = t8132 && of_property_read_bool(node, "apple,inherited-on"); if (ps->inherited_on && - (ps->firmware_owned || of_property_present(node, "apple,min-state") || + (ps->firmware_owned || ps->sleep_retained || + of_property_present(node, "apple,min-state") || of_property_present(node, "apple,always-on") || of_property_present(node, "apple,force-disable") || of_property_present(node, "apple,force-reset") || diff --git a/drivers/soc/apple/mailbox.c b/drivers/soc/apple/mailbox.c index 2591560a9b716..6f0d1f7957ec0 100644 --- a/drivers/soc/apple/mailbox.c +++ b/drivers/soc/apple/mailbox.c @@ -780,6 +780,7 @@ static int apple_mbox_probe(struct platform_device *pdev) return -ENOMEM; mbox->dev = &pdev->dev; + mbox->hold_rx_in_sleep = of_property_read_bool(dev->of_node, "apple,hold-rx-in-sleep"); mbox->hw = of_device_get_match_data(dev); if (!mbox->hw) return -EINVAL; @@ -1133,10 +1134,39 @@ static const struct of_device_id apple_mbox_of_match[] = { }; MODULE_DEVICE_TABLE(of, apple_mbox_of_match); +/* + * The receive IRQ is IRQF_NO_SUSPEND, so each message wakes a CPU out of + * suspend-to-idle. A coprocessor whose messages need not wake the system + * keeps them in the FIFO until resume. The mailbox is a supplier of its + * consumer, so this runs after the consumer suspended and before it resumes. + */ +static int apple_mbox_suspend_noirq(struct device *dev) +{ + struct apple_mbox *mbox = dev_get_drvdata(dev); + + if (mbox && mbox->hold_rx_in_sleep) + apple_mbox_hold_rx(mbox); + return 0; +} + +static int apple_mbox_resume_noirq(struct device *dev) +{ + struct apple_mbox *mbox = dev_get_drvdata(dev); + + if (mbox && mbox->hold_rx_in_sleep) + apple_mbox_release_rx(mbox); + return 0; +} + +static const struct dev_pm_ops apple_mbox_pm_ops = { + NOIRQ_SYSTEM_SLEEP_PM_OPS(apple_mbox_suspend_noirq, apple_mbox_resume_noirq) +}; + static struct platform_driver apple_mbox_driver = { .driver = { .name = "apple-mailbox", .of_match_table = apple_mbox_of_match, + .pm = pm_sleep_ptr(&apple_mbox_pm_ops), }, .probe = apple_mbox_probe, .remove = apple_mbox_remove, diff --git a/include/linux/soc/apple/mailbox.h b/include/linux/soc/apple/mailbox.h index 4c27621f22e0a..7a07772cc5967 100644 --- a/include/linux/soc/apple/mailbox.h +++ b/include/linux/soc/apple/mailbox.h @@ -64,6 +64,8 @@ struct apple_mbox { bool active; bool ascwrap_v6_cpu_running; bool removing; + /* apple,hold-rx-in-sleep: receive IRQ masked across system sleep */ + bool hold_rx_in_sleep; int irq_recv_not_empty; int irq_send_empty; /* < 0: no send-empty IRQ, TX polls */ diff --git a/tools/testing/selftests/drivers/apple/dart-s2idle-retention.py b/tools/testing/selftests/drivers/apple/dart-s2idle-retention.py new file mode 100755 index 0000000000000..44682a977f716 --- /dev/null +++ b/tools/testing/selftests/drivers/apple/dart-s2idle-retention.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Exercise the suspend-to-idle resume check of retained DARTs on the host. + +The actual state check, quarantine and map functions are extracted from +apple-dart.c. A retained DART whose translation registers changed must fail +resume and quarantine its consumers, so that later maps are refused. These CPU +tests do not qualify physical retention on a J713. +""" +from pathlib import Path +import subprocess +import tempfile + + +def definition(source, name): + start = source.index(name) + start = source.rfind('\n', 0, start) + 1 + brace = source.index('{', start) + depth, end = 1, brace + 1 + while depth: + depth += (source[end] == '{') - (source[end] == '}') + end += 1 + return source[start:end] + + +PRELUDE = r''' +#include +#include +#include +#include +#include +#include +#include +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; +typedef u64 phys_addr_t; +typedef unsigned int gfp_t; +#define BIT(n) (1UL << (n)) +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) +#define WRITE_ONCE(x, v) ((x) = (v)) +#define READ_ONCE(x) (x) +#define DART_MAX_STREAMS 32 +#define DART_MAX_TTBR 4 +#define DART_TCR(d, sid) (0x100 + 4 * (sid)) +#define DART_TTBR(d, sid, idx) (0x200 + 16 * (sid) + 4 * (idx)) +#define container_of(p, t, m) ((t *)((char *)(p) - offsetof(t, m))) +#define lockdep_assert_held(l) ((void)(l)) +#define spin_lock_irqsave(l, f) ((void)(l), (f) = 0) +#define spin_unlock_irqrestore(l, f) ((void)(l), (void)(f)) +static int errors; +#define dev_err(dev, ...) (errors++) +static void __set_bit(unsigned int n, unsigned long *p) { p[n / 64] |= 1UL << (n % 64); } + +struct apple_dart_hw { u32 tcr_bypass, ttbr_valid; unsigned int ttbr_count; }; +struct apple_dart; +struct apple_dart_dcp { bool failed; struct apple_dart *dart; }; +struct apple_dart { + const struct apple_dart_hw *hw; + u16 managed_sids; + bool managed_failed, supports_bypass, s2idle_kept; + int managed_lock; + struct { bool ready; } iommu; + struct apple_dart_dcp *dcp, *frontend[2]; + void *dev; + unsigned int num_streams; + u32 save_tcr[DART_MAX_STREAMS], save_ttbr[DART_MAX_STREAMS][DART_MAX_TTBR]; + u32 regs[0x400 / 4]; +}; +struct apple_dart_stream_map { struct apple_dart *dart; unsigned long sidmap[4]; }; +static u32 apple_dart_readl(struct apple_dart *d, unsigned int o) { return d->regs[o / 4]; } +static unsigned long disabled; +static void apple_dart_hw_disable_dma(struct apple_dart_stream_map *m) { disabled |= m->sidmap[0]; } + +struct iommu_domain { int unused; }; +struct io_pgtable_ops { + int (*map_pages)(struct io_pgtable_ops *, unsigned long, phys_addr_t, size_t, size_t, int, + gfp_t, size_t *); +}; +struct apple_dart_domain { + struct iommu_domain domain; + struct io_pgtable_ops *pgtbl_ops; + struct apple_dart_dcp *dcp; + struct apple_dart_stream_map stream_maps[1]; + u64 dma_offset, mask; +}; +#define to_dart_domain(d) container_of(d, struct apple_dart_domain, domain) +static int apple_dart_check_fw_map(struct apple_dart_domain *d, u64 dva, phys_addr_t pa, + size_t size, int prot) { return 0; } +static int apple_dart_dcp_map(struct apple_dart_dcp *dcp, unsigned long iova, phys_addr_t pa, + size_t pgsize, size_t count, int prot, gfp_t gfp, size_t *mapped) +{ assert(!"the managed radio domain is not a borrowed display domain"); return 0; } +static int maps; +static int test_map(struct io_pgtable_ops *ops, unsigned long iova, phys_addr_t pa, size_t pgsize, + size_t count, int prot, gfp_t gfp, size_t *mapped) +{ maps++; *mapped = pgsize * count; return 0; } +''' + +TEST = r''' +static const struct apple_dart_hw hw = { .tcr_bypass = 0, .ttbr_valid = 1, .ttbr_count = 1 }; +static struct apple_dart_dcp dcp, front0, front1; +static struct apple_dart d; +static struct io_pgtable_ops ops = { .map_pages = test_map }; +static struct apple_dart_domain radio; + +static void setup(void) +{ + memset(&d, 0, sizeof(d)); + dcp.failed = front0.failed = front1.failed = false; + d.hw = &hw; + d.num_streams = 16; + d.managed_sids = BIT(1) | BIT(2); + d.dcp = &dcp; + d.frontend[0] = &front0; + d.frontend[1] = &front1; + d.iommu.ready = true; + /* Live translations on the radio's streams and on an ordinary one. */ + d.regs[DART_TCR(&d, 1) / 4] = d.regs[DART_TCR(&d, 2) / 4] = d.regs[DART_TCR(&d, 5) / 4] = 1; + d.regs[DART_TTBR(&d, 1, 0) / 4] = 0x1001; + d.regs[DART_TTBR(&d, 2, 0) / 4] = 0x2001; + d.regs[DART_TTBR(&d, 5, 0) / 4] = 0x5001; + for (unsigned int sid = 0; sid < d.num_streams; sid++) { + d.save_tcr[sid] = d.regs[DART_TCR(&d, sid) / 4]; + d.save_ttbr[sid][0] = d.regs[DART_TTBR(&d, sid, 0) / 4]; + } + d.s2idle_kept = true; + disabled = 0; + maps = 0; + errors = 0; + radio = (struct apple_dart_domain){ .pgtbl_ops = &ops, .mask = ~0ULL, + .stream_maps = {{ .dart = &d }} }; +} + +static int map_16k(void) +{ + size_t mapped = 0; + return apple_dart_map_pages(&radio.domain, 0x10000, 0x800000000ULL, 0x4000, 1, 3, 0, &mapped); +} + +static void expect_quarantined(int ret) +{ + assert(ret == -EIO && errors == 1); + assert(d.managed_failed && disabled == d.managed_sids); + assert(dcp.failed && front0.failed && front1.failed); + assert(map_16k() == -EIO && maps == 0); +} + +int main(void) +{ + /* Retained: resume succeeds and the radio maps as before. */ + setup(); + assert(apple_dart_resume_retained(&d) == 0 && !d.s2idle_kept); + assert(!d.managed_failed && !disabled && !dcp.failed && !errors); + assert(map_16k() == 0 && maps == 1); + + /* A changed TTBR on a managed stream. */ + setup(); + d.regs[DART_TTBR(&d, 1, 0) / 4] = 0x9001; + expect_quarantined(apple_dart_resume_retained(&d)); + + /* A changed TCR on an ordinary stream of the same DART. */ + setup(); + d.regs[DART_TCR(&d, 5) / 4] = 0; + expect_quarantined(apple_dart_resume_retained(&d)); + + /* A DART that lost power reads back as reset. */ + setup(); + memset(d.regs, 0, sizeof(d.regs)); + memset(d.save_tcr, 0, sizeof(d.save_tcr)); + memset(d.save_ttbr, 0, sizeof(d.save_ttbr)); + expect_quarantined(apple_dart_resume_retained(&d)); + + /* No suspend-to-idle suspend: refused, nothing quarantined. */ + setup(); + d.s2idle_kept = false; + assert(apple_dart_resume_retained(&d) == -EBUSY); + assert(!d.managed_failed && !dcp.failed && map_16k() == 0); + + puts("PASS retained DART resume: kept state resumes; lost state quarantines and refuses maps"); + return 0; +} +''' + + +def main(): + root = Path(__file__).resolve().parents[5] + dart = (root / 'drivers/iommu/apple-dart.c').read_text() + code = PRELUDE + for name in ('static bool apple_dart_state_retained(', + 'static void apple_dart_managed_quarantine_locked(', + 'static void apple_dart_managed_quarantine(', + 'static void apple_dart_quarantine_borrowed(', + 'static int apple_dart_resume_retained(', + 'static int apple_dart_map_pages('): + code += definition(dart, name) + '\n' + code += TEST + quarantine = ('\tif (dart->managed_sids)\n\t\tapple_dart_managed_quarantine(dart);\n' + '\tapple_dart_quarantine_borrowed(dart);\n') + assert code.count(quarantine) == 1, 'the resume quarantine was not found' + with tempfile.TemporaryDirectory() as directory: + c, exe = Path(directory) / 'resume.c', Path(directory) / 'resume' + for control in ('production', 'missing-quarantine'): + tested = code if control == 'production' else code.replace(quarantine, '') + c.write_text(tested) + subprocess.run(['cc', '-std=c11', '-Wall', '-Wextra', '-Werror', + '-Wno-unused-parameter', '-Wno-unused-function', + str(c), '-o', str(exe)], check=True) + result = subprocess.run([str(exe)], capture_output=True, text=True) + if control == 'production': + assert result.returncode == 0, result.stderr + print(result.stdout.strip()) + else: + assert result.returncode and 'Assertion' in result.stderr, result + print('Retained DART resume ' + control + ' negative control: detected at runtime') + + +if __name__ == '__main__': + main()