INFO2 - TP n°5 Utilisation de l`outil de debug Keil avec la carte

Transcription

INFO2 - TP n°5 Utilisation de l`outil de debug Keil avec la carte
INFO2 - TP n°5
Utilisation de l'outil de debug Keil avec la carte mbed LPC1768
PARTIE 1 – Présentation et Installation de l'outil Keil
Pour faire le debug d'un code développé pour une plateforme embarqué comme le mbed LPC1768, il
y a deux solutions:
 faire des affichages sur une console
 ou bien utiliser un environnement de debug.
La première solution est simple à mettre en œuvre (il s'agit d'ajouter des printf dans le code source)
mais demeure peu pratique et peu efficace. Dans ce TP n°5 nous allons opter pour la seconde
solution en utilisant l'outil de debug Keil disponible pour les processeurs ARM. Cet outil s'utilise très
simplement depuis le port USB de la carte LPC1768. L'outil Keil permet de visualiser le code généré
par le compilateur (le code assembleur) ainsi que les différents registres du processeur ARM CortexM3 ou encore de mettre des points d'arrêts dans le code (breakpoints).
1) Faire un upgrade du bootloader (compatibilité avec l'interface CMSIS-DAP)
Avant de pouvoir lancer l'outil Keil avec la plateforme LPC1768 il faut s'assurer que la version du
bootloader est compatible. En effet, l'interface CMSIS-DAP utilisée pour reliée le LPC1768 à l'outil
Keil n’est supportée que pour la version 141212 du bootloader.
Remarque: Le bootloader est le programme lancé au démarrage de votre plateforme. Ce programme
est stocké dans la mémoire Flash du LP1768.
Pour effectuer un upgrade du bootloader (flasher votre mbed), suivre les instructions indiquées sur
ce lien:
http://developer.mbed.org/handbook/Firmware-LPC1768-LPC11U24
2) Installer les outils Keil
Les outils Keil (la MDK Version 5) sont déjà installés en salle 3A. Si jamais les outils ne sont pas
installés sur votre machine, voici le lien pour trouver les outils à installer :
http://www.keil.com/dd2/nxp/lpc1768/
Remarque : une inscription est nécessaire pour télécharger les outils Keil.
Important: c'est une version d’évaluation avec certaines restrictions comme par exemple la taille
programme + données qui est limitée à 32 kb (mais c'est largement suffisant pour ce que nous
aurons à faire dans ce TP).
INFO2 TP5
page 1
PARTIE 2 – Prise en main de l'outil Keil
3) Exporter un projet mbed sur la cible Keil µVision
Une fois l'outil Keil installé, nous allons exporter (pour la cible µVision) un projet sous
l’environnement en ligne de mbed (clic droit sur votre projet et faire "Export Program…". Cette étape
génère un .ZIP qu’il faut décompresser où vous voulez.
Créer un projet sous l'environnement de développement mbed (mbed.org) et écrire le programme
suivant qui permet de faire clignoter une LED.
// Chenillard sur les 4 leds de la carte mbed
#include "mbed.h"
DigitalOut myled[4]= {LED1, LED2, LED3, LED4};
Serial pc(USBTX, USBRX);
int main()
{
pc.printf("Chenillard...\n");
int leds = 0x1;
int i = 0;
while(1) {
myled[i] = 1;
// Led ON
wait(0.2);
// unit in second
myled[i++] = 0;
// Led ON
leds = (leds<<1);
if (leds == 0x10) {
leds = 1;
i=0;
}
}
}
4) Lancer l'outil Keil µVision
Lancer les outils Keil (µVision) et ouvrir le projet qui a été généré depuis mbed en ligne (Project->
Open Project). Le projet a une extension .uvproj (cliquer sur "Migrate to Device Pack" afin de générer
un projet compatible avec la version de Keil).
Il faut à présent vérifier la configuration de l'outil Keil pour la cible LPC1768.
Depuis l’IDE Keil, allez dans le menu
a.
Flash-> Configure Flash Tools…
b.
Sélectionner “CMSIS-DAP Debugger” dans « Use Target Driver for Flash Programming »
INFO2 TP5
page 2
c.
d.
Ensuite cliquer sur “Settings” puis “Add”
Il reste à choisir la cible, ici LPC17xx
e.
Avant de lancer le debugger il est nécessaire de compiler le projet, pour cela aller dans
"Project -> Build Target" ou plus simplement en cliquant sur la touche F7. Cette opération crée un
exécutable compatible pour le mode debug et dont l'extension du fichier est ".axf".
5) Il reste à lancer le debugger : Menu Debug -> Start/Stop Debug Session.
Si tout se passe bien vous pouvez visualiser le code désassemblé, la mémoire, les registres, mettre
des point d’arrêts, des watch de variables, etc… bref faire du debug. Mais nous verrons cela dans la
partie 3 de ce TP.
Pour lancer l’exécution du programme il suffit de faire F5 (ou Debug-> Run).
Vérifier que le programme s'exécute correctement (chenillard avec les leds de la carte mbed).
INFO2 TP5
page 3
Code désassemblé
Code C
Les registres
Vérifier que la connexion au Terminal pour les printf/scanf est bien fonctionnelle.
Tout ce que nous venons de voir (ou presque) est résumé sur cette page :
http://developer.mbed.org/handbook/CMSIS-DAP-MDK
INFO2 TP5
page 4
PARTIE 3 – Exécuter du code en mode Debug
6) Mettre des points d'arrêts dans le code et faire une exécution pas à pas
a) Les points d'arrêts (breakpoints)
Grâce à l'outil Keil, nous pouvons mettre un point d'arrêt (breakpoint) dans le code C. Un breakpoint
permet de stopper l'exécution d'un programme à un endroit précis
Remarque : il est également possible de stopper l'exécution lorsqu'un programme accède à une
donnée en mémoire en lecture ou écriture.
Pour mettre un breakpoint il suffit de cliquer à gauche dans la fenêtre affichant le code C à l'endroit
où l'on souhaite s'arrêter. Un petit rond rouge apparait alors indiquant que le breakpoint a bien été
posé (cf. Figure ci-dessous).
Le breakpoint a été posé
Lancer l'exécution du programme en appuyant successivement sur la touche F5 (i.e. Run). Que
constatez-vous? Expliquez.
b) L'exécution pas à pas (step by step)
La touche F5 permet de lancer l'exécution du programme. Dans ce cas, l'exécution ne s'arrête que si
le programme rencontre un point d'arrêt ou lorsqu'il se termine. Or, il est parfois bien utile de faire
une exécution du code pas à pas (step-by-step en anglais) afin de bien analyser et comprendre ce qui
se passe…
Les différentes possibilités de faire
une exécution pas à pas
c) Différence entre les différents modes d'exécution pas à pas
Expliquer la différence entre les différentes possibilités d'exécution pas à pas :
- Step
INFO2 TP5
page 5
-
Step over
Step out
Step to cursor line
Quels sont les touches raccourcis pour ces différentes possibilités d'exécution pas à pas?
Faites une exécution du code en utilisant les différentes possibilités d'exécution pas à pas.
7) Visualiser le contenu de la mémoire de données
Saisir le programme suivant depuis mbed.org. Une fois votre projet compilé, exportez le projet afin
de l'ouvrir dans l'environnement de debug Keil.
// Addition de 2 entiers
// --------------------#include "mbed.h"
Serial pc(USBTX,USBRX);
int iOp1, iOp2, iResult ;
int main()
{
pc.printf("Debut du programme...") ;
iOp1 = 0x1;
iOp2 = 0x4;
do {
iResult = iOp1 + iOp2 ;
pc.printf("Operand 1 = %d\n",iOp1) ;
pc.printf("Operand 2 = %d\n",iOp2) ;
pc.printf("Resultat = %d\n",iResult) ;
}
while (iResult != 0);
pc.printf("Fin du programme\n") ;
}
a) Déterminer les adresses en mémoire
Depuis l'outil de debug Keil et en vous servant de la fenêtre "Symbols" (Menu View->Symbols
Window), déterminer les adresses en mémoire où sont stockées les variables iOp1, iOp2 et iResult.
Variable
iOp1
iOp2
iResult
Adresse
En déduire l'espace utilisé pour stocker chacune des variables en mémoire.
b) Visualiser le contenu des données en mémoire
Depuis l'outil de debug Keil et en vous servant de la fenêtre "Memory" (Menu View -> Memory
Windows -> Memory 1), visualisez le contenu de ces 3 variables en effectuant une exécution pas à
pas du programme.
INFO2 TP5
page 6
c) Ajouter des watch sur des variables
Il est également possible de visualiser le contenu des variables en utilisant des "watch". Depuis l'outil
de debug Keil et en vous servant de la fenêtre "Watch" (Menu View-> Watch Windows-> Watch 1),
visualisez le contenu de ces 3 variables en effectuant une exécution pas à pas du programme.
d) Forcer la valeur des variables iOp1 et iOp2
Depuis les fenêtres "Watch 1" ou "Memory 1", modifier les valeurs des opérandes iOp1 et iOp2.
Faites une exécution pas à pas du programme afin de vérifier que les variables ont bien été modifiées
(vérifier l'affichage console également).
Fixer les valeurs suivantes pour iOp1 et iOp2:
- iOp1 = 0x7FFFFFFF
- iOp2 = 0x0000001
Faites une exécution pas à pas du programme et noter la valeur du résultat de l'addition (iResult).
Expliquer le problème. Proposer une solution à ce problème.
Quel Flag du registre d'état du processeur permet de détecter ce problème (cf. Annexe 2)?
8) Visualiser le contenu de la mémoire programme (cf. Annexe 1)
a) Déterminer l'adresse de votre fonction main.
Depuis l'outil de debug Keil et en vous servant de la fenêtre "Symbols" (Menu View -> Symbols
Window), déterminer l'adresse en mémoire programme de la fonction main (celle du type int f()).
Si ce n'est pas déjà le cas, ouvrez la fenêtre permettant de visualiser le code C et son équivalent
assembleur (Menu View -> Disassemby Window).
b) Analyse du code désassemblé
En vous aidant de l'Annexe 1, répondre à ces questions:
- qu'est-ce qu'un code assembleur?
- qu'est-ce qu'un code désassemblé?
- qu'est-ce qu'un fichier .lib?
- qu'est que la pré-compilation?
Dans le code C, mettre un point d'arrêt sur la première instruction du main (pc.printf).
Lancer l'exécution du programme et vérifier l'adresse à laquelle le programme s'est arrêté (regardez
le code désassemblé). Comme vous pouvez le constater le breakpoint est également visible au niveau
du code désassemblé.
9) Analyse du code assembleur (cf. Annexe 2)
Dans le code C, mettre un point d'arrêt sur la première instruction de la boucle do…while
(l'instruction d'addition).
a) Analyse du code assembleur pour l'addition des 2 entiers.
Visualisez et essayer de comprendre le code assembleur correspondant à l'instruction d'addition…
Combien d'instructions assembleurs sont nécessaires pour exécuter l'instruction d'addition écrite en
langage C? Quels sont les 3 types d'instructions utilisés?
b) Visualiser des registres du CPU ARM (cf. Annexe 2)
Si ce n'est pas déjà le cas, ouvrez la fenêtre permettant de visualiser les registres du processeur ARM
Cortex-M3 (Menu View -> Registers Window).
Quels registres du processeur ARM sont utilisés pour effectuer cette opération d'addition?
INFO2 TP5
page 7
Dans quel registre est rangé le résultat de l'addition (correspondant à la variable iResult)?
c) Analyse du code assembleur pour la structure itérative (do… while).
Dans le code C, mettre un point d'arrêt sur l'instruction while.
Visualisez et essayer de comprendre le code assembleur permettant de réaliser la boucle do…while.
Quel registre du processeur ARM est utilisé pour réaliser cette boucle? Quelles instructions
assembleur permettent de tester la valeur de iResult et de revenir au début de la boucle?
Quel registre du processeur ARM contient toujours l'adresse de la prochaine instruction assembleur
à exécuter? Visualiser la valeur de ce registre lorsque l'on revient au début de la boucle.
Juste avant le test de boucle, forcer la valeur de iResult à 0 afin de sortir de la boucle do…while.
Vérifier que cela marche et que le message "Fin du programme" s'affiche bien sur la console.
10) Appel de fonction
Ecrire une fonction permettant d'additionner 2 entiers passés en paramètre. Voici son prototype:
int Addition(int, int);
Il s'agit de remplacer dans le code C l'instruction
iResult = iOp1 + iOp2
par
iResult = Addition(iOp1, iOp2);
a) A quelle adresse de la mémoire programme se trouve la fonction Addition?
b) Quelle instruction permet de faire un branchement à cette adresse?
c) Dans quel registre du processeur est stockée l'adresse de retour de la fonction appelante?
d) Quels registres du processeur ARM sont utilisés pour le passage des 2 paramètres à la fonction
appelée?
e) Quel registre du processeur ARM est utilisé pour retourner le résultat à la fonction appelante?
f) Quelle instruction permet, à la fin de la fonction, de retourner dans la fonction appelante (i.e. la
fonction main) et continuer l'exécution du programme?
11) Si vous avez le temps, compléter votre programme en écrivant une routine d'interruption
permettant d'allumer une led lorsque l'on appuie sur un bouton poussoir.
Répondre aux mêmes questions que la question précédente (en remplaçant la fonction Addition par
la routine d'interruption).
INFO2 TP5
page 8
ANNEXE 1 – Chaîne de compilation
Un processeur exécute un code binaire, c'est-à-dire comprenant des 0 et des 1. Mais, il n'est bien sûr
pas envisageable de programmer en langage binaire…
Les constructeurs de processeurs (Intel, Motorola, ARM, etc.) fournissent un ensemble d'instructions
(ou jeu d'instructions, Instruction Set en anglais), appelées instructions assembleurs. Afin d'obtenir
un code binaire exécutable sur le processeur cible, le code assembleur doit être "assemblé" puis
"linké" (cf. la figure ci-dessous). Le développement de code assembleur est de plus en plus rare (mais
attention il est parfois toujours nécessaire) car il a un problème de portabilité. En effet, un code
assembleur développé pour un processeur Intel n'est pas compatible avec un processeur ARM par
exemple. Le code assembleur est donc trop proche de la machine. Un autre problème de
l'assembleur est qu'il est difficile à lire et donc à comprendre (surtout lorsque l'on ne connaît pas le
jeu d'instructions du processeur).
On a donc développé dès les années 50 des langages de plus haut niveau d’abstraction, comme par
exemple les langages Basic, Fortran, Cobol, C/C++ ou Pascal. Le code devient portable et distant du
matériel. Dans ces langages, le développeur écrit selon des règles strictes (syntaxe) mais dispose
d’instructions et de structures de données plus expressives qu’en assembleur. Ainsi des structures
algorithmiques proches du langage humain ont été définies : si-alors-sinon ; tant que-faire ; cas
selon; etc. Le code est par conséquent beaucoup plus lisible.
Il ne faut cependant pas oublier qu'un processeur ne comprend que du code binaire! Il est donc
nécessaire de traduire un langage de haut niveau (C/C++ par exemple) en langage assembleur puis en
langage binaire. C'est le rôle de la chaîne de compilation (généralement fournie avec le processeur
cible). La figure suivante propose une vue générale d'une chaîne de compilation.
Le compilateur traduit chaque instruction du langage source en une suite plus ou moins complexe
d’instructions en langage assembleur (appelée aussi langage machine). Cette opération de traduction
est complexe ; les compilateurs sont des programmes sophistiqués (et comportent parfois des
bugs!). Les programmes compilés s’exécutent plus rapidement que les programmes interprétés
(puisque la traduction est déjà faite).
INFO2 TP5
page 9
L'assembleur traduit un programme assembleur en un code binaire (les .o ou .obj) non relogé. Ceci
signifie simplement que ni les données ni le programme (les fonctions typiquement) n'a encore
d'adresses définies dans la mémoire du processeur cible (pas de mapping mémoire). Une
bibliothèque (le .lib) permet de regrouper un ensemble de fichier objets.
La dernière étape est l'édition de lien (Link en anglais). Elle consiste à définir le mapping, c'est-à-dire
à affecter des adresses aux différentes sections stockant les données (data) ou le programme (code).
A l'issue de cette étape un programme exécutable sur la machine cible est obtenue (généralement
un .exe). Il est également possible (en spécifiant l'option pour cela) de générer un fichier donnant des
informations sur le mapping mémoire (fichier .map).
INFO2 TP5
page 10
ANNEXE 2 – Le processeur ARM Cortex-M3
Généralités
Le processeur ARM Cortex-M3 est un processeur 32 bits conçu pour le
marché de l'embarqué. Introduit en 2004, il appartient à la famille des
processeurs Cortex-M et est basé sur une architecture ARMv7-M. C'est
un processeur de type RISC (Reduced Instruction Set Computer)
comportant donc un nombre limité d'instruction. Il comporte 3 étages
de pipeline (Fetch, Decode, Execute) et possède une architecture de type Harvard. La figure cidessous montre une vue simplifiée de l'architecture du ARM Cortex-M3.
Une documentation complète sur le processeur ARM Cortex-M3 est disponible à cette adresse (à
partir de la page 654): http://www.nxp.com/documents/user_manual/UM10360.pdf
Jeu d'instructions
Le jeu d'instruction est décrit à partir de la page 657 de ce document. Le processeur ARM Cortex-M3
possède seulement une centaine d'instruction assembleur. Toutes les instructions ont une longueur
de 32 bits et beaucoup d'entre elles s'exécutent en 1 seul cycle d'horloge. Il est en mesure
d'effectuer une multiplication de 2 nombres sur 32 bits en 1 seul cycle d'horloge et possède une
unité matérielle pour l'opération de division.
L'architecture est basée sur un modèle Load/Store (instruction LDR et STR), ce qui signifie qu'il n'est
pas possible d'effectuer des opérations arithmétiques ou logiques directement avec des opérandes
stockées en mémoire. Il faut en effet obligatoirement passer via des registres.
- Soit pour charger une donnée depuis la mémoire (opération Load)
- Soit pour stocker la valeur d'un registre dans la mémoire (opération Store)
Les registres
Pour fonctionner, le processeur ARM Cortex-M3 possède des registres internes de 32 bits (qui
peuvent s'utiliser comme 4 registres de 8 bits, ou 2 registres de 16 bits). Nous n'allons pas voir tous
les registres internes, mais les plus importants et ceux nécessaires pour ce TP5.
-
R0 à R12: les registres à usage général (general purpose registers). Ils sont utilisés pour
effectuer des opérations sur des données.
-
R13 à R15: les registres à usage spécifiques.
o R13 (SP) : utilisé comme pointeur de pile (Stack Pointer)
o R14 (LR) : registre de lien (Link Register), il stocke les informations de retour
(principalement l'adresse de retour) quand une fonction, une sous routine ou une
exception est appelée.
INFO2 TP5
page 11
o
-
R15 (PC) : le compteur de programme (Program Counter). Il contient toujours
l'adresse de la prochaine instruction assembleur à exécuter (i.e. l'instruction qui a été
fetchée).
PSR : le registre d'état du programme (Program Status Register) qui rend compte de l’état du
système après l’exécution d'une instruction. Chacun des bits du registre d’état est un
indicateur d’état ou Flag (drapeau). Avec l'outil de développement Keil nous pouvons
visualiser les bits 27 à 31 du PSR (nous ne nous intéresserons pas aux autres pour le
moment), respectivement:
o N (bit 31, Negative Flag) : égal à 1 si le résultat de la dernière opération est négatif ou
inférieur à, 0 sinon
o Z (bit 30, Zero Flag) : égal à 1 si le résultat de la dernière opération est nul, 0 sinon
o C (bit 29, Carry Flag) : égal à 1 si le résultat de la dernière addition a généré un bit de
retenue (carry), 0 sinon
o V (bit 28, Overflow Flag) : égal à 1 si le résultat de la dernière opération a provoqué
un débordement, 0 sinon
o Q (bit 27, Sticky Saturation Flag) : égal à 1 si le résultat d'une instruction SSAT ou
USAT a provoqué une saturation, 0 sinon (rarement utilisé).
La figure ci-dessous montre les registres qu'il est possible de visualiser depuis l'outil de
développement Keil pour le processeur ARM Cortex-M3 (View -> Registers Window).
Registres à usage
général
Registres à usage
spécifique
Registres d'état
INFO2 TP5
page 12