diff --git a/README.md b/README.md index 257ca0d..c9d031c 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,10 @@ ## Features -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. - -## Note - -Enable the following settings in 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 (positive edge) with filtering of spurious interrupts for precise phase-angle control. +2. TRIAC gate control through ESP-IDF gptimer (1 MHz resolution) with configurable dimming level (0–100%). +3. Designed for 400 Hz AC mains (1250 us half-period), with timing constants adjustable for other frequencies. +4. Handle-based API: init/deinit (with full GPIO, ISR and timer cleanup) and start/stop/set/get. ## Using diff --git a/include/zh_ac_dimmer.h b/include/zh_ac_dimmer.h index 4cb0a9c..d2afc7d 100644 --- a/include/zh_ac_dimmer.h +++ b/include/zh_ac_dimmer.h @@ -12,9 +12,9 @@ * - 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 + * - Start/stop/set/get API * - * @note The driver assumes a 400 Hz AC mains frequency (1250 µs period). + * @note The driver assumes a 400 Hz AC mains frequency (1250 us 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. @@ -23,6 +23,8 @@ #pragma once +#include "stdint.h" +#include "esp_heap_caps.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/gptimer.h" @@ -68,8 +70,7 @@ extern "C" * positive-edge interrupt), installs the ISR service, and creates the * gptimer instance. * - * @param[in] config Pointer to the initialization configuration structure. - * May point to a temporary variable (not stored internally) + * @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) * * @return ESP_OK on success @@ -87,7 +88,7 @@ extern "C" * 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) + * @param[in,out] handle Pointer to the dimmer handle, set to NULL on return (must not be NULL) * * @return ESP_OK on success * @return ESP_ERR_INVALID_ARG if handle or *handle is NULL @@ -104,7 +105,7 @@ extern "C" * 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) + * @param[in] 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 @@ -117,7 +118,7 @@ extern "C" * 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) + * @param[in] 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 @@ -131,7 +132,7 @@ extern "C" * zero-crossing event. * * @param[in] handle Pointer to the dimmer handle (must not be NULL) - * @param[in] value Dimming level from 0 (off) to 100 (full brightness) + * @param[in] value Dimming level from 0 (off) to 100 (full brightness) (must be <= 100) * * @return ESP_OK on success * @return ESP_ERR_INVALID_ARG if handle, *handle, or value is invalid diff --git a/zh_ac_dimmer.c b/zh_ac_dimmer.c index 39ac88c..e1fe844 100755 --- a/zh_ac_dimmer.c +++ b/zh_ac_dimmer.c @@ -13,6 +13,13 @@ static const char *TAG = "zh_ac_dimmer"; return err; \ } +#define ZH_ERROR_CHECK_CONT(cond, cleanup, msg, ...) \ + if (!(cond)) \ + { \ + ZH_LOGE(msg, ESP_FAIL, ##__VA_ARGS__); \ + cleanup; \ + } + /** * @brief Internal handle for AC dimmer instance. * @@ -24,7 +31,7 @@ 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) */ + uint64_t prev_zero_crossing_us; /*!< Timestamp of the last zero-crossing event (us) */ uint8_t dimmer_value; /*!< Current dimming level (0 = off, 100 = full) */ bool is_dimmer_work; /*!< `true` when the dimmer is actively running */ }; @@ -65,9 +72,10 @@ static esp_err_t _zh_ac_dimmer_gpio_init(const zh_ac_dimmer_init_config_t *confi /** * @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. + * Creates a new gptimer with a 1 MHz resolution (1 us steps) and registers + * the alarm callback that fires the TRIAC. The timer is enabled but not + * started; it is armed and started from the zero-crossing ISR for each + * half-cycle. * * @param handle Pointer to the dimmer handle (timer handle stored here) * @@ -79,13 +87,16 @@ 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. + * Executed on every positive edge of the zero-crossing signal. Ignores + * spurious interrupts that arrive sooner than 90% of the expected + * zero-crossing period (1250 us at 400 Hz). Resets the TRIAC gate low and + * updates the last zero-crossing timestamp. Depending on the dimming level: * * - 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. + * - At dimming 1–99: gptimer alarm fires after a calculated delay. The alarm + * count is mapped from the dimming value with a 330 us base offset + * (50% of the zero-crossing period, measured with a logic analyzer). * * @param arg Pointer to the dimmer handle (passed from GPIO ISR) */ @@ -101,24 +112,23 @@ static void _zh_ac_dimmer_isr_handler(void *arg); * @param edata Event data (unused) * @param user_ctx Pointer to the dimmer handle * - * @return `false` to indicate that the timer should not auto-restart + * @return false to indicate that no higher-priority task needs to be woken */ 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, zh_ac_dimmer_handle_t **handle) { ZH_LOGI("AC dimmer initialization started."); - ZH_ERROR_CHECK(config != NULL && 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(*handle != NULL, ESP_ERR_NO_MEM, NULL, "AC dimmer initialization failed. Failed to allocate 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.")}; + {ZH_ERROR_CHECK_CONT(gpio_isr_handler_remove(config->zero_cross_gpio) == ESP_OK, NULL, "Remove GPIO isr handler failed.")}; + {ZH_ERROR_CHECK_CONT(gpio_reset_pin(config->triac_gpio) == ESP_OK, NULL, "Reset GPIO failed.")}; + {ZH_ERROR_CHECK_CONT(gpio_reset_pin(config->zero_cross_gpio) == ESP_OK, 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; @@ -128,12 +138,12 @@ esp_err_t zh_ac_dimmer_deinit(zh_ac_dimmer_handle_t **handle) { ZH_LOGI("AC dimmer deinitialization started."); 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."); + ZH_ERROR_CHECK_CONT(gptimer_stop((*handle)->dimmer_timer_handle) == ESP_OK, NULL, "AC dimmer deinitialization failed. Timer stop fail."); + ZH_ERROR_CHECK_CONT(gptimer_disable((*handle)->dimmer_timer_handle) == ESP_OK, NULL, "AC dimmer deinitialization failed. Timer disable fail."); + ZH_ERROR_CHECK_CONT(gptimer_del_timer((*handle)->dimmer_timer_handle) == ESP_OK, NULL, "AC dimmer deinitialization failed. Timer delete fail."); + ZH_ERROR_CHECK_CONT(gpio_isr_handler_remove((*handle)->zero_cross_gpio) == ESP_OK, NULL, "AC dimmer deinitialization failed. Remove GPIO isr handler failed."); + ZH_ERROR_CHECK_CONT(gpio_reset_pin((*handle)->triac_gpio) == ESP_OK, NULL, "AC dimmer deinitialization failed. Reset GPIO failed."); + ZH_ERROR_CHECK_CONT(gpio_reset_pin((*handle)->zero_cross_gpio) == ESP_OK, NULL, "AC dimmer deinitialization failed. Reset GPIO failed."); heap_caps_free(*handle); *handle = NULL; ZH_LOGI("AC dimmer deinitialization completed successfully."); @@ -204,16 +214,16 @@ 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_CONT(gpio_reset_pin(config->triac_gpio) == ESP_OK, 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, 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."); + {ZH_ERROR_CHECK_CONT(gpio_reset_pin(config->triac_gpio) == ESP_OK, NULL, "Reset GPIO failed.")}; + {ZH_ERROR_CHECK_CONT(gpio_reset_pin(config->zero_cross_gpio) == ESP_OK, NULL, "Reset GPIO failed.")}, "Failed install isr service.") handle->triac_gpio = config->triac_gpio; handle->zero_cross_gpio = config->zero_cross_gpio; + ZH_ERROR_CHECK(gpio_isr_handler_add(config->zero_cross_gpio, _zh_ac_dimmer_isr_handler, handle) == ESP_OK, ESP_FAIL, + {ZH_ERROR_CHECK_CONT(gpio_reset_pin(config->triac_gpio) == ESP_OK, NULL, "Reset GPIO failed.")}; + {ZH_ERROR_CHECK_CONT(gpio_reset_pin(config->zero_cross_gpio) == ESP_OK, NULL, "Reset GPIO failed.")}, "Failed add isr handler."); return ESP_OK; } @@ -229,13 +239,13 @@ static esp_err_t _zh_ac_dimmer_timer_init(zh_ac_dimmer_handle_t *handle) .on_alarm = _zh_ac_dimmer_timer_on_alarm_cb, }; 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_CONT(gptimer_del_timer(handle->dimmer_timer_handle) == ESP_OK, 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."); + {ZH_ERROR_CHECK_CONT(gptimer_del_timer(handle->dimmer_timer_handle) == ESP_OK, NULL, "Timer delete fail.")}, "Failed enable dimmer timer."); return ESP_OK; } -static void IRAM_ATTR _zh_ac_dimmer_isr_handler(void *arg) +static void _zh_ac_dimmer_isr_handler(void *arg) { zh_ac_dimmer_handle_t *handle = (zh_ac_dimmer_handle_t *)arg; uint64_t _current_us = esp_timer_get_time(); @@ -256,7 +266,7 @@ static void IRAM_ATTR _zh_ac_dimmer_isr_handler(void *arg) 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.alarm_count = (uint64_t)((((1250 - 330) * (100 - handle->dimmer_value)) / 100) + 330); _alarm_config.flags.auto_reload_on_alarm = false; gptimer_set_alarm_action(handle->dimmer_timer_handle, &_alarm_config); gptimer_start(handle->dimmer_timer_handle); @@ -265,7 +275,7 @@ static void IRAM_ATTR _zh_ac_dimmer_isr_handler(void *arg) } } -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) +static bool _zh_ac_dimmer_timer_on_alarm_cb(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx) { (void)edata; zh_ac_dimmer_handle_t *handle = (zh_ac_dimmer_handle_t *)user_ctx;