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.
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 :
- TXD = PD1 (broche D1 sur Arduino) — Transmission
- RXD = PD0 (broche D0 sur Arduino) — Réception
Câbler TX, RX et la masse commune
| 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.
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é :
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
- Caractères illisibles : vérifiez l’horloge réelle, F_CPU, U2X0, UBRR0 et le débit du terminal.
- Aucun caractère : contrôlez le port série, TX vers RX, RX vers TX et la masse commune.
- Réception qui attend : uart_getc() est bloquante ; sans donnée, elle ne rend pas la main.
- Réception par interruption inactive : RXEN0, RXCIE0 et les interruptions globales doivent être activés.
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 .