Avant de commencer, il est judicieux de se munir de la documentation de l'atmega8 (pdf) et de la documentation du jeu d'instructions (pdf) ou encore de la version en ligne. On peut encore visiter la page de l'atmega8 avec l'ensemble des documentations.
Il est possible d'utiliser les outils de développement gnu assembleur et édition de lien et de simulation simulavr, ainsi que gtkwave pour visualiser les résultats de la simulation.La syntaxe complète du langage utilisée par l'assembleur peut être consultée en ligne ou bien téléchargée (pdf). Pour les outils gnu, il faut utiliser la syntaxe de l'assembleur GNU ou avec cette documentation plus complète en ligne ou en version pdf.
Ces outils sont disponibles sous la forme de paquetages dans linux que l'on trouve également en toolchain pour windows 10.
La mémoire flash qui contient le programme possède une zone pour les programmes d'applications et une zone de bootloader qui est un programme qui permet de charger le programme d'application dans la mémoire flash.
De plus, les premiers octets de la mémoire de programme sont réservés aux vecteurs d'interruptions dont le reset en $0000. Cette zone doit être initialisée, et le programme d'application doit commencer à la fin de cette zone.
La mémoire SRAM ne démarre pas en $0000, mais à l'adresse $0060, car les adresses de $0000 à $0031 sont occupés par les registres internes 0 à 31 et les adresses de $0032 à $005F sont occupées par les registres des périphériques (I/O).
Les registres internes et les registres des périphériques sont accessibles à l'aide du numéro de registre ou bien avec l'adresse mémoire.
Ces deux méthodes d'accès permettent d'utiliser des modes d'adressage différents pour accéder à ces registres.
On a une architecture RISC, ce qui fait que chaque mode d'adressage correspond à un ensemble d'instructions. On va donc présenter les principaux modes d'adressages avec des exemples d'instructions qui utilisent ce mode. De plus certains registres ont des utilisations particulières comme les registres R26 à R31 qui, associés par deux, sont utilisés comme registres d'adresses X=R27:R26 , Y=R29:R28 et Z=R31:R30, en utilisant le format little indian. Les registres R1 et R0 contiennent les résultats des instructions de multiplication sous la forme P=R1:R0.
Les bits, positionnés après les calculs, sont accessibles dans le registre SREG
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|
I | T | H | S | V | N | Z | C |
Notations utilisées :
Symbole | Signification | Symbole | Signification |
---|---|---|---|
Rr | Registre source | Rd | Registre destination |
Rh | Registre entre 16 et 31 | X | Registre d'adresse X=R27:R26 |
Y | Registre d'adresse Y=R29:R28 | Z | Registre d'adresse Z=R31:R30 |
K | Constante 8 bits | d | Déplacement relatif |
(K) | Adresse mémoire ou registre périphérique de valeur K | PC | Compteur de programme |
Adressage immédiat
C'est la modification d'un registre avec une valeur constante codée sur 8 bits.
Ce mode fonctionnent uniquement avec les registres R16 à R31.
Les bits ZNVC du registre d'état peuvent être modifiés après exécution de l'instruction
Adressage registre
C'est la modification d'un registre suite à un calcul.
Les bits ZNVC du registre d'état peuvent être modifiés après exécution de l'instruction
Adressage direct mémoire de donnée
C'est le transfert d'un registre depuis ou vers une donnée mémoire en spécifiant l'adresse K sur 16 bits.
Adressage direct registre périphérique
C'est le transfert d'un registre depuis ou vers un registre périphérique en spécifiant l'adresse K sur 6 bits.
Adressage indirect
C'est le transfert d'un registre depuis ou vers une donnée mémoire pour laquelle l'adresse est spécifiée dans un registre d'adresse X ou Y ou Z. Les registres X,Y ou Z restent inchangés.
Adressage indirect
C'est le transfert d'un registre depuis ou vers une donnée mémoire pour laquelle l'adresse est spécifiée dans un registre d'adresse X ou Y ou Z avec pré-décrémentation.
Adressage indirect
C'est le transfert d'un registre depuis ou vers une donnée mémoire pour laquelle l'adresse est spécifiée dans un registre d'adresse X ou Y ou Z avec post-incrémentation.
Adressage relatif
Le calcul de l'adresse se fait en ajoutant le déplacement signé pour calculer la nouvelle valeur de l'adresse de la mémoire de programme. Cela concerne les branchements conditionnels et inconditionnels.
Les instructions sont codées sur 16 bits, sauf en cas d'adressage direct, où dans ce cas l'instruction est suivie d'un autre mot de 16 bits qui contient l'adresse mémoire. Le code de l'instruction comprend des bits qui permettent d'identifier l'instruction et des bits qui permettent d'identifier l'opérande.
Exemples de codes en fonction des modes d'adressage
Adresse immédiat
Traitement | Instruction | Code (hexa) | b15...b12 | b11...b8 | b7...b4 | b3...b0 |
---|---|---|---|---|---|---|
Rh ← K | ldi Rh,K | $EKHK | 1110 | kkkk | hhhh | kkkk |
R20 ← $10 | ldi R20,$10 | $E | 1110 | 0001 | 0100 | 0000 |
Adresse Registre
Traitement | Instruction | Code (hexa) | b15...b12 | b11...b8 | b7...b4 | b3...b0 |
---|---|---|---|---|---|---|
Rd ← Rd + Rr | add Rd,Rr | $0XDR | 0000 | 11rd | dddd | rrrr |
R21 ← R21 + R20 | add R21,R20 | $0F54 | 0000 | 1111 | 0101 | 0100 |
Adresse direct (1 code instruction + adresse)
Traitement | Instruction | Code (hexa) | b15...b12 | b11...b8 | b7...b4 | b3...b0 |
---|---|---|---|---|---|---|
Rd ← (X) | lds Rd,(X) | $9XD0 | 1001 | 000d | dddd | 0000 |
$KKKK | kkkk | kkkk | kkkk | kkkk | ||
R19 ← R$0061 | lds R19,$0061 | $9130 | 1001 | 0001 | 0011 | 0000 |
$0061 | 0000 | 0000 | 0110 | 0001 |
Adresse indirect avec registre d'adresse inchangé
Traitement | Instruction | Code (hexa) | b15...b12 | b11...b8 | b7...b4 | b3...b0 |
---|---|---|---|---|---|---|
Rd ← (X) | ld Rd,(X) | $9XDC | 1001 | 000d | dddd | 1100 |
R19 ← (X) | ld R19,(X) | $913C | 1001 | 0001 | 0011 | 1100 |
Ces branchements sont réalisés après une instruction de comparaison qui positionne les bits ZNVC du registre de statut SREG comme cela a déjà été présenté dans la partie UAL du chapitre logique combinatoire.
Comparaison | Entiers non signés | Entiers signés |
---|---|---|
Rd = Rr | BREQ | BREQ |
Rd ≠ Rr | BRNE | BRNE |
Rd ≥ Rr | BRSH | BRGE |
Rd < Rr | BRLO | BRLT |
Un programme assembleur est une suite de ligne qui contiennent les instructions du processeur en respectant la syntaxe :
[etiquette:] instruction operandes ; commentaire
Afin de simplifier l'écriture des programmes, le langage assembleur comprend un ensemble de directives, dont voici quelques exemples :
[etiquette:] .nom parametres
; constantes pour l'atmega8
.include "../include/atmega8.inc"
; début de la mémoire programme
.text
; table des vecteurs d'interruption
rjmp entree ; vecteur reset en 0x0000
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
;; Début du programme d'application
entree:
clr R16 ; initialisation SREG à 0
out SREG,R16
ldi R16,LORAMEND ; initialisation adresse basse
out SPL,R16
ldi R16,HIRAMEND ; initialisation adresse basse
out SPH,R16
; initialisation de l'application
; boucle infinie de l'application
boucle:
; code de l'application
rjmp boucle
; sous-programme d'interruption qui ne fait rien
noirq:
reti
.end
Ce modèle est compatible avec les outils GNU, pour le simulateur avr_sim, le modèle est automatiquement donné lors de la création d'un nouveau projet.
Ici la représentation hexadécimale est préfixée de 0x au lieu du symbole $ qui n'est pas acceptée par tous les logiciels.
Les valeurs des registres sont des numéros de périphériques.
.text, obligatoire, indique le début de la mémoire de programme, l'assembleur utilise ce mot clé pour identifier l'adresse de début de la mémoire de programme.
.data joue le même rôle pour la mémoire RAM des données
Les lignes suivantes qui sont obligatoirement en début de la mémoire de programme, elles sont au nombre de 19 qui représentent les adresses des sous-programmes d'interruption. Si une interruption n'est pas utilisée, elle doit être branchée vers un sous-programme d'interruption qui ne fait rien (instruction reti à la fin du modèle. Seule la première ligne est utilisée pour se brancher vers le début du programme. Cette ligne doit être complétée avec l'étiquette qui correspond à l'adresse de la première instruction du programme.
L'étiquette entrée: représente l'adresse mémoire de début du programme qui se situe après la table des vecteurs d'interruptions.
Le programme est divisé en deux grandes parties :
Enfin le programme se termine par le mot clé .end
; registre d'état
.equ SREG,0x3F ; adresse du registre d'état
; bits du registre d'état
.equ IFLAG, 128
.equ TFLAG, 64
.equ HFLAG, 32
.equ SFLAG, 16
.equ VFLAG, 8
.equ NFLAG, 4
.equ ZFLAG, 2
.equ CFLAG, 1
; pointeur de pile
.equ SPH,0x3E ; adresse haute du pointeur de pile
.equ SPL,0x3D ; adresse basse du pointeur de pile
; adresse haute de la RAM pour la pile
.equ HIRAMEND,0x04 ; HAUT SRAM MSB
.equ LORAMEND,0x5F ; HAUT SRAM LSB
; adresse des ports
.equ PORTB, 0x18 ; PORTB
.equ DDRB, 0x17 ; DDRB
.equ PINB, 0x16 ; PINB
.equ PORTC, 0x15 ; PORTC
.equ DDRC, 0x14 ; DDRC
.equ PINC, 0x13 ; PIN
.equ PORTD, 0x12 ; PORTD
.equ DDRD, 0x11 ; DDRD
.equ PIND, 0x10 ; PIND
; timer 1
; registres des timers
.equ TCCR1A, 0x2F
.equ TCCR1B, 0x2E
.equ TCNT1H, 0x2D
.equ TCNT1L, 0x2C
.equ OCR1AH, 0x2B
.equ OCR1AL, 0x2A
.equ OCR1BH, 0x29
.equ OCR1BL, 0x28
.equ ICR1H, 0x27
.equ ICR1L, 0x26
; bits du registre de contrôle TCCR1A
.equ COM1A1, 128
.equ COM1A0, 64
.equ COM1B1, 32
.equ COM1B0, 16
.equ FOC1A, 8
.equ FOC1B, 4
.equ WGM11, 2
.equ WGM10, 1
; bits du registre de contrôle TCCR1B
.equ ICNC1, 128
.equ ICES1, 64
.equ CNU32, 32
.equ WGM13, 16
.equ WGM12, 8
.equ CS12, 4
.equ CS11, 2
.equ CS10, 1
; masque des interruptions
.equ TIMSK, 0x39
; bits controle d'interruption
.equ OCIE2, 128
.equ TOIE2, 64
.equ TICIE1, 32
.equ OCIE1A, 16
.equ OCIE1B, 8
.equ TOIE1, 4
.equ INU1, 2
.equ TOIE0, 1
; drapeaux des interruptions
.equ TIFR, 0x38
; bits masque
.equ OCF2, 128
.equ TOV2, 64
.equ ICF1, 32
.equ OCF1A, 16
.equ OCF1B, 8
.equ TOV1, 4
.equ IFNU1, 2
Les équivalences .equ permettent de définir les constantes pour le programme cela a deux avantages : en cas de modification, elles le sont dans tout le programme, il n'y a pas d'oubli et elles rendent le code source plus lisible.
Ces équivalences servent à définir des adresses de registres comme par exemple :
ou encore les valeurs des bits d'un registre
Pour positionner les bits I et C de SREG, il suffit d'écrire la valeur (CFLAG | IFLAG) dans le registre.
On va étudier un exemple d'addition de deux entiers codés sur 16 bits stockés en RAM, le résultat est également stocké en RAM. Comme cela déjà été vu avec la calculatrice et les systèmes combinatoires, l'addition est identique que les entiers soient signés ou non signés, ce sont les bits de statut qui permettent de définir le type des données. Les données sont stockés en mémoire au format little endian
Les étapes du programme sont donc :
; constantes pour l'atmega8
.include "../include/atmega8.inc"
; debut de la RAM
.data
motA: .word 0
motB: .word 0
som: .word 0
; début de la mémoire programme
.text
; table des vecteurs d'interruption
rjmp entree ; vecteur reset en 0x0000
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
;; Début du programme d'application
entree:
clr R16 ; initialisation SREG à 0
out SREG,R16
ldi R16,LORAMEND ; initialisation adresse basse
out SPL,R16
ldi R16,HIRAMEND ; initialisation adresse basse
out SPH,R16
; initialisation de l'application
ldi R16,0x97
sts motA,R16
ldi R16,0x04
sts motA+1,R16
ldi R16,0x38
sts motB,R16
ldi R16,0x25
sts motB+1,R16
; programme exemple sans boucle infinie
lds R20,motA
lds R21,motA+1
lds R22,motB
lds R23,motB+1
add R20,R22
adc R21,R23
sts som,R20
sts som+1,R21
; boucle infinie de l'application
boucle:
; code de l'application qui ne fait rien
rjmp boucle
; sous-programme d'interruption qui ne fait rien
noirq:
reti
.end
L'addition se trouve dans la partie initialisation car ce n'est pas nécessaire d'effectuer la même opération à l'infini. Donc, ici, la boucle infinie ne contient aucun code.
Ce programme n'est pas simplifié, il est écrit pour comprendre le codage assembleur. C'est pour cela que la partie initialisation, écrit les valeurs des entiers en mémoire RAM, nous avons un programme avec des variables initialisées.
Le processeur ne peut additionner que des nombres codées sur 8 bits, l'addition sur 16 bits est donc décomposée en deux additions de 8 bits successives sans oublier la retenue entre les deux. Cela mobilise donc deux registres par entier et pour le résultat.
La déclaration des variables en mémoire se fait avec le mot clé .word pour des entiers de 16 bits.
Les étapes du programme sont :
Le programme a été assemblé avec les outils gnu et simulé avec simulavr qui fournit un fichier vcd pour gtkwave. Les signaux utiles sont représentés ci-après.
On retrouve les étapes du programme
On a vu au chapitre sur le processeur que la méthode de multiplication était différente suivant que les opérandes sont signées ou non signées. Cette distinction se retrouve dans les instructions du processeur .
On propose de faire le produit de deux entiers non signés codés sur 8 bits stokés en RAM, le résultat, codé sur 16 bits, est stocké en RAM.
Les étapes du programme sont donc :
; constantes pour l'atmega8
.include "../include/atmega8.inc"
; debut de la RAM
.data
motA: .byte 0
motB: .byte 0
produit: .word 0
; début de la mémoire programme
.text
; table des vecteurs d'interruption
rjmp entree ; vecteur reset en 0x0000
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
;; Début du programme d'application
entree:
clr R16 ; initialisation SREG à 0
out SREG,R16
ldi R16,LORAMEND ; initialisation adresse basse
out SPL,R16
ldi R16,HIRAMEND ; initialisation adresse basse
out SPH,R16
; initialisation de l'application
ldi R16,57
sts motA,R16
ldi R16,125
sts motB,R16
; programme exemple sans boucle infinie
lds R20,motA
lds R21,motB
mul R20,R21
sts produit,R0
sts produit+1,R1
; boucle infinie de l'application
boucle:
; code de l'application qui ne fait rien
rjmp boucle
; sous-programme d'interruption qui ne fait rien
noirq:
reti
.end
Les deux entiers sont stockés en RAM. Le résultat est stocké en RAM au format little endian.
Les étapes du programme sont :
Si on traduit ces entiers non signés en base 10 on obtient : 0x39=57 et0x7D=125 qui donne le résultat 0x1BD5=7125. Le résultat est correct.
On propose de faire le produit de deux entiers non signés codés sur 8 bits, le résultat étant codé sur 16 bits.
Les étapes du programme sont donc :
; constantes pour l'atmega8
.include "../include/atmega8.inc"
; debut de la RAM
.data
motA: .byte 0
motB: .byte 0
produit: .word 0
; début de la mémoire programme
.text
; table des vecteurs d'interruption
rjmp entree ; vecteur reset en 0x0000
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
;; Début du programme d'application
entree:
clr R16 ; initialisation SREG à 0
out SREG,R16
ldi R16,LORAMEND ; initialisation adresse basse
out SPL,R16
ldi R16,HIRAMEND ; initialisation adresse basse
out SPH,R16
; initialisation de l'application
ldi R16,-45
sts motA,R16
ldi R16,-24
sts motB,R16
; programme exemple sans boucle infinie
lds R20,motA
lds R21,motB
muls R20,R21
sts produit,R0
sts produit+1,R1
; boucle infinie de l'application
boucle:
; code de l'application qui ne fait rien
rjmp boucle
; sous-programme d'interruption qui ne fait rien
noirq:
reti
.end
Les deux entiers sont stockés en RAM. Le résultat est stocké en RAM au format little endian.
Les étapes du programme sont :
Si on traduit ces entiers signés en base 10 on obtient : 0xD3=-45 et 0xE8=-24 qui donne le résultat 0x0438=1080. Le résultat est correct.
En programmation, lorsqu'un traitement est utilisé plusieurs fois, afin d'éviter de dupliquer le code, on fait une fonction qui est appelée chaque fois que ce traitement est nécessaire. Plus généralement, un programme n'est pas une suite d'instructions mais plutôt un ensemble de fonctions qui interagissent ensemble.
En assembleur une fonction, nommée sous-programme, est une suite d'instructions qui se termine par une instruction particulière ret qui indique la fin de cette fonction et le retour à l'instruction qui suit l'appel (rcall) à cette fonction.
Le programme intègre la fonction du calcul de la puissance entière d'un nombre en utilisant l'algorithme classique.
Fonction puissance
Paramètres en entrée : a ∈ ℕ et n ∈ ℕ
Paramètres en sortie : z=an
Debut traitement
z = 1
Pour i de 1 à n faire
z = a × z
FinPour
Retourner z
Fin traitement
Cet algorithme utilise la définition de la puissance présentée dans le chapitre divisibilité par la formule .
Cet algorithme se traduit par une boucle présentée dans le chapitre programmation de la calculatrice.
Toutes les valeurs sont des entiers non signés.
Les valeurs de a, n et z seront stockés en mémoire sous la forme d'entiers non signés codés sur 8 bits. Il ne faut donc pas oublier les limites des calculs imposés par ce format : an≤255, relation qui impose des limites facilement atteignables avec a=2 et n=7, a=3 et n=5, a=4 et n=3, a=5 et n=3, a=6 et n=3, a=7 et n=2, ... , a=15 et n=2.
; constantes pour l'atmega8
.include "../include/atmega8.inc"
; debut de la RAM
.data
motA: .byte 0
motn: .byte 0
motZ: .byte 0
; début de la mémoire programme
.text
; table des vecteurs d'interruption
rjmp entree ; vecteur reset en 0x0000
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
;; Début du programme d'application
entree:
clr R16 ; initialisation SREG à 0
out SREG,R16
ldi R16,LORAMEND ; initialisation adresse basse
out SPL,R16
ldi R16,HIRAMEND ; initialisation adresse basse
out SPH,R16
; initialisation de l'application
ldi R16,15 ; a=15
sts motA,R16
ldi R16,2 ; n=2
sts motn,R16
; programme exemple sans boucle infinie
lds R20,motA
lds R21,motn
rcall puissance
sts motZ,R22
; boucle infinie de l'application
boucle:
; code de l'application qui ne fait rien
rjmp boucle
; sous-programme de calcul de la puissance
; paramètre en entrée : a dans R20, n dans R21
; paramètre en sortie : z dans R22
; registres modifiés : R17,R0,R1 (non utilisé mais modifié par mul)
puissance:
ldi R22,1 ; z=1
ldi R17,1 ; i=1
psuite:
mul R22,R20 ; p=z*a
mov R22,R0 ; z=p
inc R17 ; i=i+1
cp R21,R17 ; n - i
brsh psuite ; continue si n >= i
ret
; sous-programme d'interruption qui ne fait rien
noirq:
reti
.end
Le programme principal reste le même, on s'intéresse au sous-programme de calcul de puissance.
Pour échanger les valeurs avec le sous-programme, on utilise les registres R20 (a) ,R21 (n) et R22 (z). Mais le programme a besoin de registres pour effectuer les calculs intermédiaires, que l'on précise en citant les registres modifiés qui sont R17,R0 et R1. R1 est précisé mais non utilisé car il est implicitement utilisé par l'instruction mul mais non utilisé car on fait un calcul sur des entiers non signés codés sur 8 bits.
Dans le sous-programme, on affecte les variables aux registres comme i dans R17.
Ce programme est un exemple d'utilisation de sous-programme, qui est la traduction de l'algorithme de la puissance et, qui permet d'illustrer le fonctionnement d'un sous-programme.
Les signaux en rouges sont des registres ou mémoires qui contiennent des adresses.
Ici in va s'intéresser à l'appel du sous-programme aux instants t1 à t6
Cet exemple montre bien le lien qu'il y a entre les sous-programmes et l'utilisation de la pile qui doit toujours être initialisée avec la dernière adresse de la mémoire RAM.
Dans le paragraphe précédent on a vu qu'il y a des paramètres transmis au sous-programme qui, lui-même, transmet le résultat, on dit qu'il retourne le résultat, ce résultat est la valeur de retour de la fonction. De plus le sous-programme utilise des registres, qui ne doivent pas être utilisés dans le reste du programme car cela pourrait provoquer des dysfonctionnement. Dans un programme plus complexe, il faut donc gérer ces registres afin d'éviter ce problème.
Nous allons voir, maintenant, la résolution de l'utilisation des registres par le sous-programme, en montrant comment sont sauvegardés ces registres avant leur utilisation dans le sous programme.
; sous-programme de calcul de la puissance
; paramètre en entrée : a dans R20, n dans R21
; paramètre en sortie : z dans R22
; registres modifiés : R17,R0,R1 (non utilisé mais modifié par mul)
; et sauvergardés dans la pile
puissance:
push R17 ; sauvegarde dans la pile de R17
push R0 ; sauvegarde dans la pile de R0
push R1 ; sauvegarde dans la pile de R1
ldi R22,1 ; z=1
ldi R17,1 ; i=1
psuite:
mul R22,R2 ; p=z*a
mov R22,R0 ; z=p
inc R17 ; i=i+1
cp R21,R17 ; n - i
brsh psuite ; continue si n >= i
pop R1 ; restitution de R1
pop R0 ; restitution de R0
pop R17 ; restitution de R17
ret
On ne représente que le sous programme modifié
On a simplement insérer la sauvegarde et la restitution des registres utilisés afin de ne pas interférer avec l'utilisation de ces registres dans le programme principal. Cette sauvegarde intervient en début de sous-programme et la restauration en fin de sous-programme juste avant l'instruction ret.
Il faut noter que l'ordre de restauration est inversé par rapport à l'ordre de sauvegarde. Cela vient de la structure de mémoire utilisée qui est la pile. En effet on empile (push) les données un peu comme on ferait avec une pile d'assiettes. Ce qui fait que lors de la restauration (dépilage) on accède en premier à la dernière donnée sauvegardée, comme on retirerait la dernière assiette empilée.
Cette structure de mémoire, nommée pile (stack en anglais), est bien connue sous le nom de "dernier entré, premier sorti" (LIFO pour Last In First Out en Anglais)
Ici on va s'intéresser à la sauvegarde et restauration des registres utilisés dans le sous programme
La pile est une zone mémoire importante dans un processeur. Elle est souvent implémentée dans la mémoire RAM et est très utilisée par les programmes. Ce qui signifie que la mémoire RAM ne peut être totalement réservée aux données du programme. Si on utilise trop de RAM pour les données ou si on sollicite trop la pile, cela risque de provoquer un recouvrement entre les données et le contenu de la pile et, donc, provoquer un dysfonctionnement du programme, voir un arrêt complet (plantage) si le sous-programme ne retrouve plus l'adresse de retour.
Comme cela a déjà été présenté ce circuit possède 3 ports parallèles bidirectionnels: B, C et D.
Port B :
Port C :
Port D :
Dans la plupart des montages, les broches PB6, PB7 et PC6 ne sont pas accessibles.
L'utilisation des autres broches dépend de l'application.
On propose de faire un programme qui fait clignoter une LED connectée sur la broche PB0. on va donc configurer la broche PB0 en sortie et, alternativement lui donner les valeurs 0 et 1. La période de clignotement sera réalisé par un sous-programme délai de faible valeur pour être visible en simulation.
; constantes pour l'atmega8
.include "../include/atmega8.inc"
.text
; table des vecteurs d'interruption
rjmp entree ; vecteur reset en 0x0000
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
;; Début du programme d'application
entree:
clr R16 ; initialisation SREG à 0
out SREG,R16
ldi R16,LORAMEND ; initialisation adresse basse
out SPL,R16
ldi R16,HIRAMEND ; initialisation adresse basse
out SPH,R16
; inialisation peripherique port B et broche 0 en sortie
ldi R16,1
out DDRB,R16
ldi R17,0
; initialisation registre de calcul
ldi R18,1 ; permutation bit 0
; boucle infinie de l'application
loop:
out PORTB,R17
eor R17,R18 ; permute bit 0
rcall delai ; appelle sous-programme delai
rjmp loop
; sous-programme delai
delai:
ldi R16,10 ; 255 x cycles dec + brne = 255 x (1+2)
attente:
dec R16
brne attente ; sortie de boucle si R16=0
ret
; sous-programme d'interruption qui ne fait rien
noirq:
reti
.end
Le port B comme les autres est bidirectionnel, le choix de la direction de chaque broche du port est programmable par l'intermédiaire d'un registre de direction nommé DDRB. Les broches du port B sont accessibles par l'intermédiaire du registre PORTB. Ces registres se situent dans l'espace des entrées sorties :
Dans le programme, on écrit 1 dans DDRB ce qui définit la broche PB0 en sortie et les autres broches en entrée.
La Led est connectée entre la broche PB0 et la masse, un 1 dans le registre PORTB allume la LED, un 0 dans le registre éteint la LED
Pour faire clignoter la LED, il faut écrire alternativement 0 puis 1 dans le PORTB. Cela se fait par l'intermédiaire du registre R18. Le basculement du bit se fait avec un OU exclusif. A chaque opération OU exclusif le bit change d'état et, en écrivant ce registre dans PORTB, on fait donc clignoter la LED.
La demi-période est définie par le sous-programme délai. Ce sous-programme est une boucle qui décrémente le contenu d'un registre, le temps total de cette boucle dépend de la valeur de départ. Pour déterminer la demi-période, il faut prendre en compte le temps du sous-programme plus le temps des instructions du programme principal.
Au démarrage, avant l'initialisation de la direction du port B, la sortie B0_OUT est en haute impédance, cela est représentée par un niveau de signal compris entre 0 et 1.
La demi-période du signal de sortie est décomposée en différents segments qui dépendent du nombre de cycles d'horloge de chaque instruction
Ce qui fait un total de 41 cycles de 62.5ns soit une demi-période de 41*62.5ns=2,56µs. La valeur mesurée avec gtkwave est de 2,54µs. L'estimation est donc correcte.
On remarque que la demi-période est totalement dépendante du code du programme, ce qui ne peut être entièrement satisfaisant. On va donc choisir une solution indépendante du programme en faisant appel à un timer.
On ne peut pas faire de programme de processeur sans faire le classique chenillard qui consiste à allumer une ou plusieurs LEDs parmi un ensemble de 8 LEDS en général et à faire une rotation de ces LEDs allumées.
; constantes pour l'atmega8
.include "../include/atmega8.inc"
; debut de la RAM
;.data
; début de la mémoire programme
.text
; table des vecteurs d'interruption
rjmp entree ; vecteur reset en 0x0000
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
;; Début du programme d'application
entree:
clr R16 ; initialisation SREG à 0
out SREG,R16
ldi R16,LORAMEND ; initialisation adresse basse
out SPL,R16
ldi R16,HIRAMEND ; initialisation adresse basse
out SPH,R16
; initialisation de l'application
; port D en sortie pour le chenillard 8 leds
ldi R16,0xff
out DDRD,R16
; initialisation chenillard
ldi R17,0
ldi R16,CFLAG ; mise à 1 du bit C pour la rotation
out SREG,R16
; boucle infinie de l'application
boucle:
rol R17
BRCC suiterol ; C=1 on effectue une rotation supplémentaire
rol R17
suiterol:
out PORTD,R17
rcall delai
rjmp boucle
; sous-programme de calcul de delai
delai:
ldi R16,2 ; 2 x cycles dec + brne = 2 x (1+2)
attente:
dec R16
brne attente ; sortie de boucle si R16=0
ret
; sous-programme d'interruption qui ne fait rien
noirq:
reti
.end
On utilise la port D car les bits 6 et 7 du port B ne sont pas utilisables. On initialise donc le port D en sortie, et à 0x00. Pour la rotation à gauche on utilise l'instruction rol sans oublier que la retenue C est incluse dans la rotation.
C'est pour cela que l'on initialise la retenue à 1 et le port D à 0x00.
La boucle infinie commence par la rotation, ce qui a pour effet de mettre 1 dans le registre et donc dans le port D et de positionner la retenue C à 0. Cela a pour résultat d'allumer la led 0.
Ensuite on teste la retenue, car lorsque le bit 7 sera transféré dans la retenue, le registre vaudra 0x00, la retenue 1. On ne doit pas écrire cette valeur dans le port D, car cela éteindrait toutes les LEDs. Pour que l'effet de rotation soit complet, on doit passer à 1 avant de transférer la valeur du registre dans le port D en ajoutant une nouvelle rotation. C'est le rôle des instructions brcc et rol.
Comme pour le clignotement, le délai est très court pour pouvoir observer les signaux en simulation.
A chaque boucle on a bien un décalage du registre R17 puis transfert dans le port D. La rotation est validée par les signaux de chaque bit du port D, mais également par les valeurs successives de R17 et de PORT. On vérifie que le décalage à gauche est une multiplication par 2 de la valeur précédente.
On a deux cas de parcours de programme :
Donc quelque soit le parcours on totalise 3 cycles d'horloge, ce qui fait que temps de déroulement du programme reste constant.
Le timer est un circuit de comptage indépendant du fonctionnement du processeur, ce périphérique est donc une base de temps ou une système de comptage qui libère le processeur de cette tâche.
Chaque microcontrôleur possède un ou plusieurs timers qui possèdent un ensemble de modes de fonctionnement paramétrables. Sans en faire la liste complète, on peut résumer ces modes de fonctionnement
Un compteur sur N bits qui évolue de 0 à la valeur maximale, puis revient à 0. En paramétrant la valeur maximale dans un registre, on règle la fréquence du signal carré en sortie.
L'horloge de ce compteur est liée à l'horloge du processeur soit directement, soit par l'intermédiaire d'un diviseur afin d'obtenir des fréquences plus faibles.
Le registre permet de choisir, pour chaque fréquence d'horloge, une période comprise entre T=TH (TH période en sortie du diviseur) et T=65536×TH.
Dans ce cas la valeur maximale est fixe, le compteur est comparé en permanence avec le contenu du registre. Lorsque les deux valeurs sont égales, la sortie change d'état.
La fréquence du signal de sortie est constante, c'est le rapport cyclique du signal de sortie qui dépend de la valeur du registre.
Le signal de sortie est un signal PWM (Pulse-Width Modulation) pour lequel la valeur moyenne dépend du rapport de la valeur du registre divisé par la valeur maximale :
L'ensemble des modes de fonctionnement sont des variantes de ces deux modes, ou encore une combinaison de ces deux modes.
On réalise un programme qui fournit un signal PWM sur la broche PB1 de l'atmega8 en utilisant le mode Fast PWM avec une valeur maximale de 0xFF.
; constantes pour l'atmega8
.include "../include/atmega8.inc"
; configuration timer1
; mode fast pwm avec un compteur sur 8 bits
.equ CONTROLA, (COM1A1 | COM1A0 | WGM10)
.equ CONTROLB, (WGM12)
.equ CONTROLBS, (WGM12 | CS10)
; debut de la RAM
;.data
; début de la mémoire programme
.text
; table des vecteurs d'interruption
rjmp entree ; vecteur reset en 0x0000
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
;; Début du programme d'application
entree:
clr R16 ; initialisation SREG à 0
out SREG,R16
ldi R16,LORAMEND ; initialisation adresse basse
out SPL,R16
ldi R16,HIRAMEND ; initialisation adresse basse
out SPH,R16
; initialisation de l'application
; configure timer1A
ldi R16,CONTROLA
out TCCR1A,R16
ldi R16,CONTROLB
out TCCR1B,R16
ldi R16,0
out OCR1AH,R16 ; poids fort
ldi R16,60
out OCR1AL,R16 ; poids faible
; portB pour le timer
ldi R16,2 ; bit 1 du port B en sortie
out DDRB,R16
; activation timer
ldi R16,CONTROLBS ; démarrage du timer
out TCCR1B,R16
; boucle infinie de l'application
boucle:
rjmp boucle
; sous-programme d'interruption qui ne fait rien
noirq:
reti
.end
Le timer est configurable avec les registres TCCR1A
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|
COM1A1 | COM1A0 | COM1B1 | COM1B0 | FOC1A | FOC1B | WGM11 | WGM10 |
et TCCR1B
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|
ICNC1 | ICES1 | --- | WGM13 | WGM12 | CS12 | CS11 | CS10 |
Pour obtenir le mode fast PWM, il faut COM1A1 et COM1A0 à 1, WGM10 à 1 et WGM12 à 1, les autres bits étant laissés à 0.
Ce qui donne les valeurs d'initialisation sous la forme de OU entre les constantes prédéfinies :
Les registres OCR1AH et OCR1AL sont chargés avec la valeur de la commutation qui permet de déterminer le rapport cyclique du signal. Le compteur étant utilisé sur 8 bits, seul OCR1AL est chargé avec la valeur 60 qui permet de définir le rapport cyclique de (256-60)/256 ≈ 0,76.
Il faut également initialiser le bit 1 du port B en sortie pour que le signal soit disponible sur la broche PB1.
On termine par l'activation du timer en positionnant le bit CS10 du registre TCCR1B à 1 en conservant les valeurs des autres bits.
On mesure le rapport cyclique du signal obtenu sur la broche PB1 (nommée B1-OUT par le simulateur) avec gtkwave, on obtient les temps suivants :
Ce qui donne un rapport cyclique de (15807-3717)/15807 ≈ 0.76, ce qui correspond à la valeur estimée à partir des valeurs des registres.
On utilise le timer pour générer un interruption périodique.
Un interruption est un évènement matériel lié à un périphérique que ce soit le port parallèle, série, ou encore le timer. Cet évènement matériel va provoquer l'arrêt du programme en cours pour exécuter un programme particulier nommé programme d'interruption.
Au moment où le signal d'interruption est reconnu par le processeur, celui-ci se branche à une adresse mémoire située en début de la mémoire de programme et qui correspond à l'interruption demandée.
A cette adresse mémoire, on trouve une instruction rjmp qui effectue un branchement au début du programme d'interruption. Ce programme se termine toujours par une instruction reti qui permet de reprendre le programme en cours.
Ce programme de chenillard n'utilise plus de sous-programme délai, car le code de rotation est directement inclus dans le programme d'interruption.
; constantes pour l'atmega8
.include "../include/atmega8.inc"
; configuration timer1
.equ CONTROLA, 0
.equ CONTROLB, (WGM12)
.equ CONTROLBS, (WGM12 | CS10)
; debut de la RAM
;.data
; début de la mémoire programme
.text
; table des vecteurs d'interruption
rjmp entree ; vecteur reset en 0x0000
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp timcp1A ; TIMER 1 COMPARE A
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
rjmp noirq ; pas d'interruption
;; Début du programme d'application
entree:
clr R16 ; initialisation SREG à 0
out SREG,R16
ldi R16,LORAMEND ; initialisation adresse basse
out SPL,R16
ldi R16,HIRAMEND ; initialisation adresse basse
out SPH,R16
; initialisation de l'application
; port D en sortie pour le chenillard 8 leds
ldi R16,0xff
out DDRD,R16
; configure timer1A
ldi R16,CONTROLA
out TCCR1A,R16
ldi R16,CONTROLB
out TCCR1B,R16
ldi R16,0
out OCR1AH,R16
ldi R16,5
out OCR1AL,R16
; activation interruption
ldi R16,OCIE1A
out TIMSK,R16
; initialisation chenillard
ldi R17,0
ldi R16,(CFLAG | IFLAG) ; mise à 1 du bit C pour la rotation et interruptions
out SREG,R16
ldi R16,CONTROLBS
out TCCR1B,R16
; boucle infinie de l'application
boucle:
rjmp boucle
; sous-programme d'interruption
timcp1A:
push R16
rol R17
BRCC suiterol ; C=1 on effectue une rotation supplémentaire
rol R17
suiterol:
out PORTD,R17
ldi R16,OCF1A
out TIFR,R16
pop R16
reti
; sous-programme d'interruption qui ne fait rien
noirq:
ret
Le timer est configurable avec les registres TCCR1A
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|
COM1A1 | COM1A0 | COM1B1 | COM1B0 | FOC1A | FOC1B | WGM11 | WGM10 |
TCCR1B
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|
ICNC1 | ICES1 | --- | WGM13 | WGM12 | CS12 | CS11 | CS10 |
TIMSK
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|
OCIE2 | TOIE2 | TICIE1 | OCIE1A | OCIE1B | TOIE1 | --- | TOIE0 |
et TIFR
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|
OCF2 | TOV2 | ICF1 | OCF1A | OCF1B | TOV1 | --- | TOV0 |
Pour obtenir le mode de fonctionnement en fréquence nommé CTC (Compare Match Mode), il faut que le bit WGM12 soit à 1, les autres bits étant laissés à 0.
Ce qui donne les valeurs d'initialisation sous la forme de OU entre les constantes prédéfinies :
Les registres OCR1AH et OCR1AL sont chargés avec la valeur de la commutation qui permet de déterminer la demi-période du signal, On choisit une valeur faible pour la simulation en veillant a ce que cette valeur fasse que la demi-période reste supérieure au temps d'exécution du programme d'interruption.
Il ne faut pas oublier l'initialisation du port D en sortie pour le chenillard.
Il faut ajouter la gestion des interruptions par le timer et le processeur :
On termine par l'activation du timer en positionnant le bit CS10 du registre TCCR1B à 1 en conservant les valeurs des autres bits.
On va maintenant analyser le déroulement de cette interruption
La programmation en assembleur n'est plus utilisée, mais elle permet de comprendre les points clés du fonctionnement du processeur qui sont les possibilités de calculs, la gestion de la mémoire ainsi que la gestion du temps. Ces points sont importants et, même si on ne programme plus en assembleur il est essentiel de les connaître :