diff options
Diffstat (limited to 'drivers/pwm/pwm-iqs620a.c')
| -rw-r--r-- | drivers/pwm/pwm-iqs620a.c | 270 | 
1 files changed, 270 insertions, 0 deletions
diff --git a/drivers/pwm/pwm-iqs620a.c b/drivers/pwm/pwm-iqs620a.c new file mode 100644 index 000000000000..674f0e238ba0 --- /dev/null +++ b/drivers/pwm/pwm-iqs620a.c @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Azoteq IQS620A PWM Generator + * + * Copyright (C) 2019 Jeff LaBundy <jeff@labundy.com> + * + * Limitations: + * - The period is fixed to 1 ms and is generated continuously despite changes + *   to the duty cycle or enable/disable state. + * - Changes to the duty cycle or enable/disable state take effect immediately + *   and may result in a glitch during the period in which the change is made. + * - The device cannot generate a 0% duty cycle. For duty cycles below 1 / 256 + *   ms, the output is disabled and relies upon an external pull-down resistor + *   to hold the GPIO3/LTX pin low. + */ + +#include <linux/device.h> +#include <linux/kernel.h> +#include <linux/mfd/iqs62x.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/notifier.h> +#include <linux/platform_device.h> +#include <linux/pwm.h> +#include <linux/regmap.h> +#include <linux/slab.h> + +#define IQS620_PWR_SETTINGS			0xD2 +#define IQS620_PWR_SETTINGS_PWM_OUT		BIT(7) + +#define IQS620_PWM_DUTY_CYCLE			0xD8 + +#define IQS620_PWM_PERIOD_NS			1000000 + +struct iqs620_pwm_private { +	struct iqs62x_core *iqs62x; +	struct pwm_chip chip; +	struct notifier_block notifier; +	struct mutex lock; +	bool out_en; +	u8 duty_val; +}; + +static int iqs620_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm, +			    const struct pwm_state *state) +{ +	struct iqs620_pwm_private *iqs620_pwm; +	struct iqs62x_core *iqs62x; +	int duty_scale, ret; + +	if (state->polarity != PWM_POLARITY_NORMAL) +		return -ENOTSUPP; + +	if (state->period < IQS620_PWM_PERIOD_NS) +		return -EINVAL; + +	iqs620_pwm = container_of(chip, struct iqs620_pwm_private, chip); +	iqs62x = iqs620_pwm->iqs62x; + +	/* +	 * The duty cycle generated by the device is calculated as follows: +	 * +	 * duty_cycle = (IQS620_PWM_DUTY_CYCLE + 1) / 256 * 1 ms +	 * +	 * ...where IQS620_PWM_DUTY_CYCLE is a register value between 0 and 255 +	 * (inclusive). Therefore the lowest duty cycle the device can generate +	 * while the output is enabled is 1 / 256 ms. +	 * +	 * For lower duty cycles (e.g. 0), the PWM output is simply disabled to +	 * allow an external pull-down resistor to hold the GPIO3/LTX pin low. +	 */ +	duty_scale = state->duty_cycle * 256 / IQS620_PWM_PERIOD_NS; + +	mutex_lock(&iqs620_pwm->lock); + +	if (!state->enabled || !duty_scale) { +		ret = regmap_update_bits(iqs62x->regmap, IQS620_PWR_SETTINGS, +					 IQS620_PWR_SETTINGS_PWM_OUT, 0); +		if (ret) +			goto err_mutex; +	} + +	if (duty_scale) { +		u8 duty_val = min(duty_scale - 1, 0xFF); + +		ret = regmap_write(iqs62x->regmap, IQS620_PWM_DUTY_CYCLE, +				   duty_val); +		if (ret) +			goto err_mutex; + +		iqs620_pwm->duty_val = duty_val; +	} + +	if (state->enabled && duty_scale) { +		ret = regmap_update_bits(iqs62x->regmap, IQS620_PWR_SETTINGS, +					 IQS620_PWR_SETTINGS_PWM_OUT, 0xFF); +		if (ret) +			goto err_mutex; +	} + +	iqs620_pwm->out_en = state->enabled; + +err_mutex: +	mutex_unlock(&iqs620_pwm->lock); + +	return ret; +} + +static void iqs620_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm, +				 struct pwm_state *state) +{ +	struct iqs620_pwm_private *iqs620_pwm; + +	iqs620_pwm = container_of(chip, struct iqs620_pwm_private, chip); + +	mutex_lock(&iqs620_pwm->lock); + +	/* +	 * Since the device cannot generate a 0% duty cycle, requests to do so +	 * cause subsequent calls to iqs620_pwm_get_state to report the output +	 * as disabled with duty cycle equal to that which was in use prior to +	 * the request. This is not ideal, but is the best compromise based on +	 * the capabilities of the device. +	 */ +	state->enabled = iqs620_pwm->out_en; +	state->duty_cycle = DIV_ROUND_UP((iqs620_pwm->duty_val + 1) * +					 IQS620_PWM_PERIOD_NS, 256); + +	mutex_unlock(&iqs620_pwm->lock); + +	state->period = IQS620_PWM_PERIOD_NS; +} + +static int iqs620_pwm_notifier(struct notifier_block *notifier, +			       unsigned long event_flags, void *context) +{ +	struct iqs620_pwm_private *iqs620_pwm; +	struct iqs62x_core *iqs62x; +	int ret; + +	if (!(event_flags & BIT(IQS62X_EVENT_SYS_RESET))) +		return NOTIFY_DONE; + +	iqs620_pwm = container_of(notifier, struct iqs620_pwm_private, +				  notifier); +	iqs62x = iqs620_pwm->iqs62x; + +	mutex_lock(&iqs620_pwm->lock); + +	/* +	 * The parent MFD driver already prints an error message in the event +	 * of a device reset, so nothing else is printed here unless there is +	 * an additional failure. +	 */ +	ret = regmap_write(iqs62x->regmap, IQS620_PWM_DUTY_CYCLE, +			   iqs620_pwm->duty_val); +	if (ret) +		goto err_mutex; + +	ret = regmap_update_bits(iqs62x->regmap, IQS620_PWR_SETTINGS, +				 IQS620_PWR_SETTINGS_PWM_OUT, +				 iqs620_pwm->out_en ? 0xFF : 0); + +err_mutex: +	mutex_unlock(&iqs620_pwm->lock); + +	if (ret) { +		dev_err(iqs620_pwm->chip.dev, +			"Failed to re-initialize device: %d\n", ret); +		return NOTIFY_BAD; +	} + +	return NOTIFY_OK; +} + +static const struct pwm_ops iqs620_pwm_ops = { +	.apply = iqs620_pwm_apply, +	.get_state = iqs620_pwm_get_state, +	.owner = THIS_MODULE, +}; + +static void iqs620_pwm_notifier_unregister(void *context) +{ +	struct iqs620_pwm_private *iqs620_pwm = context; +	int ret; + +	ret = blocking_notifier_chain_unregister(&iqs620_pwm->iqs62x->nh, +						 &iqs620_pwm->notifier); +	if (ret) +		dev_err(iqs620_pwm->chip.dev, +			"Failed to unregister notifier: %d\n", ret); +} + +static int iqs620_pwm_probe(struct platform_device *pdev) +{ +	struct iqs62x_core *iqs62x = dev_get_drvdata(pdev->dev.parent); +	struct iqs620_pwm_private *iqs620_pwm; +	unsigned int val; +	int ret; + +	iqs620_pwm = devm_kzalloc(&pdev->dev, sizeof(*iqs620_pwm), GFP_KERNEL); +	if (!iqs620_pwm) +		return -ENOMEM; + +	platform_set_drvdata(pdev, iqs620_pwm); +	iqs620_pwm->iqs62x = iqs62x; + +	ret = regmap_read(iqs62x->regmap, IQS620_PWR_SETTINGS, &val); +	if (ret) +		return ret; +	iqs620_pwm->out_en = val & IQS620_PWR_SETTINGS_PWM_OUT; + +	ret = regmap_read(iqs62x->regmap, IQS620_PWM_DUTY_CYCLE, &val); +	if (ret) +		return ret; +	iqs620_pwm->duty_val = val; + +	iqs620_pwm->chip.dev = &pdev->dev; +	iqs620_pwm->chip.ops = &iqs620_pwm_ops; +	iqs620_pwm->chip.base = -1; +	iqs620_pwm->chip.npwm = 1; + +	mutex_init(&iqs620_pwm->lock); + +	iqs620_pwm->notifier.notifier_call = iqs620_pwm_notifier; +	ret = blocking_notifier_chain_register(&iqs620_pwm->iqs62x->nh, +					       &iqs620_pwm->notifier); +	if (ret) { +		dev_err(&pdev->dev, "Failed to register notifier: %d\n", ret); +		return ret; +	} + +	ret = devm_add_action_or_reset(&pdev->dev, +				       iqs620_pwm_notifier_unregister, +				       iqs620_pwm); +	if (ret) +		return ret; + +	ret = pwmchip_add(&iqs620_pwm->chip); +	if (ret) +		dev_err(&pdev->dev, "Failed to add device: %d\n", ret); + +	return ret; +} + +static int iqs620_pwm_remove(struct platform_device *pdev) +{ +	struct iqs620_pwm_private *iqs620_pwm = platform_get_drvdata(pdev); +	int ret; + +	ret = pwmchip_remove(&iqs620_pwm->chip); +	if (ret) +		dev_err(&pdev->dev, "Failed to remove device: %d\n", ret); + +	return ret; +} + +static struct platform_driver iqs620_pwm_platform_driver = { +	.driver = { +		.name = "iqs620a-pwm", +	}, +	.probe = iqs620_pwm_probe, +	.remove = iqs620_pwm_remove, +}; +module_platform_driver(iqs620_pwm_platform_driver); + +MODULE_AUTHOR("Jeff LaBundy <jeff@labundy.com>"); +MODULE_DESCRIPTION("Azoteq IQS620A PWM Generator"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:iqs620a-pwm");  | 
