Vous êtes sur la page 1sur 122

LE MICROCONTRÔLEUR ARM-M3

PAR BIGONOFF

Programmation par la pratique – Révision 1

Démarrer avec un LPC1768 sur une carte de


développement

1
2
Table des matières
1. Présentation .......................................................................................................................... 5
1.1. Contenu du document...................................................................................................... 5
1.2. Pourquoi ce tutoriel ? ...................................................................................................... 5
1.3. Pré requis ......................................................................................................................... 5
1.4. Présentation de la LandTiger® ........................................................................................ 5
1.5. Avertissement .................................................................................................................. 7
1.6. Modification hardware importante .................................................................................. 8
2. La programmation ............................................................................................................. 11
2.1. Les outils utilisés ........................................................................................................... 11
2.2. Réalisation d’un nouveau projet .................................................................................... 11
2.2.1. Création de la structure de répertoires ...............................................................................12
2.2.2. Création du projet µVision® ................................................................................................12
2.2.3. Création des groupes ..........................................................................................................14
2.2.4. Ajout des fichiers à compiler ...............................................................................................14
2.2.5. Paramètres de compilation.................................................................................................16
2.2.6. Première compilation .........................................................................................................19
3. Le programme .................................................................................................................... 23
3.1. Led clignotante par temporisation software .................................................................. 23
3.2. Chargement d’un programme dans l’ARM ................................................................... 39
3.3. Accès simultané à l’écran GLCD .................................................................................. 43
3.4. L’activation software des Leds...................................................................................... 50
3.5. Comment charger un fichier exécutable dans l’ARM ................................................... 56
3.5.1. Utilisation de l’IDE ..............................................................................................................56
3.5.2. Utilisation de J-Flash® .........................................................................................................56
3.5.3. Utilisation de FlashMagic® .................................................................................................56
3.5.4. Utilisation d’une sonde J-Tag classique ..............................................................................57
3.5.5. Le côté légal ........................................................................................................................57
3.5.6. Parenthèse politique ...........................................................................................................58
4. Le débogage sur circuit ...................................................................................................... 61
4.1. Introduction ................................................................................................................... 61
4.2. Le paramétrage .............................................................................................................. 61

3
4.3. L’entrée en mode débogage .......................................................................................... 64
4.4. Les fenêtres ................................................................................................................... 65
4.5. Un mot sur les registres ................................................................................................. 68
4.6. Le débogage du programme .......................................................................................... 69
5. Parlons interruptions ......................................................................................................... 77
5.1. Les exceptions ............................................................................................................... 77
5.2. Le fichier startup_LPC17xx.s ....................................................................................... 78
5.3. Mise en service des interruptions IRQ .......................................................................... 81
5.4. Traitement de l’interruption .......................................................................................... 82
5.5. Les validations générales .............................................................................................. 83
5.6. Priorités des interruptions.............................................................................................. 84
5.7. Récapitulatif des commandes NVIC ............................................................................. 86
5.8. Interruptions et Timers par la pratique .......................................................................... 87
6. Compléments et astuces ................................................................................................... 109
6.1. Le code-protect ............................................................................................................ 109
6.2. Jouons avec les tailles ................................................................................................. 109
6.3. Les alignements ........................................................................................................... 112
6.4. Le mot clé « register » ................................................................................................. 116
6.5. Opérations supplémentaires sur les données ............................................................... 116
6.6. Compilation avec zones réservées ............................................................................... 117
Notes : ................................................................................................................................ 120
7. Informations légales ......................................................................................................... 121
7.1. Licence et droits .......................................................................................................... 121
7.2. Propriété intellectuelle................................................................................................. 122
7.3. Historique .................................................................................................................... 122

4
1. Présentation

1.1. Contenu du document

Le présent manuel vous décrit de façon simple comment démarrer un projet personnel sur
une carte LandTiger® et comment déboguer ce projet en utilisant la sonde J-Tag intégrée à la
carte. Il ne s’agit pas d’expliquer l’intégralité de l’ARM LPC1768, ça demanderait un cours de
plusieurs centaines de pages. Il s’agit plutôt de vous donner les pistes pour démarrer par vous-
même sur base d’un exemple concret. J’en profiterai au passage pour vous donner quelques
astuces et renseignements pas évidents à trouver lorsqu’on débute.

1.2. Pourquoi ce tutoriel ?

J’ai décidé d’écrire ce tutoriel parce que ceux trouvés sur le Net ne m’ont pas convaincu.
La raison première étant que ces tutoriels expliquent comment travailler d’une façon absurde :
En compilant le programme avec un IDE, puis en l’envoyant sur l’ARM à l’aide d’un autre
utilitaire, rendant vaine toute possibilité de débogage en temps réel, alors que la carte est
munie d’une sonde J-Tag. Enfin, j’ai trouvé qu’il manquait nombre de renseignements
importants.

En ce qui me concerne, il n’est même pas envisageable de développer des applications un


temps soi peu sérieuses sans disposer de ce genre d’outils, et surtout en sautant d’un logiciel à
l’autre : On n’est plus au moyen-âge des microcontrôleurs…

1.3. Pré requis

Pour réaliser un des projets de ce tutoriel, vous devrez disposer des éléments suivants :

- Une carte LandTiger® (voir : Lapalisse)


- Une alimentation régulée 5V/3W (fournie avec la carte)
- Un câble USB (fourni avec la carte)
- Le schéma de la carte (fourni avec la carte)
- Le manuel utilisateur de la carte (fourni avec la carte)
- L’environnement IDE Keil µVision® version gratuite (fourni ou sur le site de Keil®)
- Des drivers Segger® pour la sonde J-Tag (fournis ou sur le site de Segger®)

1.4. Présentation de la LandTiger®

Il est temps de passer aux choses concrètes. Commençons par voir à quoi ressemble cette
fameuse carte LandTiger® :

5
Cette carte est une carte compatible avec la Keil MCB1760®, excepté certaines
modifications mineures et surtout l’ajout d’une sonde J-Tag intégrée. Elle est basée sur un
microcontrôleur ARM 32 bits de type Cortex-M3, le LPC1768, cadencé à 100Mhz.

Elle inclut de nombreuses fonctionnalités, dont :

- Un écran tactile couleur de 240x320 pixels


- Un port USB maître/esclave (Master/Host,OTG)
- Un port Ethernet 100Mbits/s full-duplex et gestion de croisement de câbles
- 8 Leds utilisateur, des boutons, un joystick, un potentiomètre, un HP…
- 1 port RS485
- 2 ports RS232 dont un pouvant servir pour charger le programme
- Un port J-Tag
- Une sonde J-Tag avec sa propre connexion USB slave
- 2 ports CAN (bus CAN)
- Des connecteurs donnant accès à toutes les pins de l’ARM
- Une horloge temps réel (pile non fournie à placer sous l’écran)

Cette carte vous permet donc de développer de puissantes applications sans ajout de
composants externes dans la majorité des cas.

La sonde J-Tag de la carte est située sur la photo sur le dessus à gauche. Vous voyez le
câble USB branché sur le port USB slave de la sonde. Il y a donc deux connecteurs USB slave
sur la carte :

- Celui de la sonde J-Tag, en haut à gauche


- Celui connecté sur l’ARM LPC1768 sur lequel on développe, côté droit sur la photo

6
Ne confondez pas dans les explications de ce manuel. Pour programmer et débugger la
carte on utilise le port USB J-Tag, l’autre port USB n’est pas utilisé dans ce tutoriel.

Notez déjà que la carte peut être livrée avec un écran de type HY32C, comme sur la photo
précédente, soit avec un écran HY32D, comme ci-dessous :

La version « D » est reconnaissable à son double-connecteur… et au fait que c’est écrit


sur le circuit. Il n’y a qu’un seul connecteur soudé, si vous le soudez sur l’autre emplacement
prévu ou que vous en ajoutez un second, vous pouvez opérer une rotation de 180° de votre
écran (au cas où ceci vous serait utile).

1.5. Avertissement

La carte LandTiger® est considérée comme un clone de la carte originale Keil®


MCB1700. On trouve également des cartes compatibles sous les noms suivants : Hy-
LandTiger®, WayEngineer®, PowerAvr®, PowerMCU®, LPC1768®, LandRover®…

J’ai tenté de me faire une idée sur la légalité ou non de ces cartes clonées, mais au final
j’ai renoncé : D’une part ces cartes sont en vente dans des sites de vente en ligne officiels très
connus (français, belges, suisses, canadiens, allemands …), et d’autre part la réglementation
varie d’un pays à l’autre. Je veux bien essayer de respecter la loi, mais je ne prétends pas
« laver plus blanc que blanc », et de ce fait, vu que j’ai pu acheter cette carte sans aucun
problème, officiellement, et sans même savoir à l’origine qu’il s’agissait d’un clone, j’ai
décidé de l’utiliser et de partager mon expérience.

Cependant, j’ai par précaution renoncé à utiliser les librairies spécifiques fournies avec la
carte, certaines se référant explicitement à la carte originale ou limitant les licences à
l’utilisation sur des versions officielles de la carte. C’est pourquoi, via mon site, je vous
propose des librairies que j’ai écrites, open-source, ne reposant sur aucun copyright
puisqu’écrites directement à partir des datasheets des différents composants.

7
Vous pouvez vous rendre sur mon site : www.bigonoff.org, sur la page concernant les
applications ARM, et télécharger les applications présentes à titre d’exemple. Ma page
Domocan contient également une application à base de LandTiger®.

Prenez garde que si vous utilisez une carte « compatible » mais différente de la
LandTiger®, vous devez vérifier au niveau du schéma s’il n’y a pas quelques différences
mineures à répercuter dans les librairies.

Pour rappel, je partage mon expérience, je ne vous garanti rien et je vous rappelle que
vous êtes seuls responsables de vos décisions, que vous devez assumer.

1.6. Modification hardware importante

Si (et seulement si) votre carte est livrée avec un écran de type « C » (une seule rangée de
connecteurs), vous devez absolument dessouder sur le circuit de la carte la résistance R58 de
0Ohm. En effet, cette résistance permet d’amener la tension régulée de 3.3V sur l’afficheur.

Or le HY32C dispose de son propre régulateur et donc, dans ce cas précis, cette résistance
a comme effet de relier la sortie du régulateur de l’écran sur la sortie du régulateur de la carte,
ce qui est très mauvais. Enlever cette résistance est donc très important.

N’effectuez pas cette modification si vous avez un écran de type « D », sinon il ne serait
plus correctement alimenté. Soyez attentifs !

Commencez par enlever les 4 vis maintenant l’écran LCD, puis retirez la carte complète
soutenant l’écran précautionneusement (elle est enfichée dans un connecteur, tirez droit).

Vous pouvez alors visualiser R58, à côté du connecteur, près du HP :

8
Pour dessouder, rien de plus simple : Faites chauffer votre fer à souder, plaquez-le contre
un des côtés de la résistance, et les deux côtés vont très rapidement se dessouder et la
résistance collera à votre fer à souder. Sur la photo, la résistance est déjà enlevée.

Opérez proprement, le but n’est pas de décoller les pistes du circuit. Si vous craignez cette
opération, faites-vous aider par quelqu’un de compétant. Évitez également l’utilisation de fers
non adaptés, et reliez si-possible la panne de votre fer à la masse de la carte ainsi qu’à la terre,
à l’aide de pinces crocodile.

9
Notes :

10
2. La programmation

2.1. Les outils utilisés

J’ai choisi d’utiliser l’environnement de développement (IDE) Keil µVision® pour les raisons
suivantes :

- Il est fourni avec la carte (sur le CD joint)

- Il permet de configurer et d’utiliser la sonde J-Tag incluse

- Il est gratuit dans sa version limitée, dont la limite n’est pas dérangeante (taille de
32Ko pour le débogage)

- C’est un compilateur très bien fait et très puissant, même s’il lui manque l’IntelliSense

Vous pouvez utiliser la version fournie avec la carte, pour suivre au plus près ce tutoriel,
ou aller télécharger la dernière version chez Keil®.

Concernant les drivers de la sonde J-Tag, les drivers officiels Segger® fonctionnent
parfaitement. Une ancienne version du pack est fournie sur le CD, elle installe non seulement
les drivers, mais également les utilitaires gérant les téléchargements, dont vous n’avez pas
besoin si vous travaillez directement à partir de µVision.

Notez que si vous allez charger les derniers drivers chez Segger®, ceux-ci fonctionneront
parfaitement, mais les utilitaires fournis, eux, ne fonctionneront pas. Par exemple en lançant J-
Flash® (qui permet de charger un fichier .hex dans l’ARM) vous obtiendrez un message
d’erreur indiquant que la sonde est un clone et que seule une sonde originale est autorisée
pour l’utilisation de l’utilitaire. Les drivers, eux, ne semblent poser aucun problème, et vous
n’avez besoin que d’eux pour développer.

En ce qui me concerne, dans ce tutorial, j’ai utilisé exclusivement ce qui était fourni avec
le CD, et mes premières applications (carte Artnet et interface Domocan) ont été réalisées
avec succès avec µVision® démo et les anciens drivers Segger® : Je n’ai noté aucun problème.

Vous pouvez charger un fichier « .hex » déjà créé à l’aide de la sonde J-Tag ou à l’aide du
port série. Les méthodes sont décrites dans le manuel utilisateur de la carte.

2.2. Réalisation d’un nouveau projet

Installez µVision® et les drivers J-Tag, ceci avant de tenter de relier le connecteur USB de
la sonde à votre PC. Si vous avez déjà branché la sonde et qu’un driver par défaut est utilisé,
vous devrez laisser la sonde branchée, aller dans le panneau de configuration, puis dans les
options système, visualiser les périphériques, et supprimer le périphérique non fonctionnel.
Ensuite déconnectez la sonde et installez les drivers.

11
2.2.1. Création de la structure de répertoires

Maintenant vous allez créer un répertoire « Projets LandTiger » (ou ce que vous voulez),
dans lequel vous créerez un répertoire par projet. Notre répertoire du projet va s’appeler
« Tutoriel », créons donc un répertoire « Tutoriel » dans notre répertoire « Projets
LandTiger ». En ce qui me concerne j’ai un répertoire « LandTiger » contenant tout ce qui est
utile (datasheets, schéma, manuel etc.), dans lequel j’ai créé un répertoire « Projets ».

Vous aurez besoin de librairies. Je vous en fourni mais vous pouvez utiliser les vôtres si
vous avez envie de repartir de zéro (attention, c’est du boulot). Vous pouvez soit utiliser des
répertoires de librairies à la racine de votre répertoire « Projets LandTiger », soit copier ces
librairies dans le répertoire de chaque application.

L’avantage de la première méthode est de n’avoir qu’une seule fois les librairies.
L’inconvénient est que ça cache un piège : Si vous chargez une nouvelle version des
librairies, ou que vous y effectuez des modifications, cette modification interviendra pour tous
les projets déjà terminés, qui risquent alors de voir apparaître des bugs qui n’existaient pas au
départ, voire même une impossibilité de compiler.

Je vous conseille d’intégrer les librairies dans chaque répertoire de projet, vous assurant
qu’un projet vérifié reste opérationnel. Quitte à effectuer des modifications si un bug
découvert est corrigé et ensuite à revérifier si votre projet fonctionne toujours.

Dans ce tutoriel je vais utiliser un répertoire de librairies par projet, pour plus de sécurité.
Copiez donc les répertoires « Libs », « CMSIS » et « Sources » que je vous fourni dans votre
répertoire « Tutoriel ».

Il vous reste à créer deux répertoires dans votre répertoire « Tutoriel » :


- Bin
- List

La structure de votre répertoire « Projets LandTiger » est maintenant la suivante :

Projets LandTiger
Libs
Fichiers de librairies
CMSIS
Fichiers CMSIS
Sources
Fichiers du projet
Bin
List

Attention, j’ai modifié certains fichiers du répertoire CMSIS fourni avec la carte et j’en ai
ajouté: Utilisez les miens car les originaux contenaient une erreur et une valeur inappropriée.

2.2.2. Création du projet µVision®

Démarrez µVision®. Allez dans le menu « Project » et sélectionnez « New µVision


project ». Dans la fenêtre qui s’ouvre, placez-vous dans le répertoire de votre projet, et

12
tapez un nom de projet (il ne doit pas nécessairement être identique au nom de répertoire).
J’ai choisi « Tutoriel 1 » :

Remarquez la structure sur mon disque « Data » (ne jamais placer ses données dans le
disque système). Vous constaterez la présence des 5 sous-répertoires dont nous venons de
parler, dans le répertoire « Tutoriel » de notre répertoire principal de projet.

Cliquez sur « Enregistrer ». Dans la fenêtre qui s’ouvre, choisissez votre cible, le
microcontrôleur « NXP-LPC1768 », et confirmez :

Lorsque la requête pour l’ajout de « LPC17xxStartup » apparaît, choisissez « Non » car


vous avez déjà ce fichier (modifié) et il se trouve déjà au bon endroit. Répondre « Oui »
13
permet de recréer ce fichier par défaut, il contient une série d’éléments essentiels. Dans ce
cas, pensez à augmenter la taille de la pile dans la déclaration « stack size » qu’il contient,
sinon vous allez droit aux plantages incompréhensibles. Une valeur de 0x1000 est convenable
pour la plupart des cas.

2.2.3. Création des groupes

Dans la fenêtre verticale de gauche, sélectionnez l’onglet « Project ». Il s’agit de


l’explorateur de projet. En principe, si vous n’avez pas manipulé µVision®, cet onglet est déjà
sélectionné. Dans l’explorateur de projet, renommez le dossier « Source Group 1 » en
« CMSIS ».

Toujours dans l’explorateur de projet, effectuez en clic-droit sur le nom du projet


(Tutoriel 1) et sélectionnez « Add Group… ». Renommez ensuite le « New Group »
nouvellement créé en « Libs ». Créez un groupe supplémentaire et renommez-le en « App ».
Vous devriez obtenir ceci :

2.2.4. Ajout des fichiers à compiler

Vous venez de créer les groupes de sources, nous allons maintenant les remplir. Les
fichiers que nous allons y placer seront ceux qui seront compilés pour former notre projet
global.

Le groupe CMSIS contient les fichiers de base de l’ARM que vous utilisez, ils sont
communs à tout projet concernant votre LandTiger®. Cliquez avec le bouton droit sur le
groupe « CMSIS » et sélectionnez « Add files to group CMSIS ». Dans la fenêtre qui s’ouvre,
entrez dans le répertoire CMSIS de votre projet et sélectionnez les fichiers « core_cm3.c » et
« system_LPC17xx.c » qui s’y trouvent. Validez avec <Add> puis fermez avec <Close>.

Un petit « + » apparaît à côté du nom du groupe CMSIS dans l’explorateur de projet.


Cliquez dessus et vérifiez la présence de vos deux fichiers :

14
Ajoutez maintenant au groupe « Libs » les fichiers contenus dans le répertoire « Libs »
de votre projet. Ces fichiers sont les librairies que j’ai écrites, elles permettent de gérer les
différentes fonctionnalités de la carte LandTiger®, je compléterai au fur et à mesure en
fonction de mon utilisation. Attention que certaines librairies sont interdépendantes. Ainsi, par
exemple, la librairie GLCD.c, qui gère l’écran, est utilisée dans plusieurs autres librairies pour
afficher les écrans de configuration. Il en va de même pour la librairie gérant la mémoire flash
(qui contient notamment les valeurs de calibration de l’écran tactile). Dans le doute vous
pouvez y placer systématiquement toutes les librairies disponibles.

Enfin, ajoutez au groupe « App » les fichiers de votre répertoire « Sources », soit
« main.c » et « startup_LPC17xx.s ». Attention, pour voir ce dernier il faut sélectionner
comme filtre « All files » dans la fenêtre de sélection :

Vous devriez obtenir quelque chose de ce style :

15
Faites un clic-droit sur le nom du projet et validez la case à cocher « Show Include File
Dependencies ». En principe elle devrait déjà être cochée. Ceci vous permettra d’afficher les
dépendances de chaque fichier source (les fichiers « .h »). Ce sont ces dépendances que vous
devrez inclure dans votre fichier « main.c » si vous avez besoin des librairies concernées. Les
fichiers de dépendance n’apparaissent dans l’explorateur de projet qu’une fois qu’on a
compilé le projet. Mais nous n’en sommes pas encore là.

Terminez en renommant « Target 1 » en votre nom de projet, donc pour nous « Tutoriel
1 ».

2.2.5. Paramètres de compilation

Pour ouvrir la fenêtre de paramétrage du projet, vous pouvez utiliser une des méthodes
suivantes :

- Clic-droit sur le nom du projet, puis sélection de l’item « Options for Target…»
- Utilisation de l’item « Option for Target… » du menu « Project »
- Appui sur <Alt><F7>
- Utilisation du bouton à droite du nom du groupe dans la barre d’outils.

Choisissez votre méthode et ouvrez la fenêtre de paramétrage. Sélectionnez l’onglet


« Output » :

16
Vous pouvez définir le nom de l’exécutable produit. Nous laisserons le nom du projet,
inutile de nous compliquer la vie. Vous définissez ensuite si votre projet est compilé en tant
que librairie (« Create Library ») ou en tant que fichier exécutable autonome (« Create
Executable »). Choisissez l’exécutable.

Dans ce cas, vous pouvez alors déterminer quelles informations seront présentes dans le
fichier compilé. Je vous conseille ceci :

- Tant que vous êtes en train d’écrire le programme et de le déboguer, cochez « Debug
information » et « Browser information ». Ne cochez pas « Create Hex file », ce
fichier ne vous sert personnellement à rien, il n’est utile que si vous ou une tierce
personne programmez la carte sans passer par µVision®.

- Une fois que votre programme est fonctionnel, décochez les cases précédentes et
cochez « Create Hex File » pour obtenir un fichier « hex » que vous pouvez distribuer
sans obliger l’utilisateur final à recharger votre projet dans µVision®.

Bref, cochez comme sur la capture d’écran précédente.

Nous devons maintenant définir où seront placés les fichiers exécutables générés. Pour ce
faire, cliquez sur le bouton « Select Folder for Objects…. ». Dans la fenêtre qui s’ouvre,
sélectionnez le répertoire qui nous intéresse : « Bin ». Attention, pour qu’il soit réellement
sélectionné il faut entrer dedans et non simplement le sélectionner.

Une fois dans ce répertoire, son contenu étant vide, un message vous indique « Aucun
élément ne correspond à votre recherche » :

17
Validez avec <OK>. Sélectionnez maintenant l’onglet « Listing ». Validez les options
comme sur cette capture d’écran :

Cliquez sur le bouton « Select Folders for Listings… ». Entrez dans le répertoire « List » de
votre projet, et confirmez comme nous venons de le voir.

Nous allons maintenant configurer les chemins d’accès à nos répertoires. Pour ce faire,
sélectionnez l’onglet « C/C++ ». Dans la zone « Include Paths » nous allons placer les
chemins vers les répertoires contenant les fichiers de définitions. Ces répertoires sont « Libs »
et « CMSIS » de notre projet. Vous pouvez soit donner les chemins relatifs directement dans la
zone de texte, chaque chemin étant séparé par un « ; », soit utiliser la méthode interactive.

Attention, si vous entrez les chemins manuellement, pensez à entrer ces chemins en mode
relatif (.\.... ) et non absolus (D:\....). Voici la ligne à entrer dans notre cas :

.\Libs ;.\CMSIS
18
Pour éviter tout risque d’erreur, cliquez sur le petit bouton <…> situé à droite de la zone.
Dans la fenêtre qui s’ouvre, cliquez sur le bouton de gauche (un peu grisé) puis sur le bouton
<…> qui apparaît à droite de la nouvelle ligne.

Sélectionnez le répertoire « CMSIS » mais cette fois sans entrer dedans, il faut juste le
sélectionner (ce n’est pas toujours très cohérent, il faut s’adapter). Validez avec <OK>.
Cliquez n’importe où dans la zone blanche de la fenêtre, pour désélectionner la ligne
courante, puis répétez les deux opérations précédentes et choisissez cette fois le répertoire
« Libs ».

Validez avec <OK>. Vous obtenez la chaîne que vous auriez du entrer manuellement,
choisissez votre méthode :

2.2.6. Première compilation

Refermez la fenêtre de paramétrage en validant vos réglages avec <OK>, puis double-
cliquez sur le fichier « main.c », visible en dépliant le groupe « App ». Le contenu du
fichier s’affiche dans l’éditeur, partie droite de la fenêtre :

19
Lancez la compilation, soit en tapant <F7>, soit en allant sur l’item « Build Target » du
menu « Project ». Si vous n’avez pas commis d’erreurs, la compilation devrait se dérouler
sans problème :

J’attire votre attention sur le point suivant : Si vous avez un pour plusieurs warnings
(alertes) généré pour un de vos fichiers mais que la compilation a pu s’effectuer sans
problème (0 error, x warnings), ce fichier ne sera pas recompilé lors de la tentative suivante
s’il n’a pas été modifié. Vous ne verrez donc plus ce warning.

20
Pour forcer le compilateur à vous refournir les warnings, il faut l’obliger à recompiler,
soit :

- En effectuant une modification sur le fichier en question (si vous savez lequel)
- En sélectionnant l’item « Rebuild All Target Files » du menu « Project »

En principe, si votre programme est correctement réalisé, vous ne devriez jamais avoir
aucun warning. Je vous déconseille fortement de laisser traîner des warnings non résolus, car
le compilateur de µVision® est très perspicace pour détecter les problèmes potentiels délicats.

La compilation effectuée, vous voyez apparaître de petits signes « + » à droite de chacun


de vos fichiers source. Si vous cliquez dessus vous voyez les dépendances dont chaque fichier
a besoin. Par exemple si vous regardez les dépendances du fichier « DataFlash.c » qui gère la
mémoire flash présente sur la carte :

Par convention, le nom du fichier de définition (.h) porte le même nom que la librairie à
laquelle il est attaché (.c). Le fichier « DataFlash.c » utilise les définitions du fichier
« DataFlash.h ». Attention, dans l’explorateur de projet, la casse n’est pas respectée. Notez
une série d’autres fichiers de dépendance utilisés, c’est parce que cette librairie a elle-même
besoin d’autres librairies et fichiers de définitions, dont :

- LPC17xx.h : Définition des noms de registres LPC et autres généralités


- core_cm3.h : Définitions système bas niveau
- stdint.h : Définitions des types de variables
- systemLPC17xx.h : Définitions pour l’horloge de l’ARM
- GLCD.h : Définitions de la librairie GLCD.c, la librairie utilise l’écran pour
afficher des messages de confirmation ou d’erreurs
- TouchPanel.h : Définitions de la librairie TouchPanel.c pour l’écran tactile, utilisé
lorsqu’une action de l’utilisateur est requise (effacement flash etc.)
- Timing.h : Définitions de la librairie Timing.c, qui sert pour établir différentes
temporisations

21
Si vous voulez savoir quelles fonctions d’une librairie sont disponibles pour votre
programme principal, double-cliquez sur le fichier « .h » correspondant. Par exemple, notre
librairie DataFlash.c vous fournit les fonctionnalités suivantes :

Et ainsi de suite si vous déroulez l’écran. Notez que les fonctions publiques sont dans la
zone « Prototypes », qu’elles sont déclarées « extern », et que le nom d’une fonction
publique est par convention dans mes librairies, préfixées du nom évoquant la librairie suivi
d’un surlignement : (ici : « DataFlash_ »).

Examinez les fichiers « .h », ils vous informeront de tout ce qui est disponible pour votre
usage, que ce soit des fonctions ou d’autres déclarations, comme des couleurs etc.

Le fichier « LPC17xx.h », quant à lui, vous fournira le nom des différentes structures
utilisées pour accéder aux registres de votre ARM. Vous aurez souvent besoin d’y jeter un œil,
surtout au début.

22
3. Le programme

3.1. Led clignotante par temporisation software

Nous allons maintenant réaliser notre premier petit programme : Une Led clignotante,
selon mon habitude, qui semble devenue une mode sur le Net. Je ne vais pas vous apprendre
tout ce qu’il faut savoir pour programmer un microcontrôleur ARM, je vais juste vous donner
quelques pistes pour démarrer. Ce programme est donc le prétexte pour découvrir la
philosophie de la programmation ARM, ce chapitre contient plus que ce qui est nécessaire
pour réaliser le projet.

À l’aide du chapitre précédent, vous allez maintenant créer un nouveau projet, intitulé
« LedSoft ». Notez au passage que tous les chemins utilisés dans le projet sont des chemins
relatifs. De ce fait, si vous déplacez tout votre répertoire de projet, celui-ci continue de
fonctionner.

Comme fichiers de librairies, ne placez que « GLCD.c » et « Timing.c ». Double-cliquez


sur le fichier « main.c » de votre explorateur de projet, pour pouvoir l’éditer.

