Skip to Content

Teilaktualisierung des e-Paper-Displays

Teilaktualisierung des e-Paper-Displays

In diesem Tutorial lernst du, wie man ein schnelles partielles Refresh eines e-Paper-Displays durchführt. Die Hauptvorteile von e-Paper-Displays sind ihr geringer Stromverbrauch und die hervorragende Lesbarkeit. Abgesehen vom begrenzten Farbspektrum ist ihr größter Nachteil die langsame Aktualisierungsrate.

Typischerweise dauert es 2-4 Sekunden, bis ein schwarz-weißes e-Paper den gesamten Displayinhalt aktualisiert, bei Farbdisplays ist es deutlich länger (>20 Sekunden). Das vollständige Refresh geht außerdem mit starkem Flackern einher, was störend ist.

Glücklicherweise kannst du ein partielles Refresh durchführen, das nicht nur viel schneller ist (0,3 Sekunden), sondern auch das Flackern des Displays vermeidet. Für jede praktische Anwendung, die den Displayinhalt öfter als alle 10 Minuten aktualisiert, solltest du definitiv ein partielles Refresh verwenden.

Die Implementierung eines partiellen Refreshs bei e-Paper kann jedoch ziemlich knifflig sein. Dieses Tutorial zeigt dir, wie du einzelne oder mehrere Displaybereiche aktualisierst, wie du partielles Refresh mit Deep Sleep kombinierst und wie du einzelne Pixel für das Plotten teilweise aktualisierst.

Lass uns mit den benötigten Teilen starten.

Benötigte Teile

Für die benötigten Teile habe ich einen älteren ESP32 lite aufgelistet, der zwar veraltet ist, aber noch günstig erhältlich ist. Es gibt ein Nachfolgermodel (Amazon) mit verbesserten Spezifikationen. Aber jeder andere ESP32, ESP8266 oder Arduino mit ausreichend Speicher funktioniert ebenfalls.

2,9″ e-Paper Display

ESP32 lite Lolin32

ESP32 lite

USB data cable

USB-Datenkabel

Dupont wire set

Dupont-Kabelset

Half_breadboard56a

Breadboard

Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.

E-Paper Display

Für dieses Projekt verwende ich ein 2,9″ e-Paper-Displaymodul mit 296×128 Pixel Auflösung und einem eingebetteten Controller mit SPI-Schnittstelle.

Front and Back of 2.9" e-Paper display module
Vorder- und Rückseite des 2,9″ e-Paper-Displaymoduls

Beachte, dass die meisten e-Paper-Module mit SPI einen kleinen Schalter oder Jumper haben, mit dem du zwischen 4-Draht-SPI und 3-Draht-SPI wechseln kannst. Wir verwenden hier den Standard 4-Draht-SPI.

Back of display module with SPI-interface and controller
Rückseite des Displaymoduls mit SPI-Schnittstelle und Controller

Das Displaymodul läuft mit 3,3V oder 5V, hat einen sehr niedrigen Schlafstrom von 0,01µA und verbraucht nur etwa 26,4mW während des Refreshs/Aktualisierens. Das bedeutet, du kannst ein e-Paper-Display auch mit einer kleinen Batterie lange betreiben.

Für mehr Informationen zu e-Paper-Displays schau dir unser Tutorial Interfacing Arduino To An E-ink Display.

Anschluss und Test des e-Paper

Zuerst verbinden wir das e-Paper und testen die Funktion. Das folgende Bild zeigt die komplette Verkabelung für Stromversorgung und SPI.

Connecting e-Paper to ESP32 via SPI
Anschluss des e-Paper an ESP32 via SPI

Hier ist die Tabelle mit allen Verbindungen zur Übersicht. Beachte, dass du das Display mit 3,3V oder 5V versorgen kannst, aber der ESP32-lite hat nur einen 3,3V-Ausgang und die SPI-Datenleitungen müssen 3,3V sein!

e-Paper DisplayESP32 lite
CS/SS5
SCL/SCK 18
SDA/DIN/MOSI23
BUSY15
RES/RST2
DC0
VCC3,3V
GNDG

Installation der GxEPD2 Bibliothek

Bevor wir auf dem e-Paper zeichnen oder schreiben können, müssen wir zwei Bibliotheken installieren. Die Adafruit_GFX Bibliothek ist eine Kern-Grafikbibliothek, die eine gemeinsame Menge an Grafikprimitiven (Text, Punkte, Linien, Kreise usw.) bereitstellt. Und die GxEPD2 Bibliothek stellt die Grafiktreiber-Software zur Steuerung eines e-Paper-Displays via SPI bereit.

