STM32

De Wiki Robotronik
Aller à : Navigation, rechercher


Introduction

Arduino UNO
STM32 Nucleo-64 F401RE

Depuis 2014, nous utilisons des microcontrôleurs STM32 dans nos robots.

Dans cet article/tutoriel d'initiation, vous apprendrez quelques concepts de la programmation sur STM32 à travers un exemple. Le tutoriel peut paraître un peu long, et est peut être un peu difficile car il essaie de rentrer dans les détails pour expliquer l'utilité de chacune des étapes. Prenez votre temps et soyez méthodique, et vous devriez arriver à faire fonctionner votre premier programme sans trop de difficultés ;)

Si vous ne comprenez pas une chose ou êtes bloqué à une étape du tutoriel, plaignez vous ! Comme ça, on pourra vous expliquer et faire évoluer la partie qui pose problème. Toutes les remarques (constructives) sont également les bienvenues.

Présentation générale

Qu'est-ce que c'est ?

Vous connaissez certainement Arduino, que nous utilisons dans nos projets d'initiation. Ce sont des cartes de développement comprenant un microprocesseur, de la mémoire RAM, de la mémoire ROM, un programmateur, etc... Elles permettent simplement d'exécuter un programme pour interagir avec un circuit électronique. Un STM32, c'est plus ou moins la même chose.

Dans cet article, nous allons nous intéresser à l'exemple de la carte de développement STM32 Nucleo64 F401RE, mais le fonctionnement des autres MCU (Microcontroller Unit) STM32 est similaire.

Pour simplifier, on peut voir notre MCU comme la combinaison de :

  • Un microprocesseur ARM®
  • De la mémoire vive (RAM) et une mémoire flash (ROM) pour stocker nos programmes.
  • Des périphériques internes (timers, canaux DMA, ...)
  • Des périphériques externes (convertisseurs analogique/numérique, liaisons séries, ...)

Avec ça, on va pouvoir faire tourner des programmes écrits en C pour contrôler les moteurs de nos robots, récupérer des informations de différents capteurs et communiquer avec d'autres robots par exemple.

Pourquoi utiliser des STM32 ?

C'est vrai, ça, pourquoi ? Les français veulent savoir ! En fait, il y a plusieurs raisons :

  • STMicroelectronics est une entreprise partenaire de Robotronik, située à Grenoble
  • Les cartes STM32 sont puissantes et disposent de beaucoup de périphériques
  • Les outils de développement fournis permettent des configurations assez fines

Pré-requis

Vous êtes prêts à rentrer dans le merveilleux monde des STM32 ? Munissez vous d'un ordinateur tournant sur Linux, d'une connexion internet, d'une carte de développement (nous prendrons la STM32F401RE pour l'exemple), d'un câble mini-usb, et suivez le guide !

Description de l'environnement de développement

Comme nous l'avons vu en introduction, on va développer sur STM32 en C. Mais, en plus de notre éditeur favori, il va nous falloir quelques outils pour configurer notre carte et communiquer avec. Nous allons d'abord passer en revue ces outils afin de bien comprendre leur intérêt. Ne vous préoccupez pas de l'installation pour le moment, la partie suivante y est entièrement consacrée.

Couche d'abstraction matérielle

Notre microcontrolleur embarque différents périphériques, qui peuvent être plus ou moins complexes. Afin de ne pas avoir à écrire le code permettant de les exploiter, nous utiliserons une couche d'abstraction matérielle ou HAL en anglais pour Hardware Abstraction Layer. C'est une librairie, c'est à dire un ensemble de fonctions et de structures C qui permettent d'utiliser les différents périphériques des MCU - Microcontroller Unit.

Logciel de configuration

Même si nous disposons d'une HAL, cela n'est pas encore suffisant pour s'affranchir de l'écriture de code bas niveau, parce que la HAL ne peux pas deviner à notre place comment nous voulons utiliser nos périphériques. Il va donc nous falloir des fonctions d'initialisation pour chaque périphérique, qui renseigneront à la HAL des informations précises de configuration. Heureusement pour nous, STMicroelectronics a développé STM32CubeMX, un logiciel permettant de configurer les périphériques avec une interface graphique pour ensuite auto-générer un code d'initialisation.

Chaîne de compilation

Supposons que nous avons notre superbe code d'initialisation, et que nous sommes prêts à le compiler. Nous serions tentés d'utiliser notre bon vieux GCC pour compiler notre projet. Le problème, c'est que votre GCC préféré compile votre code pour votre machine, qui dispose de sa propre architecture, par exemple x86_64 ou i386 pour les nostalgiques, alors que les MCU STM32 embarquent un processeur ARM qui a une architecture différente. Vous ne pouvez donc pas exécuter du code compilé pour votre machine sur STM32, parce que les instructions du processeur ne sont pas les mêmes. C'est pourquoi nous avons besoin de faire de la cross-compilation, c'est à dire compiler le code directement pour notre cible, le STM32. Pour cela, on utilise une chaîne de compilation adaptée, ou toolchain en anglais, qui est tout simplement l'ensemble des programmes permettant de réaliser la compilation. Celle que nous utilisons est composée de deux parties :

  • La chaîne de compilation GNU Arm Embedded Toolchain, c'est à dire :
    • Le compilateur GNU Arm Embedded Compiler Collection, qui correspond à GCC dans sa version ARM.
    • La librairie Newlib qui est une librairie C pour systèmes embarqués.
    • GNU Arm Embedded Binutils qui contiens entre autre le linker et l'assembleur.
  • Un Makefile généré par STM32CubeMX qui définit comment la compilation doit être exécutée. C'est en fait un script qui sera exécuté par le programme GNU Make.

