diff --git a/src/encoder.c b/src/encoder.c index 8e6795e..1135881 100644 --- a/src/encoder.c +++ b/src/encoder.c @@ -2,252 +2,278 @@ #include "wvr_pins.h" #include "ws_log.h" #include "encoder.h" +#include "rotary_encoder.h" -// #define ROT_ENC_A_GPIO D9 -// #define ROT_ENC_B_GPIO D10 -// #define GPIO_MASK ((1ULL<. + */ + +/** + * @file rotary_encoder.c + * @brief Driver implementation for the ESP32-compatible Incremental Rotary Encoder component. + * + * Based on https://github.com/buxtronix/arduino/tree/master/libraries/Rotary + * Original header follows: + * + * Rotary encoder handler for arduino. v1.1 + * + * Copyright 2011 Ben Buxton. Licenced under the GNU GPL Version 3. + * Contact: bb@cactii.net + * + * A typical mechanical rotary encoder emits a two bit gray code + * on 3 output pins. Every step in the output (often accompanied + * by a physical 'click') generates a specific sequence of output + * codes on the pins. + * + * There are 3 pins used for the rotary encoding - one common and + * two 'bit' pins. + * + * The following is the typical sequence of code on the output when + * moving from one step to the next: + * + * Position Bit1 Bit2 + * ---------------------- + * Step1 0 0 + * 1/4 1 0 + * 1/2 1 1 + * 3/4 0 1 + * Step2 0 0 + * + * From this table, we can see that when moving from one 'click' to + * the next, there are 4 changes in the output code. + * + * - From an initial 0 - 0, Bit1 goes high, Bit0 stays low. + * - Then both bits are high, halfway through the step. + * - Then Bit1 goes low, but Bit2 stays high. + * - Finally at the end of the step, both bits return to 0. + * + * Detecting the direction is easy - the table simply goes in the other + * direction (read up instead of down). + * + * To decode this, we use a simple state machine. Every time the output + * code changes, it follows state, until finally a full steps worth of + * code is received (in the correct order). At the final 0-0, it returns + * a value indicating a step in one direction or the other. + * + * It's also possible to use 'half-step' mode. This just emits an event + * at both the 0-0 and 1-1 positions. This might be useful for some + * encoders where you want to detect all positions. + * + * If an invalid state happens (for example we go from '0-1' straight + * to '1-0'), the state machine resets to the start until 0-0 and the + * next valid codes occur. + * + * The biggest advantage of using a state machine over other algorithms + * is that this has inherent debounce built in. Other algorithms emit spurious + * output with switch bounce, but this one will simply flip between + * sub-states until the bounce settles, then continue along the state + * machine. + * A side effect of debounce is that fast rotations can cause steps to + * be skipped. By not requiring debounce, fast rotations can be accurately + * measured. + * Another advantage is the ability to properly handle bad state, such + * as due to EMI, etc. + * It is also a lot simpler than others - a static state table and less + * than 10 lines of logic. + */ +#include "Arduino.h" +#include "rotary_encoder.h" + +#include "esp_log.h" +#include "driver/gpio.h" + +#define TAG "rotary_encoder" + +//#define ROTARY_ENCODER_DEBUG + +// Use a single-item queue so that the last value can be easily overwritten by the interrupt handler +#define EVENT_QUEUE_LENGTH 1 + +#define TABLE_ROWS 7 + +#define DIR_NONE 0x0 // No complete step yet. +#define DIR_CW 0x10 // Clockwise step. +#define DIR_CCW 0x20 // Anti-clockwise step. + +// Create the half-step state table (emits a code at 00 and 11) +#define R_START 0x0 +#define H_CCW_BEGIN 0x1 +#define H_CW_BEGIN 0x2 +#define H_START_M 0x3 +#define H_CW_BEGIN_M 0x4 +#define H_CCW_BEGIN_M 0x5 + +DRAM_ATTR static const uint8_t _ttable_half[TABLE_ROWS][TABLE_COLS] = { + // 00 01 10 11 // BA + {H_START_M, H_CW_BEGIN, H_CCW_BEGIN, R_START}, // R_START (00) + {H_START_M | DIR_CCW, R_START, H_CCW_BEGIN, R_START}, // H_CCW_BEGIN + {H_START_M | DIR_CW, H_CW_BEGIN, R_START, R_START}, // H_CW_BEGIN + {H_START_M, H_CCW_BEGIN_M, H_CW_BEGIN_M, R_START}, // H_START_M (11) + {H_START_M, H_START_M, H_CW_BEGIN_M, R_START | DIR_CW}, // H_CW_BEGIN_M + {H_START_M, H_CCW_BEGIN_M, H_START_M, R_START | DIR_CCW}, // H_CCW_BEGIN_M +}; + +// Create the full-step state table (emits a code at 00 only) +# define F_CW_FINAL 0x1 +# define F_CW_BEGIN 0x2 +# define F_CW_NEXT 0x3 +# define F_CCW_BEGIN 0x4 +# define F_CCW_FINAL 0x5 +# define F_CCW_NEXT 0x6 + +DRAM_ATTR static const uint8_t _ttable_full[TABLE_ROWS][TABLE_COLS] = { + // 00 01 10 11 // BA + {R_START, F_CW_BEGIN, F_CCW_BEGIN, R_START}, // R_START + {F_CW_NEXT, R_START, F_CW_FINAL, R_START | DIR_CW}, // F_CW_FINAL + {F_CW_NEXT, F_CW_BEGIN, R_START, R_START}, // F_CW_BEGIN + {F_CW_NEXT, F_CW_BEGIN, F_CW_FINAL, R_START}, // F_CW_NEXT + {F_CCW_NEXT, R_START, F_CCW_BEGIN, R_START}, // F_CCW_BEGIN + {F_CCW_NEXT, F_CCW_FINAL, R_START, R_START | DIR_CCW}, // F_CCW_FINAL + {F_CCW_NEXT, F_CCW_FINAL, F_CCW_BEGIN, R_START}, // F_CCW_NEXT +}; + +uint8_t _process(rotary_encoder_info_t * info) +{ + uint8_t event = 0; + if (info != NULL) + { + // Get state of input pins. + uint8_t pin_state = (gpio_get_level(info->pin_b) << 1) | gpio_get_level(info->pin_a); + + // Determine new state from the pins and state table. +#ifdef ROTARY_ENCODER_DEBUG + uint8_t old_state = info->table_state; +#endif + info->table_state = info->table[info->table_state & 0xf][pin_state]; + + // Return emit bits, i.e. the generated event. + event = info->table_state & 0x30; +#ifdef ROTARY_ENCODER_DEBUG + ESP_EARLY_LOGD(TAG, "BA %d%d, state 0x%02x, new state 0x%02x, event 0x%02x", + pin_state >> 1, pin_state & 1, old_state, info->table_state, event); +#endif + } + return event; +} + +void _isr_rotenc(void * args) +{ + rotary_encoder_info_t * info = (rotary_encoder_info_t *)args; + uint8_t event = _process(info); + bool send_event = false; + + switch (event) + { + case DIR_CW: + ++info->state.position; + info->state.direction = ROTARY_ENCODER_DIRECTION_CLOCKWISE; + send_event = true; + break; + case DIR_CCW: + --info->state.position; + info->state.direction = ROTARY_ENCODER_DIRECTION_COUNTER_CLOCKWISE; + send_event = true; + break; + default: + break; + } + + if (send_event && info->queue) + { + rotary_encoder_event_t queue_event = + { + .state = + { + .position = info->state.position, + .direction = info->state.direction, + }, + }; + BaseType_t task_woken = pdFALSE; + xQueueOverwriteFromISR(info->queue, &queue_event, NULL); + if (task_woken) + { + portYIELD_FROM_ISR(); + } + } +} + +esp_err_t rotary_encoder_init(rotary_encoder_info_t * info, gpio_num_t pin_a, gpio_num_t pin_b) +{ + esp_err_t err = ESP_OK; + if (info) + { + info->pin_a = pin_a; + info->pin_b = pin_b; + info->table = &_ttable_full[0]; //enable_half_step ? &_ttable_half[0] : &_ttable_full[0]; + info->table_state = R_START; + info->state.position = 0; + info->state.direction = ROTARY_ENCODER_DIRECTION_NOT_SET; + + // configure GPIOs + pinMode(pin_a, INPUT); + pinMode(pin_b, INPUT); + attachInterruptArg((uint8_t)pin_a, _isr_rotenc, (void*)info, CHANGE); + attachInterruptArg((uint8_t)pin_b, _isr_rotenc, (void*)info, CHANGE); + + } + else + { + ESP_LOGE(TAG, "info is NULL"); + err = ESP_ERR_INVALID_ARG; + } + return err; +} + +esp_err_t rotary_encoder_enable_half_steps(rotary_encoder_info_t * info, bool enable) +{ + esp_err_t err = ESP_OK; + if (info) + { + info->table = enable ? &_ttable_half[0] : &_ttable_full[0]; + info->table_state = R_START; + } + else + { + ESP_LOGE(TAG, "info is NULL"); + err = ESP_ERR_INVALID_ARG; + } + return err; +} + +esp_err_t rotary_encoder_flip_direction(rotary_encoder_info_t * info) +{ + esp_err_t err = ESP_OK; + if (info) + { + gpio_num_t temp = info->pin_a; + info->pin_a = info->pin_b; + info->pin_b = temp; + } + else + { + ESP_LOGE(TAG, "info is NULL"); + err = ESP_ERR_INVALID_ARG; + } + return err; +} + +esp_err_t rotary_encoder_uninit(rotary_encoder_info_t * info) +{ + esp_err_t err = ESP_OK; + if (info) + { + gpio_isr_handler_remove(info->pin_a); + gpio_isr_handler_remove(info->pin_b); + } + else + { + ESP_LOGE(TAG, "info is NULL"); + err = ESP_ERR_INVALID_ARG; + } + return err; +} + +QueueHandle_t rotary_encoder_create_queue(void) +{ + return xQueueCreate(EVENT_QUEUE_LENGTH, sizeof(rotary_encoder_event_t)); +} + +esp_err_t rotary_encoder_set_queue(rotary_encoder_info_t * info, QueueHandle_t queue) +{ + esp_err_t err = ESP_OK; + if (info) + { + info->queue = queue; + } + else + { + ESP_LOGE(TAG, "info is NULL"); + err = ESP_ERR_INVALID_ARG; + } + return err; +} + +esp_err_t rotary_encoder_get_state(const rotary_encoder_info_t * info, rotary_encoder_state_t * state) +{ + esp_err_t err = ESP_OK; + if (info && state) + { + // make a snapshot of the state + state->position = info->state.position; + state->direction = info->state.direction; + } + else + { + ESP_LOGE(TAG, "info and/or state is NULL"); + err = ESP_ERR_INVALID_ARG; + } + return err; +} + +esp_err_t rotary_encoder_reset(rotary_encoder_info_t * info) +{ + esp_err_t err = ESP_OK; + if (info) + { + info->state.position = 0; + info->state.direction = ROTARY_ENCODER_DIRECTION_NOT_SET; + } + else + { + ESP_LOGE(TAG, "info is NULL"); + err = ESP_ERR_INVALID_ARG; + } + return err; +} \ No newline at end of file diff --git a/src/rotary_encoder.h b/src/rotary_encoder.h new file mode 100644 index 0000000..6953a32 --- /dev/null +++ b/src/rotary_encoder.h @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2019 David Antliff + * Copyright 2011 Ben Buxton + * + * This file is part of the esp32-rotary-encoder component. + * + * esp32-rotary-encoder is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * esp32-rotary-encoder is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with esp32-rotary-encoder. If not, see . + */ + +/** + * @file rotary_encoder.h + * @brief Interface definitions for the ESP32-compatible Incremental Rotary Encoder component. + * + * This component provides a means to interface with a typical rotary encoder such as the EC11 or LPD3806. + * These encoders produce a quadrature signal on two outputs, which can be used to track the position and + * direction as movement occurs. + * + * This component provides functions to initialise the GPIOs and install appropriate interrupt handlers to + * track a single device's position. An event queue is used to provide a way for a user task to obtain + * position information from the component as it is generated. + * + * Note that the queue is of length 1, and old values will be overwritten. Using a longer queue is + * possible with some minor modifications however newer values are lost if the queue overruns. A circular + * buffer where old values are lost would be better (maybe StreamBuffer in FreeRTOS 10.0.0?). + */ + +#ifndef ROTARY_ENCODER_H +#define ROTARY_ENCODER_H + +#include +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/queue.h" +#include "esp_err.h" +#include "driver/gpio.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef int32_t rotary_encoder_position_t; + +/** + * @brief Enum representing the direction of rotation. + */ +typedef enum +{ + ROTARY_ENCODER_DIRECTION_NOT_SET = 0, ///< Direction not yet known (stationary since reset) + ROTARY_ENCODER_DIRECTION_CLOCKWISE, + ROTARY_ENCODER_DIRECTION_COUNTER_CLOCKWISE, +} rotary_encoder_direction_t; + +// Used internally +///@cond INTERNAL +#define TABLE_COLS 4 +typedef uint8_t table_row_t[TABLE_COLS]; +///@endcond + +/** + * @brief Struct represents the current state of the device in terms of incremental position and direction of last movement + */ +typedef struct +{ + rotary_encoder_position_t position; ///< Numerical position since reset. This value increments on clockwise rotation, and decrements on counter-clockewise rotation. Counts full or half steps depending on mode. Set to zero on reset. + rotary_encoder_direction_t direction; ///< Direction of last movement. Set to NOT_SET on reset. +} rotary_encoder_state_t; + +/** + * @brief Struct carries all the information needed by this driver to manage the rotary encoder device. + * The fields of this structure should not be accessed directly. + */ +typedef struct +{ + gpio_num_t pin_a; ///< GPIO for Signal A from the rotary encoder device + gpio_num_t pin_b; ///< GPIO for Signal B from the rotary encoder device + QueueHandle_t queue; ///< Handle for event queue, created by ::rotary_encoder_create_queue + const table_row_t * table; ///< Pointer to active state transition table + uint8_t table_state; ///< Internal state + volatile rotary_encoder_state_t state; ///< Device state +} rotary_encoder_info_t; + +/** + * @brief Struct represents a queued event, used to communicate current position to a waiting task + */ +typedef struct +{ + rotary_encoder_state_t state; ///< The device state corresponding to this event +} rotary_encoder_event_t; + +/** + * @brief Initialise the rotary encoder device with the specified GPIO pins and full step increments. + * This function will set up the GPIOs as needed, + * Note: this function assumes that gpio_install_isr_service(0) has already been called. + * @param[in, out] info Pointer to allocated rotary encoder info structure. + * @param[in] pin_a GPIO number for rotary encoder output A. + * @param[in] pin_b GPIO number for rotary encoder output B. + * @return ESP_OK if successful, ESP_FAIL or ESP_ERR_* if an error occurred. + */ +esp_err_t rotary_encoder_init(rotary_encoder_info_t * info, gpio_num_t pin_a, gpio_num_t pin_b); + +/** + * @brief Enable half-stepping mode. This generates twice as many counted steps per rotation. + * @param[in] info Pointer to initialised rotary encoder info structure. + * @param[in] enable If true, count half steps. If false, only count full steps. + * @return ESP_OK if successful, ESP_FAIL or ESP_ERR_* if an error occurred. + */ +esp_err_t rotary_encoder_enable_half_steps(rotary_encoder_info_t * info, bool enable); + +/** + * @brief Reverse (flip) the sense of the direction. + * Use this if clockwise/counterclockwise are not what you expect. + * @param[in] info Pointer to initialised rotary encoder info structure. + * @return ESP_OK if successful, ESP_FAIL or ESP_ERR_* if an error occurred. + */ +esp_err_t rotary_encoder_flip_direction(rotary_encoder_info_t * info); + +/** + * @brief Remove the interrupt handlers installed by ::rotary_encoder_init. + * Note: GPIOs will be left in the state they were configured by ::rotary_encoder_init. + * @param[in] info Pointer to initialised rotary encoder info structure. + * @return ESP_OK if successful, ESP_FAIL or ESP_ERR_* if an error occurred. + */ +esp_err_t rotary_encoder_uninit(rotary_encoder_info_t * info); + +/** + * @brief Create a queue handle suitable for use as an event queue. + * @return A handle to a new queue suitable for use as an event queue. + */ +QueueHandle_t rotary_encoder_create_queue(void); + +/** + * @brief Set the driver to use the specified queue as an event queue. + * It is recommended that a queue constructed by ::rotary_encoder_create_queue is used. + * @param[in] info Pointer to initialised rotary encoder info structure. + * @param[in] queue Handle to queue suitable for use as an event queue. See ::rotary_encoder_create_queue. + * @return ESP_OK if successful, ESP_FAIL or ESP_ERR_* if an error occurred. + */ +esp_err_t rotary_encoder_set_queue(rotary_encoder_info_t * info, QueueHandle_t queue); + +/** + * @brief Get the current position of the rotary encoder. + * @param[in] info Pointer to initialised rotary encoder info structure. + * @param[in, out] state Pointer to an allocated rotary_encoder_state_t struct that will + * @return ESP_OK if successful, ESP_FAIL or ESP_ERR_* if an error occurred. + */ +esp_err_t rotary_encoder_get_state(const rotary_encoder_info_t * info, rotary_encoder_state_t * state); + +/** + * @brief Reset the current position of the rotary encoder to zero. + * @param[in] info Pointer to initialised rotary encoder info structure. + * @return ESP_OK if successful, ESP_FAIL or ESP_ERR_* if an error occurred. + */ +esp_err_t rotary_encoder_reset(rotary_encoder_info_t * info); + + +#ifdef __cplusplus +} +#endif + +#endif // ROTARY_ENCODER_H \ No newline at end of file diff --git a/src/wvr_0.3.h b/src/wvr_0.3.h index 400f59e..d92b0f9 100644 --- a/src/wvr_0.3.h +++ b/src/wvr_0.3.h @@ -1,7 +1,7 @@ #ifndef WVR_0_3_H #define WVR_0_3_H -#define VERSION_CODE "3.7.2" +#define VERSION_CODE "3.8.0" void wvr_init(bool useFTDI, bool useUsbMidi, bool checkRecoveryModePin);