Section courante

A propos

Section administrative du site

Techniques de programmation

Cette page présente les techniques de programmation courantes en assembleur 6502, incluant l'arithmétique multi-précision, la multiplication/division logicielle, la manipulation de chaînes, les tables de saut, le code auto-modifiant, les optimisations et les conventions d'appel de sous-routines.

Arithmétique 16 bits et multi-précision

Le 6502 est un microprocesseur 8 bits. Toute arithmétique sur des valeurs de plus de 8 bits doit être effectuée manuellement, octet par octet, en utilisant le drapeau C (Carry) pour propager la retenue entre les octets.

Addition 16 bits

Pour additionner deux valeurs 16 bits, on additionne d'abord les octets de poids faible (low bytes) avec ADC, puis les octets de poids fort (high bytes) avec ADC. Le drapeau Carry propage automatiquement la retenue.

  1. ; Resultat := Operande1 + Operande2 (16 bits)
  2. ; Operande1 stocke en OP1_LO / OP1_HI
  3. ; Operande2 stocke en OP2_LO / OP2_HI
  4. ; Resultat  stocke en RES_LO / RES_HI
  5.  
  6. CLC              ; effacer la retenue avant l'addition
  7. LDA OP1_LO       ; charger l'octet bas de l'operande 1
  8. ADC OP2_LO       ; additionner l'octet bas de l'operande 2
  9. STA RES_LO       ; stocker l'octet bas du resultat
  10. LDA OP1_HI       ; charger l'octet haut de l'operande 1
  11. ADC OP2_HI       ; additionner l'octet haut + retenue
  12. STA RES_HI       ; stocker l'octet haut du resultat

Si C=1 après la deuxième ADC, il y a un débordement (le résultat dépasse 16 bits).

Soustraction 16 bits

  1. ; Resultat := Operande1 - Operande2 (16 bits)
  2. SEC              ; mettre la retenue a 1 avant la soustraction
  3. LDA OP1_LO
  4. SBC OP2_LO
  5. STA RES_LO
  6. LDA OP1_HI
  7. SBC OP2_HI
  8. STA RES_HI

Si C=0 après la deuxième SBC, le résultat est négatif (emprunt non satisfait).

Incrémentation 16 bits

Le 6502 n'a pas d'instruction INC 16 bits. On peut utiliser l'addition avec 1 :

  1.     ; VAR := VAR + 1 (16 bits)
  2.     INC VAR_LO       ; incrementer l'octet bas
  3.     BNE Skip         ; si pas de debordement (!=0), termine
  4.     INC VAR_HI       ; sinon incrementer l'octet haut
  5.   Skip:

Cette technique est plus rapide (5-8 cycles) que l'addition 16 bits complète (18 cycles).

Décrémentation 16 bits

  1.     ; VAR := VAR - 1 (16 bits)
  2.     LDA VAR_LO       ; charger l'octet bas
  3.     BNE Skip2        ; si != 0, pas besoin de toucher l'octet haut
  4.     DEC VAR_HI       ; decrementer l'octet haut (emprunt)
  5.   Skip2:
  6.     DEC VAR_LO       ; decrementer l'octet bas

Comparaison 16 bits

Le 6502 n'a pas de CMP 16 bits. On compare d'abord les octets de poids fort, puis les octets de poids faible si les octets hauts sont égaux :

  1.     ; Comparer OP1 (16 bits) avec OP2 (16 bits)
  2.     LDA OP1_HI
  3.     CMP OP2_HI
  4.     BNE Done_Cmp     ; si octets hauts differents, flags valides
  5.     LDA OP1_LO
  6.     CMP OP2_LO       ; comparer les octets bas
  7.   Done_Cmp:
  8.     ; A ce point, les drapeaux C, Z, N sont corrects
  9.     ; BCC = OP1 < OP2 (non signe)
  10.     ; BCS = OP1 >= OP2 (non signe)
  11.     ; BEQ = OP1 == OP2

Décalage 16 bits

Décalage à gauche (multiplication par 2) :

  1. ASL VAR_LO       ; decaler l'octet bas a gauche
  2. ROL VAR_HI       ; decaler l'octet haut avec la retenue

Décalage a droite (division par 2, non signe) :

  1. LSR VAR_HI       ; decaler l'octet haut a droite
  2. ROR VAR_LO       ; decaler l'octet bas avec la retenue

