FreeRTOS - Paweł Szabaciuk

Paweł Szabaciuk

Software Developer

Tag: FreeRTOS

ESP32 – ku pamięci… (RAM)

Organizacja pamięci w ESP32 jest elastyczna. Wyróżniamy kilka rodzajów pamięci:

Omówmy zatem co jest co i z czym się je.

IRAM

ESP-IDF alokuje część regionu Internal SRAM0 jako pamięćRAM instrukcji. Poza pierwszym blokiem 64kB, którego używa PRO i APP CPU jako pamięc typu cache, reszta jest dostępna jako pamięć RAM. Pamięć ra przechowuje cześći aplikacji, które muszą być wykonywane z RAM.
Kilka komponentów, np. stos WiFi, są alokowane w tej pamięci przez linkera.
Jeśli potrzebujemy cześć kodu umieścić w tej pamięci, używamy słówka kluczowego IRAM_ATTR.
Co powinno zatem trafić do tej pamięci?

  • handlery przerwań muszą być umieszczone w IRAM. Mogą one wywoływać tylko funkcje umieszczone w IRAM lub w ROM. Dodatkowo, wszystko do czego handlery ISR się odwołują musi być umieszczone w IRAM. Dotyczy to np. ciągów znaków globalnych.
    Mała uwaga, na dzień dzisiejszy, API FreeRTOS jest umieszczone w IRAM. Można więc bezpiecznie wywoływać je z handlerów ISR.
  • wszystkie dane, do których czas dostępu jest krytyczny powinny trafić do IRAM. Wszystko co jest czytane z flasha, idzie przez 32kB cache. Umieszczenie więc danych krytycznych w IRAM może poprawić responsywność w dostępnie do nich, ponieważ omija cache.

IROM

Wszystko co nie jest oznaczone jako IRAM lub RTC, trafia do pamieci flash. To pamięć przechowująca wszystkie dane apliakcji. Dostęp do niej odbywa się przez pamięć cache o wielkości 32kB.

RTC fast memory

Pamięć zarezerwowana dla kodu wykonywanego po obudzeniu procesora z głebokiego uśpienia.

DRAM

Nie stała i zainicjalizowane zerem dane trafiają do tej pamięci przez linkera. Pamięć ma rozmiar256kB, ale jest zmniejszona o 64 kB przez Bluetooth (jeśli używany) i o 16 do 32 kB przez tracer pamięci.
Pozostała pamięć po zaalokowaniu statycznych danych, jest dostępna jako sterta.

Stałe dane, mogą być też wrzucone do pamięci DRAM, jeśli np. są używane przez handler ISR. Służy do tego atrybut DRAM_ATTR.

DROM

Domyślnie, stałe dane są wrzucane przez linker do regionu 4MB, który jest używany do dostępu do pamięci flash przez MMU i cache.

RTC slow memory

Globalne i statyczne zmienne, używane przez kod z pamięci RTC (np. kod wykonywany w czasie głębokiego uśpienia) musi być umieszczony w tej pamieci.

ESP32 – FreeRTOS – pierwsze starcie

Każdy kto programował jakikolwiek mikroprocesor (AVR, 8051, PIC) wie, że prędzej czy później, będzie potrzebował jakiegoś schedulera. Zawsze trafi się jakieś zadanie, które będzie musiało się z większą lub mniejszą dokładnością wykonywać w określonym czasie. O ile w przypadku małych procesorów AVR (głównie z takimi miałem do tej pory do czynienia) może być problem z pamięcią, o tyle w przypadku dużych jest to mniejszy problem. Jeśli mamy mało pamięci musimy sobie radzić bez bibliotek zewnętrznych. Jeśli mamy dość pamięci możemy użyć FreeRTOS.

