Cette page vise à présenter une première utilisation du logiciel STM32CubeIDE pour la programmation de microcontrôleurs STM32. En la suivant pas-à-pas, un développeur logiciel doit être capable:

  • de créer un projet,
  • de savoir où ajouter son programme (simple) en langage C,
  • de connaître quelles informations sont mises à disposition par l’outil pour le débogage et comprendre à quoi elles servent.
Version des outils Les différentes étapes utilisent la version v1.12.0 du logiciel STM32CubeIDE. Certaines variations au niveau des captures d’écrans peuvent apparaître si vous utilisez des versions différentes. De même, la carte utilisée est la Nucleo-F446RE.

Créer un projet

Pour créer un projet sur STM32CubeIDE, différentes étapes sont nécessaires. Celles-ci sont présentées ci-dessous.

Paramètres par défaut Par défaut, laissez les paramètres proposés lorsque des fenêtres s’ouvrent.
Étape 0 Tout d’abord, lancez STM32CubeIDE. Au démarrage, le logiciel vous demandera (si vous ne l’avez jamais utilisé) où est situé l’espace de travail. Cela correspond à l’endroit où seront placés vos futurs projets. Choisissez donc un répertoire qui vous appartient et où vous pourrez récupérer facilement vos données. Si vous souhaitez ne plus voir cette fenêtre à chaque démarrage de STM32CubeIDE, vous pouvez cocher la case correspondante.
Bonnes pratiques de nommage Pour éviter tout problème, veillez à n’utiliser que des caractères alphanumérique et des underscores (_) pour le nom des répertoires et projets. C’est une bonne habitude pour la création de n’importe quel répertoire ou fichier !

Lancement de STM32CubeIDE

Étape 1 Lors de la première ouverture, aucun projet n’existe. Pour en créer un, cliquez sur l’icône illustré ci-dessous (Start new STM32 Project). Vous pouvez aussi aller dans File -> New -> STM32 Project.

Lancement de STM32CubeIDE

Étape 2 Sur la prochaine fenêtre, la cible utilisée doit être choisie. Ici, on considère une carte Nucleo-F446RE. Pour la trouver, allez dans l’onglet Board Selector. Dans Type, cochez Nucleo-64. Enfin, dans la liste de cartes proposées, sélectionnez NUCLEO-F446RE. Cliquez ensuite sur le bouton Next>.

Sélection de la cible

Étape 3 Donnez un nom au projet que vous souhaitez créer. Laissez les autres valeurs par défaut et cliquez sur Next>. Deux fenêtres vont s’ouvrir, cliquez à chaque fois sur Yes.

Nom du projet

Étape 4 Une fois différentes données locales téléchargées, vous arrivez alors sur la fenêtre suivante. Le projet a donc été créé. Vous retrouvez sur la gauche la hiérarchie des fichiers du projet et au centre une animation présentant la configuration de votre système.
Vue d'ensemble

Ajouter du code

Liste des fichiers du projet

Après la configuration du projet, différents fichiers sont créés. Nous allons essentiellement nous concentrer sur le fichier main.c, qui contient la fonction principale main. Ouvrez-le et retrouvez la fonction suivante:

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_USART2_UART_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

Lors de la création du fichier, l’IDE pré-complète et organise la fonction main. Ainsi, en plus de fonctions d’initialisation du système, il ajoute également des commentaires vous indiquant où placer le code utilisateur (USER CODE). Pour développer une application, la partie essentielle de la fonction main est la boucle while(1). Étant infinie, elle permet de garantir que l’exécution restera bloquée à l’intérieur. Ainsi, une fois l’initialisation terminée, c’est à l’intérieur de cette boucle que l’on viendra ajouter les opérations que l’on souhaitera effectuer aussi longtemps que le microcontrôleur fonctionnera. Par exemple, pour créer une variable count qui s’incrémentera infiniment, ajoutez le code suivant:

