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.
Installation
Dans un premier temps vous avez besoin d'installer plusieurs choses, si elles ne le sont pas déjà :
gcc-arm-none-eabi
, binutils-arm-none-eabi
, gdb-arm-none-eabi
: la toolchain gcc pour processeurs ARM (compilateur, débugger, etc.).
Sous Linux les paquets
gcc-arm-none-eabi
,
gdb-arm-none-eabi
et
binutils-arm-none-eabi
sont disponibles sous Ubuntu depuis la version 14.04 (trusty) et sous Debian depuis la version jessie. Pour les versions plus anciennes d'Ubuntu, ajoutez le
repository launchpad. Pour les autres versions, téléchargez la version tarball linux sur la
page launchpad et décompressez l'archive dans
/usr
:
tar xjvf gcc-arm-none-eabi-XXX-linux.tar.bz2 -C /usr --strip-components=1
Le paquet
gdb-arm-none-eabi
sur Ubuntu pose un problème d'installation : il y a un conflit avec
gdb
au niveau de la page de man. Si vous n'avez pas les droits pour installer le package, copiez simplement le fichier
arm-none-eabi-gdb
dans votre répertoire de travail.
Sous Mac, n'installez pas la version Macports. À ce jour (en 2014) la version Macports permet de compiler des programmes, mais il y a des soucis avec l'architecture. Téléchargez la version tarball linux sur la
page launchpad et décompressez l'archive dans
/usr
:
tar xjvf gcc-arm-none-eabi-XXX-mac.tar.bz2 -C /usr --strip-components=1
Sous Windows téléchargez l'installeur sur la
page launchpad et exécutez le.
openocd
: outil de programmation et de débuggage de micro-contrôleurs.
Sous Linux le paquet
openocd
est disponible sur Ubuntu depuis la version 14.04 (trusty) et sous Debian depuis la version squeeze. Pour les autres versions, téléchargez les
sources et compilez les à vos risques et périls.
Sous Mac, le paquet
openocd
est disponible avec Macports, homebrew et fink. À ce jour (en 2014), la version Macports pose des soucis de communication avec le stm32f4 discovery. Si vous cherchez les ennuis, essayez de compiler les
sources.
Sous Windows téléchargez l'installeur sur la
page de Freddie Chopin et exécutez le. Sinon, votre bravoure peut vous pousser à essayer de compiler les
sources.
uC-sdk
: le SDK que nous allons utiliser pour programmer la carte. Ce SDK vise à simplifier le codage pour microcontrôleurs en proposant une interface unique. Il est disponible sur github. Clonez le dépôt, ou récupérez la version archive.
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 :
SCK (PE2)
: l'horloge qui permet de synchroniser les actions entre le maître et l'esclave avec qui il communique. S'il y a plusieurs esclaves, ils partagent tous la même horloge.
SS (PE4)
: le slave select, qui permet au maître de dire à l'esclave qu'il s'adresse à lui. Dans ce cas le maître utilisera un GPIO pour chaque esclave et le destinataire sera sélectionné par software.
MISO (PE5)
multiple in single out : sortie. On envoie une séquence de bits vers un autre périphérique, alignée au signal d'horloge.
MOSI (PE6)
multiple out single in : entrée. On lit une séquence de bits venant de l'exclave, alignée au signal d'horloge.
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 :
STR
: strobe input. En gros il permet de bufferiser la valeur courante du registre. Ce buffer permet d'éviter que les sorties clignotent à chaque décalage. Nous relieront ce pin au 3V.
D
: data. Il s'agit des donnée que l'on reçoit. On le connecte donc au MISO
de notre bus.
CP
: clock input. C'est notre horloge, à connecter au SCK
de notre bus.
OE
: output enable. Ce pin doit être mis à 3V quald la valeur du registre est prête à être envoyée aux sorties. Avec le strobe, ils forment un double buffer.
QP0-7
: parallel output. Il s'agit des sorties parallèles. Nous en utiliserons donc 2 pour les lignes et 3 pour les colonnes.
QS1-2
: serial output. Ils servent à récupérer les valeurs perdues par le décalage, et donc à ajouter d'autres registres à décalage en sortie. Pour cela il faut relier une de ces sorties vers le pin D
du registre suivant.
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) :
Ligne | Colonne | b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 | hexa |
1 | 1 | 1 | 0 | 0 | 1 | 1 | X | X | X | 0x98 |
1 | 2 | 1 | 0 | 1 | 0 | 1 | X | X | X | 0xa8 |
1 | 3 | 1 | 0 | 1 | 1 | 0 | X | X | X | 0xb0 |
2 | 1 | 0 | 1 | 0 | 1 | 1 | X | X | X | 0x58 |
2 | 2 | 0 | 1 | 1 | 0 | 1 | X | X | X | 0x68 |
2 | 3 | 0 | 1 | 1 | 1 | 0 | X | X | X | 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