CH32V003 : I2C et afficheur LCD

Ce que nous allons faire - Prérequis

Jusqu'à maintenant, nous n'avons abordé que des sujets d'une complexité très limitée et tout notre code tenait dans le fichier main.c. Ce n'est évidemment pas le cas de la plupart des applications réelles.

C'est pourquoi ce cours prendra l'exemple de l'utilisation d'un module d'affichage LCD comme ceux qu'on voit sur les terminaux de paiement par carte pour aborder la dimension méthodologique du développement. Nous verrons dans ce cours :

  • ce qu'est un afficheur LCD et comment l'utiliser

  • comment analyser un système complexe

  • comment traduire cette analyse en code

La dimension purement technique passera au second plan. Nous réutiliserons la base du cours "CH32V003 : I2C et EEPROM" en remplaçant l'EEPROM par le module d'affichage LCD et tout le nouveau code vous sera fourni afin de vous permettre de vous concentrer sur la dimension méthodologique.

Les prérequis de ce cours sont :

  • Avoir suivi le cours CH32V003 : I2C et EEPROM.

  • Disposer d'un module LCD2004 tel que recommandé dans les pré-requis, un bouton poussoir et un condensateur de 100nF.

Le matériel

Réalisez le câblage décrit ci-dessous. Nous utiliserons un bouton poussoir pour déclencher un compteur et mettre à jour l'affichage.

Le code

Créez un nouveau projet, modifiez system_ch32v00x.c, puis décompactez cette archive dans le répertoire User de votre projet. Cette opération écrasera le fichier main.c et en ajoutera quelques autres.

Nous allons maintenant nous intéresser à la démarche qui a permis d'arriver à ce code, étape par étape.

Point de départ de l'analyse

La démarche d'analyse commence par la collecte d'informations sur le système à modéliser. C'est une démarche fortement itérative, donc il nous faudra affiner progressivement.

Dans notre cas, nous analysons un objet physique, un module LCD d'affichage de texte, donc nous partirons de ce que nous voyons :

L'élément le plus évident est l'écran LCD, repéré A sur l'image. On ne les voit pas sur l'image, mais les points qui composent cet écran sont visibles lorsqu'il est allumé. Ils sont en nombre évidemment fini, ce qui signifie que le choix de l'écran conditionne le nombre de lignes qu'on peut afficher et le nombre de caractères par ligne.

De l'autre côté du circuit imprimé, on remarque 5 bosses noires repérées B. Il s'agit de circuits intégrés montés directement sur le circuit imprimé et protégés par de la résine époxy. L'un d'eux est le contrôleur d'affichage et les 4 autres pilotent les segments de l'écran. Le rôle du contrôleur est d'offrir au système qui utilise le module d'affichage un ensemble de fonctions de haut niveau telles que effacer l'écran positionner le curseur ou écrire un caractère.

Sur notre module LCD, le contrôleur d'affichage est un SPLCD780 (data sheet). Il fait partie d'une grande famille de contrôleurs LCD descendant du HD44780, ce qui fait que quasiment tous les contrôleurs LCD en mode texte sont compatibles entre eux. Le code de ce cours fonctionnera donc aussi bien avec un autre module LCD2004, ou même un 1602 (2 lignes de 16 caractères), un 1601 ou un 0802. Seule la taille de l'écran, et donc le nombre de circuits intégrés nécessaires pour le piloter, changent.

La dernière chose qui saute aux yeux est le petit circuit imprimé noir soudé sur le module LCD, repéré C, qui est un adaptateur permettant de communiquer via I2C avec le contrôleur d'affichage, qui est normalement limité à une interface parallèle sur 4 ou 8 bits. Cet adaptateur est basé sur le PCF8574 (data sheet).

Notez au passage que le carré bleu avec une vis blanche est un potentiomètre ajustable servant à régler le rétro-éclairage de l'écran. S'il est difficile à lire, vous pouvez modifier ce réglage dans un sens ou dans l'autre avec un tournevis pour obtenir un meilleur résultat.

En résumé, vu depuis le micro-contrôleur, un module d'affichage LCD est composé de :

  • un écran LCD d'une taille donnée,

  • un contrôleur offrant des fonctions de haut niveau pour gérer l'affichage,

  • une interface de communication pour relier le module au micro-contrôleur.

Cette première étape d'analyse nous permet d'arriver à l'ébauche de modèle suivante :

Abstraction

L'activité d'analyse est un exercice plus philosophique que technique car elle exige de s'intéresser à la nature des choses. Ces choses qui composent notre module sont des objets concrets, par exemple un contrôleur SPLC780 sous son dôme de résine, ou un écran vert de 4 lignes de 20 caractères. C'est précis et palpable.

Idéalement, nous voulons que notre code soit facile à adapter à des situations comparables. Nous ne voulons à aucun prix devoir tout réécrire pour utiliser un autre contrôleur LCD ou un écran d'une taille différente. Ceci implique de s'intéresser aux invariants communs à toute une famille d'objets comparables, par exemple tous les types d'écrans LCD destinés à l'affichage de texte. Ces invariants sont représentés par la notion de classe.

Les objets concrets sont des manifestations physiques de leur classe, on dit qu'un objet est une instance d'une classe. La balle en caoutchouc bleue avec laquelle joue mon chien est une instance de la classe "balle pour chien", classe dont "matière" et "couleur" sont des attributs.

Par exemple, tous les écrans LCD qui nous intéressent sont caractérisés par un nombre de lignes et un nombre de caractères par ligne. Ces caractéristiques sont appelées attributs de la classe écran. Un écran 2004 affectera les valeurs 4 et 20 respectivement à ces attributs et un écran 0802 leur affectera les valeurs 2 et 8.

Les attributs représentent souvent l'état d'un objet (ex. position du curseur sur l'écran), c'est-à-dire des données variables au cours de la vie de l'objet, et pas seulement ses propriétés naturelles, qui sont quant à elles fixes.

De la même façon, tous les contrôleurs LCD mettent à disposition du micro-contrôleur les opérations d'effacement d'écran, de positionnement du curseur et d'écriture d'un caractère. Ces opérations sont appelées les méthodes de la classe contrôleur. Bien entendu, si du point de vue du micro-contrôleur toutes ces opérations sont disponibles quel que soit le contrôleur, chaque contrôleur les réalisera de façon potentiellement différente.

On distingue 2 types de classes, les classes concrètes, qui représentent des objets existants, et les classes abstraites qui représentent des familles d'objets apparentés. Par exemple, la classe "interface de communication" est abstraite et les classes "interface I2C" et "interface parallèle" sont 2 classes concrètes apparentées à "interface parallèle".

On dira que "interface I2C" hérite (ou "est une spécialisation", ou "dérive") de "interface de communication". Cela signifie que "interface I2C" a tous les attributs et toutes les méthodes de "interface de communication" et peut en plus en avoir qui lui sont propres si besoin. Cela signifie aussi que si un module de l'application attend en paramètre une "interface de communication", il pourra travailler aussi bien avec une "interface I2C" qu'avec une "interface parallèle" puisqu'il n'a besoin d'aucun attribut ni d'aucune méthode autres que ceux de "interface de communication".

Bien entendu, on peut avoir plusieurs niveaux d'héritage. "Balle avec laquelle joue mon chien" hérite de "balle pour chien" qui hérite de "jouet pour chien" qui hérite à son tour de "accessoires pour animaux de compagnie". Dans la pratique, on a donc affaire à des arborescences de classes (des arbres généalogiques).

Pour savoir si et où on doit introduire des classes abstraites, on utilise la technique du "et si".

Et si mon micro-contrôleur avait plus de lignes de GPIO disponibles et que je veuille accélérer la communication avec le contrôleur LCD, je pourrais utiliser son interface parallèle à la place d'une liaison I2C. Donc mon code doit pouvoir communiquer avec le contrôleur LCD de manière indépendante de l'interface de communication. Donc je dois avoir une classe abstraite "interface de communication" qui offre les méthodes dont mon code a besoin et 2 classes dérivées concrètes "interface I2C" et "interface parallèle".

Et si le module LCD avec lequel j'ai fait mon prototype était en rupture de stock et qu'on me demande d'en utiliser un autre avec un contrôleur LCD différent et incompatible, il faudrait que mon code soit organisé de manière indépendante du contrôleur LCD utilisé. Donc je dois avoir une classe abstraite "contrôleur LCD" qui offre les méthodes dont mon code a besoin et une classe dérivée par contrôleur effectivement utilisé, par exemple "contrôleur SPLC780".

Et si on me demandait d'utiliser un module LCD 1602 à la place du 2004, en conservant le même contrôleur LCD et la même interface de communication, tout ce que ça changerait, c'est la taille de l'écran. Donc je n'ai pas besoin d'une classe différente quand la taille change, ça affecte juste les valeurs des attributs de ma classe "écran LCD".

Le diagramme ci-dessous récapitule l'ensemble de ces cogitations.

Diagrammes de classes

Pour décrire des classes et leurs relations (héritage ou associations), on utilise des diagrammes de classes basés sur la notation UML (Unified Modeling Language).

UML est un très gros morceau car il prétend couvrir tous les aspects de la modélisation dans les moindres détails. Or, qui trop embrasse mal étreint. A porter trop de détails sur un diagramme, on le rend illisible et donc inutile. Pourtant, documenter les grandes lignes d'une application sous forme de diagrammes est une aide précieuse pour un nouveau développeur qui doit prendre en charge une application existante... Ou pour son propre développeur qui doit la maintenir après avoir travaillé pendant des mois sur un autre projet.

Un autre problème posé par un niveau de détail poussé est que l'être humain n'est pas doué pour la divination et qu'il aborde les problèmes plutôt de manière itérative. Les détails nécessaires à une modélisation complète n'apparaissent donc qu'au fil du développement, à mesure qu'on découvre ce qu'on n'a pas été capable d'anticiper magiquement. Pire : quand on cherche à anticiper sans base concrète pour challenger ses idées, on commet très souvent des erreurs totalement contre-productives.

Enfin, on distingue 2 niveaux de modèles, d'une part des modèles qu'on pourrait qualifier de "conceptuels" ou "fonctionnels", qui formalisent l'analyse du problème ou la conception générale de la solution et qui sont destinés à faciliter la communication entre des personnes non-techniques (souvent appelées "le métier") et des analystes ; et d'autre part, des modèles qu'on pourrait qualifier de "physiques" ou "d'implémentation" destinés uniquement aux développeurs et prenant en compte les aspects techniques du projet (technologies, infrastructure, architecture).

Modèle conceptuel

Nous pouvons traduire notre diagramme précédent (avec classes abstraites) en un diagramme de classes réalisé avec la notation UML et ne comportant que ce qui sera nécessaire à notre application, à savoir une interface I2C, un contrôleur LCD SPLC780 et un écran LCD de 4 lignes de 20 caractères. Je rappelle que ce diagramme est destiné à la communication avec des non-informaticiens et que tout ce qui touche aux détails d'implémentation n'est pas pertinent.

Dans ce diagramme, les classes sont représentées par des boîtes à 3 étages. L'étage du haut contient le nom de la classe, l'étage intermédiaire la liste de ses attributs et l'étage du bas la liste de ses méthodes. Une classe peut ne pas avoir d'attributs et/ou de méthodes. En cas d'héritage, les attributs et méthodes de la classe mère ne sont pas répétés.

Les noms d'attributs et de méthodes sont préfixés par un caractère pour indiquer leur visibilité. Un "+" signifie que l'attribut ou la méthode est public, c'est-à-dire que n'importe quelle partie de l'application peut l'utiliser ; un "-" signifie que l'attribut ou la méthode est privé, c'est-à-dire utilisable uniquement par les méthodes de la classe elle-même ; enfin, un "#" signifie que l'attribut ou la méthode est protégé, c'est-à-dire utilisable uniquement par la classe elle-même et toutes ses classes dérivées. Dans ce diagramme, seuls les attributs et méthodes publics sont utiles.

Les lignes reliant la classe LCDModule à AbstractLink, AbstractController et LCDDisplay représentent des associations et les "1" figurant à chaque extrémité de ces lignes indiquent la cardinalité de l'association. On distingue plusieurs types d'associations indiqués par des "décorations" au point de départ de la ligne, mais il n'y a pas d'intérêt à rentrer dans ce niveau de détail pour ce diagramme.

Les lignes reliant les classes abstraites à leurs classes dérivées ont une flèche vide qui pointe vers la classe mère. C'est à ce symbole qu'on reconnaît les relations d'héritage.

Je ne l'ai pas fait ici, mais il faut faire figurer les paramètres des méthodes et leur type à chaque fois que ça a une importance du point de vue métier/fonctionnel. En fait, on représente sur ce modèle le contrat passé entre le métier (demandeur de l'application) et les développeurs qui vont réaliser le projet.

Modèle physique

Une fois d'accord avec le métier sur le fonctionnel, le développeur va devoir réfléchir à la manière dont il va implémenter la solution. Lorsqu'un langage objet tel que C++ est utilisé, on va rester très proche du modèle conceptuel car le langage de programmation supporte les concepts utilisés par la modélisation.

Cependant, dans notre cas, le langage C ne nous offre que des structures, des pointeurs sur des fonctions et des pointeurs void pour transcrire les concepts du développement orienté objet. Le modèle physique va donc être adapté pour tenir compte de ces contraintes.

Il était cependant extrêmement important de passer par l'étape précédente car sans vision correcte de la nature des éléments qui entrent en jeu dans la solution et de leurs relations, il est impossible de produire un code correct, orienté objet ou pas.

Pour notre exemple, je vous propose le modèle suivant :

Dans un objet, on a à la fois des attributs et des méthodes. Les attributs sont propres à l'instance particulière de l'objet, mais les méthodes sont partagées avec tous les autres objets de la même classe. Dans un langage objet, c'est le compilateur qui s'occupe de faire le tri. En C, c'est le développeur qui s'en charge.

Dans le modèle conceptuel, on a vu que la classe I2CLink contient un attribut et implémente toutes les méthodes de la classe abstraite AbstractLink. De manière générale, toute classe dérivée de AbstractLink aura des données de configuration (ici, l'adresse I2C), éventuellement des données d'état (ex. l'interface a t-elle été initialisée) et une implémentations des méthodes attendues (le contrat de AbstractLink).

Pour reproduire la même chose en C, il va falloir définir une structure regroupant des pointeurs sur les fonctions imposées par AbstractLink. On va ensuite devoir créer une variable globale existant en un seul exemplaire (ça s'appelle un singleton) du type de cette structure et pour laquelle on initialisera les pointeurs de fonctions avec leur implémentation pour I2C. Cette variable est un singleton car les fonctions correspondantes sont valables pour tous les objets "interface I2C" utilisés par l'application (on pourrait très bien avoir plusieurs modules LCD avec des adresses I2C différentes).

A ce stade, on n'a que les méthodes, pas les données de configuration. Or, on veut que notre classe AbstractLink soit valable pour tous les types d'interfaces de communication, ce qui signifie que les types des données de configuration de la liaison seront différents. Cela signifie que nous aurons dans notre modèle physique un niveau supplémentaire par rapport au modèle conceptuel pour "recoller" attributs et méthodes de manière générique.

Ainsi, dans notre modèle physique, la classe (= structure C) LCDLink contient un pointeur sur la structure de configuration (LCDI2CConfig) et un pointeur sur une structure préalablement initialisée avec les pointeurs vers les méthodes (LCD_I2C_LINK_IMPL). La généricité de LCDLink est assurée par le fait que le pointeur sur la configuration est de type void* et par le fait que les pointeurs vers les méthodes sont stockés dans une structure LCDLinkImpl identique pour tous les types d'interface de communication.

Chaque méthode de LCDLinkImpl recevra en premier paramètre un pointeur sur l'instance de LCDLink, ce qui lui permet d'accéder aux données de configuration et aux autres méthodes de l'implémentation.

Ca a l'air compliqué dit comme ça, mais c'est beaucoup plus simple quand on regarde le code car on se rend alors compte qu'on fait du Lego en emboîtant des structures les unes dans les autres, soit directement, soit sous forme de pointeurs.

Pour le contrôleur, c'est plus simple car il n'y a pas d'attributs dans la classe LCDController. On a donc uniquement un singleton par instance de LCDController (ici, LCD_SPLC780_CONTROLLER) avec les pointeurs vers les différentes méthodes.

Pour LCDDisplay, gros soulagement, il n'y a que des attributs et pas de méthodes, donc une simple structure fait l'affaire. Le tableau ci-dessous indique la correspondance entre sources C et modèle physique.

Modèle Source (.c et .h) Type ou singleton
LCDModule txt-module TxtModule (typedef struct)
LCDDisplay txt-display TxtDisplay (typedef struct)
LCDController txt-abstract-controller TxtController (struct)
LCD_SPLC780_CONTROLLER txt-splc780-controller TXT_SPLC780_CONTROLLER (singleton)
LCDLink comm-link-abstract CommLink (typedef struct)
LCDLinkImpl CommLinkImpl (typedef struct)
LCDI2CConfig comm-link-pcf8574 CommLinkPCF8574Config (typedef struct)
LCD_I2C_LINK_IMPL COMM_LINK_PCF8574_IMPL (singleton)

Notez ici quelques différences entre le modèle physique et le code : des fichiers et classes commençant par "Txt" au lieu de "LCD", ou CommLink au lieu de LCDLink.

Ceci est dû au fait que nous sommes des êtres humains et que nos capacités de traitement sont limitées. C'est donc en passant à l'implémentation que nous prenons conscience de choses que nous n'avons pas anticipées lors de l'analyse. C'est bien pour cette raison que toutes les approches agiles sont itératives.

Ici, c'est en passant à l'implémentation que j'ai réalisé qu'il existe aussi des modules d'affichage LCD en mode graphique, alors que mon code ne concerne que ceux en mode texte. J'ai donc jugé utile de tenir compte de ce fait.

Ensuite, comme nous le verrons dans le cours CH32V003 : afficheur graphique OLED, tous les modules d'affichage ont les mêmes besoins de communication. D'autre part, l'utilisation du PCF8574 n'est pas le seul moyen de communiquer via I2C avec un module d'affichage. J'ai donc dû revoir mes conventions de nommage pour tenir compte de ce double besoin de généricité et de précision.

Évidemment, dans la pratique, vous devez impérativement mettre à jour votre modèle pour que la documentation reste fidèle à l'implémentation. Cependant, si je l'avais fait ici, je n'aurais pas eu l'occasion de vous donner ces explications pourtant essentielles.

Ce que nous avons appris

Vous savez maintenant :

  • Analyser un problème en procédant par étapes

  • Créer un diagramme de classe en fonction de sa destination

  • Traduire l'analyse en langage C

  • Utiliser un module d'affichage LCD en mode texte


Copyright (c) 2024 Vincent DEFERT - Tous droits réservés