| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * ESWIN EIC7700 Voltage, Temperature sensor driver |
| * |
| * Copyright 2026, Beijing ESWIN Computing Technology Co., Ltd. |
| * |
| * Authors: |
| * Yulin Lu <luyulin@eswincomputing.com> |
| * Huan He <hehuan1@eswincomputing.com> |
| */ |
| |
| #include <linux/bitfield.h> |
| #include <linux/clk.h> |
| #include <linux/delay.h> |
| #include <linux/device.h> |
| #include <linux/interrupt.h> |
| #include <linux/io.h> |
| #include <linux/module.h> |
| #include <linux/of.h> |
| #include <linux/platform_device.h> |
| #include <linux/pm_runtime.h> |
| #include <linux/polynomial.h> |
| #include <linux/reset.h> |
| #include "eic7700-pvt.h" |
| |
| static const struct pvt_sensor_info pvt_info[] = { |
| PVT_SENSOR_INFO(0, "Temperature", hwmon_temp, TEMP), |
| PVT_SENSOR_INFO(0, "Voltage", hwmon_in, VOLT), |
| }; |
| |
| static const char * const pvt_clk_names[PVT_CLK_NUM] = {"enable", "apb"}; |
| |
| /* |
| * The original translation formulae of the temperature (in degrees of Celsius) |
| * to PVT data and vice-versa are following: |
| * N = 6.0818e-8*(T^4) +1.2873e-5*(T^3) + 7.2244e-3*(T^2) + 3.6484*(T^1) + |
| * 1.6198e2, |
| * T = -1.8439e-11*(N^4) + 8.0705e-8*(N^3) + -1.8501e-4*(N^2) + |
| * 3.2843e-1*(N^1) - 4.8690e1, |
| * where T = [-40, 125]C and N = [27, 771]. |
| * They must be accordingly altered to be suitable for the integer arithmetics. |
| * The technique is called 'factor redistribution', which just makes sure the |
| * multiplications and divisions are made so to have a result of the operations |
| * within the integer numbers limit. In addition we need to translate the |
| * formulae to accept millidegrees of Celsius. Here what they look like after |
| * the alterations: |
| * N = (60818e-20*(T^4) + 12873e-14*(T^3) + 72244e-9*(T^2) + 36484e-3*T + |
| * 16198e2) / 1e4, |
| * T = -18439e-12*(N^4) + 80705e-9*(N^3) - 185010e-6*(N^2) + 328430e-3*N - |
| * 48690, |
| * where T = [-40000, 125000] mC and N = [27, 771]. |
| */ |
| static const struct polynomial poly_N_to_temp = { |
| .total_divider = 1, |
| .terms = { |
| {4, -18439, 1000, 1}, |
| {3, 80705, 1000, 1}, |
| {2, -185010, 1000, 1}, |
| {1, 328430, 1000, 1}, |
| {0, -48690, 1, 1} |
| } |
| }; |
| |
| /* |
| * Similar alterations are performed for the voltage conversion equations. |
| * The original formulae are: |
| * N = 1.3905e3*V - 5.7685e2, |
| * V = (N + 5.7685e2) / 1.3905e3, |
| * where V = [0.72, 0.88] V and N = [424, 646]. |
| * After the optimization they looks as follows: |
| * N = (13905e-3*V - 5768.5) / 10, |
| * V = (N * 10^5 / 13905 + 57685 * 10^3 / 13905) / 10. |
| * where V = [720, 880] mV and N = [424, 646]. |
| */ |
| static const struct polynomial poly_N_to_volt = { |
| .total_divider = 10, |
| .terms = { |
| {1, 100000, 13905, 1}, |
| {0, 57685000, 1, 13905} |
| } |
| }; |
| |
| static inline u32 eic7700_pvt_update(void __iomem *reg, u32 mask, u32 data) |
| { |
| u32 old; |
| |
| old = readl_relaxed(reg); |
| writel((old & ~mask) | (data & mask), reg); |
| |
| return old & mask; |
| } |
| |
| static inline void eic7700_pvt_set_mode(struct pvt_hwmon *pvt, u32 mode) |
| { |
| u32 old; |
| |
| mode = FIELD_PREP(PVT_MODE_MASK, mode); |
| |
| old = eic7700_pvt_update(pvt->regs + PVT_ENA, PVT_ENA_EN, 0); |
| eic7700_pvt_update(pvt->regs + PVT_MODE, PVT_MODE_MASK, mode); |
| eic7700_pvt_update(pvt->regs + PVT_ENA, PVT_ENA_EN, old); |
| } |
| |
| static inline void eic7700_pvt_set_trim(struct pvt_hwmon *pvt, u32 val) |
| { |
| u32 old; |
| |
| old = eic7700_pvt_update(pvt->regs + PVT_ENA, PVT_ENA_EN, 0); |
| writel(val, pvt->regs + PVT_TRIM); |
| eic7700_pvt_update(pvt->regs + PVT_ENA, PVT_ENA_EN, old); |
| } |
| |
| static irqreturn_t eic7700_pvt_hard_isr(int irq, void *data) |
| { |
| struct pvt_hwmon *pvt = data; |
| u32 stat, val; |
| int active; |
| |
| if (IS_ENABLED(CONFIG_PM)) { |
| active = pm_runtime_get_if_active(pvt->dev); |
| if (active <= 0) |
| return IRQ_NONE; |
| } |
| |
| stat = readl(pvt->regs + PVT_INT); |
| if (!(stat & PVT_INT_STAT)) { |
| if (IS_ENABLED(CONFIG_PM)) |
| pm_runtime_put(pvt->dev); |
| return IRQ_NONE; |
| } |
| |
| eic7700_pvt_update(pvt->regs + PVT_INT, PVT_INT_CLR, PVT_INT_CLR); |
| /* |
| * Read the data, update the cache and notify a waiter of this event. |
| */ |
| val = readl(pvt->regs + PVT_DATA); |
| WRITE_ONCE(pvt->data_cache, FIELD_GET(PVT_DATA_OUT, val)); |
| complete(&pvt->conversion); |
| |
| if (IS_ENABLED(CONFIG_PM)) |
| pm_runtime_put(pvt->dev); |
| |
| return IRQ_HANDLED; |
| } |
| |
| static int eic7700_pvt_read_data(struct pvt_hwmon *pvt, |
| enum pvt_sensor_type type, long *val) |
| { |
| unsigned long timeout; |
| u32 data; |
| int ret; |
| |
| /* |
| * Wait for PVT conversion to complete and update the data cache. The |
| * data read procedure is following: set the requested PVT sensor mode, |
| * enable conversion, wait until conversion is finished, then disable |
| * conversion and IRQ, and read the cached data. |
| */ |
| reinit_completion(&pvt->conversion); |
| |
| eic7700_pvt_set_mode(pvt, pvt_info[type].mode); |
| eic7700_pvt_update(pvt->regs + PVT_ENA, PVT_ENA_EN, PVT_ENA_EN); |
| |
| /* |
| * Wait with timeout since in case if the sensor is suddenly powered |
| * down the request won't be completed and the caller will hang up on |
| * this procedure until the power is back up again. Multiply the |
| * timeout by the factor of two to prevent a false timeout. |
| */ |
| timeout = 2 * usecs_to_jiffies(ktime_to_us(pvt->timeout)); |
| ret = wait_for_completion_timeout(&pvt->conversion, timeout); |
| |
| eic7700_pvt_update(pvt->regs + PVT_ENA, PVT_ENA_EN, 0); |
| eic7700_pvt_update(pvt->regs + PVT_INT, PVT_INT_CLR, PVT_INT_CLR); |
| |
| if (!ret) |
| synchronize_irq(pvt->irq); |
| |
| data = READ_ONCE(pvt->data_cache); |
| |
| if (!ret) |
| return -ETIMEDOUT; |
| |
| if (type == PVT_TEMP) |
| *val = polynomial_calc(&poly_N_to_temp, data); |
| else |
| *val = polynomial_calc(&poly_N_to_volt, data); |
| |
| return 0; |
| } |
| |
| static const struct hwmon_channel_info *pvt_channel_info[] = { |
| HWMON_CHANNEL_INFO(chip, HWMON_C_REGISTER_TZ), |
| HWMON_CHANNEL_INFO(temp, HWMON_T_INPUT | HWMON_T_LABEL), |
| HWMON_CHANNEL_INFO(in, HWMON_I_INPUT | HWMON_I_LABEL), |
| NULL |
| }; |
| |
| static umode_t eic7700_pvt_hwmon_is_visible(const void *data, |
| enum hwmon_sensor_types type, |
| u32 attr, int ch) |
| { |
| switch (type) { |
| case hwmon_temp: |
| switch (attr) { |
| case hwmon_temp_input: |
| case hwmon_temp_label: |
| return 0444; |
| } |
| break; |
| case hwmon_in: |
| switch (attr) { |
| case hwmon_in_input: |
| case hwmon_in_label: |
| return 0444; |
| } |
| break; |
| default: |
| break; |
| } |
| |
| return 0; |
| } |
| |
| static int eic7700_pvt_hwmon_read(struct device *dev, |
| enum hwmon_sensor_types type, u32 attr, |
| int ch, long *val) |
| { |
| struct pvt_hwmon *pvt = dev_get_drvdata(dev); |
| int ret; |
| |
| ret = pm_runtime_get_sync(pvt->dev); |
| if (ret < 0) { |
| dev_err(pvt->dev, "Failed to resume PVT device: %d\n", ret); |
| pm_runtime_put_noidle(pvt->dev); |
| return ret; |
| } |
| |
| switch (type) { |
| case hwmon_temp: |
| switch (attr) { |
| case hwmon_temp_input: |
| ret = eic7700_pvt_read_data(pvt, ch, val); |
| break; |
| default: |
| ret = -EOPNOTSUPP; |
| } |
| break; |
| case hwmon_in: |
| if (attr == hwmon_in_input) |
| ret = eic7700_pvt_read_data(pvt, PVT_VOLT + ch, val); |
| else |
| ret = -EOPNOTSUPP; |
| break; |
| default: |
| ret = -EOPNOTSUPP; |
| } |
| |
| pm_runtime_mark_last_busy(pvt->dev); |
| pm_runtime_put_autosuspend(pvt->dev); |
| return ret; |
| } |
| |
| static int eic7700_pvt_hwmon_read_string(struct device *dev, |
| enum hwmon_sensor_types type, u32 attr, |
| int ch, const char **str) |
| { |
| switch (type) { |
| case hwmon_temp: |
| if (attr == hwmon_temp_label) { |
| *str = pvt_info[ch].label; |
| return 0; |
| } |
| break; |
| case hwmon_in: |
| if (attr == hwmon_in_label) { |
| *str = pvt_info[PVT_VOLT + ch].label; |
| return 0; |
| } |
| break; |
| default: |
| break; |
| } |
| |
| return -EOPNOTSUPP; |
| } |
| |
| static const struct hwmon_ops pvt_hwmon_ops = { |
| .is_visible = eic7700_pvt_hwmon_is_visible, |
| .read = eic7700_pvt_hwmon_read, |
| .read_string = eic7700_pvt_hwmon_read_string |
| }; |
| |
| static const struct hwmon_chip_info pvt_hwmon_info = { |
| .ops = &pvt_hwmon_ops, |
| .info = pvt_channel_info |
| }; |
| |
| static struct pvt_hwmon *eic7700_pvt_create_data(struct platform_device *pdev) |
| { |
| struct device *dev = &pdev->dev; |
| struct pvt_hwmon *pvt; |
| |
| pvt = devm_kzalloc(dev, sizeof(*pvt), GFP_KERNEL); |
| if (!pvt) |
| return ERR_PTR(-ENOMEM); |
| |
| pvt->dev = dev; |
| init_completion(&pvt->conversion); |
| |
| return pvt; |
| } |
| |
| static int eic7700_pvt_init_iface(struct pvt_hwmon *pvt) |
| { |
| /* |
| * Make sure controller are disabled so not to accidentally have ISR |
| * executed before the driver data is fully initialized. Clear the IRQ |
| * status as well. |
| */ |
| eic7700_pvt_update(pvt->regs + PVT_ENA, PVT_ENA_EN, 0); |
| eic7700_pvt_update(pvt->regs + PVT_INT, PVT_INT_CLR, PVT_INT_CLR); |
| readl(pvt->regs + PVT_INT); |
| readl(pvt->regs + PVT_DATA); |
| |
| /* Setup default sensor mode and temperature trim. */ |
| eic7700_pvt_set_mode(pvt, pvt_info[PVT_TEMP].mode); |
| |
| /* |
| * Max conversion latency (~333 µs) derived from PVT spec: |
| * maximum sampling rate = 3000 samples/sec. |
| */ |
| pvt->timeout = ns_to_ktime(PVT_TOUT_MIN); |
| |
| eic7700_pvt_set_trim(pvt, PVT_TRIM_DEF); |
| |
| return 0; |
| } |
| |
| static int eic7700_pvt_request_irq(struct pvt_hwmon *pvt) |
| { |
| struct platform_device *pdev = to_platform_device(pvt->dev); |
| int ret; |
| |
| pvt->irq = platform_get_irq(pdev, 0); |
| if (pvt->irq < 0) |
| return pvt->irq; |
| |
| ret = devm_request_threaded_irq(pvt->dev, pvt->irq, |
| eic7700_pvt_hard_isr, NULL, |
| IRQF_TRIGGER_HIGH, "pvt", pvt); |
| if (ret) { |
| dev_err(pvt->dev, "Couldn't request PVT IRQ\n"); |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static int eic7700_pvt_create_hwmon(struct pvt_hwmon *pvt) |
| { |
| pvt->hwmon = devm_hwmon_device_register_with_info(pvt->dev, "pvt", |
| pvt, &pvt_hwmon_info, |
| NULL); |
| if (IS_ERR(pvt->hwmon)) { |
| dev_err(pvt->dev, "Couldn't create hwmon device\n"); |
| return PTR_ERR(pvt->hwmon); |
| } |
| |
| return 0; |
| } |
| |
| static void eic7700_pvt_disable_pm_runtime(void *data) |
| { |
| struct pvt_hwmon *pvt = data; |
| |
| pm_runtime_dont_use_autosuspend(pvt->dev); |
| pm_runtime_disable(pvt->dev); |
| |
| if (!pm_runtime_status_suspended(pvt->dev)) { |
| clk_bulk_disable_unprepare(PVT_CLK_NUM, pvt->clks); |
| pm_runtime_set_suspended(pvt->dev); |
| } |
| } |
| |
| static int eic7700_pvt_probe(struct platform_device *pdev) |
| { |
| struct reset_control *rst; |
| struct pvt_hwmon *pvt; |
| int i, ret; |
| |
| pvt = eic7700_pvt_create_data(pdev); |
| if (IS_ERR(pvt)) |
| return PTR_ERR(pvt); |
| |
| platform_set_drvdata(pdev, pvt); |
| |
| pvt->regs = devm_platform_ioremap_resource(pdev, 0); |
| if (IS_ERR(pvt->regs)) |
| return PTR_ERR(pvt->regs); |
| |
| for (i = 0; i < PVT_CLK_NUM; i++) |
| pvt->clks[i].id = pvt_clk_names[i]; |
| |
| ret = devm_clk_bulk_get(&pdev->dev, PVT_CLK_NUM, pvt->clks); |
| if (ret) |
| return dev_err_probe(&pdev->dev, ret, |
| "Couldn't get clock descriptors\n"); |
| |
| rst = devm_reset_control_get_exclusive_deasserted(&pdev->dev, NULL); |
| if (IS_ERR(rst)) |
| return dev_err_probe(pvt->dev, PTR_ERR(rst), |
| "Couldn't get reset control\n"); |
| |
| ret = clk_bulk_prepare_enable(PVT_CLK_NUM, pvt->clks); |
| if (ret) |
| return dev_err_probe(pvt->dev, ret, |
| "Failed to enable clocks\n"); |
| |
| ret = eic7700_pvt_init_iface(pvt); |
| if (ret) { |
| clk_bulk_disable_unprepare(PVT_CLK_NUM, pvt->clks); |
| return ret; |
| } |
| |
| if (IS_ENABLED(CONFIG_PM)) |
| clk_bulk_disable_unprepare(PVT_CLK_NUM, pvt->clks); |
| |
| pm_runtime_enable(&pdev->dev); |
| pm_runtime_set_autosuspend_delay(&pdev->dev, 3000); |
| pm_runtime_use_autosuspend(&pdev->dev); |
| pm_runtime_get_noresume(&pdev->dev); |
| |
| ret = devm_add_action_or_reset(pvt->dev, eic7700_pvt_disable_pm_runtime, |
| pvt); |
| if (ret) { |
| pm_runtime_put_noidle(&pdev->dev); |
| return dev_err_probe(&pdev->dev, ret, |
| "Can't register PM cleanup\n"); |
| } |
| |
| ret = eic7700_pvt_request_irq(pvt); |
| if (ret) |
| goto err_put_pm_runtime; |
| |
| ret = eic7700_pvt_create_hwmon(pvt); |
| if (ret) |
| goto err_put_pm_runtime; |
| |
| pm_runtime_put_autosuspend(&pdev->dev); |
| |
| return 0; |
| |
| err_put_pm_runtime: |
| pm_runtime_put_noidle(&pdev->dev); |
| return ret; |
| } |
| |
| static int __maybe_unused eic7700_pvt_runtime_resume(struct device *dev) |
| { |
| struct pvt_hwmon *pvt = dev_get_drvdata(dev); |
| int ret; |
| |
| ret = clk_bulk_prepare_enable(PVT_CLK_NUM, pvt->clks); |
| if (ret) { |
| dev_err(dev, "Failed to enable clocks: %d\n", ret); |
| return ret; |
| } |
| |
| eic7700_pvt_set_trim(pvt, PVT_TRIM_DEF); |
| |
| return 0; |
| } |
| |
| static int __maybe_unused eic7700_pvt_runtime_suspend(struct device *dev) |
| { |
| struct pvt_hwmon *pvt = dev_get_drvdata(dev); |
| |
| clk_bulk_disable_unprepare(PVT_CLK_NUM, pvt->clks); |
| |
| return 0; |
| } |
| |
| static const struct dev_pm_ops eic7700_pvt_pm_ops = { |
| SYSTEM_SLEEP_PM_OPS(pm_runtime_force_suspend, pm_runtime_force_resume) |
| RUNTIME_PM_OPS(eic7700_pvt_runtime_suspend, eic7700_pvt_runtime_resume, |
| NULL) |
| }; |
| |
| static const struct of_device_id pvt_of_match[] = { |
| { .compatible = "eswin,eic7700-pvt"}, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(of, pvt_of_match); |
| |
| static struct platform_driver pvt_driver = { |
| .probe = eic7700_pvt_probe, |
| .driver = { |
| .name = "eic7700-pvt", |
| .of_match_table = pvt_of_match, |
| .pm = pm_ptr(&eic7700_pvt_pm_ops), |
| }, |
| }; |
| module_platform_driver(pvt_driver); |
| |
| MODULE_AUTHOR("Yulin Lu <luyulin@eswincomputing.com>"); |
| MODULE_AUTHOR("Huan He <hehuan1@eswincomputing.com>"); |
| MODULE_DESCRIPTION("Eswin eic7700 PVT driver"); |
| MODULE_LICENSE("GPL"); |