Enkoder obrotowy w praktyce – prawidłowe podłączenie do mikrokontrolera AVR

W artykule, który zamieściłem jakiś czas temu (Budujemy cyfrowy zasilacz – enkoder obrotowy w praktyce), przedstawiłem zasadę działania enkoderów obrotowych oraz przedstawiłem sposób ich podłączenia. Nie był to jednak najlepszy przykład, ponieważ był on dość specyficzny – właściwy dla tego, co wówczas robiłem. W niniejszym wpisie chciałbym wrócić do tematu enkoderów i opisać prawidłowy sposób ich podłączania oraz wykorzystywania w aplikacjach bazujących na mikrokontrolerach rodziny AVR (w przykładach będę korzystał z Atmega8A-PU).

Wróćmy do teorii…

Jak już pisałem w poprzednim artykule na temat enkoderów, do którego linkę możecie znaleźć powyżej, enkoder ma dwa wyjścia, nazwijmy je A i B. Podczas gdy obracamy enkoderem, na wyjściach tych pojawia się sygnał prostokątny, przesunięty w fazie względem siebie. Ten sygnał to nic innego jak 2bitowy kod Graya. Na poniższym obrazku jest to przejrzyście zobrazowane.

Jak widzimy na obrazku, jeśli enkoder jest obracany zgodnie z ruchem wskazówek zegara, to sekwencja w kodzie graya wygląda następująco: 2->3->1->0->2 itd. Jeśli obracamy gałką enkodera w przeciwnym kierunku do ruchu wskazówek zegara, wówczas sekwencja wygląda następująco: 3->2->0->1. Znając tę kolejność, wystarczy, że sprawdzimy kolejność sekwencji, i wiemy, czy gałka została poruszona oraz w którym kierunku 😉 Jest to jedna z metod odczytu kierunku enkodera.

Druga metoda polega na wykryciu opadającego zbocza sygnału jednego z pinów enkodera, a następnie sprawdzeniu, jaki stan występuje na drugim pinie. Obrazuje to poniższy rysunek.

Metoda ma jedną zasadniczą wadę. Tracimy połowę precyzji enkodera, ponieważ co drugie zbocze jest wykrywane. Można sobie z tym poradzić, podłączając drugi pin enkodera pod drugie przerwanie, które będzie wyzwalane odwrotnym zboczem do pierwszego. Czyli jeśli pierwsze przerwanie jest wyzwalane opadającym zboczem, to drugie musi być wyzwalane narastającym.

Zyskujemy w ten sposób precyzję, ale „tracimy” dwa zewnętrzne przerwania mikrokontrolera. Zazwyczaj jednak tak duża precyzja nie jest potrzebna i pozostaje się przy rozwiązaniu opartym na jednym przerwaniu.

Podłączenie do mikrokontrolera (Atmega8A-P)