Décalage a droite arithmétique (division par 2, signe) :

  1. LDA VAR_HI       ; charger l'octet haut
  2. ASL A             ; mettre le bit de signe dans C
  3. ROR VAR_HI       ; decaler en preservant le signe
  4. ROR VAR_LO       ; propager vers l'octet bas

Négation 16 bits (complément a deux)

  1. ; VAR := -VAR (16 bits)
  2. ; Methode : NOT(VAR) + 1
  3.  
  4. SEC              ; C := 1 pour l'addition de 1
  5. LDA #$00
  6. SBC VAR_LO       ; A := 0 - VAR_LO - ~C = ~VAR_LO + 1
  7. STA VAR_LO
  8. LDA #$00
  9. SBC VAR_HI       ; A := 0 - VAR_HI - ~C
  10. STA VAR_HI

Arithmétique multi-précision (24 bits, 32 bits, ...)

Le principe s'étend a n'importe quelle taille. Il suffit de chaîner les opérations ADC/SBC sur chaque octet supplémentaire.

  1. ; Addition 32 bits : RES := OP1 + OP2
  2. CLC
  3. LDA OP1+0        ; octet 0 (poids faible)
  4. ADC OP2+0
  5. STA RES+0
  6. LDA OP1+1        ; octet 1
  7. ADC OP2+1
  8. STA RES+1
  9. LDA OP1+2        ; octet 2
  10. ADC OP2+2
  11. STA RES+2
  12. LDA OP1+3        ; octet 3 (poids fort)
  13. ADC OP2+3
  14. STA RES+3

Coût en cycles : environ 6 cycles par octet supplémentaire
(LDA + ADC + STA = 3+3+3 = 9 cycles si en page zéro).

Multiplication et division en logiciel

Le 6502 ne possède aucune instruction de multiplication ou de division. Ces opérations doivent être implémentées en logiciel, a l'aide de décalages et d'additions.

Multiplication 8x8 → 16 bits

Algorithme de multiplication par décalage-et-addition (shift and add). On examine chaque bit du multiplicateur ; quand un bit est a 1, on additionne le multiplicande au résultat :

  1.     ; RESULT (16 bits) := MCAND * MPLIER (8 bits chacun)
  2.     ; MCAND   = multiplicande (page zero)
  3.     ; MPLIER  = multiplicateur (page zero)
  4.     ; RESULT  = resultat 16 bits (RES_LO, RES_HI, page zero)
  5.  
  6.   Multiply_8x8:
  7.     LDA #$00
  8.     STA RES_HI       ; initialiser le resultat haut a 0
  9.     LDX #$08         ; compteur de bits (8 bits)
  10.   Mult_Loop:
  11.     LSR MPLIER       ; decaler le multiplicateur a droite
  12.                       ; le bit de poids faible va dans C
  13.     BCC No_Add       ; si C=0, ne pas additionner
  14.     CLC
  15.     ADC MCAND        ; additionner le multiplicande (A = RES_LO)
  16.     BCC No_Add
  17.     INC RES_HI       ; propager la retenue dans l'octet haut
  18.   No_Add:
  19.     ROR RES_HI       ; decaler le resultat 16 bits a droite
  20.     ROR A             ;   (RES_HI:A) >> 1
  21.     DEX
  22.     BNE Mult_Loop
  23.     STA RES_LO       ; stocker l'octet bas du resultat
  24.     RTS

Complexité : toujours 8 itérations, environ 100-150 cycles.

Multiplication par une constante

Pour des constantes spécifiques, il est souvent plus rapide d'utiliser des décalages et des additions :

  1. ; A := A * 2
  2. ASL A             ; 1 instruction, 2 cycles
  3.  
  4. ; A := A * 4
  5. ASL A
  6. ASL A             ; 2 instructions, 4 cycles
  7.  
  8. ; A := A * 3 (= A + A*2)
  9. STA temp          ; sauvegarder A
  10. ASL A             ; A := A * 2
  11. CLC
  12. ADC temp          ; A := A*2 + A = A*3
  13.  
  14. ; A := A * 5 (= A + A*4)
  15. STA temp
  16. ASL A
  17. ASL A             ; A := A * 4
  18. CLC
  19. ADC temp          ; A := A*4 + A = A*5
  20.  
  21. ; A := A * 10 (= (A*4 + A) * 2 = A*5*2)
  22. ASL A             ; A := A * 2
  23. STA temp
  24. ASL A
  25. ASL A             ; A := A * 8
  26. CLC
  27. ADC temp          ; A := A*8 + A*2 = A*10
  28.  
  29. ; X := X * 40 (utile pour les ecrans 40 colonnes)
  30. ; 40 = 8 * 5 = 8 * (4 + 1)
  31. TXA
  32. ASL A
  33. ASL A
  34. ASL A             ; A := X * 8
  35. STA temp
  36. ASL A
  37. ASL A             ; A := X * 32
  38. CLC
  39. ADC temp          ; A := X*32 + X*8 = X*40

Division 16/8 → 8 bits quotient, 8 bits reste

  1.     ; DIVIDEND (16 bits) / DIVISOR (8 bits)
  2.     ; -> QUOTIENT (8 bits), REMAINDER (8 bits)
  3.  
  4.   Divide_16_8:
  5.     LDA #$00          ; initialiser le reste
  6.     LDX #$08          ; compteur de bits
  7.   Div_Loop:
  8.     ASL DIVIDEND_LO   ; decaler le dividende 16 bits a gauche
  9.     ROL DIVIDEND_HI
  10.     ROL A             ; le bit sortant entre dans le reste (A)
  11.     CMP DIVISOR       ; reste >= diviseur ?
  12.     BCC Div_Skip      ; non, passer
  13.     SBC DIVISOR       ; oui, soustraire le diviseur
  14.     INC DIVIDEND_LO   ; mettre le bit de quotient a 1
  15.   Div_Skip:
  16.     DEX
  17.     BNE Div_Loop
  18.     STA REMAINDER     ; stocker le reste
  19.     ; Le quotient est dans DIVIDEND_LO
  20.     RTS

Division par une puissance de 2

Diviser par une puissance de 2 est un simple décalage a droite :

  1. ; A := A / 2
  2. LSR A             ; 1 instruction, 2 cycles
  3.  
  4. ; A := A / 4
  5. LSR A
  6. LSR A
  7.  
  8. ; A := A / 16
  9. LSR A
  10. LSR A
  11. LSR A
  12. LSR A

Modulo par une puissance de 2

Le reste d'une division par une puissance de 2 est obtenu par un AND avec le masque (puissance - 1) :

  1. ; A := A MOD 8 (= A AND 7)
  2. AND #$07
  3.  
  4. ; A := A MOD 16
  5. AND #$0F
  6.  
  7. ; A := A MOD 32
  8. AND #$1F

Manipulation de chaînes de caractères

Le 6502 n'a pas d'instructions de chaînes dédiées (contrairement au 80386 avec REP MOVSB). Les chaînes sont manipulées via des boucles utilisant l'adressage indexe ou indirect indexe.

Deux conventions de chaînes sont courantes :

Copie de chaîne ASCIIZ

  1.     ; Copier la chaine a l'adresse (SRC) vers (DST)
  2.     ; SRC et DST sont des pointeurs 16 bits en page zero
  3.  
  4.   StrCopy:
  5.     LDY #$00
  6.   CopyLoop:
  7.     LDA (SRC),Y       ; charger un caractere
  8.     STA (DST),Y       ; stocker dans la destination
  9.     BEQ CopyDone      ; si c'est le zero final, termine
  10.     INY
  11.     BNE CopyLoop      ; continuer (max 256 caracteres)
  12.   CopyDone:
  13.     RTS

Longueur de chaîne ASCIIZ

  1.     ; Retourner la longueur de la chaine a (STR) dans Y
  2.   StrLen:
  3.     LDY #$FF
  4.   LenLoop:
  5.     INY
  6.     LDA (STR),Y
  7.     BNE LenLoop       ; continuer tant que non zero
  8.     ; Y contient la longueur
  9.     RTS

Comparaison de chaînes ASCIIZ

  1.     ; Comparer (STR1) et (STR2)
  2.     ; Resultat : Z=1 si egales, Z=0 si differentes
  3.   StrCmp:
  4.     LDY #$00
  5.   CmpLoop:
  6.     LDA (STR1),Y
  7.     CMP (STR2),Y
  8.     BNE CmpDiff       ; caracteres differents
  9.     CMP #$00
  10.     BEQ CmpDone       ; fin des deux chaines (egales)
  11.     INY
  12.     BNE CmpLoop
  13.   CmpDiff:
  14.     ; Z=0 (chaines differentes), C indique l'ordre
  15.   CmpDone:
  16.     RTS

Recherche d'un caractère dans une chaîne

  1.     ; Chercher le caractere dans A dans la chaine a (STR)
  2.     ; Si trouve : C=1, Y = position
  3.     ; Si non trouve : C=0
  4.   StrChr:
  5.     STA temp          ; sauvegarder le caractere cherche
  6.     LDY #$00
  7.   ChrLoop:
  8.     LDA (STR),Y
  9.     BEQ ChrNotFound   ; fin de chaine, pas trouve
  10.     CMP temp
  11.     BEQ ChrFound      ; trouve !
  12.     INY
  13.     BNE ChrLoop
  14.   ChrNotFound:
  15.     CLC               ; C=0 : pas trouve
  16.     RTS
  17.   ChrFound:
  18.     SEC               ; C=1 : trouve, Y = position
  19.     RTS

Concaténation de chaînes ASCIIZ

  1.     ; Concatener (STR2) a la fin de (STR1)
  2.     ; STR1 doit avoir assez d'espace
  3.   StrCat:
  4.     LDY #$00
  5.   FindEnd:
  6.     LDA (STR1),Y      ; trouver la fin de STR1
  7.     BEQ CatCopy
  8.     INY
  9.     BNE FindEnd
  10.   CatCopy:
  11.     LDX #$00
  12.   CatLoop:
  13.     LDA (STR2,X)      ; lire depuis STR2
  14.     STA (STR1),Y      ; ecrire a la fin de STR1
  15.     BEQ CatDone       ; zero final copie
  16.     INX
  17.     INY
  18.     BNE CatLoop
  19.   CatDone:
  20.     RTS

Conversion majuscules

  1.     ; Convertir la chaine a (STR) en majuscules
  2.   StrUpper:
  3.     LDY #$00
  4.   UpperLoop:
  5.     LDA (STR),Y
  6.     BEQ UpperDone
  7.     CMP #'a'
  8.     BCC UpperSkip     ; < 'a', pas une minuscule
  9.     CMP #'z'+1
  10.     BCS UpperSkip     ; > 'z', pas une minuscule
  11.     SEC
  12.     SBC #$20          ; convertir en majuscule (a-z -> A-Z)
  13.     STA (STR),Y
  14.   UpperSkip:
  15.     INY
  16.     BNE UpperLoop
  17.   UpperDone:
  18.     RTS

Tables de saut

Les tables de saut (jump tables) permettent d'implémenter des structures switch/case ou des dispatchers efficaces.

Table de saut simple avec JMP indirect

On peut utiliser un index pour calculer l'adresse dans une table et effectuer un saut indirect :

  1.     ; A contient le numero de commande (0, 1, 2, ...)
  2.     ASL A             ; A := A * 2 (chaque adresse fait 2 octets)
  3.     TAX               ; transferer dans X pour l'indexation
  4.     LDA JmpTable,X    ; lire l'octet bas de l'adresse
  5.     STA JmpAddr
  6.     LDA JmpTable+1,X  ; lire l'octet haut
  7.     STA JmpAddr+1
  8.     JMP (JmpAddr)     ; saut indirect
  9.  
  10.   JmpTable:
  11.     .WORD Cmd_0       ; adresse du descripteur pour la commande 0
  12.     .WORD Cmd_1       ; adresse du descripteur pour la commande 1
  13.     .WORD Cmd_2       ; adresse du descripteur pour la commande 2
  14.     .WORD Cmd_3       ; adresse du descripteur pour la commande 3

Table de saut avec RTS (technique "RTS trick")

