Re: [PATCH] hwmon: New driver for Texas Instruments TPS2480/TPS2481

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



Hi Hadrien,

On 05/21/2015 08:26 AM, Hadrien Copponnex wrote:
Attached is a patch which adds hwmon support for Texas Instruments TPS2480/TPS2481 current sensor. The code follows the same architecture found in other hwmon drivers and should be pretty easy to understand.

The chip reports measured current as the voltage drop across a shunt resistor. This value is reported in the curr1_input attribute as 10 uV steps. It is then the responsibility of the user to convert back this value in amperes to have the correct value (for instance using sensors.conf file).

The current is really reported in the current register. The voltage drop is just that, a voltage drop,
and should be reported as voltage.

Many parameters affecting the chip operation are exported through sysfs interface and thus are available for configuration from userland. Even if it is not necessary to configure the chip before usage, there might be cases where a user needs to do so.

To apply the patch on the latest kernel, simply type (assuming filename <patch>):
git apply --check <patch>

To use the driver, enable the option "SENSORS_TPS2480", in your kernel configuration file. I2C/SMBus support is required in your kernel. The code has been written in an architecture-independent way, but have only been tested on PowerPC architecture (big-endian).

As this is my first patch submitted for the Linux kernel, it is possible that the approach I used is not correct, notably the way the driver reports current/voltage information and the exported chip parameters. I don't know if it is necessary to export that much information/configuration for a simple chip like this one, or if I should keep it simple, and use the chip in the default configuration.

Please let me know any feedback you have on this patch.

Please read and follow Documentation/hwmon/submitting-patches,
and consider what it says as overall feedback. For the most
part, I won't comment on items covered by this document.
Some more comments inline.

Thanks,
Guenter

Best regards,
Hadrien Copponnex
R&D Engineer

AppearTV AS
www.appeartv.com

Signed-off-by: Hadrien Copponnex <hadrien.copponnex@xxxxxxxxxxxx>

---
  drivers/hwmon/Kconfig   |   10 ++
  drivers/hwmon/Makefile  |    1 +
  drivers/hwmon/tps2480.c |  386 +++++++++++++++++++++++++++++++++++++++++++++++

Please also provide Documentation/hwmon/tps2480.

  3 files changed, 397 insertions(+)
  create mode 100644 drivers/hwmon/tps2480.c

diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index 25d9e72..7919e7d 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -1504,6 +1504,16 @@ config SENSORS_TMP421
        This driver can also be built as a module.  If so, the module
        will be called tmp421.

+config SENSORS_TPS2480
+    tristate "Texas Instruments TPS2480/2481 (EXPERIMENTAL)"
+    depends on I2C
+    help
+      If you say yes here you get support for Texas Instruments TPS2480
+      and TPS2481 sensor chips.
+
Those are not sensor chips. "Hotswap controllers", maybe ?

+      This driver can also be built as a module.  If so, the module
+      will be called tps2480.
+
  config SENSORS_TWL4030_MADC
      tristate "Texas Instruments TWL4030 MADC Hwmon"
      depends on TWL4030_MADC
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index b4a40f1..b640247 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -145,6 +145,7 @@ obj-$(CONFIG_SENSORS_TMP102)    += tmp102.o
  obj-$(CONFIG_SENSORS_TMP103)    += tmp103.o
  obj-$(CONFIG_SENSORS_TMP401)    += tmp401.o
  obj-$(CONFIG_SENSORS_TMP421)    += tmp421.o
+obj-$(CONFIG_SENSORS_TPS2480)   += tps2480.o
  obj-$(CONFIG_SENSORS_TWL4030_MADC)+= twl4030-madc-hwmon.o
  obj-$(CONFIG_SENSORS_VEXPRESS)    += vexpress.o
  obj-$(CONFIG_SENSORS_VIA_CPUTEMP)+= via-cputemp.o
