LABORATORUL 06

Comunicația serială

Durată: 2 ore Suport: Capitolele 8–9 Regiștri: UBRR0 · UCSR0A/B/C · UDR0 PDF îndrumar EN English version

Comunicația serială permite schimbul de date între microcontroler și calculator, senzori inteligenți sau alte plăci. Este cea mai folosită interfață din practică - și singura prin care puteți vedea ce se întâmplă în interiorul unui sistem fără ecran.

1Obiectivele lucrării

  • Înțelegerea structurii cadrului asincron: start, date, paritate, stop
  • Calculul registrului UBRR și al erorii de baud rate
  • Configurarea modulului USART prin regiștri
  • Compararea standardelor TTL, RS-232 și RS-485
  • Recepția și interpretarea comenzilor primite de la calculator
  • Implementarea recepției prin întrerupere

2Materiale necesare

  • 2 Plăci Arduino Uno
  • 1 Breadboard
  • 1 LED + rezistor 220 Ω
  • 4 Fire jumper
  • 1 Modul MAX485 (opțional)

3Sincron vs. asincron

În comunicația sincronă (SPI, I²C), un fir separat transportă semnalul de ceas care marchează momentul citirii fiecărui bit. În comunicația asincronă (UART) nu există fir de ceas: ambele dispozitive trebuie configurate la aceeași viteză, iar sincronizarea se reface la fiecare cadru, pornind de la frontul bitului de start.

De ce funcționează fără ceas comunReceptorul eșantionează fiecare bit la mijlocul intervalului său, folosind un ceas intern de 16 ori mai rapid decât rata de baud. Sincronizarea se pierde treptat, dar un cadru are doar 10 biți - nu apucă să devină problemă, dacă eroarea de viteză rămâne sub 2%.

4Structura cadrului

ElementLungimeValoareRol
Linie în repaus-1 (HIGH)indică lipsa transmisiei
Bit de start1 bitîntotdeauna 0frontul descendent sincronizează receptorul
Biți de date5–9 (uzual 8)date utiletransmiși cu LSB primul
Bit de paritate0 sau 1 bitpară / imparădetectarea erorilor simple
Bit(i) de stop1 sau 2 bițiîntotdeauna 1marchează sfârșitul și separă cadrele
Eficiența transmisieiPentru un caracter de 8 biți fără paritate, cadrul complet are 10 biți. Doar 80% din trafic reprezintă date utile - restul este suprasarcină de sincronizare. La 9600 baud rezultă maximum 960 de caractere pe secundă.

5Constructor de cadru UART

Scrieți un caracter și urmăriți cum se construiește cadrul pe fir: bitul de start, cei opt biți de date transmiși cu LSB primul, paritatea opțională și bitul de stop. Schimbați viteza și observați cum se modifică durata fiecărui bit și eroarea de baud.

Cadru UART - formă de undă și temporizare
De încercatSelectați 115200 baud și observați eroarea de −3,55%. Aceasta este cauza pentru care, la viteze mari, comunicația pe un Arduino cu cristal de 16 MHz devine nesigură - mai ales dacă și celălalt capăt are o eroare de semn opus.

6Calculul vitezei de transmisie

Formula UBRR
UBRR = F_CPU / (16 · baud) − 1
Rezultatul se rotunjește la un întreg, ceea ce introduce o eroare. Comunicația funcționează sigur dacă eroarea totală, a ambelor capete, rămâne sub 2%.
Baud doritUBRR (16 MHz)Baud realEroareVerdict
2 4004162 398−0,08 %excelent
9 6001039 615+0,16 %excelent
19 2005119 231+0,16 %excelent
38 4002538 462+0,16 %bun
57 6001658 824+2,12 %⚠ la limită
115 2008111 111−3,55 %⚠ nesigur
De ce 16 MHz nu este ideal pentru viteze mariPlăcile cu cristal de 14,7456 MHz produc eroare zero pentru toate vitezele standard, fiind un multiplu exact al frecvențelor de baud. Alternativ, bitul U2X0 din UCSR0A înjumătățește divizorul și reduce eroarea la 115200 baud de la −3,55% la +2,12%.

7Regiștrii USART

Configurația implicită de mai jos este cea standard „8N1": 8 biți de date, fără paritate, un bit de stop, recepție și transmisie activate.

