Systèmes électroniques
Fermer ×

Programmation assembleur

La documentation et les outils

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.

Architecture logicielle de l'atmega8

Organnisation mémoire

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.

Les modes d'adressage

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
  • I : masque global d’interruption
  • T : utilisé par les instructions BLD et BST
  • H : demi retenue dans un calcul
  • S : signe S = N ⊕ V
  • V : dépassement dans un calcul
  • N : résultat négatif d’un calcul
  • Z : indicateur de 0, Z=1 si résultat nul
  • C : retenue dans un calcul

Notations utilisées :

SymboleSignificationSymboleSignification
RrRegistre sourceRdRegistre destination
RhRegistre entre 16 et 31XRegistre d'adresse X=R27:R26
YRegistre d'adresse Y=R29:R28ZRegistre d'adresse Z=R31:R30
KConstante 8 bitsdDéplacement relatif
(K)Adresse mémoire ou registre périphérique de valeur KPCCompteur 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

  • LDI : Rh ← K
  • SUBI : Rh ← Rh - K
  • SBCI : Rh ← Rh - K - C
  • ANDI : Rh ← Rh and K
  • ORI : Rh ← Rh or K
  • CPI : modification des bits SZNVC ← Rh - K

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

  • ADD : Rd ← Rd + Rr
  • ADC : Rd ← Rd + Rr + C
  • SUB : Rd ← Rd - Rr
  • SBC : Rd ← Rd - Rr - C
  • DEC : Rd ← Rd -1
  • INC : Rd ← Rd + 1
  • LSL : décalage logique à gauche de Rd
  • LSR : décalage logique à droite de Rd
  • ASR : décalage arithmétique à droite de Rd
  • MOV : Rd ← Rr
  • CLR : Rd ← 0
  • CP : modification des bits SZNVC ← Rd - Rr

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.

  • LDS : Rd ← (K)
  • STS : (K) ← Rr

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.

  • IN : Rd ← (K)
  • OUT : (K) ← Rr

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.

  • LD : Rd ← (X)
  • LD : Rd ← (Y)
  • LD : Rd ← (Z)
  • ST : (X) ← Rr
  • ST : (Y) ← Rr
  • ST : (Z) ← Rr

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.

  • LD : Rd ← (-X)
  • LD : Rd ← (-Y)
  • LD : Rd ← (-Z)
  • ST : (-X) ← Rr
  • ST : (-Y) ← Rr
  • ST : (-Z) ← Rr

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.

  • LD : Rd ← (X+)
  • LD : Rd ← (Y+)
  • LD : Rd ← (Z+)
  • ST : (X+) ← Rr
  • ST : (Y+) ← Rr
  • ST : (+Z) ← Rr

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.

  • RJMP : PC ← PC + d
  • RCALL : PC ← PC + d pour un sous-programme
  • BRNE : PC ← PC + d si bit Z = 0
  • BREQ : PC ← PC + d si bit Z = 1

Le codage des instructions

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

Les branchements conditionnels

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.

On obtient le tableau des branchements en fonction des comparaisons de nombre signés ou non signés avec l'instruction CP
ComparaisonEntiers non signésEntiers signés
Rd = RrBREQBREQ
Rd ≠ RrBRNEBRNE
Rd ≥ RrBRSHBRGE
Rd < RrBRLOBRLT

La syntaxe assembleur

Présentation

Un programme assembleur est une suite de ligne qui contiennent les instructions du processeur en respectant la syntaxe :

[etiquette:] instruction operandes ; commentaire
  • etiquette: n'est pas obligatoire, mais seulement présente pour les sauts et branchements, si etiquette n'est pas présent, il faut au moins un espace avant l'instruction.
  • instruction est le mnémonique de l'instruction comme, par exemple, ldi, add, ...
  • operandes : représente l'opérande ou les deux opérandes séparées par un virgule comme, par exemple R16,10
  • Ce qui suit le ; est du commentaire qu'il est très conseillé d'écrire

Afin de simplifier l'écriture des programmes, le langage assembleur comprend un ensemble de directives, dont voici quelques exemples :

[etiquette:] .nom parametres
  • etiquette: n'est pas obligatoire, seulement dans les cas ou on a besoin de l'adresse mémoire dans le programme
  • nom peut être :
    • data sans paramètres et sans etiquette pour les données en RAM
    • text sans parametres et sans etiquette pour le programme
    • equ suivi du nom et de la valeur séparés par une virgule, définit une constante.
    • byte suivi de la liste des valeurs séparées par une virgule
    • word suivi de la liste des valeurs séparées par une virgule
    • int suivi de la liste des valeurs séparées par une virgule
