feat!: added support some dimmers on one device

This commit is contained in:
aZholtikov committed 2026-08-29 21:04:17 +03:00
1 parent 0e9f663a8d
commit dbe7fce7ce
7 files changed
+465 -264

No files matched your search

+9 -43
View File
@@ -1,4 +1,4 @@
# ESP32 ESP-IDF component for AC dimmer 115V/400Hz
# ESP32 ESP-IDF component for AC dimmer for 115V/400Hz
## Wiki
@@ -10,23 +10,18 @@
## SAST Tools
[PVS-Studio](https://pvs-studio.com/pvs-studio/?utm_source=website&utm_medium=github&utm_campaign=open_source) - static analyzer for C, C++, C#, and Java code.
[PVS-Studio](https://pvs-studio.com/pvs-studio/?utm_source=website&utm_medium=github&utm_campaign=open_source) — static analyzer for C, C++, C#, and Java code.
## Attention
## Features
1. Supports frequency 400Hz only.
2. For correct operation, please enable the following settings in the menuconfig:
```text
GPIO_CTRL_FUNC_IN_IRAM
CONFIG_GPTIMER_ISR_HANDLER_IN_IRAM
CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM
CONFIG_GPTIMER_ISR_CACHE_SAFE
```
1. Zero-crossing detection via GPIO interrupt for precise phase-angle control.
2. TRIAC gate control through ESP-IDF gptimer with configurable dimming level (0–100%).
3. IRAM-safe ISR and callback functions for reliable operation.
4. Thread-safe start/stop/set/get API.
## Using
In an existing project, run the following command to install the components:
In an existing project, run the following command to install the component:
```text
cd ../your_project/components
@@ -41,33 +36,4 @@ In the application, add the component:
## Examples
```c
#include "zh_ac_dimmer.h"
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);
zh_ac_dimmer_start();
uint8_t value = 0;
for (uint8_t i = 0; i <= 100; ++i)
{
zh_ac_dimmer_set(i);
zh_ac_dimmer_get(&value);
printf("Dimmer value: %d.\n", value);
vTaskDelay(100 / portTICK_PERIOD_MS);
}
for (uint8_t i = 100; i > 0; --i)
{
zh_ac_dimmer_set(i);
zh_ac_dimmer_get(&value);
printf("Dimmer value: %d.\n", value);
vTaskDelay(100 / portTICK_PERIOD_MS);
}
zh_ac_dimmer_stop();
zh_ac_dimmer_deinit();
}
```
See Wiki [EN](WIKI_EN.md#usage-examples) | [RU](WIKI_RU.md#примеры-использования)
+106 -62
View File
@@ -18,7 +18,7 @@
`zh_ac_dimmer` is a lightweight ESP-IDF component for AC dimmer control. It provides a simple API to control AC load power using zero-crossing detection and TRIAC control. The component is designed specifically for ESP32 microcontrollers and uses ESP-IDF v5.0+ drivers (GPTimer, GPIO).
The component supports AC frequency 400Hz only and uses hardware timers for precise timing control.
The component supports AC frequency of 400 Hz and uses hardware timers for precise timing control.
---
@@ -26,36 +26,37 @@ The component supports AC frequency 400Hz only and uses hardware timers for prec
1. **AC Power Control**: Controls AC load power from 0% to 100%
2. **Zero-Crossing Detection**: Uses GPIO interrupt for precise timing
3. **TRIAC Control**: Controls TRIAC gate with precise timing
4. **Hardware Timer**: Uses GPTimer for accurate dimming control
5. **Error Handling**: Comprehensive error checking and validation
6. **Thread-Safe**: Uses ESP-IDF drivers (thread-safe)
7. **Low Memory**: Minimal memory footprint
8. **Simple API**: Easy-to-use interface
3. **TRIAC Control**: Controls TRIAC gate with precise timing via GPTimer
4. **Hardware Timer**: Uses GPTimer with 1 MHz resolution (1 µs)
5. **IRAM-safe ISR and Callback**: Interrupts and callbacks are placed in IRAM for reliable operation
6. **Error Handling**: Comprehensive error checking and configuration validation
7. **Thread-Safe**: Uses ESP-IDF drivers (thread-safe)
8. **Minimal Memory Footprint**: Low memory overhead
9. **Simple API**: Lightweight interface with start/stop/set/get support
---
## Installation
1. Navigate to your project's components directory:
Navigate to your project's components directory:
```bash
cd ../your_project/components
```
2. Clone the repository:
Clone the repository:
```bash
git clone https://github.com/aZholtikov/zh_ac_dimmer.git
```
3. In your application, include the header:
In your application, include the header:
```c
#include "zh_ac_dimmer.h"
```
4. The component will be automatically built with your project.
The component will be automatically built with your project.
### Required Configuration
@@ -77,53 +78,76 @@ CONFIG_GPTIMER_ISR_CACHE_SAFE
```c
typedef struct
{
gpio_num_t zero_cross_gpio; // Zero cross GPIO
gpio_num_t triac_gpio; // Triac GPIO
gpio_num_t zero_cross_gpio; // Zero-crossing detection GPIO
gpio_num_t triac_gpio; // TRIAC control GPIO
} zh_ac_dimmer_init_config_t;
```
Use `ZH_AC_DIMMER_INIT_CONFIG_DEFAULT()` macro to initialize with default values:
Use the `ZH_AC_DIMMER_INIT_CONFIG_DEFAULT()` macro to initialize with default values:
- `zero_cross_gpio`: GPIO_NUM_MAX
- `triac_gpio`: GPIO_NUM_MAX
---
### zh_ac_dimmer_handle_t Structure
```c
typedef struct _zh_ac_dimmer_handle_t zh_ac_dimmer_handle_t;
```
Opaque instance descriptor.
---
### zh_ac_dimmer_init()
Initializes the AC dimmer.
Initializes AC dimmer control.
Allocates memory for handle, validates configuration, initializes GPIO pins (TRIAC as output, zero-crossing as input with positive edge interrupt), sets up ISR service and creates gptimer instance.
**Parameters:**
- `config` - Pointer to AC dimmer initialization configuration structure
- `config` - Pointer to initialization configuration structure. Must not be NULL
- `handle` - Pointer to pointer to receive the created handle. Must be NULL
**Returns:**
- `ESP_OK` - Success
- `ESP_ERR_INVALID_ARG` - Invalid argument (NULL config or invalid GPIO)
- `ESP_ERR_INVALID_ARG` - Invalid argument (NULL handle or invalid GPIO)
- `ESP_ERR_INVALID_STATE` - AC dimmer is already initialized
- `ESP_FAIL` - Initialization failed (GPIO or timer initialization)
- `ESP_ERR_NO_MEM` - Memory allocation failed
- `ESP_FAIL` - Initialization error (GPIO, ISR service or timer)
**Example:**
```c
zh_ac_dimmer_handle_t *dimmer = NULL;
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);
esp_err_t err = zh_ac_dimmer_init(&config, &dimmer);
```
---
### zh_ac_dimmer_deinit()
Deinitializes the AC dimmer and releases resources.
Deinitializes AC dimmer control and releases resources.
Stops and deletes gptimer, removes GPIO ISR handler, resets GPIO pins to input mode, frees handle and sets the output pointer to NULL.
**Parameters:**
- `handle` - Pointer to handle. Must not be NULL
**Returns:**
- `ESP_OK` - Success
- `ESP_ERR_INVALID_STATE` - AC dimmer is not initialized
- `ESP_FAIL` - Deinitialization failed (GPIO or timer deinitialization)
- `ESP_ERR_INVALID_ARG` - handle or *handle is NULL
- `ESP_FAIL` - Timer stop, detach or delete error, or GPIO reset error
**Important:** After calling this function, the handle becomes invalid. To reinitialize, call `zh_ac_dimmer_init()` again.
---
@@ -131,10 +155,16 @@ Deinitializes the AC dimmer and releases resources.
Starts AC dimmer operation.
Enables the dimmer so that zero-crossing events trigger TRIAC firing according to the current dimming value.
**Parameters:**
- `handle` - Pointer to handle. Must not be NULL
**Returns:**
- `ESP_OK` - Success
- `ESP_ERR_INVALID_STATE` - AC dimmer is not initialized
- `ESP_ERR_INVALID_ARG` - handle or *handle is NULL
---
@@ -142,42 +172,50 @@ Starts AC dimmer operation.
Stops AC dimmer operation.
Disables TRIAC firing on zero-crossing events. The dimmer can be restarted with `zh_ac_dimmer_start()`.
**Parameters:**
- `handle` - Pointer to handle. Must not be NULL
**Returns:**
- `ESP_OK` - Success
- `ESP_ERR_INVALID_STATE` - AC dimmer is not initialized
- `ESP_ERR_INVALID_ARG` - handle or *handle is NULL
---
### zh_ac_dimmer_set()
Sets AC dimmer dimming value.
Sets the AC dimmer dimming value.
Sets the dimming percentage that will be applied on the next zero-crossing event.
**Parameters:**
- `value` - Dimming value (0 to 100, where 0 = off, 100 = full power)
- `handle` - Pointer to handle. Must not be NULL
- `value` - Dimming level from 0 (off) to 100 (full power)
**Returns:**
- `ESP_OK` - Success
- `ESP_ERR_INVALID_STATE` - AC dimmer is not initialized
- `ESP_ERR_INVALID_ARG` - Invalid dimming value
- `ESP_ERR_INVALID_ARG` - handle, *handle or value is invalid
---
### zh_ac_dimmer_get()
Gets current AC dimmer dimming value.
Gets the current AC dimmer dimming value.
**Parameters:**
- `value` - Pointer to store the dimming value
- `handle` - Pointer to handle. Must not be NULL
- `value` - Pointer to receive the current dimming level (0–100). Must not be NULL
**Returns:**
- `ESP_OK` - Success
- `ESP_ERR_INVALID_STATE` - AC dimmer is not initialized
- `ESP_ERR_INVALID_ARG` - Invalid argument (NULL value)
- `ESP_ERR_INVALID_ARG` - handle, *handle or value is NULL
---
@@ -191,28 +229,29 @@ Gets current AC dimmer dimming value.
void app_main(void)
{
esp_log_level_set("zh_ac_dimmer", ESP_LOG_ERROR);
zh_ac_dimmer_handle_t *dimmer = NULL;
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);
zh_ac_dimmer_start();
zh_ac_dimmer_init(&config, &dimmer);
zh_ac_dimmer_start(&dimmer);
uint8_t value = 0;
for (uint8_t i = 0; i <= 100; ++i)
{
zh_ac_dimmer_set(i);
zh_ac_dimmer_get(&value);
printf("Dimmer value: %d.\\n", value);
zh_ac_dimmer_set(&dimmer, i);
zh_ac_dimmer_get(&dimmer, &value);
printf("Dimmer value: %d.\n", value);
vTaskDelay(100 / portTICK_PERIOD_MS);
}
for (uint8_t i = 100; i > 0; --i)
{
zh_ac_dimmer_set(i);
zh_ac_dimmer_get(&value);
printf("Dimmer value: %d.\\n", value);
zh_ac_dimmer_set(&dimmer, i);
zh_ac_dimmer_get(&dimmer, &value);
printf("Dimmer value: %d.\n", value);
vTaskDelay(100 / portTICK_PERIOD_MS);
}
zh_ac_dimmer_stop();
zh_ac_dimmer_deinit();
zh_ac_dimmer_stop(&dimmer);
zh_ac_dimmer_deinit(&dimmer);
}
```
@@ -228,16 +267,18 @@ void app_main(void)
#define BUTTON_GPIO GPIO_NUM_0
static uint8_t dimmer_value = 50;
static zh_ac_dimmer_handle_t *dimmer = NULL;
static void IRAM_ATTR button_isr_handler(void *arg)
{
(void)arg;
dimmer_value = (dimmer_value + 10) % 110;
if (dimmer_value > 100)
{
dimmer_value = 0;
}
zh_ac_dimmer_set(dimmer_value);
printf("Dimmer value: %d\\n", dimmer_value);
zh_ac_dimmer_set(&dimmer, dimmer_value);
printf("Dimmer value: %d\n", dimmer_value);
}
void app_main(void)
@@ -247,7 +288,7 @@ void app_main(void)
zh_ac_dimmer_init_config_t config = ZH_AC_DIMMER_INIT_CONFIG_DEFAULT();
config.zero_cross_gpio = ZERO_CROSS_GPIO;
config.triac_gpio = TRIAC_GPIO;
zh_ac_dimmer_init(&config);
zh_ac_dimmer_init(&config, &dimmer);
// Initialize button
gpio_config_t button_config = {
.intr_type = GPIO_INTR_POSEDGE,
@@ -260,14 +301,14 @@ void app_main(void)
gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);
gpio_isr_handler_add(BUTTON_GPIO, button_isr_handler, NULL);
// Start dimmer
zh_ac_dimmer_start();
printf("Dimmer initialized. Button on GPIO %d changes dimmer value.\\n", BUTTON_GPIO);
zh_ac_dimmer_start(&dimmer);
printf("Dimmer initialized. Button on GPIO %d changes dimmer value.\n", BUTTON_GPIO);
for (;;)
{
vTaskDelay(1000 / portTICK_PERIOD_MS);
uint8_t current_value;
zh_ac_dimmer_get(&current_value);
printf("Current dimmer value: %d\\n", current_value);
zh_ac_dimmer_get(&dimmer, &current_value);
printf("Current dimmer value: %d\n", current_value);
}
}
```
@@ -278,14 +319,15 @@ void app_main(void)
| Parameter | Value |
|-----------|-------|
| **Dimming Range** | 0 - 100% |
| **AC Frequency** | 400 Hz (supported only) |
| **Dimming Range** | 0–100% (0 = off, 100 = full power) |
| **AC Frequency** | 400 Hz (zero-crossing period: 1250 µs) |
| **Zero-Crossing Detection** | GPIO interrupt (positive edge) |
| **TRIAC Control** | GPIO output |
| **Timer Resolution** | 1 MHz (1 µs) |
| **Measurement Time** | ~1250 µs per cycle |
| **Measurement Time** | ~1250 µs per half-cycle |
| **Base TRIAC Delay** | 330 µs (measured with logic analyzer) |
| **ESP-IDF Version** | >= 5.0 |
| **Platform** | ESP32 series |
| **Platform** | ESP32 family |
| **Language** | C (C99) |
---
@@ -295,9 +337,10 @@ void app_main(void)
| Error Code | Description |
|------------|-------------|
| `ESP_OK` | Operation successful |
| `ESP_ERR_INVALID_ARG` | Invalid argument (NULL pointer, invalid GPIO, or invalid dimming value) |
| `ESP_ERR_INVALID_STATE` | AC dimmer is not initialized |
| `ESP_FAIL` | General failure (GPIO or timer initialization/deinitialization) |
| `ESP_ERR_INVALID_ARG` | Invalid argument (NULL pointer, invalid GPIO or invalid dimming value) |
| `ESP_ERR_INVALID_STATE` | AC dimmer is already initialized (only for `zh_ac_dimmer_init`) |
| `ESP_ERR_NO_MEM` | Memory allocation failed (only for `zh_ac_dimmer_init`) |
| `ESP_FAIL` | General error (GPIO, ISR service, timer initialization/deinitialization) |
---
@@ -339,12 +382,13 @@ limitations under the License.
## Additional Notes
- **AC Frequency**: Currently supports 400Hz frequency only
- **Zero-Crossing Timing**: 1250 µs period (400Hz) with 330 µs compensation for timer delay
- **GPIO Interrupt**: Zero-crossing detection uses positive edge interrupt
- **IRAM Requirements**: For correct operation, enable `GPIO_CTRL_FUNC_IN_IRAM`, `CONFIG_GPTIMER_ISR_HANDLER_IN_IRAM`, `CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM`, and `CONFIG_GPTIMER_ISR_CACHE_SAFE` in menuconfig
- **Thread Safety**: The component uses ESP-IDF drivers which are thread-safe
- **AC Frequency**: Currently only 400 Hz frequency is supported (aviation frequency). The period between zero-crossings is 1250 µs.
- **Zero-Crossing Timing**: 1250 µs period (400 Hz) with 330 µs compensation for timer delay. Calculation formula: `alarm_count = (((1250 - 330) / 100) * (100 - dimmer_value)) + 330`.
- **GPIO Interrupt**: Zero-crossing detection uses positive edge interrupt with debounce protection (interrupts arriving earlier than 90% of the period are ignored).
- **IRAM Requirements**: For correct operation, enable `GPIO_CTRL_FUNC_IN_IRAM`, `CONFIG_GPTIMER_ISR_HANDLER_IN_IRAM`, `CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM` and `CONFIG_GPTIMER_ISR_CACHE_SAFE` in menuconfig.
- **Thread Safety**: The component uses ESP-IDF drivers which are thread-safe.
- **Safety**: This module interacts with mains voltage. Incorrect wiring or insufficient isolation may result in electric shock or equipment damage. Ensure proper optoisolation and safety compliance before deployment.
---
*Generated for zh_ac_dimmer v1.0.0*
*Generated for zh_ac_dimmer v3.0.0*
+92 -48
View File
@@ -18,7 +18,7 @@
`zh_ac_dimmer` - это легковесный компонент ESP-IDF для управления освещением переменного тока. Он предоставляет простой API для управления мощностью нагрузки переменного тока с использованием детекции перехода через ноль и управления TRIAC. Компонент разработан специально для микроконтроллеров ESP32 и использует драйверы ESP-IDF v5.0+ (GPTimer, GPIO).
Компонент поддерживает частоту переменного тока 400Гц и использует аппаратные таймеры для точного управления временем.
Компонент поддерживает частоту переменного тока 400 Гц и использует аппаратные таймеры для точного управления временем.
---
@@ -26,36 +26,37 @@
1. **Управление мощностью переменного тока**: Управление мощностью нагрузки от 0% до 100%
2. **Детекция перехода через ноль**: Использует прерывание GPIO для точного тайминга
3. **Управление TRIAC**: Управление затвором TRIAC с точным таймингом
4. **Аппаратный таймер**: Использует GPTimer для точного управления затуханием
5. **Обработка ошибок**: Комплексная проверка ошибок и валидация
6. **Потокобезопасность**: Использует драйверы ESP-IDF (потокобезопасно)
7. **Минимальный объем памяти**: Низкие накладные расходы на память
8. **Простой API**: Легкое в использовании интерфейс
3. **Управление TRIAC**: Управление затвором TRIAC с точным таймингом через GPTimer
4. **Аппаратный таймер**: Использует GPTimer с разрешением 1 МГц (1 мкс)
5. **IRAM-safe ISR и callback**: Прерывания и колбэки размещаются в IRAM для надёжной работы
6. **Обработка ошибок**: Комплексная проверка ошибок и валидация конфигурации
7. **Потокобезопасность**: Использует драйверы ESP-IDF (потокобезопасно)
8. **Минимальный объем памяти**: Низкие накладные расходы на память
9. **Простой API**: Легковесный интерфейс с поддержкой start/stop/set/get
---
## Установка
1. Перейдите в каталог компонентов вашего проекта:
Перейдите в каталог компонентов вашего проекта:
```bash
cd ../ваш_проект/components
```
2. Клонируйте репозиторий:
Клонируйте репозиторий:
```bash
git clone https://github.com/aZholtikov/zh_ac_dimmer.git
```
3. В вашем приложении подключите заголовочный файл:
В вашем приложении подключите заголовочный файл:
```c
#include "zh_ac_dimmer.h"
```
4. Компонент будет автоматически собран вместе с вашим проектом.
Компонент будет автоматически собран вместе с вашим проектом.
### Необходимая конфигурация
@@ -89,28 +90,43 @@ typedef struct
---
### Структура zh_ac_dimmer_handle_t
```c
typedef struct _zh_ac_dimmer_handle_t zh_ac_dimmer_handle_t;
```
Непрозрачный дескриптор экземпляра устройства.
---
### zh_ac_dimmer_init()
Инициализирует управление освещением переменного тока.
Выделяет память для handle, валидирует конфигурацию, инициализирует GPIO пины (TRIAC как выход, переход через ноль как вход с прерыванием по положительному фронту), устанавливает ISR-сервис и создаёт экземпляр gptimer.
**Параметры:**
- `config` - Указатель на структуру конфигурации инициализации освещения переменного тока
- `config` - Указатель на структуру конфигурации инициализации. Не должен быть NULL
- `handle` - Указатель на указатель для получения созданного handle. Должен быть NULL
**Возвращает:**
- `ESP_OK` - Успех
- `ESP_ERR_INVALID_ARG` - Неверный аргумент (NULL config или неверный GPIO)
- `ESP_ERR_INVALID_ARG` - Неверный аргумент (NULL handle или неверный GPIO)
- `ESP_ERR_INVALID_STATE` - Освещение переменного тока уже инициализировано
- `ESP_FAIL` - Ошибка инициализации (GPIO или таймер)
- `ESP_ERR_NO_MEM` - Не удалось выделить память
- `ESP_FAIL` - Ошибка инициализации (GPIO, ISR-сервис или таймер)
**Пример:**
```c
zh_ac_dimmer_handle_t *dimmer = NULL;
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);
esp_err_t err = zh_ac_dimmer_init(&config, &dimmer);
```
---
@@ -119,11 +135,19 @@ zh_ac_dimmer_init(&config);
Деинициализирует управление освещением переменного тока и освобождает ресурсы.
Останавливает и удаляет gptimer, удаляет ISR-обработчик GPIO, сбрасывает пины GPIO в режим ввода, освобождает handle и устанавливает выходной указатель в NULL.
**Параметры:**
- `handle` - Указатель на handle. Не должен быть NULL
**Возвращает:**
- `ESP_OK` - Успех
- `ESP_ERR_INVALID_STATE` - Освещение переменного тока не инициализировано
- `ESP_FAIL` - Ошибка деинициализации (GPIO или деинициализация таймера)
- `ESP_ERR_INVALID_ARG` - handle или *handle равен NULL
- `ESP_FAIL` - Ошибка остановки таймера, отключения или удаления, или сброса GPIO
**Важно:** После вызова этой функции handle становится недействительным. Для повторной инициализации вызовите `zh_ac_dimmer_init()` заново.
---
@@ -131,10 +155,16 @@ zh_ac_dimmer_init(&config);
Запускает работу освещения переменного тока.
Включает диммер, чтобы события перехода через ноль запускали срабатывание TRIAC в соответствии с текущим значением затухания.
**Параметры:**
- `handle` - Указатель на handle. Не должен быть NULL
**Возвращает:**
- `ESP_OK` - Успех
- `ESP_ERR_INVALID_STATE` - Освещение переменного тока не инициализировано
- `ESP_ERR_INVALID_ARG` - handle или *handle равен NULL
---
@@ -142,10 +172,16 @@ zh_ac_dimmer_init(&config);
Останавливает работу освещения переменного тока.
Отключает срабатывание TRIAC на событиях перехода через ноль. Диммер может быть перезапущен с помощью `zh_ac_dimmer_start()`.
**Параметры:**
- `handle` - Указатель на handle. Не должен быть NULL
**Возвращает:**
- `ESP_OK` - Успех
- `ESP_ERR_INVALID_STATE` - Освещение переменного тока не инициализировано
- `ESP_ERR_INVALID_ARG` - handle или *handle равен NULL
---
@@ -153,15 +189,17 @@ zh_ac_dimmer_init(&config);
Устанавливает значение затухания освещения переменного тока.
Устанавливает процент затухания, который будет применён на следующем событии перехода через ноль.
**Параметры:**
- `value` - Значение затухания (0 до 100, где 0 = выкл, 100 = полная мощность)
- `handle` - Указатель на handle. Не должен быть NULL
- `value` - Уровень затухания от 0 (выключено) до 100 (полная мощность)
**Возвращает:**
- `ESP_OK` - Успех
- `ESP_ERR_INVALID_STATE` - Освещение переменного тока не инициализировано
- `ESP_ERR_INVALID_ARG` - Неверное значение затухания
- `ESP_ERR_INVALID_ARG` - handle, *handle или value недействительны
---
@@ -171,13 +209,13 @@ zh_ac_dimmer_init(&config);
**Параметры:**
- `value` - Указатель для хранения значения затухания
- `handle` - Указатель на handle. Не должен быть NULL
- `value` - Указатель для получения текущего уровня затухания (0–100). Не должен быть NULL
**Возвращает:**
- `ESP_OK` - Успех
- `ESP_ERR_INVALID_STATE` - Освещение переменного тока не инициализировано
- `ESP_ERR_INVALID_ARG` - Неверный аргумент (NULL value)
- `ESP_ERR_INVALID_ARG` - handle, *handle или value равен NULL
---
@@ -191,28 +229,29 @@ zh_ac_dimmer_init(&config);
void app_main(void)
{
esp_log_level_set("zh_ac_dimmer", ESP_LOG_ERROR);
zh_ac_dimmer_handle_t *dimmer = NULL;
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);
zh_ac_dimmer_start();
zh_ac_dimmer_init(&config, &dimmer);
zh_ac_dimmer_start(&dimmer);
uint8_t value = 0;
for (uint8_t i = 0; i <= 100; ++i)
{
zh_ac_dimmer_set(i);
zh_ac_dimmer_get(&value);
zh_ac_dimmer_set(&dimmer, i);
zh_ac_dimmer_get(&dimmer, &value);
printf("Значение затухания: %d.\n", value);
vTaskDelay(100 / portTICK_PERIOD_MS);
}
for (uint8_t i = 100; i > 0; --i)
{
zh_ac_dimmer_set(i);
zh_ac_dimmer_get(&value);
zh_ac_dimmer_set(&dimmer, i);
zh_ac_dimmer_get(&dimmer, &value);
printf("Значение затухания: %d.\n", value);
vTaskDelay(100 / portTICK_PERIOD_MS);
}
zh_ac_dimmer_stop();
zh_ac_dimmer_deinit();
zh_ac_dimmer_stop(&dimmer);
zh_ac_dimmer_deinit(&dimmer);
}
```
@@ -228,15 +267,17 @@ void app_main(void)
#define BUTTON_GPIO GPIO_NUM_0
static uint8_t dimmer_value = 50;
static zh_ac_dimmer_handle_t *dimmer = NULL;
static void IRAM_ATTR button_isr_handler(void *arg)
{
(void)arg;
dimmer_value = (dimmer_value + 10) % 110;
if (dimmer_value > 100)
{
dimmer_value = 0;
}
zh_ac_dimmer_set(dimmer_value);
zh_ac_dimmer_set(&dimmer, dimmer_value);
printf("Значение затухания: %d\n", dimmer_value);
}
@@ -247,7 +288,7 @@ void app_main(void)
zh_ac_dimmer_init_config_t config = ZH_AC_DIMMER_INIT_CONFIG_DEFAULT();
config.zero_cross_gpio = ZERO_CROSS_GPIO;
config.triac_gpio = TRIAC_GPIO;
zh_ac_dimmer_init(&config);
zh_ac_dimmer_init(&config, &dimmer);
// Инициализация кнопки
gpio_config_t button_config = {
.intr_type = GPIO_INTR_POSEDGE,
@@ -260,13 +301,13 @@ void app_main(void)
gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);
gpio_isr_handler_add(BUTTON_GPIO, button_isr_handler, NULL);
// Запуск затухания
zh_ac_dimmer_start();
zh_ac_dimmer_start(&dimmer);
printf("Затухание инициализировано. Кнопка на GPIO %d меняет значение затухания.\n", BUTTON_GPIO);
for (;;)
{
vTaskDelay(1000 / portTICK_PERIOD_MS);
uint8_t current_value;
zh_ac_dimmer_get(&current_value);
zh_ac_dimmer_get(&dimmer, &current_value);
printf("Текущее значение затухания: %d\n", current_value);
}
}
@@ -278,12 +319,13 @@ void app_main(void)
| Параметр | Значение |
|----------|----------|
| **Диапазон затухания** | 0 - 100% |
| **Частота переменного тока** | 400 Гц (поддерживается только) |
| **Диапазон затухания** | 0–100% (0 = выкл, 100 = полная мощность) |
| **Частота переменного тока** | 400 Гц (период перехода через ноль: 1250 мкс) |
| **Детекция перехода через ноль** | Прерывание GPIO (положительный фронт) |
| **Управление TRIAC** | Выход GPIO |
| **Разрешение таймера** | 1 МГц (1 мкс) |
| **Время измерения** | ~1250 мкс на цикл |
| **Время измерения** | ~1250 мкс на полупериод |
| **Базовая задержка TRIAC** | 330 мкс (измерено логическим анализатором) |
| **Версия ESP-IDF** | >= 5.0 |
| **Платформа** | Семейство ESP32 |
| **Язык** | C (C99) |
@@ -296,8 +338,9 @@ void app_main(void)
|------------|----------|
| `ESP_OK` | Операция выполнена успешно |
| `ESP_ERR_INVALID_ARG` | Неверный аргумент (NULL указатель, неверный GPIO или неверное значение затухания) |
| `ESP_ERR_INVALID_STATE` | Освещение переменного тока не инициализировано |
| `ESP_FAIL` | Общая ошибка (GPIO или инициализация/деинициализация таймера) |
| `ESP_ERR_INVALID_STATE` | Освещение переменного тока уже инициализировано (только для `zh_ac_dimmer_init`) |
| `ESP_ERR_NO_MEM` | Не удалось выделить память (только для `zh_ac_dimmer_init`) |
| `ESP_FAIL` | Общая ошибка (GPIO, ISR-сервис, инициализация/деинициализация таймера) |
---
@@ -339,12 +382,13 @@ void app_main(void)
## Дополнительные заметки
- **Частота переменного тока**: В настоящее время поддерживается только частота 400Гц
- **Тайминг перехода через ноль**: 1250 мкс период (400Гц) с компенсацией 330 мкс для задержки таймера
- **Прерывание GPIO**: Детекция перехода через ноль использует прерывание положительного фронта
- **Требования IRAM**: Для правильной работы включите `GPIO_CTRL_FUNC_IN_IRAM`, `CONFIG_GPTIMER_ISR_HANDLER_IN_IRAM`, `CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM` и `CONFIG_GPTIMER_ISR_CACHE_SAFE` в menuconfig
- **Потокобезопасность**: Компонент использует драйверы ESP-IDF, которые являются потокобезопасными
- **Частота переменного тока**: В настоящее время поддерживается только частота 400 Гц (авиационная частота). Период между переходами через ноль составляет 1250 мкс.
- **Тайминг перехода через ноль**: 1250 мкс период (400 Гц) с компенсацией 330 мкс для задержки таймера. Формула расчёта: `alarm_count = (((1250 - 330) / 100) * (100 - dimmer_value)) + 330`.
- **Прерывание GPIO**: Детекция перехода через ноль использует прерывание положительного фронта с защитой от дребезга (игнорируются прерывания, пришедшие ранее 90% от периода).
- **Требования IRAM**: Для правильной работы включите `GPIO_CTRL_FUNC_IN_IRAM`, `CONFIG_GPTIMER_ISR_HANDLER_IN_IRAM`, `CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM` и `CONFIG_GPTIMER_ISR_CACHE_SAFE` в menuconfig.
- **Потокобезопасность**: Компонент использует драйверы ESP-IDF, которые являются потокобезопасными.
- **Безопасность**: Данный модуль взаимодействует с сетевым напряжением. Неправильное подключение или недостаточная изоляция могут привести к поражению электрическим током или повреждению оборудования. Убедитесь в наличии правильной оптоизоляции и соблюдении требований безопасности перед развёртыванием.
---
*Сгенерировано для zh_ac_dimmer v1.0.0*
*Сгенерировано для zh_ac_dimmer v3.0.0*
+1 -1
View File
@@ -1,6 +1,6 @@
description: ESP32 ESP-IDF component for AC dimmer control
url: https://github.com/aZholtikov/zh_ac_dimmer
version: "1.0.0"
version: "3.0.0"
license: "Apache-2.0"
maintainers:
- "Alexey Zholtikov <alexey.zholtikov@yandex.ru>"
+94 -30
View File
@@ -1,5 +1,24 @@
/**
* @file zh_ac_dimmer.h
*
* @brief Header file for AC dimmer driver based on zero-crossing detection and TRIAC control.
*
* This module provides an interface for controlling AC power via phase-angle
* dimming using ESP-IDF gptimer and GPIO peripherals. It detects the AC
* mains zero-crossing point and fires the TRIAC with a programmable delay
* to regulate the average power delivered to the load.
*
* Key features:
* - Zero-crossing detection via GPIO interrupt
* - TRIAC gate control through ESP-IDF gptimer
* - Configurable dimming level (0–100 %)
* - Thread-safe start/stop/set/get API
*
* @note The driver assumes a 400 Hz AC mains frequency (1250 µs period).
* Adjust the timing constants in `zh_ac_dimmer.c` for other frequencies.
* @warning This module interacts with mains voltage. Incorrect wiring or
* insufficient isolation can cause electric shock or hardware damage.
* Ensure proper opto-isolation and safety compliance before deployment.
*/
#pragma once
@@ -11,9 +30,6 @@
#include "esp_log.h"
#include "driver/gpio.h"
/**
* @brief AC dimmer initial default values.
*/
#define ZH_AC_DIMMER_INIT_CONFIG_DEFAULT() \
{ \
.zero_cross_gpio = GPIO_NUM_MAX, \
@@ -25,65 +41,113 @@ extern "C"
#endif
/**
* @brief Structure for initial initialization of AC dimmer.
* @brief Opaque handle representing an AC dimmer instance.
*
* Internal fields are defined in `zh_ac_dimmer.c` and must not be
* accessed directly by user code.
*/
typedef struct _zh_ac_dimmer_handle_t zh_ac_dimmer_handle_t;
/**
* @brief Configuration structure for AC dimmer initialization.
*
* Specifies the GPIO pins used for zero-crossing detection and TRIAC
* gate control. Both pins must be valid and different from each other.
*/
typedef struct
{
gpio_num_t zero_cross_gpio; /*!< Zero cross GPIO. */
gpio_num_t triac_gpio; /*!< Triac GPIO. */
gpio_num_t zero_cross_gpio; /*!< GPIO pin for zero-crossing detection input */
gpio_num_t triac_gpio; /*!< GPIO pin for TRIAC gate control output */
} zh_ac_dimmer_init_config_t;
/**
* @brief Initialize AC dimmer.
* @brief Initialize the AC dimmer driver.
*
* @param[in] config Pointer to AC dimmer initialized configuration structure. Can point to a temporary variable.
* Allocates memory for the dimmer handle, validates the configuration,
* initializes GPIO pins (TRIAC as output, zero-crossing as input with
* positive-edge interrupt), installs the ISR service, and creates the
* gptimer instance.
*
* @note Before initialize the AC dimmer recommend initialize zh_ac_dimmer_init_config_t structure with default values.
* @param[in] config Pointer to the initialization configuration structure.
* May point to a temporary variable (not stored internally)
* @param[out] handle Pointer to receive the created dimmer handle (must be NULL)
*
* @code zh_ac_dimmer_init_config_t config = ZH_AC_DIMMER_INIT_CONFIG_DEFAULT @endcode
*
* @return ESP_OK if success or an error code otherwise.
* @return ESP_OK on success
* @return ESP_ERR_INVALID_ARG if handle is NULL or GPIO pins are invalid
* @return ESP_ERR_INVALID_STATE if a dimmer instance is already initialized
* @return ESP_ERR_NO_MEM if memory allocation failed
* @return ESP_FAIL if GPIO, ISR service, or timer initialization failed
*/
esp_err_t zh_ac_dimmer_init(const zh_ac_dimmer_init_config_t *config);
esp_err_t zh_ac_dimmer_init(const zh_ac_dimmer_init_config_t *config, zh_ac_dimmer_handle_t **handle);
/**
* @brief Deinitialize AC dimmer.
* @brief Deinitialize the AC dimmer driver.
*
* @return ESP_OK if success or an error code otherwise.
* Stops and deletes the gptimer, removes the GPIO ISR handler, resets
* the GPIO pins to input mode, frees the dimmer handle, and sets the
* output handle to NULL.
*
* @param[in,out] handle Pointer to the dimmer handle (must not be NULL)
*
* @return ESP_OK on success
* @return ESP_ERR_INVALID_ARG if handle or *handle is NULL
* @return ESP_FAIL if timer or GPIO cleanup failed
*
* @warning After calling this function, the handle is invalid and must not
* be used. Call `zh_ac_dimmer_init()` again to reinitialize.
*/
esp_err_t zh_ac_dimmer_deinit(void);
esp_err_t zh_ac_dimmer_deinit(zh_ac_dimmer_handle_t **handle);
/**
* @brief Start AC dimmer.
* @brief Start the AC dimmer.
*
* @return ESP_OK if success or an error code otherwise.
* Enables the dimmer so that zero-crossing events trigger TRIAC firing
* according to the current dimming value.
*
* @param[in,out] handle Pointer to the dimmer handle (must not be NULL)
*
* @return ESP_OK on success
* @return ESP_ERR_INVALID_ARG if handle or *handle is NULL
*/
esp_err_t zh_ac_dimmer_start(void);
esp_err_t zh_ac_dimmer_start(zh_ac_dimmer_handle_t **handle);
/**
* @brief Stop AC dimmer.
* @brief Stop the AC dimmer.
*
* @return ESP_OK if success or an error code otherwise.
* Disables TRIAC firing on zero-crossing events. The dimmer can be
* restarted with `zh_ac_dimmer_start()`.
*
* @param[in,out] handle Pointer to the dimmer handle (must not be NULL)
*
* @return ESP_OK on success
* @return ESP_ERR_INVALID_ARG if handle or *handle is NULL
*/
esp_err_t zh_ac_dimmer_stop(void);
esp_err_t zh_ac_dimmer_stop(zh_ac_dimmer_handle_t **handle);
/**
* @brief Set AC dimmer dimming value.
* @brief Set the AC dimmer brightness level.
*
* @param[in] value Dimming value (0 to 100).
* Sets the dimming percentage that will be applied on the next
* zero-crossing event.
*
* @return ESP_OK if success or an error code otherwise.
* @param[in] handle Pointer to the dimmer handle (must not be NULL)
* @param[in] value Dimming level from 0 (off) to 100 (full brightness)
*
* @return ESP_OK on success
* @return ESP_ERR_INVALID_ARG if handle, *handle, or value is invalid
*/
esp_err_t zh_ac_dimmer_set(uint8_t value);
esp_err_t zh_ac_dimmer_set(zh_ac_dimmer_handle_t **handle, uint8_t value);
/**
* @brief Get AC dimmer dimming value.
* @brief Get the current AC dimmer brightness level.
*
* @param[out] value Pointer to dimming value.
* @param[in] handle Pointer to the dimmer handle (must not be NULL)
* @param[out] value Pointer to receive the current dimming level (0–100) (must not be NULL)
*
* @return ESP_OK if success or an error code otherwise.
* @return ESP_OK on success
* @return ESP_ERR_INVALID_ARG if handle, *handle, or value is NULL
*/
esp_err_t zh_ac_dimmer_get(uint8_t *value);
esp_err_t zh_ac_dimmer_get(zh_ac_dimmer_handle_t **handle, uint8_t *value);
#ifdef __cplusplus
}
+1 -1
View File
@@ -1 +1 @@
2.1.0
3.0.0
+162 -79
View File
@@ -13,90 +13,166 @@ static const char *TAG = "zh_ac_dimmer";
return err; \
}
static gptimer_handle_t _dimmer_timer = NULL;
static gptimer_alarm_config_t _alarm_config = {0};
static zh_ac_dimmer_init_config_t _init_config = {0};
volatile static uint64_t _prev_us = 0;
volatile static uint8_t _dimmer_value = 0;
volatile static bool _is_dimmer_work = false;
volatile static bool _is_initialized = false;
/**
* @brief Internal handle for AC dimmer instance.
*
* Stores GPIO pin assignments, gptimer handle, timing state,
* current dimming level, and running state.
*/
struct _zh_ac_dimmer_handle_t
{
gpio_num_t zero_cross_gpio; /*!< GPIO pin for zero-crossing detection input */
gpio_num_t triac_gpio; /*!< GPIO pin for TRIAC gate control output */
gptimer_handle_t dimmer_timer_handle; /*!< gptimer instance used for TRIAC firing delay */
uint64_t prev_zero_crossing_us; /*!< Timestamp of the last zero-crossing event (µs) */
uint8_t dimmer_value; /*!< Current dimming level (0 = off, 100 = full) */
bool is_dimmer_work; /*!< `true` when the dimmer is actively running */
};
/**
* @brief Validate the AC dimmer initialization configuration.
*
* Checks that both GPIO pins are valid (not `GPIO_NUM_MAX`),
* are within the acceptable range, and are not the same pin.
*
* @param config Pointer to the initialization configuration structure
*
* @return ESP_OK if the configuration is valid
* @return ESP_ERR_INVALID_ARG if any GPIO pin is invalid or both pins are the same
*/
static esp_err_t _zh_ac_dimmer_validate_config(const zh_ac_dimmer_init_config_t *config);
static esp_err_t _zh_ac_dimmer_gpio_init(const zh_ac_dimmer_init_config_t *config);
static esp_err_t _zh_ac_dimmer_timer_init(void);
/**
* @brief Initialize GPIO pins for TRIAC control and zero-crossing detection.
*
* Configures the TRIAC GPIO as an output (pulled low) and the zero-crossing
* GPIO as an input with pull-down and positive-edge interrupt. Installs the
* ESP-IDF ISR service (if not already installed) and registers the ISR
* handler for the zero-crossing pin.
*
* @param config Pointer to the initialization configuration structure
* @param handle Pointer to the dimmer handle to store GPIO pin numbers
*
* @return ESP_OK on success
* @return ESP_FAIL if GPIO configuration, ISR service installation, or ISR
* handler registration failed
*
* @note `ESP_ERR_INVALID_STATE` from `gpio_install_isr_service()` is treated
* as success because the ISR service may have been installed previously.
*/
static esp_err_t _zh_ac_dimmer_gpio_init(const zh_ac_dimmer_init_config_t *config, zh_ac_dimmer_handle_t *handle);
/**
* @brief Initialize the gptimer used for TRIAC firing delay.
*
* Creates a new gptimer with a 1 MHz resolution (1 µs steps), registers
* the alarm callback that fires the TRIAC, enables the timer, and starts
* it in auto-reload mode.
*
* @param handle Pointer to the dimmer handle (timer handle stored here)
*
* @return ESP_OK on success
* @return ESP_FAIL if timer creation, callback registration, or enable failed
*/
static esp_err_t _zh_ac_dimmer_timer_init(zh_ac_dimmer_handle_t *handle);
/**
* @brief ISR handler for zero-crossing detection.
*
* Executed on every positive edge of the zero-crossing signal. Resets the
* TRIAC gate low, updates the last zero-crossing timestamp, and schedules
* a gptimer alarm to fire the TRIAC at the appropriate phase angle.
*
* - At dimming = 0: TRIAC stays off (gate held low).
* - At dimming = 100: TRIAC fires immediately (gate set high).
* - At dimming 1–99: gptimer alarm fires after a calculated delay.
*
* @param arg Pointer to the dimmer handle (passed from GPIO ISR)
*/
static void _zh_ac_dimmer_isr_handler(void *arg);
/**
* @brief gptimer alarm callback that fires the TRIAC.
*
* Sets the TRIAC gate high to trigger conduction, then stops and resets
* the timer for the next half-cycle.
*
* @param timer Handle to the gptimer that triggered the alarm
* @param edata Event data (unused)
* @param user_ctx Pointer to the dimmer handle
*
* @return `false` to indicate that the timer should not auto-restart
*/
static bool _zh_ac_dimmer_timer_on_alarm_cb(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx);
esp_err_t zh_ac_dimmer_init(const zh_ac_dimmer_init_config_t *config) // -V2008
esp_err_t zh_ac_dimmer_init(const zh_ac_dimmer_init_config_t *config, zh_ac_dimmer_handle_t **handle) // -V2008
{
ZH_LOGI("AC dimmer initialization started.");
ZH_ERROR_CHECK(config != NULL, ESP_ERR_INVALID_ARG, NULL, "AC dimmer initialization failed. Invalid argument.");
ZH_ERROR_CHECK(_is_initialized == false, ESP_ERR_INVALID_STATE, NULL, "AC dimmer initialization failed. AC dimmer is already initialized.");
ZH_ERROR_CHECK(_zh_ac_dimmer_validate_config(config) == ESP_OK, ESP_FAIL, NULL, "AC dimmer initialization failed. Initial configuration check failed.");
ZH_ERROR_CHECK(_zh_ac_dimmer_gpio_init(config) == ESP_OK, ESP_FAIL, NULL, "AC dimmer initialization failed. GPIO initialization failed.");
ZH_ERROR_CHECK(_zh_ac_dimmer_timer_init() == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(gpio_isr_handler_remove(config->zero_cross_gpio) == ESP_OK, ESP_FAIL, NULL, "Remove GPIO isr handler failed.");
ZH_ERROR_CHECK(gpio_reset_pin(config->triac_gpio) == ESP_OK, ESP_FAIL, NULL, "Reset GPIO failed.");
ZH_ERROR_CHECK(gpio_reset_pin(config->zero_cross_gpio) == ESP_OK, ESP_FAIL, NULL, "Reset GPIO failed."), "AC dimmer initialization failed. Timer initialization failed.");
_init_config = *config;
_is_initialized = true;
ZH_ERROR_CHECK(handle != NULL, ESP_ERR_INVALID_ARG, NULL, "AC dimmer initialization failed. Invalid argument.");
ZH_ERROR_CHECK(*handle == NULL, ESP_ERR_INVALID_STATE, NULL, "AC dimmer initialization failed. AC dimmer is already initialized.");
*handle = heap_caps_calloc(1, sizeof(zh_ac_dimmer_handle_t), MALLOC_CAP_8BIT);
ZH_ERROR_CHECK(*handle != NULL, ESP_ERR_NO_MEM, NULL, "AC dimmer initialization failed. Failed to allocate 160X handle.");
ZH_ERROR_CHECK(_zh_ac_dimmer_validate_config(config) == ESP_OK, ESP_FAIL, heap_caps_free(*handle); *handle = NULL, "AC dimmer initialization failed. Initial configuration check failed.");
ZH_ERROR_CHECK(_zh_ac_dimmer_gpio_init(config, *handle) == ESP_OK, ESP_FAIL, heap_caps_free(*handle); *handle = NULL, "AC dimmer initialization failed. GPIO initialization failed.");
ZH_ERROR_CHECK(_zh_ac_dimmer_timer_init(*handle) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK(gpio_isr_handler_remove(config->zero_cross_gpio) == ESP_OK, ESP_FAIL, NULL, "Remove GPIO isr handler failed.")};
{ZH_ERROR_CHECK(gpio_reset_pin(config->triac_gpio) == ESP_OK, ESP_FAIL, NULL, "Reset GPIO failed.")};
{ZH_ERROR_CHECK(gpio_reset_pin(config->zero_cross_gpio) == ESP_OK, ESP_FAIL, NULL, "Reset GPIO failed.")};
heap_caps_free(*handle); *handle = NULL, "AC dimmer initialization failed. Timer initialization failed.");
ZH_LOGI("AC dimmer initialization completed successfully.");
return ESP_OK;
}
esp_err_t zh_ac_dimmer_deinit(void) // -V2008
esp_err_t zh_ac_dimmer_deinit(zh_ac_dimmer_handle_t **handle) // -V2008
{
ZH_LOGI("AC dimmer deinitialization started.");
ZH_ERROR_CHECK(_is_initialized == true, ESP_ERR_INVALID_STATE, NULL, "AC dimmer deinitialization failed. AC dimmer is not initialized.");
ZH_ERROR_CHECK(gptimer_stop(_dimmer_timer) == ESP_OK, ESP_FAIL, NULL, "AC dimmer deinitialization failed. Timer stop fail.");
ZH_ERROR_CHECK(gptimer_disable(_dimmer_timer) == ESP_OK, ESP_FAIL, NULL, "AC dimmer deinitialization failed. Timer disable fail.");
ZH_ERROR_CHECK(gptimer_del_timer(_dimmer_timer) == ESP_OK, ESP_FAIL, NULL, "AC dimmer deinitialization failed. Timer delete fail.");
ZH_ERROR_CHECK(gpio_isr_handler_remove(_init_config.zero_cross_gpio) == ESP_OK, ESP_FAIL, NULL, "AC dimmer deinitialization failed. Remove GPIO isr handler failed.");
ZH_ERROR_CHECK(gpio_reset_pin(_init_config.triac_gpio) == ESP_OK, ESP_FAIL, NULL, "AC dimmer deinitialization failed. Reset GPIO failed.")
ZH_ERROR_CHECK(gpio_reset_pin(_init_config.zero_cross_gpio) == ESP_OK, ESP_FAIL, NULL, "AC dimmer deinitialization failed. Reset GPIO failed.")
_is_dimmer_work = false;
_is_initialized = false;
_dimmer_value = 0;
_prev_us = 0;
ZH_ERROR_CHECK(handle != NULL && *handle != NULL, ESP_ERR_INVALID_ARG, NULL, "AC dimmer deinitialization failed. Invalid argument.");
ZH_ERROR_CHECK(gptimer_stop((*handle)->dimmer_timer_handle) == ESP_OK, ESP_FAIL, NULL, "AC dimmer deinitialization failed. Timer stop fail.");
ZH_ERROR_CHECK(gptimer_disable((*handle)->dimmer_timer_handle) == ESP_OK, ESP_FAIL, NULL, "AC dimmer deinitialization failed. Timer disable fail.");
ZH_ERROR_CHECK(gptimer_del_timer((*handle)->dimmer_timer_handle) == ESP_OK, ESP_FAIL, NULL, "AC dimmer deinitialization failed. Timer delete fail.");
ZH_ERROR_CHECK(gpio_isr_handler_remove((*handle)->zero_cross_gpio) == ESP_OK, ESP_FAIL, NULL, "AC dimmer deinitialization failed. Remove GPIO isr handler failed.");
ZH_ERROR_CHECK(gpio_reset_pin((*handle)->triac_gpio) == ESP_OK, ESP_FAIL, NULL, "AC dimmer deinitialization failed. Reset GPIO failed.");
ZH_ERROR_CHECK(gpio_reset_pin((*handle)->zero_cross_gpio) == ESP_OK, ESP_FAIL, NULL, "AC dimmer deinitialization failed. Reset GPIO failed.");
heap_caps_free(*handle);
*handle = NULL;
ZH_LOGI("AC dimmer deinitialization completed successfully.");
return ESP_OK;
}
esp_err_t zh_ac_dimmer_start(void)
esp_err_t zh_ac_dimmer_start(zh_ac_dimmer_handle_t **handle)
{
ZH_LOGI("AC dimmer start begin.");
ZH_ERROR_CHECK(_is_initialized == true, ESP_ERR_INVALID_STATE, NULL, "AC dimmer start failed. AC dimmer is not initialized.");
_is_dimmer_work = true;
ZH_ERROR_CHECK(handle != NULL && *handle != NULL, ESP_ERR_INVALID_ARG, NULL, "AC dimmer start failed. Invalid argument.");
(*handle)->is_dimmer_work = true;
ZH_LOGI("AC dimmer start completed successfully.");
return ESP_OK;
}
esp_err_t zh_ac_dimmer_stop(void)
esp_err_t zh_ac_dimmer_stop(zh_ac_dimmer_handle_t **handle)
{
ZH_LOGI("AC dimmer stop begin.");
ZH_ERROR_CHECK(_is_initialized == true, ESP_ERR_INVALID_STATE, NULL, "AC dimmer stop failed. AC dimmer is not initialized.");
_is_dimmer_work = false;
ZH_ERROR_CHECK(handle != NULL && *handle != NULL, ESP_ERR_INVALID_ARG, NULL, "AC dimmer stop failed. Invalid argument.");
(*handle)->is_dimmer_work = false;
ZH_LOGI("AC dimmer stop completed successfully.");
return ESP_OK;
}
esp_err_t zh_ac_dimmer_set(uint8_t value)
esp_err_t zh_ac_dimmer_set(zh_ac_dimmer_handle_t **handle, uint8_t value)
{
ZH_LOGI("AC dimmer setup begin.");
ZH_ERROR_CHECK(_is_initialized == true, ESP_ERR_INVALID_STATE, NULL, "AC dimmer setup failed. AC dimmer is not initialized.");
ZH_ERROR_CHECK(handle != NULL && *handle != NULL, ESP_ERR_INVALID_ARG, NULL, "AC dimmer setup failed. Invalid argument.");
ZH_ERROR_CHECK(value <= 100, ESP_ERR_INVALID_ARG, NULL, "AC dimmer setup failed. Dimming value invalid.");
_dimmer_value = value;
(*handle)->dimmer_value = value;
ZH_LOGI("AC dimmer setup completed successfully.");
return ESP_OK;
}
esp_err_t zh_ac_dimmer_get(uint8_t *value)
esp_err_t zh_ac_dimmer_get(zh_ac_dimmer_handle_t **handle, uint8_t *value)
{
ZH_LOGI("AC dimmer getting status begin.");
ZH_ERROR_CHECK(value != NULL, ESP_ERR_INVALID_ARG, NULL, "AC dimmer getting status failed. Invalid argument.");
ZH_ERROR_CHECK(_is_initialized == true, ESP_ERR_INVALID_STATE, NULL, "AC dimmer getting status failed. AC dimmer is not initialized.");
*value = _dimmer_value;
ZH_ERROR_CHECK(handle != NULL && *handle != NULL && value != NULL, ESP_ERR_INVALID_ARG, NULL, "AC dimmer getting status failed. Invalid argument.");
*value = (*handle)->dimmer_value;
ZH_LOGI("AC dimmer getting status completed successfully.");
return ESP_OK;
}
@@ -109,7 +185,7 @@ static esp_err_t _zh_ac_dimmer_validate_config(const zh_ac_dimmer_init_config_t
return ESP_OK;
}
static esp_err_t _zh_ac_dimmer_gpio_init(const zh_ac_dimmer_init_config_t *config) // -V2008
static esp_err_t _zh_ac_dimmer_gpio_init(const zh_ac_dimmer_init_config_t *config, zh_ac_dimmer_handle_t *handle) // -V2008
{
gpio_config_t triac_gpio_config = {
.intr_type = GPIO_INTR_DISABLE,
@@ -128,66 +204,73 @@ static esp_err_t _zh_ac_dimmer_gpio_init(const zh_ac_dimmer_init_config_t *confi
.pull_up_en = GPIO_PULLUP_DISABLE,
};
ZH_ERROR_CHECK(gpio_config(&zero_cross_gpio_config) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(gpio_reset_pin(config->triac_gpio) == ESP_OK, ESP_FAIL, NULL, "Reset GPIO failed."), "Zero cross GPIO configuration failed.");
{ZH_ERROR_CHECK(gpio_reset_pin(config->triac_gpio) == ESP_OK, ESP_FAIL, NULL, "Reset GPIO failed.")}, "Zero cross GPIO configuration failed.");
esp_err_t err = gpio_install_isr_service(ESP_INTR_FLAG_LOWMED);
ZH_ERROR_CHECK(err == ESP_OK || err == ESP_ERR_INVALID_STATE, ESP_FAIL,
ZH_ERROR_CHECK(gpio_reset_pin(config->triac_gpio) == ESP_OK, ESP_FAIL, NULL, "Reset GPIO failed.");
ZH_ERROR_CHECK(gpio_reset_pin(config->zero_cross_gpio) == ESP_OK, ESP_FAIL, NULL, "Reset GPIO failed."), "Failed install isr service.")
ZH_ERROR_CHECK(gpio_isr_handler_add(config->zero_cross_gpio, _zh_ac_dimmer_isr_handler, NULL) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(gpio_reset_pin(config->triac_gpio) == ESP_OK, ESP_FAIL, NULL, "Reset GPIO failed.");
ZH_ERROR_CHECK(gpio_reset_pin(config->zero_cross_gpio) == ESP_OK, ESP_FAIL, NULL, "Reset GPIO failed."), "Failed add isr handler.");
{ZH_ERROR_CHECK(gpio_reset_pin(config->triac_gpio) == ESP_OK, ESP_FAIL, NULL, "Reset GPIO failed.")};
{ZH_ERROR_CHECK(gpio_reset_pin(config->zero_cross_gpio) == ESP_OK, ESP_FAIL, NULL, "Reset GPIO failed.")}, "Failed install isr service.")
ZH_ERROR_CHECK(gpio_isr_handler_add(config->zero_cross_gpio, _zh_ac_dimmer_isr_handler, handle) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK(gpio_reset_pin(config->triac_gpio) == ESP_OK, ESP_FAIL, NULL, "Reset GPIO failed.")};
{ZH_ERROR_CHECK(gpio_reset_pin(config->zero_cross_gpio) == ESP_OK, ESP_FAIL, NULL, "Reset GPIO failed.")}, "Failed add isr handler.");
handle->triac_gpio = config->triac_gpio;
handle->zero_cross_gpio = config->zero_cross_gpio;
return ESP_OK;
}
static esp_err_t _zh_ac_dimmer_timer_init(void)
static esp_err_t _zh_ac_dimmer_timer_init(zh_ac_dimmer_handle_t *handle)
{
gptimer_config_t timer_config = {
.clk_src = GPTIMER_CLK_SRC_DEFAULT,
.direction = GPTIMER_COUNT_UP,
.resolution_hz = 1 * 1000 * 1000,
};
ZH_ERROR_CHECK(gptimer_new_timer(&timer_config, &_dimmer_timer) == ESP_OK, ESP_FAIL, NULL, "Failed create dimmer timer.");
ZH_ERROR_CHECK(gptimer_new_timer(&timer_config, &handle->dimmer_timer_handle) == ESP_OK, ESP_FAIL, NULL, "Failed create dimmer timer.");
gptimer_event_callbacks_t cbs = {
.on_alarm = _zh_ac_dimmer_timer_on_alarm_cb,
};
ZH_ERROR_CHECK(gptimer_register_event_callbacks(_dimmer_timer, &cbs, NULL) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(gptimer_del_timer(_dimmer_timer) == ESP_OK, ESP_FAIL, NULL, "Timer delete fail."), "Failed register dimmer timer event callbacks.");
ZH_ERROR_CHECK(gptimer_enable(_dimmer_timer) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(gptimer_del_timer(_dimmer_timer) == ESP_OK, ESP_FAIL, NULL, "Timer delete fail."), "Failed enable dimmer timer.");
ZH_ERROR_CHECK(gptimer_register_event_callbacks(handle->dimmer_timer_handle, &cbs, handle) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK(gptimer_del_timer(handle->dimmer_timer_handle) == ESP_OK, ESP_FAIL, NULL, "Timer delete fail.")}, "Failed register dimmer timer event callbacks.");
ZH_ERROR_CHECK(gptimer_enable(handle->dimmer_timer_handle) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK(gptimer_del_timer(handle->dimmer_timer_handle) == ESP_OK, ESP_FAIL, NULL, "Timer delete fail.")}, "Failed enable dimmer timer.");
return ESP_OK;
}
static void IRAM_ATTR _zh_ac_dimmer_isr_handler(void *arg)
{
zh_ac_dimmer_handle_t *handle = arg;
uint64_t _current_us = esp_timer_get_time();
if (_current_us - _prev_us <= (1250 * 0.9)) // 90% of zero crossing period (1250 µs) at 400 Hz.
// Ignore spurious interrupts that arrive too soon after the previous one.
if (_current_us - handle->prev_zero_crossing_us > (1250 * 0.9)) // 90% of zero-crossing period (1250 µs) at 400 Hz.
{
gpio_set_level(handle->triac_gpio, 0);
handle->prev_zero_crossing_us = _current_us;
if (handle->is_dimmer_work == true)
{
if (handle->dimmer_value != 0)
{
if (handle->dimmer_value == 100)
{
gpio_set_level(handle->triac_gpio, 1);
return;
}
gpio_set_level(_init_config.triac_gpio, 0);
_prev_us = _current_us;
if (_is_dimmer_work == false)
{
return;
}
if (_dimmer_value != 0)
{
if (_dimmer_value == 100)
{
gpio_set_level(_init_config.triac_gpio, 1);
return;
}
_alarm_config.alarm_count = (uint64_t)((((1250 - 330) / 100) * (100 - _dimmer_value)) + 330); // 330 is 50% of zero crossing time (by logic analyser).
gptimer_alarm_config_t _alarm_config = {0};
// Calculate alarm delay: maps dimming (1–99) to a µs value.
// 330 µs is the base offset (50% of zero-crossing period, measured with a logic analyzer).
_alarm_config.alarm_count = (uint64_t)((((1250 - 330) / 100) * (100 - handle->dimmer_value)) + 330);
_alarm_config.flags.auto_reload_on_alarm = false;
gptimer_set_alarm_action(_dimmer_timer, &_alarm_config);
gptimer_start(_dimmer_timer);
gptimer_set_alarm_action(handle->dimmer_timer_handle, &_alarm_config);
gptimer_start(handle->dimmer_timer_handle);
}
}
}
}
static bool IRAM_ATTR _zh_ac_dimmer_timer_on_alarm_cb(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx)
{
gpio_set_level(_init_config.triac_gpio, 1);
gptimer_stop(_dimmer_timer);
gptimer_set_raw_count(_dimmer_timer, 0);
(void)edata;
zh_ac_dimmer_handle_t *handle = user_ctx;
gpio_set_level(handle->triac_gpio, 1);
gptimer_stop(timer);
gptimer_set_raw_count(timer, 0);
return false;
}