Utiliser l’écran tactile STM32F769I-DISCO

Utiliser l’écran tactile STM32F769I-DISCO

Ecran tactile sur le petit robot

Grâce à STMicroelectronics, nous avons pu obtenir un joli petit kit 32F769I-DISCOVERY composé d’un écran tactile et de différents périphériques permettant des applications diverses et variées.

Néanmoins, la documentation sur ce kit est très peu fournie, et l’intégrer dans nos robots s’est avéré fastidieux. Voici donc un petit guide qui explique notamment comment créer une interface graphique personnalisée, interagir avec les leds, créer une liaison série pour communiquer avec les autres cartes du robot et aussi comment mesurer le temps.

Sommaire

Ce dont vous avez besoin

Premièrement, le fameux kit 32F769I-DISCOVERY que vous pouvez retrouver ici.

Écran avec l’interface graphique de l’année dernière

Il vous faudra également un éditeur de texte sympa, avec la possibilité de faire des recherches de mots dans plusieurs fichiers, ou éventuellement un IDE comme TrueStudio. Pour ici, j’utiliserai SublimeText.

Et enfin, l’IDE qui vous permettra de créer l’interface graphique et de compiler le code. Il y a plusieurs solutions, mais la plus pratique semble être TouchGFX, un logiciel développé par STMicroelectronics.

TouchGFX : prise en main et fonctionnement

Installation

Vous pouvez télécharger le logiciel ici.

L’installation peut être problématique sur certains ordinateurs (il semble que ce soit dû au format msi du fichier d’installation). Pour cela, convertissez le fichier msi en exe avec un logiciel comme celui-ci.

Attention, à partir de la version 4.13 il faudra aussi installer STM32CubeProg.
Par ailleurs, il y a eu des modifications dans les codes sources avec les nouvelles versions de TouchGFX, et celles-ci semblent poser quelques problèmes. Vous pouvez télécharger ici une application vierge pour bien démarrer.

Interface du logiciel

Lorsque vous lancez TouchGFX, voici ce qui s’ouvre.

Première ouverture de TouchGFX

Pour pouvoir créer un projet utilisable pour l’écran que nous avons, il faut penser à sélectionner la bonne plateforme et à donner un nom au projet.

Sélection de la bonne plateforme

On va créer une application au nom original de Application.

Attention : TouchGFX n’aime pas les accents et les espaces dans les chemins des fichiers, donc si vous vous appelez Chloé par exemple, attention aux noms des dossiers.

Une fois cela fait, l’interface pour désigner la partie graphique de l’application s’ouvrira.

Fenêtre où l’on créait l’interface graphique de l’écran

Le panneau de gauche permet de sélectionner les éléments à ajouter à l’interface et celui de droite offre toutes les options de personnalisation pour l’élément sélectionné.

La barre du haut permet de choisir soit le mode Canvas, pour revenir à cette interface, soit le mode Texts que nous verrons plus tard (à gauche) ; et à droite se trouvent les boutons permettant de simuler l’interface ou de compiler le code.

Le bouton Run Simulator permet de simuler l’interface graphique de l’écran sur l’ordinateur. Le bouton Generate Code créera l’ensemble des fichiers sources qui permettront de réaliser l’interface graphique créée. Et le bouton Run Target compilera le code et le flashera sur la carte écran.

Attention, TouchGFX écrasera les modifications faites en dehors des endroits « user code » (lorsque ces endroits existent) dans les fichiers sources. Si il est précisé de ne pas modifier le fichier dans l’en-tête, ça ne sert à rien de le faire.

Enfin dans la barre du bas, l’avancée de la compilation sur la gauche, et sur la droite un accès aux logs et un autre pour trouver les fichiers sources.

Plus en détails, l’onglet 1 présente les éléments disponibles à ajouter à l’interface, et l’onglet 2 permet d’ajouter des images que vous pourrez utiliser par la suite.

L’onglet 3 présente les options de personnalisation d’un élément, et l’onglet 4 offre la possibilité de créer des interactions avec les éléments que nous détaillerons plus tard.