Einfach die Bibliothek auf die übliche Weise installieren. Nach der Installation sollten sie im Library Manager wie folgt erscheinen.

Adafruit_GFX and GxEPD2 libraries in Library Manager
Adafruit_GFX und GxEPD2 Bibliotheken im Library Manager

Testcode

Hier ist ein einfacher Testcode, der den Text „Makerguides“ auf dem Display anzeigt. Schau dir zuerst den kompletten Code an, danach besprechen wir ihn.

#define ENABLE_GxEPD2_GFX 0

#include "GxEPD2_BW.h"

//CS(SS)=5, SCL(SCK)=18, SDA(MOSI)=23, BUSY=15, RES(RST)=2, DC=0
GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));

void setup() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.setTextColor(GxEPD_BLACK);   
  epd.setTextSize(2);
  epd.setFullWindow();

  epd.fillScreen(GxEPD_WHITE);     
  epd.setCursor(20, 20);
  epd.print("Makerguides");  
  epd.display();
  epd.hibernate();
}

void loop() {}

Lass uns den Code in seine Komponenten zerlegen, um zu verstehen, wie er funktioniert.

Konstanten und Bibliotheken

Der Code beginnt mit der Definition einer Konstante ENABLE_GxEPD2_GFX als 0. Du kannst sie auf 1 setzen, was laut Dokumentation der Basisklasse GxEPD2_GFX erlaubt, Referenzen oder Zeiger auf die Display-Instanz als Parameter zu übergeben. Das benötigt aber ~1,2k mehr Code und wir brauchen es nicht, daher ist sie auf 0 gesetzt.

#define ENABLE_GxEPD2_GFX 0

Als nächstes binden wir die GxEPD2_BW.h Header-Datei für das schwarz-weiße (BW) e-Paper-Display ein. Wenn du ein 3-Farben-Display hast, würdest du GxEPD2_3C.h einbinden, oder GxEPD2_4C.h für ein 4-Farben-Display, und GxEPD2_7C.h für ein 7-Farben-Display.

#include "GxEPD2_BW.h"

Display-Objekt

Die nächste Zeile ist wichtig. Sie erstellt das Display-Objekt und hängt vom Displaytyp oder Hersteller ab. Ich habe ein WeAct und ein WaveShare Display getestet und die folgende Zeile funktioniert für beide.

GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));

Die Readme for GxEPD2 Bibliothek listet alle unterstützten Displays auf, die Details findest du in den Header-Dateien, z.B. GxEPD2.h.

Setup-Funktion

Die ganze Arbeit passiert in der Setup-Funktion. Zuerst richten wir die Display-Parameter ein wie Kommunikationsgeschwindigkeit, Rotation, Schriftart, Textfarbe und Bildschirmfüllfarbe. Wenn du Probleme mit dem Refresh hast, musst du eventuell mit den Parametern der init() Funktion experimentieren.

void setup() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.setTextColor(GxEPD_BLACK);   
  epd.setTextSize(2);
  epd.setFullWindow();

  epd.fillScreen(GxEPD_WHITE);     
  epd.setCursor(20, 20);
  epd.print("Makerguides");  
  epd.display();
  epd.hibernate();
}

Dann füllen wir den Bildschirm mit Weiß, setzen den Cursor, schreiben den Text und versetzen den Displaytreiber schließlich in den Ruhezustand. Das schaltet das Display aus und versetzt den Displaycontroller in den Tiefschlaf (nicht den ESP32, nur das Display!).

Loop-Funktion

Die loop() Funktion ist in diesem Beispiel leer, da der Displayinhalt in der setup() Funktion erstellt wird und nicht kontinuierlich aktualisiert werden muss.

void loop() {}

Code hochladen und ausführen

Jetzt sind wir bereit, den Code hochzuladen und auszuführen. Wähle das Board im Board Manager aus. In meinem Fall ist es das WEMOS LOLIN32 Lite, das in den benötigten Teilen gelistet war:

WEMOS LOLIN32 Lite selected in Board Manager
WEMOS LOLIN32 Lite im Board Manager ausgewählt

Dann drücke Upload und nach etwas Flackern sollte dein Display folgenden Text anzeigen:

Test output on e-Paper display
Testausgabe auf e-Paper-Display

Vollständiges Refresh

Um zu demonstrieren, wie schlecht ein vollständiges Refresh eines e-Paper-Displays aussieht, verwenden wir folgenden Code. Er zeigt den Text „msec: “ und daneben die Millisekunden seit Start des Mikrocontrollers.

#define ENABLE_GxEPD2_GFX 0

#include "GxEPD2_BW.h"

GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));

void setup() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.setTextSize(2);
  epd.setTextColor(GxEPD_BLACK);
}

void loop() {
  epd.fillScreen(GxEPD_WHITE);
  epd.setCursor(40, 60);
  epd.print("msec: ");
  epd.setCursor(150, 60);
  epd.print(millis());
  epd.display();
  epd.hibernate();
}

Der Displayinhalt wird in der Hauptschleife so schnell wie möglich aktualisiert und refreshed. Das folgende kurze Video zeigt, wie das aussieht. Wie du siehst, gibt es viel Flackern und der Refresh dauert fast 3 Sekunden.

Full refresh of e-Paper display
Vollständiges Refresh des e-Paper-Displays

Für Anwendungen mit häufigen Inhaltsupdates ist ein vollständiges Refresh also zu langsam, um praktisch zu sein.

Partielles Refresh eines einzelnen Bereichs

Statt bei jeder Aktualisierung des Millisekundenwerts das gesamte Display zu refreshen, können wir stattdessen ein partielles Refresh nur für den Bereich durchführen. Der folgende Code macht einmal am Anfang ein vollständiges Refresh und aktualisiert danach nur den Bereich mit dem Millisekundenwert:

#define ENABLE_GxEPD2_GFX 0

#include "GxEPD2_BW.h"

GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));

void drawFull(const void* pv) {
  epd.setFullWindow();
  epd.setCursor(40, 60);
  epd.print("msec: ");
}

void drawPartial(const void* pv) {
  uint16_t x = 120, y = 50, w = 130, h = 34;
  epd.setPartialWindow(x, y, w, h);
  epd.setCursor(x + 30, y + 10);
  epd.print(millis());
  epd.drawRect(x, y, w, h, GxEPD_BLACK);
}

void setup() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.setTextSize(2);
  epd.setTextColor(GxEPD_BLACK);
  epd.drawPaged(drawFull, 0);
}

void loop() {
  epd.drawPaged(drawPartial, 0);
  epd.hibernate();
}

Das folgende Video zeigt, wie das partielle Refresh aussieht. Das Flackern ist weg und die Aktualisierung der Millisekunden ist viel schneller (< 1 Sekunde).

Partial refresh of e-Paper display
Partielles Refresh des e-Paper-Displays

Beim Start des Codes gibt es ein vollständiges Refresh mit dem üblichen Flackern und langsamen Refresh, aber das passiert nur einmal.

Paged Draw

Es gibt verschiedene Möglichkeiten, ein vollständiges oder partielles Refresh zu implementieren. Die Verwendung der drawPaged() Funktion ist wahrscheinlich die eleganteste und einfachste. Sie nimmt eine drawCallback Funktion und einen Parametervektor pv, um ein seitenweises Neuzeichnen durchzuführen:

void drawPaged(void (*drawCallback)(const void*), const void* pv)

Innerhalb der drawCallback Funktion rufen wir entweder setFullWindow() für ein vollständiges Refresh oder setPartialWindow(x, y, w, h) für ein partielles Refresh auf.

Die Verwendung von drawPaged() hat außerdem den Vorteil, dass Zeichenvorgänge speichereffizient gehandhabt werden, was besonders bei größeren Displays und begrenztem RAM nützlich ist. Es zeichnet den Inhalt in mehreren kleineren Schritten (Seiten) statt das ganze Display auf einmal zu refreshen.

drawFull Funktion

Wenn du dir die drawFull() Funktion ansiehst, siehst du, dass wir setFullWindow() aufrufen, die Cursorposition setzen und dann den statischen Text "msec: " ausgeben.

void drawFull(const void* pv) {
  epd.setFullWindow();
  epd.setCursor(40, 60);
  epd.print("msec: ");
}

In der Setup-Funktion initialisieren wir das Display, setzen die Display-Eigenschaften und rufen dann drawPaged(drawFull, 0) auf, um das vollständige Display zu refreshen.

