DEL clignotante avec MicroHOPE

Pour coder en assembleur, il faut avoir une idée de l'architecture du matériel cible. C'est suffisant de supposer que le micro-contrôleur AVR se présente au programmeur comme un ensemble de registres à usage général (General Purpose Registers, GPRs: R1 à R31), de registres de fonctions spéciales (Special Functions Registers, SFRs) qui contrôlent les périphériques, un peu de mémoire pour les données (2 kilo-octets de SRAM pour l'Atmega32). Tous les registres sont dans l'espace d'adressage des données. On a aussi de la mémoire de programme et de l'EEPROM dans des espaces d'adressage séparés.

Programmer en langage assembleur suppose de déplacer des donnée entre les GPRs, les SFRs et la RAM, et de réaliser des opérations arithmétiques et logiques sur les données.

On a quatre ports d'entrée/sortie (A, B, C et D, contrôlés par douze SFRs) qu'on peut utiliser pour ses programmes et pour afficher le résultat. Afin d'y arriver, on doit utiliser des interrupteurs et des DELs connectées à ces ports. Les exmples suivants utilisent la carte de sorties numériques (Digital Output Board, qui fournit 8 DELs) pour afficher les résultats des programmes. La figure représente la carte branchée au port B.

On commence avec un petit programme montré ci-dessous (immed.S est inclus dans les exemples fournis).

1
2
3
4
5
6
7
8
9
; immed.s  , démonstration de Load Immediate mode

       .section .text    ; denote la section de code
       .global main
main:
  ldi r16, 255      ; charge r16 avec 255
  out 0x17, r16     ; Affiche le contenu de R16
  out 0x18, r16     ; à l'aide des DELs sur le port B
  .end

On clique sur « Assembler » puis sur « Charger », ça doit allumer toutes les DELs.

Les Registres d'usage général, GPRs

Nous sommes déjà familiers avec les registres spéciaux de fonction (SFRs, y compris DDRB et PORTB) qui sont utilisés pour configurer et contrôler plusieurs propriétés du micro-contrôleur. En plus de ceux-ci l'ATmega32 a trente deux registres d'usage général (ici, trente deux est une coïncidence, le 32 dans ATmega32 fait référence aux 32 kilo-octets de mémoire flash disponibles pour programmer. Tous les micro-contrôleurs AVR, même l'ATmega8 et l'ATmega16 ont trente-deux registres d'usage général).

Toute valeur numérique qu'on doit utiliser dans le programme doit tout d'abord être chargée dans un des GPRs. Ainsi, si on veut charger 0xff dans DDRB, il faut d'abord charger 0xff dans un GPR et ensuite copier le contenu du GPR vers DDRB. Ça peut paraître une restriction inutile à ceux qui avaient l'habitude d'écrire DDRB=0xff en C, mais c'est la conséquence nécessaire de la conception en ligne de flux du matériel, que le compilateur C nous cache.

Même si les trente deux registres R0-31 sont nommés « à usage général », certains d'entre eux ont une utilsation spéciale, ce sera discuté plus tard.

Les instructions

Ce qu'on ferait intuitivement avec un opérateur d'assignation (=) en C nécessite d'utiliser plus qu'une instruction.

LDI (Load Immediate) : est utilisé pour charger une valeur constante dans un des registres R16-31 (c'est une restriction. Load Immediate ne peut pas fonctionner avec les registres R1 à R15)

OUT (output to any SFR) :  Les SFRs sont mappés aux adresses 0 à 0x3f. Par exemple, 0x17 et 0x18 sont les adresses de mappage d'entrée/sortie pour les registres DDRB et PORTB respectivement.

Les SFRs sont aussi mappés dans l'espace mémoire aux adresses 0x20 à 0x5f. À cause de cela, on peut utiliser l'instruction STS (Store Direct to SRAM) à la place de OUT mais à une adresse différente. OUT 0x17, R16 et STS 0x37, R16 ont le même résultat, mais la dernière instruction est plus compacte.

Addition de deux nombres

Le code listé ci-dessous (add.S) additionne deux nombres et affiche le résultat sur les DELs connectées au port B. Au lieu de se rappeler les adresses de DDRB et PORTB, on a inclus le fichier 'avr/io.h' qui contient tous les noms de registres. Quand on nomme le programme avec un suffixe .S (S majuscule, au lieu de s minuscule), on invoque le pré-processeur, ce qui autorise des expressions telles que 1 << PB3. (add.S)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
; programme add.S
; Charge 2 registres avec des valeurs et les additionne.
; Affiche le résultat sur le port B

#include <avr/io.h>
      .section .text    ; dénote la secion de code
      .global main
main:
     LDI    R16, 255    ; charge R16 avec 255
     STS    DDRB, R16   ; règle tous les bits du port B comme sorties
     LDI    R16, 2      ; charge R16 avec 2
     LDI    R17, 4      ; charge R17 avec 4
     ADD    R16, r17    ; R16 <- R16 + R17
     STS    PORTB, R16  ; résultat envoyé au port B
     .END

Quand on lance ce programme, cela allume les DELs D2 et D3.

Le registre de statut

Les opérations arithmétiques et logiques affectent les bits de statut du registre, comme Carry, Zero, Negative etc. On peut se référer au databook del'Atmega32 pour plus d'information.

Le registre de statut de MicroHOPE

Bit 0 : Retenue (Carry)

Bit 1 : Zéro

Bit 2 : Négatif

Bit 3 : Dépassement du complément à deux

Bit 4 : Bit de signe, OU exclusif de N et V

Bit 5 : Demi-retenue (Half Carry)

Modifions le programme précédent pour évaluer 255 + 1. Le résulat sera affiché sur le port B est le registre de drapeaux de status SREG sur le port A. (carry.S)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <avr/io.h>
      .section .text    ; dénote la section de code
      .global main
main:
     LDI    R16, 255
     STS    DDRB, R16   ; Tous les bits du port B en sorties
     STS    DDRA, R16   ; Tous les bits du port A en sorties
     LDI    R17, 3      ; charger R17 avec 1
     ADD    R16,  R17   ; R16 <- R16 + r17
     STS    PORTB, R16  ; Total vers le port B
     LDS    R16, SREG   ; On charge le registre de statut
     STS    PORTA, R16  ; et on l'affiche sur le port A
     .END

Les bits Carry, Zero et Half Carry seront allumés, sur le port A.

Exercice 1: Charger R16 et R17 avec deux nombres et étudier les résultats ainsi que les drapeaux de statut que génèrent les opérations suivantes.

COM   R16    ; Complément

NEG   R16    ; Complément à deux

TST    R16    ; test pour zéro ou moins (zero, minus)

AND  R16, R17   ; ET au niveau des bits

OR    R16, R17   ; OU au niveau des bits

ADD  R16, R17   ; summing

Exercice 2: additionner un nombre et son complément à deux, et faire de même avec son complément à un, puis comparer les résultats

LDI      R16, 10    ; chargement d'un nombre

MOV   R17, R16

NEG   R16          ; complément à deux

ADD  R17, R16

Déplacer des données

Pour manipuler des données, on a besoin de les amener dans les GPRs (R1 à R31) et il faut renvoyer les résultats dans des emplacements de la mémoire. Il y a divers modes de transferts entre les GPRs et les emplacements de la mémoire, comme expliqué ci-dessous.

Direct au registre : MOV  R1, R2  ; copie R2 vers R1 . Deux GPRs sont impliqués dans l'opération. Il y a aussi des opérations qui n'impliquent qu'un seul registre, comme INC R1

Direct aux entrées-sorties : Pour déplacer des données deentre les GPRs et les SFRs, puisque les SFRs peuvent être accédés comme des adresses d'entrée-sortie. OUT  0x17, R1  copie R1 dans DDRB.

Veuillez noter que l'adresse d'entrée-sortie est 0x20 de moins que l'adresse mappée en mémoire (0x37) du même SFR. (io-direct.S)

Immédiat : Ce mode peut être utilisé pour transférer un nombre vers n'importe quel registre entre R16 et R31, comme : LDI   R17, 200. La donnée est fournie comme une partie de l'instruction. (immed.S)

Direct aux données : Dans ce mode, l'adresse d'un emplacement mémoire contenant la donnée est spécifiée, au lieu de la donnée elle-même. LDS  R1, 0x60 déplace le contenu de la mémoire d'adresse 0x60 vers R1. STS 0x61, R1 copie R1 vers la mémoire d'adresse 0x61. (data-direct.s)

Indirect aux donnés : Dans le mode précédent, le mot d'instruction contient l'adresse d'un emplacement mémoire. Ici, l'adresse del'emplacement mémoire est prise dans le contenu de registres X, Y et Z. X, Y et Z sont des pseudo-registres de 16 bits obtenus en combinant deux resitres consécutifs de 8 bits (X c'est R26 et R27; Y c'est R28 et R29; Z c'est R30 et R31). Ceci est nécessaires pour l'accès aux mémoires d'adresses supérieures à 255. (data-indirect.s)

LDI  R26, 0x60   ; adresse pour l'emplacement 0x0060 dans X

LDI  R27, 0x00

LD   R16, X         ; charge R16 avec le contenu de la mémoire selon l'adresse dans X

Ce mode a de nombreuses variantes comme pré et post-incrémentation du registre ou en ajoutant un décalage à celui-ci. Voir le databook pour les détails.

Programmes avec des données

Le programmes ont généralement des variables, quelquefois avec des données initialisées. On les attend dans le segment .data ; l'exemple suivant montre comment on accède à une variable dans les données à l'aide des modes direct et indirect. (data-direct-var.S)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <avr/io.h>
           .section .data       ; la section des données commence ici
var1:
            .byte  0xEE         ; initialisation de la variable globale var1

           .section .text       ; section du code
           .global    __do_copy_data   ; initialise les variables globales
           .global     __do_clear_bss  ; et le pointeur de pile
           .global main
main:
           LDS  R1, var1               ; charge R1 avec le mode direct aux données
           STS   DDRA, R1              ; affiche R1 sur le port A
           STS   PORTA, R1
           LDI   R26, lo8(var1)        ; charge les octets de poids faible
           LDI   R27, hi8(var1)        ; et fort de l'adresse de var1 dans X
           LD    R16, X                ; charge R16 à l'aide du mode indirect,  la donnée pointée par X
           STS   DDRB, R16             ; affiche R16 sur le port B
           STS   PORTB, R16
   .end

Les ligness .global    __do_copy_data  et .global __do_clear_bss  disent à l'assembleur d'insérer le code pour initialiser les variables globales, ce qui est nécessaire.

Branchements et appels de sous-programmes

Les programmes écrits jusque-là ont un flot d'exécution séquentiel, du début à la fin, sans aucun branchement ni appel de sous-programme, que nécessitent la plupart des programmes. L'exécution peut être contrôlée par des instructions CALL et JMP. (call-jump.S)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <avr/io.h>
           .section .text    ; début de la section de code
disp:                        ; notre sous-programme
           STS    PORTB, R1  ; affiche R1 sur le port B
           INC    R1         ; et l'incrémente
           RET               ; puis revient

           .global main
main:
           LDI     R16, 255
           STS     DDRB, R16
           MOV     R1, R16
loop:
           RCALL   disp         ; appel relatif
           CALL    disp         ; appel direct
           RJMP    loop
           .end

Le programme principal appelle le sous-programme dans une boucle, la donnée est incrémentée à chaque appel. Utilier un oscilloscope pour visualiser le signal de tension sur chaque DEL.

Résultat produit par l'assembleur

Le code qu'on écrit est traduit par l'assembleur en instructions de langage machine. Puis il est passé au relieur (linker) pour décider de l'emplacement où seront placés le code et les données qu'il faut enregistrer avant l'exécution. Le code est enregistré dans la mémoire de programme. Même si le processeur démarre à l'adresse zéro lors d'un redémarrage (reset), le relieur place les adresses des vecteurs d'interruption à cet endroit-là, puis un peu de code d'initialisation et seulement après le cade qu'on a écrit. On peut explore le fichier .lst pour savoir les détails.

Les interruptions, les appels asynchrones

Dans certaines situations, le µC doit répondre à des évènements extérieurs, en arrant temporairement le programme courant. Cela se passe en utilisant des interruptions, qui sont des signaux externes, venant soit des bornes d'entrée-sortie ou de certains périphériques. À la réception d'un signal d'interruption, le processeur enregistre la valeur courante du compteur de programme à l'emplacement mémoire pointé par le pointeur de pile et saute au vecteur d'interruption correspondant (par exemple, le processeur sautera à l'adresse 0x0002 -- ou 0x0004 si on compte ça en octets --, si la borne d'interruption externe INT0 est activée, à condition que cette interruption soit d'avance activée par le processeur. (interrupt.S). Connecter PD2 à la masse momentanément et observer les DELs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
;interrupt.s : Montre l'utilisation des interruptions en assembleur

        .section .data    ; début de la section des données ici
        .section .text    ; dénote la section de code

     .global __vector_1   ; INT0_vect
__vector_1:
     inc r1
     out 0x18, r1
     reti

        .global main
main:
     ldi  r16, 255
     out  0x17, r16   ; DDRB
     out  0x12, r16   ; mise à VRAI du port D
     ldi  r16, 0x40   ; activation de INT0
     out  0x3b, r16
     clr  r1
     sei
loop:        rjmp loop
     .end

Un générateur de rampe à l'aide d'un convertisseur numérique-analogique 2-2R

Connecter un convertisseur numérique-analogique 2-2R au port B, comme montré ci-dessous et lancer le programme ramp-on-R2RDAC.S

image0 image1