Programmateur

Maintenant que nous avons notre programme compilé, comment fait-on pour l'installer sur notre MCU ? En effet, le rôle du MCU est d'exécuter son programme à partir du moment où il a démarré, et rien de plus. Il nous faut donc un périphérique permettant de le reprogrammer, c'est à dire modifier son programme interne. De manière très originale, on appelle ça un programmateur. Sur STM32, c'est le rôle de la puce ST-Link, totalement indépendante du MCU, que l'on peux trouver sur une carte de développement Nucleo64 par exemple.

Communication USB

Pour envoyer notre programme à la puce ST-Link, nous avons besoin d'un logiciel permettant de communiquer avec. Nous utiliserons Open On-Chip Debugger, abrégé OpenOCD.

Debugger

Imaginons que nous avons réussi à uploader correctement notre programme sur STM32 (ce sera votre cas dans très peu de temps). Si vous êtes un humain normalement constitué, il va vous arriver de faire des programmes qui compilent mais qui ne font pas ce qu'ils sont sensés faire. Afin de gagner du temps, vous pourriez avoir envie de contrôler le flot d'exécution de votre programme, c'est à dire être capable de le mettre en pause à des endroits précis pour vérifier l'état des variables par exemple. Pour faire cela, on a besoin de deux choses :

  • Un programme qui va commander notre MCU à travers un programmateur pour lui dire quand exécuter du code, quand s'arrêter, pour lui demander d'envoyer des informations, etc... Si vous avez bien suivi, vous pourriez voir deviné qu'il s'agira bien de OpenOCD.
  • Un programme qui est capable de faire le lien entre les données envoyées par le MCU et votre code source. Ici, il s'agira de GNU Project Debugger ou GDB, mais dans sa version ARM.

Installation

STM32CubeMX

Afin de démarrer un projet, la première chose à avoir est STM32CubeMX. En effet, les premières questions que l'on va se poser en démarrant notre projet sont :

  • Quel circuit électronique va-t-on réaliser avec notre MCU ?
  • Comment va-t-on configurer les différents périphériques ?

Puisque STM32CubeMX est l'outil de configuration, ce sera donc le premier pilier de notre projet. Connaîssant votre configuration, il va pouvoir :

  • Fournir les différentes parties nécessaires de la HAL
  • Générer le code d'initialisation des périphériques
  • Générer le Makefile
  • Et même exporter un rapport pdf décrivant votre configuration.

Vous trouverez le logiciel à cette adresse : STM32CubeMX. Cliquez simplement sur Get Software comme sur l'image ci-dessous, puis suivez les instructions pour récupérer le logiciel. Vous aurez besoin de renseigner une adresse e-mail et vous devriez recevoir un lien de téléchargement par mail.

GetSoftwareCubeMX.png

Une fois que vous avez récupéré en.stm32cubemx.zip, placez le dans ~/STM32Cube/ (dossier que vous aurez au préalable créé) puis lancez l'utilitaire d'installation :

cd ~/STM32Cube
mkdir tmp && cd tmp
unzip ../en.stm32cubemx.zip
ls -l
chmod u+x ./SetupSTM32CubeMX-4.23.0.linux
./SetupSTM32CubeMX-4.23.0.linux

Vous devriez alors voir une fenêtre comme celle ci :

Si cela ne fonctionne pas sur les distributions de type Ubuntu il se peut que vous ayez à installer:

sudo add-apt-repository -y ppa:webupd8team/java && sudo apt-get update && sudo apt-get install -y oracle-java8-installer #utilisez la dernière version stable
#https://doc.ubuntu-fr.org/java_proprietaire
sudo apt-get install libc6-i386

InstallCubeMX.png

Installez le logiciel dans ~/STM32Cube/STM32CubeMX. Vous pouvez ensuite supprimer les fichiers d'installation :

cd ..
rm -Ir ./tmp
rm en.stm32cubemx.zip
cd STM32CubeMX/
./STM32CubeMX

Si tout a bien fonctionné, le logiciel doit se lancer et vous devriez voir une fenêtre comme celle-ci : MainCubeMX.png

Couche d'abstraction matérielle

HAL STM32

Les librairies HAL peuvent être installées directement depuis STM32CubeMX. Pour lancer l'utilitaire d'installation, il suffit de taper Alt+U. Chaque famille de MCUs (F3, F4, L0, etc...) dispose de sa propre librairie. Pour notre exemple, on choisira la dernière version de la HAL STM32CubeF4 pour notre MCU STM32F401RE.

Cochez la librairie en question et cliquez sur Install Now.

InstallHAL.png

Documentation

Les librairies HAL sont très complètes et chaque fonction/structure a ses particularités. Afin de pouvoir l'utiliser simplement, il est donc nécessaire de récupérer sa documentation. Pour trouver la documentation d'une HAL, rendez vous sur sa page sur le site de STMicroelectronics.

Vous trouverez celle de STM32CubeF4 à cette adresse : STM32CubeF4.

Le fichier qui vous intéresse est :

DocSTM32CubeF4.png

Téléchargez le et conservez le soigneusement, par exemple dans ~/STM32Cube/.

Chaîne de compilation/debugger/OpenOCD

Les programmes que nous n'avons pas encore installés étant des logiciels libres, vous devriez pouvoir les installer directement avec le gestionnaire de paquets de votre distribution.

Exemple avec apt-get (Ubuntu/Mint/Debian) :

sudo apt-get install gcc-arm-none-eabi libnewlib-arm-none-eabi gdb-arm-none-eabi binutils-arm-none-eabi make openocd

Exemple avec pacman (Arch/Manjaro) :

sudo pacman -S arm-none-eabi-gcc arm-none-eabi-newlib arm-none-eabi-gdb arm-none-eabi-binutils make openocd

Pour les autres distributions, renseignez vous sur les forums de ces distributions ou installez les paquets manuellement.

Utilisation

Dans cette partie, nous allons voir comment développer sur STM32 à travers un exemple. Notre programme sera très simple, et se contentera de faire deux choses :

  • Faire clignoter la LED de la carte de développement.
  • Allumer/éteindre une LED externe lorsqu'on appuie sur un bouton.

Ce programme ne sert évidemment à rien et on pourrait le faire sans microprocesseur. Cela dit, cela devrait vous premettre de comprendre les bases du développement sur STM32.

Création d'un projet STM32CubeMX

Créez un dossier vide dans lequel vous allez travailler, puis lancez STM32CubeMX.

Cliquez sur New Project ou utilisez le raccourci clavier Ctrl+N.

Une fenêtre s'ouvre pour vous demander de choisir la carte sur laquellle vous allez développer. Dans notre exemple, ce sera une carte de développement Nucleo64 STM32F401RE. On la trouvera donc dans l'onglet Board selector, comme sur l'image ci-dessous :

BoardSelectorSTM32Cube.png

Double-cliquez sur la carte pour valider votre choix.

Configuration des pins

La première étape de configuration consiste à choisir les pins que nous allons utiliser, définir leur mode de fonctionnement et leur donner un nom, pour pouvoir les retrouver facilement. Ici, nous avons besoin de :

  • Une entrée pour récupérer l'état d'un bouton poussoir
  • Une sortie pour contrôler la LED de la carte Nucleo.
  • Une sortie pour contrôler la LED de notre circuit.

Sélectionnez l'onglet Pinout sur STM32CubeMX en haut tout à gauche si ce n'est pas déjà fait.

PinoutCubeMX.png

Vous remarquerez qu'un certain nombre de pins sont déjà configurées. C'est normal, puisque le MCU est connecté à certains composants de la carte de développement.

Les pins qui nous intéressent en particulier sont PC13 reliée au bouton bleu, que nous allons utiliser pour allumer/éteindre notre LED et PA5 reliée à la LED verte de la carte, que nous allons faire clignoter.

Il est important de donner des noms évocateurs aux pins, parce que ça facilite la lecture du circuit et de votre futur programme. En effet, STM32CubeMX va utiliser les noms de pins définis dans le projet pour générer leur code d'initialisation. La convention de nommage d'une pin est assez simple, voici la forme standard :

NOM_PIN [Commentaire]

La lecture du nom doit permettre de comprendre comment la pin est utilisée. Par exemple, sur l'image ci-dessus, on voit tout de suite que la pin USART_RX sera utilisée pour une liaison série (Universal Synchronous/Asynchronous Receiver/Transmitter) et RX nous indique qu'il s'agit d'une pin qui reçoit des données. Le commentaire entre crochets est utilisé pour décrire la pin plus précisément, et ne sera pas ajouté au code généré.

Pour renommer une pin, il suffit de faire un clic droit dessus et de choisir Enter User Label. Puisque nous allons utiliser PC13 pour changer l'état de notre LED, renommons le en SWITCH_BTN. De même, puisque PA5 correspond à la sortie de la LED devant clignoter, renommons la BLINK_LED.

Maintenant, nous allons ajouter une pin pour la LED que nous voulons allumer/éteindre avec notre bouton.

Cliquez sur PA6, et choisissez GPIO_Output (General Purpose Input/Output). Puis renommez la PIN en SWITCH_LED.

Vous devriez maintenant avoir une configuration qui ressemble à ça :

ExamplePinoutCubeMX.png

Enfin, cliquez sur SWITCH_BTN et vérifiez que GPIO_EXTI13 est bien sélectionné (vous comprendrez plus tard pourquoi on choisit cela).

STM32 EXTI13.png

La configuration des pins est terminée !

Réalisation du circuit

Maintenant que nous avons décidé quels composants connecter à notre MCU et sur quelles pins, il faut réaliser le circuit. Peu importe la méthode que vous allez utiliser (PCB, plaque à trou, etc...), le principe sera toujours le même.

Ici, le bouton SWITCH_BTN et la LED BLINK_LED sont déjà reliés au MCU, nous n'avons plus qu'à relier la LED SWITCH_LED.

Bien évidemment, vous remarquerez la pin PA5 correspondante au premier regard de la carte :

Nucleo64.png