Enfin, l’onglet 5 permet de gérer les différents écrans de l’application, et l’onglet 6 de créer des conteneurs d’éléments sur un écran.

Architecture du code source

Modèle utilisé par TouchGFX pour créer le code source de l’interface graphique

L’application générée par TouchGFX est basée sur une architecture MVP (Model-View-Presenter). Le principe étant de décomposer l’application en plusieurs écrans différents. Dans chaque écran, une partie Presenter et une partie View gèrent respectivement le traitement des informations en provenance de l’utilisateur et de l’application elle-même ; et l’affichage graphique.

Commune à toute l’application, une partie Model permet de faire l’interface entre la partie affichage et traitement en arrière-plan (la gestion du temps par exemple).

Ainsi, pour chaque écran créé, 4 fichiers associés seront générés :

  • Un fichier NomDeLEcranPresenter.cpp ;
  • Un fichier NomDeLEcranView.cpp ;
  • Deux fichiers headers .hpp respectivement associés aux deux fichiers précédents.

Dossiers créés

La répartition des dossiers sépare initialisation des périphériques liés au microcontrôleur, description de l’interface graphique, et interactions avec celle-ci.

Répartitions des éléments dans les dossiers

Premier exemple : interactions avec le hardware, les leds du kit et l’écran

Un exemple simple pour commencer à interagir avec le hardware. En effet, il existe de nombreux exemples pour montrer comment afficher des choses sur l’écran, mais quasiment aucun qui explique comment interagir avec l’environnement ce qui, je pense, reste le plus pertinent.

Face avant du kit

Les LEDs qu’on peut contrôler sont les 3 premières de gauche dans le groupe de LEDs entourées sur la photo ci-dessus. Les autres sont des témoins d’alimentation.

Dans l’application que nous allons créer, nous aurons un écran de démarrage appelé « Accueil » qui contiendra un bouton pour interagir avec une led.

Interface graphique pour le bouton

On y ajoutera un fond, avec l’objet Image en choisissant le style Main Bg 800x400px, un objet Text Area servant d’en-tête et un objet Button With Label auquel on donnera comme « name » : bouton_envoyer.

Écran d’accueil créé

Pour permettre une action lors de l’appui sur le bouton, nous allons implémenter une interaction (clic sur l’onglet de droite dans le panneau de droite) qui appellera une fonction Allumer_ou_eteindre_led() dans laquelle sera contenu le code pour changer l’état de la led.

On ajoute une interaction à l’aide du panneau de droite

Code pour interagir avec la led

À l’aide de Sublime Text, on ouvre le dossier correspondant au dossier créé par TouchGFX et contenant notre projet. Dans notre cas, le dossier s’appellera Application.

La fonction créée à partir de l’interface graphique apparaîtra dans le code généré par TouchGFX.

Le fichier ne doit pas être modifié, mais il nous explique comment utiliser la fonction que nous avons créée à l’aide de l’interaction ci-dessus

Comme nous l’indiquent les commentaires, il faut réimplémenter cette fonction dans les fichiers AccueilView.hpp et AccueilView.cpp .

Il faut toujours penser à mettre le prototype de la fonction que nous allons implémenter dans le .hpp correspondant, sinon une erreur de compilation apparaîtra

Attention ! Tout code ajouté pour fonctionner avec le hardware de l’écran doit être encadré par #ifndef SIMULATOR et #endif. Sinon, des erreurs de compilation apparaîtront. En effet, TouchGFX compile à la fois pour une solution simulable sur l’ordinateur (et n’interagissant pas avec le hardware de la carte) et une version implémentable sur la carte.

De plus, afin de pouvoir utiliser les fonctions permettant d’interagir avec le hardware, il faut ajouter dans l’en-tête le code suivant (ce header contient grosso-modo les prototypes des fonctions utiles) :

#ifndef SIMULATOR
  #include "stm32f769i_discovery.h"
#endif
On implémente enfin la fonction dans le .cpp correspondant à la partie « Vue » de l’écran d’accueil

