From 552d1d094f2fef3ba4703f03457cb598ffc09fce Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Tue, 21 Jul 2026 15:56:52 +0100 Subject: [PATCH 01/44] watchdog: bcm2835: Stop watchdog during suspend/resume Stop the watchdog during suspend to that it does not reset while suspended Signed-off-by: Peter Bailey --- drivers/watchdog/bcm2835_wdt.c | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/drivers/watchdog/bcm2835_wdt.c b/drivers/watchdog/bcm2835_wdt.c index ed4ae9fdf9d222..e322eb997463dc 100644 --- a/drivers/watchdog/bcm2835_wdt.c +++ b/drivers/watchdog/bcm2835_wdt.c @@ -209,6 +209,7 @@ static int bcm2835_wdt_probe(struct platform_device *pdev) watchdog_set_restart_priority(&bcm2835_wdt_wdd, 128); watchdog_stop_on_reboot(&bcm2835_wdt_wdd); + watchdog_stop_ping_on_suspend(&bcm2835_wdt_wdd); err = devm_watchdog_register_device(dev, &bcm2835_wdt_wdd); if (err) return err; @@ -232,11 +233,31 @@ static void bcm2835_wdt_remove(struct platform_device *pdev) pm_power_off = NULL; } +static int bcm2835_wdt_suspend(struct device *dev) +{ + if (watchdog_active(&bcm2835_wdt_wdd) || watchdog_hw_running(&bcm2835_wdt_wdd)) + bcm2835_wdt_stop(&bcm2835_wdt_wdd); + + return 0; +} + +static int bcm2835_wdt_resume(struct device *dev) +{ + if (watchdog_active(&bcm2835_wdt_wdd) || watchdog_hw_running(&bcm2835_wdt_wdd)) + bcm2835_wdt_start(&bcm2835_wdt_wdd); + + return 0; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(bcm2835_wdt_pm_ops, + bcm2835_wdt_suspend, bcm2835_wdt_resume); + static struct platform_driver bcm2835_wdt_driver = { .probe = bcm2835_wdt_probe, .remove = bcm2835_wdt_remove, .driver = { .name = "bcm2835-wdt", + .pm = pm_sleep_ptr(&bcm2835_wdt_pm_ops), }, }; module_platform_driver(bcm2835_wdt_driver); From 28c74baefabc9fd8b768a1728e76e3c889b76b12 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Tue, 21 Jul 2026 16:14:14 +0100 Subject: [PATCH 02/44] arm64: dts: bcm2712: wifi: Power off wifi during suspend/resume Add a device tree property so that wifi is powered off correctly during suspend. The wifi is powered off by the firmware during system suspend. The wifi driver does not allow runtime suspension so this will only affect system suspend. Signed-off-by: Peter Bailey --- arch/arm64/boot/dts/broadcom/bcm2712-rpi-5-b.dts | 1 + arch/arm64/boot/dts/broadcom/bcm2712-rpi-cm5.dtsi | 1 + 2 files changed, 2 insertions(+) diff --git a/arch/arm64/boot/dts/broadcom/bcm2712-rpi-5-b.dts b/arch/arm64/boot/dts/broadcom/bcm2712-rpi-5-b.dts index f3b16c868e2be7..d2fbad4d8478d5 100644 --- a/arch/arm64/boot/dts/broadcom/bcm2712-rpi-5-b.dts +++ b/arch/arm64/boot/dts/broadcom/bcm2712-rpi-5-b.dts @@ -427,6 +427,7 @@ dpi_16bit_gpio2: &rp1_dpi_16bit_gpio2 { }; vmmc-supply = <&wl_on_reg>; sd-uhs-ddr50; non-removable; + cap-power-off-card; status = "okay"; #address-cells = <1>; #size-cells = <0>; diff --git a/arch/arm64/boot/dts/broadcom/bcm2712-rpi-cm5.dtsi b/arch/arm64/boot/dts/broadcom/bcm2712-rpi-cm5.dtsi index dfad85a666a8a6..76ecc3d2e35826 100644 --- a/arch/arm64/boot/dts/broadcom/bcm2712-rpi-cm5.dtsi +++ b/arch/arm64/boot/dts/broadcom/bcm2712-rpi-cm5.dtsi @@ -411,6 +411,7 @@ dpi_16bit_gpio2: &rp1_dpi_16bit_gpio2 { }; vmmc-supply = <&wl_on_reg>; sd-uhs-ddr50; non-removable; + cap-power-off-card; status = "okay"; #address-cells = <1>; #size-cells = <0>; From 35b396a0c796c240c2113b32da4b085cfac03c22 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Thu, 23 Jul 2026 16:04:24 +0100 Subject: [PATCH 03/44] ACPI: PCI: bcm2712: Set acpi_storage_d3 to true on bcm2712 This makes the NVMe driver reset drives during suspend/resume. Signed-off-by: Peter Bailey --- include/linux/acpi.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/linux/acpi.h b/include/linux/acpi.h index 8871f2edca7126..a9faa5a52fa140 100644 --- a/include/linux/acpi.h +++ b/include/linux/acpi.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -1196,7 +1197,7 @@ static inline int acpi_dev_pm_attach(struct device *dev, bool power_on) } static inline bool acpi_storage_d3(struct device *dev) { - return false; + return of_machine_is_compatible("brcm,bcm2712"); } static inline bool acpi_dev_state_d0(struct device *dev) { From c3b30243eadbed2c71755373ac889d4857440211 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Wed, 12 Aug 2026 15:49:49 +0100 Subject: [PATCH 04/44] iommu: bcm2712-iommu: Add suspend/resume support Disable the iommu on suspend and reinitialise it on resume. Move allocating top_table and default_page into another function so that they do not run again on resume. Signed-off-by: Peter Bailey --- drivers/iommu/bcm2712-iommu.c | 69 +++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 16 deletions(-) diff --git a/drivers/iommu/bcm2712-iommu.c b/drivers/iommu/bcm2712-iommu.c index 566809275fecc2..8e8bd1a59dd931 100644 --- a/drivers/iommu/bcm2712-iommu.c +++ b/drivers/iommu/bcm2712-iommu.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #define MMU_WR(off, val) writel(val, mmu->reg_base + (off)) @@ -144,7 +145,24 @@ static void bcm2712_iommu_free_page(struct bcm2712_iommu *mmu, void *ptr) } } -static int bcm2712_iommu_init(struct bcm2712_iommu *mmu) +static int bcm2712_iommu_init_pages(struct bcm2712_iommu *mmu) +{ + /* + * Allocate pages for the top level table, and for the default page. + * For simplicity, both these regions are whole Linux pages. + */ + if (!bcm2712_iommu_get_page(mmu, &mmu->top_table)) + return -ENOMEM; + + if (!bcm2712_iommu_get_page(mmu, &mmu->default_page)) { + bcm2712_iommu_free_page(mmu, mmu->top_table); + return -ENOMEM; + } + + return 0; +} + +static int bcm2712_iommu_hw_init(struct bcm2712_iommu *mmu) { u32 u = MMU_RD(MMMU_DEBUG_INFO_OFFSET); @@ -205,20 +223,12 @@ static int bcm2712_iommu_init(struct bcm2712_iommu *mmu) /* * Configure the addresses of the top-level table (offset because * the aperture does not start from zero), and of the default page. - * For simplicity, both these regions are whole Linux pages. */ - u = bcm2712_iommu_get_page(mmu, &mmu->top_table); - if (!u) - return -ENOMEM; MMU_WR(MMMU_PT_PA_BASE_OFFSET, - u - ((mmu->aperture_base - mmu->dma_iova_offset) >> L1_AP_BASE_SHIFT)); - u = bcm2712_iommu_get_page(mmu, &mmu->default_page); - if (!u) { - bcm2712_iommu_free_page(mmu, mmu->top_table); - return -ENOMEM; - } - MMU_WR(MMMU_ILLEGAL_ADR_OFFSET, MMMU_ILLEGAL_ADR_ENABLE + u); - mmu->nmapped_pages = 0; + (u32)(virt_to_phys(mmu->top_table) >> IOMMU_PAGE_SHIFT) - + ((mmu->aperture_base - mmu->dma_iova_offset) >> L1_AP_BASE_SHIFT)); + MMU_WR(MMMU_ILLEGAL_ADR_OFFSET, MMMU_ILLEGAL_ADR_ENABLE + + (u32)(virt_to_phys(mmu->default_page) >> IOMMU_PAGE_SHIFT)); /* Flush (and enable) the shared TLB cache; enable this MMU. */ if (mmu->cache) @@ -714,8 +724,13 @@ static int bcm2712_iommu_probe(struct platform_device *pdev) if (ret) goto done_err; - /* Initialize hardware -- this will try to allocate 2 pages */ - ret = bcm2712_iommu_init(mmu); + /* This will try to allocate 2 pages */ + ret = bcm2712_iommu_init_pages(mmu); + if (ret) + goto done_err; + + /* Initialize hardware */ + ret = bcm2712_iommu_hw_init(mmu); if (ret) goto done_err; @@ -745,6 +760,27 @@ static void bcm2712_iommu_remove(struct platform_device *pdev) MMU_WR(MMMU_CTRL_OFFSET, 0); /* disable the MMU */ } +static int bcm2712_iommu_suspend(struct device *dev) +{ + struct bcm2712_iommu *mmu = dev_get_drvdata(dev); + + if (mmu->reg_base) + MMU_WR(MMMU_CTRL_OFFSET, 0); /* disable the MMU */ + + return 0; +} + +static int bcm2712_iommu_resume(struct device *dev) +{ + struct bcm2712_iommu *mmu = dev_get_drvdata(dev); + + return bcm2712_iommu_hw_init(mmu); +} + +static const struct dev_pm_ops bcm2712_iommu_pm_ops = { + LATE_SYSTEM_SLEEP_PM_OPS(bcm2712_iommu_suspend, bcm2712_iommu_resume) +}; + static const struct of_device_id bcm2712_iommu_of_match[] = { { . compatible = "brcm,bcm2712-iommu" @@ -757,7 +793,8 @@ static struct platform_driver bcm2712_iommu_driver = { .remove = bcm2712_iommu_remove, .driver = { .name = "bcm2712-iommu", - .of_match_table = bcm2712_iommu_of_match + .of_match_table = bcm2712_iommu_of_match, + .pm = pm_sleep_ptr(&bcm2712_iommu_pm_ops), }, }; From 6dfd94352fcccb17927595cd402aac0ed3bbbc3e Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Wed, 12 Aug 2026 15:56:37 +0100 Subject: [PATCH 05/44] drm/vc4: hdmi: Add suspend/resume support Force a runtime suspend during system suspend. This prevents occasional warnings about packet RAM being off. Signed-off-by: Peter Bailey --- drivers/gpu/drm/vc4/vc4_hdmi.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/gpu/drm/vc4/vc4_hdmi.c b/drivers/gpu/drm/vc4/vc4_hdmi.c index e858a5b878781a..ff26e8e479bb25 100644 --- a/drivers/gpu/drm/vc4/vc4_hdmi.c +++ b/drivers/gpu/drm/vc4/vc4_hdmi.c @@ -3453,6 +3453,8 @@ static const struct dev_pm_ops vc4_hdmi_pm_ops = { SET_RUNTIME_PM_OPS(vc4_hdmi_runtime_suspend, vc4_hdmi_runtime_resume, NULL) + LATE_SYSTEM_SLEEP_PM_OPS(pm_runtime_force_suspend, + pm_runtime_force_resume) }; struct platform_driver vc4_hdmi_driver = { From 401e2a658ce97d3605cdcaa22d22291950dc30c1 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Wed, 12 Aug 2026 15:57:58 +0100 Subject: [PATCH 06/44] drm/vc4: Add suspend/resume support Use the drm_mode_config_helper_ functions on suspend and resume. Signed-off-by: Peter Bailey --- drivers/gpu/drm/vc4/vc4_drv.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/drivers/gpu/drm/vc4/vc4_drv.c b/drivers/gpu/drm/vc4/vc4_drv.c index 71a74b6fcd760d..5712356914661e 100644 --- a/drivers/gpu/drm/vc4/vc4_drv.c +++ b/drivers/gpu/drm/vc4/vc4_drv.c @@ -497,6 +497,22 @@ static void vc4_platform_drm_shutdown(struct platform_device *pdev) drm_atomic_helper_shutdown(platform_get_drvdata(pdev)); } +static int vc4_drm_suspend(struct device *dev) +{ + struct drm_device *drm = dev_get_drvdata(dev); + + return drm_mode_config_helper_suspend(drm); +} + +static int vc4_drm_resume(struct device *dev) +{ + struct drm_device *drm = dev_get_drvdata(dev); + + return drm_mode_config_helper_resume(drm); +} + +static DEFINE_SIMPLE_DEV_PM_OPS(vc4_drm_pm_ops, vc4_drm_suspend, vc4_drm_resume); + static const struct of_device_id vc4_of_match[] = { { .compatible = "brcm,bcm2711-vc5", .data = (void *)VC4_GEN_5 }, /* NB GEN_6_C will be corrected on D0 hw to GEN_6_D via vc4_hvs_bind */ @@ -514,6 +530,7 @@ static struct platform_driver vc4_platform_driver = { .driver = { .name = "vc4-drm", .of_match_table = vc4_of_match, + .pm = pm_sleep_ptr(&vc4_drm_pm_ops), }, }; From 5593b5ba195057f0ccceec80acd0cd94029bfc05 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Wed, 12 Aug 2026 15:58:29 +0100 Subject: [PATCH 07/44] drm/vc4: hvs: Add suspend/resume support Reinitialise the hardware on resume. Split vc4_hvs_upload_linear_kernel into two functions. On resume, the kernels have already been allocated. Move writing to the hardware into another function and call that on resume. Signed-off-by: Peter Bailey --- drivers/gpu/drm/vc4/vc4_hvs.c | 53 +++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/drivers/gpu/drm/vc4/vc4_hvs.c b/drivers/gpu/drm/vc4/vc4_hvs.c index 97e22d6c226de0..849390f1a16350 100644 --- a/drivers/gpu/drm/vc4/vc4_hvs.c +++ b/drivers/gpu/drm/vc4/vc4_hvs.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -460,12 +461,22 @@ static const u32 nearest_neighbour_kernel[] = VC4_LINEAR_PHASE_KERNEL(0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 255, 255, 255, 255); +static void vc4_hvs_write_linear_kernel(struct vc4_hvs *hvs, + struct drm_mm_node *space, + const u32 *kernel) +{ + u32 __iomem *dst_kernel = hvs->dlist + space->start; + unsigned int i; + + for (i = 0; i < VC4_KERNEL_DWORDS; i++) + writel(kernel[i], &dst_kernel[i]); +} + static int vc4_hvs_upload_linear_kernel(struct vc4_hvs *hvs, struct drm_mm_node *space, const u32 *kernel) { - int ret, i; - u32 __iomem *dst_kernel; + int ret; /* * NOTE: We don't need a call to drm_dev_enter()/drm_dev_exit() @@ -479,10 +490,7 @@ static int vc4_hvs_upload_linear_kernel(struct vc4_hvs *hvs, return ret; } - dst_kernel = hvs->dlist + space->start; - - for (i = 0; i < VC4_KERNEL_DWORDS; i++) - writel(kernel[i], &dst_kernel[i]); + vc4_hvs_write_linear_kernel(hvs, space, kernel); return 0; } @@ -2134,6 +2142,8 @@ static int vc4_hvs_bind(struct device *dev, struct device *master, void *data) if (IS_ERR(hvs)) return PTR_ERR(hvs); + platform_set_drvdata(pdev, hvs); + hvs->regset.base = hvs->regs; if (vc4->gen == VC4_GEN_6_C) { @@ -2298,6 +2308,7 @@ static void vc4_hvs_unbind(struct device *dev, struct device *master, clk_set_min_rate(hvs->core_clk, 0); clk_disable_unprepare(hvs->core_clk); + platform_set_drvdata(to_platform_device(dev), NULL); vc4->hvs = NULL; } @@ -2316,6 +2327,35 @@ static void vc4_hvs_dev_remove(struct platform_device *pdev) component_del(&pdev->dev, &vc4_hvs_ops); } +static int vc4_hvs_resume_early(struct device *dev) +{ + struct vc4_hvs *hvs = platform_get_drvdata(to_platform_device(dev)); + struct vc4_dev *vc4; + int ret; + + if (!hvs) + return 0; + + vc4 = hvs->vc4; + if (vc4->gen >= VC4_GEN_6_C) + ret = vc6_hvs_hw_init(hvs); + else + ret = vc4_hvs_hw_init(hvs); + if (ret) + return ret; + + vc4_hvs_write_linear_kernel(hvs, &hvs->mitchell_netravali_filter, + mitchell_netravali_1_3_1_3_kernel); + vc4_hvs_write_linear_kernel(hvs, &hvs->nearest_neighbour_filter, + nearest_neighbour_kernel); + + return vc4_hvs_cob_init(hvs); +} + +static const struct dev_pm_ops vc4_hvs_pm_ops = { + LATE_SYSTEM_SLEEP_PM_OPS(NULL, vc4_hvs_resume_early) +}; + static const struct of_device_id vc4_hvs_dt_match[] = { { .compatible = "brcm,bcm2711-hvs" }, { .compatible = "brcm,bcm2712-hvs" }, @@ -2329,5 +2369,6 @@ struct platform_driver vc4_hvs_driver = { .driver = { .name = "vc4_hvs", .of_match_table = vc4_hvs_dt_match, + .pm = pm_sleep_ptr(&vc4_hvs_pm_ops), }, }; From b56b86d1e5d8ef5b2d18f97af2c73636a0224457 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Fri, 14 Aug 2026 14:22:55 +0100 Subject: [PATCH 08/44] gpio-brcmstb: Enable hibernation support Use the suspend/resume methods for hibernation. Signed-off-by: Peter Bailey --- drivers/gpio/gpio-brcmstb.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/gpio/gpio-brcmstb.c b/drivers/gpio/gpio-brcmstb.c index 7e2e3f3829efec..e004c4b1cde38a 100644 --- a/drivers/gpio/gpio-brcmstb.c +++ b/drivers/gpio/gpio-brcmstb.c @@ -10,6 +10,7 @@ #include #include #include +#include #include enum gio_reg_index { @@ -576,8 +577,7 @@ static int brcmstb_gpio_resume(struct device *dev) #endif /* CONFIG_PM_SLEEP */ static const struct dev_pm_ops brcmstb_gpio_pm_ops = { - .suspend_noirq = brcmstb_gpio_suspend, - .resume_noirq = brcmstb_gpio_resume, + SET_NOIRQ_SYSTEM_SLEEP_PM_OPS(brcmstb_gpio_suspend, brcmstb_gpio_resume) }; static int brcmstb_gpio_probe(struct platform_device *pdev) From 1011b7b82738468b04eb8a5ed5ed17c2e31ae8cf Mon Sep 17 00:00:00 2001 From: Tim Gover Date: Mon, 17 Aug 2026 08:42:25 +0100 Subject: [PATCH 09/44] mailbox: bcm2835: Add resume handler for S3 wakeup on Pi5 In S3 deep-sleep, the entire VPU, ARM, GIC infrastructure is powered off. Re-initialise the mailbox hardware on resume otherwise, the VPU won't see mailbox requests. Signed-off-by: Tim Gover --- drivers/mailbox/bcm2835-mailbox.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/drivers/mailbox/bcm2835-mailbox.c b/drivers/mailbox/bcm2835-mailbox.c index 8c54980b4b7e12..f458d477bc8aab 100644 --- a/drivers/mailbox/bcm2835-mailbox.c +++ b/drivers/mailbox/bcm2835-mailbox.c @@ -183,6 +183,25 @@ static int bcm2835_mbox_probe(struct platform_device *pdev) return ret; } +static int bcm2835_mbox_resume_noirq(struct device *dev) +{ + struct bcm2835_mbox *mbox = dev_get_drvdata(dev); + + /* + * MAIL0_CNF is reset while the SoC is powered down in suspend-to-RAM. + * Re-enable the receive interrupt before any driver resumes, otherwise + * the replies are never signalled and every firmware transaction times + * out. + */ + writel(ARM_MC_IHAVEDATAIRQEN, mbox->regs + MAIL0_CNF); + + return 0; +} + +static const struct dev_pm_ops bcm2835_mbox_pm_ops = { + NOIRQ_SYSTEM_SLEEP_PM_OPS(NULL, bcm2835_mbox_resume_noirq) +}; + static const struct of_device_id bcm2835_mbox_of_match[] = { { .compatible = "brcm,bcm2835-mbox", }, {}, @@ -193,6 +212,7 @@ static struct platform_driver bcm2835_mbox_driver = { .driver = { .name = "bcm2835-mbox", .of_match_table = bcm2835_mbox_of_match, + .pm = pm_sleep_ptr(&bcm2835_mbox_pm_ops), }, .probe = bcm2835_mbox_probe, }; From 0b7a664b0db8eeff3e1d92df4e46e7d773453445 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Mon, 17 Aug 2026 17:37:30 +0100 Subject: [PATCH 10/44] pinctrl: pinctrl-brcmstb: Save pin mux and pad during suspend Save the pin mux and pad during suspend and restore it during resume. Multiple pins are stored in each register. Store the entire register for each pin. The values will not change between storing and restoring the individual pins. This uses a few extra bytes but simplifies the code. Signed-off-by: Peter Bailey --- drivers/pinctrl/bcm/pinctrl-brcmstb-bcm2712.c | 6 +++ drivers/pinctrl/bcm/pinctrl-brcmstb.c | 49 +++++++++++++++++++ drivers/pinctrl/bcm/pinctrl-brcmstb.h | 2 + 3 files changed, 57 insertions(+) diff --git a/drivers/pinctrl/bcm/pinctrl-brcmstb-bcm2712.c b/drivers/pinctrl/bcm/pinctrl-brcmstb-bcm2712.c index 752b78e2c0d8ce..befd6680cd69b9 100644 --- a/drivers/pinctrl/bcm/pinctrl-brcmstb-bcm2712.c +++ b/drivers/pinctrl/bcm/pinctrl-brcmstb-bcm2712.c @@ -13,6 +13,7 @@ #include #include +#include #include "pinctrl-brcmstb.h" #define BRCMSTB_FSEL_COUNT 8 @@ -729,12 +730,17 @@ static const struct of_device_id bcm2712_pinctrl_match[] = { }; MODULE_DEVICE_TABLE(of, bcm2712_pinctrl_match); +static const struct dev_pm_ops bcm2712_pinctrl_pm_ops = { + SET_LATE_SYSTEM_SLEEP_PM_OPS(brcmstb_pinctrl_suspend, brcmstb_pinctrl_resume) +}; + static struct platform_driver bcm2712_pinctrl_driver = { .probe = bcm2712_pinctrl_probe, .driver = { .name = "pinctrl-bcm2712", .of_match_table = bcm2712_pinctrl_match, .suppress_bind_attrs = true, + .pm = &bcm2712_pinctrl_pm_ops, }, }; module_platform_driver(bcm2712_pinctrl_driver); diff --git a/drivers/pinctrl/bcm/pinctrl-brcmstb.c b/drivers/pinctrl/bcm/pinctrl-brcmstb.c index f46b27155c3c40..b32822558bbc4d 100644 --- a/drivers/pinctrl/bcm/pinctrl-brcmstb.c +++ b/drivers/pinctrl/bcm/pinctrl-brcmstb.c @@ -49,6 +49,8 @@ struct brcmstb_pinctrl { struct pinctrl_gpio_range gpio_range; /* Protect FSEL registers */ spinlock_t fsel_lock; + u32 *saved_mux; + u32 *saved_pad; }; static unsigned int brcmstb_pinctrl_fsel_get(struct brcmstb_pinctrl *pc, @@ -422,6 +424,13 @@ int brcmstb_pinctrl_probe(struct platform_device *pdev) pc->func_count = pdata->func_count; pc->func_names = pdata->func_names; + pc->saved_mux = devm_kcalloc(dev, num_pins, sizeof(*pc->saved_mux), + GFP_KERNEL); + pc->saved_pad = devm_kcalloc(dev, num_pins, sizeof(*pc->saved_pad), + GFP_KERNEL); + if (!pc->saved_mux || !pc->saved_pad) + return -ENOMEM; + pc->pctl_dev = devm_pinctrl_register(dev, &pc->pctl_desc, pc); if (IS_ERR(pc->pctl_dev)) return dev_err_probe(&pdev->dev, PTR_ERR(pc->pctl_dev), @@ -434,6 +443,46 @@ int brcmstb_pinctrl_probe(struct platform_device *pdev) } EXPORT_SYMBOL(brcmstb_pinctrl_probe); +int brcmstb_pinctrl_suspend(struct device *dev) +{ + struct brcmstb_pinctrl *pc = dev_get_drvdata(dev); + unsigned int i, num_pins = pc->pctl_desc.npins; + u32 bit; + + for (i = 0; i < num_pins; i++) { + bit = pc->pin_regs[i].mux_bit; + if (bit) + pc->saved_mux[i] = readl(pc->base + BIT_TO_REG(bit & ~MUX_BIT_VALID)); + + bit = pc->pin_regs[i].pad_bit; + if (bit != PAD_BIT_INVALID) + pc->saved_pad[i] = readl(pc->base + BIT_TO_REG(bit)); + } + + return 0; +} +EXPORT_SYMBOL(brcmstb_pinctrl_suspend); + +int brcmstb_pinctrl_resume(struct device *dev) +{ + struct brcmstb_pinctrl *pc = dev_get_drvdata(dev); + unsigned int i, num_pins = pc->pctl_desc.npins; + u32 bit; + + for (i = 0; i < num_pins; i++) { + bit = pc->pin_regs[i].mux_bit; + if (bit) + writel(pc->saved_mux[i], pc->base + BIT_TO_REG(bit & ~MUX_BIT_VALID)); + + bit = pc->pin_regs[i].pad_bit; + if (bit != PAD_BIT_INVALID) + writel(pc->saved_pad[i], pc->base + BIT_TO_REG(bit)); + } + + return 0; +} +EXPORT_SYMBOL(brcmstb_pinctrl_resume); + MODULE_AUTHOR("Phil Elwell"); MODULE_AUTHOR("Jonathan Bell"); MODULE_AUTHOR("Ivan T. Ivanov"); diff --git a/drivers/pinctrl/bcm/pinctrl-brcmstb.h b/drivers/pinctrl/bcm/pinctrl-brcmstb.h index c3459103e05639..e5f8049eb0e517 100644 --- a/drivers/pinctrl/bcm/pinctrl-brcmstb.h +++ b/drivers/pinctrl/bcm/pinctrl-brcmstb.h @@ -89,5 +89,7 @@ struct brcmstb_pdata { }; int brcmstb_pinctrl_probe(struct platform_device *pdev); +int brcmstb_pinctrl_suspend(struct device *dev); +int brcmstb_pinctrl_resume(struct device *dev); #endif From 288d783a77007e28e9ef53f0f12df275ff29e5bb Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Tue, 18 Aug 2026 15:22:44 +0100 Subject: [PATCH 11/44] serial: amba-pl011: Add start_rx to re-enable interrupts Re-enable interrupts in start_rx. This fixes the serial console ignoring input after a system suspend with no_console_suspend set. Signed-off-by: Peter Bailey --- drivers/tty/serial/amba-pl011.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/drivers/tty/serial/amba-pl011.c b/drivers/tty/serial/amba-pl011.c index 167ae0c0056ea5..2e1a0d2b76fbb0 100644 --- a/drivers/tty/serial/amba-pl011.c +++ b/drivers/tty/serial/amba-pl011.c @@ -1468,6 +1468,24 @@ static void pl011_stop_rx(struct uart_port *port) pl011_dma_rx_stop(uap); } +static void pl011_start_rx(struct uart_port *port) +{ + struct uart_amba_port *uap = + container_of(port, struct uart_amba_port, port); + + uap->im |= UART011_RTIM; + if (!pl011_dma_rx_running(uap)) + uap->im |= UART011_RXIM; + pl011_write(uap->im, uap, REG_IMSC); + +#ifdef CONFIG_DMA_ENGINE + if (uap->using_rx_dma) { + uap->dmacr |= UART011_RXDMAE; + pl011_write(uap->dmacr, uap, REG_DMACR); + } +#endif +} + static void pl011_throttle_rx(struct uart_port *port) { unsigned long flags; @@ -2354,6 +2372,7 @@ static const struct uart_ops amba_pl011_pops = { .stop_tx = pl011_stop_tx, .start_tx = pl011_start_tx, .stop_rx = pl011_stop_rx, + .start_rx = pl011_start_rx, .throttle = pl011_throttle_rx, .unthrottle = pl011_unthrottle_rx, .enable_ms = pl011_enable_ms, @@ -2388,6 +2407,7 @@ static const struct uart_ops sbsa_uart_pops = { .stop_tx = pl011_stop_tx, .start_tx = pl011_start_tx, .stop_rx = pl011_stop_rx, + .start_rx = pl011_start_rx, .startup = sbsa_uart_startup, .shutdown = sbsa_uart_shutdown, .set_termios = sbsa_uart_set_termios, From 8fef05a07306e37fa030211a137aeccbed47ec9b Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Tue, 18 Aug 2026 18:00:47 +0100 Subject: [PATCH 12/44] irqchip: bcm2712-mip: Restore registers after suspend The registers get reset during suspend. Restore them on resume. Signed-off-by: Peter Bailey --- drivers/irqchip/irq-bcm2712-mip.c | 38 +++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/drivers/irqchip/irq-bcm2712-mip.c b/drivers/irqchip/irq-bcm2712-mip.c index 4761974ad650a9..e1634aac32ccae 100644 --- a/drivers/irqchip/irq-bcm2712-mip.c +++ b/drivers/irqchip/irq-bcm2712-mip.c @@ -10,6 +10,7 @@ #include #include #include +#include #include @@ -170,6 +171,19 @@ static const struct msi_parent_ops mip_msi_parent_ops = { .init_dev_msi_info = msi_lib_init_dev_msi_info, }; +static void mip_hw_init(struct mip_priv *mip) +{ + /* + * All MSI-X unmasked for the host, masked for the VPU, and edge-triggered. + */ + writel(0, mip->base + MIP_INT_MASKL_HOST); + writel(0, mip->base + MIP_INT_MASKH_HOST); + writel(~0, mip->base + MIP_INT_MASKL_VPU); + writel(~0, mip->base + MIP_INT_MASKH_VPU); + writel(~0, mip->base + MIP_INT_CFGL_HOST); + writel(~0, mip->base + MIP_INT_CFGH_HOST); +} + static int mip_init_domains(struct mip_priv *mip, struct device_node *np) { struct irq_domain_info info = { @@ -184,15 +198,7 @@ static int mip_init_domains(struct mip_priv *mip, struct device_node *np) if (!msi_create_parent_irq_domain(&info, &mip_msi_parent_ops)) return -ENOMEM; - /* - * All MSI-X unmasked for the host, masked for the VPU, and edge-triggered. - */ - writel(0, mip->base + MIP_INT_MASKL_HOST); - writel(0, mip->base + MIP_INT_MASKH_HOST); - writel(~0, mip->base + MIP_INT_MASKL_VPU); - writel(~0, mip->base + MIP_INT_MASKH_VPU); - writel(~0, mip->base + MIP_INT_CFGL_HOST); - writel(~0, mip->base + MIP_INT_CFGH_HOST); + mip_hw_init(mip); return 0; } @@ -244,6 +250,7 @@ static int mip_msi_probe(struct platform_device *pdev, struct device_node *paren spin_lock_init(&mip->lock); mip->dev = &pdev->dev; + platform_set_drvdata(pdev, mip); ret = mip_parse_dt(mip, node); if (ret) @@ -279,9 +286,20 @@ static int mip_msi_probe(struct platform_device *pdev, struct device_node *paren return ret; } +static int mip_resume(struct device *dev) +{ + struct mip_priv *mip = dev_get_drvdata(dev); + + mip_hw_init(mip); + + return 0; +} + +static DEFINE_NOIRQ_DEV_PM_OPS(mip_pm_ops, NULL, mip_resume); + IRQCHIP_PLATFORM_DRIVER_BEGIN(mip_msi) IRQCHIP_MATCH("brcm,bcm2712-mip", mip_msi_probe) -IRQCHIP_PLATFORM_DRIVER_END(mip_msi) +IRQCHIP_PLATFORM_DRIVER_END(mip_msi, .pm = pm_sleep_ptr(&mip_pm_ops)) MODULE_DESCRIPTION("Broadcom BCM2712 MSI-X interrupt controller"); MODULE_AUTHOR("Phil Elwell "); MODULE_AUTHOR("Stanimir Varbanov "); From b56bc49b06bc7a6bf73187679db6abcccf19eb7d Mon Sep 17 00:00:00 2001 From: Jonathan Bell Date: Wed, 19 Aug 2026 10:57:39 +0100 Subject: [PATCH 13/44] mailbox: rp1: check received event bits more carefully There are 32 individual event bits of which 4 correspond to mailbox channels. Limit the IRQ handler to signalling mailbox events on actual mailboxes, to prevent all-1s completions (such as when the link is down) or RP1 firmware bugs from causing null pointer dereferences. Signed-off-by: Jonathan Bell --- drivers/mailbox/rp1-mailbox.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/drivers/mailbox/rp1-mailbox.c b/drivers/mailbox/rp1-mailbox.c index a9a86a9ec89b93..eb8c585aef07aa 100644 --- a/drivers/mailbox/rp1-mailbox.c +++ b/drivers/mailbox/rp1-mailbox.c @@ -66,8 +66,11 @@ static irqreturn_t rp1_mbox_irq(int irq, void *dev_id) while (evs) { doorbell = __ffs(evs); + if (doorbell >= MAX_CHANS) + break; chan = &mbox->controller.chans[doorbell]; - mbox_chan_received_data(chan, NULL); + if (chan->cl) + mbox_chan_received_data(chan, NULL); evs &= ~(1 << doorbell); } return IRQ_HANDLED; From ca91c314e92469671c36ecbf4b9dec7b291a1ca0 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Wed, 19 Aug 2026 15:06:54 +0100 Subject: [PATCH 14/44] mmc: sdhci-brcmstb: Reconfigure on resume Rerun the sdhci initialisation on resume. Signed-off-by: Peter Bailey --- drivers/mmc/host/sdhci-brcmstb.c | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/drivers/mmc/host/sdhci-brcmstb.c b/drivers/mmc/host/sdhci-brcmstb.c index 0b1823ec06c115..14947a09d1171f 100644 --- a/drivers/mmc/host/sdhci-brcmstb.c +++ b/drivers/mmc/host/sdhci-brcmstb.c @@ -52,6 +52,7 @@ struct sdhci_brcmstb_priv { void __iomem *cfg_regs; + void (*cfginit)(struct sdhci_host *host); unsigned int flags; struct clk *base_clk; u32 base_freq_hz; @@ -600,6 +601,7 @@ static int sdhci_brcmstb_probe(struct platform_device *pdev) pltfm_host->clk = clk; priv = sdhci_pltfm_priv(pltfm_host); + priv->cfginit = match_priv->cfginit; cqe = 0; device_property_read_u32(&pdev->dev, "supports-cqe", &cqe); if (cqe > 0) { @@ -681,8 +683,8 @@ static int sdhci_brcmstb_probe(struct platform_device *pdev) (priv->flags & BRCMSTB_PRIV_FLAGS_HAS_SD_EXPRESS)) host->mmc->caps2 |= MMC_CAP2_SD_EXP; - if (match_priv->cfginit) - match_priv->cfginit(host); + if (priv->cfginit) + priv->cfginit(host); /* * Supply the existing CAPS, but clear the UHS modes. This @@ -772,8 +774,20 @@ static int sdhci_brcmstb_resume(struct device *dev) struct sdhci_brcmstb_priv *priv = sdhci_pltfm_priv(pltfm_host); int ret; - ret = sdhci_pltfm_resume(dev); - if (!ret && priv->base_freq_hz) { + ret = clk_prepare_enable(pltfm_host->clk); + if (ret) + return ret; + + if (priv->cfginit) + priv->cfginit(host); + + ret = sdhci_resume_host(host); + if (ret) { + clk_disable_unprepare(pltfm_host->clk); + return ret; + } + + if (priv->base_freq_hz) { ret = clk_prepare_enable(priv->base_clk); /* * Note: using clk_get_rate() below as clk_get_rate() From 175640486b44d6b40343a8717034c29116530565 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Fri, 21 Aug 2026 16:26:43 +0100 Subject: [PATCH 15/44] media: pisp_be: Add suspend/resume support On suspend, check if there is a current job being processed. Suspending takes longer than a pisp job so assume it will be done and cancel suspending if it is not. On resume, restore hardware registers. Signed-off-by: Peter Bailey --- .../platform/raspberrypi/pisp_be/pisp_be.c | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/drivers/media/platform/raspberrypi/pisp_be/pisp_be.c b/drivers/media/platform/raspberrypi/pisp_be/pisp_be.c index c6c73097d04022..f155a54b516c98 100644 --- a/drivers/media/platform/raspberrypi/pisp_be/pisp_be.c +++ b/drivers/media/platform/raspberrypi/pisp_be/pisp_be.c @@ -5,6 +5,7 @@ * */ #include +#include #include #include #include @@ -1826,8 +1827,55 @@ static void pispbe_remove(struct platform_device *pdev) pm_runtime_disable(pispbe->dev); } +static int pispbe_suspend(struct device *dev) +{ + struct pispbe_dev *pispbe = dev_get_drvdata(dev); + bool busy; + + scoped_guard(spinlock_irqsave, &pispbe->hw_lock) { + busy = pispbe->hw_busy; + } + + if (busy) { + msleep(100); + + scoped_guard(spinlock_irqsave, &pispbe->hw_lock) { + busy = pispbe->hw_busy; + } + } + + if (busy) { + dev_err(dev, "Timed out waiting for ISP-BE to finish.\n"); + return -EBUSY; + } + + return pm_runtime_force_suspend(dev); +} + +static int pispbe_resume(struct device *dev) +{ + struct pispbe_dev *pispbe = dev_get_drvdata(dev); + int ret; + + ret = pm_runtime_force_resume(dev); + if (ret) + return ret; + + ret = pm_runtime_resume_and_get(dev); + if (ret) + return ret; + + ret = pispbe_hw_init(pispbe); + + pm_runtime_mark_last_busy(dev); + pm_runtime_put_autosuspend(dev); + + return ret; +} + static const struct dev_pm_ops pispbe_pm_ops = { SET_RUNTIME_PM_OPS(pispbe_runtime_suspend, pispbe_runtime_resume, NULL) + SYSTEM_SLEEP_PM_OPS(pispbe_suspend, pispbe_resume) }; static const struct of_device_id pispbe_of_match[] = { From 2db41a268469813fd1f0f621a46fe751f4728df4 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Mon, 24 Aug 2026 10:56:24 +0100 Subject: [PATCH 16/44] drm/rp1: dsi: Add suspend/resume support Use the modeset helper functions in suspend. Signed-off-by: Peter Bailey --- drivers/gpu/drm/rp1/rp1-dsi/rp1_dsi.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/drivers/gpu/drm/rp1/rp1-dsi/rp1_dsi.c b/drivers/gpu/drm/rp1/rp1-dsi/rp1_dsi.c index d452b0b1804e62..6f38297908ddc3 100644 --- a/drivers/gpu/drm/rp1/rp1-dsi/rp1_dsi.c +++ b/drivers/gpu/drm/rp1/rp1-dsi/rp1_dsi.c @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -522,6 +523,23 @@ static void rp1dsi_platform_shutdown(struct platform_device *pdev) rp1dsi_stopall(drm); } +static int rp1dsi_platform_suspend(struct device *dev) +{ + struct drm_device *drm = dev_get_drvdata(dev); + + return drm_mode_config_helper_suspend(drm); +} + +static int rp1dsi_platform_resume(struct device *dev) +{ + struct drm_device *drm = dev_get_drvdata(dev); + + return drm_mode_config_helper_resume(drm); +} + +static DEFINE_SIMPLE_DEV_PM_OPS(rp1dsi_pm_ops, rp1dsi_platform_suspend, + rp1dsi_platform_resume); + static const struct of_device_id rp1dsi_of_match[] = { { .compatible = "raspberrypi,rp1dsi", @@ -539,6 +557,7 @@ static struct platform_driver rp1dsi_platform_driver = { .name = DRIVER_NAME, .owner = THIS_MODULE, .of_match_table = rp1dsi_of_match, + .pm = pm_sleep_ptr(&rp1dsi_pm_ops), }, }; From 3f6d5888a7b029c8b0b7a771c5208f9fb05761ee Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Fri, 4 Sep 2026 14:48:40 +0100 Subject: [PATCH 17/44] dt-bindings: bluetooth: Add power-off-in-suspend property On a Raspberry Pi 5, the bluetooth is powered off in system suspend. Add a property for this behaviour. Signed-off-by: Peter Bailey --- .../devicetree/bindings/net/bluetooth/brcm,bluetooth.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Documentation/devicetree/bindings/net/bluetooth/brcm,bluetooth.yaml b/Documentation/devicetree/bindings/net/bluetooth/brcm,bluetooth.yaml index 3c410cadff2304..b8247da02bdf2e 100644 --- a/Documentation/devicetree/bindings/net/bluetooth/brcm,bluetooth.yaml +++ b/Documentation/devicetree/bindings/net/bluetooth/brcm,bluetooth.yaml @@ -109,6 +109,13 @@ properties: if the device's initial baud rate in normal mode is not supported by the host or if the device requires autobaud mode startup before loading FW. + brcm,power-off-in-suspend: + type: boolean + description: + The controller's supply is not maintained while the system is in a sleep + state; the controller loses its firmware and its entire configuration + across a suspend/resume cycle. + interrupts: items: - description: Handle to the line HOST_WAKE used to wake From bc9b5c621ef4fb68e8c6d79afec8ee4558102649 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Fri, 4 Sep 2026 14:50:50 +0100 Subject: [PATCH 18/44] Bluetooth: hci_bcm: Add support for powering off during suspend Implement the power-off-in-suspend property so that bluetooth is handled correctly after suspend. It is powered off so reprobe the device again on resume. Signed-off-by: Peter Bailey --- drivers/bluetooth/hci_bcm.c | 83 ++++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/drivers/bluetooth/hci_bcm.c b/drivers/bluetooth/hci_bcm.c index 0a54a7cf2bdef5..5862fe9a0e7877 100644 --- a/drivers/bluetooth/hci_bcm.c +++ b/drivers/bluetooth/hci_bcm.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -107,6 +108,8 @@ struct bcm_device_data { * @no_uart_clock_set: UART clock set command for >3Mbps mode is unavailable * @pcm_int_params: keep the initial PCM configuration * @use_autobaud_mode: start Bluetooth device in autobaud mode + * @power_off_in_suspend: controller loses power during a system suspend, so + * the device is reprobed on resume * @max_autobaud_speed: max baudrate supported by device in autobaud mode */ struct bcm_device { @@ -147,6 +150,7 @@ struct bcm_device { bool drive_rts_on_open; bool no_uart_clock_set; bool use_autobaud_mode; + bool power_off_in_suspend; u8 pcm_int_params[5]; u32 max_autobaud_speed; }; @@ -159,6 +163,38 @@ struct bcm_data { struct bcm_device *dev; }; +/* True when the controller will be powered off across a system suspend and + * rebuilt by a reprobe. Runtime PM is switched off for these devices, so the + * runtime callbacks cannot race the suspend path. + */ +static bool bcm_powers_off_in_suspend(const struct bcm_device *bdev) +{ + return IS_ENABLED(CONFIG_PM_SLEEP) && bdev && + bdev->power_off_in_suspend && bdev->hu && bdev->hu->serdev; +} + +#ifdef CONFIG_PM_SLEEP +struct bcm_reprobe { + struct device *dev; + struct work_struct work; +}; + +static void bcm_reprobe_worker(struct work_struct *work) +{ + struct bcm_reprobe *reprobe = + container_of(work, struct bcm_reprobe, work); + int ret; + + ret = device_reprobe(reprobe->dev); + if (ret && ret != -EPROBE_DEFER) + dev_err(reprobe->dev, "Reprobe error %d\n", ret); + + put_device(reprobe->dev); + kfree(reprobe); + module_put(THIS_MODULE); +} +#endif + /* List of BCM BT UART devices */ static DEFINE_MUTEX(bcm_device_lock); static LIST_HEAD(bcm_device_list); @@ -485,6 +521,12 @@ static int bcm_open(struct hci_uart *hu) out: if (bcm->dev) { + /* Since bcm_resume() reprobes the device, the suspend handling + * done by the hci_suspend_notifier is not necessary. + */ + if (bcm_powers_off_in_suspend(bcm->dev)) + set_bit(HCI_UART_NO_SUSPEND_NOTIFIER, &hu->flags); + if (bcm->dev->use_autobaud_mode) hci_uart_set_flow_control(hu, false); /* Assert BT_UART_CTS_N */ else if (bcm->dev->drive_rts_on_open) @@ -546,6 +588,7 @@ static int bcm_close(struct hci_uart *hu) if (bdev) { if (IS_ENABLED(CONFIG_PM) && bdev->irq_acquired) { + bdev->irq_acquired = false; devm_free_irq(bdev->dev, bdev->irq, bdev); device_init_wakeup(bdev->dev, false); pm_runtime_dont_use_autosuspend(bdev->dev); @@ -647,7 +690,7 @@ static int bcm_setup(struct hci_uart *hu) if (hci_test_quirk(hu->hdev, HCI_QUIRK_INVALID_BDADDR)) hci_set_quirk(hu->hdev, HCI_QUIRK_USE_BDADDR_PROPERTY); - if (!bcm_request_irq(bcm)) + if (!bcm_powers_off_in_suspend(bcm->dev) && !bcm_request_irq(bcm)) err = bcm_setup_sleep(hu); return err; @@ -826,6 +869,25 @@ static int bcm_suspend(struct device *dev) bt_dev_dbg(bdev, "suspend: is_suspended %d", bdev->is_suspended); + /* The controller loses power across the suspend and bcm_resume() + * reprobes it, so there is no state here worth preserving. Stop the + * UART and drive the controller off, as h5_btrtl_suspend() does. + */ + if (bcm_powers_off_in_suspend(bdev)) { + struct hci_dev *hdev = bdev->hu->hdev; + + /* Wait for power_on before suspending. bcm_setup causes + * a timeout if a suspend occurs duriung it + */ + if (hdev) + flush_work(&hdev->power_on); + + hci_uart_set_flow_control(bdev->hu, true); + bcm_gpio_set_power(bdev, false); + + return 0; + } + /* * When used with a device instantiated as platform_device, bcm_suspend * can be called at any time as long as the platform device is bound, @@ -860,6 +922,23 @@ static int bcm_resume(struct device *dev) bt_dev_dbg(bdev, "resume: is_suspended %d", bdev->is_suspended); + /* The device has lost all of its firmware and state so reprobe it. */ + if (bcm_powers_off_in_suspend(bdev)) { + struct bcm_reprobe *reprobe; + + reprobe = kzalloc(sizeof(*reprobe), GFP_KERNEL); + if (!reprobe) + return -ENOMEM; + + __module_get(THIS_MODULE); + + INIT_WORK(&reprobe->work, bcm_reprobe_worker); + reprobe->dev = get_device(bdev->dev); + queue_work(system_long_wq, &reprobe->work); + + return 0; + } + /* * When used with a device instantiated as platform_device, bcm_resume * can be called at any time as long as platform device is bound, @@ -1230,6 +1309,8 @@ static int bcm_of_probe(struct bcm_device *bdev) { bdev->use_autobaud_mode = device_property_read_bool(bdev->dev, "brcm,requires-autobaud-mode"); + bdev->power_off_in_suspend = device_property_read_bool(bdev->dev, + "brcm,power-off-in-suspend"); device_property_read_u32(bdev->dev, "max-speed", &bdev->oper_speed); device_property_read_u8_array(bdev->dev, "brcm,bt-pcm-int-params", bdev->pcm_int_params, 5); From afd5ebf2d44db419a2ba204b48e4f9b6e80ab362 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Tue, 25 Aug 2026 10:04:54 +0100 Subject: [PATCH 19/44] arm64: dts: broadcom: bcm2712: Power off bluetooth in suspend Add a device tree property so that bluetooth is handled correctly after suspend. It is powered off so the device is registered again on resume. Signed-off-by: Peter Bailey --- arch/arm64/boot/dts/broadcom/bcm2712-rpi-5-b.dts | 1 + arch/arm64/boot/dts/broadcom/bcm2712-rpi-cm5.dtsi | 1 + 2 files changed, 2 insertions(+) diff --git a/arch/arm64/boot/dts/broadcom/bcm2712-rpi-5-b.dts b/arch/arm64/boot/dts/broadcom/bcm2712-rpi-5-b.dts index d2fbad4d8478d5..86ffcd1d00da8e 100644 --- a/arch/arm64/boot/dts/broadcom/bcm2712-rpi-5-b.dts +++ b/arch/arm64/boot/dts/broadcom/bcm2712-rpi-5-b.dts @@ -375,6 +375,7 @@ dpi_16bit_gpio2: &rp1_dpi_16bit_gpio2 { }; compatible = "brcm,bcm43438-bt"; max-speed = <3000000>; shutdown-gpios = <&gio 29 GPIO_ACTIVE_HIGH>; + brcm,power-off-in-suspend; local-bd-address = [ 00 00 00 00 00 00 ]; }; }; diff --git a/arch/arm64/boot/dts/broadcom/bcm2712-rpi-cm5.dtsi b/arch/arm64/boot/dts/broadcom/bcm2712-rpi-cm5.dtsi index 76ecc3d2e35826..a3fba8181113b5 100644 --- a/arch/arm64/boot/dts/broadcom/bcm2712-rpi-cm5.dtsi +++ b/arch/arm64/boot/dts/broadcom/bcm2712-rpi-cm5.dtsi @@ -359,6 +359,7 @@ dpi_16bit_gpio2: &rp1_dpi_16bit_gpio2 { }; compatible = "brcm,bcm43438-bt"; max-speed = <3000000>; shutdown-gpios = <&gio 29 GPIO_ACTIVE_HIGH>; + brcm,power-off-in-suspend; local-bd-address = [ 00 00 00 00 00 00 ]; }; }; From 81941ce34f302d63a2932ffa30fbe789b5dcc2fd Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Tue, 25 Aug 2026 15:25:13 +0100 Subject: [PATCH 20/44] arm64: dts: bcm2712: Add a regulator for pcie 3v3 This adds a regulator for pcie1 using a pin on the RP1. It needs a hack to order pcie2 before pcie1 so that the regulator is found by the pcie-brcmstb driver. Signed-off-by: Peter Bailey --- arch/arm/boot/dts/overlays/README | 2 ++ .../overlays/pciex1-compat-pi5-overlay.dts | 12 +++++++ .../boot/dts/broadcom/bcm2712-rpi-5-b.dts | 29 ++++++++++++++++ .../boot/dts/broadcom/bcm2712-rpi-cm5io.dtsi | 33 +++++++++++++++++++ 4 files changed, 76 insertions(+) diff --git a/arch/arm/boot/dts/overlays/README b/arch/arm/boot/dts/overlays/README index 5f35ee995124b8..013f07bcac093b 100644 --- a/arch/arm/boot/dts/overlays/README +++ b/arch/arm/boot/dts/overlays/README @@ -4123,6 +4123,8 @@ Params: l1ss Enable RC ASPM L1 sub-state support. Requires requires DMA and MSI addresses to be 32bit. mmio-hi Move the start of outbound 32bit addresses to 2GB and expand 64bit outbound space to 14GB. + no-pwr-ctrl Disable the regulator on the PCIe 3v3 supply. + This leaves it always enabled. [ The pcf2127-rtc overlay has been deleted. See i2c-rtc. ] diff --git a/arch/arm/boot/dts/overlays/pciex1-compat-pi5-overlay.dts b/arch/arm/boot/dts/overlays/pciex1-compat-pi5-overlay.dts index 1f5adfc4add10e..6ec3241e354648 100644 --- a/arch/arm/boot/dts/overlays/pciex1-compat-pi5-overlay.dts +++ b/arch/arm/boot/dts/overlays/pciex1-compat-pi5-overlay.dts @@ -51,10 +51,22 @@ }; }; + /* + * Disable the regulator on the PCIe 3v3 supply. + * It is enabled by default by the firmware. + */ + fragment@4 { + target = <&pcie1_3v3>; + __dormant__ { + status = "disabled"; + }; + }; + __overrides__ { l1ss = <0>, "+0"; no-l0s = <0>, "+1"; no-mip = <0>, "+2"; mmio-hi = <0>, "+3"; + no-pwr-ctrl = <0>, "+4"; }; }; diff --git a/arch/arm64/boot/dts/broadcom/bcm2712-rpi-5-b.dts b/arch/arm64/boot/dts/broadcom/bcm2712-rpi-5-b.dts index 86ffcd1d00da8e..14527eef9bee74 100644 --- a/arch/arm64/boot/dts/broadcom/bcm2712-rpi-5-b.dts +++ b/arch/arm64/boot/dts/broadcom/bcm2712-rpi-5-b.dts @@ -101,6 +101,17 @@ gpio = <&rp1_gpio 46 0>; // CD1_IO0_MICCLK, to MIPI 1 connector }; + pcie1_3v3: pcie1_3v3 { + compatible = "regulator-fixed"; + regulator-name = "pcie1-3v3"; + regulator-min-microvolt = <3300000>; + regulator-max-microvolt = <3300000>; + regulator-boot-on; + enable-active-high; + gpio = <&rp1_gpio 28 GPIO_ACTIVE_HIGH>; // PCIE_RP1_WAKE + startup-delay-us = <20000>; + }; + cam_dummy_reg: cam_dummy_reg { compatible = "regulator-fixed"; regulator-name = "cam-dummy-reg"; @@ -125,6 +136,24 @@ rp1_target: &pcie2 { &pcie1 { brcm,fifo-qos-map = /bits/ 8 <3 3 3 3>; + + /* + * Hack to get linux to probe this pcie after pcie2. + * The pcie-brcmstb driver does not defer probe if + * the regulator is not found yet. This forces pcie1 + * to be probed after pcie2 so that the regulator + * is found. + */ + vpcie3v3-supply = <&pcie1_3v3>; + + pci@0,0 { + device_type = "pci"; + reg = <0 0 0 0 0>; + #address-cells = <3>; + #size-cells = <2>; + ranges; + vpcie3v3-supply = <&pcie1_3v3>; + }; }; // The system SPI for the bootloader EEPROM diff --git a/arch/arm64/boot/dts/broadcom/bcm2712-rpi-cm5io.dtsi b/arch/arm64/boot/dts/broadcom/bcm2712-rpi-cm5io.dtsi index 4c616a21dc76b7..6eb15ef06bfe08 100644 --- a/arch/arm64/boot/dts/broadcom/bcm2712-rpi-cm5io.dtsi +++ b/arch/arm64/boot/dts/broadcom/bcm2712-rpi-cm5io.dtsi @@ -1,5 +1,38 @@ // SPDX-License-Identifier: GPL-2.0 +/ { + pcie1_3v3: pcie1_3v3 { + compatible = "regulator-fixed"; + regulator-name = "pcie1-3v3"; + regulator-min-microvolt = <3300000>; + regulator-max-microvolt = <3300000>; + regulator-boot-on; + enable-active-high; + gpio = <&rp1_gpio 28 GPIO_ACTIVE_HIGH>; // PCIE_PWR_EN + startup-delay-us = <20000>; + }; +}; + +&pcie1 { + /* + * Hack to get linux to probe this pcie after pcie2. + * The pcie-brcmstb driver does not defer probe if + * the regulator is not found yet. This forces pcie1 + * to be probed after pcie2 so that the regulator + * is found. + */ + vpcie3v3-supply = <&pcie1_3v3>; + + pci@0,0 { + device_type = "pci"; + reg = <0 0 0 0 0>; + #address-cells = <3>; + #size-cells = <2>; + ranges; + vpcie3v3-supply = <&pcie1_3v3>; + }; +}; + i2c_csi_dsi1: &i2c0 { // Note: This is for CAM/DISP 1 connector symlink = "i2c-11"; }; From cea51612c596c5b35f6663a09c4be3ec2d00f2dd Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Tue, 25 Aug 2026 16:13:32 +0100 Subject: [PATCH 21/44] drivers: rtc-rpi: Clear a pending alarm when needed This clears the pending alarm when resuming from system suspend and when setting another alarm. Previously, If the rtc was used to wake from suspend more than one time in a row, It would fail. If the alarm is set and the system is suspended, resumed and suspended before the alarm occurs, the alarm will still wake the system. Signed-off-by: Peter Bailey --- drivers/rtc/rtc-rpi.c | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/drivers/rtc/rtc-rpi.c b/drivers/rtc/rtc-rpi.c index 006012333e7891..36bae15d019cab 100644 --- a/drivers/rtc/rtc-rpi.c +++ b/drivers/rtc/rtc-rpi.c @@ -87,6 +87,18 @@ static int rpi_rtc_alarm_clear_pending(struct device *dev) &data, sizeof(data)); } +static bool rpi_rtc_alarm_pending(struct device *dev) +{ + struct rpi_rtc_data *vrtc = dev_get_drvdata(dev); + u32 data[2] = {RTC_ALARM_PENDING}; + + if (rpi_firmware_property(vrtc->fw, RPI_FIRMWARE_GET_RTC_REG, + &data, sizeof(data))) + return true; + + return data[1] & 0x1; +} + static int rpi_rtc_read_alarm(struct device *dev, struct rtc_wkalrm *alarm) { struct rpi_rtc_data *vrtc = dev_get_drvdata(dev); @@ -108,6 +120,8 @@ static int rpi_rtc_set_alarm(struct device *dev, struct rtc_wkalrm *alarm) u32 data[2] = {RTC_ALARM, rtc_tm_to_time64(&alarm->time)}; int err; + rpi_rtc_alarm_clear_pending(dev); + err = rpi_firmware_property(vrtc->fw, RPI_FIRMWARE_SET_RTC_REG, &data, sizeof(data)); @@ -257,6 +271,21 @@ static int rpi_rtc_probe(struct platform_device *pdev) return devm_rtc_register_device(vrtc->rtc); } +static int rpi_rtc_resume(struct device *dev) +{ + struct rpi_rtc_data *vrtc = dev_get_drvdata(dev); + + if (!rpi_rtc_alarm_pending(dev)) + return 0; + + rpi_rtc_alarm_clear_pending(dev); + rtc_update_irq(vrtc->rtc, 1, RTC_AF); + + return 0; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(rpi_rtc_pm_ops, NULL, rpi_rtc_resume); + static const struct of_device_id rpi_rtc_dt_match[] = { { .compatible = "raspberrypi,rpi-rtc"}, {}, @@ -268,6 +297,7 @@ static struct platform_driver rpi_rtc_driver = { .driver = { .name = "rpi-rtc", .of_match_table = rpi_rtc_dt_match, + .pm = pm_sleep_ptr(&rpi_rtc_pm_ops), }, }; From fde2bafe3b01d3d9413b43cf64e2c5b5bb78d563 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Fri, 28 Aug 2026 11:42:10 +0100 Subject: [PATCH 22/44] media: imx500: Stop streaming during system suspend. This prevents the camera from sometimes freezing when using rpicam-hello -t 0 Signed-off-by: Peter Bailey --- drivers/media/i2c/imx500.c | 50 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/drivers/media/i2c/imx500.c b/drivers/media/i2c/imx500.c index 5fc593340e9588..1e44c54b477783 100644 --- a/drivers/media/i2c/imx500.c +++ b/drivers/media/i2c/imx500.c @@ -3250,6 +3250,50 @@ static int imx500_set_stream(struct v4l2_subdev *sd, int enable) return ret; } +static int __maybe_unused imx500_suspend(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct v4l2_subdev *sd = i2c_get_clientdata(client); + struct imx500 *imx500 = to_imx500(sd); + + mutex_lock(&imx500->mutex); + + if (imx500->streaming) + imx500_stop_streaming(imx500); + + mutex_unlock(&imx500->mutex); + + return 0; +} + +static int __maybe_unused imx500_resume(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct v4l2_subdev *sd = i2c_get_clientdata(client); + struct imx500 *imx500 = to_imx500(sd); + int ret; + + mutex_lock(&imx500->mutex); + + if (imx500->streaming) { + ret = imx500_start_streaming(imx500); + if (ret) + goto error; + } + + mutex_unlock(&imx500->mutex); + + return 0; + +error: + imx500_stop_streaming(imx500); + imx500->streaming = false; + + mutex_unlock(&imx500->mutex); + + return ret; +} + static int imx500_get_regulators(struct imx500 *imx500) { struct i2c_client *client = v4l2_get_subdevdata(&imx500->sd); @@ -3734,8 +3778,10 @@ static const struct of_device_id imx500_dt_ids[] = { MODULE_DEVICE_TABLE(of, imx500_dt_ids); -static const struct dev_pm_ops imx500_pm_ops = { SET_RUNTIME_PM_OPS( - imx500_power_off, imx500_power_on, NULL) }; +static const struct dev_pm_ops imx500_pm_ops = { + SET_SYSTEM_SLEEP_PM_OPS(imx500_suspend, imx500_resume) + SET_RUNTIME_PM_OPS(imx500_power_off, imx500_power_on, NULL) +}; static struct i2c_driver imx500_i2c_driver = { .driver = { From 40e11564b7437406b9a92be21efeed437d71eaa1 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Mon, 7 Sep 2026 17:33:47 +0100 Subject: [PATCH 23/44] drm/rp1: vec: Add suspend/resume support Use the modeset helper functions and poweroff the DAC in suspend. Signed-off-by: Peter Bailey --- drivers/gpu/drm/rp1/rp1-vec/rp1_vec.c | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/drivers/gpu/drm/rp1/rp1-vec/rp1_vec.c b/drivers/gpu/drm/rp1/rp1-vec/rp1_vec.c index d2e5cd57e936a2..b8c5eefe202016 100644 --- a/drivers/gpu/drm/rp1/rp1-vec/rp1_vec.c +++ b/drivers/gpu/drm/rp1/rp1-vec/rp1_vec.c @@ -579,6 +579,38 @@ static void rp1vec_platform_shutdown(struct platform_device *pdev) rp1vec_stopall(drm); } +static int rp1vec_platform_suspend(struct device *dev) +{ + struct drm_device *drm = dev_get_drvdata(dev); + struct rp1_vec *vec = drm->dev_private; + int ret; + + ret = drm_mode_config_helper_suspend(drm); + if (ret) + return ret; + + rp1vec_vidout_poweroff(vec); + clk_disable_unprepare(vec->vec_clock); + + return 0; +} + +static int rp1vec_platform_resume(struct device *dev) +{ + struct drm_device *drm = dev_get_drvdata(dev); + struct rp1_vec *vec = drm->dev_private; + int ret; + + clk_prepare_enable(vec->vec_clock); + + ret = drm_mode_config_helper_resume(drm); + + return ret; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(rp1vec_pm_ops, rp1vec_platform_suspend, + rp1vec_platform_resume); + static const struct of_device_id rp1vec_of_match[] = { { .compatible = "raspberrypi,rp1vec", @@ -596,6 +628,7 @@ static struct platform_driver rp1vec_platform_driver = { .name = DRIVER_NAME, .owner = THIS_MODULE, .of_match_table = rp1vec_of_match, + .pm = pm_sleep_ptr(&rp1vec_pm_ops), }, }; From ac567f92455f11cca0a0c60901845405b8561c30 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Tue, 8 Sep 2026 09:36:12 +0100 Subject: [PATCH 24/44] dt-bindings: usb: snps,dwc3: Add snps,power-off-in-s2ram Add a device tree property to indicate that the xHCI controller will be reset over suspend. Signed-off-by: Peter Bailey --- .../devicetree/bindings/usb/snps,dwc3-common.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Documentation/devicetree/bindings/usb/snps,dwc3-common.yaml b/Documentation/devicetree/bindings/usb/snps,dwc3-common.yaml index f44f16abb5389e..1b587c2643b678 100644 --- a/Documentation/devicetree/bindings/usb/snps,dwc3-common.yaml +++ b/Documentation/devicetree/bindings/usb/snps,dwc3-common.yaml @@ -401,6 +401,13 @@ properties: items: enum: [1, 4, 8, 16, 32, 64, 128, 256] + snps,power-off-in-s2ram: + description: + Set if the controller is powered off while the system is suspended, so + that all of its state is lost. When set, the host side is fully + reinitialised on resume instead of having its saved state restored. + type: boolean + num-hc-interrupters: maximum: 8 default: 1 From 26190bc7f7429fb44dd1d7cbf4fc9eb31485539c Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Tue, 8 Sep 2026 09:38:20 +0100 Subject: [PATCH 25/44] usb: dwc3: Add a DT property for losing power in suspend Use the snps,power-off-in-s2ram property to set power_lost in xhci-plat Signed-off-by: Peter Bailey --- drivers/usb/dwc3/core.c | 3 +++ drivers/usb/dwc3/core.h | 3 +++ drivers/usb/dwc3/host.c | 6 ++++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/drivers/usb/dwc3/core.c b/drivers/usb/dwc3/core.c index fcbbb04fdeed1d..c67f23620230e6 100644 --- a/drivers/usb/dwc3/core.c +++ b/drivers/usb/dwc3/core.c @@ -1925,6 +1925,9 @@ static void dwc3_get_properties(struct dwc3 *dwc) dwc->dis_split_quirk = device_property_read_bool(dev, "snps,dis-split-quirk"); + dwc->power_off_in_suspend = device_property_read_bool(dev, + "snps,power-off-in-s2ram"); + device_property_read_u8(dev, "snps,axi-pipe-limit", &axi_pipe_limit); diff --git a/drivers/usb/dwc3/core.h b/drivers/usb/dwc3/core.h index 1b30362bde02ad..45cd01552237e7 100644 --- a/drivers/usb/dwc3/core.h +++ b/drivers/usb/dwc3/core.h @@ -1187,6 +1187,8 @@ struct dwc3_glue_ops { * @dis_split_quirk: set to disable split boundary. * @enable_usb2_transceiver_delay: Set to insert a delay before the * assertion of the TxValid signal during a HS Chirp. + * @power_off_in_suspend: set if the controller is powered off during system + * suspend, so all of its state is lost. * @sys_wakeup: set if the device may do system wakeup. * @wakeup_configured: set if the device is configured for remote wakeup. * @suspended: set to track suspend event due to U3/L2. @@ -1441,6 +1443,7 @@ struct dwc3 { unsigned dis_split_quirk:1; unsigned enable_usb2_transceiver_delay:1; + unsigned power_off_in_suspend:1; unsigned async_callbacks:1; unsigned sys_wakeup:1; unsigned wakeup_configured:1; diff --git a/drivers/usb/dwc3/host.c b/drivers/usb/dwc3/host.c index cd59aba1fac0e0..52e0a18528181c 100644 --- a/drivers/usb/dwc3/host.c +++ b/drivers/usb/dwc3/host.c @@ -130,6 +130,7 @@ static int dwc3_host_get_irq(struct dwc3 *dwc) int dwc3_host_init(struct dwc3 *dwc) { struct platform_device *pdev = to_platform_device(dwc->dev); + struct xhci_plat_priv xhci_priv = dwc3_xhci_plat_quirk; struct property_entry props[6]; struct platform_device *xhci; int ret, irq; @@ -203,8 +204,9 @@ int dwc3_host_init(struct dwc3 *dwc) } } - ret = platform_device_add_data(xhci, &dwc3_xhci_plat_quirk, - sizeof(struct xhci_plat_priv)); + xhci_priv.power_lost = dwc->power_off_in_suspend; + + ret = platform_device_add_data(xhci, &xhci_priv, sizeof(xhci_priv)); if (ret) goto err; From 365ddcd3168e04fbed8d2d346a942f40c24a5580 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Tue, 8 Sep 2026 09:52:14 +0100 Subject: [PATCH 26/44] arm64: dts: rp1: Mark the USB controllers as powered off in suspend The RP1 will reset the USB controllers over suspend. Add snps,power-off-in-suspend to its device tree. Signed-off-by: Peter Bailey --- arch/arm64/boot/dts/broadcom/rp1.dtsi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arch/arm64/boot/dts/broadcom/rp1.dtsi b/arch/arm64/boot/dts/broadcom/rp1.dtsi index 7141dfcff4e708..d07573cb7dc07a 100644 --- a/arch/arm64/boot/dts/broadcom/rp1.dtsi +++ b/arch/arm64/boot/dts/broadcom/rp1.dtsi @@ -1127,6 +1127,7 @@ snps,enhanced-nak-fs-quirk; snps,parkmode-disable-ss-quirk; snps,parkmode-disable-fsls-quirk; + snps,power-off-in-s2ram; snps,tx-max-burst = /bits/ 8 <8>; snps,tx-thr-num-pkt = /bits/ 8 <2>; interrupts = ; @@ -1143,6 +1144,7 @@ snps,enhanced-nak-fs-quirk; snps,parkmode-disable-ss-quirk; snps,parkmode-disable-fsls-quirk; + snps,power-off-in-s2ram; snps,tx-max-burst = /bits/ 8 <8>; snps,tx-thr-num-pkt = /bits/ 8 <2>; interrupts = ; From 6d6f663ff37d63904770f7052ed73870445e846d Mon Sep 17 00:00:00 2001 From: Jonathan Bell Date: Thu, 17 Sep 2026 13:06:44 +0100 Subject: [PATCH 27/44] net: phy: mxl-86110: add PLL sleep powerdown control Several pins on the PHY are power-on reset straps. TXD/RXD pins are pulled down by default, and this (RXD2) sets PLLOFF=0 which keeps the PLL alive when PD is set in the base control register. Override this wonky default with a write to the UTP_EXT_SLEEP_CTRL register. Signed-off-by: Jonathan Bell --- drivers/net/phy/mxl-86110.c | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/drivers/net/phy/mxl-86110.c b/drivers/net/phy/mxl-86110.c index 20ea4ed636f051..dae5ba7e1dd3c6 100644 --- a/drivers/net/phy/mxl-86110.c +++ b/drivers/net/phy/mxl-86110.c @@ -70,6 +70,8 @@ /* EXT Sleep Control register */ #define MXL86110_UTP_EXT_SLEEP_CTRL_REG 0x27 +#define MXL86110_UTP_EXT_SLEEP_CTRL_PLL_IN_SLEEP_OFF 0 +#define MXL86110_UTP_EXT_SLEEP_CTRL_PLL_IN_SLEEP_MASK BIT(14) #define MXL86110_UTP_EXT_SLEEP_CTRL_EN_SLEEP_SW_OFF 0 #define MXL86110_UTP_EXT_SLEEP_CTRL_EN_SLEEP_SW_MASK BIT(15) @@ -768,6 +770,35 @@ static int mxl86110_config_rgmii_delay(struct phy_device *phydev) return ret; } +/** + * mxl86110_config_sleep_pll() - configure PLL power-down behaviour in sleep + * @phydev: pointer to the phy_device + * @pd: true = turn off PLL in sleep + * + * The initial value is set by the power-on state of the RXD2 pin, + * which is pulled down. + * 0 = PLL is kept on during suspend or sleep + * 1 = powerdown PLL in suspend or sleep. + * + * But the sense of the corresponding register bit is inverted - + * 0 = PLL is powered down in sleep + * 1 = PLL is kept on in sleep. + * + * Return: 0 or negative errno code. + */ +static int mxl86110_config_sleep_pll(struct phy_device *phydev, bool pd) +{ + u16 val = 0; + + if (!pd) + val = MXL86110_UTP_EXT_SLEEP_CTRL_PLL_IN_SLEEP_MASK; + + return __mxl86110_modify_extended_reg(phydev, + MXL86110_UTP_EXT_SLEEP_CTRL_REG, + MXL86110_UTP_EXT_SLEEP_CTRL_PLL_IN_SLEEP_MASK, + val); +} + /** * mxl86110_config_init() - initialize the MXL86110 PHY * @phydev: pointer to the phy_device @@ -913,6 +944,10 @@ static int mxl86111_config_init(struct phy_device *phydev) goto out; ret = mxl86110_broadcast_cfg(phydev); + if (ret < 0) + goto out; + + ret = mxl86110_config_sleep_pll(phydev, true); out: phy_unlock_mdio_bus(phydev); From 72bba59b488984a97f28eef686cf420aa815fa8b Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Thu, 17 Sep 2026 17:14:24 +0100 Subject: [PATCH 28/44] net: macb: Fix clock leak in gem_get_tsu_rate Release the tsu_clk in gem_get_tsu_rate after getting the rate. Signed-off-by: Peter Bailey --- drivers/net/ethernet/cadence/macb_main.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/drivers/net/ethernet/cadence/macb_main.c b/drivers/net/ethernet/cadence/macb_main.c index 4a5c3bd74a0a04..7a125291a2ec80 100644 --- a/drivers/net/ethernet/cadence/macb_main.c +++ b/drivers/net/ethernet/cadence/macb_main.c @@ -3929,8 +3929,10 @@ static unsigned int gem_get_tsu_rate(struct macb *bp) unsigned int tsu_rate; tsu_clk = devm_clk_get(&bp->pdev->dev, "tsu_clk"); - if (!IS_ERR(tsu_clk)) + if (!IS_ERR(tsu_clk)) { tsu_rate = clk_get_rate(tsu_clk); + devm_clk_put(&bp->pdev->dev, tsu_clk); + } /* try pclk instead */ else if (!IS_ERR(bp->pclk)) { tsu_clk = bp->pclk; From ba0d6628a7bd9c44f148679bbf429f0966dc41b4 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Fri, 11 Sep 2026 14:44:01 +0100 Subject: [PATCH 29/44] net: macb: don't disable an IRQ wake that was never enabled macb_suspend() ignores the return value of enable_irq_wake(), while macb_resume() calls disable_irq_wake() unconditionally. On a platform whose irqchip implements neither .irq_set_wake nor IRQCHIP_SKIP_SET_WAKE, set_irq_wake_real() returns -ENXIO and irq_set_irq_wake() resets wake_depth back to zero. Nothing else in the kernel touches wake_depth, so the unpaired disable on resume trips: Unbalanced IRQ 108 wake disable WARNING: CPU: 0 PID: 1013 at irq_set_irq_wake+0x168/0x238 ... irq_set_irq_wake+0x168/0x238 (P) macb_resume+0x23c/0x4b8 platform_pm_resume+0x30/0x70 Record whether the interrupt was actually armed as a wake source, and only disarm it if it was. Warn when arming fails rather than swallowing the error, since the MAC is then being suspended with WoL configured but no way to wake the system. Signed-off-by: Dom Cobley --- drivers/net/ethernet/cadence/macb.h | 1 + drivers/net/ethernet/cadence/macb_main.c | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/drivers/net/ethernet/cadence/macb.h b/drivers/net/ethernet/cadence/macb.h index d046a922a1a4ef..7c1eb0b8c9155b 100644 --- a/drivers/net/ethernet/cadence/macb.h +++ b/drivers/net/ethernet/cadence/macb.h @@ -1245,6 +1245,7 @@ struct macb_ptp_info { struct macb_pm_data { u32 scrt2; u32 usrio; + bool irq_wake_enabled; }; struct macb_usrio_config { diff --git a/drivers/net/ethernet/cadence/macb_main.c b/drivers/net/ethernet/cadence/macb_main.c index 7a125291a2ec80..92dfbfa2dcdd3c 100644 --- a/drivers/net/ethernet/cadence/macb_main.c +++ b/drivers/net/ethernet/cadence/macb_main.c @@ -6209,7 +6209,11 @@ static int __maybe_unused macb_suspend(struct device *dev) spin_unlock_irqrestore(&bp->lock, flags); } - enable_irq_wake(bp->queues[0].irq); + err = enable_irq_wake(bp->queues[0].irq); + if (err) + netdev_warn(netdev, "Unable to enable IRQ %d as a wake source (error %d)\n", + bp->queues[0].irq, err); + bp->pm_data.irq_wake_enabled = !err; } netif_device_detach(netdev); @@ -6287,7 +6291,10 @@ static int __maybe_unused macb_resume(struct device *dev) return err; } - disable_irq_wake(bp->queues[0].irq); + if (bp->pm_data.irq_wake_enabled) { + disable_irq_wake(bp->queues[0].irq); + bp->pm_data.irq_wake_enabled = false; + } /* Now make sure we disable phy before moving * to common restore path From ef384cbe7789d01ac82da0ef1669c2bd9fa35a9b Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Fri, 11 Sep 2026 14:44:26 +0100 Subject: [PATCH 30/44] net: macb: add MACB_CAPS_NO_WOL and set it on RP1 On Raspberry Pi 5 the MAC interrupt is routed through the RP1 irqchip, which implements neither .irq_set_wake nor IRQCHIP_SKIP_SET_WAKE, so it cannot be armed as a system wakeup source. Wake-on-LAN cannot work; the board wakes from the power button on the BCM2712 GPIO instead. The driver advertises WoL unconditionally, and userspace acts on that: LibreELEC enables it at runtime whenever CONFIG_SUSPEND is set. That makes device_may_wakeup() true, which diverts macb_suspend() away from the path that works - phylink_stop() and macb_reset_hw() are skipped, the receiver is left enabled with the WOL register armed, and neither pm_runtime_force_suspend() nor macb_clks_disable() runs - all for a wakeup that will never fire. Images that leave WoL alone resume fine. Add MACB_CAPS_NO_WOL for MACs that cannot wake the system and set it on RP1. The device is then not marked wakeup-capable, WoL is not offered to ethtool, and a request to enable it is rejected with -EOPNOTSUPP before phylink is asked, so the PHY is not left armed either. Disabling WoL still succeeds, and suspend keeps taking the path known to resume. device_set_wakeup_capable() moves below macb_configure_caps(), as bp->caps is not populated until then. Signed-off-by: Dom Cobley --- drivers/net/ethernet/cadence/macb.h | 5 +++++ drivers/net/ethernet/cadence/macb_main.c | 11 +++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/drivers/net/ethernet/cadence/macb.h b/drivers/net/ethernet/cadence/macb.h index 7c1eb0b8c9155b..f6d772b855c5ad 100644 --- a/drivers/net/ethernet/cadence/macb.h +++ b/drivers/net/ethernet/cadence/macb.h @@ -797,6 +797,11 @@ * read-back to guarantee delivery. Currently set only on RP1. */ #define MACB_CAPS_PCIE_POSTED_WRITES 0x00004000 +/* The MAC cannot act as a system wakeup source, because the interrupt it + * would raise cannot be armed as a wakeup by the parent irqchip. Refuse + * WoL rather than arming hardware that will never wake anything. + */ +#define MACB_CAPS_NO_WOL 0x00008000 #define MACB_CAPS_PCS 0x01000000 #define MACB_CAPS_HIGH_SPEED 0x02000000 #define MACB_CAPS_CLK_HW_CHG 0x04000000 diff --git a/drivers/net/ethernet/cadence/macb_main.c b/drivers/net/ethernet/cadence/macb_main.c index 92dfbfa2dcdd3c..9e5eac7f5479ec 100644 --- a/drivers/net/ethernet/cadence/macb_main.c +++ b/drivers/net/ethernet/cadence/macb_main.c @@ -3826,7 +3826,8 @@ static void macb_get_wol(struct net_device *netdev, struct ethtool_wolinfo *wol) struct macb *bp = netdev_priv(netdev); phylink_ethtool_get_wol(bp->phylink, wol); - wol->supported |= (WAKE_MAGIC | WAKE_ARP); + if (device_can_wakeup(&bp->pdev->dev)) + wol->supported |= (WAKE_MAGIC | WAKE_ARP); /* Add macb wolopts to phy wolopts */ wol->wolopts |= bp->wolopts; @@ -3837,6 +3838,9 @@ static int macb_set_wol(struct net_device *netdev, struct ethtool_wolinfo *wol) struct macb *bp = netdev_priv(netdev); int ret; + if (wol->wolopts && !device_can_wakeup(&bp->pdev->dev)) + return -EOPNOTSUPP; + /* Pass the order to phylink layer */ ret = phylink_ethtool_set_wol(bp->phylink, wol); /* Don't manage WoL on MAC, if PHY set_wol() fails */ @@ -5776,6 +5780,7 @@ static const struct macb_config raspberrypi_rp1_config = { MACB_CAPS_JUMBO | MACB_CAPS_GEM_HAS_PTP | MACB_CAPS_PCIE_POSTED_WRITES | + MACB_CAPS_NO_WOL | MACB_CAPS_EEE, .dma_burst_length = 16, .clk_init = macb_clk_init, @@ -5913,7 +5918,6 @@ static int macb_probe(struct platform_device *pdev) bp->max_tx_length = GEM_MAX_TX_LEN; bp->wol = 0; - device_set_wakeup_capable(&pdev->dev, 1); bp->usrio = macb_config->usrio; @@ -5948,6 +5952,9 @@ static int macb_probe(struct platform_device *pdev) /* setup capabilities */ macb_configure_caps(bp, macb_config); + if (!(bp->caps & MACB_CAPS_NO_WOL)) + device_set_wakeup_capable(&pdev->dev, 1); + #ifdef CONFIG_ARCH_DMA_ADDR_T_64BIT if (GEM_BFEXT(DAW64, gem_readl(bp, DCFG6))) { err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(44)); From b69da875c50132513e32cfb9109ec71cf45a2ad5 Mon Sep 17 00:00:00 2001 From: Jonathan Bell Date: Thu, 24 Sep 2026 10:27:23 +0100 Subject: [PATCH 31/44] mmc: sdhci-brcmstb: limit minimum card clock on BCM2712 There is a clock domain crossing bugette when the AXI bus clock is fast and the card clock is below 400kHz. The symptom is that a mmc_rescan can take up to 30 seconds, as the register writes are lost. The err_stats counter for "request timeout" increments instead of "command timeout". Signed-off-by: Jonathan Bell --- drivers/mmc/host/sdhci-brcmstb.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/drivers/mmc/host/sdhci-brcmstb.c b/drivers/mmc/host/sdhci-brcmstb.c index 14947a09d1171f..d45eab8fa1eaac 100644 --- a/drivers/mmc/host/sdhci-brcmstb.c +++ b/drivers/mmc/host/sdhci-brcmstb.c @@ -394,6 +394,15 @@ static int bcm2712_init_sd_express(struct sdhci_host *host, struct mmc_ios *ios) return ret; } +static unsigned int bcm2712_get_min_clock(struct sdhci_host *host) +{ + /* + * At card clock frequencies lower than 400kHz, register writes may be + * dropped by the host bus interface. + */ + return 400000; +} + static void sdhci_brcmstb_dumpregs(struct mmc_host *mmc) { sdhci_dumpregs(mmc_priv(mmc)); @@ -450,6 +459,7 @@ static struct sdhci_ops sdhci_brcmstb_ops_2712 = { .reset = brcmstb_reset, .set_uhs_signaling = sdhci_set_uhs_signaling, .init_sd_express = bcm2712_init_sd_express, + .get_min_clock = bcm2712_get_min_clock, }; static struct sdhci_ops sdhci_brcmstb_ops_7216 = { From 7078fcba9b9aae142f49da7cfdd18730bf2aaa89 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Wed, 2 Sep 2026 09:51:15 +0100 Subject: [PATCH 32/44] arm64: configs: bcm2712: Enable DPM watchdog Enable the watchdog for device power management. Use a short timeout before warning. Signed-off-by: Peter Bailey --- arch/arm64/configs/bcm2712_defconfig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/arch/arm64/configs/bcm2712_defconfig b/arch/arm64/configs/bcm2712_defconfig index fd3176805eac63..1f16f67c1902fe 100644 --- a/arch/arm64/configs/bcm2712_defconfig +++ b/arch/arm64/configs/bcm2712_defconfig @@ -62,6 +62,9 @@ CONFIG_CMDLINE="console=ttyAMA0,115200 kgdboc=ttyAMA0,115200 root=/dev/mmcblk0p2 # CONFIG_SUSPEND is not set CONFIG_PM=y CONFIG_PM_DEBUG=y +CONFIG_DPM_WATCHDOG=y +CONFIG_DPM_WATCHDOG_TIMEOUT=30 +CONFIG_DPM_WATCHDOG_WARNING_TIMEOUT=5 CONFIG_CPU_IDLE=y CONFIG_CPU_FREQ=y CONFIG_CPU_FREQ_STAT=y From 7f39fb1ff3fa9c4bee3b9f644938035bcfbdb826 Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Tue, 21 Jul 2026 15:55:04 +0100 Subject: [PATCH 33/44] arm64: configs: Enable CONFIG_SUSPEND Add support for suspend to RAM on bcm2712. Signed-off-by: Peter Bailey --- arch/arm64/configs/bcm2712_defconfig | 3 --- 1 file changed, 3 deletions(-) diff --git a/arch/arm64/configs/bcm2712_defconfig b/arch/arm64/configs/bcm2712_defconfig index 1f16f67c1902fe..08555614224373 100644 --- a/arch/arm64/configs/bcm2712_defconfig +++ b/arch/arm64/configs/bcm2712_defconfig @@ -50,7 +50,6 @@ CONFIG_ARM64_ERRATUM_2441009=y CONFIG_ARM64_16K_PAGES=y CONFIG_ARM64_VA_BITS_47=y CONFIG_NR_CPUS=4 -CONFIG_HOTPLUG_CPU=y CONFIG_NUMA=y CONFIG_COMPAT=y CONFIG_ARMV8_DEPRECATED=y @@ -59,8 +58,6 @@ CONFIG_CP15_BARRIER_EMULATION=y CONFIG_SETEND_EMULATION=y CONFIG_RANDOMIZE_BASE=y CONFIG_CMDLINE="console=ttyAMA0,115200 kgdboc=ttyAMA0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 rootwait" -# CONFIG_SUSPEND is not set -CONFIG_PM=y CONFIG_PM_DEBUG=y CONFIG_DPM_WATCHDOG=y CONFIG_DPM_WATCHDOG_TIMEOUT=30 From 8527dfe31336f4bed1fec6cdea138f6bd9d787cb Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Wed, 30 Sep 2026 18:48:32 +0100 Subject: [PATCH 34/44] PM: suspend: Hide s2idle without platform suspend support The arm64 kernel8.img is shared by Pi 3, Pi 4 and Pi 5, and CONFIG_SUSPEND is now enabled for Pi 5. Pi 3 and Pi 4 boot with spin-table, so no platform suspend_ops are registered and "mem" falls back to suspend-to-idle. On Pi 4 suspend-to-idle then never returns: the console UART is not a wakeup source, USB sits behind PCIe which is held in reset across suspend, and GENET has no WoL interrupt described. The board appears dead until power cycled. On Pi 3 it only "works" because dwc_otg has no bus_suspend, so suspend is aborted with -ENOENT. When no suspend_ops are registered (no PSCI SYSTEM_SUSPEND), drop "mem" and "freeze" from /sys/power/state and "s2idle" from /sys/power/mem_sleep, so systemctl suspend fails cleanly instead. Pi 5 registers PSCI SYSTEM_SUSPEND before late_initcall and keeps working. Signed-off-by: Dom Cobley --- kernel/power/suspend.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/kernel/power/suspend.c b/kernel/power/suspend.c index 3d4ebedad69f6a..e534b8ee5cfdae 100644 --- a/kernel/power/suspend.c +++ b/kernel/power/suspend.c @@ -197,6 +197,22 @@ void __init pm_states_init(void) mem_sleep_states[PM_SUSPEND_TO_IDLE] = mem_sleep_labels[PM_SUSPEND_TO_IDLE]; } +/* + * Without platform suspend (PSCI SYSTEM_SUSPEND) there are no wake sources + * enabled by default, so suspend-to-idle never returns. + */ +static int __init rpi_disable_s2idle(void) +{ + if (suspend_ops) + return 0; + + pm_states[PM_SUSPEND_MEM] = NULL; + pm_states[PM_SUSPEND_TO_IDLE] = NULL; + mem_sleep_states[PM_SUSPEND_TO_IDLE] = NULL; + return 0; +} +late_initcall(rpi_disable_s2idle); + static int __init mem_sleep_default_setup(char *str) { suspend_state_t state; From afeff16beb8a727dd40a427a4a7f605b1697d59a Mon Sep 17 00:00:00 2001 From: Peter Bailey Date: Thu, 1 Oct 2026 09:05:55 +0100 Subject: [PATCH 35/44] PM: suspend: Hide mem sleep when not supported The bootloader had a device tree property to indicate suspend support. If it is not 1 then suspend is not supported so hide deep option in /sys/power/state and /sys/power/mem_sleep as well as s2idle Signed-off-by: Peter Bailey --- kernel/power/suspend.c | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/kernel/power/suspend.c b/kernel/power/suspend.c index e534b8ee5cfdae..39baa6c18da817 100644 --- a/kernel/power/suspend.c +++ b/kernel/power/suspend.c @@ -203,9 +203,31 @@ void __init pm_states_init(void) */ static int __init rpi_disable_s2idle(void) { - if (suspend_ops) - return 0; + struct device_node *np; + u32 val; + int ret; + + if (!suspend_ops) + goto disable_s2idle; + + np = of_find_node_by_path("/chosen/bootloader"); + if (!np) + goto disable_suspend; + + ret = of_property_read_u32(np, "suspend-to-ram", &val); + of_node_put(np); + if (ret) + goto disable_suspend; + + if (val != 1) + goto disable_suspend; + + return 0; + +disable_suspend: + mem_sleep_states[PM_SUSPEND_MEM] = NULL; +disable_s2idle: pm_states[PM_SUSPEND_MEM] = NULL; pm_states[PM_SUSPEND_TO_IDLE] = NULL; mem_sleep_states[PM_SUSPEND_TO_IDLE] = NULL; From 59a2e2867d22739a035a9fed24e07a6abb05c09c Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Fri, 6 Nov 2026 14:55:09 +0000 Subject: [PATCH 36/44] fixup! media: pisp_be: Add suspend/resume support --- .../platform/raspberrypi/pisp_be/pisp_be.c | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/drivers/media/platform/raspberrypi/pisp_be/pisp_be.c b/drivers/media/platform/raspberrypi/pisp_be/pisp_be.c index f155a54b516c98..314a62e50e0513 100644 --- a/drivers/media/platform/raspberrypi/pisp_be/pisp_be.c +++ b/drivers/media/platform/raspberrypi/pisp_be/pisp_be.c @@ -5,7 +5,6 @@ * */ #include -#include #include #include #include @@ -15,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -242,6 +242,8 @@ struct pispbe_dev { /* protects "hw_busy" flag, streaming_map and job_queue */ spinlock_t hw_lock; bool hw_busy; /* non-zero if a job is queued or is being started */ + bool suspending; /* protected by hw_lock */ + wait_queue_head_t idle_wq; struct list_head job_queue; int irq; u32 hw_version; @@ -603,7 +605,7 @@ static void pispbe_schedule(struct pispbe_dev *pispbe, bool clear_hw_busy) if (clear_hw_busy) pispbe->hw_busy = false; - if (pispbe->hw_busy) + if (pispbe->hw_busy || pispbe->suspending) return; job = list_first_entry_or_null(&pispbe->job_queue, @@ -701,6 +703,9 @@ static irqreturn_t pispbe_isr(int irq, void *dev) /* check if there's more to do before going to sleep */ pispbe_schedule(pispbe, can_queue_another); + if (READ_ONCE(pispbe->suspending)) + wake_up(&pispbe->idle_wq); + return IRQ_HANDLED; } @@ -1784,6 +1789,7 @@ static int pispbe_probe(struct platform_device *pdev) pispbe->hw_busy = false; spin_lock_init(&pispbe->hw_lock); + init_waitqueue_head(&pispbe->idle_wq); ret = pispbe_hw_init(pispbe); if (ret) goto pm_runtime_suspend_err; @@ -1827,25 +1833,30 @@ static void pispbe_remove(struct platform_device *pdev) pm_runtime_disable(pispbe->dev); } -static int pispbe_suspend(struct device *dev) +/* + * hw_busy only covers a job that has not started yet; running_job is the one + * the hardware is working on. + */ +static bool pispbe_idle(struct pispbe_dev *pispbe) { - struct pispbe_dev *pispbe = dev_get_drvdata(dev); - bool busy; + guard(spinlock_irqsave)(&pispbe->hw_lock); - scoped_guard(spinlock_irqsave, &pispbe->hw_lock) { - busy = pispbe->hw_busy; - } + return !pispbe->hw_busy && !pispbe->running_job.node_group; +} - if (busy) { - msleep(100); +static int pispbe_suspend(struct device *dev) +{ + struct pispbe_dev *pispbe = dev_get_drvdata(dev); - scoped_guard(spinlock_irqsave, &pispbe->hw_lock) { - busy = pispbe->hw_busy; - } - } + scoped_guard(spinlock_irqsave, &pispbe->hw_lock) + WRITE_ONCE(pispbe->suspending, true); - if (busy) { + if (!wait_event_timeout(pispbe->idle_wq, pispbe_idle(pispbe), + msecs_to_jiffies(500))) { dev_err(dev, "Timed out waiting for ISP-BE to finish.\n"); + scoped_guard(spinlock_irqsave, &pispbe->hw_lock) + WRITE_ONCE(pispbe->suspending, false); + pispbe_schedule(pispbe, false); return -EBUSY; } @@ -1870,6 +1881,11 @@ static int pispbe_resume(struct device *dev) pm_runtime_mark_last_busy(dev); pm_runtime_put_autosuspend(dev); + scoped_guard(spinlock_irqsave, &pispbe->hw_lock) + WRITE_ONCE(pispbe->suspending, false); + if (!ret) + pispbe_schedule(pispbe, false); + return ret; } From 29a3acb1b7c4597a10ad882e89b99eedc16d0927 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Fri, 6 Nov 2026 14:55:22 +0000 Subject: [PATCH 37/44] fixup! PM: suspend: Hide mem sleep when not supported --- kernel/power/suspend.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/kernel/power/suspend.c b/kernel/power/suspend.c index 39baa6c18da817..8c270ea826e840 100644 --- a/kernel/power/suspend.c +++ b/kernel/power/suspend.c @@ -31,6 +31,7 @@ #include #include #include +#include #include "power.h" @@ -207,6 +208,11 @@ static int __init rpi_disable_s2idle(void) u32 val; int ret; + np = of_find_compatible_node(NULL, NULL, "raspberrypi,bcm2835-firmware"); + if (!np) + return 0; + of_node_put(np); + if (!suspend_ops) goto disable_s2idle; From f1974eb55c7e097f925004306342148988ceeabb Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Fri, 6 Nov 2026 14:55:34 +0000 Subject: [PATCH 38/44] fixup! arm64: configs: bcm2712: Enable DPM watchdog --- arch/arm64/configs/bcm2712_defconfig | 1 - 1 file changed, 1 deletion(-) diff --git a/arch/arm64/configs/bcm2712_defconfig b/arch/arm64/configs/bcm2712_defconfig index 08555614224373..e5ae218b3d6b3d 100644 --- a/arch/arm64/configs/bcm2712_defconfig +++ b/arch/arm64/configs/bcm2712_defconfig @@ -60,7 +60,6 @@ CONFIG_RANDOMIZE_BASE=y CONFIG_CMDLINE="console=ttyAMA0,115200 kgdboc=ttyAMA0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 rootwait" CONFIG_PM_DEBUG=y CONFIG_DPM_WATCHDOG=y -CONFIG_DPM_WATCHDOG_TIMEOUT=30 CONFIG_DPM_WATCHDOG_WARNING_TIMEOUT=5 CONFIG_CPU_IDLE=y CONFIG_CPU_FREQ=y From 11f6cc3dea1c8658994910d47d1be64e26c0f9e9 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Fri, 6 Nov 2026 14:55:44 +0000 Subject: [PATCH 39/44] fixup! ACPI: PCI: bcm2712: Set acpi_storage_d3 to true on bcm2712 --- drivers/nvme/host/pci.c | 5 +++++ include/linux/acpi.h | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/drivers/nvme/host/pci.c b/drivers/nvme/host/pci.c index 6aed2894ad2dc5..3d42a4debf50ba 100644 --- a/drivers/nvme/host/pci.c +++ b/drivers/nvme/host/pci.c @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -3572,6 +3573,10 @@ static struct nvme_dev *nvme_pci_alloc_dev(struct pci_dev *pdev, "platform quirk: setting simple suspend\n"); quirks |= NVME_QUIRK_SIMPLE_SUSPEND; } + /* pcie-brcmstb takes the link down over system suspend */ + if (!(quirks & NVME_QUIRK_FORCE_NO_SIMPLE_SUSPEND) && + of_machine_is_compatible("brcm,bcm2712")) + quirks |= NVME_QUIRK_SIMPLE_SUSPEND; ret = nvme_init_ctrl(&dev->ctrl, &pdev->dev, &nvme_pci_ctrl_ops, quirks); if (ret) diff --git a/include/linux/acpi.h b/include/linux/acpi.h index a9faa5a52fa140..8871f2edca7126 100644 --- a/include/linux/acpi.h +++ b/include/linux/acpi.h @@ -13,7 +13,6 @@ #include #include #include -#include #include #include #include @@ -1197,7 +1196,7 @@ static inline int acpi_dev_pm_attach(struct device *dev, bool power_on) } static inline bool acpi_storage_d3(struct device *dev) { - return of_machine_is_compatible("brcm,bcm2712"); + return false; } static inline bool acpi_dev_state_d0(struct device *dev) { From b1c0941411d7b647a03eb6cc38881668c2c855b6 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Fri, 6 Nov 2026 14:55:50 +0000 Subject: [PATCH 40/44] fixup! Bluetooth: hci_bcm: Add support for powering off during suspend --- drivers/bluetooth/hci_bcm.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/drivers/bluetooth/hci_bcm.c b/drivers/bluetooth/hci_bcm.c index 5862fe9a0e7877..0ab0e2e7d8f446 100644 --- a/drivers/bluetooth/hci_bcm.c +++ b/drivers/bluetooth/hci_bcm.c @@ -877,7 +877,11 @@ static int bcm_suspend(struct device *dev) struct hci_dev *hdev = bdev->hu->hdev; /* Wait for power_on before suspending. bcm_setup causes - * a timeout if a suspend occurs duriung it + * a timeout if a suspend occurs during it. + * bcm_device_lock is not needed: this is a serdev device, and + * bdev->hu only changes in serdev probe/remove, which hold the + * device_lock that the PM core holds here. Taking it would + * deadlock against bcm_dequeue()/bcm_recv() during setup. */ if (hdev) flush_work(&hdev->power_on); From 61ffa0148742f607b72f32b03579ae17cc2c6846 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Fri, 6 Nov 2026 14:55:50 +0000 Subject: [PATCH 41/44] fixup! pinctrl: pinctrl-brcmstb: Save pin mux and pad during suspend --- drivers/pinctrl/bcm/pinctrl-brcmstb-bcm2712.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/pinctrl/bcm/pinctrl-brcmstb-bcm2712.c b/drivers/pinctrl/bcm/pinctrl-brcmstb-bcm2712.c index befd6680cd69b9..e84016cbc5b034 100644 --- a/drivers/pinctrl/bcm/pinctrl-brcmstb-bcm2712.c +++ b/drivers/pinctrl/bcm/pinctrl-brcmstb-bcm2712.c @@ -740,7 +740,7 @@ static struct platform_driver bcm2712_pinctrl_driver = { .name = "pinctrl-bcm2712", .of_match_table = bcm2712_pinctrl_match, .suppress_bind_attrs = true, - .pm = &bcm2712_pinctrl_pm_ops, + .pm = pm_sleep_ptr(&bcm2712_pinctrl_pm_ops), }, }; module_platform_driver(bcm2712_pinctrl_driver); From 62748d6ff2f87514ad8e87589a1054bbc0a9a21e Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Fri, 6 Nov 2026 16:32:15 +0000 Subject: [PATCH 42/44] media: rp1-cfe: Stop and restart streaming over system suspend The system sleep callbacks only forced a runtime suspend, so a stream running across suspend-to-RAM did not restart: no CSI-2 channel was re-armed (CSI2_DISCARDS_INACTIVE kept rising), the FE stayed idle and userspace waited for buffers that never completed. In .suspend, stop the sensor and wait for the frame in flight to finish on every node. libcamera pairs the buffers of the CFE nodes by order, so a node that misses a frame the others completed (the embedded data EOF arrives a few ms after the FE EOF) leaves every later request paired with the wrong buffers and the pipeline stalls. Any frame still cut short is completed with an error on each node it started on. Then stop the interrupts, the CSI-2 channels, the FE and the receiver, and put the prepared buffers back at the head of their queues. In .resume, restart the channels as cfe_start_streaming() does, reopen the receiver and start the sensor. The I2C controller is only suspended in the late phase, so the sensor can still be reached from both callbacks. Driving the sensor from here means it is started only after the receiver is ready, whichever sensor driver is in use. Signed-off-by: Dom Cobley --- .../media/platform/raspberrypi/rp1_cfe/cfe.c | 148 +++++++++++++++++- 1 file changed, 147 insertions(+), 1 deletion(-) diff --git a/drivers/media/platform/raspberrypi/rp1_cfe/cfe.c b/drivers/media/platform/raspberrypi/rp1_cfe/cfe.c index 6bda6515714fd1..866f9ec2c4596d 100644 --- a/drivers/media/platform/raspberrypi/rp1_cfe/cfe.c +++ b/drivers/media/platform/raspberrypi/rp1_cfe/cfe.c @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -313,6 +314,10 @@ struct cfe_device { struct pisp_fe_device fe; int fe_csi2_channel; + + int irq; + /* Nodes that were streaming when the system suspended */ + unsigned long suspended_nodes; }; static inline bool is_fe_enabled(struct cfe_device *cfe) @@ -2342,7 +2347,8 @@ static int cfe_probe(struct platform_device *pdev) goto err_cfe_put; } - ret = devm_request_irq(&pdev->dev, ret, cfe_isr, 0, "rp1-cfe", cfe); + cfe->irq = ret; + ret = devm_request_irq(&pdev->dev, cfe->irq, cfe_isr, 0, "rp1-cfe", cfe); if (ret) { dev_err(&pdev->dev, "Unable to request interrupt\n"); ret = -EINVAL; @@ -2486,8 +2492,148 @@ static int cfe_runtime_resume(struct device *dev) return 0; } +/* + * Stop the sensor and then the receiver, and put the buffers in flight back at + * the head of their queues, so that resume can restart the stream the way + * cfe_start_streaming() does. + */ +static bool cfe_frame_in_flight(struct cfe_device *cfe) +{ + unsigned int i, fs_count = 0; + bool busy = false, first = true; + unsigned long flags; + + spin_lock_irqsave(&cfe->state_lock, flags); + for (i = 0; i < NUM_NODES; i++) { + struct cfe_node *node = &cfe->node[i]; + + if (!(cfe->suspended_nodes & BIT(i))) + continue; + if (node->cur_frm || (!first && node->fs_count != fs_count)) + busy = true; + fs_count = node->fs_count; + first = false; + } + spin_unlock_irqrestore(&cfe->state_lock, flags); + + return busy; +} + +static int __maybe_unused cfe_suspend(struct device *dev) +{ + struct cfe_device *cfe = dev_get_drvdata(dev); + unsigned int i, fs_count = 0; + unsigned long flags; + bool fe_stop, busy; + + cfe->suspended_nodes = 0; + for (i = 0; i < NUM_NODES; i++) { + if (check_state(cfe, NODE_STREAMING, i)) + cfe->suspended_nodes |= BIT(i); + } + + if (!cfe->suspended_nodes) + return 0; + + if (v4l2_subdev_call(cfe->sensor, video, s_stream, 0) < 0) + cfe_err("stream off failed in subdev\n"); + + /* + * Userspace pairs the buffers of each node by order, so every node must + * have completed the same frames when the hardware is stopped. + */ + read_poll_timeout(cfe_frame_in_flight, busy, !busy, 5000, 250000, + false, cfe); + + fe_stop = is_fe_enabled(cfe) && + test_all_nodes(cfe, NODE_ENABLED, NODE_STREAMING); + + cfg_reg_write(cfe, MIPICFG_INTE, 0); + synchronize_irq(cfe->irq); + + for (i = 0; i < NUM_NODES; i++) { + if (!(cfe->suspended_nodes & BIT(i))) + continue; + cfe_stop_channel(&cfe->node[i], fe_stop); + fe_stop = false; + } + + csi2_close_rx(&cfe->csi2); + + spin_lock_irqsave(&cfe->state_lock, flags); + for (i = 0; i < NUM_NODES; i++) { + if (cfe->suspended_nodes & BIT(i)) + fs_count = max(fs_count, cfe->node[i].fs_count); + } + for (i = 0; i < NUM_NODES; i++) { + struct cfe_node *node = &cfe->node[i]; + + if (!(cfe->suspended_nodes & BIT(i))) + continue; + + /* Fail any frame that was cut short, on every node it began on. */ + if (node->cur_frm) + cfe_process_buffer_complete(node, VB2_BUF_STATE_ERROR); + node->cur_frm = NULL; + if (node->fs_count < fs_count && node->next_frm) { + node->cur_frm = node->next_frm; + node->next_frm = NULL; + node->fs_count = fs_count; + cfe_process_buffer_complete(node, VB2_BUF_STATE_ERROR); + node->cur_frm = NULL; + } + node->fs_count = fs_count; + + if (node->next_frm) + list_add(&node->next_frm->list, &node->dma_queue); + node->next_frm = NULL; + + clear_state(cfe, FS_INT | FE_INT | NODE_STREAMING, i); + } + cfe->job_queued = false; + cfe->job_ready = false; + spin_unlock_irqrestore(&cfe->state_lock, flags); + + return 0; +} + +static int __maybe_unused cfe_resume(struct device *dev) +{ + struct cfe_device *cfe = dev_get_drvdata(dev); + unsigned long flags; + unsigned int i; + int ret; + + if (!cfe->suspended_nodes) + return 0; + + spin_lock_irqsave(&cfe->state_lock, flags); + cfe->job_ready = cfe_check_job_ready(cfe); + spin_unlock_irqrestore(&cfe->state_lock, flags); + + /* As cfe_start_streaming(): the last node to start also starts the FE. */ + for (i = 0; i < NUM_NODES; i++) { + if (!(cfe->suspended_nodes & BIT(i))) + continue; + set_state(cfe, NODE_STREAMING, i); + cfe_start_channel(&cfe->node[i]); + } + cfe->suspended_nodes = 0; + + cfg_reg_write(cfe, MIPICFG_CFG, MIPICFG_CFG_SEL_CSI); + cfg_reg_write(cfe, MIPICFG_INTE, MIPICFG_INT_CSI_DMA | MIPICFG_INT_PISP_FE); + csi2_open_rx(&cfe->csi2); + + ret = v4l2_subdev_call(cfe->sensor, video, s_stream, 1); + if (ret < 0) + cfe_err("stream on failed in subdev\n"); + + return ret; +} + static const struct dev_pm_ops cfe_pm_ops = { SET_RUNTIME_PM_OPS(cfe_runtime_suspend, cfe_runtime_resume, NULL) + SET_SYSTEM_SLEEP_PM_OPS(cfe_suspend, cfe_resume) SET_LATE_SYSTEM_SLEEP_PM_OPS(pm_runtime_force_suspend, pm_runtime_force_resume) }; From 50b93851e41978d1accf6b8d2856b10fd12db36f Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Fri, 6 Nov 2026 19:57:37 +0000 Subject: [PATCH 43/44] spi: rp2040-gpio-bridge: Add system sleep PM ops The driver only has runtime PM callbacks, which switch the "power" regulator. Since commit bca84a7b93fdc ("PM: sleep: Use DPM_FLAG_SMART_SUSPEND conditionally") a device without system sleep callbacks gets smart suspend, so if it is runtime suspended at system suspend the PM core sets it RPM_ACTIVE in the noirq resume phase without calling its runtime resume callback. The next autosuspend then disables a regulator the driver never re-enabled: unbalanced disables for cam0_reg WARNING: ... at drivers/regulator/core.c:3050 _regulator_disable rp2040_gbdg_runtime_suspend+0x28/0x80 [spi_rp2040_gpio_bridge] and the device is left in the runtime PM error state. It happens after every system suspend taken with the AI Camera idle. The sensor, which uses the bridge's GPIOs through a runtime PM device link, gets the same treatment. Add empty system sleep callbacks, so smart suspend is not used for the bridge or its consumers and their runtime PM status is kept as is. pm_runtime_force_suspend()/pm_runtime_force_resume() are not used: the bridge shares its supply with the sensor, and powering it off while streaming drops the sensor's power over system sleep, after which the sensor no longer responds when streaming is restarted. Signed-off-by: Dom Cobley --- drivers/spi/spi-rp2040-gpio-bridge.c | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/drivers/spi/spi-rp2040-gpio-bridge.c b/drivers/spi/spi-rp2040-gpio-bridge.c index 84efc3a8eada0f..2bb7b3ee98b8ea 100644 --- a/drivers/spi/spi-rp2040-gpio-bridge.c +++ b/drivers/spi/spi-rp2040-gpio-bridge.c @@ -1237,8 +1237,26 @@ static int rp2040_gbdg_runtime_resume(struct device *dev) return rp2040_gbdg_power_on(i2c_get_clientdata(client)); } -static const struct dev_pm_ops rp2040_gbdg_pm_ops = { SET_RUNTIME_PM_OPS( - rp2040_gbdg_runtime_suspend, rp2040_gbdg_runtime_resume, NULL) }; +/* + * Intentionally empty: having system sleep callbacks stops the PM core from + * treating the device as smart suspend capable, so its runtime PM status is + * left alone across system sleep. + */ +static int __maybe_unused rp2040_gbdg_suspend(struct device *dev) +{ + return 0; +} + +static int __maybe_unused rp2040_gbdg_resume(struct device *dev) +{ + return 0; +} + +static const struct dev_pm_ops rp2040_gbdg_pm_ops = { + SET_RUNTIME_PM_OPS(rp2040_gbdg_runtime_suspend, + rp2040_gbdg_runtime_resume, NULL) + SET_SYSTEM_SLEEP_PM_OPS(rp2040_gbdg_suspend, rp2040_gbdg_resume) +}; static struct i2c_driver rp2040_gbdg_driver = { .driver = { From d41b941dcc35d28e48f071d7dcdb6967ef88ad17 Mon Sep 17 00:00:00 2001 From: Dom Cobley Date: Mon, 9 Nov 2026 16:57:32 +0000 Subject: [PATCH 44/44] drm/vc4: hdmi: Suspend the audio card over system sleep While HDMI audio is playing, system suspend fails with: bcm2835-dma 1000010600.dma: PM: failed to suspend late: error -16 The ASoC card is registered against the HDMI device, but nothing suspends it, so the PCM is still running and its DMA channel is busy when bcm2835-dma checks in its suspend_late callback. Use snd_soc_suspend() and snd_soc_resume() as the HDMI device's normal system sleep callbacks. They run before the late force suspend that turns the controller off, stop the PCM (terminating the DMA), and let userspace restart it after resume. The device drvdata already points at the snd_soc_card, as vc4_hdmi_audio_init() arranges, and snd_soc_suspend() returns early when no card was registered. Signed-off-by: Dom Cobley --- drivers/gpu/drm/vc4/vc4_hdmi.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/gpu/drm/vc4/vc4_hdmi.c b/drivers/gpu/drm/vc4/vc4_hdmi.c index ff26e8e479bb25..8bde8a85648b1b 100644 --- a/drivers/gpu/drm/vc4/vc4_hdmi.c +++ b/drivers/gpu/drm/vc4/vc4_hdmi.c @@ -3453,6 +3453,8 @@ static const struct dev_pm_ops vc4_hdmi_pm_ops = { SET_RUNTIME_PM_OPS(vc4_hdmi_runtime_suspend, vc4_hdmi_runtime_resume, NULL) + /* drvdata aliases the snd_soc_card, see vc4_hdmi_audio_init() */ + SYSTEM_SLEEP_PM_OPS(snd_soc_suspend, snd_soc_resume) LATE_SYSTEM_SLEEP_PM_OPS(pm_runtime_force_suspend, pm_runtime_force_resume) };