Czym zatem jest FreeRTOS (i RTOS w ogólności)? Jest to system operacyjny, który zajmuje się rozdzielaniem czasu procesora pomiędzy zadania. Każdy procesor jedno rdzeniowy (wielordzeniowość pomaga, ale nie załatwia problemu) przy wykonywaniu kilku zadań, musi podzielić swój czas pomiędzy te zadania. Wyobraźmy sobie, że korzystamy z naszego PC, czytamy sobie ten wpis w jednym oknie, w tle ściąga się nam film na wieczór (oczywiście zakupiony i legalny ;)) a z głośników sączy się delikatnie kojąca, wprowadzająca prawie w medytacyjny stan nasz umysł, utwór pod wdzięcznym tytułem Highway to Hell. Spójrzmy zatem co się dzieje od strony systemu operacyjnego. Patrząc tylko na te trzy zadania, bo oczywiście wykonuje ich dziesiątki.

System operacyjny,mając do dyspozycji jeden rdzeń i jeden procesor, aby wykonać te wszystkie zadania jednocześnie, musi się roztroić. I może to zrobić. Każde z tych zadań jest dostatecznie mało zajmujące współczesne maszyny, aby nie było potrzeby poświęcania im 100% czasu. Co więc się dzieje? System operacyjny uruchamia wątek pierwszy na określoną ilość czasu, potem go wstrzymuje, uruchamia drugi, wstrzymuje, uruchamia trzeci. To tak jak z telewizją, nie mamy ciągłego obrazu, tylko 50 klatek na sekundę (lub 25 interpolowanych… nie ważne :)).

Powiecie, no dobra dobra, ale ja mam super hiper nowoczesny sprzęt 30 rdzeni, każdy po 4 wątki. OK. System operacyjny (jeśli potrafi) wykorzysta to tak, że na każdy z rdzeni wrzuci tyle wątków ile się da. A jak mu zabraknie rdzeni? To znowu zacznie dzielić po kilka zadań na jednym.

Każdy, kto próbował pisać aplikację wielowątkową wie że nie jest to proste. Napisanie systemu który obsługuje wątki na jednym rdzeniu jest dość proste. Natomiast na kilku już nie koniecznie (synchronizacja wątków, brrrrrr…). Z pomocą przychodzi nam tutaj oczywiście Internet. Jest wiele projektów RTOSów które możemy wykorzystać. Nie wszystkie bezpłatnie komercyjnie, niektóre w ogóle nie bezpłatnie, inne całkowicie bezpłatnie. Ja skupię się na implementacji FreeRTOS. SDK ESP32 jest oparte na FreeRTOS. Jak wygląda to licencyjnie, możemy sobie podejrzeć tutaj.

Generalnie warto by było wrzucić trochę dokumentacji:

Ok, porozmawiajmy sobie trochę o samych RTOSach. Systemy czasu rzeczywistego dzielą się generalnie na dwa rodzaje:

  • miękkie, czyli takie w których wykonanie zadania musi być w określonym czasie, ale jeśli spóźni się kilka mikro sekund to świat się nie skończy, bomba nie wybuchnie, imigranci nas nie zaleją etc.
  • twarde, jak się domyślacie, odwrotnie do miękkich – wykonanie zadania musi być idealnie przewidywalne w czasie. Tutaj spóźnienie nie wchodzi w grę. Wyobraźcie sobie np. sterowanie rakiety. Rakieta sobie startuje, leci prosto. Czujniki przesyłają cały czas dane do mikroprocesora sterującego. Na podstawie tych danych, mikroprocesor steruje lotkami, żeby rakieta leciała nam prosto do celu. Nie trudno sobie wyobrazić co się stanie, jeśli zastosujemy system miękki. Nie wykonanie od razu korekcji lotu, bo np. programista jest miłośnikiem kosmitów i podpiął naszą rakietę do programu SETI (3000 rakiet, większość leży nie używana, całkiem dużo mocy obliczeniowej się marnuje, nieprawdaż?). Wątek do SETI w dodatku ustawił na wysokim priorytecie, co powoduje, że korekcja lotu odbywa się 1ms za późno. Niby nic prawda? 1ms. Kto by tam liczył. Jak jeden grosz 🙂 Tylko zwłoka o 1ms powoduje, że kolejna poprawka musi być dużo większa. Kolejna (również spóźniona) jeszcze większa. Dochodzimy do momentu kiedy nasza rakieta już nie jest sterowalna. A to wszystko przez kosmitów…

Wywłaszczanie

