I2CBIBLIOTHÈQUE

ssd1306.py & sh1106.py

Les afficheurs OLED 128 × 64 monochromes en I2C. Deux contrôleurs différents, deux bibliothèques — et des modules qui se ressemblent tellement qu’on se trompe une fois sur deux.

La vraie question : lequel ai-je entre les mains ?

C’est la question à laquelle cette fiche sert d’abord à répondre. Les deux modules ont la même taille de dalle, le même connecteur à 4 broches, la même adresse I2C. Rien ne les distingue au premier regard.

SSD1306SH1106
Taille courante0,96 pouce1,3 pouce
Bibliothèquessd1306.pysh1106.py
Décalage RAMaucun2 colonnes
Méthode sleep()absenteprésente

La correspondance taille ↔ contrôleur est une règle empirique, pas une loi : on trouve des 1,3” en SSD1306 et l’inverse. La méthode fiable est expérimentale :

Essaie ssd1306.py en premier. Si l’image s’affiche correctement, c’est un SSD1306. Si elle apparaît décalée de 2 pixels, avec une bande parasite sur un bord, c’est un SH1106 — change de bibliothèque.

L’écran qui ne s’allume pas du tout n’est pas un problème de contrôleur mais de câblage ou d’adresse.

Câblage

Identique pour les deux. Sur la carte Vincent, l’écran est déjà relié.

ModuleESP32Rôle
SCLGPIO 22Horloge I2C
SDAGPIO 21Données I2C, bidirectionnelles
VCC3V3Certains modules acceptent 5 V — vérifier
GNDGND
RSTGPIO 16Optionnel, seulement si le module l’expose

Avant tout, vérifie que l’écran répond :

from machine import I2C, Pin
i2c = I2C(scl=Pin(22), sda=Pin(21), freq=400000)
print([hex(a) for a in i2c.scan()])     # attendu : ['0x3c']

L’adresse est 0x3c dans l’immense majorité des cas, parfois 0x3d selon un pont à souder au dos du module. Une liste vide signifie un problème d’alimentation ou de fils SCL/SDA — pas de contrôleur.

L’API, commune aux deux

Les deux classes héritent de framebuf.FrameBuffer : toutes les méthodes de dessin sont identiques. C’est la bonne nouvelle — une fois l’écran initialisé, le code ne dépend plus du contrôleur.

AppelEffet
fill(0 | 1)Efface en noir (0) ou remplit en blanc (1)
text(s, x, y, 1)Texte 8 × 8 pixels, ASCII uniquement
pixel / line / rectPrimitives de dessin
hline / vline / fill_rectTraits et rectangles pleins
scroll(dx, dy)Décale le contenu du tampon
contrast(0-255)Luminosité de la dalle
invert(True | False)Vidéo inverse
show()Envoie le tampon à l’écran — sans lui, rien n’apparaît

Le texte fait 8 pixels de haut sur 8 de large. Un écran 128 × 64 affiche donc 16 caractères sur 8 lignes, pas un de plus.

Pièges connus

Le 4ᵉ paramètre n'est pas le même dans les deux constructeurs

C’est le piège le plus coûteux, parce qu’il ne ressemble pas à une erreur de bibliothèque.

SSD1306_I2C(width, height, i2c, addr=0x3c, external_vcc=False)
SH1106_I2C (width, height, i2c, rst=None,  addr=0x3C)
#                                ↑ ce n'est PAS la même chose

Conséquences concrètes :

  • SH1106_I2C(128, 64, i2c, 0x3c) passe 0x3c comme broche de reset. Le driver appelle alors rst.value(1) sur un entier → AttributeError: 'int' object has no attribute 'value'.
  • SSD1306_I2C(128, 64, i2c, Pin(16)) passe un objet Pin comme adresse. L’écriture I2C échoue.

La parade : toujours nommer les paramètres.

oled = SH1106_I2C(128, 64, i2c, rst=Pin(16), addr=0x3c)
oled = SSD1306_I2C(128, 64, i2c, addr=0x3c)
Image décalée de 2 pixels — mauvaise bibliothèque

Le SH1106 possède une RAM de 132 colonnes pour une dalle de 128. L’image utile commence donc à la colonne 2, ce que sh1106.py compense à chaque show() :

# Le SH1106 a un offset de 2 colonnes dans sa RAM interne
self._write_cmd(0x02)               # col low  (offset 2)

Piloter un SH1106 avec ssd1306.py donne un écran qui s’allume et affiche presque correctement — décalé de 2 pixels, avec une bande parasite sur un bord. C’est ce « presque » qui fait chercher longtemps : on soupçonne le câblage, l’alimentation, tout sauf la bibliothèque.

⚠️ sleep() n'existe pas sur le SSD1306

sh1106.py fournit display.sleep(True | False) ; ssd1306.py ne l’a pas. Le même code porté d’un écran à l’autre lève AttributeError: 'SSD1306_I2C' object has no attribute 'sleep'.

