Инкрементальные энкодеры подключить параллельно. Учебный курс. Подключение энкодера. Типовая структура простой программы. Генератор на AVR. Принципиальная схема подключения энкодера к преобразователю частоты

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

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

Прежде всего энкодеры бывают нескольких типов, рассматриваемый в данной статье — механический инкрементальный. В качестве испытуемого, был использован pec12-4220f-s0024. Внешне он похож на переменный резистор, но, в отличие от резистора, он не имеет ограничителей, т.е. может крутиться бесконечно в любую сторону.

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

Теперь рассмотрим все более детально. Электрически он представляет собой 2 кнопки без фиксации, когда мы начинаем крутить они по очереди срабатывают — сначала одна, затем вторая. В зависимости от того, в какую сторону мы вращаем, одна из кнопок срабатывает раньше или позднее. Для того чтобы узнать, в каком состоянии находятся эти кнопки, ножки порта (к которому подсоединен энкодер) должны быть подтянуты к «+» питания.

На разобранном энкодере 1/3 площадки относится к 1 контакту, 1/3 к 2 контакту, сплошной участок — общий. Когда скользящие контакты попадают на изолированные участки (черные), слышны щелчки. В этот момент энкодер, находится в устойчивом состоянии, когда обе кнопки разомкнуты. На ножках порта будут лог единицы(состояние 11).

Как только мы начинаем вращать в какую либо сторону, один из контактов замыкается на землю. На этой ножеке появится лог 0, на второй ножке по прежнему будет лог1 (состояние 01). Если мы продолжаем вращать, на второй ножке появится лог0(состояние 00). Далее, на первой ножке пропадает контакт (состояние 10), в конце концов энкодер возвращается в устойчивое состояние (11). Т.е. на один щелчок приходится 4 изменения состояния. Временная диаграмма выглядит так:

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

Если выписать эти состояния в двоичной системе и перевести их в десятичную, то получится следующий порядок(для вращения в одну сторону):
11=3
01=1
00=0
10=2

При вращении в противоположную сторону:
11=3
10=2
00=0
01=1

Теперь осталось понять, как эти значение обрабатывать. Допустим, энкодер подключен к ножкам порта В0 и В1. Нам нужно прочитать эти ножки. Есть довольно хитрый способ, но для начала нам нужно понять операцию «логического и» (&).

Результат будет равен единице, только если оба числа равны 1, т.е. результат операции 1&1, будет равен 1. Следовательно 1&0=0, 0&0=0, 0&1=0.

Логическое & поможет нам вычленить из целого порта, только интересующие нас ножки. Т.е. операция x=0b00000011 & PINB; позволит нам прочитать в переменную «х» состояние первых двух ножек, независимо от того, что находится на остальных ножках. Число 0b00000011 можно перевести в шестнадцатеричную систему 0х3.

Теперь все необходимые знания для написания прошивки у нас есть. Задача: увеличивать/уменьшать переменную Vol, при помощи энкодера, результат вывести на lcd дисплей.

#include int NewState, OldState, Vol, upState, downState; #asm .equ __lcd_port= 0x12 ; PORTD #endasm #include #include interrupt [ TIM1_COMPA] void timer1_compa_isr(void ) { NewState= PINB & 0b00000011 ; if (NewState!= OldState) { switch (OldState) { case 2 : { if (NewState == 3 ) upState++; if (NewState == 0 ) downState++; break ; } case 0 : { if (NewState == 2 ) upState++; if (NewState == 1 ) downState++; break ; } case 1 : { if (NewState == 0 ) upState++; if (NewState == 3 ) downState++; break ; } case 3 : { if (NewState == 1 ) upState++; if (NewState == 2 ) downState++; break ; } } OldState= NewState; } TCNT1H= 0x00 ; TCNT1L= 0x00 ; } void main(void ) { char lcd_buf[ 17 ] ; // Input/Output Ports initialization // Port B initialization // Func7=In Func6=In Func5=In Func4=In Func3=In Func2=In Func1=In Func0=In // State7=T State6=T State5=T State4=T State3=T State2=T State1=P State0=P PORTB= 0x03 ; DDRB= 0x00 ; // Timer/Counter 1 initialization // Clock source: System Clock // Clock value: 1000,000 kHz // Mode: CTC top=OCR1A // OC1A output: Discon. // OC1B output: Discon. // Noise Canceler: Off // Input Capture on Falling Edge // Timer 1 Overflow Interrupt: Off // Input Capture Interrupt: Off // Compare A Match Interrupt: On // Compare B Match Interrupt: Off TCCR1A= 0x00 ; TCCR1B= 0x0A ; TCNT1H= 0x00 ; TCNT1L= 0x00 ; ICR1H= 0x00 ; ICR1L= 0x00 ; OCR1AH= 0x03 ; OCR1AL= 0xE8 ; OCR1BH= 0x00 ; OCR1BL= 0x00 ; // Timer(s)/Counter(s) Interrupt(s) initialization TIMSK= 0x10 ; // Global enable interrupts #asm("sei") lcd_init(8 ) ; while (1 ) { if (upState >= 4 ) { Vol++; upState = 0 ; } if (downState >= 4 ) { Vol--; downState = 0 ; } sprintf (lcd_buf, "vol=%d" , Vol) ; lcd_gotoxy(0 , 0 ) ; lcd_clear() ; lcd_puts(lcd_buf) ; } ; }

