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 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
105 if (current->dimmer->_delay)
106 gpio_ll_set_level(&GPIO, current->dimmer->_pin, LOW);
107 // calculate the next firing time:
108 // - Dimmer is off (UINT16_MAX) => remainingDelay = UINT16_MAX (will not be fired)
109 // - 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
110 // - Dimmer is on with a delay > 0 => remainingDelay = delay or PHASE_DELAY_MIN_US if delay is too low
111 current->alarm_count = current->dimmer->_delay < PHASE_DELAY_MIN_US ? PHASE_DELAY_MIN_US : current->dimmer->_delay;
112 // keep the minimum time at which we have to fire a dimmer
113 if (current->alarm_count < fire_timer_alarm_cfg.alarm_count)
114 fire_timer_alarm_cfg.alarm_count = current->alarm_count;
115 current = current->next;
116 }
117
118#ifndef MYCILA_DIMMER_NO_LOCK
119 // unlock the list of dimmers
120 portEXIT_CRITICAL_SAFE(&dimmers_spinlock);
121#endif
122
123 // get the time we spent looping and eventually waiting for the lock
124 uint64_t fire_timer_count_value;
125 if (inlined_gptimer_get_raw_count(fire_timer, &fire_timer_count_value) != ESP_OK) {
126 // failed to get the timer count: just ignore this ZC event
127 return;
128 }
129
130 // check if we had to wait too much time for the lock and we missed the 0V crossing point
131 if (fire_timer_count_value >= delayUntilZero) {
132 fire_timer_count_value -= delayUntilZero;
133
134 // check if we missed the minimum time at which we have to turn the first dimmer on (next alarm)
135 if (fire_timer_count_value <= fire_timer_alarm_cfg.alarm_count) {
136 // directly call the firing ISR to turn on the first dimmer without waiting for an alarm
137 if (inlined_gptimer_set_raw_count(fire_timer, fire_timer_count_value) == ESP_OK) {
138 _fireTimerISR(fire_timer, nullptr, nullptr);
139 }
140 } else {
141 // 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
142 }
143
144 } else {
145 // 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
146 if (inlined_gptimer_set_raw_count(fire_timer, -static_cast<uint64_t>(delayUntilZero) + fire_timer_count_value) == ESP_OK) {
147 // and set an alarm to be woken up at the right time: minimumCount
148 inlined_gptimer_set_alarm_action(fire_timer, &fire_timer_alarm_cfg);
149 }
150 }
151}
152
153// Timer ISR to be called as soon as a dimmer needs to be fired
154bool ARDUINO_ISR_ATTR Mycila::ThyristorDimmer::_fireTimerISR(gptimer_handle_t timer, const gptimer_alarm_event_data_t* event, void* arg) {
155 // prepare our next alarm for the first dimmer to be fired
156 gptimer_alarm_config_t fire_timer_alarm_cfg = {.alarm_count = UINT16_MAX, .reload_count = 0, .flags = {.auto_reload_on_alarm = false}};
157
158 // get the time we spent looping and eventually waiting for the lock
159 uint64_t fire_timer_count_value;
160 if (inlined_gptimer_get_raw_count(fire_timer, &fire_timer_count_value) != ESP_OK) {
161 // failed to get the timer count: just ignore this event
162 return false;
163 }
164
165 do {
166 fire_timer_alarm_cfg.alarm_count = UINT16_MAX;
167
168#ifndef MYCILA_DIMMER_NO_LOCK
169 // lock since we need to iterate over the list of dimmers
170 portENTER_CRITICAL_SAFE(&dimmers_spinlock);
171#endif
172
173 // go through all registered dimmers and check the ones to fire
174 struct RegisteredDimmer* current = dimmers;
175 while (current != nullptr) {
176 if (current->alarm_count != UINT16_MAX) {
177 // this dimmer has not yet been fired (< UINT16_MAX)
178 if (current->alarm_count <= fire_timer_count_value) {
179 // timer alarm has reached this dimmer alarm => time to fire this dimmer
180 gpio_ll_set_level(&GPIO, current->dimmer->_pin, HIGH);
181 // reset the alarm count to indicate that this dimmer has been fired
182 current->alarm_count = UINT16_MAX;
183 } else {
184 // dimmer has to be fired later => keep the minimum time at which we have to fire a dimmer
185 if (current->alarm_count < fire_timer_alarm_cfg.alarm_count)
186 fire_timer_alarm_cfg.alarm_count = current->alarm_count;
187 }
188 }
189 current = current->next;
190 }
191
192#ifndef MYCILA_DIMMER_NO_LOCK
193 // unlock the list of dimmers
194 portEXIT_CRITICAL_SAFE(&dimmers_spinlock);
195#endif
196
197 // refresh the current timer count value to check if we have to fire other dimmers
198 inlined_gptimer_get_raw_count(fire_timer, &fire_timer_count_value);
199 } while (fire_timer_alarm_cfg.alarm_count != UINT16_MAX && fire_timer_alarm_cfg.alarm_count <= fire_timer_count_value);
200
201 // if there are some remaining dimmers to be fired, set an alarm for the next ones
202 if (fire_timer_alarm_cfg.alarm_count != UINT16_MAX)
203 inlined_gptimer_set_alarm_action(fire_timer, &fire_timer_alarm_cfg);
204
205 return false;
206}
207
208// add a dimmer to the list of managed dimmers
209void Mycila::ThyristorDimmer::_registerDimmer(Mycila::ThyristorDimmer* dimmer) {
210 if (dimmers == nullptr) {
211 ESP_LOGI(TAG, "Starting dimmer firing ISR");
212
213 gptimer_config_t timer_config;
214 timer_config.clk_src = GPTIMER_CLK_SRC_DEFAULT;
215 timer_config.direction = GPTIMER_COUNT_UP;
216 timer_config.resolution_hz = 1000000; // 1MHz resolution
217 timer_config.flags.intr_shared = true;
218 timer_config.intr_priority = 0;
219#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
220 timer_config.flags.backup_before_sleep = false;
221#endif
222#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0)
223 timer_config.flags.allow_pd = false;
224#endif
225
226 ESP_ERROR_CHECK(gptimer_new_timer(&timer_config, &fire_timer));
227 gptimer_event_callbacks_t callbacks_config;
228 callbacks_config.on_alarm = _fireTimerISR;
229 ESP_ERROR_CHECK(gptimer_register_event_callbacks(fire_timer, &callbacks_config, nullptr));
230 ESP_ERROR_CHECK(gptimer_enable(fire_timer));
231 ESP_ERROR_CHECK(gptimer_start(fire_timer));
232 }
233
234 ESP_LOGD(TAG, "Register new dimmer %p on pin %d", dimmer, dimmer->getPin());
235
236#ifndef MYCILA_DIMMER_NO_LOCK
237 portENTER_CRITICAL_SAFE(&dimmers_spinlock);
238#endif
239
240 if (dimmers == nullptr) {
241 dimmers = new RegisteredDimmer();
242 dimmers->dimmer = dimmer;
243 } else {
244 struct RegisteredDimmer* first = new RegisteredDimmer();
245 first->next = dimmers;
246 dimmers->prev = first;
247 dimmers = first;
248 }
249
250#ifndef MYCILA_DIMMER_NO_LOCK
251 portEXIT_CRITICAL_SAFE(&dimmers_spinlock);
252#endif
253}
254
255// remove a dimmer from the list of managed dimmers
256void Mycila::ThyristorDimmer::_unregisterDimmer(Mycila::ThyristorDimmer* dimmer) {
257 ESP_LOGD(TAG, "Unregister dimmer %p on pin %d", dimmer, dimmer->getPin());
258
259#ifndef MYCILA_DIMMER_NO_LOCK
260 portENTER_CRITICAL_SAFE(&dimmers_spinlock);
261#endif
262
263 struct RegisteredDimmer* current = dimmers;
264 while (current != nullptr) {
265 if (current->dimmer == dimmer) {
266 if (current->prev != nullptr) {
267 current->prev->next = current->next;
268 } else {
269 dimmers = current->next;
270 }
271 if (current->next != nullptr) {
272 current->next->prev = current->prev;
273 }
274 delete current;
275 break;
276 }
277 current = current->next;
278 }
279
280#ifndef MYCILA_DIMMER_NO_LOCK
281 portEXIT_CRITICAL_SAFE(&dimmers_spinlock);
282#endif
283
284 if (dimmers == nullptr) {
285 ESP_LOGI(TAG, "Stopping dimmer firing ISR");
286 gptimer_stop(fire_timer); // might be already stopped
287 ESP_ERROR_CHECK(gptimer_disable(fire_timer));
288 ESP_ERROR_CHECK(gptimer_del_timer(fire_timer));
289 fire_timer = nullptr;
290 }
291}
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.