hwmon: add driver for ARCTIC Fan Controller

Add hwmon driver for the ARCTIC Fan Controller, a USB HID device
(VID 0x3904, PID 0xF001) with 10 fan channels. Exposes fan speed in
RPM (read-only) and PWM duty cycle (0-255, read/write) via sysfs.

The device pushes IN reports at ~1 Hz containing RPM readings. PWM is
set via OUT reports; the device applies the new duty cycle and sends
back a 2-byte ACK (Report ID 0x02). The driver waits up to 1 s for
the ACK using a completion. Measured device latency: max ~563 ms over
500 iterations. PWM control is manual-only: the device never changes
duty cycle autonomously.

raw_event() may run in hardirq context, so fan_rpm[] is protected by
a spinlock with irq-save. pwm_duty[] is also protected by this spinlock
because reset_resume() clears it outside the hwmon core lock. The OUT
report buffer is built and write_pending is armed under the same lock so
that no reset_resume() can race with the pwm_duty[] snapshot. priv->buf
is exclusively accessed by write(), which the hwmon core serializes.

Signed-off-by: Aureo Serrano de Souza <aureo.serrano@arctic.de>
Link: https://lore.kernel.org/r/20260508064405.38676-1-aureo.serrano@arctic.de
Signed-off-by: Guenter Roeck <linux@roeck-us.net>
This commit is contained in:
Aureo Serrano de Souza 2026-05-08 14:44:00 +08:00 committed by Guenter Roeck
parent 98be2444a6
commit e28d0c73d4
6 changed files with 451 additions and 0 deletions

View File

@ -0,0 +1,56 @@
.. SPDX-License-Identifier: GPL-2.0-or-later
Kernel driver arctic_fan_controller
=====================================
Supported devices:
* ARCTIC Fan Controller (USB HID, VID 0x3904, PID 0xF001)
Author: Aureo Serrano de Souza <aureo.serrano@arctic.de>
Description
-----------
This driver provides hwmon support for the ARCTIC Fan Controller, a USB
Custom HID device with 10 fan channels. The device sends IN reports about
once per second containing current RPM values (bytes 11-30, 10 x uint16 LE).
Fan speed control is manual-only: the device does not change PWM
autonomously; it only applies a new duty cycle when it receives an OUT
report from the host.
After the device applies an OUT report, it sends back a 2-byte ACK IN
report (Report ID 0x02, byte 1 = 0x00 on success) confirming the command
was applied.
Usage notes
-----------
Since it is a USB device, hotplug is supported. The device is autodetected.
The device does not support GET_REPORT, so the driver cannot read back the
current hardware PWM state at probe time. The cached PWM values (readable
via pwm[1-10]) start at 0 and reflect only values that have been
successfully written. Because each OUT report carries all 10 channel values,
writing a single channel also sends the cached values for all other channels.
Users should set all channels to the desired values before relying on the
cached state.
On system suspend, the device may lose power and reset its PWM channels to
hardware defaults. The driver clears its cached duty values on resume so
that reads reflect the unknown hardware state rather than stale pre-suspend
values. Userspace is responsible for re-applying the desired duty cycles
after resume.
Sysfs entries
-------------
================ ==============================================================
fan[1-10]_input Fan speed in RPM (read-only). Updated from IN reports at ~1 Hz.
pwm[1-10] PWM duty cycle (0-255). Write: sends an OUT report setting the
duty cycle (scaled from 0-255 to 0-100% for the device);
the cached value is updated only after the device ACKs the
command with a success status. Read: returns the last
successfully written value; initialized to 0 at driver load
and after resume (hardware state unknown).
================ ==============================================================

View File

@ -43,6 +43,7 @@ Hardware Monitoring Kernel Drivers
amc6821
aps-379
aquacomputer_d5next
arctic_fan_controller
asb100
asc7621
aspeed-g6-pwm-tach

View File

@ -2070,6 +2070,13 @@ S: Maintained
F: drivers/net/arcnet/
F: include/uapi/linux/if_arcnet.h
ARCTIC FAN CONTROLLER DRIVER
M: Aureo Serrano de Souza <aureo.serrano@arctic.de>
L: linux-hwmon@vger.kernel.org
S: Maintained
F: Documentation/hwmon/arctic_fan_controller.rst
F: drivers/hwmon/arctic_fan_controller.c
ARM AND ARM64 SoC SUB-ARCHITECTURES (COMMON PARTS)
M: Arnd Bergmann <arnd@arndb.de>
M: Krzysztof Kozlowski <krzk@kernel.org>

