fbpx

Enkoder to świetne urządzenie. Pewnie znasz jakieś sprzęty, gdzie gałka kręciła się w nieskończoność podczas regulacji jakiegoś parametru (np. wzmacniacz audio). Bardzo fajny pomysł na zastąpienie klasycznego potencjometru. Enkodery są również bardzo często używane do liczenia kąta obrotu w silnikach. W automatyce i robotyce są to urządzenia niezbędne. Chodź, pokażę Ci jak łatwo jest zaimplementować obsługę takiego enkodera na STM32.

Jak działa enkoder impulsowy

Enkoder obrotowy inaczej zwany impulsatorem posiada standardowo dwa wyjścia oznaczone jako A i B. Wraz z obrotem pokrętła na wyjściach tych pojawiają się sygnały prostokątne. Obydwa przebiegi tworzą sygnał kwadraturowy. Charakteryzuje się on tym, że obydwa przebiegu są przesunięte w fazie o 90 stopni. Sygnał taki widzisz na grafice poniżej.

Narysowałem dwa przebiegi – CW oraz CCW. Oznaczają one nic innego jak przebieg dla obrotu w kierunku zgodny ze wskazówkami zegara (CW) oraz w przeciwnym (CCW).

Na zielono zaznaczyłem początek i koniec zmian, które następują podczas jednego tyknięcia enkodera. W stanie spoczynku obydwie linie A i B są w stanie wysokim. W trakcie przekręcania się enkodera, na wyjściach kształtują się sygnały prostokątne. W zależności od kierunku obrotu sygnał zaczyna się zboczem opadającym na pinie A dla CW lub na pinie B dla CCW. Po prawej stronie wkleiłem jak to wygląda na żywym przykładzie z analizatora logicznego.

Możesz zauważyć, że zaznaczyłem każde zbocze. To na nich najlepiej badać kierunek, w którym obraca się drążek lub oś silnika z magnesem. Jak sprawdzić na podstawie zbocza, w którą stronę kręci się enkoder? Możesz zauważyć pewną zależność, którą przedstawiłem w poniższej tabeli.

Przejście z lewej do prawej po tabelce da Ci kierunek, w którym obraca się enkoder. Jak widzisz, można ustawić przerwania na przykład na zbocze opadające A i sprawdzać, w jakim stanie znajduje się wtedy linia B. Jeżeli niskim, to obrót był “w lewo”. Jeśli B było wysokie to obrócono “w prawo”. Proste, co nie?

Obsługa enkodera

Wypadałoby obsłużyć jakoś na mikrokontrolerze otrzymany sygnał z enkodera. Mając powyższą tabelę można już napisać obsługę przerwać. W teorii wygląda to fajnie, ale rzeczywistość jest już inna. Otóż w taki enkoder obrotowy zbudowany jest z przycisków mechanicznych. Wiesz już co to oznacza? Tak, drgania styków… Mając ustawione przerwania na jedno zbocze może się okazać, że przy pojedynczym tyknięciu takich zbocz pojawi się o wiele więcej. Mikrokontroler jest tak szybki, że obsługa przerwania zostanie wykonana kilkukrotnie dając nieprzewidziany efekt. 

Co można zrobić? Można używać jakiegoś sposobu na debouncing. Jednak takie programowe likwidowanie styków na enkoderze nie jest najwygodniejszym rozwiązaniem. Mam dla Ciebie znacznie lepsze wyjście z tej ciężkiej sytuacji. 

Pod ręką mamy oczywiście mikrokontroler STM32. Jego timery posiadają sprzętową obsługę enkoderów! Czy to nie jest piękne? Jasne, że jest 🙂

Sprzętowa obsługa enkodera na timerze

Dzisiaj będę działał na Nucleo z STM32F401RE. Zerknijmy zatem w Reference Manual układu. Na pewno znajdziemy tam coś interesującego.

Jak widzisz istnieje taki ficzer timera o nazwie Encoder interface mode. Opis zajmuje mniej więcej dwie strony, więc nie jest on obszerny. Warto się z nim zapoznać. Z drugiej strony nie musi być on rozbudowany, bo jest to dosyć prosta funkcja timera. W RM znajdziesz informacje, które rejestry skonfigurować, aby obsługa enkodera działała. Na szczęście mamy STM32CubeIDE ze wbudowanym Cube’em, który zrobi to wszystko za nas. Przejdźmy do niego.

