From 3b04e9b8056e868c3e9a04cc74168c7c9a18746a Mon Sep 17 00:00:00 2001 From: Biju Das Date: Wed, 26 Aug 2026 13:27:25 +0100 Subject: power: sequencing: Add Renesas RZ/G3L Power Ready driver Add a power sequencing driver for the Renesas RZ/G3L PWRRDY module, which signals power readiness for various IPs (USB, DSI, CSI etc.) on the SoC. The driver binds as an auxiliary device to the parent SYSC driver, using its regmap to toggle the SYS_PWRRDY_N register bits, and exposes {usb,dsi,csi}-pwrrdy pwrseq targets. Signed-off-by: Biju Das Link: https://patch.msgid.link/20260826122742.153643-4-biju.das.jz@bp.renesas.com Signed-off-by: Bartosz Golaszewski --- drivers/power/sequencing/Kconfig | 9 ++ drivers/power/sequencing/Makefile | 1 + drivers/power/sequencing/pwrseq-renesas-pwrrdy.c | 142 +++++++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 drivers/power/sequencing/pwrseq-renesas-pwrrdy.c diff --git a/drivers/power/sequencing/Kconfig b/drivers/power/sequencing/Kconfig index 1c5f5820f5b7..286d4e300845 100644 --- a/drivers/power/sequencing/Kconfig +++ b/drivers/power/sequencing/Kconfig @@ -27,6 +27,15 @@ config POWER_SEQUENCING_QCOM_WCN this driver is needed for correct power control or else we'd risk not respecting the required delays between enabling Bluetooth and WLAN. +config POWER_SEQUENCING_RENESAS_PWRRDY + tristate "Renesas Power Ready sequencing driver" + depends on SYSC_RZ || COMPILE_TEST + select AUXILIARY_BUS + help + Say Y here to enable the power sequencing driver for the Renesas + Power Ready signals. This driver handles the power ready signals + required to power on the various IP's on RZ/G3L platform. + config POWER_SEQUENCING_TH1520_GPU tristate "T-HEAD TH1520 GPU power sequencing driver" depends on (ARCH_THEAD && AUXILIARY_BUS) || COMPILE_TEST diff --git a/drivers/power/sequencing/Makefile b/drivers/power/sequencing/Makefile index 0911d4618298..b33d08d82f43 100644 --- a/drivers/power/sequencing/Makefile +++ b/drivers/power/sequencing/Makefile @@ -4,5 +4,6 @@ obj-$(CONFIG_POWER_SEQUENCING) += pwrseq-core.o pwrseq-core-y := core.o obj-$(CONFIG_POWER_SEQUENCING_QCOM_WCN) += pwrseq-qcom-wcn.o +obj-$(CONFIG_POWER_SEQUENCING_RENESAS_PWRRDY) += pwrseq-renesas-pwrrdy.o obj-$(CONFIG_POWER_SEQUENCING_TH1520_GPU) += pwrseq-thead-gpu.o obj-$(CONFIG_POWER_SEQUENCING_PCIE_M2) += pwrseq-pcie-m2.o diff --git a/drivers/power/sequencing/pwrseq-renesas-pwrrdy.c b/drivers/power/sequencing/pwrseq-renesas-pwrrdy.c new file mode 100644 index 000000000000..743855620501 --- /dev/null +++ b/drivers/power/sequencing/pwrseq-renesas-pwrrdy.c @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Renesas RZ/G3L Power Ready driver + * + */ + +#include +#include +#include +#include +#include + +#define SYS_PWRRDY_N 0xd70 +#define SYS_PWRRDY_N_USB_MASK BIT(0) +#define SYS_PWRRDY_N_DSI_MASK BIT(1) +#define SYS_PWRRDY_N_CSI_MASK BIT(2) + +static int pwrseq_rzg3l_set_pwrrdy(struct pwrseq_device *pwrseq, u32 mask, + bool val) +{ + struct regmap *regmap = pwrseq_device_get_drvdata(pwrseq); + + return regmap_assign_bits(regmap, SYS_PWRRDY_N, mask, val); +} + +static int pwrseq_rzg3l_usb_pwrrdy_enable(struct pwrseq_device *pwrseq) +{ + return pwrseq_rzg3l_set_pwrrdy(pwrseq, SYS_PWRRDY_N_USB_MASK, 0); +} + +static int pwrseq_rzg3l_usb_pwrrdy_disable(struct pwrseq_device *pwrseq) +{ + return pwrseq_rzg3l_set_pwrrdy(pwrseq, SYS_PWRRDY_N_USB_MASK, 1); +} + +static const struct pwrseq_unit_data pwrseq_rzg3l_usb_pwrrdy_unit = { + .name = "usb-pwrrdy-power-sequence", + .enable = pwrseq_rzg3l_usb_pwrrdy_enable, + .disable = pwrseq_rzg3l_usb_pwrrdy_disable, +}; + +static int pwrseq_rzg3l_dsi_pwrrdy_enable(struct pwrseq_device *pwrseq) +{ + return pwrseq_rzg3l_set_pwrrdy(pwrseq, SYS_PWRRDY_N_DSI_MASK, 0); +} + +static int pwrseq_rzg3l_dsi_pwrrdy_disable(struct pwrseq_device *pwrseq) +{ + return pwrseq_rzg3l_set_pwrrdy(pwrseq, SYS_PWRRDY_N_DSI_MASK, 1); +} + +static const struct pwrseq_unit_data pwrseq_rzg3l_dsi_pwrrdy_unit = { + .name = "dsi-pwrrdy-sequence", + .enable = pwrseq_rzg3l_dsi_pwrrdy_enable, + .disable = pwrseq_rzg3l_dsi_pwrrdy_disable, +}; + +static int pwrseq_rzg3l_csi_pwrrdy_enable(struct pwrseq_device *pwrseq) +{ + return pwrseq_rzg3l_set_pwrrdy(pwrseq, SYS_PWRRDY_N_CSI_MASK, 0); +} + +static int pwrseq_rzg3l_csi_pwrrdy_disable(struct pwrseq_device *pwrseq) +{ + return pwrseq_rzg3l_set_pwrrdy(pwrseq, SYS_PWRRDY_N_CSI_MASK, 1); +} + +static const struct pwrseq_unit_data pwrseq_rzg3l_csi_pwrrdy_unit = { + .name = "csi-pwrrdy-power-sequence", + .enable = pwrseq_rzg3l_csi_pwrrdy_enable, + .disable = pwrseq_rzg3l_csi_pwrrdy_disable, +}; + +static const struct pwrseq_target_data pwrseq_rzg3l_usb_pwrrdy_target = { + .name = "usb-pwrrdy", + .unit = &pwrseq_rzg3l_usb_pwrrdy_unit, +}; + +static const struct pwrseq_target_data pwrseq_rzg3l_dsi_pwrrdy_target = { + .name = "dsi-pwrrdy", + .unit = &pwrseq_rzg3l_dsi_pwrrdy_unit, +}; + +static const struct pwrseq_target_data pwrseq_rzg3l_csi_pwrrdy_target = { + .name = "csi-pwrrdy", + .unit = &pwrseq_rzg3l_csi_pwrrdy_unit, +}; + +static const struct pwrseq_target_data *pwrseq_rzg3l_pwrrdy_targets[] = { + &pwrseq_rzg3l_usb_pwrrdy_target, + &pwrseq_rzg3l_dsi_pwrrdy_target, + &pwrseq_rzg3l_csi_pwrrdy_target, + NULL +}; + +static const char * const pwrseq_rzg3l_pwrrdy_compats[] = { + "renesas,r9a08g046-usbphy-ctrl", + "renesas,r9a08g046-mipi-dsi", + NULL +}; + +static int pwrseq_rzg3l_pwrrdy_match(struct pwrseq_device *pwrseq, + struct device *dev) +{ + if (of_device_compatible_match(dev->of_node, pwrseq_rzg3l_pwrrdy_compats)) + return PWRSEQ_MATCH_OK; + + return PWRSEQ_NO_MATCH; +} + +static int pwrseq_rzg3l_pwrrdy_probe(struct auxiliary_device *adev, + const struct auxiliary_device_id *id) +{ + struct pwrseq_config config = { + .parent = &adev->dev, + .owner = THIS_MODULE, + .drvdata = adev->dev.platform_data, + .match = pwrseq_rzg3l_pwrrdy_match, + .targets = pwrseq_rzg3l_pwrrdy_targets, + }; + + return PTR_ERR_OR_ZERO(devm_pwrseq_device_register(&adev->dev, &config)); +} + +static const struct auxiliary_device_id pwrseq_rzg3l_pwrrdy_id_table[] = { + { .name = "rz_sysc.pwrseq-pwrrdy" }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(auxiliary, pwrseq_rzg3l_pwrrdy_id_table); + +static struct auxiliary_driver pwrseq_rzg3l_pwrrdy_driver = { + .driver = { + .name = "pwrseq-rzg3l-pwrrdy", + }, + .probe = pwrseq_rzg3l_pwrrdy_probe, + .id_table = pwrseq_rzg3l_pwrrdy_id_table, +}; +module_auxiliary_driver(pwrseq_rzg3l_pwrrdy_driver); + +MODULE_AUTHOR("Biju Das "); +MODULE_DESCRIPTION("Renesas RZ/G3L Power Ready Driver"); +MODULE_LICENSE("GPL"); -- cgit v1.2.3 From 935ecc5c86c5ae2693b2fbff83fc1bd086a9c915 Mon Sep 17 00:00:00 2001 From: Wei Deng Date: Mon, 7 Sep 2026 17:51:29 +0530 Subject: power: sequencing: pwrseq-pcie-m2: Support PCIe switch topologies The PCI parent OF node check in pwrseq_pcie_m2_notify() and pwrseq_pcie_m2_create_serdev() only looks at the immediate parent of the PCI endpoint device. This fails when a PCIe switch inserts one or more intermediate bridges between the root port and the endpoint. For example, on lemans-evk with IFP Mezzanine board, the Toshiba TC9562 PCIe switch creates two bridge devices (upstream and downstream ports) without OF nodes, so the WCN6855 BT device appears at depth 3 and its immediate parent has of_node=NULL. Add a helper pwrseq_pcie_m2_pci_parent_matches() that walks the PCI parent chain upward until it finds a device whose OF node matches the expected connector port node, or reaches a non-PCI device. Replace both single-level checks with this helper to handle arbitrary PCIe switch depths. Signed-off-by: Wei Deng Reviewed-by: Manivannan Sadhasivam Link: https://patch.msgid.link/20260907122129.1577279-1-wei.deng@oss.qualcomm.com Signed-off-by: Bartosz Golaszewski --- drivers/power/sequencing/pwrseq-pcie-m2.c | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/drivers/power/sequencing/pwrseq-pcie-m2.c b/drivers/power/sequencing/pwrseq-pcie-m2.c index de9848a9a9f1..82ba309ba5f3 100644 --- a/drivers/power/sequencing/pwrseq-pcie-m2.c +++ b/drivers/power/sequencing/pwrseq-pcie-m2.c @@ -376,6 +376,21 @@ static void pwrseq_pcie_m2_remove_serdev(struct pwrseq_pcie_m2_ctx *ctx, mutex_unlock(&ctx->list_lock); } +static bool pwrseq_pcie_m2_pci_parent_matches(struct pci_dev *pdev, + struct device_node *pci_parent) +{ + struct device *dev = pdev->dev.parent; + + while (dev) { + if (dev->of_node == pci_parent) + return true; + if (!dev_is_pci(dev)) + break; + dev = dev->parent; + } + return false; +} + static int pwrseq_pcie_m2_notify(struct notifier_block *nb, unsigned long action, void *data) { @@ -390,7 +405,7 @@ static int pwrseq_pcie_m2_notify(struct notifier_block *nb, unsigned long action */ struct device_node *pci_parent __free(device_node) = of_graph_get_remote_node(dev_of_node(ctx->dev), 0, 0); - if (!pci_parent || (pci_parent != pdev->dev.parent->of_node)) + if (!pci_parent || !pwrseq_pcie_m2_pci_parent_matches(pdev, pci_parent)) return NOTIFY_DONE; switch (action) { @@ -464,7 +479,7 @@ static int pwrseq_pcie_m2_create_serdev(struct pwrseq_pcie_m2_ctx *ctx) /* Create serdev for existing PCI devices if required */ for_each_pci_dev(pdev) { - if (!pdev->dev.parent || pci_parent != pdev->dev.parent->of_node) + if (!pwrseq_pcie_m2_pci_parent_matches(pdev, pci_parent)) continue; if (!pci_match_id(pwrseq_m2_pci_ids, pdev)) -- cgit v1.2.3 From c42e50314a7629ada42a605666b18e22867a6b44 Mon Sep 17 00:00:00 2001 From: Bartosz Golaszewski Date: Wed, 9 Sep 2026 14:37:10 +0200 Subject: power: sequencing: add kunit test cases Add a set of kunit test-cases for the power sequencing subsystem. Verify several use-cases such as detecting circural dependencies, tracking of the enable-count, shared dependencies, swnode lookup, missing target on matching provider, etc. Link: https://patch.msgid.link/20260909-pwrseq-kunit-v2-4-ef496afc89d2@oss.qualcomm.com Signed-off-by: Bartosz Golaszewski --- drivers/power/sequencing/Kconfig | 10 + drivers/power/sequencing/Makefile | 1 + drivers/power/sequencing/pwrseq-kunit.c | 1497 +++++++++++++++++++++++++++++++ 3 files changed, 1508 insertions(+) create mode 100644 drivers/power/sequencing/pwrseq-kunit.c diff --git a/drivers/power/sequencing/Kconfig b/drivers/power/sequencing/Kconfig index 6b5d187dd4b5..b6a9f25fe3e3 100644 --- a/drivers/power/sequencing/Kconfig +++ b/drivers/power/sequencing/Kconfig @@ -56,4 +56,14 @@ config POWER_SEQUENCING_PCIE_M2 connectors. This driver handles the power sequencing for the M.2 connectors exposing multiple interfaces like PCIe, SATA, UART, etc... +config POWER_SEQUENCING_KUNIT + tristate "Build power sequencing KUnit test cases" + depends on KUNIT + default KUNIT_ALL_TESTS + help + Say Y here to build the module containing KUnit test cases for the + power sequencing subsystem. + + If unsure, say N. + endif diff --git a/drivers/power/sequencing/Makefile b/drivers/power/sequencing/Makefile index b33d08d82f43..76a2775fab33 100644 --- a/drivers/power/sequencing/Makefile +++ b/drivers/power/sequencing/Makefile @@ -7,3 +7,4 @@ obj-$(CONFIG_POWER_SEQUENCING_QCOM_WCN) += pwrseq-qcom-wcn.o obj-$(CONFIG_POWER_SEQUENCING_RENESAS_PWRRDY) += pwrseq-renesas-pwrrdy.o obj-$(CONFIG_POWER_SEQUENCING_TH1520_GPU) += pwrseq-thead-gpu.o obj-$(CONFIG_POWER_SEQUENCING_PCIE_M2) += pwrseq-pcie-m2.o +obj-$(CONFIG_POWER_SEQUENCING_KUNIT) += pwrseq-kunit.o diff --git a/drivers/power/sequencing/pwrseq-kunit.c b/drivers/power/sequencing/pwrseq-kunit.c new file mode 100644 index 000000000000..c2899f3b16e6 --- /dev/null +++ b/drivers/power/sequencing/pwrseq-kunit.c @@ -0,0 +1,1497 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2026 Qualcomm Technologies, Inc. and/or its subsidiaries + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#define PWRSEQ_TEST_PARENT "pwrseq-test-parent" +#define PWRSEQ_TEST_CONSUMER "pwrseq-test-consumer" + +#define PWRSEQ_SWNODE_TEST_PROVIDER "pwrseq-swnode-test-provider" +#define PWRSEQ_SWNODE_TEST_PROVIDER_2 "pwrseq-swnode-test-provider-2" +#define PWRSEQ_PROBE_ORDER_TEST_CONSUMER "pwrseq-probe-order-test-consumer" +#define PWRSEQ_PROBE_DEFER_TEST_CONSUMER "pwrseq-probe-defer-test-consumer" + +static const struct software_node pwrseq_test_provider_swnode = { + .name = "pwrseq-test-provider", +}; + +KUNIT_DEFINE_ACTION_WRAPPER(pwrseq_device_unregister_wrapper, + pwrseq_device_unregister, + struct pwrseq_device *); + +static struct pwrseq_device * +kunit_pwrseq_device_register(struct kunit *test, + const struct pwrseq_config *config) +{ + struct pwrseq_device *pwrseq; + int ret; + + pwrseq = pwrseq_device_register(config); + if (IS_ERR(pwrseq)) + return pwrseq; + + ret = kunit_add_action_or_reset(test, pwrseq_device_unregister_wrapper, + pwrseq); + if (ret) + return ERR_PTR(ret); + + return pwrseq; +} + +KUNIT_DEFINE_ACTION_WRAPPER(pwrseq_put_wrapper, pwrseq_put, + struct pwrseq_desc *); + +static struct pwrseq_desc * +kunit_pwrseq_get(struct kunit *test, struct device *dev, const char *target) +{ + struct pwrseq_desc *desc; + int ret; + + desc = pwrseq_get(dev, target); + if (IS_ERR(desc)) + return desc; + + ret = kunit_add_action_or_reset(test, pwrseq_put_wrapper, desc); + if (ret) + return ERR_PTR(ret); + + return desc; +} + +struct pwrseq_test_ctx { + const char *consumer_name; + int enable_count_a; + int enable_count_b; + int disable_count_a; + int disable_count_b; + int enable_calls_a; + int post_enable_calls; + bool enable_error_a; + bool enable_error_b; + bool post_enable_error; +}; + +static int pwrseq_test_no_match(struct pwrseq_device *pwrseq, + struct device *dev) +{ + return PWRSEQ_NO_MATCH; +} + +static int pwrseq_test_match_by_name(struct pwrseq_device *pwrseq, + struct device *dev) +{ + struct pwrseq_test_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); + + return strcmp(dev_name(dev), ctx->consumer_name) == 0 ? + PWRSEQ_MATCH_OK : PWRSEQ_NO_MATCH; +} + +static int pwrseq_test_match_always(struct pwrseq_device *pwrseq, + struct device *dev) +{ + return PWRSEQ_MATCH_OK; +} + +static int pwrseq_test_enable_a(struct pwrseq_device *pwrseq) +{ + struct pwrseq_test_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); + + ctx->enable_calls_a++; + + if (ctx->enable_error_a) + return -EIO; + + ctx->enable_count_a++; + + return 0; +} + +static int pwrseq_test_disable_a(struct pwrseq_device *pwrseq) +{ + struct pwrseq_test_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); + + ctx->disable_count_a++; + + return 0; +} + +static int pwrseq_test_enable_b(struct pwrseq_device *pwrseq) +{ + struct pwrseq_test_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); + + if (ctx->enable_error_b) + return -EIO; + + ctx->enable_count_b++; + + return 0; +} + +static int pwrseq_test_disable_b(struct pwrseq_device *pwrseq) +{ + struct pwrseq_test_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); + + ctx->disable_count_b++; + + return 0; +} + +static int pwrseq_test_post_enable(struct pwrseq_device *pwrseq) +{ + struct pwrseq_test_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); + + ctx->post_enable_calls++; + + return ctx->post_enable_error ? -EIO : 0; +} + +static int pwrseq_test_parent_probe(struct platform_device *pdev) +{ + return 0; +} + +static struct platform_driver pwrseq_test_parent_driver = { + .probe = pwrseq_test_parent_probe, + .driver = { + .name = PWRSEQ_TEST_PARENT, + }, +}; + +static int pwrseq_test_parent_init(struct kunit *test) +{ + static const struct platform_device_info pdevinfo = { + .name = PWRSEQ_TEST_PARENT, + .id = PLATFORM_DEVID_NONE, + }; + + struct platform_device *pdev; + bool bound; + int ret; + + ret = kunit_platform_driver_register(test, &pwrseq_test_parent_driver); + KUNIT_ASSERT_EQ(test, ret, 0); + + pdev = kunit_platform_device_register_full(test, &pdevinfo); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pdev); + + wait_for_device_probe(); + scoped_guard(device, &pdev->dev) + bound = device_is_bound(&pdev->dev); + + KUNIT_ASSERT_TRUE(test, bound); + + test->priv = pdev; + + return 0; +} + +/* + * Test that registering a power sequencer whose unit dependency graph + * contains a two-node cycle (A -> B -> A) is rejected with -EINVAL. + */ +static void pwrseq_circular_deps(struct kunit *test) +{ + const struct pwrseq_unit_data *unit_a_deps[2] = { }; + const struct pwrseq_unit_data *unit_b_deps[2] = { }; + const struct pwrseq_target_data *targets[2] = { }; + struct platform_device *pdev = test->priv; + struct pwrseq_unit_data unit_a, unit_b; + struct pwrseq_target_data target; + struct pwrseq_device *pwrseq; + struct pwrseq_config config; + + unit_a = (struct pwrseq_unit_data){ + .name = "unit-a", + .deps = unit_a_deps, + }; + + unit_b = (struct pwrseq_unit_data){ + .name = "unit-b", + .deps = unit_b_deps, + }; + + unit_a_deps[0] = &unit_b; + unit_b_deps[0] = &unit_a; + + target = (struct pwrseq_target_data){ + .name = "test-target", + .unit = &unit_a, + }; + + targets[0] = ⌖ + + config = (struct pwrseq_config){ + .parent = &pdev->dev, + .match = pwrseq_test_no_match, + .targets = targets, + }; + + kunit_warning_suppress(test) { + pwrseq = kunit_pwrseq_device_register(test, &config); + KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 1); + } + + KUNIT_EXPECT_TRUE(test, IS_ERR(pwrseq)); + KUNIT_EXPECT_EQ(test, PTR_ERR(pwrseq), -EINVAL); +} + +/* + * Test that a longer chain cycle (A -> B -> C -> D -> A) is also rejected + * with -EINVAL. + */ +static void pwrseq_circular_deps_chain(struct kunit *test) +{ + struct pwrseq_unit_data unit_a, unit_b, unit_c, unit_d; + const struct pwrseq_unit_data *unit_a_deps[2] = { }; + const struct pwrseq_unit_data *unit_b_deps[2] = { }; + const struct pwrseq_unit_data *unit_c_deps[2] = { }; + const struct pwrseq_unit_data *unit_d_deps[2] = { }; + const struct pwrseq_target_data *targets[2] = { }; + struct platform_device *pdev = test->priv; + struct pwrseq_target_data target; + struct pwrseq_device *pwrseq; + struct pwrseq_config config; + + unit_a = (struct pwrseq_unit_data){ + .name = "unit-a", + .deps = unit_a_deps, + }; + unit_b = (struct pwrseq_unit_data){ + .name = "unit-b", + .deps = unit_b_deps, + }; + unit_c = (struct pwrseq_unit_data){ + .name = "unit-c", + .deps = unit_c_deps, + }; + unit_d = (struct pwrseq_unit_data){ + .name = "unit-d", + .deps = unit_d_deps, + }; + + unit_a_deps[0] = &unit_b; + unit_b_deps[0] = &unit_c; + unit_c_deps[0] = &unit_d; + unit_d_deps[0] = &unit_a; + + target = (struct pwrseq_target_data){ + .name = "test-target", + .unit = &unit_a, + }; + + targets[0] = ⌖ + + config = (struct pwrseq_config){ + .parent = &pdev->dev, + .match = pwrseq_test_no_match, + .targets = targets, + }; + + kunit_warning_suppress(test) { + pwrseq = kunit_pwrseq_device_register(test, &config); + KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 1); + } + + KUNIT_EXPECT_TRUE(test, IS_ERR(pwrseq)); + KUNIT_EXPECT_EQ(test, PTR_ERR(pwrseq), -EINVAL); +} + +/* + * Test that a valid acyclic dependency graph (A -> B) registers successfully. + */ +static void pwrseq_register_valid(struct kunit *test) +{ + const struct pwrseq_unit_data *unit_a_deps[2] = { }; + const struct pwrseq_target_data *targets[2] = { }; + struct platform_device *pdev = test->priv; + struct pwrseq_unit_data unit_a, unit_b; + struct pwrseq_target_data target; + struct pwrseq_device *pwrseq; + struct pwrseq_config config; + + unit_b = (struct pwrseq_unit_data){ + .name = "unit-b", + }; + + unit_a_deps[0] = &unit_b; + unit_a = (struct pwrseq_unit_data){ + .name = "unit-a", + .deps = unit_a_deps, + }; + + target = (struct pwrseq_target_data){ + .name = "test-target", + .unit = &unit_a, + }; + + targets[0] = ⌖ + + config = (struct pwrseq_config){ + .parent = &pdev->dev, + .match = pwrseq_test_no_match, + .targets = targets, + }; + + pwrseq = kunit_pwrseq_device_register(test, &config); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pwrseq); +} + +/* + * Test that registration is rejected with -EINVAL when .parent, .match or + * .targets is missing, or when .targets is a non-NULL but empty array. + */ +static void pwrseq_register_invalid_args(struct kunit *test) +{ + const struct pwrseq_target_data *empty_targets[1] = { }; + const struct pwrseq_target_data *targets[2] = { }; + struct platform_device *pdev = test->priv; + struct pwrseq_target_data target; + struct pwrseq_unit_data unit; + struct pwrseq_device *pwrseq; + struct pwrseq_config config; + + unit = (struct pwrseq_unit_data){ .name = "unit-a" }; + target = (struct pwrseq_target_data){ + .name = "test-target", + .unit = &unit, + }; + targets[0] = ⌖ + + config = (struct pwrseq_config){ + .parent = &pdev->dev, + .match = pwrseq_test_no_match, + .targets = targets, + }; + + config.parent = NULL; + pwrseq = kunit_pwrseq_device_register(test, &config); + KUNIT_EXPECT_TRUE(test, IS_ERR(pwrseq)); + KUNIT_EXPECT_EQ(test, PTR_ERR(pwrseq), -EINVAL); + config.parent = &pdev->dev; + + config.match = NULL; + pwrseq = kunit_pwrseq_device_register(test, &config); + KUNIT_EXPECT_TRUE(test, IS_ERR(pwrseq)); + KUNIT_EXPECT_EQ(test, PTR_ERR(pwrseq), -EINVAL); + config.match = pwrseq_test_no_match; + + config.targets = NULL; + pwrseq = kunit_pwrseq_device_register(test, &config); + KUNIT_EXPECT_TRUE(test, IS_ERR(pwrseq)); + KUNIT_EXPECT_EQ(test, PTR_ERR(pwrseq), -EINVAL); + + config.targets = empty_targets; + pwrseq = kunit_pwrseq_device_register(test, &config); + KUNIT_EXPECT_TRUE(test, IS_ERR(pwrseq)); + KUNIT_EXPECT_EQ(test, PTR_ERR(pwrseq), -EINVAL); +} + +/* + * Test that a target without a unit is rejected with -EINVAL. + */ +static void pwrseq_register_target_without_unit(struct kunit *test) +{ + const struct pwrseq_target_data *targets[2] = { }; + struct platform_device *pdev = test->priv; + struct pwrseq_target_data target; + struct pwrseq_device *pwrseq; + struct pwrseq_config config; + + target = (struct pwrseq_target_data){ + .name = "test-target", + .unit = NULL, + }; + targets[0] = ⌖ + + config = (struct pwrseq_config){ + .parent = &pdev->dev, + .match = pwrseq_test_no_match, + .targets = targets, + }; + + pwrseq = kunit_pwrseq_device_register(test, &config); + KUNIT_EXPECT_TRUE(test, IS_ERR(pwrseq)); + KUNIT_EXPECT_EQ(test, PTR_ERR(pwrseq), -EINVAL); +} + +/* + * Test that enabling and disabling a single-unit target increments and + * decrements enable_count correctly and fires the enable/disable callbacks + * exactly once. + */ +static void pwrseq_enable_disable(struct kunit *test) +{ + const struct pwrseq_target_data *targets[2] = { }; + struct platform_device *pdev = test->priv; + struct platform_device_info pdevinfo; + struct pwrseq_target_data target; + struct pwrseq_test_ctx *ctx; + struct pwrseq_unit_data unit; + struct pwrseq_device *pwrseq; + struct pwrseq_config config; + struct pwrseq_desc *desc; + int ret; + + ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx); + + unit = (struct pwrseq_unit_data){ + .name = "unit-a", + .enable = pwrseq_test_enable_a, + .disable = pwrseq_test_disable_a, + }; + + target = (struct pwrseq_target_data){ + .name = "test-target", + .unit = &unit, + }; + + targets[0] = ⌖ + + ctx->consumer_name = PWRSEQ_TEST_CONSUMER; + + config = (struct pwrseq_config){ + .parent = &pdev->dev, + .drvdata = ctx, + .match = pwrseq_test_match_by_name, + .targets = targets, + }; + + pwrseq = kunit_pwrseq_device_register(test, &config); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pwrseq); + + pdevinfo = (struct platform_device_info){ + .name = PWRSEQ_TEST_CONSUMER, + .id = PLATFORM_DEVID_NONE, + }; + + pdev = kunit_platform_device_register_full(test, &pdevinfo); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pdev); + + desc = kunit_pwrseq_get(test, &pdev->dev, "test-target"); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, desc); + + ret = pwrseq_enable(desc); + KUNIT_EXPECT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, ctx->enable_count_a, 1); + KUNIT_EXPECT_EQ(test, ctx->disable_count_a, 0); + + ret = pwrseq_disable(desc); + KUNIT_EXPECT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, ctx->enable_count_a, 1); + KUNIT_EXPECT_EQ(test, ctx->disable_count_a, 1); +} + +/* + * Test that two consumers sharing a common dependency unit cause it to be + * enabled only once and disabled only once, while enable_count tracks each + * consumer correctly. + */ +static void pwrseq_shared_deps(struct kunit *test) +{ + const struct pwrseq_unit_data *unit_a_deps[2] = { }; + const struct pwrseq_unit_data *unit_b_deps[2] = { }; + const struct pwrseq_target_data *targets[3] = { }; + struct pwrseq_unit_data dep_unit, unit_a, unit_b; + struct pwrseq_target_data target_a, target_b; + struct platform_device *parent = test->priv; + struct platform_device *pdev_a, *pdev_b; + struct platform_device_info pdevinfo; + struct pwrseq_desc *desc_a, *desc_b; + struct pwrseq_device *pwrseq; + struct pwrseq_config config; + struct pwrseq_test_ctx *ctx; + int ret; + + ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx); + + dep_unit = (struct pwrseq_unit_data){ + .name = "dep", + .enable = pwrseq_test_enable_a, + .disable = pwrseq_test_disable_a, + }; + + unit_a_deps[0] = &dep_unit; + unit_a = (struct pwrseq_unit_data){ + .name = "unit-a", + .deps = unit_a_deps, + .enable = pwrseq_test_enable_b, + .disable = pwrseq_test_disable_b, + }; + + unit_b_deps[0] = &dep_unit; + unit_b = (struct pwrseq_unit_data){ + .name = "unit-b", + .deps = unit_b_deps, + }; + + target_a = (struct pwrseq_target_data){ + .name = "target-a", + .unit = &unit_a, + }; + target_b = (struct pwrseq_target_data){ + .name = "target-b", + .unit = &unit_b, + }; + + targets[0] = &target_a; + targets[1] = &target_b; + + ctx->consumer_name = PWRSEQ_TEST_CONSUMER "-a"; + + config = (struct pwrseq_config){ + .parent = &parent->dev, + .drvdata = ctx, + .match = pwrseq_test_match_by_name, + .targets = targets, + }; + + pwrseq = kunit_pwrseq_device_register(test, &config); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pwrseq); + + pdevinfo = (struct platform_device_info){ + .name = PWRSEQ_TEST_CONSUMER "-a", + .id = PLATFORM_DEVID_NONE, + }; + pdev_a = kunit_platform_device_register_full(test, &pdevinfo); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pdev_a); + + pdevinfo = (struct platform_device_info){ + .name = PWRSEQ_TEST_CONSUMER "-b", + .id = PLATFORM_DEVID_NONE, + }; + pdev_b = kunit_platform_device_register_full(test, &pdevinfo); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pdev_b); + + /* + * match_by_name() only matches one consumer name at a time, so + * reconfigure ctx->consumer_name between the two pwrseq_get() calls + * below to acquire descriptors for both consumer-a and consumer-b. + */ + desc_a = kunit_pwrseq_get(test, &pdev_a->dev, "target-a"); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, desc_a); + + ctx->consumer_name = PWRSEQ_TEST_CONSUMER "-b"; + desc_b = kunit_pwrseq_get(test, &pdev_b->dev, "target-b"); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, desc_b); + + ret = pwrseq_enable(desc_a); + KUNIT_EXPECT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, ctx->enable_count_a, 1); + KUNIT_EXPECT_EQ(test, ctx->enable_count_b, 1); + + ret = pwrseq_enable(desc_b); + KUNIT_EXPECT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, ctx->enable_count_a, 1); + + ret = pwrseq_disable(desc_a); + KUNIT_EXPECT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, ctx->disable_count_a, 0); + KUNIT_EXPECT_EQ(test, ctx->disable_count_b, 1); + + ret = pwrseq_disable(desc_b); + KUNIT_EXPECT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, ctx->disable_count_a, 1); +} + +/* + * Test that calling pwrseq_enable() twice on the same descriptor has no + * effect. + */ +static void pwrseq_enable_idempotent(struct kunit *test) +{ + const struct pwrseq_target_data *targets[2] = { }; + struct platform_device *pdev = test->priv; + struct platform_device_info pdevinfo; + struct pwrseq_target_data target; + struct pwrseq_device *pwrseq; + struct pwrseq_unit_data unit; + struct pwrseq_config config; + struct pwrseq_test_ctx *ctx; + struct pwrseq_desc *desc; + int ret; + + ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx); + + unit = (struct pwrseq_unit_data){ + .name = "unit-a", + .enable = pwrseq_test_enable_a, + .disable = pwrseq_test_disable_a, + }; + + target = (struct pwrseq_target_data){ + .name = "test-target", + .unit = &unit, + }; + + targets[0] = ⌖ + + ctx->consumer_name = PWRSEQ_TEST_CONSUMER; + + config = (struct pwrseq_config){ + .parent = &pdev->dev, + .drvdata = ctx, + .match = pwrseq_test_match_by_name, + .targets = targets, + }; + + pwrseq = kunit_pwrseq_device_register(test, &config); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pwrseq); + + pdevinfo = (struct platform_device_info){ + .name = PWRSEQ_TEST_CONSUMER, + .id = PLATFORM_DEVID_NONE, + }; + + pdev = kunit_platform_device_register_full(test, &pdevinfo); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pdev); + + desc = kunit_pwrseq_get(test, &pdev->dev, "test-target"); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, desc); + + ret = pwrseq_enable(desc); + KUNIT_EXPECT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, ctx->enable_count_a, 1); + + /* Second power_on on same descriptor must be a no-op. */ + ret = pwrseq_enable(desc); + KUNIT_EXPECT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, ctx->enable_count_a, 1); + + ret = pwrseq_disable(desc); + KUNIT_EXPECT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, ctx->disable_count_a, 1); +} + +/* + * Test that when a dependency unit's enable() callback fails, the error + * propagates to the caller, the top unit's enable callback is never reached, + * and target->post_enable() is never invoked. + */ +static void pwrseq_enable_enable_error(struct kunit *test) +{ + const struct pwrseq_target_data *targets[2] = { }; + const struct pwrseq_unit_data *top_deps[2] = { }; + struct pwrseq_unit_data dep_unit, top_unit; + struct platform_device *pdev = test->priv; + struct platform_device_info pdevinfo; + struct pwrseq_target_data target; + struct pwrseq_device *pwrseq; + struct pwrseq_config config; + struct pwrseq_test_ctx *ctx; + struct pwrseq_desc *desc; + int ret; + + ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx); + + dep_unit = (struct pwrseq_unit_data){ + .name = "dep", + .enable = pwrseq_test_enable_a, + }; + + top_deps[0] = &dep_unit; + top_unit = (struct pwrseq_unit_data){ + .name = "top", + .deps = top_deps, + .enable = pwrseq_test_enable_b, + }; + + target = (struct pwrseq_target_data){ + .name = "test-target", + .unit = &top_unit, + .post_enable = pwrseq_test_post_enable, + }; + + targets[0] = ⌖ + + ctx->consumer_name = PWRSEQ_TEST_CONSUMER; + ctx->enable_error_a = true; + + config = (struct pwrseq_config){ + .parent = &pdev->dev, + .drvdata = ctx, + .match = pwrseq_test_match_by_name, + .targets = targets, + }; + + pwrseq = kunit_pwrseq_device_register(test, &config); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pwrseq); + + pdevinfo = (struct platform_device_info){ + .name = PWRSEQ_TEST_CONSUMER, + .id = PLATFORM_DEVID_NONE, + }; + + pdev = kunit_platform_device_register_full(test, &pdevinfo); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pdev); + + desc = kunit_pwrseq_get(test, &pdev->dev, "test-target"); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, desc); + + ret = pwrseq_enable(desc); + KUNIT_EXPECT_EQ(test, ret, -EIO); + /* + * Dependency's enable() was attempted but failed, so it never + * incremented. + */ + KUNIT_EXPECT_EQ(test, ctx->enable_calls_a, 1); + KUNIT_EXPECT_EQ(test, ctx->enable_count_a, 0); + /* Target unit's enable() was never reached. */ + KUNIT_EXPECT_EQ(test, ctx->enable_count_b, 0); + /* Target's .post_enable() must not run on an unpowered sequencer. */ + KUNIT_EXPECT_EQ(test, ctx->post_enable_calls, 0); +} + +/* + * Test that when the top unit's own enable() callback fails after its + * dependency's enable() already succeeded, the dependency is rolled back. + */ +static void pwrseq_enable_rollback_on_top_failure(struct kunit *test) +{ + const struct pwrseq_target_data *targets[2] = { }; + const struct pwrseq_unit_data *top_deps[2] = { }; + struct pwrseq_unit_data dep_unit, top_unit; + struct platform_device *pdev = test->priv; + struct platform_device_info pdevinfo; + struct pwrseq_target_data target; + struct pwrseq_device *pwrseq; + struct pwrseq_config config; + struct pwrseq_test_ctx *ctx; + struct pwrseq_desc *desc; + int ret; + + ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx); + + dep_unit = (struct pwrseq_unit_data){ + .name = "dep", + .enable = pwrseq_test_enable_a, + .disable = pwrseq_test_disable_a, + }; + + top_deps[0] = &dep_unit; + top_unit = (struct pwrseq_unit_data){ + .name = "top", + .deps = top_deps, + .enable = pwrseq_test_enable_b, + }; + + target = (struct pwrseq_target_data){ + .name = "test-target", + .unit = &top_unit, + }; + + targets[0] = ⌖ + + ctx->consumer_name = PWRSEQ_TEST_CONSUMER; + ctx->enable_error_b = true; + + config = (struct pwrseq_config){ + .parent = &pdev->dev, + .drvdata = ctx, + .match = pwrseq_test_match_by_name, + .targets = targets, + }; + + pwrseq = kunit_pwrseq_device_register(test, &config); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pwrseq); + + pdevinfo = (struct platform_device_info){ + .name = PWRSEQ_TEST_CONSUMER, + .id = PLATFORM_DEVID_NONE, + }; + + pdev = kunit_platform_device_register_full(test, &pdevinfo); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pdev); + + desc = kunit_pwrseq_get(test, &pdev->dev, "test-target"); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, desc); + + ret = pwrseq_enable(desc); + KUNIT_EXPECT_EQ(test, ret, -EIO); + /* + * Dependency was enabled, then rolled back once top's own enable + * failed. + */ + KUNIT_EXPECT_EQ(test, ctx->enable_count_a, 1); + KUNIT_EXPECT_EQ(test, ctx->disable_count_a, 1); + /* + * Target unit's own enable never incremented since it returned an + * error. + */ + KUNIT_EXPECT_EQ(test, ctx->enable_count_b, 0); +} + +/* + * Test target->post_enable() on both the success and failure paths: it must + * run exactly once per pwrseq_enable() call that actually powers the target + * unit on, and a failing post_enable() must roll the unit back and clear + * powered_on so a subsequent pwrseq_enable() is called again. + */ +static void pwrseq_enable_post_enable(struct kunit *test) +{ + const struct pwrseq_target_data *targets[2] = { }; + struct platform_device *pdev = test->priv; + struct platform_device_info pdevinfo; + struct pwrseq_target_data target; + struct pwrseq_unit_data unit; + struct pwrseq_device *pwrseq; + struct pwrseq_config config; + struct pwrseq_test_ctx *ctx; + struct pwrseq_desc *desc; + int ret; + + ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx); + + unit = (struct pwrseq_unit_data){ + .name = "unit-a", + .enable = pwrseq_test_enable_a, + .disable = pwrseq_test_disable_a, + }; + + target = (struct pwrseq_target_data){ + .name = "test-target", + .unit = &unit, + .post_enable = pwrseq_test_post_enable, + }; + + targets[0] = ⌖ + + ctx->consumer_name = PWRSEQ_TEST_CONSUMER; + + config = (struct pwrseq_config){ + .parent = &pdev->dev, + .drvdata = ctx, + .match = pwrseq_test_match_by_name, + .targets = targets, + }; + + pwrseq = kunit_pwrseq_device_register(test, &config); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pwrseq); + + pdevinfo = (struct platform_device_info){ + .name = PWRSEQ_TEST_CONSUMER, + .id = PLATFORM_DEVID_NONE, + }; + + pdev = kunit_platform_device_register_full(test, &pdevinfo); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pdev); + + desc = kunit_pwrseq_get(test, &pdev->dev, "test-target"); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, desc); + + /* Success path: post_enable() runs once and reports success. */ + ret = pwrseq_enable(desc); + KUNIT_EXPECT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, ctx->post_enable_calls, 1); + KUNIT_EXPECT_EQ(test, ctx->enable_count_a, 1); + + ret = pwrseq_disable(desc); + KUNIT_ASSERT_EQ(test, ret, 0); + + /* Failure path: post_enable() fails, unit is rolled back. */ + ctx->post_enable_error = true; + ret = pwrseq_enable(desc); + KUNIT_EXPECT_EQ(test, ret, -EIO); + KUNIT_EXPECT_EQ(test, ctx->post_enable_calls, 2); + KUNIT_EXPECT_EQ(test, ctx->enable_count_a, 2); + KUNIT_EXPECT_EQ(test, ctx->disable_count_a, 2); + + /* powered_on must have been cleared. */ + ret = pwrseq_enable(desc); + KUNIT_EXPECT_EQ(test, ret, -EIO); + KUNIT_EXPECT_EQ(test, ctx->enable_count_a, 3); +} + +/* + * Test that pwrseq_get() returns -ENOENT when the matched provider does not + * have the requested target name. + */ +static void pwrseq_get_target_not_found(struct kunit *test) +{ + const struct pwrseq_target_data *targets[2] = { }; + struct platform_device *pdev = test->priv; + struct platform_device_info pdevinfo; + struct pwrseq_target_data target; + struct pwrseq_unit_data unit; + struct pwrseq_device *pwrseq; + struct pwrseq_config config; + struct pwrseq_desc *desc; + + unit = (struct pwrseq_unit_data){ + .name = "unit-a", + }; + + target = (struct pwrseq_target_data){ + .name = "real-target", + .unit = &unit, + }; + + targets[0] = ⌖ + + config = (struct pwrseq_config){ + .parent = &pdev->dev, + .match = pwrseq_test_match_always, + .targets = targets, + }; + + pwrseq = kunit_pwrseq_device_register(test, &config); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pwrseq); + + pdevinfo = (struct platform_device_info){ + .name = PWRSEQ_TEST_CONSUMER, + .id = PLATFORM_DEVID_NONE, + }; + + pdev = kunit_platform_device_register_full(test, &pdevinfo); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pdev); + + desc = kunit_pwrseq_get(test, &pdev->dev, "nonexistent-target"); + KUNIT_EXPECT_TRUE(test, IS_ERR(desc)); + KUNIT_EXPECT_EQ(test, PTR_ERR(desc), -ENOENT); +} + +/* + * Test that pwrseq_put() on a descriptor that is still powered on disables + * it as part of tear-down, instead of leaking the power-on state. + */ +static void pwrseq_put_disables_powered_desc(struct kunit *test) +{ + const struct pwrseq_target_data *targets[2] = { }; + struct platform_device *pdev = test->priv; + struct platform_device_info pdevinfo; + struct pwrseq_target_data target; + struct pwrseq_unit_data unit; + struct pwrseq_device *pwrseq; + struct pwrseq_test_ctx *ctx; + struct pwrseq_config config; + struct pwrseq_desc *desc; + int ret; + + ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx); + + unit = (struct pwrseq_unit_data){ + .name = "unit-a", + .enable = pwrseq_test_enable_a, + .disable = pwrseq_test_disable_a, + }; + + target = (struct pwrseq_target_data){ + .name = "test-target", + .unit = &unit, + }; + + targets[0] = ⌖ + + config = (struct pwrseq_config){ + .parent = &pdev->dev, + .drvdata = ctx, + .match = pwrseq_test_match_always, + .targets = targets, + }; + + pwrseq = kunit_pwrseq_device_register(test, &config); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pwrseq); + + pdevinfo = (struct platform_device_info){ + .name = PWRSEQ_TEST_CONSUMER, + .id = PLATFORM_DEVID_NONE, + }; + + pdev = kunit_platform_device_register_full(test, &pdevinfo); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pdev); + + desc = kunit_pwrseq_get(test, &pdev->dev, "test-target"); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, desc); + + ret = pwrseq_enable(desc); + KUNIT_ASSERT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, ctx->disable_count_a, 0); + + /* + * Run the deferred pwrseq_put() action now instead of at test + * teardown, so its effect can be asserted below. + */ + kunit_release_action(test, pwrseq_put_wrapper, desc); + KUNIT_EXPECT_EQ(test, ctx->disable_count_a, 1); +} + +/* + * Test that pwrseq_device_unregister() on a target with an active user + * triggers "REMOVING POWER SEQUENCER WITH ACTIVE USERS", and that both + * pwrseq_enable() and pwrseq_disable() start returning -ENODEV for + * previously acquired descriptors afterwards. + */ +static void pwrseq_unregister_with_active_user(struct kunit *test) +{ + struct platform_device *pdev = test->priv, *cons_a, *cons_b; + const struct pwrseq_target_data *targets[2] = { }; + struct platform_device_info pdevinfo; + struct pwrseq_desc *desc_a, *desc_b; + struct pwrseq_target_data target; + struct pwrseq_unit_data unit; + struct pwrseq_device *pwrseq; + struct pwrseq_test_ctx *ctx; + struct pwrseq_config config; + bool bound; + int ret; + + ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx); + + unit = (struct pwrseq_unit_data){ + .name = "unit-a", + .enable = pwrseq_test_enable_a, + .disable = pwrseq_test_disable_a, + }; + + target = (struct pwrseq_target_data){ + .name = "test-target", + .unit = &unit, + }; + + targets[0] = ⌖ + + config = (struct pwrseq_config){ + .parent = &pdev->dev, + .drvdata = ctx, + .match = pwrseq_test_match_always, + .targets = targets, + }; + + pwrseq = kunit_pwrseq_device_register(test, &config); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pwrseq); + + pdevinfo = (struct platform_device_info){ + .name = PWRSEQ_TEST_CONSUMER, + .id = 0, + }; + cons_a = kunit_platform_device_register_full(test, &pdevinfo); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons_a); + + wait_for_device_probe(); + scoped_guard(device, &cons_a->dev) + bound = device_is_bound(&cons_a->dev); + + KUNIT_ASSERT_FALSE(test, bound); + + pdevinfo.id = 1; + cons_b = kunit_platform_device_register_full(test, &pdevinfo); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons_b); + + wait_for_device_probe(); + scoped_guard(device, &cons_b->dev) + bound = device_is_bound(&cons_b->dev); + + KUNIT_ASSERT_FALSE(test, bound); + + desc_a = kunit_pwrseq_get(test, &cons_a->dev, "test-target"); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, desc_a); + desc_b = kunit_pwrseq_get(test, &cons_b->dev, "test-target"); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, desc_b); + + ret = pwrseq_enable(desc_a); + KUNIT_ASSERT_EQ(test, ret, 0); + + kunit_warning_suppress(test) { + kunit_release_action(test, pwrseq_device_unregister_wrapper, pwrseq); + KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 1); + } + + ret = pwrseq_enable(desc_b); + KUNIT_EXPECT_EQ(test, ret, -ENODEV); + + ret = pwrseq_disable(desc_a); + KUNIT_EXPECT_EQ(test, ret, -ENODEV); +} + +/* + * Test that pwrseq_to_device() returns the pwrseq provider's own device, + * and NULL for a NULL descriptor. + */ +static void pwrseq_to_device_test(struct kunit *test) +{ + struct platform_device *parent = test->priv, *cons; + const struct pwrseq_target_data *targets[2] = { }; + struct platform_device_info pdevinfo; + struct pwrseq_target_data target; + struct pwrseq_unit_data unit; + struct pwrseq_device *pwrseq; + struct pwrseq_config config; + struct pwrseq_desc *desc; + struct device *dev; + + unit = (struct pwrseq_unit_data){ .name = "unit-a" }; + target = (struct pwrseq_target_data){ + .name = "test-target", + .unit = &unit, + }; + targets[0] = ⌖ + + config = (struct pwrseq_config){ + .parent = &parent->dev, + .match = pwrseq_test_match_always, + .targets = targets, + }; + + pwrseq = kunit_pwrseq_device_register(test, &config); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pwrseq); + + pdevinfo = (struct platform_device_info){ + .name = PWRSEQ_TEST_CONSUMER, + .id = PLATFORM_DEVID_NONE, + }; + + cons = kunit_platform_device_register_full(test, &pdevinfo); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons); + + desc = kunit_pwrseq_get(test, &cons->dev, "test-target"); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, desc); + + dev = pwrseq_to_device(desc); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev); + KUNIT_EXPECT_PTR_EQ(test, dev->parent, &parent->dev); + KUNIT_EXPECT_TRUE(test, device_is_registered(dev)); + + KUNIT_EXPECT_PTR_EQ(test, pwrseq_to_device(NULL), NULL); +} + +static struct kunit_case pwrseq_tests[] = { + KUNIT_CASE(pwrseq_circular_deps), + KUNIT_CASE(pwrseq_circular_deps_chain), + KUNIT_CASE(pwrseq_register_valid), + KUNIT_CASE(pwrseq_register_invalid_args), + KUNIT_CASE(pwrseq_register_target_without_unit), + KUNIT_CASE(pwrseq_enable_disable), + KUNIT_CASE(pwrseq_shared_deps), + KUNIT_CASE(pwrseq_enable_idempotent), + KUNIT_CASE(pwrseq_enable_enable_error), + KUNIT_CASE(pwrseq_enable_rollback_on_top_failure), + KUNIT_CASE(pwrseq_enable_post_enable), + KUNIT_CASE(pwrseq_get_target_not_found), + KUNIT_CASE(pwrseq_put_disables_powered_desc), + KUNIT_CASE(pwrseq_unregister_with_active_user), + KUNIT_CASE(pwrseq_to_device_test), + { } +}; + +static struct kunit_suite pwrseq_test_suite = { + .name = "pwrseq", + .init = pwrseq_test_parent_init, + .test_cases = pwrseq_tests, +}; + +/* + * Match a consumer to this test provider by resolving its "pwrseq-provider" + * software-node reference and comparing it to the provider's own fwnode. + */ +static int pwrseq_test_match_by_swnode_ref(struct pwrseq_device *pwrseq, + struct device *dev) +{ + const struct software_node *provider_swnode; + struct fwnode_handle *provider_fwnode, *ref; + bool match; + + if (!dev_fwnode(dev)) + return PWRSEQ_NO_MATCH; + + ref = fwnode_find_reference(dev_fwnode(dev), "pwrseq-provider", 0); + if (IS_ERR_OR_NULL(ref)) + return PWRSEQ_NO_MATCH; + + provider_swnode = pwrseq_device_get_drvdata(pwrseq); + provider_fwnode = software_node_fwnode(provider_swnode); + match = (ref == provider_fwnode); + fwnode_handle_put(ref); + + return match ? PWRSEQ_MATCH_OK : PWRSEQ_NO_MATCH; +} + +static const struct pwrseq_unit_data pwrseq_swnode_test_unit = { + .name = "test-target", +}; + +static const struct pwrseq_target_data pwrseq_swnode_test_target = { + .name = "test-target", + .unit = &pwrseq_swnode_test_unit, +}; + +static const struct pwrseq_target_data *pwrseq_swnode_test_targets[] = { + &pwrseq_swnode_test_target, + NULL, +}; + +struct pwrseq_swnode_provider_pdata { + const struct software_node *provider_swnode; +}; + +static int pwrseq_swnode_provider_probe(struct platform_device *pdev) +{ + const struct pwrseq_swnode_provider_pdata *pdata = dev_get_platdata(&pdev->dev); + struct pwrseq_config config; + + config = (struct pwrseq_config){ + .parent = &pdev->dev, + .drvdata = (void *)pdata->provider_swnode, + .match = pwrseq_test_match_by_swnode_ref, + .targets = pwrseq_swnode_test_targets, + }; + + return PTR_ERR_OR_ZERO(devm_pwrseq_device_register(&pdev->dev, &config)); +} + +static struct platform_driver pwrseq_swnode_provider_driver = { + .probe = pwrseq_swnode_provider_probe, + .driver = { + .name = PWRSEQ_SWNODE_TEST_PROVIDER, + }, +}; + +static struct platform_driver pwrseq_swnode_provider_driver_2 = { + .probe = pwrseq_swnode_provider_probe, + .driver = { + .name = PWRSEQ_SWNODE_TEST_PROVIDER_2, + }, +}; + +struct pwrseq_probe_order_pdata { + unsigned int probe_count; + int pwrseq_err; +}; + +static const struct pwrseq_probe_order_pdata pwrseq_probe_order_pdata_template; + +static int pwrseq_probe_order_consumer_probe(struct platform_device *pdev) +{ + struct pwrseq_probe_order_pdata *pdata = dev_get_platdata(&pdev->dev); + struct pwrseq_desc *desc; + + pdata->probe_count++; + + desc = devm_pwrseq_get(&pdev->dev, "test-target"); + pdata->pwrseq_err = PTR_ERR_OR_ZERO(desc); + if (IS_ERR(desc)) + return PTR_ERR(desc); + + return 0; +} + +static struct platform_driver pwrseq_probe_order_consumer_driver = { + .probe = pwrseq_probe_order_consumer_probe, + .driver = { + .name = PWRSEQ_PROBE_ORDER_TEST_CONSUMER, + }, +}; + +static struct platform_driver pwrseq_probe_defer_consumer_driver = { + .probe = pwrseq_probe_order_consumer_probe, + .driver = { + .name = PWRSEQ_PROBE_DEFER_TEST_CONSUMER, + }, +}; + +/* + * Verify that driver core orders the probe of a pwrseq consumer after its + * provider. The consumer references the provider through a software node and + * is registered first and we rely on devlink for ordering. + */ +static void pwrseq_swnode_probe_order(struct kunit *test) +{ + struct property_entry properties[2] = { }; + struct pwrseq_probe_order_pdata *pdata; + struct platform_device_info pdevinfo; + struct platform_device *prvd, *cons; + struct fwnode_handle *fwnode; + bool bound = false; + int ret; + + ret = kunit_platform_driver_register(test, &pwrseq_swnode_provider_driver); + KUNIT_ASSERT_EQ(test, ret, 0); + + ret = kunit_platform_driver_register(test, &pwrseq_probe_order_consumer_driver); + KUNIT_ASSERT_EQ(test, ret, 0); + + fwnode = kunit_software_node_register(test, &pwrseq_test_provider_swnode); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode); + + properties[0] = PROPERTY_ENTRY_REF("pwrseq-provider", + &pwrseq_test_provider_swnode); + + pdevinfo = (struct platform_device_info){ + .name = PWRSEQ_PROBE_ORDER_TEST_CONSUMER, + .id = PLATFORM_DEVID_NONE, + .data = &pwrseq_probe_order_pdata_template, + .size_data = sizeof(pwrseq_probe_order_pdata_template), + .properties = properties, + }; + + cons = kunit_platform_device_register_full(test, &pdevinfo); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons); + + wait_for_device_probe(); + scoped_guard(device, &cons->dev) + bound = device_is_bound(&cons->dev); + + KUNIT_ASSERT_FALSE(test, bound); + + pdata = dev_get_platdata(&cons->dev); + KUNIT_ASSERT_EQ(test, pdata->probe_count, 0); + + pdevinfo = (struct platform_device_info){ + .name = PWRSEQ_SWNODE_TEST_PROVIDER, + .id = PLATFORM_DEVID_NONE, + .swnode = &pwrseq_test_provider_swnode, + .data = &(const struct pwrseq_swnode_provider_pdata){ + .provider_swnode = &pwrseq_test_provider_swnode, + }, + .size_data = sizeof(struct pwrseq_swnode_provider_pdata), + }; + + prvd = kunit_platform_device_register_full(test, &pdevinfo); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, prvd); + + wait_for_device_probe(); + + scoped_guard(device, &prvd->dev) + bound = device_is_bound(&prvd->dev); + KUNIT_ASSERT_TRUE(test, bound); + + scoped_guard(device, &cons->dev) + bound = device_is_bound(&cons->dev); + KUNIT_ASSERT_TRUE(test, bound); + + pdata = dev_get_platdata(&cons->dev); + KUNIT_EXPECT_EQ(test, pdata->probe_count, 1); + KUNIT_EXPECT_EQ(test, pdata->pwrseq_err, 0); +} + +/* + * Verify that a pwrseq consumer referencing a provider whose software node is + * not registered yet, defers its probe instead of failing. + * + * The provider software node is deliberately left unregistered when the + * consumer is added. fw_devlink cannot resolve the reference, so it creates no + * supplier link and does not order the consumer - the consumer's probe() runs + * and calls pwrseq_get(), which finds no matching provider and returns + * -EPROBE_DEFER. Once the provider software node and device appear, the + * deferred consumer probes again and binds. + */ +static void pwrseq_swnode_probe_defer_on_unregistered(struct kunit *test) +{ + struct property_entry properties[2] = { }; + struct pwrseq_probe_order_pdata *pdata; + struct platform_device_info pdevinfo; + struct platform_device *prvd, *cons; + struct fwnode_handle *fwnode; + bool bound = false; + int ret; + + ret = kunit_platform_driver_register(test, &pwrseq_swnode_provider_driver_2); + KUNIT_ASSERT_EQ(test, ret, 0); + + ret = kunit_platform_driver_register(test, &pwrseq_probe_defer_consumer_driver); + KUNIT_ASSERT_EQ(test, ret, 0); + + properties[0] = PROPERTY_ENTRY_REF("pwrseq-provider", + &pwrseq_test_provider_swnode); + + pdevinfo = (struct platform_device_info){ + .name = PWRSEQ_PROBE_DEFER_TEST_CONSUMER, + .id = PLATFORM_DEVID_NONE, + .data = &pwrseq_probe_order_pdata_template, + .size_data = sizeof(pwrseq_probe_order_pdata_template), + .properties = properties, + }; + + cons = kunit_platform_device_register_full(test, &pdevinfo); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, cons); + + wait_for_device_probe(); + scoped_guard(device, &cons->dev) + bound = device_is_bound(&cons->dev); + + KUNIT_ASSERT_FALSE(test, bound); + + pdata = dev_get_platdata(&cons->dev); + KUNIT_ASSERT_GT(test, pdata->probe_count, 0); + KUNIT_ASSERT_EQ(test, pdata->pwrseq_err, -EPROBE_DEFER); + + fwnode = kunit_software_node_register(test, &pwrseq_test_provider_swnode); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, fwnode); + + pdevinfo = (struct platform_device_info){ + .name = PWRSEQ_SWNODE_TEST_PROVIDER_2, + .id = PLATFORM_DEVID_NONE, + .swnode = &pwrseq_test_provider_swnode, + .data = &(const struct pwrseq_swnode_provider_pdata){ + .provider_swnode = &pwrseq_test_provider_swnode, + }, + .size_data = sizeof(struct pwrseq_swnode_provider_pdata), + }; + + prvd = kunit_platform_device_register_full(test, &pdevinfo); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, prvd); + + wait_for_device_probe(); + + scoped_guard(device, &prvd->dev) + bound = device_is_bound(&prvd->dev); + KUNIT_ASSERT_TRUE(test, bound); + + scoped_guard(device, &cons->dev) + bound = device_is_bound(&cons->dev); + KUNIT_ASSERT_TRUE(test, bound); + + pdata = dev_get_platdata(&cons->dev); + KUNIT_EXPECT_EQ(test, pdata->pwrseq_err, 0); +} + +static int pwrseq_swnode_test_init(struct kunit *test) +{ + /* + * A device link teardown from an earlier test case, or from this + * suite's previous module load, may still be queued on device_link_mq. + * Flush it so software_node_register() below doesn't spuriously see + * the about-to-be-reused node name as still taken. + */ + device_link_wait_removal(); + + return 0; +} + +static struct kunit_case pwrseq_swnode_probe_order_tests[] = { + KUNIT_CASE(pwrseq_swnode_probe_order), + KUNIT_CASE(pwrseq_swnode_probe_defer_on_unregistered), + { } +}; + +static struct kunit_suite pwrseq_swnode_probe_order_test_suite = { + .name = "pwrseq-swnode-probe-order", + .init = pwrseq_swnode_test_init, + .test_cases = pwrseq_swnode_probe_order_tests, +}; + +kunit_test_suites(&pwrseq_test_suite, + &pwrseq_swnode_probe_order_test_suite); + +MODULE_DESCRIPTION("KUnit test cases for the power sequencing subsystem"); +MODULE_AUTHOR("Bartosz Golaszewski "); +MODULE_LICENSE("GPL"); -- cgit v1.2.3 From bf544a7a60e9eb45f3e22bd1a5184ccc16d75cc8 Mon Sep 17 00:00:00 2001 From: Loic Poulain Date: Fri, 4 Sep 2026 17:42:18 +0200 Subject: power: sequencing: Add pwrseq_is_controllable() API On some boards a power sequencing target has no host-controllable enable for its function, for instance when the enable line is not wired up to a GPIO and is hardwired to an always-on level. The pcie-m2 "uart" target is one such example: when the M.2 connector does not route the W_DISABLE2# signal to a host GPIO, its enable/disable are no-ops and the consumer cannot gate the Bluetooth function at all or exclusively. Add a generic pwrseq_is_controllable() helper. It reports whether the target's final unit provides a host-controllable dedicated power actuator. The target can implement a new optional is_controllable() callback, reporting whether that actuator is effective on this target (for example depending on GPIO presence). If the target does not provide the callback, it is assumed to be controllable. Note this only describes the target's own enable actuator. It does not imply that a power-off reaches an electrical OFF state, since a target may have multiple consumers. It also does not mean that power is uncontrolled for the target's dependencies: those may still be gated on their own. And it does not restrict consumers from calling pwrseq_power_off() either, which remains valid to drop this consumer's vote on the (possibly shared) resources and dependencies of the target. Reviewed-by: Manivannan Sadhasivam Acked-by: Bartosz Golaszewski Reviewed-by: Manivannan Sadhasivam Signed-off-by: Loic Poulain Link: https://patch.msgid.link/20260904-monza-wireless-v6-3-d8c5042b3efd@oss.qualcomm.com Signed-off-by: Bartosz Golaszewski --- drivers/power/sequencing/core.c | 49 +++++++++++++++++++++++++++++++++++++++++ include/linux/pwrseq/consumer.h | 7 ++++++ include/linux/pwrseq/provider.h | 8 +++++++ 3 files changed, 64 insertions(+) diff --git a/drivers/power/sequencing/core.c b/drivers/power/sequencing/core.c index 721e888b658d..88a507deedda 100644 --- a/drivers/power/sequencing/core.c +++ b/drivers/power/sequencing/core.c @@ -182,12 +182,15 @@ static void pwrseq_unit_release(struct kref *ref) * 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. + * @is_controllable: Optional callback reporting whether enabling/disabling + * this target actually controls power. */ struct pwrseq_target { struct list_head list; const char *name; struct pwrseq_unit *unit; pwrseq_power_state_func post_enable; + pwrseq_is_controllable_func is_controllable; }; static struct pwrseq_target * @@ -206,6 +209,7 @@ pwrseq_target_new(const struct pwrseq_target_data *data) } target->post_enable = data->post_enable; + target->is_controllable = data->is_controllable; return target; } @@ -991,6 +995,51 @@ struct device *pwrseq_to_device(struct pwrseq_desc *desc) } EXPORT_SYMBOL_GPL(pwrseq_to_device); +/** + * pwrseq_is_controllable() - Check whether the target provides a + * host-controllable power actuator. + * @desc: Descriptor referencing the power sequencer. + * + * Some power sequencing targets provide no host-controllable enable for their + * function on a given board, for instance when the enable line is not wired up + * and is instead hardwired to an always-on level. For such targets a call to + * pwrseq_power_off() is still allowed, so that the consumer can drop its vote + * on the (possibly shared) resources, but the host cannot gate the function + * on its own. + * + * Returns: + * True if the target provides a host-controllable power actuator, false + * otherwise. Also returns false if @desc is NULL. + */ +bool pwrseq_is_controllable(struct pwrseq_desc *desc) +{ + struct pwrseq_device *pwrseq; + struct pwrseq_target *target; + struct pwrseq_unit *unit; + + might_sleep(); + + if (!desc) + return false; + + pwrseq = desc->pwrseq; + target = desc->target; + unit = target->unit; + + guard(rwsem_read)(&pwrseq->rw_lock); + if (!device_is_registered(&pwrseq->dev)) + return false; + + if (!unit->enable && !unit->disable) + return false; + + if (!target->is_controllable) + return true; + + return target->is_controllable(pwrseq); +} +EXPORT_SYMBOL_GPL(pwrseq_is_controllable); + #if IS_ENABLED(CONFIG_DEBUG_FS) struct pwrseq_debugfs_count_ctx { diff --git a/include/linux/pwrseq/consumer.h b/include/linux/pwrseq/consumer.h index 16fad5f3e3ab..b69117ad3fa7 100644 --- a/include/linux/pwrseq/consumer.h +++ b/include/linux/pwrseq/consumer.h @@ -25,6 +25,8 @@ int pwrseq_disable(struct pwrseq_desc *desc); struct device *pwrseq_to_device(struct pwrseq_desc *desc); +bool pwrseq_is_controllable(struct pwrseq_desc *desc); + #else /* CONFIG_POWER_SEQUENCING */ static inline struct pwrseq_desc * __must_check @@ -58,6 +60,11 @@ static inline struct device *pwrseq_to_device(struct pwrseq_desc *desc) return NULL; } +static inline bool pwrseq_is_controllable(struct pwrseq_desc *desc) +{ + return false; +} + #endif /* CONFIG_POWER_SEQUENCING */ #endif /* __POWER_SEQUENCING_CONSUMER_H__ */ diff --git a/include/linux/pwrseq/provider.h b/include/linux/pwrseq/provider.h index 33b3d2c2e39d..7285ad94a221 100644 --- a/include/linux/pwrseq/provider.h +++ b/include/linux/pwrseq/provider.h @@ -6,12 +6,15 @@ #ifndef __POWER_SEQUENCING_PROVIDER_H__ #define __POWER_SEQUENCING_PROVIDER_H__ +#include + 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 *); +typedef bool (*pwrseq_is_controllable_func)(struct pwrseq_device *); #define PWRSEQ_NO_MATCH 0 #define PWRSEQ_MATCH_OK 1 @@ -43,11 +46,16 @@ struct pwrseq_unit_data { * 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. + * @is_controllable: Optional callback checking whether enabling/disabling this + * target actually controls power (for example when the + * controlling GPIO is wired up). If not provided, the + * target's power is assumed to be always controllable. */ struct pwrseq_target_data { const char *name; const struct pwrseq_unit_data *unit; pwrseq_power_state_func post_enable; + pwrseq_is_controllable_func is_controllable; }; /** -- cgit v1.2.3 From 48925a1d5e569735031bcc2f2c19e6ce7a38e424 Mon Sep 17 00:00:00 2001 From: Loic Poulain Date: Fri, 4 Sep 2026 17:42:19 +0200 Subject: power: sequencing: pcie-m2: Report power controllability The M.2 Key E connector gates/resets its functions through the W_DISABLE1# (PCIe/WiFi) and W_DISABLE2# (Bluetooth) signals. When a signal is not routed to a host GPIO, the corresponding enable/disable callbacks are no-ops, so the consumer cannot gate or reset that function on its own. Implement the .is_controllable() callback on the "uart" and "pcie" targets so that consumers can query this per instance (based on the runtime presence of the W_DISABLE2#/W_DISABLE1# GPIOs) via pwrseq_is_controllable(). Reviewed-by: Dmitry Baryshkov Reviewed-by: Manivannan Sadhasivam Reviewed-by: Manivannan Sadhasivam Signed-off-by: Loic Poulain Link: https://patch.msgid.link/20260904-monza-wireless-v6-4-d8c5042b3efd@oss.qualcomm.com Signed-off-by: Bartosz Golaszewski --- drivers/power/sequencing/pwrseq-pcie-m2.c | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/drivers/power/sequencing/pwrseq-pcie-m2.c b/drivers/power/sequencing/pwrseq-pcie-m2.c index de9848a9a9f1..bee1aa0e077d 100644 --- a/drivers/power/sequencing/pwrseq-pcie-m2.c +++ b/drivers/power/sequencing/pwrseq-pcie-m2.c @@ -83,6 +83,18 @@ static int pwrseq_pci_m2_e_uart_disable(struct pwrseq_device *pwrseq) return gpiod_set_value_cansleep(ctx->w_disable2_gpio, 1); } +static bool pwrseq_pci_m2_e_uart_is_controllable(struct pwrseq_device *pwrseq) +{ + struct pwrseq_pcie_m2_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); + + /* + * The UART enable is driven through the W_DISABLE2# line. When it is not + * wired up on this connector the enable/disable callbacks are no-ops, so + * the host cannot gate the Bluetooth function on its own. + */ + return !!ctx->w_disable2_gpio; +} + static const struct pwrseq_unit_data pwrseq_pcie_m2_e_uart_unit_data = { .name = "uart-enable", .deps = pwrseq_pcie_m2_unit_deps, @@ -104,6 +116,18 @@ static int pwrseq_pci_m2_e_pcie_disable(struct pwrseq_device *pwrseq) return gpiod_set_value_cansleep(ctx->w_disable1_gpio, 1); } +static bool pwrseq_pci_m2_e_pcie_is_controllable(struct pwrseq_device *pwrseq) +{ + struct pwrseq_pcie_m2_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); + + /* + * The PCIe/WiFi enable is driven through the W_DISABLE1# line. When it + * is not wired up on this connector the enable/disable callbacks are + * no-ops, so the host cannot gate the PCIe/WiFi function on its own. + */ + return !!ctx->w_disable1_gpio; +} + static const struct pwrseq_unit_data pwrseq_pcie_m2_e_pcie_unit_data = { .name = "pcie-enable", .deps = pwrseq_pcie_m2_unit_deps, @@ -132,12 +156,14 @@ static const struct pwrseq_target_data pwrseq_pcie_m2_e_uart_target_data = { .name = "uart", .unit = &pwrseq_pcie_m2_e_uart_unit_data, .post_enable = pwrseq_pcie_m2_e_pwup_delay, + .is_controllable = pwrseq_pci_m2_e_uart_is_controllable, }; static const struct pwrseq_target_data pwrseq_pcie_m2_e_pcie_target_data = { .name = "pcie", .unit = &pwrseq_pcie_m2_e_pcie_unit_data, .post_enable = pwrseq_pcie_m2_e_pwup_delay, + .is_controllable = pwrseq_pci_m2_e_pcie_is_controllable, }; static const struct pwrseq_target_data pwrseq_pcie_m2_m_pcie_target_data = { -- cgit v1.2.3 From 295eeaf2c6543f746c1c7d9871f6d2f71a7ed4cc Mon Sep 17 00:00:00 2001 From: Loic Poulain Date: Fri, 4 Sep 2026 17:42:20 +0200 Subject: power: sequencing: qcom-wcn: Report power controllability The "bluetooth" and "wlan" targets drive the BT_EN and WLAN_EN GPIOs to gate the respective functions. These GPIOs are optional, on some boards they are hardwired to an always-on pull-up and not routed to a host GPIO. In that case the corresponding enable/disable callbacks are no-ops, so the consumer cannot gate or reset that function on its own. Implement the .is_controllable() callback on the "bluetooth" and "wlan" targets so that consumers can query this via the pwrseq API. Reviewed-by: Manivannan Sadhasivam Reviewed-by: Dmitry Baryshkov Reviewed-by: Manivannan Sadhasivam Signed-off-by: Loic Poulain Link: https://patch.msgid.link/20260904-monza-wireless-v6-5-d8c5042b3efd@oss.qualcomm.com Signed-off-by: Bartosz Golaszewski --- drivers/power/sequencing/pwrseq-qcom-wcn.c | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/drivers/power/sequencing/pwrseq-qcom-wcn.c b/drivers/power/sequencing/pwrseq-qcom-wcn.c index d41793e1fcd9..7f88a29b2c33 100644 --- a/drivers/power/sequencing/pwrseq-qcom-wcn.c +++ b/drivers/power/sequencing/pwrseq-qcom-wcn.c @@ -183,6 +183,19 @@ static int pwrseq_qcom_wcn_bt_disable(struct pwrseq_device *pwrseq) return 0; } +static bool pwrseq_qcom_wcn_bt_is_controllable(struct pwrseq_device *pwrseq) +{ + struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); + + /* + * The Bluetooth enable is driven through the BT_EN GPIO. When it is not + * wired up (e.g. hardwired to an always-on pull-up), the enable/disable + * callbacks are no-ops, so the host cannot gate the Bluetooth function + * on its own. + */ + return !!ctx->bt_gpio; +} + static const struct pwrseq_unit_data pwrseq_qcom_wcn_bt_unit_data = { .name = "bluetooth-enable", .deps = pwrseq_qcom_wcn_unit_deps, @@ -217,6 +230,19 @@ static int pwrseq_qcom_wcn_wlan_disable(struct pwrseq_device *pwrseq) return 0; } +static bool pwrseq_qcom_wcn_wlan_is_controllable(struct pwrseq_device *pwrseq) +{ + struct pwrseq_qcom_wcn_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); + + /* + * The WLAN enable is driven through the WLAN_EN GPIO. When it is not + * wired up (e.g. hardwired to an always-on pull-up), the enable/disable + * callbacks are no-ops, so the host cannot gate the WLAN function on + * its own. + */ + return !!ctx->wlan_gpio; +} + static const struct pwrseq_unit_data pwrseq_qcom_wcn_wlan_unit_data = { .name = "wlan-enable", .deps = pwrseq_qcom_wcn_unit_deps, @@ -257,12 +283,14 @@ 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, + .is_controllable = pwrseq_qcom_wcn_bt_is_controllable, }; 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, + .is_controllable = pwrseq_qcom_wcn_wlan_is_controllable, }; /* There are no separate BT and WLAN enablement pins */ @@ -280,12 +308,14 @@ static const struct pwrseq_target_data pwrseq_qcom_wcn6855_bt_target_data = { .name = "bluetooth", .unit = &pwrseq_qcom_wcn6855_bt_unit_data, .post_enable = pwrseq_qcom_wcn6855_xo_clk_deassert, + .is_controllable = pwrseq_qcom_wcn_bt_is_controllable, }; static const struct pwrseq_target_data pwrseq_qcom_wcn6855_wlan_target_data = { .name = "wlan", .unit = &pwrseq_qcom_wcn6855_wlan_unit_data, .post_enable = pwrseq_qcom_wcn6855_xo_clk_deassert, + .is_controllable = pwrseq_qcom_wcn_wlan_is_controllable, }; static const struct pwrseq_target_data *pwrseq_qcom_wcn_targets[] = { -- cgit v1.2.3