logo elektroda
logo elektroda
X
logo elektroda
REKLAMA
REKLAMA
Adblock/uBlockOrigin/AdGuard mogą powodować znikanie niektórych postów z powodu nowej reguły.

Algorytmy odbioru danych przez UART w ATtiny2313 - buforowanie czy bajt po bajcie?

ergovision 08 Lis 2007 23:48 3259 7
REKLAMA
  • #1 4461036
    ergovision
    Poziom 11  
    Posty: 38
    Pomógł: 1
    Witam!

    Mam problem z algorytmem odbioru ciągu bajtów przez UART mojego AVRa (ATtiny2313). Prosiłbym o podsuniecie mi kilku koncepcji z wykorzystaniem bufora. Nie zależy mi na kodzie źródłowym C (chociaż naturalnie nim nie pogardzę, zwłaszcza jakimś okomentowanym przykladzikiem ;) ), bardziej interesuje mnie sam schemat działania. Nie wiem np. czy czekać na ramkę az przyjdzie w całości i dopiero wtedy ją przetwarzać, czy może przetwarzać ją w momencie kiedy przychodzi - "bajt by bajt". O ile nie będzie to problemem to proszę również o określenie wad i zalet poszczególnych algorytmów (rozwiązań).

    Z góry dziękuje za wszelkie sugestie i pomoc.

    Pozdrawiam
  • REKLAMA
  • #2 4461193
    kamyczek
    Poziom 38  
    Posty: 3994
    Pomógł: 394
    Ocena: 573
    Kolega poczyta sobie o przerwaniach które są zgłaszane przez UARTA jeśli je odpowiednio skonfigurujemy po każdym odebranym bajcie wystąpi przerwanie reszta to już dowolna interpretacja bufora
  • REKLAMA
  • #3 4461196
    ergovision
    Poziom 11  
    Posty: 38
    Pomógł: 1
    przerwania znam, niemniej wlaśnie nie mam koncepcji na sam bufor, niby tablica, alemoze zrobic z niego liste jednokierunkową ?, jak go odczytywac, czy po bajcie czy dopiero zrzucić całą ramkę jak przyjdzie? oznaczac koniec i poczatek ramki?, jak obsluzyc blad w postaci np zaginionego bajtu? Jak daleko posunac sie w kodzie obslugi przerwania zeby nie trwalo za dlugo?

    moj procek jest taktowany wewnetrznym oscylatorem 8Mhz wyliczylem(nie wiem czy dobrze), ze miedzy bajtami mam jakieś 2000 cykli zegara. (UART 38400 b/s 8N1)

    mam nadzieje ze ktoś mi naświetli lekko temat.
  • Pomocny post
    #4 4461275
    shg
    Poziom 35  
    Posty: 2289
    Pomógł: 339
    Ocena: 135
    Bufor cykliczny - tablica o długości N bajtów.
    Dwa wskażniki na elementy tegoż. Pierwszy wskażnik do odczytu (wo), drugi do zapisu (wz). Początkowo oba wskażniki ustawione na początek bufora.
    Zapis:
    Procedura przerwania po każdym odebranym bajcie zapisuje go w miejscu wskazanym przez wz. Następnie wz jest inkrementowany i jeżeli wskazuje na element tablicy (bufora) N+1, to ustawiany jest na pierwszy element.
    Odczyt:
    Sprawdzić, czy bufor nie jest pusty (wo != wz), odczytać bajt na który wskazuje wo. Inkrementacja wo tak samo jak wz powyżej.
    Zalety - bardzo wydajny kod, możliwość banalnie prostej realizacji "zapętlania" bufora, gdy jego długość jest potęgą dwójki - po każdej inkrementacji operacja AND, oczywiście to w wypadku, gdy wskaźniki (a dokładniej to najmniej znaczące bity adresu, które biorą udział w adresowaniu bufora) będą zaczynały się od zera, albo będą indeksami tablicy. Można to też obejść w prosty sposób, odjąć adres pierwszego elementu, wykonać AND (N-1), dodać adres pierwszego elementu. Da się to zmieścić to w 14, lub mniej, instrukcjach (łącznie z zapisem i odczytem z pamięci), to i tak chyba szybciej wyjdzie niż porównanie i skok warunkowy.
    Wady bufora cyklicznego - marnuje się jeden bajt ze względu na dwuznaczność sytuacji wo=wz (bufor pusty, albo N elementów w buforze), ale to się da akurat obejść stosując flagi.
    Bufor może być opróżniany przez funkcję wywoływaną z głównej pętli programu. Przerwanie może na przykład ustawiać jakąś flagę, gdy odebrany bajt = 0, czy znak końca linii.
    Można też przetwarzać na bieżąco, po jednym bajcie.
    To, czy wydajniejsze jest przetwarzanie po jednym bajcie, czy całego bufora, to już zależy od konkretnego zastosowania. Termina robiłem tak, żę przychodzące znaki obsługiwane były na bieżąco -echo i backspace, natomiast to co zostało odebrane było przetwarzane w całości - procedura sprawdzała, czy w buforze znajduje się znak końca łańcucha (w przerwaniu LF zamieniany był na zero), jeżeli tak, to pobierała z bufora cały łańcuch i dalej go przetwarzała.

    Żeby radzić sobie z błędami transmisji należało by użyć jakiegoś protokołu dwukierunkowego. Ramki (pakiety) ponumerować, dodać do nich sumy kontrolne, Określić conajmniej dwa rodzaje ramek - ramki z danymi i ramki kontrolne, te pierwsze to wiadomo, a te drugie miały by służyć do kontroli transmisji - potwierdzenie odbioru, żądanie powtórnego wysłania ramki o danym numerze. Uzgadnianie transmisji albo na zasadzie przesłania informacji o ilości danych przed ich wysłaniem, albo wysłanie ramki kontrolnej ze stosowną informacją po zakończeniu nadawania.
    Pierwsza metoda jest lepsza, bo druga może "zgubić" ostatnie ramki danych w wypadku, gdy ramka nie zostanie rozpoznana przez odbiornik.
    Bezpieczne jest również oczekiwanie przez nadawcę na potwierdzenie odbioru każdej ramki a dopiero potem wysłanie kolejnej, ale spowalnia nieco transmisję. Oczywiście i z tym mozna sobie poradzić wprowadzająć różne sposoby przesyłania danych, na przykład tak jak w USB. Z potwierdzeniem po każdej ramce dla danych o znaczeniu kluczowym i z powtórzeniem ramek na żądanie bez czekania na potwierdzenie przy danych "masowych".

    Z podziałem danych na ramki można zrobić taki trick, że każda ramka będzie zawierała na początku słowo synchronizacji (jakąkolwiek znaną sekwencję). Do momentu napotkania takiej sekwencji procedura wypełniająca bufor (przerwanie) może ignorować przychodzące bajty. Po jej napotkaniu przełącza się w tryb wypełniania bufora. Dla danych należy ustalić maksymalny możliwy odstęp między słowami synchronizacji i użyć bufora o długości dwukrotnie większej. Jest to potrzebne z tego względu, że dane takie same jak słowo synchronizacji mogły by zostać źle zinterpretowane. Od momentu rozpoczęcia wypełniania bufora, "w locie" liczona powinna być suma kontrolna. Informacja od długośc ramki powinna znaleźć się w okolicy jej początku, trzeba też dodać jakąś kontrolę tej wartości, tzn. automatycznie odrzucać ramki o nieprawidłowej (większej od możliwego minimum i większej od przyjętego maksimum) długości. W miejscu spodziewanego końca ramki powinna znaleźć się suma kontrolna danych wysłanych przez nadawcę (ale wcale nie musi, równie dobrze suma ta może znajdować się na początku ramki, kwestia gustu). Sumę obliczoną i odebraną porównuje się.
    Oczywiście tak wyglądało by w przypadku idealnym, a teraz przyczyna posiadania dwukrotnie większego bufora:
    Załóżmy że urządzenie rozpoczęło odbiór (zostało włączone) tuż po chwili gdy nadawca zaczął wysyłać ramkę. Powiedzmy że cały nagłówek "przeleciał" i urządzenie rozpoczęło odbiór od danch, a pierwszą daną okazały się być wartości różne słowu synchronizacji. Jeżeli teraz zawiedzie kontrola długości ramki (przypadkiem tak zinterpretowane dane dadzą sensowny wynik), to urządzenie będzie nadal odbierać bezsensowne dane, być może dane te zakończą się, a wśród nich "zaplącze" się słowo synchronizacji kolejnej ramki.
    Dopiero po odebraniu ilości danych wynikającej z danych zinterpretowanych jako nagłówek będzie możliwe porównanie sumy kontrolnej. Mimo tego odbiór danych nie powinien zostać w tym momencie przerwany. Jeżeli nastąpi niezgodność sumy kontrolnej, to procedura odbioru ma wrócić na początek odebranych danych, przeskoczyć fałszywie zinterpretowane słowo synchronizacji i szukać go w dalszej części strumienia.
    Jedyną sytuacją, gdy możliwe jest przestawienie "odbiornika" w tryb oczekiwania (tj. ignorowania wszystkiego aż do napotkania słowa synchronizacji) jest zdekodowanie poprawnej ramki i pusty bufor odbioru.
    Całość da sięw prosty sposób zrealizować na zasadzie maszyny stanów.
    Dobrze było by żeby transmisja w obie strony wyglądała tak samo. To i tak jeszcze nie koniec problemów, bo mozę się okazać że gdzieś zginie cała ramka, ale o tym już pisałem - oczekiwanie potwierdzenie po każdej nadanej ramce. Do tego mozna (trzeba) by dodać jeszcze maksymalny czas oczekiwania (powiedzmy 0.5s) i ponowne wysłanie ramki po jego przekroczeniu.

    Napiszanie naprawdę dobrego protokołu komunikacji, posiadającego dużą odporność na błędy jest niemałym wyzwaniem, a jeszcze trzeba to upchnąć w taki mały mikrokontroler...

    2000 cykli na bajt to bardzo dużo, można tu upchać nawet kompletny terminal z handshakingiem.
  • REKLAMA
  • #5 4462865
    ergovision
    Poziom 11  
    Posty: 38
    Pomógł: 1
    Czy jest szansza na wiecej szczegółów dot. zapętlania bufora? Wskazniki są ok niemniej to dzialanie AND... , nie do końca rozumiem idee przedstawioną powyżej, niemniej dziękuje za oświecenie :D
  • REKLAMA
  • Pomocny post
    #6 4463803
    shg
    Poziom 35  
    Posty: 2289
    Pomógł: 339
    Ocena: 135
    Idea polega na tym, że wskaźniki krążą po buforze w tym samym kierunku, tak jakby koniec bufora połączony był z początkiem, stąd nazwa "bufor cykliczny". Po dojściu do końca bufora wskaźnik przeskakuje na jego początek. Pierwszy zawsze jest wskaźnik zapisu, za nim podąża wskaźnik odczytu. Jeżeli oba wskazują na to samo miejsce to znaczy że bufor jest pusty (lub w szczególnym przypadku, wymagającym dodatkowej obsługi - bufor całkowicie pełny). Podstawowy warunek jest taki, że wskaźnik odczytu nie może "wyprzedzić" wskaźnika zapisu, bo odczytane zostały by śmieci z lokacji, które miały by być zapisane dopiero w przyszłości, ani też wskaźnik zapisu nie może "zdublować" (a w najprostszym przypadku również "dogonić") wskaźnika odczytu, zawsze musi go wyprzedzać, lub w wypadku pustego bufora, wskaźnik odczytu musi znajdować się tam gdzie wskaźnik zapisu.

    Może tak w postaci kodu najprostszego i niepozbawionego wad:

    #define BUF 64 /* dlugosc bufora */
    char bufor[BUF];
    
    char *wo=bufor; /* wskaznik odczytu */
    char *wz=bufor; /* wskaznik zapisu */
    
    /* funkcja wywolywana przez przerwanie po odebraniu znaku */
    void zapisz_znak(char c) {
     *wz = c;
     wz = ((wz - bufor) & (BUF - 1)) + bufor);
    }
    
    int czytaj_znak(void) {
    int c;
     if(wo != wz) {
      c = *wo;
      wo = ((wo - bufor) & (BUF - 1)) + bufor);
      return c;
     }
     return -1; /* bufor pusty */
    }
    
    (...)
    #define LIN 32 /* dlugosc linii */
    char linia[LIN];
    unsigned char lp; /* aktualna pozycje w buforze linii */
    int c;
    (...)
    
    /* gdzieś w glownej petli programu */
    
    /* czyta kolejne znaki z bufora dopoki nie trafi na koniec lancucha (znak o kodzie 0), lub nie zapelni bufora */
     lp = 0;
     while((c = czytaj_znak()) != 0) {
      if(c == -1) { /* odczytano wszystko z bufora */
       continue; /* sprobuj odczytac znak ponownie */
      }
      linia[lp++] = c;
      if(lp == (LIN-1)) { /* bufor linii pelny */
       break;
      }
     }
     linia[lp] = 0; /* dodaje znacznik konca lancucha na koncu odebranego lancucha */
    


    Z tym AND to jest po prostu operacja modulo, można to na kilka sposobów zapisać:

    1)
    x++;
    if(x == (KONIEC + 1)) {
     x = POCZATEK;
    }


    2)
    x++;
    x %= DLUGOSC;
    


    3)
    x++;
    x &= (DLUGOSC - 1);
    


    Pierwszy przypadek - najprostszy, jeżeli x przekroczy koniec bufora, to wraca na początek. Czasem dobre, na niektórych architekturach wręcz świetnie działa (na przykład na ARM7, która to posiada instrukcje warunkowe, a nie tylko skoki). Skok warunkowy komplikuje też sprawę w systemach posiadających cache instrukcji, tu na szczęście z AVR nie ma problemu.

    Przypadki 2 i 3 (a 2 przynajmniej z wyglądu) nie "psują" nic w cache i w odróżnieniu od pierwszego w postaci takiej jak tu służą raczej do operacji na indeksach tablicy zaczynających się od zera. Jeżeli miały by działać na wskaźnikach, to musiały by zostać zapisane tak jak w kodzie na początku, tak żeby z arytmetycznego punktu widzenia przesunąć najpierw wskaźnik tak, jakby początek obszaru danych zaczynał się od adresu zero, wykonać operację, a potem przywrócić wskaźnik do poprzedniego obszaru (w praktyce oznacza to najpierw odjęcie, a potem dodanie adresu pierwszego elementu tablicy).
    Przypadek 2 jest niestety mało fajny z tego względu że używa dzielenia. Tam jest akurat operator modulo, ale nie zmienia to faktu, że przy okazji liczenia modulo wykonywane jest dzielenie. Jeżeli procesor potrafi je wykonać w sensownym czasie (max. kilka cykli), to może okazać się bardzo przydatny, ponieważ pozwala stosować bufory o dowolnej długości. Jeżeli procesor nie potrafi szybko dzielić, to przy długości bufora będącej potęgą dwójki kompilator może, ale wcale nie musi zoptymalizować to do postaci takiej jak przypadek 3.

    Przypadek 3 opiera sie na zależności:
    a MOD b = a AND (b - 1) <=> b = 2^n
    Dla przykładu:
    a MOD 256 = a AND 255, a MOD 32 = a AND 31.

    W kodzie na początku darowałem sobie rzutowanie, które powinno się tam znaleźć. Kompilator najprawdopodobniej "wypluje" ostrzeżenie o rzutowaniu wskaźnika na typ całkowity.

    Funkcja zapisz_znak() powinna sprawdzać czy jest jeszcze wolne miejsce w buforze. Można różnie, chyba najwydajniej będzie zrobić kopię wskaźnika (wz), zwiększyć ją o 1 (z zapętlaniem) tak jak tam jest, tylko przed zapisem znaku. Jeżeli kopia wz zwiększona i "zapętlona" jest różna od wskaźnika odczytu, to zapisać znak, a wartość kopii zapisać do wz, w przeciwnym wypadku zasygnalizować przepełnienie bufora.

    Przypadek 1 można by też zastosować w kodzie który podałem na początku. gcc w wersji 4 (w najnowszym WinAVR) optymalizuje już dużo lepiej niż wersja 3, więc i może coś konkretnego z tego wyjdzie.
    Po zastosowaniu reszty uwag kod mógłby wyglądać mniej więcej tak:
    #define BUF 64 /* dlugosc bufora */
    char bufor[BUF];
    
    char *wo=bufor; /* wskaznik odczytu */
    char *wz=bufor; /* wskaznik zapisu */
    
    #define FLAGA_PRZEPELNIENIE 1
    volatile unsigned char flagi = 0;
    
    /* funkcja wywolywana przez przerwanie po odebraniu znaku */
    void zapisz_znak(char c) {
    char *kopia_wz;
     kopia_wz = wz+1;
     if(kopia_wz == bufor[BUF]) { /* jezeli wskaznik "wyjechal" za koniec bufora */
      kopia_wz = bufor; /* przeskok na poczatek
     }
     if(kopia_wz == wo) { /* wskaznik zapisu pokryje sie ze wskaznikiem odczytu - przepelnienie bufora */
      flagi |= FLAGA_PRZEPELNIENIE;
      return;
     }
     wz = kopia_wz;
     *wz = c;
    }
    
    int czytaj_znak(void) {
    int c;
    
     if(wo != wz) { /* wskazniki rozne - cos jest w buforze */
      c = *wo;
      cli(); /* patrz opis */
      wo++;
      if(wo == bufor[BUF]) { /* jezeli wskaznik "wyjechal" za koniec bufora, patrz opis */
       wo = bufor; /* przeskok na poczatek */
      }
      flagi &= ~FLAGA_PRZEPELNIENIE; /* bufor juz nie jest przepelniony */
      sei();
      return c;
     }
     return -1 /* bufor pusty */
    }


    Pojawia się jeszcze jeden problem. Jeżeli procedura zapisu do bufora wywoływana jest z przerwania, a odczytu z głównego programu, to może dojść do paskudnej sytuacji w wypadku, gdy procedura odczytu odczyta bajt i zmieni wartość wskaźnika wo. Jeżeli w tym momencie miało nastąpić "zapętlenie" wskaźnika i w tym momencie zostanie wywołane przerwanie, to procedura zapisu otrzyma fałszywą informację o położeniu wskaźnik odczytu. Podobnie z flagą przepełnienia bufora, po uaktualnieniu wskaźnika odczytu może zostać wywołane przerwanie, które zapełni bufor i ustawi flagę, a po wyjściu z przerwania flaga ta zostanie wyzerowana przez dalszą część procedury odczytu. Dlatego koneiczne wykonanie jest tych operacji w sposób uniemożliwiający operowanie na niepełnych danych. Najprościej jest wyłączyć na moment przerwania, ale nie zawsze tak można i wtedy trzeba kombinować. Myślę że w Twoim przypadku wyłączenie przerwań w zupełności wystarczy.

    Konstrukcja if(wo == bufor[BUF]) mimo odwołania do nieistniejącego elementu tablicy, jest prawidłowa. Standard C gwarantuje poprawność arytmetyki na wskaźniku na taki element.

    Ilość danych zgromadzonych w takim buforze można policzyć tak:
    n = (wz - wo) % DLUGOSC;
    Podobnie jak wcześniej, operację modulo można zastąpić operacją AND. Właściwie to nawet trzeba, bo efektem działania operatora '%' w C nie jest wynik operacji modulo, tylko reszta z dzielenia. Reszta ta ma znak taki sam jak operator po lewej stronie. Można też użyć konstrukcji warunkowych - zmienić znak na przeciwny jeżeli różnica będzie ujemna.
    Wbrew pozorom, wielkość ta jest mało przydatna, w większości wypadków wygodniej i wydajniej robi się wszystko na wskaźnikach.
  • #7 4464479
    ergovision
    Poziom 11  
    Posty: 38
    Pomógł: 1
    zrobilem wersje z buforem , ale przy sporej dawce danych potrafi sie pogubić... Chyba czeka mnie iplementacja tej maszyny stanów i czegoś na wzór modbusa :/. Co sądzicie?
  • #8 4464511
    shg
    Poziom 35  
    Posty: 2289
    Pomógł: 339
    Ocena: 135
    Można użyć dodatkowych końcówek procesora i zrobić sprzętowy handshaking (dość łatwo).
    Można też zrobić handshaking programowy (XON/XOFF).
    Druga wersja jest nieco upierdliwa, bo po wysłaniu XOFF trzeba i tak być przygotowanym na odebranie danych. Ponadto trzeba być pewnym, że nadawca ma włączony nadajnik, czego sprawdzić nie możemy i chyba najprościej jest co jakiś czas wysyłać XON.

    Na czym się będziesz wzorował, to już w zasadzie obojętne, ważne żeby istniały podstawowe mechanizmy gwarantujące bezbłędną wymianę dancyh. Ze względu na popularność, modbus mógł by być dobrym pomysłem. Chyba nawet łatwiej było by zaimplementować ten protokół opierając się na dokumentacji, niż napisać dużo prostszy własny od zera.

