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.
| SSD1306 | SH1106 | |
|---|---|---|
| Taille courante | 0,96 pouce | 1,3 pouce |
| Bibliothèque | ssd1306.py | sh1106.py |
| Décalage RAM | aucun | 2 colonnes |
Méthode sleep() | absente | pré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.pyen 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é.
| Module | ESP32 | Rôle |
|---|---|---|
| SCL | GPIO 22 | Horloge I2C |
| SDA | GPIO 21 | Données I2C, bidirectionnelles |
| VCC | 3V3 | Certains modules acceptent 5 V — vérifier |
| GND | GND | |
| RST | GPIO 16 | Optionnel, 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.
| Appel | Effet |
|---|---|
| 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 / rect | Primitives de dessin |
| hline / vline / fill_rect | Traits 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
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 choseConséquences concrètes :
SH1106_I2C(128, 64, i2c, 0x3c)passe0x3ccomme broche de reset. Le driver appelle alorsrst.value(1)sur un entier →AttributeError: 'int' object has no attribute 'value'.SSD1306_I2C(128, 64, i2c, Pin(16))passe un objetPincomme 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) 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.
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.
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 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() # indispensableChoisir 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)- Fais le scan I2C : quelle adresse répond,
0x3cou0x3d? - Affiche 8 lignes de 16 caractères pour vérifier que l’écran est bien plein
- Change le contraste :
oled.contrast(1)puisoled.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
- Atelier Station météo — température DS18B20 + courbe
- Atelier Mesure de distance — barre de progression ultrason
- Cours Capteurs et afficheurs — la partie théorique
- Carte Vincent — écran 1,3 pouce, donc SH1106, avec reset sur GPIO 16
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))