Această lucrare pregătitoare fixează singura tehnică fără de care restul semestrului
nu are sens: manipularea individuală a biților dintr-un registru. Funcțiile de bibliotecă precum
digitalWrite() există doar pe Arduino; măștile de biți funcționează identic pe AVR, STM32,
ESP32, PIC sau orice alt microcontroler din lume.
1Obiectivele lucrării
- Conversia rapidă între binar, hexazecimal și zecimal
- Utilizarea corectă a celor șase operatori pe biți
- Construirea măștilor cu operatorul de deplasare
- Aplicarea celor patru idiomuri fundamentale: setare, ștergere, comutare, testare
- Scrierea și citirea câmpurilor de mai mulți biți
- Înțelegerea naturii citește–modifică–scrie și a problemelor de atomicitate
- Scrierea de cod portabil, independent de biblioteca Arduino
2De ce lucrăm direct pe regiștri
Un microcontroler se configurează exclusiv prin scrierea unor valori în regiștri - locații speciale de memorie legate direct la hardware. Fiecare bit dintr-un registru comandă o funcție concretă: direcția unui pin, activarea unui periferic, alegerea unui prescaler.
| Aspect | Funcție de bibliotecă | Acces direct la registru |
|---|---|---|
| Viteză | ~50 cicluri (≈ 3 µs la 16 MHz) | 1 ciclu (62,5 ns) |
| Memorie Flash ocupată | sute de octeți de cod auxiliar | o instrucțiune |
| Portabilitate | doar unde există biblioteca | oriunde, cu alte nume de regiștri |
| Control | doar ce a prevăzut autorul bibliotecii | tot ce permite hardware-ul |
| Lizibilitate | foarte bună pentru începători | necesită documentația (datasheet) |
| Depanare | ascunde ce se întâmplă | corespondență directă cu datasheet-ul |
3Baze de numerație
Un registru de 8 biți se scrie natural în binar, dar binarul este greu de citit. Hexazecimalul este compromisul standard: fiecare cifră hexazecimală corespunde exact la 4 biți.
| Binar | Hexa | Zecimal | Binar | Hexa | Zecimal |
|---|---|---|---|---|---|
| 0000 | 0 | 0 | 1000 | 8 | 8 |
| 0001 | 1 | 1 | 1001 | 9 | 9 |
| 0010 | 2 | 2 | 1010 | A | 10 |
| 0011 | 3 | 3 | 1011 | B | 11 |
| 0100 | 4 | 4 | 1100 | C | 12 |
| 0101 | 5 | 5 | 1101 | D | 13 |
| 0110 | 6 | 6 | 1110 | E | 14 |
| 0111 | 7 | 7 | 1111 | F | 15 |
0b10110100 // binar - se vede fiecare bit 0xB4 // hexa - compact: B=1011, 4=0100 180 // zecimal - nu spune nimic despre biti // toate trei sunt IDENTICE pentru compilator
1011 0100 → B și 4 → 0xB4. Invers la fel de simplu.
Cu exercițiu, conversia devine instantanee - și veți avea nevoie de ea la fiecare pagină de datasheet.| Poziția bitului | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| Valoare zecimală | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
1 << n | 0x80 | 0x40 | 0x20 | 0x10 | 0x08 | 0x04 | 0x02 | 0x01 |
0b01100001 = 0x61 = 97.
Fig. 2.3 din îndrumar4Operatorii pe biți
| Operator | Nume | Regulă | Folosit pentru |
|---|---|---|---|
& | ȘI | 1 doar dacă ambii biți sunt 1 | testarea și ștergerea biților |
| | SAU | 1 dacă cel puțin un bit este 1 | setarea biților |
^ | SAU exclusiv | 1 dacă biții diferă | comutarea biților |
~ | negare | inversează fiecare bit | construirea măștilor de ștergere |
<< | deplasare stânga | mută biții spre stânga, completează cu 0 | poziționarea măștii |
>> | deplasare dreapta | mută biții spre dreapta | extragerea câmpurilor |
Tabelele de adevăr
| A | B | A & B | A | B | A ^ B | ~A |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 1 |
| 0 | 1 | 0 | 1 | 1 | 1 |
| 1 | 0 | 0 | 1 | 1 | 0 |
| 1 | 1 | 1 | 1 | 0 | 0 |
1 & 2 dă 0 - biții nu se
suprapun (01 și 10). Dar 1 && 2 dă 1 - ambele valori sunt nenule, deci
adevărate. Aceeași capcană există între | și ||. Confuzia produce erori
care se compilează fără nicio avertizare.Proprietățile care fac totul să funcționeze
| Proprietate | Consecință practică |
|---|---|
x | 0 = x | SAU cu 0 nu schimbă bitul → biții din mască puși pe 0 rămân neatinși |
x | 1 = 1 | SAU cu 1 forțează pe 1 → biții din mască puși pe 1 se setează |
x & 1 = x | ȘI cu 1 nu schimbă bitul → biții din mască puși pe 1 rămân neatinși |
x & 0 = 0 | ȘI cu 0 forțează pe 0 → biții din mască puși pe 0 se șterg |
x ^ 0 = x | XOR cu 0 nu schimbă bitul |
x ^ 1 = ~x | XOR cu 1 inversează bitul |
5Laborator de operatori
Comutați biții direct pe reprezentarea binară și urmăriți cum se formează rezultatul, poziție cu poziție. Verificați experimental fiecare proprietate din tabelul de mai sus.
6Construirea măștilor
O mască este o valoare în care biții care ne interesează sunt marcați cu 1. Se construiește cu operatorul de deplasare, pornind de la valoarea 1:
1 -> 0000 0001 (bitul 0) 1 << 1 -> 0000 0010 (bitul 1) 1 << 2 -> 0000 0100 (bitul 2) 1 << 5 -> 0010 0000 (bitul 5) 1 << 7 -> 1000 0000 (bitul 7) ~(1 << 5) -> 1101 1111 (toti biti 1, mai putin bitul 5) (1 << 3) | (1 << 1) -> 0000 1010 (bitii 3 SI 1 deodata)
PORTB |= 0b00100000; funcționează, dar trebuie să numărați pozițiile de fiecare dată.
PORTB |= (1 << PB5); spune explicit care bit, folosind numele din datasheet.
Compilatorul produce exact același cod mașină - deplasarea se calculează la compilare, nu la execuție.1 este un int
de 16 biți. Pentru regiștri pe 8 biți nu contează, dar pentru bitul 31 al unui registru pe 32 de biți
(STM32, ESP32) trebuie scris 1UL << 31, altfel rezultatul este nedefinit.7Simulator de măști
Alegeți o operație și o poziție de bit, apoi apăsați „aplică". Urmăriți masca generată, expresia corespunzătoare și bitul care se modifică - evidențiat cu portocaliu. Observați că restul biților rămân întotdeauna neatinși.
0b00001010. Setați
bitul 5, apoi ștergeți bitul 1, apoi comutați bitul 3 de două ori. Verificați de fiecare dată că biții
neimplicați și-au păstrat valoarea - aceasta este întreaga rațiune a măștilor.8Cele patru idiomuri fundamentale
Practic tot codul embedded din lume folosește aceste patru forme. Merită memorate ca atare:
| Operație | Idiom | De ce funcționează |
|---|---|---|
| Setare (pune pe 1) | REG |= (1 << n); | SAU cu 1 forțează 1; SAU cu 0 nu schimbă nimic |
| Ștergere (pune pe 0) | REG &= ~(1 << n); | ȘI cu 0 forțează 0; ȘI cu 1 nu schimbă nimic |
| Comutare | REG ^= (1 << n); | XOR cu 1 inversează; XOR cu 0 nu schimbă nimic |
| Testare | if (REG & (1 << n)) | ȘI izolează bitul; rezultat nenul dacă bitul e 1 |
#define BIT(n) (1UL << (n))
#define SET_BIT(reg, n) ((reg) |= BIT(n))
#define CLR_BIT(reg, n) ((reg) &= ~BIT(n))
#define TGL_BIT(reg, n) ((reg) ^= BIT(n))
#define CHK_BIT(reg, n) (((reg) & BIT(n)) != 0)
// scrie o valoare de 0 sau 1 intr-un bit, fara ramificatie
#define WRT_BIT(reg, n, v) ((reg) = ((reg) & ~BIT(n)) | ((v) ? BIT(n) : 0))
// exemple de utilizare
SET_BIT(DDRB, PB5); // pinul 13 devine iesire
SET_BIT(PORTB, PB5); // pinul 13 pe HIGH
CLR_BIT(PORTB, PB5); // pinul 13 pe LOW
TGL_BIT(PORTB, PB5); // comuta pinul 13
if (CHK_BIT(PIND, PD2)) { // butonul NU este apasat (pull-up)
// ...
}
SET_BIT(x, a + 1) s-ar expanda greșit, din cauza
priorității operatorilor. Aceasta este o sursă clasică de erori foarte greu de depistat.9Câmpuri de mai mulți biți
Multe setări nu ocupă un singur bit, ci un grup. De exemplu, prescalerul unui timer este dat de trei
biți CS12:CS10, iar canalul ADC de patru biți MUX3:MUX0.
// Sa zicem ca bitii 3, 4, 5 ai portului B formeaza un "camp" de 3 biti
// (o valoare de la 0 la 7 trimisa catre un circuit exterior), iar bitii
// 0, 1, 2 comanda cu totul altceva si NU trebuie atinsi.
// GRESIT: sterge tot restul registrului
PORTB = 0b00101000; // campul are valoarea 5... dar bitii 0-2 s-au pierdut!
// CORECT: se sterge doar campul, apoi se scrie valoarea
PORTB &= ~((1 << PB5) | (1 << PB4) | (1 << PB3)); // 1. curata campul
PORTB |= (1 << PB5); // 2. scrie valoarea noua
// Forma generala, cu masca si deplasare
#define FIELD_SET(reg, mask, shift, val) \
((reg) = ((reg) & ~((mask) << (shift))) | (((val) & (mask)) << (shift)))
// exemplu: scrie valoarea 5 in campul de 3 biti care incepe la pozitia 3
FIELD_SET(PORTB, 0x07, 3, 5);
Citirea unui câmp
// Se izoleaza campul cu ȘI, apoi se aduce la pozitia 0 prin deplasare uint8_t jumatateaDeJos = (PORTB & 0x0F); // bitii 0-3, incep la pozitia 0 // camp care NU incepe de la pozitia 0: intai deplasare, apoi masca uint8_t campul3biti = (PORTB >> 3) & 0x07; // bitii 3-5 uint8_t bitulDeSus = (PORTB >> 5) & 0x01; // un singur bit, pozitia 5 #define FIELD_GET(reg, mask, shift) (((reg) >> (shift)) & (mask))
10Citește–modifică–scrie
Expresia PORTB |= (1 << PB5); arată ca o singură operație, dar procesorul o execută
în trei pași:
PORTB |= (1 << PB5);
; se traduce in:
in r24, 0x05 ; 1. CITESTE PORTB in registrul r24
ori r24, 0x20 ; 2. MODIFICA: SAU cu masca 0b00100000
out 0x05, r24 ; 3. SCRIE r24 inapoi in PORTB
PORTB, PORTD, DDRB) și pentru o mască
constantă cu un singur bit, compilatorul folosește instrucțiunile sbi / cbi,
care sunt atomice și durează un singur ciclu. Pentru regiștrii din memoria extinsă
(TCCR1B, ADCSRA, UCSR0B) sau pentru măști calculate la execuție,
se generează secvența completă de trei instrucțiuni.11Atomicitate
O operație este atomică dacă nu poate fi întreruptă la jumătate. Problema apare când același registru sau aceeași variabilă este modificată atât din programul principal, cât și dintr-o rutină de întrerupere.
| Situație | Atomic? | Soluție |
|---|---|---|
PORTB |= (1 << PB5); - mască constantă, registru I/O jos | da (sbi) | nimic de făcut |
TCCR1B |= (1 << CS12); - registru extins | nu | secțiune critică, dacă ISR-ul atinge același registru |
Citirea unui uint16_t modificat în ISR | nu | secțiune critică obligatorie |
contor++ pe uint8_t volatile | nu | secțiune critică |
Citirea unui uint8_t volatile | da | doar volatile |
// GRESIT: reactiveaza intreruperile chiar daca apelantul le avea oprite
cli();
valoare = contor;
sei(); // <-- presupune ca erau active. Nu e garantat!
// CORECT: se salveaza si se restaureaza starea exacta
uint8_t sreg = SREG; // SREG contine bitul I (Global Interrupt Enable)
cli();
valoare = contor; // sectiune critica: nimic nu o poate intrerupe
SREG = sreg; // restaureaza EXACT starea anterioara
// CEL MAI BINE: macro din biblioteca standard AVR
#include <util/atomic.h>
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
valoare = contor; // salvarea si restaurarea se fac automat
}
volatile este suficient. Pentru structuri mai complexe
se poate folosi tehnica „double read": se citește variabila de două ori și se acceptă rezultatul doar
dacă cele două citiri coincid.12Simulator: coruperea unei operații RMW
Urmăriți exact cum o întrerupere care apare între citire și scriere face ca modificarea ei să dispară fără urmă. Este una dintre cele mai greu de depistat erori din programarea embedded, pentru că apare rar și aparent aleator.
loop(), cât și
dintr-un ISR, orice operație RMW din programul principal trebuie protejată cu
ATOMIC_BLOCK. Alternativ - și de preferat - se stabilește regula ca fiecare registru să fie
modificat dintr-un singur loc.13Regiștri mapați în memorie
Numele PORTB nu este o variabilă obișnuită: este o macrodefiniție care indică spre o
adresă fizică din spațiul de memorie, legată direct la hardware.
// In fisierele antet ale AVR: #define PORTB (*(volatile uint8_t *)0x25) // │ │ └── adresa fizica a registrului // │ └── conversie la pointer spre octet volatil // └── dereferentiere: lucram cu VALOAREA de la acea adresa // Deci PORTB |= 0x20; inseamna: *(volatile uint8_t *)0x25 |= 0x20; // Acelasi lucru se poate scrie explicit: volatile uint8_t *portb = (volatile uint8_t *)0x25; *portb |= (1 << 5);
volatile este obligatoriu aiciValoarea unui registru se poate
schimba fără ca programul să o modifice - de exemplu PIND reflectă starea fizică a
pinilor. Fără volatile, compilatorul ar citi registrul o singură dată și ar refolosi
valoarea memorată, iar bucla while (!(UCSR0A & (1 << RXC0))); nu s-ar
termina niciodată.
PIND este marcat R - doar citire.
Fig. 2.4 din îndrumar(1 << DDD4).
Read/Write spune dacă puteți scrie în el, iar Initial Value valoarea de după reset.
Veți regăsi acest format la absolut toți regiștrii, în toate lucrările următoare.14Portarea către alte controlere
Idiomurile pe biți rămân identice. Se schimbă doar numele regiștrilor și, uneori, arhitectura perifericului. Iată aceeași operație - „aprinde un LED" - pe patru platforme:
// ---------- AVR / ATmega328P (Arduino Uno) ---------- DDRB |= (1 << PB5); // pin ca iesire PORTB |= (1 << PB5); // pin pe HIGH PORTB &= ~(1 << PB5); // pin pe LOW // ---------- STM32 (seria F1/F4, registre directe) ---------- RCC->APB2ENR |= (1 << 3); // porneste ceasul portului C GPIOC->CRH |= (1 << 20); // configureaza pinul 13 ca iesire GPIOC->BSRR = (1 << 13); // pin pe HIGH (registru dedicat, ATOMIC) GPIOC->BSRR = (1 << 29); // pin pe LOW (bitul 13 + 16) // ---------- ESP32 ---------- GPIO.enable_w1ts = (1 << 2); // pin ca iesire GPIO.out_w1ts = (1 << 2); // pin pe HIGH (write-1-to-set, ATOMIC) GPIO.out_w1tc = (1 << 2); // pin pe LOW (write-1-to-clear) // ---------- PIC (seria 16F) ---------- TRISB &= ~(1 << 5); // pin ca iesire (0 = iesire la PIC!) LATB |= (1 << 5); // pin pe HIGH LATB &= ~(1 << 5); // pin pe LOW
| Aspect | AVR | STM32 | ESP32 | PIC |
|---|---|---|---|---|
| Lățime registru | 8 biți | 32 biți | 32 biți | 8 biți |
| Direcție pin | DDRx, 1 = ieșire | MODER / CRL | enable | TRISx, 0 = ieșire |
| Scriere nivel | PORTx | ODR sau BSRR | out | LATx |
| Citire pin | PINx | IDR | in | PORTx |
| Setare atomică | sbi (automat) | BSRR | w1ts/w1tc | - |
| Ceas periferic | mereu pornit | trebuie activat (RCC) | gestionat de SDK | mereu pornit |
BSRR la STM32, w1ts/w1tc la ESP32).
Scrierea în ei este o singură operație, deci atomică prin construcție - problema din secțiunea
11 dispare complet. Existența acestor regiștri este dovada că problema RMW este reală și suficient de
importantă încât producătorii să adauge hardware special pentru ea.TRISx folosește convenția
inversă față de AVR: 0 înseamnă ieșire, 1 înseamnă intrare. Codul portat mecanic funcționează
exact pe dos. Verificați întotdeauna convenția în datasheet, nu presupuneți.15Cod sursă
Afișarea binară a unei valori
// Legati 6 LED-uri pe pinii 8...13, adica pe bitii 0...5 ai portului B.
// Bara de LED-uri devine "afisajul binar" al oricarei valori pe 6 biti.
void arata(uint8_t v) {
PORTB = v & 0x3F; // bitii 0-5 ai valorii ajung pe cele 6 LED-uri
_delay_ms(1500); // pauza, ca sa aveti timp sa cititi bara
}
int main(void) {
DDRB = 0x3F; // toti cei 6 pini, iesiri
uint8_t reg = 0b00001010;
arata(reg); // initial : 001010
reg |= (1 << 5); arata(reg); // set b5 : 101010
reg &= ~(1 << 1); arata(reg); // clr b1 : 101000
reg ^= (1 << 3); arata(reg); // tgl b3 : 100000
// TESTAREA unui bit: LED-ul de pe pinul 7 arata raspunsul
DDRD |= (1 << PD7);
if (reg & (1 << 5)) PORTD |= (1 << PD7); // bitul 5 este SETAT
else PORTD &= ~(1 << PD7); // bitul 5 este sters
while (1) { }
}
Exerciții de manipulare
// 1. Numararea bitilor setati (algoritmul lui Kernighan)
uint8_t numaraBiti(uint8_t v) {
uint8_t n = 0;
while (v) {
v &= (v - 1); // sterge cel mai din dreapta bit setat
n++;
}
return n;
}
// 2. Testarea daca o valoare este putere a lui 2
bool estePutereDe2(uint8_t v) {
return v && !(v & (v - 1));
}
// 3. Inversarea ordinii bitilor
uint8_t inverseazaBiti(uint8_t v) {
uint8_t r = 0;
for (uint8_t i = 0; i < 8; i++) {
r = (r << 1) | (v & 1);
v >>= 1;
}
return r;
}
// 4. Schimbarea a doua valori fara variabila temporara
void interschimba(uint8_t *a, uint8_t *b) {
*a ^= *b; *b ^= *a; *a ^= *b;
}
// 5. Extragerea nibble-ului superior si inferior
uint8_t nibbleSus(uint8_t v) { return (v >> 4) & 0x0F; }
uint8_t nibbleJos(uint8_t v) { return v & 0x0F; }
Aceeași aplicație, trei niveluri de abstractizare
// ============ NIVELUL 1: biblioteca Arduino ============
// avantaj: lizibil imediat dezavantaj: lent, neportabil
void blinkArduino() {
pinMode(13, OUTPUT);
digitalWrite(13, HIGH);
delay(500);
digitalWrite(13, LOW);
delay(500);
}
// ============ NIVELUL 2: registre AVR, cu nume ============
// avantaj: rapid, explicit dezavantaj: legat de AVR
void blinkRegistri() {
DDRB |= (1 << PB5);
PORTB |= (1 << PB5);
_delay_ms(500);
PORTB &= ~(1 << PB5);
_delay_ms(500);
}
// ============ NIVELUL 3: strat de abstractizare propriu ============
// avantaj: rapid SI portabil - se schimba doar acest bloc la portare
#if defined(__AVR__)
#define LED_DIR_REG DDRB
#define LED_OUT_REG PORTB
#define LED_PIN_NR PB5
#elif defined(ESP32)
#define LED_DIR_REG GPIO.enable
#define LED_OUT_REG GPIO.out
#define LED_PIN_NR 2
#endif
#define LED_INIT() (LED_DIR_REG |= (1UL << LED_PIN_NR))
#define LED_ON() (LED_OUT_REG |= (1UL << LED_PIN_NR))
#define LED_OFF() (LED_OUT_REG &= ~(1UL << LED_PIN_NR))
#define LED_TOGGLE() (LED_OUT_REG ^= (1UL << LED_PIN_NR))
void blinkPortabil() {
LED_INIT();
LED_ON(); _delay_ms(500);
LED_OFF(); _delay_ms(500);
}
LED_ON(), iar portarea pe alt microcontroler înseamnă rescrierea unui singur bloc
de definiții. Veți folosi această structură la proiectul final.Măsurarea diferenței de performanță
// Nu avem inca monitorul serial (lucrarea 6), deci nu tiparim durata:
// o facem VIZIBILA. Fiecare metoda tine aprins un LED-martor cat timp
// ruleaza bucla. Cu cat metoda e mai rapida, cu atat martorul clipeste
// mai scurt. Pe osciloscop, latimea impulsului ESTE durata masurata.
#define N 20000
int main(void) {
DDRB |= (1 << PB0); // pinul care se comuta
DDRD |= (1 << PD5) | (1 << PD6) | (1 << PD7); // cei trei martori
uint16_t i;
while (1) {
PORTD |= (1 << PD7); // varianta cu functie
for (i = 0; i < N; i++) { digitalWrite(8, HIGH); digitalWrite(8, LOW); }
PORTD &= ~(1 << PD7);
_delay_ms(300);
PORTD |= (1 << PD6); // varianta cu registru
for (i = 0; i < N; i++) { PORTB |= (1 << PB0); PORTB &= ~(1 << PB0); }
PORTD &= ~(1 << PD6);
_delay_ms(300);
PORTD |= (1 << PD5); // comutare prin scriere in PINB
for (i = 0; i < N; i++) { PINB = (1 << PB0); PINB = (1 << PB0); }
PORTD &= ~(1 << PD5);
_delay_ms(300);
}
}
PINx
comută bitul corespunzător din PORTx. Este cea mai rapidă metodă de comutare a unui
pin - o singură instrucțiune, atomică. Detaliu care se găsește doar în datasheet, niciodată într-un
tutorial de Arduino.16Montaj interactiv
Aici vedeți simultan cele trei fețe ale aceluiași lucru: instrucțiunea C pe care o scrieți, bitul care se schimbă în registru și LED-ul care se aprinde. Panoul de regiștri este și interactiv - dați clic direct pe un bit ca să-l comutați manual și priviți ce se întâmplă cu montajul.
<< mută acel bit - de aceea efectul se obține dintr-o singură linie.17Sarcini de lucru
- Convertiți manual, fără calculator:
0b11010110în hexa și zecimal;0x3Fîn binar. - Implementați funcția de afișare binară și verificați-o pentru 0, 1, 128 și 255.
- Scrieți cele patru macrouri (SET, CLR, TGL, CHK) și folosiți-le pentru a comanda un LED.
- Setați simultan biții 1, 3 și 5 dintr-o variabilă, printr-o singură expresie.
- Ștergeți un câmp de 3 biți și scrieți în el valoarea 5, fără a afecta restul registrului.
- Extrageți nibble-ul superior și pe cel inferior dintr-un octet și afișați-le separat.
- Implementați numărarea biților setați și verificați rezultatul pentru
0b10110110. - Rulați benchmark-ul și notați raportul dintre cele trei metode de comutare a unui pin.
- Scrieți același program de blink în cele trei niveluri de abstractizare și comparați dimensiunea codului compilat.
- Identificați în datasheet-ul ATmega328P adresa registrului
PORTDși accesați-l prin pointer, fără a folosi numele.
18Aplicație de aprofundare
pinInit(port, bit, dir), pinWrite(port, bit, val),
pinRead(port, bit) și pinToggle(port, bit), folosind exclusiv operații pe
biți și pointeri către regiștri.
Cerințe: să funcționeze pe toate cele trei porturi (B, C, D), să fie de cel puțin 10 ori mai rapidă decât
digitalWrite() și să nu folosească nicio funcție din biblioteca Arduino.
Comparați dimensiunea codului compilat cu varianta care folosește biblioteca standard.
Extindere: adăugați o variantă
pinToggleAtomic() care folosește trucul cu
PINx și demonstrați, cu un analizor logic sau cu un al doilea pin, că este atomică.