[PATCH v2] hwmon: add driver for ARCTIC Fan Controller

Aureo Serrano de Souza posted 1 patch 3 weeks, 3 days ago
There is a newer version of this series
Documentation/hwmon/arctic_fan_controller.rst |  40 +++
Documentation/hwmon/index.rst                 |   1 +
MAINTAINERS                                   |   7 +
drivers/hwmon/Kconfig                         |  12 +
drivers/hwmon/Makefile                        |   1 +
drivers/hwmon/arctic_fan_controller.c         | 278 ++++++++++++++++++
6 files changed, 339 insertions(+)
create mode 100644 Documentation/hwmon/arctic_fan_controller.rst
create mode 100644 drivers/hwmon/arctic_fan_controller.c
[PATCH v2] hwmon: add driver for ARCTIC Fan Controller
Posted by Aureo Serrano de Souza 3 weeks, 3 days ago
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 at ~1 Hz; PWM is set via interrupt OUT reports.

Fan speed control is manual-only: the device does not change PWM
autonomously. After applying an OUT report, the device sends back a
2-byte ACK (Report ID 0x02); the driver waits up to 1 s for this ACK
using a completion.

The report buffer is kmalloc'd per write for two reasons:

1. Stack placement (suggested by reviewers with __aligned()) was tested
   but hard-rejected by usb_hcd_map_urb_for_dma(), which calls
   object_is_on_stack() and returns -EAGAIN regardless of alignment
   attributes. Confirmed on AMD X670E with AMD-Vi (IOMMU) enabled.

2. Struct-embedded placement (suggested as an alternative) would create
   a shared resource between concurrent arctic_fan_write() calls, causing
   a race if two users write different PWM channels simultaneously.

kmalloc per write avoids both problems: the buffer is heap-allocated
(passes DMA checks) and private to each write call (no sharing).

Signed-off-by: Aureo Serrano de Souza <aureo.serrano@arctic.de>
---
Hi,

Thanks for the detailed review. Here is v2 addressing your feedback.
If I missed anything, please let me know.

Changes since v1:
- Remove module_param debug flag; use hid_dbg() / dyndbg instead
- Move hid_device_id table next to hid_driver struct
- Replace spinlock_irqsave with mutex for fan_rpm/pwm_duty; use a
  lightweight spinlock only to protect the completion signal shared with
  raw_event() which runs in BH/softirq context
- Use guard() from cleanup.h to eliminate manual unlock on error paths
- Use get_unaligned_le16() for RPM parsing instead of manual byte shift
- Remove all impossible bounds/NULL checks that the hwmon core never triggers
- Remove retry loop; use a single hid_hw_output_report() call
- Add hid_is_usb() guard in probe
- Remove hwmon_dev field from struct; use local variable only
- Remove VID/PID from MODULE_DESCRIPTION
- Fix: do not update pwm_duty from IN reports; device is manual-only so
  the host cache is the authoritative PWM source. This eliminates the race
  where an incoming report could overwrite a value just written.
- Add completion/ACK mechanism: wait for device ACK after OUT report,
  signalled from raw_event() via complete(); return device ACK status
- Report buffer is kmalloc'd per write; see commit message for full
  rationale. In summary: stack placement (suggested with __aligned()) is
  hard-rejected by usb_hcd_map_urb_for_dma() via object_is_on_stack();
  struct-embedded placement (suggested as alternative) creates a shared
  resource race between concurrent writes. kmalloc per write avoids both.
- Add Documentation/hwmon/arctic_fan_controller.rst to index.rst
- Add MAINTAINERS entry

 Documentation/hwmon/arctic_fan_controller.rst |  40 +++
 Documentation/hwmon/index.rst                 |   1 +
 MAINTAINERS                                   |   7 +
 drivers/hwmon/Kconfig                         |  12 +
 drivers/hwmon/Makefile                        |   1 +
 drivers/hwmon/arctic_fan_controller.c         | 278 ++++++++++++++++++
 6 files changed, 339 insertions(+)
 create mode 100644 Documentation/hwmon/arctic_fan_controller.rst
 create mode 100644 drivers/hwmon/arctic_fan_controller.c

