From a6bb36a50f2b421cc4cdad641fafc73e0520c765 Mon Sep 17 00:00:00 2001 From: Alexey Zholtikov Date: Fri, 2 Jan 2026 12:06:32 +0300 Subject: [PATCH] feat: initial --- CMakeLists.txt | 2 +- README.md | 57 ++++++++++++++- component.mk | 0 include/main.h | 0 include/zh_inclinometer.h | 76 +++++++++++++++++++ main.c | 0 version.txt | 1 + zh_inclinometer.c | 149 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 282 insertions(+), 3 deletions(-) delete mode 100644 component.mk delete mode 100644 include/main.h create mode 100644 include/zh_inclinometer.h delete mode 100644 main.c create mode 100644 zh_inclinometer.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ab5c51..43aedfd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1 +1 @@ -idf_component_register(SRCS "main.c" INCLUDE_DIRS "include") \ No newline at end of file +idf_component_register(SRCS "zh_inclinometer.c" INCLUDE_DIRS "include" REQUIRES driver) \ No newline at end of file diff --git a/README.md b/README.md index 3df3e4d..95a264a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,56 @@ -# esp_component_template +# ESP32 ESP-IDF component for inclinometer (via rotary encoder) -esp_component_template \ No newline at end of file +## Tested on + +1. [ESP32 ESP-IDF v5.5.1](https://docs.espressif.com/projects/esp-idf/en/v5.5.1/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. + +## Attention + +1. For correct operation, please enable the following settings in the menuconfig: + +```text +GPIO_CTRL_FUNC_IN_IRAM +``` + +## Using + +In an existing project, run the following command to install the components: + +```text +cd ../your_project/components +git clone http://git.zh.com.ru/esp_components/zh_inclinometer +``` + +In the application, add the component: + +```c +#include "zh_inclinometer.h" +``` + +## Examples + +```c +#include "zh_inclinometer.h" + +double inclinometer_position = 0; + +void app_main(void) +{ + esp_log_level_set("zh_inclinometer", ESP_LOG_ERROR); + zh_inclinometer_init_config_t config = ZH_INCLINOMETER_INIT_CONFIG_DEFAULT(); + config.a_gpio_number = GPIO_NUM_26; + config.b_gpio_number = GPIO_NUM_27; + config.encoder_pulses = 3600; + zh_inclinometer_init(&config); + for (;;) + { + zh_inclinometer_get(&inclinometer_position); + printf("Inclinometer position is %0.2f degrees.\n", inclinometer_position); + vTaskDelay(100 / portTICK_PERIOD_MS); + } +} +``` diff --git a/component.mk b/component.mk deleted file mode 100644 index e69de29..0000000 diff --git a/include/main.h b/include/main.h deleted file mode 100644 index e69de29..0000000 diff --git a/include/zh_inclinometer.h b/include/zh_inclinometer.h new file mode 100644 index 0000000..6de5536 --- /dev/null +++ b/include/zh_inclinometer.h @@ -0,0 +1,76 @@ +/** + * @file zh_inclinometer.h + */ + +#pragma once + +#include "esp_log.h" +#include "driver/gpio.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +/** + * @brief Inclinometer initial default values. + */ +#define ZH_INCLINOMETER_INIT_CONFIG_DEFAULT() \ + { \ + .a_gpio_number = GPIO_NUM_MAX, \ + .b_gpio_number = GPIO_NUM_MAX, \ + .encoder_pulses = 0} + +#ifdef __cplusplus +extern "C" +{ +#endif + + /** + * @brief Structure for initial initialization of inclinometer. + */ + typedef struct + { + uint8_t a_gpio_number; /*!< Encoder A GPIO number. */ + uint8_t b_gpio_number; /*!< Encoder B GPIO number. */ + uint16_t encoder_pulses; /*!< Number of pulses per one rotation. */ + } zh_inclinometer_init_config_t; + + /** + * @brief Initialize inclinometer. + * + * @param[in] config Pointer to inclinometer initialized configuration structure. Can point to a temporary variable. + * + * @note Before initialize the inclinometer recommend initialize zh_inclinometer_init_config_t structure with default values. + * + * @code zh_inclinometer_init_config_t config = ZH_INCLINOMETER_INIT_CONFIG_DEFAULT() @endcode + * + * @return ESP_OK if success or an error code otherwise. + */ + esp_err_t zh_inclinometer_init(const zh_inclinometer_init_config_t *config); + + /** + * @brief Deinitialize inclinometer. + * + * @return ESP_OK if success or an error code otherwise. + */ + esp_err_t zh_inclinometer_deinit(void); + + /** + * @brief Get inclinometer position. + * + * @param[out] position inclinometer position. + * + * @return ESP_OK if success or an error code otherwise. + */ + esp_err_t zh_inclinometer_get(double *position); + + /** + * @brief Reset inclinometer position. + * + * @note The inclinometer will be set to 0 position. + * + * @return ESP_OK if success or an error code otherwise. + */ + esp_err_t zh_inclinometer_reset(void); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/main.c b/main.c deleted file mode 100644 index e69de29..0000000 diff --git a/version.txt b/version.txt index e69de29..afaf360 100644 --- a/version.txt +++ b/version.txt @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/zh_inclinometer.c b/zh_inclinometer.c new file mode 100644 index 0000000..9f64358 --- /dev/null +++ b/zh_inclinometer.c @@ -0,0 +1,149 @@ +#include "zh_inclinometer.h" + +#define TAG "zh_inclinometer" + +#define ZH_LOGI(msg, ...) ESP_LOGI(TAG, msg, ##__VA_ARGS__) +#define ZH_LOGE(msg, err, ...) ESP_LOGE(TAG, "[%s:%d:%s] " msg, __FILE__, __LINE__, esp_err_to_name(err), ##__VA_ARGS__) + +#define ZH_ERROR_CHECK(cond, err, cleanup, msg, ...) \ + if (!(cond)) \ + { \ + ZH_LOGE(msg, err, ##__VA_ARGS__); \ + cleanup; \ + return err; \ + } + +#define ZH_ENCODER_DIRECTION_CW 0x10 +#define ZH_ENCODER_DIRECTION_CCW 0x20 + +static const uint8_t _encoder_matrix[7][4] = { + {0x03, 0x02, 0x01, 0x00}, + {0x23, 0x00, 0x01, 0x00}, + {0x13, 0x02, 0x00, 0x00}, + {0x03, 0x05, 0x04, 0x00}, + {0x03, 0x03, 0x04, 0x00}, + {0x03, 0x05, 0x03, 0x00}, +}; + +static portMUX_TYPE _spinlock = portMUX_INITIALIZER_UNLOCKED; + +static double _encoder_step; +volatile static double _encoder_position; +volatile static uint8_t _encoder_state; +static uint8_t _a_gpio_number; +static uint8_t _b_gpio_number; +static bool _is_initialized = false; +static bool _is_prev_gpio_isr_handler = false; + +static esp_err_t _zh_inclinometer_validate_config(const zh_inclinometer_init_config_t *config); +static esp_err_t _zh_inclinometer_gpio_init(const zh_inclinometer_init_config_t *config); +static void _zh_inclinometer_isr_handler(void *arg); + +esp_err_t zh_inclinometer_init(const zh_inclinometer_init_config_t *config) +{ + ZH_LOGI("Inclinometer initialization started."); + esp_err_t err = _zh_inclinometer_validate_config(config); + ZH_ERROR_CHECK(err == ESP_OK, err, NULL, "Inclinometer initialization failed. Initial configuration check failed."); + err = _zh_inclinometer_gpio_init(config); + ZH_ERROR_CHECK(err == ESP_OK, err, NULL, "Inclinometer initialization failed. GPIO initialization failed."); + _encoder_position = 0; + _encoder_step = 360.0 / (double)config->encoder_pulses; + printf("_encoder_step is %0.2f degrees.\n", _encoder_step); + _a_gpio_number = config->a_gpio_number; + _b_gpio_number = config->b_gpio_number; + _is_initialized = true; + ZH_LOGI("Inclinometer initialization completed successfully."); + return ESP_OK; +} + +esp_err_t zh_inclinometer_deinit(void) +{ + ZH_LOGI("Inclinometer deinitialization started."); + ZH_ERROR_CHECK(_is_initialized == true, ESP_FAIL, NULL, "Inclinometer deinitialization failed. Inclinometer not initialized."); + gpio_isr_handler_remove((gpio_num_t)_a_gpio_number); + gpio_isr_handler_remove((gpio_num_t)_b_gpio_number); + gpio_reset_pin((gpio_num_t)_a_gpio_number); + gpio_reset_pin((gpio_num_t)_b_gpio_number); + if (_is_prev_gpio_isr_handler == false) + { + gpio_uninstall_isr_service(); + } + _is_initialized = false; + ZH_LOGI("Inclinometer deinitialization completed successfully."); + return ESP_OK; +} + +esp_err_t zh_inclinometer_get(double *position) +{ + ZH_LOGI("Inclinometer get position started."); + ZH_ERROR_CHECK(position != NULL, ESP_ERR_INVALID_ARG, NULL, "Inclinometer get position failed. Invalid argument."); + ZH_ERROR_CHECK(_is_initialized == true, ESP_FAIL, NULL, "Inclinometer get position failed. Inclinometer not initialized."); + *position = _encoder_position; + ZH_LOGI("Inclinometer get position completed successfully."); + return ESP_OK; +} + +esp_err_t zh_inclinometer_reset(void) +{ + ZH_LOGI("Inclinometer reset started."); + ZH_ERROR_CHECK(_is_initialized == true, ESP_FAIL, NULL, "Inclinometer reset failed. Inclinometer not initialized."); + taskENTER_CRITICAL(&_spinlock); + _encoder_position = 0; + taskEXIT_CRITICAL(&_spinlock); + ZH_LOGI("Inclinometer reset completed successfully."); + return ESP_OK; +} + +static esp_err_t _zh_inclinometer_validate_config(const zh_inclinometer_init_config_t *config) +{ + ZH_ERROR_CHECK(config != NULL, ESP_ERR_INVALID_ARG, NULL, "Invalid configuration."); + ZH_ERROR_CHECK(config->encoder_pulses > 0, ESP_ERR_INVALID_ARG, NULL, "Invalid encoder pulses."); + return ESP_OK; +} + +static esp_err_t _zh_inclinometer_gpio_init(const zh_inclinometer_init_config_t *config) // -V2008 +{ + 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 pin_config = { + .mode = GPIO_MODE_INPUT, + .pin_bit_mask = (1ULL << config->a_gpio_number) | (1ULL << config->b_gpio_number), + .pull_up_en = GPIO_PULLUP_ENABLE, + .intr_type = GPIO_INTR_ANYEDGE}; + esp_err_t err = gpio_config(&pin_config); + ZH_ERROR_CHECK(err == ESP_OK, err, NULL, "GPIO initialization failed."); + err = gpio_install_isr_service(ESP_INTR_FLAG_LOWMED); + ZH_ERROR_CHECK(err == ESP_OK || err == ESP_ERR_INVALID_STATE, err, gpio_reset_pin((gpio_num_t)config->a_gpio_number); gpio_reset_pin((gpio_num_t)config->b_gpio_number), "Failed install isr service."); + if (err == ESP_ERR_INVALID_STATE) + { + _is_prev_gpio_isr_handler = true; + } + err = gpio_isr_handler_add((gpio_num_t)config->a_gpio_number, _zh_inclinometer_isr_handler, NULL); + ZH_ERROR_CHECK(err == ESP_OK, err, gpio_reset_pin((gpio_num_t)config->a_gpio_number); gpio_reset_pin((gpio_num_t)config->b_gpio_number), "Interrupt initialization failed."); + err = gpio_isr_handler_add((gpio_num_t)config->b_gpio_number, _zh_inclinometer_isr_handler, NULL); + if (_is_prev_gpio_isr_handler == true) + { + ZH_ERROR_CHECK(err == ESP_OK, err, gpio_isr_handler_remove((gpio_num_t)config->a_gpio_number); gpio_reset_pin((gpio_num_t)config->a_gpio_number); gpio_reset_pin((gpio_num_t)config->b_gpio_number), "Interrupt initialization failed."); + } + else + { + ZH_ERROR_CHECK(err == ESP_OK, err, gpio_isr_handler_remove((gpio_num_t)config->a_gpio_number); gpio_uninstall_isr_service(); gpio_reset_pin((gpio_num_t)config->a_gpio_number); gpio_reset_pin((gpio_num_t)config->b_gpio_number), "Interrupt initialization failed."); + } + return ESP_OK; +} + +static void IRAM_ATTR _zh_inclinometer_isr_handler(void *arg) +{ + _encoder_state = _encoder_matrix[_encoder_state & 0x0F][(gpio_get_level((gpio_num_t)_b_gpio_number) << 1) | gpio_get_level((gpio_num_t)_a_gpio_number)]; + switch (_encoder_state & 0x30) + { + case ZH_ENCODER_DIRECTION_CW: + _encoder_position = _encoder_position + _encoder_step; + break; + case ZH_ENCODER_DIRECTION_CCW: + _encoder_position = _encoder_position - _encoder_step; + break; + default: + break; + } +} \ No newline at end of file