← Retour← Back
CV

Microcontrôleur 32 bits STM32 : GPIO, UART, interruptions32-bit STM32 microcontroller: GPIO, UART, interrupts

Systèmes embarquésEmbedded systemsSTM32F446RECortex-M4FCNUMU3
QuoiWhat
Une carte Nucleo-64 STM32F446RE (Cortex-M4F) : GPIO et timers, UART avec FIFO, puis interruptions. Tout en C, directement sur les registres.A Nucleo-64 STM32F446RE board (Cortex-M4F): GPIO and timers, UART with FIFOs, then interrupts. All in C, directly on the registers.
PourquoiWhy
Comprendre ce qu'un microcontrôleur fait sous les bibliothèques : accès aux registres, communication série, et organisation du code par interruptions.To understand what a microcontroller does under the libraries: register access, serial communication, and organizing code around interrupts.
CommentHow
STM32CubeIDE, registres manipulés par masques puis par structures (GPIOA->BSRR…), anti-rebond non bloquant, FIFO circulaires, ISR de l'USART2 et du TIM1.STM32CubeIDE, registers handled with masks then with structures (GPIOA->BSRR…), non-blocking debounce, circular FIFOs, USART2 and TIM1 ISRs.
RésultatsResults
Chiffres clés : UART à 115 200 bit/s (≈ 87 µs par caractère), TIM7 à 2 kHz (ARR = 1999 pour une demi-période d'1 s), TIM1 à 20 kHz (50 µs) pour une PWM de LED.Key figures: UART at 115,200 bit/s (≈ 87 µs per character), TIM7 at 2 kHz (ARR = 1999 for a 1 s half-period), TIM1 at 20 kHz (50 µs) for an LED PWM.

Synthèse des TPs de microcontrôleur 32 bits (module NUMU3) sur une carte Nucleo-64 avec un STM32F446RE : un Cortex-M4 avec unité de calcul flottant, programmé en C directement sur les registres, sans passer par les fonctions haut niveau du constructeur. La carte n'a qu'une LED verte (PA5) et un bouton bleu (PC13), alors pour tout le reste on passe par la liaison série vers le PC.

GPIO et timers

  • Les registres, bit par bit : on ne peut accéder qu'à 32 bits à la fois, donc modifier un bit veut dire lire, masquer (| pour mettre à 1, & ~ pour mettre à 0), puis réécrire. On teste un bit avec if (tmp & (1 << n)), jamais avec == 1.
  • ODR, IDR, BSRR : ODR et IDR donnent l'état des sorties et des broches. BSRR met à 1 ou à 0 certaines broches sans toucher aux autres, en une seule écriture (avantage : atomique et lisible ; inconvénient : registre en écriture seule, donc on ne le relit pas).
  • Du registre à la structure : GPIOA est un pointeur vers un GPIO_TypeDef dont les champs (MODER, IDR, ODR, BSRR…) sont superposés aux adresses des registres. On écrit GPIOA->BSRR au lieu de définir un nom par registre.
  • Anti-rebond non bloquant : un appui change l'état de la LED une seule fois, tout de suite, sans réaction au relâchement. Pour cela, on ignore les changements pendant 10 à 20 ms après le premier front, sans bloquer le programme.
  • Le timer 7 comme base de temps : configuré à 2 kHz, son compteur va de 0 à ARR. Avec ARR = 3999 la période fait 2 s, donc pour une demi-période d'une seconde il faut ARR = 1999. Au lieu de compter des tours de boucle, on attend le bit de dépassement du registre SR.

Communication UART

8 bits de données (D0 à D7, poids faible en premier)8 data bits (D0 to D7, least significant first)repos (1)idle (1)start (0)stop (1)1 bit = 1 / 115 200 ≈ 8,68 µs · trame de 10 bits ≈ 87 µs par caractère1 bit = 1 / 115,200 ≈ 8.68 µs · 10-bit frame ≈ 87 µs per character
Une trame UART à 115 200 bit/s (8 bits, sans parité) : la durée d'un caractère fixe l'attente minimale entre deux envois.A UART frame at 115,200 bit/s (8 bits, no parity): the duration of one character sets the minimum wait between two sends.
  • Envoi : on écrit un caractère dans DR et la machine d'états de l'USART2 le sort. Un seul caractère à la fois : il faut attendre au moins la durée d'une trame (≈ 87 µs à 115 200 bit/s) entre deux envois, ou mieux, tester le bit TXE.
  • Réception : le bit RXNE de SR indique qu'un caractère est arrivé, et on le lit dans DR. Le programme renvoie les lettres reçues, puis comprend les commandes ON, OFF, BLINK (1 Hz) et MSG.
  • FIFO circulaires : une pour l'émission, une pour la réception, avec Wait_UART(), write() et read(). C'est la version primitive d'un pilote de périphérique.
01234567ir : plus ancienir: oldestiw : prochaine écritureiw: next writeTableau circulaire de taille N = 8 (vert = occupé)Circular array of size N = 8 (green = occupied)vide : ir = iw · pleine : (iw + 1) mod N = irempty: ir = iw · full: (iw + 1) mod N = ir
La FIFO circulaire : deux indices modulo N suffisent à gérer l'attente d'émission et la file de réception.The circular FIFO: two indices modulo N are enough to handle the transmit queue and the receive queue.

Interruptions

Une interruption ne reçoit rien, ne renvoie rien et n'attend jamais. Elle ne communique avec main que par des variables globales ou par les registres du périphérique.

PC(terminal)USART2_IRQHandlerpriorité 1FIFO RXFIFO TXmainget_char, get_str,send_stringmainget_char, get_str,send_stringTIM1 ISR50 µsTIM1 ISR50 µsRXTXRXNETXELED_freq_HzLED_power
Architecture avec interruptions : elles alimentent les FIFO, le programme principal les consulte, et le timer génère la PWM de la LED à partir de variables globales.Architecture with interrupts: they feed the FIFOs, the main program reads them, and the timer generates the LED PWM from global variables.
  • Réception en interruption : le bit RXNEIE de CR1 autorise USART2_IRQHandler à se déclencher sur RXNE. La fonction lit le caractère et le range dans la FIFO de réception, que get_char() et get_str() consultent.
  • Émission en interruption : on autorise aussi TXE, mais TXE vaut 1 quand il n'y a rien à envoyer, ce qui déclencherait l'interruption en boucle. Il faut donc mettre TXEIE à 1 quand on remplit la FIFO d'émission (send_string()) et le remettre à 0 quand elle est vide.
  • PWM de la LED : le TIM1 à 20 kHz (une interruption toutes les 50 µs) compte ses périodes pour faire clignoter la LED selon LED_freq_Hz et LED_power (le rapport cyclique).

Les pièges

  • volatile : sans lui, le compilateur peut supprimer une boucle d'attente, donc le compteur doit être déclaré volatile.
  • Interruption sans effacement du drapeau : si l'ISR d'un timer n'efface pas son bit de statut, elle se redéclenche aussitôt et monopolise le processeur, ce qui coupe la LED, le bouton et l'UART. Une priorité plus faible (numériquement plus grande, de 0 à 15) laisse passer les autres interruptions, mais ne remplace pas l'effacement du drapeau.
  • Une attente dans une ISR : jamais. Pendant l'attente, tout ce qui est moins prioritaire est bloqué, donc aucune fonction bloquante depuis une interruption.
  • Du flottant dans une ISR : sur un Cortex-M4F l'unité de calcul flottant est matérielle, donc ça coûte moins cher qu'on ne le pense.

Synthesis of the labs of the 32-bit microcontroller module (NUMU3) on a Nucleo-64 board with an STM32F446RE: a Cortex-M4 with a floating-point unit, programmed in C directly on the registers, without the manufacturer's high-level functions. The board has just one green LED (PA5) and one blue button (PC13), so for everything else you go through the serial link to the PC.

GPIO and timers

  • Registers, bit by bit: only 32-bit accesses are possible, so changing one bit means reading, masking (| to set, & ~ to clear), then writing back. A bit is tested with if (tmp & (1 << n)), never with == 1.
  • ODR, IDR, BSRR: ODR and IDR give the state of the outputs and the pins. BSRR sets or clears some pins without touching the others, in a single write (advantage: atomic and readable; drawback: write-only register, so it can't be read back).
  • From register to structure: GPIOA is a pointer to a GPIO_TypeDef whose fields (MODER, IDR, ODR, BSRR…) overlay the register addresses. You write GPIOA->BSRR instead of defining one name per register.
  • Non-blocking debounce: a press toggles the LED exactly once, right away, with no reaction on release. To do that, changes are ignored for 10 to 20 ms after the first edge, without blocking the program.
  • Timer 7 as a time base: set at 2 kHz, its counter runs from 0 to ARR. With ARR = 3999 the period is 2 s, so for a one-second half-period you need ARR = 1999. Instead of counting loop turns, you wait on the overflow bit of the SR register.

UART communication

8 bits de données (D0 à D7, poids faible en premier)8 data bits (D0 to D7, least significant first)repos (1)idle (1)start (0)stop (1)1 bit = 1 / 115 200 ≈ 8,68 µs · trame de 10 bits ≈ 87 µs par caractère1 bit = 1 / 115,200 ≈ 8.68 µs · 10-bit frame ≈ 87 µs per character
Une trame UART à 115 200 bit/s (8 bits, sans parité) : la durée d'un caractère fixe l'attente minimale entre deux envois.A UART frame at 115,200 bit/s (8 bits, no parity): the duration of one character sets the minimum wait between two sends.
  • Sending: you write one character to DR and the USART2 state machine sends it out. One character at a time: you need to wait at least one frame duration (≈ 87 µs at 115,200 bit/s) between two sends, or better, test the TXE bit.
  • Receiving: the RXNE bit of SR says a character has arrived, and you read it from DR. The program echoes the received letters, then understands the commands ON, OFF, BLINK (1 Hz) and MSG.
  • Circular FIFOs: one for transmit, one for receive, with Wait_UART(), write() and read(). It is the primitive version of a device driver.
01234567ir : plus ancienir: oldestiw : prochaine écritureiw: next writeTableau circulaire de taille N = 8 (vert = occupé)Circular array of size N = 8 (green = occupied)vide : ir = iw · pleine : (iw + 1) mod N = irempty: ir = iw · full: (iw + 1) mod N = ir
La FIFO circulaire : deux indices modulo N suffisent à gérer l'attente d'émission et la file de réception.The circular FIFO: two indices modulo N are enough to handle the transmit queue and the receive queue.

Interrupts

An interrupt receives nothing, returns nothing and never waits. It only talks to main through global variables or the peripheral's registers.

PC(terminal)USART2_IRQHandlerpriorité 1FIFO RXFIFO TXmainget_char, get_str,send_stringmainget_char, get_str,send_stringTIM1 ISR50 µsTIM1 ISR50 µsRXTXRXNETXELED_freq_HzLED_power
Architecture avec interruptions : elles alimentent les FIFO, le programme principal les consulte, et le timer génère la PWM de la LED à partir de variables globales.Architecture with interrupts: they feed the FIFOs, the main program reads them, and the timer generates the LED PWM from global variables.
  • Receiving by interrupt: the RXNEIE bit of CR1 lets USART2_IRQHandler fire on RXNE. The function reads the character and puts it in the receive FIFO, which get_char() and get_str() read from.
  • Sending by interrupt: TXE is enabled too, but TXE is 1 when there is nothing to send, which would fire the interrupt in a loop. So TXEIE must be set to 1 when the transmit FIFO gets filled (send_string()) and cleared when it is empty.
  • LED PWM: TIM1 at 20 kHz (an interrupt every 50 µs) counts its periods to blink the LED according to LED_freq_Hz and LED_power (the duty cycle).

The pitfalls

  • volatile: without it the compiler can remove a wait loop, so the counter has to be declared volatile.
  • Interrupt that doesn't clear its flag: if a timer ISR doesn't clear its status bit, it fires again at once and hogs the processor, which kills the LED, the button and the UART. A lower priority (numerically larger, from 0 to 15) lets the other interrupts through, but doesn't replace clearing the flag.
  • A wait inside an ISR: never. While it waits, everything with lower priority is blocked, so no blocking function gets called from an interrupt.
  • Floating point in an ISR: on a Cortex-M4F the floating-point unit is in hardware, so it costs less than you'd think.