Primary Git Repository for the Zephyr Project. Zephyr is a new generation, scalable, optimized, secure RTOS for multiple hardware architectures.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

385 lines
13 KiB

/*
* 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)