Le but étant qu’à chaque appui sur le bouton, la led s’allume ou s’éteigne (cela dépendra de son état initial). On utilisera donc une fonction déjà existante BSP_LED_Toggle qui a ce comportement.

Il conviendra d’initialiser l’utilisation de la led avec le fonction BSP_LED_Init qui sera appelée dans la fonction AccueilView::setupScreen (fonction appelée à chaque affichage de l’écran « Accueil »).

Une fois cela fait, branchez un câble USB sur le connecteur ST-Link du kit, et appuyez sur Run Target 🙂

Créer une liaison série

Afin de communiquer avec les autres cartes, nous allons créer ici une liaison série sur l’USART6. On choisit cette liaison en particulier car les pins TX/RX sont situés sur les connecteurs Arduino à l’arrière de la carte, et sont donc facilement accessibles.

Représentation schématique des connecteurs Arduino à l’arrière de la carte
Vue arrière du kit

Afin de ne pas rendre bloquantes les communications passant par la liaison série, nous allons utiliser les fonctions de callback pour travailler en parallèle des autres tâches.

Nous lirons les caractères les uns après les autres. Donc la fonction de callback sera automatiquement appelée dès qu’un caractère sera disponible sur la liaison série. On utilisera pour cela un buffer d’une taille choisie dans lequel on ajoutera chaque caractère reçu au fur et à mesure.

Une variable index sera incrémentée à chaque appel de la fonction de callback. On saura donc à chaque instant le nombre de caractères stockés dans le buffer. Enfin, un caractère de fin sera défini et entraînera la remise à zéro du buffer s’il est reçu.

Ainsi, la lecture des données reçues sur la liaison série suivra le principe suivant :

Fonction RX Callback sur UART
Si c'est bien l'USART6 qui a appelé la fonction de Callback
| Si l'index est nul
| ...Alors on initialise le buffer à une valeur par défaut
| Si on reçoit un autre caractère que le caractère de fin et si on ne dépasse pas la taille du buffer
| ...Alors on incrémente l'index du buffer et on ajoute le caractère au buffer
| Sinon on traite le buffer et on réinitialise l'index
Fin fonction

Paramétrer la liaison série

  • Dans Core/Src/main.cpp
    Créer la variable correspondante à la liaison (après UART_HandleTypeDef huart1; )
    UART_HandleTypeDef huart6;
    Fonction d’initialisation de la liaison USART6 (après static void MX_USART1_UART_Init(void){…} )
static void MX_USART6_UART_Init(void)
{
    huart6.Instance = USART6;
    huart6.Init.BaudRate = 115200;
    huart6.Init.WordLength = UART_WORDLENGTH_8B;
    huart6.Init.StopBits = UART_STOPBITS_1;
    huart6.Init.Parity = UART_PARITY_NONE;
    huart6.Init.Mode = UART_MODE_TX_RX;
    huart6.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    huart6.Init.OverSampling = UART_OVERSAMPLING_16;
    huart6.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
    huart6.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
    if (HAL_UART_Init(&huart6) != HAL_OK)
    {
        Error_Handler();
    }
}

Initialiser la liaison USART6 (après /* USER CODE BEGIN 2 */)
MX_USART6_UART_Init();
Et on oublie pas d’ajouter son prototype (après static void MX_USART1_UART_Init(void); ) :
static void MX_USART6_UART_Init(void);

  • Dans Core/Src/stm32f7xx_hal_msp.c
    Paramétrer et activer les interruptions matérielles sur les pins de la liaison USART6 (après if(huart->Instance==USART1){…} dans la fonction HAL_UART_MspInit )