Voir le modèle pour les nouveaux programmes

; 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 :

  • La partie initialisation qui inclut l'initialisation du système et ensuite l'initialisation des paramètres de l'application. Elle est composée de deux sous-parties :
    • La partie initialisation système, qui, au minimum, positionne le registre d'état à 0 et qui initialise le pointeur de pile avec l'adresse haute de la RAM de données.
    • La partie initialisation de l'application pour, par exemple, initialiser les périphériques utilisés.
  • Une boucle infinie qui contient le programme de l'application, elle est obligatoire car un processeur fonctionne en permanence

Enfin le programme se termine par le mot clé .end

Voir le fichier des constantes de l'atmega8

; 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 :

  • SREG : adresse du registre d'état
  • SPL : adtresse basse du pointeur de pile
  • SPH : adresse haute du pointeur de pile
  • ...

ou encore les valeurs des bits d'un registre

  • CFLAG : de valeur 1 qui définit le bit 0
  • IFLAG : de valeur 128 qui définit le bit 7

Pour positionner les bits I et C de SREG, il suffit d'écrire la valeur (CFLAG | IFLAG) dans le registre.

Addition

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 :

  1. Copier les deux entiers dans des registres à partir de la mémoire
  2. Additionner les deux entiers
  3. Copier les registres qui contiennent le résultat en mémoire
Voir le code assembleur de l'addition

; 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 :

  1. Initialisation système
    1. Initialisation SREG à 0
    2. Initialisation pointeur de pile avec l'adresse haute de la RAM 0x045F
  2. Initialisation des paramètres de l'application
    1. Initialisation de la première variable avec la valeur 0x0497
    2. Initialisation de la deuxième variable avec la valeur 0x2538
  3. Calcul de l'addition
    1. Chargement du premier entier dans deux registres
    2. Chargement du deuxième entier dans deux autres registres
    3. Addition des octets de poids faibles : 0x97 et 0x38
    4. Addition des octets de poids fort 0x04 et 0x25 avec la retenue du calcul précédent
    5. Stockage du résultat 0x29CF en mémoire
Voir le résultat de la simulation

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

  1. Initialisation système
    1. Initialisation SREG à 0
    2. Initialisation pointeur de pile avec l'adresse haute de la RAM 0x045F
  2. Initialisation des paramètres de l'application
    1. Initialisation de la première variable avec la valeur 0x0497
    2. Initialisation de la deuxième variable avec la valeur 0x2538
  3. Calcul de l'addition
    1. Chargement du premier entier dans deux registres
    2. Chargement du deuxième entier dans deux autres registres
    3. Addition des octets de poids faibles : 0x97 et 0x38
    4. Addition des octets de poids fort 0c04 et 0x25 avec la retenue du calcul précédent
    5. Stockage du résultat 0x29CF en mémoire du résultat
      A la fin du calcul intermédiaire, on remarque la valeur 14 de SREG qui signifie que N=1 qui donne S=1. On a additionné deux nombres positifs qui donne un nombre négatif. A la fin du calcul complet SREG=0 indique qu'il n'y a pas d 'erreur que les entiers soient signés ou non.
      si on traduit ces valeurs en entiers non signés, on obtient l'opération :
      1175 + 9528 = 10703
      Ce qui est correct pour l'addition de deux entiers non signés codés sur 16 bits
      si on traduit ces valeurs en entiers signés, on obtient l'opération :
      1175 + 9528 = 10703
      Ce qui est également correct pour l'addition de deux entiers non signés codés sur 16 bits

Multiplication

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 .

Multiplication entière non signée

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 :

  1. Copier les deux entiers dans des registres à partir de la mémoire
  2. Multiplier les deux entiers
  3. Copier les registres qui contiennent les résultats en mémoire
Voir le code assembleur de la multiplication non signée

; 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 :

  1. Initialisation système
    1. Initialisation SREG à 0
    2. Initialisation pointeur de pile avec l'adresse haute de la RAM 0x045F
  2. Initialisation des paramètres de l'application
    1. Initialisation de la première variable avec la valeur 57=0x39
    2. Initialisation de la deuxième variable avec la valeur 125=0x7D
  3. Calcul de la multiplication
    1. Chargement du premier entier dans 1 registre
    2. Chargement du deuxième entier dans 1 autre registre
    3. Multiplication des entiers : 57×125
    4. Stockage du résultat 7125=0x1BD5 en mémoire au format little endian : 0xD5 et 0x1B.
