[PATCH 2/2] hwmon: add Axiado AX3000 and AX3005 PWM fan controller driver

Petar Stepanovic <[email protected]> Thu, 06 Aug 2026 02:01:30 -0700
Newsgroups org.kernel.vger.linux-hwmon,org.infradead.lists.linux-arm-kernel,org.kernel.vger.linux-devicetree,org.kernel.vger.linux-doc,org.kernel.vger.linux-kernel,org.kernel.vger.linux-pwm
Message-ID <[email protected]>
Add support for the PWM fan controller found on the Axiado AX3000 and
AX3005 SoCs.

The controller uses a hardware tachometer block and interrupt to measure
fan speed. Fan speed is controlled through a PWM signal supplied by an
external PWM controller.

Register the controller with the hwmon subsystem and expose the standard
fan1_input and pwm1 attributes. The number of tachometer pulses generated
per fan revolution can be configured using the pulses-per-revolution
Devicetree property.

The driver was tested on AX3000 and AX3005 boards.

Signed-off-by: Petar Stepanovic <[email protected]>
---
 Documentation/hwmon/axiado-pwm-fan.rst |  38 ++++
 Documentation/hwmon/index.rst          |   1 +
 MAINTAINERS                            |   2 +
 drivers/hwmon/Kconfig                  |  12 +
 drivers/hwmon/Makefile                 |   1 +
 drivers/hwmon/axiado-pwm-fan.c         | 389 +++++++++++++++++++++++++++++++++
 6 files changed, 443 insertions(+)

diff --git a/Documentation/hwmon/axiado-pwm-fan.rst b/Documentation/hwmon/axiado-pwm-fan.rst
new file mode 100644
index 000000000000..d110e170b0df
--- /dev/null
+++ b/Documentation/hwmon/axiado-pwm-fan.rst
@@ -0,0 +1,38 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+Kernel driver axiado-pwm-fan
+============================
+
+Supported chips:
+
+  * Axiado AX3000
+  * Axiado AX3005
+
+    Datasheet: Not publicly available
+
+Author:
+
+  * Petar Stepanovic <[email protected]>
+
+Description
+-----------
+
+The Axiado AX3000 and AX3005 PWM fan controllers measure the rotational
+speed of one fan using a hardware tachometer block. Fan speed is controlled
+through a PWM signal supplied by an external PWM controller.
+
+The number of tachometer pulses generated per fan revolution is configured
+through the ``pulses-per-revolution`` devicetree property. If the property
+is not specified, the driver uses two pulses per revolution.
+
+Sysfs attributes
+----------------
+
+The driver provides the following standard hwmon attributes:
+
+=============== ====== =====================================================
+fan1_input      RO     Fan speed in revolutions per minute (RPM).
+
+pwm1            RW     Relative PWM control value from 0 to 255. A value of
+                       255 selects the maximum PWM duty cycle.
+=============== ====== =====================================================
diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst
index 29130df44d12..857924205861 100644
--- a/Documentation/hwmon/index.rst
+++ b/Documentation/hwmon/index.rst
@@ -51,6 +51,7 @@ Hardware Monitoring Kernel Drivers
    asus_ec_sensors
    asus_rog_ryujin
    asus_wmi_sensors
+   axiado-pwm-fan
    bcm54140
    bel-pfe
    bpa-rs600
diff --git a/MAINTAINERS b/MAINTAINERS
index f528b1a42e0f..61e7a0939002 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4464,6 +4464,8 @@ M:	Prasad Bolisetty <[email protected]>
 L:	[email protected]
 S:	Supported
 F:	Documentation/devicetree/bindings/hwmon/axiado,ax3000-pwm-fan.yaml
+F:	Documentation/hwmon/axiado-pwm-fan.rst
+F:	drivers/hwmon/axiado-pwm-fan.c
 
 AXIADO SPI DB DRIVER
 M:	Vladimir Moravcevic <[email protected]>
diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index 2bfbcc033d59..161054ba7312 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -469,6 +469,18 @@ config SENSORS_ATXP1
 	  This driver can also be built as a module. If so, the module
 	  will be called atxp1.
 
+config SENSORS_AXIADO_PWM_FAN
+	tristate "Axiado PWM fan controller"
+	depends on ARCH_AXIADO || COMPILE_TEST
+	depends on PWM
+	help
+	  This driver supports the Axiado PWM fan controller. It uses a PWM
+	  output to control fan speed and a tachometer interrupt to report fan
+	  speed as RPM through the hardware monitoring sysfs interface.
+
+	  This driver can also be built as a module. If so, the module will be
+	  called axiado-pwm-fan.
+
 config SENSORS_CGBC
 	tristate "Congatec Board Controller Sensors"
 	depends on MFD_CGBC
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index 63effc0ab8d1..78fcb22a1777 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -58,6 +58,7 @@ obj-$(CONFIG_SENSORS_ASPEED)	+= aspeed-pwm-tacho.o
 obj-$(CONFIG_SENSORS_ASPEED_G6) += aspeed-g6-pwm-tach.o
 obj-$(CONFIG_SENSORS_ASUS_ROG_RYUJIN)	+= asus_rog_ryujin.o
 obj-$(CONFIG_SENSORS_ATXP1)	+= atxp1.o
