diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst index d4b422edbe3a..48bfa7887dd4 100644 --- a/Documentation/hwmon/index.rst +++ b/Documentation/hwmon/index.rst @@ -143,6 +143,7 @@ Hardware Monitoring Kernel Drivers npcm750-pwm-fan nsa320 ntc_thermistor + nzxt-kraken2 occ pc87360 pc87427 diff --git a/Documentation/hwmon/nzxt-kraken2.rst b/Documentation/hwmon/nzxt-kraken2.rst new file mode 100644 index 000000000000..94025de65a81 --- /dev/null +++ b/Documentation/hwmon/nzxt-kraken2.rst @@ -0,0 +1,42 @@ +.. SPDX-License-Identifier: GPL-2.0-or-later + +Kernel driver nzxt-kraken2 +========================== + +Supported devices: + +* NZXT Kraken X42 +* NZXT Kraken X52 +* NZXT Kraken X62 +* NZXT Kraken X72 + +Author: Jonas Malaco + +Description +----------- + +This driver enables hardware monitoring support for NZXT Kraken X42/X52/X62/X72 +all-in-one CPU liquid coolers. Three sensors are available: fan speed, pump +speed and coolant temperature. + +Fan and pump control, while supported by the firmware, are not currently +exposed. The addressable RGB LEDs, present in the integrated CPU water block +and pump head, are not supported either. But both features can be found in +existing user-space tools (e.g. `liquidctl`_). + +.. _liquidctl: https://github.com/liquidctl/liquidctl + +Usage Notes +----------- + +As these are USB HIDs, the driver can be loaded automatically by the kernel and +supports hot swapping. + +Sysfs entries +------------- + +======================= ======================================================== +fan1_input Fan speed (in rpm) +fan2_input Pump speed (in rpm) +temp1_input Coolant temperature (in millidegrees Celsius) +======================= ======================================================== diff --git a/MAINTAINERS b/MAINTAINERS index 3aaeab8c6b15..13df46b0e88c 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -12911,6 +12911,13 @@ L: linux-nfc@lists.01.org (moderated for non-subscribers) S: Supported F: drivers/nfc/nxp-nci +NZXT-KRAKEN2 HARDWARE MONITORING DRIVER +M: Jonas Malaco +L: linux-hwmon@vger.kernel.org +S: Maintained +F: Documentation/hwmon/nzxt-kraken2.rst +F: drivers/hwmon/nzxt-kraken2.c + OBJAGG M: Jiri Pirko L: netdev@vger.kernel.org diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig index 54f04e61fb83..0ddc974b102e 100644 --- a/drivers/hwmon/Kconfig +++ b/drivers/hwmon/Kconfig @@ -1492,6 +1492,16 @@ config SENSORS_NSA320 This driver can also be built as a module. If so, the module will be called nsa320-hwmon. +config SENSORS_NZXT_KRAKEN2 + tristate "NZXT Kraken X42/X51/X62/X72 liquid coolers" + depends on USB_HID + help + If you say yes here you get support for hardware monitoring for the + NZXT Kraken X42/X52/X62/X72 all-in-one CPU liquid coolers. + + This driver can also be built as a module. If so, the module + will be called nzxt-kraken2. + source "drivers/hwmon/occ/Kconfig" config SENSORS_PCF8591 diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile index fe38e8a5c979..59e78bc212cf 100644 --- a/drivers/hwmon/Makefile +++ b/drivers/hwmon/Makefile @@ -155,6 +155,7 @@ obj-$(CONFIG_SENSORS_NCT7904) += nct7904.o obj-$(CONFIG_SENSORS_NPCM7XX) += npcm750-pwm-fan.o obj-$(CONFIG_SENSORS_NSA320) += nsa320-hwmon.o obj-$(CONFIG_SENSORS_NTC_THERMISTOR) += ntc_thermistor.o +obj-$(CONFIG_SENSORS_NZXT_KRAKEN2) += nzxt-kraken2.o obj-$(CONFIG_SENSORS_PC87360) += pc87360.o obj-$(CONFIG_SENSORS_PC87427) += pc87427.o obj-$(CONFIG_SENSORS_PCF8591) += pcf8591.o diff --git a/drivers/hwmon/nzxt-kraken2.c b/drivers/hwmon/nzxt-kraken2.c new file mode 100644 index 000000000000..89f7ea4f42d4 --- /dev/null +++ b/drivers/hwmon/nzxt-kraken2.c @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * nzxt-kraken2.c - hwmon driver for NZXT Kraken X42/X52/X62/X72 coolers + * + * The device asynchronously sends HID reports (with id 0x04) twice a second to + * communicate current fan speed, pump speed and coolant temperature. The + * device does not respond to Get_Report requests for this status report. + * + * Copyright 2019-2021 Jonas Malaco + */ + +#include +#include +#include +#include +#include + +#define STATUS_REPORT_ID 0x04 +#define STATUS_VALIDITY 2 /* seconds; equivalent to 4 missed updates */ + +static const char *const kraken2_temp_label[] = { + "Coolant", +}; + +static const char *const kraken2_fan_label[] = { + "Fan", + "Pump", +}; + +struct kraken2_priv_data { + struct hid_device *hid_dev; + struct device *hwmon_dev; + s32 temp_input[1]; + u16 fan_input[2]; + unsigned long updated; /* jiffies */ +}; + +static umode_t kraken2_is_visible(const void *data, + enum hwmon_sensor_types type, + u32 attr, int channel) +{ + return 0444; +} + +static int kraken2_read(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + struct kraken2_priv_data *priv = dev_get_drvdata(dev); + + if (time_after(jiffies, priv->updated + STATUS_VALIDITY * HZ)) + return -ENODATA; + + switch (type) { + case hwmon_temp: + *val = priv->temp_input[channel]; + break; + case hwmon_fan: + *val = priv->fan_input[channel]; + break; + default: + return -EOPNOTSUPP; /* unreachable */ + } + + return 0; +} + +static int kraken2_read_string(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, const char **str) +{ + switch (type) { + case hwmon_temp: + *str = kraken2_temp_label[channel]; + break; + case hwmon_fan: + *str = kraken2_fan_label[channel]; + break; + default: + return -EOPNOTSUPP; /* unreachable */ + } + return 0; +} + +static const struct hwmon_ops kraken2_hwmon_ops = { + .is_visible = kraken2_is_visible, + .read = kraken2_read, + .read_string = kraken2_read_string, +}; + +static const struct hwmon_channel_info *kraken2_info[] = { + HWMON_CHANNEL_INFO(temp, + HWMON_T_INPUT | HWMON_T_LABEL), + HWMON_CHANNEL_INFO(fan, + HWMON_F_INPUT | HWMON_F_LABEL, + HWMON_F_INPUT | HWMON_F_LABEL), + NULL +}; + +static const struct hwmon_chip_info kraken2_chip_info = { + .ops = &kraken2_hwmon_ops, + .info = kraken2_info, +}; + +static int kraken2_raw_event(struct hid_device *hdev, + struct hid_report *report, u8 *data, int size) +{ + struct kraken2_priv_data *priv; + + if (size < 7 || report->id != STATUS_REPORT_ID) + return 0; + + priv = hid_get_drvdata(hdev); + + /* + * The fractional byte of the coolant temperature has been observed to + * be in the interval [1,9], but some of these steps are also + * consistently skipped for certain integer parts. + * + * For the lack of a better idea, assume that the resolution is 0.1°C, + * and that the missing steps are artifacts of how the firmware + * processes the raw sensor data. + */ + priv->temp_input[0] = data[1] * 1000 + data[2] * 100; + + priv->fan_input[0] = get_unaligned_be16(data + 3); + priv->fan_input[1] = get_unaligned_be16(data + 5); + + priv->updated = jiffies; + + return 0; +} + +static int kraken2_probe(struct hid_device *hdev, + const struct hid_device_id *id) +{ + struct kraken2_priv_data *priv; + int ret; + + priv = devm_kzalloc(&hdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->hid_dev = hdev; + hid_set_drvdata(hdev, priv); + + /* + * Initialize ->updated to STATUS_VALIDITY seconds in the past, making + * the initial empty data invalid for kraken2_read without the need for + * a special case there. + */ + priv->updated = jiffies - STATUS_VALIDITY * HZ; + + ret = hid_parse(hdev); + if (ret) { + hid_err(hdev, "hid parse failed with %d\n", ret); + return ret; + } + + /* + * Enable hidraw so existing user-space tools can continue to work. + */ + ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW); + if (ret) { + hid_err(hdev, "hid hw start failed with %d\n", ret); + goto fail_and_stop; + } + + ret = hid_hw_open(hdev); + if (ret) { + hid_err(hdev, "hid hw open failed with %d\n", ret); + goto fail_and_close; + } + + priv->hwmon_dev = hwmon_device_register_with_info(&hdev->dev, "kraken2", + priv, &kraken2_chip_info, + NULL); + if (IS_ERR(priv->hwmon_dev)) { + ret = PTR_ERR(priv->hwmon_dev); + hid_err(hdev, "hwmon registration failed with %d\n", ret); + goto fail_and_close; + } + + return 0; + +fail_and_close: + hid_hw_close(hdev); +fail_and_stop: + hid_hw_stop(hdev); + return ret; +} + +static void kraken2_remove(struct hid_device *hdev) +{ + struct kraken2_priv_data *priv = hid_get_drvdata(hdev); + + hwmon_device_unregister(priv->hwmon_dev); + + hid_hw_close(hdev); + hid_hw_stop(hdev); +} + +static const struct hid_device_id kraken2_table[] = { + { HID_USB_DEVICE(0x1e71, 0x170e) }, /* NZXT Kraken X42/X52/X62/X72 */ + { } +}; + +MODULE_DEVICE_TABLE(hid, kraken2_table); + +static struct hid_driver kraken2_driver = { + .name = "nzxt-kraken2", + .id_table = kraken2_table, + .probe = kraken2_probe, + .remove = kraken2_remove, + .raw_event = kraken2_raw_event, +}; + +static int __init kraken2_init(void) +{ + return hid_register_driver(&kraken2_driver); +} + +static void __exit kraken2_exit(void) +{ + hid_unregister_driver(&kraken2_driver); +} + +/* + * When compiled into the kernel, initialize after the hid bus. + */ +late_initcall(kraken2_init); +module_exit(kraken2_exit); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Jonas Malaco "); +MODULE_DESCRIPTION("Hwmon driver for NZXT Kraken X42/X52/X62/X72 coolers");