if(huart->Instance==USART6)
{
    HAL_NVIC_SetPriority(USART6_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(USART6_IRQn);

    __HAL_RCC_USART6_CLK_ENABLE();
    __HAL_RCC_GPIOC_CLK_ENABLE();

    GPIO_InitStruct.Pin = ARD_D0_RX_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF8_USART6;
    HAL_GPIO_Init(ARD_D0_RX_GPIO_Port, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = ARDUINO_TX_D1_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    GPIO_InitStruct.Alternate = GPIO_AF8_USART6;
    HAL_GPIO_Init(ARDUINO_TX_D1_GPIO_Port, &GPIO_InitStruct);
}
  • Désactiver l’interruption matérielle (entre autres) dans la fonction de désallocation (après if(huart->Instance==USART1){…} dans la fonction HAL_UART_MspDeInit )
if(huart->Instance==USART6)
  {
    __HAL_RCC_USART6_CLK_DISABLE();
    HAL_GPIO_DeInit(GPIOC, ARD_D0_RX_Pin|ARDUINO_TX_D1_Pin);
    HAL_NVIC_DisableIRQ(USART6_IRQn);
  }
  • Dans Core/Src/stm32f7xx_it.c
    Variable utile à la liaison (après /* USER CODE BEGIN EV */ )
    extern UART_HandleTypeDef huart6;
    Fonction pour gérer l’interruption (après /* USER CODE BEGIN 1 */ )
    void USART6_IRQHandler(void)
    | {
    | HAL_UART_IRQHandler(&huart6);
    | }

Améliorer l’écran d’accueil

Pour nous rendre sur un autre écran, nous ajouterons une interaction associée à un bouton qui permettra de changer d’écran.

Pour revenir à l’écran d’accueil, nous pourrons utiliser le même principe. C’est l’icône de la maison sur les écrans suivants qui utilise une interaction de changement vers l’écran d’accueil.

Envoyer des données

Nous allons utiliser un bouton sur l’écran. Chaque appui sur celui-ci enverra des données sur la liaison série. Pour cela, il suffit d’ajouter une interaction appelant une fonction virtuelle à chaque appui sur le bouton.

Nouvel écran « Communiquer » accessible depuis l’écran « Accueil »

On commence par inclure les fichiers nécessaires au début de TouchGFX/gui/src/communiquer_screen/CommuniquerView.cpp :

#ifndef SIMULATOR
	#include "stm32f7xx_hal.h"
	#include "stm32f769i_discovery.h"

	extern UART_HandleTypeDef huart6;
#endif 

Puis on implémente la fonction pour envoyer dans TouchGFX/gui/src/communiquer_screen/CommuniquerView.cpp sans oublier le prototype correspondant dans TouchGFX/gui/include/communiquer_screen/CommuniquerView.hpp.

void CommuniquerView::envoyer_sur_serie()
{
	#ifndef SIMULATOR
	char message[] = "Test_envoi\n";
	HAL_UART_Transmit(&huart6, (uint8_t*) message ,12, 0xFFF);
	#endif
}

Recevoir des données…

Il convient tout d’abord de créer les variables nécessaires au bon fonctionnement de la fonction RX callback. Pour cela, on les définit dans Core/Src/main.cpp (après /* USER CODE BEGIN PV */ ) :

uint8_t rx_index = 0;
uint8_t rx_data;
uint8_t rx_buffer[32];

Dans Core/Src/main.cpp ; lancer la première lecture (après MX_USART6_UART_Init(); ) :

__HAL_UART_ENABLE_IT(&huart6, UART_IT_RXNE);
HAL_UART_Receive_IT(&huart6, &rx_data, 1);

Le booléen finDeTransmission est un flag permettant de savoir quand le buffer est plein. On ajoute en début du fichier TouchGFX/gui/src/communiquer_screen/CommuniquerView.cpp :

#ifndef SIMULATOR
	#include "stm32f7xx_hal.h"
	#include "stm32f769i_discovery.h"
   
	bool finDeTransmission = false;

	extern UART_HandleTypeDef huart6;
	extern uint8_t rx_index;
	extern uint8_t rx_data;
	extern uint8_t rx_buffer[32];
#endif

Cela inclut les fichiers headers contenant les prototypes utiles, et les variables utilisées dans le code.

On traduit le pseudo-code de la fonction de callback décrite plus haut, et on l’implémente dans TouchGFX/gui/src/communiquer_screen/CommuniquerView.cpp :

#ifndef SIMULATOR
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart->Instance == USART6)
  {
    if(rx_index == 0)
    {
      for(int i = 0; i<32; i++)
      {
        rx_buffer[i] = 0;
      }
    }
    if (rx_data != (uint8_t)'\n')
    {
      rx_buffer[rx_index] = rx_data;
      rx_index++;
      if (rx_index>=31)
      {
        rx_index = 0;
        finDeTransmission = true;
      }
    }
    else
    {
      rx_index = 0;
      finDeTransmission = true;
    }
    HAL_UART_Receive_IT(&huart6, &rx_data,1);
  }
}
#endif

