Как использовать поворотный энкодер в проекте на микроконтроллере

Добавлено 21 марта 2017 в 13:10

Узнайте, как использовать инкрементальный поворотный энкодер в проекте на Arduino.

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

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

В данной статье мы покажем вам, как использовать инкрементальный поворотный энкодер в проекте на Arduino. Мы объясним, как бороться с дребезгом контактов и интерпретировать сигналы энкодера в программе микроконтроллера, используя прерывания.

Сигнал квадратурного выхода инкрементального энкодера

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

Сигналы на выходах инкрементального поворотного энкодера при вращении вала по часовой стрелке и против
Сигналы на выходах инкрементального поворотного энкодера при вращении вала по часовой стрелке и против

Как видно из рисунка, оба выхода в изначально находятся в состоянии логической единицы. Когда вал энкодера начинает вращаться в направлении по часовой стрелке, первым падает до логического нуля состояние на выходе A, а затем с отставанием за ним следует и выход B. При вращении против часовой стрелки всё происходит наоборот. Временные интервалы на диаграмме сигнала зависят от скорости вращения, но отставание сигналов гарантируется в любом случае. На основе этой характеристики инкрементального поворотного энкодера мы напишем программу для Arduino.

Фильтрация дребезга контактов механического энкодера

Механические энкодеры имеют встроенные переключатели, которые формируют сигнал на квадратурном выходе во время вращения.

Дребезг контактов на выходе механического энкодера
Дребезг контактов на выходе механического энкодера

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

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

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

RC фильтр и форма сигнала на его выходе
RC фильтр и форма сигнала на его выходе

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

Простое приложение

Мы создадим приложение, демонстрирующее, как использовать поворотный энкодер в проекте на Arduino. Мы будем использовать энкодер для навигации, ввода данных и выбора. Ниже приведена принципиальная схема приложения.

Принципиальная схема примера приложения с использованием поворотного энкодера на Arduino
Принципиальная схема примера приложения с использованием поворотного энкодера на Arduino

Схема построена на базе платы Arduino Uno. Для графического интерфейса используется LCD дисплей Nokia 5110. В качестве средств управления добален механический поворотный энкодер с кнопкой и RC-фильтрами.

Собранная схема примера использования поворотного энкодера на Arduino
Собранная схема примера использования поворотного энкодера на Arduino

Мы разработаем простое программное меню, в котором и продемонстрируем работу поворотного энкодера.

Обработка сигналов энкодера с помощью прерываний

Сигналы энкодера должны быть обнаружены и интерпретированы в программе как можно быстрее, чтобы не блокировать основной поток программы. Мы можем детектировать сигналы путем опроса в основном цикле, или используя прерывания. Опрос не эффективен, так как вам необходимо зарезервировать время и ресурсы в основном цикле, что приводит к дополнительным задержкам. Использование прерываний – это более быстрое и экономичное решение. Мы покажем вам, как использовать прерывания для обработки сигналов энкодера.

В Atmega328 есть два типа прерываний, которые можно использовать для этих целей; внешнее прерывание и прерывание по изменению состояния вывода. Выводы INT0 и INT1 назначены на внешнее прерывание, а PCINT0-PCIN15 назначены на прерывание по изменению состояния вывода. Внешнее прерывание может определить, произошел ли спад или нарастание входного сигнала, и может быть запущено при одном из следующих состояний: нарастание, спад или переключение. Для прерывания по изменению состояния выводов существует гораздо больше аппаратных ресурсов, но оно не может обнаруживать нарастающий и спадающий фронты, и оно вызывается, когда происходит любое изменение логического состояния (переключение) на выводе.

Чтобы использовать прерывание по изменению состояния выводов, подключите выходы поворота энкодера A и B к выводам A1 и A2, а выход кнопки – к выводу A0 платы Arduino, как показано на принципиальной схеме. Установите выводы A0, A1 и A2 в режим входа и включите их внутренние подтягивающие резисторы. Включите прерывание по изменению состояния выводов в регистре PCICR и включите прерывания для выводов A0, A1 и A2 в регистре PCMS1. При обнаружении любого изменения логического состояния на одном из этих входов будет вызовано ISR(PCINT1_vect) (прерывание по изменению состояния выводов).

