Architecture & systèmes · L1 · Section 5/6
Annales corrigées
Progression
#Annales corrigées
Les exercices qui suivent sont tirés des documents réellement distribués dans l'UE SPUE42 « Architecture des processeurs », deuxième année de Licence Électronique à l'Université Côte d'Azur, campus Valrose. Le cours porte sur les microcontrôleurs AVR, et plus précisément sur l'ATmega2560 de la carte Arduino Mega, programmé en assembleur avec avr-gcc.
Trois sources :
- les feuilles de travaux dirigés TD 1 à TD 6, qui couvrent les mémoires, l'évaluation d'expressions selon l'architecture, les branchements conditionnels, les modes d'adressage, les sous-programmes et les automates programmés ;
- les sujets de travaux pratiques TP 1 à TP 3 ;
- les exemples de programmes du cours, notamment l'algorithme du PGCD décliné sur les trois structures de processeurs (chapitre 5) et les calculs de temporisation.
Les énoncés sont reproduits fidèlement. Chaque correction est détaillée : méthode, déroulement, calcul du nombre de cycles, vérification.
#Exercice 1 — Caractéristiques d'une mémoire
TD 1, exercice 1.
Soit une mémoire ayant une capacité de 4K × 16.
- Combien de lignes de données et de lignes d'adresses sont nécessaires ?
- Donner sa capacité en octets.
Correction détaillée
Lecture de la notation. Une capacité s'écrit toujours sous la forme (nombre de mots × largeur d'un mot). « 4K × 16 » signifie donc 4096 mots de 16 bits. Le second nombre est la largeur du bus de données, le premier se déduit du bus d'adresses.
1. Lignes nécessaires.
- Lignes de données : elles portent un mot, donc leur nombre est la largeur du mot, soit 16 lignes .
- Lignes d'adresses : il faut pouvoir désigner chacune des 4096 cases. Or , donc il faut 12 lignes d'adresses .
La règle générale est lignes d'adresses pour mots, et lignes de données pour des mots de bits.
2. Capacité en octets. La capacité totale en bits est le produit des deux :
Ce qu'il faut retenir. La conversion bits → octets divise par 8, et elle ne dépend pas de la façon dont la mémoire est organisée : une mémoire 2K × 32 contient aussi 65 536 bits, soit 8 Ko, mais avec 11 lignes d'adresses et 32 lignes de données. Deux mémoires de même capacité en octets peuvent donc avoir des bus très différents — et c'est cette organisation, pas la capacité, qui détermine la façon de les câbler à un processeur.
#Exercice 2 — Mémoire multi-banc
TD 1, exercice 3.
Combien faut-il de circuits mémoire ayant chacun une capacité de 1024 mots de 8 bits pour obtenir une mémoire d'une capacité de 4096 mots de 16 bits ? Donnez les adresses mémoire de chaque circuit et indiquez la capacité en Ko des circuits et de la mémoire réalisée.
Correction détaillée
Nombre de circuits. On compare les deux dimensions séparément :
- en profondeur : il faut fois plus de mots ;
- en largeur : il faut fois plus de bits par mot.
Les deux extensions sont indépendantes et se multiplient :
Chaque circuit apporte 8 des 16 bits d'un mot, et quatre groupes de deux circuits se partagent la plage d'adresses.
Capacités.
- Un circuit : bits octets .
- La mémoire réalisée : bits . On retrouve bien Ko.
Adresses. La mémoire complète compte 4096 mots, donc 12 lignes d'adresses . Chaque circuit ne contient que 1024 mots, donc 10 lignes d'adresses : ce sont les lignes basses , communes à tous les circuits. Les deux lignes hautes sélectionnent le banc :
| Banc | Plage d'adresses | Bits fournis | |
|---|---|---|---|
| Banc 0 | 00 | 000 – 3FF | circuits 0 (bits 0–7) et 1 (bits 8–15) |
| Banc 1 | 01 | 400 – 7FF | circuits 2 et 3 |
| Banc 2 | 10 | 800 – BFF | circuits 4 et 5 |
| Banc 3 | 11 | C00 – FFF | circuits 6 et 7 |
Au sein d'un banc, les deux circuits reçoivent les mêmes adresses et sont activés par le même signal de sélection : l'un pilote , l'autre . C'est l'extension en largeur, réalisée en parallèle.
Le point de méthode. Deux extensions, deux câblages différents. Pour augmenter la profondeur, on ajoute des circuits et on décode des adresses hautes pour n'en activer qu'un à la fois : les bus de données sont partagés et mis en haute impédance quand le circuit n'est pas sélectionné. Pour augmenter la largeur, on ajoute des circuits activés simultanément, chacun fournissant une tranche de bits. Confondre les deux mène à un câblage qui « a l'air » de fonctionner mais ne lit jamais les bons bits.
#Exercice 3 — Décodage d'adresses
TD 1, exercices 4 et 5.
Exercice 4. Pour accéder à sa mémoire, un microprocesseur émet des adresses sur seize bits et un signal MEM (accès mémoire, actif à 0). La taille du mot mémoire est de 32 bits. Cette mémoire se compose :
- d'un boîtier de RAM d'une capacité de 16k × 32 (de
0000à3FFF) ; - d'un boîtier de ROM d'une capacité de 16k × 32 (de
4000à7FFF) ; - de deux boîtiers d'EPROM d'une capacité de 16k × 16 (de
C000àFFFF).
Réalisez le circuit de décodage du système.
Exercice 5. Concevez le système de décodage d'adresses pour un espace mémoire réparti comme suit, pour un processeur à données 8 bits :
0000–7FFF: ROM (quatre boîtiers de 8K) ;8000–9FFF: RAM (en un seul banc) ;FFF8–FFFB: boîtier d'entrée/sortie ;FFFC–FFFD: boîtier d'entrée/sortie ;FFFE–FFFF: boîtier d'entrée/sortie.
Correction détaillée
Exercice 4.
Étape 1 — combien de bits pour chaque boîtier ? Un boîtier de 16k contient mots. Il lui faut donc 14 lignes d'adresses, , communes à tous les boîtiers. Les deux lignes hautes font le décodage.
Étape 2 — lire les plages en binaire.
| Plage | Boîtier | Largeur | |
|---|---|---|---|
0000 – 3FFF | 00 | RAM | 32 bits |
4000 – 7FFF | 01 | ROM | 32 bits |
8000 – BFFF | 10 | non utilisé | — |
C000 – FFFF | 11 | deux EPROM | 16 bits chacune |
Étape 3 — le décodage. Un décodeur 2 vers 4 commandé par produit les quatre sélections, dont trois sont utilisées. Chaque sortie est combinée avec le signal MEM (actif à 0) par une porte OU : la sélection n'est active que si MEM et la sortie du décodeur sont actifs simultanément. En logique active-bas, cela s'écrit CS = MEM + SEL_i — un OU, pas un ET, ce qui est l'erreur classique.
Étape 4 — les deux EPROM. Chacune est un boîtier de 16k × 16, et il en faut deux pour couvrir les 32 bits du mot : elles partagent les mêmes adresses et sont activées ensemble par la même sélection, l'une fournissant et l'autre . C'est une extension en largeur, comme dans l'exercice 2. Leurs et leur sont donc câblés en parallèle.
Étape 5 — le bus de données. Le bus est de 32 bits. La RAM et la ROM le pilotent en entier ; les EPROM n'en pilotent que la moitié, mais comme elles vont par deux, l'ensemble est cohérent. Le point délicat est la haute impédance : tout boîtier non sélectionné doit relâcher le bus, sans quoi deux boîtiers le piloteraient en même temps et le résultat serait un conflit électrique, pas une valeur logique.
Exercice 5.
Étape 1 — dimensionner. Le processeur a 8 bits de données, donc un mot par adresse. Chaque boîtier de 8K contient mots et demande 13 lignes d'adresses .
Étape 2 — la ROM, 0000–7FFF. Cette plage fait 32K octets, soit quatre boîtiers de 8K. Il faut donc deux lignes supplémentaires pour choisir le boîtier : sélectionnent l'un des quatre, et valide la plage.
| Plage | Boîtier ROM | |
|---|---|---|
0000 – 1FFF | 000 | ROM 0 |
2000 – 3FFF | 001 | ROM 1 |
4000 – 5FFF | 010 | ROM 2 |
6000 – 7FFF | 011 | ROM 3 |
Étape 3 — la RAM, 8000–9FFF. Cette plage fait 8K octets, soit un seul boîtier de 8K : et et suffisent à l'identifier. C'est le cas le plus simple : un seul boîtier, pas de sélection interne à décoder.
Étape 4 — les entrées/sorties, FFF8–FFFF. Ces huit adresses occupent le haut de l'espace. En binaire, FFF8 = 1111 1111 1111 1000 et FFFF = 1111 1111 1111 1111 : les treize bits de poids fort valent tous 1, et seuls varient. La détection est donc un ET de toutes les lignes hautes :
C'est exactement le motif d'un décodeur de type « une seule plage en haut de l'espace », et c'est ainsi que les microcontrôleurs réservent leurs registres de périphériques : le plan mémoire de l'ATmega2560 place les registres étendus d'entrée/sortie de $0100 à $01FF, et les registres internes de $0020 à $005F.
Étape 5 — le trou. La plage A000–FFF7 n'est affectée à rien. Il faut décider explicitement de son comportement : soit elle n'est pas décodée, soit elle est décodée vers un boîtier absent, soit elle est volontairement laissée en défaut pour déclencher une exception en cas d'accès erroné. Une plage non décodée qui n'est pas documentée est un bogue en attente : un pointeur perdu y écrit et personne ne s'en aperçoit.
#Exercice 4 — Une ROM comme additionneur-soustracteur
TD 1, exercice 2.
Quelle taille doit avoir une ROM pour réaliser un additionneur/soustracteur binaire quatre bits ?
Correction détaillée
Méthode. Une ROM n'est rien d'autre qu'une table de vérité câblée : chaque combinaison d'entrées est une adresse, chaque sortie est un bit du mot. Pour dimensionner la ROM, il faut donc compter les entrées et les sorties.
Entrées. L'opération porte sur deux opérandes de 4 bits, et , soit 8 bits. À cela s'ajoute le choix de l'opération — addition ou soustraction — qui est une entrée supplémentaire de 1 bit. Total : bits d'entrée.
La ROM doit donc contenir
Sorties. Le résultat est un mot de 4 bits, , auquel s'ajoute l'indicateur de retenue , soit bits.
Taille de la ROM : 512 mots × 5 bits.
Vérification par le contenu. Vérifions que 512 mots suffisent bien à contenir toute la fonction. Pour l'addition, le résultat maximal est , qui s'écrit sur 5 bits (11110) — donc 4 bits de somme plus 1 bit de retenue suffisent, et aucune combinaison n'est perdue. Pour la soustraction, on peut soit produire le complément à deux sur 4 bits avec un indicateur de signe, soit réserver le bit supplémentaire à la retenue empruntée. Dans les deux cas, 5 bits de sortie couvrent exactement l'ensemble des résultats possibles.
Le commentaire à faire. Cette ROM coûte 2560 bits de stockage pour une fonction qu'un additionneur à retenues anticipées réalise avec quelques centaines de portes. La ROM gagne en temps d'accès constant — quel que soit le couple d'opérandes, le résultat sort en un cycle — et en simplicité de conception : on remplit une table, on ne simplifie rien. Elle perd en surface et en consommation, et surtout elle n'est pas modifiable sans reprogrammer la mémoire. C'est exactement le compromis que tranche un FPGA avec ses tables de correspondance (LUT), qui sont des ROM de quelques bits.
#Exercice 5 — Évaluer une expression arithmétique sur une machine à pile
TD 2.
On considère l'expression , codée sur des entiers signés 16 bits en complément à deux. La machine à pile dispose des trois opérateurs mult, add, sub et de deux instructions de manipulation de la pile, push et pop. Chaque opérateur arithmétique op est évalué comme (pop op pop) push.
- Dessinez l'arbre d'évaluation.
- Écrivez un programme qui évalue .
- Dessinez le contenu de la pile lors de l'exécution.
Correction détaillée
1. Arbre d'évaluation. La racine est la soustraction, dont le sous-arbre gauche est et le sous-arbre droit est :
1 sub2 / \3 mult mult4 / \ / \5 b b 4 mult6 / \7 a cLa structure de l'arbre impose l'ordre des opérations : on évalue d'abord les deux multiplications, puis la soustraction.
La convention de pile, à fixer d'abord. C'est le point délicat de l'exercice, et il n'est pas symétrique. La convention retenue — celle du cours, vérifiée sur l'implémentation du PGCD au chapitre 5 — est la suivante : la pile se lit | gauche droite, l'opérateur dépile d'abord l'opérande droite (le sommet), puis l'opérande gauche (l'élément en dessous), calcule gauche op droite, et empile le résultat. La preuve en est donnée par l'implémentation du PGCD : sur une pile | A B, l'instruction CMP teste — donc , plus profond, est bien l'opérande gauche.
2. Programme. En parcourant l'arbre en profondeur :
1push b ; | b2push b ; | b b3mult ; | b²4push 4 ; | b² 45push a ; | b² 4 a6push c ; | b² 4 a c7mult ; | b² 4 ac8mult ; | b² 4ac9sub ; | b² − 4ac10pop delta ; | (vide), delta contient b² − 4ac3. Contenu de la pile, étape par étape.
| Instruction | Pile après exécution | Commentaire |
|---|---|---|
push b | | b | |
push b | | b b | |
mult | | b² | dépile b (droite) et b (gauche), empile le produit |
push 4 | | b² 4 | |
push a | | b² 4 a | |
push c | | b² 4 a c | |
mult | | b² 4 ac | |
mult | | b² 4ac | l'opérande gauche est 4, la droite ac |
sub | | b²−4ac | gauche = , droite = |
pop delta | | | la pile est vidée, le résultat est rangé |
Vérification sur un exemple. Prenons , , : le discriminant vaut , donc deux racines réelles distinctes. Prenons , , : , donc aucune racine réelle. Le programme doit produire , ce qui exige que la soustraction soit faite dans le bon sens : inversée, elle donnerait , et l'erreur ne serait détectée que sur les cas où le discriminant est négatif — un bogue redoutable, car il ne se manifeste que sur la moitié des données.
Le commentaire à faire. L'arbre d'évaluation et la pile sont isomorphes : chaque feuille de l'arbre devient un push, chaque nœud interne un opérateur. C'est ce qui rend les machines à pile si élégantes pour compiler des expressions — mais l'ordre des opérandes devient une convention, et une convention non écrite est une source de bogues. Une machine à registres généraux, elle, nomme explicitement ses opérandes (SUB Rd, Rr) : l'ordre est dans l'instruction, pas dans la discipline du programmeur.
#Exercice 6 — Branchements conditionnels et boucles d'attente
TD 3.
- Écrire le programme qui, si est supérieur à 40, fait recevoir à , sinon met 25 dans .
- Si est supérieur à , faire .
- Si est supérieur à et que est égal à , faire .
- Donner l'organigramme et le programme d'une boucle qui fait « tant que ».
- Donner l'organigramme et le programme d'une boucle « faire … tant que ».
- Expliquez ce que fait ce programme :
1 ldi R23, 2552boucle: subi R23, 13 nop4 brne boucleCorrection détaillée
1. Alternative simple. Le schéma est toujours le même : on teste la négation de la condition et on saute par-dessus le bloc « alors ».
1 cpi r0, 41 ; comparer r0 à 412 brlo sinon ; si r0 < 41, c'est-à-dire r0 <= 40 : aller à sinon3 mov r1, r2 ; bloc « alors »4 sub r1, r3 ; r1 = r2 − r35 rjmp finsi6sinon: ldi r15, 257finsi:Trois points de méthode. D'abord, la comparaison est non signée avec brlo ; pour des valeurs signées il faudrait brlt. Choisir la mauvaise famille est l'erreur la plus fréquente, et elle ne se voit que sur les valeurs négatives. Ensuite, brlo branche si , ce qui est bien la négation de « » — attention au sens strict de la comparaison : comparer à 40 au lieu de 41 laisserait passer le cas dans la mauvaise branche. Enfin, plutôt que d'inverser mentalement chaque condition, on écrit la négation telle qu'elle se lit : « si n'est pas strictement supérieur à 40, alors… ».
2. Condition simple. .
1 cpi r16, 0x5C ; comparer r16 à 922 brlo suite ; si r16 < 92, c'est-à-dire r16 <= 91 : rien à faire3 subi r19, -20 ; r19 = r19 + 204suite:Même précaution : « supérieur à » exclut la valeur 91 elle-même, d'où la comparaison à 92. L'astuce subi r19, -20 mérite d'être notée : le jeu d'instruction AVR ne propose pas d'instruction addi (addition immédiate), seulement subi. Soustraire revient à ajouter 20 — c'est le moyen standard d'écrire une addition immédiate sur AVR. Le piège est que subi travaille sur des entiers non signés de 8 bits : pour des valeurs signées, il faut passer par adiw sur une paire de registres ou charger la constante dans un registre.
3. Condition composée (ET logique). La structure change : avec un ET, chaque condition fausse doit sauter hors du bloc. Pour « », on compare dans l'ordre inverse afin d'obtenir directement la négation :
1 cp r17, r162 brsh pasfais ; si r17 >= r16, c.-à-d. r16 <= r17 : sauter3 cp r19, r204 brne pasfais ; si r19 != r20 : sauter5 ldi r21, 0 ; les deux conditions sont vraies6pasfais:Pour un OU, la structure est différente : chaque condition vraie saute dans le bloc. Retenir ces deux formes — ET = « sauter si faux », OU = « sauter si vrai » — évite de se tromper en écrivant une condition composée, où l'inversion de la logique est la source d'erreur classique.
4. Boucle « tant que ». Le test est en tête : si la condition est fausse au départ, le corps n'est jamais exécuté.
1tantque: cp r1, r02 breq fintantque3 ; ... corps de la boucle ...4 rjmp tantque5fintantque:5. Boucle « faire … tant que ». Le test est en pied : le corps est exécuté au moins une fois. Là encore on compare dans l'ordre inverse pour brancher directement sur la condition :
1faire: ; ... corps de la boucle ...2 cp r3, r23 brlo faire ; si r3 < r2, c.-à-d. r2 > r3 : recommencerLa différence entre les deux formes n'est pas cosmétique : elle change le nombre d'exécutions du corps quand la condition est fausse dès le départ. Une boucle « tant que » s'exécute zéro fois, une boucle « faire … tant que » s'exécute une fois.
6. La boucle d'attente. Le programme fait décroître R23 de 255 jusqu'à 0 en ne faisant rien d'autre qu'un nop entre chaque décrément, puis sort quand R23 atteint 0.
Calcul du nombre de cycles. Avec les temps du jeu d'instruction : subi = 1 cycle, nop = 1 cycle, brne = 2 cycles si le branchement est pris et 1 sinon.
- Une itération complète prise : cycles.
- La dernière itération : le
brnen'est pas pris, donc cycles. R23passe de 255 à 0, soit 255 itérations.
À 16 MHz, cela représente — une temporisation très courte, obtenue sans aucune instruction spécialisée. C'est la brique de base de toutes les temporisations du TP 1 : pour clignoter une LED à un rythme visible, il faut empiler ces boucles.
Pourquoi 255 et pas 256 ? ldi R23, 255 charge la valeur maximale d'un octet non signé. La boucle s'exécute donc 255 fois, pas 256 : subi décrémente et positionne le drapeau Z quand le résultat est nul. C'est une boucle « jusqu'à zéro », la forme la plus efficace sur AVR, car elle n'a pas besoin d'une comparaison supplémentaire.
#Exercice 7 — Modes d'adressage
TD 4, exercice 1.
Soit a une donnée codée sur 8 bits rangée à l'adresse mémoire 1000 dans la mémoire de données. Écrivez en pseudo-code et en assembleur la liste des instructions nécessaires pour ranger en mémoire la valeur à la suite de a, en utilisant l'adressage :
- direct ;
- indirect ;
- indirect avec déplacement constant ;
- indirect avec post-incrémentation.
Correction détaillée
Ce que change le mode d'adressage. Le calcul est toujours le même — lire a, ajouter 15, écrire le résultat — mais la façon de désigner l'adresse varie. C'est précisément l'objet de l'exercice : montrer que le même algorithme s'écrit différemment selon l'architecture sous-jacente, et que le mode d'adressage disponible détermine la longueur et la lisibilité du code.
1. Adressage direct. L'adresse de l'opérande est écrite en clair dans l'instruction. Sur AVR, la mémoire de données s'adresse par les instructions LDS/STS.
1lds r16, 1000 ; r16 <- mem[1000] (lire a)2subi r16, -15 ; r16 <- r16 + 15 (pas de ADDI sur AVR)3sts 1001, r16 ; mem[1001] <- r16 (écrire b)Trois instructions, et l'adresse est figée : si l'on veut répéter l'opération sur dix valeurs consécutives, il faut dix blocs d'instructions distincts. C'est le mode le plus simple et le moins souple.
2. Adressage indirect. L'adresse est dans un registre pointeur (X, Y ou Z, soit les paires r27:r26, r29:r28, r31:r30).
1ldi r26, lo8(1000) ; charger l'adresse basse dans XL2ldi r27, hi8(1000) ; charger l'adresse haute dans XH3ld r16, X ; r16 <- mem[X] (lire a)4subi r16, -155st X, r16 ; mem[X] <- r16Ici le calcul de l'adresse est séparé de l'instruction d'accès : le pointeur peut être modifié à l'exécution, ce qui rend le mode indirect indispensable pour parcourir un tableau.
3. Indirect avec déplacement constant. AVR propose LDD/STD avec un déplacement constant de 0 à 63 appliqué au pointeur Y ou Z :
1ldi r28, lo8(1000)2ldi r29, hi8(1000)3ldd r16, Y+0 ; lire mem[Y + 0] = a4subi r16, -155std Y+1, r16 ; écrire mem[Y + 1] = bLe déplacement est dans l'instruction, donc aucune instruction n'est dépensée pour calculer l'adresse de b : c'est le mode le plus économique quand l'écart entre les deux cases est connu à la compilation. Sa limite est la plage du déplacement — 63 octets — et le fait qu'il ne s'applique qu'à Y et Z.
4. Indirect avec post-incrémentation. L'instruction incrémente le pointeur après l'accès, ce qui enchaîne naturellement deux cases consécutives :
1ldi r26, lo8(1000)2ldi r27, hi8(1000)3ld r16, X+ ; lire mem[1000], puis X <- 10014subi r16, -155st X+, r16 ; écrire mem[1001], puis X <- 1002Après ces instructions, X pointe sur 1002 : le pointeur est prêt pour la valeur suivante. C'est le mode de tous les parcours de tableau — une seule instruction d'accès, aucune instruction d'incrémentation, donc le code le plus court et le plus rapide.
Synthèse.
| Mode | Instructions | Souplesse | Usage typique |
|---|---|---|---|
| Direct | 3 | nulle (adresse figée) | variable isolée, registre de périphérique |
| Indirect | 4 | totale (pointeur calculable) | parcours avec pas variable |
| Indirect + déplacement | 4 | partielle (écart constant) | accès à deux champs voisins d'une structure |
| Indirect + post-incrément | 4 | totale, la plus efficace | parcours séquentiel de tableau |
Ce que l'exercice démontre. Les quatre programmes font exactement la même chose. Le choix n'est donc pas une question de correction mais de coût : deux instructions de chargement du pointeur sont payées une fois, et amorties sur tout le parcours. Sur une seule valeur, l'adressage direct gagne ; sur mille, l'adressage indirect avec post-incrémentation gagne largement. C'est la conclusion générale du TD 2 : l'implémentation d'un algorithme dépend fortement de l'architecture qui effectue les manipulations de données.
#Exercice 8 — Produit scalaire et adressage d'une matrice
TD 4, exercice 2.
- Deux tableaux de 4 valeurs 8 bits sont rangés à partir des adresses 1024 et 1028 en mémoire de données. Proposez un programme assembleur capable de faire le produit scalaire de ces deux vecteurs et de ranger le résultat à l'adresse 1032.
- Une matrice de 16 lignes et 8 colonnes de caractères commence à l'adresse 2048 de la mémoire de données. Écrivez le code assembleur qui écrit le caractère
A(code ASCII 65) aux coordonnées , sachant que et sont rangés aux adresses 0 et 1.
Correction détaillée
1. Produit scalaire.
Le calcul. Le produit scalaire de deux vecteurs de 4 composantes est . Les deux tableaux sont contigus : occupe les adresses 1024 à 1027, les adresses 1028 à 1031. La distance entre et est constante, égale à 4 — donc l'adressage indirect avec déplacement est le plus adapté.
1 ldi r26, lo8(1024) ; X pointe sur A[0]2 ldi r27, hi8(1024)3 ldi r28, lo8(1028) ; Y pointe sur B[0]4 ldi r29, hi8(1028)5 clr r18 ; accumulateur, octet bas6 clr r19 ; accumulateur, octet milieu7 clr r20 ; accumulateur, octet haut8 clr r22 ; registre à zéro, pour propager la seule retenue9 ldi r21, 4 ; compteur de boucle10 11boucle: ld r16, X+ ; r16 <- A[k], X pointe sur A[k+1]12 ld r17, Y+ ; r17 <- B[k], Y pointe sur B[k+1]13 mul r16, r17 ; R1:R0 <- A[k] * B[k]14 add r18, r0 ; accumuler l'octet basLes trois points techniques. D'abord, mul sur AVR place un produit 8 × 8 = 16 bits dans la paire R1:R0 : il faut donc accumuler sur plusieurs octets, avec add pour l'octet bas et adc pour les suivants — c'est la propagation de la retenue qui est ici essentielle, et l'oublier est l'erreur classique.
Ensuite, le troisième octet demande une attention particulière. Le produit n'occupe que deux octets : la contribution à l'octet haut se réduit donc à la seule retenue sortie de l'octet milieu. Écrire adc r20, r1 serait une faute — cela rajouterait r1, déjà comptabilisé dans r19. On introduit pour cela un registre à zéro (r22), et l'instruction adc r20, r22 ne propage que la retenue. Ce détail est invisible sur de petites valeurs, où la retenue ne sort jamais de l'octet milieu.
Enfin, les deux pointeurs X et Y avancent en parallèle grâce au post-incrément, sans aucune instruction supplémentaire.
Le piège de dimensionnement. Vérifions que le résultat tient sur trois octets. Le maximum du produit scalaire est atteint quand les huit octets des deux vecteurs valent 255 :
Or : le résultat ne tient pas sur 16 bits, et il faut au moins 18 bits, donc trois octets à partir de l'adresse 1032. Écrire le résultat sur deux octets « parce que mul produit 16 bits » est une erreur d'un facteur 4 sur la plage couverte — et elle ne se voit que sur les grandes valeurs, donc rarement pendant les tests. Si l'on veut se limiter à deux octets, il faut le démontrer : par exemple borner les entrées à , ce qui donne , tout juste dans l'intervalle. Ce raisonnement préalable — quelle est la valeur maximale du résultat ? — doit précéder tout choix de format.
2. Adressage d'une matrice.
La formule d'adressage. La matrice est rangée ligne par ligne (row-major) à partir de l'adresse 2048. La ligne occupe donc 8 octets, et l'élément se trouve à
Vérification aux bornes : donne 2048, le premier élément ; donne , le dernier — et , ce qui confirme que la matrice occupe bien 128 octets contigus.
Le programme. L'astuce est de calculer l'adresse par décalages : multiplier par 8 revient à décaler de trois rangs vers la gauche.
1 lds r16, 0 ; r16 <- i2 lds r17, 1 ; r17 <- j3 clr r18 ; r18 = octet haut de l'adresse4 lsl r16 ; i * 25 rol r18 ; (propager dans l'octet haut)6 lsl r16 ; i * 47 rol r188 lsl r16 ; i * 89 rol r1810 add r16, r17 ; + j (aucune retenue possible, voir ci-dessous)11 subi r16, low(-2048) ; ajouter 2048 à l'adresse12 sbci r18, high(-2048)13 movw r30, r16 ; Z <- adresse calculée14 ldi r17, 65 ; caractère 'A'Ce qu'il faut remarquer. La multiplication par 8 se fait par trois décalages, chacun suivi d'une rotation dans l'octet haut : c'est la façon standard de multiplier par une puissance de 2 sur un processeur 8 bits, et elle ne coûte que 6 cycles. Le rol r18 est le point qu'on oublie : sans lui, une adresse dépassant 255 serait tronquée — le bogue ne se manifeste que sur les grandes valeurs, donc rarement pendant les tests.
L'addition de se fait par un simple add, sans propagation de retenue. Ce n'est pas une négligence : c'est une conséquence de la formule. Comme , le produit vaut au plus 120, et comme , la somme vaut au plus 127 — elle ne peut donc jamais dépasser 255, et aucun report vers l'octet haut n'est possible. Ajouter un adc inutile serait non seulement superflu mais dangereux : il consommerait la retenue précédente et propagerait une valeur parasite. Savoir qu'une retenue ne peut pas se produire vaut mieux que la propager par précaution.
L'ajout de 2048 utilise l'idiome AVR standard : subi/sbci avec la constante négée, l'assembleur calculant les octets bas et haut. Enfin, movw (copie de mot, 1 cycle) est plus efficace que deux mov pour charger le pointeur Z.
La formule générale à retenir. Pour une matrice de lignes et colonnes rangée ligne par ligne à partir de l'adresse de base , l'élément est à . Le produit est le seul calcul non trivial, et il se fait par décalages quand est une puissance de 2 — ce qui explique pourquoi les dimensions des matrices en traitement du signal sont presque toujours des puissances de 2.
#Exercice 9 — Sous-programmes et sauvegarde des registres
TD 5, exercice 1.
Soit le programme suivant :
1main: ldi r16, 152 ldi r17, 103 call addition4 mov r20, r185 ...6addition: add r16, r177 mov r18, r168 ret- Expliquez très précisément ce que fait ce programme et ce que fait chacune des instructions.
- Expliquez pourquoi cette structure de programme ne peut pas être complètement correcte.
- Proposez une solution.
Correction détaillée
1. Déroulement.
| Instruction | Effet |
|---|---|
ldi r16, 15 | charge la valeur 15 dans le registre r16 |
ldi r17, 10 | charge la valeur 10 dans r17 |
call addition | empile l'adresse de retour, puis saute à l'étiquette addition |
add r16, r17 | r16 ← 15 + 10 = 25 |
mov r18, r16 | r18 ← 25 |
ret | dépile l'adresse de retour et reprend l'exécution dans main |
mov r20, r18 | r20 ← 25 |
Le programme calcule donc la somme de 15 et 10, et range le résultat 25 dans r20. L'usage de r18 comme registre de retour est une convention implicite : rien dans le jeu d'instruction ne l'impose.
2. Pourquoi ce n'est pas correct. Le sous-programme détruit r16 sans le sauvegarder. L'appelant avait placé 15 dans r16 et le retrouve à 25 après l'appel : la valeur d'origine est perdue. Rien ne le signale — l'assembleur ne dit rien, et le programme « marche » tant que l'appelant n'a plus besoin de r16.
Trois défauts, du plus visible au plus insidieux :
- L'appelant ne peut pas savoir ce qui sera détruit. Le sous-programme modifie
r16etr18; s'il en modifiait vingt, la liste ne serait écrite nulle part. C'est un contrat implicite, et un contrat implicite n'est pas un contrat. - Les paramètres passent par des registres fixes.
additionlitr16etr17: ce n'est pas une fonction réutilisable, c'est un fragment de code qui suppose qu'on a bien voulu lui préparer ces registres. Impossible de l'appeler deux fois de suite avec des valeurs différentes sans recharger les registres — et c'est précisément ce que fait la boucleboucle3de la temporisation. - La conséquence est intermittente. Le bogue ne se manifeste que si l'appelant réutilise
r16après l'appel. C'est la signature des bogues d'assemblage : ils apparaissent quand on ajoute du code, pas quand on l'écrit.
3. Solution. Deux disciplines, qui se combinent.
Sauvegarder dans le sous-programme — c'est au sous-programme de rendre l'état qu'il a trouvé :
1addition: push r16 ; sauvegarder ce qu'on va abîmer2 add r16, r173 mov r18, r164 pop r16 ; restaurer AVANT le ret5 retOu documenter et respecter une convention d'appel, comme le fait avr-gcc : les paramètres passent dans r24, r22, etc., le résultat revient dans r24, et les registres r18 à r27 (dits call-saved) doivent être préservés par le sous-programme. Ce n'est plus de la sauvegarde systématique, c'est un partage des responsabilités : l'appelant sauvegarde les registres volatils qu'il utilise encore, l'appelé sauvegarde les autres. Cette convention est la version matérielle de ce que le langage C appelle une ABI.
L'ordre compte, et c'est un piège. La pile est LIFO : si l'on empile r16 puis r17, il faut dépiler r17 puis r16. Inverser l'ordre ne provoque aucune erreur de compilation — le programme restaure simplement les mauvaises valeurs dans les mauvais registres, et le bogue se manifeste plus tard, ailleurs. Pour sauvegarder plusieurs registres, l'ordre de restauration est toujours l'inverse de l'ordre de sauvegarde.
Le lien avec les processus. Ce que fait un appel de sous-programme à petite échelle — empiler une adresse de retour, sauvegarder quelques registres, restaurer à la sortie — le noyau le fait à grande échelle lors d'un changement de contexte : il empile tous les registres, le compteur programme et le registre d'état, puis restaure ceux d'un autre fil d'exécution. La pile est le mécanisme, et c'est pourquoi sa taille est une ressource critique.
#Exercice 10 — Temporisations : du cycle au sous-programme
TD 5, exercice 2.
On considère la temporisation suivante :
1tempo1: ldi r16, 2552boucle2: ldi r17, 2553boucle1: subi r17, 14 brne boucle15 subi r16, 16 brne boucle2- Dessinez l'organigramme de cette fonction et donnez l'équivalent en nombre de cycles de chacune des instructions.
- Calculez le nombre total de cycles de cette temporisation, puis la durée correspondante.
- Quelles instructions ajouter pour en faire un sous-programme complet ? Recalculez le nombre de cycles.
- À partir de cette temporisation, on veut une deuxième temporisation d'une seconde grâce à :
1tempo2: ldi r16, X2boucle3: call tempo13 subi r16, 14 brne boucle35 retQue fait ce programme ? Combien de fois call tempo1 est-il exécuté ? Calculez pour obtenir une temporisation d'une seconde.
Correction détaillée
1. Coût des instructions. Avec le jeu d'instruction du ATmega2560 : ldi = 1 cycle, subi = 1 cycle, brne = 2 cycles si le branchement est pris, 1 sinon.
2. Boucle interne. r17 part de 255 et décroît jusqu'à 0, soit 255 itérations. Chacune coûte subi + brne pris = 3 cycles, sauf la dernière où brne n'est pas pris :
Boucle externe. Chaque itération comprend ldi r17, 255 (1), la boucle interne (764) et subi r16, 1 (1), soit 766 cycles, plus le brne final (2 ou 1). r16 part de 255 et décroît jusqu'à 0, soit 255 itérations :
En ajoutant le ldi r16, 255 initial, le corps de tempo1 coûte 195 840 cycles.
Durée. Le Makefile distribué en TP fixe F_CPU = 16000000UL, soit 16 MHz pour la carte Arduino Mega : c'est cette valeur qui sert à convertir les cycles en temps. Une période d'horloge vaut donc ns, et la temporisation dure
3. En faire un sous-programme. Il faut ajouter ret à la fin, et — pour respecter la discipline de l'exercice précédent — sauvegarder les registres utilisés si l'appelant peut en avoir besoin :
1tempo1: push r162 push r173 ldi r16, 2554boucle2: ldi r17, 2555boucle1: subi r17, 16 brne boucle17 subi r16, 18 brne boucle29 pop r1710 pop r1611 retCoût : le corps (195 840) plus ret (5) plus les quatre instructions de sauvegarde/restauration (push et pop = 2 cycles chacun) :
4. La temporisation d'une seconde.
Que fait le programme ? tempo2 charge X dans r16, puis appelle tempo1 X fois : chaque itération appelle la temporisation de 12 ms, décrémente r16 et reboucle jusqu'à zéro. call tempo1 est donc exécuté exactement X fois.
Coût d'une itération de boucle3 : call (5 cycles) + tempo1 complet (195 853, ret inclus) + subi (1) + brne pris (2) = 195 861 cycles.
Valeur de X. On veut s, soit cycles :
La dernière itération n'a pas de brne pris, ce qui retire 1 cycle : le total vaut .
| Cycles | Durée | Verdict | |
|---|---|---|---|
| 81 | 15 864 740 | 0,9915 s | trop court de 8,5 ms |
| 82 | 16 059 945 | 1,0037 s | retenu |
On retient , seule valeur entière donnant une durée au moins égale à la seconde visée. La précision obtenue est de — largement suffisante pour un clignotement de LED, insuffisante pour une horloge. Et c'est précisément la limite de cette méthode : la temporisation par boucle est bloquante (le processeur ne fait rien d'autre) et imprécise (elle dépend de la fréquence d'horloge, des interruptions et du compilateur). Pour une mesure du temps fiable, on utilise un timer matériel avec interruption, ce qui libère le processeur et se cale sur l'horloge du quartz.
Temporisations de 10 et 100 secondes. Il suffit d'empiler un niveau supplémentaire : une troisième boucle appelant tempo2 dix fois donne 10 s, cent fois donne 100 s. La structure est récursive, et chaque niveau multiplie la durée par son compteur. Attention toutefois à la profondeur de pile : chaque call empile 2 octets (ou 3 sur ATmega2560, dont le compteur programme fait 3 octets), et chaque sous-programme empile ses registres sauvegardés. Sur un microcontrôleur, la pile vit dans la SRAM — 8 Ko sur l'ATmega2560 — et un débordement de pile corrompt silencieusement les données.
#Exercice 11 — Automates programmés : le chenillard et le moteur pas-à-pas
TD 6, exercices 1 et 2.
Chenillard. Soit l'automate :
| État | Sorties |
|---|---|
| 0 | 1001 |
| 1 | 0101 |
| 2 | 1010 |
| 3 | 0100 |
En admettant que l'on choisisse une structure de type Moore, avec chaque transition équivalente à une période d'horloge, dessinez le graphe d'état puis programmez l'automate.
Moteur pas-à-pas. Un moteur comporte 4 bobines B3 à B0 qui doivent être alimentées suivant le cycle :
| B3 | B2 | B1 | B0 |
|---|---|---|---|
| 0 | 0 | 1 | 1 |
| 0 | 1 | 1 | 1 |
| 0 | 1 | 1 | 0 |
| 1 | 1 | 1 | 0 |
| 1 | 1 | 0 | 0 |
| 1 | 1 | 0 | 1 |
| 1 | 0 | 0 | 1 |
| 1 | 0 | 1 | 1 |
Dessinez le graphe d'état de cet automate puis programmez-le.
Correction détaillée
Chenillard — graphe d'état. L'automate de Moore a quatre états formant un cycle : A → B → C → D → A, chacun portant le motif de LED indiqué. Chaque transition consomme une période d'horloge.
1 1001 0101 1010 01002 ( A ) ---> ( B ) ---> ( C ) ---> ( D )3 ^ |4 +--------------------------------------+Programme. La structure est celle d'une machine de Moore sans entrée : un état interne, une table de sorties, et une transition inconditionnelle à chaque tour.
1 ; r20 = état courant (0..3)2 clr r203boucle:4 ; --- table de sorties : motif selon l'état ---5 ldi r30, lo8(motifs)6 ldi r31, hi8(motifs)7 add r30, r20 ; Z <- motifs + état8 clr r1 ; r1 = 0, indispensable pour l'adc9 adc r31, r110 lpm r16, Z ; r16 <- motif[état]11 out PORTB, r16 ; l'afficher sur le port12 13 ; --- transition : état suivant ---14 inc r20Trois points de méthode. Le modulo 4 se fait par un ET avec 0b11 : plus court et plus rapide qu'une comparaison, et c'est la raison pour laquelle les automates cycliques se codent sur un nombre d'états qui est une puissance de 2. La table de motifs est rangée en mémoire de programme et lue par lpm (load program memory) : c'est ainsi qu'on évite une cascade de comparaisons. Et clr r1 avant adc r31, r1 n'est pas décoratif : l'addition de l'indice à l'adresse de la table peut produire une retenue qu'il faut reporter sur l'octet haut, et adc ajoute la retenue plus son second opérande. Il faut donc un registre garanti à zéro — c'est le rôle de r1, qui est le registre à zéro de la convention AVR-GCC. L'oublier fait propager une valeur parasite dans l'octet haut de l'adresse, et le bogue ne se manifeste que lorsque la retenue se produit réellement.
Moteur pas-à-pas — graphe d'état. L'automate a huit états, un par ligne du tableau. Une propriété remarquable apparaît immédiatement : chaque transition ne change qu'un seul bit. Vérifions-le sur les trois premières :
0011→0111: B2 passe de 0 à 1 ;0111→0110: B0 passe de 1 à 0 ;0110→1110: B3 passe de 0 à 1.
Et la propriété tient aussi pour le retour cyclique 1011 → 0011 (B3 passe de 1 à 0). La séquence est donc un cycle de Gray sur 4 bits, ce qui n'est pas un hasard : une seule bobine commute à la fois, donc un seul transistor de puissance change d'état à chaque pas. Cela limite les pics de courant d'alimentation et les perturbations électromagnétiques — exactement la raison pour laquelle le code de Gray est utilisé dans les capteurs de position. C'est le lien direct entre l'électronique numérique de première année et la commande de puissance.
Programme. Même structure que le chenillard, avec huit motifs :
1 clr r202boucle:3 ldi r30, lo8(phases)4 ldi r31, hi8(phases)5 add r30, r206 clr r17 adc r31, r18 lpm r16, Z9 out PORTB, r1610 11 ; --- temporisation : la vitesse du moteur dépend d'elle ---12 call tempo113 14 inc r20Le lien vitesse / temporisation. « A chaque commutation des bobines, le rotor tourne d'un angle constant. La vitesse du moteur est donc directement liée à la fréquence de commutation. » Avec huit états par tour électrique et une temporisation de 12,2 ms entre deux commutations, la période d'un tour complet vaut ms, soit environ 10 tours par seconde. Diviser la temporisation par deux double la vitesse — et c'est exactement ainsi qu'on pilote un moteur pas-à-pas : par la fréquence, jamais par l'amplitude.
Le point commun des deux exercices. Les deux automates se programment avec la même structure : un compteur d'état, une table de motifs, une transition. C'est la traduction logicielle de la machine de Moore du cours : l'état interne détermine la sortie, et une fonction de transition détermine l'état suivant. Changer d'automate ne change que la table et la loi de transition.
#Exercice 12 — Automates de Mealy : déterminisme et complétude
TD 6, exercices 3 et 4.
On considère un automate de Mealy dont les transitions sont étiquetées par des conditions sur les entrées et et par les sorties correspondantes. Certaines transitions portent l'étiquette « ? », et plusieurs transitions sortantes d'un même état ont des conditions qui se recouvrent, par exemple a.b et a.b.c.
- Quels problèmes de conception présente cet automate ?
- Comment les résoudre avant de programmer ?
Correction détaillée
1. Les deux problèmes. Un automate fini déterministe complet doit satisfaire deux conditions sur chaque état :
- complétude : la somme des conditions de toutes les transitions sortantes doit être égale à 1 — autrement dit, quel que soit le mot d'entrée, l'automate a toujours une transition à prendre ;
- déterminisme : le produit deux à deux des conditions sortantes doit être égal à 0 — autrement dit, jamais deux transitions ne sont simultanément valides.
Or l'automate de l'énoncé viole les deux.
Défaut de complétude. Les transitions étiquetées « ? » ne précisent aucune condition : on ne sait pas ce qui doit se passer. En pratique, cela signifie qu'aucune transition n'est définie pour certaines combinaisons d'entrées. Le programme résultant n'a alors aucun cas à exécuter : il ne fait rien, ou pire, il tombe dans une transition choisie par défaut. C'est exactement le problème de l'état inutilisé d'une machine séquentielle : une situation non prévue où le système se perd.
Défaut de déterminisme. Sur un même état, a.b et a.b.c peuvent être vraies simultanément — il suffit que , et . Deux transitions sortantes sont alors valides, et l'automate doit en choisir une. Cette situation n'est pas « imprévisible » au sens matériel : dans un programme, l'ordre des tests décide. Mais c'est un choix implicite, qui dépend de l'ordre dans lequel le programmeur a écrit ses comparaisons. Modifier cet ordre change le comportement du système sans que rien ne le signale.
2. Comment les résoudre. Deux méthodes, à choisir explicitement et à documenter.
Rendre les conditions exclusives. On réécrit les transitions de façon que leurs conditions soient deux à deux incompatibles. Ici, la transition a.b.c est incluse dans a.b : il suffit de restreindre la première à ce qui la distingue, a.b.\bar{c}, et de laisser a.b.c couverte par la transition générale a.b. Les deux conditions deviennent alors exclusives.
Définir une priorité. On décide explicitement qu'une transition est prioritaire, on la teste en premier, et on documente le choix. C'est la solution adoptée dans les corrigés de l'UE pour ce type de conflit : « il faut rendre une des transitions prioritaire, sinon on ne pourra jamais implémenter le système ». La condition de priorité s'écrit alors dans le code par l'ordre des tests, et le commentaire qui l'accompagne devient une pièce du cahier des charges.
Compléter les « ? ». On remplace chaque « ? » par une condition explicite, et on ajoute une transition « autre » qui couvre tous les cas restants :
1 ; conditions de l'état courant, testées dans l'ordre de priorité2 sbic PIND, 0 ; test du bit a3 rjmp cas_a4 sbic PIND, 1 ; test du bit b5 rjmp cas_b6 rjmp cas_defaut ; « autre » : aucune condition satisfaiteLa transition « autre » est ce qui rend l'automate complet : elle garantit qu'il y a toujours une action à exécuter, même si les entrées prennent une valeur non prévue.
3. Ce que cela enseigne. Un automate mal spécifié n'est pas un automate difficile : c'est un automate non programmable. Le passage du graphe d'état au programme oblige à lever toutes les ambiguïtés, et l'exercice du TD est fait pour le montrer. La règle est la même pour une machine séquentielle matérielle : un état inutilisé doit toujours avoir une transition définie vers un état de reprise, sans quoi la machine peut s'y perdre définitivement. Et la leçon vaut au-delà de l'automatique : un comportement non spécifié n'est pas un comportement laissé libre, c'est un comportement laissé au hasard — dans le meilleur des cas l'ordre des tests, dans le pire la valeur résiduelle d'un registre non initialisé.
#Exercice 13 — Programmation du PGCD sur les trois architectures
Cours, chapitre 5, application 1.
Algorithme de convergence par soustraction, pour deux entiers strictement positifs et :
1tant que A != B2 si A > B alors A = A − B3 sinon B = B − A4fin tant que5pgcd = AImplémentez cet algorithme sur les trois structures de processeurs vues en cours : machine à pile, machine à accumulateur, machine à registres généraux. Comparez.
Correction détaillée
Ce que l'algorithme demande. Trois opérations : une comparaison d'égalité, une comparaison d'ordre, et une soustraction — plus deux branchements conditionnels. C'est un cas d'école, car il ne demande aucune multiplication ni division : toute la difficulté est dans les branchements et la conservation des deux valeurs.
Invariant et terminaison. Avant d'écrire le code, il faut le justifier : et restent des entiers strictement positifs (l'invariant de boucle), et décroît strictement à chaque itération. Une suite d'entiers positifs strictement décroissante est finie : la boucle termine. Ce raisonnement est exigible, et il est indépendant de l'architecture.
1. Machine à pile. Avec la convention de pile | gauche droite et op = « dépiler droite, dépiler gauche, calculer, empiler » :
1 push A2 push B3Bcl: CMP ; teste A − B, pile inchangée4 BEQZ Done ; si Z = 1, A = B : terminé5 BLTZ Else ; si A < B, aller à Else6 SWAP ; | B A7 OVER ; | B A B8 SUB ; | B A−B9 BT Bcl10Else: OVER ; | A B A11 SUB ; | A B−A12 BT Bcl13Done: DROP ; | A14 POP PGCDLes primitives de manipulation de pile (swap, over, drop, roll, dup) font tout le travail : elles réorganisent les opérandes sans jamais toucher à la mémoire. Le conseil du cours est explicite — « moins il y aura de références mémoires, mieux cela vaudra », et « trouver l'ordre des variables qui fait ressortir l'invariant de boucle » — car c'est cet ordre qui détermine combien de manipulations sont nécessaires.
2. Machine à accumulateur. Un seul registre de calcul : il faut donc conserver les deux variables en mémoire et recharger à chaque fois.
1 LOAD A2 STORE sauvA ; mem[sauvA] = A3 LOAD B4 STORE sauvB ; mem[sauvB] = B5Bcl: SUB sauvA ; Accu = B − A6 BEQ Done ; si Z = 1, A = B7 BLT Then ; si le résultat est négatif, A > B : aller à Then8 STORE sauvB ; sinon A < B : sauvB = B − A9 B Bcl10Then: NEG ; Accu = A − B11 STORE sauvA ; sauvA = A − B12 LOAD sauvB ; recharger B13 B Bcl14Done: LOAD sauvALe coût est frappant : chaque itération touche la mémoire plusieurs fois, alors que la machine à pile ne touchait que la pile. L'astuce intéressante est l'usage du drapeau de signe : SUB sauvA calcule , et si le résultat est négatif c'est que — la comparaison d'ordre est donc gratuite, obtenue comme sous-produit de la soustraction.
3. Machine à registres généraux. C'est le cas le plus confortable : les deux valeurs tiennent dans des registres, et l'ALU accepte deux registres quelconques.
1 LDI r16, A2 LDI r17, B3Bcl: CP r16, r174 BREQ Done ; A = B : terminé5 BRLO sinon ; A < B6 SUB r16, r17 ; A ← A − B7 RJMP Bcl8sinon: SUB r17, r16 ; B ← B − A9 RJMP Bcl10Done: ; pgcd dans r16Aucune référence mémoire dans la boucle : tout se passe dans les registres, et CP fournit les drapeaux dont BREQ et BRLO se servent directement. C'est la version la plus courte et la plus rapide des trois.
Comparaison. Le même algorithme, trois programmes qui n'ont presque rien en commun :
| Architecture | Boucle typique | Forces | Faiblesses |
|---|---|---|---|
| Pile | swap, over, sub | aucune référence mémoire, expression de l'arbre directe | beaucoup d'instructions de manipulation, difficile à lire |
| Accumulateur | sub, store, load | un seul registre à gérer, comparaison gratuite via NEG | aller-retours mémoire permanents |
| Registres généraux | cp, breq, sub | la plus courte et la plus rapide | nécessite plusieurs registres et une allocation |
La conclusion à retenir. L'implémentation d'un algorithme donné dépend fortement du type d'architecture qui effectue les manipulations de données. Ce n'est pas une remarque théorique : c'est la raison pour laquelle un compilateur doit connaître précisément la cible, et pourquoi le même code C peut produire des binaires de performances très différentes selon la machine visée. Les machines à pile ont longtemps prospéré dans les calculatrices, mais n'ont quasiment jamais été réellement accélérées ; ce sont les architectures à registres généraux qui ont emporté le marché.
Vérification. Sur et : , , , , donc — ce qui est bien . Le programme du TP 2 propose de compter les allumages de LED : faire clignoter la LED un nombre de fois égal au PGCD calculé permet de vérifier le résultat sans afficheur, ce qui est la seule solution disponible quand on n'a qu'une sortie binaire à disposition.
#Exercice 14 — Maximum d'un tableau en assembleur
TP 2, exercice 3.
Écrire un programme qui calcule la valeur maximale des éléments d'un tableau de 10 valeurs entières 8 bits et allume la LED si cette valeur maximale est supérieure à 50, l'éteint sinon. Le tableau est déclaré ainsi :
1 .section .text2Tableau:3 .byte 1, 2, 3, 4, 5, 6, 7, 8, 9, 104 .align 25 .global main6main: ...Correction détaillée
Structure de l'algorithme. Un parcours classique de recherche de maximum : initialiser le maximum au premier élément, puis comparer chaque élément suivant et retenir le plus grand.
1max <- T[0]2pour k de 1 à 9 :3 si T[k] > max alors max <- T[k]Le point de départ délicat. Initialiser le maximum à 0 serait un bogue latent : sur un tableau de valeurs négatives — ou de valeurs non signées inférieures à celles qu'on aurait choisies — le résultat serait faux. La bonne pratique est d'initialiser avec le premier élément du tableau, qui existe toujours.
Programme. Les données sont en mémoire de programme (déclarées par .byte dans la section .text), donc lues par LPM avec le pointeur Z.
1 ldi r30, lo8(Tableau)2 ldi r31, hi8(Tableau)3 lpm r18, Z+ ; r18 <- T[0], maximum initial ; Z pointe sur T[1]4 ldi r20, 9 ; 9 éléments restants5 6boucle: lpm r16, Z+ ; r16 <- T[k], Z avance7 cp r16, r188 brlo pasmax ; si T[k] < max, on garde l'ancien9 mov r18, r16 ; nouveau maximum10pasmax: dec r2011 brne boucle12 13 ; --- test final : max > 50 ? ---14 cpi r18, 50Trois points techniques.
lpm r16, Z+ lit en mémoire de programme avec post-incrémentation : le pointeur avance tout seul, aucune instruction supplémentaire. C'est l'équivalent exact, pour la mémoire de programme, de ld r16, X+ pour la mémoire de données — et cela montre que le choix du mode d'adressage est le même problème quel que soit l'espace mémoire.
*brlo est un branchement non signé : T[k] < max au sens des entiers naturels. Pour des valeurs signées, il faudrait brlt. Comme les données du TP sont des entiers 8 bits non signés (1 à 10), brlo est correct — mais le commenter évite une erreur silencieuse si le tableau change.
sbi et cbi mettent un bit à 1 ou à 0 dans un registre d'entrée/sortie. DDRB doit avoir été configuré en sortie au préalable, sinon PORTB ne pilote rien : c'est le premier programme du TP 1, et l'erreur classique du débutant est d'oublier la configuration de direction.
Vérification. Le tableau déclaré est , donc le maximum vaut 10. Comme , la LED doit rester éteinte. Pour tester l'autre branche, il suffit de modifier la déclaration, par exemple .byte 1, 2, 3, 4, 5, 6, 7, 8, 9, 100 : le maximum vaut 100 et la LED doit s'allumer. C'est exactement la consigne du TP — « testez-le avec l'allumage de la LED puis avec l'extinction de la LED » — et c'est la bonne façon de tester un programme sans afficheur : choisir les données pour parcourir chaque branche, plutôt que d'espérer que le cas se présente.