Voir le résultat de la simulation
  1. Initialisation système
    1. Initialisation SREG à 0
    2. Initialisation pointeur de pile avec l'adresse haute de la RAM 0x045F
  2. Initialisation des paramètres de l'application
    1. Initialisation de la première variable avec la valeur 0x39
    2. Initialisation de la deuxième variable avec la valeur 0x7D
  3. Calcul de l'addition
    1. Chargement du premier entier dans deux registres
    2. Multiplication des octets : 0x39 × 0x7D
    3. Stockage du résultat 0xD5 en mémoire
    4. Stockage du résultat 0x1B en mémoire

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.

Multiplication entière signée

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 :

  1. Copier les deux entiers dans des registres à partir de la mémoire
  2. Multiplier les deux entiers
  3. Copier les registres qui contiennent les résultats en mémoire
Voir le code assembleur de la multiplication signée

; 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 :

  1. Initialisation système
    1. Initialisation SREG à 0
    2. Initialisation pointeur de pile avec l'adresse haute de la RAM 0x045F
  2. Initialisation des paramètres de l'application
    1. Initialisation de la première variable avec la valeur -45=0xD3
    2. Initialisation de la deuxième variable avec la valeur -24=0xE8
  3. Calcul de la multiplication
    1. Chargement du premier entier dans 1 registre
    2. Chargement du deuxième entier dans 1 autre registre
    3. Multiplication des entiers : -45 × -24
    4. Stockage du résultat 1080=0x0438 en mémoire au format little endian : 0x38 et 0x04.
Voir le résultat de la simulation
  1. Initialisation système
    1. Initialisation SREG à 0
    2. Initialisation pointeur de pile avec l'adresse haute de la RAM 0x045F
  2. Initialisation des paramètres de l'application
    1. Initialisation de la première variable avec la valeur 0xD3
    2. Initialisation de la deuxième variable avec la valeur 0xE8
  3. Calcul de l'addition
    1. Chargement du premier entier dans deux registres
    2. Multiplication des octets de poids : 0xD3 × 0xE8
    3. Stockage du résultat 0x38 en mémoire
    4. Stockage du résultat 0x04 en mémoire

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.

Les fonctions et la pile

Fonctionnement

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.

Exemple :

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 a n = a × a × a × a × × a n f o i s .

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.

Voir le code assembleur de la fonction puissance

; 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.

  • z=1 se traduit par R22 ← 1
  • i=1 se traduit par R17 ← 1
  • a=z*a ne peut pas être réalisé en une seule instruction, on calcule le produit p ← z×a qui se traduit par mul R22,R20, puis on transfert le résultat z ← p qui se traduit par mov R22,R0
  • i=i+1 se traduit pas inc R17
  • on reste dans la boucle si i ≤ n se traduit également par deux instructions cp R21,R17 qui compare i et n et le branchement si n ≥ i
  • Lorsque la condition n'est plus respectée, on passe à l'instruction rte qui retourne au programme principal à l'adresse de l'instruction qui suit l'appel au sous-programme rcall

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.

Voir le résultat de la simulation

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

  1. avant t1, on a le déroulement du programme comme dans les exemples précédents.
  2. t1 : on charge depuis la RAM (IRAM0) la valeur de a=15 au premier paramètre (R20)
  3. t2 : on charge depuis la RAM (IRAM1) la valeur de n=2 au deuxième paramètre (R21)
  4. t3 : on exécute l'appel au sous-programme (rcall), cela consiste à sauvegarder l'adresse de l'instruction suivante (0x0024) dans la pile, ce qui a pour effet de décrémenter l'adresse du pointeur de pile de 2 octets. On retrouve également cette valeur d'adresse dans le haut de la pile en RAM (IRAM1022 et IRAM1023).
  5. t4 : le compteur de programme est chargé avec l'adresse de début de sous-programme (0x0027) et on exécute les instructions du sous-programme. La comparaison positionne le bit C, ce qui a pour effet de continuer la boucle tant que ce bit est à 0. Lorsque ce bit C passe à 1 on sort de la boucle.
  6. t5 : on exécute l'instruction ret de fin de sous-programme, ce qui a pour effet de recharger le compteur de programme avec l'adresse qui était stocké dans la pile soit l'adresse de l'instruction qui suit l'instruction rcall.
  7. t6 : on exécute l'instruction du programme qui est la sauvegarde du résultat du calcul contenu dans R22 dans la mémoire RAM (IRAM2).

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.

Paramètres en entrées, valeur de retour et variables locales

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.

Voir le code assembleur du sous-programme modifié

; 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)

Voir la simulation du sous-programme modifié

Ici on va s'intéresser à la sauvegarde et restauration des registres utilisés dans le sous programme

  • t1 : on exécute l'appel au sous-programme (rcall), cela consiste à sauvegarder l'adresse l'instruction suivante (0x0024) dans la pile, ce qui a pour effet de décrémenter l'adresse du pointeur de pile de 2 octets. On retrouve également cette valeur d'adresse dans le haut de la pile en RAM (IRAM1022 et IRAM1023).
  • t2 : sauvegarde du premier registre dans la pile, le pointeur de pile est décrémenté de 1, la valeur du registre apparaît dans la RAM (IRAM1021)
  • t3 : sauvegarde du deuxième registre dans la pile le pointeur de pile est décrémenté de 1, la valeur du registre apparaît dans la RAM (IRAM1020)
  • t4 : sauvegarde du troisième registre dans la pile, le pointeur de pile est décrémenté de 1, la valeur du registre apparaît dans la RAM (IRAM1019)
  • t5 : restauration du troisième registre depuis la pile, le pointeur de pile est incrémenté de 1
  • t6 : restauration du deuxième registre depuis la pile, le pointeur de pile est incrémenté de 1
  • t7 : restauration du premier registre depuis la pile, le pointeur de pile est incrémenté de 1
  • t8 : on exécute l'instruction ret de fin de sous-programme, ce qui a pour effet de recharger le compteur de programme avec l'adresse qui était stocké dans la pile soit l'adresse de l'instruction qui suit l'instruction rcall.

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.

Les interfaces

Les interfaces disponibles et les broches multitifonctions

Comme cela a déjà été présenté ce circuit possède 3 ports parallèles bidirectionnels: B, C et D.

Port B :

  • PB7 (bit 7) et PB6 (bit 6) ne sont généralement pas accessibles car elles sont utilisées par le quartz.
  • PB2 (bit 2) à PB5 (bit 5) ne sont pas accessibles si l'interface SPI est utilisée.
  • PB0 (bit 0) à PB2 (bit 2) ne sont pas accessibles si l'interface timer 1 (compteur) est utilisée.
  • PB3 (bit 3) n'est pas accessible si le timer 2 est utilisé.

Port C :

  • PC6 (bit 6) n'est généralement pas accessible car c'est l'entrée RESET externe
  • PC0 (bit 0) à PC5 (bit 5) ne sont pas accessibles si les entrées analogiques correspondantes sont utilisées.
  • PC4 (bit 0) et PC5 (bit 5) ne sont pas accessibles si l'interface I2C est utilisée.

Port D :

  • PD0 (bit 0) et PD1 (bit 1) ne sont généralement pas accessibles mais utilisées par l'interface série asynchrone.
  • PD2 (bit 2) et PD3 (bit 3) ne sont pas accessibles si on les utilise comme entrée d'interruption matérielle.
  • PD4 (bit 4) et PD5 (bit 5) ne sont pas accessibles si on utilise les entrées d'horloges des timers 0 et 1 sont externes.
  • PD6 (bit 6) et PD7 (bit 7) ne sont pas accessibles si on utilise les entrées de comparaisons analogiques 0 et 1.

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.

L'interface parallèle

La diode qui clignote

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.

Voir le programme de clignotement

; 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 :

  • DDRB à l'adresse 0x17, un 1 dans un bit de ce registre programme la broche correspondante à ce bit en sortie, un 0 en entrée
  • PORTB à l'adresse 0x18 :
    • En entrée, lorsqu'une broche de ce port est au +Vcc, cela donne 1 dans le bit correspondant du registre
    • En sortie, l'écriture d'un 1 dans un bit de ce registre fait que la sortie est connectée au +Vcc

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.

Voir la simulation du clignotement

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

  • t2-t1 : instruction out, 1 cycle d'horloge
  • t3-t2 : instruction eor, 1 cycle d'horloge
  • t4-t3 : instruction rcall, 3 cycles d'horloge
  • t5-t4 : instruction ldi, 1 cycle d'horloge
  • t7-t5 : instructions dec+brne, 1+2=3 cycles d'horloge répété 9 fois soit 27 cycles d'horloge
  • t8-t7 : instruction dec, 1 cycle d'horloge
  • t9-t8 : instruction brne, 1 cycle d'horloge car on sort de la boucle
  • t10-t9 : instruction ret, 4 cycles d'horloge
  • t11-t10 : instruction rjmp, 2 cycles d'horloge

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.