void setup() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.setTextSize(2);
  epd.setTextColor(GxEPD_BLACK);
  epd.drawPaged(drawFull, 0);
}

Der Parametervektor pv ist auf 0 gesetzt, da wir keine Parameter übergeben. Du könntest hier aber Schriftart, Farbe, Text oder andere relevante Informationen für die drawFull Funktion übergeben.

drawPartial Funktion

Der Hauptunterschied der drawPartial() Funktion zur drawFull() ist, dass setPartialWindow(x, y, w, h) aufgerufen wird, um den zu refreshenden Displaybereich einzuschränken.

void drawPartial(const void* pv) {
  uint16_t x = 120, y = 50, w = 130, h = 34;
  epd.setPartialWindow(x, y, w, h);
  epd.setCursor(x + 30, y + 10);
  epd.print(millis());
  epd.drawRect(x, y, w, h, GxEPD_BLACK);
}

Danach setzen wir den Cursor, geben den Millisekunden-Text aus und zeichnen ein Rechteck, um das Refresh-Fenster zu markieren.

Schließlich wird die drawPartial() über drawPaged(drawPartial, 0) in der Hauptschleife aufgerufen, was ein wiederholtes Refresh des angezeigten Millisekundenwerts bewirkt.

void loop() {
  epd.drawPaged(drawPartial, 0);
  epd.hibernate();
}

Beachte, dass alles, was außerhalb des partiellen Fensterbereichs gezeichnet wird, nicht angezeigt wird. In drawPartial() setzen wir den Cursor innerhalb des Bereichs und geben den Millisekundenwert innerhalb des Bereichs aus.

Um den Bereich des partiellen Refreshs zu zeigen, zeichnen wir ein Rechteck darum. Das ist jedoch nicht ganz genau! Mehr dazu im nächsten Abschnitt.

Ausrichtung des Refresh-Fensters

Aufgrund von Adressierungsbeschränkungen der e-Paper-Display-Controller müssen die Koordinaten des Refresh-Fensters auf Vielfache von 8 ausgerichtet sein. Konkret:

  • x und w sollten Vielfache von 8 sein, bei Display-Rotation 0 oder 2,
  • y und h sollten Vielfache von 8 sein, bei Display-Rotation 1 oder 3,

Die setPartialWindow(x, y, w, h) Funktion erlaubt beliebige Werte, richtet diese intern aber wie erforderlich aus. Das bedeutet, dass das tatsächliche Refresh-Fenster größer sein kann als angegeben.

Das ist wichtig, denn es bedeutet, dass Bildschirmbereiche aktualisiert werden, die du vielleicht nicht beabsichtigt hast. Das Bild unten zeigt den beabsichtigten Refresh-Bereich (schwarzes Rechteck) und den tatsächlichen Refresh-Bereich (weiß gefülltes Rechteck).

Intended and actual refresh window
Beabsichtigtes und tatsächliches Refresh-Fenster

Wie du siehst, ist die Höhe des tatsächlichen Refresh-Fensters größer als die des angegebenen Fensters. Wenn du das Layout für deinen Inhalt gestaltest, musst du das berücksichtigen. Besonders, da die drawRect() Funktion den Hintergrund des tatsächlichen Refresh-Fensters komplett mit Weiß füllt. Das bedeutet, dass Inhalte außerhalb des angegebenen, aber innerhalb des tatsächlichen Refresh-Fensters gelöscht werden.

Wenn du das obige Experiment nachstellen möchtest, hier ist der Code. Er invertiert einfach die Hintergrundfarbe und den Text des vollständigen Neuzeichnens, um das tatsächliche Refresh-Fenster sichtbar zu machen.

#define ENABLE_GxEPD2_GFX 0

#include "GxEPD2_BW.h"

GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));

void drawFull(const void* pv) {
  epd.setFullWindow();
  epd.fillScreen(GxEPD_BLACK);
  epd.setCursor(40, 60);
  epd.print("msec: ");
}

void drawPartial(const void* pv) {
  uint16_t x = 120, y = 50, w = 130, h = 34;
  epd.setTextColor(GxEPD_BLACK);
  epd.setPartialWindow(x, y, w, h);
  epd.setCursor(x + 30, y + 10);
  epd.print(millis());
  epd.drawRect(x, y, w, h, GxEPD_BLACK);
}

void setup() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.setTextSize(2);
  epd.setTextColor(GxEPD_WHITE);
  epd.drawPaged(drawFull, 0);
}

void loop() {
  epd.drawPaged(drawPartial, 0);
  epd.hibernate();
}

Partielles Refresh mit Deep Sleep

e-Paper-Displays werden oft in batteriebetriebenen Projekten verwendet, bei denen der Mikrocontroller typischerweise in den Deep-Sleep-Modus geht. Wie man den Deep-Sleep des ESP32 mit dem partiellen Refresh eines e-Paper-Displays kombiniert, ist Thema dieses Abschnitts.

Die Hauptschwierigkeit ist, dass wir nur beim ersten Start (Reset) des ESP32 ein vollständiges Refresh durchführen wollen, aber bei jedem Aufwachen aus dem Deep-Sleep nur ein partielles Refresh. Der folgende Code erreicht das. Schau ihn dir kurz an, dann gehen wir ins Detail.

#define ENABLE_GxEPD2_GFX 0

#include "GxEPD2_BW.h"

GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));

RTC_DATA_ATTR bool initial = true;

void drawFull(const void* pv) {
  epd.setFullWindow();
  epd.setCursor(40, 60);
  epd.print("msec: ");
  initial = false;
}

void drawPartial(const void* pv) {
  uint16_t x = 120, y = 50, w = 130, h = 34;
  epd.setPartialWindow(x, y, w, h);
  epd.setCursor(x + 30, y + 10);
  epd.print(millis());
  epd.setCursor(x + 100, y + 10);
  epd.print(random(10));  
  epd.drawRect(x, y, w, h, GxEPD_BLACK);
}

void setup() {
  epd.init(115200, initial, 50, false);
  epd.setRotation(1);
  epd.setTextSize(2);
  epd.setTextColor(GxEPD_BLACK);
  if (initial)
    epd.drawPaged(drawFull, 0);
}

void loop() {
  epd.drawPaged(drawPartial, 0);
  epd.hibernate();  
  esp_sleep_enable_timer_wakeup(1000);
  esp_deep_sleep_start();
}

Der Code ist grundsätzlich derselbe wie im vorherigen Beispiel, mit ein paar wichtigen Änderungen.

Zuerst definieren wir eine Variable initial, die im RTC memory gespeichert wird, was bedeutet, dass ihr Wert im Deep-Sleep erhalten bleibt.

RTC_DATA_ATTR bool initial = true;

Die initial Variable wird auf true gesetzt und nach dem ersten vollständigen Refresh via drawFull() auf false gesetzt. Das bedeutet initial=true nach einem Reset, aber false nach Deep-Sleep.

Als nächstes müssen wir dem Display mitteilen, dass es nach Deep-Sleep kein vollständiges Refresh durchführen soll. Die init(serial_diag_bitrate, initial, reset_duration pulldown_rst_mode) Funktion hat dafür den Parameter initial:

epd.init(115200, initial, 50, false); 

Wir sind fast fertig. In der setup Funktion führen wir nur dann ein vollständiges Neuzeichnen durch, wenn initial==true gilt. Also nur nach einem Reset, nicht nach dem Aufwachen aus Deep-Sleep.

void setup() {
  ...
  if (initial)
    epd.drawPaged(drawFull, 0);
}

Schließlich versetzen wir im Loop die ESP32 nach dem partiellen Refresh in den Schlafmodus.

void loop() {
  epd.drawPaged(drawPartial, 0);
  epd.hibernate();  
  esp_sleep_enable_timer_wakeup(1000);
  esp_deep_sleep_start();
}

Laut Dokumentation der init() Funktion muss nur sichergestellt sein, dass die Display-Stromversorgung während des Deep-Sleep erhalten bleibt. Der oben gezeigte Aufruf von hibernate ist dafür geeignet. Das folgende Video zeigt, wie das aussieht.

Partial refresh of e-Paper display with deep-sleep
Partielles Refresh des e-Paper-Displays mit Deep-Sleep

Da millis() nach Deep-Sleep zurückgesetzt wird und konstant 151 Millisekunden anzeigt, zeige ich zusätzlich eine Zufallszahl (0..9) nach dem msec-Wert an, um das Refresh sichtbar zu machen.

Partielles Refresh mehrerer Bereiche

Manchmal möchte man mehrere Bereiche an unterschiedlichen Positionen und Zeiten partiell refreshen. Zum Beispiel könnte man die Anzeige einer Uhr jede Sekunde aktualisieren, aber die Temperaturanzeige nur alle 5 Minuten.

