View Categories

Опрос энкодера

Для урока понадобится
В наборе GyverKIT START IOT EXTRA
Arduino NANO
Макетная плата
Энкодер
Светодиод 5мм
Резистор 220 Ом

Энкодер #

Инкрементальный энкодер по сути заменяет собой две кнопки - при вращении в одну сторону нажимается одна, в другую - вторая. При вращении он генерирует квадратный сигнал со смещением на половину фазы, поэтому для обработки нужен специальный алгоритм.

Подключение #

Голый энкодер просто замыкает контакты, поэтому пинам нужна подтяжка: внешняя либо внутренняя. Также для избавления от дребезга контактов при вращении можно поставить на пины RC цепи:

Примерно по этой схеме сделан вот такой популярный магазинный модуль, он не самый дешёвый - но своих денег однозначно стоит:

Модуль подключается к питанию и любым цифровым пинам, пин KEY - вывод кнопки энкодера, его можно не подключать если не используется:

В примерах ниже я использую такой модуль - пины подтянуты аппаратно, поэтому режим пинов оставляю INPUT.

Программирование #

Рассмотрим самый эффективный алгоритм на основе кода Грея. Простейший случай - выводим в порт "направление" текущего поворота. Для работы алгоритма нужно хранить предыдущие состояния пинов:

#define ENC_A 2
#define ENC_B 3

bool p0, p1;

void pollEnc(bool e0, bool e1) {
    if (p0 ^ p1 ^ e0 ^ e1) {
        Serial.println(p1 ^ e0);    // направление
        p0 = e0;
        p1 = e1;
    }
}

void setup() {
    Serial.begin(115200);
    p0 = digitalRead(ENC_A);    // стартовые значения
    p1 = digitalRead(ENC_B);
}

void loop() {
    pollEnc(digitalRead(ENC_A), digitalRead(ENC_B));    // постоянный опрос в loop
}

Можно опрашивать энкодер в прерывании - для этого нужно подключить оба пина на прерывание по CHANGE. Чтобы не опрашивать каждый раз актуальные состояния пинов, можно просто считать, что они инвертируются:

#define ENC_A 2
#define ENC_B 3

volatile bool p0, p1;

void pollEnc(bool e0, bool e1) {
    if (p0 ^ p1 ^ e0 ^ e1) {
        Serial.println(p1 ^ e0);
        p0 = e0;
        p1 = e1;
    }
}

void isrA() {
    pollEnc(!p0, p1);
}
void isrB() {
    pollEnc(p0, !p1);
}

void setup() {
    Serial.begin(115200);
    attachInterrupt(0, isrA, CHANGE);
    attachInterrupt(1, isrB, CHANGE);

    p0 = digitalRead(ENC_A);
    p1 = digitalRead(ENC_B);
}

void loop() {
    // тут ничего нет, опрос в прерывании
}

Вывод в Serial в прерывании может работать не очень корректно, но тут суть в самом алгоритме. В реальном применении можно завести например счётчик и флаг срабатывания:

#define ENC_A 2
#define ENC_B 3

volatile bool p0, p1;
volatile bool flag;
volatile int counter;

void pollEnc(bool e0, bool e1) {
    if (p0 ^ p1 ^ e0 ^ e1) {
        (p1 ^ e0) ? ++counter : --counter;
        flag = 1;
        p0 = e0;
        p1 = e1;
    }
}

void isrA() {
    pollEnc(!p0, p1);
}
void isrB() {
    pollEnc(p0, !p1);
}

void setup() {
    Serial.begin(115200);
    attachInterrupt(0, isrA, CHANGE);
    attachInterrupt(1, isrB, CHANGE);

    p0 = digitalRead(ENC_A);
    p1 = digitalRead(ENC_B);
}

void loop() {
    if (flag) {
        flag = false;
        Serial.println(counter);
    }

    delay(200);  // имитация "загруженной" программы
}

Изменение счётчика будет производиться даже в условиях "задержек".

Тип энкодера #

Аппаратно энкодеры могут быть устроены по разному, а именно - точка фиксации рукоятки может находиться в разных местах сигнала. В своей библиотеке EncButton я классифицирую их следующим образом, пунктиром отмечена точка фиксации рукоятки:

Рассмотренный выше алгоритм обрабатывает каждое изменение сигнала, то есть энкодер с типом EB_STEP4_LOW будет "срабатывать" 4 раза за один щелчок - именно таким является энкодер на круглой плате. Вы скорее всего заметили это, если загружали пример. Для удобства работы с энкодером применительно к навигации по меню электронного устройства нужно пропустить лишние срабатывания. Реализацию можно посмотреть например здесь.

Библиотеки #

Для более удобной работы с энкодером любого типа без изобретения алгоритмов можно использовать библиотеку EncButton - она имеет огромное количество возможностей и сценариев использования для энкодера с кнопкой, см. полную документацию по ссылке. Самый простой пример:

#include <EncButton.h>
EncButton eb(2, 3, 4);

void setup() {
    Serial.begin(115200);
}
void loop() {
    eb.tick();  // опрос тут

    // обработка поворота раздельная
    // if (eb.left()) Serial.println("Лево");
    // if (eb.right()) Serial.println("Право");

    // if (eb.leftH()) Serial.println("Лево нажатый");
    // if (eb.rightH()) Serial.println("Право нажатый");

    // обработка поворота общая
    if (eb.turn()) {
        Serial.print("направление ");
        Serial.print(eb.dir());
        Serial.print(", быстрый ");
        Serial.print(eb.fast());
        Serial.print(", кнопка ");
        Serial.print(eb.pressing());
        Serial.print(", счётчик ");
        Serial.println(eb.counter);
    }
}

Управляем светодиодом #

Напишем пример, в котором яркость светодиода на D5 управляется энкодером с кнопкой:

  • Поворот - изменение яркости с шагом 5
  • Поворот с удержанием кнопки - изменение яркости с шагом 20
  • Клик - включить/выключить

#include <EncButton.h>
EncButton eb(2, 3, 4);

#define LED_PIN 5

// по умолч. включен с яркостью 128
bool state = 1;
int bright = 128;

void updateLED() {
    analogWrite(LED_PIN, state ? bright : 0);
}

void setup() {
    updateLED();
}

void loop() {
    eb.tick();

    if (eb.click()) {
        state = !state;
        updateLED();
    }

    if (eb.turn()) {
        bright += (eb.pressing() ? 20 : 5) * eb.dir();
        bright = constrain(bright, 0, 255);
        updateLED();
    }
}

Полезные страницы #

0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх