Tym razem chciałbym przedstawić mój pomysł na obsługę enkodera.
Biblioteka którą prezentuję została wykonana w celu przypomnienia pewnych zagadnień związanych z programowaniem mikrokontrolerów, a także w celu zrozumienia działania enkodera. Nie twierdzę więc, że moja bibliotek jest najlepsza. W sieci na pewno znajdują się rozwiązania bardziej optymalne pod względem objętości, oraz szybkości wykonywania. Przykładem takim jest rozwiązanie przedstawione pod tym adresem: http://www.mikrocontroller.net/articles/Drehgeber Polecam zapoznanie się z sposobem który został tam przedstawiony – jest on bardzo ciekawy.
A teraz zapraszam do zapoznania się z moją koncepcją dot. enkodera.
Identyfikacja obrotów enkodera odbywa się przy udziale dwóch kanałów A i B. Odpowiednie przełączanie kanałów determinuje kierunek obrotu naszego enkodera. Przedstawia się to w niniejszy sposób:

Poniżej przebiegów zostały umieszczone wartości binarne jakie uzyskuje kanał A i B podczas obrotu. Występuje regularność podczas obrotu enkodera – ta regularność ma swoją nazwę – Kod Greya.W swojej bibliotece wykorzystałem te zależności i powołałem do życia zmienną stan, która przechowuje etap zmiany tych wartości. Zostało to przedstawione na poniższym rysunku:

Stan 1 oznacza wyłączenie kanału (stan wysokiej impedancji – kanał nie podłączony do masy), natomiast 0 oznacza włączenie kanału (podciągnięcie do masy – jak to ma miejsce przy przyciskach).
Cztery najmniej znaczące bity określają kierunek obrotów zgodny z ruchem wskazówek zegara (CW), natomiast 4 najbardziej znaczące kierunek przeciwny do ruchu wskazówek zegara (CCW). Poniżej w dwóch kolumnach znajduje się reprezentacja stanu kanałów A i B podczas obrotu w danym kierunku. Należy zaznaczyć, że poziom logiczny 0 oznacza, że kanał jest aktywny (tak jak to ma miejsce podczas obsługi przycisków). Podczas każdej zmiany stanu kanału w odpowiedniej 4-ce bitów zmiennej stan ustawiane są w 1 bitowe (odpowiednim miejscu) w zależności który etap został wykryty (etapy zmiany stanów określone są jako A B C D).
Jeżeli został wykryty etap A dla kierunku zgodnego z ruchem wskazówek zegara, kolejne etapy będą wykrywane tylko dla tego obrotu – dzięki czemu podczas zakończenia ruchu zmienna stan osiągnie wartość 0xF0 (dla CCW) bądź 0x0F (dla CW). Po wykonanym obrocie można sprawdzić wartość tej zmiennej i określić czy obrót został dobrze zinterpretowany przez program czy nie.
Dodatkowo podczas rozpoczynania obrotu zostaje uruchomiony licznik (taki mój watchdog), który kończąc odliczanie w przypadku nieprawidłowego odczytu danych z enkodera powoduje zresetowanie odpowiednich zmiennych, które są użyte do działania programu. Możliwa jest taka sytuacja, gdzie enkoder znajdzie się w innym położeniu niż referencyjne i obrót spowoduje nieprawidłowe odczytanie danych z kanału. Licznik ten zapewnia poprawność odczytu w takich sytuacjach.
Aby rozjaśnić jak działa moja biblioteka przedstawiam algorytm:

Działanie programu oparte jest na zdarzeniach. Użytkownik dostaje do dyspozycji następujące funkcje:
void ENCODER_EVENT(void);
void register_encoder_event( void(*encoder_callback)(void) ); //rejestracja funkcji obsługi dowolnego obrotu, użytkownik ma do dyspozycji całą strukturę
void register_encoder_switch_event( void(*encoder_callback)(void) ); //rejestracja funkcji obsługi przycisku
void encoder_init(void); //inicjalizacja enkodera
void encoder_proccedure(void); //procedura pracy enkodera - musi znaleźć się w przerwania wykonywanym co 1ms
void encoder_clear_value(void); //wyzerowanie wszystkich wartosci w zmiennej enkoder_status
Pierwsze trzy funkcje dotyczą bezpośrednio zdarzeń (2 i 3 funkcja powoduje rejestrację odpowiednio zdarzenia dla obrotu i przycisku, natomiast pierwsza wykonuje zdarzenie dla enkodera). Kolejne dotyczą odpowiednio inicjalizacji enkodera (nie następuje konfiguracja przerwań), wykonania programu obsługi enkodera, oraz wyzerowania wszystkich wartości w zmiennej enkoder_status, która jest również do dyspozycji użytkownika i ma postać struktury:
typedef struct{
uint8_t switch_active; //czy przycisk aktywny
uint8_t switch_toggle; //zmienna toggle dla przycisku
uint16_t rotation_clockwise; //ilosc impulsów zgodnie z ruchem wskazówek
uint16_t rotation_counter_clockwise; //ilosc impulsów przeciwnie do ruchu wskazówek
uint8_t full_rotation_cw; //liczba pełnych obrotów zgodnych do ruchu wskazówek zegara (clockwise)
uint8_t full_rotation_ccw; //liczba pełnych obrotów przeciwnych do ruchu wskazówek zegara(counter clockwise)
}DS_ENCODER;
Biblioteka składa się z 3 plików, które przedstawiają się następująco:
/*
* ds_enc.h
*
* Created on: 04-12-2015
* Author: Daniel
*
*
* UWAGA!!!
*
* W tym pliku nie należy niczego zmieniać
*
*/
#ifndef DS_ENC_H_
#define DS_ENC_H_
#include "ds_enc_conf.h"
//definicje
// Makra upraszczające dostęp do portów
// *** PORT
#define PORT(x) SPORT(x)
#define SPORT(x) (PORT##x)
// *** PIN
#define PIN(x) SPIN(x)
#define SPIN(x) (PIN##x)
// *** DDR
#define DDR(x) SDDR(x)
#define SDDR(x) (DDR##x)
#define ENC_SW_A !(PIN(ENC_PORT_A) & (1<<ENC_PIN_A))
#define ENC_SW_B !(PIN(ENC_PORT_B) & (1<<ENC_PIN_B))
#if ENC_SWITCH_ENABLE
#define SW_E !(PIN(ENC_PORT_E) & (1<<ENC_PIN_E))
#endif
typedef struct{
uint8_t switch_active; //czy przycisk aktywny
uint8_t switch_toggle; //zmienna toggle dla przycisku
uint16_t rotation_clockwise; //ilosc impulsów zgodnie z ruchem wskazówek
uint16_t rotation_counter_clockwise; //ilosc impulsów przeciwnie do ruchu wskazówek
uint8_t full_rotation_cw; //liczba pełnych obrotów zgodnych do ruchu wskazówek zegara (clockwise)
uint8_t full_rotation_ccw; //liczba pełnych obrotów przeciwnych do ruchu wskazówek zegara(counter clockwise)
}DS_ENCODER;
extern volatile DS_ENCODER enkoder_status;
//********** funkcje
//******** zewnętrzne
void encoder_init(void); //inicjalizacja enkodera
void encoder_proccedure(void); //procedura pracy enkodera - musi znaleźć się w przerwania wykonywanym co 1ms
void encoder_clear_value(void); //wyzerowanie wszystkich wartosci w zmiennej enkoder_status
//***** zdarzenia
void ENCODER_EVENT(void);
void register_encoder_event( void(*encoder_callback)(void) ); //rejestracja funkcji obsługi dowolnego obrotu, użytkownik ma do dyspozycji całą strukturę
#if ENC_SWITCH_ENABLE
void register_encoder_switch_event( void(*encoder_callback)(void) ); //rejestracja funkcji obsługi przycisku
#endif
#endif /* DS_ENC_H_ */
/*
* ds_enc.c
*
* Created on: 04-12-2015
* Edited on: 11-12-2015
* Author: Daniel
*
* All rights reserved.
*
* Przy pomocy książki:
* Mikrokontrolery AVR
* Język C - podstawy programowania
*
* Autor:
* Mirosław Kardaś
* Język C.
* Pasja programowania mikrokontrolerów 8-bitowych
*
* Autor:
* Mirosław Kardaś
*
* Do użytkownika dostępne są następujące elementy:
*
* - struktura DS_ENCODER, oraz zmienna enkoder_status, która przechowuje wszystkie aktualne stany dotyczące enkkodera.Użytkownika może dowolnie je zmieniać
* - funkcjia do inicjalizacji enkodera encoder_init
* - funkcja obsługi enkodera encoder_proccedure, która powinna znaleźć się w przerwania od timera który wykonuje się co 1ms (jest to warunek konieczny do prawidłowego działania biblioteki)
* - funkcja zerujące wszystkie zmienna w strukturze enkoder_status: encoder_clear_value
* - zdarzenie od enkodera ENCODER_EVENT, które należy umiescic w pętli głównej programu
* - rejestrację funkcji obsługi enkodera
* - rejestrację funkcji obsługi przycisku z enkodera
*
* Konfigurację biblioteki należy dokonać w pliku ds_enc_conf.h
*
*
* W strukturze dostępnej dla użytkownika znajduje się następujące dane:
* - switch_active - czy przycisk aktywny
* - switch_toggle; - zmienna toggle dla przycisku
* - rotation_clockwise; - ilosc impulsów zgodnie z ruchem wskazówek
* - rotation_counter_clockwise; - ilosc impulsów przeciwnie do ruchu wskazówek
* - full_rotation_cw; - liczba pełnych obrotów zgodnych do ruchu wskazówek zegara (clockwise)
* - full_rotation_ccw; - liczba pełnych obrotów przeciwnych do ruchu wskazówek zegara(counter clockwise)
*
*
* UWAGA!!!
*
* W tym pliku nie należy niczego zmieniać
* Do prawidłowego działania biblioteki, konieczne jest umieszczenie funkci encoder_proccedure w przerwaniu od timera, które wykonuje się do 1ms
*
*
*
*/
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <util/delay.h>
#include "ds_enc.h"
#include "ds_enc_conf.h"
//********** zmienne
volatile DS_ENCODER enkoder_status;
//********** funkcje
//przerwanie
void encoder_proccedure(void){
static volatile uint8_t licznik; //odmierza czas do wyzerowania zmiennych pomocniczych w przypadku złego odczytu z enkodera
static volatile uint8_t stan=0x00; //przechowuje informację o tym, w jakim położeniu jest enkoder (podczas przełączania)
static volatile uint8_t enkoder=0; //stan kanału A i B
static volatile uint8_t tmp_enkoder=0x03; //pamięta ostatnio stan kanału A i B
if(licznik){
//odmierza czas do wyzerowania zmiennych pomocniczych
--licznik;
}
enkoder = (!ENC_SW_A<<1) | !ENC_SW_B; //odczytuje obecny stan kanału A i B
//reakcja dla licznika gdy odliczy swoj czas
if(!licznik && (stan != 0x00) && (enkoder == 0x03) ){
stan = 0x00; //wyzeruje dany stan położenia enkodera
tmp_enkoder = 0x03; //przywróc początkowe wartosci odczytanych wczesniej kanałów
}
if( (stan==0x00)){
//gdy jest stan początkowy
if(enkoder == 0x01){
//Kanał A czyli zgodnie z ruchem wskazówek
stan = 0x01; //4 najmniej znaczące bity odnoszą się do obrotu zgodnie z ruchem wskazówek - pierwsza zmiana
licznik=ENC_TIME_COUNTER;
}else if(enkoder == 0x02){
//Kanał B czyli przeciwnie z ruchem wskazówek
stan = 0x10; //4 najbardzej znaczące bity odnoszą się do obrotu przeciwnie z ruchem wskazówek - pierwsza zmiana
licznik=ENC_TIME_COUNTER;
}
tmp_enkoder = enkoder;
//0x00
}else if(stan & 0x01){
//obroty zgodnie z ruchem wskazówek zegara
if( (enkoder == 0x00) && (tmp_enkoder == 0x01) ){
stan |= 0x02; //druga zmiana
tmp_enkoder = enkoder;//zapamiętanie stanu kanału A i B
}else if( (enkoder == 0x02) && (tmp_enkoder == 0x00) ){
stan |= 0x04; //trzecia zmiana
tmp_enkoder = enkoder;//zapamiętanie stanu kanału A i B
}else if( (enkoder==0x03) && (tmp_enkoder==0x02) ){
stan |= 0x08; //czwarta zmiana
tmp_enkoder = enkoder;//zapamiętanie stanu kanału A i B
if(stan == 0x0F){
stan = 0x00; //wyzerowanie
enkoder_status.rotation_clockwise++;
licznik = 0;
}
}
//0x01
}else if(stan & 0x10){
//obroty przeciwne z ruchem wskazówek zegara
if( (enkoder == 0x00) && (tmp_enkoder == 0x02) ){
stan |= 0x20; //druga zmiana
tmp_enkoder = enkoder;//zapamiętanie stanu kanału A i B
}else if( (enkoder == 0x01) && (tmp_enkoder == 0x00) ){
stan |= 0x40; //trzecia zmiana
tmp_enkoder = enkoder;//zapamiętanie stanu kanału A i B
}else if( (enkoder==0x03) && (tmp_enkoder==0x01) ){
stan |= 0x80; //czwarta zmiana
tmp_enkoder = enkoder; //zapamiętanie stanu kanału A i B
if(stan == 0xF0){
stan = 0x00;
enkoder_status.rotation_counter_clockwise++;
licznik = 0;
}
}
}//0x10
//odczyt stanu przycisku w enkoderze
#if ENC_SWITCH_ENABLE
if(SW_E && !enkoder_status.switch_active){
enkoder_status.switch_active = 1;
enkoder_status.switch_toggle ^= 1;
}else if(!SW_E && enkoder_status.switch_active){
enkoder_status.switch_active = 0;
}
#endif
}//encoder_procedure
//******** wewnętrzne
void encoder_full_rotation(void){
static uint16_t tmp_cw;
static uint16_t tmp_ccw;
if( enkoder_status.rotation_clockwise && !(enkoder_status.rotation_clockwise % ENC_MAX_IMP) && (enkoder_status.rotation_clockwise != tmp_cw) ){
enkoder_status.full_rotation_cw++; //zwiększ licznik pełnych obrotów
tmp_cw = enkoder_status.rotation_clockwise; //zapamiętanie obecnej wartosci impulsów
}
if( enkoder_status.rotation_counter_clockwise && !(enkoder_status.rotation_counter_clockwise % ENC_MAX_IMP) && (enkoder_status.rotation_counter_clockwise != tmp_ccw) ){
enkoder_status.full_rotation_ccw++; //zwiększ licznik pełnych obrotów
tmp_ccw = enkoder_status.rotation_counter_clockwise; //zapamiętanie obecnej wartosci impulsów
}
}
//******** zewnętrzne
void encoder_init(void){
//inicjalizacja portów
//kierunek - wejście
DDR(ENC_PORT_A) &= ~(1<<ENC_PIN_A); //A
DDR(ENC_PORT_B) &= ~(1<<ENC_PIN_B); //B
#if ENC_SWITCH_ENABLE
DDR(ENC_PORT_E) &= ~(1<<ENC_PIN_E); //switch
#endif
//podciągnięcie do VCC
PORT(ENC_PORT_A) |= (1<<ENC_PIN_A); //A
PORT(ENC_PORT_B) |= (1<<ENC_PIN_B); //B
#if ENC_SWITCH_ENABLE
PORT(ENC_PORT_E) |= (1<<ENC_PIN_E); //switch
#endif
}
void encoder_clear_value(void){
//wyzerowanie wszystkich liczników enkodera
enkoder_status.full_rotation_ccw=0;
enkoder_status.full_rotation_ccw=0;
enkoder_status.rotation_clockwise=0;
enkoder_status.rotation_counter_clockwise=0;
enkoder_status.switch_active=0;
enkoder_status.switch_toggle=0;
}
//***** zdarzenia
static void (*encoder_event_callback)(void); //zmienna przechowująca wskaźnik do funkcji
#if ENC_SWITCH_ENABLE
static void (*encoder_switch_event_callback)(void);
#endif
void ENCODER_EVENT(void){
encoder_full_rotation(); //sprawdzenie czy odbył się pełny obrót
if( encoder_event_callback )
if ( enkoder_status.rotation_clockwise || enkoder_status.rotation_counter_clockwise ) //sprawdź czy funkcja została zarejestrowana
(*encoder_event_callback)(); //jeżeli tak to ją wywołaj
#if ENC_SWITCH_ENABLE
if( encoder_switch_event_callback )
if( enkoder_status.switch_active )
(*encoder_switch_event_callback)();
#endif
}
void register_encoder_event( void(*encoder_callback)(void) ){ //rejestracja funkcji obsługi przycisków
encoder_event_callback = encoder_callback;
}
#if ENC_SWITCH_ENABLE
void register_encoder_switch_event( void(*encoder_callback)(void) ){
//rejestracja funkcji obsługi przycisku
encoder_switch_event_callback = encoder_callback;
}
#endif
oraz plik konfiguracyjny bibliotekę. Użytkownik w tym pliku może dokonywać zmian. W poprzednich dwóch nie może.
/*
* ds_enc_conf.h
*
* Created on: 04-12-2015
* Author: Daniel
*
* Plik konfiguracyjny dla użytkownika.
*
* Użytkownik biblioteki może dokonywać zmian parametrów.
*
*/
#ifndef DS_ENC_CONF_H_
#define DS_ENC_CONF_H_
//ustalenie portu do którego podłączone są kanały A,B enkodera oraz przycisk E
#define ENC_PORT_A B
#define ENC_PORT_B B
#define ENC_PORT_E B
//ustalenie pinu do którego podłączone są kanały A,B enkodera oraz przycisk E
#define ENC_PIN_A 0
#define ENC_PIN_B 1
#define ENC_PIN_E 2
//ustalenie wartosci licznik czuwającego nad poprawną pracą enkodera (*1ms) -maks 255
#define ENC_TIME_COUNTER 150
//ustalenie ile impulsów posiada enkoder na pełny obrót
#define ENC_MAX_IMP 20
//***** wybór właciwosci bilioteki
#define ENC_SWITCH_ENABLE 1 //czy jest dostępny przycisk w enkoderze - jeżeli tak to jest także dostępne zdarzenie od przycisku
#endif /* DS_ENC_CONF_H_ */
Użytkownik może podłączyć enkoder do dowolnych pinów mikrokontrolera – nie muszą być to piny na tym samym porcie. Dodatkowo może ustalić czy biblioteka ma obsługiwać przycisk w enkoderze czy też nie. Z racji tej, że enkodery posiadają różną ilość impulsów na pełny obrót możliwe jest również ustawienie ilości impulsów jaką posiada nasz enkoder. W pliku konfiguracyjnym można również ustawić czas licznika (taki watchdog) który czuwa na poprawnością działania programu.
Aby biblioteka działała prawidłowo, konieczne jest:
– zastosowanie kwarcu 8MHz bądź większego;
– odpowiednie skonfigurowanie pliku ds_enc_conf.h;
– aktywowanie dowolnego timera na przerwanie wykonywane co 1 ms i umieszczenie w obsłudze tego przerwania funkcji encoder_proccedure();
– wykonanie funkcji encoder_init() na samym początku programu;
– zarejestrowanie funkcjie obsługujących obroty i/lub przycisk (wykorzystując funkcje rejestrujące przedstawione wcześniej);
– umieszczenie w pętli głównej funkcji zdarzenia ENCODER_EVENT();
– w funkcji odczytu obrotu należy umieścić polecenie zerującą dwie zmienne:
//wyzerowanie zmiennych w enkoder_status
enkoder_status.rotation_clockwise = 0;
enkoder_status.rotation_counter_clockwise = 0;
Należy zwrócić uwagę, że po wyzerowaniu tych zmiennych informacje o ilości pełnych obrotów nie są poprawnie uzupełniane.
Poniżej przedstawiam przykład zastosowania tej biblioteki:
#include <avr/io.h>
#include <avr/interrupt.h>
#include "DS_ENC/ds_enc.h"
#include "DS_ENC/ds_enc_conf.h"
//zmienne pomocnicze
uint8_t time_ls=0;
volatile uint8_t time_10ms = 0;
volatile uint8_t time_1ms = 0;
volatile uint8_t time_1s = 0;
ISR(TIMER0_COMP_vect){
if(++time_1ms>9){
time_1ms=0;
if(++time_10ms>99){
time_10ms=0;
if(++time_1s>59) time_1s=0;
}
}
encoder_proccedure();
}// /isr
void obsluga_enkodera(void){
//kod obsługi dla enkodera
//wyzerowanie zmiennych w enkoder_status
enkoder_status.rotation_clockwise = 0;
enkoder_status.rotation_counter_clockwise = 0;
};
void obsluga_przycisk(void){
//kod obsługi dla przycisku
};
int main(void){
//inicjalizacja I/O
DDRC |= (1<<PC0); //debuger kontrolny miga co 1 s
//*******************************inicjalizacja I/O enkodera
encoder_init();
//*******************************
//inicjalizacja przerwań
//timer0 w trybie ctc
TCCR0 |= (1<<WGM01); //tryb CTC
TCCR0 |= (1<<CS01) | (1<<CS00); //preskaler 64
OCR0 = 249; //co 1ms dla preskalera 64 bity i kwarcu 16MHz
TIMSK |= (1<<OCIE0); //aktywacja przerwania
//aktywacja przerwań
sei();
//*******************************inicjalizacja zdarzeń enkodera
register_encoder_event(obsluga_enkodera);
register_encoder_switch_event(obsluga_przycisk);
//*******************************
while(1){
//wywołanie zdarzenia od enkodera
ENCODER_EVENT();
//puls programu;)
if( time_1s != time_ls ){
time_ls = time_1s;
PORTC ^= (1<<PC0);
}
//..reszta kodu
}
}
Podsumowując – biblioteka umożliwia odpowiednią interpretację obrotów enkodera oraz przycisku, który znajduje się w enkoderze. Dzięki zastosowaniu licznika (programowego watchdoga), oraz przerwania wykonywanego co 1ms możliwe było zniwelowanie błędnych odczytów spowodowanych drganiami styków. Do dyspozycji użytkownika biblioteki zostały oddane funkcje obsługujące zdarzenia oraz sam enkoder, a także zmienną w której strukturze znajdują się wszystkie informacje dotyczące obrotów enkodera.
Na chwilę obecną biblioteka obsługuje tylko enkodery z tzw. pełnym krokiem. W przyszłości zostanie ona uzupełniona o obsługę kolejnych typów enkoderów.
Poniżej umieszczam całą bibliotekę:
Pobierz “DS_ENC.zip”
DS_ENC.zip – Pobrano 420 razy – 3,96 KBZachęcam do pobierania.
W razie jakichkolwiek pytań proszę pisać wykorzystując formularz kontaktowy.