Laboratorul 07 - Programare în timp-real pe Arduino

Fire de execuție în Ardunio

Întrucât microcontroller-ele prezente pe Arduino sunt single-chip și single core, un singur fir de execuție poate rula simultan. Mai mult, Arduino este construit în jurul unui singur fir de execuție principal, ce rulează în interiorul funcției void loop()

void setup()
{
 // Cod ce rulează la încărcarea programului sau alimentarea plăcuței
}
 
void loop()
{
 // Cod ce rulează continuu, similar unei bucle while (true)
}

Pentru a putea diviza programul în mai multe fire de execuție și a avea o rulare cvasi-paralelă a acestora, utilizăm biblioteca FreeRTOS, pe care am instalat-o în cadrul laboratorului trecut.

Ce este FreeRTOS?

În general, un sistem de operare controlează procesele și resursele hardware ale unui calculator, definind regulile care permit unui program să acceseze servicii și să interacționeze cu sistemul de calcul.
Pentru a fi considerat de timp real, un sistem de operare trebuie să îndeplinească mai multe caracteristici, precum:

  • Respectarea limitelor de timp (hard și soft);
  • Utilizarea de algoritmi de planificare specializați și de priorități de execuție;
  • Asigurarea unui răspuns determinist la rularea succesivă a unei secțiuni de cod;
  • Utilizarea de mecanisme de comunicație între procese, multitasking, limitarea accesului mutual la resurse.

FreeRTOS reprezintă un kernel (doar baza unui sistem de operare, deoarece sistemele încorporate nu necesită toate caracteristicile unui OS generalizat), cu ajutorul căruia aplicațiile de timp real pot respecta limite de timp impuse. De asemenea, poate fi utilizat conceptul de multitasking, unde schimbarea contextului de execuție pe un procesor cu un singur nucleu conduce la execuția cvasi-paralelă a mai multor secțiuni de cod din aplicație. Firul de execuție care are acces la resursele hardware este ales de către algoritmul de planificare, prin asignarea de priorități și urmărirea perioadelor de timp în care un fir de execuție este suspendat.

FreeRTOS este open-source, ceea ce înseamnă că nu este necesară achiziționarea acestuia (după cum sugerează și numele) pentru utilizare în aplicații comerciale. Totuși, există posibilitate achiziționării de garanții suplimentare din partea dezvoltatorului Real Time Engineers Ltd., dacă situația din proiect o cere. Ca medii de dezvoltare compatibile, pot fi amintite familiile de microcontrolere ARM Cortex, Atmel SAM, Cypress PSoC, Microchip PIC etc.

Compatibilitatea cu unii dintre membrii familiei Arduino (Uno, Nano, Leonardo, Mega) vine tocmai din utilizarea microprocesoarelor amintite ulterior. În plus, kernel-ul a fost portat sub formă de bibliotecă, instalarea realizându-se din Library Manager. Trebuie specificat că toate funcțiile FreeRTOS pot fi utilizate pe Arduino, iar dintre acestea se pot aminti:

  • Preempțiune pentru taskurile cu prioritate mai mare;
  • Asignarea și modificarea priorității taskurilor;
  • Mecanisme de notificare;
  • Cozi;
  • Semafoare și mutexuri;
  • Timere software.

Toate caracteristicile menționate anterior sunt utilizate cu ajutorul API-urilor specializate. Un Application Programming Interface (API) reprezintă un grup de funcții ce pot fi apelate, fiecare având un rezultat diferit. De exemplu, crearea semafoarelor și mutexurilor, ștergerea și modificarea acestora se realizează cu ajutorul Semaphore API. Toate aceste funcții sunt definite și prezentate detaliat în documentația FreeRTOS, disponibilă pe site-ul https://www.freertos.org/.

FreeRTOS Scheduler

Tot mecanismul de funcționare FreeRTOS are la bază un scheduler, care gestionează task-urile și alte elemente real-time și realizează preempțiunea, atunci când este cazul. Funcția de pornire a scheduler-ului este vTaskStartScheduler() și, în mod normal, este apleată la sfârșitul funcției void setup(), pentru a începe gestionarea elementelor de programare real-time.

Task-uri FreeRTOS

Crearea, ștergerea și gestionarea task-urilor se realizează cu ajutorul Task and Scheduler API. Pentru a utiliza task-uri, trebuie inclusă și biblioteca task.h, alături de FreeRTOS.h.

Utilizarea FreeRTOS face ca funcția clasică void loop() să nu mai aibă nicio utilitate, întrucât toată logica programului se desfășoară în task-uri separate. Astfel, funcția va fi doar definită, pentru a nu primi erori la compilare, dar va rămâne goală

Crearea unui Task

Pentru a crea un nou task, utilizăm funcția BaseType_t xTaskCreate( TaskFunction_t pvTaskCode, const char * const pcName, unsigned short usStackDepth, void *pvParameters, UBaseType_t uxPriority, TaskHandle_t *pxCreatedTask ), care are următorii parametri:

  • pvTaskCode - pointer către funcția ce va rula la execuția task-ului
  • pcName - un nume pentru task, necesar la debugging
  • usStackDepth - dimensiune stivei de memorie ce este alocată pentru task, sub formă de număr de cuvinte ( un cuvânt de memorie = 4 bytes, pentru Arduino Uno/Mega/Nano etc)
  • *pvParameters - parametru ce va fi transmis funcției specificate la pvTaskCode
  • uxPriority - prioritatea task-ului. 0 este valoarea minimă și specifică prioritatea cea mai mică
  • *pxCreatedTask - opțional, specificarea unui hadler pentru task