+obj-$(CONFIG_SENSORS_AXIADO_PWM_FAN) += axiado-pwm-fan.o
 obj-$(CONFIG_SENSORS_AXI_FAN_CONTROL) += axi-fan-control.o
 obj-$(CONFIG_SENSORS_CGBC)	+= cgbc-hwmon.o
 obj-$(CONFIG_SENSORS_CHIPCAP2) += chipcap2.o
diff --git a/drivers/hwmon/axiado-pwm-fan.c b/drivers/hwmon/axiado-pwm-fan.c
new file mode 100644
index 000000000000..b15bd3513d5e
--- /dev/null
+++ b/drivers/hwmon/axiado-pwm-fan.c
@@ -0,0 +1,389 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2021-2026 Axiado Corporation.
+ */
+
+#include <linux/bits.h>
+#include <linux/cleanup.h>
+#include <linux/clk.h>
+#include <linux/hwmon.h>
+#include <linux/io.h>
+#include <linux/interrupt.h>
+#include <linux/math64.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/platform_device.h>
+#include <linux/property.h>
+#include <linux/pwm.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+
+#define PWM_MAX	255
+
+/* TACH Register offsets */
+#define AX_TACH_CTRL_REG	0x00
+#define AX_TACH_TIMER_COUNT_REG	0x04
+#define AX_TACH_COUNT_REG	0x08
+#define AX_TACH_INT_STATUS_REG	0x0c
+
+#define AX_TACH_CTRL_ENABLE		BIT(0)
+#define AX_TACH_CTRL_INT_ENABLE		BIT(1)
+
+#define AX_TACH_INT_PENDING             BIT(0)
+
+struct axiado_pwm_fan_tach {
+	int irq;
+	u32 pulses_per_revolution;
+	u32 timer_count;
+	u32 count;
+};
+
+struct axiado_pwm_fan_ctx {
+	/* Protects PWM state and cached PWM value. */
+	struct mutex pwm_lock;
+	/* Protects tachometer count updated from interrupt context. */
+	spinlock_t tach_lock;
+	struct pwm_device *pwm;
+	struct pwm_state pwm_state;
+	void __iomem *tach_base;
+	struct axiado_pwm_fan_tach tach;
+	unsigned int pwm_value;
+};
+
+static const struct hwmon_channel_info * const pwm_fan_info[] = {
+	HWMON_CHANNEL_INFO(pwm, HWMON_PWM_INPUT),
+	HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT),
+	NULL
+};
+
+static irqreturn_t axiado_tach_irq_handler(int irq, void *dev)
+{
+	struct axiado_pwm_fan_ctx *ctx = dev;
+	u32 status;
+
+	status = ioread32(ctx->tach_base + AX_TACH_INT_STATUS_REG);
+
+	if (!status)
+		return IRQ_NONE;
+
+	scoped_guard(spinlock_irqsave, &ctx->tach_lock) {
+		ctx->tach.count = ioread32(ctx->tach_base + AX_TACH_COUNT_REG);
+	}
+
+	iowrite32(AX_TACH_INT_PENDING, ctx->tach_base + AX_TACH_INT_STATUS_REG);
+
+	return IRQ_HANDLED;
+}
+
+static int axiado_set_pwm(struct axiado_pwm_fan_ctx *ctx, unsigned int pwm)
+{
+	struct pwm_state state;
+	int ret;
+
+	guard(mutex)(&ctx->pwm_lock);
+
+	if (ctx->pwm_value == pwm)
+		return 0;
+
+	state = ctx->pwm_state;
+	state.duty_cycle = DIV_ROUND_UP_ULL((u64)pwm * state.period, PWM_MAX);
+	state.enabled = pwm > 0;
+
+	ret = pwm_apply_might_sleep(ctx->pwm, &state);
+	if (ret)
+		return ret;
+
+	ctx->pwm_state = state;
+	ctx->pwm_value = pwm;
+
+	return 0;
+}
+
+static int axiado_pwm_fan_write(struct device *dev,
+				enum hwmon_sensor_types type, u32 attr,
+				int channel, long val)
+{
+	struct axiado_pwm_fan_ctx *ctx = dev_get_drvdata(dev);
+
+	if (type != hwmon_pwm || attr != hwmon_pwm_input)
+		return -EOPNOTSUPP;
+
+	if (val < 0 || val > PWM_MAX)
+		return -EINVAL;
+
+	return axiado_set_pwm(ctx, val);
+}
+
+static unsigned int axiado_tach_get_rpm(struct axiado_pwm_fan_ctx *ctx)
+{
+	u32 pulses_per_revolution = ctx->tach.pulses_per_revolution;
+	u64 pulses, rpm;
+
+	scoped_guard(spinlock_irqsave, &ctx->tach_lock)
+		pulses = ctx->tach.count;
+
+	if (!pulses_per_revolution)
+		return 0;
+
+	rpm = pulses * 60;
+	do_div(rpm, pulses_per_revolution);
+
+	return rpm;
+}
+
+static int axiado_pwm_fan_read(struct device *dev, enum hwmon_sensor_types type,
+			       u32 attr, int channel, long *val)
+{
+	struct axiado_pwm_fan_ctx *ctx = dev_get_drvdata(dev);
+
+	switch (type) {
+	case hwmon_pwm:
+		if (attr != hwmon_pwm_input)
+			return -EOPNOTSUPP;
+
+		scoped_guard(mutex, &ctx->pwm_lock)
+			*val = ctx->pwm_value;
+
+		return 0;
+
+	case hwmon_fan:
+		if (attr != hwmon_fan_input)
+			return -EOPNOTSUPP;
+
+		*val = axiado_tach_get_rpm(ctx);
+
+		return 0;
+
+	default:
+		return -EOPNOTSUPP;
+	}
+}
+
+static umode_t axiado_pwm_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 const struct hwmon_ops pwm_fan_hwmon_ops = {
+	.is_visible = axiado_pwm_fan_is_visible,
+	.read = axiado_pwm_fan_read,
+	.write = axiado_pwm_fan_write,
+};
+
+static const struct hwmon_chip_info pwm_fan_chip_info = {
+	.ops = &pwm_fan_hwmon_ops,
+	.info = pwm_fan_info,
+};
+
+static int axiado_pwm_apply_disabled(struct axiado_pwm_fan_ctx *ctx)
+{
+	struct pwm_state state;
+
+	guard(mutex)(&ctx->pwm_lock);
+
+	if (!ctx->pwm_value)
+		return 0;
+
+	state = ctx->pwm_state;
+	state.duty_cycle = 0;
+	state.enabled = false;
+
+	return pwm_apply_might_sleep(ctx->pwm, &state);
+}
+
+static void axiado_pwm_disable(void *data)
+{
+	struct axiado_pwm_fan_ctx *ctx = data;
+
+	axiado_pwm_apply_disabled(ctx);
+}
+
+static void axiado_tach_enable(struct axiado_pwm_fan_ctx *ctx)
+{
+	iowrite32(AX_TACH_INT_PENDING,
+		  ctx->tach_base + AX_TACH_INT_STATUS_REG);
+	iowrite32(ctx->tach.timer_count,
+		  ctx->tach_base + AX_TACH_TIMER_COUNT_REG);
+	iowrite32(AX_TACH_CTRL_ENABLE | AX_TACH_CTRL_INT_ENABLE,
+		  ctx->tach_base + AX_TACH_CTRL_REG);
+}
+
+static void axiado_tach_disable(void *data)
+{
+	struct axiado_pwm_fan_ctx *ctx = data;
+
+	iowrite32(0, ctx->tach_base + AX_TACH_CTRL_REG);
+	iowrite32(AX_TACH_INT_PENDING,
+		  ctx->tach_base + AX_TACH_INT_STATUS_REG);
+}
+
+static int axiado_pwm_fan_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct axiado_pwm_fan_ctx *ctx;
+	unsigned long tach_clk_rate;
+	struct device *hwmon;
+	struct clk *tach_clk;
+	int ret;
+
+	ctx = devm_kzalloc(dev, sizeof(*ctx), GFP_KERNEL);
+	if (!ctx)
+		return -ENOMEM;
+
+	mutex_init(&ctx->pwm_lock);
+	spin_lock_init(&ctx->tach_lock);
+
+	ctx->tach_base = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(ctx->tach_base))
+		return PTR_ERR(ctx->tach_base);
+
+	ctx->pwm = devm_pwm_get(dev, NULL);
+	if (IS_ERR(ctx->pwm))
+		return dev_err_probe(dev, PTR_ERR(ctx->pwm),
+				     "Could not get PWM\n");
+
+	platform_set_drvdata(pdev, ctx);
+
+	pwm_init_state(ctx->pwm, &ctx->pwm_state);
+
+	if (!ctx->pwm_state.period)
+		return dev_err_probe(dev, -EINVAL, "PWM period is zero\n");
+
+	tach_clk = devm_clk_get_enabled(dev, NULL);
+	if (IS_ERR(tach_clk))
+		return dev_err_probe(dev, PTR_ERR(tach_clk),
+				     "Failed to get tachometer clock\n");
+
+	tach_clk_rate = clk_get_rate(tach_clk);
+	if (!tach_clk_rate || tach_clk_rate > U32_MAX)
+		return dev_err_probe(dev, -EINVAL,
+				     "Invalid tachometer clock rate: %lu\n",
+				     tach_clk_rate);
+
+	ctx->tach.timer_count = tach_clk_rate;
+	ctx->tach.pulses_per_revolution = 2;
+	device_property_read_u32(dev, "pulses-per-revolution",
+				 &ctx->tach.pulses_per_revolution);
+
+	if (!ctx->tach.pulses_per_revolution)
+		return dev_err_probe(dev, -EINVAL,
+				     "pulses-per-revolution cannot be zero\n");
+
+	ctx->tach.irq = platform_get_irq(pdev, 0);
+	if (ctx->tach.irq < 0)
+		return dev_err_probe(dev, ctx->tach.irq,
+				     "Failed to get tachometer IRQ\n");
+
+	ret = devm_request_irq(dev, ctx->tach.irq, axiado_tach_irq_handler, 0,
+			       dev_name(dev), ctx);
+	if (ret)
+		return dev_err_probe(dev, ret, "Failed to request tach IRQ\n");
+
+	ret = devm_add_action_or_reset(dev, axiado_tach_disable, ctx);
+	if (ret)
+		return ret;
+
+	dev_dbg(dev, "Fan tachometer: irq=%d, pulses_per_revolution=%u\n",
+		ctx->tach.irq, ctx->tach.pulses_per_revolution);
+
+	axiado_tach_enable(ctx);
+
+	ret = axiado_set_pwm(ctx, PWM_MAX);
+
+	if (ret)
+		return dev_err_probe(dev, ret, "Failed to configure PWM\n");
+
+	ret = devm_add_action_or_reset(dev, axiado_pwm_disable, ctx);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "Failed to add PWM disable action\n");
+
+	hwmon = devm_hwmon_device_register_with_info(dev, "axpwmfan", ctx,
+						     &pwm_fan_chip_info, NULL);
+	if (IS_ERR(hwmon))
+		return dev_err_probe(dev, PTR_ERR(hwmon),
+				     "Failed to register hwmon device\n");
+
+	return 0;
+}
+
+static int axiado_pwm_fan_disable(struct device *dev)
+{
+	struct axiado_pwm_fan_ctx *ctx = dev_get_drvdata(dev);
+	int ret;
+
+	ret = axiado_pwm_apply_disabled(ctx);
+	if (ret)
+		return ret;
+
+	axiado_tach_disable(ctx);
+	synchronize_irq(ctx->tach.irq);
+
+	return 0;
+}
+
+static void axiado_pwm_fan_shutdown(struct platform_device *pdev)
+{
+	struct axiado_pwm_fan_ctx *ctx = platform_get_drvdata(pdev);
+
+	/* Best effort during shutdown. */
+	axiado_pwm_apply_disabled(ctx);
+
+	axiado_tach_disable(ctx);
+	synchronize_irq(ctx->tach.irq);
+}
+
+static int axiado_pwm_fan_suspend(struct device *dev)
+{
+	return axiado_pwm_fan_disable(dev);
+}
+
+static int axiado_pwm_fan_resume(struct device *dev)
+{
+	struct axiado_pwm_fan_ctx *ctx = dev_get_drvdata(dev);
+	int ret;
+
+	axiado_tach_enable(ctx);
+
+	scoped_guard(mutex, &ctx->pwm_lock)
+		ret = pwm_apply_might_sleep(ctx->pwm, &ctx->pwm_state);
+
+	if (ret)
+		axiado_tach_disable(ctx);
+
+	return ret;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(axiado_pwm_fan_pm,
+				axiado_pwm_fan_suspend,
+				axiado_pwm_fan_resume);
+
+static const struct of_device_id axiado_pwm_fan_match[] = {
+	{ .compatible = "axiado,ax3000-pwm-fan" },
+	{ },
+};
+MODULE_DEVICE_TABLE(of, axiado_pwm_fan_match);
+
+static struct platform_driver axiado_pwm_fan_driver = {
+	.probe		= axiado_pwm_fan_probe,
+	.shutdown	= axiado_pwm_fan_shutdown,
+	.driver	= {
+		.name		= "axiado-pwm-fan",
+		.pm		= pm_sleep_ptr(&axiado_pwm_fan_pm),
+		.of_match_table	= axiado_pwm_fan_match,
+	},
+};
+
+module_platform_driver(axiado_pwm_fan_driver);
+MODULE_AUTHOR("Axiado Corporation");
+MODULE_DESCRIPTION("Axiado PWM fan controller driver");
+MODULE_LICENSE("GPL");
+

-- 
2.34.1