...ou pas.

Du coup, on va avoir besoin de récupérer un schéma de la carte.

Schéma d'une carte de développement

Allez sur la page correspondant à la Nucleo64 F401RE sur le site de STMicroelectronics.

Scrollez jusqu'à trouver la section Schematic Pack qui nous intéresse :

Nucleo64SchematicPack.png

Téléchargez le fichier et placez le dans ~/STM32Cube/ par exemple, puis extrayez le :

unzip en.nucleo_64pins_sch.zip

Ouvrez le dossier extrait puis ouvrez le .pdf. Scrollez jusqu'à la section Extension connectors :

F4ExtensionConnectors.png

On voit que la pin PA6 est reliée à la pin 13 du connecteur Morpho CN10, celui à droite de la carte. Elle est aussi connectée à la pin 5 du connecteur Arduino CN5, qui correspond à la pin D12 Arduino. Nous pouvons alors réaliser notre circuit, dont voici une représentation :

CircuitExempleF4.png

Pensez à bien relier le jumper IDD qui permet d'alimenter la carte, le jumper U5V qui permet de sélectionner l'alimentaiton ST-LINK USB et les deux jumpers ST-LINK qui permettent la programmation du MCU via ST-LINK.

Alimentation

Comme vous avez pu le voir, il y a une résistance dans le circuit, pour laquelle je n'ai pas encore donné de valeur.

Facile, me direz vous... Il suffit d'aller dans la datasheet de la LED, de récupérer l'information Continuous Forward Current et de choisir la résistance de telle sorte que ce courant ne soit pas dépassé.

Si vous faites ça, sachant que la Nucleo délivre du 3.3V, sur ses pins GPIO, ça devrait bien se passer.

Alors, pourquoi faire une partie Alimentation dans ce tutoriel ?

Parce qu'il faut que l'on se mette tout de suite d'accord sur une chose :

Un MCU n'est pas un composant de puissance

Je pourrai traduire cette phrase simplement en disant que si vous prenez un moteur par exemple et que vous branchez son alimentation sur un MCU, il y a de fortes chances pour qu'il crame. En effet, les MCUs sont des composants logiques : il sont là pour traiter des informations (signaux) provenant d'un circuit, et renvoyer des informations à ce circuit. Ils ne sont donc pas conçus pour délivrer des courants importants.

De manière générale, essayez de ne pas dépasser 10mA par pin, et en alimentation USB 5V (U5V), ne dépassez pas les 100mA de consommation totale sur une carte Nucleo.

Documentation

Puisqu'on commence à réaliser des circuits, il pourrait être intéressant de récupérer la documentation de notre carte.

Si vous avez bien suivi jusqu'ici, cette page devrait déjà être votre page d'accueil ;)

Scrollez jusqu'au manuel qui vous intéresse :

Nucleo64Manual.png

Si vous voulez savoir des choses plus précises à propos du MCU STM32F401RE, vous devriez trouver votre bonheur ici : STM32F401RE

Il est également possible de récupérer les documentations depuis STM32CubeMX en allant dans Help > Docs & Resources.

Configuration d'un timer

Dans notre exemple, nous voulons faire clignoter une LED, à une fréquence régulière. Prenons une fréquence de 2Hz.

On pourrait faire un programme qui se contente d'attendre 500 ms pour changer l'état de la LED, le tout dans une boucle infinie.

Mais pour s'entraîner, on va faire quelque chose d'un peu plus technique, qui pourrait vous servir par la suite : on va utiliser un timer matériel, c'est à dire un composant physique qui va lui même activer du code permettant de faire changer l'état de la LED à intervalles régulier.

Mais alors Jamy, comment ça marche cette chose là ?

La programmation par interruption : introduction

Pour illustrer ce que sont les interruptions, prenons un exemple très simple. Imaginez que vous développez un système d'exploitation, et que vous vous intéressez à la gestion du clavier. Le problème en soit est relativement simple : lorsque l'utilisateur appuie sur une touche, il faut le détecter, pour informer l'application au premier plan de cette action. Une solution consiste à faire une boucle while, qui regarde sans arrêt si une touche est activée, et exécute une action si c'est le cas. Mais il y a deux problèmes. Le premier, c'est que la plupart du temps, l'utilisateur n'appuie pas sur une touche, donc la boucle tourne pour rien. Le deuxième, c'est que votre système a bien d'autres choses à faire que de vérifier les touches du clavier, et ce n'est donc pas forcément facile de revenir régulièrement sur du code gérant le clavier.

