diff options
Diffstat (limited to 'drivers/pwm/pwm-sti.c')
| -rw-r--r-- | drivers/pwm/pwm-sti.c | 418 | 
1 files changed, 418 insertions, 0 deletions
diff --git a/drivers/pwm/pwm-sti.c b/drivers/pwm/pwm-sti.c new file mode 100644 index 000000000000..b95115cdaea7 --- /dev/null +++ b/drivers/pwm/pwm-sti.c @@ -0,0 +1,418 @@ +/* + * PWM device driver for ST SoCs. + * Author: Ajit Pal Singh <[email protected]> + * + * Copyright (C) 2013-2014 STMicroelectronics (R&D) Limited + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +#include <linux/clk.h> +#include <linux/math64.h> +#include <linux/mfd/syscon.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/pwm.h> +#include <linux/regmap.h> +#include <linux/slab.h> +#include <linux/time.h> + +#define STI_DS_REG(ch)	(4 * (ch))	/* Channel's Duty Cycle register */ +#define STI_PWMCR	0x50		/* Control/Config register */ +#define STI_INTEN	0x54		/* Interrupt Enable/Disable register */ +#define PWM_PRESCALE_LOW_MASK		0x0f +#define PWM_PRESCALE_HIGH_MASK		0xf0 + +/* Regfield IDs */ +enum { +	PWMCLK_PRESCALE_LOW, +	PWMCLK_PRESCALE_HIGH, +	PWM_EN, +	PWM_INT_EN, + +	/* Keep last */ +	MAX_REGFIELDS +}; + +struct sti_pwm_compat_data { +	const struct reg_field *reg_fields; +	unsigned int num_chan; +	unsigned int max_pwm_cnt; +	unsigned int max_prescale; +}; + +struct sti_pwm_chip { +	struct device *dev; +	struct clk *clk; +	unsigned long clk_rate; +	struct regmap *regmap; +	struct sti_pwm_compat_data *cdata; +	struct regmap_field *prescale_low; +	struct regmap_field *prescale_high; +	struct regmap_field *pwm_en; +	struct regmap_field *pwm_int_en; +	struct pwm_chip chip; +	struct pwm_device *cur; +	unsigned int en_count; +	struct mutex sti_pwm_lock; /* To sync between enable/disable calls */ +	void __iomem *mmio; +}; + +static const struct reg_field sti_pwm_regfields[MAX_REGFIELDS] = { +	[PWMCLK_PRESCALE_LOW]	= REG_FIELD(STI_PWMCR, 0, 3), +	[PWMCLK_PRESCALE_HIGH]	= REG_FIELD(STI_PWMCR, 11, 14), +	[PWM_EN]		= REG_FIELD(STI_PWMCR, 9, 9), +	[PWM_INT_EN]		= REG_FIELD(STI_INTEN, 0, 0), +}; + +static inline struct sti_pwm_chip *to_sti_pwmchip(struct pwm_chip *chip) +{ +	return container_of(chip, struct sti_pwm_chip, chip); +} + +/* + * Calculate the prescaler value corresponding to the period. + */ +static int sti_pwm_get_prescale(struct sti_pwm_chip *pc, unsigned long period, +				unsigned int *prescale) +{ +	struct sti_pwm_compat_data *cdata = pc->cdata; +	unsigned long val; +	unsigned int ps; + +	/* +	 * prescale = ((period_ns * clk_rate) / (10^9 * (max_pwm_count + 1)) - 1 +	 */ +	val = NSEC_PER_SEC / pc->clk_rate; +	val *= cdata->max_pwm_cnt + 1; + +	if (period % val) { +		return -EINVAL; +	} else { +		ps  = period / val - 1; +		if (ps > cdata->max_prescale) +			return -EINVAL; +	} +	*prescale = ps; + +	return 0; +} + +/* Calculate the number of PWM devices configured with a period. */ +static unsigned int sti_pwm_count_configured(struct pwm_chip *chip) +{ +	struct pwm_device *pwm; +	unsigned int ncfg = 0; +	unsigned int i; + +	for (i = 0; i < chip->npwm; i++) { +		pwm = &chip->pwms[i]; +		if (test_bit(PWMF_REQUESTED, &pwm->flags)) { +			if (pwm_get_period(pwm)) +				ncfg++; +		} +	} + +	return ncfg; +} + +/* + * For STiH4xx PWM IP, the PWM period is fixed to 256 local clock cycles. + * The only way to change the period (apart from changing the PWM input clock) + * is to change the PWM clock prescaler. + * The prescaler is of 8 bits, so 256 prescaler values and hence + * 256 possible period values are supported (for a particular clock rate). + * The requested period will be applied only if it matches one of these + * 256 values. + */ +static int sti_pwm_config(struct pwm_chip *chip, struct pwm_device *pwm, +			 int duty_ns, int period_ns) +{ +	struct sti_pwm_chip *pc = to_sti_pwmchip(chip); +	struct sti_pwm_compat_data *cdata = pc->cdata; +	struct pwm_device *cur = pc->cur; +	struct device *dev = pc->dev; +	unsigned int prescale = 0, pwmvalx; +	int ret; +	unsigned int ncfg; +	bool period_same = false; + +	ncfg = sti_pwm_count_configured(chip); +	if (ncfg) +		period_same = (period_ns == pwm_get_period(cur)); + +	/* Allow configuration changes if one of the +	 * following conditions satisfy. +	 * 1. No channels have been configured. +	 * 2. Only one channel has been configured and the new request +	 *    is for the same channel. +	 * 3. Only one channel has been configured and the new request is +	 *    for a new channel and period of the new channel is same as +	 *    the current configured period. +	 * 4. More than one channels are configured and period of the new +	 *    requestis the same as the current period. +	 */ +	if (!ncfg || +	    ((ncfg == 1) && (pwm->hwpwm == cur->hwpwm)) || +	    ((ncfg == 1) && (pwm->hwpwm != cur->hwpwm) && period_same) || +	    ((ncfg > 1) && period_same)) { +		/* Enable clock before writing to PWM registers. */ +		ret = clk_enable(pc->clk); +		if (ret) +			return ret; + +		if (!period_same) { +			ret = sti_pwm_get_prescale(pc, period_ns, &prescale); +			if (ret) +				goto clk_dis; + +			ret = +			regmap_field_write(pc->prescale_low, +					   prescale & PWM_PRESCALE_LOW_MASK); +			if (ret) +				goto clk_dis; + +			ret = +			regmap_field_write(pc->prescale_high, +				(prescale & PWM_PRESCALE_HIGH_MASK) >> 4); +			if (ret) +				goto clk_dis; +		} + +		/* +		 * When PWMVal == 0, PWM pulse = 1 local clock cycle. +		 * When PWMVal == max_pwm_count, +		 * PWM pulse = (max_pwm_count + 1) local cycles, +		 * that is continuous pulse: signal never goes low. +		 */ +		pwmvalx = cdata->max_pwm_cnt * duty_ns / period_ns; + +		ret = regmap_write(pc->regmap, STI_DS_REG(pwm->hwpwm), pwmvalx); +		if (ret) +			goto clk_dis; + +		ret = regmap_field_write(pc->pwm_int_en, 0); + +		pc->cur = pwm; + +		dev_dbg(dev, "prescale:%u, period:%i, duty:%i, pwmvalx:%u\n", +			prescale, period_ns, duty_ns, pwmvalx); +	} else { +		return -EINVAL; +	} + +clk_dis: +	clk_disable(pc->clk); +	return ret; +} + +static int sti_pwm_enable(struct pwm_chip *chip, struct pwm_device *pwm) +{ +	struct sti_pwm_chip *pc = to_sti_pwmchip(chip); +	struct device *dev = pc->dev; +	int ret = 0; + +	/* +	 * Since we have a common enable for all PWM channels, +	 * do not enable if already enabled. +	 */ +	mutex_lock(&pc->sti_pwm_lock); +	if (!pc->en_count) { +		ret = clk_enable(pc->clk); +		if (ret) +			goto out; + +		ret = regmap_field_write(pc->pwm_en, 1); +		if (ret) { +			dev_err(dev, "failed to enable PWM device:%d\n", +				pwm->hwpwm); +			goto out; +		} +	} +	pc->en_count++; +out: +	mutex_unlock(&pc->sti_pwm_lock); +	return ret; +} + +static void sti_pwm_disable(struct pwm_chip *chip, struct pwm_device *pwm) +{ +	struct sti_pwm_chip *pc = to_sti_pwmchip(chip); + +	mutex_lock(&pc->sti_pwm_lock); +	if (--pc->en_count) { +		mutex_unlock(&pc->sti_pwm_lock); +		return; +	} +	regmap_field_write(pc->pwm_en, 0); + +	clk_disable(pc->clk); +	mutex_unlock(&pc->sti_pwm_lock); +} + +static const struct pwm_ops sti_pwm_ops = { +	.config = sti_pwm_config, +	.enable = sti_pwm_enable, +	.disable = sti_pwm_disable, +	.owner = THIS_MODULE, +}; + +static int sti_pwm_probe_dt(struct sti_pwm_chip *pc) +{ +	struct device *dev = pc->dev; +	const struct reg_field *reg_fields; +	struct device_node *np = dev->of_node; +	struct sti_pwm_compat_data *cdata = pc->cdata; +	u32 num_chan; + +	of_property_read_u32(np, "st,pwm-num-chan", &num_chan); +	if (num_chan) +		cdata->num_chan = num_chan; + +	reg_fields = cdata->reg_fields; + +	pc->prescale_low = devm_regmap_field_alloc(dev, pc->regmap, +					reg_fields[PWMCLK_PRESCALE_LOW]); +	if (IS_ERR(pc->prescale_low)) +		return PTR_ERR(pc->prescale_low); + +	pc->prescale_high = devm_regmap_field_alloc(dev, pc->regmap, +					reg_fields[PWMCLK_PRESCALE_HIGH]); +	if (IS_ERR(pc->prescale_high)) +		return PTR_ERR(pc->prescale_high); + +	pc->pwm_en = devm_regmap_field_alloc(dev, pc->regmap, +					     reg_fields[PWM_EN]); +	if (IS_ERR(pc->pwm_en)) +		return PTR_ERR(pc->pwm_en); + +	pc->pwm_int_en = devm_regmap_field_alloc(dev, pc->regmap, +						 reg_fields[PWM_INT_EN]); +	if (IS_ERR(pc->pwm_int_en)) +		return PTR_ERR(pc->pwm_int_en); + +	return 0; +} + +static const struct regmap_config sti_pwm_regmap_config = { +	.reg_bits = 32, +	.val_bits = 32, +	.reg_stride = 4, +}; + +static int sti_pwm_probe(struct platform_device *pdev) +{ +	struct device *dev = &pdev->dev; +	struct sti_pwm_compat_data *cdata; +	struct sti_pwm_chip *pc; +	struct resource *res; +	int ret; + +	pc = devm_kzalloc(dev, sizeof(*pc), GFP_KERNEL); +	if (!pc) +		return -ENOMEM; + +	cdata = devm_kzalloc(dev, sizeof(*cdata), GFP_KERNEL); +	if (!cdata) +		return -ENOMEM; + +	res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + +	pc->mmio = devm_ioremap_resource(dev, res); +	if (IS_ERR(pc->mmio)) +		return PTR_ERR(pc->mmio); + +	pc->regmap = devm_regmap_init_mmio(dev, pc->mmio, +					   &sti_pwm_regmap_config); +	if (IS_ERR(pc->regmap)) +		return PTR_ERR(pc->regmap); + +	/* +	 * Setup PWM data with default values: some values could be replaced +	 * with specific ones provided from Device Tree. +	 */ +	cdata->reg_fields   = &sti_pwm_regfields[0]; +	cdata->max_prescale = 0xff; +	cdata->max_pwm_cnt  = 255; +	cdata->num_chan     = 1; + +	pc->cdata = cdata; +	pc->dev = dev; +	pc->en_count = 0; +	mutex_init(&pc->sti_pwm_lock); + +	ret = sti_pwm_probe_dt(pc); +	if (ret) +		return ret; + +	pc->clk = of_clk_get_by_name(dev->of_node, "pwm"); +	if (IS_ERR(pc->clk)) { +		dev_err(dev, "failed to get PWM clock\n"); +		return PTR_ERR(pc->clk); +	} + +	pc->clk_rate = clk_get_rate(pc->clk); +	if (!pc->clk_rate) { +		dev_err(dev, "failed to get clock rate\n"); +		return -EINVAL; +	} + +	ret = clk_prepare(pc->clk); +	if (ret) { +		dev_err(dev, "failed to prepare clock\n"); +		return ret; +	} + +	pc->chip.dev = dev; +	pc->chip.ops = &sti_pwm_ops; +	pc->chip.base = -1; +	pc->chip.npwm = pc->cdata->num_chan; +	pc->chip.can_sleep = true; + +	ret = pwmchip_add(&pc->chip); +	if (ret < 0) { +		clk_unprepare(pc->clk); +		return ret; +	} + +	platform_set_drvdata(pdev, pc); + +	return 0; +} + +static int sti_pwm_remove(struct platform_device *pdev) +{ +	struct sti_pwm_chip *pc = platform_get_drvdata(pdev); +	unsigned int i; + +	for (i = 0; i < pc->cdata->num_chan; i++) +		pwm_disable(&pc->chip.pwms[i]); + +	clk_unprepare(pc->clk); + +	return pwmchip_remove(&pc->chip); +} + +static const struct of_device_id sti_pwm_of_match[] = { +	{ .compatible = "st,sti-pwm", }, +	{ /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, sti_pwm_of_match); + +static struct platform_driver sti_pwm_driver = { +	.driver = { +		.name = "sti-pwm", +		.of_match_table = sti_pwm_of_match, +	}, +	.probe = sti_pwm_probe, +	.remove = sti_pwm_remove, +}; +module_platform_driver(sti_pwm_driver); + +MODULE_AUTHOR("Ajit Pal Singh <[email protected]>"); +MODULE_DESCRIPTION("STMicroelectronics ST PWM driver"); +MODULE_LICENSE("GPL");  |