Jak zaprogramować pilot i odbiornik podczerwieni na AVR ATTINY84A

Odbiornik podczerwieni i pilot to chyba najprostszy sposób zdalnego przesyłania sygnałów. Dzisiaj pójdziemy trochę dalej z tematem przetwarzania sygnałów i podejmiemy próbę odebrania sygnału z pilota oraz zdekodowania go w taki sposób aby było wiadomo jaki guzik został wciśnięty na pilocie.

Mamy już przerobione podłączanie ekranu LCD, więc mamy całkiem spore i wygodne możliwości testowania. Możliwe że trochę sobie poesperymentujemy.

Czujnik podczerwieni

Podstawowym elementem jaki umożliwia komunikację mikrokontrolera z pilotem na podczerwień jest czujnik podczerwieni. Akurat mam pod ręką moduł odbiornika podczerwieni 1838. Jest to rozwiązanie dość tanie, na ten moment kosztuje tylko kilka złotych.

Ważna sprawa: Nie pomyl podłączenia. Z początku sugerowałem się schematem czystego czujnika podczerwieni bez modułu i okazało się że podłączyłem źle i nic nie działało jak trzeba. W module wyjścia zostały „Zamienione”. Jeśli dokładnie się przyjrzysz modułowi zauważysz że jest tam zaznaczony plus i minus, trzecie wyjście to sygnał, który podpinamy do mikrokontrolera.

Napięcie wymagane przez czujnik to od 2,7V do 5,5V czyli spokojnie możemy podpiąć zasilanie z programatora na testy, później możemy wykorzystać np. dwie baterie paluszki 1,5V. Wykorzystam tutaj PIN PB1 mojego ATTINY84A-PU, który wykorzystywałem do tej pory. Można dodać do VCC rezystor 100R oraz podciągnąć pod sygnał przez stosunkowo mocny rezystor ok 20K. Można też dodać kondensator pomiędzy VCC a GND co powinno wyeliminować zakłócenia, ale bez tego pewnie też zadziała.

Do testów wykorzystuję także rozwiązanie z wyświetlaczem LCD (4×16 nie wytrzymał moich wszystkich testów więc mam teraz 4×20 ale obsługuje się go dokładnie tak samo jak w artykule). Jak zaprogramować wyświetlacz LCD (HD44780) na AVR ATTINY84A

Pilot ARD-8948

Mam też bardzo tani pilot dosłownie za kilka złotych ARD-8948.

Jeśli zabierasz się za ćwiczenie z innym pilotem musisz koniecznie określić standard kodowania. W moim przypadku jest to standard NEC, który za chwilę z grubsza postaram się opisać. Dodatkowe parametry to kąt czy odległość do jakiej pilot działa (w tym przypadku do 8 metrów).

Skoro mamy już złożony nasz prototyp testowy i wszystkie jego elementy możemy przejść dalej. Zanim zaczniemy programować trzeba najpierw zrozumieć jak działa standard kodowania pilota.

Standard kodowania NEC

W standardzie NEC sygnał zaczyna się od zmiany stanu na pinie na ok 9ms, po których następuje ok 4,5ms przerwy. Niestety te czasy nie są całkiem dokładne co do mikrosekundy, ale za chwilę zobaczyć przykład jak można sobie z tym poradzić.

Następnie jest przesyłane czterech bajtów danych, czyli 32 bity. Każdy bit rozpoczyna się od zmiany napięcia na ok 560 mikrosekund po czym następuje przerwa. Właśnie od długości przerwy zależy wartość przesyłanego bitu. W przypadku 1 długość przerwy stanowi 3×560 czyli ok 1680 mikrosekund, w przypadku zera przerwa ta trwa tylko ok 560 mikrosekund.

Aby stwierdzić czy jest to 0 czy 1 wystarczy odczekać jakąś wartość środkową od rozpoczęcia przerwy np 1000 mikrosekund czyli więcej niż 560 a mniej niż 1680.

Pierwszy bajt czyli 8 bitów to adres odbiornika, kolejne 8 bitów danych stanowi jego negację. Oznacza to że tam gdzie w pierwszym bajcie adresu była 1 teraz będzie 0. Dzięki temu możemy łatwo zweryfikować poprawność odebranych danych.

Kolejne dwa bajty to analogiczna sytuacja dla przesyłanej komendy. Czyli pierwszy bajt stanowi komendę a drugi jej negację.

Wartość bajtu komendy stanowi kod przycisku na pilocie jeśli została przesłana poprawnie. Nasz projekt pozwoli odczytać kody komendy w bardzo prosty i przejrzysty sposób.

Co chcemy napisać?

Napiszemy sobie dzisiaj niezbyt skomplikowany program, który na wyświetlaczu LCD zaprezentuje nam trzy informacje:

  • kod komendy danego przycisku na pilocie
  • wynik weryfikacji poprawności odczytania komendy
  • informację jaki guzik został wciśnięty (obrazek wyniku jak niżej)

Jak odczytać sygnał NEC z pilota i odbiornika podczerwieni?

Na początek w funkcji głównej programu zdefiniujemy tablicę, w której będziemy przechowywać 16 bitów komendy. (komendę w właściwej postaci oraz jej negację). Stworzę sobie także tablicę na adres

int command[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
int address[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};

Dalej w pętli głównej programu będziemy nasłuchiwać czy pojawił się jakikolwiek sygnał na naszym PINie, jak wcześniej ustaliliśmy będzie to PIN PB1. Jeśli pojawi się sygnał odczekam na wszelki wypadek kilka mikrosekund żeby się upewnić że nie jest to jakieś zakłócenie. (choć pewnie ten ostatni element może być zbędny). Jeśli nie jest to tylko zakłócenie to czekam aż skończy się pierwsza faza sygnału.

if(!(PINB & (1<<PB1))){ //Poczętek sygnału
    _delay_ms(5);
    if((PINB & (1<<PB1))){
        continue; //to tylko jakieś zakłucenie
    } else { 
        while(!(PINB & (1<<PB1))){ //czekam do końca pierwszej fazy
            continue;
        }
    }
    //Tutaj będę dalej odczytywał sygnał
}

Nie czekam tutaj 9ms tylko wykorzystuję pętlę while dopóki stan się nie zmieni ponieważ zazwyczaj długość tego etapu nie trwa dokładnie 9ms. Tym sposobem niezależnie jak niedokładny będzie ten sygnał tolerancja jest tutaj niezbyt ograniczona.

Dalej zamiast czekać 4,5ms na zakończenie oczekiwania wykorzystuję metodę jak wyżej, dzięki czemu nie muszę się przejmować dokładności tego czasu.

while((PINB & (1<<PB1))){
    continue;
}

Kiedy stan na wejściu się zmieni znaczy to że nasz odbiornik zaczyna przesyłać kolejno 16 bitów adresu, po 8 na adres i jego negację.

Wiem już że najpierw jest sygnał, który trwa około 560us po czym następuje przerwa. To właśnie od tej przerwy zależy czy będzie to 0 czy 1. Więc tutaj przeczekuję sygnał po czym czekam więcej niż 560us a mniej niż 1680us i sprawdzam jaki jest stan na wejściu. Bo wiem że jeśli jest to nowy sygnał to jest to 0 a jeśli przerwa jeszcze trwa to 1. Zapisuję informację o bicie do tablicy (żeby to później można było łatwo wyświetlić) i czekam do końca przerwy lub przechodzę do kolejnej iteracji pętli.

for(int i = 0; i<16;i++){
    while(!(PINB & (1<<PB1))){
        continue;
    }
    _delay_us(700);
    if(!(PINB & (1<<PB1))) {
        address[i] = 0;
    } else {
        address[i] = 1;
    }
    while((PINB & (1<<PB1))){
        continue;
    }
}

Analogicznie odczytuję bity komendy. Pewnie można by wykorzystać do tego pętlę wyżej.

for(int i = 0; i<16;i++){
    while(!(PINB & (1<<PB1))){
        continue;
    }
    _delay_us(700);
    if(!(PINB & (1<<PB1))) {
        command[i] = 0;
    } else {
        command[i] = 1;
    }
    while((PINB & (1<<PB1))){
        continue;
     }
}

I w zasadzie już w tym momencie mam wszystkie informacje, jakie potrzebuję odczytać z sygnału. Teraz wystarczy to w jakiś sposób obrobić według uznania. Ja postanowiłem sobie wyświetlić wartości poszczególnych bitów komendy i jej negacji. Żeby wynik był bardziej czytelny oddzieliłem komendę od negacji spacją. Teraz na pierwszy rzut oka będzie widać czy dane zostały dobrze odczytane, czyli tam gdzie w komendzie są jedynki w negacji będą zera i na odwrót.

Przy okazji liczę poprawne bity do walidacji, można to rozwiązać na kilka lepszych sposobów jak wszystko. Pewnie później pokażę jakiś prosty projekt ze znacznie bardziej zoptymalizowanym kodem.

LCD_Clear(); //Czyszcze ekran
LCD_GotoXY(0, 0); //Ustawiam kursor na pierwszym znaku (x) pierwszego rzędu (y)
LCD_PrintString("Komenda: "); //Wypisuję słowo czas na ekranie
LCD_GotoXY(0,1);
int isValid = 0;
for(int i = 0; i<16;i++){
if(i==8){ //sprawdzam czy jest to połowa
LCD_PrintString(" "); //oddzielam komendę od jej negacji
}
LCD_PrintInteger(command[i]); //wyświetlam bit
if(i < 8){ // połowa to komenda a druga połowa negacja
if(command[i] == command[i+8]){ //sprawdzam czy poprawny bit
isValid += 1; // dodaję poprawny bit
}
}
}

Najważniejsze mamy już za sobą. Na tej podstawie można stwierdzić czy dane się poprawnie przesłały. Dalej jeszcze wyświetlimy informację czy nie ma błędu w komendzie oraz zamienimy tablicę bitów na guzik. (odczytałem tylko trzy pierwsze bo do bardziej praktycznych zastosowań ta metoda nie za bardzo ma sens ale za to moim zdaniem doskonale pozwala zrozumieć jak to działa).

if(isValid > 0){
LCD_GotoXY(0,2);
LCD_PrintString("Error");
} else {
LCD_GotoXY(0,2);
LCD_PrintString("OK!");
LCD_GotoXY(0,3);
LCD_PrintString("Kliknieto: ");
LCD_PrintInteger(getKey(command));
}
_delay_ms(1000);

Jeśli komenda nie poprawna to wyświetlam w kolejnym rzędzie Error a jeśli poprawna to OK! i w następnym rzędzie wyświetlacza jaki to przycisk. Jeśli masz na swoim wyświetlaczu tylko dwie linie to bity wyświetliłbym w pierwszym zamiast komenda a w drugim OK! i nr guzika.

Zapewne zauważyłeś że w powyższym kodzie wykorzystuję komendę getKey(command) do której przekazuję tablicę z bitami. W tej funkcji po prostu porównuję bity z tymi do których pasują guziki. Po prostu odczytałem je sobie z wyświetlacza. Normalnie zamiast bawić się w tablicy po prostu odczytujemy kod przycisku co daje jakąś wartość i na podstawie tej wartości wykonujemy działanie przypisane do przycisku.

Poniżej moje metody do odczytania guzika, które dodałem tylko co przed funkcją main. Zaprogramowałem tylko 3 pierwsze przyciski, każdy kolejny daje 404 ale nic nie stoi na przeszkodzie żeby podobnie zrobić z pozostałymi.

int compareArrays(int array1[], int array2[]){
for(int i = 0; i < 16; i++){
if(array1[i] != array2[i]){
return 0;
}
}
return 1;
}
int getKey(int code[]){
    int one[16] = {1,0,1,0,0,0,1,0,0,1,0,1,1,1,0,1};
    int two[16] = {0,1,1,0,0,0,1,0,1,0,0,1,1,1,0,1};
    int three[16] = {1,1,1,0,0,0,1,0,0,0,0,1,1,1,0,1};
    if(compareArrays(one, code)){
        return 1;
    }
    if(compareArrays(two, code)){
        return 2;
     }
     if(compareArrays(three, code)){
          return 3;
     }
     return 404;
}

A taki efekt powinieneś otrzymać na koniec. Mamy tutaj kod komendy, walidację przesłanych danych i ostatecznie informację co zostało wciśnięte.

I na koniec obrazek tego całego potworka. W sumie tych kabli powinieneś mieć ze trzy razy mniej gdyby wpiąć odbiornik i potencjometr bezpośrednio w płytkę prototypową. Plus mam tu czujnik temperatury o którym w innym artykule 😛