Как работает поворотный энкодер. Взаимодействие с Ардуино

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

Благодаря своей надежности и точному цифровому управлению энкодеры используются во многих приложениях, включая робототехнику, станки с ЧПУ и так далее.

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

Поворотные энкодеры против потенциометров

Поворотные энкодеры являются современным цифровым эквивалентом потенциометра и они более универсальны, чем потенциометры. Они могут полностью вращаться без концевых упоров, в то время как потенциометр может вращаться только примерно на  3/4 круга.

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

Как работают поворотные энкодеры

Внутри энкодера находится диск с прорезями, подключенный к общему контакту «C», и два контакта «A» и «B», как показано ниже:

Как работают поворотные энкодеры

Когда вы поворачиваете ручку, выводы «A» и «B» входят в контакт с общим контактом «C» в определенном порядке в соответствии с направлением, в котором вы поворачиваете ручку.

Когда они вступают в контакт с выводом «С», они вырабатывают сигналы. Эти сигналы сдвинуты по фазе на 90° друг относительно друга, поскольку один вывод входит в контакт раньше другого:

квадратурное кодированием

Когда вы поворачиваете ручку по часовой стрелке, сначала подключается контакт «A», а затем контакт «B». Когда вы поворачиваете ручку против часовой стрелки, сначала подключается контакт «B», а затем контакт «A».

Отслеживая последовательность замыкания и размыкания с контактом «C», мы можем определить в каком направлении была повернута ручка энкодера. Вы можете сделать это, просто наблюдая за состоянием «B» в момент когда «A» меняет состояние.

Когда сигнал на выводе  «А» меняет состояние, то:

  • если B! = A, то ручка повернута по часовой стрелке:

ручка повернута по часовой стрелке

  • если B = A, то ручка повернута против часовой стрелки:

повернута против часовой стрелки

 

Распиновка поворотного энкодера

Распиновка у модуля поворотного энкодера следующая:

Распиновка поворотного энкодера

 

  • GND — земля.
  • VCC — питание обычно 3,3В или 5В.
  • SW — активная кнопка низкого уровня. Когда кнопка нажата, напряжение падает.
  • DT (выход «B») — аналогичен выходному сигналу CLK, но отстает от CLK на 90°. Этот выходной сигнал можно использовать для определения направления вращения.
  • CLK (выход A) — основной выходной импульс для определения величины вращения. Каждый раз, когда ручка поворачивается на один щелчок в любом направлении, на выводе «CLK» вырабатывается один импульс — HIGH, а затем LOW.

Подключение поворотного энкодера к Ардуино

Теперь, когда мы знаем все о поворотном энкодере, пришло время применить его! Подключим поворотный энкодер к Ардуино. Подключение довольно простое. Начните с подключения вывода VCC на модуле к 5 В на Ардуино и вывода GND к  GND Ардуино.

Теперь подключите контакты CLK и DT к цифровым контактам №2 и №3 Ардуино соответственно. Наконец, подключите вывод SW к цифровому выводу №4. На следующем рисунке показана схема соединения:

Подключение поворотного энкодера к Ардуино

 

Скетч Ардуино — чтение поворотных энкодеров

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

// Входы поворотного энкодера
#define CLK 2
#define DT 3
#define SW 4

int counter = 0;
int currentStateCLK;
int lastStateCLK;
String currentDir ="";
unsigned long lastButtonPress = 0;

void setup() {
  
  // Устанавливаем контакты энкодера как входы
  pinMode(CLK,INPUT);
  pinMode(DT,INPUT);
  pinMode(SW, INPUT_PULLUP);

  // Настройка последовательного монитора
  Serial.begin(9600);

  // Считываем начальное состояние CLK
  lastStateCLK = digitalRead(CLK);
}

void loop() {
  
  // Считываем текущее состояние CLK
  currentStateCLK = digitalRead(CLK);

  // Если последнее и текущее состояние CLK различаются, то произошел импульс.
  // Реагируем только на одно изменение состояния, чтобы избежать двойного счета
  if (currentStateCLK != lastStateCLK  && currentStateCLK == 1){

    // Если состояние DT отличается от состояния CLK, то
    // энкодер вращается против часовой стрелки, поэтому уменьшаем
    if (digitalRead(DT) != currentStateCLK) {
      counter --;
      currentDir ="CCW";
    } else {
      // Энкодер вращается по часовой стрелке, поэтому увеличиваем
      counter ++;
      currentDir ="CW";
    }

    Serial.print("Direction: ");
    Serial.print(currentDir);
    Serial.print(" | Counter: ");
    Serial.println(counter);
  }

  // Запоминаем последнее состояние CLK
  lastStateCLK = currentStateCLK;

  // Считываем состояние кнопки
  int btnState = digitalRead(SW);

  //Если мы обнаруживаем LOW сигнал, кнопка нажата
  if (btnState == LOW) {
    // если с момента последнего LOW импульса прошло 50 мс, это означает, что кнопка была нажата, отпущена и снова нажата
    if (millis() - lastButtonPress > 50) {
      Serial.println("Button pressed!");
    }

    // Запомнить последнее нажатие кнопки
    lastButtonPress = millis();
  }

  // Даем небольшую задержку, чтобы облегчить чтение
  delay(1);
}

Если все в порядке, то вы должны в мониторе последовательного порта увидеть следующее:

результат

Если сообщения о вращении противоположно ожидаемому, попробуйте поменять местами строки CLK и DT.

Пояснение к коду:

Скетч начинается с объявления выводов Ардуино, к которым подключены выводы CLK, DT и SW энкодера:

// Входы поворотного энкодера
#define CLK 2
#define DT 3
#define SW 4

Затем определяются несколько переменных. Переменная counter представляет собой счетчик, который будет изменен каждый раз, когда ручка энкодера будет повернута на один щелчок.

Переменные currentStateCLK и lastStateCLK содержат состояние выхода CLK и используются для определения величины вращения. Вызываемая строка currentDir используется при печати текущего направления вращения в последовательный монитор. Переменная lastButtonPress используется для исключения дребезга контактов кнопки:

int counter = 0;
int currentStateCLK;
int lastStateCLK;
String currentDir ="";
unsigned long lastButtonPress = 0;

Далее в функции setup() мы сначала определяем подключения к энкодеру как входы, а затем подключаем подтягивающий резистор на выводе SW. Мы также настраиваем последовательный монитор. В конце мы считываем текущее состояние вывода CLK и сохраняем его в переменной lastStateCLK:

void setup() {
  
  // Устанавливаем контакты энкодера как входы
  pinMode(CLK,INPUT);
  pinMode(DT,INPUT);
  pinMode(SW, INPUT_PULLUP);

  // Настройка последовательного монитора
  Serial.begin(9600);

  // Считываем начальное состояние CLK
  lastStateCLK = digitalRead(CLK);
}

В функции loop() мы снова проверяем состояние CLK и сравниваем его со значением lastStateCLK. Если они разные, это означает, что ручка повернута и возник импульс. Мы также проверяем, равно ли значение currentStateCLK 1, чтобы отреагировать только на одно изменение состояния, чтобы избежать двойного счета:

// Считываем текущее состояние CLK
  currentStateCLK = digitalRead(CLK);

  // Если последнее и текущее состояние CLK различаются, то произошел импульс.
  // Реагируем только на одно изменение состояния, чтобы избежать двойного счета
  if (currentStateCLK != lastStateCLK  && currentStateCLK == 1){

Внутри оператора if мы определяем направление вращения. Для этого мы просто считываем вывод DT на модуле энкодера и сравниваем его с текущим состоянием вывода CLK.

Если они разные, это означает, что ручка повернута против часовой стрелки. Затем мы уменьшаем значение счетчика и устанавливаем его currentDir на «CCW».

Если два значения совпадают, это означает, что ручка вращается по часовой стрелке. Затем мы увеличиваем счетчик и устанавливаем currentDir на «CW»:

if (currentStateCLK != lastStateCLK  && currentStateCLK == 1){

  // Если состояние DT отличается от состояния CLK, то
  // энкодер вращается против часовой стрелки, поэтому уменьшаем
  if (digitalRead(DT) != currentStateCLK) {
    counter --;
    currentDir ="CCW";
  } else {
    // Энкодер вращается по часовой стрелке, поэтому увеличиваем
    counter ++;
    currentDir ="CW";
  }

Затем мы отправляем наши результаты в монитор последовательного порта:

Serial.print("Direction: ");
Serial.print(currentDir);
Serial.print(" | Counter: ");
Serial.println(counter);

Вне оператора if мы обновляем lastStateCLK текущее состояние CLK:

// Запоминаем последнее состояние CLK
lastStateCLK = currentStateCLK;

Далее идет логика для считывания кнопки и предотвращения дребезга ее контактов. Сначала мы читаем текущее состояние кнопки, если оно LOW, мы ждем 50 мсек, чтобы отметить нажатие кнопки. Если кнопка остается в LOW состоянии более 50 мс, мы печатаем в монитор последовательного порта «Button pressed!«:

// Считываем состояние кнопки
int btnState = digitalRead(SW);

//Если мы обнаруживаем LOW сигнал, кнопка нажата
if (btnState == LOW) {
  // если с момента последнего LOW импульса прошло 50 мс, это означает, что кнопка была нажата, отпущена и снова нажата
  if (millis() - lastButtonPress > 50) {
    Serial.println("Button pressed!");
  }

  // Запомнить последнее нажатие кнопки
  lastButtonPress = millis();
}

Затем мы все повторяем снова.

Код Ардуино с использованием прерываний

Чтобы поворотный энкодер работал, нам необходимо постоянно отслеживать изменения в сигналах DT и CLK. Чтобы определить, когда происходят такие изменения, мы можем постоянно их опрашивать (как мы это делали в нашем предыдущем скетче). Однако это не лучшее решение по нижеприведенным причинам:

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

Широко распространенным решением является использование прерывания. С прерыванием вам не нужно постоянно опрашивать конкретное событие. Это освобождает Ардуино для выполнения какой-то другой работы, не пропуская событие.

Подключение энкодера к Ардуино с учетом прерывания

Поскольку большинство Ардуино (включая Ардуино UNO) имеют только два внешних прерывания, мы можем отслеживать только изменения в сигналах DT и CLK. Вот почему мы удалили соединение вывода SW с предыдущей схемы подключения.

Итак, теперь схема подключения выглядит так:

Подключение энкодера к Ардуино с учетом прерывания

Некоторые платы (например, Ардуино Mega 2560) имеют больше внешних прерываний. Если у вас есть что-то из этого, вы можете оставить соединение для вывода SW и расширить рисунок ниже, чтобы использовать код и для кнопки.

Код Ардуино

Ниже приведен скетч, демонстрирующий использование прерываний при чтении углового энкодера:

// Входы поворотного энкодера
#define CLK 2
#define DT 3

int counter = 0;
int currentStateCLK;
int lastStateCLK;
String currentDir ="";

void setup() {
  
  // Устанавливаем контакты энкодера как входы
  pinMode(CLK,INPUT);
  pinMode(DT,INPUT);

  // Настройка последовательного монитора
  Serial.begin(9600);

  // Считываем начальное состояние CLK
  lastStateCLK = digitalRead(CLK);
  
  // Вызов updateEncoder () при обнаружении любого изменения максимума/минимума при прерывании 0 (вывод 2) или прерывании 1 (вывод 3)
  attachInterrupt(0, updateEncoder, CHANGE);
  attachInterrupt(1, updateEncoder, CHANGE);
}

void loop() {
  //Здесь какой-либо полезный код
}

void updateEncoder(){
  // Считываем текущее состояние CLK
  currentStateCLK = digitalRead(CLK);

  // Если последнее и текущее состояние CLK различаются, то произошел импульс.
  // Реагируем только на одно изменение состояния, чтобы избежать двойного счета
  if (currentStateCLK != lastStateCLK  && currentStateCLK == 1){

    // Если состояние DT отличается от состояния CLK, то
    // энкодер вращается против часовой стрелки, поэтому уменьшаем
    if (digitalRead(DT) != currentStateCLK) {
      counter --;
      currentDir ="CCW";
    } else {
      // Энкодер вращается по часовой стрелке, поэтому увеличиваем
      counter ++;
      currentDir ="CW";
    }

    Serial.print("Direction: ");
    Serial.print(currentDir);
    Serial.print(" | Counter: ");
    Serial.println(counter);
  }

  // Запоминаем последнее состояние CLK
  lastStateCLK = currentStateCLK;
}

Обратите внимание, что основной цикл этой программы loop() остается пустым, поэтому Ардуино ничего не делает.

Между тем, эта программа отслеживает изменение значения на цифровом выводе 2 (соответствует прерыванию 0) и цифровом выводе 3 (соответствует прерыванию 1). Другими словами, программа ищет изменение напряжения с HIGH на LOW или с LOW на HIGH, которое происходит при повороте ручки.

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

За все это отвечают две строки в коде. Эта функция attachInterrupt() которая сообщает Ардуино, какой вывод следует контролировать, какой ISR выполнять, если прерывание срабатывает, и какой тип триггера следует искать.

Добавить комментарий

Ваш электронный адрес не будет опубликован.


*