Подключение энкодера к Ардуино и полнофункциональный код обработки для него
Энкодер — это устройство преобразования механического перемещения или угловых изменений положения в цифровой сигнал. В статье рассматривается самый популярный в DIY сообществе инкрементальный энкодер EC11 с кнопкой. При его вращении на выходах A и B формируются TTL сигналы в виде импульсов сдвинутые между собой по фазе на 90 градусов. Таким образом с его помощью, можно определить направление и скорость вращения, а так же рассчитать угол поворота. В отличие от потенциометров, энкодер KY-040 гораздо надежней и долговечный.

Немного подробностей
Собирая один из проектов с использованием encoder. Я не смог найти код для Ардуино выполняющий все мои условия. Так как для проекта нужно обрабатывать следующие команды: «Вращение без нажатия», «Вращение с нажатием», «Нажатие» и «Длинное нажатие», а так же требуется стабильная работа энкодера. Скетчи использующие один пин с прерыванием INT0 или INT1, работают отвратительно и при вращении вала энкодера вылетает очень много ошибок. Код без использования прерываний работает стабильно, но он не работает в фоновом режиме, его нужно встраивать в тело основной программы, что в свою очередь приводит к не своевременному срабатыванию обработчика и пропускам при вращении энкодера. Еще хуже обстоят дела с обработкой нажатия с вращением вала энкодера и обычным с нажатием. Пришлось написать свой код обработки, который исключает описанные выше проблемы. С дребезгом контактов я не стал бороться программно, так как это приводит к задержкам обработки. Проще и надежней использовать керамические конденсаторы.
Схема подключения энкодера к Ардуино
Для считывания сигналов с выходов EC-11, нужно использовать три цифровых входа Arduino. В схеме подключения я использовал редко используемые мной в своих проектах выводы Arduino(A1, A2 и A3). Внешние подтягивающие резисторы отсутствуют, так как я использовал внутреннюю подтяжку микроконтроллера. Конденсаторы нужны для гашения импульсов дребезга контактов. Если у вас новый и хороший энкодер, то можно обойтись и без них. Но на кнопку в любом случае потребуется конденсатор, так как ее дребезг неизбежен.
Используемые в схеме компоненты:

Скетч для Ардуино
Для того что бы отслеживать изменение положения энкодера в фоновом режиме, я использую прерывание PCINT1. Обработка всех функций происходит в прерывании, обработчик в зависимости от произошедшего действия изменяет переменную enc_state. Если значение переменной enc_state=0 — ничего не произошло, enc_state=1 — экодер вращался без нажатия, enc_state=2 — экодер вращался с нажатием, enc_state=3 — было нажатие на кнопку, enc_state=4 — было длинное нажатие на кнопку, Прерывание будет срабатывать каждый раз по изменению состояния входов, как с высокого уровня на низкий, так и наоборот. То есть при одном щелчке энкодера прерывание сработает 4 раза. Или по 2 раза для каждого из входов. Но обработчик выдаст сигнал поворота только 1 раз на все 4 прерывания.
Код обработчика при каждом срабатывании записывает в переменную lastcomb состояние входов, к которым подключен энкодер. И ждет состояние когда выходы A и B будут замкнуты на GND, это гарантированный сигнал того, что энкодер вращается. После того как этот сигнал получен, обработчик проверяет в какую сторону было вращение. Для этого он сравнивает его предыдущее значение из переменной lastcomb и в зависимости от фазы сдвига определит в какую сторону был поворот ротора. Как я писал ранее, сложнее всего отслеживать нажатие кнопки.
Так как использовать определенные тайминги я не планировал, потому, что они неизбежно приводят длительным задержкам работы обработчика и основной программы, или требуют использование таймера, которых в микроконтроллере всего 3 шт. их, как правило никогда не хватает. Собственно проблема состояла в том, чтобы разделить «нажатие с последующим вращением» от простого нажатия. В итоге как вы уже можете убедиться, я решил эту задачу. Оптимизацией кода я не стал заниматься, потому как все работает и меня все устраивает. Для наглядности в коде все действия с энкодером, отображаются в Serial мониторе программы Adruino IDE.
/* При публичном размещении кода ссылка на первоисточник обязательна. */ #define btn_long_push 1000 // Длительность долинного нажатия кнопки volatile uint8_t lastcomb=7, enc_state, btn_push=0; volatile int enc_rotation=0, btn_enc_rotate=0; volatile boolean btn_press=0; volatile uint32_t timer; //******************************** void setup() < pinMode(A1,INPUT_PULLUP); // ENC-A pinMode(A2,INPUT_PULLUP); // ENC-B pinMode(A3,INPUT_PULLUP); // BUTTON PCICR = 0b00000010; // PCICR |= (1<//**************************************** void loop() < switch (enc_state) < case 1: < Serial.print("Вращение без нажатия "); Serial.println(enc_rotation); >break; case 2: < Serial.print("Вращение с нажатием "); Serial.println(btn_enc_rotate); >break; case 3: Serial.println("Нажатие кнопки "); break; case 4: Serial.println("Длинное нажатие кнопки "); break; > enc_state=0; //обнуляем статус энкодера > //**************************************** ISR (PCINT1_vect) //Обработчик прерывания от пинов A1, A2, A3 < uint8_t comb = bitRead(PINC, 3) if (comb == 0) //Если было промежуточное положение энкодера и нажатие, то проверяем его предыдущее состояние < if (lastcomb == 1) --btn_enc_rotate; //вращение по часовой стрелке if (lastcomb == 2) ++btn_enc_rotate; //вращение против частовой enc_state=2; // был поворот энкодера с нажатием enc_rotation=0; //обнулить показания вращения без нажатия btn_press=0; //обнулить показания кнопки >if (comb == 7 && lastcomb == 3 && btn_press) //Если было отпускание кнопки, то проверяем ее предыдущее состояние < if (millis() - timer >btn_long_push) // проверяем сколько прошло миллисекунд < enc_state=4; // было длинное нажатие >else < enc_state=3; // было нажатие >btn_press=0; //обнулить статус кнопки > timer = millis(); //сброс таймера lastcomb = comb; //сохраняем текущее состояние энкодера >
Заключение
Результат работы кода меня порадовал и теперь я могу продолжить работу над своим новым проектом, который скоро здесь выложу. Надеюсь эта короткая статья вам понравилась и вы сможете воспользоваться моей наработкой в своих самоделках.
Если у Вас остались вопросы и замечания, пишите их в комментариях. Я с удовольствием на них отвечу.
Подключение энкодера к Arduino. GyverEncoder v4.9

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

