Поворотный энкодер — это тип датчика положения, который преобразует угловое положение (вращение) ручки в выходной сигнал, используемый для определения направления вращения ручки.
Благодаря своей надежности и точному цифровому управлению энкодеры используются во многих приложениях, включая робототехнику, станки с ЧПУ и так далее.
Существует два типа поворотных энкодеров – абсолютный и инкрементальный. Абсолютный энкодер дает нам точное значение положения ручки в градусах, в то время как инкрементальный энкодер сообщает, на сколько шагов переместился вал. Сегодня мы поговорим об инкрементальном энкодере для Ардуино.
Поворотные энкодеры против потенциометров
Поворотные энкодеры являются современным цифровым эквивалентом потенциометра и они более универсальны, чем потенциометры. Они могут полностью вращаться без концевых упоров, в то время как потенциометр может вращаться только примерно на 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 выполнять, если прерывание срабатывает, и какой тип триггера следует искать.
Последний скетч заработал только после переноса функции обработки энкодера void updateEncoder в самый верх, перед void setup. Ну и обработка положения энкодера имеет одну особенность: при вращении в сторону ПРОТИВ часовой (CWW) последний шаг иногда обрабатывается как вращение ПО часовой (CW). Это происходит когда крутилка уже пошла на следующий шаг, но вернулась на место.
всем Здравствуйте! помогите кто может деду -это мне. как этот скетч применить для управления ш.д.? что надо добавить. имею станочек хочу чтобы продольная подача повторяла направление вращения шпинделя.
супер спас положение .без наворотов
для энкодера
#define (key)CLK 4
#define S2 3
#define S1 4
видимо это от модели энкодера