From fd3a1ffc21027f8c37745fa92780befb0b0abcd5 Mon Sep 17 00:00:00 2001 From: morris Date: Sat, 28 May 2022 17:02:07 +0800 Subject: [PATCH 1/8] mcpwm: deprecate legacy driver --- components/driver/Kconfig | 14 + .../{include => deprecated}/driver/mcpwm.h | 292 +---------------- .../deprecated/driver/mcpwm_types_legacy.h | 307 ++++++++++++++++++ .../{mcpwm.c => deprecated/mcpwm_legacy.c} | 37 ++- .../legacy_mcpwm_driver/CMakeLists.txt | 5 + .../test_apps/legacy_mcpwm_driver/README.md | 2 + .../legacy_mcpwm_driver/main/CMakeLists.txt | 7 + .../legacy_mcpwm_driver/main/test_app_main.c | 40 +++ .../main/test_legacy_mcpwm.c} | 3 - .../pytest_legacy_mcpwm.py | 21 ++ .../legacy_mcpwm_driver/sdkconfig.ci.release | 5 + .../legacy_mcpwm_driver/sdkconfig.defaults | 3 + 12 files changed, 436 insertions(+), 300 deletions(-) rename components/driver/{include => deprecated}/driver/mcpwm.h (59%) create mode 100644 components/driver/deprecated/driver/mcpwm_types_legacy.h rename components/driver/{mcpwm.c => deprecated/mcpwm_legacy.c} (97%) create mode 100644 components/driver/test_apps/legacy_mcpwm_driver/CMakeLists.txt create mode 100644 components/driver/test_apps/legacy_mcpwm_driver/README.md create mode 100644 components/driver/test_apps/legacy_mcpwm_driver/main/CMakeLists.txt create mode 100644 components/driver/test_apps/legacy_mcpwm_driver/main/test_app_main.c rename components/driver/{test/test_pwm.c => test_apps/legacy_mcpwm_driver/main/test_legacy_mcpwm.c} (99%) create mode 100644 components/driver/test_apps/legacy_mcpwm_driver/pytest_legacy_mcpwm.py create mode 100644 components/driver/test_apps/legacy_mcpwm_driver/sdkconfig.ci.release create mode 100644 components/driver/test_apps/legacy_mcpwm_driver/sdkconfig.defaults diff --git a/components/driver/Kconfig b/components/driver/Kconfig index bb46a084f6..8def437f13 100644 --- a/components/driver/Kconfig +++ b/components/driver/Kconfig @@ -352,6 +352,20 @@ menu "Driver Configurations" cache misses, and also be able to run whilst the cache is disabled. (e.g. SPI Flash write) + config MCPWM_SUPPRESS_DEPRECATE_WARN + bool "Suppress leagcy driver deprecated warning" + default n + help + Wether to suppress the deprecation warnings when using legacy MCPWM driver (driver/mcpwm.h). + If you want to continue using the legacy driver, and don't want to see related deprecation warnings, + you can enable this option. + + config MCPWM_ENABLE_DEBUG_LOG + bool "Enable debug log" + default n + help + Wether to enable the debug log message for MCPWM driver. + Note that, this option only controls the MCPWM driver log, won't affect other drivers. endmenu # MCPWM Configuration menu "I2S Configuration" diff --git a/components/driver/include/driver/mcpwm.h b/components/driver/deprecated/driver/mcpwm.h similarity index 59% rename from components/driver/include/driver/mcpwm.h rename to components/driver/deprecated/driver/mcpwm.h index 08f4adc8b1..e8ff38778f 100644 --- a/components/driver/include/driver/mcpwm.h +++ b/components/driver/deprecated/driver/mcpwm.h @@ -9,298 +9,16 @@ #include #include #include "esp_err.h" -#include "esp_bit_defs.h" -#include "esp_intr_alloc.h" -#include "soc/soc_caps.h" -#include "hal/mcpwm_types.h" +#include "driver/mcpwm_types_legacy.h" + +#if !CONFIG_MCPWM_SUPPRESS_DEPRECATE_WARN +#warning "legacy MCPWM driver is deprecated, please migrate to the new driver (include driver/mcpwm_prelude.h)" +#endif #ifdef __cplusplus extern "C" { #endif -/** - * @brief IO signals for the MCPWM - * - * - 6 MCPWM output pins that generate PWM signals - * - 3 MCPWM fault input pins to detect faults like overcurrent, overvoltage, etc. - * - 3 MCPWM sync input pins to synchronize MCPWM outputs signals - * - 3 MCPWM capture input pins to gather feedback from controlled motors, using e.g. hall sensors - */ -typedef enum { - MCPWM0A = 0, /*! +#include +#include "esp_bit_defs.h" +#include "soc/soc_caps.h" +#include "hal/mcpwm_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief IO signals for the MCPWM + * + * - 6 MCPWM output pins that generate PWM signals + * - 3 MCPWM fault input pins to detect faults like over-current, over-voltage, etc. + * - 3 MCPWM sync input pins to synchronize MCPWM outputs signals + * - 3 MCPWM capture input pins to gather feedback from controlled motors, using e.g. hall sensors + */ +typedef enum { + MCPWM0A = 0, /*!dev, op, gen, MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_LOW); mcpwm_ll_generator_set_action_on_timer_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_FULL, MCPWM_ACTION_NO_CHANGE); mcpwm_ll_generator_set_action_on_compare_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_UP, gen, MCPWM_ACTION_FORCE_HIGH); - } else if (duty_type == MCPWM_HAL_GENERATOR_MODE_FORCE_LOW) { + } else if (duty_type == MCPWM_DUTY_MODE_FORCE_LOW) { mcpwm_ll_generator_set_action_on_timer_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_ACTION_FORCE_LOW); mcpwm_ll_generator_set_action_on_timer_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_FULL, MCPWM_ACTION_FORCE_LOW); mcpwm_ll_generator_set_action_on_compare_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_UP, gen, MCPWM_ACTION_FORCE_LOW); - } else if (duty_type == MCPWM_HAL_GENERATOR_MODE_FORCE_HIGH) { + } else if (duty_type == MCPWM_DUTY_MODE_FORCE_HIGH) { mcpwm_ll_generator_set_action_on_timer_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_ACTION_FORCE_HIGH); mcpwm_ll_generator_set_action_on_timer_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_FULL, MCPWM_ACTION_FORCE_HIGH); mcpwm_ll_generator_set_action_on_compare_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_UP, gen, MCPWM_ACTION_FORCE_HIGH); @@ -345,11 +346,11 @@ esp_err_t mcpwm_set_duty_type(mcpwm_unit_t mcpwm_num, mcpwm_timer_t timer_num, m mcpwm_ll_generator_set_action_on_timer_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_DOWN, MCPWM_TIMER_EVENT_FULL, MCPWM_ACTION_FORCE_HIGH); mcpwm_ll_generator_set_action_on_timer_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_DOWN, MCPWM_TIMER_EVENT_EMPTY, MCPWM_ACTION_NO_CHANGE); mcpwm_ll_generator_set_action_on_compare_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_DOWN, gen, MCPWM_ACTION_FORCE_LOW); - } else if (duty_type == MCPWM_HAL_GENERATOR_MODE_FORCE_LOW) { + } else if (duty_type == MCPWM_DUTY_MODE_FORCE_LOW) { mcpwm_ll_generator_set_action_on_timer_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_DOWN, MCPWM_TIMER_EVENT_FULL, MCPWM_ACTION_FORCE_LOW); mcpwm_ll_generator_set_action_on_timer_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_DOWN, MCPWM_TIMER_EVENT_EMPTY, MCPWM_ACTION_FORCE_LOW); mcpwm_ll_generator_set_action_on_compare_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_DOWN, gen, MCPWM_ACTION_FORCE_LOW); - } else if (duty_type == MCPWM_HAL_GENERATOR_MODE_FORCE_HIGH) { + } else if (duty_type == MCPWM_DUTY_MODE_FORCE_HIGH) { mcpwm_ll_generator_set_action_on_timer_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_DOWN, MCPWM_TIMER_EVENT_FULL, MCPWM_ACTION_FORCE_HIGH); mcpwm_ll_generator_set_action_on_timer_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_DOWN, MCPWM_TIMER_EVENT_EMPTY, MCPWM_ACTION_FORCE_HIGH); mcpwm_ll_generator_set_action_on_compare_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_DOWN, gen, MCPWM_ACTION_FORCE_HIGH); @@ -364,14 +365,14 @@ esp_err_t mcpwm_set_duty_type(mcpwm_unit_t mcpwm_num, mcpwm_timer_t timer_num, m mcpwm_ll_generator_set_action_on_timer_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_ACTION_FORCE_LOW); mcpwm_ll_generator_set_action_on_compare_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_UP, gen, MCPWM_ACTION_FORCE_HIGH); mcpwm_ll_generator_set_action_on_compare_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_DOWN, gen, MCPWM_ACTION_FORCE_LOW); - } else if (duty_type == MCPWM_HAL_GENERATOR_MODE_FORCE_LOW) { + } else if (duty_type == MCPWM_DUTY_MODE_FORCE_LOW) { mcpwm_ll_generator_set_action_on_timer_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_DOWN, MCPWM_TIMER_EVENT_FULL, MCPWM_ACTION_FORCE_LOW); mcpwm_ll_generator_set_action_on_timer_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_DOWN, MCPWM_TIMER_EVENT_EMPTY, MCPWM_ACTION_FORCE_LOW); mcpwm_ll_generator_set_action_on_timer_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_ACTION_FORCE_LOW); mcpwm_ll_generator_set_action_on_timer_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_FULL, MCPWM_ACTION_FORCE_LOW); mcpwm_ll_generator_set_action_on_compare_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_DOWN, gen, MCPWM_ACTION_FORCE_LOW); mcpwm_ll_generator_set_action_on_compare_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_UP, gen, MCPWM_ACTION_FORCE_LOW); - } else if (duty_type == MCPWM_HAL_GENERATOR_MODE_FORCE_HIGH) { + } else if (duty_type == MCPWM_DUTY_MODE_FORCE_HIGH) { mcpwm_ll_generator_set_action_on_timer_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_DOWN, MCPWM_TIMER_EVENT_FULL, MCPWM_ACTION_FORCE_HIGH); mcpwm_ll_generator_set_action_on_timer_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_DOWN, MCPWM_TIMER_EVENT_EMPTY, MCPWM_ACTION_FORCE_HIGH); mcpwm_ll_generator_set_action_on_timer_event(hal->dev, op, gen, MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_ACTION_FORCE_HIGH); @@ -463,13 +464,13 @@ uint32_t mcpwm_get_duty_in_us(mcpwm_unit_t mcpwm_num, mcpwm_timer_t timer_num, m esp_err_t mcpwm_set_signal_high(mcpwm_unit_t mcpwm_num, mcpwm_timer_t timer_num, mcpwm_generator_t gen) { //the driver currently always use the timer x for operator x - return mcpwm_set_duty_type(mcpwm_num, timer_num, gen, MCPWM_HAL_GENERATOR_MODE_FORCE_HIGH); + return mcpwm_set_duty_type(mcpwm_num, timer_num, gen, MCPWM_DUTY_MODE_FORCE_HIGH); } esp_err_t mcpwm_set_signal_low(mcpwm_unit_t mcpwm_num, mcpwm_timer_t timer_num, mcpwm_generator_t gen) { //the driver currently always use the timer x for operator x - return mcpwm_set_duty_type(mcpwm_num, timer_num, gen, MCPWM_HAL_GENERATOR_MODE_FORCE_LOW); + return mcpwm_set_duty_type(mcpwm_num, timer_num, gen, MCPWM_DUTY_MODE_FORCE_LOW); } esp_err_t mcpwm_carrier_enable(mcpwm_unit_t mcpwm_num, mcpwm_timer_t timer_num) @@ -943,3 +944,19 @@ esp_err_t mcpwm_set_timer_sync_output(mcpwm_unit_t mcpwm_num, mcpwm_timer_t time mcpwm_critical_exit(mcpwm_num); return ESP_OK; } + +/** + * @brief This function will be called during start up, to check that this legacy mcpwm driver is not running along with the new MCPWM driver + */ +__attribute__((constructor)) +static void check_mcpwm_driver_conflict(void) +{ + // This function was declared as weak here. The new MCPWM driver has the implementation. + // So if the new MCPWM driver is not linked in, then `mcpwm_acquire_group_handle()` should be NULL at runtime. + extern __attribute__((weak)) void *mcpwm_acquire_group_handle(int group_id); + if ((void *)mcpwm_acquire_group_handle != NULL) { + ESP_EARLY_LOGE(TAG, "CONFLICT! driver_ng is not allowed to be used with the legacy driver"); + abort(); + } + ESP_EARLY_LOGW(TAG, "legacy driver is deprecated, please migrate to `driver/mcpwm_prelude.h`"); +} diff --git a/components/driver/test_apps/legacy_mcpwm_driver/CMakeLists.txt b/components/driver/test_apps/legacy_mcpwm_driver/CMakeLists.txt new file mode 100644 index 0000000000..6a6fac6b51 --- /dev/null +++ b/components/driver/test_apps/legacy_mcpwm_driver/CMakeLists.txt @@ -0,0 +1,5 @@ +# This is the project CMakeLists.txt file for the test subproject +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(legacy_mcpwm_driver_test) diff --git a/components/driver/test_apps/legacy_mcpwm_driver/README.md b/components/driver/test_apps/legacy_mcpwm_driver/README.md new file mode 100644 index 0000000000..5ab630aafd --- /dev/null +++ b/components/driver/test_apps/legacy_mcpwm_driver/README.md @@ -0,0 +1,2 @@ +| Supported Targets | ESP32 | ESP32-S3 | +| ----------------- | ----- | -------- | diff --git a/components/driver/test_apps/legacy_mcpwm_driver/main/CMakeLists.txt b/components/driver/test_apps/legacy_mcpwm_driver/main/CMakeLists.txt new file mode 100644 index 0000000000..f5ad62dfcf --- /dev/null +++ b/components/driver/test_apps/legacy_mcpwm_driver/main/CMakeLists.txt @@ -0,0 +1,7 @@ +set(srcs "test_app_main.c" + "test_legacy_mcpwm.c") + +# In order for the cases defined by `TEST_CASE` to be linked into the final elf, +# the component can be registered as WHOLE_ARCHIVE +idf_component_register(SRCS ${srcs} + WHOLE_ARCHIVE) diff --git a/components/driver/test_apps/legacy_mcpwm_driver/main/test_app_main.c b/components/driver/test_apps/legacy_mcpwm_driver/main/test_app_main.c new file mode 100644 index 0000000000..027d2b69c0 --- /dev/null +++ b/components/driver/test_apps/legacy_mcpwm_driver/main/test_app_main.c @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "unity.h" +#include "unity_test_runner.h" +#include "esp_heap_caps.h" + +#define TEST_MEMORY_LEAK_THRESHOLD (-300) + +static size_t before_free_8bit; +static size_t before_free_32bit; + +static void check_leak(size_t before_free, size_t after_free, const char *type) +{ + ssize_t delta = after_free - before_free; + printf("MALLOC_CAP_%s: Before %u bytes free, After %u bytes free (delta %d)\n", type, before_free, after_free, delta); + TEST_ASSERT_MESSAGE(delta >= TEST_MEMORY_LEAK_THRESHOLD, "memory leak"); +} + +void setUp(void) +{ + before_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT); + before_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT); +} + +void tearDown(void) +{ + size_t after_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT); + size_t after_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT); + check_leak(before_free_8bit, after_free_8bit, "8BIT"); + check_leak(before_free_32bit, after_free_32bit, "32BIT"); +} + +void app_main(void) +{ + unity_run_menu(); +} diff --git a/components/driver/test/test_pwm.c b/components/driver/test_apps/legacy_mcpwm_driver/main/test_legacy_mcpwm.c similarity index 99% rename from components/driver/test/test_pwm.c rename to components/driver/test_apps/legacy_mcpwm_driver/main/test_legacy_mcpwm.c index 0a3240b01c..72a41a7bc8 100644 --- a/components/driver/test/test_pwm.c +++ b/components/driver/test_apps/legacy_mcpwm_driver/main/test_legacy_mcpwm.c @@ -12,7 +12,6 @@ #include "hal/gpio_hal.h" #include "esp_rom_gpio.h" #include "esp_private/esp_clk.h" -#if SOC_MCPWM_SUPPORTED #include "soc/mcpwm_periph.h" #include "driver/pulse_cnt.h" #include "driver/mcpwm.h" @@ -568,5 +567,3 @@ TEST_CASE("MCPWM capture test", "[mcpwm]") } } } - -#endif // SOC_MCPWM_SUPPORTED diff --git a/components/driver/test_apps/legacy_mcpwm_driver/pytest_legacy_mcpwm.py b/components/driver/test_apps/legacy_mcpwm_driver/pytest_legacy_mcpwm.py new file mode 100644 index 0000000000..25da5646d2 --- /dev/null +++ b/components/driver/test_apps/legacy_mcpwm_driver/pytest_legacy_mcpwm.py @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded import Dut + + +@pytest.mark.esp32 +@pytest.mark.esp32s3 +@pytest.mark.generic +@pytest.mark.parametrize( + 'config', + [ + 'release', + ], + indirect=True, +) +def test_legacy_mcpwm(dut: Dut) -> None: + dut.expect('Press ENTER to see the list of tests') + dut.write('*') + dut.expect_unity_test_output() diff --git a/components/driver/test_apps/legacy_mcpwm_driver/sdkconfig.ci.release b/components/driver/test_apps/legacy_mcpwm_driver/sdkconfig.ci.release new file mode 100644 index 0000000000..91d93f163e --- /dev/null +++ b/components/driver/test_apps/legacy_mcpwm_driver/sdkconfig.ci.release @@ -0,0 +1,5 @@ +CONFIG_PM_ENABLE=y +CONFIG_FREERTOS_USE_TICKLESS_IDLE=y +CONFIG_COMPILER_OPTIMIZATION_SIZE=y +CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y +CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT=y diff --git a/components/driver/test_apps/legacy_mcpwm_driver/sdkconfig.defaults b/components/driver/test_apps/legacy_mcpwm_driver/sdkconfig.defaults new file mode 100644 index 0000000000..4c808e5adc --- /dev/null +++ b/components/driver/test_apps/legacy_mcpwm_driver/sdkconfig.defaults @@ -0,0 +1,3 @@ +CONFIG_FREERTOS_HZ=1000 +CONFIG_ESP_TASK_WDT=n +CONFIG_MCPWM_SUPPRESS_DEPRECATE_WARN=y From 1571417679fe73c94ed82d55d106ef94ea8e6eb6 Mon Sep 17 00:00:00 2001 From: morris Date: Sat, 28 May 2022 17:03:05 +0800 Subject: [PATCH 2/8] mcpwm: new driver implementation --- components/driver/.build-test-rules.yml | 8 + components/driver/CMakeLists.txt | 15 +- components/driver/include/driver/mcpwm_cap.h | 194 ++++++ components/driver/include/driver/mcpwm_cmpr.h | 91 +++ .../driver/include/driver/mcpwm_fault.h | 112 +++ components/driver/include/driver/mcpwm_gen.h | 187 +++++ components/driver/include/driver/mcpwm_oper.h | 159 +++++ .../driver/include/driver/mcpwm_prelude.h | 20 + components/driver/include/driver/mcpwm_sync.h | 114 ++++ .../driver/include/driver/mcpwm_timer.h | 144 ++++ .../driver/include/driver/mcpwm_types.h | 145 ++++ components/driver/include/esp_private/mcpwm.h | 36 + components/driver/mcpwm/mcpwm_cap.c | 420 ++++++++++++ components/driver/mcpwm/mcpwm_cmpr.c | 210 ++++++ components/driver/mcpwm/mcpwm_com.c | 135 ++++ components/driver/mcpwm/mcpwm_fault.c | 302 ++++++++ components/driver/mcpwm/mcpwm_gen.c | 264 +++++++ components/driver/mcpwm/mcpwm_oper.c | 363 ++++++++++ components/driver/mcpwm/mcpwm_private.h | 221 ++++++ components/driver/mcpwm/mcpwm_sync.c | 297 ++++++++ components/driver/mcpwm/mcpwm_timer.c | 364 ++++++++++ .../driver/test_apps/mcpwm/CMakeLists.txt | 18 + components/driver/test_apps/mcpwm/README.md | 2 + .../test_apps/mcpwm/main/CMakeLists.txt | 14 + .../test_apps/mcpwm/main/test_app_main.c | 51 ++ .../test_apps/mcpwm/main/test_mcpwm_cap.c | 238 +++++++ .../test_apps/mcpwm/main/test_mcpwm_cmpr.c | 113 +++ .../test_apps/mcpwm/main/test_mcpwm_fault.c | 93 +++ .../test_apps/mcpwm/main/test_mcpwm_gen.c | 646 ++++++++++++++++++ .../test_apps/mcpwm/main/test_mcpwm_oper.c | 380 +++++++++++ .../test_apps/mcpwm/main/test_mcpwm_sync.c | 204 ++++++ .../test_apps/mcpwm/main/test_mcpwm_timer.c | 186 +++++ .../test_apps/mcpwm/main/test_mcpwm_utils.c | 20 + .../test_apps/mcpwm/main/test_mcpwm_utils.h | 17 + .../driver/test_apps/mcpwm/pytest_mcpwm.py | 22 + .../test_apps/mcpwm/sdkconfig.ci.iram_safe | 5 + .../test_apps/mcpwm/sdkconfig.ci.release | 5 + .../driver/test_apps/mcpwm/sdkconfig.defaults | 2 + 38 files changed, 5815 insertions(+), 2 deletions(-) create mode 100644 components/driver/include/driver/mcpwm_cap.h create mode 100644 components/driver/include/driver/mcpwm_cmpr.h create mode 100644 components/driver/include/driver/mcpwm_fault.h create mode 100644 components/driver/include/driver/mcpwm_gen.h create mode 100644 components/driver/include/driver/mcpwm_oper.h create mode 100644 components/driver/include/driver/mcpwm_prelude.h create mode 100644 components/driver/include/driver/mcpwm_sync.h create mode 100644 components/driver/include/driver/mcpwm_timer.h create mode 100644 components/driver/include/driver/mcpwm_types.h create mode 100644 components/driver/include/esp_private/mcpwm.h create mode 100644 components/driver/mcpwm/mcpwm_cap.c create mode 100644 components/driver/mcpwm/mcpwm_cmpr.c create mode 100644 components/driver/mcpwm/mcpwm_com.c create mode 100644 components/driver/mcpwm/mcpwm_fault.c create mode 100644 components/driver/mcpwm/mcpwm_gen.c create mode 100644 components/driver/mcpwm/mcpwm_oper.c create mode 100644 components/driver/mcpwm/mcpwm_private.h create mode 100644 components/driver/mcpwm/mcpwm_sync.c create mode 100644 components/driver/mcpwm/mcpwm_timer.c create mode 100644 components/driver/test_apps/mcpwm/CMakeLists.txt create mode 100644 components/driver/test_apps/mcpwm/README.md create mode 100644 components/driver/test_apps/mcpwm/main/CMakeLists.txt create mode 100644 components/driver/test_apps/mcpwm/main/test_app_main.c create mode 100644 components/driver/test_apps/mcpwm/main/test_mcpwm_cap.c create mode 100644 components/driver/test_apps/mcpwm/main/test_mcpwm_cmpr.c create mode 100644 components/driver/test_apps/mcpwm/main/test_mcpwm_fault.c create mode 100644 components/driver/test_apps/mcpwm/main/test_mcpwm_gen.c create mode 100644 components/driver/test_apps/mcpwm/main/test_mcpwm_oper.c create mode 100644 components/driver/test_apps/mcpwm/main/test_mcpwm_sync.c create mode 100644 components/driver/test_apps/mcpwm/main/test_mcpwm_timer.c create mode 100644 components/driver/test_apps/mcpwm/main/test_mcpwm_utils.c create mode 100644 components/driver/test_apps/mcpwm/main/test_mcpwm_utils.h create mode 100644 components/driver/test_apps/mcpwm/pytest_mcpwm.py create mode 100644 components/driver/test_apps/mcpwm/sdkconfig.ci.iram_safe create mode 100644 components/driver/test_apps/mcpwm/sdkconfig.ci.release create mode 100644 components/driver/test_apps/mcpwm/sdkconfig.defaults diff --git a/components/driver/.build-test-rules.yml b/components/driver/.build-test-rules.yml index d25382f32d..69fef02250 100644 --- a/components/driver/.build-test-rules.yml +++ b/components/driver/.build-test-rules.yml @@ -8,6 +8,10 @@ components/driver/test_apps/i2s_test_apps/legacy_i2s_adc_dac: disable: - if: SOC_I2S_SUPPORTS_ADC_DAC != 1 +components/driver/test_apps/legacy_mcpwm_driver: + disable: + - if: SOC_MCPWM_SUPPORTED != 1 + components/driver/test_apps/legacy_pcnt_driver: disable: - if: SOC_PCNT_SUPPORTED != 1 @@ -20,6 +24,10 @@ components/driver/test_apps/legacy_rtc_temp_driver: disable: - if: SOC_TEMP_SENSOR_SUPPORTED != 1 +components/driver/test_apps/mcpwm: + disable: + - if: SOC_MCPWM_SUPPORTED != 1 + components/driver/test_apps/pulse_cnt: disable: - if: SOC_PCNT_SUPPORTED != 1 diff --git a/components/driver/CMakeLists.txt b/components/driver/CMakeLists.txt index c4c6e852be..5dc25f9776 100644 --- a/components/driver/CMakeLists.txt +++ b/components/driver/CMakeLists.txt @@ -32,7 +32,15 @@ if(CONFIG_SOC_ADC_DMA_SUPPORTED) endif() if(CONFIG_SOC_MCPWM_SUPPORTED) - list(APPEND srcs "mcpwm.c") + list(APPEND srcs "mcpwm/mcpwm_cap.c" + "mcpwm/mcpwm_cmpr.c" + "mcpwm/mcpwm_com.c" + "mcpwm/mcpwm_fault.c" + "mcpwm/mcpwm_gen.c" + "mcpwm/mcpwm_oper.c" + "mcpwm/mcpwm_sync.c" + "mcpwm/mcpwm_timer.c" + "deprecated/mcpwm_legacy.c") endif() if(CONFIG_SOC_DEDICATED_GPIO_SUPPORTED) @@ -88,9 +96,12 @@ if(CONFIG_SOC_TOUCH_SENSOR_SUPPORTED) list(APPEND srcs "touch_sensor_common.c" "${target}/touch_sensor.c") endif() +if(CONFIG_SOC_SDIO_SLAVE_SUPPORTED) + list(APPEND srcs "sdio_slave.c") +endif() + if(${target} STREQUAL "esp32") list(APPEND srcs "dac_common.c" - "sdio_slave.c" "deprecated/adc_i2s_deprecated.c" "esp32/dac.c") endif() diff --git a/components/driver/include/driver/mcpwm_cap.h b/components/driver/include/driver/mcpwm_cap.h new file mode 100644 index 0000000000..54aa3db59b --- /dev/null +++ b/components/driver/include/driver/mcpwm_cap.h @@ -0,0 +1,194 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include "esp_err.h" +#include "driver/mcpwm_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief MCPWM capture timer configuration structure + */ +typedef struct { + int group_id; /*!< Specify from which group to allocate the capture timer */ + mcpwm_capture_clock_source_t clk_src; /*!< MCPWM capture timer clock source */ +} mcpwm_capture_timer_config_t; + +/** + * @brief Create MCPWM capture timer + * + * @param[in] config MCPWM capture timer configuration + * @param[out] ret_cap_timer Returned MCPWM capture timer handle + * @return + * - ESP_OK: Create MCPWM capture timer successfully + * - ESP_ERR_INVALID_ARG: Create MCPWM capture timer failed because of invalid argument + * - ESP_ERR_NO_MEM: Create MCPWM capture timer failed because out of memory + * - ESP_ERR_NOT_FOUND: Create MCPWM capture timer failed because can't find free resource + * - ESP_FAIL: Create MCPWM capture timer failed because of other error + */ +esp_err_t mcpwm_new_capture_timer(const mcpwm_capture_timer_config_t *config, mcpwm_cap_timer_handle_t *ret_cap_timer); + +/** + * @brief Delete MCPWM capture timer + * + * @param[in] cap_timer MCPWM capture timer, allocated by `mcpwm_new_capture_timer()` + * @return + * - ESP_OK: Delete MCPWM capture timer successfully + * - ESP_ERR_INVALID_ARG: Delete MCPWM capture timer failed because of invalid argument + * - ESP_FAIL: Delete MCPWM capture timer failed because of other error + */ +esp_err_t mcpwm_del_capture_timer(mcpwm_cap_timer_handle_t cap_timer); + +/** + * @brief Enable MCPWM capture timer + * + * @param[in] cap_timer MCPWM capture timer handle, allocated by `mcpwm_new_capture_timer()` + * @return + * - ESP_OK: Enable MCPWM capture timer successfully + * - ESP_ERR_INVALID_ARG: Enable MCPWM capture timer failed because of invalid argument + * - ESP_ERR_INVALID_STATE: Enable MCPWM capture timer failed because timer is enabled already + * - ESP_FAIL: Enable MCPWM capture timer failed because of other error + */ +esp_err_t mcpwm_capture_timer_enable(mcpwm_cap_timer_handle_t cap_timer); + +/** + * @brief Disable MCPWM capture timer + * + * @param[in] cap_timer MCPWM capture timer handle, allocated by `mcpwm_new_capture_timer()` + * @return + * - ESP_OK: Disable MCPWM capture timer successfully + * - ESP_ERR_INVALID_ARG: Disable MCPWM capture timer failed because of invalid argument + * - ESP_ERR_INVALID_STATE: Disable MCPWM capture timer failed because timer is disabled already + * - ESP_FAIL: Disable MCPWM capture timer failed because of other error + */ +esp_err_t mcpwm_capture_timer_disable(mcpwm_cap_timer_handle_t cap_timer); + +/** + * @brief Start MCPWM capture timer + * + * @param[in] cap_timer MCPWM capture timer, allocated by `mcpwm_new_capture_timer()` + * @return + * - ESP_OK: Start MCPWM capture timer successfully + * - ESP_ERR_INVALID_ARG: Start MCPWM capture timer failed because of invalid argument + * - ESP_FAIL: Start MCPWM capture timer failed because of other error + */ +esp_err_t mcpwm_capture_timer_start(mcpwm_cap_timer_handle_t cap_timer); + +/** + * @brief Start MCPWM capture timer + * + * @param[in] cap_timer MCPWM capture timer, allocated by `mcpwm_new_capture_timer()` + * @return + * - ESP_OK: Stop MCPWM capture timer successfully + * - ESP_ERR_INVALID_ARG: Stop MCPWM capture timer failed because of invalid argument + * - ESP_FAIL: Stop MCPWM capture timer failed because of other error + */ +esp_err_t mcpwm_capture_timer_stop(mcpwm_cap_timer_handle_t cap_timer); + +/** + * @brief MCPWM Capture timer sync phase configuration + */ +typedef struct { + mcpwm_sync_handle_t sync_src; /*!< The sync event source */ + uint32_t count_value; /*!< The count value that should lock to upon sync event */ + mcpwm_timer_direction_t direction; /*!< The count direction that should lock to upon sync event */ +} mcpwm_capture_timer_sync_phase_config_t; + +/** + * @brief Set sync phase for MCPWM capture timer + * + * @param[in] cap_timer MCPWM capture timer, allocated by `mcpwm_new_capture_timer()` + * @param[in] config MCPWM capture timer sync phase configuration + * @return + * - ESP_OK: Set sync phase for MCPWM capture timer successfully + * - ESP_ERR_INVALID_ARG: Set sync phase for MCPWM capture timer failed because of invalid argument + * - ESP_FAIL: Set sync phase for MCPWM capture timer failed because of other error + */ +esp_err_t mcpwm_capture_timer_set_phase_on_sync(mcpwm_cap_timer_handle_t cap_timer, const mcpwm_capture_timer_sync_phase_config_t *config); + +/** + * @brief MCPWM capture channel configuration structure + */ +typedef struct { + int gpio_num; /*!< GPIO used capturing input signal */ + uint32_t prescale; /*!< Prescale of input signal, effective frequency = cap_input_clk/prescale */ + struct { + uint32_t pos_edge: 1; /*!< Whether to capture on positive edge */ + uint32_t neg_edge: 1; /*!< Whether to capture on negative edge */ + uint32_t pull_up: 1; /*!< Whether to pull up internally */ + uint32_t pull_down: 1; /*!< Whether to pull down internally */ + uint32_t invert_cap_signal: 1; /*!< Invert the input capture signal */ + uint32_t io_loop_back: 1; /*!< For debug/test, the signal output from the GPIO will be fed to the input path as well */ + } flags; /*!< Extra configuration flags for capture channel */ +} mcpwm_capture_channel_config_t; + +/** + * @brief Create MCPWM capture channel + * + * @param[in] cap_timer MCPWM capture timer, allocated by `mcpwm_new_capture_timer()`, will be connected to the new capture channel + * @param[in] config MCPWM capture channel configuration + * @param[out] ret_cap_channel Returned MCPWM capture channel + * @return + * - ESP_OK: Create MCPWM capture channel successfully + * - ESP_ERR_INVALID_ARG: Create MCPWM capture channel failed because of invalid argument + * - ESP_ERR_NO_MEM: Create MCPWM capture channel failed because out of memory + * - ESP_ERR_NOT_FOUND: Create MCPWM capture channel failed because can't find free resource + * - ESP_FAIL: Create MCPWM capture channel failed because of other error + */ +esp_err_t mcpwm_new_capture_channel(mcpwm_cap_timer_handle_t cap_timer, const mcpwm_capture_channel_config_t *config, mcpwm_cap_channel_handle_t *ret_cap_channel); + +/** + * @brief Delete MCPWM capture channel + * + * @param[in] cap_channel MCPWM capture channel handle, allocated by `mcpwm_new_capture_channel()` + * @return + * - ESP_OK: Delete MCPWM capture channel successfully + * - ESP_ERR_INVALID_ARG: Delete MCPWM capture channel failed because of invalid argument + * - ESP_FAIL: Delete MCPWM capture channel failed because of other error + */ +esp_err_t mcpwm_del_capture_channel(mcpwm_cap_channel_handle_t cap_channel); + +/** + * @brief Group of supported MCPWM capture event callbacks + * @note The callbacks are all running under ISR environment + */ +typedef struct { + mcpwm_capture_event_cb_t on_cap; /*!< Callback function that would be invoked when capture event occurred */ +} mcpwm_capture_event_callbacks_t; + +/** + * @brief Set event callbacks for MCPWM capture channel + * + * @param[in] cap_channel MCPWM capture channel handle, allocated by `mcpwm_new_capture_channel()` + * @param[in] cbs Group of callback functions + * @param[in] user_data User data, which will be passed to callback functions directly + * @return + * - ESP_OK: Set event callbacks successfully + * - ESP_ERR_INVALID_ARG: Set event callbacks failed because of invalid argument + * - ESP_FAIL: Set event callbacks failed because of other error + */ +esp_err_t mcpwm_capture_channel_register_event_callbacks(mcpwm_cap_channel_handle_t cap_channel, const mcpwm_capture_event_callbacks_t *cbs, void *user_data); + +/** + * @brief Trigger a catch by software + * + * @param[in] cap_channel MCPWM capture channel handle, allocated by `mcpwm_new_capture_channel()` + * @return + * - ESP_OK: Trigger software catch successfully + * - ESP_ERR_INVALID_ARG: Trigger software catch failed because of invalid argument + * - ESP_FAIL: Trigger software catch failed because of other error + */ +esp_err_t mcpwm_capture_channel_trigger_soft_catch(mcpwm_cap_channel_handle_t cap_channel); + +#ifdef __cplusplus +} +#endif diff --git a/components/driver/include/driver/mcpwm_cmpr.h b/components/driver/include/driver/mcpwm_cmpr.h new file mode 100644 index 0000000000..d539f8a220 --- /dev/null +++ b/components/driver/include/driver/mcpwm_cmpr.h @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include "esp_err.h" +#include "driver/mcpwm_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief MCPWM comparator configuration + */ +typedef struct { + struct { + uint32_t update_cmp_on_tez: 1; /*!< Whether to update compare value when timer count equals to zero (tez) */ + uint32_t update_cmp_on_tep: 1; /*!< Whether to update compare value when timer count equals to peak (tep) */ + uint32_t update_cmp_on_sync: 1; /*!< Whether to update compare value on sync event */ + } flags; /*!< Extra configuration flags for comparator */ +} mcpwm_comparator_config_t; + +/** + * @brief Create MCPWM comparator + * + * @param[in] oper MCPWM operator, allocated by `mcpwm_new_operator()`, the new comparator will be allocated from this operator + * @param[in] config MCPWM comparator configuration + * @param[out] ret_cmpr Returned MCPWM comparator + * @return + * - ESP_OK: Create MCPWM comparator successfully + * - ESP_ERR_INVALID_ARG: Create MCPWM comparator failed because of invalid argument + * - ESP_ERR_NO_MEM: Create MCPWM comparator failed because out of memory + * - ESP_ERR_NOT_FOUND: Create MCPWM comparator failed because can't find free resource + * - ESP_FAIL: Create MCPWM comparator failed because of other error + */ +esp_err_t mcpwm_new_comparator(mcpwm_oper_handle_t oper, const mcpwm_comparator_config_t *config, mcpwm_cmpr_handle_t *ret_cmpr); + +/** + * @brief Delete MCPWM comparator + * + * @param[in] cmpr MCPWM comparator handle, allocated by `mcpwm_new_comparator()` + * @return + * - ESP_OK: Delete MCPWM comparator successfully + * - ESP_ERR_INVALID_ARG: Delete MCPWM comparator failed because of invalid argument + * - ESP_FAIL: Delete MCPWM comparator failed because of other error + */ +esp_err_t mcpwm_del_comparator(mcpwm_cmpr_handle_t cmpr); + +/** + * @brief Group of supported MCPWM compare event callbacks + * @note The callbacks are all running under ISR environment + */ +typedef struct { + mcpwm_compare_event_cb_t on_reach; /*!< ISR callback function which would be invoked when counter reaches compare value */ +} mcpwm_comparator_event_callbacks_t; + +/** + * @brief Set event callbacks for MCPWM comparator + * + * @param[in] cmpr MCPWM comparator handle, allocated by `mcpwm_new_comparator()` + * @param[in] cbs Group of callback functions + * @param[in] user_data User data, which will be passed to callback functions directly + * @return + * - ESP_OK: Set event callbacks successfully + * - ESP_ERR_INVALID_ARG: Set event callbacks failed because of invalid argument + * - ESP_FAIL: Set event callbacks failed because of other error + */ +esp_err_t mcpwm_comparator_register_event_callbacks(mcpwm_cmpr_handle_t cmpr, const mcpwm_comparator_event_callbacks_t *cbs, void *user_data); + +/** + * @brief Set MCPWM comparator's compare value + * + * @param[in] cmpr MCPWM comparator handle, allocated by `mcpwm_new_comparator()` + * @param[in] cmp_ticks The new compare value + * @return + * - ESP_OK: Set MCPWM compare value successfully + * - ESP_ERR_INVALID_ARG: Set MCPWM compare value failed because of invalid argument (e.g. the cmp_ticks is out of range) + * - ESP_ERR_INVALID_STATE: Set MCPWM compare value failed because the operator doesn't have a timer connected + * - ESP_FAIL: Set MCPWM compare value failed because of other error + */ +esp_err_t mcpwm_comparator_set_compare_value(mcpwm_cmpr_handle_t cmpr, uint32_t cmp_ticks); + +#ifdef __cplusplus +} +#endif diff --git a/components/driver/include/driver/mcpwm_fault.h b/components/driver/include/driver/mcpwm_fault.h new file mode 100644 index 0000000000..ddc635c679 --- /dev/null +++ b/components/driver/include/driver/mcpwm_fault.h @@ -0,0 +1,112 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include "esp_err.h" +#include "driver/mcpwm_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief MCPWM GPIO fault configuration structure + */ +typedef struct { + int group_id; /*!< In which MCPWM group that the GPIO fault belongs to */ + int gpio_num; /*!< GPIO used by the fault signal */ + struct { + uint32_t active_level: 1; /*!< On which level the fault signal is treated as active */ + uint32_t io_loop_back: 1; /*!< For debug/test, the signal output from the GPIO will be fed to the input path as well */ + uint32_t pull_up: 1; /*!< Whether to pull up internally */ + uint32_t pull_down: 1; /*!< Whether to pull down internally */ + } flags; /*!< Extra configuration flags for GPIO fault */ +} mcpwm_gpio_fault_config_t; + +/** + * @brief Create MCPWM GPIO fault + * + * @param[in] config MCPWM GPIO fault configuration + * @param[out] ret_fault Returned GPIO fault handle + * @return + * - ESP_OK: Create MCPWM GPIO fault successfully + * - ESP_ERR_INVALID_ARG: Create MCPWM GPIO fault failed because of invalid argument + * - ESP_ERR_NO_MEM: Create MCPWM GPIO fault failed because out of memory + * - ESP_ERR_NOT_FOUND: Create MCPWM GPIO fault failed because can't find free resource + * - ESP_FAIL: Create MCPWM GPIO fault failed because of other error + */ +esp_err_t mcpwm_new_gpio_fault(const mcpwm_gpio_fault_config_t *config, mcpwm_fault_handle_t *ret_fault); + +/** + * @brief MCPWM software fault configuration structure + */ +typedef struct { +} mcpwm_soft_fault_config_t; + +/** + * @brief Create MCPWM software fault + * + * @param[in] config MCPWM software fault configuration + * @param[out] ret_fault Returned software fault handle + * @return + * - ESP_OK: Create MCPWM software fault successfully + * - ESP_ERR_INVALID_ARG: Create MCPWM software fault failed because of invalid argument + * - ESP_ERR_NO_MEM: Create MCPWM software fault failed because out of memory + * - ESP_FAIL: Create MCPWM software fault failed because of other error + */ +esp_err_t mcpwm_new_soft_fault(const mcpwm_soft_fault_config_t *config, mcpwm_fault_handle_t *ret_fault); + +/** + * @brief Delete MCPWM fault + * + * @param[in] fault MCPWM fault handle allocated by `mcpwm_new_gpio_fault()` or `mcpwm_new_soft_fault()` + * @return + * - ESP_OK: Delete MCPWM fault successfully + * - ESP_ERR_INVALID_ARG: Delete MCPWM fault failed because of invalid argument + * - ESP_FAIL: Delete MCPWM fault failed because of other error + */ +esp_err_t mcpwm_del_fault(mcpwm_fault_handle_t fault); + +/** + * @brief Activate the software fault, trigger the fault event for once + * + * @param[in] fault MCPWM soft fault, allocated by `mcpwm_new_soft_fault()` + * @return + * - ESP_OK: Trigger MCPWM software fault event successfully + * - ESP_ERR_INVALID_ARG: Trigger MCPWM software fault event failed because of invalid argument + * - ESP_FAIL: Trigger MCPWM software fault event failed because of other error + */ +esp_err_t mcpwm_soft_fault_activate(mcpwm_fault_handle_t fault); + +/** + * @brief Group of supported MCPWM fault event callbacks + * @note The callbacks are all running under ISR environment + */ +typedef struct { + mcpwm_fault_event_cb_t on_fault_enter; /*!< ISR callback function that would be invoked when fault signal becomes active */ + mcpwm_fault_event_cb_t on_fault_exit; /*!< ISR callback function that would be invoked when fault signal becomes inactive */ +} mcpwm_fault_event_callbacks_t; + +/** + * @brief Set event callbacks for MCPWM fault + * + * @param[in] fault MCPWM GPIO fault handle, allocated by `mcpwm_new_gpio_fault()` + * @param[in] cbs Group of callback functions + * @param[in] user_data User data, which will be passed to callback functions directly + * @return + * - ESP_OK: Set event callbacks successfully + * - ESP_ERR_INVALID_ARG: Set event callbacks failed because of invalid argument + * - ESP_FAIL: Set event callbacks failed because of other error + */ +esp_err_t mcpwm_fault_register_event_callbacks(mcpwm_fault_handle_t fault, const mcpwm_fault_event_callbacks_t *cbs, void *user_data); + +#ifdef __cplusplus +} +#endif diff --git a/components/driver/include/driver/mcpwm_gen.h b/components/driver/include/driver/mcpwm_gen.h new file mode 100644 index 0000000000..96b31e9b96 --- /dev/null +++ b/components/driver/include/driver/mcpwm_gen.h @@ -0,0 +1,187 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include "esp_err.h" +#include "driver/mcpwm_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief MCPWM generator configuration + */ +typedef struct { + int gen_gpio_num; /*!< The GPIO number used to output the PWM signal */ + struct { + uint32_t invert_pwm: 1; /*!< Whether to invert the PWM signal (done by GPIO matrix) */ + uint32_t io_loop_back: 1; /*!< For debug/test, the signal output from the GPIO will be fed to the input path as well */ + } flags; /*!< Extra configuration flags for generator */ +} mcpwm_generator_config_t; + +/** + * @brief Allocate MCPWM generator from given operator + * + * @param[in] oper MCPWM operator, allocated by `mcpwm_new_operator()` + * @param[in] config MCPWM generator configuration + * @param[out] ret_gen Returned MCPWM generator + * @return + * - ESP_OK: Create MCPWM generator successfully + * - ESP_ERR_INVALID_ARG: Create MCPWM generator failed because of invalid argument + * - ESP_ERR_NO_MEM: Create MCPWM generator failed because out of memory + * - ESP_ERR_NOT_FOUND: Create MCPWM generator failed because can't find free resource + * - ESP_FAIL: Create MCPWM generator failed because of other error + */ +esp_err_t mcpwm_new_generator(mcpwm_oper_handle_t oper, const mcpwm_generator_config_t *config, mcpwm_gen_handle_t *ret_gen); + +/** + * @brief Delete MCPWM generator + * + * @param[in] gen MCPWM generator handle, allocated by `mcpwm_new_generator()` + * @return + * - ESP_OK: Delete MCPWM generator successfully + * - ESP_ERR_INVALID_ARG: Delete MCPWM generator failed because of invalid argument + * - ESP_FAIL: Delete MCPWM generator failed because of other error + */ +esp_err_t mcpwm_del_generator(mcpwm_gen_handle_t gen); + +/** + * @brief Set force level for MCPWM generator + * + * @note The force level will be applied to the generator immediately, regardless any other events that would change the generator's behaviour. + * @note If the `hold_on` is true, the force level will retain forever, until user removes the force level by setting the force level to `-1`. + * @note If the `hold_on` is false, the force level can be overridden by the next event action. + * + * @param[in] gen MCPWM generator handle, allocated by `mcpwm_new_generator()` + * @param[in] level GPIO level to be applied to MCPWM generator, specially, -1 means to remove the force level + * @param[in] hold_on Whether the forced PWM level should retain (i.e. will remain unchanged until manually remove the force level) + * @return + * - ESP_OK: Set force level for MCPWM generator successfully + * - ESP_ERR_INVALID_ARG: Set force level for MCPWM generator failed because of invalid argument + * - ESP_FAIL: Set force level for MCPWM generator failed because of other error + */ +esp_err_t mcpwm_generator_set_force_level(mcpwm_gen_handle_t gen, int level, bool hold_on); + +/** + * @brief Generator action on specific timer event + */ +typedef struct { + mcpwm_timer_direction_t direction; /*!< Timer direction */ + mcpwm_timer_event_t event; /*!< Timer event */ + mcpwm_generator_action_t action; /*!< Generator action should perform */ +} mcpwm_gen_timer_event_action_t; + +/** + * @brief Help macros to construct a mcpwm_gen_timer_event_action_t entry + */ +#define MCPWM_GEN_TIMER_EVENT_ACTION(dir, ev, act) \ + (mcpwm_gen_timer_event_action_t) { .direction = dir, .event = ev, .action = act } +#define MCPWM_GEN_TIMER_EVENT_ACTION_END() \ + (mcpwm_gen_timer_event_action_t) { .event = MCPWM_TIMER_EVENT_INVALID } + +/** + * @brief Set generator actions on different MCPWM timer events + * + * @param[in] gen MCPWM generator handle, allocated by `mcpwm_new_generator()` + * @param[in] ev_act MCPWM timer event action list, must be terminated by `MCPWM_GEN_TIMER_EVENT_ACTION_END()` + * @return + * - ESP_OK: Set generator actions successfully + * - ESP_ERR_INVALID_ARG: Set generator actions failed because of invalid argument + * - ESP_ERR_INVALID_STATE: Set generator actions failed because of timer is not connected to operator + * - ESP_FAIL: Set generator actions failed because of other error + */ +esp_err_t mcpwm_generator_set_actions_on_timer_event(mcpwm_gen_handle_t gen, mcpwm_gen_timer_event_action_t ev_act, ...); + +/** + * @brief Generator action on specific comparator event + */ +typedef struct { + mcpwm_timer_direction_t direction; /*!< Timer direction */ + mcpwm_cmpr_handle_t comparator; /*!< Comparator handle */ + mcpwm_generator_action_t action; /*!< Generator action should perform */ +} mcpwm_gen_compare_event_action_t; + +/** + * @brief Help macros to construct a mcpwm_gen_compare_event_action_t entry + */ +#define MCPWM_GEN_COMPARE_EVENT_ACTION(dir, cmp, act) \ + (mcpwm_gen_compare_event_action_t) { .direction = dir, .comparator = cmp, .action = act } +#define MCPWM_GEN_COMPARE_EVENT_ACTION_END() \ + (mcpwm_gen_compare_event_action_t) { .comparator = NULL } + +/** + * @brief Set generator actions on different MCPWM compare events + * + * @param[in] generator MCPWM generator handle, allocated by `mcpwm_new_generator()` + * @param[in] ev_act MCPWM compare event action list, must be terminated by `MCPWM_GEN_COMPARE_EVENT_ACTION_END()` + * @return + * - ESP_OK: Set generator actions successfully + * - ESP_ERR_INVALID_ARG: Set generator actions failed because of invalid argument + * - ESP_FAIL: Set generator actions failed because of other error + */ +esp_err_t mcpwm_generator_set_actions_on_compare_event(mcpwm_gen_handle_t generator, mcpwm_gen_compare_event_action_t ev_act, ...); + +/** + * @brief Generator action on specific brake event + */ +typedef struct { + mcpwm_timer_direction_t direction; /*!< Timer direction */ + mcpwm_operator_brake_mode_t brake_mode; /*!< Brake mode */ + mcpwm_generator_action_t action; /*!< Generator action should perform */ +} mcpwm_gen_brake_event_action_t; + +/** + * @brief Help macros to construct a mcpwm_gen_brake_event_action_t entry + */ +#define MCPWM_GEN_BRAKE_EVENT_ACTION(dir, mode, act) \ + (mcpwm_gen_brake_event_action_t) { .direction = dir, .brake_mode = mode, .action = act } +#define MCPWM_GEN_BRAKE_EVENT_ACTION_END() \ + (mcpwm_gen_brake_event_action_t) { .brake_mode = MCPWM_OPER_BRAKE_MODE_INVALID } + +/** + * @brief Set generator actions on different MCPWM brake events + * + * @param[in] generator MCPWM generator handle, allocated by `mcpwm_new_generator()` + * @param[in] ev_act MCPWM brake event action list, must be terminated by `MCPWM_GEN_BRAKE_EVENT_ACTION_END()` + * @return + * - ESP_OK: Set generator actions successfully + * - ESP_ERR_INVALID_ARG: Set generator actions failed because of invalid argument + * - ESP_FAIL: Set generator actions failed because of other error + */ +esp_err_t mcpwm_generator_set_actions_on_brake_event(mcpwm_gen_handle_t generator, mcpwm_gen_brake_event_action_t ev_act, ...); + +/** + * @brief MCPWM dead time configuration structure + */ +typedef struct { + uint32_t posedge_delay_ticks; /*!< delay time applied to rising edge, 0 means no rising delay time */ + uint32_t negedge_delay_ticks; /*!< delay time applied to falling edge, 0 means no falling delay time */ + struct { + uint32_t invert_output: 1; /*!< Invert the signal after applied the dead time */ + } flags; /*!< Extra flags for dead time configuration */ +} mcpwm_dead_time_config_t; + +/** + * @brief Set dead time for MCPWM generator + * + * @param[in] in_generator MCPWM generator, before adding the dead time + * @param[in] out_generator MCPWM generator, after adding the dead time + * @param[in] config MCPWM dead time configuration + * @return + * - ESP_OK: Set dead time for MCPWM generator successfully + * - ESP_ERR_INVALID_ARG: Set dead time for MCPWM generator failed because of invalid argument + * - ESP_FAIL: Set dead time for MCPWM generator failed because of other error + */ +esp_err_t mcpwm_generator_set_dead_time(mcpwm_gen_handle_t in_generator, mcpwm_gen_handle_t out_generator, const mcpwm_dead_time_config_t *config); + +#ifdef __cplusplus +} +#endif diff --git a/components/driver/include/driver/mcpwm_oper.h b/components/driver/include/driver/mcpwm_oper.h new file mode 100644 index 0000000000..5f89f806fc --- /dev/null +++ b/components/driver/include/driver/mcpwm_oper.h @@ -0,0 +1,159 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include "esp_err.h" +#include "driver/mcpwm_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief MCPWM operator configuration + */ +typedef struct { + int group_id; /*!< Specify from which group to allocate the MCPWM operator */ + struct { + uint32_t update_gen_action_on_tez: 1; /*!< Whether to update generator action when timer counts to zero */ + uint32_t update_gen_action_on_tep: 1; /*!< Whether to update generator action when timer counts to peak */ + uint32_t update_gen_action_on_sync: 1; /*!< Whether to update generator action on sync event */ + uint32_t update_dead_time_on_tez: 1; /*!< Whether to update dead time when timer counts to zero */ + uint32_t update_dead_time_on_tep: 1; /*!< Whether to update dead time when timer counts to peak */ + uint32_t update_dead_time_on_sync: 1; /*!< Whether to update dead time on sync event */ + } flags; /*!< Extra configuration flags for operator */ +} mcpwm_operator_config_t; + +/** + * @brief Create MCPWM operator + * + * @param[in] config MCPWM operator configuration + * @param[out] ret_oper Returned MCPWM operator handle + * @return + * - ESP_OK: Create MCPWM operator successfully + * - ESP_ERR_INVALID_ARG: Create MCPWM operator failed because of invalid argument + * - ESP_ERR_NO_MEM: Create MCPWM operator failed because out of memory + * - ESP_ERR_NOT_FOUND: Create MCPWM operator failed because can't find free resource + * - ESP_FAIL: Create MCPWM operator failed because of other error + */ +esp_err_t mcpwm_new_operator(const mcpwm_operator_config_t *config, mcpwm_oper_handle_t *ret_oper); + +/** + * @brief Delete MCPWM operator + * + * @param[in] oper MCPWM operator, allocated by `mcpwm_new_operator()` + * @return + * - ESP_OK: Delete MCPWM operator successfully + * - ESP_ERR_INVALID_ARG: Delete MCPWM operator failed because of invalid argument + * - ESP_FAIL: Delete MCPWM operator failed because of other error + */ +esp_err_t mcpwm_del_operator(mcpwm_oper_handle_t oper); + +/** + * @brief Connect MCPWM operator and timer, so that the operator can be driven by the timer + * + * @param[in] oper MCPWM operator handle, allocated by `mcpwm_new_operator()` + * @param[in] timer MCPWM timer handle, allocated by `mcpwm_new_timer()` + * @return + * - ESP_OK: Connect MCPWM operator and timer successfully + * - ESP_ERR_INVALID_ARG: Connect MCPWM operator and timer failed because of invalid argument + * - ESP_FAIL: Connect MCPWM operator and timer failed because of other error + */ +esp_err_t mcpwm_operator_connect_timer(mcpwm_oper_handle_t oper, mcpwm_timer_handle_t timer); + +/** + * @brief MCPWM brake configuration structure + */ +typedef struct { + mcpwm_fault_handle_t fault; /*!< Which fault causes the operator to brake */ + mcpwm_operator_brake_mode_t brake_mode; /*!< Brake mode */ + struct { + uint32_t cbc_recover_on_tez: 1; /*!< Recovery CBC brake state on tez event */ + uint32_t cbc_recover_on_tep: 1; /*!< Recovery CBC brake state on tep event */ + } flags; /*!< Extra flags for brake configuration */ +} mcpwm_brake_config_t; + +/** + * @brief Set brake method for MCPWM operator + * + * @param[in] operator MCPWM operator, allocated by `mcpwm_new_operator()` + * @param[in] config MCPWM brake configuration + * @return + * - ESP_OK: Set trip for operator successfully + * - ESP_ERR_INVALID_ARG: Set trip for operator failed because of invalid argument + * - ESP_FAIL: Set trip for operator failed because of other error + */ +esp_err_t mcpwm_operator_set_brake_on_fault(mcpwm_oper_handle_t operator, const mcpwm_brake_config_t *config); + +/** + * @brief Try to make the operator recover from fault + * + * @note To recover from fault or escape from trip, you make sure the fault signal has dissappeared already. + * Otherwise the recovery can't succeed. + * + * @param[in] operator MCPWM operator, allocated by `mcpwm_new_operator()` + * @param[in] fault MCPWM fault handle + * @return + * - ESP_OK: Recover from fault successfully + * - ESP_ERR_INVALID_ARG: Recover from fault failed because of invalid argument + * - ESP_ERR_INVALID_STATE: Recover from fault failed because the fault source is still active + * - ESP_FAIL: Recover from fault failed because of other error + */ +esp_err_t mcpwm_operator_recover_from_fault(mcpwm_oper_handle_t operator, mcpwm_fault_handle_t fault); + +/** + * @brief Group of supported MCPWM operator event callbacks + * @note The callbacks are all running under ISR environment + */ +typedef struct { + mcpwm_brake_event_cb_t on_brake_cbc; /*!< callback function when mcpwm operator brakes in CBC */ + mcpwm_brake_event_cb_t on_brake_ost; /*!< callback function when mcpwm operator brakes in OST */ +} mcpwm_operator_event_callbacks_t; + +/** + * @brief Set event callbacks for MCPWM operator + * + * @param[in] oper MCPWM operator handle, allocated by `mcpwm_new_operator()` + * @param[in] cbs Group of callback functions + * @param[in] user_data User data, which will be passed to callback functions directly + * @return + * - ESP_OK: Set event callbacks successfully + * - ESP_ERR_INVALID_ARG: Set event callbacks failed because of invalid argument + * - ESP_FAIL: Set event callbacks failed because of other error + */ +esp_err_t mcpwm_operator_register_event_callbacks(mcpwm_oper_handle_t oper, const mcpwm_operator_event_callbacks_t *cbs, void *user_data); + +/** + * @brief MCPWM carrier configuration structure + */ +typedef struct { + uint32_t frequency_hz; /*!< Carrier frequency in Hz */ + uint32_t first_pulse_duration_us; /*!< The duration of the first PWM pulse, in us */ + float duty_cycle; /*!< Carrier duty cycle */ + struct { + uint32_t invert_before_modulate: 1; /*!< Invert the raw signal */ + uint32_t invert_after_modulate: 1; /*!< Invert the modulated signal */ + } flags; /*!< Extra flags for carrier configuration */ +} mcpwm_carrier_config_t; + +/** + * @brief Apply carrier feature for MCPWM operator + * + * @param[in] oper MCPWM operator, allocated by `mcpwm_new_operator()` + * @param[in] config MCPWM carrier specific configuration + * @return + * - ESP_OK: Set carrier for operator successfully + * - ESP_ERR_INVALID_ARG: Set carrier for operator failed because of invalid argument + * - ESP_FAIL: Set carrier for operator failed because of other error + */ +esp_err_t mcpwm_operator_apply_carrier(mcpwm_oper_handle_t oper, const mcpwm_carrier_config_t *config); + +#ifdef __cplusplus +} +#endif diff --git a/components/driver/include/driver/mcpwm_prelude.h b/components/driver/include/driver/mcpwm_prelude.h new file mode 100644 index 0000000000..fd460ece38 --- /dev/null +++ b/components/driver/include/driver/mcpwm_prelude.h @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @brief MCPWM peripheral contains many submodules, whose drivers are scattered in different header files. + * This header file serves as a prelude, contains every thing that is needed to work with the MCPWM peripheral. + */ + +#pragma once + +#include "driver/mcpwm_timer.h" +#include "driver/mcpwm_oper.h" +#include "driver/mcpwm_cmpr.h" +#include "driver/mcpwm_gen.h" +#include "driver/mcpwm_fault.h" +#include "driver/mcpwm_sync.h" +#include "driver/mcpwm_cap.h" diff --git a/components/driver/include/driver/mcpwm_sync.h b/components/driver/include/driver/mcpwm_sync.h new file mode 100644 index 0000000000..7f691e9fa6 --- /dev/null +++ b/components/driver/include/driver/mcpwm_sync.h @@ -0,0 +1,114 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include "esp_err.h" +#include "driver/mcpwm_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief MCPWM timer sync source configuration + */ +typedef struct { + mcpwm_timer_event_t timer_event; /*!< Timer event, upon which MCPWM timer will generate the sync signal */ + struct { + uint32_t propagate_input_sync: 1; /*!< The input sync signal would be routed to its sync output */ + } flags; /*!< Extra configuration flags for timer sync source */ +} mcpwm_timer_sync_src_config_t; + +/** + * @brief Create MCPWM timer sync source + * + * @param[in] timer MCPWM timer handle, allocated by `mcpwm_new_timer()` + * @param[in] config MCPWM timer sync source configuration + * @param[out] ret_sync Returned MCPWM sync handle + * @return + * - ESP_OK: Create MCPWM timer sync source successfully + * - ESP_ERR_INVALID_ARG: Create MCPWM timer sync source failed because of invalid argument + * - ESP_ERR_NO_MEM: Create MCPWM timer sync source failed because out of memory + * - ESP_ERR_INVALID_STATE: Create MCPWM timer sync source failed because the timer has created a sync source before + * - ESP_FAIL: Create MCPWM timer sync source failed because of other error + */ +esp_err_t mcpwm_new_timer_sync_src(mcpwm_timer_handle_t timer, const mcpwm_timer_sync_src_config_t *config, mcpwm_sync_handle_t *ret_sync); + +/** + * @brief MCPWM GPIO sync source configuration + */ +typedef struct { + int group_id; /*!< MCPWM group ID */ + int gpio_num; /*!< GPIO used by sync source */ + struct { + uint32_t active_neg: 1; /*!< Whether the sync signal is active on negedge, by default, the sync signal's posedge is treated as active */ + uint32_t io_loop_back: 1; /*!< For debug/test, the signal output from the GPIO will be fed to the input path as well */ + uint32_t pull_up: 1; /*!< Whether to pull up internally */ + uint32_t pull_down: 1; /*!< Whether to pull down internally */ + } flags; /*!< Extra configuration flags for GPIO sync source */ +} mcpwm_gpio_sync_src_config_t; + +/** + * @brief Create MCPWM GPIO sync source + * + * @param[in] config MCPWM GPIO sync source configuration + * @param[out] ret_sync Returned MCPWM GPIO sync handle + * @return + * - ESP_OK: Create MCPWM GPIO sync source successfully + * - ESP_ERR_INVALID_ARG: Create MCPWM GPIO sync source failed because of invalid argument + * - ESP_ERR_NO_MEM: Create MCPWM GPIO sync source failed because out of memory + * - ESP_ERR_NOT_FOUND: Create MCPWM GPIO sync source failed because can't find free resource + * - ESP_FAIL: Create MCPWM GPIO sync source failed because of other error + */ +esp_err_t mcpwm_new_gpio_sync_src(const mcpwm_gpio_sync_src_config_t *config, mcpwm_sync_handle_t *ret_sync); + +/** + * @brief MCPWM software sync configuration structure + */ +typedef struct { +} mcpwm_soft_sync_config_t; + +/** + * @brief Create MCPWM software sync source + * + * @param[in] config MCPWM software sync source configuration + * @param[out] ret_sync Returned software sync handle + * @return + * - ESP_OK: Create MCPWM software sync successfully + * - ESP_ERR_INVALID_ARG: Create MCPWM software sync failed because of invalid argument + * - ESP_ERR_NO_MEM: Create MCPWM software sync failed because out of memory + * - ESP_FAIL: Create MCPWM software sync failed because of other error + */ +esp_err_t mcpwm_new_soft_sync_src(const mcpwm_soft_sync_config_t *config, mcpwm_sync_handle_t *ret_sync); + +/** + * @brief Delete MCPWM sync source + * + * @param[in] sync MCPWM sync handle, allocated by `mcpwm_new_timer_sync_src()` or `mcpwm_new_gpio_sync_src()` or `mcpwm_new_soft_sync_src()` + * @return + * - ESP_OK: Delete MCPWM sync source successfully + * - ESP_ERR_INVALID_ARG: Delete MCPWM sync source failed because of invalid argument + * - ESP_FAIL: Delete MCPWM sync source failed because of other error + */ +esp_err_t mcpwm_del_sync_src(mcpwm_sync_handle_t sync); + +/** + * @brief Activate the software sync, trigger the sync event for once + * + * @param[in] sync MCPWM soft sync handle, allocated by `mcpwm_new_soft_sync_src()` + * @return + * - ESP_OK: Trigger MCPWM software sync event successfully + * - ESP_ERR_INVALID_ARG: Trigger MCPWM software sync event failed because of invalid argument + * - ESP_FAIL: Trigger MCPWM software sync event failed because of other error + */ +esp_err_t mcpwm_soft_sync_activate(mcpwm_sync_handle_t sync); + +#ifdef __cplusplus +} +#endif diff --git a/components/driver/include/driver/mcpwm_timer.h b/components/driver/include/driver/mcpwm_timer.h new file mode 100644 index 0000000000..3bcec03ab5 --- /dev/null +++ b/components/driver/include/driver/mcpwm_timer.h @@ -0,0 +1,144 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include "esp_err.h" +#include "driver/mcpwm_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Group of supported MCPWM timer event callbacks + * @note The callbacks are all running under ISR environment + */ +typedef struct { + mcpwm_timer_event_cb_t on_full; /*!< callback function when MCPWM timer counts to peak value */ + mcpwm_timer_event_cb_t on_empty; /*!< callback function when MCPWM timer counts to zero */ + mcpwm_timer_event_cb_t on_stop; /*!< callback function when MCPWM timer stops */ +} mcpwm_timer_event_callbacks_t; + +/** + * @brief MCPWM timer configuration + */ +typedef struct { + int group_id; /*!< Specify from which group to allocate the MCPWM timer */ + mcpwm_timer_clock_source_t clk_src; /*!< MCPWM timer clock source */ + uint32_t resolution_hz; /*!< Counter resolution in Hz, ranges from around 300KHz to 80MHz. + The step size of each count tick equals to (1 / resolution_hz) seconds */ + mcpwm_timer_count_mode_t count_mode; /*!< Count mode */ + uint32_t period_ticks; /*!< Number of count ticks within a period */ + struct { + uint32_t update_period_on_empty: 1; /*!< Whether to update period when timer counts to zero */ + uint32_t update_period_on_sync: 1; /*!< Whether to update period on sync event */ + } flags; /*!< Extra configuration flags for timer */ +} mcpwm_timer_config_t; + +/** + * @brief Create MCPWM timer + * + * @param[in] config MCPWM timer configuration + * @param[out] ret_timer Returned MCPWM timer handle + * @return + * - ESP_OK: Create MCPWM timer successfully + * - ESP_ERR_INVALID_ARG: Create MCPWM timer failed because of invalid argument + * - ESP_ERR_NO_MEM: Create MCPWM timer failed because out of memory + * - ESP_ERR_NOT_FOUND: Create MCPWM timer failed because all hardware timers are used up and no more free one + * - ESP_FAIL: Create MCPWM timer failed because of other error + */ +esp_err_t mcpwm_new_timer(const mcpwm_timer_config_t *config, mcpwm_timer_handle_t *ret_timer); + +/** + * @brief Delete MCPWM timer + * + * @param[in] timer MCPWM timer handle, allocated by `mcpwm_new_timer()` + * @return + * - ESP_OK: Delete MCPWM timer successfully + * - ESP_ERR_INVALID_ARG: Delete MCPWM timer failed because of invalid argument + * - ESP_ERR_INVALID_STATE: Delete MCPWM timer failed because timer is not in init state + * - ESP_FAIL: Delete MCPWM timer failed because of other error + */ +esp_err_t mcpwm_del_timer(mcpwm_timer_handle_t timer); + +/** + * @brief Enable MCPWM timer + * + * @param[in] timer MCPWM timer handle, allocated by `mcpwm_new_timer()` + * @return + * - ESP_OK: Enable MCPWM timer successfully + * - ESP_ERR_INVALID_ARG: Enable MCPWM timer failed because of invalid argument + * - ESP_ERR_INVALID_STATE: Enable MCPWM timer failed because timer is enabled already + * - ESP_FAIL: Enable MCPWM timer failed because of other error + */ +esp_err_t mcpwm_timer_enable(mcpwm_timer_handle_t timer); + +/** + * @brief Disable MCPWM timer + * + * @param[in] timer MCPWM timer handle, allocated by `mcpwm_new_timer()` + * @return + * - ESP_OK: Disable MCPWM timer successfully + * - ESP_ERR_INVALID_ARG: Disable MCPWM timer failed because of invalid argument + * - ESP_ERR_INVALID_STATE: Disable MCPWM timer failed because timer is disabled already + * - ESP_FAIL: Disable MCPWM timer failed because of other error + */ +esp_err_t mcpwm_timer_disable(mcpwm_timer_handle_t timer); + +/** + * @brief Send specific start/stop commands to MCPWM timer + * + * @param[in] timer MCPWM timer handle, allocated by `mcpwm_new_timer()` + * @param[in] command Supported command list for MCPWM timer + * @return + * - ESP_OK: Start or stop MCPWM timer successfully + * - ESP_ERR_INVALID_ARG: Start or stop MCPWM timer failed because of invalid argument + * - ESP_ERR_INVALID_STATE: Start or stop MCPWM timer failed because timer is not enabled + * - ESP_FAIL: Start or stop MCPWM timer failed because of other error + */ +esp_err_t mcpwm_timer_start_stop(mcpwm_timer_handle_t timer, mcpwm_timer_start_stop_cmd_t command); + +/** + * @brief Set event callbacks for MCPWM timer + * + * @param[in] timer MCPWM timer handle, allocated by `mcpwm_new_timer()` + * @param[in] cbs Group of callback functions + * @param[in] user_data User data, which will be passed to callback functions directly + * @return + * - ESP_OK: Set event callbacks successfully + * - ESP_ERR_INVALID_ARG: Set event callbacks failed because of invalid argument + * - ESP_ERR_INVALID_STATE: Set event callbacks failed because timer is not in init state + * - ESP_FAIL: Set event callbacks failed because of other error + */ +esp_err_t mcpwm_timer_register_event_callbacks(mcpwm_timer_handle_t timer, const mcpwm_timer_event_callbacks_t *cbs, void *user_data); + +/** + * @brief MCPWM Timer sync phase configuration + */ +typedef struct { + mcpwm_sync_handle_t sync_src; /*!< The sync event source. Set to NULL will disable the timer being synced by others */ + uint32_t count_value; /*!< The count value that should lock to upon sync event */ + mcpwm_timer_direction_t direction; /*!< The count direction that should lock to upon sync event */ +} mcpwm_timer_sync_phase_config_t; + +/** + * @brief Set sync phase for MCPWM timer + * + * @param[in] timer MCPWM timer handle, allocated by `mcpwm_new_timer()` + * @param[in] config MCPWM timer sync phase configuration + * @return + * - ESP_OK: Set sync phase for MCPWM timer successfully + * - ESP_ERR_INVALID_ARG: Set sync phase for MCPWM timer failed because of invalid argument + * - ESP_FAIL: Set sync phase for MCPWM timer failed because of other error + */ +esp_err_t mcpwm_timer_set_phase_on_sync(mcpwm_timer_handle_t timer, const mcpwm_timer_sync_phase_config_t *config); + +#ifdef __cplusplus +} +#endif diff --git a/components/driver/include/driver/mcpwm_types.h b/components/driver/include/driver/mcpwm_types.h new file mode 100644 index 0000000000..d25cff5614 --- /dev/null +++ b/components/driver/include/driver/mcpwm_types.h @@ -0,0 +1,145 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include "hal/mcpwm_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Type of MCPWM timer handle + */ +typedef struct mcpwm_timer_t *mcpwm_timer_handle_t; + +/** + * @brief Type of MCPWM operator handle + */ +typedef struct mcpwm_oper_t *mcpwm_oper_handle_t; + +/** + * @brief Type of MCPWM comparator handle + */ +typedef struct mcpwm_cmpr_t *mcpwm_cmpr_handle_t; + +/** + * @brief Type of MCPWM generator handle + */ +typedef struct mcpwm_gen_t *mcpwm_gen_handle_t; + +/** + * @brief Type of MCPWM fault handle + */ +typedef struct mcpwm_fault_t *mcpwm_fault_handle_t; + +/** + * @brief Type of MCPWM sync handle + */ +typedef struct mcpwm_sync_t *mcpwm_sync_handle_t; + +/** + * @brief Type of MCPWM capture timer handle + */ +typedef struct mcpwm_cap_timer_t *mcpwm_cap_timer_handle_t; + +/** + * @brief Type of MCPWM capture channel handle + */ +typedef struct mcpwm_cap_channel_t *mcpwm_cap_channel_handle_t; + +/** + * @brief MCPWM timer event data + */ +typedef struct { + uint32_t count_value; /*!< MCPWM timer count value */ + mcpwm_timer_direction_t direction; /*!< MCPWM timer count direction */ +} mcpwm_timer_event_data_t; + +/** + * @brief MCPWM timer event callback function + * + * @param[in] timer MCPWM timer handle + * @param[in] edata MCPWM timer event data, fed by driver + * @param[in] user_ctx User data, set in `mcpwm_timer_register_event_callbacks()` + * @return Whether a high priority task has been waken up by this function + */ +typedef bool (*mcpwm_timer_event_cb_t)(mcpwm_timer_handle_t timer, const mcpwm_timer_event_data_t *edata, void *user_ctx); + +/** + * @brief MCPWM brake event data + */ +typedef struct { +} mcpwm_brake_event_data_t; + +/** + * @brief MCPWM operator brake event callback function + * + * @param[in] operator MCPWM operator handle + * @param[in] edata MCPWM brake event data, fed by driver + * @param[in] user_ctx User data, set in `mcpwm_operator_register_event_callbacks()` + * @return Whether a high priority task has been waken up by this function + */ +typedef bool (*mcpwm_brake_event_cb_t)(mcpwm_oper_handle_t operator, const mcpwm_brake_event_data_t *edata, void *user_ctx); + +/** + * @brief MCPWM fault event data + */ +typedef struct { +} mcpwm_fault_event_data_t; + +/** + * @brief MCPWM fault event callback function + * + * @param fault MCPWM fault handle + * @param ev_data MCPWM fault event data, fed by driver + * @param user_ctx User data, set in `mcpwm_fault_register_event_callbacks()` + * @return whether a task switch is needed after the callback returns + */ +typedef bool (*mcpwm_fault_event_cb_t)(mcpwm_fault_handle_t fault, const mcpwm_fault_event_data_t *ev_data, void *user_ctx); + +/** + * @brief MCPWM compare event data + */ +typedef struct { + uint32_t compare_ticks; /*!< Compare value */ + mcpwm_timer_direction_t direction; /*!< Count direction */ +} mcpwm_compare_event_data_t; + +/** + * @brief MCPWM comparator event callback function + * + * @param comparator MCPWM comparator handle + * @param edata MCPWM comparator event data, fed by driver + * @param user_ctx User data, set in `mcpwm_comparator_register_event_callbacks()` + * @return Whether a high priority task has been waken up by this function + */ +typedef bool (*mcpwm_compare_event_cb_t)(mcpwm_cmpr_handle_t comparator, const mcpwm_compare_event_data_t *edata, void *user_ctx); + +/** + * @brief MCPWM capture event data + */ +typedef struct { + uint32_t cap_value; /*!< Captured value */ + mcpwm_capture_edge_t cap_edge; /*!< Capture edge */ +} mcpwm_capture_event_data_t; + +/** + * @brief MCPWM capture event callback function + * + * @param cap_channel MCPWM capture channel handle + * @param ev_data MCPWM capture event data, fed by driver + * @param user_ctx User data, set in `mcpwm_capture_channel_register_event_callbacks()` + * @return Whether a high priority task has been waken up by this function + */ +typedef bool (*mcpwm_capture_event_cb_t)(mcpwm_cap_channel_handle_t cap_channel, const mcpwm_capture_event_data_t *edata, void *user_ctx); + +#ifdef __cplusplus +} +#endif diff --git a/components/driver/include/esp_private/mcpwm.h b/components/driver/include/esp_private/mcpwm.h new file mode 100644 index 0000000000..62f44ce435 --- /dev/null +++ b/components/driver/include/esp_private/mcpwm.h @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// DO NOT USE THESE APIS IN YOUR APPLICATIONS +// The following APIs are for internal use, public to other IDF components, but not for users' applications. + +#pragma once + +#include +#include +#include "esp_err.h" +#include "driver/mcpwm_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Get MCPWM timer phase + * + * @param[in] timer MCPWM timer handle, allocated by `mcpwm_new_timer()` + * @param[out] count_value Returned MCPWM timer phase + * @param[out] direction Returned MCPWM timer counting direction + * @return + * - ESP_OK: Get MCPWM timer status successfully + * - ESP_ERR_INVALID_ARG: Get MCPWM timer status failed because of invalid argument + * - ESP_FAIL: Get MCPWM timer status failed because of other error + */ +esp_err_t mcpwm_timer_get_phase(mcpwm_timer_handle_t timer, uint32_t *count_value, mcpwm_timer_direction_t *direction); + +#ifdef __cplusplus +} +#endif diff --git a/components/driver/mcpwm/mcpwm_cap.c b/components/driver/mcpwm/mcpwm_cap.c new file mode 100644 index 0000000000..94667a6f14 --- /dev/null +++ b/components/driver/mcpwm/mcpwm_cap.c @@ -0,0 +1,420 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include "sdkconfig.h" +#if CONFIG_MCPWM_ENABLE_DEBUG_LOG +// The local log level must be defined before including esp_log.h +// Set the maximum log level for this source file +#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG +#endif +#include "freertos/FreeRTOS.h" +#include "esp_attr.h" +#include "esp_check.h" +#include "esp_private/esp_clk.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_memory_utils.h" +#include "soc/soc_caps.h" +#include "soc/mcpwm_periph.h" +#include "hal/mcpwm_ll.h" +#include "driver/mcpwm_cap.h" +#include "driver/gpio.h" +#include "mcpwm_private.h" + +static const char *TAG = "mcpwm"; + +static void mcpwm_capture_default_isr(void *args); + +static esp_err_t mcpwm_cap_timer_register_to_group(mcpwm_cap_timer_t *cap_timer, int group_id) +{ + mcpwm_group_t *group = mcpwm_acquire_group_handle(group_id); + ESP_RETURN_ON_FALSE(group, ESP_ERR_NO_MEM, TAG, "no mem for group (%d)", group_id); + + bool new_timer = false; + portENTER_CRITICAL(&group->spinlock); + if (!group->cap_timer) { + group->cap_timer = cap_timer; + new_timer = true; + } + portEXIT_CRITICAL(&group->spinlock); + + if (!new_timer) { + mcpwm_release_group_handle(group); + group = NULL; + } else { + cap_timer->group = group; + } + ESP_RETURN_ON_FALSE(new_timer, ESP_ERR_NOT_FOUND, TAG, "no free cap timer in group (%d)", group_id); + return ESP_OK; +} + +static void mcpwm_cap_timer_unregister_from_group(mcpwm_cap_timer_t *cap_timer) +{ + mcpwm_group_t *group = cap_timer->group; + + portENTER_CRITICAL(&group->spinlock); + group->cap_timer = NULL; + portEXIT_CRITICAL(&group->spinlock); + + // capture timer has a reference on group, release it now + mcpwm_release_group_handle(group); +} + +static esp_err_t mcpwm_cap_timer_destory(mcpwm_cap_timer_t *cap_timer) +{ + if (cap_timer->pm_lock) { + ESP_RETURN_ON_ERROR(esp_pm_lock_delete(cap_timer->pm_lock), TAG, "delete pm_lock failed"); + } + if (cap_timer->group) { + mcpwm_cap_timer_unregister_from_group(cap_timer); + } + free(cap_timer); + return ESP_OK; +} + +esp_err_t mcpwm_new_capture_timer(const mcpwm_capture_timer_config_t *config, mcpwm_cap_timer_handle_t *ret_cap_timer) +{ +#if CONFIG_MCPWM_ENABLE_DEBUG_LOG + esp_log_level_set(TAG, ESP_LOG_DEBUG); +#endif + esp_err_t ret = ESP_OK; + mcpwm_cap_timer_t *cap_timer = NULL; + ESP_GOTO_ON_FALSE(config && ret_cap_timer, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + ESP_GOTO_ON_FALSE(config->group_id < SOC_MCPWM_GROUPS && config->group_id >= 0, ESP_ERR_INVALID_ARG, + err, TAG, "invalid group ID:%d", config->group_id); + + cap_timer = heap_caps_calloc(1, sizeof(mcpwm_cap_timer_t), MCPWM_MEM_ALLOC_CAPS); + ESP_GOTO_ON_FALSE(cap_timer, ESP_ERR_NO_MEM, err, TAG, "no mem for capture timer"); + + switch (config->clk_src) { + case MCPWM_CAPTURE_CLK_SRC_APB: + cap_timer->resolution_hz = esp_clk_apb_freq(); +#if CONFIG_PM_ENABLE + ret = esp_pm_lock_create(ESP_PM_APB_FREQ_MAX, 0, "mcpwm_cap_timer", &cap_timer->pm_lock); + ESP_GOTO_ON_ERROR(ret, err, TAG, "create ESP_PM_APB_FREQ_MAX lock failed"); +#endif // CONFIG_PM_ENABLE + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_INVALID_ARG, err, TAG, "invalid clock source:%d", config->clk_src); + } + + ESP_GOTO_ON_ERROR(mcpwm_cap_timer_register_to_group(cap_timer, config->group_id), err, TAG, "register timer failed"); + mcpwm_group_t *group = cap_timer->group; + int group_id = group->group_id; + + // fill in other capture timer specific members + cap_timer->spinlock = (portMUX_TYPE)portMUX_INITIALIZER_UNLOCKED; + cap_timer->fsm = MCPWM_CAP_TIMER_FSM_INIT; + *ret_cap_timer = cap_timer; + ESP_LOGD(TAG, "new capture timer at %p, in group (%d)", cap_timer, group_id); + return ESP_OK; + +err: + if (cap_timer) { + mcpwm_cap_timer_destory(cap_timer); + } + return ret; +} + +esp_err_t mcpwm_del_capture_timer(mcpwm_cap_timer_handle_t cap_timer) +{ + ESP_RETURN_ON_FALSE(cap_timer, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + ESP_RETURN_ON_FALSE(cap_timer->fsm == MCPWM_CAP_TIMER_FSM_INIT, ESP_ERR_INVALID_STATE, TAG, "timer not in init state"); + for (int i = 0; i < SOC_MCPWM_CAPTURE_CHANNELS_PER_TIMER; i++) { + ESP_RETURN_ON_FALSE(!cap_timer->cap_channels[i], ESP_ERR_INVALID_STATE, TAG, "cap channel still in working"); + } + mcpwm_group_t *group = cap_timer->group; + + ESP_LOGD(TAG, "del capture timer in group %d", group->group_id); + // recycle memory resource + ESP_RETURN_ON_ERROR(mcpwm_cap_timer_destory(cap_timer), TAG, "destory capture timer failed"); + return ESP_OK; +} + +esp_err_t mcpwm_capture_timer_enable(mcpwm_cap_timer_handle_t cap_timer) +{ + ESP_RETURN_ON_FALSE(cap_timer, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + ESP_RETURN_ON_FALSE(cap_timer->fsm == MCPWM_CAP_TIMER_FSM_INIT, ESP_ERR_INVALID_STATE, TAG, "timer not in init state"); + if (cap_timer->pm_lock) { + ESP_RETURN_ON_ERROR(esp_pm_lock_acquire(cap_timer->pm_lock), TAG, "acquire pm_lock failed"); + } + cap_timer->fsm = MCPWM_CAP_TIMER_FSM_ENABLE; + return ESP_OK; +} + +esp_err_t mcpwm_capture_timer_disable(mcpwm_cap_timer_handle_t cap_timer) +{ + ESP_RETURN_ON_FALSE(cap_timer, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + ESP_RETURN_ON_FALSE(cap_timer->fsm == MCPWM_CAP_TIMER_FSM_ENABLE, ESP_ERR_INVALID_STATE, TAG, "timer not in enable state"); + if (cap_timer->pm_lock) { + ESP_RETURN_ON_ERROR(esp_pm_lock_release(cap_timer->pm_lock), TAG, "release pm_lock failed"); + } + cap_timer->fsm = MCPWM_CAP_TIMER_FSM_INIT; + return ESP_OK; +} + +esp_err_t mcpwm_capture_timer_start(mcpwm_cap_timer_handle_t cap_timer) +{ + ESP_RETURN_ON_FALSE(cap_timer, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + ESP_RETURN_ON_FALSE(cap_timer->fsm == MCPWM_CAP_TIMER_FSM_ENABLE, ESP_ERR_INVALID_STATE, TAG, "timer not enabled yet"); + mcpwm_group_t *group = cap_timer->group; + + portENTER_CRITICAL_SAFE(&cap_timer->spinlock); + mcpwm_ll_capture_enable_timer(group->hal.dev, true); + portEXIT_CRITICAL_SAFE(&cap_timer->spinlock); + + return ESP_OK; +} + +esp_err_t mcpwm_capture_timer_stop(mcpwm_cap_timer_handle_t cap_timer) +{ + ESP_RETURN_ON_FALSE(cap_timer, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + ESP_RETURN_ON_FALSE(cap_timer->fsm == MCPWM_CAP_TIMER_FSM_ENABLE, ESP_ERR_INVALID_STATE, TAG, "timer not enabled yet"); + mcpwm_group_t *group = cap_timer->group; + + portENTER_CRITICAL_SAFE(&cap_timer->spinlock); + mcpwm_ll_capture_enable_timer(group->hal.dev, false); + portEXIT_CRITICAL_SAFE(&cap_timer->spinlock); + + return ESP_OK; +} + +static esp_err_t mcpwm_capture_channel_register_to_timer(mcpwm_cap_channel_t *cap_channel, mcpwm_cap_timer_t *cap_timer) +{ + int cap_chan_id = -1; + portENTER_CRITICAL(&cap_timer->spinlock); + for (int i = 0; i < SOC_MCPWM_CAPTURE_CHANNELS_PER_TIMER; i++) { + if (!cap_timer->cap_channels[i]) { + cap_timer->cap_channels[i] = cap_channel; + cap_chan_id = i; + break; + } + } + portEXIT_CRITICAL(&cap_timer->spinlock); + ESP_RETURN_ON_FALSE(cap_chan_id >= 0, ESP_ERR_NOT_FOUND, TAG, "no free channel in the timer (%d)", cap_timer->group->group_id); + + cap_channel->cap_chan_id = cap_chan_id; + cap_channel->cap_timer = cap_timer; + return ESP_OK; +} + +static void mcpwm_capture_channel_unregister_from_timer(mcpwm_cap_channel_t *cap_chan) +{ + mcpwm_cap_timer_t *cap_timer = cap_chan->cap_timer; + int cap_chan_id = cap_chan->cap_chan_id; + + portENTER_CRITICAL(&cap_timer->spinlock); + cap_timer->cap_channels[cap_chan_id] = NULL; + portEXIT_CRITICAL(&cap_timer->spinlock); +} + +static esp_err_t mcpwm_capture_channel_destory(mcpwm_cap_channel_t *cap_chan) +{ + if (cap_chan->intr) { + ESP_RETURN_ON_ERROR(esp_intr_free(cap_chan->intr), TAG, "delete interrupt service failed"); + } + if (cap_chan->cap_timer) { + mcpwm_capture_channel_unregister_from_timer(cap_chan); + } + free(cap_chan); + return ESP_OK; +} + +esp_err_t mcpwm_new_capture_channel(mcpwm_cap_timer_handle_t cap_timer, const mcpwm_capture_channel_config_t *config, mcpwm_cap_channel_handle_t *ret_cap_channel) +{ + esp_err_t ret = ESP_OK; + mcpwm_cap_channel_t *cap_chan = NULL; + ESP_GOTO_ON_FALSE(cap_timer && config && ret_cap_channel, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + ESP_GOTO_ON_FALSE(config->prescale && config->prescale <= MCPWM_LL_MAX_CAPTURE_PRESCALE, ESP_ERR_INVALID_ARG, err, TAG, "invalid prescale"); + + // create instance firstly, then install onto platform + cap_chan = calloc(1, sizeof(mcpwm_cap_channel_t)); + ESP_GOTO_ON_FALSE(cap_chan, ESP_ERR_NO_MEM, err, TAG, "no mem for capture channel"); + + ESP_GOTO_ON_ERROR(mcpwm_capture_channel_register_to_timer(cap_chan, cap_timer), err, TAG, "register channel failed"); + mcpwm_group_t *group = cap_timer->group; + mcpwm_hal_context_t *hal = &group->hal; + int cap_chan_id = cap_chan->cap_chan_id; + + mcpwm_ll_capture_enable_channel(hal->dev, cap_chan_id, true); // enable channel + mcpwm_ll_capture_enable_negedge(hal->dev, cap_chan_id, config->flags.neg_edge); + mcpwm_ll_capture_enable_posedge(hal->dev, cap_chan_id, config->flags.pos_edge); + mcpwm_ll_invert_input(hal->dev, cap_chan_id, config->flags.invert_cap_signal); + mcpwm_ll_capture_set_prescale(hal->dev, cap_chan_id, config->prescale); + + if (config->gpio_num >= 0) { + // GPIO configuration + gpio_config_t gpio_conf = { + .intr_type = GPIO_INTR_DISABLE, + .mode = GPIO_MODE_INPUT | (config->flags.io_loop_back ? GPIO_MODE_OUTPUT : 0), // also enable the output path if `io_loop_back` is enabled + .pin_bit_mask = (1ULL << config->gpio_num), + .pull_down_en = config->flags.pull_down, + .pull_up_en = config->flags.pull_up, + }; + ESP_GOTO_ON_ERROR(gpio_config(&gpio_conf), err, TAG, "config capture GPIO failed"); + esp_rom_gpio_connect_in_signal(config->gpio_num, mcpwm_periph_signals.groups[group->group_id].captures[cap_chan_id].cap_sig, 0); + } + + cap_chan->gpio_num = config->gpio_num; + *ret_cap_channel = cap_chan; + ESP_LOGD(TAG, "new capture channel (%d,%d) at %p", group->group_id, cap_chan_id, cap_chan); + return ESP_OK; +err: + if (cap_chan) { + mcpwm_capture_channel_destory(cap_chan); + } + return ret; +} + +esp_err_t mcpwm_del_capture_channel(mcpwm_cap_channel_handle_t cap_channel) +{ + ESP_RETURN_ON_FALSE(cap_channel, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + mcpwm_cap_timer_t *cap_timer = cap_channel->cap_timer; + mcpwm_group_t *group = cap_timer->group; + mcpwm_hal_context_t *hal = &group->hal; + int cap_chan_id = cap_channel->cap_chan_id; + + ESP_LOGD(TAG, "del capture channel (%d,%d)", group->group_id, cap_channel->cap_chan_id); + if (cap_channel->gpio_num >= 0) { + gpio_reset_pin(cap_channel->gpio_num); + } + + portENTER_CRITICAL(&group->spinlock); + mcpwm_ll_intr_enable(hal->dev, MCPWM_LL_EVENT_CAPTURE(cap_chan_id), false); + mcpwm_ll_intr_clear_status(hal->dev, MCPWM_LL_EVENT_CAPTURE(cap_chan_id)); + portEXIT_CRITICAL(&group->spinlock); + + // disable capture channel + mcpwm_ll_capture_enable_channel(group->hal.dev, cap_channel->cap_chan_id, false); + + // recycle memory resource + ESP_RETURN_ON_ERROR(mcpwm_capture_channel_destory(cap_channel), TAG, "destory capture channel failed"); + return ESP_OK; +} + +esp_err_t mcpwm_capture_channel_register_event_callbacks(mcpwm_cap_channel_handle_t cap_channel, const mcpwm_capture_event_callbacks_t *cbs, void *user_data) +{ + ESP_RETURN_ON_FALSE(cap_channel && cbs, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + mcpwm_group_t *group = cap_channel->cap_timer->group; + mcpwm_hal_context_t *hal = &group->hal; + int group_id = group->group_id; + int cap_chan_id = cap_channel->cap_chan_id; + +#if CONFIG_MCWPM_ISR_IRAM_SAFE + if (cbs->on_cap) { + ESP_RETURN_ON_FALSE(esp_ptr_in_iram(cbs->on_cap), ESP_ERR_INVALID_ARG, TAG, "on_cap callback not in IRAM"); + } + if (user_data) { + ESP_RETURN_ON_FALSE(esp_ptr_internal(user_data), ESP_ERR_INVALID_ARG, TAG, "user context not in internal RAM"); + } +#endif + + // lazy install interrupt service + if (!cap_channel->intr) { + // we want the interrupt servie to be enabled after allocation successfully + int isr_flags = MCPWM_INTR_ALLOC_FLAG & ~ ESP_INTR_FLAG_INTRDISABLED; + ESP_RETURN_ON_ERROR(esp_intr_alloc_intrstatus(mcpwm_periph_signals.groups[group_id].irq_id, isr_flags, + (uint32_t)mcpwm_ll_intr_get_status_reg(hal->dev), MCPWM_LL_EVENT_CAPTURE(cap_chan_id), + mcpwm_capture_default_isr, cap_channel, &cap_channel->intr), TAG, "install interrupt service for cap channel failed"); + } + + portENTER_CRITICAL(&group->spinlock); + mcpwm_ll_intr_enable(hal->dev, MCPWM_LL_EVENT_CAPTURE(cap_chan_id), cbs->on_cap != NULL); + portEXIT_CRITICAL(&group->spinlock); + + cap_channel->on_cap = cbs->on_cap; + cap_channel->user_data = user_data; + + return ESP_OK; +} + +esp_err_t mcpwm_capture_channel_trigger_soft_catch(mcpwm_cap_channel_handle_t cap_channel) +{ + ESP_RETURN_ON_FALSE(cap_channel, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + mcpwm_cap_timer_t *cap_timer = cap_channel->cap_timer; + mcpwm_group_t *group = cap_timer->group; + + // note: soft capture can also triggers the interrupt routine + mcpwm_ll_trigger_soft_capture(group->hal.dev, cap_channel->cap_chan_id); + return ESP_OK; +} + +esp_err_t mcpwm_capture_timer_set_phase_on_sync(mcpwm_cap_timer_handle_t cap_timer, const mcpwm_capture_timer_sync_phase_config_t *config) +{ + ESP_RETURN_ON_FALSE(cap_timer, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + // capture timer only support count up + ESP_RETURN_ON_FALSE(config->direction == MCPWM_TIMER_DIRECTION_UP, ESP_ERR_INVALID_ARG, TAG, "invalid sync direction"); + mcpwm_group_t *group = cap_timer->group; + mcpwm_sync_t *sync_source = config->sync_src; + + // a non-NULL sync_src means to enable sync feature + if (sync_source) { + switch (sync_source->type) { + case MCPWM_SYNC_TYPE_GPIO: { + ESP_RETURN_ON_FALSE(group == sync_source->group, ESP_ERR_INVALID_ARG, TAG, "capture timer and sync source are not in the same group"); + mcpwm_gpio_sync_src_t *gpio_sync_src = __containerof(sync_source, mcpwm_gpio_sync_src_t, base); + mcpwm_ll_capture_set_gpio_sync(group->hal.dev, gpio_sync_src->sync_id); + ESP_LOGD(TAG, "enable sync to GPIO (%d,%d) for cap timer (%d)", + group->group_id, gpio_sync_src->sync_id, group->group_id); + break; + } + case MCPWM_SYNC_TYPE_TIMER: { + ESP_RETURN_ON_FALSE(group == sync_source->group, ESP_ERR_INVALID_ARG, TAG, "capture timer and sync source are not in the same group"); + mcpwm_timer_sync_src_t *timer_sync_src = __containerof(sync_source, mcpwm_timer_sync_src_t, base); + mcpwm_ll_capture_set_timer_sync(group->hal.dev, timer_sync_src->timer->timer_id); + ESP_LOGD(TAG, "enable sync to pwm timer (%d,%d) for cap timer (%d)", + group->group_id, timer_sync_src->timer->timer_id, group->group_id); + break; + } + case MCPWM_SYNC_TYPE_SOFT: { + mcpwm_soft_sync_src_t *soft_sync = __containerof(sync_source, mcpwm_soft_sync_src_t, base); + soft_sync->soft_sync_from = MCPWM_SOFT_SYNC_FROM_CAP; + soft_sync->cap_timer = cap_timer; + soft_sync->base.group = group; + break; + } + } + mcpwm_ll_capture_enable_timer_sync(group->hal.dev, true); + mcpwm_ll_capture_set_sync_phase_value(group->hal.dev, config->count_value); + } else { // disable sync feature + mcpwm_ll_capture_enable_timer_sync(group->hal.dev, false); + ESP_LOGD(TAG, "disable sync for cap timer (%d)", group->group_id); + } + return ESP_OK; +} + +IRAM_ATTR static void mcpwm_capture_default_isr(void *args) +{ + mcpwm_cap_channel_t *cap_chan = (mcpwm_cap_channel_t *)args; + mcpwm_group_t *group = cap_chan->cap_timer->group; + mcpwm_hal_context_t *hal = &group->hal; + int cap_id = cap_chan->cap_chan_id; + bool need_yield = false; + + uint32_t status = mcpwm_ll_intr_get_status(hal->dev); + mcpwm_ll_intr_clear_status(hal->dev, status & MCPWM_LL_EVENT_CAPTURE(cap_id)); + + // read capture value and pass to user + mcpwm_capture_event_data_t data = { + .cap_value = mcpwm_ll_capture_get_value(hal->dev, cap_id), + .cap_edge = mcpwm_ll_capture_get_edge(hal->dev, cap_id), + }; + if (status & MCPWM_LL_EVENT_CAPTURE(cap_id)) { + mcpwm_capture_event_cb_t cb = cap_chan->on_cap; + if (cb) { + if (cb(cap_chan, &data, cap_chan->user_data)) { + need_yield = true; + } + } + } + + if (need_yield) { + portYIELD_FROM_ISR(); + } +} diff --git a/components/driver/mcpwm/mcpwm_cmpr.c b/components/driver/mcpwm/mcpwm_cmpr.c new file mode 100644 index 0000000000..c0f9e82aad --- /dev/null +++ b/components/driver/mcpwm/mcpwm_cmpr.c @@ -0,0 +1,210 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include "sdkconfig.h" +#if CONFIG_MCPWM_ENABLE_DEBUG_LOG +// The local log level must be defined before including esp_log.h +// Set the maximum log level for this source file +#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG +#endif +#include "freertos/FreeRTOS.h" +#include "esp_attr.h" +#include "esp_check.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_memory_utils.h" +#include "soc/soc_caps.h" +#include "soc/mcpwm_periph.h" +#include "hal/mcpwm_ll.h" +#include "driver/mcpwm_cmpr.h" +#include "mcpwm_private.h" + +static const char *TAG = "mcpwm"; + +static void mcpwm_comparator_default_isr(void *args); + +static esp_err_t mcpwm_comparator_register_to_operator(mcpwm_cmpr_t *cmpr, mcpwm_oper_t *oper) +{ + int cmpr_id = -1; + portENTER_CRITICAL(&oper->spinlock); + for (int i = 0; i < SOC_MCPWM_COMPARATORS_PER_OPERATOR; i++) { + if (!oper->comparators[i]) { + oper->comparators[i] = cmpr; + cmpr_id = i; + break; + } + } + portEXIT_CRITICAL(&oper->spinlock); + ESP_RETURN_ON_FALSE(cmpr_id >= 0, ESP_ERR_NOT_FOUND, TAG, "no free comparator in operator (%d,%d)", oper->group->group_id, oper->oper_id); + + cmpr->cmpr_id = cmpr_id; + cmpr->operator = oper; + return ESP_OK; +} + +static void mcpwm_comparator_unregister_from_operator(mcpwm_cmpr_t *cmpr) +{ + mcpwm_oper_t *oper = cmpr->operator; + int cmpr_id = cmpr->cmpr_id; + + portENTER_CRITICAL(&oper->spinlock); + oper->comparators[cmpr_id] = NULL; + portEXIT_CRITICAL(&oper->spinlock); +} + +static esp_err_t mcpwm_comparator_destory(mcpwm_cmpr_t *cmpr) +{ + if (cmpr->intr) { + ESP_RETURN_ON_ERROR(esp_intr_free(cmpr->intr), TAG, "uninstall interrupt service failed"); + } + if (cmpr->operator) { + mcpwm_comparator_unregister_from_operator(cmpr); + } + free(cmpr); + return ESP_OK; +} + +esp_err_t mcpwm_new_comparator(mcpwm_oper_handle_t oper, const mcpwm_comparator_config_t *config, mcpwm_cmpr_handle_t *ret_cmpr) +{ + esp_err_t ret = ESP_OK; + mcpwm_cmpr_t *cmpr = NULL; + ESP_GOTO_ON_FALSE(oper && config && ret_cmpr, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + + cmpr = heap_caps_calloc(1, sizeof(mcpwm_cmpr_t), MCPWM_MEM_ALLOC_CAPS); + ESP_GOTO_ON_FALSE(cmpr, ESP_ERR_NO_MEM, err, TAG, "no mem for comparator"); + + ESP_GOTO_ON_ERROR(mcpwm_comparator_register_to_operator(cmpr, oper), err, TAG, "register comparator failed"); + mcpwm_group_t *group = oper->group; + mcpwm_hal_context_t *hal = &group->hal; + int oper_id = oper->oper_id; + int cmpr_id = cmpr->cmpr_id; + + mcpwm_ll_operator_enable_update_compare_on_tez(hal->dev, oper_id, cmpr_id, config->flags.update_cmp_on_tez); + mcpwm_ll_operator_enable_update_compare_on_tep(hal->dev, oper_id, cmpr_id, config->flags.update_cmp_on_tep); + mcpwm_ll_operator_enable_update_compare_on_sync(hal->dev, oper_id, cmpr_id, config->flags.update_cmp_on_sync); + + // fill in other comparator members + cmpr->spinlock = (portMUX_TYPE)portMUX_INITIALIZER_UNLOCKED; + *ret_cmpr = cmpr; + ESP_LOGD(TAG, "new comparator (%d,%d,%d) at %p", group->group_id, oper_id, cmpr_id, cmpr); + return ESP_OK; + +err: + if (cmpr) { + mcpwm_comparator_destory(cmpr); + } + return ret; +} + +esp_err_t mcpwm_del_comparator(mcpwm_cmpr_handle_t cmpr) +{ + ESP_RETURN_ON_FALSE(cmpr, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + mcpwm_oper_t *operator= cmpr->operator; + mcpwm_group_t *group = operator->group; + mcpwm_hal_context_t *hal = &group->hal; + int oper_id = operator->oper_id; + int cmpr_id = cmpr->cmpr_id; + + portENTER_CRITICAL(&group->spinlock); + mcpwm_ll_intr_enable(hal->dev, MCPWM_LL_EVENT_CMP_EQUAL(oper_id, cmpr_id), false); + mcpwm_ll_intr_clear_status(hal->dev, MCPWM_LL_EVENT_CMP_EQUAL(oper_id, cmpr_id)); + portEXIT_CRITICAL(&group->spinlock); + + ESP_LOGD(TAG, "del comparator (%d,%d,%d)", group->group_id, oper_id, cmpr_id); + // recycle memory resource + ESP_RETURN_ON_ERROR(mcpwm_comparator_destory(cmpr), TAG, "destory comparator failed"); + return ESP_OK; +} + +esp_err_t mcpwm_comparator_set_compare_value(mcpwm_cmpr_handle_t cmpr, uint32_t cmp_ticks) +{ + ESP_RETURN_ON_FALSE(cmpr, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + mcpwm_oper_t *oper = cmpr->operator; + mcpwm_group_t *group = oper->group; + mcpwm_timer_t *timer = oper->timer; + ESP_RETURN_ON_FALSE(timer, ESP_ERR_INVALID_STATE, TAG, "timer and operator are not connected"); + ESP_RETURN_ON_FALSE(cmp_ticks < timer->peak_ticks, ESP_ERR_INVALID_ARG, TAG, "compare value out of range"); + + portENTER_CRITICAL_SAFE(&cmpr->spinlock); + mcpwm_ll_operator_set_compare_value(group->hal.dev, oper->oper_id, cmpr->cmpr_id, cmp_ticks); + portEXIT_CRITICAL_SAFE(&cmpr->spinlock); + + cmpr->compare_ticks = cmp_ticks; + return ESP_OK; +} + +esp_err_t mcpwm_comparator_register_event_callbacks(mcpwm_cmpr_handle_t cmpr, const mcpwm_comparator_event_callbacks_t *cbs, void *user_data) +{ + ESP_RETURN_ON_FALSE(cmpr && cbs, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + mcpwm_oper_t *oper = cmpr->operator; + mcpwm_group_t *group = oper->group; + mcpwm_hal_context_t *hal = &group->hal; + int group_id = group->group_id; + int oper_id = oper->oper_id; + int cmpr_id = cmpr->cmpr_id; + +#if CONFIG_MCWPM_ISR_IRAM_SAFE + if (cbs->on_reach) { + ESP_RETURN_ON_FALSE(esp_ptr_in_iram(cbs->on_reach), ESP_ERR_INVALID_ARG, TAG, "on_reach callback not in IRAM"); + } + if (user_data) { + ESP_RETURN_ON_FALSE(esp_ptr_internal(user_data), ESP_ERR_INVALID_ARG, TAG, "user context not in internal RAM"); + } +#endif + + // lazy install interrupt service + if (!cmpr->intr) { + // we want the interrupt servie to be enabled after allocation successfully + int isr_flags = MCPWM_INTR_ALLOC_FLAG & ~ ESP_INTR_FLAG_INTRDISABLED; + ESP_RETURN_ON_ERROR(esp_intr_alloc_intrstatus(mcpwm_periph_signals.groups[group_id].irq_id, isr_flags, + (uint32_t)mcpwm_ll_intr_get_status_reg(hal->dev), MCPWM_LL_EVENT_CMP_EQUAL(oper_id, cmpr_id), + mcpwm_comparator_default_isr, cmpr, &cmpr->intr), TAG, "install interrupt service for comparator failed"); + } + + portENTER_CRITICAL(&group->spinlock); + mcpwm_ll_intr_enable(hal->dev, MCPWM_LL_EVENT_CMP_EQUAL(oper_id, cmpr_id), cbs->on_reach != NULL); + portEXIT_CRITICAL(&group->spinlock); + + cmpr->on_reach = cbs->on_reach; + cmpr->user_data = user_data; + + return ESP_OK; +} + +static void IRAM_ATTR mcpwm_comparator_default_isr(void *args) +{ + mcpwm_cmpr_t *cmpr = (mcpwm_cmpr_t *)args; + mcpwm_oper_t *oper = cmpr->operator; + mcpwm_group_t *group = oper->group; + mcpwm_hal_context_t *hal = &group->hal; + int oper_id = oper->oper_id; + int cmpr_id = cmpr->cmpr_id; + bool need_yield = false; + + uint32_t status = mcpwm_ll_intr_get_status(hal->dev); + mcpwm_ll_intr_clear_status(hal->dev, status & MCPWM_LL_EVENT_CMP_EQUAL(oper_id, cmpr_id)); + + mcpwm_compare_event_data_t edata = { + .compare_ticks = cmpr->compare_ticks, + // .direction = TODO + }; + + if (status & MCPWM_LL_EVENT_CMP_EQUAL(oper_id, cmpr_id)) { + mcpwm_compare_event_cb_t cb = cmpr->on_reach; + if (cb) { + if (cb(cmpr, &edata, cmpr->user_data)) { + need_yield = true; + } + } + } + + if (need_yield) { + portYIELD_FROM_ISR(); + } +} diff --git a/components/driver/mcpwm/mcpwm_com.c b/components/driver/mcpwm/mcpwm_com.c new file mode 100644 index 0000000000..7d9e555e32 --- /dev/null +++ b/components/driver/mcpwm/mcpwm_com.c @@ -0,0 +1,135 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "sdkconfig.h" +#if CONFIG_MCPWM_ENABLE_DEBUG_LOG +// The local log level must be defined before including esp_log.h +// Set the maximum log level for this source file +#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG +#endif +#include "esp_log.h" +#include "esp_check.h" +#include "esp_private/periph_ctrl.h" +#include "soc/mcpwm_periph.h" +#include "hal/mcpwm_ll.h" +#include "mcpwm_private.h" + +static const char *TAG = "mcpwm"; + +typedef struct { + _lock_t mutex; // platform level mutex lock + mcpwm_group_t *groups[SOC_MCPWM_GROUPS]; // array of MCPWM group instances + int group_ref_counts[SOC_MCPWM_GROUPS]; // reference count used to protect group install/uninstall +} mcpwm_platform_t; + +static mcpwm_platform_t s_platform; // singleton platform + +mcpwm_group_t *mcpwm_acquire_group_handle(int group_id) +{ + bool new_group = false; + mcpwm_group_t *group = NULL; + + // prevent install mcpwm group concurrently + _lock_acquire(&s_platform.mutex); + if (!s_platform.groups[group_id]) { + group = heap_caps_calloc(1, sizeof(mcpwm_group_t), MCPWM_MEM_ALLOC_CAPS); + if (group) { + new_group = true; + s_platform.groups[group_id] = group; + group->group_id = group_id; + group->spinlock = (portMUX_TYPE)portMUX_INITIALIZER_UNLOCKED; + // enable APB to access MCPWM registers + periph_module_enable(mcpwm_periph_signals.groups[group_id].module); + periph_module_reset(mcpwm_periph_signals.groups[group_id].module); + // initialize HAL context + mcpwm_hal_init_config_t hal_config = { + .group_id = group_id + }; + mcpwm_hal_context_t *hal = &group->hal; + mcpwm_hal_init(hal, &hal_config); + // disable all interrupts and clear pending status + mcpwm_ll_intr_enable(hal->dev, UINT32_MAX, false); + mcpwm_ll_intr_clear_status(hal->dev, UINT32_MAX); + } + } else { // group already install + group = s_platform.groups[group_id]; + } + if (group) { + // someone acquired the group handle means we have a new object that refer to this group + s_platform.group_ref_counts[group_id]++; + } + _lock_release(&s_platform.mutex); + + if (new_group) { + ESP_LOGD(TAG, "new group(%d) at %p", group_id, group); + } + return group; +} + +void mcpwm_release_group_handle(mcpwm_group_t *group) +{ + int group_id = group->group_id; + bool do_deinitialize = false; + + _lock_acquire(&s_platform.mutex); + s_platform.group_ref_counts[group_id]--; + if (s_platform.group_ref_counts[group_id] == 0) { + do_deinitialize = true; + s_platform.groups[group_id] = NULL; // deregister from platfrom + // hal layer deinitialize + mcpwm_hal_deinit(&group->hal); + periph_module_disable(mcpwm_periph_signals.groups[group_id].module); + free(group); + } + _lock_release(&s_platform.mutex); + + if (do_deinitialize) { + ESP_LOGD(TAG, "del group(%d)", group_id); + } +} + +esp_err_t mcpwm_select_periph_clock(mcpwm_group_t *group, mcpwm_timer_clock_source_t clk_src) +{ + esp_err_t ret = ESP_OK; + uint32_t periph_src_clk_hz = 0; + bool clock_selection_conflict = false; + bool do_clock_init = false; + // check if we need to update the group clock source, group clock source is shared by all mcpwm objects + portENTER_CRITICAL(&group->spinlock); + if (group->clk_src == 0) { + group->clk_src = clk_src; + do_clock_init = true; + } else { + clock_selection_conflict = (group->clk_src != clk_src); + } + portEXIT_CRITICAL(&group->spinlock); + ESP_RETURN_ON_FALSE(!clock_selection_conflict, ESP_ERR_INVALID_STATE, TAG, + "group clock conflict, already is %d but attempt to %d", group->clk_src, clk_src); + + if (do_clock_init) { + // [clk_tree] ToDo: replace the following switch-case table by clock_tree APIs + switch (clk_src) { + case MCPWM_TIMER_CLK_SRC_DEFAULT: + periph_src_clk_hz = 160000000; +#if CONFIG_PM_ENABLE + sprintf(group->pm_lock_name, "mcpwm_%d", group->group_id); // e.g. mcpwm_0 + ret = esp_pm_lock_create(ESP_PM_APB_FREQ_MAX, 0, group->pm_lock_name, &group->pm_lock); + ESP_RETURN_ON_ERROR(ret, TAG, "create ESP_PM_APB_FREQ_MAX lock failed"); + ESP_LOGD(TAG, "install ESP_PM_APB_FREQ_MAX lock for MCPWM group(%d)", group->group_id); +#endif // CONFIG_PM_ENABLE + break; + default: + ESP_RETURN_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, TAG, "clock source %d is not supported", clk_src); + break; + } + mcpwm_ll_group_set_clock_prescale(group->hal.dev, MCPWM_PERIPH_CLOCK_PRE_SCALE); + group->resolution_hz = periph_src_clk_hz / MCPWM_PERIPH_CLOCK_PRE_SCALE; + ESP_LOGD(TAG, "group (%d) clock resolution:%uHz", group->group_id, group->resolution_hz); + } + return ret; +} diff --git a/components/driver/mcpwm/mcpwm_fault.c b/components/driver/mcpwm/mcpwm_fault.c new file mode 100644 index 0000000000..44936d7215 --- /dev/null +++ b/components/driver/mcpwm/mcpwm_fault.c @@ -0,0 +1,302 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include "sdkconfig.h" +#if CONFIG_MCPWM_ENABLE_DEBUG_LOG +// The local log level must be defined before including esp_log.h +// Set the maximum log level for this source file +#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG +#endif +#include "freertos/FreeRTOS.h" +#include "esp_attr.h" +#include "esp_check.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_memory_utils.h" +#include "soc/soc_caps.h" +#include "soc/mcpwm_periph.h" +#include "hal/mcpwm_ll.h" +#include "driver/mcpwm_fault.h" +#include "driver/gpio.h" +#include "mcpwm_private.h" + +static const char *TAG = "mcpwm"; + +static void mcpwm_gpio_fault_default_isr(void *args); +static esp_err_t mcpwm_del_gpio_fault(mcpwm_fault_handle_t fault); +static esp_err_t mcpwm_del_soft_fault(mcpwm_fault_handle_t fault); + +static esp_err_t mcpwm_gpio_fault_register_to_group(mcpwm_gpio_fault_t *fault, int group_id) +{ + mcpwm_group_t *group = mcpwm_acquire_group_handle(group_id); + ESP_RETURN_ON_FALSE(group, ESP_ERR_NO_MEM, TAG, "no mem for group (%d)", group_id); + + int fault_id = -1; + portENTER_CRITICAL(&group->spinlock); + for (int i = 0; i < SOC_MCPWM_GPIO_FAULTS_PER_GROUP; i++) { + if (!group->gpio_faults[i]) { + fault_id = i; + group->gpio_faults[i] = fault; + break; + } + } + portEXIT_CRITICAL(&group->spinlock); + if (fault_id < 0) { + mcpwm_release_group_handle(group); + group = NULL; + } else { + fault->base.group = group; + fault->fault_id = fault_id; + } + ESP_RETURN_ON_FALSE(fault_id >= 0, ESP_ERR_NOT_FOUND, TAG, "no free gpio fault in group (%d)", group_id); + return ESP_OK; +} + +static void mcpwm_gpio_fault_unregister_from_group(mcpwm_gpio_fault_t *fault) +{ + mcpwm_group_t *group = fault->base.group; + int fault_id = fault->fault_id; + + portENTER_CRITICAL(&group->spinlock); + group->gpio_faults[fault_id] = NULL; + portEXIT_CRITICAL(&group->spinlock); + + // fault has a reference on group, release it now + mcpwm_release_group_handle(group); +} + +static esp_err_t mcpwm_gpio_fault_destory(mcpwm_gpio_fault_t *fault) +{ + if (fault->intr) { + ESP_RETURN_ON_ERROR(esp_intr_free(fault->intr), TAG, "uninstall interrupt service failed"); + } + if (fault->base.group) { + mcpwm_gpio_fault_unregister_from_group(fault); + } + free(fault); + return ESP_OK; +} + +esp_err_t mcpwm_new_gpio_fault(const mcpwm_gpio_fault_config_t *config, mcpwm_fault_handle_t *ret_fault) +{ +#if CONFIG_MCPWM_ENABLE_DEBUG_LOG + esp_log_level_set(TAG, ESP_LOG_DEBUG); +#endif + esp_err_t ret = ESP_OK; + mcpwm_gpio_fault_t *fault = NULL; + ESP_GOTO_ON_FALSE(config && ret_fault, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + ESP_GOTO_ON_FALSE(config->group_id < SOC_MCPWM_GROUPS && config->group_id >= 0, ESP_ERR_INVALID_ARG, + err, TAG, "invalid group ID:%d", config->group_id); + + fault = heap_caps_calloc(1, sizeof(mcpwm_gpio_fault_t), MCPWM_MEM_ALLOC_CAPS); + ESP_GOTO_ON_FALSE(fault, ESP_ERR_NO_MEM, err, TAG, "no mem for gpio fault"); + + ESP_GOTO_ON_ERROR(mcpwm_gpio_fault_register_to_group(fault, config->group_id), err, TAG, "register gpio fault failed"); + mcpwm_group_t *group = fault->base.group; + int group_id = group->group_id; + mcpwm_hal_context_t *hal = &group->hal; + int fault_id = fault->fault_id; + + // GPIO configuration + gpio_config_t gpio_conf = { + .intr_type = GPIO_INTR_DISABLE, + .mode = GPIO_MODE_INPUT | (config->flags.io_loop_back ? GPIO_MODE_OUTPUT : 0), // also enable the output path if `io_loop_back` is enabled + .pin_bit_mask = (1ULL << config->gpio_num), + .pull_down_en = config->flags.pull_down, + .pull_up_en = config->flags.pull_up, + }; + ESP_GOTO_ON_ERROR(gpio_config(&gpio_conf), err, TAG, "config fault GPIO failed"); + esp_rom_gpio_connect_in_signal(config->gpio_num, mcpwm_periph_signals.groups[group_id].gpio_faults[fault_id].fault_sig, 0); + + // set fault detection polarity + // different gpio faults share the same config register, using a group level spin lock + portENTER_CRITICAL(&group->spinlock); + mcpwm_ll_fault_set_active_level(hal->dev, fault_id, config->flags.active_level); + portEXIT_CRITICAL(&group->spinlock); + + // enable fault detection + mcpwm_ll_fault_enable_detection(hal->dev, fault_id, true); + + // fill in other operator members + fault->base.type = MCPWM_FAULT_TYPE_GPIO; + fault->gpio_num = config->gpio_num; + fault->base.del = mcpwm_del_gpio_fault; + *ret_fault = &fault->base; + ESP_LOGD(TAG, "new gpio fault (%d,%d) at %p, GPIO: %d", group_id, fault_id, fault, config->gpio_num); + return ESP_OK; + +err: + if (fault) { + mcpwm_gpio_fault_destory(fault); + } + return ret; +} + +static esp_err_t mcpwm_del_gpio_fault(mcpwm_fault_handle_t fault) +{ + mcpwm_gpio_fault_t *gpio_fault = __containerof(fault, mcpwm_gpio_fault_t, base); + mcpwm_group_t *group = fault->group; + mcpwm_hal_context_t *hal = &group->hal; + int fault_id = gpio_fault->fault_id; + + ESP_LOGD(TAG, "del GPIO fault (%d,%d)", group->group_id, fault_id); + gpio_reset_pin(gpio_fault->gpio_num); + + portENTER_CRITICAL(&group->spinlock); + mcpwm_ll_intr_enable(hal->dev, MCPWM_LL_EVENT_FAULT_MASK(fault_id), false); + mcpwm_ll_intr_clear_status(hal->dev, MCPWM_LL_EVENT_FAULT_MASK(fault_id)); + portEXIT_CRITICAL(&group->spinlock); + + // disable fault detection + mcpwm_ll_fault_enable_detection(hal->dev, fault_id, false); + + // recycle memory resource + ESP_RETURN_ON_ERROR(mcpwm_gpio_fault_destory(gpio_fault), TAG, "destory GPIO fault failed"); + return ESP_OK; +} + +esp_err_t mcpwm_new_soft_fault(const mcpwm_soft_fault_config_t *config, mcpwm_fault_handle_t *ret_fault) +{ + esp_err_t ret = ESP_OK; + mcpwm_soft_fault_t *soft_fault = NULL; + ESP_GOTO_ON_FALSE(config && ret_fault, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + soft_fault = heap_caps_calloc(1, sizeof(mcpwm_soft_fault_t), MCPWM_MEM_ALLOC_CAPS); + ESP_GOTO_ON_FALSE(soft_fault, ESP_ERR_NO_MEM, err, TAG, "no mem for soft fault"); + + // fill in other fault members + soft_fault->base.type = MCPWM_FAULT_TYPE_SOFT; + soft_fault->base.del = mcpwm_del_soft_fault; + *ret_fault = &soft_fault->base; + ESP_LOGD(TAG, "new soft fault at %p", soft_fault); + return ESP_OK; + +err: + if (soft_fault) { + free(soft_fault); + } + return ret; +} + +static esp_err_t mcpwm_del_soft_fault(mcpwm_fault_handle_t fault) +{ + mcpwm_soft_fault_t *soft_fault = __containerof(fault, mcpwm_soft_fault_t, base); + ESP_LOGD(TAG, "del soft fault %p", soft_fault); + free(soft_fault); + return ESP_OK; +} + +esp_err_t mcpwm_soft_fault_activate(mcpwm_fault_handle_t fault) +{ + ESP_RETURN_ON_FALSE(fault, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + ESP_RETURN_ON_FALSE(fault->type == MCPWM_FAULT_TYPE_SOFT, ESP_ERR_INVALID_ARG, TAG, "not a valid soft fault"); + mcpwm_group_t *group = fault->group; + mcpwm_soft_fault_t *soft_fault = __containerof(fault, mcpwm_soft_fault_t, base); + mcpwm_oper_t *operator = soft_fault->operator; + ESP_RETURN_ON_FALSE(operator, ESP_ERR_INVALID_STATE, TAG, "no operator is assigned to the fault"); + + switch (operator->brake_mode_on_soft_fault) { + case MCPWM_OPER_BRAKE_MODE_CBC: + mcpwm_ll_brake_trigger_soft_cbc(group->hal.dev, operator->oper_id); + break; + case MCPWM_OPER_BRAKE_MODE_OST: + mcpwm_ll_brake_trigger_soft_ost(group->hal.dev, operator->oper_id); + break; + default: + ESP_RETURN_ON_FALSE(false, ESP_ERR_INVALID_STATE, TAG, "unknown brake mode:%d", operator->brake_mode_on_soft_fault); + break; + } + return ESP_OK; +} + +esp_err_t mcpwm_del_fault(mcpwm_fault_handle_t fault) +{ + ESP_RETURN_ON_FALSE(fault, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + return fault->del(fault); +} + +esp_err_t mcpwm_fault_register_event_callbacks(mcpwm_fault_handle_t fault, const mcpwm_fault_event_callbacks_t *cbs, void *user_data) +{ + ESP_RETURN_ON_FALSE(fault && cbs, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + ESP_RETURN_ON_FALSE(fault->type == MCPWM_FAULT_TYPE_GPIO, ESP_ERR_INVALID_ARG, TAG, "only gpio fault can register event callback"); + mcpwm_gpio_fault_t *gpio_fault = __containerof(fault, mcpwm_gpio_fault_t, base); + mcpwm_group_t *group = fault->group; + int group_id = group->group_id; + mcpwm_hal_context_t *hal = &group->hal; + int fault_id = gpio_fault->fault_id; + +#if CONFIG_MCWPM_ISR_IRAM_SAFE + if (cbs->on_fault_enter) { + ESP_RETURN_ON_FALSE(esp_ptr_in_iram(cbs->on_fault_enter), ESP_ERR_INVALID_ARG, TAG, "on_fault_enter callback not in IRAM"); + } + if (cbs->on_fault_exit) { + ESP_RETURN_ON_FALSE(esp_ptr_in_iram(cbs->on_fault_exit), ESP_ERR_INVALID_ARG, TAG, "on_fault_exit callback not in IRAM"); + } + if (user_data) { + ESP_RETURN_ON_FALSE(esp_ptr_internal(user_data), ESP_ERR_INVALID_ARG, TAG, "user context not in internal RAM"); + } +#endif + + // lazy install interrupt service + if (!gpio_fault->intr) { + // we want the interrupt servie to be enabled after allocation successfully + int isr_flags = MCPWM_INTR_ALLOC_FLAG & ~ESP_INTR_FLAG_INTRDISABLED; + ESP_RETURN_ON_ERROR(esp_intr_alloc_intrstatus(mcpwm_periph_signals.groups[group_id].irq_id, isr_flags, + (uint32_t)mcpwm_ll_intr_get_status_reg(hal->dev), MCPWM_LL_EVENT_FAULT_MASK(fault_id), + mcpwm_gpio_fault_default_isr, gpio_fault, &gpio_fault->intr), TAG, "install interrupt service for gpio fault failed"); + } + + // different mcpwm events share the same interrupt control register + portENTER_CRITICAL(&group->spinlock); + mcpwm_ll_intr_enable(hal->dev, MCPWM_LL_EVENT_FAULT_ENTER(fault_id), cbs->on_fault_enter != NULL); + mcpwm_ll_intr_enable(hal->dev, MCPWM_LL_EVENT_FAULT_EXIT(fault_id), cbs->on_fault_exit != NULL); + portEXIT_CRITICAL(&group->spinlock); + + gpio_fault->on_fault_enter = cbs->on_fault_enter; + gpio_fault->on_fault_exit = cbs->on_fault_exit; + gpio_fault->user_data = user_data; + return ESP_OK; +} + +static void IRAM_ATTR mcpwm_gpio_fault_default_isr(void *args) +{ + mcpwm_gpio_fault_t *fault = (mcpwm_gpio_fault_t *)args; + mcpwm_group_t *group = fault->base.group; + mcpwm_hal_context_t *hal = &group->hal; + int fault_id = fault->fault_id; + bool need_yield = false; + + uint32_t status = mcpwm_ll_intr_get_status(hal->dev); + mcpwm_ll_intr_clear_status(hal->dev, status & MCPWM_LL_EVENT_FAULT_MASK(fault_id)); + + mcpwm_fault_event_data_t edata = { + // TODO + }; + + if (status & MCPWM_LL_EVENT_FAULT_ENTER(fault_id)) { + mcpwm_fault_event_cb_t cb = fault->on_fault_enter; + if (cb) { + if (cb(&fault->base, &edata, fault->user_data)) { + need_yield = true; + } + } + } + + if (status & MCPWM_LL_EVENT_FAULT_EXIT(fault_id)) { + mcpwm_fault_event_cb_t cb = fault->on_fault_exit; + if (cb) { + if (cb(&fault->base, &edata, fault->user_data)) { + need_yield = true; + } + } + } + + if (need_yield) { + portYIELD_FROM_ISR(); + } +} diff --git a/components/driver/mcpwm/mcpwm_gen.c b/components/driver/mcpwm/mcpwm_gen.c new file mode 100644 index 0000000000..41ec7c2f62 --- /dev/null +++ b/components/driver/mcpwm/mcpwm_gen.c @@ -0,0 +1,264 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include "sdkconfig.h" +#if CONFIG_MCPWM_ENABLE_DEBUG_LOG +// The local log level must be defined before including esp_log.h +// Set the maximum log level for this source file +#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG +#endif +#include "freertos/FreeRTOS.h" +#include "esp_attr.h" +#include "esp_check.h" +#include "esp_err.h" +#include "esp_log.h" +#include "soc/soc_caps.h" +#include "soc/mcpwm_periph.h" +#include "hal/mcpwm_ll.h" +#include "driver/gpio.h" +#include "driver/mcpwm_gen.h" +#include "mcpwm_private.h" + +static const char *TAG = "mcpwm"; + +static esp_err_t mcpwm_generator_register_to_operator(mcpwm_gen_t *gen, mcpwm_oper_t *oper) +{ + int gen_id = -1; + portENTER_CRITICAL(&oper->spinlock); + for (int i = 0; i < SOC_MCPWM_GENERATORS_PER_OPERATOR; i++) { + if (!oper->generators[i]) { + oper->generators[i] = gen; + gen_id = i; + break; + } + } + portEXIT_CRITICAL(&oper->spinlock); + ESP_RETURN_ON_FALSE(gen_id >= 0, ESP_ERR_NOT_FOUND, TAG, "no free generator in operator (%d,%d)", oper->group->group_id, oper->oper_id); + + gen->gen_id = gen_id; + gen->operator = oper; + return ESP_OK; +} + +static void mcpwm_generator_unregister_from_operator(mcpwm_gen_t *gen) +{ + mcpwm_oper_t *oper = gen->operator; + int gen_id = gen->gen_id; + + portENTER_CRITICAL(&oper->spinlock); + oper->generators[gen_id] = NULL; + portEXIT_CRITICAL(&oper->spinlock); +} + +static esp_err_t mcpwm_generator_destory(mcpwm_gen_t *gen) +{ + if (gen->operator) { + mcpwm_generator_unregister_from_operator(gen); + } + free(gen); + return ESP_OK; +} + +esp_err_t mcpwm_new_generator(mcpwm_oper_handle_t oper, const mcpwm_generator_config_t *config, mcpwm_gen_handle_t *ret_gen) +{ + esp_err_t ret = ESP_OK; + mcpwm_gen_t *gen = NULL; + ESP_GOTO_ON_FALSE(oper && config && ret_gen, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + + gen = heap_caps_calloc(1, sizeof(mcpwm_gen_t), MCPWM_MEM_ALLOC_CAPS); + ESP_GOTO_ON_FALSE(gen, ESP_ERR_NO_MEM, err, TAG, "no mem for generator"); + + ESP_GOTO_ON_ERROR(mcpwm_generator_register_to_operator(gen, oper), err, TAG, "register generator failed"); + mcpwm_group_t *group = oper->group; + mcpwm_hal_context_t *hal = &group->hal; + int oper_id = oper->oper_id; + int gen_id = gen->gen_id; + + // reset generator + mcpwm_hal_generator_reset(hal, oper_id, gen_id); + + // GPIO configuration + gpio_config_t gpio_conf = { + .intr_type = GPIO_INTR_DISABLE, + .mode = GPIO_MODE_OUTPUT | (config->flags.io_loop_back ? GPIO_MODE_INPUT : 0), // also enable the input path if `io_loop_back` is enabled + .pin_bit_mask = (1ULL << config->gen_gpio_num), + .pull_down_en = false, + .pull_up_en = true, + }; + ESP_GOTO_ON_ERROR(gpio_config(&gpio_conf), err, TAG, "config gen GPIO failed"); + esp_rom_gpio_connect_out_signal(config->gen_gpio_num, + mcpwm_periph_signals.groups[group->group_id].operators[oper_id].generators[gen_id].pwm_sig, + config->flags.invert_pwm, 0); + + // fill in other generator members + gen->gen_gpio_num = config->gen_gpio_num; + gen->spinlock = (portMUX_TYPE)portMUX_INITIALIZER_UNLOCKED; + *ret_gen = gen; + ESP_LOGD(TAG, "new generator (%d,%d,%d) at %p, GPIO %d", group->group_id, oper_id, gen_id, gen, gen->gen_gpio_num); + return ESP_OK; + +err: + if (gen) { + mcpwm_generator_destory(gen); + } + return ret; +} + +esp_err_t mcpwm_del_generator(mcpwm_gen_handle_t gen) +{ + ESP_RETURN_ON_FALSE(gen, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + mcpwm_oper_t *oper = gen->operator; + mcpwm_group_t *group = oper->group; + + ESP_LOGD(TAG, "del generator (%d,%d,%d)", group->group_id, oper->oper_id, gen->gen_id); + // recycle memory resource + ESP_RETURN_ON_ERROR(mcpwm_generator_destory(gen), TAG, "destory generator failed"); + return ESP_OK; +} + +esp_err_t mcpwm_generator_set_force_level(mcpwm_gen_handle_t gen, int level, bool hold_on) +{ + ESP_RETURN_ON_FALSE(gen && level <= 1, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + mcpwm_oper_t *oper = gen->operator; + mcpwm_group_t *group = oper->group; + mcpwm_hal_context_t *hal = &group->hal; + int oper_id = oper->oper_id; + int gen_id = gen->gen_id; + + if (level < 0) { // to remove the force level + if (hold_on) { + mcpwm_ll_gen_disable_continue_force_action(hal->dev, oper_id, gen_id); + } else { + mcpwm_ll_gen_disable_noncontinue_force_action(hal->dev, oper_id, gen_id); + } + } else { // to enable the force output level + if (hold_on) { + mcpwm_ll_gen_set_continue_force_level(hal->dev, oper_id, gen_id, level); + } else { + mcpwm_ll_gen_set_noncontinue_force_level(hal->dev, oper_id, gen_id, level); + mcpwm_ll_gen_trigger_noncontinue_force_action(hal->dev, oper_id, gen_id); + } + } + return ESP_OK; +} + +esp_err_t mcpwm_generator_set_actions_on_timer_event(mcpwm_gen_handle_t gen, mcpwm_gen_timer_event_action_t ev_act, ...) +{ + ESP_RETURN_ON_FALSE(gen, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + mcpwm_oper_t *operator= gen->operator; + mcpwm_group_t *group = operator->group; + mcpwm_timer_t *timer = operator->timer; + ESP_RETURN_ON_FALSE(timer, ESP_ERR_INVALID_STATE, TAG, "no timer is connected to the operator"); + mcpwm_gen_timer_event_action_t ev_act_itor = ev_act; + bool invalid_utep = false; + bool invalid_dtez = false; + va_list it; + va_start(it, ev_act); + while (ev_act_itor.event != MCPWM_TIMER_EVENT_INVALID) { + invalid_utep = (timer->count_mode == MCPWM_TIMER_COUNT_MODE_UP_DOWN) && + (ev_act_itor.direction == MCPWM_TIMER_DIRECTION_UP) && + (ev_act_itor.event == MCPWM_TIMER_EVENT_FULL); + invalid_dtez = (timer->count_mode == MCPWM_TIMER_COUNT_MODE_UP_DOWN) && + (ev_act_itor.direction == MCPWM_TIMER_DIRECTION_DOWN) && + (ev_act_itor.event == MCPWM_TIMER_EVENT_EMPTY); + if (invalid_utep || invalid_dtez) { + va_end(it); + ESP_RETURN_ON_FALSE(false, ESP_ERR_INVALID_ARG, TAG, "UTEP and DTEZ can't be reached under MCPWM_TIMER_COUNT_MODE_UP_DOWN mode"); + } + mcpwm_ll_generator_set_action_on_timer_event(group->hal.dev, operator->oper_id, gen->gen_id, + ev_act_itor.direction, ev_act_itor.event, ev_act_itor.action); + ev_act_itor = va_arg(it, mcpwm_gen_timer_event_action_t); + } + va_end(it); + return ESP_OK; +} + +esp_err_t mcpwm_generator_set_actions_on_compare_event(mcpwm_gen_handle_t gen, mcpwm_gen_compare_event_action_t ev_act, ...) +{ + ESP_RETURN_ON_FALSE(gen, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + mcpwm_oper_t *operator= gen->operator; + mcpwm_group_t *group = operator->group; + mcpwm_gen_compare_event_action_t ev_act_itor = ev_act; + va_list it; + va_start(it, ev_act); + while (ev_act_itor.comparator) { + mcpwm_ll_generator_set_action_on_compare_event(group->hal.dev, operator->oper_id, gen->gen_id, + ev_act_itor.direction, ev_act_itor.comparator->cmpr_id, ev_act_itor.action); + ev_act_itor = va_arg(it, mcpwm_gen_compare_event_action_t); + } + va_end(it); + return ESP_OK; +} + +esp_err_t mcpwm_generator_set_actions_on_brake_event(mcpwm_gen_handle_t gen, mcpwm_gen_brake_event_action_t ev_act, ...) +{ + ESP_RETURN_ON_FALSE(gen, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + mcpwm_oper_t *operator= gen->operator; + mcpwm_group_t *group = operator->group; + mcpwm_gen_brake_event_action_t ev_act_itor = ev_act; + va_list it; + va_start(it, ev_act); + while (ev_act_itor.brake_mode != MCPWM_OPER_BRAKE_MODE_INVALID) { + mcpwm_ll_generator_set_action_on_brake_event(group->hal.dev, operator->oper_id, gen->gen_id, + ev_act_itor.direction, ev_act_itor.brake_mode, ev_act_itor.action); + ev_act_itor = va_arg(it, mcpwm_gen_brake_event_action_t); + } + va_end(it); + return ESP_OK; +} + +esp_err_t mcpwm_generator_set_dead_time(mcpwm_gen_handle_t in_generator, mcpwm_gen_handle_t out_generator, const mcpwm_dead_time_config_t *config) +{ + ESP_RETURN_ON_FALSE(in_generator && out_generator && config, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + ESP_RETURN_ON_FALSE(in_generator->operator == out_generator->operator, ESP_ERR_INVALID_ARG, TAG, "in/out generator are not derived from the same operator"); + ESP_RETURN_ON_FALSE(config->negedge_delay_ticks < MCPWM_LL_MAX_DEAD_DELAY && config->posedge_delay_ticks < MCPWM_LL_MAX_DEAD_DELAY, + ESP_ERR_INVALID_ARG, TAG, "delay time out of range"); + mcpwm_oper_t *operator= in_generator->operator; + mcpwm_group_t *group = operator->group; + mcpwm_hal_context_t *hal = &group->hal; + int oper_id = operator->oper_id; + + // Note: to better understand the following code, you should read the deadtime module topology diagram in the TRM + // check if we want to bypass the deadtime module + bool bypass = (config->negedge_delay_ticks == 0) && (config->posedge_delay_ticks == 0); + // check is we want to delay on the both edge + bool delay_on_both_edge = config->posedge_delay_ticks && config->negedge_delay_ticks; + int out_path_id = -1; + if (bypass) { + // out path is same to the input path of generator + out_path_id = in_generator->gen_id; + } else if (config->negedge_delay_ticks) { + out_path_id = 1; // FED path + } else { + out_path_id = 0; // RED path + } + bool swap_path = out_path_id != out_generator->gen_id; + mcpwm_ll_deadtime_bypass_path(hal->dev, oper_id, out_path_id, bypass); // S0/1 + if (!bypass) { + if (config->posedge_delay_ticks) { + mcpwm_ll_deadtime_red_select_generator(hal->dev, oper_id, in_generator->gen_id); // S4 + } else { + mcpwm_ll_deadtime_fed_select_generator(hal->dev, oper_id, in_generator->gen_id); // S5 + } + mcpwm_ll_deadtime_enable_deb(hal->dev, oper_id, delay_on_both_edge); // S8 + mcpwm_ll_deadtime_invert_outpath(hal->dev, oper_id, out_path_id, config->flags.invert_output); // S2/3 + mcpwm_ll_deadtime_swap_out_path(hal->dev, oper_id, out_generator->gen_id, swap_path); // S6/S7 + } + // set delay time + if (config->posedge_delay_ticks) { + mcpwm_ll_deadtime_set_rising_delay(hal->dev, oper_id, config->posedge_delay_ticks); + } + if (config->negedge_delay_ticks) { + mcpwm_ll_deadtime_set_falling_delay(hal->dev, oper_id, config->negedge_delay_ticks); + } + + ESP_LOGD(TAG, "operator (%d,%d) dead time (R:%u,F:%u), topology code:%x", group->group_id, oper_id, + config->posedge_delay_ticks, config->negedge_delay_ticks, mcpwm_ll_deadtime_get_switch_topology(hal->dev, oper_id)); + return ESP_OK; +} diff --git a/components/driver/mcpwm/mcpwm_oper.c b/components/driver/mcpwm/mcpwm_oper.c new file mode 100644 index 0000000000..cbf3219dea --- /dev/null +++ b/components/driver/mcpwm/mcpwm_oper.c @@ -0,0 +1,363 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include "sdkconfig.h" +#if CONFIG_MCPWM_ENABLE_DEBUG_LOG +// The local log level must be defined before including esp_log.h +// Set the maximum log level for this source file +#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG +#endif +#include "freertos/FreeRTOS.h" +#include "esp_attr.h" +#include "esp_check.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_memory_utils.h" +#include "soc/soc_caps.h" +#include "soc/mcpwm_periph.h" +#include "hal/mcpwm_ll.h" +#include "driver/mcpwm_oper.h" +#include "mcpwm_private.h" + +static const char *TAG = "mcpwm"; + +static void mcpwm_operator_default_isr(void *args); + +static esp_err_t mcpwm_operator_register_to_group(mcpwm_oper_t *oper, int group_id) +{ + mcpwm_group_t *group = mcpwm_acquire_group_handle(group_id); + ESP_RETURN_ON_FALSE(group, ESP_ERR_NO_MEM, TAG, "no mem for group (%d)", group_id); + + int oper_id = -1; + portENTER_CRITICAL(&group->spinlock); + for (int i = 0; i < SOC_MCPWM_OPERATORS_PER_GROUP; i++) { + if (!group->operators[i]) { + oper_id = i; + group->operators[i] = oper; + break; + } + } + portEXIT_CRITICAL(&group->spinlock); + if (oper_id < 0) { + mcpwm_release_group_handle(group); + group = NULL; + } else { + oper->group = group; + oper->oper_id = oper_id; + } + ESP_RETURN_ON_FALSE(oper_id >= 0, ESP_ERR_NOT_FOUND, TAG, "no free operators in group (%d)", group_id); + return ESP_OK; +} + +static void mcpwm_operator_unregister_from_group(mcpwm_oper_t *oper) +{ + mcpwm_group_t *group = oper->group; + int oper_id = oper->oper_id; + + portENTER_CRITICAL(&group->spinlock); + group->operators[oper_id] = NULL; + portEXIT_CRITICAL(&group->spinlock); + + // operator has a reference on group, release it now + mcpwm_release_group_handle(group); +} + +static esp_err_t mcpwm_operator_destory(mcpwm_oper_t *oper) +{ + if (oper->intr) { + ESP_RETURN_ON_ERROR(esp_intr_free(oper->intr), TAG, "uninstall interrupt service failed"); + } + if (oper->group) { + mcpwm_operator_unregister_from_group(oper); + } + free(oper); + return ESP_OK; +} + +esp_err_t mcpwm_new_operator(const mcpwm_operator_config_t *config, mcpwm_oper_handle_t *ret_oper) +{ +#if CONFIG_MCPWM_ENABLE_DEBUG_LOG + esp_log_level_set(TAG, ESP_LOG_DEBUG); +#endif + esp_err_t ret = ESP_OK; + mcpwm_oper_t *operator= NULL; + ESP_GOTO_ON_FALSE(config && ret_oper, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + ESP_GOTO_ON_FALSE(config->group_id < SOC_MCPWM_GROUPS && config->group_id >= 0, ESP_ERR_INVALID_ARG, + err, TAG, "invalid group ID:%d", config->group_id); + + operator= heap_caps_calloc(1, sizeof(mcpwm_oper_t), MCPWM_MEM_ALLOC_CAPS); + ESP_GOTO_ON_FALSE(operator, ESP_ERR_NO_MEM, err, TAG, "no mem for operator"); + + ESP_GOTO_ON_ERROR(mcpwm_operator_register_to_group(operator, config->group_id), err, TAG, "register operator failed"); + mcpwm_group_t *group = operator->group; + int group_id = group->group_id; + mcpwm_hal_context_t *hal = &group->hal; + int oper_id = operator->oper_id; + + // reset MCPWM operator + mcpwm_hal_operator_reset(hal, oper_id); + + // set the time point that the generator can update the action + mcpwm_ll_operator_enable_update_action_on_tez(hal->dev, oper_id, config->flags.update_gen_action_on_tez); + mcpwm_ll_operator_enable_update_action_on_tep(hal->dev, oper_id, config->flags.update_gen_action_on_tep); + mcpwm_ll_operator_enable_update_action_on_sync(hal->dev, oper_id, config->flags.update_gen_action_on_sync); + // set the time point that the deadtime can update the delay parameter + mcpwm_ll_deadtime_enable_update_delay_on_tez(hal->dev, oper_id, config->flags.update_dead_time_on_tez); + mcpwm_ll_deadtime_enable_update_delay_on_tep(hal->dev, oper_id, config->flags.update_dead_time_on_tep); + mcpwm_ll_deadtime_enable_update_delay_on_sync(hal->dev, oper_id, config->flags.update_dead_time_on_sync); + // set the clock source for dead time submodule, the resolution is the same to the MCPWM group + mcpwm_ll_operator_set_deadtime_clock_src(hal->dev, oper_id, MCPWM_LL_DEADTIME_CLK_SRC_GROUP); + operator->deadtime_resolution_hz = group->resolution_hz; + + // fill in other operator members + operator->spinlock = (portMUX_TYPE)portMUX_INITIALIZER_UNLOCKED; + *ret_oper = operator; + ESP_LOGD(TAG, "new operator (%d,%d) at %p", group_id, oper_id, operator); + return ESP_OK; + +err: + if (operator) { + mcpwm_operator_destory(operator); + } + return ret; +} + +esp_err_t mcpwm_del_operator(mcpwm_oper_handle_t oper) +{ + ESP_RETURN_ON_FALSE(oper, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + for (int i = 0; i < SOC_MCPWM_COMPARATORS_PER_OPERATOR; i++) { + ESP_RETURN_ON_FALSE(!oper->comparators[i], ESP_ERR_INVALID_STATE, TAG, "comparator still in working"); + } + for (int i = 0; i < SOC_MCPWM_GENERATORS_PER_OPERATOR; i++) { + ESP_RETURN_ON_FALSE(!oper->generators[i], ESP_ERR_INVALID_STATE, TAG, "generator still in working"); + } + ESP_RETURN_ON_FALSE(!oper->soft_fault, ESP_ERR_INVALID_STATE, TAG, "soft fault still in working"); + mcpwm_group_t *group = oper->group; + int oper_id = oper->oper_id; + mcpwm_hal_context_t *hal = &group->hal; + + portENTER_CRITICAL(&group->spinlock); + mcpwm_ll_intr_enable(hal->dev, MCPWM_LL_EVENT_OPER_MASK(oper_id), false); + mcpwm_ll_intr_clear_status(hal->dev, MCPWM_LL_EVENT_OPER_MASK(oper_id)); + portEXIT_CRITICAL(&group->spinlock); + + ESP_LOGD(TAG, "del operator (%d,%d)", group->group_id, oper_id); + // recycle memory resource + ESP_RETURN_ON_ERROR(mcpwm_operator_destory(oper), TAG, "destory operator failed"); + return ESP_OK; +} + +esp_err_t mcpwm_operator_connect_timer(mcpwm_oper_handle_t oper, mcpwm_timer_handle_t timer) +{ + ESP_RETURN_ON_FALSE(oper && timer, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + ESP_RETURN_ON_FALSE(oper->group == timer->group, ESP_ERR_INVALID_ARG, TAG, "operator and timer should reside in the same group"); + mcpwm_group_t *group = oper->group; + mcpwm_hal_context_t *hal = &group->hal; + + // connect operator and timer + mcpwm_ll_operator_connect_timer(hal->dev, oper->oper_id, timer->timer_id); + // change the the clock source of deadtime submodule to use MCPWM timer + mcpwm_ll_operator_set_deadtime_clock_src(hal->dev, oper->oper_id, MCPWM_LL_DEADTIME_CLK_SRC_TIMER); + oper->deadtime_resolution_hz = timer->resolution_hz; + + oper->timer = timer; + ESP_LOGD(TAG, "connect operator (%d) and timer (%d) in group (%d)", oper->oper_id, timer->timer_id, group->group_id); + return ESP_OK; +} + +esp_err_t mcpwm_operator_apply_carrier(mcpwm_oper_handle_t oper, const mcpwm_carrier_config_t *config) +{ + ESP_RETURN_ON_FALSE(oper, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + mcpwm_group_t *group = oper->group; + mcpwm_hal_context_t *hal = &group->hal; + int oper_id = oper->oper_id; + uint32_t real_frequency = 0; + uint32_t real_fpd = 0; + float real_duty = 0.0; + + if (config && config->frequency_hz) { + uint8_t pre_scale = group->resolution_hz / 8 / config->frequency_hz; + mcpwm_ll_carrier_set_prescale(hal->dev, oper_id, pre_scale); + real_frequency = group->resolution_hz / 8 / pre_scale; + + uint8_t duty = (uint8_t)(config->duty_cycle * 8); + mcpwm_ll_carrier_set_duty(hal->dev, oper_id, duty); + real_duty = (float) duty / 8.0F; + + uint8_t first_pulse_ticks = (uint8_t)(config->first_pulse_duration_us * real_frequency / 1000000UL); + ESP_RETURN_ON_FALSE(first_pulse_ticks > 0 && first_pulse_ticks <= MCPWM_LL_MAX_CARRIER_ONESHOT, + ESP_ERR_INVALID_ARG, TAG, "invalid first pulse duration"); + mcpwm_ll_carrier_set_first_pulse_width(hal->dev, oper_id, first_pulse_ticks); + real_fpd = first_pulse_ticks * 1000000UL / real_frequency; + + mcpwm_ll_carrier_in_invert(hal->dev, oper_id, config->flags.invert_before_modulate); + mcpwm_ll_carrier_out_invert(hal->dev, oper_id, config->flags.invert_after_modulate); + } + + mcpwm_ll_carrier_enable(hal->dev, oper_id, real_frequency > 0); + + if (real_frequency > 0) { + ESP_LOGD(TAG, "enable carrier modulation for operator(%d,%d), freq=%uHz, duty=%.2f, FPD=%dus", + group->group_id, oper_id, real_frequency, real_duty, real_fpd); + } else { + ESP_LOGD(TAG, "disable carrier for operator (%d,%d)", group->group_id, oper_id); + } + return ESP_OK; +} + +esp_err_t mcpwm_operator_register_event_callbacks(mcpwm_oper_handle_t oper, const mcpwm_operator_event_callbacks_t *cbs, void *user_data) +{ + ESP_RETURN_ON_FALSE(oper && cbs, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + mcpwm_group_t *group = oper->group; + mcpwm_hal_context_t *hal = &group->hal; + int group_id = group->group_id; + int oper_id = oper->oper_id; + +#if CONFIG_MCWPM_ISR_IRAM_SAFE + if (cbs->on_brake_cbc) { + ESP_RETURN_ON_FALSE(esp_ptr_in_iram(cbs->on_brake_cbc), ESP_ERR_INVALID_ARG, TAG, "on_brake_cbc callback not in IRAM"); + } + if (cbs->on_brake_ost) { + ESP_RETURN_ON_FALSE(esp_ptr_in_iram(cbs->on_brake_ost), ESP_ERR_INVALID_ARG, TAG, "on_brake_ost callback not in IRAM"); + } + if (user_data) { + ESP_RETURN_ON_FALSE(esp_ptr_internal(user_data), ESP_ERR_INVALID_ARG, TAG, "user context not in internal RAM"); + } +#endif + + // lazy install interrupt service + if (!oper->intr) { + // we want the interrupt servie to be enabled after allocation successfully + int isr_flags = MCPWM_INTR_ALLOC_FLAG & ~ ESP_INTR_FLAG_INTRDISABLED; + ESP_RETURN_ON_ERROR(esp_intr_alloc_intrstatus(mcpwm_periph_signals.groups[group_id].irq_id, isr_flags, + (uint32_t)mcpwm_ll_intr_get_status_reg(hal->dev), MCPWM_LL_EVENT_OPER_MASK(oper_id), + mcpwm_operator_default_isr, oper, &oper->intr), TAG, "install interrupt service for operator failed"); + } + + // enable/disable interrupt events + portENTER_CRITICAL(&group->spinlock); + mcpwm_ll_intr_enable(hal->dev, MCPWM_LL_EVENT_OPER_BRAKE_CBC(oper_id), cbs->on_brake_cbc != NULL); + mcpwm_ll_intr_enable(hal->dev, MCPWM_LL_EVENT_OPER_BRAKE_OST(oper_id), cbs->on_brake_ost != NULL); + portEXIT_CRITICAL(&group->spinlock); + + oper->on_brake_cbc = cbs->on_brake_cbc; + oper->on_brake_ost = cbs->on_brake_ost; + oper->user_data = user_data; + + return ESP_OK; +} + +esp_err_t mcpwm_operator_set_brake_on_fault(mcpwm_oper_handle_t operator, const mcpwm_brake_config_t *config) +{ + ESP_RETURN_ON_FALSE(operator && config, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + mcpwm_group_t *group = operator->group; + mcpwm_fault_t *fault = config->fault; + + int oper_id = operator->oper_id; + mcpwm_ll_brake_enable_cbc_refresh_on_tez(group->hal.dev, oper_id, config->flags.cbc_recover_on_tez); + mcpwm_ll_fault_enable_cbc_refresh_on_tep(group->hal.dev, oper_id, config->flags.cbc_recover_on_tep); + + switch (fault->type) { + case MCPWM_FAULT_TYPE_GPIO: { + ESP_RETURN_ON_FALSE(group == fault->group, ESP_ERR_INVALID_ARG, TAG, "fault and operator not in the same group"); + mcpwm_gpio_fault_t *gpio_fault = __containerof(fault, mcpwm_gpio_fault_t, base); + mcpwm_ll_brake_enable_cbc_mode(group->hal.dev, oper_id, gpio_fault->fault_id, config->brake_mode == MCPWM_OPER_BRAKE_MODE_CBC); + mcpwm_ll_brake_enable_oneshot_mode(group->hal.dev, oper_id, gpio_fault->fault_id, config->brake_mode == MCPWM_OPER_BRAKE_MODE_OST); + operator->brake_mode_on_gpio_fault[gpio_fault->fault_id] = config->brake_mode; + break; + } + case MCPWM_FAULT_TYPE_SOFT: { + mcpwm_soft_fault_t *soft_fault = __containerof(fault, mcpwm_soft_fault_t, base); + ESP_RETURN_ON_FALSE(!soft_fault->operator || soft_fault->operator == operator, ESP_ERR_INVALID_STATE, TAG, "soft fault already used by another operator"); + soft_fault->operator = operator; + soft_fault->base.group = operator->group; + mcpwm_ll_brake_enable_soft_cbc(group->hal.dev, oper_id, config->brake_mode == MCPWM_OPER_BRAKE_MODE_CBC); + mcpwm_ll_brake_enable_soft_ost(group->hal.dev, oper_id, config->brake_mode == MCPWM_OPER_BRAKE_MODE_OST); + operator->brake_mode_on_soft_fault = config->brake_mode; + break; + } + default: + ESP_RETURN_ON_FALSE(false, ESP_ERR_INVALID_ARG, TAG, "unknown fault type:%d", fault->type); + break; + } + return ESP_OK; +} + +esp_err_t mcpwm_operator_recover_from_fault(mcpwm_oper_handle_t operator, mcpwm_fault_handle_t fault) +{ + ESP_RETURN_ON_FALSE(operator && fault, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + mcpwm_group_t *group = operator->group; + mcpwm_operator_brake_mode_t brake_mode; + + // check the brake mode on the fault event + switch (fault->type) { + case MCPWM_FAULT_TYPE_GPIO: { + mcpwm_gpio_fault_t *gpio_fault = __containerof(fault, mcpwm_gpio_fault_t, base); + brake_mode = operator->brake_mode_on_gpio_fault[gpio_fault->fault_id]; + break; + } + case MCPWM_FAULT_TYPE_SOFT: + brake_mode = operator->brake_mode_on_soft_fault; + break; + default: + ESP_RETURN_ON_FALSE(false, ESP_ERR_INVALID_ARG, TAG, "unknown fault type:%d", fault->type); + break; + } + + bool fault_signal_is_active = false; + if (brake_mode == MCPWM_OPER_BRAKE_MODE_OST) { + fault_signal_is_active = mcpwm_ll_ost_brake_active(group->hal.dev, operator->oper_id); + // OST brake can't recover automatically, need to manually recovery the operator + if (!fault_signal_is_active) { + mcpwm_ll_brake_clear_ost(group->hal.dev, operator->oper_id); + } + } else { + fault_signal_is_active = mcpwm_ll_cbc_brake_active(group->hal.dev, operator->oper_id); + // CBC brake can recover automatically after deactivating the fault signal + } + + ESP_RETURN_ON_FALSE(!fault_signal_is_active, ESP_ERR_INVALID_STATE, TAG, "recover fail, fault signal still active"); + return ESP_OK; +} + +static void IRAM_ATTR mcpwm_operator_default_isr(void *args) +{ + mcpwm_oper_t *oper = (mcpwm_oper_t *)args; + mcpwm_group_t *group = oper->group; + mcpwm_hal_context_t *hal = &group->hal; + int oper_id = oper->oper_id; + bool need_yield = false; + + uint32_t status = mcpwm_ll_intr_get_status(hal->dev); + mcpwm_ll_intr_clear_status(hal->dev, status & MCPWM_LL_EVENT_OPER_MASK(oper_id)); + + mcpwm_brake_event_data_t edata = {}; + + if (status & MCPWM_LL_EVENT_OPER_BRAKE_CBC(oper_id)) { + mcpwm_brake_event_cb_t cb = oper->on_brake_cbc; + if (cb) { + if (cb(oper, &edata, oper->user_data)) { + need_yield = true; + } + } + } + + if (status & MCPWM_LL_EVENT_OPER_BRAKE_OST(oper_id)) { + mcpwm_brake_event_cb_t cb = oper->on_brake_ost; + if (cb) { + if (cb(oper, &edata, oper->user_data)) { + need_yield = true; + } + } + } + + if (need_yield) { + portYIELD_FROM_ISR(); + } +} diff --git a/components/driver/mcpwm/mcpwm_private.h b/components/driver/mcpwm/mcpwm_private.h new file mode 100644 index 0000000000..b5881280f8 --- /dev/null +++ b/components/driver/mcpwm/mcpwm_private.h @@ -0,0 +1,221 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include "sdkconfig.h" +#include "freertos/FreeRTOS.h" +#include "esp_err.h" +#include "esp_intr_alloc.h" +#include "esp_heap_caps.h" +#include "esp_pm.h" +#include "soc/soc_caps.h" +#include "hal/mcpwm_hal.h" +#include "hal/mcpwm_types.h" +#include "driver/mcpwm_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#if CONFIG_MCPWM_ISR_IRAM_SAFE +#define MCPWM_MEM_ALLOC_CAPS (MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT) +#else +#define MCPWM_MEM_ALLOC_CAPS MALLOC_CAP_DEFAULT +#endif + +#if CONFIG_MCPWM_ISR_IRAM_SAFE +#define MCPWM_INTR_ALLOC_FLAG (ESP_INTR_FLAG_SHARED | ESP_INTR_FLAG_INTRDISABLED | ESP_INTR_FLAG_IRAM) +#else +#define MCPWM_INTR_ALLOC_FLAG (ESP_INTR_FLAG_SHARED | ESP_INTR_FLAG_INTRDISABLED) +#endif + +#define MCPWM_PERIPH_CLOCK_PRE_SCALE (2) +#define MCPWM_PM_LOCK_NAME_LEN_MAX 16 + +typedef struct mcpwm_group_t mcpwm_group_t; +typedef struct mcpwm_timer_t mcpwm_timer_t; +typedef struct mcpwm_cap_timer_t mcpwm_cap_timer_t; +typedef struct mcpwm_oper_t mcpwm_oper_t; +typedef struct mcpwm_cmpr_t mcpwm_cmpr_t; +typedef struct mcpwm_gen_t mcpwm_gen_t; +typedef struct mcpwm_fault_t mcpwm_fault_t; +typedef struct mcpwm_gpio_fault_t mcpwm_gpio_fault_t; +typedef struct mcpwm_soft_fault_t mcpwm_soft_fault_t; +typedef struct mcpwm_sync_t mcpwm_sync_t; +typedef struct mcpwm_gpio_sync_src_t mcpwm_gpio_sync_src_t; +typedef struct mcpwm_timer_sync_src_t mcpwm_timer_sync_src_t; +typedef struct mcpwm_soft_sync_src_t mcpwm_soft_sync_src_t; +typedef struct mcpwm_cap_channel_t mcpwm_cap_channel_t; + +struct mcpwm_group_t { + int group_id; // group ID, index from 0 + mcpwm_hal_context_t hal; // HAL instance is at group level + portMUX_TYPE spinlock; // group level spinlock + uint32_t resolution_hz; // MCPWM group clock resolution + esp_pm_lock_handle_t pm_lock; // power management lock + mcpwm_timer_clock_source_t clk_src; // source clock + mcpwm_cap_timer_t *cap_timer; // mcpwm capture timers + mcpwm_timer_t *timers[SOC_MCPWM_TIMERS_PER_GROUP]; // mcpwm timer array + mcpwm_oper_t *operators[SOC_MCPWM_OPERATORS_PER_GROUP]; // mcpwm operator array + mcpwm_gpio_fault_t *gpio_faults[SOC_MCPWM_GPIO_FAULTS_PER_GROUP]; // mcpwm fault detectors array + mcpwm_gpio_sync_src_t *gpio_sync_srcs[SOC_MCPWM_GPIO_SYNCHROS_PER_GROUP]; // mcpwm gpio sync array +#if CONFIG_PM_ENABLE + char pm_lock_name[MCPWM_PM_LOCK_NAME_LEN_MAX]; // pm lock name +#endif +}; + +typedef enum { + MCPWM_TIMER_FSM_INIT, + MCPWM_TIMER_FSM_ENABLE, +} mcpwm_timer_fsm_t; + +struct mcpwm_timer_t { + int timer_id; // timer ID, index from 0 + mcpwm_group_t *group; // which group the timer belongs to + mcpwm_timer_fsm_t fsm; // driver FSM + portMUX_TYPE spinlock; // spin lock + intr_handle_t intr; // interrupt handle + uint32_t resolution_hz; // resolution of the timer + uint32_t peak_ticks; // peak ticks that the timer could reach to + mcpwm_timer_sync_src_t *sync_src; // timer sync_src + mcpwm_timer_count_mode_t count_mode; // count mode + mcpwm_timer_event_cb_t on_full; // callback function when MCPWM timer counts to peak value + mcpwm_timer_event_cb_t on_empty; // callback function when MCPWM timer counts to zero + mcpwm_timer_event_cb_t on_stop; // callback function when MCPWM timer stops + void *user_data; // user data which would be passed to the timer callbacks +}; + +struct mcpwm_oper_t { + int oper_id; // operator ID, index from 0 + mcpwm_group_t *group; // which group the timer belongs to + mcpwm_timer_t *timer; // which timer is connected to this operator + portMUX_TYPE spinlock; // spin lock + intr_handle_t intr; // interrupt handle + mcpwm_gen_t *generators[SOC_MCPWM_GENERATORS_PER_OPERATOR]; // mcpwm generator array + mcpwm_cmpr_t *comparators[SOC_MCPWM_COMPARATORS_PER_OPERATOR]; // mcpwm comparator array + mcpwm_soft_fault_t *soft_fault; // mcpwm software fault + mcpwm_operator_brake_mode_t brake_mode_on_soft_fault; // brake mode on software triggered fault + mcpwm_operator_brake_mode_t brake_mode_on_gpio_fault[SOC_MCPWM_GPIO_FAULTS_PER_GROUP]; // brake mode on GPIO triggered faults + uint32_t deadtime_resolution_hz; // resolution of deadtime submodule + mcpwm_brake_event_cb_t on_brake_cbc; // callback function which would be invoked when mcpwm operator goes into trip zone + mcpwm_brake_event_cb_t on_brake_ost; // callback function which would be invoked when mcpwm operator goes into trip zone + void *user_data; // user data which would be passed to the trip zone callback +}; + +struct mcpwm_cmpr_t { + int cmpr_id; // comparator ID, index from 0 + mcpwm_oper_t *operator; // which operator that the comparator resides in + intr_handle_t intr; // interrupt handle + portMUX_TYPE spinlock; // spin lock + uint32_t compare_ticks; // compare value of this comparator + mcpwm_compare_event_cb_t on_reach; // ISR callback function which would be invoked on timer counter reaches compare value + void *user_data; // user data which would be passed to the comparator callbacks +}; + +struct mcpwm_gen_t { + int gen_id; // generator ID, index from 0 + mcpwm_oper_t *operator; // which operator that the generator resides in + int gen_gpio_num; // GPIO number used by the generator + portMUX_TYPE spinlock; // spin lock +}; + +typedef enum { + MCPWM_FAULT_TYPE_GPIO, // external GPIO fault + MCPWM_FAULT_TYPE_SOFT, // software fault +} mcpwm_fault_type_t; + +struct mcpwm_fault_t { + mcpwm_group_t *group; // which group the fault belongs to + mcpwm_fault_type_t type; // fault type + esp_err_t (*del)(mcpwm_fault_t *fault); +}; + +struct mcpwm_gpio_fault_t { + mcpwm_fault_t base; // base class + int fault_id; // fault detector ID, index from 0 + int gpio_num; // GPIO number of fault detector + intr_handle_t intr; // interrupt handle + mcpwm_fault_event_cb_t on_fault_enter; // ISR callback function that would be invoked when fault signal got triggered + mcpwm_fault_event_cb_t on_fault_exit; // ISR callback function that would be invoked when fault signal got clear + void *user_data; // user data which would be passed to the isr_cb +}; + +struct mcpwm_soft_fault_t { + mcpwm_fault_t base; // base class + mcpwm_oper_t *operator; // the operator where the soft fault allocated from +}; + +typedef enum { + MCPWM_SYNC_TYPE_TIMER, // sync event generated by MCPWM timer count event + MCPWM_SYNC_TYPE_GPIO, // sync event generated by GPIO + MCPWM_SYNC_TYPE_SOFT, // sync event generated by software +} mcpwm_sync_src_type_t; + +struct mcpwm_sync_t { + mcpwm_group_t *group; // which group the sync_src belongs to + mcpwm_sync_src_type_t type; // sync_src type + esp_err_t (*del)(mcpwm_sync_t *sync_src); +}; + +struct mcpwm_gpio_sync_src_t { + mcpwm_sync_t base; // base class + int sync_id; // sync signal ID + int gpio_num; // GPIO number +}; + +struct mcpwm_timer_sync_src_t { + mcpwm_sync_t base; // base class + mcpwm_timer_t *timer; // timer handle, where this sync_src allocated from +}; + +typedef enum { + MCPWM_SOFT_SYNC_FROM_NONE, // the software sync event generator has not been assigned + MCPWM_SOFT_SYNC_FROM_TIMER, // the software sync event is generated by MCPWM timer + MCPWM_SOFT_SYNC_FROM_CAP, // the software sync event is generated by MCPWM capture timer +} mcpwm_soft_sync_source_t; + +struct mcpwm_soft_sync_src_t { + mcpwm_sync_t base; // base class + mcpwm_soft_sync_source_t soft_sync_from; // where the software sync event is generated by + union { + mcpwm_timer_t *timer; // soft sync is generated by which MCPWM timer + mcpwm_cap_timer_t *cap_timer; // soft sync is generated by which MCPWM capture timer + }; +}; + +typedef enum { + MCPWM_CAP_TIMER_FSM_INIT, + MCPWM_CAP_TIMER_FSM_ENABLE, +} mcpwm_cap_timer_fsm_t; + +struct mcpwm_cap_timer_t { + mcpwm_group_t *group; // which group the capture timer belongs to + portMUX_TYPE spinlock; // spin lock, to prevent concurrently accessing capture timer level resources, including registers + uint32_t resolution_hz; // resolution of capture timer + mcpwm_cap_timer_fsm_t fsm; // driver FSM + esp_pm_lock_handle_t pm_lock; // power management lock + mcpwm_cap_channel_t *cap_channels[SOC_MCPWM_CAPTURE_CHANNELS_PER_TIMER]; // capture channel array +}; + +struct mcpwm_cap_channel_t { + int cap_chan_id; // capture channel ID, index from 0 + mcpwm_cap_timer_t *cap_timer; // which capture timer that the channel resides in + uint32_t prescale; // prescale of capture signal + int gpio_num; // GPIO number used by the channel + intr_handle_t intr; // Interrupt handle + mcpwm_capture_event_cb_t on_cap; // Callback function which would be invoked in capture interrupt routine + void *user_data; // user data which would be passed to the capture callback +}; + +mcpwm_group_t *mcpwm_acquire_group_handle(int group_id); +void mcpwm_release_group_handle(mcpwm_group_t *group); +esp_err_t mcpwm_select_periph_clock(mcpwm_group_t *group, mcpwm_timer_clock_source_t clk_src); + +#ifdef __cplusplus +} +#endif diff --git a/components/driver/mcpwm/mcpwm_sync.c b/components/driver/mcpwm/mcpwm_sync.c new file mode 100644 index 0000000000..164c36d9ee --- /dev/null +++ b/components/driver/mcpwm/mcpwm_sync.c @@ -0,0 +1,297 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include "sdkconfig.h" +#if CONFIG_MCPWM_ENABLE_DEBUG_LOG +// The local log level must be defined before including esp_log.h +// Set the maximum log level for this source file +#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG +#endif +#include "freertos/FreeRTOS.h" +#include "esp_attr.h" +#include "esp_check.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_memory_utils.h" +#include "soc/soc_caps.h" +#include "soc/mcpwm_periph.h" +#include "hal/mcpwm_ll.h" +#include "driver/mcpwm_sync.h" +#include "driver/gpio.h" +#include "mcpwm_private.h" + +static const char *TAG = "mcpwm"; + +static esp_err_t mcpwm_del_timer_sync_src(mcpwm_sync_t *sync_src); +static esp_err_t mcpwm_del_gpio_sync_src(mcpwm_sync_t *sync_src); +static esp_err_t mcpwm_del_soft_sync_src(mcpwm_sync_t *sync_src); + +static esp_err_t mcpwm_timer_sync_src_register_to_timer(mcpwm_timer_sync_src_t *timer_sync_src, mcpwm_timer_t *timer) +{ + bool new_sync = false; + portENTER_CRITICAL(&timer->spinlock); + if (!timer->sync_src) { + new_sync = true; + timer->sync_src = timer_sync_src; + } + portEXIT_CRITICAL(&timer->spinlock); + ESP_RETURN_ON_FALSE(new_sync, ESP_ERR_INVALID_STATE, TAG, "timer sync_src already installed for timer (%d,%d)", + timer->group->group_id, timer->timer_id); + + timer_sync_src->timer = timer; + return ESP_OK; +} + +static void mcpwm_timer_sync_src_unregister_from_timer(mcpwm_timer_sync_src_t *timer_sync_src) +{ + mcpwm_timer_t *timer = timer_sync_src->timer; + + portENTER_CRITICAL(&timer->spinlock); + timer->sync_src = NULL; + portEXIT_CRITICAL(&timer->spinlock); +} + +static esp_err_t mcpwm_timer_sync_src_destory(mcpwm_timer_sync_src_t *timer_sync_src) +{ + if (timer_sync_src->timer) { + mcpwm_timer_sync_src_unregister_from_timer(timer_sync_src); + } + free(timer_sync_src); + return ESP_OK; +} + +esp_err_t mcpwm_new_timer_sync_src(mcpwm_timer_handle_t timer, const mcpwm_timer_sync_src_config_t *config, mcpwm_sync_handle_t *ret_sync) +{ + esp_err_t ret = ESP_OK; + mcpwm_timer_sync_src_t *timer_sync_src = NULL; + ESP_GOTO_ON_FALSE(timer && config && ret_sync, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + timer_sync_src = heap_caps_calloc(1, sizeof(mcpwm_timer_sync_src_t), MCPWM_MEM_ALLOC_CAPS); + ESP_GOTO_ON_FALSE(timer_sync_src, ESP_ERR_NO_MEM, err, TAG, "no mem for timer sync_src"); + + ESP_GOTO_ON_ERROR(mcpwm_timer_sync_src_register_to_timer(timer_sync_src, timer), err, TAG, "register timer sync_src failed"); + mcpwm_group_t *group = timer->group; + mcpwm_hal_context_t *hal = &group->hal; + int timer_id = timer->timer_id; + + if (config->flags.propagate_input_sync) { + mcpwm_ll_timer_propagate_input_sync(hal->dev, timer_id); + } else { + switch (config->timer_event) { + case MCPWM_TIMER_EVENT_EMPTY: + mcpwm_ll_timer_sync_out_on_timer_event(hal->dev, timer_id, MCPWM_TIMER_EVENT_EMPTY); + break; + case MCPWM_TIMER_EVENT_FULL: + mcpwm_ll_timer_sync_out_on_timer_event(hal->dev, timer_id, MCPWM_TIMER_EVENT_FULL); + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_INVALID_ARG, err, TAG, "unknown timer sync event:%d", config->timer_event); + break; + } + } + + timer_sync_src->base.group = group; + timer_sync_src->base.type = MCPWM_SYNC_TYPE_TIMER; + timer_sync_src->base.del = mcpwm_del_timer_sync_src; + *ret_sync = &timer_sync_src->base; + ESP_LOGD(TAG, "new timer sync_src at %p in timer (%d,%d), event:%c", timer_sync_src, group->group_id, timer_id, "EP?"[config->timer_event]); + return ESP_OK; + +err: + if (timer_sync_src) { + mcpwm_timer_sync_src_destory(timer_sync_src); + } + return ret; +} + +static esp_err_t mcpwm_del_timer_sync_src(mcpwm_sync_t *sync_src) +{ + mcpwm_timer_sync_src_t *timer_sync_src = __containerof(sync_src, mcpwm_timer_sync_src_t, base); + mcpwm_timer_t *timer = timer_sync_src->timer; + int timer_id = timer->timer_id; + mcpwm_group_t *group = sync_src->group; + + mcpwm_ll_timer_disable_sync_out(group->hal.dev, timer_id); + ESP_LOGD(TAG, "del timer sync_src in timer (%d,%d)", group->group_id, timer_id); + ESP_RETURN_ON_ERROR(mcpwm_timer_sync_src_destory(timer_sync_src), TAG, "destory timer sync_src failed"); + return ESP_OK; +} + +static esp_err_t mcpwm_gpio_sync_src_register_to_group(mcpwm_gpio_sync_src_t *gpio_sync_src, int group_id) +{ + mcpwm_group_t *group = mcpwm_acquire_group_handle(group_id); + ESP_RETURN_ON_FALSE(group, ESP_ERR_NO_MEM, TAG, "no mem for group (%d)", group_id); + + int sync_id = -1; + portENTER_CRITICAL(&group->spinlock); + for (int i = 0; i < SOC_MCPWM_GPIO_SYNCHROS_PER_GROUP; i++) { + if (!group->gpio_sync_srcs[i]) { + sync_id = i; + group->gpio_sync_srcs[i] = gpio_sync_src; + break; + } + } + portEXIT_CRITICAL(&group->spinlock); + + if (sync_id < 0) { + mcpwm_release_group_handle(group); + group = NULL; + } else { + gpio_sync_src->base.group = group; + gpio_sync_src->sync_id = sync_id; + } + ESP_RETURN_ON_FALSE(sync_id >= 0, ESP_ERR_NOT_FOUND, TAG, "no free gpio sync_src in group (%d)", group_id); + + return ESP_OK; +} + +static void mcpwm_gpio_sync_src_unregister_from_group(mcpwm_gpio_sync_src_t *gpio_sync_src) +{ + mcpwm_group_t *group = gpio_sync_src->base.group; + int sync_id = gpio_sync_src->sync_id; + + portENTER_CRITICAL(&group->spinlock); + group->gpio_sync_srcs[sync_id] = NULL; + portEXIT_CRITICAL(&group->spinlock); + + // sync_src has a reference on group, release it now + mcpwm_release_group_handle(group); +} + +static esp_err_t mcpwm_gpio_sync_src_destory(mcpwm_gpio_sync_src_t *gpio_sync_src) +{ + if (gpio_sync_src->base.group) { + mcpwm_gpio_sync_src_unregister_from_group(gpio_sync_src); + } + free(gpio_sync_src); + return ESP_OK; +} + +esp_err_t mcpwm_new_gpio_sync_src(const mcpwm_gpio_sync_src_config_t *config, mcpwm_sync_handle_t *ret_sync) +{ +#if CONFIG_MCPWM_ENABLE_DEBUG_LOG + esp_log_level_set(TAG, ESP_LOG_DEBUG); +#endif + esp_err_t ret = ESP_OK; + mcpwm_gpio_sync_src_t *gpio_sync_src = NULL; + ESP_GOTO_ON_FALSE(config && ret_sync, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + ESP_GOTO_ON_FALSE(config->group_id < SOC_MCPWM_GROUPS && config->group_id >= 0, ESP_ERR_INVALID_ARG, + err, TAG, "invalid group ID:%d", config->group_id); + + gpio_sync_src = heap_caps_calloc(1, sizeof(mcpwm_gpio_sync_src_t), MCPWM_MEM_ALLOC_CAPS); + ESP_GOTO_ON_FALSE(gpio_sync_src, ESP_ERR_NO_MEM, err, TAG, "no mem for gpio sync_src"); + + ESP_GOTO_ON_ERROR(mcpwm_gpio_sync_src_register_to_group(gpio_sync_src, config->group_id), err, TAG, "register gpio sync_src failed"); + mcpwm_group_t *group = gpio_sync_src->base.group; + int group_id = group->group_id; + int sync_id = gpio_sync_src->sync_id; + + // GPIO configuration + gpio_config_t gpio_conf = { + .intr_type = GPIO_INTR_DISABLE, + .mode = GPIO_MODE_INPUT | (config->flags.io_loop_back ? GPIO_MODE_OUTPUT : 0), // also enable the output path if `io_loop_back` is enabled + .pin_bit_mask = (1ULL << config->gpio_num), + .pull_down_en = config->flags.pull_down, + .pull_up_en = config->flags.pull_up, + }; + ESP_GOTO_ON_ERROR(gpio_config(&gpio_conf), err, TAG, "config sync GPIO failed"); + esp_rom_gpio_connect_in_signal(config->gpio_num, mcpwm_periph_signals.groups[group_id].gpio_synchros[sync_id].sync_sig, 0); + + // different ext sync share the same config register, using a group level spin lock + portENTER_CRITICAL(&group->spinlock); + mcpwm_ll_invert_gpio_sync_input(group->hal.dev, sync_id, config->flags.active_neg); + portEXIT_CRITICAL(&group->spinlock); + + // fill in other operator members + gpio_sync_src->base.type = MCPWM_SYNC_TYPE_GPIO; + gpio_sync_src->gpio_num = config->gpio_num; + gpio_sync_src->base.del = mcpwm_del_gpio_sync_src; + *ret_sync = &gpio_sync_src->base; + ESP_LOGD(TAG, "new gpio sync_src (%d,%d) at %p, GPIO:%d", group_id, sync_id, gpio_sync_src, config->gpio_num); + return ESP_OK; + +err: + if (gpio_sync_src) { + mcpwm_gpio_sync_src_destory(gpio_sync_src); + } + return ret; +} + +static esp_err_t mcpwm_del_gpio_sync_src(mcpwm_sync_t *sync_src) +{ + mcpwm_gpio_sync_src_t *gpio_sync_src = __containerof(sync_src, mcpwm_gpio_sync_src_t, base); + mcpwm_group_t *group = sync_src->group; + + ESP_LOGD(TAG, "del gpio sync_src (%d,%d)", group->group_id, gpio_sync_src->sync_id); + gpio_reset_pin(gpio_sync_src->gpio_num); + + // recycle memory resource + ESP_RETURN_ON_ERROR(mcpwm_gpio_sync_src_destory(gpio_sync_src), TAG, "destory GPIO sync_src failed"); + return ESP_OK; +} + +esp_err_t mcpwm_new_soft_sync_src(const mcpwm_soft_sync_config_t *config, mcpwm_sync_handle_t *ret_sync) +{ + esp_err_t ret = ESP_OK; + mcpwm_soft_sync_src_t *soft_sync = NULL; + ESP_GOTO_ON_FALSE(config && ret_sync, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + soft_sync = heap_caps_calloc(1, sizeof(mcpwm_soft_sync_src_t), MCPWM_MEM_ALLOC_CAPS); + ESP_GOTO_ON_FALSE(soft_sync, ESP_ERR_NO_MEM, err, TAG, "no mem for soft sync"); + + // fill in other sync member + soft_sync->soft_sync_from = MCPWM_SOFT_SYNC_FROM_NONE; + soft_sync->base.type = MCPWM_SYNC_TYPE_SOFT; + soft_sync->base.del = mcpwm_del_soft_sync_src; + *ret_sync = &soft_sync->base; + ESP_LOGD(TAG, "new soft sync at %p", soft_sync); + return ESP_OK; + +err: + if (soft_sync) { + free(soft_sync); + } + return ret; +} + +static esp_err_t mcpwm_del_soft_sync_src(mcpwm_sync_t *sync_src) +{ + mcpwm_soft_sync_src_t *soft_sync = __containerof(sync_src, mcpwm_soft_sync_src_t, base); + ESP_LOGD(TAG, "del soft sync %p", soft_sync); + free(soft_sync); + return ESP_OK; +} + +esp_err_t mcpwm_del_sync_src(mcpwm_sync_handle_t sync_src) +{ + ESP_RETURN_ON_FALSE(sync_src, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + return sync_src->del(sync_src); +} + +esp_err_t mcpwm_soft_sync_activate(mcpwm_sync_handle_t sync_src) +{ + ESP_RETURN_ON_FALSE(sync_src, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + ESP_RETURN_ON_FALSE(sync_src->type == MCPWM_SYNC_TYPE_SOFT, ESP_ERR_INVALID_ARG, TAG, "not a valid soft sync"); + mcpwm_group_t *group = sync_src->group; + mcpwm_soft_sync_src_t *soft_sync = __containerof(sync_src, mcpwm_soft_sync_src_t, base); + + switch (soft_sync->soft_sync_from) { + case MCPWM_SOFT_SYNC_FROM_TIMER: { + mcpwm_timer_t *timer = soft_sync->timer; + mcpwm_ll_timer_trigger_soft_sync(group->hal.dev, timer->timer_id); + break; + } + case MCPWM_SOFT_SYNC_FROM_CAP: { + mcpwm_ll_capture_trigger_sw_sync(group->hal.dev); + break; + } + default: + ESP_RETURN_ON_FALSE(false, ESP_ERR_INVALID_STATE, TAG, "no soft sync generator is assigned"); + break; + } + return ESP_OK; +} diff --git a/components/driver/mcpwm/mcpwm_timer.c b/components/driver/mcpwm/mcpwm_timer.c new file mode 100644 index 0000000000..e816a7db7a --- /dev/null +++ b/components/driver/mcpwm/mcpwm_timer.c @@ -0,0 +1,364 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include "sdkconfig.h" +#if CONFIG_MCPWM_ENABLE_DEBUG_LOG +// The local log level must be defined before including esp_log.h +// Set the maximum log level for this source file +#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG +#endif +#include "freertos/FreeRTOS.h" +#include "esp_attr.h" +#include "esp_check.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_memory_utils.h" +#include "soc/soc_caps.h" +#include "soc/mcpwm_periph.h" +#include "hal/mcpwm_ll.h" +#include "driver/mcpwm_timer.h" +#include "esp_private/mcpwm.h" +#include "mcpwm_private.h" + +static const char *TAG = "mcpwm"; + +static void mcpwm_timer_default_isr(void *args); + +static esp_err_t mcpwm_timer_register_to_group(mcpwm_timer_t *timer, int group_id) +{ + mcpwm_group_t *group = mcpwm_acquire_group_handle(group_id); + ESP_RETURN_ON_FALSE(group, ESP_ERR_NO_MEM, TAG, "no mem for group (%d)", group_id); + + int timer_id = -1; + portENTER_CRITICAL(&group->spinlock); + for (int i = 0; i < SOC_MCPWM_TIMERS_PER_GROUP; i++) { + if (!group->timers[i]) { + timer_id = i; + group->timers[i] = timer; + break; + } + } + portEXIT_CRITICAL(&group->spinlock); + if (timer_id < 0) { + mcpwm_release_group_handle(group); + group = NULL; + } else { + timer->group = group; + timer->timer_id = timer_id; + } + ESP_RETURN_ON_FALSE(timer_id >= 0, ESP_ERR_NOT_FOUND, TAG, "no free timer in group (%d)", group_id); + return ESP_OK; +} + +static void mcpwm_timer_unregister_from_group(mcpwm_timer_t *timer) +{ + mcpwm_group_t *group = timer->group; + int timer_id = timer->timer_id; + + portENTER_CRITICAL(&group->spinlock); + group->timers[timer_id] = NULL; + portEXIT_CRITICAL(&group->spinlock); + + // timer has a reference on group, release it now + mcpwm_release_group_handle(group); +} + +static esp_err_t mcpwm_timer_destory(mcpwm_timer_t *timer) +{ + if (timer->intr) { + ESP_RETURN_ON_ERROR(esp_intr_free(timer->intr), TAG, "uninstall interrupt service failed"); + } + if (timer->group) { + mcpwm_timer_unregister_from_group(timer); + } + free(timer); + return ESP_OK; +} + +esp_err_t mcpwm_new_timer(const mcpwm_timer_config_t *config, mcpwm_timer_handle_t *ret_timer) +{ +#if CONFIG_MCPWM_ENABLE_DEBUG_LOG + esp_log_level_set(TAG, ESP_LOG_DEBUG); +#endif + esp_err_t ret = ESP_OK; + mcpwm_timer_t *timer = NULL; + ESP_GOTO_ON_FALSE(config && ret_timer, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + ESP_GOTO_ON_FALSE(config->group_id < SOC_MCPWM_GROUPS && config->group_id >= 0, ESP_ERR_INVALID_ARG, + err, TAG, "invalid group ID:%d", config->group_id); + + timer = heap_caps_calloc(1, sizeof(mcpwm_timer_t), MCPWM_MEM_ALLOC_CAPS); + ESP_GOTO_ON_FALSE(timer, ESP_ERR_NO_MEM, err, TAG, "no mem for timer"); + + ESP_GOTO_ON_ERROR(mcpwm_timer_register_to_group(timer, config->group_id), err, TAG, "register timer failed"); + mcpwm_group_t *group = timer->group; + int group_id = group->group_id; + mcpwm_hal_context_t *hal = &group->hal; + int timer_id = timer->timer_id; + // select the clock source + ESP_GOTO_ON_ERROR(mcpwm_select_periph_clock(group, config->clk_src), err, TAG, "set group clock failed"); + // reset the timer to a determined state + mcpwm_hal_timer_reset(hal, timer_id); + // set timer resolution + uint32_t prescale = group->resolution_hz / config->resolution_hz; + mcpwm_ll_timer_set_clock_prescale(hal->dev, timer_id, prescale); + timer->resolution_hz = group->resolution_hz / prescale; + if (timer->resolution_hz != config->resolution_hz) { + ESP_LOGW(TAG, "adjust timer resolution to %uHz", timer->resolution_hz); + } + + // set the peak tickes that the timer can reach to + timer->count_mode = config->count_mode; + uint32_t peak_ticks = config->period_ticks; + if (timer->count_mode == MCPWM_TIMER_COUNT_MODE_UP_DOWN) { + peak_ticks /= 2; // in symmetric mode, peak_ticks = period_ticks / 2 + } + timer->peak_ticks = peak_ticks; + mcpwm_ll_timer_set_peak(hal->dev, timer_id, peak_ticks, timer->count_mode == MCPWM_TIMER_COUNT_MODE_UP_DOWN); + // set count direction + mcpwm_ll_timer_set_count_mode(hal->dev, timer_id, timer->count_mode); + // what time is allowed to update the period + mcpwm_ll_timer_enable_update_period_on_sync(hal->dev, timer_id, config->flags.update_period_on_sync); + mcpwm_ll_timer_enable_update_period_on_tez(hal->dev, timer_id, config->flags.update_period_on_empty); + + // fill in other timer specific members + timer->spinlock = (portMUX_TYPE)portMUX_INITIALIZER_UNLOCKED; + timer->fsm = MCPWM_TIMER_FSM_INIT; + *ret_timer = timer; + ESP_LOGD(TAG, "new timer(%d,%d) at %p, resolution:%uHz, peak:%u, count_mod:%c", + group_id, timer_id, timer, timer->resolution_hz, timer->peak_ticks, "SUDB"[timer->count_mode]); + return ESP_OK; + +err: + if (timer) { + mcpwm_timer_destory(timer); + } + return ret; +} + +esp_err_t mcpwm_del_timer(mcpwm_timer_handle_t timer) +{ + ESP_RETURN_ON_FALSE(timer, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + // check child resources are in free state + ESP_RETURN_ON_FALSE(!timer->sync_src, ESP_ERR_INVALID_STATE, TAG, "timer sync_src still in working"); + ESP_RETURN_ON_FALSE(timer->fsm == MCPWM_TIMER_FSM_INIT, ESP_ERR_INVALID_STATE, TAG, "timer not in init state"); + mcpwm_group_t *group = timer->group; + int timer_id = timer->timer_id; + mcpwm_hal_context_t *hal = &group->hal; + + // disable and clear the pending interrupt + portENTER_CRITICAL(&group->spinlock); + mcpwm_ll_intr_enable(hal->dev, MCPWM_LL_EVENT_TIMER_MASK(timer_id), false); + mcpwm_ll_intr_clear_status(hal->dev, MCPWM_LL_EVENT_TIMER_MASK(timer_id)); + portEXIT_CRITICAL(&group->spinlock); + + ESP_LOGD(TAG, "del timer (%d,%d)", group->group_id, timer_id); + // recycle memory resource + ESP_RETURN_ON_ERROR(mcpwm_timer_destory(timer), TAG, "destory timer failed"); + return ESP_OK; +} + +esp_err_t mcpwm_timer_register_event_callbacks(mcpwm_timer_handle_t timer, const mcpwm_timer_event_callbacks_t *cbs, void *user_data) +{ + ESP_RETURN_ON_FALSE(timer && cbs, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + ESP_RETURN_ON_FALSE(timer->fsm == MCPWM_TIMER_FSM_INIT, ESP_ERR_INVALID_STATE, TAG, "timer not in init state"); + mcpwm_group_t *group = timer->group; + int group_id = group->group_id; + int timer_id = timer->timer_id; + mcpwm_hal_context_t *hal = &group->hal; + +#if CONFIG_MCWPM_ISR_IRAM_SAFE + if (cbs->on_empty) { + ESP_RETURN_ON_FALSE(esp_ptr_in_iram(cbs->on_empty), ESP_ERR_INVALID_ARG, TAG, "on_empty callback not in IRAM"); + } + if (cbs->on_full) { + ESP_RETURN_ON_FALSE(esp_ptr_in_iram(cbs->on_full), ESP_ERR_INVALID_ARG, TAG, "on_full callback not in IRAM"); + } + if (cbs->on_stop) { + ESP_RETURN_ON_FALSE(esp_ptr_in_iram(cbs->on_stop), ESP_ERR_INVALID_ARG, TAG, "on_stop callback not in IRAM"); + } + if (user_data) { + ESP_RETURN_ON_FALSE(esp_ptr_internal(user_data), ESP_ERR_INVALID_ARG, TAG, "user context not in internal RAM"); + } +#endif + + // lazy install interrupt service + if (!timer->intr) { + int isr_flags = MCPWM_INTR_ALLOC_FLAG; + ESP_RETURN_ON_ERROR(esp_intr_alloc_intrstatus(mcpwm_periph_signals.groups[group_id].irq_id, isr_flags, + (uint32_t)mcpwm_ll_intr_get_status_reg(hal->dev), MCPWM_LL_EVENT_TIMER_MASK(timer_id), + mcpwm_timer_default_isr, timer, &timer->intr), TAG, "install interrupt service for timer failed"); + } + + // enable/disable interrupt events + portENTER_CRITICAL(&group->spinlock); + mcpwm_ll_intr_enable(hal->dev, MCPWM_LL_EVENT_TIMER_FULL(timer_id), cbs->on_full != NULL); + mcpwm_ll_intr_enable(hal->dev, MCPWM_LL_EVENT_TIMER_EMPTY(timer_id), cbs->on_empty != NULL); + mcpwm_ll_intr_enable(hal->dev, MCPWM_LL_EVENT_TIMER_STOP(timer_id), cbs->on_stop != NULL); + portEXIT_CRITICAL(&group->spinlock); + + timer->on_stop = cbs->on_stop; + timer->on_full = cbs->on_full; + timer->on_empty = cbs->on_empty; + timer->user_data = user_data; + return ESP_OK; +} + +esp_err_t mcpwm_timer_get_phase(mcpwm_timer_handle_t timer, uint32_t *count_value, mcpwm_timer_direction_t *direction) +{ + ESP_RETURN_ON_FALSE(timer && count_value && direction, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + mcpwm_group_t *group = timer->group; + int timer_id = timer->timer_id; + mcpwm_hal_context_t *hal = &group->hal; + + portENTER_CRITICAL(&timer->spinlock); + *count_value = mcpwm_ll_timer_get_count_value(hal->dev, timer_id); + *direction = mcpwm_ll_timer_get_count_direction(hal->dev, timer_id); + portEXIT_CRITICAL(&timer->spinlock); + return ESP_OK; +} + +esp_err_t mcpwm_timer_enable(mcpwm_timer_handle_t timer) +{ + ESP_RETURN_ON_FALSE(timer, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + ESP_RETURN_ON_FALSE(timer->fsm == MCPWM_TIMER_FSM_INIT, ESP_ERR_INVALID_STATE, TAG, "timer not in init state"); + mcpwm_group_t *group = timer->group; + if (timer->intr) { + ESP_RETURN_ON_ERROR(esp_intr_enable(timer->intr), TAG, "enable interrupt failed"); + } + if (group->pm_lock) { + ESP_RETURN_ON_ERROR(esp_pm_lock_acquire(group->pm_lock), TAG, "acquire pm lock failed"); + } + timer->fsm = MCPWM_TIMER_FSM_ENABLE; + return ESP_OK; +} + +esp_err_t mcpwm_timer_disable(mcpwm_timer_handle_t timer) +{ + ESP_RETURN_ON_FALSE(timer, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + ESP_RETURN_ON_FALSE(timer->fsm == MCPWM_TIMER_FSM_ENABLE, ESP_ERR_INVALID_STATE, TAG, "timer not in enable state"); + mcpwm_group_t *group = timer->group; + if (timer->intr) { + ESP_RETURN_ON_ERROR(esp_intr_disable(timer->intr), TAG, "disable interrupt failed"); + } + if (group->pm_lock) { + ESP_RETURN_ON_ERROR(esp_pm_lock_release(group->pm_lock), TAG, "acquire pm lock failed"); + } + timer->fsm = MCPWM_TIMER_FSM_INIT; + return ESP_OK; +} + +esp_err_t mcpwm_timer_start_stop(mcpwm_timer_handle_t timer, mcpwm_timer_start_stop_cmd_t command) +{ + ESP_RETURN_ON_FALSE(timer, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + ESP_RETURN_ON_FALSE(timer->fsm == MCPWM_TIMER_FSM_ENABLE, ESP_ERR_INVALID_STATE, TAG, "timer not in enable state"); + mcpwm_group_t *group = timer->group; + + portENTER_CRITICAL_SAFE(&timer->spinlock); + mcpwm_ll_timer_set_start_stop_command(group->hal.dev, timer->timer_id, command); + portEXIT_CRITICAL_SAFE(&timer->spinlock); + return ESP_OK; +} + +esp_err_t mcpwm_timer_set_phase_on_sync(mcpwm_timer_handle_t timer, const mcpwm_timer_sync_phase_config_t *config) +{ + ESP_RETURN_ON_FALSE(timer && config, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + mcpwm_group_t *group = timer->group; + mcpwm_hal_context_t *hal = &group->hal; + int group_id = group->group_id; + int timer_id = timer->timer_id; + mcpwm_sync_handle_t sync_source = config->sync_src; + + // enable sync feature and set sync phase + if (sync_source) { + ESP_RETURN_ON_FALSE(config->count_value < MCPWM_LL_MAX_COUNT_VALUE, ESP_ERR_INVALID_ARG, TAG, "invalid sync count value"); + switch (sync_source->type) { + case MCPWM_SYNC_TYPE_TIMER: { + ESP_RETURN_ON_FALSE(group == sync_source->group, ESP_ERR_INVALID_ARG, TAG, "timer and sync source are not in the same group"); + mcpwm_timer_sync_src_t *timer_sync_src = __containerof(sync_source, mcpwm_timer_sync_src_t, base); + mcpwm_ll_timer_set_timer_sync_input(hal->dev, timer_id, timer_sync_src->timer->timer_id); + ESP_LOGD(TAG, "enable sync to timer (%d,%d) for timer (%d,%d)", + group_id, timer_sync_src->timer->timer_id, group_id, timer_id); + break; + } + case MCPWM_SYNC_TYPE_GPIO: { + ESP_RETURN_ON_FALSE(group == sync_source->group, ESP_ERR_INVALID_ARG, TAG, "timer and sync source are not in the same group"); + mcpwm_gpio_sync_src_t *gpio_sync_src = __containerof(sync_source, mcpwm_gpio_sync_src_t, base); + mcpwm_ll_timer_set_gpio_sync_input(hal->dev, timer_id, gpio_sync_src->sync_id); + ESP_LOGD(TAG, "enable sync to gpio (%d) for timer (%d,%d)", + gpio_sync_src->gpio_num, group_id, timer_id); + break; + } + case MCPWM_SYNC_TYPE_SOFT: { + mcpwm_soft_sync_src_t *soft_sync = __containerof(sync_source, mcpwm_soft_sync_src_t, base); + if (soft_sync->soft_sync_from == MCPWM_SOFT_SYNC_FROM_TIMER && soft_sync->timer != timer) { + ESP_RETURN_ON_FALSE(false, ESP_ERR_INVALID_STATE, TAG, "soft sync already used by another timer"); + } + soft_sync->soft_sync_from = MCPWM_SOFT_SYNC_FROM_TIMER; + soft_sync->timer = timer; + soft_sync->base.group = group; + break; + } + } + + mcpwm_ll_timer_set_sync_phase_direction(hal->dev, timer_id, config->direction); + mcpwm_ll_timer_set_sync_phase_value(hal->dev, timer_id, config->count_value); + mcpwm_ll_timer_enable_sync_input(hal->dev, timer_id, true); + } else { // disable sync feature + mcpwm_ll_timer_enable_sync_input(hal->dev, timer_id, false); + ESP_LOGD(TAG, "disable sync for timer (%d,%d)", group_id, timer_id); + } + return ESP_OK; +} + +static void IRAM_ATTR mcpwm_timer_default_isr(void *args) +{ + mcpwm_timer_t *timer = (mcpwm_timer_t *)args; + mcpwm_group_t *group = timer->group; + mcpwm_hal_context_t *hal = &group->hal; + int timer_id = timer->timer_id; + bool need_yield = false; + uint32_t status = mcpwm_ll_intr_get_status(hal->dev); + mcpwm_ll_intr_clear_status(hal->dev, status & MCPWM_LL_EVENT_TIMER_MASK(timer_id)); + + mcpwm_timer_event_data_t edata = { + .direction = mcpwm_ll_timer_get_count_direction(hal->dev, timer_id), + .count_value = mcpwm_ll_timer_get_count_value(hal->dev, timer_id), + }; + + if (status & MCPWM_LL_EVENT_TIMER_STOP(timer_id)) { + mcpwm_timer_event_cb_t cb = timer->on_stop; + if (cb) { + if (cb(timer, &edata, timer->user_data)) { + need_yield = true; + } + } + } + + if (status & MCPWM_LL_EVENT_TIMER_FULL(timer_id)) { + mcpwm_timer_event_cb_t cb = timer->on_full; + if (cb) { + if (cb(timer, &edata, timer->user_data)) { + need_yield = true; + } + } + } + + if (status & MCPWM_LL_EVENT_TIMER_EMPTY(timer_id)) { + mcpwm_timer_event_cb_t cb = timer->on_empty; + if (cb) { + if (cb(timer, &edata, timer->user_data)) { + need_yield = true; + } + } + } + + if (need_yield) { + portYIELD_FROM_ISR(); + } +} diff --git a/components/driver/test_apps/mcpwm/CMakeLists.txt b/components/driver/test_apps/mcpwm/CMakeLists.txt new file mode 100644 index 0000000000..ad15f86dc1 --- /dev/null +++ b/components/driver/test_apps/mcpwm/CMakeLists.txt @@ -0,0 +1,18 @@ +# This is the project CMakeLists.txt file for the test subproject +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(mcpwm_test) + +if(CONFIG_COMPILER_DUMP_RTL_FILES) + add_custom_target(check_test_app_sections ALL + COMMAND ${PYTHON} $ENV{IDF_PATH}/tools/ci/check_callgraph.py + --rtl-dir ${CMAKE_BINARY_DIR}/esp-idf/driver/ + --elf-file ${CMAKE_BINARY_DIR}/mcpwm_test.elf + find-refs + --from-sections=.iram0.text + --to-sections=.flash.text,.flash.rodata + --exit-code + DEPENDS ${elf} + ) +endif() diff --git a/components/driver/test_apps/mcpwm/README.md b/components/driver/test_apps/mcpwm/README.md new file mode 100644 index 0000000000..5ab630aafd --- /dev/null +++ b/components/driver/test_apps/mcpwm/README.md @@ -0,0 +1,2 @@ +| Supported Targets | ESP32 | ESP32-S3 | +| ----------------- | ----- | -------- | diff --git a/components/driver/test_apps/mcpwm/main/CMakeLists.txt b/components/driver/test_apps/mcpwm/main/CMakeLists.txt new file mode 100644 index 0000000000..a6eebbfd72 --- /dev/null +++ b/components/driver/test_apps/mcpwm/main/CMakeLists.txt @@ -0,0 +1,14 @@ +set(srcs "test_app_main.c" + "test_mcpwm_cap.c" + "test_mcpwm_cmpr.c" + "test_mcpwm_fault.c" + "test_mcpwm_gen.c" + "test_mcpwm_oper.c" + "test_mcpwm_sync.c" + "test_mcpwm_timer.c" + "test_mcpwm_utils.c") + +# In order for the cases defined by `TEST_CASE` to be linked into the final elf, +# the component can be registered as WHOLE_ARCHIVE +idf_component_register(SRCS ${srcs} + WHOLE_ARCHIVE) diff --git a/components/driver/test_apps/mcpwm/main/test_app_main.c b/components/driver/test_apps/mcpwm/main/test_app_main.c new file mode 100644 index 0000000000..6f449a0046 --- /dev/null +++ b/components/driver/test_apps/mcpwm/main/test_app_main.c @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "unity.h" +#include "unity_test_runner.h" +#include "esp_heap_caps.h" + +// Some resources are lazy allocated in GPTimer driver, the threshold is left for that case +#define TEST_MEMORY_LEAK_THRESHOLD (-300) + +static size_t before_free_8bit; +static size_t before_free_32bit; + +static void check_leak(size_t before_free, size_t after_free, const char *type) +{ + ssize_t delta = after_free - before_free; + printf("MALLOC_CAP_%s: Before %u bytes free, After %u bytes free (delta %d)\n", type, before_free, after_free, delta); + TEST_ASSERT_MESSAGE(delta >= TEST_MEMORY_LEAK_THRESHOLD, "memory leak"); +} + +void setUp(void) +{ + before_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT); + before_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT); +} + +void tearDown(void) +{ + size_t after_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT); + size_t after_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT); + check_leak(before_free_8bit, after_free_8bit, "8BIT"); + check_leak(before_free_32bit, after_free_32bit, "32BIT"); +} + +void app_main(void) +{ + // __ __ ____ ______ ____ __ _____ _ + // | \/ |/ ___| _ \ \ / / \/ | |_ _|__ ___| |_ + // | |\/| | | | |_) \ \ /\ / /| |\/| | | |/ _ \/ __| __| + // | | | | |___| __/ \ V V / | | | | | | __/\__ \ |_ + // |_| |_|\____|_| \_/\_/ |_| |_| |_|\___||___/\__| + printf(" __ __ ____ ______ ____ __ _____ _\r\n"); + printf("| \\/ |/ ___| _ \\ \\ / / \\/ | |_ _|__ ___| |_\r\n"); + printf("| |\\/| | | | |_) \\ \\ /\\ / /| |\\/| | | |/ _ \\/ __| __|\r\n"); + printf("| | | | |___| __/ \\ V V / | | | | | | __/\\__ \\ |_\r\n"); + printf("|_| |_|\\____|_| \\_/\\_/ |_| |_| |_|\\___||___/\\__|\r\n"); + unity_run_menu(); +} diff --git a/components/driver/test_apps/mcpwm/main/test_mcpwm_cap.c b/components/driver/test_apps/mcpwm/main/test_mcpwm_cap.c new file mode 100644 index 0000000000..2a6dd37943 --- /dev/null +++ b/components/driver/test_apps/mcpwm/main/test_mcpwm_cap.c @@ -0,0 +1,238 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/event_groups.h" +#include "unity.h" +#include "soc/soc_caps.h" +#include "esp_private/esp_clk.h" +#include "driver/mcpwm_cap.h" +#include "driver/mcpwm_sync.h" +#include "driver/gpio.h" +#include "test_mcpwm_utils.h" + +TEST_CASE("mcpwm_capture_install_uninstall", "[mcpwm]") +{ + printf("install mcpwm capture timers\r\n"); + mcpwm_capture_timer_config_t cap_timer_config = { + .clk_src = MCPWM_CAPTURE_CLK_SRC_DEFAULT, + }; + int total_cap_timers = SOC_MCPWM_GROUPS * SOC_MCPWM_CAPTURE_TIMERS_PER_GROUP; + mcpwm_cap_timer_handle_t cap_timers[total_cap_timers]; + int k = 0; + for (int i = 0; i < SOC_MCPWM_GROUPS; i++) { + cap_timer_config.group_id = i; + for (int j = 0; j < SOC_MCPWM_CAPTURE_TIMERS_PER_GROUP; j++) { + TEST_ESP_OK(mcpwm_new_capture_timer(&cap_timer_config, &cap_timers[k++])); + } + TEST_ESP_ERR(ESP_ERR_NOT_FOUND, mcpwm_new_capture_timer(&cap_timer_config, &cap_timers[0])); + } + + printf("install mcpwm capture channels\r\n"); + mcpwm_capture_channel_config_t cap_chan_config = { + .gpio_num = 0, + .prescale = 2, + .flags.pos_edge = true, + .flags.pull_up = true, + }; + mcpwm_cap_channel_handle_t cap_channels[total_cap_timers][SOC_MCPWM_CAPTURE_CHANNELS_PER_TIMER]; + for (int i = 0; i < total_cap_timers; i++) { + for (int j = 0; j < SOC_MCPWM_CAPTURE_CHANNELS_PER_TIMER; j++) { + TEST_ESP_OK(mcpwm_new_capture_channel(cap_timers[i], &cap_chan_config, &cap_channels[i][j])); + } + TEST_ESP_ERR(ESP_ERR_NOT_FOUND, mcpwm_new_capture_channel(cap_timers[i], &cap_chan_config, &cap_channels[i][0])); + } + + printf("uninstall mcpwm capture channels and timers\r\n"); + for (int i = 0; i < total_cap_timers; i++) { + for (int j = 0; j < SOC_MCPWM_CAPTURE_CHANNELS_PER_TIMER; j++) { + TEST_ESP_OK(mcpwm_del_capture_channel(cap_channels[i][j])); + } + TEST_ESP_OK(mcpwm_del_capture_timer(cap_timers[i])); + } +} + +TEST_MCPWM_CALLBACK_ATTR +static bool test_capture_callback(mcpwm_cap_channel_handle_t cap_channel, const mcpwm_capture_event_data_t *edata, void *user_data) +{ + uint32_t *cap_value = (uint32_t *)user_data; + if (edata->cap_edge == MCPWM_CAP_EDGE_NEG) { + cap_value[1] = edata->cap_value; + } else { + cap_value[0] = edata->cap_value; + } + return false; +} + +TEST_CASE("mcpwm_capture_ext_gpio", "[mcpwm]") +{ + printf("install mcpwm capture timer\r\n"); + mcpwm_cap_timer_handle_t cap_timer = NULL; + mcpwm_capture_timer_config_t cap_timer_config = { + .clk_src = MCPWM_CAPTURE_CLK_SRC_APB, + .group_id = 0, + }; + TEST_ESP_OK(mcpwm_new_capture_timer(&cap_timer_config, &cap_timer)); + + const int cap_gpio = 0; + // put the GPIO into a preset state + gpio_set_level(cap_gpio, 0); + + printf("install mcpwm capture channel\r\n"); + mcpwm_cap_channel_handle_t pps_channel; + mcpwm_capture_channel_config_t cap_chan_config = { + .gpio_num = cap_gpio, + .prescale = 1, + .flags.pos_edge = true, + .flags.neg_edge = true, + .flags.io_loop_back = true, // so we can use GPIO functions to simulate the external capture signal + .flags.pull_up = true, + }; + TEST_ESP_OK(mcpwm_new_capture_channel(cap_timer, &cap_chan_config, &pps_channel)); + + printf("install callback for capture channel\r\n"); + mcpwm_capture_event_callbacks_t cbs = { + .on_cap = test_capture_callback, + }; + uint32_t cap_value[2] = {0}; + TEST_ESP_OK(mcpwm_capture_channel_register_event_callbacks(pps_channel, &cbs, cap_value)); + + printf("enable and start capture timer\r\n"); + TEST_ESP_OK(mcpwm_capture_timer_enable(cap_timer)); + TEST_ESP_OK(mcpwm_capture_timer_start(cap_timer)); + + printf("simulate GPIO capture signal\r\n"); + gpio_set_level(cap_gpio, 1); + vTaskDelay(pdMS_TO_TICKS(100)); + gpio_set_level(cap_gpio, 0); + vTaskDelay(pdMS_TO_TICKS(100)); + printf("capture value: Pos=%u, Neg=%u\r\n", cap_value[0], cap_value[1]); + // Capture timer is clocked from APB by default + uint32_t clk_src_res = esp_clk_apb_freq(); + TEST_ASSERT_UINT_WITHIN(100000, clk_src_res / 10, cap_value[1] - cap_value[0]); + + printf("uninstall capture channel and timer\r\n"); + TEST_ESP_OK(mcpwm_del_capture_channel(pps_channel)); + TEST_ESP_OK(mcpwm_capture_timer_disable(cap_timer)); + TEST_ESP_OK(mcpwm_del_capture_timer(cap_timer)); +} + +typedef struct { + uint32_t cap_data[2]; + int cap_data_index; +} test_soft_catch_user_data_t; + +TEST_MCPWM_CALLBACK_ATTR +static bool soft_cap_callback(mcpwm_cap_channel_handle_t cap_channel, const mcpwm_capture_event_data_t *data, void *user_data) +{ + test_soft_catch_user_data_t *cbdata = (test_soft_catch_user_data_t *)user_data; + cbdata->cap_data[cbdata->cap_data_index++] = data->cap_value; + return false; +} + +TEST_CASE("mcpwm_capture_software_catch", "[mcpwm]") +{ + printf("install mcpwm capture timer\r\n"); + mcpwm_cap_timer_handle_t cap_timer = NULL; + mcpwm_capture_timer_config_t cap_timer_config = { + .clk_src = MCPWM_CAPTURE_CLK_SRC_DEFAULT, + .group_id = 0, + }; + TEST_ESP_OK(mcpwm_new_capture_timer(&cap_timer_config, &cap_timer)); + + printf("install mcpwm capture channel\r\n"); + mcpwm_cap_channel_handle_t cap_channel = NULL; + mcpwm_capture_channel_config_t cap_chan_config = { + .gpio_num = -1, // don't need any GPIO, we use software to trigger a catch + .prescale = 2, + }; + test_soft_catch_user_data_t test_callback_data = {}; + TEST_ESP_OK(mcpwm_new_capture_channel(cap_timer, &cap_chan_config, &cap_channel)); + + printf("register event callback for capture channel\r\n"); + mcpwm_capture_event_callbacks_t cbs = { + .on_cap = soft_cap_callback, + }; + TEST_ESP_OK(mcpwm_capture_channel_register_event_callbacks(cap_channel, &cbs, &test_callback_data)); + + printf("enable and start capture timer\r\n"); + TEST_ESP_OK(mcpwm_capture_timer_enable(cap_timer)); + TEST_ESP_OK(mcpwm_capture_timer_start(cap_timer)); + + printf("trigger software catch\r\n"); + TEST_ESP_OK(mcpwm_capture_channel_trigger_soft_catch(cap_channel)); + vTaskDelay(pdMS_TO_TICKS(10)); + TEST_ESP_OK(mcpwm_capture_channel_trigger_soft_catch(cap_channel)); + vTaskDelay(pdMS_TO_TICKS(10)); + + // check user data + TEST_ASSERT_EQUAL(2, test_callback_data.cap_data_index); + uint32_t delta = test_callback_data.cap_data[1] - test_callback_data.cap_data[0]; + esp_rom_printf("duration=%u ticks\r\n", delta); + // Capture timer is clocked from APB by default + uint32_t clk_src_res = esp_clk_apb_freq(); + TEST_ASSERT_UINT_WITHIN(80000, clk_src_res / 100, delta); + + printf("uninstall capture channel and timer\r\n"); + TEST_ESP_OK(mcpwm_capture_timer_disable(cap_timer)); + TEST_ESP_OK(mcpwm_del_capture_channel(cap_channel)); + TEST_ESP_OK(mcpwm_del_capture_timer(cap_timer)); +} + +TEST_MCPWM_CALLBACK_ATTR +static bool test_capture_after_sync_callback(mcpwm_cap_channel_handle_t cap_channel, const mcpwm_capture_event_data_t *data, void *user_data) +{ + uint32_t *cap_data = (uint32_t *)user_data; + *cap_data = data->cap_value; + return false; +} + +TEST_CASE("mcpwm_capture_timer_sync_phase_lock", "[mcpwm]") +{ + mcpwm_capture_timer_config_t cap_timer_config = { + .group_id = 0, + .clk_src = MCPWM_CAPTURE_CLK_SRC_DEFAULT, + }; + mcpwm_cap_timer_handle_t cap_timer = NULL; + TEST_ESP_OK(mcpwm_new_capture_timer(&cap_timer_config, &cap_timer)); + + mcpwm_sync_handle_t soft_sync = NULL; + mcpwm_soft_sync_config_t soft_sync_config = {}; + TEST_ESP_OK(mcpwm_new_soft_sync_src(&soft_sync_config, &soft_sync)); + + mcpwm_capture_timer_sync_phase_config_t sync_config = { + .count_value = 1000, + .direction = MCPWM_TIMER_DIRECTION_UP, + .sync_src = soft_sync, + }; + TEST_ESP_OK(mcpwm_capture_timer_set_phase_on_sync(cap_timer, &sync_config)); + mcpwm_cap_channel_handle_t cap_channel = NULL; + mcpwm_capture_channel_config_t cap_chan_config = { + .gpio_num = -1, // don't need any GPIO, we use software to trigger a catch + .prescale = 1, + }; + TEST_ESP_OK(mcpwm_new_capture_channel(cap_timer, &cap_chan_config, &cap_channel)); + + mcpwm_capture_event_callbacks_t cbs = { + .on_cap = test_capture_after_sync_callback, + }; + uint32_t cap_data; + TEST_ESP_OK(mcpwm_capture_channel_register_event_callbacks(cap_channel, &cbs, &cap_data)); + + TEST_ESP_OK(mcpwm_capture_channel_trigger_soft_catch(cap_channel)); + vTaskDelay(pdMS_TO_TICKS(10)); + printf("capture data before sync: %u\r\n", cap_data); + + TEST_ESP_OK(mcpwm_soft_sync_activate(soft_sync)); + TEST_ESP_OK(mcpwm_capture_channel_trigger_soft_catch(cap_channel)); + vTaskDelay(pdMS_TO_TICKS(10)); + printf("capture data after sync: %u\r\n", cap_data); + TEST_ASSERT_EQUAL(1000, cap_data); + TEST_ESP_OK(mcpwm_del_capture_channel(cap_channel)); + TEST_ESP_OK(mcpwm_del_capture_timer(cap_timer)); + TEST_ESP_OK(mcpwm_del_sync_src(soft_sync)); +} diff --git a/components/driver/test_apps/mcpwm/main/test_mcpwm_cmpr.c b/components/driver/test_apps/mcpwm/main/test_mcpwm_cmpr.c new file mode 100644 index 0000000000..5e0404d18b --- /dev/null +++ b/components/driver/test_apps/mcpwm/main/test_mcpwm_cmpr.c @@ -0,0 +1,113 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "unity.h" +#include "soc/soc_caps.h" +#include "driver/mcpwm_timer.h" +#include "driver/mcpwm_oper.h" +#include "driver/mcpwm_cmpr.h" + +TEST_CASE("mcpwm_comparator_install_uninstall", "[mcpwm]") +{ + mcpwm_timer_handle_t timer; + mcpwm_oper_handle_t operator; + mcpwm_cmpr_handle_t comparators[SOC_MCPWM_COMPARATORS_PER_OPERATOR]; + + mcpwm_timer_config_t timer_config = { + .group_id = 0, + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .resolution_hz = 1 * 1000 * 1000, + .period_ticks = 10 * 1000, + .count_mode = MCPWM_TIMER_COUNT_MODE_UP, + }; + mcpwm_operator_config_t operator_config = { + .group_id = 0, + }; + printf("install timer and operator"); + TEST_ESP_OK(mcpwm_new_timer(&timer_config, &timer)); + TEST_ESP_OK(mcpwm_new_operator(&operator_config, &operator)); + + printf("install comparator\r\n"); + mcpwm_comparator_config_t comparator_config = {}; + for (int i = 0; i < SOC_MCPWM_COMPARATORS_PER_OPERATOR; i++) { + TEST_ESP_OK(mcpwm_new_comparator(operator, &comparator_config, &comparators[i])); + } + TEST_ESP_ERR(ESP_ERR_NOT_FOUND, mcpwm_new_comparator(operator, &comparator_config, &comparators[0])); + + printf("connect MCPWM timer and operators\r\n"); + TEST_ESP_OK(mcpwm_operator_connect_timer(operator, timer)); + + printf("uninstall timer, operator and comparators\r\n"); + // can't delete operator if the comparators are still in working + TEST_ESP_ERR(ESP_ERR_INVALID_STATE, mcpwm_del_operator(operator)); + for (int i = 0; i < SOC_MCPWM_COMPARATORS_PER_OPERATOR; i++) { + TEST_ESP_OK(mcpwm_del_comparator(comparators[i])); + } + TEST_ESP_OK(mcpwm_del_operator(operator)); + TEST_ESP_OK(mcpwm_del_timer(timer)); +} + +static bool test_compare_on_reach(mcpwm_cmpr_handle_t cmpr, const mcpwm_compare_event_data_t *ev_data, void *user_data) +{ + uint32_t *counts = (uint32_t *)user_data; + (*counts)++; + return false; +} + +TEST_CASE("mcpwm_comparator_event_callback", "[mcpwm]") +{ + mcpwm_timer_handle_t timer; + mcpwm_oper_handle_t operator; + mcpwm_cmpr_handle_t comparator; + + mcpwm_timer_config_t timer_config = { + .group_id = 0, + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .resolution_hz = 1 * 1000 * 1000, + .period_ticks = 10 * 1000, // 10ms + .count_mode = MCPWM_TIMER_COUNT_MODE_UP, + }; + mcpwm_operator_config_t operator_config = { + .group_id = 0, + }; + mcpwm_comparator_config_t comparator_config = {}; + printf("install timer, operator and comparator"); + TEST_ESP_OK(mcpwm_new_timer(&timer_config, &timer)); + TEST_ESP_OK(mcpwm_new_operator(&operator_config, &operator)); + TEST_ESP_OK(mcpwm_new_comparator(operator, &comparator_config, &comparator)); + + // set compare value before connecting timer and operator will fail + TEST_ESP_ERR(ESP_ERR_INVALID_STATE, mcpwm_comparator_set_compare_value(comparator, 5000)); + printf("connect MCPWM timer and operators\r\n"); + TEST_ESP_OK(mcpwm_operator_connect_timer(operator, timer)); + // compare ticks can't exceed the timer's period ticks + TEST_ESP_ERR(ESP_ERR_INVALID_ARG, mcpwm_comparator_set_compare_value(comparator, 20 * 1000)); + TEST_ESP_OK(mcpwm_comparator_set_compare_value(comparator, 5 * 1000)); + + printf("register compare event callback\r\n"); + uint32_t compare_counts = 0; + mcpwm_comparator_event_callbacks_t cbs = { + .on_reach = test_compare_on_reach, + }; + TEST_ESP_OK(mcpwm_comparator_register_event_callbacks(comparator, &cbs, &compare_counts)); + + printf("start timer\r\n"); + TEST_ESP_OK(mcpwm_timer_enable(timer)); + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP)); + + vTaskDelay(pdMS_TO_TICKS(1000)); + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_STOP_EMPTY)); + printf("compare_counts=%u\r\n", compare_counts); + // the timer period is 10ms, the expected compare_counts = 1s/10ms = 100 + TEST_ASSERT_INT_WITHIN(1, 100, compare_counts); + + printf("uninstall timer, operator and comparator\r\n"); + TEST_ESP_OK(mcpwm_timer_disable(timer)); + TEST_ESP_OK(mcpwm_del_comparator(comparator)); + TEST_ESP_OK(mcpwm_del_operator(operator)); + TEST_ESP_OK(mcpwm_del_timer(timer)); +} diff --git a/components/driver/test_apps/mcpwm/main/test_mcpwm_fault.c b/components/driver/test_apps/mcpwm/main/test_mcpwm_fault.c new file mode 100644 index 0000000000..c516b34d0b --- /dev/null +++ b/components/driver/test_apps/mcpwm/main/test_mcpwm_fault.c @@ -0,0 +1,93 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "unity.h" +#include "driver/mcpwm_fault.h" +#include "driver/mcpwm_oper.h" +#include "driver/gpio.h" + +TEST_CASE("mcpwm_fault_install_uninstall", "[mcpwm]") +{ + printf("install and uninstall gpio faults\r\n"); + mcpwm_gpio_fault_config_t gpio_fault_config = { + .gpio_num = 0, + }; + int total_gpio_faults = SOC_MCPWM_GPIO_FAULTS_PER_GROUP * SOC_MCPWM_GROUPS; + mcpwm_fault_handle_t gpio_faults[total_gpio_faults]; + int fault_itor = 0; + for (int i = 0; i < SOC_MCPWM_GROUPS; i++) { + gpio_fault_config.group_id = i; + for (int j = 0; j < SOC_MCPWM_GPIO_FAULTS_PER_GROUP; j++) { + TEST_ESP_OK(mcpwm_new_gpio_fault(&gpio_fault_config, &gpio_faults[fault_itor++])); + } + TEST_ESP_ERR(ESP_ERR_NOT_FOUND, mcpwm_new_gpio_fault(&gpio_fault_config, &gpio_faults[0])); + } + for (int i = 0; i < total_gpio_faults; i++) { + TEST_ESP_OK(mcpwm_del_fault(gpio_faults[i])); + } + + printf("install and uninstall software fault\r\n"); + mcpwm_soft_fault_config_t soft_fault_config = {}; + mcpwm_fault_handle_t soft_fault = NULL; + TEST_ESP_OK(mcpwm_new_soft_fault(&soft_fault_config, &soft_fault)); + TEST_ESP_OK(mcpwm_del_fault(soft_fault)); +} + +static bool test_fault_enter_callback(mcpwm_fault_handle_t detector, const mcpwm_fault_event_data_t *status, void *user_data) +{ + TaskHandle_t task_handle = (TaskHandle_t)user_data; + BaseType_t high_task_wakeup = pdFALSE; + esp_rom_printf("fault found\r\n"); + vTaskNotifyGiveFromISR(task_handle, &high_task_wakeup); + return high_task_wakeup == pdTRUE; +} + +static bool test_fault_exit_callback(mcpwm_fault_handle_t detector, const mcpwm_fault_event_data_t *status, void *user_data) +{ + TaskHandle_t task_handle = (TaskHandle_t)user_data; + BaseType_t high_task_wakeup = pdFALSE; + esp_rom_printf("fault relieved\r\n"); + vTaskNotifyGiveFromISR(task_handle, &high_task_wakeup); + return high_task_wakeup == pdTRUE; +} + +TEST_CASE("mcpwm_gpio_fault_event_callbacks", "[mcpwm]") +{ + printf("create gpio fault\r\n"); + const int fault_gpio = 0; + mcpwm_fault_handle_t fault = NULL; + mcpwm_gpio_fault_config_t gpio_fault_config = { + .group_id = 0, + .gpio_num = fault_gpio, + .flags.active_level = true, // active on high level + .flags.pull_down = true, + .flags.io_loop_back = true, // for debug, so that we can use gpio_set_level to mimic a fault source + }; + TEST_ESP_OK(mcpwm_new_gpio_fault(&gpio_fault_config, &fault)); + + // put fault GPIO into a safe state + gpio_set_level(fault_gpio, 0); + + printf("register callback for the gpio fault\r\n"); + mcpwm_fault_event_callbacks_t cbs = { + .on_fault_enter = test_fault_enter_callback, + .on_fault_exit = test_fault_exit_callback, + }; + TaskHandle_t task_to_notify = xTaskGetCurrentTaskHandle(); + TEST_ESP_OK(mcpwm_fault_register_event_callbacks(fault, &cbs, task_to_notify)); + TEST_ASSERT_EQUAL(0, ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(1000))); + + printf("trigget a fault event\r\n"); + gpio_set_level(fault_gpio, 1); + TEST_ASSERT_NOT_EQUAL(0, ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(10))); + + printf("remove the fault source\r\n"); + gpio_set_level(fault_gpio, 0); + TEST_ASSERT_NOT_EQUAL(0, ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(10))); + + TEST_ESP_OK(mcpwm_del_fault(fault)); +} diff --git a/components/driver/test_apps/mcpwm/main/test_mcpwm_gen.c b/components/driver/test_apps/mcpwm/main/test_mcpwm_gen.c new file mode 100644 index 0000000000..1d397bd0e2 --- /dev/null +++ b/components/driver/test_apps/mcpwm/main/test_mcpwm_gen.c @@ -0,0 +1,646 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "unity.h" +#include "soc/soc_caps.h" +#include "driver/mcpwm_timer.h" +#include "driver/mcpwm_oper.h" +#include "driver/mcpwm_cmpr.h" +#include "driver/mcpwm_gen.h" +#include "driver/gpio.h" + +TEST_CASE("mcpwm_generator_install_uninstall", "[mcpwm]") +{ + mcpwm_operator_config_t oper_config = { + .group_id = 0, + }; + mcpwm_oper_handle_t oper = NULL; + printf("create a MCPWM operator\r\n"); + TEST_ESP_OK(mcpwm_new_operator(&oper_config, &oper)); + + printf("create MCPWM generators from that operator\r\n"); + mcpwm_gen_handle_t gens[SOC_MCPWM_GENERATORS_PER_OPERATOR]; + mcpwm_generator_config_t gen_config = { + .gen_gpio_num = 0, + }; + for (int i = 0; i < SOC_MCPWM_GENERATORS_PER_OPERATOR; i++) { + TEST_ESP_OK(mcpwm_new_generator(oper, &gen_config, &gens[i])); + } + TEST_ESP_ERR(ESP_ERR_NOT_FOUND, mcpwm_new_generator(oper, &gen_config, &gens[0])); + + printf("delete generators and operator\r\n"); + // can't delete operator if the generator is till in working + TEST_ESP_ERR(ESP_ERR_INVALID_STATE, mcpwm_del_operator(oper)); + for (int i = 0; i < SOC_MCPWM_GENERATORS_PER_OPERATOR; i++) { + TEST_ESP_OK(mcpwm_del_generator(gens[i])); + } + TEST_ESP_OK(mcpwm_del_operator(oper)); +} + +TEST_CASE("mcpwm_generator_force_level_hold_on", "[mcpwm]") +{ + // The operator can even work without the timer + printf("create operator and generator\r\n"); + mcpwm_oper_handle_t operator = NULL; + mcpwm_operator_config_t operator_config = { + .group_id = 0, + }; + TEST_ESP_OK(mcpwm_new_operator(&operator_config, &operator)); + + mcpwm_gen_handle_t generator = NULL; + const int gen_gpio = 0; + mcpwm_generator_config_t generator_config = { + .gen_gpio_num = gen_gpio, + .flags.io_loop_back = true, // loop back for test + }; + TEST_ESP_OK(mcpwm_new_generator(operator, &generator_config, &generator)); + + printf("add force level to the generator, hold on"); + for (int i = 0; i < 10; i++) { + TEST_ESP_OK(mcpwm_generator_set_force_level(generator, 0, true)); + vTaskDelay(pdMS_TO_TICKS(10)); + TEST_ASSERT_EQUAL(0, gpio_get_level(gen_gpio)); + TEST_ESP_OK(mcpwm_generator_set_force_level(generator, 1, true)); + vTaskDelay(pdMS_TO_TICKS(10)); + TEST_ASSERT_EQUAL(1, gpio_get_level(gen_gpio)); + } + + printf("remove the force level\r\n"); + TEST_ESP_OK(mcpwm_generator_set_force_level(generator, -1, true)); + + printf("delete generator and operator\r\n"); + TEST_ESP_OK(mcpwm_del_generator(generator)); + TEST_ESP_OK(mcpwm_del_operator(operator)); +} + +TEST_CASE("mcpwm_generator_force_level_recovery", "[mcpwm]") +{ + printf("create mcpwm timer\r\n"); + mcpwm_timer_config_t timer_config = { + .group_id = 0, + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .count_mode = MCPWM_TIMER_COUNT_MODE_UP, + .resolution_hz = 1000000, + .period_ticks = 50000, + }; + mcpwm_timer_handle_t timer = NULL; + TEST_ESP_OK(mcpwm_new_timer(&timer_config, &timer)); + TEST_ESP_OK(mcpwm_timer_enable(timer)); + + printf("create operator\r\n"); + mcpwm_oper_handle_t operator = NULL; + mcpwm_operator_config_t operator_config = { + .group_id = 0, + .flags.update_gen_action_on_tez = true, + }; + TEST_ESP_OK(mcpwm_new_operator(&operator_config, &operator)); + TEST_ESP_OK(mcpwm_operator_connect_timer(operator, timer)); + + printf("create generator\r\n"); + mcpwm_gen_handle_t generator = NULL; + const int gen_gpio = 0; + mcpwm_generator_config_t generator_config = { + .gen_gpio_num = gen_gpio, + .flags.io_loop_back = true, // loop back for test + }; + TEST_ESP_OK(mcpwm_new_generator(operator, &generator_config, &generator)); + + printf("add force level to the generator, and recovery by events"); + TEST_ESP_OK(mcpwm_generator_set_force_level(generator, 0, false)); + TEST_ASSERT_EQUAL(0, gpio_get_level(gen_gpio)); + TEST_ESP_OK(mcpwm_generator_set_force_level(generator, 1, false)); + TEST_ASSERT_EQUAL(1, gpio_get_level(gen_gpio)); + + TEST_ESP_OK(mcpwm_generator_set_force_level(generator, 0, false)); + TEST_ASSERT_EQUAL(0, gpio_get_level(gen_gpio)); + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(generator, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + // generator should output high level on tez event, the previous force level should disappear + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP)); + vTaskDelay(pdMS_TO_TICKS(200)); + TEST_ASSERT_EQUAL(1, gpio_get_level(gen_gpio)); + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_STOP_EMPTY)); + vTaskDelay(pdMS_TO_TICKS(100)); + + TEST_ESP_OK(mcpwm_generator_set_force_level(generator, 1, false)); + TEST_ASSERT_EQUAL(1, gpio_get_level(gen_gpio)); + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(generator, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + // generator should output low level on tez event, the previous force level should disappear + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP)); + vTaskDelay(pdMS_TO_TICKS(200)); + TEST_ASSERT_EQUAL(0, gpio_get_level(gen_gpio)); + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_STOP_EMPTY)); + vTaskDelay(pdMS_TO_TICKS(100)); + + printf("delete generator, operator and timer\r\n"); + TEST_ESP_OK(mcpwm_timer_disable(timer)); + TEST_ESP_OK(mcpwm_del_generator(generator)); + TEST_ESP_OK(mcpwm_del_operator(operator)); + TEST_ESP_OK(mcpwm_del_timer(timer)); +} + +TEST_CASE("mcpwm_generator_action_on_timer_event", "[mcpwm]") +{ + const int generator_gpio = 0; + printf("create timer and operator\r\n"); + mcpwm_timer_config_t timer_config = { + .group_id = 0, + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .resolution_hz = 1000000, + .count_mode = MCPWM_TIMER_COUNT_MODE_UP, + .period_ticks = 1000, + }; + mcpwm_timer_handle_t timer = NULL; + TEST_ESP_OK(mcpwm_new_timer(&timer_config, &timer)); + TEST_ESP_OK(mcpwm_timer_enable(timer)); + + mcpwm_operator_config_t oper_config = { + .group_id = 0, + }; + mcpwm_oper_handle_t oper = NULL; + TEST_ESP_OK(mcpwm_new_operator(&oper_config, &oper)); + + printf("connect timer and operator\r\n"); + TEST_ESP_OK(mcpwm_operator_connect_timer(oper, timer)); + + printf("create generator\r\n"); + mcpwm_generator_config_t gen_config = { + .gen_gpio_num = generator_gpio, + .flags.io_loop_back = 1, // so that we can read the GPIO value by GPIO driver + }; + mcpwm_gen_handle_t gen = NULL; + TEST_ESP_OK(mcpwm_new_generator(oper, &gen_config, &gen)); + + printf("set generator to output high on timer full\r\n"); + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(gen, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_FULL, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_KEEP), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + printf("start timer\r\n"); + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP)); + vTaskDelay(pdMS_TO_TICKS(100)); + printf("stop timer on full\r\n"); + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_STOP_FULL)); + TEST_ASSERT_EQUAL(1, gpio_get_level(generator_gpio)); + + printf("set generator to output low on timer full\r\n"); + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(gen, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_FULL, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_KEEP), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + printf("start timer\r\n"); + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP)); + vTaskDelay(pdMS_TO_TICKS(100)); + printf("stop timer on full\r\n"); + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_STOP_FULL)); + TEST_ASSERT_EQUAL(0, gpio_get_level(generator_gpio)); + + printf("delete timer, operator, generator\r\n"); + TEST_ESP_OK(mcpwm_timer_disable(timer)); + TEST_ESP_OK(mcpwm_del_generator(gen)); + TEST_ESP_OK(mcpwm_del_operator(oper)); + TEST_ESP_OK(mcpwm_del_timer(timer)); +} + +typedef void (*set_gen_actions_cb_t)(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb); + +static void mcpwm_gen_action_test_template(uint32_t timer_resolution, uint32_t period, mcpwm_timer_count_mode_t count_mode, + uint32_t cmpa, uint32_t cmpb, int gpioa, int gpiob, set_gen_actions_cb_t set_generator_actions) +{ + mcpwm_timer_config_t timer_config = { + .group_id = 0, + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .count_mode = count_mode, + .resolution_hz = timer_resolution, + .period_ticks = period, + }; + mcpwm_timer_handle_t timer = NULL; + TEST_ESP_OK(mcpwm_new_timer(&timer_config, &timer)); + + mcpwm_operator_config_t operator_config = { + .group_id = 0, + }; + mcpwm_oper_handle_t operator = NULL; + TEST_ESP_OK(mcpwm_new_operator(&operator_config, &operator)); + TEST_ESP_OK(mcpwm_operator_connect_timer(operator, timer)); + + TEST_ESP_OK(mcpwm_timer_enable(timer)); + + mcpwm_cmpr_handle_t comparator_a = NULL; + mcpwm_cmpr_handle_t comparator_b = NULL; + mcpwm_comparator_config_t comparator_config = { + .flags.update_cmp_on_tez = true, + }; + TEST_ESP_OK(mcpwm_new_comparator(operator, &comparator_config, &comparator_a)); + TEST_ESP_OK(mcpwm_new_comparator(operator, &comparator_config, &comparator_b)); + TEST_ESP_OK(mcpwm_comparator_set_compare_value(comparator_a, cmpa)); + TEST_ESP_OK(mcpwm_comparator_set_compare_value(comparator_b, cmpb)); + + mcpwm_gen_handle_t generator_a = NULL; + mcpwm_gen_handle_t generator_b = NULL; + mcpwm_generator_config_t generator_config = { + .gen_gpio_num = gpioa, + }; + TEST_ESP_OK(mcpwm_new_generator(operator, &generator_config, &generator_a)); + generator_config.gen_gpio_num = gpiob; + TEST_ESP_OK(mcpwm_new_generator(operator, &generator_config, &generator_b)); + + set_generator_actions(generator_a, generator_b, comparator_a, comparator_b); + + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP)); + vTaskDelay(pdMS_TO_TICKS(100)); + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_STOP_EMPTY)); + vTaskDelay(pdMS_TO_TICKS(10)); + + TEST_ESP_OK(mcpwm_timer_disable(timer)); + TEST_ESP_OK(mcpwm_del_generator(generator_a)); + TEST_ESP_OK(mcpwm_del_generator(generator_b)); + TEST_ESP_OK(mcpwm_del_comparator(comparator_a)); + TEST_ESP_OK(mcpwm_del_comparator(comparator_b)); + TEST_ESP_OK(mcpwm_del_operator(operator)); + TEST_ESP_OK(mcpwm_del_timer(timer)); +} + +static void single_edge_active_high(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) +{ + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(gena, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(genb, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(genb, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpb, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); +} + +static void single_edge_active_low(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) +{ + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(gena, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_FULL, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(genb, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_FULL, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(genb, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpb, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); +} + +static void pulse_placement(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) +{ + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpb, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(genb, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_TOGGLE), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); +} + +static void dual_edge_active_low_asym(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) +{ + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_DOWN, cmpb, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(genb, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_DOWN, MCPWM_TIMER_EVENT_FULL, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); +} + +static void dual_edge_active_low_sym(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) +{ + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_DOWN, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(genb, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpb, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_DOWN, cmpb, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); +} + +static void dual_edge_complementary(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) +{ + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_DOWN, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(genb, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpb, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_DOWN, cmpb, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); +} + +TEST_CASE("mcpwm_generator_action_on_compare_event", "[mcpwm]") +{ + printf("[Asymmetric, SingleEdge, ActiveHigh]\r\n"); + // PWMA: high = [1->350], low = [351->499,0] + // PWMB: high = [1->200], low = [201->499,0] + mcpwm_gen_action_test_template(1000000, 500, MCPWM_TIMER_COUNT_MODE_UP, 350, 200, 0, 2, single_edge_active_high); + + printf("[Asymmetric, SingleEdge, ActiveLow]\r\n"); + // PWMA: low = [0->300], high = [301->499] + // PWMB: low = [0->150], high = [151->499] + mcpwm_gen_action_test_template(1000000, 500, MCPWM_TIMER_COUNT_MODE_UP, 300, 150, 0, 2, single_edge_active_low); + + printf("[Asymmetric, PulsePlacement]\r\n"); + // PWMA: low = [0->200], high = [201->400], low = [401->599] + // PWMB: high = [0->599], low = [0->599] + mcpwm_gen_action_test_template(1000000, 600, MCPWM_TIMER_COUNT_MODE_UP, 200, 400, 0, 2, pulse_placement); + + printf("[Asymmetric, DualEdge, ActiveLow]\r\n"); + // PWMA: low = [0->250], high = [251->599, 600->450], low = [451->1] + // PWMB: low = [0->599], low = [600->1] + mcpwm_gen_action_test_template(1000000, 1200, MCPWM_TIMER_COUNT_MODE_UP_DOWN, 250, 450, 0, 2, dual_edge_active_low_asym); + + printf("[Symmetric, DualEdge, ActiveLow]\r\n"); + // PWMA: low = [0->400], high = [401->599, 600->400], low = [399->1] + // PWMB: low = [0->500], high = [501->599, 600->500], low = [499->1] + mcpwm_gen_action_test_template(1000000, 1200, MCPWM_TIMER_COUNT_MODE_UP_DOWN, 400, 500, 0, 2, dual_edge_active_low_sym); + + printf("[Symmetric, DualEdge, Complementary]\r\n"); + // PWMA: low = [0->350], high = [351->599, 600->350], low = [349->1] + // PWMB: low = [0->400], high = [401->599, 600->400], low = [399->1] + mcpwm_gen_action_test_template(1000000, 1200, MCPWM_TIMER_COUNT_MODE_UP_DOWN, 350, 400, 0, 2, dual_edge_complementary); +} + +typedef void (*set_dead_time_cb_t)(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb); + +static void mcpwm_deadtime_test_template(uint32_t timer_resolution, uint32_t period, uint32_t cmpa, uint32_t cmpb, int gpioa, int gpiob, + set_gen_actions_cb_t set_generator_actions, set_dead_time_cb_t set_dead_time) +{ + mcpwm_timer_config_t timer_config = { + .group_id = 0, + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .resolution_hz = timer_resolution, + .period_ticks = period, + .count_mode = MCPWM_TIMER_COUNT_MODE_UP, + }; + mcpwm_timer_handle_t timer = NULL; + TEST_ESP_OK(mcpwm_new_timer(&timer_config, &timer)); + + mcpwm_operator_config_t operator_config = { + .group_id = 0, + }; + mcpwm_oper_handle_t operator = NULL; + TEST_ESP_OK(mcpwm_new_operator(&operator_config, &operator)); + TEST_ESP_OK(mcpwm_operator_connect_timer(operator, timer)); + + TEST_ESP_OK(mcpwm_timer_enable(timer)); + + mcpwm_cmpr_handle_t comparator_a = NULL; + mcpwm_cmpr_handle_t comparator_b = NULL; + mcpwm_comparator_config_t comparator_config = { + .flags.update_cmp_on_tez = true, + }; + TEST_ESP_OK(mcpwm_new_comparator(operator, &comparator_config, &comparator_a)); + TEST_ESP_OK(mcpwm_new_comparator(operator, &comparator_config, &comparator_b)); + TEST_ESP_OK(mcpwm_comparator_set_compare_value(comparator_a, cmpa)); + TEST_ESP_OK(mcpwm_comparator_set_compare_value(comparator_b, cmpb)); + + mcpwm_gen_handle_t generator_a = NULL; + mcpwm_gen_handle_t generator_b = NULL; + mcpwm_generator_config_t generator_config = { + .gen_gpio_num = gpioa, + }; + TEST_ESP_OK(mcpwm_new_generator(operator, &generator_config, &generator_a)); + generator_config.gen_gpio_num = gpiob; + TEST_ESP_OK(mcpwm_new_generator(operator, &generator_config, &generator_b)); + + set_generator_actions(generator_a, generator_b, comparator_a, comparator_b); + set_dead_time(generator_a, generator_b); + + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP)); + vTaskDelay(pdMS_TO_TICKS(100)); + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_STOP_EMPTY)); + vTaskDelay(pdMS_TO_TICKS(10)); + + TEST_ESP_OK(mcpwm_timer_disable(timer)); + TEST_ESP_OK(mcpwm_del_generator(generator_a)); + TEST_ESP_OK(mcpwm_del_generator(generator_b)); + TEST_ESP_OK(mcpwm_del_comparator(comparator_a)); + TEST_ESP_OK(mcpwm_del_comparator(comparator_b)); + TEST_ESP_OK(mcpwm_del_operator(operator)); + TEST_ESP_OK(mcpwm_del_timer(timer)); +} + +static void ahc_set_generator_actions(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) +{ + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(gena, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); +} + +static void ahc_set_dead_time(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb) +{ + mcpwm_dead_time_config_t dead_time_config = { + .posedge_delay_ticks = 50, + .negedge_delay_ticks = 0 + }; + TEST_ESP_OK(mcpwm_generator_set_dead_time(gena, gena, &dead_time_config)); + dead_time_config.posedge_delay_ticks = 0; + dead_time_config.negedge_delay_ticks = 100; + dead_time_config.flags.invert_output = true; + TEST_ESP_OK(mcpwm_generator_set_dead_time(gena, genb, &dead_time_config)); +} + +static void alc_set_generator_actions(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) +{ + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(gena, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); +} + +static void alc_set_dead_time(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb) +{ + mcpwm_dead_time_config_t dead_time_config = { + .posedge_delay_ticks = 50, + .negedge_delay_ticks = 0, + .flags.invert_output = true + }; + TEST_ESP_OK(mcpwm_generator_set_dead_time(gena, gena, &dead_time_config)); + dead_time_config.posedge_delay_ticks = 0; + dead_time_config.negedge_delay_ticks = 100; + dead_time_config.flags.invert_output = false; + TEST_ESP_OK(mcpwm_generator_set_dead_time(gena, genb, &dead_time_config)); +} + +static void ah_set_generator_actions(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) +{ + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(gena, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); +} + +static void ah_set_dead_time(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb) +{ + mcpwm_dead_time_config_t dead_time_config = { + .posedge_delay_ticks = 50, + .negedge_delay_ticks = 0, + }; + TEST_ESP_OK(mcpwm_generator_set_dead_time(gena, gena, &dead_time_config)); + dead_time_config.posedge_delay_ticks = 0; + dead_time_config.negedge_delay_ticks = 100; + TEST_ESP_OK(mcpwm_generator_set_dead_time(gena, genb, &dead_time_config)); +} + +static void al_set_generator_actions(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) +{ + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(gena, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); +} + +static void al_set_dead_time(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb) +{ + mcpwm_dead_time_config_t dead_time_config = { + .posedge_delay_ticks = 50, + .negedge_delay_ticks = 0, + .flags.invert_output = true + }; + TEST_ESP_OK(mcpwm_generator_set_dead_time(gena, gena, &dead_time_config)); + dead_time_config.posedge_delay_ticks = 0; + dead_time_config.negedge_delay_ticks = 100; + TEST_ESP_OK(mcpwm_generator_set_dead_time(gena, genb, &dead_time_config)); +} + +static void reda_only_set_generator_actions(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) +{ + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(gena, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(genb, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(genb, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpb, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); +} + +static void reda_only_set_dead_time(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb) +{ + mcpwm_dead_time_config_t dead_time_config = { + .posedge_delay_ticks = 50, + .negedge_delay_ticks = 0, + }; + // apply deadtime to generator_a + TEST_ESP_OK(mcpwm_generator_set_dead_time(gena, gena, &dead_time_config)); + // bypass deadtime module for generator_b + dead_time_config.posedge_delay_ticks = 0; + TEST_ESP_OK(mcpwm_generator_set_dead_time(genb, genb, &dead_time_config)); +} + +static void fedb_only_set_generator_actions(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) +{ + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(gena, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(genb, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(genb, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpb, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); +} + +static void fedb_only_set_dead_time(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb) +{ + mcpwm_dead_time_config_t dead_time_config = { + .posedge_delay_ticks = 0, + .negedge_delay_ticks = 0, + }; + // generator_a bypass the deadtime module (no delay) + TEST_ESP_OK(mcpwm_generator_set_dead_time(gena, gena, &dead_time_config)); + // apply dead time to generator_b + dead_time_config.negedge_delay_ticks = 50; + TEST_ESP_OK(mcpwm_generator_set_dead_time(genb, genb, &dead_time_config)); + +} + +static void redfedb_only_set_generator_actions(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) +{ + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(gena, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(genb, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_compare_event(genb, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpb, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); +} + +static void redfedb_only_set_dead_time(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb) +{ + mcpwm_dead_time_config_t dead_time_config = { + .posedge_delay_ticks = 0, + .negedge_delay_ticks = 0, + }; + // generator_a bypass the deadtime module (no delay) + TEST_ESP_OK(mcpwm_generator_set_dead_time(gena, gena, &dead_time_config)); + // apply dead time on both edge for generator_b + dead_time_config.negedge_delay_ticks = 50; + dead_time_config.posedge_delay_ticks = 50; + TEST_ESP_OK(mcpwm_generator_set_dead_time(genb, genb, &dead_time_config)); +} + +TEST_CASE("mcpwm_generator_deadtime_classical_configuration", "[mcpwm]") +{ + printf("Active High Complementary\r\n"); + mcpwm_deadtime_test_template(1000000, 600, 200, 400, 0, 2, ahc_set_generator_actions, ahc_set_dead_time); + + printf("Active Low Complementary\r\n"); + mcpwm_deadtime_test_template(1000000, 600, 200, 400, 0, 2, alc_set_generator_actions, alc_set_dead_time); + + printf("Active High\r\n"); + mcpwm_deadtime_test_template(1000000, 600, 200, 400, 0, 2, ah_set_generator_actions, ah_set_dead_time); + + printf("Active Low\r\n"); + mcpwm_deadtime_test_template(1000000, 600, 200, 400, 0, 2, al_set_generator_actions, al_set_dead_time); + + printf("RED on A, Bypass B\r\n"); + mcpwm_deadtime_test_template(1000000, 500, 350, 350, 0, 2, reda_only_set_generator_actions, reda_only_set_dead_time); + + printf("Bypass A, FED on B\r\n"); + mcpwm_deadtime_test_template(1000000, 500, 350, 350, 0, 2, fedb_only_set_generator_actions, fedb_only_set_dead_time); + + printf("Bypass A, RED + FED on B\r\n"); + mcpwm_deadtime_test_template(1000000, 500, 350, 350, 0, 2, redfedb_only_set_generator_actions, redfedb_only_set_dead_time); +} diff --git a/components/driver/test_apps/mcpwm/main/test_mcpwm_oper.c b/components/driver/test_apps/mcpwm/main/test_mcpwm_oper.c new file mode 100644 index 0000000000..6c013e705c --- /dev/null +++ b/components/driver/test_apps/mcpwm/main/test_mcpwm_oper.c @@ -0,0 +1,380 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "unity.h" +#include "soc/soc_caps.h" +#include "driver/mcpwm_oper.h" +#include "driver/mcpwm_timer.h" +#include "driver/mcpwm_gen.h" +#include "driver/mcpwm_fault.h" +#include "driver/gpio.h" + +TEST_CASE("mcpwm_operator_install_uninstall", "[mcpwm]") +{ + const int total_operators = SOC_MCPWM_OPERATORS_PER_GROUP * SOC_MCPWM_GROUPS; + mcpwm_timer_handle_t timers[SOC_MCPWM_GROUPS]; + mcpwm_oper_handle_t operators[total_operators]; + + mcpwm_timer_config_t timer_config = { + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .resolution_hz = 1 * 1000 * 1000, + .period_ticks = 10 * 1000, + .count_mode = MCPWM_TIMER_COUNT_MODE_UP, + }; + mcpwm_operator_config_t operator_config = { + }; + printf("install one MCPWM timer for each group\r\n"); + for (int i = 0; i < SOC_MCPWM_GROUPS; i++) { + timer_config.group_id = i; + TEST_ESP_OK(mcpwm_new_timer(&timer_config, &timers[i])); + } + printf("install MCPWM operators for each group\r\n"); + int k = 0; + for (int i = 0; i < SOC_MCPWM_GROUPS; i++) { + operator_config.group_id = i; + for (int j = 0; j < SOC_MCPWM_OPERATORS_PER_GROUP; j++) { + TEST_ESP_OK(mcpwm_new_operator(&operator_config, &operators[k++])); + } + TEST_ESP_ERR(ESP_ERR_NOT_FOUND, mcpwm_new_operator(&operator_config, &operators[0])); + } + printf("connect MCPWM timer and operators\r\n"); + k = 0; + for (int i = 0; i < SOC_MCPWM_GROUPS; i++) { + for (int j = 0; j < SOC_MCPWM_OPERATORS_PER_GROUP; j++) { + TEST_ESP_OK(mcpwm_operator_connect_timer(operators[k++], timers[i])); + } + } + TEST_ESP_ERR(ESP_ERR_INVALID_ARG, mcpwm_operator_connect_timer(operators[0], timers[1])); + printf("uninstall operators and timers\r\n"); + for (int i = 0; i < total_operators; i++) { + TEST_ESP_OK(mcpwm_del_operator(operators[i])); + } + for (int i = 0; i < SOC_MCPWM_GROUPS; i++) { + TEST_ESP_OK(mcpwm_del_timer(timers[i])); + } +} + +TEST_CASE("mcpwm_operator_carrier", "[mcpwm]") +{ + mcpwm_timer_config_t timer_config = { + .group_id = 0, + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .resolution_hz = 1000000, // 1MHz, 1us per tick + .period_ticks = 20000, + .count_mode = MCPWM_TIMER_COUNT_MODE_UP, + }; + mcpwm_timer_handle_t timer = NULL; + TEST_ESP_OK(mcpwm_new_timer(&timer_config, &timer)); + + mcpwm_operator_config_t operator_config = { + .group_id = 0, + }; + mcpwm_oper_handle_t operator = NULL; + TEST_ESP_OK(mcpwm_new_operator(&operator_config, &operator)); + TEST_ESP_OK(mcpwm_operator_connect_timer(operator, timer)); + + mcpwm_generator_config_t generator_config = { + .gen_gpio_num = 0, + }; + mcpwm_gen_handle_t generator = NULL; + TEST_ESP_OK(mcpwm_new_generator(operator, &generator_config, &generator)); + + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(generator, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_TOGGLE), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + + printf("add carrier to PWM wave\r\n"); + mcpwm_carrier_config_t carrier_config = { + .frequency_hz = 1000000, // 1MHz carrier + .duty_cycle = 0.5, + .first_pulse_duration_us = 10, + }; + TEST_ESP_OK(mcpwm_operator_apply_carrier(operator, &carrier_config)); + + TEST_ESP_OK(mcpwm_timer_enable(timer)); + + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP)); + vTaskDelay(pdMS_TO_TICKS(100)); + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_STOP_EMPTY)); + vTaskDelay(pdMS_TO_TICKS(100)); + + printf("remove carrier from PWM wave\r\n"); + carrier_config.frequency_hz = 0; + TEST_ESP_OK(mcpwm_operator_apply_carrier(operator, &carrier_config)); + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP)); + vTaskDelay(pdMS_TO_TICKS(200)); + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_STOP_EMPTY)); + vTaskDelay(pdMS_TO_TICKS(100)); + + TEST_ESP_OK(mcpwm_timer_disable(timer)); + TEST_ESP_OK(mcpwm_del_generator(generator)); + TEST_ESP_OK(mcpwm_del_operator(operator)); + TEST_ESP_OK(mcpwm_del_timer(timer)); +} + +static bool test_cbc_brake_on_gpio_fault_callback(mcpwm_oper_handle_t operator, const mcpwm_brake_event_data_t *edata, void *user_data) +{ + esp_rom_printf("cbc brake\r\n"); + return false; +} + +static bool test_ost_brake_on_gpio_fault_callback(mcpwm_oper_handle_t operator, const mcpwm_brake_event_data_t *edata, void *user_data) +{ + esp_rom_printf("ost brake\r\n"); + return false; +} + +TEST_CASE("mcpwm_operator_brake_on_gpio_fault", "[mcpwm]") +{ + printf("install timer\r\n"); + mcpwm_timer_config_t timer_config = { + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .group_id = 0, + .resolution_hz = 1000000, // 1MHz, 1us per tick + .period_ticks = 20000, + .count_mode = MCPWM_TIMER_COUNT_MODE_UP, + }; + mcpwm_timer_handle_t timer = NULL; + TEST_ESP_OK(mcpwm_new_timer(&timer_config, &timer)); + + printf("install operator\r\n"); + mcpwm_operator_config_t operator_config = { + .group_id = 0, + }; + mcpwm_oper_handle_t operator = NULL; + TEST_ESP_OK(mcpwm_new_operator(&operator_config, &operator)); + TEST_ESP_OK(mcpwm_operator_connect_timer(operator, timer)); + + printf("set brake event callbacks for operator\r\n"); + mcpwm_operator_event_callbacks_t cbs = { + .on_brake_cbc = test_cbc_brake_on_gpio_fault_callback, + .on_brake_ost = test_ost_brake_on_gpio_fault_callback, + }; + TEST_ESP_OK(mcpwm_operator_register_event_callbacks(operator, &cbs, NULL)); + + printf("install gpio fault\r\n"); + mcpwm_gpio_fault_config_t gpio_fault_config = { + .group_id = 0, + .flags.active_level = 1, + .flags.io_loop_back = true, + .flags.pull_down = true, + }; + mcpwm_fault_handle_t gpio_cbc_fault = NULL; + mcpwm_fault_handle_t gpio_ost_fault = NULL; + const int cbc_fault_gpio = 4; + const int ost_fault_gpio = 5; + + gpio_fault_config.gpio_num = cbc_fault_gpio; + TEST_ESP_OK(mcpwm_new_gpio_fault(&gpio_fault_config, &gpio_cbc_fault)); + gpio_fault_config.gpio_num = ost_fault_gpio; + TEST_ESP_OK(mcpwm_new_gpio_fault(&gpio_fault_config, &gpio_ost_fault)); + + // put fault GPIO into a safe state + gpio_set_level(cbc_fault_gpio, 0); + gpio_set_level(ost_fault_gpio, 0); + + printf("set brake mode on fault\r\n"); + mcpwm_brake_config_t brake_config = { + .fault = gpio_cbc_fault, + .brake_mode = MCPWM_OPER_BRAKE_MODE_CBC, + .flags.cbc_recover_on_tez = true, + }; + TEST_ESP_OK(mcpwm_operator_set_brake_on_fault(operator, &brake_config)); + brake_config.fault = gpio_ost_fault; + brake_config.brake_mode = MCPWM_OPER_BRAKE_MODE_OST; + TEST_ESP_OK(mcpwm_operator_set_brake_on_fault(operator, &brake_config)); + + printf("create generators\r\n"); + const int gen_a_gpio = 0; + const int gen_b_gpio = 2; + mcpwm_gen_handle_t gen_a = NULL; + mcpwm_gen_handle_t gen_b = NULL; + mcpwm_generator_config_t generator_config = { + .flags.io_loop_back = true, + }; + generator_config.gen_gpio_num = gen_a_gpio; + TEST_ESP_OK(mcpwm_new_generator(operator, &generator_config, &gen_a)); + generator_config.gen_gpio_num = gen_b_gpio; + TEST_ESP_OK(mcpwm_new_generator(operator, &generator_config, &gen_b)); + + printf("set generator actions on timer event\r\n"); + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(gen_a, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(gen_b, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + + printf("set generator actions on brake event\r\n"); + TEST_ESP_OK(mcpwm_generator_set_actions_on_brake_event(gen_a, + MCPWM_GEN_BRAKE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_OPER_BRAKE_MODE_CBC, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_BRAKE_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_brake_event(gen_b, + MCPWM_GEN_BRAKE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_OPER_BRAKE_MODE_OST, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_BRAKE_EVENT_ACTION_END())); + + printf("enable and start timer\r\n"); + TEST_ESP_OK(mcpwm_timer_enable(timer)); + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP)); + + printf("trigger GPIO fault signal, brake in CBC mode\r\n"); + for (int i = 0; i < 10; i++) { + gpio_set_level(cbc_fault_gpio, 1); + vTaskDelay(pdMS_TO_TICKS(10)); + TEST_ASSERT_EQUAL(1, gpio_get_level(gen_a_gpio)); + // remove the fault signal + gpio_set_level(cbc_fault_gpio, 0); + // recovery + TEST_ESP_OK(mcpwm_operator_recover_from_fault(operator, gpio_cbc_fault)); + vTaskDelay(pdMS_TO_TICKS(40)); + // should recovery automatically + TEST_ASSERT_EQUAL(0, gpio_get_level(gen_a_gpio)); + } + + printf("trigger GPIO fault signal, brake in OST mode\r\n"); + for (int i = 0; i < 10; i++) { + gpio_set_level(ost_fault_gpio, 1); + vTaskDelay(pdMS_TO_TICKS(10)); + TEST_ASSERT_EQUAL(1, gpio_get_level(gen_b_gpio)); + // can't recover because fault signal is still active + TEST_ESP_ERR(ESP_ERR_INVALID_STATE, mcpwm_operator_recover_from_fault(operator, gpio_ost_fault)); + // remove the fault signal + gpio_set_level(ost_fault_gpio, 0); + vTaskDelay(pdMS_TO_TICKS(40)); + // for ost brake, the generator can't recover before we manually recover it + TEST_ASSERT_EQUAL(1, gpio_get_level(gen_b_gpio)); + // now it's safe to recover the operator + TEST_ESP_OK(mcpwm_operator_recover_from_fault(operator, gpio_ost_fault)); + vTaskDelay(pdMS_TO_TICKS(40)); + // should recovery now + TEST_ASSERT_EQUAL(0, gpio_get_level(gen_b_gpio)); + } + + printf("delete all mcpwm objects\r\n"); + TEST_ESP_OK(mcpwm_timer_disable(timer)); + TEST_ESP_OK(mcpwm_del_fault(gpio_cbc_fault)); + TEST_ESP_OK(mcpwm_del_fault(gpio_ost_fault)); + TEST_ESP_OK(mcpwm_del_generator(gen_a)); + TEST_ESP_OK(mcpwm_del_generator(gen_b)); + TEST_ESP_OK(mcpwm_del_operator(operator)); + TEST_ESP_OK(mcpwm_del_timer(timer)); +} + +TEST_CASE("mcpwm_operator_brake_on_soft_fault", "[mcpwm]") +{ + printf("install timer\r\n"); + mcpwm_timer_config_t timer_config = { + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .group_id = 0, + .resolution_hz = 1000000, // 1MHz, 1us per tick + .period_ticks = 20000, + .count_mode = MCPWM_TIMER_COUNT_MODE_UP, + }; + mcpwm_timer_handle_t timer = NULL; + TEST_ESP_OK(mcpwm_new_timer(&timer_config, &timer)); + + printf("install operator\r\n"); + mcpwm_operator_config_t operator_config = { + .group_id = 0, + }; + mcpwm_oper_handle_t operator = NULL; + TEST_ESP_OK(mcpwm_new_operator(&operator_config, &operator)); + TEST_ESP_OK(mcpwm_operator_connect_timer(operator, timer)); + + printf("install soft fault\r\n"); + mcpwm_soft_fault_config_t soft_fault_config = {}; + mcpwm_fault_handle_t soft_fault = NULL; + TEST_ESP_OK(mcpwm_new_soft_fault(&soft_fault_config, &soft_fault)); + + printf("set brake mode on fault\r\n"); + mcpwm_brake_config_t brake_config = { + .fault = soft_fault, + .brake_mode = MCPWM_OPER_BRAKE_MODE_CBC, + .flags.cbc_recover_on_tez = true, + }; + TEST_ESP_OK(mcpwm_operator_set_brake_on_fault(operator, &brake_config)); + + printf("create generators\r\n"); + const int gen_a_gpio = 0; + const int gen_b_gpio = 2; + mcpwm_gen_handle_t gen_a = NULL; + mcpwm_gen_handle_t gen_b = NULL; + mcpwm_generator_config_t generator_config = { + .flags.io_loop_back = true, + }; + generator_config.gen_gpio_num = gen_a_gpio; + TEST_ESP_OK(mcpwm_new_generator(operator, &generator_config, &gen_a)); + generator_config.gen_gpio_num = gen_b_gpio; + TEST_ESP_OK(mcpwm_new_generator(operator, &generator_config, &gen_b)); + + printf("set generator actions on timer event\r\n"); + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(gen_a, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_timer_event(gen_b, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + + printf("set generator actions on brake event\r\n"); + TEST_ESP_OK(mcpwm_generator_set_actions_on_brake_event(gen_a, + MCPWM_GEN_BRAKE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_OPER_BRAKE_MODE_CBC, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_BRAKE_EVENT_ACTION_END())); + TEST_ESP_OK(mcpwm_generator_set_actions_on_brake_event(gen_b, + MCPWM_GEN_BRAKE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_OPER_BRAKE_MODE_OST, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_BRAKE_EVENT_ACTION_END())); + + printf("enable and start timer\r\n"); + TEST_ESP_OK(mcpwm_timer_enable(timer)); + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP)); + + printf("trigger soft fault signal, brake in CBC mode\r\n"); + for (int i = 0; i < 1; i++) { + // stop the timer, so the operator can't recover from fault automatically + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_STOP_EMPTY)); + vTaskDelay(pdMS_TO_TICKS(40)); + // check initial generator output + TEST_ASSERT_EQUAL(0, gpio_get_level(gen_a_gpio)); + TEST_ESP_OK(mcpwm_soft_fault_activate(soft_fault)); + // check generate output on fault event + TEST_ASSERT_EQUAL(1, gpio_get_level(gen_a_gpio)); + // start the timer, so that operator can recover at a specific event (e.g. tez) + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP)); + // recover on tez + TEST_ESP_OK(mcpwm_operator_recover_from_fault(operator, soft_fault)); + vTaskDelay(pdMS_TO_TICKS(40)); + // the generator output should be recoverd automatically + TEST_ASSERT_EQUAL(0, gpio_get_level(gen_a_gpio)); + } + + printf("change the brake mode to ost\r\n"); + brake_config.brake_mode = MCPWM_OPER_BRAKE_MODE_OST; + TEST_ESP_OK(mcpwm_operator_set_brake_on_fault(operator, &brake_config)); + + printf("trigger soft fault signal, brake in OST mode\r\n"); + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP)); + for (int i = 0; i < 10; i++) { + // check initial generator output + TEST_ASSERT_EQUAL(0, gpio_get_level(gen_b_gpio)); + TEST_ESP_OK(mcpwm_soft_fault_activate(soft_fault)); + TEST_ASSERT_EQUAL(1, gpio_get_level(gen_b_gpio)); + vTaskDelay(pdMS_TO_TICKS(40)); + // don't recover without a manual recover + TEST_ASSERT_EQUAL(1, gpio_get_level(gen_b_gpio)); + TEST_ESP_OK(mcpwm_operator_recover_from_fault(operator, soft_fault)); + vTaskDelay(pdMS_TO_TICKS(10)); + // should recovery now + TEST_ASSERT_EQUAL(0, gpio_get_level(gen_b_gpio)); + } + + printf("delete all mcpwm objects\r\n"); + TEST_ESP_OK(mcpwm_timer_disable(timer)); + TEST_ESP_OK(mcpwm_del_fault(soft_fault)); + TEST_ESP_OK(mcpwm_del_generator(gen_a)); + TEST_ESP_OK(mcpwm_del_generator(gen_b)); + TEST_ESP_OK(mcpwm_del_operator(operator)); + TEST_ESP_OK(mcpwm_del_timer(timer)); +} diff --git a/components/driver/test_apps/mcpwm/main/test_mcpwm_sync.c b/components/driver/test_apps/mcpwm/main/test_mcpwm_sync.c new file mode 100644 index 0000000000..27cd0f7f63 --- /dev/null +++ b/components/driver/test_apps/mcpwm/main/test_mcpwm_sync.c @@ -0,0 +1,204 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "unity.h" +#include "soc/soc_caps.h" +#include "driver/mcpwm_timer.h" +#include "driver/mcpwm_sync.h" +#include "driver/gpio.h" +#include "esp_private/mcpwm.h" +#include "test_mcpwm_utils.h" + +TEST_CASE("mcpwm_sync_source_install_uninstall", "[mcpwm]") +{ + printf("install timer sync_src\r\n"); + mcpwm_timer_config_t timer_config = { + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .resolution_hz = 1000000, // 1MHz + .period_ticks = 200, + .count_mode = MCPWM_TIMER_COUNT_MODE_UP, + }; + const int total_timers = SOC_MCPWM_TIMERS_PER_GROUP * SOC_MCPWM_GROUPS; + mcpwm_timer_handle_t timers[total_timers]; + int k = 0; + for (int i = 0; i < SOC_MCPWM_GROUPS; i++) { + timer_config.group_id = i; + for (int j = 0; j < SOC_MCPWM_TIMERS_PER_GROUP; j++) { + TEST_ESP_OK(mcpwm_new_timer(&timer_config, &timers[k++])); + } + } + mcpwm_timer_sync_src_config_t timer_sync_src_config = { + .timer_event = MCPWM_TIMER_EVENT_EMPTY, + }; + mcpwm_sync_handle_t timer_syncs[total_timers]; + for (int i = 0; i < total_timers; i++) { + TEST_ESP_OK(mcpwm_new_timer_sync_src(timers[i], &timer_sync_src_config, &timer_syncs[i])); + } + TEST_ESP_ERR(ESP_ERR_INVALID_STATE, mcpwm_new_timer_sync_src(timers[0], &timer_sync_src_config, &timer_syncs[0])); + + printf("install gpio sync_src\r\n"); + mcpwm_gpio_sync_src_config_t gpio_sync_config = { + .gpio_num = 0, + }; + const int total_gpio_sync_srcs = SOC_MCPWM_GROUPS * SOC_MCPWM_GPIO_SYNCHROS_PER_GROUP; + mcpwm_sync_handle_t gpio_sync_srcs[total_gpio_sync_srcs]; + k = 0; + for (int i = 0; i < SOC_MCPWM_GROUPS; i++) { + gpio_sync_config.group_id = i; + for (int j = 0; j < SOC_MCPWM_GPIO_SYNCHROS_PER_GROUP; j++) { + TEST_ESP_OK(mcpwm_new_gpio_sync_src(&gpio_sync_config, &gpio_sync_srcs[k++])); + } + } + TEST_ESP_ERR(ESP_ERR_NOT_FOUND, mcpwm_new_gpio_sync_src(&gpio_sync_config, &gpio_sync_srcs[0])); + + printf("delete synchors\r\n"); + for (int i = 0; i < total_gpio_sync_srcs; i++) { + TEST_ESP_OK(mcpwm_del_sync_src(gpio_sync_srcs[i])); + } + for (int i = 0; i < total_timers; i++) { + TEST_ESP_OK(mcpwm_del_sync_src(timer_syncs[i])); + TEST_ESP_OK(mcpwm_del_timer(timers[i])); + } +} + +TEST_CASE("mcpwm_soft_sync_timer_phase_lock", "[mcpwm]") +{ + mcpwm_timer_config_t timer_config = { + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .group_id = 0, + .resolution_hz = 1000000, // 1MHz + .period_ticks = 200, + .count_mode = MCPWM_TIMER_COUNT_MODE_UP_DOWN, + }; + mcpwm_timer_handle_t timer = NULL; + TEST_ESP_OK(mcpwm_new_timer(&timer_config, &timer)); + TEST_ESP_OK(mcpwm_timer_enable(timer)); + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_STOP_FULL)); + vTaskDelay(pdMS_TO_TICKS(10)); + check_mcpwm_timer_phase(&timer, 1, timer_config.period_ticks / 2, MCPWM_TIMER_DIRECTION_DOWN); + + printf("install soft sync source\r\n"); + mcpwm_sync_handle_t soft_sync = NULL; + mcpwm_soft_sync_config_t soft_sync_config = {}; + TEST_ESP_OK(mcpwm_new_soft_sync_src(&soft_sync_config, &soft_sync)); + + mcpwm_timer_sync_phase_config_t sync_phase_config = { + .count_value = 77, + .direction = MCPWM_TIMER_DIRECTION_UP, + .sync_src = soft_sync, + }; + TEST_ESP_OK(mcpwm_timer_set_phase_on_sync(timer, &sync_phase_config)); + TEST_ESP_OK(mcpwm_soft_sync_activate(soft_sync)); + check_mcpwm_timer_phase(&timer, 1, 77, MCPWM_TIMER_DIRECTION_UP); + + TEST_ESP_OK(mcpwm_timer_disable(timer)); + TEST_ESP_OK(mcpwm_del_timer(timer)); + TEST_ESP_OK(mcpwm_del_sync_src(soft_sync)); +} + +TEST_CASE("mcpwm_gpio_sync_timer_phase_lock", "[mcpwm]") +{ + // GPIO + // | + // v + // timer0-->timer1-->timer2 + mcpwm_timer_config_t timer_config = { + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .group_id = 0, + .resolution_hz = 1000000, // 1MHz, 1us per tick + .period_ticks = 500, + .count_mode = MCPWM_TIMER_COUNT_MODE_UP, + }; + mcpwm_timer_sync_src_config_t sync_config = { + .flags.propagate_input_sync = 1, // reuse the input sync source as the output sync trigger + }; + mcpwm_timer_handle_t timers[SOC_MCPWM_TIMERS_PER_GROUP]; + mcpwm_sync_handle_t sync_srcs[SOC_MCPWM_TIMERS_PER_GROUP]; + for (int i = 0; i < SOC_MCPWM_TIMERS_PER_GROUP; i++) { + TEST_ESP_OK(mcpwm_new_timer(&timer_config, &timers[i])); + TEST_ESP_OK(mcpwm_new_timer_sync_src(timers[i], &sync_config, &sync_srcs[i])); + } + mcpwm_timer_sync_phase_config_t sync_phase_config = { + .count_value = 100, + .direction = MCPWM_TIMER_DIRECTION_UP, + }; + mcpwm_sync_handle_t gpio_sync_src; + const int gpio_num = 0; + mcpwm_gpio_sync_src_config_t gpio_sync_config = { + .group_id = 0, + .gpio_num = gpio_num, + .flags.io_loop_back = true, // so that we can use gpio driver to simulate the sync signal + .flags.pull_down = true, // internally pull down + }; + TEST_ESP_OK(mcpwm_new_gpio_sync_src(&gpio_sync_config, &gpio_sync_src)); + // put the GPIO into initial state + gpio_set_level(gpio_num, 0); + for (int i = 1; i < SOC_MCPWM_TIMERS_PER_GROUP; i++) { + sync_phase_config.sync_src = sync_srcs[i - 1]; + TEST_ESP_OK(mcpwm_timer_set_phase_on_sync(timers[i], &sync_phase_config)); + } + sync_phase_config.sync_src = gpio_sync_src; + TEST_ESP_OK(mcpwm_timer_set_phase_on_sync(timers[0], &sync_phase_config)); + + // simulate an GPIO sync singal + gpio_set_level(gpio_num, 1); + gpio_set_level(gpio_num, 0); + check_mcpwm_timer_phase(timers, SOC_MCPWM_CAPTURE_TIMERS_PER_GROUP, 100, MCPWM_TIMER_DIRECTION_UP); + + TEST_ESP_OK(mcpwm_del_sync_src(gpio_sync_src)); + for (int i = 0; i < SOC_MCPWM_TIMERS_PER_GROUP; i++) { + TEST_ESP_OK(mcpwm_del_sync_src(sync_srcs[i])); + TEST_ESP_OK(mcpwm_del_timer(timers[i])); + } +} + +TEST_CASE("mcpwm_timer_sync_timer_phase_lock", "[mcpwm]") +{ + // +->timer1 + // | + // timer0---+ + // | + // +->timer2 + mcpwm_timer_config_t timer_config = { + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .group_id = 0, + .resolution_hz = 1000000, // 1MHz, 1us per tick + .period_ticks = 500, + .count_mode = MCPWM_TIMER_COUNT_MODE_UP_DOWN, + }; + mcpwm_timer_handle_t timers[SOC_MCPWM_TIMERS_PER_GROUP]; + for (int i = 0; i < SOC_MCPWM_TIMERS_PER_GROUP; i++) { + TEST_ESP_OK(mcpwm_new_timer(&timer_config, &timers[i])); + } + + mcpwm_timer_sync_src_config_t sync_config = { + .timer_event = MCPWM_TIMER_EVENT_FULL, + }; + mcpwm_sync_handle_t sync_src; + TEST_ESP_OK(mcpwm_new_timer_sync_src(timers[0], &sync_config, &sync_src)); + + mcpwm_timer_sync_phase_config_t sync_phase_config = { + .count_value = 50, + .direction = MCPWM_TIMER_DIRECTION_DOWN, + .sync_src = sync_src, + }; + for (int i = 1; i < SOC_MCPWM_TIMERS_PER_GROUP; i++) { + TEST_ESP_OK(mcpwm_timer_set_phase_on_sync(timers[i], &sync_phase_config)); + } + + TEST_ESP_OK(mcpwm_timer_enable(timers[0])); + TEST_ESP_OK(mcpwm_timer_start_stop(timers[0], MCPWM_TIMER_START_STOP_FULL)); + vTaskDelay(pdMS_TO_TICKS(10)); + + check_mcpwm_timer_phase(&timers[1], 2, 50, MCPWM_TIMER_DIRECTION_DOWN); + + TEST_ESP_OK(mcpwm_timer_disable(timers[0])); + TEST_ESP_OK(mcpwm_del_sync_src(sync_src)); + for (int i = 0; i < SOC_MCPWM_TIMERS_PER_GROUP; i++) { + TEST_ESP_OK(mcpwm_del_timer(timers[i])); + } +} diff --git a/components/driver/test_apps/mcpwm/main/test_mcpwm_timer.c b/components/driver/test_apps/mcpwm/main/test_mcpwm_timer.c new file mode 100644 index 0000000000..2c69fd7dbe --- /dev/null +++ b/components/driver/test_apps/mcpwm/main/test_mcpwm_timer.c @@ -0,0 +1,186 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/event_groups.h" +#include "unity.h" +#include "soc/soc_caps.h" +#include "driver/mcpwm_timer.h" +#include "esp_private/mcpwm.h" +#include "test_mcpwm_utils.h" + +TEST_CASE("mcpwm_timer_start_stop", "[mcpwm]") +{ + mcpwm_timer_config_t config = { + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .resolution_hz = 1000000, // 1MHz + .period_ticks = 400, + .count_mode = MCPWM_TIMER_COUNT_MODE_UP_DOWN, + }; + const int num_timers = SOC_MCPWM_TIMERS_PER_GROUP * SOC_MCPWM_GROUPS; + + printf("create mcpwm timer instances\r\n"); + mcpwm_timer_handle_t timers[num_timers]; + for (int i = 0; i < SOC_MCPWM_GROUPS; i++) { + for (int j = 0; j < SOC_MCPWM_TIMERS_PER_GROUP; j++) { + config.group_id = i; + TEST_ESP_OK(mcpwm_new_timer(&config, &timers[i * SOC_MCPWM_TIMERS_PER_GROUP + j])); + } + TEST_ESP_ERR(ESP_ERR_NOT_FOUND, mcpwm_new_timer(&config, &timers[0])); + } + + // can't do start/stop control before enable + TEST_ESP_ERR(ESP_ERR_INVALID_STATE, mcpwm_timer_start_stop(timers[0], MCPWM_TIMER_START_NO_STOP)); + + printf("enable timers\r\n"); + for (int i = 0; i < num_timers; i++) { + TEST_ESP_OK(mcpwm_timer_enable(timers[i])); + } + + printf("start timer and then stop when empty\r\n"); + for (int i = 0; i < num_timers; i++) { + TEST_ESP_OK(mcpwm_timer_start_stop(timers[i], MCPWM_TIMER_START_STOP_EMPTY)); + } + vTaskDelay(pdMS_TO_TICKS(10)); + check_mcpwm_timer_phase(timers, num_timers, 0, MCPWM_TIMER_DIRECTION_UP); + + printf("start timer and then stop when full\r\n"); + for (int i = 0; i < num_timers; i++) { + TEST_ESP_OK(mcpwm_timer_start_stop(timers[i], MCPWM_TIMER_START_STOP_FULL)); + } + vTaskDelay(pdMS_TO_TICKS(10)); + check_mcpwm_timer_phase(timers, num_timers, config.period_ticks / 2, MCPWM_TIMER_DIRECTION_DOWN); + + printf("start freely and stop manually when full\r\n"); + for (int i = 0; i < num_timers; i++) { + TEST_ESP_OK(mcpwm_timer_start_stop(timers[i], MCPWM_TIMER_START_NO_STOP)); + vTaskDelay(pdMS_TO_TICKS(10)); + // stop at next counter full + TEST_ESP_OK(mcpwm_timer_start_stop(timers[i], MCPWM_TIMER_STOP_FULL)); + vTaskDelay(pdMS_TO_TICKS(10)); + } + check_mcpwm_timer_phase(timers, num_timers, config.period_ticks / 2, MCPWM_TIMER_DIRECTION_DOWN); + + printf("start freely and stop manually when empty\r\n"); + for (int i = 0; i < num_timers; i++) { + TEST_ESP_OK(mcpwm_timer_start_stop(timers[i], MCPWM_TIMER_START_NO_STOP)); + vTaskDelay(pdMS_TO_TICKS(10)); + // stop at next counter empty + TEST_ESP_OK(mcpwm_timer_start_stop(timers[i], MCPWM_TIMER_STOP_EMPTY)); + vTaskDelay(pdMS_TO_TICKS(10)); + } + check_mcpwm_timer_phase(timers, num_timers, 0, MCPWM_TIMER_DIRECTION_UP); + + // can't delete timer before disable + TEST_ESP_ERR(ESP_ERR_INVALID_STATE, mcpwm_del_timer(timers[0])); + + printf("disable timers\r\n"); + for (int i = 0; i < num_timers; i++) { + TEST_ESP_OK(mcpwm_timer_disable(timers[i])); + } + + printf("delete timers\r\n"); + for (int i = 0; i < num_timers; i++) { + TEST_ESP_OK(mcpwm_del_timer(timers[i])); + } +} + +#define TEST_MCPWM_TIMER_EVENT_BIT_FULL (1 << 0) +#define TEST_MCPWM_TIMER_EVENT_BIT_EMPTY (1 << 1) +#define TEST_MCPWM_TIMER_EVENT_BIT_STOP (1 << 2) + +typedef struct { + EventGroupHandle_t event_group; + uint32_t expected_full_counts; + uint32_t expected_empty_counts; + uint32_t accumulate_full_counts; + uint32_t accumulate_empty_counts; +} test_mcpwm_timer_user_data_t; + +static bool test_on_stop(mcpwm_timer_handle_t timer, const mcpwm_timer_event_data_t *edata, void *user_data) +{ + test_mcpwm_timer_user_data_t *udata = (test_mcpwm_timer_user_data_t *)user_data; + BaseType_t high_task_wakeup = pdFALSE; + esp_rom_printf("timer stopped at %u\r\n", edata->count_value); + TEST_ASSERT_EQUAL(0, edata->count_value); + xEventGroupSetBitsFromISR(udata->event_group, TEST_MCPWM_TIMER_EVENT_BIT_STOP, &high_task_wakeup); + return high_task_wakeup == pdTRUE; +} + +static bool test_on_full(mcpwm_timer_handle_t timer, const mcpwm_timer_event_data_t *edata, void *user_data) +{ + test_mcpwm_timer_user_data_t *udata = (test_mcpwm_timer_user_data_t *)user_data; + BaseType_t high_task_wakeup = pdFALSE; + udata->accumulate_full_counts++; + if (udata->accumulate_full_counts >= udata->expected_full_counts) { + udata->accumulate_full_counts = 0; + xEventGroupSetBitsFromISR(udata->event_group, TEST_MCPWM_TIMER_EVENT_BIT_FULL, &high_task_wakeup); + } + return high_task_wakeup == pdTRUE; +} + +static bool test_on_empty(mcpwm_timer_handle_t timer, const mcpwm_timer_event_data_t *edata, void *user_data) +{ + test_mcpwm_timer_user_data_t *udata = (test_mcpwm_timer_user_data_t *)user_data; + BaseType_t high_task_wakeup = pdFALSE; + udata->accumulate_empty_counts++; + if (udata->accumulate_empty_counts >= udata->expected_empty_counts) { + udata->accumulate_empty_counts = 0; + xEventGroupSetBitsFromISR(udata->event_group, TEST_MCPWM_TIMER_EVENT_BIT_EMPTY, &high_task_wakeup); + } + return high_task_wakeup == pdTRUE; +} + +TEST_CASE("mcpwm_timer_event_callbacks", "[mcpwm]") +{ + EventGroupHandle_t event_group = xEventGroupCreate(); + EventBits_t bits = 0; + mcpwm_timer_config_t timer_config = { + .group_id = 0, + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .resolution_hz = 1 * 1000 * 1000, // 1MHz, 1us per tick + .period_ticks = 20 * 1000, // 20ms, 50Hz + .count_mode = MCPWM_TIMER_COUNT_MODE_UP, + }; + mcpwm_timer_handle_t timer = NULL; + printf("create mcpwm timer\r\n"); + TEST_ESP_OK(mcpwm_new_timer(&timer_config, &timer)); + + printf("register event callbacks\r\n"); + mcpwm_timer_event_callbacks_t cbs = { + .on_stop = test_on_stop, + .on_full = test_on_full, + .on_empty = test_on_empty, + }; + test_mcpwm_timer_user_data_t udata = { + .event_group = event_group, + .expected_empty_counts = 50, + .expected_full_counts = 50, + }; + TEST_ESP_OK(mcpwm_timer_register_event_callbacks(timer, &cbs, &udata)); + + printf("enable timer\r\n"); + TEST_ESP_OK(mcpwm_timer_enable(timer)); + + printf("start timer\r\n"); + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP)); + + printf("wait for full and empty events\r\n"); + bits = xEventGroupWaitBits(event_group, TEST_MCPWM_TIMER_EVENT_BIT_FULL | TEST_MCPWM_TIMER_EVENT_BIT_EMPTY, pdTRUE, pdTRUE, pdMS_TO_TICKS(1050)); + TEST_ASSERT_EQUAL(TEST_MCPWM_TIMER_EVENT_BIT_FULL | TEST_MCPWM_TIMER_EVENT_BIT_EMPTY, bits); + + printf("stop timer and wait for event\r\n"); + TEST_ESP_OK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_STOP_EMPTY)); + bits = xEventGroupWaitBits(event_group, TEST_MCPWM_TIMER_EVENT_BIT_STOP, pdTRUE, pdTRUE, pdMS_TO_TICKS(50)); + TEST_ASSERT_EQUAL(TEST_MCPWM_TIMER_EVENT_BIT_STOP, bits); + + printf("disable timer\r\n"); + TEST_ESP_OK(mcpwm_timer_disable(timer)); + + printf("delete timer\r\n"); + TEST_ESP_OK(mcpwm_del_timer(timer)); + vEventGroupDelete(event_group); +} diff --git a/components/driver/test_apps/mcpwm/main/test_mcpwm_utils.c b/components/driver/test_apps/mcpwm/main/test_mcpwm_utils.c new file mode 100644 index 0000000000..d1c933b62a --- /dev/null +++ b/components/driver/test_apps/mcpwm/main/test_mcpwm_utils.c @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "unity.h" +#include "esp_private/mcpwm.h" +#include "test_mcpwm_utils.h" + +void check_mcpwm_timer_phase(mcpwm_timer_handle_t *timers, size_t num_timers, + uint32_t expected_count, mcpwm_timer_direction_t expected_direction) +{ + uint32_t count_value; + mcpwm_timer_direction_t direction; + for (size_t i = 0; i < num_timers; i++) { + TEST_ESP_OK(mcpwm_timer_get_phase(timers[i], &count_value, &direction)); + TEST_ASSERT_INT_WITHIN(1, expected_count, count_value); + TEST_ASSERT_EQUAL(expected_direction, direction); + } +} diff --git a/components/driver/test_apps/mcpwm/main/test_mcpwm_utils.h b/components/driver/test_apps/mcpwm/main/test_mcpwm_utils.h new file mode 100644 index 0000000000..feb98a415f --- /dev/null +++ b/components/driver/test_apps/mcpwm/main/test_mcpwm_utils.h @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "sdkconfig.h" +#include "esp_attr.h" +#include "driver/mcpwm_types.h" + +#if CONFIG_MCPWM_ISR_IRAM_SAFE +#define TEST_MCPWM_CALLBACK_ATTR IRAM_ATTR +#else +#define TEST_MCPWM_CALLBACK_ATTR +#endif // CONFIG_MCPWM_ISR_IRAM_SAFE + +void check_mcpwm_timer_phase(mcpwm_timer_handle_t *timers, size_t num_timers, + uint32_t expected_count, mcpwm_timer_direction_t expected_direction); diff --git a/components/driver/test_apps/mcpwm/pytest_mcpwm.py b/components/driver/test_apps/mcpwm/pytest_mcpwm.py new file mode 100644 index 0000000000..6def6d2cf4 --- /dev/null +++ b/components/driver/test_apps/mcpwm/pytest_mcpwm.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded import Dut + + +@pytest.mark.esp32 +@pytest.mark.esp32s3 +@pytest.mark.generic +@pytest.mark.parametrize( + 'config', + [ + 'release', + 'iram_safe', + ], + indirect=True, +) +def test_mcpwm(dut: Dut) -> None: + dut.expect('Press ENTER to see the list of tests') + dut.write('*') + dut.expect_unity_test_output() diff --git a/components/driver/test_apps/mcpwm/sdkconfig.ci.iram_safe b/components/driver/test_apps/mcpwm/sdkconfig.ci.iram_safe new file mode 100644 index 0000000000..f5e68a8539 --- /dev/null +++ b/components/driver/test_apps/mcpwm/sdkconfig.ci.iram_safe @@ -0,0 +1,5 @@ +CONFIG_COMPILER_DUMP_RTL_FILES=y +CONFIG_MCPWM_ISR_IRAM_SAFE=y + +# silent the error check, as the error string are stored in rodata, causing RTL check failure +CONFIG_COMPILER_OPTIMIZATION_CHECKS_SILENT=y diff --git a/components/driver/test_apps/mcpwm/sdkconfig.ci.release b/components/driver/test_apps/mcpwm/sdkconfig.ci.release new file mode 100644 index 0000000000..91d93f163e --- /dev/null +++ b/components/driver/test_apps/mcpwm/sdkconfig.ci.release @@ -0,0 +1,5 @@ +CONFIG_PM_ENABLE=y +CONFIG_FREERTOS_USE_TICKLESS_IDLE=y +CONFIG_COMPILER_OPTIMIZATION_SIZE=y +CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y +CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT=y diff --git a/components/driver/test_apps/mcpwm/sdkconfig.defaults b/components/driver/test_apps/mcpwm/sdkconfig.defaults new file mode 100644 index 0000000000..b308cb2ddd --- /dev/null +++ b/components/driver/test_apps/mcpwm/sdkconfig.defaults @@ -0,0 +1,2 @@ +CONFIG_FREERTOS_HZ=1000 +CONFIG_ESP_TASK_WDT=n From 938b3d717fb4716ad87bdb6a90891b146f9e8fae Mon Sep 17 00:00:00 2001 From: morris Date: Sat, 28 May 2022 17:03:57 +0800 Subject: [PATCH 3/8] example: update bdc speed control example with new driver API --- examples/peripherals/.build-test-rules.yml | 16 + .../mcpwm/mcpwm_bdc_speed_control/README.md | 107 +++---- .../bdc_speed_dashboard.png | Bin 0 -> 124458 bytes .../components/bdc_motor/CMakeLists.txt | 9 + .../components/bdc_motor/README.md | 7 + .../components/bdc_motor/include/bdc_motor.h | 145 +++++++++ .../bdc_motor/interface/bdc_motor_interface.h | 116 +++++++ .../components/bdc_motor/src/bdc_motor.c | 62 ++++ .../bdc_motor/src/bdc_motor_mcpwm_impl.c | 185 ++++++++++++ .../main/Kconfig.projbuild | 12 + .../main/mcpwm_bdc_control_example_main.c | 282 ++++++------------ .../pytest_bdc_speed_control.py | 17 ++ .../serial-studio-dashboard.json | 28 ++ .../serial-studio-proto-map.json | 22 -- tools/ci/check_copyright_ignore.txt | 3 - 15 files changed, 736 insertions(+), 275 deletions(-) create mode 100644 examples/peripherals/mcpwm/mcpwm_bdc_speed_control/bdc_speed_dashboard.png create mode 100644 examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/CMakeLists.txt create mode 100644 examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/README.md create mode 100644 examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/include/bdc_motor.h create mode 100644 examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/interface/bdc_motor_interface.h create mode 100644 examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/src/bdc_motor.c create mode 100644 examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/src/bdc_motor_mcpwm_impl.c create mode 100644 examples/peripherals/mcpwm/mcpwm_bdc_speed_control/main/Kconfig.projbuild create mode 100644 examples/peripherals/mcpwm/mcpwm_bdc_speed_control/pytest_bdc_speed_control.py create mode 100644 examples/peripherals/mcpwm/mcpwm_bdc_speed_control/serial-studio-dashboard.json delete mode 100644 examples/peripherals/mcpwm/mcpwm_bdc_speed_control/serial-studio-proto-map.json diff --git a/examples/peripherals/.build-test-rules.yml b/examples/peripherals/.build-test-rules.yml index 9845fe85fb..391a727b4f 100644 --- a/examples/peripherals/.build-test-rules.yml +++ b/examples/peripherals/.build-test-rules.yml @@ -70,6 +70,22 @@ examples/peripherals/mcpwm: disable: - if: SOC_MCPWM_SUPPORTED != 1 +examples/peripherals/mcpwm/mcpwm_bdc_speed_control: + disable: + - if: SOC_MCPWM_SUPPORTED != 1 + disable_test: + - if: IDF_TARGET != "esp32s3" + temporary: true + reason: lack of runners + +examples/peripherals/mcpwm/mcpwm_bldc_hall_control: + disable: + - if: SOC_MCPWM_SUPPORTED != 1 + disable_test: + - if: IDF_TARGET != "esp32s3" + temporary: true + reason: lack of runners + examples/peripherals/pcnt: disable: - if: SOC_PCNT_SUPPORTED != 1 diff --git a/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/README.md b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/README.md index e2e90b2fb3..09ab448a55 100644 --- a/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/README.md +++ b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/README.md @@ -4,7 +4,11 @@ (See the README.md file in the upper level 'examples' directory for more information about examples.) -This example mainly illustrates how to drive a brushed DC motor by generating two specific PWM signals. However the PWM signals from ESP32 can't drive motors directly as the motor usually consumes high current. So an H-bridge like [DRV8848](https://www.ti.com/product/DRV8848) should be used to provide the needed voltage and current for brushed DC motor. To measure the speed of motor, a photoelectric encoder is used to generate the "speed feedback" signals (e.g. a pair of quadrature signal). The example uses a simple PID control approach to keep the motor speed in a constant speed. The example provides a console command line interface for user to update the PID parameters according to actual situation. +This example mainly illustrates how to drive a brushed DC motor by generating two specific PWM signals. However the PWM signals from ESP chip can't drive motors directly as the motor usually consumes high current. So an H-bridge like [DRV8848](https://www.ti.com/product/DRV8848) should be used to provide the needed voltage and current for brushed DC motor. To simplify the DC motor control of MCPWM peripheral driver, there's a component called [bdc_motor](components/bdc_motor/README.md) which abstracts the common operations into a generic interface. The most useful operations are: `forward`, `reverse`, `coast` and `brake`. + +To measure the speed of motor, a photoelectric encoder is used to generate the "speed feedback" signals (e.g. a pair of quadrature signal). In the example, we use the PCNT peripheral to decode that quadrature signals. For more information, please refer to [rotary encoder example](../../pcnt/rotary_encoder/README.md) as well. + +The example uses a simple PID algorithm to keep the motor spin in a stable speed. The PID component is fetched from the [IDF Component Registry](https://components.espressif.com/component/espressif/pid_ctrl). ## How to Use Example @@ -12,7 +16,7 @@ Before project configuration and build, be sure to set the correct chip target u ### Hardware Required -* A development board with any Espressif SoC which features MCPWM and PCNT peripheral (e.g., ESP32-DevKitC, ESP-WROVER-KIT, etc.) +* A development board with any Espressif SoC which features MCPWM and PCNT peripheral (e.g., ESP32-DevKitC, ESP32-S3-Motor-Devkit, etc.) * A USB cable for Power supply and programming * A separate 12V power supply for brushed DC motor and H-bridge (the voltage depends on the motor model used in the example) * A motor driving board to transfer pwm signal into driving signal @@ -23,28 +27,28 @@ Connection : ``` Power(12V) | - v -+----------------+ +--------------------+ -| | | H-Bridge | -| GND +<----------->| GND | +--------------+ -| | | | | | -| GENA_GPIO_NUM +----PWM0A--->| IN_A OUT_A +----->| Brushed | -| | | | | DC | -| GENB_GPIO_NUM +----PWM0B--->| IN_B OUT_B +----->| Motor | -| | | | | | -| ESP | +--------------------+ | | -| | +------+-------+ -| | | -| | +--------------------+ | -| VCC3.3 +------------>| VCC Encoder | | -| | | | | -| GND +<----------->| |<------------+ -| | | | -|PHASEA_GPIO_NUM |<---PhaseA---+ C1 | -| | | | -|PHASEB_GPIO_NUM |<---PhaseB---+ C2 | -| | | | -+----------------+ +--------------------+ + ESP v ++-------------------+ +--------------------+ +| | | H-Bridge | +| GND +<----------->| GND | +--------------+ +| | | | | | +| BDC_MCPWM_GPIO_A +----PWM0A--->| IN_A OUT_A +----->| Brushed | +| | | | | DC | +| BDC_MCPWM_GPIO_B +----PWM0B--->| IN_B OUT_B +----->| Motor | +| | | | | | +| | +--------------------+ | | +| | +------+-------+ +| | | +| | +--------------------+ | +| VCC3.3 +------------>| VCC Encoder | | +| | | | | +| GND +<----------->| |<------------+ +| | | | +|BDC_ENCODER_GPIO_A |<---PhaseA---+ C1 | +| | | | +|BDC_ENCODER_GPIO_B |<---PhaseB---+ C2 | +| | | | ++-------------------+ +--------------------+ ``` ### Build and Flash @@ -62,51 +66,30 @@ Run the example, you will see the following output log: ``` I (0) cpu_start: Starting scheduler on APP CPU. -configure mcpwm gpio -init mcpwm driver -init and start rotary encoder -init PID control block -init motor control timer -D (561) gptimer: new group (0) @0x3fce0a24 -D (561) gptimer: new gptimer (0,0) at 0x3fce0964, resolution=1000000Hz -create motor control task -start motor control timer -D (571) gptimer: install interrupt service for timer (0,0) -install console command line - -Type 'help' to get the list of commands. -Use UP/DOWN arrows to navigate through command history. -Press TAB when typing command name to auto-complete. -dc-motor> -dc-motor> help -help - Print the list of registered commands - -pid [-p ] [-i ] [-d ] - Set PID parameters - -p Set Kp value of PID - -i Set Ki value of PID - -d Set Kd value of PID +I (308) example: Create DC motor +I (308) gpio: GPIO[7]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (318) gpio: GPIO[15]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (328) example: Init pcnt driver to decode rotary signal +I (328) gpio: GPIO[36]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (338) gpio: GPIO[35]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (348) gpio: GPIO[35]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (358) gpio: GPIO[36]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (368) example: Create PID control block +I (378) example: Create a timer to do PID calculation periodically +I (378) example: Enable motor +I (388) example: Forward motor +I (388) example: Start motor speed loop ``` -### Set PID parameters +### View velocity curve in [Serial Studio](https://github.com/Serial-Studio/Serial-Studio) -* Command: `pid -p -i -d -t ` -* 'p' - proportion value -* 'i' - integral value -* 'd' - differential value -* 't' - PID calculation type (locational or incremental). +To help tune the PID parameters (i.e. `Kp`, `Ki` and `Kd` in the example), this example supports to log a short string frame of runtime motor speed. The string frame can be parsed by [Serial Studio](https://github.com/Serial-Studio/Serial-Studio). This example also provides the [communication description file](serial-studio-dashboard.json) out of the box, which can be loaded by Serial Studio and then plot the curves as follows: -```bash -mcpwm-motor> pid -p 0.8 -i 0.02 -d 0.1 -t inc -pid: kp = 0.800 -pid: ki = 0.020 -pid: kd = 0.100 -pid: type = increment -``` +![bdc_speed_dashboard](bdc_speed_dashboard.png) ## Troubleshooting * Make sure your ESP board and H-bridge module have been connected to the same GND panel. +* The PID parameter set in ths example might not work well in all kinds of motors, because it's not adaptive. You need to fine tune the parameters again by yourself. For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon. diff --git a/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/bdc_speed_dashboard.png b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/bdc_speed_dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..24318617d322456b051adcf97e150276966b97ff GIT binary patch literal 124458 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

$mqGpSCpe=FFQvqv!O#P1$Gj>*M`{Ip1b) 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 MPgg&ebxsLQ0Ft9sX#fBK literal 0 HcmV?d00001 diff --git a/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/CMakeLists.txt b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/CMakeLists.txt new file mode 100644 index 0000000000..e8f680b259 --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/CMakeLists.txt @@ -0,0 +1,9 @@ +set(srcs "src/bdc_motor.c") + +if(CONFIG_SOC_MCPWM_SUPPORTED) + list(APPEND srcs "src/bdc_motor_mcpwm_impl.c") +endif() + +idf_component_register(SRCS ${srcs} + INCLUDE_DIRS "include" "interface" + PRIV_REQUIRES "driver") diff --git a/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/README.md b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/README.md new file mode 100644 index 0000000000..8918067552 --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/README.md @@ -0,0 +1,7 @@ +# Brushed DC Motor Component + +This directory contains an implementation for Brushed DC Motor by different peripherals. Currently only MCPWM is supported as the BDC motor backend. + +To learn more about how to use this component, please check API Documentation from header file [bdc_motor.h](./include/bdc_motor.h). + +Please note that this component is not considered to be a part of ESP-IDF stable API. It may change and it may be removed in the future releases. diff --git a/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/include/bdc_motor.h b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/include/bdc_motor.h new file mode 100644 index 0000000000..1b102d91f6 --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/include/bdc_motor.h @@ -0,0 +1,145 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Brushed DC Motor handle + */ +typedef struct bdc_motor_t *bdc_motor_handle_t; + +/** + * @brief Enable BDC motor + * + * @param motor: BDC Motor handle + * + * @return + * - ESP_OK: Enable motor successfully + * - ESP_ERR_INVALID_ARG: Enable motor failed because of invalid parameters + * - ESP_FAIL: Enable motor failed because other error occurred + */ +esp_err_t bdc_motor_enable(bdc_motor_handle_t motor); + +/** + * @brief Disable BDC motor + * + * @param motor: BDC Motor handle + * + * @return + * - ESP_OK: Disable motor successfully + * - ESP_ERR_INVALID_ARG: Disable motor failed because of invalid parameters + * - ESP_FAIL: Disable motor failed because other error occurred + */ +esp_err_t bdc_motor_disable(bdc_motor_handle_t motor); + +/** + * @brief Set speed for bdc motor + * + * @param motor: BDC Motor handle + * @param speed: BDC speed + * + * @return + * - ESP_OK: Set motor speed successfully + * - ESP_ERR_INVALID_ARG: Set motor speed failed because of invalid parameters + * - ESP_FAIL: Set motor speed failed because other error occurred + */ +esp_err_t bdc_motor_set_speed(bdc_motor_handle_t motor, uint32_t speed); + +/** + * @brief Forward BDC motor + * + * @param motor: BDC Motor handle + * + * @return + * - ESP_OK: Forward motor successfully + * - ESP_FAIL: Forward motor failed because some other error occurred + */ +esp_err_t bdc_motor_forward(bdc_motor_handle_t motor); + +/** + * @brief Reverse BDC Motor + * + * @param strip: BDC Motor handle + * + * @return + * - ESP_OK: Reverse motor successfully + * - ESP_FAIL: Reverse motor failed because some other error occurred + */ +esp_err_t bdc_motor_reverse(bdc_motor_handle_t motor); + +/** + * @brief Stop motor in a coast way (a.k.a Fast Decay) + * + * @param motor: BDC Motor handle + * + * @return + * - ESP_OK: Stop motor successfully + * - ESP_FAIL: Stop motor failed because some other error occurred + */ +esp_err_t bdc_motor_coast(bdc_motor_handle_t motor); + +/** + * @brief Stop motor in a brake way (a.k.a Slow Decay) + * + * @param motor: BDC Motor handle + * + * @return + * - ESP_OK: Stop motor successfully + * - ESP_FAIL: Stop motor failed because some other error occurred + */ +esp_err_t bdc_motor_brake(bdc_motor_handle_t motor); + +/** + * @brief Free BDC Motor resources + * + * @param strip: BDC Motor handle + * + * @return + * - ESP_OK: Free resources successfully + * - ESP_FAIL: Free resources failed because error occurred + */ +esp_err_t bdc_motor_del(bdc_motor_handle_t motor); + +/** + * @brief BDC Motor Configuration + */ +typedef struct { + uint32_t pwma_gpio_num; /*!< BDC Motor PWM A gpio number */ + uint32_t pwmb_gpio_num; /*!< BDC Motor PWM B gpio number */ + uint32_t pwm_freq_hz; /*!< PWM frequency, in Hz */ +} bdc_motor_config_t; + +/** + * @brief BDC Motor MCPWM specific configuration + */ +typedef struct { + int group_id; /*!< MCPWM group number */ + uint32_t resolution_hz; /*!< MCPWM timer resolution */ +} bdc_motor_mcpwm_config_t; + +/** + * @brief Create BDC Motor based on MCPWM peripheral + * + * @param motor_config: BDC Motor configuration + * @param mcpwm_config: MCPWM specific configuration + * @param ret_motor Returned BDC Motor handle + * @return + * - ESP_OK: Create BDC Motor handle successfully + * - ESP_ERR_INVALID_ARG: Create BDC Motor handle failed because of invalid argument + * - ESP_ERR_NO_MEM: Create BDC Motor handle failed because of out of memory + * - ESP_FAIL: Create BDC Motor handle failed because some other error + */ +esp_err_t bdc_motor_new_mcpwm_device(const bdc_motor_config_t *motor_config, const bdc_motor_mcpwm_config_t *mcpwm_config, bdc_motor_handle_t *ret_motor); + +#ifdef __cplusplus +} +#endif diff --git a/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/interface/bdc_motor_interface.h b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/interface/bdc_motor_interface.h new file mode 100644 index 0000000000..35e9697ac2 --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/interface/bdc_motor_interface.h @@ -0,0 +1,116 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct bdc_motor_t bdc_motor_t; /*!< Type of BDC motor */ + +/** + * @brief BDC motor interface definition + */ +struct bdc_motor_t { + /** + * @brief Enable BDC motor + * + * @param motor: BDC Motor handle + * + * @return + * - ESP_OK: Enable motor successfully + * - ESP_ERR_INVALID_ARG: Enable motor failed because of invalid parameters + * - ESP_FAIL: Enable motor failed because other error occurred + */ + esp_err_t (*enable)(bdc_motor_t *motor); + + /** + * @brief Disable BDC motor + * + * @param motor: BDC Motor handle + * + * @return + * - ESP_OK: Disable motor successfully + * - ESP_ERR_INVALID_ARG: Disable motor failed because of invalid parameters + * - ESP_FAIL: Disable motor failed because other error occurred + */ + esp_err_t (*disable)(bdc_motor_t *motor); + + /** + * @brief Set speed for bdc motor + * + * @param motor: BDC Motor handle + * @param speed: BDC speed + * + * @return + * - ESP_OK: Set motor speed successfully + * - ESP_ERR_INVALID_ARG: Set motor speed failed because of invalid parameters + * - ESP_FAIL: Set motor speed failed because other error occurred + */ + esp_err_t (*set_speed)(bdc_motor_t *motor, uint32_t speed); + + /** + * @brief Forward BDC motor + * + * @param motor: BDC Motor handle + * + * @return + * - ESP_OK: Forward motor successfully + * - ESP_FAIL: Forward motor failed because some other error occurred + */ + esp_err_t (*forward)(bdc_motor_t *motor); + + /** + * @brief Reverse BDC Motor + * + * @param motor: BDC Motor handle + * + * @return + * - ESP_OK: Reverse motor successfully + * - ESP_FAIL: Reverse motor failed because some other error occurred + */ + esp_err_t (*reverse)(bdc_motor_t *motor); + + /** + * @brief Stop motor in a coast way (a.k.a Fast Decay) + * + * @param motor: BDC Motor handle + * + * @return + * - ESP_OK: Stop motor successfully + * - ESP_FAIL: Stop motor failed because some other error occurred + */ + esp_err_t (*coast)(bdc_motor_t *motor); + + /** + * @brief Stop motor in a brake way (a.k.a Slow Decay) + * + * @param motor: BDC Motor handle + * + * @return + * - ESP_OK: Stop motor successfully + * - ESP_FAIL: Stop motor failed because some other error occurred + */ + esp_err_t (*brake)(bdc_motor_t *motor); + + /** + * @brief Free BDC Motor handle resources + * + * @param motor: BDC Motor handle + * + * @return + * - ESP_OK: Free resources successfully + * - ESP_FAIL: Free resources failed because error occurred + */ + esp_err_t (*del)(bdc_motor_t *motor); +}; + +#ifdef __cplusplus +} +#endif diff --git a/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/src/bdc_motor.c b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/src/bdc_motor.c new file mode 100644 index 0000000000..fbbc440312 --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/src/bdc_motor.c @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include "esp_log.h" +#include "esp_check.h" +#include "bdc_motor.h" +#include "bdc_motor_interface.h" + +static const char *TAG = "bdc_motor"; + +esp_err_t bdc_motor_enable(bdc_motor_handle_t motor) +{ + ESP_RETURN_ON_FALSE(motor, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + return motor->enable(motor); +} + +esp_err_t bdc_motor_disable(bdc_motor_handle_t motor) +{ + ESP_RETURN_ON_FALSE(motor, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + return motor->disable(motor); +} + +esp_err_t bdc_motor_set_speed(bdc_motor_handle_t motor, uint32_t speed) +{ + ESP_RETURN_ON_FALSE(motor, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + return motor->set_speed(motor, speed); +} + +esp_err_t bdc_motor_forward(bdc_motor_handle_t motor) +{ + ESP_RETURN_ON_FALSE(motor, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + return motor->forward(motor); +} + +esp_err_t bdc_motor_reverse(bdc_motor_handle_t motor) +{ + ESP_RETURN_ON_FALSE(motor, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + return motor->reverse(motor); +} + +esp_err_t bdc_motor_coast(bdc_motor_handle_t motor) +{ + ESP_RETURN_ON_FALSE(motor, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + return motor->coast(motor); +} + +esp_err_t bdc_motor_brake(bdc_motor_handle_t motor) +{ + ESP_RETURN_ON_FALSE(motor, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + return motor->brake(motor); +} + +esp_err_t bdc_motor_del(bdc_motor_handle_t motor) +{ + ESP_RETURN_ON_FALSE(motor, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + return motor->del(motor); +} diff --git a/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/src/bdc_motor_mcpwm_impl.c b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/src/bdc_motor_mcpwm_impl.c new file mode 100644 index 0000000000..2b502a6a44 --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/components/bdc_motor/src/bdc_motor_mcpwm_impl.c @@ -0,0 +1,185 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include "esp_log.h" +#include "esp_check.h" +#include "driver/mcpwm_prelude.h" +#include "bdc_motor.h" +#include "bdc_motor_interface.h" + +static const char *TAG = "bdc_motor_mcpwm"; + +typedef struct { + bdc_motor_t base; + mcpwm_timer_handle_t timer; + mcpwm_oper_handle_t operator; + mcpwm_cmpr_handle_t cmpa; + mcpwm_cmpr_handle_t cmpb; + mcpwm_gen_handle_t gena; + mcpwm_gen_handle_t genb; +} bdc_motor_mcpwm_obj; + +static esp_err_t bdc_motor_mcpwm_set_speed(bdc_motor_t *motor, uint32_t speed) +{ + bdc_motor_mcpwm_obj *mcpwm_motor = __containerof(motor, bdc_motor_mcpwm_obj, base); + ESP_RETURN_ON_ERROR(mcpwm_comparator_set_compare_value(mcpwm_motor->cmpa, speed), TAG, "set compare value failed"); + ESP_RETURN_ON_ERROR(mcpwm_comparator_set_compare_value(mcpwm_motor->cmpb, speed), TAG, "set compare value failed"); + return ESP_OK; +} + +static esp_err_t bdc_motor_mcpwm_enable(bdc_motor_t *motor) +{ + bdc_motor_mcpwm_obj *mcpwm_motor = __containerof(motor, bdc_motor_mcpwm_obj, base); + ESP_RETURN_ON_ERROR(mcpwm_timer_enable(mcpwm_motor->timer), TAG, "enable timer failed"); + ESP_RETURN_ON_ERROR(mcpwm_timer_start_stop(mcpwm_motor->timer, MCPWM_TIMER_START_NO_STOP), TAG, "start timer failed"); + return ESP_OK; +} + +static esp_err_t bdc_motor_mcpwm_disable(bdc_motor_t *motor) +{ + bdc_motor_mcpwm_obj *mcpwm_motor = __containerof(motor, bdc_motor_mcpwm_obj, base); + ESP_RETURN_ON_ERROR(mcpwm_timer_start_stop(mcpwm_motor->timer, MCPWM_TIMER_STOP_EMPTY), TAG, "stop timer failed"); + ESP_RETURN_ON_ERROR(mcpwm_timer_disable(mcpwm_motor->timer), TAG, "disable timer failed"); + return ESP_OK; +} + +static esp_err_t bdc_motor_mcpwm_forward(bdc_motor_t *motor) +{ + bdc_motor_mcpwm_obj *mcpwm_motor = __containerof(motor, bdc_motor_mcpwm_obj, base); + ESP_RETURN_ON_ERROR(mcpwm_generator_set_force_level(mcpwm_motor->gena, -1, true), TAG, "disable force level for gena failed"); + ESP_RETURN_ON_ERROR(mcpwm_generator_set_force_level(mcpwm_motor->genb, 0, true), TAG, "set force level for genb failed"); + return ESP_OK; +} + +static esp_err_t bdc_motor_mcpwm_reverse(bdc_motor_t *motor) +{ + bdc_motor_mcpwm_obj *mcpwm_motor = __containerof(motor, bdc_motor_mcpwm_obj, base); + ESP_RETURN_ON_ERROR(mcpwm_generator_set_force_level(mcpwm_motor->genb, -1, true), TAG, "disable force level for genb failed"); + ESP_RETURN_ON_ERROR(mcpwm_generator_set_force_level(mcpwm_motor->gena, 0, true), TAG, "set force level for gena failed"); + return ESP_OK; +} + +static esp_err_t bdc_motor_mcpwm_coast(bdc_motor_t *motor) +{ + bdc_motor_mcpwm_obj *mcpwm_motor = __containerof(motor, bdc_motor_mcpwm_obj, base); + ESP_RETURN_ON_ERROR(mcpwm_generator_set_force_level(mcpwm_motor->gena, 0, true), TAG, "set force level for gena failed"); + ESP_RETURN_ON_ERROR(mcpwm_generator_set_force_level(mcpwm_motor->genb, 0, true), TAG, "set force level for genb failed"); + return ESP_OK; +} + +static esp_err_t bdc_motor_mcpwm_brake(bdc_motor_t *motor) +{ + bdc_motor_mcpwm_obj *mcpwm_motor = __containerof(motor, bdc_motor_mcpwm_obj, base); + ESP_RETURN_ON_ERROR(mcpwm_generator_set_force_level(mcpwm_motor->gena, 1, true), TAG, "set force level for gena failed"); + ESP_RETURN_ON_ERROR(mcpwm_generator_set_force_level(mcpwm_motor->genb, 1, true), TAG, "set force level for genb failed"); + return ESP_OK; +} + +static esp_err_t bdc_motor_mcpwm_del(bdc_motor_t *motor) +{ + bdc_motor_mcpwm_obj *mcpwm_motor = __containerof(motor, bdc_motor_mcpwm_obj, base); + mcpwm_del_generator(mcpwm_motor->gena); + mcpwm_del_generator(mcpwm_motor->genb); + mcpwm_del_comparator(mcpwm_motor->cmpa); + mcpwm_del_comparator(mcpwm_motor->cmpb); + mcpwm_del_operator(mcpwm_motor->operator); + mcpwm_del_timer(mcpwm_motor->timer); + free(mcpwm_motor); + return ESP_OK; +} + +esp_err_t bdc_motor_new_mcpwm_device(const bdc_motor_config_t *motor_config, const bdc_motor_mcpwm_config_t *mcpwm_config, bdc_motor_handle_t *ret_motor) +{ + bdc_motor_mcpwm_obj *mcpwm_motor = NULL; + esp_err_t ret = ESP_OK; + ESP_GOTO_ON_FALSE(motor_config && mcpwm_config && ret_motor, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + mcpwm_motor = calloc(1, sizeof(bdc_motor_mcpwm_obj)); + ESP_GOTO_ON_FALSE(mcpwm_motor, ESP_ERR_NO_MEM, err, TAG, "no mem for rmt motor"); + + // mcpwm timer + mcpwm_timer_config_t timer_config = { + .group_id = mcpwm_config->group_id, + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .resolution_hz = mcpwm_config->resolution_hz, + .period_ticks = mcpwm_config->resolution_hz / motor_config->pwm_freq_hz, + .count_mode = MCPWM_TIMER_COUNT_MODE_UP, + }; + ESP_GOTO_ON_ERROR(mcpwm_new_timer(&timer_config, &mcpwm_motor->timer), err, TAG, "create MCPWM timer failed"); + + mcpwm_operator_config_t operator_config = { + .group_id = mcpwm_config->group_id, + }; + ESP_GOTO_ON_ERROR(mcpwm_new_operator(&operator_config, &mcpwm_motor->operator), err, TAG, "create MCPWM operator failed"); + + ESP_GOTO_ON_ERROR(mcpwm_operator_connect_timer(mcpwm_motor->operator, mcpwm_motor->timer), err, TAG, "connect timer and operator failed"); + + mcpwm_comparator_config_t comparator_config = { + .flags.update_cmp_on_tez = true, + }; + ESP_GOTO_ON_ERROR(mcpwm_new_comparator(mcpwm_motor->operator, &comparator_config, &mcpwm_motor->cmpa), err, TAG, "create comparator failed"); + ESP_GOTO_ON_ERROR(mcpwm_new_comparator(mcpwm_motor->operator, &comparator_config, &mcpwm_motor->cmpb), err, TAG, "create comparator failed"); + + // set the initial compare value for both comparators + mcpwm_comparator_set_compare_value(mcpwm_motor->cmpa, 0); + mcpwm_comparator_set_compare_value(mcpwm_motor->cmpb, 0); + + mcpwm_generator_config_t generator_config = { + .gen_gpio_num = motor_config->pwma_gpio_num, + }; + ESP_GOTO_ON_ERROR(mcpwm_new_generator(mcpwm_motor->operator, &generator_config, &mcpwm_motor->gena), err, TAG, "create generator failed"); + generator_config.gen_gpio_num = motor_config->pwmb_gpio_num; + ESP_GOTO_ON_ERROR(mcpwm_new_generator(mcpwm_motor->operator, &generator_config, &mcpwm_motor->genb), err, TAG, "create generator failed"); + + mcpwm_generator_set_actions_on_timer_event(mcpwm_motor->gena, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END()); + mcpwm_generator_set_actions_on_compare_event(mcpwm_motor->gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, mcpwm_motor->cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END()); + mcpwm_generator_set_actions_on_timer_event(mcpwm_motor->genb, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END()); + mcpwm_generator_set_actions_on_compare_event(mcpwm_motor->genb, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, mcpwm_motor->cmpb, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END()); + + mcpwm_motor->base.enable = bdc_motor_mcpwm_enable; + mcpwm_motor->base.disable = bdc_motor_mcpwm_disable; + mcpwm_motor->base.forward = bdc_motor_mcpwm_forward; + mcpwm_motor->base.reverse = bdc_motor_mcpwm_reverse; + mcpwm_motor->base.coast = bdc_motor_mcpwm_coast; + mcpwm_motor->base.brake = bdc_motor_mcpwm_brake; + mcpwm_motor->base.set_speed = bdc_motor_mcpwm_set_speed; + mcpwm_motor->base.del = bdc_motor_mcpwm_del; + *ret_motor = &mcpwm_motor->base; + return ESP_OK; + +err: + if (mcpwm_motor) { + if (mcpwm_motor->gena) { + mcpwm_del_generator(mcpwm_motor->gena); + } + if (mcpwm_motor->genb) { + mcpwm_del_generator(mcpwm_motor->genb); + } + if (mcpwm_motor->cmpa) { + mcpwm_del_comparator(mcpwm_motor->cmpa); + } + if (mcpwm_motor->cmpb) { + mcpwm_del_comparator(mcpwm_motor->cmpb); + } + if (mcpwm_motor->operator) { + mcpwm_del_operator(mcpwm_motor->operator); + } + if (mcpwm_motor->timer) { + mcpwm_del_timer(mcpwm_motor->timer); + } + free(mcpwm_motor); + } + return ret; +} diff --git a/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/main/Kconfig.projbuild b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/main/Kconfig.projbuild new file mode 100644 index 0000000000..67e1060d3d --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/main/Kconfig.projbuild @@ -0,0 +1,12 @@ +menu "Example Configuration" + + config SERIAL_STUDIO_DEBUG + bool "Enable log that can be parsed by Serial Studio" + default "n" + help + Enable this option, the example will print a string at runtime with a specific format, + which can be parsed by the Serial Studio tool. + With the "serial-studio-dashboard.json" template file provided in this example, + user can observe the speed in a curve window in the Serial Studio. + +endmenu diff --git a/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/main/mcpwm_bdc_control_example_main.c b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/main/mcpwm_bdc_control_example_main.c index e0ae9205d5..f718225eca 100644 --- a/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/main/mcpwm_bdc_control_example_main.c +++ b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/main/mcpwm_bdc_control_example_main.c @@ -5,176 +5,97 @@ */ #include +#include "sdkconfig.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/queue.h" -#include "driver/gptimer.h" +#include "esp_log.h" +#include "esp_timer.h" #include "driver/pulse_cnt.h" -#include "driver/mcpwm.h" +#include "bdc_motor.h" #include "pid_ctrl.h" -#include "esp_console.h" -#include "argtable3/argtable3.h" + +static const char *TAG = "example"; // Enable this config, we will print debug formated string, which in return can be captured and parsed by Serial-Studio -#define SERIAL_STUDIO_DEBUG 0 +#define SERIAL_STUDIO_DEBUG CONFIG_SERIAL_STUDIO_DEBUG -#define BDC_MCPWM_UNIT 0 -#define BDC_MCPWM_TIMER 0 -#define BDC_MCPWM_GENA_GPIO_NUM 7 -#define BDC_MCPWM_GENB_GPIO_NUM 15 -#define BDC_MCPWM_FREQ_HZ 1500 +#define BDC_MCPWM_TIMER_RESOLUTION_HZ 10000000 // 10MHz, 1 tick = 0.1us +#define BDC_MCPWM_FREQ_HZ 25000 // 25KHz PWM +#define BDC_MCPWM_DUTY_TICK_MAX (BDC_MCPWM_TIMER_RESOLUTION_HZ / BDC_MCPWM_FREQ_HZ) // maximum value we can set for the duty cycle, in ticks +#define BDC_MCPWM_GPIO_A 7 +#define BDC_MCPWM_GPIO_B 15 -#define BDC_ENCODER_PCNT_HIGH_LIMIT 100 -#define BDC_ENCODER_PCNT_LOW_LIMIT -100 -#define BDC_ENCODER_PHASEA_GPIO_NUM 36 -#define BDC_ENCODER_PHASEB_GPIO_NUM 35 +#define BDC_ENCODER_GPIO_A 36 +#define BDC_ENCODER_GPIO_B 35 +#define BDC_ENCODER_PCNT_HIGH_LIMIT 1000 +#define BDC_ENCODER_PCNT_LOW_LIMIT -1000 -#define BDC_PID_CALCULATION_PERIOD_US 10000 -#define BDC_PID_FEEDBACK_QUEUE_LEN 10 - -static pid_ctrl_parameter_t pid_runtime_param = { - .kp = 0.6, - .ki = 0.3, - .kd = 0.12, - .cal_type = PID_CAL_TYPE_INCREMENTAL, - .max_output = 100, - .min_output = -100, - .max_integral = 1000, - .min_integral = -1000, -}; -static bool pid_need_update = false; -static int expect_pulses = 300; -static int real_pulses; +#define BDC_PID_LOOP_PERIOD_MS 10 // calculate the motor speed every 10ms +#define BDC_PID_EXPECT_SPEED 400 // expected motor speed, in the pulses counted by the rotary encoder typedef struct { - pcnt_unit_handle_t hall_pcnt_encoder; - int accumu_count; - QueueHandle_t pid_feedback_queue; -} motor_control_timer_context_t; - -typedef struct { - QueueHandle_t pid_feedback_queue; + bdc_motor_handle_t motor; + pcnt_unit_handle_t pcnt_encoder; pid_ctrl_block_handle_t pid_ctrl; -} motor_control_task_context_t; + int accumu_count; + int report_pulses; +} motor_control_context_t; static bool example_pcnt_on_reach(pcnt_unit_handle_t unit, const pcnt_watch_event_data_t *edata, void *user_ctx) { - motor_control_timer_context_t *ctx = (motor_control_timer_context_t *)user_ctx; - ctx->accumu_count += edata->watch_point_value; + int *accumu_count = (int *)user_ctx; + *accumu_count += edata->watch_point_value; return false; } -static void brushed_motor_set_duty(float duty_cycle) -{ - /* motor moves in forward direction, with duty cycle = duty % */ - if (duty_cycle > 0) { - mcpwm_set_signal_low(BDC_MCPWM_UNIT, BDC_MCPWM_TIMER, MCPWM_OPR_A); - mcpwm_set_duty(BDC_MCPWM_UNIT, BDC_MCPWM_TIMER, MCPWM_OPR_B, duty_cycle); - mcpwm_set_duty_type(BDC_MCPWM_UNIT, BDC_MCPWM_TIMER, MCPWM_OPR_B, MCPWM_DUTY_MODE_0); - } - /* motor moves in backward direction, with duty cycle = -duty % */ - else { - mcpwm_set_signal_low(BDC_MCPWM_UNIT, BDC_MCPWM_TIMER, MCPWM_OPR_B); - mcpwm_set_duty(BDC_MCPWM_UNIT, BDC_MCPWM_TIMER, MCPWM_OPR_A, -duty_cycle); - mcpwm_set_duty_type(BDC_MCPWM_UNIT, BDC_MCPWM_TIMER, MCPWM_OPR_A, MCPWM_DUTY_MODE_0); - } -} - -static bool motor_ctrl_timer_cb(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *arg) +static void pid_loop_cb(void *args) { static int last_pulse_count = 0; - BaseType_t high_task_awoken = pdFALSE; - motor_control_timer_context_t *user_ctx = (motor_control_timer_context_t *)arg; - pcnt_unit_handle_t pcnt_unit = user_ctx->hall_pcnt_encoder; + motor_control_context_t *ctx = (motor_control_context_t *)args; + pcnt_unit_handle_t pcnt_unit = ctx->pcnt_encoder; + pid_ctrl_block_handle_t pid_ctrl = ctx->pid_ctrl; + bdc_motor_handle_t motor = ctx->motor; + // get the result from rotary encoder int cur_pulse_count = 0; pcnt_unit_get_count(pcnt_unit, &cur_pulse_count); - cur_pulse_count += user_ctx->accumu_count; - - int delta = cur_pulse_count - last_pulse_count; + cur_pulse_count += ctx->accumu_count; + int real_pulses = cur_pulse_count - last_pulse_count; last_pulse_count = cur_pulse_count; - xQueueSendFromISR(user_ctx->pid_feedback_queue, &delta, &high_task_awoken); + ctx->report_pulses = real_pulses; - return high_task_awoken == pdTRUE; -} + // calculate the speed error + float error = BDC_PID_EXPECT_SPEED - real_pulses; + float new_speed = 0; -static void bdc_ctrl_task(void *arg) -{ - float duty_cycle = 0; - motor_control_task_context_t *user_ctx = (motor_control_task_context_t *)arg; - while (1) { - xQueueReceive(user_ctx->pid_feedback_queue, &real_pulses, portMAX_DELAY); - float error = expect_pulses - real_pulses; - pid_compute(user_ctx->pid_ctrl, error, &duty_cycle); - brushed_motor_set_duty(duty_cycle); - } -} - -static struct { - struct arg_dbl *kp; - struct arg_dbl *ki; - struct arg_dbl *kd; - struct arg_end *end; -} pid_ctrl_args; - -static int do_pid_ctrl_cmd(int argc, char **argv) -{ - int nerrors = arg_parse(argc, argv, (void **)&pid_ctrl_args); - if (nerrors != 0) { - arg_print_errors(stderr, pid_ctrl_args.end, argv[0]); - return 0; - } - if (pid_ctrl_args.kp->count) { - pid_runtime_param.kp = pid_ctrl_args.kp->dval[0]; - } - if (pid_ctrl_args.ki->count) { - pid_runtime_param.ki = pid_ctrl_args.ki->dval[0]; - } - if (pid_ctrl_args.kd->count) { - pid_runtime_param.kd = pid_ctrl_args.kd->dval[0]; - } - - pid_need_update = true; - return 0; -} - -static void register_pid_console_command(void) -{ - pid_ctrl_args.kp = arg_dbl0("p", NULL, "", "Set Kp value of PID"); - pid_ctrl_args.ki = arg_dbl0("i", NULL, "", "Set Ki value of PID"); - pid_ctrl_args.kd = arg_dbl0("d", NULL, "", "Set Kd value of PID"); - pid_ctrl_args.end = arg_end(2); - const esp_console_cmd_t pid_ctrl_cmd = { - .command = "pid", - .help = "Set PID parameters", - .hint = NULL, - .func = &do_pid_ctrl_cmd, - .argtable = &pid_ctrl_args - }; - ESP_ERROR_CHECK(esp_console_cmd_register(&pid_ctrl_cmd)); + // set the new speed + pid_compute(pid_ctrl, error, &new_speed); + bdc_motor_set_speed(motor, (uint32_t)new_speed); } void app_main(void) { - static motor_control_timer_context_t my_timer_ctx = {}; - - QueueHandle_t pid_fb_queue = xQueueCreate(BDC_PID_FEEDBACK_QUEUE_LEN, sizeof(int)); - assert(pid_fb_queue); - - printf("configure mcpwm gpio\r\n"); - ESP_ERROR_CHECK(mcpwm_gpio_init(BDC_MCPWM_UNIT, MCPWM0A, BDC_MCPWM_GENA_GPIO_NUM)); - ESP_ERROR_CHECK(mcpwm_gpio_init(BDC_MCPWM_UNIT, MCPWM0B, BDC_MCPWM_GENB_GPIO_NUM)); - printf("init mcpwm driver\n"); - mcpwm_config_t pwm_config = { - .frequency = BDC_MCPWM_FREQ_HZ, - .cmpr_a = 0, - .cmpr_b = 0, - .counter_mode = MCPWM_UP_COUNTER, - .duty_mode = MCPWM_DUTY_MODE_0, + static motor_control_context_t motor_ctrl_ctx = { + .accumu_count = 0, + .pcnt_encoder = NULL, }; - ESP_ERROR_CHECK(mcpwm_init(BDC_MCPWM_UNIT, BDC_MCPWM_TIMER, &pwm_config)); - printf("init and start rotary encoder\r\n"); + ESP_LOGI(TAG, "Create DC motor"); + bdc_motor_config_t motor_config = { + .pwm_freq_hz = BDC_MCPWM_FREQ_HZ, + .pwma_gpio_num = BDC_MCPWM_GPIO_A, + .pwmb_gpio_num = BDC_MCPWM_GPIO_B, + }; + bdc_motor_mcpwm_config_t mcpwm_config = { + .group_id = 0, + .resolution_hz = BDC_MCPWM_TIMER_RESOLUTION_HZ, + }; + bdc_motor_handle_t motor = NULL; + ESP_ERROR_CHECK(bdc_motor_new_mcpwm_device(&motor_config, &mcpwm_config, &motor)); + motor_ctrl_ctx.motor = motor; + + ESP_LOGI(TAG, "Init pcnt driver to decode rotary signal"); pcnt_unit_config_t unit_config = { .high_limit = BDC_ENCODER_PCNT_HIGH_LIMIT, .low_limit = BDC_ENCODER_PCNT_LOW_LIMIT, @@ -186,14 +107,14 @@ void app_main(void) }; ESP_ERROR_CHECK(pcnt_unit_set_glitch_filter(pcnt_unit, &filter_config)); pcnt_chan_config_t chan_a_config = { - .edge_gpio_num = BDC_ENCODER_PHASEA_GPIO_NUM, - .level_gpio_num = BDC_ENCODER_PHASEB_GPIO_NUM, + .edge_gpio_num = BDC_ENCODER_GPIO_A, + .level_gpio_num = BDC_ENCODER_GPIO_B, }; pcnt_channel_handle_t pcnt_chan_a = NULL; ESP_ERROR_CHECK(pcnt_new_channel(pcnt_unit, &chan_a_config, &pcnt_chan_a)); pcnt_chan_config_t chan_b_config = { - .edge_gpio_num = BDC_ENCODER_PHASEB_GPIO_NUM, - .level_gpio_num = BDC_ENCODER_PHASEA_GPIO_NUM, + .edge_gpio_num = BDC_ENCODER_GPIO_B, + .level_gpio_num = BDC_ENCODER_GPIO_A, }; pcnt_channel_handle_t pcnt_chan_b = NULL; ESP_ERROR_CHECK(pcnt_new_channel(pcnt_unit, &chan_b_config, &pcnt_chan_b)); @@ -204,70 +125,55 @@ void app_main(void) ESP_ERROR_CHECK(pcnt_unit_add_watch_point(pcnt_unit, BDC_ENCODER_PCNT_HIGH_LIMIT)); ESP_ERROR_CHECK(pcnt_unit_add_watch_point(pcnt_unit, BDC_ENCODER_PCNT_LOW_LIMIT)); pcnt_event_callbacks_t pcnt_cbs = { - .on_reach = example_pcnt_on_reach, + .on_reach = example_pcnt_on_reach, // accumulate the overflow in the callback }; - ESP_ERROR_CHECK(pcnt_unit_register_event_callbacks(pcnt_unit, &pcnt_cbs, &my_timer_ctx)); + ESP_ERROR_CHECK(pcnt_unit_register_event_callbacks(pcnt_unit, &pcnt_cbs, &motor_ctrl_ctx.accumu_count)); ESP_ERROR_CHECK(pcnt_unit_enable(pcnt_unit)); ESP_ERROR_CHECK(pcnt_unit_clear_count(pcnt_unit)); ESP_ERROR_CHECK(pcnt_unit_start(pcnt_unit)); + motor_ctrl_ctx.pcnt_encoder = pcnt_unit; - printf("init PID control block\r\n"); - pid_ctrl_block_handle_t pid_ctrl; + ESP_LOGI(TAG, "Create PID control block"); + pid_ctrl_parameter_t pid_runtime_param = { + .kp = 0.6, + .ki = 0.4, + .kd = 0.2, + .cal_type = PID_CAL_TYPE_INCREMENTAL, + .max_output = BDC_MCPWM_DUTY_TICK_MAX - 1, + .min_output = 0, + .max_integral = 1000, + .min_integral = -1000, + }; + pid_ctrl_block_handle_t pid_ctrl = NULL; pid_ctrl_config_t pid_config = { .init_param = pid_runtime_param, }; ESP_ERROR_CHECK(pid_new_control_block(&pid_config, &pid_ctrl)); + motor_ctrl_ctx.pid_ctrl = pid_ctrl; - printf("init motor control timer\r\n"); - gptimer_handle_t gptimer; - gptimer_config_t timer_config = { - .clk_src = GPTIMER_CLK_SRC_DEFAULT, - .direction = GPTIMER_COUNT_UP, - .resolution_hz = 1000000, // 1MHz, 1 tick = 1us + ESP_LOGI(TAG, "Create a timer to do PID calculation periodically"); + const esp_timer_create_args_t periodic_timer_args = { + .callback = pid_loop_cb, + .arg = &motor_ctrl_ctx, + .name = "pid_loop" }; - ESP_ERROR_CHECK(gptimer_new_timer(&timer_config, &gptimer)); + esp_timer_handle_t pid_loop_timer = NULL; + ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &pid_loop_timer)); - printf("create motor control task\r\n"); - static motor_control_task_context_t my_ctrl_task_ctx = {}; - my_ctrl_task_ctx.pid_feedback_queue = pid_fb_queue; - my_ctrl_task_ctx.pid_ctrl = pid_ctrl; - xTaskCreate(bdc_ctrl_task, "bdc_ctrl_task", 4096, &my_ctrl_task_ctx, 5, NULL); + ESP_LOGI(TAG, "Enable motor"); + ESP_ERROR_CHECK(bdc_motor_enable(motor)); + ESP_LOGI(TAG, "Forward motor"); + ESP_ERROR_CHECK(bdc_motor_forward(motor)); - printf("start motor control timer\r\n"); - my_timer_ctx.pid_feedback_queue = pid_fb_queue; - my_timer_ctx.hall_pcnt_encoder = pcnt_unit; - gptimer_event_callbacks_t gptimer_cbs = { - .on_alarm = motor_ctrl_timer_cb, - }; - ESP_ERROR_CHECK(gptimer_register_event_callbacks(gptimer, &gptimer_cbs, &my_timer_ctx)); - gptimer_alarm_config_t alarm_config = { - .reload_count = 0, - .alarm_count = BDC_PID_CALCULATION_PERIOD_US, - .flags.auto_reload_on_alarm = true, - }; - ESP_ERROR_CHECK(gptimer_set_alarm_action(gptimer, &alarm_config)); - ESP_ERROR_CHECK(gptimer_enable(gptimer)); - ESP_ERROR_CHECK(gptimer_start(gptimer)); - - printf("install console command line\r\n"); - esp_console_repl_t *repl = NULL; - esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT(); - repl_config.prompt = "dc-motor>"; - esp_console_dev_uart_config_t uart_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT(); - ESP_ERROR_CHECK(esp_console_new_repl_uart(&uart_config, &repl_config, &repl)); - register_pid_console_command(); - ESP_ERROR_CHECK(esp_console_start_repl(repl)); + ESP_LOGI(TAG, "Start motor speed loop"); + ESP_ERROR_CHECK(esp_timer_start_periodic(pid_loop_timer, BDC_PID_LOOP_PERIOD_MS * 1000)); while (1) { vTaskDelay(pdMS_TO_TICKS(100)); - // the following logging format is according to the requirement of serial-studio - // also see the parser mapping file `serial-studio-proto-map.json` in the project folder + // the following logging format is according to the requirement of serial-studio frame format + // also see the dashboard config file `serial-studio-dashboard.json` for more information #if SERIAL_STUDIO_DEBUG - printf("/*%d*/\r\n", real_pulses); + printf("/*%d*/\r\n", motor_ctrl_ctx.report_pulses); #endif - if (pid_need_update) { - pid_update_parameters(pid_ctrl, &pid_runtime_param); - pid_need_update = false; - } } } diff --git a/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/pytest_bdc_speed_control.py b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/pytest_bdc_speed_control.py new file mode 100644 index 0000000000..95579418bd --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/pytest_bdc_speed_control.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded import Dut + + +@pytest.mark.esp32s3 +@pytest.mark.generic +def test_bdc_speed_control_example(dut: Dut) -> None: + dut.expect_exact('example: Create DC motor') + dut.expect_exact('example: Init pcnt driver to decode rotary signal') + dut.expect_exact('example: Create PID control block') + dut.expect_exact('example: Create a timer to do PID calculation periodically') + dut.expect_exact('example: Enable motor') + dut.expect_exact('example: Forward motor') + dut.expect_exact('example: Start motor speed loop') diff --git a/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/serial-studio-dashboard.json b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/serial-studio-dashboard.json new file mode 100644 index 0000000000..efa33c1430 --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/serial-studio-dashboard.json @@ -0,0 +1,28 @@ +{ + "frameEnd": "*/", + "frameStart": "/*", + "groups": [ + { + "datasets": [ + { + "alarm": 1000, + "fft": false, + "fftSamples": 1024, + "graph": true, + "led": false, + "log": false, + "max": 500, + "min": 0, + "title": "current speed", + "units": "", + "value": "%1", + "widget": "gauge" + } + ], + "title": "speed", + "widget": "" + } + ], + "separator": ",", + "title": "Brushed DC Motor Speed" +} diff --git a/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/serial-studio-proto-map.json b/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/serial-studio-proto-map.json deleted file mode 100644 index 29ac2d6695..0000000000 --- a/examples/peripherals/mcpwm/mcpwm_bdc_speed_control/serial-studio-proto-map.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "fe": "*/", - "fs": "/*", - "g": [ - { - "d": [ - { - "g": true, - "max": 100, - "min": 0, - "t": "pulses within 10ms", - "u": "", - "v": "%1", - "w": "" - } - ], - "t": "Encoder Feedback", - } - ], - "s": ",", - "t": "Brushed DC Motor Speed" -} diff --git a/tools/ci/check_copyright_ignore.txt b/tools/ci/check_copyright_ignore.txt index 11589d787c..e13723ab90 100644 --- a/tools/ci/check_copyright_ignore.txt +++ b/tools/ci/check_copyright_ignore.txt @@ -1773,9 +1773,6 @@ examples/peripherals/i2c/i2c_tools/main/cmd_i2ctools.h examples/peripherals/i2c/i2c_tools/main/i2ctools_example_main.c examples/peripherals/ledc/ledc_basic/main/ledc_basic_example_main.c examples/peripherals/ledc/ledc_fade/main/ledc_fade_example_main.c -examples/peripherals/mcpwm/mcpwm_brushed_dc_control/main/cmd_mcpwm_motor.c -examples/peripherals/mcpwm/mcpwm_brushed_dc_control/main/mcpwm_brushed_dc_control_example.c -examples/peripherals/mcpwm/mcpwm_brushed_dc_control/main/mcpwm_brushed_dc_control_example.h examples/peripherals/mcpwm/mcpwm_servo_control/main/mcpwm_servo_control_example_main.c examples/peripherals/sdio/host/main/app_main.c examples/peripherals/sdio/sdio_test.py From b77446b5c834a2447a3a5012aa900a50d3a15685 Mon Sep 17 00:00:00 2001 From: morris Date: Sat, 28 May 2022 17:04:38 +0800 Subject: [PATCH 4/8] example: update bldc example with new driver API --- .../mcpwm/mcpwm_bldc_hall_control/README.md | 111 +++-- .../mcpwm_bldc_hall_control_example_main.c | 436 +++++++++++------- .../pytest_bldc_hall_control.py | 25 + 3 files changed, 360 insertions(+), 212 deletions(-) create mode 100644 examples/peripherals/mcpwm/mcpwm_bldc_hall_control/pytest_bldc_hall_control.py diff --git a/examples/peripherals/mcpwm/mcpwm_bldc_hall_control/README.md b/examples/peripherals/mcpwm/mcpwm_bldc_hall_control/README.md index 5fa70fe40d..9e53e1c2ea 100644 --- a/examples/peripherals/mcpwm/mcpwm_bldc_hall_control/README.md +++ b/examples/peripherals/mcpwm/mcpwm_bldc_hall_control/README.md @@ -1,51 +1,54 @@ | Supported Targets | ESP32 | ESP32-S3 | | ----------------- | ----- | -------- | -# MCPWM BLDC Hall motor control Example +# MCPWM BLDC Motor Control with HALL Sensor Example (See the README.md file in the upper level 'examples' directory for more information about examples.) -This example will illustrate how to use MCPWM driver to control BLDC motor with hall sensor feedback. In the example, a timer is running at the background to update the motor speed periodically. - -With the hardware fault detection feature of MCPWM, the example will shut down the MOSFETs when over current happens. +The MCPWM peripheral can generate three pairs of complementary PWMs by the internal dead time submodule, which is suitable for a BLDC motor application. This example demonstrates how to use the MCPWM peripheral to control a BLDC motor in a six-step commutation scheme. +We will change the on/off state of the six MOSFETs in a predefined order when the Hall sensor detects a change of the motor phase, so that the motor can spin in a predefined direction. ## How to Use Example ### Hardware Required -1. The BLDC motor used in this example has a hall sensor capture sequence of `6-->4-->5-->1-->3-->2-->6-->4-->` and so on. -2. A three-phase gate driver, this example uses [IR2136](https://www.infineon.com/cms/en/product/power/gate-driver-ics/ir2136s/). -3. Six N-MOSFETs, this example uses [IRF540NS](https://www.infineon.com/cms/en/product/power/mosfet/12v-300v-n-channel-power-mosfet/irf540ns/). -4. A development board with any Espressif SoC which features MCPWM peripheral (e.g., ESP32-DevKitC, ESP-WROVER-KIT, etc.) -5. A USB cable for Power supply and programming. +1. A ESP board with MCPWM peripheral supported (e.g. ESP32-S3-Motor-DevKit) +2. A BLDC motor whose commutation table is `6-->4-->5-->1-->3-->2-->6` +3. A three-phase gate driver, for example, the [DRV8302](https://www.ti.com.cn/product/zh-cn/DRV8302) +4. Six N-MOSFETs, for example, the [IRF540NS](https://www.infineon.com/cms/en/product/power/mosfet/12v-300v-n-channel-power-mosfet/irf540ns/) +5. A USB cable for Power supply and programming Connection : ``` - ┌─────────────────────────────────────────────┐ - │ │ - │ ┌───────────────────────────┐ │ - │ │ │ │ -┌─────────┴─────────┴───────────┐ ┌────────┴───────┴──────────┐ -│ GPIO19 GPIO18 │ │ EN FAULT │ -│ GPIO21├──────┤PWM_UH │ ┌────────────┐ -│ GPIO22├──────┤PWM_UL U├────────┤ │ -│ │ │ │ │ │ -│ GPIO23├──────┤PWM_VH V├────────┤ BLDC │ -│ ESP Board GPIO25├──────┤PWM_VL 3-Phase Bridge │ │ │ -│ │ │ + W├────────┤ │ -│ GPIO26├──────┤PWM_WH MOSFET │ └─┬───┬───┬──┘ -│ GPIO27├──────┤PWM_WL │ │ │ │ -│ GPIO5 GPIO4 GPIO2 │ │ │ │ │ │ -└─────┬──────┬──────┬───────────┘ └───────────────────────────┘ │ │ │ - │ │ │ Hall U │ │ │ - │ │ └─────────────────────────────────────────────────────────┘ │ │ - │ │ Hall V │ │ - │ └────────────────────────────────────────────────────────────────────┘ │ - │ Hall W │ - └───────────────────────────────────────────────────────────────────────────────┘ + +---------------------------------------------------------------------------------+ + | | + | +---------------------------------------------+ | VM + | | | | ^ + | | +---------------------------+ | | | + | | | | | | | ++-------------+-----------------------------+---------+-----------+ +--------+-------+-----+---++ +| GND BLDC_DRV_FAULT_GPIO BLDC_DRV_EN_GPIO | | EN FAULT GND | +| BLDC_PWM_UH_GPIO +------+PWM_UH | +------------+ +| BLDC_PWM_UL_GPIO +------+PWM_UL U+--------+ | +| | | | | | +| ESP Board BLDC_PWM_VH_GPIO +------+PWM_VH V+--------+ BLDC | +| BLDC_PWM_VL_GPIO +------+PWM_VL 3-Phase Bridge | | | +| | | + W+--------+ | +| BLDC_PWM_WH_GPIO +------+PWM_WH MOSFET | +-+---+---+--+ +| BLDC_PWM_WL_GPIO +------+PWM_WL | | | | +| HALL_CAP_W_GPIO HALL_CAP_V_GPIO HALL_CAP_U_GPIO | | | | | | ++-----------+------------------+------------------+---------------+ +---------------------------+ | | | + | | | Hall U | | | + | | +-------------------------------------------------------------+ | | + | | Hall V | | + | +------------------------------------------------------------------------------------+ | + | Hall W | + +-----------------------------------------------------------------------------------------------------------+ ``` +You can change the GPIO number in the [example code](main/mcpwm_bldc_hall_control_example_main.c) according to your board. You can define the spin direction in the code as well by the `BLDC_SPIN_DIRECTION_CCW` macro. + ### Build and Flash Run `idf.py -p PORT flash monitor` to build, flash and monitor the project. @@ -62,20 +65,42 @@ Run the example, you will see the following output log: ``` ... I (0) cpu_start: Starting scheduler on APP CPU. -I (327) example: Disable gate driver -I (327) gpio: GPIO[18]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 -I (337) example: Setup PWM and Hall GPIO (pull up internally) -I (347) example: Initialize PWM (default to turn off all MOSFET) -I (357) example: Initialize over current fault action -I (357) example: Initialize Hall sensor capture -I (367) example: Please turn on the motor power -I (5367) example: Enable gate driver -I (5367) example: Changing speed at background +I (307) example: Disable MOSFET gate +I (307) gpio: GPIO[46]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 +I (317) example: Create MCPWM timer +I (317) example: Create MCPWM operator +I (327) example: Connect operators to the same timer +I (327) example: Create comparators +I (337) example: Create over current fault detector +I (337) gpio: GPIO[10]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (347) example: Set brake mode on the fault event +I (357) example: Create PWM generators +I (357) gpio: GPIO[47]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (367) gpio: GPIO[21]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (377) gpio: GPIO[14]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (387) gpio: GPIO[13]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (397) gpio: GPIO[12]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (407) gpio: GPIO[11]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (417) example: Set generator actions +I (417) example: Setup deadtime +I (427) example: Turn off all the gates by default +I (427) example: Create Hall sensor capture channels +I (437) gpio: GPIO[4]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (447) gpio: GPIO[5]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (457) gpio: GPIO[6]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (457) example: Register event callback for capture channels +I (467) example: Start a timer to adjust motor speed periodically +I (477) example: Enable MOSFET gate +I (477) example: Start the MCPWM timer ... ``` -## Dive into the example +The BLDC motor will update the speed periodically. -1. How to change the rotation direction? +## Troubleshooting - The rotation direction is controlled by how the hall sensor value is parsed. If you pass `false` to `bldc_get_hall_sensor_value`, the BLDC should rotate in clock wise. Likewise, passing `true` to that function will make tha BLDC rotate in counter clock wise. +* Make sure your ESP board and H-bridge module have been connected to the same GND panel. +* Check the fault signal polarity, otherwise the motor will not spin if the MCPWM detector treats the normal level as a fault one. +* Don't use the PC USB as the power source of the BLDC motor (see the `VM` in the above connection diagram), it might damage your UAB port. + +For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon. diff --git a/examples/peripherals/mcpwm/mcpwm_bldc_hall_control/main/mcpwm_bldc_hall_control_example_main.c b/examples/peripherals/mcpwm/mcpwm_bldc_hall_control/main/mcpwm_bldc_hall_control_example_main.c index f4a0f62bbb..3fcb12d21e 100644 --- a/examples/peripherals/mcpwm/mcpwm_bldc_hall_control/main/mcpwm_bldc_hall_control_example_main.c +++ b/examples/peripherals/mcpwm/mcpwm_bldc_hall_control/main/mcpwm_bldc_hall_control_example_main.c @@ -8,47 +8,46 @@ #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" -#include "driver/mcpwm.h" -#include "driver/gpio.h" -#include "esp_timer.h" #include "esp_attr.h" #include "esp_log.h" +#include "esp_timer.h" +#include "driver/mcpwm_prelude.h" +#include "driver/gpio.h" -#define PWM_DEFAULT_FREQ 14400 -#define PWM_MIN_DUTY 40.0 -#define PWM_MAX_DUTY 80.0 -#define PWM_DUTY_STEP 5.0 -#define BLDC_MCPWM_GROUP 0 -#define BLDC_MCPWM_TIMER_U 0 -#define BLDC_MCPWM_TIMER_V 1 -#define BLDC_MCPWM_TIMER_W 2 -#define BLDC_MCPWM_GEN_HIGH MCPWM_GEN_A -#define BLDC_MCPWM_GEN_LOW MCPWM_GEN_B +#define BLDC_MCPWM_TIMER_RESOLUTION_HZ 10000000 // 10MHz, 1 tick = 0.1us +#define BLDC_MCPWM_PERIOD 500 // 50us, 20KHz +#define BLDC_SPIN_DIRECTION_CCW false // define the spin direction +#define BLDC_SPEED_UPDATE_PERIOD_US 200000 // 200ms +#define BLDC_DRV_EN_GPIO 46 +#define BLDC_DRV_FAULT_GPIO 10 +#define BLDC_PWM_UH_GPIO 47 +#define BLDC_PWM_UL_GPIO 21 +#define BLDC_PWM_VH_GPIO 14 +#define BLDC_PWM_VL_GPIO 13 +#define BLDC_PWM_WH_GPIO 12 +#define BLDC_PWM_WL_GPIO 11 +#define HALL_CAP_U_GPIO 4 +#define HALL_CAP_V_GPIO 5 +#define HALL_CAP_W_GPIO 6 -#define BLDC_DRV_EN_GPIO 18 -#define BLDC_DRV_FAULT_GPIO 19 -#define BLDC_DRV_OVER_CURRENT_FAULT MCPWM_SELECT_F0 - -#define BLDC_PWM_UH_GPIO 21 -#define BLDC_PWM_UL_GPIO 22 -#define BLDC_PWM_VH_GPIO 23 -#define BLDC_PWM_VL_GPIO 25 -#define BLDC_PWM_WH_GPIO 26 -#define BLDC_PWM_WL_GPIO 27 -#define HALL_CAP_U_GPIO 2 -#define HALL_CAP_V_GPIO 4 -#define HALL_CAP_W_GPIO 5 +#define BLDC_MCPWM_OP_INDEX_U 0 +#define BLDC_MCPWM_OP_INDEX_V 1 +#define BLDC_MCPWM_OP_INDEX_W 2 +#define BLDC_MCPWM_GEN_INDEX_HIGH 0 +#define BLDC_MCPWM_GEN_INDEX_LOW 1 static const char *TAG = "example"; +typedef void (*bldc_hall_phase_action_t)(mcpwm_gen_handle_t (*gens)[2]); + static inline uint32_t bldc_get_hall_sensor_value(bool ccw) { uint32_t hall_val = gpio_get_level(HALL_CAP_U_GPIO) * 4 + gpio_get_level(HALL_CAP_V_GPIO) * 2 + gpio_get_level(HALL_CAP_W_GPIO) * 1; return ccw ? hall_val ^ (0x07) : hall_val; } -static bool IRAM_ATTR bldc_hall_updated(mcpwm_unit_t mcpwm, mcpwm_capture_channel_id_t cap_channel, const cap_event_data_t *edata, void *user_data) +static bool IRAM_ATTR bldc_hall_updated(mcpwm_cap_channel_handle_t cap_channel, const mcpwm_capture_event_data_t *edata, void *user_data) { TaskHandle_t task_to_notify = (TaskHandle_t)user_data; BaseType_t high_task_wakeup = pdFALSE; @@ -56,101 +55,99 @@ static bool IRAM_ATTR bldc_hall_updated(mcpwm_unit_t mcpwm, mcpwm_capture_channe return high_task_wakeup == pdTRUE; } -static void update_bldc_speed(void *arg) +// U+V- +static void bldc_set_phase_up_vm(mcpwm_gen_handle_t (*gens)[2]) { - static float duty = PWM_MIN_DUTY; - static float duty_step = PWM_DUTY_STEP; - duty += duty_step; - if (duty > PWM_MAX_DUTY || duty < PWM_MIN_DUTY) { - duty_step *= -1; - } - mcpwm_set_duty(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U, BLDC_MCPWM_GEN_HIGH, duty); - mcpwm_set_duty(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U, BLDC_MCPWM_GEN_LOW, duty); - mcpwm_set_duty(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V, BLDC_MCPWM_GEN_HIGH, duty); - mcpwm_set_duty(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V, BLDC_MCPWM_GEN_LOW, duty); - mcpwm_set_duty(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W, BLDC_MCPWM_GEN_HIGH, duty); - mcpwm_set_duty(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W, BLDC_MCPWM_GEN_LOW, duty); + // U+ = PWM, U- = _PWM_ + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_U][BLDC_MCPWM_GEN_INDEX_HIGH], -1, true); + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_U][BLDC_MCPWM_GEN_INDEX_LOW], -1, true); + // V+ = 0, V- = 1 + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_V][BLDC_MCPWM_GEN_INDEX_HIGH], 0, true); + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_V][BLDC_MCPWM_GEN_INDEX_LOW], 1, true); + // W+ = 0, W- = 0 + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_W][BLDC_MCPWM_GEN_INDEX_HIGH], 0, true); + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_W][BLDC_MCPWM_GEN_INDEX_LOW], 0, true); } -// U+V- / A+B- -static void bldc_set_phase_up_vm(void) +// W+U- +static void bldc_set_phase_wp_um(mcpwm_gen_handle_t (*gens)[2]) { - mcpwm_set_duty_type(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U, BLDC_MCPWM_GEN_HIGH, MCPWM_DUTY_MODE_0); // U+ = PWM - mcpwm_deadtime_enable(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U, MCPWM_ACTIVE_HIGH_COMPLIMENT_MODE, 3, 3); // U- = _PWM_ - mcpwm_deadtime_disable(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V); - mcpwm_set_signal_low(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V, BLDC_MCPWM_GEN_HIGH); // V+ = 0 - mcpwm_set_signal_high(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V, BLDC_MCPWM_GEN_LOW); // V- = 1 - mcpwm_deadtime_disable(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W); - mcpwm_set_signal_low(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W, BLDC_MCPWM_GEN_HIGH); // W+ = 0 - mcpwm_set_signal_low(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W, BLDC_MCPWM_GEN_LOW); // W- = 0 + // U+ = 0, U- = 1 + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_U][BLDC_MCPWM_GEN_INDEX_HIGH], 0, true); + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_U][BLDC_MCPWM_GEN_INDEX_LOW], 1, true); + + // V+ = 0, V- = 0 + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_V][BLDC_MCPWM_GEN_INDEX_HIGH], 0, true); + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_V][BLDC_MCPWM_GEN_INDEX_LOW], 0, true); + + // W+ = PWM, W- = _PWM_ + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_W][BLDC_MCPWM_GEN_INDEX_HIGH], -1, true); + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_W][BLDC_MCPWM_GEN_INDEX_LOW], -1, true); } -// W+U- / C+A- -static void bldc_set_phase_wp_um(void) +// W+V- +static void bldc_set_phase_wp_vm(mcpwm_gen_handle_t (*gens)[2]) { - mcpwm_deadtime_disable(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U); - mcpwm_set_signal_low(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U, BLDC_MCPWM_GEN_HIGH); // U+ = 0 - mcpwm_set_signal_high(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U, BLDC_MCPWM_GEN_LOW); // U- = 1 - mcpwm_deadtime_disable(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V); - mcpwm_set_signal_low(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V, BLDC_MCPWM_GEN_HIGH); // V+ = 0 - mcpwm_set_signal_low(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V, BLDC_MCPWM_GEN_LOW); // V- = 0 - mcpwm_set_duty_type(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W, BLDC_MCPWM_GEN_HIGH, MCPWM_DUTY_MODE_0); // W+ = PWM - mcpwm_deadtime_enable(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W, MCPWM_ACTIVE_HIGH_COMPLIMENT_MODE, 3, 3); // W- = _PWM_ + // U+ = 0, U- = 0 + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_U][BLDC_MCPWM_GEN_INDEX_HIGH], 0, true); + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_U][BLDC_MCPWM_GEN_INDEX_LOW], 0, true); + + // V+ = 0, V- = 1 + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_V][BLDC_MCPWM_GEN_INDEX_HIGH], 0, true); + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_V][BLDC_MCPWM_GEN_INDEX_LOW], 1, true); + + // W+ = PWM, W- = _PWM_ + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_W][BLDC_MCPWM_GEN_INDEX_HIGH], -1, true); + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_W][BLDC_MCPWM_GEN_INDEX_LOW], -1, true); } -// W+V- / C+B- -static void bldc_set_phase_wp_vm(void) +// V+U- +static void bldc_set_phase_vp_um(mcpwm_gen_handle_t (*gens)[2]) { - mcpwm_deadtime_disable(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U); - mcpwm_set_signal_low(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U, BLDC_MCPWM_GEN_HIGH); // U+ = 0 - mcpwm_set_signal_low(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U, BLDC_MCPWM_GEN_LOW); // U- = 0 - mcpwm_deadtime_disable(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V); - mcpwm_set_signal_low(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V, BLDC_MCPWM_GEN_HIGH); // V+ = 0 - mcpwm_set_signal_high(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V, BLDC_MCPWM_GEN_LOW); // V- = 1 - mcpwm_set_duty_type(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W, BLDC_MCPWM_GEN_HIGH, MCPWM_DUTY_MODE_0); // W+ = PWM - mcpwm_deadtime_enable(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W, MCPWM_ACTIVE_HIGH_COMPLIMENT_MODE, 3, 3); // W- = _PWM_ + // U+ = 0, U- = 1 + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_U][BLDC_MCPWM_GEN_INDEX_HIGH], 0, true); + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_U][BLDC_MCPWM_GEN_INDEX_LOW], 1, true); + + // V+ = PWM, V- = _PWM_ + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_V][BLDC_MCPWM_GEN_INDEX_HIGH], -1, true); + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_V][BLDC_MCPWM_GEN_INDEX_LOW], -1, true); + + // W+ = 0, W- = 0 + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_W][BLDC_MCPWM_GEN_INDEX_HIGH], 0, true); + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_W][BLDC_MCPWM_GEN_INDEX_LOW], 0, true); } -// V+U- / B+A- -static void bldc_set_phase_vp_um(void) +// V+W- +static void bldc_set_phase_vp_wm(mcpwm_gen_handle_t (*gens)[2]) { - mcpwm_deadtime_disable(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U); - mcpwm_set_signal_low(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U, BLDC_MCPWM_GEN_HIGH); // U+ = 0 - mcpwm_set_signal_high(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U, BLDC_MCPWM_GEN_LOW); // U- = 1 - mcpwm_set_duty_type(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V, BLDC_MCPWM_GEN_HIGH, MCPWM_DUTY_MODE_0); // V+ = PWM - mcpwm_deadtime_enable(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V, MCPWM_ACTIVE_HIGH_COMPLIMENT_MODE, 3, 3); // V- = _PWM_ - mcpwm_deadtime_disable(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W); - mcpwm_set_signal_low(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W, BLDC_MCPWM_GEN_HIGH); // W+ = 0 - mcpwm_set_signal_low(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W, BLDC_MCPWM_GEN_LOW); // W- = 0 -} + // U+ = 0, U- = 0 + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_U][BLDC_MCPWM_GEN_INDEX_HIGH], 0, true); + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_U][BLDC_MCPWM_GEN_INDEX_LOW], 0, true); -// V+W- / B+C- -static void bldc_set_phase_vp_wm(void) -{ - mcpwm_deadtime_disable(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U); - mcpwm_set_signal_low(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U, BLDC_MCPWM_GEN_HIGH); // U+ = 0 - mcpwm_set_signal_low(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U, BLDC_MCPWM_GEN_LOW); // U- = 0 - mcpwm_set_duty_type(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V, BLDC_MCPWM_GEN_HIGH, MCPWM_DUTY_MODE_0); // V+ = PWM - mcpwm_deadtime_enable(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V, MCPWM_ACTIVE_HIGH_COMPLIMENT_MODE, 3, 3); // V- = _PWM_ - mcpwm_deadtime_disable(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W); - mcpwm_set_signal_low(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W, BLDC_MCPWM_GEN_HIGH); // W+ = 0 - mcpwm_set_signal_high(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W, BLDC_MCPWM_GEN_LOW); // W- = 1 + // V+ = PWM, V- = _PWM_ + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_V][BLDC_MCPWM_GEN_INDEX_HIGH], -1, true); + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_V][BLDC_MCPWM_GEN_INDEX_LOW], -1, true); + + // W+ = 0, W- = 1 + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_W][BLDC_MCPWM_GEN_INDEX_HIGH], 0, true); + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_W][BLDC_MCPWM_GEN_INDEX_LOW], 1, true); } // U+W- / A+C- -static void bldc_set_phase_up_wm(void) +static void bldc_set_phase_up_wm(mcpwm_gen_handle_t (*gens)[2]) { - mcpwm_set_duty_type(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U, BLDC_MCPWM_GEN_HIGH, MCPWM_DUTY_MODE_0); // U+ = PWM - mcpwm_deadtime_enable(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U, MCPWM_ACTIVE_HIGH_COMPLIMENT_MODE, 3, 3); // U- = _PWM_ - mcpwm_deadtime_disable(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V); - mcpwm_set_signal_low(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V, BLDC_MCPWM_GEN_HIGH); // V+ = 0 - mcpwm_set_signal_low(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V, BLDC_MCPWM_GEN_LOW); // V- = 0 - mcpwm_deadtime_disable(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W); - mcpwm_set_signal_low(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W, BLDC_MCPWM_GEN_HIGH); // W+ = 0 - mcpwm_set_signal_high(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W, BLDC_MCPWM_GEN_LOW); // W- = 1 -} + // U+ = PWM, U- = _PWM_ + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_U][BLDC_MCPWM_GEN_INDEX_HIGH], -1, true); + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_U][BLDC_MCPWM_GEN_INDEX_LOW], -1, true); -typedef void (*bldc_hall_phase_action_t)(void); + // V+ = 0, V- = 0 + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_V][BLDC_MCPWM_GEN_INDEX_HIGH], 0, true); + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_V][BLDC_MCPWM_GEN_INDEX_LOW], 0, true); + + // W+ = 0, W- = 1 + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_W][BLDC_MCPWM_GEN_INDEX_HIGH], 0, true); + mcpwm_generator_set_force_level(gens[BLDC_MCPWM_OP_INDEX_W][BLDC_MCPWM_GEN_INDEX_LOW], 1, true); +} static const bldc_hall_phase_action_t s_hall_actions[] = { [2] = bldc_set_phase_up_vm, @@ -161,90 +158,191 @@ static const bldc_hall_phase_action_t s_hall_actions[] = { [3] = bldc_set_phase_up_wm, }; +static void update_motor_speed_callback(void *arg) +{ + static int step = 20; + static int cur_speed = 0; + if ((cur_speed + step) > 300 || (cur_speed + step) < 0) { + step *= -1; + } + cur_speed += step; + + mcpwm_cmpr_handle_t *cmprs = (mcpwm_cmpr_handle_t *)arg; + for (int i = 0; i < 3; i++) { + ESP_ERROR_CHECK(mcpwm_comparator_set_compare_value(cmprs[i], cur_speed)); + } +} + void app_main(void) { - uint32_t hall_sensor_value = 0; - TaskHandle_t cur_task = xTaskGetCurrentTaskHandle(); - - ESP_LOGI(TAG, "Disable gate driver"); + ESP_LOGI(TAG, "Disable MOSFET gate"); gpio_config_t drv_en_config = { .mode = GPIO_MODE_OUTPUT, - .pin_bit_mask = 1 << BLDC_DRV_EN_GPIO, + .pin_bit_mask = 1ULL << BLDC_DRV_EN_GPIO, }; ESP_ERROR_CHECK(gpio_config(&drv_en_config)); gpio_set_level(BLDC_DRV_EN_GPIO, 0); - ESP_LOGI(TAG, "Setup PWM and Hall GPIO (pull up internally)"); - mcpwm_pin_config_t mcpwm_gpio_config = { - .mcpwm0a_out_num = BLDC_PWM_UH_GPIO, - .mcpwm0b_out_num = BLDC_PWM_UL_GPIO, - .mcpwm1a_out_num = BLDC_PWM_VH_GPIO, - .mcpwm1b_out_num = BLDC_PWM_VL_GPIO, - .mcpwm2a_out_num = BLDC_PWM_WH_GPIO, - .mcpwm2b_out_num = BLDC_PWM_WL_GPIO, - .mcpwm_cap0_in_num = HALL_CAP_U_GPIO, - .mcpwm_cap1_in_num = HALL_CAP_V_GPIO, - .mcpwm_cap2_in_num = HALL_CAP_W_GPIO, - .mcpwm_sync0_in_num = -1, //Not used - .mcpwm_sync1_in_num = -1, //Not used - .mcpwm_sync2_in_num = -1, //Not used - .mcpwm_fault0_in_num = BLDC_DRV_FAULT_GPIO, - .mcpwm_fault1_in_num = -1, //Not used - .mcpwm_fault2_in_num = -1 //Not used + ESP_LOGI(TAG, "Create MCPWM timer"); + mcpwm_timer_handle_t timer = NULL; + mcpwm_timer_config_t timer_config = { + .group_id = 0, + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .resolution_hz = BLDC_MCPWM_TIMER_RESOLUTION_HZ, + .count_mode = MCPWM_TIMER_COUNT_MODE_UP, + .period_ticks = BLDC_MCPWM_PERIOD, }; - ESP_ERROR_CHECK(mcpwm_set_pin(BLDC_MCPWM_GROUP, &mcpwm_gpio_config)); - // In case there's no pull-up resister for hall sensor on board - gpio_pullup_en(HALL_CAP_U_GPIO); - gpio_pullup_en(HALL_CAP_V_GPIO); - gpio_pullup_en(HALL_CAP_W_GPIO); - gpio_pullup_en(BLDC_DRV_FAULT_GPIO); + ESP_ERROR_CHECK(mcpwm_new_timer(&timer_config, &timer)); - ESP_LOGI(TAG, "Initialize PWM (default to turn off all MOSFET)"); - mcpwm_config_t pwm_config = { - .frequency = PWM_DEFAULT_FREQ, - .cmpr_a = PWM_MIN_DUTY, - .cmpr_b = PWM_MIN_DUTY, - .counter_mode = MCPWM_UP_COUNTER, - .duty_mode = MCPWM_HAL_GENERATOR_MODE_FORCE_LOW, + ESP_LOGI(TAG, "Create MCPWM operator"); + mcpwm_oper_handle_t operators[3]; + mcpwm_operator_config_t operator_config = { + .group_id = 0, }; - ESP_ERROR_CHECK(mcpwm_init(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U, &pwm_config)); - ESP_ERROR_CHECK(mcpwm_init(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V, &pwm_config)); - ESP_ERROR_CHECK(mcpwm_init(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W, &pwm_config)); + for (int i = 0; i < 3; i++) { + ESP_ERROR_CHECK(mcpwm_new_operator(&operator_config, &operators[i])); + } - ESP_LOGI(TAG, "Initialize over current fault action"); - ESP_ERROR_CHECK(mcpwm_fault_init(BLDC_MCPWM_GROUP, MCPWM_LOW_LEVEL_TGR, BLDC_DRV_OVER_CURRENT_FAULT)); - ESP_ERROR_CHECK(mcpwm_fault_set_cyc_mode(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_U, BLDC_DRV_OVER_CURRENT_FAULT, MCPWM_ACTION_FORCE_LOW, MCPWM_ACTION_FORCE_LOW)); - ESP_ERROR_CHECK(mcpwm_fault_set_cyc_mode(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_V, BLDC_DRV_OVER_CURRENT_FAULT, MCPWM_ACTION_FORCE_LOW, MCPWM_ACTION_FORCE_LOW)); - ESP_ERROR_CHECK(mcpwm_fault_set_cyc_mode(BLDC_MCPWM_GROUP, BLDC_MCPWM_TIMER_W, BLDC_DRV_OVER_CURRENT_FAULT, MCPWM_ACTION_FORCE_LOW, MCPWM_ACTION_FORCE_LOW)); + ESP_LOGI(TAG, "Connect operators to the same timer"); + for (int i = 0; i < 3; i++) { + ESP_ERROR_CHECK(mcpwm_operator_connect_timer(operators[i], timer)); + } - ESP_LOGI(TAG, "Initialize Hall sensor capture"); - mcpwm_capture_config_t cap_config = { - .cap_edge = MCPWM_BOTH_EDGE, - .cap_prescale = 1, - .capture_cb = bldc_hall_updated, - .user_data = cur_task, + ESP_LOGI(TAG, "Create comparators"); + mcpwm_cmpr_handle_t comparators[3]; + mcpwm_comparator_config_t compare_config = { + .flags.update_cmp_on_tez = true, }; - ESP_ERROR_CHECK(mcpwm_capture_enable_channel(BLDC_MCPWM_GROUP, 0, &cap_config)); - ESP_ERROR_CHECK(mcpwm_capture_enable_channel(BLDC_MCPWM_GROUP, 1, &cap_config)); - ESP_ERROR_CHECK(mcpwm_capture_enable_channel(BLDC_MCPWM_GROUP, 2, &cap_config)); - ESP_LOGI(TAG, "Please turn on the motor power"); - vTaskDelay(pdMS_TO_TICKS(5000)); - ESP_LOGI(TAG, "Enable gate driver"); + for (int i = 0; i < 3; i++) { + ESP_ERROR_CHECK(mcpwm_new_comparator(operators[i], &compare_config, &comparators[i])); + // set compare value to 0, we will adjust the speed in a period timer callback + ESP_ERROR_CHECK(mcpwm_comparator_set_compare_value(comparators[i], 0)); + } + + ESP_LOGI(TAG, "Create over current fault detector"); + mcpwm_fault_handle_t over_cur_fault = NULL; + mcpwm_gpio_fault_config_t gpio_fault_config = { + .gpio_num = BLDC_DRV_FAULT_GPIO, + .group_id = 0, + .flags.active_level = 0, // low level means fault, refer to DRV8302 datasheet + .flags.pull_up = true, // internally pull up + }; + ESP_ERROR_CHECK(mcpwm_new_gpio_fault(&gpio_fault_config, &over_cur_fault)); + + ESP_LOGI(TAG, "Set brake mode on the fault event"); + mcpwm_brake_config_t brake_config = { + .brake_mode = MCPWM_OPER_BRAKE_MODE_CBC, + .fault = over_cur_fault, + .flags.cbc_recover_on_tez = true, + }; + for (int i = 0; i < 3; i++) { + ESP_ERROR_CHECK(mcpwm_operator_set_brake_on_fault(operators[i], &brake_config)); + } + + ESP_LOGI(TAG, "Create PWM generators"); + mcpwm_gen_handle_t generators[3][2] = {}; + mcpwm_generator_config_t gen_config = {}; + const int gen_gpios[3][2] = { + {BLDC_PWM_UH_GPIO, BLDC_PWM_UL_GPIO}, + {BLDC_PWM_VH_GPIO, BLDC_PWM_VL_GPIO}, + {BLDC_PWM_WH_GPIO, BLDC_PWM_WL_GPIO}, + }; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 2; j++) { + gen_config.gen_gpio_num = gen_gpios[i][j]; + ESP_ERROR_CHECK(mcpwm_new_generator(operators[i], &gen_config, &generators[i][j])); + } + } + + ESP_LOGI(TAG, "Set generator actions"); + for (int i = 0; i < 3; i++) { + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(generators[i][BLDC_MCPWM_GEN_INDEX_HIGH], + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(generators[i][BLDC_MCPWM_GEN_INDEX_HIGH], + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, comparators[i], MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_brake_event(generators[i][BLDC_MCPWM_GEN_INDEX_HIGH], + MCPWM_GEN_BRAKE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_OPER_BRAKE_MODE_CBC, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_BRAKE_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_brake_event(generators[i][BLDC_MCPWM_GEN_INDEX_HIGH], + MCPWM_GEN_BRAKE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_OPER_BRAKE_MODE_CBC, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_BRAKE_EVENT_ACTION_END())); + } + + ESP_LOGI(TAG, "Setup deadtime"); + mcpwm_dead_time_config_t dt_config = { + .posedge_delay_ticks = 5, + }; + for (int i = 0; i < 3; i++) { + ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(generators[i][BLDC_MCPWM_GEN_INDEX_HIGH], generators[i][BLDC_MCPWM_GEN_INDEX_HIGH], &dt_config)); + } + dt_config = (mcpwm_dead_time_config_t) { + .negedge_delay_ticks = 5, + .flags.invert_output = true, + }; + for (int i = 0; i < 3; i++) { + ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(generators[i][BLDC_MCPWM_GEN_INDEX_HIGH], generators[i][BLDC_MCPWM_GEN_INDEX_LOW], &dt_config)); + } + + ESP_LOGI(TAG, "Turn off all the gates"); + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 2; j++) { + ESP_ERROR_CHECK(mcpwm_generator_set_force_level(generators[i][j], 0, true)); + } + } + + ESP_LOGI(TAG, "Create Hall sensor capture channels"); + mcpwm_cap_timer_handle_t cap_timer = NULL; + mcpwm_capture_timer_config_t cap_timer_config = { + .group_id = 0, + .clk_src = MCPWM_CAPTURE_CLK_SRC_DEFAULT, + }; + ESP_ERROR_CHECK(mcpwm_new_capture_timer(&cap_timer_config, &cap_timer)); + mcpwm_cap_channel_handle_t cap_channels[3]; + mcpwm_capture_channel_config_t cap_channel_config = { + .prescale = 1, + .flags.pull_up = true, + .flags.neg_edge = true, + .flags.pos_edge = true, + }; + const int cap_chan_gpios[3] = {HALL_CAP_U_GPIO, HALL_CAP_V_GPIO, HALL_CAP_W_GPIO}; + for (int i = 0; i < 3; i++) { + cap_channel_config.gpio_num = cap_chan_gpios[i]; + ESP_ERROR_CHECK(mcpwm_new_capture_channel(cap_timer, &cap_channel_config, &cap_channels[i])); + } + + ESP_LOGI(TAG, "Register event callback for capture channels"); + TaskHandle_t task_to_notify = xTaskGetCurrentTaskHandle(); + for (int i = 0; i < 3; i++) { + mcpwm_capture_event_callbacks_t cbs = { + .on_cap = bldc_hall_updated, + }; + ESP_ERROR_CHECK(mcpwm_capture_channel_register_event_callbacks(cap_channels[i], &cbs, task_to_notify)); + } + + ESP_LOGI(TAG, "Start a timer to adjust motor speed periodically"); + esp_timer_handle_t periodic_timer = NULL; + const esp_timer_create_args_t periodic_timer_args = { + .callback = update_motor_speed_callback, + .arg = comparators, + }; + ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &periodic_timer)); + ESP_ERROR_CHECK(esp_timer_start_periodic(periodic_timer, BLDC_SPEED_UPDATE_PERIOD_US)); + + ESP_LOGI(TAG, "Enable MOSFET gate"); gpio_set_level(BLDC_DRV_EN_GPIO, 1); - ESP_LOGI(TAG, "Changing speed at background"); - const esp_timer_create_args_t bldc_timer_args = { - .callback = update_bldc_speed, - .name = "bldc_speed" - }; - esp_timer_handle_t bldc_speed_timer; - ESP_ERROR_CHECK(esp_timer_create(&bldc_timer_args, &bldc_speed_timer)); - ESP_ERROR_CHECK(esp_timer_start_periodic(bldc_speed_timer, 2000000)); + ESP_LOGI(TAG, "Start the MCPWM timer"); + ESP_ERROR_CHECK(mcpwm_timer_enable(timer)); + ESP_ERROR_CHECK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP)); + + uint32_t hall_sensor_value = 0; while (1) { // The rotation direction is controlled by inverting the hall sensor value - hall_sensor_value = bldc_get_hall_sensor_value(false); - if (hall_sensor_value >= 1 && hall_sensor_value < sizeof(s_hall_actions) / sizeof(s_hall_actions[0])) { - s_hall_actions[hall_sensor_value](); + hall_sensor_value = bldc_get_hall_sensor_value(BLDC_SPIN_DIRECTION_CCW); + if (hall_sensor_value >= 1 && hall_sensor_value <= 6) { + s_hall_actions[hall_sensor_value](generators); } else { ESP_LOGE(TAG, "invalid bldc phase, wrong hall sensor value:%d", hall_sensor_value); } diff --git a/examples/peripherals/mcpwm/mcpwm_bldc_hall_control/pytest_bldc_hall_control.py b/examples/peripherals/mcpwm/mcpwm_bldc_hall_control/pytest_bldc_hall_control.py new file mode 100644 index 0000000000..ccc6a4e34e --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_bldc_hall_control/pytest_bldc_hall_control.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded import Dut + + +@pytest.mark.esp32s3 +@pytest.mark.generic +def test_bldc_hall_control_example(dut: Dut) -> None: + dut.expect_exact('example: Disable MOSFET gate') + dut.expect_exact('example: Create MCPWM timer') + dut.expect_exact('example: Create MCPWM operator') + dut.expect_exact('example: Connect operators to the same timer') + dut.expect_exact('example: Create comparators') + dut.expect_exact('example: Create over current fault detector') + dut.expect_exact('example: Set brake mode on the fault event') + dut.expect_exact('example: Create PWM generators') + dut.expect_exact('example: Set generator actions') + dut.expect_exact('example: Setup deadtime') + dut.expect_exact('example: Turn off all the gates') + dut.expect_exact('example: Create Hall sensor capture channels') + dut.expect_exact('example: Start a timer to adjust motor speed periodically') + dut.expect_exact('example: Enable MOSFET gate') + dut.expect_exact('example: Start the MCPWM timer') From 6751b229f11da471bc531951d7b6670f1949f420 Mon Sep 17 00:00:00 2001 From: morris Date: Sat, 28 May 2022 17:05:09 +0800 Subject: [PATCH 5/8] example: update capture example with new driver API --- .../mcpwm/mcpwm_capture_hc_sr04/README.md | 42 +++-- .../main/mcpwm_capture_hc_sr04.c | 170 +++++++++--------- .../mcpwm_capture_hc_sr04/pytest_hc_sr04.py | 16 ++ 3 files changed, 120 insertions(+), 108 deletions(-) create mode 100644 examples/peripherals/mcpwm/mcpwm_capture_hc_sr04/pytest_hc_sr04.py diff --git a/examples/peripherals/mcpwm/mcpwm_capture_hc_sr04/README.md b/examples/peripherals/mcpwm/mcpwm_capture_hc_sr04/README.md index 4df1e96735..941d25a886 100644 --- a/examples/peripherals/mcpwm/mcpwm_capture_hc_sr04/README.md +++ b/examples/peripherals/mcpwm/mcpwm_capture_hc_sr04/README.md @@ -1,14 +1,15 @@ | Supported Targets | ESP32 | ESP32-S3 | | ----------------- | ----- | -------- | -# HC-SR04 Example +# HC-SR04 Example based on MCPWM Capture (See the README.md file in the upper level 'examples' directory for more information about examples.) -The capture module in MCPWM peripheral is designed to accurately log the time stamp on the hardware side when an event happens (compared to GPIO ISR which requires a software-based logging method). Each capture unit has three channels, which can be used together to capture IO events parallelly. -This example shows how to make use of the HW features to decode the pulse width signals generated from a common HC-SR04 sonar range finder -- [HC-SR04](https://www.sparkfun.com/products/15569). +The capture module in MCPWM peripheral is designed to accurately log the time stamp on the hardware side when an event happens (compared to GPIO ISR which requires a software-based logging method). Each capture unit has three channels, which can be used together to capture IO events in parallel. -The signal that HC-SR04 produces (and what can be handled by this example) is a simple pulse whose width indicates the measured distance. A pulse is required to send to HC-SR04 on `Trig` pin to begin a new measurement. Then the pulse described above will be sent back on `Echo` pin for decoding. +This example shows how to make use of the hardware features to decode the pulse width signals generated from a common HC-SR04 sonar sensor -- [HC-SR04](https://www.sparkfun.com/products/15569). + +The signal that HC-SR04 produces (and what can be handled by this example) is a simple pulse whose width indicates the measured distance. An excitation pulse is required to send to HC-SR04 on `Trig` pin to begin a new measurement. Then the pulse described above will appear on the `Echo` pin after a while. Typical signals: @@ -30,8 +31,8 @@ Echo +-----+ ### Hardware Required -* An ESP development board -* HC-SR04 module +* An ESP development board that features the MCPWM peripheral +* An HC-SR04 sensor module Connection : @@ -60,21 +61,24 @@ See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/l ## Example Output ``` -I (314) hc-sr04: HC-SR04 example based on capture function from MCPWM -I (324) hc-sr04: Echo pin configured -I (324) gpio: GPIO[19]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 -I (334) hc-sr04: Trig pin configured -I (344) hc-sr04: trig task started -I (444) hc-sr04: Pulse width: 419us, Measured distance: 7.22cm -I (544) hc-sr04: Pulse width: 419us, Measured distance: 7.22cm -I (644) hc-sr04: Pulse width: 416us, Measured distance: 7.17cm -I (744) hc-sr04: Pulse width: 415us, Measured distance: 7.16cm -I (844) hc-sr04: Pulse width: 415us, Measured distance: 7.16cm -I (944) hc-sr04: Pulse width: 416us, Measured distance: 7.17cm -I (1044) hc-sr04: Pulse width: 391us, Measured distance: 6.74cm +I (0) cpu_start: Starting scheduler on APP CPU. +I (304) example: Create capture queue +I (304) example: Install capture timer +I (304) example: Install capture channel +I (314) gpio: GPIO[2]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (324) example: Register capture callback +I (324) example: Create a timer to trig HC_SR04 periodically +I (334) example: Configure Trig pin +I (334) gpio: GPIO[0]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 +I (344) example: Enable and start capture timer +I (434) example: Pulse width: 189.02us, Measured distance: 3.26cm +I (534) example: Pulse width: 189.02us, Measured distance: 3.26cm +I (634) example: Pulse width: 189.01us, Measured distance: 3.26cm +I (734) example: Pulse width: 188.98us, Measured distance: 3.26cm +I (834) example: Pulse width: 188.99us, Measured distance: 3.26cm ``` -This example runs at 10Hz sampling rate. out of range data is dropped and only valid measurement is printed. +This example runs at 10Hz sampling rate. Measure data that out of the range is dropped and only valid measurement is printed out. ## Troubleshooting diff --git a/examples/peripherals/mcpwm/mcpwm_capture_hc_sr04/main/mcpwm_capture_hc_sr04.c b/examples/peripherals/mcpwm/mcpwm_capture_hc_sr04/main/mcpwm_capture_hc_sr04.c index e7fefeb6d2..b18fbfe81e 100644 --- a/examples/peripherals/mcpwm/mcpwm_capture_hc_sr04/main/mcpwm_capture_hc_sr04.c +++ b/examples/peripherals/mcpwm/mcpwm_capture_hc_sr04/main/mcpwm_capture_hc_sr04.c @@ -6,119 +6,111 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" -#include "freertos/queue.h" #include "esp_log.h" #include "esp_private/esp_clk.h" -#include "driver/mcpwm.h" +#include "driver/mcpwm_cap.h" #include "driver/gpio.h" -const static char *TAG = "hc-sr04"; +const static char *TAG = "example"; -#define HC_SR04_SAMPLE_PERIOD_MS 100 -_Static_assert(HC_SR04_SAMPLE_PERIOD_MS > 50, "Sample period too short!"); -#define HC_SR04_PIN_ECHO GPIO_NUM_18 -#define HC_SR04_PIN_TRIG GPIO_NUM_19 +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////// Please update the following configuration according to your board spec //////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#define HC_SR04_TRIG_GPIO 0 +#define HC_SR04_ECHO_GPIO 2 -#define TRIGGER_THREAD_STACK_SIZE 512 -#define TRIGGER_THREAD_PRIORITY 5 - -typedef struct { - uint32_t capture_signal; - mcpwm_capture_signal_t sel_cap_signal; -} capture; - -static uint32_t cap_val_begin_of_sample = 0; -static uint32_t cap_val_end_of_sample = 0; - -static QueueHandle_t cap_queue; - -/** - * @brief generate single pulse on Trig pin to activate a new sample - */ -static void gen_trig_output(void *arg) { - TickType_t xLastWakeTime = xTaskGetTickCount(); - while (true) { - vTaskDelayUntil(&xLastWakeTime, HC_SR04_SAMPLE_PERIOD_MS / portTICK_PERIOD_MS); - ESP_ERROR_CHECK(gpio_set_level(HC_SR04_PIN_TRIG, 1)); // set high - esp_rom_delay_us(10); - ESP_ERROR_CHECK(gpio_set_level(HC_SR04_PIN_TRIG, 0)); // set low - } -} - -/** - * @brief this is an ISR callback, we take action according to the captured edge - */ -static bool sr04_echo_isr_handler(mcpwm_unit_t mcpwm, mcpwm_capture_channel_id_t cap_sig, const cap_event_data_t *edata, - void *arg) { - //calculate the interval in the ISR, - //so that the interval will be always correct even when cap_queue is not handled in time and overflow. +static bool hc_sr04_echo_callback(mcpwm_cap_channel_handle_t cap_chan, const mcpwm_capture_event_data_t *edata, void *user_data) +{ + static uint32_t cap_val_begin_of_sample = 0; + static uint32_t cap_val_end_of_sample = 0; + TaskHandle_t task_to_notify = (TaskHandle_t)user_data; BaseType_t high_task_wakeup = pdFALSE; - if (edata->cap_edge == MCPWM_POS_EDGE) { + + //calculate the interval in the ISR, + //so that the interval will be always correct even when capture_queue is not handled in time and overflow. + if (edata->cap_edge == MCPWM_CAP_EDGE_POS) { // store the timestamp when pos edge is detected cap_val_begin_of_sample = edata->cap_value; cap_val_end_of_sample = cap_val_begin_of_sample; } else { cap_val_end_of_sample = edata->cap_value; - // following formula refers to: https://www.elecrow.com/download/HC_SR04%20Datasheet.pdf - uint32_t pulse_count = cap_val_end_of_sample - cap_val_begin_of_sample; - // send measurement back though queue - xQueueSendFromISR(cap_queue, &pulse_count, &high_task_wakeup); + uint32_t tof_ticks = cap_val_end_of_sample - cap_val_begin_of_sample; + + // notify the task to calculate the distance + xTaskNotifyFromISR(task_to_notify, tof_ticks, eSetValueWithOverwrite, &high_task_wakeup); } + return high_task_wakeup == pdTRUE; } -void app_main(void) { - ESP_LOGI(TAG, "HC-SR04 example based on capture function from MCPWM"); +/** + * @brief generate single pulse on Trig pin to start a new sample + */ +static void gen_trig_output(void) +{ + gpio_set_level(HC_SR04_TRIG_GPIO, 1); // set high + esp_rom_delay_us(10); + gpio_set_level(HC_SR04_TRIG_GPIO, 0); // set low +} - // the queue where we read data - cap_queue = xQueueCreate(1, sizeof(uint32_t)); - if (cap_queue == NULL) { - ESP_LOGE(TAG, "failed to alloc cap_queue"); - return; - } - - /* configure Echo pin */ - // set CAP_0 on GPIO - ESP_ERROR_CHECK(mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM_CAP_0, HC_SR04_PIN_ECHO)); - // enable pull down CAP0, to reduce noise - ESP_ERROR_CHECK(gpio_pulldown_en(HC_SR04_PIN_ECHO)); - // enable both edge capture on CAP0 - mcpwm_capture_config_t conf = { - .cap_edge = MCPWM_BOTH_EDGE, - .cap_prescale = 1, - .capture_cb = sr04_echo_isr_handler, - .user_data = NULL +void app_main(void) +{ + ESP_LOGI(TAG, "Install capture timer"); + mcpwm_cap_timer_handle_t cap_timer = NULL; + mcpwm_capture_timer_config_t cap_conf = { + .clk_src = MCPWM_CAPTURE_CLK_SRC_DEFAULT, + .group_id = 0, }; - ESP_ERROR_CHECK(mcpwm_capture_enable_channel(MCPWM_UNIT_0, MCPWM_SELECT_CAP0, &conf)); - ESP_LOGI(TAG, "Echo pin configured"); + ESP_ERROR_CHECK(mcpwm_new_capture_timer(&cap_conf, &cap_timer)); - /* configure Trig pin */ + ESP_LOGI(TAG, "Install capture channel"); + mcpwm_cap_channel_handle_t cap_chan = NULL; + mcpwm_capture_channel_config_t cap_ch_conf = { + .gpio_num = HC_SR04_ECHO_GPIO, + .prescale = 1, + // capture on both edge + .flags.neg_edge = true, + .flags.pos_edge = true, + // pull up internally + .flags.pull_up = true, + }; + ESP_ERROR_CHECK(mcpwm_new_capture_channel(cap_timer, &cap_ch_conf, &cap_chan)); + + ESP_LOGI(TAG, "Register capture callback"); + TaskHandle_t cur_task = xTaskGetCurrentTaskHandle(); + mcpwm_capture_event_callbacks_t cbs = { + .on_cap = hc_sr04_echo_callback, + }; + ESP_ERROR_CHECK(mcpwm_capture_channel_register_event_callbacks(cap_chan, &cbs, cur_task)); + + ESP_LOGI(TAG, "Configure Trig pin"); gpio_config_t io_conf = { - .intr_type = GPIO_INTR_DISABLE, .mode = GPIO_MODE_OUTPUT, - .pull_down_en = GPIO_PULLDOWN_DISABLE, - .pull_up_en = GPIO_PULLUP_DISABLE, - .pin_bit_mask = BIT64(HC_SR04_PIN_TRIG), + .pin_bit_mask = 1ULL << HC_SR04_TRIG_GPIO, }; ESP_ERROR_CHECK(gpio_config(&io_conf)); - ESP_ERROR_CHECK(gpio_set_level(HC_SR04_PIN_TRIG, 0)); // drive low by default - ESP_LOGI(TAG, "Trig pin configured"); + // drive low by default + ESP_ERROR_CHECK(gpio_set_level(HC_SR04_TRIG_GPIO, 0)); - // start generating trig signal - xTaskCreate(gen_trig_output, "gen_trig_output", TRIGGER_THREAD_STACK_SIZE, NULL, TRIGGER_THREAD_PRIORITY, NULL); - ESP_LOGI(TAG, "trig task started"); - // forever loop - while (true) { - uint32_t pulse_count; - // block and wait for new measurement - xQueueReceive(cap_queue, &pulse_count, portMAX_DELAY); - uint32_t pulse_width_us = pulse_count * (1000000.0 / esp_clk_apb_freq()); - // following formula is based on: https://www.elecrow.com/download/HC_SR04%20Datasheet.pdf - if (pulse_width_us > 35000) { - // out of range - continue; + ESP_LOGI(TAG, "Enable and start capture timer"); + ESP_ERROR_CHECK(mcpwm_capture_timer_enable(cap_timer)); + ESP_ERROR_CHECK(mcpwm_capture_timer_start(cap_timer)); + + uint32_t tof_ticks; + while (1) { + // trigger the sensor to start a new sample + gen_trig_output(); + // wait for echo done signal + if (xTaskNotifyWait(0x00, ULONG_MAX, &tof_ticks, pdMS_TO_TICKS(1000)) == pdTRUE) { + float pulse_width_us = tof_ticks * (1000000.0 / esp_clk_apb_freq()); + if (pulse_width_us > 35000) { + // out of range + continue; + } + // convert the pulse width into measure distance + float distance = (float) pulse_width_us / 58; + ESP_LOGI(TAG, "Measured distance: %.2fcm", distance); } - float distance = (float) pulse_width_us / 58; - ESP_LOGI(TAG, "Pulse width: %uus, Measured distance: %.2fcm", pulse_width_us, distance); + vTaskDelay(pdMS_TO_TICKS(500)); } } diff --git a/examples/peripherals/mcpwm/mcpwm_capture_hc_sr04/pytest_hc_sr04.py b/examples/peripherals/mcpwm/mcpwm_capture_hc_sr04/pytest_hc_sr04.py new file mode 100644 index 0000000000..6e519dff82 --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_capture_hc_sr04/pytest_hc_sr04.py @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded import Dut + + +@pytest.mark.esp32 +@pytest.mark.esp32s3 +@pytest.mark.generic +def test_hc_sr04_example(dut: Dut) -> None: + dut.expect_exact('example: Install capture timer') + dut.expect_exact('example: Install capture channel') + dut.expect_exact('example: Register capture callback') + dut.expect_exact('example: Configure Trig pin') + dut.expect_exact('example: Enable and start capture timer') From 1557a533fe881d1766ac1cd7c18c548e06a4e7d0 Mon Sep 17 00:00:00 2001 From: morris Date: Sat, 28 May 2022 17:05:36 +0800 Subject: [PATCH 6/8] example: update servo example with new driver API --- .../mcpwm/mcpwm_servo_control/README.md | 40 ++++--- .../main/mcpwm_servo_control_example_main.c | 100 +++++++++++++----- .../pytest_servo_mg996r.py | 17 +++ tools/ci/check_copyright_ignore.txt | 1 - 4 files changed, 115 insertions(+), 43 deletions(-) create mode 100644 examples/peripherals/mcpwm/mcpwm_servo_control/pytest_servo_mg996r.py diff --git a/examples/peripherals/mcpwm/mcpwm_servo_control/README.md b/examples/peripherals/mcpwm/mcpwm_servo_control/README.md index cc174b1b1f..c25d811e51 100644 --- a/examples/peripherals/mcpwm/mcpwm_servo_control/README.md +++ b/examples/peripherals/mcpwm/mcpwm_servo_control/README.md @@ -4,7 +4,7 @@ (See the README.md file in the upper level 'examples' directory for more information about examples.) -This example illustrates how to drive a typical [RC Servo](https://en.wikipedia.org/wiki/Servo_%28radio_control%29) by sending a PWM signal using the MCPWM driver. The PWM pulse has a frequency of 50Hz (period of 20ms), and the active-high time (which controls the rotation) ranges from 1ms to 2ms with 1.5ms always being center of range. +This example illustrates how to drive a typical [RC Servo](https://en.wikipedia.org/wiki/Servo_%28radio_control%29) by sending a PWM signal using the MCPWM driver. The PWM pulse has a frequency of 50Hz (period of 20ms), and the active-high time (which controls the rotation) ranges from 0.5s to 2.5ms with 1.5ms always being center of range. ## How to Use Example @@ -17,15 +17,16 @@ This example illustrates how to drive a typical [RC Servo](https://en.wikipedia. Connection : ``` -+-------+ +-----------------+ -| | | | -| +-+ GPIO18++ PWM(Orange) +----------+ | -| ESP |---5V------+ Vcc(Red) +--------------| Servo Motor | -| +---------+ GND(Brown) +----------+ | -| | | | -+-------+ +-----------------+ + ESP Board Servo Motor 5V ++-------------------+ +---------------+ ^ +| SERVO_PULSE_GPIO +-----+PWM VCC +----+ +| | | | +| GND +-----+GND | ++-------------------+ +---------------+ ``` +Note that, some kind of servo might need a higher current supply than the development board usually can provide. It's recommended to power the servo separately. + ### Build and Flash Run `idf.py -p PORT flash monitor` to build, flash and monitor the project. @@ -42,11 +43,22 @@ Run the example, you will see the following output log: ``` ... I (0) cpu_start: Starting scheduler on APP CPU. -I (349) example: Angle of rotation: -90 -I (449) example: Angle of rotation: -89 -I (549) example: Angle of rotation: -88 -I (649) example: Angle of rotation: -87 -I (749) example: Angle of rotation: -86 +I (305) example: Create timer and operator +I (305) example: Connect timer and operator +I (305) example: Create comparator and generator from the operator +I (315) gpio: GPIO[44]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (325) example: Set generator action on timer and compare event +I (335) example: Enable and start timer +I (335) example: Angle of rotation: 0 +I (1345) example: Angle of rotation: 2 +I (2345) example: Angle of rotation: 4 +I (3345) example: Angle of rotation: 6 +I (4345) example: Angle of rotation: 8 +I (5345) example: Angle of rotation: 10 +I (6345) example: Angle of rotation: 12 +I (7345) example: Angle of rotation: 14 +I (8345) example: Angle of rotation: 16 +I (9345) example: Angle of rotation: 18 ... ``` @@ -54,6 +66,4 @@ The servo will rotate from -90 degree to 90 degree, and then turn back again. ## Troubleshooting -Note that, some kind of servo might need a higher current supply than the development board usually can provide. It's recommended to power the servo separately. - For any technical queries, please open an [issue] (https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon. diff --git a/examples/peripherals/mcpwm/mcpwm_servo_control/main/mcpwm_servo_control_example_main.c b/examples/peripherals/mcpwm/mcpwm_servo_control/main/mcpwm_servo_control_example_main.c index 0c11a6db84..c905eeafc8 100644 --- a/examples/peripherals/mcpwm/mcpwm_servo_control/main/mcpwm_servo_control_example_main.c +++ b/examples/peripherals/mcpwm/mcpwm_servo_control/main/mcpwm_servo_control_example_main.c @@ -1,47 +1,93 @@ -/* Servo Motor control example +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ - This example code is in the Public Domain (or CC0 licensed, at your option.) - - Unless required by applicable law or agreed to in writing, this - software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. -*/ #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_log.h" -#include "driver/mcpwm.h" +#include "driver/mcpwm_prelude.h" static const char *TAG = "example"; -// You can get these value from the datasheet of servo you use, in general pulse width varies between 1000 to 2000 mocrosecond -#define SERVO_MIN_PULSEWIDTH_US (1000) // Minimum pulse width in microsecond -#define SERVO_MAX_PULSEWIDTH_US (2000) // Maximum pulse width in microsecond -#define SERVO_MAX_DEGREE (90) // Maximum angle in degree upto which servo can rotate +// Please consult the datasheet of your servo before changing the following parameters +#define SERVO_MIN_PULSEWIDTH_US 500 // Minimum pulse width in microsecond +#define SERVO_MAX_PULSEWIDTH_US 2500 // Maximum pulse width in microsecond +#define SERVO_MIN_DEGREE -90 // Minimum angle +#define SERVO_MAX_DEGREE 90 // Maximum angle -#define SERVO_PULSE_GPIO (18) // GPIO connects to the PWM signal line +#define SERVO_PULSE_GPIO 0 // GPIO connects to the PWM signal line +#define SERVO_TIMEBASE_RESOLUTION_HZ 1000000 // 1MHz, 1us per tick +#define SERVO_TIMEBASE_PERIOD 20000 // 20000 ticks, 20ms -static inline uint32_t example_convert_servo_angle_to_duty_us(int angle) +static inline uint32_t example_angle_to_compare(int angle) { - return (angle + SERVO_MAX_DEGREE) * (SERVO_MAX_PULSEWIDTH_US - SERVO_MIN_PULSEWIDTH_US) / (2 * SERVO_MAX_DEGREE) + SERVO_MIN_PULSEWIDTH_US; + return (angle - SERVO_MIN_DEGREE) * (SERVO_MAX_PULSEWIDTH_US - SERVO_MIN_PULSEWIDTH_US) / (SERVO_MAX_DEGREE - SERVO_MIN_DEGREE) + SERVO_MIN_PULSEWIDTH_US; } void app_main(void) { - mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM0A, SERVO_PULSE_GPIO); // To drive a RC servo, one MCPWM generator is enough - - mcpwm_config_t pwm_config = { - .frequency = 50, // frequency = 50Hz, i.e. for every servo motor time period should be 20ms - .cmpr_a = 0, // duty cycle of PWMxA = 0 - .counter_mode = MCPWM_UP_COUNTER, - .duty_mode = MCPWM_DUTY_MODE_0, + ESP_LOGI(TAG, "Create timer and operator"); + mcpwm_timer_handle_t timer = NULL; + mcpwm_timer_config_t timer_config = { + .group_id = 0, + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .resolution_hz = SERVO_TIMEBASE_RESOLUTION_HZ, + .period_ticks = SERVO_TIMEBASE_PERIOD, + .count_mode = MCPWM_TIMER_COUNT_MODE_UP, }; - mcpwm_init(MCPWM_UNIT_0, MCPWM_TIMER_0, &pwm_config); + ESP_ERROR_CHECK(mcpwm_new_timer(&timer_config, &timer)); + mcpwm_oper_handle_t operator = NULL; + mcpwm_operator_config_t operator_config = { + .group_id = 0, // operator must be in the same group to the timer + }; + ESP_ERROR_CHECK(mcpwm_new_operator(&operator_config, &operator)); + + ESP_LOGI(TAG, "Connect timer and operator"); + ESP_ERROR_CHECK(mcpwm_operator_connect_timer(operator, timer)); + + ESP_LOGI(TAG, "Create comparator and generator from the operator"); + mcpwm_cmpr_handle_t comparator = NULL; + mcpwm_comparator_config_t comparator_config = { + .flags.update_cmp_on_tez = true, + }; + ESP_ERROR_CHECK(mcpwm_new_comparator(operator, &comparator_config, &comparator)); + + mcpwm_gen_handle_t generator = NULL; + mcpwm_generator_config_t generator_config = { + .gen_gpio_num = SERVO_PULSE_GPIO, + }; + ESP_ERROR_CHECK(mcpwm_new_generator(operator, &generator_config, &generator)); + + // set the initial compare value, so that the servo will spin to the center position + ESP_ERROR_CHECK(mcpwm_comparator_set_compare_value(comparator, example_angle_to_compare(0))); + + ESP_LOGI(TAG, "Set generator action on timer and compare event"); + // go high on counter empty + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(generator, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + // go low on compare threshold + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(generator, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, comparator, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + + ESP_LOGI(TAG, "Enable and start timer"); + ESP_ERROR_CHECK(mcpwm_timer_enable(timer)); + ESP_ERROR_CHECK(mcpwm_timer_start_stop(timer, MCPWM_TIMER_START_NO_STOP)); + + int angle = 0; + int step = 2; while (1) { - for (int angle = -SERVO_MAX_DEGREE; angle < SERVO_MAX_DEGREE; angle++) { - ESP_LOGI(TAG, "Angle of rotation: %d", angle); - ESP_ERROR_CHECK(mcpwm_set_duty_in_us(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_A, example_convert_servo_angle_to_duty_us(angle))); - vTaskDelay(pdMS_TO_TICKS(100)); //Add delay, since it takes time for servo to rotate, generally 100ms/60degree rotation under 5V power supply + ESP_LOGI(TAG, "Angle of rotation: %d", angle); + ESP_ERROR_CHECK(mcpwm_comparator_set_compare_value(comparator, example_angle_to_compare(angle))); + //Add delay, since it takes time for servo to rotate, usually 200ms/60degree rotation under 5V power supply + vTaskDelay(pdMS_TO_TICKS(500)); + if ((angle + step) > 60 || (angle + step) < -60) { + step *= -1; } + angle += step; } } diff --git a/examples/peripherals/mcpwm/mcpwm_servo_control/pytest_servo_mg996r.py b/examples/peripherals/mcpwm/mcpwm_servo_control/pytest_servo_mg996r.py new file mode 100644 index 0000000000..d22aa363d5 --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_servo_control/pytest_servo_mg996r.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded import Dut + + +@pytest.mark.esp32 +@pytest.mark.esp32s3 +@pytest.mark.generic +def test_servo_mg996r_example(dut: Dut) -> None: + dut.expect_exact('example: Create timer and operator') + dut.expect_exact('example: Connect timer and operator') + dut.expect_exact('example: Create comparator and generator from the operator') + dut.expect_exact('example: Set generator action on timer and compare event') + dut.expect_exact('example: Enable and start timer') + dut.expect_exact('example: Angle of rotation: 0') diff --git a/tools/ci/check_copyright_ignore.txt b/tools/ci/check_copyright_ignore.txt index e13723ab90..40f6d52873 100644 --- a/tools/ci/check_copyright_ignore.txt +++ b/tools/ci/check_copyright_ignore.txt @@ -1773,7 +1773,6 @@ examples/peripherals/i2c/i2c_tools/main/cmd_i2ctools.h examples/peripherals/i2c/i2c_tools/main/i2ctools_example_main.c examples/peripherals/ledc/ledc_basic/main/ledc_basic_example_main.c examples/peripherals/ledc/ledc_fade/main/ledc_fade_example_main.c -examples/peripherals/mcpwm/mcpwm_servo_control/main/mcpwm_servo_control_example_main.c examples/peripherals/sdio/host/main/app_main.c examples/peripherals/sdio/sdio_test.py examples/peripherals/sdio/slave/main/app_main.c From 169a43b8ebc3ee0d7f15036d2d8e46db360308fd Mon Sep 17 00:00:00 2001 From: morris Date: Sat, 28 May 2022 17:06:06 +0800 Subject: [PATCH 7/8] example: update MCPWM sync example with new driver API --- .../CMakeLists.txt | 2 +- .../peripherals/mcpwm/mcpwm_sync/README.md | 88 ++++++++ .../mcpwm/mcpwm_sync/main/CMakeLists.txt | 2 + .../mcpwm/mcpwm_sync/main/Kconfig.projbuild | 20 ++ .../mcpwm_sync/main/mcpwm_sync_example_main.c | 211 ++++++++++++++++++ .../mcpwm/mcpwm_sync/pytest_mcpwm_sync.py | 24 ++ .../mcpwm/mcpwm_sync/sdkconfig.ci.gpio | 1 + .../mcpwm/mcpwm_sync/sdkconfig.ci.soft | 1 + .../mcpwm/mcpwm_sync/sdkconfig.ci.tez | 1 + .../mcpwm/mcpwm_sync_example/README.md | 74 ------ .../mcpwm_sync_example/main/CMakeLists.txt | 2 - .../main/mcpwm_sync_example.c | 146 ------------ .../mcpwm_sync_example/readme_res/overall.png | Bin 35797 -> 0 bytes .../readme_res/sync_phase.png | Bin 10151 -> 0 bytes .../mcpwm_sync_example/readme_res/synced.png | Bin 16120 -> 0 bytes 15 files changed, 349 insertions(+), 223 deletions(-) rename examples/peripherals/mcpwm/{mcpwm_sync_example => mcpwm_sync}/CMakeLists.txt (88%) create mode 100644 examples/peripherals/mcpwm/mcpwm_sync/README.md create mode 100644 examples/peripherals/mcpwm/mcpwm_sync/main/CMakeLists.txt create mode 100644 examples/peripherals/mcpwm/mcpwm_sync/main/Kconfig.projbuild create mode 100644 examples/peripherals/mcpwm/mcpwm_sync/main/mcpwm_sync_example_main.c create mode 100644 examples/peripherals/mcpwm/mcpwm_sync/pytest_mcpwm_sync.py create mode 100644 examples/peripherals/mcpwm/mcpwm_sync/sdkconfig.ci.gpio create mode 100644 examples/peripherals/mcpwm/mcpwm_sync/sdkconfig.ci.soft create mode 100644 examples/peripherals/mcpwm/mcpwm_sync/sdkconfig.ci.tez delete mode 100644 examples/peripherals/mcpwm/mcpwm_sync_example/README.md delete mode 100644 examples/peripherals/mcpwm/mcpwm_sync_example/main/CMakeLists.txt delete mode 100644 examples/peripherals/mcpwm/mcpwm_sync_example/main/mcpwm_sync_example.c delete mode 100644 examples/peripherals/mcpwm/mcpwm_sync_example/readme_res/overall.png delete mode 100644 examples/peripherals/mcpwm/mcpwm_sync_example/readme_res/sync_phase.png delete mode 100644 examples/peripherals/mcpwm/mcpwm_sync_example/readme_res/synced.png diff --git a/examples/peripherals/mcpwm/mcpwm_sync_example/CMakeLists.txt b/examples/peripherals/mcpwm/mcpwm_sync/CMakeLists.txt similarity index 88% rename from examples/peripherals/mcpwm/mcpwm_sync_example/CMakeLists.txt rename to examples/peripherals/mcpwm/mcpwm_sync/CMakeLists.txt index ba6c087f5d..7bfcd825e0 100644 --- a/examples/peripherals/mcpwm/mcpwm_sync_example/CMakeLists.txt +++ b/examples/peripherals/mcpwm/mcpwm_sync/CMakeLists.txt @@ -3,4 +3,4 @@ cmake_minimum_required(VERSION 3.16) include($ENV{IDF_PATH}/tools/cmake/project.cmake) -project(mcpwm_sync_example) +project(mcpwm_sync) diff --git a/examples/peripherals/mcpwm/mcpwm_sync/README.md b/examples/peripherals/mcpwm/mcpwm_sync/README.md new file mode 100644 index 0000000000..983731e2d0 --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_sync/README.md @@ -0,0 +1,88 @@ +| Supported Targets | ESP32 | ESP32-S3 | +| ----------------- | ----- | -------- | + +# MCPWM Sync Example + +(See the README.md file in the upper level 'examples' directory for more information about examples.) + +MCPWM timers can't be started together because you have to call **mcpwm_timer_start_stop** function timer by timer, so the generators driven by them are not in sync with each other. But there're several ways to force these timers jump to the same point by setting sync phase for timers and then wait for a proper sync event to happen. + +This example illustrates how to generate three PWMs that are in perfect synchronization. + +## How to Use Example + +### Hardware Required + +* A development board with any Espressif SoC which features MCPWM peripheral (e.g., ESP32-DevKitC, ESP-WROVER-KIT, etc.) +* A USB cable for Power supply and programming + +It is recommended to have an oscilloscope or logic analyzer to view the generated PWM waveforms. + +Connection : + +``` + ESP Board oscilloscope / logic analyzer ++--------------------------+ +----------------------------+ +| | | | +| EXAMPLE_GEN_GPIO0 +-----------------+ channelA | +| | | | +| EXAMPLE_GEN_GPIO1 +-----------------+ channelB | +| | | | +| EXAMPLE_GEN_GPIO2 +-----------------+ channelC | +| | | | +| GND +-----------------+ GND | +| | | | ++--------------------------+ +----------------------------+ +``` + +Above used GPIO numbers (e.g. `EXAMPLE_GEN_GPIO0`) can be changed in [the source file](main/mcpwm_sync_example_main.c). + +### Configure the project + +``` +idf.py menuconfig +``` + +You can select the way to synchronize the MCPWM timers in the menu: `Example Configuration` -> `Where the sync event is generated from`. + +* GPIO + * This approach will consume a GPIO, where a configurable pulse on the GPIO is treated as the sync event. And the sync event is routed to each MCPWM timers. +* Timer TEZ + * This approach won't consume any GPIO, the sync even is generated by a main timer, and then routed to other MCPWM timers. The drawback of this approach is, the main timer will have a tiny phase shift to other two timers. +* Software (optional) + * This approach won't consume any GPIO as well and also doesn't have the drawback in the `Timer TEZ` approach. The main timer is sync by software, and it will propagate the sync event to other timers. + +### Build and Flash + +Run `idf.py -p PORT flash monitor` to build, flash and monitor the project. + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +``` +I (0) cpu_start: Starting scheduler on APP CPU. +I (305) example: Create timers +I (305) example: Create operators +I (305) example: Connect timers and operators with each other +I (315) example: Create comparators +I (315) example: Create generators +I (325) gpio: GPIO[0]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (335) gpio: GPIO[2]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (345) gpio: GPIO[4]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (355) example: Set generator actions on timer and compare event +I (355) example: Start timers one by one, so they are not synced +I (495) example: Force the output level to low, timer still running +I (495) example: Setup sync strategy +I (495) example: Create GPIO sync source +I (495) gpio: GPIO[5]| InputEn: 1| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 1| Intr:0 +I (505) example: Set timers to sync on the GPIO +I (505) example: Trigger a pulse on the GPIO as a sync event +I (515) example: Now the output PWMs should in sync +``` + +## Troubleshooting + +For any technical queries, please open an [issue] (https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon. diff --git a/examples/peripherals/mcpwm/mcpwm_sync/main/CMakeLists.txt b/examples/peripherals/mcpwm/mcpwm_sync/main/CMakeLists.txt new file mode 100644 index 0000000000..cdf25dbaf1 --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_sync/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "mcpwm_sync_example_main.c" + INCLUDE_DIRS ".") diff --git a/examples/peripherals/mcpwm/mcpwm_sync/main/Kconfig.projbuild b/examples/peripherals/mcpwm/mcpwm_sync/main/Kconfig.projbuild new file mode 100644 index 0000000000..3bb0fe9fee --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_sync/main/Kconfig.projbuild @@ -0,0 +1,20 @@ +menu "Example Configuration" + + choice EXAMPLE_SYNC_FROM + prompt "Where the sync event is generated from" + default EXAMPLE_SYNC_FROM_GPIO + help + Select MCPWM sync source. + + config EXAMPLE_SYNC_FROM_GPIO + bool "GPIO" + + config EXAMPLE_SYNC_FROM_TEZ + bool "Timer TEZ" + + config EXAMPLE_SYNC_FROM_SOFT + bool "Software" + depends on SOC_MCPWM_SWSYNC_CAN_PROPAGATE + endchoice + +endmenu diff --git a/examples/peripherals/mcpwm/mcpwm_sync/main/mcpwm_sync_example_main.c b/examples/peripherals/mcpwm/mcpwm_sync/main/mcpwm_sync_example_main.c new file mode 100644 index 0000000000..e68bb3c96b --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_sync/main/mcpwm_sync_example_main.c @@ -0,0 +1,211 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include "sdkconfig.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "driver/mcpwm_prelude.h" +#include "driver/gpio.h" + +const static char *TAG = "example"; + +#define EXAMPLE_TIMER_RESOLUTION_HZ 1000000 // 1MHz, 1us per tick +#define EXAMPLE_TIMER_PERIOD 1000 // 1000 ticks, 1ms +#define EXAMPLE_GEN_GPIO0 0 +#define EXAMPLE_GEN_GPIO1 2 +#define EXAMPLE_GEN_GPIO2 4 +#define EXAMPLE_SYNC_GPIO 5 + +#if CONFIG_EXAMPLE_SYNC_FROM_GPIO +static void example_setup_sync_strategy(mcpwm_timer_handle_t timers[]) +{ + // +----GPIO----+ + // | | | + // | | | + // v v v + // timer0 timer1 timer2 + ESP_LOGI(TAG, "Create GPIO sync source"); + mcpwm_sync_handle_t gpio_sync_source = NULL; + mcpwm_gpio_sync_src_config_t gpio_sync_config = { + .group_id = 0, // GPIO fault should be in the same group of the above timers + .gpio_num = EXAMPLE_SYNC_GPIO, + .flags.pull_down = true, + .flags.active_neg = false, // by default, a posedge pulse can trigger a sync event + .flags.io_loop_back = true, // then we can trigger a sync event using `gpio_set_level` on the same GPIO + }; + ESP_ERROR_CHECK(mcpwm_new_gpio_sync_src(&gpio_sync_config, &gpio_sync_source)); + + ESP_LOGI(TAG, "Set timers to sync on the GPIO"); + mcpwm_timer_sync_phase_config_t sync_phase_config = { + .count_value = 0, + .direction = MCPWM_TIMER_DIRECTION_UP, + .sync_src = gpio_sync_source, + }; + for (int i = 0; i < 3; i++) { + ESP_ERROR_CHECK(mcpwm_timer_set_phase_on_sync(timers[i], &sync_phase_config)); + } + + ESP_LOGI(TAG, "Trigger a pulse on the GPIO as a sync event"); + gpio_set_level(EXAMPLE_SYNC_GPIO, 0); + gpio_set_level(EXAMPLE_SYNC_GPIO, 1); +} +#endif // CONFIG_EXAMPLE_SYNC_FROM_GPIO + +#if CONFIG_EXAMPLE_SYNC_FROM_TEZ +static void example_setup_sync_strategy(mcpwm_timer_handle_t timers[]) +{ + // +->timer1 + // (TEZ) | + // timer0---+ + // | + // +->timer2 + ESP_LOGI(TAG, "Create TEZ sync source from timer0"); + mcpwm_sync_handle_t timer_sync_source = NULL; + mcpwm_timer_sync_src_config_t timer_sync_config = { + .timer_event = MCPWM_TIMER_EVENT_EMPTY, // generate sync event on timer empty + }; + ESP_ERROR_CHECK(mcpwm_new_timer_sync_src(timers[0], &timer_sync_config, &timer_sync_source)); + + ESP_LOGI(TAG, "Set other timers sync to the first timer"); + mcpwm_timer_sync_phase_config_t sync_phase_config = { + .count_value = 0, + .direction = MCPWM_TIMER_DIRECTION_UP, + .sync_src = timer_sync_source, + }; + for (int i = 1; i < 3; i++) { + ESP_ERROR_CHECK(mcpwm_timer_set_phase_on_sync(timers[i], &sync_phase_config)); + } + + ESP_LOGI(TAG, "Wait some time for the timer TEZ event"); + vTaskDelay(pdMS_TO_TICKS(10)); +} +#endif // CONFIG_EXAMPLE_SYNC_FROM_TEZ + +#if CONFIG_EXAMPLE_SYNC_FROM_SOFT +static void example_setup_sync_strategy(mcpwm_timer_handle_t timers[]) +{ + // soft + // | + // v + // +-timer0--+ + // | | + // v v + // timer1 timer2 + ESP_LOGI(TAG, "Create software sync source"); + mcpwm_sync_handle_t soft_sync_source = NULL; + mcpwm_soft_sync_config_t soft_sync_config = {}; + ESP_ERROR_CHECK(mcpwm_new_soft_sync_src(&soft_sync_config, &soft_sync_source)); + + ESP_LOGI(TAG, "Create timer sync source to propagate the sync event"); + mcpwm_sync_handle_t timer_sync_source; + mcpwm_timer_sync_src_config_t timer_sync_config = { + .flags.propagate_input_sync = true, + }; + ESP_ERROR_CHECK(mcpwm_new_timer_sync_src(timers[0], &timer_sync_config, &timer_sync_source)); + + ESP_LOGI(TAG, "Set sync phase for timers"); + mcpwm_timer_sync_phase_config_t sync_phase_config = { + .count_value = 0, + .direction = MCPWM_TIMER_DIRECTION_UP, + .sync_src = soft_sync_source, + }; + ESP_ERROR_CHECK(mcpwm_timer_set_phase_on_sync(timers[0], &sync_phase_config)); + sync_phase_config.sync_src = timer_sync_source; + for (int i = 1; i < 3; ++i) { + ESP_ERROR_CHECK(mcpwm_timer_set_phase_on_sync(timers[i], &sync_phase_config)); + } + + ESP_LOGI(TAG, "Trigger the software sync event"); + ESP_ERROR_CHECK(mcpwm_soft_sync_activate(soft_sync_source)); +} +#endif // CONFIG_EXAMPLE_SYNC_FROM_SOFT + +void app_main(void) +{ + ESP_LOGI(TAG, "Create timers"); + mcpwm_timer_handle_t timers[3]; + mcpwm_timer_config_t timer_config = { + .clk_src = MCPWM_TIMER_CLK_SRC_DEFAULT, + .group_id = 0, + .resolution_hz = EXAMPLE_TIMER_RESOLUTION_HZ, + .period_ticks = EXAMPLE_TIMER_PERIOD, + .count_mode = MCPWM_TIMER_COUNT_MODE_UP, + }; + for (int i = 0; i < 3; i++) { + ESP_ERROR_CHECK(mcpwm_new_timer(&timer_config, &timers[i])); + } + + ESP_LOGI(TAG, "Create operators"); + mcpwm_oper_handle_t operators[3]; + mcpwm_operator_config_t operator_config = { + .group_id = 0, // operator should be in the same group of the above timers + }; + for (int i = 0; i < 3; ++i) { + ESP_ERROR_CHECK(mcpwm_new_operator(&operator_config, &operators[i])); + } + + ESP_LOGI(TAG, "Connect timers and operators with each other"); + for (int i = 0; i < 3; i++) { + ESP_ERROR_CHECK(mcpwm_operator_connect_timer(operators[i], timers[i])); + } + + ESP_LOGI(TAG, "Create comparators"); + mcpwm_cmpr_handle_t comparators[3]; + mcpwm_comparator_config_t compare_config = { + .flags.update_cmp_on_tez = true, + }; + for (int i = 0; i < 3; i++) { + ESP_ERROR_CHECK(mcpwm_new_comparator(operators[i], &compare_config, &comparators[i])); + // init compare for each comparator + ESP_ERROR_CHECK(mcpwm_comparator_set_compare_value(comparators[i], 200)); + } + + ESP_LOGI(TAG, "Create generators"); + mcpwm_gen_handle_t generators[3]; + const int gen_gpios[3] = {EXAMPLE_GEN_GPIO0, EXAMPLE_GEN_GPIO1, EXAMPLE_GEN_GPIO2}; + mcpwm_generator_config_t gen_config = {}; + for (int i = 0; i < 3; i++) { + gen_config.gen_gpio_num = gen_gpios[i]; + ESP_ERROR_CHECK(mcpwm_new_generator(operators[i], &gen_config, &generators[i])); + } + + ESP_LOGI(TAG, "Set generator actions on timer and compare event"); + for (int i = 0; i < 3; i++) { + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(generators[i], + // when the timer value is zero, and is counting up, set output to high + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(generators[i], + // when compare event happens, and timer is counting up, set output to low + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, comparators[i], MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + } + + ESP_LOGI(TAG, "Start timers one by one, so they are not synced"); + for (int i = 0; i < 3; i++) { + ESP_ERROR_CHECK(mcpwm_timer_enable(timers[i])); + ESP_ERROR_CHECK(mcpwm_timer_start_stop(timers[i], MCPWM_TIMER_START_NO_STOP)); + vTaskDelay(pdMS_TO_TICKS(10)); + } + vTaskDelay(pdMS_TO_TICKS(100)); + + // Manually added this "IDLE" phase, which can help us distinguish the wave forms before and after sync + ESP_LOGI(TAG, "Force the output level to low, timer still running"); + for (int i = 0; i < 3; i++) { + ESP_ERROR_CHECK(mcpwm_generator_set_force_level(generators[i], 0, true)); + } + + ESP_LOGI(TAG, "Setup sync strategy"); + example_setup_sync_strategy(timers); + + ESP_LOGI(TAG, "Now the output PWMs should in sync"); + for (int i = 0; i < 3; ++i) { + // remove the force level on the generator, so that we can see the PWM again + ESP_ERROR_CHECK(mcpwm_generator_set_force_level(generators[i], -1, true)); + } + vTaskDelay(pdMS_TO_TICKS(100)); +} diff --git a/examples/peripherals/mcpwm/mcpwm_sync/pytest_mcpwm_sync.py b/examples/peripherals/mcpwm/mcpwm_sync/pytest_mcpwm_sync.py new file mode 100644 index 0000000000..3e130c6646 --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_sync/pytest_mcpwm_sync.py @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import pytest +from pytest_embedded import Dut + + +@pytest.mark.esp32 +@pytest.mark.esp32s3 +@pytest.mark.generic +@pytest.mark.parametrize('config', [ + pytest.param('gpio', marks=[pytest.mark.esp32, pytest.mark.esp32s3]), + pytest.param('tez', marks=[pytest.mark.esp32, pytest.mark.esp32s3]), + pytest.param('soft', marks=[pytest.mark.esp32s3]), +], indirect=True) +def test_mcpwm_sync_example(dut: Dut) -> None: + dut.expect_exact('example: Create timers') + dut.expect_exact('example: Create operators') + dut.expect_exact('example: Create comparators') + dut.expect_exact('example: Create generators') + dut.expect_exact('example: Set generator actions on timer and compare event') + dut.expect_exact('example: Start timers one by one, so they are not synced') + dut.expect_exact('example: Setup sync strategy') + dut.expect_exact('example: Now the output PWMs should in sync') diff --git a/examples/peripherals/mcpwm/mcpwm_sync/sdkconfig.ci.gpio b/examples/peripherals/mcpwm/mcpwm_sync/sdkconfig.ci.gpio new file mode 100644 index 0000000000..3e98959154 --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_sync/sdkconfig.ci.gpio @@ -0,0 +1 @@ +CONFIG_EXAMPLE_SYNC_FROM_GPIO=y diff --git a/examples/peripherals/mcpwm/mcpwm_sync/sdkconfig.ci.soft b/examples/peripherals/mcpwm/mcpwm_sync/sdkconfig.ci.soft new file mode 100644 index 0000000000..6e2b3b5e1a --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_sync/sdkconfig.ci.soft @@ -0,0 +1 @@ +CONFIG_EXAMPLE_SYNC_FROM_SOFT=y diff --git a/examples/peripherals/mcpwm/mcpwm_sync/sdkconfig.ci.tez b/examples/peripherals/mcpwm/mcpwm_sync/sdkconfig.ci.tez new file mode 100644 index 0000000000..4e6ee21440 --- /dev/null +++ b/examples/peripherals/mcpwm/mcpwm_sync/sdkconfig.ci.tez @@ -0,0 +1 @@ +CONFIG_EXAMPLE_SYNC_FROM_TEZ=y diff --git a/examples/peripherals/mcpwm/mcpwm_sync_example/README.md b/examples/peripherals/mcpwm/mcpwm_sync_example/README.md deleted file mode 100644 index 761aaf9927..0000000000 --- a/examples/peripherals/mcpwm/mcpwm_sync_example/README.md +++ /dev/null @@ -1,74 +0,0 @@ -| Supported Targets | ESP32 | ESP32-S3 | -| ----------------- | ----- | -------- | - -# MCPWM sync Example - -(See the README.md file in the upper level 'examples' directory for more information about examples.) - -This example aims to show howto sync those timers within the same MCPWM unit to produce fully synchronized output. - -The example will: - -- init MCPWM -- sync all three timers with the help of one extra GPIO -- mess the synchronized timers by stopping and restarting -- sync all three timers with software sync event, no GPIO required. (Only targets with `SOC_MCPWM_SWSYNC_CAN_PROPAGATE` ability can support). -- sync all three timers, but adding 10% delay between pulses from different channels - -## How to Use Example - -### Hardware Required - -* An ESP32/ESP32S3 development board - -It is recommended to have an oscilloscope or logic analyzer to verify the output pulse - -Connection : - -| Pin | Func | Mode | -| :---------: | :----------------: | :----: | -| GPIO_NUM_16 | MCPWM0.Timer0.GenA | Output | -| GPIO_NUM_17 | MCPWM0.Timer1.GenA | Output | -| GPIO_NUM_18 | MCPWM0.Timer2.GenA | Output | -| GPIO_NUM_21 | GPIO_SYNC0 | INPUT | -| GPIO_NUM_19 | simulate input | OUTPUT | - -GPIO_NUM_21 and GPIO_NUM_19 **SHOULD** be wired together to provide simulated input. - -Above pin selection can be changed within file `mcpwm_sync_example.c`. - -### Build and Flash - -Run `idf.py -p PORT flash monitor` to build, flash and monitor the project. - -(To exit the serial monitor, type ``Ctrl-]``.) - -See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for full steps to configure and use ESP-IDF to build projects. - -## Example Output - -``` -I (306) sync_example: MCPWM sync example -I (306) sync_example: PWM started, not synchronized -I (3316) sync_example: Sync timers with GPIO approach -I (3326) sync_example: Output should already be synchronized -I (6326) sync_example: force synchronous lost -I (9326) sync_example: Output should already be synchronized on esp32s3 -I (12336) sync_example: Each output pulse should be placed with 10 percents of period -``` - -Overall pulse graph: - -![](readme_res/overall.png) - -Sync: - -![](readme_res/synced.png) - -Sync with phase: - -![](readme_res/sync_phase.png) - -## Troubleshooting - -For any technical queries, please open an [issue] (https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon. diff --git a/examples/peripherals/mcpwm/mcpwm_sync_example/main/CMakeLists.txt b/examples/peripherals/mcpwm/mcpwm_sync_example/main/CMakeLists.txt deleted file mode 100644 index d4f23669d5..0000000000 --- a/examples/peripherals/mcpwm/mcpwm_sync_example/main/CMakeLists.txt +++ /dev/null @@ -1,2 +0,0 @@ -idf_component_register(SRCS "mcpwm_sync_example.c" - INCLUDE_DIRS ".") diff --git a/examples/peripherals/mcpwm/mcpwm_sync_example/main/mcpwm_sync_example.c b/examples/peripherals/mcpwm/mcpwm_sync_example/main/mcpwm_sync_example.c deleted file mode 100644 index bc7b6c30a1..0000000000 --- a/examples/peripherals/mcpwm/mcpwm_sync_example/main/mcpwm_sync_example.c +++ /dev/null @@ -1,146 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Unlicense OR CC0-1.0 - */ - -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "esp_log.h" -#include "driver/mcpwm.h" -#include "driver/gpio.h" - -const static char *TAG = "example"; - -#define TARGET_MCPWM_UNIT MCPWM_UNIT_0 -#define TIMER0_OUTPUT_GPIO GPIO_NUM_16 -#define TIMER1_OUTPUT_GPIO GPIO_NUM_17 -#define TIMER2_OUTPUT_GPIO GPIO_NUM_18 -#define SIMU_GPIO_SYNC_SOURCE_GPIO GPIO_NUM_21 -#define SIMU_GPIO_SYNC_SIMULATE_GPIO GPIO_NUM_19 - -void app_main(void) { - ESP_LOGI(TAG, "MCPWM sync example"); - - // init MCPWM: 10% duty cycle on all three timers in MCPWM unit 0 (currently not synchronous) - mcpwm_config_t pwm_config = { - .frequency = 1000, - .cmpr_a = 10, - .cmpr_b = 0, - .counter_mode = MCPWM_UP_COUNTER, - .duty_mode = MCPWM_DUTY_MODE_0, - }; - ESP_ERROR_CHECK(mcpwm_init(TARGET_MCPWM_UNIT, MCPWM_TIMER_0, &pwm_config)); - ESP_ERROR_CHECK(mcpwm_init(TARGET_MCPWM_UNIT, MCPWM_TIMER_1, &pwm_config)); - ESP_ERROR_CHECK(mcpwm_init(TARGET_MCPWM_UNIT, MCPWM_TIMER_2, &pwm_config)); - - // bind output to GPIO - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM0A, TIMER0_OUTPUT_GPIO)); - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM1A, TIMER1_OUTPUT_GPIO)); - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM2A, TIMER2_OUTPUT_GPIO)); - ESP_LOGI(TAG, "PWM started, not synchronized"); - - vTaskDelay(pdMS_TO_TICKS(1000)); - // temporarily disable GPIO output, by binding to GenBs which have 0 output - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM0B, TIMER0_OUTPUT_GPIO)); - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM1B, TIMER1_OUTPUT_GPIO)); - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM2B, TIMER2_OUTPUT_GPIO)); - vTaskDelay(pdMS_TO_TICKS(2000)); - - ESP_LOGI(TAG, "Sync timers with GPIO approach"); - // first configure sync source - mcpwm_sync_config_t sync_conf = { - .sync_sig = MCPWM_SELECT_GPIO_SYNC0, - .timer_val = 0, - .count_direction = MCPWM_TIMER_DIRECTION_UP, - }; - ESP_ERROR_CHECK(mcpwm_sync_configure(TARGET_MCPWM_UNIT, MCPWM_TIMER_0, &sync_conf)); - ESP_ERROR_CHECK(mcpwm_sync_configure(TARGET_MCPWM_UNIT, MCPWM_TIMER_1, &sync_conf)); - ESP_ERROR_CHECK(mcpwm_sync_configure(TARGET_MCPWM_UNIT, MCPWM_TIMER_2, &sync_conf)); - // then configure GPIO - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM_SYNC_0, SIMU_GPIO_SYNC_SOURCE_GPIO)); - gpio_config_t io_conf = {}; - io_conf.intr_type = GPIO_INTR_DISABLE; - io_conf.mode = GPIO_MODE_OUTPUT; - io_conf.pin_bit_mask = BIT64(SIMU_GPIO_SYNC_SIMULATE_GPIO); - io_conf.pull_down_en = 0; - io_conf.pull_up_en = 0; - ESP_ERROR_CHECK(gpio_config(&io_conf)); - ESP_ERROR_CHECK(gpio_set_level(SIMU_GPIO_SYNC_SIMULATE_GPIO, 0)); - ESP_ERROR_CHECK(gpio_set_level(SIMU_GPIO_SYNC_SIMULATE_GPIO, 1)); - // wait for at least one TEP - vTaskDelay(pdMS_TO_TICKS(10)); - // re-enable GPIO output, to see the result - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM0A, TIMER0_OUTPUT_GPIO)); - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM1A, TIMER1_OUTPUT_GPIO)); - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM2A, TIMER2_OUTPUT_GPIO)); - ESP_LOGI(TAG, "Output should already be synchronized"); - - vTaskDelay(pdMS_TO_TICKS(1000)); - - // stop and restart timers to mess them - ESP_ERROR_CHECK(mcpwm_stop(TARGET_MCPWM_UNIT, MCPWM_TIMER_2)); - ESP_ERROR_CHECK(mcpwm_stop(TARGET_MCPWM_UNIT, MCPWM_TIMER_1)); - ESP_ERROR_CHECK(mcpwm_stop(TARGET_MCPWM_UNIT, MCPWM_TIMER_0)); - vTaskDelay(pdMS_TO_TICKS(2000)); - ESP_ERROR_CHECK(mcpwm_start(TARGET_MCPWM_UNIT, MCPWM_TIMER_0)); - ESP_ERROR_CHECK(mcpwm_start(TARGET_MCPWM_UNIT, MCPWM_TIMER_1)); - ESP_ERROR_CHECK(mcpwm_start(TARGET_MCPWM_UNIT, MCPWM_TIMER_2)); - ESP_LOGI(TAG, "force synchronous lost"); - - vTaskDelay(pdMS_TO_TICKS(1000)); - // temporarily disable GPIO output, by binding to GenBs which have 0 output - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM0B, TIMER0_OUTPUT_GPIO)); - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM1B, TIMER1_OUTPUT_GPIO)); - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM2B, TIMER2_OUTPUT_GPIO)); - vTaskDelay(pdMS_TO_TICKS(2000)); - -#ifdef SOC_MCPWM_SWSYNC_CAN_PROPAGATE - // use the trick that only available on esp32s3 - mcpwm_set_timer_sync_output(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_SWSYNC_SOURCE_SYNCIN); - sync_conf.sync_sig = MCPWM_SELECT_TIMER0_SYNC; - mcpwm_sync_configure(MCPWM_UNIT_0, MCPWM_TIMER_0, &sync_conf); - mcpwm_sync_configure(MCPWM_UNIT_0, MCPWM_TIMER_1, &sync_conf); - mcpwm_sync_configure(MCPWM_UNIT_0, MCPWM_TIMER_2, &sync_conf); - // then send soft sync event to timer0 - mcpwm_timer_trigger_soft_sync(MCPWM_UNIT_0, MCPWM_TIMER_0); - // re-enable GPIO output - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM0A, TIMER0_OUTPUT_GPIO)); - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM1A, TIMER1_OUTPUT_GPIO)); - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM2A, TIMER2_OUTPUT_GPIO)); - ESP_LOGI(TAG, "Output should already be synchronized on esp32s3"); - - vTaskDelay(pdMS_TO_TICKS(1000)); -#endif - - // temporarily disable GPIO output, by binding to GenBs which have 0 output - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM0B, TIMER0_OUTPUT_GPIO)); - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM1B, TIMER1_OUTPUT_GPIO)); - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM2B, TIMER2_OUTPUT_GPIO)); - vTaskDelay(pdMS_TO_TICKS(2000)); - // create phase between each timer. - // for this case all timers has 10% of period phase between each other - sync_conf.sync_sig = MCPWM_SELECT_GPIO_SYNC0; - sync_conf.timer_val = 0; // no phase applied - mcpwm_sync_configure(MCPWM_UNIT_0, MCPWM_TIMER_0, &sync_conf); - sync_conf.timer_val = 900; // fill the counter with 90.0% of period will cause next pulse being delayed 10% period - mcpwm_sync_configure(MCPWM_UNIT_0, MCPWM_TIMER_1, &sync_conf); - sync_conf.timer_val = 800; // fill the counter with 80.0% of period will cause next pulse being delayed 20% period - mcpwm_sync_configure(MCPWM_UNIT_0, MCPWM_TIMER_2, &sync_conf); - // trigger positive edge - ESP_ERROR_CHECK(gpio_set_level(SIMU_GPIO_SYNC_SIMULATE_GPIO, 0)); - ESP_ERROR_CHECK(gpio_set_level(SIMU_GPIO_SYNC_SIMULATE_GPIO, 1)); - // wait for at least one TEP - vTaskDelay(pdMS_TO_TICKS(10)); - // re-enable GPIO output, to see the result - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM0A, TIMER0_OUTPUT_GPIO)); - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM1A, TIMER1_OUTPUT_GPIO)); - ESP_ERROR_CHECK(mcpwm_gpio_init(TARGET_MCPWM_UNIT, MCPWM2A, TIMER2_OUTPUT_GPIO)); - ESP_LOGI(TAG, "Each output pulse should be placed with 10 percents of period"); - - vTaskDelay(pdMS_TO_TICKS(1000)); - - ESP_ERROR_CHECK(mcpwm_stop(TARGET_MCPWM_UNIT, MCPWM_TIMER_2)); - ESP_ERROR_CHECK(mcpwm_stop(TARGET_MCPWM_UNIT, MCPWM_TIMER_1)); - ESP_ERROR_CHECK(mcpwm_stop(TARGET_MCPWM_UNIT, MCPWM_TIMER_0)); -} diff --git a/examples/peripherals/mcpwm/mcpwm_sync_example/readme_res/overall.png b/examples/peripherals/mcpwm/mcpwm_sync_example/readme_res/overall.png deleted file mode 100644 index 092cebd55edeb34b73c437927c3a6b226744059c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35797 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%0D)XKzzUcaBs&cCoR*}bB&^5I$Y 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 diff --git a/examples/peripherals/mcpwm/mcpwm_sync_example/readme_res/sync_phase.png b/examples/peripherals/mcpwm/mcpwm_sync_example/readme_res/sync_phase.png deleted file mode 100644 index 47e8aedf7599dded297468acbb170776a1468dd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10151 zcmeAS@N?(olHy`uVBq!ia0y~yVAEn?V0gsA#K6E1zn)_n0|NtRfk$L90|Va?5N4dJ z%_q&kpuphi;uumf=k49{n$V{Y?LRzEYoEz-h(|kVM$*BkHKo^gEqKJTHq3SZt=O$w zcmFNT+In?c-?jBo2LeUI?`CWjEo|aWbLW2hb<2J_2?bcFJlS_&|9?eM z@p(&syL&R9YKqSJu354~g@J*A!NKIxtSXSJ7`CLmQU{4BEvW)FVSR1wV z*R$>Wwx)cX)G7Sl*Ul*C#s)!UcQMhgH+Cc*4)(YG`sedx;ZB!N!L`4>Sibuq{OhZB z<=3m>@;mPpSO4V;uY8LonIdI z{oURAAB*Mff4?#IxA|A`Y2ubGTP~lsSX}@AU--Xw<))^l7wf-TU$6NjX;JWC#^!VH zE-gLXC917DYR&#`>JZ25Be@;@Kli%6?>ivGU#mBwhpS?RPBem)K=gy-^ z@iyOXJnpw&=sVl&^Ru(NU!SwSwkA^8DdYOOyR70pvIYeYc9p(XyI1Z1R(n;Z>HL~m z&o!(1p9jap{FyxepN{U^{rl_YnY=vNE?2YRonv=*_x`GTKVx^7>FUN7{C>NAebmmP z@NHQ_zLWg-9`~K7dd%n&MUk7+TKoS$tpEG<`v0F7 zoNsRnonLoMGJn5q?d4^@iY*Th^Z$HR`F#JMOWpjoKOWq^yj)$VGxzqkTwU$zHy7RG zYM)MBc)0EBtGmC~n&0P9eDdT;&HLZsDQRhT-(D#9+x+=(ZEf^xzs2tTa<)}p_I#hW zJHGbmROkH#8U6G2PKy5e=H}+w-{0*2|2#jh;?GC*`4vgK*M4hCo8<2teRTEe`L*9} zrq8c6v$y}hqkZ<;$1he;%;EKRdVjaj$vJj)I3jpT`^I->a_vpMLq7 zaNzfMuJ`vpVqdy+DZl-n^Z)-mzkl`W)!myyugvnal6rbt zpRBdbuN%p?x8-`v{XNwEZ~2N98K0N=1~14!zV^#${k>Zf54RoL?lrgOm2IBKOjt7XPzg%u+<3W0$|`&538BZApH2oR%*k8s z3gw7z&$9cO(jxHfcK-hL^UCA4=iNPh>eR9R-@kh#jqSc}tp4_<@Py+0`p3QIS@N1X zI&%!_?mTptiTv%MH1Wsr*mBk5=WRZ(pX}D7p`#PCr=l<}?%oT-2aexjr%eqF4ZT|XX+_}jnui+>fBN)EOItg>_UqKLx3_quEHtJ~`}WCO|8DvH zBeQ1Bnl{~kZ?vjVXZihF^Og(Sa&NnMtclvHwKJFNnXpsG(WK;KbH0B2@-lXJ>EHJ$ zR;5)BTQwzLzI4Abv%h9i|Cd*Hty>IIPksJa@n>u1v{&(F0kKRe4*mYe(e`T6_JKWEwrE?v5GZu!5T{x<(^ zbnEYn`2MkKyVt?#0XA~_+S=TTTk`M!YZ3O7`Tl+X^x8d>EZ=N6e0yu|@Av8RB@Gf1 zPCdK4v-tU{RjbPHRqo!ktLoO4O!a=NrM|PxR)??O_jKBSU%Q9)|9^g;z|5a>b=B2n zzO#c2<})(0=|pT$_!&LfbMm}Bzh>E&yt}h=)25;e3l4_=|F#4&T`%=!QSEKE&JHUIlH|LWDN`aUy1olxdK{`jNp_xbbY z*=1aKQ2zXk=RdA3wZFg3wJQDf>7=^;p9jr1H>a!1zqz>ic~AMh$}bn)tG{UcI#;e= z`|sz;{ePbRe|>4`>FxXe&YfwTer}%aYS(9*ABTBPuX+3Wa6A9~Et!`~um3Zg`dQK_ z#p8P3-FLfQpIcx1S9&Y|z8{CU6^YXT3 zYV-DNGGYZ)Nx8SS$lCm9==su9et+)VJN~Y&NB2~I_Ot%G<+i%djr{%lZr;4PIqh7| zt(sW|i{{x@e|>pbU0waT_VYQr6A!n!%T-p~y0VftcWRUV!YFt7;xn64Pp6&#_hS2N zR#sMJx4u7@=dY7CO7VDG9Aq%Rm0SGUl`C_KUS&?6SMzAc{B@9juGc1CixdH$z#K}&v<>~3^_lv*ZO`li!?CA8EO=V|i?fLU(`@I@%)6blW zHoxB(&*ifI`tox670u-~Kc5JLnsZ7MD;_lduK%)F{{Ihk`?KfI*T1>wo_)VtHHo0-nr@>=C^#(q1?CQ z-L35G>ozN=o!4H!=hE8h@7i@A4!?QxW^4BKl$4a5+uPRq-rg1(8oE{2$2HY8E^c2< z*5}Ma!C&IeE&ok*A8$IXH~-6% z$(NV;{>{*uIaAs^&*xwLuk-c)YQJ1`FMoAKvywm2MlOC=$;)PbyMm+qbvL%<-agaQ zBXHgiRhR!!51vnf8-!^q5@_kDA-z)0;PMo-7|05|);lzV%8?*&Qv*`7TNu>;C@w{eJiR z2hH}^*T?UFnZI{x(@&YI7YkR1t(|3`e^33dR*=nn@%WmDN5$hg6h%ZtV!r-fJzY0` z+N_@{LY*_^=SSSDd_I@^SYK`3rzfjct@76UdCT_uoxiWN*Yijie0Y8T$FVL^?YR2C zTlY@zH}26eHr`xydz+~CwBwIIn%}Sa{3LMsm$$qBH?h8dZ*oJVl2et}^232sr&c}T zT<95bUojjHIF|2s1;VfXI_}Z@$f;jefdLI@wHJ~wY0&&*4B2XieDJd zr#USxODR=ngzKu}Q8{p0!U(c!MCu4QFqpKP_AI3gn>MdkmC+&vMtaI@0PtjU-xVB zw%l9uiqGvlslCO1<=NThYooX4y}qV<_4YQ^)my9bRXwuq+r78G|MT5tf7`EjivO>y zE%^JZRMxu8N6yw**|;U({Z{pj5jl^K^(wa9GWR`y=FFN^xBksk@0AD)y7Ycm_WHHv zO}wwK=ilA5G z3k#RetBSh2-|4Ev&lZ91qoDT5rcIl^KYRBfeR+_={I@!hn=Ev7*OtD%wzvBGy9)~s z&(7Z?=rqIl`CUoldy)nP2fm%})0uzaj1s7Ee!J}LrcIkB$)}~Hc|rq`!lcyQ4E_s#RK|1F#>5TrVP;o&yD=xsSa zKRx|eQS+(zeD2LXJ9q4`Xt}Vz{{QClcE8t~-~I9L_xtPio3ET$9u^jMt?;<~|9jQ% zb8l_Yth^n2eO;_GM`mW`-R>?i{kT1U*)-?)U!J$D?n+5ZYZJJ&EqC_x>Dxa^;~=3w3G&WSQ6^ZXxsZ(v*MW5=7jRrr%< zqpDlcg#f!={e#~?v8p5X`u6tx+uL%rwY9_7#LS#M`?n_FAJ1M1%cPHwT=n-Ds2YR8 z&w0`L`#!qOx2xs*^(MWzxY*z7?nzCLU2#NjSmdgW&PPcI!YZ4C{Jk4GlI+w=Ha zS6A1WGiQ2wdLneDO`B$xbHlL}4AgriE-uTx&C2_?CCa_~%<_9{qqb(5*8EVEwJv*e zXXodc>3x02AHP5Ef3(Jl&wG-dA0uC+V&n>Pt-VZtc}|G>i+)!OQ*-i1q2v; zyjML*T=vX7+t*Wq{dIM8Y(A}6Y*qFm;QOU{ZA<*@eqYe6GXL!PF6IB<@9_b<7Q9nh zXf{7`bDEk^r=YTX*^3Jgw_Ns%-TU0yFIWA(AECu&~VX1vldoMfG;GQVni!phA@ zJl-vtSR-}w)nu=gH9;wz6YqFB|7>xazT)?VpO(sE-MunDpE5GDA4}Tk+AVgs`25}8 zI}N*n*YEvyD|`LhC+xGfY}vA}_WS#*tFNE^U+w*A;>z{=s&40QpEhmUzsK_By>`VV zzq|Ek+SlL9-F`PJIQa6yO6$hR1(y|Mcn8 zak<)(XJ=-DO406LBI3)JF8%uIuC==Qb9MW_kwyPMR{Z(@_mTK)v)n(A#P`prIW*~L zcmBWI@!@MCHeO!8!u473`>@-b z>?(boxBKg~%YOEc*K9t2Z&T`NZTuaO8UtE0r%0IOa-#8za?^$Xn`sw7UQ@;+&ufMya@bMW*V~|^( zu>S9Kd34VD-HwDet^5Dpoq969VqNU+vgPrCuby0%@w-^9JAccTEqbxLa<1E7dlnoQ zSNHjBz4iY+pZzT!9=XKIowxJpv?unzzUE!sAvV*0uRYV#Cr_4~6I%SS<`t+bb0j#u z^J0Y^Z|+ykC4P3ATaWw@c|N~wTULzlGhuB_&7W7?K|_cQ$CkeRqX`;Q{E)eFK8Ws^ z@QagyfuVr|KlknJua|dx6rNSZb#_*bl=}Q*Czls~x~VB>>SxzG+5hpA%N9RPP?^~` zf4;f>RTyN)**Qenf#!m%!S=n<_6rxLn}1r_VRq+b`s}MUPd1-_yDjppyPaUJ#NE8v zw`*oaznNd<{PV=}LS41_cW>s3xZj!k^Mt$IZsYm28X$q<-$qZB=d0b!ef({qez4vA zB5tfq`L*Ls;>|O;ehq^7et8OXN`VmgujSuVjZ`w`Kzc(UQBX=JV}bWN_2U zcW>EO#c+{#3pGJOeW7SctAJatmFmxf%OzxrTpundRd(hGULU)+`+x1LWJw7$@TRyq z!qe0925<2CSlitXIvroTz58|5-}>IN=ciU4>y@^DxQP3>*;gF~hK6a!qLhOkwMI$q z-dy-;-tNN3_PesH+se(RUM~goi zTHbMG?VPOD8n$4OeJd=l`IlbZ=2xWw@=a}E@%s5$x!0XF7lPvN8|QDIzaiR@HajIi za|j9ZH@v^<@2S@Yis&2aZ&rT|Ub{;is&>}w8{PVQ_srg{p{Hl}YlZUn6FFtU-nt9b z?CqCs*|KG2IwJ$a7L&8-rq`7sPdTmyc{ccbB7gQuRj5N)ulaj=fnD)z!BIGS+0{d@ zmoHg)ecd$&P_UYGPFXR(>GT4S>t=bZx&qU(S7+<119nT`YrJFvfmZmg-*fW13+&7E%WuV-pbs$vko7H@CCkJR)3rXcALM;-!GXt<%S^_ zoI$QKarnC>y6nxuCM}SU7Vv$&JTGv2(W+=y)?84?O}KT%zxrNT?d7uED}DYMM?gt) z?*XsLpbY$`_=^9;+Z&B!NnQ{CxeK<>!^3{{N2ZTKo4>UHP94w|}>q z&W(>KIj1l7^{#|dRNBkP-?lYziJ9NG%;7h$dARGl-^`egp6mB}Z+ZXWmiQ{ZT^Yr{ z)Mw^;oSyah?7#2xqBp3mU3A~}^ZEUWxpBU8J#fB${` z#Ga{Zv$@jN*!-OSp8x-XwCzl}cU;Z?njG3|F8w%YaVj$d!?$IZE?t5olZ=z!9t52` zTfS0z+P;jGi)Z9Y-zAO0}{Ri^{zK3^AAdtuHI^%=PxxfVPB zuT4L`@59^7{r_%F>Ir7QTbJgy=$*>`t-n6>t4Cfo&CmPIyLjKnU-{=|8Jp}r9lQU{ zu5;#~roR6+6Ed#`oC`v*Zus=H&ye}sk@%MARITTwyt+loHso$)! zysC6|>daL4{qs2#TU59UHfLL*fPPczWDkUKJTjI zhj!hXr?KtXg+Euluk%Js;aGTYyFmY1%f;^(+g^|RwbcA~NZPtg@$+1YEfZc``PiHI zbpGD7>|5$HbI+c(^!~;j`(D$VTd~FE`OawLW7{I<%?k@npCoDRXEe?9`3AGx_}Cbc z`)4B<85k6{uACnfaQ1=S{{34se5-PzXMg#zKz~oL?4{*<|DX1rzJ2ZcvwO6ombY)J z@cR_pS>ac?B4_Tr6Ta(tKEClM`e`!vOyQq~i{pjTByhl$xx~aH# zQK(K!zun%CAO8OT{`%$dn(V~+tJu_M?lhejccCgUu$V{PmpKkxSY|*+q8qay7csQ`16ImWG=eos2lvl+U-Xe*e|v{kzP{`S%^SWME)e zqVP4C%WL-TAJ)tDx|gT#|Fm4u`+e=_Gnu9vPrWkZouXqI{di$$Rq7|9r4d!Gc_;2k z^-n#%*|{n;PbbH#MPOCb^7c;^WpCw#KF-+`a8qlqTFVvzp|zK^o%bww^Z1WK783&l zyDw-hz`K_(CfUbd-S?mWzFlS2o_pN$C+$8_U2yLDt@qz=d~NrM+Wzi_{W<%GPfu;x zGH1JA*6qJ5cg~gCC$sY2%{CXCb3rs*!h)R%S}yAMjZR|UasPE$EVGD zbIdF5u3HAG+^(FzeQU|0SFg&nwLgc4JPm7I!#nk-w$|rs=2uu$Z=Jn;YghK{)V0nW zichx1T`|6w8|Qj#k?!4XTca+Si#c&@T%-DT)2Dm4gPv;w=UXKbYboLe^0tE@B8NSY;(P4 zrLF#zFB&zq2c|X8UzxFF%c}BinIAVTVoi+Pf6Y{}MIid-{?gKy)7E|bZ+!CAe?Oyb zug*Ab?^z{bTz2mHv>hKnB|_QkU!BKNed6tELJuxE%{9F=`|r8(s`IyYeB!>Gb!b{? zyWQl9zqj|Me?J##v+2i8=F@z&ImJ~SYh?pJZC|}h*W16OKFRewqph6X*0;Cfe*XVe z_WVcj>Gn1Jin`CIzW=_aBIDi4%d1vzzPv4UdFb-`k}0d4ciY#xye@6MnQfh-qCNl9 zqi^zG&a64pQC_otO=y~1W!bG$!q)opHx|9&owFllm;Sx=c+TG`>(!J@+tN6wm<8htg26avTysVp7+zHO=%Zc#U5|``t<&H_Wvf%yL~P9 z*Prw0vj5|hT5ddh^zAdhSI#M}yLJ(O?CnqP``hkZA2;KLP3pz#b-Q=@Dz%*1_PH%h z=kFoM>2Y)JlrH*SdbzGB+m&PC)lWBi?$>`?`De=+U(wj*Yfktcxh8w<_qO*>*5?Xu z3eU5A;QzU8eeT!y!o}V1>>a1&@a2wzp?U%9C7Nn z>bI$Q&G9F#Cq0k-eyV@sd}Q_KM0rpFerNypZg?ng9P9r~BD_I2vF3{qVlbpFb5_R$UdB zH~xFD?)=?r?|0tjoB1zc|4IAZTXl5p*XMHFT(A4%<_XE~{Bvq9E3W_d=T+CFSuWqL z@9%uIqx_xxhd;dTdka54emVDdY1QJ}$C~fw#BR7d=XPrS(;m&~wgIBQe@r}MDmuNl z^5-(AVUwPxZ#eY9wKm7kv`opRBqb^>EUh;n0qeqV} zUAn|kwx{^#f?I88Y?%E%Bg?e8?&Bp>>q@^x z=jE5j?GH}_weQ~~-m8)U^wlguKN}8d?gbs)`dB@tWbzhJMIZ!g_enlJ=AZG( z(m_}o(aQ8QWnefkFI7`~tL=^{-BYJbc{5QK)XF^2cc+FOTk8_7u{p|RU|_)25+5mn b^Wr~qlL(Awp8<4;YkzRI487-S!uXVSk}bF)z!rts3R`A zVe!3e*S%%}Q&)3DOi=Qex@f_rYJH~07M4rZ_kFjW`K>6yq_S|2!=YdQYiFG~H>dKO z&F<4`v*g`f`?ee&LG?@uKzRZ?3$2*zUgP(b7UIXSzO z6Bkab(3pL8Tkh*?JBy#+c>ArRqa*y-{#6&}tedS9zwgcSpPyY_0;llFT7P?Wb#-EM z+x;5j()aiFPUXM0Eq1q}a~m5g>(V7E=jR%4|Fh%Xy?gxjHj65|#Vjo?i_^~E+F$=a zwCeTr==E`Xe|>!~FCZ{u`tNVxi>e(XEO;Mlxr)26xB;IXj-rkm*el*i; zcK*Jf(`L+=@u#Wjf8B?DmtU6j{+qsf9+Tk33=`)zzTUpRy69Qq%zFfZ3%+4RSCBT{M)Xpan zHDBvnU5=y}^;W;D^!7Zte12WfzkJ1&8~?{vC`?;Duj$&_XnX6lHH$Qq9u-)`)xA7A zXU-h0WD9M-V~2b-y_#g|DaShOIU=(0K9hxBQFi z>-Wd4kNce+CMIOggaR9T`}(W-``1QntU3DWDLcPR z#_er)o95S_JAFD>KnGNudOS!km>K0T6*=RTD{rIB|B1neE7XR_jcCCh;>D6dX0-T?pA*}ab90Tck8tE zfv($fZ*Qx)xoOXxJ&E7%Y%V_Dw`kF#88ghj->H88e_4&?`{nZw9Xj;u+w1LxkJ;qq z=Rc3H_;`J)_VT*Fp@D%HI|P;W%vht=e)}mNXHoRy!;_QqRn=5%zFauGaie=tQBjtk zqa{CozhUz6mtTK9KK?j4Ia$q%VeqW-U5Wo&3z_;HbIJD=>Xuh(vOb#(~|3$Oltr>d&z+pdqto*(Ot z-BWQ`uddpE7Ro)Ha0dkdk(&dw9mV@Wo7pEbq{%^-rd?cStKTk%f`m0s^tB~ijRw? zYELuIxUj(g{=WKuAKfciyaII)8t2Gd3U~Kyb5m@6oNd|A)>KdH(EK*!BOj z=FGVhwl=!EWBc@QudGu`I_HPUDgOKM_q*un>C2ZceS34W^}8Dze|>%3-EsNlmIxhH zRn@T7U%xpvUt1SBc|t*qOHxwOqD71C{yuqJ`}>=inAkkiOYE$yQ>RXSdw2KotgU%> zcU{desj8})b@tgb-DusI55;b7$L81nn>cmu-HpNOf-i5czwfQTHzv+C@5YLY(}Vv# zfA(zI)vWh>-|ss)+5Z2l*DI5c^{tLc*!{~y%6I?1zkmMz)w-slrlK;}CNd`GO^H=k zSJ$cfs#)gwa^m9R($d)*Bc|!ci^<8!iHKa;5U4B|xx1|QYS!ItrLR?&97r%=VPSc9 zW8>kZjWTvM7TVgY;~X01=(8#<(m1st@%x*bd#&}}Mf{WsD#+Aav95Ji@P$>O*{*@p z^kOGXox1hG^Ph@}3wfnYcew=@T>WLQuCD&=#Kc95oOa)RR$#GdlhM*&=hyDuz58m` z)~K~+v(Kh&o*Vl6a)t@N-Iojc-qUsB_GqZ8F8wq8i`3itm2X8lJRA}p>?+N!|5sV} zbVZh*V@%8){`>XikB)RcKHi^ycbBP#K|r7g$SoRLTEAYc{x54+qM@t%_WS+t>Z+<; zyLV5|mlqSewmyIVOyhcs?SIaK62O90uj1?fPMtU}E-Y$Q*Q|*X11~-_UE&avp|kF5 zyrSUCdwbirPrv>ubblz{nzn!COB*9zIdI*%Yun}Rb?Vfq9JA=7e`aYVuaGfL{+Q}> zFn9a?c@ro8J9}mQ<42FYyu7kH7BYb(9H%}%x@y(AKM&j2MsG_=N%`~b_WH8jwh9Uh ziq5Pv5xbuEKfE$1#`*v2IezW?_wVnJu`YXaV`K8y*X-dx*X`N6_pr1{#NMjXr&BLp zym;`SV(`8>N=jXezU+9&w)mpS!@b{o-5&4TTb+LH&%&A}ubnL|EXyxj{^+xwuK#;! z{r}l~f4_!@g@tvC>AI+xnufZ}@k=iX3aW^xD7C%4DfP79T&w-R-|eof+_`VxzUT99 zT;CP9x61az182VWCzPi1>+YMz|CyM&XU-`~^o; zR++_?VqvZq19{;^YWRbNlVDk>${&&8U$zkm1c-;?0~w{{jE|M&N|R+v`TrT@!j zo3DwFkBCrElIm46G0FM-%vU>n&4t%r{cV0;xqGsQlo@wTg3rNzbEe4Kv%{@&i+*7?ih6?LPx zN}0YY6f7(+m;Zj}Ghci1^>uUa*ZyAHu_khJ+U;*|=USCjJvi8IWNiHS(Id0mTOvYI zzy5IR@7lRDH9h@hiPh}0(`L=GdVO5}{jS&Rj=S}qI(O=m>1=&7)>W&{&HjGbTYvAL zPuh>qrhT4tw7a{j>)dRn=7R<)FD}e9PM^mAb9vaLCT}mtsk(E_S>4Q&S6+?!m>wB9 z^J2=LXF&yDf7@GHW`5Z}FYD5hm&(fSX=$ndb`_Pq{wvK6n_p11k@Mmm3AU{Fjorb2I9Q%@W5d2bZL*?S@9M8hbS%wV z;^6Y_?fvkGh=BcdAC#5leLXDx{d{ur>D`Jv9#N37$3$lQ{o|u{`TPC)@7E7DH##~d zAMZPT!XtivUF}rEIaj5UKc>oXoU_tjCm<&F?ZaW^w7)-+U1AmM=GJyJX=8-W z>uc+FemAr5AT*=x_mkR&E4Of>hr5!pEW<_<;~019JX5b{+b$x zReF;ayZ4{_Rru_}Lg#kg-RUnU9i45KyQ)a*Py zH!ODf(c5V54yU)+hug-8;fBkmuwk=yi+7E9metzyYJ8#~tEs9#nAm>vewVvh(8e{r&v> z!~~lU7JSy-?lb$FZ|U7b5j8i17p%(4%X{Zv{JiYd6-zCxR}UJQO_v`qJ@#nVh5w3* ziW36^H>aOpwdZW*ex`MX0^WB$8ZW=}@$vBR@er68x;lLC*K4(nZBob?KTlIxnA`{QLWxY5v7Ud%hLs@l3s)sIzYm^TD0Zk17d?$=Riyx$*S$ zwA{P9p006&Ce+I>&)I6;+gCd~(^ z^mN!vNy(><-5sk|RaWj4y?*J%*Q(ny%dh5L-xmw22D4VJS_NwL&A(Txq@>g*XRD=j z@ZiDD&Q4G}|QpNw;c@bE|&NDtmit=j=P->*G|HJP6v)n!dH* zUcTd{lS}gM?t1<8*QQNI`uh6u@%NuRnX+e3%+6&&1yg_jpOYr~v)U

-hQtx`}Uo7$s z?GBy)o$vZ~{oj%~p3`lPR)0Al@cB9ayY(yAK0npJ+kMNewEt$eb{5}duNN)ed?}Q> zsXcyEV7u3Li=x)zZ!0*xRJSfaJ%8u<3ZwV2lbSYvJXNhl$?LB-diUPQetR;@M$Gx*>Me|1&{`6Kou+)eX94>r26<$T< z)VFWmCDQ9~ZfAyl!r5A{YVWce^&1nvl?blx-WPZC`E;vWxhi{)t^QYP{Ag|CDl=R)hyds{SD{^(nyfP~4Sd9Z9*68dWD*3U6cr_}$xKTvjD_u;eX56|@ev@B|0a?O3e z^SSA3v<@u3CQ|3b=%iqcKmhhV1u*#o% zW^FItz_jw}8Sne)hwi<-t9rNW-|98ufnGabZ<#E=@nn-|>B6YAFlKYP&wmZFljq;k z%YT}_-&nuxoZZ5c2DNcF&pfY4+5a={sMd1PJ#kShT;i6nP2PO|Y*gUOi3w}ndQEN~ zf1MToJGjkUENkP%E1M46+)MiV@{{@O_E|^6S8F>RTqC~J>g|@+kFAF0-Cm;Sd|ujJ z`jPf!*FD~-PIyJO)H=Gt&G%+|rIz-~cpVkZ%uKQ1V%KQ_GdBE)@|hI$&ZOwvw#Dyj ztBNINiq|-Q ztnjsKuHHV8HSzZ9YUe0#QTxZUVpe_b`fYjb;yrHn$h#F&!x*lVv_Np0 zQLw>BJK>w@VKd(!?>y@uX}ZGd=IN@nJ?@h#-?nEjut?nh{O7xC)xGm}e0xw-xRddA zyzM8mi_)KTe?6RW>%gij!ohCy%=Y|yS!aK$f49b`)eq$!`z61XyBBq_cf&1bP}$A8 z`QQ2tzw6A}@bkmFqiwkr70X)Z2h7+I&J(S+W{z~}gtxDhAF7MkJ(=|7xuE#@ zxw|Wjw=PZGZXb9mI*55q*cdvWTfYv)sRfB#9evy8CY)HcskGeV&z?bC|8pO4>M{n|Z0uTt^wq*s&L#DAUH z=yB<8jNG$b0Wn*YGWENB;$6gD*Y&@?9KY_3cYAyC@xHUO&DYoe|95BcCsD)N!wLct zJAPd@V!m$sXD8o2w*Pf|-k2T#>-28H%Eh;m_nJrkwfukfTJrZ9-1ZA62<~$eEDmi6 z6#RVOV)^PW=Vdq6ao@IGwfax*&b!Vjg?%T5{cca_RyxAGVo4yVUSfIw@AB39dq46^ zZ;Hg2rNvXUlfq3KlYd@WDx3)KFtBbm~^0r-avG?l8 z;$>f-r{qj4xYZUTq9`}Vwp#4#azPHag0IW9<6^C1jc$Fsu(-0l;&d#imi%Dq^*G4& zNmrLvWaLb-y}r}mna1u~y<D z>HGmn=jq4FMds1)9`9HJd%gs2O zRY5;ETYWFiRjn0I+37Yf{?hWYN0YW(>HjPy%=e9>X7BY0kAKNRECscSIas(qytS{* zz1vm4+jySuME0xq`On8V+D-U&(ydj3_ z9sck0Lob+p7uw$9v`pue{g(glu2|K2ItG?sY>hSF+JEQI`3aZpl)#yVZJ+fwukecW`@kE{(PDO+|5|Roa>1}shxh2-`LZ{&(<)f+7KyJc^DEycCnt02 zR#l95P;GSiSHFq%Kl@%+=7!xWZ5NBEdty`n`5a@gUHdAX+_Nci`CU!&*0)P@9Ui_G z&c0q1TR%P6QO)$VNzJ+X_U$uD9dhOEn%3Ljt&iAl`dq(OF7w;-2R*WvqUZTGYYA(0 zMKA2Pev$hqK>m=wM$CyP^*ik^Urv0#?NNDPYGj>Tl~w1jlF75S=3H+&T(J6sFqi1h zlmBmTsGri9e03?a@$VHur^6@yee+|}%;=)K^NTW*1tH0-dD->(VOt~D?Oomf@_GAp z@v=p|LX%7)q`S2FdZx(k`=2~_>JF(Jk^Q^3?_o};US-=Jw9%yh__?y!e?KGD6w^FA z)o%rTwcYt5Jj>BjHRIgl{-~%ZDKRlIK|w(|xwyEvyu3WVyL(at)J_5&DHrg1^0l%)uN&D6HzNBFPugI2bjN|os%y?ZzUrR)?dqC|<^Jn7 z^6sqs|I~b?oba>UJ9{JBy^rxkN%}|5GOBo9?d)O1RWkp_5-X`5@!Ksn9kW!{ z_)olYE9=C6kDpuve7-)^)F653|+clL4v=ksi~=ffk4)4as9Xp?km=)QhtN|)ia+SncqEd_F1j({2JW}S!=UxvL9bHS-tMl(MFr8G5a1* zIQY>zo!9oi!b^Wu&t;YA%(K6qS4uAky7cA1ud>BX+y_m{Jv}>T&64t0eJK9w)hnf) zdh8W1K%L(fg~|^P7;8Mes_J*0J129tL56kGttFE)r>aXPUy|$!-o&k&n7V`SOH#Tm z--(T;I_?W2`N4mh7 zhK7yzUcAVt5b3j4Q(LxW%asck0vsJ3E#5IQvmH>2iiwGdiw6ymll%{lNOV^*Hm!l#%ddT`~PQWn;$AX+|F+2^ue*IU$}1j1((i)y|&&= zHQ5|J7ZNSkV{8Lhv?B8$S zxRfP8^yTZ<;$mV&{ilvxSm>PS+OdTDkg%|tPlG*Qp7X3nB;7$ zSmsN{341YosLbf`l3MW3gsIWNM7G0}U6uL9d^yvskmQi7E9`q6Op<1O$`)^Yz0*1~hb~j2fW`{@v)Ue@G{WLk zVRG-^mko)BZ*9xf{_|SNwih(u_V8lG@5>)Qe%vrou|`?f!@paCo4b2;`1-D0B3~!; z=%=^5Zz?#%$jm0-t>M%1fAfnM1-ECp*(k)+{`>c@udlD=vXHli%#$ZixVX4PUX;&% z$lkl%e(n18`a+A(dH8qF`19w_g$n`N+S-BQE=3)EeQG(+S+bOq;$M9I@})!alz>!X zVq(kNIVEpyOq?-Sc*cgn8}i7PKFEILTk6{7%fG+7YduR` z`}yAL?=QAXrF5=cn_Ey|;OxZD&%Z8u$@^PdPv5;e_u0{B)&27J|GvGwy+=I0p{2Do zGCKP5o*Om!%F4>?j0J>+g{!~5a{Zk2d9SI7$)!t|Ec|zB>FVa*-j?g=$XFXyQS0?04ZHkRuUB3)VCqRrOLLQd@#@vBB~wf?&isA3 zWQoeHf3FX*yv$^An$&2O09pyPJ^%Qg4Y!}ae%-CE7^0PaZqCB@g=}nWkM?Nvc<#^n z_2S}UrI)P*tlM0z%HR29cgt85FgzDBx*@i+`7T!T*{ZH`5u0CV{f(j zd)E9rCini8Eccu1QUn@i;NS$Ee-OSd=HL{rd3o}@Hy&@c@0lBGqg@j++!xpSg>qCdW2cewNUIg8$MPKRe#SI=Y#3euPIez8cRc6(0X zv9)&E@pJY1BebsWQePk5So=_tVT#Re4xD6qJ^i0Qf8H-=8|Cl+{?5+gx3{(mmlnQa{Q70N|NI2&jn&`Zm6erk z$+YHRx$kLJb3;wvKkUu#mAp^htq80vEd24+q|;^6#rcZnXZq`7l_<<~7*v?=%Yw#&=?yH_Rf{Q3KP`m#w2AHTc1`@+V` z&uKTl?yvm3%wyUeMS)(~dB=Dr2!1-o^veHp*Nj&$|NNt;TU12EnwXtMH#em&(i3EAbm))(j38((ODBW~Gi9p~k*&-?p_t&?~3@;qMi^o-tN z`F(oPMvZ!NVj@4SkMdsc?|to;h&v0<%(B|5DQt7TeGA<%HzVo7@mD|GwcnL5U%B$< zhlhtRT?*po?>}(B;RcuD20oU?46Bf-)4FzXa5P@1_~wF|L=o8z|MWP71-a%H)36mi+sGFo&;P`SSk$`b5!hZ*E?`bEoG0y}dqxt1kE7-sSN#O*C@# zwHA^1`OCA5D~{N@GouKwT3meT zrpIcdr`O(9N?#Idxv^cbv1|M7w!Ur?OX&?)w{&kx>MoNtnRWW)sndn;nuF!edQMjB zmAC(Q$5Lsvt{}(3L!~pk-Z)n6-PrK!@*X9(DkYO8 zZ>Qh!jCYcc&HQxp{bbF)bD%!*rr=Ghxz$f*_ucyU<$PIGHUA=|SFfIV{IL%I z8Qt6Yr)S?c`K|RSm-qVpUt8xLmStsQv$D4S{qyI+-;ZkEbF>6#=>M5Jb7jQkO)8oz zwg&2QrkeU(?K)By!r7?61Hc_A;g zX3vpkb@eyzG1$NCePPY-o`bnsLGts8Vjo3rdG>Oy=-s87%PVIryVdzi`kMUP^xqpF zTvO_;|7vFy@~5t_V%^b$e$VYIpPfniZ0Mxq_%}07Fxb{NS#Ii%9TsfX@lT&V_4D(4 zlx!a2>bi8x7Lm)BXWTW3NXS{N$l}zqK;B_h<6#5Q1mjgl=5UJMe8|49=2>y$Pv4iJ z>fIinSNwSUcjNT<^PTGiGuM?aubjE;mgmOA)RpU2D3}Ll>hTsyP4ib~{<^%<_0Crl zw#{?(1E+VG_8!fXV!F6w<(d^KlILEVgid?#J0jrk=QUR!JZ>wmQ~5TL_2|k!J65|f zzw-a=_4vYNi7#dQ{{4Qxe{J;ku3avrQ>IS+`sU{6XJ=<;bVw_*R2WY4U6J*jl{nZ%BQ9ziRpE1^tV1m#j#CdR)HV z-g?t@pJOLL{aRh_Rh#5wi|!Zw`E~kA&4PPZ51hK0{Cv^DteeYyN+X_3t~$K2SHr$Q zdxm^>hpgec>PyRnf6KUUTkTi`ny6I=1!a`7E zp{{P9$GY1ZUrm@A6}o)B|K#S`9=~Jm&ZJJQy9IipnYqtbwCrVj^F_1!M0M4@-j0j& z%NMPe(G^?yGa=gj;8gjGFEaQ1%}g;DX>=%9zG$KDuT2*3n+=tx&Ap(~SZ~AgJSahQ zrp>*gAE%hx=UG-snXTs3>a%?C?@q$*?nbSs1FZ>;kKg()uC);0Xh~S{mVNS*tEFvG zdUvbT)2m-grAB`&@ipJMwKOo5{m*x+Yip;Qysn9Giz_fLKlOO=>f`#me%V;YM=jp| z@l2_;P1s8<&JS$@y;7hm3Dh|0bUUfHrRD5)f4=1mm(#=fCN4E~TlZn_=0oD)b{%T` z(546rQ=@PYcX}5Wt$Vh{&(f&wj_&D>1F3iG z;vhHK1E_d&PL9)-=_RNhgJNoa3`>xZuxAby< z?{9hPTpFak#pl|Oh;5TsdD&Dx*t+z&>*BCW3=(`hY-OA2s z+mu9Oqn`ym*;o4G)9gPxGFh4wHf3cQ7v^lyVwCvt^P@<7)q|R&jh|VA#aQe?$)&co z*7VP^uRq&gd^KS$yM2Ab6066-3p?NcTw*A8tvc`C%?th8*FM$q_CC#ZRb96FfsSwB ztzxU0Q}=a9`72Fie0$e@of@-{>;K6iI(k$JJmKV%Ns}gt>&KlqVGzdBsj_d|9ILW7KmPr#4xA3^&Ut}C$j(kqEG{@W_yj17 z()QQ(I;T(LTw?P3OXlU6>f7D(yPeZsZ_~f|?a8M*=hnPc-{^4jR_;$d>F>p2&!1(f z&i}Ia`neQe>znhsV|m%v{J#GsTJG>_`JHR`nbicxoDVuB#FHQ-$)Zx#49O&ixQMb2Hub$$8O`aPHOInWnQY-S}K8byaXybL@!|ukV(x zpLOJN)8`%W0*XRye6qhD9p#=SEdT7BlZBtR@bU5e`}dEBGx71s+qY*IABx>w z23j4vhnbD1!ChH-aXY{KrM1^;c zgNY}|dn%6p-rtM*I?BaLH3gK|wiVmj*u1db;ppgCA*7|D!NS5aW9H0@PU~WJHho?c zpq-tWd2!E%-+F(xnK515;qW&zZX%0quS3ECKD{@R$&2p?q&~|3IcwIeb91f5rT@n5 zELz&w$atlY-~X^PB!H`+8O8ujXq>g~oZgZ7a>Qxeq4?AAT6ADLOKfJCh zjBK2=XVtuEhSOg87pnE-U$`k$mB^=QnZGdHKYPohIZ=g`b%)NgU*Nu+W%6D|Qu61Y zKwqGt>Cx&-qbXa`Q~8Sf)jLy}P~UkcmWnLVIV?C9cZG_P#nh^UD>31)^?#Zj5T}aWj7Q@9{sgGjV%ZTEvZDQ_ZSt$02VAokd3)`~(`jM9_S{$TmEb#lxF(xt=fgdweD)%HCal@Q z6ejVoSX+%#&N^eo*LRB;Weq>x=j!$jog%{XRZ~s$e$B6$f0uJGTyi=5?!s?n?f=e2 zVqXqz*7Ew)dNkGWW9Eh1yS=4nF!vPvPc4dcRo#6jA!-J5Pri+Zdz>HJGoH`s=hkMo zPy5h6EjqjOY2&W98Lf?+z3iop_tx7N?f-Wo@m#Kh|5`KMDW^|qzu)h*Y`^KWorQZ% zb4CTHNjpr}?KBi=Q{3KmU&?3R9AS1-s~(;f zj}LcS-X33kuYJuj{v%&ldKqnRU#wo(ckl`i+Z6|4-s%hP26A^=o$~??CwlQQuaf!o zB&Il7gU5I7<}2dRXf!4{}+u` zcOEMqVY^UqK6AZSgfD<$Z5P%+Q0xA-11CFnf~(bZ62{ht$+Uh zHC!ECXY+1i!vp2W`paUh-vsLFf1O=E-_Lsel6yB4&U7nDH7z`RvGQ57SQM{dspBh1 zZM#L~zz4?Af`rBQH@J4X?Q!kcwC9$~-Bz!nET?_%Tn@{5c$(`a{Q>o*bU@{RV^x$8 zhyRCt-~N5b{GIa0^VDjuXRJ7Uj z`i%XbyN+&H*#d5MY*G23Vfo?W^#%WZ4`fe`@)5i*yZtEljrn3HZ8oiwjmnEN3Hx!~ z)u7?N!h_biWp|W0Wg#seRmRB7)<>V1-hC99bZF%bQ?* zkjwh5f1&I^-P~^Ta=z(x`>vK8n;yDe|K^rO-*~lzxAm5UZzK_=H$u3Ei%>5>cg79M(j>$mb=Q+KXN|HI~Xv)r%ku-I8C z_3yoi&dJlKAD1rqetNq8?-duEc9p(%TfejNg}=Av%NgtE+)Vv(`1)1vf8XajPG8%8 zy*Kvy)8`-mzl&~ZJFQb}J zl>Ghq^5h$Fxp}tLV&9hE+-lFle(CZghmcA4SKip#$Y;rPa_$^yqu~6~zQ4a-zTL-G z;r1%NVvol24XbZYoUXgZtm0jf+{#9gtrrAO?)cun=1()DfA}qa_rIAFKU-N9T-oa! zvsm=+VjJ~SXFr_#o&4bb{`#`AGA=G&(`t6p?+2UNCr_G`lbh@7>$~vX^G~(Al+yO* z8GB8hptAhl*_4z2zKb8%WHQKoUiE(B?R9787k^&x>DKc$=cU(H^4}`Fome#^{pYRf z%OQ-Zr+7V&KAPcQvfpyKTYS#OdsDZ(Z!LZK=jjz2PZgsDwfYL-lM@;K*y|rT_$-||!Ld`ppU zma+d{yy=dR-uL*{7TKEp6Mnoqq*`|4=hmo%2bSB~nz9{>Rz*ga@7~Mp6Zg#jUi}j5+$#-NatQ*H6gQFugp|t!@K&+`IS8D>z-`;b?bv~p{?i3{(4F#d1G;V9KWzq{(c zcy~?^&SHNM`TGOc%3Wz&Ladit$Mf^^wXR$@G1YM4)SxRbtWW+t@bK#4=u6wTudcuQ z=YGxf;Pab2rJTiOY<}N&@onrk$eCN>yLjh@+g~?G_a56TEnZP#dHuq%~OPjEL-B3hH-2|_`5QA8hkvzYMe`-=_p|wm zpQvjn)i^DDvwm~qpA)CdXT7h!n9O!qO@OI!PW}1R9dG~Nwid2fvCY2W{EgF>9!w1h z(p>7gXwy~=r=>!h!ls#NU0QZ2d-0|zN|}MTmQ2{|u%IuxICZ-hcml=J@k@#?Swqo0`Hmm1KY z-)HWOfYieWXWQ3Z<#dVOyx{e(>SkNs5}CgTr)lU#Jkq!3@z|oj(;!}GnOjWzA7c^ZixXLT!t}AW0vb1*B zqG=J{w*Sr+?qPlCm{s*+<&%rtACKRg@%_k!bB$s!e;y0ubqtx!yF5q0Gk=4tHDoG_Ausr3GhyG?=D;J?QL(&Q5u6x4Ma$>$^`qYMS%og81_W z+q1e0mhWc%Wcz!O1*=x3Y_wbK{IZ>2jxK%5JH5roJaA#ef;`7N9}e7S&F$7LOJCjJ zx#Qr!Ki{W#ofMN7?)m#Ps%@RN^6gz)I`}^C3xDW)o$dDVisxsyTU*;!>0Nk}zNAU| z;CVSiU&Ez8pS!nBHMaWj%6jXK{6(KP$~=F@By6vo_Rl@{^+v8~&%1Ork26Q?5WHAB z-9nCkZOtq%Hb0*l2aCtYH*Q=$JH{@0j``85mA-|%7Q3p;N+-OVGUu!7blHL@ui`uV zR;_DMmKT0AyUYG@Zu^_dULWhsDh-3<12f(J&C{*zOxh@6c(d{G+FE}30=c7COus!i z*qnWR9c#7J3y$~Nacg|9^lmvHK1sHvsbWpX3JK=ZOHZ%$FISc+*~Y!({=v=6Ka>5L zZ602lYjb4Yy_a^)^XDebe)gsM`@7uy{QSH;yX89!e2_t6FL@6b6|>RXgXwmGNt3ujitX7Yu;VImEx0M z`DOW6M!An`H$D5gTfgb$uiM){sI4#+5)zs=ZQAE&XE$%!^r+<0?1DSCZZ9MhR5yS7IsgK^Ty!cAv zRzS3G&{l3=p0tO|yONLdZCKvO7v#L+*5s_sX|Fs(-elG^>E<0=YUP&isk`A(Z9PNv zx2PqYOYZZzpJ8A*Q@mseN01J4&fKj(VIcxhYr*G$2g3(eH;1zcNZa4N6* z4!dZGh(4+jZ|D3x0d7I6X=FhdVw!WP8^jpb|`_Vaj*6;szfHm;* z`g#ekYpa{@nD@)uSYOh3mpr>Ot8L$+p1-!)8Oc++POf})Gw)dt&`yS8Y`&CW}0!Y1mQK8dD2d9Qi)Oyd-gEwano z0-jccovbhGO52~gXL{WHm-dGrSM_%mzcEu|^OM!8yc}t}u-LMq;oPRHUp6`XvMu=6 zcyC7IpAL7n4^8q{w?4g}{Y!&my0;^EA=^qfWQl=nBqWnZ%0{&(uGn`PdQe^y%~ zJip(~x|;ZV+1agIqITR!li@!sFITyNYpwr9(H}9P3)Uu`@;qHzK9{4n^w0HcoSFKm z)m}~W-f5-SXmZ&V``<`&-1Y8X=7!~seaebjU)Shlw@i}j^6CqTzptUEx6B~x+NlST z+RRz2I4>FPIeS2Hf<$!e6wAfo-W{9@j9lT`bJu-7W`F9pNWD7$-j5p^O3gTQ>K`>0 zvfVx+`qusRK8Jqclij){6T;csTcr~Gi@9IC%yUYZb@!QM;jd<$zzMN)U)iN)-dy%1 z}PFcGSl_@x&JH=^_={2;l@8T2i6{J&;KcX zhJAkizT$Tu=N}Dy|6{+kcFNq=IXr*%@8&to_AfD8tK{6z*z5DYefs3|CX<<&+3q`c z)vnnIvZ}8QHD#uXT$vE||IMV;?)8Y-nkKgd! z=>1$Ov%fiabBp*v*Z+3+Q;H_G$7*Y9%iC6!JU=(rLZ&ZL`uu|I#aE_S^h^ES%y#I> z!lpSU?))b-Xa1C{UiU#?%Q_ueg>Ysp^HZwZyYJMEr#~N^X`lTptER8?Lg~rS`6e4| zwCletJj4C%?@qs{BP>qNcO6&1QS6GcnRNYY$Jv{)z3ZPWSz5!d${P2ZCu!pi&ERD> zZr;4RuXc8%^g1K`pRUWkPMY|rz)R+z+JH9X8`PfA4&FnOZAGd8yPsg|aT*_1}^+H4= zFH1;%xp_|Qm!nN5H}7w^d?R*uVyg4G)!wT-cK&g2EtalnW?@XKwEtCLdghhpc~yDq zvNLDS^vs+mw`!4wIxpY9*tSpauTH-lmlvOR$lrGEym@&!IcpXzO1gRE%gf6zr3zB~ z7f&$acK-A2*W4L1?^{itESPJpobuD^&-t&Z`=(sqVp=ZmE@6E86UR4`3z|xDYp#SD zx~9fYJ8>X1vvsuxZ|M)k+w#)_yks4<1~IJJ;51UjJv+I*58N7VY-o5Yd?soq!_GGIFo5sVFX=i60-KIM4LPC;R^^{pb$E}2fg^i7k zrEMwqD-I&iB>yfIS&Re+O`Jd&a4ws$|j-O^~nN@oZeBU~;IQh!%4VsTc&WkMLQXWVaUWKXS@tpgV8@pLDdWqp9e!9jAKiBT)|olM0f&PwON-7wD7I$J znnQ;U3FN+g_RKAM{;w4e+)M8=oyflFx+8 z+w1fePyfDPj$YL4HBWyAc^|8tZ&BEEbe-!L2HU&7H?pcC-Gth^GCW`3F!D~?>;`X( z28pjRVA*>>OLuSRW)aI3^*i#GXn%Y2TA+DZqN3}qG#wcqfg`-m_qP048?Z9-_4%fQ zA#-ONUvT^M_3l-zD{Ve=g9D!U{d>8>T{=1TpX>T=Yodjm#o@-udVDmZApex|1z zHFxr(f9^Hewz?;jrF?7uZa@C;+~-3fi|@;tF5JOw`sD4bH`=MXlHW^XuhltsKHL0a zpSMh3`@|ZfBmU{R&*K8-Un=-=w&{9J)eL71z15pOSj{atvGuT;sZ`0`zo4a0PH$d* zn^%~@T(xUNLAA6y)DptY#vZ!`g&i#FJuPyBP%%MK_$#Ylp*E!FR``2F4Ebd>`cG}N& zzRZgC&!v8A&T>21>A^p9_c712AMP3N*)7?=sq^dCCG(0lot-?ZR5Dw9-={MjK^*sg0;tOs5wH*%p#3HIH^Je}t#fuj&f@<97PZslP z?GVgpnS4aRNN=gM{J$Ojd;F_5-ukZidtL61UBBDT=H2_=Vy7eHbL-aGvjP*U_U_f! z_}Vh(?XJzmou8Oa1iSjn-*l|;V8}{I*^+%-@0Qg56II6r>QT+Ml$H>mr5-?E64Q#tjvsoxV`boI%} z&I3}f^7LkfP1v!ayt;V$*NPb@#Z4-l4tM?%2J1zGZRSwqu3QEQIeSt|;?z zJooR*Rdu$uDAt1$cgvPeHH@Dh_o?{%Ni+Yd1oiID)IR}78I}IVuHQeYT+aHC?W>=L z!80d4F1x36{M4$o)u#L7CqG%0C~fz0-E{@4i#f**N9>D9U&r8o;JSI#?4Fm44`g&X z6u_$Y>Y%l$Do?NbxF+g_T>AA${MD~7i@N2-w(e^c&3rDX8E}M`|4;0NZ8wDewQgAvg!RjarzKlyrgO=H1P=d-+RpVuCrGe;&l zIr-GamX?-@6DQ7{J9p|-(a>V0IpTXAcrs`s>jtd!K$i4(a)ZH!i}&v4E) z^}MQQVpw4#d=%UcIP^|;?XmDY)4e@SIZA@gKRzzatd(l~uy|fXtY751cTKM^-)&ax ziaKK>#Kg=zb*lBELxOAfRDMo7664|L=hh`BC-?UDk)_X$sqEh&cj~u@0t4uFuhz+F zbrUnif=#TZwOXmDq@0~)dgqv>r@y~_^*5cT5*wGFJ$qKLy6W1R$SvQ#9B5>2RgsgN z85SS-BrkkNeNcV&dHqg4o@?cxh5@UV_GOV2eh&RRxVl!JpgsU)?pdy=%B->sD=TZR@ReXJzD$ zD_2i`r0Bm{U07Rf`Kt8Ys>ZV>PI8-Jo`0`T)*58%iRMK=HpR@{|4!=Ei z{bBr4Vy?K~=69{Do>i5UfE7n?>B;r|zOq^8Z^vF)x^(Hsk00gjY7(v~{PUZ4|D(yj z`@b6b7C){yan~(>=2aI*{v}h_?XH)q+`~{U^}@pYtMdh+-u-(j9$l53?xz=&|5Zjg z)Q+!s^LPECT|dvn_m>OG_Sr-nZT}$abo%)!rd-irzo@W(|EjF2C#Tgb-;2{TH{X8Y z!i8_|l0$ORGj2H*yw`JmVwNqsH15sqj}t*Qfm#nop1rC2@-S=0)!=*G)3(@6x8ARl zyLw?{+rNnt*WWkxy?&SdW=Da|$GztM-_u+z#Wv=%m@HJW=IH%3LFTU8(e8k9;Cv{T1M}ln#&*F=_^e;RPuvoNYsY(cW0OMu zrZ3O$ddNa|`D|x^Sg+2@dHSZi^&a=1vKCyNtPdR}|UKeAaa6MoRpv-^4pZFO6x1qM}x+(Vf=+f771Tm3-PeLwbLhpVXl{CA>Mc=cIeX zqccz6XpC2Y6|mE0-8;c8dhy4W&C`#w{(EdUtIRu*zxU~L<~&}zn$!0a?E2r`_}-o5 zeChvdMw25hDtfzWoUa(XPHQ=~i*$Q zcUNaBm)2HZ`xxqHtyLrc$)HMO;ljLKzm!Cnx0SFyn>%4m{+i^~%O|pBJb#$rA*O6-cjNAXYL;B{(p8G@6MWhEc#Bxvx5h;56pWecHweU``Yd5 z>KZ5cjKboN?2C05xE%3+L5(e2{J&oZzedaN7giBr%Y3E3KV*y4+;AAx{^=nZ5ip=TP^kZ{uI+m}yr^3TOFZ`diLRxvP@_yf}Cw=^NDNOCF)qn3$Q2*j~ zXu_pSK~MC%6+XXto4Msvln~eB+OQvP>9v_5SLW>3HA&rY=Mb&B&&@(i1{?PtC9OHVXtaTOsG=I1heq50EqQ+YHAv+qpD+`poy!ZWX`1@Ob;#{@F`D6?5B4e|r}Bc-FZs9TpX}J&!$`XP^FM zxI?xP`$N8U*b}7 z^L(#`vI&rdSw|G zT#@RBk@{S&x9%H!cGWjI`TNuKNpBV?c16{^=stB`>QBV&X2WV(o@1*%ZH{jcTh}x% z&!Xb<@%2`Ga}QU7?r3~1`sLKAQzs{@i;IbsIaH{BikhT#?u_K7Cw!l>w*_`9g?spL z*GvC9UDtf$-k+UUuI`pgQ~bU7>e}YzfATMXC^L2k4R1PazHc(c{8d6~NNM`&SKGoK z1T(z_Pdl;Lz3&Z;s#=|fBk$4CSA{-epal)^W^%{cDc!B(Zws* z>#Jqn+35S2{<<3|eCkbr^SyN^_XS;>R2Y9f^{x4o47=`kyN^7qkT~hCAlB+@?qT5N z^C#HF>gXy~uS(t6ylcC*O#Z%L zPTu1M(EciDa;|HSzU1w^S@R2jbvW(QEUI~b|Ly#KPLI^-pDv{BD{yU_rM-{y;=#Dg z%7afN{Juu+i{i2tJnDxu>BelP~|?^U;zn?DXP+qiGum0-VZHzQV^j@bQSYP#E=#WyarvdMYg z3>52;QMLq4xJ^l1wf5)g9N)`{$?}z+FZb0x{jv3gaaM=W{HjMY&fU1�qKx^aNb` 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 diff --git a/docs/_static/mcpwm-overview.png b/docs/_static/mcpwm-overview.png deleted file mode 100644 index 6d3204466a5dedc1f5202756332e1998eaa237ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18209 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 - covers how to allocate various MCPWM objects, like timers, operators, comparators, generators and so on. These objects are the basis of the following IO setting and control functions. +- `Timer Operations and Events <#timer-operations-and-events>`__ - describes control functions and event callbacks that supported by the MCPWM timer. +- `Comparator Operations and Events `__ - describes control functions and event callbacks that supported by the MCPWM comparator. +- `Generator Actions on Events <#generator-actions-on-events>`__ - describes how to set actions for MCPWM generators on particular events that generated by the MCPWM timer and comparators. +- `Classical PWM Waveforms and Generator Configurations <#classical-pwm-waveforms-and-generator-configurations>`__ - demonstrates some classical PWM waveforms that can be achieved by configuring generator actions. +- `Dead Time <#dead-time>`__ - describes how to set dead time for MCPWM generators. +- `Classical PWM Waveforms and Dead Time Configurations <#classical-pwm-waveforms-and-dead-time-configurations>`__ - demonstrates some classical PWM waveforms that can be achieved by configuring dead time. +- `Carrier Modulation <#carrier-modulation>`__ - describes how to set modulate a high frequency onto the final PWM waveforms. +- `Faults and Brake Actions <#faults-and-brake-actions>`__ - describes how to set brake actions for MCPWM operators on particular fault event. +- `Generator Force Actions <#generator-force-actions>`__ - describes how to control the generator output level asynchronously in a forceful way. +- `Synchronization <#synchronization>`__ - describes how to synchronize the MCPWM timers and get a fixed phase difference between the generated PWM signals. +- `Capture <#capture>`__ - describes how to use the MCPWM capture module to measure the pulse width of a signal. +- `Power Management <#power-management>`__ - describes how different source clock will affect power consumption. +- `IRAM Safe <#iram-safe>`__ - describes tips on how to make the RMT interrupt work better along with a disabled cache. +- `Thread Safety <#thread-safety>`__ - lists which APIs are guaranteed to be thread safe by the driver. +- `Kconfig Options <#kconfig-options>`__ - lists the supported Kconfig options that can bring different effects to the driver. -.. figure:: ../../../_static/mcpwm-block-diagram.png - :align: center - :alt: MCPWM Block Diagram - :figclass: align-center +Resource Allocation and Initialization +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - MCPWM Block Diagram +As displayed in the diagram above, the MCPWM peripheral consists of several submodules. Each submodule has its own resource allocation, which is described in the following sections. -Description of this API starts with configuration of MCPWM's **Timer** and **Generator** submodules to provide the basic motor control functionality. Then it discusses more advanced submodules and functionalities of a **Fault Handler**, signal **Capture** and **Carrier**. +MCPWM Timers +~~~~~~~~~~~~ -Contents --------- +You can allocate a MCPWM timer object by calling :cpp:func:`mcpwm_new_timer` function, with a configuration structure :cpp:type:`mcpwm_timer_config_t` as the parameter. The configuration structure is defined as: -* `Configure`_ a basic functionality of the outputs -* `Operate`_ the outputs to drive a motor -* `Adjust`_ how the motor is driven -* `Synchronize`_ sync timers to work together -* `Capture`_ external signals to provide additional control over the outputs -* Use `Fault Handler`_ to detect and manage faults -* Add a higher frequency `Carrier`_, if output signals are passed through an isolation transformer -* Extra configuration of `Resolution`_. +- :cpp:member:`mcpwm_timer_config_t::group_id` specifies the MCPWM group ID. The ID should belong to [0, :c:macro:`SOC_MCPWM_GROUPS` - 1] range. Please note, timers located in different groups are totally independent. +- :cpp:member:`mcpwm_timer_config_t::clk_src` sets the clock source of the timer. +- :cpp:member:`mcpwm_timer_config_t::resolution_hz` set the expected resolution of the timer, the driver internally will set a proper divider based on the clock source and the resolution. +- :cpp:member:`mcpwm_timer_config_t::count_mode` sets the count mode of the timer. +- :cpp:member:`mcpwm_timer_config_t::period_ticks` sets the period of the timer, in ticks (the tick resolution is set in the :cpp:member:`mcpwm_timer_config_t::resolution_hz`). +- :cpp:member:`mcpwm_timer_config_t::update_period_on_empty` sets whether to update the period value when the timer counts to zero. +- :cpp:member:`mcpwm_timer_config_t::update_period_on_sync` sets whether to update the period value when the timer takes a sync signal. + +The :cpp:func:`mcpwm_new_timer` will return a pointer to the allocated timer object if the allocation succeeds. Otherwise, it will return error code. Specifically, when there are no more free timers in the MCPWM group, this function will return :c:macro:`ESP_ERR_NOT_FOUND` error. [1]_ + +On the contrary, calling :cpp:func:`mcpwm_del_timer` function will free the allocated timer object. + +MCPWM Operators +~~~~~~~~~~~~~~~ + +You can allocate a MCPWM operator object by calling :cpp:func:`mcpwm_new_operator` function, with a configuration structure :cpp:type:`mcpwm_operator_config_t` as the parameter. The configuration structure is defined as: + +- :cpp:member:`mcpwm_operator_config_t::group_id` specifies the MCPWM group ID. The ID should belong to [0, :c:macro:`SOC_MCPWM_GROUPS` - 1] range. Please note, operators located in different groups are totally independent. +- :cpp:member:`mcpwm_operator_config_t::update_gen_action_on_tez` sets whether to update the generator action when the timer counts to zero. Here and below, the timer refers to the one that is connected to the operator by :cpp:func:`mcpwm_operator_connect_timer`. +- :cpp:member:`mcpwm_operator_config_t::update_gen_action_on_tep` sets whether to update the generator action when the timer counts to peak. +- :cpp:member:`mcpwm_operator_config_t::update_gen_action_on_sync` sets whether to update the generator action when the timer takes a sync signal. +- :cpp:member:`mcpwm_operator_config_t::update_dead_time_on_tez` sets whether to update the dead time when the timer counts to zero. +- :cpp:member:`mcpwm_operator_config_t::update_dead_time_on_tep` sets whether to update the dead time when the timer counts to peak. +- :cpp:member:`mcpwm_operator_config_t::update_dead_time_on_sync` sets whether to update the dead time when the timer takes a sync signal. + +The :cpp:func:`mcpwm_new_operator` will return a pointer to the allocated operator object if the allocation succeeds. Otherwise, it will return error code. Specifically, when there are no more free operators in the MCPWM group, this function will return :c:macro:`ESP_ERR_NOT_FOUND` error. [1]_ + +On the contrary, calling :cpp:func:`mcpwm_del_operator` function will free the allocated operator object. + +MCPWM Comparators +~~~~~~~~~~~~~~~~~ + +You can allocate a MCPWM comparator object by calling :cpp:func:`mcpwm_new_comparator` function, with a MCPWM operator handle and configuration structure :cpp:type:`mcpwm_comparator_config_t` as the parameter. The operator handle is created by :cpp:func:`mcpwm_new_operator`. The configuration structure is defined as: + +- :cpp:member:`mcpwm_comparator_config_t::update_cmp_on_tez` sets whether to update the compare threshold when the timer counts to zero. +- :cpp:member:`mcpwm_comparator_config_t::update_cmp_on_tep` sets whether to update the compare threshold when the timer counts to peak. +- :cpp:member:`mcpwm_comparator_config_t::update_cmp_on_sync` sets whether to update the compare threshold when the timer takes a sync signal. + +The :cpp:func:`mcpwm_new_comparator` will return a pointer to the allocated comparator object if the allocation succeeds. Otherwise, it will return error code. Specifically, when there are no more free comparators in the MCPWM operator, this function will return :c:macro:`ESP_ERR_NOT_FOUND` error. [1]_ + +On the contrary, calling :cpp:func:`mcpwm_del_comparator` function will free the allocated comparator object. + +MCPWM Generators +~~~~~~~~~~~~~~~~ + +You can allocate a MCPWM generator object by calling :cpp:func:`mcpwm_new_generator` function, with a MCPWM operator handle and configuration structure :cpp:type:`mcpwm_generator_config_t` as the parameter. The operator handle is created by :cpp:func:`mcpwm_new_operator`. The configuration structure is defined as: + +- :cpp:member:`mcpwm_generator_config_t::gen_gpio_num` sets the GPIO number used by the generator. +- :cpp:member:`mcpwm_generator_config_t::invert_pwm` sets whether to invert the PWM signal. +- :cpp:member:`mcpwm_generator_config_t::io_loop_back` sets whether to enable the loop back mode. It is for debugging purposes only. It enables both the GPIO's input and output ability through the GPIO matrix peripheral. + +The :cpp:func:`mcpwm_new_generator` will return a pointer to the allocated generator object if the allocation succeeds. Otherwise, it will return error code. Specifically, when there are no more free generators in the MCPWM operator, this function will return :c:macro:`ESP_ERR_NOT_FOUND` error. [1]_ + +On the contrary, calling :cpp:func:`mcpwm_del_generator` function will free the allocated generator object. + +MCPWM Faults +~~~~~~~~~~~~ + +There are two types of faults: A fault signal reflected from the GPIO and a fault generated by software. To allocate a GPIO fault object, you can call :cpp:func:`mcpwm_new_gpio_fault` function, with configuration structure :cpp:type:`mcpwm_gpio_fault_config_t` as the parameter. The configuration structure is defined as: + +- :cpp:member:`mcpwm_gpio_fault_config_t::group_id` sets the MCPWM group ID. The ID should belong to [0, :c:macro:`SOC_MCPWM_GROUPS` - 1] range. Please note, GPIO fault located in different groups are totally independent, i.e. GPIO fault in group 0 can not be detected by the operator in group 1. +- :cpp:member:`mcpwm_gpio_fault_config_t::gpio_num` sets the GPIO number used by the fault. +- :cpp:member:`mcpwm_gpio_fault_config_t::active_level` sets the active level of the fault signal. +- :cpp:member:`mcpwm_gpio_fault_config_t::pull_up` and :cpp:member:`mcpwm_gpio_fault_config_t::pull_down` set whether to pull up and/or pull down the GPIO internally. +- :cpp:member:`mcpwm_gpio_fault_config_t::io_loop_back` sets whether to enable the loop back mode. It is for debugging purposes only. It enables both the GPIO's input and output ability through the GPIO matrix peripheral. + +The :cpp:func:`mcpwm_new_gpio_fault` will return a pointer to the allocated fault object if the allocation succeeds. Otherwise, it will return error code. Specifically, when there are no more free GPIO faults in the MCPWM group, this function will return :c:macro:`ESP_ERR_NOT_FOUND` error. [1]_ + +Software fault object can be used to trigger a fault by calling a function :cpp:func:`mcpwm_soft_fault_activate` instead of waiting for a real fault signal on the GPIO. A software fault object can be allocated by calling :cpp:func:`mcpwm_new_soft_fault` function, with configuration structure :cpp:type:`mcpwm_soft_fault_config_t` as the parameter. Currently this configuration structure is left for future purpose. :cpp:func:`mcpwm_new_soft_fault` function will return a pointer to the allocated fault object if the allocation succeeds. Otherwise, it will return error code. Specifically, when there are no memory left for the fault object, this function will return :c:macro:`ESP_ERR_NO_MEM` error. Although the software fault and GPIO fault are of different types, but the returned fault handle is of the same type. + +On the contrary, calling :cpp:func:`mcpwm_del_fault` function will free the allocated fault object, this function works for both software and GPIO fault. + +MCPWM Sync Sources +~~~~~~~~~~~~~~~~~~ + +The sync source is what can be used to synchronize the MCPWM timer and MCPWM capture timer. There're three types of sync sources: A sync source reflected from the GPIO, a sync source generated by software and a sync source generated by MCPWM timer event. + +To allocate a GPIO sync source, you can call :cpp:func:`mcpwm_new_gpio_sync_src` function, with configuration structure :cpp:type:`mcpwm_gpio_sync_src_config_t` as the parameter. The configuration structure is defined as: + +- :cpp:member:`mcpwm_gpio_sync_src_config_t::group_id` sets the MCPWM group ID. The ID should belong to [0, :c:macro:`SOC_MCPWM_GROUPS` - 1] range. Please note, GPIO sync source located in different groups are totally independent, i.e. GPIO sync source in group 0 can not be detected by the timers in group 1. +- :cpp:member:`mcpwm_gpio_sync_src_config_t::gpio_num` sets the GPIO number used by the sync source. +- :cpp:member:`mcpwm_gpio_sync_src_config_t::active_neg` sets whether the sync signal is active on falling edge. +- :cpp:member:`mcpwm_gpio_sync_src_config_t::pull_up` and :cpp:member:`mcpwm_gpio_sync_src_config_t::pull_down` set whether to pull up and/or pull down the GPIO internally. +- :cpp:member:`mcpwm_gpio_sync_src_config_t::io_loop_back` sets whether to enable the loop back mode. It is for debugging purposes only. It enables both the GPIO's input and output ability through the GPIO matrix peripheral. + +The :cpp:func:`mcpwm_new_gpio_sync_src` will return a pointer to the allocated sync source object if the allocation succeeds. Otherwise, it will return error code. Specifically, when there are no more free GPIO sync sources in the MCPWM group, this function will return :c:macro:`ESP_ERR_NOT_FOUND` error. [1]_ + +To allocate a Timer event sync source, you can call :cpp:func:`mcpwm_new_timer_sync_src` function, with configuration structure :cpp:type:`mcpwm_timer_sync_src_config_t` as the parameter. The configuration structure is defined as: + +- :cpp:member:`mcpwm_timer_sync_src_config_t::timer_event` specifies on what timer event to generate the sync signal. +- :cpp:member:`mcpwm_timer_sync_src_config_t::propagate_input_sync` sets whether to propagate the input sync signal (i.e. the input sync signal will be routed to its sync output). + +The :cpp:func:`mcpwm_new_timer_sync_src` will return a pointer to the allocated sync source object if the allocation succeeds. Otherwise, it will return error code. Specifically, if a sync source has been allocated from the same timer before, this function will return :c:macro:`ESP_ERR_INVALID_STATE` error. + +Last but not least, to allocate a software sync source, you can call :cpp:func:`mcpwm_new_soft_sync_src` function, with configuration structure :cpp:type:`mcpwm_soft_sync_config_t` as the parameter. Currently this configuration structure is left for future purpose. :cpp:func:`mcpwm_new_soft_sync_src` will return a pointer to the allocated sync source object if the allocation succeeds. Otherwise, it will return error code. Specifically, when there are no memory left for the sync source object, this function will return :c:macro:`ESP_ERR_NO_MEM` error. Please note, to make a software sync source take effect, don't forget to call :cpp:func:`mcpwm_soft_sync_activate`. + +On the contrary, calling :cpp:func:`mcpwm_del_sync_src` function will free the allocated sync source object, this function works for all types of sync sources. + +MCPWM Capture Timer and Channels +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The MCPWM group has a dedicated timer which is used to capture the timestamp when specific event occurred. The capture timer is connected with several independent channels, each channel is assigned with a GPIO. + +To allocate a capture timer, you can call :cpp:func:`mcpwm_new_capture_timer` function, with configuration structure :cpp:type:`mcpwm_capture_timer_config_t` as the parameter. The configuration structure is defined as: + +- :cpp:member:`mcpwm_capture_timer_config_t::group_id` sets the MCPWM group ID. The ID should belong to [0, :c:macro:`SOC_MCPWM_GROUPS` - 1] range. +- :cpp:member:`mcpwm_capture_timer_config_t::clk_src` sets the clock source of the capture timer. + +The :cpp:func:`mcpwm_new_capture_timer` will return a pointer to the allocated capture timer object if the allocation succeeds. Otherwise, it will return error code. Specifically, when there are no free capture timer left in the MCPWM group, this function will return :c:macro:`ESP_ERR_NOT_FOUND` error. [1]_ + +Next, to allocate a capture channel, you can call :cpp:func:`mcpwm_new_capture_channel` function, with a capture timer handle and configuration structure :cpp:type:`mcpwm_capture_channel_config_t` as the parameter. The configuration structure is defined as: + +- :cpp:member:`mcpwm_capture_channel_config_t::gpio_num` sets the GPIO number used by the capture channel. +- :cpp:member:`mcpwm_capture_channel_config_t::prescale` sets the prescaler of the input signal. +- :cpp:member:`mcpwm_capture_channel_config_t::pos_edge` and :cpp:member:`mcpwm_capture_channel_config_t::neg_edge` set whether to capture on the positive and/or negative edge of the input signal. +- :cpp:member:`mcpwm_capture_channel_config_t::pull_up` and :cpp:member:`mcpwm_capture_channel_config_t::pull_down` set whether to pull up and/or pull down the GPIO internally. +- :cpp:member:`mcpwm_capture_channel_config_t::invert_cap_signal` sets whether to invert the capture signal. +- :cpp:member:`mcpwm_capture_channel_config_t::io_loop_back` sets whether to enable the loop back mode. It is for debugging purposes only. It enables both the GPIO's input and output ability through the GPIO matrix peripheral. + +The :cpp:func:`mcpwm_new_capture_channel` will return a pointer to the allocated capture channel object if the allocation succeeds. Otherwise, it will return error code. Specifically, when there are no free capture channel left in the capture timer, this function will return :c:macro:`ESP_ERR_NOT_FOUND` error. + +On the contrary, calling :cpp:func:`mcpwm_del_capture_channel` and :cpp:func:`mcpwm_del_capture_timer` function will free the allocated capture channel and timer object accordingly. + +Timer Operations and Events +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Register Event Callbacks +~~~~~~~~~~~~~~~~~~~~~~~~ + +The MCPWM timer can generate different events at runtime. If you have some function that should be called when particular event happens, you should hook your function to the interrupt service routine by calling :cpp:func:`mcpwm_timer_register_event_callbacks`. The callback function prototype is declared in :cpp:type:`mcpwm_timer_event_cb_t`. All supported event callbacks are listed in the :cpp:type:`mcpwm_timer_event_callbacks_t`: + +- :cpp:member:`mcpwm_timer_event_callbacks_t::on_full` sets callback function for timer when it counts to peak value. +- :cpp:member:`mcpwm_timer_event_callbacks_t::on_empty` sets callback function for timer when it counts to zero. +- :cpp:member:`mcpwm_timer_event_callbacks_t::on_stop` sets callback function for timer when it is stopped. + +The callback functions above are called within the ISR context, so they should **not** attempt to block (e.g., make sure that only FreeRTOS APIs with ``ISR`` suffix is called within the function). + +The parameter ``user_data`` of :cpp:func:`mcpwm_timer_register_event_callbacks` function is used to save user's own context, it will be passed to each callback function directly. + +This function will lazy install interrupt service for the MCPWM timer without enabling it. It is only allowed to be called before before :cpp:func:`mcpwm_timer_enable`, otherwise the :c:macro:`ESP_ERR_INVALID_STATE` error will be returned. See also `Enable and Disable timer <#enable-and-disable-timer>`__ for more information. + +Enable and Disable Timer +~~~~~~~~~~~~~~~~~~~~~~~~ + +Before doing IO control to the timer, user needs to enable the timer first, by calling :cpp:func:`mcpwm_timer_enable`. Internally, this function will: + +* switch the timer state from **init** to **enable**. +* enable the interrupt service if it has been lazy installed by :cpp:func:`mcpwm_timer_register_event_callbacks`. +* acquire a proper power management lock if a specific clock source (e.g. PLL_160M clock) is selected. See also `Power management <#power-management>`__ for more information. + +On the contrary, calling :cpp:func:`mcpwm_timer_disable` will put the timer driver back to **init** state, disable the interrupts service and release the power management lock. + +Start and Stop Timer +~~~~~~~~~~~~~~~~~~~~ + +The basic IO operation of a timer is to start and stop. Calling :cpp:func:`mcpwm_timer_start_stop` with different :cpp:type:`mcpwm_timer_start_stop_cmd_t` commands can start the timer immediately or stop the timer at a specific event. What're more, you can even start the timer for only one round, that means, the timer will count to peak value or zero, and then stop itself. + +Connect Timer with Operator +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The allocated MCPWM Timer should be connected with a MCPWM operator by calling :cpp:func:`mcpwm_operator_connect_timer`, so that the operator can take that timer as its time base, and generate the required PWM waves. Make sure the MCPWM timer and operator are in the same group, otherwise, this function will return :c:macro:`ESP_ERR_INVALID_ARG` error. + +Comparator Operations and Events +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Register Event Callbacks +~~~~~~~~~~~~~~~~~~~~~~~~ + +The MCPWM comparator can inform the user when the timer counter equals to the compare value. If you have some function that should be called when this event happens, you should hook your function to the interrupt service routine by calling :cpp:func:`mcpwm_comparator_register_event_callbacks`. The callback function prototype is declared in :cpp:type:`mcpwm_compare_event_cb_t`. All supported event callbacks are listed in the :cpp:type:`mcpwm_comparator_event_callbacks_t`: + +- :cpp:member:`mcpwm_comparator_event_callbacks_t::on_reach` sets callback function for comparator when the timer counter equals to the compare value. + +The callback function will provide event specific data of type :cpp:type:`mcpwm_compare_event_data_t` to the user. The callback function is called within the ISR context, so is should **not** attempt to block (e.g., make sure that only FreeRTOS APIs with ``ISR`` suffix is called within the function). + +The parameter ``user_data`` of :cpp:func:`mcpwm_comparator_register_event_callbacks` function is used to save user's own context, it will be passed to the callback function directly. + +This function will lazy install interrupt service for the MCPWM comparator, whereas the service can only be removed in :cpp:type:`mcpwm_del_comparator`. + +Set Compare Value +~~~~~~~~~~~~~~~~~ + +You can set the compare value for the MCPWM comparator at runtime by calling :cpp:func:`mcpwm_comparator_set_compare_value`. There're a few points to note: + +- New compare value might won't take effect immediately. The update time for the compare value is set by :cpp:member:`mcpwm_comparator_config_t::update_cmp_on_tez` or :cpp:member:`mcpwm_comparator_config_t::update_cmp_on_tep` or :cpp:member:`mcpwm_comparator_config_t::update_cmp_on_sync`. +- Make sure the operator has connected to one MCPWM timer already by :cpp:func:`mcpwm_operator_connect_timer`. Otherwise, it will return error code :c:macro:`ESP_ERR_INVALID_STATE`. +- The compare value shouldn't exceed timer's count peak, otherwise, the compare event will never got triggered. + +Generator Actions on Events +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Set Generator Action on Timer Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +One generator can set multiple actions on different timer events, by calling :cpp:func:`mcpwm_generator_set_actions_on_timer_event` with variable number of action configurations. The action configuration is defined in :cpp:type:`mcpwm_gen_timer_event_action_t`: + +- :cpp:member:`mcpwm_gen_timer_event_action_t::direction` specific the timer direction. The supported directions are listed in :cpp:type:`mcpwm_timer_direction_t`. +- :cpp:member:`mcpwm_gen_timer_event_action_t::event` specifies the timer event. The supported timer events are listed in :cpp:type:`mcpwm_timer_event_t`. +- :cpp:member:`mcpwm_gen_timer_event_action_t::action` specifies the generator action to be taken. The supported actions are listed in :cpp:type:`mcpwm_generator_action_t`. + +There's a helper macro :c:macro:`MCPWM_GEN_TIMER_EVENT_ACTION` to simplify the construction of a timer event action entry. + +Please note, the argument list of :cpp:func:`mcpwm_generator_set_actions_on_timer_event` **must** be terminated by :c:macro:`MCPWM_GEN_TIMER_EVENT_ACTION_END`. + +Set Generator Action on Compare Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +One generator can set multiple actions on different compare events, by calling :cpp:func:`mcpwm_generator_set_actions_on_compare_event` with variable number of action configurations. The action configuration is defined in :cpp:type:`mcpwm_gen_compare_event_action_t`: + +- :cpp:member:`mcpwm_gen_compare_event_action_t::direction` specific the timer direction. The supported directions are listed in :cpp:type:`mcpwm_timer_direction_t`. +- :cpp:member:`mcpwm_gen_compare_event_action_t::comparator` specifies the comparator handle. See `MCPWM Comparators <#mcpwm-comparators>`__ for how to allocate a comparator. +- :cpp:member:`mcpwm_gen_compare_event_action_t::action` specifies the generator action to be taken. The supported actions are listed in :cpp:type:`mcpwm_generator_action_t`. + +There's a helper macro :c:macro:`MCPWM_GEN_COMPARE_EVENT_ACTION` to simplify the construction of a compare event action entry. + +Please note, the argument list of :cpp:func:`mcpwm_generator_set_actions_on_compare_event` **must** be terminated by :c:macro:`MCPWM_GEN_COMPARE_EVENT_ACTION_END`. + +Classical PWM Waveforms and Generator Configurations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This section will demonstrate the classical PWM waveforms that can be generated by the pair of the generators. The code snippet that is used to generate the waveforms is also provided below the diagram. Some general summary: + +- The **Symmetric** or **Asymmetric** of the waveforms are determined by the count mode of the MCPWM timer. +- The **active level** of the waveform pair is determined by the level of the PWM with a smaller duty cycle. +- The period of the PWM waveform is determined by the timer's period and count mode. +- The duty cycle of the PWM waveform is determined by the generator's various action combinations. + +Asymmetric Single Edge Active High +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. wavedrom:: /../_static/diagrams/mcpwm/single_edge_asym_active_high.json + +.. code:: c + + static void gen_action_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) + { + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(gena, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(genb, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(genb, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpb, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + } + +Asymmetric Single Edge Active Low +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. wavedrom:: /../_static/diagrams/mcpwm/single_edge_asym_active_low.json + +.. code:: c + + static void gen_action_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) + { + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(gena, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_FULL, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(genb, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_FULL, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(genb, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpb, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + } + +Asymmetric Pulse Placement +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. wavedrom:: /../_static/diagrams/mcpwm/pulse_placement_asym.json + +.. code:: c + + static void gen_action_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) + { + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpb, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(genb, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_TOGGLE), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + } + +Asymmetric Dual Edge Active Low +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. wavedrom:: /../_static/diagrams/mcpwm/dual_edge_asym_active_low.json + +.. code:: c + + static void gen_action_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) + { + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_DOWN, cmpb, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(genb, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_DOWN, MCPWM_TIMER_EVENT_FULL, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + } + +Symmetric Dual Edge Active Low +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. wavedrom:: /../_static/diagrams/mcpwm/dual_edge_sym_active_low.json + +.. code:: c + + static void gen_action_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) + { + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_DOWN, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(genb, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpb, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_DOWN, cmpb, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + } + +Symmetric Dual Edge Complementary +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. wavedrom:: /../_static/diagrams/mcpwm/dual_edge_sym_complementary.json + +.. code:: c + + static void gen_action_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) + { + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_DOWN, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(genb, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpb, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_DOWN, cmpb, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + } -Configure ---------- +Dead Time +^^^^^^^^^ -The scope of configuration depends on the motor type, in particular how many outputs and inputs are required, and what will be the sequence of signals to drive the motor. +In power electronics, the rectifier and inverter are commonly used. This requires the use of rectifier bridge and inverter bridge. Each bridge arm has two power electronic devices, such as MOSFET, IGBT, etc. The two MOSFETs on the same arm can't conduct at the same time, otherwise there will be a short circuit. The fact is that, although the PWM wave shows it is turning off the switch, but the MOSFET still needs a small time window to make that happen. This requires an extra delay to be added to the existing PWM wave that generated by setting `Generator Actions on Events <#generator-actions-on-events>`__. -In this case we will describe a simple configuration to control a brushed DC motor that is using only some of the available MCPWM's resources. An example circuit is shown below. It includes a `H-Bridge `_ to switch polarization of a voltage applied to the motor (M) and to provide sufficient current to drive it. +The dead-time driver works like a *decorator*, which is also reflected in the function parameters of :cpp:func:`mcpwm_generator_set_dead_time`, where it takes the primary generator handle (``in_generator``), and returns a generator (``out_generator``) after applying the dead-time. Please note, if the ``out_generator`` and ``in_generator`` are the same, it means we're adding the time delay to the PWM waveform in a "in-place" fashion. In turn, if the ``out_generator`` and ``in_generator`` are different, it means we're deriving a new PWM waveform from the existing ``in_generator``. -.. figure:: ../../../_static/mcpwm-brushed-dc-control.png - :align: center - :alt: Example of Brushed DC Motor Control with MCPWM - :figclass: align-center +Dead-time specific configuration is listed in the :cpp:type:`mcpwm_dead_time_config_t` structure: - Example of Brushed DC Motor Control with MCPWM - -Configuration covers the following steps: - -1. Selection of a MCPWM unit that will be used to drive the motor. There are two units available on-board of {IDF_TARGET_NAME} and enumerated in :cpp:type:`mcpwm_unit_t`. -2. Initialization of two GPIOs as output signals within selected unit by calling :cpp:func:`mcpwm_gpio_init`. The two output signals are typically used to command the motor to rotate right or left. All available signal options are listed in :cpp:type:`mcpwm_io_signals_t`. To set more than a single pin at a time, use function :cpp:func:`mcpwm_set_pin` together with :cpp:type:`mcpwm_pin_config_t`. -3. Selection of a timer. There are three timers available within the unit. The timers are listed in :cpp:type:`mcpwm_timer_t`. -4. Setting of the timer frequency and initial duty within :cpp:type:`mcpwm_config_t` structure. -5. Setting timer resolution if necessary, by calling :cpp:func:`mcpwm_group_set_resolution` and :cpp:func:`mcpwm_timer_set_resolution` -6. Calling of :cpp:func:`mcpwm_init` with the above parameters to make the configuration effective. - - -Operate -------- - -To operate a motor connected to the MCPWM unit, e.g. turn it left or right, or vary the speed, we should apply some control signals to the unit's outputs. The outputs are organized into three pairs. Within a pair they are labeled "A" and "B" and each driven by a submodule called an "Generator". To provide a PWM signal, the Operator itself, which contains two Generator, should be clocked by one of three available Timers. To make the API simpler, each Timer is automatically associated by the API to drive an Operator of the same index, e.g. Timer 0 is associated with Operator 0. - -There are the following basic ways to control the outputs: - -* We can drive particular signal steady high or steady low with function :cpp:func:`mcpwm_set_signal_high` or :cpp:func:`mcpwm_set_signal_low`. This will make the motor to turn with a maximum speed or stop. Depending on selected output A or B the motor will rotate either right or left. -* Another option is to drive the outputs with the PWM signal by calling :cpp:func:`mcpwm_start` or :cpp:func:`mcpwm_stop`. The motor speed will be proportional to the PWM duty. -* To vary PWM's duty call :cpp:func:`mcpwm_set_duty` and provide the duty value in %. Optionally, you may call :cpp:func:`mcpwm_set_duty_in_us`, if you prefer to set the duty in microseconds. Checking of currently set value is possible by calling :cpp:func:`mcpwm_get_duty`. Phase of the PWM signal may be altered by calling :cpp:func:`mcpwm_set_duty_type`. The duty is set individually for each A and B output using :cpp:type:`mcpwm_generator_t` in specific function calls. The duty value refers either to high or low output signal duration. This is configured when calling :cpp:func:`mcpwm_init`, as discussed in section `Configure`_, and selecting one of options from :cpp:type:`mcpwm_duty_type_t`. +- :cpp:member:`mcpwm_dead_time_config_t::posedge_delay_ticks` and :cpp:member:`mcpwm_dead_time_config_t::negedge_delay_ticks` set the number of ticks to delay the PWM waveform on the rising and falling edge. Specifically, setting both of them to zero means to bypass the dead-time module. The resolution of the dead-time tick is the same to the timer that is connected with the operator by :cpp:func:`mcpwm_operator_connect_timer`. +- :cpp:member:`mcpwm_dead_time_config_t::invert_output`: Whether to invert the signal after applying the dead-time, which can be used to control the delay edge polarity. .. note:: - Call function :cpp:func:`mcpwm_set_duty_type` every time after :cpp:func:`mcpwm_set_signal_high` or :cpp:func:`mcpwm_set_signal_low` to resume with previously set duty cycle. + It is also possible to generate the required dead time by setting `Generator Actions on Events <#generator-actions-on-events>`__, especially by controlling edge placement using different comparators. However, if the more classical edge delay-based dead time with polarity control is required, then the dead-time submodule should be used. +Classical PWM Waveforms and Dead Time Configurations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Adjust ------- +This section will demonstrate the classical PWM waveforms that can be generated by the dead-time submodule. The code snippet that is used to generate the waveforms is also provided below the diagram. -There are couple of ways to adjust a signal on the outputs and changing how the motor operates. +Active High Complementary +~~~~~~~~~~~~~~~~~~~~~~~~~ -* Set specific PWM frequency by calling :cpp:func:`mcpwm_set_frequency`. This may be required to adjust to electrical or mechanical characteristics of particular motor and driver. To check what frequency is set, use function :cpp:func:`mcpwm_get_frequency`. -* Introduce a dead time between outputs A and B when they are changing the state to reverse direction of the motor rotation. This is to make up for on/off switching delay of the motor driver FETs. The dead time options are defined in :cpp:type:`mcpwm_deadtime_type_t` and enabled by calling :cpp:func:`mcpwm_deadtime_enable`. To disable this functionality call :cpp:func:`mcpwm_deadtime_disable`. -* Synchronize outputs of operator submodules, e.g. to get raising edge of PWM0A/B and PWM1A/B to start exactly at the same time, or shift them between each other by a given phase. Synchronization is triggered by ``SYNC SIGNALS`` shown on the :ref:`block diagram ` of the MCPWM above, and defined in :cpp:type:`mcpwm_sync_signal_t`. To attach the signal to a GPIO call :cpp:func:`mcpwm_gpio_init`. You can then enable synchronization with function :cpp:func:`mcpwm_sync_configure`. As input parameters provide MCPWM unit, timer to synchronize, the synchronization signal and a phase to delay the timer. +.. wavedrom:: /../_static/diagrams/mcpwm/deadtime_active_high_complementary.json -.. note:: +.. code:: c - Synchronization signals are referred to using two different enumerations. First one :cpp:type:`mcpwm_io_signals_t` is used together with function :cpp:func:`mcpwm_gpio_init` when selecting a GPIO as the signal input source. The second one :cpp:type:`mcpwm_sync_signal_t` is used when enabling or disabling synchronization with :cpp:func:`mcpwm_sync_configure` or :cpp:func:`mcpwm_sync_disable`. + static void gen_action_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) + { + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(gena, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + } - -* Vary the pattern of the A/B output signals by getting MCPWM counters to count up, down and up/down (automatically changing the count direction). Respective configuration is done when calling :cpp:func:`mcpwm_init`, as discussed in section `Configure`_, and selecting one of counter types from :cpp:type:`mcpwm_counter_type_t`. For explanation of how A/B PWM output signals are generated, see *{IDF_TARGET_NAME} Technical Reference Manual* > *Motor Control PWM (MCPWM)* [`PDF <{IDF_TARGET_TRM_EN_URL}#mcpwm>`__]. - - -Synchronize ------------ - -Each PWM timer has a synchronization input and a synchronization output. The synchronization input can be selected from other timers' synchronization outputs or GPIO signals via the GPIO matrix. Timer's synchronization signal can be generated from either the input sync signal or when the count value reaches peak/zero. Thus, the PWM timers can be chained together with their phase-locked. During synchronization, the PWM timer clock prescaler will reset its counter in order to synchronize the PWM timer clock. - -The functionality is enabled in following steps: - -1. Make sure the PWM timer and operator are already configured so that sync will inherit its config (count mode, freq and duty). -2. Enabling sync input of the timer by invoking :cpp:func:`mcpwm_sync_configure`, selecting desired signal input from :cpp:type:`mcpwm_sync_signal_t`, and setting the desired phase range from 0 to 999 which is mapped to 0%~99.9%. 0 means zero phase is applied and output is fired at the same time. And selecting desired counting direction. -3. Enabling one of sync event source from another timer or from external GPIO input. - -To sync with another timer: - -Enabling sync output of another timer by invoking :cpp:func:`mcpwm_set_timer_sync_output` and selecting desired event to generate sync output from :cpp:type:`mcpwm_timer_sync_trigger_t`. - -To sync with GPIO positive edge input (negative edge requires :cpp:func:`mcpwm_sync_invert_gpio_synchro`): - -Configuring GPIOs to act as the sync signal inputs by calling functions :cpp:func:`mcpwm_gpio_init` or :cpp:func:`mcpwm_set_pin`, which were described in section `Configure`_. - -It's normal condition that chained sync signal may have tens or even hundreds of nanoseconds of delay between each timer output due to hardware limitation. To sync two timers accurately it is required to have the third timer occupied to produce sync event that can be consumed parallel by other two timer, so that those two timer will have no delay between each other but have the same delay between the timer which provides events. Another solution is introducing an external GPIO event source so that all three timers can be synced together with no delay. - -.. only:: SOC_MCPWM_SWSYNC_CAN_PROPAGATE - - Software sync event which triggered on one timer can be propagated to other timers on {IDF_TARGET_NAME}, which can be used as a tricky way to get all three timers synced without any extra requirement. - - .. code-block:: c - - // configure timer0 as trigger source - mcpwm_set_timer_sync_output(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_SWSYNC_SOURCE_SYNCIN); - mcpwm_sync_config_t sync_conf = { - .sync_sig = MCPWM_SELECT_TIMER0_SYNC, - .timer_val = 0, - .count_direction = MCPWM_TIMER_DIRECTION_UP, + static void dead_time_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb) + { + mcpwm_dead_time_config_t dead_time_config = { + .posedge_delay_ticks = 50, + .negedge_delay_ticks = 0 }; - mcpwm_sync_configure(TARGET_MCPWM_UNIT, MCPWM_TIMER_0, &sync_conf); - mcpwm_sync_configure(TARGET_MCPWM_UNIT, MCPWM_TIMER_1, &sync_conf); - mcpwm_sync_configure(TARGET_MCPWM_UNIT, MCPWM_TIMER_2, &sync_conf); - // then send soft sync event to timer0 - mcpwm_timer_trigger_soft_sync(MCPWM_UNIT_0, MCPWM_TIMER_0); + ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(gena, gena, &dead_time_config)); + dead_time_config.posedge_delay_ticks = 0; + dead_time_config.negedge_delay_ticks = 100; + dead_time_config.flags.invert_output = true; + ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(gena, genb, &dead_time_config)); + } -If not required anymore, the capture functionality may be disabled with :cpp:func:`mcpwm_sync_disable`. +Active Low Complementary +~~~~~~~~~~~~~~~~~~~~~~~~ +.. wavedrom:: /../_static/diagrams/mcpwm/deadtime_active_low_complementary.json + +.. code:: c + + static void gen_action_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) + { + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(gena, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + } + + static void dead_time_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb) + { + mcpwm_dead_time_config_t dead_time_config = { + .posedge_delay_ticks = 50, + .negedge_delay_ticks = 0, + .flags.invert_output = true + }; + ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(gena, gena, &dead_time_config)); + dead_time_config.posedge_delay_ticks = 0; + dead_time_config.negedge_delay_ticks = 100; + dead_time_config.flags.invert_output = false; + ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(gena, genb, &dead_time_config)); + } + +Active High +~~~~~~~~~~~ + +.. wavedrom:: /../_static/diagrams/mcpwm/deadtime_active_high.json + +.. code:: c + + static void gen_action_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) + { + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(gena, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + } + + static void dead_time_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb) + { + mcpwm_dead_time_config_t dead_time_config = { + .posedge_delay_ticks = 50, + .negedge_delay_ticks = 0, + }; + ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(gena, gena, &dead_time_config)); + dead_time_config.posedge_delay_ticks = 0; + dead_time_config.negedge_delay_ticks = 100; + ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(gena, genb, &dead_time_config)); + } + +Active Low +~~~~~~~~~~ + +.. wavedrom:: /../_static/diagrams/mcpwm/deadtime_active_low.json + +.. code:: c + + static void gen_action_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) + { + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(gena, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + } + + static void dead_time_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb) + { + mcpwm_dead_time_config_t dead_time_config = { + .posedge_delay_ticks = 50, + .negedge_delay_ticks = 0, + .flags.invert_output = true + }; + ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(gena, gena, &dead_time_config)); + dead_time_config.posedge_delay_ticks = 0; + dead_time_config.negedge_delay_ticks = 100; + ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(gena, genb, &dead_time_config)); + } + +Rising Delay on PWMA, Bypass deadtime for PWMB +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. wavedrom:: /../_static/diagrams/mcpwm/deadtime_reda_bypassb.json + +.. code:: c + + static void gen_action_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) + { + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(gena, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(genb, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(genb, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpb, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + } + + static void dead_time_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb) + { + mcpwm_dead_time_config_t dead_time_config = { + .posedge_delay_ticks = 50, + .negedge_delay_ticks = 0, + }; + // apply deadtime to generator_a + ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(gena, gena, &dead_time_config)); + // bypass deadtime module for generator_b + dead_time_config.posedge_delay_ticks = 0; + ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(genb, genb, &dead_time_config)); + } + +Falling Delay on PWMB, Bypass deadtime for PWMA +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. wavedrom:: /../_static/diagrams/mcpwm/deadtime_fedb_bypassa.json + +.. code:: c + + static void gen_action_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) + { + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(gena, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(genb, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(genb, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpb, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + } + + static void dead_time_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb) + { + mcpwm_dead_time_config_t dead_time_config = { + .posedge_delay_ticks = 0, + .negedge_delay_ticks = 0, + }; + // generator_a bypass the deadtime module (no delay) + ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(gena, gena, &dead_time_config)); + // apply dead time to generator_b + dead_time_config.negedge_delay_ticks = 50; + ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(genb, genb, &dead_time_config)); + + } + +Rising and Falling Delay on PWMB, Bypass deadtime for PWMA +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. wavedrom:: /../_static/diagrams/mcpwm/deadtime_redb_fedb_bypassa.json + +.. code:: c + + static void gen_action_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb, mcpwm_cmpr_handle_t cmpa, mcpwm_cmpr_handle_t cmpb) + { + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(gena, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(gena, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpa, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_timer_event(genb, + MCPWM_GEN_TIMER_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, MCPWM_TIMER_EVENT_EMPTY, MCPWM_GEN_ACTION_HIGH), + MCPWM_GEN_TIMER_EVENT_ACTION_END())); + ESP_ERROR_CHECK(mcpwm_generator_set_actions_on_compare_event(genb, + MCPWM_GEN_COMPARE_EVENT_ACTION(MCPWM_TIMER_DIRECTION_UP, cmpb, MCPWM_GEN_ACTION_LOW), + MCPWM_GEN_COMPARE_EVENT_ACTION_END())); + } + + static void dead_time_config(mcpwm_gen_handle_t gena, mcpwm_gen_handle_t genb) + { + mcpwm_dead_time_config_t dead_time_config = { + .posedge_delay_ticks = 0, + .negedge_delay_ticks = 0, + }; + // generator_a bypass the deadtime module (no delay) + ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(gena, gena, &dead_time_config)); + // apply dead time on both edge for generator_b + dead_time_config.negedge_delay_ticks = 50; + dead_time_config.posedge_delay_ticks = 50; + ESP_ERROR_CHECK(mcpwm_generator_set_dead_time(genb, genb, &dead_time_config)); + } + +Carrier Modulation +^^^^^^^^^^^^^^^^^^ + +The MCPWM operator has a carrier submodule that can be used if galvanic isolation from the motor driver is required (e.g. isolated digital power application) by passing the PWM output signals through transformers. Any of PWM output signals may be at 100% duty and not changing whenever motor is required to run steady at the full load. Coupling of non alternating signals with a transformer is problematic, so the signals are modulated by the carrier submodule to create an AC waveform, to make the coupling possible. + +To configure the carrier submodule, you can call :cpp:func:`mcpwm_operator_apply_carrier`, and provide configuration structure :cpp:type:`mcpwm_carrier_config_t`: + +- :cpp:member:`mcpwm_carrier_config_t::frequency_hz`: The carrier frequency in Hz. +- :cpp:member:`mcpwm_carrier_config_t::duty_cycle`: The duty cycle of the carrier. Note that, the supported choices of duty cycle are discrete, the driver will search the nearest one based the user configuration. +- :cpp:member:`mcpwm_carrier_config_t::first_pulse_duration_us`: The duration of the first pulse in microseconds. The resolution of the first pulse duration is determined by the carrier frequency you set in the :cpp:member:`mcpwm_carrier_config_t::frequency_hz`. The first pulse duration can't be zero, and it has to be at least one period of the carrier. A longer pulse width can help conduct the inductance quicker. +- :cpp:member:`mcpwm_carrier_config_t::invert_before_modulate` and :cpp:member:`mcpwm_carrier_config_t::invert_after_modulate`: Set whether to invert the carrier output before and after modulation. + +Specifically, the carrier submodule can be disabled by calling :cpp:func:`mcpwm_operator_apply_carrier` with a ``NULL`` configuration. + +Faults and Brake Actions +^^^^^^^^^^^^^^^^^^^^^^^^ + +The MCPWM operator is able to sense external signals with information about failure of the motor, the power driver or any other device connected. These failure signals are encapsulated into `MCPWM fault objects <#mcpwm-faults>`__. + +The user should determine possible failure modes of the motor and what action should be performed on detection of particular fault, e.g. drive all outputs low for a brushed motor, or lock current state for a stepper motor, etc. As result of this action the motor should be put into a safe state to reduce likelihood of a damage caused by the fault. + +Set Operator Brake Mode on Fault +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The way that MCPWM operator reacts to the fault is called **Brake**. The MCPWM operator can be configured to perform different brake modes for each fault object by calling :cpp:func:`mcpwm_operator_set_brake_on_fault`. Brake specific configuration is passed as a structure :cpp:type:`mcpwm_brake_config_t`: + +- :cpp:member:`mcpwm_brake_config_t::fault` set which fault that the operator should react to. +- :cpp:member:`mcpwm_brake_config_t::brake_mode` set the brake mode that should be used for the fault. The supported brake modes are listed in the :cpp:type:`mcpwm_operator_brake_mode_t`. For :cpp:enumerator:`MCPWM_OPER_BRAKE_MODE_CBC` mode, the operator will recover itself automatically as long as the fault disappears. You can specify the recovery time in :cpp:member:`mcpwm_brake_config_t::cbc_recover_on_tez` and :cpp:member:`mcpwm_brake_config_t::cbc_recover_on_tep`. For :cpp:enumerator:`MCPWM_OPER_BRAKE_MODE_OST` mode, the operator can't recover even though the fault disappears. User has to call :cpp:func:`mcpwm_operator_recover_from_fault` to manually recover it. + +Set Generator Action on Brake Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +One generator can set multiple actions on different brake events, by calling :cpp:func:`mcpwm_generator_set_actions_on_brake_event` with variable number of action configurations. The action configuration is defined in :cpp:type:`mcpwm_gen_brake_event_action_t`: + +- :cpp:member:`mcpwm_gen_brake_event_action_t::direction` specific the timer direction. The supported directions are listed in :cpp:type:`mcpwm_timer_direction_t`. +- :cpp:member:`mcpwm_gen_brake_event_action_t::brake_mode` specifies the brake mode. The supported brake modes are listed in the :cpp:type:`mcpwm_operator_brake_mode_t`. +- :cpp:member:`mcpwm_gen_brake_event_action_t::action` specifies the generator action to be taken. The supported actions are listed in :cpp:type:`mcpwm_generator_action_t`. + +There's a helper macro :c:macro:`MCPWM_GEN_BRAKE_EVENT_ACTION` to simplify the construction of a brake event action entry. + +Please note, the argument list of :cpp:func:`mcpwm_generator_set_actions_on_brake_event` **must** be terminated by :c:macro:`MCPWM_GEN_BRAKE_EVENT_ACTION_END`. + +Register Fault Event Callbacks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The MCPWM fault detector can inform the user when it detects a valid fault or a fault signal disappears. If you have some function that should be called when such event happens, you should hook your function to the interrupt service routine by calling :cpp:func:`mcpwm_fault_register_event_callbacks`. The callback function prototype is declared in :cpp:type:`mcpwm_fault_event_cb_t`. All supported event callbacks are listed in the :cpp:type:`mcpwm_fault_event_callbacks_t`: + +- :cpp:member:`mcpwm_fault_event_callbacks_t::on_fault_enter` sets callback function that will be called when a fault is detected. +- :cpp:member:`mcpwm_fault_event_callbacks_t::on_fault_exit` sets callback function that will be called when a fault is cleared. + +The callback function is called within the ISR context, so is should **not** attempt to block (e.g., make sure that only FreeRTOS APIs with ``ISR`` suffix is called within the function). + +The parameter ``user_data`` of :cpp:func:`mcpwm_fault_register_event_callbacks` function is used to save user's own context, it will be passed to the callback function directly. + +This function will lazy install interrupt service for the MCPWM fault, whereas the service can only be removed in :cpp:type:`mcpwm_del_fault`. + +Register Brake Event Callbacks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The MCPWM operator can inform the user when it going to take a brake action. If you have some function that should be called when this event happens, you should hook your function to the interrupt service routine by calling :cpp:func:`mcpwm_operator_register_event_callbacks`. The callback function prototype is declared in :cpp:type:`mcpwm_brake_event_cb_t`. All supported event callbacks are listed in the :cpp:type:`mcpwm_operator_event_callbacks_t`: + +- :cpp:member:`mcpwm_operator_event_callbacks_t::on_brake_cbc` sets callback function that will be called when the operator is going to take a *CBC* action. +- :cpp:member:`mcpwm_operator_event_callbacks_t::on_brake_ost` sets callback function that will be called when the operator is going to take an *OST* action. + +The callback function is called within the ISR context, so is should **not** attempt to block (e.g., make sure that only FreeRTOS APIs with ``ISR`` suffix is called within the function). + +The parameter ``user_data`` of :cpp:func:`mcpwm_operator_register_event_callbacks` function is used to save user's own context, it will be passed to the callback function directly. + +This function will lazy install interrupt service for the MCPWM operator, whereas the service can only be removed in :cpp:type:`mcpwm_del_operator`. + +Generator Force Actions +^^^^^^^^^^^^^^^^^^^^^^^ + +Software can override generator output level at runtime, by calling :cpp:func:`mcpwm_generator_set_force_level`. The software force level always has a higher priority than other event actions set in e.g. :cpp:func:`mcpwm_generator_set_actions_on_timer_event`. + +- Set the ``level`` to -1 means to disable the force action, and the generator's output level will be controlled by the event actions again. +- Set the ``hold_on`` to true, the force output level will keep alive, until it's removed by assigning ``level`` to -1. +- Set the ``hole_on`` to false, the force output level will only be active for a short time, any upcoming event can override it. + +Synchronization +^^^^^^^^^^^^^^^ + +When a sync signal is taken by the MCPWM timer, the timer will be forced into a predefined **phase**, where the phase is determined by count value and count direction. You can set the sync phase by calling :cpp:func:`mcpwm_timer_set_phase_on_sync`. The sync phase configuration is defined in :cpp:type:`mcpwm_timer_sync_phase_config_t` structure: + +- :cpp:member:`mcpwm_timer_sync_phase_config_t::sync_src` sets the sync signal source. See `MCPWM Sync Sources <#mcpwm-sync-sources>`__ for how to create a sync source object. Specifically, if this is set to ``NULL``, the driver will disable the sync feature for the MCPWM timer. +- :cpp:member:`mcpwm_timer_sync_phase_config_t::count_value` sets the count value to load when the sync signal is taken. +- :cpp:member:`mcpwm_timer_sync_phase_config_t::direction` sets the count direction when the sync signal is taken. + +Likewise, the MCPWM capture timer `MCPWM Capture Timer <#mcpwm-capture-timer-and-channels>`__ can be synced as well. You can set the sync phase for the capture timer by calling :cpp:func:`mcpwm_capture_timer_set_phase_on_sync`. The sync phase configuration is defined in :cpp:type:`mcpwm_capture_timer_sync_phase_config_t` structure: + +- :cpp:member:`mcpwm_capture_timer_sync_phase_config_t::sync_src` sets the sync signal source. See `MCPWM Sync Sources <#mcpwm-sync-sources>`__ for how to create a sync source object. Specifically, if this is set to ``NULL``, the driver will disable the sync feature for the MCPWM capture timer. +- :cpp:member:`mcpwm_capture_timer_sync_phase_config_t::count_value` sets the count value to load when the sync signal is taken. +- :cpp:member:`mcpwm_capture_timer_sync_phase_config_t::direction` sets the count direction when the sync signal is taken. Note that, different from MCPWM Timer, the capture timer can only support one count direction: :cpp:enumerator:`MCPWM_TIMER_DIRECTION_UP`. + +Sync Timers by GPIO +~~~~~~~~~~~~~~~~~~~ + +.. blockdiag:: + :caption: GPIO Sync All MCPWM Timers + :align: center + + blockdiag { + GPIO -> Timer0, Timer1, Timer2; + } + +.. code-block:: c + + static void example_setup_sync_strategy(mcpwm_timer_handle_t timers[]) + { + mcpwm_sync_handle_t gpio_sync_source = NULL; + mcpwm_gpio_sync_src_config_t gpio_sync_config = { + .group_id = 0, // GPIO fault should be in the same group of the above timers + .gpio_num = EXAMPLE_SYNC_GPIO, + .flags.pull_down = true, + .flags.active_neg = false, // by default, a posedge pulse can trigger a sync event + }; + ESP_ERROR_CHECK(mcpwm_new_gpio_sync_src(&gpio_sync_config, &gpio_sync_source)); + + mcpwm_timer_sync_phase_config_t sync_phase_config = { + .count_value = 0, // sync phase: target count value + .direction = MCPWM_TIMER_DIRECTION_UP, // sync phase: count direction + .sync_src = gpio_sync_source, // sync source + }; + for (int i = 0; i < 3; i++) { + ESP_ERROR_CHECK(mcpwm_timer_set_phase_on_sync(timers[i], &sync_phase_config)); + } + } Capture -------- +^^^^^^^ -One of requirements of BLDC (Brushless DC, see figure below) motor control is sensing of the rotor position. To facilitate this task each MCPWM unit provides three sensing inputs together with dedicated hardware. The hardware is able to detect the input signal's edge and measure time between signals. As result the control software is simpler and the CPU power may be used for other tasks. +The basic functionality of MCPWM capture is to record the time when any pulse edge of the capture signal turns active. Then you can get the pulse width and convert it into other physical quantity like distance or speed in the capture callback function. For example, in the BLDC (Brushless DC, see figure below) scenario, we can use the capture submodule to sense the rotor position from Hall sensor. .. figure:: ../../../_static/mcpwm-bldc-control.png :align: center :alt: Example of Brushless DC Motor Control with MCPWM :figclass: align-center - Example of Brushless DC Motor Control with MCPWM + MCPWM BLDC with Hall Sensor -The capture functionality may be used for other types of motors or tasks. The functionality is enabled in two steps: +The capture timer is usually connected with several capture channels, please refer to ``__ for resource allocation. -1. Configuration of GPIOs to act as the capture signal inputs by calling functions :cpp:func:`mcpwm_gpio_init` or :cpp:func:`mcpwm_set_pin`, that were described in section `Configure`_. -2. Enabling of the functionality itself by invoking :cpp:func:`mcpwm_capture_enable_channel`, selecting desired signal input from :cpp:type:`mcpwm_capture_channel_id_t`, setting the signal edge, signal count prescaler and user callback within :cpp:type:`mcpwm_capture_config_t` +Register Event Callbacks +~~~~~~~~~~~~~~~~~~~~~~~~ -Within the second step above a 32-bit capture timer is enabled. The timer runs continuously driven by the APB clock. The clock frequency is typically 80 MHz. On each capture event the capture timer’s value is stored in time-stamp register that may be then checked by calling :cpp:func:`mcpwm_capture_signal_get_value`. The edge of the last signal may be checked with :cpp:func:`mcpwm_capture_signal_get_edge`. Those data are also provided inside callback function as event data :cpp:type:`cap_event_data_t` +The MCPWM capture channel can inform the user when there's a valid edge detected on the signal. You have to register a callback function to get the timer count value of the capture moment, by calling :cpp:func:`mcpwm_capture_channel_register_event_callbacks`. The callback function prototype is declared in :cpp:type:`mcpwm_capture_event_cb_t`. All supported capture callbacks are listed in the :cpp:type:`mcpwm_capture_event_callbacks_t`: -If not required anymore, the capture functionality may be disabled with :cpp:func:`mcpwm_capture_disable_channel`. +- :cpp:member:`mcpwm_capture_event_callbacks_t::on_cap` sets callback function for the capture channel when a valid edge is detected. -Capture prescale is different from other modules as it is applied to the input signal, not the timer source. Prescaler has maintained its own level state with the initial value set to low and is detecting the positive edge of the input signal to change its internal state. That means if two pairs of positive and negative edges are passed to input, the prescaler's internal state will change twice. ISR will report on this internal state change, not the input signal. For example, setting prescale to 2 will generate ISR callback on each positive edge of input if both edge is selected via :cpp:type:`mcpwm_capture_config_t`. Or each 2 positive edges of input if only one edge is selected though :cpp:type:`mcpwm_capture_config_t`. +The callback function will provide event specific data of type :cpp:type:`mcpwm_capture_event_data_t`, so that you can get the the edge of the capture signal in :cpp:member:`mcpwm_capture_event_data_t::cap_edge` and the count value of that moment in :cpp:member:`mcpwm_capture_event_data_t::cap_value`. +The callback function is called within the ISR context, so is should **not** attempt to block (e.g., make sure that only FreeRTOS APIs with ``ISR`` suffix is called within the function). -Fault Handler -------------- +The parameter ``user_data`` of :cpp:func:`mcpwm_capture_channel_register_event_callbacks` function is used to save user's own context, it will be passed to the callback function directly. -Each unit of the MCPWM is able to sense external signals with information about failure of the motor, the motor driver or any other device connected to the MCPWM. There are three fault inputs per unit that may be routed to user selectable GPIOs. The MCPWM may be configured to perform one of four predefined actions on A/B outputs when a fault signal is received: +This function will lazy install interrupt service for the MCPWM capture channel, whereas the service can only be removed in :cpp:type:`mcpwm_del_capture_channel`. -* lock current state of the output -* set the output low -* set the output high -* toggle the output +Enable and Disable Capture Timer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The user should determine possible failure modes of the motor and what action should be performed on detection of particular fault, e.g. drive all outputs low for a brushed motor, or lock current state for a stepper motor, etc. As result of this action the motor should be put into a safe state to reduce likelihood of a damage caused by the fault. +Before doing IO control to the capture timer, user needs to enable the timer first, by calling :cpp:func:`mcpwm_capture_timer_enable`. Internally, this function will: -The fault handler functionality is enabled in two steps: +* switch the capture timer state from **init** to **enable**. +* acquire a proper power management lock if a specific clock source (e.g. APB clock) is selected. See also `Power management <#power-management>`__ for more information. -1. Configuration of GPIOs to act as fault signal inputs. This is done in analogous way as described for capture signals in section above. It includes setting the signal level to trigger the fault as defined in :cpp:type:`mcpwm_fault_input_level_t`. -2. Initialization of the fault handler by calling either :cpp:func:`mcpwm_fault_set_oneshot_mode` or :cpp:func:`mcpwm_fault_set_cyc_mode`. These functions set the mode that MCPWM should operate once fault signal becomes inactive. There are two modes possible: +On the contrary, calling :cpp:func:`mcpwm_capture_timer_disable` will put the timer driver back to **init** state, and release the power management lock. - * State of MCPWM unit will be locked until reset - :cpp:func:`mcpwm_fault_set_oneshot_mode`. - * The MCPWM will resume operation once fault signal becoming inactive - :cpp:func:`mcpwm_fault_set_cyc_mode`. +Start and Stop Capture Timer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - The function call parameters include selection of one of three fault inputs defined in :cpp:type:`mcpwm_fault_signal_t` and specific action on outputs A and B defined in :cpp:type:`mcpwm_action_on_pwmxa_t` and :cpp:type:`mcpwm_action_on_pwmxb_t`. +The basic IO operation of a capture timer is to start and stop. Calling :cpp:func:`mcpwm_capture_timer_start` can start the timer and calling :cpp:func:`mcpwm_capture_timer_stop` can stop the timer immediately. -Particular fault signal may be disabled at the runtime by calling :cpp:func:`mcpwm_fault_deinit`. +Trigger a Software Capture Event +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Sometime, the software also wants to trigger a "fake" capture event. The :cpp:func:`mcpwm_capture_channel_trigger_soft_catch` is provided for that purpose. Please note that, even though it's a "fake" capture event, it can still cause an interrupt, thus your capture event callback function will get invoked as well. -Carrier -------- +Power Management +^^^^^^^^^^^^^^^^ -The MCPWM has a carrier submodule used if galvanic isolation from the motor driver is required by passing the A/B output signals through transformers. Any of A and B output signals may be at 100% duty and not changing whenever motor is required to run steady at the full load. Coupling of non alternating signals with a transformer is problematic, so the signals are modulated by the carrier submodule to create an AC waveform, to make the coupling possible. +When power management is enabled (i.e. :ref:`CONFIG_PM_ENABLE` is on), the system will adjust the PLL, APB frequency before going into light sleep, thus potentially changing the period of a MCPWM timers' counting step and leading to inaccurate time keeping. -To use the carrier submodule, it should be first initialized by calling :cpp:func:`mcpwm_carrier_init`. The carrier parameters are defined in :cpp:type:`mcpwm_carrier_config_t` structure invoked within the function call. Then the carrier functionality may be enabled by calling :cpp:func:`mcpwm_carrier_enable`. +However, the driver can prevent the system from changing APB frequency by acquiring a power management lock of type :cpp:enumerator:`ESP_PM_APB_FREQ_MAX`. Whenever the driver creates a MCPWM timer instance that has selected :cpp:enumerator:`MCPWM_TIMER_CLK_SRC_PLL160M` as its clock source, the driver will guarantee that the power management lock is acquired when enable the timer by :cpp:func:`mcpwm_timer_enable`. Likewise, the driver releases the lock when :cpp:func:`mcpwm_timer_disable` is called for that timer. -The carrier parameters may be then altered at a runtime by calling dedicated functions to change individual fields of the :cpp:type:`mcpwm_carrier_config_t` structure, like :cpp:func:`mcpwm_carrier_set_period`, :cpp:func:`mcpwm_carrier_set_duty_cycle`, :cpp:func:`mcpwm_carrier_output_invert`, etc. +Likewise, Whenever the driver creates a MCPWM capture timer instance that has selected :cpp:enumerator:`MCPWM_CAPTURE_CLK_SRC_APB` as its clock source, the driver will guarantee that the power management lock is acquired when enable the timer by :cpp:func:`mcpwm_capture_timer_enable`. And will release the lock in :cpp:func:`mcpwm_capture_timer_disable`. -This includes enabling and setting duration of the first pulse of the career with :cpp:func:`mcpwm_carrier_oneshot_mode_enable`. For more details, see *{IDF_TARGET_NAME} Technical Reference Manual* > *Motor Control PWM (MCPWM)* > *PWM Carrier Submodule* [`PDF <{IDF_TARGET_TRM_EN_URL}#mcpwm>`__]. +IRAM Safe +^^^^^^^^^ -To disable carrier functionality call :cpp:func:`mcpwm_carrier_disable`. +By default, the MCPWM interrupt will be deferred when the Cache is disabled for reasons like writing/erasing Flash. Thus the event callback functions will not get executed in time, which is not expected in a real-time application. +There's a Kconfig option :ref:`CONFIG_MCPWM_ISR_IRAM_SAFE` that will: -Interrupts ----------- +1. Enable the interrupt being serviced even when cache is disabled +2. Place all functions that used by the ISR into IRAM [2]_ +3. Place driver object into DRAM (in case it's mapped to PSRAM by accident) -Registering of the MCPWM interrupt handler is possible by calling :cpp:func:`mcpwm_isr_register`. Note if :cpp:func:`mcpwm_capture_enable_channel` is used then a default ISR routine will be installed hence please do not call this function to register any more. +This will allow the interrupt to run while the cache is disabled but will come at the cost of increased IRAM consumption. +Thread Safety +^^^^^^^^^^^^^ -Resolution ----------- +The factory functions like :cpp:func:`mcpwm_new_timer` are guaranteed to be thread safe by the driver, which means, you can call it from different RTOS tasks without protection by extra locks. -The default resolution for MCPWM group and MCPWM timer are configured to **10MHz** and **1MHz** in :cpp:func:`mcpwm_init`, which might be not enough for some applications. -The driver also provides two APIs that can be used to override the default resolution: :cpp:func:`mcpwm_group_set_resolution` and :cpp:func:`mcpwm_timer_set_resolution`. +No functions are allowed to run within ISR environment. -Note that, these two APIs won't update the frequency and duty automatically, to achieve that, one has to call :cpp:func:`mcpwm_set_frequency` and :cpp:func:`mcpwm_set_duty` accordingly. +Functions that are not related to `Resource Allocation <#resource-allocation-and-initialization>`__, are not thread safe. Thus, you should avoid calling them in different tasks without mutex protection. -To get PWM pulse that is below 15Hz, please set the resolution to a lower value. For high frequency PWM with limited step range, please set them with higher value. +Kconfig Options +^^^^^^^^^^^^^^^ +- :ref:`CONFIG_MCPWM_ISR_IRAM_SAFE` controls whether the default ISR handler can work when cache is disabled, see `IRAM Safe <#iram-safe>`__ for more information. +- :ref:`CONFIG_MCPWM_ENABLE_DEBUG_LOG` is used to enabled the debug log output. Enable this option will increase the firmware binary size. -Application Example -------------------- +Application Examples +-------------------- -MCPWM example are located under: :example:`peripherals/mcpwm`: - -* Control of BLDC (brushless DC) motor with hall sensor feedback - :example:`peripherals/mcpwm/mcpwm_bldc_hall_control` -* Brushed DC motor control - :example:`peripherals/mcpwm/mcpwm_bdc_speed_control` -* Servo motor control - :example:`peripherals/mcpwm/mcpwm_servo_control` -* HC-SR04 sensor with capture - :example:`peripherals/mcpwm/mcpwm_capture_hc_sr04` +* Brushed DC motor speed control by PID algorithm: :example:`peripherals/mcpwm/mcpwm_bdc_speed_control` +* BLDC motor control with hall sensor feedback: :example:`peripherals/mcpwm/mcpwm_bldc_hall_control` +* Ultrasonic sensor (HC-SR04) distance measurement: :example:`peripherals/mcpwm/mcpwm_capture_hc_sr04` +* Servo motor angle control: :example:`peripherals/mcpwm/mcpwm_servo_control` +* MCPWM synchronization between timers: :example:`peripherals/mcpwm/mcpwm_sync` API Reference ------------- -.. include-build-file:: inc/mcpwm_types.inc -.. include-build-file:: inc/mcpwm.inc +.. include-build-file:: inc/mcpwm_timer.inc +.. include-build-file:: inc/mcpwm_oper.inc +.. include-build-file:: inc/mcpwm_cmpr.inc +.. include-build-file:: inc/mcpwm_gen.inc +.. include-build-file:: inc/mcpwm_fault.inc +.. include-build-file:: inc/mcpwm_sync.inc +.. include-build-file:: inc/mcpwm_cap.inc +.. include-build-file:: inc/components/driver/include/driver/mcpwm_types.inc +.. include-build-file:: inc/components/hal/include/hal/mcpwm_types.inc +.. [1] + Different ESP chip series might have different number of MCPWM resources (e.g. groups, timers, comparators, operators, generators and so on). Please refer to the [`TRM <{IDF_TARGET_TRM_EN_URL}#mcpwm>`__] for details. The driver won't forbid you from applying for more MCPWM resources, but it will return error when there's no hardware resources available. Please always check the return value when doing `Resource Allocation <#resource-allocation-and-initialization>`__. + +.. [2] + Callback function and the sub-functions invoked by itself should also be placed in IRAM, users need to take care of this by themselves. diff --git a/docs/en/migration-guides/release-5.x/peripherals.rst b/docs/en/migration-guides/release-5.x/peripherals.rst index fd980f58f3..c2584d4466 100644 --- a/docs/en/migration-guides/release-5.x/peripherals.rst +++ b/docs/en/migration-guides/release-5.x/peripherals.rst @@ -213,6 +213,7 @@ LEDC Breaking Changes in Usage ~~~~~~~~~~~~~~~~~~~~~~~~~ + - Channel installation has been separated for TX and RX channels into :cpp:func:`rmt_new_tx_channel` and :cpp:func:`rmt_new_rx_channel`. - ``rmt_set_clk_div`` and ``rmt_get_clk_div`` are removed. Channel clock configuration can only be done during channel installation. - ``rmt_set_rx_idle_thresh`` and ``rmt_get_rx_idle_thresh`` are removed. In the new driver, the RX channel IDLE threshold is redesigned into a new concept :cpp:member:`rmt_receive_config_t::signal_range_max_ns`. @@ -259,11 +260,63 @@ LCD MCPWM ----- - - ``mcpwm_capture_enable`` is removed. To enable capture channel, please use :cpp:func:`mcpwm_capture_enable_channel`. - - ``mcpwm_capture_disable`` is remove. To disable capture channel, please use :cpp:func:`mcpwm_capture_capture_disable_channel`. - - ``mcpwm_sync_enable`` is removed. To configure synchronization, please use :cpp:func:`mcpwm_sync_configure`. - - ``mcpwm_isr_register`` is removed. You can register event callbacks, for capture channels. e.g. :cpp:member:`mcpwm_capture_config_t::capture_cb`. - - ``mcpwm_carrier_oneshot_mode_disable`` is removed. Disable the first pulse (a.k.a the one-shot pulse) in the carrier is not supported by hardware. + MCPWM driver was redesigned (see :doc:`MCPWM <../../api-reference/peripherals/mcpwm>`), meanwhile, the legacy driver is deprecated. The new driver's aim is to make each MCPWM submodule independent to each other, and give the freedom of resource connection back to users. Although it's recommended to use the new driver APIs, the legacy driver is still available in the previous include path ``driver/mcpwm.h``. However, by default, using legacy driver will bring compile warnings like ``legacy MCPWM driver is deprecated, please migrate to the new driver (include driver/mcpwm_prelude.h)``. This warning can be suppressed by the Kconfig option :ref:`CONFIG_MCPWM_SUPPRESS_DEPRECATE_WARN`. + + The major breaking changes in concept and usage are listed as follows: + + Breaking Changes in Concepts + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + The new MCPWM driver is object-oriented, where most of the MCPWM submodule has a driver object associated with it. The driver object is created by factory function like :cpp:func:`mcpwm_new_timer`. IO control function always needs an object handle, in the first place. + + The legacy driver has an inappropriate assumption, that is the MCPWM operator should be connected to different MCPWM timer. In fact, the hardware doesn't have such limitation. In the new driver, a MCPWM timer can be connected to multiple operators, so that the operators can achieve the best synchronization performance. + + The legacy driver preset the way to generate a PWM waveform into a so called ``mcpwm_duty_type_t``, however, the duty cycle modes listed there are far from sufficient. Likewise, legacy driver has several preset ``mcpwm_deadtime_type_t``, which also doesn't cover all the use cases. What's more, user usually gets confused by the name of the duty cycle mode and dead-time mode. In the new driver, there're no such limitation, but user has to construct the generator behavior from scratch. + + In the legacy driver, the ways to synchronize the MCPWM timer by GPIO, software and other timer module are not unified. It increased learning costs for users. In the new driver, the synchronization APIs are unified. + + The legacy driver has mixed the concepts of "Fault detector" and "Fault handler". Which make the APIs very confusing to users. In the new driver, the fault object just represents a failure source, and we introduced a new concept -- **brake** to express the concept of "Fault handler". What's more, the new driver supports software fault. + + The legacy drive only provides callback functions for the capture submodule. The new driver provides more useful callbacks for various MCPWM submodules, like timer stop, compare match, fault enter, brake, etc. + + - ``mcpwm_io_signals_t`` and ``mcpwm_pin_config_t`` are not used. GPIO configuration has been moved into submodule's configuration structure. + - ``mcpwm_timer_t``, ``mcpwm_generator_t`` are not used. Timer and generator are represented by :cpp:type:`mcpwm_timer_handle_t` and :cpp:type:`mcpwm_gen_handle_t`. + - ``mcpwm_fault_signal_t`` and ``mcpwm_sync_signal_t`` are not used. Fault and sync source are represented by :cpp:type:`mcpwm_fault_handle_t` and :cpp:type:`mcpwm_sync_handle_t`. + - ``mcpwm_capture_signal_t`` is not used. A capture channel is represented by :cpp:type:`mcpwm_cap_channel_handle_t`. + + Breaking Changes in Usage + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + - ``mcpwm_gpio_init`` and ``mcpwm_set_pin``: GPIO configurations are moved to submodule's own configuration. e.g. set the PWM GPIO in :cpp:member:`mcpwm_generator_config_t::gen_gpio_num`. + - ``mcpwm_init``: To get an expected PWM waveform, you need to allocated at least one MCPWM timer and MCPWM operator, then connect them by calling :cpp:func:`mcpwm_operator_connect_timer`. After that, you should set the generator's actions on various events by calling e.g. :cpp:func:`mcpwm_generator_set_actions_on_timer_event`, :cpp:func:`mcpwm_generator_set_actions_on_compare_event`. + - ``mcpwm_group_set_resolution``: in the new driver, the group resolution is fixed to the maximum, usually it's 80MHz. + - ``mcpwm_timer_set_resolution``: MCPWM Timer resolution is set in :cpp:member:`mcpwm_timer_config_t::resolution_hz`. + - ``mcpwm_set_frequency``: PWM frequency is determined by :cpp:member:`mcpwm_timer_config_t::resolution_hz`, :cpp:member:`mcpwm_timer_config_t::count_mode` and :cpp:member:`mcpwm_timer_config_t::period_ticks`. + - ``mcpwm_set_duty``: To set the PWM duty cycle, you should call :cpp:func:`mcpwm_comparator_set_compare_value` to change comparator's threshold. + - ``mcpwm_set_duty_type``: There won't be any preset duty types, the duty type is configured by setting different generator actions. e.g. :cpp:func:`mcpwm_generator_set_actions_on_timer_event`. + - ``mcpwm_set_signal_high`` and ``mcpwm_set_signal_low`` are replaced by :cpp:func:`mcpwm_generator_set_force_level`. In the new driver, it's implemented by setting force action for the generator, instead of changing the duty cycle to 0% or 100% at the background. + - ``mcpwm_start`` and ``mcpwm_stop`` are replaced by :cpp:func:`mcpwm_timer_start_stop`. You have more modes to start and stop the MCPWM timer, see :cpp:type:`mcpwm_timer_start_stop_cmd_t`. + - ``mcpwm_carrier_init``: It's replaced by :cpp:func:`mcpwm_operator_apply_carrier`. + - ``mcpwm_carrier_enable`` and ``mcpwm_carrier_disable``: Enabling and disabling carrier submodule is done automatically by checking whether the carrier configuration structure :cpp:type:`mcpwm_carrier_config_t` is NULL. + - ``mcpwm_carrier_set_period`` is replaced by :cpp:member:`mcpwm_carrier_config_t::frequency_hz`. + - ``mcpwm_carrier_set_duty_cycle`` is replaced by :cpp:member:`mcpwm_carrier_config_t::duty_cycle`. + - ``mcpwm_carrier_oneshot_mode_enable`` is replaced by :cpp:member:`mcpwm_carrier_config_t::first_pulse_duration_us`. + - ``mcpwm_carrier_oneshot_mode_disable`` is removed. Disabling the first pulse (a.k.a the one-shot pulse) in the carrier is never supported by the hardware. + - ``mcpwm_carrier_output_invert`` is replaced by :cpp:member:`mcpwm_carrier_config_t::invert_before_modulate` and :cpp:member:`mcpwm_carrier_config_t::invert_after_modulate`. + - ``mcpwm_deadtime_enable`` and ``mcpwm_deadtime_disable`` are replaced by :cpp:func:`mcpwm_generator_set_dead_time`. + - ``mcpwm_fault_init`` is replaced by :cpp:func:`mcpwm_new_gpio_fault`. + - ``mcpwm_fault_set_oneshot_mode``, ``mcpwm_fault_set_cyc_mode`` are replaced by :cpp:func:`mcpwm_operator_set_brake_on_fault` and :cpp:func:`mcpwm_generator_set_actions_on_brake_event`. + - ``mcpwm_capture_enable`` is removed. It's duplicated to :cpp:func:`mcpwm_capture_enable_channel`. + - ``mcpwm_capture_disable`` is removed. It's duplicated to :cpp:func:`mcpwm_capture_capture_disable_channel`. + - ``mcpwm_capture_enable_channel`` and ``mcpwm_capture_disable_channel`` are replaced by :cpp:func:`mcpwm_new_capture_channel` and :cpp:func:`mcpwm_del_capture_channel`. + - ``mcpwm_capture_signal_get_value`` and ``mcpwm_capture_signal_get_edge``: Capture timer count value and capture edge are provided in the capture event callback, via :cpp:type:`mcpwm_capture_event_data_t`. Capture data are only valuable when capture event happens. Providing single API to fetch capture data is meaningless. + - ``mcpwm_sync_enable`` is removed. It's duplicated to :cpp:func:`mcpwm_sync_configure`. + - ``mcpwm_sync_configure`` is replaced by :cpp:func:`mcpwm_timer_set_phase_on_sync`. + - ``mcpwm_sync_disable`` is equivalent to setting :cpp:member:`mcpwm_timer_sync_phase_config_t::sync_src` to ``NULL``. + - ``mcpwm_set_timer_sync_output`` is replaced by :cpp:func:`mcpwm_new_timer_sync_src`. + - ``mcpwm_timer_trigger_soft_sync`` is replaced by :cpp:func:`mcpwm_soft_sync_activate`. + - ``mcpwm_sync_invert_gpio_synchro`` is equivalent to setting :cpp:member:`mcpwm_gpio_sync_src_config_t::active_neg`. + - ``mcpwm_isr_register`` is removed. You can register various event callbacks instead. For example, to register capture event callback, you can use :cpp:func:`mcpwm_capture_channel_register_event_callbacks`. .. only:: SOC_DEDICATED_GPIO_SUPPORTED diff --git a/docs/zh_CN/api-reference/peripherals/mcpwm.rst b/docs/zh_CN/api-reference/peripherals/mcpwm.rst index a918a58c90..368f3c7f86 100644 --- a/docs/zh_CN/api-reference/peripherals/mcpwm.rst +++ b/docs/zh_CN/api-reference/peripherals/mcpwm.rst @@ -1 +1 @@ -.. include:: ../../../en/api-reference/peripherals/mcpwm.rst \ No newline at end of file +.. include:: ../../../en/api-reference/peripherals/mcpwm.rst

fEq%dE1vS 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 kI3hP{6axbT(jv9L@_(nMXzopr01~;oasU7T From e4868548c7ffae5a349fd8c4bafafe4dfda2c450 Mon Sep 17 00:00:00 2001 From: morris Date: Sat, 28 May 2022 17:07:17 +0800 Subject: [PATCH 8/8] doc: update MCPWM api reference with new driver API --- .../diagrams/mcpwm/deadtime_active_high.json | 26 + .../deadtime_active_high_complementary.json | 26 + .../diagrams/mcpwm/deadtime_active_low.json | 26 + .../deadtime_active_low_complementary.json | 26 + .../diagrams/mcpwm/deadtime_fedb_bypassa.json | 28 + .../diagrams/mcpwm/deadtime_reda_bypassb.json | 28 + .../mcpwm/deadtime_redb_fedb_bypassa.json | 29 + .../mcpwm/dual_edge_asym_active_low.json | 15 + .../mcpwm/dual_edge_sym_active_low.json | 15 + .../mcpwm/dual_edge_sym_complementary.json | 15 + .../diagrams/mcpwm/mcpwm_overview.diag | 59 ++ .../diagrams/mcpwm/pulse_placement_asym.json | 15 + .../mcpwm/single_edge_asym_active_high.json | 15 + .../mcpwm/single_edge_asym_active_low.json | 15 + docs/_static/mcpwm-block-diagram.png | Bin 56162 -> 0 bytes docs/_static/mcpwm-brushed-dc-control.png | Bin 20746 -> 0 bytes docs/_static/mcpwm-overview.png | Bin 18209 -> 0 bytes docs/doxygen/Doxyfile_esp32 | 9 +- docs/doxygen/Doxyfile_esp32s3 | 9 +- docs/en/api-reference/peripherals/mcpwm.rst | 976 +++++++++++++++--- .../release-5.x/peripherals.rst | 63 +- .../zh_CN/api-reference/peripherals/mcpwm.rst | 2 +- 22 files changed, 1233 insertions(+), 164 deletions(-) create mode 100644 docs/_static/diagrams/mcpwm/deadtime_active_high.json create mode 100644 docs/_static/diagrams/mcpwm/deadtime_active_high_complementary.json create mode 100644 docs/_static/diagrams/mcpwm/deadtime_active_low.json create mode 100644 docs/_static/diagrams/mcpwm/deadtime_active_low_complementary.json create mode 100644 docs/_static/diagrams/mcpwm/deadtime_fedb_bypassa.json create mode 100644 docs/_static/diagrams/mcpwm/deadtime_reda_bypassb.json create mode 100644 docs/_static/diagrams/mcpwm/deadtime_redb_fedb_bypassa.json create mode 100644 docs/_static/diagrams/mcpwm/dual_edge_asym_active_low.json create mode 100644 docs/_static/diagrams/mcpwm/dual_edge_sym_active_low.json create mode 100644 docs/_static/diagrams/mcpwm/dual_edge_sym_complementary.json create mode 100644 docs/_static/diagrams/mcpwm/mcpwm_overview.diag create mode 100644 docs/_static/diagrams/mcpwm/pulse_placement_asym.json create mode 100644 docs/_static/diagrams/mcpwm/single_edge_asym_active_high.json create mode 100644 docs/_static/diagrams/mcpwm/single_edge_asym_active_low.json delete mode 100644 docs/_static/mcpwm-block-diagram.png delete mode 100644 docs/_static/mcpwm-brushed-dc-control.png delete mode 100644 docs/_static/mcpwm-overview.png diff --git a/docs/_static/diagrams/mcpwm/deadtime_active_high.json b/docs/_static/diagrams/mcpwm/deadtime_active_high.json new file mode 100644 index 0000000000..8a396f18f5 --- /dev/null +++ b/docs/_static/diagrams/mcpwm/deadtime_active_high.json @@ -0,0 +1,26 @@ +{ + "signal": [ + { + "name": "origin", + "wave": "0...1.....0...", + "node": "....a.....b..." + }, + { + "name": "pwm_A", + "wave": "0....1....0...", + "node": ".....c....." + }, + { + "name": "pwm_B", + "wave": "0...1......0..", + "node": "...........d.." + } + ], + "edge": [ + "a|->c RED", + "b|->d FED" + ], + "head": { + "text": "Active High" + } +} diff --git a/docs/_static/diagrams/mcpwm/deadtime_active_high_complementary.json b/docs/_static/diagrams/mcpwm/deadtime_active_high_complementary.json new file mode 100644 index 0000000000..fa11d72590 --- /dev/null +++ b/docs/_static/diagrams/mcpwm/deadtime_active_high_complementary.json @@ -0,0 +1,26 @@ +{ + "signal": [ + { + "name": "origin", + "wave": "0...1.....0...", + "node": "....a.....b..." + }, + { + "name": "pwm_A", + "wave": "0....1....0...", + "node": ".....c....." + }, + { + "name": "pwm_B", + "wave": "1...0......1..", + "node": "...........d.." + } + ], + "edge": [ + "a|->c RED", + "b|->d FED" + ], + "head": { + "text": "Active High, Complementary" + } +} diff --git a/docs/_static/diagrams/mcpwm/deadtime_active_low.json b/docs/_static/diagrams/mcpwm/deadtime_active_low.json new file mode 100644 index 0000000000..b225dff2e0 --- /dev/null +++ b/docs/_static/diagrams/mcpwm/deadtime_active_low.json @@ -0,0 +1,26 @@ +{ + "signal": [ + { + "name": "origin", + "wave": "0...1.....0...", + "node": "....a.....b..." + }, + { + "name": "pwm_A", + "wave": "1....0....1...", + "node": ".....c....." + }, + { + "name": "pwm_B", + "wave": "1...0......1..", + "node": "...........d.." + } + ], + "edge": [ + "a|->c RED", + "b|->d FED" + ], + "head": { + "text": "Active Low" + } +} diff --git a/docs/_static/diagrams/mcpwm/deadtime_active_low_complementary.json b/docs/_static/diagrams/mcpwm/deadtime_active_low_complementary.json new file mode 100644 index 0000000000..eefd2b88ff --- /dev/null +++ b/docs/_static/diagrams/mcpwm/deadtime_active_low_complementary.json @@ -0,0 +1,26 @@ +{ + "signal": [ + { + "name": "origin", + "wave": "0...1.....0...", + "node": "....a.....b..." + }, + { + "name": "pwm_A", + "wave": "1....0....1...", + "node": ".....c....." + }, + { + "name": "pwm_B", + "wave": "0...1......0..", + "node": "...........d.." + } + ], + "edge": [ + "a|->c RED", + "b|->d FED" + ], + "head": { + "text": "Active Low, Complementary" + } +} diff --git a/docs/_static/diagrams/mcpwm/deadtime_fedb_bypassa.json b/docs/_static/diagrams/mcpwm/deadtime_fedb_bypassa.json new file mode 100644 index 0000000000..d4fcfc0d8b --- /dev/null +++ b/docs/_static/diagrams/mcpwm/deadtime_fedb_bypassa.json @@ -0,0 +1,28 @@ +{ + "signal": [ + { + "name": "origin_A", + "wave": "0...1.....0..." + }, + { + "name": "origin_B", + "wave": "0...1.....0...", + "node": "..........a..." + }, + { + "name": "pwm_A", + "wave": "0...1.....0..." + }, + { + "name": "pwm_B", + "wave": "0...1......0..", + "node": "...........b..." + } + ], + "edge": [ + "a|->b FED" + ], + "head": { + "text": "FED on B, Bypass A" + } +} diff --git a/docs/_static/diagrams/mcpwm/deadtime_reda_bypassb.json b/docs/_static/diagrams/mcpwm/deadtime_reda_bypassb.json new file mode 100644 index 0000000000..f65f031223 --- /dev/null +++ b/docs/_static/diagrams/mcpwm/deadtime_reda_bypassb.json @@ -0,0 +1,28 @@ +{ + "signal": [ + { + "name": "origin_A", + "wave": "0...1.....0...", + "node": "....a........." + }, + { + "name": "origin_B", + "wave": "0...1.....0..." + }, + { + "name": "pwm_A", + "wave": "0....1....0...", + "node": ".....b....." + }, + { + "name": "pwm_B", + "wave": "0...1.....0..." + } + ], + "edge": [ + "a|->b RED" + ], + "head": { + "text": "RED on A, Bypass B" + } +} diff --git a/docs/_static/diagrams/mcpwm/deadtime_redb_fedb_bypassa.json b/docs/_static/diagrams/mcpwm/deadtime_redb_fedb_bypassa.json new file mode 100644 index 0000000000..936978185a --- /dev/null +++ b/docs/_static/diagrams/mcpwm/deadtime_redb_fedb_bypassa.json @@ -0,0 +1,29 @@ +{ + "signal": [ + { + "name": "origin_A", + "wave": "0...1.....0..." + }, + { + "name": "origin_B", + "wave": "0...1.....0...", + "node": "....a.....b..." + }, + { + "name": "pwm_A", + "wave": "0...1.....0..." + }, + { + "name": "pwm_B", + "wave": "0....1.....0..", + "node": ".....c.....d..." + } + ], + "edge": [ + "a|->c RED", + "b|->d FED" + ], + "head": { + "text": "Bypass A, RED + FED on B" + } +} diff --git a/docs/_static/diagrams/mcpwm/dual_edge_asym_active_low.json b/docs/_static/diagrams/mcpwm/dual_edge_asym_active_low.json new file mode 100644 index 0000000000..b5981ef7b3 --- /dev/null +++ b/docs/_static/diagrams/mcpwm/dual_edge_asym_active_low.json @@ -0,0 +1,15 @@ +{ + "signal": [ + { + "name": "pwm_A", + "wave": "01..0..1..0." + }, + { + "name": "pwm_B", + "wave": "0..1..0..1.." + } + ], + "head": { + "text": "Dual Edge Asymmetric Waveform, Active Low" + } +} diff --git a/docs/_static/diagrams/mcpwm/dual_edge_sym_active_low.json b/docs/_static/diagrams/mcpwm/dual_edge_sym_active_low.json new file mode 100644 index 0000000000..680b3295bf --- /dev/null +++ b/docs/_static/diagrams/mcpwm/dual_edge_sym_active_low.json @@ -0,0 +1,15 @@ +{ + "signal": [ + { + "name": "pwm_A", + "wave": "0.1..0...1..0.." + }, + { + "name": "pwm_B", + "wave": "0..10.....10..." + } + ], + "head": { + "text": "Dual Edge Symmetric Waveform, Active Low" + } +} diff --git a/docs/_static/diagrams/mcpwm/dual_edge_sym_complementary.json b/docs/_static/diagrams/mcpwm/dual_edge_sym_complementary.json new file mode 100644 index 0000000000..44b27470ab --- /dev/null +++ b/docs/_static/diagrams/mcpwm/dual_edge_sym_complementary.json @@ -0,0 +1,15 @@ +{ + "signal": [ + { + "name": "pwm_A", + "wave": "01..0...1..0" + }, + { + "name": "pwm_B", + "wave": "1.01.....01." + } + ], + "head": { + "text": "Dual Edge Symmetric Waveform, Complementary" + } +} diff --git a/docs/_static/diagrams/mcpwm/mcpwm_overview.diag b/docs/_static/diagrams/mcpwm/mcpwm_overview.diag new file mode 100644 index 0000000000..ea3a6debb4 --- /dev/null +++ b/docs/_static/diagrams/mcpwm/mcpwm_overview.diag @@ -0,0 +1,59 @@ +blockdiag mcpwm_overview { + default_fontsize = 18; + node_width = 130; + node_height = 100; + default_group_color = lightgrey; + + mcpwm_timers [label = "MCPWM\nTimer", stacked]; + timer_sync [label = "Timer Sync", stacked]; + timer_sync <-> mcpwm_timers; + + mcpwm_capture_timer [label = "MCPWM\nCapture Timer"]; + mcpwm_capture_channels [label = "MCPWM\nCapture Chan", stacked]; + mcpwm_capture_gpio [label = "Cap\nGPIO", shape = minidiamond]; + mcpwm_capture_timer -> mcpwm_capture_channels; + mcpwm_capture_channels <- mcpwm_capture_gpio; + + timer_sync -> mcpwm_capture_timer; + gpio_sync [label = "Sync\nGPIO", shape = minidiamond]; + gpio_sync -> mcpwm_timers; + gpio_sync -> mcpwm_capture_timer; + + mcpwm_compares [label = "MCPWM\nComparators", stacked]; + mcpwm_generators [label = "MCPWM\nGenerators", stacked]; + mcpwm_dead_time [label = "Dead Time"]; + mcpwm_carrier [label = "Carrier\nModulation"]; + mcpwm_brake [label = "Brake"]; + pwma [label = "PWM_A", shape = minidiamond]; + pwmb [label = "PWM_B", shape = minidiamond]; + stub [shape = none]; + mcpwm_timers -> mcpwm_generators; + mcpwm_timers -> mcpwm_compares; + mcpwm_compares -> mcpwm_generators [folded]; + mcpwm_generators -> mcpwm_dead_time; + mcpwm_dead_time -> mcpwm_carrier; + mcpwm_carrier -> mcpwm_brake; + mcpwm_brake -> stub; + stub -> pwma, pwmb; + mcpwm_generators -> mcpwm_carrier; + mcpwm_generators -> mcpwm_brake; + mcpwm_generators -> stub; + + gpio_faults [label = "Fault\nGPIO", shape = minidiamond]; + mcpwm_brake <- gpio_faults [folded]; + + group { + label = "MCPWM Operators"; + mcpwm_compares, mcpwm_generators, mcpwm_dead_time, mcpwm_carrier, mcpwm_brake; + } + + group { + label = "MCPWM Capture"; + mcpwm_capture_timer, mcpwm_capture_channels, mcpwm_capture_gpio; + } + + group { + label = "MCPWM Sync"; + gpio_sync, timer_sync; + } +} diff --git a/docs/_static/diagrams/mcpwm/pulse_placement_asym.json b/docs/_static/diagrams/mcpwm/pulse_placement_asym.json new file mode 100644 index 0000000000..ae0097d351 --- /dev/null +++ b/docs/_static/diagrams/mcpwm/pulse_placement_asym.json @@ -0,0 +1,15 @@ +{ + "signal": [ + { + "name": "pwm_A", + "wave": "0.1..0..1..0..1..0..1..0.." + }, + { + "name": "pwm_B", + "wave": "01.....0.....1.....0.....1" + } + ], + "head": { + "text": "Pulse Placement Asymmetric Waveform" + } +} diff --git a/docs/_static/diagrams/mcpwm/single_edge_asym_active_high.json b/docs/_static/diagrams/mcpwm/single_edge_asym_active_high.json new file mode 100644 index 0000000000..f7b4bd6446 --- /dev/null +++ b/docs/_static/diagrams/mcpwm/single_edge_asym_active_high.json @@ -0,0 +1,15 @@ +{ + "signal": [ + { + "name": "pwm_A", + "wave": "01....0..1....0..1" + }, + { + "name": "pwm_B", + "wave": "01..0....1..0....1" + } + ], + "head": { + "text": "Single Edge Asymmetric Waveform, Active High" + } +} diff --git a/docs/_static/diagrams/mcpwm/single_edge_asym_active_low.json b/docs/_static/diagrams/mcpwm/single_edge_asym_active_low.json new file mode 100644 index 0000000000..68386ffd9f --- /dev/null +++ b/docs/_static/diagrams/mcpwm/single_edge_asym_active_low.json @@ -0,0 +1,15 @@ +{ + "signal": [ + { + "name": "pwm_A", + "wave": "10....1..0....1..0" + }, + { + "name": "pwm_B", + "wave": "10..1....0..1....0" + } + ], + "head": { + "text": "Single Edge Asymmetric Waveform, Active Low" + } +} diff --git a/docs/_static/mcpwm-block-diagram.png b/docs/_static/mcpwm-block-diagram.png deleted file mode 100644 index 97620c29da0b34496de769660834f53ae44ccd18..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56162 zcmeAS@N?(olHy`uVBq!ia0y~yVCrRHVBEmL#K6F4a^y!60|NtRfk$L90|Va?5N4dJ z%_q&ka9hOF#WAE}&YQWMHdD?^A4tEe&r!!qT4@z{>;kCYC17fn~RHU zi*ie>AxqKi59VT5b051r;QuLn{h|7w+w)KJcpCHnFs;V}Dj!8)mX&|0k25 zZPNl8w%$!VFlT=LxtHrIzi>=guwCZFsY34{za$<1qOj}I`C-oPRi{>dv*NDWcG7%u zcv{Eg=~GwU+*$o?X5n0?TpqQaj<~DYE0-N>)Sqt|?iMDUTO?XCv6Ayt!ucZ+m@; z@rzjb=zZ54jux+4zg}Km{=Ce+q-&xTHnSh~SKjvCsNecV!(nRfGh=HZySEEISM`QG zC^6rWxF@&b{FLN}$x$3-=LM#HKO+94maUA zZQnJ!s%-9mTM@We&aTEoo@w>O`lo_IH7jy$_@`btaMMcUp0l6Q#2L(novo>@XDiN~ z{QLLs#}j6L6+7NGG%!?GSJ%Z_6#lQPD0zEZ+$zaD_s|}blIv>{JNiFgU9fIlpSslY z+xH5-&d!@|=FPUXA-3D~n63Yv|5tVvKlht!_3-3$?K9?TJ~!@tyZ<3_x30d<_j>;N zXZvPp*;;R7x+15#KW}&2qXUcB%XQOJRy7^+lZu=ZJ*6UJMUMIQJq-Qs&%Rk2ZC`d; zPEwLHUwYq3>4;5L8aFj(-*gq-WV_$&+5PL0ewQpYH)w9OT=(g)MsmRZNJk#Il-pOX zY-tove)e#o-(+!m!YO`;VB3lMi2=@}%f?==|?lYjYB9&RbnzyDlwc&$B*{ZG(8+)z`)|#P`L& z_$vQws+`6A4_9~I*;exXYos}68n1U-=G+@MBAA(*TUuCtZLYZYge9+spQ}f8%SFp| z7ff{bq*s_289n+M)zthWclq=Bto7G~>z&%B?P(CGcx<9|@_yorb&o#Ps9MH^FDle3 zJIlm;t!ZuUHtBS=X~*{Nz2^7u<*qLup8M97+`O`leO-iUMT-8{V{CH_*Uqt7ym+bv z)A1h3;>X8$Us%ld+$S_O-OjIe8rw<^J0_o#o%g>r@IB7Vo2?vP7jEnpad++S#_hM4 zZ{J^Isb6w_$F|k)(@q(=CHj_jZ9iFh+HBU^=H=QeCFD=;+8;Ad>eI7RUl090z0pF~ zbgQqAkA%>n?Vdh9JSAuHyl2_2W))j;?3K`w{M(PueQ{`)Idiv*Wz~#d;St`Si{{DN z?7DaM$o$Py`c5-HmnuJ7o1}Q|bm6lHzt`r(saO8^kXtcj^X(O)#Vn7u`|eKXi4?o* zCAeV43XXnntE(rZPgk???#%6;{7I1ew83{X;kmjOH7$fURnPx;v}51xv!9xJ%blV5cG z_3?&3A-_1b>~4*X`t?q>-Tdd1mJbbgG<2d|Tv&=%1_cK{EIe)D%Wz2T%#s#^`^Fh3 za^LSs-dk9cah5md&XNO5wtU=wQgYW8nfp)X#6A1@^Jm2DJ^rUZJ~ey&?Tuu(#s2tv z`^@!j^~_i6xNr7$o+4KiGspA8?itP#-xt5X!qU1YRdO>U*PF9^yBYs0PY76$U(~q& z(u?<%S07)x$;M@||HQ>?J2olD*?+$FM>AXd9J%v$>l3aYA6K?7H@7TSSlhlo+vD`2 zqP+|Bxc9Z4i&<#?x8P7}+l(XmyO$p`m{sj7zEWV>ujHD^kMrg~%ACBp<=Wx$>WSR_ z)+_U#RkQth)yOKs=(?<~jpOzc>2-C#fB9cM@26{gW9fC7`=4(8j56uCzimy)U4w4h z$9uR0H{aeP9CCvp0z{amI0{%Tvi{>mQ*t~YUt&C8e0{&J{#o#z_6=rj(El{XH* z2aC?`zkkh@W76AYF2Q?qH_p;iGnti@+r5in5#!x$E)M&RIHJW^TDBQ^v|L!v^T=SG zTITj%;l{gVPgnn$!C<8H6dN**D*1ci`Up| z6Mt3KspqW!tZ|by*j>l)#$9892f5Slez@m4b8gg~(}HsA9b27}_T-+jbzZgEaDB1- zd*^tGeCcQbmuUGfcC-2YEfV&;?OLwCU60?SdgJ|oX@At`TsidVV1NB$Ym?}^Ykz-R za{km6L62JkHsVjtZunpPPPh5Eri|L=tNYIL)$Tm5$94GEg0|rQ+fv*4?@5}^c-8Wq zr~kt|$G08kZ5g}uEUvQ`vc7z~jAd8uLSb?7>-+2LXH9>4dXnOV_}9%5_rIN(s4Q>) z?7U6wv>26%Y!y|9wpS;WnCO03yjTJKmR#-i3+5CCQ|1(ws`*@6#wi|JL^Lr|~>%H%v?Qzo=?d@Vbv2Sn2)gD7O`s| z#2K%dvA;;SbWD1*U-DJ?t>SNu`6rC$SU#A)Ox^K28y+UX8iX-*L7faSg z^%y1}Yq-U=;@o}}Rqsui^*dkRT`;#fWk$496DW;X8XeYO5;%=#XNLFNKM`9uioY&A za_EtfsOq)kj`K0>Z0&zjT-6Sme)AuP)q2x~i=6&XbaY?FS@~yYZhH5>cBLN|(sDSyeCLr| zmiAqHderSTQosM~eCsS|qcbt__%qjJ>3N)fKZg7KZ;lab2{iAGnq|avzUI8cjQpac2lbPkefu&^-(Pv9dMn7bYTbMd*@8dL zyIx7mXL_a1Rpnw|bZc8~^t)zWiv^iQZ`%KT%hYH1z;AHh_`&tu>iuWRCD*v==)RpN zeSh|x54M+?Z)g27i?dMfK014jzKFd&LIm3E#w-LE{Q zf8NH~XSOVJuE_r+W0YEIy?#UWWMk%3>3hZt_PXp|$+UstOp0t|MSc;}cbMid~m>rq3 zrYtpO#r}pprhC5!J+thZuA-`ZHdW8b?Z=|PzQUh7^)usQt=6yW`ZZ<4`s(A`p3FI! ze7){(_`J|CU#6~LhpW*Fmv=8%baS>)Wb#af1u4s~Ui$UB)>d+_u84N?mHdXea~AEB zm7X2Od&{FSE#}=Ox!z@)UQXcV=U-lVt3OhD-;TT;x6D=c=x$ej7^7`bLy zcHn}&o1aEym*w=9J1y|>^}XJ3)r5DQDX&KM9?jPue`|02_Gr7mzKliKwl^Ox+&^rn zp31KLDro5~R&NpGZ4tYfubJ4yZ35&nSRNWR=_VP;_v84Oh+S<0wozcSC{IrElD(JwsQxC+G0&l)JJ-a}lBGveP+UXt+ z*1$6l-*aq?@Gz@S+{vxCJ7ddquFcTi6i5-KVx9<0pt$jDeRb*kvu)e9Fo79Y-v+gX(A%+*=k>^A2^z|Ae9>J=|8 zFF*hLd(xGoM|OB9ojr2huAsR1@J#huUuLz*QeUnW8%SlyFH`&waC5`5>hnBT=Ub(n zaF{B5c+Dj*d6}AD$%*ry1+0Ja^!el!vhOCGoWICa{QazJ%&RkRv^>kNo^)P>{nUzc ztJbWkiRFxmp5ZDQw>`{|OP6Efv}-mKBcI-IvngBuSl%Y2EF$7Yd$;K}y_ijhG9PZ) z`(PvAcZ*fkFD@)RKi|Io<0IDj<}06>Z&FDx!(6&wwgto%DQcGsgd zFV}v28eQ_p^X%8(dewLS{<(Hx&t->Q);!;TJ8#>OwlBfe&Wy+P#;mRrQW-q&wa+EI zE<7@GmX!t{&&I_(j_WH|8~^b3%kV7K-g}mPMVs+P-UF8Dn?mFH7`hG~DYr{*I4=>k z!*TOQbVQOBaMBuWU4PnGu|=G&S3on{m$XrH_}q3@a0EsZR9| zdR=P#U%BS4qv8j@weQw?NxA<`O21$9W@mJB$}fp6b7!sn|EC~NXJ6)3JNwOavvfjo>;+Xh7OQh_Z+~?%4N#xnJ6UVCniR=$U1!rpE^ z*(sIN!Q@7I1jeeLbyvkTMaE!Fu@6`sCl(xu=Z6M1B-dpNi>*n=l;5PLXr z;WEVyZnrg=yVpCztS?sk@FHF+Ha zUWt`^?)^-Pd$I3`R(b5p$1AUP7e*uqE1wKI(=VS|G%x=4{SNiVZ(=Ji-7G9L{pPgs zdd2CF$0r{T-ZOE->fIBJj!IlJv3ayIQ(@96z8SNR8r8VpoALc-TE5mnhB|Ss?71IX z8hkDZX3zb|Eg14`qPyR^WPQW-**sw$>)GsA|A@c2<(h?cPHwK`vIAY)O#LNr`J0r`-l^js`zpm=)wm&> zZ%*`MmMkf=dW$P23b&-vBQM|Hmistr-jypM_4W0|#l;mzoSuk?n8kV9zWV>?@{;2H zZx01cFO#pk;2ha_X+yjJ&!>TD{`>DgnYETftNPf1vpR>DpJ0x-?5}lzV`07fv7833 zcHQWLy`>v1S8m~No6-1I_puJ+hL%J3{#MNO3_m3;=MG8;Wk0O*Pw&$^^YFG$$u^h$ z@`|S~YlXy^+`Cq>SYA3k^0LE((+_TZePj7SS$ltDT1uo-Ma~@kjs=_I|L-^&l-{l< zU-TjK+2S4V59v?ZanL4PS}X0xuDDfqpT7{dS1{(7V#}m(d)ND!7tT*)Ww4odz3@}y zyZy5{v}VV?KJ)8CaJGDis8bv8@0HuBBSeJ+&U8~;Djbf@gZNe1W7A79ohL&fVz%hhxq9OouI|=Qb_dI5&~uc&b-t zXXm_mQ}|`KbV_^(v@<-jYV+}rQ_r{8-~RS==ftIzXLP!+T|U1&RQFnbr|*O3RpLr3 za`@z@e!NnW%znPkVnXfjZx^dIee-hG?Ry+v5%FbdTJoHmbC;%zKY6@oOGo;2uJrd# zanb1u4!Zrjc`*BR&vjL?IqCHaRv!6!K++%1V2pFa1mKK#PAj$cCQ&ZokVz)A713ZK4x&NF?ca?Y`rce{S> z`Rfw0|J^M2yUWGIru)W}Nu6#n{xf%1v(|3884mBG0{{87g&kkW$g28ZH*fi0voeXawaIa}mZ+ZT=50^d z=4cr$pJQUWH|?xcp!mYV%Nbcx7aan8%|$_2D%H3C$FJ4p;kw_R@66A(Tz+{@@%?jW zUzvPAWPa+Gd#0r1jdPqj>y}79-T7&Ame#_~-$~w&SHCpv=l<4zWzo$WKiFTC&0JH% zn9|l#!!!Td>9%6E)c+b{(>G+bwTOfpt@$=>Ll#@_#%22QJGRt6nH0B`)nAjTc8PG~ zBE6%^&NFxXJf5rD@gV5(pQe~;|EDimrF8t@mT5cQ%)b32YuA_8-V8FSj%T&j@7U60 z=KB2>&*Ez)0V~)Zy!-vg^^OteFW<;R`S)*`Jup9-z@~Pg!e^DC()wjguk1buS0ChP zKNfzuEw)HkclqTq2fn}e?*uR$Iy^ntMpS>zviGdD?nP&p>{_*H7SFlVYZ6Znnz?VA zHhtmuYdLezOw?m;SKnXfHTi|zm$;dZZGWwf?)iJnbn;~X{kfZd-*0pOx17yarugZx zqf@=hzt->j;N!Ghu6Wm?-dw}oULIszX%M{q=CSOR^$Zn% zUM*Q?a#U$T`JVjsOE1d%KYzPp`lfAWo2Kq7Hdnon|M>FK-S=)k(ovmyEd28SsvD~= zRK8v%8T66M-_-fvvu9H#GKQUN&r0_;_^VS_(W8bsd+i&=A z%0K(QRq>ri&gFk?c04n_`~P@J{VxWu>Vj2Hn^&f*FzCa= z)sX2OYbN{Y+VW|0=gi-b^;$ANKflYp>_V%l+WJ*rvi7{WnQC?G@kKp`WeN9Aa&P=ay&4FMb|xx7kabNh;oV`u}ql2FV%6Z#Df7 zy!QIyR`a)S*8IKLrEeU$e97U*mZ_0ex%QvaAG$yISS2~bXKn0t$+juI7oJuZo@zSP zyDn~Z?C$2p^CPR0_t(5jDzvanV*R&USf+RVvV8OFZ|rN1ZQUdld|5a3gx~4x?Q=AC ze|-K)GyApWhqK2mg>PT`CY2TG82>Kuo_6Yk^s=v?_)6brx5Zw#DY$coEW`ZBYVG?T zCwV&J{|VmD<&^O=-TNuf_4HO-{_X$Ymj|dTSkDn#_xe}O$4LiMySKcvlaJW!7<m~R1)xO-jVbY7eKdze9`m`}R z{y3))W%ci{v+wk2)85_O-2Qr-u(*EQ9LwUg!(OTyLNCS2SVA?1x16n952>WTdAbx0Pc^OH2DOQ(*P%B@4Yv3a|HXF!(pk z<;R0nFYEsP+FScu%!)5|^_@*-0SlitUAhz8l{?q_Us|-b*M9kJD-OSYAJ}?5vE`cX z?LX&^Zrxn;*lF7O+eHki%s9s-s`hURTC70^O$`%~e)f5YG zWNqBi(~~p*|KH`0n&19jYxU6O;9J)lC5|~&D^w2dU6yE?e7}#cCuj4o!m=RI7mBVo zkL?ZOs!YG zn#qmJTDj?EZi*rqe@tWlzFsor+M)c^1C|QLNpaB+mT4&y8`(Hl zNXTpM*&oyJy+L}9u2FDsaE@^5x|m%hCr_SS$>=I1cf~h%SHrBmcQtSC{;YB#!v9ye zrexUXlkrblqn12)>9zVUqi1i>>s42_yF9YpxFYR9us(N}=DfLorMyj@d9JR_U9sDw zv1|Dk;inn~?FNpQVi@|_u4uJh?O~N$G51^d-pe=M2I$w!dUt{|<;~j9XBvMmp0u~m z#&7=}-ycr}HGFq1+s?l>*ZxxH`&k=fAI#6-Qt_Bo;%lxM;QQEV-umAy-7GPtvL#kl zhf}tA&wjA6wsujFM)v_bl|Qf6F8{iQiD$81?$&EFq-K?>ZjiX4$+=|Zv`%gmESrQz- zv?_LpexA8){@Mb8#k0~BUgt|~Gq3e}7d>~{-{O!eDV=BK|%Gf%DV@0>j; z?`=&iC)=YHZicH~?uBzM%E zD#NEI#1^EmcQHTQDASPAuej^AP2(ED(;-JwS!F);bpB6UaliAa=K1qevrFxww@1j# zD!&zV*Ko^+jayH7oZ*W(r#d^#xasxLg>BhBR;Dk`-j$AY)G_|D@5hrTY1uaAT(5Y} z1ulCZd41+xW!1Rnc7`o&H?EW%bw0B}?%b@l6%oI*XE6uFnk%WXn|_?c`~7a1Wc9wZ znMZ=ow?@1?b?W5SqpP#s_TId+|Np1Z2-C~Q8y_-nJ3jHm6EC6Y%NI8p9h%ppWM-;s z-&?fi+%ZYn&D)$?twh#`7%TW^C8oYPnb|GLw?R1L#4)=MMYaYv7uj#>WkSR;DrkpntC&9C$;5g{w%n5ZJMNH zEbn)@uK0`DnI#$K`iIX}f0zFgAJ1KEGhJ-{tgxK>QE7J`P1#+?w8yRNVmV)WtHCVC z>?}zunck1lUfk&$<{V+jU1j=&JylA3K~&#TiT>2Ots9(cvZ5rug{Z$Os!IHG;*9u< ztFGM=S52&|SJlWUyuO$w(Y{&ci^=_+PamJK6^a*}Jxi?1_G-1~zvQYjsT@mY_AP$@ zfNPf8pVsCq|F7F_i9NGZlRv&Uzt-wx%;m-I{dyV;zHEN^`*M@q_iYhRdQ0)BTUX?-dgQ?o#2JR7Gs-t|ltL|7oEN0{1*v;JdXiCKOf6QmqPW9TEtPO3S zFh{=e=!2Bs8^3>Al(D;{z%3^$s*K}kLUHIM2mMVKE^y0TUmt&eb9(Q`q#F|3wymq| z&)6E4ID3+RL_|bFUsz?UnApZGA0|x_x}|yI>gw=^8>6@9&9}e4?r-2F9o>_!vkD)E z-gGjOm?>^0?6YTuB5VGFdwQ}m0<&j5tL^DwsQhnqb@tY|hRJNrzTuUXKR0dKbanHN z{8N`5ZrrqN->c)Nb|E`_TU~F1{-2!-EvFc~*nYZ~X~*)1UB14j{a62;)~ao_yD;C9 zp{9P~^o)&i3kta23BQG{XZ5ELDer(y1&D#G@ZV=~Q+xB2rsGXvj z>Dk}gb*4SOH%D{jkJArza#T0I{v#mq{aL_Sf&R-EeYY-qx}3G`{`SpBr03qOAjmU*FT*i)ZQjopRoomeJ?x?tb|^lYoXvFf-5QhVSq1`{%bdTvTE_E+*-B zWZAOS#f>4}r^3BgnmO&;G<~{w*k!ijnxyl(!slGo^{jpWw>ool`1*Qp-SX;^j?Pc{ z&yse1)zy)Pc^u3y@9s`Z-g+j+@9oxOeK)R{Fesk<&$2gJ=5%}7_jj`z&v(livy^AD zwdh=M=*gP(SG=#8Uwn;BX_vnE!#SdVx9d3eS98m6T$#D>jfD8$pEm8Y?$s$}eO@%H zQQ9%Q^YOL`m!n&5T?Yk1KUKzPiHv@MJu8oZlZb79HpQyJ4% z+NB;XirDaYXV=?z%UO=?)pt1}A5r?s_f+CE$+EWxGIum@oR#g)P|iQ`rtEyTdbTj* z1A%89R7`b`FI%Rl`YBkn@cH4QJG*~fuPnRDWBIM*sN?GJH|LDjtX|N5V8?ffPYWY8 zm*0$ieQWU%O}AZd&T%mbUNQOha)RL&9hno+TL1QP%zC`(`}w>((O%hR?ICNYTJ-YY z-W69Pe<*6Au*COkTT-IWPMGo~hA&-GbJ>lM`&ZoS9{-)ZVCU4Wi%K#)LlX=Oa@L0Z zxh-zIg>S`dtpbBH+Fi5s5<@S4n#RALtz?KS{7RDLA(B-*4lhC}W)^o>tPKwq=v8 zBV}~Y$_8a;%;i>Ek@--kI-OIVJyFWz+x$glx>G(LTX)rWS=R!i_d>ci`m)-kfB!fr zDtM)&GE}qOO_KMF0#i$W>0*`Tryjmp@-)5W!$YCRXP3_x=4G|$?~)X}^5yXQ=`LG4 zD$h-rD|3#koNwC(^Kg-)CpeG0o?&YJy!*+9)vWEC|F{ZpAFz1eq1ZG_ZBIw!1&5=V zFA74I&iu1>@swryCCsljNolq^c40>oTYYX^2_7Rsf-tRH)me;TD3^k(<|*jpE&<%4>s-UrTnig zHoV{S|DpVcX$~y8H)J^0E=z5Dvomr}yJEn#1bLgfd)8Azxme~qR)L0sqI4G2o)nN- zv1~RME^L&s+k(aEki+|Y7N<)NLA?$hQXZ=WIXsy?O_>xk6;`?_sPL*>aadv4U+5}d%oI3a|4-mCYE&DCvg z7)n|>Uz3n}*L`u3@Y4M|EmOC?&Yt!)PHc*+x^Adv^0xY@yMKb7iPRrIS1xg+M`Hi| zm~87yNptGj-xqFQxBstkO^)`j&FwjB<~BP=s{NV#qu|iN@49e0Ih!$H_P(c`VYP2>>`B#b^-|DgUe(p@r+#gFhtA!{tDO5LnWbiT z9nv}5_?2aX!nw`%vfWGgF8Xg}3aVtdkQyZxv?=NC=JfL`=3P_!de(Ps3pDpvo=Cgl#3xr!QTvxUwPfutB*~NzLBkpj@{D#W&umU%eUmq^fMS zrK-xQkDj*{>({rtH$=knRW9#ioHF0Qrn*`S-w2^-B~m1!^KUn&zz74`G!+BI?)%gh{Cx6r`~5BKeklT(4!K87)|YP1IC@4q;FVA2(U1=-!4`@MEk5)1 z-MhJo-)w##^EnHn5Zu{Kc@NP<5 z(9~I>1$8%gB#!_8!8EJ(f|*m%B7TFZ3Dl^n+)-HKCAPp&*~?7EcIb#vzFg|oHS zzARbLyC_6ug6`S{9zOABC8xSJS_lgz&$zn9@k&~*qt|J}^vMT8cF8I%z3o5St+sH2 zb{$Lg6ov~t=}wDc{*+8$bJia6SW=+@9|9*S> z()9ODKD737?eX^>3v!&eo|Ue#JDs5x=4MrVMMBu&Wm@CpG+FuD|MRL&Jh?pK#XKfq zJC&in5f@KIiPZk(KcJx})8E1Az`OpxI6n&Nvqu2pVBiQU^W zlMVkoGgDJj1E#&qG1lDpc1>!^g`O5;MaO^XH~goRxu<(pl{m6x%z zv&-CD^7_I5v-=;aO78kAYG(1{Te*Dv0kC9hUJYGQo9*|x&VgQx%b zd&A>P_VjNP_G7XAaI8A)+KWgF*INrb?%O|%Wc`_TX{88Z>HZ^$AA7#ftpHN?rTfO{mqki^(HFL zOjYdPTGDl9#-b##&%f~iTTdRCupWm|ke*C+T z-1qjzwYbd8mp3=3&zZZiKg=wuODHre|XZ)&vWl`T8N57gCY&aC5^Q|y%Vqh{}& zbu)vWH~v~5U+b^u_59(QIfeHxAInw#mle8frLVpG-qjCNCu&G7TkWZA_U*56O}^gs zja=Hb&-3@sYfSR0U-Rqv%}smni)zL@XWP3^TH@f!bEW9jUG;xHE7W)Wo_y)S$Kuwa zL)VKgteYowXKVI!&^U&5$%}yS@bELeGo95YR!BHEo_v3AZ@0aje~h>8F`4=QY7#A$ znq-uQUd`&tKD2Q19u*b00?umZi=QrA%$WFW$^?T;Z|nbQtCd|VIsez^*xOqIrUFy5 zazq+`)qZ_h#mi8Y*!Y`Q^Z#WL)3dd2#Y>M(nj~8)lQb{dy6b&jqKYZk$_Lk&7+%%7 zFYGl>;MsE5e!@zQybz9Q^Eqp4YwJ>59XiCWtbb-KQ^mO0t+(pO2gOF4htleH^XxD0 zG&pqV&>D^T)sc%AEeeW=IPoqgD{IxZvcvlYt}E;5t*ih4PsiWV!s5rzpBueT{9Lmj zvm=qYv1!RJ!G>?)jvrViWr1t(SmsOZ9GwdLk` z=tmb88n*L1a=+JD^6iG0qLfs z2Z!7FdC#{P8yOj8Wo5Ald3k#ad!OJ_RaNDaoaCXxm;U+FrytQ09-p6`z4=sAUH$mO zqB-Y<817mB3!A;*Dnp{w-Vo&n7x@8W{;0S1ek)_Us2+O)D#@SMoO= zr2l-$&2}ictgLLdS*}Hm#A8mK^C4P1m1!v{M~)q<`~8jgn4zIz;)RnDPH%Mfb154t ztjuHiVB*kEs52MqGu%908z^NLhdR2(kqYHB7{nwgl0oDw?iS#2QT_4VRn z_n#3GE7C$kL$~MLJoHINq%zD8BEY82X>uBCDuVA+gL6E`U>mO=Z4Ck-Z zJnVSW$?$Q(v<=hNd8O@MWbwUJqMc)E{QHg8rB#<@He~X(&oNxGn102k&jozTPRU{$o;N@B^hzli(MRE#N6wDp4`%D$HEUkS{ASj5_0#%wH?~=<{>2cm{Z{O`inHq$DDnQ34~b9R zT=w=fSE!6t(6MCsH7`@kmo^+saPsr(TeeJXKi7Scm9%PHU z-2bss!kzcm{lm^Pl7r9hf6!t3%Om@C5B(#8E54R{jFZWvmtECKF4XHZ<9WK z{u{}9v+T*MtVvU*ykC9TNOro#7KNuj{Wa$o=Vlsv$!NOv?vB6Aw`F?YiUl^xeu1j( z?x(qS-FU=5|M%6~+w)&wNSM^nEcfXj;#QF2*Pn=lz?aj@-)!#S$IoJ4L zrWCiB$f4jBX;z}@Iw36F_38nKQ;g5eVEPfxeP6V6-v1}(wAYKTwd$?x&a=>~`SSbZ zpSUN+@{MZ}Gao))ugI;Rx^?B6HAiM~K8fG=wYSqtQ$yp=>M4OsBT|)4o(Pzi^82C3 z5>1X29+_8J8kZWf%sTWXr%jpi;?*msiO;iRKvFSPsq>-S;rU+e76%eKb9sArb_SJlQ~@biqjZ?kPk zv(G>tW;p*B*RUmeos*Eq51@Tbp-oU5bwZrMpq&^&gKG2-RVkFC+` z*~EO91m1jq)hVq0Nmc$z>oeQhogYKVuiAe5t)9tGxb8QJ z?wet~q%6wWZH+d=x-S%gt)eI@T!pWX2M`28l+vI}=Msj>TV?mxA( zCuY}$dbfnsO1sXjr>1J zE0tl<9IGc4|27Ce*w)C=sIY#W-YX@A9|~7frWzh>oxdrorX=j<-eaZpn@p`{CO%Kx z*_W*O`niU@*%LFnx(Qn})@>@f#@zkKrk{83Zzk2-Q}62d?VlIEM^<-zw#K3PEPR3K zo145sCLd^22ruu@<(-}t^w4(p$J#A%qFbwfJzT1H<-px1*O(0Kf1Fvhd;Y>pad)?W z?JM7(S-s3JC~!w~h~zS(eOo@9TYvEFamL9d>ND9N=dW5lyIn1{?xpGHFVh9)iuwoH z6f;hL13Gh_pPT#n`|>jz65^h(SiSo8YVoJcZ?@&$o)Ds3(ByWyrKP>SefMtb({pYA z{Q0vWGTXYH!G66T{5Hpt{zJA2-^{EKp8LoLT(BVTBEU z)ZRmT0zK>6<#pTmUd%Nw>0dgJyM#gEeO+x%SmLY9-zhJ6XUv}Z;q}3HySGJbkz_W% z$@@coTFrCW*X8@=*sPv7yyx7#WX=ASv-5n8Eb@K1xu)@8otMe>O=)c>BWfzXY^vh( z-l~m7U%C-F?G{|7Y?tO|H*-qs*>+`{&kIt4*^W{|L~DdN5md zdffkZ*V_hW9_#OBRM+#qDCK#vXyKx_hL&8~nC9k$#!6EUlx{G5o_ zrOGC+WJ?_0e|7$x&~oxz`oit!w=fI0fBz7Z+U9z@PU%(75pA7YuKkj-w^m7={oWeq z+`f2ksfl0SwquKAc`6>&e&iH;d*j&3wqJ9<=I+^$A!`!S{O!z>s`EuKhpO-NYil{-Y&7dp_vKz^+uena?|i*ptY-ZFs79a(=gMz8ta(?Z&YP5T zZ%^g#Z*O~d-Cccoll;RiT3)^PF4#H~T`xra#}z6TiQ3>RBJ{ zvUEirs|n6N4;^+aJuCFs(W>T*@0tTs_NQD{Su*RIMk+&>5_{r{yK~=a-dK|E!Qm{m zNLrfjO>gqsGN}*m>Yk{6yJDa)VfG%G*X+z?vp2GpElzm&E#Ssv@ls6}hshIOE#s_t z|K#uKM{@n5_M1LEO-;SECE!{9#3PJ{^wr+nezL3d^^LV0>mv6>eZ8r+Cicv;|CfZz zd!k-H+gdH3IeXqaw(qHZv;A1>ev0orkmeWnz;NcIOvkmw=h`&R%)YnhoPck0UQeHz z;?)}q4Da1|y1nnYUxbU@x_jRrT%OXuH}q#(V*HPifcKsjZ{P1p>YVcGP|_WLCT%`B zn;Yxn?W?P+59En;m3)8jazRk^sq3{bYR_L!{+0j3_T!i82{t$Xcdxt2xLCA(%Br-< zOO~!qJaeijGsxE2gnR3c&l^lRS3ct{x+tW(QZ*!9-gM>ZrB5>d%-|1B{=WLr%TqTL z+1cC5qy&EIzLfb<9=-Z-XZ-!;*JtXjFjX%*5OP`5$XQVAdHI6qM-iWQ2k*-7&pn#F zbbGk6#?{;_Qx~pNoABY_zn{T(^m*p&e0^}HX=z)Roy#pPuZukbe`hUmF!nRqqspnZ zeVX(6=lf#xg0s)O__4lA@42kxrT#xsxWz!m8$?^!FS=&McnzVum=Hp7}1JQqK7t0`Vpe^X)XEf?lBk852F zWAH;m7X#N%ecw%Oe;Vsm{Is`!`>uXk9vt_7=gpH+Z@&EqdAd&f?B&p) zqWeOx-yW_rWpthUF8NOR*T4I-&jp0B&ybOIHx8NNxy;<@N#uD8!56mAW8Z0q|DNDA z%TRenzf!H;*T>GX=G)319yr4PzCw3N*CZC-s1)bP6S8`r&8_UpUmCDnc;UKd-jZ27 zRVVoDGg$s_Uc6_gxEV9|?MCCtjC)@;Nfjr3zH{p;%ZpvM%hhC_t~>qNHNIxIUj+|; z*;?z26(6T8`Mc@&#d&$Ga&sYj|-SG2YY zi!NCnVg4*WBWV56mdl%d@3?wnreWaxf~e5YtH$Z)E|^_Q&3)*x>$z&hy&U)b|I+dw z-_O1B{(E}86+?K4&foG^r6$!M=ZfUrEk4=C+h3#}7rV9O{%h7NE^V9Fem@eL_x}9+ z`!Pn@eT|aL<{t%iKl<45^58+ncJr53Kk7d}my7SwT(32CvYLf=&Z5Nfe`0Pv3O2D* zmR~eduxINs`BR_%CwkTVThFV$%kDZBBde{DYU*x#%D5|CLe#zIRMu;H-K$v7o48&ZTtL;w`n`(>`7J zdf<4}dg(uB15UWu{KznjI<%JCoO5NLFR%H9ZAYYc|M@r1MEUx=DHor8RMSoRP-}Hi ztldM1oq1jYx50}{7va-4HlBOUHYv;V!GpuBHXd8iyZ7DITng(xANZ)9Wox?^w0U?! zlairn_icw=B^jEplsM+7DPDbhrQ)ShSnUpzCqc{H-+ehPRTpJ#AL{$6^Re*y<;tD9KPFz>HSvD^R2J$swW$vW?AX>Jk9cdhH!PTKbW-IdkaRoiyV z`*GYm;i-^?w$m^9d1CGA%a$#BHbK#OgRX#}PG?`AoN=2`X!X%9(Hm{SJy*_g-!WaX z^!DAwzc(${)Ok65=IyNK()(Xdzq)F=Zl}EI&+HXP&vZ8i>xkdbWc)oXnYl>wSXuZB zU;95d-tF4*HO21igWx*B60hNv z_eWkoZ87EFGcU9SsfvlSL?`3`F3gBp9xi~HU1r$VEg0uin z-M7m>RdVyj^UUUQcUt2kKW0mt?E|$wwnxUNUKhTc=Y3Ug8L#DT34iVwo9B(m)y)q& zPu{Uf%rspn=&O@_yj8kz^O+lU+o$u)Y6T8U>#q&#NrpO0} z9(Z)`ZEyUN->-Uleu*wjtkL^E(L42!+523f)_izY&%SSq<+OV1AMe^H=SjUhd#&`m zw0*_@?DwxjA2?pK{kxBE=X)jwOaGf68k%@ApI_mw3{Ml>A8IA_miaXI9?4%5B{$Sp z^adwh=$NAQ&%^X#<%6py*|(?P^52oYa^ugHeEr4$r#=bc6rXWJi{s%L=KoS}7QKJ) z{O_CVPJ7eloZg&vR;q2n);RNR0p{ORPHxM+J!A3(i?TNo%eysiPMv=1VojvD&dUGi zTmN6l+MkmcpnUS^{>5z*u6TEIS)`t}_H|knT=eM5?94}7S0#-~p>7oKH)pY?d|BCS&Q))QY8Qut$>CQN>qs@d@8 z2;a2h`{Ea#VX;rVDA<3|$i^f^u0p)pKIwi$rEI_g_S;qS)LTsX=d97t<)3Ok!Fx_@ z#pLTwX4CqM&rIw5cv)r1EO%{HuBGwuVa=*R3m(1uo#f6q)l=hxS?D&qy}L66JXEJt zK3FnID^8936K9TGMLVBtLjSCM(>)h>S>OGd4`;#xc>gfFbXj@rCNxnzR z`Zo`a8_izrU6Y<`SH93lt8QBBKZd!Y`S*B_2bI2g`|U6D!Ba^KJUvYNPo(U(_ANaT z=cl%XAt>iZq{E$UolD*o7QXwHedt)TqJ7<;iXR^opRZi}dj8YHk8AE;)sNhy}I3C#DC}0mnXkgUOV<7 z_h07Z30Y;l|X)`y?Yq)yjd85C{8%1cfI$@sF;fS`++5fgxoMTqsqpp^+lDB)AFFUWP|F`+A zQmh#~F+~|CG726qRjf{`^EjtumZ0{m=I_MYj?Po{cXwQTbBu$@+1dF}=IpKz^;CQB zGgW3Q4j=iJWqVk7kM7r)gyu*;w}UIXWuLWX={|hU{DPzY`P${CQe4bp|3184+O^7P zMVDcI)xYK8_wy4kmYBAE%`oQS?)T+BuYJzo?OT?;%l1xx^7nL)FYob1Cp}LlKYUze zH6?tS-5;rr*4JCET)%8|Y?At)UGpai`*Zq#kh^-7PwdS*b$gvC_O0_kr$H>LJ=OJh z%2%20v<11G@u3stYZjV+dTjVLZ5G>HUCBkYzdtvA(0^9@k@K@k7N^2*f5DLJEM?F0 zTuek)wmoA1Z(v??hcR}x@5Ez?&q57dW<3B_9W+B)2S2ONO78|$( zPh!?Nys#p98ndDP*@FUWm$xyRJv%ONcFT0((;4fM^&kGJoiWj4B3sLwjiy$!xFP@&|dbFx|Pios6PD!SQyINh(7m3=$9P{%CerK|B(L3#pemCOJ1d4@k zmbY+rauH_nG-YZOaF^N}q93*8#hIDLi}YCS71OrQa8WXHT>DXJL44=t+NZ~y7EjM| z)>@>;%B1LKQnk`eL4XI#(gFC`8f2Y;k>kLSI{3TVY}0 z-{0TU?bOuNva++6FI%?BCM_jp#p>11pFTB|7ZnvfnPSxGa;SL8e^wC@k%+VoSyNNf zo1$!DQ=jT49KUfR;?`SZ8DC%Dne}?NF5bI0?=Jtl$Bz&1+~iWB@`pufq8LZS)D6~7 zE7K}l_$A+3)c>n#Q?4<|JU-Jn-AHDkM}ftwSFegJxFpZDKCDsxV$bw<;?L)c>^J>B zXc<14qaQkTv~j+8r^)=x6Td|p4=CuIXXrG!pP8_lv#~)zSJzFU(#=F>$2ltuRSQ=?7tGajB=_w8f-)a4GP(_7-TnS)=fOTX=- zwIznDIQ(?v+||w9#g|E^H|9h)0zMHjnE|wEv2>{$8n_r3B+^9_;yncv!3=X;j%*4Zl6}g#5~8@2dkRxYPjB*UdU94=L#3w z_VV}nl~=YO`{&%Fo_qT}*Du+Ios#kLsfoXhF0bI_*%@v>(S{ow8)>?4Cw}<$Zs}zH z`}MDH6uZ>?>8sl4sm1?qdB3Ii6ZIL_&u5))H`l+c#a=$G@_WYptsBH^H19nZKR@r} zwH>?pWv?}@lXqBms`lHP$X@qVPd6yuEl_j$APaGKZF=XnY3Z-}Bm=U9tJnAc+;Dxu zyOcop)!g^j>ol(WEEfCRrDpy70Cu$&zRZ>K()TqZJ}s`e+s-3>Y}tOlNqLqzHw>6K zrJa6F=Vw*0jsH@n4qi5-Ht1e&;2n#3lGrX?N@Ce|W#YX5xj$drNuH2$cj(zfPe!K~enUM3q%jq@{W-F5lS?p#}@I`)SkOWrgJyCt35D0Vn#m80!M zu}@Iv3yO)gt-ZK(;qGS}{l2vq*9kVC5wn$23%B3p^KJKK{`G8W3~NuH+{qJJ=;P5d z>C2Z}tEW6~D*Jlw@6Vb4`ePpdb62}=ecVvdF@^2z+sRL!J`82J@=WKu%z+OolR~nd zNFK_X@-Vk5b^kXb`FMG5d-=2dmpa5fyt@U%!oqkIf9L4TpB5~W_%P8SucG3^u3g2V zhSM&8PdazNnEhHjpZS!d(rYp1W*@4tJ~ z#xLz{Gq;u7!q)!abryFeAAGLN+0Gn4>HXR_bJL_P_r~tpmg(`1U4KSG)M`HC;5wgB z#_sCv=5-VEmb37vy_bBq?Nsp&nVh3*DlJp&*PM9d@IFUcJa;AYvRQtDiqc8#GA66u zC6xU;{`DmDzwd9)o?9rx)dEQ;*IC#~q#g~$AS~vE$-(@h8*qC?k=*op+uJ58(M&5~kxZmzl2IG%{S(g91 zxHgsa2LD^_KCSM$=f{(UbGo4(dC0sWF)5t)dON3N{LIJVbsU%G)+&EJyR=Zp=<$>Z z9GB+G$jBHhZ(dtfA-Q46Z2CNPhU%Gtx_MH21 zg5Pgu1@JLDRyvw__)pKjwlB7uO|(TNlykQFlSyaR$n0LQYMQ7a7Z=wn&;kdJ_}5;& z0imJd+rQP%54-)zNx47Te{USClT6OEd#f0xF5Q1}d9?AJC+9a`jkAB^aMt|9nd|3u ze?K|6P5gPC2q;T!p0+J;N*~LnrGaxbwZaplQUzmYZ_`ZmpHP@0+IlexI&)Z2km=PuDhE2e~}`wEpx5 zjkBk3dhcgit?9aSUz!K6Y)!bqchi@7v%h`3^n1eDz13Bzr=+Hd*)8iAu>16A!5dGqnWuJxGOBpq<{M;J4 z(zw3brt`(kx-H?qnnPu}pwY3s%APCErl~Y%<+=OZ|F@+EbIuVGTC!@(7LkA@4t1B; zzUO(a<>h^Ph0@(s=co9`*{G;+dAhl=O-Z%7?c)_PU!>&P;(uR6{nD2%NnIebpUXDk zWzDU(84rxQ_wj0bZnmGVdH8qo^!c*f{})eAzcI1x_(k@6{mXvbNN{&~#tW79-?j#xJlC4UC^+Z0_@qq3bP*y1H%~UU+-2 zyjxwpdh@#nE4R5hYy%w#`D^m8&3tQmjAw2$S-Dqc#`BkJ^_$HV1vyT0rhim!TXV5j zw%+$^wa9dy2Vd75|GeGq^_%wgf2EeztR9Ozly!C846! zmp|FjJ@2TNwjXDwV}zeYw|rpKH^b_GM|rIBSr^qE{M7o@F61Q7ugY}sHO=q)_4kGt zY?HeqmhZGkV4Co`NA5kjYF4us1ur?U|LOKy>c(GfEas^Gugoahnf>HZ8NXRn-j0tm z`CSE#LG71!_xJa|{qK)8;4cwzDZ-Z5{sg;K#tzpQp)R`y=yw z`PR?DQ^Wl;YdAXt#fn&cW@~-@;PLFlQ@?5nzgye3@&A&2a3}Z7LjCU&U*(RsuxRdI z|KNA4Rn*-#pYPT8Yaj54+><_?|Bk>+DQ?;1{yMW4&xC$2t;>|Q2NmH_ghKTcH!&8uhDa9x;NbXfKN z#_xbkorna76vtiE)AI>**4^F4w zva#^8`DIx+QF3#LTbr2Ox*vZRy?%CRi*Nj2Q88=C>Vxg`Dqn8z^|0aSg*|?Iqx}ERC~kJHWMX2HC|SHB;o2qlrmstvPQ54cUwhBnJ7j?1as zEPTuBTy6RCb4<3k*ZTi47R`5B6rp*ACv-i-gCfnDbzfOQYrjf3t@tK;I3!x_RFOz` zYC6FqQGR#F+e7=Kre+gE#j z4e}~IRZ%r3W|HBa(EH0RzpA+|Tej@h&6|5G0@GhRyDm-RlzaOy`G(3=G-<6c2PxL!N%(E+|S1RU;W-(XJ_yW>vMURm)P=|Pm9X)sLo;j zruJ`j=IYCVAIoM(|I#^s^6l)?hJ4?%va{{$|H)i9S>~K#q}O-ar0S1Bb|yECrqmnCc{#CN%zW%etlP6!kxjFs$+1bZy_7pum_2S}Ud7Fv@pE&Nf z{rdIm$?BvE>M&(F<$T#)kpuu(Y6--$bgUiME`_s@Iq{s7~WrJ%lPW~NK9 zi@^P>M?fLU(aDsV>7v}W{3Ww5i0>=NStqWb3+~B!D+>s@PQG^O=g*(K=bya4zrW^W zJF}~+>m2=q4|VQ$hX)28)H~e7(dU#$W$c42u9zA;0%gbxltXVfssY?|W7Oq^mQdLz|M_7pY^yUQ%7@lQj zWvyDU;6Th3^Za`&R<1OR*|=qkiII_z^-}Lv{ilNR6Q;R6P_?tG`~B_hg^eXIgVIiJ z*>>z(8Iz`_CeQK(9}?0ZPKDck48M9}{ z@2$Fe{(S#Wk+qg*FDM*uSg&GiY#bgQZYSne;}9ivka@OwzFplPi_^g~KCrNDx_3gV z?e&WnAIjC(YC2b}UcGs?_=WBH@oQ4g@uV@Oet&njMyKaT!xob!_JrHgc7|`x9olAY z&Di{~CZ{y{fH=$PFmtvX!GamAEVu9d(e3%6uq;Yy^S5gsYQ%J-T4r8%`oKMHZ641G zGfR~tI@3P-q-I{-E>Whw_!+v;zBzIZ;`Rwb;mYjxO3u5wnV zUsqQj+B1hmIW9`-TOZ5D0Krq6H*fyDDs=S)A?}tV%6d_|wrnZ6u_4i?jF3q2i8oGPNpFaYbt2JafpIGq~d+e(35n5>xux@GdmNi}PH|G~$`TKg?^0~#n zAJ2ZACv0}AOlIOAwK>z4FI}oC^78C#^W@K}0vw$#U!tsxjD)nF|9X7sssFRR*4sbb zvz~A0aBahu4~8p#QqsY0JtE3i-K z{ukEttmNpGt5-|Q%jM1U?%ds7{xhh8WxvMx(~spi8<_>OGgr({WUjusDfLUEtNENi z&*#>Cy3xX8m!ufK{>^NoPc9Wljc?psmysOyWvAM*>iuH>wRJyL@y>7S{$d$()+*E5 z>w9A2`71lLdBQ^i0vbN$WM`kgKhwvTE8BHt2iK{vh3mbwzk1G(=Chsu`<4AC{rkJx zCVf5Qy+8NC?Qd69ma3MYPPjZ%#eUx=i%HJ&l6Gc!@pK=N70k}`_4RdjK0MdDJnznq ziDDMnuBkgDXS{h_`YGJkeidj%9wW!}*12zwFZi|k>e59UBu^RKdHh!X$DInPRTCDS zTdES;C}tFIr}}#0fv3k)9r=YBe^~$e@SR=T>QcPmHV#m7J?rKs;8IqRdNTRU&oiZN zmuBgyeuzD(^{8p~QEshy>nbf%_A5S~cECc=J5W5^<^J?{_xIc9@LxA=TRl0|W{)P{ zQO?=E->&?=6fekJy2k6d;L7=nHZZ(O`)I^nGruvdwkp;nR7P&|t3#_58F=(~GuD;` z1Xe#;z9ajKYVcv`x5|t%3439IbH~rUtcJ=>?zVoJvw)29wSG8>7d;Dd_ zPUAGSlK)GO3RTCD2^s$Rv5|vYZq*3% zKgGdyA?NS>Un;ZfXS~ZrMX{(^Y*W(zo%?dAD#`Sh{6Tdm?rf_|tL4*^`<_h`uR0Jm zIep1ob+HRuS6y0X_Upsnn!L%AlfPs?`+wUiC{AL_vHAllSsA4h<6ph_`sVWP@7Wut z#+X&y>)d_q%06j^OSjG)?X3P8?G>n~2eRk6Yq!|c>C-=JJ2Rg3aPHu{^kMsPh6u)3 z_OPn8w{A?j_Je)02A{&xKZe?K>>Att@W1*$_3W*!>2wn9>mJLn47zKk89Tcb=Th4?Vr2MXwtHEimP{* z+JAUzEVew%qburL+_YcEzR&1=cV)wtcbB#nN}V}-=lA2^8GE&|zJEFFy}mQ|mDK+n zvAEbjWjog|^J=^{W6SmM1kRO-Mg2YJk4(2tlx3+no;p?a60iPD35BvKsl(^a^;N6L zC{#YPHd+-cl$6f;FyeMt)m2Tce|x%k3VJ;r7b`9?eYfX-t7WOA)c-H9kEdkc{&Ti! z&uNV{oFSWv7oPj_{+UzdyO&lRLQNJ~nJpPBwRESZrQUC6+qox(Ek1hdBzsk-4-GT2 zGFv!SKGV1q>YBONtxfgmuJ}jO`kQ`kEVK&C>|}IbTYV$`@3qK9Rcjt`U%JF`=ZZTYGzP^8OaInKnW!+u}4ry3Bh{b8qgcRMsCr)21In2s@+_}78Z`-=Ly)BkO=Y0Jo zq*GRget-FR*Xt8sruI+En5eZs_TrV|g595Mzg~DIT)oBjYTOKwTif3nJ@wgtl3~gT zjdQDXICCp*3UDwgZj9RX>-x!z$;qy%%TLzF*J-aVU!_v_|LDAQvx_Hgrq|ZO`ok>kW%%`tjnHU=EeP@6C3Gu7* zjN4Opgnai5U4Q<;nU;Is6c}%F@|;ZNnOk|5QC(>DQto$dXXft}u<|&|e%)9$Z0eQb zOADvOXTLf+Eh{!#y!p?!eFJAc6PFQMz4Yp_=yx^W&R^ajsnOTDcFFCOm|e%W$6t)v8~*24 z!HiI=NoB_`Zc6)SL|IQ_d9S`(f+h;yQDN@r>U{6jAMMz^X?;~aQNn& z1DXMe93eqLLiLL|E?&GiBUU_D>&x<_q@)esMS2eJ2^HmOIlexA|DkZ6_TDuDhuo94 znxuf&VVlmbYM&NzdA`z0N&CAV&#%TrMD5ye>~dXT+(V@^mCAGtN9Zq}DcbrleT=%@$kThZh&SPxR;(6-IG< zJ11E#ge(ItQ7F!S)TWZ1$r8Td?6sS@=hm!R6%`Qh;O*P9HK)#=P(huWOh&y8PQx7OEu#@xe; z=61yvzx^wybE`gWyUDC=v8%p!VBNZLwU)ie-(5_fpR-WcF0K z)-g zw|r4z`I&P&*oz4izcqm_N(Ja1?>4AK)MNR63>WAE{{}S)XNB-pf8TrC7 zDk{b2<>sdADz(4A)qZ~`Te2oYSYBJz641#zxya9J$c&Pg`73aAmt}K zi#M%0E>vItpOKl3W0AojmTj*%mp{E;te^Mc}Z`dE*o*5T*XGhni zwr<&Sf}>`0^V|^e>CZQNKdpYn(Y|@pC9W@Te|}nZsY~nFR~08WPrqKpmJK$}JYTH~ z`>$|VZb&oQzsV^oDy1abw&Skm8)f0s!EMex7EdytU+*%z!&}Jp%2)cD7^hBrwDw^( z*)3JBI?lDy`qOyxZz!lwTj-InuhsMP`8zg;=BLi{@b2zulvV56#nH=OCY$+2dGiEr z=bi_Y_{(dYyIJ6cMUi<25-(Z-LdLNZ@uSKBZfdf*&>^I3ELv* zHVYSD&48~vpI-R9bN#=?=ht{flrid~eZh(r#J3}R|fiVgFP@-7yA=^we#*0f@VspG%TId{JN?~B`i zF?M6gwu7fWZ#=U9UDvcWp8ktz&8Arf%tBvu<}ElW2mM{oEfBR{x2IExSufGoMsEw{| zaX()2oxfV`M*9UjG4KFwKh>du`z_wUPRed_7q5&N~LZ29l^^HxV& z?ChVtti0;`yPwr>@|hnL@7a6)QSa$L+gQU+{poD~`)6U{&f1eVpWE~W`~1+zH(g*P^)T+qrHB~-> zFaOTdowUlK{}%82iPN6B#^@s@z1`Rn0BgvrB*`W`)|keFX?CZEqSZZdDDJ9Wau>ixJI^6jup?oDP`{I z&+g_on%Tro{%0(`ehyFd#tT^-=TELO>iPYpe&XsX+ked)y*P4rWPG0y0cx-tx~wdl zT3Nl~p~L;G=bst6crVwwt@T}0f7~}Ps%SgUK55TSkF~k|jh_aK%Wuoe=GlM$*2!$f z>em?`YJ#^Kyhu5=uVn9AyMtG{>sS2O4tW-6_J{W@XT$L+hmU>8Q0G4|V{K(+^upUu z3eCj!?|jPoi#5r^yV%J;8+1s1A^RbgKo#Wqt(z_@^Ojwj^J+y^ z?n_PGwRt~J3459QhZpX-^7IQ&Q~YULKX+k)%*A1s<)5ZH9e zKeceS&`Jvd?(_EMPu;)H;tPUxn|IGX%AFNo=cX@D#9l`&2seVRr$H))--Xc$~#rBO2foX zul*7$RBwCuPDMkH5K3zc}u(Jpd|v_IYUW z8}&7Pp1x+3nvB)e!hj3qW|xK6@4LU@iQmHa6`~rfZP%}bR=USYzdLtH&WgQdUygHD z!Wlyb4KeON=k`P-n@^W=lhs>w^~{tBj~x29*T-zpHr3V8y%edfb@=Ro6#4a&_n+X7 z-h8gz&aUO$2kY~1IqdYLT%)2=USvvs7MUMY(&@6u#&-IHz{DzscrHcjn*EAT>u*eC z+iJV|{d zFFd9{f3-gQl(l--Bt7PHo#_qQ1aJ`}Ar-@7=erDL*jJ-aPNo z`I9q3xq0_?ES=i${*unk;%|+w_gp-3DJd*wwt4=$*RQkl^Up_lduyJ3^Xb-udiU?O zD}#Q#|M%ri`GQ@)C*(@6p8j`%w7^pNPfKQ{D1>Ld{JQWwTV|%;gX`xd>g$&MUw!rR z|A@1~c{+c7PWQA}n{vYVbg#a-NzTJ#_xIPQpPN(p{G6=D4W8v1&JEy+0f()<&o^bC zoqB!W9Qld$<;Txus^6V}wZ>6Fg;ILGPXj*P02_p0TWtit>tL z3p>5~0`s(U%kTEF>1}?YJ)?+o&fM}foVPi1kMs)n?fG}Yb&2JH4^0VE{`?B|?OV2Q z`9J#?zBN(_aso@GKP>?*KVE(>C}7D_%fPtMS>N@A_b2S;f4lfq(V?rWZf`s(zw89} zrWB@Qsb4Y=W$K&PiJlcw3Vw9sK&e@UvqRJ@VbRc~k@dkJySJcBDw?EC^gI9m%3|Jw znrGYi+>`%I`?THW5yQdGb!!A(e0vNk&^-NmkNaN+PaiTbFnYiE?F(T?{yjU@BCIZ{ zYn|l(&w8SSH(E&XHz$wfmY}TjGS9;vX&zFl{eMJh+QaS1cK=UkDNI{F;pdDwNB?M^ zt0)k!d)mKci$*782ZQ9yKhGE5e7bI8_ifu789N^yOSkbna#>7h+e8k-cl*6&X55%- zX)~$5tNrS(>GCEB^nw`u)kG5|Fd{zBll#DPNKP>|irH zkK(p<;;M(k1epy#-v5!hNG3Bg@X+m9DlflrwjR|@_3D?kyze%vRHW!H|Mo|j$E9y? z680|=>U{F}@#dV!yE2a}#nwD`GB2~fH(}E!uYEfA9HOG8d`S6T{v30jR5;@%3%g(C zv537@rh7d$zxDuWtGVOKl9?H}Xx)*+N;}r?R9?z>GrCmxO4Z-G+Cx*_blz6j`|-)$ zWX&(HdVOu}m$-Fl-;c;oE8CNJc(?!cZCYL~&!k=4-7nu(Pnni}=0wOT(dE|6d&=L` zOsGq8I=?OVv9a+%+vPhN7w5FQ)p=X7|GRtbRl%d{`Rh6Vf1b1J&jTUdQ;U-gD$f^e z51n`TX-1@??pfA(IsLMn*QQSks|xFnV|o8ZQLhIne|-yj4_oE^!^rafb7pZjjc&oHxpdwslJ>Q~ zzI^&r^lQQ?t**Z+l9`%ip8x*-zP={Xn6L8RkB`ig-v%%Dv$V3hW*siLc)@}N_i7)V zZ~3r3-ZAgWxv!6F+I9AX#_cOpWzZ5k{%p>#nQphPv(H)TFY+j1^OrEz6*_-??e6aS zw$UkS7Prk?cWsf|$%hnd4&Dwr-}-E>{ruDAnmNLpJ#Xw?nY5-}P?+d3T|YjJC|XQ0zv4<{KNn_#N5)->R9?bE(G=a_rPCclZ>_4np~nFj5K zD{GD~oVPl@#=fvMZprG{B{iQvC(d4R<43@)+UMsYG%j6=@is~a_U+vH^T&N|=fxWN3AhSb?%ge5{_AwAYS`L|E2W+s-_u*q{{Oz@|7owb&*nOC zrTqTF>>Rn$$~mBIAs_66su->v58U;Bjf;2h787wf_4s4@mzXx%USqJn_wVwrxx25w zii;Cje_dz2PUxh^a=mTKmMv>-b#-xJDap4N+*)}tB5u8gj`h6wC`I;q%X_1~@EOwMIk#^!#+W+Ua7dKh$Fx6yJ6pZI;bHBw+BX`cK8# zzhYw3KklnsI^j(P^U7!i(I6)KQvTWV7pN&;kdNWgww_D zQXldbatg`G^+ldkHgol{DBE*VQeAZG^iXEaPuJ`H-rlV1OX3WZVl zpKf3FM&fO>!v3`W#Fa{Oe}DT}cYWhG*48a1;uTS5dv?Z8Uz;4A>C>7af7xq8}>|y+PmXgn!%+@F%zDqKKF`>pT)HQ*QG@Yuk&Zk z+0sRg+z#g_@THa6$x*@9XVW@-~} zCYzo(;qm{>dbR~_OD0Zx)^zwn><{J3?>n!q(9}E6zIE-p%FoXVJ0zu~q-16HR+rz6 z%HW)kUM66c6BHKa_WtSRHydY#%-WFXH(m77rI>>KcGr@PCOSTLsFL|IPT%x!82g+527gHg9EI_NOs_vR)sd zkSfIdZokj7ZjZ(9FD+_W?kWCrBH#0Bxkzi5qx;fdf7P!m_E%U}wkJ_v>b1w?gST(b zzL#ydOZ;?3@7+H02l4ZnGBX29`^`F|_uj70Epl0r)%;V91Q43|} zPdvP4jZRx_X#UZfX&%=18tRqnJ-oXoIbU1<(S2K>O6)SOCXvo7E8O=auC}&R=R3cA z%^9C#x!Jw!FFBS4ELxr)zDHO5oPw^dmkjf+2iqS=e);SuSh~G;#nQXc@3Pl?;^}xJ z6XG7}DjI(#qf7Dd7pqUI@}Ro&u7li~Spv6nn?D`(SYmKq=T(u0 zZ`raSA#>eK#y_8Y9C!UaU%#hzs%rQ0kA@6#wY(;;vX0xz2?xvzohte>PA>7#yMsS| zHBYb=QDa%P)UY`4NX8eXUiNuu{h|N&EJ_vpaetEGcfXvu)o-7x^tvV7pDugkiR7V! zZE4BJbL|c)I#*WyWM=2P@J%K0+NB@j;mh_L+?tnZ|8U*XW6zdb1?l*FK0nX)_pPnj zM^4>Taj|+I&-U-c>2~=OK8t;J$L>hDm@)V18!|)eX5AGwzgBhkLrkih zLSkj*&myg&qM|RG&N9q9IyERZEblGTr1+DoCq&GCW?%o$=Jc757gSt+%{a^cXJYh< zD_bvZo_h61yp-akKh}LSJc75BJQh!vm!E$r=-5fKS5K$@{=WIH_{;P2?RBHJJeak4 z*DkB>eJ1+d+LM`O=K1u@@lMK}C7+q87cpl_e?-JFTe+#S>y@Lmw@DaO1-$v&!tdHs zY!IQeZFc^H`*->qH+?!KAIVxXlUrBILq*8j^_SJh=MJ&kRv)zFsoJ@F_htQcDU9!q zPQAMSoI9`e-U#qVTSg;>GK6|f9y^y zRNegH^OhY&g}yeL)0X|5IPIC!q4Vr)x27%9bCtEZ`LS*StNawLrQ2MV8TKhz*|?o~ zGq2!HMfh_2-Jev$U*6ruE}CHS{#pIctdKc9G3g!yV|HLl@Vp`|w|EXlTd%|rP`A8&u$_AXEIzvZMGq3ibA zAG$17|3%_e`7^`T3vVjIU-L%^&i5$z^>@q0+D_FJ22fk^@`oF9Rc}SjTUk4$am$td zd#^v+y)k{_%qf*{+nt!HlMJzC<~@ujD6_6PTG{BP?oPJb12k2l!e zpY?6C^3Hd&S;MsyFF3AMf4HUP?7L3OmZy@pk6Vry3YF|F@U2pdzBk|7c*hzEd z%n4f?Re0jFf#?g1q9+`u-`|5Q0XTA_Pkq(rud)As=es|Uf3R9|8>7#+kKfKKxvyUt zuxge0q4FJ%X3m-wBCQkmp%~t)< z?fH;ssjKqis^%>F6`dUm-aN+9J-nVy5Z(MiTuZ(p;(ep{d)wbf5 zXKrk-fBl}-n!$tZR2qCvY4<&j`ENG-`2$}7V0w6?hj($}bJ^FukDXm(xda^|<}uz@ zz1-{NyZO<#jjTt6>i+x?^fEeox-!QnZNl%O&1+QU5)Lc;SK5A8WFi}M`sh64SGOM< zGM8x0bzf4qDmt=di;4K8O;WqUiD4jk-7SBp>q3IVq6(P2z)3Q+vXzH-@)3?jI5oGZvmO?b)H7H|4ZI^tLwqClp@!y*Re8SQh$EDjom^?W) zhuPcPllNtwP0d`REQQ5$FFcy`X=|K0$8$}e8#h0y8I{i!W%{cJYHIr3P~FILJpayY z<5y>Ib=~Fv_h*#OUYZsDIBD5$)qQtZFI`%s@;ZN&+vjtO^jbM)Sf{W* z-15h0w|d@(*K1V*boZv7o+hmBC*XZs#pTxHb0zoA^m}r>l{UDV^1E}z+3sVXHX0-~ zZM&Jb&!aRUVZpwAbu~3Me>b}K%iZ5s`})dC;m;cvG+aLBylT~|FB3n`NK9~_Ebwxf z=J|$OAM7%|c11+}_WZlkJ*(xz>wO!}uJ-lymF%lIcz zX@d>(t4W!5(_Ct9?5t^8(X9|6wb$eE&mgO9zwfNspSf}6!)aA|58v-8H3jd{+x+9> zuj6awE9YxG-KXYzC;E?@ z%*y6}V<6c5O;h_@@U`M7ldoyH%Jx5;@`Ju@K5`~KVq3G7Ps`gxow|@o^XK$51&Ujr znLX*Z*~90|IR@QAE!ocU(so7YXONpTvz zH}*|7X|~f@?0EB7*`mabv|Fb;@7)eQ%mCWn7t}uUYx0R{W`2E@3we@{eUV($`^u{M z9`}8Y2=$qYQn?KBpR-Kf`$WuB44B6&+qZ6ue4cfjyj9YlX}e_ief{cw?&;pm3(Wr< zn8SZEQo1(paKVexEiyd+H)KTDzCU)Xes^sSqU7j24xL+DHr9rpm-6f` zdwx?~M<%Y%j=f@`x%}mgG7nnoQbY^RGRASF8KnmcJe!-eey>8f;(sVzt#SRo~tca7>2h;x;|W<3^UnCOT@ma}Kmh6eURJUOglD zq5q+i5D)qeIj2i5yV}>}9R2iLGw$${C$rXe|6Tt2;z8T=rpnXX#kpnb-+Se6Us$rX zv*-0S?eO%!e|K6(S8u+yWP)Mn&Uu%l@?t;hT~NPc8E~mi(nZ+ulU$7CC&THlM2%&| zd|4LU`sh^{xmBLWmdW8Mclg(Zo3H)N-|&B1bMo~($--yBySwXsPb!Me|NSL;=emq+ zg;*z+MH`hWm3Y5JFa5RV?zX#WZxRE(pOuQ9JZnoeN2d7;v(HUV@|!EV*S-%;YxCXp zsQk~DUAofSa+dmcIGBW~1T0xz^>*qt&eA0{%T-v&a;kBxlA@2I4%44Me@1W5 zYvs$&&#x#cEL41OZ*TSXrlv!`zP>(v`t--$+jDLP1q3jxU$<=8vDMYr*TqIgMy^}8 zZq}?>KkjOWue);X+P6=ietml@UH5%s^6^QNCW-0Cty#VL^y=SlZf^eg@niAxb8qkO zx3Am2cJ126M#qGN2bjMd%s+6X6DJ&)@EjA8X6flHYApn zmHoK;>({T(&(3ytcYpr={rT0qi{1Iv)t4VRa^&&x{>SCZmo1aCsn`&_+;4T*+DBLC zPMo+g{k$A6FR!_|`JcSd(5s-s{FRQap1r~2lUCb|*)t@P^5TvP6mJ$WPft((kv)6H z431Z|$Jv%F7fj-vvYk)XYE9&3w$Dk6{Lbtxe;+sR&WEWkH+bV)Tkd{vJuq2%-y_c5 z3$qm8Y+LMfrZ+SGPoA*WkvnW59Xo#qD?V#Gt8nXa`*Gf{jEUw~v|H{5%oi+UKG?l* zh0CO+JLcNg%iXgRu=4O%J|b1c2}%coM%CYPuFQ8+kSHuHT=?E!S%9Nug+<|`mR-Ac zeOTZ9#l&fiO8qx6UGH-j#wEv%fpTG0SOY-S8S*xs7Cnk103kmzMJ7nS3RW|22Pj~;D za&l{ip8JStM8z285(cbCbB4yJuLcF%7+ zdv#LM^i6;A^AaUzTK@R9>0kKrdj46*`~t&O-^ykdRo}UI_uAV{?<>~Xhwij`v?!Us zGN7zFb3OO7`#02G6r`9zuH3+oUcXCAIjjG4+u`f>(^hQ>3ZAm=*Z1e;8@=jRZ~gsk zy~Mq)wN^>qH@8~O-SJ}TqaTyCr@h_#*Viq5%i(KxS0CRp`RL4mijegB-z%*y)TWeg zwKANZeKmc}QG=aWF)XWf8G&~(7zQGmKN{0OYcOl3~is++;hC@*zw|=-4jIEbM{n+FP*95yyH*HVtMz| zg&9pJzT6l3z2wjN_U$j3e{F4lFDCgmvQa+0v1?7Z(QmQWi>LE0n%E3;=2P|ewpHi1 z1n<`U+sjsZJ*fTf%Sn6Y&!1;sb-(7_&W@8ZU%K{`2E_eYcr*HR%=z`w9@k5klyzIi zU99>3Payg1xp4pembq7*4qs6}x5dV?;^=}MD<=GR)p>KNj`cHV>&fHxKW1&zU;aT@N#N1> zRiUf5Y~9*g56Kzp{+0av`;_^v^}T;WH)D61t+;t(@qByrw~`mnr@nW8Ke2~-$0Bcy z7>~0$erm?$-A4@rb9>Ye&eqO9TNd2E9MLj8c4NML#?*6hh8*?_Cn(x1Wc2tohmFNOpkAsmfpOix zEoT~J3j-&v|UB2uY}W?7f@r*E=aYH-(UjS!dg%57=v$&a4d^?06| zoVnV*Xi1)T+`X1hwc?feH4?&tKbrN{{+UudGuR@2OY_~uAQk zb;;IetWSp6#NN&JyZX|fy>|ukk5EbeJ?44VqCd5s`fpwJ?ZL_JZEs>S*LZ$E9Q3&| zOZwy0{7pr^0iIKWzjxL->pf|?rJ0!eW6$}DAMfhD-`V`mUr=&QGk7j&<+H)gT~Z!_ z#|@qwm-pZMaD|9VxQfEk6&K4VeQrFwx_$ML3tb`K%AUVC+5g;b*G8B2O9kuvzHc|O zxS{^#WuIbKOT8i})kQR{wvYY$`f0DY{puG^9t(_K&)mPmvp>Jr(m1z4Bt(Du^H*