diff options
| author | Cheick Traore <cheick.traore@foss.st.com> | 2025-03-11 15:30:35 +0100 | 
|---|---|---|
| committer | Patrice Chotard <patrice.chotard@foss.st.com> | 2025-03-12 16:39:39 +0100 | 
| commit | be5523d38296a3c8bd7b6c0d64db6b953e95b770 (patch) | |
| tree | 399c42c72ad3190ff0d027aa45764d3ff1b21ec1 /drivers | |
| parent | 17d57b74948c1061793b197e28c0dffec74d2d85 (diff) | |
pwm: stm32: add driver to support pwm with timer
Add driver to support pwm on STM32MP1X SoCs. The PWM signal is generated
using a multifuntion timer which provide a pwm feature. Clock rate and
addresses are retrieved from the multifunction timer driver.
Signed-off-by: Cheick Traore <cheick.traore@foss.st.com>
Reviewed-by: Fabrice Gasnier <fabrice.gasnier@foss.st.com>
Reviewed-by: Patrice Chotard <patrice.chotard@foss.st.com>
Diffstat (limited to 'drivers')
| -rw-r--r-- | drivers/pwm/Kconfig | 8 | ||||
| -rw-r--r-- | drivers/pwm/Makefile | 1 | ||||
| -rw-r--r-- | drivers/pwm/pwm-stm32.c | 205 | 
3 files changed, 214 insertions, 0 deletions
| diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig index 6e79868d0ef..de312656746 100644 --- a/drivers/pwm/Kconfig +++ b/drivers/pwm/Kconfig @@ -105,6 +105,14 @@ config PWM_TEGRA  	  32KHz clock is supported by the driver but the duty cycle is  	  configurable. +config PWM_STM32 +	bool "Enable support for STM32 PWM" +	depends on DM_PWM && MFD_STM32_TIMERS +	help +	  This enables PWM driver for STMicroelectronics STM32 pulse width +	  modulation. It uses STM32 timer devices that can have up to 4 output +	  channels, with complementary outputs and configurable polarity. +  config PWM_SUNXI  	bool "Enable support for the Allwinner Sunxi PWM"  	depends on DM_PWM diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile index e4d10c8dc3e..76305b93bc9 100644 --- a/drivers/pwm/Makefile +++ b/drivers/pwm/Makefile @@ -22,5 +22,6 @@ obj-$(CONFIG_PWM_ROCKCHIP)	+= rk_pwm.o  obj-$(CONFIG_PWM_SANDBOX)	+= sandbox_pwm.o  obj-$(CONFIG_PWM_SIFIVE)	+= pwm-sifive.o  obj-$(CONFIG_PWM_TEGRA)		+= tegra_pwm.o +obj-$(CONFIG_PWM_STM32)		+= pwm-stm32.o  obj-$(CONFIG_PWM_SUNXI)		+= sunxi_pwm.o  obj-$(CONFIG_PWM_TI_EHRPWM)	+= pwm-ti-ehrpwm.o diff --git a/drivers/pwm/pwm-stm32.c b/drivers/pwm/pwm-stm32.c new file mode 100644 index 00000000000..5fa649b5903 --- /dev/null +++ b/drivers/pwm/pwm-stm32.c @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2025, STMicroelectronics - All Rights Reserved + * Author: Cheick Traore <cheick.traore@foss.st.com> + * + * Originally based on the Linux kernel v6.10 drivers/pwm/pwm-stm32.c. + */ + +#include <div64.h> +#include <dm.h> +#include <pwm.h> +#include <asm/io.h> +#include <asm/arch/timers.h> +#include <dm/device_compat.h> +#include <linux/time.h> + +#define CCMR_CHANNEL_SHIFT	8 +#define CCMR_CHANNEL_MASK	0xFF + +struct stm32_pwm_priv { +	bool have_complementary_output; +	bool invert_polarity; +}; + +static u32 active_channels(struct stm32_timers_plat *plat) +{ +	return readl(plat->base + TIM_CCER) & TIM_CCER_CCXE; +} + +static int stm32_pwm_set_config(struct udevice *dev, uint channel, +				uint period_ns, uint duty_ns) +{ +	struct stm32_timers_plat *plat = dev_get_plat(dev_get_parent(dev)); +	struct stm32_timers_priv *priv = dev_get_priv(dev_get_parent(dev)); +	unsigned long long prd, div, dty; +	unsigned int prescaler = 0; +	u32 ccmr, mask, shift; + +	if (duty_ns > period_ns) +		return -EINVAL; + +	/* +	 * Period and prescaler values depends on clock rate +	 * First we need to find the minimal value for prescaler such that +	 * +	 *        period_ns * clkrate +	 *   ------------------------------ < max_arr + 1 +	 *   NSEC_PER_SEC * (prescaler + 1) +	 * +	 * This equation is equivalent to +	 * +	 *        period_ns * clkrate +	 *   ---------------------------- < prescaler + 1 +	 *   NSEC_PER_SEC * (max_arr + 1) +	 * +	 * Using integer division and knowing that the right hand side is +	 * integer, this is further equivalent to +	 * +	 *   (period_ns * clkrate) // (NSEC_PER_SEC * (max_arr + 1)) ≤ prescaler +	 */ + +	div = (unsigned long long)priv->rate * period_ns; +	do_div(div, NSEC_PER_SEC); +	prd = div; + +	do_div(div, priv->max_arr + 1); +	prescaler = div; +	if (prescaler > MAX_TIM_PSC) +		return -EINVAL; + +	do_div(prd, prescaler + 1); +	if (!prd) +		return -EINVAL; + +	/* +	 * All channels share the same prescaler and counter so when two +	 * channels are active at the same time we can't change them +	 */ +	if (active_channels(plat) & ~(1 << channel * 4)) { +		u32 psc, arr; + +		psc = readl(plat->base + TIM_PSC); +		arr = readl(plat->base + TIM_ARR); +		if (psc != prescaler || arr != prd - 1) +			return -EBUSY; +	} + +	writel(prescaler, plat->base + TIM_PSC); +	writel(prd - 1, plat->base + TIM_ARR); +	setbits_le32(plat->base + TIM_CR1, TIM_CR1_ARPE); + +	/* Calculate the duty cycles */ +	dty = prd * duty_ns; +	do_div(dty, period_ns); + +	writel(dty, plat->base + TIM_CCRx(channel + 1)); + +	/* Configure output mode */ +	shift = (channel & 0x1) * CCMR_CHANNEL_SHIFT; +	ccmr = (TIM_CCMR_PE | TIM_CCMR_M1) << shift; +	mask = CCMR_CHANNEL_MASK << shift; +	if (channel < 2) +		clrsetbits_le32(plat->base + TIM_CCMR1, mask, ccmr); +	else +		clrsetbits_le32(plat->base + TIM_CCMR2, mask, ccmr); + +	setbits_le32(plat->base + TIM_BDTR, TIM_BDTR_MOE); + +	return 0; +} + +static int stm32_pwm_set_enable(struct udevice *dev, uint channel, +				bool enable) +{ +	struct stm32_timers_plat *plat = dev_get_plat(dev_get_parent(dev)); +	struct stm32_pwm_priv *priv = dev_get_priv(dev); +	u32 mask; + +	/* Enable channel */ +	mask = TIM_CCER_CC1E << (channel * 4); +	if (priv->have_complementary_output) +		mask |= TIM_CCER_CC1NE << (channel * 4); + +	if (enable) { +		setbits_le32(plat->base + TIM_CCER, mask); +		/* Make sure that registers are updated */ +		setbits_le32(plat->base + TIM_EGR, TIM_EGR_UG); +		/* Enable controller */ +		setbits_le32(plat->base + TIM_CR1, TIM_CR1_CEN); +	} else { +		clrbits_le32(plat->base + TIM_CCER, mask); +		/* When all channels are disabled, we can disable the controller */ +		if (!active_channels(plat)) +			clrbits_le32(plat->base + TIM_CR1, TIM_CR1_CEN); +	} + +	return 0; +} + +static int stm32_pwm_set_invert(struct udevice *dev, uint channel, +				bool polarity) +{ +	struct stm32_timers_plat *plat = dev_get_plat(dev_get_parent(dev)); +	struct stm32_pwm_priv *priv = dev_get_priv(dev); +	u32 mask; + +	mask = TIM_CCER_CC1P << (channel * 4); +	if (priv->have_complementary_output) +		mask |= TIM_CCER_CC1NP << (channel * 4); + +	clrsetbits_le32(plat->base + TIM_CCER, mask, polarity ? mask : 0); + +	return 0; +} + +static void stm32_pwm_detect_complementary(struct udevice *dev) +{ +	struct stm32_timers_plat *plat = dev_get_plat(dev_get_parent(dev)); +	struct stm32_pwm_priv *priv = dev_get_priv(dev); +	u32 ccer; + +	/* +	 * If complementary bit doesn't exist writing 1 will have no +	 * effect so we can detect it. +	 */ +	setbits_le32(plat->base + TIM_CCER, TIM_CCER_CC1NE); +	ccer = readl(plat->base + TIM_CCER); +	clrbits_le32(plat->base + TIM_CCER, TIM_CCER_CC1NE); + +	priv->have_complementary_output = (ccer != 0); +} + +static int stm32_pwm_probe(struct udevice *dev) +{ +	struct stm32_timers_priv *timer = dev_get_priv(dev_get_parent(dev)); + +	if (timer->rate > 1000000000) { +		dev_err(dev, "Clock freq too high (%lu)\n", timer->rate); +		return -EINVAL; +	} + +	stm32_pwm_detect_complementary(dev); + +	return 0; +} + +static const struct pwm_ops stm32_pwm_ops = { +	.set_config	= stm32_pwm_set_config, +	.set_enable	= stm32_pwm_set_enable, +	.set_invert	= stm32_pwm_set_invert, +}; + +static const struct udevice_id stm32_pwm_ids[] = { +	{ .compatible = "st,stm32-pwm" }, +	{ } +}; + +U_BOOT_DRIVER(stm32_pwm) = { +	.name		= "stm32_pwm", +	.id		= UCLASS_PWM, +	.of_match	= stm32_pwm_ids, +	.ops		= &stm32_pwm_ops, +	.probe          = stm32_pwm_probe, +	.priv_auto	= sizeof(struct stm32_pwm_priv), +}; | 
