Implémentation d'un anneau circulaire pour la réception UART — avec gestion par interruption, protection contre le débordement, et sans allocation dynamique.
En UART bare-metal, les octets arrivent caractère par caractère à des moments imprévisibles. Sans buffer, si l'application est occupée au moment où un octet arrive, il est perdu.
Le ring buffer (ou circular buffer) est la solution classique : il découple la réception des données (dans l'interruption) de leur traitement (dans la boucle principale).
Producer (interruption UART) → écrit dans le buffer. Consumer (application) → lit depuis le buffer. Les deux opèrent indépendamment, sans se bloquer mutuellement.
Le buffer est un tableau de taille fixe avec deux indices : head (où le prochain octet sera écrit) et tail (où le prochain octet sera lu). Quand un index atteint la fin du tableau, il repart à zéro — d'où le "circulaire".
Le mot-clé volatile est indispensable — il indique au compilateur que ces variables peuvent changer hors du flux normal (dans une ISR).
#define UART_BUF_SIZE 64U // doit être une puissance de 2 #define UART_BUF_MASK (UART_BUF_SIZE - 1U) typedef struct { uint8_t buffer[UART_BUF_SIZE]; volatile uint16_t head; // écrit par l'ISR volatile uint16_t tail; // lu par l'application } RingBuffer_t;
// Écriture (appelée depuis l'ISR UART) static bool ring_push(RingBuffer_t *rb, uint8_t data) { uint16_t next_head = (rb->head + 1U) & UART_BUF_MASK; if (next_head == rb->tail) { return false; // buffer plein — octet perdu } rb->buffer[rb->head] = data; rb->head = next_head; // mise à jour atomique en dernier return true; } // Lecture (appelée depuis la boucle principale) static bool ring_pop(RingBuffer_t *rb, uint8_t *out) { if (rb->tail == rb->head) { return false; // buffer vide } *out = rb->buffer[rb->tail]; rb->tail = (rb->tail + 1U) & UART_BUF_MASK; return true; } // ISR UART — appelée automatiquement à chaque octet reçu void USART2_IRQHandler(void) { if (USART2->SR & USART_SR_RXNE) { uint8_t byte = (uint8_t)(USART2->DR & 0xFF); ring_push(&uart_rx_buf, byte); // stocker l'octet } } // Dans la boucle principale void UART_ProcessRx(void) { uint8_t c; while (ring_pop(&uart_rx_buf, &c)) { App_HandleChar(c); // traiter l'octet } }
La mise à jour de head doit se faire après l'écriture des données. Si le compilateur réordonne les instructions, utiliser une barrière mémoire (__DMB()) ou déclarer le champ volatile.
Plutôt qu'un if (index >= SIZE) index = 0;, on utilise un ET binaire : index = (index + 1) & MASK;. C'est plus rapide (pas de branchement) et c'est exact si SIZE est une puissance de 2.
volatile — sinon le compilateur peut cacher sa valeur dans un registre et ne pas voir les mises à jour.static) — pas d'allocation dynamique, pas de heap fragmentation.Ce même pattern ring buffer s'applique pour la transmission UART, les événements entre tâches FreeRTOS, et les messages de logging — c'est une structure fondamentale en firmware.