diff options
Diffstat (limited to 'target/linux/generic-2.6/files/drivers')
| -rw-r--r-- | target/linux/generic-2.6/files/drivers/pwm/Kconfig | 31 | ||||
| -rw-r--r-- | target/linux/generic-2.6/files/drivers/pwm/Makefile | 6 | ||||
| -rw-r--r-- | target/linux/generic-2.6/files/drivers/pwm/atmel-pwm.c | 592 | ||||
| -rw-r--r-- | target/linux/generic-2.6/files/drivers/pwm/gpio.c | 298 | ||||
| -rw-r--r-- | target/linux/generic-2.6/files/drivers/pwm/pwm.c | 643 | 
5 files changed, 1570 insertions, 0 deletions
| diff --git a/target/linux/generic-2.6/files/drivers/pwm/Kconfig b/target/linux/generic-2.6/files/drivers/pwm/Kconfig new file mode 100644 index 000000000..1c24e1107 --- /dev/null +++ b/target/linux/generic-2.6/files/drivers/pwm/Kconfig @@ -0,0 +1,31 @@ +# +# PWM infrastructure and devices +# + +menuconfig GENERIC_PWM +	tristate "PWM Support" +	depends on SYSFS +	help +	  This enables PWM support through the generic PWM library. +	  If unsure, say N. + +if GENERIC_PWM + +config ATMEL_PWM +	tristate "Atmel AT32/AT91 PWM support" +	depends on AVR32 || ARCH_AT91 +	help +	  This option enables device driver support for the PWMC +	  peripheral channels found on certain Atmel processors. +	  Pulse Width Modulation is used many for purposes, including +	  software controlled power-efficient backlights on LCD +	  displays, motor control, and waveform generation.  If +	  unsure, say N. + +config GPIO_PWM +       tristate "PWM emulation using GPIO" +       help +         This option enables a single-channel PWM device using +	 a kernel interval timer and a GPIO pin.  If unsure, say N. + +endif diff --git a/target/linux/generic-2.6/files/drivers/pwm/Makefile b/target/linux/generic-2.6/files/drivers/pwm/Makefile new file mode 100644 index 000000000..e8cacc574 --- /dev/null +++ b/target/linux/generic-2.6/files/drivers/pwm/Makefile @@ -0,0 +1,6 @@ +# +# Makefile for pwm devices +# +obj-y := pwm.o +obj-$(CONFIG_ATMEL_PWM)		+= atmel-pwm.o +obj-$(CONFIG_GPIO_PWM)		+= gpio.o diff --git a/target/linux/generic-2.6/files/drivers/pwm/atmel-pwm.c b/target/linux/generic-2.6/files/drivers/pwm/atmel-pwm.c new file mode 100644 index 000000000..158bb922c --- /dev/null +++ b/target/linux/generic-2.6/files/drivers/pwm/atmel-pwm.c @@ -0,0 +1,592 @@ +/* + * drivers/pwm/atmel-pwm.c + * + * Copyright (C) 2010 Bill Gatliff <bgat@billgatliff.com> + * 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/clk.h> +#include <linux/err.h> +#include <linux/io.h> +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <linux/slab.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, +}; + +struct atmel_pwm { +	struct pwm_device pwm; +	spinlock_t lock; +	void __iomem *iobase; +	struct clk *clk; +	u32 *sync_mask; +	int irq; +	u32 ccnt_mask; +}; + +static inline struct atmel_pwm *to_atmel_pwm(const struct pwm_channel *p) +{ +	return container_of(p->pwm, struct atmel_pwm, pwm); +} + +static inline void +pwmc_writel(const struct atmel_pwm *p, +	    unsigned offset, u32 val) +{ +	__raw_writel(val, p->iobase + offset); +} + +static inline u32 +pwmc_readl(const struct atmel_pwm *p, +	   unsigned offset) +{ +	return __raw_readl(p->iobase + offset); +} + +static inline void +pwmc_chan_writel(const struct pwm_channel *p, +		 u32 offset, u32 val) +{ +	const struct atmel_pwm *ap = to_atmel_pwm(p); + +	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 +		    + (p->chan * PWMC_CHAN_STRIDE), val); +} + +static inline u32 +pwmc_chan_readl(const struct pwm_channel *p, +		u32 offset) +{ +	const struct atmel_pwm *ap = to_atmel_pwm(p); + +	return pwmc_readl(ap, offset + PWMC_CHAN_BASE +			  + (p->chan * PWMC_CHAN_STRIDE)); +} + +static inline int +__atmel_pwm_is_on(struct pwm_channel *p) +{ +	struct atmel_pwm *ap = to_atmel_pwm(p); +	return (pwmc_readl(ap, PWMC_SR) & (1 << p->chan)) ? 1 : 0; +} + +static inline void +__atmel_pwm_unsynchronize(struct pwm_channel *p, +			  struct pwm_channel *to_p) +{ +	const struct atmel_pwm *ap = to_atmel_pwm(p); +	int wchan; + +	if (to_p) { +		ap->sync_mask[p->chan] &= ~(1 << to_p->chan); +		ap->sync_mask[to_p->chan] &= ~(1 << p->chan); +		goto done; +	} + +	ap->sync_mask[p->chan] = 0; +	for (wchan = 0; wchan < ap->pwm.nchan; wchan++) +		ap->sync_mask[wchan] &= ~(1 << p->chan); +done: +	dev_dbg(p->pwm->dev, "sync_mask %x\n", ap->sync_mask[p->chan]); +} + +static inline void +__atmel_pwm_synchronize(struct pwm_channel *p, +			struct pwm_channel *to_p) +{ +	const struct atmel_pwm *ap = to_atmel_pwm(p); + +	if (!to_p) +		return; + +	ap->sync_mask[p->chan] |= (1 << to_p->chan); +	ap->sync_mask[to_p->chan] |= (1 << p->chan); + +	dev_dbg(p->pwm->dev, "sync_mask %x\n", ap->sync_mask[p->chan]); +} + +static inline void +__atmel_pwm_stop(struct pwm_channel *p) +{ +	struct atmel_pwm *ap = to_atmel_pwm(p); +	u32 chid = 1 << p->chan; + +	pwmc_writel(ap, PWMC_DIS, ap->sync_mask[p->chan] | chid); +} + +static inline void +__atmel_pwm_start(struct pwm_channel *p) +{ +	struct atmel_pwm *ap = to_atmel_pwm(p); +	u32 chid = 1 << p->chan; + +	pwmc_writel(ap, PWMC_ENA, ap->sync_mask[p->chan] | chid); +} + +static int +atmel_pwm_synchronize(struct pwm_channel *p, +		      struct pwm_channel *to_p) +{ +	unsigned long flags; +	spin_lock_irqsave(&p->lock, flags); +	__atmel_pwm_synchronize(p, to_p); +	spin_unlock_irqrestore(&p->lock, flags); +	return 0; +} + +static int +atmel_pwm_unsynchronize(struct pwm_channel *p, +			struct pwm_channel *from_p) +{ +	unsigned long flags; +	spin_lock_irqsave(&p->lock, flags); +	__atmel_pwm_unsynchronize(p, from_p); +	spin_unlock_irqrestore(&p->lock, flags); +	return 0; +} + +static inline int +__atmel_pwm_config_polarity(struct pwm_channel *p, +			    struct pwm_channel_config *c) +{ +	u32 cmr = pwmc_chan_readl(p, PWMC_CMR); + +	if (c->polarity) +		cmr &= ~BIT(PWMC_CMR_CPOL); +	else +		cmr |= BIT(PWMC_CMR_CPOL); +	pwmc_chan_writel(p, PWMC_CMR, cmr); +	p->active_high = c->polarity ? 1 : 0; + +	dev_dbg(p->pwm->dev, "polarity %d\n", c->polarity); +	return 0; +} + +static inline int +__atmel_pwm_config_duty_ticks(struct pwm_channel *p, +			      struct pwm_channel_config *c) +{ +	u32 cmr, cprd, cpre, cdty; + +	cmr = pwmc_chan_readl(p, PWMC_CMR); +	cprd = pwmc_chan_readl(p, PWMC_CPRD); + +	cpre = cmr & PWMC_CMR_CPRE_MASK; +	cmr &= ~BIT(PWMC_CMR_CPD); + +	cdty = cprd - (c->duty_ticks >> cpre); + +	p->duty_ticks = c->duty_ticks; + +	if (__atmel_pwm_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->pwm->dev, "duty_ticks = %lu cprd = %x" +		" cdty = %x cpre = %x\n", p->duty_ticks, +		cprd, cdty, cpre); + +	return 0; +} + +static inline int +__atmel_pwm_config_period_ticks(struct pwm_channel *p, +				struct pwm_channel_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->pwm->dev, "period_ticks = %lu cprd = %x cpre = %x\n", +		 p->period_ticks, cprd, cpre); + +	return 0; +} + +static int +atmel_pwm_config_nosleep(struct pwm_channel *p, +			 struct pwm_channel_config *c) +{ +	int ret = 0; +	unsigned long flags; + +	spin_lock_irqsave(&p->lock, flags); + +	switch (c->config_mask) { + +	case PWM_CONFIG_DUTY_TICKS: +		__atmel_pwm_config_duty_ticks(p, c); +		break; + +	case PWM_CONFIG_STOP: +		__atmel_pwm_stop(p); +		break; + +	case PWM_CONFIG_START: +		__atmel_pwm_start(p); +		break; + +	case PWM_CONFIG_POLARITY: +		__atmel_pwm_config_polarity(p, c); +		break; + +	default: +		ret = -EINVAL; +		break; +	} + +	spin_unlock_irqrestore(&p->lock, flags); +	return ret; +} + +static int +atmel_pwm_stop_sync(struct pwm_channel *p) +{ +	struct atmel_pwm *ap = container_of(p->pwm, struct atmel_pwm, pwm); +	int ret; +	int was_on = __atmel_pwm_is_on(p); + +	if (was_on) { +		do { +			init_completion(&p->complete); +			set_bit(FLAG_STOP, &p->flags); +			pwmc_writel(ap, PWMC_IER, 1 << p->chan); + +			dev_dbg(p->pwm->dev, "waiting on stop_sync completion...\n"); + +			ret = wait_for_completion_interruptible(&p->complete); + +			dev_dbg(p->pwm->dev, "stop_sync complete (%d)\n", ret); + +			if (ret) +				return ret; +		} while (p->flags & BIT(FLAG_STOP)); +	} + +	return was_on; +} + +static int +atmel_pwm_config(struct pwm_channel *p, +		 struct pwm_channel_config *c) +{ +	int was_on = 0; + +	if (p->pwm->config_nosleep) { +		if (!p->pwm->config_nosleep(p, c)) +			return 0; +	} + +	might_sleep(); + +	dev_dbg(p->pwm->dev, "config_mask %x\n", c->config_mask); + +	was_on = atmel_pwm_stop_sync(p); +	if (was_on < 0) +		return was_on; + +	if (c->config_mask & PWM_CONFIG_PERIOD_TICKS) { +		__atmel_pwm_config_period_ticks(p, c); +		if (!(c->config_mask & PWM_CONFIG_DUTY_TICKS)) { +			struct pwm_channel_config d = { +				.config_mask = PWM_CONFIG_DUTY_TICKS, +				.duty_ticks = p->duty_ticks, +			}; +			__atmel_pwm_config_duty_ticks(p, &d); +		} +	} + +	if (c->config_mask & PWM_CONFIG_DUTY_TICKS) +		__atmel_pwm_config_duty_ticks(p, c); + +	if (c->config_mask & PWM_CONFIG_POLARITY) +		__atmel_pwm_config_polarity(p, c); + +	if ((c->config_mask & PWM_CONFIG_START) +	    || (was_on && !(c->config_mask & PWM_CONFIG_STOP))) +		__atmel_pwm_start(p); + +	return 0; +} + +static void +__atmel_pwm_set_callback(struct pwm_channel *p, +			 pwm_callback_t callback) +{ +	struct atmel_pwm *ap = container_of(p->pwm, struct atmel_pwm, pwm); + +	p->callback = callback; +	pwmc_writel(ap, p->callback ? PWMC_IER : PWMC_IDR, 1 << p->chan); +} + +static int +atmel_pwm_set_callback(struct pwm_channel *p, +		       pwm_callback_t callback) +{ +	struct atmel_pwm *ap = to_atmel_pwm(p); +	unsigned long flags; + +	spin_lock_irqsave(&ap->lock, flags); +	__atmel_pwm_set_callback(p, callback); +	spin_unlock_irqrestore(&ap->lock, flags); + +	return 0; +} + +static int +atmel_pwm_request(struct pwm_channel *p) +{ +	struct atmel_pwm *ap = to_atmel_pwm(p); +	unsigned long flags; + +	spin_lock_irqsave(&p->lock, flags); +	clk_enable(ap->clk); +	p->tick_hz = clk_get_rate(ap->clk); +	__atmel_pwm_unsynchronize(p, NULL); +	__atmel_pwm_stop(p); +	spin_unlock_irqrestore(&p->lock, flags); + +	return 0; +} + +static void +atmel_pwm_free(struct pwm_channel *p) +{ +	struct atmel_pwm *ap = to_atmel_pwm(p); +	clk_disable(ap->clk); +} + +static irqreturn_t +atmel_pwmc_irq(int irq, void *data) +{ +	struct atmel_pwm *ap = data; +	struct pwm_channel *p; +	u32 isr; +	int chid; +	unsigned long flags; + +	spin_lock_irqsave(&ap->lock, flags); + +	isr = pwmc_readl(ap, PWMC_ISR); +	for (chid = 0; isr; chid++, isr >>= 1) { +		p = &ap->pwm.channels[chid]; +		if (isr & 1) { +			if (p->callback) +				p->callback(p); +			if (p->flags & BIT(FLAG_STOP)) { +				__atmel_pwm_stop(p); +				clear_bit(FLAG_STOP, &p->flags); +			} +			complete_all(&p->complete); +		} +	} + +	spin_unlock_irqrestore(&ap->lock, flags); + +	return IRQ_HANDLED; +} + +static int __devinit +atmel_pwmc_probe(struct platform_device *pdev) +{ +	struct atmel_pwm *ap; +	struct resource *r = platform_get_resource(pdev, IORESOURCE_MEM, 0); +	int ret = 0; + +	ap = kzalloc(sizeof(*ap), GFP_KERNEL); +	if (!ap) { +		ret = -ENOMEM; +		goto err_atmel_pwm_alloc; +	} + +	spin_lock_init(&ap->lock); +	platform_set_drvdata(pdev, ap); + +	ap->pwm.dev = &pdev->dev; +	ap->pwm.bus_id = dev_name(&pdev->dev); + +	ap->pwm.nchan = 4; /* TODO: true only for SAM9263 and AP7000 */ +	ap->ccnt_mask = 0xffffUL; /* TODO: true only for SAM9263 */ + +	ap->sync_mask = kzalloc(ap->pwm.nchan * sizeof(u32), GFP_KERNEL); +	if (!ap->sync_mask) { +		ret = -ENOMEM; +		goto err_alloc_sync_masks; +	} + +	ap->pwm.owner = THIS_MODULE; +	ap->pwm.request = atmel_pwm_request; +	ap->pwm.free = atmel_pwm_free; +	ap->pwm.config_nosleep = atmel_pwm_config_nosleep; +	ap->pwm.config = atmel_pwm_config; +	ap->pwm.synchronize = atmel_pwm_synchronize; +	ap->pwm.unsynchronize = atmel_pwm_unsynchronize; +	ap->pwm.set_callback = atmel_pwm_set_callback; + +	ap->clk = clk_get(&pdev->dev, "pwm_clk"); +	if (PTR_ERR(ap->clk)) { +		ret = -ENODEV; +		goto err_clk_get; +	} + +	ap->iobase = ioremap_nocache(r->start, r->end - r->start + 1); +	if (!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); + +	ap->irq = platform_get_irq(pdev, 0); +	if (ap->irq != -ENXIO) { +		ret = request_irq(ap->irq, atmel_pwmc_irq, 0, +				  ap->pwm.bus_id, ap); +		if (ret) +			goto err_request_irq; +	} + +	ret = pwm_register(&ap->pwm); +	if (ret) +		goto err_pwm_register; + +	return 0; + +err_pwm_register: +	if (ap->irq != -ENXIO) +		free_irq(ap->irq, ap); +err_request_irq: +	iounmap(ap->iobase); +err_ioremap: +	clk_put(ap->clk); +err_clk_get: +	platform_set_drvdata(pdev, NULL); +err_alloc_sync_masks: +	kfree(ap); +err_atmel_pwm_alloc: +	return ret; +} + +static int __devexit +atmel_pwmc_remove(struct platform_device *pdev) +{ +	struct atmel_pwm *ap = platform_get_drvdata(pdev); +	int ret; + +	/* TODO: what can we do if this fails? */ +	ret = pwm_unregister(&ap->pwm); + +	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_pwm_driver = { +	.driver = { +		.name = "atmel_pwmc", +		.owner = THIS_MODULE, +	}, +	.probe = atmel_pwmc_probe, +	.remove = __devexit_p(atmel_pwmc_remove), +}; + +static int __init atmel_pwm_init(void) +{ +	return platform_driver_register(&atmel_pwm_driver); +} +module_init(atmel_pwm_init); + +static void __exit atmel_pwm_exit(void) +{ +	platform_driver_unregister(&atmel_pwm_driver); +} +module_exit(atmel_pwm_exit); + +MODULE_AUTHOR("Bill Gatliff <bgat@billgatliff.com>"); +MODULE_DESCRIPTION("Driver for Atmel PWMC peripheral"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:atmel_pwmc"); diff --git a/target/linux/generic-2.6/files/drivers/pwm/gpio.c b/target/linux/generic-2.6/files/drivers/pwm/gpio.c new file mode 100644 index 000000000..dff5d1d62 --- /dev/null +++ b/target/linux/generic-2.6/files/drivers/pwm/gpio.c @@ -0,0 +1,298 @@ +/* + * drivers/pwm/gpio.c + * + * Models a single-channel PWM device using a timer and a GPIO pin. + * + * Copyright (C) 2010 Bill Gatliff <bgat@billgatliff.com> + * + * 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/hrtimer.h> +#include <linux/err.h> +#include <linux/platform_device.h> +#include <linux/workqueue.h> +#include <linux/gpio.h> +#include <linux/slab.h> +#include <linux/pwm/pwm.h> + +struct gpio_pwm { +	struct pwm_device pwm; +	struct hrtimer timer; +	struct work_struct work; +	pwm_callback_t callback; +	int gpio; +	unsigned long polarity : 1; +	unsigned long active : 1; +}; + +static inline struct gpio_pwm *to_gpio_pwm(const struct pwm_channel *p) +{ +	return container_of(p->pwm, struct gpio_pwm, pwm); +} + +static void +gpio_pwm_work (struct work_struct *work) +{ +	struct gpio_pwm *gp = container_of(work, struct gpio_pwm, work); + +	if (gp->active) +		gpio_direction_output(gp->gpio, gp->polarity ? 1 : 0); +	else +		gpio_direction_output(gp->gpio, gp->polarity ? 0 : 1); +} + +static enum hrtimer_restart +gpio_pwm_timeout(struct hrtimer *t) +{ +	struct gpio_pwm *gp = container_of(t, struct gpio_pwm, timer); +	ktime_t tnew; + +	if (unlikely(gp->pwm.channels[0].duty_ticks == 0)) +		gp->active = 0; +	else if (unlikely(gp->pwm.channels[0].duty_ticks +			  == gp->pwm.channels[0].period_ticks)) +		gp->active = 1; +	else +		gp->active ^= 1; + +	if (gpio_cansleep(gp->gpio)) +		schedule_work(&gp->work); +	else +		gpio_pwm_work(&gp->work); + +	if (!gp->active && gp->pwm.channels[0].callback) +		gp->pwm.channels[0].callback(&gp->pwm.channels[0]); + +	if (unlikely(!gp->active && +		     (gp->pwm.channels[0].flags & BIT(FLAG_STOP)))) { +		clear_bit(FLAG_STOP, &gp->pwm.channels[0].flags); +		complete_all(&gp->pwm.channels[0].complete); +		return HRTIMER_NORESTART; +	} + +	if (gp->active) +		tnew = ktime_set(0, gp->pwm.channels[0].duty_ticks); +	else +		tnew = ktime_set(0, gp->pwm.channels[0].period_ticks +				 - gp->pwm.channels[0].duty_ticks); +	hrtimer_start(&gp->timer, tnew, HRTIMER_MODE_REL); + +	return HRTIMER_NORESTART; +} + +static void gpio_pwm_start(struct pwm_channel *p) +{ +	struct gpio_pwm *gp = to_gpio_pwm(p); + +	gp->active = 0; +	gpio_pwm_timeout(&gp->timer); +} + +static int +gpio_pwm_config_nosleep(struct pwm_channel *p, +			struct pwm_channel_config *c) +{ +	struct gpio_pwm *gp = to_gpio_pwm(p); +	int ret = 0; +	unsigned long flags; + +	spin_lock_irqsave(&p->lock, flags); + +	switch (c->config_mask) { + +	case PWM_CONFIG_DUTY_TICKS: +		p->duty_ticks = c->duty_ticks; +		break; + +	case PWM_CONFIG_START: +		if (!hrtimer_active(&gp->timer)) { +			gpio_pwm_start(p); +		} +		break; +	default: +		ret = -EINVAL; +		break; +	} + +	spin_unlock_irqrestore(&p->lock, flags); +	return ret; +} + +static int +gpio_pwm_stop_sync(struct pwm_channel *p) +{ +	struct gpio_pwm *gp = to_gpio_pwm(p); +	int ret; +	int was_on = hrtimer_active(&gp->timer); + +	if (was_on) { +		do { +			init_completion(&p->complete); +			set_bit(FLAG_STOP, &p->flags); +			ret = wait_for_completion_interruptible(&p->complete); +			if (ret) +				return ret; +		} while (p->flags & BIT(FLAG_STOP)); +	} + +	return was_on; +} + +static int +gpio_pwm_config(struct pwm_channel *p, +		struct pwm_channel_config *c) +{ +	struct gpio_pwm *gp = to_gpio_pwm(p); +	int was_on = 0; + +	if (p->pwm->config_nosleep) { +		if (!p->pwm->config_nosleep(p, c)) +			return 0; +	} + +	might_sleep(); + +	was_on = gpio_pwm_stop_sync(p); +	if (was_on < 0) +		return was_on; + +	if (c->config_mask & PWM_CONFIG_PERIOD_TICKS) +		p->period_ticks = c->period_ticks; + +	if (c->config_mask & PWM_CONFIG_DUTY_TICKS) +		p->duty_ticks = c->duty_ticks; + +	if (c->config_mask & PWM_CONFIG_POLARITY) { +		gp->polarity = c->polarity ? 1 : 0; +		p->active_high = gp->polarity; +	} + +	if ((c->config_mask & PWM_CONFIG_START) +	    || (was_on && !(c->config_mask & PWM_CONFIG_STOP))) +		gpio_pwm_start(p); + +	return 0; +} + +static int +gpio_pwm_set_callback(struct pwm_channel *p, +		      pwm_callback_t callback) +{ +	struct gpio_pwm *gp = to_gpio_pwm(p); +	gp->callback = callback; +	return 0; +} + +static int +gpio_pwm_request(struct pwm_channel *p) +{ +	p->tick_hz = 1000000000UL; +	return 0; +} + +static int __devinit +gpio_pwm_probe(struct platform_device *pdev) +{ +	struct gpio_pwm *gp; +	struct gpio_pwm_platform_data *gpd = pdev->dev.platform_data; +	int ret = 0; + +	/* TODO: create configfs entries, so users can assign GPIOs to +	 * PWMs at runtime instead of creating a platform_device +	 * specification and rebuilding their kernel */ + +	if (!gpd || gpio_request(gpd->gpio, dev_name(&pdev->dev))) +		return -EINVAL; + +	gp = kzalloc(sizeof(*gp), GFP_KERNEL); +	if (!gp) { +		ret = -ENOMEM; +		goto err_alloc; +	} + +	platform_set_drvdata(pdev, gp); + +	gp->pwm.dev = &pdev->dev; +	gp->pwm.bus_id = dev_name(&pdev->dev); +	gp->pwm.nchan = 1; +	gp->gpio = gpd->gpio; + +	INIT_WORK(&gp->work, gpio_pwm_work); + +	hrtimer_init(&gp->timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); +	gp->timer.function = gpio_pwm_timeout; + +	gp->pwm.owner = THIS_MODULE; +	gp->pwm.config_nosleep = gpio_pwm_config_nosleep; +	gp->pwm.config = gpio_pwm_config; +	gp->pwm.request = gpio_pwm_request; +	gp->pwm.set_callback = gpio_pwm_set_callback; + +	ret = pwm_register(&gp->pwm); +	if (ret) +		goto err_pwm_register; + +	return 0; + +err_pwm_register: +	platform_set_drvdata(pdev, 0); +	kfree(gp); +err_alloc: +	return ret; +} + +static int __devexit +gpio_pwm_remove(struct platform_device *pdev) +{ +	struct gpio_pwm *gp = platform_get_drvdata(pdev); +	int ret; + +	ret = pwm_unregister(&gp->pwm); +	hrtimer_cancel(&gp->timer); +	cancel_work_sync(&gp->work); +	platform_set_drvdata(pdev, 0); +	kfree(gp); + +	return 0; +} + +static struct platform_driver gpio_pwm_driver = { +	.driver = { +		.name = "gpio_pwm", +		.owner = THIS_MODULE, +	}, +	.probe = gpio_pwm_probe, +	.remove = __devexit_p(gpio_pwm_remove), +}; + +static int __init gpio_pwm_init(void) +{ +	return platform_driver_register(&gpio_pwm_driver); +} +module_init(gpio_pwm_init); + +static void __exit gpio_pwm_exit(void) +{ +	platform_driver_unregister(&gpio_pwm_driver); +} +module_exit(gpio_pwm_exit); + +MODULE_AUTHOR("Bill Gatliff <bgat@billgatliff.com>"); +MODULE_DESCRIPTION("PWM output using GPIO and a high-resolution timer"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:gpio_pwm"); diff --git a/target/linux/generic-2.6/files/drivers/pwm/pwm.c b/target/linux/generic-2.6/files/drivers/pwm/pwm.c new file mode 100644 index 000000000..c1596e9e7 --- /dev/null +++ b/target/linux/generic-2.6/files/drivers/pwm/pwm.c @@ -0,0 +1,643 @@ +/* + * drivers/pwm/pwm.c + * + * Copyright (C) 2010 Bill Gatliff <bgat@billgatliff.com> + * + * 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/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/device.h> +#include <linux/spinlock.h> +#include <linux/fs.h> +#include <linux/completion.h> +#include <linux/workqueue.h> +#include <linux/list.h> +#include <linux/sched.h> +#include <linux/slab.h>    /*kcalloc, kfree since 2.6.34 */ +#include <linux/pwm/pwm.h> + +static int __pwm_create_sysfs(struct pwm_device *pwm); + +static const char *REQUEST_SYSFS = "sysfs"; +static LIST_HEAD(pwm_device_list); +static DEFINE_MUTEX(device_list_mutex); +static struct class pwm_class; +static struct workqueue_struct *pwm_handler_workqueue; + +int pwm_register(struct pwm_device *pwm) +{ +	struct pwm_channel *p; +	int wchan; +	int ret; + +	spin_lock_init(&pwm->list_lock); + +	p = kcalloc(pwm->nchan, sizeof(*p), GFP_KERNEL); +	if (!p) +		return -ENOMEM; + +	for (wchan = 0; wchan < pwm->nchan; wchan++) { +		spin_lock_init(&p[wchan].lock); +		init_completion(&p[wchan].complete); +		p[wchan].chan = wchan; +		p[wchan].pwm = pwm; +	} + +	pwm->channels = p; + +	mutex_lock(&device_list_mutex); + +	list_add_tail(&pwm->list, &pwm_device_list); +	ret = __pwm_create_sysfs(pwm); +	if (ret) { +		mutex_unlock(&device_list_mutex); +		goto err_create_sysfs; +	} + +	mutex_unlock(&device_list_mutex); + +	dev_info(pwm->dev, "%d channel%s\n", pwm->nchan, +		 pwm->nchan > 1 ? "s" : ""); +	return 0; + +err_create_sysfs: +	kfree(p); + +	return ret; +} +EXPORT_SYMBOL(pwm_register); + +static int __match_device(struct device *dev, void *data) +{ +	return dev_get_drvdata(dev) == data; +} + +int pwm_unregister(struct pwm_device *pwm) +{ +	int wchan; +	struct device *dev; + +	mutex_lock(&device_list_mutex); + +	for (wchan = 0; wchan < pwm->nchan; wchan++) { +	  if (pwm->channels[wchan].flags & BIT(FLAG_REQUESTED)) { +			mutex_unlock(&device_list_mutex); +			return -EBUSY; +		} +	} + +	for (wchan = 0; wchan < pwm->nchan; wchan++) { +		dev = class_find_device(&pwm_class, NULL, +					&pwm->channels[wchan], +					__match_device); +		if (dev) { +			put_device(dev); +			device_unregister(dev); +		} +	} + +	kfree(pwm->channels); +	list_del(&pwm->list); +	mutex_unlock(&device_list_mutex); + +	return 0; +} +EXPORT_SYMBOL(pwm_unregister); + +static struct pwm_device * +__pwm_find_device(const char *bus_id) +{ +	struct pwm_device *p; + +	list_for_each_entry(p, &pwm_device_list, list) { +		if (!strcmp(bus_id, p->bus_id)) +			return p; +	} +	return NULL; +} + +static int +__pwm_request_channel(struct pwm_channel *p, +		      const char *requester) +{ +	int ret; + +	if (test_and_set_bit(FLAG_REQUESTED, &p->flags)) +		return -EBUSY; + +	if (p->pwm->request) { +		ret = p->pwm->request(p); +		if (ret) { +			clear_bit(FLAG_REQUESTED, &p->flags); +			return ret; +		} +	} + +	p->requester = requester; +	if (!strcmp(requester, REQUEST_SYSFS)) +		p->pid = current->pid; + +	return 0; +} + +struct pwm_channel * +pwm_request(const char *bus_id, +	    int chan, +	    const char *requester) +{ +	struct pwm_device *p; +	int ret; + +	mutex_lock(&device_list_mutex); + +	p = __pwm_find_device(bus_id); +	if (!p || chan >= p->nchan) +		goto err_no_device; + +	if (!try_module_get(p->owner)) +		goto err_module_get_failed; + +	ret = __pwm_request_channel(&p->channels[chan], requester); +	if (ret) +		goto err_request_failed; + +	mutex_unlock(&device_list_mutex); +	return &p->channels[chan]; + +err_request_failed: +	module_put(p->owner); +err_module_get_failed: +err_no_device: +	mutex_unlock(&device_list_mutex); +	return NULL; +} +EXPORT_SYMBOL(pwm_request); + +void pwm_free(struct pwm_channel *p) +{ +	mutex_lock(&device_list_mutex); + +	if (!test_and_clear_bit(FLAG_REQUESTED, &p->flags)) +		goto done; + +	pwm_stop(p); +	pwm_unsynchronize(p, NULL); +	pwm_set_handler(p, NULL, NULL); + +	if (p->pwm->free) +		p->pwm->free(p); +	module_put(p->pwm->owner); +done: +	mutex_unlock(&device_list_mutex); +} +EXPORT_SYMBOL(pwm_free); + +unsigned long pwm_ns_to_ticks(struct pwm_channel *p, +			      unsigned long nsecs) +{ +	unsigned long long ticks; + +	ticks = nsecs; +	ticks *= p->tick_hz; +	do_div(ticks, 1000000000); +	return ticks; +} +EXPORT_SYMBOL(pwm_ns_to_ticks); + +unsigned long pwm_ticks_to_ns(struct pwm_channel *p, +			      unsigned long ticks) +{ +	unsigned long long ns; + +	if (!p->tick_hz) +		return 0; + +	ns = ticks; +	ns *= 1000000000UL; +	do_div(ns, p->tick_hz); +	return ns; +} +EXPORT_SYMBOL(pwm_ticks_to_ns); + +static void +pwm_config_ns_to_ticks(struct pwm_channel *p, +		       struct pwm_channel_config *c) +{ +	if (c->config_mask & PWM_CONFIG_PERIOD_NS) { +		c->period_ticks = pwm_ns_to_ticks(p, c->period_ns); +		c->config_mask &= ~PWM_CONFIG_PERIOD_NS; +		c->config_mask |= PWM_CONFIG_PERIOD_TICKS; +	} + +	if (c->config_mask & PWM_CONFIG_DUTY_NS) { +		c->duty_ticks = pwm_ns_to_ticks(p, c->duty_ns); +		c->config_mask &= ~PWM_CONFIG_DUTY_NS; +		c->config_mask |= PWM_CONFIG_DUTY_TICKS; +	} +} + +static void +pwm_config_percent_to_ticks(struct pwm_channel *p, +			    struct pwm_channel_config *c) +{ +	if (c->config_mask & PWM_CONFIG_DUTY_PERCENT) { +		if (c->config_mask & PWM_CONFIG_PERIOD_TICKS) +			c->duty_ticks = c->period_ticks; +		else +			c->duty_ticks = p->period_ticks; + +		c->duty_ticks *= c->duty_percent; +		c->duty_ticks /= 100; +		c->config_mask &= ~PWM_CONFIG_DUTY_PERCENT; +		c->config_mask |= PWM_CONFIG_DUTY_TICKS; +	} +} + +int pwm_config_nosleep(struct pwm_channel *p, +		       struct pwm_channel_config *c) +{ +	if (!p->pwm->config_nosleep) +		return -EINVAL; + +	pwm_config_ns_to_ticks(p, c); +	pwm_config_percent_to_ticks(p, c); + +	return p->pwm->config_nosleep(p, c); +} +EXPORT_SYMBOL(pwm_config_nosleep); + +int pwm_config(struct pwm_channel *p, +	       struct pwm_channel_config *c) +{ +	int ret = 0; + +	if (unlikely(!p->pwm->config)) +		return -EINVAL; + +	pwm_config_ns_to_ticks(p, c); +	pwm_config_percent_to_ticks(p, c); + +	switch (c->config_mask & (PWM_CONFIG_PERIOD_TICKS +				  | PWM_CONFIG_DUTY_TICKS)) { +	case PWM_CONFIG_PERIOD_TICKS: +		if (p->duty_ticks > c->period_ticks) { +			ret = -EINVAL; +			goto err; +		} +		break; +	case PWM_CONFIG_DUTY_TICKS: +		if (p->period_ticks < c->duty_ticks) { +			ret = -EINVAL; +			goto err; +		} +		break; +	case PWM_CONFIG_DUTY_TICKS | PWM_CONFIG_PERIOD_TICKS: +		if (c->duty_ticks > c->period_ticks) { +			ret = -EINVAL; +			goto err; +		} +		break; +	default: +		break; +	} + +err: +	dev_dbg(p->pwm->dev, "%s: config_mask %d period_ticks %lu duty_ticks %lu" +		" polarity %d duty_ns %lu period_ns %lu duty_percent %d\n", +		__func__, c->config_mask, c->period_ticks, c->duty_ticks, +		c->polarity, c->duty_ns, c->period_ns, c->duty_percent); + +	if (ret) +		return ret; +	return p->pwm->config(p, c); +} +EXPORT_SYMBOL(pwm_config); + +int pwm_set_period_ns(struct pwm_channel *p, +		      unsigned long period_ns) +{ +	struct pwm_channel_config c = { +		.config_mask = PWM_CONFIG_PERIOD_TICKS, +		.period_ticks = pwm_ns_to_ticks(p, period_ns), +	}; + +	return pwm_config(p, &c); +} +EXPORT_SYMBOL(pwm_set_period_ns); + +unsigned long pwm_get_period_ns(struct pwm_channel *p) +{ +	return pwm_ticks_to_ns(p, p->period_ticks); +} +EXPORT_SYMBOL(pwm_get_period_ns); + +int pwm_set_duty_ns(struct pwm_channel *p, +		    unsigned long duty_ns) +{ +	struct pwm_channel_config c = { +		.config_mask = PWM_CONFIG_DUTY_TICKS, +		.duty_ticks = pwm_ns_to_ticks(p, duty_ns), +	}; +	return pwm_config(p, &c); +} +EXPORT_SYMBOL(pwm_set_duty_ns); + +unsigned long pwm_get_duty_ns(struct pwm_channel *p) +{ +	return pwm_ticks_to_ns(p, p->duty_ticks); +} +EXPORT_SYMBOL(pwm_get_duty_ns); + +int pwm_set_duty_percent(struct pwm_channel *p, +			 int percent) +{ +	struct pwm_channel_config c = { +		.config_mask = PWM_CONFIG_DUTY_PERCENT, +		.duty_percent = percent, +	}; +	return pwm_config(p, &c); +} +EXPORT_SYMBOL(pwm_set_duty_percent); + +int pwm_set_polarity(struct pwm_channel *p, +		     int active_high) +{ +	struct pwm_channel_config c = { +		.config_mask = PWM_CONFIG_POLARITY, +		.polarity = active_high, +	}; +	return pwm_config(p, &c); +} +EXPORT_SYMBOL(pwm_set_polarity); + +int pwm_start(struct pwm_channel *p) +{ +	struct pwm_channel_config c = { +		.config_mask = PWM_CONFIG_START, +	}; +	return pwm_config(p, &c); +} +EXPORT_SYMBOL(pwm_start); + +int pwm_stop(struct pwm_channel *p) +{ +	struct pwm_channel_config c = { +		.config_mask = PWM_CONFIG_STOP, +	}; +	return pwm_config(p, &c); +} +EXPORT_SYMBOL(pwm_stop); + +int pwm_synchronize(struct pwm_channel *p, +		    struct pwm_channel *to_p) +{ +	if (p->pwm != to_p->pwm) { +		/* TODO: support cross-device synchronization */ +		return -EINVAL; +	} + +	if (!p->pwm->synchronize) +		return -EINVAL; + +	return p->pwm->synchronize(p, to_p); +} +EXPORT_SYMBOL(pwm_synchronize); + +int pwm_unsynchronize(struct pwm_channel *p, +		      struct pwm_channel *from_p) +{ +	if (from_p && (p->pwm != from_p->pwm)) { +		/* TODO: support cross-device synchronization */ +		return -EINVAL; +	} + +	if (!p->pwm->unsynchronize) +		return -EINVAL; + +	return p->pwm->unsynchronize(p, from_p); +} +EXPORT_SYMBOL(pwm_unsynchronize); + +static void pwm_handler(struct work_struct *w) +{ +	struct pwm_channel *p = container_of(w, struct pwm_channel, +					     handler_work); +	if (p->handler && p->handler(p, p->handler_data)) +		pwm_stop(p); +} + +static void __pwm_callback(struct pwm_channel *p) +{ +	queue_work(pwm_handler_workqueue, &p->handler_work); +	dev_dbg(p->pwm->dev, "handler %p scheduled with data %p\n", +		p->handler, p->handler_data); +} + +int pwm_set_handler(struct pwm_channel *p, +		    pwm_handler_t handler, +		    void *data) +{ +	if (p->pwm->set_callback) { +		p->handler_data = data; +		p->handler = handler; +		INIT_WORK(&p->handler_work, pwm_handler); +		return p->pwm->set_callback(p, handler ? __pwm_callback : NULL); +	} +	return -EINVAL; +} +EXPORT_SYMBOL(pwm_set_handler); + +static ssize_t pwm_run_store(struct device *dev, +			     struct device_attribute *attr, +			     const char *buf, +			     size_t len) +{ +	struct pwm_channel *p = dev_get_drvdata(dev); +	if (sysfs_streq(buf, "1")) +		pwm_start(p); +	else if (sysfs_streq(buf, "0")) +		pwm_stop(p); +	return len; +} +static DEVICE_ATTR(run, 0200, NULL, pwm_run_store); + +static ssize_t pwm_duty_ns_show(struct device *dev, +				struct device_attribute *attr, +				char *buf) +{ +	struct pwm_channel *p = dev_get_drvdata(dev); +	return sprintf(buf, "%lu\n", pwm_get_duty_ns(p)); +} + +static ssize_t pwm_duty_ns_store(struct device *dev, +				 struct device_attribute *attr, +				 const char *buf, +				 size_t len) +{ +	unsigned long duty_ns; +	struct pwm_channel *p = dev_get_drvdata(dev); + +	if (1 == sscanf(buf, "%lu", &duty_ns)) +		pwm_set_duty_ns(p, duty_ns); +	return len; +} +static DEVICE_ATTR(duty_ns, 0644, pwm_duty_ns_show, pwm_duty_ns_store); + +static ssize_t pwm_period_ns_show(struct device *dev, +				  struct device_attribute *attr, +				  char *buf) +{ +	struct pwm_channel *p = dev_get_drvdata(dev); +	return sprintf(buf, "%lu\n", pwm_get_period_ns(p)); +} + +static ssize_t pwm_period_ns_store(struct device *dev, +				   struct device_attribute *attr, +				   const char *buf, +				   size_t len) +{ +	unsigned long period_ns; +	struct pwm_channel *p = dev_get_drvdata(dev); + +	if (1 == sscanf(buf, "%lu", &period_ns)) +		pwm_set_period_ns(p, period_ns); +	return len; +} +static DEVICE_ATTR(period_ns, 0644, pwm_period_ns_show, pwm_period_ns_store); + +static ssize_t pwm_polarity_show(struct device *dev, +				 struct device_attribute *attr, +				 char *buf) +{ +	struct pwm_channel *p = dev_get_drvdata(dev); +	return sprintf(buf, "%d\n", p->active_high ? 1 : 0); +} + +static ssize_t pwm_polarity_store(struct device *dev, +				  struct device_attribute *attr, +				  const char *buf, +				  size_t len) +{ +	int polarity; +	struct pwm_channel *p = dev_get_drvdata(dev); + +	if (1 == sscanf(buf, "%d", &polarity)) +		pwm_set_polarity(p, polarity); +	return len; +} +static DEVICE_ATTR(polarity, 0644, pwm_polarity_show, pwm_polarity_store); + +static ssize_t pwm_request_show(struct device *dev, +				struct device_attribute *attr, +				char *buf) +{ +	struct pwm_channel *p = dev_get_drvdata(dev); +	mutex_lock(&device_list_mutex); +	__pwm_request_channel(p, REQUEST_SYSFS); +	mutex_unlock(&device_list_mutex); + +	if (p->pid) +		return sprintf(buf, "%s %d\n", p->requester, p->pid); +	else +		return sprintf(buf, "%s\n", p->requester); +} + +static ssize_t pwm_request_store(struct device *dev, +				 struct device_attribute *attr, +				 const char *buf, +				 size_t len) +{ +	struct pwm_channel *p = dev_get_drvdata(dev); +	pwm_free(p); +	return len; +} +static DEVICE_ATTR(request, 0644, pwm_request_show, pwm_request_store); + +static const struct attribute *pwm_attrs[] = +{ +	&dev_attr_run.attr, +	&dev_attr_polarity.attr, +	&dev_attr_duty_ns.attr, +	&dev_attr_period_ns.attr, +	&dev_attr_request.attr, +	NULL, +}; + +static const struct attribute_group pwm_device_attr_group = { +	.attrs = (struct attribute **)pwm_attrs, +}; + +static int __pwm_create_sysfs(struct pwm_device *pwm) +{ +	int ret = 0; +	struct device *dev; +	int wchan; + +	for (wchan = 0; wchan < pwm->nchan; wchan++) { +		dev = device_create(&pwm_class, pwm->dev, MKDEV(0, 0), +				    pwm->channels + wchan, +				    "%s:%d", pwm->bus_id, wchan); +		if (!dev) +			goto err_dev_create; +		ret = sysfs_create_group(&dev->kobj, &pwm_device_attr_group); +		if (ret) +			goto err_dev_create; +	} + +	return ret; + +err_dev_create: +	for (wchan = 0; wchan < pwm->nchan; wchan++) { +		dev = class_find_device(&pwm_class, NULL, +					&pwm->channels[wchan], +					__match_device); +		if (dev) { +			put_device(dev); +			device_unregister(dev); +		} +	} + +	return ret; +} + +static struct class_attribute pwm_class_attrs[] = { +	__ATTR_NULL, +}; + +static struct class pwm_class = { +	.name = "pwm", +	.owner = THIS_MODULE, + +	.class_attrs = pwm_class_attrs, +}; + +static int __init pwm_init(void) +{ +	int ret; + +	/* TODO: how to deal with devices that register very early? */ +	pr_err("%s\n", __func__); +	ret = class_register(&pwm_class); +	if (ret < 0) +		return ret; + +	pwm_handler_workqueue = create_workqueue("pwmd"); + +	return 0; +} +postcore_initcall(pwm_init); | 
