mercredi 20 novembre 2019

Afficheur OLED SSD1306 : comparons



Afficheur OLED SSD1306 : comparons


Le but de cet article n'est pas de faire un nième tutorial sur les afficheurs OLED SSD1306, mais plutôt de faire une comparaison des différentes librairies existantes.

Comme vous l'avez peut-être remarqué la librairie AdaFruit occupe une place phénoménale en mémoire RAM. Il existe heureusement une alternative.

J'ai déjà parlé de ces petits afficheurs :
https://riton-duino.blogspot.com/2018/09/lcd-tft-et-arduino.html
Voir  4. Les petits afficheurs OLED

1. Câblage

Le câblage est simple (je ne vais pas vous faire un schéma) :
  • VCC sur 3.3V de l'ARDUINO
  • GND sur GND de l'ARDUINO
  • SDA sur A4 de l'ARDUINO
  • SCL sur A5 de l'ARDUINO
J'ai utilisé un afficheur I2C de 128x64 pixels, comme sur la photo ci-dessus.

Pour vérifier l'adresse I2C  de l'afficheur (en général 0x3C), il suffit d'utiliser ce sketch :
https://gist.github.com/tfeldmann/5411375

2. Les librairies

Dans ce comparatif nous allons essayer d'écrire une application affichant les objets suivants :
  • un titre centré en haut de l'écran
  • un texte justifié à gauche
  • un texte justifié à droite
  • un texte suivi d'une valeur, grande taille, justifié à gauche
  • un indicateur de capacité de batterie en haut à gauche
  • un logo en haut à droite
L'indicateur de capacité batterie est un rectangle de 14 ou 16 pixels de haut, 6 de large, plus ou moins plein. Si la capacité batterie est supérieure à 10%, l'affichage est fixe, sinon il clignote.


Quand ces applications seront écrites nous comparerons la facilité de mise en œuvre et l'occupation mémoire.

1.1. Librairie AdaFruit

Cette librairie peut être récupérée ici :
https://github.com/adafruit/Adafruit_SSD1306

Voici le sketch :

#define SSD1306_128_64

#include <Adafruit_SSD1306.h>

Adafruit_SSD1306 display(-1);

void setup()
{
  Serial.begin(115200);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.setTextColor(WHITE);
}

void loop()
{
  display.clearDisplay();
  printCentered("SSD1306 TUTO", 0, 1);
  displayLogo(0, 100);
  printLeft("LEFT JUSTIFIED", 20, 1);
  printRight("RIGHT JUSTIFIED", 30, 1);
  printValue(100, 50, 2);
  displayBatteryLevel(0, 0, 0);
  displayLogo(112, 0);
  display.display();
  delay(500);
}

void printLeft(const char *s, int y, int size)
{
  display.setTextSize(size);
  display.setCursor(0, y);
  display.print(s);
}

void printRight(const char *s, int y, int size)
{
  int16_t x1, y1;
  uint16_t w, h;

  display.setTextSize(size);
  display.getTextBounds(s, 0, 0, &x1, &y1, &w, &h);
  display.setCursor(display.width() - w, y);
  display.print(s);
}

void printCentered(const char *s, int y, int size)
{
  int16_t x1, y1;
  uint16_t w, h;

  display.setTextSize(size);
  display.getTextBounds(s, 0, 0, &x1, &y1, &w, &h);
  display.setCursor((display.width() - w) / 2, y);
  display.print(s);
}

void printValue(int value, int y, int size)
{
  char s[20];
  int16_t x1, y1;
  uint16_t w, h;

  sprintf(s, "VALUE: %d", value);
  display.getTextBounds(s, 0, 0, &x1, &y1, &w, &h);
  display.setTextSize(size);
  display.setCursor(0, y);
  display.print(s);
}

static const unsigned char PROGMEM logo16_glcd_bmp[] =
{ B00000000, B11000000,
  B00000001, B11000000,
  B00000001, B11000000,
  B00000011, B11100000,
  B11110011, B11100000,
  B11111110, B11111000,
  B01111110, B11111111,
  B00110011, B10011111,
  B00011111, B11111100,
  B00001101, B01110000,
  B00011011, B10100000,
  B00111111, B11100000,
  B00111111, B11110000,
  B01111100, B11110000,
  B01110000, B01110000,
  B00000000, B00110000
};

