From dcb8c994cc09d6aa31b88d8aca47573cec708a2b Mon Sep 17 00:00:00 2001 From: Anton Viktorov Date: Fri, 9 Jan 2026 02:24:01 +0100 Subject: [PATCH] [ac_dimmer] Added support for ESP-IDF (5+) (#7072) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/ac_dimmer/ac_dimmer.cpp | 35 ++-- esphome/components/ac_dimmer/ac_dimmer.h | 10 +- .../components/ac_dimmer/hw_timer_esp_idf.cpp | 152 ++++++++++++++++++ .../components/ac_dimmer/hw_timer_esp_idf.h | 17 ++ esphome/components/ac_dimmer/output.py | 1 - .../components/ac_dimmer/test.esp32-ard.yaml | 5 - .../components/ac_dimmer/test.esp32-idf.yaml | 5 + 7 files changed, 195 insertions(+), 30 deletions(-) create mode 100644 esphome/components/ac_dimmer/hw_timer_esp_idf.cpp create mode 100644 esphome/components/ac_dimmer/hw_timer_esp_idf.h delete mode 100644 tests/components/ac_dimmer/test.esp32-ard.yaml create mode 100644 tests/components/ac_dimmer/test.esp32-idf.yaml diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp index 04c01948c8..1e850a18fe 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.cpp +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -1,5 +1,3 @@ -#ifdef USE_ARDUINO - #include "ac_dimmer.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -9,12 +7,12 @@ #ifdef USE_ESP8266 #include #endif -#ifdef USE_ESP32_FRAMEWORK_ARDUINO -#include + +#ifdef USE_ESP32 +#include "hw_timer_esp_idf.h" #endif -namespace esphome { -namespace ac_dimmer { +namespace esphome::ac_dimmer { static const char *const TAG = "ac_dimmer"; @@ -27,7 +25,14 @@ static AcDimmerDataStore *all_dimmers[32]; // NOLINT(cppcoreguidelines-avoid-no /// However other factors like gate driver propagation time /// are also considered and a really low value is not important /// See also: https://github.com/esphome/issues/issues/1632 -static const uint32_t GATE_ENABLE_TIME = 50; +static constexpr uint32_t GATE_ENABLE_TIME = 50; + +#ifdef USE_ESP32 +/// Timer frequency in Hz (1 MHz = 1µs resolution) +static constexpr uint32_t TIMER_FREQUENCY_HZ = 1000000; +/// Timer interrupt interval in microseconds +static constexpr uint64_t TIMER_INTERVAL_US = 50; +#endif /// Function called from timer interrupt /// Input is current time in microseconds (micros()) @@ -154,7 +159,7 @@ void IRAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) { #ifdef USE_ESP32 // ESP32 implementation, uses basically the same code but needs to wrap // timer_interrupt() function to auto-reschedule -static hw_timer_t *dimmer_timer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static HWTimer *dimmer_timer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void IRAM_ATTR HOT AcDimmerDataStore::s_timer_intr() { timer_interrupt(); } #endif @@ -194,15 +199,15 @@ void AcDimmer::setup() { setTimer1Callback(&timer_interrupt); #endif #ifdef USE_ESP32 - // timer frequency of 1mhz - dimmer_timer = timerBegin(1000000); - timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr); + dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ); + timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr); // For ESP32, we can't use dynamic interval calculation because the timerX functions // are not callable from ISR (placed in flash storage). // Here we just use an interrupt firing every 50 µs. - timerAlarm(dimmer_timer, 50, true, 0); + timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0); #endif } + void AcDimmer::write_state(float state) { state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation auto new_value = static_cast(roundf(state * 65535)); @@ -210,6 +215,7 @@ void AcDimmer::write_state(float state) { this->store_.init_cycle = this->init_with_half_cycle_; this->store_.value = new_value; } + void AcDimmer::dump_config() { ESP_LOGCONFIG(TAG, "AcDimmer:\n" @@ -230,7 +236,4 @@ void AcDimmer::dump_config() { ESP_LOGV(TAG, " Estimated Frequency: %.3fHz", 1e6f / this->store_.cycle_time_us / 2); } -} // namespace ac_dimmer -} // namespace esphome - -#endif // USE_ARDUINO +} // namespace esphome::ac_dimmer diff --git a/esphome/components/ac_dimmer/ac_dimmer.h b/esphome/components/ac_dimmer/ac_dimmer.h index fd1bbc28db..ca2a19210a 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.h +++ b/esphome/components/ac_dimmer/ac_dimmer.h @@ -1,13 +1,10 @@ #pragma once -#ifdef USE_ARDUINO - #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/components/output/float_output.h" -namespace esphome { -namespace ac_dimmer { +namespace esphome::ac_dimmer { enum DimMethod { DIM_METHOD_LEADING_PULSE = 0, DIM_METHOD_LEADING, DIM_METHOD_TRAILING }; @@ -64,7 +61,4 @@ class AcDimmer : public output::FloatOutput, public Component { DimMethod method_; }; -} // namespace ac_dimmer -} // namespace esphome - -#endif // USE_ARDUINO +} // namespace esphome::ac_dimmer diff --git a/esphome/components/ac_dimmer/hw_timer_esp_idf.cpp b/esphome/components/ac_dimmer/hw_timer_esp_idf.cpp new file mode 100644 index 0000000000..543b476085 --- /dev/null +++ b/esphome/components/ac_dimmer/hw_timer_esp_idf.cpp @@ -0,0 +1,152 @@ +#ifdef USE_ESP32 + +#include "hw_timer_esp_idf.h" + +#include "freertos/FreeRTOS.h" +#include "esphome/core/log.h" + +#include "driver/gptimer.h" +#include "esp_clk_tree.h" +#include "soc/clk_tree_defs.h" + +static const char *const TAG = "hw_timer_esp_idf"; + +namespace esphome::ac_dimmer { + +// GPTimer divider constraints from ESP-IDF documentation +static constexpr uint32_t GPTIMER_DIVIDER_MIN = 2; +static constexpr uint32_t GPTIMER_DIVIDER_MAX = 65536; + +using voidFuncPtr = void (*)(); +using voidFuncPtrArg = void (*)(void *); + +struct InterruptConfigT { + voidFuncPtr fn{nullptr}; + void *arg{nullptr}; +}; + +struct HWTimer { + gptimer_handle_t timer_handle{nullptr}; + InterruptConfigT interrupt_handle{}; + bool timer_started{false}; +}; + +HWTimer *timer_begin(uint32_t frequency) { + esp_err_t err = ESP_OK; + uint32_t counter_src_hz = 0; + uint32_t divider = 0; + soc_module_clk_t clk; + for (auto clk_candidate : SOC_GPTIMER_CLKS) { + clk = clk_candidate; + esp_clk_tree_src_get_freq_hz(clk, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &counter_src_hz); + divider = counter_src_hz / frequency; + if ((divider >= GPTIMER_DIVIDER_MIN) && (divider <= GPTIMER_DIVIDER_MAX)) { + break; + } else { + divider = 0; + } + } + + if (divider == 0) { + ESP_LOGE(TAG, "Resolution not possible; aborting"); + return nullptr; + } + + gptimer_config_t config = { + .clk_src = static_cast(clk), + .direction = GPTIMER_COUNT_UP, + .resolution_hz = frequency, + .flags = {.intr_shared = true}, + }; + + HWTimer *timer = new HWTimer(); + + err = gptimer_new_timer(&config, &timer->timer_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "GPTimer creation failed; error %d", err); + delete timer; + return nullptr; + } + + err = gptimer_enable(timer->timer_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "GPTimer enable failed; error %d", err); + gptimer_del_timer(timer->timer_handle); + delete timer; + return nullptr; + } + + err = gptimer_start(timer->timer_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "GPTimer start failed; error %d", err); + gptimer_disable(timer->timer_handle); + gptimer_del_timer(timer->timer_handle); + delete timer; + return nullptr; + } + + timer->timer_started = true; + return timer; +} + +bool IRAM_ATTR timer_fn_wrapper(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *args) { + auto *isr = static_cast(args); + if (isr->fn) { + if (isr->arg) { + reinterpret_cast(isr->fn)(isr->arg); + } else { + isr->fn(); + } + } + // Return false to indicate that no higher-priority task was woken and no context switch is requested. + return false; +} + +static void timer_attach_interrupt_functional_arg(HWTimer *timer, void (*user_func)(void *), void *arg) { + if (timer == nullptr) { + ESP_LOGE(TAG, "Timer handle is nullptr"); + return; + } + gptimer_event_callbacks_t cbs = { + .on_alarm = timer_fn_wrapper, + }; + + timer->interrupt_handle.fn = reinterpret_cast(user_func); + timer->interrupt_handle.arg = arg; + + if (timer->timer_started) { + gptimer_stop(timer->timer_handle); + } + gptimer_disable(timer->timer_handle); + esp_err_t err = gptimer_register_event_callbacks(timer->timer_handle, &cbs, &timer->interrupt_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Timer Attach Interrupt failed; error %d", err); + } + gptimer_enable(timer->timer_handle); + if (timer->timer_started) { + gptimer_start(timer->timer_handle); + } +} + +void timer_attach_interrupt(HWTimer *timer, voidFuncPtr user_func) { + timer_attach_interrupt_functional_arg(timer, reinterpret_cast(user_func), nullptr); +} + +void timer_alarm(HWTimer *timer, uint64_t alarm_value, bool autoreload, uint64_t reload_count) { + if (timer == nullptr) { + ESP_LOGE(TAG, "Timer handle is nullptr"); + return; + } + gptimer_alarm_config_t alarm_cfg = { + .alarm_count = alarm_value, + .reload_count = reload_count, + .flags = {.auto_reload_on_alarm = autoreload}, + }; + esp_err_t err = gptimer_set_alarm_action(timer->timer_handle, &alarm_cfg); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Timer Alarm Write failed; error %d", err); + } +} + +} // namespace esphome::ac_dimmer +#endif diff --git a/esphome/components/ac_dimmer/hw_timer_esp_idf.h b/esphome/components/ac_dimmer/hw_timer_esp_idf.h new file mode 100644 index 0000000000..1b2401ebda --- /dev/null +++ b/esphome/components/ac_dimmer/hw_timer_esp_idf.h @@ -0,0 +1,17 @@ +#pragma once +#ifdef USE_ESP32 + +#include "driver/gptimer_types.h" + +namespace esphome::ac_dimmer { + +struct HWTimer; + +HWTimer *timer_begin(uint32_t frequency); + +void timer_attach_interrupt(HWTimer *timer, void (*user_func)()); +void timer_alarm(HWTimer *timer, uint64_t alarm_value, bool autoreload, uint64_t reload_count); + +} // namespace esphome::ac_dimmer + +#endif diff --git a/esphome/components/ac_dimmer/output.py b/esphome/components/ac_dimmer/output.py index 9f9afb6d80..efc24b65e7 100644 --- a/esphome/components/ac_dimmer/output.py +++ b/esphome/components/ac_dimmer/output.py @@ -32,7 +32,6 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), - cv.only_with_arduino, ) diff --git a/tests/components/ac_dimmer/test.esp32-ard.yaml b/tests/components/ac_dimmer/test.esp32-ard.yaml deleted file mode 100644 index eaa4901f03..0000000000 --- a/tests/components/ac_dimmer/test.esp32-ard.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - gate_pin: GPIO4 - zero_cross_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/ac_dimmer/test.esp32-idf.yaml b/tests/components/ac_dimmer/test.esp32-idf.yaml new file mode 100644 index 0000000000..3ec069f430 --- /dev/null +++ b/tests/components/ac_dimmer/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + gate_pin: GPIO18 + zero_cross_pin: GPIO19 + +<<: !include common.yaml