Elektroda.pl
Elektroda.pl
X
CControls
Proszę, dodaj wyjątek www.elektroda.pl do Adblock.
Dzięki temu, że oglądasz reklamy, wspierasz portal i użytkowników.

[STM32][C++] - Obsługa peryferii / Wzorce projektowe

Sparrowhawk 08 Lut 2018 21:36 855 9
  • #1 08 Lut 2018 21:36
    Sparrowhawk
    Poziom 21  

    1. W jaki sposób piszecie kod do obsługi peryferiów występujących pojedyńczo w mikrokontrolerze? Np RTC, LCD, RNG, czy CRC.

    Kod: c
    Zaloguj się, aby zobaczyć kod

    2. W przypadku np interfejsów komunikacyjnych takich jak USART, czy SPI, chyba rzadko się zdarza, że wszystkie pracują w tych samych trybach (Np. bez przerwań), więc trudno jest utworzyć klasę, tak aby zapewniała elastyczny i lekki kod. Ilekroć próbuję, kończę na czymś przypominającym w swej prostocie HAL. A jeszcze sprawę komplikują funkcje obsługi przerwań, czy konfiguracja wyprowadzeń.

    3. Wszystkie książki o tematyce embedded jakie miałem okazje czytać / przeglądać zazwyczaj opisują każdy peryferial osobno, jeśli znajdzie się w nich jakiś rozbudowany przykład, to zazwyczaj ma silnie powiązaną strukturę.

    4. Właściwie to szukam sposobu na tworzenie ogólnego kodu obsługi peryferiów, któremu w łatwy sposób w docelowym projekcie będzie można nadać specjalizację. Szukałem inspiracji w sieci (Rozbudowane projekty z kodem, który można łatwo przenieść), jednak nie natrafiłem na zbyt dużo kompletnych projektów (a nie takich pokazujących działanie 1 peryferiala).

    Z ciekawszych stron znam takie:
    Andys Workshop - Przyznam wywarło to na mnie pozytywne zaskoczenie, ale widać też ogrom pracy autora.
    STM32F4 Discovery - Ogromna biblioteka, ale napisana w "C" i oparta chyba o SPL.

    0 9
  • CControls
  • Pomocny post
    #2 08 Lut 2018 22:07
    Freddie Chopin
    Specjalista - Mikrokontrolery

    Sparrowhawk napisał:
    1. W jaki sposób piszecie kod do obsługi peryferiów występujących pojedyńczo w mikrokontrolerze? Np RTC, LCD, RNG, czy CRC.

    Najzupełniej normalnie, czyli jako zwyczajną klasę. Jeśli jest tylko jeden taki układ peryferyjny, to w projekcie po prostu mam jeden taki obiekt. Z pewnością nie bawię się w żadne singletony czy klasę w której wszystko jest statyczne.

    Sparrowhawk napisał:
    2. W przypadku np interfejsów komunikacyjnych takich jak USART, czy SPI, chyba rzadko się zdarza, że wszystkie pracują w tych samych trybach (Np. bez przerwań), więc trudno jest utworzyć klasę, tak aby zapewniała elastyczny i lekki kod. Ilekroć próbuję, kończę na czymś przypominającym w swej prostocie HAL. A jeszcze sprawę komplikują funkcje obsługi przerwań, czy konfiguracja wyprowadzeń.

    To jest ciężki temat. Generalnie USART czy SPI - niezależnie od tego w jakim trybie pracują - mają ten sam interfejs publiczny, bo czy to za pomocą DMA czy przerwań czy pollingu pozwalają użytkownikowi zrobić to samo - wysłać/odebrać ileśtam bajtów z jakąśtam prędkością itd. Tak więc w tych przypadkach po prostu jest bazowa klasa interfejsowa (pure abstract) + kilka implementacji. Jeśli jakiś kod się powtarza (np. do ustawiania baudrate'u), to po drodze jest jeszcze jedna klasa z takimi właśnie wspólnymi rzeczami. Kwestia konfiguracji pinów moim zdaniem jest nierozwiązywalna w sposób uniwersalny, więc osobiście kiedyś postanowiłem ją pominąć - klasa od UARTa zajmuje się wysyłaniem danych przez UARTa - to żeby ten UART był dostępny na jakichś pinach, które są prawidłowo skonfigurowane, to już zadanie dla kodu z warstwy "board". Przerwania nie są takim wielkim problemem i również zrzucam je do warstwy "board" - utworzony obiekt po prostu przypięty jest do właściwego wektora i tyle. W systemie embedded wg mnie nie ma sensu bawić się w jakieś wyszukane dynamiczne rejestracje przerwań - szkoda RAMu na coś co nie ma żadnego uzasadnienia praktycznego.

    Sparrowhawk napisał:
    4. Właściwie to szukam sposobu na tworzenie ogólnego kodu obsługi peryferiów, któremu w łatwy sposób w docelowym projekcie będzie można nadać specjalizację. Szukałem inspiracji w sieci (Rozbudowane projekty z kodem, który można łatwo przenieść), jednak nie natrafiłem na zbyt dużo kompletnych projektów (a nie takich pokazujących działanie 1 peryferiala).

    To co opisujesz jest do zrobienia przy użyciu klas interfejsowych. Ewentualnie można rozważyć podział drivera na tzw. górną i dolną połówkę. Dolna połówka zawiera absolutne minimum kodu który jest specyficzny dla danego układu, implementując przy tym interfejs - np. dla STM32F4 będzie inna implementacja UARTa niż dla STM32F7, niemniej jeden i drugi ma taki sam niskopoziomowy interfejs. Górna połówka to w pełni uniwersalny driver, w którym implementujesz wszystkie bajery typu buforowanie, cudowanie, timeouty - ma ona zwyczajny interfejs dla użytkownika, do wszystkich zabaw sprzętowych używa dolnej połówki.





    Dokładnie taki model jest właśnie zaimplementowany w distortos, czyli w RTOSie napisanym całkowicie w C++11 nad którym pracuję:
    - górna połówka https://github.com/DISTORTEC/distortos/blob/m...r/source/devices/communication/SerialPort.cpp + https://github.com/DISTORTEC/distortos/blob/m...istortos/devices/communication/SerialPort.hpp
    - interfejs górnej połówki - https://github.com/DISTORTEC/distortos/blob/master/source/devices/communication/UartBase.cpp + https://github.com/DISTORTEC/distortos/blob/m.../distortos/devices/communication/UartBase.hpp
    - interfejs dolnej połówki - https://github.com/DISTORTEC/distortos/blob/m...source/devices/communication/UartLowLevel.cpp + https://github.com/DISTORTEC/distortos/blob/m...tortos/devices/communication/UartLowLevel.hpp
    - implementacja dolnej połówki - https://github.com/DISTORTEC/distortos/blob/m...ls/USARTv1/STM32-USARTv1-ChipUartLowLevel.cpp + https://github.com/DISTORTEC/distortos/blob/m...1/include/distortos/chip/ChipUartLowLevel.hpp

    Sparrowhawk napisał:
    Przyznam wywarło to na mnie pozytywne zaskoczenie, ale widać też ogrom pracy autora.

    To tylko wrapper na SPL/HAL (jeśli mówimy o bibliotece STM32Plus).

    Sparrowhawk napisał:
    Ogromna biblioteka, ale napisana w "C" i oparta chyba o SPL.

    W tej bibliotece nie ma SPL/HAL, niemniej jednak - niestety - jej autor należy do (większości?) osób, które nie wiedzą co to jest const-correctness.

    0
  • CControls
  • #3 08 Lut 2018 22:13
    BlueDraco
    Specjalista - Mikrokontrolery

    Niezależnie od tego, w jakim języku piszę, używam zmiennych i l struktur o rozmiarze odpowiednim do tego, co chcę w nich przechowywać, czyli np. numer godziny, minuty czy sekundy trzymam w uint8_t lub polu bitowym o odpowiedniej szerokości, a nie w uint32_t. ;)

    0
  • #4 08 Lut 2018 22:45
    grko
    Poziom 33  

    @BlueDraco Chyba jednak autor pyta o ogólną koncepcję projektowania sterowników a nie o typy zmiennych w strukturze drivera RTC. Poza tym nie wiem po co Tworzyć oddzielne struktury z datą i godzina jak jest coś takiego jak time.h.

    0
  • #5 09 Lut 2018 08:41
    Sparrowhawk
    Poziom 21  

    Freddie Chopin napisał:
    Przerwania nie są takim wielkim problemem i również zrzucam je do warstwy "board" - utworzony obiekt po prostu przypięty jest do właściwego wektora i tyle.
    Mógłbyś rozwinąć temat?
    Ja na samym początku przerzucam wektory przerwań na początek pamięci RAM. A później wstrzykuję swoją funkcję w miejsce oryginalnego wektora. Tyle, że jeśli chcę zrobić to z poziomu jakiegoś obiektu, to funkcja jest funkcją zewnętrzną, a wymiana danych z obiektem wymaga zadeklarowania ich w sekcji publicznej klasy.
    Ale skoro mamy globalne obiekty, to można wywołania odpowiednich metod wrzucić w handlery przerwań?

    0
  • Pomocny post
    #6 09 Lut 2018 09:29
    Freddie Chopin
    Specjalista - Mikrokontrolery

    Sparrowhawk napisał:
    Ja na samym początku przerzucam wektory przerwań na początek pamięci RAM. A później wstrzykuję swoją funkcję w miejsce oryginalnego wektora.

    Po rozmyślaniu na ten temat doszedłem do następujących wniosków.
    1. Możliwość dynamicznej rejestracji przerwań (najlepiej w wysoce abstrakcyjny sposób, tak aby funkcje nie musiały być publiczne) byłaby z pewnością super-wygodna w rozbudowanym frameworku.
    2. Fakt że istnieją współdzielone przerwania kompletnie kładzie tą ideę i sprawia że jej zaimplementowanie jest - moim zdaniem - w zasadzie nierealne. No chyba że ktoś chce się bawić w abstrakcję że współdzielone przerwania tak naprawdę nie są współdzielone - niby możliwe, do momentu aż chcemy komuś wytłumaczyć czemu dla tych niby-nie-wspołdzielonych-przerwań nie można ustawiać priorytetów... ups, priorytet jednak jest współdzielony...
    3. W systemie embedded, który realizuje jeden, ściśle określony program (z rozważań wyłączamy - zupełnie nieprzydatne moim zdaniem - dynamicznie ładowane aplikacje; owszem, technicznie jest to możliwe na takim STM32F4, niemniej jednak raczej jako ciekawostka dydaktyczna) naprawdę ciężko wyobrazić sobie sytuację, aby dynamiczna rejestracja przerwań miała jakieś uzasadnienie. Do danego przerwania generalnie będzie podpięty tylko jeden handler, ciężko mi wymyślić sensowną sytuację w której przez 5 sekund byłby podpięty jeden handler, a potem przez 3 kolejne - inny. No bo po co? Przecież jak aplikacja używa SPI, to on jest fizycznie jakoś na tej płytce podpięty - to nie jest tak, że przez chwilę jest masterem, a potem nagle kasujemy ten driver i już jest tam SPI slave. Owszem - fizycznie możliwe, tylko po co?
    4. STM32 mają sporo RAMu, ale nawet wersja "tępa" takiej rejestracji to ~400 bajtów minimum (powiedzmy że układ ma średnio około 100 przerwań). Gdyby chcieć to zrobić fajniej (funkcja + obiekt), to w zasadzie trzeba by mieć już ~800 albo i nawet ~1200 bajtów (ciekawostka - wskaźniki na funkcje w klasie nie muszą mieć rozmiaru wskaźników na zwykłe funkcje, często są 2x większe). Kilkaset bajtów RAMu zmarnowane na nieprzydatną funkcję to trochę przesada. Dodajmy jeszcze do tego, że często pewne wektory nie istnieją, więc wtedy ten RAM który na nie idzie jest zmarnowany kompletnie. Można niby zrobić dynamiczna rejestrację tylko dla niektórych wektorów, ale tu już zaczynamy dochodzić do absurdu - nieprzydatna funkcja jest do tego jeszcze bardzo skomplikowana...

    Sparrowhawk napisał:
    Tyle, że jeśli chcę zrobić to z poziomu jakiegoś obiektu, to funkcja jest funkcją zewnętrzną, a wymiana danych z obiektem wymaga zadeklarowania ich w sekcji publicznej klasy.

    U siebie zrobiłem to tak, że przerwania są obsługiwane przez dolną połówkę drivera, oczywiście handler musi być funkcją publiczną, ale użytkownik raczej nie ma potrzeby bawienia się dolną połówką. Zresztą - powiedzmy sobie szczerze - raczej ciężko żeby ktoś przypadkiem i przez pomyłkę wywołał funkcję o nazwie "interruptHandler" (; A nawet jakby kombinować z jakimś `friend` to i tak ominięcie zabezpieczenia funkcji prywatnych sprowadza się do jednej magicznej linijki `#define private public`. Przy okazji `friend` daje przerwaniu dostęp do wszystkiego (bez dodatkowych kombinacji), włącznie z prywatnymi zmiennymi klasy. Szkoda kombinować.

    https://github.com/DISTORTEC/distortos/blob/2...lude/distortos/chip/ChipUartLowLevel.hpp#L146

    Sparrowhawk napisał:
    Mógłbyś rozwinąć temat?

    Po prostu moim zdaniem nie jest realne zrobienie uniwersalnie takiego kodu, który by się zajmował zarówno danym układem peryferyjnym (np. USART), jak i tym żeby go przypiąć do przerwań, włączyć w RCC i jeszcze skonfigurować piny. Może jeśli się ograniczysz tylko i wyłacznie do STM32F4 to by się i dało, ale już we wszystkich STM32 jest tyle różnic, że to jest nie do ogarnięcia.

    Tworząc frameworka (przynajmniej taki jest mój plan) staram się zachowywać pewien podział na warstwy. Zwykle jest więc kod ogólny (generalnie niezależny od układu docelowego), kod architektury (np. specyficzny dla ARMv7-M kod zmiany kontekstu), kod danego układu (np. specyficzne dla STM32F4 wektory przerwań, specyficzne dla danej rodziny drivery UARTa, SPI, GPIO, ...) i kod płytki. Kod płytki jest miejscem które integruje wszystko.

    (następny akapit to opis tego do czego dążę, bo chwilowo niestety nie do końca tak to wygląda)

    Po prostu driver UARTa zajmuje się UARTem. Jeśli user nie skonfigurował GPIO - sorry. Jeśli user nie włączył przerwań - sorry. Jeśli w RCC nie działa zegar dla tego UARTa - sorry. Jeśli driver wymaga DMA a ono nie jest włączone - sorry. W driverze od UARTa jest więc funkcja `interruptHandler()` która ma zostać wywołana przez przerwanie i tyle. W warstwie "board", która jest specyficzna dla bardzo konkretnej płytki, w której wszystko jest już podłączone w bardzo konkretny sposób, po prostu jest sobie gdzieś plik nagłówkowy który deklaruje globalny obiekt `usart3`, natomiast w pliku źródłowym masz:
    1. Definicję obiektu (;
    2. Niskopoziomowy inicjalizator, wywoływany automatycznie przed wejściem do main(), który robi wszystkie dziwne rzeczy (np. właśnie konfiguruje piny TX i RX, ustawia priorytet przerwania i je włącza, ...).
    3. Globalne przerwanie w postaci:

    Code:
    void USART3_IRQHandler()
    
    {
    usart3.interruptHandler();
    }


    Mój aktualny plan dla distortos jest taki, że warstwa "board" będzie w dużej mierze generowana automatycznie, tak więc fakt że trzeba tam dać te wszystkie dziwne rzeczy - co może być upierdliwe i mało wygodne - jest do zaakceptowania.

    Warstwa "board" nawet nie musi udostępniać "gołego" obiektu drivera dolnej połówki - równie dobrze może przecież deklarować jako globalny już gotowy wysokopoziomowy driver typu `SerialPort` czy `Rs485` - w takim wypadku to, że dany obiekt wewnętrznie używa np. USART3 jest bez znaczenia - obiekt ten jest całkowicie lokalny i niedostępny dla reszty kodu.

    Tak czy siak - jeśli potrzebujesz faktycznie rozwiązywania takich dziwnych problemów, to moim zdaniem istnieje tylko jeden wzorzec projektowy który się tu może sprawdzić - dependency injection.

    W każdym razie moim zdaniem taki podział na warstwy ma sens. Z punktu widzenia aplikacji mało istotne jest czy dana komunikacja odbywa się po UART3 na pinach PA2 i PA3, przy użyciu przerwania nr 42 oraz DMA2 na strumieniu 5, czy może przez UART5 na pinach PG0 i PC15, przy użyciu przerwnia nr 65 oraz DMA1 na strumieniu 1. Aplikacja ma to gdzieś - jej jest potrzebny obiekt przy pomocy którego może się komunikować. Jakby się uprzeć, to można to traktować jako "dependency injection" na poziomie aplikacji - dostaje ona obiekt, zupełnie nie przejmując się tym skąd się wziął i co trzeba zrobić żeby go zainicjalizować.

    Oczywiście jak ktoś ma inne opinie lub inne przemyślenia, to ja bardzo chętnie poczytam, bo od nadmiaru wiedzy głowa nie boli (;

    0
  • #7 09 Lut 2018 10:21
    Sparrowhawk
    Poziom 21  

    Freddie Chopin napisał:
    ciężko mi wymyślić sensowną sytuację w której przez 5 sekund byłby podpięty jeden handler, a potem przez 3 kolejne - inny. No bo po co?
    Timery i dokładne odmierzanie czasu w różnych miejscach kodu. Zamiast korzystać z kilku timerów, można w różnych blokach kodu konfigurować 1 timer i wstrzykiwać odpowiednią funkcję przerwania. Oczywiście pod warunkiem, że nie będziemy musieli odmierzać czasu dla dwóch bloków jednocześnie.

    Freddie Chopin napisał:
    Po prostu driver UARTa zajmuje się UARTem. Jeśli user nie skonfigurował GPIO - sorry. Jeśli user nie włączył przerwań - sorry. Jeśli w RCC nie działa zegar dla tego UARTa - sorry. Jeśli driver wymaga DMA a ono nie jest włączone - sorry.
    Też tak o tym myślałem, tyle, że u mnie np. dla UART , w klasie jest statyczna funkcja init(), która konfiguruje moduł, a user musi napisać jej implementację.

    Cytat:
    2. Fakt że istnieją współdzielone przerwania kompletnie kładzie tą ideę...

    Taki kod testowy do przerwań EXTI poczyniłem:
    Kod: c
    Zaloguj się, aby zobaczyć kod

    Wywołanie w main:
    Kod: c
    Zaloguj się, aby zobaczyć kod


    Samo wstrzykiwanie przerwań wygląda tak:
    Kod: c
    Zaloguj się, aby zobaczyć kod


    Freddie Chopin napisał:
    Po prostu moim zdaniem nie jest realne zrobienie uniwersalnie takiego kodu, który by się zajmował zarówno danym układem peryferyjnym (np. USART), jak i tym żeby go przypiąć do przerwań, włączyć w RCC i jeszcze skonfigurować piny. Może jeśli się ograniczysz tylko i wyłacznie do STM32F4 to by się i dało, ale już we wszystkich STM32 jest tyle różnic, że to jest nie do ogarnięcia.
    Zgadzam się, ale w tworzeniu całej tej abstrakcji łatwo przekombinować, stąd moje pytania ;-)

    0
  • Pomocny post
    #8 09 Lut 2018 10:42
    Freddie Chopin
    Specjalista - Mikrokontrolery

    Sparrowhawk napisał:
    Timery i dokładne odmierzanie czasu w różnych miejscach kodu. Zamiast korzystać z kilku timerów, można w różnych blokach kodu konfigurować 1 timer i wstrzykiwać odpowiednią funkcję przerwania. Oczywiście pod warunkiem, że nie będziemy musieli odmierzać czasu dla dwóch bloków jednocześnie.

    Wygląda na to, że potrzebujesz po prostu callbacka w jednym i tym samym przerwaniu timera. To nie do końca to samo co inne handlery - każdy Twój callback musiałby zrobić dokładnie tą samą operację sprzętową - skasować flagę timera. Do tego jeszcze każdy Twój callback musiałby wiedzieć którą flagę kasować i jak ten timer ogarnąć (np gdy włączysz 3 porównania od CC). Nie jestem przekonany <; Zauważ że w RTOSie masz jeden wspólny kod odpowiedzialny za timery programowe, który potem wywołuje callbacki usera. User nie musi pisać od zera kodu timerów programowych 3x jeśli chce wywołać w programie 3 różne callbacki.

    Sparrowhawk napisał:
    Też tak o tym myślałem, tyle, że u mnie np. dla UART , w klasie jest statyczna funkcja init(), która konfiguruje moduł, a user musi napisać jej implementację.

    Czyli masz warstwę "board" tyle że w innym miejscu.

    Sparrowhawk napisał:
    Taki kod testowy do przerwań EXTI poczyniłem:

    No i to jest takie "leaky abstraction" właśnie - niby masz osobne handlery, ale już nie możesz im osobno ustawić priorytetu albo osobno włączyć/wyłączyć ich w NVIC. Te dwie funkcjonalności musiałeś wywalić aby mieć wspólny interfejs. Tak czy siak gdzieś masz warstwę "board", w której to przerwanie współdzielone musisz włączyć i ustawić mu ewentualnie priorytet.

    Sparrowhawk napisał:
    Samo wstrzykiwanie przerwań wygląda tak:

    Ale i tak zawsze na pozycji odpowiedzialnej za EXTI2_IRQHandler() masz wskaźnik na EXTI2_IRQHandler(). Czyli w RAM trzymasz wartość która jest absolutnie stała i niezmienna dla danego programu. Właśnie o to mi chodzi - niby fajnie, możesz sobie rejestrować dynamicznie, ale po co? Faktyczne callbacki i tak musisz mieć gdzieś indziej - Twoje funkcje exec(). Dokładnie to samo można zrobić z timerem i finalnie zużyjesz mniej RAMu na to niż dynamicznie się bawiąc w takie rejestracje.

    W STM32F4 jeszcze to się wydaje ogarnialne, ale zobacz jakie są tablice wektorów np. w STM32F0 albo w STM32L0 - tam to mało który _NIE_ jest współdzielony...

    Sparrowhawk napisał:
    Zgadzam się, ale w tworzeniu całej tej abstrakcji łatwo przekombinować, stąd moje pytania

    Podstawowym pytaniem powinno być to zakres abstrakcji (; Jeśli chcesz zrobić abstrakcję dla całej rodziny STM32F4, to jak najbardziej jest to możliwe. Dla wszystkich STM32 - pewnie możliwe, ale wymagany na to wysiłek raczej mocno przewyższa korzyści takiej abstrakcji, nie mówiąc już o koszcie w postaci pamięci RAM i czasu wykonywania. Dla wszystkich możliwych Cortexów - zapewne niemożliwe (;

    Dla mnie jeszcze jednak dochodzi kwestia tego czy to ma sens i właśnie z mojej perspektywy w przypadku przerwań nie ma - w 99,666% przypadków pod przerwanie X będzie podpięty handler Y - zawsze tylko ten jeden. Pozostałe 0.334% przypadków jest albo tak dziwnych że i tak żadna abstrakcja ich nie obejmie, albo po prostu błędnie wyspecyfikowanych i spokojnie mieszczą się w przypadku ogólnym po uproszczeniu założeń (np. zamiast różnych handlerów dla timera - uniwersalny handler który po prostu obsługuje callbacki).

    0
  • #9 09 Lut 2018 15:52
    Sparrowhawk
    Poziom 21  

    Zgadzam się, że im bardziej dążymy do ogólnego przypadku tym więcej opcji musimy pominąć. Myślałem tylko, że to ja nie potrafię tego tak zrobić, że będzie ogólnie, wydajnie, prosto i elastycznie ;-)

    Czyli lepiej przyjąć taki model obsługi przerwań:

    Kod: c
    Zaloguj się, aby zobaczyć kod

    0
  • #10 09 Lut 2018 15:58
    Freddie Chopin
    Specjalista - Mikrokontrolery

    Lepiej czy gorzej - na to pytanie nie ma odpowiedzi (; Mnie osobiście taka koncepcja najbardziej pasuje i daje się wg mnie zastosować uniwersalnie w obliczu takich upierdliwości jak właśnie współdzielone przerwania. Przykładowo jednak w mbed jest rejestracja dynamiczna i to taka "na całego", z magicznymi template'ami aby można było rejestrować funkcje z klas (; (a przynajmniej tak mi się kojarzy że widziałem to w mbed)

    Kasowanie flag i inne tego typu rzeczy też bym wrzucił do funkcji handlerIRQ(). Callbacki - jeśli faktycznie Ci potrzebne - zrobiłbym właśnie tak jak pokazałeś. Dla timera może i faktycznie mają sens, za to dla bardzo wielu układów peryferyjnych już niekoniecznie. Ewentualnie są to callbacki-zdarzenia, które służą do informowania górnej połówki drivera o tym że coś ciekawego się zdarzyło - coś takiego mam u siebie w klasie `UartBase`.

    0