wtorek, 11 stycznia 2022

STM32G0 Bare Metal - Maszyna stanów (FSM) i nieblokująca transmisja I2C.

O ile pisanie kodu na MCU jest zwykłym zajęciem o tyle pisanie kodu nieblokującego jest już z pogranicza sztuki :). Taką stawiam tezę. Większość z nas hobbystów standardowo pisze kody blokujące bo tak jest najprościej.  Jeśli chcemy jednak iść dalej i wzbić się ponad przeciętność :) ,stawiać kolejne kamienie milowe w swoim rozwoju w zakresie programowania MCU to prędzej czy później staniemy przed zagadnieniem kodu nieblokującego. Odróżnijmy jednak tutaj naukę pisania kodu nieblokującego od "wyklikania" takiego kodu za pomocą konfiguratorów kodu. Po co mamy sobie zawracać głowę nauką czegokolwiek jeśli kod do nieblokującej obsługi np. I2C możemy "wyklikać" w STM32 CUBE MX . Jeśli ktoś chce być znudzonym niewolnikiem biblioteki jednego producenta to poprzestanie na tym . Ja uważam , że takie biblioteki oczywiście są potrzebne ale zabijają w człowieku kreatywność. W przypadku kodu nieblokującego w zasadzie możemy śmiało mówić o projektowaniu takiego kodu a nie samym pisaniu. Często trzeba się posłużyć grafami np. w języku UML aby opisać zależności w kodzie . Jest to kawał sporej wiedzy po przyswojeniu której staniemy na wyższym "levelu" :) . Patrzmy w górę ku gwiazdom  a nie pod nogi :).

Poszukując wiedzy na temat nieblokującej obsługi kodu na pewno natrafimy na wątek dotyczący FSM (Finite State Machine). W ramach poszerzenia swojej wiedzy na ten temat, chciałem organoleptycznie przećwiczyć działanie FSM na wybranym zagadnieniu jakim jest np. stworzenie nieblokującej obsługi I2C. Do takiej obsługi standardowo mamy do dyspozycji następujące metody - Pooling, IRQ, DMA. Należy jednak zauważyć , że używanie nieblokującej obsługi I2C wiąże się  z pewnymi problemami , które w kodzie blokującym nie istnieją np. problem związany z wielokrotnym wywołaniem następujących po sobie transmisji . Nie będę jednak wchodził w ten temat i skupię się tylko na jak najprostszych działaniach .

Mój przykład będzie może trochę naiwny w implementacji ale chodzi mi o to aby pokazać w jakim kierunku się poruszać. W przykładzie pokażę jak skonstruować maszynę stanów do nieblokującej obsługi I2C ,wybiorczo, dla funkcji odczytującej dane. Przyjrzyjmy się zatem jak taka funkcja pierwotnie wygląda w wersji blokującej :


W powyższej funkcji mamy cztery blokujące pętle while(). Całkowity czas transmisji dla wysłania w sumie czterech bajtów przez funkcję to ok. 0.6 ms. Przez tyle czasu blokujemy MCU , który nie może w tym czasie wykonać innych działań. Dla MCU jest to relatywnie długi czas "bezczynności".
Blokującą funkcję rozbijam na cztery funkcjonalne człony i usuwam pętle while(). Tworzę cztery oddzielne funkcje. 



Pierwsza funkcja I2C1_Read_Start() jest funkcją inicjującą transmisję i wysyła na magistralę I2C , bajt adresujący slave'a i bajt adresujący rejestr urządzenia z którym chcemy prowadzić rozmowę. Aby ta funkcja wystartowała musi być spełniony warunek wolnej magistrali I2C. Jeśli linia nie jest zajęta, inicjowana jest transmisja . Przy wyjściu z funkcji inicjującej stan maszyny jest ustawiony na I2C_STATE_READ_TRANSFER_PROGRESS . Czyli informujemy maszynę stanów, że proces  trwa. 

Przejdźmy w tym momencie do definicji stanów naszej maszyny.



Na podstawie funkcjonalności funkcji pierwotnej blokującej, wykontycypowałem stany reprezentujące te funkcjonalności i je opisałem nazwami :). Było to proste zadanie. Potem stworzyłem  strukturę , którą docelowo maszyna stanów będzie się posługiwała. W strukturze tej możemy umieścić szereg innych funkcjonalności np. tutaj możemy przechowywać odebrane dane.



Deklaruję obiekt i2c_object o typie wcześniej powołanej struktury. W pliku i2c_manager.c musimy to  powielić bez przedrostka extern.



Teraz musimy stworzyć funkcję , która będzie nam fizycznie realizować naszą maszynę stanów . Funkcja jest oparta o selektor wyboru switch/case. W bardziej rozbudowanych maszynach lepiej jest zastąpić to tablicą przejść.



Po uruchomieniu programu maszynę ustawiamy w stanie początkowym :



Funkcję maszyny stanów wywołujemy w pętli głównej programu :



Program co 0.1 s inicjuje transmisję I2C a maszyna stanów śledzi dalszy przebieg zdarzeń w module sprzętowym I2C. Moja maszyna nie jest doskonała , wiele rzeczy można tutaj poprawić ale działa i spełnia swoje zadanie doskonale. 



Tym szybkim wpisem zasygnalizowałem temat innego podejścia do programowania . Patrzenie na kod przez pryzmat maszyny stanów jest bardzo pouczającym i ciekawym doświadczeniem , warto tą drogą podążać. 

Pozdrawiam

PICmajster
picmajster.blog@gmail.com


3 komentarze:

  1. świetny artykuł , uczy jak opanować chaos w C i wybrnąć z januszowego kodowania.

    OdpowiedzUsuń
  2. Zastanawiam się jak zrobić semihosting pod Seg. emb. studio

    OdpowiedzUsuń
  3. cytat : "Embedded Studio fully supports the SEGGER Semihosting standard, including terminal I/O, file I/O, system operations, and host-based formatting. It also easily creates programs that use semihosting.
    Using Embedded Studio is one of the easiest ways to create and run programs using semihosting" Tu nie ma co się zastanawiać tylko używać. W STM32 CUBE wymaga to jakiejś tam konfiguracji pieprzenia aby używać semihostingu. U Seggera to już jest zaimplementowane i nic nie trzeba konfigurować tylko używać . Dołącz #include i użyj np. printf("data %d!\n", data); W konsoli debugera odczytasz.Ale zważ na to, że semihosting jest szybszy od UARTA ale nadal wolny i wstrzymuje pracę core MCU na czas wysyłania danych semihostingiem, dlatego nie można semihostingu używać do debugowania np.stosów komunikacyjnych albo aplikacji czasu rzeczywistego . Segger ma coś lepszego niż semihosting , nazywa się to RTT (Real Time Transfer) i nie ma ograniczeń takich jak semihosting.

    OdpowiedzUsuń