Поскольку прерывание по изменению состояния выводов вызывается для любого логического изменения, нам необходимо отслеживать оба сигнала (и A, и B) и обнаруживать вращение при получение ожидаемой последовательности. Как видно из диаграммы сигналов, движение по часовой стрелке генерирует A = …0011… и B = …1001…. Когда мы записываем оба сигналы в байты seqA и seqB, сдвигая последнее чтение вправо, мы можем сравнить эти значения и определить новый шаг вращения.

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

void setup() 
{
  pinMode(A0, INPUT);
  pinMode(A1, INPUT);
  pinMode(A2, INPUT);
  
  // Включить внутренние подтягивающие резисторы
  digitalWrite(A0, HIGH);
  digitalWrite(A1, HIGH);
  digitalWrite(A2, HIGH);
 
  PCICR =  0b00000010; // 1. PCIE1: Включить прерывание 1 по изменению состояния
  PCMSK1 = 0b00000111; // Включить прерывание по изменению состояния для A0, A1, A2
}

void loop() 
{
  // Основной цикл
}

ISR (PCINT1_vect) 
{

  // Если прерывание вызвано кнопкой
  if (!digitalRead(A0)) 
  {  
    button = true;
  }

  // Если прерывание вызвано сигналами энкодера
  else 
  {
    
    // Прочитать сигналы A и B
    boolean A_val = digitalRead(A1);
    boolean B_val = digitalRead(A2);
    
    // Записать сигналы A и B в отдельные последовательности
    seqA <<= 1;
    seqA |= A_val;
    
    seqB <<= 1;
    seqB |= B_val;
    
    // Маскировать четыре старших бита
    seqA &= 0b00001111;
    seqB &= 0b00001111;
    
    // Сравнить запсанную последовательность с ожидаемой последовательностью
    if (seqA == 0b00001001 && seqB == 0b00000011) 
    {
      cnt1++;
      left = true;
    }
     
    if (seqA == 0b00000011 && seqB == 0b00001001) 
    {
      cnt2++;
      right = true;
    }
  }
  
}  

Использование внешнего прерывания делает процесс более простым, но поскольку для этого прерывания назначено только два вывода, то вы не сможете использовать его для других целей, если займете его энкодером. Чтобы использовать внешнее прерывание, вы должны установить выводы 2 (INT0) и 3 (INT1) в режим входа и включить их внутренние подтягивающие резисторы. Затем выберите вариант спадающего фронта для вызова обоих прерываний в регистре EICRA. Включите внешние прерывания в регистре EIMSK. Когда начнется вращение вала энкодера, сначала ведущий сигнал падает до логического нуля, а второй сигнал некоторое время остается на уровне логической единицы. Поэтому нам нужно определить, какой из сигналов во время прерывания находится в состоянии логической единицы. После того, как ведущий сигнал упал до логического нуля, через некоторое время второй сигнал также упадет до логического нуля, что вызовет другое прерывание. Но этот раз и другой (ведущий) сигнал будет на низком логическом уровне, что означает, что это не начало вращения, поэтому мы игнорируем его.

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

void setup() 
{
  pinMode(2, INPUT);
  pinMode(3, INPUT);
  
  // Включить внутренние подтягивающие резисторы
  digitalWrite(2, HIGH);
  digitalWrite(3, HIGH);
  
  EICRA = 0b00001010; // Выбрать вызов по спадающему фронту
  EIMSK = 0b00000011; // Включить внешнее прерывание
}

void loop() 
{
  // Основной цикл
}

ISR (INT0_vect) 
{
  // Если второй сигнал находится в состоянии логической единицы, то это новое вращение
  if (digitalRead(3) == HIGH) 
  {
    left = true;
  }

}