#include int NewState,OldState,Vol,upState,downState; #asm .equ __lcd_port=0x12 ;PORTD #endasm #include #include interrupt void timer1_compa_isr(void) { NewState=PINB & 0b00000011; if(NewState!=OldState) { switch(OldState) { case 2: { if(NewState == 3) upState++; if(NewState == 0) downState++; break; } case 0: { if(NewState == 2) upState++; if(NewState == 1) downState++; break; } case 1: { if(NewState == 0) upState++; if(NewState == 3) downState++; break; } case 3: { if(NewState == 1) upState++; if(NewState == 2) downState++; break; } } OldState=NewState; } TCNT1H=0x00; TCNT1L=0x00; } void main(void) { char lcd_buf; // Input/Output Ports initialization // Port B initialization // Func7=In Func6=In Func5=In Func4=In Func3=In Func2=In Func1=In Func0=In // State7=T State6=T State5=T State4=T State3=T State2=T State1=P State0=P PORTB=0x03; DDRB=0x00; // Timer/Counter 1 initialization // Clock source: System Clock // Clock value: 1000,000 kHz // Mode: CTC top=OCR1A // OC1A output: Discon. // OC1B output: Discon. // Noise Canceler: Off // Input Capture on Falling Edge // Timer 1 Overflow Interrupt: Off // Input Capture Interrupt: Off // Compare A Match Interrupt: On // Compare B Match Interrupt: Off TCCR1A=0x00; TCCR1B=0x0A; TCNT1H=0x00; TCNT1L=0x00; ICR1H=0x00; ICR1L=0x00; OCR1AH=0x03; OCR1AL=0xE8; OCR1BH=0x00; OCR1BL=0x00; // Timer(s)/Counter(s) Interrupt(s) initialization TIMSK=0x10; // Global enable interrupts #asm("sei") lcd_init(8); while (1) { if (upState >= 4) { Vol++; upState = 0; } if (downState >= 4) { Vol--; downState = 0; } sprintf(lcd_buf,"vol=%d",Vol); lcd_gotoxy(0,0); lcd_clear(); lcd_puts(lcd_buf); }; }

В качестве пояснений: таймер 1 настроен на срабатывание 1000 раз в секунду, строкой NewState=PINB & 0b00000011; считываем состояние ножек 0 и 1 портаВ. if(NewState!=OldState) если состояние не изменилось, значит вращения нет.
Если состояние изменилось конструкция switch определяет в какую сторону было произведено вращение, в зависимости от этого увеличивается значение переменной downState(влево) или upState(вправо).

От щелчка до следующего щелчка 4 изменения состояния, поэтому 1 раз за 4 импульса изменяем переменную Vol. Ее же и выводим на дисплей. Прошивка доступна

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В 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, включающий основной цикл приведен ниже:

#include #include #include 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; } } }

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

Модуль энкодер KY-040 является механическим датчиком угла поворота, он преобразует угол поворота вращающегося объекта(например вала) в электрические сигналы сдвинутые на 90 градусов относительно друг друга. Данный модуль имеет три вывода - CLK, DT и SW. Сигналы, сдвинутые на 90 градусов относительно друг друга появляются именно на выводах CLK и DT при вращении по/против часовой стрелки, вывод SW используется для получения состояния центральной оси энкодера, которая работает как кнопка.

