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#ifndef GPIO_IS_VALID_OUTPUT_GPIO
24 #define GPIO_IS_VALID_OUTPUT_GPIO(gpio_num) ((gpio_num >= 0) && \
25 (((1ULL << (gpio_num)) & SOC_GPIO_VALID_OUTPUT_GPIO_MASK) != 0))
26#endif
27
28#ifndef GPIO_IS_VALID_GPIO
29 #define GPIO_IS_VALID_GPIO(gpio_num) ((gpio_num >= 0) && \
30 (((1ULL << (gpio_num)) & SOC_GPIO_VALID_GPIO_MASK) != 0))
31#endif
32
33// Minimum delay to reach the voltage required for a gate current of 30mA.
34// delay_us = asin((gate_resistor * gate_current) / grid_volt_max) / pi * period_us
35// delay_us = asin((330 * 0.03) / 325) / pi * 10000 = 97us
36#define PHASE_DELAY_MIN_US (90)
37
38#define TAG "Thyristor"
39
40struct RegisteredDimmer;
42 Mycila::ThyristorDimmer* dimmer = nullptr;
43 RegisteredDimmer* prev = nullptr;
44 RegisteredDimmer* next = nullptr;
45 uint16_t alarm_count = UINT16_MAX; // when to fire the dimmer
46};
47
48static struct RegisteredDimmer* dimmers = nullptr;
49static gptimer_handle_t fire_timer = nullptr;
50
51#ifndef MYCILA_DIMMER_NO_LOCK
52static portMUX_TYPE dimmers_spinlock = portMUX_INITIALIZER_UNLOCKED;
53#endif
54
56 if (_enabled)
57 return;
58
59 if (!GPIO_IS_VALID_OUTPUT_GPIO(_pin)) {
60 ESP_LOGE(TAG, "Invalid pin: %" PRId8, _pin);
61 return;
62 }
63
64 ESP_LOGI(TAG, "Enable dimmer on pin %" PRId8, _pin);
65
66 pinMode(_pin, OUTPUT);
67 digitalWrite(_pin, LOW);
68 _registerDimmer(this);
69 _enabled = true;
70
71 // restart with last saved value
72 setDutyCycle(_dutyCycle);
73}
74
76 if (!_enabled)
77 return;
78 _enabled = false;
79 _online = false;
80 ESP_LOGI(TAG, "Disable dimmer on pin %" PRId8, _pin);
81 _apply();
82 _unregisterDimmer(this);
83 digitalWrite(_pin, LOW);
84}
85
86void ARDUINO_ISR_ATTR Mycila::ThyristorDimmer::onZeroCross(int16_t delayUntilZero, void* arg) {
87 // prepare our next alarm for the next dimmer to be fired
88 gptimer_alarm_config_t fire_timer_alarm_cfg = {.alarm_count = UINT16_MAX, .reload_count = 0, .flags = {.auto_reload_on_alarm = false}};
89
90 // immediately reset the firing timer to start counting from this ZC event and avoid it to trigger other alarms
91 if (inlined_gptimer_set_raw_count(fire_timer, 0) != ESP_OK) {
92 // failed to reset the timer: probably not initialized yet: just ignore this ZC event
93 return;
94 }
95
96#ifndef MYCILA_DIMMER_NO_LOCK
97 // lock since we need to iterate over the list of dimmers
98 portENTER_CRITICAL_SAFE(&dimmers_spinlock);
99#endif
100
101 // go through all registered dimmers to prepare the next firing
102 struct RegisteredDimmer* current = dimmers;
103 while (current != nullptr) {
104 if (current->dimmer->_delay) {
105 // 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
106 gpio_ll_set_level(&GPIO, current->dimmer->_pin, LOW);
107 // calculate the next firing time:
108 // - If Dimmer is off (UINT16_MAX) => current->alarm_count == UINT16_MAX => dimmer will not be fired
109 // - If Dimmer is on with a delay > 0 => check to be sure it is PHASE_DELAY_MIN_US minimum
110 current->alarm_count = current->dimmer->_delay < PHASE_DELAY_MIN_US ? PHASE_DELAY_MIN_US : current->dimmer->_delay;
111 // keep track the minimum delay at which we have to fire a dimmer
112 if (current->alarm_count < fire_timer_alarm_cfg.alarm_count)
113 fire_timer_alarm_cfg.alarm_count = current->alarm_count;
114 } else {
115 // no delay: dimmer has to be kept on: do nothing
116 gpio_ll_set_level(&GPIO, current->dimmer->_pin, HIGH);
117 // reset the alarm count to indicate that this dimmer was fired
118 current->alarm_count = UINT16_MAX;
119 }
120 current = current->next;
121 }
122
123#ifndef MYCILA_DIMMER_NO_LOCK
124 // unlock the list of dimmers
125 portEXIT_CRITICAL_SAFE(&dimmers_spinlock);
126#endif
127
128 // get the time we spent looping and eventually waiting for the lock
129 uint64_t fire_timer_count_value;
130 if (inlined_gptimer_get_raw_count(fire_timer, &fire_timer_count_value) != ESP_OK) {
131 // failed to get the timer count: just ignore this ZC event
132 return;
133 }
134
135 // check if we had to wait too much time for the lock and we missed the 0V crossing point
136 if (fire_timer_count_value >= delayUntilZero) {
137 fire_timer_count_value -= delayUntilZero;
138
139 // check if we missed the minimum time at which we have to turn the first dimmer on (next alarm)
140 if (fire_timer_count_value <= fire_timer_alarm_cfg.alarm_count) {
141 // directly call the firing ISR to turn on the first dimmer without waiting for an alarm
142 if (inlined_gptimer_set_raw_count(fire_timer, fire_timer_count_value) == ESP_OK) {
143 _fireTimerISR(fire_timer, nullptr, nullptr);
144 }
145 } else {
146 // 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
147 }
148
149 } else {
150 // 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
151 if (inlined_gptimer_set_raw_count(fire_timer, -static_cast<uint64_t>(delayUntilZero) + fire_timer_count_value) == ESP_OK) {
152 // and set an alarm to be woken up at the right time: minimumCount
153 inlined_gptimer_set_alarm_action(fire_timer, &fire_timer_alarm_cfg);
154 }
155 }
156}
157
158// Timer ISR to be called as soon as a dimmer needs to be fired
159bool ARDUINO_ISR_ATTR Mycila::ThyristorDimmer::_fireTimerISR(gptimer_handle_t timer, const gptimer_alarm_event_data_t* event, void* arg) {
160 // prepare our next alarm for the first dimmer to be fired
161 gptimer_alarm_config_t fire_timer_alarm_cfg = {.alarm_count = UINT16_MAX, .reload_count = 0, .flags = {.auto_reload_on_alarm = false}};
162
163 // get the time we spent looping and eventually waiting for the lock
164 uint64_t fire_timer_count_value;
165 if (inlined_gptimer_get_raw_count(fire_timer, &fire_timer_count_value) != ESP_OK) {
166 // failed to get the timer count: just ignore this event
167 return false;
168 }
169
170 do {
171 fire_timer_alarm_cfg.alarm_count = UINT16_MAX;
172
173#ifndef MYCILA_DIMMER_NO_LOCK
174 // lock since we need to iterate over the list of dimmers
175 portENTER_CRITICAL_SAFE(&dimmers_spinlock);
176#endif
177
178 // go through all registered dimmers and check the ones to fire
179 struct RegisteredDimmer* current = dimmers;
180 while (current != nullptr) {
181 if (current->alarm_count != UINT16_MAX) {
182 // this dimmer has not yet been fired (< UINT16_MAX)
183 if (current->alarm_count <= fire_timer_count_value) {
184 // timer alarm has reached this dimmer alarm => time to fire this dimmer
185 gpio_ll_set_level(&GPIO, current->dimmer->_pin, HIGH);
186 // reset the alarm count to indicate that this dimmer has been fired
187 current->alarm_count = UINT16_MAX;
188 } else {
189 // dimmer has to be fired later => keep the minimum time at which we have to fire a dimmer
190 if (current->alarm_count < fire_timer_alarm_cfg.alarm_count)
191 fire_timer_alarm_cfg.alarm_count = current->alarm_count;
192 }
193 }
194 current = current->next;
195 }
196
197#ifndef MYCILA_DIMMER_NO_LOCK
198 // unlock the list of dimmers
199 portEXIT_CRITICAL_SAFE(&dimmers_spinlock);
200#endif
201
202 // refresh the current timer count value to check if we have to fire other dimmers
203 inlined_gptimer_get_raw_count(fire_timer, &fire_timer_count_value);
204 } while (fire_timer_alarm_cfg.alarm_count != UINT16_MAX && fire_timer_alarm_cfg.alarm_count <= fire_timer_count_value);
205
206 // if there are some remaining dimmers to be fired, set an alarm for the next ones
207 if (fire_timer_alarm_cfg.alarm_count != UINT16_MAX)
208 inlined_gptimer_set_alarm_action(fire_timer, &fire_timer_alarm_cfg);
209
210 return false;
211}
212
213// add a dimmer to the list of managed dimmers
214void Mycila::ThyristorDimmer::_registerDimmer(Mycila::ThyristorDimmer* dimmer) {
215 if (dimmers == nullptr) {
216 ESP_LOGI(TAG, "Starting dimmer firing ISR");
217
218 gptimer_config_t timer_config;
219 timer_config.clk_src = GPTIMER_CLK_SRC_DEFAULT;
220 timer_config.direction = GPTIMER_COUNT_UP;
221 timer_config.resolution_hz = 1000000; // 1MHz resolution
222 timer_config.flags.intr_shared = true;
223 timer_config.intr_priority = 0;
224#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
225 timer_config.flags.backup_before_sleep = false;
226#endif
227#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0)
228 timer_config.flags.allow_pd = false;
229#endif
230
231 ESP_ERROR_CHECK(gptimer_new_timer(&timer_config, &fire_timer));
232 gptimer_event_callbacks_t callbacks_config;
233 callbacks_config.on_alarm = _fireTimerISR;
234 ESP_ERROR_CHECK(gptimer_register_event_callbacks(fire_timer, &callbacks_config, nullptr));
235 ESP_ERROR_CHECK(gptimer_enable(fire_timer));
236 ESP_ERROR_CHECK(gptimer_start(fire_timer));
237 }
238
239 ESP_LOGD(TAG, "Register new dimmer %p on pin %d", dimmer, dimmer->getPin());
240
241#ifndef MYCILA_DIMMER_NO_LOCK
242 portENTER_CRITICAL_SAFE(&dimmers_spinlock);
243#endif
244
245 if (dimmers == nullptr) {
246 dimmers = new RegisteredDimmer();
247 dimmers->dimmer = dimmer;
248 } else {
249 struct RegisteredDimmer* first = new RegisteredDimmer();
250 first->next = dimmers;
251 dimmers->prev = first;
252 dimmers = first;
253 }
254
255#ifndef MYCILA_DIMMER_NO_LOCK
256 portEXIT_CRITICAL_SAFE(&dimmers_spinlock);
257#endif
258}
259
260// remove a dimmer from the list of managed dimmers
261void Mycila::ThyristorDimmer::_unregisterDimmer(Mycila::ThyristorDimmer* dimmer) {
262 ESP_LOGD(TAG, "Unregister dimmer %p on pin %d", dimmer, dimmer->getPin());
263
264#ifndef MYCILA_DIMMER_NO_LOCK
265 portENTER_CRITICAL_SAFE(&dimmers_spinlock);
266#endif
267
268 struct RegisteredDimmer* current = dimmers;
269 while (current != nullptr) {
270 if (current->dimmer == dimmer) {
271 if (current->prev != nullptr) {
272 current->prev->next = current->next;
273 } else {
274 dimmers = current->next;
275 }
276 if (current->next != nullptr) {
277 current->next->prev = current->prev;
278 }
279 delete current;
280 break;
281 }
282 current = current->next;
283 }
284
285#ifndef MYCILA_DIMMER_NO_LOCK
286 portEXIT_CRITICAL_SAFE(&dimmers_spinlock);
287#endif
288
289 if (dimmers == nullptr) {
290 ESP_LOGI(TAG, "Stopping dimmer firing ISR");
291 gptimer_stop(fire_timer); // might be already stopped
292 ESP_ERROR_CHECK(gptimer_disable(fire_timer));
293 ESP_ERROR_CHECK(gptimer_del_timer(fire_timer));
294 fire_timer = nullptr;
295 }
296}
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)
void begin() override
Enable a dimmer on a specific GPIO pin.
void end() override
Disable the dimmer.