Dekodowanie sygnału pilota RC6 na AVR.

Zastosowanie pilota podczerwieni w projekcie jest często wygodnym rozwiązaniem interfejsu użytkownika. Niewielka liczba elementów, brak części mechanicznych, niskie koszty, duża liczba przycisków. Zazwyczaj projektanci stosowali kod RC5 z pilotów Philipsa. Kod ten dzięki sowjej popularności doczekał się wielu przykadów, bibliotek i implementacji chyba w każdym języku na większość platform. Od jakiegoś czasu już Philips stosuje kolejną wersję swojego kodu – RC6, co powoduje, że piloty RC5 są coraz mniej popularne. Zdecydowałem się więc na stworzenie implementacji dekodera RC6 – który powinien współpracować z nowymi pilotami.

Z poprzednikiem, czyli RC5, nowy kod łączy częstotliowść nośna – 36kHz. Z taką częstotliowścią nadaje dioda IR, stopień wypełnienia 25% – 50%. Do odbioru RC6 najwygodniej zastosować scalony odbiornik, praktycznie dowolny z grupy TSOP-xx36. W przykładzie użyłem TSOP-1736, ale w projektach mam róznież TSOP-4836 który działa identycznie.

Poniżej wykonana przeze mnie ilustacja ramki RC6 (klawisz 8 w pilocie od telewizora), z opisem poszczególnych sekcji. Odbiornik pracował przy napięciu 5V. Obraz został rozciągnięty dwukrotnie w poziomie, ale wszystkie widoczne opisy skali z oscyloskopu są poprawne.

Ramka RC6

 

W kodzie RC6 (jak i w RC5) zastosowane jest kodowanie Manchester – oznacza to, że w środku bitu zawsze nastepuje zmiana stanu. Scalone odbiorniki IR ustawiają na wyjściu stan niski, kiedy sygnał jest odbierany i wysoki, kiedy brak sygnału (stan spoczynkowy linii). Ostatecznie na wyjściu odbiornika – przejście z 1 na 0 oznacza logiczne 0, przejście z 0 na 1 oznacza 1. Połowa bitu ma długość około 0,444ms.

Poszczególne fragmenty ramki koleno oznaczają:

– trailer (leader), rozbiegówka. Sygnał nadawny jest ciągle przez 2,666ms, następnie 0,889ms przerwy.

– bit startu – zawsze 1

– 3 bity trybu. Wszystkie moje piloty przesyłają tu trzy zera. Poniższy kod dekodera uwzględnia tylko taki tryb.

– 1 bit toggle. Jest to bit, który się zmienia za każdym naciśnieciem przycisku. W ten sposób można odróżnić czy przycisk jest stale wciśnięty (wówczas wartość tego bitu będzie stała), czy jest seria naciśnięć. Tutaj istotna uwaga – bit ten ma podwójną długość (jego połowa około 0,890ms).

– 8 bitów adresu (kontrolnych) – adres urządzenia. telewizor ma adres 0.  Kod w tym trybie może zaadresować do 255 urządzeń.

– 8 bitów polecenia – definiują konkretny klawisz. Przyciski numeryczne mają wartość komendy równą swojej cyfrze.

Zasada działania dekodera opiera się na pomiarze czasu pomiędzy zmianami stanu na wyjściu odbiornika. Do pomiaru czasu użyty jest timer (w tym przykłądzie jest to licznik 16 bitowy, pracujący z częstotliwością 1MHz. Jeden takt licznika odpowiada zatem jednej mikrosekundzie.

Dekodowanie realizowane jest w przerwaniu. Tutaj istnieje kilka alternatyw:

– można użyć przerwania zewnętrznego (np. INT0, jak w przykładzie). Ustawić wyzwalanie  obydwoma zboczami. W obsłudze przerwania odczytywana jest (na samym początku) wartość rejestru licznika TCNT. Sposób jest skuteczny, aczkolwiek należy pamiętać, że jeśli wystąpią znaczne opóźnienia w obsłudze przerwania (np w danej chwili wykonywana jest obsługa innego przerwania) wartość w TCNT będzie nieaktualna i sygnał nie będzie poprawnie dekodowany.

– można (zalecam) użyć wejścia przechwytującego licznika ICP. Wówczas w momencie wystąpienia przerwania wartość licznika zostanie zatrzaśnieta w odpowiednim rejestrze sprzętowo. Ten sposób powinien zapewnić najwyższą dokładność pomiaru. Wejście ICP da się ustawić tylko  dla jednego zbocza – zatem po wystąpieniu przerwania należy je przestawić na przeciwne.

– można użyć przerwania PCINT – to przerwanie pracuje domyślnie na obu zboczach – technika jak dla INTx.

Sposobów zapewne jest więcej – wybór w zależności od dostępności zasobów mikrokontrolera. Prezentowany przykład jest dla ATmegi8, aczkolwiek właściwie każdy AVR powinien sobie poradzić z dekodowaniem tego sygnału.

Schemat podłączenia; Ponieważ jest prosty i nic nowego nie da się już tutaj wymyśleć, prezentuję schemat z noty katalogowej odbiornika TSOP-1736 firmy Vishay:

Podłączenie TSOP-1736

Podłączenie TSOP-1736, karta katalogowa

Elementy oznaczone gwiazdką są do pewnego stopnia opcjonalne (wejście można podciągnąć wewnętrznym pull-upem w AVR, zasilanie i tak powinno być własciwie odfiltrowane), aczkolwiek zastosowanie się do zaleceń producenta z pewnością pomoże uniknąć potencjalnych problemów.

Czas zatem spojrzeć na kod, ideą jest dekodowanie sygnału w przerwaniu – po spełnieniu określonych warunków (długość impulsu i/lub stan linii) ustawiany jest kolejny stan dekodera i następuje powrót z przerwania. W przypadku niespełniania warunków, dekoder jest resetowany – wraca na początek. Pomiędzy zmianami sygnału główna pętla programu może realizować inne zadania. Jak wspominałem połówka bitu trwa ponad 400 mikrosekund i procesor AVR nawet z zegarem 1MHz może calkiem sporo w tym czasie zrobić.

#include <avr/io.h>
#include <avr/interrupt.h> // include interrupt support
#include "RC6.h"
#define S_L 320
#define S_H 590
#define L_L 750
#define L_H 1150

Najpierw deklaracje. Definiowane są długości trwania sygnału. I tak S_H i S_L oznaczają minimlany i maksymalny czas trwania połowy bitu (standardowo jest to 444us). Definicje są w us – z racji, że odpowiada to jednemu „cyknięciu” licznika, który tutaj pracuje z f=1MHz.

L_L i L_H odpowienio oznaczają długość całego bitu. Pewna tolerancja wynika z: zakłoceń, niedoskonałości pilota lub odbiornika, niedokładności zegara mikrokontrolera (podany przykład działa doskonale na oscylatorze RC).

Parametry te należy dostosować do szybkości licznika  w projekcie.

Poniżej deklaracja zmiennej enumeracyjnej RC6state, przechowującej stan dekodera, a takze pozostałych zmiennych, które mogą nas interesować.

 volatile enum RC6_State {IDLE, LEADER1, LEADER2, S1, S2, H1, H2,
 H3, H4, H5, T1, T2, CR1} RC6state;
volatile uint8_t RC6_ready;
volatile uint8_t RC6_Control;
volatile uint8_t RC6_Command;

void RC6_idle(uint8_t *half_bit, uint8_t *RC6_Command_r, 
uint8_t *RC6_bits_cntr, uint8_t *trailer)
{
 //TCCR1B &= ~_BV(ICES1);
 RC6state = IDLE;
 *half_bit = 0;
 *RC6_bits_cntr = 0;
 *trailer = 0;
 *RC6_Command_r = 0;
 RC6_Control = 0;
}

Zmienna RC6_ready będzie ustawiana na wartość rożną od false, kiedy uda się poprawnie odebrać ramkę RC6.

Zmienne Command i Control będą przechowywały odpowiednio adres urzadzenia i kod wciśniętego przycisku. Zmienne globalne – dostepne poza przerwaniem. Zwracam uwagę na słowo kluczowe volatile. Wskazuje ono kompilatorowi, żeby nie optymalizował zmiennej – optymalizacja zmiennych modyfikowanych w przerwaniach może spowodowac pominięcie ich w procedurze przerwań i w rezultacie błędne działanie programu.

Funkcja RC6_idle powoduje zresetowanie dekodera, jeśli w czasie odbierania wystąpi sygnał nie mieszczący się standardzie.

Procedura obsługi przerwania:

ISR(INT0_vect)
{
 static uint8_t RC6_Command_r;
 static uint8_t RC6_Command_p = 99;
 static uint8_t half_bit;
 static uint8_t RC6_bits_cntr;
 static uint8_t toggle;
 static uint8_t toggle_p;
 static uint16_t ICR1_P = 0;
 uint16_t tmp = TCNT1;
uint16_t length = tmp - ICR1_P;
ICR1_P = tmp;

Deklaracja zmiennych statycznych. W tym momencie istotna jest zmienna ICR1_P przechowująca stan licznika z poprzedniego przerwania. Do zmiennej tmp wpisywana jest wartość licznika zaraz po przejściu do procedury obsługi przerwania.

 if(RC6state == IDLE)
 {
 if (length > 2500 && length < 2880 && bit_is_set(PIND,2)) 
{RC6state = LEADER1;
return; } 
else 
RC6_idle(&half_bit, &RC6_Command_r, &RC6_bits_cntr, &toggle); }

Stan po odebraniu początku rozbiegówki (LEADER1). Sprawdzana jest długość impulsu i stan wejścia. Teraz test poprawności drugiej części rozbiegówki.

 if(RC6state == LEADER1)
 {
if (length > L_L && length < L_H && bit_is_clear(PIND,2)) 
{RC6state = LEADER2; return;}
 else 
RC6_idle(&half_bit, &RC6_Command_r, &RC6_bits_cntr, &toggle);
 }

W tym momencie jesteśmy po odebraniu LEADERa. Stan LEADER2 trwa w czasie odbioru pierwszej połowy bitu Start.

Teraz spodziewamy się sygnału krótkiego (pół bitu), po którym wystąpi stan wysoki.

if (RC6state == LEADER2)
 {
if(length > S_L && length < S_H && bit_is_set(PIND,2))
 {RC6state = S1; return; } 
else 
RC6_idle(&half_bit, &RC6_Command_r, &RC6_bits_cntr, &toggle); }

Stan wysoki musi trwać okres całego bitu (przejście z 1 – bit startu na pierwsze 0 bitów trybu):

  if (RC6state == S1) 
{ if(length > L_L && length < L_H && bit_is_clear(PIND,2)) 
{ RC6state = S2; return; } 
else 
RC6_idle(&half_bit, &RC6_Command_r, &RC6_bits_cntr, &toggle);}

W tym momencie spodziewamy się trzech zer. Oznacza to przerwania co pół bitu, kolejno:

 if(RC6state == S2)
 {
if(length > S_L && length < S_H && bit_is_set(PIND,2))
 {RC6state = H1; return;}
 else 
RC6_idle(&half_bit, &RC6_Command_r, &RC6_bits_cntr, &toggle);
 }
if(RC6state == H1)
 {
 if(length > S_L && length < S_H && bit_is_clear(PIND,2))
 { RC6state = H2; return; }
 else 
RC6_idle(&half_bit, &RC6_Command_r, &RC6_bits_cntr, &toggle);
 }
if(RC6state == H2)
 {
if(length > S_L && length < S_H && bit_is_set(PIND,2))
 {RC6state = H3; return;}
 else 
RC6_idle(&half_bit, &RC6_Command_r, &RC6_bits_cntr, &toggle);
 }
if(RC6state == H3)
 {
 if(length > S_L && length < S_H && bit_is_clear(PIND,2))
 { RC6state = H4; return; }
 else 
RC6_idle(&half_bit, &RC6_Command_r, &RC6_bits_cntr, &toggle);
 }

Stan H4 to jest druga połowa ostatniego bitu trybu. W tym momencie w zależności od stanu bitu toggle:

– jeśli będzie równy 0 przerwanie wystąpi po okresie połowy bitu (czyli na początku bitu toggle) – będzie to stan H5

– jeśli będzie równy 1 przerwanie nastapi w środku bitu toggle, tj. po okresie połowy normalnego bitu + okres połowy bitu toggle (2x dłuzszy), zatem po 1,5 okresu normalnego bitu – będzie to stan T1.

W tym momencie ustawiana jest wartość bitu toggle.

if(RC6state == H4) 
{
if(length > S_L && length < S_H && bit_is_set(PIND,2))
{ RC6state = H5; toggle = 0; return; }
else if 
(length > S_L+L_L && length < S_H + L_H && bit_is_set(PIND,2))
{ RC6state = T1; toggle = 1; return; }
else 
RC6_idle(&half_bit, &RC6_Command_r, &RC6_bits_cntr, &toggle);
}

W stanie H5 – początek bitu toggle spodziewamy się tylko długiego okresu (połowa bitu toggle):

if(RC6state == H5)
{
if(length > L_L && length < L_H && bit_is_clear(PIND,2))
{ RC6state = T1; return; }
else 
RC6_idle(&half_bit, &RC6_Command_r, &RC6_bits_cntr, &toggle);
}

Stan T1 – środek bitu toggle.

W tym momecie jeśli sygnał trwał okres 1 bitu – jesteśmy na początku pierwszego bitu adresu (stan T2).

Jeśli trwał 1,5 bitu –  w środku bitu adresu (stan CR1). W tym momencie zaczynamy dekodowanie – należy zacząć odczyt w połowie pierwszego bitu adresu. Długość przypisujemy na sztywno – ponieważ bez wychodzenia z przerwania przechodzimy już do zliczania bitów. Aktualny stan odpowiada pierwszemu bitowi adresu, a dla zachowania generyczności i niezawodności mechanizmu dla kolejnych przerwań nie rozpoznaje on długości 1,5 bitu. (można by było ustawić długość krótkiego impulsu i nie ustawiać haflbit, wówczas generyczna obsługa dla stanu CR1 zapewnia to sama).

if(RC6state == T1){
if(length > L_L && length < L_H)
{
RC6state = T2; return;
}
else if (length > S_L + L_L && length < S_H + L_H)
{ RC6state = CR1; half_bit = 1; length = 800; }
else 
RC6_idle(&half_bit, &RC6_Command_r, &RC6_bits_cntr, &toggle);
}

Jeśli dekoder był na początku pierwszego bitu adresu – w tym momencie przechodzi na jego środek – stan CR1. Dla spójności kodu impuls jest „zamieniany” na długi i ustawiana jest zmienna halfbit (jest to wyłącznie dla czytelności – tych instrukcji mogłoby nie być).

if(RC6state == T2)
{
if(length > S_L && length < S_H )
{ RC6state = CR1; half_bit = 1; length = 800; }
else 
RC6_idle(&half_bit, &RC6_Command_r, &RC6_bits_cntr, &toggle);
}

Zmienna half bit ustawiana jest na 1 w połowie bitu. Przerwanie w połowie bitu wystąpi zawsze –  na początku tylko przy zmianie stanu. Zatem przerwanie po połowie bitu zmienia stan zmiennej half bit, przerwanie po okresie całego bitu nie.

Dekoder nie zmienia już stanu (niejako stan jest kombinacją wartości zmiennej RC^state i licznika bitów). Jeśli przerwanie nastąpi w połowie bitu – sprawdzany jest stan linii, wpisywany do zmiennej a kolejny bit zliczany .

Jeśli bitów będzie 16 – oznacza to pomyślne odebranie całej ramki.

if(RC6state == CR1)
{
if(length > S_L && length < S_H )
{ half_bit ^= 1; }
else if(length > L_L && length < L_H );
else 
RC6_idle(&half_bit, &RC6_Command_r, &RC6_bits_cntr, &toggle);

if(half_bit && RC6_bits_cntr >=8)
{
RC6_Command_r = (RC6_Command_r<<1);
if(bit_is_set(PIND,2)) RC6_Command_r++;
RC6_bits_cntr++;
}

if(half_bit && RC6_bits_cntr <8)
{
RC6_Control = (RC6_Control<<1);
if(bit_is_set(PIND,2)) RC6_Control++;
RC6_bits_cntr++;
}

Tutaj znajdują się operacje wykonywane po odebraniu ramki.

W tym przypadku rejestrowane są tylko osobne przyciśniecia przycisku, ustawiana jest zmienna RC6_ready.

W pętli głownej po sprawdzeniu tej zmiennej warto ją wyzerować i zresetować stan dekodera.

if(RC6_bits_cntr == 16)

 {RC6_Command = RC6_Command_r;
 if((toggle != toggle_p && RC6_Command == RC6_Command_p) || 
(RC6_Command != RC6_Command_p))
 {
 toggle_p = toggle;
 RC6_Command_p = RC6_Command;
 RC6_ready = 1;
 }
}
return;
}

Dla formalności funkcja main, w któej następuje ustawienie timera i przerwań.

main()
{
TCCR1B = 1;	//timer 1 no prescaling
MCUCR  = 1;
GICR = 0b01000000;

sei();

while(1)
{}

}

Powyższy przykład nie zawiera oczywiście  obsługi wykorzystania zdekodowanego sygnału. Pomysły są oczywiście dowolne, piloty RC6 są popularne i tanie. Kod mozna jeszcze zapewne nieco zoptymalizować, można wyciągnąć zmienne toggle i command poza przerwanie, jeśli ich wartość będzie istotna w programie. Zachęcam do wykorzystania przykładu we własnych projektach.

W przygotowaniu wersja na STM32 – planowana raczej szybko, w dalszej przyszłości niewykluczone, że pojawi się wersja dla MSP430.

RC6 – kod

Ten wpis został opublikowany w kategorii Bez kategorii. Dodaj zakładkę do bezpośredniego odnośnika.