Initial implementation of the power sequencing subsystem for linux v6.11

-----BEGIN PGP SIGNATURE-----
 
 iQIzBAABCgAdFiEEFp3rbAvDxGAT0sefEacuoBRx13IFAmZpT80ACgkQEacuoBRx
 13IftRAAq6VS7HWQ07npHeBp1YGuw/+3zjymS4esPoDouhMVeuNpsqqy7Y7JaaZ9
 kLHi0FxdFH2qDZ8ATDqGm8UntORH4ugvXt/bphZiEXh6dFINDCIUmFITx/08MJc3
 t6myhtWQTGgzGWZ+rlYGLPLOXwhQeYp9xNKt5ddQzRtPWuZxYnkwORr8hPlO8CqE
 3XqF1ctf/j6UtQRxZEGLAvvatVlxPEnGxjnVfd7ShavjMvmxhYIAaBS0hg7tbLjx
 M9Q3iXn+n+0/XfCteZ05I4wq//HaQ2BUpUfxOp0kBOXB5BukaUk68xBV55PtG/ZT
 wfK8jtOV3iHV2np0hbRK3TZvqjpD00qRDOZGRGgPM5V6HmxJCvaLBBTC8S7vrwad
 8t+WhXq1XeDVc0qcY3Alo+s3ECOyHsntCAvFywu8iF9unE0lWB6DIW98uOPTXTDM
 z+/VPDi+Wk6NY8LM6BqIXmoST5pjlo8FVytGpkwSG3pSGzFW9kj2sylvp8HvGnmf
 K0u43d5oeTSUvHUH5c5vtSwRgVM4Hcnm4w+tzdnY8rpZiVfuzg0iIVAShPJ1i4Qv
 P2Cw+4+G+sklV9HfYGSQcS203B+qYZfDQ5GqtzA9Pxx2gks2Qokm6mHIAVmlKqEY
 HZC5Dfplq5vLoHuyuS5mPvAC/EgsOjRnTK5qw9j7vCvdheWfosg=
 =Ft39
 -----END PGP SIGNATURE-----

Merge tag 'pwrseq-initial-for-v6.11' of git://git.kernel.org/pub/scm/linux/kernel/git/brgl/linux into HEAD

Initial implementation of the power sequencing subsystem for linux v6.11
This commit is contained in:
Luiz Augusto von Dentz 2024-07-15 10:09:20 -04:00
commit f497862d99
9 changed files with 1617 additions and 0 deletions

View File

@ -17908,6 +17908,14 @@ F: include/linux/pm_*
F: include/linux/powercap.h
F: kernel/configs/nopm.config
POWER SEQUENCING
M: Bartosz Golaszewski <brgl@bgdev.pl>
L: linux-pm@vger.kernel.org
S: Maintained
T: git git://git.kernel.org/pub/scm/linux/kernel/git/brgl/linux.git
F: drivers/power/sequencing/
F: include/linux/pwrseq/
POWER STATE COORDINATION INTERFACE (PSCI)
M: Mark Rutland <mark.rutland@arm.com>
M: Lorenzo Pieralisi <lpieralisi@kernel.org>

View File

@ -1,3 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only
source "drivers/power/reset/Kconfig"
source "drivers/power/sequencing/Kconfig"
source "drivers/power/supply/Kconfig"

View File

@ -1,3 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_POWER_RESET) += reset/
obj-$(CONFIG_POWER_SEQUENCING) += sequencing/
obj-$(CONFIG_POWER_SUPPLY) += supply/

View File