diff --git a/drivers/hwmon/tps2480.c b/drivers/hwmon/tps2480.c
new file mode 100644
index 0000000..caaf847
--- /dev/null
+++ b/drivers/hwmon/tps2480.c
@@ -0,0 +1,386 @@
+/*
+ * Hardware monitoring driver for TI TPS2480 / TPS2481
+ *
+ * Copyright (c) 2014 AppearTV AS
+ *

2015 ?

+ * Relevant documentation:
+ *    http://www.ti.com/lit/ds/symlink/tps2480.pdf
+ *
Plus tps2481 ? it might be better to point to the datasheet in the
documentation file. This way it only needs to be updated once.

+ * Parts of this file are based from:
+ *    ltc4151.c (tps2480_update_device function)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#include <linux/kernel.h>
+#include <linux/delay.h>

Is this needed ?

+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/err.h>
+#include <linux/i2c.h>
+#include <linux/slab.h>
+#include <linux/hwmon.h>
+#include <linux/hwmon-sysfs.h>
+#include <linux/jiffies.h>
+
Please provide include files in alphabetical order.

+#define TPS2480_REG_CONFIG        0x00 /* R/W */
+#define TPS2480_REG_SHUNT_VOLTAGE 0x01 /* R   */
+#define TPS2480_REG_BUS_VOLTAGE   0x02 /* R   */
+#define TPS2480_REG_POWER         0x03 /* R   */
+#define TPS2480_REG_CURRENT       0x04 /* R   */
+#define TPS2480_REG_CALIBRATION   0x05 /* R/W */
+#define TPS2480_MAX_REGISTERS     6
+
+struct tps2480_data {
+    struct device  *hwmon_dev;

Not needed (see below).

+    struct mutex    lock;
+    u16             registers[TPS2480_MAX_REGISTERS];
+    unsigned long   last_updated;
+    u8              valid;

Please use bool.

+} tps2480_data;
+
+/*
+ * Chip operating functions
+ */
+
+static s32 write_register(struct i2c_client *client, u8 reg, u16 value)
+{
+    s32 err = i2c_smbus_write_word_data(client, reg, cpu_to_le16(value));
+
i2c_smbus_write_word_data does not need cpu_to_le16(). If the byte order is wrong,
you need to use i2c_smbus_write_word_swapped().

+    if (err)
+        dev_err(&client->dev, "cannot write i2c command");
+    return err;
+}

Please just use i2c_smbus_write_word_data (or probably i2c_smbus_write_word_swapped)
and return the error to use space.

