Articolul prezintă o abordare care accelerează faza de prototipare a proiectării sistemelor embedded. Acesta va ilustra modul de utilizare a unui driver hardware agnostic în combinație cu un senzor pentru a face mult mai ușoară selectarea componentelor pentru un întreg sistem embedded. Articolul descrie componentele, structura software tipică unui sistem embedded și implementarea driverului. Articolul următor (din ediția următoare), “Îmbunătățirea proiectării sistemelor embedded prin utilizarea unei abordări hardware agnostice: Implementarea driverului”, va detalia și mai mult procesul de punere în aplicare.
Introducere
Utilizarea unui driver hardware agnostic permite proiectanților să aleagă tipul de microcontroler sau procesor pentru a controla senzorul fără a depinde de hardware. Avantajul acestei abordări este că oferă opțiunea de a adăuga straturi software pe lângă cel de bază asigurat de un furnizor, precum și posibilitatea de a simplifica integrarea senzorului. Articolul va utiliza ca exemplu un senzor IMU (Inertial Measurement Unit), însă abordarea este scalabilă la alți senzori și componente. Driverul este configurat utilizând limbajul de programare C și testat cu un microcontroler generic.
Selectarea componentelor
Senzorii IMU sunt utilizați, în special, pentru detectarea mișcărilor și pentru măsurarea intensității mișcărilor prin accelerații și viteze de rotație. Senzorul IMU ADIS16500 (figura 1) a fost selectat în acest exercițiu, deoarece permite o metodă simplificată și rentabilă de a integra în sistemele industriale o detecție inerțială precisă, pe mai multe axe, în comparație cu complexitatea și investițiile asociate conceptelor discrete.
Principalele aplicații sunt:
- Navigație, stabilizare și instrumentație
- Vehicule fără pilot și autonome
- Agricultură inteligentă și utilaje de construcții
- Automatizarea fabricilor, robotică
- Realitatea virtuală/augmentată
- Internetul obiectelor în mișcare
ADIS16500 este un sistem microelectromecanic (MEMS) IMU de precizie, miniatural, care integrează un giroscop triaxial, un accelerometru triaxial și un senzor de temperatură. (Vezi figura 2). Acesta este calibrat în fabrică pentru sensibilitate, polarizare, aliniere, accelerație liniară (polarizare giroscop) și poziția (sau locația) accelerometrului. Aceasta înseamnă că măsurătorile senzorului sunt precise într-un set larg de condiții.
Microcontrolerul poate scrie și citi în această interfață regiștrii de control ai utilizatorului, precum și să citească regiștrii de date de ieșire de unde pot fi achiziționate datele accelerometrului, giroscopului sau senzorului de temperatură. Din acest motiv, a fost dezvoltat întregul software (inclusiv firmware-ul necesar) pentru a gestiona interfața. În figura 2 este ilustrat pinul DR (Data Ready). Acest pin este un semnal digital care indică momentul în care sunt disponibile noi date pentru a fi citite de la senzor. Pinul DR poate fi gestionat cu ușurință de un microcontroler, deoarece poate fi considerat ca o intrare printr-un port GPIO (General-Purpose Input/Output).
Dintr-o perspectivă hardware, senzorul IMU și microcontrolerul se vor conecta utilizând interfața SPI, care este o interfață cu 4 fire formată din pinii nCS, SCLK, DIN și DOUT. Pinul DR trebuie să fie conectat la unul dintre GPIO-urile microcontrolerului. Senzorul IMU are nevoie, de asemenea, de o tensiune de alimentare cuprinsă între 3 V și 3,6 V, deci 3,3 V este suficient.
Înțelegerea structurii software tipice unui sistem embedded
Înțelegerea structurii generice de software și firmware a unui sistem embedded este esențială pentru interfațarea cu un driver de senzor. Acest lucru îl va ajuta pe proiectant să construiască un modul software care este suficient de flexibil și ușor de integrat în orice proiect. În plus, driverul trebuie să fie implementat într-o manieră modulară, astfel încât proiectantul să poată adăuga funcții de nivel superior bazându-se pe cele existente.
Structura software a unui sistem embedded este ilustrată în figura 3. Aici, ierarhia începe cu stratul de aplicație, locul unde este scris codul aplicației. Stratul de aplicații include un fișier principal, module de aplicații care se bazează pe senzor și module care se bazează pe drivere periferice care gestionează configurația procesorului. În plus, în cadrul stratului de aplicații, există toate modulele legate de sarcinile pe care microcontrolerul trebuie să le proceseze. De exemplu, acesta include toate programele care gestionează o sarcină folosind întreruperi sau interogări, o mașină de stare și multe altele. Acest nivel al stratului variază în funcție de tipul de proiect, astfel încât fiecare proiect are coduri diferite implementate în el. În stratul aplicației, toți senzorii sistemului sunt inițializați și configurați în conformitate cu fișele lor tehnice. Toate funcțiile publice oferite de driverele senzorilor sunt apelabile. De exemplu, citirea unui registru din care pot fi extrase date sau o procedură de scriere a unui registru care va modifica o setare/calibrare.
Sub stratul aplicației se află stratul driverului senzorului, care are două tipuri de interfețe. La acest nivel, sunt implementate toate funcțiile care pot fi apelate din stratul de aplicații. În plus, prototipurile funcțiilor sunt inserate în fișierul antet al driverului (.h). Astfel, examinând fișierul antet al driverului unui senzor, puteți înțelege interfața driverului și, prin urmare, funcțiile sale apelabile de la niveluri superioare. Nivelurile inferioare vor fi interfațate cu drivere periferice specifice și dependente de microcontrolerul care gestionează senzorul. Driverele periferice includ toate modulele care gestionează perifericele microcontrolerului, cum ar fi SPI, I2C, UART, USB, CAN, SPORT etc. sau modulele care gestionează blocurile interne ale procesorului, cum ar fi timerele, memoriile, ADC-urile etc. Acestea pot fi numite funcții de nivel scăzut deoarece sunt strict legate de hardware. De exemplu, fiecare driver SPI diferă în funcție de tipul microcontrolerelor. Să luăm, ca exemplu, dispozitivul ADIS16500. Interfața este SPI, deci driverul său va fi “împachetat” cu driverul SPI al microcontrolerului (ceea ce înseamnă că software-ul care controlează ADIS16500 va folosi funcțiile și mecanismele oferite de driverul SPI al microcontrolerului pentru a realiza comunicația). Acest lucru va fi la fel pentru senzori diferiți și interfețe diferite. De exemplu, dacă un alt senzor are interfața I2C, atunci, similar, “împachetarea” cu driverul I2C al microcontrolerului va avea loc în procedura de inițializare a senzorului.
Sub nivelul driverului senzorului se află driverele periferice, care diferă în funcție de fiecare tip de microcontroler. În figura 3, există o împărțire între driverele periferice și driverele de nivel scăzut. În esență, driverele periferice oferă funcțiile de citire și scriere prin protocoalele de comunicație disponibile. Deoarece driverul de nivel scăzut va gestiona stratul fizic al semnalelor, există o dependență puternică de hardware-ul utilizat de proiectant. De obicei, straturile driverelor periferice și de nivel scăzut sunt generate din mediul integrat de dezvoltare (IDE) al microcontrolerului prin intermediul instrumentelor de vizualizare, în funcție de placa de evaluare pe care este montat microcontrolerul.
Implementarea driverului
O abordare hardware agnostică permite utilizarea aceluiași driver în diferite aplicații și, prin urmare, pentru diferite microcontrolere sau procesoare. Această abordare depinde de modul în care este implementat driverul. Pentru a înțelege implementarea driverului, ne vom uita, mai întâi, la interfață sau la fișierul antet al senzorului (adis16500.h) ilustrat în figura 4.
Fișierul antet conține macro-uri accesibile (publice) utile. Acestea includ adresele regiștrilor, viteza maximă SPI, rata implicită a datelor de ieșire (ODR), măști pe biți (bitmasks) și sensibilitatea de ieșire a accelerometrului, giroscopului și senzorului de temperatură, care sunt legate de numărul de biți (16 sau 32) cu care sunt reprezentate datele. Aceste macro-uri sunt raportate în figura 4. Sunt prezentate doar câteva adrese ale regiștrilor pentru a oferi un exemplu. Codul la care se referă articolul este disponibil în anexă. (https://www.analog.com/media/en/technical-documentation/tech-articles/leveraging-hardware-agnostic-approach-appendix.pdf)
Figura 3 din anexă prezintă toate variabilele publice și declarațiile de tip public care pot fi utilizate de fiecare modul, inclusiv adis16500.h. Aici, sunt definite noi tipuri pentru a gestiona datele mai eficient. Pentru a oferi un exemplu, tipul ADIS16500_XL_OUT este definit ca o structură care conține trei numere cu virgulă mobilă, una pentru fiecare axă (x, y și z). Există, de asemenea, o enumerare care permite senzorului să fie configurat în moduri diferite, oferind proiectantului flexibilitatea de a alege configurația care se potrivește cel mai bine nevoilor sale. Cea mai interesantă parte de aici este secțiunea care face driverul agnostic față de hardware. La începutul părții de variabile publice (figura 3 din anexă), există trei definiții de tip esențiale: pointeri către trei funcții fundamentale, sau funcții de transmisie și recepție SPI și funcția de întârziere necesară între două accesări SPI pentru a produce timpul de staționare corect. Aceste linii de cod arată, de asemenea, prototipul funcției care poate fi vizată. Funcția de transmisie SPI ia ca intrare un pointer la valoarea ce trebuie transmisă și returnează o valoare care poate fi verificată pentru a vedea dacă transmisia a avut succes. Același lucru se poate spune și despre funcția de recepție SPI care ia ca intrare un pointer către o variabilă în care va fi stocată valoarea citită la recepție. Funcția de întârziere (delay) ia ca intrare o valoare un număr cu virgulă mobilă reprezentând numărul de microsecunde pe care proiectantul dorește să le aștepte și nu are o valoare de returnare (void). În acest fel, proiectantul poate declara aceste trei funcții cu aceste prototipuri specifice, la nivelul aplicației (în fișierul principal, de exemplu). Odată declarate, acestea pot atribui cele trei funcții câmpurilor unei structuri private ADIS16500_INIT. Pentru a înțelege mai bine acest ultim pas, în figura 2 din anexă este prezentat un exemplu.
Funcțiile emițător, receptor SPI și funcția de întârziere sunt declarate ca fiind statice în fișierul principal, deci la nivelul aplicației. Acestea sunt dependente de funcțiile driverului periferic, ceea ce înseamnă că driverul senzorului nu depinde de hardware. Cele trei funcții sunt atribuite câmpurilor acestei variabile care sunt pointeri la funcții. În acest fel, proiectantul poate include senzorul și microcontrolerul fără să modifice codul driverului senzorului. Dacă proiectantul schimbă microcontrolerul, trebuie doar să ajusteze fișierul principal prin înlocuirea funcțiilor de nivel scăzut din interiorul celor trei funcții statice cu funcțiile corespunzătoare pentru noul microcontroler. Această abordare face ca driverul să fie agnostic față de hardware, deoarece proiectantul nu trebuie să modifice codul driverului senzorului. Funcțiile de nivel scăzut, precum spiSelect, spiReceive, spiUnselect, chThdSleepMicroseconds etc., sunt, de regulă, deja disponibile în IDE-ul microcontrolerului. În acest caz specific, placa de evaluare a microcontrolerului utilizată a fost SDP-K1, care integrează un microcontroler STM32F469NIH6 Cortex®-M4. IDE-ul a fost într-adevăr ChibiOS, un mediu de dezvoltare Arm® gratuit.
Figura 4 din anexă prezintă prototipuri ale funcțiilor apelabile de la nivelul aplicației. Aceste prototipuri se află în fișierul antet al driverului senzorului (adis16500.h), împreună cu toate celelalte software-uri și firmware-uri discutate în figurile 2 și 3 din anexă. Mai întâi, există funcția de inițializare (adis16500_init) ce ia ca intrare un pointer la o structură ADIS16500_INIT și returnează un cod de stare care precizează dacă inițializarea a avut succes. Implementarea funcției de inițializare se face în fișierul sursă (adis16500.c) al driverului senzorului. Figura 5 din anexă prezintă codul pentru funcția adis16500_init. Mai întâi se definește un tip numit ADIS16500_PRIV, care conține cel puțin toate câmpurile structurii ADIS16500_INIT, apoi se declară o variabilă privată numită _adis16500_priv, aparținând acestui tip. În cadrul funcției de inițializare, toate câmpurile structurii ADIS16500_INIT transmise de nivelul de aplicație vor fi atribuite câmpurilor variabilei private _adis16500_priv. Aceasta înseamnă că toate apelurile ulterioare către driverul senzorului vor utiliza funcțiile de scriere și citire SPI și funcția de întârziere a procesorului, care au fost transmise de stratul de aplicație. Acesta este un punct cheie, deoarece face ca driverul senzorului să fie agnostic din punct de vedere hardware. Dacă proiectantul dorește să schimbe microcontrolerul, trebuie doar să schimbe funcțiile pe care le trece în funcția adis16500_init. Nu trebuie să modifice codul driverului senzorului în sine. La începutul funcției de inițializare, câmpul de inițializare al variabilei _adis16500_priv este setat “false” deoarece procesul de inițializare nu a fost încă finalizat. La sfârșitul funcției, înainte de returnare, acesta va fi setat “true”. De fiecare dată când proiectantul apelează o altă funcție publică (figura 4 din anexă) se efectuează următoarea verificare: dacă _adis16500_priv.initialized este “true” se poate continua, dacă este “false” se va returna imediat o eroare denumită ADIS16500_RET_VALERROR. Scopul este de a împiedica utilizatorii să apeleze o funcție fără a inițializa mai întâi driverul senzorului. Continuând cu discuția despre funcția de inițializare, se efectuează următorii pași:
- Se verifică ID-ul produsului, care este cunoscut a priori, prin citirea registrului ADIS16500_REG_ PROD_ID.
- Setarea polarității pinului Data Ready (DR) prin scrierea registrului ADIS16500_REG_MSC_CTRL în câmpul de biți corespunzător, cu valoarea transferată de la nivelul de aplicație (c).
- Setați modul de sincronizare prin scrierea registrului ADIS16500_REG_MSC_CTRL în câmpul de biți corespunzător, cu valoarea transferată de la nivelul de aplicație (c).
- Setați rata de decimare prin scrierea registrului ADIS16500_REG_DEC_RATE, cu valoarea transferată de la nivelul de aplicație (c).
Funcția de inițializare depinde de funcțiile de citire și scriere a regiștrilor (figura 6 din anexă). Acesta este motivul pentru care cele patru rutine de mai sus se fac după atribuirile variabilei _adis16500_priv. În caz contrar, atunci când sunt apelate funcțiile de registru de citire sau de scriere, acestea nu ar ști ce funcții de transmițător SPI, de receptor și de întârziere a procesorului să utilizeze.
Referindu-ne la figura 4 din anexă, există și alte funcții publice care pot fi apelate după funcția de inițializare. O descriere a funcționalității rutinelor implementate este prezentată mai jos, arătându-le pe cele de nivel scăzut. A doua parte a articolului va trece în revistă detaliile funcțiilor implementate ale altor drivere. Toate funcțiile următoare trebuie să fie apelate numai după funcția de inițializare. Din acest motiv, se va face o dublă verificare la începutul fiecărei funcții pentru a vedea dacă senzorul a fost inițializat sau nu. Dacă răspunsul este nu, atunci procedura returnează imediat o eroare.
-
adis16500_rd_reg_16
Această funcție este utilizată pentru a citi un registru pe 16-biți. Implementarea sa este disponibilă în figura 6 din anexă. Intrările sunt ad (o variabilă uint8_t care reprezintă adresa registrului care trebuie citit) și *p_reg_val (un pointer către o variabilă de tip uint16_t, care reprezintă locul în care va fi atribuită valoarea citită). Pentru a efectua citirea unui registru prin protocolul SPI, sunt necesare două accesări SPI; primul pentru a transmite adresa, al doilea pentru a citi înapoi valoarea registrului adresat. Între cele două accesări este necesar un timp de staționare, de aceea este necesară o funcție de întârziere. În timpul primei accesări se transmite bitul de citire/scriere, care în acest caz este 1 (R = 1, W = 0), cu adresa registrului decalată cu 8 biți plus 8 biți la 0, deci următoarea secvență:
R/W | AD6 | AD5 | AD4 | AD3 | AD2 | AD1 | AD0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Unde AD reprezintă adresa, iar R/W este bitul de citire/scriere.
După o întârziere, funcția citește valoarea prin SPI și o trece la pointerul de intrare. Regiștrii dispozitivului ADIS16500 au o adresă “high” care conține valoarea “high” (cei mai semnificativi 8 biți) și o adresă “low” care conține valoarea “low” (cei mai puțin semnificativi 8 biți). Pentru a obține întreaga valoare (“low” și “high”) de 16 biți este suficient să utilizați adresa “low” ca ad, deoarece adresele “low” și “high” sunt succesive.
-
adis16500_wr_reg_16
Această funcție este utilizată pentru a scrie într-un registru de 16-biți. Implementarea sa este disponibilă în figura 6 din anexă. Intrările sunt ad (o variabilă de tip uint8_t reprezentând adresa registrului ce urmează să fie scrisă) și reg_val (o variabilă de tip uint16_t, reprezentând valoarea ce urmează să fie scrisă în registru). În ceea ce privește funcția de citire, adresele și valorile “low” și “high” trebuie să fie luate în considerare. Din acest motiv, conform fișei tehnice, scrierea în registrul ADIS16500 necesită două accesări SPI la transmitere. Primul va trimite bitul R/W egal cu 0, urmat de adresa de registru “low” urmată de valoarea “low”, astfel încât secvența va fi următoarea:
R/W | AD6 | AD5 | AD4 | AD3 | AD2 | AD1 | AD0 | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | Unde D reprezintă date.
A doua accesare a transmițătorului SPI va trimite bitul R/W egal cu 0 urmat de adresa registrului “high” urmată de valoarea “high”, astfel încât secvența va fi următoarea:
R/W | AD14 | AD13 | AD12 | AD11 | AD10 | AD9 | AD8 | D15 | D14 | D13 | D12 | D11 | D10 | D9 | D8 |.
Funcțiile de scriere și citire a regiștrilor ar putea fi, în realitate, definite și ca private și, prin urmare, nevizibile și apelabile din afara modulului software al driverului. Motivul pentru care acestea sunt definite ca fiind publice este acela de a permite depanarea. Acest lucru permite proiectantului să acceseze rapid orice registru din senzor pentru citire sau scriere, ceea ce poate fi util pentru depanarea problemelor.
-
adis16500_rd_acc
Această funcție citește datele de accelerație x, y, z din regiștrii de date de ieșire și returnează valorile acestora în [m/sec2]. Implementarea sa este disponibilă în figura 7 din anexă. Intrarea este un pointer către o structură ADIS16500_XL_OUT, care înglobează, pur și simplu, trei câmpuri: accelerația x, y, z, exprimată ca un număr cu virgulă mobilă. Modul de citire a accelerației este același pentru toate cele trei axe, singurele diferențe fiind regiștrii care urmează să fie citiți. Fiecare axă are propriul registru: axa x trebuie citită pe registrul de date de ieșire al accelerației x, iar axele y și z, în consecință. Valoarea accelerației va fi reprezentată printr-o valoare pe 32-biți, deci vor trebui citiți doi regiștri. Unul pentru cei mai semnificativi 16 biți și unul pentru cei mai puțin semnificativi 16 biți. Din acest motiv, aruncând o privire la program, există două accesări de citire a regiștrilor cu operații corespunzătoare de deplasare (shift) și OR pe biți. Aceste operații permit stocarea întregii valori binare pe o variabilă int32_t privată numită _temp. În acest moment va avea loc conversia din binar în complement față de doi. După conversie, valoarea complement față de doi este împărțită la un factor de sensibilitate exprimat în [LSB/(m/sec2)], astfel încât valoarea finală va fi accelerația exprimată în [m/sec2]. Această valoare va fi înregistrată în câmpul x, y sau z al pointerului la structura care a fost transferată ca intrare.
-
adis16500_rd_gyro
Funcția de citire a giroscopului face exact același lucru ca funcția de citire a accelerației. Evident, aceasta va citi datele giroscopului x, y, z exprimate în [°/sec]. Implementarea sa este descrisă în figura 8 din anexă. Intrarea funcției este, ca și în cazul accelerației, un pointer către o structură ADIS16500_GYRO_OUT care încorporează datele giroscopului x, y și z, exprimate ca număr cu virgulă mobilă. Regiștrii citiți sunt regiștrii de date de ieșire ai giroscopului. Valoarea binară va fi reprezentată cu 32 de biți și sunt necesari aceiași pași ca pentru accelerație pentru a ajunge la valoarea în complement față de doi. După conversia din binar în complement față de doi, valoarea va fi împărțită la un factor de sensibilitate exprimat în [LSB/(°/sec)], astfel încât valoarea finală va fi exprimată în [°/sec] și va fi înregistrată în câmpul x, y sau z al pointerului la structura care a fost transmisă ca intrare. (LSB – Least Significant Bit)
Concluzie
În acest articol, a fost ilustrată o stivă software/firmware tipică unui sistem embedded. A fost lansată implementarea driverului senzorului IMU. O abordare hardware agnostică oferă o metodă repetabilă pentru diferiți senzori sau componente, chiar dacă interfețele (SPI, I2C, UART etc.) sunt diferite. Articolul următor “Îmbunătățirea proiectării sistemelor embedded prin utilizarea unei abordări hardware agnostice: Implementarea driverului”, va explica în detaliu implementarea driverului pentru senzori.
Autor: Giacomo Paterniani, Field Applications Engineer
Despre autor
Giacomo Paterniani a obținut o diplomă în inginerie biomedicală la Universitatea din Bologna. Și-a finalizat masteratul în inginerie electronică la Universitatea din Modena și Reggio Emilia. După absolvire, a petrecut un an ca cercetător la Universitatea din Modena și Reggio Emilia. În aprilie 2022, s-a alăturat programului pentru absolvenți al Analog Devices în calitate de inginer absolvent de aplicații de teren. În aprilie 2023, a devenit FAE.
Vizitați https://ez.analog.com