@ -0,0 +1,29 @@
# SPDX-License-Identifier: GPL-2.0-only
menuconfig POWER_SEQUENCING
tristate "Power Sequencing support"
help
Say Y here to enable the Power Sequencing subsystem.
This subsystem is designed to control power to devices that share
complex resources and/or require specific power sequences to be run
during power-up.
If unsure, say no.
if POWER_SEQUENCING
config POWER_SEQUENCING_QCOM_WCN
tristate "Qualcomm WCN family PMU driver"
default m if ARCH_QCOM
help
Say Y here to enable the power sequencing driver for Qualcomm
WCN Bluetooth/WLAN chipsets.
Typically, a package from the Qualcomm WCN family contains the BT
and WLAN modules whose power is controlled by the PMU module. As the
former two share the power-up sequence which is executed by the PMU,
this driver is needed for correct power control or else we'd risk not
respecting the required delays between enabling Bluetooth and WLAN.
endif

View File

@ -0,0 +1,6 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_POWER_SEQUENCING) += pwrseq-core.o
pwrseq-core-y := core.o
obj-$(CONFIG_POWER_SEQUENCING_QCOM_WCN) += pwrseq-qcom-wcn.o

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,336 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (C) 2024 Linaro Ltd.
*/
#include <linux/clk.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/gpio/consumer.h>
#include <linux/jiffies.h>
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/regulator/consumer.h>
#include <linux/pwrseq/provider.h>
#include <linux/string.h>
#include <linux/types.h>
struct pwrseq_qcom_wcn_pdata {
const char *const *vregs;
size_t num_vregs;
unsigned int pwup_delay_ms;
unsigned int gpio_enable_delay_ms;
};
struct pwrseq_qcom_wcn_ctx {
struct pwrseq_device *pwrseq;
struct device_node *of_node;
const struct pwrseq_qcom_wcn_pdata *pdata;
struct regulator_bulk_data *regs;
struct gpio_desc *bt_gpio;
struct gpio_desc *wlan_gpio;
struct clk *clk;
unsigned long last_gpio_enable_jf;
};
static void pwrseq_qcom_wcn_ensure_gpio_delay(struct pwrseq_qcom_wcn_ctx *ctx)
{
unsigned long diff_jiffies;
unsigned int diff_msecs;
if (!ctx->pdata->gpio_enable_delay_ms)
return;
diff_jiffies = jiffies - ctx->last_gpio_enable_jf;
diff_msecs = jiffies_to_msecs(diff_jiffies);
if (diff_msecs < ctx->pdata->gpio_enable_delay_ms)
msleep(ctx->pdata->gpio_enable_delay_ms - diff_msecs);
}
static int pwrseq_qcom_wcn_vregs_enable(struct pwrseq_device *pwrseq)
{
struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq);
return regulator_bulk_enable(ctx->pdata->num_vregs, ctx->regs);
}
static int pwrseq_qcom_wcn_vregs_disable(struct pwrseq_device *pwrseq)
{
struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq);
return regulator_bulk_disable(ctx->pdata->num_vregs, ctx->regs);
}
static const struct pwrseq_unit_data pwrseq_qcom_wcn_vregs_unit_data = {
.name = "regulators-enable",
.enable = pwrseq_qcom_wcn_vregs_enable,
.disable = pwrseq_qcom_wcn_vregs_disable,
};
static int pwrseq_qcom_wcn_clk_enable(struct pwrseq_device *pwrseq)
{
struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq);
return clk_prepare_enable(ctx->clk);
}
static int pwrseq_qcom_wcn_clk_disable(struct pwrseq_device *pwrseq)
{
struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq);
clk_disable_unprepare(ctx->clk);
return 0;
}
static const struct pwrseq_unit_data pwrseq_qcom_wcn_clk_unit_data = {
.name = "clock-enable",
.enable = pwrseq_qcom_wcn_clk_enable,
.disable = pwrseq_qcom_wcn_clk_disable,
};
static const struct pwrseq_unit_data *pwrseq_qcom_wcn_unit_deps[] = {
&pwrseq_qcom_wcn_vregs_unit_data,
&pwrseq_qcom_wcn_clk_unit_data,
NULL
};
static int pwrseq_qcom_wcn_bt_enable(struct pwrseq_device *pwrseq)
{
struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq);
pwrseq_qcom_wcn_ensure_gpio_delay(ctx);
gpiod_set_value_cansleep(ctx->bt_gpio, 1);
ctx->last_gpio_enable_jf = jiffies;
return 0;
}
static int pwrseq_qcom_wcn_bt_disable(struct pwrseq_device *pwrseq)
{
struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq);
gpiod_set_value_cansleep(ctx->bt_gpio, 0);
return 0;
}
static const struct pwrseq_unit_data pwrseq_qcom_wcn_bt_unit_data = {
.name = "bluetooth-enable",
.deps = pwrseq_qcom_wcn_unit_deps,
.enable = pwrseq_qcom_wcn_bt_enable,
.disable = pwrseq_qcom_wcn_bt_disable,
};
static int pwrseq_qcom_wcn_wlan_enable(struct pwrseq_device *pwrseq)
{
struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq);
pwrseq_qcom_wcn_ensure_gpio_delay(ctx);
gpiod_set_value_cansleep(ctx->wlan_gpio, 1);
ctx->last_gpio_enable_jf = jiffies;
return 0;
}
static int pwrseq_qcom_wcn_wlan_disable(struct pwrseq_device *pwrseq)
{
struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq);
gpiod_set_value_cansleep(ctx->wlan_gpio, 0);
return 0;
}
static const struct pwrseq_unit_data pwrseq_qcom_wcn_wlan_unit_data = {
.name = "wlan-enable",
.deps = pwrseq_qcom_wcn_unit_deps,
.enable = pwrseq_qcom_wcn_wlan_enable,
.disable = pwrseq_qcom_wcn_wlan_disable,
};
static int pwrseq_qcom_wcn_pwup_delay(struct pwrseq_device *pwrseq)
{
struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq);
if (ctx->pdata->pwup_delay_ms)
msleep(ctx->pdata->pwup_delay_ms);
return 0;
}
static const struct pwrseq_target_data pwrseq_qcom_wcn_bt_target_data = {
.name = "bluetooth",
.unit = &pwrseq_qcom_wcn_bt_unit_data,
.post_enable = pwrseq_qcom_wcn_pwup_delay,
};
static const struct pwrseq_target_data pwrseq_qcom_wcn_wlan_target_data = {
.name = "wlan",
.unit = &pwrseq_qcom_wcn_wlan_unit_data,
.post_enable = pwrseq_qcom_wcn_pwup_delay,
};
static const struct pwrseq_target_data *pwrseq_qcom_wcn_targets[] = {
&pwrseq_qcom_wcn_bt_target_data,
&pwrseq_qcom_wcn_wlan_target_data,
NULL
};
static const char *const pwrseq_qca6390_vregs[] = {
"vddio",
"vddaon",
"vddpmu",
"vddrfa0p95",
"vddrfa1p3",
"vddrfa1p9",
"vddpcie1p3",
"vddpcie1p9",
};
static const struct pwrseq_qcom_wcn_pdata pwrseq_qca6390_of_data = {
.vregs = pwrseq_qca6390_vregs,
.num_vregs = ARRAY_SIZE(pwrseq_qca6390_vregs),
.pwup_delay_ms = 60,
.gpio_enable_delay_ms = 100,
};
static const char *const pwrseq_wcn7850_vregs[] = {
"vdd",
"vddio",
"vddio1p2",
"vddaon",
"vdddig",
"vddrfa1p2",
"vddrfa1p8",
};
static const struct pwrseq_qcom_wcn_pdata pwrseq_wcn7850_of_data = {
.vregs = pwrseq_wcn7850_vregs,
.num_vregs = ARRAY_SIZE(pwrseq_wcn7850_vregs),
.pwup_delay_ms = 50,
};
static int pwrseq_qcom_wcn_match(struct pwrseq_device *pwrseq,
struct device *dev)
{
struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq);
struct device_node *dev_node = dev->of_node;
/*
* The PMU supplies power to the Bluetooth and WLAN modules. both
* consume the PMU AON output so check the presence of the
* 'vddaon-supply' property and whether it leads us to the right
* device.
*/
if (!of_property_present(dev_node, "vddaon-supply"))
return 0;
struct device_node *reg_node __free(device_node) =
of_parse_phandle(dev_node, "vddaon-supply", 0);
if (!reg_node)
return 0;
/*
* `reg_node` is the PMU AON regulator, its parent is the `regulators`
* node and finally its grandparent is the PMU device node that we're
* looking for.
*/
if (!reg_node->parent || !reg_node->parent->parent ||
reg_node->parent->parent != ctx->of_node)
return 0;
return 1;
}
static int pwrseq_qcom_wcn_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct pwrseq_qcom_wcn_ctx *ctx;
struct pwrseq_config config;
int i, ret;
ctx = devm_kzalloc(dev, sizeof(*ctx), GFP_KERNEL);
if (!ctx)
return -ENOMEM;
ctx->of_node = dev->of_node;
ctx->pdata = of_device_get_match_data(dev);
if (!ctx->pdata)
return dev_err_probe(dev, -ENODEV,
"Failed to obtain platform data\n");
ctx->regs = devm_kcalloc(dev, ctx->pdata->num_vregs,
sizeof(*ctx->regs), GFP_KERNEL);
if (!ctx->regs)
return -ENOMEM;
for (i = 0; i < ctx->pdata->num_vregs; i++)
ctx->regs[i].supply = ctx->pdata->vregs[i];
ret = devm_regulator_bulk_get(dev, ctx->pdata->num_vregs, ctx->regs);
if (ret < 0)
return dev_err_probe(dev, ret,
"Failed to get all regulators\n");
ctx->bt_gpio = devm_gpiod_get_optional(dev, "bt-enable", GPIOD_OUT_LOW);
if (IS_ERR(ctx->bt_gpio))
return dev_err_probe(dev, PTR_ERR(ctx->bt_gpio),
"Failed to get the Bluetooth enable GPIO\n");
ctx->wlan_gpio = devm_gpiod_get_optional(dev, "wlan-enable",
GPIOD_OUT_LOW);
if (IS_ERR(ctx->wlan_gpio))
return dev_err_probe(dev, PTR_ERR(ctx->wlan_gpio),
"Failed to get the WLAN enable GPIO\n");
ctx->clk = devm_clk_get_optional(dev, NULL);
if (IS_ERR(ctx->clk))
return dev_err_probe(dev, PTR_ERR(ctx->clk),
"Failed to get the reference clock\n");
memset(&config, 0, sizeof(config));
config.parent = dev;
config.owner = THIS_MODULE;
config.drvdata = ctx;
config.match = pwrseq_qcom_wcn_match;
config.targets = pwrseq_qcom_wcn_targets;
ctx->pwrseq = devm_pwrseq_device_register(dev, &config);
if (IS_ERR(ctx->pwrseq))
return dev_err_probe(dev, PTR_ERR(ctx->pwrseq),
"Failed to register the power sequencer\n");
return 0;
}
static const struct of_device_id pwrseq_qcom_wcn_of_match[] = {
{
.compatible = "qcom,qca6390-pmu",
.data = &pwrseq_qca6390_of_data,
},
{
.compatible = "qcom,wcn7850-pmu",
.data = &pwrseq_wcn7850_of_data,
},
{ }
};
MODULE_DEVICE_TABLE(of, pwrseq_qcom_wcn_of_match);
static struct platform_driver pwrseq_qcom_wcn_driver = {
.driver = {
.name = "pwrseq-qcom_wcn",
.of_match_table = pwrseq_qcom_wcn_of_match,
},
.probe = pwrseq_qcom_wcn_probe,
};
module_platform_driver(pwrseq_qcom_wcn_driver);
MODULE_AUTHOR("Bartosz Golaszewski <bartosz.golaszewski@linaro.org>");
MODULE_DESCRIPTION("Qualcomm WCN PMU power sequencing driver");
MODULE_LICENSE("GPL");