Skonfiguruję TIM1 jako ten, który będzie czytał sygnały A i B  z enkodera. W ustawieniach tego licznika wybierz w opcji Combined Channels funkcję Encoder Mode. Większość pozostałych funkcji licznika wyszarzy się.

Na modelu MCU pojawią się skonfigurowane dwa piny – CH1 i CH2 dla Timera nr 1. Możesz je nazwać według podłączenia. Niektóre moduły będą miały oznaczone sygnały jako A i B, a niektóre będą miały analogicznie CLK i DT.

Podłącz odpowiednio enkoder do pinów Nucleo. PA8 odpowiada D7 na złączu Arduino, a PA9 – D8.

Gdy już masz włączony tryb enkodera oraz ogarnąłeś podłączenie do Nucleo to przychodzi czas na konfigurację sposobu pracy tego Timera. W oknie Configuration znajdziesz mnóstwo rzeczy. Na szczęście wiele z nich nie będzie miało znaczenia dla działania licznika z enkoderem. Zaznaczyłem te ustawienia, które powinny Cię interesować.

  • Counter Mode – decyduje, czy licznik będzie się zwiększał wraz z ruchem CW czy zmniejszał.
  • Counter Period – do jakiej wartości licznik będzie zliczał. Po jej przekroczeniu przekręci się. Można to ograniczyć w specyficznych zastosowaniach, ale ja lubię pełny 16-bitowy zakres z uwagi na magię arytmetyki liczb binarnych o czym się zaraz przekonasz 🙂
  • Encoder Mode – Decyduje, na którym kanale będzie odbywało się naliczanie impulsów. Jeżeli ustawisz samo TI1 lub TI2, licznik będzie reagował na zbocza tylko jednego z sygnałów. Ja ustawię na tryb kombinowany, aby reagował na wszystkie.
  • Polarity – Nie zauważyłem różnicy w działaniu dla różnych zbocz. Zostaw na narastającym.
  • Input Filter – Bardzo ważny parametr. Decyduje on o długości próbkowania wejść sygnałów, zanim nastąpi zmiana licznika. Mając taki ręczny enkoder warto ustawić go na wartość maksymalną. Skutecznie wyeliminuje to efekt drgania styków.

Ja dodatkowo skonfigurowałem jeszcze UART2, który pociągnięty jest do ST-Link’a.

Oprogramowanie enkodera

Kod dla STM32F401RE wygenerowałem przy pomocy STM32CubeIDE 1.0.2 z biblioteką HAL F4 w wersji 1.24.1.

Skonfigurowany według powyższego opisu Cube wygeneruje kod prawie gotowy do akcji. Potrzebujesz jedynie uruchomić timer wraz ze wszystkimi kanałami.

HAL_TIM_Encoder_Start(&htim1, TIM_CHANNEL_ALL);

Teraz każde poruszenie enkoderem będzie zliczone a aktualna ilość zliczonych impulsów dla TIM1 znajduje się w rejestrze CNT tego licznika.

htim1.Instance->CNT

W pętli głównej wrzuciłem printowanie wartości tego rejestru na terminal i… licznik zlicza po 4 impulsy. 

Czy to jest błąd? Otóż nie! Przypomnij sobie konfigurację trybu enkodera. Ustawiliśmy tak, aby reagował zarówno na zbocza sygnału A i B. Dla jednego przeskoku enkodera sygnały te wygenerują po dwa zbocza, więc finalnie licznik zliczy ich cztery. Stąd wzięła się ta wartość. Jeżeli ustawisz Encoder Mode na T1 lub T2, licznik będzie zmieniany o wartość równą 2 z każdym tyknięciem enkodera.

Obracając powoli enkoderem i trzymając go w stanie między stabilnymi pozycjami możesz zaobserwować na liczniku wartości pośrednie – świadczy to właśnie o tym, że każde zbocze jest traktowane osobno. 

Łatwo jest przeliczyć ile tyknięć nastąpiło na enkodzerze. Wystarczy podzielić przez 4 i gotowe!

Jak widzisz obsługa enkodera jest prosta 🙂

No dobra panie, ale co zrobić z tą wartością?

