Rotační enkodér elegantně

Rotační enkodér je užitečný ovládací prvek. Často ho uvidíte na autorádiu nebo mikrovlnné troubě. Ideální použití najde tam, kde cokoli měníte v diskrétních krocích. Může zastat funkci mechanického přepínače i potenciometru a rozhodně stojí za to ho do vašich konstrukcí zabudovat.

Kde se hodí ?

Typicky tam kde nastavujete číslicovou hodnotu. Frekvenci v rádiu, čas v minutkách, takt v metronomu. Poslouží i v některých aplikacích jako běžný přepínač, může mít totiž velmi (pří)jemný chod. V neposlední řadě může nahradit klávesnici, tam kde by vám její rozměry vadily. A protože pro mě je to neodmyslitelný ovládací prvek, nemohl jsem si jeho předvedení odpustit.

Před tím než začneme

Enkodér je uvnitř jen dvojice spínačů se společným vývodem. Jeho výstupy mohou stejně jako tlačítka zakmitávat a proto je dobré zákmity ošetřit. Nejjednodušší varianta jak enkodér připravit k provozu je připojit jeho vstupy A a B k Atmelu a zapnout na příslušných pinech interní pull-up rezistory. Ty mají hodnotu okolo 50kOhm, takže k odstranění zákmitů připojíme na každý z pinů kondenzátor třeba 10-100nF (proti zemi). Máme-li k dispozici enkodér vybavený tlačítkem, ošetříme tlačítko stejným způsobem. Zbylé vývody enkodéru připojíme na zem. V neutrální poloze enkodéru jsou oba spínače rozepnuté, takže na všech pinech máme Vcc. Otočením o jeden krok se nejprve sepne jeden spínač (na příslušném kanálu vzniká log.0) a chvíli poté druhý spínač, a ve stejném pořadí se oba spínače zase rozepnou. Při otáčení opačným směrem se spínají v opačném pořadí. Viz obrázek č.1 a č.2.

Obr 1. Otáčení jedním směremObr 2. Otáčení druhým směrem

Implementace

Pro úplnost, pracuji na Attiny24 a pro něj bude psán i program. Uživatelé jiného atmelu si budou muset změnit názvy rutin přerušení, ale princip činnosti programu bude totožný. Tak jak všechno i polohu enkodéru by šlo snímat pollingem, ale touto cestou se nevydáme. Necháme hardware čipu ať udělá práci za nás. Jeden z kanálů připojíme na vstup vnějšího přerušení (INT0) a nastavíme ho na detekci sestupné hrany. Ta je totiž dobře vychovaná (viz obrázky 1 a 2). Stisk tlačítka enkodéru (které je úplně nezávislé), detekuji pomocí PCINT8 (PB0). Pokud jste pozorně četli článek o externím přerušení, bude vám "zdroják" jasný. V přerušení od INT0 se jen podíváme zda druhý kanál enkodéru nabývá log.1 nebo lo.0 a z toho usoudíme kterým směrem uživatel otáčí. Každý krok otočení se rutina zavolá jen jednou. Můžeme tedy nějákou proměnnou inkrementovat nebo dekrementovat podle směru otáčení. Většinou si v kódu pak budete muset ošetřit přetečení nebo "dorazy" proměnné counter. Pokud to bude například cifra, budete muset hlídat aby byla v rozsahu 0-9.

// Rotační enkodér s pomocí přerušení
#include <avr/io.h>
#include <avr/interrupt.h>

#define DEFAULT 5

int position=DEFAULT;	// tuto proměnnou budeme měnit enkodérem

// rutina INT0 - pohyb enkodérem
ISR(EXT_INT0_vect){
	if(PINB & (1<<PINB1)){
		position++; // krok vpřed		
	}
	else{
		position--; // krok vzad
	}
}

// rutina stisknutí tlačítka (PCINT8)
ISR(PCINT1_vect){
	if(!(PINB & (1<<PINB0))){
		position=DEFAULT; // vynuluj
	}
}

