[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