Итак, не вдаваясь в подробности внутрисхемного устройства энкодера(об этом будет отдельная статья), произведем его подключение к плате Arduino Uno. Схема подключения модуля энкодера в связке с многоразрядным семисегментным индикатором:

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

Из диаграммы видно что, каждый раз когда сигнал А(контакт CLK энкодера) переходит от высокого уровня к низкому, считывается состояние сигнала Б(контакт DT энкодера). Если сигнал Б даёт высокий уровень сигнала, это означает что вращение энкодера происходит по часовой стрелке. Если сигнал Б даёт низкий уровень сигнала при переходе сигнала А из низкого в высокий уровень, то это означает что вращение энкодера происходит против часовой стрелки. Считывая оба этих сигнала в программе, можно определить направление вращения, также при подсчете импульсов сигнала Б, можно инкрементировать либо декрементировать программный счетчик импульсов.

Чтобы считывать сигналы А и Б, а также сигналы центральной оси энкодера(напомню - она работает как кнопка), можно применить нижеописанную конструкцию. Эту конструкцию можно встраивать в скетч, добавляя в неё различный функционал, всё ограничивается только желанием и фантазией разработчика. Тело конструкции достаточно хорошо прокомментировано, в дальнейшем для простоты текста скетча можно убрать комментарии. Итак, конструкция для считывания и преобразования сигналов энкодера в полезные данные:

//Временные переменные для хранения уровней сигналов //полученных от энкодера unsigned char encoder_A, encoder_B, encoder_A_prev; //Переменная для отслеживания нажатий кнопки - //центральной оси энкодера static bool SW_State = false; void setup() { //Предварительные установки //Объявления переменных //Инициализации портов и т.д. } void loop() { //CLK подключаем к пину 3 на плате Arduino //DT подключаем к пину 4 на плате Arduino //Считываем значения выходов энкодера //И сохраняем их в переменных encoder_A = digitalRead(3); encoder_B = digitalRead(4); //Если уровень сигнала А низкий, //и в предидущем цикле он был высокий if(!encoder_A && encoder_A_prev) { //Если уровень сигнала В высокий if(encoder_B) { //Значит вращение происходит по часовой стрелке //Здесь можно вставить операцию инкремента //Здесь можно вставлять какие либо свои //операции по обработке данных в нужном направлении } //Если уровень сигнала В низкий else { //Значит вращение происходит против часовой стрелки //Здесь можно вставить операцию декремента //Здесь можно вставлять какие либо свои //операции по обработке данных в нужном направлении } } //Обязательно нужно сохранить состояние текущего уровня сигнала А //для использования этого значения в следующем цикле сканирования программы encoder_A_prev = encoder_A; //Работаем с центральной осью энкодера - кнопкой //Этот кусок кода образует собой как бы перекидной триггер //Считываем значение пина 2 на плате Arduino //к которому подключен контакт SW энкодера //Если центральная ось нажата - то сигнал SW будет иметь низкий уровень if(!digitalRead(2)) { //Если переменная SW_State установлена в false то установить её в true if(!SW_State) { //И запомнить состояние SW_State = true; } //И наоборот - если переменная SW_State установлена в true, //то сбросить её в false else { //И запомнить состояние SW_State = false; } } }

Скетч, для обработки сигналов энкодера и выводу значения счетчика на дисплей, показан ниже. В этот скетч встроена конструкция для считывания и преобразования сигналов энкодера, которая была описана выше.

