| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * T-HEAD TH1520 GPU Power Sequencer Driver | |
| * | |
| * Copyright (c) 2025 Samsung Electronics Co., Ltd. | |
| * Author: Michal Wilczynski <m.wilczynski@samsung.com> | |
| * | |
| * This driver implements the power sequence for the Imagination BXM-4-64 | |
| * GPU on the T-HEAD TH1520 SoC. The sequence requires coordinating resources | |
| * from both the sequencer's parent device node (clkgen_reset) and the GPU's | |
| * device node (clocks and core reset). | |
| * | |
| * The `match` function is used to acquire the GPU's resources when the | |
| * GPU driver requests the "gpu-power" sequence target. | |
| */ | |
| #include <linux/auxiliary_bus.h> | |
| #include <linux/clk.h> | |
| #include <linux/delay.h> | |
| #include <linux/module.h> | |
| #include <linux/of.h> | |
| #include <linux/pwrseq/provider.h> | |
| #include <linux/reset.h> | |
| #include <linux/slab.h> | |
| #include <dt-bindings/power/thead,th1520-power.h> | |
| struct pwrseq_thead_gpu_ctx { | |
| struct pwrseq_device *pwrseq; | |
| struct reset_control *clkgen_reset; | |
| struct device_node *aon_node; | |
| /* Consumer resources */ | |
| struct device_node *consumer_node; | |
| struct clk_bulk_data *clks; | |
| int num_clks; | |
| struct reset_control *gpu_reset; | |
| }; | |
| static int pwrseq_thead_gpu_enable(struct pwrseq_device *pwrseq) | |
| { | |
| struct pwrseq_thead_gpu_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); | |
| int ret; | |
| if (!ctx->clks || !ctx->gpu_reset) | |
| return -ENODEV; | |
| ret = clk_bulk_prepare_enable(ctx->num_clks, ctx->clks); | |
| if (ret) | |
| return ret; | |
| ret = reset_control_deassert(ctx->clkgen_reset); | |
| if (ret) | |
| goto err_disable_clks; | |
| /* | |
| * According to the hardware manual, a delay of at least 32 clock | |
| * cycles is required between de-asserting the clkgen reset and | |
| * de-asserting the GPU reset. Assuming a worst-case scenario with | |
| * a very high GPU clock frequency, a delay of 1 microsecond is | |
| * sufficient to ensure this requirement is met across all | |
| * feasible GPU clock speeds. | |
| */ | |
| udelay(1); | |
| ret = reset_control_deassert(ctx->gpu_reset); | |
| if (ret) | |
| goto err_assert_clkgen; | |
| return 0; | |
| err_assert_clkgen: | |
| reset_control_assert(ctx->clkgen_reset); | |
| err_disable_clks: | |
| clk_bulk_disable_unprepare(ctx->num_clks, ctx->clks); | |
| return ret; | |
| } | |
| static int pwrseq_thead_gpu_disable(struct pwrseq_device *pwrseq) | |
| { | |
| struct pwrseq_thead_gpu_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); | |
| int ret = 0, err; | |
| if (!ctx->clks || !ctx->gpu_reset) | |
| return -ENODEV; | |
| err = reset_control_assert(ctx->gpu_reset); | |
| if (err) | |
| ret = err; | |
| err = reset_control_assert(ctx->clkgen_reset); | |
| if (err && !ret) | |
| ret = err; | |
| clk_bulk_disable_unprepare(ctx->num_clks, ctx->clks); | |
| /* ret stores values of the first error code */ | |
| return ret; | |
| } | |
| static const struct pwrseq_unit_data pwrseq_thead_gpu_unit = { | |
| .name = "gpu-power-sequence", | |
| .enable = pwrseq_thead_gpu_enable, | |
| .disable = pwrseq_thead_gpu_disable, | |
| }; | |
| static const struct pwrseq_target_data pwrseq_thead_gpu_target = { | |
| .name = "gpu-power", | |
| .unit = &pwrseq_thead_gpu_unit, | |
| }; | |
| static const struct pwrseq_target_data *pwrseq_thead_gpu_targets[] = { | |
| &pwrseq_thead_gpu_target, | |
| NULL | |
| }; | |
| static int pwrseq_thead_gpu_match(struct pwrseq_device *pwrseq, | |
| struct device *dev) | |
| { | |
| struct pwrseq_thead_gpu_ctx *ctx = pwrseq_device_get_drvdata(pwrseq); | |
| static const char *const clk_names[] = { "core", "sys" }; | |
| struct of_phandle_args pwr_spec; | |
| int i, ret; | |
| /* We only match the specific T-HEAD TH1520 GPU compatible */ | |
| if (!of_device_is_compatible(dev->of_node, "thead,th1520-gpu")) | |
| return PWRSEQ_NO_MATCH; | |
| ret = of_parse_phandle_with_args(dev->of_node, "power-domains", | |
| "#power-domain-cells", 0, &pwr_spec); | |
| if (ret) | |
| return PWRSEQ_NO_MATCH; | |
| /* Additionally verify consumer device has AON as power-domain */ | |
| if (pwr_spec.np != ctx->aon_node || pwr_spec.args[0] != TH1520_GPU_PD) { | |
| of_node_put(pwr_spec.np); | |
| return PWRSEQ_NO_MATCH; | |
| } | |
| of_node_put(pwr_spec.np); | |
| /* If a consumer is already bound, only allow a re-match from it */ | |
| if (ctx->consumer_node) | |
| return ctx->consumer_node == dev->of_node ? | |
| PWRSEQ_MATCH_OK : PWRSEQ_NO_MATCH; | |
| ctx->num_clks = ARRAY_SIZE(clk_names); | |
| ctx->clks = kzalloc_objs(*ctx->clks, ctx->num_clks); | |
| if (!ctx->clks) | |
| return -ENOMEM; | |
| for (i = 0; i < ctx->num_clks; i++) | |
| ctx->clks[i].id = clk_names[i]; | |
| ret = clk_bulk_get(dev, ctx->num_clks, ctx->clks); | |
| if (ret) | |
| goto err_free_clks; | |
| ctx->gpu_reset = reset_control_get_shared(dev, NULL); | |
| if (IS_ERR(ctx->gpu_reset)) { | |
| ret = PTR_ERR(ctx->gpu_reset); | |
| goto err_put_clks; | |
| } | |
| ctx->consumer_node = of_node_get(dev->of_node); | |
| return PWRSEQ_MATCH_OK; | |
| err_put_clks: | |
| clk_bulk_put(ctx->num_clks, ctx->clks); | |
| err_free_clks: | |
| kfree(ctx->clks); | |
| ctx->clks = NULL; | |
| return ret; | |
| } | |
| static int pwrseq_thead_gpu_probe(struct auxiliary_device *adev, | |
| const struct auxiliary_device_id *id) | |
| { | |
| struct device *dev = &adev->dev; | |
| struct device *parent_dev = dev->parent; | |
| struct pwrseq_thead_gpu_ctx *ctx; | |
| struct pwrseq_config config = {}; | |
| ctx = devm_kzalloc(dev, sizeof(*ctx), GFP_KERNEL); | |
| if (!ctx) | |
| return -ENOMEM; | |
| ctx->aon_node = parent_dev->of_node; | |
| ctx->clkgen_reset = | |
| devm_reset_control_get_exclusive(parent_dev, "gpu-clkgen"); | |
| if (IS_ERR(ctx->clkgen_reset)) | |
| return dev_err_probe( | |
| dev, PTR_ERR(ctx->clkgen_reset), | |
| "Failed to get GPU clkgen reset from parent\n"); | |
| config.parent = dev; | |
| config.owner = THIS_MODULE; | |
| config.drvdata = ctx; | |
| config.match = pwrseq_thead_gpu_match; | |
| config.targets = pwrseq_thead_gpu_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 power sequencer\n"); | |
| auxiliary_set_drvdata(adev, ctx); | |
| return 0; | |
| } | |
| static void pwrseq_thead_gpu_remove(struct auxiliary_device *adev) | |
| { | |
| struct pwrseq_thead_gpu_ctx *ctx = auxiliary_get_drvdata(adev); | |
| if (ctx->gpu_reset) | |
| reset_control_put(ctx->gpu_reset); | |
| if (ctx->clks) { | |
| clk_bulk_put(ctx->num_clks, ctx->clks); | |
| kfree(ctx->clks); | |
| } | |
| if (ctx->consumer_node) | |
| of_node_put(ctx->consumer_node); | |
| } | |
| static const struct auxiliary_device_id pwrseq_thead_gpu_id_table[] = { | |
| { .name = "th1520_pm_domains.pwrseq-gpu" }, | |
| {}, | |
| }; | |
| MODULE_DEVICE_TABLE(auxiliary, pwrseq_thead_gpu_id_table); | |
| static struct auxiliary_driver pwrseq_thead_gpu_driver = { | |
| .driver = { | |
| .name = "pwrseq-thead-gpu", | |
| }, | |
| .probe = pwrseq_thead_gpu_probe, | |
| .remove = pwrseq_thead_gpu_remove, | |
| .id_table = pwrseq_thead_gpu_id_table, | |
| }; | |
| module_auxiliary_driver(pwrseq_thead_gpu_driver); | |
| MODULE_AUTHOR("Michal Wilczynski <m.wilczynski@samsung.com>"); | |
| MODULE_DESCRIPTION("T-HEAD TH1520 GPU power sequencer driver"); | |
| MODULE_LICENSE("GPL"); |