Самым хорошим модулем с энкодером на Aliexpress является вот такой, на круглой плате:

ПОДКЛЮЧЕНИЕ
Подключается модуль энкодера очень просто: питание на питание (GND и VCC), логические пины CLK, DT (тактовые выводы энкодера) и SW (вывод кнопки) на любые пины Arduino (D или A). У круглых модулей выводы энкодера подписаны как S1 и S2, а вывод кнопки как Key, подключаются точно так же. От порядка подключения тактовых выводов энкодера зависит “направление” его работы, но это можно поправить в программе.
У модулей энкодера тактовые выводы подтянуты к питанию и дают низкий сигнал при срабатывании, также на них стоят RC цепи для гашения дребезга. Вывод кнопки никуда не подтянут! Промышленный энкодер подключается точно так же, чёрный и красный провода у него питание, остальные – тактовые выходы.
У модулей энкодеров тактовые выходы и кнопка подтянуты к питанию, у круглого модуля также стоят RC цепи для аппаратного подавления дребезга контактов, у KY-40 (прямоугольный) распаяна только подтяжка. Если нужно подключить “голый” энкодер к плате – в целом можно подключить напрямую без обвязки, как на схеме ниже, моя библиотека отработает и подтяжку средствами микроконтроллера (INPUT_PULLUP), и программный антидребезг. Но рекомендуется всё-таки делать RC цепи для кнопки и для тактовых выходов энкодера.

Подключаем модуль

Схема KY-40

Энкодер без обвязки

Схема круглого модуля

RC цепь на выводы энка
Бывает два типа энкодеров, я назвал их одноимпульсные и двухимпульсные, тип энкодера можно определить по внешнему виду самого энкодера:

Чем отличаются энкодеры на практике: если опрашивать одноимпульсный энкодер как двухимпульсный, то для отработки одного тика нужно повернуть рукоятку на два тика. Если опрашивать двухимпульсный как одноимпульсный, то для отработки одного тика нужно повернуть рукоятку на два тика. То есть при неправильном использовании причина сразу видна.
КОД
Если вам важна не функциональность библиотеки, а максимальная скорость работы с энкодером и минимальное время его опроса, предлагаю следующий код. Здесь используется аппаратное прерывание (одно на один энкодер), для увеличения скорости выполнения прерывания используется функция bitRead вместо digitalRead (скорость выполнения разнится в десятки раз). Также в этом коде предусмотрен выбор типа энкодера соответствующей настройкой. Отработка энкодера идёт параллельно выполнению скетча, переменная encCounter меняет своё значение при повороте рукоятки и выводится через порт.
/* Максимально быстрый универсальный код для обработки энкодера Работает на перывании (используется одно) Тут код построен на bitRead(PIND..) — только для Arduino NANO! */ #define ENC_A 2 // пин энкодера #define ENC_B 4 // пин энкодера #define ENC_TYPE 1 // тип энкодера, 0 или 1 volatile int encCounter; volatile boolean state0, lastState, turnFlag; void setup() < Serial.begin(9600); attachInterrupt(0, int0, CHANGE); >void int0() < state0 = bitRead(PIND, ENC_A); if (state0 != lastState) < #if (ENC_TYPE == 1) turnFlag = !turnFlag; if (turnFlag) encCounter += (bitRead(PIND, ENC_B) != lastState) ? -1 : 1; #else encCounter += (bitRead(PIND, ENC_B) != lastState) ? -1 : 1; #endif lastState = state0; >> void loop()
/* Максимально быстрый универсальный код для обработки энкодера Работает на перывании (используется одно) Тут код построен на digitalRead, что делает его универсальным для всех плат Arduino */ #define ENC_A 2 // пин энкодера #define ENC_B 4 // пин энкодера #define ENC_TYPE 1 // тип энкодера, 0 или 1 volatile int encCounter; volatile boolean state0, lastState, turnFlag; void setup() < Serial.begin(9600); attachInterrupt(0, int0, CHANGE); >void int0() < state0 = digitalRead(ENC_A); if (state0 != lastState) < #if (ENC_TYPE == 1) turnFlag = !turnFlag; if (turnFlag) encCounter += (digitalRead(ENC_B) != lastState) ? -1 : 1; #else encCounter += (digitalRead(ENC_B) != lastState) ? -1 : 1; #endif lastState = state0; >> void loop()
// алгоритм с "таблицей", позволяющий увеличить точность энкодера // в 4 раза, работает максимально чётко даже с плохими энкодерами. // Для увеличения скорости опроса используйте PCINT и чтение из PINn #define CLK 3 #define DT 2 long pos = 0; byte lastState = 0; const int8_t increment[16] = ; void setup() < Serial.begin(9600); >void loop() < byte state = digitalRead(CLK) | (digitalRead(DT) >
#define ENC_A 2 // пин энкодера #define ENC_B 3 // пин энкодера volatile int encCounter; volatile boolean flag, resetFlag; volatile byte curState, prevState; void setup() < Serial.begin(115200); attachInterrupt(0, int0, CHANGE); attachInterrupt(1, int0, CHANGE); >void int0() < encTick(); >// алгоритм со сбросом от Ярослава Куруса void encTick() < curState = digitalRead(ENC_A) | digitalRead(ENC_B) if (curState == 0b00) resetFlag = 1; prevState = curState; > void loop() < if (flag) < Serial.println(encCounter); flag = 0; >>
volatile int counter = 0; // счётчик volatile bool encFlag = 0; // флаг поворота void setup() < Serial.begin(9600); attachInterrupt(0, encIsr, CHANGE); attachInterrupt(1, encIsr, CHANGE); >void loop() < if (encFlag) < Serial.println(counter); encFlag = 0; >> volatile byte reset = 0, last = 0; void encIsr() < byte state = (PIND & 0b1100) >> 2; // D2 + D3 if (reset && state == 0b11) < int prevCount = counter; if (last == 0b10) counter++; else if (last == 0b01) counter--; if (prevCount != counter) encFlag = 1; reset = 0; >if (!state) reset = 1; last = state; >
БИБЛИОТЕКА
Внимание, библиотека устарела! Я разработал новую библиотеку — EncButton, она гораздо легче и быстрее этой, но практически полностью с ней совместима. Скачать и ознакомиться можно на GitHub. × Закрыть это предупреждение.
GyverEncoder v4.9
Я не нашёл в интернете нормальных библиотек для энкодера с хорошей функциональностью, поэтому написал свою, GyverEncoder. Что умеет:
- Отработка поворота рукоятки энкодера
- Обычный поворот
- “Нажатый поворот”
- “Быстрый” поворот
- Быстрый – но не справляется с люфтами
- Бинарный – медленнее, лучше справляется с люфтами
- Высокоточный – ещё медленнее, но работает даже с убитым энкодером
- Отработка нажатия
- Клика
- Двойного клика
- Удержания
- Антидребезг контактов
- Возможность полностью убрать код кнопки для быстродействия
Поддерживаемые платформы: все Arduino (используются стандартные Wiring-функции)
УСТАНОВКА
- Библиотеку можно найти и установить через менеджер библиотек по названию GyverEncoder в:
- Arduino IDE (Инструменты/Управлять библиотеками)
- Arduino IDE v2 (вкладка “Library Manager”)
- PlatformIO (PIO Home, вкладка “Libraries”)
ДОКУМЕНТАЦИЯ
Документация
Инициализация
Объект энкодера может быть создан несколькими способами:
Encoder enc; // не привязан к пину (для виртуального энкодера, см. пример) Encoder enc(пин CLK, пин DT); // энкодер без кнопки (ускоренный опрос) Encoder enc(пин CLK, пин DT, пин SW); // энкодер с кнопкой Encoder enc(пин CLK, пин DT, пин SW, тип); // энкодер с кнопкой и указанием типа Encoder enc(пин CLK, пин DT, ENC_NO_BUTTON, тип); // энкодер без кнопки и с указанием типа
Опрос
- В loop() — у вас должен быть «прозрачный» loop() без задержек
- В прерывании таймера — достаточно опрашивать энкодер каждые 5 мс (зависит от скорости поворота)
- В аппаратном прерывании (достаточно завести одну таковую ногу энкодера)
- isTurn(); // возвращает true при любом повороте, сама сбрасывается в false
- isRight(); // возвращает true при повороте направо, сама сбрасывается в false
- isLeft(); // возвращает true при повороте налево, сама сбрасывается в false
- isRightH(); // возвращает true при удержании кнопки и повороте направо, сама сбрасывается в false
- isLeftH(); // возвращает true при удержании кнопки и повороте налево, сама сбрасывается в false
- isFastR(); // возвращает true при быстром повороте
- isFastL(); // возвращает true при быстром повороте
- isPress(); // возвращает true при нажатии кнопки, сама сбрасывается в false
- isRelease(); // возвращает true при отпускании кнопки, сама сбрасывается в false
- isClick(); // возвращает true при нажатии и отпускании кнопки, сама сбрасывается в false
- isHolded(); // возвращает true при удержании кнопки, сама сбрасывается в false
- isHold(); // возвращает true при удержании кнопки, НЕ СБРАСЫВАЕТСЯ
- isSingle(); // возвращает true при одиночном клике (после таймаута), сама сбрасывается в false
- isDouble(); // возвращает true при двойном клике, сама сбрасывается в false
Настройки в скетче
- setType(type); // тип энкодера TYPE1 одношаговый, TYPE2 двухшаговый. Если ваш энкодер работает странно, смените тип
- setTickMode(tickMode); // MANUAL / AUTO — ручной или автоматический опрос энкодера функцией tick() (по умолчанию ручной)
- setDirection(direction); // NORM / REVERSE — направление вращения энкодера
- setFastTimeout(timeout); // установка таймаута быстрого поворота
- setPinMode(mode); // тип подключения пинов энкодера, подтяжка HIGH_PULL (внутренняя) или LOW_PULL (внешняя на GND)
- setBtnPinMode(mode); // тип подключения кнопки, подтяжка HIGH_PULL (внутренняя) или LOW_PULL (внешняя на GND)
Настройки в библиотеке
- ENC_DEBOUNCE_TURN 1 // время антидребезга для энкодера, миллисекунд
- ENC_DEBOUNCE_BUTTON 80 // время антидребезга для кнопки, миллисекунд
- ENC_HOLD_TIMEOUT 700 // таймаут удержания кнопки, миллисекунд
- ENC_DOUBLE_TIMEOUT 300 // таймаут двойного клика
- #define ENC_WITH_BUTTON // если закомментировать данную строку, опрос кнопки будет полностью «убран» из кода, что сделает его легче и чуть быстрее
- #define DEFAULT_ENC_PULL LOW_PULL // тип подключения энкодера по умолчанию (LOW_PULL или HIGH_PULL)
- #define DEFAULT_BTN_PULL HIGH_PULL // тип подключения кнопки энкодера по умолчанию (LOW_PULL или HIGH_PULL)
Алгоритмы опроса энкодера
- #define FAST_ALGORITHM // быстрый, не справляется с люфтами
- #define BINARY_ALGORITHM // медленнее, лучше справляется с люфтами
- #define PRECISE_ALGORITHM // медленнее, но работает даже с убитым энкодером (по мотивам https://github.com/mathertel/RotaryEncoder)
Работа с «виртуальным» энкодером
Версия библиотеки 4+ поддерживает работу с виртуальным энкодером, т.е. алгоритм опрашивает не напрямую цифровой пин микроконтроллера, а логическую величину, которую ему передадут. Таким образом можно попробовать опрашивать несколько энкодеров, подключенных через расширитель пинов. Для работы с таким энкодером нужно инициализировать энкодер без указания пина:
Encoder enc; // не привязан к пину
Работа с таким энкодером ничем не отличается от обычного, кроме метода tick() — в него нужно передать состояния тактовых пинов энкодера (CLK и DT), а также пина кнопки (опционально):
enc1.tick(stateCLK, stateDT, stateSW); // с кнопкой enc1.tick(stateCLK, stateDT); // без кнопки
Смотрите пример external_enc в папке с примерами
Список функцийEncoder(); // для непривязанного к пинам энкодера Encoder(uint8_t clk, uint8_t dt, int8_t sw = -1, bool type = false); // CLK, DT, SW, тип (TYPE1 / TYPE2) TYPE1 одношаговый, TYPE2 двухшаговый. Если ваш энкодер работает странно, смените тип // Варианты инициализации: // Encoder enc; // не привязан к пину // Encoder enc(пин CLK, пин DT); // энкодер без кнопки (ускоренный опрос) // Encoder enc(пин CLK, пин DT, пин SW); // энкодер с кнопкой // Encoder enc(пин CLK, пин DT, пин SW, тип); // энкодер с кнопкой и указанием типа // Encoder enc(пин CLK, пин DT, ENC_NO_BUTTON, тип); // энкодер без кнопкой и с указанием типа void tick(); // опрос энкодера, нужно вызывать постоянно или в прерывании void setType(boolean type); // TYPE1 / TYPE2 - тип энкодера TYPE1 одношаговый, TYPE2 двухшаговый. Если ваш энкодер работает странно, смените тип void setTickMode(boolean tickMode); // MANUAL / AUTO - ручной или автоматический опрос энкодера функцией tick(). (по умолчанию ручной) void setDirection(boolean direction); // NORM / REVERSE - направление вращения энкодера void setFastTimeout(int timeout); // установка таймаута быстрого поворота void setPinMode(bool mode); // тип подключения энкодера, подтяжка HIGH_PULL (внутренняя) или LOW_PULL (внешняя на GND) void setBtnPinMode(bool mode); // тип подключения кнопки, подтяжка HIGH_PULL (внутренняя) или LOW_PULL (внешняя на GND) boolean isTurn(); // возвращает true при любом повороте, сама сбрасывается в false boolean isRight(); // возвращает true при повороте направо, сама сбрасывается в false boolean isLeft(); // возвращает true при повороте налево, сама сбрасывается в false boolean isRightH(); // возвращает true при удержании кнопки и повороте направо, сама сбрасывается в false boolean isLeftH(); // возвращает true при удержании кнопки и повороте налево, сама сбрасывается в false boolean isFastR(); // возвращает true при быстром повороте boolean isFastL(); // возвращает true при быстром повороте boolean isPress(); // возвращает true при нажатии кнопки, сама сбрасывается в false boolean isRelease(); // возвращает true при отпускании кнопки, сама сбрасывается в false boolean isClick(); // возвращает true при нажатии и отпускании кнопки, сама сбрасывается в false boolean isHolded(); // возвращает true при удержании кнопки, сама сбрасывается в false boolean isHold(); // возвращает true при удержании кнопки, НЕ СБРАСЫВАЕТСЯ boolean isSingle(); // возвращает true при одиночном клике (после таймаута), сама сбрасывается в false boolean isDouble(); // возвращает true при двойном клике, сама сбрасывается в false void resetStates(); // сбрасывает все is-флаги
ПРИМЕРЫ
Остальные примеры смотри в папке examples библиотеки, также примеры можно открыть из Arduino IDE/Файл/Примеры
Все возможности библиотеки
#define CLK 2 #define DT 3 #define SW 4 #include "GyverEncoder.h" //Encoder enc1(CLK, DT); // для работы без кнопки Encoder enc1(CLK, DT, SW); // для работы c кнопкой //Encoder enc1(CLK, DT, SW, TYPE2); // для работы c кнопкой и сразу выбираем тип //Encoder enc1(CLK, DT, ENC_NO_BUTTON, TYPE2); // для работы без кнопки и сразу выбираем тип // Варианты инициализации: // Encoder enc; // не привязан к пину // Encoder enc(пин CLK, пин DT); // энкодер без кнопки (ускоренный опрос) // Encoder enc(пин CLK, пин DT, пин SW); // энкодер с кнопкой // Encoder enc(пин CLK, пин DT, пин SW, тип); // энкодер с кнопкой и указанием типа // Encoder enc(пин CLK, пин DT, ENC_NO_BUTTON, тип); // энкодер без кнопкой и с указанием типа void setup() < Serial.begin(9600); enc1.setType(TYPE2); >void loop() < // обязательная функция отработки. Должна постоянно опрашиваться enc1.tick(); if (enc1.isTurn()) < // если был совершён поворот (индикатор поворота в любую сторону) // ваш код >if (enc1.isRight()) Serial.println("Right"); // если был поворот if (enc1.isLeft()) Serial.println("Left"); if (enc1.isRightH()) Serial.println("Right holded"); // если было удержание + поворот if (enc1.isLeftH()) Serial.println("Left holded"); if (enc1.isPress()) Serial.println("Press"); // нажатие на кнопку (+ дебаунс) if (enc1.isClick()) Serial.println("Click"); // отпускание кнопки (+ дебаунс) //if (enc1.isRelease()) Serial.println("Release"); // то же самое, что isClick if (enc1.isHolded()) Serial.println("Holded"); // если была удержана и энк не поворачивался //if (enc1.isHold()) Serial.println("Hold"); // возвращает состояние кнопки >Меняем величину с разным шагом
#define CLK 7 #define DT 8 #define SW 9 #include "GyverEncoder.h" Encoder enc1(CLK, DT, SW); int value = 0; void setup() < Serial.begin(9600); enc1.setType(TYPE2); // тип энкодера TYPE1 одношаговый, TYPE2 двухшаговый. Если ваш энкодер работает странно, смените тип >void loop() < // обязательная функция отработки. Должна постоянно опрашиваться enc1.tick(); if (enc1.isRight()) value++; // если был поворот направо, увеличиваем на 1 if (enc1.isLeft()) value--; // если был поворот налево, уменьшаем на 1 if (enc1.isRightH()) value += 5; // если было удержание + поворот направо, увеличиваем на 5 if (enc1.isLeftH()) value -= 5; // если было удержание + поворот налево, уменьшаем на 5 if (enc1.isTurn()) < // если был совершён поворот (индикатор поворота в любую сторону) Serial.println(value); // выводим значение при повороте >>
Режим быстрого поворота
#define CLK 2 #define DT 3 #define SW 4 #include "GyverEncoder.h" Encoder enc1(CLK, DT, SW); int value = 0; void setup() < Serial.begin(9600); enc1.setType(TYPE1); // тип энкодера TYPE1 одношаговый, TYPE2 двухшаговый. Если ваш энкодер работает странно, смените тип\= enc1.setFastTimeout(40); // таймаут на скорость isFastR. По умолч. 50 >void loop() < // обязательная функция отработки. Должна постоянно опрашиваться enc1.tick(); if (enc1.isRight()) value++; // если был поворот направо, увеличиваем на 1 if (enc1.isLeft()) value--; // если был поворот налево, уменьшаем на 1 if (enc1.isRightH()) value += 5; // если было удержание + поворот направо, увеличиваем на 5 if (enc1.isLeftH()) value -= 5; // если было удержание + поворот налево, уменьшаем на 5 if (enc1.isFastR()) value += 10; // если был быстрый поворот направо, увеличиваем на 10 if (enc1.isFastL()) value -= 10; // если был быстрый поворот налево, уменьшаем на 10 if (enc1.isTurn()) < // если был совершён поворот (индикатор поворота в любую сторону) Serial.println(value); // выводим значение при повороте >>
Подключаем два энкодера
// два энкодера #include "GyverEncoder.h" Encoder enc1(4, 3, 2); Encoder enc2(7, 6, 5); void setup() < Serial.begin(9600); >void loop() < // обязательная функция отработки. Должна постоянно опрашиваться enc1.tick(); enc2.tick(); if (enc1.isLeft()) Serial.println("enc 1 left"); if (enc1.isRight()) Serial.println("enc 1 right"); if (enc2.isLeft()) Serial.println("enc 2 left"); if (enc2.isRight()) Serial.println("enc 2 right"); >Подключаем два энкодера
// два энкодера #include "GyverEncoder.h" Encoder enc1(4, 3, 2); Encoder enc2(7, 6, 5); void setup() < Serial.begin(9600); >void loop() < // обязательная функция отработки. Должна постоянно опрашиваться enc1.tick(); enc2.tick(); if (enc1.isLeft()) Serial.println("enc 1 left"); if (enc1.isRight()) Serial.println("enc 1 right"); if (enc2.isLeft()) Serial.println("enc 2 left"); if (enc2.isRight()) Serial.println("enc 2 right"); >Настройка энкодера в другой тип
/* В последнее время китайцы стали делать одинаковые модули (ку 40) с разными типами энкодеров - полный период и полпериода. Если ваш энкодер ведёт себя странно (один тик считает за два поворота), то смените тип энкодера */ #define CLK 4 #define DT 3 #define SW 2 #include "GyverEncoder.h" Encoder enc1(CLK, DT, SW); void setup() < Serial.begin(9600); enc1.setType(TYPE2); // тип энкодера TYPE1 одношаговый, TYPE2 двухшаговый. Если ваш энкодер работает странно, смените тип >void loop() < // обязательная функция отработки. Должна постоянно опрашиваться enc1.tick(); if (enc1.isRight()) Serial.println("Right"); // если был поворот if (enc1.isLeft()) Serial.println("Left"); if (enc1.isRightH()) Serial.println("Right holded"); // если было удержание + поворот if (enc1.isLeftH()) Serial.println("Left holded"); >Отработка в авто режиме
#define CLK 6 #define DT 5 #define SW 4 #include "GyverEncoder.h" Encoder enc1(CLK, DT, SW); void setup() < Serial.begin(9600); enc1.setTickMode(AUTO); >void loop() < // enc1.tick(); // не нужна, в этом режиме (AUTO) она входит в каждую функцию! if (enc1.isTurn()) < // если был совершён поворот (индикатор поворота в любую сторону) // ваш код >if (enc1.isRight()) Serial.println("Right"); // если был поворот if (enc1.isLeft()) Serial.println("Left"); if (enc1.isRightH()) Serial.println("Right holded"); // если было удержание + поворот if (enc1.isLeftH()) Serial.println("Left holded"); if (enc1.isPress()) Serial.println("Press"); // нажатие на кнопку (+ дебаунс) if (enc1.isRelease()) Serial.println("Release"); // отпускание кнопки (+ дебаунс) if (enc1.isHolded()) Serial.println("Holded"); // если была удержана и энк не поворачивался //if (enc1.isHold()) Serial.println("Hold"); // возвращает состояние кнопки >Отработка с прерыванием
/* Пример работы с энкодером с прерыванием. Максимальная чёткость работы в любом быдлокоде! */ #define CLK 2 #define DT 3 #define SW 4 #include "GyverEncoder.h" Encoder enc1(CLK, DT, SW); void setup() < Serial.begin(9600); attachInterrupt(0, isr, CHANGE); // прерывание на 2 пине! CLK у энка >void isr() < enc1.tick(); // отработка в прерывании >void loop() < enc1.tick(); // отработка if (enc1.isRight()) Serial.println("Right"); // если был поворот if (enc1.isLeft()) Serial.println("Left"); if (enc1.isRightH()) Serial.println("Right holded"); // если было удержание + поворот if (enc1.isLeftH()) Serial.println("Left holded"); >Отработка с прерыванием по таймеру
/* * Отработка по прерыванию таймера */ #define CLK 7 #define DT 8 #define SW 9 #include "GyverEncoder.h" #include "TimerOne.h" Encoder enc1(CLK, DT, SW); void setup() < Serial.begin(9600); enc1.setType(TYPE2); // тип энкодера TYPE1 одношаговый, TYPE2 двухшаговый. Если ваш энкодер работает странно, смените тип Timer1.initialize(1000); // установка таймера на каждые 1000 микросекунд (= 1 мс) Timer1.attachInterrupt(timerIsr); // запуск таймера >void timerIsr() < // прерывание таймера enc1.tick(); // отработка теперь находится здесь >void loop() < if (enc1.isRight()) Serial.println("Right"); // если был поворот if (enc1.isLeft()) Serial.println("Left"); if (enc1.isRightH()) Serial.println("Right holded"); // если было удержание + поворот if (enc1.isLeftH()) Serial.println("Left holded"); >Как подключить энкодер к Ардуино
Энкодер – это устройство, предназначенное для определения угла поворота и преобразующее данные в электрический сигнал. Другое название энкодера – «датчик угла поворота».
По выходному сигналу различают:
- Инкрементные энкодеры — на выходе формируется две последовательности импульсных сигналов (нулей и единиц), при этом каждая «единица» соответствует фиксированному углу (зависит от точности датчика). Отслеживание полного поворота происходит по специальному сигналу (программному), а направление вращения определяется по смещению второй последовательности относительно первой. Для понимания принципа работы инкрементных датчиков угла лучше всего изучить изображение ниже.

