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
MycilaDimmerCycleStealing.cpp
1// SPDX-License-Identifier: MIT
2/*
3 * Copyright (C) 2023-2025 Mathieu Carbou
4 */
5#include <MycilaDimmerCycleStealing.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#define TAG "CycleStealing"
34
35static gptimer_handle_t fire_timer = nullptr;
36static bool isr_running = false; // Re-entry guard: only accessed from _fireTimerISR, no volatile needed
37
38#ifndef MYCILA_DIMMER_NO_LOCK
39static portMUX_TYPE dimmers_spinlock = portMUX_INITIALIZER_UNLOCKED;
40#endif
41
42Mycila::CycleStealingDimmer::RegisteredDimmer* Mycila::CycleStealingDimmer::dimmers = nullptr;
43
45 if (_enabled)
46 return true;
47
48 if (!GPIO_IS_VALID_OUTPUT_GPIO(_pin)) {
49 ESP_LOGE(TAG, "Invalid pin: %" PRId8, _pin);
50 return false;
51 }
52
53 ESP_LOGI(TAG, "Enable dimmer on pin %" PRId8, _pin);
54
55 pinMode(_pin, OUTPUT);
56 digitalWrite(_pin, LOW);
57 _registerDimmer(this);
58 _enabled = true;
59
60 // restart with last saved value
61 setDutyCycle(_dutyCycle);
62 return true;
63}
64
66 if (!_enabled)
67 return;
68 _enabled = false;
69 _online = false;
70 ESP_LOGI(TAG, "Disable dimmer on pin %" PRId8, _pin);
71 _apply();
72 _unregisterDimmer(this);
73 digitalWrite(_pin, LOW);
74}
75
76bool Mycila::CycleStealingDimmer::_apply() {
77 if (!_enabled)
78 return false;
79 if (!_online || !_semiPeriod || _dutyCycleFire == 0) {
80 if (_running) {
81 if (fire_timer != nullptr) {
82 ESP_ERROR_CHECK(gptimer_set_alarm_action(fire_timer, nullptr));
83 }
84 _running = false;
85 }
86 } else {
87 if (!_running) {
88 // Start the firing timer with a period equal to the semi-period
89 // If a ZCD is there, the start of the timer will be synced by the ZCD signal to be just before the 0V crossing
90 // Otherwise, we rely on the clock accuracy considering that a ZC dimmer can only activate or deactivate at 0V crossing
91 gptimer_alarm_config_t fire_timer_alarm_cfg = {
92 .alarm_count = _semiPeriod,
93 .reload_count = 0,
94 .flags = {.auto_reload_on_alarm = true}};
95 if (fire_timer != nullptr) {
96 ESP_ERROR_CHECK(gptimer_set_raw_count(fire_timer, 0));
97 ESP_ERROR_CHECK(gptimer_set_alarm_action(fire_timer, &fire_timer_alarm_cfg));
98 }
99 _running = true;
100 }
101 }
102 return true;
103}
104
105void ARDUINO_ISR_ATTR Mycila::CycleStealingDimmer::onZeroCross(int16_t delayUntilZero, void* arg) {
106 // sync the firering timer to start a little before 0V crossing
107 if (inlined_gptimer_set_raw_count(fire_timer, 0) != ESP_OK) {
108 // failed to reset the timer: probably not initialized yet: just ignore this ZC event
109 return;
110 }
111}
112
113// Timer ISR to be called as soon as a dimmer needs to be fired
114bool ARDUINO_ISR_ATTR Mycila::CycleStealingDimmer::_fireTimerISR(gptimer_handle_t timer, const gptimer_alarm_event_data_t* event, void* arg) {
115 // Prevent re-entry: if this ISR takes longer than the timer period,
116 // we must not allow concurrent execution which could cause race conditions
117 if (isr_running) {
118 // ISR is already running - skip this alarm to prevent re-entry
119 return false;
120 }
121 isr_running = true;
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 event
127 isr_running = false;
128 return false;
129 }
130
131 // Note: if locking takes too long (more than semi-period), a new timer event may be triggered
132 // while we are still in this ISR, but it will be ignored by the isr_running guard
133
134#ifndef MYCILA_DIMMER_NO_LOCK
135 // lock since we need to iterate over the list of dimmers
136 portENTER_CRITICAL_SAFE(&dimmers_spinlock);
137#endif
138
139 // go through all registered dimmers and compute the next action
140 struct RegisteredDimmer* current = dimmers;
141 while (current != nullptr) {
142 // if (current->alarm_count != UINT16_MAX) {
143 // // this dimmer has not yet been fired (< UINT16_MAX)
144 // if (current->alarm_count <= fire_timer_count_value) {
145 // // timer alarm has reached this dimmer alarm => time to fire this dimmer
146 // gpio_ll_set_level(&GPIO, current->dimmer->_pin, HIGH);
147 // // reset the alarm count to indicate that this dimmer has been fired
148 // current->alarm_count = UINT16_MAX;
149 // } else {
150 // // dimmer has to be fired later => keep the minimum time at which we have to fire a dimmer
151 // if (current->alarm_count < fire_timer_alarm_cfg.alarm_count)
152 // fire_timer_alarm_cfg.alarm_count = current->alarm_count;
153 // }
154 // }
155 current = current->next;
156 }
157
158#ifndef MYCILA_DIMMER_NO_LOCK
159 // unlock the list of dimmers
160 portEXIT_CRITICAL_SAFE(&dimmers_spinlock);
161#endif
162
163 isr_running = false;
164 return false;
165}
166
167// add a dimmer to the list of managed dimmers
168void Mycila::CycleStealingDimmer::_registerDimmer(Mycila::CycleStealingDimmer* dimmer) {
169 if (dimmers == nullptr) {
170 ESP_LOGI(TAG, "Starting dimmer firing ISR");
171
172 gptimer_config_t timer_config;
173 timer_config.clk_src = GPTIMER_CLK_SRC_DEFAULT;
174 timer_config.direction = GPTIMER_COUNT_UP;
175 timer_config.resolution_hz = 1000000; // 1MHz resolution
176 timer_config.flags.intr_shared = true;
177 timer_config.intr_priority = 0;
178#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
179 timer_config.flags.backup_before_sleep = false;
180#endif
181#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0)
182 timer_config.flags.allow_pd = false;
183#endif
184
185 ESP_ERROR_CHECK(gptimer_new_timer(&timer_config, &fire_timer));
186 gptimer_event_callbacks_t callbacks_config;
187 callbacks_config.on_alarm = _fireTimerISR;
188 ESP_ERROR_CHECK(gptimer_register_event_callbacks(fire_timer, &callbacks_config, nullptr));
189 ESP_ERROR_CHECK(gptimer_enable(fire_timer));
190 ESP_ERROR_CHECK(gptimer_start(fire_timer));
191 }
192
193 ESP_LOGD(TAG, "Register new dimmer %p on pin %d", dimmer, dimmer->getPin());
194
195#ifndef MYCILA_DIMMER_NO_LOCK
196 portENTER_CRITICAL_SAFE(&dimmers_spinlock);
197#endif
198
199 if (dimmers == nullptr) {
200 dimmers = new RegisteredDimmer();
201 dimmers->dimmer = dimmer;
202 } else {
203 struct RegisteredDimmer* additional = new RegisteredDimmer();
204 additional->dimmer = dimmer;
205 additional->next = dimmers;
206 additional->prev = nullptr;
207 dimmers->prev = additional;
208 dimmers = additional;
209 }
210
211#ifndef MYCILA_DIMMER_NO_LOCK
212 portEXIT_CRITICAL_SAFE(&dimmers_spinlock);
213#endif
214}
215
216// remove a dimmer from the list of managed dimmers
217void Mycila::CycleStealingDimmer::_unregisterDimmer(Mycila::CycleStealingDimmer* dimmer) {
218 ESP_LOGD(TAG, "Unregister dimmer %p on pin %d", dimmer, dimmer->getPin());
219
220#ifndef MYCILA_DIMMER_NO_LOCK
221 portENTER_CRITICAL_SAFE(&dimmers_spinlock);
222#endif
223
224 struct RegisteredDimmer* current = dimmers;
225 while (current != nullptr) {
226 if (current->dimmer == dimmer) {
227 if (current->prev != nullptr) {
228 current->prev->next = current->next;
229 } else {
230 dimmers = current->next;
231 }
232 if (current->next != nullptr) {
233 current->next->prev = current->prev;
234 }
235 delete current;
236 break;
237 }
238 current = current->next;
239 }
240
241#ifndef MYCILA_DIMMER_NO_LOCK
242 portEXIT_CRITICAL_SAFE(&dimmers_spinlock);
243#endif
244
245 if (dimmers == nullptr) {
246 ESP_LOGI(TAG, "Stopping dimmer firing ISR");
247 gptimer_stop(fire_timer); // might be already stopped
248 ESP_ERROR_CHECK(gptimer_disable(fire_timer));
249 ESP_ERROR_CHECK(gptimer_del_timer(fire_timer));
250 fire_timer = nullptr;
251 }
252}
void end() override
Disable the dimmer.
bool begin() override
Enable a dimmer on a specific GPIO pin.
static void onZeroCross(int16_t delayUntilZero, void *args)
gpio_num_t getPin() const
Get the GPIO pin used for the dimmer.
bool setDutyCycle(float dutyCycle)
Set the power duty.