Compare commits

..
2 Commits
Author SHA1 Message Date
aZholtikov 9bbded33b2 doc: added wiki 2026-10-18 21:08:34 +03:00
aZholtikov 51fd157ed2 doc: updated readme 2026-10-18 21:07:48 +03:00
2 changed files with 460 additions and 11 deletions

No files matched your search

+135 -11
View File
@@ -1,12 +1,22 @@
# ESP32 ESP-IDF component for AC dimmer # ESP32 ESP-IDF component for AC dimmer (phase-angle control)
## Features ## Features
1. Zero-crossing detection via GPIO interrupt on both edges (`GPIO_INTR_ANYEDGE`), with edge polarity determined from the current GPIO level and duplicate/glitch edge rejection. Spurious-interrupt filtering: the interval between rising edges must be within the half-cycle range derived from the frequency 30 - 500 Hz (shorter intervals are ignored, longer ones reset the timing and calibration state). 1. Zero-crossing detection via GPIO interrupt on both edges with edge polarity determined from the current GPIO level and duplicate/glitch edge rejection.
2. Measured zero-crossing half-period is low-pass filtered and used to calculate the TRIAC firing delay, so the dimmer adapts to the actual mains frequency. 2. Spurious-interrupt filtering based on half-cycle range derived from the mains frequency limits (30 to 500 Hz).
3. TRIAC gate control through ESP-IDF gptimer (1 MHz resolution) with configurable dimming level (0–100%): 0 keeps the load off, 100 fires the TRIAC immediately. The firing delay is clamped to leave a guard band (period / 20, at least 5 us) before the end of the half-cycle. 3. Low-pass filtered zero-crossing half-period measurement adapts the firing delay to the actual mains frequency.
4. Automatic runtime calibration of the hardware base offset: on each falling edge the width of the zero-crossing pulse is measured and fed into an EMA (`pulse_width / 2`), compensating for the zero-crossing detector advance relative to the physical mains zero crossing. 4. TRIAC gate control through ESP-IDF gptimer with 1 MHz resolution and configurable dimming level from 0 to 100 percent.
5. Handle-based API: init/deinit (with full GPIO, ISR and timer cleanup), start/stop, set/get dimming level. 5. Firing delay clamped to a guard band of at least 5 microseconds before the end of the half-cycle.
6. Automatic runtime calibration of the hardware base offset using exponential moving average of the zero-crossing pulse width.
7. Handle-based API with init, deinit, start, stop, set and get functions.
8. Memory managed through ESP-IDF heap_caps functions with full cleanup on initialization failure.
## Attention
1. Only one dimmer instance per zero-crossing GPIO pin. Initializing a second instance on the same pin replaces the ISR handler of the first one.
2. Not thread-safe. Volatile fields prevent compiler optimizations but do not provide mutual exclusion between tasks.
3. Calibration assumes a symmetric zero-crossing optocoupler pulse. Asymmetric pulses introduce a systematic offset error.
4. Supported mains frequency range is limited by the compile-time constants 30 to 500 Hz. Out-of-range intervals reset the timing and calibration state.
## Using ## Using
@@ -23,7 +33,9 @@ In the application, add the component:
#include "zh_ac_dimmer.h" #include "zh_ac_dimmer.h"
``` ```
## Example ## Examples
### Basic Example: Initialize and Control Dimming Level
```c ```c
#include "zh_ac_dimmer.h" #include "zh_ac_dimmer.h"
@@ -36,24 +48,136 @@ void app_main(void)
zh_ac_dimmer_init_config_t config = ZH_AC_DIMMER_INIT_CONFIG_DEFAULT(); zh_ac_dimmer_init_config_t config = ZH_AC_DIMMER_INIT_CONFIG_DEFAULT();
config.zero_cross_gpio = GPIO_NUM_16; config.zero_cross_gpio = GPIO_NUM_16;
config.triac_gpio = GPIO_NUM_17; config.triac_gpio = GPIO_NUM_17;
zh_ac_dimmer_init(&config, &ac_dimmer_handle); esp_err_t ret = zh_ac_dimmer_init(&config, &ac_dimmer_handle);
zh_ac_dimmer_start(&ac_dimmer_handle); if (ret != ESP_OK)
{
printf("Dimmer initialization error: %s\n", esp_err_to_name(ret));
return;
}
ret = zh_ac_dimmer_start(&ac_dimmer_handle);
if (ret != ESP_OK)
{
printf("Dimmer start error: %s\n", esp_err_to_name(ret));
}
uint8_t val1 = 50;
ret = zh_ac_dimmer_set(&ac_dimmer_handle, val1);
if (ret != ESP_OK)
{
printf("Dimmer set error: %s\n", esp_err_to_name(ret));
}
uint8_t value = 0;
ret = zh_ac_dimmer_get(&ac_dimmer_handle, &value);
if (ret == ESP_OK)
{
printf("Dimmer value: %u\n", value);
}
else
{
printf("Dimmer get error: %s\n", esp_err_to_name(ret));
}
vTaskDelay(5000 / portTICK_PERIOD_MS);
ret = zh_ac_dimmer_stop(&ac_dimmer_handle);
if (ret != ESP_OK)
{
printf("Dimmer stop error: %s\n", esp_err_to_name(ret));
}
zh_ac_dimmer_deinit(&ac_dimmer_handle);
}
```
---
### Fade In Fade Out Example
```c
#include "zh_ac_dimmer.h"
zh_ac_dimmer_handle_t *ac_dimmer_handle = NULL;
void app_main(void)
{
esp_log_level_set("zh_ac_dimmer", ESP_LOG_ERROR);
zh_ac_dimmer_init_config_t config = ZH_AC_DIMMER_INIT_CONFIG_DEFAULT();
config.zero_cross_gpio = GPIO_NUM_16;
config.triac_gpio = GPIO_NUM_17;
esp_err_t ret = zh_ac_dimmer_init(&config, &ac_dimmer_handle);
if (ret != ESP_OK)
{
printf("Dimmer initialization error: %s\n", esp_err_to_name(ret));
return;
}
ret = zh_ac_dimmer_start(&ac_dimmer_handle);
if (ret != ESP_OK)
{
printf("Dimmer start error: %s\n", esp_err_to_name(ret));
}
uint8_t value = 0; uint8_t value = 0;
for (uint8_t i = 0; i <= 100; ++i) for (uint8_t i = 0; i <= 100; ++i)
{ {
zh_ac_dimmer_set(&ac_dimmer_handle, i); zh_ac_dimmer_set(&ac_dimmer_handle, i);
zh_ac_dimmer_get(&ac_dimmer_handle, &value); zh_ac_dimmer_get(&ac_dimmer_handle, &value);
printf("Dimmer value: %d.\n", value); printf("Dimmer value: %u\n", value);
vTaskDelay(100 / portTICK_PERIOD_MS); vTaskDelay(100 / portTICK_PERIOD_MS);
} }
for (uint8_t i = 100; i > 0; --i) for (uint8_t i = 100; i > 0; --i)
{ {
zh_ac_dimmer_set(&ac_dimmer_handle, i); zh_ac_dimmer_set(&ac_dimmer_handle, i);
zh_ac_dimmer_get(&ac_dimmer_handle, &value); zh_ac_dimmer_get(&ac_dimmer_handle, &value);
printf("Dimmer value: %d.\n", value); printf("Dimmer value: %u\n", value);
vTaskDelay(100 / portTICK_PERIOD_MS); vTaskDelay(100 / portTICK_PERIOD_MS);
} }
zh_ac_dimmer_stop(&ac_dimmer_handle); zh_ac_dimmer_stop(&ac_dimmer_handle);
zh_ac_dimmer_deinit(&ac_dimmer_handle); zh_ac_dimmer_deinit(&ac_dimmer_handle);
} }
``` ```
---
### Error Handling Example
```c
#include "zh_ac_dimmer.h"
zh_ac_dimmer_handle_t *ac_dimmer_handle = NULL;
void app_main(void)
{
esp_log_level_set("zh_ac_dimmer", ESP_LOG_ERROR);
zh_ac_dimmer_init_config_t config = ZH_AC_DIMMER_INIT_CONFIG_DEFAULT();
config.zero_cross_gpio = GPIO_NUM_16;
config.triac_gpio = GPIO_NUM_16;
esp_err_t ret = zh_ac_dimmer_init(&config, &ac_dimmer_handle);
if (ret != ESP_OK)
{
printf("Expected init error: %s\n", esp_err_to_name(ret));
}
config.triac_gpio = GPIO_NUM_17;
ret = zh_ac_dimmer_init(&config, &ac_dimmer_handle);
if (ret != ESP_OK)
{
printf("Dimmer initialization error: %s\n", esp_err_to_name(ret));
return;
}
ret = zh_ac_dimmer_init(&config, &ac_dimmer_handle);
if (ret != ESP_OK)
{
printf("Expected double init error: %s\n", esp_err_to_name(ret));
}
zh_ac_dimmer_start(&ac_dimmer_handle);
ret = zh_ac_dimmer_set(&ac_dimmer_handle, 101);
if (ret != ESP_OK)
{
printf("Expected set range error: %s\n", esp_err_to_name(ret));
}
ret = zh_ac_dimmer_set(&ac_dimmer_handle, 75);
if (ret != ESP_OK)
{
printf("Dimmer set error: %s\n", esp_err_to_name(ret));
}
uint8_t value = 0;
zh_ac_dimmer_get(&ac_dimmer_handle, &value);
printf("Dimmer value: %u\n", value);
zh_ac_dimmer_stop(&ac_dimmer_handle);
zh_ac_dimmer_deinit(&ac_dimmer_handle);
}
```
+325
View File
@@ -0,0 +1,325 @@
# Описание компонента zh_ac_dimmer
## Общие сведения
**zh_ac_dimmer** — это драйвер для ESP-IDF, реализующий фазовое управление мощностью
переменного тока (AC dimming) через детектирование перехода через ноль и управление
затвором тиристора (TRIAC). Компонент определяет момент перехода сетевого напряжения
через ноль, измеряет период полуволны и зажигает TRIAC с программируемой задержкой,
регулируя среднюю мощность, подаваемую на нагрузку.
Компонент предоставляет полный набор функций для инициализации, запуска, остановки
и управления уровнем затемнения (0–100 %), а также автоматическую калибровку
аппаратного смещения в режиме реального времени.
### Основные возможности
1. **Детектирование перехода через ноль** — через GPIO-прерывание с измерением периода.
2. **Управление TRIAC** — через ESP-IDF gptimer с программируемой задержкой.
3. **Уровень затемнения 0–100 %** — плавная регулировка мощности нагрузки.
4. **Автоматическая калибровка** — аппаратное смещение калибруется во время работы.
5. **Фильтрация периодов** — низкочастотный фильтр для стабильного измерения частоты сети.
6. **Защитный зазор (guard band)** — гарантированное отключение TRIAC до конца полуволны.
7. **Валидация частоты** — отбрасывание помех и некорректных импульсов.
8. **Старт/стоп API** — простое управление состоянием dimmer.
---
## Внутреннее устройство
### Структура `zh_ac_dimmer_handle_t`
Внутренняя структура хранит все состояние экземпляра dimmer: GPIO-пины, таймер,
состояние синхронизации и текущий уровень затемнения.
| Поле | Тип | Описание |
| ------ | ----- | ---------- |
| `zero_cross_gpio` | `gpio_num_t` | GPIO-пин для детектирования перехода через ноль |
| `triac_gpio` | `gpio_num_t` | GPIO-пин для управления затвором TRIAC (выход) |
| `dimmer_timer_handle` | `gptimer_handle_t` | Экземпляр gptimer для задержки зажигания TRIAC |
| `prev_zero_crossing_us` | `uint64_t` | Метка времени предыдущего фронтa (мкс) |
| `pulse_start_us` | `uint64_t` | Метка времени текущего фронтa для измерения ширины импульса (мкс) |
| `period_us` | `uint32_t` | Отфильтрованный период полуволны в мкс (0 до первого измерения) |
| `base_offset_us` | `volatile uint32_t` | Автоматически откалиброванное опережение ISR относительно нуля (мкс) |
| `auto_base_samples` | `volatile uint32_t` | Количество образцов, использованных для калибровки |
| `dimmer_value` | `volatile uint8_t` | Текущий уровень затемнения, 0 (выкл) .. 100 (полная мощность) |
| `is_dimmer_work` | `volatile bool` | Флаг активной работы dimmer |
| `last_edge_was_rising` | `bool` | Флаг полярности последнего фронта для отбрасывания дубликатов |
### Управление памятью
Память выделяется через `heap_caps_calloc()` в функции `zh_ac_dimmer_init()`
с флагом `MALLOC_CAP_8BIT`. Освобождение происходит в `zh_ac_dimmer_deinit()`
через `heap_caps_free()`. При неудачной инициализации освобождённая память
очищается через цепочку cleanup в макросе `ZH_ERROR_CHECK`.
### Структура `zh_ac_dimmer_init_config_t`
Конфигурационная структура, передаваемая пользователем при инициализации.
| Поле | Тип | Описание |
|------|-----|----------|
| `zero_cross_gpio` | `gpio_num_t` | GPIO-пин для детектирования перехода через ноль |
| `triac_gpio` | `gpio_num_t` | GPIO-пин для управления затвором TRIAC |
---
## API (интерфейс)
### Инициализация и освобождение
| Функция | Описание |
|---------|----------|
| `zh_ac_dimmer_init()` | Инициализирует драйвер: выделяет память, настраивает GPIO, устанавливает ISR-обработчик, создаёт gptimer |
| `zh_ac_dimmer_deinit()` | Полностью деинициализирует драйвер: удаляет ISR, сбрасывает GPIO, удаляет таймер, освобождает память |
### Управление состоянием
| Функция | Описание |
|---------|----------|
| `zh_ac_dimmer_start()` | Включает dimmer — переходы через ноль начинают запускать TRIAC |
| `zh_ac_dimmer_stop()` | Останавливает dimmer — TRIAC не зажигается до следующего перехода через ноль |
### Установка и получение уровня
| Функция | Описание |
|---------|----------|
| `zh_ac_dimmer_set()` | Устанавливает уровень затемнения (0–100 %), применяется на следующем полупериоде |
| `zh_ac_dimmer_get()` | Возвращает текущий установленный уровень затемнения |
---
## Потокобезопасность
Компонент **не является потокобезопасным**. Все поля структуры `zh_ac_dimmer_handle_t`,
изменяемые как из основного потока, так и из ISR (`dimmer_value`, `is_dimmer_work`,
`base_offset_us`, `auto_base_samples`), помечены как `volatile` для предотвращения
оптимизаций компилятора. Синхронизация между потоками не предусмотрена — пользователь
должен обеспечивать взаимное исключение на уровне приложения, если dimmer используется
из нескольких задач FreeRTOS.
---
## Алгоритмы
### Калибровка аппаратного смещения
При детектировании спадающего фронта импульса перехода через ноль вычисляется ширина
импульса (`pulse_width`), которая делится пополам для получения опережения:
```
measured_adv = pulse_width / 2
```
Первое измерение устанавливает `base_offset_us` напрямую. Последующие значения
сглаживаются экспоненциальным скользящим средним (EMA) с коэффициентом 15/16:
```
base_offset_us = (base_offset_us * 15 + measured_adv) / 16
```
Валидация импульса: ширина должна быть в диапазоне `[4 мкс, period_us / 4]`.
Если `period_us` ещё не измерен, используется верхний предел 2000 мкс.
### Фильтрация периода полуволны
Первый принятый фронт устанавливает `period_us` напрямую. Последующие значения
сглаживаются EMA с коэффициентом 7/8:
```
period_us = (period_us * 7 + delta_us) / 8
```
Если интервал между фронтами выходит за пределы `[period_min_us, period_max_us]`,
состояние сбрасывается: `period_us = 0`, `auto_base_samples = 0`, TRIAC сбрасывается.
Границы периода рассчитываются из настраиваемой частоты сети:
```
period_min_us = 1 000 000 / (2 * ZH_AC_DIMMER_FREQ_MAX_HZ)
period_max_us = 1 000 000 / (2 * ZH_AC_DIMMER_FREQ_MIN_HZ)
```
### Расчёт задержки зажигания TRIAC
Для каждого принятого фронтa вычисляется задержка до зажигания TRIAC:
1. Вычисляется целевая задержка на основе уровня затемнения:
```
t_fire = period * (100 - dimmer_value) / 100
```
2. Добавляется откалиброванное смещение:
```
delay_us = t_fire + base_offset_us
```
Если `base_offset_us >= period`, смещение ограничивается значением `period`.
3. Если `delay_us <= 0`, TRIAC зажигается немедленно.
4. Вычисляется защитный зазор: `guard_us = max(period / 20, 5 мкс)`.
5. Если `delay_us > period - guard_us`, задержка урезается до `period - guard_us`.
6. Таймер запускается с вычисленной задержкой.
### Обработка ISR перехода через ноль
ISR обрабатывает оба фронта сигнала:
1. **Спадающий фронт** (GPIO = 0, предыдущий фронт был возрастающий):
- Измеряется ширина импульса.
- Калибруется `base_offset_us` через EMA.
- Действий с TRIAC не выполняется.
2. **Возрастающий фронт** (GPIO = 1):
- Сохраняется метка времени `pulse_start_us`.
- Если это первый фронт — только запоминается время, TRIAC сбрасывается.
- Интервал сравнивается с допустимыми границами:
- Если интервал слишком короткий — фронт отбрасывается как помеха.
- Если интервал слишком длинный — состояние сбрасывается.
- Обновляется отфильтрованный период `period_us`.
- Если dimmer не работает или уровень = 0 — таймер останавливается.
- Иначе — вычисляется задержка и запускается таймер для зажигания TRIAC.
---
## Примеры использования
### Базовый пример инициализации и управления
```c
#include "zh_ac_dimmer.h"
zh_ac_dimmer_handle_t *dimmer = NULL;
void app_main(void)
{
// Настройка уровня логирования
esp_log_level_set("zh_ac_dimmer", ESP_LOG_ERROR);
// Конфигурация пинов
zh_ac_dimmer_init_config_t config = ZH_AC_DIMMER_INIT_CONFIG_DEFAULT();
config.zero_cross_gpio = GPIO_NUM_16;
config.triac_gpio = GPIO_NUM_17;
// Инициализация и запуск
zh_ac_dimmer_init(&config, &dimmer);
zh_ac_dimmer_start(&dimmer);
// Установка уровня затемнения 50 %
zh_ac_dimmer_set(&dimmer, 50);
// Чтение текущего уровня
uint8_t level = 0;
zh_ac_dimmer_get(&dimmer, &level);
// Остановка и деинициализация
zh_ac_dimmer_stop(&dimmer);
zh_ac_dimmer_deinit(&dimmer);
}
```
### Динамическое изменение яркости
```c
#include "zh_ac_dimmer.h"
zh_ac_dimmer_handle_t *dimmer = NULL;
void app_main(void)
{
esp_log_level_set("zh_ac_dimmer", ESP_LOG_ERROR);
zh_ac_dimmer_init_config_t config = ZH_AC_DIMMER_INIT_CONFIG_DEFAULT();
config.zero_cross_gpio = GPIO_NUM_16;
config.triac_gpio = GPIO_NUM_17;
zh_ac_dimmer_init(&config, &dimmer);
zh_ac_dimmer_start(&dimmer);
// Плавное нарастание яркости от 0 до 100 %
for (uint8_t i = 0; i <= 100; ++i)
{
zh_ac_dimmer_set(&dimmer, i);
vTaskDelay(100 / portTICK_PERIOD_MS);
}
// Плавное затухание от 100 до 0 %
for (uint8_t i = 100; i > 0; --i)
{
zh_ac_dimmer_set(&dimmer, i);
vTaskDelay(100 / portTICK_PERIOD_MS);
}
zh_ac_dimmer_stop(&dimmer);
zh_ac_dimmer_deinit(&dimmer);
}
```
---
## Логирование
Компонент использует ESP-IDF логирование с тегом `"zh_ac_dimmer"`.
```c
esp_log_level_set("zh_ac_dimmer", ESP_LOG_ERROR);
```
Доступные уровни: `ESP_LOG_ERROR`, `ESP_LOG_WARN`, `ESP_LOG_INFO`, `ESP_LOG_DEBUG`.
Настройка `ESP_LOG_ERROR` оставляет только сообщения об ошибках. Уровень `ESP_LOG_INFO`
добавляет сообщения об успешной инициализации, запуске и остановке.
---
## Ошибки
| Код | Описание |
| ----- | ---------- |
| `ESP_OK` | Успешное выполнение |
| `ESP_ERR_INVALID_ARG` | Неверный аргумент: NULL-указатель, некорректный GPIO, одинаковые пины, значение > 100 |
| `ESP_ERR_INVALID_STATE` | Драйвер уже инициализирован (handle указывает на существующий экземпляр) |
| `ESP_ERR_NO_MEM` | Не удалось выделить память для handle |
| `ESP_FAIL` | Не удалось настроить GPIO, установить ISR-обработчик или создать таймер |
---
## Ограничения
1. **Один экземпляр на GPIO-пин** — инициализация второго dimmer на том же
zero-cross GPIO заменяет ISR-обработчик первого экземпляра. Каждый пин должен
принадлежать одному экземпляру.
2. **Не потокобезопасный** — нет внутренней синхронизации между задачами.
`volatile` защищает только от оптимизаций компилятора, но не от гонки данных.
3. **Диапазон частот сети** — определяется макросами `ZH_AC_DIMMER_FREQ_MIN_HZ`
(по умолчанию 30 Гц) и `ZH_AC_DIMMER_FREQ_MAX_HZ` (по умолчанию 500 Гц).
Выход за эти границы приводит к сбросу состояния.
4. **Симметричный оптоизолятор** — калибровка смещения предполагает симметричный
импульс перехода через ноль (`base_offset = pulse_width / 2`). Асимметрия
внесёт систематическую погрешность.
5. **Защитный зазор** — минимальная задержка перед концом полуволны ограничена
`max(period / 20, 5 мкс)`, что может не обеспечить достаточного времени
для полного затухания TRIAC на очень низких частотах.
---
## Требования для сборки
- ESP-IDF (фреймворк для ESP32)
- FreeRTOS (входит в состав ESP-IDF)
- Драйверы `driver/gpio` и `driver/gptimer` (входят в состав ESP-IDF)
Компонент размещается в директории `components/` проекта ESP-IDF.
---
## Установка
```bash
cd ваш_проект/components
git clone https://github.com/aZholtikov/zh_ac_dimmer
```
```c
#include "zh_ac_dimmer.h"
```