View File

@ -388,6 +388,18 @@ config SENSORS_APPLESMC
Say Y here if you have an applicable laptop and want to experience
the awesome power of applesmc.
config SENSORS_ARCTIC_FAN_CONTROLLER
tristate "ARCTIC Fan Controller"
depends on USB_HID
help
If you say yes here you get support for the ARCTIC Fan Controller,
a USB HID device (VID 0x3904, PID 0xF001) with 10 fan channels.
The driver exposes fan speed (RPM) and PWM control via the hwmon
sysfs interface.
This driver can also be built as a module. If so, the module
will be called arctic_fan_controller.
config SENSORS_ARM_SCMI
tristate "ARM SCMI Sensors"
depends on ARM_SCMI_PROTOCOL

View File

@ -49,6 +49,7 @@ obj-$(CONFIG_SENSORS_ADT7475) += adt7475.o
obj-$(CONFIG_SENSORS_AHT10) += aht10.o
obj-$(CONFIG_SENSORS_APPLESMC) += applesmc.o
obj-$(CONFIG_SENSORS_AQUACOMPUTER_D5NEXT) += aquacomputer_d5next.o
obj-$(CONFIG_SENSORS_ARCTIC_FAN_CONTROLLER) += arctic_fan_controller.o
obj-$(CONFIG_SENSORS_ARM_SCMI) += scmi-hwmon.o
obj-$(CONFIG_SENSORS_ARM_SCPI) += scpi-hwmon.o
obj-$(CONFIG_SENSORS_AS370) += as370-hwmon.o

View File