Quand vous scrutez sans arrêt l'état d'un périphérique, on dit que vous faites du polling. La programmation par interruption amène une autre approche, qui consiste à se dire : et si c'était le clavier qui disait à mon système quand une touche est activée ? L'idée est simple : lorsque l'utilisateur appuiera sur une touche, un signal sera envoyé au processeur pour lui dire qu'il doit traiter l'événement. Le processeur va alors arrêter ce qu'il était en train de faire (c'est pour ça qu'on parle d'interruption), et exécuter du code spécifique pour gérer l'appui d'une touche. Une fois l'événement géré, le programme qui tournait sur le processeur pourra reprendre.

Sur STM32F4, c'est un composant appelé Nested Vectoried Interrupt Controller (NVIC) qui va communiquer avec le processeur pour lui dire quand un événement qui l'intéresse s'est produit. Le processeur va donc interrompre le programme en cours et charger le programme qu'il doit exécuter pour cet événement, dont l'adresse se trouve dans la table des interruptions préalablement configurée. Une fois le bout de code exécuté, le programme qui tournait avant sera restauré et pourra continuer.

Le retour de STM32CubeMX

Maintenant que vous savez tout (ou presque) sur le concept de programmation par interruptions, il est temps de retourner sur l'onglet Pinout de STM32CubeMX.

Le timer que nous allons utiliser est TIM3 qui fait partie des timers dits General-purpose timers (p311 sur la doc du STM32F401RE).

Cliquez sur TIM3 et sélectionnez Internal Clock (CK_INT) comme source.

TIM3.png

Rendez vous maintenant sur l'onglet Clock Configuration.

STM32CubeClock.png

En se promenant un peu dans la doc, on lit que le timer TIM3 est relié à APB1. Ici, APB1 est réglé sur 84MHz pour les timers. On pourrait changer cette configuration, mais ce n'est pas nécessaire.

Rendez vous dans l'onglet Configuration.

ConfigurationCubeMX.png

C'est depuis cet onglet que l'on peut configurer les différents périphériques. Cliquez sur TIM3 pour le configurer.

TIM3Config.png

Faisons le point sur le fonctionnement du timer. C'est un composant qui contient un compteur relié à l'horloge APB1. Initialement, le compteur vaut 0. Tous les PSC+1 fronts montants de l'horloge, le compteur est incrémenté. Lorsque la valeur contenue dans le registre AutoReload est atteinte, un signal de Reset remet le compteur à 0. Ce qui est intéressant, c'est qu'il est possible de générer une interruption au moment de la remise à 0 du compteur.

Pour avoir une fréquence de 2 Hz, nous allons donc positionner le prescaler PSC à 42000-1, ainsi le compteur sera incrémenté tous les 42000 fronts montants de l'horloge APB1 qui est à 84MHz ce qui nous donne une fréquence d'incrémentation de 2 kHz. Reste à placer la valeur de AutoReload à 1000-1 (puisque le compteur commence à 0) pour obtenir notre fréquence de 2Hz.

Enfin, il ne reste plus qu'à activer l'interruption dans l'onglet NVIC Settings :

TIM3Interrupt.png

Configuration GPIO

Il ne reste plus qu'à configurer les pins d'entrée/sortie. De retour dans l'onglet Configuration, cliquez sur GPIO.

Configurez les pins GPIO comme sur l'image ci-dessous si ce n'est pas déjà fait.

STM32 GPIO.png

PA5 et PA6 sont de simples sorties qui serviront à allumer et éteindre les leds.

PC13-ANTI_TAMP est la pin connectée au bouton que l'on veut utiliser pour changer l'état de la LED sur demande. Nous utiliserons là aussi une interruption, c'est pour cela que l'on a choisi de relier le bouton à GPIO_EXTI13 dans la configuration des pins. En sélectionnant External Interrupt Mode with Falling edge trigger detection, une interruption sera déclenchée sur la ligne EXTI13 du contrôleur lorsque le bouton sera appuyé.

Si vous avez bien suivi la partie précédente, vous savez qu'il ne suffit pas qu'un signal d'interruption existe pour qu'elle soit traitée : encore faut-il que le matériel soit configuré pour relayer l'information au CPU. Retournez alors dans l'onglet Configuration et cliquez sur NVIC. Cochez alors EXTI line[15:10] interrupts et validez avec Ok.

STM32 NVIC.png

J'ai une bonne nouvelle : la configuration du projet est terminée ! Vous devez vous dire que c'était plutôt long pour un projet pourtant très minimaliste. C'est normal, car nous avons pris le temps de détailler toutes les étapes, lire la doc, etc... Une fois que vous aurez pris en main STM32CubeMX, vous verrez que vous pourrez refaire ce mini projet en même pas 2 minutes, chrono en main.

Exportation du projet

Allez dans le menu Project > Settings (Alt-P).

Choisissez un nom de projet, et un dossier pour l'enregistrer. Enfin, sélectionnez Makefile dans Toolchain / IDE. Rappelez vous, le Makefile va donner au programme GNU Make les instructions pour compiler notre projet.

STM32 Project.png

Dans l'onglet Code Generator, utilisez cette configuration :

STM32 CodeGenerator.png

L'onglet Advanced Settings permet entre autres de choisir les fonctions à générer. Vous ne devriez pas avoir besoin de les modifier ici.

Cliquez sur Ok.

De retour dans STM32CubeMX, vous pouvez générer un rapport au format PDF décrivant le projet en cliquant sur Project > Generate Report (Ctrl+R).

Enfin, générez le code du projet : Project > Generate Code (Ctrl+Shift+G).

Programmation

Préparation de l'environnement de développement

Récupérez le Makefile de base ainsi que la configuration d'OpenOCD sur le dépôt Git Robotronik : https://github.com/robotronik/cdfr2018/tree/master/Makefile

Créez un dossier contenant ces fichiers et déplacez le dossier de votre projet STM32CubeMX dedans. Par exemple si votre projet se nomme Exemple_LED, vous devriez avoir cette arborescence :

./
├── Exemple_LED
│   ├── Drivers/
│   ├── Exemple_LED.ioc
│   ├── Inc
│   │   ├── main.h
│   │   ├── stm32f4xx_hal_conf.h
│   │   └── stm32f4xx_it.h
│   ├── Makefile
│   ├── mx.scratch
│   ├── Src
│   │   ├── gpio.tmp
│   │   ├── license.tmp
│   │   ├── main.c
│   │   ├── stm32f4xx_hal_msp.c
│   │   ├── stm32f4xx_it.c
│   │   ├── system_stm32f4xx.c
│   │   └── system.tmp
│   ├── startup_stm32f401xe.s
│   └── STM32F401RETx_FLASH.ld
├── Makefile
├── openocd
│   ├── attach_f3.gdb
│   ├── attach_f4.gdb
│   ├── gdb-pipe.cfg
│   ├── st_nucleo_f3.cfg
│   └── st_nucleo_f4.cfg

Ouvrez le Makefile (dans ./) et modifiez BOARD, PROJECT_DIR et PROJECT_NAME. Dans l'exemple, ce serait:

BOARD=f4
PROJECT_DIR=Exemple_LED
PROJECT_NAME=Exemple_LED

Ensuite, ouvrez le Makefile dans ./Exemple_LED/Makefile (celui généré par STM32CubeMX). Renseignez le dossier dans lequel se trouvent vos programmes dans la ligne BINPATH, par exemple sur la plupart des distributions linux :

BINPATH=/usr/bin
Dans la section C_SOURCES, assurez vous qu'aucune ligne n'est présente deux fois ! Une ligne se termine par un antislash.

Si c'est le cas, supprimez les doublons (attention aux 4 lignes commençant par Src pour cet exemple).

Une version de STM32CubeMX ajoute en effet plusieurs fois certains fichiers à cause d'un bug, ce qui cause l'échec de la compilation. Si vous avez du mal à trouvez les doublons, ils vont apparaître comme source d'erreur lors de la compilation. Si vous voulez ajouter des fichiers sources au projet, vous devrez les ajouter à C_SOURCES.

Vous êtes prêts à compiler le projet ! Retournez dans ./ et lancez la commande make. Si vous n'avez pas d'erreurs, vous pouvez continuer ce tutoriel. Sinon, vérifiez que vous avez bien suivi les étapes de cette partie, et si ça ne fonctionne toujours pas, demandez de l'aide à un membre du club.

Compilation, flash et debugger

Compilation

Pour compiler le projet, il suffit de lancer make à la racine du projet (là où il y a le dossier openocd et le Makefile que vous avez récupéré). Pour effacer les traces de compilation, lancez make clean.

Flash

Pour écrire le programme compilé dans la ROM du MCU, connectez le en USB et lancez make f.

Après un flash réussi, le programme commence à s'exécuter sur le STM32. Pour relancer le programme depuis le début, appuyez sur le bouton Reset.

Si vous avez l'erreur open failed, LIBUSB_Open failed ou quelque chose de similaire, essayez de lancer la commande avec les droits root : sudo make f.

Pour éviter d'avoir à faire sudo, il est possible de créer une règle udev pour vous autoriser à écrire sur le port USB. Pour plus d'informations, rendez vous sur le wiki d'Archlinux : https://wiki.archlinux.org/index.php/udev#Writing_udev_rules

Debugger

Pour debugger votre programme, lancez make debug (après un flash réussi). Cela vous permettra de suivre l'évolution de votre programme avec GDB.

L'utilisation de GDB n'est pas détaillée dans ce tutoriel car c'est un sujet à part entière complètement indépendant des STM32. Cela dit, vous devriez trouver votre bonheur sur internet si vous voulez en savoir davantage.

Retour sur l'exemple

Si vous êtes arrivé jusqu'ici dans le tutoriel, vous savez déjà tout ce qu'il faut pour pouvoir commencer à développer sur STM32, et je pourrai arrêter le tutoriel ici.

Mais vous allez me dire que c'est bien beau tout ça, on s'est bien amusé avec STM32CubeMX, mais la LED BLINK_LED elle clignote toujours pas, et il se passe rien quand on appuie sur le bouton B1.

Dans cette ultime partie, on va donc ajouter le code permettant de faire fonctionner l'exemple.

Munissez vous de la doc de la HAL. Vous devriez déjà l'avoir, mais au cas où, voici le lien.

Le code est donné dans ce tutoriel, pour être sûr que chacun puisse avoir quelque chose qui fonctionne à la fin, mais essayez de rechercher dans la doc par vous même. C'est une bonne habitude à prendre et ça vous permettra d'aller plus vite pour faire quelque chose que vous ne connaissez pas plus tard.

STM32CubeMX a déjà généré un code permettant d'initialiser les différents périphériques selon la configuration du projet. Je vous encourage à lire les fichiers .c dans Src/ et .h dans Inc/ pour avoir une idée de ce qui est fait. Cependant, il n'est pas nécessaire de tout comprendre.

Gestion des interruptions avec la HAL

De manière générale en programmation, chaque librairie a ses spécificités et sa façon de fonctionner. Ici, nous voulons savoir comment les interruptions sont gérées avec la HAL. Rendez vous dans la section 2.5.3 HAL interrupt handler and callback functions de la doc.

On y apprend que la gestion des interruptions est déjà prise en charge dans stm32f4xx_it.c qui est généré par STM32CubeMX, et que tout ce qu'il nous reste à faire est de créer une fonction utilisateur Callback propre à l'interruption, et qui sera exécutée lorsque l'interruption est déclenchée. Dans la section précédente de la doc, 2.5.2, il y a également une information intéressante : les fonctions d'un périphérique PPP commencent par HAL_PPP_. Avec ça, on va pouvoir faire des recherches dans la doc pour trouver rapidement ce dont on a besoin. Le sommaire est aussi un bon moyen de trouver une section, surtout si on ne sait pas vraiment ce qu'on cherche (ça arrive).

Changer l'état d'une pin GPIO

Pour tout savoir sur la gestion des GPIO sur STM32, rendez-vous dans la section 29 de la doc. L'initialisation des pins étant prise en charge par STM32CubeMX, nous allons nous intéresser à la fonction permettant de passer d'un état à l'autre (1 si c'est 0, 0 si c'est 1).

Cette fonction est HAL_GPIO_TogglePin().

HAL GPIO TogglePin.png

La fonction prend en paramètre le port GPIO de la pin et son numéro. Pour les obtenir, regardez du côté de main.h

#define SWITCH_BTN_Pin GPIO_PIN_13
#define SWITCH_BTN_GPIO_Port GPIOC
#define SWITCH_BTN_EXTI_IRQn EXTI15_10_IRQn
#define USART_TX_Pin GPIO_PIN_2
#define USART_TX_GPIO_Port GPIOA
#define USART_RX_Pin GPIO_PIN_3
#define USART_RX_GPIO_Port GPIOA
#define BLINK_LED_Pin GPIO_PIN_5
#define BLINK_LED_GPIO_Port GPIOA
#define SWITCH_LED_Pin GPIO_PIN_6
#define SWITCH_LED_GPIO_Port GPIOA

Puisque nous avons soigneusement nommé les pins lors de la configuration, il est très facile de voir que pour changer l'état de nos leds, il suffira d'appeler la fonction comme ceci :

HAL_GPIO_TogglePin(SWITCH_LED_GPIO_Port, SWITCH_LED_Pin); //Change l'état de SWITCH_LED
HAL_GPIO_TogglePin(BLINK_LED_GPIO_Port, BLINK_LED_Pin); //Change l'état de BLINK_LED

Faire clignoter la LED BLINK_LED

Ouvrez main.c dans votre éditeur favori.

La première chose notable concerne la présence de commentaires :

/* USER CODE BEGIN Includes */
 
/* USER CODE END Includes */

Cela peut sembler superflu, mais il est très important de mettre votre code à l'intérieur de ces délimiteurs. En effet, ce sont ces commentaires qui vont permettre à STM32CubeMX de faire la différence entre votre code et le sien. Si jamais vous voulez modifier la configuration du projet et que vous regénérez le code, STM32CubeMX conservera tout le code entre un /* USER CODE BEGIN ... */ et un /* USER CODE END ...*/ et remplacera tout le reste.

Si vous voulez écrire beaucoup de code, faites des fichiers .c et .h et ajoutez les à la liste des fichiers à compiler dans le Makefile de STM32CubeMX. Il faudra rajouter vos fichiers dans le Makefile si vous regénérez le code, mais vos fichiers devraient être conservés (dans le doute, faites des sauvegardes).

La deuxième chose qui peut attirer votre attention est la présence de la variable globale TIM_HandleTypeDef htim3, qui concerne notre timer TIM3. On vous a certainement dit un jour que les variables globales en programmation sont une mauvaise pratique. On vous a menti.

En fait, quand vous faites de la programmation séquentielle, c'est à dire que les instructions qui doivent s'exécuter sont déterminées, c'est une très mauvaise idée de faire des variables globales, et c'est inutile car vous pouvez transmettre les données d'une fonction à l'autre. Mais lorsque vous utilisez des interruptions, vous faites de la programmation événementielle : les instructions que le CPU va exécuter peuvent être modifiées en fonction des événements. Dans ce cas, certaines données ne peuvent pas être transmises par appel de fonctions, car certaines fonctions ne sont jamais appelées par une autre ! Si ça vous paraît obscur, ce n'est pas grave : vous allez comprendre en pratiquant.

Rassurez vous, il est possible d'être rigoureux en utilisant des variables globales, et il est tout à fait possible de faire de la preuve d'algorithme sur de tels programmes, à condition de respecter certaines règles.

Rendez-vous dans la section 65 HAL TIM Generic Driver. Au paragraphe 65.1.9, on apprend que la structure TIM_HandleTypeDef contiens un champ Instance qui permet d'identifier le timer instancié. La section 2.2.1 explique plus en détail l'utilité de ces structures HandleTypeDef.

La section 65.2.2 explique comment utiliser la HAL avec les timers. Chaque périphérique dispose d'une section semblable. La partie initialisation étant déjà faite, nous nous intéressons à la fonction permettant de démarrer le timer en mode interruption : HAL_TIM_Base_Start_IT().

Pour démarrer le timer, il suffira d'appeler cette fonction comme ceci :

HAL_TIM_Base_Start_IT(&htim3);

Ouvrez main.c et rajoutez cette ligne :

int main(void)
{
 
/* USER CODE BEGIN 1 */
 
/* USER CODE END 1 */
 
/* MCU Configuration----------------------------------------------------------*/
 
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
 
/* USER CODE BEGIN Init */
 
/* USER CODE END Init */
 
/* Configure the system clock */
SystemClock_Config();
 
/* USER CODE BEGIN SysInit */
 
/* USER CODE END SysInit */
 
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM3_Init();
 
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim3);
/* USER CODE END 2 */
 
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
 
/* USER CODE BEGIN 3 */
 
}
/* USER CODE END 3 */
 
}

