PILOTER UN ECRAN LCD 16X2 (PARTIE 1/2)

062414_2213_piloterunec1

Objet

Les deux prochains billets vont présenter un grand classique en informatique industrielle : l’écran LCD monochrome 16×2.

LCD est l’acronyme de Liquid Crystal Display, autrement dit un écran à cristaux liquide. Concernant la techno derrière le fonctionnement d’un LCD, je vous renvoie vers la page wikipedia ([1] LCD – Wikipedia).

L’objet de ce premier billet sera de décrire l’interface de commande matérielle et logicielle de l’écran et d’afficher un simple Hello World. Le second billet présentera l’affichage de caractères personnalisés, le défilement de texte, le mode 4 bits et le curseur.

Nous utiliserons comme support matériel une plaquette de développement DIY à base de PIC 8 bits 18F2550 (plus de détails dans un billet précédent).

Présentation

L’écran utilisé ici sera un LCD 16×2 de la marque Hitashi piloté par un contrôleur HD44780U, dont la datasheet est disponible ici : [2] Datasheet HD44780.

Vous pouvez déjà ouvrir le lien, on se référera sans arrêt à la datasheet dans ce billet  !

Par la suite, j’emploierai le terme générique « LCD » bien qu’il ne s’agisse en réalité le plus souvent que du seul contrôleur HD44780U dont il est question.

Cet écran est très souvent utilisé dans les projets DIY ou pour du prototypage en industrie car il est très simple à commander et bon marché (compter environ 5$ sur DealExtreme). Vous l’avez probablement déjà rencontré lui ou l’un de ses grands frères sur la machine à café de votre entreprise.

Voilà le modèle que nous allons utiliser (Figure 1) :

Figure 1 Ecran LCD Hitashi 16×2

Il est capable d’afficher 16 symboles par lignes pour un total de deux lignes.

Pour le commander, son interface se compose de 8 signaux de données identifiées D0 à D7 (bus parallèle) et de 3 signaux de contrôles (Tableau 1) :

Signaux Définition Fonction
RS Register Select permet la sélection du registre d’instruction (0) ou de donnée (1).
R/W Read/Write permet la sélection du mode lecture (1) ou écriture (0)
E Enable permet d’initier une lecture ou écriture

                                                                           Tableau 1 Signaux de contrôles du LCD

L’écran LCD est adressable en mode 8 bits via les pins D0 à D7 ou en mode 4 bits via les pins D4 à D7. Le mode 4 bits est souvent utilisé dans les projets où le nombre d’entrées/sorties est restreint (par exemple sur des projets à base de microcontrôleurs 8 bits, ce qui est notre cas ici). Le mode 4 bits ne sera pas détaillé ici et fera l’objet d’un prochain billet.

A cela s’ajoutent des signaux de contrôle du rétro-éclairage (Anode, Cathode) et du contraste (Vo). Bien que le rétro-éclairage et le contraste soit pilotables par logiciel, cela ne sera pas traité ici. Le réglage se fera au moyen de résistances variables.

Outre les signaux de contrôles vus précédemment, la commande du LCD se fait par l’intermédiaire de deux registres 8 bits :

  • Un registre d’instruction IR, pour stocker les codes instructions (commandes) ou une adresse (pour un accès à la RAM). Lorsqu’une adresse est écrite dans ce registre, une lecture est initiée automatiquement depuis l’une des deux RAM disponibles et le résultat est stocké dans le registre de donnée ;
  • Un registre de donnée DR, pour les données à écrire en RAM, ou stocker les données lues depuis la RAM. Chaque octet écrit dans ce registre est automatiquement envoyé dans l’une des deux RAM disponibles à l’adresse définie dans le compteur d’adresse AC. AC est automatiquement incrémenté ou décrémenté (selon configuration) après une écriture.

Le LCD possède deux RAM :

  • La Data Display RAM ou DDRAM, qui est utilisée pour stocker les codes des symboles connus de l’afficheur et représente la matrice d’affichage sur l’écran LCD ;
  • La Character Generator RAM ou CGRAM, utilisée pour la programmation de symbole personnalisé pixel par pixel (ou pattern) pour créer de nouveaux symboles. Cela sera abordé dans le prochain billet.

La distinction entre les deux RAM se fait au niveau du mot de commande (registre IR).

Nous venons de faire le tour des signaux de contrôles, de la mémoire et des registres.

Nous avons tous les éléments pour communiquer avec notre LCD. Nous allons maintenant nous intéresser au schéma de câblage de l’afficheur.

