📡 Communication UART — ATmega328P

Implémentez Serial.print() vous-même : registres USART, calcul du baud rate, envoi/réception et redirection de printf

Le module USART de l'ATmega328P

L'ATmega328P intègre un module USART (Universal Synchronous/Asynchronous Receiver/Transmitter) matériel complet. C'est ce module que le framework Arduino utilise en coulisses quand vous appelez Serial.begin() et Serial.print(). En programmation bare metal, nous allons configurer ce module directement via ses registres.

Les broches UART de l'ATmega328P :

📋 Les registres USART

RegistreRôle
UDR0Data Register — buffer d'envoi et de réception (même adresse, deux registres physiques)
UCSR0AStatus Register A — drapeaux RXC0 (réception complète), TXC0 (transmission complète), UDRE0 (buffer vide)
UCSR0BControl Register B — activation TX (TXEN0), RX (RXEN0), interruptions (RXCIE0, TXCIE0)
UCSR0CControl Register C — format de trame : bits de données (UCSZ), parité (UPM), bits de stop (USBS)
UBRR0H:UBRR0LBaud Rate Register (16 bits) — définit la vitesse de communication

⚙️ Calcul du baud rate (UBRR)

La valeur à écrire dans UBRR0 dépend de la fréquence du cristal et du baud rate désiré :

UBRR = (F_CPU / (16 × BAUD)) − 1

Valeurs pré-calculées pour F_CPU = 16 MHz

Baud rateUBRRErreur
9600103+0.16%
1920051+0.16%
3840025+0.16%
5760016+2.1%
1152008−3.5%

Une erreur inférieure à ±2% est acceptable. Au-delà, la communication peut être instable. Pour 115200 bauds, activez le mode U2X (double vitesse) pour réduire l'erreur.

💻 Code complet : bibliothèque UART minimaliste

#include <avr/io.h>
#include <util/delay.h>

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

/* ---- Initialisation UART ---- */
void uart_init(void) {
    // Baud rate
    UBRR0H = (uint8_t)(UBRR_VAL >> 8);
    UBRR0L = (uint8_t)UBRR_VAL;

    // Activer TX et RX
    UCSR0B = (1 << TXEN0) | (1 << RXEN0);

    // Format : 8 bits de données, pas de parité, 1 stop bit (8N1)
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
}

/* ---- Envoyer un octet ---- */
void uart_putc(char c) {
    while (!(UCSR0A & (1 << UDRE0)));  // Attendre buffer vide
    UDR0 = c;                            // Écrire l'octet
}

/* ---- Envoyer une chaîne ---- */
void uart_puts(const char *s) {
    while (*s) {
        uart_putc(*s++);
    }
}

/* ---- Recevoir un octet (bloquant) ---- */
char uart_getc(void) {
    while (!(UCSR0A & (1 << RXC0)));   // Attendre réception
    return UDR0;                          // Lire l'octet
}

/* ---- Envoyer un entier en décimal ---- */
void uart_print_int(int16_t val) {
    char buf[7];
    itoa(val, buf, 10);
    uart_puts(buf);
}

/* ---- Programme principal ---- */
int main(void) {
    uart_init();
    uart_puts("ATmega328P UART pret !\r\n");

    uint16_t compteur = 0;

    while (1) {
        uart_puts("Compteur : ");
        uart_print_int(compteur++);
        uart_puts("\r\n");
        _delay_ms(1000);
    }
}

Ouvrez un terminal série (PuTTY, minicom, ou le moniteur série Arduino IDE) à 9600 bauds pour voir les messages. C'est l'exact équivalent de Serial.print() mais en ~200 octets de Flash au lieu de ~2 Ko pour le framework Arduino.

📥 Réception avec interruption

Pour une réception non bloquante, utilisez l'interruption RX :

#include <avr/interrupt.h>

volatile char rx_buffer[64];
volatile uint8_t rx_index = 0;
volatile uint8_t rx_complete = 0;

ISR(USART_RX_vect) {
    char c = UDR0;
    if (c == '\n' || rx_index >= 63) {
        rx_buffer[rx_index] = '\0';
        rx_complete = 1;
        rx_index = 0;
    } else {
        rx_buffer[rx_index++] = c;
    }
}

// Dans uart_init(), ajouter :
UCSR0B |= (1 << RXCIE0);  // Activer interruption RX
sei();                        // Interruptions globales

// Dans main loop :
if (rx_complete) {
    uart_puts("Recu : ");
    uart_puts((char *)rx_buffer);
    uart_puts("\r\n");
    rx_complete = 0;
}

📌 Comparaison Arduino vs bare metal

ArduinoBare metal AVR
Serial.begin(9600)uart_init() — configure UBRR, UCSR0B, UCSR0C
Serial.print("texte")uart_puts("texte")
Serial.println(42)uart_print_int(42); uart_puts("\r\n")
Serial.read()uart_getc()
~1800 octets Flash~200 octets Flash