Studentul care apasă butonul de încărcare în Arduino IDE trăiește un moment de magie: câteva secunde mai târziu, un LED clipește. Între apăsare și clipire se petrec însă o duzină de operații distincte, executate de tot atâtea programe separate, niciuna vizibilă pe ecran. Deschidem astăzi această cutie neagră: ce înseamnă, la propriu, a programa un microcontroler, prin ce forme trece textul scris de noi până devine biți în memoria Flash, cum arată fișierul care ajunge la programator, și de ce mesajele de eroare din primele proiecte aproape niciodată nu indică adevărata cauză a problemei.
1Obiectul și structura cursului7 min
A programa un microcontroler înseamnă, la nivelul cel mai concret, a modifica starea unor celule de memorie nevolatilă aflate în interiorul cipului, astfel încât, la următoarea alimentare, unitatea centrală să găsească acolo o secvență de instrucțiuni pe care le poate executa. Nimic mai mult. Microcontrolerul nu „înțelege" C, nu știe ce e o funcție și nu are noțiunea de fișier - are un numărător de program care indică o adresă în Flash, citește de acolo un cuvânt de 16 biți, îl interpretează ca instrucțiune, o execută și trece mai departe.
A doua diferență ține de locul unde se lucrează. Programul e scris, compilat și transformat pe
un calculator gazdă, cu altă arhitectură decât ținta. Un compilator care rulează pe x86 și produce
cod pentru un AVR pe 8 biți se numește compilator încrucișat (cross-compiler), iar
ansamblul uneltelor care îl însoțesc formează un lanț de unelte (toolchain) - pentru AVR,
acesta se numește avr-gcc.
- Memoria Flash păstrează programul și rămâne scrisă după oprirea alimentării, spre deosebire de SRAM, care se golește (Cursul 02)
- Unitatea centrală citește instrucțiuni din Flash prin numărătorul de program, pe o magistrală separată de cea de date - arhitectura Harvard (Cursul 02)
- Frecvența oscilatorului determină durata unui ciclu de instrucțiune; vom vedea astăzi ce se întâmplă când programul presupune altă frecvență decât cea reală (Cursul 02)
- Să enumerați, în ordine, cele cinci transformări prin care trece codul sursă până la biți în Flash, cu unealta și fișierul rezultat pentru fiecare
- Să explicați de ce fișierul .elf nu poate fi trimis direct programatorului
- Să citiți o linie de fișier Intel HEX și să verificați suma ei de control
- Să comparați programarea prin bootloader cu cea prin ISP, ca hardware necesar și spațiu consumat
- Să estimați ocuparea memoriei Flash și SRAM a unui program și să recunoașteți simptomele epuizării fiecăreia
- Să identificați cauza reală a unui program care „merge de două ori mai încet" sau se resetează singur
2Etapele programării: de la sursă la ELF12 min
Un mediu integrat afișează o singură comandă, „Compilează" sau „Încarcă". În spate se execută, în ordine, cinci transformări succesive, fiecare producând un fișier intermediar cu un rol precis. Mediile integrate șterg fișierele intermediare la sfârșit - ceea ce nu înseamnă că nu au existat, ci doar că studentul nu are ocazia să le vadă.
1. Preprocesarea
Prima unealtă care atinge codul sursă nu e compilatorul, ci preprocesorul - el nu
cunoaște C, pentru el fișierul e doar text. Execută directivele care încep cu diez
(#include, #define, #ifdef) și livrează compilatorului un
text din care acestea au dispărut. Explică de ce un program de cincisprezece linii ajunge, după
preprocesare, la zeci de mii de linii: antetul <avr/io.h> trage după el fișierul
specific dispozitivului, cu definițiile tuturor registrelor și biților lor. Când scriem
PORTB, preprocesorul îl înlocuiește cu adresa fizică a registrului; compilatorul nu
vede niciodată numele, ci doar accesul la o adresă.
2. Compilarea propriu-zisă
Compilatorul primește textul preprocesat și produce asamblare pentru arhitectura țintă. Aici se pierde definitiv structura de nivel înalt: buclele, funcțiile, tipurile de date devin instrucțiuni AVR, iar variabilele sunt repartizate în cele 32 de registre generale sau, dacă nu încap, în SRAM.
volatileDacă o variabilă e modificată într-o rutină de întrerupere, dar programul principal doar o
citește într-o buclă, compilatorul, care analizează numai bucla, constată că nimeni nu o
modifică și o citește o singură dată, păstrând-o într-un registru - bucla devine infinită, deși
variabila se schimbă în memorie. volatile interzice exact această optimizare: nu e
un truc, ci o informație pe care programatorul o are și compilatorul nu.
3. Asamblarea
Asamblorul traduce unu-la-unu mnemonicele în cod mașină binar - nicio decizie de optimizare,
doar corespondență fixă. Rezultatul, fișierul .o, nu e cod executabil: e organizat pe
secțiuni (.text pentru instrucțiuni, .data pentru variabile globale
inițializate nenul, .bss pentru cele inițializate cu zero, .rodata pentru
constante) și e incomplet - dacă apelează o funcție din alt fișier, asamblorul nu-i cunoaște adresa
și lasă un gol, notat în tabela de relocări.
4. Editarea de legături
Linker-ul primește toate fișierele obiect și bibliotecile, cu trei sarcini: rezolvarea
simbolurilor (caută definiția fiecărui nume nedefinit local - de aici erorile „undefined
reference" și „multiple definition"), alocarea adreselor (concatenează secțiunile de
același fel și le așază în spațiul de adrese, conform unui script de legare - pentru ATmega328P,
.text începe la adresa 0 din Flash, .data/.bss în SRAM) și
relocarea (completează golurile lăsate de asamblor, acum că adresele sunt cunoscute).
.data din Flash în SRAM, umple .bss cu zero și abia apoi apelează
main. Valorile inițiale ale variabilelor globale ocupă deci spațiu și în Flash, și în
SRAM - o observație care revine la discuția despre epuizarea SRAM.Rezultatul e un fișier executabil ELF - conține codul relocat complet, plus informația de
depanare (nume de simboluri, corespondența adrese-linii, tipuri de variabile), esențială pentru
orice depanator și motiv pentru care fișierul .elf nu trebuie aruncat.
3De la ELF la memoria Flash9 min
Un programator de circuite nu are nevoie de simboluri sau informație de depanare - are nevoie
de un răspuns la o singură întrebare: ce octet se scrie la ce adresă. Din ELF se extrag doar
secțiunile care ajung efectiv în Flash (.text și imaginea inițială a lui
.data) și se transcriu în formatul Intel HEX, cu utilitarul
avr-objcopy.
Codul mașină e deja gata în ELF - conversia e o simplă reambalare, dintr-un container complex
într-un format textual minimal. Din același ELF se extrage separat conținutul destinat EEPROM,
în fișierul .eep.
| Extensie | Produs de | Conținut |
|---|---|---|
| .i | preprocesor | sursa cu includerile rezolvate |
| .s | compilator | cod în asamblare AVR |
| .o | asamblor | cod mașină pe secțiuni, adrese nerezolvate |
| .elf | linker | executabil relocat, cu simboluri |
| .hex | objcopy | perechi adresă-octet, pentru programator |
| .map | linker | adresa fiecărui simbol |
Transferul în memorie
Ultima etapă e singura care implică hardware. Un program de pe calculatorul gazdă
(avrdude - AVR Downloader Uploader) citește fișierul .hex, îl împarte în
pagini și le trimite, printr-un protocol serial, către logica de scriere din Flash-ul
microcontrolerului.
Pentru ATmega328P, o pagină are 64 de cuvinte (128 octeți), iar cei 32 KB de Flash sunt împărțiți în 256 de pagini. Tehnologia Flash permite trecerea unui bit din 1 în 0 prin scriere, dar nu invers - readucerea la 1 se face numai prin ștergere, tot la nivel de pagină sau de cip întreg. Secvența corectă e deci: ștergere, apoi încărcarea paginii într-un registru tampon, apoi comanda de scriere. Numărul de cicluri de ștergere-scriere e finit, de ordinul zecilor de mii - suficient pentru dezvoltare, dar relevant dacă cineva ar vrea să folosească Flash-ul ca memorie de date rescrisă frecvent.
După scriere se recomandă verificarea: programatorul recitește conținutul și îl compară cu sursa. O nepotrivire indică alimentare insuficientă, frecvență de programare prea mare, sau memorie uzată.
.hex nu ating microcontrolerul și nu depind de
el - pot rula fără ca placa să fie conectată. Doar ultima etapă cere hardware. Dacă mesajul de
eroare apare înainte de bara de progres a încărcării, problema e în cod sau în configurare, nu în
cablu, placă sau port.4Formatul fișierului Intel HEX10 min
Fișierul .hex e ultimul obiect pe care îl poate inspecta un om înainte ca
programul să dispară în cip - și singurul din lanț suficient de simplu pentru a fi citit cu ochiul
liber. Formatul, definit în anii 1970 pentru transferul prin legături seriale lente și nesigure,
e text ASCII pur, fiecare linie e autonomă și poartă propria adresă, iar fiecare linie are o sumă
de control.
:BBaaAATTDD...DDCCaaAA - adresa de la care se scriu octeții, octetul cel mai semnificativ primul.
TT - tipul înregistrării.
DD...DD - octeții de date propriu-ziși, exact codul mașină generat de compilator.
CC - suma de control a liniei.
| Tip | Denumire | Rol |
|---|---|---|
| 00 | Date | octeți de program, de scris la adresa indicată |
| 01 | Sfârșit de fișier | ultima linie, nu conține date |
| 04 | Adresă liniară extinsă | octeții superiori de adresă, peste 64 KB (nu apare la ATmega328P) |
Se dă linia :0A000000182800000000000000288E. Identificați câmpurile și verificați
suma de control.
Vezi rezolvarea
Numărătorul de octeți e 0A (10 octeți de date). Adresa e 0000. Tipul e
00 (date de program). Urmează cei 10 octeți de date: 18 28 00 00 00 00 00 00 00
28. Ultimul octet, 8E, e suma de control.
Se adună toți octeții, fără suma de control:
0A+00+00+00+18+28+00×7+28 = 72₁₆ = 114₁₀.
Complementul față de doi al lui 72₁₆ = 0111 0010₂: se inversează biții
(1000 1101) și se adună unu: 1000 1110 = 8E₁₆. Coincide cu octetul din
fișier - linia e corectă.
Verificare alternativă, mai rapidă: 72+8E = 100₁₆, al cărui octet inferior e zero -
exact ce cere proprietatea sumei de control: suma tuturor octeților liniei, inclusiv suma de
control, dă zero pe opt biți.
Ultima linie a oricărui fișier Intel HEX e întotdeauna :00000001FF - zero octeți de
date, tip 01, sumă de control FF (complementul lui 01). Absența ei face ca programatorul să refuze
fișierul, considerându-l trunchiat.
O primă linie tipică: numărător 10 (16 octeți), adresă 0000, tip
00, date 0C 94 34 00 urmat de 0C 94 3E 00 repetat de trei
ori.
Vezi explicația
Octeții se citesc doi câte doi, în ordine inversă (AVR stochează cuvintele cu octetul inferior
primul): perechea 0C 94 e cuvântul 940C, codul instrucțiunii de salt
absolut jmp; perechea următoare dă adresa destinație. Ceea ce vedem la începutul
memoriei e deci tabela de vectori de întrerupere - o listă de salturi, câte unul pentru
fiecare sursă de întrerupere. Prima intrare, la adresa 0, e vectorul de resetare și sare la codul
de pornire; celelalte, nefolosite în acest program, sar toate la rutina implicită care doar
resetează microcontrolerul.
5Structura memoriei Flash și bootloader-ul10 min
Cine scrie în Flash? Memoria e internă cipului, iar terminalele lui nu expun magistrala de
memorie direct. Există două răspunsuri: ori un dispozitiv extern preia controlul cipului printr-o
interfață serială dedicată, ori microcontrolerul se programează singur, executând un program deja
prezent în el, folosind instrucțiunea spm (store program memory), care scrie o pagină
în Flash. Programul care face asta se numește bootloader.
Cele două secțiuni ale memoriei Flash
Pentru siguranță, Flash-ul lui ATmega328P e împărțit în secțiunea de aplicație (partea de
jos, de la adresa zero, programul utilizatorului) și secțiunea de pornire (partea de sus,
bootloader-ul). Distincția e impusă hardware: instrucțiunea spm are efect numai când
PC-ul se află în secțiunea de pornire - un program de aplicație obișnuit nu poate rescrie Flash-ul
nici din greșeală, nici intenționat.
La resetare, bootloader-ul inițializează interfața de comunicație și așteaptă câteva sute de
milisecunde un mesaj de sincronizare. Dacă sosește, primește pagini de cod, le scrie cu
spm și sare la adresa zero. Dacă nu sosește, sare direct la aplicație - explicația
secundei de întârziere observată la alimentarea unei plăci Arduino.
Bootloader-ul Optiboot, folosit pe Arduino Uno, ocupă 512 octeți - de aceea mediul Arduino raportează 32256 de octeți disponibili, nu 32768. Dacă bootloader-ul e șters (de exemplu printr-o programare ISP care execută o ștergere completă), microcontrolerul continuă să execute perfect aplicația, dar pierde posibilitatea de a mai fi programat serial - orice modificare ulterioară cere un programator extern, până la rescrierea bootloader-ului. Aceasta e funcția comenzii „Burn Bootloader": programează imaginea bootloader-ului prin ISP și setează siguranțele BOOTRST și BOOTSZ corespunzător.
6Programarea modulelor Arduino8 min
Pe o placă Arduino Uno, pe lângă ATmega328P, un al doilea circuit se ocupă de conversia USB-serial - calculatorul gazdă îl vede ca pe un port serial virtual, ale cărui linii de transmisie/recepție sunt legate la terminalele RXD/TXD ale microcontrolerului principal. Acest circuit doar transportă octeți, nu programează nimic.
Resetarea automată
Bootloader-ul ascultă doar imediat după resetare. Pe plăcile vechi, utilizatorul trebuia să apese resetarea exact la momentul potrivit. Soluția actuală: linia DTR a portului serial virtual e legată la terminalul de resetare printr-un condensator mic. Când programul de pe calculator deschide portul, comută DTR, iar condensatorul transmite doar frontul, generând un impuls scurt de resetare - placa se resetează singură exact la momentul potrivit.
avrdude -C avrdude.conf -p atmega328p -c arduino \
-P /dev/ttyUSB0 -b 115200 -D \
-U flash:w:prog.hex:i
-p indică microcontrolerul țintă, -c protocolul de programare,
-P portul, -b viteza, -U descrie operația (memoria flash,
scriere, fișierul, formatul Intel HEX). -D dezactivează ștergerea completă a cipului -
tocmai pentru a nu distruge bootloader-ul.
Protocolul de dialog se numește STK500: gazda cere sincronizarea, întreabă semnătura cipului, trimite adresa și conținutul fiecărei pagini, cere scrierea, trece la pagina următoare. Bootloader-ul Optiboot implementează doar un subset, suficient pentru scrierea în Flash - nu poate scrie siguranțele sau citi EEPROM, tocmai pentru a rămâne în cei 512 octeți alocați.
Viteza de comunicație trebuie să corespundă exact între cele două capete, fiind fixată în codul bootloader-ului (Optiboot: 115200 b/s; bootloadere mai vechi: 57600). Cea mai frecventă cauză a erorii e un tip de placă greșit selectat în mediul de dezvoltare față de placa conectată efectiv.
7Programarea prin ISP9 min
Programarea în sistem (ISP - In-System Programming, sau ICSP) e modalitatea de bază, care nu depinde de niciun cod deja prezent în microcontroler. Folosește interfața serială sincronă SPI a cipului, într-un regim special activat prin menținerea terminalului de resetare în stare activă - cât timp cipul e ținut în resetare, unitatea centrală nu execută nimic, iar logica de programare preia controlul prin MOSI/MISO/SCK.
Dialogul: activare programare, citirea semnăturii cipului (trei octeți: fabricant și model), ștergere, încărcarea paginilor, verificare.
Ceasul SCK aplicat de programator nu poate depăși un sfert din frecvența de ceas a microcontrolerului, pentru că logica de programare e sincronizată intern cu ceasul sistemului. Un cip nou, pe oscilatorul intern divizat (ceas de 1 MHz), acceptă cel mult 250 kHz pe SCK - un programator configurat pentru o frecvență mai mare va raporta că nu găsește niciun dispozitiv, deși legăturile sunt corecte. Majoritatea programatoarelor au o opțiune „ceas lent" pentru această situație.
| Criteriu | Bootloader (serial) | ISP (SPI) |
|---|---|---|
| Hardware necesar | doar cablul USB | programator dedicat |
| Cod prezent în cip | bootloader deja scris | niciunul; funcționează pe cip nou |
| Spațiu Flash consumat | 512 octeți sau mai mult | niciunul |
| Acces la siguranțe / biți de blocare | nu | da |
| Poate scrie bootloader-ul | nu | da |
| Întârziere la pornire | da (fereastra de ascultare) | nu |
| Utilizare tipică | dezvoltare curentă, actualizări | prima programare, reparare, producție |
8Limbaje de programare și costul bibliotecilor11 min
Un microcontroler poate fi programat, în principiu, în orice limbaj cu compilator pentru arhitectura respectivă. În practică, alegerea e restrânsă de resursă: cu 32 KB de Flash și 2 KB de SRAM, nu există loc pentru o mașină virtuală, un colector de gunoaie sau o bibliotecă standard bogată.
Asamblare, C, C++
Asamblarea dă codul cel mai compact și cel mai rapid posibil, cu consum de resurse perfect cunoscut - esențial în bucle cu termen strict sau întreruperi cu timp garantat. Devine însă greu de citit și neportabilă dincolo de câteva zeci de linii; astăzi se folosește punctual, în rutine scurte și critice.
C ocupă poziția dominantă, din motive de fond: tipurile corespund unităților naturale ale procesorului, operatorii pe biți permit manipularea directă a registrelor, pointerii permit accesul la adrese fizice. Un compilator bun generează, pentru cod scris cu grijă, un rezultat comparabil cu asamblarea scrisă de om. Prețul: o curbă de învățare abruptă și nicio plasă de siguranță - C permite fără să protesteze scrierea în afara unui tablou sau un pointer neinițializat.
C++ adaugă clase și încapsulare - bibliotecile Arduino (Serial,
Servo) beneficiază vizibil. Costul: alocarea dinamică, tratarea excepțiilor și
identificarea tipurilor la execuție consumă memorie pe care 2 KB de SRAM n-o pot susține, de aceea
practica embedded folosește un subset disciplinat, fără aceste mecanisme.
Ce ascund bibliotecile Arduino
Mediul Arduino ascunde registrele în spatele unor funcții cu nume citibile - alegere pedagogică bună la început, costisitoare mai târziu.
/* varianta cu functii Arduino */
void setup() { pinMode(13, OUTPUT); }
void loop() { digitalWrite(13, HIGH); digitalWrite(13, LOW); }
/* aceeasi operatie, direct in registre */
#include <avr/io.h>
int main(void) {
DDRB |= (1 << PB5);
while (1) {
PORTB |= (1 << PB5);
PORTB &= ~(1 << PB5);
}
}
Diferența de comportament e nulă - pe placa Uno, pinul 13 e chiar PB5. Diferența de cost e mare:
PORTB |= (1<<PB5) generează instrucțiunea sbi, doi cicluri.
digitalWrite(13, HIGH) traduce numărul de pin în port și bit citind două tabele din
Flash, verifică dacă pinul e atașat unui canal PWM și, dacă da, îl detașează, dezactivează
întreruperile, modifică registrul și le reactivează - zeci de cicluri, în loc de două.
ATmega328P la 16 MHz. Estimați frecvența maximă a unui semnal dreptunghiular obținut prin
comutarea unui pin în buclă, cu digitalWrite (~50 cicluri/apel) față de acces direct
la registru (sbi/cbi, 2 cicluri).
Vezi rezolvarea
T_clk = 1/(16×10⁶) = 62,5 ns. O perioadă completă cere o comutare în sus și una în
jos.
Cu bibliotecă: 2×50 = 100 cicluri, T ≈ 100×62,5 ns = 6,25 µs,
f ≈ 160 kHz.
Cu acces direct: 2×2 = 4 cicluri, T ≈ 4×62,5 ns = 250 ns,
f ≈ 4 MHz.
Raportul e ~25, exact raportul numărului de cicluri. Pentru aprinderea unui LED sau citirea unui buton, diferența e complet irelevantă - omul nu percepe microsecunde. Pentru generarea unui semnal, comunicație implementată prin program, sau o întrerupere care trebuie să se încheie repede, diferența decide dacă aplicația funcționează.
9Depanarea unui microcontroler9 min
Depanarea unui microcontroler e structural mai grea decât pe un calculator: nu are ecran, consolă sau sistem de fișiere pentru un jurnal; interacționează cu lumea fizică în timp real (un motor nu se oprește pentru un punct de întrerupere); multe defecte sunt la granița cod-hardware (un fir prost lipit, o alimentare care cade), invizibile unui depanator software; iar resursele necesare depanării - memorie de program, pini - sunt exact resursele care lipsesc.
Depanare la nivel hardware
Pentru AVR-urile mici, mecanismul se numește debugWIRE - folosește un singur fir, chiar terminalul de resetare, transformat într-o legătură bidirecțională de comandă: oprește procesorul, citește/scrie registre și memorie, execută pas cu pas, fixează puncte de întrerupere.
debugWIRE se activează prin siguranța DWEN - odată activată, terminalul de resetare nu mai funcționează ca resetare obișnuită, deci programarea prin ISP, care depinde de el, devine imposibilă. Ieșirea se face printr-o comandă specială a depanatorului; dacă legătura se pierde înainte, cipul cere programare paralelă la înaltă tensiune, cu echipament specializat. Plăcile Arduino Uno nu expun comod acest mod, pentru că resetarea e deja legată la circuitul de resetare automată.
JTAG (patru-cinci terminale, pe AVR mari și pe majoritatea ARM) și SWD (varianta
redusă la două terminale, specifică ARM) oferă aceleași funcții de bază - puncte de întrerupere,
execuție pas cu pas, inspecția variabilelor și a stivei - funcționând pentru că dispozitivul de
depanare citește fișierul .elf și știe ce adresă corespunde cărei linii de cod.
De ce mesajele pe serială rămân unealta principală
Nu cere hardware suplimentar (legătura există deja, aceeași cu cea de încărcare), nu oprește execuția (sistemul continuă să funcționeze în timp ce raportează), permite observarea evoluției în timp - o valoare care derivează lent se vede într-un jurnal, nu într-o oprire.
Consumă timp - la 9600 b/s, un mesaj de treizeci de caractere durează peste treizeci de milisecunde, o eternitate pentru o întrerupere. Consumă SRAM (secțiunea următoare). Și, cel mai perfid, poate schimba comportamentul programului: un defect cauzat de o sincronizare strânsă poate dispărea când adăugăm tipăriri și reapărea când le scoatem. Când suspiciunea cade pe timp, o alternativă mai bună e comutarea unui pin liber, observat pe osciloscop sau analizor logic.
10Flash plin, SRAM epuizată: bugetul de memorie11 min
Memoria Flash plină
Cea mai simplă eroare, semnalată clar de mediul de dezvoltare. Cauza obișnuită nu e codul scris de student, ci bibliotecile incluse - o bibliotecă grafică, de comunicație în rețea, sau folosirea aritmeticii în virgulă mobilă pot adăuga fiecare mii de octeți. Din cei 32768 de octeți, bootloader-ul consumă deja 512.
Soluții, în ordinea efortului: eliminarea bibliotecilor neutilizate, renunțarea la virgula
mobilă unde întregii sunt suficienți, mutarea tabelelor constante în Flash cu PROGMEM,
și, în ultimă instanță, eliminarea bootloader-ului și programarea prin ISP.
Memoria SRAM epuizată
Mult mai insidioasă - nu produce niciun mesaj. Programul se compilează, se încarcă, pornește, funcționează un timp, apoi dă rezultate absurde sau se resetează singur.
SRAM găzduiește trei lucruri: variabilele globale/statice (secțiunile .data și
.bss, cunoscute la compilare), alocarea dinamică (dacă se folosește), și stiva, care
crește de la adresa cea mai mare spre adrese mai mici. Compilatorul raportează dimensiunea primei
categorii, dar nu poate ști cât de adânc va crește stiva la execuție. Când stiva coboară peste
zona variabilelor globale, începe să le suprascrie, și invers - fără nicio unitate de protecție
care să oprească asta.
Un program folosește douăzeci de mesaje constante de câte treizeci de caractere, tipărite pe serială. Estimați consumul de SRAM.
Vezi rezolvarea
Un literal "Temperatura masurata: " e, implicit, copiat din Flash în SRAM de codul
de pornire - AVR are spații de adrese separate pentru program și date, iar pointerii obișnuiți
indică în SRAM. Douăzeci de mesaje de treizeci de caractere: 20×30 = 600 octeți, aproape
o treime din memoria disponibilă, fără ca programatorul să fi declarat vreo variabilă.
Remediul: macroul F() din biblioteca Arduino (respectiv atributul
PROGMEM și funcțiile _P în C simplu) păstrează șirul în Flash și îl
citește caracter cu caracter, la nevoie, cu instrucțiunea lpm.
/* consuma SRAM */
Serial.println("Temperatura masurata: ");
/* pastreaza sirul in Flash */
Serial.println(F("Temperatura masurata: "));
11Ceasul și alte confuzii7 min
Ceasul configurat altfel decât presupune codul
Se manifestă prin întârzieri greșite și comunicație serială ilizibilă. Codul nu măsoară timpul,
numără cicluri: _delay_ms calculează numărul de iterații pornind de la macroul
F_CPU, definit la compilare; din același F_CPU se calculează și
divizorul de viteză al interfeței seriale.
Un program compilat cu F_CPU = 16000000 conține _delay_ms(500).
Microcontrolerul funcționează însă pe oscilatorul intern la 1 MHz. Cât durează în realitate?
Vezi rezolvarea
Compilatorul a calculat ciclurile presupunând frecvența declarată:
N = 0,5 s × 16×10⁶ = 8×10⁶ cicluri - număr fix, înscris în cod.
La execuție, ciclurile se consumă la 1 MHz: t = 8×10⁶ / 1×10⁶ = 8 s, de șaisprezece
ori mai mult decât cele 0,5 s intenționate - exact raportul frecvențelor (16 MHz / 1 MHz = 16).
Metodă de diagnostic: se măsoară o întârziere cunoscută și se calculează frecvența reală ca produs dintre frecvența declarată și raportul dintre durata așteptată și cea măsurată.
Programat prin ISP, la care nimeni nu a setat siguranțele pentru oscilatorul extern, sau când în mediul de dezvoltare e selectată o placă cu alt cristal decât cel montat fizic. Verificare simplă: o buclă care aprinde LED-ul o secundă și îl stinge o secundă, cronometrată - dacă perioada observată e de opt sau șaisprezece ori mai mare decât cea așteptată, cauza e ceasul, nu programul.
Alte confuzii frecvente
Un tip de placă greșit selectat duce la viteză de comunicație greșită cu bootloader-ul. Portul
serial ocupat de altă aplicație face încărcarea să eșueze fără explicație clară. Un pin folosit
simultan de aplicație și de interfața ISP (PB3, PB4, PB5) poate împiedica programarea dacă circuitul
extern încarcă prea mult liniile. Omiterea volatile la variabile partajate cu
întreruperi produce un program care funcționează la optimizare zero și se blochează la nivelul
implicit - sugerând, greșit, o eroare de compilator.
12Erori frecvente4 min
- „Programul s-a compilat fără erori, deci este corect." Compilarea verifică doar sintaxa și tipurile. Un program care încape în Flash, dar epuizează SRAM la execuție, se compilează impecabil și se comportă aleatoriu pe placă. Citiți raportul de memorie afișat la sfârșitul compilării, nu doar linia „Done compiling", și urmăriți procentul de SRAM ocupat de variabilele globale.
- „Am modificat codul, dar placa se comportă la fel - probabil e o problemă de hardware." Cel mai adesea încărcarea nu a avut loc: portul selectat e greșit, placa aleasă în meniu nu corespunde, sau transferul a eșuat, iar în Flash a rămas programul anterior. Verificați mesajul de la sfârșitul încărcării (numărul de octeți scriși și verificați) înainte de a bănui hardware-ul.
- „Pot copia fișierul .elf direct pe microcontroler, e doar un executabil."
Fals - ELF conține adrese, secțiuni și informație de depanare pe care programatorul nu le
poate interpreta; microcontrolerul nu are sistem de fișiere și nici încărcător.
Programatorului i se trimite fișierul Intel HEX, obținut prin conversie cu
avr-objcopy; .elf se păstrează doar pentru depanare. - „Am setat F_CPU în cod, deci microcontrolerul va rula la frecvența aceea." F_CPU nu comandă nimic în hardware - e doar o constantă folosită de compilator pentru a calcula bucle de întârziere. Frecvența reală vine din oscilator și din biții de configurare. Potriviți F_CPU cu oscilatorul real; dacă întârzierile ies de două ori mai lungi sau mai scurte, acolo e cauza, nu în cod.
13Rezumat și glosar5 min
A programa înseamnă a scrie într-o memorie nevolatilă la adrese fizice exacte, nu a instala un
fișier. Lanțul de transformare merge din codul sursă prin preprocesare, compilare, asamblare și
editare de legături până la un executabil ELF, apoi prin conversie la formatul textual Intel HEX,
scris pe pagini în Flash de avrdude. Flash-ul lui ATmega328P se împarte în secțiunea
de aplicație și secțiunea de pornire, unde locuiește bootloader-ul - un program care se scrie
singur, prin spm, activat doar în zona lui, cu prețul a 512 octeți și al unei
întârzieri la pornire. ISP e alternativa universală, care nu depinde de niciun cod prezent în cip
și are acces la siguranțe. C domină programarea microcontrolerelor pentru apropierea lui de
mașină; bibliotecile Arduino tranzacționează viteză și memorie pentru lizibilitate. Depanarea e
structural mai grea decât pe un calculator - mesajele pe serială rămân unealta principală, iar cele
mai insidioase defecte (SRAM epuizată, ceas nepotrivit) nu produc niciun mesaj de eroare.
14Întrebări de verificare7 min
- Descrieți, în ordine, transformările prin care trece un fișier sursă C până la înscrierea codului în Flash, precizând unealta și fișierul rezultat pentru fiecare etapă.
- Explicați de ce fișierul .elf nu poate fi transmis direct programatorului și ce informație se pierde la conversia în Intel HEX.
- Care sunt cele trei sarcini ale editorului de legături? Ce înseamnă relocarea?
- De ce un program obișnuit de aplicație nu poate scrie în propria memorie Flash, și ce condiție hardware trebuie îndeplinită pentru ca spm să aibă efect?
- Comparați programarea prin bootloader cu ISP din punctul de vedere al hardware-ului necesar, al spațiului consumat și al accesului la biții de configurare.
- Explicați mecanismul prin care o placă Arduino se resetează singură la începutul încărcării, și legați-l de faptul că deschiderea monitorului serial repornește programul.
- Un program folosește douăzeci de mesaje constante de câte patruzeci de caractere. Estimați consumul de SRAM și explicați cum îl reduce macroul F().
15Direcții de aprofundare2 min
Cursul următor coboară de la lanțul de unelte la periferia cea mai simplă și mai folosită a unui microcontroler: porturile de intrare-ieșire - configurarea lor pe biți, citirea și scrierea directă prin registre, și capcanele electrice ale unui pin digital (flotant, pull-up, multiplexare).