diff --git a/Documentation/hwmon/arctic_fan_controller.rst b/Documentation/hwmon/arctic_fan_controller.rst
new file mode 100644
index 000000000..58366551c
--- /dev/null
+++ b/Documentation/hwmon/arctic_fan_controller.rst
@@ -0,0 +1,40 @@
+.. 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.
+
+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). Read:
+                 returns the last value written; the device is manual-only so
+                 the host cache is authoritative.
+================ ==============================================================
diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst
index b2ca8513c..c34713040 100644
--- a/Documentation/hwmon/index.rst
+++ b/Documentation/hwmon/index.rst
@@ -42,6 +42,7 @@ Hardware Monitoring Kernel Drivers
    aht10
    amc6821
    aquacomputer_d5next
+   arctic_fan_controller
    asb100
    asc7621
    aspeed-g6-pwm-tach
diff --git a/MAINTAINERS b/MAINTAINERS
index 837db4f7b..8c32dddd6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -2053,6 +2053,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>
diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index 486152a8e..b6ab72752 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -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 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
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index 5833c807c..ef831c337 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -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
diff --git a/drivers/hwmon/arctic_fan_controller.c b/drivers/hwmon/arctic_fan_controller.c
new file mode 100644
index 000000000..24db8a7ee
--- /dev/null
+++ b/drivers/hwmon/arctic_fan_controller.c
@@ -0,0 +1,278 @@
+// 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%).
+ * PWM is manual-only: the device does not change duty autonomously, only
+ * when it receives an OUT report from the host.
+ */
+
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/err.h>
+#include <linux/hid.h>
+#include <linux/hwmon.h>
+#include <linux/hwmon-sysfs.h>
+#include <linux/jiffies.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/unaligned.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 */
+/* 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 */
+#define ARCTIC_ACK_TIMEOUT_MS	1000
+
+struct arctic_fan_data {
+	struct hid_device *hdev;
+	struct mutex lock;		/* protects fan_rpm, pwm_duty */
+	spinlock_t in_report_lock;	/* protects ack_status and in_report_received */
+	struct completion in_report_received; /* ACK (ID 0x02) received in raw_event */
+	int ack_status;			/* 0 = OK, negative errno on device error */
+	u32 fan_rpm[ARCTIC_NUM_FANS];
+	u8 pwm_duty[ARCTIC_NUM_FANS];	/* 0-255 matching sysfs range; converted to 0-100 on send */
+};
+
+/*
+ * 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.
+ */
+static void arctic_fan_parse_report(struct arctic_fan_data *priv, u8 *buf,
+				    int len, int rpm_off)
+{
+	int i;
+
+	if (len < rpm_off + 20)
+		return;
+	{
+		guard(mutex)(&priv->lock);
+		for (i = 0; i < ARCTIC_NUM_FANS; i++)
+			priv->fan_rpm[i] = get_unaligned_le16(&buf[rpm_off + i * 2]);
+	}
+}
+
+/*
+ * 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);
+
+	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(&priv->in_report_lock);
+		priv->ack_status = data[1] == 0x00 ? 0 : -EIO;
+		complete(&priv->in_report_received);
+		spin_unlock(&priv->in_report_lock);
+		return 0;
+	}
+
+	if (report->id != ARCTIC_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);
+
+	{
+		guard(mutex)(&priv->lock);
+		if (type == hwmon_fan && attr == hwmon_fan_input) {
+			*val = priv->fan_rpm[channel];
+			return 0;
+		}
+		if (type == hwmon_pwm && attr == hwmon_pwm_input) {
+			*val = priv->pwm_duty[channel];
+			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 *buf;
+	long t;
+	int i, ret;
+
+	/*
+	 * Must use a heap-allocated buffer: usb_hcd_map_urb_for_dma() rejects
+	 * stack buffers with -EAGAIN, preventing USB transfers on DMA-capable
+	 * host controllers.
+	 */
+	buf = kmalloc(ARCTIC_REPORT_LEN, GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+
+	{
+		guard(mutex)(&priv->lock);
+		priv->pwm_duty[channel] = (u8)val;
+		buf[0] = ARCTIC_REPORT_ID;
+		for (i = 0; i < ARCTIC_NUM_FANS; i++)
+			buf[1 + i] = DIV_ROUND_CLOSEST((unsigned int)priv->pwm_duty[i] * 100, 255);
+	}
+	memset(buf + 11, 0, ARCTIC_REPORT_LEN - 11);
+
+	spin_lock_bh(&priv->in_report_lock);
+	priv->ack_status = -ETIMEDOUT;
+	reinit_completion(&priv->in_report_received);
+	spin_unlock_bh(&priv->in_report_lock);
+
+	ret = hid_hw_output_report(priv->hdev, buf, ARCTIC_REPORT_LEN);
+	kfree(buf);
+	if (ret < 0)
+		return ret;
+
+	t = wait_for_completion_interruptible_timeout(&priv->in_report_received,
+						      msecs_to_jiffies(ARCTIC_ACK_TIMEOUT_MS));
+	if (t == 0)
+		return -ETIMEDOUT;
+	if (t < 0)
+		return t; /* interrupted by signal */
+	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_probe(struct hid_device *hdev,
+			    const struct hid_device_id *id)
+{
+	struct arctic_fan_data *priv;
+	struct device *hwmon_dev;
+	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;
+	mutex_init(&priv->lock);
+	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;
+
+	hid_device_io_start(hdev);
+
+	hwmon_dev = devm_hwmon_device_register_with_info(&hdev->dev, "arctic_fan",
+							 priv, &arctic_fan_chip_info,
+							 NULL);
+	if (IS_ERR(hwmon_dev)) {
+		ret = PTR_ERR(hwmon_dev);
+		goto out_close;
+	}
+	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 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,
+};
+
+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");
-- 
2.43.0
Re: [PATCH v2] hwmon: add driver for ARCTIC Fan Controller
Posted by Guenter Roeck 2 weeks, 6 days ago
Hi,

On Fri, Mar 13, 2026 at 07:19:55PM +0800, Aureo Serrano de Souza wrote:
> 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 at ~1 Hz; PWM is set via interrupt OUT reports.
> 
> Fan speed control is manual-only: the device does not change PWM
> autonomously. After applying an OUT report, the device sends back a
> 2-byte ACK (Report ID 0x02); the driver waits up to 1 s for this ACK
> using a completion.
> 
> The report buffer is kmalloc'd per write for two reasons:
> 
> 1. Stack placement (suggested by reviewers with __aligned()) was tested
>    but hard-rejected by usb_hcd_map_urb_for_dma(), which calls
>    object_is_on_stack() and returns -EAGAIN regardless of alignment
>    attributes. Confirmed on AMD X670E with AMD-Vi (IOMMU) enabled.
> 
> 2. Struct-embedded placement (suggested as an alternative) would create
>    a shared resource between concurrent arctic_fan_write() calls, causing
>    a race if two users write different PWM channels simultaneously.
> 
> kmalloc per write avoids both problems: the buffer is heap-allocated
> (passes DMA checks) and private to each write call (no sharing).
> 
> Signed-off-by: Aureo Serrano de Souza <aureo.serrano@arctic.de>
> ---

Please also see AI review feedback at
https://sashiko.dev/#/patchset/20260313111955.36811-1-aureo.serrano%40arctic.de.

Thanks,
Guenter
Re: [PATCH v2] hwmon: add driver for ARCTIC Fan Controller
Posted by Thomas Weißschuh 3 weeks, 3 days ago
When sending new revisions, please don't send them as response to the
previous one. It results in much too large threads.

On 2026-03-13 19:19:55+0800, Aureo Serrano de Souza wrote:
> 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 at ~1 Hz; PWM is set via interrupt OUT reports.
> 
> Fan speed control is manual-only: the device does not change PWM
> autonomously. After applying an OUT report, the device sends back a
> 2-byte ACK (Report ID 0x02); the driver waits up to 1 s for this ACK
> using a completion.
> 
> The report buffer is kmalloc'd per write for two reasons:
> 
> 1. Stack placement (suggested by reviewers with __aligned()) was tested
>    but hard-rejected by usb_hcd_map_urb_for_dma(), which calls
>    object_is_on_stack() and returns -EAGAIN regardless of alignment
>    attributes. Confirmed on AMD X670E with AMD-Vi (IOMMU) enabled.

Thanks for the clarifications!

> 2. Struct-embedded placement (suggested as an alternative) would create
>    a shared resource between concurrent arctic_fan_write() calls, causing
>    a race if two users write different PWM channels simultaneously.
> 
> kmalloc per write avoids both problems: the buffer is heap-allocated
> (passes DMA checks) and private to each write call (no sharing).
> 
> Signed-off-by: Aureo Serrano de Souza <aureo.serrano@arctic.de>
> ---
> Hi,
> 
> Thanks for the detailed review. Here is v2 addressing your feedback.
> If I missed anything, please let me know.

(...)

>  Documentation/hwmon/arctic_fan_controller.rst |  40 +++
>  Documentation/hwmon/index.rst                 |   1 +
>  MAINTAINERS                                   |   7 +
>  drivers/hwmon/Kconfig                         |  12 +
>  drivers/hwmon/Makefile                        |   1 +
>  drivers/hwmon/arctic_fan_controller.c         | 278 ++++++++++++++++++
>  6 files changed, 339 insertions(+)
>  create mode 100644 Documentation/hwmon/arctic_fan_controller.rst
>  create mode 100644 drivers/hwmon/arctic_fan_controller.c

(...)

> diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
> index 486152a8e..b6ab72752 100644
> --- a/drivers/hwmon/Kconfig
> +++ b/drivers/hwmon/Kconfig
> @@ -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 HID

Given the the driver will only ever probe for usb devices,
this could also '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

(...)

> diff --git a/drivers/hwmon/arctic_fan_controller.c b/drivers/hwmon/arctic_fan_controller.c
> new file mode 100644
> index 000000000..24db8a7ee
> --- /dev/null
> +++ b/drivers/hwmon/arctic_fan_controller.c
> @@ -0,0 +1,278 @@
> +// 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.

I would also drop the VID and PID from here. It has no additional value.

> + * 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/cleanup.h>
> +#include <linux/completion.h>
> +#include <linux/err.h>
> +#include <linux/hid.h>
> +#include <linux/hwmon.h>
> +#include <linux/hwmon-sysfs.h>
> +#include <linux/jiffies.h>
> +#include <linux/kernel.h>

Try to avoid kernel.h. It includes everything and the kitchensink.

> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/slab.h>
> +#include <linux/spinlock.h>
> +#include <linux/unaligned.h>
> +
> +#define ARCTIC_VID		0x3904
> +#define ARCTIC_PID		0xF001
> +#define ARCTIC_NUM_FANS		10
> +#define ARCTIC_REPORT_ID	0x01
> +#define ARCTIC_REPORT_LEN	32

As there is also ARCTIC_*ACK*_REPORT_ID, maybe call this one
ARCTIC_*OUTPUT*_REPORT_ID, etc.

> +#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 */
> +#define ARCTIC_ACK_TIMEOUT_MS	1000

One second is a very long time. How long does the device take in
practice?

> +struct arctic_fan_data {
> +	struct hid_device *hdev;
> +	struct mutex lock;		/* protects fan_rpm, pwm_duty */
> +	spinlock_t in_report_lock;	/* protects ack_status and in_report_received */
> +	struct completion in_report_received; /* ACK (ID 0x02) received in raw_event */
> +	int ack_status;			/* 0 = OK, negative errno on device error */
> +	u32 fan_rpm[ARCTIC_NUM_FANS];
> +	u8 pwm_duty[ARCTIC_NUM_FANS];	/* 0-255 matching sysfs range; converted to 0-100 on send */
> +};
> +
> +/*
> + * 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.
> + */
> +static void arctic_fan_parse_report(struct arctic_fan_data *priv, u8 *buf,
> +				    int len, int rpm_off)
> +{
> +	int i;
> +
> +	if (len < rpm_off + 20)
> +		return;
> +	{

Avoid scopes like this. Here it is not needed at all and in those cases
where you need a new scope for guard() use scoped_guard().

> +		guard(mutex)(&priv->lock);
> +		for (i = 0; i < ARCTIC_NUM_FANS; i++)

Nowadays you could declare 'int i' inside the for(). Your choice.

> +			priv->fan_rpm[i] = get_unaligned_le16(&buf[rpm_off + i * 2]);
> +	}
> +}

(...)

> +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 *buf;
> +	long t;
> +	int i, ret;
> +
> +	/*
> +	 * Must use a heap-allocated buffer: usb_hcd_map_urb_for_dma() rejects
> +	 * stack buffers with -EAGAIN, preventing USB transfers on DMA-capable
> +	 * host controllers.
> +	 */
> +	buf = kmalloc(ARCTIC_REPORT_LEN, GFP_KERNEL);
> +	if (!buf)
> +		return -ENOMEM;
> +
> +	{
> +		guard(mutex)(&priv->lock);
> +		priv->pwm_duty[channel] = (u8)val;
> +		buf[0] = ARCTIC_REPORT_ID;
> +		for (i = 0; i < ARCTIC_NUM_FANS; i++)
> +			buf[1 + i] = DIV_ROUND_CLOSEST((unsigned int)priv->pwm_duty[i] * 100, 255);
> +	}
> +	memset(buf + 11, 0, ARCTIC_REPORT_LEN - 11);

The magic '11' should be '1 + ARCTIC_NUM_FANS'.
Or even easier, use kzalloc() above.

> +
> +	spin_lock_bh(&priv->in_report_lock);
> +	priv->ack_status = -ETIMEDOUT;
> +	reinit_completion(&priv->in_report_received);
> +	spin_unlock_bh(&priv->in_report_lock);
> +
> +	ret = hid_hw_output_report(priv->hdev, buf, ARCTIC_REPORT_LEN);
> +	kfree(buf);
> +	if (ret < 0)
> +		return ret;
> +
> +	t = wait_for_completion_interruptible_timeout(&priv->in_report_received,

Without the implicit locking from the hwmon core, this would be racy
against concurrent invocations of arctic_fan_write().
Could you add a comment for that?

> +						      msecs_to_jiffies(ARCTIC_ACK_TIMEOUT_MS));
> +	if (t == 0)
> +		return -ETIMEDOUT;
> +	if (t < 0)
> +		return t; /* interrupted by signal */

First test for errors (t < 0), then non-error cases 't == 0'.

> +	return priv->ack_status; /* 0=OK, -EIO=device error */
> +}

(...)

> +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;
> +
> +	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;
> +	mutex_init(&priv->lock);

Use devm_mutex_init().

> +	spin_lock_init(&priv->in_report_lock);
> +	init_completion(&priv->in_report_received);
> +	hid_set_drvdata(hdev, priv);

(...)
Re: [PATCH v2] hwmon: add driver for ARCTIC Fan Controller
Posted by Guenter Roeck 3 weeks, 3 days ago
On Fri, Mar 13, 2026 at 07:19:55PM +0800, Aureo Serrano de Souza wrote:
> 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 at ~1 Hz; PWM is set via interrupt OUT reports.
> 
> Fan speed control is manual-only: the device does not change PWM
> autonomously. After applying an OUT report, the device sends back a
> 2-byte ACK (Report ID 0x02); the driver waits up to 1 s for this ACK
> using a completion.
> 
> The report buffer is kmalloc'd per write for two reasons:
> 
> 1. Stack placement (suggested by reviewers with __aligned()) was tested
>    but hard-rejected by usb_hcd_map_urb_for_dma(), which calls
>    object_is_on_stack() and returns -EAGAIN regardless of alignment
>    attributes. Confirmed on AMD X670E with AMD-Vi (IOMMU) enabled.
> 
> 2. Struct-embedded placement (suggested as an alternative) would create
>    a shared resource between concurrent arctic_fan_write() calls, causing
>    a race if two users write different PWM channels simultaneously.
> 
> kmalloc per write avoids both problems: the buffer is heap-allocated
> (passes DMA checks) and private to each write call (no sharing).