L’équivalent commun aux deux est poweroff() / poweron().

À noter : dans notre sh1106.py, init_display() se termine déjà par la commande 0xAF (allumage). Le display.sleep(False) qu’on voit souvent juste après la création de l’objet est donc redondant — inoffensif, mais inutile.

⚠️ Rien ne s'affiche : l'oubli de show()

Comme sur l’e-paper, les méthodes de dessin ne travaillent qu’en mémoire. Tant que show() n’est pas appelé, l’écran ne change pas.

oled.fill(0)
oled.text("Bonjour", 0, 0, 1)
oled.show()                  # ← sans cette ligne, écran noir
⚠️ Les accents ne s'affichent pas

Même limite que sur l’e-paper : la police de framebuf est en ASCII pur. Écris « Temperature », « degres », « Ardeche ».

Code minimal

from machine import I2C, Pin

i2c = I2C(scl=Pin(22), sda=Pin(21), freq=400000)

# --- Écran 0,96" (SSD1306) ---
from ssd1306 import SSD1306_I2C
oled = SSD1306_I2C(128, 64, i2c, addr=0x3c)

# --- Écran 1,3" (SH1106) ---
# from sh1106 import SH1106_I2C
# oled = SH1106_I2C(128, 64, i2c, rst=Pin(16), addr=0x3c)

oled.fill(0)
oled.text("Fablab Payzac", 8, 10, 1)
oled.hline(0, 22, 128, 1)
oled.text("MicroPython", 16, 32, 1)
oled.show()                 # indispensable

Choisir automatiquement

Plutôt que de commenter et décommenter, un commutateur en tête de programme — la méthode employée dans oled-base-ENIM.py :

OLED = 0          # 0 : 0.96 pouce (SSD1306)   1 : 1.3 pouce (SH1106)

if OLED == 1:
    from sh1106 import SH1106_I2C
    oled = SH1106_I2C(128, 64, i2c, rst=Pin(16), addr=0x3c)
else:
    from ssd1306 import SSD1306_I2C
    oled = SSD1306_I2C(128, 64, i2c, addr=0x3c)
🔧 À toi de jouer
  • Fais le scan I2C : quelle adresse répond, 0x3c ou 0x3d ?
  • Affiche 8 lignes de 16 caractères pour vérifier que l’écran est bien plein
  • Change le contraste : oled.contrast(1) puis oled.contrast(255)
  • Essaie oled.invert(True) → tout s’inverse, sans redessiner
  • Supprime volontairement le show() : rien ne se passe. Le réflexe s’acquiert là
  • Si tu as les deux écrans : lance le code SSD1306 sur le 1,3 pouce et observe le décalage de 2 pixels

Où sont utilisés ces écrans

Les fichiers source

ssd1306.py — écrans 0,96 pouce .python
cours-exemples/oled/ssd1306.py
100 lignes GitHub
# Driver SSD1306 OLED — MicroPython
# Source : micropython/micropython-lib (MIT License)
# À copier sur l'ESP32 via Thonny (clic droit → Upload to /)

import framebuf

SET_CONTRAST        = const(0x81)
SET_ENTIRE_ON       = const(0xa4)
SET_NORM_INV        = const(0xa6)
SET_DISP            = const(0xae)
SET_MEM_ADDR        = const(0x20)
SET_COL_ADDR        = const(0x21)
SET_PAGE_ADDR       = const(0x22)
SET_DISP_START_LINE = const(0x40)
SET_SEG_REMAP       = const(0xa0)
SET_MUX_RATIO       = const(0xa8)
SET_COM_OUT_DIR     = const(0xc0)
SET_DISP_OFFSET     = const(0xd3)
SET_COM_PIN_CFG     = const(0xda)
SET_DISP_CLK_DIV    = const(0xd5)
SET_PRECHARGE       = const(0xd9)
SET_VCOM_DESEL      = const(0xdb)
SET_CHARGE_PUMP     = const(0x8d)