void displayLogo(int x, int y)
{
  display.drawBitmap(x, y,  logo16_glcd_bmp, 16, 16, WHITE);
}

void displayBatteryLevel(int batteryLevel, int x, int y)
{
  static bool flash = false;
  if (flash || batteryLevel > 10) {
    display.drawRect(x, y, 6, 16, WHITE);
    display.fillRect(x, y + ((16 - batteryLevel / 6)), 6, batteryLevel / 6, WHITE);
  }
  flash = !flash;
}


1.2. Librairie Greiman

Cette librairie peut être récupérée ici :
https://github.com/greiman/SSD1306Ascii.git

Voici le sketch :

#include "SSD1306Ascii.h"
#include "SSD1306AsciiAvrI2c.h"

SSD1306AsciiAvrI2c display;

void setup()
{
  Serial.begin(115200);
  display.begin(&Adafruit128x64, 0x3C);
  display.setFont(System5x7);
}

void loop()
{
  printCentered("SSD1306 TUTO", 0, 1);
  displayLogo(0, 100);
  printLeft("LEFT JUSTIFIED", 2, 1);
  printRight("RIGHT JUSTIFIED", 3, 1);
  printValue(100, 6, 2);
  displayBatteryLevel(0, 0, 0);
  displayLogo(112, 0);
  delay(500);
}

void printLeft(const char *s, int y, int size)
{
  if (size == 1) {
    display.set1X();
  }
  else {
    display.set2X();
  }
  display.setCursor(0, y);
  display.print(s);
}

void printRight(const char *s, int y, int size)
{
  size_t w;

  if (size == 1) {
    display.set1X();
  }
  else {
    display.set2X();
  }
  w = display.strWidth(s);
  display.setCursor(display.displayWidth() - w, y);
  display.print(s);
}

void printCentered(const char *s, int y, int size)
{
  size_t w;

  if (size == 1) {
    display.set1X();
  }
  else {
    display.set2X();
  }
  w = display.strWidth(s);
  display.setCursor((display.displayWidth() - w) / 2, y);
  display.print(s);
}

void printValue(int value, int y, int size)
{
  char s[20];
  size_t w;

  sprintf(s, "VALUE: %d", value);
  w = display.strWidth(s);
  if (size == 1) {
    display.set1X();
  }
  else {
    display.set2X();
  }
  display.setCursor(0, y);
  display.print(s);
}

void displayLogo(int x, int y)
{
  //  rien ici pour l'instant
}

GLCDFONTDECL(Batt6x14) = {
  0x0, 0x0, // size of zero indicates fixed width font,
  6,     // width
  14,     // height
  0x20,
  7,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // space
  0xff, 0x01, 0x01, 0x01, 0x01, 0xff, 0x3f, 0x20, 0x20, 0x20, 0x20, 0x3f,        // Battery Empty
  0xff, 0x01, 0x01, 0x01, 0x01, 0xff, 0x3f, 0x38, 0x38, 0x38, 0x38, 0x3f,        // Battery 20%
  0xff, 0x01, 0x01, 0x01, 0x01, 0xff, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f,             // Battery 40%
  0xff, 0xc1, 0xc1, 0xc1, 0xc1, 0xff, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f,              // Battery 60%
  0xff, 0xf1, 0xf1, 0xf1, 0xf1, 0xff, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f,                 // Battery 80%
  0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f,                      // Battery 100%
};

void displayBatteryLevel(int batteryLevel, int x, int y)
{
  static bool flash = false;

  display.set1X();
  display.setCursor(x, y);
  if (flash || batteryLevel > 10) {
    display.setFont(Batt6x14);
    display.print((char)(' ' + 1 + round(batteryLevel / 20)));
    display.setFont(System5x7);
  }
  else {
    display.setFont(Batt6x14);
    display.print(' ');
    display.setFont(System5x7);
  }
  flash = !flash;
}


3. Comparaison

3.1. Occupation mémoire

