Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions drivers/pwm/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ zephyr_library_sources_ifdef(CONFIG_PWM_SILABS_LETIMER pwm_silabs_letimer.c)
zephyr_library_sources_ifdef(CONFIG_PWM_SILABS_SIWX91X pwm_silabs_siwx91x.c)
zephyr_library_sources_ifdef(CONFIG_PWM_SILABS_TIMER pwm_silabs_timer.c)
zephyr_library_sources_ifdef(CONFIG_PWM_STM32 pwm_stm32.c)
zephyr_library_sources_ifdef(CONFIG_PWM_STM32_LPTIM pwm_stm32_lptim.c)
zephyr_library_sources_ifdef(CONFIG_PWM_TELINK_B91 pwm_b91.c)
zephyr_library_sources_ifdef(CONFIG_PWM_TEST pwm_test.c)
zephyr_library_sources_ifdef(CONFIG_PWM_WCH_GPTM pwm_wch_gptm.c)
Expand Down
10 changes: 10 additions & 0 deletions drivers/pwm/Kconfig.stm32
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,13 @@ config PWM_STM32
This option enables the PWM driver for STM32 family of
processors. Say y if you wish to use PWM port on STM32
MCU.

config PWM_STM32_LPTIM
bool "STM32 MCU LPTIM PWM driver"
default y
depends on DT_HAS_ST_STM32_LPTIM_PWM_ENABLED
select RESET
select PINCTRL
help
This option enables the PWM driver for low power timers
on STM32 family of processors.
238 changes: 238 additions & 0 deletions drivers/pwm/pwm_stm32_lptim.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
/*
* Copyright (c) 2016 Linaro Limited.
* Copyright (c) 2020 Teslabs Engineering S.L.
* Copyright (c) 2023 Nobleo Technology
* Copyright (c) 2025 CodeWrights GmbH
*
* SPDX-License-Identifier: Apache-2.0
*/

#define DT_DRV_COMPAT st_stm32_lptim_pwm

#include <soc.h>
#include <stm32_ll_rcc.h>
#include <stm32_ll_lptim.h>
#include <zephyr/drivers/pwm.h>
#include <zephyr/drivers/pinctrl.h>
#include <zephyr/drivers/reset.h>
#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <zephyr/init.h>

#include <zephyr/drivers/clock_control/stm32_clock_control.h>
#include <zephyr/dt-bindings/pwm/stm32_pwm.h>

#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(pwm_stm32_lptim, CONFIG_PWM_LOG_LEVEL);

/** PWM data. */
struct pwm_stm32_lptim_data {
/** Timer clock (Hz). */
uint32_t tim_clk;
/* Reset controller device configuration */
const struct reset_dt_spec reset;
};

/** PWM configuration. */
struct pwm_stm32_lptim_config {
LPTIM_TypeDef *timer;
uint32_t prescaler;
const struct stm32_pclken *pclken;
size_t pclk_len;
const struct pinctrl_dev_config *pcfg;
};

/** Maximum number of timer channels */
#define TIMER_MAX_CH 2u

/** Channel to LL mapping. */
static const uint32_t ch2ll[TIMER_MAX_CH] = {
LL_LPTIM_CHANNEL_CH1,
LL_LPTIM_CHANNEL_CH2,
};

/** Channel to compare set function mapping. */
static void (*const set_timer_compare[TIMER_MAX_CH])(LPTIM_TypeDef *, uint32_t) = {
LL_LPTIM_OC_SetCompareCH1,
LL_LPTIM_OC_SetCompareCH2,
};

/**
* Obtain LL polarity from PWM flags.
*
* @param flags PWM flags.
*
* @return LL polarity.
*/
static uint32_t get_polarity(pwm_flags_t flags)
{
/* PWM_POLARITY_NORMAL (active-high) pulses correspond to
* LL_LPTIM_OUTPUT_POLARITY_INVERSE and vice versa.
*/
return (flags & PWM_POLARITY_MASK) == PWM_POLARITY_NORMAL
? LL_LPTIM_OUTPUT_POLARITY_INVERSE
: LL_LPTIM_OUTPUT_POLARITY_REGULAR;
}

