UART ATmega328P : communication série en C

Configurer l’USART sans la bibliothèque Arduino : TX/RX, registres, 9 600 bauds, réception et printf.

Par JLQDeveloppement · Mis à jour le

Apprendre la communication série sur ATmega328P

Dans ce tutoriel, vous allez envoyer du texte vers un ordinateur avec l’UART de l’ATmega328P, directement en langage C avec AVR-GCC et avr-libc. Vous découvrirez quels registres remplacent les appels Serial.begin() et Serial.print() d’Arduino.

Le premier exemple utilise une horloge réelle de 16 MHz et une liaison 9 600 bauds, 8 bits, sans parité, un bit de stop (8N1) . Définir F_CPU ne configure pas l’oscillateur ni les fusibles du microcontrôleur.

Pour commencer, consultez l’ introduction à l’ATmega328P et le guide du langage C embarqué .

Comprendre l’UART et l’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 :

Câbler TX, RX et la masse commune

Connexion à un adaptateur USB vers UART TTL compatible
ATmega328P Adaptateur USB-UART
PD1 / TXD (D1 sur Arduino Uno) RX
PD0 / RXD (D0 sur Arduino Uno) TX
GND GND

Vérifiez la compatibilité des niveaux logiques de l’adaptateur avec l’alimentation du montage. Utilisez une interface UART TTL adaptée ; une sortie RS-232 nécessite une adaptation de niveaux.

Microcontrôleur ATmega328P utilisé pour les exemples de communication UART en C
Le module USART est intégré à l’ATmega328P ; ses broches série sont RXD et TXD.

Les registres USART : UDR0, UCSR0A/B/C et UBRR0

Registre Rôle
UDR0 Data Register — buffer d’envoi et de réception (même adresse, deux registres physiques)
UCSR0A Status Register A — drapeaux RXC0 (réception complète), TXC0 (transmission complète), UDRE0 (buffer vide)
UCSR0B Control Register B — activation TX (TXEN0), RX (RXEN0), interruptions (RXCIE0, TXCIE0)
UCSR0C Control Register C — format de trame : bits de données (UCSZ), parité (UPM), bits de stop (USBS)
UBRR0H:UBRR0L Deux octets de registre, dont 12 bits UBRR0 implémentés : règlent le débit série.

Calculer le baud rate et configurer UBRR0

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 à 16 MHz en mode asynchrone normal (U2X0 = 0)

Baud rate UBRR Erreur
9600 103 +0.16%
19200 51 +0.16%
38400 25 +0.16%
57600 16 +2,12 %
115200 8 −3,55 %

La tolérance dépend du format de trame et des horloges des deux appareils : ±2 % n’est pas une garantie universelle. À 115 200 bauds, le mode double vitesse U2X0=1 avec UBRR0=16 donne environ 117 647 bauds, soit +2,12 % d’erreur. Vérifiez que cette erreur convient aux deux appareils.

En double vitesse, la formule devient BAUD = F_CPU / (8 × (UBRR0 + 1)). À 9 600 bauds et 16 MHz, le mode normal suffit pour cet exemple.

Exemple complet UART en C à 9 600 bauds

#define F_CPU 16000000UL  // Avant util/delay.h

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

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

void uart_init(void) {
    UCSR0A = 0;  // U2X0=0 : vitesse normale

    UBRR0H = (uint8_t)(UBRR_VAL >> 8);
    UBRR0L = (uint8_t)UBRR_VAL;

    UCSR0B = (1 << TXEN0) | (1 << RXEN0);
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00); // 8N1
}

void uart_putc(char c) {
    while (!(UCSR0A & (1 << UDRE0))) {
    }

    UDR0 = (uint8_t)c;
}

void uart_puts(const char *s) {
    while (*s) {
        uart_putc(*s++);
    }
}

uint8_t uart_getc(void) {
    while (!(UCSR0A & (1 << RXC0))) {
    }

    return UDR0;
}

void uart_print_uint(uint16_t valeur) {
    char texte[6];  // 65535 + terminaison nulle

    utoa(valeur, texte, 10);
    uart_puts(texte);
}

int main(void) {
    uint16_t compteur = 0;

    uart_init();
    uart_puts("ATmega328P UART pret !\r\n");

    while (1) {
        uart_puts("Compteur : ");
        uart_print_uint(compteur++);
        uart_puts("\r\n");

        _delay_ms(1000);
    }
}

