Le YASEP n'a pas d'instructions de lecture ou d'écriture en mémoire. Contrairement à l'écrasante majorité des processeurs existants (de la catégorie "Load-Store architecture"), il accède à la mémoire au travers des registres, plus ou moins comme le faisait le CDC6600 (sauf que les registres du YASEP ne sont pas dédiés à l'écriture ou la lecture). Cela économise des opcodes, augmente le nombre d'accès mémoire par intruction, et garde la structure des instructions très orthogonale.
L'architecture du YASEP est conçue autour de 16 registres répartis en 4 types. Voici la liste des noms, avec leur numéro :
C'est un peu plus compliqué qu'une architecture RISC classique mais cela réduit le nombre d'opcodes en leur permettant de remplir de nombreuses fonctions différentes. Il existe aussi deux bits de condition (Carry/retenue et Zero) mais ils sont accessibles seulement au travers des instructions conditionnelles. Le mécanisme de sauvegarde et la restauration de l'état en cas d'exception n'est pas encore déterminé.
R1, R2, R3, R4 et R5 sont des registres normaux. Comme dans les autres architectures, ils sont lus et écrits sans déclencher d'action implicite.
; Exemple 1 MOV 1234h R2 ; écrit la valeur 1234h dans le registre R2 ; Exemple 2 ADD 4 R1 ; R1 <- R1 + 4 ; Exemple 3 ADD 32 R1 R3 ; R3 <- R1 + 32
On les utilise pour contenir les résultats intermédiaires des calculs, les compteurs de boucles, les paramètres d'appel aux fonctions... Les formes d'instructions étendues permettent aussi d'incrémenter ou de décrémenter ces registres. (en développement)
Ce registre est plus spécial : PC est le pointeur vers l'instruction en cours. Les lettres "PC" signifient "Program Counter". Il est automatiquement incrémenté à chaque nouvelle instruction, et peut être lu et sur-écrit par toutes les instructions.
; Exemple : ADD 1234h PC A1 ; charge l'adresse PC+1234h dans A1
; Exemple 1 MOV 1234h PC ; Saute à l'addresse 1234h ; Exemple 2 ADD 4 PC ; Saute à PC+4 ; Exemple 3 ADD R1 32 PC ; Saute à R1+32
On peut remarquer que les instructions de YASEP sont encodées sur 2 ou 4 octets et que toutes les adresses ont une granularité d'un octet. De plus, les instructions sont toujours à des adresses paires, donc le bit de poids faible des adresses d'instructions est toujours implicitement à 0.
Cependant, il ne faut pas se fier à cette particularité car cela pourra changer dans le futur. En attendant, au mieux, l'écriture d'un 1 dans le bit de poids faible peut être ignorée, au pire cela peut bloquer le processeur, le réinitialiser ou déclencher une interruption, selon l'implémentation. Donc gardez ce bit à 0 !
Les registres A1, A2, A3, A4 et A5 contiennent l'adresse où une donnée sera lue ou écrite en mémoire. Ils peuvent être mis à jour (incrémentés ou décrémentés) par les instructions de forme étendue, afin d'accéder facilement à des données contigües.
D1, D2, D3, D4 et D5 sont des registres de données, associés aux registres d'adresse : A1 est lié à D1, A2 à D2 etc. Le YASEP maintient toujours la cohérence entre les registres d'adresse et de donnée d'une paire, afin de préserver la relation Dx=memoire[Ax] :
Si on écrit et lit le même registre de donnée (qui est alors utilisé comme opérande et destination), alors cela met à jour la mémoire. C'est une manière de faire des opérations "RMW" (read-modify-write) avec un cœur RISC orthogonal.
; Exemple 1 MOV 1234h A1 ; pointe A1 vers l'addresse 1234h ==> D1 contient la valeur à cette adresse. ADD D1 R3 ; ajoute le contenu de la mémoire à l'adresse 1234 au registre R3. ; Exemple 2 MOV 1234h A3 ; pointe A3 vers l'adresse 1234h ==> D3 contient la valeur à cette adresse. ADD D3 R2 ; ajoute le contenu de la mémoire à l'adresse 1234 au registre R2. ; Exemple 3 ADD 1234h R1 A4 ; additionne R1+1234h et met le résultat dans A4 ==> D4 contient la valeur à cette adresse. ADD R2 D4 ; lit le mot situé à l'adresse [R1+1234h], ; ajoute la valeur de R2 et réécrit le résultat à la même adresse.
En conjonction avec les fonctions d'incrémentation et décrémentation, ces registres peuvent réaliser des piles. Par convention, A5 est le pointeur de pile et D5 est le sommet de la pile, mais rien n'empêche de créer 2 ou 3 piles, ou bien de réaliser la pile standard avec d'autres registres
Lorsque deux registres d'Adresses (ou plus) pointent vers le même mot en mémoire, il ne faut pas s'attendre à ce que les valeurs des registres de Données restent cohérents après une écriture. Par exemple, dans le cas où A1=A2, alors une écriture dans D1 ne va pas automatiquement mettre à jour la valeur de D2.
Pour les premières réalisations, il n'est pas raisonnable de comparer 5 registres d'Adresses entre eux et d'écrire conditionnellement 5 registres de données. La longueur du pipeline ainsi que le nombre de portes logiques augmenteraient beaucoup trop ! Dans les cas les plus simples, on peut considérer que le banc de registre sert de tampon à la mémoire, ce qui permet de conserver l'état même lors de changements de contextes.
Cependant il est possible et même probable que des réalisations plus sophistiquée résolvent le problème, au moyen de structures différentes.
Dans tous les cas, lorsqu'un alias ou chevauchement de pointeurs est attendu ou même simplement possible, il ne faut pas hésiter à utiliser une section critique (voir l'opcode CRIT) et le plus sûr est de n'utiliser qu'une seule paire de registres Adresse/Donnée pour accéder à ces mots en mémoire. Comme l'a montré l'exemple précédent, les opérations de "read-modify-write" fonctionnement mieux et sont plus courtes lorsqu'une seule paire est utilisée.
Attention : Le YASEP est une architecture "little Endian" où les pointeurs peuvent adresser des octets mais TOUS les accès à la mémoire sont alignés sur les frontières naturelles des mots.
Les accès non alignés ne provoquent pas d'erreur ou d'exception. Les registres de données contiennent une copie de la mémoire, sans décalage.
On pourrait même schématiser de cette manière :
Les bits "perdus" servent juste à sélectionner un octet dans le (demi-)mot.
; Exemple 1 (lecture en mémoire) MOV 1231h A5 ; pointe A5 vers l'addresse 1231h (impaire donc non alignée) MOV D5 R2 ; copie le mot à l'adresse 1230 dans R2. ; Exemple 1 bis (YASEP32 seulement) MOV 1232h A1 ; pointe A1 vers l'addresse 1232h (non alignée sur une frontière de mot) MOV D1 R1 ; copie le mot à l'adresse 1230 dans R1. ; Exemple 2 (écriture en mémoire) MOV 1231h A2 ; pointe A2 vers l'addresse 1231h (impaire donc non alignée) MOV R2 D2 ; écrit R2 en mémoire à l'adresse 1230 ; Exemple 2 bis (YASEP32 seulement) MOV 1232h A3 ; pointe A3 vers l'addresse 1232h (non alignée sur une frontière de mot) MOV R4 D3 ; écrit R4 en mémoire à l'adresse 1230
Si on traite des octets ou demi-mots, l'alignement peut être effectué automatiquement par certaines instructions :
; Exemple 1' (lecture en mémoire) MOV 1231h A5 ; pointe A5 vers l'addresse 1231h (impaire donc non alignée) ESB D5 R2 ; Extrait l'octet à l'adresse 1231, duplique le bit de signe et écrit le résultat dans R2. ; Exemple 1' bis (YASEP32 seulement) MOV 1232h A1 ; pointe A1 vers l'addresse 1232h (non alignée sur une frontière de mot) ESH D1 R1 ; Extrait le demi-mot à l'adresse 1232, duplique le bit de signe et écrit le résultat dans R1. ; Exemple 2' (écriture en mémoire) MOV 1231h A2 ; pointe A2 vers l'addresse 1231h (impaire donc non alignée) IB R2 D2 ; prend l'octet de poids faible en R2, insère le résultat dans l'octet de poids fort de D2. ; Exemple 2' bis (YASEP32 seulement) MOV 1232h A3 ; pointe A3 vers l'addresse 1232h (non alignée sur une frontière de mot) IH R4 D3 ; prend le demi-mot de poids faible en R4, insère le résultat dans le demi-mot de poids fort de D3.
Les mots ou demi-mots non alignés doivent être reconstitués au moyen de séquences d'instructions appropriées.
(à écrire)
(ajouté au blog le mardi 8 Novembre 2011)
Ainsi donc, l'architecture du YASEP spécifie 5 registres "normaux" et 5 paires de registres adresses/données (A1/D1, A2/D2...) et il est assez difficile de trouver un équilibre entre ces nombres, car chaque application et chaque cas requiert un nombre différent de registres.
Si plus de registres normaux sont nécessaires (imaginons qu'on aie besoin de R6 et R7) alors on pourrait les assigner à D1 et D2 par exemple. Cependant, auparavant, il faut donner des adresses convenables à A1 et A2, sinon le programme pourrait planter à cause de valeurs aléatoires (ou pire) dans A1 et A2, ce qui écrirait D1 et D2 n'importe où...
Une autre situation indésirable pourrait se produire si on se sert de A1 et A2 comme registres de données. Chaque écriture déclencherait une lecture de la mémoire. Si en plus un système de mémoire paginée est utilisé, chaque écriture risque de déclencher une faute de page (ou de protection), ce qui ralentirait énormément le programme...
Le système de "parking" résout cela en définissant des adresses standardisées pour "garer" les registres d'adresses en évitant des effets de bords néfastes.
Les adresses de parking sont "négatives", avec tous les bits de poids fort à 1. Ces adresses se retrouvent tout "en haut" de la carte de la mémoire, dans une zone qui n'est pas nécessairement implémentée, ou alors réservée à des constantes adressées par des adresses immédiates courtes (de -8 à +7) :
MOV 6, A3 ; mem[6] contient une constante utile ou une valeur intermédiaire MOV D3,... ; dont l'adresse tient sur 4 bits
Afin de garder le système compatible avec les version qui n'implémentent pas le "parking", les adresses sont définies globalement pour tous les logiciels ou programmes. Ces adresses sont très simples à retenir, comme le montre le code suivant :
; gare tous les registres MOV -1, A1 MOV -2, A2 MOV -3, A3 MOV -4, A4 MOV -5, A5
Ces instructions pourront devenir des macros ou des pseudo-instructions.
Pour faciliter la réalisation matérielle, la numérotation des registres est adaptée (changée par rapport aux versions du YASEP pré-2011). En effet, en numérotant à l'envers, il y a maintenant un lien direct et binaire entre le numéro du registre et l'adresse de parking :
adresse binaire reg.bin reg.# registre -11111 111115 A1 -21110 110113 A2 -31101 101111 A3 -41100 10019 A4 -51011 01117 A5
On voit que les bits 0 à 2 de l'adresse sont identiques aux bits 1 à 3 du numéro de registre. C'est donc une condition très facile à détecter avec des portes logiques.
Au niveau de l'architecture, cela ne change quasiment rien. Le registres de données sont habituellement "cachés" au niveau du banc de registre. Ce que le système de garage ajoute, c'est juste l'inhibition du signal d'écriture vers la mémoire, qui serait normalement actif à chaque fois qu'on écrit dans le registre de données.
Alias : Les alias, ou collisions d'adresses, ne sont pas détectées. Si A4/D4 écrit à l'adresse -2, D2 n'est pas mis à jour. Le contraire signifierait que le bus d'écriture du pipeline puisse écrire simultanément dans 5 registres différents à la fois, ce qui n'est pas raisonnable.
Sauvegarde et restauration du contexte d'un thread : différentes approches sont possibles. Si le système de parking n'est pas utilisé, alors sauver le registre d'adresse suffit car le registre de donnée correspondant sera remis à la bonne valeur en relisant la mémoire lors de la restauration. Si le parking est implémenté alors le banc de registres pourra servir de "cache" pour la mémoire.
Ce système de parking, qui utilise des adresses prédéfinies, est plus "propre" que des alternatives où un bit par paire de registres définit si la paire est utilisée pour accéder à la mémoire ou comme registres normaux. L'avantage de cette alternative est qu'il est possible de libérer deux registres d'un coup, mais c'est plus compliqué à utiliser avec un compilateur ou au niveau d'un code source de haut niveau. En plus il faudrait sauver 5 bits supplémentaires lors d'un changement de contexte, le vrai cauchemar...
Finalement, le parking n'est pas aussi complexe qu'il n'y paraît : il ajoute quelques portes logiques pour inhiber les accès à la mémoire si un registre a une certaine valeur. Pour le programmeur, cela veut dire qu'il est possible d'avoir plus de registres de données, au détriment des accès à la mémoire, que le YASEP en question dispose (ou pas) du mécanisme de parking. Il est possible alors d'avoir R6, R7 et R8 mais cela empêchera l'utilisation de A1/D1, A2/D2 et A3/D3. À vous de choisir !
La retenue est un registre d'un bit qui mémorise la retenue de la dernière instruction ADD ou SUB executée. Le bit de retenue est mis à 1 lorsqu'une addition dépasse la largeur des registres :
.profile YASEP16 MOV 5678h R1 ADD CDEFh R1 R2 ; R2 <- 5678h + CDEFh = 12467h > FFFFh donc "carry" mis à 1 ADD 1234h R1 R2 ; R2 <- 5678h + 1234h = 68ACh <= FFFFh donc "carry" mis à 0
Le bit "carry" peut ensuite être testé par une instruction conditionnelle :
ADD 1234h R1 ; R1 <- R1 + 1234h (change la retenue) ADD 5 R2 CARRY ; Si la retenue est égale à 1, alors on additionne R2 et 5
L'instruction SUB est basée sur l'addition et donc utilise le même bit de retenue que ADD. Cependant, pour SUB, la valeur du bit est inversée, ce dernier est à 1 lorsque la soustraction n'a pas généré de retenue :
MOV 4 R1 SUB 3 R1 R2 ; R2 = 3 - 4 = -1 ==> carry=0 SUB 4 R1 R2 ; R2 = 4 - 4 = 0 ==> carry=1 SUB 5 R1 R2 ; R2 = 5 - 4 = 1 ==> carry=1
Seules les instructions marquées par le flag "CHANGE_CARRY" peuvent changer le bit de retenue. Parmi celles-cis on trouve CMPU et CMPS, qui sont comme l'instruction SUB mais qui n'écrivent pas le résultat de la soustraction. Il y a aussi ESH EZH et IH qui indiquent un alignement qui dépasse du mot, en mettant ce bit à 1. Toutes les autres instructions préserveront ce bit.
La retenue peut donc être testée de nombreux cycles après l'exécution de ADD/SUB/CMPU/CMPS, même après des appels ou des retours de fonctions. La meilleure manière de forcer la valeur de la retenue est d'utiliser astucieusement les instructions CMPU ou CMPS, avec des opérandes qui donneront toujours le même résultat :
; mettre la retenue à 0 : CMPU R1, R1 ; R1 est égal à R1 donc le flag ne peut pas être à 1. ; mettre la retenue à 1 : CMPU 0, PC ; PC est (quasiment) toujours supérieur à 0 donc la retenue est forcément à 1. ; (devrait devenir une macro/substitution)
Comme pour le bit de retenue, le bit Zero est mis à jour pour les opcodes marqués par le flag CHANGE_ZERO.
Cela peut sembler redondant avec la condition "zero" qui peut tester presque tous les registres. Cependant, certaines instructions n'écrivent par le résultat du calcul dans un registre : CMPU/CMPS.