Podsumowanie tematu

LABEL_AI_GENERATED
Dyskusja dotyczy algorytmów odbioru danych przez UART w mikrokontrolerze ATtiny2313, ze szczególnym uwzględnieniem buforowania danych. Rozważane są dwie główne strategie: przetwarzanie bajt po bajcie w momencie ich odbioru oraz oczekiwanie na pełną ramkę przed przetwarzaniem. Proponowanym rozwiązaniem jest zastosowanie bufora cyklicznego (ring buffer) implementowanego jako tablica o stałej długości z dwoma wskaźnikami – do zapisu i odczytu. Wskaźniki te poruszają się po buforze w sposób zapętlony, co umożliwia efektywne zarządzanie pamięcią i minimalizuje ryzyko nadpisania danych. Omówiono także kwestie synchronizacji i obsługi przerwań UART, które sygnalizują nadejście każdego bajtu, co pozwala na szybkie zapisywanie danych do bufora. Wskazano na konieczność stosowania maszyn stanów i protokołów komunikacyjnych (np. Modbus) w celu zapewnienia bezbłędnej wymiany danych i detekcji błędów, zwłaszcza przy dużym natężeniu transmisji. Poruszono również temat sprzętowego i programowego handshakingu (np. XON/XOFF) jako metod kontroli przepływu danych. Wskazano, że implementacja protokołu Modbus może być korzystna ze względu na jego popularność i dostępność dokumentacji.
Podsumowanie AI na podstawie dyskusji. Może zawierać błędy.
REKLAMA