TP IHMZ : prototypage hardware
Dans ce TP vous allez réaliser des montages de base à l'aide d'une carte STM32F429J-DISCO. STM32F429 correspond au microcontrôleur de la carte, 32 pour 32 bits des mots mémoire, F4 pour la série à base d'ARM Cortex M4, doté d'un FPU (floating point unit).

L'ensemble des documentations est disponible ici.

Table des Matières

Installation

Dans un premier temps vous avez besoin d'installer plusieurs choses, si elles ne le sont pas déjà :

uC-sdk

Le SDK est en C, tout comme le code des programmes pour microcontrôleurs ST microelectronics et bien d'autres. Il inclut : Récupérez le
Makefile, le programme d'exemple et les documentations des divers composants avec lesquels on va travailler. Décompressez cette archive dans votre
home
et placez-y le répertoire contenant le sdk et en le renommant
uC-sdk
, ou créez un lien symbolique nommé uC-sdk vers le répertoire où vous avez mis le sdk. Par exemple :
ln -s ../uc-sdk uC-sdk
Regardez le
Makefile
, qui contient la configuration du SDK, la cible que l'on souhaite construire (
test.elf
). Si vous souhaitez renommer
test.c
, n'oubliez pas de modifier le
Makefile
. De même si vous voulez ajouter d'autres fichiers sources, ajoutez les cibles (
.o
) dans une nouvelle variable
TARGET_OBJS
.
Nous utiliserons 3 onglets de terminal. Le permier servira à compiler votre programme. Le deuxième servira à lancer
openocd
. Le troisième servira à débugguer avec
gdb
.

Compilation

Compilez votre programme en tapant tout simplement
make
.

openocd

Lancez
openocd
avec la commande :
openocd -f board/stm32f4discovery.cfg
Celui-ci va se connecter au ST-link intégré à la carte, ce qui nous permettra d'y charger notre programme, et de débugguer.

gdb

Lancez
gdb
avec la commande suivante, en remplaçant
test.elf
si besoin est par le nom de votre programme :
arm-none-eabi-gdb -ex "target extended-remote :3333" test.elf
Celui-ci va se connecter à openocd. Dans ce terminal nous pourrons saisir des commandes usuelles de gdb afin de contrôler le programme.Voici quelques commandes utiles :
monitor reset halt
arrêter l'exécution du programme et remettre la carte à 0
load
charger le programme sur la carte
continue
commencer/reprendre l'exécution du programme
break fichier:ligne
placer un point d'arrêt dans le fichier indiqué à la ligne indiquée
clear
supprimer le point d'arrêt à la ligne courante
delete x
supprimer le point d'arrêt x
step
exécuter la prochaine instruction
stepi
exécuter la prochaine insctruction assembleur
list
afficher le code source autour de la position courante
print x
afficher le contenu de la variable x

LED world

Alors que les premiers pas lorsque l'on apprend à programmer un ordinateur consiste à écrire un message dans une console, les micro-contrôleurs doivent être approchés avec plus de modestie. Commençons par allumer une simple LED. Pour allumer une simple LED il faut paramétrer une poignée de registres afin de configurer le GPIO relié à la LED. L'uC-SDK nous simplifie la tâche en proposant des procédures qui font presque tout le travail (
uC-sdk/hardware/include/gpio.h
).
void gpio_config(pin_t pin, pin_dir_t dir); //configurer un pin en entrée/sortie void gpio_set(pin_t pin, int enabled); //écrire 1 ou 0 sur un pin int gpio_get(pin_t pin); //lire la valeur d'un pin (1 ou 0)
Ainsi l'exemple fourni permet d'allumer une des LEDs de la carte, reliée au pin
PG13
:
#include <gpio.h> pin_t led = MAKE_PIN(GPIO_PORT_G, 13); int main() { gpio_config(led, pin_dir_write); gpio_set(led, 1); return 0; }

Ma première LED

Branchez une LED rouge sur le pin
PA5
et modifiez le code pour que le signal utilise le bon pin. N'oubliez pas d'ajouter une résistance en série avec la LED pour éviter la sur-tension. Rappelez-vous de la loi d'OHM :
U=RI
, que les tensions (
U
) s'additionnent en série et que les courants (
I
) sont les mêmes sur un même brin. La résistance interne de la LED et le courant idéal sont écrits dans sa documentation.