ISR (INT1_vect) 
{  
  // Если второй сигнал находится в состоянии логической единицы, то это новое вращение
  if (digitalRead(2) == HIGH) 
  {
    right = true;
  }

}
Макет для проверки кода работы с инкрементальным поворотным энкодером на Arduino
Макет для проверки кода работы с инкрементальным поворотным энкодером на Arduino

Полный код скетча Arduino, включающий основной цикл приведен ниже:

#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_PCD8544.h>

volatile byte seqA = 0;
volatile byte seqB = 0;
volatile byte cnt1 = 0;
volatile byte cnt2 = 0;
volatile boolean right = false;
volatile boolean left = false;
volatile boolean button = false;
boolean backlight = true;
byte menuitem = 1;
byte page = 1;

Adafruit_PCD8544 display = Adafruit_PCD8544(13, 12,11, 8, 10);

void setup() 
{
  
  pinMode(A0, INPUT);
  pinMode(A1, INPUT);
  pinMode(A2, INPUT);
    
  // Включить внутренние подтягивающие резисторы
  digitalWrite(A0, HIGH);
  digitalWrite(A1, HIGH);
  digitalWrite(A2, HIGH);

  // Включить подсветку LCD
  pinMode(9, OUTPUT);
  digitalWrite(9, HIGH);
  
  PCICR =  0b00000010; // 1. PCIE1: Включить прерывание 1 по изменению состояния
  PCMSK1 = 0b00000111; // Включить прерывание по изменению состояния для A0, A1, A2

  // Initialize LCD
  display.setRotation(2); // Установить ориентацию LDC
  display.begin(60);      // Установить контрастность LCD
  display.clearDisplay(); // Очистить дисплей
  display.display();      // Применить изменения

  sei();
}

void loop() {
  
  // Создать страницы меню
  if (page==1) 
  {
    
    display.setTextSize(1);
    display.clearDisplay();
    display.setTextColor(BLACK, WHITE);
    display.setCursor(15, 0);
    display.print("MAIN MENU");
    display.drawFastHLine(0,10,83,BLACK);
    
    display.setCursor(0, 15);
    if (menuitem==1) 
    { 
      display.setTextColor(WHITE, BLACK);
    }
    else 
    {
      display.setTextColor(BLACK, WHITE);
    }
    display.print(">Contrast: 99%");
    
    display.setCursor(0, 25);
    if (menuitem==2) 
    { 
      display.setTextColor(WHITE, BLACK);
    }
    else 
    {
      display.setTextColor(BLACK, WHITE);
    }    
    display.print(">Test Encoder");
    
    if (menuitem==3) 
    { 
      display.setTextColor(WHITE, BLACK);
    }
    else 
    {
      display.setTextColor(BLACK, WHITE);
    }  
    display.setCursor(0, 35);
    display.print(">Backlight:");
    if (backlight) 
    {
      display.print("ON");
    }
    else 
    {
      display.print("OFF");
    }
    display.display();
  
  }
  else if (page==2) 
  {
    display.setTextSize(1);
    display.clearDisplay();
    display.setTextColor(BLACK, WHITE);
    display.setCursor(15, 0);
    display.print("ENC. TEST");
    display.drawFastHLine(0,10,83,BLACK);
    display.setCursor(5, 15);
    display.print("LEFT    RIGHT");
    display.setTextSize(2);
    display.setCursor(5, 25);
    display.print(cnt1);
    display.setCursor(55, 25);
    display.print(cnt2);
    display.setTextSize(2);
    display.display();
  }

  // Выполнить действие, если от энкодера принята новая команда
  if (left) 
  {
    left = false;
    menuitem--;
    if (menuitem==0) 
    {
      menuitem=3;
    }      
  }

  if (right) 
  {
    right = false;
    menuitem++;
    if (menuitem==4) 
    {
      menuitem=1;
    }
  }

  if (button) 
  {
    button = false;
    
    if (page == 1 && menuitem==3) 
    {
      digitalWrite(9, LOW);
      if (backlight) 
      {
        backlight = false; digitalWrite(9, LOW);
      }
      else 
      {
        backlight = true; digitalWrite(9, HIGH);
      }
    }
    else if (page == 1 && menuitem==2) 
    {
      page=2;
      cnt1=0;
      cnt2=0;        
    }
    else if (page == 2) 
    {
      page=1;
    }
  }
   
}