int main(void){
DDRB &=~((1<<DDB0) | (1<<DDB1) | (1<<DDB2));	// konfigurujeme jako vstupy
PORTB |= (1<<PORTB0) | (1<<PORTB1) | (1<<PORTB2);	// s interním pull-up rezistorem
MCUCR |= (1<<ISC01);	// přerušení na INT0 má přijít od sestupné hrany
PCMSK1 |= (1<<PCINT8);	// Tlačítko enkodéru bude vyvolávat přerušení pomocí PCINT8
GIMSK |= (1<<PCIE1) | (1<<INT0);	// povolme vybraná přerušení
sei(); // povolme přerušení globálně

    while (1){} // nic nedělej
}

Předchozí příklad se dá použít například na zadávání digitálního čísla. Rotací nastavujete hodnotu mezi 0-9 a stiskem tlačítka se přesunete na další cifru čísla. Dvoj a trojciferná čísla, případně časové údaje se takhle dají zadávat celkem komfortně. U vícemístných čísel už stojí za to zvážit použití klávesnice. Další možností jak použít enkodér je k nastavení digitální hodnoty ve velkém rozsahu. Typický příklad je ladění rádia, oblasti kde není stanice přejedete rychlým otáčením a až stanici najdete tak ji můžete doladit krok po kroku. Někdy potřebujete enkodérem projet širokou oblast, dejme tomu 1000 hodnot. Jestliže má enkodér obvykle 12 nebo 24 kroků na otáčku, museli by jste jím otočit 40x nebo 80x a to není komfortní. Zvláště pokud bude rozsah ještě větší. Na tyto situace existuje řešení. Stačí snímat rychlost otáčení a při rychlejším pohybu zvětšovat krok enkodéru. Když obsluha přelaďuje z hodnoty 100 na hodnotu 900 začne enkodérem otáčet rychleji a vy jí pomůžete.

V následujícím kódu předvedeme jeden z přístupů jakým se takové chování dá naprogramovat. Čítačem 0 budeme měřit rychlost otáčení. Každých 65ms změříme o kolik kroků se poloha enkodéru změnila. Pokud o více jak LIMIT, přičteme nebo odečteme od aktuální pozice enkodéru nějákou hodnotu (KROK). U toho všeho přirozeně musíme ohlídat meze hodnot. Jak zvolit LIMIT, KROK a čas snímání je dle mého docela alchymie. A já na ni nemám žádnou kuchařku. Zvolíte-li LIMIT příliš nízký nebo KROK příliš vysoký, bude se obsluze obtížně nastavovat přesná hodnota. Zolíte-li KROK malý nebude to obsluze moc pomáhat. Za úvahu stojí zpracovat celý proces vícestupňově. Změřit rychlost otáčení, pokud je nízká tak do procesu vůbec nezasahovat, pokud je rychlost střední pomáhat obsluze trochu a pokud je rychlost vysoká, tak jí pomáhat hodně. Problém je tím obtížnější ším větší rozsah hodnot pokrýváte. Na rozsah 1-1000 celkem v pohodě stačí jednostupňový systém. Vyšší rozsahy už budou vyžadovat alespoň dva stupně. Tohle je nejslepší si vyzkoušet. V následujících dvou programech enkodérem měníte hodnotu PWM na OC1B kanále. Zkuste si je spustit a měnit hodnoty LIMIT a KROK. Pokud nemáte osciloskop, připojte na výstup (OC1B) RC článek. Připravíte si tak improvizovaný DAC převodník. Viz schéma č.2. Použitelné mohou být hodnoty 10kOhm a 10uF. První program používá jen jeden stupeň rychlosti, druhý je dvoustupňový.


Schéma č.1 - jednoduchý DA převodník řízený enkodérem

Jednostupňová detekce rychlosti

// Rotační enkodér s detekcí rychlého otáčení
#include <avr/io.h>
#include <avr/interrupt.h>

#define STROP 1000
#define DEFAULT 500
#define LIMIT 8
#define KROK 30

unsigned int pozice=DEFAULT;	// tuto proměnnou budeme měnit enkodérem