Des threads

On ne sait pas encore écrire dans un terminal, mais on sait déjà faire des threads. C'est comme ça, et c'est grâce à FreeRTOS. Si vous voulez utiliser les threads, commencez par inclure les fichier suivants :
#include <FreeRTOS.h> #include <task.h>
Le code exécuté par le thread doit utiliser une en-tête standard pour les threads :
static void moncode(void *p);
Créez ensuite la tâche qui va exécuter le thread. Le plus important est le premier paramètre qui correspond à la fonction que vous avez écrite à la phase précédente. Le nom est optionnel,
params
peut être
NULL
si vous n'avez pas de paramètre à passer à votre thread, et handle sert à manipuler la tâche plus tard,
NULL
est généralement suffisant.
xTaskCreate(moncode, (signed char *) "nom", configMINIMAL_STACK_SIZE, (void *)params, tskIDLE_PRIORITY, handle);
Quand vous avez créé les tâches pour tous vos threads, lancez le scheduler. Il va ordonnancer l'exécution des threads.
vTaskStartScheduler();
Nous pouvez mettre un thread en pause pendant un délai exprimé en millisecondes, et gérer des sections critiques avec des mutex.
vTaskDelay(delai); taskENTER_CRITICAL(); taskEXIT_CRITICAL();
Pour découvrir les autres fonctionnalités, explorez les fichiers de
uC-sdk/FreeRTOS/Source/include
pour en savoir plus.
Créez un thread qui fait clignoter la led chaque seconde.

Les timers et le PWM

Cette façon de faire clignoter une LED est simple, mais fait travailler le CPU. Or sur des microcontrôleurs, le CPU est une ressource précieuse. Les timers vont nous permettre d'obtenir le même effet, mais sans faire travailler le CPU.
Rappelez-vous que le travail d'un timer est d'incrémenter un registre et de déclencher des événements, par exemple lorsque le compteur atteint une valeur définie ou lorsque le compteur revient à 0. Nous utiliserons deux procédures d'initialisation.
timer_init
initialise le timer et le channel que nous allons utiliser.
prescale
est une constante qui va diviser la fréquence de fonctionnement du timer.
period
correspond à la valeur maximale du registre avant de revenir à 0.
timer_init_pwmchannel
configure le mode de fonctionnement du timer.
pin
représente le pin sur lequel le timer est branché.
pulse
correspond à la valeur qui va déclencher notre événement. Ainsi en utilisant cette configuration, le
pin
est à l'état haut jusqu'à ce que le compteur vaut
pulse
, puis est à l'état bas jusqu'à ce que le compteur vaut
period
. À ce moment là le cycle recommence. Ainsi on crée un signal PWM de rapport cyclique
pulse/period
et dont la fréquence dépend de la fréquence de fonctionnement du timer, du
prescale
et de
period
.
#include <timer.h> void timer_init( uint8_t timer, uint8_t channel, uint16_t prescale, uint32_t period); void timer_init_pwmchannel( uint8_t timer, uint8_t channel, pin_t pin, uint32_t pulse);
Branchez votre LED au pin PB4, qui est relié au Timer 3 - Channel 1. Initialisez un timer pour faire clignoter votre LED. Si votre LED ne clignote pas, soit sa fréquence est trop élevée et vous ne pouvez pas voir le clignotement, soit elle est trop lente et vous devrez attendre trop longtemps avant que le clignotement ait lieu. Configurez une fréquence suffisament élevée pour que le clignotement ne soit pas perceptible. Faites un effet glow avec la led en faisant varier le rapport cyclique dans une boucle.

Le transistor et le Vibreur

Nous avons vu en cours qu'un transistor ou un FET peut être utilisé comme interrupteur programmable, pour contrôler des circuits de puissance par exemple. C'est exactement ce dont on a besoin pour piloter un vibreur, qui a besoin plus de courant que nos GPIOs ne peuvent fournir. Réalisez le montage ci-dessous, et envoyez un signal PWM de 250Hz sur le pin
PB4
. Faites varier le rapport cyclique, et observez les résultats.

