Dans cet article, nous allons mettre en pratique un mode d’adressage que nous avons vu dans l’article précédent et nous allons commencer à commander de petits périphériques d’affichage : un afficheur 7 segments pour commencer puis un afficheur LCD de deux lignes de seize caractères. Ces deux types d’afficheur sont très souvent employés en modélisme ferroviaire pour afficher des informations ou des états provenant d’un automatisme. Mais vous verrez que vous pouvez aussi utiliser les mêmes principes de programmation pour créer des animations lumineuses (feux tricolores, enseignes de commerçant, signalisation ferroviaire lumineuse, etc.).
L’assembleur
L’assembleur (8)
Commander des périphériques
.
Par :
DIFFICULTÉ :★★★
Avant de vous plonger dans la lecture de cet article, il est nécessaire d’avoir bien assimilé les différents modes d’adressage des microcontrôleurs AVR que nous avons décrits dans l’article L’assembleur (7) et surtout l’adressage de constantes présentes en mémoire programme post-incrémenté avec l’instruction LPM Z+ ou ELPM Z+. C’est en effet l’instruction LPM Z+ que nous allons utiliser pour envoyer des données à nos petits périphériques et vous verrez que ce mode d’adressage est extrêmement puissant.
Rappel sur l’adressage post-incrémenté
Dans ce qui suit, on ne considèrera que l’instruction LPM Z+ qui convient bien pour l’ATmega328P qui équipe la carte Uno mais le principe reste le même avec l’instruction ELPM Z+ qui fait appel à un registre supplémentaire appelé RAMPZ. Reportez-vous à la figure 15 de l’article L’assembleur (7) pour voir que ce mode d’adressage est fait pour accéder à la mémoire de programme dans laquelle on a stocké des données constantes. Cette mémoire programme est organisée en mots, eux-mêmes composés de deux octets. L’adresse du mot de mémoire est mise dans le registre Z (constitué des deux registres R30 et R31) : les bits 1 à 15 permettent d’accéder à 32K de mots mémoire programme et le bit 0 permet d’accéder à un des deux octets qui composent le mot selon que sa valeur est 0 (low byte) ou 1 (high byte). Les 16 bits du registre Z sont donc utilisés afin d’accéder à 64K octets de la mémoire programme. Comme l’ATmega328P ne dispose que de 32 K octets de mémoire programme, ce mode d’adressage convient parfaitement pour accéder à tout l’espace mémoire. L’instruction LPM signifie Load Program Memory : une fois que l’adressage a permis d’accéder à un octet, le registre Z est automatiquement incrémenté et est ainsi prêt à accéder à l’octet suivant dans la mémoire de programme (d’où l’instruction complète qui est LPM Z+).
Pour utiliser cet adressage post-incrémenté, il est donc nécessaire de charger le registre Z avec l’adresse où se trouve notre première donnée, puis utiliser l’instruction LPM Z+ autant de fois que nécessaire pour accéder à toutes les données qui nous intéressent. Il est indispensable de tenir à jour un compteur d’octets à extraire, comme nous le verrons dans nos programmes, afin de ne pas dépasser la zone où sont stockées les données.
Commande d’un afficheur 7 segments
La figure 1 montre un afficheur 7 segments ; il est constitué de 7 LED en forme de segment et si toutes sont allumées, l’afficheur affiche le chiffre 8. En fonction des segments allumés, on peut afficher les chiffres 0 à 9 (pour un affichage décimal) ainsi que les caractères A à F (pour un affichage hexadécimal : pour ne pas être confondus avec les chiffres, les lettres B et D sont en minuscules b et d). Certains afficheurs comprennent également un point qui constitue un huitième segment. Les huit segments, numérotés de a à h, peuvent être connectés aux 8 lignes d’un port d’entrées-sorties via des résistances de limitation de courant, par exemple le PORT D de l’ATmega328P qui correspond aux sorties 0 à 7 de la carte Uno.
La figure 2 montre comment relier un afficheur 7 segments à anode commune à la carte Uno : chaque segment est donc commandé par sa cathode avec un signal LOW pour l’allumer. PD0 commande le segment a, PD1 commande le segment b et ainsi de suite jusqu’à PD6 qui commande le segment g ; PD7 peut ou non commander le segment h (le point). La correspondance entre segment et broche de l’afficheur est à vérifier avec la datasheet du composant.
Le tableau suivant montre la correspondance entre chiffre à afficher et valeur binaire du PORT D ainsi que sa valeur en hexadécimal (pour un afficheur à anodes communes où l’allumage nécessite un état LOW).
Chiffre | Valeur binaire | Valeur hexadécimale |
---|---|---|
Les valeurs à afficher sur le PORT D sont stockées en mémoire programme, à partir de la première adresse donc 0x0000 ; ceci est réalisé grâce à la directive .CSEG (segment de code) et .DB qui permet de réserver des octets en mémoire programme et de les initialiser avec les bonnes valeurs. Comme on ne précise pas à quelle adresse les octets doivent se trouver, l’assembleur les place à la première adresse possible, soit 0x0000. L’adressage post-indexé permet d’accéder à ces valeurs qui sont au nombre de 10. Chacune des valeurs est successivement mise sur le PORT D initialisé en sortie : entre chaque affichage, on attend une seconde à peu près grâce au sous-programme delay (le principe a été expliqué dans l’article L’assembleur (3)), puis le programme passe à la valeur suivante. Une fois que les 10 chiffres ont été affichés, le programme recommence la séquence d’affichage.
Le registre Z constitué de R30 (appelé ZL) et R31 (appelé ZH) est initialisé avec l’adresse de la première valeur, soit 0x0000. Le registre R18 reçoit le nombre de valeurs à afficher et il est décrémenté chaque fois qu’un affichage est fait. Les registres R8, R9 et R16 servent au sous-programme « delay ». Le registre R20 reçoit la valeur et la transmet au PORT D. Enfin, le registre R17 sert simplement à initialiser le PORT D en sortie.
Voici le listing du programme que vous pouvez reprendre avec Microchip Studio 7. Pour ceux qui sont sous Mac OS ou sous Linux, vous pouvez créer avec l’IDE d’Arduino une fonction en assembleur, appelée depuis la fonction setup : la procédure a été décrite dans l’article L’assembleur (6). Cependant, le listing donné ici doit être légèrement adapté car les directives d’assemblage de l’IDE diffèrent de celles de Studio 7 : vous trouverez en fin d’article une solution pour l’IDE d’Arduino pour le programme de l’afficheur 7 segments et celui de l’afficheur LCD développé un peu plus loin.
;
; Auto_Afficheur_7_Segments.asm
;
; Created: 14/02/2021 15:46:02
; Author : Christian
;
.CSEG
motifs: .DB $c0, $f9, $a4, $b0, $99, $92, $82, $f8, $80, $90
nop
; Initialisation des entrées-sorties
ldi r17, 0b11111111
out DDRD, r17 ; lignes en sortie
; Début de programme
start:
ldi ZL, 0 ; low byte Z-Register (R30)
ldi ZH, 0 ; high byte Z-Register (R31)
ldi r18, 10 ; nombre de motifs
loop_motif:
lpm r20, Z+ ; charge R20 avec motif pointé par Z
; puis incrémente Z
out PORTD, r20 ; copie R20 sur PORT D
rcall delay ; attente 1 s
dec r18 ; un motif de moins restant à faire
brne loop_motif ; passe au motif suivant si R18 non nul
rjmp start ; séquence recommence
delay: ; SBR attente
ldi r16, 81 ; réglé pour une seconde
clr r8
clr r9
loop:
dec r8
brne loop
dec r9
brne loop
dec r16
brne loop
ret
Logiquement, un programme en mémoire démarre à l’adresse 0x0000 car c’est là que pointe le vecteur RESET. Dans notre programme, nous avons mis des données à cette adresse et pourtant, le programme fonctionne à la mise en route et après un RESET par le bouton poussoir de la carte. Cela peut donc paraître étrange, mais il faut se rappeler que le microcontrôleur de la carte Uno utilise un bootloader et qu’il a la possibilité de déporter le vecteur RESET (et éventuellement les vecteurs d’interruptions) au commencement de la section bootloader de la mémoire Flash et ainsi d’initialiser ce vecteur RESET à l’adresse où commence le programme. C’est la combinaison de deux bits (BOOTRST et IVSEL) qui permettent de choisir où sont placés les vecteurs RESET et Interruptions. Ce programme tel qu’il est écrit ne fonctionnerait pas avec un microcontrôleur sans bootloader (ATtiny par exemple). Nous verrons dans un prochain article comment régler ce problème nous-mêmes.
Commande d’un afficheur LCD
Il existe plusieurs types d’afficheurs LCD selon le nombre de lignes qu’ils peuvent afficher et le nombre de caractères par ligne. Cependant, comme ils utilisent tous quasiment le même circuit de contrôle, le mode opératoire pour les commander est le même ou presque, et les différents afficheurs ne diffèrent que par leur rapidité d’affichage et la capacité de la mémoire contenant les caractères à afficher.
Le programme que nous allons développer pourra sans doute convenir à votre afficheur si celui-ci est du type 2 lignes de 16 caractères (LCD1602). Pour tenir compte des temps de réponses qui peuvent varier d’un afficheur à l’autre, nous avons prévu des temporisations assez longues à l’échelle de la microélectronique mais pour vous, l’affichage sera instantané.
La figure 3 montre comment relier l’afficheur à la carte Arduino Uno : la commande se fait grâce à un bus de 8 fils ( DB0 à DB7 du LCD) directement relié au PORT D du microcontrôleur (sorties 0 à 7 de la carte Uno). Une partie du PORT B sert à contrôler les signaux de commandes qui sont au nombre de 3 : PB0 commande le signal RS, PB1 commande le signal RW et PB2 commande le signal E.
Si RS est au niveau LOW, c’est une commande (ou instruction) qu’on envoie au LCD, si RS est au niveau HIGH, c’est une donnée qu’on envoie au LCD (par exemple le code ASCII du caractère qu’on veut afficher).
Si RW est au niveau LOW, le LCD est en mode écriture (cas le plus courant), si RW est au niveau HIGH, le LCD est en mode lecture (il envoie donc une information au microcontrôleur).
Enfin, tant que E est au niveau LOW, le LCD est déconnecté du microcontrôleur et lorsqu’il est au niveau HIGH, le LCD est connecté. On se sert de ce signal pour valider la prise en compte d’une donnée sur le bus d’échange.
Les instructions envoyées au LCD permettent par exemple d’effacer l’écran, de positionner le curseur, etc. Nous allons voir les principales commandes dans le paragraphe suivant.
Les caractères à afficher sont stockés dans une mémoire propre au LCD : l’adresse du caractère à afficher à la première position de la première ligne est 0x00 mais l’adresse du caractère à afficher à la première position de la deuxième ligne est 0x40. Les caractères de la première ligne sont stockés à des adresses comprises entre 0x00 et 0x27, alors que ceux de la deuxième ligne ont pour adresse 0x40 à 0x67. En conséquence, un affichage sur la deuxième ligne doit se faire en ayant d’abord sélectionné la bonne adresse de la mémoire de données du module LCD !
Il est nécessaire de vérifier dans la datasheet du composant ces adresses qui peuvent différer en fonction de la quantité de mémoire du module LCD.
Comme le montre la figure 4, le programme développé plus loin a pour challenge d’afficher « Christian » sur la première ligne du LCD et « de LOCODUINO. » sur la deuxième ligne du LCD. Bien évidemment, vous pouvez remplacer mon prénom par le vôtre et LOCODUINO par votre ville ! Cet exercice va nous permettre de comprendre que l’assembleur peut commander n’importe quel périphérique mais qu’il est important de comprendre comment celui-ci fonctionne. De plus, il va nous permettre de mettre en pratique d’autres notions concernant les directives d’assemblage. Avant de coder, voyons quelles sont les instructions ou commandes qu’on peut envoyer au LCD.
Jeu d’instructions du LCD
Seules les instructions qui vont nous servir sont décrites ici. Notre programme ne fera pas autant de choses que la bibliothèque LiquidCrystal d’Arduino mais peut tout de même servir de base à de nombreux projets. Pour plus de détails sur le jeu d’instructions du LCD, il faut se référer à la datasheet du composant.
Clear display
Cette instruction écrit des espaces dans toute la mémoire de données du LCD, ce qui a pour résultat d’effacer le module, puis elle place le curseur sur la première position (adresse 0). Par défaut, elle sélectionne un déplacement du curseur vers la droite.
Le code à envoyer pour valider cette instruction est :
RS | RW | DB7 | DB6 | DB5 | DB4 | DB3 | DB2 | DB1 | DB0 |
---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
Display ON/OFF
Permet d’activer l’afficheur ainsi que son curseur et décider si le curseur doit clignoter. Le code à envoyer pour valider cette instruction est :
RS | RW | DB7 | DB6 | DB5 | DB4 | DB3 | DB2 | DB1 | DB0 |
---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 1 | D | C | B |
D = 1 pour afficheur ON, C = 1 pour curseur ON et B = 1 pour que le curseur clignote.
Function Set
Cette instruction est très importante puisqu’elle définit comment le LDC doit fonctionner (taille du bus de données sur 4 bits ou 8 bits, nombre de lignes de l’afficheur, taille des caractères). Le code à envoyer pour valider cette instruction est :
RS | RW | DB7 | DB6 | DB5 | DB4 | DB3 | DB2 | DB1 | DB0 |
---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 1 | DL | N | F | X | X |
DL = 1 pour bus de données de 8 bits (notre cas), N = 1 pour 2 lignes (notre cas), F = 0 (caractère de 5 x 7 points), X = 0 ou 1 car le bit est indéterminé.
Pour initialiser notre LCD comme nous le voulons, le code est donc 0b00111000 en plus de RS = 0 et RW = 0.
Set DDRAM Address
Permet de sélectionner l’adresse de la mémoire du LCD où on veut écrire un caractère (cette adresse est écrite dans un registre AC (Address Counter) qui est ensuite incrémenté pour les caractères suivants). Le code à envoyer est :
RS | RW | DB7 | DB6 | DB5 | DB4 | DB3 | DB2 | DB1 | DB0 |
---|---|---|---|---|---|---|---|---|---|
0 | 0 | 1 | AC6 | AC5 | AC4 | AC3 | AC2 | AC1 | AC0 |
AC6:AC0 est l’adresse voulue, par exemple 1000000 pour 0x40, adresse du premier caractère de la deuxième ligne.
Read Busy Flag and Address
Cette instruction permet de s’assurer que l’opération en cours sur le module LCD est terminée ( BF (Busy Flag) = 1 tant que l’opération est en cours et 0 une fois terminée). RW = 1 pour que le LCD soit en mode lecture. Bien évidemment, aucune instruction ou commande ne doit être envoyée au LCD tant qu’une opération est encore en cours. Le code envoyé par le LCD est :
RS | RW | DB7 | DB6 | DB5 | DB4 | DB3 | DB2 | DB1 | DB0 |
---|---|---|---|---|---|---|---|---|---|
0 | 1 | BF | AC6 | AC5 | AC4 | AC3 | AC2 | AC1 | AC0 |
Il suffit donc de surveiller DB7 et d’attendre qu’il soit à 0 pour pouvoir envoyer une instruction au LCD.
Write Data to DDRAM
On envoie une donnée donc RS = 1 et le module est écrit donc RW = 0. Les huit autres bits sont le code ASCII du caractère à écrire. Le code à envoyer pour valider cette instruction est :
RS | RW | DB7 DB6 DB5 DB4 DB3 DB2 DB1 DB0 |
---|---|---|
1 | 0 |
Autres instructions
Il existe d’autres instructions pour écrire ou lire le module LCD comme par exemple Home qui positionne le curseur à l’adresse de départ sans modifier le contenu de la mémoire de donnée du LCD, ou bien encore des instructions permettant de définir le déplacement du curseur ou de l’afficheur (ce qui permet de faire du scrolling). Nous ne les commenterons pas ici puisque nous n’en avons pas besoin pour le programme.
Le programme
Le listing ci-dessous vous donne l’ensemble du programme qui est expliqué juste après :
;
; Afficheur_LCD.asm
;
; Created: 20/02/2021 16:36:17
; Author : Christian
;
; Affiche sur un écran LCD (mode 8 bits)
; En première ligne : Christian
; En deuxième ligne : de LOCODUINO.
; PORT B PB0 RS = 0 instruction - RS = 1 donnée
; PB1 RW = 0 écriture - RW = 1 lecture
; PB2 E signal de validation
; PORT D D0-D7
;
.CSEG
; Initialisation des PORT B et D
nop
ldi r19, $FF
out DDRB, r19 ; PORT B en sortie
out DDRD, r19 ; PORT D en sortie
; Réglage des lignes de contrôle RS = PB0, RW = PB1, E = PB2
ldi r19, $00
out PORTB, r19 ; Lignes RS, RW et E à 0
; Et LED_BUILTIN off
; Initialisation du LCD
rcall LCD_INIT
; Display ON
ldi r16, 0b00001111 ; Code display on, cursor on, blink on
rcall LCD_REG ; Envoie code au LCD
rcall delay_1MS ; Attente pour réponse LCD
; Clear display
ldi r16, 0b00000001 ; Code pour effacer LCD
rcall LCD_REG ; Envoie code au LCD
rcall delay_1MS ; Attente pour réponse LCD
rcall delay_1MS ; Temps > 1.52 ms
; Initialisation des registres
ldi ZH, $00
ldi ZL, $C0 ; Initialisation Z-Register ($00C0 = 192)
; Affichage première ligne
ldi r17, 10 ; Nombre de caractères du message 1
start_1:
rcall delay_1MS ; Attente pour réponse LCD
LPM r16, Z+ ; Caractère dans R16
rcall LCD_DATA ; Envoie caractère au LCD
dec r17 ; Actualise nombre de caractères restant
brne start_1 ; Recommence avec caractère suivant
nop ; R17 = 0, message complet
; Position en mémoire d'affichage pour deuxième ligne
rcall delay_1MS ; Attente pour réponse LCD
cbi PORTB, 0 ; RS = 0
cbi PORTB, 1 ; RW = 0
ldi r16, 0b11000000 ; DB7 = 1 et adresse = 0x40 (début 2ème ligne)
out PORTD, r16 ; Envoie le code au LCD
rcall LCD_E ; Génère impulsion sur E
; Affichage deuxième ligne
ldi r17, 14 ; Nombre de caractères du message 2
start_2:
rcall delay_1MS ; Attente pour réponse LCD
LPM r16, Z+ ; Caractère dans R16
rcall LCD_DATA ; Envoie caractère au LCD
dec r17 ; Actualise nombre de caractères restant
brne start_2 ; Recommence avec caractère suivant
nop ; R17 = 0, message complet
; Allumage LED_BUILTIN et boucle infinie
sbi PORTB, 5 ; LED_BUILTIN on
infini:
nop
rjmp infini ; Boucle infinie fin de programme
; SOUS-PROGRAMMES
;************************************************************************
LCD_INIT: ; Initialise le LCD
ldi r16, 0b00111000 ; LCD : 8 bits, 16 x 2, 5 x 7
rcall LCD_REG ; Envoie instruction
rcall delay_5MS ; Attend 5 millisecondes
ldi r16, 0b00111000 ; Idem deuxième fois
rcall LCD_REG
rcall delay_5MS
ldi r16, 0b00111000 ; Idem troisième fois
rcall LCD_REG
rcall delay_5MS ; Total 15 millisecondes
ret
LCD_REG: ; Envoie instruction au LCD
cbi PORTB, 0 ; PB0 = RS = 0 : instruction
out PORTD, r16
rcall LCD_BUSY
rcall LCD_E ; Génère impulsion sur E
ret
LCD_DATA: ; Envoie donnée au LCD
cbi PORTB, 0 ; PB0 = RS = 0 : instruction
out PORTD, r16 ; caractère sur le PORT D
rcall LCD_BUSY ; Attend que LCD se libère
sbi PORTB, 0 ; PB0 = RS = 1 : donnée
rcall LCD_E ; Génère impulsion sur E
ret
LCD_BUSY: ; Attend tant que LCD non prêt
sbi PORTB, 1 ; Met le LCD en mode lecture RW = 1
ldi r19, $00
out DDRD, r19 ; PORT D en entrée
sbi PORTB, 2 ; Active signal E
L_BUSY:
sbic PIND, 7 ; Teste busy flag
rjmp L_BUSY ; Bit = 1, recommence
nop ; Bit = 0, LCD prêt
ldi r19, $FF
out DDRD, r19 ; PORT D en sortie
cbi PORTB, 1 ; Met le LCD en mode écriture RW = 0
ret
LCD_E: ; Active ligne E pendant 1 µs au moins
sbi PORTB, 2 ; E = 1
rcall delay_500 ; Attente 500 ns
rcall delay_500 ; Attente 500 ns
cbi PORTB, 2 ; E = 0
ret
delay_500: ; Attente de 500 ns (8 cycles de 62.5 ns)
; 3 cycles pour rcall
nop ; 1 cycle pour nop
ret ; 4 cycles pour ret
delay_1MS: ; Attente de 1 milliseconde
ldi r19, 21 ; Charge R19
clr r8 ; R8 = 0 (équivalent à 256)
loop_1:
dec r8 ; Boucle sur R8
brne loop_1
dec r19 ; Boucle sur R19
brne loop_1
ret
delay_5MS: ; Attente de 5 millisecondes
ldi r19, 104 ; Charge R19
clr r8 ; R8 = 0 (équivalent à 256)
loop_5:
dec r8 ; Boucle sur R8
brne loop_5
dec r19 ; Boucle sur R19
brne loop_5
ret
; MESSAGES
;***************************************************************************
.CSEG
.ORG $0060 ; $0060 = 96
message_1: .DB 'C', 'h', 'r', 'i', 's', 't', 'i', 'a', 'n', ' '
message_2: .DB 'd', 'e', ' ', 'L', 'O', 'C', 'O', 'D', 'U', 'I', 'N', 'O', '.', ' '
Le programme commence par initialiser les ports B et D en sortie et mettre les lignes RS, RW et E au niveau LOW (lignes 18 à 28). Ensuite, il effectue une initialisation du LCD grâce au sous-programme « LCD_INIT » (lignes 29 à 31), il active le LCD et efface son contenu en envoyant les bonnes instructions grâce au sous-programme « LCD_REG » (lignes 32 à 42). La collecte des caractères à afficher se fait en post-incrémentation du registre Z comme cela a été réalisé plus haut pour l’afficheur 7 segments. Le code ASCII du caractère est envoyé au LCD grâce au sous-programme « LCD_DATA ». Les caractères à afficher constituent deux messages, un premier de 10 caractères sur la première ligne et un deuxième de 14 caractères sur la deuxième ligne [1]. Les codes ASCII sont stockés en mémoire de programme à l’adresse 0x0060 (= 96) grâce aux directives .ORG et .DB (voir plus loin). Cette fois, les données sont après le programme et il faut choisir une adresse éloignée de la fin des octets de programmes pour ne pas interférer (Microchip Studio 7 vous fera savoir s’il y a un recouvrement des zones de données et de programme et il faut alors modifier cette adresse : ce peut être le cas si vous rajoutez des instructions à ce programme).
L’adresse qui suit .ORG représente une adresse de l’espace mémoire de programme, donc un mot de 16 bits. Il faut donc la multiplier par 2 pour la transformer en adresse de l’octet contenant la première donnée, ceci pour charger le registre Z avec la bonne valeur. Par exemple, on a choisi l’adresse de mémoire programme égale à 0x0060 (= 96), le double est 0x00C0 (= 192 = 96 x 2). ZH est donc égal à 0x00 et ZL est égal à 0xC0 (lignes 43 à 45).
Une fois la première ligne affichée (lignes 47 à 56 : le curseur étant à la bonne position grâce à l’instruction Clear Display), il faut sélectionner l’adresse correspondant à la première position de la deuxième ligne dans la mémoire de donnée du LCD (soit 0x40). Ceci est fait de la ligne 57 à la ligne 64. Ensuite, on affiche les 14 caractères correspondant au deuxième message (lignes 65 à 74).
Lorsque la deuxième ligne est affichée, on rentre dans une boucle infinie (ligne 77) après avoir allumé la LED_BUILTIN de la carte Uno pour le signifier (ligne 76).
Les sous-programmes
Pour les écrire, je me suis inspiré de programmes réalisés pour une carte de développement conçue autour d’un PIC16F84 appelée PIC’Trainer, elle-même munie d’un afficheur LCD de type 1602.
LCD_INIT
Ce sous-programme envoie l’instruction Function Set avec le code 00111000 comme expliqué plus haut. Le LCD est initialisé avec un bus de 8 lignes, 2 lignes d’affichage et des caractères de 5 x 7. Une temporisation de 5 millisecondes est ensuite respectée et la procédure est faite trois fois de suite pour durer au total 15 ms, durée recommandée par certaines datasheets.
LCD_REG
Ce sous-programme permet d’envoyer un code d’instruction au LCD. Ce code est d’abord chargé dans le registre R16 puis est transféré au PORT D.
LCD_DATA
Ce sous-programme envoie le code ASCII du caractère à afficher au module LCD. Ce code est d’abord chargé dans le registre R16 puis est transféré au PORT D.
LCD_BUSY
Ce sous-programme surveille le Busy Flag et ne fait rien tant que celui-ci n’est pas mis à 0 par le module LCD. Il empêche qu’une instruction soit transmise au LCD tant que celui-ci n’est pas prêt à la recevoir.
LCD_E
Ce sous-programme active la ligne E à l’état haut pendant 1 µs (deux fois 500 ns) pour valider la prise en compte d’une instruction ou d’une donnée.
Les registres utilisés
R8 sert aux sous-programmes de temporisation longue (1 ms et 5 ms).
R16 sert à transférer les informations (instruction ou code ASCII) sur le bus DB7:DB0 .
R17 contient le nombre de caractères du message restant à afficher. Il est décrémenté après chaque opération d’affichage. Lorsqu’il devient égal à 0, l’ensemble du message de la ligne a été affiché.
R19 sert à l’initialisation des ports et des lignes RS, RW et E. Il sert aussi dans les sous-programmes de temporisation longue pour régler la durée.
R30 et R31, encore appelés ZL et ZH, forment le registre Z utilisé pour stocké l’adresse en mémoire programme du caractère à afficher.
Les temporisations
La temporisation de 500 ns doit attendre 8 cycles de 62.5 ns. L’appel par RCALL représente 3 cycles auxquels on ajoute un cycle pour le NOP et quatre cycles pour le RET, ce qui fait bien 8 cycles au total soit 8 x 62.5 = 500 ns. Un double appel permet la temporisation préconisée de 1 µS pour l’impulsion de validation sur la ligne E.
Une temporisation de 1 ms permet de laisser le temps au LCD pour terminer une opération précédente. Sa durée pourrait sans doute être raccourcie pour optimiser le programme. Seule l’instruction Clear Display nécessite un délai supérieur à 1.52 ms pour être terminée.
Une temporisation de 5 ms est nécessaire pour initialiser le LCD et laisser le temps à l’alimentation du module d’atteindre la bonne valeur. Au moins 10 ms sont préconisées pour cela et l’initialisation dure au total 15 ms dans ce programme.
Les messages à afficher
On les trouve en fin de programme. La directive .CSEG indique qu’on les place en mémoire programme, la directive .ORG donne l’adresse du mot de mémoire, la directive .DB réserve les octets nécessaires et les initialise avec les codes ASCII de chaque caractère ; on peut utiliser la notation du C pour les caractères (‘C’ : l’octet est égal au code ASCII du caractère C majuscule qui est 0x43).
La mémoire de programme étant organisée en mots de 2 octets, il faut un nombre pair d’octets, raison pour laquelle chaque message se termine par un espace. Si vous ne le faites pas, Microchip Studio 7 vous rappelle à l’ordre.
Dans ce programme, le premier caractère du message à afficher sur la première ligne du LCD est le 193ème octet de mémoire programme (adresse 0x00C0 = 192). Or, le programme occupe 190 octets (adresse de 0x0000 à 0x00BD) ; il n’y a que deux octets libres entre programme et données comme le montre la figure 4 (la fin du programme est surlignée en jaune et les données le sont en vert). Il n’y a donc pas de place pour étendre le programme sauf en déplaçant les données.
Utilisation de l’IDE d’Arduino pour développer ces deux programmes
Si vous ne disposez pas de Microchip Studio 7, vous pouvez utiliser l’IDE d’Arduino pour programmer votre carte Uno ou Nano avec ces programmes en assembleur. La méthode a été expliquée dans l’article L’assembleur (6) ; il suffit d’écrire une fonction en assembleur (avec extension .S) appelée une seule fois depuis la fonction setup du programme. Cette fonction doit être déclarée extern "C"
dans le programme en C et .global
dans le code en assembleur. Les directives d’assemblage devront être changées car la syntaxe n’est pas la même entre l’IDE et Studio 7. De plus, la partie assembleur de l’IDE ne connaît pas les noms de registres comme DDRD et PORTD qui doivent être remplacés par les adresses en mémoire (ou bien il faut inclure un fichier donnant les correspondances). Enfin, les données en hexadécimal doivent être entrées sous la forme 0x12AB au lieu de $12AB.
L’archive à télécharger ci-dessous offre une solution pour l’IDE d’Arduino, développée grâce à l’aide de Jean-Luc. Il suffit de mettre les deux répertoires qu’elle contient dans le répertoire où sont stockés vos programmes Arduino. Nous vous invitons à la regarder et la comparer à ce qui a été fait avec Studio 7. Au final, les programmes occupent plus de place en mémoire Flash, ce qui est normal puisqu’ils sont constitués d’un corps en C qui appelle une routine en assembleur. En contrepartie, c’est l’IDE qui gère la position en Flash des données (motifs ou messages), ce qui évite un chevauchement (overlap en anglais) entre programme et données.
Cet article nous a permis de voir la puissance de certaines techniques d’adressage des microcontrôleurs AVR et de comprendre qu’il est possible de commander des périphériques sans utiliser les bibliothèques écrites en C/C++ pour Arduino. L’avantage d’écrire soi-même des routines en assembleur pour commander des périphériques est que le code ne comporte que ce dont il a besoin. Il est donc incomparablement plus compact et plus rapide. L’inconvénient est le temps que cela demande, d’abord pour comprendre comment s’utilise le périphérique, puis le temps pour écrire, tester et déboguer le programme. Le prochain et dernier article de cette série sur l’assembleur vous donnera quelques conseils et quelques règles de bonnes pratiques pour écrire vos programmes.
[1] Chaque message se termine par un espace pour avoir un nombre pair d’octets comme expliqué en fin d’article.