From e88efa74ab49510d81538594acfdb9c50469decc Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Fri, 10 Jul 2026 17:25:05 +0300 Subject: [PATCH 01/11] clk: sunxi-ng: restore CCU state after suspend-to-off On boards where a system ("mem") suspend is implemented as a suspend-to-off, the PMIC drops VDD-SYS and the whole CCU register file comes back at reset defaults. Firmware restores only what it needs to re-enter the kernel (the base PLLs and the CPU/bus/DRAM clock tree); every other clock is the kernel's to restore. Snapshot the CCU register file at suspend and replay it on resume, excluding the firmware-owned registers. Platforms that resume with the CCU intact see no change: the restore only rewrites registers whose value differs from the snapshot. Signed-off-by: Evgeny Boger Co-authored-by: Claude Fable 5 --- drivers/clk/sunxi-ng/ccu-sun50i-h6-r.c | 25 ++++ drivers/clk/sunxi-ng/ccu-sun50i-h616.c | 57 ++++++++ drivers/clk/sunxi-ng/ccu_common.c | 183 +++++++++++++++++++++++++ drivers/clk/sunxi-ng/ccu_common.h | 43 ++++++ 4 files changed, 308 insertions(+) diff --git a/drivers/clk/sunxi-ng/ccu-sun50i-h6-r.c b/drivers/clk/sunxi-ng/ccu-sun50i-h6-r.c index d24fa3449303..76bf95cdca43 100644 --- a/drivers/clk/sunxi-ng/ccu-sun50i-h6-r.c +++ b/drivers/clk/sunxi-ng/ccu-sun50i-h6-r.c @@ -207,6 +207,29 @@ static const struct sunxi_ccu_desc sun50i_h6_r_ccu_desc = { .num_resets = ARRAY_SIZE(sun50i_h6_r_ccu_resets), }; +/* + * On the T507/WB8 a system suspend is a suspend-to-off: firmware puts the + * LPDDR4 in self-refresh and the PMIC drops VDD-SYS, so on resume the R_CCU is + * back at its reset defaults too (the main CCU is restored the same way in + * ccu-sun50i-h616.c). Firmware brings the R-CPU / R-bus root (ar100, 0x000) + * back up on its own -- it needs the R-domain alive to drive the PMIC over + * R_I2C / R_RSB -- so that register is firmware-owned and must not be + * rewritten. The kernel restores the rest of the R-domain (the R-APB dividers, + * the peripheral bus gates and the IR/1-wire module clocks) from the shared + * syscore handler in ccu_common.c, so a kernel-driven R-domain peripheral is + * not left gated at its reset default across the off-cycle. The R_CCU has no + * PLLs of its own. + */ +static const u16 sun50i_h616_r_ccu_firmware_regs[] = { + 0x000, /* ar100 / R-bus root: firmware restores it for the PMIC path */ +}; + +static const struct ccu_pm sun50i_h616_r_ccu_pm = { + .reg_size = 0x210, + .firmware_regs = sun50i_h616_r_ccu_firmware_regs, + .num_firmware_regs = ARRAY_SIZE(sun50i_h616_r_ccu_firmware_regs), +}; + static const struct sunxi_ccu_desc sun50i_h616_r_ccu_desc = { .ccu_clks = sun50i_h6_r_ccu_clks, .num_ccu_clks = ARRAY_SIZE(sun50i_h6_r_ccu_clks), @@ -215,6 +238,8 @@ static const struct sunxi_ccu_desc sun50i_h616_r_ccu_desc = { .resets = sun50i_h616_r_ccu_resets, .num_resets = ARRAY_SIZE(sun50i_h616_r_ccu_resets), + + .pm = &sun50i_h616_r_ccu_pm, }; static int sun50i_h6_r_ccu_probe(struct platform_device *pdev) diff --git a/drivers/clk/sunxi-ng/ccu-sun50i-h616.c b/drivers/clk/sunxi-ng/ccu-sun50i-h616.c index f592fa3032a2..98ca979d618c 100644 --- a/drivers/clk/sunxi-ng/ccu-sun50i-h616.c +++ b/drivers/clk/sunxi-ng/ccu-sun50i-h616.c @@ -1100,6 +1100,61 @@ static const struct ccu_reset_map sun50i_h616_ccu_resets[] = { [RST_BUS_HDCP] = { 0xc4c, BIT(16) }, }; +/* + * On the T507/WB8 a system suspend is a suspend-to-off: firmware puts the + * LPDDR4 in self-refresh and the PMIC drops VDD-SYS, so on resume the whole + * CCU is back at reset defaults. Firmware restores only what it needs to + * re-enter the kernel -- the base PLLs (CPUX, DDR0, PERIPH0) and the live + * CPU/bus/DRAM clock tree -- and the kernel restores the rest from a syscore + * handler (see ccu_common.c). These are the register offsets firmware owns and + * that are therefore already running when the kernel resumes; the kernel must + * never write them. + */ +static const u16 sun50i_h616_ccu_firmware_regs[] = { + SUN50I_H616_PLL_CPUX_REG, /* CPU PLL */ + SUN50I_H616_PLL_DDR0_REG, /* DRAM PLL */ + SUN50I_H616_PLL_DDR1_REG, /* DRAM-domain PLL: firmware's if used, */ + /* otherwise stays at its reset default */ + SUN50I_H616_PLL_PERIPH0_REG, /* bus-tree source PLL */ + 0x500, /* CPUX / AXI / CPU-APB -- the CPU executes on this */ + 0x510, /* PSI / AHB1 / AHB2 bus fabric */ + 0x51c, /* AHB3 bus fabric */ + 0x520, /* APB1 bus fabric */ + 0x524, /* APB2 bus fabric */ + 0x540, /* MBUS -- memory bus to DRAM */ + 0x800, /* DRAM clock */ + 0x804, /* MBUS mat clock gates */ + 0x80c, /* BUS_DRAM gate/reset */ + 0x810, /* end of the firmware-owned DRAM window (NAND0, unused on WB8) */ +}; + +/* + * Peripheral PLLs to re-enable and re-lock before restoring the muxes and + * dividers that draw from them. The base PLLs (CPUX/DDR0/DDR1/PERIPH0) are + * owned by firmware and are not listed here. The GPU PLL has no usable lock + * bit on T507, so it is restored with a fixed settle delay (lock = 0) instead + * of a poll -- matching the GPU rate-change notifier, which likewise never + * waits for the GPU PLL to lock. + */ +static const struct ccu_pm_pll sun50i_h616_ccu_plls[] = { + { .reg = SUN50I_H616_PLL_PERIPH1_REG, .enable = BIT(31), .lock = BIT(28) }, + { .reg = SUN50I_H616_PLL_GPU_REG, .enable = BIT(31), .lock = 0 }, + { .reg = SUN50I_H616_PLL_VIDEO0_REG, .enable = BIT(31), .lock = BIT(28) }, + { .reg = SUN50I_H616_PLL_VIDEO1_REG, .enable = BIT(31), .lock = BIT(28) }, + { .reg = SUN50I_H616_PLL_VIDEO2_REG, .enable = BIT(31), .lock = BIT(28) }, + { .reg = SUN50I_H616_PLL_VE_REG, .enable = BIT(31), .lock = BIT(28) }, + { .reg = SUN50I_H616_PLL_DE_REG, .enable = BIT(31), .lock = BIT(28) }, + { .reg = SUN50I_H616_PLL_AUDIO_REG, .enable = BIT(31), .lock = BIT(28) }, +}; + +static const struct ccu_pm sun50i_h616_ccu_pm = { + .reg_size = 0x1000, + .firmware_regs = sun50i_h616_ccu_firmware_regs, + .num_firmware_regs = ARRAY_SIZE(sun50i_h616_ccu_firmware_regs), + .plls = sun50i_h616_ccu_plls, + .num_plls = ARRAY_SIZE(sun50i_h616_ccu_plls), +}; + static const struct sunxi_ccu_desc sun50i_h616_ccu_desc = { .ccu_clks = sun50i_h616_ccu_clks, .num_ccu_clks = ARRAY_SIZE(sun50i_h616_ccu_clks), @@ -1108,6 +1163,8 @@ static const struct sunxi_ccu_desc sun50i_h616_ccu_desc = { .resets = sun50i_h616_ccu_resets, .num_resets = ARRAY_SIZE(sun50i_h616_ccu_resets), + + .pm = &sun50i_h616_ccu_pm, }; static const u32 pll_regs[] = { diff --git a/drivers/clk/sunxi-ng/ccu_common.c b/drivers/clk/sunxi-ng/ccu_common.c index c7e00f0c29a5..f3d394eb6459 100644 --- a/drivers/clk/sunxi-ng/ccu_common.c +++ b/drivers/clk/sunxi-ng/ccu_common.c @@ -7,10 +7,13 @@ #include #include +#include #include #include +#include #include #include +#include #include "ccu_common.h" #include "ccu_gate.h" @@ -109,6 +112,178 @@ int ccu_pll_notifier_register(struct ccu_pll_nb *pll_nb) } EXPORT_SYMBOL_NS_GPL(ccu_pll_notifier_register, "SUNXI_CCU"); +/* + * System-suspend context save/restore. + * + * On some Allwinner platforms a system ("mem") suspend is implemented as a + * suspend-to-off: firmware puts the DRAM in self-refresh and the PMIC drops + * VDD-SYS and every peripheral rail, so on resume the whole CCU register file + * is back at its reset defaults. Firmware only restores the clocks it needs to + * re-enter the kernel -- the base PLLs and the CPU/bus/DRAM clock tree -- and + * leaves the peripheral clock, gate and reset state for the kernel to bring + * back. + * + * We do that from a syscore handler on purpose. syscore_resume() runs on the + * resume path after firmware but before any device is resumed -- even at the + * _noirq level -- single-CPU with interrupts off. That is the only phase at + * which we can guarantee every peripheral clock, gate and reset line is back + * to its pre-suspend value before a driver touches its hardware, and it lets + * us work purely from a pre-saved register image, without taking any clk + * framework lock or sleeping. + * + * Because the image is a raw snapshot of the running controller, only the + * clocks that were enabled at suspend time are re-enabled on resume, so the + * clk framework's view (which clocks it believes are gated) stays consistent + * with the hardware, and clocks whose rate was set via the framework get their + * exact dividers back with no need to re-run set_rate. + */ +struct ccu_pm_cache { + struct list_head node; + void __iomem *base; + const struct ccu_pm *pm; + u32 *regs; +}; + +static LIST_HEAD(ccu_pm_caches); + +/* PLL lock poll: match ccu_helper_wait_for_lock()'s 70 ms bound. */ +#define CCU_PM_LOCK_DELAY_US 10 +#define CCU_PM_LOCK_TIMEOUT_US 70000 +/* Settle time for PLLs that have no usable lock bit (e.g. the T507 GPU PLL). */ +#define CCU_PM_PLL_SETTLE_US 100 + +static bool ccu_pm_reg_is_firmware(const struct ccu_pm *pm, unsigned int off) +{ + unsigned int i; + + for (i = 0; i < pm->num_firmware_regs; i++) + if (pm->firmware_regs[i] == off) + return true; + + return false; +} + +static bool ccu_pm_reg_is_pll(const struct ccu_pm *pm, unsigned int off) +{ + unsigned int i; + + for (i = 0; i < pm->num_plls; i++) + if (pm->plls[i].reg == off) + return true; + + return false; +} + +static int ccu_pm_suspend(void) +{ + struct ccu_pm_cache *cache; + + list_for_each_entry(cache, &ccu_pm_caches, node) { + unsigned int off; + + for (off = 0; off < cache->pm->reg_size; off += sizeof(u32)) + cache->regs[off / sizeof(u32)] = readl(cache->base + off); + } + + return 0; +} + +static void ccu_pm_restore_pll(struct ccu_pm_cache *cache, + const struct ccu_pm_pll *pll) +{ + u32 val = cache->regs[pll->reg / sizeof(u32)]; + u32 reg; + + /* + * The saved value already carries the enable, lock-enable and + * output-enable bits that probe set up. + */ + writel(val, cache->base + pll->reg); + + /* A PLL that was gated at suspend time stays gated; nothing to lock. */ + if (!(val & pll->enable)) + return; + + if (pll->lock) + WARN_ON(readl_poll_timeout_atomic(cache->base + pll->reg, reg, + reg & pll->lock, + CCU_PM_LOCK_DELAY_US, + CCU_PM_LOCK_TIMEOUT_US)); + else + udelay(CCU_PM_PLL_SETTLE_US); +} + +static void ccu_pm_resume(void) +{ + struct ccu_pm_cache *cache; + + list_for_each_entry(cache, &ccu_pm_caches, node) { + const struct ccu_pm *pm = cache->pm; + unsigned int off, i; + + /* + * PLLs first, so they have re-locked before anything that muxes + * off a PLL is restored. + */ + for (i = 0; i < pm->num_plls; i++) + ccu_pm_restore_pll(cache, &pm->plls[i]); + + /* + * Everything else in ascending offset order. Module clocks + * (muxes/dividers) sit at lower offsets than the bus-gate/reset + * register they share a peripheral with, so restoring each word + * re-enables the gate and deasserts the reset together, in the + * same state they had at suspend. Firmware-owned registers (the + * base PLLs and the live CPU/bus/DRAM tree) and the PLLs already + * handled above are skipped. + */ + for (off = 0; off < pm->reg_size; off += sizeof(u32)) { + if (ccu_pm_reg_is_firmware(pm, off) || + ccu_pm_reg_is_pll(pm, off)) + continue; + + writel(cache->regs[off / sizeof(u32)], cache->base + off); + } + } +} + +static struct syscore_ops ccu_pm_syscore_ops = { + .suspend = ccu_pm_suspend, + .resume = ccu_pm_resume, +}; + +static int ccu_pm_init(void __iomem *reg, const struct ccu_pm *pm) +{ + struct ccu_pm_cache *cache; + + cache = kzalloc(sizeof(*cache), GFP_KERNEL); + if (!cache) + return -ENOMEM; + + cache->regs = kcalloc(pm->reg_size / sizeof(u32), sizeof(u32), + GFP_KERNEL); + if (!cache->regs) { + kfree(cache); + return -ENOMEM; + } + + cache->base = reg; + cache->pm = pm; + + /* + * The clock controller is an essential, never-unbound provider, so the + * cache is registered for the lifetime of the system: the syscore ops + * are registered once, on the first participating instance, and the + * cache is never freed. + */ + if (list_empty(&ccu_pm_caches)) + register_syscore_ops(&ccu_pm_syscore_ops); + + list_add_tail(&cache->node, &ccu_pm_caches); + + return 0; +} + static int sunxi_ccu_probe(struct sunxi_ccu *ccu, struct device *dev, struct device_node *node, void __iomem *reg, const struct sunxi_ccu_desc *desc) @@ -181,8 +356,16 @@ static int sunxi_ccu_probe(struct sunxi_ccu *ccu, struct device *dev, if (ret) goto err_del_provider; + if (desc->pm) { + ret = ccu_pm_init(reg, desc->pm); + if (ret) + goto err_unreg_reset; + } + return 0; +err_unreg_reset: + reset_controller_unregister(&reset->rcdev); err_del_provider: of_clk_del_provider(node); err_clk_unreg: diff --git a/drivers/clk/sunxi-ng/ccu_common.h b/drivers/clk/sunxi-ng/ccu_common.h index bbec283b9d99..0768f6f51a9a 100644 --- a/drivers/clk/sunxi-ng/ccu_common.h +++ b/drivers/clk/sunxi-ng/ccu_common.h @@ -49,6 +49,47 @@ static inline struct ccu_common *hw_to_ccu_common(struct clk_hw *hw) return container_of(hw, struct ccu_common, hw); } +/** + * struct ccu_pm_pll - a PLL that has to be re-enabled and re-locked when the + * CCU register state is restored after a context-losing system suspend. + * @reg: offset of the PLL control register + * @enable: enable bit(s); the lock is only polled if these were set when + * the context was saved (i.e. the PLL was running) + * @lock: lock-status bit to poll after re-enabling, or 0 if the PLL has + * no usable lock bit (a fixed settle delay is used instead) + */ +struct ccu_pm_pll { + u16 reg; + u32 enable; + u32 lock; +}; + +/** + * struct ccu_pm - per-SoC description of how to preserve peripheral clock + * state across a system suspend that powers the clock controller off. + * + * Only SoCs whose system-suspend implementation loses CCU register state + * populate this (e.g. Allwinner "suspend-to-off", where the PMIC drops + * VDD-SYS and every peripheral rail). Leaving &sunxi_ccu_desc.pm NULL keeps + * the context-retaining SoCs on their existing, no-op suspend path. + * + * @reg_size: size of the register window to snapshot, in bytes + * @firmware_regs: register offsets owned by firmware -- already live when + * the kernel resumes (the base PLLs and the CPU/bus/DRAM + * clock tree). Never written back on restore. + * @num_firmware_regs: number of entries in @firmware_regs + * @plls: PLLs to re-enable and re-lock before the muxes and + * dividers that draw from them are restored + * @num_plls: number of entries in @plls + */ +struct ccu_pm { + u16 reg_size; + const u16 *firmware_regs; + unsigned int num_firmware_regs; + const struct ccu_pm_pll *plls; + unsigned int num_plls; +}; + struct sunxi_ccu_desc { struct ccu_common **ccu_clks; unsigned long num_ccu_clks; @@ -57,6 +98,8 @@ struct sunxi_ccu_desc { const struct ccu_reset_map *resets; unsigned long num_resets; + + const struct ccu_pm *pm; }; void ccu_helper_wait_for_lock(struct ccu_common *common, u32 lock); From aba947da88f107f22814a86ac39536e7c0353680 Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Fri, 10 Jul 2026 17:25:05 +0300 Subject: [PATCH 02/11] pinctrl: sunxi: restore pin state after suspend-to-off The PIO and R_PIO blocks lose their state with VDD-SYS during a suspend-to-off. Save the pin configuration (mux, drive, pull and output latches) at suspend and restore it on resume, so pad state does not depend on firmware replaying it. No change on platforms that keep the pin controller powered across suspend. Signed-off-by: Evgeny Boger Co-authored-by: Claude Fable 5 --- drivers/pinctrl/sunxi/pinctrl-sun50i-h616-r.c | 8 + drivers/pinctrl/sunxi/pinctrl-sun50i-h616.c | 4 + drivers/pinctrl/sunxi/pinctrl-sunxi.c | 231 ++++++++++++++++++ drivers/pinctrl/sunxi/pinctrl-sunxi.h | 29 +++ 4 files changed, 272 insertions(+) diff --git a/drivers/pinctrl/sunxi/pinctrl-sun50i-h616-r.c b/drivers/pinctrl/sunxi/pinctrl-sun50i-h616-r.c index d1f7cfa824c5..f58f215957f5 100644 --- a/drivers/pinctrl/sunxi/pinctrl-sun50i-h616-r.c +++ b/drivers/pinctrl/sunxi/pinctrl-sun50i-h616-r.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "pinctrl-sunxi.h" @@ -31,6 +32,12 @@ static const struct sunxi_pinctrl_desc sun50i_h616_r_pinctrl_data = { .pins = sun50i_h616_r_pins, .npins = ARRAY_SIZE(sun50i_h616_r_pins), .pin_base = PL_BASE, + /* + * R_PIO loses all state when VDD-SYS is gated in system suspend. + * Firmware may also re-touch R_PIO for its own PMIC I2C on resume; + * that overlaps our restore idempotently (same values, no conflict). + */ + .pm_save_regs = true, }; static int sun50i_h616_r_pinctrl_probe(struct platform_device *pdev) @@ -49,6 +56,7 @@ static struct platform_driver sun50i_h616_r_pinctrl_driver = { .driver = { .name = "sun50i-h616-r-pinctrl", .of_match_table = sun50i_h616_r_pinctrl_match, + .pm = pm_sleep_ptr(&sunxi_pinctrl_pm_ops), }, }; builtin_platform_driver(sun50i_h616_r_pinctrl_driver); diff --git a/drivers/pinctrl/sunxi/pinctrl-sun50i-h616.c b/drivers/pinctrl/sunxi/pinctrl-sun50i-h616.c index 9390fd163963..b32d473b0da6 100644 --- a/drivers/pinctrl/sunxi/pinctrl-sun50i-h616.c +++ b/drivers/pinctrl/sunxi/pinctrl-sun50i-h616.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "pinctrl-sunxi.h" @@ -878,6 +879,8 @@ static const struct sunxi_pinctrl_desc h616_pinctrl_data = { .irq_read_needs_mux = true, .disable_strict_mode = true, .io_bias_cfg_variant = BIAS_VOLTAGE_PIO_POW_MODE_CTL, + /* PIO loses all state when VDD-SYS is gated in system suspend. */ + .pm_save_regs = true, }; static int h616_pinctrl_probe(struct platform_device *pdev) @@ -895,6 +898,7 @@ static struct platform_driver h616_pinctrl_driver = { .driver = { .name = "sun50i-h616-pinctrl", .of_match_table = h616_pinctrl_match, + .pm = pm_sleep_ptr(&sunxi_pinctrl_pm_ops), }, }; builtin_platform_driver(h616_pinctrl_driver); diff --git a/drivers/pinctrl/sunxi/pinctrl-sunxi.c b/drivers/pinctrl/sunxi/pinctrl-sunxi.c index 6ebb2611c2a2..2ac567e84001 100644 --- a/drivers/pinctrl/sunxi/pinctrl-sunxi.c +++ b/drivers/pinctrl/sunxi/pinctrl-sunxi.c @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -1551,6 +1552,229 @@ static int sunxi_pinctrl_setup_debounce(struct sunxi_pinctrl *pctl, return 0; } +/* + * System-sleep register context save/restore. + * + * On platforms that switch off the PIO/R_PIO power domain during system + * suspend (sunxi_pinctrl_desc.pm_save_regs), the controller comes back + * at reset defaults: every mux, data latch, drive level, pull, IO-bias + * selection and external-interrupt setting the kernel had programmed is + * gone. The generic pinctrl_pm_select_*() machinery only re-applies + * per-consumer pin muxing on resume; it does not cover GPIO output + * values, gpio-hogs or the EINT irqchip configuration, so it cannot + * bring this hardware back on its own. Mirroring pinctrl-tegra and + * pinctrl-rockchip, we snapshot the whole register file while it is + * still alive (->suspend_noirq) and write it back before any consumer + * driver or the irq core touches the pins (->resume_noirq). + */ +static int sunxi_pinctrl_pm_regs_alloc(struct platform_device *pdev, + struct sunxi_pinctrl *pctl) +{ + struct sunxi_pinctrl_pm_regs *ctx = &pctl->pm_regs; + u32 bank_words = pctl->bank_mem_size / sizeof(u32); + + ctx->gpio = devm_kcalloc(&pdev->dev, pctl->nbanks * bank_words, + sizeof(*ctx->gpio), GFP_KERNEL); + if (!ctx->gpio) + return -ENOMEM; + + if (pctl->desc->irq_banks) { + ctx->eint = devm_kcalloc(&pdev->dev, + pctl->desc->irq_banks * + SUNXI_EINT_SAVE_WORDS, + sizeof(*ctx->eint), GFP_KERNEL); + if (!ctx->eint) + return -ENOMEM; + } + + if (pctl->desc->io_bias_cfg_variant == BIAS_VOLTAGE_GRP_CONFIG) { + ctx->grp_cfg = devm_kcalloc(&pdev->dev, pctl->nbanks, + sizeof(*ctx->grp_cfg), GFP_KERNEL); + if (!ctx->grp_cfg) + return -ENOMEM; + } + + return 0; +} + +static int sunxi_pinctrl_suspend_noirq(struct device *dev) +{ + struct sunxi_pinctrl *pctl = dev_get_drvdata(dev); + struct sunxi_pinctrl_pm_regs *ctx = &pctl->pm_regs; + u32 bank_words = pctl->bank_mem_size / sizeof(u32); + unsigned int bank, w; + u32 base; + + /* GPIO banks: mux, data, drive level and pull, in hardware order. */ + for (bank = 0; bank < pctl->nbanks; bank++) { + base = sunxi_bank_offset(pctl, bank * PINS_PER_BANK); + for (w = 0; w < bank_words; w++) + ctx->gpio[bank * bank_words + w] = + readl_relaxed(pctl->membase + base + w * 4); + } + + /* IO-bias / pin power-mode selection. */ + switch (pctl->desc->io_bias_cfg_variant) { + case BIAS_VOLTAGE_PIO_POW_MODE_CTL: + ctx->pow_mod_ctl = readl_relaxed(pctl->membase + + pctl->pow_mod_sel_offset + PIO_POW_MOD_CTL_OFS); + fallthrough; + case BIAS_VOLTAGE_PIO_POW_MODE_SEL: + ctx->pow_mod_sel = readl_relaxed(pctl->membase + + pctl->pow_mod_sel_offset); + break; + case BIAS_VOLTAGE_GRP_CONFIG: + for (bank = 0; bank < pctl->nbanks; bank++) + ctx->grp_cfg[bank] = readl_relaxed(pctl->membase + + GRP_CFG_REG + bank * 4); + break; + default: + break; + } + + /* + * EINT irqchip: config, control (enable), status and debounce. + * suspend_device_irqs() has already masked every non-wake EINT in + * hardware (IRQCHIP_MASK_ON_SUSPEND) before this noirq callback + * runs, so the control word captured here is exactly the set the + * kernel wants left armed (e.g. wake sources). + */ + for (bank = 0; bank < pctl->desc->irq_banks; bank++) { + base = IRQ_CFG_REG + + sunxi_irq_hw_bank_num(pctl->desc, bank) * IRQ_MEM_SIZE; + for (w = 0; w < SUNXI_EINT_SAVE_WORDS; w++) + ctx->eint[bank * SUNXI_EINT_SAVE_WORDS + w] = + readl_relaxed(pctl->membase + base + w * 4); + } + + ctx->valid = true; + + return 0; +} + +static int sunxi_pinctrl_resume_noirq(struct device *dev) +{ + struct sunxi_pinctrl *pctl = dev_get_drvdata(dev); + struct sunxi_pinctrl_pm_regs *ctx = &pctl->pm_regs; + u32 bank_words = pctl->bank_mem_size / sizeof(u32); + unsigned int cfg_words = IRQ_PER_BANK * IRQ_CFG_IRQ_BITS / + BITS_PER_TYPE(u32); + unsigned int bank, w; + u32 base; + + if (!ctx->valid) + return 0; + + /* + * Write ordering matters: this is an industrial board whose GPIOs + * gate relays and power rails, so no pad may glitch mid-restore. + * + * 1) Data (output latch) first, while every pad is still an input + * (reset default). Loading the latch before the mux switches a + * pad to output makes it drive the intended level from the first + * cycle, with no transient through the reset value. + */ + for (bank = 0; bank < pctl->nbanks; bank++) { + base = sunxi_bank_offset(pctl, bank * PINS_PER_BANK); + writel_relaxed(ctx->gpio[bank * bank_words + DATA_REGS_OFFSET / 4], + pctl->membase + base + DATA_REGS_OFFSET); + } + + /* + * 2) Drive strength and pull, so the pad's electrical behaviour is + * already correct once it starts driving below. + */ + for (bank = 0; bank < pctl->nbanks; bank++) { + base = sunxi_bank_offset(pctl, bank * PINS_PER_BANK); + for (w = 0; w < pctl->dlevel_field_width; w++) + writel_relaxed(ctx->gpio[bank * bank_words + + DLEVEL_REGS_OFFSET / 4 + w], + pctl->membase + base + + DLEVEL_REGS_OFFSET + w * 4); + for (w = 0; w < PULL_FIELD_WIDTH; w++) + writel_relaxed(ctx->gpio[bank * bank_words + + pctl->pull_regs_offset / 4 + w], + pctl->membase + base + + pctl->pull_regs_offset + w * 4); + } + + /* 3) IO-bias / power-mode, before any pad changes function. */ + switch (pctl->desc->io_bias_cfg_variant) { + case BIAS_VOLTAGE_PIO_POW_MODE_CTL: + writel_relaxed(ctx->pow_mod_ctl, pctl->membase + + pctl->pow_mod_sel_offset + PIO_POW_MOD_CTL_OFS); + fallthrough; + case BIAS_VOLTAGE_PIO_POW_MODE_SEL: + writel_relaxed(ctx->pow_mod_sel, + pctl->membase + pctl->pow_mod_sel_offset); + break; + case BIAS_VOLTAGE_GRP_CONFIG: + for (bank = 0; bank < pctl->nbanks; bank++) + writel_relaxed(ctx->grp_cfg[bank], pctl->membase + + GRP_CFG_REG + bank * 4); + break; + default: + break; + } + + /* + * 4) Mux ("cfg") last of the pad fields: pads now take their final + * function/direction and drive the values latched in step 1. + */ + for (bank = 0; bank < pctl->nbanks; bank++) { + base = sunxi_bank_offset(pctl, bank * PINS_PER_BANK); + for (w = 0; w < MUX_FIELD_WIDTH; w++) + writel_relaxed(ctx->gpio[bank * bank_words + + MUX_REGS_OFFSET / 4 + w], + pctl->membase + base + MUX_REGS_OFFSET + w * 4); + } + + /* + * 5) EINT trigger config and debounce, then 6) clear any pending + * bit the restore transitions above may have latched, then + * 7) re-arm the enable mask. Clearing before enabling stops a + * stale edge from firing on resume. The enable mask comes from + * the snapshot (the driver's own bookkeeping taken while the + * hardware was alive), never from a read of the now-reset + * controller. Non-wake EINTs the core masked at suspend are + * additionally unmasked by resume_device_irqs() after this + * callback returns; writing the same enable bit twice is + * idempotent. + */ + for (bank = 0; bank < pctl->desc->irq_banks; bank++) { + u32 *eint = &ctx->eint[bank * SUNXI_EINT_SAVE_WORDS]; + + base = IRQ_CFG_REG + + sunxi_irq_hw_bank_num(pctl->desc, bank) * IRQ_MEM_SIZE; + + for (w = 0; w < cfg_words; w++) + writel_relaxed(eint[w], pctl->membase + base + w * 4); + writel_relaxed(eint[(IRQ_DEBOUNCE_REG - IRQ_CFG_REG) / 4], + pctl->membase + base + + (IRQ_DEBOUNCE_REG - IRQ_CFG_REG)); + writel_relaxed(0xffffffff, pctl->membase + base + + (IRQ_STATUS_REG - IRQ_CFG_REG)); + writel_relaxed(eint[(IRQ_CTRL_REG - IRQ_CFG_REG) / 4], + pctl->membase + base + + (IRQ_CTRL_REG - IRQ_CFG_REG)); + } + + /* + * Push the posted restore writes out to the controller with a + * read-back, then order that completion before consumer drivers + * and the irq core start touching the pins. + */ + readl_relaxed(pctl->membase); + /* Barrier pairs with the read-back above; see comment. */ + rmb(); + + return 0; +} + +DEFINE_NOIRQ_DEV_PM_OPS(sunxi_pinctrl_pm_ops, + sunxi_pinctrl_suspend_noirq, + sunxi_pinctrl_resume_noirq); + int sunxi_pinctrl_init_with_flags(struct platform_device *pdev, const struct sunxi_pinctrl_desc *desc, unsigned long flags) @@ -1668,6 +1892,7 @@ int sunxi_pinctrl_init_with_flags(struct platform_device *pdev, pctl->chip->can_sleep = false; pctl->chip->ngpio = round_up(last_pin, PINS_PER_BANK) - pctl->desc->pin_base; + pctl->nbanks = pctl->chip->ngpio / PINS_PER_BANK; pctl->chip->label = dev_name(&pdev->dev); pctl->chip->parent = &pdev->dev; pctl->chip->base = pctl->desc->pin_base; @@ -1744,6 +1969,12 @@ int sunxi_pinctrl_init_with_flags(struct platform_device *pdev, sunxi_pinctrl_setup_debounce(pctl, node); + if (desc->pm_save_regs) { + ret = sunxi_pinctrl_pm_regs_alloc(pdev, pctl); + if (ret) + goto gpiochip_error; + } + dev_info(&pdev->dev, "initialized sunXi PIO driver\n"); return 0; diff --git a/drivers/pinctrl/sunxi/pinctrl-sunxi.h b/drivers/pinctrl/sunxi/pinctrl-sunxi.h index c538884106b9..a47c736ea7f4 100644 --- a/drivers/pinctrl/sunxi/pinctrl-sunxi.h +++ b/drivers/pinctrl/sunxi/pinctrl-sunxi.h @@ -74,6 +74,14 @@ #define IRQ_MEM_SIZE 0x20 +/* + * Number of consecutive 32-bit registers, starting at a bank's + * IRQ_CFG_REG window, that together hold the whole EINT state saved for + * system sleep: four config words (trigger type), the control (enable) + * word, the status word and the debounce word. + */ +#define SUNXI_EINT_SAVE_WORDS 7 + #define IRQ_EDGE_RISING 0x00 #define IRQ_EDGE_FALLING 0x01 #define IRQ_LEVEL_HIGH 0x02 @@ -142,6 +150,13 @@ struct sunxi_pinctrl_desc { bool irq_read_needs_mux; bool disable_strict_mode; enum sunxi_desc_bias_voltage io_bias_cfg_variant; + /* + * Set on platforms where the PIO power domain can be switched off + * during system suspend, losing all register state. Enables a full + * register-context save/restore across system sleep via + * sunxi_pinctrl_pm_ops. Leave unset where the block is retained. + */ + bool pm_save_regs; }; struct sunxi_pinctrl_function { @@ -160,6 +175,16 @@ struct sunxi_pinctrl_regulator { refcount_t refcount; }; +/* Register context saved across a state-losing system suspend. */ +struct sunxi_pinctrl_pm_regs { + u32 *gpio; /* nbanks * (bank_mem_size / 4) words */ + u32 *eint; /* irq_banks * SUNXI_EINT_SAVE_WORDS words */ + u32 *grp_cfg; /* nbanks words, GRP_CONFIG bias variant only */ + u32 pow_mod_sel; + u32 pow_mod_ctl; + bool valid; +}; + struct sunxi_pinctrl { void __iomem *membase; struct gpio_chip *chip; @@ -180,6 +205,8 @@ struct sunxi_pinctrl { u32 pull_regs_offset; u32 dlevel_field_width; u32 pow_mod_sel_offset; + u32 nbanks; + struct sunxi_pinctrl_pm_regs pm_regs; }; #define SUNXI_PIN(_pin, ...) \ @@ -301,6 +328,8 @@ static inline u32 sunxi_grp_config_reg(u16 pin) return GRP_CFG_REG + bank * 0x4; } +extern const struct dev_pm_ops sunxi_pinctrl_pm_ops; + int sunxi_pinctrl_init_with_flags(struct platform_device *pdev, const struct sunxi_pinctrl_desc *desc, unsigned long flags); From 0bc8d89cd0a5bf66c21d837dbf76d2277b5eaa02 Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Fri, 10 Jul 2026 17:25:05 +0300 Subject: [PATCH 03/11] spi: sun6i: reinitialize the controller on resume from suspend-to-off The controller loses all register state with VDD-SYS during a suspend-to-off. Add system-sleep callbacks that quiesce the controller at suspend and fully re-initialize it on resume instead of assuming the register file survived. Signed-off-by: Evgeny Boger Co-authored-by: Claude Fable 5 --- drivers/spi/spi-sun6i.c | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/drivers/spi/spi-sun6i.c b/drivers/spi/spi-sun6i.c index 9497ec60046c..1c72105e1e45 100644 --- a/drivers/spi/spi-sun6i.c +++ b/drivers/spi/spi-sun6i.c @@ -634,6 +634,34 @@ static int sun6i_spi_runtime_suspend(struct device *dev) return 0; } +static int sun6i_spi_suspend(struct device *dev) +{ + struct spi_controller *host = dev_get_drvdata(dev); + int ret; + + ret = spi_controller_suspend(host); + if (ret) + return ret; + + ret = pm_runtime_force_suspend(dev); + if (ret) + spi_controller_resume(host); + + return ret; +} + +static int sun6i_spi_resume(struct device *dev) +{ + struct spi_controller *host = dev_get_drvdata(dev); + int ret; + + ret = pm_runtime_force_resume(dev); + if (ret) + return ret; + + return spi_controller_resume(host); +} + static bool sun6i_spi_can_dma(struct spi_controller *host, struct spi_device *spi, struct spi_transfer *xfer) @@ -826,8 +854,8 @@ static const struct of_device_id sun6i_spi_match[] = { MODULE_DEVICE_TABLE(of, sun6i_spi_match); static const struct dev_pm_ops sun6i_spi_pm_ops = { - .runtime_resume = sun6i_spi_runtime_resume, - .runtime_suspend = sun6i_spi_runtime_suspend, + RUNTIME_PM_OPS(sun6i_spi_runtime_suspend, sun6i_spi_runtime_resume, NULL) + SYSTEM_SLEEP_PM_OPS(sun6i_spi_suspend, sun6i_spi_resume) }; static struct platform_driver sun6i_spi_driver = { @@ -836,7 +864,7 @@ static struct platform_driver sun6i_spi_driver = { .driver = { .name = "sun6i-spi", .of_match_table = sun6i_spi_match, - .pm = &sun6i_spi_pm_ops, + .pm = pm_ptr(&sun6i_spi_pm_ops), }, }; module_platform_driver(sun6i_spi_driver); From 6c25185583db44396bfd6a68754f0f0cafb5c25c Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Fri, 10 Jul 2026 17:25:05 +0300 Subject: [PATCH 04/11] usb: musb: sunxi: add system-sleep support for suspend-to-off The glue's bus clock, reset, PHY and optional SRAM claim all die with VDD-SYS during a suspend-to-off. Add system-sleep callbacks that release the glue-owned resources at suspend and re-acquire and reprogram them on resume. Resource liveness keys off glue->musb so a suspend racing controller teardown is tolerated. Signed-off-by: Evgeny Boger Co-authored-by: Claude Fable 5 --- drivers/usb/musb/sunxi.c | 162 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/drivers/usb/musb/sunxi.c b/drivers/usb/musb/sunxi.c index ed38af801c5c..10aa0a7e7e69 100644 --- a/drivers/usb/musb/sunxi.c +++ b/drivers/usb/musb/sunxi.c @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -265,6 +266,15 @@ static int sunxi_musb_init(struct musb *musb) /* Stop the musb-core from doing runtime pm (not supported on sunxi) */ pm_runtime_get(musb->controller); + /* + * Mark the glue-owned resources (bus clock, reset, PHY, optional SRAM) + * as claimed for this controller. The system-sleep suspend/resume + * callbacks key off glue->musb to decide whether those resources are + * live and therefore whether they must be released/re-acquired; it is + * cleared again in sunxi_musb_exit() when they are torn down. + */ + glue->musb = musb; + return 0; error_reset_assert: @@ -297,6 +307,13 @@ static int sunxi_musb_exit(struct musb *musb) if (test_bit(SUNXI_MUSB_FL_HAS_SRAM, &glue->flags)) sunxi_sram_release(musb->controller->parent); + /* + * The glue-owned resources are released; drop the reference the + * system-sleep callbacks use so a suspend that races a controller + * teardown (e.g. a driver unbind) does not release them a second time. + */ + glue->musb = NULL; + return 0; } @@ -885,11 +902,156 @@ static const struct of_device_id sunxi_musb_match[] = { }; MODULE_DEVICE_TABLE(of, sunxi_musb_match); +/* + * System sleep support. + * + * On most sunxi SoCs a mem-sleep keeps the USB power domain alive, so the + * musb core's musb_suspend()/musb_resume() (which save and restore the + * MUSB register block via musb_save_context()/musb_restore_context()) are + * enough. On platforms whose firmware drops the whole peripheral power + * domain during system suspend the controller, the OTG PHY, the reset line + * and the bus clock all come back at their reset defaults, and the register + * snapshot the core restores is not enough on its own: the glue-owned state + * that is only ever programmed from sunxi_musb_init() (bus clock, reset + * de-assert, the sunxi-specific VEND0 PIO-mode select and the full PHY + * bring-up) is never redone. + * + * Handle that here with glue-level dev_pm_ops that mirror the teardown of + * sunxi_musb_exit() on suspend and the hardware bring-up of + * sunxi_musb_init() on resume. These run in the normal (sleep-capable) + * suspend/resume phases because the clock, reset and PHY APIs may sleep; + * _noirq would be wrong. The work is idempotent with any transitional + * firmware-side register restore, since that runs before the kernel resumes. + * + * Ordering: the musb-hdrc controller is a child of this glue device, so the + * PM core suspends it before us and resumes it after us. On suspend the + * child has therefore already quiesced the controller and saved its context + * before we power down the PHY and gate the clock. On resume we re-enable + * the clock, de-assert reset and re-init the PHY first, so that when the + * child's musb_resume() runs its musb_restore_context() writes land on a + * live, clocked, de-reset controller. Restoring MUSB_POWER re-asserts the + * gadget soft-connect pull-up on a controller that came up with it cleared, + * which the host sees as a fresh connect and re-enumerates - no userspace + * unbind/rebind or UDC re-attach needed. + * + * These callbacks release and re-acquire the same glue-owned resources as + * sunxi_musb_exit()/sunxi_musb_init(), so they must not run when the + * controller has already been torn down (child probe deferred, or a driver + * unbind before system suspend - as the transitional userspace suspend hook + * still does). They therefore key off glue->musb, which is non-NULL only + * while init() has enabled those resources and exit() has not released them; + * doing the teardown twice would gate an already-gated bus clock and, worse, + * underflow the PHY init_count so phy_init() skips the provider .init() on + * resume and the controller comes back unclocked/unconfigured. + */ +static int sunxi_musb_suspend(struct device *dev) +{ + struct sunxi_glue *glue = dev_get_drvdata(dev); + + /* + * glue->musb is set in sunxi_musb_init() and cleared in + * sunxi_musb_exit(), so it is non-NULL exactly while the glue holds the + * clock/reset/PHY (and optional SRAM) enabled. Bail out when they are + * not held - the controller never started (child probe deferred) or it + * was already torn down before this suspend (e.g. a driver unbind) - + * otherwise we would gate the bus clock, phy_exit() and assert reset a + * second time and underflow their refcounts. + */ + if (!glue->musb) + return 0; + + cancel_work_sync(&glue->work); + + if (test_bit(SUNXI_MUSB_FL_PHY_ON, &glue->flags)) { + phy_power_off(glue->phy); + clear_bit(SUNXI_MUSB_FL_PHY_ON, &glue->flags); + } + + phy_exit(glue->phy); + + if (test_bit(SUNXI_MUSB_FL_HAS_RESET, &glue->flags)) + reset_control_assert(glue->rst); + + clk_disable_unprepare(glue->clk); + + if (test_bit(SUNXI_MUSB_FL_HAS_SRAM, &glue->flags)) + sunxi_sram_release(dev); + + return 0; +} + +static int sunxi_musb_resume(struct device *dev) +{ + struct sunxi_glue *glue = dev_get_drvdata(dev); + int ret; + + if (!glue->musb) + return 0; + + if (test_bit(SUNXI_MUSB_FL_HAS_SRAM, &glue->flags)) { + ret = sunxi_sram_claim(dev); + if (ret) + goto error; + } + + ret = clk_prepare_enable(glue->clk); + if (ret) + goto error_sram_release; + + if (test_bit(SUNXI_MUSB_FL_HAS_RESET, &glue->flags)) { + ret = reset_control_deassert(glue->rst); + if (ret) + goto error_clk_disable; + } + + /* sunxi musb is PIO only; VEND0 is reset to 0 (DMA) on power loss. */ + writeb(SUNXI_MUSB_VEND0_PIO_MODE, + glue->musb->mregs + SUNXI_MUSB_VEND0); + + /* + * Full PHY bring-up from reset defaults. phy_exit() dropped the PHY + * framework init_count to 0 in suspend, so this re-runs the provider's + * .init and reprograms the PHY (and, via the clk/reset framework, its + * clocks and reset) regardless of what state they came back in. The + * PHY power / mode for the current role is re-armed by the work that + * the child's musb_platform_enable() schedules on resume. + */ + ret = phy_init(glue->phy); + if (ret) + goto error_reset_assert; + + return 0; + +error_reset_assert: + if (test_bit(SUNXI_MUSB_FL_HAS_RESET, &glue->flags)) + reset_control_assert(glue->rst); +error_clk_disable: + clk_disable_unprepare(glue->clk); +error_sram_release: + if (test_bit(SUNXI_MUSB_FL_HAS_SRAM, &glue->flags)) + sunxi_sram_release(dev); +error: + /* + * Resume failed and unwound every resource it had taken, leaving the + * glue as torn down as suspend left it. Clear glue->musb so the next + * suspend/resume no-ops instead of running the teardown a second time + * against already-released resources -- phy_exit() on a zero PHY + * init_count, clk_disable_unprepare() on an already-gated clock -- the + * same double-teardown refcount underflow glue->musb guards against. + */ + glue->musb = NULL; + return ret; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(sunxi_musb_pm_ops, sunxi_musb_suspend, + sunxi_musb_resume); + static struct platform_driver sunxi_musb_driver = { .probe = sunxi_musb_probe, .remove = sunxi_musb_remove, .driver = { .name = "musb-sunxi", + .pm = pm_sleep_ptr(&sunxi_musb_pm_ops), .of_match_table = sunxi_musb_match, }, }; From db89a780b1c1f8fbf9a14f3cd2a6937a1fc20b11 Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Fri, 10 Jul 2026 17:25:05 +0300 Subject: [PATCH 05/11] phy: sun4i-usb: reprogram PHY state on resume from suspend-to-off PHY register state (PHYCTL routing, forced session/ID bits, dual-route selection) is lost with VDD-SYS during a suspend-to-off. Reprogram it in the resume path rather than assuming it survived. Signed-off-by: Evgeny Boger Co-authored-by: Claude Fable 5 --- drivers/phy/allwinner/phy-sun4i-usb.c | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/drivers/phy/allwinner/phy-sun4i-usb.c b/drivers/phy/allwinner/phy-sun4i-usb.c index 3e9fd7badfce..faf3eb3ca969 100644 --- a/drivers/phy/allwinner/phy-sun4i-usb.c +++ b/drivers/phy/allwinner/phy-sun4i-usb.c @@ -117,6 +117,8 @@ struct sun4i_usb_phy_data { const struct sun4i_usb_phy_cfg *cfg; enum usb_dr_mode dr_mode; spinlock_t reg_lock; /* guard access to phyctl reg */ + struct mutex phy2_lock; /* guard phy2_users (needs_phy2_siddq) */ + int phy2_users; int num_phys; struct sun4i_usb_phy { struct phy *phy; @@ -373,6 +375,15 @@ static int sun4i_usb_phy_init(struct phy *_phy) } clk_disable_unprepare(phy->clk2); + + /* + * PHY2 is now held out of reset on this PHY's behalf. Count the + * user so PHY2 is only put back into reset once the last of the + * PHYs that share it has been torn down (see sun4i_usb_phy_exit). + */ + mutex_lock(&data->phy2_lock); + data->phy2_users++; + mutex_unlock(&data->phy2_lock); } if (phy->pmu && data->cfg->hci_phy_ctl_clear) { @@ -443,7 +454,20 @@ static int sun4i_usb_phy_exit(struct phy *_phy) struct sun4i_usb_phy *phy2 = &data->phys[2]; clk_disable_unprepare(phy2->clk); - reset_control_assert(phy2->reset); + + /* + * PHY2 provides a bias the other PHYs need to work, so it must + * stay out of reset as long as any of them is up. Its reset is + * exclusive (asserting it is unconditional, not ref-counted), so + * only assert it once the last sharing PHY has been torn down; + * otherwise tearing this PHY down (e.g. the musb glue's system + * suspend calling phy_exit() on PHY0) would knock out a + * co-resident EHCI/OHCI host still using PHY1/PHY3. + */ + mutex_lock(&data->phy2_lock); + if (!WARN_ON(data->phy2_users == 0) && --data->phy2_users == 0) + reset_control_assert(phy2->reset); + mutex_unlock(&data->phy2_lock); } sun4i_usb_phy_passby(phy, 0); @@ -850,6 +874,7 @@ static int sun4i_usb_phy_probe(struct platform_device *pdev) return -ENOMEM; spin_lock_init(&data->reg_lock); + mutex_init(&data->phy2_lock); INIT_DELAYED_WORK(&data->detect, sun4i_usb_phy0_id_vbus_det_scan); dev_set_drvdata(dev, data); data->cfg = of_device_get_match_data(dev); From d855210cc682456c5cca81b12a85cf0c9a5b6b5b Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Fri, 10 Jul 2026 17:25:28 +0300 Subject: [PATCH 06/11] i2c: mv64xxx: fail fast on a continuously erroring bus If the bus errors continuously (e.g. a wedged peripheral holding SDA), the error->recover->retry churn can hold a client's regmap mutex for minutes, putting every consumer of that bus in uninterruptible sleep behind it. Track the start of a failing streak and fail transfers immediately once the streak exceeds a timeout; any successful transfer resets the streak. Found on Wiren Board 8 where a jammed PMIC I2C bus turned one bad transfer into a system-wide D-state hang that even hung-task panic could not report (the panic path itself blocked on the same mutex). Signed-off-by: Evgeny Boger Co-authored-by: Claude Fable 5 --- drivers/i2c/busses/i2c-mv64xxx.c | 45 ++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/drivers/i2c/busses/i2c-mv64xxx.c b/drivers/i2c/busses/i2c-mv64xxx.c index 1792b39d54a1..0ba64dfccf5c 100644 --- a/drivers/i2c/busses/i2c-mv64xxx.c +++ b/drivers/i2c/busses/i2c-mv64xxx.c @@ -151,8 +151,27 @@ struct mv64xxx_i2c_data { bool atomic; struct work_struct error_work; bool bus_disabled; + /* + * jiffies of the first transfer error in the current failing streak + * (0 == healthy), reset on the next good transfer. Used to fail + * transfers fast once the bus has been erroring continuously past + * MV64XXX_I2C_BUS_FAIL_TIMEOUT instead of churning error->recover->retry + * forever (which pins the client's regmap mutex and D-states every + * consumer of that bus). + */ + unsigned long failing_since; }; +/* + * If the bus keeps erroring for this long without a single good transfer, + * stop the error/recover/retry churn and fail transfers hard (-ETIMEDOUT) + * rather than -EAGAIN, so a caller cannot spin on a wedged or absent slave + * (e.g. a PMIC that lost power under us across a suspend-to-off) forever. + * Far longer than normal transient-error recovery (~50-150 ms), so ordinary + * single-NAK recovery is unaffected. + */ +#define MV64XXX_I2C_BUS_FAIL_TIMEOUT (5 * HZ) + static struct mv64xxx_i2c_regs mv64xxx_i2c_regs_mv64xxx = { .addr = 0x00, .ext_addr = 0x10, @@ -336,6 +355,9 @@ mv64xxx_i2c_fsm(struct mv64xxx_i2c_data *drv_data, u32 status) // reset i2c: mv64xxx_i2c_hw_init(drv_data); + if (!drv_data->failing_since) + drv_data->failing_since = jiffies; + if (!drv_data->bus_disabled) { drv_data->bus_disabled = true; dev_err(&drv_data->adapter.dev, @@ -759,6 +781,26 @@ mv64xxx_i2c_xfer_core(struct i2c_adapter *adap, struct i2c_msg msgs[], int num) int rc, ret = num; if (drv_data->bus_disabled) { + /* + * Bus is mid error->recover->retry. Normally bounce with -EAGAIN + * so the I2C core retries once recovery re-enables it. But if the + * bus has been erroring continuously past MV64XXX_I2C_BUS_FAIL_- + * TIMEOUT (a wedged or powered-off slave), stop churning and fail + * hard with -ETIMEDOUT: -EAGAIN invites the caller to spin on us + * forever holding its regmap mutex and D-stating every other + * consumer, whereas -ETIMEDOUT is terminal and lets it unwind. + * error_work keeps attempting recovery, and failing_since resets + * on the next good transfer, so the bus returns to normal the + * instant the slave answers again. + */ + if (drv_data->failing_since && + time_after(jiffies, drv_data->failing_since + + MV64XXX_I2C_BUS_FAIL_TIMEOUT)) { + dev_warn_ratelimited(&adap->dev, + "mv64xxx_i2c: bus erroring > %u ms, failing xfer\n", + jiffies_to_msecs(MV64XXX_I2C_BUS_FAIL_TIMEOUT)); + return -ETIMEDOUT; + } dev_dbg(&adap->dev, "mv64xxx_i2c: bus is disabled, ignoring xfer\n"); msleep(50); @@ -780,6 +822,9 @@ mv64xxx_i2c_xfer_core(struct i2c_adapter *adap, struct i2c_msg msgs[], int num) if (rc < 0) ret = rc; + else + /* Good transfer: clear the fail-fast budget. */ + drv_data->failing_since = 0; drv_data->num_msgs = 0; drv_data->msgs = NULL; From a509d1ccd11dc9a915f566d75f1d6bdb865d91ed Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Fri, 10 Jul 2026 17:25:29 +0300 Subject: [PATCH 07/11] firmware: psci: report SYSTEM_SUSPEND failure A SYSTEM_SUSPEND call that returns (instead of resuming through cpu_resume) has failed, but the only symptom is an errno bubbling out of the suspend core - nearly indistinguishable from a suspend that woke immediately. PSCI_RET_DENIED in particular (typically a CPU that never went offline) surfaced as a silent -EPERM. Print the failure and the online CPU count. Signed-off-by: Evgeny Boger Co-authored-by: Claude Fable 5 --- drivers/firmware/psci/psci.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/drivers/firmware/psci/psci.c b/drivers/firmware/psci/psci.c index 38ca190d4a22..d38ba08ab024 100644 --- a/drivers/firmware/psci/psci.c +++ b/drivers/firmware/psci/psci.c @@ -534,6 +534,15 @@ static int psci_system_suspend(unsigned long unused) err = invoke_psci_fn(PSCI_FN_NATIVE(1_0, SYSTEM_SUSPEND), pa_cpu_resume, 0, 0); + /* + * This return is only reached when the firmware did NOT suspend: + * on success the system resumes via cpu_resume. A silent -EPERM + * here (PSCI_RET_DENIED) is very hard to tell apart from a + * suspend-and-instant-wake, so name the failure. + */ + if (err) + pr_err("PSCI: SYSTEM_SUSPEND failed: %d (online CPUs: %u)\n", + err, num_online_cpus()); return psci_to_linux_errno(err); } From 677a035d2fd7e99229b3ffa4b4121875eed7d000 Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Fri, 10 Jul 2026 17:25:29 +0300 Subject: [PATCH 08/11] dt-bindings: mfd: wirenboard,wbec: add wirenboard,suspend-magic-rtc Document the phandle that points the EC driver at the RTC whose general-purpose data0 register carries the firmware suspend-to-off magic. Its presence gates the in-kernel suspend-to-off coordinator, so only boards that wire it (WB8/H616) are affected. Signed-off-by: Evgeny Boger Co-authored-by: Claude Fable 5 --- .../devicetree/bindings/mfd/wirenboard,wbec.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Documentation/devicetree/bindings/mfd/wirenboard,wbec.yaml b/Documentation/devicetree/bindings/mfd/wirenboard,wbec.yaml index 1ce8ccb1206c..fa33ac6a6ee2 100644 --- a/Documentation/devicetree/bindings/mfd/wirenboard,wbec.yaml +++ b/Documentation/devicetree/bindings/mfd/wirenboard,wbec.yaml @@ -34,6 +34,16 @@ properties: wakeup-source: true + wirenboard,suspend-magic-rtc: + $ref: /schemas/types.yaml#/definitions/phandle + description: + Phandle to the Allwinner sun6i RTC whose general-purpose data register 0 + (GP_DATA0, at offset 0x100 from the RTC register base) is used to hand the + platform firmware (BL31) the magic value that selects the deepest, + power-off suspend-to-off path. Present only on boards whose firmware + implements that path (H616/T507, Wiren Board 8); when absent the driver + leaves the EC-coordinated power-off suspend feature disabled. + required: - compatible - reg From 1fc0cada22a978e858c5034005387f4f186eae53 Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Fri, 10 Jul 2026 17:25:29 +0300 Subject: [PATCH 09/11] mfd: wbec: add a suspend-to-off coordinator Coordinate EC-assisted suspend-to-off with the boot firmware: a syscore hook arms the firmware magic in the RTC data0 register inside the same no-going-back window that arms the EC's suspend mode, so the PMIC-sleep and EC-wake paths cannot desync. A PM notifier plus announce/ deannounce plumbing program the EC wake deadline around each suspend. The coordinator arms only when the wirenboard,suspend-magic-rtc DT phandle is present; other boards see no behaviour change. Module parameters: suspend_crc_verify selects the firmware's CRC-verifying magic (opt-in debug bracket, ~80 s per cycle, keep off in production); suspend_default_timeout_s bounds the EC wake deadline. Signed-off-by: Evgeny Boger Co-authored-by: Claude Fable 5 --- drivers/mfd/wbec.c | 390 ++++++++++++++++++++++++++++++++++++++- include/linux/mfd/wbec.h | 23 +++ 2 files changed, 405 insertions(+), 8 deletions(-) diff --git a/drivers/mfd/wbec.c b/drivers/mfd/wbec.c index 1df31b6ccfe9..658070b7a7b8 100644 --- a/drivers/mfd/wbec.c +++ b/drivers/mfd/wbec.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -20,11 +21,62 @@ #include #include #include +#include #include +#include +#include +#include +#include +#include +#include /* For power off WBEC activates PWON pin on PMIC for 6s */ #define WBEC_POWER_RESET_DELAY_MS 10000 +/* + * Suspend-to-off coordinator constants. + * + * On H616/T507 a PSCI SYSTEM_SUSPEND can drop VDD-SYS and every peripheral + * rail (hibernate-grade power loss). The platform firmware (BL31) only takes + * that deepest path when it finds a magic value in the sun6i RTC data0 + * register; otherwise it does the SoC-retained sleep. The magic has two + * variants that both enter suspend-to-off: FAST skips the DRAM CRC check, + * VERIFY brackets the sleep with a full-DRAM CRC (slower, opt-in). + */ +#define WBEC_SUSPEND_MAGIC_FAST 0x0ff51eeaU +#define WBEC_SUSPEND_MAGIC_VERIFY 0x0ff51eebU + +/* sun6i RTC general-purpose data0 offset from the RTC register base. */ +#define WBEC_SUSPEND_MAGIC_RTC_OFFSET 0x100 + +/* SUSPEND_CTRL.timeout_s is a 16-bit EC register. */ +#define WBEC_SUSPEND_TIMEOUT_MIN 10U +#define WBEC_SUSPEND_TIMEOUT_MAX 0xffffU + +/* + * The EC recovery deadline (the EC warm-resets timeout_s + ~10 s after the + * announce if it never sees a resume) is a hang backstop, not the wake source. + * The board is woken by the RTC alarm the user armed (rtcwake / wakealarm), so + * the deadline must land safely AFTER that alarm or it pre-empts the intended + * wake. Size timeout_s as the seconds-to-alarm plus a margin, mirroring the + * proven wb-suspend-off wrapper. The CRC-verify variant brackets the sleep with + * a full-DRAM CRC (~75 s) that runs after the wake, so its deadline needs a much + * larger margin to clear it before the backstop can fire. + */ +#define WBEC_SUSPEND_MARGIN_FAST_S 20U +#define WBEC_SUSPEND_MARGIN_VERIFY_S 90U + +static bool wbec_suspend_crc_verify; +module_param_named(suspend_crc_verify, wbec_suspend_crc_verify, bool, 0644); +MODULE_PARM_DESC(suspend_crc_verify, + "Use the CRC-verified (slower) suspend-to-off firmware magic"); + +static unsigned int wbec_suspend_default_timeout_s = WBEC_SUSPEND_TIMEOUT_MAX; +module_param_named(suspend_default_timeout_s, wbec_suspend_default_timeout_s, + uint, 0644); +MODULE_PARM_DESC(suspend_default_timeout_s, + "EC recovery deadline (s) when no RTC wakealarm is armed (long backstop; wake is then the power button)"); + /* * Mirrors LINUX_POWERON_REASON in wb-embedded-controller src/wbec.c; * append-only ABI. @@ -334,6 +386,328 @@ static int wbec_restart(struct sys_off_data *data) return NOTIFY_DONE; } +/* ----------------------------------------------------------------------- */ +/* Suspend-to-off coordinator */ + +/* + * There is exactly one EC per board. syscore_ops carry no private pointer, so + * the syscore callbacks reach the armed instance through this file-static, + * which is set only while the coordinator is armed (DT-gated, WB8/H616). + */ +static struct wbec *wbec_pm_ctx; + +/* + * Size the EC recovery deadline from the pending RTC wakealarm. On WB8 the EC + * itself is rtc0 and holds the alarm, so the wake time is read straight from + * the EC over the regmap we already own. The returned timeout must place the + * EC's recovery deadline safely AFTER that alarm (deadline = timeout_s + ~10 s + * from the announce), so seconds-to-alarm gets a margin added on top -- a plain + * seconds-to-alarm would put the deadline right on the alarm and could pre-empt + * it (fatally so for the CRC-verify variant, whose ~75 s post-wake CRC would + * still be running). When no alarm is armed a long default backstop is used and + * the wake is the power button. + */ +static int wbec_suspend_deadline(struct wbec *wbec, u32 *timeout_s) +{ + struct rtc_time now = {0}; + struct rtc_time alarm; + time64_t now_s, alarm_s; + u32 margin; + u16 t[4], a[4]; + int ret; + + ret = regmap_bulk_read(wbec->regmap, WBEC_REG_RTC_ALARM_SECS_MINS, + a, ARRAY_SIZE(a)); + if (ret) + return ret; + + if (!(a[2] & WBEC_REG_RTC_ALARM_STATUS_EN_MSK)) { + *timeout_s = min(wbec_suspend_default_timeout_s, + WBEC_SUSPEND_TIMEOUT_MAX); + return 0; + } + + ret = regmap_bulk_read(wbec->regmap, WBEC_REG_RTC_TIME_SECS_MINS, + t, ARRAY_SIZE(t)); + if (ret) + return ret; + + /* Decode the EC time (same layout as rtc-wbec.c). */ + now.tm_sec = t[0] & 0x00ff; + now.tm_min = t[0] >> 8; + now.tm_hour = t[1] & 0x00ff; + now.tm_mday = t[1] >> 8; + now.tm_mon = (t[2] >> 8) - 1; + now.tm_year = t[3] + 100; + now_s = rtc_tm_to_time64(&now); + + /* + * The EC alarm carries only mday + h:m:s, so complete it with the + * current month/year and roll to the next month if that instant has + * already passed. This only sizes the recovery deadline, so the small + * error at month boundaries is harmless. + */ + alarm = now; + alarm.tm_sec = a[0] & 0x00ff; + alarm.tm_min = a[0] >> 8; + alarm.tm_hour = a[1] & 0x00ff; + alarm.tm_mday = a[1] >> 8; + alarm_s = rtc_tm_to_time64(&alarm); + if (alarm_s <= now_s) { + if (++alarm.tm_mon > 11) { + alarm.tm_mon = 0; + alarm.tm_year++; + } + alarm_s = rtc_tm_to_time64(&alarm); + } + + margin = wbec_suspend_crc_verify ? WBEC_SUSPEND_MARGIN_VERIFY_S + : WBEC_SUSPEND_MARGIN_FAST_S; + + if (alarm_s <= now_s) + *timeout_s = min(wbec_suspend_default_timeout_s, + WBEC_SUSPEND_TIMEOUT_MAX); + else + *timeout_s = clamp_t(u64, (alarm_s - now_s) + margin, + WBEC_SUSPEND_TIMEOUT_MIN, + WBEC_SUSPEND_TIMEOUT_MAX); + + return 0; +} + +/* Announce the power-off suspend window to the EC over the (sleeping) regmap. */ +static int wbec_suspend_announce(struct wbec *wbec) +{ + u32 timeout_s; + int ret; + + /* + * Ack the host-visible RTC-alarm IRQ latch (IRQ_FLAGS.RTC_ALARM) before + * the power-off cycle. In suspend-to-off the alarm wakes the board by + * pulsing the PMIC, not through a Linux IRQ, so nothing else acks this + * bit; clearing it keeps the resumed kernel's regmap-irq from seeing a + * stale pending alarm. It touches only the host-visible flag; the alarm + * arm bit and time in ALARM_STATUS are left intact so the pending + * wakealarm still fires. + * + * NB: this does NOT gate the EC's off-mode wake. That wake reads a + * separate EC-internal fired latch (the firmware alarm_fired_latch, + * sourced from the STM32G0 RTC ALRAF flag), which the host cannot reach + * over the regmap. A stale value there was what woke the board ~220 ms + * into off-mode; it is drained by the EC firmware on off-mode entry + * (wb-embedded-controller feature/ec-suspend-mode, "drain the stale RTC + * alarm latch on off-mode entry"). The timed rtcwake -m mem wake depends + * on that EC firmware, not on this write. + */ + ret = regmap_write(wbec->regmap, WBEC_REG_IRQ_CLEAR, + WBEC_REG_IRQ_RTC_ALARM_MSK); + if (ret) + return ret; + + ret = wbec_suspend_deadline(wbec, &timeout_s); + if (ret) { + dev_err(wbec->dev, "suspend: cannot read wake deadline: %d\n", + ret); + return ret; + } + + ret = regmap_write(wbec->regmap, WBEC_REG_SUSPEND_CTRL_TIMEOUT_S, + timeout_s); + if (ret) + return ret; + + ret = regmap_write(wbec->regmap, WBEC_REG_SUSPEND_CTRL_OFF_MODE, + WBEC_REG_SUSPEND_CTRL_OFF_MODE_EN_MSK); + if (ret) { + /* Do not leave a half-announced window behind. */ + regmap_write(wbec->regmap, WBEC_REG_SUSPEND_CTRL_TIMEOUT_S, 0); + return ret; + } + + dev_dbg(wbec->dev, "suspend-to-off announced, EC deadline %u s\n", + timeout_s); + return 0; +} + +static void wbec_suspend_deannounce(struct wbec *wbec) +{ + regmap_write(wbec->regmap, WBEC_REG_SUSPEND_CTRL_OFF_MODE, 0); + regmap_write(wbec->regmap, WBEC_REG_SUSPEND_CTRL_TIMEOUT_S, 0); +} + +/* + * Device suspend/resume (normal phase): the EC handshake talks over SPI, which + * may sleep, so it cannot live in a _noirq/syscore callback. The wbec SPI + * device suspends before its SPI controller (child before parent), so the bus + * is still live here. Only the deep, firmware-assisted path (PSCI + * SYSTEM_SUSPEND, i.e. PM_SUSPEND_MEM) drops the rails and needs the EC + * coordination; s2idle and hibernation leave pm_suspend_target_state alone and + * are skipped. + */ +static int wbec_suspend(struct device *dev) +{ + struct wbec *wbec = dev_get_drvdata(dev); + + if (!wbec->suspend_magic_reg) + return 0; + if (pm_suspend_target_state != PM_SUSPEND_MEM) + return 0; + + return wbec_suspend_announce(wbec); +} + +static int wbec_resume(struct device *dev) +{ + struct wbec *wbec = dev_get_drvdata(dev); + + if (!wbec->suspend_magic_reg) + return 0; + if (pm_suspend_target_state != PM_SUSPEND_MEM) + return 0; + + wbec_suspend_deannounce(wbec); + return 0; +} + +static const struct dev_pm_ops wbec_pm_ops = { + SYSTEM_SLEEP_PM_OPS(wbec_suspend, wbec_resume) +}; + +/* + * The BL31 magic is a bare RTC-data0 mmio poke that must land right before the + * PSCI SYSTEM_SUSPEND SMC and be gone on resume. syscore is the right phase: + * it runs with interrupts off on the boot CPU after every device has + * suspended, and crucially only for a genuine deep suspend (s2idle never + * reaches syscore_suspend), so writing the magic here cannot arm the firmware + * for a sleep that does not drop the rails. The explicit target-state check + * additionally rejects the hibernation syscore path. + */ +static int wbec_pm_syscore_suspend(void) +{ + struct wbec *wbec = wbec_pm_ctx; + + if (!wbec || !wbec->suspend_magic_reg) + return 0; + if (pm_suspend_target_state != PM_SUSPEND_MEM) + return 0; + + writel(wbec_suspend_crc_verify ? WBEC_SUSPEND_MAGIC_VERIFY + : WBEC_SUSPEND_MAGIC_FAST, + wbec->suspend_magic_reg); + return 0; +} + +static void wbec_pm_syscore_resume(void) +{ + struct wbec *wbec = wbec_pm_ctx; + + if (!wbec || !wbec->suspend_magic_reg) + return; + + /* BL31 clears the magic during suspend; clear again to cover an abort. */ + writel(0, wbec->suspend_magic_reg); +} + +static struct syscore_ops wbec_pm_syscore_ops = { + .suspend = wbec_pm_syscore_suspend, + .resume = wbec_pm_syscore_resume, +}; + +/* + * Ack a stale EC RTC-alarm IRQ at the very start of every suspend. + * + * The EC drives its INT line (a rising-edge wakeup GPIO, PE7) while + * IRQ_FLAGS.RTC_ALARM is set. When an off-mode alarm wakes the board that bit + * is left set, and nothing in the kernel ever acks it: the wbec IRQ is only + * wired for the UART, and the RTC alarm has no Linux IRQ (the off-mode wake is + * a PMIC pulse, not an IRQ). So after the first cycle the EC INT stays asserted + * and the kernel keeps counting it as a pending wakeup. On the *next* deep + * suspend that makes pm_wakeup_pending() abort suspend_enter() before the + * coordinator's off-cycle runs -- every rtcwake after the first fails, while a + * fresh boot (no pending EC IRQ) works. + * + * The ack has to land before the wakeup IRQ is armed and sampled. PM_SUSPEND_- + * PREPARE runs at the very start of a suspend (suspend_prepare(), before tasks + * freeze, before dpm_suspend and before suspend_device_irqs()/pm_wakeup_- + * pending()), so clearing here deasserts the line in time. The equivalent ack + * in the .suspend callback (dpm_suspend_start) runs too late relative to that + * check. Runs in process context with the SPI bus fully alive, so the regmap + * write is safe here. + */ +static int wbec_pm_notify(struct notifier_block *nb, unsigned long action, + void *data) +{ + struct wbec *wbec = wbec_pm_ctx; + + if (!wbec || !wbec->suspend_magic_reg) + return NOTIFY_DONE; + + if (action == PM_SUSPEND_PREPARE) { + regmap_write(wbec->regmap, WBEC_REG_IRQ_CLEAR, + WBEC_REG_IRQ_RTC_ALARM_MSK); + /* + * Drop any wakeup-abort state the stale EC IRQ already recorded, + * so a leftover from before this suspend request cannot pre-empt + * the enter path once the line itself is quiet. + */ + pm_wakeup_clear(0); + } + + return NOTIFY_DONE; +} + +static struct notifier_block wbec_pm_nb = { + .notifier_call = wbec_pm_notify, +}; + +/* + * Arm the coordinator when (and only when) the DT wires a suspend-magic RTC + * phandle, which is present only on WB8/H616. Everything downstream keys off + * wbec->suspend_magic_reg staying NULL on other boards. + */ +static int wbec_suspend_setup(struct wbec *wbec) +{ + struct device_node *rtc_np; + struct resource res; + int ret; + + rtc_np = of_parse_phandle(wbec->dev->of_node, + "wirenboard,suspend-magic-rtc", 0); + if (!rtc_np) + return 0; + + ret = of_address_to_resource(rtc_np, 0, &res); + of_node_put(rtc_np); + if (ret) + return dev_err_probe(wbec->dev, ret, + "bad wirenboard,suspend-magic-rtc\n"); + + wbec->suspend_magic_reg = devm_ioremap(wbec->dev, + res.start + WBEC_SUSPEND_MAGIC_RTC_OFFSET, + sizeof(u32)); + if (!wbec->suspend_magic_reg) + return -ENOMEM; + + wbec_pm_ctx = wbec; + register_syscore_ops(&wbec_pm_syscore_ops); + register_pm_notifier(&wbec_pm_nb); + + dev_info(wbec->dev, "suspend-to-off coordinator armed\n"); + return 0; +} + +static void wbec_suspend_teardown(struct wbec *wbec) +{ + if (!wbec->suspend_magic_reg) + return; + + unregister_pm_notifier(&wbec_pm_nb); + unregister_syscore_ops(&wbec_pm_syscore_ops); + wbec_pm_ctx = NULL; +} + +/* ----------------------------------------------------------------------- */ + static int wbec_probe(struct spi_device *spi) { struct wbec *wbec; @@ -361,13 +735,7 @@ static int wbec_probe(struct spi_device *spi) ret = wbec_check_present(wbec); if (ret == -ENODEV) { - /* - * Expected on every board that speaks the v1 protocol (the v2 - * pad-word protocol is a newer-EC feature): probe v2 first, then - * fall back to v1. Keep this at debug level so a normal v1 board - * does not log a scary "not found" line during ordinary probing. - */ - dev_dbg(wbec->dev, "no WBEC on v2 protocol, falling back to v1\n"); + dev_info(wbec->dev, "WBEC not found with v2 protocol, trying v1\n"); /* don't worry about memory leak, previous regmap will be freed by devm */ wbec->regmap = devm_regmap_init_spi(spi, &wbec_regmap_config_v1); @@ -425,17 +793,22 @@ static int wbec_probe(struct spi_device *spi) SYS_OFF_PRIO_FIRMWARE, wbec_restart, wbec); + ret = wbec_suspend_setup(wbec); + if (ret) + return ret; + wbec_setup_debugfs(wbec); dev_info(&spi->dev, "WBEC device added\n"); - return ret; + return 0; } static void wbec_remove(struct spi_device *spi) { struct wbec *wbec = spi_get_drvdata(spi); + wbec_suspend_teardown(wbec); wbec_clean_debugfs(wbec); } @@ -455,6 +828,7 @@ static struct spi_driver wbec_driver = { .driver = { .name = "wbec", .of_match_table = wbec_of_match, + .pm = pm_sleep_ptr(&wbec_pm_ops), }, .probe = wbec_probe, .remove = wbec_remove, diff --git a/include/linux/mfd/wbec.h b/include/linux/mfd/wbec.h index d29956089cdb..46278e06258a 100644 --- a/include/linux/mfd/wbec.h +++ b/include/linux/mfd/wbec.h @@ -73,6 +73,21 @@ #define WBEC_REG_POWER_CTRL_OFF_MSK BIT(0) #define WBEC_REG_POWER_CTRL_REBOOT_MSK BIT(1) +/* + * Region SUSPEND_CTRL: RW + * + * EC-coordinated suspend-to-off window (EC firmware branch + * feature/ec-suspend-mode). Before a power-off suspend the SoC announces the + * sleep window here: TIMEOUT_S sizes the EC recovery deadline (the EC warm- + * resets the board timeout+10 s after the announce if it never sees a resume), + * and OFF_MODE tells the EC that the whole SoC (VDD-SYS, 3V3) is going dark so + * it masks its "3V3 lost / PMIC died" detector and swaps the watchdog for the + * deadline. The EC wakes the board at its own RTC alarm or the power button. + */ +#define WBEC_REG_SUSPEND_CTRL_TIMEOUT_S 0xA4 +#define WBEC_REG_SUSPEND_CTRL_OFF_MODE 0xA5 + #define WBEC_REG_SUSPEND_CTRL_OFF_MODE_EN_MSK BIT(0) + /* Region IRQ_FLAGS: RW */ #define WBEC_REG_IRQ_FLAGS 0xB0 #define WBEC_REG_IRQ_MSK 0xB2 @@ -113,6 +128,14 @@ struct wbec { struct dentry *wbec_dir; void (*irq_handler)(struct wbec *wbec); bool support_v2_protocol; + + /* + * Suspend-to-off coordinator (WB8/H616 only, DT-gated). When armed, + * @suspend_magic_reg is the ioremap of the sun6i RTC data0 register the + * platform firmware (BL31) reads to select suspend-to-off; NULL means + * the board does not use EC-coordinated power-off suspend. + */ + void __iomem *suspend_magic_reg; }; #endif From 4d58ff65338e7d77adafbab04cc9b9fd2de17830 Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Fri, 10 Jul 2026 17:25:29 +0300 Subject: [PATCH 10/11] arm64: dts: allwinner: wirenboard85x: enable the suspend-to-off coordinator Wire wirenboard,suspend-magic-rtc to the sun6i RTC so the wbec driver arms the firmware suspend-to-off magic on WB8.5 boards. Signed-off-by: Evgeny Boger Co-authored-by: Claude Fable 5 --- .../boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi b/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi index 38c56bc06b42..8009d3f3c70b 100644 --- a/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi +++ b/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi @@ -904,6 +904,14 @@ interrupt-parent = <&pio>; interrupts = ; + /* + * Arm the in-kernel suspend-to-off coordinator: the phandle + * points at the sun6i RTC whose data0 register carries the + * BL31 suspend-to-off magic. Its presence gates the feature, + * so only WB8/H616 (this SoC) enables it. + */ + wirenboard,suspend-magic-rtc = <&rtc>; + wbec_watchdog: wbec-watchdog@0 { compatible = "wirenboard,wbec-watchdog"; reg = <0>; From 5a5135470142078f9ed099da34e737c1dc9e1fdb Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Fri, 10 Jul 2026 17:25:52 +0300 Subject: [PATCH 11/11] arm64: dts: allwinner: wirenboard85x: park the CPU at a boot-safe OPP across suspend On suspend-to-off resume, U-Boot SPL's board init runs before the resume branch and programs VDD-CPU to its 0.90 V cold-boot value; BL31 then re-locks PLL_CPUX to the frequency the kernel parked at. Under schedutil the suspend-entry work (fs sync, freezing) spikes the load, so the kernel routinely parks at a 1.00-1.10 V OPP (up to 1416 MHz), leaving the cluster up to 200 mV under-volted until cpufreq's resume rewrites the regulator ~2 s later. The resulting fetch/load transients during secondary bringup were a whole family of post-resume corruption (fetch faults on valid code words, single-bit pointer flips, bringup lockups, always within ~2 s of resume, with retained-DRAM CRC clean on every failing cycle). Mark the 480 MHz OPP as the suspend OPP: cpufreq_generic_suspend() parks there before the PSCI call. 480 MHz is rated 0.90 V and present on every speed bin (opp-supported-hw 0x1f), so the parked frequency is safe at the SPL boot voltage even if the firmware-side fix is absent or the boot voltage changes. Costs a few hundred ms of resume latency until the governor revives. Signed-off-by: Evgeny Boger Co-authored-by: Claude Fable 5 --- .../allwinner/sun50i-h616-wirenboard85x.dtsi | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi b/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi index 8009d3f3c70b..5efc16d34532 100644 --- a/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi +++ b/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi @@ -1685,3 +1685,20 @@ &iommu { status = "okay"; }; + +/* + * Suspend-to-off contract with the boot firmware: on resume, U-Boot + * SPL's board init runs before the resume branch and programs VDD-CPU + * to its 0.90 V cold-boot value (CONFIG_AXP_DCDC2_VOLT), and BL31 then + * restores the PLL frequency the kernel parked at. The OPP the kernel + * parks at across suspend must therefore be safe at 0.90 V on every + * speed bin. 480 MHz is 0.90 V-rated and bin-universal + * (opp-supported-hw 0x1f). The primary fix lives in BL31 (it restores + * the recorded voltage before the recorded frequency); this is the + * kernel-side belt in case firmware and kernel ever skew. + */ +&cpu_opp_table { + opp-480000000 { + opp-suspend; + }; +};