@ -0,0 +1,374 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Linux hwmon driver for ARCTIC Fan Controller
*
* USB Custom HID device 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%).
* PWM is manual-only: the device does not change duty autonomously, only
* when it receives an OUT report from the host.
*/
#include <linux/completion.h>
#include <linux/dma-mapping.h>
#include <linux/err.h>
#include <linux/hid.h>
#include <linux/hwmon.h>
#include <linux/jiffies.h>
#include <linux/minmax.h>
#include <linux/module.h>
#include <linux/spinlock.h>
#include <linux/string.h>
#include <linux/unaligned.h>
#define ARCTIC_VID 0x3904
#define ARCTIC_PID 0xF001
#define ARCTIC_NUM_FANS 10
#define ARCTIC_OUTPUT_REPORT_ID 0x01
#define ARCTIC_REPORT_LEN 32
#define ARCTIC_RPM_OFFSET 11 /* bytes 11-30: 10 x uint16 LE */
/* ACK report: device sends Report ID 0x02, 2 bytes (ID + status) after applying OUT report */
#define ARCTIC_ACK_REPORT_ID 0x02
#define ARCTIC_ACK_REPORT_LEN 2
/*
* Time to wait for ACK report after send.
* Measured over 500 iterations: max ~563 ms. Keep 1 s as margin.
*/
#define ARCTIC_ACK_TIMEOUT_MS 1000
struct arctic_fan_data {
struct hid_device *hdev;
struct device *hwmon_dev; /* stored for explicit unregister in remove() */
spinlock_t in_report_lock; /* protects fan_rpm, ack_status, write_pending, pwm_duty */
struct completion in_report_received; /* ACK (ID 0x02) received in raw_event */
int ack_status; /* 0 = OK, negative errno on device error */
bool write_pending; /* true while an OUT report ACK is in flight */
u32 fan_rpm[ARCTIC_NUM_FANS];
u8 pwm_duty[ARCTIC_NUM_FANS]; /* 0-255 matching sysfs range; converted to 0-100 on send */
/*
* OUT report buffer passed to hid_hw_output_report(). Embedded in the
* devm_kzalloc'd struct so it is heap-allocated and passes
* usb_hcd_map_urb_for_dma(). Exclusively accessed by write(), which
* the hwmon core serializes.
*/
__dma_from_device_group_begin();
u8 buf[ARCTIC_REPORT_LEN];
__dma_from_device_group_end();
};
/*
* Parse RPM values from the periodic status report (10 x uint16 LE at rpm_off).
* pwm_duty is not updated from the report: the device is manual-only, so the
* host cache is the authoritative source for PWM.
* Called from raw_event which may run in IRQ context; must not sleep.
*/
static void arctic_fan_parse_report(struct arctic_fan_data *priv, u8 *buf,
int len, int rpm_off)
{
unsigned long flags;
int i;
if (len < rpm_off + 20)
return;
spin_lock_irqsave(&priv->in_report_lock, flags);
for (i = 0; i < ARCTIC_NUM_FANS; i++)
priv->fan_rpm[i] = get_unaligned_le16(&buf[rpm_off + i * 2]);
spin_unlock_irqrestore(&priv->in_report_lock, flags);
}
/*
* raw_event: IN reports.
*
* Status report: Report ID 0x01, 32 bytes:
* byte 0 = report ID, bytes 1-10 = PWM 0-100%, bytes 11-30 = 10 x RPM uint16 LE.
* Device pushes these at ~1 Hz; no GET_REPORT.
*
* ACK report: Report ID 0x02, 2 bytes:
* byte 0 = 0x02, byte 1 = status (0x00 = OK, 0x01 = ERROR).
* Sent once after accepting and applying an OUT report (ID 0x01).
*/
static int arctic_fan_raw_event(struct hid_device *hdev,
struct hid_report *report, u8 *data, int size)
{
struct arctic_fan_data *priv = hid_get_drvdata(hdev);
unsigned long flags;
hid_dbg(hdev, "arctic_fan: raw_event id=%u size=%d\n", report->id, size);
if (report->id == ARCTIC_ACK_REPORT_ID && size == ARCTIC_ACK_REPORT_LEN) {
spin_lock_irqsave(&priv->in_report_lock, flags);
/*
* Only deliver if a write is in flight. This prevents a
* late-arriving ACK from a timed-out write from erroneously
* satisfying a subsequent write's completion wait.
*/
if (priv->write_pending) {
priv->ack_status = data[1] == 0x00 ? 0 : -EIO;
complete(&priv->in_report_received);
}
spin_unlock_irqrestore(&priv->in_report_lock, flags);
return 0;
}
if (report->id != ARCTIC_OUTPUT_REPORT_ID || size != ARCTIC_REPORT_LEN) {
hid_dbg(hdev, "arctic_fan: raw_event id=%u size=%d ignored\n",
report->id, size);
return 0;
}
arctic_fan_parse_report(priv, data, size, ARCTIC_RPM_OFFSET);
return 0;
}
static umode_t arctic_fan_is_visible(const void *data,
enum hwmon_sensor_types type,
u32 attr, int channel)
{
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 (type == hwmon_fan && attr == hwmon_fan_input) {
spin_lock_irqsave(&priv->in_report_lock, flags);
*val = priv->fan_rpm[channel];
spin_unlock_irqrestore(&priv->in_report_lock, flags);
return 0;
}
if (type == hwmon_pwm && attr == hwmon_pwm_input) {
spin_lock_irqsave(&priv->in_report_lock, flags);
*val = priv->pwm_duty[channel];
spin_unlock_irqrestore(&priv->in_report_lock, flags);
return 0;
}
return -EINVAL;
}
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);
u8 new_duty = (u8)clamp_val(val, 0, 255);
unsigned long flags;
unsigned long t;
int i, ret;
/*
* Build the buffer and arm write_pending under in_report_lock so that
* reset_resume() cannot clear pwm_duty[] between the pwm_duty[] read
* and the buffer write, and raw_event() cannot deliver a stale ACK
* from a previous write into this write's completion.
*
* priv->buf is heap-allocated (embedded in the devm_kzalloc'd struct),
* satisfying usb_hcd_map_urb_for_dma(). Exclusively accessed by
* write() which the hwmon core serializes.
*
* pwm_duty[channel] is committed only after a positive device ACK so a
* failed or timed-out write does not corrupt the cached state.
*
* Residual theoretical race: if write A times out (write_pending
* cleared), write B sets write_pending = true, and a late ACK from
* write Adelayed beyond ARCTIC_ACK_TIMEOUT_MSarrives during write
* B's pending window, it would falsely satisfy write B's completion.
* This cannot be prevented in driver code without protocol support
* (for example, a correlation ID echoed in the device ACK report).
* In testing, observed ACK latency stayed below the 1 s timeout
* (maximum ~563 ms over 500 iterations).
*
* The wait is non-interruptible so that a signal cannot cause write()
* to return early while the OUT report is already in flight; an
* interruptible early return would create the same late-ACK window
* without even the timeout guard.
* Serialized by the hwmon core: only one arctic_fan_write() at a time.
* Use irqsave to match the IRQ context in which raw_event may run.
*/
spin_lock_irqsave(&priv->in_report_lock, flags);
priv->buf[0] = ARCTIC_OUTPUT_REPORT_ID;
for (i = 0; i < ARCTIC_NUM_FANS; i++) {
u8 d = i == channel ? new_duty : priv->pwm_duty[i];
priv->buf[1 + i] = DIV_ROUND_CLOSEST((unsigned int)d * 100, 255);
}
priv->ack_status = -ETIMEDOUT;
priv->write_pending = true;
reinit_completion(&priv->in_report_received);
spin_unlock_irqrestore(&priv->in_report_lock, flags);
ret = hid_hw_output_report(priv->hdev, priv->buf, ARCTIC_REPORT_LEN);
if (ret < 0) {
spin_lock_irqsave(&priv->in_report_lock, flags);
priv->write_pending = false;
spin_unlock_irqrestore(&priv->in_report_lock, flags);
return ret;
}
t = wait_for_completion_timeout(&priv->in_report_received,
msecs_to_jiffies(ARCTIC_ACK_TIMEOUT_MS));
spin_lock_irqsave(&priv->in_report_lock, flags);
priv->write_pending = false;
/* Commit inside the lock so reset_resume() cannot race with this write */
if (t && priv->ack_status == 0)
priv->pwm_duty[channel] = new_duty;
spin_unlock_irqrestore(&priv->in_report_lock, flags);
if (!t)
return -ETIMEDOUT;
return priv->ack_status; /* 0=OK, -EIO=device error */
}
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_reset_resume(struct hid_device *hdev)
{
struct arctic_fan_data *priv = hid_get_drvdata(hdev);
unsigned long flags;
/*
* The device resets its PWM channels to hardware defaults on power
* loss during suspend. Clear the cached duty values so they reflect
* the unknown hardware state, consistent with probe-time behaviour
* (the device has no GET_REPORT support). Hold in_report_lock so
* this does not race with a concurrent pwm read or write callback.
*/
spin_lock_irqsave(&priv->in_report_lock, flags);
memset(priv->pwm_duty, 0, sizeof(priv->pwm_duty));
spin_unlock_irqrestore(&priv->in_report_lock, flags);
return 0;
}
static int arctic_fan_probe(struct hid_device *hdev,
const struct hid_device_id *id)
{
struct arctic_fan_data *priv;
int ret;
if (!hid_is_usb(hdev))
return -ENODEV;
ret = hid_parse(hdev);
if (ret)
return ret;
priv = devm_kzalloc(&hdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->hdev = hdev;
spin_lock_init(&priv->in_report_lock);
init_completion(&priv->in_report_received);
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;
/*
* Start IO before registering with hwmon. If IO were started after
* hwmon registration, a sysfs write arriving in that narrow window
* would send an OUT report but the ACK could not be delivered (the HID
* core discards events until io_started), causing a spurious timeout.
*/
hid_device_io_start(hdev);
/*
* Use the non-devm variant and store the pointer so remove() can
* call hwmon_device_unregister() before tearing down the HID
* transport. devm_hwmon_device_register_with_info() would defer
* unregistration until after remove() returns, leaving a window
* where a concurrent sysfs write could call hid_hw_output_report()
* on an already-stopped device (use-after-free).
*/
priv->hwmon_dev = hwmon_device_register_with_info(&hdev->dev, "arctic_fan",
priv, &arctic_fan_chip_info,
NULL);
if (IS_ERR(priv->hwmon_dev)) {
ret = PTR_ERR(priv->hwmon_dev);
goto out_close;
}
return 0;
out_close:
hid_device_io_stop(hdev);
hid_hw_close(hdev);
out_stop:
hid_hw_stop(hdev);
return ret;
}
static void arctic_fan_remove(struct hid_device *hdev)
{
struct arctic_fan_data *priv = hid_get_drvdata(hdev);
/*
* Unregister hwmon before stopping the HID transport. This removes
* the sysfs files and waits for any in-progress write() callback to
* return, so no hwmon op can call hid_hw_output_report() after
* hid_hw_stop() frees the underlying USB resources.
* Matches the pattern used by nzxt-smart2 and aquacomputer_d5next.
*
* The HID core clears hdev->io_started before invoking ->remove(),
* so hid_device_io_stop() is not called here; doing so would emit
* a spurious "io already stopped" warning.
*/
hwmon_device_unregister(priv->hwmon_dev);
hid_hw_close(hdev);
hid_hw_stop(hdev);
}
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);
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,
.reset_resume = arctic_fan_reset_resume,
};
module_hid_driver(arctic_fan_driver);
MODULE_AUTHOR("Aureo Serrano de Souza <aureo.serrano@arctic.de>");
MODULE_DESCRIPTION("HID hwmon driver for ARCTIC Fan Controller");
MODULE_LICENSE("GPL");