Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Documentation/devicetree/bindings/mfd/wirenboard,wbec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,14 @@
interrupt-parent = <&pio>;
interrupts = <PE 7 IRQ_TYPE_EDGE_RISING>;

/*
* 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>;
Expand Down Expand Up @@ -1677,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;
};
};
25 changes: 25 additions & 0 deletions drivers/clk/sunxi-ng/ccu-sun50i-h6-r.c
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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)
Expand Down
57 changes: 57 additions & 0 deletions drivers/clk/sunxi-ng/ccu-sun50i-h616.c
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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[] = {
Expand Down
183 changes: 183 additions & 0 deletions drivers/clk/sunxi-ng/ccu_common.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@

#include <linux/clk.h>
#include <linux/clk-provider.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/iopoll.h>
#include <linux/list.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/syscore_ops.h>

#include "ccu_common.h"
#include "ccu_gate.h"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
Loading