Compilez cet exemple avec AVR-GCC pour l’ATmega328P. Ouvrez ensuite un terminal série à 9 600 bauds, 8N1, sans contrôle de flux : un compteur doit être transmis environ chaque seconde. uart_getc() est bloquante et n’est pas appelée dans cette première boucle. La taille Flash dépend du programme et des options de compilation ; aucun gain chiffré n’est garanti ici.

Recevoir des données UART par interruption

L’interruption USART_RX_vect reçoit un octet sans obliger la boucle principale à attendre dans uart_getc(). Cet exemple remplace le main précédent : conservez les fonctions UART et ajoutez les déclarations ci-dessous.

// Complément à la bibliothèque précédente : réception d'un octet.

#include <avr/interrupt.h>
#include <util/atomic.h>

static volatile uint8_t rx_octet;
static volatile uint8_t rx_disponible = 0;

ISR(USART_RX_vect) {
    uint8_t statut = UCSR0A; // Lire les erreurs avant UDR0
    uint8_t octet = UDR0;

    if (statut & ((1 << FE0) | (1 << DOR0) | (1 << UPE0))) {
        return;
    }

    if (!rx_disponible) {
        rx_octet = octet;
        rx_disponible = 1;
    }
}

// Remplace le main précédent pour tester la réception.
int main(void) {
    uart_init();

    UCSR0B |= (1 << RXCIE0);
    sei();

    while (1) {
        uint8_t octet = 0;
        uint8_t pret = 0;

        ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
            if (rx_disponible) {
                octet = rx_octet;
                rx_disponible = 0;
                pret = 1;
            }
        }

        if (pret) {
            uart_putc((char)octet); // Echo vers le terminal
        }
    }
}

Cette boîte aux lettres garde un seul octet : un nouvel octet est abandonné si le précédent attend encore d’être traité. Le bloc atomique protège sa récupération. Pour un flux continu, utilisez un tampon circulaire avec gestion explicite des débordements. Cet exemple et la réception bloquante ne doivent pas lire simultanément UDR0.

Arduino Serial et programmation AVR en C

Arduino Bare metal AVR
Serial.begin(9600) uart_init() — configure UBRR, UCSR0B, UCSR0C
Serial.print("texte") uart_puts("texte")
Serial.println(42) uart_print_uint(42); uart_puts("\r\n");
Serial.read() uart_getc() attend un octet ; Serial.read() renvoie -1 si aucun octet n’est disponible.
Abstraction et tampon gérés par la bibliothèque Configuration et stratégie de réception explicites

Rediriger printf vers l’UART avec avr-libc

Pour utiliser printf(), reliez la sortie standard stdout à une fonction qui transmet chaque caractère. Ce fragment complète le premier exemple : ajoutez les déclarations au-dessus de main(), puis les deux lignes indiquées dans votre programme.

#include <stdio.h>

static int uart_putchar(char c, FILE *flux) {
    (void)flux;

    if (c == '\n') {
        uart_putc('\r');
    }

    uart_putc(c);

    return 0;
}

static FILE uart_stdout =
    FDEV_SETUP_STREAM(uart_putchar, NULL, _FDEV_SETUP_WRITE);

// Dans main(), après uart_init() :
// stdout = &uart_stdout;
// printf("Compteur : %u\n", (unsigned int)compteur);

Le caractère de fin de ligne est converti en CR+LF pour le terminal. Le formatage avec printf peut augmenter la taille du programme ; les nombres flottants demandent des options de bibliothèque spécifiques.

Dépannage : caractères illisibles ou aucune réception

Quelle différence entre UART et USART ?

L’USART peut fonctionner en synchrone ou en asynchrone. Ce tutoriel utilise le mode asynchrone, couramment appelé UART.

F_CPU suffit-il pour passer le microcontrôleur à 16 MHz ?

Non. F_CPU décrit la fréquence au compilateur. L’oscillateur, les fusibles et un éventuel préscaler doivent correspondre à cette valeur.

Ce code utilise-t-il la bibliothèque Arduino ?

Non. Il utilise les registres de l’ATmega328P et avr-libc. Sur une carte Arduino Uno, le chargement de ce programme remplace le programme qui était présent.

Pour aller plus loin

Continuez avec les interruptions AVR, les timers de l’ATmega328P ou le convertisseur ADC. Pour distinguer les plateformes, consultez le comparatif PIC, Arduino et ATmega .

Documentation technique