The second problem does not exist since the hwmon core serializes sysfs
attribute accesses, and a single once-allocated buffer would be sufficient
for the same reason.

> 
> Signed-off-by: Aureo Serrano de Souza <aureo.serrano@arctic.de>
> ---
> Hi,
> 
> Thanks for the detailed review. Here is v2 addressing your feedback.
> If I missed anything, please let me know.
> 
> Changes since v1:
> - Remove module_param debug flag; use hid_dbg() / dyndbg instead
> - Move hid_device_id table next to hid_driver struct
> - Replace spinlock_irqsave with mutex for fan_rpm/pwm_duty; use a
>   lightweight spinlock only to protect the completion signal shared with
>   raw_event() which runs in BH/softirq context
> - Use guard() from cleanup.h to eliminate manual unlock on error paths
> - Use get_unaligned_le16() for RPM parsing instead of manual byte shift
> - Remove all impossible bounds/NULL checks that the hwmon core never triggers
> - Remove retry loop; use a single hid_hw_output_report() call
> - Add hid_is_usb() guard in probe
> - Remove hwmon_dev field from struct; use local variable only
> - Remove VID/PID from MODULE_DESCRIPTION
> - Fix: do not update pwm_duty from IN reports; device is manual-only so
>   the host cache is the authoritative PWM source. This eliminates the race
>   where an incoming report could overwrite a value just written.
> - Add completion/ACK mechanism: wait for device ACK after OUT report,
>   signalled from raw_event() via complete(); return device ACK status
> - Report buffer is kmalloc'd per write; see commit message for full
>   rationale. In summary: stack placement (suggested with __aligned()) is
>   hard-rejected by usb_hcd_map_urb_for_dma() via object_is_on_stack();
>   struct-embedded placement (suggested as alternative) creates a shared
>   resource race between concurrent writes. kmalloc per write avoids both.
> - Add Documentation/hwmon/arctic_fan_controller.rst to index.rst
> - Add MAINTAINERS entry
> 
>  Documentation/hwmon/arctic_fan_controller.rst |  40 +++
>  Documentation/hwmon/index.rst                 |   1 +
>  MAINTAINERS                                   |   7 +
>  drivers/hwmon/Kconfig                         |  12 +
>  drivers/hwmon/Makefile                        |   1 +
>  drivers/hwmon/arctic_fan_controller.c         | 278 ++++++++++++++++++
>  6 files changed, 339 insertions(+)
>  create mode 100644 Documentation/hwmon/arctic_fan_controller.rst
>  create mode 100644 drivers/hwmon/arctic_fan_controller.c
> 
> diff --git a/Documentation/hwmon/arctic_fan_controller.rst b/Documentation/hwmon/arctic_fan_controller.rst
> new file mode 100644
> index 000000000..58366551c
> --- /dev/null
> +++ b/Documentation/hwmon/arctic_fan_controller.rst
> @@ -0,0 +1,40 @@
> +.. 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.
> +
> +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). Read:
> +                 returns the last value written; the device is manual-only so
> +                 the host cache is authoritative.
> +================ ==============================================================
> diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst
> index b2ca8513c..c34713040 100644
> --- a/Documentation/hwmon/index.rst
> +++ b/Documentation/hwmon/index.rst
> @@ -42,6 +42,7 @@ Hardware Monitoring Kernel Drivers
>     aht10
>     amc6821
>     aquacomputer_d5next
> +   arctic_fan_controller
>     asb100
>     asc7621
>     aspeed-g6-pwm-tach
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 837db4f7b..8c32dddd6 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -2053,6 +2053,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>
> diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
> index 486152a8e..b6ab72752 100644
> --- a/drivers/hwmon/Kconfig
> +++ b/drivers/hwmon/Kconfig
> @@ -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 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
> diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
> index 5833c807c..ef831c337 100644
> --- a/drivers/hwmon/Makefile
> +++ b/drivers/hwmon/Makefile
> @@ -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
> diff --git a/drivers/hwmon/arctic_fan_controller.c b/drivers/hwmon/arctic_fan_controller.c
> new file mode 100644
> index 000000000..24db8a7ee
> --- /dev/null
> +++ b/drivers/hwmon/arctic_fan_controller.c
> @@ -0,0 +1,278 @@
> +// 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%).
> + * PWM is manual-only: the device does not change duty autonomously, only
> + * when it receives an OUT report from the host.
> + */
> +
> +#include <linux/cleanup.h>
> +#include <linux/completion.h>
> +#include <linux/err.h>
> +#include <linux/hid.h>
> +#include <linux/hwmon.h>
> +#include <linux/hwmon-sysfs.h>