static int pwm_stm32_lptim_set_cycles(const struct device *dev, uint32_t channel,
uint32_t period_cycles, uint32_t pulse_cycles,
pwm_flags_t flags)
{
const struct pwm_stm32_lptim_config *cfg = dev->config;
LPTIM_TypeDef *timer = cfg->timer;
uint32_t polarity;
uint32_t ll_channel;

if (channel < 1u || channel > TIMER_MAX_CH) {
LOG_ERR("Invalid channel (%d)", channel);
return -EINVAL;
}

/*
* Timers count from 0 up to the value in the ARR register (16-bit).
* Thus period_cycles cannot be greater than UINT16_MAX.
*/
if (period_cycles > UINT16_MAX) {
LOG_ERR("Cannot set PWM output, value exceeds 16-bit timer limit.");
return -ENOTSUP;
}

ll_channel = ch2ll[channel - 1u];

if (period_cycles == 0u) {
LL_LPTIM_CC_DisableChannel(timer, ll_channel);
return 0;
}

polarity = get_polarity(flags);

if (pulse_cycles == 0U) {
/* hardware does not support generating 0% duty cycle
* workaround by setting to 100% and inverting the polarity
*/
polarity = (polarity == LL_LPTIM_OUTPUT_POLARITY_REGULAR)
? LL_LPTIM_OUTPUT_POLARITY_INVERSE
: LL_LPTIM_OUTPUT_POLARITY_REGULAR;
/* set to max to have 100% duty cycle (of inverse) */
pulse_cycles = 0xffff;
} else if (pulse_cycles == period_cycles) {
/* set to max to have 100% duty cycle */
pulse_cycles = 0xffff;
} else {
/* remove 1 pulse cycle, accounts for 1 extra low cycle */
pulse_cycles -= 1U;
}

/* remove 1 period cycle, accounts for 1 extra low cycle */
period_cycles -= 1U;

LL_LPTIM_OC_SetPolarity(timer, ll_channel, polarity);
set_timer_compare[channel - 1u](timer, pulse_cycles);
LL_LPTIM_SetAutoReload(timer, period_cycles);

if (!LL_LPTIM_CC_IsEnabledChannel(timer, ll_channel)) {
LL_LPTIM_CC_SetChannelMode(timer, ll_channel, LL_LPTIM_CCMODE_OUTPUT_PWM);
LL_LPTIM_CC_EnableChannel(timer, ll_channel);
}

return 0;
}

static int pwm_stm32_lptim_get_cycles_per_sec(const struct device *dev, uint32_t channel,
uint64_t *cycles)
{
struct pwm_stm32_lptim_data *data = dev->data;
const struct pwm_stm32_lptim_config *cfg = dev->config;

*cycles = (uint64_t)(data->tim_clk / cfg->prescaler);

return 0;
}
Comment on lines +142 to +151
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Add prescaler validation to prevent division by zero.

The function divides by cfg->prescaler without validation. If the prescaler is zero or not a power of 2 (1, 2, 4, 8, 16, 32, 64, 128), this will cause either a division-by-zero error or incorrect frequency calculations.