// rutina INT0 - pohyb enkodérem
ISR(EXT_INT0_vect){
	if(PINB & (1<<PINB1)){
		if(pozice<STROP){pozice++;} // krok vpřed jen pokud nepřejde meze		
	}
	else{		
		if(pozice>0){pozice--;} // krok vzad jen pokud je kam
	}
OCR1B=pozice;
}

// rutina přetečení časovače 0 (každých 65ms)
ISR(TIM0_OVF_vect){
static unsigned int stara_pozice=DEFAULT;	// chceme vědět kolik kroků udělal enkodér za minulých 65ms
if(pozice>stara_pozice){	// pokud se pozice zvyšuje
	if((pozice-stara_pozice)>LIMIT){	// a pokud uživatel točí enkodérem dost rychle
		if(pozice<=(STROP-KROK)){	// a pokud je ještě možné pozici zvětšovat
			pozice=pozice + KROK;	// tak mu pomůžeme !
			}
		}
	}
else{	// pokud pozice klesá nebo se nemění
	if((stara_pozice-pozice)>LIMIT){	// a pokud uživatel točí enkodérem dost rychle
		if(pozice>KROK){	// a pokud je ještě kam pozici zmenšovat
			pozice=pozice - KROK;	// tak mu pomůžeme !
			}
		}
	}
stara_pozice=pozice;	// zapamatujme si kde je enkodér teď abychom měli přiště s čím porovnávat
OCR1B=pozice;
}

// rutina stisknutí tlačítka (PCINT8)
ISR(PCINT1_vect){
	if(!(PINB & (1<<PINB0))){
		pozice=DEFAULT; // vynuluj
		OCR1B=pozice;
	}
}

int main(void){
DDRB &=~((1<<DDB0) | (1<<DDB1) | (1<<DDB2));	// konfigurujeme jako vstupy
PORTB |= (1<<PORTB0) | (1<<PORTB1) | (1<<PORTB2);	// s interním pull-up rezistorem
MCUCR |= (1<<ISC01);	// přerušení na INT0 má přijít od sestupné hrany
PCMSK1 |= (1<<PCINT8);	// Tlačítko enkodéru bude vyvolávat přerušení pomocí PCINT8
GIMSK |= (1<<PCIE1) | (1<<INT0);	// povolme vybraná přerušení
sei(); // povolme přerušení globálně
// časovačem 0 budeme měřit rychlost otáčení enkodérem
TCCR0B = (1<<CS00) | (1<<CS02);	// pouštím časovač 0 s periodou přetečení 65ms
TIMSK0 = (1<<TOIE0);	// povoluji přerušení od přetečení časovače 0
// časovačem 1 budeme generovat "užitečný" signál - PWM
DDRA = (1<<DDA5);	// výstup pro PWM ... PA5 (OC1B),
TCCR1A = (1<<COM1B1) | (1<<WGM10) | (1<<WGM11);	// čítač 1 Fast PWM s OCR1A jako TOP
TCCR1B = (1<<WGM12) | (1<<WGM13) | (1<<CS10); // 1MHz clock pro čítač 1 (frekvence CPU)
OCR1A=STROP;	// perioda čítače 1
OCR1B=pozice;	// počáteční hodnota PWM

    while (1){} // nic nedělej
}

Dvojstupňová detekce rychlosti

// Rotační enkodér s dvoustupňovou detekcí rychlého otáčení
#include <avr/io.h>
#include <avr/interrupt.h>

#define STROP 1000
#define DEFAULT 500
#define LIMIT1 6
#define KROK1 15
#define LIMIT2 20
#define KROK2 80

unsigned int pozice=DEFAULT;	// tuto proměnnou budeme měnit enkodérem

// rutina INT0 - pohyb enkodérem
ISR(EXT_INT0_vect){
	if(PINB & (1<<PINB1)){
		if(pozice<STROP){pozice++;} // krok vpřed jen pokud nepřejde meze		
	}
	else{		
		if(pozice>0){pozice--;} // krok vzad jen pokud je kam
	}
OCR1B=pozice;
}