Input

Jusque là nous nous sommes concentrés sur l'output. Nous allons explorer maintenant l'input. Construisez un circuit avec un bouton poussoir relié au pin PC3. Configurez-le entrée avec
gpio_config
. Écrivez ensuite un code qui va allumer une LED quand le bouton a été pressé, et qui l'éteint quand il a été pressé à nouveau. Utilisez la procédure
gpio_get
pour récupérer la valeur.

Le jeu du duel

Le jeu du duel est simple. Chaque joueur possède un bouton et une LED. Une troisème LED centrale s'allume à un instant aléatoire. Le premier joueur qui appuie sur son bouton après que la 3e LED soit allumée a gagné. Le vainqueur est signalé par sa LED allumée. Si un joueur appuie sur le bouton avant que la 3e LED est allumée il a perdu.
Implémentez ce jeu avec 3 LEDs et 2 boutons poussoirs.

Le jeu du Simon

Le jeu du Simon est un jeu de mémoire. Le joueur doit observer et mémoriser une série de lumières de couleurs qui s'allume. À la fin de la série il doit rejouer la série avec des boutons associés à chaque lumière. Si le joueur se trompe dans la série, il doit recommencer. Si il réussit à jouer la séquence dans le bon ordre, la séquence suivante ajoute une lumière à la précédente.
Implémentez le jeu du Simon avec 2 LEDs, une rouge et une verte, et deux boutons.

ADC : analog world

Comme vous l'aurez remarqué, on ne peut que lire des valeurs binaires (numériques) avec
gpio_get
. Cependant de nombreux capteurs permettent de recueillir des informations analogiques : capteurs de force, de lumière, joysticks. Nous allons lire ces informations avec un convertisseur Analogique-Numérique (ADC). Nous en utiliserons deux : le pin PA5 (ADC 1 channel 5) et PC3 (ADC 1 channel 3). Premièrement nous devons initialiser l'ensemble des ADC :
#include <adc.h> void adc_config_all();

Lire un capteur

Ensuite nous pouvons initialiser chaque ADC au besoin. Pour notre premier programme nous allons mesurer la lumière avec une photorésistance, ou avec un capteur de force. Réalisez le montage correspondant basé sur un diviseur de tension, avec notre pin ADC mesurant la tension de sortie et le capteur l'une des deux résistances du diviseur de tension.
Les fonctions suivantes permettent de configurer un channel ADC, et de récupérer la valeur. Les ADC des STM32F4 échantillonnent sur 12 bits, les valeurs sont donc entre 0 et 4095.
void adc_config_single(uint8_t adc, uint8_t channel, pin_t pin); uint16_t adc_get(uint8_t adc);

Lire plusieurs capteurs en continu

Tout comme nos premiers exemples avec les LEDS, nous souhaitons économiser le temps CPU. Tout comme beaucoup de composants des microcontôleurs modernes, les ADC peuvent fonctionner avec un DMA. Les DMA permettent de transférer directement des valeurs entre des périphériques et la mémoire sans passer par des instructions CPU. Dans notre cas nous pouvons demander à l'ADC de convertir la valeur en continu, et de la ranger dans une variable.
Réalisez un montage avec les joysticks afin de récupérer la position en
x
sur un channel ADC et
y
sur un autre. La variable
channel
est un tableau contenant les deux channels,
pin
un tableau qui contient les deux pins,
dest
est un tableau de dimension 2 dans lequel l'ADC rangera les valeurs et
nb
vaudra 2 car nous récupérons 2 valeurs de 2 channels. Faites varier la luminosité de 2 LED, chacune indiquant la valeur d'un des axes.
void adc_config_continuous(uint8_t adc, uint8_t *channel, pin_t *pin, uint16_t *dest, uint8_t nb);

SPI et registre à décalage

