ESPHome 2026.2.1
Loading...
Searching...
No Matches
ac_dimmer.cpp
Go to the documentation of this file.
1#include "ac_dimmer.h"
3#include "esphome/core/log.h"
4#include <cmath>
5#include <numbers>
6
7#ifdef USE_ESP8266
8#include <core_esp8266_waveform.h>
9#endif
10
11#ifdef USE_ESP32
12#include "hw_timer_esp_idf.h"
13#endif
14
16
17static const char *const TAG = "ac_dimmer";
18
19// Global array to store dimmer objects
20static AcDimmerDataStore *all_dimmers[32]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
21
28static constexpr uint32_t GATE_ENABLE_TIME = 50;
29
30#ifdef USE_ESP32
32static constexpr uint32_t TIMER_FREQUENCY_HZ = 1000000;
34static constexpr uint64_t TIMER_INTERVAL_US = 50;
35#endif
36
40uint32_t IRAM_ATTR HOT AcDimmerDataStore::timer_intr(uint32_t now) {
41 // If no ZC signal received yet.
42 if (this->crossed_zero_at == 0)
43 return 0;
44
45 uint32_t time_since_zc = now - this->crossed_zero_at;
46 if (this->value == 65535 || this->value == 0) {
47 return 0;
48 }
49
50 if (this->enable_time_us != 0 && time_since_zc >= this->enable_time_us) {
51 this->enable_time_us = 0;
52 this->gate_pin.digital_write(true);
53 // Prevent too short pulses
54 this->disable_time_us = std::max(this->disable_time_us, time_since_zc + GATE_ENABLE_TIME);
55 }
56 if (this->disable_time_us != 0 && time_since_zc >= this->disable_time_us) {
57 this->disable_time_us = 0;
58 this->gate_pin.digital_write(false);
59 }
60
61 if (time_since_zc < this->enable_time_us) {
62 // Next event is enable, return time until that event
63 return this->enable_time_us - time_since_zc;
64 } else if (time_since_zc < disable_time_us) {
65 // Next event is disable, return time until that event
66 return this->disable_time_us - time_since_zc;
67 }
68
69 if (time_since_zc >= this->cycle_time_us) {
70 // Already past last cycle time, schedule next call shortly
71 return 100;
72 }
73
74 return this->cycle_time_us - time_since_zc;
75}
76
78uint32_t IRAM_ATTR HOT timer_interrupt() {
79 // run at least with 1kHz
80 uint32_t min_dt_us = 1000;
81 uint32_t now = micros();
82 for (auto *dimmer : all_dimmers) {
83 if (dimmer == nullptr) {
84 // no more dimmers
85 break;
86 }
87 uint32_t res = dimmer->timer_intr(now);
88 if (res != 0 && res < min_dt_us)
89 min_dt_us = res;
90 }
91 // return time until next timer1 interrupt in µs
92 return min_dt_us;
93}
94
96void IRAM_ATTR HOT AcDimmerDataStore::gpio_intr() {
97 uint32_t prev_crossed = this->crossed_zero_at;
98
99 // 50Hz mains frequency should give a half cycle of 10ms a 60Hz will give 8.33ms
100 // in any case the cycle last at least 5ms
101 this->crossed_zero_at = micros();
102 uint32_t cycle_time = this->crossed_zero_at - prev_crossed;
103 if (cycle_time > 5000) {
104 this->cycle_time_us = cycle_time;
105 } else {
106 // Otherwise this is noise and this is 2nd (or 3rd...) fall in the same pulse
107 // Consider this is the right fall edge and accumulate the cycle time instead
108 this->cycle_time_us += cycle_time;
109 }
110
111 if (this->value == 65535) {
112 // fully on, enable output immediately
113 this->gate_pin.digital_write(true);
114 } else if (this->init_cycle) {
115 // send a full cycle
116 this->init_cycle = false;
117 this->enable_time_us = 0;
119 } else if (this->value == 0) {
120 // fully off, disable output immediately
121 this->gate_pin.digital_write(false);
122 } else {
123 auto min_us = this->cycle_time_us * this->min_power / 1000;
124 if (this->method == DIM_METHOD_TRAILING) {
125 this->enable_time_us = 1; // cannot be 0
126 // calculate time until disable in µs with integer arithmetic and take into account min_power
127 this->disable_time_us = std::max((uint32_t) 10, this->value * (this->cycle_time_us - min_us) / 65535 + min_us);
128 } else {
129 // calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic
130 // also take into account min_power
131 this->enable_time_us = std::max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535);
132
133 if (this->method == DIM_METHOD_LEADING_PULSE) {
134 // Minimum pulse time should be enough for the triac to trigger when it is close to the ZC zone
135 // this is for brightness near 99%
136 this->disable_time_us = std::max(this->enable_time_us + GATE_ENABLE_TIME, (uint32_t) cycle_time_us / 10);
137 } else {
138 this->gate_pin.digital_write(false);
139 this->disable_time_us = this->cycle_time_us;
140 }
141 }
142 }
143}
144
146 // Attaching pin interrupts on the same pin will override the previous interrupt
147 // However, the user expects that multiple dimmers sharing the same ZC pin will work.
148 // We solve this in a bit of a hacky way: On each pin interrupt, we check all dimmers
149 // if any of them are using the same ZC pin, and also trigger the interrupt for *them*.
150 for (auto *dimmer : all_dimmers) {
151 if (dimmer == nullptr)
152 break;
153 if (dimmer->zero_cross_pin_number == store->zero_cross_pin_number) {
154 dimmer->gpio_intr();
155 }
156 }
157}
158
159#ifdef USE_ESP32
160// ESP32 implementation, uses basically the same code but needs to wrap
161// timer_interrupt() function to auto-reschedule
162static HWTimer *dimmer_timer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
164#endif
165
167 // extend all_dimmers array with our dimmer
168
169 // Need to be sure the zero cross pin is setup only once, ESP8266 fails and ESP32 seems to fail silently
170 auto setup_zero_cross_pin = true;
171
172 for (auto &all_dimmer : all_dimmers) {
173 if (all_dimmer == nullptr) {
174 all_dimmer = &this->store_;
175 break;
176 }
177 if (all_dimmer->zero_cross_pin_number == this->zero_cross_pin_->get_pin()) {
178 setup_zero_cross_pin = false;
179 }
180 }
181
182 this->gate_pin_->setup();
183 this->store_.gate_pin = this->gate_pin_->to_isr();
185 this->store_.min_power = static_cast<uint16_t>(this->min_power_ * 1000);
186 this->min_power_ = 0;
187 this->store_.method = this->method_;
188
189 if (setup_zero_cross_pin) {
190 this->zero_cross_pin_->setup();
194 }
195
196#ifdef USE_ESP8266
197 // Uses ESP8266 waveform (soft PWM) class
198 // PWM and AcDimmer can even run at the same time this way
199 setTimer1Callback(&timer_interrupt);
200#endif
201#ifdef USE_ESP32
202 dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ);
204 // For ESP32, we can't use dynamic interval calculation because the timerX functions
205 // are not callable from ISR (placed in flash storage).
206 // Here we just use an interrupt firing every 50 µs.
207 timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0);
208#endif
209}
210
212 state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation
213 auto new_value = static_cast<uint16_t>(roundf(state * 65535));
214 if (new_value != 0 && this->store_.value == 0)
216 this->store_.value = new_value;
217}
218
220 ESP_LOGCONFIG(TAG,
221 "AcDimmer:\n"
222 " Min Power: %.1f%%\n"
223 " Init with half cycle: %s",
224 this->store_.min_power / 10.0f, YESNO(this->init_with_half_cycle_));
225 LOG_PIN(" Output Pin: ", this->gate_pin_);
226 LOG_PIN(" Zero-Cross Pin: ", this->zero_cross_pin_);
228 ESP_LOGCONFIG(TAG, " Method: leading pulse");
229 } else if (method_ == DIM_METHOD_LEADING) {
230 ESP_LOGCONFIG(TAG, " Method: leading");
231 } else {
232 ESP_LOGCONFIG(TAG, " Method: trailing");
233 }
234
235 LOG_FLOAT_OUTPUT(this);
236 ESP_LOGV(TAG, " Estimated Frequency: %.3fHz", 1e6f / this->store_.cycle_time_us / 2);
237}
238
239} // namespace esphome::ac_dimmer
virtual void setup()=0
void digital_write(bool value)
Definition gpio.cpp:148
virtual uint8_t get_pin() const =0
void attach_interrupt(void(*func)(T *), T *arg, gpio::InterruptType type) const
Definition gpio.h:107
virtual ISRInternalGPIOPin to_isr() const =0
void write_state(float state) override
InternalGPIOPin * gate_pin_
Definition ac_dimmer.h:57
InternalGPIOPin * zero_cross_pin_
Definition ac_dimmer.h:58
AcDimmerDataStore store_
Definition ac_dimmer.h:59
bool state
Definition fan.h:2
void timer_attach_interrupt(HWTimer *timer, voidFuncPtr user_func)
void timer_alarm(HWTimer *timer, uint64_t alarm_value, bool autoreload, uint64_t reload_count)
uint32_t IRAM_ATTR HOT timer_interrupt()
Run timer interrupt code and return in how many µs the next event is expected.
Definition ac_dimmer.cpp:78
HWTimer * timer_begin(uint32_t frequency)
@ INTERRUPT_FALLING_EDGE
Definition gpio.h:51
uint32_t IRAM_ATTR HOT micros()
Definition core.cpp:27
uint32_t cycle_time_us
Time between the last two ZC pulses.
Definition ac_dimmer.h:23
bool init_cycle
Set to send the first half ac cycle complete.
Definition ac_dimmer.h:31
uint32_t disable_time_us
Time since last ZC pulse to disable gate pin. 0 means no disable.
Definition ac_dimmer.h:29
uint32_t enable_time_us
Time since last ZC pulse to enable gate pin. 0 means not set.
Definition ac_dimmer.h:27
uint16_t min_power
Minimum power for activation.
Definition ac_dimmer.h:21
uint32_t crossed_zero_at
Time (in micros()) of last ZC signal.
Definition ac_dimmer.h:25
uint8_t zero_cross_pin_number
Zero-cross pin number - used to share ZC pin across multiple dimmers.
Definition ac_dimmer.h:15
uint16_t value
Value of the dimmer - 0 to 65535.
Definition ac_dimmer.h:19
uint32_t timer_intr(uint32_t now)
Function called from timer interrupt Input is current time in microseconds (micros()) Returns when ne...
Definition ac_dimmer.cpp:40
ISRInternalGPIOPin gate_pin
Output pin to write to.
Definition ac_dimmer.h:17
ISRInternalGPIOPin zero_cross_pin
Zero-cross pin.
Definition ac_dimmer.h:13
DimMethod method
Dimmer method.
Definition ac_dimmer.h:33
void gpio_intr()
GPIO interrupt routine, called when ZC pin triggers.
Definition ac_dimmer.cpp:96
static void s_gpio_intr(AcDimmerDataStore *store)