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