вторник, 10 октября 2017 г.

Работа с энкодером вращения с использованием прерываний

Энкодер вращения KY-040

Теперь, когда мы научились избавляться от дребезга контактов (смотрите предыдущую статью), можно попробовать другой способ работы с энкодером: с использованием прерываний. Я буду экспериментировать все с тем же модулем KY-040, Arduino Uno и инвертирующим триггером Шмитта (SN74HC14N).

Для начала пара слов о прерываниях в Ардуино. Данная тема подробно изложена здесь и здесь. Сейчас же я ограничусь определением и небольшим описанием внешних прерываний Ардуино.

Прерывание - это сигнал от аппаратного обеспечения, сообщающий о наступлении некоторого приоритетного события, требующего немедленного внимания. В ответ на запрос прерывания микроконтроллер приостанавливает выполнение текущего кода и вызывает специальную функцию - обработчик прерывания (Interrupt Service Routine, ISR). Данная функция реагирует на событие, выполняя определенные действия, после чего возвращает управление в прерванный код.

Для работы с энкодером мы будем использовать внешние прерывания Ардуино. Внешними они называются потому, что их инициирует внешнее по отношению к микроконтроллеру устройство. Плата Arduino Uno (а также большинство других плат из семейства Ардуино), к которой я собираюсь подключать энкодер, может обрабатывать до двух внешних прерываний на цифровых выводах 2 и 3. Для этого необходимо задать функции обработки прерываний при помощи функции attachinterrupt(interrupt, function, mode). Параметры данной функции:
  • interrupt - номер прерывания. Может быть указан явно (0 для цифрового вывода 2 и 1 для вывода 3 в случае использования Arduno Uno), но лучше воспользоваться функцией digitalPinToInterrupt(pin), которая по номеру вывода вернет номер привязанного к нему внешнего прерывания;
  • function -  функция обработки прерывания - функция, которая будет вызываться каждый раз при возникновении прерывания. Должна быть без параметров и не возвращать никаких значений;
  • mode - задает режим обработки прерывания. Может принимать одно из следующих значений:
    • LOW - вызывает прерывание, когда на входе сигнал низкого уровня;
    • CHANGE - вызывает прерывание при изменении уровня сигнала на входе;
    • RISING - прерывание вызывается при изменении сигнала от низкого уровня к высокому;
    • FALLING - прерывание вызывается при изменении сигнала от высокого уровня к низкому.

Для работы с прерываниями подключим модуль энкодера KY-040 к Ардуино по нижеприведенной схеме.
Подключение энкодера вращения к Ардуино через триггер Шмитта

Все 3 вывода энкодера заводим на микросхему SN74HC14N для подавления дребезга контактов. Необходимо также учесть, что в модуле нет подтягивающего резистора для сигнала SW, поэтому он добавлен в схему.

Если раньше для того чтобы поймать момент изменения сигнала энкодера приходилось постоянно опрашивать цифровой вход и сравнивать его значение с предыдущим, то сейчас достаточно назначить обработчик прерывания функцией attachinterrupt. Так как в нашем распоряжении только 2 внешних прерывания, то одно из них будем использовать для отслеживания изменения сигнала CLK, второе для кнопки. Загрузите нижеприведенный код в Ардуино и проверьте его работу в мониторе порта: при вращении энкодера значение счетчика будет изменяться и выводиться в Serial, а при нажатии на кнопку счетчик сбрасывается.

#define pin_CLK 2 // Энкодер пин A
#define pin_DT  4 // Энкодер пин B
#define pin_Btn 3 // Кнопка

volatile long Position = 0;
long oldPosition = 0;

void EncoderRotate() {
  if (digitalRead(pin_CLK) == digitalRead(pin_DT)) {
    Position++;
  } else {
    Position--;
  }
}

void EncoderClick() {
  Position = 0;
}

