From bcc74996841cc237511bdf1a47e886c93d703374 Mon Sep 17 00:00:00 2001 From: David Ullmann Date: Tue, 18 Jul 2023 20:20:43 -0400 Subject: [PATCH] drivers: rt6xx ctimer pwm driver using ctimer to implement pwm api Signed-off-by: David Ullmann --- .../mimxrt685_evk_cm33-pinctrl.dtsi | 9 + .../arm/mimxrt685_evk/mimxrt685_evk_cm33.dts | 2 +- .../clock_control/clock_control_mcux_syscon.c | 2 +- drivers/pwm/CMakeLists.txt | 1 + drivers/pwm/Kconfig | 2 + drivers/pwm/Kconfig.mcux_ctimer | 9 + drivers/pwm/pwm_mcux_ctimer.c | 277 ++++++++++++++++++ dts/bindings/pwm/nxp,ctimer-pwm.yaml | 30 ++ modules/hal_nxp/CMakeLists.txt | 3 + soc/arm/nxp_imx/rt6xx/soc.c | 1 + 10 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 drivers/pwm/Kconfig.mcux_ctimer create mode 100644 drivers/pwm/pwm_mcux_ctimer.c create mode 100644 dts/bindings/pwm/nxp,ctimer-pwm.yaml diff --git a/boards/arm/mimxrt685_evk/mimxrt685_evk_cm33-pinctrl.dtsi b/boards/arm/mimxrt685_evk/mimxrt685_evk_cm33-pinctrl.dtsi index 43c579d558..ef48a752ef 100644 --- a/boards/arm/mimxrt685_evk/mimxrt685_evk_cm33-pinctrl.dtsi +++ b/boards/arm/mimxrt685_evk/mimxrt685_evk_cm33-pinctrl.dtsi @@ -220,4 +220,13 @@ drive-strength = "normal"; }; }; + + pinmux_ctimer2_pwm: pinmux_ctimer2_pwm { + group0 { + pinmux = ; + slew-rate = "normal"; + drive-strength = "normal"; + }; + }; + }; diff --git a/boards/arm/mimxrt685_evk/mimxrt685_evk_cm33.dts b/boards/arm/mimxrt685_evk/mimxrt685_evk_cm33.dts index ecca26796f..f0ef313626 100644 --- a/boards/arm/mimxrt685_evk/mimxrt685_evk_cm33.dts +++ b/boards/arm/mimxrt685_evk/mimxrt685_evk_cm33.dts @@ -55,7 +55,7 @@ }; }; - leds { + leds: leds { compatible = "gpio-leds"; green_led: led_1 { gpios = <&gpio0 14 0>; diff --git a/drivers/clock_control/clock_control_mcux_syscon.c b/drivers/clock_control/clock_control_mcux_syscon.c index a5b8200cf5..a8cf4f240f 100644 --- a/drivers/clock_control/clock_control_mcux_syscon.c +++ b/drivers/clock_control/clock_control_mcux_syscon.c @@ -127,7 +127,7 @@ static int mcux_lpc_syscon_clock_control_get_subsys_rate( break; #endif /* defined(CONFIG_CAN_MCUX_MCAN) */ -#if defined(CONFIG_COUNTER_MCUX_CTIMER) +#if defined(CONFIG_COUNTER_MCUX_CTIMER) || defined(CONFIG_PWM_MCUX_CTIMER) case (MCUX_CTIMER0_CLK + MCUX_CTIMER_CLK_OFFSET): *rate = CLOCK_GetCTimerClkFreq(0); break; diff --git a/drivers/pwm/CMakeLists.txt b/drivers/pwm/CMakeLists.txt index 4619914abc..faa741fa78 100644 --- a/drivers/pwm/CMakeLists.txt +++ b/drivers/pwm/CMakeLists.txt @@ -36,6 +36,7 @@ zephyr_library_sources_ifdef(CONFIG_PWM_BBLED_XEC pwm_mchp_xec_bbled.c) zephyr_library_sources_ifdef(CONFIG_PWM_INTEL_BLINKY pwm_intel_blinky.c) zephyr_library_sources_ifdef(CONFIG_PWM_XMC4XXX_CCU4 pwm_xmc4xxx_ccu4.c) zephyr_library_sources_ifdef(CONFIG_PWM_XMC4XXX_CCU8 pwm_xmc4xxx_ccu8.c) +zephyr_library_sources_ifdef(CONFIG_PWM_MCUX_CTIMER pwm_mcux_ctimer.c) zephyr_library_sources_ifdef(CONFIG_USERSPACE pwm_handlers.c) zephyr_library_sources_ifdef(CONFIG_PWM_CAPTURE pwm_capture.c) diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig index 0c467faf56..c7a7833496 100644 --- a/drivers/pwm/Kconfig +++ b/drivers/pwm/Kconfig @@ -93,4 +93,6 @@ source "drivers/pwm/Kconfig.xmc4xxx_ccu4" source "drivers/pwm/Kconfig.xmc4xxx_ccu8" +source "drivers/pwm/Kconfig.mcux_ctimer" + endif # PWM diff --git a/drivers/pwm/Kconfig.mcux_ctimer b/drivers/pwm/Kconfig.mcux_ctimer new file mode 100644 index 0000000000..cc3ce3030f --- /dev/null +++ b/drivers/pwm/Kconfig.mcux_ctimer @@ -0,0 +1,9 @@ +# (c) Meta Platforms, Inc. and affiliates. +# SPDX-License-Identifier: Apache-2.0 + +config PWM_MCUX_CTIMER + bool "MCUX CTimer PWM driver" + default y + depends on DT_HAS_NXP_CTIMER_PWM_ENABLED + help + Enable ctimer based pwm driver. diff --git a/drivers/pwm/pwm_mcux_ctimer.c b/drivers/pwm/pwm_mcux_ctimer.c new file mode 100644 index 0000000000..64dc9e45fe --- /dev/null +++ b/drivers/pwm/pwm_mcux_ctimer.c @@ -0,0 +1,277 @@ +/* + * (c) Meta Platforms, Inc. and affiliates. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT nxp_ctimer_pwm + +#include +#include +#include +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(pwm_mcux_ctimer, CONFIG_PWM_LOG_LEVEL); + +#define CHANNEL_COUNT kCTIMER_Match_3 + 1 + +enum pwm_ctimer_channel_role { + PWM_CTIMER_CHANNEL_ROLE_NONE = 0, + PWM_CTIMER_CHANNEL_ROLE_PULSE, + PWM_CTIMER_CHANNEL_ROLE_PERIOD, +}; + +struct pwm_ctimer_channel_state { + enum pwm_ctimer_channel_role role; + uint32_t cycles; +}; + +struct pwm_mcux_ctimer_data { + struct pwm_ctimer_channel_state channel_states[CHANNEL_COUNT]; + ctimer_match_t current_period_channel; + bool is_period_channel_set; + uint32_t num_active_pulse_chans; +}; + +struct pwm_mcux_ctimer_config { + CTIMER_Type *base; + uint32_t prescale; + uint32_t period_channel; + const struct device *clock_control; + clock_control_subsys_t clock_id; + const struct pinctrl_dev_config *pincfg; +}; + +/* + * All pwm signals generated from the same ctimer must have same period. To avoid this, we check + * if the new parameters will NOT change the period for a ctimer with active pulse channels + */ +static bool mcux_ctimer_pwm_is_period_valid(struct pwm_mcux_ctimer_data *data, + uint32_t new_pulse_channel, uint32_t new_period_cycles, + uint32_t current_period_channel) +{ + /* if we aren't changing the period, we're ok */ + if (data->channel_states[current_period_channel].cycles == new_period_cycles) { + return true; + } + + /* + * if we are changing it but there aren't any pulse channels that depend on it, then we're + * ok too + */ + if (data->num_active_pulse_chans == 0) { + return true; + } + + if (data->num_active_pulse_chans > 1) { + return false; + } + + /* + * there is exactly one pulse channel that depends on existing period and its not the + * one we're changing now + */ + if (data->channel_states[new_pulse_channel].role != PWM_CTIMER_CHANNEL_ROLE_PULSE) { + return false; + } + + return true; +} + +/* + * Each ctimer channel can either be used as a pulse or period channel. Each channel has a counter. + * The duty cycle is counted by the pulse channel. When the period channel counts down, it resets + * the pulse channel (and all counters in the ctimer instance). The pwm api does not permit us to + * specify a period channel (only pulse channel). So we need to figure out an acceptable period + * channel in the driver (if that's even possible) + */ +static int mcux_ctimer_pwm_select_period_channel(struct pwm_mcux_ctimer_data *data, + uint32_t new_pulse_channel, + uint32_t new_period_cycles, + uint32_t *ret_period_channel) +{ + if (data->is_period_channel_set) { + if (!mcux_ctimer_pwm_is_period_valid(data, new_pulse_channel, new_period_cycles, + data->current_period_channel)) { + LOG_ERR("Cannot set channel %u to %u as period channel", + *ret_period_channel, new_period_cycles); + return -EINVAL; + } + + *ret_period_channel = data->current_period_channel; + if (new_pulse_channel != *ret_period_channel) { + /* the existing period channel will not conflict with new pulse_channel */ + return 0; + } + } + + /* we need to find an unused channel to use as period_channel */ + *ret_period_channel = new_pulse_channel + 1; + *ret_period_channel %= CHANNEL_COUNT; + while (data->channel_states[*ret_period_channel].role != PWM_CTIMER_CHANNEL_ROLE_NONE) { + if (new_pulse_channel == *ret_period_channel) { + LOG_ERR("no available channel for period counter"); + return -EBUSY; + } + (*ret_period_channel)++; + *ret_period_channel %= CHANNEL_COUNT; + } + + return 0; +} + +static void mcux_ctimer_pwm_update_state(struct pwm_mcux_ctimer_data *data, uint32_t pulse_channel, + uint32_t pulse_cycles, uint32_t period_channel, + uint32_t period_cycles) +{ + if (data->channel_states[pulse_channel].role != PWM_CTIMER_CHANNEL_ROLE_PULSE) { + data->num_active_pulse_chans++; + } + + data->channel_states[pulse_channel].role = PWM_CTIMER_CHANNEL_ROLE_PULSE; + data->channel_states[pulse_channel].cycles = pulse_cycles; + + data->is_period_channel_set = true; + data->current_period_channel = period_channel; + data->channel_states[period_channel].role = PWM_CTIMER_CHANNEL_ROLE_PERIOD; + data->channel_states[period_channel].cycles = period_cycles; +} + +static int mcux_ctimer_pwm_set_cycles(const struct device *dev, uint32_t pulse_channel, + uint32_t period_cycles, uint32_t pulse_cycles, + pwm_flags_t flags) +{ + const struct pwm_mcux_ctimer_config *config = dev->config; + struct pwm_mcux_ctimer_data *data = dev->data; + uint32_t period_channel = data->current_period_channel; + int ret = 0; + status_t status; + + if (pulse_channel >= CHANNEL_COUNT) { + LOG_ERR("Invalid channel %u. muse be less than %u", pulse_channel, CHANNEL_COUNT); + return -EINVAL; + } + + if (period_cycles == 0) { + LOG_ERR("Channel can not be set to zero"); + return -ENOTSUP; + } + + ret = mcux_ctimer_pwm_select_period_channel(data, pulse_channel, period_cycles, + &period_channel); + if (ret != 0) { + LOG_ERR("could not select valid period channel. ret=%d", ret); + return ret; + } + + if (flags & PWM_POLARITY_INVERTED) { + if (pulse_cycles == 0) { + /* make pulse cycles greater than period so event never occurs */ + pulse_cycles = period_cycles + 1; + } else { + pulse_cycles = period_cycles - pulse_cycles; + } + } + + status = CTIMER_SetupPwmPeriod(config->base, period_channel, pulse_channel, period_cycles, + pulse_cycles, false); + if (kStatus_Success != status) { + LOG_ERR("failed setup pwm period. status=%d", status); + return -EIO; + } + mcux_ctimer_pwm_update_state(data, pulse_channel, pulse_cycles, period_channel, + period_cycles); + + CTIMER_StartTimer(config->base); + return 0; +} + +static int mcux_ctimer_pwm_get_cycles_per_sec(const struct device *dev, uint32_t channel, + uint64_t *cycles) +{ + const struct pwm_mcux_ctimer_config *config = dev->config; + int err = 0; + + + /* clean up upper word of return parameter */ + *cycles &= 0xFFFFFFFF; + + err = clock_control_get_rate(config->clock_control, config->clock_id, (uint32_t *)cycles); + if (err != 0) { + LOG_ERR("could not get clock rate"); + return err; + } + + if (config->prescale > 0) { + *cycles /= config->prescale; + } + + return err; +} + +static int mcux_ctimer_pwm_init(const struct device *dev) +{ + const struct pwm_mcux_ctimer_config *config = dev->config; + ctimer_config_t pwm_config; + int err; + + err = pinctrl_apply_state(config->pincfg, PINCTRL_STATE_DEFAULT); + if (err) { + return err; + } + + if (config->period_channel >= CHANNEL_COUNT) { + LOG_ERR("invalid period_channel: %d. must be less than %d", config->period_channel, + CHANNEL_COUNT); + return -EINVAL; + } + + CTIMER_GetDefaultConfig(&pwm_config); + pwm_config.prescale = config->prescale; + + CTIMER_Init(config->base, &pwm_config); + return 0; +} + +static const struct pwm_driver_api pwm_mcux_ctimer_driver_api = { + .set_cycles = mcux_ctimer_pwm_set_cycles, + .get_cycles_per_sec = mcux_ctimer_pwm_get_cycles_per_sec, +}; + +#define PWM_MCUX_CTIMER_PINCTRL_DEFINE(n) PINCTRL_DT_INST_DEFINE(n); +#define PWM_MCUX_CTIMER_PINCTRL_INIT(n) .pincfg = PINCTRL_DT_INST_DEV_CONFIG_GET(n), + +#define PWM_MCUX_CTIMER_DEVICE_INIT_MCUX(n) \ + static struct pwm_mcux_ctimer_data pwm_mcux_ctimer_data_##n = { \ + .channel_states = \ + { \ + [kCTIMER_Match_0] = {.role = PWM_CTIMER_CHANNEL_ROLE_NONE, \ + .cycles = 0}, \ + [kCTIMER_Match_1] = {.role = PWM_CTIMER_CHANNEL_ROLE_NONE, \ + .cycles = 0}, \ + [kCTIMER_Match_2] = {.role = PWM_CTIMER_CHANNEL_ROLE_NONE, \ + .cycles = 0}, \ + [kCTIMER_Match_3] = {.role = PWM_CTIMER_CHANNEL_ROLE_NONE, \ + .cycles = 0}, \ + }, \ + .current_period_channel = kCTIMER_Match_0, \ + .is_period_channel_set = false, \ + }; \ + PWM_MCUX_CTIMER_PINCTRL_DEFINE(n) \ + static const struct pwm_mcux_ctimer_config pwm_mcux_ctimer_config_##n = { \ + .base = (CTIMER_Type *)DT_INST_REG_ADDR(n), \ + .prescale = DT_INST_PROP(n, prescaler), \ + .clock_control = DEVICE_DT_GET(DT_INST_CLOCKS_CTLR(n)), \ + .clock_id = (clock_control_subsys_t)(DT_INST_CLOCKS_CELL(n, name) + \ + MCUX_CTIMER_CLK_OFFSET), \ + PWM_MCUX_CTIMER_PINCTRL_INIT(n)}; \ + \ + DEVICE_DT_INST_DEFINE(n, mcux_ctimer_pwm_init, NULL, &pwm_mcux_ctimer_data_##n, \ + &pwm_mcux_ctimer_config_##n, POST_KERNEL, \ + CONFIG_KERNEL_INIT_PRIORITY_DEVICE, &pwm_mcux_ctimer_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(PWM_MCUX_CTIMER_DEVICE_INIT_MCUX) diff --git a/dts/bindings/pwm/nxp,ctimer-pwm.yaml b/dts/bindings/pwm/nxp,ctimer-pwm.yaml new file mode 100644 index 0000000000..7b7f8e31a7 --- /dev/null +++ b/dts/bindings/pwm/nxp,ctimer-pwm.yaml @@ -0,0 +1,30 @@ +# (c) Meta Platforms, Inc. and affiliates. +# SPDX-License-Identifier: Apache-2.0 + +description: NXP CTimer PWM + +compatible: "nxp,ctimer-pwm" + +include: [pwm-controller.yaml, pinctrl-device.yaml, base.yaml, "nxp,lpc-ctimer.yaml"] + +properties: + reg: + required: true + + prescaler: + type: int + default: 1 + description: prescaling value + + clk-source: + type: int + required: true + description: clock to use + + "#pwm-cells": + const: 3 + +pwm-cells: + - channel + - period + - flags diff --git a/modules/hal_nxp/CMakeLists.txt b/modules/hal_nxp/CMakeLists.txt index 572b0bc654..d187c8fda4 100644 --- a/modules/hal_nxp/CMakeLists.txt +++ b/modules/hal_nxp/CMakeLists.txt @@ -8,5 +8,8 @@ if(CONFIG_HAS_MCUX OR CONFIG_HAS_IMX_HAL OR CONFIG_HAS_S32_HAL) add_subdirectory(${ZEPHYR_CURRENT_MODULE_DIR} hal_nxp) add_subdirectory_ifdef(CONFIG_USB_DEVICE_DRIVER usb) + zephyr_sources_ifdef(CONFIG_PWM_MCUX_CTIMER ${ZEPHYR_CURRENT_MODULE_DIR}/mcux/mcux-sdk/drivers/ctimer/fsl_ctimer.c) + zephyr_include_directories_ifdef(CONFIG_PWM_MCUX_CTIMER + ${ZEPHYR_CURRENT_MODULE_DIR}/mcux/mcux-sdk/drivers/ctimer/) zephyr_include_directories(.) endif() diff --git a/soc/arm/nxp_imx/rt6xx/soc.c b/soc/arm/nxp_imx/rt6xx/soc.c index b691187c86..8ba2aa9dce 100644 --- a/soc/arm/nxp_imx/rt6xx/soc.c +++ b/soc/arm/nxp_imx/rt6xx/soc.c @@ -298,6 +298,7 @@ static ALWAYS_INLINE void clock_init(void) #endif DT_FOREACH_STATUS_OKAY(nxp_lpc_ctimer, CTIMER_CLOCK_SETUP) + DT_FOREACH_STATUS_OKAY(nxp_ctimer_pwm, CTIMER_CLOCK_SETUP) #if (DT_NODE_HAS_COMPAT_STATUS(DT_NODELABEL(i3c0), nxp_mcux_i3c, okay)) CLOCK_AttachClk(kFFRO_to_I3C_CLK);