Câblage

Voici le schématique du câblage en mode 8 bits (Figure 2), réalisé à l’aide de tinyCAD (cliquer pour agrandir) :

lcd

Figure 2 Câblage de l’écran LCD en mode 8 bits

Voici quelques commentaires sur le schéma :

Le bus de donnée est câblé sur le port B du micro : RB0/DB0 – RB7/DB7.

Les signaux de contrôles RS, R/W, E sont respectivement câblés sur les sorties RA2, RA1, RA0 du micro.

L’anode du rétroéclairage est reliée au 5V. On ajoute une résistance de 22 ohms en série pour limiter le courant. La cathode est reliée à la masse.

Le contraste est un cas un peu à part. Il existe deux types d’afficheurs :

  • les afficheurs à contraste positif ;
  • les afficheurs à contraste négatif.

Pour faire varier le contraste et dans le cas d’un contraste positif, une simple résistance variable reliée au 5V suffit. Dans mon cas j’ai un afficheur à contraste négatif et c’est un tout petit peu plus compliqué. En effet il faut pouvoir générer une tension négative à partir du 5V d’alimentation. Pour ce faire il faut un circuit à pompe de charge comme l’ICL7660S. Il est disponible sur RS-particulier (frais de port offerts les weekends). Le câblage de ce composant ne présente pas de difficultés particulières (Figure 3) :

Figure 3 Convertisseur de tension négative

Voilà il reste à jouer du tournevis sur un potentiomètre pour régler le contraste.

On va maintenant voir comment commander cet écran.

Commande

Alors maintenant que nous avons présenté tous les éléments permettant de communiquer avec notre LCD (signaux de contrôles, registres, RAM et bus de donnée), la question est de savoir comment faire cohabiter tout cela pour lire et écrire sur l’afficheur.

Je vais détailler le principe d’une écriture, pour la lecture ce sera la même philosophie !

La datasheet du contrôleur donne les chronogrammes suivants pour une écriture (Figure 4) :

Figure 4 Opération d’écriture

Toutes les références de tensions hautes (V*H*) et basses (V*L*) sont des multiples de la tension de référence (5V), par exemple 0.7Vcc.

Les valeurs sont disponibles en page 48 de la datasheet.

On ne va pas se compliquer la vie et prendre comme référence haute 5V et basse 0V. Concrètement cela signifie positionner un 1 logique ou un 0 sur la sortie du port concerné du micro (pour le signal E, par exemple, ce sera RA2).

Pour ce qui est des timings (t*) les valeurs minimums varient de quelques nanosecondes jusqu’à la microseconde. Là encore pour ne pas s’embêter nous utiliserons une base de temps fixe de 1 microseconde pour être sûr de respecter les contraintes temporelles.

Note : pour les plus rigoureux, les valeurs sont disponibles page 49

Si on doit résumer le cycle d’écriture de la Figure 4 cela donne le pseudo code suivant :

  1. Mettre le signal RS à 0 pour une écriture dans le registre d’instruction ou à 1 pour le registre de donnée ;
  2. Mettre le signal R/W à 0 (écriture) ;
  3. Ecrire l’octet à envoyer (DB7..0) sur le port B du micro. On s’assure ainsi que les données seront valides au moment de l’écriture réelle en RAM ;
  4. Attendre 1 µs ;
  5. Mettre le signal E à 1 ;
  6. Attendre 1 µs ;
  7. Mettre le signal E à 0 ;
  8. Attendre 1 µs ;

Lors de la mise sous tension du LCD, celui-ci doit être initialisé. La datasheet indique qu’un circuit interne s’occupe de cette initialisation (page 23) :

An internal reset circuit automatically initializes the HD44780U when the power is turned on.

Cependant en y regardant de plus près on peut lire la note suivante :

Note: If the electrical characteristics conditions listed under the table Power Supply Conditions Using Internal Reset Circuit are not met, the internal reset circuit will not operate normally and will fail to initialize the HD44780U. For such a case, initialization must be performed by the MPU as explained in the section, Initializing by Instruction.

Et en fouillant encore un peu plus dans la datasheet on y trouve ces fameuses conditions de mises sous tension (Figure 3) :

Figure 5 Internal Power Supply Reset

Dans notre cas il est difficile de s’assurer que ces conditions sont respectées à chaque démarrage. Par ailleurs par défaut l’initialisation automatique est forcée en mode 8 bits sur une ligne (au lieu des 2 disponibles).