// rutina přetečení časovače 0 (každých 65ms)
ISR(TIM0_OVF_vect){
static unsigned int stara_pozice=DEFAULT;	// chceme vědět kolik kroků udělal enkodér za minulých 65ms
// když se enkodérem točí středně rychle
if(pozice>stara_pozice){	// pokud se pozice zvyšuje
	if((pozice-stara_pozice)>LIMIT1){	// a pokud uživatel točí enkodérem dost rychle
		if(pozice<=(STROP-KROK1)){	// a pokud je ještě možné pozici zvětšovat
			pozice=pozice + KROK1;	// tak mu pomůžeme !
			}
		}
	}
else{	// pokud pozice klesá nebo se nemění
	if((stara_pozice-pozice)>LIMIT1){	// a pokud uživatel točí enkodérem dost rychle
		if(pozice>KROK1){	// a pokud je ještě kam pozici zmenšovat
			pozice=pozice - KROK1;	// tak mu pomůžeme !
			}
		}
	}
// když se enkodérem točí hodně rychle
if(pozice>stara_pozice){	// pokud se pozice zvyšuje
	if((pozice-stara_pozice)>LIMIT2){	// a pokud uživatel točí enkodérem dost rychle
		if(pozice<=(STROP-KROK2)){	// a pokud je ještě možné pozici zvětšovat
			pozice=pozice + KROK2;	// tak mu pomůžeme !
			}
		}
	}
else{	// pokud pozice klesá nebo se nemění
	if((stara_pozice-pozice)>LIMIT2){	// a pokud uživatel točí enkodérem dost rychle
		if(pozice>KROK2){	// a pokud je ještě kam pozici zmenšovat
			pozice=pozice - KROK2;	// tak mu pomůžeme !
			}
		}
	}
stara_pozice=pozice;	// zapamatujme si kde je enkodér teď abychom měli přiště s čím porovnávat
OCR1B=pozice;
}

// rutina stisknutí tlačítka (PCINT8)
ISR(PCINT1_vect){
	if(!(PINB & (1<<PINB0))){
		pozice=DEFAULT; // vynuluj
		OCR1B=pozice;
	}
}

int main(void){
DDRB &=~((1<<DDB0) | (1<<DDB1) | (1<<DDB2));	// konfigurujeme jako vstupy
PORTB |= (1<<PORTB0) | (1<<PORTB1) | (1<<PORTB2);	// s interním pull-up rezistorem
MCUCR |= (1<<ISC01);	// přerušení na INT0 má přijít od sestupné hrany
PCMSK1 |= (1<<PCINT8);	// Tlačítko enkodéru bude vyvolávat přerušení pomocí PCINT8
GIMSK |= (1<<PCIE1) | (1<<INT0);	// povolme vybraná přerušení
sei(); // povolme přerušení globálně
// časovačem 0 budeme měřit rychlost otáčení enkodérem
TCCR0B = (1<<CS00) | (1<<CS02);	// pouštím časovač 0 s periodou přetečení 65ms
TIMSK0 = (1<<TOIE0);	// povoluji přerušení od přetečení časovače 0
// časovačem 1 budeme generovat "užitečný" signál - PWM
DDRA = (1<<DDA5);	// výstup pro PWM ... PA5 (OC1B),
TCCR1A = (1<<COM1B1) | (1<<WGM10) | (1<<WGM11);	// čítač 1 Fast PWM s OCR1A jako TOP
TCCR1B = (1<<WGM12) | (1<<WGM13) | (1<<CS10); // 1MHz clock pro čítač 1 (frekvence CPU)
OCR1A=STROP;	// perioda čítače 1
OCR1B=pozice;	// počáteční hodnota PWM

    while (1){} // nic nedělej
}

Závěrem...

Mnou zvolené hodnoty LIMIT a KROK mají ještě daleko k dokonalým. Pokud příklad vyzkoušíte a podaří se vám nalézt vhodné parametry, buďte tak laskaví a nechte je v komentářích pod článkem.