L’assembleur (8)

Commander des périphériques

. Par : Christian. URL : https://www.locoduino.org/spip.php?article291

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

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.

Figure 1
Figure 1
Afficheur 7 segments à LED.

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.

Figure 2
Figure 2
Interfaçage entre afficheur et Arduino.

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

ChiffreValeur binaireValeur hexadécimale
0
11000000
0xC0
1
11111001
0xF9
2
10100100
0xA4
3
10110000
0xB0
4
10011001
0x99
5
10010010
0x92
6
10000010
0x82
7
11111000
0xF8
8
10000000
0x80
9
10010000
0x90

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.

Figure 3
Figure 3
Interfaçage entre afficheur LCD et Arduino.

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.

Figure 4
Figure 4
Affichage de deux lignes sur écran 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 :

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

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

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

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

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

RSRWDB7 DB6 DB5 DB4 DB3 DB2 DB1 DB0
1 0
Code ASCII du caractère

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.

Figure 5
Figure 5
Mapping de la mémoire Flash. On voit le programme en jaune et les données en vert.

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.

Archive
Les deux programmes pour l’IDE d’Arduino.

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.

[1Chaque message se termine par un espace pour avoir un nombre pair d’octets comme expliqué en fin d’article.