Nous allons donc procéder à une initialisation par instructions. Voici le pseudo code de cette initialisation :

  • Attendre plus de 15 ms ;
  • Envoyer l’instruction Function set  ;

RS

R/W

DB7

DB6

DB5

DB4

DB3

DB2

DB1

DB0

0

0

0

0

1

1

X

X

X

X

Note : les X sont utilisés pour indiquer que les bits ne sont pas significatifs et peuvent prendre la valeur 0 ou 1.

  • Attendre plus de 4.1 ms ;
  • Envoyer l’instruction Function set ;

RS

R/W

DB7

DB6

DB5

DB4

DB3

DB2

DB1

DB0

0

0

0

0

1

1

X

X

X

X

  • Attendre plus de 100 µs ;
  • Envoyer l’instruction Function set ;

RS

R/W

DB7

DB6

DB5

DB4

DB3

DB2

DB1

DB0

0

0

0

0

1

1

X

X

X

X

  • Envoyer l’instruction Function set en spécifiant le mode nombre de lignes et la taille de font ;

RS

R/W

DB7

DB6

DB5

DB4

DB3

DB2

DB1

DB0

0

0

0

0

1

DL

N

F

X

X

DL = 0 : 4 bits / DL = 1 : 8 bits

N = 0 : 1 ligne / N = 1 : 2 lignes

F = 0 : 5×8 dots ; F = 1 : 5×10 dots

  • Envoyer l’instruction Display ON ;

RS

R/W

DB7

DB6

DB5

DB4

DB3

DB2

DB1

DB0

0

0

0

0

0

0

1

1

C

B

C = 0 : pas de curseur (j’en reparlerai dans un autre billet).

B = 0 : pas de clignotement du curseur (j’en reparlerai dans un autre billet).

  • Envoyer l’instruction Display Clear ;

RS

R/W

DB7

DB6

DB5

DB4

DB3

DB2

DB1

DB0

0

0

0

0

0

0

0

0

0

1

  • Envoyer l’instruction Entry mode set ;

RS

R/W

DB7

DB6

DB5

DB4

DB3

DB2

DB1

DB0

0

0

0

0

0

0

0

1

I/D

S

I/D = 1 : incrémentation de l’adresse en DDRAM lorsqu’un caractère est envoyé ou lu sur l’afficheur (nous y reviendrons).

S = 0 : pas de décalage (j’en reparlerai dans un autre billet).

La liste complète des instructions est disponible en page 24, Table 6.

Un dernier point à souligner, chaque instruction a une durée d’exécution. Il est impératif d’attendre ce temps avant d’envoyer une autre instruction. Pour cela il y a deux méthodes. Soit on programme un temps d’attente, par exemple 2 ms (l’instruction la plus longue dure 1.52ms !) soit on vient lire un flag, le busy flag (BF). Lorsque celui-ci est à 1 cela signifie que le LCD est en cours de traitement et on doit attendre que ce flag soit mis à 0 avant d’envoyer la prochaine instruction.

La lecture du busy flag est la seule instruction autorisée lorsque le LCD est occupé à traiter l’instruction précédente :

Voici le pseudo code de lecture du busy flag :

  1. Mettre le registre de direction du PORT B en lecture ;
  2. Mettre le signal E à 1 ;
  3. Mettre le signal RS à 0 pour une écriture dans le registre d’instruction ;
  4. Mettre le signal R/W à 1 (lecture) ;
  5. Attendre 1 µs ;
  6. Mettre le signal E à 0 ;
  7. Attendre 1 µs ;
  8. Mettre le signal E à 1 ;
  9. Lire le PORT B, le bit de poids fort (bit 7) contient le busy flag ;
  10. Recommencer à partir du point 5 tant que le bit est à 1 ;
  11. Remettre le registre de direction du PORT B en écriture ;

Hello World

Initialisation du LCD

Pour coder en C la fonction d’initialisation du LCD, on va avoir besoin d’une fonction de temporisation et d’une fonction pour l’envoie des instructions.

Le compilateur C18 met à disposition un ensemble de fonctions de temporisations. Ces fonctions sont basées sur l’envoie d’instructions NOP (No OPeration = ne rien faire). Chaque instruction NOP dure 1 cycle d’instruction. En connaissant la durée du temps de cycle, lié à la fréquence du processeur et en envoyant un nombre défini d’instructions NOP on peut réaliser une fonction de temporisation.

