Kursy • Poradniki • Inspirujące DIY • Forum
Program reagujący na przychodzące dane
Jak wiadomo istnieją dwie szkoły obsługi układów peryferyjnych mikrokontrolera. Przez tak zwany polling, czyli sprawdzanie odpowiedniej flagi w głównej pętli programu oraz przez system przerwań.
W przypadku interfejsu USART pierwszy sposób nie jest zbyt praktyczny, dlatego zajmiemy się obsługą przerwaniową. Zdarzeniem wyzwalającym przerwanie będzie odebranie wiadomości. Reakcją układu będzie wysłanie wiadomości zwrotnej. Program działający w ten sposób jest nieprzyzwoicie wręcz prosty:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#include <avr/io.h> #include <avr/interrupt.h> //ustawiona częstotliwość - 18432000 //pomocnicze stałe #define USART_BAUDRATE 9600 #define BAUD_PRESCALE (((F_CPU / (USART_BAUDRATE * 16UL))) - 1) void usart_init(void) //funkcja inicjalizująca usart { UBRRH = (BAUD_PRESCALE >> 8); //wpisanie starszego bajtu UBRRL = BAUD_PRESCALE; //wpisanie mlodszego bajtu //UCSRA bez zmian - 0x00 UCSRB = (1<<RXCIE)|(1<<RXEN)|(1<<TXEN); //aktywne przerwanie od odbioru oraz zmiana trybu działania pinów D0 i D1 UCSRC = (1 << URSEL) | (1 << UCSZ0) | (1 << UCSZ1); //praca synchroniczna, brak kontroli parzystości, 1 bit stopu, 8 bitów danych } int main(void) { PORTD = 0x02; //pullup na TXC usart_init(); sei(); //aktywacja przerwań for(;;); } ISR(USART_RXC_vect) //przerwanie od odbioru danej { static char a; //zmienna pomocnicza a = UDR; //zapis odebranej danej a ^= 0xff; //operacja bitowa XOR UDR = a; //wysłanie danej zwrotnej } |
Moja Atmega działa na zewnętrznym kwarcu 18,432MHz. Przed wgraniem programu należy pamiętać o odpowiedniej modyfikacji fuse bitów. Dzięki wpisaniu stałej baudrate oraz zastosowaniu wzoru na baud prescale, powyższy kod jest niezwykle prosty do przystosowania dla innych częstotliwości.
Dalsza część kodu jest podzielona na 3 części - konfiguracja USARTa, główna pętla i przerwanie od Rx. Komentarze do kodu tłumaczą działanie poszczególnych części. Zwracam tutaj uwagę na zmienną pomocniczą a, która jest niezbędna, aby dokonać jakiejś obróbki otrzymanej wiadomości. Drugą, wyraźnie widoczną w tym kodzie rzeczą jest podwójna rola rejestru UDR, przechowującego raz odbieraną, a raz wysyłaną informację w zależności od tego, po której stronie znaku równości się znajduje.
Kolejną ważną obserwacją jest to, że zapis UDR = a jest wystarczający do wysłania wiadomości i dzięki niemu nie trzeba już wykonywać żadnych dodatkowych operacji. Aby przetestować działanie programu, wystarczy użyć terminala z poprzedniej części artykułu.
Program przesyłający dane do komputera
Innym prostym, aczkolwiek niezwykle przydatnym (szczególnie przy debugowaniu, testowaniu czy identyfikacji układów) programem jest praca USARTa, ograniczająca się do samego wysyłania danych.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
#include <avr/io.h> #include <avr/interrupt.h> //ustawiona częstotliwość - 18432000 //pomocnicze stałe #define USART_BAUDRATE 9600 #define BAUD_PRESCALE (((F_CPU / (USART_BAUDRATE * 16UL))) - 1) void usart_init(void) //funkcja inicjalizująca usart { UBRRH = (BAUD_PRESCALE >> 8); //wpisanie starszego bajtu UBRRL = BAUD_PRESCALE; //wpisanie mlodszego bajtu //UCSRA bez zmian - 0x00 UCSRB = (1<<TXEN); //zmiana trybu działania tylko dla D1 UCSRC = (1 << URSEL) | (1 << UCSZ0) | (1 << UCSZ1); //praca synchroniczna, brak kontroli parzystości, 1 bit stopu, 8 bitów danych } void timer0_init(void) { //praca w przerwaniu od przepelnienia, preskaler 256, wysylanie danych co 3,5ms TCCR0 |= (1<<CS02); TIMSK |= (1<<TOIE0); } int main(void) { PORTD = 0x02; //pullup na TXC PORTB = 0xFF; //port b -wejscia z pullupem usart_init(); timer0_init(); sei(); //aktywacja przerwań for(;;); } ISR(TIMER0_OVF_vect) { UDR = PINB; } |
Tym razem nasz program potrzebował tylko pinu Tx interfejsu, aby wysyłać dane do komputera. Jego zadaniem było wysyłanie danych co określony czas. Z tego powodu właśnie zostało użyte przerwanie od timera.
Jak widać przerwanie od USARTa nie było tu w ogóle potrzebne. Do komputera wysyłane były dane odczytane z portu B mikrokontrolera. Do czego mogłaby się przydać podobna aplikacja? Wyobraźmy sobie, że do portu B podłączone są czujniki linefollowera, a zamiast pustej pętli, wykonuje się algorytm PID i sterowanie PWMem. W ten prosty sposób możemy otrzymać informacje o odczytach czujników.
Poza tym nic nie stoi na przeszkodzie, żeby zamiast wartości PINB wysyłać na przykład wartość PWM czy odczyty ADC. Tego typu informacje o działaniu układów mikrokontrolera są wprost niezastąpione w procesie tworzenia programów. Ten tryb pracy jest również bardzo przydatny, jeśli nasz układ ma zajmować się mierzeniem i archiwizowaniem jakichś danych pomiarowych. Na przykład: co określony przedział czasu ma wysyłać do komputera analogowy odczyt z czujnika temperatury. Po stronie komputera można bez problemu dopisać odpowiedni kod, zapisujący te dane do plików, robiący wykresy itp.
Podobny program, tylko napisany w asemblerze zastosowałem do dekodowania sygnału z pilota. Był mi potrzebny do odtworzenia ramki danych odczytywanej przez mikrokontroler. Więcej szczegółów w odpowiednim artykule: http://www.forbot.pl/forum/topics20/komunikacja-jak-przystosowac-domowego-pilota-do-wlasnych-celow-vt5994.htm
Buforowanie danych
Podczas wysyłania informacji może się okazać, że niektóre dane są gubione. Dzieje się tak dlatego, że są one generowane szybciej niż układ może je wysyłać. W tym wypadku przydatne może okazać się stworzenie programowego bufora czyli struktury FIFO (first in first out, po polsku kolejka). Jest to tablica o zadanej przez nas wartości oraz zmienna, wskazująca koniec tablicy.
Dopisanie elementu powoduje inkrementację wskaźnika, natomiast wysłanie powoduje przesunięcie wszystkich elementów tablicy na wyższą pozycję oraz dekrementację wskaźnika.
Alternatywą dla takiej kolejki może być bufor cykliczny, w którym mamy wskaźnik na pierwszy element i miejsce za ostatnim elementem, w które można zapisać nową daną. Wysłanie danych powoduje zwiększenie wartości wskaźnika początku, natomiast dopisanie zwiększa końcowy wskaźnik. Struktura jest pusta jeśli oba wskaźniki są sobie równe, poza tym niezbędne jest ograniczenie, aby dodawanie danych nie nadpisywało początku.
Należy pamiętać, że dopisanie danej do bufora nie powoduje bezpośrednio wysłania. Trzeba zawsze sprawdzić, czy bufor jest pusty i jeśli tak, wysłać dane w sposób klasyczny. Zastosowanie tego typu struktury pozwala na zatrzymanie w pamięci informacji, które normalnie byłyby tracone i dosłanie ich później, gdy nowe dane przestaną dochodzić.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
#include <avr/io.h> #include <avr/interrupt.h> //ustawiona częstotliwość - 18432000 //pomocnicze stałe #define USART_BAUDRATE 9600 #define BAUD_PRESCALE (((F_CPU / (USART_BAUDRATE * 16UL))) - 1) #define ROZMIAR 16 //deklaracja i inicjalizacja bufora typedef struct { unsigned char tablica[ROZMIAR]; unsigned char poczatek; unsigned char koniec; } bufor_cykliczny; volatile bufor_cykliczny bufor = {.poczatek=0, .koniec=0}; //poczatkowe wartosci dla wskaznikow //dodatkowe funkcje bufora void bufor_dopisz(unsigned char data) { if(bufor.koniec+1 < ROZMIAR) //standardowy przypadek { if(bufor.koniec+1 != bufor.poczatek) {bufor.tablica[bufor.koniec++]=data;} //jeżeli jest miejsce w buforze to zapisz, aktualizuj koniec, jeżeli nie ma miejsca to nic nie rób } else //przypadek gdy trzeba wrócić do 0 { if(bufor.poczatek != 0) {bufor.tablica[15]=data; bufor.koniec=0;} } } void bufor_wyslij(void) { if(bufor.poczatek != bufor.koniec) //jeżeli bufor nie jest pusty { UDR = bufor.tablica[bufor.poczatek++]; //wyślij daną z początku i zwiększ wskaźnik } } void usart_init(void) //funkcja inicjalizująca usart { UBRRH = (BAUD_PRESCALE >> 8); //wpisanie starszego bajtu UBRRL = BAUD_PRESCALE; //wpisanie mlodszego bajtu //UCSRA bez zmian - 0x00 UCSRB = (1<<TXEN)|(1<<UDRE); //zmiana trybu działania pinu D1, przerwanie gdy rejestr nadawczy jest pusty (USART data register empty) UCSRC = (1 << URSEL) | (1 << UCSZ0) | (1 << UCSZ1); //praca synchroniczna, brak kontroli parzystości, 1 bit stopu, 8 bitów danych } void timer0_init(void) { //praca w przerwaniu od przepelnienia, preskaler 256, wysylanie danych co 3,5ms TCCR0 |= (1<<CS02); TIMSK |= (1<<TOIE0); } int main(void) { PORTD = 0x02; //pullup na TX PORTB = 0xFF; //port b -wejscia z pullupem usart_init(); timer0_init(); sei(); //aktywacja przerwań for(;;); //nieskonczona petla } ISR(TIMER0_OVF_vect) { static unsigned char b; b = PINB; bufor_dopisz(b); if(bufor.poczatek+1==bufor.koniec) bufor_wyslij(); //jeżeli przed dopisaniem bufor był pusty, wyślij wiadomość z bufora } ISR(USART_UDRE_vect) { bufor_wyslij(); } |
Powyższy kod to modyfikacja poprzedniego przykładu - w praktyce nie zmieniająca jego funkcjonalności. Możemy tutaj jednak zobaczyć strukturę bufora cyklicznego oraz funkcje dopisywania i wysyłania.
Dodatkowo, doszło przerwanie od zwolnienia rejestru, z którego są wysyłane dane, a w nim wysyłana jest jedna wiadomość z bufora. Zmodyfikowane zostało także przerwanie od timera, żeby wysyłanie się nie zawieszało, kiedy nie dochodzą do bufora nowe dane. W przykładzie bufor ma 16 bajtów, ale wartość tę można łatwo zmienić dzięki zadeklarowaniu stałej na początku.
Bufory mają szerokie zastosowanie w transmisji danych. Można je stosować również przy odbiorze. Sprawdzają się idealnie, kiedy raz na jakiś czas musimy szybko przesłać dużą porcję informacji, a łącze nie jest wykorzystywane pomiędzy pakietami.
Po stronie komputera klasa Visual C# SerialPort ma zaimplementowane buforowanie danych wysyłanych oraz odbieranych. Operacje na buforach można tam wykonywać, traktując je jako Stringi, tablice bajtów lub tablice charów. Więcej szczegółów można znaleźć na msdn.
Wydawanie poleceń
Popularną konfiguracją jest układ, w którym komputer wysyła polecenia sterujące, natomiast mikrokontroler zgodnie z nimi zmienia swój tryb pracy, czy wysyła informacje zwrotne. Można to bardzo prosto zrealizować za pomocą zdefiniowanych stałych oraz polecenia sterującego switch w funkcji przerwania od odebranej wiadomości.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
#include <avr/io.h> #include <avr/interrupt.h> //ustawiona częstotliwość - 18432000 //pomocnicze stałe #define USART_BAUDRATE 9600 #define BAUD_PRESCALE (((F_CPU / (USART_BAUDRATE * 16UL))) - 1) //stale do obslugi switchy #define RESETUJ 0x00 #define ZAPIS 0x01 #define ODCZYT 0x02 #define WYSWIETLAJ 0x03 #define PRZESTAN_W 0x04 #define AAA 0x01 #define BBB 0x02 #define CCC 0x03 volatile unsigned char a,b,c, stan=0, wysw=0; void usart_init(void) //funkcja inicjalizująca usart { UBRRH = (BAUD_PRESCALE >> 8); //wpisanie starszego bajtu UBRRL = BAUD_PRESCALE; //wpisanie mlodszego bajtu //UCSRA bez zmian - 0x00 UCSRB = (1<<TXEN)|(1<<RXEN)|(1<<RXCIE); //zmiana D0 i D1, aktywacja przerwania RX UCSRC = (1 << URSEL) | (1 << UCSZ0) | (1 << UCSZ1); //praca synchroniczna, brak kontroli parzystości, 1 bit stopu, 8 bitów danych } void timer0_init(void) { TCCR0 = (1<<CS02); TIMSK |= (1<<TOIE0); } int main(void) { PORTD = 0x02; //pullup na TXC PORTB = 0xFF; //pullup na port b usart_init(); timer0_init(); sei(); //aktywacja przerwań for(;;); //nieskonczona petla } ISR(TIMER0_OVF_vect) { if(wysw) UDR = PINB; //jeżeli zmienna wysw jest różna od 0 - stan pinb jest wysyłany do komputera } ISR(USART_RXC_vect) //przerwanie od odbioru danej { static unsigned char odebrana; //zmienna pomocnicza odebrana = UDR; //zapis odebranej danej UDR = odebrana; if(stan==0) //stan domyslny { switch(odebrana) { case(RESETUJ): {a=0; b=0; c=0; wysw=0; UDR=0xFF;} case(ZAPIS): {stan=1; UDR=0xFF;} case(ODCZYT): {stan=2; UDR=0xFF;} case(WYSWIETLAJ): {wysw=1; UDR=0x00;} case(PRZESTAN_W): {wysw=0; UDR=0x00;} } } if(stan==1) //stan zapisu { switch(odebrana) { case(AAA): {UDR=0xFF; stan=3;} case(BBB): {UDR=0xFF; stan=4;} case(CCC): {UDR=0xFF; stan=5;} } } else if(stan==2) //stan odczytu { switch(odebrana) { case(AAA): {UDR=a; stan=0;} case(BBB): {UDR=b; stan=0;} case(CCC): {UDR=c; stan=0;} } } else if(stan==3) //stan zapisu zmiennej a { a=odebrana; UDR=0xFF; stan=0; } else if(stan==4) //stan zapisu zmiennej a { b=odebrana; UDR=0xFF; stan=0; } else if(stan==5) //stan zapisu zmiennej a { c=odebrana; UDR=0xFF; stan=0; } } |
Program w przerwaniu odczytuje otrzymaną daną, a następnie - w zależności od jej wartości - podejmuje odpowiednią akcję. Można odczytać lub zmienić wartość jednej ze zmiennych a, b lub c, albo rozpocząć lub zakończyć przesyłanie aktualnych wartości portu B.
Zmienna pomocnicza stan pozwala na budowanie kolejnych poziomów, na których może znajdować się program. Na każdej gałęzi wykonywane są inne polecenia. W ten sposób można zbudować nawet skomplikowane drzewa zależności. Oczywiście kod zaprezentowany powyżej jest tylko przykładem i nic wielkiego nie robi. Pomaga jedynie zrozumieć ogólną zasadę działania.
Podsumowanie
Mam nadzieję, że przedstawione tutaj przykłady dobrze pokazują podstawy obchodzenia się z USARTem. Doświadczeni programiści pewnie nie znaleźli tutaj nic ciekawego. Myślę jednak, że początkujący będą mieli ułatwione zadanie dzięki tym kilku przykładom.
Artykuł był pisany w pośpiechu, a sprzęt na którym robiłem testy wiele już przeszedł, dlatego możliwe, że wkradły się jakieś błędy. Jeśli coś nie będzie działać, dajcie znać, a postaram się wyłapać i poprawić ewentualne błędy. Po raz kolejny artykuł okazał się za mały, aby wyczerpać temat.
W dalszym ciągu brakuje opisu protokołu komunikacyjnego z prawdziwego zdarzenia czy dedykowanego programu w C# do obsługi konkretnego układu przy pomocy USARTa. Poza tym możnaby poruszyć temat bootloaderów. Jeśli będę miał więcej czasu oraz będzie takie zapotrzebowanie, prawdopodobnie powstaną kolejne części.
To nie koniec, sprawdź również
Przeczytaj powiązane artykuły oraz aktualnie popularne wpisy lub losuj inny artykuł »
avr, bufor, komunikacja, odbieranie, przerwania, RS232, rxd, txd, USART, wysyłanie
Trwa ładowanie komentarzy...