Halo halo! Co, jeżeli używam enkodera jako pokrętła do zwiększania głośności we wzmacniaczu powiedzmy w zakresie 0-30?! Przecież nie mogę użyć zmiennej htim1.Instance->CNT jako wartość głośności, bo nawet ograniczając zliczanie do 30, będzie ona się przekręcać! Poza tym po resecie MCU wartość ta zawsze będzie miała zero! Jak żyć?

Pokażę Ci pewien trick oraz to dlaczego prawie zawsze rekomenduję używać pełnego zakresu licznika.

Powołam zmienną do przechowywania aktualnego poziomu głośności.

int8_t AudioVolume;

Możesz spytać się dlaczego int8_t, a nie ‘klasyczne’ uint8_t. Otóż chcąc trzymać zakres 0-30 łatwiej będzie sprawdzać, czy zmienna zeszła poniżej zera niż to, czy się przekręciła z powrotem na 255.

Teraz cała  magia. Wydzieliłem sobie odrębną funkcję dla czytelności kodu.

void UpdateAudioVolume(void)
{
	static uint16_t LastTimerCounter = 0;
	int TimerDif = htim1.Instance->CNT - LastTimerCounter;
	if(TimerDif >= 4 || TimerDif <= -4)
	{
		TimerDif /= 4;
		AudioVolume += (int8_t)TimerDif;
		if(AudioVolume > 30) AudioVolume = 30;
		if(AudioVolume < 0) AudioVolume = 0;
		LastTimerCounter = htim1.Instance->CNT;
	}
}

Pozwól, że wyjaśnię po kolei jak to rozwiązałem i gdzie jest magia.

W pierwszej kolejności powołałem zmienną statyczną pamiętającą ostatni stan licznika w momencie zmiany głośności.

static uint16_t LastTimerCounter = 0;

W każdym obiegu sprawdzam różnice między poprzednim stanem licznika przy zmianie głośności a teraźniejszym

int TimerDif = htim1.Instance->CNT - LastTimerCounter;

Warunek wejścia do zmiany głośności. Jeżeli stan zmienił się o więcej niż 4 (kierunek zwiększający) lub mniej niż -4 (kierunek zmniejszający) to wchodzę dalej.

if(TimerDif >= 4 || TimerDif <= -4)

Dzielę oczywiście, aby uzyskać ilość pojedynczych tyknięć enkodera.

TimerDif /= 4;

Oraz zwiększam/zmniejszam głośność.

AudioVolume += (int8_t)TimerDif;

Na koniec sprawdzam, czy głośność nie wypadła poza granice oraz spisuję aktualny stan licznika.

if(AudioVolume > 30) AudioVolume = 30;

if(AudioVolume < 0) AudioVolume = 0;

LastTimerCounter = htim1.Instance->CNT;

No dobra, ale gdzie jest ta magia? Podpowiem Ci. Chodzi o dwa szczególne przejścia licznika htim1.Instance->CNT. Są to dokładnie przejścia 0->65535 oraz w drugą stronę.

Dlaczego? Mam na liczniku zero oraz ostatnia znana wartość to również zero. Przekręcam enkoder w lewo(do tyłu) i co się dzieje?

Dla uint16_t 0 – 4 = 65532. Teraz odejmując poprzednią wartość (0) od aktualnej (65532) otrzymujemy dla zadeklarowanej zmiennej TimerDif jako int wartość 65532. Teraz według algorytmu dzielisz przez 4 otrzymując 16383 którą według algorytmu dodajesz do zmiennej głośności.

Widzisz, że jest coś nie tak, bo przekręciłeś w kierunku, który ma zmniejszyć głośność, a finalnie ma ją zwiększyć o 16383! Masakra, co nie? 

Ale możesz zauważyć jeden, mały szczegół. Otóż zrzutowałem wartość TimerDif na int8_t. Dla małych wartości (od -129 do 127) nie będzie to miało większego znaczenia. ALE dla takiej wartości jak 16383 ma już kolosalne znaczenie. Wiesz, jaki będzie wynik tego rzutowania? Będzie to… uwaga -1! Tak, magicznym sposobem przechodząc przez przekręcenie się licznika, otrzymasz wartość, której oczekiwałeś. Ekstra, co nie?

To samo będzie z przekręceniem się w drugą stronę. Jeżeli nie wierzysz, sprawdź to na jakimś kompilatorze online jaki będzie wynik dla rzutowania na int8_t zmiennej o wartości 16383. 

