Как работает энкодер и подключение к Arduino

Как работает энкодер и подключение к Arduino

Вы когда-нибудь крутили ручку громкости на колонке, настраивали параметры микроволновки или использовали колёсико прокрутки на компьютерной мыши? Если да, то вы уже пользовались энкодером — возможно, даже не подозревая об этом! Эти умные маленькие устройства повсюду вокруг нас, спрятанные внутри таких устройств, как духовки, автомобильные панели приборов и цифровые камеры. Они помогают плавно и точно управлять вещами простым поворотом.

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

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

Давайте начнём!

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

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

How Rotary Encoder Works and Interface It with Arduino

Когда вы поворачиваете ручку энкодера, диск с прорезями вращается вместе с ней. При этом выводы A и B многократно замыкаются на общую землю (вывод C).

Важно понять, как именно происходит замыкание. Из-за расположения прорезей выводы A и B касаются земли не одновременно. Один вывод всегда касается чуть раньше другого. Это создаёт два отдельных сигнала, слегка сдвинутых по времени. Технически они «сдвинуты по фазе на 90 градусов».

How Rotary Encoder Works and Interface It with Arduino

Итак, как мы определяем направление вращения ручки? Мы делаем это, наблюдая за состоянием вывода B в момент изменения состояния вывода A.

Когда вывод A меняет своё состояние:

  • Если состояние вывода B отличается от вывода A (B ≠ A), то ручка вращается по часовой стрелке.

Временная диаграмма импульсов энкодера при вращении по часовой стрелке
  • Если состояние вывода B совпадает с выводом A (B = A), то ручка вращается против часовой стрелки.

Временная диаграмма импульсов энкодера при вращении против часовой стрелки

Этот метод отслеживания движения называется квадратурным кодированием.

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

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

How Rotary Encoder Works and Interface It with Arduino

GND — это вывод заземления.

  • (VCC) — питание энкодера. Обычно подключается к выводу 5V или 3.3V на Arduino.

SW (Switch) — подключён к встроенной кнопке внутри энкодера. Обычно этот вывод удерживается в состоянии HIGH с помощью внутреннего подтягивающего резистора Arduino или внешнего подтягивающего резистора. При нажатии кнопки вывод SW переходит в состояние LOW.

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

DT — обеспечивает второй квадратурный выходной сигнал. Этот сигнал аналогичен CLK, но сдвинут по фазе на 90 градусов. Как и CLK, DT подключается к другому цифровому входному выводу Arduino.

Подключение энкодера к Arduino

Теперь, когда мы понимаем, как работает энкодер, давайте подключим его к Arduino и начнём использовать! Подключение простое и понятное.

Сначала нам нужно обеспечить питание энкодера. Подключите вывод + (VCC) энкодера к 5V Arduino, а вывод GND — к GND Arduino.

Далее подключим сигнальные выводы. Вывод CLK энкодера следует подключить к цифровому выводу #2 Arduino, а вывод DT — к цифровому выводу #3.

Наконец, нужно подключить вывод кнопки. Подключите вывод SW к цифровому выводу #4 Arduino.

В следующей таблице перечислены подключения выводов:

Rotary EncoderArduino
VCC (+)5V
GNDGND
SW4
DT3
CLK2

Вы также можете обратиться к схеме подключения ниже для наглядности:

How Rotary Encoder Works and Interface It with Arduino

После завершения подключений ваш энкодер будет готов к использованию с Arduino!

Пример кода Arduino 1 — Чтение энкодера

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

Сначала попробуйте скетч ниже, а затем мы подробно рассмотрим, как он работает.

// Rotary Encoder Inputs
#define CLK 2
#define DT 3
#define SW 4

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

void setup() {

  // Set encoder pins as inputs
  pinMode(CLK, INPUT);
  pinMode(DT, INPUT);
  pinMode(SW, INPUT_PULLUP);

  // Setup Serial Monitor
  Serial.begin(9600);

  // Read the initial state of CLK
  lastStateCLK = digitalRead(CLK);
}

