92f3fb79fe
This introduces PWM driver with supporting PWM output APIs based on NXP S32 EMIOS peripheral. This supports three mode: OPWFMB, OPWMCB and OPWMB. OPWFMB uses internal counter, the new period and duty cycle takes effect immediately. OPWMCB and OPWMB use external counter as timebase, changing PWM period at runtime will impact to all channels share the same timebase. Also the new period and duty cycle take effect in next period boundary of the timebase Signed-off-by: Dat Nguyen Duy <dat.nguyenduy@nxp.com>
404 lines
14 KiB
C
404 lines
14 KiB
C
/*
|
|
* Copyright 2023 NXP
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#include <zephyr/device.h>
|
|
#include <zephyr/drivers/pwm.h>
|
|
#include <zephyr/drivers/pinctrl.h>
|
|
#include <zephyr/drivers/clock_control.h>
|
|
#include <Emios_Mcl_Ip.h>
|
|
#include <Emios_Pwm_Ip.h>
|
|
|
|
#define LOG_MODULE_NAME nxp_s32_emios_pwm
|
|
#include <zephyr/logging/log.h>
|
|
LOG_MODULE_REGISTER(LOG_MODULE_NAME, CONFIG_PWM_LOG_LEVEL);
|
|
|
|
#define DT_DRV_COMPAT nxp_s32_emios_pwm
|
|
|
|
/*
|
|
* Need to fill to this array at runtime, cannot do at build time like
|
|
* the HAL over configuration tool due to limitation of the integration
|
|
*/
|
|
extern uint8 eMios_Pwm_Ip_IndexInChState[EMIOS_PWM_IP_INSTANCE_COUNT][EMIOS_PWM_IP_CHANNEL_COUNT];
|
|
|
|
struct pwm_nxp_s32_data {
|
|
uint32_t emios_clk;
|
|
};
|
|
|
|
struct pwm_nxp_s32_pulse_info {
|
|
uint8_t pwm_pulse_channels;
|
|
Emios_Pwm_Ip_ChannelConfigType *pwm_info;
|
|
};
|
|
|
|
struct pwm_nxp_s32_config {
|
|
eMIOS_Type *base;
|
|
uint8_t instance;
|
|
const struct device *clock_dev;
|
|
clock_control_subsys_t clock_subsys;
|
|
const struct pinctrl_dev_config *pincfg;
|
|
struct pwm_nxp_s32_pulse_info *pulse_info;
|
|
};
|
|
|
|
#ifdef EMIOS_PWM_IP_MODE_OPWFMB_USED
|
|
static int pwm_nxp_s32_set_cycles_internal_timebase(uint8_t instance, uint32_t channel,
|
|
uint32_t period_cycles, uint32_t pulse_cycles)
|
|
{
|
|
bool need_update = false;
|
|
|
|
if ((period_cycles > EMIOS_PWM_IP_MAX_CNT_VAL) ||
|
|
(period_cycles <= EMIOS_PWM_IP_MIN_CNT_VAL)) {
|
|
LOG_ERR("period_cycles is out of range");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (Emios_Pwm_Ip_GetPeriod(instance, channel) != period_cycles) {
|
|
Emios_Pwm_Ip_SetPeriod(instance, channel, period_cycles);
|
|
need_update = true;
|
|
}
|
|
|
|
if (Emios_Pwm_Ip_GetDutyCycle(instance, channel) != pulse_cycles) {
|
|
need_update = true;
|
|
if (Emios_Pwm_Ip_SetDutyCycle(instance, channel, pulse_cycles)) {
|
|
LOG_ERR("Cannot set pulse cycles");
|
|
return -EIO;
|
|
}
|
|
}
|
|
|
|
if (need_update) {
|
|
/* Force match so that the new period, duty cycle takes effect immediately */
|
|
Emios_Pwm_Ip_ForceMatchTrailingEdge(instance, channel, true);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
#if defined(EMIOS_PWM_IP_MODE_OPWMCB_USED) || defined(EMIOS_PWM_IP_MODE_OPWMB_USED)
|
|
static int pwm_nxp_s32_set_cycles_external_timebase(uint8_t instance, uint32_t channel,
|
|
uint32_t period_cycles, uint32_t pulse_cycles)
|
|
{
|
|
uint8_t master_channel;
|
|
|
|
if ((period_cycles > EMIOS_PWM_IP_MAX_CNT_VAL) ||
|
|
(period_cycles <= EMIOS_PWM_IP_MIN_CNT_VAL)) {
|
|
LOG_ERR("period_cycles is out of range");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (Emios_Pwm_Ip_GetPeriod(instance, channel) != period_cycles) {
|
|
/*
|
|
* This mode uses internal counter, so change period and cycle
|
|
* don't effect to the others
|
|
*/
|
|
master_channel = Emios_Pwm_Ip_GetMasterBusChannel(instance, channel);
|
|
|
|
if (Emios_Mcl_Ip_SetCounterBusPeriod(instance, master_channel, period_cycles)) {
|
|
LOG_ERR("Cannot set counter period");
|
|
return -EIO;
|
|
}
|
|
}
|
|
|
|
if (Emios_Pwm_Ip_GetDutyCycle(instance, channel) != pulse_cycles) {
|
|
if (Emios_Pwm_Ip_SetDutyCycle(instance, channel, pulse_cycles)) {
|
|
LOG_ERR("Cannot set pulse cycles");
|
|
return -EIO;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
static int pwm_nxp_s32_set_cycles(const struct device *dev, uint32_t channel,
|
|
uint32_t period_cycles, uint32_t pulse_cycles,
|
|
pwm_flags_t flags)
|
|
{
|
|
const struct pwm_nxp_s32_config *config = dev->config;
|
|
|
|
Emios_Pwm_Ip_PwmModeType mode;
|
|
|
|
if (channel >= EMIOS_PWM_IP_CHANNEL_COUNT) {
|
|
LOG_ERR("invalid channel %d", channel);
|
|
return -EINVAL;
|
|
}
|
|
|
|
mode = Emios_Pwm_Ip_GetChannelMode(config->instance, channel);
|
|
if (mode == EMIOS_PWM_IP_MODE_NODEFINE) {
|
|
LOG_ERR("Channel %d is not configured for PWM", channel);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (flags) {
|
|
LOG_ERR("Only support configuring output polarity at boot time");
|
|
return -ENOTSUP;
|
|
}
|
|
|
|
switch (mode) {
|
|
#ifdef EMIOS_PWM_IP_MODE_OPWFMB_USED
|
|
case EMIOS_PWM_IP_MODE_OPWFMB_FLAG:
|
|
return pwm_nxp_s32_set_cycles_internal_timebase(config->instance, channel,
|
|
period_cycles, pulse_cycles);
|
|
#endif
|
|
|
|
#ifdef EMIOS_PWM_IP_MODE_OPWMCB_USED
|
|
case EMIOS_PWM_IP_MODE_OPWMCB_TRAIL_EDGE_FLAG:
|
|
case EMIOS_PWM_IP_MODE_OPWMCB_LEAD_EDGE_FLAG:
|
|
if ((period_cycles % 2)) {
|
|
LOG_ERR("OPWMCB mode: period must be an even number");
|
|
return -EINVAL;
|
|
}
|
|
|
|
return pwm_nxp_s32_set_cycles_external_timebase(config->instance, channel,
|
|
(period_cycles + 2) / 2,
|
|
pulse_cycles);
|
|
#endif
|
|
|
|
#if defined(EMIOS_PWM_IP_MODE_OPWMB_USED)
|
|
case EMIOS_PWM_IP_MODE_OPWMB_FLAG:
|
|
if ((Emios_Pwm_Ip_GetPhaseShift(config->instance, channel) +
|
|
pulse_cycles) > period_cycles) {
|
|
LOG_ERR("OPWMB mode: new duty cycle + phase shift must <= new period");
|
|
return -EINVAL;
|
|
}
|
|
|
|
return pwm_nxp_s32_set_cycles_external_timebase(config->instance, channel,
|
|
period_cycles, pulse_cycles);
|
|
#endif
|
|
|
|
default:
|
|
/* Never reach here */
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int pwm_nxp_s32_get_cycles_per_sec(const struct device *dev,
|
|
uint32_t channel,
|
|
uint64_t *cycles)
|
|
{
|
|
const struct pwm_nxp_s32_config *config = dev->config;
|
|
struct pwm_nxp_s32_data *data = dev->data;
|
|
|
|
uint8_t internal_prescaler, global_prescaler, master_bus;
|
|
|
|
if (Emios_Pwm_Ip_GetChannelMode(config->instance, channel) == EMIOS_PWM_IP_MODE_NODEFINE) {
|
|
LOG_ERR("Channel %d is not configured for PWM", channel);
|
|
return -EINVAL;
|
|
}
|
|
|
|
master_bus = Emios_Pwm_Ip_GetMasterBusChannel(config->instance, channel);
|
|
internal_prescaler = (config->base->CH.UC[master_bus].C2 & eMIOS_C2_UCEXTPRE_MASK) >>
|
|
eMIOS_C2_UCEXTPRE_SHIFT;
|
|
|
|
/* Clock source for internal prescaler is from either eMIOS or eMIOS / global prescaler */
|
|
if (config->base->CH.UC[master_bus].C2 & eMIOS_C2_UCPRECLK_MASK) {
|
|
*cycles = data->emios_clk / (internal_prescaler + 1);
|
|
} else {
|
|
global_prescaler = (config->base->MCR & eMIOS_MCR_GPRE_MASK) >>
|
|
eMIOS_MCR_GPRE_SHIFT;
|
|
*cycles = data->emios_clk / ((internal_prescaler + 1) * (global_prescaler + 1));
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int pwm_nxp_s32_init(const struct device *dev)
|
|
{
|
|
const struct pwm_nxp_s32_config *config = dev->config;
|
|
struct pwm_nxp_s32_data *data = dev->data;
|
|
|
|
const Emios_Pwm_Ip_ChannelConfigType *pwm_info;
|
|
int err = 0;
|
|
uint8_t ch_id;
|
|
|
|
static uint8_t logic_ch;
|
|
|
|
if (!device_is_ready(config->clock_dev)) {
|
|
return -ENODEV;
|
|
}
|
|
|
|
if (clock_control_get_rate(config->clock_dev, config->clock_subsys,
|
|
&data->emios_clk)) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
err = pinctrl_apply_state(config->pincfg, PINCTRL_STATE_DEFAULT);
|
|
if (err < 0) {
|
|
return err;
|
|
}
|
|
|
|
for (ch_id = 0; ch_id < config->pulse_info->pwm_pulse_channels; ch_id++) {
|
|
pwm_info = &config->pulse_info->pwm_info[ch_id];
|
|
eMios_Pwm_Ip_IndexInChState[config->instance][pwm_info->ChannelId] = logic_ch++;
|
|
Emios_Pwm_Ip_InitChannel(config->instance, pwm_info);
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static const struct pwm_driver_api pwm_nxp_s32_driver_api = {
|
|
.set_cycles = pwm_nxp_s32_set_cycles,
|
|
.get_cycles_per_sec = pwm_nxp_s32_get_cycles_per_sec,
|
|
};
|
|
|
|
/* Macros used to glue devicetree with RTD's definition */
|
|
#define BUS_A EMIOS_PWM_IP_BUS_A
|
|
#define BUS_B EMIOS_PWM_IP_BUS_BCDE
|
|
#define BUS_C EMIOS_PWM_IP_BUS_BCDE
|
|
#define BUS_D EMIOS_PWM_IP_BUS_BCDE
|
|
#define BUS_E EMIOS_PWM_IP_BUS_BCDE
|
|
#define BUS_F EMIOS_PWM_IP_BUS_F
|
|
|
|
#define EMIOS_PWM_MODE(mode) DT_CAT3(EMIOS_PWM_IP_, mode, _FLAG)
|
|
#define EMIOS_PWM_POLARITY(mode) DT_CAT(EMIOS_PWM_IP_, mode)
|
|
#define EMIOS_PWM_PS_SRC(mode) DT_CAT(EMIOS_PWM_IP_PS_SRC_, mode)
|
|
|
|
/*
|
|
* If timebase is configured in MCB up/down count mode: pwm period = (2 * master bus's period - 2)
|
|
*/
|
|
#define EMIOS_PWM_PERIOD_TIME_BASE(node_id) \
|
|
COND_CODE_1(DT_ENUM_HAS_VALUE(node_id, mode, MCB_UP_DOWN_COUNTER), \
|
|
(2 * DT_PROP_BY_PHANDLE(node_id, master_bus, period) - 2), \
|
|
(DT_PROP_BY_PHANDLE(node_id, master_bus, period)))
|
|
|
|
#define EMIOS_PWM_IS_MODE_OPWFMB(node_id) \
|
|
DT_ENUM_HAS_VALUE(node_id, pwm_mode, MODE_OPWFMB)
|
|
|
|
#define EMIOS_PWM_IS_MODE_OPWMCB(node_id) \
|
|
UTIL_OR(DT_ENUM_HAS_VALUE(node_id, pwm_mode, MODE_OPWMCB_TRAIL_EDGE), \
|
|
DT_ENUM_HAS_VALUE(node_id, pwm_mode, MODE_OPWMCB_LEAD_EDGE)) \
|
|
|
|
#define EMIOS_PWM_IS_MODE_OPWMB(node_id) \
|
|
DT_ENUM_HAS_VALUE(node_id, pwm_mode, MODE_OPWMB)
|
|
|
|
#define EMIOS_PWM_VERIFY_MASTER_BUS(node_id) \
|
|
BUILD_ASSERT(BIT(DT_PROP(node_id, channel)) & \
|
|
DT_PROP_BY_PHANDLE(node_id, master_bus, channel_mask), \
|
|
"Node "DT_NODE_PATH(node_id)": invalid master bus");
|
|
|
|
#define EMIOS_PWM_LOG(node_id, msg) \
|
|
DT_NODE_PATH(node_id) ": " DT_PROP(node_id, pwm_mode) ": " msg \
|
|
|
|
#define EMIOS_PWM_VERIFY_MODE_OPWFMB(node_id) \
|
|
BUILD_ASSERT(DT_NODE_HAS_PROP(node_id, period), \
|
|
EMIOS_PWM_LOG(node_id, "period must be configured")); \
|
|
BUILD_ASSERT(IN_RANGE(DT_PROP(node_id, period), EMIOS_PWM_IP_MIN_CNT_VAL + 1, \
|
|
EMIOS_PWM_IP_MAX_CNT_VAL), \
|
|
EMIOS_PWM_LOG(node_id, "period is out of range")); \
|
|
BUILD_ASSERT(DT_PROP(node_id, duty_cycle) <= DT_PROP(node_id, period), \
|
|
EMIOS_PWM_LOG(node_id, "duty-cycle must <= period")); \
|
|
BUILD_ASSERT(!DT_NODE_HAS_PROP(node_id, master_bus), \
|
|
EMIOS_PWM_LOG(node_id, "master-bus must not be configured")); \
|
|
BUILD_ASSERT(DT_PROP(node_id, dead_time) == 0, \
|
|
EMIOS_PWM_LOG(node_id, "dead-time must not be configured")); \
|
|
BUILD_ASSERT(DT_PROP(node_id, phase_shift) == 0, \
|
|
EMIOS_PWM_LOG(node_id, "phase-shift must not be configured"));
|
|
|
|
#define EMIOS_PWM_VERIFY_MODE_OPWMCB(node_id) \
|
|
BUILD_ASSERT(DT_ENUM_HAS_VALUE(DT_PHANDLE(node_id, master_bus), mode, \
|
|
MCB_UP_DOWN_COUNTER), \
|
|
EMIOS_PWM_LOG(node_id, "master-bus must be configured in MCB up-down")); \
|
|
BUILD_ASSERT((DT_PROP(node_id, duty_cycle) + DT_PROP(node_id, dead_time)) <= \
|
|
EMIOS_PWM_PERIOD_TIME_BASE(node_id), \
|
|
EMIOS_PWM_LOG(node_id, "duty-cycle + dead-time must <= period")); \
|
|
BUILD_ASSERT(DT_PROP(node_id, dead_time) <= DT_PROP(node_id, duty_cycle), \
|
|
EMIOS_PWM_LOG(node_id, "dead-time must <= duty-cycle")); \
|
|
BUILD_ASSERT(DT_PROP(node_id, phase_shift) == 0, \
|
|
EMIOS_PWM_LOG(node_id, "phase-shift must not be configured")); \
|
|
BUILD_ASSERT(!DT_NODE_HAS_PROP(node_id, period), \
|
|
EMIOS_PWM_LOG(node_id, "period must not be configured," \
|
|
" driver takes the value from master bus")); \
|
|
BUILD_ASSERT(!DT_NODE_HAS_PROP(node_id, prescaler), \
|
|
EMIOS_PWM_LOG(node_id, "prescaler must not be configured," \
|
|
" driver takes the value from master bus")); \
|
|
BUILD_ASSERT(DT_ENUM_HAS_VALUE(node_id, prescaler_src, PRESCALED_CLOCK), \
|
|
EMIOS_PWM_LOG(node_id, "prescaler-src must not be configured," \
|
|
" always use prescalered source")); \
|
|
|
|
#define EMIOS_PWM_VERIFY_MODE_OPWMB(node_id) \
|
|
BUILD_ASSERT(DT_ENUM_HAS_VALUE(DT_PHANDLE(node_id, master_bus), mode, MCB_UP_COUNTER), \
|
|
EMIOS_PWM_LOG(node_id, "master-bus must be configured in MCB up")); \
|
|
BUILD_ASSERT(!DT_NODE_HAS_PROP(node_id, period), \
|
|
EMIOS_PWM_LOG(node_id, "period must not be configured," \
|
|
" driver takes the value from master bus")); \
|
|
BUILD_ASSERT((DT_PROP(node_id, duty_cycle) + DT_PROP(node_id, phase_shift)) <= \
|
|
EMIOS_PWM_PERIOD_TIME_BASE(node_id), \
|
|
EMIOS_PWM_LOG(node_id, "duty-cycle + phase-shift must <= period")); \
|
|
BUILD_ASSERT(DT_PROP(node_id, dead_time) == 0, \
|
|
EMIOS_PWM_LOG(node_id, "dead-time must not be configured")); \
|
|
BUILD_ASSERT(!DT_NODE_HAS_PROP(node_id, prescaler), \
|
|
EMIOS_PWM_LOG(node_id, "prescaler must not be configured")); \
|
|
BUILD_ASSERT(DT_ENUM_HAS_VALUE(node_id, prescaler_src, PRESCALED_CLOCK), \
|
|
EMIOS_PWM_LOG(node_id, "prescaler-src must not be configured," \
|
|
" always use prescalered source")); \
|
|
|
|
#define EMIOS_PWM_VERIFY_CONFIG(node_id) \
|
|
IF_ENABLED(DT_NODE_HAS_PROP(node_id, master_bus), \
|
|
(EMIOS_PWM_VERIFY_MASTER_BUS(node_id))) \
|
|
IF_ENABLED(EMIOS_PWM_IS_MODE_OPWFMB(node_id), \
|
|
(EMIOS_PWM_VERIFY_MODE_OPWFMB(node_id))) \
|
|
IF_ENABLED(EMIOS_PWM_IS_MODE_OPWMCB(node_id), \
|
|
(EMIOS_PWM_VERIFY_MODE_OPWMCB(node_id))) \
|
|
IF_ENABLED(EMIOS_PWM_IS_MODE_OPWMB(node_id), \
|
|
(EMIOS_PWM_VERIFY_MODE_OPWMB(node_id))) \
|
|
|
|
#define EMIOS_PWM_CONFIG(node_id) \
|
|
{ \
|
|
.ChannelId = DT_PROP(node_id, channel), \
|
|
.Mode = EMIOS_PWM_MODE(DT_STRING_TOKEN(node_id, pwm_mode)), \
|
|
.InternalPsSrc = EMIOS_PWM_PS_SRC(DT_STRING_TOKEN(node_id, prescaler_src)), \
|
|
.InternalPs = DT_PROP_OR(node_id, prescaler, \
|
|
DT_PROP_BY_PHANDLE(node_id, master_bus, prescaler)) - 1,\
|
|
.Timebase = DT_STRING_TOKEN_OR(DT_PHANDLE(node_id, master_bus), bus_type, \
|
|
EMIOS_PWM_IP_BUS_INTERNAL), \
|
|
.PhaseShift = DT_PROP(node_id, phase_shift), \
|
|
.DeadTime = DT_PROP(node_id, dead_time), \
|
|
.OutputDisableSource = EMIOS_PWM_IP_OUTPUT_DISABLE_NONE, \
|
|
.OutputPolarity = EMIOS_PWM_POLARITY(DT_STRING_TOKEN(node_id, polarity)), \
|
|
.DebugMode = DT_PROP(node_id, freeze), \
|
|
.PeriodCount = DT_PROP_OR(node_id, period, EMIOS_PWM_PERIOD_TIME_BASE(node_id)),\
|
|
.DutyCycle = DT_PROP(node_id, duty_cycle), \
|
|
},
|
|
|
|
#define EMIOS_PWM_GENERATE_CONFIG(n) \
|
|
DT_INST_FOREACH_CHILD_STATUS_OKAY(n, EMIOS_PWM_VERIFY_CONFIG) \
|
|
const Emios_Pwm_Ip_ChannelConfigType emios_pwm_##n##_init[] = { \
|
|
DT_INST_FOREACH_CHILD_STATUS_OKAY(n, EMIOS_PWM_CONFIG) \
|
|
}; \
|
|
const struct pwm_nxp_s32_pulse_info emios_pwm_##n##_info = { \
|
|
.pwm_pulse_channels = ARRAY_SIZE(emios_pwm_##n##_init), \
|
|
.pwm_info = (Emios_Pwm_Ip_ChannelConfigType *)emios_pwm_##n##_init, \
|
|
};
|
|
|
|
#define EMIOS_NXP_S32_INSTANCE_CHECK(idx, node_id) \
|
|
((DT_REG_ADDR(node_id) == IP_EMIOS_##idx##_BASE) ? idx : 0)
|
|
|
|
#define EMIOS_NXP_S32_GET_INSTANCE(node_id) \
|
|
LISTIFY(__DEBRACKET eMIOS_INSTANCE_COUNT, EMIOS_NXP_S32_INSTANCE_CHECK, (|), node_id)
|
|
|
|
#define PWM_NXP_S32_INIT_DEVICE(n) \
|
|
PINCTRL_DT_INST_DEFINE(n); \
|
|
EMIOS_PWM_GENERATE_CONFIG(n) \
|
|
static const struct pwm_nxp_s32_config pwm_nxp_s32_config_##n = { \
|
|
.base = (eMIOS_Type *)DT_REG_ADDR(DT_INST_PARENT(n)), \
|
|
.instance = EMIOS_NXP_S32_GET_INSTANCE(DT_INST_PARENT(n)), \
|
|
.clock_dev = DEVICE_DT_GET(DT_CLOCKS_CTLR(DT_INST_PARENT(n))), \
|
|
.clock_subsys = (clock_control_subsys_t)DT_CLOCKS_CELL(DT_INST_PARENT(n), name),\
|
|
.pulse_info = (struct pwm_nxp_s32_pulse_info *)&emios_pwm_##n##_info, \
|
|
.pincfg = PINCTRL_DT_INST_DEV_CONFIG_GET(n) \
|
|
}; \
|
|
static struct pwm_nxp_s32_data pwm_nxp_s32_data_##n; \
|
|
DEVICE_DT_INST_DEFINE(n, \
|
|
&pwm_nxp_s32_init, \
|
|
NULL, \
|
|
&pwm_nxp_s32_data_##n, \
|
|
&pwm_nxp_s32_config_##n, \
|
|
POST_KERNEL, \
|
|
CONFIG_PWM_INIT_PRIORITY, \
|
|
&pwm_nxp_s32_driver_api);
|
|
|
|
DT_INST_FOREACH_STATUS_OKAY(PWM_NXP_S32_INIT_DEVICE)
|