| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Copyright (C) 2023 Analog Devices, Inc. |
| * Author: Antoniu Miclaus <antoniu.miclaus@analog.com> |
| */ |
| |
| #include <linux/bitops.h> |
| #include <linux/err.h> |
| #include <linux/hwmon.h> |
| #include <linux/i2c.h> |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/property.h> |
| #include <linux/regmap.h> |
| #include <linux/regulator/consumer.h> |
| |
| #define LTC2991_STATUS_LOW 0x00 |
| #define LTC2991_CH_EN_TRIGGER 0x01 |
| #define LTC2991_V1_V4_CTRL 0x06 |
| #define LTC2991_V5_V8_CTRL 0x07 |
| #define LTC2991_PWM_TH_LSB_T_INT 0x08 |
| #define LTC2991_PWM_TH_MSB 0x09 |
| #define LTC2991_CHANNEL_V_MSB(x) (0x0A + ((x) * 2)) |
| #define LTC2991_CHANNEL_T_MSB(x) (0x0A + ((x) * 4)) |
| #define LTC2991_CHANNEL_C_MSB(x) (0x0C + ((x) * 4)) |
| #define LTC2991_T_INT_MSB 0x1A |
| #define LTC2991_VCC_MSB 0x1C |
| |
| #define LTC2991_V7_V8_EN BIT(7) |
| #define LTC2991_V5_V6_EN BIT(6) |
| #define LTC2991_V3_V4_EN BIT(5) |
| #define LTC2991_V1_V2_EN BIT(4) |
| #define LTC2991_T_INT_VCC_EN BIT(3) |
| |
| #define LTC2991_V3_V4_FILT_EN BIT(7) |
| #define LTC2991_V3_V4_TEMP_EN BIT(5) |
| #define LTC2991_V3_V4_DIFF_EN BIT(4) |
| #define LTC2991_V1_V2_FILT_EN BIT(3) |
| #define LTC2991_V1_V2_TEMP_EN BIT(1) |
| #define LTC2991_V1_V2_DIFF_EN BIT(0) |
| |
| #define LTC2991_V7_V8_FILT_EN BIT(7) |
| #define LTC2991_V7_V8_TEMP_EN BIT(5) |
| #define LTC2991_V7_V8_DIFF_EN BIT(4) |
| #define LTC2991_V5_V6_FILT_EN BIT(3) |
| #define LTC2991_V5_V6_TEMP_EN BIT(1) |
| #define LTC2991_V5_V6_DIFF_EN BIT(0) |
| |
| #define LTC2991_REPEAT_ACQ_EN BIT(4) |
| #define LTC2991_T_INT_FILT_EN BIT(3) |
| |
| #define LTC2991_MAX_CHANNEL 4 |
| #define LTC2991_T_INT_CH_NR 4 |
| #define LTC2991_VCC_CH_NR 0 |
| |
| struct ltc2991_state { |
| struct regmap *regmap; |
| u32 r_sense_uohm[LTC2991_MAX_CHANNEL]; |
| bool temp_en[LTC2991_MAX_CHANNEL]; |
| }; |
| |
| static int ltc2991_read_reg(struct ltc2991_state *st, u8 addr, u8 reg_len, |
| int *val) |
| { |
| __be16 regvals; |
| int ret; |
| |
| if (reg_len < 2) |
| return regmap_read(st->regmap, addr, val); |
| |
| ret = regmap_bulk_read(st->regmap, addr, ®vals, reg_len); |
| if (ret) |
| return ret; |
| |
| *val = be16_to_cpu(regvals); |
| |
| return 0; |
| } |
| |
| static int ltc2991_get_voltage(struct ltc2991_state *st, u32 reg, long *val) |
| { |
| int reg_val, ret, offset = 0; |
| |
| ret = ltc2991_read_reg(st, reg, 2, ®_val); |
| if (ret) |
| return ret; |
| |
| if (reg == LTC2991_VCC_MSB) |
| /* Vcc 2.5V offset */ |
| offset = 2500; |
| |
| /* Vx, 305.18uV/LSB */ |
| *val = DIV_ROUND_CLOSEST(sign_extend32(reg_val, 14) * 30518, |
| 1000 * 100) + offset; |
| |
| return 0; |
| } |
| |
| static int ltc2991_read_in(struct device *dev, u32 attr, int channel, long *val) |
| { |
| struct ltc2991_state *st = dev_get_drvdata(dev); |
| u32 reg; |
| |
| switch (attr) { |
| case hwmon_in_input: |
| if (channel == LTC2991_VCC_CH_NR) |
| reg = LTC2991_VCC_MSB; |
| else |
| reg = LTC2991_CHANNEL_V_MSB(channel - 1); |
| |
| return ltc2991_get_voltage(st, reg, val); |
| default: |
| return -EOPNOTSUPP; |
| } |
| } |
| |
| static int ltc2991_get_curr(struct ltc2991_state *st, u32 reg, int channel, |
| long *val) |
| { |
| int reg_val, ret; |
| |
| ret = ltc2991_read_reg(st, reg, 2, ®_val); |
| if (ret) |
| return ret; |
| |
| /* Vx-Vy, 19.075uV/LSB */ |
| *val = DIV_ROUND_CLOSEST(sign_extend32(reg_val, 14) * 19075, |
| st->r_sense_uohm[channel]); |
| |
| return 0; |
| } |
| |
| static int ltc2991_read_curr(struct device *dev, u32 attr, int channel, |
| long *val) |
| { |
| struct ltc2991_state *st = dev_get_drvdata(dev); |
| u32 reg; |
| |
| switch (attr) { |
| case hwmon_curr_input: |
| reg = LTC2991_CHANNEL_C_MSB(channel); |
| return ltc2991_get_curr(st, reg, channel, val); |
| default: |
| return -EOPNOTSUPP; |
| } |
| } |
| |
| static int ltc2991_get_temp(struct ltc2991_state *st, u32 reg, int channel, |
| long *val) |
| { |
| int reg_val, ret; |
| |
| ret = ltc2991_read_reg(st, reg, 2, ®_val); |
| if (ret) |
| return ret; |
| |
| /* Temp LSB = 0.0625 Degrees */ |
| *val = DIV_ROUND_CLOSEST(sign_extend32(reg_val, 12) * 1000, 16); |
| |
| return 0; |
| } |
| |
| static int ltc2991_read_temp(struct device *dev, u32 attr, int channel, |
| long *val) |
| { |
| struct ltc2991_state *st = dev_get_drvdata(dev); |
| u32 reg; |
| |
| switch (attr) { |
| case hwmon_temp_input: |
| if (channel == LTC2991_T_INT_CH_NR) |
| reg = LTC2991_T_INT_MSB; |
| else |
| reg = LTC2991_CHANNEL_T_MSB(channel); |
| |
| return ltc2991_get_temp(st, reg, channel, val); |
| default: |
| return -EOPNOTSUPP; |
| } |
| } |
| |
| static int ltc2991_read(struct device *dev, enum hwmon_sensor_types type, |
| u32 attr, int channel, long *val) |
| { |
| switch (type) { |
| case hwmon_in: |
| return ltc2991_read_in(dev, attr, channel, val); |
| case hwmon_curr: |
| return ltc2991_read_curr(dev, attr, channel, val); |
| case hwmon_temp: |
| return ltc2991_read_temp(dev, attr, channel, val); |
| default: |
| return -EOPNOTSUPP; |
| } |
| } |
| |
| static umode_t ltc2991_is_visible(const void *data, |
| enum hwmon_sensor_types type, u32 attr, |
| int channel) |
| { |
| const struct ltc2991_state *st = data; |
| |
| switch (type) { |
| case hwmon_in: |
| switch (attr) { |
| case hwmon_in_input: |
| if (channel == LTC2991_VCC_CH_NR) |
| return 0444; |
| if (st->temp_en[(channel - 1) / 2]) |
| break; |
| if (channel % 2) |
| return 0444; |
| if (!st->r_sense_uohm[(channel - 1) / 2]) |
| return 0444; |
| } |
| break; |
| case hwmon_curr: |
| switch (attr) { |
| case hwmon_curr_input: |
| if (st->r_sense_uohm[channel]) |
| return 0444; |
| break; |
| } |
| break; |
| case hwmon_temp: |
| switch (attr) { |
| case hwmon_temp_input: |
| if (channel == LTC2991_T_INT_CH_NR || |
| st->temp_en[channel]) |
| return 0444; |
| break; |
| } |
| break; |
| default: |
| break; |
| } |
| |
| return 0; |
| } |
| |
| static const struct hwmon_ops ltc2991_hwmon_ops = { |
| .is_visible = ltc2991_is_visible, |
| .read = ltc2991_read, |
| }; |
| |
| static const struct hwmon_channel_info *ltc2991_info[] = { |
| HWMON_CHANNEL_INFO(temp, |
| HWMON_T_INPUT, |
| HWMON_T_INPUT, |
| HWMON_T_INPUT, |
| HWMON_T_INPUT, |
| HWMON_T_INPUT |
| ), |
| HWMON_CHANNEL_INFO(curr, |
| HWMON_C_INPUT, |
| HWMON_C_INPUT, |
| HWMON_C_INPUT, |
| HWMON_C_INPUT |
| ), |
| HWMON_CHANNEL_INFO(in, |
| HWMON_I_INPUT, |
| HWMON_I_INPUT, |
| HWMON_I_INPUT, |
| HWMON_I_INPUT, |
| HWMON_I_INPUT, |
| HWMON_I_INPUT, |
| HWMON_I_INPUT, |
| HWMON_I_INPUT, |
| HWMON_I_INPUT |
| ), |
| NULL |
| }; |
| |
| static const struct hwmon_chip_info ltc2991_chip_info = { |
| .ops = <c2991_hwmon_ops, |
| .info = ltc2991_info, |
| }; |
| |
| static const struct regmap_config ltc2991_regmap_config = { |
| .reg_bits = 8, |
| .val_bits = 8, |
| .max_register = 0x1D, |
| }; |
| |
| static int ltc2991_init(struct ltc2991_state *st, struct device *dev) |
| { |
| int ret; |
| u32 val, addr; |
| u8 v5_v8_reg_data = 0, v1_v4_reg_data = 0; |
| |
| ret = devm_regulator_get_enable(dev, "vcc"); |
| if (ret) |
| return dev_err_probe(dev, ret, |
| "failed to enable regulator\n"); |
| |
| device_for_each_child_node_scoped(dev, child) { |
| ret = fwnode_property_read_u32(child, "reg", &addr); |
| if (ret < 0) |
| return ret; |
| |
| if (addr > 3) |
| return -EINVAL; |
| |
| ret = fwnode_property_read_u32(child, |
| "shunt-resistor-micro-ohms", |
| &val); |
| if (!ret) { |
| if (!val) |
| return dev_err_probe(dev, -EINVAL, |
| "shunt resistor value cannot be zero\n"); |
| |
| st->r_sense_uohm[addr] = val; |
| |
| switch (addr) { |
| case 0: |
| v1_v4_reg_data |= LTC2991_V1_V2_DIFF_EN; |
| break; |
| case 1: |
| v1_v4_reg_data |= LTC2991_V3_V4_DIFF_EN; |
| break; |
| case 2: |
| v5_v8_reg_data |= LTC2991_V5_V6_DIFF_EN; |
| break; |
| case 3: |
| v5_v8_reg_data |= LTC2991_V7_V8_DIFF_EN; |
| break; |
| default: |
| break; |
| } |
| } |
| |
| ret = fwnode_property_read_bool(child, |
| "adi,temperature-enable"); |
| if (ret) { |
| st->temp_en[addr] = ret; |
| |
| switch (addr) { |
| case 0: |
| v1_v4_reg_data |= LTC2991_V1_V2_TEMP_EN; |
| break; |
| case 1: |
| v1_v4_reg_data |= LTC2991_V3_V4_TEMP_EN; |
| break; |
| case 2: |
| v5_v8_reg_data |= LTC2991_V5_V6_TEMP_EN; |
| break; |
| case 3: |
| v5_v8_reg_data |= LTC2991_V7_V8_TEMP_EN; |
| break; |
| default: |
| break; |
| } |
| } |
| } |
| |
| ret = regmap_write(st->regmap, LTC2991_V5_V8_CTRL, v5_v8_reg_data); |
| if (ret) |
| return dev_err_probe(dev, ret, |
| "Error: Failed to set V5-V8 CTRL reg.\n"); |
| |
| ret = regmap_write(st->regmap, LTC2991_V1_V4_CTRL, v1_v4_reg_data); |
| if (ret) |
| return dev_err_probe(dev, ret, |
| "Error: Failed to set V1-V4 CTRL reg.\n"); |
| |
| ret = regmap_write(st->regmap, LTC2991_PWM_TH_LSB_T_INT, |
| LTC2991_REPEAT_ACQ_EN); |
| if (ret) |
| return dev_err_probe(dev, ret, |
| "Error: Failed to set continuous mode.\n"); |
| |
| /* Enable all channels and trigger conversions */ |
| return regmap_write(st->regmap, LTC2991_CH_EN_TRIGGER, |
| LTC2991_V7_V8_EN | LTC2991_V5_V6_EN | |
| LTC2991_V3_V4_EN | LTC2991_V1_V2_EN | |
| LTC2991_T_INT_VCC_EN); |
| } |
| |
| static int ltc2991_i2c_probe(struct i2c_client *client) |
| { |
| int ret; |
| struct device *hwmon_dev; |
| struct ltc2991_state *st; |
| |
| st = devm_kzalloc(&client->dev, sizeof(*st), GFP_KERNEL); |
| if (!st) |
| return -ENOMEM; |
| |
| st->regmap = devm_regmap_init_i2c(client, <c2991_regmap_config); |
| if (IS_ERR(st->regmap)) |
| return PTR_ERR(st->regmap); |
| |
| ret = ltc2991_init(st, &client->dev); |
| if (ret) |
| return ret; |
| |
| hwmon_dev = devm_hwmon_device_register_with_info(&client->dev, |
| client->name, st, |
| <c2991_chip_info, |
| NULL); |
| |
| return PTR_ERR_OR_ZERO(hwmon_dev); |
| } |
| |
| static const struct of_device_id ltc2991_of_match[] = { |
| { .compatible = "adi,ltc2991" }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(of, ltc2991_of_match); |
| |
| static const struct i2c_device_id ltc2991_i2c_id[] = { |
| { "ltc2991" }, |
| {} |
| }; |
| MODULE_DEVICE_TABLE(i2c, ltc2991_i2c_id); |
| |
| static struct i2c_driver ltc2991_i2c_driver = { |
| .driver = { |
| .name = "ltc2991", |
| .of_match_table = ltc2991_of_match, |
| }, |
| .probe = ltc2991_i2c_probe, |
| .id_table = ltc2991_i2c_id, |
| }; |
| |
| module_i2c_driver(ltc2991_i2c_driver); |
| |
| MODULE_AUTHOR("Antoniu Miclaus <antoniu.miclaus@analog.com>"); |
| MODULE_DESCRIPTION("Analog Devices LTC2991 HWMON Driver"); |
| MODULE_LICENSE("GPL"); |