Po takim zabiegu nie ważne, z jakiej wartości początkowej będziesz startował enkoderem, zmienna AudioVolume zawsze zachowa się tak, jak od niej wymagamy. Nigdy nie wyjdzie ponad 30 ani poniżej 0.

Podsumowanie

Pokazałem Ci, że obsługa enkodera na STM32 jest banalnie prosta. Z prostym trickiem możesz również korzystać z enkodera do kontrolowanej modyfikacji zmiennych.

Na rynku możesz znaleźć inne typy enkoderów. Innymi popularnymi są te do zliczania obrotów silnika. Są to na przykład enkodery działające na obracające się pole magnetyczne. Takie również możesz obsługiwać timerem w trybie enkodera.

A Ty jaką masz opinię o enkoderach? Podziel się w komentarzu.

Pełny projekt wraz z biblioteką znajdziesz jak zwykle na moim GitHubie: link

Jeśli zauważyłeś jakiś błąd, nie zgadzasz się z czymś, chciałbyś coś dodać istotnego lub po prostu uważasz, że chciałbyś podyskutować na ten temat, napisz komentarz. Pamiętaj, że dyskusja ma być kulturalna i zgodna z zasadami języka polskiego.

4.7/5 - (9 votes)

Podobne artykuły

.

19 komentarzy

Kamil · 26/02/2022 o 14:36

A jak zwiększyć skalę np. z 30/-30 do zakresu long long int ?

    Kamil · 26/02/2022 o 14:54

    Bo chciałbym sterować silnikiem dc z encoderem i kiedy podstawiam zmienną long long int na AudioVolume to przy cofaniu pojawia mi się wartość:16383
    zamiast -1,-2,-3,-4 itd.
    I chciałbym się spytać czy to jest najlepszy sposób do odczytywania pulsów silnika dla enkodera inkrementalnego 2 fazowego ?

      Kamil · 26/02/2022 o 14:59

      Dobra zdaje się żę chyba naprawiłem zrzutowałem TimetDiff na int8_t i wszystko gra.

Mateusz G · 12/04/2021 o 12:35

Mam pytanko, czy mógłbyś podpowiedzieć jak najprościej obsłużyć wciśnięcie przycisku enkodera ? Czy po przez przerwanie od timera, a może po prostu wykrywanie stanu na danym pinie GPIO ?

    Mateusz Salamon · 13/04/2021 o 09:07

    Dokładnie tak samo jak zwykły przycisk 🙂 Proponuję maszynę stanów na pollingu jeśli nie robisz jakichś wielkich rzeczy w pętli głównej.

Tomek · 24/11/2020 o 19:00

Rozumiem ,że nie ma żadnego problemu przy zasilaniu enkodera z 3.3V? Widziałem tylko takie przeznaczone dla 5V ale sądzę ,że to napięcie maksymalne 🙂

    Mateusz Salamon · 24/11/2020 o 20:11

    Zerkinj na schemat jak to działa 😛 “Zasilanie” enkodera to nic innego jak stan wysoki, który ma przełączać. Opis na PCB jest pod Arduino.

Marcin · 24/06/2020 o 22:57

Pytanie odnosnie czsci spzretowej.
Czy doenkoder trzeba podpinacrezystory podciagajace do+ czy samtimer ma pull-up-y? Czy maja juz ustawiona funkcje GPIO dla timera mozna pull-upy wlaczac?

    Mateusz Salamon · 30/06/2020 o 09:51

    Moduł enkodera który użyłem ma swoje pullupy na płytce.

Andrzej · 18/05/2020 o 18:53

Ja już wyprodukowałem 2 enkodery optyczne używając mechaniki od potencjometru i elementów (ząbkowana tarczka + diody i fototranzystory) ze starej myszy optycznej.Działa bez pudła. I bez drgań styków.

    Mateusz Salamon · 18/05/2020 o 19:34

    W sumie do takich enkoderów ten układ służy a zwłaszcza do silników 🙂

SaS · 03/12/2019 o 22:22