void setup() {
  pinMode(pin_CLK, INPUT);
  pinMode(pin_DT, INPUT);
  pinMode(pin_Btn, INPUT);
  attachInterrupt(digitalPinToInterrupt(pin_CLK), EncoderRotate, RISING);
  attachInterrupt(digitalPinToInterrupt(pin_Btn), EncoderClick, RISING);
  Serial.begin (9600);
}

void loop() {
  if (oldPosition != Position)
  {
    Serial.println(Position);
    oldPosition = Position;
  }
}

При изменении сигнала CLK от низкого уровня к высокому вызывается функция EncoderRotate, которая проверяет сигнал DT и изменяет значение счетчика. Если в функции attachInterrupt установить режим обработки прерывания CHANGE, то функция EncoderRotate будет вызываться как по заднему фронту CLK, так и по переднему. Таким образом можно увеличить разрешение энкодера в 2 раза. А если мы задействуем внешнее прерывание и для сигнала DT (вместо кнопки) и напишем для него аналогичный обработчик, то 4 фронта дадут нам 4х кратное увеличение разрешения энкодера. Пример такой программы, а также другую полезную информацию по энкодерам вращения можно найти по ссылке https://playground.arduino.cc/Main/RotaryEncoders. Там же представлены несколько библиотек, одну из которых я предлагаю рассмотреть - это библиотека Encoder.