#include "LedControl.h" /* * Подключаем библиотеку LedControl.h * и создаём объект класса LedControl * при этом, 7-ми сегметный дисплей с драйвером MAX72xx * должен быть подключен к плате Arduino следующим образом: * Arduino -> Display Module MAX72xx * Arduino -> Display Module MAX72xx * Arduino -> Display Module MAX72xx * Arduino -> Display Module MAX72xx * Arduino -> Display Module MAX72xx * Arduino -> Encoder Module * Arduino -> Encoder Module * Arduino -> Encoder Module */ LedControl lc = LedControl(12, 11, 10, 1); //Проименовываем адреса портов на плате Arduino const int Dir = 4; const int Step = 3; const int Switch = 2; //Counter - переменная для хранения значения счетчика static long Counter = 0; //SW_State - флаг триггер отслеживания нажатия центральной оси static bool SW_State = false; //Временные переменные для хранения уровней сигналов энкодера unsigned char encoder_A, encoder_B, encoder_A_prev; void setup() { //Устройство(7-ми сегментный дисплей) выводим из спящего режима lc.shutdown(0, false); //Установить яркость дисплея на 8 //Всего возможных режимов яркости от 0 до 15 lc.setIntensity(0,8); //Очистить дисплей lc.clearDisplay(0); //Конфигурируем порты для энкодера pinMode(Dir, INPUT); pinMode(Step, INPUT); pinMode(Switch, INPUT); } void loop() { //Считываем значения выходов энкодера //И сохраняем их в переменных encoder_A = digitalRead(Step); encoder_B = digitalRead(Dir); //Если уровень сигнала А низкий, //и в предидущем цикле он был высокий if(!encoder_A && encoder_A_prev) { //Если уровень сигнала В высокий if(encoder_B) { //Значит вращение происходит по часовой стрелке //Наше условие: //Если значение счетчика больше или равно максимальному числу if(Counter >= 99999999) { //Обнулить значение счетчика Counter = 0; } else { //Иначе инкрементировать при каждом щелчке на единицу Counter ++; } } //Если уровень сигнала В низкий else { //Значит вращение происходит против часовой стрелки //Если значение счетчика меньше или равно нулю if(Counter <= 0) { //проинициализировать значение максимальным числом Counter = 99999999; } else { //Иначе декрементировать при каждом щелчке на единицу Counter --; } } } //Обязательно нужно сохранить состояние текущего уровня сигнала А //для использования этого значения в следующем цикле сканирования программы encoder_A_prev = encoder_A; //Работаем с центральной осью энкодера - кнопкой //Этот кусок кода образует собой как бы перекидной триггер //Считываем значение пина 2 на плате Arduino //которомый проименован как Switch //Если центральная ось нажата - то сигнал Switch будет иметь низкий уровень if(!digitalRead(Switch)) { //Если переменная SW_State установлена в false то установить её в true if(!SW_State) { //И запомнить состояние SW_State = true; } //И наоборот - если переменная SW_State установлена в true, //то сбросить её в false else { //И запомнить состояние SW_State = false; } } //Часть программы которая заполняет разряды //семисегментного дисплея значением счетчика long intCounter = Counter; int divCounter; for(int i = 0; i < 8; i ++) { divCounter = intCounter % 10; intCounter = intCounter / 10; if(intCounter == 0 && SW_State) { if(divCounter == 0) { if(i == 0) { lc.setChar(0, 0, "0", false); } else { lc.setChar(0, i, " ", false); } } else { lc.setDigit(0, i, divCounter, false); } } else { lc.setDigit(0, i, divCounter, false); } } }

Видео, как это работает:


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

  • Использование промышленных оптических энкодеров в Arduino
Пожалуйста, включите javascript для работы комментариев.

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

Совсем коротко, о чем идет речь, т.е. о классификации энкодеров.

Энкодеры это цифровые датчики угла поворота. Другими словами преобразователи угол-код.

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

Энкодеры бывают абсолютные и накапливающие (инкрементальные).

Абсолютные энкодеры формируют на выходе код, соответствующий текущему углу положения вала. У них нет памяти. Можно выключить устройство, повернуть вал энкодера, включить и на выходе будет новый код, показывающий новое положение вала. Такие энкодеры сложные, дорогие, часто используют для подключения стандартные цифровые интерфейсы RS-485 и им подобные.

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

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

В подобных случаях инкрементальные энкодеры становятся идеальными устройствами управления, установки параметров, выбора меню. Они намного удобнее, чем кнопки ”+” и ”-”.

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

Энкодер это всего лишь цифровой датчик угла поворота, не более того.

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

Если с абсолютным энкодером все просто, то с инкрементальным бывают сложности. Как его обрабатывать?

С Энкодера выходят два сигнала А и В, сдвинутых на 90 градусов по фазе, выглядит это так:

В оптическом же может быть два фонаря и два фотодиода, святящие через диск с прорезями (шариковая мышка, ага. Оно самое).

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


Оптический подключается в зависимости от типа оптодатчика, обычно там стоит два с общим анодом.

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

Метод прост:
Подставим нули и единички, в соответствии с уровнем сигнала и запишем последовательность кода:


A:0 0 1 1 0 0 1 1 0 0 1 1 0
B:1 0 0 1 1 0 0 1 1 0 0 1 1

Если A и B идут на одни порт контроллера (например на A=PB0 B=PB1), то при вращении энкодера у нас возникает меняющийся код:

11 = 3
10 = 2
00 = 0
01 = 1
11 = 3

