hwmon: add driver for NZXT Kraken X42/X52/X62/X72

These are "all-in-one" CPU liquid coolers that can be monitored and
controlled through a proprietary USB HID protocol.

While the models have differently sized radiators and come with varying
numbers of fans, they are all indistinguishable at the software level.

The driver exposes fan/pump speeds and coolant temperature through the
standard hwmon sysfs interface.

Fan and pump control, while supported by the devices, are not currently
exposed.  The firmware accepts up to 61 trip points per channel
(fan/pump), but the same set of trip temperatures has to be maintained
for both; with pwmX_auto_point_Y_temp attributes, users would need to
maintain this invariant themselves.

Instead, fan and pump control, as well as LED control (which the device
also supports for 9 addressable RGB LEDs on the CPU water block) are
left for existing and already mature user-space tools, which can still
be used alongside the driver, thanks to hidraw.  A link to one, which I
also maintain, is provided in the documentation.

The implementation is based on USB traffic analysis.  It has been
runtime tested on x86_64, both as a built-in driver and as a module.

Signed-off-by: Jonas Malaco <jonas@protocubo.io>
Link: https://lore.kernel.org/r/20210319045544.416138-1-jonas@protocubo.io
[groeck: Removed unnecessary spinlock.h include]
Signed-off-by: Guenter Roeck <linux@roeck-us.net>
This commit is contained in:
Jonas Malaco 2021-03-19 01:55:44 -03:00 committed by Guenter Roeck
parent af9a973040
commit 82e3430dfa
6 changed files with 295 additions and 0 deletions

View File

@ -143,6 +143,7 @@ Hardware Monitoring Kernel Drivers
npcm750-pwm-fan
nsa320
ntc_thermistor
nzxt-kraken2
occ
pc87360
pc87427

View File

@ -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)
======================= ========================================================

View File

@ -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 <jonas@protocubo.io>
L: linux-hwmon@vger.kernel.org
S: Maintained
F: Documentation/hwmon/nzxt-kraken2.rst
F: drivers/hwmon/nzxt-kraken2.c
OBJAGG
M: Jiri Pirko <jiri@nvidia.com>
L: netdev@vger.kernel.org

View File

@ -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

View File

@ -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

View File

@ -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 <jonas@protocubo.io>
*/
#include <asm/unaligned.h>
#include <linux/hid.h>
#include <linux/hwmon.h>
#include <linux/jiffies.h>
#include <linux/module.h>
#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 <jonas@protocubo.io>");
MODULE_DESCRIPTION("Hwmon driver for NZXT Kraken X42/X52/X62/X72 coolers");