diff mbox series

[V4,2/2] pwm: Add GPIO PWM driver

Message ID 20240204220851.4783-3-wahrenst@gmx.net
State New
Headers show
Series pwm: Add GPIO PWM driver | expand

Commit Message

Stefan Wahren Feb. 4, 2024, 10:08 p.m. UTC
From: Vincent Whitchurch <vincent.whitchurch@axis.com>

Add a software PWM which toggles a GPIO from a high-resolution timer.

This will naturally not be as accurate or as efficient as a hardware
PWM, but it is useful in some cases.  I have for example used it for
evaluating LED brightness handling (via leds-pwm) on a board where the
LED was just hooked up to a GPIO, and for a simple verification of the
timer frequency on another platform.

Since high-resolution timers are used, sleeping gpio chips are not
supported and are rejected in the probe function.

Signed-off-by: Vincent Whitchurch <vincent.whitchurch@axis.com>
Co-developed-by: Stefan Wahren <wahrenst@gmx.net>
Signed-off-by: Stefan Wahren <wahrenst@gmx.net>
---
 drivers/pwm/Kconfig    |  11 ++
 drivers/pwm/Makefile   |   1 +
 drivers/pwm/pwm-gpio.c | 228 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 240 insertions(+)
 create mode 100644 drivers/pwm/pwm-gpio.c

--
2.34.1

Comments

Dhruva Gole Feb. 5, 2024, 6:11 a.m. UTC | #1
On Feb 04, 2024 at 23:08:51 +0100, Stefan Wahren wrote:
> From: Vincent Whitchurch <vincent.whitchurch@axis.com>
> 
> Add a software PWM which toggles a GPIO from a high-resolution timer.
> 
> This will naturally not be as accurate or as efficient as a hardware
> PWM, but it is useful in some cases.  I have for example used it for
> evaluating LED brightness handling (via leds-pwm) on a board where the
> LED was just hooked up to a GPIO, and for a simple verification of the
> timer frequency on another platform.
> 
> Since high-resolution timers are used, sleeping gpio chips are not
> supported and are rejected in the probe function.
> 
> Signed-off-by: Vincent Whitchurch <vincent.whitchurch@axis.com>
> Co-developed-by: Stefan Wahren <wahrenst@gmx.net>
> Signed-off-by: Stefan Wahren <wahrenst@gmx.net>
> ---
>  drivers/pwm/Kconfig    |  11 ++
>  drivers/pwm/Makefile   |   1 +
>  drivers/pwm/pwm-gpio.c | 228 +++++++++++++++++++++++++++++++++++++++++
>  3 files changed, 240 insertions(+)
>  create mode 100644 drivers/pwm/pwm-gpio.c
> 
> diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig
> index 4b956d661755..7cfda2cde130 100644
> --- a/drivers/pwm/Kconfig
> +++ b/drivers/pwm/Kconfig
> @@ -227,6 +227,17 @@ config PWM_FSL_FTM
>  	  To compile this driver as a module, choose M here: the module
>  	  will be called pwm-fsl-ftm.
> 
> +config PWM_GPIO
> +	tristate "GPIO PWM support"
> +	depends on GPIOLIB
> +	depends on HIGH_RES_TIMERS
> +	help
> +	  Generic PWM framework driver for a software PWM toggling a GPIO pin

>> a software PWM
Nit: remove the "a", as it's not required.

> +	  from kernel high-resolution timers.
> +
> +	  To compile this driver as a module, choose M here: the module
> +	  will be called pwm-gpio.
> +
[..snip..]
> +
> +static int pwm_gpio_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
> +			       struct pwm_state *state)
> +{
> +	struct pwm_gpio *gpwm = container_of(chip, struct pwm_gpio, chip);
> +	unsigned long flags;
> +
> +	spin_lock_irqsave(&gpwm->lock, flags);
> +
> +	if (gpwm->changing)
> +		*state = gpwm->next_state;
> +	else
> +		*state = gpwm->state;
> +
> +	spin_unlock_irqrestore(&gpwm->lock, flags);
> +
> +	return 0;
> +}
> +
> +static const struct pwm_ops pwm_gpio_ops = {
> +	.apply = pwm_gpio_apply,

Kinda looks like this is setting state? Can we be consistent with the
naming then, like pwm_gpio_set_state?

This will help those contributing to the driver or referring to it to
understand better what each function is doing exactly.

> +	.get_state = pwm_gpio_get_state,
> +};

Otherwise, driver looks good to me,
Reviewed-by: Dhruva Gole <d-gole@ti.com>
Stefan Wahren Feb. 5, 2024, 12:21 p.m. UTC | #2
Hi Uwe,

Am 05.02.24 um 11:09 schrieb Uwe Kleine-König:
> Hello,
>
> On Sun, Feb 04, 2024 at 11:08:51PM +0100, Stefan Wahren wrote:
>> +static enum hrtimer_restart pwm_gpio_timer(struct hrtimer *gpio_timer)
>> +{
>> +	struct pwm_gpio *gpwm = container_of(gpio_timer, struct pwm_gpio,
>> +					     gpio_timer);
>> +	unsigned long next_toggle;
>> +	unsigned long flags;
>> +	bool new_level;
>> +
>> +	spin_lock_irqsave(&gpwm->lock, flags);
>> +
>> +	/* Apply new state at end of current period */
>> +	if (!gpwm->level && gpwm->changing) {
>> +		gpwm->changing = false;
>> +		gpwm->state = gpwm->next_state;
>> +		new_level = !!gpwm->state.duty_cycle;
>> +	} else {
>> +		new_level = !gpwm->level;
>> +	}
>> +
>> +	next_toggle = pwm_gpio_toggle(gpwm, new_level);
>> +	if (next_toggle) {
>> +		hrtimer_forward(gpio_timer, hrtimer_get_expires(gpio_timer),
>> +				ns_to_ktime(next_toggle));
> How does this work in relation to hrtimer_resolution? If the resolution
> is (say) 300 and next_toggle is 2000. Does the trigger trigger at
> hrtimer_get_expires(...) + 1800, or at ... + 2100?
>
> If you assume we have period = 6000 and duty_cycle = 2000, the delta to
> forward the driver alternates between 1800 and 3900 (if rounding down)
> or between 2100 and 4200 if rounding up. Both is not optimal.
>
> Ideally you'd round down the active phase (i.e. pick 1800) and for the
> inactive phase you'd use rounddown(period) - rounddown(duty_cycle) which
> gives 4200 here. Does this make sense?
oh dear, looks like a can of worms. I will look into it. Btw according
to multi_v7_defconfig the hrtimer_resolution on the Pi is 1 ns.

Does it make sense to log the hrtimer_resolution e.g. at probe?
>
>> +	}
>> +
>> +	spin_unlock_irqrestore(&gpwm->lock, flags);
>> +
>> +	return next_toggle ? HRTIMER_RESTART : HRTIMER_NORESTART;
>> +}
>> +
>> +static int pwm_gpio_apply(struct pwm_chip *chip, struct pwm_device *pwm,
>> +			  const struct pwm_state *state)
>> +{
>> +	struct pwm_gpio *gpwm = container_of(chip, struct pwm_gpio, chip);
>> +	bool invert = state->polarity == PWM_POLARITY_INVERSED;
>> +	unsigned long flags;
>> +
>> +	if (state->duty_cycle && state->duty_cycle < hrtimer_resolution)
>> +		return -EINVAL;
>> +
>> +	if (state->duty_cycle != state->period &&
>> +	    (state->period - state->duty_cycle < hrtimer_resolution))
>> +		return -EINVAL;
> If you assume that hrtimer_resolution = 300 again, you don't want to
> refuse
>
> 	.duty_cycle = 200
> 	.period = 200
>
> do you?
Actually i had rejected it. Yes, this specific corner case does work
with such a resolution. But if the user want a steady level, the user
would use a plain GPIO not a PWM. As soon the duty cycle get lower this
would be rejected and as a user i would be confused.

Another issue here, is that we don't have a good interface to tell the
limitations.
> I think you want:
>
> 	mininterval = min(state->duty_cycle, state->period - state->duty_cycle);
> 	if (mininterval && mininterval < hrtimer_resolution)
> 		return -EINVAL;
>
> to catch both cases in a single check.
>
>> +	if (!state->enabled) {
>> +		hrtimer_cancel(&gpwm->gpio_timer);
>> +	} else if (!gpwm->running) {
>> +		/*
>> +		 * This just enables the output, but pwm_gpio_toggle()
>> +		 * really starts the duty cycle.
>> +		 */
>> +		int ret = gpiod_direction_output(gpwm->gpio, invert);
>> +
>> +		if (ret)
>> +			return ret;
>> +	}
>> +
>> +	spin_lock_irqsave(&gpwm->lock, flags);
>> +
>> +	if (!state->enabled) {
>> +		gpwm->state = *state;
>> +		gpwm->running = false;
>> +		gpwm->changing = false;
>> +
>> +		gpiod_set_value(gpwm->gpio, invert);
>> +	} else if (gpwm->running) {
>> +		gpwm->next_state = *state;
>> +		gpwm->changing = true;
>> +	} else {
>> +		unsigned long next_toggle;
>> +
>> +		gpwm->state = *state;
>> +		gpwm->changing = false;
>> +
>> +		next_toggle = pwm_gpio_toggle(gpwm, !!state->duty_cycle);
>> +		if (next_toggle) {
>> +			hrtimer_start(&gpwm->gpio_timer, next_toggle,
>> +				      HRTIMER_MODE_REL);
>> +		}
> The curly braces can be dropped here and in a few more locations.
>
>> +	}
>> +
>> +	spin_unlock_irqrestore(&gpwm->lock, flags);
>> +
>> +	return 0;
>> +}
>> +
>> +static int pwm_gpio_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
>> +			       struct pwm_state *state)
>> +{
>> +	struct pwm_gpio *gpwm = container_of(chip, struct pwm_gpio, chip);
>> +	unsigned long flags;
>> +
>> +	spin_lock_irqsave(&gpwm->lock, flags);
>> +
>> +	if (gpwm->changing)
>> +		*state = gpwm->next_state;
>> +	else
>> +		*state = gpwm->state;
>> +
>> +	spin_unlock_irqrestore(&gpwm->lock, flags);
>> +
>> +	return 0;
>> +}
>> +
>> +static const struct pwm_ops pwm_gpio_ops = {
>> +	.apply = pwm_gpio_apply,
>> +	.get_state = pwm_gpio_get_state,
>> +};
>> +
>> +static int pwm_gpio_probe(struct platform_device *pdev)
>> +{
>> +	struct device *dev = &pdev->dev;
>> +	struct pwm_gpio *gpwm;
>> +	int ret;
>> +
>> +	gpwm = devm_kzalloc(dev, sizeof(*gpwm), GFP_KERNEL);
>> +	if (!gpwm)
>> +		return -ENOMEM;
>> +
>> +	spin_lock_init(&gpwm->lock);
>> +
>> +	gpwm->gpio = devm_gpiod_get(dev, NULL, GPIOD_ASIS);
>> +	if (IS_ERR(gpwm->gpio)) {
>> +		return dev_err_probe(dev, PTR_ERR(gpwm->gpio),
>> +				     "could not get gpio\n");
>> +	}
>> +
>> +	if (gpiod_cansleep(gpwm->gpio)) {
>> +		return dev_err_probe(dev, -EINVAL,
>> +				     "sleeping GPIO %d not supported\n",
>> +				     desc_to_gpio(gpwm->gpio));
>> +	}
> Is it still state of the art to add gpio numbers to error messages?
I was unsure how to handle this user-friendly. Just simply logging
"sleeping GPIO not supported" doesn't provide a reference on the
affected GPIO. GPIO names are optional, so maybe empty.

I'm open to suggestions :-)

Best regards
>
>> +	gpwm->chip.dev = dev;
>> +	gpwm->chip.ops = &pwm_gpio_ops;
>> +	gpwm->chip.npwm = 1;
>> +	gpwm->chip.atomic = true;
>> +
>> +	hrtimer_init(&gpwm->gpio_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
>> +	gpwm->gpio_timer.function = pwm_gpio_timer;
>> +
>> +	ret = pwmchip_add(&gpwm->chip);
>> +	if (ret < 0)
>> +		return dev_err_probe(dev, ret, "could not add pwmchip\n");
>> +
>> +	platform_set_drvdata(pdev, gpwm);
>> +
>> +	return 0;
>> +}
> Best regards
> Uwe
>
Stefan Wahren Feb. 27, 2024, 8:24 p.m. UTC | #3
Hi,

Am 27.02.24 um 17:41 schrieb Andy Shevchenko:
> On Sun, Feb 04, 2024 at 11:08:51PM +0100, Stefan Wahren wrote:
> ...
>
>> +	if (gpiod_cansleep(gpwm->gpio)) {
>> +		return dev_err_probe(dev, -EINVAL,
>> +				     "sleeping GPIO %d not supported\n",
>> +				     desc_to_gpio(gpwm->gpio));
> Do not use plain GPIO numbers.
Uwe already complained this, but i didn't receive a reply on the
question how do we provide a useful log message (reference to the
affected GPIO) here? AFAIK the GPIO names are optional.

Regards
Andy Shevchenko Feb. 27, 2024, 8:47 p.m. UTC | #4
On Tue, Feb 27, 2024 at 10:25 PM Stefan Wahren <wahrenst@gmx.net> wrote:
>
> Hi,
>
> Am 27.02.24 um 17:41 schrieb Andy Shevchenko:
> > On Sun, Feb 04, 2024 at 11:08:51PM +0100, Stefan Wahren wrote:
> > ...
> >
> >> +    if (gpiod_cansleep(gpwm->gpio)) {
> >> +            return dev_err_probe(dev, -EINVAL,
> >> +                                 "sleeping GPIO %d not supported\n",
> >> +                                 desc_to_gpio(gpwm->gpio));
> > Do not use plain GPIO numbers.
> Uwe already complained this, but i didn't receive a reply on the
> question how do we provide a useful log message (reference to the
> affected GPIO) here? AFAIK the GPIO names are optional.

You have a firmware node path, also you may add a label to GPIO, but
it's unrelated to the message (as it's constant).
%pfw
Stefan Wahren Feb. 28, 2024, 6:06 p.m. UTC | #5
Am 27.02.24 um 21:47 schrieb Andy Shevchenko:
> On Tue, Feb 27, 2024 at 10:25 PM Stefan Wahren <wahrenst@gmx.net> wrote:
>> Hi,
>>
>> Am 27.02.24 um 17:41 schrieb Andy Shevchenko:
>>> On Sun, Feb 04, 2024 at 11:08:51PM +0100, Stefan Wahren wrote:
>>> ...
>>>
>>>> +    if (gpiod_cansleep(gpwm->gpio)) {
>>>> +            return dev_err_probe(dev, -EINVAL,
>>>> +                                 "sleeping GPIO %d not supported\n",
>>>> +                                 desc_to_gpio(gpwm->gpio));
>>> Do not use plain GPIO numbers.
>> Uwe already complained this, but i didn't receive a reply on the
>> question how do we provide a useful log message (reference to the
>> affected GPIO) here? AFAIK the GPIO names are optional.
> You have a firmware node path, also you may add a label to GPIO, but
> it's unrelated to the message (as it's constant).
> %pfw
Thanks
diff mbox series

Patch

diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig
index 4b956d661755..7cfda2cde130 100644
--- a/drivers/pwm/Kconfig
+++ b/drivers/pwm/Kconfig
@@ -227,6 +227,17 @@  config PWM_FSL_FTM
 	  To compile this driver as a module, choose M here: the module
 	  will be called pwm-fsl-ftm.

+config PWM_GPIO
+	tristate "GPIO PWM support"
+	depends on GPIOLIB
+	depends on HIGH_RES_TIMERS
+	help
+	  Generic PWM framework driver for a software PWM toggling a GPIO pin
+	  from kernel high-resolution timers.
+
+	  To compile this driver as a module, choose M here: the module
+	  will be called pwm-gpio.
+
 config PWM_HIBVT
 	tristate "HiSilicon BVT PWM support"
 	depends on ARCH_HISI || COMPILE_TEST
diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile
index c5ec9e168ee7..59d1a46bb1af 100644
--- a/drivers/pwm/Makefile
+++ b/drivers/pwm/Makefile
@@ -19,6 +19,7 @@  obj-$(CONFIG_PWM_DWC_CORE)	+= pwm-dwc-core.o
 obj-$(CONFIG_PWM_DWC)		+= pwm-dwc.o
 obj-$(CONFIG_PWM_EP93XX)	+= pwm-ep93xx.o
 obj-$(CONFIG_PWM_FSL_FTM)	+= pwm-fsl-ftm.o
+obj-$(CONFIG_PWM_GPIO)		+= pwm-gpio.o
 obj-$(CONFIG_PWM_HIBVT)		+= pwm-hibvt.o
 obj-$(CONFIG_PWM_IMG)		+= pwm-img.o
 obj-$(CONFIG_PWM_IMX1)		+= pwm-imx1.o
diff --git a/drivers/pwm/pwm-gpio.c b/drivers/pwm/pwm-gpio.c
new file mode 100644
index 000000000000..4435f275f509
--- /dev/null
+++ b/drivers/pwm/pwm-gpio.c
@@ -0,0 +1,228 @@ 
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Generic software PWM for modulating GPIOs
+ *
+ * Copyright (C) 2020 Axis Communications AB
+ * Copyright (C) 2020 Nicola Di Lieto
+ * Copyright (C) 2024 Stefan Wahren
+ */
+
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/gpio/consumer.h>
+#include <linux/hrtimer.h>
+#include <linux/module.h>
+#include <linux/mod_devicetable.h>
+#include <linux/platform_device.h>
+#include <linux/pwm.h>
+#include <linux/spinlock.h>
+
+struct pwm_gpio {
+	struct pwm_chip chip;
+	struct hrtimer gpio_timer;
+	struct gpio_desc *gpio;
+	struct pwm_state state;
+	struct pwm_state next_state;
+
+	/* Protect internal state between pwm_ops and hrtimer */
+	spinlock_t lock;
+
+	bool changing;
+	bool running;
+	bool level;
+};
+
+static unsigned long pwm_gpio_toggle(struct pwm_gpio *gpwm, bool level)
+{
+	const struct pwm_state *state = &gpwm->state;
+	bool invert = state->polarity == PWM_POLARITY_INVERSED;
+
+	gpwm->level = level;
+	gpiod_set_value(gpwm->gpio, gpwm->level ^ invert);
+
+	if (!state->duty_cycle || state->duty_cycle == state->period) {
+		gpwm->running = false;
+		return 0;
+	}
+
+	gpwm->running = true;
+	return level ? state->duty_cycle : state->period - state->duty_cycle;
+}
+
+static enum hrtimer_restart pwm_gpio_timer(struct hrtimer *gpio_timer)
+{
+	struct pwm_gpio *gpwm = container_of(gpio_timer, struct pwm_gpio,
+					     gpio_timer);
+	unsigned long next_toggle;
+	unsigned long flags;
+	bool new_level;
+
+	spin_lock_irqsave(&gpwm->lock, flags);
+
+	/* Apply new state at end of current period */
+	if (!gpwm->level && gpwm->changing) {
+		gpwm->changing = false;
+		gpwm->state = gpwm->next_state;
+		new_level = !!gpwm->state.duty_cycle;
+	} else {
+		new_level = !gpwm->level;
+	}
+
+	next_toggle = pwm_gpio_toggle(gpwm, new_level);
+	if (next_toggle) {
+		hrtimer_forward(gpio_timer, hrtimer_get_expires(gpio_timer),
+				ns_to_ktime(next_toggle));
+	}
+
+	spin_unlock_irqrestore(&gpwm->lock, flags);
+
+	return next_toggle ? HRTIMER_RESTART : HRTIMER_NORESTART;
+}
+
+static int pwm_gpio_apply(struct pwm_chip *chip, struct pwm_device *pwm,
+			  const struct pwm_state *state)
+{
+	struct pwm_gpio *gpwm = container_of(chip, struct pwm_gpio, chip);
+	bool invert = state->polarity == PWM_POLARITY_INVERSED;
+	unsigned long flags;
+
+	if (state->duty_cycle && state->duty_cycle < hrtimer_resolution)
+		return -EINVAL;
+
+	if (state->duty_cycle != state->period &&
+	    (state->period - state->duty_cycle < hrtimer_resolution))
+		return -EINVAL;
+
+	if (!state->enabled) {
+		hrtimer_cancel(&gpwm->gpio_timer);
+	} else if (!gpwm->running) {
+		/*
+		 * This just enables the output, but pwm_gpio_toggle()
+		 * really starts the duty cycle.
+		 */
+		int ret = gpiod_direction_output(gpwm->gpio, invert);
+
+		if (ret)
+			return ret;
+	}
+
+	spin_lock_irqsave(&gpwm->lock, flags);
+
+	if (!state->enabled) {
+		gpwm->state = *state;
+		gpwm->running = false;
+		gpwm->changing = false;
+
+		gpiod_set_value(gpwm->gpio, invert);
+	} else if (gpwm->running) {
+		gpwm->next_state = *state;
+		gpwm->changing = true;
+	} else {
+		unsigned long next_toggle;
+
+		gpwm->state = *state;
+		gpwm->changing = false;
+
+		next_toggle = pwm_gpio_toggle(gpwm, !!state->duty_cycle);
+		if (next_toggle) {
+			hrtimer_start(&gpwm->gpio_timer, next_toggle,
+				      HRTIMER_MODE_REL);
+		}
+	}
+
+	spin_unlock_irqrestore(&gpwm->lock, flags);
+
+	return 0;
+}
+
+static int pwm_gpio_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
+			       struct pwm_state *state)
+{
+	struct pwm_gpio *gpwm = container_of(chip, struct pwm_gpio, chip);
+	unsigned long flags;
+
+	spin_lock_irqsave(&gpwm->lock, flags);
+
+	if (gpwm->changing)
+		*state = gpwm->next_state;
+	else
+		*state = gpwm->state;
+
+	spin_unlock_irqrestore(&gpwm->lock, flags);
+
+	return 0;
+}
+
+static const struct pwm_ops pwm_gpio_ops = {
+	.apply = pwm_gpio_apply,
+	.get_state = pwm_gpio_get_state,
+};
+
+static int pwm_gpio_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct pwm_gpio *gpwm;
+	int ret;
+
+	gpwm = devm_kzalloc(dev, sizeof(*gpwm), GFP_KERNEL);
+	if (!gpwm)
+		return -ENOMEM;
+
+	spin_lock_init(&gpwm->lock);
+
+	gpwm->gpio = devm_gpiod_get(dev, NULL, GPIOD_ASIS);
+	if (IS_ERR(gpwm->gpio)) {
+		return dev_err_probe(dev, PTR_ERR(gpwm->gpio),
+				     "could not get gpio\n");
+	}
+
+	if (gpiod_cansleep(gpwm->gpio)) {
+		return dev_err_probe(dev, -EINVAL,
+				     "sleeping GPIO %d not supported\n",
+				     desc_to_gpio(gpwm->gpio));
+	}
+
+	gpwm->chip.dev = dev;
+	gpwm->chip.ops = &pwm_gpio_ops;
+	gpwm->chip.npwm = 1;
+	gpwm->chip.atomic = true;
+
+	hrtimer_init(&gpwm->gpio_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
+	gpwm->gpio_timer.function = pwm_gpio_timer;
+
+	ret = pwmchip_add(&gpwm->chip);
+	if (ret < 0)
+		return dev_err_probe(dev, ret, "could not add pwmchip\n");
+
+	platform_set_drvdata(pdev, gpwm);
+
+	return 0;
+}
+
+static void pwm_gpio_remove(struct platform_device *pdev)
+{
+	struct pwm_gpio *gpwm = platform_get_drvdata(pdev);
+
+	pwmchip_remove(&gpwm->chip);
+	hrtimer_cancel(&gpwm->gpio_timer);
+}
+
+static const struct of_device_id pwm_gpio_dt_ids[] = {
+	{ .compatible = "pwm-gpio" },
+	{ /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, pwm_gpio_dt_ids);
+
+static struct platform_driver pwm_gpio_driver = {
+	.driver = {
+		.name = "pwm-gpio",
+		.of_match_table = pwm_gpio_dt_ids,
+	},
+	.probe = pwm_gpio_probe,
+	.remove_new = pwm_gpio_remove,
+};
+module_platform_driver(pwm_gpio_driver);
+
+MODULE_DESCRIPTION("PWM GPIO driver");
+MODULE_AUTHOR("Vincent Whitchurch");
+MODULE_LICENSE("GPL");