Notre fichier TouchGFX/gui/src/communiquer_screen/CommuniquerView.cpp ressemble donc à celà :

…et mettre à jour dynamiquement les informations à l’écran

Et oui, une fois les données reçues, il serait bien de pouvoir les afficher tout de même ! Pour cela, nous allons modifier un champ de texte sur l’écran en le remplissant avec la chaîne de caractères reçus.

Pour cela, nous allons créer une ressource, qui correspond globalement à une variable dont on peut afficher le contenu sur l’interface graphique.

On nomme cette ressource caracteresRecus et on lui donne comme valeur : Caractères reçus : <v> . Ainsi, c’est seulement la balise <v> qui sera modifiée lorsque l’on y accèdera dans le code.

Création de la « ressource » qui sera modifiée par le programme pour afficher les caractères reçus

Néanmoins, pour des soucis d’optimisation, tous les caractères d’une police de texte ne sont par chargés dans le code généré par TouchGFX. Cela peut amener à ne pas pouvoir écrire certains caractères reçus. Pour cela, on va forcer TouchGFX à inclure l’ensemble des caractères utiles de la table ASCII (numéros 32 à 127 en décimal).

Il faut dire à TouchGFX d’inclure la plage de caractères usuels pour qu’il les compile, sinon il pourrait ne pas reconnaître les caractères reçus qu’on essaye d’afficher

Une fois la ressource définie, on peut l’utiliser en créant un champ de texte spécifiant l’utilisation de celle-ci. On peut également modifier le wildcard (valeur joker) pour afficher un texte par défaut (avant que l’on le modifie avec les données reçues par exemple).

La valeur par défaut est définie à l’aide des paramètres de l’objet « Text Area » (clic sur l’objet et panneau de droite)

Afin de pouvoir modifier le contenu affiché à l’écran avec les informations provenant du back-end, nous allons passer par les parties Presenter et Model de l’architecture de l’application (voir la partie architecture plus haut).

En clair, nous avons une fonction de réception autonome qui remplit un buffer et lève un flag une fois celui-ci remplit ou une fois qu’on a reçu un caractère de fin de transmission (le \n pour nous). Nous allons donc vérifier régulièrement grâce à la fonction Tick() présente dans le Model si le flag est levé ou non.

La fonction tick() est déjà implémentée dans TouchGFX/gui/src/model/Model.cpp. Cette fonction est appelée à chaque affichage d’une nouvelle frame à l’écran. On tourne autour des 60Hz, donc cette fonction sera appelée approximativement 60 fois par seconde.

La fonction tick permettra de vérifier régulièrement si on a reçu une trame complète à afficher

Il faudra donc créer une méthode Actualisation_P() dans la classe ModelListener que nous réimplémenterons dans la classe CommuniquerPresenter qui hérite de ModelListener. Cette méthode permet d’éviter de devoir réimplémenter cette méthode dans chaque Presenter de chaque écran, mais uniquement dans celui qui s’en sert (attention à bien mettre le « virtual »).

Plus concrètement, on insère la ligne suivante dans TouchGFX/gui/include/gui/model/ModelListener.hpp (juste avant protected) :

virtual void Actualisation_P(){};

Si le flag est levé, alors on transmettra l’information à la partie Presenter par le biais d’une méthode Actualisation_P(). Le Presenter permettra alors d’appeler une fonction Actualiser() dans la partie View.

Le Presenter fait la liaison entre le Model (les ticks ici) et la View (l’affichage)

Finalement la fonction Actualiser qui modifie une variable caracteresRecusBuffer créée automatiquement par TouchGFX (qui utilise le nom de la ressource que nous avons créée en rajoutant « Buffer » à la fin).