Une technique classique du 6502 consiste a empiler une adresse sur la pile puis utiliser RTS pour sauter. RTS dépile l'adresse et y ajoute 1, donc on entrepose (adresse - 1) dans la table.

  1.     ; A contient le numero de commande (0, 1, 2, ...)
  2.     ASL A             ; A := A * 2
  3.     TAX
  4.     LDA JmpTableHi,X  ; octet haut de (adresse - 1)
  5.     PHA               ; empiler
  6.     LDA JmpTableLo,X  ; octet bas de (adresse - 1)
  7.     PHA               ; empiler
  8.     RTS               ; sauter a l'adresse empilee + 1
  9.  
  10.   JmpTableLo:
  11.     .BYTE <(Cmd_0-1), <(Cmd_1-1), <(Cmd_2-1), <(Cmd_3-1)
  12.   JmpTableHi:
  13.     .BYTE >(Cmd_0-1), >(Cmd_1-1), >(Cmd_2-1), >(Cmd_3-1)

Avantages de la technique RTS :

Table de saut avec JMP ($xxFF) - Bug du 6502 NMOS

Attention : sur le 6502 NMOS, l'instruction JMP indirect a un bug quand l'adresse du vecteur est a la fin d'une page ($xxFF). Au lieu de lire l'octet haut a $xx00 de la page suivante, il le lit a $xx00 de la MEME page.

Exemple :

JMP ($02FF) lit l'octet bas a $02FF et l'octet haut a $0200 (au lieu de $0300)

Ce bug est corrige sur le 65C02. Sur le 6502 NMOS, éviter de placer des pointeurs a des adresses de fin de page.

Code auto-modifiant

Le code auto-modifiant (self-modifying code, SMC) est une technique ou le programme modifie ses propres instructions pendant l'exécution. Sur le 6502, c'est une technique courante et souvent nécessaire pour contourner les limitations du microprocesseur.

Pourquoi le code auto-modifiant ?

Le 6502 n'a que 3 registres 8 bits (A, X, Y) et des modes d'adressage limites. Le code auto-modifiant permet de :

Modification d'une adresse

La technique la plus courante consiste a modifier l'adresse d'une instruction LDA/STA/etc. :

  1.     ; Lire une valeur a une adresse calculee
  2.     LDA addr_lo
  3.     STA Load+1        ; modifier l'octet bas de l'adresse
  4.     LDA addr_hi
  5.     STA Load+2        ; modifier l'octet haut de l'adresse
  6.   Load:
  7.     LDA $0000         ; cette instruction sera modifiee

Ici, Load+1 est l'octet bas de l'adresse dans l'instruction LDA absolue (opcode $AD suivi de l'adresse en little-endian).

Copie de bloc mémoire rapide

En utilisant le code auto-modifiant, on peut créer une copie de bloc sans pointeurs en page zéro :

  1.   CopyBlock:
  2.     LDX #$00
  3.   CopyBLoop:
  4.   ReadAddr:
  5.     LDA $1000,X       ; l'adresse sera modifiee
  6.   WriteAddr:
  7.     STA $2000,X       ; l'adresse sera modifiee
  8.     INX
  9.     BNE CopyBLoop
  10.     ; Pour copier plus de 256 octets, incrementer les octets
  11.     ; hauts des adresses :
  12.     INC ReadAddr+2    ; incrementer la page source
  13.     INC WriteAddr+2   ; incrementer la page destination
  14.     ; ... continuer selon le nombre de pages ...
  15.     RTS

Précautions avec le code auto-modifiant

Optimisations courantes

Utiliser la page zéro