Le résultat de la compilation avec la librairie AdaFruit :
Le croquis utilise 14222 octets (44%) de l'espace de stockage de programmes. Le maximum est de 32256 octets.
Les variables globales utilisent 1544 octets (75%) de mémoire dynamique, ce qui laisse 504 octets pour les variables locales. Le maximum est de 2048 octets.
La mémoire disponible faible, des problèmes de stabilité pourraient survenir.


On sait déjà que l'application sera très limitée en taille mémoire dynamique (les variables globales).

Le résultat de la compilation avec la librairie Greiman :

Le croquis utilise 5852 octets (18%) de l'espace de stockage de programmes. Le maximum est de 32256 octets.
Les variables globales utilisent 274 octets (13%) de mémoire dynamique, ce qui laisse 1774 octets pour les variables locales. Le maximum est de 2048 octets.


On remarque vite que l'application est beaucoup moins limitée en taille mémoire dynamique, et que l'on sera beaucoup plus à l'aise pour créer des variables globales.

D'un point de vue occupation mémoire FLASH (programme), la librairie AdaFruit n'est pas catastrophique, mais la quantité de mémoire RAM (données) qu'elle monopolise limite son utilisation à des applications utilisant peu de données globales ou statiques.

Le développement d'une application conséquente avec la librairie AdaFruit se fera donc de préférence sur une carte ARDUINO MEGA, alors que la librairie Greiman se contentera facilement d'un ATMEGA328.

3.2. Principe de fonctionnement

La librairie AdaFruit travaille avec un espace mémoire tampon interne à la librairie (ce qui explique la taille mémoire occupée en RAM).
- on efface la zone tampon avec clearDisplay()
- on écrit avec print(), drawRect(), fillRect(), etc.
- on affiche avec display()
La librairie ne met à jour que les pixels qui ont changé par rapport au dernier affichage.
Avec cette librairie on a donc tendance à ré-afficher la totalité de l'écran, car le rafraîchissement est invisible.

La librairie Greiman travaille différemment, en direct avec l'écran
- on n'efface pas
- on écrit avec print() et l'affichage se fait dans la foulée
Avec cette librairie on préfère afficher sans effacer la totalité de l'écran, car le rafraîchissement serait trop visible.