Die Erweiterung des obigen Codes, um mehrere Bereiche partiell zu refreshen, ist einfach. Schau dir den folgenden kompletten Code an, der den Millisekundenwert in zwei verschiedenen Bereichen aktualisiert.

#define ENABLE_GxEPD2_GFX 0

#include "GxEPD2_BW.h"

GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));

void drawText(int16_t x, int16_t y, const char *text) {
  int16_t x1, y1;
  uint16_t w, h;
  epd.getTextBounds(text, x, y, &x1, &y1, &w, &h);
  epd.setPartialWindow(x1, y1, w, h);
  epd.setCursor(x, y);
  epd.print(text);   
  epd.drawRect(x1, y1, w, h, GxEPD_BLACK); 
}

void drawFull(const void* pv) {
  epd.setFullWindow();
  epd.setCursor(40, 60);
  epd.print("msec: ");
}

void drawPartial1(const void* pv) {
  char text[16];
  sprintf(text, "1: %8d", millis());
  drawText(120, 60, text);
}

void drawPartial2(const void* pv) {
  char text[16];
  sprintf(text, "2: %8d", millis());
  drawText(120, 40, text);
}

void setup() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.setTextColor(GxEPD_BLACK);
  epd.setTextSize(2);
  epd.drawPaged(drawFull, 0);
}

void loop() {  
  epd.drawPaged(drawPartial1, 0);
  epd.drawPaged(drawPartial2, 0);
  epd.hibernate();
}

Wie du siehst, definieren wir einfach zwei Funktionen drawPartial1() und drawPartial2(), die in der Loop-Funktion aufgerufen werden, um die partiellen Refreshs der zwei Displaybereiche durchzuführen.

void loop() {  
  epd.drawPaged(drawPartial1, 0);
  epd.drawPaged(drawPartial2, 0);
  epd.hibernate();
}

Um es etwas interessanter und praktischer zu machen, habe ich eine Funktion drawText() hinzugefügt, die die Begrenzungsbox für einen gegebenen Text berechnet, das Refresh-Fenster auf dieselben Dimensionen setzt, den Text ausgibt und die Begrenzungsbox zeichnet:

void drawText(int16_t x, int16_t y, const char *text) {
  int16_t x1, y1;
  uint16_t w, h;
  epd.getTextBounds(text, x, y, &x1, &y1, &w, &h);
  epd.setPartialWindow(x1, y1, w, h);
  epd.setCursor(x, y);
  epd.print(text);   
  epd.drawRect(x1, y1, w, h, GxEPD_BLACK); 
}

Wenn du diesen Code verwendest, stelle sicher, dass der Text immer die gleiche Länge hat. Ein Text, der zwischen den Updates schrumpft, kann Display-Artefakte hinterlassen, da die Begrenzungsbox und das Refresh-Fenster kleiner werden. Im obigen Code verwende ich den Format-String "%8d", um sicherzustellen, dass der Text trotz wechselnder Zahlen konstant lang bleibt. Der Clip unten zeigt den Code in Aktion:

Partial refresh of multiple e-Paper display areas
Partielles Refresh mehrerer e-Paper-Displaybereiche

Beachte, dass das Refresh jetzt doppelt so lange dauert, da wir zwei unabhängige Updates durchführen. Du kannst nicht zwei verschiedene Refresh-Fenster (setPartialWindow) in derselben Draw-Funktion verwenden. Aber du könntest ein größeres Fenster angeben, das beide Inhalte umfasst, die du aktualisieren möchtest. Dann müsstest du dir auch keine Sorgen um unterschiedliche Textlängen machen.

Partielles Refresh mit Bildschirmpuffer

Als letztes Beispiel wollte ich einen einfachen Datenplotter implementieren, z.B. zur Überwachung der Temperatur über die Zeit. Das folgende Bild zeigt, wie der Plotter aussehen wird. Er hat einen festen Titel „Temperature“, eine feste x-Achse und dynamisch wechselnde, simulierte Temperaturwerte.

Temperature plotter on e-Paper display
Temperatur-Plotter auf e-Paper-Display

Idealerweise würden wir einzelne Pixel (Datenpunkte) mit einem 1-Pixel Refresh-Fenster plotten. Das funktioniert aber nicht, da die Dimensionen des Refresh-Fensters auf Vielfache von 8 begrenzt sind (siehe oben). Wenn du das versuchst, wirst du feststellen, dass das größere Refresh-Fenster einige zuvor geplottete Pixel und Teile der Achse löscht.

