CURSUL 03

Programarea unui Microcontroler

Durată: 121 min de predare Nivel: licență, anul II - recomandat după Cursul 02 Disciplină: Microcontrollere și Microprocesoare PDF: descarcă suportul EN English version

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.

Prima diferență majoră față de un calculator personal Pe un PC, programul rămâne un fișier pe disc; sistemul de operare îl încarcă în RAM, îi rezolvă legăturile cu bibliotecile, îi dă un spațiu de adrese virtual și îl pornește. Microcontrolerul nu are sistem de operare, disc, sau încărcător - programul trebuie să se afle deja în memoria nevolatilă, la adresa fizică exactă la care unitatea centrală îl va căuta. De aceea procesul se numește programare, nu instalare: nu copiem un fișier undeva, ci înscriem permanent o structură de biți într-o memorie.

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.

Cele două jumătăți ale drumului
Prima e pur software și se petrece integral pe calculatorul gazdă: scriere, compilare, editare de legături, până la un fișier în formatul înțeles de programator. A doua e o operație de transfer și scriere fizică în memorie, urmată de execuția codului pe microcontroler. Cursul urmează exact această împărțire.
Recapitulare din Cursul 02
  • 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

main.csursapreprocesor.icompilator.sasamblor.olinker.elfobjcopy.hexavrdudeFlashfiecare cutie este un program separat, invizibil in mediul integratsub fiecare cutie: fisierul pe care il produce
Lantul de unelte, de la textul scris de programator pana la bitii din memoria Flash. Mediul integrat afiseaza o singura comanda, dar executa toate aceste programe pe rand, stergand fisierele intermediare la final.

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.

Optimizatorul și capcana lui volatile

Dacă 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).

Codul de pornire pe care nu l-am scris niciodată Linker-ul adaugă codul de pornire (crt), care ocupă primele adrese din Flash, instalează tabela de vectori de întrerupere, inițializează indicatorul de stivă, copiază conținutul secțiunii .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.

Lanțul de unelte, pas cu pas: puneți etapele în ordine

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.

Conversia nu compilează nimic

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.

ExtensieProdus deConținut
.ipreprocesorsursa cu includerile rezolvate
.scompilatorcod în asamblare AVR
.oasamblorcod mașină pe secțiuni, adrese nerezolvate
.elflinkerexecutabil relocat, cu simboluri
.hexobjcopyperechi adresă-octet, pentru programator
.maplinkeradresa 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.

Scrierea în Flash se face pe pagini, nu pe octeți

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ă.

O distincție utilă la depanare Etapele până la generarea fișierului .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.
Potriviți fișierul cu ce conține

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.

Structura unei linii: :BBaaAATTDD...DDCC
BB - numărul de octeți de date de pe linie (uzual 16, adică 0x10).
aaAA - 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.
TipDenumireRol
00Dateocteți de program, de scris la adresa indicată
01Sfârșit de fișierultima linie, nu conține date
04Adresă liniară extinsăocteții superiori de adresă, peste 64 KB (nu apare la ATmega328P)
Exercițiu rezolvat - verificarea sumei de control

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.

Prima linie a unui program AVR - tabela de vectori de întrerupere

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.

Dimensiunea secțiunii de pornire nu e fixă Se alege dintre patru valori (256, 512, 1024 sau 2048 cuvinte), prin doi biți de configurare permanentă (fuses) BOOTSZ1/BOOTSZ0. O a treia siguranță, BOOTRST, decide dacă execuția pornește de la adresa zero (direct din aplicație) sau din secțiunea de pornire (din bootloader) - siguranțe ce nu se modifică prin bootloader, ci numai printr-un programator extern.

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.

Ce se pierde dacă bootloader-ul dispare

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.

Un cip nou de la distribuitor
Nu are bootloader și nu poate fi programat serial. Pentru a-l folosi pe o placă de tip Arduino trebuie mai întâi programat o dată, prin ISP, cu bootloader-ul și siguranțele potrivite - același motiv pentru care un cip înlocuit după o defecțiune nu răspunde la încărcarea prin USB decât după această operație inițială.

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.