Библиотека для работы с энкодерами вращения Encoder (домашняя страница проекта: https://www.pjrc.com/teensy/td_libs_Encoder.html) использует очень эффективные обработчики прерываний, написанные на ассемблере и предусматривает три варианта подключения энкодера к Ардуино, определяющих эффективность работы:

  • Наилучшая эффективность, достигается при подключении обоих сигналов энкодера к выводам, поддерживающим прерывания;
  • Средняя эффективность, при подключении одного сигнала энкодера к поддерживающему внешнее прерывание выводу. Второй сигнал подключается к любому цифровому входу;
  • Низкая эффективность будет получена при подключении энкодера к выводам Ардуино, не поддерживающим прерывания. При этом для отслеживания изменения сигналов используется метод, который мы рассматривали ранее при опросе энкодера в цикле программы.

Давайте установим данную библиотеку и, не изменяя схему подключения энкодера, загрузим в Ардуино следующий код.

#include "Encoder.h"

#define pin_CLK 2 // Энкодер пин A
#define pin_DT  4 // Энкодер пин B
#define pin_Btn 3 // Кнопка

long oldPosition = 0;
Encoder myEnc(pin_CLK, pin_DT);

void EncoderClick() {
  myEnc.write(0); // При нажатии кнопки сбрасываем позицию
}

void setup() {
  pinMode(pin_Btn, INPUT);
  // Назначаем обработчик прерывания для кнопки
  attachInterrupt(digitalPinToInterrupt(pin_Btn), EncoderClick, RISING);
  Serial.begin (9600);
}

void loop() {
  long newPosition = myEnc.read() >> 2;
  if (newPosition != oldPosition) {
    oldPosition = newPosition;
    Serial.println(newPosition);
  }
}

Проверьте результат в мониторе порта, он должен быть такой же, как и для предыдущего примера. Выше я описывал способ увеличения разрешения энкодера, он как раз используется в данной библиотеке. Поэтому считанное значение я делю в программе на 4 (сдвигаю вправо на 2 разряда). В остальном, полагаю, приведенный пример не должен вызывать вопросов.

Вот так просто можно задействовать прерывания для работы с энкодером и избавиться от необходимости опрашивать его в основном цикле. Чуть позже я поделюсь опытом создания пользовательского меню на Ардуино с использованием энкодера вращения.

26 комментариев:

  1. Взял я обычный энкодер с Али, не на платке, а просто без ничего.
    Подтянул все нужные контакты внутренними резисторами Ардуино Про мини к плюсу и на всякий случай всё же дополнил поверх резисторами на 11 кОм.
    Эти же выводы конденсаторами (правда мелкими, по 10нФ) законнектил с землёй для какого-никакого сглаживания.
    И провернул со средней скоростью в одну сторону. Вот, что увидел:

    1
    0
    1
    2
    2
    3
    2
    3
    4
    3
    4
    5
    5
    3
    4
    5
    5
    6
    5
    6
    5
    4
    5
    4
    5
    4
    5
    4

    Это нормлальный код?)) Может быть для оптического или какого-то иного в вакууме..

    Все эти увеличения разрешения бессмысленны, и даже ухудшают ситуацию по сравнению со стандартным методом, если не применять метода борьбы с дрожением или с кривостью энкодеров..

    ОтветитьУдалить
  2. А вот так выходит на той же физической схеме, но уже с кодом получше:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29

    На самом деле там шаг как бы 0.5, и подсчёт исходя из "CHANGE" и выведено округлённое до целых значение. Иногда конечно потери бывают, но, как видите, результат несколько получше...

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Вы невнимательно читали публикацию. Из первого же абзаца понятно, что речь идет о работе с энкодером, сигналы которого уже очищены от дребезга.
      Вы серьезно думаете, что с такими номиналами сможете избавиться от дребезга? Здесь https://tsibrov.blogspot.com/2017/09/1.html я показывал, как выглядит неочищенный сигнал с энкодера. Чтобы его размазать потребовалась емкость 0,2мкФ.
      Попробуйте лучше скетч с программным подавлением дребезга, это будет разумнее. Нет смысла заводить искаженный сигнал на входы прерываний Ардуино и искать код, который переварит этот шум.

      Удалить
    2. Этот комментарий был удален автором.

      Удалить
    3. "Попробуйте лучше скетч с программным подавлением дребезга, это будет разумнее."

      "Нет смысла заводить искаженный сигнал на входы прерываний Ардуино и искать код, который переварит этот шум."

      Так какой из этих фраз по вашему стоит прислушаться? Они же противоречат друг другу. Первая про программную очистку, вторая про физическую.

      0,2 мкФ при быстром вращении делает сигнал просто пилой, причём даже пики пилы не особо высокие. Если двигать еле-еле, то всё ОК конечно же. Мой код, результат которого я привёл как раз программно и борется. Выжидает 2-3 мс, а потом сравнивает что надо.
      Я изучал сигнал на осциллографе. Думаю, там основная проблема даже не столько в дрожании, сколько в корявости самих энкодеров. Скважность не стабильна, люфты наверное от рождения.

      Я на самом деле прочитал ваш пост нормально. Но сейчас полез, перепроверил.
      То, что вы сказали, что "когда мы научились избавляться от дребезга контактов, можно попробовать другой способ работы с энкодером: с использованием прерываний."
      - "другой способ", вы противопоставляете его изученному способу с фильтрацией, вы тут не отвертитесь, это ваш косяк, и никак не упоминаете о необходимости предварительной фильтрации при использовании этого способа.
      Напротив, урок этот рассказывает о работе прерываний с нуля, как сам в себе законченный материал, да ещё с супер-пупер библиотеками.
      "Если раньше для того чтобы...", "Библиотека ... ...использует очень эффективные обработчики прерываний"
      - Читаешь прям и воодушевляешься, ну, сейчас возьму этот код и всё заработает.. Ведь ни слова о том, что надо что-то делать дополнительно, ведь описывается, цитирую "другой способ работы с энкодером".

      Может быть для вас что-то там и очевидно, но об этом нет ни слова. Читайте глазами того, кто ищет такие статьи.

      Удалить
    4. Вы понимаете что такое прерывания? мне кажется нет, иначе бы подобные вопросы не возникали.
      Есть два варианта работы с кнопками, энкодерами и другими подобными устройствами:
      1. Опрос устройства, когда микроконтроллер постоянно отслеживает его состояние.
      2. Вариант с использованием прерываний, когда устройство само сообщает о наступлении события, которое мы ожидаем.

      Как думаете, что будет если в качестве источника внешнего прерывания использовать неочищенный сигнал энкодера? - Ничего хорошего. Вместо одного события будут зафиксированы десятки или сотни ложных событий. Хотите использовать прерывания - очищайте сигнал = используйте аппаратное подавление дребезга, нет смысла искать код, который переварит ваш шум. Это касательно моей второй фразы.

      Касательно первой фразы, про использование скетча с программным подавлением дребезга, не имеющего ничего общего с прерываниями. Я уже приводил пример такого кода, когда состояние энкодера опрашивается в цикле. В чем именно здесь противоречие, если речь идет о другом способе работы с энкодером?

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

      -Нет, вы все равно невнимательно прочитали статью и пытаетесь вырванными фразами обвинить меня в каком-то косяке. Еще раз: "другой способ работы с энкодером: с использованием прерываний" - это тот самый способ, который подразумевает работу с прерываниями, о котором я могу рассказать после публикации об аппаратном подавлении дребезга, потому что он требует очищенный сигнал; это способ, который я противопоставляю программному опросу.

      "и никак не упоминаете о необходимости предварительной фильтрации при использовании этого способа." - конечно, нет. Я лишь привел схему с триггером Шмитта и подписал, что "Все 3 вывода энкодера заводим на микросхему SN74HC14N для подавления дребезга контактов."

      Я понимаю, хочется найти что-то готовое, скопировать-вставить и чтобы всё работало. Так потрудитесь прочитать и вникнуть в статью, для каких случаев приведен данный код. Тогда всё будет работать и не придется тыкать: "у вас код плохой, а с другого сайта я взял - там хороший"

      Удалить
    5. Здравствуйте! А Вы могли бы глянуть этот скетч и сказать, можно ли в нем программно убрать дребезг энкодера?

      #include
      #include

      #include
      Encoder myEnc(2,3);
      const int pinLed = LED_BUILTIN;
      const int pinButton = 4;
      int bulbPin = 9;
      int oldPosition = -999;
      boolean muted = 0;
      int safePosition = 0;
      int volume = 0;
      int oldVolume = 0;
      int actualVolume = 0;
      void setup() {
      pinMode(7, INPUT);
      pinMode(8, INPUT);
      pinMode(pinLed, OUTPUT);
      pinMode(pinButton, INPUT_PULLUP);
      Serial.begin(9600);
      Consumer.begin();
      delay(1000);
      }

      void loop() {
      int newPosition = myEnc.read();
      if (newPosition != oldPosition) {
      safePosition = newPosition;
      if(newPosition < 0){
      safePosition = 0;
      myEnc.write(safePosition);
      }
      if(newPosition > 400){
      safePosition = 400;
      myEnc.write(safePosition);
      }
      oldPosition = safePosition;
      volume = safePosition / 4;
      volume = volume / 2;
      noInterrupts();
      Serial.print("calc volume: ");
      Serial.print(volume);
      Serial.print(" act volume: ");
      Serial.print(actualVolume);
      Serial.println("");
      changeVolume();
      interrupts();

      }

      if (!digitalRead(pinButton)) {

      Consumer.write(MEDIA_VOLUME_MUTE);
      muted = !muted;
      delay(300);
      }
      if(muted){
      analogWrite(bulbPin, 0);
      }
      else{
      analogWrite(bulbPin, map(volume, 0, 100, 0, 255));
      }




      }

      void changeVolume(){
      if (volume != oldVolume) {
      if(volume > oldVolume){
      //delay(100);
      Consumer.write(MEDIA_VOLUME_UP);
      actualVolume = actualVolume + 5;
      oldVolume = volume;
      }
      else{
      //delay(100);
      Consumer.write(MEDIA_VOLUME_DOWN);
      actualVolume = actualVolume - 5;
      oldVolume = volume;
      }
      }
      }

      Удалить
    6. Добрый день!
      Я так понимаю, используется библиотека Encoder. Она рассчитана на чистый сигнал, поэтому требуется аппаратное подавление дребезга. Бороться программно с дребезгом можно (https://tsibrov.blogspot.com/2017/09/blog-post.html - здесь я приводил пример), но это будет уже самостоятельная процедура опроса энкодера, без использования библиотеки.

      Удалить
    7. Благодарю! Я, конечно, больше техник, чем программист, поэтому буду пробовать аппаратные решения.

      Удалить
    8. Советую MC14490, она мне очень понравилась

      Удалить
  3. MC14490 не нашел в продаже, только на алиэеспрессе, собрал на SN74HC14N работает отлично!

    ОтветитьУдалить
    Ответы
    1. Поздравляю! Ну теперь нужно применить схему в каком-нибудь интересном проекте!

      Удалить
  4. Спасибо за статью. на ее основе забацал такую платку https://easyeda.com/Igoryan/sh74debound работает все отлично.

    ОтветитьУдалить
    Ответы
    1. Спасибо! Всегда приятно слышать, что мои публикации помогают.
      Платка получилась здоровская. Готовый модуль энкодера с подавлением дребезга.

      Удалить
  5. Добрый день!
    Возможно ли добавить возможность работы в заданном диапазоне (min-max) и входа установки в 0 и другое какое либо число?

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Вы про библиотеку Encoder.h? В ней для установки значения есть функция write().
      min и max в библиотеке, конечно, нет. Она просто считает импульсы, сколько насчитала, столько и возвращает. Поэтому при чтении положения добавьте собственную проверку на min/max

      Удалить
  6. Добрый день.
    Важный момент, о котором не пишут нигде (по крайней мере я не нашёл): существует два типа энкодеров! Визуально они почти одинаковые (отличается размер, смотрите картинку сверху), но при повороте на один “тик” первый тип даёт один импульс, а второй тип даёт два импульса, из-за чего возникают проблемы с библиотеками и примерами из интернета. Если энкодер отрабатывается не по своему типу, то вас ждёт или два срабатывания вместо одного (как будто повернули два раза), или 0.5 срабатывания (нужно повернуть на два тика, чтобы отработался один). В моей библиотеке это всё настраивается. Также заметил, что энкодеры второго типа чаще “барахлят”. Скачать библиотеку GyverEncoder можно по кнопке ниже, она входит в пак библиотек GyverLibs.
    Эта цитата взята с сайта https://alexgyver.ru/encoder/
    У меня как раз проблема, потому что энкодер срабатывает 0,5. Можно что-то изменить в библиотеке, чтобы выбирать тип энкодера.

    ОтветитьУдалить
  7. Добрый день!
    "Можно что-то изменить в библиотеке, чтобы выбирать тип энкодера" - о какой библиотеке идет речь? Если Encoder.h, то она считает не шаги энкодера, а изменения сигналов на его выходах. В моем случае на один шаг энкодера приходится 4 изменения, поэтому чтобы перевести их в шаги я в приведенном примере скетча делю возвращаемое библиотекой значение на 4 (сдвигая значение вправо на 2 разряда):
    long newPosition = myEnc.read() >> 2;
    Соответственно, вам нужно сдвинуть на 1 разряд:
    long newPosition = myEnc.read() >> 1;
    Я правильно понял ваш вопрос?

    ОтветитьУдалить
    Ответы
    1. Добрый день.
      Извините, сразу просматривал несколько статей. И по невнимательности задал вопрос не в той статье. Я имел ввиду библиотеку LiquidCrystal_I2C_Ext. У меня энкодер KY-040 на 2 щелчка выдает один сигнал. Я попробовал сам подправить библиотеку, но моих знаний не хватило. Буду признателен, если направите куда копать.

      Удалить
    2. Еще раз добрый день.
      Вроде нашел в интернете способ. Но в библиотеку пока не лез. Ниже код, незнаю как правильно вставить:
      //Временные переменные для хранения уровней сигналов
      //полученных от энкодера
      uint8_t curState, encState, prevState;
      bool flag_enc_type = false; // false - это мой 1 тик -> 1 импульс
      // true - 2 тика -> 1 импульс
      void setup() {
      Serial.begin(115200);
      }
      void loop() {
      Tick();
      if (encState == 2) {
      encState = 0;
      Serial.println("Right");
      }
      if (encState == 1) {
      encState = 0;
      Serial.println("Left");
      }
      }
      void Tick() {
      //CLK подключаем к пину 2 на плате Arduino
      //DT подключаем к пину 3 на плате Arduino
      //Считываем значения выходов энкодера
      //И сохраняем их в переменных
      curState = digitalRead(2);
      curState += digitalRead(3) << 1;
      encState = 0;
      if (curState == 0b11) {
      if (prevState == 0b10) encState = 1;
      else if (prevState == 0b01) encState = 2;
      } else if (curState == 0b00 && !flag_enc_type) {
      if (prevState == 0b01) encState = 1;
      else if (prevState == 0b10) encState = 2;
      }
      prevState = curState;
      }

      Удалить
    3. Не нужен вам этот код. Чем искать подходящие скетчи, разбираться в них и думать как присобачить их к библиотеке, лучше откройте LiquidCrystal_I2C_Ext.cpp и посмотрите функцию getEncoderState(). Первая ее часть относится к кнопке, вторая - к сигналам энкодера. Следующий код:
      if ((!encoderA) && (_pinAPrev)) {
      if (encoderB) Result = eRight;
      else Result = eLeft;
      }
      а вернее код внутри условия выполняется, когда текущее значение сигнала A = 0 (!encoderA) и предыдущее было 1 (_pinAPrev). Другими словами, мы отслеживаем момент изменения сигнала A от 1 к 0 и когда он наступает, мы анализируем сигнал B, чтобы определить направление вращения.

      Изменение сигнала A от 0 к 1 здесь не отслеживается, его-то вам и не хватает. Ну и добавьте после этого условия еще одно:
      else if ((encoderA) && (!_pinAPrev)){
      if (encoderB) Result = eLeft;
      else Result = eRight;
      }
      Ничего сложного нет, верно?

      Удалить
    4. https://www.pjrc.com/teensy/td_libs_Encoder.html
      - по этой ссылке есть очень хорошая иллюстрация изменения сигналов на выходах энкодера. Можно пощелкать кнопки, посмотреть, как меняется их состояние при вращении вправо или влево.

      Удалить
    5. Добрый день. И огромное спасибо.
      Я в С++ еще плаваю. Последние 15 лет я плотно занимался 1С. Поэтому библиотеки для меня еще дело новое. Я добавил в код еще проверку типа энкодера, все получилось и работает отлично. Вот что у меня вышло:
      else {
      _pinButtonPrev = 1;
      encoderA = digitalRead(_pinA);
      encoderB = digitalRead(_pinB);
      if ((!encoderA) && (_pinAPrev)) {
      if (encoderB) Result = eLeft;
      else Result = eRight;
      }
      if (!_eType) {
      if ((encoderA) && (!_pinAPrev)){
      if (encoderB) Result = eRight;
      else Result = eLeft;
      }
      }
      _pinAPrev = encoderA;
      }
      Соответственно _eType добавил в переменные и изменил:
      void attachEncoder(uint8_t, uint8_t, uint8_t, uint8_t);
      В скетче: lcd.attachEncoder(pinDT, pinCLK, pinSW, encType);
      Еще раз огромное спасибо.

      Удалить
  8. Добрый день! Подскажите, что добавить в коде, чтоб при достижении определенного значения счетчик сбрасывался в ноль и начинал считать заново. Спасибо.

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Если вы про первый скетч, то можно вместо Position++; написать if (Position == 100) Position = 0; else Position++;
      и подставьте свое значение вместо 100

      Удалить