Рис. 1. Изображение выходного сигнала инкрементного энкодера

Рис. 2. Схема инкрементного энкодера
- Абсолютные энкодеры – на выходе формируются не просто «единицы», а сразу последовательности фиксированной длины (зависит от точности измерения), которые явно говорят о конкретном угле, то есть значение угла поворота специальным образом кодируется (код Грея). Понять принцип его работы лучше поможет следующая схема.

Рис. 3. Схема принципа работы абсолютного энкодера

Рис. 4. Схема принципа работы абсолютного энкодера
Конструктивно (в зависимости от технологии измерения) экодеры могут быть:
- Оптическими;
- Магнитными;
- Резисторными.
Подключение и работа с энкодерами в Arduino в первую очередь зависит от способа кодирования угла.
Начнём с инкрементных датчиков.
Подключение инкрементных энкодеров
Чтобы притянуть контакты датчика к логической единице, необходимо использовать резисторы (10кОм). Типовая схема выглядит следующим образом (плата выбрана для примера, входные контакты на Ардуино могут быть изменены на другие).

Рис. 5. Типовая схема
Здесь приведён пример с использованием энкодера с кнопкой (есть и такие модели, кнопкой служит сама ручка).
При использовании Atmega можно просто включить встроенные резисторы микроконтроллера специальной командой.
Если вы сталкиваетесь с дребезгом датчика (довольно частое явление для замыкающихся контактов, а именно они используются в инкрементных энкодерах), можно аппаратно решить проблему путём сборки следующей схемы.

