LABORATORUL 02

Portul digital de intrare-ieșire

Durată: 2 ore Suport: Capitolul 2 Regiștri: DDRx · PORTx · PINx PDF îndrumar EN English version

Porturile digitale sunt interfața microcontrolerului cu lumea exterioară. Fiecare port este controlat de exact trei regiștri, iar înțelegerea rolului lor explică tot ce se întâmplă la nivel de pin: de la aprinderea unui LED până la citirea unui buton fără componente externe.

1Obiectivele lucrării

  • Înțelegerea rolului regiștrilor DDRx, PORTx și PINx
  • Configurarea pinilor prin acces direct la regiștri, cu operații pe biți
  • Utilizarea rezistoarelor de pull-up interne pentru citirea butoanelor
  • Implementarea eliminării vibrațiilor contactelor
  • Compararea vitezei între digitalWrite() și scrierea directă în registru

2Materiale necesare

  • 1 Arduino Uno
  • 1 Breadboard
  • 4 LED-uri
  • 4 Rezistoare 220 Ω
  • 2 Butoane
  • 10 Fire jumper

3Cei trei regiștri ai unui port

ATmega328P are trei porturi: B (pinii digitali 8–13), C (pinii analogici A0–A5) și D (pinii digitali 0–7). Fiecare este controlat de trei regiștri de câte 8 biți:

RegistruDenumire completăRolAcces
DDRxData Direction Registerstabilește direcția: 0 = intrare, 1 = ieșirecitire / scriere
PORTxData Registerla ieșire: nivelul logic
la intrare: activează pull-up-ul intern
citire / scriere
PINxPort Input Pinsreflectă starea reală a pinilordoar citire
Repartizarea pinilor ATmega328P pe porturile B, C și D
Fig. 1 - Repartizarea fizică a pinilor microcontrolerului ATmega328P pe cele trei porturi. Observați funcțiile alternative din paranteze: PCINT, ADC, OC (ieșiri de timer), INT. Fig. 2.1 din îndrumar
Regiștrii PORTD, DDRD și PIND
Fig. 2 - Cei trei regiștri ai portului D, cu adresele, numele biților și drepturile de acces. Fig. 2.4 din îndrumar

Tabela de configurare completă

DDRxnPORTxnConfigurațieRezistență echivalentăUtilizare
00intrare, flotantăfoarte mare (Z înalt)de evitat - citiri aleatoare
01intrare cu pull-up20–50 kΩ spre 5 Vcitirea butoanelor, fără componente externe
10ieșire LOW~25 Ω spre GNDstinge LED-ul / atrage sarcina la masă
11ieșire HIGH~25 Ω spre 5 Vaprinde LED-ul
Pinul flotantUn pin configurat ca intrare, fără pull-up și fără nimic conectat, se comportă ca o antenă: captează zgomotul electromagnetic din jur și citește valori care se schimbă aleator. Folosiți întotdeauna un pull-up, intern sau extern.

4Structura internă a unui pin

Vcc = 5 V GND DDRxn direcție PORTxn nivel / pull-up PINxn driver pull-up 20k PIN exterior citire spre PINxn Comutatorul pull-up se închide doar când DDRxn = 0 și PORTxn = 1
Fig. 3 - Schema simplificată a unui pin de I/O. Același pin poate fi ieșire comandată, intrare flotantă sau intrare cu pull-up, în funcție de combinația celor doi biți de control.

5Simulator de regiștri

Apăsați pe biți pentru a modifica regiștrii. Starea pinilor din partea de jos se actualizează exact după tabela de configurare - la fel ca pe microcontrolerul real.

Portul B - direcție, nivel și stare rezultată
De încercatBitul 5 din DDRB și PORTB este deja setat - corespunde pinului D13, LED-ul de pe placă, aprins. Puneți acum DDRB bit 0 pe 0 și PORTB bit 0 pe 1: pinul D8 devine intrare cu pull-up. Observați cum se schimbă eticheta din partea de jos.

6Laborator de măști

Nu se modifică niciodată tot registrul dacă se dorește schimbarea unui singur pin - s-ar afecta ceilalți șapte. Se folosesc măști de biți, construite cu operatorul de deplasare:

OperațieSintaxăEfect
Setare bit (1)PORTB |= (1 << PB5);doar bitul 5 devine 1
Ștergere bit (0)PORTB &= ~(1 << PB5);doar bitul 5 devine 0
Comutare (toggle)PORTB ^= (1 << PB5);bitul 5 își schimbă starea
Testare bitif (PINB & (1 << PB0))adevărat dacă bitul 0 este 1
Setare grupDDRB |= 0b00001111;pinii 8–11 devin ieșiri

Verificați mai jos, bit cu bit, ce face fiecare operație. Alegeți operatorul & cu operandul B pe 11011111 ca să vedeți cum se șterge exact bitul 5.

Operatori pe biți - construirea măștilor
De ce funcționează ~(1 << n) 1 << 5 produce 00100000. Negarea dă 11011111 - toți biții 1, mai puțin cel de pe poziția 5. ȘI cu această mască lasă neatinși toți ceilalți biți și forțează pe 0 doar bitul dorit.

7Scheme de montaj

Arduino Uno D2 (INPUT_PULLUP) SW GND 20k Vcc - pull-up INTERN repaus → pinul citește 1 apăsat → pinul citește 0
Fig. 4 - Buton conectat între pin și masă. Cu pull-up-ul intern activat, logica este inversată față de intuiție: apăsarea produce 0, nu 1.
Montaj practic pentru aprinderea unui LED
Fig. 5 - Montajul practic pentru aprinderea unui LED, așa cum îl veți realiza pe masa de lucru. Fig. 2.5 din îndrumar
Montaj practic pentru citirea stării unui buton
Fig. 6 - Montajul pentru citirea stării unui buton, cu alimentare de la baterii. Fig. 2.6 din îndrumar
Registrul PORTD cu bitul 0 setat pentru activarea pull-up-ului
Fig. 7 - Activarea rezistorului de pull-up pentru pinul 0 al portului D: PORTD = 0b00000001, cu DDRD având bitul 0 pe 0 (intrare). Fig. 2.7 din îndrumar

8Vibrațiile contactului

Un buton mecanic nu comută curat. Lamelele metalice vibrează câteva milisecunde la contact, generând zeci de tranziții false. Fără filtrare, o singură apăsare fizică este văzută de program ca mai multe apăsări.

semnal real la pin - cu vibrații apăsare eliberare după filtrare software (debounce 40 ms)
Fig. 8 - Sus: tranzițiile false generate de vibrația contactului. Jos: semnalul curat obținut prin filtrare software.
Principiul filtrăriiSe reține momentul ultimei schimbări observate. O stare nouă este acceptată doar dacă s-a menținut neschimbată mai mult decât intervalul de debounce (tipic 20–50 ms). Orice tranziție care apare mai devreme repornește cronometrul.

9Simulator de debounce

Urmăriți pas cu pas cum algoritmul respinge tranzițiile false și acceptă doar starea stabilă. Observați că vibrațiile de la t = 102 și t = 107 nu produc nicio incrementare.

Filtrarea vibrațiilor - execuție pas cu pas

10Cod sursă

Funcții Arduino vs. acces direct la regiștri

porturi_comparatie.ino
void setup() {
  // --- varianta Arduino: lizibila, dar lenta ---
  pinMode(13, OUTPUT);
  pinMode(2, INPUT_PULLUP);

  // --- varianta echivalenta cu registri: rapida ---
  DDRB  |= (1 << PB5);    // pinul 13 = iesire
  DDRD  &= ~(1 << PD2);   // pinul 2  = intrare
  PORTD |= (1 << PD2);    // activeaza pull-up pe pinul 2
}

void loop() {
  // citirea butonului: 0 = apasat, din cauza pull-up-ului
  if (!(PIND & (1 << PD2))) {
    PORTB |= (1 << PB5);    // aprinde
  } else {
    PORTB &= ~(1 << PB5);   // stinge
  }
}
Diferență de performanță măsurabilădigitalWrite() are nevoie de aproximativ 50 de cicluri de ceas: verifică validitatea pinului, caută portul într-un tabel din Flash și dezactivează PWM-ul dacă era activ. PORTB |= (1 << PB5) se compilează într-o singură instrucțiune. La 16 MHz, diferența este între aproximativ 3 µs și 62 ns - de circa 50 de ori.

Măsurarea diferenței de viteză

Nu avem încă monitorul serial (lucrarea 6), așa că nu tipărim durata: o facem vizibilă. Fiecare metodă comută pinul într-o buclă strânsă, iar pinul comandă un LED separat. Cu cât metoda este mai rapidă, cu atât bucla se termină mai devreme și LED-ul de „ocupat” stă aprins mai puțin. Diferența se vede cu ochiul liber, iar pe osciloscop se poate și măsura.

benchmark.ino
const uint16_t N = 20000;

void setup() {
  DDRB |= (1 << PB0);          // pinul 8: pinul care se comuta
  DDRB |= (1 << PB4);          // pinul 12: martor "ruleaza digitalWrite"
  DDRB |= (1 << PB3);          // pinul 11: martor "ruleaza varianta cu registru"
}

void loop() {
  // ---- varianta cu functie: martorul de pe pinul 12 sta aprins cat dureaza
  PORTB |= (1 << PB4);
  for (uint16_t i = 0; i < N; i++) {
    digitalWrite(8, HIGH);
    digitalWrite(8, LOW);
  }
  PORTB &= ~(1 << PB4);

  delay(400);

  // ---- varianta cu registru: martorul de pe pinul 11
  PORTB |= (1 << PB3);
  for (uint16_t i = 0; i < N; i++) {
    PORTB |=  (1 << PB0);
    PORTB &= ~(1 << PB0);
  }
  PORTB &= ~(1 << PB3);

  delay(400);
}
Ce trebuie să vedețiMartorul variantei cu digitalWrite stă aprins vizibil - zeci de milisecunde. Martorul variantei cu registru clipește atât de scurt încât abia se observă. Raportul dintre cele două durate este chiar factorul de aproximativ 50 din caseta de mai sus.

Debounce complet

debounce.ino
const uint8_t PIN_BUTON = 2;
const uint8_t PIN_LED   = 13;
const uint16_t INTERVAL_DEBOUNCE = 40;   // ms

bool stareLed      = false;
bool ultimaCitire  = HIGH;
bool stareStabila  = HIGH;
unsigned long ultimulMoment = 0;

void setup() {
  pinMode(PIN_BUTON, INPUT_PULLUP);
  pinMode(PIN_LED, OUTPUT);
}

void loop() {
  bool citire = digitalRead(PIN_BUTON);

  if (citire != ultimaCitire) {          // orice schimbare reporneste cronometrul
    ultimulMoment = millis();
    ultimaCitire  = citire;
  }

  if (millis() - ultimulMoment > INTERVAL_DEBOUNCE) {
    if (citire != stareStabila) {        // starea s-a stabilizat
      stareStabila = citire;

      if (stareStabila == LOW) {         // front descendent = apasare reala
        stareLed = !stareLed;
        digitalWrite(PIN_LED, stareLed);   // o singura comutare pe apasare
      }
    }
  }
}

Efect de lumini pe patru LED-uri

knight_rider.ino
void setup() {
  DDRB |= 0b00001111;    // pinii 8, 9, 10, 11 ca iesiri, dintr-o singura operatie
}

void loop() {
  // deplasare la stanga
  for (uint8_t i = 0; i < 4; i++) {
    PORTB = (PORTB & 0xF0) | (1 << i);   // pastreaza bitii 4-7, seteaza doar unul din 0-3
    delay(120);
  }
  // deplasare la dreapta, fara a repeta capetele
  for (int8_t i = 2; i > 0; i--) {
    PORTB = (PORTB & 0xF0) | (1 << i);
    delay(120);
  }
}
De ce (PORTB & 0xF0)Masca păstrează neatinși biții 4–7 (printre care și D13, LED-ul de pe placă) și lasă doar biții 0–3 să fie rescriși. Fără ea, atribuirea directă ar stinge tot ce se afla pe ceilalți pini ai portului.

11Montaj interactiv

Un pin de intrare se citește din PINx, nu din PORTx - iar când folosiți rezistența internă de ridicare, logica se inversează: butonul apăsat înseamnă 0. Montajele de mai jos vă lasă să apăsați butonul cu mouse-ul și să vedeți bitul din PIND schimbându-se în timp real.

Buton cu pull-up intern - de ce apăsat înseamnă zero
#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    DDRD  &= ~(1 << PD2);      // pinul 2 este INTRARE
    PORTD |=  (1 << PD2);      // rezistenta interna de ridicare, activa
    DDRB  |=  (1 << PB5);      // pinul 13 este IESIRE
    DDRD  |=  (1 << PD7);      // pinul 7 este IESIRE

    while (1) {
        if (!(PIND & (1 << PD2))) {     // bitul este 0 -> buton APASAT
            PORTB |=  (1 << PB5);       // LED-ul "apasat" se aprinde
        } else {
            PORTB &= ~(1 << PB5);
        }

        /* Al doilea LED copiaza bitul brut, fara negare.
           Priviti-le pe amandoua: sunt mereu in opozitie. */
        if (PIND & (1 << PD2)) PORTD |=  (1 << PD7);
        else                   PORTD &= ~(1 << PD7);

        _delay_ms(150);
    }
}
De încercatPriviți bitul PIND2 din panoul de regiștri: în repaus este 1, pentru că rezistența internă îl trage la 5 V. Apăsarea butonului leagă pinul la masă și bitul devine 0. Cele două LED-uri sunt mereu în opoziție - acesta este întregul înțeles al logicii inversate. Încercați să ștergeți linia PORTD |= (1 << PD2); - fără rezistența de ridicare, intrarea rămâne „în aer” și citirea devine imprevizibilă.
Exercițiu - comutator cu memorie și eliminarea vibrațiilor
#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    DDRD  &= ~(1 << PD2);      // pinul 2 = INTRARE (butonul)
    PORTD |=  (1 << PD2);      // rezistenta interna de ridicare
    DDRB  |=  (1 << PB5);      // pinul 13 = IESIRE (LED-ul)

    /* Butonul in repaus da 1. Apasat, da 0.
       Daca am scrie doar "daca este 0, comuta LED-ul", LED-ul ar comuta
       de sute de ori cat tinem butonul apasat. Noi vrem O SINGURA data,
       chiar in clipa apasarii - adica exact cand citirea TRECE din 1 in 0.
       Ca sa prindem trecerea, tinem minte ce am citit rotirea trecuta. */

    unsigned char stareAnterioara = 1;   // in repaus butonul da 1

    while (1) {
        unsigned char stareCurenta = (PIND & (1 << PD2)) ? 1 : 0;

        /* --- PASUL 1 -----------------------------------------------
           Inlocuiti 0 de mai jos cu conditia de trecere din 1 in 0:
           "inainte era 1 SI acum este 0", scris cu == si &&.
           Cat timp lasati 0, conditia e mereu falsa si LED-ul NU se
           aprinde - exact ce vedeti daca porniti acum programul.     */

        if (0) {

            /* --- PASUL 2 -------------------------------------------
               Comutati bitul PB5 din PORTB. Idiomul de COMUTARE este
               cel cu ^= , de la lucrarea trecuta. Scrieti linia aici: */



            /* --- PASUL 3 -------------------------------------------
               Lamelele butonului vibreaza cateva milisecunde. Puneti
               aici o pauza de 20 ms ca sa le lasati sa se linisteasca: */


        }

        /* --- PASUL 4 -----------------------------------------------
           Citirea de acum devine "cea anterioara" pentru rotirea
           urmatoare. Fara linia asta, comparatia nu are cu ce sa se
           faca si LED-ul nu se aprinde niciodata. Scrieti-o aici:    */


        _delay_ms(5);
    }
}
De încercatDacă porniți acum, LED-ul nu se aprinde - și este normal: condiția de la pasul 1 este scrisă if (0), adică mereu falsă, deci corpul ei nu se execută niciodată. Aveți patru linii de completat, marcate în cod. Ideea întregului exercițiu: nu ne interesează dacă butonul este apăsat, ci clipa în care a fost apăsat. Aceasta se numește front căzător și se prinde comparând citirea de acum cu cea de la rotirea anterioară a buclei - de aceea avem nevoie de variabila stareAnterioara și de pasul 4, care o ține la zi. Fără cei 20 ms de la pasul 3, un singur clic poate fi văzut ca zeci de apăsări, pentru că lamelele contactului vibrează câteva milisecunde. Verificați pe osciloscop: LED-ul trebuie să comute exact o dată la fiecare apăsare. Dacă vă blocați, butonul 💡 vezi soluția încarcă o variantă corectă în editor.

12Sarcini de lucru

  • Montați 4 LED-uri pe pinii 8–11 și aprindeți-le secvențial, folosind doar operații pe regiștri.
  • Conectați un buton pe pinul 2 cu pull-up intern; afișați pe serial valoarea în repaus și la apăsare.
  • Verificați ce se citește pe un pin de intrare lăsat flotant, fără pull-up.
  • Implementați debounce software și numărați apăsările; verificați că fiecare apăsare fizică produce exact o incrementare.
  • Modificați programul astfel încât fiecare apăsare să schimbe direcția efectului de lumini.
  • Rulați benchmark-ul și notați raportul dintre digitalWrite() și accesul direct.
  • Realizați un numărător binar pe 4 LED-uri, incrementat la fiecare apăsare de buton.
  • Încercați PORTB = 0b00000001; fără mască și observați ce se întâmplă cu LED-ul de pe D13.

13Aplicație de aprofundare

ExtindereImplementați un „zar electronic": la apăsarea butonului, LED-urile clipesc aleator timp de o secundă, apoi afișează în binar o valoare între 1 și 6. Pentru ca secvența să fie diferită la fiecare pornire, folosiți ca sămânță durata măsurată între pornirea plăcii și prima apăsare a butonului: randomSeed(millis());

Extindere suplimentară: detectați apăsarea lungă (peste 1 secundă) și folosiți-o pentru a reseta un contor al aruncărilor, afișat tot în binar pe LED-uri.

14Întrebări de verificare

15Resurse