Faites bien attention à mettre la ligne après MX_TIM3_Init() : il ne faut pas démarrer le timer avant son initialisation.

Remarquez qu'il y a une boucle infinie à la fin du main. En effet, on ne peut pas sortir de la fonction main, puisque le programme se terminerait et le MCU s'arrêterait brutalement car il n'aurait plus de code à exécuter. Si jamais vous plantez un MCU, pas de panique : le bouton Reset est votre ami.


De retour dans la documentation, allez à la section 65.2.11 pour obtenir la liste de ces fameuses fonctions Callback dont nous parlions plus haut. Celle qui nous intéresse est HAL_TIM_PeriodElapsedCallback(). Puisque nous avons configuré le timer TIM3 pour qu'il soit remis à 0 tous les 0,5 s, cette fonction sera appelée toutes les 0,5s. Cette fonction a pour prototype :

void HAL_TIM_PeriodElapsedCallback (TIM_HandleTypeDef * htim);

Elle sera donc exécutée toutes les 0,5s et recevra en paramètre une structure TIM_HandleTypeDef contenant les paramètres du timer ayant déclenché l'interruption. La fonction que nous devons coder est donc très simple :

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef* htim){
if(htim->Instance == htim3.Instance){
HAL_GPIO_TogglePin(BLINK_LED_GPIO_Port, BLINK_LED_Pin);
}
}

