| From f5bee334b0a56b8c69f5d82b1ebf513d94e6ede0 Mon Sep 17 00:00:00 2001 |
| From: Avery Yang <avery.zl.yang@fii-na.corp-partner.google.com> |
| Date: Tue, 5 Nov 2024 19:26:02 +0800 |
| Subject: [PATCH] hwmon mp2981 driver |
| |
| Signed-off-by: Avery Yang <avery.zl.yang@fii-na.corp-partner.google.com> |
| --- |
| drivers/hwmon/Kconfig | 10 ++ |
| drivers/hwmon/Makefile | 1 + |
| drivers/hwmon/mp2981.c | 212 +++++++++++++++++++++++++++++++++++++++++ |
| 3 files changed, 223 insertions(+) |
| create mode 100644 drivers/hwmon/mp2981.c |
| |
| diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig |
| index d48268e9cb63..136b37e1d50b 100644 |
| --- a/drivers/hwmon/Kconfig |
| +++ b/drivers/hwmon/Kconfig |
| @@ -1386,6 +1386,16 @@ config SENSORS_LM95245 |
| This driver can also be built as a module. If so, the module |
| will be called lm95245. |
| |
| +config SENSORS_MP2981 |
| + tristate "Monolithic Power System MP2981" |
| + depends on I2C |
| + select REGMAP_I2C |
| + help |
| + If you say yes here you get support for MP2981 temperature sensor. |
| + |
| + This driver can also be built as a module. If so, the module |
| + will be called mp2981. |
| + |
| config SENSORS_PC87360 |
| tristate "National Semiconductor PC87360 family" |
| depends on !PPC |
| diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile |
| index bfe3f3c919a4..b8e8df35cb98 100644 |
| --- a/drivers/hwmon/Makefile |
| +++ b/drivers/hwmon/Makefile |
| @@ -144,6 +144,7 @@ obj-$(CONFIG_SENSORS_MAX31760) += max31760.o |
| obj-$(CONFIG_SENSORS_MAX31790) += max31790.o |
| obj-$(CONFIG_SENSORS_MC13783_ADC)+= mc13783-adc.o |
| obj-$(CONFIG_SENSORS_MCP3021) += mcp3021.o |
| +obj-$(CONFIG_SENSORS_MP2981) += mp2981.o |
| obj-$(CONFIG_SENSORS_TC654) += tc654.o |
| obj-$(CONFIG_SENSORS_TPS23861) += tps23861.o |
| obj-$(CONFIG_SENSORS_MLXREG_FAN) += mlxreg-fan.o |
| diff --git a/drivers/hwmon/mp2981.c b/drivers/hwmon/mp2981.c |
| new file mode 100644 |
| index 000000000000..6dd9c97c3fea |
| --- /dev/null |
| +++ b/drivers/hwmon/mp2981.c |
| @@ -0,0 +1,212 @@ |
| +// SPDX-License-Identifier: GPL-2.0-or-later |
| +/* |
| + * Hardware monitoring driver for MPS Digital LLC Controllers(MP2981) |
| + */ |
| + |
| +#include <linux/err.h> |
| +#include <linux/hwmon.h> |
| +#include <linux/hwmon-sysfs.h> |
| +#include <linux/i2c.h> |
| +#include <linux/module.h> |
| + |
| +/* MP2981 telemetry register */ |
| +#define MP2981_VIN_REG 0x88 |
| +#define MP2981_VOUT_REG 0x8B |
| +#define MP2981_IOUT_REG 0x8C |
| +#define MP2981_TEMP_REG 0x8D |
| +#define MP2981_POUT_REG 0x96 |
| + |
| +#define MP2981_VIN_UINT 125 |
| +#define MP2981_VOUT_UINT 625 |
| +#define MP2981_VOUT_DIV 10 |
| +#define MP2981_IOUT_UINT 500 |
| +#define MP2981_TEMP_UINT 1000 |
| +#define MP2981_POUT_UINT 1000000 |
| + |
| +/* Each client has this additional data */ |
| +struct mp2981_data { |
| + struct i2c_client *client; |
| + /* lock for preventing concurrency issue */ |
| + struct mutex lock; |
| +}; |
| + |
| +static umode_t mp2981_is_visible(const void *data, enum hwmon_sensor_types type, |
| + u32 attr, int channel) |
| +{ |
| + switch (type) { |
| + case hwmon_temp: |
| + case hwmon_in: |
| + case hwmon_curr: |
| + case hwmon_power: |
| + return 0444; |
| + default: |
| + break; |
| + } |
| + |
| + return 0; |
| +} |
| + |
| +static int mp2981_read(struct device *dev, enum hwmon_sensor_types type, |
| + u32 attr, int channel, long *val) |
| +{ |
| + int ret; |
| + struct mp2981_data *data; |
| + |
| + data = dev_get_drvdata(dev); |
| + if (!data) { |
| + dev_err(dev, "failed to get driver data\n"); |
| + return -ENOMEM; |
| + } |
| + |
| + mutex_lock(&data->lock); |
| + |
| + switch (type) { |
| + case hwmon_in: |
| + ret = i2c_smbus_write_byte_data(data->client, 0, 0); |
| + if (ret < 0) { |
| + dev_err(dev, "failed to change page, errno = %d\n", ret); |
| + break; |
| + } |
| + |
| + ret = i2c_smbus_read_word_data(data->client, channel == 0 ? |
| + MP2981_VIN_REG : MP2981_VOUT_REG); |
| + if (ret < 0) { |
| + dev_err(dev, "failed to read %s, errno = %d\n", channel == 0 ? |
| + "vin" : "vout", ret); |
| + break; |
| + } |
| + |
| + *val = channel == 0 ? (ret & GENMASK(9, 0)) * MP2981_VIN_UINT : |
| + DIV_ROUND_CLOSEST((ret & GENMASK(8, 0)) * MP2981_VOUT_UINT, |
| + MP2981_VOUT_DIV); |
| + break; |
| + case hwmon_temp: |
| + ret = i2c_smbus_write_byte_data(data->client, 0, 0); |
| + if (ret < 0) { |
| + dev_err(dev, "failed to change page, errno = %d\n", ret); |
| + break; |
| + } |
| + |
| + ret = i2c_smbus_read_byte_data(data->client, MP2981_TEMP_REG); |
| + if (ret < 0) { |
| + dev_err(dev, "failed to read temp, errno = %d\n", ret); |
| + break; |
| + } |
| + |
| + *val = ret * MP2981_TEMP_UINT; |
| + break; |
| + case hwmon_curr: |
| + ret = i2c_smbus_write_byte_data(data->client, 0, 0); |
| + if (ret < 0) { |
| + dev_err(dev, "failed to change page, errno = %d\n", ret); |
| + break; |
| + } |
| + |
| + ret = i2c_smbus_read_word_data(data->client, MP2981_IOUT_REG); |
| + if (ret < 0) { |
| + dev_err(dev, "failed to read iout, errno = %d\n", ret); |
| + break; |
| + } |
| + |
| + *val = (ret & GENMASK(9, 0)) * MP2981_IOUT_UINT; |
| + break; |
| + case hwmon_power: |
| + ret = i2c_smbus_write_byte_data(data->client, 0, 0); |
| + if (ret < 0) { |
| + dev_err(dev, "failed to change page, errno = %d\n", ret); |
| + break; |
| + } |
| + |
| + ret = i2c_smbus_read_word_data(data->client, MP2981_POUT_REG); |
| + if (ret < 0) { |
| + dev_err(dev, "failed to read pout, errno = %d\n", ret); |
| + break; |
| + } |
| + |
| + *val = (ret & GENMASK(10, 0)) * MP2981_POUT_UINT; |
| + break; |
| + default: |
| + ret = -EINVAL; |
| + break; |
| + } |
| + |
| + mutex_unlock(&data->lock); |
| + return ret; |
| +} |
| + |
| +static const struct hwmon_channel_info *mp2981_info[] = { |
| + HWMON_CHANNEL_INFO(in, HWMON_I_INPUT, HWMON_I_INPUT), |
| + HWMON_CHANNEL_INFO(temp, HWMON_T_INPUT), |
| + HWMON_CHANNEL_INFO(curr, HWMON_C_INPUT), |
| + HWMON_CHANNEL_INFO(power, HWMON_P_INPUT), |
| + NULL |
| +}; |
| + |
| +static const struct hwmon_ops mp2981_hwmon_ops = { |
| + .is_visible = mp2981_is_visible, |
| + .read = mp2981_read, |
| +}; |
| + |
| +static const struct hwmon_chip_info mp2981_chip_info = { |
| + .ops = &mp2981_hwmon_ops, |
| + .info = mp2981_info, |
| +}; |
| + |
| +static int mp2981_probe(struct i2c_client *client) |
| +{ |
| + struct device *dev = &client->dev; |
| + struct device *hwmon_dev; |
| + struct mp2981_data *data; |
| + |
| + if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA | |
| + I2C_FUNC_SMBUS_WORD_DATA)) { |
| + dev_err(dev, "check failed, smbus byte and/or word data not supported!\n"); |
| + return -ENODEV; |
| + } |
| + |
| + data = devm_kzalloc(dev, sizeof(struct mp2981_data), GFP_KERNEL); |
| + if (!data) |
| + return -ENOMEM; |
| + |
| + mutex_init(&data->lock); |
| + data->client = client; |
| + |
| + hwmon_dev = devm_hwmon_device_register_with_info(dev, client->name, |
| + data, &mp2981_chip_info, |
| + NULL); |
| + if (IS_ERR(hwmon_dev)) { |
| + dev_err(dev, "unable to register mp2981 hwmon device\n"); |
| + return PTR_ERR(hwmon_dev); |
| + } |
| + |
| + dev_info(dev, "%s: sensor '%s'\n", dev_name(hwmon_dev), client->name); |
| + |
| + return 0; |
| +} |
| + |
| +static const struct i2c_device_id mp2981_ids[] = { |
| + {"mp2981", 0}, |
| + {} |
| +}; |
| +MODULE_DEVICE_TABLE(i2c, mp2981_ids); |
| + |
| +static const struct of_device_id __maybe_unused mp2981_of_match[] = { |
| + {.compatible = "mps,mp2981"}, |
| + {} |
| +}; |
| +MODULE_DEVICE_TABLE(of, mp2981_of_match); |
| + |
| +static struct i2c_driver mp2981_driver = { |
| + .class = I2C_CLASS_HWMON, |
| + .driver = { |
| + .name = "mp2981", |
| + .of_match_table = mp2981_of_match, |
| + }, |
| + .probe_new = mp2981_probe, |
| + .id_table = mp2981_ids, |
| +}; |
| +module_i2c_driver(mp2981_driver); |
| + |
| +MODULE_AUTHOR("Noah Wang <Noah.Wang@monolithicpower.com>"); |
| +MODULE_DESCRIPTION("MP2981 driver"); |
| +MODULE_LICENSE("GPL"); |
| -- |
| 2.34.1 |
| |