Re: [PATCH v6 2/3] pwm: rp1: Add RP1 PWM controller driver
Matthias Brugger <[email protected]>
| Newsgroups | org.kernel.vger.linux-pwm,org.infradead.lists.linux-arm-kernel,org.kernel.vger.linux-devicetree,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <[email protected]> |
On 03/07/2026 19:05, Andrea della Porta wrote: > From: Naushir Patuck <[email protected]> > > The Raspberry Pi RP1 southbridge features an embedded PWM > controller with 4 output channels, alongside an RPM interface > to read the fan speed on the Raspberry Pi 5. > > Add the supporting driver. > > Signed-off-by: Naushir Patuck <[email protected]> > Co-developed-by: Stanimir Varbanov <[email protected]> > Signed-off-by: Stanimir Varbanov <[email protected]> > Signed-off-by: Andrea della Porta <[email protected]> Reviewed-by: Matthias Brugger <[email protected]> > --- > drivers/pwm/Kconfig | 9 + > drivers/pwm/Makefile | 1 + > drivers/pwm/pwm-rp1.c | 435 ++++++++++++++++++++++++++++++++++++++++++ > 3 files changed, 445 insertions(+) > create mode 100644 drivers/pwm/pwm-rp1.c > > diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig > index e8886a9b64d96..0373dcf8d400d 100644 > --- a/drivers/pwm/Kconfig > +++ b/drivers/pwm/Kconfig > @@ -637,6 +637,15 @@ config PWM_ROCKCHIP > Generic PWM framework driver for the PWM controller found on > Rockchip SoCs. > > +config PWM_RASPBERRYPI_RP1 > + tristate "RP1 PWM support" > + depends on MISC_RP1 || COMPILE_TEST > + depends on HAS_IOMEM > + select REGMAP_MMIO > + select MFD_SYSCON > + help > + PWM framework driver for Raspberry Pi RP1 controller. > + > config PWM_SAMSUNG > tristate "Samsung PWM support" > depends on PLAT_SAMSUNG || ARCH_S5PV210 || ARCH_EXYNOS || COMPILE_TEST > diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile > index 5630a521a7cff..c07fd24f69f39 100644 > --- a/drivers/pwm/Makefile > +++ b/drivers/pwm/Makefile > @@ -57,6 +57,7 @@ obj-$(CONFIG_PWM_RENESAS_RZG2L_GPT) += pwm-rzg2l-gpt.o > obj-$(CONFIG_PWM_RENESAS_RZ_MTU3) += pwm-rz-mtu3.o > obj-$(CONFIG_PWM_RENESAS_TPU) += pwm-renesas-tpu.o > obj-$(CONFIG_PWM_ROCKCHIP) += pwm-rockchip.o > +obj-$(CONFIG_PWM_RASPBERRYPI_RP1) += pwm-rp1.o > obj-$(CONFIG_PWM_SAMSUNG) += pwm-samsung.o > obj-$(CONFIG_PWM_SIFIVE) += pwm-sifive.o > obj-$(CONFIG_PWM_SL28CPLD) += pwm-sl28cpld.o > diff --git a/drivers/pwm/pwm-rp1.c b/drivers/pwm/pwm-rp1.c > new file mode 100644 > index 0000000000000..8ef5ec9ed3a07 > --- /dev/null > +++ b/drivers/pwm/pwm-rp1.c > @@ -0,0 +1,435 @@ > +// SPDX-License-Identifier: GPL-2.0 > +/* > + * pwm-rp1.c > + * > + * Raspberry Pi RP1 PWM. > + * > + * Copyright © 2026 Raspberry Pi Ltd. > + * > + * Author: Naushir Patuck ([email protected]) > + * > + * Based on the pwm-bcm2835 driver by: > + * Bart Tanghe <[email protected]> > + * > + * Datasheet: https://pip-assets.raspberrypi.com/categories/892-raspberry-pi-5/documents/RP-008370-DS-1-rp1-peripherals.pdf?disposition=inline > + * > + * Limitations: > + * - Channels can be enabled/disabled through a global update flag, while the > + * period and duty per-channel registers are independently updatable, and > + * they are latched on the end of (specific channel) period strobe. > + * This means that period and duty changes might result in glitches if the > + * period/duty is changed exactly during an end of period strobe. > + * - Since the duty/period registers are freely updatable (do not depend on > + * the global update flag), setting one of them close to the period end and > + * the other right afterwards results in a mixed output for that cycle because > + * the write ops are not atomic. > + * - The global update flag prevents mis-sampling of multi-bit bus signals in > + * the PWM clock domain. This ensures that all PWM channel settings update > + * on the same PWM clock cycle. Channels start in sync only if they share the > + * same period. > + * - If both duty and period are set to 0, the output is a constant low signal > + * if polarity is normal or a constant high signal if polarity is inversed. > + * - When disabled the output is driven to 0 if polarity is normal, or to 1 > + * if polarity is inversed. > + * - Disabling the PWM stops the output immediately, without waiting for current > + * period to complete first. > + * - Channels are phase-capable, but on RPi5, the firmware can use a channel > + * phase register to report the RPM of the fan connected to that PWM > + * channel. As a result, phase control will be ignored for now. > + */ > + > +#include <linux/bitops.h> > +#include <linux/clk.h> > +#include <linux/err.h> > +#include <linux/io.h> > +#include <linux/module.h> > +#include <linux/of.h> > +#include <linux/platform_device.h> > +#include <linux/pwm.h> > +#include <linux/regmap.h> > +#include <linux/mfd/syscon.h> > +#include <linux/units.h> > + > +#define RP1_PWM_GLB_CTRL 0x000 > +#define RP1_PWM_GLB_CTRL_CHANNEL_ENABLE(chan) BIT(chan) > +#define RP1_PWM_GLB_CTRL_SET_UPDATE BIT(31) > + > +#define RP1_PWM_CHAN_CTRL(chan) (0x014 + ((chan) * 0x10)) > +#define RP1_PWM_CHAN_CTRL_POLARITY BIT(3) > +#define RP1_PWM_CHAN_CTRL_FIFO_POP_MASK BIT(8) > +#define RP1_PWM_CHAN_CTRL_MODE GENMASK(2, 0) > +enum rp1_pwm_ctrl_mode { > + RP1_PWM_CHAN_CTRL_MODE_ZERO, > + RP1_PWM_CHAN_CTRL_MODE_TE_MS, > + RP1_PWM_CHAN_CTRL_MODE_PC_MS, > + RP1_PWM_CHAN_CTRL_MODE_PD_ENC, > + RP1_PWM_CHAN_CTRL_MODE_MSB_SER, > + RP1_PWM_CHAN_CTRL_MODE_PPM, > + RP1_PWM_CHAN_CTRL_MODE_LE_MS, > + RP1_PWM_CHAN_CTRL_MODE_LSB_SER, > +}; > + > +#define RP1_PWM_CHAN_CTRL_DEFAULT (RP1_PWM_CHAN_CTRL_FIFO_POP_MASK + \ > + FIELD_PREP(RP1_PWM_CHAN_CTRL_MODE, \ > + RP1_PWM_CHAN_CTRL_MODE_TE_MS)) > + > +#define RP1_PWM_RANGE(chan) (0x018 + ((chan) * 0x10)) > +#define RP1_PWM_PHASE(chan) (0x01C + ((chan) * 0x10)) > +#define RP1_PWM_DUTY(chan) (0x020 + ((chan) * 0x10)) > + > +#define RP1_PWM_NUM_PWMS 4 > + > +struct rp1_pwm { > + struct regmap *regmap; > + struct clk *clk; > + unsigned long clk_rate; > + bool clk_enabled; > +}; > + > +struct rp1_pwm_waveform { > + u32 period_ticks; > + u32 duty_ticks; > + bool enabled; > + bool inverted_polarity; > +}; > + > +static const struct regmap_config rp1_pwm_regmap_config = { > + .reg_bits = 32, > + .val_bits = 32, > + .reg_stride = 4, > + .max_register = 0x60, > +}; > + > +static void rp1_pwm_apply_config(struct pwm_chip *chip, struct pwm_device *pwm) > +{ > + struct rp1_pwm *rp1 = pwmchip_get_drvdata(chip); > + u32 value; > + > + /* update the changed registers on the next strobe to avoid glitches */ > + regmap_read(rp1->regmap, RP1_PWM_GLB_CTRL, &value); > + value |= RP1_PWM_GLB_CTRL_SET_UPDATE; > + regmap_write(rp1->regmap, RP1_PWM_GLB_CTRL, value); > +} > + > +static int rp1_pwm_request(struct pwm_chip *chip, struct pwm_device *pwm) > +{ > + struct rp1_pwm *rp1 = pwmchip_get_drvdata(chip); > + > + /* init channel to reset defaults, preserving the polarity bit */ > + regmap_update_bits(rp1->regmap, RP1_PWM_CHAN_CTRL(pwm->hwpwm), > + ~(u32)RP1_PWM_CHAN_CTRL_POLARITY, RP1_PWM_CHAN_CTRL_DEFAULT); > + return 0; > +} > + > +static int rp1_pwm_round_waveform_tohw(struct pwm_chip *chip, > + struct pwm_device *pwm, > + const struct pwm_waveform *wf, > + void *_wfhw) > +{ > + struct rp1_pwm *rp1 = pwmchip_get_drvdata(chip); > + u64 period_ticks, duty_ticks, offset_ticks; > + struct rp1_pwm_waveform *wfhw = _wfhw; > + u64 clk_rate = rp1->clk_rate; > + int ret = 0; > + > + if (!wf->period_length_ns) { > + wfhw->enabled = false; > + wfhw->inverted_polarity = (pwm_get_polarity(pwm) == PWM_POLARITY_INVERSED); > + return 0; > + } > + > + period_ticks = mul_u64_u64_div_u64(wf->period_length_ns, clk_rate, NSEC_PER_SEC); > + > + /* > + * The period is limited to U32_MAX, and it will be decremented by one later > + * to allow 100% duty cycle. > + */ > + if (period_ticks > U32_MAX) { > + period_ticks = U32_MAX; > + } else if (period_ticks < 2) { > + period_ticks = 2; > + ret = 1; > + } > + > + duty_ticks = mul_u64_u64_div_u64(wf->duty_length_ns, clk_rate, NSEC_PER_SEC); > + duty_ticks = min(duty_ticks, period_ticks); > + offset_ticks = mul_u64_u64_div_u64(wf->duty_offset_ns, clk_rate, NSEC_PER_SEC); > + if (offset_ticks >= period_ticks) { > + u64 remainder; > + > + div64_u64_rem(offset_ticks, period_ticks, &remainder); > + offset_ticks = remainder; > + } > + if (duty_ticks && offset_ticks && > + duty_ticks + offset_ticks >= period_ticks) { > + wfhw->duty_ticks = period_ticks - duty_ticks; > + wfhw->inverted_polarity = true; > + } else { > + wfhw->duty_ticks = duty_ticks; > + wfhw->inverted_polarity = false; > + } > + /* Account for the extra tick at the end of the period */ > + wfhw->period_ticks = period_ticks - 1; > + > + wfhw->enabled = true; > + > + return ret; > +} > + > +static int rp1_pwm_round_waveform_fromhw(struct pwm_chip *chip, > + struct pwm_device *pwm, > + const void *_wfhw, > + struct pwm_waveform *wf) > +{ > + struct rp1_pwm *rp1 = pwmchip_get_drvdata(chip); > + const struct rp1_pwm_waveform *wfhw = _wfhw; > + u64 clk_rate = rp1->clk_rate; > + u64 ticks; > + > + *wf = (struct pwm_waveform){ }; > + > + if (!wfhw->enabled) > + return 0; > + > + wf->period_length_ns = DIV_ROUND_UP_ULL(((u64)wfhw->period_ticks + 1) * NSEC_PER_SEC, > + clk_rate); > + > + if (!wfhw->inverted_polarity) { > + wf->duty_length_ns = DIV_ROUND_UP_ULL((u64)wfhw->duty_ticks * NSEC_PER_SEC, > + (u32)clk_rate); > + } else { > + if (wfhw->duty_ticks > (u64)wfhw->period_ticks + 1) { > + /* 100% duty cycle case */ > + ticks = 0; > + } else { > + ticks = (u64)wfhw->period_ticks + 1 - wfhw->duty_ticks; > + } > + wf->duty_length_ns = DIV_ROUND_UP_ULL(ticks * NSEC_PER_SEC, clk_rate); > + wf->duty_offset_ns = wf->period_length_ns - wf->duty_length_ns; > + } > + > + return 0; > +} > + > +static int rp1_pwm_write_waveform(struct pwm_chip *chip, > + struct pwm_device *pwm, > + const void *_wfhw) > +{ > + struct rp1_pwm *rp1 = pwmchip_get_drvdata(chip); > + const struct rp1_pwm_waveform *wfhw = _wfhw; > + u32 value, ctrl; > + > + /* set polarity */ > + regmap_read(rp1->regmap, RP1_PWM_CHAN_CTRL(pwm->hwpwm), &value); > + if (!wfhw->inverted_polarity) > + value &= ~RP1_PWM_CHAN_CTRL_POLARITY; > + else > + value |= RP1_PWM_CHAN_CTRL_POLARITY; > + regmap_write(rp1->regmap, RP1_PWM_CHAN_CTRL(pwm->hwpwm), value); > + > + /* early exit if disabled */ > + regmap_read(rp1->regmap, RP1_PWM_GLB_CTRL, &ctrl); > + if (!wfhw->enabled) { > + ctrl &= ~RP1_PWM_GLB_CTRL_CHANNEL_ENABLE(pwm->hwpwm); > + goto exit_disable; > + } > + > + /* set period and duty cycle */ > + regmap_write(rp1->regmap, > + RP1_PWM_RANGE(pwm->hwpwm), wfhw->period_ticks); > + regmap_write(rp1->regmap, > + RP1_PWM_DUTY(pwm->hwpwm), wfhw->duty_ticks); > + > + /* enable the channel */ > + ctrl |= RP1_PWM_GLB_CTRL_CHANNEL_ENABLE(pwm->hwpwm); > +exit_disable: > + regmap_write(rp1->regmap, RP1_PWM_GLB_CTRL, ctrl); > + > + rp1_pwm_apply_config(chip, pwm); > + > + return 0; > +} > + > +static int rp1_pwm_read_waveform(struct pwm_chip *chip, > + struct pwm_device *pwm, > + void *_wfhw) > +{ > + struct rp1_pwm *rp1 = pwmchip_get_drvdata(chip); > + struct rp1_pwm_waveform *wfhw = _wfhw; > + u32 value; > + > + regmap_read(rp1->regmap, RP1_PWM_GLB_CTRL, &value); > + wfhw->enabled = !!(value & RP1_PWM_GLB_CTRL_CHANNEL_ENABLE(pwm->hwpwm)); > + > + regmap_read(rp1->regmap, RP1_PWM_CHAN_CTRL(pwm->hwpwm), &value); > + wfhw->inverted_polarity = !!(value & RP1_PWM_CHAN_CTRL_POLARITY); > + > + if (wfhw->enabled) { > + regmap_read(rp1->regmap, RP1_PWM_RANGE(pwm->hwpwm), &wfhw->period_ticks); > + regmap_read(rp1->regmap, RP1_PWM_DUTY(pwm->hwpwm), &wfhw->duty_ticks); > + } else { > + wfhw->period_ticks = 0; > + wfhw->duty_ticks = 0; > + } > + > + return 0; > +} > + > +static const struct pwm_ops rp1_pwm_ops = { > + .sizeof_wfhw = sizeof(struct rp1_pwm_waveform), > + .request = rp1_pwm_request, > + .round_waveform_tohw = rp1_pwm_round_waveform_tohw, > + .round_waveform_fromhw = rp1_pwm_round_waveform_fromhw, > + .read_waveform = rp1_pwm_read_waveform, > + .write_waveform = rp1_pwm_write_waveform, > +}; > + > +static int rp1_pwm_probe(struct platform_device *pdev) > +{ > + struct device *dev = &pdev->dev; > + struct device_node *np = dev->of_node; > + unsigned long clk_rate; > + struct pwm_chip *chip; > + void __iomem *base; > + struct rp1_pwm *rp1; > + int ret; > + > + chip = devm_pwmchip_alloc(dev, RP1_PWM_NUM_PWMS, sizeof(*rp1)); > + if (IS_ERR(chip)) > + return PTR_ERR(chip); > + > + rp1 = pwmchip_get_drvdata(chip); > + > + base = devm_platform_ioremap_resource(pdev, 0); > + if (IS_ERR(base)) > + return PTR_ERR(base); > + > + rp1->regmap = devm_regmap_init_mmio(dev, base, &rp1_pwm_regmap_config); > + if (IS_ERR(rp1->regmap)) > + return dev_err_probe(dev, PTR_ERR(rp1->regmap), "Cannot initialize regmap\n"); > + > + rp1->clk = devm_clk_get(dev, NULL); > + if (IS_ERR(rp1->clk)) > + return dev_err_probe(dev, PTR_ERR(rp1->clk), "Clock not found\n"); > + > + ret = clk_prepare_enable(rp1->clk); > + if (ret) > + return dev_err_probe(dev, ret, "Failed to enable clock\n"); > + rp1->clk_enabled = true; > + > + ret = devm_clk_rate_exclusive_get(dev, rp1->clk); > + if (ret) { > + dev_err_probe(dev, ret, "Failed to get exclusive rate\n"); > + goto err_disable_clk; > + } > + > + clk_rate = clk_get_rate(rp1->clk); > + if (!clk_rate) { > + ret = dev_err_probe(dev, -EINVAL, "Failed to get clock rate\n"); > + goto err_disable_clk; > + } > + /* > + * To prevent u64 overflow in period calculations: > + * mul_u64_u64_div_u64(period_ns, clk_rate, NSEC_PER_SEC) > + * If clk_rate > 1 GHz, the result can overflow. > + */ > + if (clk_rate > HZ_PER_GHZ) { > + ret = dev_err_probe(dev, -EINVAL, "Clock rate > 1 GHz is not supported\n"); > + goto err_disable_clk; > + } > + rp1->clk_rate = clk_rate; > + > + chip->ops = &rp1_pwm_ops; > + chip->atomic = true; > + > + platform_set_drvdata(pdev, chip); > + > + ret = pwmchip_add(chip); > + if (ret) { > + dev_err_probe(dev, ret, "Failed to register PWM chip\n"); > + goto err_disable_clk; > + } > + > + ret = of_syscon_register_regmap(np, rp1->regmap); > + if (ret) { > + dev_err_probe(dev, ret, "Failed to register syscon\n"); > + goto err_remove_chip; > + } > + > + return 0; > + > +err_remove_chip: > + pwmchip_remove(chip); > +err_disable_clk: > + clk_disable_unprepare(rp1->clk); > + > + return ret; > +} > + > +static void rp1_pwm_remove(struct platform_device *pdev) > +{ > + struct pwm_chip *chip = platform_get_drvdata(pdev); > + struct rp1_pwm *rp1 = pwmchip_get_drvdata(chip); > + > + pwmchip_remove(chip); > + > + if (rp1->clk_enabled) { > + clk_disable_unprepare(rp1->clk); > + rp1->clk_enabled = false; > + } > +} > + > +static int rp1_pwm_suspend(struct device *dev) > +{ > + struct pwm_chip *chip = dev_get_drvdata(dev); > + struct rp1_pwm *rp1 = pwmchip_get_drvdata(chip); > + > + if (rp1->clk_enabled) { > + clk_disable_unprepare(rp1->clk); > + rp1->clk_enabled = false; > + } > + > + return 0; > +} > + > +static int rp1_pwm_resume(struct device *dev) > +{ > + struct pwm_chip *chip = dev_get_drvdata(dev); > + struct rp1_pwm *rp1 = pwmchip_get_drvdata(chip); > + int ret; > + > + ret = clk_prepare_enable(rp1->clk); > + if (ret) { > + dev_err(dev, "Failed to enable clock on resume: %pe\n", ERR_PTR(ret)); > + return ret; > + } > + > + rp1->clk_enabled = true; > + > + return 0; > +} > + > +static DEFINE_SIMPLE_DEV_PM_OPS(rp1_pwm_pm_ops, rp1_pwm_suspend, rp1_pwm_resume); > + > +static const struct of_device_id rp1_pwm_of_match[] = { > + { .compatible = "raspberrypi,rp1-pwm" }, > + { /* sentinel */ } > +}; > +MODULE_DEVICE_TABLE(of, rp1_pwm_of_match); > + > +static struct platform_driver rp1_pwm_driver = { > + .probe = rp1_pwm_probe, > + .remove = rp1_pwm_remove, > + .driver = { > + .name = "rp1-pwm", > + .of_match_table = rp1_pwm_of_match, > + .pm = pm_ptr(&rp1_pwm_pm_ops), > + .suppress_bind_attrs = true, > + }, > +}; > +builtin_platform_driver(rp1_pwm_driver); > + > +MODULE_DESCRIPTION("RP1 PWM driver"); > +MODULE_AUTHOR("Naushir Patuck <[email protected]>"); > +MODULE_AUTHOR("Andrea della Porta <[email protected]>"); > +MODULE_LICENSE("GPL");