On vérifie si l'instance du timer est bien celle de TIM3 et si oui, alors il faut changer l'état de la LED BLINK_LED. Placez cette fonction dans main.c, entre /* USER CODE BEGIN 4 */ et /* USER CODE END 4 */

Changer l'état de la LED SWITCH_LED

Maintenant que vous avez compris comment fonctionnent les interruptions avec la HAL, vous devriez être capables de coder celle qui va gérer le changement d'état de la LED BLINK_LED lorsque l'on appuie sur le bouton B1.

Comme je suis gentil, je vous le donne :

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
if(GPIO_Pin == SWITCH_BTN_Pin){
HAL_GPIO_TogglePin(SWITCH_LED_GPIO_Port, SWITCH_LED_Pin);
}
}

Placez ce code également dans main.c entre /* USER CODE BEGIN 4 */ et /* USER CODE END 4 */.

Test et explications

C'est terminé ! Vous pouvez revenir dans le dossier parent du projet STM32CubeMX et faire make. Si ça compile, branchez le STM32 et faites make flash. Normalement, si ça fonctionne, la LED verte du STM32 devrait se mettre s'allumer et s'éteindre une fois par seconde et la LED SWITCH_LED devrait s'allumer si vous appuyez sur B1 et s'éteindre si vous appuyez à nouveau.

