Re: [PATCH] hwmon: add driver for ARCTIC Fan Controller
From: Thomas Weißschuh
Date: Tue Mar 03 2026 - 13:14:34 EST
Hi!
On 2026-03-03 08:25:04+0000, Aureo Serrano wrote:
> From 1cc962124ca4343e682219372b08dec5d611d1af Mon Sep 17 00:00:00 2001
> From: Aureo Serrano de Souza <aureo.serrano@xxxxxxxxx>
> Date: Tue, 3 Mar 2026 15:06:35 +0800
> Subject: [PATCH] hwmon: add driver for ARCTIC Fan Controller
>
> Add hwmon driver for the ARCTIC Fan Controller (USB HID VID 0x3904,
> PID 0xF001) with 10 fan channels. Exposes fan RPM and PWM via sysfs.
> Device pushes IN reports ~1 Hz; PWM set via OUT reports.
>
> Signed-off-by: Aureo Serrano de Souza <aureo.serrano@xxxxxxxxx>
> ---
> Documentation/hwmon/arctic_fan_controller.rst | 33 ++
> drivers/hwmon/Kconfig | 12 +
> drivers/hwmon/Makefile | 1 +
> drivers/hwmon/arctic_fan_controller.c | 297 ++++++++++++++++++
> 4 files changed, 343 insertions(+)
> create mode 100644 Documentation/hwmon/arctic_fan_controller.rst
> create mode 100644 drivers/hwmon/arctic_fan_controller.c
(...)
> diff --git a/drivers/hwmon/arctic_fan_controller.c b/drivers/hwmon/arctic_fan_controller.c
> new file mode 100644
> index 0000000..0738d41
> --- /dev/null
> +++ b/drivers/hwmon/arctic_fan_controller.c
> @@ -0,0 +1,297 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Linux hwmon driver for ARCTIC Fan Controller
> + *
> + * USB Custom HID device (VID 0x3904, PID 0xF001) with 10 fan channels.
> + * Exposes fan RPM (input) and PWM (0-255) via hwmon. Device pushes IN reports
> + * at ~1 Hz; no GET_REPORT. OUT reports set PWM duty (bytes 1-10, 0-100%).
> + */
> +
> +#include <linux/delay.h>
> +#include <linux/err.h>
> +#include <linux/hid.h>
> +#include <linux/hwmon.h>
> +#include <linux/hwmon-sysfs.h>
> +#include <linux/kernel.h>
> +#include <linux/module.h>
> +#include <linux/spinlock.h>
> +
> +#define ARCTIC_VID 0x3904
> +#define ARCTIC_PID 0xF001
> +#define ARCTIC_NUM_FANS 10
> +#define ARCTIC_REPORT_ID 0x01
> +#define ARCTIC_REPORT_LEN 32
> +#define ARCTIC_RPM_OFFSET 11 /* bytes 11-30: 10 x uint16 LE */
> +
> +static int arctic_fan_debug;
> +module_param_named(debug, arctic_fan_debug, int, 0644);
> +MODULE_PARM_DESC(debug, "Enable debug prints (0=off, 1=on)");
No need for this, hid_dbg() uses the dyndbg infrastructure under the
hood for dynamic control of debugging messages.
https://www.kernel.org/doc/html/v6.19/admin-guide/dynamic-debug-howto.html
In general avoid adding any new module parameters.
> +
> +struct arctic_fan_data {
> + struct hid_device *hdev;
> + struct device *hwmon_dev;
> + spinlock_t lock; /* protects fan_rpm, pwm_duty, out_buf payload */
> + u8 *out_buf;
> + u32 fan_rpm[ARCTIC_NUM_FANS];
> + u8 pwm_duty[ARCTIC_NUM_FANS];
> +};
> +
> +static const struct hid_device_id arctic_fan_id_table[] = {
> + { HID_USB_DEVICE(ARCTIC_VID, ARCTIC_PID) },
> + { }
> +};
> +MODULE_DEVICE_TABLE(hid, arctic_fan_id_table);
I would move this below, right before arctic_hid_driver.
> +/*
> + * Parse report buffer: PWM at pwm_off (10 bytes 0-100), RPM at rpm_off
> + * (10 x uint16 LE). Called from raw_event in atomic context; must use
> + * spinlock only.
> + */
> +static void arctic_fan_parse_report(struct arctic_fan_data *priv, u8 *buf,
> + int len, int pwm_off, int rpm_off)
> +{
> + int i;
> + unsigned long flags;
> +
> + if (len < rpm_off + 20 || pwm_off + 10 > len)
> + return;
> + spin_lock_irqsave(&priv->lock, flags);
> + for (i = 0; i < ARCTIC_NUM_FANS; i++) {
> + u8 d = buf[pwm_off + i];
> +
> + priv->pwm_duty[i] = d <= 100 ? d : 0;
When would it ever be larger than 100?
> + }
> + for (i = 0; i < ARCTIC_NUM_FANS; i++) {
> + priv->fan_rpm[i] = (u32)(buf[rpm_off + i * 2] |
> + (buf[rpm_off + i * 2 + 1] << 8));
get_unaligned_u32()
> + }
> + spin_unlock_irqrestore(&priv->lock, flags);
> + if (arctic_fan_debug)
> + hid_dbg(priv->hdev, "arctic_fan: fan1=%u pwm1=%u\n",
> + (unsigned int)priv->fan_rpm[0], (unsigned int)priv->pwm_duty[0]);
> +}
> +
> +/*
> + * raw_event: IN report id 0x01 len 32 (PWM 1-10, RPM 11-30) or id 0 len 31
> + * (PWM 0-9, RPM 10-29). No GET_REPORT support; device pushes only.
> + */
> +static int arctic_fan_raw_event(struct hid_device *hdev,
> + struct hid_report *report, u8 *data, int size)
> +{
> + struct arctic_fan_data *priv;
> + int rpm_off;
> + bool match = false;
> +
> + if (report->id == ARCTIC_REPORT_ID) {
> + if (size == ARCTIC_REPORT_LEN) {
> + rpm_off = ARCTIC_RPM_OFFSET;
> + match = true;
> + } else if (size == ARCTIC_REPORT_LEN - 1) {
> + rpm_off = ARCTIC_RPM_OFFSET - 1;
> + match = true;
> + }
> + } else if (report->id == 0 && size >= 31) {
> + rpm_off = 10;
> + match = true;
> + }
> + if (!match) {
> + if (arctic_fan_debug)
> + hid_dbg(hdev, "arctic_fan: raw_event id=%u size=%d ignored\n",
> + report->id, size);
> + return 0;
> + }
> + priv = hid_get_drvdata(hdev);
> + if (!priv)
> + return 0;
This can never happen.
> + if (arctic_fan_debug)
> + hid_dbg(hdev, "arctic_fan: raw_event id=%u size=%d\n",
> + report->id, size);
> + arctic_fan_parse_report(priv, data, size, rpm_off - 10, rpm_off);
> + return 0;
> +}
> +
> +static umode_t arctic_fan_is_visible(const void *data,
> + enum hwmon_sensor_types type,
> + u32 attr, int channel)
> +{
> + if (channel < 0 || channel >= ARCTIC_NUM_FANS)
> + return 0;
> + if (type == hwmon_fan && attr == hwmon_fan_input)
> + return 0444;
> + if (type == hwmon_pwm && attr == hwmon_pwm_input)
> + return 0644;
> + return 0;
> +}
> +
> +static int arctic_fan_read(struct device *dev, enum hwmon_sensor_types type,
> + u32 attr, int channel, long *val)
> +{
> + struct arctic_fan_data *priv = dev_get_drvdata(dev);
> + unsigned long flags;
> +
> + if (channel < 0 || channel >= ARCTIC_NUM_FANS)
> + return -EINVAL;
> + spin_lock_irqsave(&priv->lock, flags);
> + if (type == hwmon_fan && attr == hwmon_fan_input) {
> + *val = priv->fan_rpm[channel];
> + } else if (type == hwmon_pwm && attr == hwmon_pwm_input) {
> + *val = (long)priv->pwm_duty[channel] * 255 / 100;
DIV_ROUND_CLOSEST()
> + } else {
> + spin_unlock_irqrestore(&priv->lock, flags);
You can use the new guard() syntax from cleanup.h to avoid manual
unlocks on error paths.
> + return -EINVAL;
> + }
> + spin_unlock_irqrestore(&priv->lock, flags);
> + return 0;
> +}
> +
> +/* Send OUT report; buf[0]=0x01, buf[1..10]=PWM 0-100. May sleep. */
> +static int arctic_fan_send_report(struct hid_device *hdev, u8 *buf)
> +{
> + int i, ret;
> + const int max_retries = 8;
> +
> + for (i = 0; i < max_retries; i++) {
> + ret = hid_hw_output_report(hdev, buf, ARCTIC_REPORT_LEN);
> + if (ret >= 0)
> + return ret;
Return '0' here instead of normalizing it in the caller.
> + if (ret != -EAGAIN)
> + break; /* e.g. -ENOSYS when transport lacks output_report */
USB HID does have ->output_report. You could enforce usage of the USB
transport with hid_is_usb().
> + msleep(25);
Use fsleep().
> + }
> + for (i = 0; i < max_retries; i++) {
> + ret = hid_hw_raw_request(hdev, ARCTIC_REPORT_ID, buf,
> + ARCTIC_REPORT_LEN, HID_OUTPUT_REPORT,
> + HID_REQ_SET_REPORT);
> + if (ret != -EAGAIN)
> + return ret;
> + msleep(25);
> + }
Are these retries really necessary?
> + return -EAGAIN;
> +}
> +
> +static int arctic_fan_write(struct device *dev, enum hwmon_sensor_types type,
> + u32 attr, int channel, long val)
> +{
> + struct arctic_fan_data *priv = dev_get_drvdata(dev);
> + unsigned long flags;
> + int i, ret;
> + u8 duty;
> +
> + if (channel < 0 || channel >= ARCTIC_NUM_FANS)
> + return -EINVAL;
This can never happen (the same in other functions).
> + if (type != hwmon_pwm || attr != hwmon_pwm_input)
> + return -EINVAL;
This can also never happen.
> + if (val < 0 || val > 255)
> + return -EINVAL;
> + duty = DIV_ROUND_CLOSEST(val * 100, 255);
> + if (duty > 100)
> + duty = 100;
This seems also impossible.
> + if (!priv->out_buf)
> + return -ENOMEM;
This can never happen.
> + spin_lock_irqsave(&priv->lock, flags);
> + priv->pwm_duty[channel] = duty;
> + priv->out_buf[0] = ARCTIC_REPORT_ID;
> + for (i = 0; i < ARCTIC_NUM_FANS; i++)
> + priv->out_buf[1 + i] = priv->pwm_duty[i] <= 100 ?
> + priv->pwm_duty[i] : 0;
> + memset(priv->out_buf + 11, 0, ARCTIC_REPORT_LEN - 11);
> + spin_unlock_irqrestore(&priv->lock, flags);
> + ret = arctic_fan_send_report(priv->hdev, priv->out_buf);
> + return ret < 0 ? ret : 0;
> +}
> +
> +static const struct hwmon_ops arctic_fan_ops = {
> + .is_visible = arctic_fan_is_visible,
> + .read = arctic_fan_read,
> + .write = arctic_fan_write,
> +};
> +
> +static const struct hwmon_channel_info *arctic_fan_info[] = {
> + HWMON_CHANNEL_INFO(fan,
> + HWMON_F_INPUT, HWMON_F_INPUT, HWMON_F_INPUT,
> + HWMON_F_INPUT, HWMON_F_INPUT, HWMON_F_INPUT,
> + HWMON_F_INPUT, HWMON_F_INPUT, HWMON_F_INPUT,
> + HWMON_F_INPUT),
> + HWMON_CHANNEL_INFO(pwm,
> + HWMON_PWM_INPUT, HWMON_PWM_INPUT, HWMON_PWM_INPUT,
> + HWMON_PWM_INPUT, HWMON_PWM_INPUT, HWMON_PWM_INPUT,
> + HWMON_PWM_INPUT, HWMON_PWM_INPUT, HWMON_PWM_INPUT,
> + HWMON_PWM_INPUT),
> + NULL
> +};
> +
> +static const struct hwmon_chip_info arctic_fan_chip_info = {
> + .ops = &arctic_fan_ops,
> + .info = arctic_fan_info,
> +};
> +
> +static int arctic_fan_probe(struct hid_device *hdev,
> + const struct hid_device_id *id)
> +{
> + struct arctic_fan_data *priv;
> + struct device *hwmon_dev;
> + int ret;
> +
> + ret = hid_parse(hdev);
> + if (ret)
> + return ret;
> +
> + priv = devm_kzalloc(&hdev->dev, sizeof(*priv), GFP_KERNEL);
> + if (!priv)
> + return -ENOMEM;
> +
> + priv->out_buf = devm_kmalloc(&hdev->dev, ARCTIC_REPORT_LEN, GFP_KERNEL);
> + if (!priv->out_buf)
> + return -ENOMEM;
The 32 byte buffer could be on the stack, saving this allocation and
avoiding a shared resource.
> +
> + priv->hdev = hdev;
> + spin_lock_init(&priv->lock);
> + hid_set_drvdata(hdev, priv);
> +
> + ret = hid_hw_start(hdev, HID_CONNECT_DRIVER);
> + if (ret)
> + return ret;
> +
> + ret = hid_hw_open(hdev);
> + if (ret)
> + goto out_stop;
> +
> + hid_device_io_start(hdev);
> +
> + hwmon_dev = devm_hwmon_device_register_with_info(&hdev->dev, "arctic_fan",
> + priv, &arctic_fan_chip_info,
> + NULL);
You could assign directly to priv->hwmon_dev.
> + if (IS_ERR(hwmon_dev)) {
> + ret = PTR_ERR(hwmon_dev);
> + goto out_close;
> + }
> + priv->hwmon_dev = hwmon_dev;
> + return 0;
> +
> +out_close:
> + hid_hw_close(hdev);
> +out_stop:
> + hid_hw_stop(hdev);
> + return ret;
> +}
> +
> +static void arctic_fan_remove(struct hid_device *hdev)
> +{
> + hid_device_io_stop(hdev);
> + hid_hw_close(hdev);
> + hid_hw_stop(hdev);
> +}
> +
> +static struct hid_driver arctic_fan_driver = {
> + .name = "arctic_fan",
> + .id_table = arctic_fan_id_table,
> + .probe = arctic_fan_probe,
> + .remove = arctic_fan_remove,
> + .raw_event = arctic_fan_raw_event,
> +};
> +
> +module_hid_driver(arctic_fan_driver);
> +
> +MODULE_AUTHOR("Aureo Serrano de Souza <aureo.serrano@xxxxxxxxx>");
> +MODULE_DESCRIPTION("HID hwmon driver for ARCTIC Fan Controller (VID 0x3904, PID 0xF001)");
No need to list the VID and PID here, they are already part of the
module metadata through MODULE_DEVICE_TABLE().
> +MODULE_LICENSE("GPL");
> --
> 2.25.1
>