Add validation in the init function to ensure prescaler is valid:

 static int pwm_stm32_lptim_init(const struct device *dev)
 {
 	struct pwm_stm32_lptim_data *data = dev->data;
 	const struct pwm_stm32_lptim_config *cfg = dev->config;
 	LPTIM_TypeDef *timer = cfg->timer;
 	const struct device *clk = DEVICE_DT_GET(STM32_CLOCK_CONTROL_NODE);
 	uint32_t tim_clk;
 	int r;
 
+	/* Validate prescaler is a power of 2 and non-zero */
+	if (cfg->prescaler == 0 || (cfg->prescaler & (cfg->prescaler - 1)) != 0) {
+		LOG_ERR("Invalid prescaler value: %u (must be power of 2)", cfg->prescaler);
+		return -EINVAL;
+	}
+
 	if (!device_is_ready(clk)) {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In drivers/pwm/pwm_stm32_lptim.c around lines 144 to 153, the function divides
by cfg->prescaler without validating it; add prescaler validation during driver
init to prevent division-by-zero or wrong calculations by checking
cfg->prescaler is non-zero and one of the allowed LPTIM prescaler values
(1,2,4,8,16,32,64,128), return a negative error code (e.g., -EINVAL) if invalid,
and keep get_cycles_per_sec relying on the validated prescaler (or defensively
check again and return error) so cycles calculation cannot divide by zero or use
unsupported prescaler values.


static DEVICE_API(pwm, pwm_stm32_lptim_driver_api) = {
.set_cycles = pwm_stm32_lptim_set_cycles,
.get_cycles_per_sec = pwm_stm32_lptim_get_cycles_per_sec,
};

static int pwm_stm32_lptim_init(const struct device *dev)
{
struct pwm_stm32_lptim_data *data = dev->data;
const struct pwm_stm32_lptim_config *cfg = dev->config;
LPTIM_TypeDef *timer = cfg->timer;
const struct device *clk = DEVICE_DT_GET(STM32_CLOCK_CONTROL_NODE);
int r;

if (!device_is_ready(clk)) {
LOG_ERR("clock control device not ready");
return -ENODEV;
}

/* Enable clock and store its speed */
r = clock_control_on(clk, (clock_control_subsys_t)&cfg->pclken[0]);
if (r < 0) {
LOG_ERR("Could not initialize clock (%d)", r);
return r;
}

/* Enable Timer clock source */
r = clock_control_configure(clk, (clock_control_subsys_t)&cfg->pclken[1], NULL);
if (r != 0) {
LOG_ERR("Could not configure clock (%d)", r);
return r;
}

r = clock_control_get_rate(clk, (clock_control_subsys_t)&cfg->pclken[1], &data->tim_clk);
if (r < 0) {
LOG_ERR("Timer clock rate get error (%d)", r);
return r;
}

/* Reset timer to default state using RCC */
(void)reset_line_toggle_dt(&data->reset);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor: Check reset operation return value.

The reset toggle return value is cast to void, discarding potential error information. While reset failures are rare, checking the return value would improve robustness.

🔎 Suggested improvement
-	(void)reset_line_toggle_dt(&data->reset);
+	r = reset_line_toggle_dt(&data->reset);
+	if (r < 0) {
+		LOG_WRN("Reset failed (%d), continuing anyway", r);
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
(void)reset_line_toggle_dt(&data->reset);
r = reset_line_toggle_dt(&data->reset);
if (r < 0) {
LOG_WRN("Reset failed (%d), continuing anyway", r);
}
🤖 Prompt for AI Agents
In drivers/pwm/pwm_stm32_lptim.c around line 192, the call to
reset_line_toggle_dt(&data->reset) currently casts the result to void and
discards errors; capture the return value, check it for failure, and handle
failures instead of ignoring them — e.g., int rc =
reset_line_toggle_dt(&data->reset); if (rc) { LOG_ERR("reset_line_toggle_dt
failed: %d", rc); return rc; } (or propagate/translate the error per the
surrounding function's error-handling convention).


/* Configure pinmux */
r = pinctrl_apply_state(cfg->pcfg, PINCTRL_STATE_DEFAULT);
if (r < 0) {
LOG_ERR("PWM pinctrl setup failed (%d)", r);
return r;
}
Comment on lines +195 to +199
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Major: Pinctrl error path leaks enabled clocks.

If pinctrl configuration fails, the function returns without disabling the peripheral and timer clocks enabled earlier (lines 172 and 178-179).

🔎 Proposed fix
 	r = pinctrl_apply_state(cfg->pcfg, PINCTRL_STATE_DEFAULT);
 	if (r < 0) {
 		LOG_ERR("PWM pinctrl setup failed (%d)", r);
+		(void)clock_control_off(clk, (clock_control_subsys_t)&cfg->pclken[0]);
 		return r;
 	}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In drivers/pwm/pwm_stm32_lptim.c around lines 172, 178-179 and 195-199, the
pinctrl error path returns early and leaks the peripheral and timer clocks
enabled previously; modify the error path so that before returning on
pinctrl_apply_state failure you explicitly disable the timer clock and the
peripheral clock (matching the order used for enable) using the same
clock_control_off calls (and handle/ignore their return values or log failures),
then return the original pinctrl error code; ensure both clocks are always
turned off on any early exit after they were enabled.


/* Initialize timer */
LL_LPTIM_SetClockSource(timer, LL_LPTIM_CLK_SOURCE_INTERNAL);
LL_LPTIM_SetPrescaler(timer, __CLZ(__RBIT(cfg->prescaler)) << LPTIM_CFGR_PRESC_Pos);
LL_LPTIM_SetAutoReload(timer, 0U);
LL_LPTIM_SetUpdateMode(timer, LL_LPTIM_UPDATE_MODE_ENDOFPERIOD);

LL_LPTIM_Enable(timer);

/* Start the LPTIM counter in continuous mode */
LL_LPTIM_StartCounter(timer, LL_LPTIM_OPERATING_MODE_CONTINUOUS);

return 0;
}

#define PWM(index) DT_DRV_INST(index)

#define PWM_DEVICE_INIT(index) \
static struct pwm_stm32_lptim_data pwm_stm32_lptim_data_##index = { \
.reset = RESET_DT_SPEC_GET(PWM(index)), \
}; \
\
PINCTRL_DT_INST_DEFINE(index); \
\
BUILD_ASSERT(DT_NUM_CLOCKS(PWM(index)) == 2, "Timer clock source is required"); \
static const struct stm32_pclken pclken_##index[] = STM32_DT_CLOCKS(PWM(index)); \
\
static const struct pwm_stm32_lptim_config pwm_stm32_lptim_config_##index = { \
.timer = (LPTIM_TypeDef *)DT_REG_ADDR(PWM(index)), \
.prescaler = DT_PROP(PWM(index), st_prescaler), \
.pclken = pclken_##index, \
.pclk_len = DT_NUM_CLOCKS(PWM(index)), \
.pcfg = PINCTRL_DT_INST_DEV_CONFIG_GET(index), \
}; \
\
DEVICE_DT_INST_DEFINE(index, &pwm_stm32_lptim_init, NULL, &pwm_stm32_lptim_data_##index, \
&pwm_stm32_lptim_config_##index, POST_KERNEL, \
CONFIG_PWM_INIT_PRIORITY, &pwm_stm32_lptim_driver_api);
DT_INST_FOREACH_STATUS_OKAY(PWM_DEVICE_INIT)
31 changes: 31 additions & 0 deletions dts/bindings/pwm/st,stm32-lptim-pwm.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
description: STM32 LPTIM PWM

compatible: "st,stm32-lptim-pwm"

include:
- name: pwm-controller.yaml
- name: base.yaml
- name: pinctrl-device.yaml
- name: st,stm32-lptim.yaml
property-blocklist:
- st,timeout

properties:
pinctrl-0:
required: true

pinctrl-names:
required: true

"#pwm-cells":
const: 3
description: |
Number of items to expect in a PWM
- channel of the timer used for PWM
- period to set in ns
- flags : combination of standard flags like PWM_POLARITY_NORMAL

pwm-cells:
- channel
- period
- flags
34 changes: 34 additions & 0 deletions samples/drivers/led/pwm/boards/stm32h573i_dk.overlay
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2025 CodeWrights GmbH
*
* SPDX-License-Identifier: Apache-2.0
*/

/ {
leds {
status = "disabled";
};

pwmleds {
compatible = "pwm-leds";

red_pwm_led: red_pwm_led {
pwms = <&lptim5 2 PWM_MSEC(20) PWM_POLARITY_INVERTED>;
label = "LD3 - red led";
};
};
};

&lptim5 {
compatible = "st,stm32-lptim-pwm";
clocks = <&rcc STM32_CLOCK(APB3, 14)>,
<&rcc STM32_SRC_LSE LPTIM5_SEL(3)>;
#pwm-cells = <3>;

st,prescaler = <2>;

pinctrl-0 = <&lptim5_ch2_pf1>;
pinctrl-names = "default";

status = "okay";
};
1 change: 1 addition & 0 deletions samples/drivers/led/pwm/sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ tests:
platform_exclude: reel_board
integration_platforms:
- frdm_k22f
- stm32h573i_dk
timeout: 20
harness: console
harness_config:
Expand Down
Loading