Si ça ne fonctionne pas, vérifiez l'installation des différents programmes, la configuration du projet et votre code. Si vous êtes bloqué, n'hésitez pas à demander de l'aide à un membre du club.

Si c'est la première fois que vous faites de la programmation événementielle, je vous dois une dernière explication. Lorsque le programme démarre, le main s'exécute : les différents périphériques sont initialisés, puis la boucle while(1){} s'exécute indéfiniment, en ne faisant rien... Il est alors normal de se demander comment un programme qui rentre dans une boucle infinie sans rien faire peut fonctionner, et comment les fonctions Callback font pour faire leur travail sans jamais être appelées. C'est là toute la puissance des interruptions.

Lorsque vous appuyez sur le bouton B1, qui est connecté à la ligne EXTI 13, vous générez un signal qui est détecté par le matériel, puisque nous l'avons configuré pour.

Le signal est alors envoyé au NVIC Nested Vectored Interrupt Controller puisque la ligne EXTI 13 y est connectée. Tout cela est fait au niveau matériel : pendant ce temps, votre CPU est toujours bloqué dans sa boucle infinie. Comme nous avons configuré le NVIC pour qu'il n'ignore pas la ligne EXTI 13, il envoie un signal d'interruption au CPU. Le CPU sauvegarde son état et lit dans la table des interruptions l'adresse de la fonction qu'il doit exécuter pour cette interruption... qui n'est autre que la première ligne de HAL_GPIO_EXTI_Callback puisque nous avons configuré cela grâce à STM32CubeMX. Une fois la fonction exécutée, le processeur restaure l'état dans lequel il était et se retrouve donc dans le main à continuer sa boucle infinie, comme si rien ne s'était passé.

Notez qu'on aurait très bien pu mettre des choses dans la boucle while, et ça fonctionnerait indépendemment des interruptions, sauf que certaines instructions seraient arrêtées en pleine exécution et seraient reprises plus tard, augmentant ainsi leur temps effectif d'exécution.