Implémentez Serial.print() vous-même : registres USART, calcul du baud rate, envoi/réception et redirection de printf
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 :
| 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 | Baud Rate Register (16 bits) — définit la vitesse de communication |
La valeur à écrire dans UBRR0 dépend de la fréquence du cristal et du baud rate désiré :
| Baud rate | UBRR | Erreur |
|---|---|---|
| 9600 | 103 | +0.16% |
| 19200 | 51 | +0.16% |
| 38400 | 25 | +0.16% |
| 57600 | 16 | +2.1% |
| 115200 | 8 | −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.
#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.
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;
}
| Arduino | Bare 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 |