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. v2: - Rename gpio to gpios in binding - Calculate next expiry from expected current expiry rather than "now" - Only change configuration after current period ends - Implement get_state() - Add error message for probe failures - Stop PWM before unregister Vincent Whitchurch (2): dt-bindings: pwm: Add pwm-gpio pwm: Add GPIO PWM driver .../devicetree/bindings/pwm/pwm-gpio.yaml | 29 +++ drivers/pwm/Kconfig | 10 + drivers/pwm/Makefile | 1 + drivers/pwm/pwm-gpio.c | 195 ++++++++++++++++++ 4 files changed, 235 insertions(+) create mode 100644 Documentation/devicetree/bindings/pwm/pwm-gpio.yaml create mode 100644 drivers/pwm/pwm-gpio.c Range-diff: 1: 0e672c567516 ! 1: 9efea8f7fb29 dt-bindings: pwm: Add pwm-gpio @@ Documentation/devicetree/bindings/pwm/pwm-gpio.yaml (new) + "#pwm-cells": + const: 2 + -+ gpio: ++ gpios: + maxItems: 1 + description: GPIO to toggle. + 2: c9df282b1bd4 ! 2: f5a4a9391e78 pwm: Add GPIO PWM driver @@ drivers/pwm/pwm-gpio.c (new) + +#include <linux/gpio/consumer.h> +#include <linux/platform_device.h> ++#include <linux/spinlock.h> +#include <linux/hrtimer.h> +#include <linux/module.h> +#include <linux/slab.h> @@ drivers/pwm/pwm-gpio.c (new) + struct pwm_chip chip; + struct hrtimer hrtimer; + struct gpio_desc *gpio; -+ ktime_t on_interval; -+ ktime_t off_interval; -+ bool invert; -+ bool on; ++ struct pwm_state state; ++ struct pwm_state nextstate; ++ 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 *hrtimer) +{ + struct pwm_gpio *gpwm = container_of(hrtimer, struct pwm_gpio, hrtimer); -+ bool newon = !gpwm->on; ++ unsigned long nexttoggle; ++ unsigned long flags; ++ bool newlevel; ++ ++ 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->nextstate; ++ newlevel = !!gpwm->state.duty_cycle; ++ } else { ++ newlevel = !gpwm->level; ++ } + -+ gpwm->on = newon; -+ gpiod_set_value(gpwm->gpio, newon ^ gpwm->invert); ++ nexttoggle = pwm_gpio_toggle(gpwm, newlevel); ++ if (nexttoggle) ++ hrtimer_forward(hrtimer, hrtimer_get_expires(hrtimer), ++ ns_to_ktime(nexttoggle)); + -+ hrtimer_forward_now(hrtimer, newon ? gpwm->on_interval : gpwm->off_interval); ++ spin_unlock_irqrestore(&gpwm->lock, flags); + -+ return HRTIMER_RESTART; ++ return nexttoggle ? 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); ++ unsigned long flags; + -+ hrtimer_cancel(&gpwm->hrtimer); ++ if (!state->enabled) ++ hrtimer_cancel(&gpwm->hrtimer); ++ ++ spin_lock_irqsave(&gpwm->lock, flags); + + if (!state->enabled) { ++ gpwm->state = *state; ++ gpwm->running = false; ++ gpwm->changing = false; ++ + gpiod_set_value(gpwm->gpio, 0); -+ return 0; ++ } else if (gpwm->running) { ++ gpwm->nextstate = *state; ++ gpwm->changing = true; ++ } else { ++ unsigned long nexttoggle; ++ ++ gpwm->state = *state; ++ gpwm->changing = false; ++ ++ nexttoggle = pwm_gpio_toggle(gpwm, !!state->duty_cycle); ++ if (nexttoggle) ++ hrtimer_start(&gpwm->hrtimer, nexttoggle, ++ HRTIMER_MODE_REL); + } + -+ gpwm->on_interval = ns_to_ktime(state->duty_cycle); -+ gpwm->off_interval = ns_to_ktime(state->period - state->duty_cycle); -+ gpwm->invert = state->polarity == PWM_POLARITY_INVERSED; ++ spin_unlock_irqrestore(&gpwm->lock, flags); ++ ++ return 0; ++} ++ ++static void 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; + -+ gpwm->on = !!gpwm->on_interval; -+ gpiod_set_value(gpwm->gpio, gpwm->on ^ gpwm->invert); ++ spin_lock_irqsave(&gpwm->lock, flags); + -+ if (gpwm->on_interval && gpwm->off_interval) -+ hrtimer_start(&gpwm->hrtimer, gpwm->on_interval, HRTIMER_MODE_REL); ++ if (gpwm->changing) ++ *state = gpwm->nextstate; ++ else ++ *state = gpwm->state; + -+ return 0; ++ spin_unlock_irqrestore(&gpwm->lock, flags); +} + +static const struct pwm_ops pwm_gpio_ops = { + .owner = THIS_MODULE, + .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(&pdev->dev, sizeof(*gpwm), GFP_KERNEL); ++ gpwm = devm_kzalloc(dev, sizeof(*gpwm), GFP_KERNEL); + if (!gpwm) + return -ENOMEM; + -+ gpwm->gpio = devm_gpiod_get(&pdev->dev, NULL, GPIOD_OUT_LOW); ++ spin_lock_init(&gpwm->lock); ++ ++ gpwm->gpio = devm_gpiod_get(dev, NULL, GPIOD_OUT_LOW); + if (IS_ERR(gpwm->gpio)) -+ return PTR_ERR(gpwm->gpio); ++ return dev_err_probe(dev, PTR_ERR(gpwm->gpio), ++ "could not get gpio\n"); + + if (gpiod_cansleep(gpwm->gpio)) -+ return -EINVAL; ++ return dev_err_probe(dev, -EINVAL, ++ "sleeping GPIOs not supported\n"); + -+ gpwm->chip.dev = &pdev->dev; ++ gpwm->chip.dev = dev; + gpwm->chip.ops = &pwm_gpio_ops; + gpwm->chip.base = pdev->id; + gpwm->chip.npwm = 1; @@ drivers/pwm/pwm-gpio.c (new) + + ret = pwmchip_add(&gpwm->chip); + if (ret < 0) -+ return ret; ++ return dev_err_probe(dev, ret, ++ "could not add pwmchip\n"); + + platform_set_drvdata(pdev, gpwm); + @@ drivers/pwm/pwm-gpio.c (new) +{ + struct pwm_gpio *gpwm = platform_get_drvdata(pdev); + ++ pwm_disable(&gpwm->chip.pwms[0]); ++ + return pwmchip_remove(&gpwm->chip); +} + -- 2.28.0