Nie, nie chodzi o wyrzucanie ludzi z mieszkań. Wróćmy do naszej rakiety. Przez 99% czasu swojego życia nie robi nic. 1% potrzebuje na dolecenie do celu. Wobec tego, przez 99% czasu, zadanie sterowania rakietą nie robi nic. Wyobraźmy sobie zatem następującą sytuację. Zadanie sterowania jest uruchomione, sprawdza stan rakiety i wie, że rakieta leży sobie w silosie i nic nie robi. Oczekuje zatem, na sygnał startu. W tym czasie procesor i tak nic nie robi, więc zadanie oddaje sterowanie do OS, który czasie oczekiwania na start uruchamia zadanie drugie, które zostanie przerwane natychmiastowo w momencie startu i odda wtedy cały czas procesora do zadania sterowania rakietą. Takie działanie (przełączenie na inne zadanie, w czasie, kiedy pierwsze i tak nic nie robi) nazywa się wywłaszczaniem. Inny przykład? mikroprocesor sterujący silnikiem rolety. Zadanie obsługi silnika jest priorytetowe, natomiast UI możemy odświeżyć w dowolnym pozostałym czasie. Jest to więc generalnie prioryteryzacja zadań, zadanie mniej ważne jest przerywane na rzecz bardziej ważnego, które nie może mieć żadnych opóźnień.

Jeśli mamy procesor na tyle silny, że potrafi uporać się ze wszystkimi zadaniami przed czasem, przełącza się na proces Idle. Jest to proces,który sprząta pamieć, a poza tym jeśli nie ma nic więcej do roboty może np. uśpić procesor w celu bycia zielonym.

Scheduler

Czym jest scheduler, jest już chyba jasne. Jest to część systemu operacyjnego, zajmująca się rozdzielaniem czasu procesora pomiędzy poszczególne zadania. Uwzględnia priorytety i wymagania czasowe zadań. Jest kilka rodzajów implementacji, ale my nie piszemy RTOSa tylko go wykorzystujemy, więc na tę (tą?) chwilę nie ma potrzeby zaśmiecać się wiedzą na ten temat.

FreeRTOS

System FreeRTOS jest wywłaszczalnym systemem operacyjnym czasu rzeczywistego. Jest to system open source, może być wykorzystywany komercyjnie. Jeśli zależy nam na wsparciu technicznym, oczywiście jest taka opcja, możemy kupić licencję na OpenRTOS. Licencja to zmodyfikowane GNU, z wyłączeniem wirusowego otwarcia kodu aplikacji, która używa FreeRTOS.

Podział zadań

W systemie FreeRTOS wyróżniamy dwa typu procesów:

  • Task – zadanie, czyli niezależny proces. Posiada własny kontekst (czyli widzi rejestrt i stos, jak by był jedynym zadaniem procesora). Czyli z dużych systemów operacyjnych jest to po prostu proces.
  • Co-routine – współprogram. Jest podobny do zadania, natomiast współdzieli stos z innymi współprogramami. Czyli jest to wątek.

Co ważne, zadania i współprogramy nie mogą się komunikować przy użyciu semaforów i kolejek. Współprogramy też mają niższy priorytet niż zadania.

Każde zadanie może przyjąć jeden ze stanów:

  • Running – aktualnie wykonywane zadanie
  • Ready – oczekuje na zasoby
  • Blocked – oczekuje na przerwanie lub upływ czasu
  • Suspended – wstrzymane, nie jest brane pod uwagę przez schedulera

Współprogramy posiadają następujące stany:

  • Ready
  • Running
  • Blocked

Definicja zadania

Zadanie jest po prostu funkcją, nie zwracająca żadnej wartości, przyjmującą parametr typu void*. Konstrukcja funkcji musi być nieskończoną pętlą. Zadanie dodajemy do schedulera przy pomocy xTaskCreate() a usuwamy przy użyciu vTaskDelete().

Zwyczajowo w RTOS x w nazwie oznacza funkcję zwracającą wartość, v nie zwracającą.

Zajrzyjmy więc do naszej aplikacji Hello, World poprzedniego wpisu:

 C++ | 
 
 copy code |
?