Not needed.

> +#include <linux/jiffies.h>
> +#include <linux/kernel.h>
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/slab.h>
> +#include <linux/spinlock.h>
> +#include <linux/unaligned.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 */
> +/* 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 */
> +#define ARCTIC_ACK_TIMEOUT_MS	1000
> +
> +struct arctic_fan_data {
> +	struct hid_device *hdev;
> +	struct mutex lock;		/* protects fan_rpm, pwm_duty */
> +	spinlock_t in_report_lock;	/* protects ack_status and in_report_received */
> +	struct completion in_report_received; /* ACK (ID 0x02) received in raw_event */
> +	int ack_status;			/* 0 = OK, negative errno on device error */
> +	u32 fan_rpm[ARCTIC_NUM_FANS];
> +	u8 pwm_duty[ARCTIC_NUM_FANS];	/* 0-255 matching sysfs range; converted to 0-100 on send */
> +};
> +
> +/*
> + * 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.
> + */
> +static void arctic_fan_parse_report(struct arctic_fan_data *priv, u8 *buf,
> +				    int len, int rpm_off)
> +{
> +	int i;
> +
> +	if (len < rpm_off + 20)
> +		return;
> +	{
> +		guard(mutex)(&priv->lock);

The { } around the guard() are unnecessary, both here and elsewhere in the code.

If the guard() is added is because the call is from an event handler,
use hwmon_lock() and hwmon_unlock() to serialize accesses.

> +		for (i = 0; i < ARCTIC_NUM_FANS; i++)
> +			priv->fan_rpm[i] = get_unaligned_le16(&buf[rpm_off + i * 2]);
> +	}
> +}
> +
> +/*
> + * 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);
> +
> +	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(&priv->in_report_lock);
> +		priv->ack_status = data[1] == 0x00 ? 0 : -EIO;
> +		complete(&priv->in_report_received);
> +		spin_unlock(&priv->in_report_lock);
> +		return 0;
> +	}
> +
> +	if (report->id != ARCTIC_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);
> +
> +	{
> +		guard(mutex)(&priv->lock);
> +		if (type == hwmon_fan && attr == hwmon_fan_input) {
> +			*val = priv->fan_rpm[channel];
> +			return 0;
> +		}
> +		if (type == hwmon_pwm && attr == hwmon_pwm_input) {
> +			*val = priv->pwm_duty[channel];
> +			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 *buf;
> +	long t;
> +	int i, ret;
> +
> +	/*
> +	 * Must use a heap-allocated buffer: usb_hcd_map_urb_for_dma() rejects
> +	 * stack buffers with -EAGAIN, preventing USB transfers on DMA-capable
> +	 * host controllers.
> +	 */
> +	buf = kmalloc(ARCTIC_REPORT_LEN, GFP_KERNEL);
> +	if (!buf)
> +		return -ENOMEM;
> +
> +	{
> +		guard(mutex)(&priv->lock);
> +		priv->pwm_duty[channel] = (u8)val;
> +		buf[0] = ARCTIC_REPORT_ID;
> +		for (i = 0; i < ARCTIC_NUM_FANS; i++)
> +			buf[1 + i] = DIV_ROUND_CLOSEST((unsigned int)priv->pwm_duty[i] * 100, 255);
> +	}
> +	memset(buf + 11, 0, ARCTIC_REPORT_LEN - 11);
> +
> +	spin_lock_bh(&priv->in_report_lock);
> +	priv->ack_status = -ETIMEDOUT;
> +	reinit_completion(&priv->in_report_received);
> +	spin_unlock_bh(&priv->in_report_lock);
> +
> +	ret = hid_hw_output_report(priv->hdev, buf, ARCTIC_REPORT_LEN);
> +	kfree(buf);
> +	if (ret < 0)
> +		return ret;
> +
> +	t = wait_for_completion_interruptible_timeout(&priv->in_report_received,
> +						      msecs_to_jiffies(ARCTIC_ACK_TIMEOUT_MS));
> +	if (t == 0)
> +		return -ETIMEDOUT;
> +	if (t < 0)
> +		return t; /* interrupted by signal */
> +	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_probe(struct hid_device *hdev,
> +			    const struct hid_device_id *id)
> +{
> +	struct arctic_fan_data *priv;
> +	struct device *hwmon_dev;
> +	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;
> +	mutex_init(&priv->lock);
> +	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;
> +
> +	hid_device_io_start(hdev);
> +
> +	hwmon_dev = devm_hwmon_device_register_with_info(&hdev->dev, "arctic_fan",
> +							 priv, &arctic_fan_chip_info,
> +							 NULL);
> +	if (IS_ERR(hwmon_dev)) {
> +		ret = PTR_ERR(hwmon_dev);
> +		goto out_close;
> +	}
> +	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 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,
> +};
> +
> +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");