// SPDX-License-Identifier: GPL-2.0-only /* * Copyright (C) 2021, Linaro Limited. All rights reserved. */ #include #include #include #include #include #include #include #include #define LMH_NODE_DCVS 0x44435653 #define LMH_CLUSTER0_NODE_ID 0x6370302D #define LMH_CLUSTER1_NODE_ID 0x6370312D #define LMH_SUB_FN_THERMAL 0x54484D4C #define LMH_SUB_FN_CRNT 0x43524E54 #define LMH_SUB_FN_REL 0x52454C00 #define LMH_SUB_FN_BCL 0x42434C00 #define LMH_ALGO_MODE_ENABLE 0x454E424C #define LMH_TH_HI_THRESHOLD 0x48494748 #define LMH_TH_LOW_THRESHOLD 0x4C4F5700 #define LMH_TH_ARM_THRESHOLD 0x41524D00 #define LMH_REG_DCVS_INTR_CLR 0x8 struct lmh_hw_data { void __iomem *base; struct irq_domain *domain; int irq; }; static irqreturn_t lmh_handle_irq(int hw_irq, void *data) { struct lmh_hw_data *lmh_data = data; int irq = irq_find_mapping(lmh_data->domain, 0); /* Call the cpufreq driver to handle the interrupt */ if (irq) generic_handle_irq(irq); return 0; } static void lmh_enable_interrupt(struct irq_data *d) { struct lmh_hw_data *lmh_data = irq_data_get_irq_chip_data(d); /* Clear the existing interrupt */ writel(0xff, lmh_data->base + LMH_REG_DCVS_INTR_CLR); enable_irq(lmh_data->irq); } static void lmh_disable_interrupt(struct irq_data *d) { struct lmh_hw_data *lmh_data = irq_data_get_irq_chip_data(d); disable_irq_nosync(lmh_data->irq); } static struct irq_chip lmh_irq_chip = { .name = "lmh", .irq_enable = lmh_enable_interrupt, .irq_disable = lmh_disable_interrupt }; static int lmh_irq_map(struct irq_domain *d, unsigned int irq, irq_hw_number_t hw) { struct lmh_hw_data *lmh_data = d->host_data; irq_set_chip_and_handler(irq, &lmh_irq_chip, handle_simple_irq); irq_set_chip_data(irq, lmh_data); return 0; } static const struct irq_domain_ops lmh_irq_ops = { .map = lmh_irq_map, .xlate = irq_domain_xlate_onecell, }; static int lmh_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct device_node *np = dev->of_node; struct device_node *cpu_node; struct lmh_hw_data *lmh_data; int temp_low, temp_high, temp_arm, cpu_id, ret; u32 node_id; lmh_data = devm_kzalloc(dev, sizeof(*lmh_data), GFP_KERNEL); if (!lmh_data) return -ENOMEM; lmh_data->base = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(lmh_data->base)) return PTR_ERR(lmh_data->base); cpu_node = of_parse_phandle(np, "cpus", 0); if (!cpu_node) return -EINVAL; cpu_id = of_cpu_node_to_id(cpu_node); of_node_put(cpu_node); ret = of_property_read_u32(np, "qcom,lmh-temp-high-millicelsius", &temp_high); if (ret) { dev_err(dev, "missing qcom,lmh-temp-high-millicelsius property\n"); return ret; } ret = of_property_read_u32(np, "qcom,lmh-temp-low-millicelsius", &temp_low); if (ret) { dev_err(dev, "missing qcom,lmh-temp-low-millicelsius property\n"); return ret; } ret = of_property_read_u32(np, "qcom,lmh-temp-arm-millicelsius", &temp_arm); if (ret) { dev_err(dev, "missing qcom,lmh-temp-arm-millicelsius property\n"); return ret; } /* * Only sdm845 has lmh hardware currently enabled from hlos. If this is needed * for other platforms, revisit this to check if the should be part * of a dt match table. */ if (cpu_id == 0) { node_id = LMH_CLUSTER0_NODE_ID; } else if (cpu_id == 4) { node_id = LMH_CLUSTER1_NODE_ID; } else { dev_err(dev, "Wrong CPU id associated with LMh node\n"); return -EINVAL; } if (!qcom_scm_lmh_dcvsh_available()) return -EINVAL; ret = qcom_scm_lmh_dcvsh(LMH_SUB_FN_CRNT, LMH_ALGO_MODE_ENABLE, 1, LMH_NODE_DCVS, node_id, 0); if (ret) dev_err(dev, "Error %d enabling current subfunction\n", ret); ret = qcom_scm_lmh_dcvsh(LMH_SUB_FN_REL, LMH_ALGO_MODE_ENABLE, 1, LMH_NODE_DCVS, node_id, 0); if (ret) dev_err(dev, "Error %d enabling reliability subfunction\n", ret); ret = qcom_scm_lmh_dcvsh(LMH_SUB_FN_BCL, LMH_ALGO_MODE_ENABLE, 1, LMH_NODE_DCVS, node_id, 0); if (ret) dev_err(dev, "Error %d enabling BCL subfunction\n", ret); ret = qcom_scm_lmh_dcvsh(LMH_SUB_FN_THERMAL, LMH_ALGO_MODE_ENABLE, 1, LMH_NODE_DCVS, node_id, 0); if (ret) { dev_err(dev, "Error %d enabling thermal subfunction\n", ret); return ret; } ret = qcom_scm_lmh_profile_change(0x1); if (ret) { dev_err(dev, "Error %d changing profile\n", ret); return ret; } /* Set default thermal trips */ ret = qcom_scm_lmh_dcvsh(LMH_SUB_FN_THERMAL, LMH_TH_ARM_THRESHOLD, temp_arm, LMH_NODE_DCVS, node_id, 0); if (ret) { dev_err(dev, "Error setting thermal ARM threshold%d\n", ret); return ret; } ret = qcom_scm_lmh_dcvsh(LMH_SUB_FN_THERMAL, LMH_TH_HI_THRESHOLD, temp_high, LMH_NODE_DCVS, node_id, 0); if (ret) { dev_err(dev, "Error setting thermal HI threshold%d\n", ret); return ret; } ret = qcom_scm_lmh_dcvsh(LMH_SUB_FN_THERMAL, LMH_TH_LOW_THRESHOLD, temp_low, LMH_NODE_DCVS, node_id, 0); if (ret) { dev_err(dev, "Error setting thermal ARM threshold%d\n", ret); return ret; } lmh_data->irq = platform_get_irq(pdev, 0); lmh_data->domain = irq_domain_add_linear(np, 1, &lmh_irq_ops, lmh_data); if (!lmh_data->domain) { dev_err(dev, "Error adding irq_domain\n"); return -EINVAL; } /* Disable the irq and let cpufreq enable it when ready to handle the interrupt */ irq_set_status_flags(lmh_data->irq, IRQ_NOAUTOEN); ret = devm_request_irq(dev, lmh_data->irq, lmh_handle_irq, IRQF_ONESHOT | IRQF_NO_SUSPEND, "lmh-irq", lmh_data); if (ret) { dev_err(dev, "Error %d registering irq %x\n", ret, lmh_data->irq); irq_domain_remove(lmh_data->domain); return ret; } return 0; } static const struct of_device_id lmh_table[] = { { .compatible = "qcom,sdm845-lmh", }, {} }; MODULE_DEVICE_TABLE(of, lmh_table); static struct platform_driver lmh_driver = { .probe = lmh_probe, .driver = { .name = "qcom-lmh", .of_match_table = lmh_table, .suppress_bind_attrs = true, }, }; module_platform_driver(lmh_driver); MODULE_LICENSE("GPL v2"); MODULE_DESCRIPTION("QCOM LMh driver");