summaryrefslogtreecommitdiffstats
path: root/drivers/pmdomain/sunxi
diff options
context:
space:
mode:
authorJinke Han <jinkehan@didiglobal.com>2026-09-08 15:37:42 +0800
committerIngo Molnar <mingo@kernel.org>2026-09-17 09:48:42 +0200
commita5f7a5bb3b7f28ba7e4fa246775b29a0e5537255 (patch)
tree73fef824f9a846cf935869d0dbc5070ef6e2f741 /drivers/pmdomain/sunxi
downloadlinux-stable-a5f7a5bb3b7f28ba7e4fa246775b29a0e5537255.tar.gz
linux-stable-a5f7a5bb3b7f28ba7e4fa246775b29a0e5537255.zip
x86/kprobes: Fix crash when probing CS CALL instructionsgrafted
When using eBPF to probe CS CALL instructions within a function, a crash can be triggered. The eBPF tool probes offset 257 of the __hrtimer_run_queues() function: <__hrtimer_run_queues+249>: nopl 0x0(%rax,%rax,1) <__hrtimer_run_queues+254>: mov %r14,%rdi <__hrtimer_run_queues+257>: cs call <__x86_indirect_thunk_r12> <__hrtimer_run_queues+263>: mov %eax,%r12d <__hrtimer_run_queues+266>: xchg %ax,%ax <__hrtimer_run_queues+268>: mov %r13,%rdi Which triggers this crash: BUG: unable to handle page fault for address: 00000000000f41c9 #PF: supervisor write access in kernel mode #PF: error_code(0x0002) - not-present page PGD 0 P4D 0 Oops: 0002 [#1] SMP NOPTI CPU: 1 PID: 0 Comm: swapper/1 Kdump: loaded Tainted: P RIP: 0010:__hrtimer_run_queues+0x106/0x230 Note that __hrtimer_run_queues+0x106 is __hrtimer_run_queues+262, which is at the 6th byte of the above CS CALL instruction. Since the CS CALL instruction occupies 6 bytes, the exception occurred in the middle of that call instruction. The root cause is that when using eBPF tools to probe in the middle of a function, a kprobe with INT3 is used as the underlying implementation. During single-step emulation of the original CALL instruction, int3_emulate_call() assumes that the probed CALL instruction is 5 bytes long. However, the actual CS-prefixed CALL instruction occupies 6 bytes, so it constructs an incorrect exception return address. When the CPU returns from the kprobe handler, the next instruction to be executed is at the address of the last byte of that CS CALL instruction. Coincidentally, starting from that address, the CPU fetches and decodes a completely different instruction, which ultimately triggers a kernel crash. Fix the issue by using the actual instruction length obtained from the instruction decoder when constructing the exception return address, rather than relying on the hardcoded CALL_INSN_SIZE macro. [ mingo: Refined the changelog ] Fixes: 6256e668b7af ("x86/kprobes: Use int3 instead of debug trap for single-step") Suggested-by: Masami Hiramatsu (Google) <mhiramat@kernel.org> Signed-off-by: Jinke Han <jinkehan@didiglobal.com> Signed-off-by: Ingo Molnar <mingo@kernel.org> Reviewed-by: Masami Hiramatsu (Google) <mhiramat@kernel.org> Acked-by: Yafang Shao <laoar.shao@gmail.com> Acked-by: Borislav Petkov <bp@alien8.de> Cc: Peter Zijlstra <peterz@infradead.org> Link: https://patch.msgid.link/20260908073742.GA10517@didi-ThinkCentre-M920t-N000
Diffstat (limited to 'drivers/pmdomain/sunxi')
-rw-r--r--drivers/pmdomain/sunxi/Kconfig33
-rw-r--r--drivers/pmdomain/sunxi/Makefile4
-rw-r--r--drivers/pmdomain/sunxi/sun20i-ppu.c239
-rw-r--r--drivers/pmdomain/sunxi/sun50i-h6-prcm-ppu.c208
-rw-r--r--drivers/pmdomain/sunxi/sun55i-pck600.c270
5 files changed, 754 insertions, 0 deletions
diff --git a/drivers/pmdomain/sunxi/Kconfig b/drivers/pmdomain/sunxi/Kconfig
new file mode 100644
index 000000000..858446594
--- /dev/null
+++ b/drivers/pmdomain/sunxi/Kconfig
@@ -0,0 +1,33 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+config SUN20I_PPU
+ tristate "Allwinner D1 PPU power domain driver"
+ depends on ARCH_SUNXI || COMPILE_TEST
+ depends on PM
+ default ARCH_SUNXI
+ select PM_GENERIC_DOMAINS
+ help
+ Say y to enable the PPU power domain driver. This is required
+ to enable power to certain peripherals, such as the display
+ engine.
+
+config SUN50I_H6_PRCM_PPU
+ tristate "Allwinner H6 PRCM power domain driver"
+ depends on ARCH_SUNXI || COMPILE_TEST
+ depends on PM
+ select PM_GENERIC_DOMAINS
+ help
+ Say y to enable the Allwinner H6/H616 PRCM power domain driver.
+ This is required to enable the Mali GPU in the H616 SoC, it is
+ optional for the H6.
+
+config SUN55I_PCK600
+ tristate "Allwinner A523 PCK-600 power domain driver"
+ depends on ARCH_SUNXI || COMPILE_TEST
+ depends on PM
+ default ARCH_SUNXI
+ select PM_GENERIC_DOMAINS
+ help
+ Say y to enable the PCK-600 power domain driver. This is required
+ to enable power to certain peripherals, such as the display and
+ video engines.
diff --git a/drivers/pmdomain/sunxi/Makefile b/drivers/pmdomain/sunxi/Makefile
new file mode 100644
index 000000000..e344b232f
--- /dev/null
+++ b/drivers/pmdomain/sunxi/Makefile
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-only
+obj-$(CONFIG_SUN20I_PPU) += sun20i-ppu.o
+obj-$(CONFIG_SUN50I_H6_PRCM_PPU) += sun50i-h6-prcm-ppu.o
+obj-$(CONFIG_SUN55I_PCK600) += sun55i-pck600.o
diff --git a/drivers/pmdomain/sunxi/sun20i-ppu.c b/drivers/pmdomain/sunxi/sun20i-ppu.c
new file mode 100644
index 000000000..b65876a68
--- /dev/null
+++ b/drivers/pmdomain/sunxi/sun20i-ppu.c
@@ -0,0 +1,239 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/pm_domain.h>
+#include <linux/reset.h>
+
+#define PD_STATE_ON 1
+#define PD_STATE_OFF 2
+
+#define PD_RSTN_REG 0x00
+#define PD_CLK_GATE_REG 0x04
+#define PD_PWROFF_GATE_REG 0x08
+#define PD_PSW_ON_REG 0x0c
+#define PD_PSW_OFF_REG 0x10
+#define PD_PSW_DELAY_REG 0x14
+#define PD_OFF_DELAY_REG 0x18
+#define PD_ON_DELAY_REG 0x1c
+#define PD_COMMAND_REG 0x20
+#define PD_STATUS_REG 0x24
+#define PD_STATUS_COMPLETE BIT(1)
+#define PD_STATUS_BUSY BIT(3)
+#define PD_STATUS_STATE GENMASK(17, 16)
+#define PD_ACTIVE_CTRL_REG 0x2c
+#define PD_GATE_STATUS_REG 0x30
+#define PD_RSTN_STATUS BIT(0)
+#define PD_CLK_GATE_STATUS BIT(1)
+#define PD_PWROFF_GATE_STATUS BIT(2)
+#define PD_PSW_STATUS_REG 0x34
+
+#define PD_REGS_SIZE 0x80
+
+struct sun20i_ppu_desc {
+ const char *const *names;
+ unsigned int num_domains;
+};
+
+struct sun20i_ppu_pd {
+ struct generic_pm_domain genpd;
+ void __iomem *base;
+};
+
+#define to_sun20i_ppu_pd(_genpd) \
+ container_of(_genpd, struct sun20i_ppu_pd, genpd)
+
+static bool sun20i_ppu_pd_is_on(const struct sun20i_ppu_pd *pd)
+{
+ u32 status = readl(pd->base + PD_STATUS_REG);
+
+ return FIELD_GET(PD_STATUS_STATE, status) == PD_STATE_ON;
+}
+
+static int sun20i_ppu_pd_set_power(const struct sun20i_ppu_pd *pd, bool power_on)
+{
+ u32 state, status;
+ int ret;
+
+ if (sun20i_ppu_pd_is_on(pd) == power_on)
+ return 0;
+
+ /* Wait for the power controller to be idle. */
+ ret = readl_poll_timeout(pd->base + PD_STATUS_REG, status,
+ !(status & PD_STATUS_BUSY), 100, 1000);
+ if (ret)
+ return ret;
+
+ state = power_on ? PD_STATE_ON : PD_STATE_OFF;
+ writel(state, pd->base + PD_COMMAND_REG);
+
+ /* Wait for the state transition to complete. */
+ ret = readl_poll_timeout(pd->base + PD_STATUS_REG, status,
+ FIELD_GET(PD_STATUS_STATE, status) == state &&
+ (status & PD_STATUS_COMPLETE), 100, 1000);
+ if (ret)
+ return ret;
+
+ /* Clear the completion flag. */
+ writel(status, pd->base + PD_STATUS_REG);
+
+ return 0;
+}
+
+static int sun20i_ppu_pd_power_on(struct generic_pm_domain *genpd)
+{
+ const struct sun20i_ppu_pd *pd = to_sun20i_ppu_pd(genpd);
+
+ return sun20i_ppu_pd_set_power(pd, true);
+}
+
+static int sun20i_ppu_pd_power_off(struct generic_pm_domain *genpd)
+{
+ const struct sun20i_ppu_pd *pd = to_sun20i_ppu_pd(genpd);
+
+ return sun20i_ppu_pd_set_power(pd, false);
+}
+
+static int sun20i_ppu_probe(struct platform_device *pdev)
+{
+ const struct sun20i_ppu_desc *desc;
+ struct device *dev = &pdev->dev;
+ struct genpd_onecell_data *ppu;
+ struct sun20i_ppu_pd *pds;
+ struct reset_control *rst;
+ void __iomem *base;
+ struct clk *clk;
+ int ret;
+
+ desc = of_device_get_match_data(dev);
+ if (!desc)
+ return -EINVAL;
+
+ pds = devm_kcalloc(dev, desc->num_domains, sizeof(*pds), GFP_KERNEL);
+ if (!pds)
+ return -ENOMEM;
+
+ ppu = devm_kzalloc(dev, sizeof(*ppu), GFP_KERNEL);
+ if (!ppu)
+ return -ENOMEM;
+
+ ppu->domains = devm_kcalloc(dev, desc->num_domains,
+ sizeof(*ppu->domains), GFP_KERNEL);
+ if (!ppu->domains)
+ return -ENOMEM;
+
+ ppu->num_domains = desc->num_domains;
+ platform_set_drvdata(pdev, ppu);
+
+ base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(base))
+ return PTR_ERR(base);
+
+ clk = devm_clk_get_enabled(dev, NULL);
+ if (IS_ERR(clk))
+ return PTR_ERR(clk);
+
+ rst = devm_reset_control_get_exclusive(dev, NULL);
+ if (IS_ERR(rst))
+ return PTR_ERR(rst);
+
+ ret = reset_control_deassert(rst);
+ if (ret)
+ return ret;
+
+ for (unsigned int i = 0; i < ppu->num_domains; ++i) {
+ struct sun20i_ppu_pd *pd = &pds[i];
+
+ pd->genpd.name = desc->names[i];
+ pd->genpd.power_off = sun20i_ppu_pd_power_off;
+ pd->genpd.power_on = sun20i_ppu_pd_power_on;
+ pd->base = base + PD_REGS_SIZE * i;
+
+ ret = pm_genpd_init(&pd->genpd, NULL, sun20i_ppu_pd_is_on(pd));
+ if (ret) {
+ dev_warn(dev, "Failed to add '%s' domain: %d\n",
+ pd->genpd.name, ret);
+ continue;
+ }
+
+ ppu->domains[i] = &pd->genpd;
+ }
+
+ ret = of_genpd_add_provider_onecell(dev->of_node, ppu);
+ if (ret)
+ dev_warn(dev, "Failed to add provider: %d\n", ret);
+
+ return 0;
+}
+
+static const char *const sun20i_d1_ppu_pd_names[] = {
+ "CPU",
+ "VE",
+ "DSP",
+};
+
+static const struct sun20i_ppu_desc sun20i_d1_ppu_desc = {
+ .names = sun20i_d1_ppu_pd_names,
+ .num_domains = ARRAY_SIZE(sun20i_d1_ppu_pd_names),
+};
+
+static const char *const sun8i_v853_ppu_pd_names[] = {
+ "RISCV",
+ "NPU",
+ "VE",
+};
+
+static const struct sun20i_ppu_desc sun8i_v853_ppu_desc = {
+ .names = sun8i_v853_ppu_pd_names,
+ .num_domains = ARRAY_SIZE(sun8i_v853_ppu_pd_names),
+};
+
+static const char *const sun55i_a523_ppu_pd_names[] = {
+ "DSP",
+ "NPU",
+ "AUDIO",
+ "SRAM",
+ "RISCV",
+};
+
+static const struct sun20i_ppu_desc sun55i_a523_ppu_desc = {
+ .names = sun55i_a523_ppu_pd_names,
+ .num_domains = ARRAY_SIZE(sun55i_a523_ppu_pd_names),
+};
+
+static const struct of_device_id sun20i_ppu_of_match[] = {
+ {
+ .compatible = "allwinner,sun20i-d1-ppu",
+ .data = &sun20i_d1_ppu_desc,
+ },
+ {
+ .compatible = "allwinner,sun8i-v853-ppu",
+ .data = &sun8i_v853_ppu_desc,
+ },
+ {
+ .compatible = "allwinner,sun55i-a523-ppu",
+ .data = &sun55i_a523_ppu_desc,
+ },
+ { }
+};
+MODULE_DEVICE_TABLE(of, sun20i_ppu_of_match);
+
+static struct platform_driver sun20i_ppu_driver = {
+ .probe = sun20i_ppu_probe,
+ .driver = {
+ .name = "sun20i-ppu",
+ .of_match_table = sun20i_ppu_of_match,
+ /* Power domains cannot be removed while they are in use. */
+ .suppress_bind_attrs = true,
+ },
+};
+module_platform_driver(sun20i_ppu_driver);
+
+MODULE_AUTHOR("Samuel Holland <samuel@sholland.org>");
+MODULE_DESCRIPTION("Allwinner D1 PPU power domain driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/pmdomain/sunxi/sun50i-h6-prcm-ppu.c b/drivers/pmdomain/sunxi/sun50i-h6-prcm-ppu.c
new file mode 100644
index 000000000..d59644499
--- /dev/null
+++ b/drivers/pmdomain/sunxi/sun50i-h6-prcm-ppu.c
@@ -0,0 +1,208 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) Arm Ltd. 2024
+ *
+ * Allwinner H6/H616 PRCM power domain driver.
+ * This covers a few registers inside the PRCM (Power Reset Clock Management)
+ * block that control some power rails, most prominently for the Mali GPU.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/pm_domain.h>
+#include <linux/reset.h>
+
+/*
+ * The PRCM block covers multiple devices, starting with some clocks,
+ * then followed by the power rails.
+ * The clocks are covered by a different driver, so this driver's MMIO range
+ * starts later in the PRCM MMIO frame, not at the beginning of it.
+ * To keep the register offsets consistent with other PRCM documentation,
+ * express the registers relative to the beginning of the whole PRCM, and
+ * subtract the PPU offset this driver is bound to.
+ */
+#define PD_H6_PPU_OFFSET 0x250
+#define PD_H6_VDD_SYS_REG 0x250
+#define PD_H616_ANA_VDD_GATE BIT(4)
+#define PD_H6_CPUS_VDD_GATE BIT(3)
+#define PD_H6_AVCC_VDD_GATE BIT(2)
+#define PD_H6_GPU_REG 0x254
+#define PD_H6_GPU_GATE BIT(0)
+
+struct sun50i_h6_ppu_pd {
+ struct generic_pm_domain genpd;
+ void __iomem *reg;
+ u32 gate_mask;
+ bool negated;
+};
+
+#define FLAG_PPU_ALWAYS_ON BIT(0)
+#define FLAG_PPU_NEGATED BIT(1)
+
+struct sun50i_h6_ppu_desc {
+ const char *name;
+ u32 offset;
+ u32 mask;
+ unsigned int flags;
+};
+
+static const struct sun50i_h6_ppu_desc sun50i_h6_ppus[] = {
+ { "AVCC", PD_H6_VDD_SYS_REG, PD_H6_AVCC_VDD_GATE },
+ { "CPUS", PD_H6_VDD_SYS_REG, PD_H6_CPUS_VDD_GATE },
+ { "GPU", PD_H6_GPU_REG, PD_H6_GPU_GATE },
+};
+static const struct sun50i_h6_ppu_desc sun50i_h616_ppus[] = {
+ { "PLL", PD_H6_VDD_SYS_REG, PD_H6_AVCC_VDD_GATE,
+ FLAG_PPU_ALWAYS_ON | FLAG_PPU_NEGATED },
+ { "ANA", PD_H6_VDD_SYS_REG, PD_H616_ANA_VDD_GATE, FLAG_PPU_ALWAYS_ON },
+ { "GPU", PD_H6_GPU_REG, PD_H6_GPU_GATE, FLAG_PPU_NEGATED },
+};
+
+struct sun50i_h6_ppu_data {
+ const struct sun50i_h6_ppu_desc *descs;
+ int nr_domains;
+};
+
+static const struct sun50i_h6_ppu_data sun50i_h6_ppu_data = {
+ .descs = sun50i_h6_ppus,
+ .nr_domains = ARRAY_SIZE(sun50i_h6_ppus),
+};
+
+static const struct sun50i_h6_ppu_data sun50i_h616_ppu_data = {
+ .descs = sun50i_h616_ppus,
+ .nr_domains = ARRAY_SIZE(sun50i_h616_ppus),
+};
+
+#define to_sun50i_h6_ppu_pd(_genpd) \
+ container_of(_genpd, struct sun50i_h6_ppu_pd, genpd)
+
+static bool sun50i_h6_ppu_power_status(const struct sun50i_h6_ppu_pd *pd)
+{
+ bool bit = readl(pd->reg) & pd->gate_mask;
+
+ return bit ^ pd->negated;
+}
+
+static int sun50i_h6_ppu_pd_set_power(const struct sun50i_h6_ppu_pd *pd,
+ bool set_bit)
+{
+ u32 reg = readl(pd->reg);
+
+ if (set_bit)
+ writel(reg | pd->gate_mask, pd->reg);
+ else
+ writel(reg & ~pd->gate_mask, pd->reg);
+
+ return 0;
+}
+
+static int sun50i_h6_ppu_pd_power_on(struct generic_pm_domain *genpd)
+{
+ const struct sun50i_h6_ppu_pd *pd = to_sun50i_h6_ppu_pd(genpd);
+
+ return sun50i_h6_ppu_pd_set_power(pd, !pd->negated);
+}
+
+static int sun50i_h6_ppu_pd_power_off(struct generic_pm_domain *genpd)
+{
+ const struct sun50i_h6_ppu_pd *pd = to_sun50i_h6_ppu_pd(genpd);
+
+ return sun50i_h6_ppu_pd_set_power(pd, pd->negated);
+}
+
+static int sun50i_h6_ppu_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct genpd_onecell_data *ppu;
+ struct sun50i_h6_ppu_pd *pds;
+ const struct sun50i_h6_ppu_data *data;
+ void __iomem *base;
+ int ret, i;
+
+ data = of_device_get_match_data(dev);
+ if (!data)
+ return -EINVAL;
+
+ pds = devm_kcalloc(dev, data->nr_domains, sizeof(*pds), GFP_KERNEL);
+ if (!pds)
+ return -ENOMEM;
+
+ ppu = devm_kzalloc(dev, sizeof(*ppu), GFP_KERNEL);
+ if (!ppu)
+ return -ENOMEM;
+
+ ppu->num_domains = data->nr_domains;
+ ppu->domains = devm_kcalloc(dev, data->nr_domains,
+ sizeof(*ppu->domains), GFP_KERNEL);
+ if (!ppu->domains)
+ return -ENOMEM;
+
+ platform_set_drvdata(pdev, ppu);
+
+ base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(base))
+ return PTR_ERR(base);
+
+ for (i = 0; i < data->nr_domains; i++) {
+ struct sun50i_h6_ppu_pd *pd = &pds[i];
+ const struct sun50i_h6_ppu_desc *desc = &data->descs[i];
+
+ pd->genpd.name = desc->name;
+ pd->genpd.power_off = sun50i_h6_ppu_pd_power_off;
+ pd->genpd.power_on = sun50i_h6_ppu_pd_power_on;
+ if (desc->flags & FLAG_PPU_ALWAYS_ON)
+ pd->genpd.flags = GENPD_FLAG_ALWAYS_ON;
+ pd->negated = !!(desc->flags & FLAG_PPU_NEGATED);
+ pd->reg = base + desc->offset - PD_H6_PPU_OFFSET;
+ pd->gate_mask = desc->mask;
+
+ ret = pm_genpd_init(&pd->genpd, NULL,
+ !sun50i_h6_ppu_power_status(pd));
+ if (ret) {
+ dev_warn(dev, "Failed to add %s power domain: %d\n",
+ desc->name, ret);
+ goto out_remove_pds;
+ }
+ ppu->domains[i] = &pd->genpd;
+ }
+
+ ret = of_genpd_add_provider_onecell(dev->of_node, ppu);
+ if (!ret)
+ return 0;
+
+ dev_warn(dev, "Failed to add provider: %d\n", ret);
+out_remove_pds:
+ for (i--; i >= 0; i--)
+ pm_genpd_remove(&pds[i].genpd);
+
+ return ret;
+}
+
+static const struct of_device_id sun50i_h6_ppu_of_match[] = {
+ { .compatible = "allwinner,sun50i-h6-prcm-ppu",
+ .data = &sun50i_h6_ppu_data },
+ { .compatible = "allwinner,sun50i-h616-prcm-ppu",
+ .data = &sun50i_h616_ppu_data },
+ { }
+};
+MODULE_DEVICE_TABLE(of, sun50i_h6_ppu_of_match);
+
+static struct platform_driver sun50i_h6_ppu_driver = {
+ .probe = sun50i_h6_ppu_probe,
+ .driver = {
+ .name = "sun50i-h6-prcm-ppu",
+ .of_match_table = sun50i_h6_ppu_of_match,
+ /* Power domains cannot be removed while they are in use. */
+ .suppress_bind_attrs = true,
+ },
+};
+module_platform_driver(sun50i_h6_ppu_driver);
+
+MODULE_AUTHOR("Andre Przywara <andre.przywara@arm.com>");
+MODULE_DESCRIPTION("Allwinner H6 PRCM power domain driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/pmdomain/sunxi/sun55i-pck600.c b/drivers/pmdomain/sunxi/sun55i-pck600.c
new file mode 100644
index 000000000..8a37d11b6
--- /dev/null
+++ b/drivers/pmdomain/sunxi/sun55i-pck600.c
@@ -0,0 +1,270 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Allwinner PCK-600 power domain support
+ *
+ * Copyright (c) 2025 Chen-Yu Tsai <wens@csie.org>
+ *
+ * The hardware is likely based on the Arm PCK-600 IP, since some of
+ * the registers match Arm's documents, with additional delay controls
+ * that are in registers listed as reserved.
+ *
+ * Documents include:
+ * - "Arm CoreLink PCK-600 Power Control Kit" TRM
+ * - "Arm Power Policy Unit" architecture specification (DEN0051E)
+ */
+
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/container_of.h>
+#include <linux/device.h>
+#include <linux/dev_printk.h>
+#include <linux/err.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/pm_domain.h>
+#include <linux/reset.h>
+#include <linux/slab.h>
+#include <linux/string_choices.h>
+
+#define PPU_PWPR 0x0
+#define PPU_PWSR 0x8
+#define PPU_DCDR0 0x170
+#define PPU_DCDR1 0x174
+
+/* shared definition for PPU_PWPR and PPU_PWSR */
+#define PPU_PWR_STATUS GENMASK(3, 0)
+#define PPU_POWER_MODE_ON 0x8
+#define PPU_POWER_MODE_OFF 0x0
+
+#define PPU_REG_SIZE 0x1000
+
+struct sunxi_pck600_pd_desc {
+ const char *name;
+ unsigned int flags;
+};
+
+struct sunxi_pck600_desc {
+ const struct sunxi_pck600_pd_desc *pd_descs;
+ unsigned int num_domains;
+ u32 logic_power_switch0_delay_offset;
+ u32 logic_power_switch1_delay_offset;
+ u32 off2on_delay_offset;
+ u32 device_ctrl0_delay;
+ u32 device_ctrl1_delay;
+ u32 logic_power_switch0_delay;
+ u32 logic_power_switch1_delay;
+ u32 off2on_delay;
+ bool has_rst_clk;
+};
+
+struct sunxi_pck600_pd {
+ struct generic_pm_domain genpd;
+ struct sunxi_pck600 *pck;
+ void __iomem *base;
+};
+
+struct sunxi_pck600 {
+ struct device *dev;
+ struct genpd_onecell_data genpd_data;
+ struct sunxi_pck600_pd pds[];
+};
+
+#define to_sunxi_pd(gpd) container_of(gpd, struct sunxi_pck600_pd, genpd)
+
+static int sunxi_pck600_pd_set_power(struct sunxi_pck600_pd *pd, bool on)
+{
+ struct sunxi_pck600 *pck = pd->pck;
+ struct generic_pm_domain *genpd = &pd->genpd;
+ int ret;
+ u32 val, reg;
+
+ val = on ? PPU_POWER_MODE_ON : PPU_POWER_MODE_OFF;
+
+ reg = readl(pd->base + PPU_PWPR);
+ FIELD_MODIFY(PPU_PWR_STATUS, &reg, val);
+ writel(reg, pd->base + PPU_PWPR);
+
+ /* push write out to hardware */
+ reg = readl(pd->base + PPU_PWPR);
+
+ ret = readl_poll_timeout_atomic(pd->base + PPU_PWSR, reg,
+ FIELD_GET(PPU_PWR_STATUS, reg) == val,
+ 0, 10000);
+ if (ret)
+ dev_err(pck->dev, "failed to turn domain \"%s\" %s: %d\n",
+ genpd->name, str_on_off(on), ret);
+
+ return ret;
+}
+
+static int sunxi_pck600_power_on(struct generic_pm_domain *domain)
+{
+ struct sunxi_pck600_pd *pd = to_sunxi_pd(domain);
+
+ return sunxi_pck600_pd_set_power(pd, true);
+}
+
+static int sunxi_pck600_power_off(struct generic_pm_domain *domain)
+{
+ struct sunxi_pck600_pd *pd = to_sunxi_pd(domain);
+
+ return sunxi_pck600_pd_set_power(pd, false);
+}
+
+static void sunxi_pck600_pd_setup(struct sunxi_pck600_pd *pd,
+ const struct sunxi_pck600_desc *desc)
+{
+ writel(desc->device_ctrl0_delay, pd->base + PPU_DCDR0);
+ writel(desc->device_ctrl1_delay, pd->base + PPU_DCDR1);
+ writel(desc->logic_power_switch0_delay,
+ pd->base + desc->logic_power_switch0_delay_offset);
+ writel(desc->logic_power_switch1_delay,
+ pd->base + desc->logic_power_switch1_delay_offset);
+ writel(desc->off2on_delay, pd->base + desc->off2on_delay_offset);
+}
+
+static int sunxi_pck600_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ const struct sunxi_pck600_desc *desc;
+ struct genpd_onecell_data *genpds;
+ struct sunxi_pck600 *pck;
+ struct reset_control *rst;
+ struct clk *clk;
+ void __iomem *base;
+ int i, ret;
+
+ desc = of_device_get_match_data(dev);
+
+ pck = devm_kzalloc(dev, struct_size(pck, pds, desc->num_domains), GFP_KERNEL);
+ if (!pck)
+ return -ENOMEM;
+
+ pck->dev = &pdev->dev;
+ platform_set_drvdata(pdev, pck);
+
+ genpds = &pck->genpd_data;
+ genpds->num_domains = desc->num_domains;
+ genpds->domains = devm_kcalloc(dev, desc->num_domains,
+ sizeof(*genpds->domains), GFP_KERNEL);
+ if (!genpds->domains)
+ return -ENOMEM;
+
+ base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(base))
+ return PTR_ERR(base);
+
+ if (desc->has_rst_clk) {
+ rst = devm_reset_control_get_exclusive_released(dev, NULL);
+ if (IS_ERR(rst))
+ return dev_err_probe(dev, PTR_ERR(rst), "failed to get reset control\n");
+ }
+
+ clk = devm_clk_get_enabled(dev, NULL);
+ if (IS_ERR(clk))
+ return dev_err_probe(dev, PTR_ERR(clk), "failed to get clock\n");
+
+ for (i = 0; i < desc->num_domains; i++) {
+ struct sunxi_pck600_pd *pd = &pck->pds[i];
+ const struct sunxi_pck600_pd_desc *pd_desc = &desc->pd_descs[i];
+
+ pd->genpd.name = pd_desc->name;
+ pd->genpd.power_off = sunxi_pck600_power_off;
+ pd->genpd.power_on = sunxi_pck600_power_on;
+ pd->genpd.flags = pd_desc->flags;
+ pd->base = base + PPU_REG_SIZE * i;
+
+ sunxi_pck600_pd_setup(pd, desc);
+ ret = pm_genpd_init(&pd->genpd, NULL, false);
+ if (ret) {
+ dev_err_probe(dev, ret, "failed to initialize power domain\n");
+ goto err_remove_pds;
+ }
+
+ genpds->domains[i] = &pd->genpd;
+ }
+
+ ret = of_genpd_add_provider_onecell(dev_of_node(dev), genpds);
+ if (ret) {
+ dev_err_probe(dev, ret, "failed to add PD provider\n");
+ goto err_remove_pds;
+ }
+
+ return 0;
+
+err_remove_pds:
+ for (i--; i >= 0; i--)
+ pm_genpd_remove(genpds->domains[i]);
+
+ return ret;
+}
+
+static const struct sunxi_pck600_pd_desc sun55i_a523_pck600_pds[] = {
+ { "VE", }, { "GPU", }, { "VI", }, { "VO0", }, { "VO1", },
+ { "DE", }, { "NAND", }, { "PCIE", },
+};
+
+static const struct sunxi_pck600_desc sun55i_a523_pck600_desc = {
+ .pd_descs = sun55i_a523_pck600_pds,
+ .num_domains = ARRAY_SIZE(sun55i_a523_pck600_pds),
+ .logic_power_switch0_delay_offset = 0xc00,
+ .logic_power_switch1_delay_offset = 0xc04,
+ .off2on_delay_offset = 0xc10,
+ .device_ctrl0_delay = 0xffffff,
+ .device_ctrl1_delay = 0xffff,
+ .logic_power_switch0_delay = 0x8080808,
+ .logic_power_switch1_delay = 0x808,
+ .off2on_delay = 0x8,
+ .has_rst_clk = true,
+};
+
+static const struct sunxi_pck600_pd_desc sun60i_a733_pck600_pds[] = {
+ { "VI", }, { "DE_SYS", }, { "VE_DEC", }, { "VE_ENC", }, { "NPU", },
+ { "GPU_TOP", }, { "GPU_CORE", GENPD_FLAG_ALWAYS_ON },
+ { "PCIE", }, { "USB2", }, { "VO", }, { "VO1", },
+};
+
+static const struct sunxi_pck600_desc sun60i_a733_pck600_desc = {
+ .pd_descs = sun60i_a733_pck600_pds,
+ .num_domains = ARRAY_SIZE(sun60i_a733_pck600_pds),
+ .logic_power_switch0_delay_offset = 0xc00,
+ .logic_power_switch1_delay_offset = 0xc04,
+ .off2on_delay_offset = 0xc10,
+ .device_ctrl0_delay = 0x1f1f1f,
+ .device_ctrl1_delay = 0x1f1f,
+ .logic_power_switch0_delay = 0x8080808,
+ .logic_power_switch1_delay = 0x808,
+ .off2on_delay = 0x8,
+ .has_rst_clk = false,
+};
+
+static const struct of_device_id sunxi_pck600_of_match[] = {
+ {
+ .compatible = "allwinner,sun55i-a523-pck-600",
+ .data = &sun55i_a523_pck600_desc,
+ },
+ {
+ .compatible = "allwinner,sun60i-a733-pck-600",
+ .data = &sun60i_a733_pck600_desc,
+ },
+ {}
+};
+MODULE_DEVICE_TABLE(of, sunxi_pck600_of_match);
+
+static struct platform_driver sunxi_pck600_driver = {
+ .probe = sunxi_pck600_probe,
+ .driver = {
+ .name = "sunxi-pck-600",
+ .of_match_table = sunxi_pck600_of_match,
+ /* Power domains cannot be removed if in use. */
+ .suppress_bind_attrs = true,
+ },
+};
+module_platform_driver(sunxi_pck600_driver);
+
+MODULE_DESCRIPTION("Allwinner PCK-600 power domain driver");
+MODULE_AUTHOR("Chen-Yu Tsai <wens@csie.org>");
+MODULE_LICENSE("GPL");