diff --git a/board_control/main/CMakeLists.txt b/board_control/main/CMakeLists.txt index 72a232c..664223b 100644 --- a/board_control/main/CMakeLists.txt +++ b/board_control/main/CMakeLists.txt @@ -8,6 +8,7 @@ idf_component_register( "http.cpp" "auto.cpp" "uart.cpp" + "encoder.cpp" INCLUDE_DIRS "." ) diff --git a/board_control/main/encoder.cpp b/board_control/main/encoder.cpp new file mode 100644 index 0000000..887335a --- /dev/null +++ b/board_control/main/encoder.cpp @@ -0,0 +1,81 @@ + #include "encoder.h" +extern "C" +{ +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/gpio.h" +#include "esp_log.h" +} + +#include "encoder.hpp" + +//------------------------- +//------- variables ------- +//------------------------- +static const char * TAG = "encoder"; +uint16_t encoderCount; +rotary_encoder_btn_state_t encoderButtonState = {}; +QueueHandle_t encoderQueue = NULL; + +//encoder config +rotary_encoder_t encoderConfig = { + .pin_a = PIN_A, + .pin_b = PIN_B, + .pin_btn = PIN_BUTTON, + .code = 1, + .store = encoderCount, + .index = 0, + .btn_pressed_time_us = 20000, + .btn_state = encoderButtonState +}; + + + +//================================== +//========== encoder_init ========== +//================================== +//initialize encoder +void encoder_init(){ + encoderQueue = xQueueCreate(QUEUE_SIZE, sizeof(rotary_encoder_event_t)); + rotary_encoder_init(encoderQueue); + rotary_encoder_add(&encoderConfig); +} + + + +//================================== +//========== task_encoder ========== +//================================== +//receive and handle encoder events +void task_encoder(void *arg) { + rotary_encoder_event_t ev; //store event data + while (1) { + if (xQueueReceive(encoderQueue, &ev, portMAX_DELAY)) { + //log enocder events + switch (ev.type){ + case RE_ET_CHANGED: + ESP_LOGI(TAG, "Event type: RE_ET_CHANGED, diff: %d", ev.diff); + break; + case RE_ET_BTN_PRESSED: + ESP_LOGI(TAG, "Button pressed"); + break; + case RE_ET_BTN_RELEASED: + ESP_LOGI(TAG, "Button released"); + break; + case RE_ET_BTN_CLICKED: + ESP_LOGI(TAG, "Button clicked"); + break; + case RE_ET_BTN_LONG_PRESSED: + ESP_LOGI(TAG, "Button long-pressed"); + break; + default: + ESP_LOGW(TAG, "Unknown event type"); + break; + } + } + } +} + diff --git a/board_control/main/encoder.hpp b/board_control/main/encoder.hpp new file mode 100644 index 0000000..18881ed --- /dev/null +++ b/board_control/main/encoder.hpp @@ -0,0 +1,18 @@ +extern "C" { +#include "freertos/FreeRTOS.h" // FreeRTOS related headers +#include "freertos/task.h" +#include "encoder.h" +} + +//config +#define QUEUE_SIZE 10 +#define PIN_A GPIO_NUM_25 +#define PIN_B GPIO_NUM_26 +#define PIN_BUTTON GPIO_NUM_27 + + +//init encoder with config in encoder.cpp +void encoder_init(); + +//task that handles encoder events +void task_encoder(void *arg); diff --git a/board_control/main/main.cpp b/board_control/main/main.cpp index 260310a..5537cf9 100644 --- a/board_control/main/main.cpp +++ b/board_control/main/main.cpp @@ -18,6 +18,7 @@ extern "C" #include "uart.hpp" +#include "encoder.hpp" //========================= @@ -28,6 +29,13 @@ extern "C" //#define UART_TEST_ONLY +//========================= +//====== encoder TEST ===== +//========================= +//only start encoder task +#define ENCODER_TEST_ONLY + + //tag for logging static const char * TAG = "main"; @@ -157,7 +165,7 @@ void setLoglevels(void){ //=========== app_main ============ //================================= extern "C" void app_main(void) { -#ifndef UART_TEST_ONLY +#if !defined(ENCODER_TEST_ONLY) && !defined(UART_TEST_ONLY) //enable 5V volate regulator gpio_pad_select_gpio(GPIO_NUM_17); gpio_set_direction(GPIO_NUM_17, GPIO_MODE_OUTPUT); @@ -214,24 +222,35 @@ extern "C" void app_main(void) { // vTaskDelay(2000 / portTICK_PERIOD_MS); // ESP_LOGI(TAG, "initializing http server"); // http_init_server(); - - #endif + //------------------------------------------- //--- create tasks for uart communication --- //------------------------------------------- - +#ifndef ENCODER_TEST_ONLY uart_init(); xTaskCreate(task_uartReceive, "task_uartReceive", 4096, NULL, 10, NULL); xTaskCreate(task_uartSend, "task_uartSend", 4096, NULL, 10, NULL); +#endif + + + //-------------------------------------------- + //----- create task that handles encoder ----- + //-------------------------------------------- +#ifndef UART_TEST_ONLY + encoder_init(); + xTaskCreate(task_encoder, "task_encoder", 4096, NULL, 10, NULL); +#endif + + //--- main loop --- //does nothing except for testing things //--- testing force http mode after startup --- vTaskDelay(5000 / portTICK_PERIOD_MS); - control.changeMode(controlMode_t::HTTP); + //control.changeMode(controlMode_t::HTTP); while(1){ vTaskDelay(1000 / portTICK_PERIOD_MS); //--------------------------------- diff --git a/components/encoder/.eil.yml b/components/encoder/.eil.yml new file mode 100644 index 0000000..d741eb2 --- /dev/null +++ b/components/encoder/.eil.yml @@ -0,0 +1,21 @@ +name: encoder +description: HW timer-based driver for incremental rotary encoders +version: 1.0.0 +groups: + - input +code_owners: + - UncleRus +depends: + - driver + - freertos + - log +thread_safe: yes +targets: + - esp32 + - esp8266 + - esp32s2 + - esp32c3 +license: BSD-3 +copyrights: + - name: UncleRus + year: 2019 diff --git a/components/encoder/CMakeLists.txt b/components/encoder/CMakeLists.txt new file mode 100644 index 0000000..0dc9a93 --- /dev/null +++ b/components/encoder/CMakeLists.txt @@ -0,0 +1,13 @@ +if(${IDF_TARGET} STREQUAL esp8266) + set(req esp8266 freertos log esp_timer) +elseif(${IDF_VERSION_MAJOR} STREQUAL 4 AND ${IDF_VERSION_MINOR} STREQUAL 1 AND ${IDF_VERSION_PATCH} STREQUAL 3) + set(req driver freertos log) +else() + set(req driver freertos log esp_timer) +endif() + +idf_component_register( + SRCS encoder.c + INCLUDE_DIRS . + REQUIRES ${req} +) diff --git a/components/encoder/Kconfig b/components/encoder/Kconfig new file mode 100644 index 0000000..5ef2a56 --- /dev/null +++ b/components/encoder/Kconfig @@ -0,0 +1,27 @@ +menu "Rotary encoders" + + config RE_MAX + int "Maximum number of rotary encoders" + default 1 + + config RE_INTERVAL_US + int "Polling interval, us" + default 1000 + + config RE_BTN_DEAD_TIME_US + int "Button dead time, us" + default 10000 + + choice RE_BTN_PRESSED_LEVEL + prompt "Logical level on pressed button" + config RE_BTN_PRESSED_LEVEL_0 + bool "0" + config RE_BTN_PRESSED_LEVEL_1 + bool "1" + endchoice + + config RE_BTN_LONG_PRESS_TIME_US + int "Long press timeout, us" + default 500000 + +endmenu diff --git a/components/encoder/LICENSE b/components/encoder/LICENSE new file mode 100644 index 0000000..b280b32 --- /dev/null +++ b/components/encoder/LICENSE @@ -0,0 +1,26 @@ +Copyright 2019 Ruslan V. Uss + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of itscontributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/components/encoder/component.mk b/components/encoder/component.mk new file mode 100644 index 0000000..a03513e --- /dev/null +++ b/components/encoder/component.mk @@ -0,0 +1,7 @@ +COMPONENT_ADD_INCLUDEDIRS = . + +ifdef CONFIG_IDF_TARGET_ESP8266 +COMPONENT_DEPENDS = esp8266 freertos log +else +COMPONENT_DEPENDS = driver freertos log +endif diff --git a/components/encoder/encoder.c b/components/encoder/encoder.c new file mode 100644 index 0000000..06b64e8 --- /dev/null +++ b/components/encoder/encoder.c @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2019 Ruslan V. Uss + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of itscontributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file encoder.c + * + * ESP-IDF HW timer-based driver for rotary encoders + * + * Copyright (c) 2019 Ruslan V. Uss + * + * BSD Licensed as described in the file LICENSE + */ +#include "encoder.h" +#include +#include +#include +#include + +#define MUTEX_TIMEOUT 10 + +#ifdef CONFIG_RE_BTN_PRESSED_LEVEL_0 +#define BTN_PRESSED_LEVEL 0 +#else +#define BTN_PRESSED_LEVEL 1 +#endif + +static const char *TAG = "encoder"; +static rotary_encoder_t *encs[CONFIG_RE_MAX] = { 0 }; +static const int8_t valid_states[] = { 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0 }; +static SemaphoreHandle_t mutex; +static QueueHandle_t _queue; + +#define GPIO_BIT(x) ((x) < 32 ? BIT(x) : ((uint64_t)(((uint64_t)1)<<(x)))) +#define CHECK(x) do { esp_err_t __; if ((__ = x) != ESP_OK) return __; } while (0) +#define CHECK_ARG(VAL) do { if (!(VAL)) return ESP_ERR_INVALID_ARG; } while (0) + +inline static void read_encoder(rotary_encoder_t *re) +{ + rotary_encoder_event_t ev = { + .sender = re + }; + + if (re->pin_btn < GPIO_NUM_MAX) + do + { + if (re->btn_state == RE_BTN_PRESSED && re->btn_pressed_time_us < CONFIG_RE_BTN_DEAD_TIME_US) + { + // Dead time + re->btn_pressed_time_us += CONFIG_RE_INTERVAL_US; + break; + } + + // read button state + if (gpio_get_level(re->pin_btn) == BTN_PRESSED_LEVEL) + { + if (re->btn_state == RE_BTN_RELEASED) + { + // first press + re->btn_state = RE_BTN_PRESSED; + re->btn_pressed_time_us = 0; + ev.type = RE_ET_BTN_PRESSED; + xQueueSendToBack(_queue, &ev, 0); + break; + } + + re->btn_pressed_time_us += CONFIG_RE_INTERVAL_US; + + if (re->btn_state == RE_BTN_PRESSED && re->btn_pressed_time_us >= CONFIG_RE_BTN_LONG_PRESS_TIME_US) + { + // Long press + re->btn_state = RE_BTN_LONG_PRESSED; + ev.type = RE_ET_BTN_LONG_PRESSED; + xQueueSendToBack(_queue, &ev, 0); + } + } + else if (re->btn_state != RE_BTN_RELEASED) + { + bool clicked = re->btn_state == RE_BTN_PRESSED; + // released + re->btn_state = RE_BTN_RELEASED; + ev.type = RE_ET_BTN_RELEASED; + xQueueSendToBack(_queue, &ev, 0); + if (clicked) + { + ev.type = RE_ET_BTN_CLICKED; + xQueueSendToBack(_queue, &ev, 0); + } + } + } while(0); + + re->code <<= 2; + re->code |= gpio_get_level(re->pin_a); + re->code |= gpio_get_level(re->pin_b) << 1; + re->code &= 0xf; + + if (!valid_states[re->code]) + return; + + int8_t inc = 0; + + re->store = (re->store << 4) | re->code; + + if (re->store == 0xe817) inc = 1; + if (re->store == 0xd42b) inc = -1; + + if (inc) + { + ev.type = RE_ET_CHANGED; + ev.diff = inc; + xQueueSendToBack(_queue, &ev, 0); + } +} + +static void timer_handler(void *arg) +{ + if (!xSemaphoreTake(mutex, 0)) + return; + + for (size_t i = 0; i < CONFIG_RE_MAX; i++) + if (encs[i]) + read_encoder(encs[i]); + + xSemaphoreGive(mutex); +} + +static const esp_timer_create_args_t timer_args = { + .name = "__encoder__", + .arg = NULL, + .callback = timer_handler, + .dispatch_method = ESP_TIMER_TASK +}; + +static esp_timer_handle_t timer; + +esp_err_t rotary_encoder_init(QueueHandle_t queue) +{ + CHECK_ARG(queue); + _queue = queue; + + mutex = xSemaphoreCreateMutex(); + if (!mutex) + { + ESP_LOGE(TAG, "Failed to create mutex"); + return ESP_ERR_NO_MEM; + } + + CHECK(esp_timer_create(&timer_args, &timer)); + CHECK(esp_timer_start_periodic(timer, CONFIG_RE_INTERVAL_US)); + + ESP_LOGI(TAG, "Initialization complete, timer interval: %dms", CONFIG_RE_INTERVAL_US / 1000); + return ESP_OK; +} + +esp_err_t rotary_encoder_add(rotary_encoder_t *re) +{ + CHECK_ARG(re); + if (!xSemaphoreTake(mutex, MUTEX_TIMEOUT)) + { + ESP_LOGE(TAG, "Failed to take mutex"); + return ESP_ERR_INVALID_STATE; + } + + bool ok = false; + for (size_t i = 0; i < CONFIG_RE_MAX; i++) + if (!encs[i]) + { + re->index = i; + encs[i] = re; + ok = true; + break; + } + if (!ok) + { + ESP_LOGE(TAG, "Too many encoders"); + xSemaphoreGive(mutex); + return ESP_ERR_NO_MEM; + } + + // setup GPIO + gpio_config_t io_conf; + memset(&io_conf, 0, sizeof(gpio_config_t)); + io_conf.mode = GPIO_MODE_INPUT; + if (BTN_PRESSED_LEVEL == 0) { + io_conf.pull_up_en = GPIO_PULLUP_ENABLE; + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + } else { + io_conf.pull_up_en = GPIO_PULLUP_DISABLE; + io_conf.pull_down_en = GPIO_PULLDOWN_ENABLE; + } + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.pin_bit_mask = GPIO_BIT(re->pin_a) | GPIO_BIT(re->pin_b); + if (re->pin_btn < GPIO_NUM_MAX) + io_conf.pin_bit_mask |= GPIO_BIT(re->pin_btn); + CHECK(gpio_config(&io_conf)); + + re->btn_state = RE_BTN_RELEASED; + re->btn_pressed_time_us = 0; + + xSemaphoreGive(mutex); + + ESP_LOGI(TAG, "Added rotary encoder %d, A: %d, B: %d, BTN: %d", re->index, re->pin_a, re->pin_b, re->pin_btn); + return ESP_OK; +} + +esp_err_t rotary_encoder_remove(rotary_encoder_t *re) +{ + CHECK_ARG(re); + if (!xSemaphoreTake(mutex, MUTEX_TIMEOUT)) + { + ESP_LOGE(TAG, "Failed to take mutex"); + return ESP_ERR_INVALID_STATE; + } + + for (size_t i = 0; i < CONFIG_RE_MAX; i++) + if (encs[i] == re) + { + encs[i] = NULL; + ESP_LOGI(TAG, "Removed rotary encoder %d", i); + xSemaphoreGive(mutex); + return ESP_OK; + } + + ESP_LOGE(TAG, "Unknown encoder"); + xSemaphoreGive(mutex); + return ESP_ERR_NOT_FOUND; +} diff --git a/components/encoder/encoder.h b/components/encoder/encoder.h new file mode 100644 index 0000000..fc672c8 --- /dev/null +++ b/components/encoder/encoder.h @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2019 Ruslan V. Uss + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of itscontributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file encoder.h + * @defgroup encoder encoder + * @{ + * + * ESP-IDF HW timer-based driver for rotary encoders + * + * Copyright (c) 2019 Ruslan V. Uss + * + * BSD Licensed as described in the file LICENSE + */ +#ifndef __ENCODER_H__ +#define __ENCODER_H__ + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + + +/** + * Button state + */ +typedef enum { + RE_BTN_RELEASED = 0, //!< Button currently released + RE_BTN_PRESSED = 1, //!< Button currently pressed + RE_BTN_LONG_PRESSED = 2 //!< Button currently long pressed +} rotary_encoder_btn_state_t; + +/** + * Rotary encoder descriptor + */ +typedef struct +{ + gpio_num_t pin_a, pin_b, pin_btn; //!< Encoder pins. pin_btn can be >= GPIO_NUM_MAX if no button used + uint8_t code; + uint16_t store; + size_t index; + uint64_t btn_pressed_time_us; + rotary_encoder_btn_state_t btn_state; +} rotary_encoder_t; + +/** + * Event type + */ +typedef enum { + RE_ET_CHANGED = 0, //!< Encoder turned + RE_ET_BTN_RELEASED, //!< Button released + RE_ET_BTN_PRESSED, //!< Button pressed + RE_ET_BTN_LONG_PRESSED, //!< Button long pressed (press time (us) > RE_BTN_LONG_PRESS_TIME_US) + RE_ET_BTN_CLICKED //!< Button was clicked +} rotary_encoder_event_type_t; + +/** + * Event + */ +typedef struct +{ + rotary_encoder_event_type_t type; //!< Event type + rotary_encoder_t *sender; //!< Pointer to descriptor + int32_t diff; //!< Difference between new and old positions (only if type == RE_ET_CHANGED) +} rotary_encoder_event_t; + +/** + * @brief Initialize library + * + * @param queue Event queue to send encoder events + * @return `ESP_OK` on success + */ +esp_err_t rotary_encoder_init(QueueHandle_t queue); + +/** + * @brief Add new rotary encoder + * + * @param re Encoder descriptor + * @return `ESP_OK` on success + */ +esp_err_t rotary_encoder_add(rotary_encoder_t *re); + +/** + * @brief Remove previously added rotary encoder + * + * @param re Encoder descriptor + * @return `ESP_OK` on success + */ +esp_err_t rotary_encoder_remove(rotary_encoder_t *re); + +#ifdef __cplusplus +} +#endif + +/**@}*/ + +#endif /* __ENCODER_H__ */