| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * ADM1177 Hot Swap Controller and Digital Power Monitor with Soft Start Pin |
| * |
| * Copyright 2015-2019 Analog Devices Inc. |
| */ |
| |
| #include <linux/bits.h> |
| #include <linux/device.h> |
| #include <linux/hwmon.h> |
| #include <linux/i2c.h> |
| #include <linux/init.h> |
| #include <linux/module.h> |
| #include <linux/regulator/consumer.h> |
| |
| /* Command Byte Operations */ |
| #define ADM1177_CMD_V_CONT BIT(0) |
| #define ADM1177_CMD_I_CONT BIT(2) |
| #define ADM1177_CMD_VRANGE BIT(4) |
| |
| /* Extended Register */ |
| #define ADM1177_REG_ALERT_TH 2 |
| |
| #define ADM1177_BITS 12 |
| |
| /** |
| * struct adm1177_state - driver instance specific data |
| * @client pointer to i2c client |
| * @reg regulator info for the the power supply of the device |
| * @r_sense_uohm current sense resistor value |
| * @alert_threshold_ua current limit for shutdown |
| * @vrange_high internal voltage divider |
| */ |
| struct adm1177_state { |
| struct i2c_client *client; |
| struct regulator *reg; |
| u32 r_sense_uohm; |
| u32 alert_threshold_ua; |
| bool vrange_high; |
| }; |
| |
| static int adm1177_read_raw(struct adm1177_state *st, u8 num, u8 *data) |
| { |
| return i2c_master_recv(st->client, data, num); |
| } |
| |
| static int adm1177_write_cmd(struct adm1177_state *st, u8 cmd) |
| { |
| return i2c_smbus_write_byte(st->client, cmd); |
| } |
| |
| static int adm1177_write_alert_thr(struct adm1177_state *st, |
| u32 alert_threshold_ua) |
| { |
| u64 val; |
| int ret; |
| |
| val = 0xFFULL * alert_threshold_ua * st->r_sense_uohm; |
| val = div_u64(val, 105840000U); |
| val = div_u64(val, 1000U); |
| if (val > 0xFF) |
| val = 0xFF; |
| |
| ret = i2c_smbus_write_byte_data(st->client, ADM1177_REG_ALERT_TH, |
| val); |
| if (ret) |
| return ret; |
| |
| st->alert_threshold_ua = alert_threshold_ua; |
| return 0; |
| } |
| |
| static int adm1177_read(struct device *dev, enum hwmon_sensor_types type, |
| u32 attr, int channel, long *val) |
| { |
| struct adm1177_state *st = dev_get_drvdata(dev); |
| u8 data[3]; |
| long dummy; |
| int ret; |
| |
| switch (type) { |
| case hwmon_curr: |
| switch (attr) { |
| case hwmon_curr_input: |
| ret = adm1177_read_raw(st, 3, data); |
| if (ret < 0) |
| return ret; |
| dummy = (data[1] << 4) | (data[2] & 0xF); |
| /* |
| * convert to milliamperes |
| * ((105.84mV / 4096) x raw) / senseResistor(ohm) |
| */ |
| *val = div_u64((105840000ull * dummy), |
| 4096 * st->r_sense_uohm); |
| return 0; |
| case hwmon_curr_max_alarm: |
| *val = st->alert_threshold_ua; |
| return 0; |
| default: |
| return -EOPNOTSUPP; |
| } |
| case hwmon_in: |
| ret = adm1177_read_raw(st, 3, data); |
| if (ret < 0) |
| return ret; |
| dummy = (data[0] << 4) | (data[2] >> 4); |
| /* |
| * convert to millivolts based on resistor devision |
| * (V_fullscale / 4096) * raw |
| */ |
| if (st->vrange_high) |
| dummy *= 26350; |
| else |
| dummy *= 6650; |
| |
| *val = DIV_ROUND_CLOSEST(dummy, 4096); |
| return 0; |
| default: |
| return -EOPNOTSUPP; |
| } |
| } |
| |
| static int adm1177_write(struct device *dev, enum hwmon_sensor_types type, |
| u32 attr, int channel, long val) |
| { |
| struct adm1177_state *st = dev_get_drvdata(dev); |
| |
| switch (type) { |
| case hwmon_curr: |
| switch (attr) { |
| case hwmon_curr_max_alarm: |
| adm1177_write_alert_thr(st, val); |
| return 0; |
| default: |
| return -EOPNOTSUPP; |
| } |
| default: |
| return -EOPNOTSUPP; |
| } |
| } |
| |
| static umode_t adm1177_is_visible(const void *data, |
| enum hwmon_sensor_types type, |
| u32 attr, int channel) |
| { |
| const struct adm1177_state *st = data; |
| |
| switch (type) { |
| case hwmon_in: |
| switch (attr) { |
| case hwmon_in_input: |
| return 0444; |
| } |
| break; |
| case hwmon_curr: |
| switch (attr) { |
| case hwmon_curr_input: |
| if (st->r_sense_uohm) |
| return 0444; |
| return 0; |
| case hwmon_curr_max_alarm: |
| if (st->r_sense_uohm) |
| return 0644; |
| return 0; |
| } |
| break; |
| default: |
| break; |
| } |
| return 0; |
| } |
| |
| static const struct hwmon_channel_info *adm1177_info[] = { |
| HWMON_CHANNEL_INFO(curr, |
| HWMON_C_INPUT | HWMON_C_MAX_ALARM), |
| HWMON_CHANNEL_INFO(in, |
| HWMON_I_INPUT), |
| NULL |
| }; |
| |
| static const struct hwmon_ops adm1177_hwmon_ops = { |
| .is_visible = adm1177_is_visible, |
| .read = adm1177_read, |
| .write = adm1177_write, |
| }; |
| |
| static const struct hwmon_chip_info adm1177_chip_info = { |
| .ops = &adm1177_hwmon_ops, |
| .info = adm1177_info, |
| }; |
| |
| static void adm1177_remove(void *data) |
| { |
| struct adm1177_state *st = data; |
| |
| regulator_disable(st->reg); |
| } |
| |
| static int adm1177_probe(struct i2c_client *client) |
| { |
| struct device *dev = &client->dev; |
| struct device *hwmon_dev; |
| struct adm1177_state *st; |
| u32 alert_threshold_ua; |
| int ret; |
| |
| st = devm_kzalloc(dev, sizeof(*st), GFP_KERNEL); |
| if (!st) |
| return -ENOMEM; |
| |
| st->client = client; |
| |
| st->reg = devm_regulator_get_optional(&client->dev, "vref"); |
| if (IS_ERR(st->reg)) { |
| if (PTR_ERR(st->reg) == -EPROBE_DEFER) |
| return -EPROBE_DEFER; |
| |
| st->reg = NULL; |
| } else { |
| ret = regulator_enable(st->reg); |
| if (ret) |
| return ret; |
| ret = devm_add_action_or_reset(&client->dev, adm1177_remove, |
| st); |
| if (ret) |
| return ret; |
| } |
| |
| if (device_property_read_u32(dev, "shunt-resistor-micro-ohms", |
| &st->r_sense_uohm)) |
| st->r_sense_uohm = 0; |
| if (device_property_read_u32(dev, "adi,shutdown-threshold-microamp", |
| &alert_threshold_ua)) { |
| if (st->r_sense_uohm) |
| /* |
| * set maximum default value from datasheet based on |
| * shunt-resistor |
| */ |
| alert_threshold_ua = div_u64(105840000000, |
| st->r_sense_uohm); |
| else |
| alert_threshold_ua = 0; |
| } |
| st->vrange_high = device_property_read_bool(dev, |
| "adi,vrange-high-enable"); |
| if (alert_threshold_ua && st->r_sense_uohm) |
| adm1177_write_alert_thr(st, alert_threshold_ua); |
| |
| ret = adm1177_write_cmd(st, ADM1177_CMD_V_CONT | |
| ADM1177_CMD_I_CONT | |
| (st->vrange_high ? 0 : ADM1177_CMD_VRANGE)); |
| if (ret) |
| return ret; |
| |
| hwmon_dev = |
| devm_hwmon_device_register_with_info(dev, client->name, st, |
| &adm1177_chip_info, NULL); |
| return PTR_ERR_OR_ZERO(hwmon_dev); |
| } |
| |
| static const struct i2c_device_id adm1177_id[] = { |
| {"adm1177", 0}, |
| {} |
| }; |
| MODULE_DEVICE_TABLE(i2c, adm1177_id); |
| |
| static const struct of_device_id adm1177_dt_ids[] = { |
| { .compatible = "adi,adm1177" }, |
| {}, |
| }; |
| MODULE_DEVICE_TABLE(of, adm1177_dt_ids); |
| |
| static struct i2c_driver adm1177_driver = { |
| .class = I2C_CLASS_HWMON, |
| .driver = { |
| .name = "adm1177", |
| .of_match_table = adm1177_dt_ids, |
| }, |
| .probe_new = adm1177_probe, |
| .id_table = adm1177_id, |
| }; |
| module_i2c_driver(adm1177_driver); |
| |
| MODULE_AUTHOR("Beniamin Bia <beniamin.bia@analog.com>"); |
| MODULE_AUTHOR("Michael Hennerich <michael.hennerich@analog.com>"); |
| MODULE_DESCRIPTION("Analog Devices ADM1177 ADC driver"); |
| MODULE_LICENSE("GPL v2"); |