01
#include "stdio.h"
02
#include "freertos/FreeRTOS.h"
03
#include "freertos/task.h"
04
#include "esp_system.h"
05
#include "nvs_flash.h"
06
 
07
void hello_task(void *pvParameter)
08
{
09
    printf("Hello world!\n");
10
    for (int i = 10; i >= 0; i--) {
11
        printf("Restarting in %d seconds...\n", i);
12
        vTaskDelay(1000 / portTICK_RATE_MS);
13
    }
14
    printf("Restarting now.\n");
15
    fflush(stdout);
16
    system_restart();
17
}
18
 
19
#ifdef __cplusplus
20
extern "C"
21
#endif
22
void app_main()
23
{
24
     nvs_flash_init();
25
    xTaskCreate(&hello_task, "hello_task"2048NULL5NULL);
26
}

Od linijki 7 widzimy definicję funkcji  hello_task , która jest naszym jedynym (poza  Idle ) zadaniem. W ciele funkcji widzimy co dane zadanie robi.

Linijka 25 tworzy nam zadanie. Pierwszy parametr to wskaźnik na funkcję, drugi to nazwa zadania, trzeci to rozmiar stosu przydzielony do zadania, czwarty parametr (czyli u nas nic), piąty to priorytet i wreszcie ostatni to wskaźnik do uchwytu zadania (używany np. do usunięcia zadania).

Wyjaśnienia wymagają linijki 19-21. Jest to eksport funkcji C do kompilatora C++. W VisualGDB standardowo piszemy w C++, czyli obiektowo. Natomiast wywołanie funkcji app_main jest w kodzie kompilowanym przez C, zatem musimy wyeksportować tą funkcję do kompilatora C++, żeby potrafił jej użyć oraz cały proces kompilacji poprawnie zlinkował nam kod C z C++.

Dobra, lecimy dalej. Linia 12 jest wstrzymaniem wykonania zadania na określony czas. W naszym przypadku jest to 1000ms. portTICK_RATE_MS jest to stała, określająca czas 1 ms w oparciu o taktowanie zegara mikroprocesora. Do sterowania przebiegiem mamy też funkcję vTaskDelayUntil , która różni się tym, że jest dokładna. To znaczy, że wywołanie vTaskDelay nie zawsze będzie precyzyjne. Wpływ na to mają instrukcje warunkowe w samym zadaniu, przerwania i wywłaszczenia. Druga funkcja natomiast dostaje w parametrze liczbę taktów, które upłynęły od momentu uruchomienia schedulera ( xTaskGetTickCount ) oraz ilość na jaką ma być wstrzymane zadanie. To pozwala precyzyjnie określić moment wznowienia zadania.

Ustawianie i odczyt priorytetów zapewniają nam funkcje uxTaskPriorityGet oraz vTaskPrioritySet .

Wstrzymywanie i wznawianie zadań to odpowiednio: vTaskSuspend i vTaskResume.

Komunikacja między zadaniami

Huh. Dużo o tym czytałem, zawsze była to czarna magia. Do momentu, kiedy w jednym z projektów Embedded Linux musiałem obsługiwać z kilku programów dostęp do jednej magistrali RS485. Zasada komunikacji między procesami czy różnymi aplikacjami jest podobna. Musimy mieć jakiś pojemnik na dane, które chcemy współdzielić oraz miejsce na oznaczanie blokowania danego zasobu, który jest wpółdzielony.

We FreeRTOS do wymiany danych służą Queues. Są to zwykłe kolejki FIFO. Działanie jest proste, zadanie A zapisuje dane do kolejki, zadanie B odczytuje. Kolejka, jak to kolejka, po odczycie przesuwa wskaźnik na kolejny element. Rozmiar kolejki jest definiowalny, zawsze można się podeprzeć zapisem wskaźników jako wartości do kolejki 🙂

Tworzenie kolejki to polecenie xQueueCreate. Podajemy w parametrach oczywiście wielkość i rozmiar elementu. Zwraca nam uchwyt do kolejki.

