components/esp32-rotary-encoder/rotary_encoder.c

Tue, 08 Oct 2019 12:00:31 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Tue, 08 Oct 2019 12:00:31 +0200
changeset 0
88d965579617
child 73
c18d2951e8d7
permissions
-rw-r--r--

Initial import of the CO2 meter application.

/*
 * 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 <https://www.gnu.org/licenses/>.
 */

/**
 * @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 "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

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

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
};

static 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;
}

static 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, &task_woken);
        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
        gpio_pad_select_gpio(info->pin_a);
        gpio_set_pull_mode(info->pin_a, GPIO_PULLUP_ONLY);
        gpio_set_direction(info->pin_a, GPIO_MODE_INPUT);
        gpio_set_intr_type(info->pin_a, GPIO_INTR_ANYEDGE);

        gpio_pad_select_gpio(info->pin_b);
        gpio_set_pull_mode(info->pin_b, GPIO_PULLUP_ONLY);
        gpio_set_direction(info->pin_b, GPIO_MODE_INPUT);
        gpio_set_intr_type(info->pin_b, GPIO_INTR_ANYEDGE);

        // install interrupt handlers
        gpio_isr_handler_add(info->pin_a, _isr_rotenc, info);
        gpio_isr_handler_add(info->pin_b, _isr_rotenc, info);
    }
    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;
}

mercurial