TP 1 — Allumer une LED, lire un bouton
Objectifs
créer un projet STM32CubeIDE pour la carte, avec la configuration minimale ;
comprendre le schéma d’une LED et le calcul de sa résistance ;
piloter une sortie : clignotement, puis chenillard sur quatre LED ;
lire une entrée, constater le rebond d’un bouton et l’éliminer.
Durée indicative : une séance de 3 h. Matériel : voir Prise en main.
Ce qu’il faut savoir
Les LED de façade
La carte porte quatre LED bleues en façade, chacune pilotée directement par une broche du microcontrôleur, à travers une résistance de 220 Ω :
LED |
Broche |
Repère |
Résistance |
|---|---|---|---|
LED_1 |
PD4 |
D9 |
R28, 220 Ω |
LED_2 |
PD3 |
D10 |
R29, 220 Ω |
LED_3 |
PD2 |
D11 |
R30, 220 Ω |
LED_4 |
PC7 |
D12 |
R31, 220 Ω |
Elles sont actives à l’état haut : la broche à 1 (3,3 V) fait passer le courant dans la LED, qui s’allume ; la broche à 0 l’éteint.

Une LED de façade : la broche, à l’état haut, alimente la LED à travers 220 Ω.
Le courant. Une LED bleue a une tension de seuil d’environ 3 V. Il reste 3,3 − 3,0 = 0,3 V aux bornes de la résistance, soit un courant d’environ 0,3 / 220 ≈ 1,4 mA. C’est peu, mais une LED moderne est bien visible à ce courant ; et une broche du STM32H750 ne doit de toute façon pas fournir plus de quelques milliampères.
Une broche en sortie
Une broche configurée en sortie (GPIO_Output, mode push-pull) impose sa tension : 3,3 V à l’état haut, 0 V à l’état bas. La bibliothèque HAL fournit trois fonctions :
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_4, GPIO_PIN_SET); // 1 : allumée
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_4, GPIO_PIN_RESET); // 0 : éteinte
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_4); // inverse l'état
HAL_Delay(ms) attend le nombre de millisecondes demandé, en comptant les
interruptions d’un minuteur du cœur (SysTick), réglé par CubeMX.
Les boutons
Les boutons SW1 à SW4 de la carte sont aussi actifs à l’état haut : au repos, une résistance de 10 kΩ tire la broche à la masse (0) ; un appui la relie au 3,3 V (1). La résistance est sur la carte : la broche se configure sans tirage interne (No pull-up and no pull-down).
Bouton |
Broche |
Au repos / appuyé |
|---|---|---|
SW1 |
PE10 |
0 / 1 |
SW2 |
PA9 |
0 / 1 |
SW3 |
PE8 |
0 / 1 |
SW4 |
PE7 |
0 / 1 |
Étape 1 — Créer le projet
Dans STM32CubeIDE : File → New → STM32 Project.
Dans l’onglet MCU/MPU Selector, cherchez
STM32H750VBT6(ouSTM32H750VBTx), sélectionnez-le, puis Next.Nommez le projet
tp01_gpio, langage C, type Executable, puis Finish. Acceptez l’ouverture de la perspective CubeMX.
CubeMX affiche le boîtier du microcontrôleur, broche par broche.
Étape 2 — Configurer le minimum
Le débogage. Dans System Core → SYS, réglez Debug sur Serial Wire. Sans ce réglage, CubeMX peut réaffecter les broches de la sonde, et la carte devient difficile à reprogrammer.
La LED. Cliquez sur la broche PD4 et choisissez GPIO_Output. Puis clic droit sur la broche, Enter User Label :
LED_1.L’horloge. Ne changez rien : le microcontrôleur tourne sur son oscillateur interne HSI, à 64 MHz (onglet Clock Configuration, pour le constater).
Enregistrez (Ctrl-S) : CubeMX propose de générer le code ; acceptez.
Le code généré initialise l’horloge et la broche PD4. Ouvrez
Core/Src/main.c : la fonction main() appelle HAL_Init(),
SystemClock_Config() et MX_GPIO_Init(), puis entre dans une boucle
while (1).
Important
N’écrivez que entre les commentaires /* USER CODE BEGIN … */ et
/* USER CODE END … */. À chaque nouvelle génération, CubeMX réécrit
tout le reste du fichier, et le code placé ailleurs disparaît.
Étape 3 — Faire clignoter LED_1
Dans la boucle while (1), dans le bloc USER CODE BEGIN 3, faites
clignoter LED_1 deux fois par seconde. Le label LED_1 donné dans CubeMX a
créé deux constantes dans main.h : LED_1_GPIO_Port et LED_1_Pin,
à utiliser plutôt que GPIOD et GPIO_PIN_4.
Compilez (Project → Build Project), puis programmez la carte et lancez le programme : Run → Run As → STM32 C/C++ Application, configuration par défaut.
Note
Si STM32CubeIDE n’arrive pas à se connecter à la carte, ouvrez Run → Run Configurations… → Debugger et choisissez Connect under reset : la sonde prend alors la main au moment du redémarrage, avant que le programme en place ne démarre.
Questions
Quelle est la période du clignotement ? Mesurez-la (montre, ou vidéo au ralenti d’un téléphone) et comparez-la au code.
Remplacez l’attente par
HAL_Delay(1). Que voit-on ? Pourquoi ?Pourquoi utiliser
LED_1_Pinplutôt queGPIO_PIN_4?
Solution de l’étape 3
Dans main.c, entre /* USER CODE BEGIN 3 */ et /* USER CODE END 3 */,
c’est-à-dire dans la boucle while (1) :
HAL_GPIO_TogglePin(LED_1_GPIO_Port, LED_1_Pin);
HAL_Delay(250);
Ce qui se passe. HAL_GPIO_TogglePin lit l’état actuel de la sortie
dans le registre ODR (Output Data Register) du port D, et écrit
l’inverse dans le registre BSRR (Bit Set/Reset Register) : un bit
« set » pour passer la broche à 1, un bit « reset » pour la passer à 0.
BSRR modifie une seule broche sans toucher aux quinze autres du
port, en une seule écriture : c’est pour cela que la HAL l’utilise.
HAL_Delay(250) attend que le compteur de millisecondes uwTick, augmenté
par l’interruption SysTick 1 000 fois par seconde, ait avancé de 250.
Pendant ce temps, le processeur tourne en rond.
Deux fois par seconde : le clignotement complet (allumée puis éteinte)
dure deux bascules, soit 2 × 250 = 500 ms. Une erreur fréquente est d’écrire
HAL_Delay(500), qui donne un cycle d’une seconde.
Réponses aux questions 1 à 3
1. La période. Elle vaut 500 ms : 250 ms allumée, 250 ms éteinte.
Mesurée au chronomètre sur 20 clignotements (10 s), on trouve 10 s à
quelques dixièmes près. La HAL ajoute en réalité une milliseconde à
chaque attente (HAL_Delay attend au moins le délai demandé, et arrondit
au tic suivant), soit 251 ms par bascule : 0,4 % d’écart, invisible ici.
2. Avec HAL_Delay(1). La LED semble allumée en permanence, un peu moins brillante. Elle clignote pourtant à 500 Hz (1 ms allumée, 1 ms éteinte), mais l’œil ne distingue pas un clignotement au-delà de 50 à 100 Hz environ : c’est la persistance rétinienne. Il perçoit la moyenne de la lumière : allumée la moitié du temps, la LED paraît moins lumineuse. C’est exactement le principe de la MLI (PWM) du TP 2, qui règle la luminosité en jouant sur cette proportion.
3. LED_1_Pin plutôt que GPIO_PIN_4. LED_1_Pin et LED_1_GPIO_Port
sont des #define écrits par CubeMX dans main.h, à partir du label.
Si la LED est un jour déplacée sur une autre broche, il suffit de changer
la broche dans CubeMX : le code suit sans modification. Et le code se
lit : LED_1 dit ce qu’on commande, GPIO_PIN_4 seulement où.
Étape 4 — Le chenillard
Dans CubeMX (double-clic sur le fichier
tp01_gpio.ioc), configurez PD3, PD2 et PC7 en GPIO_Output, labelsLED_2,LED_3etLED_4. Régénérez le code.Écrivez un chenillard : une seule LED allumée à la fois, de LED_1 à LED_4, puis on recommence. Une LED toutes les 200 ms.
Rangez les quatre LED dans un tableau, pour parcourir les LED avec un indice plutôt que d’écrire quatre fois le même code :
typedef struct { GPIO_TypeDef *port; uint16_t pin; } led_t;
static const led_t leds[4] = {
{ LED_1_GPIO_Port, LED_1_Pin },
{ LED_2_GPIO_Port, LED_2_Pin },
{ LED_3_GPIO_Port, LED_3_Pin },
{ LED_4_GPIO_Port, LED_4_Pin },
};
Questions
Dans quel ordre les LED s’allument-elles sur la façade ? L’ordre physique est-il celui des broches ?
Ajoutez au cycle une étape « les quatre allumées », puis une étape « toutes éteintes ». À quel défaut de câblage chacune des deux étapes permet-elle de penser ?
Solution de l’étape 4
Le tableau leds se place dans /* USER CODE BEGIN PV */ (variables
privées), avant main. Une fonction qui n’allume que la LED d’indice
i se place dans /* USER CODE BEGIN 0 */ :
/* N'allume que la LED d'indice i (0 à 3), éteint les trois autres. */
static void leds_show_one(uint32_t i)
{
for (uint32_t k = 0u; k < 4u; k++)
{
HAL_GPIO_WritePin(leds[k].port, leds[k].pin,
(k == i) ? GPIO_PIN_SET : GPIO_PIN_RESET);
}
}
Dans la boucle (USER CODE BEGIN 3) :
static uint32_t i = 0u; /* static : garde sa valeur d'un tour à l'autre */
leds_show_one(i);
i = (i + 1u) % 4u; /* 0, 1, 2, 3, 0, 1... */
HAL_Delay(200);
Pourquoi un tableau de structures. Les quatre LED ne sont pas sur le même port (PD4, PD3, PD2 sur le port D, mais PC7 sur le port C) : on ne peut pas les commander par un simple numéro de bit. Chaque entrée du tableau garde le couple (port, broche), et la boucle traite les quatre LED de la même façon.
Pourquoi static. Une variable locale ordinaire est recréée à chaque
passage dans le bloc, et repartirait de 0 à chaque tour : le chenillard
resterait sur LED_1. static la place en mémoire globale tout en la
gardant visible seulement dans ce bloc.
Le modulo. (i + 1) % 4 donne le reste de la division par 4 : après 3,
on revient à 0, sans if.
Réponses aux questions 4 et 5
4. L’ordre. Le programme allume PD4, PD3, PD2 puis PC7, dans l’ordre du tableau. Sur la façade, regardez si les LED s’allument de gauche à droite : l’ordre des repères D9 à D12 sur le circuit, l’ordre physique et l’ordre des broches sont trois choses différentes. Le tableau sert aussi à cela : pour changer l’ordre d’affichage, il suffit de permuter ses lignes, sans toucher au reste du programme.
5. Les étapes de test.
« Toutes allumées » révèle une LED qui ne s’allume jamais : LED montée à l’envers, soudure absente, résistance coupée, ou mauvaise broche dans CubeMX.
« Toutes éteintes » révèle une LED qui ne s’éteint jamais : broche en court-circuit avec le 3,3 V, ou erreur de label (deux labels sur la même LED, par exemple).
Dans un chenillard seul, une LED défaillante passe facilement inaperçue : ces deux étapes forment un test de câblage élémentaire, qu’on retrouve au démarrage de beaucoup d’appareils.
Étape 5 — Lire un bouton
Dans CubeMX, configurez PE10 en GPIO_Input, label
SW1, No pull-up and no pull-down. Régénérez le code.Faites suivre le bouton par LED_1 : allumée tant que SW1 est enfoncé, éteinte sinon. La lecture se fait avec :
GPIO_PinState etat = HAL_GPIO_ReadPin(SW1_GPIO_Port, SW1_Pin);
Question
Pourquoi la broche est-elle configurée sans tirage interne ? Que lirait-on, bouton relâché, si la résistance de 10 kΩ n’était pas sur la carte ?
Solution de l’étape 5
HAL_GPIO_WritePin(LED_1_GPIO_Port, LED_1_Pin,
HAL_GPIO_ReadPin(SW1_GPIO_Port, SW1_Pin));
HAL_GPIO_ReadPin lit le bit de la broche dans le registre IDR (Input
Data Register) du port E, et renvoie GPIO_PIN_SET (1) ou
GPIO_PIN_RESET (0). Le bouton et la LED sont tous deux actifs à
l’état haut : la valeur lue peut être recopiée telle quelle sur la LED.
Si le bouton était actif à l’état bas, il faudrait inverser la valeur.
La boucle tourne sans attente : la broche est lue plusieurs millions de fois par seconde, et la LED suit le bouton sans délai perceptible.
Réponse à la question 6
6. Sans tirage interne. La carte porte déjà, pour chaque bouton, une résistance de 10 kΩ entre la broche et la masse (pull-down). Activer en plus le tirage interne mettrait deux résistances en jeu :
un pull-down interne (environ 40 kΩ) serait en parallèle : sans gravité, mais inutile ;
un pull-up interne serait contraire : bouton relâché, la broche serait au milieu d’un pont diviseur 40 kΩ / 10 kΩ, vers 0,66 V — lu 0, mais avec une marge réduite et un courant permanent inutile.
Sans la résistance de 10 kΩ, bouton relâché, la broche ne serait reliée à rien : une entrée CMOS flottante. Son impédance est énorme (de l’ordre du gigaohm) ; la moindre charge électrostatique, le doigt qui approche, une piste voisine qui commute suffisent à la faire basculer. On lirait 0 ou 1 au hasard. Toute entrée doit avoir un état défini au repos : c’est le rôle des résistances de tirage.
Étape 6 — Le rebond
On veut maintenant qu”un appui sur SW1 fasse avancer le chenillard d’une LED, au lieu de le faire tourner tout seul.
Détectez le front montant : le moment où la lecture passe de 0 à 1. Gardez pour cela la lecture précédente dans une variable.
Appuyez dix fois sur SW1, lentement, et comptez de combien de LED le chenillard a avancé.
Un contact mécanique ne se ferme pas proprement : pendant quelques millisecondes, il rebondit, et la broche passe plusieurs fois de 0 à 1 avant de se stabiliser. Le programme, qui lit la broche des millions de fois par seconde, voit alors plusieurs appuis là où il n’y en a eu qu’un.
Éliminez le rebond par échantillonnage : lisez le bouton toutes les 20 ms seulement, et ne considérez un changement d’état que lorsque deux lectures successives concordent.
Questions
Sans anti-rebond, de combien de LED le chenillard avançait-il pour dix appuis ? Ce nombre est-il le même d’un essai à l’autre ?
Pourquoi 20 ms ? Que se passe-t-il avec 1 ms ? Avec 500 ms ?
Avec l’anti-rebond, le programme ne fait plus rien d’autre pendant l’attente de 20 ms s’il utilise
HAL_Delay. Comment faire avancer un clignotement et surveiller le bouton dans la même boucle ? Indice :HAL_GetTick()renvoie le nombre de millisecondes écoulées depuis le démarrage.
Solution de l’étape 6
Première version, sans anti-rebond : on garde la lecture précédente, et on détecte le passage de 0 à 1.
static GPIO_PinState previous = GPIO_PIN_RESET;
static uint32_t i = 0u;
GPIO_PinState now = HAL_GPIO_ReadPin(SW1_GPIO_Port, SW1_Pin);
if ((previous == GPIO_PIN_RESET) && (now == GPIO_PIN_SET))
{
i = (i + 1u) % 4u; /* front montant : un "appui" */
}
previous = now;
leds_show_one(i);
Version avec anti-rebond, dans une fonction (USER CODE BEGIN 0) qui
renvoie 1 une seule fois par appui :
#define DEBOUNCE_MS 20u
static uint32_t sw1_pressed(void)
{
static uint32_t last_sample_ms = 0u;
static GPIO_PinState previous_read = GPIO_PIN_RESET;
static GPIO_PinState stable = GPIO_PIN_RESET;
uint32_t now = HAL_GetTick();
uint32_t edge = 0u;
if ((now - last_sample_ms) >= DEBOUNCE_MS) /* une lecture / 20 ms */
{
GPIO_PinState read = HAL_GPIO_ReadPin(SW1_GPIO_Port, SW1_Pin);
last_sample_ms = now;
/* deux lectures successives identiques : l'état est stable */
if ((read == previous_read) && (read != stable))
{
stable = read;
edge = (stable == GPIO_PIN_SET) ? 1u : 0u;
}
previous_read = read;
}
return edge;
}
et dans la boucle :
static uint32_t i = 0u;
if (sw1_pressed()) { i = (i + 1u) % 4u; }
leds_show_one(i);
Comment ça marche. La fonction distingue trois choses :
read: ce que la broche vaut maintenant — peut-être en plein rebond ;previous_read: ce qu’elle valait 20 ms plus tôt ;stable: l’état reconnu du bouton, qui ne change que si deux lectures espacées de 20 ms concordent.
Un rebond dure typiquement de 1 à 10 ms. Deux lectures identiques à 20 ms
d’intervalle ne peuvent donc pas être toutes deux tombées dans le même
rebond : l’état est vraiment établi. On renvoie 1 seulement quand stable
passe à 1 : c’est le front montant de l’état stable, un appui et un seul.
Pourquoi sans HAL_Delay. La fonction compare HAL_GetTick() à l’instant
de sa dernière lecture, et rend la main aussitôt si 20 ms ne se sont pas
écoulées. La boucle principale n’est jamais bloquée : c’est la réponse à la
question 9.
Le calcul now − last_sample_ms. Il reste juste même quand le compteur de
millisecondes, un entier de 32 bits, revient à zéro (au bout de 49,7
jours) : la soustraction en entiers non signés se fait modulo 2³², et donne
toujours l’écart réel. Comparer now >= last_sample_ms + DEBOUNCE_MS
serait faux à ce moment-là.
Réponses aux questions 7 à 9
7. Sans anti-rebond. Pour dix appuis, le chenillard avance de plus de dix LED, et pas du même nombre d’un essai à l’autre. Chaque appui produit plusieurs fronts montants : à la fermeture, les lames du contact rebondissent l’une sur l’autre pendant quelques millisecondes, et la tension oscille entre 0 et 3,3 V. La boucle, qui lit la broche toutes les quelques dizaines de nanosecondes, voit chaque oscillation. Le nombre de rebonds dépend de la force et de la vitesse de l’appui : il n’est pas reproductible. Un oscilloscope sur la broche le montre très bien.
8. Le choix de 20 ms. Il faut une période plus longue que le rebond (quelques ms) et plus courte qu’un appui (un appui bref dure de 50 à 100 ms) :
à 1 ms, deux lectures successives peuvent tomber dans le même rebond : les faux appuis reviennent ;
à 500 ms, il faut tenir le bouton une seconde (deux lectures concordantes) pour qu’il soit reconnu, et deux appuis rapides n’en font qu’un : le bouton devient paresseux.
Entre 10 et 30 ms, le filtrage est sûr et le délai imperceptible (au plus 40 ms entre l’appui et sa prise en compte).
9. Faire deux choses à la fois. On n’attend plus : on mesure le temps écoulé. Chaque activité retient l’instant de sa dernière action, et la boucle vérifie à chaque tour si son délai est écoulé :
static uint32_t t_blink = 0u;
if (HAL_GetTick() - t_blink >= 250u)
{
t_blink = HAL_GetTick();
HAL_GPIO_TogglePin(LED_4_GPIO_Port, LED_4_Pin);
}
if (sw1_pressed()) { i = (i + 1u) % 4u; }
C’est la méthode de la boucle non bloquante. Elle marche, mais chaque nouvelle activité ajoute une variable de temps et un test : c’est pour gérer proprement plusieurs activités qu’existent les systèmes temps réel, comme FreeRTOS dans la partie 2.
Tout le code de la correction
main_user_code.c
— compilé sans avertissement contre la HAL du STM32H7.
Ce n’est pas un main.c complet : ce sont les extraits à placer dans
les blocs USER CODE du main.c généré par CubeMX, chacun sous le
commentaire qui indique son bloc. La boucle while (1) contient une
variante par étape, encadrée par #if 0 / #if 1 : n’en activer
qu’une à la fois. La configuration CubeMX attendue est rappelée en tête
du fichier, et les réponses aux questions à la fin.
Exercices supplémentaires
Ces exercices n’ont pas de correction.
Compteur binaire : chaque appui sur SW1 ajoute 1 à un compteur de 0 à 15, affiché en binaire sur les quatre LED ; SW2 le remet à zéro.
Vitesse réglable : SW1 accélère le chenillard, SW2 le ralentit, entre 50 ms et 1 s par LED.
Sens de rotation : SW3 inverse le sens du chenillard.
Jeu de réflexe : une LED s’allume au hasard ; il faut appuyer sur le bouton correspondant (SW1 à SW4) le plus vite possible. Les LED affichent ensuite le temps de réaction, en dixièmes de seconde, en binaire.
Appui long : distinguer un appui court (moins de 500 ms) d’un appui long sur SW1, et leur donner deux actions différentes.
Après le TP
Votre programme a remplacé le chargeur d’amorçage de RatBot dans la flash interne. Pour rendre la carte au robot, voir Remettre la carte en état RatBot.