Jeśli stosujemy drogi enkoder optyczny, wówczas to, co piszę w tym paragrafie, nie ma większego znaczenia, ponieważ enkodery te dają bardzo czysty sygnał, którego nie ma potrzeby debouncować itd. Niestety w przypadku tanich enkoderów mechanicznych sprawa ma się inaczej. Nie dość, że styki w ich wnętrzu drgają i wnoszą zakłócenia, to jeszcze musimy pamiętać o nieprzekraczaniu ich parametrów technicznych, które w przypadku tanich enkoderów mogą być naprawdę mizerne. I mam tu na myśli takie parametry jak maksymalna prędkość obrotowa, przy której enkoder daje czysty (w miarę czysty 😉 sygnał, żywotność, która w przypadku tanich enkoderów wynosi 10-20 tys. cykli, co jest dość niewielką ilością dla enkodera, a po której producent nie gwarantuje dalszego prawidłowego działania.

Przy podłączaniu enkodera do mikrokontrolera proponuję nie bawić się w programowy debouncing, który może okazać się dość skomplikowany i zależeć od prędkości obrotowej enkodera, jak i prędkości taktowania zegara procesora. Zamiast tego można wykonać odpowiedni filtr dolnoprzepustowy RC podłączony do obydwóch wyprowadzeń enkodera. Takie podłączenie powinno wyglądać jak na poniższym rysunku.

Na powyższym schemacie rezystor i podłączony do niego równolegle kondensator tworzą filtr dolnoprzepustowy, którego częstotliwość graniczną możemy obliczyć ze wzoru fg = 1 / 2*pi*R*C, co po podstawieniu przedstawionych na schemacie wartości daje nam:

fg = 1 / 2*3.1415*10000*0.0000001 ~= 159.2Hz

Rozwiązanie oparte na przerwaniach

Pierwszym możliwym rozwiązaniem jest rozwiązanie oparte na przerwaniach, w którym to wykrywamy zbocza narastające lub/i opadające, i w zależności od tego, w jakim stanie znajduje się drugi pin enkodera, określamy kierunek.

Najpierw jednak (zakładając, że nasze wyprowadzenia A i B enkodera są podłączone odpowiednio do pinów PD2 i PD3 mikrokontrolera) musimy ustawić PD2 i PD3 jako wejścia:


/* set PD2 and PD3 as input */ 
  DDRD &=~ (1 << PD2);				/* PD2 and PD3 as input */ 
  DDRD &=~ (1 << PD3);        
  PORTD |= (1 << PD3)|(1 << PD2);   /* PD2 and PD3 pull-up enabled   */

a następnie włączyć obsługę przerwań zewnętrznych. W tym celu w funkcji main naszego programu wpisujemy odpowiednie wartości do rejestrów:


GICR |= (1<<INT0)|(1<<INT1);	// enable INT0 and INT1
MCUCR |= (1<<ISC01)|(1<<ISC11)|(1<<ISC10); // INT0 - falling edge, INT1 - raising edge

oraz włączamy obsługę przerwań:


  /* enable interrupts */ 
  sei();

Teraz nie pozostaje nam nic innego jak napisać „procedury” obsługi tych przerwań. Może to wyglądać m/w tak:


//INT0 interrupt 
ISR(INT0_vect ) 
{ 
	if(!bit_is_clear(PIND, PD3)) 
	{ 
		UART_putchar(*PSTR("+")); 
	} 
	else 
	{ 
		UART_putchar(*PSTR("-")); 
	} 
} 

//INT1 interrupt 
ISR(INT1_vect ) 
{ 
	if(!bit_is_clear(PIND, PD2)) 
	{ 
		UART_putchar(*PSTR("+")); 
	} 
	else 
	{ 
		UART_putchar(*PSTR("-")); 
	} 
}

Kod przerwania dla INT0, jak i dla INT1 jest praktycznie identyczny. Po wejściu w przerwanie sprawdzamy, jaki jest aktualny stan na drugim wejściu, i w zależności od tego, czy jest to stan wysoki czy niski, jesteśmy w stanie określić kierunek obrotu. W powyższym przykładzie, jeśli ruch jest zgodny z kierunkiem ruchu wskazówek zegara, jest wysyłany przy pomocy UARTu znak +, w przeciwnym wypadku wysyłany jest znak „-”. Jeśli usuniemy jedno przerwanie, wówczas kod będzie nadal pracował poprawnie, z tym że dokładność zmniejszy się o połowę – czyli co drugi „click” enkodera będzie zauważany przez nasz program. Cały kod programu może wyglądać m/w tak:


#define F_CPU 8000000 
#define UART_BAUD 9600				/* serial transmission speed */ 
#define UART_CONST F_CPU/16/UART_BAUD-1 

#include <stdio.h>
#include <avr/io.h>
#include <util/delay.h> 
#include <avr/pgmspace.h> 
#include <avr/interrupt.h> 

#include "uart.h" 

//INT0 interrupt 
ISR(INT0_vect ) 
{ 
	if(!bit_is_clear(PIND, PD3)) 
	{ 
		UART_putchar(*PSTR("+")); 
	} 
	else 
	{ 
		UART_putchar(*PSTR("-")); 
	} 
} 

//INT1 interrupt 
ISR(INT1_vect ) 
{ 
	if(!bit_is_clear(PIND, PD2)) 
	{ 
		UART_putchar(*PSTR("+")); 
	} 
	else 
	{ 
		UART_putchar(*PSTR("-")); 
	} 
} 

int main(void) 
{ 
  /* init uart */ 
  UART_init(UART_CONST); 

  /* set PD2 and PD3 as input */ 
  DDRD &=~ (1 << PD2);				/* PD2 and PD3 as input */ 
  DDRD &=~ (1 << PD3);        
  PORTD |= (1 << PD3)|(1 << PD2);   /* PD2 and PD3 pull-up enabled   */ 

  GICR |= (1<<INT0)|(1<<INT1);		/* enable INT0 and INT1 */ 
  MCUCR |= (1<<ISC01)|(1<<ISC11)|(1<<ISC10); /* INT0 - falling edge, INT1 - reising edge */ 

  /* enable interrupts */ 
  sei(); 


   while(1) 
   { 
	//do nothing ;) 
	_delay_ms(1); 
   } 

  return 0; 
}

Możecie go ściągnąć z mojego firmowego repozytorium tutaj: https://github.com/leuconoeSH/avr-examples/tree/master/rotary-encoder/interrupt. Są tam dostępne również odpowiednie pliki Makefile oraz kilka prostych procedur do obsługi transmisji szeregowej.

Metoda z wykorzystaniem kodu Graya

Drugą metodą jest wykorzystanie kodu graya. W tym wypadku nie jest konieczne stosowanie przerwań w ogóle.
Zacznijmy od tego, że będziemy potrzebować procedurki, która zmieni dwa stany logiczne na wejściach A i B (piny PD2 i PD3) na wartość binarną w taki sposób, że wartość logiczna z PD2 będzie 1-szym najstarszym bitem, a wartość logiczna z wejścia PD3 będzie 0-owym najstarszym bitem.


uint8_t read_gray_code_from_encoder(void ) 
{ 
 uint8_t val=0; 

  if(!bit_is_clear(PIND, PD2)) 
	val |= (1<<1); 

  if(!bit_is_clear(PIND, PD3)) 
	val |= (1<<0); 

  return val; 
}

W powyższym kodzie mamy zadeklarowaną 8bitową zmienną unsigned int, której początkowo przypisujemy wartość 0. A więc binarnie będzie ona zawierała osiem zer (00000000). Następnie sprawdzamy, czy na wejściu PD2 znajduje się stan wysoki, i jeśli tak jest, przypisujemy na 1szej pozycji (licząc od prawej strony) wartość 1. Mamy wówczas zapisaną w zmiennej val wartość 00000010. Następnie robimy to samo z wejściem PD3, z tym że wartość zapisujemy na pozycji 0-owej. W ten sposób w zmiennej val znajduje się wartość kodu graya wysłanego przez enkoder i może ona wynosić 0 (00), 1 (01), 2(10) lub 3 (11).

Następnie nie pozostaje nam już nic innego jak zapisanie odczytanej wartości jako wartość początkowa, a następnie cykliczne sprawdzanie, czy pojawiła się nowa wartość oraz czy kolejna (nowa) wartość pasuje do ciągu 2->3->1->0 czy do ciągu 3->2->0->1 i tym samym określenie kierunku obroty gałki enkodera.


  /* ready start value */ 
  val = read_gray_code_from_encoder(); 

   while(1) 
   { 
	   val_tmp = read_gray_code_from_encoder(); 

	   if(val != val_tmp) 
	   { 
		   if( /*(val==2 && val_tmp==3) ||*/ 
			   (val==3 && val_tmp==1) || 
			   /*(val==1 && val_tmp==0) ||*/ 
			   (val==0 && val_tmp==2) 
			 ) 
		   { 
				UART_putchar(*PSTR("+")); 
		   } 
		   else if( /*(val==3 && val_tmp==2) ||*/ 
			   (val==2 && val_tmp==0) || 
			   /*(val==0 && val_tmp==1) ||*/ 
			   (val==1 && val_tmp==3) 
			 ) 
		   { 
				UART_putchar(*PSTR("-")); 
		   } 

		   val = val_tmp; 
	   } 

	   _delay_ms(1); 
   }

W powyższym kodzie sekwencje przejścia 2->3, 1->0, 3->2 oraz 0->1 zostały zakomentowane, dlatego że określają one stany przejściowe. Gdybyśmy je odkomentowali, wówczas każdy „click” enkodera zostałby zinterpretowany jako dwa „clicki”.
Jeśli nie chcemy umieszczać naszego kodu obsługi enkodera w głównej pętli programu, możemy uruchomić timer/licznik wraz z jego przerwaniem, które jest wywoływane po jego przepełnieniu i tam umieścić nasz kod. W ten sposób zwolnimy miejsce w głównej pętli programu.
Cały nasz kod może wyglądać m/w tak:


#define F_CPU 8000000						/* crystal f				 */ 
#define UART_BAUD 9600						/* serial transmission speed */ 
#define UART_CONST F_CPU/16/UART_BAUD-1 

#include <stdio.h> 
#include <avr/io.h> 
#include <util/delay.h> 
#include <avr/pgmspace.h> 
#include <avr/interrupt.h> 
#include "uart.h" 

uint8_t read_gray_code_from_encoder(void ) 
{ 
 uint8_t val=0; 

  if(!bit_is_clear(PIND, PD2)) 
	val |= (1<<1); 

  if(!bit_is_clear(PIND, PD3)) 
	val |= (1<<0); 

  return val; 
} 

int main(void) 
{ 
  uint8_t val=0, val_tmp =0; 

  /* init UART */ 
  UART_init(UART_CONST); 

  /* set PD2 and PD3 as input */ 
  DDRD &=~ (1 << PD2);				/* PD2 and PD3 as input */ 
  DDRD &=~ (1 << PD3);        
  PORTD |= (1 << PD3)|(1 << PD2);   /* PD2 and PD3 pull-up enabled   */ 

  /* ready start value */ 
  val = read_gray_code_from_encoder(); 

   while(1) 
   { 
	   val_tmp = read_gray_code_from_encoder(); 

	   if(val != val_tmp) 
	   { 
		   if( /*(val==2 && val_tmp==3) ||*/ 
			   (val==3 && val_tmp==1) || 
			   /*(val==1 && val_tmp==0) ||*/ 
			   (val==0 && val_tmp==2) 
			 ) 
		   { 
				UART_putchar(*PSTR("+")); 
		   } 
		   else if( /*(val==3 && val_tmp==2) ||*/ 
			   (val==2 && val_tmp==0) || 
			   /*(val==0 && val_tmp==1) ||*/ 
			   (val==1 && val_tmp==3) 
			 ) 
		   { 
				UART_putchar(*PSTR("-")); 
		   } 

		   val = val_tmp; 
	   } 

	   _delay_ms(1); 
   } 

  return 0; 
}

Cały ten kod, wraz z plikiem uart.h (oraz uart.c) zwierającym obsługę połączenia szeregowego, możecie znaleźć w moim firmowym repozytorium tutaj: https://github.com/leuconoeSH/avr-examples/tree/master/rotary-encoder/normal.

Podsumowanie

Jak sami widzicie, obsługa enkodera jest banalnie prosta, a największe trudności może (i zapewne będzie) nastręczał sam enkoder oraz jego debouncowanie. Jak już pisałem na wstępie, rozwiązaniem jest stosowanie enkoderów optycznych. Niestety ich ceny zaczynają się od setek złotych. Niemniej jednak, w projektach, które mają posłużyć lata, warto rozważyć zainwestowanie w nie. Są one praktycznie niezniszczalne, jeśli chodzi o trwałość, a ich parametry graniczne, takie jak maksymalna przetwarzana prędkość obrotowa, są o wiele, wiele lepsze od enkoderów mechanicznych.

Nie pozostaje mi już nic innego jak życzyć udanej zabawy przy podłączaniu i korzystaniu z enkoderów w waszych projektach! 🙂

15 odpowiedzi na “Enkoder obrotowy w praktyce – prawidłowe podłączenie do mikrokontrolera AVR”

  1. Magy pisze:

    Hi,I’ve tried v.1.3 Beta with Atmega1280 and I have to say that software doesn’t work peprroly.I’ve read the ATmega1280 on Arduino mega 1280 Board.Start of Bootloader is at 1F000h but eXtreme Burner is reading it into buffer at 10000h.Yes I’ve also add following lines in chips.xml for ATmega 1280:ATmega128013107240960x0003971E256YESYESYESYESYES.\Images\Placements\ZIF_DIP_40.bmpEverything is same as for ATmega2560 but Flash size.Another thing is with loading the same Bootloader from hex file. As I said it sould load to 1F000h but the eXtreme Burner loaded file to 0F000h.Third thing what I’ve noticed is memory size definition for ATmega640 in chips.xml. It should be 65536.Other programmers (Bascom and ProgISP) are reading this ATmega 1280 peprroly also hex file as well.I’ve also checked Arduino board with ATmega328. It reads the ATmega328 peprroly and hex file too. For this I’ve added definitions for hi fuses in fuselayout .xml and put definitions for chip in chips.xml.fuselayout.xml:chips.xml:ATmega328(P)3276810240x0000F951E128YESYESYESYESYES.\Images\Placements\ZIF_DIP_40.bmpBest regards,Jaka

  2. peyman pisze:

    thank you for program.

  3. roman pisze:

    Dzięki, to super jasno wyjaśniony problem

  4. leniwiec pisze:

    @ROMAN fajnie że komuś się przydało 😉

  5. Adrian pisze:

    Przykłady działają i wielkie dzięki że Ci się chciało, ale przy lekko zużytych i kiepskiej jakości enkoderach jest wielki problem żeby to nie wariowało.Nawet mimo filtrów. Właśnie ślęczę nad obsługą kilka godzin i jakoś specjalnie zachwycony nie jestem – ani metoda przerwań ani analizy kodu Graya nie są pozbawione wad. Najczęstszym problemem jest wykonywanie kroku “wstecz” przy obracaniu “naprzód”. Mam kilka enkoderów i prawie zawsze to samo – obracamy enkoderem w jedną stronę a mimo to często program zalicza mniejszościowe “wpadki” jakby to był ruch w drugą stronę. No chyba że ja mam jakieś zmasakrowane enkodery (wszystkie 3 – byłoby t co najmniej dziwne). Acha, gdyby kto pytał: nie pomyliłem wyprowadzeń 🙂 – bo i tak niektórzy błądzą 🙂

  6. leniwiec pisze:

    Adrian zauważ że enkoder na allegro kosztuje 2zł a prawdziwy enkoder stosowany w sprzecie np. audio to 100zł. Pozostaje jedynie ganianie z softem i obsługiwanie tych “złych” zachowan lub zmiana na lepsze enkodery. Przy czym to ganianie moze nigdy nie miec konca, bo enkoder znow sie wyrobi w jakis inny sposob i znow przyjdzie nam to reperowac…

  7. grants pisze:

    Thank you for the nice article. BTW, I have question on “MCUCR |= (1<<ISC01)|(1<<ISC11)|(1<<ISC10);" line. Does the falling edge on external interupt pins should be MCUCR ISC01=1, ISC00=0 (for INT0) and ISCO11=1, ISC10=0 (for INT1)?

  8. Emin Kulturel pisze:

    Thanks so much. It was so helpful..

  9. True pisze:

    Nie wiem czy to komuś pomoże, ale enkodery optyczne były wykorzystywane w starych myszkach komputerowych takich z kulką. Były tam 2 diody podczerwieni i 2 odbiorniki oraz oczywiście tarcza kółka była odpowiednio wykonana.

  10. Adam pisze:

    Czy można użyć tego kodu do przetwornika optycznego np. i nkrementalny MHK40

  11. leniwiec pisze:

    @ADAM można, ale pewnie trzeba będzie coś dostosować 😉

  12. Adam pisze:

    Czy można użyć tego kodu do przetwornika optycznego np. inkrementalny MHK40

  13. Adam pisze:

    nie za bardzo rozumię

  14. Levas pisze:

    first example, with dual int is bad in real world. If using mechanical encoder there are lots of false interupts and it gives us random number generator, not proper encoder.

  15. Levas pisze:

    Second, with gray code. count only to one side.
    Ever tested it in real harware?

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *