KursyPoradnikiInspirujące DIYForum

Pochwal się swoim elektronicznym projektem DIY i odbierz 50 zł rabatu do sklepu Botland. Sprawdź szczegóły »

Port szeregowy i interfejs USART – #2

Port szeregowy i interfejs USART – #2

Poprzednia część praktycznie w całości została poświęcona tworzeniu programu od strony komputera. Teraz przyszła pora zająć się mikrokontrolerem.

W tej części dokonałem przeglądu prostych technik programistycznych związanych z pisaniem programu na AVRa.

« Poprzedni artykuł z serii

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:

#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.

#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ć.

#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.

#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.

« Poprzedni artykuł z serii

avr, bufor, komunikacja, odbieranie, przerwania, RS232, rxd, txd, USART, wysyłanie

Trwa ładowanie komentarzy...