Placez-y les commentaires appropriés. Effacez la partie « Interruptions », nous y


reviendrons prochainement. Votre programme devrait ressembler à ceci :

//////////////////////////////////////////////////////////////////////////////////////////
// TUTORIEL : CLIGNOTEMENT D'UNE LED //
//////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------
// Auteur : Bigonoff
// Date : 26/05/2015
// Révision : 1.0
// Cible : Carte LandTiger - Cortex-M3 NXP LPC1768
//----------------------------------------------------------------------------------------
// Historique:
// -----------
// V1.0 : 26/05/2015 : Première version opérationnelle
//----------------------------------------------------------------------------------------
// Explications:
// -------------
// Ce petit programme permet de se familiariser avec la programmation ARM sur LandTiger
//
//////////////////////////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
// INCLUDES //
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////

#include "LPC17xx.h" // toujours présent


#include "LPC17xx_Bits.h" // toujours présent

//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
// DEFINES //
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////

23
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
// FONCTIONS PROTOTYPES //
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////

void InitializeGPIO(void); // Initialiser GPIO

//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
// MAIN //
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
int main(void)
{
SystemInit(); // initialise horloge : INDISPENSABLE
InitializeGPIO(); // initialise GPIO

while(1) // boucle principale


{

}
}

//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
// FONCTIONS //
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////////////////////////
// INITIALISATION DES GPIO //
//////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------
// Contient l'initialisation des pins GPIO.
// La configuration des pins LCD_DIR (P0.21) et LCD_EN (P0.19) est indispensable:
// Il faut mettre le transceiver SN74ALVC164245 en entrée côté ARM pour éviter les conflits
// avec les pins P2.0/P2.7 (LCD_DIR = 1) tout en évitant d'avoir un conflit entre la sortie
// du latch 74LV573PW et le canal A du transceiver -> lCD_EN = 0
//----------------------------------------------------------------------------------------
void InitializeGPIO(void)
{
// INDISPENSABLE
// -------------
LPC_GPIO0->FIODIR = 0x00280000; // LCD_DIR et LCD_EN en sortie
LPC_GPIO0->FIOPIN |= 0x00200000; // Transceiver en entrée, latch hors-service

// APPLICATION
// -----------
LPC_GPIO2->FIODIR0 = 0xFF; // P2.0 -> P2.7 en sortie (Leds)
LPC_GPIO1->FIODIR &= 0xC1FFFFFF; // P1.25 -> P1.29 en entrée (Joystick)
}

Tel que le fichier startup « startup_LPC17xx.s » est conçu, il lance la fonction « main.c »
lors du démarrage ou du reset de l’ARM. Je vous encourage fortement à étudier les fichiers du
groupe « CMSIS » ainsi que « startup_LPC17xx.s », ils contiennent des tas de renseignements
utiles pour comprendre « comment ça marche ». Ne vous inquiétez pas si vous ne comprenez
pas tout au départ, il s’agit dans certains cas de programmation assez pointue, parfois de
routines en langage machine, et également utilise des particularités spécifiques de l’ARM.

24
Comprenez que, comme d’habitude, il n’y a aucune magie : Si tout semble se dérouler
d’une façon automatique, c’est que les informations nécessaires sont déjà fournies et codées
dans les fichiers fournis. Remarquez que j’ai effectué une modification dans le fichier de
startup et une dans « system_LPC17xx.c ». Je vous laisse examiner leur zone de commentaire
et vérifier la raison de la seconde modification dans le datasheet de l’ARM1768.

Tant qu’on en parle, pour développer vous aurez besoin de ce datasheet et également du
schéma de la carte LandTiger®, même si l’utilisation de mes librairies vous évite une grande
partie du travail de recherche.

Si vous examinez la fonction « main() », vous constaterez qu’elle ressemble dans sa


conception à la structure des fichiers maquettes pour PIC® que je vous propose dans mes
cours. Comprenez qu’ainsi que je vous l’avais indiqué, il y a beaucoup de points communs
dans la philosophie des microcontrôleurs.

Vous retrouvez donc une partie concernant l’initialisation, puis le programme actif, avec
la fameuse boucle sans fin composée ici d’un While(1).

À propos de « true » et « false », j’ai ajouté leur définition dans les fichiers que vous
incluez, vous disposez donc du type « Boolean » (qui est en fait un unsigned char). Je vous
conseille d’utiliser ce type ainsi que les deux valeurs liées, ça rendra votre code plus clair.

Vous pouvez évidemment, dans vos programmes, utiliser les définitions standardisées du
C99 concernant les types de variables dans vos programmes, mais vous disposez également de
types plus « clairs », comme int16_t (valeur signée sur 16 bits) ou uint32_t (valeur non
signée sur 32 bits). Vous évitez ainsi certaines questions existentielles sur la taille de chaque
type, même si dans notre cas nous avons bel et bien affaire à un processeur 32 bits. Vous
trouverez la liste des types autorisés dans le fichier « stdin.h », pensez à y jeter un œil.

Notez au passage que l’ARM travaille par défaut en mode « Little Endian », c'est-à-dire
que lorsque vous stockez en mémoire une variable codée sur plusieurs octets, il écrit en
premier le poids faible. Par exemple, si vous sauvez la valeur 0x19654253 à l’adresse 0000 de
la mémoire RAM, vous obtenez ceci :

Adresse 0000 0x53


Adresse 0001 0x42
Adresse 0002 0x65
Adresse 0003 0x19

Pensez-y lorsque vous voudrez examiner des zones mémoires, lorsque vous manipulez des
octets constituant une valeur unique de taille supérieure, ou lorsque vous récupérez sur le net
des fonctions en C destinées à un autre microcontrôleur.

Sachez qu’il est possible de configurer ce processeur pour qu’il travaille en mode « Big-
Endian » (d’abord le poids fort, puis le poids faible), mais pensez que ça impacte sur le
fonctionnement des librairies, qui risquent de ne plus fonctionner, ce qui vous obligerait à les
réécrire. Obliger l’ARM à travailler en mode Big-Endian n’a généralement pas d’intérêt en
termes de performances (ça peut cependant simplifier la vie dans des cas spéciaux, comme le
travail sur des trames construites dans ce mode), mais vous pouvez le faire.

25
Revenons à notre fonction principale, « main() ». La première instruction est un appel de
fonction :

SystemInit(); // initialise horloge : INDISPENSABLE

La fonction en question permet d’initialiser l’horloge de l’ARM en fonction des


composants (quartz de 12Mhz) qu’elle contient et de la vitesse d’horloge qu’on veut obtenir
(100Mhz). Notez que l’ARM fournit des diviseurs et des multiplicateurs qu’on peut combiner,
ce qui explique que la fréquence obtenue ne soit pas un entier multiple de celle du quartz. La
fonction en question se trouve dans le fichier « system_LPCxx.c » :

extern void SystemInit (void);

Comment le compilateur sait-il que cette fonction est disponible ? Parce que c’est déclaré
dans le fichier de définition correspondant, « system_LPCxx.h », qui fait partie des
dépendances de votre « main.c » par le biais des directives includes que vous avez placés, les
fichiers inclus ajoutant eux-mêmes d’autres dépendances.

Vous devriez rapidement comprendre le principe et vous familiariser avec ce mode de


travail, typique du C sur microcontrôleurs. Notez au passage que sur ce type de cible le
langage C constitue un bon choix, mais vous pouvez évidemment utiliser un autre langage,
voire programmer directement en langage d’assemblage. Ce dernier est plus compliqué à
apprendre que celui des PIC®, mais, en contrepartie, il s’agit presque d’un langage évolué, tant
les possibilités et les modes d’adressages sont puissants et variés. Faites votre propre choix…

Bref, l’appel de « SystemInit() » doit être systématique dans tous vos programmes, et
constituer la première ligne de la méthode « main() ». Si vous avez à réaliser des applications
spécifiques nécessitant d’autres modes de fonctionnement vous pourrez configurer l’horloge
selon vos désirs sans appeler « SystemInit() », mais dans tous les cas vous devez procéder à
l’initialisation.

Une bonne pratique est d’ensuite initialiser les entrées sorties, appelées « GPIO » (General
Purpose Input Output) sur notre ARM :

InitializeGPIO(); // initialise GPIO

Comme sur les PIC® on va trouver des ports, des registres de direction, et des registres
d’entrées/sorties. Je vous l’ai dit : Les notions de base se retrouvent partout. Les ports sont
numérotés et préfixés de «P », donc P0,P1,P2… Les pins qui les composent sont en suffixe
après un point de séparation. Ainsi l’entrée/sortie n° 18 du port numéro 2 s’appelle P2.18.

Pensez qu’il s’agit d’un microcontrôleur 32 bits, vous pouvez donc avoir 32 bits ou pins
par registre ou par port. Voyons la méthode en question :

26
//////////////////////////////////////////////////////////////////////////////////////////
// INITIALISATION DES GPIO //
//////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------
// Contient l'initialisation des pins GPIO.
// La configuration des pins LCD_DIR (P0.21) et LCD_EN (P0.19) est indispensable:
// Il faut mettre le transceiver SN74ALVC164245 en entrée côté ARM pour éviter les conflits
// avec les pins P2.0/P2.7 (LCD_DIR = 1) tout en évitant d'avoir un conflit entre la sortie
// du latch 74LV573PW et le canal A du transceiver -> lCD_EN = 0
//----------------------------------------------------------------------------------------
void InitializeGPIO(void)
{
// INDISPENSABLE
// -------------
LPC_GPIO0->FIODIR = 0x00280000; // LCD_DIR et LCD_EN en sortie
LPC_GPIO0->FIOPIN |= 0x00200000; // Transceiver en entrée, latch hors-service

// APPLICATION
// -----------
LPC_GPIO2->FIODIR0 = 0xFF; // P2.0 -> P2.7 en sortie (Leds)
LPC_GPIO1->FIODIR &= 0xC1FFFFFF; // P1.25 -> P1.29 en entrée (Joystick)
}

Vous travaillez sur une carte dont un grand nombre de fonctions sont multiplexées. Pour
tout comprendre vous devez impérativement utiliser le schéma de la carte et avoir des
connaissances en électronique. Le datasheet de chaque composant de la carte est également
indispensable pour tout gérer. J’ai cependant déjà effectué la majorité du travail pour vous, et,
contrairement aux librairies d’origine fournies, les miennes tiennent compte des conflits
potentiels.

Le commentaire explique la raison de la configuration en question. Ce qui nous intéresse


c’est la façon de procéder, car cela explique comment configurer une pin.

La première ligne place la valeur 0x280000 dans LPC_GPIO0->FIODIR. Qu’est-ce que c’est
que ce truc ?

Votre réflexe dans ce genre de situation doit être d’examiner le fichier « LPC17xx.h » et
d’y rechercher quelque chose qui ressemble au début mais sans le chiffre, et donc
« LPC_GPIO ». Vous allez trouver ceci :

/*------------- General Purpose Input/Output (GPIO) --------------------------*/


typedef struct
{
union {
__IO uint32_t FIODIR;
struct {
__IO uint16_t FIODIRL;
__IO uint16_t FIODIRH;
};
struct {
__IO uint8_t FIODIR0;
__IO uint8_t FIODIR1;
__IO uint8_t FIODIR2;
__IO uint8_t FIODIR3;
};
};
uint32_t RESERVED0[3];
union {
27
__IO uint32_t FIOMASK;
struct {
__IO uint16_t FIOMASKL;
__IO uint16_t FIOMASKH;
};
struct {
__IO uint8_t FIOMASK0;
__IO uint8_t FIOMASK1;
__IO uint8_t FIOMASK2;
__IO uint8_t FIOMASK3;
};
};
union {
__IO uint32_t FIOPIN;
struct {
__IO uint16_t FIOPINL;
__IO uint16_t FIOPINH;
};
struct {
__IO uint8_t FIOPIN0;
__IO uint8_t FIOPIN1;
__IO uint8_t FIOPIN2;
__IO uint8_t FIOPIN3;
};
};
union {
__IO uint32_t FIOSET;
struct {
__IO uint16_t FIOSETL;
__IO uint16_t FIOSETH;
};
struct {
__IO uint8_t FIOSET0;
__IO uint8_t FIOSET1;
__IO uint8_t FIOSET2;
__IO uint8_t FIOSET3;
};
};
union {
__O uint32_t FIOCLR;
struct {
__O uint16_t FIOCLRL;
__O uint16_t FIOCLRH;
};
struct {
__O uint8_t FIOCLR0;
__O uint8_t FIOCLR1;
__O uint8_t FIOCLR2;
__O uint8_t FIOCLR3;
};
};
} LPC_GPIO_TypeDef;

Il s’agit d’une déclaration du genre « typedef struct », c’est-à-dire qu’on déclare une
structure contenant les éléments, mais sans lui affecter un réalité physique. La raison en est
simple : Nous avons plusieurs ports GPIO dans notre ARM, avec chaque fois des registres
situés à des adresses différentes. Comme l’ARM a été bien étudié, ces registres se succèdent à

28
chaque fois dans le même ordre, et donc on peut déclarer une structure contenant tous ces
registres qui est la même pour chacun des ports.

Quelque part dans l’ensemble de nos fichiers, nous allons donc trouver une structure de
type LPC_GPIO appelée LPC_GPIO0 et qui est initialisée à l’adresse du premier registre de
cette structure, qui est « FIODIR »

__IO uint32_t FIODIR;

Contrairement à certains éléments contenus dans ce fichier, qui doivent être alloués en
fonction de certains paramètres, l’adresse des registres dans l’ARM est figée. Nous devrions
donc trouver quelque part de simples affectations statiques. De fait, plus bas dans ce fichier
nous trouvons :

#define LPC_GPIO0 ((LPC_GPIO_TypeDef *) LPC_GPIO0_BASE )

Qui affecte au terme “LPC_GPIO » une valeur numérique « LPC_GPIO_BASE » castée en


structure de type LPC_GPIO.

L’effet est similaire à celui que vous auriez obtenu en déclarant dans votre programme :

LPC_GPIO *LPC_GPIO0 = LPC_GPIO_BASE

Dans le second cas, vous déclarez un pointeur sur une structure de type LPC_GPIO, que
vous initialisez avec la valeur LPC_GPIO_BASE, qui est donc son adresse.

La différence est qu’avec la première méthode vous ne déclarez rien et n’utilisez aucune
variable, c’est du simple traitement de texte qui remplacera chaque référence à LPC_GPIO0
faite dans votre programme par son adresse castée en structure LPC_GPIO. En effet, les
registres ARM sont connus, ne changent pas d’adresse, et existent déjà physiquement.
Pourquoi gaspiller une variable pour accéder à une adresse figée ?

Reste à trouver que vaut LPC_GPIO_BASE. En fait, c’est de nouveau une définition, qu’on
retrouve plus haut :

#define LPC_GPIO0_BASE (LPC_GPIO_BASE + 0x00000)

Ce qui nous amène à LPC_GPIO_BASE :

#define LPC_GPIO_BASE (0x2009C000UL)

Notre structure LPC_GPIO_BASE se retrouve affectée à l’adresse 0x2009C000 (Les


adresses également sont sur 32 bits). Le suffixe « UL » signifie qu’on travaille en Unsigned
Long (uint32_t). Il faut être rigoureux lorsqu’on utilise des déclarations de ce type, on ignore
dans quel contexte elles seront utilisées, ce qui explique les précisions de types et les
parenthèses multiples.

S’agissant d’un pointeur, les différents éléments sont accessibles à l’aide d’une flèche ->,
par exemple :
29
LPC_GPIO0->FIODIR = xxxxx ;

Si vous avez suivi ce qui précède, cette ligne sera convertie par le précompilateur en :

((LPC_GPIO)(0x2009C000))->FIODIR = xxxx ;

Donc, comme nous l’avons indiqué, aucune variable n’est utilisée.

Et étant donné que la structure est évidemment connue et que FIODIR est le premier
registre de cette structure, ça donnera au final :

*0x2009C000 = xxxx ;

Et donc une simple affectation d’une valeur à un registre dont l’adresse est connue.

Remarquez le point important suivant, sachant que nous avons plusieurs structures GPIO
possibles :

- Dans les fichiers fournis le numéro du port est renseigné au niveau de la base
(LPC_GPIO0)

- Dans le datasheet, le numéro est renseigné au niveau du registre lui-même, et donc


dans notre cas « FIO0DIR ». En effet, la structure n’existe pas en tant que telle au
niveau de l’ARM, même si ses concepteurs ont veillé à ce que soit directement
transposable comme tel.

Vous retrouverez cette façon de procéder partout dans vos projets, c’est important de
comprendre la philosophie si vous voulez retrouver aisément les structures correspondant à ce
que vous allez découvrir dans le datasheet.

Le registre FIODIR est le premier que nous trouvons dans la structure, l’adresse de la
structure est donc également celle de FIODIR.

Si vous examinez le datasheet de l’ARM, vous allez trouver ceci :

Nous avons bien notre registre FIO0DIR, portant le nom générique FIODIR, qui est se
trouve à l’adresse 0x2009C000. Au passage, ne confondez par la lettre « O » et le chiffre
« 0 ».

Je vous ai montré la démarche pour comprendre un code déjà écrit. Lorsque vous allez
créer votre propre code, vous procéderez de façon inverse :
30
- Le datasheet vous fourni un nom de registre
- Vous enlevez les chiffres de ce nom de registre
- Vous recherchez dans les fichiers fournis (principalement « LPC17xx.h ») ce registre
- Vous trouvez dans quelle structure il est déclaré, et de quelle façon.
- Vous obtenez ensuite la méthode pour y accéder.

Notez que tout étant déjà (ou presque) déclaré, vous n’avez plus du tout à vous préoccuper
des adresses. Vous voulez accéder au registre FIO0DIR ? Vous tapez :

LPC_GPIO0->FIODIR

C’est FIO1DIR qui vous intéresse ? Simple :

LPC_GPIO1->FIODIR

Une fois compris le mécanisme, l’accès aux registres sera un jeu d’enfant. Ajoutez qu’au
fur et à mesure vous saurez par cœur à quoi accéder, et qu’en plus vous avez des exemples
concrets dans les librairies.

Notez que notre structure contient la particularité suivante :