void loop() {

  // Read the current state of CLK
  currentStateCLK = digitalRead(CLK);

  // If last and current state of CLK are different, then pulse occurred
  // React to only 1 state change to avoid double count
  if (currentStateCLK != lastStateCLK && currentStateCLK == 1) {

    // If the DT state is different than the CLK state then
    // the encoder is rotating CCW so decrement
    if (digitalRead(DT) != currentStateCLK) {
      counter--;
      currentDir = "CCW";
    } else {
      // Encoder is rotating CW so increment
      counter++;
      currentDir = "CW";
    }

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

  // Remember last CLK state
  lastStateCLK = currentStateCLK;

  // Read the button state
  int btnState = digitalRead(SW);

  //If we detect LOW signal, button is pressed
  if (btnState == LOW) {
    //if 50ms have passed since last LOW pulse, it means that the
    //button has been pressed, released and pressed again
    if (millis() - lastButtonPress > 50) {
      Serial.println("Button pressed!");
    }

    // Remember last button press event
    lastButtonPress = millis();
  }

  // Put in a slight delay to help debounce the reading
  delay(1);
}

После загрузки кода на Arduino откройте монитор последовательного порта. Поверните ручку в обоих направлениях и наблюдайте, как изменяются значения счётчика на экране. Попробуйте нажать кнопку и посмотрите, появится ли сообщение «Button pressed!».

How Rotary Encoder Works and Interface It with Arduino

Если вы заметили, что направление вращения противоположно ожидаемому, вы можете легко исправить это, поменяв местами подключения выводов CLK и DT.

Объяснение кода

В начале кода мы определяем, какие выводы Arduino подключены к выводам CLK, DT и SW энкодера.

#define CLK 2
#define DT 3
#define SW 4

Мы также создаём несколько важных переменных.

  • Переменная counter отслеживает количество шагов, на которые повернулся энкодер.

  • Переменные currentStateCLK и lastStateCLK хранят состояние вывода CLK в разные моменты времени, что помогает обнаруживать движение.

  • Переменная currentDir сообщает, вращается ли энкодер по часовой стрелке (CW) или против часовой стрелки (CCW).

  • Наконец, переменная lastButtonPress помогает предотвратить ложные нажатия кнопки, гарантируя, что мы не считаем несколько нажатий, когда вы нажали кнопку только один раз.

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

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

Наконец, мы считываем начальное состояние вывода CLK и сохраняем его в lastStateCLK. Это важно, потому что помогает обнаруживать перемещение ручки.

pinMode(CLK,INPUT);
pinMode(DT,INPUT);
pinMode(SW, INPUT_PULLUP);

Serial.begin(9600);

lastStateCLK = digitalRead(CLK);

В функции loop() мы считываем текущее состояние вывода CLK и сравниваем его с предыдущим состоянием, хранящимся в lastStateCLK. Если состояние изменилось, мы знаем, что ручка была повёрнута. Чтобы определить направление, мы проверяем вывод DT.

Если DT отличается от CLK, энкодер вращается против часовой стрелки (CCW), и мы уменьшаем счётчик, устанавливая currentDir в «CCW». Если DT совпадает с CLK, энкодер вращается по часовой стрелке (CW), и мы увеличиваем счётчик, устанавливая currentDir в «CW».

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

  if (digitalRead(DT) != currentStateCLK) {
    counter--;
    currentDir = "CCW";
  } else {
    counter++;
    currentDir = "CW";
  }

Затем мы выводим направление и значение счётчика в монитор последовательного порта.

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

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

lastStateCLK = currentStateCLK;

Для кнопки мы проверяем, нажата ли она (когда SW в состоянии LOW). Чтобы избежать ложных срабатываний от дребезга контактов (микровибраций при нажатии), мы ждём 50 миллисекунд, чтобы убедиться, что кнопка действительно нажата. Если она остаётся нажатой в течение этого времени, мы выводим «Button pressed!» в монитор последовательного порта. Мы также записываем время нажатия для корректного обнаружения будущих нажатий.

int btnState = digitalRead(SW);

if (btnState == LOW) {
    if (millis() - lastButtonPress > 50) {
        Serial.println("Button pressed!");
    }
    lastButtonPress = millis();
}

Мы завершаем цикл небольшой задержкой в 1 миллисекунду для стабилизации показаний перед повторным запуском.

delay(1);

Пример кода Arduino 2 — Использование прерываний

Когда мы используем энкодер, нам нужно постоянно проверять изменения сигналов DT и CLK для отслеживания его движения. В первом примере мы делали это путём постоянной проверки этих значений снова и снова в основном цикле программы. Хотя этот метод работает, он не самый эффективный по нескольким причинам:

  1. Arduino приходится постоянно проверять эти сигналы, даже когда ничего не происходит. Это как постоянно спрашивать «Что-нибудь изменилось?» Это расходует ценные вычислительные ресурсы.

  2. Иногда возникает небольшая задержка между моментом поворота ручки и моментом, когда Arduino это замечает. Если Arduino занят чем-то другим, он может среагировать не сразу.

  3. Если вы вращаете ручку очень быстро, Arduino может полностью пропустить некоторые движения, потому что не проверял состояние в нужный момент.

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

Схема подключения

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

How Rotary Encoder Works and Interface It with Arduino

Код Arduino

После того как вы всё подключили, как показано на схеме, загрузите этот новый код на Arduino.

// Rotary Encoder Inputs
#define CLK 2
#define DT 3

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

void setup() {
  // Set encoder pins as inputs
  pinMode(CLK, INPUT);
  pinMode(DT, INPUT);

  // Setup Serial Monitor
  Serial.begin(9600);

  // Read the initial state of CLK
  lastStateCLK = digitalRead(CLK);

  // Call updateEncoder() when a change is seen on CLK pin
  attachInterrupt(digitalPinToInterrupt(CLK), updateEncoder, CHANGE);
}

void loop() {
  //Do some useful stuff here
}

void updateEncoder() {
  // Read the current state of CLK
  currentStateCLK = digitalRead(CLK);

  // If last and current state of CLK are different, then pulse occurred
  // React to only 1 state change to avoid double count
  if (currentStateCLK != lastStateCLK && currentStateCLK == 1) {

    // If the DT state is different than the CLK state then
    // the encoder is rotating CCW so decrement
    if (digitalRead(DT) != currentStateCLK) {
      counter--;
      currentDir = "CCW";
    } else {
      // Encoder is rotating CW so increment
      counter++;
      currentDir = "CW";
    }

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

  // Remember last CLK state
  lastStateCLK = currentStateCLK;
}

Когда вы запустите код и повернёте ручку энкодера, вы увидите, что направление и значения счётчика мгновенно обновляются в мониторе последовательного порта. Благодаря использованию прерываний Arduino реагирует намного точнее, чем раньше.

How Rotary Encoder Works and Interface It with Arduino

Объяснение кода

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

В начале кода мы определяем выводы для CLK и DT, а также несколько переменных для отслеживания движения энкодера, как и раньше.

#define CLK 2
#define DT 3

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

В функции setup() мы настраиваем выводы CLK и DT как входы и запускаем монитор последовательного порта для просмотра результатов. Затем мы считываем начальное состояние вывода CLK и сохраняем его в переменной lastStateCLK, что помогает обнаруживать будущие изменения.

// Set encoder pins as inputs
pinMode(CLK, INPUT);
pinMode(DT, INPUT);

// Setup Serial Monitor
Serial.begin(9600);

// Read the initial state of CLK
lastStateCLK = digitalRead(CLK);

Самая важная часть настройки — функция attachInterrupt(). Эта специальная функция указывает Arduino внимательно следить за выводом CLK и вызывать функцию updateEncoder() при каждом изменении состояния вывода.

attachInterrupt(digitalPinToInterrupt(CLK), updateEncoder, CHANGE);

Функция attachInterrupt() принимает три параметра: вывод, который мы хотим отслеживать, функцию, которую хотим вызывать при возникновении прерывания, и условие срабатывания. Мы используем условие CHANGE, что означает, что прерывание будет срабатывать при каждом переключении сигнала с HIGH на LOW или с LOW на HIGH.

В предыдущем примере нам приходилось проверять состояние энкодера снова и снова в функции loop(). Теперь весь этот код перенесён в функцию updateEncoder().

void updateEncoder() {
  // Read the current state of CLK
  currentStateCLK = digitalRead(CLK);

  // If last and current state of CLK are different, then pulse occurred
  // React to only 1 state change to avoid double count
  if (currentStateCLK != lastStateCLK && currentStateCLK == 1) {

    // If the DT state is different than the CLK state then
    // the encoder is rotating CCW so decrement
    if (digitalRead(DT) != currentStateCLK) {
      counter--;
      currentDir = "CCW";
    } else {
      // Encoder is rotating CW so increment
      counter++;
      currentDir = "CW";
    }

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

  // Remember last CLK state
  lastStateCLK = currentStateCLK;
}

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

void loop() {
  //Do some useful stuff here
}

Пример кода Arduino 3 — Управление сервоприводом с помощью энкодера

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

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

How Rotary Encoder Works and Interface It with Arduino

Схема подключения

Теперь мы добавляем сервопривод к нашей конфигурации. Вам нужно подключить красный провод сервопривода к выводу 5V Arduino, чёрный или коричневый провод к GND, а оранжевый или жёлтый провод (сигнал управления) к цифровому выводу 9 (вывод с поддержкой PWM).

How Rotary Encoder Works and Interface It with Arduino

Код Arduino

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

// Include the Servo Library
#include <Servo.h>

// Rotary Encoder Inputs
#define CLK 2
#define DT 3

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

void setup() {
  // Set encoder pins as inputs
  pinMode(CLK, INPUT);
  pinMode(DT, INPUT);

  // Setup Serial Monitor
  Serial.begin(9600);

  // Attach servo on pin 9 to the servo object
  servo.attach(9);
  servo.write(counter);

  // Read the initial state of CLK
  lastStateCLK = digitalRead(CLK);

  // Call updateEncoder() when a change is seen on CLK pin
  attachInterrupt(digitalPinToInterrupt(CLK), updateEncoder, CHANGE);
}

void loop() {
  //Do some useful stuff here
}

void updateEncoder() {
  // Read the current state of CLK
  currentStateCLK = digitalRead(CLK);

  // If last and current state of CLK are different, then pulse occurred
  // React to only 1 state change to avoid double count
  if (currentStateCLK != lastStateCLK && currentStateCLK == 1) {

    // If the DT state is different than the CLK state then
    // the encoder is rotating CCW so decrement
    if (digitalRead(DT) != currentStateCLK) {
      counter--;
      if (counter < 0)
        counter = 0;
    } else {
      // Encoder is rotating CW so increment
      counter++;
      if (counter > 179)
        counter = 179;
    }
    // Move the servo
    servo.write(counter);
    Serial.print("Position: ");
    Serial.println(counter);
  }

  // Remember last CLK state
  lastStateCLK = currentStateCLK;
}

Объяснение кода

Если вы посмотрите на код, то увидите, что он очень похож на предыдущий скетч с прерываниями. Однако мы добавили несколько важных изменений для работы с сервоприводом.

Первое важное изменение — в начале скетча, где мы подключаем библиотеку Servo. Эта библиотека даёт нам инструменты для простого управления сервоприводами с Arduino. После этого мы создаём объект servo для представления нашего сервопривода.

#include <Servo.h>

Servo servo;

В функции setup() мы используем функцию attach() для подключения объекта servo к выводу 9, где подключён управляющий провод сервопривода. Мы также указываем сервоприводу переместиться в начальную позицию с помощью функции write(), передавая текущее значение счётчика, начинающееся с 0.

servo.attach(9);
servo.write(counter);

Внутри функции updateEncoder() мы добавили новый код для управления сервоприводом при вращении энкодера. Как и прежде, мы проверяем изменения на выводе CLK и, если изменение обнаружено, определяем направление вращения, сравнивая вывод DT с выводом CLK. Если энкодер вращается против часовой стрелки, мы уменьшаем счётчик. Если по часовой стрелке — увеличиваем.

Но вот новая часть: мы добавили ограничения для счётчика. Это связано с тем, что сервоприводы принимают только значения положения от 0 до 179 градусов. Если счётчик опускается ниже 0, мы сбрасываем его в 0. Если превышает 179, ограничиваем значением 179. Это гарантирует, что мы отправляем сервоприводу только допустимые значения.

if (digitalRead(DT) != currentStateCLK) {
  counter--;
  if (counter < 0)
    counter = 0;
} else {
  counter++;
  if (counter > 179)
    counter = 179;
}

Получив правильное значение, мы используем servo.write(counter) для перемещения сервопривода на соответствующий угол. Новая позиция также выводится в монитор последовательного порта, чтобы вы видели, куда именно направлен сервопривод при каждом повороте энкодера.

servo.write(counter);
Serial.print("Position: ");
Serial.println(counter);

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

void loop() {
  //Do some useful stuff here
}