În cartea intitulată "Expert C Programming Deep Secrets", Peter Van Der Linde descrie o serie de greșeli și probleme cauzate de folosirea limbajului de programare ANSI C fără o înțelegere deplină a acestuia. Autorul descrie felul în care au fost descoperite diverse probleme și rezolvarea lor, atenția lui orientându-se către probleme generate de folosirea greșită a compilatorului sau a unor erori de sintaxă ANSI C.
În acest articol, voi descrie un astfel de scenariu, dar axându-mă asupra părții hardware a arhitecturii unui microcontroler pe care are loc execuția codului sursă. Problemele care pot să apară sunt cauzate nu de sintaxa folosită pentru a scrie rutina software, ci de modul în care secvența software folosește resursa hardware în sistemele multicore.
Astfel de probleme pot fi distructive pentru felul în care are loc execuția codului și sunt extrem de greu de depistat/corectat, mai ales dacă înțelegerea asupra arhitecturii hardware nu este deplină. În aceste cazuri particulare conținutul acestui articol poate fi considerat drept o sumă de "bune practici".
În programarea embedded, metoda software citește-modifică-scrie este o practică uzuală. În procesul de accesare a unui registru la nivel de bit, pentru a seta sau șterge un anumit bit, orice programator se va gândi în primul rând la varianta software prin care inițial va citi registrul, va aplica o mască, iar la final va rescrie registrul cu nouă valoare.
O persoană care folosește limbajul de programare C va folosi cel mai probabil una din cele doua linii de cod pentru a seta sau șterge un bit specific dintr-un registru.
Registru |= 0x0002 //seteaza bitul 1
Registru &= 0xFFFC //sterge bitul 0 și 1
Chiar dacă în sintaxa C avem o linie de cod, după compilare vom obține o serie de instrucțiuni ASM care, prin execuția lor vor seta sau șterge bitul dorit.
Fără a intra în detalii de limbaj ASM, ne putem imagina cum ar arăta într-un mod abstract următoarea linie de cod C.
Registru |= 0x0002 //setează bitul 1
Aceasta ar însemna:
Aceștia sunt pașii pentru a realiza rutina software de citire-modificare-scriere. Acest mecanism software nu trebuie confundat cu mecanismul hardware de citire-modificare-scriere care se realizează automat în sistemele care au implementat ECC și în care o scriere se face cu o dimensiune mai mică decât lățimea busului de date. Aceasta este un alt subiect care însă nu intră în scopul acestui articol.
Într-un sistem cu un singur core nu sunt multe lucruri de adăugat despre această rutină software folosită pentru setarea sau ștergerea unui anumit bit, parte a unui registru. Mecanismul de citire-modificare-scriere funcționează fără să creeze probleme, după cum este de așteptat.
Vom discuta în continuare despre același mecanism software de citire-modificare-scriere folosit într-un sistem multi core în care se execută rutine de cod în paralel pe două sau mai multe core-uri care folosesc aceleași resurse (periferice).
Pentru a prezenta lucrurile într-un mod cât mai clar voi descrie un scenariu real. Modulul DMA este implementat uzual în arhitectura unui microcontroler, el fiind utilizat pentru transferuri de date cu scopul de a înlătura aceasta sarcină "din grija" procesorului. Din acest motiv este foarte probabil ca modulul DMA să fie folosit de toate core-urile din cadrul arhitecturii. Modulul are mai multe canale care pot fi valorificate simultan pentru a face transferuri de date, fiecare canal având câte un bit de activare. Biții de activare sunt parte a unui registru care e alcătuit din biții aferenți tuturor canalelor. Pentru a activa sau opri un singur canal este nevoie de setarea sau ștergerea unui singur bit.
Din punct de vedere software, rutinele care se execută în paralel pe două sau mai multe core-uri sunt independente și pot fi scrise de persoane diferite, având cerințe diferite.
În continuare, vom vedea ce se poate întâmpla dacă folosim rutina clasică software citire-modificare-scriere. Presupunem că unul din core-uri folosește canalul DMA 4 [CH4], iar cel de-al doilea core canalul DMA 6 [CH6].
Dacă ambele core-uri doresc să activeze canalele DMA aferente, codul va arăta ca în tabelul următor:
Numărul core-ului | Core-ul unu | Core-ul doi |
---|---|---|
Ce se execută? | DMAreg = 0x0010 | DMAreg = 0x0040 |
Fiecare core va executa propriul cod. Astfel, în ochii fiecărui programator codul scris de el trebuie să funcționeze corect. Dacă facem abstracție de la faptul că vorbim despre un sistem multicore care împarte aceleași resurse, folosirea rutinei software citește-modifica-scrie nu poate crea probleme. În realitate, nu putem ignora faptul că execuția codului are loc într-un sistem multicore.
Dacă analizăm care poate fi problema, printr-o simplă abstractizare a instrucțiunilor C (într-o rutină generică) ne vom putea imagina următoarea secvență care rulează pe cele două core-uri (nota: fiecare core are propriul registru de lucru R0):
Ce se execută? | Core-ul 1 | Core-ul 2 |
---|---|---|
PASUL 1 | Citește DMAreg în R0 | Citește din zona perifericelor |
PASUL 2 | Aplică masca valorii din R0 | Verifică un bit oarecare |
PASUL 3 | Scrie valoarea din R0 în DMAreg (activare CH4) | Fă un salt |
PASUL 4 | Citește din RAM | Citește DMAreg în R0 |
PASUL 5 | Adună x la valoarea citită | Aplică masca valorii din R0 |
PASUL 6 | Scrie în RAM noua valoare | Scrie valoarea din R0 în DMAreg (activare CH6) |
Exemplul de mai sus funcționează, dar trebuie avut în vedere faptul că execuția pe cele două core-uri este asincronă și la un moment dat putem avea următorul scenariu:
Ce se execută? | Core-ul 1 | Core-ul 2 | Valoare DMAreg |
---|---|---|---|
PASUL 1 | Citește DMAreg în R0 (ex: 0x8000) | Make a branch | 0x8000 |
PASUL 2 | Aplică masca valorii din R0 | Citește DMAreg în R0 (!!! Se va citi vechea valoare 0x8000) | 0x8000 |
PASUL 3 | Scrie valoarea din R0 în DMAreg | Aplică masca valorii din R0 | 0x8010 |
(activare CH4) | |||
PASUL 4 | Citește din RAM | Scrie valoarea din R0 în DMAreg | 0x8040 |
(activare CH6) |
Acum problema este evidentă. Primul core va activa canalul CH4 al DMA în PASUL 3, dar imediat, în PASUL 4, al doilea core va activa CH6 și va dezactiva CH4. Ambele core-uri aplică masca de activare aceleiași valori inițiale ale registrul DMAreg, adică valoarea 0x8000. La finalul PASULUI 4, valoarea dorită a registrului DMAreg este 0x8050, dar în realitate este 0x8040.
Analizate separat cele două rutine software nu au erori din punct de vedere al limbajului de programare C sau al firului propriu de execuție, dar din cauza specificului arhitecturii pe care rulează, codul induce un bug greu de detectat.
Soluțiile, după cum vă puteți imagina există, unele din ele fiind hardware. Le voi descrie în continuare pe două dintre ele.
O prima soluție este folosirea semafoarelor hardware când scriem cod într-o situație similară cu cea descrisă. În acest fel, unul din core-uri va aștepta până când semafoarele vor indica faptul că registrul a cărei valoare trebuie modificată este liber (nu este folosit de un alt master). Semafoarele hardware sunt o implementare comună (aș putea spune standard) în sistemele multicore.
A doua soluție este una specifică unui anumit tip de implementare a modulului periferic. Din păcate, nu toate perifericele vor oferi acest mecanism. Unele implementări ale perifericelor oferă registre pentru setarea sau ștergerea unui anumit bit din cadrul altor registre într-o operație atomică. Aceasta înseamnă că prin intermediul unui registru SETEAZĂ/ȘTERGE putem scrie sau șterge biți într-un alt registru (de exemplu, un registru care conține biții de activare al unui canal DMA, a unei anumite surse de întrerupere sau biți de stare) fără să afectăm alți biți, și cel mai important, fără nevoia de a citi acel registru.
Din punct de vedere al execuției codului, aceasta înseamnă:
Core-ul 1 | Core-ul 2 | Valoarea DMAreg | |
---|---|---|---|
Pasul 1 | Scrie registrul SET | Orice instrucțiune | 0x8010 |
Pasul 2 | Orice instrucțiune | Scrie registrul SET | 0x8050 |
O scriere pe același tact al acestui registru de către ambele core-uri nu este posibilă, deoarece este o resursă unică. Bazat pe prioritatea atribuită celor două core-uri, în urma unei arbitrări, doar unul din cele două core-uri va face scrierea în acel tact.
Cum cea de-a doua soluție nu este disponibilă în toate tipurile de implementări, ca regulă generală, în sistemele multicore ar trebui folosite semafoarele pentru a șterge sau scrie biți în cadrul unor registre folosite în rutine software care rulează pe mai multe core-uri. Este important de adăugat că în sistemele multicore, semafoarele pot fi emulate și software, în cazul în care implementarea hardware lipsește.
Registrele care sunt folosite de mai multe core-uri și pot fi scrise utilizând una din soluțiile prezentate anterior, fiind definite ca atare pe baza unei arhitecturi software pentru fiecare proiect în parte. Dacă un periferic este folosit de codul executat pe mai mult de un core (ex: DMA, SPI), este o bună practică pentru că modificarea conținutului registrelor să fie făcută folosind una din soluțiile hardware disponibile.
de Ovidiu Mățan
de Ovidiu Mățan
de Dan Sabadis