View File

@ -0,0 +1,56 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Copyright (C) 2024 Linaro Ltd.
*/
#ifndef __POWER_SEQUENCING_CONSUMER_H__
#define __POWER_SEQUENCING_CONSUMER_H__
#include <linux/err.h>
struct device;
struct pwrseq_desc;
#if IS_ENABLED(CONFIG_POWER_SEQUENCING)
struct pwrseq_desc * __must_check
pwrseq_get(struct device *dev, const char *target);
void pwrseq_put(struct pwrseq_desc *desc);
struct pwrseq_desc * __must_check
devm_pwrseq_get(struct device *dev, const char *target);
int pwrseq_power_on(struct pwrseq_desc *desc);
int pwrseq_power_off(struct pwrseq_desc *desc);
#else /* CONFIG_POWER_SEQUENCING */
static inline struct pwrseq_desc * __must_check
pwrseq_get(struct device *dev, const char *target)
{
return ERR_PTR(-ENOSYS);
}
static inline void pwrseq_put(struct pwrseq_desc *desc)
{
}
static inline struct pwrseq_desc * __must_check
devm_pwrseq_get(struct device *dev, const char *target)
{
return ERR_PTR(-ENOSYS);
}
static inline int pwrseq_power_on(struct pwrseq_desc *desc)
{
return -ENOSYS;
}
static inline int pwrseq_power_off(struct pwrseq_desc *desc)
{
return -ENOSYS;
}
#endif /* CONFIG_POWER_SEQUENCING */
#endif /* __POWER_SEQUENCING_CONSUMER_H__ */

