summaryrefslogtreecommitdiff
path: root/drivers/pmdomain/marvell/pxa1908-power-controller.c
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/pmdomain/marvell/pxa1908-power-controller.c')
-rw-r--r--drivers/pmdomain/marvell/pxa1908-power-controller.c274
1 files changed, 274 insertions, 0 deletions
diff --git a/drivers/pmdomain/marvell/pxa1908-power-controller.c b/drivers/pmdomain/marvell/pxa1908-power-controller.c
new file mode 100644
index 000000000000..ff5e6e82d3f8
--- /dev/null
+++ b/drivers/pmdomain/marvell/pxa1908-power-controller.c
@@ -0,0 +1,274 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright 2025 Duje Mihanović <duje@dujemihanovic.xyz>
+ */
+
+#include <linux/auxiliary_bus.h>
+#include <linux/container_of.h>
+#include <linux/dev_printk.h>
+#include <linux/device.h>
+#include <linux/mfd/syscon.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/pm_domain.h>
+#include <linux/regmap.h>
+
+#include <dt-bindings/power/marvell,pxa1908-power.h>
+
+/* VPU, GPU, ISP */
+#define APMU_PWR_CTRL_REG 0xd8
+#define APMU_PWR_BLK_TMR_REG 0xdc
+#define APMU_PWR_STATUS_REG 0xf0
+
+/* DSI */
+#define APMU_DEBUG 0x88
+#define DSI_PHY_DVM_MASK BIT(31)
+
+#define POWER_ON_LATENCY_US 300
+#define POWER_OFF_LATENCY_US 20
+#define POWER_POLL_TIMEOUT_US (25 * USEC_PER_MSEC)
+#define POWER_POLL_SLEEP_US 6
+
+#define NR_DOMAINS 5
+
+#define to_pxa1908_pd(_genpd) container_of(_genpd, struct pxa1908_pd, genpd)
+
+struct pxa1908_pd_ctrl {
+ struct generic_pm_domain *domains[NR_DOMAINS];
+ struct genpd_onecell_data onecell_data;
+ struct regmap *base;
+ struct device *dev;
+};
+
+struct pxa1908_pd_data {
+ u32 reg_clk_res_ctrl;
+ u32 pwr_state;
+ u32 hw_mode;
+ bool keep_on;
+ int id;
+};
+
+struct pxa1908_pd {
+ const struct pxa1908_pd_data data;
+ struct pxa1908_pd_ctrl *ctrl;
+ struct generic_pm_domain genpd;
+ bool initialized;
+};
+
+static inline bool pxa1908_pd_is_on(struct pxa1908_pd *pd)
+{
+ struct pxa1908_pd_ctrl *ctrl = pd->ctrl;
+
+ return pd->data.id != PXA1908_POWER_DOMAIN_DSI
+ ? regmap_test_bits(ctrl->base, APMU_PWR_STATUS_REG, pd->data.pwr_state)
+ : regmap_test_bits(ctrl->base, APMU_DEBUG, DSI_PHY_DVM_MASK);
+}
+
+static int pxa1908_pd_power_on(struct generic_pm_domain *genpd)
+{
+ struct pxa1908_pd *pd = to_pxa1908_pd(genpd);
+ const struct pxa1908_pd_data *data = &pd->data;
+ struct pxa1908_pd_ctrl *ctrl = pd->ctrl;
+ unsigned int status;
+ int ret = 0;
+
+ regmap_set_bits(ctrl->base, data->reg_clk_res_ctrl, data->hw_mode);
+ if (data->id != PXA1908_POWER_DOMAIN_ISP)
+ regmap_write(ctrl->base, APMU_PWR_BLK_TMR_REG, 0x20001fff);
+ regmap_set_bits(ctrl->base, APMU_PWR_CTRL_REG, data->pwr_state);
+
+ ret = regmap_read_poll_timeout(ctrl->base, APMU_PWR_STATUS_REG, status,
+ status & data->pwr_state, POWER_POLL_SLEEP_US,
+ POWER_ON_LATENCY_US + POWER_POLL_TIMEOUT_US);
+ if (ret == -ETIMEDOUT)
+ dev_err(ctrl->dev, "timed out powering on domain '%s'\n", pd->genpd.name);
+
+ return ret;
+}
+
+static int pxa1908_pd_power_off(struct generic_pm_domain *genpd)
+{
+ struct pxa1908_pd *pd = to_pxa1908_pd(genpd);
+ const struct pxa1908_pd_data *data = &pd->data;
+ struct pxa1908_pd_ctrl *ctrl = pd->ctrl;
+ unsigned int status;
+ int ret;
+
+ regmap_clear_bits(ctrl->base, APMU_PWR_CTRL_REG, data->pwr_state);
+
+ ret = regmap_read_poll_timeout(ctrl->base, APMU_PWR_STATUS_REG, status,
+ !(status & data->pwr_state), POWER_POLL_SLEEP_US,
+ POWER_OFF_LATENCY_US + POWER_POLL_TIMEOUT_US);
+ if (ret == -ETIMEDOUT) {
+ dev_err(ctrl->dev, "timed out powering off domain '%s'\n", pd->genpd.name);
+ return ret;
+ }
+
+ return regmap_clear_bits(ctrl->base, data->reg_clk_res_ctrl, data->hw_mode);
+}
+
+static inline int pxa1908_dsi_power_on(struct generic_pm_domain *genpd)
+{
+ struct pxa1908_pd *pd = to_pxa1908_pd(genpd);
+ struct pxa1908_pd_ctrl *ctrl = pd->ctrl;
+
+ return regmap_set_bits(ctrl->base, APMU_DEBUG, DSI_PHY_DVM_MASK);
+}
+
+static inline int pxa1908_dsi_power_off(struct generic_pm_domain *genpd)
+{
+ struct pxa1908_pd *pd = to_pxa1908_pd(genpd);
+ struct pxa1908_pd_ctrl *ctrl = pd->ctrl;
+
+ return regmap_clear_bits(ctrl->base, APMU_DEBUG, DSI_PHY_DVM_MASK);
+}
+
+#define DOMAIN(_id, _name, ctrl, mode, state) \
+ [_id] = { \
+ .data = { \
+ .reg_clk_res_ctrl = ctrl, \
+ .hw_mode = BIT(mode), \
+ .pwr_state = BIT(state), \
+ .id = _id, \
+ }, \
+ .genpd = { \
+ .name = _name, \
+ .power_on = pxa1908_pd_power_on, \
+ .power_off = pxa1908_pd_power_off, \
+ }, \
+ }
+
+static struct pxa1908_pd domains[NR_DOMAINS] = {
+ DOMAIN(PXA1908_POWER_DOMAIN_VPU, "vpu", 0xa4, 19, 2),
+ DOMAIN(PXA1908_POWER_DOMAIN_GPU, "gpu", 0xcc, 11, 0),
+ DOMAIN(PXA1908_POWER_DOMAIN_GPU2D, "gpu2d", 0xf4, 11, 6),
+ DOMAIN(PXA1908_POWER_DOMAIN_ISP, "isp", 0x38, 15, 4),
+ [PXA1908_POWER_DOMAIN_DSI] = {
+ .genpd = {
+ .name = "dsi",
+ .power_on = pxa1908_dsi_power_on,
+ .power_off = pxa1908_dsi_power_off,
+ /*
+ * TODO: There is no DSI driver written yet and until then we probably
+ * don't want to power off the DSI PHY ever.
+ */
+ .flags = GENPD_FLAG_ALWAYS_ON,
+ },
+ .data = {
+ /* See above. */
+ .keep_on = true,
+ },
+ },
+};
+
+static void pxa1908_pd_remove(struct auxiliary_device *auxdev)
+{
+ struct pxa1908_pd *pd;
+ int ret;
+
+ for (int i = NR_DOMAINS - 1; i >= 0; i--) {
+ pd = &domains[i];
+
+ if (!pd->initialized)
+ continue;
+
+ if (pxa1908_pd_is_on(pd) && !pd->data.keep_on)
+ pxa1908_pd_power_off(&pd->genpd);
+
+ ret = pm_genpd_remove(&pd->genpd);
+ if (ret)
+ dev_err(&auxdev->dev, "failed to remove domain '%s': %d\n",
+ pd->genpd.name, ret);
+ }
+}
+
+static int
+pxa1908_pd_init(struct pxa1908_pd_ctrl *ctrl, int id, struct device *dev)
+{
+ struct pxa1908_pd *pd = &domains[id];
+ int ret;
+
+ ctrl->domains[id] = &pd->genpd;
+
+ pd->ctrl = ctrl;
+
+ /* Make sure the state of the hardware is synced with the domain table above. */
+ if (pd->data.keep_on) {
+ ret = pd->genpd.power_on(&pd->genpd);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to power on domain '%s'\n",
+ pd->genpd.name);
+ } else {
+ if (pxa1908_pd_is_on(pd)) {
+ dev_warn(dev,
+ "domain '%s' is on despite being default off; powering off\n",
+ pd->genpd.name);
+
+ ret = pd->genpd.power_off(&pd->genpd);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to power off domain '%s'\n",
+ pd->genpd.name);
+ }
+ }
+
+ ret = pm_genpd_init(&pd->genpd, NULL, !pd->data.keep_on);
+ if (ret)
+ return dev_err_probe(dev, ret, "domain '%s' failed to initialize\n",
+ pd->genpd.name);
+
+ pd->initialized = true;
+
+ return 0;
+}
+
+static int
+pxa1908_pd_probe(struct auxiliary_device *auxdev, const struct auxiliary_device_id *aux_id)
+{
+ struct pxa1908_pd_ctrl *ctrl;
+ struct device *dev = &auxdev->dev;
+ int ret;
+
+ ctrl = devm_kzalloc(dev, sizeof(*ctrl), GFP_KERNEL);
+ if (!ctrl)
+ return -ENOMEM;
+
+ auxiliary_set_drvdata(auxdev, ctrl);
+
+ ctrl->base = syscon_node_to_regmap(dev->parent->of_node);
+ if (IS_ERR(ctrl->base))
+ return dev_err_probe(dev, PTR_ERR(ctrl->base), "no regmap available\n");
+
+ ctrl->dev = dev;
+ ctrl->onecell_data.domains = ctrl->domains;
+ ctrl->onecell_data.num_domains = NR_DOMAINS;
+
+ for (int i = 0; i < NR_DOMAINS; i++) {
+ ret = pxa1908_pd_init(ctrl, i, dev);
+ if (ret)
+ goto err;
+ }
+
+ return of_genpd_add_provider_onecell(dev->parent->of_node, &ctrl->onecell_data);
+
+err:
+ pxa1908_pd_remove(auxdev);
+ return ret;
+}
+
+static const struct auxiliary_device_id pxa1908_pd_id[] = {
+ { .name = "clk_pxa1908_apmu.power" },
+ { }
+};
+MODULE_DEVICE_TABLE(auxiliary, pxa1908_pd_id);
+
+static struct auxiliary_driver pxa1908_pd_driver = {
+ .probe = pxa1908_pd_probe,
+ .remove = pxa1908_pd_remove,
+ .id_table = pxa1908_pd_id,
+};
+module_auxiliary_driver(pxa1908_pd_driver);
+
+MODULE_AUTHOR("Duje Mihanović <duje@dujemihanovic.xyz>");
+MODULE_DESCRIPTION("Marvell PXA1908 power domain driver");
+MODULE_LICENSE("GPL");