Рис. 6. Типовая схема
Существует и программный способ сглаживания дребезга за счёт использования логики прерываний (смотри скетч во вложениях к материалу). Но подключение прибора следует выполнять только на 2 и 3 пины.
Простейший код для обработки данных с датчика выглядит следующим образом.

Для более сложных задач можно использовать готовые библиотеки, например:
- https://github.com/GreyGnome/AdaEncoder
- https://github.com/PaulStoffregen/Encoder
- https://github.com/mathertel/RotaryEncoder
- и другие.
Во вложении можно найти готовый скетч (см. здесь) с подробными комментариями для работы с меню (часто используемый функционал для энкодеров с кнопкой).
Отрабатывается перемещение по иерархии меню и выбор между доступными пунктами.
Подключение абсолютных энкодеров
Абсолютные энкодеры не так распространены, как инкрементные. И схема подключения во многом зависит от требований производителя. Так, например, высокоточный 128-битный датчик Bourns ACE-128 легко подключается к Ардуино или Raspberry Pi. Производитель предлагает свои собственные библиотеки для работы с энкодером и даже типовую схему для тестирования функционала.
Схема соединения выглядит так.

Рис. 7. Схема соединения энкодеров
А схема для тестирования с выводом числового показателя угла поворота на дисплей, так.

