mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-07-27 15:40:10 -04:00
Define power domain which needs to be enabled in order for audio to work on the PXA1908-based samsung,coreprimevelte smartphone. In the downstream code, this power-on method is marked as specific to the ULCx series which is allegedly a codename of sorts with ULC1 corresponding to the PXA1908. No other audio components needed for sound to work on this phone are currently available mainline but some successful testing was performed with the vendor variants of the respective drivers and with the domain forced always-on. Signed-off-by: Karel Balej <balejk@matfyz.cz> Reviewed-by: Duje Mihanović <duje@dujemihanovic.xyz> Signed-off-by: Ulf Hansson <ulf.hansson@linaro.org>
306 lines
7.8 KiB
C
306 lines
7.8 KiB
C
// 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 APMU_AUDIO_CLK 0x80
|
|
#define AUDIO_ULCX_ENABLE 0x0d
|
|
|
|
#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 6
|
|
|
|
#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;
|
|
|
|
switch (pd->data.id) {
|
|
case PXA1908_POWER_DOMAIN_AUDIO:
|
|
return regmap_test_bits(ctrl->base, APMU_AUDIO_CLK, AUDIO_ULCX_ENABLE);
|
|
case PXA1908_POWER_DOMAIN_DSI:
|
|
return regmap_test_bits(ctrl->base, APMU_DEBUG, DSI_PHY_DVM_MASK);
|
|
default:
|
|
return regmap_test_bits(ctrl->base, APMU_PWR_STATUS_REG, pd->data.pwr_state);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
static inline int pxa1908_audio_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_AUDIO_CLK, AUDIO_ULCX_ENABLE);
|
|
}
|
|
|
|
static inline int pxa1908_audio_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_AUDIO_CLK, AUDIO_ULCX_ENABLE);
|
|
}
|
|
|
|
#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,
|
|
},
|
|
},
|
|
[PXA1908_POWER_DOMAIN_AUDIO] = {
|
|
.genpd = {
|
|
.name = "audio",
|
|
.power_on = pxa1908_audio_power_on,
|
|
.power_off = pxa1908_audio_power_off,
|
|
},
|
|
},
|
|
};
|
|
|
|
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");
|