Sprzętowa obsługa enkodera przez timer dla enkodera nieprzemysłowego tylko “zwykłego” używanego do obsługi np menu to zły pomysł. W takich enkoderach drgają styki ponadto jak pamiętam tylko timer 1 i 9 obsłuży enkoder. Enkodery używane do zmiany parametrów urządzenia prze użytkownika najlepiej obsłużyć na przerwaniu 1ms. W przerwaniu należy zrealizować programowy filtr dolnoprzepustowy a za nim, po wykryciu zmiany jednego z wejść sprawdzić stan drugiego aby określić kierunek. Można wykrywać zmiany obu wejść aby zwiększyć rozdzielczość 2 razy albo oba zbocza obu wejść aby zwiększyć 4 razy.

    Mateusz Salamon · 08/12/2019 o 15:10

    Robiłem taką obsługę na 1 ms interwale na AVR. Faktycznie działa to trochę lepiej z drganiami styków 🙂

Optimex · 04/11/2019 o 21:24

Filtr RC na obu liniach enkodera definitywnie rozwiązuje problem drgania styków, wystarczy dobrać odpowiedni filtr dla przewidywanej prędkości obrotów wtedy nie będzie konieczne opóźnienie sprzętowe ale też w niczym nie zaszkodzi.
I jeszcze jedna rzecz mi w oczy wpadła – zmienne statyczne nie muszą być jawnie inicjalizowane 0, one są z domniemania zerowane jeśli się im nie przypisze innej wartości. Być może szczegółu się czepiłem ale podobno diabeł tkwi w szczegółach 😉

    Mateusz Salamon · 04/11/2019 o 22:08

    Cześć. Dzięki za odpowiedź 🙂 Oczywiście taki filtr załatwi sprawę. Myślę, że taki prosty jak do przycisków z 10k i 10nF dałby radę. Nie testowałem jeszcze. Co do statycznej to fakt. Jednak uważam, że nadmiar takiego kodu będzie bardziej czytelny 🙂 im mniej do domysłu lub poleganiu na pamięci tym lepiej. Sam pewnie w momencie pisania zapomniałem o tym, że zostanie zainicjowana zerem 🙂

dambo · 22/08/2019 o 21:30

Masz coś nie tak przy kopiowaniu znaczków w kodzie nie tak – jest np “-&” zamiast “->”.
Sztuczka fajna 🙂
Od siebie dodam, że w mikrokontrolerach te powszechnie używane enkodery to raczej nie jest główny target – bardziej chodzi o takie dla silników – wtedy bez sprzętowego wsparcia kiepsko dla szybkich obrotów + nie ma też drgań styków dla nich itp.

    Mateusz Salamon · 23/08/2019 o 07:30

    Ten problem ze zmieniającymi się znaczkami powoduje, że powoli łysieję 🙁 Muszę znaleźć lepszą wtyczkę do snippetów… Po którejś aktualizacji właśnie zaczęło się tak robić.

    O tak enkodery silnikowe to jest to, czego spodziewamy się w pracy z takim timerem. Muszę się nimi zająć bo jestem po Automatyce i Robotyce, a nigdy robota nie zbudowałem hehe 😀

Andrzeja · 22/08/2019 o 13:29

Witam,
ciekawy artykuł. Mam jednak pytanie dotyczące stabilności odczytu wartości pozycji. Chodzi mi dokładnie o podatność na drgania styków enkoderow mechanicznych. Kiedyś próbowałem użyć sprzętowej obsługi enkodera wraz z enkoderkiem mechanicznym i efekty były mizerne. Czasami jeden krok był zliczany jako kilka kroków. Skończyło się na obsłudze programowej. Mechanizm sprzętowej obsługi enkodera działa bardzo dobrze z przemysłowymi enkoderami inkrementalnymi, choć i tak używam zewnętrznych układów kondycjonujacych sygnał. Może wrzucisz jakiś filmik pokazujący działanie układu?

    Mateusz Salamon · 23/08/2019 o 07:33

    Przy takich tanich enkoderach mechanicznych na pewno ważny jest czas próbkowania. Jedyne co zaobserwowałem przy moim kodzie to zgubienie kroku przy na prawdę szybkim kręceniu. Fajnie byłoby się podpiąć pod oscyloskop i zobaczyć co na prawdę dzieje się na stykach. Obstawiam, że jest tam masa śmieci. Spróbuję domontować wyświetlacz i nagrać jakiś film przez weekend 🙂

Dodaj komentarz

Avatar placeholder

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