Рис. 8. Схема для тестирования с выводом числового показателя угла поворота на дисплей
Готовая библиотека для Arduino, подробные инструкции и примеры скетчей можно найти на ГитХабе: https://github.com/arielnh56/ACE128
Аналогично и с изделиями других производителей – необходимо изучать их рекомендации.
Подключение энкодера к Ардуино и новый код обработки для него
Энкодер – это устройство преобразования механического перемещения или угловых изменений положения в цифровой сигнал. В статье рассматривается самый популярный в DIY сообществе инкрементальный энкодер EC11 с кнопкой. При его вращении на выходах A и B формируются TTL сигналы в виде импульсов сдвинутые между собой по фазе на 90 градусов. Таким образом с его помощью, можно определить направление и скорость вращения, а так же рассчитать угол поворота. В отличие от потенциометров, KY-040 гораздо надежней и долговечней.

Немного подробностей
Собирая один из проектов с использованием encoder. Я не смог найти код для Ардуино выполняющий все мои условия. Так как для проекта нужно обрабатывать следующие команды: “Вращение без нажатия”, “Вращение с нажатием”, “Нажатие” и “Длинное нажатие”, а так же требуется стабильная работа энкодера. Скетчи использующие один пин с прерыванием INT0 или INT1, работают отвратительно и при вращении вала энкодера вылетает очень много ошибок. Код без использования прерываний работает стабильно, но он не работает в фоновом режиме, его нужно встраивать в тело основной программы, что в свою очередь приводит к не своевременному срабатыванию обработчика и пропускам при вращении энкодера. Еще хуже обстоят дела с обработкой нажатия с вращением вала энкодера и обычным с нажатием. Пришлось написать свой код обработки, который исключает описанные выше проблемы. С дребезгом контактов я не стал бороться программно, так как это приводит к задержкам обработки. Проще и надежней использовать керамические конденсаторы.
Схема подключения энкодера к Ардуино
Для считывания сигналов с выходов EC-11, нужно использовать три цифровых входа Arduino.
В схеме подключения я использовал редко используемые мной в своих проектах выводы Arduino(A1, A2 и A3). Внешние подтягивающие резисторы отсутствуют, так как я использовал внутреннюю подтяжку микроконтроллера. Конденсаторы нужны для гашения импульсов дребезга контактов. Если у вас новый и хороший энкодер, то можно обойтись и без них. Но на кнопку в любом случае потребуется конденсатор, так как ее дребезг неизбежен.
Скетч для Ардуино
Для того что бы отслеживать изменение положения энкодера в фоновом режиме, я использую прерывание PCINT1. Обработка всех функций происходит в прерывании, обработчик в зависимости от произошедшего действия изменяет переменную enc_state. Если значение переменной enc_state=0 – ничего не произошло, enc_state=1 – экодер вращался без нажатия, enc_state=2 – экодер вращался с нажатием, enc_state=3 – было нажатие на кнопку, enc_state=4 – было длинное нажатие на кнопку, Прерывание будет срабатывать каждый раз по изменению состояния входов, как с высокого уровня на низкий, так и наоборот. То есть при одном щелчке энкодера прерывание сработает 4 раза. Или по 2 раза для каждого из входов. Но обработчик выдаст сигнал поворота только 1 раз на все 4 прерывания.
Код обработчика при каждом срабатывании записывает в переменную lastcomb состояние входов, к которым подключен энкодер. И ждет состояние когда выходы A и B будут замкнуты на GND, это гарантированный сигнал того, что энкодер вращается. После того как этот сигнал получен, обработчик проверяет в какую сторону было вращение. Для этого он сравнивает его предыдущее значение из переменной lastcomb и в зависимости от фазы сдвига определит в какую сторону был поворот ротора. Как я писал ранее, сложнее всего отслеживать нажатие кнопки. Так как использовать определенные тайминги я не планировал, потому, что они неизбежно приводят длительным задержкам работы обработчика и основной программы, или требуют использование таймера, которых в микроконтроллере всего 3 шт. их, как правило никогда не хватает. Собственно проблема состояла в том, чтобы разделить “нажатие с последующим вращением” от простого нажатия. В итоге как вы уже можете убедиться, я решил эту задачу. Оптимизацией кода я не стал заниматься, потому как все работает и меня все устраивает. Для наглядности в коде все действия с энкодером отображаются в Serial мониторе программы Adruino IDE./* Код написан при содействии магазина DUINO.RU и является его собственностью. При публичном размещении кода ссылка на первоисточник обязательна. */ #define btn_long_push 1000 // Длительность долинного нажатия кнопки volatile uint8_t lastcomb=7, enc_state, btn_push=0; volatile int enc_rotation=0, btn_enc_rotate=0; volatile boolean btn_press=0; volatile uint32_t timer; //******************************** void setup() < pinMode(A1,INPUT_PULLUP); // ENC-A pinMode(A2,INPUT_PULLUP); // ENC-B pinMode(A3,INPUT_PULLUP); // BUTTON PCICR = 0b00000010; // PCICR |= (1<
//**************************************** void loop() < switch (enc_state) < case 1: < Serial.print("Вращение без нажатия "); Serial.println(enc_rotation); >break; case 2: < Serial.print("Вращение с нажатием "); Serial.println(btn_enc_rotate); >break; case 3: Serial.println("Нажатие кнопки "); break; case 4: Serial.println("Длинное нажатие кнопки "); break; > enc_state=0; //обнуляем статус энкодера > //**************************************** ISR (PCINT1_vect) //Обработчик прерывания от пинов A1, A2, A3 < uint8_t comb = bitRead(PINC, 3) if (comb == 0) //Если было промежуточное положение энкодера и нажатие, то проверяем его предыдущее состояние < if (lastcomb == 1) --btn_enc_rotate; //вращение по часовой стрелке if (lastcomb == 2) ++btn_enc_rotate; //вращение против частовой enc_state=2; // был поворот энкодера с нажатием enc_rotation=0; //обнулить показания вращения без нажатия btn_press=0; //обнулить показания кнопки >if (comb == 7 && lastcomb == 3 && btn_press) //Если было отпусание кнопки, то проверяем ее предыдущее состояние < if (millis() - timer >btn_long_push) // проверяем сколько прошло миллисекунд < enc_state=4; // было длинное нажатие >else < enc_state=3; // было нажатие >btn_press=0; //обнулить статус кнопки > timer = millis(); //сброс таймера lastcomb = comb; //сохраняем текущее состояние энкодера > Заключение
Результат работы кода меня порадовал и теперь я могу продолжить работу над своим новым проектом, который скоро здесь выложу. Надеюсь эта короткая статья вам понравилась и вы сможете воспользоваться моей наработкой в своих самоделках.
Если у Вас остались вопросы и замечания, пишите их в комментариях. Я с удовольствием на них отвечу.2 комментария

Ivan 29 ноября 2023 00:44