View Categories

Инкрементальный энкодер

Энкодер – общее название устройств, преобразующих одну величину в другую. В данном случае энкодер – это устройство, преобразующее вращательное механическое движение в цифровой сигнал, а сам энкодер в этом случае называется "поворотным" (вращательным, круговым).

Рассмотренный ниже инкрементальный поворотный энкодер с кнопкой служит очень удобным органом управления для электронного устройства и заменяет сразу несколько кнопок или джойстик, обеспечивая быструю навигацию по пунктам меню и изменение настроек: например в некоторых 3D принтерах с огромным сложным меню с кучей настроек навигация производится при помощи одного только энкодера! По сути похож на потенциометр, но не имеет ограничения по углу поворота, а само вращение рукоятки - "ступенчатое", что позволяет тактильно ощущать каждый шаг изменения значения.

В наборе GyverKIT START IOT EXTRA
Энкодер

При вращении он генерирует квадратный сигнал со смещением на половину фазы - квадратурный сигнал:

Точно так же работают энкодеры, которые стоят на некоторых моторчиках, например популярных JGA25 и JGB37, которые часто используются для самодельных роботов:

Рассмотренные ниже алгоритмы можно использовать в том числе для таких моторов.

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

Модуль #

Модуль подключается на питание, логические выводы – на любые цифровые пины. В случае с Wemos – на все кроме D8, так как подтяжка к VCC помешает МК запуститься. Подключу выводы энкодера на D2 и D3, а кнопку – на D4:

Особенности данного модуля:

  • Качественный инкрементальный энкодер с кнопкой
  • 20 "щелчков" на один оборот
  • Выведено питание (5V, GND), два пина энкодера (S1, S2) и пин кнопки (KEY)
  • Работает также от 3.3V (для Wemos)
  • Все логические пины подтянуты к VCC резисторами на плате
  • RC цепи гашения дребезга на выводах энкодера

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

Голый #

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

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

Код Грея #

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

#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);   // инверт p0
}
void isrB() {
    pollEnc(p0, !p1);   // инверт 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);  // имитация "загруженной" программы
}

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

Данный алгоритм:

  • Считает каждое изменение с обоих выводов энкодера, т.е. обрабатывает поворот с максимальным "разрешением"
  • Автоматически игнорирует шумы и некорректные сигналы, т.к. код Грея подразумевает только один способ перейти из текущего состояния в следующее
  • Для работы на прерываниях требует подключить оба пина на прерывания по CHANGE

Дата-клок #

Можно обрабатывать поворот более просто - один пин считать "клоком", а второй - направлением. Алгоритм после упрощения до одной строчки может выглядеть так:

#define ENC_A 2
#define ENC_B 3

bool prev;

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

void setup() {
    Serial.begin(115200);
    prev = digitalRead(ENC_A);
}

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

Для работы на прерываниях понадобится подключить один пин на CHANGE. По аналогии с предыдущим примером - флаг и счётчик:

#define ENC_A 2
#define ENC_B 3

volatile bool p0;
volatile bool flag;
volatile int counter;

void isrA() {
    p0 ^= 1;
    counter += (digitalRead(ENC_B) ^ p0) ? 1 : -1;
    flag = true;
}

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

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

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

Данный алгоритм, в отличие от предыдущего:

  • Считает изменение только одного пина энкодера, то есть имеет "разрешение" поворота в 2 раза меньше
  • Боится шумов и некачественных дребезжащих энкодеров - значение счётчика может "уплыть"
  • Требует опроса состояния пина в прерывании, что может быть долго в Ардуино-реализации
  • Для работы на прерываниях требует подключить только один пин, в этом его единственное преимущество

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

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

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

Библиотеки #

В рамках примеров и проектов будем использовать библиотеку EncButton, она позволяет работать как отдельно с энкодером, так и с энкодером+кнопкой для сложных сценариев управления и выбора. Её можно установить/обновить из встроенного менеджера библиотек Arduino по названию EncButton. Краткая документация находится по ссылке выше, базовые примеры есть в самой библиотеке.

Для работы библиотеки нужно вызывать метод tick() в loop() и опрашивать нужные события. Их там много - смотрите документацию.

Меняем значение переменной
/*
  Меняем значение переменной при помощи энкодера
  Обычный поворот +-1
  "Нажатый" поворот +-5
*/

#include <EncButton.h>
EncButton enc(2, 3, 4);  // выводы энкодера на 2 и 3, кнопка на 4

void setup() {
  Serial.begin(115200);
}

int val = 0;  // будем управлять этой переменной

void loop() {
  // опрос энкодера происходит тут
  enc.tick();

  // вправо
  if (enc.right()) {
    val += 1;
    Serial.println(val);
  }

  // влево
  if (enc.left()) {
    val -= 1;
    Serial.println(val);
  }

  // вправо нажатый
  if (enc.rightH()) {
    val += 5;
    Serial.println(val);
  }

  // влево нажатый
  if (enc.leftH()) {
    val -= 5;
    Serial.println(val);
  }
}
Меняем яркость и состояние светодиода
/*
  Меняем яркость светодиода на пине 13 (программный ШИМ)
  Клик - переключить состояние (вкл выкл)
*/

#include <EncButton.h>
EncButton enc(2, 3, 4);  // выводы энкодера на 2 и 3, кнопка на 4

void setup() {
  // пин 13 как выход (для мигания светодиодом)
  pinMode(13, OUTPUT);
}

int bright = 128; // храним яркость
bool state = 1;   // состояние светодиода

void loop() {
  // опрос энкодера происходит тут
  enc.tick();

  // передаём яркость, умноженную на state (0 или 1)
  // то есть получится bright или 0 в зависимости от state
  softPWM(13, bright * state);

  // вправо - увеличиваем на 10
  if (enc.right()) bright = constrain(bright + 10, 0, 255);

  // влево - уменьшаем на 10
  if (enc.left()) bright = constrain(bright - 10, 0, 255);

  // клик - переключаем состояние по клику
  if (enc.click()) state = !state;
}

// софт шим
void softPWM(byte pin, byte val) {
  static byte count;
  count++;
  if (count == 0 && val != 0) digitalWrite(pin, 1);
  if (count == val) digitalWrite(pin, 0);
}

Дополнительный контент доступен владельцам набора GyverKIT и по подписке, подробнее читай здесь. Блок содержит:

  • Дополнительно, Пример со светодиодом
  • 1 изображений
  • 1 блоков кода

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

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

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