Utiliser la puissance graphique de Qt pour les afficheurs LCD pilotés par un ST7789
La difficulté principale dans l'utilisation d'afficheurs LCD ou OLED consiste essentiellement en l'absence presque totale de librairies graphiques permettant de dessiner ou d'écrire du texte sur ces afficheurs. Les classes CLCD7789 et CLCD7789PaintDevice tentent d'apporter une réponse à ce besoin en se basant sur les capacités graphiques de Qt.
Exemples simples
Les quelques images ci-dessous sont des exemples de ce qui est facilement réalisable :
![]() |
![]() |
![]() |
![]() |
Matériel
Les classes CLCD7789 et CLCD7789PaintDevice ont été développées et testées au moyen de ce LCD :
-https://www.waveshare.com/2inch-LCD-Module.htm
Il existe bien sur beaucoup d'autres LCD supportés par le composant ST7789. Je donnerai plus loin dans cette page les directives de portage des classes vers d'autres LCD pouvant présenter des résolutions horizontales et verticales de valeurs différentes. Ce LCD est d'une taille de 2 pouces avec une résolution de 320 X 240 pixels.
Cablage
Le ST7789 est interfacé avec le Raspberry Pi via le bus SPI. Etant donné les transferts relativement importants sur ce bus pour rafraichir le LCD, je conseille que cet équipement soit le seul sur ce bus. En effet, lorsque vous rafraichissez le LCD, vous pouvez envoyer vers ce dernier jusqu'à 150 KOctets. Si vous le rafraichissez une dizaine de fois par seconde, cela fait pas mal de traffic. Cela n'est pas lié à Qt, mais au LCD lui même. Ceci dit, ce genre d'afficheur n'est pas non plus destiné à des rafraichissements à haut débit.
Ci-dessous un schéma de cablage entre la prise 40 broches GPIO du Raspberry Pi et la platine du LCD :

Notez que les liaisons SPI0 MOSI/DIN et SPI0 CLK/CLK sont imposées par le bus SPI. La liaison GPIO18 PWM/BL est imposée car la broche BL contrôle l'éclairage du LCD qui doit être piloter en PWM, hors la seule broche capable de générer du PWM sur le port GPIO est la broche GPIO18. Les liaisons CS, DC et RST peuvent être reliées à d'autres sorties numériques du port GPIO. Notez également que je n'utilise pas le broche SPI0 CS0 pour la commande de la ligne CS du bus SPI. Il est préférable de faire ce choix afin de contrôler nous même les basculements de CS qui seraient plus fréquents si on laissai faire le pilote SPI du Raspberry PI.
Selon ce cablage le code complet de création et d'initialisation des objets requis est alors le suivant :
CSPIBus* m_pSPIBus;
CLCD7789* m_pLCD7789;
m_pDriver = new C2835Driver();
m_pSPIBus = new CSPIBus(m_pDriver);
m_pLCD7789 = new CLCD7789(m_pSPIBus,
GPIO_5,
SPI_CS_POLARITY_NEGATIVE,
SPI_BIT_ORDER_MSBFIRST,
SPI_CLOCK_DIVIDER_32,
SPI_DATA_MODE_0,
GPIO_6,
GPIO_13);
m_pDriver->Initialize();
m_pSPIBus->Open(SPI_BIT_ORDER_MSBFIRST,
SPI_CLOCK_DIVIDER_256,
SPI_DATA_MODE_1)
m_pLCD7789->Initialize();
En interne la fonction "Initialize"" de la classe CLCD7789 exécute donc ces appels (les variables m_eRESETPin et m_eDCPin ayant été fixées aux valeurs GPIO_13 et GPIO_6 par le constructeur) :
if(!m_pSPIBus->Driver()->SetDigitalBitFunction(m_eRESETPin, GPIO_FUNCTION_OUTPUT)) return false;
if(!m_pSPIBus->Driver()->SetDigitalBitFunction(m_eDCPin, GPIO_FUNCTION_OUTPUT)) return false;
// Configure the PWM for BL : Always GPIO18 : Set it to ~ 90% of luminosity
if(!m_pSPIBus->Driver()->EnablePWMOnGPIO18(PWM_CLOCK_DIVIDER_16, PWM_MODE_MARKSPACE, 1024, 920)) return false;
Gestion des couleurs
Ce LCD et son pilote ST7789 peuvent coder les couleurs selon deux normes : RGB565 et RGB18. La norme RGB565 autorise 65 536 couleurs, la norme RGB18 autorise 262 144 couleurs.
Les classes CLCD7789 et CLCD7789PaintDevice ne supportent que la norme RGB565. La raison à cela est que la norme RGB18 impose 3 octets par pixels au lieu de 2 pour la norme RGB565. Cela augmente le traffic sur le bus SPI de 50%. Hors, sur des afficheurs à peine plus grands qu'un timbre poste, la différence de qualité des couleurs entre ces deux normes est à peine visible. C'est la raison du choix de la norme RGB565. En plus, cela simplifie le code.
De plus le composant ST7789 a un codage bien à lui de la norme RGB565. Une fonction de la librairie nommée "Color565ToLCDColor" encode les couleurs RGB565 selon le codage attendu par le LCD
Qt et la plupart des librairies graphiques encodent les couleurs sur 24 bit (8 bit par couleur fondamentale). A titre d'exemple, c'est le codage de base des couleurs par la classe "QColor" de Qt. il existe dans la classe CLCD7789 une fonction nommée "RGBToColor565" qui convertie les couleurs 24 bit en RGB565.
Enfin la classe CLCD7789 exporte 17 constantes codées en RGB565 pour les couleurs de base :
const RGB565 CLCD7789::Black = 0x0000;
const RGB565 CLCD7789::White = 0xFFFF;
const RGB565 CLCD7789::Red = 0xF800;
const RGB565 CLCD7789::DarkRed = 0x8000;
const RGB565 CLCD7789::Green = 0x07E0;
const RGB565 CLCD7789::DarkGreen = 0x0400;
const RGB565 CLCD7789::Blue = 0x001F;
const RGB565 CLCD7789::DarkBlue = 0x0010;
const RGB565 CLCD7789::Cyan = 0x07FF;
const RGB565 CLCD7789::DarkCyan = 0x0410;
const RGB565 CLCD7789::Magenta = 0xF81F;
const RGB565 CLCD7789::DarkMagenta = 0x8010;
const RGB565 CLCD7789::Yellow = 0xFFE0;
const RGB565 CLCD7789::DarkYellow = 0x8400;
const RGB565 CLCD7789::Gray = 0xA514;
const RGB565 CLCD7789::DarkGray = 0x8410;
const RGB565 CLCD7789::LightGray = 0xC618;
Portage vers d'autres LCD
La seule figure imposée pour porter ce code vers d'autres LCD est que ces LCD doivent aussi être pilotés par un ST7789. Le codage des couleurs ne devrait pas requérir de modifications. Les seules modifications concernent la définition des constantes "CLCD7789::LCDWidth et CLCD7789::LCDHeight" définies par la classe CLCD7789. En effet, tout ce qui concerne l'accès aux registres du ST7789 est directement issu des librairies fournies par Waveshare. Ils ont en effet, fait un job assez complet. Hors, à ma connaissance, mais je peux me tromper... Leur librairie pour les ST7789 est la même pour toutes les tailles de LCD qu'ils proposent. Si ce n'est pas le cas, on pourra modifier les fonctions "InitRegister, SetWindow et Clear" en reprenant le code de Waveshare et en l'adaptant en remplacant leurs appels SPI par les appels SPI de la librairie cpp2835. La fonction "DisplayPaintDevice" très spécifique à cette librairie est à peu de choses près équivalente à la fonction "Clear" à la différence qu'au lieu d'envoyer une couleur unique pour tous les pixels, elle envoie une image issue de la classe CLCD7789PaintDevice.
Dessiner et/ou écrire sur le LCD
Comme mentionné dans le pragraphe d'introduction de cette page, la vraie difficulté dans l'utilisation de LCD de ce type est de disposer d'une librairie graphique nous permettant de ne pas avoir à traiter pixel par pixel. Les quelques librairies graphiques que j'ai pu identifier sont trop restreintes. Celle fournie par Waveshare, même si elle marche bien en est un bon exemple. Et je ne parle pas des polices de caractères...
Comme cette classe fait partie d'une librairie dédiée à Qt, on peut alors décider d'utiliser la librairie graphique de Qt pour tracer sur le LCD. Le principe est le suivant.
Pour dessiner dans Qt on utilise la classe "QPainter". Cette classe dessine dans un objet d'une classe héritant de la classe "QPaintDevice". Beaucoup de classes héritent de "QPaintDevice". Par exemple, un "QWidget"". Parmi les classes héritant de "QPaintDevice", l'une d'entre elle propose une solution à notre besoin.
La classe "QImage" qui hérite de "QPaintDevice" permet d'utiliser toutes les méthodes de dessin de "QPainter" pour dessiner "Off Screen". Et cette classe a de bonnes manières. En effet, elle exporte une méthode nommée "bits" qui donne accès au buffer de pixels de l'image dessinée. Donc on dessine dans "QImage"" et pour mettre à jour le LCD il nous suffit alors de transférer (moyennant la gestion des couleurs telle que décrite plus haut) le buffer retourné par "bits" au LCD.
L'idée de base étant posée, comment cela se passe-t-il en pratique ? La librairie exporte une classe nommée CLCD7789PaintDevice. Cette classe hérite de la classe Qt "QImage", donc on peut dessiner dans cette classe via les centaines de fonctions de "QPainter". La classe CLCD7789PaintDevice n'ajoute qu'une seule fonction à la classe "QImage" nommée "DisplayOnLCD". Cette méhode récupère le buffer de l'image créée par "QPainter", passe les pixels dans la moulinette d'encodage en RGB565, puis apelle la fonction "DisplayPaintDevice" de la classe CLCD7789 qui transfère tout ce petit monde au LCD.
Vu par l'utilisateur de la librairie, tout ce qu'il y a à faire est de dessiner via Qt dans CLCD7789PaintDevice, puis, quand le tracé est terminé de simplement appeler la fonction "DisplayOnLCD". Et c'est tout !!! Exemple : Dessin d'un rectangle rouge situé à 10 pixels de chaque bord du LCD :
CLCD7789PaintDevice* m_pLCD7789PaintDevice = new CLCD7789PaintDevice(m_pLCD7789);
// On fabrique un QPainter pour ce CLCD7789PaintDevice
QPainter Painter(m_pLCD7789PaintDevice);
On dessine via le QPainter
Painter.fillRect(QRect(10,10,220,300), QColor(255,0,0,255)); // Du rouge
// Et on met à jour le LCD à ce que l'on vient de dessiner....
m_pLCD7789PaintDevice->DisplayOnLCD();
Bref, tout l'interêt de la chose est que vous disposez d'une des librairies graphiques les plus vastes et réputées pour dessiner sur votre LCD.
Orientation du LCD
Un LCD rectangulaire comme celui utilisé ici peut tout aussi bien être utilisé en position horizontale que verticale. Par défaut il est vertical, soit 240 pixels horizontaux et 320 pixels verticaux. Que faire si l'on veut l'inverse ?
Deux options sont possibles :
La première cconsiste à faire une rotation dans la mémoire du LCD. C'est une option peu pratique car elle complexifie le code de gestion du ST7789 et de ce fait peut ralentir les mises à jour.
La seconde consiste à utiliser les capacités de transformations de coordonnées de la classe "QPainter" de Qt. Le grand avantage de cette méthode est que vous pouvez dessiner dans toutes les orientations avec un code restant à peu de choses près le même. Ci-dessous un extrait de code qui fait faire une rotation de 90° dans le sens horaire et recale l'origine des axes X et Y en haut à gauche du LCD :
// Inverser width et height et recalage de l'origine en haut à gauche
Painter.translate(QPoint(m_pLCD7789PaintDevice->Width(), 0));
Painter.rotate(90);
// Pour ne pas se planter on récupère les coordonnées maximales ddans les 2 sens
int nLCDWidth = m_pLCD7789PaintDevice->height(); // La hauteur est devenue la largeur
int nLCDHeight = m_pLCD7789PaintDevice->width(); // ET inversement
// On dessine
Painter.fillRect(QRect(0,0,nLCDWidth,nLCDHeight), QColor(0,0,0,255)); // Fond noir
QPen pen(QColor(255,255,255,255)); // Texte blanc
Painter.setPen(pen);
Painter.setFont(QApplication::font()); // Avec la police de base
Painter.drawText(10, 10, "Hello World"); // On ecrit en haut à gauche à 10 pixels des bords
// Et on met à jour le LCD à ce que l'on vient de dessiner....
m_pLCD7789PaintDevice->DisplayOnLCD();



