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