MycilaJSY 13.0.0
Arduino / ESP32 library for the JSY1031, JSY-MK-163, JSY-MK-193, JSY-MK-194, JSY-MK-227, JSY-MK-229, JSY-MK-333 families single-phase and three-phase AC bidirectional meters from Shenzhen Jiansiyan Technologies Co, Ltd.
Loading...
Searching...
No Matches
MycilaDimmerThyristor.cpp
1// SPDX-License-Identifier: MIT
2/*
3 * Copyright (C) 2023-2025 Mathieu Carbou
4 */
5#include <MycilaDimmerThyristor.h>
6
7// lock
8#include <freertos/FreeRTOS.h>
9
10// gpio
11#include <driver/gpio.h>
12#include <driver/gptimer_types.h>
13#include <esp32-hal-gpio.h>
14#include <hal/gpio_ll.h>
15#include <soc/gpio_struct.h>
16
17// logging
18#include <esp32-hal-log.h>
19
20// timers
21#include "priv/inlined_gptimer.h"
22
23#ifdef MYCILA_LOGGER_SUPPORT
24 #include <MycilaLogger.h>
25extern Mycila::Logger logger;
26 #define LOGD(tag, format, ...) logger.debug(tag, format, ##__VA_ARGS__)
27 #define LOGI(tag, format, ...) logger.info(tag, format, ##__VA_ARGS__)
28 #define LOGW(tag, format, ...) logger.warn(tag, format, ##__VA_ARGS__)
29 #define LOGE(tag, format, ...) logger.error(tag, format, ##__VA_ARGS__)
30#else
31 #define LOGD(tag, format, ...) ESP_LOGD(tag, format, ##__VA_ARGS__)
32 #define LOGI(tag, format, ...) ESP_LOGI(tag, format, ##__VA_ARGS__)
33 #define LOGW(tag, format, ...) ESP_LOGW(tag, format, ##__VA_ARGS__)
34 #define LOGE(tag, format, ...) ESP_LOGE(tag, format, ##__VA_ARGS__)
35#endif
36
37#ifndef GPIO_IS_VALID_OUTPUT_GPIO
38 #define GPIO_IS_VALID_OUTPUT_GPIO(gpio_num) ((gpio_num >= 0) && \
39 (((1ULL << (gpio_num)) & SOC_GPIO_VALID_OUTPUT_GPIO_MASK) != 0))
40#endif
41
42#ifndef GPIO_IS_VALID_GPIO
43 #define GPIO_IS_VALID_GPIO(gpio_num) ((gpio_num >= 0) && \
44 (((1ULL << (gpio_num)) & SOC_GPIO_VALID_GPIO_MASK) != 0))
45#endif
46
47// Minimum delay to reach the voltage required for a gate current of 30mA.
48// delay_us = asin((gate_resistor * gate_current) / grid_volt_max) / pi * period_us
49// delay_us = asin((330 * 0.03) / 325) / pi * 10000 = 97us
50#define PHASE_DELAY_MIN_US (90)
51
52#define TAG "ZC_DIMMER"
53
54struct EnabledDimmer;
56 Mycila::ThyristorDimmer* dimmer = nullptr;
57 EnabledDimmer* prev = nullptr;
58 EnabledDimmer* next = nullptr;
59 uint16_t alarm_count = UINT16_MAX; // when to fire the dimmer
60};
61
62static struct EnabledDimmer* dimmers = nullptr;
63static gptimer_handle_t fire_timer = nullptr;
64
65#define USE_DIMMER_LOCK 1
66#if USE_DIMMER_LOCK
67static portMUX_TYPE dimmers_spinlock = portMUX_INITIALIZER_UNLOCKED;
68#endif
69
71 if (_enabled)
72 return;
73
74 if (!GPIO_IS_VALID_OUTPUT_GPIO(_pin)) {
75 LOGE(TAG, "Disable ZC Dimmer: Invalid pin: %" PRId8, _pin);
76 return;
77 }
78
79 LOGI(TAG, "Enable Thyristor Dimmer on pin %" PRId8, _pin);
80
81 pinMode(_pin, OUTPUT);
82 digitalWrite(_pin, LOW);
83 _registerDimmer(this);
84 _enabled = true;
85
86 // restart with last saved value
87 setDutyCycle(_dutyCycle);
88}
89
91 if (!_enabled)
92 return;
93 _enabled = false;
94 _online = false;
95 LOGI(TAG, "Disable ZC Dimmer on pin %" PRId8, _pin);
96 _apply();
97 _unregisterDimmer(this);
98 digitalWrite(_pin, LOW);
99}
100
101void ARDUINO_ISR_ATTR Mycila::ThyristorDimmer::onZeroCross(int16_t delayUntilZero, void* arg) {
102 // prepare our next alarm for the next dimmer to be fired
103 gptimer_alarm_config_t fire_timer_alarm_cfg = {.alarm_count = UINT16_MAX, .reload_count = 0, .flags = {.auto_reload_on_alarm = false}};
104
105 // immediately reset the firing timer to start counting from this ZC event and avoid it to trigger other alarms
106 if (inlined_gptimer_set_raw_count(fire_timer, 0) != ESP_OK) {
107 // failed to reset the timer: probably not initialized yet: just ignore this ZC event
108 return;
109 }
110
111#if USE_DIMMER_LOCK
112 // lock since we need to iterate over the list of dimmers
113 portENTER_CRITICAL_SAFE(&dimmers_spinlock);
114#endif
115
116 // go through all registered dimmers to prepare the next firing
117 struct EnabledDimmer* current = dimmers;
118 while (current != nullptr) {
119 // if a delay is applied (dimmer is off - UINT16_MAX - or on with a delay > 0), turn off the triac and it will be turned on again later
120 if (current->dimmer->_delay)
121 gpio_ll_set_level(&GPIO, current->dimmer->_pin, LOW);
122 // calculate the next firing time:
123 // - Dimmer is off (UINT16_MAX) => remainingDelay = UINT16_MAX (will not be fired)
124 // - Dimmer is on at full power (0) => remainingDelay = PHASE_DELAY (and we did not turned off the triac and it will be turned on again at the next ZC event + PHASE_DELAY
125 // - Dimmer is on with a delay > 0 => remainingDelay = delay or PHASE_DELAY_MIN_US if delay is too low
126 current->alarm_count = current->dimmer->_delay < PHASE_DELAY_MIN_US ? PHASE_DELAY_MIN_US : current->dimmer->_delay;
127 // keep the minimum time at which we have to fire a dimmer
128 if (current->alarm_count < fire_timer_alarm_cfg.alarm_count)
129 fire_timer_alarm_cfg.alarm_count = current->alarm_count;
130 current = current->next;
131 }
132
133#if USE_DIMMER_LOCK
134 // unlock the list of dimmers
135 portEXIT_CRITICAL_SAFE(&dimmers_spinlock);
136#endif
137
138 // get the time we spent looping and eventually waiting for the lock
139 uint64_t fire_timer_count_value;
140 if (inlined_gptimer_get_raw_count(fire_timer, &fire_timer_count_value) != ESP_OK) {
141 // failed to get the timer count: just ignore this ZC event
142 return;
143 }
144
145 // check if we had to wait too much time for the lock and we missed the 0V crossing point
146 if (fire_timer_count_value >= delayUntilZero) {
147 fire_timer_count_value -= delayUntilZero;
148
149 // check if we missed the minimum time at which we have to turn the first dimmer on (next alarm)
150 if (fire_timer_count_value <= fire_timer_alarm_cfg.alarm_count) {
151 // directly call the firing ISR to turn on the first dimmer without waiting for an alarm
152 if (inlined_gptimer_set_raw_count(fire_timer, fire_timer_count_value) == ESP_OK) {
153 _fireTimerISR(fire_timer, nullptr, nullptr);
154 }
155 } else {
156 // we are too late: do nothing: this is better to wait for the next ZC event than trying to turn on dimmers too late, which would create flickering
157 }
158
159 } else {
160 // 0V crossing point not yet reached: set the counter to be at the right current position (very large number) before 0: the timer count will then overflow
161 if (inlined_gptimer_set_raw_count(fire_timer, -static_cast<uint64_t>(delayUntilZero) + fire_timer_count_value) == ESP_OK) {
162 // and set an alarm to be woken up at the right time: minimumCount
163 inlined_gptimer_set_alarm_action(fire_timer, &fire_timer_alarm_cfg);
164 }
165 }
166}
167
168// Timer ISR to be called as soon as a dimmer needs to be fired
169bool ARDUINO_ISR_ATTR Mycila::ThyristorDimmer::_fireTimerISR(gptimer_handle_t timer, const gptimer_alarm_event_data_t* event, void* arg) {
170 // prepare our next alarm for the first dimmer to be fired
171 gptimer_alarm_config_t fire_timer_alarm_cfg = {.alarm_count = UINT16_MAX, .reload_count = 0, .flags = {.auto_reload_on_alarm = false}};
172
173 // get the time we spent looping and eventually waiting for the lock
174 uint64_t fire_timer_count_value;
175 if (inlined_gptimer_get_raw_count(fire_timer, &fire_timer_count_value) != ESP_OK) {
176 // failed to get the timer count: just ignore this event
177 return false;
178 }
179
180 do {
181 fire_timer_alarm_cfg.alarm_count = UINT16_MAX;
182
183#if USE_DIMMER_LOCK
184 // lock since we need to iterate over the list of dimmers
185 portENTER_CRITICAL_SAFE(&dimmers_spinlock);
186#endif
187
188 // go through all registered dimmers and check the ones to fire
189 struct EnabledDimmer* current = dimmers;
190 while (current != nullptr) {
191 if (current->alarm_count != UINT16_MAX) {
192 // this dimmer has not yet been fired (< UINT16_MAX)
193 if (current->alarm_count <= fire_timer_count_value) {
194 // timer alarm has reached this dimmer alarm => time to fire this dimmer
195 gpio_ll_set_level(&GPIO, current->dimmer->_pin, HIGH);
196 // reset the alarm count to indicate that this dimmer has been fired
197 current->alarm_count = UINT16_MAX;
198 } else {
199 // dimmer has to be fired later => keep the minimum time at which we have to fire a dimmer
200 if (current->alarm_count < fire_timer_alarm_cfg.alarm_count)
201 fire_timer_alarm_cfg.alarm_count = current->alarm_count;
202 }
203 }
204 current = current->next;
205 }
206
207#if USE_DIMMER_LOCK
208 // unlock the list of dimmers
209 portEXIT_CRITICAL_SAFE(&dimmers_spinlock);
210#endif
211
212 // refresh the current timer count value to check if we have to fire other dimmers
213 inlined_gptimer_get_raw_count(fire_timer, &fire_timer_count_value);
214 } while (fire_timer_alarm_cfg.alarm_count != UINT16_MAX && fire_timer_alarm_cfg.alarm_count <= fire_timer_count_value);
215
216 // if there are some remaining dimmers to be fired, set an alarm for the next ones
217 if (fire_timer_alarm_cfg.alarm_count != UINT16_MAX)
218 inlined_gptimer_set_alarm_action(fire_timer, &fire_timer_alarm_cfg);
219
220 return false;
221}
222
223// add a dimmer to the list of managed dimmers
224void Mycila::ThyristorDimmer::_registerDimmer(Mycila::ThyristorDimmer* dimmer) {
225 if (dimmers == nullptr) {
226 LOGI(TAG, "Starting dimmer firing ISR");
227
228 gptimer_config_t timer_config;
229 timer_config.clk_src = GPTIMER_CLK_SRC_DEFAULT;
230 timer_config.direction = GPTIMER_COUNT_UP;
231 timer_config.resolution_hz = 1000000; // 1MHz resolution
232 timer_config.flags.intr_shared = true;
233 timer_config.intr_priority = 0;
234#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
235 timer_config.flags.backup_before_sleep = false;
236#endif
237#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0)
238 timer_config.flags.allow_pd = false;
239#endif
240
241 ESP_ERROR_CHECK(gptimer_new_timer(&timer_config, &fire_timer));
242 gptimer_event_callbacks_t callbacks_config;
243 callbacks_config.on_alarm = _fireTimerISR;
244 ESP_ERROR_CHECK(gptimer_register_event_callbacks(fire_timer, &callbacks_config, nullptr));
245 ESP_ERROR_CHECK(gptimer_enable(fire_timer));
246 ESP_ERROR_CHECK(gptimer_start(fire_timer));
247 }
248
249 LOGD(TAG, "Register new dimmer %p on pin %d", dimmer, dimmer->getPin());
250
251#if USE_DIMMER_LOCK
252 portENTER_CRITICAL_SAFE(&dimmers_spinlock);
253#endif
254
255 if (dimmers == nullptr) {
256 dimmers = new EnabledDimmer();
257 dimmers->dimmer = dimmer;
258 } else {
259 struct EnabledDimmer* first = new EnabledDimmer();
260 first->next = dimmers;
261 dimmers->prev = first;
262 dimmers = first;
263 }
264
265#if USE_DIMMER_LOCK
266 portEXIT_CRITICAL_SAFE(&dimmers_spinlock);
267#endif
268}
269
270// remove a dimmer from the list of managed dimmers
271void Mycila::ThyristorDimmer::_unregisterDimmer(Mycila::ThyristorDimmer* dimmer) {
272 LOGD(TAG, "Unregister dimmer %p on pin %d", dimmer, dimmer->getPin());
273
274#if USE_DIMMER_LOCK
275 portENTER_CRITICAL_SAFE(&dimmers_spinlock);
276#endif
277
278 struct EnabledDimmer* current = dimmers;
279 while (current != nullptr) {
280 if (current->dimmer == dimmer) {
281 if (current->prev != nullptr) {
282 current->prev->next = current->next;
283 } else {
284 dimmers = current->next;
285 }
286 if (current->next != nullptr) {
287 current->next->prev = current->prev;
288 }
289 delete current;
290 break;
291 }
292 current = current->next;
293 }
294
295#if USE_DIMMER_LOCK
296 portEXIT_CRITICAL_SAFE(&dimmers_spinlock);
297#endif
298
299 if (dimmers == nullptr) {
300 LOGI(TAG, "Stopping dimmer firing ISR");
301 gptimer_stop(fire_timer); // might be already stopped
302 ESP_ERROR_CHECK(gptimer_disable(fire_timer));
303 ESP_ERROR_CHECK(gptimer_del_timer(fire_timer));
304 fire_timer = nullptr;
305 }
306}
bool setDutyCycle(float dutyCycle)
Set the power duty.
Thyristor (TRIAC) based dimmer implementation for TRIAC and Random SSR dimmers.
gpio_num_t getPin() const
Get the GPIO pin used for the dimmer.
static void onZeroCross(int16_t delayUntilZero, void *args)
virtual void begin()
Enable a dimmer on a specific GPIO pin.
virtual void end()
Disable the dimmer.