În laboratorul de astăzi veți învăța cum să interactionați cu diferite periferice prin I2C și cum datele de la hardware sunt transmise utilizatorului prin diferite niveluri de indirectare. Vom explora atât noțiuni low-level (modul de organizare internă a memoriei unui display) cât și aspecte mai high-level (sistemul de graphics) construite peste.
Un modul de kernel (sau un device driver) este o bucată specializată de cod care este compilată și adăugată la kernel-ul sistemului de operare. În acest mod, sistemul de operare are posibilitatea de a interacționa cu diferite periferice - în absența driverelor, un kernel nu ar putea accesa lumea exterioară CPU-ului. De exemplu, dacă ne dorim ca sistemul nostru să poată transfera date prin controller-ul de I2C, avem nevoie de un driver pentru I2C care să programeze aceste tranzacții de transfer de date. Analog, dacă ne dorim să interacționăm cu un senzor, avem nevoie de un device driver care știe să programeze acel senzor.
Din punct de vedere al user-ului, driverele pot să fie expuse în două moduri:
Atunci când vorbim de unități de măsură pentru lumină ne referim la lumeni și luxi:
Astfel, pentru a putea măsura complet o sursă de lumină, avem nevoie de ambele unități de măsură. De exemplu, o lanternă poate emite o cantitate foarte mare de lumeni, dar dacă razele sunt dispersate pe o suprafață întinsă cantitatea de luxi rezultată va fi redusă.
Altfel spus, chiar dacă ambele unități de măsură sunt relevante, intensitatea luminoasă “reală” se măsoară mai degrabă prin luxi.
LTR308 este un senzor de lumină care comunică prin I2C și este capabil să ofere utilizatorului intensitatea luminoasă exprimată în luxi. În NuttX, senzorii sunt expuși prin char device-uri in /dev/
și pot fi accesați prin apeluri de sistem, asemănător implementării Linux.
Figura de mai jos ilustrează implementarea char device-urilor în Linux - astfel, fiecare periferic este liber să își definească propriile metode de open, read, write, ioctl, close.
Pe de altă parte, deoarece NuttX este un RTOS (Real Time Operating System) care își propune să aibă un memory-footprint cât mai scăzut, design-ul pentru char device-uri (ale senzorilor, dar constrângerea se respectă intr-o mare măsura și la alte periferice) este puțin schimbat.
La nivelul sistemului de operare există o interfață uniformizată denumită upperhalf driver
(nuttx/drivers/sensors/sensor.c
), comună tutoror senzorilor, care interceptează apelurile de sistem asociate acestora. Mai departe, comenzile sunt trimise către lowerhalf driver
(nuttx/drivers/sensors/ltr308.c
) care este specific device-ului și se ocupă de interacțiunea prin I2C/SPI cu senzorul.
O altă particularitate a NuttX este că pentru a citi datele unui senzor nu se parcurge întregul lanț de comandă de la aplicație până în regiștri senzorului. În schimb, driver-ul creează un thread separat care se ocupă de polling, iar atunci când detectează că au venit date le trimite printr-o notificare în upperhalf driver într-un buffer circular. Mai departe, datele ajung la aplicație.
Un aspect interesant când vine vorba despre display-uri este cum produc ele culoarea. Dacă în cazul unui LCD avem pixeli colorați care sunt luminați pentru a produce culorile, în cazul unui OLED avem control asupra fiecarui pixel în parte.
De exemplu, pentru a produce culoarea negru un LCD va colora pixelii în negru și îi va lumina folosind un backlight. În schimb, deoarece un display de tip OLED permite control granular asupra pixelilor, este suficient să nu mai luminăm anumiți pixeli, iar astfel zona aceea de ecran va apărea neagră către utilizator.
Astfel, se spune că un display OLED poate să obțină “true darkness”.
Deși la nivel tehnic există distincția între cele două tipuri de display-uri, în NuttX ambele sunt organizate sub denumirea de LCD-uri. Este responsabilitatea fiecărui driver să interfațeze corect interacțiunea cu device-ul său asociat.
Pornind de la design-ul driver-ului de senzori, putem regăsi aceleași principii și în cazul LCD-urilor.
Cea mai simplă metodă de a interacționa cu un display este prin apeluri de sistem ioctl()
pe un LCD char device. Acestea vor fi trimise mai departe către driver-ul specific display-ului care este responsabil de configurarea lui și de afișarea mesajelor.
În cazul lui SSD1306, acesta poate fi accesat atât prin I2C, cât și prin SPI. Pe plăcile ESP32-Sparrow comunicația se realizează prin I2C.
Dacă ar fi să ne uităm în interiorul unui display, acesta este doar o matrice de pixeli organizată pe linii și coloane.
În cazul unui display cu rezoluție 128×32 precum cel de pe plăcile Sparrow, există:
Datorită acestei organizări interne putem să scriem o coloană individuală, dar pentru a scrie un rând este nevoie să scriem întreaga pagină asociată rândului.
Char device-ul pentru LCD-uri este cea mai simplă metodă de a interacționa cu un display din punct de vedere al consumului de memorie - nu se face niciun fel de buffering al datelor. De exemplu, pentru a desena două linii:
Următorul nivel de complexitate este reprezentat de framebuffer. Acesta introduce un API propriu, dar care în final apelează tot callback-uri din driver-ul device-ului. Poate cea mai importantă modificare față de un simplu char device este chiar în numele lui - oferă support de buffering.
Ultimul nivel de complexitate este reprezentat de subsistemul de graphics.
Aplicațiile interacționează în mod direct atât cu biblioteca nuttx/libs/libnx/
, cât și cu nuttx/graphics/
. Componenta principală este nxglib
și este responsabilă de interacțiunea în mod direct cu LCD-ul folosindu-se de interfața de framebuffer explicată anterior.
O altă componentă foarte importantă este nxmu
(mu - multi-user). Astfel, se folosește un mecanism de tipul client-server, în care server-ul rulează pe un thread dedicat și serializează accesul tuturor clienților (thread-urilor) care concurează pentru acelasi display.
Restul componentelor le puteți găsi detaliate în documentația oficială NuttX.
0. Folosind comanda ps
din linia de comandă a shell-ului NuttX, observați că thread-ul pentru LTR308 este suspendat în waiting. Acesta va fi planificat din nou pe CPU atunci când o aplicație va face un apel de sistem open()
.
1. Pentru a vă acomoda cu API-ul specific senzorilor, inspectați codul din apps/examples/ltr308/
, rulați aplicația și observați output-ul de la stdout.
/dev/uorb/sensor_<type>N
.sensor_ioctl()
din nuttx/drivers/sensors/sensor.c
.
2. Aplicația apps/examples/nxhello/
va afișa pe ecran “Hello, World!” după care își va termina execuția. Scopul acestui exercițiu este să afișați “Hello, World!” de mai multe ori, într-un loop. Urmăriți pașii de mai jos:
nuttx/include/nuttx/
. Puteți căuta recursiv din linia de comandă folosind grep -r <string>
.
nxhello
și inspectați output-ul. Ne interesează să aflăm cum aplicația obține date despre:nx_fill
. Căutați prototipul ei.struct nxgl_rect_s
și mai departe struct nxgl_point_s
.nx_fill
inspectați funcția nxhello_position
. Aceasta este apelată la inițializarea aplicației și este modalitatea prin care clientul primește de la server atât rezoluția display-ului cât și un handle (pointer) către o instanță a unei “ferestre”.nx_fill
pentru a șterge ecranul, iar mai apoi reapelați nxhello_hello
.
3. Bonus - Modificați aplicația nxhello
astfel încât o dată pe secundă să citiți senzorul de lumină și să afișați rezultatul pe ecran. Folosiți-vă de pthreads pentru a crea un thread separat, dedicat interacționării cu senzorul.