Voici les fonctions disponibles :

  1. Delay1TCY() pour une temporisation de 1 temps de cycle ;
  2. Delay10TCYx( unsigned char unit ), basée sur des multiples de 10 ;
  3. Delay100TCYx( unsigned char unit ), basée sur des multiples de 100 ;
  4. Delay1KTCYx( unsigned char unit ), basée sur des multiples de 1000 ;
  5. Delay10KTCYx( unsigned char unit ), basée sur des multiples de 10000.

Dans notre cas, la fréquence processeur étant de 20 MHz, une instruction NOP prend un temps de cycle. Un cycle instruction est exécuté en 4 coups d’horloge interne (fetch-decode-execute). Pour plus d’information, se reporter à la page 63 de la [3] Datasheet pic 18F2550.

Nous pouvons donc en déduire la durée d’un cycle instruction :

Tcycle = 1 / (Fosc / 4) = 200 ns

Exemple : si on prend la fonction Delay10TCYx à laquelle on passe la valeur 1 en paramètre, le temps d’attente sera : T = 200 ns * 10 * 1 = 2 µs.

Voici le code d’une fonction qui attend le nombre de millisecondes passées en paramètre :


void Wait_k_ms(unsigned short kms) {

  while (--kms) {
     Delay1KTCYx(5); /* (4/20Mhz) * 5 * 1000 = 1ms */
  }
}

Voici maintenant la fonction complète d’initialisation du LCD en langage C :


//commands

//=================================

#define CLEAR         (0x01)
#define HOME          (0x02)
#define MODE_SET      (0x04)
#define DISPLAY       (0x08)
#define SET_FUNCTION  (0x20)
#define SET_DDRAM_ADD (0x80)

//options

//=================================

#define ADDRESS     (0x7F)

#define BUSY           (0x80)
#define INCREASE       (0x02)
#define DECREASE       (0x00)
#define SHIFTED        (0x01)
#define NO_SHIFTED     (0x00)
#define BLINK_ON       (0x01)
#define BLINK_OFF      (0x00)
#define CURSOR_ON      (0x02)
#define CURSOR_OFF     (0x00)
#define DISPLAY_ON     (0x04)
#define DISPLAY_OFF    (0x00)
#define DISPLAY_SHIFT  (0x08)
#define CURSOR_MOVE    (0x00)
#define RIGHT_SHIFT    (0x04)
#define LEFT_SHIFT     (0x00)
#define BITS_8         (0x10)
#define BITS_4         (0x00)
#define LINES_1        (0x00)
#define LINES_2        (0x08)
#define DOTS_10        (0x04)
#define DOTS_8         (0x00)
#define DIRECTION_ENABLE      (TRISAbits.TRISA0)
#define DIRECTION_RW          (TRISAbits.TRISA1)
#define DIRECTION_RS          (TRISAbits.TRISA2)

void InitLCD(void) {

  /* data port are outputs */
  TRISB = 0; /* PORTB en sortie */
  DIRECTION_ENABLE = 0; /* RA0 en sortie (signal enable) */
  DIRECTION_RW = 0; /* RA1 en sortie (signal RW) */
  DIRECTION_RS = 0; /* RA2 en sortie (signal RS) */

  Wait_k_ms(15);
  Send_Command(SET_FUNCTION | BITS_8);
  Wait_k_ms(5);
  Send_Command(SET_FUNCTION | BITS_8);
  Wait_k_ms(1);
  Send_Command(SET_FUNCTION | BITS_8);
  /* Function set */
  Send_Command(SET_FUNCTION | BITS_8 | LINES_2 | DOTS_8);
  /* Display on/off control */
  Send_Command(DISPLAY | DISPLAY_ON | CURSOR_OFF | BLINK_OFF);
  /* Display CLEAR */
  Send_Command(CLEAR);
  /* Entry mode set */
  Send_Command(MODE_SET | INCREASE | NO_SHIFTED);
}

Envoie de commande et de données

Voici le code pour l’envoie de commande :


#define ENABLE    (LATAbits.LATA0)
#define RW (LATAbits.LATA1)
#define RS (LATAbits.LATA2)

static void Send_Enable(void) {
  Wait_1us();
  ENABLE = 1;
  Wait_1us();
  ENABLE = 0;
  Wait_1us();
}

static void Send_Command(char cmd) {

  RS = 0; /* selects instruction register */
  RW = 0; /* selects write */
  PORTB = cmd ;
  Send_Enable();
}

Et enfin pour terminer voici le code pour l’envoie de caractères sur le LCD :

static void Send_Data(char data) {

  RS = 1; /* selects data register */
  RW = 0; /* selects write */
  PORTB = data ;
  Send_Enable();
  RS = 0 ;
}

Pour ceux qui se demandent comment le code ci-dessus marche, ce n’est pas bien compliqué

Pour envoyer le caractère ‘a’ par exemple il suffit d’appeler la fonction :

Send_Data('a') ;

Comment le LCD interprète cet appel de fonction ?

Le caractère passé en paramètre de la fonction est envoyé au LCD sous la forme d’un code ASCII : pour ‘a’ par exemple ce sera la valeur hexadécimale 61 soit 01100001 en binaire. La table 4 en page 17 de la datasheet du LCD nous donne la table de correspondance entre le code du caractère et son pattern contenu en ROM dont voici un extrait :

Note : Les codes caractères ne correspondent pas toujours aux codes ASCII. C’est valable pour les caractères les plus communément utilisés (a-z, A-Z, 0-9). La table 4 donne l’ensemble des caractères affichables et leur code correspondant. Dans le doute se référer à cette table.

Maintenant que le LCD est initialisé, et que l’on sait envoyer un caractère, nous allons envoyer la chaîne de caractère « Hello World » sur la première ligne de l’afficheur.

Voici la représentation des 2 lignes de l’afficheur en fonction de l’adresse en DDRAM (Figure 6)  :

Figure 6 Représentation de l’afficheur

Par défaut le premier caractère envoyé est à l’adresse 0 ce qui correspond au premier emplacement à gauche de la première ligne. Le second sera envoyé à l’adresse 1 et ainsi de suite jusqu’à l’adresse 27. Ceci est valable si lors de l’initialisation du LCD nous avons programmé une incrémentation du compteur d’adresse, ce qui est notre cas !


/* Entry mode set */
Send_Command(MODE_SET | INCREASE | NO_SHIFTED);

Remember ?

On peut noter que l’afficheur ne peut afficher que 16 caractères par lignes. Au-delà de l’adresse 15 les caractères ne seront donc pas visibles !

Comment ?

Nous y reviendrons dans le prochain billet en détaillant le décalage de caractères.

Pour écrire sur la deuxième ligne il faut se positionner explicitement à l’adresse 40 (en hexadécimal) par l’envoie de la commande dédiée : Set DDRAM address :

RS

R/W

DB7

DB6

DB5

DB4

DB3

DB2

DB1

DB0

0

0

1

ADD6 ADD5 ADD4 ADD3 ADD2

ADD1

ADD0

Les 7 bits de poids faibles ADD6:ADD0 servent à renseigner l’adresse, par exemple 0x40, et DB7 est le bit dédié à cette commande.

Le code C ci-dessous écrit le caractère ‘A’ sur la deuxième ligne :


#define SET_DDRAM_ADD (0x80)

Send_Command(SET_DDRAM_ADD + 0x40) ;
Wait_k_ms(2);
Send_Data('A') ;
while(1) ;

Dans l’exemple ci-dessus, on attend 2 ms avant d’envoyer la prochaine instruction mais il est plus sage de ne pas attendre inutilement et lire le busy flag.

Voici la fonction de lecture du busy flag :


static void WaitForBusyFlag(void) {

  volatile unsigned char data = 0;

  TRISB = 0xFF ;
  ENABLE = 1 ;
  RS = 0 ;
  RW = 1 ; /* selects read mode */

  do {
    DelayTCY() ;
    ENABLE = 0 ;
    DelayTCY() ;
    ENABLE = 1 ;
    data = PORTB;
  } while (data & 0x80); /* wait till the end of internal operation mode */

  TRISB = 0 ;
  RW = 0; /* selects write */
}

Et enfin pour afficher un Hello World :

void SendLCD(char *str) {

  unsigned char i = 0;
  /* Send each character to data port */
  while (str[i] != '') {
    Send_Data(str[i]);
    i++;
    WaitForBusyFlag();
  }
}
void main (void) {

  char str[] = "Hello World ! " ;

  InitLCD() ;
  SendLCD(str) ;
  while(1) ;
}

Et voilà le résultat ! (Figure 7) :

Figure 7 Hello World!

Ce billet un peu long a permis de poser les bases de l’interface d’un écran LCD 16×2.

Dans le prochain billet nous verrons comment faire défiler du texte, commander l’afficheur en mode 4 bits pour économiser des E/S et plus intéressant créer ses propres caractères (mode CGRAM) !

A bientôt !

Références :

[1] LCD – Wikipedia ;

[2] Datasheet HD44780 ;

[3] Datasheet pic 18F2550.

Publicités