Compare commits

...
10 Commits
6 changed files with 1119 additions and 376 deletions

No files matched your search

+225 -58
View File
@@ -1,32 +1,26 @@
# ESP32 ESP-IDF component for rotary encoder
## Tested on
1. [ESP32 ESP-IDF v6.0.0](https://docs.espressif.com/projects/esp-idf/en/v6.0/esp32/index.html)
## 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.
# ESP32 ESP-IDF component for rotary encoder (PCNT peripheral)
## Features
1. Support up to 8 encoders on one device.
1. Quadrature decoding through the PCNT peripheral hardware.
2. Configurable value range and step with float precision.
3. Push-button support with software debouncing timer.
4. Multiple encoders sharing a single processing task (limited by PCNT units and GPIOs).
5. Asynchronous rotation and button notifications through esp_event.
6. Built-in error statistics with queue overflow and stack watermark counters.
## Attention
1. For correct operation, please enable the following settings in the menuconfig:
```text
GPIO_CTRL_FUNC_IN_IRAM
PCNT_CTRL_FUNC_IN_IRAM
PCNT_ISR_IRAM_SAFE
```
1. Encoder number must be unique across all initialized encoders and greater than zero.
2. Task priority, stack size and queue size of the first initialized encoder are used by the shared processing task.
3. Position is initialized to the midpoint of the configured range.
4. Position is clamped to the configured minimum and maximum values.
## 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
```bash
cd ../your_project/components
git clone https://github.com/aZholtikov/zh_encoder
```
@@ -39,61 +33,234 @@ In the application, add the component:
## Examples
One encoder with button on device:
### Basic Example: Init and Get Position
```c
#include "zh_encoder.h"
#define ENCODER_NUMBER 0x01
zh_encoder_handle_t encoder_handle = {0};
void zh_encoder_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
zh_encoder_handle_t *encoder_handle = NULL;
void app_main(void)
{
esp_log_level_set("zh_encoder", ESP_LOG_ERROR);
esp_event_loop_create_default();
esp_event_handler_instance_register(ZH_ENCODER, ESP_EVENT_ANY_ID, &zh_encoder_event_handler, NULL, NULL);
zh_encoder_init_config_t config = ZH_ENCODER_INIT_CONFIG_DEFAULT();
config.task_priority = 5;
config.stack_size = configMINIMAL_STACK_SIZE;
config.queue_size = 5;
config.a_gpio_number = GPIO_NUM_4;
config.b_gpio_number = GPIO_NUM_16;
config.s_gpio_number = GPIO_NUM_15;
config.encoder_min_value = -10;
config.encoder_max_value = 10;
config.encoder_step = 0.001;
config.encoder_number = ENCODER_NUMBER;
zh_encoder_init(&config, &encoder_handle);
float encoder_position = 0;
zh_encoder_get(&encoder_handle, &encoder_position);
printf("Encoder position %0.3f.\n", encoder_position);
for (;;)
config.encoder_step = 1;
config.encoder_number = 1;
esp_err_t ret = zh_encoder_init(&config, &encoder_handle);
if (ret != ESP_OK)
{
const zh_encoder_stats_t *stats = zh_encoder_get_stats();
printf("Number of event post error: %ld.\n", stats->event_post_error);
printf("Number of queue overflow error: %ld.\n", stats->queue_overflow_error);
printf("Minimum free stack size: %ld.\n", stats->min_stack_size);
vTaskDelay(60000 / portTICK_PERIOD_MS);
printf("Encoder initialization error: %s\n", esp_err_to_name(ret));
return;
}
}
void zh_encoder_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
switch (event_id)
float position = 0;
ret = zh_encoder_get(&encoder_handle, &position);
if (ret == ESP_OK)
{
case ZH_BUTTON_EVENT:
zh_encoder_button_event_on_isr_t *button_event = event_data;
printf("Encoder number %d button %s.\n", button_event->encoder_number, (button_event->button_status == 1) ? "released" : "pressed");
break;
case ZH_ENCODER_EVENT:
zh_encoder_event_on_isr_t *encoder_event = event_data;
printf("Encoder number %d position %0.3f.\n", encoder_event->encoder_number, encoder_event->encoder_position);
break;
default:
break;
printf("Encoder position: %0.3f\n", position);
}
else
{
printf("Error getting position: %s\n", esp_err_to_name(ret));
}
ret = zh_encoder_deinit(&encoder_handle);
if (ret != ESP_OK)
{
printf("Error deinitializing encoder: %s\n", esp_err_to_name(ret));
}
}
```
### Set and Reset Position Example
```c
#include "zh_encoder.h"
zh_encoder_handle_t *encoder_handle = NULL;
void app_main(void)
{
esp_log_level_set("zh_encoder", ESP_LOG_ERROR);
zh_encoder_init_config_t config = ZH_ENCODER_INIT_CONFIG_DEFAULT();
config.a_gpio_number = GPIO_NUM_4;
config.b_gpio_number = GPIO_NUM_16;
config.encoder_min_value = -100;
config.encoder_max_value = 100;
config.encoder_step = 0.5;
config.encoder_number = 1;
esp_err_t ret = zh_encoder_init(&config, &encoder_handle);
if (ret != ESP_OK)
{
printf("Encoder initialization error: %s\n", esp_err_to_name(ret));
return;
}
float position = 25.0f;
ret = zh_encoder_set(&encoder_handle, position);
if (ret == ESP_OK)
{
printf("Position set to %0.3f\n", position);
}
else
{
printf("Error setting position: %s\n", esp_err_to_name(ret));
}
float current = 0;
ret = zh_encoder_get(&encoder_handle, &current);
if (ret == ESP_OK)
{
printf("Current position: %0.3f\n", current);
}
else
{
printf("Error getting position: %s\n", esp_err_to_name(ret));
}
ret = zh_encoder_reset(&encoder_handle);
if (ret == ESP_OK)
{
printf("Position reset to midpoint\n");
}
else
{
printf("Error resetting position: %s\n", esp_err_to_name(ret));
}
zh_encoder_deinit(&encoder_handle);
}
```
### Reinit Range Example
```c
#include "zh_encoder.h"
zh_encoder_handle_t *encoder_handle = NULL;
void app_main(void)
{
esp_log_level_set("zh_encoder", ESP_LOG_ERROR);
zh_encoder_init_config_t config = ZH_ENCODER_INIT_CONFIG_DEFAULT();
config.a_gpio_number = GPIO_NUM_4;
config.b_gpio_number = GPIO_NUM_16;
config.encoder_min_value = -10;
config.encoder_max_value = 10;
config.encoder_step = 1;
config.encoder_number = 1;
esp_err_t ret = zh_encoder_init(&config, &encoder_handle);
if (ret != ESP_OK)
{
printf("Encoder initialization error: %s\n", esp_err_to_name(ret));
return;
}
ret = zh_encoder_reinit(&encoder_handle, 0, 255, 1);
if (ret == ESP_OK)
{
printf("Encoder range changed to 0..255\n");
}
else
{
printf("Error reinitializing encoder: %s\n", esp_err_to_name(ret));
}
float position = 0;
ret = zh_encoder_get(&encoder_handle, &position);
if (ret == ESP_OK)
{
printf("Position after reinit: %0.3f\n", position);
}
else
{
printf("Error getting position: %s\n", esp_err_to_name(ret));
}
zh_encoder_deinit(&encoder_handle);
}
```
---
### Button Event Example
```c
#include "zh_encoder.h"
zh_encoder_handle_t *encoder_handle = NULL;
void zh_encoder_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
(void)arg;
(void)event_base;
if (event_id == ZH_BUTTON_EVENT)
{
zh_encoder_button_event_on_isr_t *button_event = (zh_encoder_button_event_on_isr_t *)event_data;
printf("Encoder %d button %s\n", button_event->encoder_number, button_event->button_status ? "released" : "pressed");
}
else if (event_id == ZH_ENCODER_EVENT)
{
zh_encoder_event_on_isr_t *encoder_event = (zh_encoder_event_on_isr_t *)event_data;
printf("Encoder %d position %0.3f %s\n", encoder_event->encoder_number, encoder_event->encoder_position, encoder_event->encoder_status ? "increased" : "reduced");
}
}
void app_main(void)
{
esp_log_level_set("zh_encoder", ESP_LOG_ERROR);
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(esp_event_handler_instance_register(ZH_ENCODER, ESP_EVENT_ANY_ID, &zh_encoder_event_handler, NULL, NULL));
zh_encoder_init_config_t config = ZH_ENCODER_INIT_CONFIG_DEFAULT();
config.a_gpio_number = GPIO_NUM_4;
config.b_gpio_number = GPIO_NUM_16;
config.s_gpio_number = GPIO_NUM_15;
config.s_gpio_debounce_time = 50000;
config.encoder_min_value = 0;
config.encoder_max_value = 100;
config.encoder_step = 1;
config.encoder_number = 1;
esp_err_t ret = zh_encoder_init(&config, &encoder_handle);
if (ret != ESP_OK)
{
printf("Encoder initialization error: %s\n", esp_err_to_name(ret));
return;
}
for (;;)
{
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
```
---
### Error Statistics Example
```c
#include "zh_encoder.h"
zh_encoder_handle_t *encoder_handle = NULL;
void app_main(void)
{
esp_log_level_set("zh_encoder", ESP_LOG_ERROR);
zh_encoder_init_config_t config = ZH_ENCODER_INIT_CONFIG_DEFAULT();
config.a_gpio_number = GPIO_NUM_4;
config.b_gpio_number = GPIO_NUM_16;
config.encoder_min_value = -10;
config.encoder_max_value = 10;
config.encoder_step = 1;
config.encoder_number = 1;
esp_err_t ret = zh_encoder_init(&config, &encoder_handle);
if (ret != ESP_OK)
{
printf("Encoder initialization error: %s\n", esp_err_to_name(ret));
return;
}
for (;;)
{
const zh_encoder_stats_t *stats = zh_encoder_get_stats();
printf("Event post errors: %lu\n", (unsigned long)stats->event_post_error);
printf("Queue overflow errors: %lu\n", (unsigned long)stats->queue_overflow_error);
printf("Minimum free stack: %lu\n", (unsigned long)stats->min_stack_size);
zh_encoder_reset_stats();
vTaskDelay(pdMS_TO_TICKS(60000));
}
}
```
+11
View File
@@ -0,0 +1,11 @@
description: ESP32 ESP-IDF component for rotary encoder
url: https://github.com/aZholtikov/zh_encoder
version: "3.1.0"
license: "Apache-2.0"
maintainers:
- "Alexey Zholtikov <alexey.zholtikov@yandex.ru>"
dependencies:
idf: ">=5.0"
zh_vector:
git: "https://github.com/aZholtikov/zh_vector.git"
version: "dev"
+155 -100
View File
@@ -1,9 +1,31 @@
/**
* @file zh_encoder.h
*
* @brief Rotary encoder driver for ESP-IDF using the PCNT peripheral.
*
* Provides a FreeRTOS-based driver for rotary encoders with optional
* push-button support. Uses ESP-IDF PCNT peripheral for quadrature
* decoding and esp_event for asynchronous event notification.
* Rotation counts are accumulated in a shared task, converted into
* configurable steps and clamped to the configured value range.
*
* Key features:
* - Quadrature decoding via PCNT peripheral (ESP-IDF-style edge/level actions)
* - Configurable min/max/step values with float precision
* - Configurable PCNT counts per encoder step (detent)
* - Push-button support with timer-based debouncing
* - Multiple simultaneous encoders (number is limited by available PCNT units and GPIOs)
* - Event-based notification (rotation and button events are posted from task context)
* - Error statistics tracking
*
* @note All public functions require a valid handle pointer obtained through zh_encoder_init().
* @note Rotation and button events are posted from task context (shared processing task and timer callback).
*/
#pragma once
#include "math.h"
#include "string.h"
#include "esp_log.h"
#include "driver/gpio.h"
#include "driver/pulse_cnt.h"
@@ -11,10 +33,8 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_event.h"
#include "zh_vector.h"
/**
* @brief Encoder initial default values.
*/
#define ZH_ENCODER_INIT_CONFIG_DEFAULT() \
{ \
.task_priority = 1, \
@@ -23,182 +43,217 @@
.a_gpio_number = GPIO_NUM_MAX, \
.b_gpio_number = GPIO_NUM_MAX, \
.s_gpio_number = GPIO_NUM_MAX, \
.s_gpio_debounce_time = 10, \
.s_gpio_debounce_time = 15000, \
.pullup = true, \
.encoder_min_value = -10, \
.encoder_max_value = 10, \
.encoder_step = 1, \
.encoder_number = 0}
.pcnt_counts_per_step = 4, \
.encoder_number = 1}
#ifdef __cplusplus
extern "C"
{
#endif
extern TaskHandle_t zh_encoder; /*!< Encoder Task Handle. */
/**
* @brief Handle to the shared encoder ISR processing task.
*/
extern TaskHandle_t zh_encoder;
/**
* @brief Structure for initial initialization of encoder.
* @brief Opaque encoder handle.
*/
typedef struct _zh_encoder_handle_t zh_encoder_handle_t;
/**
* @brief Configuration structure for encoder initialization.
*
* Holds all parameters required to create an encoder: GPIO assignments,
* value range, step size, shared task settings and a unique encoder number.
* Use ZH_ENCODER_INIT_CONFIG_DEFAULT() to obtain a valid starting configuration.
*
* @note All GPIO numbers must be valid ESP-IDF GPIO numbers or GPIO_NUM_MAX (disabled).
* @note encoder_number must be unique across all initialized encoders.
* @note Only the first initialized encoder's task_priority, stack_size and
* queue_size take effect (shared processing task/queue).
*/
typedef struct
{
float encoder_step; /*!< Encoder step. @note Must be greater than 0. */
float encoder_min_value; /*!< Encoder min value. @note Must be less than encoder_max_value. */
float encoder_max_value; /*!< Encoder max value. @note Must be greater than encoder_min_value. */
uint8_t task_priority; /*!< Task priority for the encoder isr processing. @note Minimum value is 1. */
uint8_t queue_size; /*!< Queue size for task for the encoder processing. @note Minimum value is 1. */
uint8_t a_gpio_number; /*!< Encoder A GPIO number. */
uint8_t b_gpio_number; /*!< Encoder B GPIO number. */
uint8_t s_gpio_number; /*!< Encoder button GPIO number. */
bool pullup; /*!< Pullup GPIO enable/disable. */
uint16_t s_gpio_debounce_time; /*!< Encoder button debounce_time. @note In microseconds. */
uint8_t encoder_number; /*!< Unique encoder number. @note Must be greater than 0. */
uint16_t stack_size; /*!< Stack size for task for the encoder isr processing processing. @note The minimum size is configMINIMAL_STACK_SIZE. */
float encoder_step; /*!< Encoder step per detent. @note Must be greater than 0 */
float encoder_min_value; /*!< Encoder min value. @note Must be less than encoder_max_value */
float encoder_max_value; /*!< Encoder max value. @note Must be greater than encoder_min_value */
gpio_num_t a_gpio_number; /*!< Encoder A GPIO number */
gpio_num_t b_gpio_number; /*!< Encoder B GPIO number */
gpio_num_t s_gpio_number; /*!< Encoder button GPIO number */
uint16_t s_gpio_debounce_time; /*!< Encoder button debounce time in microseconds. Rounded up to whole milliseconds for the debounce timer */
uint16_t stack_size; /*!< Stack size for the shared processing task. @note Minimum configMINIMAL_STACK_SIZE */
uint8_t task_priority; /*!< Task priority for the shared processing task. @note Minimum value is 1 */
uint8_t queue_size; /*!< Queue size for the shared processing queue. @note Minimum value is 1 */
uint8_t encoder_number; /*!< Unique encoder number. @note Must be greater than 0 */
uint8_t pcnt_counts_per_step; /*!< PCNT counts per encoder step (detent). @note Must be greater than 0. Typically 4 for EC11 with proper quadrature */
bool pullup; /*!< Pullup GPIO enable/disable for encoder A/B and button */
} zh_encoder_init_config_t;
/**
* @brief Encoder handle.
* @brief Structure for encoder error statistics.
*
* Collects error counters and task health metrics reported by the shared
* processing task and the interrupt handlers.
*/
typedef struct
{
bool s_gpio_status; /*!< Encoder button status. */
bool is_initialized; /*!< Encoder initialization flag. */
uint8_t encoder_number; /*!< Encoder unique number. */
uint8_t s_gpio_number; /*!< Encoder button GPIO number. */
uint16_t s_gpio_debounce_time; /*!< Encoder button debounce_time. */
uint64_t s_gpio_prev_time; /*!< Encoder button prev interrupt time. */
float encoder_step; /*!< Encoder step. */
float encoder_position; /*!< Encoder position. */
float encoder_min_value; /*!< Encoder min value. */
float encoder_max_value; /*!< Encoder max value. */
pcnt_unit_handle_t pcnt_unit_handle; /*!< Encoder unique pcnt unit handle. */
pcnt_channel_handle_t pcnt_channel_a_handle; /*!< Encoder unique pcnt channel handle. */
pcnt_channel_handle_t pcnt_channel_b_handle; /*!< Encoder unique pcnt channel handle. */
} zh_encoder_handle_t;
/**
* @brief Structure for error statistics storage.
*/
typedef struct
{
uint32_t event_post_error; /*!< Number of event post error. */
uint32_t queue_overflow_error; /*!< Number of queue overflow error. */
uint32_t min_stack_size; /*!< Minimum free stack size. */
uint32_t event_post_error; /*!< Number of event post errors */
uint32_t queue_overflow_error; /*!< Number of queue overflow errors */
uint32_t min_stack_size; /*!< Minimum free stack size (watermark) */
} zh_encoder_stats_t;
/**
* @brief Enumeration of encoder event ID.
* @brief Encoder event identifiers for esp_event base.
*
* Used as the event ID when registering handlers on the ZH_ENCODER event base.
*/
typedef enum
{
ZH_BUTTON_EVENT, /*!< Button event. */
ZH_ENCODER_EVENT /*!< Encoder event. */
ZH_BUTTON_EVENT, /*!< Encoder push-button event */
ZH_ENCODER_EVENT /*!< Encoder rotation event */
} zh_encoder_event_id_t;
ESP_EVENT_DECLARE_BASE(ZH_ENCODER);
/**
* @brief Structure for sending data to the event handler when cause encoder interrupt.
* @brief Event data structure for encoder rotation events.
*
* Posted from the shared processing task via esp_event_post() (not from ISR).
*
* @note Should be used with ZH_ENCODER event base and ZH_ENCODER_EVENT event ID.
*/
typedef struct
{
float encoder_position; /*!< Encoder current position. */
uint8_t encoder_number; /*!< Encoder unique number. */
float encoder_position; /*!< Encoder current position */
uint8_t encoder_number; /*!< Encoder unique number */
bool encoder_status; /*!< true if position increased, false if reduced */
} zh_encoder_event_on_isr_t;
/**
* @brief Structure for sending data to the event handler when cause encoder button interrupt.
* @brief Event data structure for encoder button events.
*
* Posted from the button debounce timer callback via esp_event_post() when the
* debounced button state actually changed.
*
* @note Should be used with ZH_ENCODER event base and ZH_BUTTON_EVENT event ID.
*/
typedef struct
{
uint8_t encoder_number; /*!< Encoder unique number. */
bool button_status; /*!< Encoder button status. */
uint8_t encoder_number; /*!< Encoder unique number */
bool button_status; /*!< Encoder button status */
} zh_encoder_button_event_on_isr_t;
/**
* @brief Initialize encoder.
* @brief Initialize encoder with the provided configuration.
*
* Creates and configures a rotary encoder using the ESP-IDF PCNT peripheral.
* The encoder is initialized at the midpoint of the configured range.
*
* @note The encoder will be set to the position (encoder_min_value + encoder_max_value)/2.
* @note Before initialization, configure zh_encoder_init_config_t with default values.
*
* @param[in] config Pointer to encoder configuration structure. Can point to a temporary variable (must not be NULL)
* @param[out] handle Pointer to store the unique encoder handle (must be NULL)
*
* @return ESP_OK on success
* @return ESP_ERR_INVALID_ARG if config or handle is NULL, or parameters are invalid
* @return ESP_ERR_INVALID_STATE if the encoder is already initialized
* @return ESP_ERR_NO_MEM if memory allocation fails
* @return ESP_FAIL if resource allocation or peripheral initialization fails
*/
esp_err_t zh_encoder_init(const zh_encoder_init_config_t *config, zh_encoder_handle_t **handle);
/**
* @brief Deinitialize encoder and release all resources.
*
* Stops the PCNT peripheral, removes GPIO ISR handlers and the button debounce timer,
* and frees allocated memory. If this is the last encoder, the shared processing task
* and queue are also deleted.
*
* @param[in,out] handle Pointer to unique encoder handle (must not be NULL)
*
* @return ESP_OK on success
* @return ESP_ERR_INVALID_ARG if handle is NULL or points to an uninitialized encoder
*/
esp_err_t zh_encoder_deinit(zh_encoder_handle_t **handle);
/**
* @brief Reinitialize encoder by updating min, max, and step values.
*
* Updates the encoder's value range and step size without reallocating resources.
* The encoder position is reset to the midpoint of the new range.
*
* @note The encoder will be set to the position (encoder_min_value + encoder_max_value)/2.
*
* @param[in] config Pointer to encoder initialized configuration structure. Can point to a temporary variable.
* @param[out] handle Pointer to unique encoder handle.
* @param[in,out] handle Pointer to unique encoder handle (must not be NULL)
* @param[in] min Encoder minimum value. Must be less than max
* @param[in] max Encoder maximum value. Must be greater than min
* @param[in] step Encoder step size. Must be greater than 0
*
* @note Before initialize the encoder recommend initialize zh_encoder_init_config_t structure with default values.
*
* @code zh_encoder_init_config_t config = ZH_ENCODER_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 parameters are invalid
* @return ESP_FAIL if PCNT count clear fails
*/
esp_err_t zh_encoder_init(const zh_encoder_init_config_t *config, zh_encoder_handle_t *handle);
/**
* @brief Deinitialize encoder.
*
* @param[in, out] handle Pointer to unique encoder handle.
*
* @return ESP_OK if success or an error code otherwise.
*/
esp_err_t zh_encoder_deinit(zh_encoder_handle_t *handle);
/**
* @brief Reinitialize encoder (change min, max and step values).
*
* @note The encoder will be set to the position (encoder_min_value + encoder_max_value)/2.
*
* @param[in, out] handle Pointer to unique encoder handle.
* @param[in] min Encoder min value. @note Must be less than encoder_max_value.
* @param[in] max Encoder max value. @note Must be greater than encoder_min_value.
* @param[in] step Encoder step. @note Must be greater than 0.
*
* @return ESP_OK if success or an error code otherwise.
*/
esp_err_t zh_encoder_reinit(zh_encoder_handle_t *handle, float min, float max, float step);
esp_err_t zh_encoder_reinit(zh_encoder_handle_t **handle, float min, float max, float step);
/**
* @brief Set encoder position.
*
* @param[in, out] handle Pointer to unique encoder handle.
* @param[in] position Encoder position (must be between encoder_min_value and encoder_max_value).
* @param[in,out] handle Pointer to unique encoder handle (must not be NULL)
* @param[in] position Encoder position (must be between encoder_min_value and encoder_max_value)
*
* @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 parameters are invalid
* @return ESP_FAIL if PCNT count clear fails
*/
esp_err_t zh_encoder_set(zh_encoder_handle_t *handle, float position);
esp_err_t zh_encoder_set(zh_encoder_handle_t **handle, float position);
/**
* @brief Get encoder position.
*
* @param[in] handle Pointer to unique encoder handle.
* @param[out] position Encoder position.
* @param[in] handle Pointer to unique encoder handle (must not be NULL)
* @param[out] position Pointer to store the current encoder position
*
* @return ESP_OK if success or an error code otherwise.
* @return ESP_OK on success
* @return ESP_ERR_INVALID_ARG if handle or position is NULL
*/
esp_err_t zh_encoder_get(const zh_encoder_handle_t *handle, float *position);
esp_err_t zh_encoder_get(zh_encoder_handle_t **handle, float *position);
/**
* @brief Reset encoder position.
* @brief Reset encoder position to the midpoint of the configured range.
*
* @note The encoder will be set to the position (encoder_min_value + encoder_max_value)/2.
*
* @param[in, out] handle Pointer to unique encoder handle.
* @param[in,out] handle Pointer to unique encoder handle (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 is NULL
* @return ESP_FAIL if PCNT count clear fails
*/
esp_err_t zh_encoder_reset(zh_encoder_handle_t *handle);
esp_err_t zh_encoder_reset(zh_encoder_handle_t **handle);
/**
* @brief Get error statistics.
* @brief Get a pointer to the encoder error statistics.
*
* @return Pointer to the statistics structure.
* Returns a read-only pointer to the global statistics structure
* containing error counts and task health metrics.
*
* @return Pointer to the statistics structure. Valid until zh_encoder_reset_stats() is called
*/
const zh_encoder_stats_t *zh_encoder_get_stats(void);
/**
* @brief Reset error statistics.
* @brief Reset all error statistics to zero.
*
* Clears error counters and stack watermarks.
*
* @return No return value
*/
void zh_encoder_reset_stats(void);
+1 -1
View File
@@ -1 +1 @@
2.4.0
3.1.0
+299
View File
@@ -0,0 +1,299 @@
# Описание компонента zh_encoder
## Общие сведения
**zh_encoder** — это компонент для ESP-IDF, реализующий драйвер поворотного энкодера на аппаратном периферийном блоке PCNT (Pulse Counter) с поддержкой кнопки и асинхронными событиями через `esp_event`.
Компонент обрабатывает сигналы квадрантурного энкодера на уровне прерываний, накапливает сырые счётчики PCNT в общей задаче, преобразует их в шаги настраиваемой величины и рассылает события поворота и нажатия кнопки.
## Основные возможности
1. **Аппаратное квадрантурное декодирование** — сигнал каналов A/B обрабатывается периферией PCNT без участия CPU.
2. **Дробная точность позиции** — диапазон и шаг задаются числами с плавающей точкой (`float`).
3. **Настраиваемая цена шага** — количество импульсов PCNT на один фиксатор энкодера (`pcnt_counts_per_step`).
4. **Поддержка кнопки** — программное подавление дребезга через одноразовый таймер FreeRTOS.
5. **Множественные энкодеры** — несколько энкодеров используют одну общую задачу обработки.
6. **Событийная модель** — события поворота и кнопки рассылаются через `esp_event` из контекста задачи.
7. **Встроенная статистика ошибок** — счётчики ошибок отправки событий, переполнения очереди и минимальный остаток стека.
## Внутреннее устройство
### Структура `zh_encoder_handle_t`
Внутренняя структура энкодера (opaque-хэндл, доступен только через API компонента):
| Поле | Тип | Описание |
| ------ | ----- | ---------- |
| `s_gpio_status` | `bool` | Текущее состояние кнопки (обновляется только колбэком таймера дребезга) |
| `encoder_number` | `uint8_t` | Уникальный номер энкодера, передаётся в событиях |
| `s_gpio_number` | `gpio_num_t` | GPIO кнопки (`GPIO_NUM_MAX`, если кнопка отключена) |
| `encoder_step` | `float` | Приращение позиции на один фиксатор энкодера |
| `encoder_position` | `float` | Текущая позиция энкодера |
| `encoder_min_value` | `float` | Минимальное значение позиции |
| `encoder_max_value` | `float` | Максимальное значение позиции |
| `count_accumulator` | `int32_t` | Остаток импульсов PCNT, не образующий целого шага |
| `pcnt_counts_per_step` | `uint8_t` | Количество импульсов PCNT на один шаг |
| `pcnt_unit_handle` | `pcnt_unit_handle_t` | Дескриптор единицы PCNT |
| `pcnt_channel_a_handle` | `pcnt_channel_handle_t` | Дескриптор канала A (edge A, level B) |
| `pcnt_channel_b_handle` | `pcnt_channel_handle_t` | Дескриптор канала B (edge B, level A) |
| `button_timer` | `TimerHandle_t` | Одноразовый таймер подавления дребезга кнопки |
Поля, к которым обращаются из прерываний и задачи обработки, защищены критической секцией на спинлоке.
### Структура сообщения очереди `zh_encoder_queue_t`
Передаётся из ISR точки наблюдения PCNT в общую задачу обработки:
| Поле | Тип | Описание |
|------|-----|----------|
| `count` | `int32_t` | Знаковое значение счётчика PCNT, прочитанное перед сбросом |
| `handle` | `zh_encoder_handle_t *` | Указатель на хэндл энкодера-источника |
Указатель на хэндл читается без спинлока: это безопасно, так как память хэндла освобождается в `zh_encoder_deinit()` только после удаления общей задачи обработки (при удалении последнего энкодера).
### Общие (статические) ресурсы
| Ресурс | Тип | Описание |
| -------- | ----- | ---------- |
| `zh_encoder` | `TaskHandle_t` | Дескриптор общей задачи обработки, создаётся для первого энкодера |
| `_queue_handle` | `QueueHandle_t` | Общая очередь сообщений от ISR PCNT к задаче обработки |
| `_spinlock` | `portMUX_TYPE` | Спинлок для защиты общих данных энкодеров |
| `_stats` | `zh_encoder_stats_t` | Глобальная статистика ошибок (счётчики и watermark стека) |
| `_vector` | `zh_vector_t *` | Вектор `zh_vector` с номерами зарегистрированных энкодеров |
Общая задача, очередь и вектор создаются при инициализации **первого** энкодера и удаляются при деинициализации **последнего**. Параметры `task_priority`, `stack_size` и `queue_size` первого энкодера действуют для всей системы.
### Конфигурация PCNT
Каждый энкодер занимает одну единицу PCNT с двумя каналами:
- Единица настраивается с пределами `high_limit = 100`, `low_limit = -100` и фильтром от помех `max_glitch_ns = 1000`.
- Канал A: front A + уровень B, канал B: front B + уровень A; действия каналов задаются так, чтобы вращение по часовой стрелке увеличивало счётчик.
- Регистрируются две точки наблюдения: `+pcnt_counts_per_step` и `-pcnt_counts_per_step`, что даёт прерывание на каждый полный шаг энкодера.
## API (интерфейс)
### Инициализация и освобождение
| Функция | Описание |
| --------- | ---------- |
| `zh_encoder_init()` | Создаёт энкодер по конфигурации: проверяет параметры, регистрирует номер в векторе, при необходимости создаёт общую очередь и задачу, инициализирует PCNT и GPIO кнопки. Позиция устанавливается в середину диапазона. Требует, чтобы входной `*handle` был равен `NULL`. |
| `zh_encoder_deinit()` | Останавливает PCNT, удаляет каналы и единицу, снимает обработчик GPIO прерываний и таймер кнопки, удаляет номер из вектора. При удалении последнего энкодера удаляет общую задачу, очередь и вектор. Устанавливает входной указатель в `NULL`. |
| `zh_encoder_reinit()` | Меняет диапазон (`min`, `max`) и шаг (`step`) без перераспределения ресурсов, сбрасывает позицию в середину нового диапазона и обнуляет счётчик PCNT. |
### Управление позицией
| Функция | Описание |
|---------|----------|
| `zh_encoder_set()` | Устанавливает позицию в заданное значение (в пределах настроенного диапазона), обнуляет аккумулятор и счётчик PCNT. |
| `zh_encoder_get()` | Читает текущую позицию энкодера. |
| `zh_encoder_reset()` | Сбрасывает позицию в середину диапазона `(min + max) / 2` и обнуляет счётчик PCNT. |
### Статистика
| Функция | Описание |
|---------|----------|
| `zh_encoder_get_stats()` | Возвращает read-only указатель на глобальную структуру статистики `zh_encoder_stats_t`. Указатель действителен до вызова `zh_encoder_reset_stats()`. |
| `zh_encoder_reset_stats()` | Обнуляет все счётчики статистики. |
### Типы и события
| Элемент | Описание |
| --------- | ---------- |
| `zh_encoder_init_config_t` | Конфигурация инициализации: GPIO, диапазон, шаг, параметры задачи и уникальный номер. Значения по умолчанию задаёт `ZH_ENCODER_INIT_CONFIG_DEFAULT()`. |
| `zh_encoder_stats_t` | Статистика: `event_post_error`, `queue_overflow_error`, `min_stack_size`. |
| `zh_encoder_event_on_isr_t` | Данные события поворота: позиция, номер энкодера, направление (`true` — увеличение). |
| `zh_encoder_button_event_on_isr_t` | Данные события кнопки: номер энкодера, состояние. |
| `zh_encoder_event_id_t` | Идентификаторы событий: `ZH_BUTTON_EVENT`, `ZH_ENCODER_EVENT`. |
| `ZH_ENCODER` | База событий `esp_event`, в которую компонент постит все сообщения. |
## Потокобезопасность
1. **Спинлок (`portMUX_TYPE`)** — защищает позицию, аккумулятор и параметры диапазона от одновременного доступа из задачи обработки, обработчиков публичного API и ISR PCNT. В прерываниях используются `taskENTER_CRITICAL_ISR` / `taskEXIT_CRITICAL_ISR`.
2. **Очередь FreeRTOS** — ISR PCNT передаёт сырой счётчик в общую задачу через `xQueueSendFromISR()`, без блокировок и без работы с позицией в прерывании.
3. **Критическая секция в ISR** — атомарная пара операций «чтение + сброс счётчика PCNT» внутри обработчика точки наблюдения.
4. **Таймер дребезга кнопки** — состояние кнопки читается и пишется только в колбэке таймера (контекст задачи таймеров FreeRTOS), поэтому конкуренции за это поле нет.
5. **События `esp_event`** — публикуются из контекста задачи (общая задача обработки и колбэк таймера), а не из ISR, с таймаутом блокировки 10 мс.
Публичные функции `zh_encoder_init()` / `zh_encoder_deinit()` не защищены спинлоком между собой: их следует вызывать из одной задачи (например, из `app_main`).
## Алгоритмы
### Преобразование импульсов PCNT в шаги
Общая задача обработки для каждого сообщения из очереди выполняет:
1. `total = count_accumulator + count` — суммирование остатка с новым значением счётчика.
2. `full_steps = total / pcnt_counts_per_step` — целочисленное деление со знаком; остаток сохраняется в `count_accumulator`.
3. Если `full_steps != 0`, вычисляется кандидат новой позиции `candidate = position + full_steps * encoder_step`.
4. Кандидат ограничивается диапазоном: `candidate > max → max`, `candidate < min → min`.
5. Если `|candidate| < encoder_step / 2`, значение «прилипает» к нулю (`candidate = 0`). Это исключает «хвосты» при шагах с плавающей точкой.
6. Событие поворота публикуется только при фактическом изменении позиции.
### Точки наблюдения PCNT
- Прерывание генерируется при достижении счётчиком значений `+pcnt_counts_per_step` и `-pcnt_counts_per_step`, то есть ровно на один зафиксированный шаг.
- После чтения счётчик сбрасывается, поэтому накопление идёт «от события к событию», а пределы единицы (±100) служат лишь страховкой от переполнения при переполнении очереди.
### Подавление дребезга кнопки
1. Прерывание по любому front'у только перезапускает одноразовый таймер (`xTimerResetFromISR`) — перезагрузка таймера «задвигает» окончание дребезга.
2. По истечении задержки колбэк таймера читает уровень пина и постит событие **только при изменении** состояния относительно предыдущего.
3. Задержка `s_gpio_debounce_time` задаётся в микросекундах и округляется вверх до целых миллисекунд (минимум 1 мс).
### Регистрация энкодеров
- Номера занятых энкодеров хранятся в векторе `zh_vector`; при инициализации проверяется уникальность номера, при деинициализации — он удаляется из вектора.
- Общая очередь, задача и вектор создаются, когда размер вектора становится равен 1, и освобождаются, когда он становится пустым.
## Примеры использования
### Базовый пример: подписка на события
```c
#include "zh_encoder.h"
zh_encoder_handle_t *encoder_handle = NULL;
// Обработчик событий энкодера
void zh_encoder_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
if (event_id == ZH_ENCODER_EVENT) {
zh_encoder_event_on_isr_t *rot = (zh_encoder_event_on_isr_t *)event_data;
printf("Encoder %d: position %0.3f (%s)\n", rot->encoder_number,
rot->encoder_position, rot->encoder_status ? "up" : "down");
} else if (event_id == ZH_BUTTON_EVENT) {
zh_encoder_button_event_on_isr_t *btn = (zh_encoder_button_event_on_isr_t *)event_data;
printf("Encoder %d: button %s\n", btn->encoder_number,
btn->button_status ? "released" : "pressed");
}
}
void app_main(void)
{
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(esp_event_handler_instance_register(ZH_ENCODER, ESP_EVENT_ANY_ID,
&zh_encoder_event_handler, NULL, NULL));
zh_encoder_init_config_t config = ZH_ENCODER_INIT_CONFIG_DEFAULT();
config.a_gpio_number = GPIO_NUM_4; // канал A
config.b_gpio_number = GPIO_NUM_16; // канал B
config.s_gpio_number = GPIO_NUM_15; // кнопка
config.encoder_number = 1; // уникальный номер
if (zh_encoder_init(&config, &encoder_handle) != ESP_OK) {
return; // ошибка инициализации
}
for (;;) {
vTaskDelay(pdMS_TO_TICKS(1000));
}
// По завершении работы: zh_encoder_deinit(&encoder_handle);
}
```
### Работа с позицией и диапазоном
```c
#include "zh_encoder.h"
zh_encoder_handle_t *encoder_handle = NULL;
void app_main(void)
{
zh_encoder_init_config_t config = ZH_ENCODER_INIT_CONFIG_DEFAULT();
config.a_gpio_number = GPIO_NUM_4;
config.b_gpio_number = GPIO_NUM_16;
config.encoder_min_value = 0;
config.encoder_max_value = 100;
config.encoder_step = 0.5f; // дробный шаг
config.encoder_number = 1;
if (zh_encoder_init(&config, &encoder_handle) != ESP_OK) {
return;
}
zh_encoder_set(&encoder_handle, 42.5f); // установить позицию
float position = 0;
zh_encoder_get(&encoder_handle, &position); // прочитать позицию
printf("Position: %0.3f\n", position);
zh_encoder_reinit(&encoder_handle, -10, 10, 1); // сменить диапазон и шаг
zh_encoder_reset(&encoder_handle); // сброс в середину диапазона
zh_encoder_deinit(&encoder_handle); // освобождение ресурсов
}
```
### Мониторинг статистики ошибок
```c
#include "zh_encoder.h"
zh_encoder_handle_t *encoder_handle = NULL;
void app_main(void)
{
zh_encoder_init_config_t config = ZH_ENCODER_INIT_CONFIG_DEFAULT();
config.a_gpio_number = GPIO_NUM_4;
config.b_gpio_number = GPIO_NUM_16;
config.encoder_number = 1;
if (zh_encoder_init(&config, &encoder_handle) != ESP_OK) {
return;
}
for (;;) {
const zh_encoder_stats_t *stats = zh_encoder_get_stats();
if (stats->queue_overflow_error > 0) {
printf("Queue overflow: %lu\n", (unsigned long)stats->queue_overflow_error);
}
if (stats->min_stack_size < 512) {
printf("Low stack watermark: %lu\n", (unsigned long)stats->min_stack_size);
}
zh_encoder_reset_stats(); // сброс счётчиков
vTaskDelay(pdMS_TO_TICKS(60000));
}
// По завершении работы: zh_encoder_deinit(&encoder_handle);
}
```
## Логирование
Компонент использует ESP-IDF логирование с тегом `"zh_encoder"`. Сообщения об ошибках включают имя файла, номер строки и имя кода ошибки.
```c
esp_log_level_set("zh_encoder", ESP_LOG_ERROR);
```
## Ошибки
| Код | Описание |
| ----- | ---------- |
| `ESP_OK` | Операция выполнена успешно |
| `ESP_ERR_INVALID_ARG` | NULL-аргументы, неверные параметры конфигурации (диапазон, шаг, номер, GPIO), дублирование номера энкодера, позиция вне диапазона |
| `ESP_ERR_INVALID_STATE` | Энкодер по входному хэндлу уже инициализирован (`*handle != NULL`) |
| `ESP_ERR_NO_MEM` | Не удалось выделить память под хэндл энкодера |
| `ESP_FAIL` | Ошибка инициализации GPIO/PCNT, создания задачи, очереди или вектора, сброса счётчика PCNT |
## Ограничения
1. **Уникальный номер энкодера** — `encoder_number` должен быть больше 0 и не повторяться среди всех инициализированных энкодеров (проверяется по вектору зарегистрированных номеров).
2. **Общие задача и очередь** — `task_priority`, `stack_size` и `queue_size` первого инициализированного энкодера используются всей системой; для последующих энкодеров они игнорируются.
3. **Ёмкость очереди по умолчанию равна 1** — при быстрой ротации или медленной задаче обработки возможны потери сообщений; факт потери фиксируется счётчиком `queue_overflow_error`.
4. **Число энкодеров ограничено железом** — количество доступных единиц PCNT и GPIO конкретной модели ESP32.
5. **Позиция инициализируется серединой диапазона** — `(min + max) / 2`, а не нулём; при `min = -10, max = 10` стартовая позиция равна 0.
6. **Ограничение и «прилипание» позиции** — значение за краями диапазона отбрасывается, а значение ближе половины шага к нулю заменяется нулём.
7. **Параметры не могут меняться на лету** — GPIO, номер энкодера и `pcnt_counts_per_step` фиксируются при инициализации; `zh_encoder_reinit()` меняет только диапазон и шаг.
8. **Зависимость от `zh_vector`** — компонент использует сторонний вектор для реестра энкодеров; он подтягивается автоматически как зависимость компонента.
9. **Требуется цикл событий `esp_event`** — без созданного default loop (`esp_event_loop_create_default()`) события не доставляются.
## Требования для сборки
- ESP-IDF версии **5.0 или выше** (используется PCNT driver из компонента `driver`).
- FreeRTOS (входит в состав ESP-IDF) — задачи, очереди, таймеры.
- `esp_event` (входит в состав ESP-IDF) — рассылка событий.
- `esp_timer` (входит в состав ESP-IDF) — таймер дребезга кнопки.
- Компонент `zh_vector` — автоматически устанавливается как зависимость через `idf_component.yml`.
## Установка
```bash
cd ваш_проект/components
git clone https://github.com/aZholtikov/zh_encoder
```
```c
#include "zh_encoder.h"
```
+428 -217
View File
@@ -13,151 +13,332 @@
return err; \
}
#define ZH_ENCODER_DIRECTION_CW 1
#define ZH_ENCODER_DIRECTION_CCW -1
#define ZH_ERROR_CHECK_CONT(cond, cleanup, msg, ...) \
if (!(cond)) \
{ \
ZH_LOGE(msg, ESP_FAIL, ##__VA_ARGS__); \
cleanup; \
}
#define ZH_ENCODER_PCNT_HIGH_LIMIT 100
#define ZH_ENCODER_PCNT_LOW_LIMIT -100
/**
* @brief Internal encoder handle structure.
*
* Stores the runtime state of a single encoder: button GPIO state and the software
* debounce timer, value range and step, PCNT count accumulator, and PCNT unit/channel handles.
*
* @note Fields accessed from both ISR and task context are protected by a spinlock
* in the public API functions.
*/
struct _zh_encoder_handle_t
{
bool s_gpio_status; /*!< Current encoder button status */
uint8_t encoder_number; /*!< Unique encoder number */
gpio_num_t s_gpio_number; /*!< Encoder button GPIO number */
float encoder_step; /*!< Encoder step per detent */
float encoder_position; /*!< Current encoder position */
float encoder_min_value; /*!< Encoder minimum value */
float encoder_max_value; /*!< Encoder maximum value */
int32_t count_accumulator; /*!< Leftover PCNT counts not yet converted into a full step */
uint8_t pcnt_counts_per_step; /*!< PCNT counts per encoder step (detent) */
pcnt_unit_handle_t pcnt_unit_handle; /*!< PCNT unit handle */
pcnt_channel_handle_t pcnt_channel_a_handle; /*!< PCNT channel A handle */
pcnt_channel_handle_t pcnt_channel_b_handle; /*!< PCNT channel B handle */
TimerHandle_t button_timer; /*!< One-shot debounce timer for the button */
};
/**
* @brief Queue message structure for encoder ISR processing.
*
* Passed from the PCNT watch-point ISR to the shared processing task through
* the queue. Carries the raw PCNT count together with the source encoder handle.
*
* @note The handle pointer is read without holding the spinlock. It is safe because
* the handle is freed in zh_encoder_deinit() only after the processing task
* has been deleted (the last encoder removal).
*/
typedef struct
{
int32_t count; /*!< PCNT count read before clearing (signed) */
zh_encoder_handle_t *handle; /*!< Pointer to the encoder handle */
} zh_encoder_queue_t;
TaskHandle_t zh_encoder = NULL;
static QueueHandle_t _queue_handle = NULL;
static portMUX_TYPE _spinlock = portMUX_INITIALIZER_UNLOCKED;
volatile static uint8_t _encoder_counter = 0;
static zh_encoder_stats_t _stats = {0};
volatile static uint8_t _encoder_number_matrix[8] = {0};
static zh_vector_t *_vector = NULL;
/**
* @brief Validate the initialization configuration and copy parameters into the handle.
*
* Checks task, queue, range, step, encoder number and watch-point settings. When a
* vector of registered encoders exists, verifies that the encoder number is not yet used.
* On success, populates the handle fields and sets the position to the midpoint of the range.
*
* @param config Pointer to the encoder configuration structure
* @param handle Pointer to the encoder handle to populate
*
* @return ESP_OK on success
* @return ESP_ERR_INVALID_ARG if any configuration parameter is invalid or the encoder number is already present
*/
static esp_err_t _zh_encoder_validate_config(const zh_encoder_init_config_t *config, zh_encoder_handle_t *handle);
/**
* @brief Initialize the PCNT unit, channels, watch points and event callbacks.
*
* Configures encoder A/B GPIOs as inputs, creates a PCNT unit with a glitch filter,
* adds two channels with quadrature edge/level actions, registers high and low watch
* points, then enables, clears and starts the unit. Any failure frees the resources
* created so far before returning.
*
* @param config Pointer to the encoder configuration structure
* @param handle Pointer to the encoder handle to store PCNT handles in
*
* @return ESP_OK on success
* @return ESP_ERR_INVALID_ARG if A/B GPIO numbers are invalid or identical
* @return ESP_FAIL if any GPIO or PCNT operation fails
*/
static esp_err_t _zh_encoder_pcnt_init(const zh_encoder_init_config_t *config, zh_encoder_handle_t *handle);
/**
* @brief Initialize the push-button GPIO and register its interrupt handler.
*
* Does nothing when the button is disabled (s_gpio_number equals GPIO_NUM_MAX). Installs
* the shared GPIO ISR service when needed, configures the button pin for any-edge
* interrupt, creates the one-shot software debounce timer (microseconds rounded up to
* milliseconds, minimum 1 ms) and registers the button interrupt handler.
*
* @param config Pointer to the encoder configuration structure
* @param handle Pointer to the encoder handle to store button state in
*
* @return ESP_OK on success
* @return ESP_ERR_INVALID_ARG if the button GPIO number is invalid or equals the A/B GPIO number
* @return ESP_FAIL if GPIO configuration, ISR service installation or handler registration fails
*/
static esp_err_t _zh_encoder_gpio_init(const zh_encoder_init_config_t *config, zh_encoder_handle_t *handle);
/**
* @brief Create the shared processing queue for the first registered encoder.
*
* The queue is allocated once, when the encoder vector contains exactly one entry.
*
* @param config Pointer to the encoder configuration structure
*
* @return ESP_OK on success
* @return ESP_FAIL if queue creation fails
*/
static esp_err_t _zh_encoder_resources_init(const zh_encoder_init_config_t *config);
static esp_err_t _zh_encoder_task_init(const zh_encoder_init_config_t *config, zh_encoder_handle_t *handle);
/**
* @brief Create the shared ISR processing task for the first registered encoder.
*
* The task is created once, when the encoder vector contains exactly one entry,
* pinned to no specific core with the configured priority and stack size.
*
* @param config Pointer to the encoder configuration structure
*
* @return ESP_OK on success
* @return ESP_FAIL if task creation fails
*/
static esp_err_t _zh_encoder_task_init(const zh_encoder_init_config_t *config);
/**
* @brief PCNT watch-point interrupt handler.
*
* Reads and clears the unit count, then posts a queue message to the shared processing
* task. Increments the queue overflow counter when the queue is full.
*
* @param unit PCNT unit handle that reached the watch point
* @param edata Watch-point event data (unused)
* @param user_ctx Pointer to the encoder handle passed during callback registration
*
* @return true if a higher priority task was woken and a context switch is required
* @return false otherwise
*/
static bool _zh_encoder_isr_handler(pcnt_unit_handle_t unit, const pcnt_watch_event_data_t *edata, void *user_ctx);
/**
* @brief Shared processing task body for encoder rotation events.
*
* Receives queue messages, converts raw PCNT counts into whole steps using the
* accumulator, clamps the position to the configured range, snaps values closer to
* zero than half a step to zero, and posts a rotation event when the position changes.
* Updates the stack watermark on each iteration.
*
* @param pvParameter Task parameter (unused)
*
* @return Never returns, the task deletes itself when the queue receive fails
*/
static void _zh_encoder_isr_processing_task(void *pvParameter);
/**
* @brief Button debounce timer callback.
*
* Runs in the timer daemon task context after the debounce period elapses. Reads the
* button level and posts a button event only when the state actually changed. Increments
* the event post error counter when posting fails.
*
* @note @c s_gpio_status is read and written without holding the spinlock. It is safe
* because only this callback ever touches the field, so no concurrent access occurs.
*
* @param xTimer Timer handle whose ID holds the encoder handle pointer
*
* @return No return value
*/
static void _zh_encoder_button_timer_cb(TimerHandle_t xTimer);
/**
* @brief Push-button GPIO interrupt handler.
*
* Restarts the one-shot software debounce timer on every edge. The button state is
* checked in the timer callback, so the ISR itself performs no event posting.
*
* @param arg Pointer to the encoder handle passed during handler registration
*
* @return No return value
*/
static void _zh_encoder_button_isr_handler(void *arg);
ESP_EVENT_DEFINE_BASE(ZH_ENCODER);
esp_err_t zh_encoder_init(const zh_encoder_init_config_t *config, zh_encoder_handle_t *handle) // -V2008
esp_err_t zh_encoder_init(const zh_encoder_init_config_t *config, zh_encoder_handle_t **handle)
{
ZH_LOGI("Encoder initialization started.");
ZH_ERROR_CHECK(config != NULL && handle != NULL, ESP_ERR_INVALID_ARG, NULL, "Encoder initialization failed. Invalid argument.");
ZH_ERROR_CHECK(handle->is_initialized == false, ESP_ERR_INVALID_STATE, NULL, "Encoder initialization failed. Encoder is already initialized.");
ZH_ERROR_CHECK(_encoder_counter < sizeof(_encoder_number_matrix), ESP_ERR_INVALID_ARG, NULL, "Encoder initialization failed. Maximum quantity reached.");
ZH_ERROR_CHECK(_zh_encoder_validate_config(config, handle) == ESP_OK, ESP_FAIL, NULL, "Encoder initialization failed. Initial configuration check failed.");
ZH_ERROR_CHECK(_zh_encoder_resources_init(config) == ESP_OK, ESP_FAIL, NULL, "Encoder initialization failed. Resources initialization failed.");
ZH_ERROR_CHECK(_zh_encoder_task_init(config, handle) == ESP_OK, ESP_FAIL,
vQueueDelete(_queue_handle), "Encoder initialization failed. Processing task initialization failed.");
ZH_ERROR_CHECK(_zh_encoder_pcnt_init(config, handle) == ESP_OK, ESP_FAIL,
vQueueDelete(_queue_handle);
vTaskDelete(zh_encoder), "Encoder initialization failed. PCNT initialization failed.");
ZH_ERROR_CHECK(_zh_encoder_gpio_init(config, handle) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(pcnt_unit_stop(handle->pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT unit stop fail.");
ZH_ERROR_CHECK(pcnt_unit_disable(handle->pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT unit disable fail.");
ZH_ERROR_CHECK(pcnt_del_channel(handle->pcnt_channel_a_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_channel(handle->pcnt_channel_b_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_unit(handle->pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail.");
vQueueDelete(_queue_handle);
vTaskDelete(zh_encoder), "Encoder initialization failed. GPIO initialization failed.");
ZH_ERROR_CHECK(*handle == NULL, ESP_ERR_INVALID_STATE, NULL, "Encoder initialization failed. Encoder is already initialized.");
*handle = heap_caps_calloc(1, sizeof(zh_encoder_handle_t), MALLOC_CAP_8BIT);
ZH_ERROR_CHECK(*handle != NULL, ESP_ERR_NO_MEM, NULL, "Encoder initialization failed. Failed to allocate encoder handle.");
ZH_ERROR_CHECK(_zh_encoder_validate_config(config, *handle) == ESP_OK, ESP_FAIL, heap_caps_free(*handle); *handle = NULL, "Encoder initialization failed. Initial configuration check failed.");
if (_vector == NULL)
{
ZH_ERROR_CHECK(zh_vector_init(&_vector, sizeof(uint8_t)) == ESP_OK, ESP_FAIL, heap_caps_free(*handle); *handle = NULL, "Encoder initialization failed. Failed to create vector.");
}
ZH_ERROR_CHECK(zh_vector_push_back(&_vector, &config->encoder_number) == ESP_OK, ESP_FAIL, heap_caps_free(*handle); *handle = NULL, "Encoder initialization failed. Failed to add vector data.");
ZH_ERROR_CHECK(_zh_encoder_resources_init(config) == ESP_OK, ESP_FAIL, zh_vector_delete_back(&_vector); heap_caps_free(*handle); *handle = NULL, "Encoder initialization failed. Resources initialization failed.");
ZH_ERROR_CHECK(_zh_encoder_task_init(config) == ESP_OK, ESP_FAIL,
zh_vector_delete_back(&_vector);
heap_caps_free(*handle); *handle = NULL, "Encoder initialization failed. Processing task initialization failed.");
ZH_ERROR_CHECK(_zh_encoder_pcnt_init(config, *handle) == ESP_OK, ESP_FAIL,
zh_vector_delete_back(&_vector);
heap_caps_free(*handle); *handle = NULL, "Encoder initialization failed. PCNT initialization failed.");
ZH_ERROR_CHECK(_zh_encoder_gpio_init(config, *handle) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK_CONT(pcnt_unit_stop((*handle)->pcnt_unit_handle) == ESP_OK, NULL, "PCNT unit stop fail.")};
{ZH_ERROR_CHECK_CONT(pcnt_unit_disable((*handle)->pcnt_unit_handle) == ESP_OK, NULL, "PCNT unit disable fail.")};
{ZH_ERROR_CHECK_CONT(pcnt_del_channel((*handle)->pcnt_channel_a_handle) == ESP_OK, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK_CONT(pcnt_del_channel((*handle)->pcnt_channel_b_handle) == ESP_OK, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK_CONT(pcnt_del_unit((*handle)->pcnt_unit_handle) == ESP_OK, NULL, "PCNT delete unit fail.")};
zh_vector_delete_back(&_vector); heap_caps_free(*handle); *handle = NULL, "Encoder initialization failed. GPIO initialization failed.");
if (_stats.min_stack_size == 0)
{
_stats.min_stack_size = config->stack_size;
}
handle->is_initialized = true;
++_encoder_counter;
for (uint8_t i = 0; i < sizeof(_encoder_number_matrix); ++i)
{
if (_encoder_number_matrix[i] == 0)
{
_encoder_number_matrix[i] = handle->encoder_number;
break;
}
}
ZH_LOGI("Encoder initialization completed successfully.");
return ESP_OK;
}
esp_err_t zh_encoder_deinit(zh_encoder_handle_t *handle) // -V2008
esp_err_t zh_encoder_deinit(zh_encoder_handle_t **handle)
{
ZH_LOGI("Encoder deinitialization started.");
ZH_ERROR_CHECK(handle != NULL, ESP_ERR_INVALID_ARG, NULL, "Encoder deinitialization failed. Invalid argument.");
ZH_ERROR_CHECK(handle->is_initialized == true, ESP_FAIL, NULL, "Encoder deinitialization failed. Encoder not initialized.");
ZH_ERROR_CHECK(pcnt_unit_stop(handle->pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "Encoder deinitialization failed. PCNT unit stop fail.");
ZH_ERROR_CHECK(pcnt_unit_disable(handle->pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "Encoder deinitialization failed. PCNT unit disable fail.");
ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(handle->pcnt_unit_handle, ZH_ENCODER_DIRECTION_CW) == ESP_OK, ESP_FAIL, NULL, "Encoder deinitialization failed. PCNT unit remove watch point fail.");
ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(handle->pcnt_unit_handle, ZH_ENCODER_DIRECTION_CCW) == ESP_OK, ESP_FAIL, NULL, "Encoder deinitialization failed. PCNT unit remove watch point fail.");
ZH_ERROR_CHECK(pcnt_del_channel(handle->pcnt_channel_a_handle) == ESP_OK, ESP_FAIL, NULL, "Encoder deinitialization failed. PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_channel(handle->pcnt_channel_b_handle) == ESP_OK, ESP_FAIL, NULL, "Encoder deinitialization failed. PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_unit(handle->pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "Encoder deinitialization failed. PCNT delete unit fail.");
if (handle->s_gpio_number != GPIO_NUM_MAX)
ZH_ERROR_CHECK(handle != NULL && *handle != NULL, ESP_ERR_INVALID_ARG, NULL, "Encoder deinitialization failed. Invalid argument.");
ZH_ERROR_CHECK_CONT(pcnt_unit_stop((*handle)->pcnt_unit_handle) == ESP_OK, NULL, "Encoder deinitialization failed. PCNT unit stop fail.");
ZH_ERROR_CHECK_CONT(pcnt_unit_disable((*handle)->pcnt_unit_handle) == ESP_OK, NULL, "Encoder deinitialization failed. PCNT unit disable fail.");
ZH_ERROR_CHECK_CONT(pcnt_unit_remove_watch_point((*handle)->pcnt_unit_handle, (*handle)->pcnt_counts_per_step) == ESP_OK, NULL, "Encoder deinitialization failed. PCNT unit remove watch point fail.");
ZH_ERROR_CHECK_CONT(pcnt_unit_remove_watch_point((*handle)->pcnt_unit_handle, -(*handle)->pcnt_counts_per_step) == ESP_OK, NULL, "Encoder deinitialization failed. PCNT unit remove watch point fail.");
ZH_ERROR_CHECK_CONT(pcnt_del_channel((*handle)->pcnt_channel_a_handle) == ESP_OK, NULL, "Encoder deinitialization failed. PCNT delete channel fail.");
ZH_ERROR_CHECK_CONT(pcnt_del_channel((*handle)->pcnt_channel_b_handle) == ESP_OK, NULL, "Encoder deinitialization failed. PCNT delete channel fail.");
ZH_ERROR_CHECK_CONT(pcnt_del_unit((*handle)->pcnt_unit_handle) == ESP_OK, NULL, "Encoder deinitialization failed. PCNT delete unit fail.");
if ((*handle)->s_gpio_number != GPIO_NUM_MAX)
{
ZH_ERROR_CHECK(gpio_isr_handler_remove((gpio_num_t)handle->s_gpio_number) == ESP_OK, ESP_FAIL, NULL, "Encoder deinitialization failed. Remove GPIO isr handler failed.");
ZH_ERROR_CHECK(gpio_reset_pin((gpio_num_t)handle->s_gpio_number) == ESP_OK, ESP_FAIL, NULL, "Encoder deinitialization failed. Reset GPIO failed.");
}
if (_encoder_counter == 1)
{
vQueueDelete(_queue_handle);
vTaskDelete(zh_encoder);
}
handle->is_initialized = false;
--_encoder_counter;
for (uint8_t i = 0; i < sizeof(_encoder_number_matrix); ++i)
{
if (_encoder_number_matrix[i] == handle->encoder_number)
ZH_ERROR_CHECK_CONT(gpio_isr_handler_remove((*handle)->s_gpio_number) == ESP_OK, NULL, "Encoder deinitialization failed. Remove GPIO isr handler failed.");
ZH_ERROR_CHECK_CONT(gpio_reset_pin((*handle)->s_gpio_number) == ESP_OK, NULL, "Encoder deinitialization failed. Reset GPIO failed.");
if ((*handle)->button_timer != NULL)
{
_encoder_number_matrix[i] = 0;
break;
xTimerStop((*handle)->button_timer, portMAX_DELAY);
xTimerDelete((*handle)->button_timer, portMAX_DELAY);
(*handle)->button_timer = NULL;
}
}
int32_t index = 0;
ZH_ERROR_CHECK(zh_vector_find_item(&_vector, &(*handle)->encoder_number, &index) == ESP_OK, ESP_ERR_INVALID_ARG, NULL, "Encoder deinitialization failed. Failed to find vector item.");
ZH_ERROR_CHECK(zh_vector_delete_item(&_vector, (uint16_t)index) == ESP_OK, ESP_FAIL, NULL, "Encoder deinitialization failed. Vector delete item failed.");
uint16_t vector_size = 0;
ZH_ERROR_CHECK(zh_vector_get_size(&_vector, &vector_size) == ESP_OK, ESP_FAIL, NULL, "Encoder deinitialization failed. Failed to get vector size.");
if (vector_size == 0)
{
if (zh_encoder != NULL)
{
vTaskDelete(zh_encoder);
zh_encoder = NULL;
}
if (_queue_handle != NULL)
{
vQueueDelete(_queue_handle);
_queue_handle = NULL;
}
ZH_ERROR_CHECK_CONT(zh_vector_free(&_vector) == ESP_OK, NULL, "Encoder deinitialization failed. Free vector failed.");
}
heap_caps_free(*handle);
*handle = NULL;
ZH_LOGI("Encoder deinitialization completed successfully.");
return ESP_OK;
}
esp_err_t zh_encoder_reinit(zh_encoder_handle_t *handle, float min, float max, float step) // -V2008
esp_err_t zh_encoder_reinit(zh_encoder_handle_t **handle, float min, float max, float step)
{
ZH_LOGI("Encoder reinitialization started.");
ZH_ERROR_CHECK(handle != NULL, ESP_ERR_INVALID_ARG, NULL, "Encoder reinitialization failed. Invalid argument.");
ZH_ERROR_CHECK(handle->is_initialized == true, ESP_FAIL, NULL, "Encoder reinitialization failed. Encoder not initialized.");
ZH_ERROR_CHECK(handle != NULL && *handle != NULL, ESP_ERR_INVALID_ARG, NULL, "Encoder reinitialization failed. Invalid argument.");
ZH_ERROR_CHECK(max > min, ESP_ERR_INVALID_ARG, NULL, "Encoder reinitialization failed. Invalid encoder min/max value.");
ZH_ERROR_CHECK(step > 0, ESP_ERR_INVALID_ARG, NULL, "Encoder reinitialization failed. Invalid encoder step.");
taskENTER_CRITICAL(&_spinlock);
handle->encoder_min_value = min;
handle->encoder_max_value = max;
handle->encoder_step = step;
handle->encoder_position = (handle->encoder_min_value + handle->encoder_max_value) / 2;
(*handle)->encoder_min_value = min;
(*handle)->encoder_max_value = max;
(*handle)->encoder_step = step;
(*handle)->encoder_position = ((*handle)->encoder_min_value + (*handle)->encoder_max_value) / 2;
(*handle)->count_accumulator = 0;
taskEXIT_CRITICAL(&_spinlock);
ZH_ERROR_CHECK(pcnt_unit_clear_count((*handle)->pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "Encoder reinitialization failed. Internal error.");
ZH_LOGI("Encoder reinitialization completed successfully.");
return ESP_OK;
}
esp_err_t zh_encoder_set(zh_encoder_handle_t *handle, float position)
esp_err_t zh_encoder_set(zh_encoder_handle_t **handle, float position)
{
ZH_LOGI("Encoder set position started.");
ZH_ERROR_CHECK(handle != NULL, ESP_ERR_INVALID_ARG, NULL, "Encoder set position failed. Invalid argument.");
ZH_ERROR_CHECK(handle->is_initialized == true, ESP_FAIL, NULL, "Encoder set position failed. Encoder not initialized.");
ZH_ERROR_CHECK(position <= handle->encoder_max_value && position >= handle->encoder_min_value, ESP_ERR_INVALID_ARG, NULL, "Encoder set position failed. Invalid argument.");
ZH_ERROR_CHECK(handle != NULL && *handle != NULL, ESP_ERR_INVALID_ARG, NULL, "Encoder set position failed. Invalid argument.");
taskENTER_CRITICAL(&_spinlock);
handle->encoder_position = position;
ZH_ERROR_CHECK(position <= (*handle)->encoder_max_value && position >= (*handle)->encoder_min_value, ESP_ERR_INVALID_ARG, taskEXIT_CRITICAL(&_spinlock), "Encoder set position failed. Invalid argument.");
(*handle)->encoder_position = position;
(*handle)->count_accumulator = 0;
taskEXIT_CRITICAL(&_spinlock);
ZH_ERROR_CHECK(pcnt_unit_clear_count((*handle)->pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "Encoder set position failed. Internal error.");
ZH_LOGI("Encoder set position completed successfully.");
return ESP_OK;
}
esp_err_t zh_encoder_get(const zh_encoder_handle_t *handle, float *position)
esp_err_t zh_encoder_get(zh_encoder_handle_t **handle, float *position)
{
ZH_LOGI("Encoder get position started.");
ZH_ERROR_CHECK(handle != NULL && position != NULL, ESP_ERR_INVALID_ARG, NULL, "Encoder get position failed. Invalid argument.");
ZH_ERROR_CHECK(handle->is_initialized == true, ESP_FAIL, NULL, "Encoder get position failed. Encoder not initialized.");
*position = handle->encoder_position;
ZH_ERROR_CHECK(handle != NULL && *handle != NULL && position != NULL, ESP_ERR_INVALID_ARG, NULL, "Encoder get position failed. Invalid argument.");
taskENTER_CRITICAL(&_spinlock);
*position = (*handle)->encoder_position;
taskEXIT_CRITICAL(&_spinlock);
ZH_LOGI("Encoder get position completed successfully.");
return ESP_OK;
}
esp_err_t zh_encoder_reset(zh_encoder_handle_t *handle)
esp_err_t zh_encoder_reset(zh_encoder_handle_t **handle)
{
ZH_LOGI("Encoder reset started.");
ZH_ERROR_CHECK(handle != NULL, ESP_ERR_INVALID_ARG, NULL, "Encoder reset failed. Invalid argument.");
ZH_ERROR_CHECK(handle->is_initialized == true, ESP_FAIL, NULL, "Encoder reset failed. Encoder not initialized.");
ZH_ERROR_CHECK(handle != NULL && *handle != NULL, ESP_ERR_INVALID_ARG, NULL, "Encoder reset failed. Invalid argument.");
taskENTER_CRITICAL(&_spinlock);
handle->encoder_position = (handle->encoder_min_value + handle->encoder_max_value) / 2;
(*handle)->encoder_position = ((*handle)->encoder_min_value + (*handle)->encoder_max_value) / 2;
(*handle)->count_accumulator = 0;
taskEXIT_CRITICAL(&_spinlock);
ZH_ERROR_CHECK(pcnt_unit_clear_count((*handle)->pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "Encoder reset failed. Internal error.");
ZH_LOGI("Encoder reset completed successfully.");
return ESP_OK;
}
@@ -176,32 +357,44 @@ void zh_encoder_reset_stats(void)
ZH_LOGI("Error statistic reset successfully.");
}
static esp_err_t _zh_encoder_validate_config(const zh_encoder_init_config_t *config, zh_encoder_handle_t *handle) // -V2008
static esp_err_t _zh_encoder_validate_config(const zh_encoder_init_config_t *config, zh_encoder_handle_t *handle)
{
ZH_ERROR_CHECK(config->task_priority >= 1 && config->stack_size >= configMINIMAL_STACK_SIZE, ESP_ERR_INVALID_ARG, NULL, "Invalid task settings.");
ZH_ERROR_CHECK(config->queue_size >= 1, ESP_ERR_INVALID_ARG, NULL, "Invalid queue size.");
ZH_ERROR_CHECK(config->encoder_max_value > config->encoder_min_value, ESP_ERR_INVALID_ARG, NULL, "Invalid encoder min/max value.");
ZH_ERROR_CHECK(config->encoder_step > 0, ESP_ERR_INVALID_ARG, NULL, "Invalid encoder step.");
ZH_ERROR_CHECK(config->encoder_number > 0, ESP_ERR_INVALID_ARG, NULL, "Invalid encoder number.");
for (uint8_t i = 0; i < sizeof(_encoder_number_matrix); ++i)
ZH_ERROR_CHECK(config->pcnt_counts_per_step > 0, ESP_ERR_INVALID_ARG, NULL, "Invalid pcnt_counts_per_step.");
if (_vector != NULL)
{
ZH_ERROR_CHECK(config->encoder_number != _encoder_number_matrix[i], ESP_ERR_INVALID_ARG, NULL, "Encoder number already present.");
int32_t index = 0;
ZH_ERROR_CHECK(zh_vector_find_item(&_vector, &config->encoder_number, &index) == ESP_ERR_NOT_FOUND, ESP_ERR_INVALID_ARG, NULL, "Encoder number already present.");
}
handle->encoder_number = config->encoder_number;
handle->encoder_min_value = config->encoder_min_value;
handle->encoder_max_value = config->encoder_max_value;
handle->encoder_step = config->encoder_step;
handle->pcnt_counts_per_step = config->pcnt_counts_per_step;
handle->encoder_position = (handle->encoder_min_value + handle->encoder_max_value) / 2;
handle->s_gpio_number = GPIO_NUM_MAX;
return ESP_OK;
}
static esp_err_t _zh_encoder_pcnt_init(const zh_encoder_init_config_t *config, zh_encoder_handle_t *handle) // -V2008
static esp_err_t _zh_encoder_pcnt_init(const zh_encoder_init_config_t *config, zh_encoder_handle_t *handle)
{
ZH_ERROR_CHECK(config->a_gpio_number < GPIO_NUM_MAX && config->b_gpio_number < GPIO_NUM_MAX, ESP_ERR_INVALID_ARG, NULL, "Invalid GPIO number.")
ZH_ERROR_CHECK(config->a_gpio_number != config->b_gpio_number, ESP_ERR_INVALID_ARG, NULL, "Encoder A and B GPIO is same.")
gpio_config_t io_config = {
.pin_bit_mask = (1ULL << config->a_gpio_number) | (1ULL << config->b_gpio_number),
.mode = GPIO_MODE_INPUT,
.pull_up_en = config->pullup == true ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ZH_ERROR_CHECK(gpio_config(&io_config) == ESP_OK, ESP_FAIL, NULL, "GPIO initialization failed.");
pcnt_unit_config_t pcnt_unit_config = {
.high_limit = 10,
.low_limit = -10,
.high_limit = ZH_ENCODER_PCNT_HIGH_LIMIT,
.low_limit = ZH_ENCODER_PCNT_LOW_LIMIT,
};
pcnt_unit_handle_t pcnt_unit_handle = NULL;
ZH_ERROR_CHECK(pcnt_new_unit(&pcnt_unit_config, &pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT initialization failed.");
@@ -209,91 +402,86 @@ static esp_err_t _zh_encoder_pcnt_init(const zh_encoder_init_config_t *config, z
.max_glitch_ns = 1000,
};
ZH_ERROR_CHECK(pcnt_unit_set_glitch_filter(pcnt_unit_handle, &pcnt_glitch_filter_config) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail."), "PCNT initialization failed.");
{ZH_ERROR_CHECK_CONT(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, NULL, "PCNT delete unit fail.")}, "PCNT initialization failed.");
pcnt_chan_config_t pcnt_chan_a_config = {
.edge_gpio_num = config->a_gpio_number,
.level_gpio_num = config->b_gpio_number,
};
pcnt_channel_handle_t pcnt_channel_a_handle = NULL;
ZH_ERROR_CHECK(pcnt_new_channel(pcnt_unit_handle, &pcnt_chan_a_config, &pcnt_channel_a_handle) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail."), "PCNT initialization failed.");
{ZH_ERROR_CHECK_CONT(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, NULL, "PCNT delete unit fail.")}, "PCNT initialization failed.");
pcnt_chan_config_t pcnt_chan_b_config = {
.edge_gpio_num = config->b_gpio_number,
.level_gpio_num = config->a_gpio_number,
};
pcnt_channel_handle_t pcnt_channel_b_handle = NULL;
ZH_ERROR_CHECK(pcnt_new_channel(pcnt_unit_handle, &pcnt_chan_b_config, &pcnt_channel_b_handle) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail."), "PCNT initialization failed.");
ZH_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_channel_a_handle, PCNT_CHANNEL_EDGE_ACTION_DECREASE, PCNT_CHANNEL_EDGE_ACTION_HOLD) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail."), "PCNT initialization failed.");
ZH_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_channel_a_handle, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_HOLD) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail."), "PCNT initialization failed.");
ZH_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_channel_b_handle, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_HOLD) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail."), "PCNT initialization failed.");
ZH_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_channel_b_handle, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_HOLD) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail."), "PCNT initialization failed.");
ZH_ERROR_CHECK(pcnt_unit_add_watch_point(pcnt_unit_handle, ZH_ENCODER_DIRECTION_CW) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail."), "PCNT initialization failed.");
ZH_ERROR_CHECK(pcnt_unit_add_watch_point(pcnt_unit_handle, ZH_ENCODER_DIRECTION_CCW) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(pcnt_unit_handle, ZH_ENCODER_DIRECTION_CW) == ESP_OK, ESP_FAIL, NULL, "PCNT unit remove watch point fail.");
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail."), "PCNT initialization failed.");
{ZH_ERROR_CHECK_CONT(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK_CONT(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, NULL, "PCNT delete unit fail.")}, "PCNT initialization failed.");
ZH_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_channel_a_handle, PCNT_CHANNEL_EDGE_ACTION_DECREASE, PCNT_CHANNEL_EDGE_ACTION_INCREASE) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK_CONT(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK_CONT(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK_CONT(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, NULL, "PCNT delete unit fail.")}, "PCNT initialization failed.");
ZH_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_channel_a_handle, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK_CONT(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK_CONT(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK_CONT(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, NULL, "PCNT delete unit fail.")}, "PCNT initialization failed.");
ZH_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_channel_b_handle, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_DECREASE) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK_CONT(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK_CONT(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK_CONT(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, NULL, "PCNT delete unit fail.")}, "PCNT initialization failed.");
ZH_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_channel_b_handle, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK_CONT(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK_CONT(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK_CONT(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, NULL, "PCNT delete unit fail.")}, "PCNT initialization failed.");
ZH_ERROR_CHECK(pcnt_unit_add_watch_point(pcnt_unit_handle, handle->pcnt_counts_per_step) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail.")}, "PCNT initialization failed.");
ZH_ERROR_CHECK(pcnt_unit_add_watch_point(pcnt_unit_handle, -handle->pcnt_counts_per_step) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(pcnt_unit_handle, handle->pcnt_counts_per_step) == ESP_OK, ESP_FAIL, NULL, "PCNT unit remove watch point fail.")};
{ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail.")}, "PCNT initialization failed.");
pcnt_event_callbacks_t cbs = {
.on_reach = _zh_encoder_isr_handler,
};
ZH_ERROR_CHECK(pcnt_unit_register_event_callbacks(pcnt_unit_handle, &cbs, NULL) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(pcnt_unit_handle, ZH_ENCODER_DIRECTION_CW) == ESP_OK, ESP_FAIL, NULL, "PCNT unit remove watch point fail.");
ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(pcnt_unit_handle, ZH_ENCODER_DIRECTION_CCW) == ESP_OK, ESP_FAIL, NULL, "PCNT unit remove watch point fail.");
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail."), "PCNT initialization failed.");
ZH_ERROR_CHECK(pcnt_unit_register_event_callbacks(pcnt_unit_handle, &cbs, handle) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(pcnt_unit_handle, handle->pcnt_counts_per_step) == ESP_OK, ESP_FAIL, NULL, "PCNT unit remove watch point fail.")};
{ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(pcnt_unit_handle, -handle->pcnt_counts_per_step) == ESP_OK, ESP_FAIL, NULL, "PCNT unit remove watch point fail.")};
{ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail.")}, "PCNT initialization failed.");
ZH_ERROR_CHECK(pcnt_unit_enable(pcnt_unit_handle) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(pcnt_unit_handle, ZH_ENCODER_DIRECTION_CW) == ESP_OK, ESP_FAIL, NULL, "PCNT unit remove watch point fail.");
ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(pcnt_unit_handle, ZH_ENCODER_DIRECTION_CCW) == ESP_OK, ESP_FAIL, NULL, "PCNT unit remove watch point fail.");
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail."), "PCNT initialization failed.");
{ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(pcnt_unit_handle, handle->pcnt_counts_per_step) == ESP_OK, ESP_FAIL, NULL, "PCNT unit remove watch point fail.")};
{ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(pcnt_unit_handle, -handle->pcnt_counts_per_step) == ESP_OK, ESP_FAIL, NULL, "PCNT unit remove watch point fail.")};
{ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail.")}, "PCNT initialization failed.");
ZH_ERROR_CHECK(pcnt_unit_clear_count(pcnt_unit_handle) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(pcnt_unit_disable(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT unit disable fail.");
ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(pcnt_unit_handle, ZH_ENCODER_DIRECTION_CW) == ESP_OK, ESP_FAIL, NULL, "PCNT unit remove watch point fail.");
ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(pcnt_unit_handle, ZH_ENCODER_DIRECTION_CCW) == ESP_OK, ESP_FAIL, NULL, "PCNT unit remove watch point fail.");
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail."), "PCNT initialization failed.");
{ZH_ERROR_CHECK(pcnt_unit_disable(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT unit disable fail.")};
{ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(pcnt_unit_handle, handle->pcnt_counts_per_step) == ESP_OK, ESP_FAIL, NULL, "PCNT unit remove watch point fail.")};
{ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(pcnt_unit_handle, -handle->pcnt_counts_per_step) == ESP_OK, ESP_FAIL, NULL, "PCNT unit remove watch point fail.")};
{ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail.")}, "PCNT initialization failed.");
ZH_ERROR_CHECK(pcnt_unit_start(pcnt_unit_handle) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(pcnt_unit_disable(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT unit disable fail.");
ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(pcnt_unit_handle, ZH_ENCODER_DIRECTION_CW) == ESP_OK, ESP_FAIL, NULL, "PCNT unit remove watch point fail.");
ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(pcnt_unit_handle, ZH_ENCODER_DIRECTION_CCW) == ESP_OK, ESP_FAIL, NULL, "PCNT unit remove watch point fail.");
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.");
ZH_ERROR_CHECK(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail."), "PCNT initialization failed.");
if (config->pullup == false)
{
ZH_ERROR_CHECK(gpio_pullup_dis((gpio_num_t)config->a_gpio_number) == ESP_OK, ESP_FAIL, NULL, "GPIO pullup disable fail.");
ZH_ERROR_CHECK(gpio_pullup_dis((gpio_num_t)config->b_gpio_number) == ESP_OK, ESP_FAIL, NULL, "GPIO pullup disable fail.");
}
{ZH_ERROR_CHECK(pcnt_unit_disable(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT unit disable fail.")};
{ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(pcnt_unit_handle, handle->pcnt_counts_per_step) == ESP_OK, ESP_FAIL, NULL, "PCNT unit remove watch point fail.")};
{ZH_ERROR_CHECK(pcnt_unit_remove_watch_point(pcnt_unit_handle, -handle->pcnt_counts_per_step) == ESP_OK, ESP_FAIL, NULL, "PCNT unit remove watch point fail.")};
{ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_a_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK(pcnt_del_channel(pcnt_channel_b_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete channel fail.")};
{ZH_ERROR_CHECK(pcnt_del_unit(pcnt_unit_handle) == ESP_OK, ESP_FAIL, NULL, "PCNT delete unit fail.")}, "PCNT initialization failed.");
handle->pcnt_unit_handle = pcnt_unit_handle;
handle->pcnt_channel_a_handle = pcnt_channel_a_handle;
handle->pcnt_channel_b_handle = pcnt_channel_b_handle;
return ESP_OK;
}
static esp_err_t _zh_encoder_gpio_init(const zh_encoder_init_config_t *config, zh_encoder_handle_t *handle) // -V2008
static esp_err_t _zh_encoder_gpio_init(const zh_encoder_init_config_t *config, zh_encoder_handle_t *handle)
{
ZH_ERROR_CHECK(config->s_gpio_number <= GPIO_NUM_MAX, ESP_ERR_INVALID_ARG, NULL, "Invalid GPIO number.")
ZH_ERROR_CHECK(config->a_gpio_number != config->s_gpio_number && config->b_gpio_number != config->s_gpio_number, ESP_ERR_INVALID_ARG, NULL, "Encoder GPIO and button GPIO is same.")
ZH_ERROR_CHECK(config->s_gpio_number <= GPIO_NUM_MAX, ESP_ERR_INVALID_ARG, NULL, "Invalid GPIO number.");
ZH_ERROR_CHECK(config->a_gpio_number != config->s_gpio_number && config->b_gpio_number != config->s_gpio_number, ESP_ERR_INVALID_ARG, NULL, "Encoder GPIO and button GPIO is the same.");
if (config->s_gpio_number != GPIO_NUM_MAX)
{
gpio_config_t pin_config = {
@@ -302,47 +490,72 @@ static esp_err_t _zh_encoder_gpio_init(const zh_encoder_init_config_t *config, z
.pin_bit_mask = (1ULL << config->s_gpio_number),
.intr_type = GPIO_INTR_ANYEDGE};
ZH_ERROR_CHECK(gpio_config(&pin_config) == ESP_OK, ESP_FAIL, NULL, "GPIO initialization failed.");
uint32_t debounce_ms = (config->s_gpio_debounce_time + 999) / 1000;
if (debounce_ms < 1)
{
debounce_ms = 1;
}
handle->button_timer = xTimerCreate("zh_encoder_button_timer", pdMS_TO_TICKS(debounce_ms), pdFALSE, handle, _zh_encoder_button_timer_cb);
ZH_ERROR_CHECK(handle->button_timer != NULL, ESP_FAIL, gpio_isr_handler_remove(config->s_gpio_number); gpio_reset_pin(config->s_gpio_number), "Failed to create button debounce timer.");
handle->s_gpio_number = config->s_gpio_number;
handle->s_gpio_status = gpio_get_level(config->s_gpio_number);
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((gpio_num_t)config->s_gpio_number) == ESP_OK, ESP_FAIL, NULL, "Reset GPIO failed."), "Failed install isr service.");
ZH_ERROR_CHECK(gpio_isr_handler_add((gpio_num_t)config->s_gpio_number, _zh_encoder_button_isr_handler, handle) == ESP_OK, ESP_FAIL,
ZH_ERROR_CHECK(gpio_reset_pin((gpio_num_t)config->s_gpio_number) == ESP_OK, ESP_FAIL, NULL, "Reset GPIO failed."), "Interrupt initialization failed.");
handle->s_gpio_number = config->s_gpio_number;
handle->s_gpio_debounce_time = config->s_gpio_debounce_time;
handle->s_gpio_status = gpio_get_level((gpio_num_t)config->s_gpio_number);
{ZH_ERROR_CHECK_CONT(gpio_reset_pin(config->s_gpio_number) == ESP_OK, NULL, "Reset GPIO failed.")};
xTimerDelete(handle->button_timer, portMAX_DELAY); handle->button_timer = NULL, "Failed install isr service.");
ZH_ERROR_CHECK(gpio_isr_handler_add(config->s_gpio_number, _zh_encoder_button_isr_handler, handle) == ESP_OK, ESP_FAIL,
{ZH_ERROR_CHECK_CONT(gpio_reset_pin(config->s_gpio_number) == ESP_OK, NULL, "Reset GPIO failed.")};
xTimerDelete(handle->button_timer, portMAX_DELAY); handle->button_timer = NULL, "Interrupt initialization failed.");
}
return ESP_OK;
}
static esp_err_t _zh_encoder_resources_init(const zh_encoder_init_config_t *config)
{
if (_encoder_counter == 0)
uint16_t vector_size = 0;
zh_vector_get_size(&_vector, &vector_size);
if (vector_size == 1)
{
_queue_handle = xQueueCreate(config->queue_size, sizeof(int));
_queue_handle = xQueueCreate(config->queue_size, sizeof(zh_encoder_queue_t));
ZH_ERROR_CHECK(_queue_handle != NULL, ESP_FAIL, NULL, "Failed to create queue.");
}
return ESP_OK;
}
static esp_err_t _zh_encoder_task_init(const zh_encoder_init_config_t *config, zh_encoder_handle_t *handle)
static esp_err_t _zh_encoder_task_init(const zh_encoder_init_config_t *config)
{
if (_encoder_counter == 0)
uint16_t vector_size = 0;
zh_vector_get_size(&_vector, &vector_size);
if (vector_size == 1)
{
ZH_ERROR_CHECK(xTaskCreatePinnedToCore(&_zh_encoder_isr_processing_task, "zh_encoder_isr_processing", config->stack_size, handle, config->task_priority, &zh_encoder, tskNO_AFFINITY) == pdPASS,
ZH_ERROR_CHECK(xTaskCreatePinnedToCore(&_zh_encoder_isr_processing_task, "zh_encoder_isr_processing", config->stack_size, NULL, config->task_priority, &zh_encoder, tskNO_AFFINITY) == pdPASS,
ESP_FAIL, NULL, "Failed to create isr processing task.");
}
return ESP_OK;
}
static bool IRAM_ATTR _zh_encoder_isr_handler(pcnt_unit_handle_t unit, const pcnt_watch_event_data_t *edata, void *user_ctx)
static bool _zh_encoder_isr_handler(pcnt_unit_handle_t unit, const pcnt_watch_event_data_t *edata, void *user_ctx)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
esp_err_t err = pcnt_unit_clear_count(unit);
if (err != ESP_OK)
(void)edata;
zh_encoder_handle_t *encoder_handle = (zh_encoder_handle_t *)user_ctx;
zh_encoder_queue_t encoder_queue = {0};
int count = 0;
taskENTER_CRITICAL_ISR(&_spinlock);
if (pcnt_unit_get_count(unit, &count) != ESP_OK)
{
taskEXIT_CRITICAL_ISR(&_spinlock);
return false;
}
if (xQueueSendFromISR(_queue_handle, &edata->watch_point_value, &xHigherPriorityTaskWoken) != pdTRUE)
if (pcnt_unit_clear_count(unit) != ESP_OK)
{
taskEXIT_CRITICAL_ISR(&_spinlock);
return false;
}
taskEXIT_CRITICAL_ISR(&_spinlock);
encoder_queue.count = count;
encoder_queue.handle = encoder_handle;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
if (xQueueSendFromISR(_queue_handle, &encoder_queue, &xHigherPriorityTaskWoken) != pdTRUE)
{
++_stats.queue_overflow_error;
}
@@ -353,82 +566,80 @@ static bool IRAM_ATTR _zh_encoder_isr_handler(pcnt_unit_handle_t unit, const pcn
return false;
}
static void IRAM_ATTR _zh_encoder_isr_processing_task(void *pvParameter)
static void _zh_encoder_isr_processing_task(void *pvParameter)
{
zh_encoder_handle_t *encoder_handle = (zh_encoder_handle_t *)pvParameter;
int pcnt_count = {0};
bool is_value_changed = false;
while (xQueueReceive(_queue_handle, &pcnt_count, portMAX_DELAY) == pdTRUE)
(void)pvParameter;
zh_encoder_queue_t encoder_queue = {0};
while (xQueueReceive(_queue_handle, &encoder_queue, portMAX_DELAY) == pdTRUE)
{
switch (pcnt_count)
zh_encoder_handle_t *handle = encoder_queue.handle;
bool position_changed = false;
bool encoder_status = false;
float encoder_position = 0.0;
uint8_t encoder_number = 0;
taskENTER_CRITICAL(&_spinlock);
int32_t total = handle->count_accumulator + encoder_queue.count;
int32_t full_steps = total / (int32_t)handle->pcnt_counts_per_step;
handle->count_accumulator = total - full_steps * (int32_t)handle->pcnt_counts_per_step;
if (full_steps != 0)
{
case ZH_ENCODER_DIRECTION_CW:
if (encoder_handle->encoder_position < encoder_handle->encoder_max_value)
float candidate = handle->encoder_position + (float)full_steps * handle->encoder_step;
if (candidate > handle->encoder_max_value)
{
encoder_handle->encoder_position = encoder_handle->encoder_position + encoder_handle->encoder_step;
if (encoder_handle->encoder_position > encoder_handle->encoder_max_value)
{
encoder_handle->encoder_position = encoder_handle->encoder_max_value;
}
is_value_changed = true;
candidate = handle->encoder_max_value;
}
break;
case ZH_ENCODER_DIRECTION_CCW:
if (encoder_handle->encoder_position > encoder_handle->encoder_min_value)
else if (candidate < handle->encoder_min_value)
{
encoder_handle->encoder_position = encoder_handle->encoder_position - encoder_handle->encoder_step;
if (encoder_handle->encoder_position < encoder_handle->encoder_min_value)
{
encoder_handle->encoder_position = encoder_handle->encoder_min_value;
}
is_value_changed = true;
candidate = handle->encoder_min_value;
}
if (fabsf(candidate) < handle->encoder_step * 0.5f)
{
candidate = 0.0f;
}
if (candidate != handle->encoder_position)
{
handle->encoder_position = candidate;
position_changed = true;
encoder_status = full_steps > 0 ? true : false;
encoder_position = candidate;
encoder_number = handle->encoder_number;
}
break;
default:
break;
}
if (is_value_changed == true)
taskEXIT_CRITICAL(&_spinlock);
if (position_changed == true)
{
is_value_changed = false;
zh_encoder_event_on_isr_t encoder_data = {0};
encoder_data.encoder_number = encoder_handle->encoder_number;
encoder_data.encoder_position = encoder_handle->encoder_position;
esp_err_t err = esp_event_post(ZH_ENCODER, ZH_ENCODER_EVENT, &encoder_data, sizeof(zh_encoder_event_on_isr_t), 1000 / portTICK_PERIOD_MS);
if (err != ESP_OK)
{
++_stats.event_post_error;
ZH_LOGE("Encoder isr processing failed. Failed to post interrupt event.", err);
}
encoder_data.encoder_number = encoder_number;
encoder_data.encoder_position = encoder_position;
encoder_data.encoder_status = encoder_status;
ZH_ERROR_CHECK_CONT(esp_event_post(ZH_ENCODER, ZH_ENCODER_EVENT, &encoder_data, sizeof(zh_encoder_event_on_isr_t), pdMS_TO_TICKS(10)) == ESP_OK, ++_stats.event_post_error, "Encoder isr processing failed. Failed to post interrupt event.");
}
_stats.min_stack_size = (uint32_t)uxTaskGetStackHighWaterMark(NULL);
}
vTaskDelete(NULL);
}
static void IRAM_ATTR _zh_encoder_button_isr_handler(void *arg)
static void _zh_encoder_button_timer_cb(TimerHandle_t xTimer)
{
zh_encoder_handle_t *encoder_handle = (zh_encoder_handle_t *)pvTimerGetTimerID(xTimer);
bool s_gpio_status = gpio_get_level(encoder_handle->s_gpio_number);
if (encoder_handle->s_gpio_status != s_gpio_status)
{
encoder_handle->s_gpio_status = s_gpio_status;
zh_encoder_button_event_on_isr_t encoder_data = {0};
encoder_data.encoder_number = encoder_handle->encoder_number;
encoder_data.button_status = s_gpio_status;
ZH_ERROR_CHECK_CONT(esp_event_post(ZH_ENCODER, ZH_BUTTON_EVENT, &encoder_data, sizeof(encoder_data), pdMS_TO_TICKS(10)) == ESP_OK, ++_stats.event_post_error, "Encoder button isr processing failed. Failed to post interrupt event.");
}
}
static void _zh_encoder_button_isr_handler(void *arg)
{
zh_encoder_handle_t *encoder_handle = (zh_encoder_handle_t *)arg;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
uint64_t _current_us = esp_timer_get_time();
if (_current_us - encoder_handle->s_gpio_prev_time >= encoder_handle->s_gpio_debounce_time)
{
bool s_gpio_status = gpio_get_level((gpio_num_t)encoder_handle->s_gpio_number);
if (encoder_handle->s_gpio_status != s_gpio_status)
{
encoder_handle->s_gpio_status = s_gpio_status;
zh_encoder_button_event_on_isr_t encoder_data = {0};
encoder_data.encoder_number = encoder_handle->encoder_number;
encoder_data.button_status = encoder_handle->s_gpio_status;
esp_err_t err = esp_event_isr_post(ZH_ENCODER, ZH_BUTTON_EVENT, &encoder_data, sizeof(zh_encoder_button_event_on_isr_t), &xHigherPriorityTaskWoken);
if (err != ESP_OK)
{
++_stats.event_post_error;
}
}
}
encoder_handle->s_gpio_prev_time = _current_us;
xTimerResetFromISR(encoder_handle->button_timer, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken == pdTRUE)
{
portYIELD_FROM_ISR();
};
}
}