class SSD1306(framebuf.FrameBuffer):
    def __init__(self, width, height, external_vcc):
        self.width = width
        self.height = height
        self.external_vcc = external_vcc
        self.pages = self.height // 8
        self.buffer = bytearray(self.pages * self.width)
        super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
        self.init_display()

    def init_display(self):
        for cmd in (
            SET_DISP | 0x00,           # off
            SET_MEM_ADDR, 0x00,        # horizontal
            SET_DISP_START_LINE | 0x00,
            SET_SEG_REMAP | 0x01,      # remap
            SET_MUX_RATIO, self.height - 1,
            SET_COM_OUT_DIR | 0x08,    # flip
            SET_DISP_OFFSET, 0x00,
            SET_COM_PIN_CFG, 0x02 if (self.width == 128 and self.height == 32) else 0x12,
            SET_DISP_CLK_DIV, 0x80,
            SET_PRECHARGE, 0x22 if self.external_vcc else 0xf1,
            SET_VCOM_DESEL, 0x30,
            SET_CONTRAST, 0xff,
            SET_ENTIRE_ON,
            SET_NORM_INV,
            SET_CHARGE_PUMP, 0x10 if self.external_vcc else 0x14,
            SET_DISP | 0x01,           # on
        ):
            self.write_cmd(cmd)
        self.fill(0)
        self.show()

    def poweroff(self):
        self.write_cmd(SET_DISP | 0x00)

    def poweron(self):
        self.write_cmd(SET_DISP | 0x01)

    def contrast(self, contrast):
        self.write_cmd(SET_CONTRAST)
        self.write_cmd(contrast)

    def invert(self, invert):
        self.write_cmd(SET_NORM_INV | (invert & 1))

    def show(self):
        x0 = 0
        x1 = self.width - 1
        self.write_cmd(SET_COL_ADDR)
        self.write_cmd(x0)
        self.write_cmd(x1)
        self.write_cmd(SET_PAGE_ADDR)
        self.write_cmd(0)
        self.write_cmd(self.pages - 1)
        self.write_data(self.buffer)


class SSD1306_I2C(SSD1306):
    def __init__(self, width, height, i2c, addr=0x3c, external_vcc=False):
        self.i2c = i2c
        self.addr = addr
        self.temp = bytearray(2)
        self.write_list = [b"\x40", None]
        super().__init__(width, height, external_vcc)

    def write_cmd(self, cmd):
        self.temp[0] = 0x80
        self.temp[1] = cmd
        self.i2c.writeto(self.addr, self.temp)

    def write_data(self, buf):
        self.write_list[1] = buf
        self.i2c.writevto(self.addr, self.write_list)
sh1106.py — écrans 1,3 pouce .python
cours-exemples/oled/sh1106.py
86 lignes GitHub
# Driver SH1106 OLED — MicroPython
# Compatible OLED 1.3 pouces (contrôleur SH1106)
# À copier sur l'ESP32 via Thonny (clic droit → Upload to /)

import framebuf
from time import sleep_ms

class SH1106(framebuf.FrameBuffer):

    def __init__(self, width, height, external_vcc=False):
        self.width        = width
        self.height       = height
        self.external_vcc = external_vcc
        self.pages        = height // 8
        self._buffer      = bytearray(self.pages * width)
        super().__init__(self._buffer, width, height, framebuf.MONO_VLSB)
        self.init_display()

    def init_display(self):
        self._write_cmd(0xAE)   # display off
        self._write_cmd(0xD5)   # clock div
        self._write_cmd(0x80)
        self._write_cmd(0xA8)   # multiplex
        self._write_cmd(self.height - 1)
        self._write_cmd(0xD3)   # display offset
        self._write_cmd(0x00)
        self._write_cmd(0x40)   # start line
        self._write_cmd(0xAD)   # charge pump
        self._write_cmd(0x8B if not self.external_vcc else 0x8A)
        self._write_cmd(0xA1)   # seg remap
        self._write_cmd(0xC8)   # com scan dir
        self._write_cmd(0xDA)   # com pins
        self._write_cmd(0x12)
        self._write_cmd(0x81)   # contrast
        self._write_cmd(0xFF)
        self._write_cmd(0xD9)   # precharge
        self._write_cmd(0x1F if not self.external_vcc else 0x22)
        self._write_cmd(0xDB)   # vcom deselect
        self._write_cmd(0x40)
        self._write_cmd(0xA4)   # entire display on
        self._write_cmd(0xA6)   # normal display
        self.fill(0)
        self.show()
        self._write_cmd(0xAF)   # display on

    def poweroff(self):
        self._write_cmd(0xAE)

    def poweron(self):
        self._write_cmd(0xAF)

    def sleep(self, on):
        self._write_cmd(0xAE if on else 0xAF)

    def contrast(self, contrast):
        self._write_cmd(0x81)
        self._write_cmd(contrast)

    def invert(self, invert):
        self._write_cmd(0xA7 if invert else 0xA6)

    def show(self):
        # Le SH1106 a un offset de 2 colonnes dans sa RAM interne
        for page in range(self.pages):
            self._write_cmd(0xB0 | page)        # page address
            self._write_cmd(0x02)               # col low  (offset 2)
            self._write_cmd(0x10)               # col high
            self._write_data(self._buffer[page * self.width:(page + 1) * self.width])


class SH1106_I2C(SH1106):

    def __init__(self, width, height, i2c, rst=None, addr=0x3C):
        self.i2c  = i2c
        self.addr = addr
        self.rst  = rst
        if rst is not None:
            rst.value(1)
        super().__init__(width, height)

    def _write_cmd(self, cmd):
        self.i2c.writeto(self.addr, bytes([0x00, cmd]))

    def _write_data(self, buf):
        self.i2c.writeto(self.addr, b'\x40' + bytes(buf))