Skip to the content.

Welcome here! | Articles | Main projects | About me

(FR) Analysons le microprocesseur MOS Technology 6502


Introduction

Ceux qui me connaissent le savent déjà, mais je suis un passionné de programmation bas niveau, plus spécifiquement d’émulation de consoles rétro, telles que la NES. Au cours de cet article, je vais présenter le processeur MOS Technology 6502, qui est (grossièrement) celui utilisé par la NES. Il s’agit d’un processeur 8 bits, c’est-à-dire que les valeurs qu’il manipule sont sur un octet. Ainsi, ses registres peuvent stocker des valeurs entre 0x00 et 0xff (0 à 255 ou -127 à 127 si l’on considère le bit de signe). Une exception existe pour le registre PC (le program counter, que nous aborderons plus tard), qui peut quant à lui stocker des valeurs allant de 0x0000 à 0xffff.


Mémoire

La mémoire du 6502 est segmentée en pages de 256 octets : $00-$ff, $100-$1ff, et ainsi de suite jusqu’à $ffff, soit un total de… 256 pages !

Note - En 6502, les nombres en hexadécimal sont préfixés par un $.

La plage de $00 à $ff incluse correspond à la “Zero Page”. Cette page a l’avantage d’être la plus rapide d’accès car un seul octet est nécessaire pour représenter l’adresse, contre deux pour les autres adresses. En effet, $ff peut être stocké dans un seul octet, tandis que $100 nécessite deux octets, contenant respectivement $00 et $01 (dans cet ordre à cause de l’endianness). Ainsi, le CPU n’a besoin de lire qu’un octet pour accéder à une adresse entre $00 et $ff là où il aurait fallu en lire deux pour une adresse plus grosse.

La page de $100 à $1ff correspond à zone occupée par la pile. La pile est une zone mémoire de type LIFO (Last In, First Out) sur laquelle on entrepose (push) des valeurs telles que des adresses mémoires, paramètres de fonctions, ou encore des registres avant modification. Le registre correspondant (S, que nous aborderons dans la section suivante) pointe en premier lieu sur $1ff, puis $1fe, et ainsi de suite au fur et à mesure que l’on push.

Concrètement, si l’on push 1, puis 2, puis 3, la pile ressemblera à ceci :

+--------- $0200 ---------+
|                         |
|    $1ff :  1            |
|    $1fe :  2            |
|    $1fd :  3            |
|                         |
            ...
|                         |
|                         |
+--------- $0100 ---------+

Il est ensuite possible d’accéder manuellement au contenu de la pile grâce au registre S ou aux adresses correspondantes. En effet, si l’on reprend l’exemple ci-dessus, nous savons que si l’on accède manuellement à l’adresse $1fd, nous obtiendrons la valeur 3.

Le reste de la mémoire est libre d’accès. Ainsi, la RAM du 6502 est organisée comme suit :

+-------- $10000 ---------+
|                         |
|                         |
|                         |
           ...
|                         |
|                         |
|                         |
+--------- $0200 ---------+
|                         |
|        Stack page       |
|                         |
+--------- $0100 ---------+
|                         |
|        Zero page        |
|                         |
+--------- $0000 ---------+

Registres & modes d’adressage

Le 6502 dispose des registres suivants :

Sans rentrer dans les détails car il me faudrait un article entier pour en parler, le 6502 dispose d’une multitude de modes d’adressages. Il s’agit des différentes façons d’effectuer des opérations. Plus d’informations dont disponibles ici.

Par exemple, les valeurs $ff et #$1ff correspondent à deux modes d’adressages différents, car l’une contient une adresse et l’autre une valeur dite “immédiate”. Ces deux modes d’adressages sont appelés “Absolute” et “Immediate”. Accéder à la “Zero Page” ou à une adresse supérieure à $ff correspond également à deux modes d’adressages différents, car l’opcode pour adresser la plage $00-$ff est différent. De cette manière, le processeur sait s’il doit lire un ou deux octets après l’instruction.


Instructions

Le 6502 dispose d’un jeu d’instructions assez riche permettant d’accéder aux différents registres et à la mémoire. L’instruction lda (“LoaD into Accumulator”) permet d’écrire dans A, tandis que ldx (“LoaD into X index”) et ldy (“LoaD into Y index”) écrivent respectivement dans X et Y.

Exemple :

lda #$ff  ; on écrit 255 dans A. Notez le '#', correspondant à une valeur "immédiate".
ldx $1ff  ; on écrit le haut de la pile dans X.

Note - En 6502, une valeur numérique doit être préfixée par un #. Autrement, elle sera considérée comme une adresse. Ainsi, lda $ff ne stockera non pas $ff dans A, mais la valeur située à l’adresse $ff.

Il est possible de stocker les valeurs des différents registres en mémoire via les instructions sta (“STore Accumulator”), stx (“STore X index”) et sty (“STore Y index”) :

sta $42   ; on écrit le contenu de A à l'adresse $42
stx $100  ; on écrit le contenu de X à l'adresse $100

Les instructions tax (“Transfer Accumulator to X index”), tay (“Transfer Accumulator to Y index”), txa (“Transfer X index to Accumulator”) et tya (“Transfer Y index to Accumulator”) permettent de copier un registre dans un autre. Il existe également les instructions txs (“Transfer X index to Stack pointer”) et tsx (“Transfer Stack pointer to X index”)

Ainsi, pour transférer X dans Y, il faut préalablement passer par une adresse :

stx $100  ; on copie X à l'adresse $100
ldy $100  ; on copie la valeur située à l'adresse $100 dans Y

Ou par A :

txa  ; on copie X dans A
tay  ; on copie A dans Y

Néanmoins, on perd la valeur stockée dans A. Pour la récupérer, nous avons un formidable outil : la pile ! Les instructions pour manipuler la pile sont pha (“PusH A”) et pla (“PulL A”). Il est également possible de placer le registre P sur la pile via php (“PusH Processor status”) et de le récupérer grâce à l’instruction plp (“PulL Processor status”). Enfin, on peut accéder à la pile dans le cadre des subroutines, dont nous parlerons plus tard.

Exemple d’utilisation de la pile :

lda #$12 ; situation initiale : nous assignons la valeur #$12 à A.

pha  ; la pile contient désormais #$12
txa  ; on copie X dans A
tay  ; on copie A dans Y
pla  ; A récupère la valeur située sur la pile.

En assembleur, il existe des labels, qui servent pour les sauts. Cela équivaut à des fonctions dans la plupart des langages de programmation. On peut appeler une subroutine (équivalent d’une fonction en assembleur) via l’instruction jsr (“Jump to SubRoutine”) :

mon_label:
    lda #$00
    tax
           
jsr mon_label  ; on place PC sur la pile

L’appel à jsr va ici faire deux choses :

Si vous êtes attentifs, vous remarquerez que l’on a là un magnifique cas de boucle infinie ! En effet, nous ne sortons jamais correctement de mon_label, et PC pointera à nouveau sur l’instruction jsr mon_label ! C’est là qu’intervient l’instruction rts (“ReTurn from Subroutine”) :

mon_label:
    lda #$00
    tax
    rts ; on 'pull' PC, puis on l'incrémente de 1
           
jsr mon_label  ; on place PC sur la pile
               ; <- à l'appel de rts, nous arriverons ici

Le registre PC faisant deux octets (pour rappel, il peut stocker des valeurs entre $0000 et $ffff), il doit être stocké en deux temps sur la pile : l’octet de poids fort, puis l’octet de poids faible. Concrètement, si PC contient la valeur $abcd, alors $ab sera d’abord poussé sur la pile, puis $cd.

À présent, nous savons comment manipuler les registres, la mémoire, la pile et les subroutines. Nous connaissons désormais les rudiments de l’assembleur 6502 ! Intéressons-nous à un cas d’école classique, j’ai nommé la NES !


Cas concret : la NES

La NES (Nintendo Entertainment System), ou Famicom au Japon, est l’une des consoles phares de Nintendo. Elle nous intéresse ici car son processeur, le RP2A03 (RP2A07 chez nous) de Ricoh, se base sur le 6502 et embarque son jeu d’instructions. Dans cette partie, nous nous intéresserons à la façon dont la NES utilise le 6502.

La RAM de la NES est organisée comme suit :

Adresses Description
$0000-$00ff Zero Page
$0100-$01ff Stack
$0200-$02ff Sprites (OAM)
$0300-$07ff RAM utilisable
$0800-$1fff Miroirs de $0000 à $07ff
$2000-$2007 Registres du PPU
$2008-$3fff Miroirs de $2000 à $2007
$4000-$4017 Registres de l’APU + joypads
$4018-$5fff Inutilisé
$6000-$7fff SRAM
$8000-$ffff PRG de la ROM

Concrètement, seules les 8 premières pages (soit la plage d’adresses entre $0000 et $07ff) sont réellement consacrées à la RAM adressable. Le reste est dédié à d’autres utilisations propres à la NES.

La plage de $0200 à $02ff, l’OAM (Object Atributte Memory), contient les informations sur jusqu’à 64 sprites à afficher. Chaque sprite est décrit via 4 octets, comme présenté sur cette page. Elle est réécrite à chaque frame.

Nous ne parlerons pas du PPU (Picture Processing Unit) ni de l’APU (Audio Processing Unit) ici, qui mériteraient chacun un article.

Les deux joypads sont lus depuis les adresses $4016 et $4017. Chaque manette n’a que huit touches (quatre directions et quatres boutons), un seul octet suffit donc pour stocker les différents inputs. Les touches sont mappées ainsi :

Bit Touche
0 A
1 B
2 Select
3 Start
4 Haut
5 Bas
6 Gauche
7 Droite

Pour rappel, une manette NES ressemble à ceci :

Entre $6000 et $7fff se trouve la SRAM (Save RAM), qui est utilisée pour les sauvegardes.

Enfin, de $8000 à $ffff se trouve la PRG ROM (PRoGram ROM). Il s’agit tout simplement du code de la ROM qui doit être exécuté. Il est chargé en mémoire d’un bloc afin d’y accéder plus rapidement.


Conclusion

En somme, le 6502 est un processeur non seulement très intéressant mais également très amusant à manipuler ! Son jeu d’instructions est certes limité, mais permet néanmoins de faire beaucoup de choses ! La mémoire est suffisante pour effectuer la plupart des traitements requis par des machines telles que la NES, tout en sachant qu’elle peut être étendue si besoin, mais cela fera l’objet d’un prochain article.

Si vous voulez vous exercer avec l’assembleur 6502, je vous conseille ce site, qui met à disposition un IDE et une série de tutoriels.