Une autre différence de taille (c'est le cas de le dire) est que la librairie AdaFruit est capable de positionner son curseur en absolu, en nombre de pixels X et Y.

Avec la librairie Greiman, si le positionnement se fait en absolu dans le sens X (colonnes), il se fait en nombre de lignes de texte (une ligne faisant 8 pixels) dans le sens Y (lignes). La librairie Greiman est tout de même capable de faire du scrolling sur l'axe X, en pixels.
En bref, la librairie Greiman favorise un travail en ligne de texte, la librairie AdaFruit favorise un travail en mode graphique pur.

La librairie Greiman n'oblige pas à effacer un message précédemment affiché avant de le remplacer par un autre, par contre si le nouveau message est plus court que le précédent, la fin du premier message subsistera. On peut combler (padding) avec des espaces avant ou après le message, suivant que l'on justifie à droite, à gauche ou au centre.

On peut aussi positionner le curseur avec la méthod setCursor(x, y) et effacer la ligne avec la méthode clearToEOL() avant de ré-afficher :

  display.setCursor(0, 2);
  display.clearToEOL();
  display.print("A LINE");
  display.setCursor(0, 3);
  display.clearToEOL();  display.print("ANOTHER LINE");

La méthode clear() permet d'effacer une zone. Dans cet exemple on affiche deux lignes avec un retour à la ligne '\n', après avoir effacé les lignes 2 à 3 :

  display.setCursor(0, 2);
  display.clear(0, display.displayWidth(), 2, 3);
  display.print("A LINE\nANOTHER LINE");


La méthode clearField permet aussi d'effacer une zone. Cet exemple fait la même chose que le précédent :

  display.setCursor(0, 2);
  display.clearField(0, display.displayWidth(), 2);
  display.print("A LINE\nANOTHER LINE");


Les trois méthodes produisent le même résultat.

3.3. Affichage d'un indicateur de capacité batterie

Dans le sketch de la version AdaFruit l'indicateur de niveau batterie est réalisé simplement en dessinant un cadre puis un rectangle plein à l'intérieur de ce cadre.
Pour effacer, l'indicateur, on se contente de ne pas l'afficher.

Dans le sketch de la version Greiman l'indicateur de niveau batterie est réalisé à l'aide d'une police de caractères spéciale (Batt6x14) que j'ai fabriqué moi-même.
Cette police possède 7 caractères dont le premier est 0x20 : un ESPACE.

Pour afficher l'indicateur 0%, 20%, 40%, 60%, 80%, 100% il suffit d'afficher le caractère ESPACE (' ' ou 0x20) + n (0x21, 0x22, 0x23, 0x24, 0x25, 0x26).

Le premier caractère ESPACE (0x20) sert à effacer le bitmap dans le cas où l'on veut faire clignoter l'indicateur, comme dans l'exemple ci-dessus.

3.4. Routines graphiques

La librairie AdaFruit possède un jeu de routines graphiques assez complet que la librairie Greiman ne possède pas.

AdaFruit propose en outre un outil Windows de création de bitmaps :
https://learn.adafruit.com/monochrome-oled-breakouts/arduino-library-and-examples
Je ne l'ai pas essayé, ne possédant pas de machine Windows et n'ayant pas du tout l'intention d'utiliser ce système d'exploitation dépassé (alergie profonde). Je vous laisse faire avec plaisir.

Avec la librairie Greiman, il n'y a pas de possibilité d'affichage de bitmaps, mais on pourrait en créer (avec plus ou moins de facilité) en utilisant la même méthode que pour l'indicateur de capacité batterie, comme vu précédemment.

4. Conclusion

Personnellement, depuis que j'ai découvert la librairie Greiman, je l'utilise en lieu et place de la librairie AdaFruit, trop gourmande. Avec un afficheur de 128x64 pixels, mon besoin n'est pas de créer des oeuvres d'art graphique.


Cordialement
Henri

10 commentaires:

  1. Pile poil ce que je cherchais !
    Un grand merci pour cet excellent tuto :)

    RépondreSupprimer
  2. Bonjour
    je souhaiterai utiliser la bibliothèque SSD1306Ascii pour piloter un afficheur oled en I2C et ceci par un ATtiny85. Quelles modifications devrait on apporter dans votre sketch pour le rendre compatible avec cette puce?
    Merci par avance pour votre aide.

    RépondreSupprimer
    Réponses
    1. La librairie SSD1306Ascii ne compilera pas avec un ATTINY85 qui a un I2C particulier. Il vaut mieux se tourner vers une solution dédiée.
      Peut-être : https://www.instructables.com/ATTiny85-connects-to-I2C-OLED-display-Great-Things/

      Supprimer
    2. Merci pour la réponse rapide.
      C'est vraiment dommage :(
      La bibliothèque SSD1306Ascii me semblait idéale pour mes futures applications(du texte uniquement).
      Mais bon on fera sans...

      Supprimer
    3. La librairie SSD1306_minimal du lien précédent sait afficher du texte.

      Supprimer
    4. oui tout à fait, mais à priori on ne peut pas changer la taille du texte et je ne trouve beaucoup de doc ne serait ce les instructions...
      Au sujet de SSD1306Ascii j'ai cru comprendre qu'elle n'est pas compatible avec le core David A. Mellis que j'ai installé et que je souhaite garder donc ce n'est pas gagné...

      Supprimer
    5. En fait il y a une possibilité : il faut utiliser Wire
      #include "SSD1306Ascii.h"
      #include "SSD1306AsciiWire.h"
      Voir l'exemple HelloWorldWire.ino

      Supprimer
  3. Est ce que la librairie Adafruit rafraichit tout l'écran lorsque seule un (petit ) rectangle est modifie (ex: on dessine un tout petit bonhomme qui se déforme -sprite-; pour faire une animation).
    Est ce u'elle est compatible avec des cartes généreusement dotées de RAM (ESPxx, picopi entre autres qui ont 100 fois plus de RZM qu'un UNO canale historico)

    RépondreSupprimer
    Réponses
    1. La librairie Adafruit rafraîchit uniquement la zone modifiée.
      J'utilise couramment des SSD1306 avec des ESP32, et la librairie Adafruit. Je ne vois pas pourquoi la quantité de RAM poserait un problème, bien au contraire.

      Supprimer