Le chenillard

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.

Voir le programme de chenillard

; 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.

Voir la simulation du chenillard

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.

  • t1 : le bit 7 de R17 est positionné à 1
  • t2 : on retourne au programme principal avec le bit 7 à 1, on effectue une rotation qui transfert le bit 7 dans la retenue. Le branchement ne se fait pas et on a une nouvelle rotation qui transfert la retenue dans le bit 0.
  • t3 : le bit 0 est à 1, la rotation transfert le 1 du bit 0 dans le bit 1, comme C est à 0, le branchement passe directement au transfert dans le port D.

On a deux cas de parcours de programme :

  • C=0 une rotation rol d'un cycle d'horloge et un branchement brcc en début de chargement de deux cycles d'horloges
  • C=1 une rotation rol d'un cycle d'horloge, une instruction de branchement d'un cycle d'horloge qui passe à l'instruction suivante rol d'un cycle d'horloge

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

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 :
Nmax-nregNmax

L'ensemble des modes de fonctionnement sont des variantes de ces deux modes, ou encore une combinaison de ces deux modes.

Le signal PWM

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.

Voir le programme pwm avec le timer 1

; 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

76543210
COM1A1COM1A0COM1B1COM1B0FOC1AFOC1BWGM11WGM10

et TCCR1B

76543210
ICNC1ICES1---WGM13WGM12CS12CS11CS10

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 :

  • CONTROLA = COM1A1 | COM1A0 | WGM10
  • CONTROLB = WGM12

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.

Voir la simulation du PWM

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 :

  • t2-t1 ≈ 3717ns
  • t3-t2 ≈ 15807ns

Ce qui donne un rapport cyclique de (15807-3717)/15807 ≈ 0.76, ce qui correspond à la valeur estimée à partir des valeurs des registres.

Une base de temps pour le chenillard

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.

Voir le programme du chenillard avec le timer en 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

76543210
COM1A1COM1A0COM1B1COM1B0FOC1AFOC1BWGM11WGM10

TCCR1B

76543210
ICNC1ICES1---WGM13WGM12CS12CS11CS10

TIMSK

76543210
OCIE2TOIE2TICIE1OCIE1AOCIE1BTOIE1---TOIE0

et TIFR

76543210
OCF2TOV2ICF1OCF1AOCF1BTOV1---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 :

  • CONTROLA = 0
  • CONTROLB = WGM12

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 :

  • Charger l'adresse du début du programme d'interruption dans la table des vecteurs d'interruptions, cela correspond au vecteur numéro 6, en faisant suivre l'instruction rjmp avec l'étiquette timcp1a qui correspond au début du programme d'interruption
  • Activer l'interruption dans le timer en positionnant le bit OCIE1A dabs le registre TIMSK
  • Activer le bit IFLAG du registre d'état SREG en plus de la retenue utilisée par le chenillard

On termine par l'activation du timer en positionnant le bit CS10 du registre TCCR1B à 1 en conservant les valeurs des autres bits.

Voir la simulation du chenillard avec le timer

On va maintenant analyser le déroulement de cette interruption

  • t1 : Le compteur du timer passe à 0, il déclenche l'interruption et positionne le bit OCF1A du registre des drapeaux (flag en anglais) d'interruption TIFR (numéro de bit identique à celui du registre TIMSK
  • t2 : Le processeur sauve l'adresse du programme dans la pile, et repositionne le drapeau à 0.
  • t3 : Le processeur exécute l'instruction à l'adresse 6 qui correspond à l'interruption du timer
  • t4 : Le processeur se positionne à l'adresse du début du programme d'interruption et exécute l'instruction rol
  • t5 : Le processeur exécute l'instruction brcc et se branhce à l'étiquette
  • t6 : Le processeur exécute l'instruction out et charge la valeur du registre dans le port B
  • t7 : Le processeur exécute l'instruction reti, il restaure l'adresse du programme interrompu contenue dans la pile
  • t8 : Le processeur exécute l'instruction du programme principal
  • t9 : Nouvelle interruption, car en parallèle le compteur continuait de fonctionner, et il est arrivé de nouveau à 0
  • Conclusion

    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 :