+
+static s32 reset_chip(struct device *dev)
+{
+    struct i2c_client *client = to_i2c_client(dev);
+    struct tps2480_data *data = i2c_get_clientdata(client);
+
+    mutex_lock(&data->lock);
+    data->valid = 0;
+    mutex_unlock(&data->lock);

This mutex does not protect anything.

+
+    return write_register(client, TPS2480_REG_CONFIG, 1 << 15);

If really needed, this would probably leave the chip unconfigured.
Is this what you want ? Also, are you sure this is needed ?
It is quite unusual, and may result in a system reset.
If that is the idea, it would be better to register a restart handler.
See register_restart_handler() / unregister_restart_handler().

+}
+
+/* returns the driver data associated to the device
+ * in addition, reads the latest status from registers
+ * don't forget to release the lock when you have finished using the data
+ * structure
+ */
+static struct tps2480_data *tps2480_update_device(struct device *dev)
+{
+    struct i2c_client *client = to_i2c_client(dev);
+    struct tps2480_data *data = i2c_get_clientdata(client);
+    u8 i;
+
Please just use int. It generates better code on most architectures.

+    mutex_lock(&data->lock);
+
+    if (time_after(jiffies, data->last_updated + HZ + HZ / 2) ||

With this chip, it probably doesn't make much sense to use
this much caching time. Worst case conversion time is 68ms.
Given that, it might not even make sense to cache anything,
and to just read the registers directly.

You might consider using regmap instead of direct i2c accesses.
Regmap could be configured to cache the configuration and calibration
registers permanently, which would probably make sense.

+        !data->valid) {
+        /* read data and update device structure */
+        for (i = 0; i < TPS2480_MAX_REGISTERS; ++i) {
+            s32 val = i2c_smbus_read_word_data(client, i);
+
+            if (val < 0) {
+                data->valid = 0;
+                goto finish;
+            }
+            data->registers[i] = le16_to_cpu((u16)val);

i2c_smbus_read_word_data returns data in host byte order. If the byte order
is wrong, you would need to use use i2c_smbus_read_word_swapped().

+        }
+        data->valid = 1;
+        data->last_updated = jiffies;
+    }
+
+finish:

I don't see an unlock operation, which is highly unusual (more on that below).

+    return data;
+}
+
+/*
+ * Sysfs callbacks
+ */
+
+static ssize_t perform_show(struct device  *dev, char *buf, u8 reg, const u16
+*p, const char *fmt)
+{
+    struct tps2480_data *data = tps2480_update_device(dev);
+    u16 value = data->registers[reg];

This is an unsigned short which may be displayed as short with %hd.
Only reason for the compiler not to complain is probably that the format
is passed as parameter. This results in the compiler not being able
to do format checking, which is really nice to have. One more reason
to use individual functions for the different attributes.

+    u8 valid = data->valid;
+
Unnecessary variable.

+    mutex_unlock(&data->lock);
+    if (!valid)

Ah, this is where the unlock is hiding. Please don't do that,
and keep the locking within one function. Please have
tps2480_update_device() return the actual error,
and return that error to user space.

+        return -EIO;

Replacing one error with another is never a good idea. Static checkers
will complain about that if they catch it.

+    value = (value >> p[1]) & p[0];

The printed value must follow the ABI (mV, mA, uW). Given the
different calculations necessary, it might be better to just use
a separate function for each attribute.

Per the datasheets, the shunt voltage must include the programmed gain.
Current and power depend on the calibration values, so you'll have to
do some complex calculations to convert the raw values into mA and uW.

+    return snprintf(buf, PAGE_SIZE, fmt, value);
+}
+
+static ssize_t perform_store(struct device *dev, const char *buf, size_t count,
+u8 reg, const u16 *p)
+{

As with the show command, I would suggest to use separate write functions
for each (writable) attribute.

+    struct tps2480_data *data = tps2480_update_device(dev);

Not needed here. You don't need to read anything from the chip
to update one of its registers.

+    struct i2c_client *client = to_i2c_client(dev);
+    unsigned long val = 0;
+    ssize_t result = count;
+    s32 err;
+
+    if (kstrtoul(buf, 10, &val) || val > p[0]) {
+        result = -EINVAL;

kstrtoul() returns an error, which you should return

+        goto finish;

directly without locking / goto.

+    }
+
Locking is needed from here

+    data->registers[reg] = (val & p[0]) << p[1];
+
+    err = write_register(client, reg, data->registers[reg]);

to here.

+    if (err)
+        result = err;
+
+finish:
+    mutex_unlock(&data->lock);

but not here.

+
+    return result;
+}
+
+/* Variable parameters
+   [0]: mask
+   [1]: shift */
+static const u16 in0[2] = {0xfff8, 0x01};
+static const u16 cu1[2] = {0xffff, 0x00};
+static const u16 cu2[2] = {0xffff, 0x00};
+static const u16 pow[2] = {0xffff, 0x00};
+static const u16 bvr[2] = {0x0001, 0x0d};
+static const u16 pga[2] = {0x0003, 0x0b};
+static const u16 bad[2] = {0x000f, 0x07};
+static const u16 sad[2] = {0x000f, 0x03};
+static const u16 mod[2] = {0x0007, 0x00};
+static const u16 cal[2] = {0xffff, 0x00};
+
+static ssize_t show_in0(struct device *dev, struct device_attribute *attr, char
+*buf)
+{
+    return perform_show(dev, buf, TPS2480_REG_BUS_VOLTAGE, in0, "%hu\n");
+}
+
+static ssize_t show_curr1(struct device *dev, struct device_attribute *attr,
+char *buf)
+{
+    return perform_show(dev, buf, TPS2480_REG_SHUNT_VOLTAGE, cu1, "%hd\n");
+}
+
+static ssize_t show_curr2(struct device *dev, struct device_attribute *attr,
+char *buf)
+{
+    return perform_show(dev, buf, TPS2480_REG_CURRENT, cu2, "%hd\n");
+}
+
+static ssize_t show_power(struct device *dev, struct device_attribute *attr,
+char *buf)
+{
+    return perform_show(dev, buf, TPS2480_REG_POWER, pow, "%hu\n");
+}
+
+static ssize_t store_reset(struct device *dev, struct device_attribute *attr,
+const char *buf, size_t count)
+{
+    s32 err = reset_chip(dev);
+
+    return err ? err : count;
+}
+
+static ssize_t show_bus_voltage_range(struct device *dev, struct
+device_attribute *attr, char *buf)
+{
+    return perform_show(dev, buf, TPS2480_REG_CONFIG, bvr, "%hu\n");
+}
+
+static ssize_t store_bus_voltage_range(struct device *dev, struct
+device_attribute *attr, const char *buf, size_t count)
+{
+    return perform_store(dev, buf, count, TPS2480_REG_CONFIG, bvr);
+}
+
+static ssize_t show_pga(struct device *dev, struct device_attribute *attr, char
+*buf)
+{
+    return perform_show(dev, buf, TPS2480_REG_CONFIG, pga, "%hu\n");
+}
+
+static ssize_t store_pga(struct device *dev, struct device_attribute *attr,
+const char *buf, size_t count)
+{
+    return perform_store(dev, buf, count, TPS2480_REG_CONFIG, pga);
+}
+
+static ssize_t show_badc(struct device *dev, struct device_attribute *attr,
+char *buf)
+{
+    return perform_show(dev, buf, TPS2480_REG_CONFIG, bad, "%hu\n");
+}
+
+static ssize_t store_badc(struct device *dev, struct device_attribute *attr,
+const char *buf, size_t count)
+{
+    return perform_store(dev, buf, count, TPS2480_REG_CONFIG, bad);
+}
+
+static ssize_t show_sadc(struct device *dev, struct device_attribute *attr,
+char *buf)
+{
+    return perform_show(dev, buf, TPS2480_REG_CONFIG, sad, "%hu\n");
+}
+
+static ssize_t store_sadc(struct device *dev, struct device_attribute *attr,
+const char *buf, size_t count)
+{
+    return perform_store(dev, buf, count, TPS2480_REG_CONFIG, sad);
+}
+
+static ssize_t show_mode(struct device *dev, struct device_attribute *attr,
+char *buf)
+{
+    return perform_show(dev, buf, TPS2480_REG_CONFIG, mod, "%hu\n");
+}
+
+static ssize_t store_mode(struct device *dev, struct device_attribute *attr,
+const char *buf, size_t count)
+{
+    return perform_store(dev, buf, count, TPS2480_REG_CONFIG, mod);
+}
+
+static ssize_t show_calibration(struct device *dev, struct device_attribute
+*attr, char *buf)
+{
+    return perform_show(dev, buf, TPS2480_REG_CALIBRATION, cal, "%hu\n");
+}
+
+static ssize_t store_calibration(struct device *dev, struct device_attribute
+*attr, const char  *buf, size_t count)
+{
+    return perform_store(dev, buf, count, TPS2480_REG_CALIBRATION, cal);
+}
+
+static SENSOR_DEVICE_ATTR(in0_input, S_IRUGO, show_in0, NULL, 0); /* Bus
+voltage */

You don't really use the parameter (last value), so providing it is quite useless.

the bus voltage has an alarm bit, so you could provide in0_alarm.

+static SENSOR_DEVICE_ATTR(curr1_input, S_IRUGO, show_curr1, NULL, 1); /* Shunt
+voltage */

This is a voltage, not a current ??
Also, the shunt voltage can be negative per the datasheet.

+static SENSOR_DEVICE_ATTR(curr2_input, S_IRUGO, show_curr2, NULL, 1); /*
+Current */
+static SENSOR_DEVICE_ATTR(power_input, S_IRUGO, show_power, NULL, 1); /*
+Power */

power1_input

+static SENSOR_DEVICE_ATTR(reset, S_IWUSR, NULL, store_reset, 0); /* Reset */

I don't think this should be exported.

+static SENSOR_DEVICE_ATTR(bus_voltage_range, S_IRUGO | S_IWUSR,
+show_bus_voltage_range, store_bus_voltage_range, 0); /* Bus voltage range */

Maybe in1_max.

+static SENSOR_DEVICE_ATTR(pga, S_IRUGO | S_IWUSR, show_pga, store_pga, 0); /*
+PGA */
+static SENSOR_DEVICE_ATTR(badc, S_IRUGO | S_IWUSR, show_badc, store_badc, 0);
+/* BADC */
+static SENSOR_DEVICE_ATTR(sadc, S_IRUGO | S_IWUSR, show_sadc, store_sadc, 0);
+/* SADC */
+static SENSOR_DEVICE_ATTR(mode, S_IRUGO | S_IWUSR, show_mode, store_mode, 0);
+/* Mode */
+static SENSOR_DEVICE_ATTR(calibration, S_IRUGO | S_IWUSR, show_calibration,
+store_calibration, 0); /* Calibration */
+
Those are not standard attributes supported by the ABI. Where needed, they
should be provided either with platform data or with devicetree data or both,
but not with attributes. It might make sense to provide some abstraction
instead of just raw register values.

Some of the parameters may be implementable as in[01]_max (for shunt and bus
voltage range) and curr1_max (for the maximum supported current).

You could use the update_interval attribute to set the conversion time,
though that is in milli-seconds so you'd only affect the number of samples
taken per value.

+static struct attribute *tps2480_attrs[] = {
+    &sensor_dev_attr_in0_input.dev_attr.attr,
+    &sensor_dev_attr_curr1_input.dev_attr.attr,
+    &sensor_dev_attr_curr2_input.dev_attr.attr,
+    &sensor_dev_attr_power_input.dev_attr.attr,
+    &sensor_dev_attr_reset.dev_attr.attr,
+    &sensor_dev_attr_bus_voltage_range.dev_attr.attr,
+    &sensor_dev_attr_pga.dev_attr.attr,
+    &sensor_dev_attr_badc.dev_attr.attr,
+    &sensor_dev_attr_sadc.dev_attr.attr,
+    &sensor_dev_attr_mode.dev_attr.attr,
+    &sensor_dev_attr_calibration.dev_attr.attr,
+    NULL
+};
+
+static struct attribute_group tps2480_attr_grp = {
+    .attrs = tps2480_attrs,
+};
+
+/*
+ * Driver interface functions
+ */
+
+static int tps2480_probe(struct i2c_client *client, const struct i2c_device_id
+*id)
+{
+    struct tps2480_data *data;
+    int err;
+
+    if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA |
+I2C_FUNC_SMBUS_WORD_DATA)) {
+        dev_err(&client->dev, "i2c adapter doesn't support smbus");
+        return -EOPNOTSUPP;
+    }
+
+    data = devm_kzalloc(&client->dev,
+                        sizeof(struct tps2480_data),
+                        GFP_KERNEL);

Not all continuation lines are needed here.

+    if (data == NULL) {
+        dev_err(&client->dev, "cannot allocate memory");

Memory allocation failures already display an error message.
Another one is not needed.

+        return -ENOMEM;
+    }
+    i2c_set_clientdata(client, data);
+
+    mutex_init(&data->lock);
+
+    err = sysfs_create_group(&client->dev.kobj, &tps2480_attr_grp);
+    if (err) {
+        dev_err(&client->dev, "error creating sysfs attribute group");
+        return err;
+    }
+
+    data->hwmon_dev = hwmon_device_register(&client->dev);

Please use devm_hwmon_device_register_with_groups(). You can then
just return PTR_ERR_OR_ZERO(hwmon_dev), and you don't need to call
sysfs_create_group().

+    if (IS_ERR(data->hwmon_dev)) {
+        err = PTR_ERR(data->hwmon_dev);
+        dev_err(&client->dev, "error registering hwmon device");
+        sysfs_remove_group(&client->dev.kobj, &tps2480_attr_grp);
+        return err;
+    }
+
+    return 0;
+}
+
+static int tps2480_remove(struct i2c_client *client)

Not needed if you use devm_hwmon_device_register_with_groups().

+{
+    struct tps2480_data *data = i2c_get_clientdata(client);
+
+    hwmon_device_unregister(data->hwmon_dev);
+    sysfs_remove_group(&client->dev.kobj, &tps2480_attr_grp);
+
+    return 0;
+}
+
+static const struct i2c_device_id tps2480_id[] = {
+    {"tps2480", 0},

It might make sense to also provide "tps2481" to simplify / support
instantiation on systems with that chip.

+    {}
+};
+
+MODULE_DEVICE_TABLE(i2c, tps2480_id);
+
+static struct i2c_driver tps2480_driver = {
+    .class = I2C_CLASS_HWMON,
+    .driver = {
+           .name = "tps2480",
+           },
+    .probe = tps2480_probe,
+    .remove = tps2480_remove,
+    .id_table = tps2480_id,
+};
+
+module_i2c_driver(tps2480_driver);
+
+MODULE_AUTHOR("Hadrien Copponnex <hadrien.copponnex@xxxxxxxxxxxx>");
+MODULE_DESCRIPTION("I2C/SMBus Driver for TI TPS2480/TPS2481");
+MODULE_LICENSE("GPL");


_______________________________________________
lm-sensors mailing list
lm-sensors@xxxxxxxxxxxxxx
http://lists.lm-sensors.org/mailman/listinfo/lm-sensors




[Index of Archives]     [Linux Kernel]     [Linux Hardware Monitoring]     [Linux USB Devel]     [Linux Audio Users]     [Linux Kernel]     [Linux SCSI]     [Yosemite Backpacking]

  Powered by Linux