diff options
Diffstat (limited to 'drivers/hwmon/hwmon.c')
-rw-r--r-- | drivers/hwmon/hwmon.c | 485 |
1 files changed, 458 insertions, 27 deletions
diff --git a/drivers/hwmon/hwmon.c b/drivers/hwmon/hwmon.c index 649a68d119b4..3e4cc442a089 100644 --- a/drivers/hwmon/hwmon.c +++ b/drivers/hwmon/hwmon.c @@ -12,6 +12,7 @@ #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt +#include <linux/bitops.h> #include <linux/device.h> #include <linux/err.h> #include <linux/gfp.h> @@ -21,6 +22,7 @@ #include <linux/pci.h> #include <linux/slab.h> #include <linux/string.h> +#include <linux/thermal.h> #define HWMON_ID_PREFIX "hwmon" #define HWMON_ID_FORMAT HWMON_ID_PREFIX "%d" @@ -28,9 +30,35 @@ struct hwmon_device { const char *name; struct device dev; + const struct hwmon_chip_info *chip; + + struct attribute_group group; + const struct attribute_group **groups; }; + #define to_hwmon_device(d) container_of(d, struct hwmon_device, dev) +struct hwmon_device_attribute { + struct device_attribute dev_attr; + const struct hwmon_ops *ops; + enum hwmon_sensor_types type; + u32 attr; + int index; +}; + +#define to_hwmon_attr(d) \ + container_of(d, struct hwmon_device_attribute, dev_attr) + +/* + * Thermal zone information + * In addition to the reference to the hwmon device, + * also provides the sensor index. + */ +struct hwmon_thermal_data { + struct hwmon_device *hwdev; /* Reference to hwmon device */ + int index; /* sensor index */ +}; + static ssize_t show_name(struct device *dev, struct device_attribute *attr, char *buf) { @@ -78,25 +106,286 @@ static struct class hwmon_class = { static DEFINE_IDA(hwmon_ida); -/** - * hwmon_device_register_with_groups - register w/ hwmon - * @dev: the parent device - * @name: hwmon name attribute - * @drvdata: driver data to attach to created device - * @groups: List of attribute groups to create - * - * hwmon_device_unregister() must be called when the device is no - * longer needed. - * - * Returns the pointer to the new device. - */ -struct device * -hwmon_device_register_with_groups(struct device *dev, const char *name, - void *drvdata, - const struct attribute_group **groups) +/* Thermal zone handling */ + +#if IS_REACHABLE(CONFIG_THERMAL) && defined(CONFIG_THERMAL_OF) +static int hwmon_thermal_get_temp(void *data, int *temp) +{ + struct hwmon_thermal_data *tdata = data; + struct hwmon_device *hwdev = tdata->hwdev; + int ret; + long t; + + ret = hwdev->chip->ops->read(&hwdev->dev, hwmon_temp, hwmon_temp_input, + tdata->index, &t); + if (ret < 0) + return ret; + + *temp = t; + + return 0; +} + +static struct thermal_zone_of_device_ops hwmon_thermal_ops = { + .get_temp = hwmon_thermal_get_temp, +}; + +static int hwmon_thermal_add_sensor(struct device *dev, + struct hwmon_device *hwdev, int index) +{ + struct hwmon_thermal_data *tdata; + + tdata = devm_kzalloc(dev, sizeof(*tdata), GFP_KERNEL); + if (!tdata) + return -ENOMEM; + + tdata->hwdev = hwdev; + tdata->index = index; + + devm_thermal_zone_of_sensor_register(&hwdev->dev, index, tdata, + &hwmon_thermal_ops); + + return 0; +} +#else +static int hwmon_thermal_add_sensor(struct device *dev, + struct hwmon_device *hwdev, int index) +{ + return 0; +} +#endif /* IS_REACHABLE(CONFIG_THERMAL) && defined(CONFIG_THERMAL_OF) */ + +/* sysfs attribute management */ + +static ssize_t hwmon_attr_show(struct device *dev, + struct device_attribute *devattr, char *buf) +{ + struct hwmon_device_attribute *hattr = to_hwmon_attr(devattr); + long val; + int ret; + + ret = hattr->ops->read(dev, hattr->type, hattr->attr, hattr->index, + &val); + if (ret < 0) + return ret; + + return sprintf(buf, "%ld\n", val); +} + +static ssize_t hwmon_attr_store(struct device *dev, + struct device_attribute *devattr, + const char *buf, size_t count) +{ + struct hwmon_device_attribute *hattr = to_hwmon_attr(devattr); + long val; + int ret; + + ret = kstrtol(buf, 10, &val); + if (ret < 0) + return ret; + + ret = hattr->ops->write(dev, hattr->type, hattr->attr, hattr->index, + val); + if (ret < 0) + return ret; + + return count; +} + +static int hwmon_attr_base(enum hwmon_sensor_types type) +{ + if (type == hwmon_in) + return 0; + return 1; +} + +static struct attribute *hwmon_genattr(struct device *dev, + const void *drvdata, + enum hwmon_sensor_types type, + u32 attr, + int index, + const char *template, + const struct hwmon_ops *ops) +{ + struct hwmon_device_attribute *hattr; + struct device_attribute *dattr; + struct attribute *a; + umode_t mode; + char *name; + + /* The attribute is invisible if there is no template string */ + if (!template) + return ERR_PTR(-ENOENT); + + mode = ops->is_visible(drvdata, type, attr, index); + if (!mode) + return ERR_PTR(-ENOENT); + + if ((mode & S_IRUGO) && !ops->read) + return ERR_PTR(-EINVAL); + if ((mode & S_IWUGO) && !ops->write) + return ERR_PTR(-EINVAL); + + if (type == hwmon_chip) { + name = (char *)template; + } else { + name = devm_kzalloc(dev, strlen(template) + 16, GFP_KERNEL); + if (!name) + return ERR_PTR(-ENOMEM); + scnprintf(name, strlen(template) + 16, template, + index + hwmon_attr_base(type)); + } + + hattr = devm_kzalloc(dev, sizeof(*hattr), GFP_KERNEL); + if (!hattr) + return ERR_PTR(-ENOMEM); + + hattr->type = type; + hattr->attr = attr; + hattr->index = index; + hattr->ops = ops; + + dattr = &hattr->dev_attr; + dattr->show = hwmon_attr_show; + dattr->store = hwmon_attr_store; + + a = &dattr->attr; + sysfs_attr_init(a); + a->name = name; + a->mode = mode; + + return a; +} + +static const char * const hwmon_chip_attr_templates[] = { + [hwmon_chip_temp_reset_history] = "temp_reset_history", + [hwmon_chip_update_interval] = "update_interval", + [hwmon_chip_alarms] = "alarms", +}; + +static const char * const hwmon_temp_attr_templates[] = { + [hwmon_temp_input] = "temp%d_input", + [hwmon_temp_type] = "temp%d_type", + [hwmon_temp_lcrit] = "temp%d_lcrit", + [hwmon_temp_lcrit_hyst] = "temp%d_lcrit_hyst", + [hwmon_temp_min] = "temp%d_min", + [hwmon_temp_min_hyst] = "temp%d_min_hyst", + [hwmon_temp_max] = "temp%d_max", + [hwmon_temp_max_hyst] = "temp%d_max_hyst", + [hwmon_temp_crit] = "temp%d_crit", + [hwmon_temp_crit_hyst] = "temp%d_crit_hyst", + [hwmon_temp_emergency] = "temp%d_emergency", + [hwmon_temp_emergency_hyst] = "temp%d_emergency_hyst", + [hwmon_temp_alarm] = "temp%d_alarm", + [hwmon_temp_lcrit_alarm] = "temp%d_lcrit_alarm", + [hwmon_temp_min_alarm] = "temp%d_min_alarm", + [hwmon_temp_max_alarm] = "temp%d_max_alarm", + [hwmon_temp_crit_alarm] = "temp%d_crit_alarm", + [hwmon_temp_emergency_alarm] = "temp%d_emergency_alarm", + [hwmon_temp_fault] = "temp%d_fault", + [hwmon_temp_offset] = "temp%d_offset", + [hwmon_temp_label] = "temp%d_label", + [hwmon_temp_lowest] = "temp%d_lowest", + [hwmon_temp_highest] = "temp%d_highest", + [hwmon_temp_reset_history] = "temp%d_reset_history", +}; + +static const char * const *__templates[] = { + [hwmon_chip] = hwmon_chip_attr_templates, + [hwmon_temp] = hwmon_temp_attr_templates, +}; + +static const int __templates_size[] = { + [hwmon_chip] = ARRAY_SIZE(hwmon_chip_attr_templates), + [hwmon_temp] = ARRAY_SIZE(hwmon_temp_attr_templates), +}; + +static int hwmon_num_channel_attrs(const struct hwmon_channel_info *info) +{ + int i, n; + + for (i = n = 0; info->config[i]; i++) + n += hweight32(info->config[i]); + + return n; +} + +static int hwmon_genattrs(struct device *dev, + const void *drvdata, + struct attribute **attrs, + const struct hwmon_ops *ops, + const struct hwmon_channel_info *info) +{ + const char * const *templates; + int template_size; + int i, aindex = 0; + + if (info->type >= ARRAY_SIZE(__templates)) + return -EINVAL; + + templates = __templates[info->type]; + template_size = __templates_size[info->type]; + + for (i = 0; info->config[i]; i++) { + u32 attr_mask = info->config[i]; + u32 attr; + + while (attr_mask) { + struct attribute *a; + + attr = __ffs(attr_mask); + attr_mask &= ~BIT(attr); + if (attr >= template_size) + return -EINVAL; + a = hwmon_genattr(dev, drvdata, info->type, attr, i, + templates[attr], ops); + if (IS_ERR(a)) { + if (PTR_ERR(a) != -ENOENT) + return PTR_ERR(a); + continue; + } + attrs[aindex++] = a; + } + } + return aindex; +} + +static struct attribute ** +__hwmon_create_attrs(struct device *dev, const void *drvdata, + const struct hwmon_chip_info *chip) +{ + int ret, i, aindex = 0, nattrs = 0; + struct attribute **attrs; + + for (i = 0; chip->info[i]; i++) + nattrs += hwmon_num_channel_attrs(chip->info[i]); + + if (nattrs == 0) + return ERR_PTR(-EINVAL); + + attrs = devm_kcalloc(dev, nattrs + 1, sizeof(*attrs), GFP_KERNEL); + if (!attrs) + return ERR_PTR(-ENOMEM); + + for (i = 0; chip->info[i]; i++) { + ret = hwmon_genattrs(dev, drvdata, &attrs[aindex], chip->ops, + chip->info[i]); + if (ret < 0) + return ERR_PTR(ret); + aindex += ret; + } + + return attrs; +} + +static struct device * +__hwmon_device_register(struct device *dev, const char *name, void *drvdata, + const struct hwmon_chip_info *chip, + const struct attribute_group **groups) { struct hwmon_device *hwdev; - int err, id; + struct device *hdev; + int i, j, err, id; /* Do not accept invalid characters in hwmon name attribute */ if (name && (!strlen(name) || strpbrk(name, "-* \t\n"))) @@ -112,28 +401,128 @@ hwmon_device_register_with_groups(struct device *dev, const char *name, goto ida_remove; } + hdev = &hwdev->dev; + + if (chip && chip->ops->is_visible) { + struct attribute **attrs; + int ngroups = 2; + + if (groups) + for (i = 0; groups[i]; i++) + ngroups++; + + hwdev->groups = devm_kcalloc(dev, ngroups, sizeof(*groups), + GFP_KERNEL); + if (!hwdev->groups) + return ERR_PTR(-ENOMEM); + + attrs = __hwmon_create_attrs(dev, drvdata, chip); + if (IS_ERR(attrs)) { + err = PTR_ERR(attrs); + goto free_hwmon; + } + + hwdev->group.attrs = attrs; + ngroups = 0; + hwdev->groups[ngroups++] = &hwdev->group; + + if (groups) { + for (i = 0; groups[i]; i++) + hwdev->groups[ngroups++] = groups[i]; + } + + hdev->groups = hwdev->groups; + } else { + hdev->groups = groups; + } + hwdev->name = name; - hwdev->dev.class = &hwmon_class; - hwdev->dev.parent = dev; - hwdev->dev.groups = groups; - hwdev->dev.of_node = dev ? dev->of_node : NULL; - dev_set_drvdata(&hwdev->dev, drvdata); - dev_set_name(&hwdev->dev, HWMON_ID_FORMAT, id); - err = device_register(&hwdev->dev); + hdev->class = &hwmon_class; + hdev->parent = dev; + hdev->of_node = dev ? dev->of_node : NULL; + hwdev->chip = chip; + dev_set_drvdata(hdev, drvdata); + dev_set_name(hdev, HWMON_ID_FORMAT, id); + err = device_register(hdev); if (err) - goto free; + goto free_hwmon; + + if (chip && chip->ops->is_visible && chip->ops->read && + chip->info[0]->type == hwmon_chip && + (chip->info[0]->config[0] & HWMON_C_REGISTER_TZ)) { + const struct hwmon_channel_info **info = chip->info; + + for (i = 1; info[i]; i++) { + if (info[i]->type != hwmon_temp) + continue; + + for (j = 0; info[i]->config[j]; j++) { + if (!chip->ops->is_visible(drvdata, hwmon_temp, + hwmon_temp_input, j)) + continue; + if (info[i]->config[j] & HWMON_T_INPUT) + hwmon_thermal_add_sensor(dev, hwdev, j); + } + } + } - return &hwdev->dev; + return hdev; -free: +free_hwmon: kfree(hwdev); ida_remove: ida_simple_remove(&hwmon_ida, id); return ERR_PTR(err); } + +/** + * hwmon_device_register_with_groups - register w/ hwmon + * @dev: the parent device + * @name: hwmon name attribute + * @drvdata: driver data to attach to created device + * @groups: List of attribute groups to create + * + * hwmon_device_unregister() must be called when the device is no + * longer needed. + * + * Returns the pointer to the new device. + */ +struct device * +hwmon_device_register_with_groups(struct device *dev, const char *name, + void *drvdata, + const struct attribute_group **groups) +{ + return __hwmon_device_register(dev, name, drvdata, NULL, groups); +} EXPORT_SYMBOL_GPL(hwmon_device_register_with_groups); /** + * hwmon_device_register_with_info - register w/ hwmon + * @dev: the parent device + * @name: hwmon name attribute + * @drvdata: driver data to attach to created device + * @info: Pointer to hwmon chip information + * @groups - pointer to list of driver specific attribute groups + * + * hwmon_device_unregister() must be called when the device is no + * longer needed. + * + * Returns the pointer to the new device. + */ +struct device * +hwmon_device_register_with_info(struct device *dev, const char *name, + void *drvdata, + const struct hwmon_chip_info *chip, + const struct attribute_group **groups) +{ + if (chip && (!chip->ops || !chip->info)) + return ERR_PTR(-EINVAL); + + return __hwmon_device_register(dev, name, drvdata, chip, groups); +} +EXPORT_SYMBOL_GPL(hwmon_device_register_with_info); + +/** * hwmon_device_register - register w/ hwmon * @dev: the device to register * @@ -211,6 +600,48 @@ error: } EXPORT_SYMBOL_GPL(devm_hwmon_device_register_with_groups); +/** + * devm_hwmon_device_register_with_info - register w/ hwmon + * @dev: the parent device + * @name: hwmon name attribute + * @drvdata: driver data to attach to created device + * @info: Pointer to hwmon chip information + * @groups - pointer to list of driver specific attribute groups + * + * Returns the pointer to the new device. The new device is automatically + * unregistered with the parent device. + */ +struct device * +devm_hwmon_device_register_with_info(struct device *dev, const char *name, + void *drvdata, + const struct hwmon_chip_info *chip, + const struct attribute_group **groups) +{ + struct device **ptr, *hwdev; + + if (!dev) + return ERR_PTR(-EINVAL); + + ptr = devres_alloc(devm_hwmon_release, sizeof(*ptr), GFP_KERNEL); + if (!ptr) + return ERR_PTR(-ENOMEM); + + hwdev = hwmon_device_register_with_info(dev, name, drvdata, chip, + groups); + if (IS_ERR(hwdev)) + goto error; + + *ptr = hwdev; + devres_add(dev, ptr); + + return hwdev; + +error: + devres_free(ptr); + return hwdev; +} +EXPORT_SYMBOL_GPL(devm_hwmon_device_register_with_info); + static int devm_hwmon_match(struct device *dev, void *res, void *data) { struct device **hwdev = res; |