int main(void) {
  /* USER CODE BEGIN 2 */
  uint32_t count = 0;
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1) {
    /* USER CODE END WHILE */
	  count = count + 1;
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

La partie USER CODE 2 étant avant la boucle, alors celle-ci ne s’exécutera qu’une seule fois. En revanche, la partie USER CODE 3 étant dans la boucle, alors elle sera exécutée aussi longtemps que le système restera en marche. Dans notre cas, on souhaite créer la variable count une seule fois au départ: on la place donc dans USER CODE 2. Par la suite, on souhaite l’incrémenter tout le long du fonctionnement donc on place le code correspondant dans USER CODE 3.

De la même manière, il sera possible d’exécuter indéfiniment des tâches plus complexes en les plaçant dans cette boucle infinie. C’est ce qui est fait lorsque l’on souhaite qu’un système réalise une mission bien précise et répétitive dans le temps.

De manière générale, on organisera notre code de la manière suivante:

  • L’initialisation du système s’effectuera dans les parties USER CODE Init ou USER CODE SysInit. Ainsi, si les configurations ne seront pas écrasées par l’initialisation du système effectuée par défaut par les fonctions HAL_Init() ou SystemClock_Config().
  • L’initialisation des périphériques s’effectuera dans la partie USER CODE 2, le code exécuté une seule fois avant la boucle infinie. Là encore, cela permettra de mettre les périphériques dans des états connus, en étant sûr que les fonctions par défaut MX_GPIO_Init() et MX_USART2_UART_Init() ne viennent pas les modifier.
  • La déclaration de variables locales se fera au départ de la fonction main, dans la partie USER CODE 1 avant leur utilisation.
  • Les opérations initiales (exécutées qu’une seule fois) seront également effectuées dans la partie USER CODE 2.
  • Enfin, les opérations récurrentes (qui doivent être ré-exécutées indéfiniment) seront placées dans la parties USER CODE 3, c’est-à-dire dans la boucle while(1).
Les indications précédentes sont uniquement un moyen d’organiser votre code pour limiter l’apparition de bus inattendus (mauvaise valeur initiale, périphérique désactivé etc.). Bien évidéemment, rien n’empêche d’organiser votre code autrement: supprimer les fonctions par défaut, ne pas avoir de boucle while (1) etc. Pour la suite de ces expérimentations, il vous est cependant demandé de suivre ces indications et de valider les résultats attendus avant de tenter vos propres solutions.

Debogage

Une fois le code modifié, il est nécessaire de s’assurer qu’il réalise la fonctionnalité voulue. Il faut donc passer par une phase de débogage (ou debug), où le code est directement exécuté. STM32CubeIDE met pour cela à disposition une interface dédiée.

Connexion de la carte Pour réaliser le débogage, pensez à connecter votre carte et à vous assurer qu’elle a bien été détectée par le logiciel.

Lancement

Pour lancer le mode de débogage, cliquez sur l’icône Debug (icône en forme d’insecte). Une fenêtre va s’ouvrir pour configurer la session de débogage. Laissez les paramètres par défaut et cliquez sur OK.

Fenêtres

En mode débogage, plusieurs fenêtres permettent d’avoir différentes informations sur le système. Voici ci-dessous un rapide récapitulatif de ces fenêtres, accessibles par des onglets sur la droite du logiciel.

Variables Cette fenêtre liste les différentes variables du programme. Pour chacune d’elles, sa valeur est également précisée.

Variables

Points d’arrêts Cette fenêtre liste les différents points d’arrêt (breakpoints) existants. Un point d’arrêt sert durant le débogage à arrêter l’exécution à un endroit précis. Nous verrons dans la prochaine partie exécution comment les utiliser.

Points d'arrêts

Registres Cette fenêtre liste les différents registres internes du processeur. Pour chacun d’eux, il est possible de connaître la valeur qu’il contient.

Registres

Registres spéciaux Cette fenêtre liste les différents registres spéciaux (SFRs pour Special Function Registers) du système. Nous verrons certains d’entre eux lors de l’utilisation des périphériques.

Registres spéciaux

Désassemblé Cette fenêtre présente le code assembleur correspondant au code C compilé. C’est ce qu’on appelle le désassemblé (ou disassembly). Pour activer cette fenêtre, cliquez sur Window -> Show View -> Disassembly.

Désassemblé

Explorateur mémoire Cette fenêtre présente un explorateur pour la mémoire. En tapant le nom d’une variable, il est possible de visualiser sa valeur drectement en mémoire. Pour activer cette fenêtre, cliquez sur Window -> Show View -> Memory Browser.

Explorateur mémoire

Analyseur mémoire Cette fenêtre présente la répartition du programme en mémoire. Il est possible de découvrir où sont situées chaque variable, fonction etc. Pour activer cette fenêtre, cliquez sur Window -> Show View -> Build Analyzer.

Analyseur mémoire

Exécution

Icônes pour l'exécution

Pour réaliser le débogage d’un code, il est nécessaire de l’exécuter. Différentes commandes peuvent être utilisées pour cela. Voici les principales icônes et leurs fonctions:

  1. Relancer le mode Debug.
  2. Lancer l’exécution indéfiniment.
  3. Terminer l’exécution en cours et en relancer une nouvelle depuis le début.
  4. Poursuivre l’exécution qui a été interrompue.
  5. Interrompre l’exécution en cours.
  6. Terminer l’exécution en cours.
  7. Exécuter la prochaine ligne de code. En cas d’appel de fonction, l’exécution s’arrête au début de la fonction.
  8. Exécuter la prochaine ligne de code. En cas d’appel de fonction, celle-ci est entièrement exécutée.

En utilisant ces différentes commandes, il est donc possible d’avancer dans l’exécution et de voir si une application fonctionne correctement. Cependant, elles peuvent s’avérer limitées lorsqu’il s’agit d’observer le comportement d’une ligne de code en particulier. Pour cela, il est possible d’introduire des points d’arrêts (breakpoints).

Exemple de points d'arrêts

Un point d’arrêt est une directive indiquant au système d’interrompre l’exécution dès qu’il arrive à une certaine ligne du code. En débogage, il est possible d’introduire des points d’arrêts en cliquant dans la zone bleue sur la gauche du code, comme sur la figure ci-dessus. Ici, deux points d’arrêts sont placés lignes 92 et 100: à chaque fois que l’exécution arrivera à l’une de ces lignes, elle sera interrompue et devra être relancée manuellement.

On notera que des points d’arrêts peuvent également être placés directement dans le programme désassemblé. Cela peut dans certains cas permettre une granularité plus fine, en analysant l’évolution du programme instruction par instruction.

Exercices

Utilisation d’une variable

On propose d’étudier l’évolution d’une variable. Pour cela, reprenez le code suivant:

int main(void) {
  /* USER CODE BEGIN 2 */
  uint32_t count = 0;
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1) {
    /* USER CODE END WHILE */
	  count = count + 1;
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}
Question 1

Analysez l’exécution du programme ci-dessus. Essayez de retrouver:

  • le code désassemblé correspondant à la création de la variable,
  • le code désassemblé correspondant à l’incrémentation de la variable,
  • le rôle de chacune des instructions de ces blocs de code désassemblé,
  • l’impact de ces blocs de code sur les registres internes du processeur.
Question 2 Ajoutez des points d’arrêts dans le code désassemblé au niveau des instructions effectuant l’incrément de la variable. Avancez l’exécution instruction par instruction. À chaque fois, visualisez la modification des registres internes et de la variable en mémoire. Les deux sont-elles effectuées en même temps ? Quelles instructions sont responsables de cela ?
Question 3 Déplacez votre variable count comme une variable globale située juste avant le main. Exécutez le programme jusqu’à ce que la variable ait été incrémentée. Dans la fenêtre Build Analyzer, retrouvez où est placée la variable. Pourquoi est-elle placée dans cette mémoire ?
Question 4 De la même manière, retrouvez la mémoire où est située la fonction main. Pourquoi est-elle placée dans cette mémoire ?

Utilisation d’une fonction

On propose d’étudier l’utilisation d’une fonction. Pour cela, reprenez le code suivant:

uint32_t add32 (uint32_t a, uint32_t b) {
  return a + b;
}

int main(void) {
  /* USER CODE BEGIN 2 */
  uint32_t count = 0;
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1) {
    /* USER CODE END WHILE */
	  count = add32(count, 1);
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}
Question 5 Retrouvez dans la mémoire où est située la fonction add32. Pourquoi est-elle placée dans cette mémoire ?
Question 6 À partir des valeurs des registres, analysez comment évolue le PC au cours de l’exécution.
Question 7 Analysez le désassemblé de l’appel de la fonction, de la fonction elle-même et du retour. Quelles instructions sont responsables du saut/ retour de fonction ?