Les accès en page zéro sont plus rapides (1 cycle de moins) et les instructions sont plus courtes (1 octet de moins pour l'adresse). Placer les variables fréquemment utilisées en page zéro.

  1. LDA $00           ; page zero : 3 cycles, 2 octets
  2. LDA $0100         ; absolu : 4 cycles, 3 octets

Dérouler les boucles (loop unrolling)

Au lieu d'une boucle avec branchement, répéter le corps de la boucle pour éliminer le coût du branchement :

  1.     ; Boucle normale (effacer 4 octets) :
  2.     LDA #$00
  3.     LDX #$03
  4.   ClearLoop:
  5.     STA $0400,X
  6.     DEX
  7.     BPL ClearLoop     ; 4 iterations * (4+2+3) = 36 cycles
  8.  
  9.     ; Version deroulee :
  10.     LDA #$00
  11.     STA $0400         ; 4 cycles
  12.     STA $0401         ; 4 cycles
  13.     STA $0402         ; 4 cycles
  14.     STA $0403         ; 4 cycles = 16 cycles total

Le déroulement est efficace quand la taille du code n'est pas un facteur limitant.

Utiliser X et Y au lieu de la mémoire

Les registres X et Y sont rapides pour les compteurs :

  1. ; Avec variable en memoire :
  2. DEC counter       ; 5 cycles
  3. BNE Loop          ; 3 cycles = 8 cycles
  4.  
  5. ; Avec registre X :
  6. DEX               ; 2 cycles
  7. BNE Loop          ; 3 cycles = 5 cycles

Tester un bit sans charger la valeur

L'instruction BIT teste les bits 7 et 6 directement sans modifier A :

  1. BIT $D011         ; teste le registre du VIC-II
  2. BPL Wait          ; branche si bit 7 = 0
  3. BVS Skip          ; branche si bit 6 = 1

Utiliser TXA/TYA au lieu de LDA

Si la valeur est déjà dans X ou Y, un transfert est plus rapide qu'un chargement mémoire :

  1. TXA               ; 2 cycles
  2. ; vs
  3. LDA variable      ; 3-4 cycles

Remplacer CMP #$00 par des tests implicites

Beaucoup d'instructions mettent déjà a jour les drapeaux N et Z. Un CMP #$00 supplémentaire est inutile :

  1. ; Inutile :
  2. LDA value
  3. CMP #$00
  4. BEQ IsZero
  5.  
  6. ; Mieux :
  7. LDA value         ; Z est deja mis a jour par LDA
  8. BEQ IsZero

Les instructions qui mettent a jour N et Z :

Utiliser DEC/BNE au lieu de CMP pour les boucles

Quand on compte vers zero, DEC suivi de BNE est plus compact que CMP + BNE :

  1.     ; Au lieu de :
  2.     LDX #$05
  3.   Loop1:
  4.     ; ... corps ...
  5.     DEX
  6.     CPX #$00
  7.     BNE Loop1
  8.  
  9.     ; Utiliser :
  10.     LDX #$05
  11.   Loop2:
  12.     ; ... corps ...
  13.     DEX
  14.     BNE Loop2         ; DEX met Z a 1 quand X atteint 0

Multiplier/diviser par des puissances de 2

Utiliser les décalages ASL/LSR au lieu de routines de multiplication/division pour les puissances de 2 :

  1. ASL A    ; * 2
  2. ASL A    ; * 4
  3. ASL A    ; * 8   (3 instructions, 6 cycles)
  4.  
  5. ; vs multiplication generique : 100+ cycles

Exploiter le wraparound de la pile

La pile du 6502 fait 256 octets ($0100-$01FF) et enveloppe automatiquement (wraparound). On peut utiliser la pile comme un tampon en anneau sans vérification de débordement pour certaines applications.

Eviter les branchements quand possible

Les branchements pris coûtent un cycle de plus (3 cycles au lieu de 2) et un cycle supplémentaire s'ils traversent une frontière de page. Organiser le code pour que le cas le plus fréquent ne branche pas :

  1. ; Si value est generalement non-zero :
  2. LDA value
  3. BEQ RareCase      ; branchement rarement pris (2 cycles)
  4. ; ... code frequent ...

Utiliser le mode décimal avec précaution

Le mode décimal (SED/CLD) est utile pour l'affichage de pointages et de compteurs BCD, mais :

Tableau récapitulatif des coûts en cycles

Opération Cycles Notes
LDA immédiate 2 La plus rapide
LDA page zero 3 Economique
LDA absolu 4 Standard
LDA absolu,X / absolu,Y 4-5 +1 si traversée de page
LDA (indirect),Y 5-6 +1 si traversée de page
LDA (indirect,X) 6 Toujours 6 cycles
STA page zéro 3  
STA absolu 4  
ASL A 2 Rapide pour multiplier
INC page zéro 5 Plus lent que INC registre
INX / INY 2 Rapide
JSR 6 Appel de sous-routine
RTS 6 Retour
BEQ/BNE (pris) 3-4 +1 si traversée de page
BEQ/BNE (non pris) 2 Rapide
JMP absolu 3 Saut direct
JMP indirect 5 6502 uniquement

Sous-routines et conventions d'appel

Appel et retour de base

L'instruction JSR (Jump to Subroutine) empile l'adresse de retour moins 1 (PC-1, soit le dernier octet de l'instruction JSR) sur la pile et transfère le contrôle au sous-programme. RTS (Return from Subroutine) dépile cette adresse, l'incrémente de 1 et reprend l'exécution.

  1.     JSR ma_routine     ; appel : empile (PC-1), saute
  2.     ; ... execution continue ici apres RTS ...
  3.  
  4.   ma_routine:
  5.     ; ... corps du sous-programme ...
  6.     RTS                ; retour : depile PC, PC := PC + 1

Etat de la pile lors de l'appel :

État Description
Avant JSR SP → [ancien contenu]
Après JSR SP → [PCL (adresse retour - 1, octet bas)]
[PCH (adresse retour - 1, octet haut)]
[ancien contenu]

Note : JSR empile PC-1, pas PC. C'est pourquoi RTS fait PC := PC+1 après le dépilage. C'est une différence importante avec le 68000 (BSR/JSR empilent le PC exact) et le 80386 (CALL empile EIP exact).

Passage de parametres par registre

La méthode la plus simple et la plus rapide. Les paramètres sont places dans A, X, Y avant l'appel. Le résultat est généralement retourne dans A (ou A/X pour 16 bits).

  1.     ; Appel : resultat = Max(a, b)
  2.     LDA a             ; premier parametre dans A
  3.     LDX b             ; deuxieme parametre dans X
  4.     JSR Max
  5.     ; A contient le resultat
  6.  
  7.   Max:
  8.     STX temp
  9.     CMP temp          ; comparer A avec X
  10.     BCS MaxDone       ; si A >= X (non signe), A est deja le max
  11.     TXA               ; sinon, X est le max
  12.   MaxDone:
  13.     RTS

Conventions courantes pour les registres :

Registre Usage typique
A Paramètre principal / valeur de retour
X Paramètre secondaire / index
Y Paramètre tertiaire / index
C (Carry) Indicateur d'erreur (C=1 souvent = erreur) ou retour booléen (C=1 = vrai)
A:X Valeur de retour 16 bits (A=haut, X=bas) ou (X=haut, A=bas) selon la convention

Passage de paramètres par la pile

Pour les sous-routines avec plus de 3 paramètres, ou pour la compatibilité avec du code généré par un compilateur, on peut passer les paramètres par la pile :

  1. ; Appel : resultat = ma_fonction(arg1, arg2, arg3)
  2. LDA arg3
  3. PHA               ; empiler arg3
  4. LDA arg2
  5. PHA               ; empiler arg2
  6. LDA arg1
  7. PHA               ; empiler arg1
  8. JSR ma_fonction
  9. ; Nettoyer la pile apres l'appel :
  10. PLA               ; depiler arg1
  11. PLA               ; depiler arg2
  12. PLA               ; depiler arg3
  13. ; A contient le resultat

État de la pile dans le sous-programme :

Accès aux paramètres dans le sous-programme (via TSX) :

  1.   ma_fonction:
  2.     TSX               ; X := SP
  3.     LDA $0103,X       ; arg1 (SP+3)
  4.     ; ... utiliser les parametres ...
  5.     RTS

Attention : les déplacements sont calcules par rapport a $0100+SP, car la pile se trouve dans la page $01.

Passage de paramètres inline (apres le JSR)

Une technique élégante du 6502 : les paramètres sont placés immédiatement après l'instruction JSR dans le code. Le sous-programme lit les paramètres en modifiant l'adresse de retour sur la pile.

  1.     JSR PrintString
  2.     .BYTE "Hello, World!", $00  ; chaine inline
  3.  
  4.     ; L'execution continue ici apres le RTS
  5.  
  6.   PrintString:
  7.     ; L'adresse de retour sur la pile pointe sur
  8.     ; le dernier octet du JSR, donc (adresse retour) + 1
  9.     ; pointe sur le premier caractere de la chaine.
  10.     PLA               ; depiler PCL
  11.     STA ptr_lo
  12.     PLA               ; depiler PCH
  13.     STA ptr_hi
  14.     ; ptr pointe maintenant sur (adresse retour - 1)
  15.     ; Il faut incrementer de 1 pour pointer sur les donnees
  16.     INC ptr_lo
  17.     BNE NoCarry
  18.     INC ptr_hi
  19.   NoCarry:
  20.     LDY #$00
  21.   PrintLoop:
  22.     LDA (ptr_lo),Y
  23.     BEQ PrintDone     ; zero final
  24.     JSR OutputChar    ; afficher le caractere
  25.     INY
  26.     BNE PrintLoop
  27.   PrintDone:
  28.     ; Ajuster l'adresse de retour pour sauter les donnees
  29.     ; ptr + Y pointe sur le zero final
  30.     ; RTS attend (adresse - 1), donc on empile ptr + Y
  31.     CLC
  32.     TYA
  33.     ADC ptr_lo
  34.     TAX               ; X = (ptr + Y) bas
  35.     LDA #$00
  36.     ADC ptr_hi        ; A = (ptr + Y) haut
  37.     PHA               ; empiler PCH ajuste
  38.     TXA
  39.     PHA               ; empiler PCL ajuste
  40.     RTS               ; sauter apres les donnees inline

Sauvegarde et restauration des registres

Le 6502 n'a pas d'instruction MOVEM comme le 68000 ou PUSHA comme le 80386. La sauvegarde/restauration est manuelle :

  1.     ma_routine:
  2.       PHA              ; sauvegarder A
  3.       TXA
  4.       PHA              ; sauvegarder X
  5.       TYA
  6.       PHA              ; sauvegarder Y
  7.  
  8.       ; ... corps du sous-programme ...
  9.  
  10.       PLA
  11.       TAY              ; restaurer Y
  12.       PLA
  13.       TAX              ; restaurer X
  14.       PLA              ; restaurer A
  15.       RTS

Important : les registres doivent être dépilés dans l'ordre INVERSE de l'empilement (LIFO).

Coût de la sauvegarde/restauration complète :

PHA + TXA + PHA + TYA + PHA = 3+2+3+2+3 = 13 cycles
PLA + TAY + PLA + TAX + PLA = 4+2+4+2+4 = 16 cycles
Total = 29 cycles de surcharge

Pour les routines très courtes, il est souvent préférable de ne sauvegarder que les registres effectivement modifies.

Conventions d'appel par système

Chaque système a ses propres conventions :

Commodore 64 (KERNAL) :

Apple II (moniteur) :

NES :

Récursivité sur le 6502

La récursivité est possible mais limitée par la taille de la pile (256 octets). Chaque appel JSR consomme 2 octets (adresse de retour) plus les registres sauvegardes.

  1.     ; Factorielle(n) : n dans A, resultat dans A
  2.     ; Limitation : n <= ~40 (debordement pile et 8 bits)
  3.  
  4.   Factorielle:
  5.     CMP #$01
  6.     BCS FactRecurse   ; si A > 1, recurser
  7.     LDA #$01          ; factorielle(0) = factorielle(1) = 1
  8.     RTS
  9.   FactRecurse:
  10.     PHA               ; sauvegarder n sur la pile
  11.     SEC
  12.     SBC #$01          ; A := n - 1
  13.     JSR Factorielle   ; appel recursif
  14.     ; A contient factorielle(n-1)
  15.     STA temp
  16.     PLA               ; recuperer n
  17.     ; Multiplier A * temp (simplification pour petit n)
  18.     TAX
  19.     LDA #$00
  20.   FactMult:
  21.     CLC
  22.     ADC temp          ; A := A + factorielle(n-1)
  23.     DEX
  24.     BNE FactMult      ; repeter n fois
  25.     RTS

Profondeur de pile typique par appel :

En pratique, la récursivité profonde est déconseillé sur le 6502. Préférer les solutions itératives quand c'est possible.

Comparaison des conventions d'appel

Microprocesseur Sauvegarde retour Passage paramètres Sauvegarde registres
MOS 6502 Pile (2 oct) A, X, Y Manuel PHA/PLA
Zilog Z80 Pile (2 oct) Registres PUSH/POP paires
Intel 8080 Pile (2 octets) Registres PUSH/POP paires
Motorola 6800 Pile (2 octets) A, B PSHA/PSHB
Motorola 6809 Pile (2 octets) Registres PSHS/PULS multi
Motorola 68000 Pile (4 octets) Registres ou pile MOVEM.L
Intel 80386 Pile (4 octets) Pile (conv. C) PUSHA/POPA


Dernière mise à jour : Vendredi, le 27 mars 2026