Dacă valoarea întoarsă de funcție este pdPASS, atunci task-ul a fost creat cu succes.

Exemplu:

// Definim structura xStruct, cu 2 elemente
typedef struct A_STRUCT
{
 char cStructMember1;
 char cStructMember2;
} xStruct;
 
// Definim o variabilă de tipul xStruct
xStruct xParameter = { 1, 2 };
 
 
/* Define a function that creates a task. This could be called either before or after the
scheduler has been started. */
void setup()
{
 
 ...
 
 // Definim un element de tipul TaskHandle, pentru a referenția viitorul task
 TaskHandle_t xHandle;
 
 // Creăm task-ul
 if( xTaskCreate(
 vTaskCode, //Pointer către funcția ce va rula la pornirea task-ului
 "Demo task", // Un nume pentru task
 200, // Sunt alocate 200 cuvinte de memorie din stivă pentru acest task
 (void*) &xParameter, // Cast la (void*) pentru parametrul ce va fi transmis funcției
 1, // Task-ul are setată prioritatea
 &xHandle // Handle pentru task-ul creat
 ) != pdPASS )
 {
 // Task-ul nu a putut fi creat. Nu există suficient spațiu în stack.
 }
 else
 {
 // Task-ul a fost creat cu succes
 
 // Schimbăm prioritatea task-ului, utilizând handle-ul xHandle
 vTaskPrioritySet( xHandle, 2 );
 }
 
 // Pornim scheduler-ul FreeRTOS. Fără acest pas, task-ul nu ar rula. 
 vTaskStartScheduler();
 
}
 
// Funcția ce va fi rulată la execuția task-ului
void vTaskCode( void * pvParameters )
{
xStruct *pxParameters;
 
 // Cast al parametrului înapoi la tipul necesar
 pxParameters = ( xStruct * ) pvParameters;
 
 // Lucru cu parametrul
 if( pxParameters->cStructMember1 != 1 )
 {
 /* Etc. */
 }
 
 // Buclă infinită
 for( ;; )
 {
  ...
 }
}
 
// Definim funcția void loop(), dar rămâne goală, pentru că utilizăm task-uri
void loop(){}

vTaskDelay()

O funcție foarte importantă din Task API este void vTaskDelay( TickType_t xTicksToDelay );. Aceasta înlocuiește clasica funcție delay() din Arduino. Funcția plasează task-ul din care a fost apelată în starea BLOCAT pentru o perioadă de X ticks, unde X este oferit ca parametru, timp în care alte task-uri, de exemplu cele cu o prioritate mai mică, pot accesa procesorul.

Pentru a transforma timpul din milisecunde în ticks, utilizăm funcția pdMS_TO_TICKS().

Utilizarea funcției delay() nu este recomandată împreună cu FreeRTOS, deoarece conduce la suspendarea activității procesorului, adică a tutoror task-urilor care rulează. Dacă în cazul programării clasice, acest aspect nu era o problemă (exista un singur task), în cazul FreeRTOS pot apărea efecte neașteptate.

Exemplu:

void vAnotherTask( void * pvParameters )
{
 for( ;; )
 {
  ...
 // Intră în starea BLOCAT pentru 20 ticks
 vTaskDelay( 20 );
 
 // Intră în starea blocat pentru 20 ms
 vTaskDelay( pdMS_TO_TICKS( 20 ) );
 }
}

Alte funcții utile

  • vTaskDelete( TaskHandle_t pxTask ) - Șterge task-ul aferent hande-ului oferit ca parametru. Este eliberată memoria alocată automat la momentul creării task-ului
  • xTaskGetCurrentTaskHandle() - Este întors handle-ul task-ului care se află în execuție la acel moment
  • xTaskGetHandle( const char *pcNameToQuery ) - Este întors handle-ul task-ului cu numele oferit ca parametru
  • pcTaskGetName( TaskHandle_t xTaskToQuery ) - Întoarce numele task-ului referit prin handle
  • uxTaskPriorityGet( TaskHandle_t pxTask ) - Întoarce prioritatea task-ului referit prin hadle, la acel moment
  • vTaskPrioritySet( TaskHandle_t pxTask, UBaseType_t uxNewPriority ) - Setează o nouă prioritate pentru un task
  • vTaskSuspend( TaskHandle_t pxTaskToSuspend ) - Pune task-ul în starea Suspendat
  • vTaskResume( TaskHandle_t pxTaskToResume) - Pune task-ul în starea Running. Funcționează doar pentru task-uri suspendate
  • taskYIELD() - Se apelează dintr-un task Running, care iși oferă partiția de timp către alte task-uri de aceeași prioritate
patr/laboratoare/07.txt · Last modified: 2022/02/14 15:16 by alexandru.ionita99
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0