This device is an integrated module of the Delta AHE-50DC Open19 power shelf. For lack of proper documentation, this driver has been developed referencing an existing (GPL) driver that was included in a code release from LinkedIn [1]. It provides four fan speeds, four temperatures, and one voltage reading, as well as a handful of warning and fault indicators. [1] https://github.com/linkedin/o19-bmc-firmware/blob/master/meta-openbmc/meta-linkedin/meta-deltapower/recipes-kernel/fancontrol-mod/files/fancontrol.c Signed-off-by: Zev Weiss <zev@xxxxxxxxxxxxxxxxx> --- MAINTAINERS | 6 + drivers/hwmon/Kconfig | 11 ++ drivers/hwmon/Makefile | 1 + drivers/hwmon/delta-ahe50dc-fan.c | 265 ++++++++++++++++++++++++++++++ 4 files changed, 283 insertions(+) create mode 100644 drivers/hwmon/delta-ahe50dc-fan.c diff --git a/MAINTAINERS b/MAINTAINERS index 0ac052200ecb..d3f54b6f9550 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -5425,6 +5425,12 @@ W: https://linuxtv.org T: git git://linuxtv.org/media_tree.git F: drivers/media/platform/sti/delta +DELTA AHE-50DC FAN CONTROL MODULE DRIVER +M: Zev Weiss <zev@xxxxxxxxxxxxxxxxx> +L: linux-hwmon@xxxxxxxxxxxxxxx +S: Maintained +F: drivers/hwmon/delta-ahe50dc-fan.c + DELTA DPS920AB PSU DRIVER M: Robert Marko <robert.marko@xxxxxxxxxx> L: linux-hwmon@xxxxxxxxxxxxxxx diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig index 43e5245874ad..394a9e99fedc 100644 --- a/drivers/hwmon/Kconfig +++ b/drivers/hwmon/Kconfig @@ -505,6 +505,17 @@ config SENSORS_DELL_SMM When option I8K is also enabled this driver provides legacy /proc/i8k userspace interface for i8kutils package. +config SENSORS_DELTA_AHE50DC_FAN + tristate "Delta AHE-50DC fan control module" + depends on I2C + select REGMAP_I2C + help + If you say yes here you get support for the fan control + module of the Delta AHE-50DC Open19 power shelf. + + This driver can also be built as a module. If so, the module + will be called delta-ahe50dc-fan. + config SENSORS_DA9052_ADC tristate "Dialog DA9052/DA9053 ADC" depends on PMIC_DA9052 diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile index 3a1551b3d570..ea73ddab5fd3 100644 --- a/drivers/hwmon/Makefile +++ b/drivers/hwmon/Makefile @@ -63,6 +63,7 @@ obj-$(CONFIG_SENSORS_CORSAIR_PSU) += corsair-psu.o obj-$(CONFIG_SENSORS_DA9052_ADC)+= da9052-hwmon.o obj-$(CONFIG_SENSORS_DA9055)+= da9055-hwmon.o obj-$(CONFIG_SENSORS_DELL_SMM) += dell-smm-hwmon.o +obj-$(CONFIG_SENSORS_DELTA_AHE50DC_FAN) += delta-ahe50dc-fan.o obj-$(CONFIG_SENSORS_DME1737) += dme1737.o obj-$(CONFIG_SENSORS_DRIVETEMP) += drivetemp.o obj-$(CONFIG_SENSORS_DS620) += ds620.o diff --git a/drivers/hwmon/delta-ahe50dc-fan.c b/drivers/hwmon/delta-ahe50dc-fan.c new file mode 100644 index 000000000000..d76301d9128a --- /dev/null +++ b/drivers/hwmon/delta-ahe50dc-fan.c @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Delta AHE-50DC power shelf fan control module driver + * + * Copyright 2021 Zev Weiss <zev@xxxxxxxxxxxxxxxxx> + */ + +#include <linux/module.h> +#include <linux/hwmon.h> +#include <linux/i2c.h> +#include <linux/regmap.h> + +static const u8 AHE50DC_FAN_TEMP_REGS[] = { 0x8d, 0x8e, 0x8f, 0xd0 }; +static const u8 AHE50DC_FAN_SPEED_REGS[] = { 0x90, 0x91, 0x92, 0x93 }; +static const u8 AHE50DC_FAN_FAN_STATUS_REGS[] = { 0x81, 0x82 }; +#define AHE50DC_FAN_VIN_REG 0x88 +#define AHE50DC_FAN_CHIP_STATUS_REG 0x79 + +static int ahe50dc_fan_get_temp(struct regmap *regmap, int channel, long *val) +{ + int ret; + unsigned int reg; + + if (channel >= ARRAY_SIZE(AHE50DC_FAN_TEMP_REGS)) + return -EINVAL; + + ret = regmap_read(regmap, AHE50DC_FAN_TEMP_REGS[channel], ®); + if (ret < 0) + return ret; + + /* Temp regs report tenths of a degree C; convert to millidegrees */ + *val = reg * 100; + + return 0; +} + +static int ahe50dc_fan_get_fan(struct regmap *regmap, int channel, long *val) +{ + int ret; + unsigned int reg; + + if (channel >= ARRAY_SIZE(AHE50DC_FAN_SPEED_REGS)) + return -EINVAL; + + ret = regmap_read(regmap, AHE50DC_FAN_SPEED_REGS[channel], ®); + if (ret < 0) + return ret; + + *val = reg; + + return 0; +} + +static int ahe50dc_fan_get_fan_status(struct regmap *regmap, int channel, u32 attr, long *val) +{ + int ret; + unsigned int reg, bit; + int regidx = channel / 2; + + if (regidx >= ARRAY_SIZE(AHE50DC_FAN_FAN_STATUS_REGS) || + (attr != hwmon_fan_fault && attr != hwmon_fan_alarm)) + return -EINVAL; + + ret = regmap_read(regmap, AHE50DC_FAN_FAN_STATUS_REGS[regidx], ®); + if (ret < 0) + return ret; + + /* + * Fan status register bits: + * bit 0: fan 1/3 fault + * bit 1: fan 1/3 warn + * bit 8: fan 2/4 fault + * bit 9: fan 2/4 warn + */ + bit = ((channel % 2) * 8) + (attr == hwmon_fan_alarm); + *val = !!(reg & BIT(bit)); + + return 0; +} + +static int ahe50dc_fan_get_vin(struct regmap *regmap, int channel, long *val) +{ + int ret; + unsigned int reg; + + if (channel) + return -EINVAL; + + ret = regmap_read(regmap, AHE50DC_FAN_VIN_REG, ®); + if (ret < 0) + return ret; + + *val = reg; + + return 0; +} + +static int ahe50dc_fan_get_chip_status(struct regmap *regmap, enum hwmon_sensor_types type, + int channel, u32 attr, long *val) +{ + int ret; + unsigned int reg, bit; + + if (type == hwmon_temp && attr == hwmon_temp_alarm && channel == 4) + bit = 2; + else if (type == hwmon_fan && attr == hwmon_fan_alarm && channel == 4) + bit = 11; + else if (type == hwmon_in && attr == hwmon_in_alarm && channel == 1) + bit = 15; + else + return -EINVAL; + + ret = regmap_read(regmap, AHE50DC_FAN_CHIP_STATUS_REG, ®); + if (ret < 0) + return ret; + + *val = !!(reg & BIT(bit)); + + return 0; +} + +static int ahe50dc_fan_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, + int channel, long *val) +{ + struct regmap *regmap = dev_get_drvdata(dev); + + switch (type) { + case hwmon_temp: + switch (attr) { + case hwmon_temp_input: + return ahe50dc_fan_get_temp(regmap, channel, val); + case hwmon_temp_alarm: + return ahe50dc_fan_get_chip_status(regmap, type, channel, attr, val); + default: + return -EOPNOTSUPP; + } + + case hwmon_fan: + switch (attr) { + case hwmon_fan_input: + return ahe50dc_fan_get_fan(regmap, channel, val); + case hwmon_fan_alarm: + case hwmon_fan_fault: + if (channel == 4) + return ahe50dc_fan_get_chip_status(regmap, type, channel, + attr, val); + return ahe50dc_fan_get_fan_status(regmap, channel, attr, val); + default: + return -EOPNOTSUPP; + } + + case hwmon_in: + switch (attr) { + case hwmon_in_input: + return ahe50dc_fan_get_vin(regmap, channel, val); + case hwmon_in_alarm: + return ahe50dc_fan_get_chip_status(regmap, type, channel, attr, val); + default: + return -EOPNOTSUPP; + } + + default: + return -EOPNOTSUPP; + } +} + +static int ahe50dc_fan_read_labels(struct device *dev, enum hwmon_sensor_types type, u32 attr, + int channel, const char **str) +{ + if (attr == hwmon_in_input && channel < 2) { + *str = channel ? "vout" : "vin"; + return 0; + } + + return -EOPNOTSUPP; +} + +static umode_t ahe50dc_fan_is_visible(const void *data, enum hwmon_sensor_types type, + u32 attr, int channel) +{ + return 0444; +} + +static const struct hwmon_channel_info *ahe50dc_fan_info[] = { + HWMON_CHANNEL_INFO(temp, + HWMON_T_INPUT, + HWMON_T_INPUT, + HWMON_T_INPUT, + HWMON_T_INPUT, + HWMON_T_ALARM), + HWMON_CHANNEL_INFO(fan, + HWMON_F_INPUT | HWMON_F_FAULT | HWMON_F_ALARM, + HWMON_F_INPUT | HWMON_F_FAULT | HWMON_F_ALARM, + HWMON_F_INPUT | HWMON_F_FAULT | HWMON_F_ALARM, + HWMON_F_INPUT | HWMON_F_FAULT | HWMON_F_ALARM, + HWMON_F_ALARM), + HWMON_CHANNEL_INFO(in, + HWMON_I_INPUT | HWMON_I_LABEL, + HWMON_I_ALARM | HWMON_I_LABEL), + NULL +}; + +static const struct hwmon_ops ahe50dc_fan_ops = { + .is_visible = ahe50dc_fan_is_visible, + .read = ahe50dc_fan_read, + .read_string = ahe50dc_fan_read_labels, +}; + +static const struct hwmon_chip_info ahe50dc_fan_chip_info = { + .ops = &ahe50dc_fan_ops, + .info = ahe50dc_fan_info, +}; + +static const struct regmap_config ahe50dc_fan_regmap_config = { + .reg_bits = 8, + .val_bits = 16, + .val_format_endian = REGMAP_ENDIAN_LITTLE, +}; + +static int ahe50dc_fan_probe(struct i2c_client *client) +{ + struct regmap *regmap; + struct device *hwmon_dev; + struct device *dev = &client->dev; + + regmap = devm_regmap_init_i2c(client, &ahe50dc_fan_regmap_config); + if (IS_ERR(regmap)) { + dev_err(dev, "regmap init failed\n"); + return PTR_ERR(regmap); + } + + i2c_set_clientdata(client, regmap); + + hwmon_dev = devm_hwmon_device_register_with_info(dev, client->name, regmap, + &ahe50dc_fan_chip_info, NULL); + + return PTR_ERR_OR_ZERO(hwmon_dev); +} + +static const struct i2c_device_id ahe50dc_fan_id[] = { + { "ahe50dc_fan" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, ahe50dc_fan_id); + +const struct of_device_id __maybe_unused ahe50dc_fan_of_match[] = { + { .compatible = "delta,ahe50dc-fan" }, + { } +}; +MODULE_DEVICE_TABLE(of, ahe50dc_fan_of_match); + +static struct i2c_driver ahe50dc_fan_driver = { + .class = I2C_CLASS_HWMON, + .driver = { + .name = "ahe50dc_fan", + .of_match_table = of_match_ptr(ahe50dc_fan_of_match), + }, + .probe_new = ahe50dc_fan_probe, + .id_table = ahe50dc_fan_id, +}; +module_i2c_driver(ahe50dc_fan_driver); + +MODULE_AUTHOR("Zev Weiss <zev@xxxxxxxxxxxxxxxxx>"); +MODULE_DESCRIPTION("Driver for Delta AHE-50DC power shelf fan control module"); +MODULE_LICENSE("GPL"); -- 2.34.1