Semafory. Podobnie jak w kolejnictwie (kto się nie bawił kolejkami elektrycznymi w dzieciństwie ten traci), służą do synchronizacji oraz wykluczania dostępu. Zasada działania jest taka, że jeśli semafor jest nieaktywny, to blokuje on wykonanie zadania, natomiast aktywacja semafora wznawia dane zadanie. Przykładowo, mamy jedną magistralę RS485 i kilka zadań. Każde z zadań chce rozmawiać z innym urządzeniem peryferyjnym, podłączonym do magistrali. W momencie rozpoczynania transmisji, ustawiany jest semafor. W trakcie transmisji tylko dane zadanie może rozmawiać po magistrali. Wszystkie pozostałe zadania oczekują na zwolnienie semafora. Po zwolnieniu kolejne zadanie rozmawia ze swoim odbiorą itd. To zapewnia nam brak zakłóceń, gdyby więcej niż jedno zadanie rozmawiało w tym samym czasie.

To generalnie całość wiedzy, potrzebnej do rozpoczęcia programowania przy użyciu FreeRTOS. Oczywiście jak znajdę chwile to omówię dokładniej kolejki oraz semafory.

W kolejnym odcinku napiszemy sobie jakąś prostą aplikację łączącą się z domowym WiFi i hostującą prostą stronę z pomiarem temperatury/wilgotności. W między czasie czekam na ESP WROVER KIT jako platformę do podstawowych testów oraz programator JTAG.

ESP32 – pierwsza aplikacja

Zaczynamy więc z pierwszą aplikacją. Ja zacznę od prostego przykładu, opartego na szablonie z VisualGDB.

Projekt rozpoczynamy od wybrania New -> Project -> Embedded Project Wizard. Wypełniamy ścieżki do katalogów, podajemy nazwę i idziemy dalej.

Odznaczamy budowanie dodatkowego pliku .bin (co u mnie nie działa, muszę później w ustawieniach projektu wyłączyć). Ja wybieram MSBuild, Makefile się u mnie nie kompiluje a i tak nie ma standardowych poleceń z ESP32 IDF (make menuconfig etc.).

Wybieramy urządzenie (ESP32) i idziemy dalej.

Po drodze projekt nam się wstępnie skompiluje (jest to tylko środowisko do budowania, jeszcze bez RTOS).

Na kolejnym kroku możemy wybrać jeden z czterech template przygotowanych przez SysProg. Wybierzmy sobie podstawowy Hello, World, jako że to nasz pierwszy program na ESP32 a tradycja bardzo dobre imię dal dziewczynki 🙂

Ostatni krok to wybranie debuggera sprzętowego jakiego będziemy używać. W tej chwili nie ma znaczenia, jako że żadnego (jeszcze) nie posiadam. Kliknięcie Finish stworzy nam projekt, który następnie zbudujemy.

Efektem naszego budowania powinien być wyświetlony Memory utilization report.

Co nasza pierwsza aplikacja robi? Zajrzyjmy w kod:

Jak widać nie za dużo. No dobra, może nie widać. Nie każdy miał do czynienia z FreeRTOS wcześniej 🙂

Ale jak miał do czynienia z jakimkolwiek programem w C pochodnych, to domyśli się co może robić:

  1. Wyświetla Hello world!
  2. Wchodzi w pętlę 10 iteracji, przy każdym jej obrocie:
    1. wyświetla za ile system zostanie zrestarowany
    2. usypia wątek na sekundę
  3. Wyświetla Restarting now.
  4. Restartuje procesor.

Całość operacji, oczywiście możemy podejrzeć w terminalu. Ustawienia terminala:

  • Port COM – port pod którym mamy naszą płytkę
  • Szybkość transmisji – 115200

Ja używam do tego celu PuTTY.

Wynik jaki powinniśmy otrzymać w terminalu powinien być zbliżony do poniższego:

W następnym wpisie zajmę się podstawami FreeRTOS.

 

UWAGA

Pamiętajmy o poprzednim wpisie, trzeba przestawić dwie flagi w ustawieniach projektu. Najlepiej przed pierwszą kompilacją, po przestawieniu i tak nam przebuduje całość 🙂

© 2018 Paweł Szabaciuk

Theme by Anders NorenUp ↑