La fonction « Actualiser » de la partie View permet de mettre à jour le champ de texte avec les données reçues dans le buffer

Comme à chaque fois, il faudra penser à déclarer les prototypes dans TouchGFX/gui/src/communiquer_screen/CommuniquerPresenter.hpp et dans TouchGFX/gui/src/communiquer_screen/CommuniquerView.hpp.

Compter le temps

Je vais présenter ici une solution simple à mettre en œuvre qui ne nécessite pas de créer des timers ou autre. Mais si vous trouvez une meilleure méthode, je suis preneur 😉

Nous allons donc utiliser la fonction tick() déjà implémentée dans TouchGFX/gui/src/model/Model.cpp. Cette fonction est appelée à chaque affichage d’une nouvelle frame à l’écran. On tourne autour des 60Hz, donc cette fonction sera appelée approximativement 60 fois par seconde.

On va commencer par créer un nouvel écran que l’on appellera « Temps ». On ajoutera une interaction sur le bouton supérieur pour lancer le compteur. Cela se fera par le biais d’une fonction demarrerCompteur().

Écran « Temps » avec le bouton supérieur pour lancer le décompteur

Le champs de texte où sera affiché le temps utilisera une ressource appelée tempsEnCours :

Ressource qui sera utilisée pour afficher le décompteur

Le principe est d’afficher le temps restant du match en cours. Pour cela, nous allons décompter les secondes grâce à la fonction tick() implémentée dans gui/src/model/Model.cpp. On ajoute le code suivant dans cette fonction :

if(compterTemps)
{
    static int n = 0;
    n++;
    if(n>=65)
    {
        modelListener->PactualiserTemps();
        n=0;
    }
}

Le nombre 65 est trouvé expérimentalement, il peut varier donc n’hésitez pas à l’ajuster pour qu’il corresponde à la durée dont vous avez besoin. L’utilisation d’un timer sera bien sûr toujours plus précise et la méthode des ticks n’est pas sûre pour limiter le temps d’un match à la coupe.

La variable compterTemps, qui permet d’activer ou pas le décompte (si un match est en cours par exemple), doit être signalée au compilateur en début du fichier gui/src/model/Model.cpp :

extern bool compterTemps;

Voici donc à quoi ressemble maintenant le fichier gui/src/model/Model.cpp :

La fonction ActualisationTemps_P(), quant à elle, est le lien entre le Presenter et la View. On la retrouve dans les différents fichiers suivants.

Dans TouchGFX/gui/include/gui/model/ModelListener.hpp :

Dans TouchGFX/gui/include/gui/temps_screen/TempsPresenter.hpp :

Dans TouchGFX/gui/src/temps_screen/TempsPresenter.cpp :

On définit une fonction Actualiser() dans TouchGFX/gui/src/temps_screen/TempsView.cpp :

void TempsView::Actualiser()
{
    #ifndef SIMULATOR
        if(compterTemps)
        {
            Unicode::snprintf(tempsEnCoursBuffer, 4, "%d", tempsRestant);
            tempsEnCours.invalidate();
            if(tempsRestant>=1)
            {
                tempsRestant--;
            }
        }
    #endif

Les variables utiles à cette fonction doivent être ajoutées dans l’en-tête. Et elles devront être référencées dans tous les en-têtes les utilisant (avec le mot extern) :

bool compterTemps = false;

int tempsRestant = 99;

Il faudra aussi définir son prototype dans TouchGFX/gui/include/gui/temps_screen/TempsView.hpp.

Pour finir, on implémente la fonction qui permet de lancer le compteur dans TouchGFX/gui/src/temps_screen/TempsView.cpp :

void TempsView::demarrerCompteur()
{
    #ifndef SIMULATOR
        compterTemps = true;
    #endif
}

Et comme d’habitude le prototype doit être écrit dans TouchGFX/gui/include/gui/temps_screen/TempsView.hpp .

Au final, le fichier TouchGFX/gui/src/temps_screen/TempsView.cpp ressemble à cela :

Et voilà !

Téléchargez le code source ici 🙂

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *