diff mbox series

[4/7] soc: rockchip: add mfpwm driver

Message ID 20250408-rk3576-pwm-v1-4-a49286c2ca8e@collabora.com
State New
Headers show
Series Add Rockchip RK3576 PWM Support Through MFPWM | expand

Commit Message

Nicolas Frattaroli April 8, 2025, 12:32 p.m. UTC
With the Rockchip RK3576, the PWM IP used by Rockchip has changed
substantially. Looking at both the downstream pwm-rockchip driver as
well as the mainline pwm-rockchip driver made it clear that with all its
additional features and its differences from previous IP revisions, it
is best supported in a new driver.

This brings us to the question as to what such a new driver should be.
To me, it soon became clear that it should actually be several new
drivers, most prominently when Uwe Kleine-König let me know that I
should not implement the pwm subsystem's capture callback, but instead
write a counter driver for this functionality.

Combined with the other as-of-yet unimplemented functionality of this
new IP, it became apparent that it needs to be spread across several
subsystems.

For this reason, we add a new platform bus based driver, called mfpwm
(short for "Multi-function PWM"). This "parent" driver makes sure that
only one device function driver is using the device at a time, and is in
charge of registering the platform bus devices for the individual device
functions offered by the device.

An acquire/release pattern is used to guarantee that device function
drivers don't step on each other's toes.

Signed-off-by: Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
---
 MAINTAINERS                   |   2 +
 drivers/soc/rockchip/Kconfig  |  13 +
 drivers/soc/rockchip/Makefile |   1 +
 drivers/soc/rockchip/mfpwm.c  | 608 ++++++++++++++++++++++++++++++++++++++++++
 include/soc/rockchip/mfpwm.h  | 505 +++++++++++++++++++++++++++++++++++
 5 files changed, 1129 insertions(+)

Comments

Heiko Stübner April 8, 2025, 8:03 p.m. UTC | #1
Hi,

not a full review, just me making a first pass.

> +unsigned long mfpwm_clk_get_rate(struct rockchip_mfpwm *mfpwm)
> +{
> +	if (!mfpwm || !mfpwm->chosen_clk)
> +		return 0;
> +
> +	return clk_get_rate(mfpwm->chosen_clk);
> +}
> +EXPORT_SYMBOL_NS_GPL(mfpwm_clk_get_rate, "ROCKCHIP_MFPWM");

aren't you just re-implemeting a clk-mux with the whole chosen-clk
mechanism? See drivers/clk/clk-mux.c, so in theory you should be
able to just do a clk_register_mux(...) similar to for example
sound/soc/samsung/i2s.c .


> +
> +__attribute__((nonnull))
> +static int mfpwm_do_acquire(struct rockchip_mfpwm_func *pwmf)
> +{
> +	struct rockchip_mfpwm *mfpwm = pwmf->parent;
> +	unsigned int cnt;
> +
> +	if (mfpwm->active_func && pwmf->id != mfpwm->active_func->id)
> +		return -EBUSY;
> +
> +	if (!mfpwm->active_func)
> +		mfpwm->active_func = pwmf;
> +
> +	if (!check_add_overflow(mfpwm->acquire_cnt, 1, &cnt)) {
> +		mfpwm->acquire_cnt = cnt;
> +	} else {
> +		WARN(1, "prevented acquire counter overflow in %s\n", __func__);

dev_warn, as you have the mfpwm pointing to a pdev?

> +		return -EOVERFLOW;
> +	}
> +
> +	dev_dbg(&mfpwm->pdev->dev, "%d acquired mfpwm, acquires now at %u\n",
> +		pwmf->id, mfpwm->acquire_cnt);
> +
> +	return clk_enable(mfpwm->pclk);
> +}

> +/**
> + * mfpwm_get_clk_src - read the currently selected clock source
> + * @mfpwm: pointer to the driver's private &struct rockchip_mfpwm instance
> + *
> + * Read the device register to extract the currently selected clock source,
> + * and return it.
> + *
> + * Returns:
> + * * the numeric clock source ID on success, 0 <= id <= 2
> + * * negative errno on error
> + */
> +static int mfpwm_get_clk_src(struct rockchip_mfpwm *mfpwm)
> +{
> +	u32 val;
> +
> +	clk_enable(mfpwm->pclk);
> +	val = mfpwm_reg_read(mfpwm->base, PWMV4_REG_CLK_CTRL);
> +	clk_disable(mfpwm->pclk);
> +
> +	return (val & PWMV4_CLK_SRC_MASK) >> PWMV4_CLK_SRC_SHIFT;
> +}
> +
> +static int mfpwm_choose_clk(struct rockchip_mfpwm *mfpwm)
> +{
> +	int ret;
> +
> +	ret = mfpwm_get_clk_src(mfpwm);
> +	if (ret < 0) {
> +		dev_err(&mfpwm->pdev->dev, "couldn't get current clock source: %pe\n",
> +			ERR_PTR(ret));
> +		return ret;
> +	}
> +	if (ret == PWMV4_CLK_SRC_CRYSTAL) {
> +		if (mfpwm->osc_clk) {
> +			mfpwm->chosen_clk = mfpwm->osc_clk;
> +		} else {
> +			dev_warn(&mfpwm->pdev->dev, "initial state wanted 'osc' as clock source, but it's unavailable. Defaulting to 'pwm'.\n");
> +			mfpwm->chosen_clk = mfpwm->pwm_clk;
> +		}
> +	} else {
> +		mfpwm->chosen_clk = mfpwm->pwm_clk;
> +	}
> +
> +	return clk_rate_exclusive_get(mfpwm->chosen_clk);
> +}
>
> +/**
> + * mfpwm_switch_clk_src - switch between PWM clock sources
> + * @mfpwm: pointer to &struct rockchip_mfpwm driver data
> + * @clk_src: one of either %PWMV4_CLK_SRC_CRYSTAL or %PWMV4_CLK_SRC_PLL
> + *
> + * Switch between clock sources, ``_exclusive_put``ing the old rate,
> + * ``clk_rate_exclusive_get``ing the new one, writing the registers and
> + * swapping out the &struct_rockchip_mfpwm->chosen_clk.
> + *
> + * Returns:
> + * * %0        - Success
> + * * %-EINVAL  - A wrong @clk_src was given or it is unavailable
> + * * %-EBUSY   - Device is currently in use, try again later
> + */
> +__attribute__((nonnull))
> +static int mfpwm_switch_clk_src(struct rockchip_mfpwm *mfpwm,
> +					  unsigned int clk_src)
> +{
> +	struct clk *prev;
> +	int ret = 0;
> +
> +	scoped_cond_guard(spinlock_try, return -EBUSY, &mfpwm->state_lock) {
> +		/* Don't fiddle with any of this stuff if the PWM is on */
> +		if (mfpwm->active_func)
> +			return -EBUSY;
> +
> +		prev = mfpwm->chosen_clk;
> +		ret = mfpwm_get_clk_src(mfpwm);
> +		if (ret < 0)
> +			return ret;
> +		if (ret == clk_src)
> +			return 0;
> +
> +		switch (clk_src) {
> +		case PWMV4_CLK_SRC_PLL:
> +			mfpwm->chosen_clk = mfpwm->pwm_clk;
> +			break;
> +		case PWMV4_CLK_SRC_CRYSTAL:
> +			if (!mfpwm->osc_clk)
> +				return -EINVAL;
> +			mfpwm->chosen_clk = mfpwm->osc_clk;
> +			break;
> +		default:
> +			return -EINVAL;
> +		}
> +
> +		clk_enable(mfpwm->pclk);
> +
> +		mfpwm_reg_write(mfpwm->base, PWMV4_REG_CLK_CTRL,
> +				PWMV4_CLK_SRC(clk_src));
> +		clk_rate_exclusive_get(mfpwm->chosen_clk);
> +		if (prev)
> +			clk_rate_exclusive_put(prev);
> +
> +		clk_disable(mfpwm->pclk);
> +	}
> +
> +	return ret;
> +}

ok, the relevant part might be the 
	/* Don't fiddle with any of this stuff if the PWM is on */
thing, which will require special set_rate operation, but in general I
think, if it ticks like a clock, it probably should be a real clock ;-) .


> +static ssize_t chosen_clock_show(struct device *dev,
> +				 struct device_attribute *attr, char *buf)
> +{
> +	struct rockchip_mfpwm *mfpwm = dev_get_drvdata(dev);
> +	unsigned long clk_src = 0;
> +
> +	/*
> +	 * Why the weird indirection here? I have the suspicion that if we
> +	 * emitted to sysfs with the lock still held, then a nefarious program
> +	 * could hog the lock by somehow forcing a full buffer condition and
> +	 * then refusing to read from it. Don't know whether that's feasible
> +	 * to achieve in reality, but I don't want to find out the hard way
> +	 * either.
> +	 */
> +	scoped_guard(spinlock, &mfpwm->state_lock) {
> +		if (mfpwm->chosen_clk == mfpwm->pwm_clk)
> +			clk_src = PWMV4_CLK_SRC_PLL;
> +		else if (mfpwm->osc_clk && mfpwm->chosen_clk == mfpwm->osc_clk)
> +			clk_src = PWMV4_CLK_SRC_CRYSTAL;
> +		else
> +			return -ENODEV;
> +	}
> +
> +	if (clk_src == PWMV4_CLK_SRC_PLL)
> +		return sysfs_emit(buf, "pll\n");
> +	else if (clk_src == PWMV4_CLK_SRC_CRYSTAL)
> +		return sysfs_emit(buf, "crystal\n");
> +
> +	return -ENODEV;
> +}

which brings me to my main point of contention. Why does userspace
need to select a clock source for the driver via sysfs.

Neither the commit message nor the code does seem to explain that,
or I'm just blind - which is also a real possibility.

In general I really think, userspace should not need to care about if
a PLL or directly the oscillator is used a clock input.
I assume which is needed results from some runtime factor, so the
driver should be able to select the correct one?

A mux-clock could ust use clk_mux_determine_rate_flags() to select
the best parent depending on a requested rate instead.


> +static ssize_t chosen_clock_store(struct device *dev,
> +				  struct device_attribute *attr,
> +				  const char *buf, size_t count)
> +{
> +	struct rockchip_mfpwm *mfpwm = dev_get_drvdata(dev);
> +	int ret;
> +
> +	if (sysfs_streq(buf, "pll")) {
> +		ret = mfpwm_switch_clk_src(mfpwm, PWMV4_CLK_SRC_PLL);
> +		if (ret)
> +			return ret;
> +		return count;
> +	} else if (sysfs_streq(buf, "crystal")) {
> +		ret = mfpwm_switch_clk_src(mfpwm, PWMV4_CLK_SRC_CRYSTAL);
> +		if (ret)
> +			return ret;
> +		return count;
> +	} else {
> +		return -EINVAL;
> +	}
> +}
> +
> +static DEVICE_ATTR_RW(chosen_clock);
> +
> +static ssize_t available_clocks_show(struct device *dev,
> +				     struct device_attribute *attr, char *buf)
> +{
> +	struct rockchip_mfpwm *mfpwm = dev_get_drvdata(dev);
> +	ssize_t size = 0;
> +
> +	size += sysfs_emit_at(buf, size, "pll\n");
> +	if (mfpwm->osc_clk)
> +		size += sysfs_emit_at(buf, size, "crystal\n");
> +
> +	return size;
> +}
> +
> +static DEVICE_ATTR_RO(available_clocks);
> +
> +static struct attribute *mfpwm_attrs[] = {
> +	&dev_attr_available_clocks.attr,
> +	&dev_attr_chosen_clock.attr,
> +	NULL,
> +};

Not understanding the need for the sysfs stuff was my main point this
evening :-)

Heiko
Nicolas Frattaroli April 9, 2025, 1:01 p.m. UTC | #2
On Tuesday, 8 April 2025 22:03:01 Central European Summer Time Heiko Stübner wrote:
> Hi,
> 
> not a full review, just me making a first pass.
> 
> > +unsigned long mfpwm_clk_get_rate(struct rockchip_mfpwm *mfpwm)
> > +{
> > +	if (!mfpwm || !mfpwm->chosen_clk)
> > +		return 0;
> > +
> > +	return clk_get_rate(mfpwm->chosen_clk);
> > +}
> > +EXPORT_SYMBOL_NS_GPL(mfpwm_clk_get_rate, "ROCKCHIP_MFPWM");
> 
> aren't you just re-implemeting a clk-mux with the whole chosen-clk
> mechanism? See drivers/clk/clk-mux.c, so in theory you should be
> able to just do a clk_register_mux(...) similar to for example
> sound/soc/samsung/i2s.c .

Probably yes. I didn't know clk-mux was a thing. If I do decide to keep the
clock switching at all (more on that below), then I'll rewrite it around
clk-mux.

> > +
> > +__attribute__((nonnull))
> > +static int mfpwm_do_acquire(struct rockchip_mfpwm_func *pwmf)
> > +{
> > +	struct rockchip_mfpwm *mfpwm = pwmf->parent;
> > +	unsigned int cnt;
> > +
> > +	if (mfpwm->active_func && pwmf->id != mfpwm->active_func->id)
> > +		return -EBUSY;
> > +
> > +	if (!mfpwm->active_func)
> > +		mfpwm->active_func = pwmf;
> > +
> > +	if (!check_add_overflow(mfpwm->acquire_cnt, 1, &cnt)) {
> > +		mfpwm->acquire_cnt = cnt;
> > +	} else {
> > +		WARN(1, "prevented acquire counter overflow in %s\n", __func__);
> 
> dev_warn, as you have the mfpwm pointing to a pdev?

Will do.

> > +		return -EOVERFLOW;
> > +	}
> > +
> > +	dev_dbg(&mfpwm->pdev->dev, "%d acquired mfpwm, acquires now at %u\n",
> > +		pwmf->id, mfpwm->acquire_cnt);
> > +
> > +	return clk_enable(mfpwm->pclk);
> > +}
> 
> > +/**
> > + * mfpwm_get_clk_src - read the currently selected clock source
> > + * @mfpwm: pointer to the driver's private &struct rockchip_mfpwm instance
> > + *
> > + * Read the device register to extract the currently selected clock source,
> > + * and return it.
> > + *
> > + * Returns:
> > + * * the numeric clock source ID on success, 0 <= id <= 2
> > + * * negative errno on error
> > + */
> > +static int mfpwm_get_clk_src(struct rockchip_mfpwm *mfpwm)
> > +{
> > +	u32 val;
> > +
> > +	clk_enable(mfpwm->pclk);
> > +	val = mfpwm_reg_read(mfpwm->base, PWMV4_REG_CLK_CTRL);
> > +	clk_disable(mfpwm->pclk);
> > +
> > +	return (val & PWMV4_CLK_SRC_MASK) >> PWMV4_CLK_SRC_SHIFT;
> > +}
> > +
> > +static int mfpwm_choose_clk(struct rockchip_mfpwm *mfpwm)
> > +{
> > +	int ret;
> > +
> > +	ret = mfpwm_get_clk_src(mfpwm);
> > +	if (ret < 0) {
> > +		dev_err(&mfpwm->pdev->dev, "couldn't get current clock source: %pe\n",
> > +			ERR_PTR(ret));
> > +		return ret;
> > +	}
> > +	if (ret == PWMV4_CLK_SRC_CRYSTAL) {
> > +		if (mfpwm->osc_clk) {
> > +			mfpwm->chosen_clk = mfpwm->osc_clk;
> > +		} else {
> > +			dev_warn(&mfpwm->pdev->dev, "initial state wanted 'osc' as clock source, but it's unavailable. Defaulting to 'pwm'.\n");
> > +			mfpwm->chosen_clk = mfpwm->pwm_clk;
> > +		}
> > +	} else {
> > +		mfpwm->chosen_clk = mfpwm->pwm_clk;
> > +	}
> > +
> > +	return clk_rate_exclusive_get(mfpwm->chosen_clk);
> > +}
> >
> > +/**
> > + * mfpwm_switch_clk_src - switch between PWM clock sources
> > + * @mfpwm: pointer to &struct rockchip_mfpwm driver data
> > + * @clk_src: one of either %PWMV4_CLK_SRC_CRYSTAL or %PWMV4_CLK_SRC_PLL
> > + *
> > + * Switch between clock sources, ``_exclusive_put``ing the old rate,
> > + * ``clk_rate_exclusive_get``ing the new one, writing the registers and
> > + * swapping out the &struct_rockchip_mfpwm->chosen_clk.
> > + *
> > + * Returns:
> > + * * %0        - Success
> > + * * %-EINVAL  - A wrong @clk_src was given or it is unavailable
> > + * * %-EBUSY   - Device is currently in use, try again later
> > + */
> > +__attribute__((nonnull))
> > +static int mfpwm_switch_clk_src(struct rockchip_mfpwm *mfpwm,
> > +					  unsigned int clk_src)
> > +{
> > +	struct clk *prev;
> > +	int ret = 0;
> > +
> > +	scoped_cond_guard(spinlock_try, return -EBUSY, &mfpwm->state_lock) {
> > +		/* Don't fiddle with any of this stuff if the PWM is on */
> > +		if (mfpwm->active_func)
> > +			return -EBUSY;
> > +
> > +		prev = mfpwm->chosen_clk;
> > +		ret = mfpwm_get_clk_src(mfpwm);
> > +		if (ret < 0)
> > +			return ret;
> > +		if (ret == clk_src)
> > +			return 0;
> > +
> > +		switch (clk_src) {
> > +		case PWMV4_CLK_SRC_PLL:
> > +			mfpwm->chosen_clk = mfpwm->pwm_clk;
> > +			break;
> > +		case PWMV4_CLK_SRC_CRYSTAL:
> > +			if (!mfpwm->osc_clk)
> > +				return -EINVAL;
> > +			mfpwm->chosen_clk = mfpwm->osc_clk;
> > +			break;
> > +		default:
> > +			return -EINVAL;
> > +		}
> > +
> > +		clk_enable(mfpwm->pclk);
> > +
> > +		mfpwm_reg_write(mfpwm->base, PWMV4_REG_CLK_CTRL,
> > +				PWMV4_CLK_SRC(clk_src));
> > +		clk_rate_exclusive_get(mfpwm->chosen_clk);
> > +		if (prev)
> > +			clk_rate_exclusive_put(prev);
> > +
> > +		clk_disable(mfpwm->pclk);
> > +	}
> > +
> > +	return ret;
> > +}
> 
> ok, the relevant part might be the 
> 	/* Don't fiddle with any of this stuff if the PWM is on */
> thing, which will require special set_rate operation, but in general I
> think, if it ticks like a clock, it probably should be a real clock ;-) .

I agree; we can guarantee it doesn't get changed after all by just marking it
as exclusive instead of marking either pwm_clk or osc_clk as exclusive.

> > +static ssize_t chosen_clock_show(struct device *dev,
> > +				 struct device_attribute *attr, char *buf)
> > +{
> > +	struct rockchip_mfpwm *mfpwm = dev_get_drvdata(dev);
> > +	unsigned long clk_src = 0;
> > +
> > +	/*
> > +	 * Why the weird indirection here? I have the suspicion that if we
> > +	 * emitted to sysfs with the lock still held, then a nefarious program
> > +	 * could hog the lock by somehow forcing a full buffer condition and
> > +	 * then refusing to read from it. Don't know whether that's feasible
> > +	 * to achieve in reality, but I don't want to find out the hard way
> > +	 * either.
> > +	 */
> > +	scoped_guard(spinlock, &mfpwm->state_lock) {
> > +		if (mfpwm->chosen_clk == mfpwm->pwm_clk)
> > +			clk_src = PWMV4_CLK_SRC_PLL;
> > +		else if (mfpwm->osc_clk && mfpwm->chosen_clk == mfpwm->osc_clk)
> > +			clk_src = PWMV4_CLK_SRC_CRYSTAL;
> > +		else
> > +			return -ENODEV;
> > +	}
> > +
> > +	if (clk_src == PWMV4_CLK_SRC_PLL)
> > +		return sysfs_emit(buf, "pll\n");
> > +	else if (clk_src == PWMV4_CLK_SRC_CRYSTAL)
> > +		return sysfs_emit(buf, "crystal\n");
> > +
> > +	return -ENODEV;
> > +}
> 
> which brings me to my main point of contention. Why does userspace
> need to select a clock source for the driver via sysfs.

It doesn't need to. Basically, this is a weird hardware feature. Downstream did
not bother implementing it at all, and I found out through the TRM's register
listing and thought "that's weird, I wonder if it even works", and lo and behold
it does. At that point, like two rewrites ago, I was committed to ensuring that
the driver can handle this edge case of the PWM clock being changed. As I lacked
the imagination as to why someone would change it and the knowledge as to which
kernel interfaces exist to change it, sysfs offered itself as a natural dumping
ground for switches that probably shouldn't exist.

> Neither the commit message nor the code does seem to explain that,
> or I'm just blind - which is also a real possibility.
> 
> In general I really think, userspace should not need to care about if
> a PLL or directly the oscillator is used a clock input.
> I assume which is needed results from some runtime factor, so the
> driver should be able to select the correct one?
> 
> A mux-clock could ust use clk_mux_determine_rate_flags() to select
> the best parent depending on a requested rate instead.

Yeah, the only use-case I can come up with is that we really want to use an
either 100 MHz or 50 MHz clock on one chip, but have a channel hit a precise
timing with the 24 MHz clock on the same chip. If the fixed crystal oscillator
were 25 MHz instead of 24 MHz, this would be entirely pointless, as they're all
multiples of it.

Thanks for the hint about clk_mux_determine_rate_flags, it doesn't appear to be
documented (classic) but it looks to do at least half of what a proper solution
would need to do. The other half is figuring out what ideal target rate we
actually want to optimise for for a given e.g. waveform consisting of period
and duty cycle in nanoseconds. There's some logic to think about regarding where
rounding errors are acceptable, e.g. a long period with a low duty cycle is
probably better off using the 100-50-24 mux with 100 MHz as the rate. I'm not
sure if 50 MHz is ever a sensible option since it is a dividend of 100 MHz, and
I'm not about to reason about imagined power draw of the PWM hardware without
laboratory grade test equipment.

For what it's worth, this is a niche enough hardware feature that if it causes
too much friction getting it supported in a driver, I'll just drop it entirely
instead. I tried to preemptively combat technical debt by supporting this in
some way, but instead managed to introduce scope creep.

One option is to always just choose the PLL muxed clock and then always set it
to 100 MHz, because it's probably the best option unless there are specific
PWM-based applications that make heavy use of 24-derived timings (maybe the IR
stuff?)

> 
> > +static ssize_t chosen_clock_store(struct device *dev,
> > +				  struct device_attribute *attr,
> > +				  const char *buf, size_t count)
> > +{
> > +	struct rockchip_mfpwm *mfpwm = dev_get_drvdata(dev);
> > +	int ret;
> > +
> > +	if (sysfs_streq(buf, "pll")) {
> > +		ret = mfpwm_switch_clk_src(mfpwm, PWMV4_CLK_SRC_PLL);
> > +		if (ret)
> > +			return ret;
> > +		return count;
> > +	} else if (sysfs_streq(buf, "crystal")) {
> > +		ret = mfpwm_switch_clk_src(mfpwm, PWMV4_CLK_SRC_CRYSTAL);
> > +		if (ret)
> > +			return ret;
> > +		return count;
> > +	} else {
> > +		return -EINVAL;
> > +	}
> > +}
> > +
> > +static DEVICE_ATTR_RW(chosen_clock);
> > +
> > +static ssize_t available_clocks_show(struct device *dev,
> > +				     struct device_attribute *attr, char *buf)
> > +{
> > +	struct rockchip_mfpwm *mfpwm = dev_get_drvdata(dev);
> > +	ssize_t size = 0;
> > +
> > +	size += sysfs_emit_at(buf, size, "pll\n");
> > +	if (mfpwm->osc_clk)
> > +		size += sysfs_emit_at(buf, size, "crystal\n");
> > +
> > +	return size;
> > +}
> > +
> > +static DEVICE_ATTR_RO(available_clocks);
> > +
> > +static struct attribute *mfpwm_attrs[] = {
> > +	&dev_attr_available_clocks.attr,
> > +	&dev_attr_chosen_clock.attr,
> > +	NULL,
> > +};
> 
> Not understanding the need for the sysfs stuff was my main point this
> evening :-)
> 
> Heiko
> 

Thank you for your quick preliminary review! This already gives me some good
points to look into for a v2.

Kind regards,
Nicolas Frattaroli
diff mbox series

Patch

diff --git a/MAINTAINERS b/MAINTAINERS
index 407179d2a90dd49800f2bb5770a1280c5afebb5a..e6a9347be1e7889089e1d9e655cb23c2d8399b40 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -20891,6 +20891,8 @@  L:	linux-rockchip@lists.infradead.org
 L:	linux-pwm@vger.kernel.org
 S:	Maintained
 F:	Documentation/devicetree/bindings/pwm/rockchip,rk3576-pwm.yaml
+F:	drivers/soc/rockchip/mfpwm.c
+F:	include/soc/rockchip/mfpwm.h
 
 ROCKCHIP RK3568 RANDOM NUMBER GENERATOR SUPPORT
 M:	Daniel Golle <daniel@makrotopia.org>
diff --git a/drivers/soc/rockchip/Kconfig b/drivers/soc/rockchip/Kconfig
index 785f60c6f3ad1a09f517e69a69726a8178bed168..4e1e4926c514a5a2c4d4caf8cf9809a098badc7d 100644
--- a/drivers/soc/rockchip/Kconfig
+++ b/drivers/soc/rockchip/Kconfig
@@ -30,4 +30,17 @@  config ROCKCHIP_DTPM
 	  on this platform. That will create all the power capping capable
 	  devices.
 
+config ROCKCHIP_MFPWM
+	tristate "Rockchip multi-function PWM controller"
+	depends on OF
+	depends on HAS_IOMEM
+	help
+	  Some Rockchip SoCs, such as the RK3576, use a PWM controller that has
+	  several different functions, such as generating PWM waveforms but also
+	  counting waveforms.
+
+	  This driver manages the overall device, and selects between different
+	  functionalities at runtime as needed, with drivers for them
+	  implemented in their respective subsystems.
+
 endif
diff --git a/drivers/soc/rockchip/Makefile b/drivers/soc/rockchip/Makefile
index 23d414433c8c58557effc214337ec8e6ff17a461..ba12dbd01ac794910d9407c268e89071cd2b3139 100644
--- a/drivers/soc/rockchip/Makefile
+++ b/drivers/soc/rockchip/Makefile
@@ -5,3 +5,4 @@ 
 obj-$(CONFIG_ROCKCHIP_GRF) += grf.o
 obj-$(CONFIG_ROCKCHIP_IODOMAIN) += io-domain.o
 obj-$(CONFIG_ROCKCHIP_DTPM) += dtpm.o
+obj-$(CONFIG_ROCKCHIP_MFPWM) += mfpwm.o
diff --git a/drivers/soc/rockchip/mfpwm.c b/drivers/soc/rockchip/mfpwm.c
new file mode 100644
index 0000000000000000000000000000000000000000..9331c530f0581573e2b74f62a6622b8625c5b2c5
--- /dev/null
+++ b/drivers/soc/rockchip/mfpwm.c
@@ -0,0 +1,608 @@ 
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2025 Collabora Ltd.
+ *
+ * A driver to manage all the different functionalities exposed by Rockchip's
+ * PWMv4 hardware.
+ *
+ * This driver is chiefly focused on guaranteeing non-concurrent operation
+ * between the different device functions, as well as setting the clocks.
+ * It registers the device function platform devices, e.g. PWM output or
+ * PWM capture.
+ *
+ * Authors:
+ *     Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
+ */
+
+#include <linux/clk.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/overflow.h>
+#include <linux/platform_device.h>
+#include <linux/spinlock.h>
+#include <soc/rockchip/mfpwm.h>
+
+/**
+ * struct rockchip_mfpwm - private mfpwm driver instance state struct
+ * @pdev: pointer to this instance's &struct platform_device
+ * @base: pointer to the memory mapped registers of this device
+ * @pwm_clk: pointer to the PLL clock the PWM signal may be derived from
+ * @osc_clk: pointer to the fixed crystal the PWM signal may be derived from
+ * @chosen_clk: is one of either @pwm_clk or @osc_clk, depending on choice.
+ *              May only be swapped out while holding @state_lock.
+ * @pclk: pointer to the APB bus clock needed for mmio register access
+ * @pwm_dev: pointer to the &struct platform_device of the pwm output driver
+ * @counter_dev: pointer to the &struct platform_device of the counter driver
+ * @active_func: pointer to the currently active device function, or %NULL if no
+ *              device function is currently actively using any of the shared
+ *              resources. May only be checked/modified with @state_lock held.
+ * @acquire_cnt: number of times @active_func has currently mfpwm_acquire()'d
+ *               it. Must only be checked or modified while holding @state_lock.
+ * @pwmclk_enable_cnt: number of times @active_func has enabled the pwmclk sans
+ *                     disabling it. Must only be checked or modified while
+ *                     holding @state_lock. Only exists to fix a splat on mfpwm
+ *                     driver remove.
+ * @state_lock: this lock is held while either the active device function, the
+ *              enable register, or the chosen clock is being changed.
+ * @irq: the IRQ number of this device
+ */
+struct rockchip_mfpwm {
+	struct platform_device *pdev;
+	void __iomem *base;
+	struct clk *pwm_clk;
+	struct clk *osc_clk;
+	struct clk *chosen_clk;
+	struct clk *pclk;
+	struct platform_device *pwm_dev;
+	struct platform_device *counter_dev;
+	struct rockchip_mfpwm_func *active_func;
+	unsigned int acquire_cnt;
+	unsigned int pwmclk_enable_cnt;
+	spinlock_t state_lock;
+	int irq;
+};
+
+static atomic_t subdev_id = ATOMIC_INIT(0);
+
+static inline struct rockchip_mfpwm *to_rockchip_mfpwm(struct platform_device *pdev)
+{
+	return platform_get_drvdata(pdev);
+}
+
+unsigned long mfpwm_clk_get_rate(struct rockchip_mfpwm *mfpwm)
+{
+	if (!mfpwm || !mfpwm->chosen_clk)
+		return 0;
+
+	return clk_get_rate(mfpwm->chosen_clk);
+}
+EXPORT_SYMBOL_NS_GPL(mfpwm_clk_get_rate, "ROCKCHIP_MFPWM");
+
+static int mfpwm_check_pwmf(const struct rockchip_mfpwm_func *pwmf,
+			    const char *fname)
+{
+	if (IS_ERR_OR_NULL(pwmf)) {
+		WARN(1, "called %s with an erroneous handle, no effect\n",
+		     fname);
+		return -EINVAL;
+	}
+
+	if (IS_ERR_OR_NULL(pwmf->parent)) {
+		WARN(1, "called %s with an erroneous mfpwm_func parent, no effect\n",
+		     fname);
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+__attribute__((nonnull))
+static bool mfpwm_pwmf_is_active_pwmf(const struct rockchip_mfpwm_func *pwmf)
+{
+	if (pwmf->parent->active_func) {
+		if (pwmf->parent->active_func->id == pwmf->id)
+			return true;
+	}
+
+	return false;
+}
+
+int mfpwm_pwmclk_enable(struct rockchip_mfpwm_func *pwmf)
+{
+	unsigned long flags;
+	int ret;
+
+	ret = mfpwm_check_pwmf(pwmf, "mfpwm_pwmclk_enable");
+	if (ret)
+		return ret;
+
+	spin_lock_irqsave(&pwmf->parent->state_lock, flags);
+	if (mfpwm_pwmf_is_active_pwmf(pwmf)) {
+		ret = clk_enable(pwmf->parent->chosen_clk);
+		pwmf->parent->pwmclk_enable_cnt++;
+	} else {
+		ret = -EBUSY;
+	}
+
+	spin_unlock_irqrestore(&pwmf->parent->state_lock, flags);
+
+	return ret;
+}
+EXPORT_SYMBOL_NS_GPL(mfpwm_pwmclk_enable, "ROCKCHIP_MFPWM");
+
+void mfpwm_pwmclk_disable(struct rockchip_mfpwm_func *pwmf)
+{
+	unsigned long flags;
+
+	if (mfpwm_check_pwmf(pwmf, "mfpwm_pwmclk_enable"))
+		return;
+
+	spin_lock_irqsave(&pwmf->parent->state_lock, flags);
+	if (mfpwm_pwmf_is_active_pwmf(pwmf)) {
+		clk_disable(pwmf->parent->chosen_clk);
+		pwmf->parent->pwmclk_enable_cnt--;
+	}
+	spin_unlock_irqrestore(&pwmf->parent->state_lock, flags);
+}
+EXPORT_SYMBOL_NS_GPL(mfpwm_pwmclk_disable, "ROCKCHIP_MFPWM");
+
+__attribute__((nonnull))
+static int mfpwm_do_acquire(struct rockchip_mfpwm_func *pwmf)
+{
+	struct rockchip_mfpwm *mfpwm = pwmf->parent;
+	unsigned int cnt;
+
+	if (mfpwm->active_func && pwmf->id != mfpwm->active_func->id)
+		return -EBUSY;
+
+	if (!mfpwm->active_func)
+		mfpwm->active_func = pwmf;
+
+	if (!check_add_overflow(mfpwm->acquire_cnt, 1, &cnt)) {
+		mfpwm->acquire_cnt = cnt;
+	} else {
+		WARN(1, "prevented acquire counter overflow in %s\n", __func__);
+		return -EOVERFLOW;
+	}
+
+	dev_dbg(&mfpwm->pdev->dev, "%d acquired mfpwm, acquires now at %u\n",
+		pwmf->id, mfpwm->acquire_cnt);
+
+	return clk_enable(mfpwm->pclk);
+}
+
+int mfpwm_acquire(struct rockchip_mfpwm_func *pwmf)
+{
+	struct rockchip_mfpwm *mfpwm;
+	unsigned long flags;
+	int ret = 0;
+
+	ret = mfpwm_check_pwmf(pwmf, "mfpwm_acquire");
+	if (ret)
+		return ret;
+
+	mfpwm = pwmf->parent;
+	dev_dbg(&mfpwm->pdev->dev, "%d is attempting to acquire\n", pwmf->id);
+
+	if (!spin_trylock_irqsave(&mfpwm->state_lock, flags))
+		return -EBUSY;
+
+	ret = mfpwm_do_acquire(pwmf);
+
+	spin_unlock_irqrestore(&mfpwm->state_lock, flags);
+
+	return ret;
+}
+EXPORT_SYMBOL_NS_GPL(mfpwm_acquire, "ROCKCHIP_MFPWM");
+
+__attribute__((nonnull))
+static void mfpwm_do_release(const struct rockchip_mfpwm_func *pwmf)
+{
+	struct rockchip_mfpwm *mfpwm = pwmf->parent;
+
+	if (!mfpwm->active_func)
+		return;
+
+	if (mfpwm->active_func->id != pwmf->id)
+		return;
+
+	/*
+	 * No need to check_sub_overflow here, !mfpwm->active_func above catches
+	 * this type of problem already.
+	 */
+	mfpwm->acquire_cnt--;
+
+	if (!mfpwm->acquire_cnt)
+		mfpwm->active_func = NULL;
+
+	clk_disable(mfpwm->pclk);
+}
+
+void mfpwm_release(const struct rockchip_mfpwm_func *pwmf)
+{
+	struct rockchip_mfpwm *mfpwm;
+	unsigned long flags;
+
+	if (mfpwm_check_pwmf(pwmf, "mfpwm_release"))
+		return;
+
+	mfpwm = pwmf->parent;
+
+	spin_lock_irqsave(&mfpwm->state_lock, flags);
+	mfpwm_do_release(pwmf);
+	dev_dbg(&mfpwm->pdev->dev, "%d released mfpwm, acquires now at %u\n",
+		pwmf->id, mfpwm->acquire_cnt);
+	spin_unlock_irqrestore(&mfpwm->state_lock, flags);
+}
+EXPORT_SYMBOL_NS_GPL(mfpwm_release, "ROCKCHIP_MFPWM");
+
+void mfpwm_remove_func(struct rockchip_mfpwm_func *pwmf)
+{
+	struct rockchip_mfpwm *mfpwm;
+	unsigned long flags;
+
+	if (mfpwm_check_pwmf(pwmf, "mfpwm_remove_func"))
+		return;
+
+	mfpwm = pwmf->parent;
+	spin_lock_irqsave(&mfpwm->state_lock, flags);
+
+	if (mfpwm_pwmf_is_active_pwmf(pwmf)) {
+		dev_dbg(&mfpwm->pdev->dev, "removing active function %d\n",
+			pwmf->id);
+
+		while (mfpwm->acquire_cnt > 0)
+			mfpwm_do_release(pwmf);
+		for (; mfpwm->pwmclk_enable_cnt > 0; mfpwm->pwmclk_enable_cnt--)
+			clk_disable(mfpwm->chosen_clk);
+
+		mfpwm_reg_write(mfpwm->base, PWMV4_REG_ENABLE,
+				PWMV4_EN(false) | PWMV4_CLK_EN(false));
+	}
+
+	if (mfpwm->pwm_dev && mfpwm->pwm_dev->id == pwmf->id) {
+		dev_dbg(&mfpwm->pdev->dev, "clearing pwm_dev pointer\n");
+		mfpwm->pwm_dev = NULL;
+	} else if (mfpwm->counter_dev && mfpwm->counter_dev->id == pwmf->id) {
+		dev_dbg(&mfpwm->pdev->dev, "clearing counter_dev pointer\n");
+		mfpwm->counter_dev = NULL;
+	} else {
+		WARN(1, "trying to remove an unknown mfpwm device function");
+	}
+
+	spin_unlock_irqrestore(&mfpwm->state_lock, flags);
+}
+EXPORT_SYMBOL_NS_GPL(mfpwm_remove_func, "ROCKCHIP_MFPWM");
+
+/**
+ * mfpwm_register_subdev - register a single mfpwm_func
+ * @mfpwm: pointer to the parent &struct rockchip_mfpwm
+ * @target: pointer to where the &struct platform_device pointer should be
+ *          stored, usually a member of @mfpwm
+ * @name: sub-device name string
+ *
+ * Allocate a single &struct mfpwm_func, fill its members with appropriate data,
+ * and register a new platform device, saving its pointer to @target. The
+ * allocation is devres tracked, so will be automatically freed on mfpwm remove.
+ *
+ * Returns: 0 on success, negative errno on error
+ */
+static int mfpwm_register_subdev(struct rockchip_mfpwm *mfpwm,
+				 struct platform_device **target,
+				 const char *name)
+{
+	struct rockchip_mfpwm_func *func;
+	struct platform_device *child;
+
+	func = devm_kzalloc(&mfpwm->pdev->dev, sizeof(*func), GFP_KERNEL);
+	if (IS_ERR(func))
+		return PTR_ERR(func);
+	func->irq = mfpwm->irq;
+	func->parent = mfpwm;
+	func->id = atomic_inc_return(&subdev_id);
+	func->base = mfpwm->base;
+	child = platform_device_register_data(&mfpwm->pdev->dev, name, func->id,
+					      func, sizeof(*func));
+
+	if (IS_ERR(child))
+		return PTR_ERR(child);
+
+	*target = child;
+
+	return 0;
+}
+
+static int mfpwm_register_subdevs(struct rockchip_mfpwm *mfpwm)
+{
+	int ret;
+
+	ret = mfpwm_register_subdev(mfpwm, &mfpwm->pwm_dev, "pwm-rockchip-v4");
+	if (ret)
+		return ret;
+
+	ret = mfpwm_register_subdev(mfpwm, &mfpwm->counter_dev,
+				    "rockchip-pwm-capture");
+	if (ret)
+		goto err_unreg_pwm_dev;
+
+	return 0;
+
+err_unreg_pwm_dev:
+	platform_device_unregister(mfpwm->pwm_dev);
+
+	return ret;
+}
+
+/**
+ * mfpwm_get_clk_src - read the currently selected clock source
+ * @mfpwm: pointer to the driver's private &struct rockchip_mfpwm instance
+ *
+ * Read the device register to extract the currently selected clock source,
+ * and return it.
+ *
+ * Returns:
+ * * the numeric clock source ID on success, 0 <= id <= 2
+ * * negative errno on error
+ */
+static int mfpwm_get_clk_src(struct rockchip_mfpwm *mfpwm)
+{
+	u32 val;
+
+	clk_enable(mfpwm->pclk);
+	val = mfpwm_reg_read(mfpwm->base, PWMV4_REG_CLK_CTRL);
+	clk_disable(mfpwm->pclk);
+
+	return (val & PWMV4_CLK_SRC_MASK) >> PWMV4_CLK_SRC_SHIFT;
+}
+
+static int mfpwm_choose_clk(struct rockchip_mfpwm *mfpwm)
+{
+	int ret;
+
+	ret = mfpwm_get_clk_src(mfpwm);
+	if (ret < 0) {
+		dev_err(&mfpwm->pdev->dev, "couldn't get current clock source: %pe\n",
+			ERR_PTR(ret));
+		return ret;
+	}
+	if (ret == PWMV4_CLK_SRC_CRYSTAL) {
+		if (mfpwm->osc_clk) {
+			mfpwm->chosen_clk = mfpwm->osc_clk;
+		} else {
+			dev_warn(&mfpwm->pdev->dev, "initial state wanted 'osc' as clock source, but it's unavailable. Defaulting to 'pwm'.\n");
+			mfpwm->chosen_clk = mfpwm->pwm_clk;
+		}
+	} else {
+		mfpwm->chosen_clk = mfpwm->pwm_clk;
+	}
+
+	return clk_rate_exclusive_get(mfpwm->chosen_clk);
+}
+
+/**
+ * mfpwm_switch_clk_src - switch between PWM clock sources
+ * @mfpwm: pointer to &struct rockchip_mfpwm driver data
+ * @clk_src: one of either %PWMV4_CLK_SRC_CRYSTAL or %PWMV4_CLK_SRC_PLL
+ *
+ * Switch between clock sources, ``_exclusive_put``ing the old rate,
+ * ``clk_rate_exclusive_get``ing the new one, writing the registers and
+ * swapping out the &struct_rockchip_mfpwm->chosen_clk.
+ *
+ * Returns:
+ * * %0        - Success
+ * * %-EINVAL  - A wrong @clk_src was given or it is unavailable
+ * * %-EBUSY   - Device is currently in use, try again later
+ */
+__attribute__((nonnull))
+static int mfpwm_switch_clk_src(struct rockchip_mfpwm *mfpwm,
+					  unsigned int clk_src)
+{
+	struct clk *prev;
+	int ret = 0;
+
+	scoped_cond_guard(spinlock_try, return -EBUSY, &mfpwm->state_lock) {
+		/* Don't fiddle with any of this stuff if the PWM is on */
+		if (mfpwm->active_func)
+			return -EBUSY;
+
+		prev = mfpwm->chosen_clk;
+		ret = mfpwm_get_clk_src(mfpwm);
+		if (ret < 0)
+			return ret;
+		if (ret == clk_src)
+			return 0;
+
+		switch (clk_src) {
+		case PWMV4_CLK_SRC_PLL:
+			mfpwm->chosen_clk = mfpwm->pwm_clk;
+			break;
+		case PWMV4_CLK_SRC_CRYSTAL:
+			if (!mfpwm->osc_clk)
+				return -EINVAL;
+			mfpwm->chosen_clk = mfpwm->osc_clk;
+			break;
+		default:
+			return -EINVAL;
+		}
+
+		clk_enable(mfpwm->pclk);
+
+		mfpwm_reg_write(mfpwm->base, PWMV4_REG_CLK_CTRL,
+				PWMV4_CLK_SRC(clk_src));
+		clk_rate_exclusive_get(mfpwm->chosen_clk);
+		if (prev)
+			clk_rate_exclusive_put(prev);
+
+		clk_disable(mfpwm->pclk);
+	}
+
+	return ret;
+}
+
+static ssize_t chosen_clock_show(struct device *dev,
+				 struct device_attribute *attr, char *buf)
+{
+	struct rockchip_mfpwm *mfpwm = dev_get_drvdata(dev);
+	unsigned long clk_src = 0;
+
+	/*
+	 * Why the weird indirection here? I have the suspicion that if we
+	 * emitted to sysfs with the lock still held, then a nefarious program
+	 * could hog the lock by somehow forcing a full buffer condition and
+	 * then refusing to read from it. Don't know whether that's feasible
+	 * to achieve in reality, but I don't want to find out the hard way
+	 * either.
+	 */
+	scoped_guard(spinlock, &mfpwm->state_lock) {
+		if (mfpwm->chosen_clk == mfpwm->pwm_clk)
+			clk_src = PWMV4_CLK_SRC_PLL;
+		else if (mfpwm->osc_clk && mfpwm->chosen_clk == mfpwm->osc_clk)
+			clk_src = PWMV4_CLK_SRC_CRYSTAL;
+		else
+			return -ENODEV;
+	}
+
+	if (clk_src == PWMV4_CLK_SRC_PLL)
+		return sysfs_emit(buf, "pll\n");
+	else if (clk_src == PWMV4_CLK_SRC_CRYSTAL)
+		return sysfs_emit(buf, "crystal\n");
+
+	return -ENODEV;
+}
+
+static ssize_t chosen_clock_store(struct device *dev,
+				  struct device_attribute *attr,
+				  const char *buf, size_t count)
+{
+	struct rockchip_mfpwm *mfpwm = dev_get_drvdata(dev);
+	int ret;
+
+	if (sysfs_streq(buf, "pll")) {
+		ret = mfpwm_switch_clk_src(mfpwm, PWMV4_CLK_SRC_PLL);
+		if (ret)
+			return ret;
+		return count;
+	} else if (sysfs_streq(buf, "crystal")) {
+		ret = mfpwm_switch_clk_src(mfpwm, PWMV4_CLK_SRC_CRYSTAL);
+		if (ret)
+			return ret;
+		return count;
+	} else {
+		return -EINVAL;
+	}
+}
+
+static DEVICE_ATTR_RW(chosen_clock);
+
+static ssize_t available_clocks_show(struct device *dev,
+				     struct device_attribute *attr, char *buf)
+{
+	struct rockchip_mfpwm *mfpwm = dev_get_drvdata(dev);
+	ssize_t size = 0;
+
+	size += sysfs_emit_at(buf, size, "pll\n");
+	if (mfpwm->osc_clk)
+		size += sysfs_emit_at(buf, size, "crystal\n");
+
+	return size;
+}
+
+static DEVICE_ATTR_RO(available_clocks);
+
+static struct attribute *mfpwm_attrs[] = {
+	&dev_attr_available_clocks.attr,
+	&dev_attr_chosen_clock.attr,
+	NULL,
+};
+
+ATTRIBUTE_GROUPS(mfpwm);
+
+static int rockchip_mfpwm_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct rockchip_mfpwm *mfpwm;
+	int ret = 0;
+
+	mfpwm = devm_kzalloc(&pdev->dev, sizeof(*mfpwm), GFP_KERNEL);
+	if (IS_ERR(mfpwm))
+		return PTR_ERR(mfpwm);
+
+	mfpwm->pdev = pdev;
+
+	spin_lock_init(&mfpwm->state_lock);
+
+	mfpwm->base = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(mfpwm->base))
+		return dev_err_probe(dev, PTR_ERR(mfpwm->base),
+				     "failed to ioremap address\n");
+
+	mfpwm->pclk = devm_clk_get_prepared(dev, "pclk");
+	if (IS_ERR(mfpwm->pclk))
+		return dev_err_probe(dev, PTR_ERR(mfpwm->pclk),
+				     "couldn't get and prepare 'pclk' clock\n");
+
+	mfpwm->irq = platform_get_irq(pdev, 0);
+	if (mfpwm->irq < 0)
+		return dev_err_probe(dev, mfpwm->irq, "couldn't get irq 0\n");
+
+	mfpwm->pwm_clk = devm_clk_get_prepared(dev, "pwm");
+	if (IS_ERR(mfpwm->pwm_clk))
+		return dev_err_probe(dev, PTR_ERR(mfpwm->pwm_clk),
+				     "couldn't get and prepare 'pwm' clock\n");
+
+	mfpwm->osc_clk = devm_clk_get_optional_prepared(dev, "osc");
+	if (IS_ERR(mfpwm->osc_clk))
+		return dev_err_probe(dev, PTR_ERR(mfpwm->osc_clk),
+				     "couldn't get and prepare 'osc' clock\n");
+
+	ret = mfpwm_choose_clk(mfpwm);
+	if (ret)
+		return ret;
+
+	platform_set_drvdata(pdev, mfpwm);
+
+	ret = mfpwm_register_subdevs(mfpwm);
+	if (ret) {
+		dev_err(dev, "failed to register sub-devices: %pe\n",
+			ERR_PTR(ret));
+		return ret;
+	}
+
+	return ret;
+}
+
+static void rockchip_mfpwm_remove(struct platform_device *pdev)
+{
+	struct rockchip_mfpwm *mfpwm = to_rockchip_mfpwm(pdev);
+	unsigned long flags;
+
+	spin_lock_irqsave(&mfpwm->state_lock, flags);
+
+	if (mfpwm->chosen_clk)
+		clk_rate_exclusive_put(mfpwm->chosen_clk);
+
+	spin_unlock_irqrestore(&mfpwm->state_lock, flags);
+}
+
+static const struct of_device_id rockchip_mfpwm_of_match[] = {
+	{
+		.compatible = "rockchip,rk3576-pwm",
+	},
+	{ /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, rockchip_mfpwm_of_match);
+
+static struct platform_driver rockchip_mfpwm_driver = {
+	.driver = {
+		.name = KBUILD_MODNAME,
+		.of_match_table = rockchip_mfpwm_of_match,
+		.dev_groups = mfpwm_groups,
+	},
+	.probe = rockchip_mfpwm_probe,
+	.remove = rockchip_mfpwm_remove,
+};
+module_platform_driver(rockchip_mfpwm_driver);
+
+MODULE_AUTHOR("Nicolas Frattaroli <nicolas.frattaroli@collabora.com>");
+MODULE_DESCRIPTION("Rockchip MFPWM Driver");
+MODULE_LICENSE("GPL");
diff --git a/include/soc/rockchip/mfpwm.h b/include/soc/rockchip/mfpwm.h
new file mode 100644
index 0000000000000000000000000000000000000000..345f13f438b57159a15cb2e0ae250800fb96ed43
--- /dev/null
+++ b/include/soc/rockchip/mfpwm.h
@@ -0,0 +1,505 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (c) 2025 Collabora Ltd.
+ *
+ * Common header file for all the Rockchip Multi-function PWM controller
+ * drivers that are spread across subsystems.
+ *
+ * Authors:
+ *     Nicolas Frattaroli <nicolas.frattaroli@collabora.com>
+ */
+
+#ifndef __SOC_ROCKCHIP_MFPWM_H__
+#define __SOC_ROCKCHIP_MFPWM_H__
+
+#include <linux/clk.h>
+#include <linux/io.h>
+#include <linux/spinlock.h>
+#include <soc/rockchip/utils.h>
+
+struct rockchip_mfpwm;
+
+/**
+ * struct rockchip_mfpwm_func - struct representing a single function driver
+ *
+ * @id: unique id for this function driver instance
+ * @base: pointer to start of MMIO registers
+ * @parent: a pointer to the parent mfpwm struct
+ * @irq: the shared IRQ gotten from the parent mfpwm device
+ */
+struct rockchip_mfpwm_func {
+	int id;
+	void __iomem *base;
+	struct rockchip_mfpwm *parent;
+	int irq;
+};
+
+/*
+ * PWMV4 Register Definitions
+ * --------------------------
+ *
+ * Attributes:
+ *  RW  - Read-Write
+ *  RO  - Read-Only
+ *  WO  - Write-Only
+ *  W1T - Write high, Self-clearing
+ *  W1C - Write high to clear interrupt
+ *
+ * Bit ranges to be understood with Verilog-like semantics,
+ * e.g. [03:00] is 4 bits: 0, 1, 2 and 3.
+ *
+ * All registers must be accessed with 32-bit width accesses only
+ */
+
+#define PWMV4_REG_VERSION		0x000
+/*
+ * VERSION Register Description
+ * [31:24] RO  | Hardware Major Version
+ * [23:16] RO  | Hardware Minor Version			docArrive
+ * [15:15] RO  | Reserved
+ * [14:14] RO  | Hardware supports biphasic counters
+ * [13:13] RO  | Hardware supports filters
+ * [12:12] RO  | Hardware supports waveform generation
+ * [11:11] RO  | Hardware supports counter
+ * [10:10] RO  | Hardware supports frequency metering
+ * [09:09] RO  | Hardware supports power key functionality
+ * [08:08] RO  | Hardware supports infrared transmissions
+ * [07:04] RO  | Channel index of this instance
+ * [03:00] RO  | Number of channels the base instance supports
+ */
+static inline __pure u32 pwmv4_ver_chn_num(u32 val)
+{
+	return (val & GENMASK(3, 0));
+}
+
+static inline __pure u32 pwmv4_ver_chn_idx(u32 val)
+{
+	return (val & GENMASK(7, 4)) >> 4;
+}
+
+static inline __pure u32 pwmv4_ver_major(u32 val)
+{
+	return (val & GENMASK(31, 24)) >> 24;
+}
+
+static inline __pure u32 pwmv4_ver_minor(u32 val)
+{
+	return (val & GENMASK(23, 16)) >> 16;
+}
+
+#define PWMV4_REG_ENABLE		0x004
+/*
+ * ENABLE Register Description
+ * [31:16] WO  | Write Enable Mask for the lower half of the register
+ *               Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ *               the same write operation
+ * [15:06] RO  | Reserved
+ * [05:05] RW  | PWM Channel Counter Read Enable, 1 = enabled
+ */
+#define PWMV4_CHN_CNT_RD_EN(v)		REG_UPDATE_BIT_WE((v), 5)
+/*
+ * [04:04] W1T | PWM Globally Joined Control Enable
+ *               1 = this PWM channel will be enabled by a global pwm enable
+ *               bit instead of the PWM Enable bit.
+ */
+#define PWMV4_GLOBAL_CTRL_EN(v)		REG_UPDATE_BIT_WE((v), 4)
+/*
+ * [03:03] RW  | Force Clock Enable
+ *               0 = disabled, if the PWM channel is inactive then so is the
+ *               clock prescale module
+ */
+#define PWMV4_FORCE_CLK_EN(v)		REG_UPDATE_BIT_WE((v), 3)
+/*
+ * [02:02] W1T | PWM Control Update Enable
+ *               1 = enabled, commits modifications of _CTRL, _PERIOD, _DUTY and
+ *               _OFFSET registers once 1 is written to it
+ */
+#define PWMV4_CTRL_UPDATE_EN(v)		REG_UPDATE_BIT_WE((v), 2)
+#define PWMV4_CTRL_UPDATE_EN_MASK	BIT(2)
+/*
+ * [01:01] RW  | PWM Enable, 1 = enabled
+ *               If in one-shot mode, clears after end of operation
+ */
+#define PWMV4_EN(v)			REG_UPDATE_BIT_WE((v), 1)
+#define PWMV4_EN_MASK			BIT(1)
+/*
+ * [00:00] RW  | PWM Clock Enable, 1 = enabled
+ *               If in one-shot mode, clears after end of operation
+ */
+#define PWMV4_CLK_EN(v)			REG_UPDATE_BIT_WE((v), 0)
+#define PWMV4_CLK_EN_MASK		BIT(0)
+#define PWMV4_EN_BOTH_MASK		(PWMV4_EN_MASK | PWMV4_CLK_EN_MASK)
+static inline __pure bool pwmv4_is_enabled(unsigned int val)
+{
+	return (val & PWMV4_EN_BOTH_MASK);
+}
+
+#define PWMV4_REG_CLK_CTRL		0x008
+/*
+ * CLK_CTRL Register Description
+ * [31:16] WO  | Write Enable Mask for the lower half of the register
+ *               Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ *               the same write operation
+ * [15:15] RW  | Clock Global Selection
+ *               0 = current channel scale clock
+ *               1 = global channel scale clock
+ */
+#define PWMV4_CLK_GLOBAL(v)		REG_UPDATE_BIT_WE((v), 15)
+/*
+ * [14:13] RW  | Clock Source Selection
+ *               0 = Clock from PLL, frequency can be configured
+ *               1 = Clock from crystal oscillator, frequency is fixed
+ *               2 = Clock from RC oscillator, frequency is fixed
+ *               3 = Reserved
+ *               NOTE: This reg is of questionable usefulness on RK3576, as it
+ *                     just muxes between 100m_50m_24m and 24m directly. The
+ *                     only use-case I can come up with is if you must use 100m
+ *                     or 50m PWM on one channel but absolutely require to use
+ *                     the lower rate 24m clock on another channel on the same
+ *                     chip, which doesn't seem like a realistic use case. I
+ *                     suspect that's why downstream doesn't use it.
+ */
+#define PWMV4_CLK_SRC_PLL		0x0U
+#define PWMV4_CLK_SRC_CRYSTAL		0x1U
+#define PWMV4_CLK_SRC_RC		0x2U
+#define PWMV4_CLK_SRC(v)		REG_UPDATE_WE((v), 13, 14)
+#define PWMV4_CLK_SRC_SHIFT		13
+#define PWMV4_CLK_SRC_MASK		GENMASK(14, 13)
+/*
+ * [12:04] RW  | Scale Factor to apply to pre-scaled clock
+ *               1 <= v <= 256, v means clock divided by 2*v
+ */
+#define PWMV4_CLK_SCALE_F(v)		REG_UPDATE_WE((v), 4, 12)
+/*
+ * [03:03] RO  | Reserved
+ * [02:00] RW  | Prescale Factor
+ *               v here means the input clock is divided by pow(2, v)
+ */
+#define PWMV4_CLK_PRESCALE_F(v)		REG_UPDATE_WE((v), 0, 2)
+
+#define PWMV4_REG_CTRL			0x00C
+/*
+ * CTRL Register Description
+ * [31:16] WO  | Write Enable Mask for the lower half of the register
+ *               Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ *               the same write operation
+ * [15:09] RO  | Reserved
+ * [08:06] RW  | PWM Input Channel Selection
+ *               By default, the channel selects its own input, but writing v
+ *               here selects PWM input from channel v instead.
+ */
+#define PWMV4_CTRL_IN_SEL(v)		REG_UPDATE_WE((v), 6, 8)
+/* [05:05] RW  | Aligned Mode, 0 = Valid, 1 = Invalid */
+#define PWMV4_CTRL_UNALIGNED(v)		REG_UPDATE_BIT_WE((v), 5)
+/* [04:04] RW  | Output Mode, 0 = Left Aligned, 1 = Centre Aligned */
+#define PWMV4_LEFT_ALIGNED		0x0U
+#define PWMV4_CENTRE_ALIGNED		0x1U
+#define PWMV4_CTRL_OUT_MODE(v)		REG_UPDATE_BIT_WE((v), 4)
+/*
+ * [03:03] RW  | Inactive Polarity for when the channel is either disabled or
+ *               has completed outputting the entire waveform in one-shot mode.
+ *               0 = Negative, 1 = Positive
+ */
+#define PWMV4_POLARITY_N		0x0U
+#define PWMV4_POLARITY_P		0x1U
+#define PWMV4_INACTIVE_POL(v)		REG_UPDATE_BIT_WE((v), 3)
+/*
+ * [02:02] RW  | Duty Cycle Polarity to use at the start of the waveform.
+ *               0 = Negative, 1 = Positive
+ */
+#define PWMV4_DUTY_POL(v)		REG_UPDATE_BIT_WE((v), 2)
+#define PWMV4_DUTY_POL_MASK		BIT(2)
+#define PWMV4_DUTY_POL_SHIFT		2
+/*
+ * [01:00] RW  | PWM Mode
+ *               0 = One-shot mode, PWM generates waveform RPT times
+ *               1 = Continuous mode
+ *               2 = Capture mode, PWM measures cycles of input waveform
+ *               3 = Reserved
+ */
+#define PWMV4_MODE_ONESHOT		0x0U
+#define PWMV4_MODE_CONT			0x1U
+#define PWMV4_MODE_CAPTURE		0x2U
+#define PWMV4_MODE(v)			REG_UPDATE_WE((v), 0, 1)
+#define PWMV4_CTRL_COM_FLAGS	(PWMV4_INACTIVE_POL(PWMV4_POLARITY_N) | \
+				 PWMV4_DUTY_POL(PWMV4_POLARITY_P) | \
+				 PWMV4_CTRL_OUT_MODE(PWMV4_LEFT_ALIGNED) | \
+				 PWMV4_CTRL_UNALIGNED(true))
+#define PWMV4_CTRL_CONT_FLAGS	(PWMV4_MODE(PWMV4_MODE_CONT) | \
+				 PWMV4_CTRL_COM_FLAGS)
+#define PWMV4_CTRL_CAP_FLAGS	(PWMV4_MODE(PWMV4_MODE_CAPTURE) | \
+				 PWMV4_CTRL_COM_FLAGS)
+
+#define PWMV4_REG_PERIOD		0x010
+/*
+ * PERIOD Register Description
+ * [31:00] RW  | Period of the output waveform
+ *               Constraints: should be even if CTRL_OUT_MODE is CENTRE_ALIGNED
+ */
+
+#define PWMV4_REG_DUTY			0x014
+/*
+ * DUTY Register Description
+ * [31:00] RW  | Duty cycle of the output waveform
+ *               Constraints: should be even if CTRL_OUT_MODE is CENTRE_ALIGNED
+ */
+
+#define PWMV4_REG_OFFSET		0x018
+/*
+ * OFFSET Register Description
+ * [31:00] RW  | Offset of the output waveform, based on the PWM clock
+ *               Constraints: 0 <= v <= (PERIOD - DUTY)
+ */
+
+#define PWMV4_REG_RPT			0x01C
+/*
+ * RPT Register Description
+ * [31:16] RW  | Second dimensional of the effective number of waveform
+ *               repetitions. Increases by one every first dimensional times.
+ *               Value `n` means `n + 1` repetitions. The final number of
+ *               repetitions of the waveform in one-shot mode is:
+ *               `(first_dimensional + 1) * (second_dimensional + 1)`
+ * [15:00] RW  | First dimensional of the effective number of waveform
+ *               repetitions. Value `n` means `n + 1` repetitions.
+ */
+
+#define PWMV4_REG_FILTER_CTRL		0x020
+/*
+ * FILTER_CTRL Register Description
+ * [31:16] WO  | Write Enable Mask for the lower half of the register
+ *               Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ *               the same write operation
+ * [15:10] RO  | Reserved
+ * [09:04] RW  | Filter window number
+ * [03:01] RO  | Reserved
+ * [00:00] RW  | Filter Enable, 0 = disabled, 1 = enabled
+ */
+
+#define PWMV4_REG_CNT			0x024
+/*
+ * CNT Register Description
+ * [31:00] RO  | Current value of the PWM Channel 0 counter in pwm clock cycles,
+ *               0 <= v <= 2^32-1
+ */
+
+#define PWMV4_REG_ENABLE_DELAY		0x028
+/*
+ * ENABLE_DELAY Register Description
+ * [31:16] RO  | Reserved
+ * [15:00] RW  | PWM enable delay, in an unknown unit but probably cycles
+ */
+
+#define PWMV4_REG_HPC			0x02C
+/*
+ * HPC Register Description
+ * [31:00] RW  | Number of effective high polarity cycles of the input waveform
+ *               in capture mode. Based on the PWM clock. 0 <= v <= 2^32-1
+ */
+
+#define PWMV4_REG_LPC			0x030
+/*
+ * LPC Register Description
+ * [31:00] RW  | Number of effective low polarity cycles of the input waveform
+ *               in capture mode. Based on the PWM clock. 0 <= v <= 2^32-1
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_CTRL0	0x040
+/*
+ * BIPHASIC_CNT_CTRL0 Register Description
+ * [31:16] WO  | Write Enable Mask for the lower half of the register
+ *               Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ *               the same write operation
+ * [15:10] RO  | Reserved
+ * [09:09] RW  | Biphasic Counter Phase Edge Selection for mode 0,
+ *               0 = rising edge (posedge), 1 = falling edge (negedge)
+ * [08:08] RW  | Biphasic Counter Clock force enable, 1 = force enable
+ * [07:07] W1T | Synchronous Enable
+ * [06:06] W1T | Mode Switch
+ *               0 = Normal Mode, 1 = Switch timer clock and measured clock
+ *               Constraints: "Biphasic Counter Mode" must be 0 if this is 1
+ * [05:03] RW  | Biphasic Counter Mode
+ *               0x0 = Mode 0, 0x1 = Mode 1, 0x2 = Mode 2, 0x3 = Mode 3,
+ *               0x4 = Mode 4, 0x5 = Reserved
+ * [02:02] RW  | Biphasic Counter Clock Selection
+ *               0 = clock is from PLL and frequency can be configured
+ *               1 = clock is from crystal oscillator and frequency is fixed
+ * [01:01] RW  | Biphasic Counter Continuous Mode
+ * [00:00] W1T | Biphasic Counter Enable
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_CTRL1	0x044
+/*
+ * BIPHASIC_CNT_CTRL1 Register Description
+ * [31:16] WO  | Write Enable Mask for the lower half of the register
+ *               Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ *               the same write operation
+ * [15:11] RO  | Reserved
+ * [10:04] RW  | Biphasic Counter Filter Window Number
+ * [03:01] RO  | Reserved
+ * [00:00] RW  | Biphasic Counter Filter Enable
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_TIMER	0x048
+/*
+ * BIPHASIC_CNT_TIMER Register Description
+ * [31:00] RW  | Biphasic Counter Timer Value, in number of biphasic counter
+ *               timer clock cycles
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_RES	0x04C
+/*
+ * BIPHASIC_CNT_RES Register Description
+ * [31:00] RO  | Biphasic Counter Result Value
+ *               Constraints: Can only be read after INTSTS[9] is asserted
+ */
+
+#define PWMV4_REG_BIPHASIC_CNT_RES_S	0x050
+/*
+ * BIPHASIC_CNT_RES_S Register Description
+ * [31:00] RO  | Biphasic Counter Result Value with synchronised processing
+ *               Can be read in real-time if BIPHASIC_CNT_CTRL0[7] was set to 1
+ */
+
+#define PWMV4_REG_INTSTS		0x070
+/*
+ * INTSTS Register Description
+ * [31:10] RO  | Reserved
+ * [09:09] W1C | Biphasic Counter Interrupt Status, 1 = interrupt asserted
+ * [08:08] W1C | Waveform Middle Interrupt Status, 1 = interrupt asserted
+ * [07:07] W1C | Waveform Max Interrupt Status, 1 = interrupt asserted
+ * [06:06] W1C | IR Transmission End Interrupt Status, 1 = interrupt asserted
+ * [05:05] W1C | Power Key Match Interrupt Status, 1 = interrupt asserted
+ * [04:04] W1C | Frequency Meter Interrupt Status, 1 = interrupt asserted
+ * [03:03] W1C | Reload Interrupt Status, 1 = interrupt asserted
+ * [02:02] W1C | Oneshot End Interrupt Status, 1 = interrupt asserted
+ * [01:01] W1C | HPC Capture Interrupt Status, 1 = interrupt asserted
+ * [00:00] W1C | LPC Capture Interrupt Status, 1 = interrupt asserted
+ */
+#define PWMV4_INT_LPC			BIT(0)
+#define PWMV4_INT_HPC			BIT(1)
+#define PWMV4_INT_LPC_W(v)		REG_UPDATE_BIT_WE(((v) ? 1 : 0), 0)
+#define PWMV4_INT_HPC_W(v)		REG_UPDATE_BIT_WE(((v) ? 1 : 0), 1)
+
+#define PWMV4_REG_INT_EN		0x074
+/*
+ * INT_EN Register Description
+ * [31:16] WO  | Write Enable Mask for the lower half of the register
+ *               Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ *               the same write operation
+ * [15:10] RO  | Reserved
+ * [09:09] RW  | Biphasic Counter Interrupt Enable, 1 = enabled
+ * [08:08] W1C | Waveform Middle Interrupt Enable, 1 = enabled
+ * [07:07] W1C | Waveform Max Interrupt Enable, 1 = enabled
+ * [06:06] W1C | IR Transmission End Interrupt Enable, 1 = enabled
+ * [05:05] W1C | Power Key Match Interrupt Enable, 1 = enabled
+ * [04:04] W1C | Frequency Meter Interrupt Enable, 1 = enabled
+ * [03:03] W1C | Reload Interrupt Enable, 1 = enabled
+ * [02:02] W1C | Oneshot End Interrupt Enable, 1 = enabled
+ * [01:01] W1C | HPC Capture Interrupt Enable, 1 = enabled
+ * [00:00] W1C | LPC Capture Interrupt Enable, 1 = enabled
+ */
+
+#define PWMV4_REG_INT_MASK		0x078
+/*
+ * INT_MASK Register Description
+ * [31:16] WO  | Write Enable Mask for the lower half of the register
+ *               Set bit `n` here to 1 if you wish to modify bit `n >> 16` in
+ *               the same write operation
+ * [15:10] RO  | Reserved
+ * [09:09] RW  | Biphasic Counter Interrupt Masked, 1 = masked
+ * [08:08] W1C | Waveform Middle Interrupt Masked, 1 = masked
+ * [07:07] W1C | Waveform Max Interrupt Masked, 1 = masked
+ * [06:06] W1C | IR Transmission End Interrupt Masked, 1 = masked
+ * [05:05] W1C | Power Key Match Interrupt Masked, 1 = masked
+ * [04:04] W1C | Frequency Meter Interrupt Masked, 1 = masked
+ * [03:03] W1C | Reload Interrupt Masked, 1 = masked
+ * [02:02] W1C | Oneshot End Interrupt Masked, 1 = masked
+ * [01:01] W1C | HPC Capture Interrupt Masked, 1 = masked
+ * [00:00] W1C | LPC Capture Interrupt Masked, 1 = masked
+ */
+
+inline u32 mfpwm_reg_read(void __iomem *base, u32 reg)
+{
+	return readl(base + reg);
+}
+
+inline void mfpwm_reg_write(void __iomem *base, u32 reg, u32 val)
+{
+	writel(val, base + reg);
+}
+
+/**
+ * mfpwm_clk_get_rate - get the currently used clock's rate, in Hz
+ * @mfpwm: pointer to the &struct rockchip_mfpwm instance
+ *
+ * Returns: %0 on error, clock rate in Hz on success
+ */
+unsigned long mfpwm_clk_get_rate(struct rockchip_mfpwm *mfpwm);
+
+/**
+ * mfpwm_acquire - try becoming the active mfpwm function device
+ * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func
+ *
+ * mfpwm device "function" drivers must call this function before doing anything
+ * that either modifies or relies on the parent device's state, such as clocks,
+ * enabling/disabling outputs, modifying shared regs etc.
+ *
+ * The return statues should always be checked.
+ *
+ * All mfpwm_acquire() calls must be balanced with corresponding mfpwm_release()
+ * calls once the device is no longer making changes that affect other devices,
+ * or stops producing user-visible effects that depend on the current device
+ * state being kept as-is. (e.g. after the PWM output signal is stopped)
+ *
+ * The same device function may mfpwm_acquire() multiple times while it already
+ * is active, i.e. it is re-entrant, though it needs to balance this with the
+ * same number of mfpwm_release() calls.
+ *
+ * Context: This function does not sleep.
+ *
+ * Return:
+ * * %0                 - success
+ * * %-EBUSY            - a different device function is active
+ * * %-EOVERFLOW        - the acquire counter is at its maximum
+ */
+int __must_check mfpwm_acquire(struct rockchip_mfpwm_func *pwmf);
+
+/**
+ * mfpwm_release - drop usage of active mfpwm device function by 1
+ * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func
+ *
+ * This is the balancing call to mfpwm_acquire(). If no users of the device
+ * function remain, set the mfpwm device to have no active device function,
+ * allowing other device functions to claim it.
+ */
+void mfpwm_release(const struct rockchip_mfpwm_func *pwmf);
+
+/**
+ * mfpwm_pwmclk_enable - enable the pwm clock the signal and timing is based on
+ * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func
+ *
+ * Context: must be the active device function to call this
+ *
+ * Returns: 0 on success, negative errno on error.
+ */
+int mfpwm_pwmclk_enable(struct rockchip_mfpwm_func *pwmf);
+
+/**
+ * mfpwm_pwmclk_disable - disable the pwm clock the signal and timing is based on
+ * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func
+ *
+ * Context: must be the active device function to call this
+ */
+void mfpwm_pwmclk_disable(struct rockchip_mfpwm_func *pwmf);
+
+/**
+ * mfpwm_remove_func - remove a device function driver from the mfpwm
+ * @pwmf: pointer to the calling driver instance's &struct rockchip_mfpwm_func
+ *
+ * If the device function driver described by @pwmf is the currently active
+ * device function, then it'll have its mfpwm_acquires and its pwmclk_enables
+ * balanced and be removed as the active device function driver.
+ */
+void mfpwm_remove_func(struct rockchip_mfpwm_func *pwmf);
+
+#endif /* __SOC_ROCKCHIP_MFPWM_H__ */