View File

@ -0,0 +1,75 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Copyright (C) 2024 Linaro Ltd.
*/
#ifndef __POWER_SEQUENCING_PROVIDER_H__
#define __POWER_SEQUENCING_PROVIDER_H__
struct device;
struct module;
struct pwrseq_device;
typedef int (*pwrseq_power_state_func)(struct pwrseq_device *);
typedef int (*pwrseq_match_func)(struct pwrseq_device *, struct device *);
/**
* struct pwrseq_unit_data - Configuration of a single power sequencing
* unit.
* @name: Name of the unit.
* @deps: Units that must be enabled before this one and disabled after it
* in the order they come in this array. Must be NULL-terminated.
* @enable: Callback running the part of the power-on sequence provided by
* this unit.
* @disable: Callback running the part of the power-off sequence provided
* by this unit.
*/
struct pwrseq_unit_data {
const char *name;
const struct pwrseq_unit_data **deps;
pwrseq_power_state_func enable;
pwrseq_power_state_func disable;
};
/**
* struct pwrseq_target_data - Configuration of a power sequencing target.
* @name: Name of the target.
* @unit: Final unit that this target must reach in order to be considered
* enabled.
* @post_enable: Callback run after the target unit has been enabled, *after*
* the state lock has been released. It's useful for implementing
* boot-up delays without blocking other users from powering up
* using the same power sequencer.
*/
struct pwrseq_target_data {
const char *name;
const struct pwrseq_unit_data *unit;
pwrseq_power_state_func post_enable;
};
/**
* struct pwrseq_config - Configuration used for registering a new provider.
* @parent: Parent device for the sequencer. Must be set.
* @owner: Module providing this device.
* @drvdata: Private driver data.
* @match: Provider callback used to match the consumer device to the sequencer.
* @targets: Array of targets for this power sequencer. Must be NULL-terminated.
*/
struct pwrseq_config {
struct device *parent;
struct module *owner;
void *drvdata;
pwrseq_match_func match;
const struct pwrseq_target_data **targets;
};
struct pwrseq_device *
pwrseq_device_register(const struct pwrseq_config *config);
void pwrseq_device_unregister(struct pwrseq_device *pwrseq);
struct pwrseq_device *
devm_pwrseq_device_register(struct device *dev,
const struct pwrseq_config *config);
void *pwrseq_device_get_drvdata(struct pwrseq_device *pwrseq);
#endif /* __POWER_SEQUENCING_PROVIDER_H__ */