union {
__IO uint32_t FIODIR;
struct {
__IO uint16_t FIODIRL;
__IO uint16_t FIODIRH;
};
struct {
__IO uint8_t FIODIR0;
__IO uint8_t FIODIR1;
__IO uint8_t FIODIR2;
__IO uint8_t FIODIR3;
};

FIODIR est inclus dans une union. L’union précise qu’au même emplacement se trouvent
des éléments différents. Dans le cas qui nous occupe, nous avons soit :

- Le registre unique FIODIR, de 32 bits


- Deux registres de 16 bits : FIODIRL et FIODIRH
- Quatre registres de 8 bits : FIODIR0,FIODIR1,FIODIR2 et FIODIR3

Nous avons donc trois façons différentes d’accéder au même registre : Soit en lisant ou en
écrivant 32 bits dans FIODIR, l’intégralité du registre de direction, soit en accédant seulement
à la moitié d’un port sans modifier l’autre moitié, soit en accédant au registre de direction par
groupe de 8 bits, correspondant évidemment à 8 pins GPIO.

Ceci se retrouve évidemment dans le datasheet au niveau des possibilités de l’ARM.


En effet, il ne suffit pas d’écrire cette possibilité dans une structure pour que ça fonctionne, il
faut que le processeur accepte cette façon de procéder :

31
Remarquez que dans notre structure ainsi que dans le datasheet, le registre 32 bits FIODIR
se retrouve scindé dans l’ordre poids faible/poids fort, puisque nous travaillons en little-
endian. Nous avons donc d’abord FIODIR0 (bits 7/0 de FIODIR) situé à la même adresse que
FIODIR et on termine avec FIODIR3 (bits 31/24 de FIODIR), situé 3 adresses plus loin.

Après cette longue parenthèse, revenons à notre programme :

LPC_GPIO0->FIODIR = 0x00280000; // LCD_DIR et LCD_EN en sortie

Vous avez maintenant compris (sinon, relisez ce qui précède) que nous allons mettre la
valeur 0x00280000 dans le registre FIO0DIR, ce qui donne en binaire :

0000 0000 0010 1000 0000 0000 0000 0000

32
Comprenez au passage que ce n’est pas parce que l’ARM stocke les valeurs en RAM en
little-endian que les valeurs vont être inversées dans le datasheet : Un entier de 32 bits reste
un entier de 32 bits, avec poids fort à gauche et poids faible à droite.

Nous allons, avec cette instruction, placer la pin P0.19 et P0.21 en sortie. Sur un ARM,
placer « 1 » dans le registre de direction configure la pin en sortie, la valeur au reset valant
« 0 ». C’est le contraire des PIC®. La philosophie reste cependant identique : On a un registre
de configuration du sens de fonctionnement et après un reset les pins sont en entrée.

Ensuite, nous allons placer les valeurs de ces deux pins en accédant au registre FIOPIN.

LPC_GPIO0->FIOPIN |= 0x00200000; // Transceiver en entrée, latch hors-service

On utilise un « |= » pour forcer à 1 uniquement la pin P0.21, les pins étant initialisées à 0
au reset. On agit à l’initialisation, on aurait pu utiliser un « = », mais bon…

Notez que vous pourriez forcer cette pin à 1 également en n’accédant qu’au registre
FIOPIN2, FIOPIN étant scindé exactement comme FIODIR, comme nous l’apprend le datasheet
et notre structure LPC_GPIO:

LPC_GPIO0->FIOPIN2 |= 0x20;

Vous disposez également de la possibilité de forcer une pin à 1 avec FIOSET ou à 0 avec
FIOCLR, scindés également de la même façon:

LPC_GPIO0->FIOSET2 = 0x20;

Enfin, vous pouvez également établir des groupes de sorties avec FIOMASK : Une fois ce
registre configuré les opérations d’écriture n’affectent que les sorties dont le bit correspondant
dans FIOMASK est placé à 0 (au reset la valeur de FIOMASK est 0 et donc toutes les pins sont
accessibles). Attention à la logique : « 0 » signifie « non masquée » et donc « accessible » et
« 1 » signifie « masquée » et donc « invariante ».

LPC_GPIO0->FIOMASK = ~0x00280000; // On n'agit que sur P0.19 et P0.21


LPC_GPIO0->FIOPIN = 0x00200000; // P0.21 = 1, P0.19 = 0
LPC_GPIO0->FIOMASK = 0; // rétablir accès total

Évidemment ça n’a pas grand intérêt dans ce cas précis, mais ça peut se révéler très
pratique dans certains cas. Le « ~ » vous évite de vous casser la tête pour inverser
0x00280000 afin d’avoir les bits 19 et 21 à 0 et les autres à 1.

Le masque est pratique pour vous éviter de faire des « And » et des « Or » lorsque vous
manipulez des groupes de pins (par exemple des pins accédant à des périphériques, un
ensemble de Leds etc.). Vous écrivez ensuite la valeur à obtenir dans FIOPIN (ou FIOSET ou
FIOCLR ou les registres scindés) et l’écriture n’affecte que les pins définies dans le masque.

33
Attention, n’oubliez pas de remettre le masque à 0 après utilisation, sinon toute librairie
ou fonction ultérieure qui utiliserait le port n’agirait plus que sur les pins non masquées. Mes
librairies partent du principe qu’on les appelle avec les pins non masquées, sans quoi j’aurais
été obligé de reseter les masques partout et majoritairement de façon parfaitement inutile.

Vous pouvez conserver un masque pour toute une application, mais uniquement si aucune
pin masquée n’est utilisée pour ce port dans une librairie que vous référencez (ceci dépend du
hardware que vous gérez dans votre application).

Notez que lorsque vous voulez agir sur un bit, plutôt que de vous amuser à calculer quelle
est sa valeur hexadécimale, vous pouvez demander au précompilateur de calculer cette valeur
pour vous. Ainsi :

LPC_GPIO0->FIOPIN = 0x00200000 // bit 21 à 1

Peut avantageusement s’écrire:

LPC_GPIO0->FIOPIN = 1UL<<21 // bit 21 à 1

C’est le précompilateur qui calcule ça au moment de la compilation, pas l’ARM au


moment de l’exécution. Ça n’augmente pas la taille du code et ne consomme aucun temps
CPU, le code produit est identique.

Le « UL » pour être rigoureux et indiquer qu’on travaille sur 32 bits, mais vous pouvez ne
pas l’écrire (1<<21)

La fonction d’initialisation se termine avec un exemple :

// APPLICATION
// -----------
LPC_GPIO2->FIODIR0 = 0xFF; // P2.0 -> P2.7 en sortie (Leds)
LPC_GPIO1->FIODIR &= 0xC1FFFFFF; // P1.25 -> P1.29 en entrée (Joystick)

Nous décidons de faire clignoter la Led LD4. Nous devons donc jeter un œil au schéma
pour voir comment y accéder :

34
LD4 se pilote avec la pin DB00, multiplexée avec d’autres fonctions, qui ne nous
intéressent pas pour l’instant (parce que j’ai vérifié). La Led est attaquée par un driver
74LV244PW. L’étude de son datasheet (désolé) montre que pour allumer la Led :

- Il faut un niveau bas sur l’entrée 2G


- Il faut un niveau haut sur l’entrée 2A4

Eh oui, vous devez étudier tous les datasheets de tous les composants, mais dans mes
librairies c’est déjà fait, ce qui vous épargne bien du travail.

Imposer un niveau bas sur 2G s’effectue en mettant en place le jumper JP8, ce que vous
devez faire (par défaut il est déjà en place, il se trouve près du haut-parleur.

Imposer un niveau haut sur 2A4 s’effectue en envoyant un niveau haut sur la pin DB07,
qui est en réalité au niveau de l’ARM la pin P2.7.

Modifions donc notre routine d’initialisation pour placer P2.7 en sortie :

// APPLICATION
// -----------
LPC_GPIO2->FIODIR0 = 0x80; // P2.7 en sortie

35
Il nous reste à faire clignoter notre Led. Comme nous voulons la faire clignoter à 1Hz (on
ne change pas une équipe qui gagne), nous allons avoir, dans notre fonction « main() » un
code du genre :

Début boucle
Allumer Led
Attendre 500ms
Éteindre Led
Attendre 500ms
Boucler

Que nous pouvons simplifier en :

Début boucle
Inverser Led
Attendre 500ms
Boucler

Nous avons donc besoin d’une temporisation de 500ms.

Attention : La chronologie des instructions dans un ARM est beaucoup plus complexe que
dans un PIC® 8 bits : Non seulement ça dépend du contexte, des instructions utilisées, mais de
plus du type de mémoire où est stocké le programme, sans compter que le nombre de cycles
dépend de la vitesse de la Flash interne comparée à l’horloge de l’ARM. Oubliez le comptage
d’instructions « nop » ou équivalant !

Dans les librairies d’exemple fournies avec la carte vous trouverez plein de
« temporisations » effectuée à l’aide de boucles « for ». Vous devez absolument éviter de
travailler de cette façon barbare, on ne programme pas un micro de ce type de cette façon,
d’autant plus en langage évolué où le code produit dépend de plus du compilateur.

Vous avez des timers, vous devez vous en servir. Mais pour vous simplifier la tâche, j’ai
explicitement créé une librairie vous permettant d’effectuer des temporisations softwares sur
base d’un des timers.

Notez bien que, exactement comme pour les Pic®, la temporisation software donne un
temps minimal : Il y a une très légère incertitude (en ns) du fait de la boucle de contrôle dans
la fonction d’attente, mais surtout un allongement imprévisible de ce temps si les
interruptions sont en service et qu’une interruption intervient durant le délai d’attente.

Mes librairies ne sont pas réentrantes : N’utilisez donc pas les mêmes fonctions à la fois
dans une interruption et dans le programme principal, sous peine de problèmes sérieux
aléatoires. N’utilisez pas non plus les appels à partir de deux interruptions de priorités
différentes. Ceci se résout cependant de façon assez simple, d’autant qu’il y a possibilité de
déclencher une interruption software.

Bref, vous avez besoin d’une librairie de temporisation et je vous ai fait référencer la
librairie « Timing » dans votre projet. Déployez « Timing.c » dans votre explorateur de projet
(au besoin, compilez une fois avec <F7>, puis double-cliquez que le fichier « Timing.h ».
36
Dans la zone « Prototypes », vous trouvez les fonctions qui vous sont accessibles. La
philosophie de toutes mes librairies est identique : En général vous allez trouver une fonction
d’initialisation contenant « Initialize » et préfixée, comme toutes les fonctions d’une même
librairie, d’un terme évoquant la librairie (ici : « Timing »), suivit d’un symbole de surlignage.

Nous trouvons ici la fonction « Timing_InitializeTimer() » :

//----------------------------------------------------------------------------
// SÉLECTIONNE LE TIMER DES TEMPORISATIONS ET LE CONFIGURE
// Si cette fonction n'est pas appelée avant l'utilisation des temporisations,
// le TIMER0 sera configuré par défaut
// numTimer: numéro du timer, de TIMER0 à TIMER3
//----------------------------------------------------------------------------
extern void Timing_InitializeTimer(int numTimer);

La méthode prend une valeur en paramètre : Le numéro du timer, de TIMER0 à TIMER3.


Ces constantes sont du reste déclarées au début du fichier « Timing.h » et vous sont
accessibles à partir de votre programme principal :

// Numéro des timers


// ------------------
#define TIMER0 0
#define TIMER1 1
#define TIMER2 2
#define TIMER3 3

Évidemment, si vous voulez que votre programme principal prenne connaissance de ces
déclarations, vous devez inclure le fichier « Timing.h » en début de code :

#include "LPC17xx.h" // toujours présent


#include "LPC17xx_Bits.h" // toujours présent
#include "Timing.h" // accès à la librairie Timing.c

Nous allons de ce fait inclure l’initialisation de notre librairie dans notre séquence
d’initialisation :

int main(void)
{
SystemInit(); // initialise horloge : INDISPENSABLE
InitializeGPIO(); // initialise GPIO
Timing_InitializeTimer(TIMER0); // Initialiser timer pour tempos

while(1) // boucle principale


{
}
}

Vous avez le choix du timer utilisé mais vous ne pouvez pas utiliser ce timer pour autre-
chose une fois initialisé. Prenez acte du fait que plusieurs de mes librairies utilisent des
temporisations basées sur cette librairie, je vous conseille de toujours référencer Timing.c et
de commencer par l’appel de la méthode précédente avant d’initialiser les autres librairies.
Ceci vous donne le contrôle sur le timer qui sera effectivement utilisé.

37
Comme indiqué dans les commentaires, si vous utilisez cette librairie sans appeler la
méthode d’initialisation, c’est le timer0 qui sera utilisé automatiquement. Ceci vaut
évidemment si vous initialisez une librairie qui utilise les temporisations sans lui fournir le
numéro du timer.

Dans le cas présent vous pouviez vous passer d’appeler la fonction d’initialisation,
puisque nous utilisons le timer0. Cependant vous n’y gagneriez strictement rien, la méthode
étant appelée en interne automatiquement lors de la première temporisation, et de plus vous y
perdez en lisibilité.

Pour réaliser une temporisation effective, vous avez deux méthodes à votre disposition :

//--------------------------------------------------------------------------------
// ATTEND LA DURÉE PRÉCISÉE EN µs
// time : nombre minimum de µs d'attente, de 0 à 0xFFFFFFFF
//--------------------------------------------------------------------------------
extern void Timing_WaitUs (uint32_t time);

//--------------------------------------------------------------------------------
// ATTEND LA DURÉE PRÉCISÉE EN ms
// time : nombre minimum de ms d'attente, de 0 à 400.000.000
//--------------------------------------------------------------------------------
extern void Timing_WaitMs (uint32_t time);

Si vous passez « 0 » comme valeur vous obtiendrez un temps correspondant à l’appel et


au retour de la fonction, indéterminé mais très court, de l’ordre de quelques dizaines de
nanosecondes.

Nous avons besoin d’une temporisation de 500ms, nous allons donc utiliser la fonction
« Timing_WaitMs() » .

Le temps maximal qu’il est possible d’atteindre avec la librairie est de 0xFFFFFFFF µs, soit
4294967295 µs, soit plus de 70 minutes avec une résolution de 1µs paramétrée lors de
l’initialisation.

Les timers en eux-mêmes permettent de descendre à une résolution de 10ns (1/100ème de


µs) avec une durée totale maximale de l’ordre de 43 secondes. Je parle en temps d’attente
simples. En mesure de temps vous avez une précision de 10ns avec une durée maximale de 55
jours.

À l’inverse, ils permettent des temps d’attente de plus 1475739525896 secondes avec une
résolution de l’ordre de 343secondes. Ceci vous permet des délais allant jusque 46795
années. L’utilisation de timers en 32 bits vous permet de travailler pratiquement sans limites.

Vous disposez de plus, avec ces timers, de la possibilité de détecter des durées
intermédiaires.

Notre délai de 500ms se résume donc à sa plus simple expression :

Timing_WaitMs(500); // attendre 500ms

38
Il ne reste plus qu’à inverser notre Led, et donc d’inverser notre pin P2.7. Vous avez
plusieurs méthodes, j’ai choisi l’accès à FIOPIN0. En replaçant le « 1 » du « while » par
« true », ce qui est plus propre, nous obtenons :

int main(void)
{
SystemInit(); // initialise horloge : INDISPENSABLE
InitializeGPIO(); // initialise GPIO
Timing_InitializeTimer(TIMER0); // Initialiser timer pour tempos

while(true) // boucle principale


{
Timing_WaitMs(500); // attendre 500ms
LPC_GPIO2->FIOPIN0 ^= 0x80; // inverser LED
}
}

Lancez la compilation, vous ne devriez obtenir aucune erreur ni warning.

3.2. Chargement d’un programme dans l’ARM

Nous avons réalisé notre premier programme, il nous faut maintenant l’envoyer dans
l’ARM, afin de vérifier que tout est bien fonctionnel. Si vous suivez ce manuel dans l’ordre,
vous avez déjà installé les drivers de la sonde J-Tag, sinon faites-le.

Reliez maintenant la sonde J-Tag à un port USB de votre PC. Attention, je rappelle que le
port USB de la sonde J-Tag de la carte LandTiger® est celui situé à côté du connecteur noir « J-
Tag », et non celui situé près de l’interrupteur d’alimentation. Allumez votre carte :

Vous devriez avoir un message de confirmation de reconnaissance de périphérique. Vous


pouvez également obtenir ce message, ne vous en préoccupez pas :

Nous allons maintenant paramétrer notre sonde J-Tag. Pour ce faire, ouvrez la fenêtre de
paramétrage du projet, par exemple à l’aide du bouton de la barre d’outils à droite du nom du
projet. Allez ensuite dans l’onglet « Utilities » :

39
Nous allons commencer par choisir notre outil de communication, et donc notre sonde
J-Tag. Dans le menu déroulant, choisissez « Cortex-M/R J-LINK/J-Trace » :

Cliquez ensuite sur le bouton <Settings>. Configurez l’onglet « Flash Download »


comme suit:

40
Ceci vous permet de démarrer l’application une fois le programme chargé sans devoir
presser le bouton « Reset », et efface les secteurs utilisés avant programmation.

Cliquez maintenant sur le bouton <Add> et sélectionnez « LPC17xx IAP 512KB Flash » :

Sélectionnez maintenant l’onglet « Debug » :

41
Selon la configuration par défaut, il se peut que vous entendiez un « clic » dans le haut-
parleur, que votre carte resette ou que vous receviez une longue série de messages d’erreurs.
Dans ce dernier cas, validez tout sans vous en préoccuper.

La première chose qui est importante à configurer, c’est le type de port. En principe vous
devriez avoir « JTAG » par défaut, mais, contrairement à ce qu’on pourrait penser, vous devez
sélectionner « SW ». Une fois que c’est fait, le nom de votre sonde devrait apparaître dans le
cadre « JTAG Device Chain ». Configurez pour obtenir ceci (sauf le n° de série) :

42
Si vous obtenez des messages d’erreur, baissez la fréquence. Chez moi ça fonctionne
jusque 2Mhz mais ça dépend d’une série de paramètres. Voici le type de messages que vous
pourriez recevoir avec une vitesse trop importante (en deux séries d’exemplaires concernant
chaque registre, ça fait du mode à valider) :

Vous pouvez maintenant envoyer votre programme dans l’ARM, en utilisant l’item
« Download » du menu « Flash ». Vous connaissez maintenant la formule d’usage :

Félicitations, votre Led clignote maintenant à 1Hz !

Notez que les autres Leds restent allumées. C’est du au fait que les pins correspondantes
sont configurées en entrée, et donc sont considérées à l’état « 1 » par le driver de Leds.

Ceci ne fait pas très « propre », aussi nous allons éteindre les Leds inutilisées. La
modification est très simple :

Plutôt que de placer P2.7 en sortie :

LPC_GPIO2->FIODIR0 = 0x80; // P2.7 en sortie

Nous plaçons les 8 pins correspondant aux 8 Leds, P2.0 à P2.7 :

LPC_GPIO2->FIODIR0 = 0xFF; // P2.0 à P2.7 en sortie

Les sorties sont à 0 au reset, la boucle n’inverse que P2.7, il n’y a rien d’autre à modifier.
Compilez, envoyez en Flash, et vérifier que ça fonctionne comme prévu.

3.3. Accès simultané à l’écran GLCD

Nous allons maintenant améliorer notre programme pour lui faire afficher des
informations sur l’écran graphique couleur. Ceci va nous permettre d’appréhender quelques
notions intéressantes.

Je vous ai déjà fait référencer la librairie « GLCD.c » dans votre groupe « List ». Ajoutez
maintenant évidemment en début de programme l’inclusion du fichier de définition
correspondant : « GLCD.h »

#include "LPC17xx.h" // toujours présent


#include "LPC17xx_Bits.h" // toujours présent
#include "Timing.h" // accès à la librairie Timing.c

43
#include "GLCD.h" // accès à la librairie graphique

Comme la plupart de mes librairies, nous devons appeler la fonction d’initialisation. Un


coup d’œil sur le fichier « GLCD.h » nous apprend que cette fonction, qui ne prend aucun
paramètre, s’appelle « GLCD_Initialize() » :

//----------------------------------------------------------------------------------------
// INITIALISE ET ALLUME L'AFFICHEUR LCD
// La méthode Timing_InitializeTimer() doit avoir été appelée, sinon le timer0 sera utilisé
// par défaut pour générer les délais
// Ne prend en charge que les afficheurs pilotés par le driver SSD1289
// return: false si échec
//----------------------------------------------------------------------------------------
extern Boolean GLCD_Initialize (void); // Initialise l'afficheur

Attention, comme indiqué la librairie ne prend en charge que les drivers de type
SSD1289. Il semble que certaines cartes soient équipées d’un driver différent. Vérifiez la
valeur de retour de GLCD_Initialize() pour vérifier qu’il n’y a pas eu de problème.

Si cette méthode retourne « false », vous avez deux possibilités :

- Soit vous modifiez la librairie pour tenir compte de ce driver, auquel cas pensez à
m’envoyer la version modifiée

- Soit vous m’envoyez un message pour me signaler le problème. Attention, je ne sais


pas travailler sur des cartes que je ne possède pas, pensez-donc à m’envoyer un
exemplaire de la carte munie du driver en question.

Nous allons créer une fonction dans notre programme, appelée « InitializeGLCD() » :

//////////////////////////////////////////////////////////////////////////////////////////
// INITIALISE LE DRIVER GLCD //
//////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------
// Initialise le driver et l'écran GLCD.
// En cas de problème, bloque pour empêcher le clignotement de la LED
//----------------------------------------------------------------------------------------
void InitializeGLCD(void)
{

}
, que nous appellerons à partir de notre fonction « main() » :

int main(void)
{
SystemInit(); // initialise horloge : INDISPENSABLE
InitializeGPIO(); // initialise GPIO
Timing_InitializeTimer(TIMER0); // Initialiser timer pour tempos
InitializeGLCD(); // Initialiser GLCD

while(true) // boucle principale


{
Timing_WaitMs(500); // attendre 500ms
LPC_GPIO2->FIOPIN0 ^= 0x80; // inverser LED
}
}

44
Pour que le tout fonctionne sans problème, nous ajouterons le prototype dans la zone
concernée :

//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
// FONCTIONS PROTOTYPES //
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////

void InitializeGPIO(void); // Initialiser GPIO


void InitializeGLCD(void); // initialiser écran

Intéressons-nous au contenu de notre fonction. Tout d’abord nous devons initialiser


l’écran et récupérer le résultat. Si ce résultat est « false », nous n’allons pas plus loin et
bloquons le programme, non sans maintenir la Led LD11 allumée pour informer l’utilisateur
d’un problème.

Puisque nous ajoutons des lignes à notre fichier, j’en profite pour vous signaler qu’un
fichier dans µVision® doit toujours avoir au moins une ligne vide en fin de fichier, sinon vous
obtiendrez un warning à la compilation indiquant que la dernière ligne utile d’un des fichiers
se termine sans un saut de ligne.

Bref, nous avons quelque chose du genre :

void InitializeGLCD(void)
{
if (!GLCD_Initialize()) // initialiser l'écran
{ // si échec
LPC_GPIO2->FIODIR0 = 0xFF; // remettre pins en sortie
LPC_GPIO2->FIOPIN = 0x01; // P2.0 = 1
while (true); // on arrête là
}

Ne vous inquiétez pas pour la ligne mettant les pins en sortie, vous comprendrez bientôt
pourquoi.

Ensuite, nous allons placer un message qui ne bougera plus, et donc qu’on peut mettre au
moment de l’initialisation. Nous obtenons ce pseudo-code :
Début
Effacer l’écran avec une couleur précisée
Indiquer les couleurs de fond et de texte
Écrire le ou les messages
Fin

Les couleurs sont définies dans un mot de 16 bits, avec la répartition suivante :

b15/b11 : Niveau du rouge sur 5 bits


b10/b6 : Niveau du rouge sur 5 bits
b5/b0 : Niveau du bleu sur 6 bits

Vous trouverez une série de couleurs déjà définies au début du fichier « GLCD.h »

45
Nous allons placer un premier texte en bleu au-dessus de l’écran : « Tutoriel LedSoft », et
un second en noir au milieu : « Compteur : 000 ». L’écran sera sur fond jaune.

Vous pouvez avoir accès à plus de couleurs mais alors vous devez modifier la librairie ou
utiliser la vôtre. J’ai estimé que des couleurs 16 bits étaient parfaites pour la plupart des
applications.

D’après les fonctions disponibles dans « GLCD.h », tout ceci nous donne :

GLCD_Clear(YELLOW); // Effacer l'écran


GLCD_SetTextColors(YELLOW,BLUE); // couleurs de texte: Bleu sur fond jaune
GLCD_Print(32,8,"Tutoriel LedSoft"); // Texte à afficher
GLCD_SetTextColor(BLACK); // texte en noir
GLCD_Print(16,96,"Compteur: 000"); // compteur

Compilez déjà votre programme puis chargez-le dans votre carte. Vous devriez obtenir
ceci :

Si par malheur votre écran reste vierge et votre Led LD11 allumée, votre driver d’écran
n’est pas géré par la librairie. Procédez comme précédemment expliqué.

Sinon, l’écran indique bien nos messages mais toutes les Leds sont allumées et aucune
ne clignote. Tout d’abord, notez que :

- L’affichage s’effectue avec l’écran en mode « paysage »


- Les dimensions sont 320 pixels de large sur 240 pixels de haut
- La fonte utilisée fait 16 pixels de large et 24 de haut (donc 10 lignes de 20 caractères)
- Les espaces entre caractères (hauteur et largeur) sont intégrés à la fonte

L’écran utilise la police « Font_Regular_24_16 » qui est incluse automatiquement si vous


utilisez la librairie « GLCD.c ». C’est une fonte que je vous ai construite, elle est adaptée à
partir de fontes open-source et manipulée pour une utilisation simple au niveau de l’écran.

46
Elle est à espacement fixe pour plus de simplicité. Elle ne comporte pas de caractères
accentués majuscules. Vous pouvez l’éditer pour y ajouter les caractères que vous voulez.

Pour comprendre pourquoi nos Leds ne fonctionnent plus, il suffit de regarder le schéma
de la carte. Souvenez-vous que nos Leds sont pilotées par les lignes DB00 à DB07 selon la
nomenclature du schéma. Vous devez donc chercher si ces lignes ne sont pas utilisées pour
une autre fonctionnalité, et c’est le cas :

Constatez que ces pins sont utilisées pour accéder au driver d’écran GLD, via un circuit qui
permet, en fait, de construire des mots de 16 bits à partir de 8pins, ainsi que d’assurer la
lecture et l’écriture dans le driver à partir des mêmes lignes.

Si vous voulez comprendre comment ça fonctionne, il vous faut lire le datasheet de tous
les composants de cette portion de schéma, ou lire le contenu de ma librairie.

Il est cependant assez intuitif de comprendre que, puisque nous avons accédé à l’écran,
que non seulement nous avons modifié les pins P2.0 à P2.7, mais qu’en plus ces pins passent
alternativement d’entrée en sortie selon les opérations réalisées au niveau de l’écran.

Tout accès à l’écran perturbe donc le fonctionnement des Leds. Autrement dit, nous
devons remettre les Leds dans la bonne configuration après chaque modification de
l’écran. Nous pourrions nous contenter d’initialiser la Led après l’initialisation de l’écran,
mais dans une application réelle l’écran sera très probablement modifié plusieurs fois.

Nous allons donc créer une fonction d’allumage/extinction de notre Led :

//////////////////////////////////////////////////////////////////////////////////////////
// ALLUMAGE/EXINCTION DE LA LED //
//////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------
// Allume ou éteind la led
// bright : true si allumage, false si extinction
//----------------------------------------------------------------------------------------
void SetLed(Boolean bright)
{
47
LPC_GPIO2->FIOPIN0 = (bright)? 0x80 : 0x00; // allumage/Extinction
LPC_GPIO2->FIODIR0 = 0xFF; // pin en sortie
}

On met d’abord la bonne valeur et puis on passe en entrée, ça évite les états intermédiaires
indésirables. Notez la syntaxe de la première ligne avec l’affectation sur condition.

Vous devez maintenant ajouter la fonction prototype dans la zone prévue, et donc :

void SetLed(Boolean bright); // allumage/Extinction Led

Maintenant, évidemment, la configuration en entrée/sortie de la Led est faite à chaque


allumage, on peut donc supprimer l’initialisation de la Led dans la routine « InitializeGPIO() ».

Il nous reste à remplacer l’ancienne ligne d’allumage de notre boucle dans « main() »…

LPC_GPIO2->FIOPIN0 ^= 0x80; // inverser LED

…par un appel de la fonction que nous venons d’écrire. Le problème est que nous devons
indiquer si on allume ou on éteint, et que l’inversion n’est plus possible vu que la pin a été
modifiée par notre librairie « GLCD.c ».

Il nous faut donc une variable qui va mémoriser l’état actuel de la Led. Cette variable peut
être déclarée dans le « main() », vu qu’elle ne sert qu’à cet endroit :

int main(void)
{
Boolean ledOn = false; // état actuel de la led

SystemInit(); // initialise horloge : INDISPENSABLE


InitializeGPIO(); // initialise GPIO
Timing_InitializeTimer(TIMER0); // Initialiser timer pour tempos
InitializeGLCD(); // Initialiser GLCD

while(true) // boucle principale


{
Timing_WaitMs(500); // attendre 500ms
ledOn = !ledOn; // inverser statut led
SetLed(ledOn); // inverser Led
}
}

Il y a évidemment, en informatique, toujours plusieurs façons de procéder, l’important est


que vous compreniez les pièges.

Compilez le programme, celui-ci fonctionne maintenant comme prévu.

Nous allons maintenant faire incrémenter le compteur affiché à l’écran une fois par
seconde. Nous ajoutons donc une variable « compteur » de type « unsigned char » dans
notre fonction « main() » et nous l’initialisons à 0. Vous constatez que notre librairie GLCD
contient une fonction « Print_Val8 », qui permet d’afficher en décimal la valeur d’une

48
variable « unsigned char », avec en paramètre les coordonnées x et y (en pixels) et le nombre
minimal de chiffres à afficher.

Notre fonction d’initialisation de l’écran affichait le message « Compteur : 000 » aux


coordonnées (16,96). Sachant qu’un caractère fait 16 pixels de large, et sachant que nous
devons afficher le compteur à la place des 3 zéros, nous devons afficher la valeur 10
caractères plus loin que le « C » de « Compteur » et donc en 16 + (10*16) = 176. L’ordonnée
reste évidemment à 96, vu qu’on écrit horizontalement.

Pour éviter d’effacer les caractères excédentaires (il ne s’agit que d’un exercice), nous
afficherons systématiquement 3 chiffres (de 000 à 255), nous passons dès lors la valeur « 3 »
comme nombre de caractères minimal.

Ceci nous donne :

int main(void)
{
Boolean ledOn = false; // état actuel de la led
unsigned char compteur = 0; // compteur

SystemInit(); // initialise horloge :


InitializeGPIO(); // initialise GPIO
Timing_InitializeTimer(TIMER0); // Initialiser timer pour tempos
InitializeGLCD(); // Initialiser GLCD

while(true) // boucle principale


{
Timing_WaitMs(500); // attendre 500ms
if (ledOn) // si allumage
GLCD_Print_Val8(176,96,compteur++,3); // actualiser compteur

ledOn = !ledOn; // inverser statut led


SetLed(ledOn); // inverser Led
}
}

Constatez qu’avec l’utilisation des librairies, c’est très simple. Jetez un œil à la librairie
pour voir le code qui est réellement utilisé : Ce n’est pas tellement le langage C qui simplifie
le code dans un microcontrôleur, ce sont les librairies fournies.

Comme quoi, étudier un microcontrôleur à partir du langage C masque le fait qu’on étudie
plus les librairies que le microcontrôleur, j’essaye de ne pas tomber dans ce piège en vous
montrant plutôt les subtilités que vous devez garder à l’esprit.

Compilez et chargez le programme. Tout se passe parfaitement : Le compteur est


incrémenté, la Led clignote, sauf que, sauf que….

À chaque modification de l’afficheur les Leds clignotent brièvement car les pins P2.0 à
P2.7 sont utilisées pour accéder à l’affichage. Or, ce sera le cas pour tous vos programmes
utilisant à la fois les Leds et l’afficheur. Ce serait pourtant dommage de se passer de l’un ou
de l’autre.

49
Comment résoudre ce problème ? L’idéal serait de couper brièvement les Leds lorsqu’on
accède à l’afficheur. En effet, autant un allumage bref de l’intégralité des Leds est
particulièrement visible, autant une courte extinction des Leds allumées passe complètement
inaperçue.

Malheureusement, il y a tellement de fonctionnalités embarquées sur cette carte que ce cas


n’est pas prévu. Vous allez voir cependant qu’il suffit d’un brin d’imagination pour résoudre
ce problème.

3.4. L’activation software des Leds

Vous disposez d’un jumper JP8 pour activer ou désactiver les Leds, et donc vous vous
retrouvez à devoir choisir une des deux possibilités suivantes :

- Soit vous désactivez les Leds.

- Soit vous les activez et en plus des fonctions prévues elles vont brièvement « flasher »
à chaque accès à l’écran.

Si une de ces solutions vous convient, vous n’avez rien de plus à faire, sauf décider si
vous enlevez (Leds désactivées) ou laissez (Leds activées) le jumper.

Il existe cependant une façon de procéder plus élégante : Il suffit d’utiliser une des GPIO
de l’ARM pour piloter l’activation des Leds, en lieu et place du jumper JP8. Il nous faut
commencer par trouver une pin qui soit bonne candidate, c’est-à-dire qui ne soit pas déjà
utilisée pour une autre fonctionnalité qui pourrait interférer.

J’ai choisi la pin P2.11, qui est utilisée pour lire l’état du bouton-poussoir KEY1 (celui le
plus éloigné des Leds). Ceci entraîne les conséquences suivantes :

50
- En cas d’action sur le bouton-poussoir KEY1, les Leds sont actives durant l’appui

- On ne peut jamais mettre un niveau « 1 » sur P2.11 sans quoi l’appui sur KEY1
réaliserait un court-circuit sur cette pin

La première restriction n’a pas d’impact du tout si on n’utilise pas KEY1, et a un impact
minime si on l’utilise et uniquement durant le temps de l’appui (on retrouve le
fonctionnement normal).

La seconde restriction est aisée à contourner : Vu qu’il y a une résistance de rappel au


+5V sur les entrées « 1G » et « 2G » du driver 4LV244PW, il suffit de placer P2.11 en entrée
pour valider un niveau haut.

Dit autrement :

- Pour valider les Leds on mettra P2.11 en sortie avec un niveau 0

- Pour invalider les Leds, on mettra P2.11 en entrée

Bien évidemment il nous faut maintenant relier P2.11 à nos entrées d’activation des Leds
sur notre driver. Afin de vous offrir une méthode réversible et n’endommageant strictement
rien sur votre carte, j’ai procédé comme suit :

Étape 1 : Vous soudez une pin de connecteur sur P2.11, qui est reprise sur le connecteur
situé à côté de l’écran, côté HP. Vous pouvez enlever l’écran pour plus d’aisance :

51
Ensuite vous enlevez le jumper JP8 et vous y soudez un fil.

Et enfin, vous soudez l’autre extrémité de ce fil à la base de la pin côté HP où se trouvait
pluggé JP8.

Vous obtenez ceci :

À partir d’ici, vous avez 3 possibilités :

- Soit vous pluggez le jumper sur P2.11 comme sur la photo ci-dessus et vos Leds sont
activées par software.

- Soit vous laissez le jumper en l’air et vos Leds sont désactivées

- Soit vous remettez le jumper dans sa position initiale et vos Leds sont activées comme
avant (voir photo ci-dessous)

52
Vous obtenez ainsi toutes les possibilités de pilotage de vos Leds.

Voyons le côté software. Nous devons désactiver nos Leds avant toute modification
d’écran, et les réactiver après en avoir terminé. Nous allons modifier notre programme
comme suit :

Tout d’abord, nous modifions notre fonction « SetLeds() ». Nous ne lui passerons plus en
paramètre l’état de la Led (allumée ou éteinte) mais l’état d’activation des leds (activées ou
désactivées). Nous ajoutons évidemment la gestion de la pin P2.11. Vous pouvez aussi choisir
de passer deux paramètres (activation et valeur) ou utiliser une valeur spécifique (-1) pour
désactiver etc. Il existe des tas de façons de réaliser un programme, notre but n’est pas
d’optimiser mais d’être didactique.

Notre fonction sera valable quel que soit le nombre de Leds utilisées. Pour savoir quelles
Leds allumer ou éteindre, nous utiliserons la variable globale « _leds ». Faites précéder les
noms de variables globales par un « underscore », ceci indiquera à l’utilisateur que la
variable est globale.

Nous obtenons un pseudo-code du genre :

Fonction SetLeds
Si demande d’activation
On copie la variable _leds dans P2.0 à P2.7
On met P2.0 à P2.7 en sortie
On met 0 sur P2.11
On met P2.11 en sortie (dans cet ordre pour éviter d’envoyer « 1 »)
Sinon
On met P2.11 en entrée

Ce qui nous donne le code suivant :

53
//////////////////////////////////////////////////////////////////////////////////////////
// ALLUMAGE/EXINCTION DE LA LED //
//////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------
// Allume ou éteind la led
// ledsOn : true si leds en service, false si désactivées
// La valeur des leds est contenue dans la variable globale _Leds
//----------------------------------------------------------------------------------------
void SetLeds(Boolean ledsOn)
{
if (ledsOn) // si activation des leds
{
LPC_GPIO2->FIOPIN0 = _leds; // Leds sur P2.0 à P2.7
LPC_GPIO2->FIODIR0 = 0xFF; // P2.0 à P2.7 en sortie
LPC_GPIO2->FIOCLR = 1UL << 11; // 0 sur P2.11
LPC_GPIO2->FIODIR |= 1UL << 11; // P2.11 en sortie
}
else // si désactivation
LPC_GPIO2->FIODIR &= ~(1UL << 1); // P2.11 en entrée
}

N’oubliez pas d’ajouter cette fonction dans vos fonctions prototypes. Bien entendu nous
devons déclarer et initialiser la variable globale _leds, hors de notre programme, dans une
zone réservée à cet usage :

//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
// VARIABLES GLOBALES //
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////

unsigned char _leds = 0; // état actuel des leds

Il nous faut maintenant modifier notre boucle principale pour tenir compte qu’une
modification des Leds s’effectue en modifiant la variable globale :

int main(void)
{
unsigned char compteur = 0; // compteur

SystemInit(); // initialise horloge : INDISPENSABLE


InitializeGPIO(); // initialise GPIO
Timing_InitializeTimer(TIMER0); // Initialiser timer pour tempos
InitializeGLCD(); // Initialiser GLCD

while(true) // boucle principale


{
Timing_WaitMs(500); // attendre 500ms
_leds = 0x00; // valeur Leds
SetLeds(true); // valider
Timing_WaitMs(500); // Attendre 500ms
SetLeds(false); // invalider Leds
GLCD_Print_Val8(176,96,compteur++,3); // actualiser compteur
_leds = 0x80; // pour led allumée
SetLeds(true); // Valider Leds
}
}

54
Compilez et chargez votre programme. Vous constatez que dorénavant vous n’avez plus
d’allumages intempestifs : Votre Led clignote « proprement ».

Pensez qu’avec la « bête » que vous avez entre les mains, si vous n’arrivez pas à faire
quelque chose, c’est que vous n’avez simplement pas encore trouvé comment opérer.

J’ai utilisé dans cet exercice des valeurs immédiates pour accéder au matériel. Dans le
cadre d’un projet réel, pensez à utiliser des directives « #DEFINE » pour définir en-tête de
fichier les différentes références, c’est plus propre et plus aisément modifiable.

Comme preuve du multiplexage que nous avons introduit avec KEY1, pressez cette touche,
les Leds retrouvent leur fonctionnement erratique d’origine tant que vous maintenez la
pression, car vous forcez la validation du driver.

Cet exercice est terminé, vous trouverez le projet tel qu’il se devrait se trouver à la fin de
ce chapitre dans les fichiers téléchargés avec le cours.

Souvenez-vous si vous voulez distribuer l’exécutable d’un de vos projets, de veiller à


produire cet exécutable à la compilation. Pour ce faire, retournez dans les paramètres de votre
projet, onglet « Output », et réglez pour générer un fichier « .hex » dépourvu des
informations inutiles, comme ceci :

Recompilez après avoir modifié, votre fichier exécutable « LedSoft.hex » se trouve


maintenant dans votre répertoire « Bin ».

55
3.5. Comment charger un fichier exécutable dans l’ARM

Il se peut que vous deviez charger un exécutable au format « .hex » dans votre ARM, ou
que vous partagiez un tel fichier de façon publique. La question qui se pose est : Comment
charger un tel fichier dans un ARM ?

En fait, il existe plusieurs méthodes, recourant à des canaux de communications différents.


En voici quelques-uns.

3.5.1. Utilisation de l’IDE

La première possibilité est de faire comme nous venons d’opérer, via le projet dans
µVision® ou un autre IDE. Pour procéder de cette façon il faut évidemment qu’on puisse
construire ou reconstruire le projet, et donc il faut disposer des fichiers sources.

3.5.2. Utilisation de J-Flash®

Si on ne dispose que du fichier exécutable, on peut utiliser l’utilitaire J-Flash® de chez


Segger®, qui est installé sur la machine via le fichier d’installation fourni :
« Setup_JLinkARM_V414c.exe ». Attention cependant, J-Flash® ne fonctionne qu’avec une
ancienne version (la 4.14 fournie par exemple). Si vous tentez l’opération avec une version
récente téléchargée chez Segger®, vous obtiendrez un message d’erreur signalant que la sonde
est un clone et que l’utilisation de J-Flash® n’est pas autorisée.

La procédure est assez intuitive, il suffit de suivre les étapes et de bien configurer le
matériel.

Segger® a donc maintenant verrouillé son logiciel de façon à ne plus accepter que les
sondes J-Tag qu’il vend (de bonne qualité, mais chères pour l’amateur occasionnel). Notez
que les drivers Segger®, eux, fonctionnent parfaitement avec notre sonde (donc dans
µVision®), c’est donc uniquement au niveau du logiciel que Segger® refuse l’utilisation de
notre sonde.

La communication s’établit en reliant le PC au connecteur USB J-Tag de la carte.

3.5.3. Utilisation de FlashMagic®

Vous pouvez également vous tourner vers le logiciel gratuit FlashMagic®. Il est disponible
en téléchargement sur http://www.flashmagictool.com/ . La procédure est décrite en détails
dans le manuel utilisateur de la carte.

La communication s’établit en reliant le port série du PC (qui doit de fait en être pourvu) au
port COM0 de la carte. FlashMagic® utilise en effet une autre méthode pour charger le
programme : La programmation ISP via l’UART0 de l’ARM (c’est une fonctionnalité de
l’ARM).

Attention, le câblage du port série doit être complet (inclus les lignes de commande) et non
limité aux lignes Tx/Rx. Je ne suis pas certain du tout que les « convertisseurs USB/Série »
fonctionnent, du moins pas tous. Utilisez plutôt une vraie carte « série » et un câble « full ».

56
3.5.4. Utilisation d’une sonde J-Tag classique

La méthode la plus élégante, surtout si vous comptez développer sur ARM de multiples
projets ou que vous comptez utiliser plusieurs sortes de cartes de développement, est
d’acheter une sonde J-Tag. On en trouve pour une trentaine d’euros qui remplissent
parfaitement leur rôle.

Dans ce cas, plus d’interrogation sur la légitimité, vous achetez la sonde et le constructeur
vous fourni les drivers et utilitaires correspondants officiels (du moins en principe, ça devient
difficile de s’y retrouver réellement).

Il ne reste qu’à désactiver la sonde J-Tag présente sur la carte via les jumpers concernés, et
de relier la sonde au connecteur noir J-Tag de la carte.

Le gros avantage de cette méthode est que vous pouvez programmer des ARM placés sur
des cartes ne disposant pas d’une sonde intégrée. Il suffit que cette carte dispose du
connecteur J-Tag pour y brancher votre sonde.

3.5.5. Le côté légal

Ainsi que je vous l’ai déjà dit, je ne suis pas juriste et j’ai acheté cette carte parfaitement
légalement en France sur un célèbre site de vente en ligne. J’ignorais à la base totalement
qu’il s’agissait d’un clone quelconque. J’ai vu les caractéristiques, le prix, et j’ai bondi.
Maintenant, j’ai investit considérablement en temps de développement, et donc je compte
bien rentabiliser ce temps en utilisant autant de cartes que nécessaire.

Notez que ce site de vente a soi-disant supprimé la carte (et d’autres) de ses articles en
vente, mais en fait a simplement supprimé la référence (pour la France) au terme
« LandTiger® ». Le vendeur est toujours actif, les mêmes cartes sont toujours en vente (sous la
dénomination LPC1768), et le site empoche toujours son bénéfice sur les ventes. J’ai même pu
obtenir le lien direct pour une vente en Belgique, alors que le seul lien qu’on trouvait de façon
classique indiquait « Ce vendeur ne livre pas en Belgique ». Il en va de même pour les autres
sites connus sur lesquels j’ai fait quelques recherches. Comme quoi hypocrisie et commerce
font bon ménage.

Lorsque j’ai reçu le paquet et examiné le CD, j’ai bien compris que le « crack » pour
µVision® proposé sur le CD n’était pas légal dans nos pays, aussi je ne l’ai pas utilisé. Je ne
vais pas écrire un cours utilisant un crack… Pour le reste, c’est beaucoup plus flou, c’est à
vous de prendre vos responsabilités et de faire votre choix. En ce qui me concerne, je ne sais
toujours pas réellement et honnêtement ce qui est autorisé ou non, si la carte est brevetée, etc.
Manifestement les sites de vente en ligne non plus, alors…

Si vous avez un doute et que soit ça vous dérange moralement soit votre application
n’autorise pas le doute (application commerciale par exemple), vous achetez une sonde J-Tag
de prix raisonnable (mettons, jusque 50€) et vous achetez la carte originale Keil MCB1760®
(ne disposant pas de la sonde) pour un prix tournant autours de 320€ si ma mémoire est
bonne. Pour 370€ au lieu de 60€ vous aurez la conscience tranquille et vous pourrez suivre ce
tutoriel sans problème. Á vous de voir…

57
3.5.6. Parenthèse politique

Ceux que mes opinions politiques énervent sont priés de sauter ce chapitre. Si vous lisez
quand même, ne venez pas m’écrire en vous plaignant que j’exprime mes idées, vous êtes
prévenus.

J’en viens à quelques réflexions personnelles politiques, et je me dis que « non », les
Chinois ne sont pas des renégats qui font fi des lois. Les commerçants font comme les nôtres,
ils respectent les lois et les valeurs nationales. Or, en Chine, la contrefaçon (si contrefaçon il
y a, je n’en sais rien) n’est pas un délit.

Comment est-ce possible ? Pour deux raisons principales :

D’une part, ce n’est pas parce qu’on est inféodés aux valeurs américaines sur le
protectionnisme à outrance que ces valeurs sont mondiales ou moralement incontestables :
Ce ne sont que des choix bassement matérialistes et capitalistes. Les Chinois ont une vision
du monde différente, et une juridiction en conséquence, c’est leur droit. Il faut être bien imbu
de soi-même pour prétendre détenir la vérité suprême.

Entre parenthèses, j’avoue que je reste hautement dubitatif sur la position française visant,
par exemple, à envoyer au bûcher un internaute qui télécharge de la musique : Les accords
initiaux sur les droits d’auteurs prévoyaient pourtant l’autorisation de copie à des fins
privées. Privé dans le sens initial : Ne pas utiliser l’œuvre dans un but financier tel que la
diffusion radio, les salles de dance etc. C’était le « deal » de départ en échange de
l’instauration d’un droit d’auteur (que tout le monde semble trouver évident, mais qui est
pourtant récent… et sujet à critiques comme n’importe quoi d’autre).

D’autre part, si les Chinois copient les industriels américains et européens, c’est tout
simplement… parce que ce sont eux qui leur ont appris à le faire, et qui leur ont expliqué que
c’était « bien » et que ça relèverait leur niveau de vie. Ils ont appelé ça « la délocalisation »
(vous savez, une des raisons pour lesquelles vous n’avez plus d’emploi ?).

La délocalisation consiste en fait à faire fabriquer moins cher ailleurs des appareils que
ceux qui les fabriquent ne peuvent pas se payer, et qui seront vendus chez nous à des gens
qui n’ont plus de travail, au prix auquel ils devraient être vendus s’ils étaient fabriqués par
ceux qui les achètent (rien que l’écrire est un art, pensez à l’esprit tordu qu’il faut pour
mettre ça au point dans la réalité).

La différence de prix de revient, parfaitement illégitime, va directement dans la poche des


gentils actionnaires (ceux qui font opérer des licenciements de masse pour gagner toujours
plus, la bourse étant un mécanisme permettant aux grosses sociétés d’acheter les petites).

Les deux dindons de la farce sont donc les ouvriers locaux sous-payés (ce ont eux les
victimes), et les consommateurs (vous, moi…).

Mais les Chinois, contrairement à d’autres pays exploités, ont simplement décidé d’utiliser
les ressources importées pour réellement relever le niveau de vie de leurs nationaux, plutôt
que de continuer à servir d’esclaves à des actionnaires. Vous trouvez vraiment ça
« moralement indéfendable ? »

58
Quant à ceux qui délocalisent il est aisé de comprendre que si on ne veut pas se faire
copier, on ne fournit pas les plans de ce qu’on veut protéger, on n’enseigne pas les
techniques de fabrication, et on ne fournit pas le matériel nécessaire : On fait fabriquer chez
soi au juste prix, et on vend en fonction de ce prix de revient.

Autrement dit, le côté « juridique » est une chose (logique, ce sont ceux qui ont de l’argent
qui font les lois), mais le côté moral est bien plus délicat que ce qu’on vous annonce sans
arrêt sur « les méchants chinois qui copient », ou « les incompétents chinois qui font de la
m…. ».

En chine, on sous-traite pour tous les pays puissants, et donc si on demande de la qualité,
on l’obtient. Par contre, si un industriel occidental demande à une entreprise chinoise de lui
fabriquer des guirlandes de Noël avec comme unique cahier des charges que ça coûte moins
d’un euro, l’entreprise s’exécutera, faisant fi des consignes de sécurités, puisqu’inexistantes
dans le cahier des charges et non obligatoires en Chine. Vous n’allez quand même pas
réclamer aux Chinois de faire preuve de moralité à la place des occidentaux ?

Revenons-en à notre carte, pour parler d’un cas concret. Pour ce qui est de la carte
« originale », vendue 320€ sans sonde par rapport à la carte LandTiger® vendue 60€, si on ne
considère que le côté « pratique » de la chose :

- Le temps de développement de la carte est excessivement réduit et représente une


partie absolument négligeable du prix total à la vente. En effet, je peux sans trop de
problème créer le schéma d’une carte de ce type en quelques jours, alors pensez
qu’une équipe qui ne fait que ça… Pensez également que sur la LandTiger® on a
ajouté une sonde J-Tag et plusieurs composants : Les Chinois également ont du faire
de la conception.

- La réalisation du circuit de la carte « officielle » et son montage sont plus que


probablement sous-traité en Chine (ou ailleurs). Les deux cartes sont donc, au final,
montées au même endroit, ou du moins reviennent à des prix équivalents

- Les composants qui équipent les cartes sont identiques : Ce sont des composants
standards qu’on trouve sur le marché. L’ARM est un Philips NXP, l’écran est identique,
les connecteurs sont de bonne qualité, etc. Le marché de l’électronique est le même
pour tous, et les prix disponibles pour les industriels, identiques.

- Le service après-vente est nickel concernant la carte LandTiger® : Il suffit de contacter


le vendeur pour s’en assurer : réponse garantie et efficace, et en plus dans votre langue
nationale, quelle qu’elle soit : Essayez aux USA…

Du coup, on se demande bien pourquoi, si les Chinois arrivent à proposer des cartes à ce
prix, pourquoi les cartes « originales » sont-elles vendues aussi cher ? Pour les impôts ? Vous
savez, ce truc qui ne sert qu’à financer les intérêts de la dette publique, intérêts qui n’ont
aucune légitimité et qui n’existent que parce qu’on oblige les états à emprunter à des banques
privées..

Je pose la question, chacun y répondra comme il l’estime judicieux. Notez qu’en plus de
la carte vous avez un CD, un câble Ethernet, un câble USB et une alimentation régulée.

59
Ah, j’oubliais : Certains vont rétorquer que les Chinois ont copié les exemples de Keil®.
Ben oui, sauf que les fichiers CMSIS sont des fichiers provenant directement des concepteurs
de l’ARM, pas du monteur de carte, et ils sont disponibles partout parfaitement gratuitement.
Quant aux fichiers d’exemples et les librairies, qui sont fournis, ils peuvent tout juste servir
d’exemples de base car en réalité ils sont complètement pourris niveau programmation
(erreurs, copié/collés dans lesquels on oublie de mettre à jour la fréquence d’horloge réelle,
conflits de pins, temporisation par boucles « for », et j’en passe). Je dirais « Bof »… Sans
compter qu’on peut également les télécharger gratuitement.

Je ne cherche pas à défendre tel ou tel point de vue, je montre simplement qu’autant on
trouve d’arguments pour accuser les Chinois de tous les maux, autant en réfléchissant un
minimum on peut trouver au moins autant d’arguments pour leur défense. La réalité n’est pas
blanche ou noire, ça demande un minimum de discernement.

Bref, faites-vous votre propre opinion : Ce tutoriel fonctionne avec la carte chinoise et
avec la carte américaine : Faites votre choix en fonction des informations légales dont vous
disposez, de vos valeurs morales, de vos convictions politiques et peut-être aussi…. de votre
budget (LOL).

En ce qui me concerne personnellement, je n’aurais pas acheté la carte officielle pour


cause de manque de budget, d’autant qu’il me faut plusieurs cartes pour plusieurs projets
(open-source, donc qui ne me rapportent strictement rien). Sans LandTiger® je n’en aurais
pas pour autant acheté une MCB1760®.

Alors, bien sûr qu’il existe des Chinois qui trompent leur monde, qui vous vendent des
contrefaçons dangereuses, et autres sournoiseries. Mais vous pensez qu’un industriel
européen qui vous vend un appareil à obsolescence programmée ne vous escroque pas d’une
façon identique ?

Et pourtant ma machine à laver (allemande) et celle d’une de mes parentes, de même


marque, viennent de tomber en panne (mécanique) après la fin de la garantie, à quelques
semaines d’intervalles. Force est de constater en démontant que la panne était
obligatoirement prévisible dès la conception, que les éléments étaient volontairement conçus
pour avoir une « réserve d’usure » mécanique visible permettant de s’assurer de résister le
temps suffisant, mais pas trop. Le remplacement de cette pièce en plastique nécessitant, de
nouveau par le fait d’une intention délibérée manifeste, le remplacement de tout le pourtour
du tambour, avec démontage complet. Autrement dit un prix prohibitif si c’est fait par un
professionnel, et donc le déclassement de l’appareil. À 800€ le bout, que pensez-vous de
cette arnaque, bien européenne et connue au point qu’une législation est à l’étude ? Alors, la
Chine…

Fin de l’aparté, ne m’agressez pas, je ne vous oblige pas à adhérer, je vous donne juste ma
vision des choses différente de la vision officielle. Votre achat réel (LandTiger® ou
MCB1760®) sera la transposition (ou pas) de votre opinion sur ce sujet : Tâchez juste d’être
cohérent avec vous-même, parce que m’écrire en râlant contre mon opinion alors que vous
aurez acheté une LandTiger®, ça fait tâche.

60
4. Le débogage sur circuit

4.1. Introduction

Nous avons vu comment utiliser notre sonde J-Tag pour charger un programme dans
l’ARM. Mais là n’est pas l’unique intérêt de ce type de sonde. L’intérêt majeur pour un
développeur, c’est de permettre le débogage sur circuit de son programme, exactement
comme un ICD® ou un PicKit® le permet avec un PIC®.

La bonne nouvelle, c’est que nous avons déjà opéré la grande majorité des étapes nous
permettant de déboguer notre circuit. Il nous suffit maintenant de paramétrer notre matériel
pour permettre l’utilisation de notre débogueur sur notre carte.

Pour rappel, ou pour information, un débogueur sur circuit de ce type permet, à partir de
l’IDE, de :

- Charger un programme dans l’ARM.

- Faire avancer le programme en pas-à-pas en interaction avec le matériel réel.

- Visualiser l’état des variables, registres, et emplacements mémoire.

- Modifier les variables, registres, et mémoire en cours de fonctionnement.

- Forcer le programme à se brancher à n’importe quel endroit à n’importe quel moment.

- Forcer un arrêt lors de l’accès en lecture et/ou en écriture à des variables spécifiques.

- Mettre des points d’arrêt dans le programme à des endroits spécifiques.

- Établir des points d’arrêt conditionnels.

Bref, vous avez accès à « l’intérieur » de votre ARM pendant qu’il fonctionne. Je ne vais
pas vous expliquer toutes les fonctionnalités du débogueur, les menus sont assez explicites
pour que vous découvriez vous-même, sur base de ce que je viens d’énumérer, les différentes
possibilités et la façon de les utiliser. C’est assez intuitif. Je vais juste vous donner la façon de
démarrer et les options principales.

4.2. Le paramétrage

Je vais maintenant vous mettre sur les bonnes pistes, et vous expliquer certains points de
détails particuliers. Commençons par paramétrer notre matériel, et, pour ce faire, ouvrez la
fenêtre de configuration de notre projet. Rendez-vous dans l’onglet « Debug » (on s’en serait
douté) :

61
Par défaut, le débogueur est configuré sur le simulateur, qui est paramétré par la partie
gauche de la fenêtre. Le débogueur physique sur circuit se configure par la partie droite. La
première chose à faire est de valider à droite le bouton-radio : « Use ». Ensuite, dans le combo
déroulant, vous choisissez « Cortex M/R J-LINK/J-Trace ». Attention, votre carte doit
évidemment être allumée et reliée au PC par l’intermédiaire du port USB de la sonde J-Tag :

Il nous faut maintenant configurer cette sonde, en cliquant sur le bouton <Settings> :

62
En fait, vu que nous avons choisi la même sonde J-Tag que lors de la configuration de
notre projet, onglet « Utilities », nous nous retrouvons dans la même fenêtre avec les mêmes
paramètres. En principe vous devriez déjà avoir la configuration de cette capture d’écran,
sinon opérez les modifications nécessaires.

Attention : J’ignore totalement pourquoi, mais selon la carte (j’en ai deux), selon
l’installation, ou selon différents paramètres qui m’échappent, il se peut que vous receviez
deux longues séries de messages d’erreur, indiquant que le registre R0 n’est pas configuré,
puis le registre R1, etc. Si cela se produit, c’est que la vitesse dans « Max Clock » est trop
élevée. Soit vous utilisez le bouton <Auto Clk > pour calculer automatiquement la bonne
vitesse, soit vous sélectionnez une vitesse inférieure. Si vous perdez la connexion J-Tag en
cours de débogage, pensez également à baisser la vitesse.

Lors de mes premiers essais, il m’a été impossible de dépasser 50Khz, j’ai réalisé ma
première application bloqué à cette vitesse. Des essais ultérieurs m’ont permis de monter à
2Mhz sur ma seconde carte, vitesse à laquelle j’ai réalisé ma seconde application. Et
aujourd’hui que je tente de vous faire une capture d’écran des messages d’erreur, je
m’aperçois que j’arrive à monter à la vitesse maximale de 10Mhz. La logique de tout ceci
m’échappe, et j’ignore ce qui provoque ces variations de vitesse maximale. Retenez donc que
si vous avez des messages, vous les acquittez tous, vous réduisez la vitesse, puis vous
retentez. Si quelqu’un comprend le phénomène, qu’il n’hésite pas à m’envoyer l’explication.

En principe l’onglet « Flash Download » a déjà été configuré, puisque vous savez charger
le programme et que la sonde est reconnue dans l’onglet précédent. Sinon, commencez par
configurer cet onglet comme nous l’avons déjà vu.

Validez en fermant la fenêtre avec le bouton <OK>.

63
Notre débogueur a besoin d’informations concernant notre programme, ce sont les
informations de débogage. Sans ces informations vous pouvez uniquement travailler sur le
langage machine présent dans l’ARM, ce qui ne serait pas pratique, vu que nous travaillons en
C dans un programme documenté.

Nous allons donc revenir à notre onglet « Output » et cocher à nouveau les cases «Debug
Information » et « Browser Information ». Vous pouvez décocher la case « Create Hex
File », générer un fichier exécutable distribuable ne sert à rien en phase de débogage, mais ça
ne dérange pas, c’est juste une légère perte de temps.

Validez avec <OK> et fermez également la fenêtre de paramétrage en validant avec <OK>.
Nous revenons dans notre IDE.

4.3. L’entrée en mode débogage

Recompilez votre projet (sinon vous n’aurez pas les informations de débogage), et
sélectionnez ensuite l’item « Start/Stop Debug Session », ou tapez <CTRL><F5>. Sachez que
cette même commande permet d’entrer en mode débogage lorsqu’on est en mode édition, et
en mode édition lorsqu’on est en mode débogage.

Vous ne pouvez pas recompiler un programme en mode débogage, vous devez revenir au
mode édition.

Une fois l’item activé, vous obtenez un message d’avertissement :

Le message indique la taille maximale du code que vous pouvez déboguer avec la version
d’évaluation gratuite. Cette taille est plus que suffisante pour un projet déjà conséquent, vous
mettrez du temps avant d’être éventuellement coincé.
64
Si vous n’obtenez pas ce message, c’est que :

- Soit vous avez acheté la version complète de µVision (Biennnnnnnn !)


- Soit vous avez utilisé le crack fourni sur le CD (Pas Biennnnnnnnn !)

En ce qui me concerne, honnêtement et franchement, j’utilise la version d’évaluation


absolument sans problème et mes deux premiers projets sur cette carte sont déjà pas trop mal
fournis en fonctionnalités. Si vous avez besoin de la version complète, achetez-là, le prix en
vaut la peine si vos projets sont à ce point complexes. J’ai tenté d’utiliser un IDE gratuit open-
source, mais je ne suis pas arrivé à faire reconnaître la sonde J-Tag en mode débogage. Je vous
l’ai signalé, je ne reviens pas à mes débuts en informatique : sans débogueur je ne programme
plus.

L’entrée en mode débogage s’effectue en deux étapes, vu nos configurations :

1) Le programme est automatiquement chargé dans l’ARM, si et seulement si il a été


recompilé depuis la dernière entrée en mode débogage.

2) Après éventuellement le message précédent, les informations de débogage sont


chargées et le programme entre réellement en mode de débogage

4.4. Les fenêtres

Á ce stade, vous obtenez un écran qui est scindé en plusieurs fenêtres :

65
La fenêtre en haut à gauche vous permet soit de visualiser les registres de l’ARM (onglet
« Registers »), soit de visualiser l’arborescence de votre projet via l’explorateur de projet
(onglet « Project »).

La fenêtre en haut à droite vous donne votre programme en langage d’assemblage.

La fenêtre juste dessous vous montre votre programme source, les onglets permettent de
vous déplacer dans les différents fichiers, que vous pouvez éventuellement ajouter à partir de
l’explorateur de projet.

En bas à gauche vous avez les commandes envoyées à la sonde J-Tag, ce n’est pas
vraiment intéressant en ce qui concerne ce tutoriel.

Enfin, en bas à droite, vous avez la fenêtre de visualisation des variables et des zones
mémoires. Vous pouvez ajouter ou supprimer des onglets à cette fenêtre via le menu
« View », items « Watch Windows » et « Memory Windows » :

La première chose à savoir, c’est que vous pouvez afficher le code généré par le
compilateur pour une instruction ou une fonction donnée. Il vous suffit de cliquer sur la ligne
en question dans la fenêtre de code source.

Essayons avec l’instruction « SystemInit() de notre fonction « main() » :

S’il vous est impossible d’agir au niveau de la fenêtre d’édition du code source, c’est
tout simplement que vous avez oublié de cocher la case « Debug Information » dans les
paramètres de votre projet, onglet « Output ». Quittez le mode de débogage (sinon l’accès
aux menus est différent) et validez cette option. Compilez puis revenez ensuite en mode
débogage.

Vous voyez la correspondance entre les deux langages. L’instruction en C est surlignée
en gris, et la première instruction correspondante en langage d’assemblage est surlignée en
jaune. Dans la fenêtre « Disassembly », vous voyez le numéro de ligne (68) et la ligne C avec
ses commentaires, qui correspond à la ligne sélectionnée dans la fenêtre d’édition. Ensuite,
vous avez la liste des instructions en langage d’assemblage correspondant à l’instruction C, la
66
première ligne étant surlignée en jaune. Ici, il n’y a qu’une seule instruction « machine »
produite par l’instruction C (un simple saut), mais il pourrait y en avoir plusieurs, selon la
complexité de l’instruction.

Si vous connaissez le langage d’assemblage de l’ARM, ceci peut vous permettre de


déboguer finement. Mais même si vous ignorez tout de ce langage, vous pourrez récupérer
une tonne d’informations qui pourraient vous être utiles. Par exemple, visualiser quel registre
contient votre variable (R0,R1…) et donc vérifier dans la fenêtre « Registers » la valeur
correspondante. Il faut savoir que l’ARM fait une utilisation intensive de ses registres de base,
bon nombre de variables de votre programme n’existent en fait physiquement pas en RAM,
elles sont stockées dans un registre.

Vous pouvez également, si vous tentez d’optimiser, vérifier l’impact d’un choix opéré sur
une fonction en termes de taille du code machine produit. Un clic-droit dans la fenêtre
« Disassembly » vous permettra de sélectionner une série de paramètres concernant cette
fenêtre. Ceci implique que vous disposiez de quelques connaissances de base sur le langage
d’assemblage de l’ARM.

La fenêtre « Registers » vous permet de visualiser le contenu d’un registre. Elle vous
permet également de modifier sa valeur. Double-cliquez sur la valeur affectée au registre
« R0 » :

Vous pouvez maintenant entrer une valeur hexadécimale en la faisant précéder de « 0x »,


ou une valeur décimale (sans préfixe). Vous pouvez ainsi modifier le contenu des registres à
n’importe quel point de votre programme.

La fenêtre en bas à droite vous permet de visualiser variables, mémoire, ou même les
appels stockés sur la pile.

Pour ajouter une variable à une des fenêtres « Watch », il suffit de cliquer à droite sur le
nom d’une variable, par exemple sur « compteur » de notre fonction « main() » :

67
Pour que cela fonctionne, il faut répondre à plusieurs conditions :

- Le programme doit avoir accès à la variable, et donc, dans notre exemple, il doit être
en train d’exécuter le « main() », ce qui n’est pas le cas.

- La variable doit physiquement exister en Ram et ne pas correspondre à une valeur


stockée dans un registre.

Sinon, vous obtiendrez un message d’erreur, précisant que soit l’opération est impossible,
soit que la variable est inaccessible. Vous déciderez que faire le cas échéant.

Il peut aussi arriver que vous vouliez visualiser un résultat intermédiaire au débogage dans
une variable qui ne sert en réalité à rien. Le compilateur va s’apercevoir de l’inutilité de la
variable et l’ignorer, vous n’y aurez donc pas accès. Pour remédier à ça, déclarez votre
variable comme « volatile », ça oblige le compilateur à en tenir compte.

L’onglet « Memory 1 » et suivants vous permet de visualiser une zone mémoire. Il vous
suffit de taper l’adresse désirée. Attention, les adresses sont en 32 bits, et je rappelle qu’une
variable de plus de 8 bits est stockée en RAM en commençant par le poids faible.

Certaines zones mémoire vous seront cependant inaccessibles, vous n’obtiendrez par
exemple que des valeurs de 0xAA. C’est le cas par exemple pour les zones réservées aux accès
DMA.

4.5. Un mot sur les registres

Nous avons vu comment modifier les registres de l’ARM en cours de fonctionnement. Je


vais vous fournir quelques informations concernant ces registres. Ils sont numérotés de R0 à
R15.

68
Les registres R0 à R12 sont des registres à usage général. C’est-à-dire qu’on y met ce
qu’on veut et qu’on les utilise comme on veut (du moins, celui qui programme en langage
d’assemblage le peut, votre compilateur C aussi).

Notez cependant une particularité : Lorsque vous appelez une fonction avec la plupart des
processeurs, les paramètres passés à la fonction sont passés sur la pile (Stack). Dans le cas de
l’ARM, il existe une convention respectée en principe par tous les compilateurs : Si vous avez
un maximum de 4 paramètres passés, ils sont passés dans l’ordre dans les registres R0 à R3.
Les paramètres excédentaires sont passés par la pile. Pensez en écrivant vos fonctions que si
vous utilisez plus de 4 paramètres ça a un impact sur la pile et sur la vitesse d’exécution (mais
bon, pas de panique, vous avez de la marge).

Toujours par convention, la valeur de retour est passée par le registre R0.

Le registre R13 est aussi appelé « SP », pour « Stack Pointer ». C’est le pointeur de pile,
sa valeur d’initialisation dépend de ce que vous avez inscrit comme taille de pile (stack_size)
dans le fichier « startup_LPC17xx.s ». Vous pourrez vérifier que vous avez un éventuel
débordement de pile si vous obtenez des plantages curieux.

Le registre R14, appelé également « LR », pour « Link Register » est un registre qui
contient l’adresse de retour vers le dernier appelant d’un sous-programme (une fonction). Ça
permet un retour rapide sans devoir dépiler des adresses sur la pile ;

Quant au registre R15, baptisé « PC », il s’agit tout simplement de notre compteur ordinal,
ou, autrement dit, du registre qui pointe sur la prochaine instruction à exécuter.

xPSR est notre registre d’état. Si vous dépliez vous verrez les bits Z, N etc. Il y a plusieurs
registres d’état dépendant du contexte, mais dans notre cas c’est peu important.

Ces informations vous seront très utiles en cours de débogage.

4.6. Le débogage du programme

Lorsque l’ARM est mis en mode « débogage », le programme s’arrête et est reseté. Nous
allons maintenant déboguer notre programme « LedSoft ». Afin d’éviter d’entrer dans les
fichiers d’initialisation, nous allons placer un point d’arrêt sur la première ligne qui nous
intéresse : La première instruction de notre fonction « main() ». Vous pouvez par la suite
démarrer en pas-à-pas dès le départ, pour voir tout ce qui est exécuté dans les fichiers inclus.

Double-cliquez à gauche du numéro de la ligne « unsigned char compteur = 0 ; ». C’est


bel et bien une instruction, puisqu’on affecte une valeur à la variable. Sans quoi ce serait une
déclaration et il ne serait pas possible d’y mettre un point d’arrêt (breakpoint). Un petit carré
rouge apparaît :

69
Un point d’arrêt signifie que si vous démarrez votre programme, et que ce dernier passe
par l’instruction concernée, il marquera l’arrêt et passera la main au débogueur.

Vous pouvez supprimer/ajouter un point d’arrêt en utilisant le double-clic ou en tapant


<F9>. Vous pouvez gérer l’intégralité de vos points d’arrêt via l’item « Breakpoints… » du
menu « Debug ».

C’est donc le moment de lancer notre programme. Il nous suffit de taper <F5>. Le programme
s’arrête à la ligne spécifiée. Tout ce qui est modifié lors de la dernière instruction exécutée est
surligné en bleu (registres, variables etc.) :

Constatez les derniers registres modifiés (pile, pointeur de programme, adresse de retour,
statut). La flèche jaune indique la prochaine instruction à exécuter, que ce soit en langage

70
d’assemblage (en haut) ou en instruction C (en bas). Si vous regardez l’adresse mémoire de la
prochaine instruction machine à exécuter, en haut, vous voyez 0x11D8 (instruction MOVS
r4,0). Ceci correspond bel et bien au contenu de notre registre PC, tout est parfait.

C’est le moment d’ajouter notre variable « compteur » à notre fenêtre « Watch 1 ». Pour
ce faire, cliquez à droite sur le nom de la variable et choisissez d’envoyer vers « Watch 1 »,
comme nous l’avons déjà vu. Cette fois, l’opération est parfaitement possible :

Vous pouvez évidemment visualiser et modifier la valeur de compteur, tant que cette
variable reste accessible (c’est une variable locale). Mais vous pouvez faire une chose
encore plus intéressante : Utiliser la variable comme déclencheur d’un arrêt du
programme. Pour ce faire, cliquez-droit sur le nom de la variable et choisissez « Set
Access Breakpoint at ‘compteur…’ » :

Vous ajoutez alors un point d’arrêt sur l’accès de votre programme à votre variable :

71
Il vous suffit de sélectionner « Read » et/ou « Write » pour déterminer si l’arrêt se fera si
le programme lit et/ou écrit la variable. C’est très utile si un bug dans votre programme
modifie une variable sans que vous sachiez comment, par exemple si un de vos tableaux
déborde. « Count » vous permet même d’arrêter après le nombre d’accès spécifié. Vous devez
d’abord cliquer sur « Define » et ensuite sélectionner la ligne et choisir les types d’accès :

Sélectionnez la ligne, cliquez sur « Kill Selected », et fermez la fenêtre. Nous allons
maintenant avancer en pas-à-pas dans notre programme. Si vous pressez <F10> vous exécutez
une instruction, un appel de fonction étant considéré comme une seule instruction. Si vous
pressez <F11> vous entrez à l’intérieur des éventuelles fonctions pour les déboguer.

Attention : Si vous cliquez à l’intérieur de la fenêtre de votre code source, vous exécutez
une instruction « C » à la fois à chaque pression de <F10>. Si par contre vous cliquez dans la
fenêtre « Disassembly » vous exécuterez une instruction machine à la fois.

L’item « Reset CPU » du menu « Debug » vous permet de relancer le programme à partir du
début, n’oubliez pas de presser <F5> pour le relancer. La petite croix rouge vous permet de
stopper le programme qui est en train de tourner.

Pressez <F10> jusqu’à ce que vous arriviez à la ligne « InitializeGLCD() ». Pressez alors
<F11> et vous arrivez à l’intérieur de la fonction « InitializeGLCD() ». Pressez ensuite
successivement <F10> pour voir l’effet de chaque ligne. L’écran, une fois initialisé, affiche la
dernière image qu’il avait affichée avant le reset.
72
Pressez <F10> jusqu’à revenir au programme principal, puis de nouveau <F10> jusque la
ligne « SetLeds(true) ». Pressez alors une seule fois <F11>. Vous arrivez sur la première ligne
de la fonction :

Nous passons à la méthode un Boolean (ledsOn). Un Boolean est un octet non signé, il
peut contenir la valeur false (0) ou true (1). Si vous avez mémorisé ce qui précède, notre
fonction n’ayant qu’un seul paramètre, celui-ci est passé dans le registre R0. Constatez dans la
fenêtre « Registers » que R0 est surligné en bleu et donc vient d’être modifié, et que son
contenu vaut « 1 » et donc « true ». Avancez en pas-à-pas avec <F10> jusque la fin du « if ».
Rien ne se passe apparemment.

Cliquez-droit sur « _leds » dans la première instruction du « if » et affichez la variable


dans la fenêtre « Watch 1 ». Vous constatez que « _leds » vaut 0, ce qui explique que tout est
éteint et que rien ne s’est passé.

Constatez au passage que la variable « compteur » est inaccessible (out of scope).


Double-cliquez sur la valeur 0x00 puis tapez « 0x55 » et ensuite <Return>. La nouvelle
valeur est mémorisée dans la variable.

Revenez dans votre code, et cliquez sur la première ligne après le crochet du « if » :

73
LPC_GPIO2->FIOPIN0 = _leds; // Leds sur P2.0 à P2.7

La première instruction « machine » est affichée dans la fenêtre supérieure :

Vous avez donc la flèche jaune dans la fenêtre du bas qui indique la prochaine instruction
à exécuter (le retour de fonction). La flèche bleue indiquant sur quoi vous travaillez, le
surlignage bleu ou gris de la fenêtre source indiquant la ligne sélectionnée, et le surlignage
jaune de la fenêtre « Disassembly » indiquant quelle est la première instruction de la séquence
d’instructions correspondant à l’instruction C sélectionnée.

Dans la fenêtre « Disassembly », sur la ligne surlignée de jaune, faites un clic-droit et


choisissez « Set Program Counter ». Ceci charge l’adresse de l’instruction (0x1146 dans ce
cas-ci) dans le PC. De fait, la flèche jaune indique maintenant que cette instruction va être
exécutée, et le PC contient effectivement 0x1146 :

74
Parcourez à nouveau cette portion de code en utilisant <F10>, non sans avoir
préalablement cliqué dans la fenêtre source (sinon vous avancerez d’un pas en langage
machine). Vous obtenez l’allumage d’une Led sur deux, conformément à la valeur 0x55 que
vous avez placée dans la variable « _leds ».

Notez que vous pouvez placer simplement le curseur de votre souris sur la variable
« _leds » dans votre fenêtre d’édition : Vous obtiendrez directement sa valeur sans l’envoyer
dans la fenêtre « Watch ».

Un dernier détail : Si vous passez un tableau ou un pointeur quelconque à une fonction, le


registre correspondant contiendra l’adresse du tableau ou la valeur du pointeur. Pour
visualiser à quoi ça correspond vous pouvez utiliser la fenêtre « Memory » et indiquer
l’adresse en question, vous obtiendrez le contenu de la mémoire.

Vous savez maintenant modifier le cours d’un programme, agir sur les registres, les
variables et la mémoire, placer des points d’arrêt, les gérer, utiliser des points d’arrêt sur les
variables, avancer en pas-à-pas externes ou internes.

Vous avez les bases pour déboguer vos programmes, il ne vous reste qu’à expérimenter.

75
Notes :

76
5. Parlons interruptions

5.1. Les exceptions

Je ne vais vous laisser à ce stade sans même aborder les interruptions, mécanisme
essentiel pour la plupart des applications. Du moins si on souhaite programmer de façon
efficace.

Dans le langage ARM, une interruption fait partie de la grande famille générique des
« Exceptions ». La philosophie utilisée est assez proche de celle d’un processeur de la famille
68000, pour ceux qui connaissent. L’ARM classifie les exceptions en plusieurs catégories. Ces
catégories sont réparties en deux types principaux: Les exceptions masquables et les
exceptions non masquables. La différence entre les deux étant qu’on peut autoriser ou
interdire les premières, alors qu’il est impossible d’empêcher l’exécution des secondes : On
ne sait pas les « masquer ».

Les grandes catégories d’exceptions sont, dans l’ordre de la plus prioritaire à la moins
prioritaire :

- Reset : C’est considéré comme une exception à part entière, puisque ça détourne le
programme principal de son déroulement normal. C’est une simple question de point
de vue et ça ne change rien à la nature du reset par rapport à un autre microcontrôleur.
Le reset, à partir du moment où on le considère comme exception, constitue
évidemment l’exception la plus prioritaire, puisqu’elle interrompt n’importe quelle
autre.

- NMI : Pour Non Maskable Interrupt : On a rangé dans cette catégorie l’intégralité des
interruptions non masquables. Ces exceptions ont le plus haut niveau de priorité,
excepté le reset, et donc une NMI interrompt toute autre exception en cours. Le niveau
de priorité d’une NMI est « -2 » (plus le nombre est petit, plus grande est la priorité).

- Hard Fault , Memory Management Fault, Bus Fault, Usage Fault: Ce sont des
exceptions liées principalement à des erreurs de programmation, comme la division
par zéro, l’accès à des variables non alignées, des erreurs de structures de retour
d’exception etc. Si vous tombez sur une exception de ce type, pistez votre programme
pour voir ce qui la déclenche et corrigez l’erreur. La HardFault a une priorité de -1, les
autres sont configurables.

- SVCall et PendSV : Retenez que ce sont des exceptions surtout utilisées lorsque vous
travaillez au niveau d’un OS. Si vous travaillez au niveau d’un OS vous n’avez pas
besoin de mes explications, et si vous ne développez pas d’OS vous n’avez pas en
principe à vous préoccuper de ces exceptions. La priorité est configurable.

- SysTick : Il s’agit d’une interruption masquable, déclenchée à intervalles réguliers


(Tick). Vous avez donc une espèce de « tempo » qui ne nécessite pas l’utilisation d’un
timer, ou plutôt qui utilise un timer « système » spécifique. Ce mécanisme est très
utilisé dans les OS pour appeler des fonctions à intervalles précis, mais vous pouvez
évidemment vous servir de ce mécanisme pour n’importe quel autre usage.

77
- IRQ : Pour Interrupt ReQuest, ou requête d’interruption. Ce sont ce que nous appelons
communément « les interruptions ». La priorité de chaque interruption du groupe IRQ
est configurable de façon indépendante. Elles sont évidemment masquables, et sont
désactivées par défaut.

Les exceptions à priorité négative ont une priorité figée. Les exceptions à priorité
configurable ont une priorité de zéro par défaut, et ne peuvent jamais être configurées avec
une valeur négative.

5.2. Le fichier startup_LPC17xx.s

Nous allons maintenant nous intéresser aux interruptions « classiques », c’est-à-dire que
nous pouvons valider à l’envie, et qui sont liées à du matériel spécifique. En fait, ce qu’on
appelle communément « interruptions » la plupart du temps.

Nous allons aborder ce sujet par le côté apprentissage, en voyant tout d’abord comment
tout ceci est configuré dans les fichiers CMSIS fournis avec l’ARM. Dans votre projet LedSoft,
ouvrez votre fichier « startup_LPC17xx.s ». Vous y trouvez ceci :

__Vectors DCD __initial_sp ; Top of Stack


DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler

; External Interrupts
DCD WDT_IRQHandler ; 16: Watchdog Timer
DCD TIMER0_IRQHandler ; 17: Timer0
DCD TIMER1_IRQHandler ; 18: Timer1
DCD TIMER2_IRQHandler ; 19: Timer2
DCD TIMER3_IRQHandler ; 20: Timer3

Il faut savoir que dans l’ARM, les adresses correspondant à une exception d’un type donné
ne sont pas figées, elles sont librement définissables. Ce n’est pas le cas d’un PIC16F, par
exemple, qui impose l’adresse 0x04 comme point de départ des interruptions.

Dans notre ARM, les adresses de branchement en cas d’exception sont appelées
« Vecteurs d’exception ». Il y a un vecteur par type d’exception possible, on n’a donc pas à
« filtrer » dans le programme quelle est l’interruption survenue, on aura une fonction par type
d’interruption.
78
Comme il faut bien que l’ARM connaisse la correspondance entre l’exception et l’adresse
à laquelle il doit se connecter, on dispose d’un tableau de vecteurs, structuré d’une façon
prédéfinie et figée, dans lequel on va ranger tous nos vecteurs, dans le bon ordre.

La liste « _Vectors » précédente est en réalité ce tableau de vecteurs. À chaque


emplacement on stocke l’adresse d’une fonction à exécuter lorsque l’exception
correspondante est rencontrée.

Par exemple, directement après l’adresse du début de pile, vous trouvez le vecteur de
reset. De nouveau, ce vecteur ne pointe pas spécifiquement vers l’adresse 0x00 ni vers aucune
adresse imposée, vous placez au contraire dans ce vecteur l’adresse de la fonction que vous
voulez que l’ARM exécute après un reset.

Vous trouvez dans ce vecteur une adresse symbolique, « Reset_Handler », qui est
évidemment l’adresse d’une fonction qui doit exister quelque part. Si vous cherchez plus bas
dans ce fichier, vous allez trouver ceci :

Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
LDR R0, =__main
BX R0
ENDP

C’est une routine en langage d’assemblage. En fait, cette routine charge l’adresse de la
fonction « main() » et effectue un saut vers cette adresse. C’est donc là qu’est défini le fait
que vous devez créer une fonction « main() » qui est le point de départ de votre programme.

Si vous regardez maintenant cette ligne, correspondant à notre vecteur HardFault :

DCD HardFault_Handler ; Hard Fault Handler

Et que vous cherchez la fonction correspondante, vous trouvez ceci :

HardFault_Handler\
PROC
EXPORT HardFault_Handler [WEAK]
B .
ENDP

Il s’agit également d’une routine (procédure) en langage d’assemblage, délimitée par les
directives « PROC » et « ENDP ». Vous y trouvez le nom de la fonction exportée avec la
directive « [WEAK] ». Comme j’utilise cette directive (sous une autre forme) dans mes
programmes, et que ça m’a valu du courrier, je vais expliquer l’intérêt de cette directive, bien
que ça fasse plus partie de l’apprentissage du langage C que de l’étude de l’ARM.

En fait, « WEAK » définit un lien faible. Pour imager le concept, on pourrait dire que ça
signifie qu’on dit au compilateur : « Utilise ceci si tu ne trouves rien d’autre ».

Autrement dit, si on a déclaré un prototype de fonction comme étant « WEAK », alors :

79
- Si le compilateur trouve une fonction « non Weak » (classique), alors il appelle cette
fonction.

- Sinon, si le compilateur trouve une fonction correspondante « Weak », il appelle la


fonction « Weak ».

- Sinon (s’il ne trouve rien), l’appel est remplacé par un « nop » (aucune opération).

Ceci permet d’implémenter des fonctions par défaut, qui seront « Weak », tout en
permettant à l’utilisateur d’implémenter une autre fonction « classique » qui pourra remplacer
celle utilisée par défaut.

C’est un peu le principe des « surcharges » utilisé dans les langages objets, sauf qu’ici on
n’a qu’une seule possibilité de surcharge possible, puisque, pour que le compilateur (plus
spécifiquement l’éditeur de liens) s’y retrouve, il ne peut y avoir au maximum qu’une seule
fonction « Weak » du même nom et qu’une seule fonction « classique » du même nom. En
cas de doublon sur le nom, seule une combinaison : Une fonction « Weak » plus une fonction
« classique » est autorisée : Un lien faible et un lien fort.

Dans le cas qui nous occupe, nous devons placer une adresse dans notre tableau de
vecteurs, pas question d’y mettre un « nop ». C’est pourquoi la solution « aucune opération »
est invalide, et que la syntaxe est donc un peu différente : Plutôt que d’utiliser une fonction
prototype « Weak », on déclare un « Export Weak » dans la fonction exécutée par défaut.

Si vous avez tout suivi (sinon, relisez), si vous désirez laisser le traitement par défaut,
vous ne faites rien, le lien faible sera appelé. Si vous désirez opérer un traitement spécifique,
vous créez une fonction « HardFault_Handler » sans indiquer « Weak ». S’agissant d’un lien
fort, c’est votre fonction qui sera appelée en lieu et place de celle par défaut. Vous ne pouvez
pas désactiver, vous pouvez juste décider de traiter vous-même ou de laisser par défaut.

Quel est le code utilisé par défaut dans notre fonction « Weak » ? Simplement
l’instruction en langage d’assemblage suivante :

B .

Cette instruction est un saut sur l’instruction elle-même, et donc une boucle sans fin.

Si votre programme rencontre une erreur de type « HardFault » et que vous n’avez pas
placé un code de traitement par défaut, votre programme se retrouvera piégé dans cette
boucle. Non seulement ce sera visible à l’exécution car votre programme ne fonctionnera
plus, mais également vous pourrez détecter l’anomalie en mode débogage.

Ce qui va nous intéresser le plus, ce sont les lignes suivantes du tableau de vecteur :

DCD WDT_IRQHandler ; 16: Watchdog Timer


DCD TIMER0_IRQHandler ; 17: Timer0
DCD TIMER1_IRQHandler ; 18: Timer1
DCD TIMER2_IRQHandler ; 19: Timer2

80
Vous trouvez des noms très explicites, qui devraient vous éviter de vous plonger dans le
datasheet pour savoir à quelle adresse est rangée quelle interruption masquable IRQ.

Bien évidemment, ces noms correspondent à des fonctions exportées « Weak », suivies
par le code par défaut consistant en une boucle sans fin. Soit exactement ce que nous venons
d’expliquer, sauf que nous avons une seule et même boucle pour l’intégralité de toutes les
interruptions possibles. Le but de valider une interruption IRQ est de la traiter, le traitement
par défaut ne sert donc que si vous avez validé une interruption et oublié de créer la fonction
de traitement (ou si vous avez commis une erreur dans son nom). Si vous arrivez sur la boucle
sans fin d’une IRQ, c’est forcément que vous n’avez pas correctement traité l’interruption en
question. Ceci explique pourquoi on n’a placé qu’un code commun pour l’intégralité des IRQ.

Lorsque vous validerez une interruption, vous devrez donc venir dans le fichier
« startup_LPC17xx.s » voir comment vous devez appeler votre fonction de traitement. Par
exemple si vous validez l’interruption sur le Timer3, votre fonction d’interruption, qu’on
appelle « handler d’interruption », devra s’appeler « TIMER3_IRQHandler », et vu qu’un
handler d’interruption ne prend aucun paramètre et ne retourne aucun paramètre, vous
obtenez une fonction de la forme :

void TIMER3_IRQHandler(void)
{
// Traiter l’interruption TIMER3 ici
}

5.3. Mise en service des interruptions IRQ

Nous avons vu comment appeler notre fonction de traitement, il nous faut maintenant voir
comment valider une interruption, les IRQ étant toutes désactivées par défaut.

Les interruptions sont gérées par le contrôleur d’interruptions vectorisées intégré : Nested
Vectored Interrupt Controller, ou NVIC. Vous pourriez écrire vous-même les instructions
d’activation/désactivation d’une interruption, sachant que :

- Les interruptions sont numérotées, les valeurs sont dans « LPC17xx.h »


- Les bits d’activation se trouvent dans le registre NVIC-ISER.

Voici les numéros des interruptions :

/****** LPC17xx Specific Interrupt Numbers


*******************************************************/
WDT_IRQn = 0, /*!< Watchdog Timer Interrupt*/
TIMER0_IRQn = 1, /*!< Timer0 Interrupt*/
TIMER1_IRQn = 2, /*!< Timer1 Interrupt*/
TIMER2_IRQn = 3, /*!< Timer2 Interrupt*/
TIMER3_IRQn = 4, /*!< Timer3 Interrupt*/
UART0_IRQn = 5, /*!< UART0 Interrupt*/

En fait, inutile de trop chercher, le numéro d’une interruption dont le handler s’appelle
« xxx_IRQHandler » sera appelé « xxx_IRQn ».

81
Il est cependant plus simple d’utiliser les fonctions déjà fournies. Elles sont déclarées dans
« core_cm3.h ». Vous pourriez trouver curieux qu’une fonction soit déclarée dans un fichier « .h » et
non « .c » ou « .s », mais en fait ces fonctions sont préfixées de la directive « __inline », ce qui veut
dire que le compilateur va remplacer le nom de la fonction par le code présent (une simple recopie) et
non appeler la fonction comme une fonction traditionnelle.

En d’autres termes : Ce sont des macros.

Pour valider une interruption, il suffit d’appeler la fonction inline suivante:

static __INLINE void NVIC_EnableIRQ(IRQn_Type IRQn)

Pour valider notre interruption Timer3, nous devrons donc écrire :

NVIC_EnableIRQ(TIMER3_IRQn); // Interruptions Timer3 en service

Les interruptions générales sont déjà en service, il n’y a donc rien d’autre à valider. De la
même façon, désactiver notre interruption se fera avec un appel à la fonction inline suivante :

static __INLINE void NVIC_DisableIRQ(IRQn_Type IRQn)

…et donc pout notre Timer3:

NVIC_DisableIRQ(TIMER3_IRQn); // Interruptions Timer3 hors service

Récapitulons ! Pour utiliser une interruption de type « xxx », vous devez :

- Créer le handler d’interruption : void xxx_IRQHandler(void)


- Valider l’interruption correspondante : NVIC_EnableIRQ(xxx_IRQn);

Rien de plus simple !

5.4. Traitement de l’interruption

Nous avons vu comment enclencher une interruption et comment définir le handler


d’interruption correspondant. Il nous reste à voir comment réaliser le traitement de
l’interruption au cœur de la fonction.

En fait, nous ne recevons aucun paramètre, il n’y a rien de spécifique à ce niveau. Par
contre vous constatez que vous avez une seule source d’interruption par module : Ce sera la
même interruption pour toutes les causes d’interruption possibles d’un même module. Ainsi,
notre Timer3, par exemple, sera capable de générer une interruption pour plusieurs conditions
différentes, par exemple une interruption pour la comparaison avec chacun des registres de
comparaison.

Lorsque plusieurs causes d’interruption sont validées pour le même module, vous devez
donc vérifier dans votre interruption la cause réelle de l’interruption. Tout ce que vous pouvez
dire, c’est que, puisque vous vous trouvez dans la routine d’interruption d’un module donné,
que l’interruption que vous traitez est bien celle du module concerné, ce n’est déjà pas si mal.

82
Ce qui suit est important à retenir, sans quoi vous rencontrerez des problèmes que
nous verrons par la pratique dans la suite de ce tutoriel. Dans la plupart des cas, excepté les
interruptions acquittées automatiquement sur action d’un registre spécifique (lecture d’une
valeur par exemple), vous devez acquitter explicitement l’interruption en resetant son flag
d’interruption.

Dans un microcontrôleur comme les PIC®, par exemple, nous avions une routine du
genre :

Interruption
Vérification du type d’interruption
Traitement de l’interruption
Reset du flag d’interruption
Retour d’interruption

Si vous procédez comme ça avec un ARM ça ne fonctionnera pas , ou, du moins, pas
comme prévu. Pourquoi ? Parce que l’ARM procède un mécanisme de pipeline, qui fait que
lorsqu’une instruction est en cours d’exécution, la suivante est déjà décodée et une troisième
déjà chargée.

Le côté pratique de ceci, concernant les interruptions, est que l’annulation du flag survient
trop tard par rapport à l’instruction de sortie d’interruption (en C : L’accolade fermante de la
fin de fonction), l’ARM sortira de l’interruption avant que le flag ne soit effectivement effacé,
et donc rentrera immédiatement une seconde fois dans la fonction de traitement de
l’interruption, puis l’effacement du flag deviendra effectif. Le handler sera exécuté deux fois.

Vous devez donc déplacer le reset du flag d’interruption ailleurs plus haut dans votre
traitement d’interruption, pourquoi pas au début ?

Retenez que si votre handler d’interruption s’exécute deux fois consécutives sans raison
valide, c’est que l’effacement du flag d’interruption est trop proche de la fin du handler.

5.5. Les validations générales

Vous savez maintenant activer et désactiver n’importe quelle interruption. La question qui
se pose est : « Est-il possible de désactiver toutes les interruptions d’un seul coup ? », avec,
bien évidemment, la méthode pour les réactiver. Ceci nécessite un bit de validation globale.

La réponse est « oui », et tient dans deux fonctions « inline » (macros) :

__disable_irq() ; // Désactive les interruptions

__enable_irq(); // Active les interruptions

Ceci pourrait vous être utile, si vous devez exécuter des instructions en étant certain de ne
pas être interrompu, sans devoir pour autant procéder interruption par interruption.

83
5.6. Priorités des interruptions

Nous avons vu que nos interruptions IRQ avaient un niveau de priorité configurable. Nous
allons examiner comment ceci fonctionne et par conséquence comment on utilise les priorités.

Sachez déjà que toutes les exceptions de priorité configurable ont un niveau de priorité
zéro par défaut. Si vous n’avez pas besoin de hiérarchiser vos interruptions, ce que nous
avons déjà vu est suffisant.

Le système de hiérarchisation des priorités fonctionne par groupements. Une interruption


spécifique est affectée à un groupe, le groupe de numéro le plus faible dispose de la priorité la
plus élevée.

Dans un groupe donné, chaque interruption spécifique peut se voir attribuer un numéro
interne, appelé « sous-priorité ». Ce numéro définit la priorité à l’intérieur du groupe, avec,
de nouveau, la priorité la plus élevée correspondant à la valeur la plus faible.

Le principe est le suivant :

- Une interruption d’un groupe donné peut interrompre une interruption d’un autre
groupe de numéro plus élevé.

- Une interruption d’un groupe donné ne peut jamais interrompre une interruption d’un
même groupe

- En cas de survenance simultanée de deux interruptions d’un même groupe,


l’interruption de plus haute priorité est exécutée en premier

En utilisant les termes spécifiques :

- Une interruption peut interrompre une interruption dont la valeur de groupe-priorité


est supérieure

- Une interruption ne peut jamais interrompre une autre interruption de groupe-priorité


identique

- En cas de survenance simultanée de deux interruptions de même groupe-priorité, celle


disposant de la plus faible valeur de sous-priorité sera exécutée en premier.

La valeur de priorité globale d’un vecteur d’interruption donné est donc définit par un
nombre de bits définissant son groupe-priorité, plus un nombre de bits définissants son
numéro de sous-priorité. Le nombre total de bits utilisés pour la combinaison groupe-
priorité/sous-priorité est fixée à 8. Par contre, la répartition des 8 bits est paramétrable.

Pour ce faire, une valeur codée sur 3 bits, de 0 à 7, qu’on appellera PriGroup, permet de
sélectionner la répartition des 8 bits selon notre bon vouloir. Voici le tableau des possibilités
générales pour un ARM-M3. Les lettres « x » représentent un bit codant une valeur de groupe-
priorité, et un « y » codant une valeur de sous-priorité :

84
PriGroup Format Groupes Sous-priorités
0 xxxxxxx.y 128 2
1 xxxxxx.yy 64 4
2 xxxxx.yyy 32 8
3 xxxx.yyyy 16 16
4 xxx.yyyyy 8 32
5 xx.yyyyyy 4 64
6 x.yyyyyyy 2 128
7 yyyyyyyy 1 256

La liberté est donc totale. Cependant, dans le cas particulier de notre LPC-1768, tous les
bits ne sont pas implémentés. Ainsi, les trois bits de poids faibles b2/b0 valent toujours 0.

Ceci transforme notre tableau de la façon suivante :

PriGroup Format (b7/b0) Groupes Sous-priorités


0 xxxxx00.0 32 1
1 xxxxx0.00 32 1
2 xxxxx.000 32 1
3 xxxx.y.000 16 2
4 xxx.yy.000 8 4
5 xx.yyy.000 4 8
6 x.yyyy.000 2 16
7 yyyyy.000 1 32

Si vous trouvez que 32 niveaux de priorités sont insuffisants, vous êtes particulièrement
exigeant. Pour définir le PriGroup, vous devez utiliser la fonction suivante :

NVIC_SetPriorityGrouping(PriGroup); // Définit le groupement des interruptions

Ensuite vous affecterez une valeur de priorité pour chaque interruption utilisée, sachant
que ce nombre de 8 bits respectera le format du tableau précédent, à la ligne correspondant à
la valeur de PriGroup que vous aurez passée à la fonction que nous venons de voir.
L’affectation de cette valeur se fait via la fonction :

NVIC_SetPriority(Numéro d’interruption,priorité); // Affecter priorité

Prenons un exemple pratique : Nous désirons trois traitements d’interruption, un


concernant le timer1, un pour le timer 2, et un concernant le timer 3.

Nous avons besoin que l’interruption du timer1 puisse interrompre les deux autres, et
nous décidons que si une interruption timer2 et timer3 interviennent ensemble, ce sera le
timer2 qui sera traité en premier. Le timer2 et le timer3 ne peuvent pas s’interrompre.

85
Il y a des tas de façons de configurer nos registres pour répondre à ce besoin simple. Nous
allons décider de former 2 groupes de priorités (il nous en faut au minimum 2, mais s’il y en a
plus ça ne dérange pas).

Notre tableau nous indique qu’il nous faut une valeur de PriGroup égale à 6. Le numéro
de groupe sera donc codé dans « b7 ».

Nous placerons notre Timer1 dans le groupe le plus prioritaire, et donc dans le groupe 0.
Nos deux autres timers, aucun ne pouvant interrompre l’autre, feront obligatoirement partie
du même groupe, moins prioritaire que celui du Timer1, et donc le groupe 1.

Nous affecterons une plus grande sous-priorité au timer2 qu’au timer3, et donc, par
exemple, nous affecterons une sous-priorité de 0 au timer2 et de 1 au timer3. Notre tableau
nous indique que les valeurs de sous-priorités seront codées dans les bits b6/b3.

Ceci nous donne la séquence d’initialisation suivante :

NVIC_SetPriorityGrouping(6); // 2 groupes de 16 sous-priorités


NVIC_SetPriority(TIMER1_IRQn, 0x00); // Timer1 => groupe 0 / sous-priorité 0
NVIC_SetPriority(TIMER2_IRQn, 0x80); // Timer2 => groupe 1 / sous-priorité 0
NVIC_SetPriority(TIMER3_IRQn, 0x88); // Timer3 => groupe 1 / sous-priorité 1
NVIC_EnableIRQ(TIMER1_IRQn); // interruption Timer1 en service
NVIC_EnableIRQ(TIMER2_IRQn); // interruption Timer2 en service
NVIC_EnableIRQ(TIMER3_IRQn); // interruption Timer3 en service

Regardez bien le format si vous avez un doute : 0x88 nous donne B’10001000’, avec le
format « x.yyyy.000 », soit «1.0001.000 », et donc « Groupe = 1 » et « Sous-priorité = 1 ».

C’est tout ce qu’il y a à savoir concernant les priorités, c’est plus long à expliquer qu’à
comprendre et à mettre en pratique.

5.7. Récapitulatif des commandes NVIC

Nous allons énumérer les fonctions disponibles concernant notre contrôleur


d’interruptions. Nous en avons vu les principales, je vais brièvement commenter les autres :

// Définit le groupement des priorités


void NVIC_SetPriorityGrouping(uint32_t priority_grouping)

// Active l’interruption de numéro renseigné


void NVIC_EnableIRQ(IRQn_t IRQn)

// Désactive l’interruption de numéro renseigné


void NVIC_DisableIRQ(IRQn_t IRQn)

// Retourne le numéro de l’interruption si celle-ci est en attente de traitement


uint32_t NVIC_GetPendingIRQ (IRQn_t IRQn)

// Force l’interruption à être traitée


void NVIC_SetPendingIRQ (IRQn_t IRQn)
// Signale l’interruption comme plus à traiter

86
void NVIC_ClearPendingIRQ (IRQn_t IRQn)

// Retourne le numéro de l’interruption si elle est active


uint32_t NVIC_GetActive (IRQn_t IRQn)

// Détermine la priorité de l’interruption


void NVIC_SetPriority (IRQn_t IRQn, uint32_t priority)

// Retourne la priorité de l’interruption spécifiée


uint32_t NVIC_GetPriority (IRQn_t IRQn) Read priority of IRQn

// Désactive toutes les interruptions IRQ (macro)


__disable_irq()

// Réactive toutes les interruptions IRQ (macro)


__enable_irq()

// Force le reset de l’ARM


void NVIC_SystemReset (void)

Concernant la dernière fonction, celle-ci vous permet de provoquer un reset de votre ARM
par software. On ne trouve aucune trace de cette possibilité dans le datasheet du LPC1768 au
niveau de l’énumération des causes de reset.

Du reste, si vous analysez cette fonction, vous verrez qu’elle fait appel au bit
SYSRESETREQ du registre AIRCR. Or le datasheet précise en plus explicitement :

This is intended to force a large system reset of all major components except for debug. Note:
support for SYSRESETREQ is not included in LPC17xx devices.

Mais pourtant… ça fonctionne sur mon LPC1768. Si vous trouvez une explication quelque
part, vous pouvez me l’envoyer. Comme quoi il ne faut jamais hésiter à expérimenter, on
découvre souvent des choses imprévues. Erreur dans le datasheet ? Exception pour le
LPC1768 à la restriction concernant les LPC17xx ? Je penche pour la seconde possibilité.

5.8. Interruptions et Timers par la pratique

Nous allons clôturer ce chapitre sur les interruptions par un petit exercice simple sur les
interruptions : Nous allons faire clignoter quelques Leds à l’aide d’interruptions.

Commencez par créer un projet « LedsInt ». Placez-y les petits commentaires utiles.
Comme librairie, référencez « GLCD.c ». Ajoutez l’inclusion du fichier de définition de cette
librairie.

Vous aurez également besoin de référencer « Timing.c » sinon vous obtiendrez une erreur,
la librairie « GLCD.c » utilisant cette librairie. Par contre, nous n’avons pour l’instant aucun
besoin apparent d’ajouter l’inclusion du fichier de définition « Timing.h », nous n’avons pas
besoin d’accéder à cette librairie à partir de notre programme principal.

Voici l’en-tête de notre exercice :


87
//////////////////////////////////////////////////////////////////////////////////////////
// TUTORIEL : CHENILLARD SUR INTERRUPTIONS //
//////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------
// Auteur : Bigonoff
// Date : 01/06/2015
// Révision : 1.0
// Cible : Carte LandTiger - Cortex-M3 NXP LPC1768
//----------------------------------------------------------------------------------------
// Historique:
// -----------
// V1.0 : 01/06/2015 : Première version opérationnelle
//----------------------------------------------------------------------------------------
// Explications:
// -------------
// Ce petit programme permet de se familiariser avec les interruptions
//
//////////////////////////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
// INCLUDES //
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////

#include "LPC17xx.h" // toujours présent


#include "LPC17xx_Bits.h" // toujours présent
#include "GLCD.h" // accès à la librairie graphique

Dans notre programme, nous allons utiliser l’écran, avec le même format que le tutoriel
précédent, ainsi que des Leds. Afin de travailler proprement, nous allons définir dans notre
zone des définitions les caractéristiques de tout ceci. Comme ça, en cas de modification ou
d’adaptation nous n’aurons que ces valeurs à modifier. C’est une saine habitude et une façon
professionnelle de travailler :

//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
// DEFINES //
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////

// Écran
#define BACKCOLOR BLACK // Couleur de fond d'écran

// Titre
#define TXTTITLE "Tutoriel LedsInf" // titre écran
#define XTITLE 32 // coordonneés du titre
#define YTITLE 8
#define COLTITLE BLUE // couleur du titre

// Compteur
#define TXTCMPT "Compteur: 000" // texte du compteur
#define COLCMPT WHITE // couleur du compteur
#define XCMPT 16 // Coordonneés compteur
#define YCMPT 96
#define XVAL 176 // abscisse du premier chiffre

// Leds
#define LEDS_FIOPIN LPC_GPIO2->FIOPIN0 // port pour leds
#define LEDS_FIODIR LPC_GPIO2->FIODIR // direction leds

88
Nous allons, dans notre zone des prototypes, déclarer les fonctions d’initialisation dont
nous aurons besoin :

//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
// FONCTIONS PROTOTYPES //
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////

void InitializeGPIO(void); // Initialiser GPIO


void InitializeGLCD(void); // initialiser écran
void InitializeTimer(void); // initialiser timer

Notre fonction « main() » va se contenter d’appeler les différentes initialisations :

//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
// MAIN //
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
int main(void)
{
SystemInit(); // initialise horloge : INDISPENSABLE
InitializeGPIO(); // initialise GPIO
InitializeGLCD(); // Initialiser GLCD
InitializeTimer(); // Initialiser timer

while(true) // boucle principale


{
}
}

Occupons-nous de nos routines d’initialisation. Tout d’abord l’initialisation des GPIO :

//////////////////////////////////////////////////////////////////////////////////////////
// INITIALISATION DES GPIO //
//////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------
// Contient l'initialisation des pins GPIO.
// La configuration des pins LCD_DIR (P0.21) et LCD_EN (P0.19) est indispensable:
// Il faut mettre le transceiver SN74ALVC164245 en entrée côté ARM pour éviter les conflits
// avec les pins P2.0/P2.7 (LCD_DIR = 1) tout en évitant d'avoir un conflit entre la sortie
// du latch 74LV573PW et le canal A du transceiver -> lCD_EN = 0
//----------------------------------------------------------------------------------------
void InitializeGPIO(void)
{
// INDISPENSABLE
// -------------
LPC_GPIO0->FIODIR = 0x00280000; // LCD_DIR et LCD_EN en sortie
LPC_GPIO0->FIOPIN |= 0x00200000; // Transceiver en entrée, latch hors-service
}

Ensuite nous initialisons l’écran :

89
//////////////////////////////////////////////////////////////////////////////////////////
// INITIALISE L'ÉCRAN GLCD //
//////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------
// Initialise le driver et l'écran GLCD.
// En cas de problème, allumage de toutes les Leds
//----------------------------------------------------------------------------------------
void InitializeGLCD(void)
{
if (!GLCD_Initialize()) // initialiser l'écran
{ // si échec
LEDS_FIODIR = 0xFF; // Leds en sortie
LEDS_FIOPIN = 0xFF; // toutes les leds allumées
while (true); // on arrête là
}

GLCD_Clear(BACKCOLOR); // Effacer l'écran


GLCD_SetTextColors(BACKCOLOR,COLTITLE); // Couleurs du titre
GLCD_Print(XTITLE,YTITLE,TXTTITLE); // Écrire texte

GLCD_SetTextColor(COLCMPT); // Couleur du compteur


GLCD_Print(XCMPT,YCMPT,TXTCMPT); // écrire compteur
}

Bien entendu, nous utilisons nos définitions pour rendre le tout paramétrable depuis notre
zone « Defines ».

Occupons-nous maintenant de notre routine d’activation/désactivation des Leds. Nous


allons réaliser deux fonctions « inline », autrement dit, deux macros.

Petit conseil en passant : Si vous pensez qu’une de vos fonctions risque d’être appelée
alors que son exécution précédente n’est pas terminée, faites-en une fonction inline si elle
n’est pas trop grande, sinon vous devrez trouver une autre solution (doubler la fonction, la
créer réentrante etc.).

Le cas que je cite pourrait arriver si, par exemple, une même fonction était utilisée par le
programme principal ainsi que par une routine d’interruption : La fonction en cours
d’exécution par le programme principal pourrait être interrompue et exécutée une seconde
fois par la routine d’interruption : Pensez à ce qu’il adviendrait par exemple des variables
utilisées dans la fonction.

Corolaire : Si vous appelez une fonction d’une librairie à partir d’une interruption, vérifiez
si cela ne pose aucun problème, principalement si votre programme principal utilise
également des fonctions de la même librairie.

De même, soyez très prudents si vous avez des variables globale qui peuvent être
accessibles à plusieurs niveaux simultanément, réfléchissez bien à tous les cas de figure et, si
possible, évitez de vous retrouver dans cette éventualité.

Ceci étant dit, voyons nos deux fonctions inline :

90
//////////////////////////////////////////////////////////////////////////////////////////
// ACTIVATION DES LEDS //
//////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------
// Active les leds et les allume
// leds: valeur d'allumage
//----------------------------------------------------------------------------------------
static __inline void EnableLeds(unsigned char leds)
{
LEDS_FIOPIN = leds; // placer valeurs leds
LEDS_FIODIR = 0xFF; // toutes les leds en sortie
LPC_GPIO2->FIOCLR |= 1UL<<11; // 0 sur P2.11
LPC_GPIO2->FIODIR |= 1UL<<11; // P2.11 en sortie
}

//////////////////////////////////////////////////////////////////////////////////////////
// DÉSACTIVATION DES LEDS //
//////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------
// Désactive les leds
//----------------------------------------------------------------------------------------
static __inline void DisableLeds(void)
{
LPC_GPIO2->FIODIR &= ~(1UL<<11); // P2.11 en entrée
}

On ne peut plus simple, je ne reviens pas sur le principe, expliqué dans le précédent
tutoriel. Ajoutez évidemment les deux prototypes dans notre zone prévue pour cet usage :

//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
// FONCTIONS PROTOTYPES //
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
void InitializeGPIO(void); // Initialiser GPIO
void InitializeGLCD(void); // initialiser écran
void InitializeTimer(void); // initialiser timer
static __inline void EnableLeds(unsigned char leds); // activation des Leds
static __inline void DisableLeds(void); // désactivation des Leds

Entrons maintenant dans ce qui nous intéresse : L’initialisation de notre timer et de son
interruption. Tout d’abord il nous faut choisir un timer. On pourrait se dire qu’il suffit de
prendre le timer0. Cependant, souvenez-vous que la librairie « GLCD.c » utilise la librairie
« Timing.c ». Si nous décidons d’utiliser le timer0 pour notre interruption, il faudra que la
librairie « Timing.c » en utilise un autre, ce qui nous oblige à appeler la fonction
« Timing_InitializeTimer() » en précisant un timer différent, avant d’initialiser l’écran.

C’est un choix. J’ai choisi de laisser la librairie « Timing.c » sélectionner son timer par
défaut, le timer0, lors du premier appel d’une fonction par la librairie « GLCD.c ». Je vais
donc utiliser arbitrairement le timer2 pour générer les interruptions.

Vous pouvez penser que tout ceci est lié à mes librairies, et que si vous ne les utilisiez pas,
le timer0 resterait libre. En fait, dès que vous accéderiez à l’écran ou à d’autres fonctions de
la carte, vous seriez aux prises avec l’obligation de réaliser des temporisations. Vu le
fonctionnement de l’ARM, vous en concluriez que vous devez utiliser un timer, et celui que
vous utiliseriez ne serait plus disponible pour le programme principal : Le résultat serait

91
identique, ou du moins comparable. La tentative d’optimisation à ce niveau induirait
énormément de complication dans les fonctions de pilotage du hardware de la carte.

//////////////////////////////////////////////////////////////////////////////////////////
// INITIALISE LE TIMER 2 //
//////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------
// Initialise le timer 2 pour déclencher une interruption toutes les 500ms
//----------------------------------------------------------------------------------------
void InitializeTimer(void)
{
}

Ceci va nous permettre d’aborder un sujet qui vous intéressera dans pratiquement tous vos
projets : L’utilisation des timers. Nous allons également rencontrer des concepts qui
concernent pratiquement tous les modules hardwares internes à l’ARM.

La première étape lorsqu’on utilise un module interne, c’est de l’alimenter. Par défaut,
certains le sont, d’autres pas, il convient de vérifier dans le datasheet. Dans le chapitre
concernant les timers, il est précisé :

Remark: On reset, Timer0/1 are enabled (PCTIM0/1 = 1), and Timer2/3 are disabled
(PCTIM2/3 = 0).

Autrement dit, notre Timer2 n’est pas alimenté par défaut (c’est du reste pourquoi je l’ai
choisi pour rencontrer tous les cas de figure, il s’agit d’un exercice).

L’alimentation d’un module s’effectue via le registre « PCONP », pour « Power Control
for Peripherals Register ». Si vous ouvrez le fichier « LPC_17xx_Bits.h » que je vous ai écrit,
et que vous avez inclus en début de votre fichier « main.c », vous trouverez la définition de
tous les bits de ce registre, sous forme symbolique.

La ligne qui nous intéresse est :

#define PCONP_PCTIM2 0x400000 // Timer 2 power/clock control bit (0)

Sachant que pour alimenter un module il faut positionner le bit correspondant à 1, et que
pour l’éteindre il faut le mettre à 0. L’accès au registre est rappelé dans le commentaire d’en-
tête de cette zone :

//////////////////////////////////////////////////////////////////////////////////////////
// PCONP : LPC_SC->PCONP //
//////////////////////////////////////////////////////////////////////////////////////////

Pour alimenter notre timer2 nous devrons donc écrire :

LPC_SC->PCONP |= PCONP_PCTIM2; // Timer2 alimenté

N’oubliez pas le signe « | », sinon vous éteindriez tous les modules internes sauf le
timer2. Cette procédure est commune à tous les modules internes, vous devrez chaque fois
vérifier si celui dont vous avez besoin est allumé ou éteint au reset. S’il est éteint, pensez à
l’alimenter. Si vous avez besoin d’économiser l’énergie au maximum (piles), vous pouvez
92
éteindre les modules inutilisés. Il s’agit bien d’alimentation (Power Control) pas d’une simple
activation software.

La seconde opération, commune également, consiste à définir l’horloge fournie au


module. En effet, vous disposez de la possibilité de choisir entre plusieurs valeurs de diviseurs
par rapport à l’horloge principale de notre ARM, qui est de 100Mhz sur notre carte LandTiger®
avec les configurations appliquées via notre fichier « startup_LPC17xx.s ».

Á propos, pour ceux qui se demandent pourquoi diable on a utilisé un quartz de 12Mhz
pour obtenir une fréquence de 100Mhz, la raison est simplement que le module USB a besoin
de cette fréquence de 12Mhz.

Bref, l’horloge qui va servir à calculer les temps dans notre timer2 se définit dans un des
deux registres : « PCLK_SEL0 » ou « PCLK_SEL1 », selon le module utilisé. Le datasheet précise
que pour le timer2 il s’agit de « PCLK_SEL1 », mais, de nouveau, ceci est décrit dans le fichier
« LPC_17xx_Bits.h », il vous suffit de rechercher après le timer2 :

#define PCLKSEL1_TIM2_4 0x00000000 // TIMER 2


#define PCLKSEL1_TIM2_1 0x00001000
#define PCLKSEL1_TIM2_2 0x00002000
#define PCLKSEL1_TIM2_8 0x00003000
#define PCLKSEL1_TIM2_MASK 0xFFFFCFFF

Vous voyez que vous disposez de 4 possibilités d’horloge, le diviseur est explicitement
indiqué et la valeur par défaut au reset est celle qui vaut 0, soit PCLKSEL1_TIM2_4.

Le diviseur appliqué par défaut vaut donc « 4 ». La fréquence par défaut appliquée au
timer2 par défaut sera donc 100Mhz/4 = 25Mhz. Si vous avez besoin d’une fréquence plus
haute, pour plus de précision, vous pourrez utiliser le diviseur « 1 » ou le diviseur « 2 ». Si,
par contre, vous avez besoin de durées plus longues, vous pourrez utiliser le diviseur « 8 »,
soit 12.5Mhz. Si vous êtes très critique niveau consommation (montage sur piles), utilisez la
fréquence la plus faible valide pour votre programme.

La valeur « MASK » permet, en appliquant un opérateur « & », de remettre à 0 les bits


concernés, et donc de revenir au diviseur « 4 ».

Pour nous familiariser avec ces registres, nous allons choisir d’utiliser un diviseur « 8 ».
Nous devons forcer les bits correspondants dans le registre concerné. Ce registre est indiqué
en préfixe, il s’agit de « PCLKSEL1 » et c’est du reste rappelé dans le commentaire d’en-tête :

//////////////////////////////////////////////////////////////////////////////////////////
// PCLKSEL1 : LPC_SC->PCLKSEL1 //
//////////////////////////////////////////////////////////////////////////////////////////

// Diviseur d'horloge pour les périphériques - Part 2


// --------------------------------------------------

Pour sélectionner une horloge de 12.5Mhz pour notre timer2, nous devrons donc écrire :

LPC_SC->PCLKSEL1 |= PCLKSEL1_TIM2_8 ; // Timer 2 en 100Mhz/8

93
Constatez qu’une fois le travail préparé avec des fichiers adéquats, tout devient de suite
très simple. Construire de tels fichiers n’a rien de compliqué, c’est juste long. Il suffit
d’étudier le datasheet du « LPC_1768 ».

Quand vous travaillez avec une nouvelle cible, je vous conseille de vous construire ce
genre de fichier au fur et à mesure des besoins, vous gagnerez énormément de temps pour vos
projets suivants. La preuve ici…

Voici notre module alimenté et son horloge définie, ces étapes sont communes à
l’utilisation de tous les modules de votre ARM. Maintenant, entrons dans les spécificités de
notre timer.

Votre timer peut fonctionner en mode timer proprement dit (il compte les cycles
d’horloge) ou en mode compteur (il compte les impulsions reçues de l’extérieur). Pour ceux
qui ont étudié les PIC®, constatez que bien que sur un processeur 32 bits d’une autre firme, on
reste sur des principes connus. Notez que les timers sont capables de bien des opérations,
comme les captures ou le déclenchement de transferts DMA (Direct Memory Access).

Nous devons donc maintenant préciser le mode de fonctionnement de notre timer. Ceci
s’effectue à l’aide du registre « CTCR ». Notre fichier « LPC17xx.h » nous apprend que ce
registre est défini dans une structure de type « LPC_TIM_TypeDef ». Vous vous en doutez,
s’agissant de notre timer2, la structure s’appellera « LPC_TIM2 ».

Du reste, s’agissant de timers, j’ai évidemment défini les différents bits dans le fichier
approprié, c’est-à-dire dans « timing.h ». Ouvrez ce fichier, vous y trouvez toutes les
définitions intéressant nos timers, dont :

//////////////////////////////////////////////////////////////////////////////////////////
// CTCR : LPC_TIMx->CTCR //
//////////////////////////////////////////////////////////////////////////////////////////

// Count/Timer Control Register : Sélection timer/compteur


// --------------------------------------------------------

// mode (un seul choix)


#define TIMCTCR_TIMERMODE 0x00 // Mode timer
#define TIMCTCR_COUNTRISE 0x01 // Mode compteur flanc montant
#define TIMCTCR_COUNTFALL 0x02 // Mode compteur flanc descendant
#define TIMCTCR_COUNTBOTH 0x03 // Mode compteur flancs montant + descendant

Nous y trouvons à la fois la définition des bits à positionner, mais également l’accès au
registre : LPC_TIM2->CTCR. Nous travaillons en mode timer, nous allons donc écrire :

LPC_TIM2->CTCR = TIMCTCR_TIMERMODE; // timer2 en mode timer

Naturellement, si nous nous contentons de ça nous obtiendrons une erreur indiquant que
cette définition est inconnue. Effectivement, nous n’avons pas jugé utile d’inclure le fichier
« timing.h » à notre projet, c’était sans compter les définitions destinées à nous simplifier la
vie.

Ajoutons la directive :

94
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
// INCLUDES //
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
#include "LPC17xx.h" // toujours présent
#include "LPC17xx_Bits.h" // toujours présent
#include "GLCD.h" // accès à la librairie graphique
#include "timing.h" // accès aux définitions des timers

Nous n’avons rien d’autre à définir dans ce registre car nous travaillons en mode timer.
En mode compteur nous devrions encore nous occuper de la pin utilisée. Á ce propos, sachez
que sur l’ARM les pins reliées aux modules internes ne sont pas toutes figées, certaines
peuvent avoir plusieurs emplacements possibles, qu’il vous appartient de préciser lors de
l’initialisation du module correspondant, en suivant la procédure imposée par le datasheet.

Nous avons choisi notre timer, nous l’avons mis sous tension, défini la fréquence de son
horloge, et configuré en timer. Nous avons maintenant la possibilité de définir un prédiviseur,
sur 32 bits, à appliquer à l’horloge du module pour indiquer après combien de « clocks » le
timer doit incrémenter d’une unité.

Notre timer fonctionne avec une horloge de 12.5MHz, et donc un « clock » dure 1/(12,5 *
106), soit 0,08µs (80ns). Constatez la précision des timers, alors même que nous avons
pourtant divisé l’horloge par 8. Sans cette division nous aurions une durée de clock de 0,01µs,
soit 10ns.

Nous n’avons pas besoin d’une pareille résolution pour notre temporisation de 500ms,
même si ça ne dérange pas. Arbitrairement nous allons fixer la durée d’une incrémentation à
1ms. Nous travaillons avec des registres 32 bits, vous ne rencontrerez jamais de cas où vous
serez ennuyés pour obtenir le bon diviseur ou la bonne durée, ni coincé avec des temps
disponibles trop courts : Vous voulez, vous pouvez ! Elle est belle la vie en 32 bits…

Pour avoir une durée de 1ms à partir de 80ns nous devons diviser par 1000000/80 =
12500. Logique, 1ms correspond à une fréquence de base de 1Khz, et nous avons une horloge
de 12,5Mhz, soit 12500 fois plus rapide. Constatez que vous avez plusieurs façons de calculer
vos diviseurs.

Nous devons donc placer notre prédiviseur dans le registre concerné, qui est le registre
« PR ». Attention cependant, il y a un piège. Même si dans notre cas actuel ça n’a pas la
moindre importance, ça pourrait être critique sur des timings précis, d’autant plus si la valeur
à placer dans PR est faible.

Pour voir le piège, il faut comprendre le fonctionnement. Le contenu du prédiviseur


(registre PCR pour Prescale Counter Register) s’incrémente à chaque évènement jusqu’à
atteindre la valeur fixée dans PR. Lors de l’évènement suivant, le contenu de PCR revient à 0
et le contenu du timer est incrémenté d’une unité.

De ce fait, si vous placez « 0 » dans PR, le contenu du prédiviseur débordera à chaque


évènement reçu, et donc tous les «1 » évènements. Si vous placez « 1 », le premier
évènement fera passer PCR à « 1 » et le second le fera déborder. Nous aurons donc une
incrémentation du contenu du timer tous les « 2 » évènements. On peut généraliser en disant
95
qu’on aura une division des évènements par « PR+1 ». Il faut donc placer dans « PR » la
valeur calculée moins 1.

PR = prédiviseur calculé - 1

Nous devrons donc placer la valeur 12499 dans PR. Comprenez qu’ici ça n’a pas la
moindre importance, mais si on travaillait avec un prédiviseur de 10, l’oubli de cette règle
induirait une erreur de 10%.

Notez au passage que le registre PCR est accessible, vous n’avez jamais de perte
d’informations si vous utilisez un prédiviseur, puisque vous savez combien d’évènements ont
été comptabilisés par ce prédiviseur : Ceci vous donne en réalité les mêmes possibilités qu’un
timer 64 bits (au cas où 32 bits ne seraient pas encore assez).

Le registre « PR » fait partie de la même structure que les registres précédents, et donc
nous écrirons :

LPC_TIM2->PR = 12500 - 1; // une incrémentation par ms

Nous allons devoir définir sur quel évènement nous déclenchons l’interruption du timer2,
car nous avons le choix. Nous disposons de 4 comparateurs « MR » pour « Match Register »,
à savoir « MR0 », « MR1 », « MR2 » et MR3. Chacun de ces registres fonctionne de façon
identique et est capable d’exécuter plusieurs opérations lorsque la valeur du timer atteint celle
d’un registre MR. Ceci vous donne une puissance incomparable au niveau de votre timer,
capable d’exécuter plusieurs actions différentes au sein d’un même cycle de comptage.

Réaliser plusieurs PWM avec un timer unique, par exemple, devient un jeu d’enfant, créer
des signaux complexes également, et je vous passe toutes les possibilités envisageables.

Nous allons maintenant déterminer comment fonctionnent nos registres MR. En fait, pour
cette application nous pourrions n’en utiliser qu’un seul, puisqu’une interruption toutes les
500ms suffit, mais je vais construire ce programme en utilisant deux de ces registres :

MR0 va déclencher après 500ms et provoquera le déplacement des Leds.


MR1 va déclencher après 1 secondes et provoquera en plus l’incrémentation du compteur

Ceci va induire que :

MR0 devra déclencher simplement une interruption, le timer continuant de compter


MR1 devra déclencher une interruption et reseter le timer, puisqu’on redémarre un cycle

Le mode de fonctionnement des registres MR se paramètre dans le registre MCR (Match


Control Register). Bien évidemment je vous ai à nouveau mâché le travail, les modes sont
définis dans le fichier « timing.h » :

96
//////////////////////////////////////////////////////////////////////////////////////////
// MCR : LPC_TIMx->MCR //
//////////////////////////////////////////////////////////////////////////////////////////

// Match Control Register : Sélection de l'action à effectuer sur égalités


// -----------------------------------------------------------------------

// Timer Counter Register = Match Register 0 (cumulatives)


#define TIMMCR_MR0I 0x01 // Interruption
#define TIMMCR_MR0R 0x02 // Reset
#define TIMMCR_MR0S 0x04 // Stop

// Timer Counter Register = Match Register 1 (cumulatives)


#define TIMMCR_MR1I 0x08 // Interruption
#define TIMMCR_MR1R 0x10 // Reset
#define TIMMCR_MR1S 0x20 // Stop

// Timer Counter Register = Match Register 2 (cumulatives)


#define TIMMCR_MR2I 0x40 // Interruption
#define TIMMCR_MR2R 0x80 // Reset
#define TIMMCR_MR2S 0x100 // Stop

// Timer Counter Register = Match Register 3 (cumulatives)


#define TIMMCR_MR3I 0x200 // Interruption
#define TIMMCR_MR3R 0x400 // Reset
#define TIMMCR_MR3S 0x800 // Stop

Le terme « cumulatives » indique que vous devez faire une opération « or » pour toutes
les options désirées. En ce qui nous concerne, nous voulons :

TIMMCR_MR0I : Interruption sur correspondance avec MR0


TIMMCR_MR1I : Interruption sur correspondance avec MR1
TIMMCR_MR1R : Reset sur correspondance avec MR1

Le timer va donc générer une première interruption après 500ms, et à 1000ms il


déclenchera une seconde interruption et redémarrera à 0 pour un nouveau cycle.

Ce qui nous donne, l’accès au registre MCR étant rappelé dans le commentaire supérieur :

// Interruption sur correspondance MR0 et MR1, reset sur MR1


LPC_TIM2->MCR = TIMMCR_MR0I | TIMMCR_MR1I | TIMMCR_MR1R;

Il nous faut maintenant initialiser nos registres MR0 et MR1 pour obtenir des temps de
respectivement 500ms et 1000ms. Il s’agit d’une correspondance, pas d’un débordement.
Nous plaçons donc la bonne valeur directement dans nos registres. Avec un temps
d’incrémentation de 1ms, les calculs sont aisés à effectuer :

LPC_TIM2->MR0 = 500; // correspondance MR0 après 500ms


LPC_TIM2->MR1 = 1000; // Correspondance MR1 après 1seconde

Il nous reste à démarrer notre timer, via le registre TCR (Timer Control Register), dont les
bits sont de nouveau décrits dans le fichier « timing.h », non sans l’avoir préalablement reseté
par précaution, et donc :

97
LPC_TIM2->TCR = TIMTCR_RESET; // reset du timer
LPC_TIM2->TCR = TIMTCR_ENABLE; // démarrage du timer

La souplesse de notre timer nous impose plusieurs étapes, mais aucune n’est compliquée
ni n’exige de douloureux compromis : Tout est simple et évident.

Il nous reste une dernière étape : Mettre en service les interruptions sur le Timer2. Mais
avant, par précaution, nous effacerons les flags éventuellement déjà positionnés. Les flags
d’interruption pour les timers se trouvent dans les registres IR (Interrupt Register). De
nouveau, tout est défini dans le fichier « timing.h » :

//////////////////////////////////////////////////////////////////////////////////////////
// IR : LPC_TIMx->IR //
//////////////////////////////////////////////////////////////////////////////////////////

// Interrupt Register : flags interruptions des timers : écrire 1 pour reseter


// ---------------------------------------------------------------------------

#define TIMIR_MR0 0x01 // Interrupt Interrupt flag for match channel 0


#define TIMIR_MR1 0x02 // Interrupt Interrupt flag for match channel 1
#define TIMIR_MR2 0x04 // Interrupt Interrupt flag for match channel 2
#define TIMIR_MR3 0x08 // Interrupt Interrupt flag for match channel 3
#define TIMIR_CR0 0x10 // Interrupt Interrupt flag for capture channel 0 event
#define TIMIR_CR1 0x20 // Interrupt Interrupt flag for capture channel 1 event

Remarquez la particularité suivante: Lorsqu’un flag d’interruption est positionné, il prend


la valeur “1”. Cependant, pour reseter un flag, vous devez écrire également « 1 ». Pour
remettre un bit à « 0 » vous devez donc y écrire « 1 ». L’ARM est truffé de particularités de ce
style, lisez toujours attentivement le datasheet et ne pensez jamais que « c’est évident ».

Nous allons reseter tous les bits à la fois, nous écrirons 0x3F dans le registre.

Ne placez jamais des « 1 » dans des bits non utilisés d’un registre de l’ARM, ceci pourrait
provoquer des fonctionnements imprévisibles. Si un bit n’est pas utilisé, ou qu’il est réservé,
n’y écrivez qu’un niveau « 0 ».

Après le reset par précaution des flags (inutile dans notre cas, mais c’est une sage
habitude), nous allons lancer les interruptions, via le contrôleur d’interruptions NVIC, ainsi
que nous l’avons déjà étudié :

LPC_TIM2->IR = 0x3F; // reset de tous les flags


NVIC_EnableIRQ(TIMER2_IRQn); // Interruptions Timer2 en service

Beaucoup d’explications pour pas grand-chose au final, puisque voici notre fonction
d’initialisation de notre timer au grand complet:

98
//////////////////////////////////////////////////////////////////////////////////////////
// INITIALISE LE TIMER 2 //
//////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------
// Initialise le timer 2 pour déclencher une interruption toutes les 500ms
//----------------------------------------------------------------------------------------
void InitializeTimer(void)
{
LPC_SC->PCONP |= PCONP_PCTIM2; // Timer2 alimenté
LPC_SC->PCLKSEL1 |= PCLKSEL1_TIM2_8 ; // Timer 2 en 100Mhz/8
LPC_TIM2->CTCR = TIMCTCR_TIMERMODE; // timer en mode timer
LPC_TIM2->PR = 12500 - 1; // une incrémentation par ms

// Interruption sur correspondance MR0 et MR1, reset sur MR1


LPC_TIM2->MCR = TIMMCR_MR0I | TIMMCR_MR1I | TIMMCR_MR1R;

LPC_TIM2->MR0 = 500; // correspondance après 500ms


LPC_TIM2->MR1 = 1000; // Correspondance après 1seconde

LPC_TIM2->TCR = TIMTCR_RESET; // reset du timer


LPC_TIM2->TCR = TIMTCR_ENABLE; // démarrage du timer
LPC_TIM2->IR = 0x3F; // reset de tous les flags
NVIC_EnableIRQ(TIMER2_IRQn); // Interruptions Timer2 en service
}

Nous allons maintenant construire notre « handler d’interruption », pour l’appeler par
son nom officiel. Nous avons vu qu’il sera de la forme « void xxx_Handler (void) ». Son nom
est imposé par le fichier « startup_LPC17xx.s ». Concernant notre timer2, ce sera donc :

//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
// INTERRUPTIONS //
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////////////////////////
// INTERRUPTION TIMER 2 //
//////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------
// Déclenchée toutes les 500ms en alternance par MR0 et MR1
//----------------------------------------------------------------------------------------
void TIMER2_IRQHandler (void)
{
}

N’oubliez pas de placer des zones et des titres. Contrairement à ce que certains esprits
chagrins, parfois partisans du moindre effort, propagent sur le net en tant qu’idéologie :
« Trop de commentaires ne tuent pas les commentaires », pour la bonne raison que « trop de
commentaires » est un argument fallacieux: Les commentaires sont lus par ceux qui étudient
votre code source, ou par vous-même, et si vous avez jugé utile de commenter, c’est que les
explications fournies sont utiles, ils ne sont jamais « de trop ». Lisez mes sources sur mon
site, et posez-vous la question de savoir si vous auriez compris mon code d’avantage avec
moins de commentaires.

Revenons à notre handler. L’interruption sera déclenchée une fois par la comparaison avec
MR0 et une fois par celle avec MR1. Nous avons décidé qu’à chaque interruption nous ferions
progresser nos Leds, et qu’à l’interruption due à MR1 nous ferions progresser le compteur.

99
Il est important d’avoir toujours le schéma de la carte concernant la partie dans laquelle
nous travaillons en tête. Nous devons modifier les Leds et l’affichage, et ce sont les mêmes
sorties qui sont utilisées pour les deux périphériques, du fait du multiplexage des fonctions
particulièrement élevé sur cette carte, très bien fournie en périphériques, nous n’allons pas
nous en plaindre.

Nous avons plusieurs façons de travailler dans le cas qui nous occupe, par exemple :

- Le handler d’interruption peut positionner un flag pour modifier les Leds et un flag
pour modifier le compteur, à charge pour le programme principal d’effectuer les vraies
écritures. L’avantage est qu’on ne monopolise pas les interruptions pour écrire dans
l’écran, l’inconvénient est que les interruptions dans notre cas ne servent plus à grand-
chose, autant tester le flag du timer directement.

- Le handler d’interruption peut gérer directement les Leds et l’affichage du compteur.


L’avantage est que le traitement est simplifié et qu’on évite des conflits,
l’inconvénient est qu’on effectue un traitement lent (l’affichage) dans le handler
d’interruption.

- Le handler d’interruption peut gérer les Leds et positionner un flag pour reporter
l’affichage du compteur sur le programme principal. L’avantage est qu’on ne perd pas
de temps dans le handler, l’inconvénient est qu’on risque d’interrompre la gestion de
l’écran par une interruption modifiant les Leds, il faut alors prendre des précautions.

Etc., nous avons en fait des tas de façons de procéder, constatez que lorsqu’on crée un
programme utilisant les interruptions qu’il y a des questions pertinentes à se poser avant de
foncer dans la programmation effective.

Dans ce tutoriel ultrasimple, ce sont plus des questions existentielles que réelles, puisque,
de toute façon, les interruptions ne surviennent que toutes les 500ms, que le programme n’a
rien d’autre à faire, et qu’il a le temps de tout faire un nombre conséquent de fois plus que ce
qui est demandé. N’empêche que, s’agissant justement d’un tutoriel, il est bon de raisonner
sainement.

Je vais choisir de ne pas encombrer inutilement le handler d’interruption avec des


opérations lentes et non prioritaires, comme la gestion de l’écran. Je reporte donc cette
opération sur le programme principal, étant entendu que les ordres de modification restent
donnés par le handler d’interruption, comme convenu.

Ceci nous pose un problème fonctionnel : Supposons que notre programme soit en train de
modifier l’écran lorsque l’interruption modifiant les Leds survient : Le résultat serait
catastrophique, car les commandes de l’écran utilisent les pins pilotant les Leds, et toute
modification brisant les séquences induirait un plantage à ce niveau.

Nous pouvons résoudre ce conflit de différentes façons :

100
- Le programme principal peut gérer l’écran en appelant une interruption software (on
dispose d’interruptions logicielles) non interruptible par l’interruption du timer. Ainsi,
le bloc d’instructions gérant l’écran serait exécuté sans interruption. C’est une façon
de faire très puissante et très pratique dans ce genre de conflits. Cependant, dans le cas
qui nous occupe ça revient au final à faire gérer l’écran dans le handler d’interruption
du timer.

- Le programme peut couper les interruptions durant les opérations d’affichage.


L’avantage est que c’est simple, l’inconvénient est que c’est un peu comme si on
allongeait le temps de l’interruption, puisqu’on l’empêche de se déclencher durant le
traitement de l’écran.

- Le programme peut couper uniquement l’interruption du timer. Ainsi les autres


interruptions prioritaires continueraient d’avoir lieu et seules les opérations générant
des conflits seraient évitées. Nous n’avons ici qu’une seule source d’interruption, et de
plus c’est de nouveau équivalent à gérer l’écran dans la routine timer, rendue moins
prioritaire que d’autres interruptions éventuelles.

Constatez que, parfois, vous vous retrouverez à faire des compromis, ou à élaborer vos
solutions de façon à éviter des conflits potentiels dès le niveau de la conception. Nous avons
posé notre cahier des charges, notre carte est construite, nous devons nous adapter.

Notez que dans notre réalité, les défauts théoriques apparents n’existent en fait
simplement pas, nous pourrions nous passer de toute précaution. En effet, le handler va
positionner le flag de modification d’écran, et nous allons revenir au programme principal qui
va boucler en tournant dans la boucle sans fin en attendant la modification du flag. La
réaction du programme principal va être pratiquement immédiate et la gestion de l’écran sera
terminée bien avant l’exécution de la prochaine interruption. Qu’on coupe ou non les
interruptions, qu’on réalise la gestion d’écran dans le handler ou dans le programme
principal, qu’on choisisse n’importe quelle autre solution : Ça ne changera strictement rien au
résultat final et il n’y aura jamais aucun délai ou retard réel. Dans notre exercice il n’y a en
fait aucun problème.

Tout ceci pour vous montrer qu’un programme semblant évident au départ peut cacher des
pièges théoriques, mais qu’une analyse des timings réels peut valider, renforcer, ou invalider
ces pièges théoriques, même si dans notre cas, l’étude du timing peut se faire aisément « à
l’estimation ».

On peut donc écrire le même code au final soit en ayant analysé tous les cas de figure, soit
en ayant tout ignoré. Le problème est que la seconde solution fonctionne parfois… et parfois
non. Et les bugs provoqués par les interruptions sont des bugs asynchrones, parfois très
difficiles à trouver si on ne reprend pas l’analyse détaillée du programme et de tous les cas de
timings possibles. Dès que vous utilisez les interruptions, pensez à envisager toutes les
possibilités, surtout sur une carte munie d’un microcontrôleur avec autant de possibilités et
autant de hardware embarqué et multiplexé.

Pensez également qu’un jour vous (ou quelqu’un d’autre) pouvez modifier le programme
ou ajouter une fonctionnalité. Si vous avez fait l’impasse sur ce qui précède, par exemple en
gérant l’écran dans le programme principal sans couper les interruptions, et que vous n’avez
pas placé un commentaire explicatif, toute modification future pourrait induire un plantage
101
asynchrone aléatoire, qui obligerait celui qui modifie à reprendre l’étude de tout le
programme pour comprendre où vous avez « simplifié » le traitement.

Pour notre programme, afin de travailler proprement, et vu que ça ne change rien en


pratique, nous couperons les interruptions durant le traitement de l’écran. Ainsi, si on ajoute
plus tard des modifications, les interruptions étant coupées à ce niveau, ça ne posera aucun
problème. Et, si l’utilisateur a besoin d’une interruption non retardée, il saura où intervenir et
pourquoi.

Après ces explications et mises en garde, revenons à notre programme. Lors de chaque
passage nous allons modifier l’allumage des Leds. Pour réaliser notre chenillard, nous avons
évidemment besoin d’une valeur de départ. Nous pouvons placer l’allumage de la première
Led directement dans notre fonction « main() », après les différentes initialisations. Nous
disposons pour ce faire d’une fonction inline « EnableLeds() » à laquelle nous passons la
valeur à placer sur les pins gérant les Leds :

int main(void)
{
SystemInit(); // initialise horloge : INDISPENSABLE
InitializeGPIO(); // initialise GPIO
InitializeGLCD(); // Initialiser GLCD
InitializeTimer(); // Initialiser timer
EnableLeds(0x01); // Allumage première Led

while(true) // boucle principale


{
}
}

Maintenant que nous avons une valeur de départ, nous allons, dans notre handler
d’interruption, décaler cette valeur d’un rang à chaque interruption, pour produire l’effet
« chenillard ». Rien de plus simple :

LEDS_FIOPIN = (LEDS_FIOPIN << 1); // Décaler leds d’un rang

Bien entendu, une fois 7 décalages effectués, notre valeur va sortir de la zone des 8 pins
gérant nos Leds (1, 2, 4, 8, 16, 32, 64,128,…..0). Nous devons alors tester la survenue de ce
cas pour remettre notre première valeur, soit 0x01, allumant la première Led :

if (LEDS_FIOPIN == 0) // Sortie, recommencer au début


LEDS_FIOPIN = 1;

Nous allons maintenant nous occuper de notre compteur. Nous déclarons un Boolean dans
les variables globales : « _changeCmpt », que nous initialisons à « false ». Par convention,
faites précéder vos variables globales d’un « underscore » suivi d’une première lettre en
minuscule. Chaque lettre début d’un mot sera ensuite en majuscule.

Le flag sera positionné par le handler d’interruption et effacé par le programme principal.
C’est une variable à accès double, mais ça ne pose aucun problème dans ce cas de figure car
on sait maîtriser un problème potentiel en réfléchissant un minimum :

102
- Si, dans le programme principal, on efface le flag d’abord puis qu’on change l’écran
ensuite, une modification du flag par le handler d’interruption alors qu’on vient de
démarrer l’affichage se traduira par un second appel de la fonction d’affichage.

- Si par contre on gère d’abord l’écran puis qu’on efface le flag, la fonction ne sera pas
appelée une seconde fois, le second positionnement du flag étant assimilé avec le
précédent.

Un positionnement du flag durant l’affichage n’interviendra jamais dans notre cas, sinon
ça voudrait dire qu’il aurait fallu plus de 1s pour gérer l’affichage précédent, nos appels étant
faits à intervalles réguliers. Mais ce n’est pas toujours le cas et l’ordre d’acquittement d’un
flag « partagé » demande réflexion. Dans notre cas nous devons incrémenter notre compteur à
chaque appel (sinon ça nécessiterait une variable globale supplémentaire). Vu que nous ne
devons rien rater, par principe nous effacerons le flag avant d’afficher (même si en pratique
nous ne raterons rien, il s’agit d’un exercice destiné à se poser les bonnes questions).

Dans notre handler, nous devons détecter spécifiquement l’interruption due à la


comparaison avec MR1, puisque c’est uniquement celle-là qui va conditionner notre
compteur.

Nous l’avons déjà vu : Nos flags se trouvent dans le registre « IR », dont les bits sont
déclarés dans « Timing.h ». Notre bit s’appelle « TIMIR_MR1 ». S’il est positionné, alors nous
positionnons notre flag.

Voici la déclaration du flag :

//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
// VARIABLES GLOBALES //
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////

Boolean _changeCmpt = false; // indique qu'on doit modifier le compteur

Et le code qui l’utilise dans le handler d’interruption :

// Gestion de l'écran
if (LPC_TIM2->IR & TIMIR_MR1) // si l'interruption est de type MR1
_changeCmpt = true; // modification du compteur demandée

Il ne nous reste qu’à effacer nos flags d’interruption : le flag MR0 si l’interruption est de
type MR0, sinon le flag MR1. Comme à chaque passage on a l’un ou l’autre flag positionné, il
suffit de reseter les deux ensemble. Souvenez-vous que pour mettre un flag à « 0 » dans le
registre IR, il faut y écrire un « 1 », ce qui donne :

// Gestion des flags d'interruption


LPC_TIM2->IR = TIMIR_MR0 | TIMIR_MR1; // Reset simultané des deux flags

Pour les doués qui vont dire « oui mais… », c’est bien vu, c’est voulu, c’est un tutoriel,
patientez. Pour les autres, poursuivez sans faire attention.

103
Voici notre handler d’interruption complet :

//////////////////////////////////////////////////////////////////////////////////////////
// INTERRUPTION TIMER 2 //
//////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------
// Déclenchée toutes les 500ms en alternance par MR0 et MR1
//----------------------------------------------------------------------------------------
void TIMER2_IRQHandler (void)
{
// Gestion des Leds
LEDS_FIOPIN = (LEDS_FIOPIN << 1); // Décaler leds d'un rang
if (LEDS_FIOPIN == 0) // Sortie, recommencer au début
LEDS_FIOPIN = 1;

// Gestion de l'écran
if (LPC_TIM2->IR & TIMIR_MR1) // si l'interruption est de type MR1
_changeCmpt = true; // modification du compteur demandée

// Gestion des flags d'interruption


LPC_TIM2->IR = TIMIR_MR0 | TIMIR_MR1; // Reset simultané des deux flags
}

Nous allons déjà tester ce que nous avons écrit. Compilez, ignorez le warning qui dit
qu’on n’a pas encore utilisé notre fonction inline « DisableLeds() », et chargez votre
programme.

L’écran affiche correctement notre compteur et le titre du tutoriel. Par contre, l’allumage
des Leds se produit une Led sur deux : 1, 3, 5,7…

Qu’est-ce qui peut bien se passer ? Pour le savoir, entrez dans une session « Debug », et
placez un point d’arrêt sur la première instruction du handler d’interruption. Tapez <F5> et,
une fois dans le handler, avancez en pas-à-pas jusque l’instruction de reset des flags, puis
tapez de nouveau <F5> et ainsi de suite. Nous constatons deux choses :

- À chaque passage dans le handler d’interruption, nous avançons d’une Led et non de
deux. Tout semble donc correct !

- À chaque passage dans le handler d’interruption nous exécutons la ligne de


positionnement du flag du compteur, alors que nous ne devrions le positionner qu’une
fois sur deux. Si nous positionnons le pointeur de la souris sur le terme « LPC_TIM2-
>IR », nous avons toujours la valeur 0x03, c'est-à-dire les flags MR0 et MR1
positionnés ensemble.

Si vous avez les deux explications, bravo. Si vous en avez au moins une, c’est bien. Si
vous n’y comprenez rien, ne paniquez pas, il vous suffit d’un peu de pratique.

L’explication pour le second phénomène est très simple : Vous avez mis un point d’arrêt
sur la première ligne de votre interruption. Quand vous arrivez au point d’arrêt un seul des
deux flags est en réalité positionné (MR0 ou MR1). Le problème c’est que votre programme
s’arrête grâce à la sonde J-TAG, mais vos périphériques, eux, continuent de fonctionner. Votre
timer continue donc de compter, et, le temps que l’affichage à l’écran se réalise et que vous
déplaciez votre souris, le timer a atteint les 500ms suivantes et le second flag se retrouve
également positionné.
104
La plus belle preuve consiste à effacer votre point d’arrêt et à le mettre sur l’instruction
« _changeCmpt = true ; ». En relançant à chaque fois par « F5 » vous constatez que vous avez
au minimum deux déplacements de Leds entre deux positionnements du flag
« _changeCmpt ». Vous en aurez parfois deux, parfois plus selon les aléas des timings, mais
vous ne passez plus sur cette ligne à chaque passage, preuve que seul MR0 ou MR1 est
positionné à l’entrée du handler d’interruption, mais pas les deux ensemble.

Nous avons l’explication du second phénomène, reste le premier. En pas-à-pas on a bien


un déplacement d’une Led par passage, le handler fonctionne théoriquement. Cependant, en
mode « Run » nous constatons un saut de deux positions.

Il s’agit manifestement d’un problème de timing puisque ça fonctionne lentement en pas-


à-pas mais pas en vitesse réelle. En fait deux interruptions sont exécutées consécutivement
sans aucun délai. Mais oui, bien sûr ! Je vous avais bien dit qu’il ne fallait jamais effacer les
flags d’interruption juste avant de sortir de l’interruption, le flag ne serait effectivement effacé
qu’après la réentrée dans la même routine d’interruption.

La solution ? Je vous l’avais donnée : Placer le reset des flags avant la sortie, et pourquoi
pas dès le début ?

Le problème est ici que si on efface les flags, on se retrouve avec l’impossibilité de tester
MR1 plus bas dans le handler puisque nous l’aurons effacé. Pour éviter de devoir mémoriser
IR avant d’effacer les flags, il nous suffit de scinder l’effacement.

MR0 peut être effacé dès le début, nous ne le testons pas. Quant à MR1, il suffit de
l’inclure au début de la zone du « if », donc immédiatement après le test. Notre handler
devient :

void TIMER2_IRQHandler (void)


{
// Effacement du flag MR0
LPC_TIM2->IR = TIMIR_MR0;

// Gestion des Leds


LEDS_FIOPIN = (LEDS_FIOPIN << 1); // Décaler leds d'un rang
if (LEDS_FIOPIN == 0) // Sortie, recommencer au début
LEDS_FIOPIN = 1;

// Gestion de l'écran
if (LPC_TIM2->IR & TIMIR_MR1) // si l'interruption est de type MR1
{
LPC_TIM2->IR = TIMIR_MR1; // Reset flag MR1
_changeCmpt = true; // modification du compteur demandée
}
}

Cette fois notre chenillard fonctionne parfaitement. Si le temps séparant le reset du flag
était trop court par rapport à la fin du handler, il suffirait par exemple d’effacer le flag deux
fois, afin de perdre le temps nécessaire.

105
Si dans un de vos handlers il est trop long ou trop compliqué d’effacer les flags après les
tests, il vous suffit de mémoriser le contenu de votre registre d’interruption dans une variable
locale, d’effacer les flags, puis de faire vos tests sur la variable.

Il nous reste simplement à nous occuper de notre compteur. Nous allons créer une
méthode « ChangeCmpt() » et donc écrire son prototype, …

//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
// FONCTIONS PROTOTYPES //
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////

void InitializeGPIO(void); // Initialiser GPIO


void InitializeGLCD(void); // initialiser écran
void InitializeTimer(void); // initialiser timer
static __inline void EnableLeds(unsigned char leds); // activation des Leds
static __inline void DisableLeds(void); // désactivation des Leds
void ChangeCmpt(void); // modification de l'écran

…et l’appeler dans la boucle sans fin du « main() ».

while(true) // boucle principale


{
if (_changeCmpt) // Si demande d'actualisation du compteur
ChangeCmpt(); // actualiser
}

Il nous reste à écrire notre fonction « ChangeCmpt() », qui démarre avec l’effacement du
flag, ainsi que nous l’avons décidé. Mais avant nous aurons besoin d’une variable contenant la
valeur du compteur à afficher. Nous n’avons besoin de la variable qu’à l’intérieur de la
fonction, ce sera donc une variable locale. Cependant, elle ne doit pas changer de valeur entre
deux appels successifs, elle devra donc être déclarée statique (« static »). Il faut ensuite
envoyer la nouvelle valeur du compteur à l’écran et incrémenter le compteur.

Nous initialisons le compteur à « 0 », et, vu qu’au démarrage l’écran indique déjà « 000 »
et que le premier appel interviendra une seconde après le démarrage du programme, nous
allons incrémenter le compteur avant de l’afficher et non après. Comme ça au premier appel
on affichera « 001 ».

Tout ceci nous donne :

//////////////////////////////////////////////////////////////////////////////////////////
// ACTUALISATION DU COMPTEUR //
//////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------
// Appelée toutes les secondes suite au positionnement du flag _changeCmpt par le
// handler d'interruption du timer2
//----------------------------------------------------------------------------------------
void ChangeCmpt(void)
{
static unsigned char cmpt = 0; // Valeur du compteur

_changeCmpt = false; // acquitter l'action


GLCD_Print_Val8(XVAL,YCMPT,++cmpt,3); // actualise le compteur après incrémentation
}

106
Tel quel ça va cependant poser problème. En effet l’affichage va modifier l’état des Leds,
et donc le handler d’interruption qui se contente d’un déplacement se retrouvera à déplacer
la dernière valeur utilisée par les fonctions d’affichage. Sans compter que les Leds vont se
retrouver en entrée, et de plus visibles durant la procédure d’affichage.

Nous devons de ce fait ajouter de quoi couper les Leds durant l’affichage, et les remettre
dans l’état où elles se trouvaient en fin d’affichage. Enfin, nous avions vu que nous ne
devions pas permettre l’interruption de cette fonction par le handler du timer2, nous
couperons donc les interruptions le temps de l’affichage.

Pour restaurer l’état actuel des Leds, nous devons le mémoriser, il nous faut une variable :
« leds ». Nous restaurerons ce que nous lisons, il ne faut donc pas que le handler du timer2
modifie les Leds après que nous ayons lu leur état : L’affectation à la variable doit donc se
faire une fois les interruptions coupées.

Pour couper et remettre les interruptions, je vous ai fait deux macros plus parlantes que les
noms d’origine, elles sont dans le fichier « LPC17xx_Bits.h » :

DISABLE_INTERRUPT; // couper les interruptions


ENABLE_INTERRUPT; // relancer les interruptions

Voilà notre fonction au complet :

//////////////////////////////////////////////////////////////////////////////////////////
// ACTUALISATION DU COMPTEUR //
//////////////////////////////////////////////////////////////////////////////////////////
//----------------------------------------------------------------------------------------
// Appelée toutes les secondes suite au positionnement du flag _changeCmpt par le
// handler d'interruption du timer2
//----------------------------------------------------------------------------------------
void ChangeCmpt(void)
{
static unsigned char cmpt = 0; // Valeur du compteur
unsigned char leds; // pour lecture des Leds

_changeCmpt = false; // acquitter l'action

DISABLE_INTERRUPT; // couper les interruptions


leds = LEDS_FIOPIN; // lire valeur des leds
DisableLeds(); // couper leds, évite perturbations
GLCD_Print_Val8(XVAL,YCMPT,++cmpt,3); // actualise le compteur après incrémentation
EnableLeds(leds); // rallume les leds
ENABLE_INTERRUPT; // relance les interruptions
}

Compilez votre programme (vous n’avez plus de warning) et chargez-le dans votre ARM.
Constatez que le tout fonctionne parfaitement et sans la moindre hésitation ni perturbation.

Félicitations, vous savez programmer, déboguer, utiliser les interruptions et manipuler les
timers. Le reste va consister à étudier les modules en fonction de vos besoins, à moins que
vous ne préfériez vous reposer sur mes librairies, que je compléterai au fur et à mesure de mes
besoins et des requêtes.

Le projet vous est fourni avec le tutoriel, tel qu’il se trouve à la fin de ce chapitre.

107
Notes :

108
6. Compléments et astuces

6.1. Le code-protect

Je commence par cette possibilité, commune à tous les microcontrôleurs, d’effectuer une
protection du code chargé. Ceci est nécessaire pour les industriels soucieux de protéger leurs
secrets logiciels.

Je ne vais pas expliquer comment procéder, car si vous travaillez en amateur vous n’avez
aucune raison de protéger votre code (il n’est connu que si vous décidez de le publier), et si
vous voulez faire du commerce de cartes programmées, vous n’avez pas besoin de mon
tutoriel pour y arriver, sinon vous devriez envisager de vous reconvertir.

Alors, pourquoi ce chapitre ? Simplement pour vous mettre en garde contre un danger qui
vous guette :

Sur l’ARM on a poussé la protection du code à son paroxysme. Afin d’éviter qu’un
utilisateur n’efface un code présent sur une carte comportant par exemple un risque létal (c’est
la version officielle), ou pour éviter qu’un utilisateur arrive à injecter dans un ARM
programmé un bout de code destiné à lire le code déjà présent, pour éviter qu’un expert arrive
à trouver une faille pour contourner la protection contre la relecture, on a muni l’ARM d’une
série de possibilités de protections très avancées.

Ceci part de la classique protection contre la relecture, mais également permet d’interdire
l’injection de code, pour, au final sur les versions actuelles, permettre au réalisateur du projet
d’interdire l’effacement de l’ARM.

Comprenez si, à partir de fichiers trouvés sur le Net, provenant de « petits plaisantins », ou
d’ignorants expérimentateurs, vous vous retrouviez à injecter dans votre ARM un fichier à
partir d’un projet prévoyant la protection totale du microcontrôleur, vous n’auriez plus aucune
possibilité de remplacer le programme ni de l’effacer : votre ARM serait bon pour la
poubelle. Idem si vous avez « joué » avec les protections sans bien comprendre ce que vous
faites.

Auparavant il y avait moyen de contourner ce cas extrême à l’aide d’astuces, et de


provoquer quand même l’effacement. Avec les versions actuelles ce n’est plus possible.
Veillez de fait à ne pas utiliser n’importe quel projet provenant de sources non sûres, ou alors
effectuez les vérifications nécessaires.

6.2. Jouons avec les tailles

L’ARM est un processeur 32 bits, il impose des contraintes concernant les adresses
utilisées. Nous allons voir ce qu’il en est à l’aide d’un morceau de code.

Ouvrez votre projet « LedSoft », histoire de ne pas nous encombrer avec les interruptions.
Nous allons ajouter une fonction qui prend en paramètre deux tableaux de 4 octets. Le but de
la fonction est de copier le premier tableau dans le second.

109
Nous écrivons :

void copyArray(unsigned char* arrayIn, unsigned char *arrayOut)


{
int i;
for(i=0;i<4;i++)
arrayOut[i] = arrayIn[i];
}

Notre fonction est on ne peut plus simple : Elle prend en paramètre un pointeur sur un
tableau d’entrée et sur un tableau de sortie, et copie les 4 octets du tableau d’entrée dans le
tableau de sortie à l’aide d’une boucle, un élément à la fois. Vous devez trouver ça logique et
évident.

Nous allons maintenant déclarer un tableau initialisé dans notre « main() », et appeler la
fonction en lui demandant de copier les octets 4 à 7 dans les emplacements 0 à 3. Pour
visualiser le résultat, nous afficherons les valeurs des emplacements 0 à 3 à l’écran.

Ceci nous donne pour les prototypes :

void InitializeGPIO(void); // Initialiser GPIO


void InitializeGLCD(void); // initialiser écran
void SetLed(Boolean bright); // allumage/Extinction Led
void SetLeds(Boolean ledsOn); // validation des leds
void copyArray(unsigned char* arrayIn, unsigned char *arrayOut);

Et pour le « main() » :

int main(void)
{
int i;
unsigned char compteur = 0; // compteur
unsigned char arrayIn[] = {1,3,5,7};
unsigned char arrayOut[] = {0,0,0,0};

SystemInit(); // initialise horloge : INDISPENSABLE


InitializeGPIO(); // initialise GPIO
Timing_InitializeTimer(TIMER0); // Initialiser timer pour tempos
InitializeGLCD(); // Initialiser GLCD

copyArray(arrayIn, arrayOut); // Appel de la fonction de copie


for(i=0;i<4;i++) // affichage du résultat à l’écran
GLCD_Print_Val8(i*50,200,arrayOut[i],1);

while(true) // boucle principale


{
Timing_WaitMs(500); // attendre 500ms
_leds = 0x00; // valeur Leds
SetLeds(true); // valider
Timing_WaitMs(500); // Attendre 500ms
SetLeds(false); // invalider Leds
GLCD_Print_Val8(176,96,compteur++,3); // actualiser compteur
_leds = 0x80; // pour led allumée
SetLeds(true); // Valider Leds
}
}

110
Rien de magique. Compilez et entrez en mode « Debug ». Les valeurs « 1 3 5 7 »
s’affichent en bas de l’écran, preuve que nos éléments 4 à 7 ont bien été copiés dans les
positions 0 à 3.

Si nous regardons notre fenêtre « Disassembly », le code généré pour la copie est
exactement ce à quoi on s’attendait (si on connaît le langage d’assemblage du LPC) :

0x00001178 2200 MOVS r2,#0x00


0x0000117A E002 B 0x00001182
0x0000117C 5C83 LDRB r3,[r0,r2]
0x0000117E 548B STRB r3,[r1,r2]
0x00001180 1C52 ADDS r2,r2,#1
0x00001182 2A04 CMP r2,#0x04
0x00001184 DBFA BLT 0x0000117C

Soit une boucle « for » transcrite en ses différentes étapes en langage d’assemblage. Je
vous traduis le code :

- 1178 : On initialise le compteur de boucles à 0, R2 est notre variable « i »

- 117A : On branche sur le test final (1182) car on n’entre dans une boucle « for » que
si la condition est vraie au départ (le compilateur ne peut pas deviner que c’est le cas,
en langage d’assemblage vous auriez économisé ce saut)

- 117C : On charge dans R3 l’octet (LDRB) n° R2 (adressage indirect pré-indexé) du


tableau pointé par R0, puisque c’est le premier paramètre reçu et donc l’adresse du
tableau est passée dans R0. Bref, ça équivaut à « R3 = arrayIn[R2] »

- 117E : On copie l’octet contenu dans R3 dans l’octet n°R2 du tableau pointé par R1
(le second paramètre, l’adresse du tableau de destination) : « arrayOut[R2] = R3 »

- 1180 : On incrémente le compteur de boucles R2

- 1182 : On compare R2 (i) avec la valeur immédiate (#) 4

- 1184 : Si R2 est inférieur à 4 on retourne en 117C

Constatez que si vous décidez d’entrer dans l’aventure du langage d’assemblage qu’il n’y
a rien de complexe, il y a uniquement plus de possibilités que dans un PIC®, par exemple.
Relire le code généré et le comprendre est relativement simple.

On ordonne donc notre ARM à copier nos 4 octets comme prévu. Cependant, en y
réfléchissant, vous ne trouvez pas un peu idiot d’écrire 32 bits en 4 étapes sur un
microcontrôleur de 32 bits? Vous allez pourtant retrouver ce cas de figure chaque fois que
vous aurez des opérations sur des tableaux d’éléments de moins de 32 bits, ainsi que dans
bien d’autres cas (travail sur des trames, des structures etc.).

On va donc essayer de dire à notre ARM que plutôt que de copier 4 octets, qu’il copie un
seul entier. L’ARM travaille en little-endian et va inverser l’ordre des octets, mais vu qu’il va
inverser à la lecture et à l’écriture, ça n’aura aucun impact.
111
La question est : Quelle syntaxe C utiliser ? En fait, c’est assez simple. Vous devez faire
croire au compilateur qu’il travaille avec des pointeurs sur des entiers non signés plutôt
qu’avec des pointeurs sur des octets non signés. Il nous suffit donc de caster les pointeurs
reçus en pointeurs sur des uint32_t :

(uint32_t*)(arrayOut) // Ceci indique que le pointeur arrayOut sera considéré


// comme pointeur sur un entier. Ne pas oublier l’étoile

Une fois que nous avons casté, il suffit d’indiquer au compilateur de mettre dans le
contenu pointé par ce pointeur le contenu pointé par l’autre pointeur, considéré également
comme pointeur sur un uint32_t.

Et donc, nous remplaçons le contenu de notre fonction par :

void copyArray(unsigned char* arrayIn, unsigned char *arrayOut)


{
*((uint32_t *)(arrayOut)) = *((uint32_t *)(arrayIn));
}

Ce qui signifie en langage C : Prend l’entier pointé par le pointeur « arrayIn » et place-le à
l’emplacement pointé par « arrayOut ». Le compilateur vous prend au mot, puisque
maintenant le code généré dans la fenêtre « Disassembly » devient :

0x00001178 6802 LDR r2,[r0,#0x00]


0x0000117A 600A STR r2,[r1,#0x00]

Soit un chargement d’entier (le LDR) suivi d’une écriture (le STR). Au lieu d’exécuter 4
fois les instructions de la boucle précédente, on n’exécute qu’une fois ces deux instructions.
Le gain est conséquent.

Pensez toujours que vous travaillez avec un microcontrôleur 32 bits, rentabilisez les 32
bits chaque fois que nécessaire. Cependant, ceci rend le code un peu moins lisible, vous devez
compenser avec de bons commentaires.

Vous pouvez évidemment travailler avec des demi-mots (16 bits) en castant en uint16_t.
Et, évidemment, vous pouvez combiner deux opérations pour copier 6 octets, ou utiliser des
boucles pour copier des multiples de 4 octets ou de deux demi-mots.

Si vous n’avez aucun avantage à tirer de ce genre d’optimisation, conservez la méthode


classique, comprenez que ce qui précède est une optimisation en taille et en vitesse, rendant le
code moins clair.

6.3. Les alignements

Faites très attention avec la pratique que nous venons de voir, à propos de l’alignement
des variables. En effet nous obligeons le compilateur à travailler sur des entiers de 32 bits,
mais les variables utilisées sont déclarées comme variables 8 bits.

112
Il faut savoir que si vous déclarez une variable 32 bits, le compilateur va la placer
automatiquement à une adresse multiple de 4. De même, si vous déclarez une variable 16 bits
elle sera placée à une adresse paire. Ceci est rendu obligatoire par le fait que la plupart des
instructions machines de l’ARM imposent cet alignement si on travaille sur des mots (32 bits)
ou des demi-mots (16 bits).

Si par malheur une instruction de ce type était exécutée sur une adresse non alignée, ceci
planterait le programme et vous connecterait sur la boucle sans fin de l’exception HardFault.

La première variable d’une fonction est toujours alignée, le compilateur s’en charge. Si
donc nous prenons un exemple :

int main(void)
{
int i; // adresse de départ : alignée
unsigned char compteur; // adresse de départ + 4
unsigned char arrayIn[4]; // adresse de départ + 5
unsigned char arrayOut[4]; // adresse de départ + 9

Vous voyez de suite que si vous manipulez les tableaux en tant qu’entiers, vous travaillez
sur des adresses non alignées, et vous avez de fait toutes les chances de provoquer le plantage
de votre programme.

Cependant, si vous testez avec notre programme précédent, vous aurez beau essayer toutes
les combinaisons de variables possibles, vous ne le planterez pas. Pourquoi ? Simplement
parce que, nous l’avons vu, le code qui manipule le tableau se résume à deux instructions en
langage machine : « LDR » et « STR », et que ces deux instructions font justement partie de
celles qui n’imposent pas un alignement. C’est un coup de chance !

Comme vous ignorez à l’avance quel sera le code généré, je vous conseille d’opter pour
de bonnes pratiques. En premier lieu, l’ordre de déclaration des variables doit être un peu
pensé, surtout dans les structures, car le compilateur ne sait pas y modifier l’ordre des
variables.

Si vous regardez cette structure :

struct test
{
int index;
unsigned char number;
uint16_t size;
uint16_t pos ;
int type;
};

Vous comprenez que pour respecter l’alignement, en prenant l’adresse relative 0x00
comme adresse de début de la structure, le compilateur va voir ceci :

113
struct test
{
int index; // adresse 0 pour un word, alignement OK
unsigned char number; // adresse 4 pour un byte, alignement OK
uint16_t size; // adresse 5 pour un half, alignement mauvais
uint16_t pos ; // adresse 7 pour un half, alignement mauvais
int type; // adresse 9 pour un word, alignement mauvais
};

Il ne va pas accepter de travailler sur des adresses non alignées, et va donc corriger
automatiquement votre structure pour en faire ceci :

struct test
{
int index; // adresse 0 pour un word
unsigned char number; // adresse 4 pour un byte
unsigned char; // 8 bits perdus pour récupérer l’alignement
uint16_t size; // adresse 6 pour un half
uint16_t pos ; // adresse 8 pour un half
int16_t ; // 16 bits perdus pour récupérer l’alignement
int type; // adresse 12 pour un word
};

Vous gaspillez donc de la place sans même vous en apercevoir, alors qu’en écrivant ceci :

struct test
{
int index; // adresse 0 pour un word, alignement OK
int type; // adresse 4 pour un word, alignement OK
uint16_t size; // adresse 8 pour un half, alignement OK
uint16_t pos ; // adresse 10 pour un half, alignement OK
unsigned char number; // adresse 12 pour un byte, alignement OK
};

C’est beaucoup mieux. Mais si vous avez des variables de type octet et que vous forcez
par la suite votre compilateur à les utiliser comme entiers, par exemple, il ne les alignera pas
tout seul, vous devrez l’y forcer préalablement afin d’être certain de l’alignement et éviter une
exception lors de vos optimisations.

Pour aligner une variable, vous pouvez utiliser mes définitions « ATTRIBUTE » et
« ALIGN4 », comme ceci :

unsigned char test[28] ATTRIBUTE ALIGN4 // Tableau aligné sur un mot

Vous pouvez également recourir à la syntaxe par défaut :

unsigned char test[28] __attribute__ ((aligned (4))); // Tableau aligné sur 1 mot

Faites-le si vous utilisez des astuces du style expliqué, ou que vous avez un intérêt
quelconque à aligner vos variables. Vous pouvez également aligner sur d’autres multiples en
changeant la valeur entre parenthèses. Vous disposez également de la directive « ALIGN »,
référez-vous à l’aide de µVision pour avoir la syntaxe complète, surtout si vous l’utilisez dans

114
des portions de code en langage d’assemblage, car l’alignement des instructions est également
obligatoire.

Bref, si votre programme plante et que vous vous retrouvez dans la boucle HardFault,
pensez à pister votre programme et à vérifier que ce n’est pas lors d’un accès à une variable
de 32 ou 16 bits que le plantage intervient. Si oui, vérifiez dans le débogueur que les adresses
utilisées sont bien alignées, sinon corrigez votre code et recourez éventuellement aux
alignements forcés.

Je rappelle quand même que si vous déclarez des variables, elles seront toujours alignées
avec leur type, le compilateur y veille.

Pensez que si vous recourez à des changements de types à la volée (cast), des
situations de désalignement peuvent survenir.

J’ai parlé de ce cas dans le cadre de l’optimisation, mais il peut survenir dans d’autres
circonstances sans même que vous ne vous en rendiez compte. Admettons que vous receviez
du mode extérieur, par liaison série par exemple, une suite d’octets structurés comme une
« trame » contenant différentes informations. Il se peut qu’on vous signale que l’en-tête de
trame comporte deux informations : La longueur de la trame codée dans un octet, suivi par
l’identificateur de trame, codé sur un mot de 16 bits, avec le format suivant :

Octet 0 : Longueur de la trame


Octet 1 : Identificateur, poids faible
Octet 2 : Identificateur, poids fort

Vous allez dès lors vouloir lire l’identificateur comme un uint16_t, pour agir en fonction
de sa valeur. Si vous faites ça, vous avez toutes les chances de planter le programme, parce
que celui qui a imaginé cette structure de trame l’a mal pensée (et ça arrive dans la vraie vie),
car le mot de 16 bits est désaligné puisque démarre à une adresse impaire à partir du début de
la trame.

Vous ne pouvez même pas créer une structure pour votre trame sous cette forme :

struct trame {unsigned char len ; unsigned short id;};

Parce que le compilateur va constater le désalignement et vous ajouter un octet entre


« len » et « id ».

struct trame {unsigned char len; unsigned char alignoctet ; unsigned short id;};

Étant donné qu’il vous est impossible d’agir au niveau de la trame reçue, la seule chose que
vous pouvez faire, c’est d’utiliser des octets :

struct trame {unsigned char len ; unsigned char id_lsb; unsigned char id_msb;};

Et ensuite déclarer une variable « unsigned short » que vous affecterez avec:

unsigned short id = (unsigned short)(id_msb<<8) | id_lsb ;


115
Comprenez par cet exemple que vous pouvez parfaitement être confronté à un
désalignement sans même chercher à optimiser.

Vous devez rester attentifs à ces deux pièges chaque fois que des octets séparés vont
représenter une partie d’un tout constituant une valeur plus grande :

- Si vous accédez à un groupe d’octets via un « cast », vous risquez un désalignement

- Si vous créez une structure contenant des types de plus de 8 bits à partir d’octets
séparés, vous risquez que votre compilateur réaligne votre structure qui, de ce fait, ne
correspondra plus à la position réelle des différents octets.

6.4. Le mot clé « register »

Certains spécialistes du C sur cibles bas-niveau font une utilisation intensive du mot-clé
« register ». Ce mot-clé précise au compilateur que vous désirez que, si possible, la variable
déclarée juste après ne soit pas un emplacement mémoire mais dans un registre. Utiliser un
registre plutôt qu’un emplacement RAM accélère le traitement, surtout dans les boucles.

Ce mot clé ne sert strictement à rien dans le cadre de l’ARM (et du reste la plupart du
temps). Ça fait partie du boulot du compilateur de veiller à utiliser des registres là où c’est
possible et intéressant. Et, avec tous les registres à disposition de notre compilateur, vu la
structure de l’ARM, il y arrive fort bien sans votre aide. Nous l’avons déjà du reste
précédemment vérifié.

Oubliez ce mot-clé, il n’a aucun intérêt !

6.5. Opérations supplémentaires sur les données

L’ARM travaille par défaut en little-endian. Or vous aurez souvent à manipuler des
données stockées dans la convention big-endian (trames Ethernet et autres données
structurées et normalisées).

Ceci risque de vous imposer au niveau du code des inversions d’octets, allant du simple
« swap » (commutation de deux octets poids fort/poids faible) à l’inversion de 4 octets
(travail sur des entiers), voire même l’inversion de tous les bits.

Il n’existe pas d’instruction C pour réaliser ce genre d’opérations. Pour un « swap » il est
encore simple d’utiliser une instruction contenant des opérations logiques et de décalage.
Mais pour les autres cas, ça devient tout de suite plus lourd.

Par contre, en langage d’assemblage ARM, ce genre d’opération est très simple à effectuer
et très rapide. C’est pourquoi le fichier « core_cm3.c » vous fournit une série de fonctions
codées en langage d’assemblage, à utiliser directement dans vos programmes.

Je prends par exemple le « swap » dont nous venons de parler :

116
__ASM uint32_t __REV16(uint16_t value)
{
rev16 r0, r0
bx lr
}

Le code en langage machine opère la permutation des deux octets constituant le


« uint16_t » (unsigned short) du registre R0 et place le résultat dans R0. La ligne « bx lr » est
l’équivalent d’un « return ». Vous utilisez cette fonction simplement comme ceci :

uint16_t varLittleEndian = __REV16(varBigEndian) ;

Sachant que par convention on passe les paramètres dans R0 (et R1,R2,R3 si nécessaire) et
qu’on retourne le résultat dans R0, ceci montre avec quelle facilité on peut intégrer du code en
langage d’assemblage dans du C, sans même se soucier de la pile.

Examinez ce fichier, il est bourré de courtes fonctions en langage d’assemblage qui vous
seront sans aucun doute d’une grande utilité.

6.6. Compilation avec zones réservées

L’ARM est capable d’utiliser des transferts DMA (Direct Memory Access), et donc
d’opérer des transferts de données sans passer par le cœur du processeur. Ainsi, par exemple,
le module EENET, responsable des opérations Ethernet, sait écrire une trame directement en
mémoire RAM sans monopoliser le processeur.

Il faut évidemment le configurer pour lui imposer les zones de travail, si vous voulez
comprendre comment ça fonctionne, lisez le datasheet ou examinez mes librairies.

Pour éviter de devoir demander dynamiquement de la mémoire, il est plus simple de se


« réserver » des zones mémoire que le compilateur n’utilisera pas pour y placer des variables.

Ceci s’effectue dans les paramètres du projet, à l’onglet « Target » :

117
Les zones IRAM1 et IRAM2 vous permettent de définir des zones que le compilateur peut
utiliser. Si la case de gauche est cochée, la zone est utilisée. Le terme IRAM signifie « Internal
Ram ».

Tout comme un Pic®, la zone mémoire interne de l’ARM est divisée en plusieurs zones :

- De 0x100000000 à 0x100007FFFF, soit 0x8000 bytes ou 32KiB de mémoire, vous avez


la mémoire locale SRAM – banque 0.

- De 0x2007C000 à 0x2007FFFF vous avez 16KiB de mémoire AHB-SRAM-banque 0

- De 0x20080000 à 0x2008C000 vous avez 16KiB de mémoire AHB-SRAM-banque 1

En général, si vous utilisez les accès DMA des modules, vous pouvez simplement laisser
les deux banques AHB libres, et donc ne pas utiliser les 0x8000 adresses à partir de l’adresse
0x2007C000. Vous pouvez alors permettre aux accès DMA d’écrire dans ces zones, elles ne
seront pas utilisées pour le reste du programme.

Mais vous pouvez évidemment définir les zones de travail que vous désirez, retenez que
seules celles qui ont la case de gauche cochée seront utilisées par le compilateur.

Notez au sujet de la mémoire S-RAM qu’il est possible d’y copier du code à partir du
programme principal puis de l’exécuter. Ceci peut être intéressant si vous devez exécuter des
routines nécessitant de très grandes vitesses, car la mémoire flash dans laquelle se trouve
118
votre code n’est pas suffisamment rapide pour suivre la vitesse maximale de votre ARM. Il y a
donc des nombres de cycles d’attente paramétrés au niveau des accès en mémoire flash.

Je ne vous explique pas le principe, c’est détaillé dans le datasheet, et lorsque vous en
serez à réaliser des optimisations aussi pointues, ça fera probablement longtemps que vous
n’utiliserez plus ce petit tutoriel.

Je termine ici, je compléterai le cas échéant en fonction des demandes. Vous avez toutes
les bases pour travailler correctement, il ne vous reste qu’à étudier les modules hardwares que
vous utiliserez.

Comprenez que vu les possibilités hardwares de l’ARM, ajoutées aux multiples


composants peuplant la carte LandTiger®, tout expliquer nécessiterait un nombre de pages à 4
chiffres, et j’ai aussi des projets personnels à réaliser.

Je vous souhaite des tas de belles réalisations, n’hésitez pas à les partager, et, si vous le
désirez, je me ferai un plaisir de les mettre en ligne sur mon site, accompagnées d’un
compteur de téléchargement.

119
Notes :

120
7. Informations légales

7.1. Licence et droits

Ce document est fourni tel quel, sans aucune garantie et sans recours d'aucune sorte.
L’auteur décline toute responsabilité en cas de dommages directs ou indirects découlant de la
lecture ou de l’utilisation de ce document et/ou des fichiers auxquels il fait référence.

Le présent document et les fichiers référencés (librairies et exemples) doivent être


proposés sous la forme d’un ensemble complet et indivisible.

Vous avez le droit d’utiliser et/ou de modifier ce document ainsi que les librairies
fournies, et d’utiliser ces dernières dans le cadre d’une utilisation privée, publique, ou
commerciale. Référez-vous aux conditions présentes dans les fichiers sources des librairies.

La redistribution de ce tutoriel est autorisée aux conditions suivantes :

- L’ensemble des documents doit être proposé dans un pack indivisible.

- Aucun commentaire original, incluant les références de l’auteur ne peut être supprimé
ou modifié.

- Toute modification doit être accompagnée de la date de modification, la raison de la


modification et l’auteur de cette modification, ceci sans altérer les commentaires
originaux.

- La page de téléchargement doit contenir le lien vers le site de l’auteur :


www.bigonoff.org,

- Il n’est pas autorisé de vendre ce document ni le pack qui le contient.

- En cas de traduction, seule la version française disponible sur le site de l’auteur fait
office de référence.

- La présente licence est soumise à modifications. Tout utilisateur est tenu de vérifier la
teneur de la dernière version directement sur le site de l’auteur.

- En cas de doute sur l’interprétation de la présente licence, la question devra être


explicitement posée à l’auteur du projet, il ne sera pas tenu compte d’une
interprétation personnelle.

- En cas de redistribution du pack non modifié, il est demandé d’utiliser sur votre site le
lien suivant : http://www.bigonoff.org/chargerpublic.php?fic=coursarm Ceci m’assure
que vous téléchargiez la dernière révision, et me permet de vérifier si le sujet vous
intéresse afin de continuer (ou non) l’aventure.

L’auteur s’est efforcé de réaliser cette application et les librairies attenantes en utilisant les
datasheets officiels des composants. Aussi, toute ressemblance avec une librairie ou
application existante ne serait que pure coïncidence.

121
7.2. Propriété intellectuelle

Le présent document est propriété intellectuelle de son auteur, identifié par son
pseudonyme, Bigonoff. L’auteur peut être contacté via son adresse email
bigocours@hotmail.com ou via l’adresse disponible sur son site : www.bigonoff.org .

7.3. Historique

Le 04/06/2015 : Révision 1 : Première version en ligne

122

Vous aimerez peut-être aussi