UCSR0B și UCSR0C - configurarea modulului serial
Registrul UCSR0A
Fig. 1 - Registrul UCSR0A - de stare: RXC0 semnalează sosirea unui octet, UDRE0 că se poate transmite, iar FE0/DOR0/UPE0 raportează erori de cadru, suprascriere și paritate. Fig. 6.2 din îndrumar
Registrul UCSR0B
Fig. 2 - Registrul UCSR0B - de control: activează întreruperile (RXCIE0, TXCIE0), receptorul și emițătorul (RXEN0, TXEN0). Observați că RXB80 este marcat doar-citire. Fig. 6.3 din îndrumar
Registrul UCSR0C
Fig. 3 - Registrul UCSR0C - formatul cadrului: UMSEL alege modul (asincron/sincron), UPM paritatea, USBS0 numărul de biți de stop, iar UCSZ dimensiunea caracterului. Fig. 6.4 din îndrumar
BitRegistruRol
RXC0UCSR0A1 = a sosit un octet complet, gata de citit
UDRE0UCSR0A1 = registrul de transmisie este liber
U2X0UCSR0Adublează viteza; reduce eroarea la baud mari
RXCIE0UCSR0Bactivează întreruperea la recepție
RXEN0 / TXEN0UCSR0Bpornesc receptorul, respectiv emițătorul
UPM01:UPM00UCSR0C00 = fără paritate · 10 = pară · 11 = impară
USBS0UCSR0C0 = un bit de stop · 1 = doi biți
UCSZ01:UCSZ00UCSR0C11 = 8 biți de date

8Standarde electrice

StandardNiveluriDistanță max.TopologieUtilizare
TTL / UART0 V / 5 V~1 mpunct la punctîntre plăci, pe același cablaj
RS-232±3…±15 V, logică inversată~15 mpunct la punctaparate de măsură, echipamente industriale vechi
RS-485diferențial ±1,5…5 V~1200 mmultipunct, până la 32 noduriindustrial, Modbus, automatizări
RS-485: două fire cu semnale în opoziție A B o perturbație afectează IDENTIC ambele fire… …iar receptorul citește DIFERENȚA A − B, deci zgomotul comun se anulează complet.
Fig. 4 - Principiul transmisiei diferențiale. Aceasta este explicația pentru care RS-485 ajunge la 1200 m, în timp ce UART-ul TTL se limitează la circa un metru.

9UART, SPI și I²C - comparație

CriteriuUARTSPII²C
Fire necesare2 (+ masă)42
Ceasnu (asincron)dada
Viteză tipică9,6–115 kbit/spână la 8 Mbit/s100–400 kbit/s
Număr dispozitive2multe (câte un pin CS fiecare)până la 127 (adresare)
Duplexfull duplexfull duplexhalf duplex
Pini pe UnoD0, D1D10–D13A4 (SDA), A5 (SCL)
Utilizare tipicăPC, module GPS/GSMcarduri SD, display-uri, nRF24senzori, EEPROM, RTC

10Schema de montaj

Arduino A Arduino B TX (D1)RX (D0)GND TX (D1) RX (D0) GND TX se leagă întotdeauna la RX - firele se încrucișează. Masa comună este obligatorie.
Fig. 5 - Conectarea a două plăci prin UART. Fără masă comună, nivelurile logice nu au referință și comunicația eșuează, chiar dacă firele de date sunt corecte.
Conflict cu programarea plăciiPinii D0 și D1 sunt folosiți și de bootloader. Dacă ambele plăci sunt legate între ele, încărcarea unui sketch nou poate eșua. Deconectați firele în timpul programării sau folosiți SoftwareSerial pe alți pini.

11Simulator: parser de comenzi

Datele sosesc caracter cu caracter, nu ca șiruri complete. Urmăriți cum se acumulează caracterele într-un buffer până la primirea terminatorului de linie, moment în care comanda devine completă și poate fi interpretată.

Recepția comenzii „ON" - caracter cu caracter
Greșeala clasicăUn simplu if (Serial.available()) urmat de o singură citire prelucrează doar un caracter per iterație a lui loop(). Dacă bucla conține un delay(), bufferul de 64 de octeți se umple și datele se pierd. Folosiți while, nu if.

12Cod sursă

Interpretor de comenzi

serial_comenzi.ino
const uint8_t PIN_LED = 13;
String comanda = "";

void setup() {
  Serial.begin(9600);
  pinMode(PIN_LED, OUTPUT);
  Serial.println("Comenzi: ON, OFF, STARE, PWM <0-255>");
}

void loop() {
  while (Serial.available() > 0) {         // WHILE, nu IF!
    char c = Serial.read();

    if (c == '\n' || c == '\r') {
      comanda.trim();
      comanda.toUpperCase();

      if (comanda == "ON") {
        digitalWrite(PIN_LED, HIGH);
        Serial.println("LED aprins");
      } else if (comanda == "OFF") {
        digitalWrite(PIN_LED, LOW);
        Serial.println("LED stins");
      } else if (comanda == "STARE") {
        Serial.print("LED este ");
        Serial.println(digitalRead(PIN_LED) ? "aprins" : "stins");
      } else if (comanda.startsWith("PWM ")) {
        int val = comanda.substring(4).toInt();
        val = constrain(val, 0, 255);
        analogWrite(9, val);
        Serial.print("PWM setat la "); Serial.println(val);
      } else if (comanda.length() > 0) {
        Serial.print("Comanda necunoscuta: ");
        Serial.println(comanda);
      }
      comanda = "";
    } else {
      comanda += c;
    }
  }
}

Configurare directă a modulului USART

usart_registri.ino
#define F_CPU 16000000UL
#define BAUD  9600
#define UBRR_VAL ((F_CPU / (16UL * BAUD)) - 1)

void usartInit(uint16_t ubrr) {
  UBRR0H = (uint8_t)(ubrr >> 8);              // partea inalta a divizorului
  UBRR0L = (uint8_t)ubrr;                     // partea joasa
  UCSR0B = (1 << RXEN0) | (1 << TXEN0);       // activeaza receptia si transmisia
  UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);     // format 8N1
}

void usartTransmite(uint8_t data) {
  while (!(UCSR0A & (1 << UDRE0)));           // asteapta golirea registrului
  UDR0 = data;
}

uint8_t usartReceptioneaza(void) {
  while (!(UCSR0A & (1 << RXC0)));            // asteapta un octet complet
  return UDR0;
}

void usartScrieText(const char *s) {
  while (*s) usartTransmite(*s++);
}

void setup() {
  usartInit(UBRR_VAL);
  usartScrieText("USART initializat\r\n");
}

void loop() {
  uint8_t c = usartReceptioneaza();
  usartTransmite(c);                          // ecou inapoi
}

Recepție prin întrerupere

Așteptarea activă blochează programul. Recepția pe întrerupere eliberează complet bucla principală:

usart_intrerupere.ino
volatile char    bufer[32];
volatile uint8_t index = 0;
volatile bool    mesajComplet = false;

ISR(USART_RX_vect) {              // se executa la fiecare octet primit
  char c = UDR0;                  // citirea sterge automat fanionul RXC0

  if (c == '\n' || index >= 31) {
    bufer[index] = '\0';
    index = 0;
    mesajComplet = true;
  } else {
    bufer[index++] = c;
  }
}

void setup() {
  Serial.begin(9600);
  UCSR0B |= (1 << RXCIE0);        // activeaza intreruperea la receptie
  sei();
  Serial.println("Gata de comenzi");
}

void loop() {
  if (mesajComplet) {
    mesajComplet = false;
    Serial.print("Am primit: ");
    Serial.println((char*)bufer);
  }
  // bucla ramane complet libera pentru alte sarcini
}

Transmiterea datelor între două plăci

emitator.ino
const uint8_t PIN_BUTON = 2;

void setup() {
  Serial.begin(9600);
  pinMode(PIN_BUTON, INPUT_PULLUP);
}

void loop() {
  static bool ultimaStare = HIGH;
  bool stare = digitalRead(PIN_BUTON);

  if (stare != ultimaStare) {
    ultimaStare = stare;
    Serial.print("B:");             // prefix pentru identificarea mesajului
    Serial.println(stare == LOW ? 1 : 0);
    delay(50);                      // debounce simplu
  }
}
receptor.ino
const uint8_t PIN_LED = 13;
String linie = "";

void setup() {
  Serial.begin(9600);
  pinMode(PIN_LED, OUTPUT);
}

void loop() {
  while (Serial.available()) {
    char c = Serial.read();

    if (c == '\n') {
      linie.trim();
      if (linie.startsWith("B:")) {
        int val = linie.substring(2).toInt();
        digitalWrite(PIN_LED, val);
      }
      linie = "";
    } else {
      linie += c;
    }
  }
}