Um dieses Problem zu umgehen, zeichnen wir zuerst auf eine sogenannte „Leinwand“ (canvas). Eine Leinwand ist im Grunde ein Bildschirmpuffer, den wir im Hintergrund aktualisieren können. Das partielle Refresh nimmt dann einen kleinen Bereich der Leinwand und nutzt ihn, um das Display zu aktualisieren.

Partial refresh using canvas
Partielles Refresh mit Canvas

Der folgende Code enthält die komplette Implementierung dieses kleinen Temperatur-Datenplotters.

#define ENABLE_GxEPD2_GFX 0

#include "GxEPD2_BW.h"

// W, H flipped due to setRotation(1)
const int W = GxEPD2_290_BS::HEIGHT;
const int H = GxEPD2_290_BS::WIDTH;

const uint16_t WHITE = GxEPD_WHITE;
const uint16_t BLACK = GxEPD_BLACK;

GxEPD2_BW<GxEPD2_290_BS, GxEPD2_290_BS::HEIGHT> epd(GxEPD2_290_BS(5, 0, 2, 15));
GFXcanvas1 canvas(W, H);

void initCanvas() {
  canvas.setTextColor(BLACK);
  canvas.fillScreen(WHITE);
  
  canvas.setTextSize(2);
  canvas.setCursor(10, 10);
  canvas.print("Temperature");

  for (int i = 0; i < 10; i++) {
    int x = 10 + i * W / 10, y = H / 2;
    canvas.setTextSize(1);
    canvas.setCursor(x - 2, y + 10);
    canvas.print(i);
    canvas.drawLine(x, y - 5, x, y + 5, BLACK);
  }
  canvas.drawLine(10, H / 2, W - 10, H / 2, BLACK);
}

void drawCanvas() {
  epd.drawBitmap(0, 0, canvas.getBuffer(), W, H, WHITE, BLACK);
}

void drawFull(const void* pv) {
  epd.setFullWindow();
  drawCanvas();
}

void drawPartial(const void* pv) {
  static int16_t x = 10;
  x += 1;
  if (x > W - 10) x = 10;
  int16_t y = (H / 2) + (x / 10.0) * sin(x / 10.0);
  canvas.writePixel(x, y, BLACK);
  epd.setPartialWindow(x, y, 1, 1);
  drawCanvas();
}

void setup() {
  initCanvas();
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.drawPaged(drawFull, 0);
}

void loop() {
  epd.drawPaged(drawPartial, 0);
  epd.hibernate();
}

Zuerst definieren wir Hilfskonstanten für die Display-Abmessungen sowie Vorder- und Hintergrundfarben. Beachte, dass Breite und Höhe vertauscht sind, da wir das Display (setRotation(1)) in der setup Funktion drehen.

// W, H flipped due to setRotation(1)
const int W = GxEPD2_290_BS::HEIGHT;
const int H = GxEPD2_290_BS::WIDTH;

const uint16_t WHITE = GxEPD_WHITE;
const uint16_t BLACK = GxEPD_BLACK;

Canvas-Objekt

Der nächste wichtige Punkt ist, dass wir das Canvas-Objekt mit denselben Dimensionen wie das Display erstellen. Da mein e-Paper-Display nur zwei Farben (Schwarz, Weiß) hat, erstellen wir ein Canvas mit 1-Bit Farbtiefe (GFXcanvas1).

GFXcanvas1 canvas(W, H);

Wenn dein Display mehr Farben oder Graustufen hat, musst du ein passendes Canvas erstellen. Schau dir dazu die Adafruit GFX Graphics Library documentation an.

initCanvas und drawCanvas

Die initCanvas() Funktion schreibt einfach den Titel und zeichnet die x-Achse auf das Canvas, zeigt aber nichts an. Die eigentliche Anzeige der Grafik erfolgt durch drawCanvas(), das das Canvas als Bitmap auf das Display kopiert.

drawFull und drawPartial

Die drawFull() Funktion führt ein vollständiges Refresh durch, während die drawPartial() Funktion ein partielles Refresh macht. Während des vollständigen Refreshs zeichnen wir Titel und Achse, beim partiellen Refresh die einzelnen Datenpunkte.

Die folgende Formel erzeugt die simulierten Temperaturdaten y, und x ist die simulierte Zeit.

