Dans cette partie nous allons aborder l’architecture sur lequel nous allons nous concentrer par la suite à savoir celle du STM32F10x. Bien que de facture relativement classique, les processeurs ARM sont des machines complexes proposant des modes de fonctionnement spécifiques qui sortent des objectifs de ce chapitre, aussi nous ne verrons que les grands principes. Entre autres choses que nous ne développerons pas dans ce qui suit, citons les coprocesseurs à virgule flottante, les modes de fonctionnement (Thread et Handler), la protection de la mémoire (MMU), les structures matérielles du debug…
Les éléments de l’architecture ARM
ARM ne fabrique aucun composant. Cette société dont le siège historique est situé à Cambridge (Royaume-Uni) s’est spécialisée au début des années 90 dans la conception d’architectures de cœur de microcontrôleurs 32 bits et maintenant 64 bits. Elle vend ensuite sous licence ses produits à des fondeurs qui fabriquent alors les microcontrôleurs en ajoutant de la mémoire, des unités périphériques diverses, le tout en quantité variable pour donner naissance à des familles de produits comme la famille STM32F10x
Les cœurs 32 bits d’ARM sont basés sur une architecture dite de type Harvard. Cela veut dire qu’elle sépare physiquement les accès à la “mémoire code” (le stockage du programme) des accès à la “mémoire données” (les variables du programme, accessibles en lecture/écriture).
ARM propose depuis 2014 la version 8 de son architecture, ce qui lui ouvre les portes aux processeurs 64 bits. Cependant pour la grosse majorité des processeurs 32 bits actuels (et surtout ceux concernés par ce cours) c’est la version V7 courante. Cette architecture repose sur 17 registres :
des registres généraux : R0 à R12,
un registre « pointeur de pile » : SP (possiblement appelé R13),
un registre dit « de lien » : LR (possiblement appelé R14),
un registre « pointeur de programme » : PC (possiblement appelé R15),
un registre de statut : xPSR (qui se décline sous 3 sous-noms APSR, EPSR ou IPSR selon le besoin qu’on en a).
Ces 17 registres se complètent par 4 registres spéciaux que nous ne détaillerons ici. Citons-les tout de même :
PRIM, FAULTMASK et BASEPRI qui sont des registres servant à la gestion des exceptions.
CONTROL dont le rôle principal consiste à gérer le niveau de privilège.
À l’aide de ces nouvelles informations, complétons le schéma présenté précédemment :
![Un processeur](https://user.oc-static.com/upload/2017/08/16/15028956340499_processeur.jpeg)
Basée sur un jeu d’instructions RISC (Reduced Instruction Set Computer) l’ALU exécute les différentes opérations pour lequel elle est conçue. Ce sont toujours des opérations basiques dont les principales sont :
l’arithmétique simple sur des entiers (addition/soustraction, multiplication/division),
des opérations logiques diverses (comparaison, ET, OU, etc.).
Outre l’exécution de l’instruction en elle-même l’ALU peut mettre à jour des fanions (flag) contenus dans le registre xPSR.
Les fanions sont au nombre de 5. Quatre d’entre eux se rencontrent sur tous les processeurs existants. Ils jouent un rôle essentiel pour la mise en place des structures algorithmiques (if then else, while, for…). Ces fanions sont :
l’indicateur C (Carry) : représente la retenue lors du calcul sur les grandeurs naturelles (non signées). Si C = 1, alors il y a eu un débordement de la représentation non signée lors de l'instruction précédente, ce qui signifie que le résultat est partiellement faux (un débordement arrive si le nombre de bits n’est pas suffisant pour représenter le résultat, par exemple en 8 bits la somme de 200 + 60 dépasse la valeur maximale 255 et provoque un débordement). La connaissance de ce bit permet, par exemple, de travailler en précision multiple ;
l’indicateur Z (Zero) : vaut 1 si le résultat de l’instruction donne zéro ;
l’indicateur N (Negative) : contient le bit de poids fort du résultat (rappel : le bit de poids fort est le bit ayant la plus grande valeur, celui de gauche pour une écriture de gauche à droite). Si la valeur est signée (c’est-à-dire qu’elle représente un nombre pouvant être négatif), N = 1 indique une valeur négative et N = 0 une valeur positive ;
l’indicateur V (oVerflow) : vaut 1 s'il y a eu débordement de la représentation signée et donc que le résultat signé est faux (par exemple, en 8 bits la somme de 140 + 40 en signé cause un débordement car les 8 bits ne permettent de représenter des valeurs que entre -128 et 127) ;
l’indicateur Q (saturation flag) : n'a de sens que pour les 2 instructions très particulières dites de saturation USAT et SSAT : le bit passe à 1 si ces instructions ont saturé le registre traité.
Ce que « voit » le processeur
L’objectif n’est pas d’apprendre à programmer en langage d’assemblage (« L.A. ») mais de pouvoir relire et comprendre un tel programme. En effet, lors de la mise au point d’un programme écrit en langage structuré (le langage C par exemple) vous pourrez être amenés à visualiser ce qu’il se passe exactement au niveau du processeur à travers la fenêtre de désassemblage.
Regardons cela à travers un exemple minimaliste qui incrémente une variable locale nommée Locale
:
![Capture d'écran du débogueur](https://user.oc-static.com/upload/2017/08/16/15028957036743_debogueur.jpeg)
Dans cette capture d’écran du débogueur, nous visualisons :
❶ : le listing source écrit en langage C ;
❷ : l’état des registres du processeur avant la réalisation de l’instruction repérée par la flèche jaune (dans la fenêtre ❸) ;
❸ : le code assembleur produit par le compilateur ;
❹ : le contenu de la mémoire à partir de l’adresse spécifiée dans le cadre ad-hoc.
L’addition de notre variable se traduit par un ADDS R1,R1,#1 (en fenêtre 3) ce qui semble raisonnable quand on sait que le mnémonique ADD signifie Addition. Dans cette même fenêtre on peut lire (première colonne) que cette instruction est stockée à l’adresse 0x08000370 et que son codage est en hexadécimal 1C49.
Revenons à ce code. Première information : la variable Locale
correspond au registre R1 et c’est normal. Le compilateur voit que c’est une variable interne (locale) à la procédure main, il n’a aucune raison d’utiliser un emplacement mémoire pour stocker cette information qui n’a aucune portée en dehors de cette procédure. L’utilisation d’un registre fonctionne tout aussi bien et même mieux puisque plus rapide.
Seconde information : À l’adresse code qui suit immédiatement cette addition (0x0x08000372), le désassembleur trouve du code (MOVS R0,#0) bien que notre programme source soit terminé. En effet la mémoire ne peut pas être vide et le désassembleur traduit en listing ce que contient la mémoire. Si nous n’y prenons pas garde, ces codes-instructions seront exécutés puisque rien n’indique au processeur qu’il a terminé son programme. Il y a donc une probabilité très grande que cela ne produira rien de bon puisque ce n’est pas du code que nous avons écrit.
Reprenons cet exemple en rajoutant une boucle infinie pour forcer le processeur à incrémenter infiniment la variable nommée Locale
et en déclarant maintenant la variable en global.
![Le débogueur après ajout d'une boucle infinie](https://user.oc-static.com/upload/2017/08/16/15028957490276_debogueur2.jpeg)
La boucle while se traduit par deux branchements (instruction B pour Branch). En fait un seul aurait suffi : celui de la fin. Mais cela fait partie des petites surprises que peut nous réserver un compilateur. Le programme débute la boucle (à l’adresse 0x0800038C) par un saut (instruction B) à l’adresse absolue 0x08000398. A cette adresse (6 lignes de programme plus bas ce qui correspondent à 12 octets d’occupation mémoire) le processeur est invité à se rendre à l’adresse 0x0800038E pour continuer sa séquence. A cette nouvelle adresse nous trouvons le corps de la structure while (l’incrémentation de la variable Locale
qui se réalise maintenant en 5 instructions). Le processeur va ensuite exécuter séquentiellement ces 5 instructions jusqu’à retrouver le second branchement ; ce qui le ramènera au début de la structure while… La boucle infinie est codée, plus de risque de voir le processeur exécuter des lignes de code hasardeuses.
Analysons maintenant le corps. En place centrale nous avons toujours l’instruction ADDS qui incrémente un registre. Ce n’est plus R1 mais R0. Il est important d’avoir à l’esprit que pour ce processeur, l’ALU ne sait travailler qu’avec des registres. Pour faire cette incrémentation il a donc besoin d’un registre qui va contenir temporairement une image du contenu de la variable stockée en mémoire.
Les lectures/écritures du contenu de cette variable sont un peu plus complexes à lire. Comme ARM conçoit des architectures de types Load/Store, les accès mémoire ne se font qu’à travers 2 instructions capables d’accéder à la mémoire LDR (pour Load Register) et STR (STore Register). Ces deux instructions se déclinent en différentes versions mais le principe est toujours le même : on donne un registre qui va recevoir (respectivement contenir) la donnée mémoire à lire (respectivement écrire) et on donne l’adresse mémoire où cet accès doit se faire. Cette adresse doit obligatoirement être pré-enregistrée dans un autre registre ; on parle alors d’adressage indirect ou accès par indirection (qui correspond à la notion de pointeur en langage structuré).
Dans notre exemple la lecture de Locale
correspond à LDR R0,[R0,#0] c’est-à-dire que l’ALU va aller lire à l’adresse contenue dans R0 décalée de 0 octets (donc l’adresse en elle-même; le décalage ici est inutile) une donnée 32 bits qu’il stockera dans R0 (en écrasant au passage l’ancien contenu qui était l’adresse). Symétriquement pour l’instruction d’écriture STR R0,[R1, #0] : l’adresse de destination est ici préalablement stockée dans R1 et le contenu de R0 va être transféré à cette adresse. Les deux autres lignes précédant ces 2 instructions (LDR R0,[PC, #12] et LDR R1,[PC #4]) permettent de pré-charger les registres « pointeurs » avec l’adresse de la variable Locale
. Celle-ci a été déposée par l’assembleur avec le code lors de la création de l’exécutable respectivement à 12 octets et à 4 octets de l’emplacement de l’instruction en elle-même. Cette technique est assez complexe à comprendre et nous y reviendrons un peu plus tard.
À la fin de ce chapitre, vous êtes capable :
de nommer les registres d'une architecture ARM v7 et d'identifier leur rôle,
de relire et de suivre le déroulement d'un code en assembleur.