Driver to allow the Atmel PWMC peripheral found on various AT91 SoCs to be controlled using the Generic PWM framework. Tested on the AT91SAM9263. Signed-off-by: Bill Gatliff <bgat@xxxxxxxxxxxxxxx> --- drivers/pwm/Kconfig | 8 + drivers/pwm/Makefile | 1 + drivers/pwm/atmel-pwmc.c | 501 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 510 insertions(+), 0 deletions(-) create mode 100644 drivers/pwm/atmel-pwmc.c diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig index 21567d6..4f7b01b 100644 --- a/drivers/pwm/Kconfig +++ b/drivers/pwm/Kconfig @@ -17,3 +17,11 @@ config GPIO_PWM pin. The PWM framework allows you to create as many of these devices as desired, subject to CPU overhead and GPIO pin availability. If unsure, say N. + +config ATMEL_PWMC + tristate "Atmel AT32/AT91 PWMC support" + depends on GENERIC_PWM && (AVR32 || ARCH_AT91SAM9263 || ARCH_AT91SAM9RL || ARCH_AT91CAP9) + help + This option enables support under the generic PWM + framework for PWMC peripheral channels found on + certain Atmel microcontrollers. If unsure, say N. diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile index ecec3e4..d274fa0 100644 --- a/drivers/pwm/Makefile +++ b/drivers/pwm/Makefile @@ -4,3 +4,4 @@ obj-$(CONFIG_GENERIC_PWM) := pwm.o obj-$(CONFIG_GPIO_PWM) += gpio-pwm.o +obj-$(CONFIG_ATMEL_PWMC) += atmel-pwmc.o diff --git a/drivers/pwm/atmel-pwmc.c b/drivers/pwm/atmel-pwmc.c new file mode 100644 index 0000000..053bb3b --- /dev/null +++ b/drivers/pwm/atmel-pwmc.c @@ -0,0 +1,501 @@ +/* + * Atmel PWMC peripheral driver + * + * Copyright (C) 2011 Bill Gatliff <bgat@xxxxxxxxxxxxxxx> + * Copyright (C) 2007 David Brownell + * + * This program is free software; you may redistribute and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ +#include <linux/module.h> +#include <linux/init.h> +#include <linux/slab.h> +#include <linux/clk.h> +#include <linux/err.h> +#include <linux/io.h> +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <linux/completion.h> +#include <linux/pwm/pwm.h> + +enum { + /* registers common to the PWMC peripheral */ + PWMC_MR = 0, + PWMC_ENA = 4, + PWMC_DIS = 8, + PWMC_SR = 0xc, + PWMC_IER = 0x10, + PWMC_IDR = 0x14, + PWMC_IMR = 0x18, + PWMC_ISR = 0x1c, + + /* registers per each PWMC channel */ + PWMC_CMR = 0, + PWMC_CDTY = 4, + PWMC_CPRD = 8, + PWMC_CCNT = 0xc, + PWMC_CUPD = 0x10, + + /* how to find each channel */ + PWMC_CHAN_BASE = 0x200, + PWMC_CHAN_STRIDE = 0x20, + + /* CMR bits of interest */ + PWMC_CMR_CPD = 10, + PWMC_CMR_CPOL = 9, + PWMC_CMR_CALG = 8, + PWMC_CMR_CPRE_MASK = 0xf, +}; + +/* TODO: NCHAN==4 only for certain AT91-ish parts! */ +#define NCHAN 4 +struct atmel_pwmc { + struct pwm_device p[NCHAN]; + struct pwm_device_ops ops; + spinlock_t lock; + struct completion complete; + void __iomem *iobase; + struct clk *clk; + int irq; + u32 ccnt_mask; +}; + +/* TODO: debugfs attributes for peripheral register values */ + +static inline void pwmc_writel(const struct atmel_pwmc *p, unsigned offset, u32 val) +{ + __raw_writel(val, p->iobase + offset); +} + +static inline u32 pwmc_readl(const struct atmel_pwmc *p, unsigned offset) +{ + return __raw_readl(p->iobase + offset); +} + +static inline void pwmc_chan_writel(const struct pwm_device *p, + u32 offset, u32 val) +{ + const struct atmel_pwmc *ap = pwm_get_drvdata(p); + int chan = p - &ap->p[0]; + + if (PWMC_CMR == offset) + val &= ((1 << PWMC_CMR_CPD) + | (1 << PWMC_CMR_CPOL) + | (1 << PWMC_CMR_CALG) + | (PWMC_CMR_CPRE_MASK)); + else + val &= ap->ccnt_mask; + + pwmc_writel(ap, offset + PWMC_CHAN_BASE + + (chan * PWMC_CHAN_STRIDE), val); +} + +static inline u32 pwmc_chan_readl(const struct pwm_device *p, u32 offset) +{ + const struct atmel_pwmc *ap = pwm_get_drvdata(p); + int chan = p - &ap->p[0]; + + return pwmc_readl(ap, offset + PWMC_CHAN_BASE + + (chan * PWMC_CHAN_STRIDE)); +} + +static inline int __atmel_pwmc_is_on(struct pwm_device *p) +{ + struct atmel_pwmc *ap = pwm_get_drvdata(p); + int chan = p - &ap->p[0]; + + return (pwmc_readl(ap, PWMC_SR) & BIT(chan)) ? 1 : 0; +} + +static inline void __atmel_pwmc_stop(struct pwm_device *p) +{ + struct atmel_pwmc *ap = pwm_get_drvdata(p); + int chan = p - &ap->p[0]; + + pwmc_writel(ap, PWMC_DIS, BIT(chan)); +} + +static inline void __atmel_pwmc_start(struct pwm_device *p) +{ + struct atmel_pwmc *ap = pwm_get_drvdata(p); + int chan = p - &ap->p[0]; + + pwmc_writel(ap, PWMC_ENA, BIT(chan)); +} + +static inline int __atmel_pwmc_config_polarity(struct pwm_device *p, + struct pwm_config *c) +{ + unsigned long cmr = pwmc_chan_readl(p, PWMC_CMR); + + if (c->polarity) + clear_bit(PWMC_CMR_CPOL, &cmr); + else + set_bit(PWMC_CMR_CPOL, &cmr); + pwmc_chan_writel(p, PWMC_CMR, cmr); + p->active_high = c->polarity ? 1 : 0; + + dev_dbg(p->dev, "polarity %d\n", c->polarity); + return 0; +} + +static inline int __atmel_pwmc_config_duty_ticks(struct pwm_device *p, + struct pwm_config *c) +{ + unsigned long cmr, cprd, cpre, cdty; + + cmr = pwmc_chan_readl(p, PWMC_CMR); + cprd = pwmc_chan_readl(p, PWMC_CPRD); + + cpre = cmr & PWMC_CMR_CPRE_MASK; + clear_bit(PWMC_CMR_CPD, &cmr); + + cdty = cprd - (c->duty_ticks >> cpre); + + p->duty_ticks = c->duty_ticks; + + if (__atmel_pwmc_is_on(p)) { + pwmc_chan_writel(p, PWMC_CMR, cmr); + pwmc_chan_writel(p, PWMC_CUPD, cdty); + } else + pwmc_chan_writel(p, PWMC_CDTY, cdty); + + dev_dbg(p->dev, "duty_ticks = %lu cprd = %lx" + " cdty = %lx cpre = %lx\n", p->duty_ticks, + cprd, cdty, cpre); + + return 0; +} + +static inline int __atmel_pwmc_config_period_ticks(struct pwm_device *p, + struct pwm_config *c) +{ + u32 cmr, cprd, cpre; + + cpre = fls(c->period_ticks); + if (cpre < 16) + cpre = 0; + else { + cpre -= 15; + if (cpre > 10) + return -EINVAL; + } + + cmr = pwmc_chan_readl(p, PWMC_CMR); + cmr &= ~PWMC_CMR_CPRE_MASK; + cmr |= cpre; + + cprd = c->period_ticks >> cpre; + + pwmc_chan_writel(p, PWMC_CMR, cmr); + pwmc_chan_writel(p, PWMC_CPRD, cprd); + p->period_ticks = c->period_ticks; + + dev_dbg(p->dev, "period_ticks = %lu cprd = %x cpre = %x\n", + p->period_ticks, cprd, cpre); + + return 0; +} + +static int atmel_pwmc_config_nosleep(struct pwm_device *p, struct pwm_config *c) +{ + struct atmel_pwmc *ap = pwm_get_drvdata(p); + int ret = 0; + unsigned long flags; + int chan = p - &ap->p[0]; + + spin_lock_irqsave(&ap->lock, flags); + + switch (c->config_mask) { + + case BIT(PWM_CONFIG_DUTY_TICKS): + __atmel_pwmc_config_duty_ticks(p, c); + break; + + case BIT(PWM_CONFIG_STOP): + __atmel_pwmc_stop(p); + break; + + case BIT(PWM_CONFIG_START): + __atmel_pwmc_start(p); + break; + + case BIT(PWM_CONFIG_POLARITY): + __atmel_pwmc_config_polarity(p, c); + break; + + case BIT(PWM_CONFIG_ENABLE_CALLBACK): + pwmc_writel(ap, PWMC_IER, BIT(chan)); + break; + + case BIT(PWM_CONFIG_DISABLE_CALLBACK): + pwmc_writel(ap, PWMC_IDR, BIT(chan)); + break; + + default: + ret = -EINVAL; + break; + } + + spin_unlock_irqrestore(&ap->lock, flags); + return ret; +} + +static int atmel_pwmc_stop_sync(struct pwm_device *p) +{ + struct atmel_pwmc *ap = pwm_get_drvdata(p); + int was_on = __atmel_pwmc_is_on(p); + int chan = p - &ap->p[0]; + int ret; + + if (was_on) { + do { + init_completion(&ap->complete); + set_bit(FLAG_STOP, &p->flags); + pwmc_writel(ap, PWMC_IER, BIT(chan)); + + dev_dbg(p->dev, "waiting on stop_sync completion...\n"); + + ret = wait_for_completion_interruptible(&ap->complete); + + dev_dbg(p->dev, "stop_sync complete (%d)\n", ret); + + if (ret) + return ret; + } while (test_bit(FLAG_STOP, &p->flags)); + } + + return was_on; +} + +static int atmel_pwmc_config(struct pwm_device *p, struct pwm_config *c) +{ + int was_on = 0; + + if (p->ops->config_nosleep) { + if (!p->ops->config_nosleep(p, c)) + return 0; + } + + might_sleep(); + + dev_dbg(p->dev, "config_mask %lx\n", c->config_mask); + + was_on = atmel_pwmc_stop_sync(p); + if (was_on < 0) + return was_on; + + if (test_bit(PWM_CONFIG_PERIOD_TICKS, &c->config_mask)) { + __atmel_pwmc_config_period_ticks(p, c); + if (!test_bit(PWM_CONFIG_DUTY_TICKS, &c->config_mask)) { + struct pwm_config d = { + .config_mask = PWM_CONFIG_DUTY_TICKS, + .duty_ticks = p->duty_ticks, + }; + __atmel_pwmc_config_duty_ticks(p, &d); + } + } + + if (test_bit(PWM_CONFIG_DUTY_TICKS, &c->config_mask)) + __atmel_pwmc_config_duty_ticks(p, c); + + if (test_bit(PWM_CONFIG_POLARITY, &c->config_mask)) + __atmel_pwmc_config_polarity(p, c); + + if (test_bit(PWM_CONFIG_START, &c->config_mask) + || (was_on && !test_bit(PWM_CONFIG_STOP, &c->config_mask))) + __atmel_pwmc_start(p); + + return 0; +} + +static int atmel_pwmc_request(struct pwm_device *p) +{ + struct atmel_pwmc *ap = pwm_get_drvdata(p); + unsigned long flags; + + spin_lock_irqsave(&ap->lock, flags); + clk_enable(ap->clk); + p->tick_hz = clk_get_rate(ap->clk); + __atmel_pwmc_stop(p); + spin_unlock_irqrestore(&ap->lock, flags); + + return 0; +} + +static void atmel_pwmc_release(struct pwm_device *p) +{ + struct atmel_pwmc *ap = pwm_get_drvdata(p); + clk_disable(ap->clk); +} + +static irqreturn_t atmel_pwmc_irq(int irq, void *data) +{ + struct atmel_pwmc *ap = data; + struct pwm_device *p; + u32 isr; + int chan; + unsigned long flags; + + spin_lock_irqsave(&ap->lock, flags); + + isr = pwmc_readl(ap, PWMC_ISR); + for (chan = 0; isr; chan++, isr >>= 1) { + p = &ap->p[chan]; + if (isr & 1) { + pwm_callback(p); + if (test_bit(FLAG_STOP, &p->flags)) { + __atmel_pwmc_stop(p); + clear_bit(FLAG_STOP, &p->flags); + } + complete_all(&ap->complete); + } + } + + spin_unlock_irqrestore(&ap->lock, flags); + + return IRQ_HANDLED; +} + +static int __devinit atmel_pwmc_probe(struct platform_device *pdev) +{ + struct atmel_pwmc *ap; + struct resource *r = platform_get_resource(pdev, IORESOURCE_MEM, 0); + int chan; + int ret = 0; + + ap = kzalloc(sizeof *ap, GFP_KERNEL); + if (!ap) { + ret = -ENOMEM; + goto err_atmel_pwmc_alloc; + } + + spin_lock_init(&ap->lock); + init_completion(&ap->complete); + platform_set_drvdata(pdev, ap); + + /* TODO: the datasheets are unclear as to how large CCNT + * actually is across all adopters of the PWMC; sixteen bits + * seems a safe assumption for now */ + ap->ccnt_mask = 0xffffUL; + + ap->ops.request = atmel_pwmc_request; + ap->ops.release = atmel_pwmc_release; + ap->ops.config_nosleep = atmel_pwmc_config_nosleep; + ap->ops.config = atmel_pwmc_config; + + ap->clk = clk_get(&pdev->dev, "pwm_clk"); + if (IS_ERR(ap->clk)) { + ret = -ENODEV; + goto err_clk_get; + } + + ap->iobase = ioremap_nocache(r->start, r->end - r->start + 1); + if (IS_ERR_OR_NULL(ap->iobase)) { + ret = -ENODEV; + goto err_ioremap; + } + + clk_enable(ap->clk); + pwmc_writel(ap, PWMC_DIS, -1); + pwmc_writel(ap, PWMC_IDR, -1); + clk_disable(ap->clk); + + for (chan = 0; chan < NCHAN; chan++) { + ap->p[chan].ops = &ap->ops; + pwm_set_drvdata(&ap->p[chan], ap); + ret = pwm_register(&ap->p[chan], &pdev->dev, chan); + if (ret) + goto err_pwm_register; + } + + ap->irq = platform_get_irq(pdev, 0); + if (ap->irq != -ENXIO) { + ret = request_irq(ap->irq, atmel_pwmc_irq, 0, + dev_name(&pdev->dev), ap); + if (ret) + goto err_request_irq; + } + + return 0; + +err_request_irq: +err_pwm_register: + for (chan = 0; chan < chan; chan++) { + if (pwm_is_registered(&ap->p[chan])) + pwm_unregister(&ap->p[chan]); + } + + iounmap(ap->iobase); +err_ioremap: + clk_put(ap->clk); +err_clk_get: + platform_set_drvdata(pdev, NULL); + kfree(ap); +err_atmel_pwmc_alloc: + dev_dbg(&pdev->dev, "%s: error, returning %d\n", __func__, ret); + return ret; +} + +static int __devexit atmel_pwmc_remove(struct platform_device *pdev) +{ + struct atmel_pwmc *ap = platform_get_drvdata(pdev); + int chan; + + for (chan = 0; chan < NCHAN; chan++) + if (pwm_is_registered(&ap->p[chan])) + pwm_unregister(&ap->p[chan]); + + clk_enable(ap->clk); + pwmc_writel(ap, PWMC_IDR, -1); + pwmc_writel(ap, PWMC_DIS, -1); + clk_disable(ap->clk); + + if (ap->irq != -ENXIO) + free_irq(ap->irq, ap); + + clk_put(ap->clk); + iounmap(ap->iobase); + + kfree(ap); + + return 0; +} + +static struct platform_driver atmel_pwmc_driver = { + .driver = { + /* note: this name has to match the one in at91*_devices.c */ + .name = "atmel_pwmc", + .owner = THIS_MODULE, + }, + .probe = atmel_pwmc_probe, + .remove = __devexit_p(atmel_pwmc_remove), +}; + +static int __init atmel_pwmc_init(void) +{ + return platform_driver_register(&atmel_pwmc_driver); +} +module_init(atmel_pwmc_init); + +static void __exit atmel_pwmc_exit(void) +{ + platform_driver_unregister(&atmel_pwmc_driver); +} +module_exit(atmel_pwmc_exit); + +MODULE_AUTHOR("Bill Gatliff <bgat@xxxxxxxxxxxxxxx>"); +MODULE_DESCRIPTION("Driver for Atmel PWMC peripheral"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:atmel_pwmc"); -- 1.7.2.3 -- To unsubscribe from this list: send the line "unsubscribe linux-embedded" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html