Browse Source

drivers: pwm: silabs: Add TIMER PWM driver for Series 2

Add PWM driver for the Timer peripheral on Series 2.
The TIMER uses the high-frequency EM01 Group A clock, and has
a 16- or 32-bit counter. It supports PWM period and pulse capture
on channel 0, and PWM output on all channels.

Signed-off-by: Aksel Skauge Mellbye <aksel.mellbye@silabs.com>
pull/92175/head
Aksel Skauge Mellbye 2 months ago committed by Benjamin Cabé
parent
commit
fccc0a7544
  1. 1
      drivers/pwm/CMakeLists.txt
  2. 11
      drivers/pwm/Kconfig.silabs
  3. 385
      drivers/pwm/pwm_silabs_timer.c
  4. 2
      modules/hal_silabs/simplicity_sdk/CMakeLists.txt
  5. 3
      modules/hal_silabs/simplicity_sdk/Kconfig
  6. 29
      tests/drivers/pwm/pwm_gpio_loopback/boards/xg29_rb4412a.overlay
  7. 54
      tests/drivers/pwm/pwm_loopback/boards/xg29_rb4412a.overlay

1
drivers/pwm/CMakeLists.txt

@ -33,6 +33,7 @@ zephyr_library_sources_ifdef(CONFIG_PWM_XLNX_AXI_TIMER pwm_xlnx_axi_timer.c) @@ -33,6 +33,7 @@ zephyr_library_sources_ifdef(CONFIG_PWM_XLNX_AXI_TIMER pwm_xlnx_axi_timer.c)
zephyr_library_sources_ifdef(CONFIG_PWM_MCUX_PWT pwm_mcux_pwt.c)
zephyr_library_sources_ifdef(CONFIG_PWM_GECKO pwm_gecko.c)
zephyr_library_sources_ifdef(CONFIG_PWM_SILABS_LETIMER pwm_silabs_letimer.c)
zephyr_library_sources_ifdef(CONFIG_PWM_SILABS_TIMER pwm_silabs_timer.c)
zephyr_library_sources_ifdef(CONFIG_PWM_SILABS_SIWX91X pwm_silabs_siwx91x.c)
zephyr_library_sources_ifdef(CONFIG_PWM_GD32 pwm_gd32.c)
zephyr_library_sources_ifdef(CONFIG_PWM_RCAR pwm_rcar.c)

11
drivers/pwm/Kconfig.silabs

@ -12,3 +12,14 @@ config PWM_SILABS_LETIMER @@ -12,3 +12,14 @@ config PWM_SILABS_LETIMER
The LETIMER peripheral has two channels which share PWM period configuration. The last
configured period will apply to both channels. The output may glitch when updating the
PWM pulse and period, as the new values will take effect immediately.
config PWM_SILABS_TIMER
bool "Silabs TIMER PWM driver"
default y
depends on DT_HAS_SILABS_TIMER_PWM_ENABLED
select SILABS_SISDK_TIMER
help
Enable the PWM driver for the TIMER peripheral on Silabs Series 2 SoCs.
The TIMER peripheral has three or more channels which share PWM period configuration.
The last configured period will apply to all channels.

385
drivers/pwm/pwm_silabs_timer.c

@ -0,0 +1,385 @@ @@ -0,0 +1,385 @@
/*
* Copyright (c) 2025 Silicon Laboratories Inc.
*
* SPDX-License-Identifier: Apache-2.0
*/
#define DT_DRV_COMPAT silabs_timer_pwm
#include <errno.h>
#include <zephyr/drivers/clock_control.h>
#include <zephyr/drivers/clock_control/clock_control_silabs.h>
#include <zephyr/drivers/pinctrl.h>
#include <zephyr/drivers/pwm.h>
#include <zephyr/logging/log.h>
#include <zephyr/pm/device.h>
#include <sl_hal_timer.h>
LOG_MODULE_REGISTER(pwm_silabs_timer, CONFIG_PWM_LOG_LEVEL);
struct silabs_timer_pwm_config {
const struct pinctrl_dev_config *pcfg;
const struct device *clock_dev;
const struct silabs_clock_control_cmu_config clock_cfg;
TIMER_TypeDef *base;
void (*irq_config_func)(const struct device *dev);
uint16_t clock_div;
uint8_t num_channels;
uint8_t counter_size;
bool run_in_debug;
};
struct silabs_timer_pwm_data {
pwm_flags_t flags;
pwm_capture_callback_handler_t cb;
void *user_data;
int skip_trigger;
};
static int silabs_timer_pwm_set_cycles(const struct device *dev, uint32_t channel,
uint32_t period_cycles, uint32_t pulse_cycles,
pwm_flags_t flags)
{
bool invert_polarity = (flags & PWM_POLARITY_MASK) == PWM_POLARITY_INVERTED;
const struct silabs_timer_pwm_config *config = dev->config;
if (channel > config->num_channels) {
return -EINVAL;
}
if (LOG2(period_cycles) >= config->counter_size ||
LOG2(pulse_cycles) >= config->counter_size) {
return -ENOTSUP;
}
if ((config->base->CC[channel].CFG & _TIMER_CC_CFG_MODE_MASK) != TIMER_CC_CFG_MODE_PWM) {
sl_hal_timer_channel_config_t ch_config = SL_HAL_TIMER_CHANNEL_CONFIG_PWM;
uint32_t timer_status = config->base->STATUS;
ch_config.output_invert = invert_polarity;
sl_hal_timer_channel_init(config->base, channel, &ch_config);
sl_hal_timer_enable(config->base);
sl_hal_timer_wait_sync(config->base);
/* The channel init function disables and reenables the timer, which may cause
* pending pulse updates on other channels to be lost. Re-arm the compare buffer
* with its existing content to ensure an OCB->OC update will happen if an update
* was pending when the timer was disabled. The same issue applies to pending
* period updates, but the period will be unconditionally updated below since all
* channels share a single period.
*/
for (int i = 0; i < config->num_channels; i++) {
if (channel != i && (timer_status & BIT(_TIMER_STATUS_OCBV0_SHIFT + i))) {
config->base->CC[i].OCB = config->base->CC[i].OCB;
}
}
} else {
if (invert_polarity) {
config->base->CC_SET[channel].CTRL = TIMER_CC_CTRL_OUTINV;
} else {
config->base->CC_CLR[channel].CTRL = TIMER_CC_CTRL_OUTINV;
}
}
if (config->base->STATUS & TIMER_STATUS_RUNNING) {
sl_hal_timer_set_top_buffer(config->base, period_cycles - 1);
sl_hal_timer_channel_set_compare_buffer(config->base, channel, pulse_cycles);
} else {
sl_hal_timer_set_top(config->base, period_cycles - 1);
sl_hal_timer_channel_set_compare(config->base, channel, pulse_cycles);
sl_hal_timer_start(config->base);
}
return 0;
}
static int silabs_timer_pwm_get_cycles_per_sec(const struct device *dev, uint32_t channel,
uint64_t *cycles)
{
const struct silabs_timer_pwm_config *config = dev->config;
uint32_t clock_rate;
int err;
if (channel > config->num_channels) {
return -EINVAL;
}
err = clock_control_get_rate(config->clock_dev, (clock_control_subsys_t)&config->clock_cfg,
&clock_rate);
if (err < 0) {
return err;
}
*cycles = clock_rate / config->clock_div;
return 0;
}
#ifdef CONFIG_PWM_CAPTURE
static int silabs_timer_pwm_configure_capture(const struct device *dev, uint32_t channel,
pwm_flags_t flags, pwm_capture_callback_handler_t cb,
void *user_data)
{
sl_hal_timer_channel_config_t ch_config = SL_HAL_TIMER_CHANNEL_CONFIG_DEFAULT;
bool invert_polarity = (flags & PWM_POLARITY_MASK) == PWM_POLARITY_INVERTED;
const struct silabs_timer_pwm_config *config = dev->config;
struct silabs_timer_pwm_data *data = dev->data;
if (channel != 0) {
LOG_ERR("Only channel 0 is supported for capture");
return -ENOTSUP;
}
if (config->base->IEN & TIMER_IEN_CC0) {
LOG_ERR("Capture in progress");
return -EBUSY;
}
data->flags = flags;
data->cb = cb;
data->user_data = user_data;
switch (flags & PWM_CAPTURE_TYPE_MASK) {
case PWM_CAPTURE_TYPE_PERIOD:
ch_config.input_capture_edge = invert_polarity ? SL_HAL_TIMER_CHANNEL_EDGE_FALLING
: SL_HAL_TIMER_CHANNEL_EDGE_RISING;
break;
case PWM_CAPTURE_TYPE_PULSE:
ch_config.input_capture_edge = invert_polarity ? SL_HAL_TIMER_CHANNEL_EDGE_RISING
: SL_HAL_TIMER_CHANNEL_EDGE_FALLING;
break;
case PWM_CAPTURE_TYPE_BOTH:
ch_config.input_capture_edge = SL_HAL_TIMER_CHANNEL_EDGE_BOTH;
/* Select the opposite edge of the one we want the interrupt to trigger on due to an
* issue on Series 2 devices. The interrupt will occur with the correct capture data
* for the most recent edge, but the previous edge is used to decide if the
* interrupt will fire.
*/
ch_config.input_capture_event = invert_polarity
? SL_HAL_TIMER_CHANNEL_EVENT_RISING
: SL_HAL_TIMER_CHANNEL_EVENT_FALLING;
break;
default:
LOG_ERR("Invalid capture type");
return -EINVAL;
}
ch_config.channel_mode = SL_HAL_TIMER_CHANNEL_MODE_CAPTURE;
sl_hal_timer_channel_init(config->base, channel, &ch_config);
sl_hal_timer_enable(config->base);
config->base->CTRL_CLR = _TIMER_CTRL_RISEA_MASK | _TIMER_CTRL_FALLA_MASK;
config->base->CTRL_SET =
invert_polarity ? TIMER_CTRL_FALLA_RELOADSTART : TIMER_CTRL_RISEA_RELOADSTART;
sl_hal_timer_set_top(config->base, GENMASK(config->counter_size - 1, 0));
return 0;
}
static int silabs_timer_pwm_enable_capture(const struct device *dev, uint32_t channel)
{
const struct silabs_timer_pwm_config *config = dev->config;
struct silabs_timer_pwm_data *data = dev->data;
if (channel != 0) {
LOG_ERR("Only channel 0 is supported for capture");
return -ENOTSUP;
}
if (config->base->IEN & TIMER_IEN_CC0) {
LOG_ERR("Capture in progress");
return -EBUSY;
}
/* Skip the first two interrupts. This should have been 1 if not for an issue on Series 2 */
data->skip_trigger = 2;
while (!(config->base->STATUS & TIMER_STATUS_ICFEMPTY0)) {
sl_hal_timer_channel_get_capture(config->base, 0);
}
sl_hal_timer_clear_interrupts(config->base, _TIMER_IF_MASK);
sl_hal_timer_enable_interrupts(config->base, TIMER_IEN_CC0);
sl_hal_timer_start(config->base);
return 0;
}
static int silabs_timer_pwm_disable_capture(const struct device *dev, uint32_t channel)
{
const struct silabs_timer_pwm_config *config = dev->config;
if (channel != 0) {
LOG_ERR("Only channel 0 is supported for capture");
return -ENOTSUP;
}
sl_hal_timer_disable_interrupts(config->base, TIMER_IEN_CC0);
sl_hal_timer_clear_interrupts(config->base, _TIMER_IF_MASK);
return 0;
}
static void silabs_timer_pwm_isr(const struct device *dev)
{
const struct silabs_timer_pwm_config *config = dev->config;
struct silabs_timer_pwm_data *data = dev->data;
uint32_t period_cycles = 0;
uint32_t pulse_cycles = 0;
if (!(sl_hal_timer_get_enabled_pending_interrupts(config->base) & TIMER_IF_CC0)) {
return;
}
sl_hal_timer_clear_interrupts(config->base, TIMER_IF_CC0);
if (data->skip_trigger) {
data->skip_trigger--;
while (!(config->base->STATUS & TIMER_STATUS_ICFEMPTY0)) {
sl_hal_timer_channel_get_capture(config->base, 0);
}
return;
}
switch (data->flags & PWM_CAPTURE_TYPE_MASK) {
case PWM_CAPTURE_TYPE_PERIOD:
period_cycles = sl_hal_timer_channel_get_capture(config->base, 0);
break;
case PWM_CAPTURE_TYPE_PULSE:
pulse_cycles = sl_hal_timer_channel_get_capture(config->base, 0);
break;
case PWM_CAPTURE_TYPE_BOTH:
pulse_cycles = sl_hal_timer_channel_get_capture(config->base, 0);
period_cycles = sl_hal_timer_channel_get_capture(config->base, 0);
break;
default:
return;
}
if ((data->flags & PWM_CAPTURE_MODE_MASK) == PWM_CAPTURE_MODE_SINGLE) {
silabs_timer_pwm_disable_capture(dev, 0);
}
if (data->cb) {
data->cb(dev, 0, period_cycles, pulse_cycles, 0, data->user_data);
}
}
#endif
static int silabs_timer_pwm_pm_action(const struct device *dev, enum pm_device_action action)
{
const struct silabs_timer_pwm_config *config = dev->config;
int err;
if (action == PM_DEVICE_ACTION_RESUME) {
err = clock_control_on(config->clock_dev,
(clock_control_subsys_t)&config->clock_cfg);
if (err < 0 && err != -EALREADY) {
return err;
}
err = pinctrl_apply_state(config->pcfg, PINCTRL_STATE_DEFAULT);
if (err < 0 && err != -ENOENT) {
return err;
}
for (int i = 0; i < config->num_channels; i++) {
if ((config->base->CC[i].CFG & _TIMER_CC_CFG_MODE_MASK) !=
TIMER_CC_CFG_MODE_OFF) {
sl_hal_timer_enable(config->base);
sl_hal_timer_start(config->base);
break;
}
}
} else if (IS_ENABLED(CONFIG_PM_DEVICE) && (action == PM_DEVICE_ACTION_SUSPEND)) {
sl_hal_timer_stop(config->base);
sl_hal_timer_disable(config->base);
err = clock_control_off(config->clock_dev,
(clock_control_subsys_t)&config->clock_cfg);
if (err < 0) {
return err;
}
err = pinctrl_apply_state(config->pcfg, PINCTRL_STATE_SLEEP);
if (err < 0 && err != -ENOENT) {
return err;
}
} else {
return -ENOTSUP;
}
return 0;
}
static int silabs_timer_pwm_init(const struct device *dev)
{
sl_hal_timer_config_t timer_config = SL_HAL_TIMER_CONFIG_DEFAULT;
const struct silabs_timer_pwm_config *config = dev->config;
int err;
err = clock_control_on(config->clock_dev, (clock_control_subsys_t)&config->clock_cfg);
if (err < 0 && err != -EALREADY) {
return err;
}
timer_config.debug_run = config->run_in_debug;
timer_config.prescaler = config->clock_div - 1;
sl_hal_timer_init(config->base, &timer_config);
if (config->irq_config_func) {
config->irq_config_func(dev);
}
return pm_device_driver_init(dev, silabs_timer_pwm_pm_action);
}
static DEVICE_API(pwm, silabs_timer_pwm_api) = {
.set_cycles = silabs_timer_pwm_set_cycles,
.get_cycles_per_sec = silabs_timer_pwm_get_cycles_per_sec,
#ifdef CONFIG_PWM_CAPTURE
.configure_capture = silabs_timer_pwm_configure_capture,
.enable_capture = silabs_timer_pwm_enable_capture,
.disable_capture = silabs_timer_pwm_disable_capture,
#endif
};
#ifdef CONFIG_PWM_CAPTURE
#define TIMER_IRQ_CONFIG_FUNC(inst) silabs_timer_pwm_irq_config_##inst
#define TIMER_IRQ_CONFIG_HANDLER(inst) \
static void silabs_timer_pwm_irq_config_##inst(const struct device *dev) \
{ \
IRQ_CONNECT(DT_IRQ(DT_INST_PARENT(inst), irq), \
DT_IRQ(DT_INST_PARENT(inst), priority), silabs_timer_pwm_isr, \
DEVICE_DT_INST_GET(inst), 0); \
irq_enable(DT_IRQ(DT_INST_PARENT(inst), irq)); \
}
#else
#define TIMER_IRQ_CONFIG_FUNC(inst) NULL
#define TIMER_IRQ_CONFIG_HANDLER(inst)
#endif
#define TIMER_PWM_INIT(inst) \
PINCTRL_DT_INST_DEFINE(inst); \
PM_DEVICE_DT_INST_DEFINE(inst, silabs_timer_pwm_pm_action); \
TIMER_IRQ_CONFIG_HANDLER(inst) \
\
static const struct silabs_timer_pwm_config timer_pwm_config_##inst = { \
.pcfg = PINCTRL_DT_INST_DEV_CONFIG_GET(inst), \
.clock_dev = DEVICE_DT_GET(DT_CLOCKS_CTLR(DT_INST_PARENT(inst))), \
.clock_cfg = SILABS_DT_CLOCK_CFG(DT_INST_PARENT(inst)), \
.base = (TIMER_TypeDef *)DT_REG_ADDR(DT_INST_PARENT(inst)), \
.irq_config_func = TIMER_IRQ_CONFIG_FUNC(inst), \
.clock_div = DT_PROP(DT_INST_PARENT(inst), clock_div), \
.num_channels = DT_PROP(DT_INST_PARENT(inst), channels), \
.counter_size = DT_PROP(DT_INST_PARENT(inst), counter_size), \
.run_in_debug = DT_PROP(DT_INST_PARENT(inst), run_in_debug), \
}; \
static struct silabs_timer_pwm_data timer_pwm_data_##inst; \
DEVICE_DT_INST_DEFINE(inst, &silabs_timer_pwm_init, PM_DEVICE_DT_INST_GET(inst), \
&timer_pwm_data_##inst, &timer_pwm_config_##inst, POST_KERNEL, \
CONFIG_PWM_INIT_PRIORITY, &silabs_timer_pwm_api);
DT_INST_FOREACH_STATUS_OKAY(TIMER_PWM_INIT)

2
modules/hal_silabs/simplicity_sdk/CMakeLists.txt

@ -263,6 +263,7 @@ if(CONFIG_SOC_GECKO_GPIO) @@ -263,6 +263,7 @@ if(CONFIG_SOC_GECKO_GPIO)
endif()
zephyr_library_sources_ifdef(CONFIG_SILABS_SISDK_LETIMER ${PERIPHERAL_DIR}/src/sl_hal_letimer.c)
zephyr_library_sources_ifdef(CONFIG_SILABS_SISDK_TIMER ${PERIPHERAL_DIR}/src/sl_hal_timer.c)
zephyr_library_sources_ifdef(CONFIG_SOC_GECKO_LDMA ${EMDRV_DIR}/dmadrv/src/dmadrv.c)
@ -275,7 +276,6 @@ zephyr_library_sources_ifdef(CONFIG_SOC_GECKO_RMU ${EMLIB_DIR}/src/em_r @@ -275,7 +276,6 @@ zephyr_library_sources_ifdef(CONFIG_SOC_GECKO_RMU ${EMLIB_DIR}/src/em_r
zephyr_library_sources_ifdef(CONFIG_SOC_GECKO_RTC ${EMLIB_DIR}/src/em_rtc.c)
zephyr_library_sources_ifdef(CONFIG_SOC_GECKO_RTCC ${EMLIB_DIR}/src/em_rtcc.c)
zephyr_library_sources_ifdef(CONFIG_SOC_GECKO_EUSART ${EMLIB_DIR}/src/em_eusart.c)
zephyr_library_sources_ifdef(CONFIG_SOC_GECKO_TIMER ${EMLIB_DIR}/src/em_timer.c)
zephyr_library_sources_ifdef(CONFIG_SOC_GECKO_USART ${EMLIB_DIR}/src/em_usart.c)
zephyr_library_sources_ifdef(CONFIG_SOC_GECKO_WDOG ${EMLIB_DIR}/src/em_wdog.c)

3
modules/hal_silabs/simplicity_sdk/Kconfig

@ -7,6 +7,9 @@ menu "SiSDK configuration" @@ -7,6 +7,9 @@ menu "SiSDK configuration"
config SILABS_SISDK_LETIMER
bool "Peripheral HAL for LETIMER"
config SILABS_SISDK_TIMER
bool "Peripheral HAL for TIMER"
config RAIL_PA_CURVE_HEADER
string "RAIL PA custom curve header file"
default "pa_curves_efr32.h"

29
tests/drivers/pwm/pwm_gpio_loopback/boards/xg29_rb4412a.overlay

@ -6,17 +6,30 @@ @@ -6,17 +6,30 @@
#include <zephyr/dt-bindings/pwm/pwm.h>
/* Connections:
* EXP15 - EXP16: LETIMER (PB2) to GPIO (PB3)
* EXP4 - EXP6: TIMER (PC0) to GPIO (PC1)
*/
/ {
zephyr,user {
pwms = <&letimer0_pwm 0 PWM_MSEC(5) PWM_POLARITY_NORMAL>;
gpios = <&gpiob 3 GPIO_ACTIVE_HIGH>; /* WPK EXP16 */
pwms = <&letimer0_pwm 0 PWM_MSEC(5) PWM_POLARITY_NORMAL>,
<&timer0_pwm 0 PWM_MSEC(5) PWM_POLARITY_NORMAL>;
gpios = <&gpiob 3 GPIO_ACTIVE_HIGH>, <&gpioc 1 GPIO_ACTIVE_HIGH>;
};
};
&pinctrl {
letimer0_default: letimer0_default {
group1 {
pins = <LETIMER0_OUT0_PB2>; /* WPK EXP15 */
pins = <LETIMER0_OUT0_PB2>;
drive-push-pull;
};
};
timer0_default: timer0_default {
group1 {
pins = <TIMER0_CC0_PC0>;
drive-push-pull;
};
};
@ -31,3 +44,13 @@ @@ -31,3 +44,13 @@
status = "okay";
};
};
&timer0 {
status = "okay";
timer0_pwm: pwm {
pinctrl-0 = <&timer0_default>;
pinctrl-names = "default";
status = "okay";
};
};

54
tests/drivers/pwm/pwm_loopback/boards/xg29_rb4412a.overlay

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
/*
* Copyright (c) 2025 Silicon Laboratories Inc.
*
* SPDX-License-Identifier: Apache-2.0
*/
/* Connections:
* EXP15 - EXP16: TIMER (PB2) to TIMER (PB3)
*/
/ {
pwm_loopback_0 {
compatible = "test-pwm-loopback";
pwms = <&pwm1 0 PWM_MSEC(5) PWM_POLARITY_NORMAL>,
<&pwm0 0 PWM_MSEC(5) PWM_POLARITY_NORMAL>;
};
};
&pinctrl {
timer0_default: timer0_default {
group1 {
pins = <TIMER0_CC0_PB3>;
input-enable;
};
};
timer1_default: timer1_default {
group1 {
pins = <TIMER1_CC0_PB2>;
drive-push-pull;
};
};
};
&timer0 {
status = "okay";
pwm0: pwm {
pinctrl-0 = <&timer0_default>;
pinctrl-names = "default";
status = "okay";
};
};
&timer1 {
status = "okay";
pwm1: pwm {
pinctrl-0 = <&timer1_default>;
pinctrl-names = "default";
status = "okay";
};
};
Loading…
Cancel
Save