13Montaj interactiv

Monitorul serial din montajele de mai jos este bidirecțional: microcontrolerul scrie în el, iar dumneavoastră puteți scrie înapoi, în câmpul de sub monitor. Este exact fluxul pe care îl veți avea în laborator, prin cablul USB.

USART pe regiștri - transmisie și recepție cu ecou
#include <avr/io.h>

#define BAUD      9600
#define UBRR_VAL  ((F_CPU / 16 / BAUD) - 1)

void usartInit(void)
{
    UBRR0H = (unsigned char)(UBRR_VAL >> 8);
    UBRR0L = (unsigned char)UBRR_VAL;
    UCSR0B = (1 << TXEN0) | (1 << RXEN0);
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);   // 8 biti de date
}

void usartTransmite(unsigned char d)
{
    while (!(UCSR0A & (1 << UDRE0))) ;        // asteapta buffer-ul liber
    UDR0 = d;
}

void usartScrie(const char *s)
{
    while (*s) usartTransmite(*s++);
}

unsigned char usartCiteste(void)
{
    while (!(UCSR0A & (1 << RXC0))) ;         // asteapta un caracter
    return UDR0;
}

int main(void)
{
    DDRB |= (1 << PB5);
    usartInit();
    usartScrie("Gata. Trimiteti 1 sau 0.\r\n");

    while (1) {
        unsigned char c = usartCiteste();

        usartScrie("am primit: ");
        usartTransmite(c);
        usartScrie("\r\n");

        if (c == '1') PORTB |=  (1 << PB5);
        if (c == '0') PORTB &= ~(1 << PB5);
    }
}
De încercatScrieți 1 în câmpul de sub monitor și apăsați Enter: LED-ul se aprinde. Priviți bitul RXC0 din UCSR0A - devine 1 în clipa în care sosește un caracter și se șterge singur când programul citește UDR0. Toată comunicația serială se reduce la aceste două semnale de stare.
Exercițiu - interpretor de comenzi
String comanda = "";

void setup() {
  pinMode(13, OUTPUT);
  Serial.begin(9600);
  Serial.println("Comenzi: LED ON, LED OFF, CITESTE");
}

void loop() {
  if (Serial.available()) {
    comanda = Serial.readStringUntil('\n');
    comanda.trim();
    comanda.toUpperCase();

    if (comanda == "LED ON") {
      digitalWrite(13, HIGH);
      Serial.println("-> LED aprins");
    }

    /* Completati:
       - comanda "LED OFF" stinge LED-ul si confirma
       - comanda "CITESTE" afiseaza valoarea de pe A0
       - orice altceva raspunde: "comanda necunoscuta: ..." */
  }
}
De încercatScrieți comenzile în câmpul de sub monitor. readStringUntil('\n') citește până la Enter, iar trim() scoate spațiile și caracterul de capăt de linie - fără el, comparația cu "LED ON" eșuează fără motiv aparent.

14Sarcini de lucru

  • Realizați un program care răspunde la comenzile ON, OFF și STARE primite pe serial.
  • Adăugați comanda PWM <valoare> care reglează intensitatea unui LED.
  • Calculați UBRR pentru 4800, 9600 și 115200 baud și verificați erorile rezultate.
  • Setați în program 9600 baud și în monitorul serial 19200 - observați ce caractere apar și explicați de ce.
  • Conectați două plăci prin TX–RX și transmiteți starea unui buton de la una la cealaltă.
  • Măsurați durata transmiterii a 100 de caractere și comparați cu valoarea teoretică.
  • Implementați recepția prin întrerupere și verificați că bucla principală rămâne responsivă chiar cu delay(500).
  • Înlocuiți while cu if în bucla de citire și observați pierderea caracterelor.

15Aplicație de aprofundare

Extindere - protocol propriu cu sumă de controlDefiniți cadre de forma <ADRESA:COMANDA:VALOARE:CRC>, de exemplu <01:PWM:180:A3>. Implementați: un analizor care extrage cele patru câmpuri, verificarea adresei (placa răspunde doar la adresa proprie), calculul unei sume de control simple (XOR pe toate caracterele) și respingerea cadrelor corupte.

Testați robustețea introducând intenționat erori în cadru și verificați că acestea sunt detectate. Extindere suplimentară: legați trei plăci pe aceeași magistrală prin module MAX485 și verificați adresarea selectivă.

16Întrebări de verificare

17Resurse