Add trivial PWM driver for Argon40 Fan HAT, which is a RaspberryPi blower fan hat which can be controlled over I2C. Model this device as a PWM, so the pwm-fan can be attached to it and handle thermal zones and RPM management in a generic manner. Signed-off-by: Marek Vasut <marek.vasut+renesas@xxxxxxxxxxx> --- Cc: "Uwe Kleine-König" <ukleinek@xxxxxxxxxx> Cc: Conor Dooley <conor+dt@xxxxxxxxxx> Cc: Krzysztof Kozlowski <krzk+dt@xxxxxxxxxx> Cc: Rob Herring <robh@xxxxxxxxxx> Cc: devicetree@xxxxxxxxxxxxxxx Cc: linux-pwm@xxxxxxxxxxxxxxx Cc: linux-renesas-soc@xxxxxxxxxxxxxxx --- V2: - Switch to waveform ops - Add shutdown hook to force the fan to maximum RPM on shutdown instead of stopping it, to be on the safe side --- drivers/pwm/Kconfig | 9 +++ drivers/pwm/Makefile | 1 + drivers/pwm/pwm-argon-fan-hat.c | 115 ++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 drivers/pwm/pwm-argon-fan-hat.c diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig index 6e113f8b4baf..3ef1757502eb 100644 --- a/drivers/pwm/Kconfig +++ b/drivers/pwm/Kconfig @@ -66,6 +66,15 @@ config PWM_APPLE To compile this driver as a module, choose M here: the module will be called pwm-apple. +config PWM_ARGON_FAN_HAT + tristate "Argon40 Fan HAT support" + depends on I2C && OF + help + Generic PWM framework driver for Argon40 Fan HAT. + + To compile this driver as a module, choose M here: the module + will be called pwm-argon-fan-hat. + config PWM_ATMEL tristate "Atmel PWM support" depends on ARCH_AT91 || COMPILE_TEST diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile index 96160f4257fc..ff4f47e5fb7a 100644 --- a/drivers/pwm/Makefile +++ b/drivers/pwm/Makefile @@ -3,6 +3,7 @@ obj-$(CONFIG_PWM) += core.o obj-$(CONFIG_PWM_AB8500) += pwm-ab8500.o obj-$(CONFIG_PWM_ADP5585) += pwm-adp5585.o obj-$(CONFIG_PWM_APPLE) += pwm-apple.o +obj-$(CONFIG_PWM_ARGON_FAN_HAT) += pwm-argon-fan-hat.o obj-$(CONFIG_PWM_ATMEL) += pwm-atmel.o obj-$(CONFIG_PWM_ATMEL_HLCDC_PWM) += pwm-atmel-hlcdc.o obj-$(CONFIG_PWM_ATMEL_TCB) += pwm-atmel-tcb.o diff --git a/drivers/pwm/pwm-argon-fan-hat.c b/drivers/pwm/pwm-argon-fan-hat.c new file mode 100644 index 000000000000..a26b58ee7f29 --- /dev/null +++ b/drivers/pwm/pwm-argon-fan-hat.c @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright (C) 2025 Marek Vasut + * + * Limitations: + * - no support for offset/polarity + * - fixed duty cycle, period changes from 0Hz..120kHz + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/pwm.h> + +static int argon_fan_hat_round_waveform_tohw(struct pwm_chip *chip, + struct pwm_device *pwm, + const struct pwm_waveform *wf, + void *_wfhw) +{ + u8 *wfhw = _wfhw; + + *wfhw = DIV_ROUND_CLOSEST_ULL(wf->duty_length_ns * 100, wf->period_length_ns); + + return 0; +} + +static int argon_fan_hat_round_waveform_fromhw(struct pwm_chip *chip, + struct pwm_device *pwm, + const void *_wfhw, + struct pwm_waveform *wf) +{ + const u8 *wfhw = _wfhw; + + /* 1 step of this hardware is cca. 1200 Hz increase in frequency */ + wf->period_length_ns = DIV64_U64_ROUND_UP(NSEC_PER_SEC, *wfhw * 1200); + wf->duty_length_ns = wf->period_length_ns / 10; + wf->duty_offset_ns = 0; + + return 0; +} + +static int argon_fan_hat_write_waveform(struct pwm_chip *chip, + struct pwm_device *pwm, + const void *_wfhw) +{ + struct i2c_client *i2c = pwmchip_get_drvdata(chip); + const u8 *wfhw = _wfhw; + u8 tx[2] = { 0x80, *wfhw }; + struct i2c_msg msg = { + .addr = i2c->addr, + .len = 2, + .buf = tx, + }; + + return (i2c_transfer(i2c->adapter, &msg, 1) == 1) ? 0 : -EINVAL; +} + +static const struct pwm_ops argon_fan_hat_pwm_ops = { + .sizeof_wfhw = sizeof(u8), + .round_waveform_fromhw = argon_fan_hat_round_waveform_fromhw, + .round_waveform_tohw = argon_fan_hat_round_waveform_tohw, + .write_waveform = argon_fan_hat_write_waveform, +}; + +static int argon_fan_hat_i2c_probe(struct i2c_client *i2c) +{ + struct pwm_chip *pc = devm_pwmchip_alloc(&i2c->dev, 1, 0); + int ret; + + if (IS_ERR(pc)) + return PTR_ERR(pc); + + pc->ops = &argon_fan_hat_pwm_ops; + pwmchip_set_drvdata(pc, i2c); + + ret = devm_pwmchip_add(&i2c->dev, pc); + if (ret) + return dev_err_probe(&i2c->dev, ret, "Could not add PWM chip\n"); + + return 0; +} + +static void argon_fan_hat_i2c_shutdown(struct i2c_client *i2c) +{ + u8 tx[2] = { 0x80, 0x64 }; + struct i2c_msg msg = { + .addr = i2c->addr, + .len = 2, + .buf = tx, + }; + + i2c_transfer(i2c->adapter, &msg, 1); +} + +static const struct of_device_id argon_fan_hat_dt_ids[] = { + { .compatible = "argon40,fan-hat" }, + { }, +}; +MODULE_DEVICE_TABLE(of, argon_fan_hat_dt_ids); + +static struct i2c_driver argon_fan_hat_driver = { + .driver = { + .name = "argon-fan-hat", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + .of_match_table = argon_fan_hat_dt_ids, + }, + .probe = argon_fan_hat_i2c_probe, + .shutdown = argon_fan_hat_i2c_shutdown, +}; + +module_i2c_driver(argon_fan_hat_driver); + +MODULE_AUTHOR("Marek Vasut <marek.vasut+renesas@xxxxxxxxxxx>"); +MODULE_DESCRIPTION("Argon40 Fan HAT"); +MODULE_LICENSE("GPL"); -- 2.47.2