Теперь остается только циклически опрашивать наш энкодер сравнивая текущее состояние с новым и на основании этого делающего выводы о вращении. Причем частота опроса должна быть такой, чтобы не пропустить ни одного импульса. Например, мой EC12 имеет 24 импульса на оборот. Вращать его предпологается вручную и я вряд ли смогу вращать его с космической скоростью, но решил все же замерить. Подключился к осциллографу, крутнул ручку что есть мочи:

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

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 // Эту задачу надо запускать каждую миллисекунду. // EncState глобальная переменная u08 -- предыдущее состояние энкодера // EncData глобальная переменная u16 -- счетный регистр энкодера void EncoderScan(void ) { u08 New; New = PINB & 0x03 ; // Берем текущее значение // И сравниваем со старым // Смотря в какую сторону оно поменялось -- увеличиваем // Или уменьшаем счетный регистр switch (EncState) { case 2 : { if (New == 3 ) EncData++; if (New == 0 ) EncData--; break ; } case 0 : { if (New == 2 ) EncData++; if (New == 1 ) EncData--; break ; } case 1 : { if (New == 0 ) EncData++; if (New == 3 ) EncData--; break ; } case 3 : { if (New == 1 ) EncData++; if (New == 2 ) EncData--; break ; } } EncState = New; // Записываем новое значение // Предыдущего состояния SetTimerTask(EncoderScan, 1 ) ; // Перезапускаем задачу через таймер диспетчера }

// Эту задачу надо запускать каждую миллисекунду. // EncState глобальная переменная u08 -- предыдущее состояние энкодера // EncData глобальная переменная u16 -- счетный регистр энкодера void EncoderScan(void) { u08 New; New = PINB & 0x03; // Берем текущее значение // И сравниваем со старым // Смотря в какую сторону оно поменялось -- увеличиваем // Или уменьшаем счетный регистр switch(EncState) { case 2: { if(New == 3) EncData++; if(New == 0) EncData--; break; } case 0: { if(New == 2) EncData++; if(New == 1) EncData--; break; } case 1: { if(New == 0) EncData++; if(New == 3) EncData--; break; } case 3: { if(New == 1) EncData++; if(New == 2) EncData--; break; } } EncState = New; // Записываем новое значение // Предыдущего состояния SetTimerTask(EncoderScan,1); // Перезапускаем задачу через таймер диспетчера }

Почему я под счетчик завел такую большую переменную? Целых два байта? Да все дело в том, что у моего энкодера, кроме импульсов есть еще тактильные щелчки. 24 импульса и 24 щелчка на оборот. А по моей логике, на один импульс приходится четыре смены состояния, т.е. полный период 3201_3201_3201 и один щелчок дает 4ре деления, что некрасиво. Поэтому я считаю до 1024, а потом делю сдвигом на четыре. Получаем на выходе один щелочок — один тик.

Скоростной опрос на прерываниях
Но это механические, с ними можно простым опросом обойтись — частота импульсов позволяет. А бывают еще и высокоскоростные энкодеры. Дающие несколько тысяч импульсов на оборот, либо работающие на приводах и вращающиеся очень быстро. Что с ними делать?

Ускорять опрос занятие тупиковое. Но нас спасает то, что у таких энкодеров, как правило, есть уже свои схемы подавления дребезгов и неопределенностей, так что на выходе у них четкий прямоугольный сигнал (правда и стоят они совершенно негуманно. От 5000р и до нескольких сотен тысяч. А что ты хотел — промышленное оборудование дешевым не бывает).

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


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

А в обработчике прерывания INT0 щупаем вторым выводом канал В. И дальше все элементарно!

Если там высокий уровень — делаем +1, если низкий -1 нашему счетному регистру. Кода на три строчки, мне даже писать его лень.

Конечно, можно такой метод прикрутить и на механический энкодер. Но тут надо будет заблокировать прерывания INT0 на несколько миллисекунд. И НИ В КОЕМ СЛУЧАЕ нельзя делать это в обработчике.

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

  • Зашли в обработчик INT0
  • Пощупали второй канал
  • +1 или -1
  • Запретили локально INT0
  • Поставили на таймер событие разрешающее INT0 через несколько миллисекунд
  • Вышли из обработчика

Сложно? Нет, не сложно. Но зачем? Проще сделать банальный опрос, как указано выше и не зависеть от выводов прерываний. Впрочем, хозяин барин.