ISR (PCINT1_vect) 
{

  // Если прерывание вызвано кнопкой 
  if (!digitalRead(A0)) 
  {  
    button = true;
  }
  // Или если прерывание вызвано сигналами энкодера
  else 
  {
    
    // Прочитать сигналы A и B
    boolean A_val = digitalRead(A1);
    boolean B_val = digitalRead(A2);
    
    // Записать сигналы A и B в отдельные последовательности
    seqA <<= 1;
    seqA |= A_val;
    
    seqB <<= 1;
    seqB |= B_val;
    
    // Маскировать четыре старших бита
    seqA &= 0b00001111;
    seqB &= 0b00001111;
    
    // Сравнить запсанную последовательность с ожидаемой последовательностью
    if (seqA == 0b00001001 && seqB == 0b00000011) 
    {
      cnt1++;
      left = true;
    }
     
    if (seqA == 0b00000011 && seqB == 0b00001001) 
    {
      cnt2++;
      right = true;
    }
  }

}

Энкодер в действии вы можете увидеть на видео, приведенном ниже:

Вот и всё! Надеюсь, статья оказалась полезной. Оставляйте комментарии!

Теги

ArduinoMCUИнкрементальный энкодерМикроконтроллерПоворотный энкодерПрерываниеПрерывание GPIOФильтрация шума

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.


  • 2018-12-19Alexei Bubnyshev

    Пожалуйста, разъясните чайнику (прочитал статью, посмотрел иллюстрации): зачем такие сложности для определения вращения? А нельзя ли вот так:
    - канал А - прерывание по спаду
    - канал В - просто digitalRead

    boolean MoveRight = false;
    boolean MoveLeft = false;

    EncoderInterrupt() {
    if (digitalRead(pinB) == HIGH)
    {
    MoveRight = true;
    }
    else
    {
    MoveLeft = true;
    }
    }

    Судя по графику сигналов энкодера: в момент спада на канале А - канал В может быть либо 1, либо 0 в зависимости от направления вращения

  • 2018-12-19Alexei Bubnyshev

    Энкодеры отличаются иногда... Например в авто есть такие, где на землю замкнут только один выход в каждый момент времени. И сигналы чередуются при вращении

  • 2018-11-05Alex

    Добрый день!

    В какой программе нарисован график?

  • 2018-06-06Vitali Mitko

    Нет у меня на каждые 2 поворота 1 щелчек был. Использовал энкодер от автомагнитолы. Насколько я понимаю энкодеры не отличаются.

  • 2018-06-05heavythelegacy

    возможно вы не разобрались в причине вашей проблемы. предполагаю, что на 2 "счилчка" у вас считало только при смене направления кручения, а дальше считало нормально ;) точнее при смене направления не засчитывало ничего на первый проворот.

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

    if (seqA == 0b00000110 && seqB == 0b00001100) {
    cntA++;
    }

    if (seqA == 0b00001100 && seqB == 0b00000110) {
    cntB++;
    }

  • 2018-05-26Vitali Mitko

    Энкодер не корректно считает, +1 на 2 счилчка.
    если это кого то раздражает легко исправить.
    seqA &= 0b00000011;
    seqB &= 0b00000011;

    if ((seqA == 0b00000001 && seqB == 0b00000011)|(seqA == 0b00000010 && seqB == 0b00000000)) // left

    if ((seqA == 0b00000000 && seqB == 0b00000010)|(seqA == 0b00000011 && seqB == 0b00000001)) // right

    но все равно, большое спасибо за проделанную работу.