Si nous voulons n'utiliser que quelques sorties, la carte possède suffisament de GPIO pour les traiter chacune séparément. Cependant d'une part ça complexifie le circuit, et d'autre part si nous voulons utiliser plus de sorties que nous n'avons de GPIO nous pouvons avoir des soucis. Pour simplifier ce genre de choses nous allons utiliser un bus. Les bus permettent d'envoyer une série d'informations à un ou plusieurs destinataires. Chaque bus a ses spécificités. Nous allons utiliser le plus simple et le plus rapide : le bus SPI. Le bus SPI est un bus bidirectionnel qui permet d'écrire et de lire en même temps. Nous utiliserons le bus
SPI4
de la carte. Il utilise 4 fils : Les fonctions suivantes permettent de communiquer à l'aide d'un bus SPI :
#include <spi.h> void spi_init(uint8_t id); void spi_read(uint8_t id, uint8_t *buffer, uint8_t nb); void spi_write(uint8_t id, uint8_t *buffer, uint8_t nb);

Matrice de leds

Nous allons utiliser le bus SPI pour afficher une matrice de
2 x 3
LEDs. Nous pourrions utiliser 6 GPIO, mais nous voulons juste montrer le principe. L'idée est qu'il faut
a + b
sorties parallèles au lieu de
a x b
GPIO. Par exemple pour une matrice de
10 x 10
LEDs, nous aurions besoin de 100 GPIO, sans compter le code pour les gérer. Pour convertir le signal série en commandes parallèles nous allons utiliser un registre à décalage.
Il existe plusieurs types de registres à décalage, permettant de manipuler des signaux en série et en parallèle. Comme son nom l'indique, ce composant possède un registre dont il décale les valeurs à chaque période du signal d'horloge. Ainsi le chargement d'une valeur de 8 bits consiste à décaler 8 fois le registre. Le registre que nous allons utiliser possède 8 sorties parallèles et deux sorties série. Les sorties séries permettent de récupérer la valeur perdue lors du décalage, et donc d'ajouter d'autres registres à décalage et ainsi augmenter le nombre de sorties. En plus de l'alimentation (VDD=3V, VSS=GND), notre registre à décalage possède utilise ces PINs : Vous aurez remarqué que nous n'utilisons pas les fils
MOSI
et
SS
du bus. La raison est simple : nous ne lisons pas de valeurs, et nous n'avons qu'un seul esclave. Notre esclave est le registre à décalage, qui lit toujours nos données. Ce circuit de multiplexage permet de choisir la LED à allumer simplement. Il faut mettre le signal de sa ligne à 3V et le signal de sa colonne à 0V. Cette différence de potentiel la fera briller. Les autres lignes doivent être à 0V et les autres colonnes à 3V. Ainsi les leds de la même ligne auront leurs deux bornes reliées à 3V, celles de la même colonne auront leurs deux bornes à 0V, et les autres auront leur polarité inversée. Si vous reliez les lignes aux sorties 0 et 1, et les colonnes aux sorties 2, 3 et 4, voici les valeurs à envoyer au bus pour allumer chaque led (X étant 0 ou 1 : on n'utilise pas les 3 dernières sorties) :
LigneColonneb0b1b2b3b4b5b6b7hexa
11 10 011XXX 0x98
12 10 101XXX 0xa8
13 10 110XXX 0xb0
21 01 011XXX 0x58
22 01 101XXX 0x68
23 01 110XXX 0x70
Créez un tableau d'
uint8_t
contenant le code associé à chaque LED. Quelle est la commande pour allumer la LED
x
, avec
0 ≤ x ≤ 5
?

Afficher un motif

L'intérêt d'avoir une matrice est d'allumer plusieurs LEDs à la fois. Pour ce faire il suffit d'allumer les LED qui nous intéressent à la suite, très rapidement. Un délai d'1 ou 2ms entre chaque LED permet de voir la LED s'allumer, sans discerner le passage d'une LED à l'autre. Nous allons écrire une fonction qui affiche un motif. Pour décrire ce motif, utilisez un
uint8_t
, dont chaque bit représente une led.

Afficher une animation

Pour aller plus loin, nous pouvons animer l'affichage. Pour cela il suffit de lire une séquence de motifs. Définissez une tableau d'
uint8_t
définissant une ligne verticale qui fait des allers/retours de droite à gauche. Écrivez ensuite une fonction qui permet de jouer une telle animation. Vous pouvez ralentir l'animation en affichant plusieurs fois de suite le même motif.

USB HID : une petite manette de jeu