De ce monitorul serial repornește programul Deschiderea monitorului serial deschide și ea portul serial virtual, comutând aceeași linie DTR - de aceea placa se resetează și programul o ia de la capăt. Nu e o eroare, e efectul mecanismului de resetare automată.
Comanda avrdude generată de mediul Arduino
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.

Eroarea „not in sync"

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.

Constrângerea de frecvență

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.

CriteriuBootloader (serial)ISP (SPI)
Hardware necesardoar cablul USBprogramator dedicat
Cod prezent în cipbootloader deja scrisniciunul; funcționează pe cip nou
Spațiu Flash consumat512 octeți sau mai multniciunul
Acces la siguranțe / biți de blocarenuda
Poate scrie bootloader-ulnuda
Întârziere la pornireda (fereastra de ascultare)nu
Utilizare tipicădezvoltare curentă, actualizăriprima programare, reparare, producție
Cele două moduri nu se exclud, ci se completează În dezvoltarea obișnuită se folosește bootloader-ul, pentru că e suficient și rapid. Programatorul ISP devine indispensabil la prima punere în funcțiune a unui cip nou, când trebuie modificată configurația de ceas sau protecție, și când aplicația are nevoie de fiecare octet de Flash și de o pornire instantanee - caz în care bootloader-ul se elimină deliberat. O placă Arduino poate juca și ea rolul de programator ISP pentru un al doilea cip, prin schița ArduinoISP - același montaj cu care se repară o placă al cărei bootloader a fost șters din greșeală.

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ă.

Exercițiu rezolvat - frecvența maximă a unui semnal generat prin program

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ă.

Nu un motiv de a evita bibliotecile, ci un compromis explicit Bibliotecile Arduino oferă siguranță și comoditate în schimbul vitezei și memoriei. Un program bine scris folosește funcțiile de bibliotecă în părțile necritice, unde lizibilitatea contează, și coboară la registre acolo unde contează fiecare ciclu.

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.

Capcana siguranței DWEN

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.

Limitele tipăririi pe serială

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.

Cel mai ieftin instrument de diagnostic Înainte de a bănui codul, verificați alimentarea și că microcontrolerul chiar rulează. O buclă care aprinde intermitent un LED, plasată la începutul programului, răspunde în trei secunde dacă cipul pornește, dacă ceasul e cel presupus și dacă programul a ajuns efectiv în Flash.

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.

Mecanismul, în detaliu

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.

Exercițiu rezolvat - consumul ascuns al șirurilor constante

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: "));
Semnul de alarmă practic Dacă variabilele globale ocupă peste ~75% din SRAM, spațiul rămas pentru stivă devine riscant, iar comportamentul aleatoriu e de așteptat.
Bugetul de memorie: încape programul în ATmega328P?

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.

Exercițiu rezolvat

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ă.

Situația apare tipic la un cip nou

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.

Numitorul comun: simptomul e departe de cauză Metoda care ajută cel mai mult e reducerea: se pornește de la cel mai mic program care funcționează sigur (clipirea unui LED) și se adaugă câte un element o dată, verificând după fiecare pas. E mai lentă decât căutarea directă a erorii, dar e singura metodă care converge întotdeauna.

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.

Toolchain
ansamblul uneltelor care transformă codul sursă în cod executabil pentru altă arhitectură.
Linker
rezolvă simbolurile, alocă adresele și relochează secțiunile din fișierele obiect.
Intel HEX
format text pentru transferul de cod către un programator, cu sumă de control pe fiecare linie.
Bootloader
program care rulează pe microcontroler și îl reprogramează singur, prin instrucțiunea spm.
ISP
programare în sistem, prin SPI, cu microcontrolerul ținut în resetare.
debugWIRE
depanare hardware pe un singur fir, prin terminalul de resetare.

14Întrebări de verificare7 min

  1. 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ă.
  2. Explicați de ce fișierul .elf nu poate fi transmis direct programatorului și ce informație se pierde la conversia în Intel HEX.
  3. Care sunt cele trei sarcini ale editorului de legături? Ce înseamnă relocarea?
  4. 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?
  5. 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.
  6. 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.
  7. 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).