y = (H / 2) + (x / 10.0) * sin(x / 10.0);

Der wichtige Teil ist, dass wir den Datenpunkt (x,y) als Pixel auf das Canvas schreiben, dann ein Refresh-Fenster genau um diesen Pixel setzen und das Canvas zeichnen:

void drawPartial(const void* pv) {
  ...
  canvas.writePixel(x, y, BLACK);
  epd.setPartialWindow(x, y, 1, 1);
  drawCanvas();
}

Da das partielle Refresh-Fenster auf (x,y,1,1) gesetzt ist, refreshen wir nicht das ganze Display, sondern nur einen kleinen Bereich um den Pixel (denk an die 8er-Multiplikator-Beschränkung). Da dieser Bereich aber vom Canvas stammt, enthält er bereits gezeichnete Inhalte und löscht keine bestehenden Inhalte auf dem Display. Der folgende Clip zeigt den Plotter in Aktion:

Data plotter on e-Paper display
Datenplotter auf e-Paper-Display

Du siehst, dass die Kurve die x-Achse schneidet, ohne sie zu löschen, und es gibt kein Flackern. Obwohl das Refresh nicht sehr schnell ist (ca. 1 Sekunde), ist es schnell genug für einen Temperaturplotter.

Beachte, dass Deep-Sleep in diesem Code nicht einfach hinzugefügt werden kann. Wir müssten das gesamte Canvas im RTC-Speicher ablegen, der normalerweise nicht groß genug (8K) ist. Du könntest nur die Datenpunkte speichern, aber dann wäre die partielle Neuzeichen-Funktion etwas komplexer, da du die gesamte Kurve neu zeichnen müsstest. Alternativ könntest du das Canvas auf einer SD-Karte oder anderem größeren externen Speicher ablegen.

Empfehlung

Die Waveshare Manual gibt folgende Empfehlungen für den Betrieb eines e-Paper-Displays (von mir gekürzt und umformuliert):

Vollständiges Refresh: Das e-Paper-Display flackert während des Refresh-Prozesses mehrmals (Anzahl der Flacker hängt von der Refresh-Zeit ab), das Flackern dient dazu, Nachbilder zu entfernen und den besten Anzeigeeffekt zu erzielen.

Partielles Refresh: In diesem Fall gibt es während des Refresh-Prozesses kein Flackern. Nach mehreren partiellen Refresh-Vorgängen sollte ein vollständiges Refresh durchgeführt werden, um Nachbilder zu entfernen. Andernfalls können Nachbilder dauerhaft werden.

Es wird empfohlen, das Refresh-Intervall des e-Ink-Bildschirms auf mindestens 180 Sekunden zu setzen (außer bei Produkten, die partielles Refresh unterstützen).

Nach einem Refresh-Vorgang wird empfohlen, das Display auszuschalten oder in den Ruhezustand zu versetzen.Das verlängert die Lebensdauer des Displays und reduziert den Stromverbrauch.

Bei Verwendung eines dreifarbigen e-Ink-Bildschirms wird empfohlen, das Display mindestens einmal alle 24 Stunden zu aktualisieren.

e-Paper-Displays sind für den Innenbereich empfohlen, nicht für den Außenbereich. Wenn du das Display draußen verwenden möchtest, platziere es im Schatten und decke den weißen Klebeteil des Verbindungskabels des e-Paper-Bildschirms vollständig mit 3M-Klebeband ab.

Fazit

In diesem Tutorial hast du gelernt, wie man partielle Refreshs eines e-Paper-Displays durchführt, um Flackern zu vermeiden und schnellere Refresh-Zeiten zu erreichen. Wir haben speziell betrachtet, wie man partielle Refreshs für einzelne und mehrere Bereiche durchführt, in Kombination mit Deep-Sleep und pixelgenauen Inhaltsupdates.

Du könntest das kleine Plotter-Beispiel leicht erweitern, indem du einen echten Temperatursensor wie den BME280 in das Projekt einbindest. Schau dir das Weather Station on e-Paper Display Tutorial für mehr Details an. Du könntest auch separate Canvas für Temperatur, Luftfeuchtigkeit und Luftdruck verwenden und dann zwischen den Canvas wechseln, um verschiedene Daten anzuzeigen.

Wenn du Fragen hast, frag gerne im Kommentarbereich. Viel Spaß beim Basteln ; )