Mój pomysł na obsługę enkodera – biblioteka

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:

Enkoder - przebiegi dla kanału A i B
Przebieg kanału A i 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:

Zmienna stan - prezentacja etapów
Zmienna stan – prezentacja etapów

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:

Algorytm działania biblioteki dla enkodera
Algorytm działania biblioteki dla enkodera

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 394 razy – 3,96 KB

Zachęcam do pobierania.

W razie jakichkolwiek pytań proszę pisać wykorzystując formularz kontaktowy.