Looking for a good e-paper library

Hi
I'd like to test e-paper displays, so I'll buy some on Aliexpress. Here are the references I chose for now:

I'm also looking for libraries to display text and images on these displays, using an ESP32. My main request is to be able to rotate the screen whenever I want. Do you know any library able to do that? (maybe it's possible to modify the library to add this function?)

Thanks for your help.

Hi lesept,

no chance with 1s refresh time for 3-color e-paper displays, physical reasons.

See GxEPD2/MyEPDs_UpdateInfos.pdf at master · ZinggJM/GxEPD2 · GitHub

Thanks for the information. So I must expect from 1 to 4 seconds for a full refresh depending on the display size, for 2 colors.

So how would you advise (to a newbie like me) to use this kind of display?

What I want to do is display the time on one and weather info on the other (including a very simple icon, such as a stylized sun or cloud)?
If I only update each second the part of the display which displays the seconds for example, is it quicker (event for 3 colors)? Are the libraries fitted for that?
Weather is not supposed to change very often...

I would recommend to use a small b/w e-paper display for frequent updates.
For b/w update time is dependent on full refresh or partial refresh mode, for the displays that have "fast partial refresh", but not dependent on the size of the partial window.

3-color displays have no "fast partial refresh", for details see the table.

Of course I recommend GxEPD2, as it supports graphics and text using Adafruit_GFX. See README.md.

See also post Waveshare e-paper displays with SPI - #788 by ZinggJM - Displays - Arduino Forum

Thanks again : I had seen your library, but does it enable to change the orientation of the display on request?

Rotation is supported for drawing through Adafruit_GFX methods. setRotation(r) must be called before drawing. Content on display (in controller buffer) can't be rotated, like with some TFTs.

That's what I meant : I want to change the rotation angle (0, 90, etc degrees) and begin drawing on an empty display. I think it's the same as what you explain.

Thanks a lot, I can't wait receiving my displays...

Good Afternoon All,

I am at present building a small project with an 1.54inch e-Paper screen and an Arduino pro-mini
and memory limitations are a concern.

I have a first prototype that works but I am at 98% memory usage so I have no room left for further features (and i would rather not change platform).

I have been using the (very good) library from Mr ZinggJM (the GxEPD2 incarnation).
The version of ePaper that I use is the new GDEH0154D67

I was wondering if any one was aware of a library with a smaller footprint.
Would the GxEPD library (whilst slightly less easy to use) have a smaller footprint?
I have looked at the waveshare library but the version that works for the GDEH0154D67 only has static bitmap/pictures display (or I was not able to find a working example with text and drawings).

The functions I use are fairly basic:

  • Drawing lines, rectangles, circles and triangles.
  • Displaying text.
  • Using partial refresh to target sections of the screen where to display items.

Any suggestion about a "lightweight" library or any way to make the existing library lighter (if there is any option to drop features that would result in program space saving) will be welcome.

I wish you all a pleasant and safe WE.
Regards.
H

GxEPD2_Example.ino compiled for Arduino Uno for GDEH0154D67:

Sketch uses 31056 bytes (96%) of program storage space. Maximum is 32256 bytes.
Global variables use 1501 bytes (73%) of dynamic memory, leaving 547 bytes for local variables. Maximum is 2048 bytes.

same with bitmaps disabled:

// select only one to fit in code space
//#include "bitmaps/Bitmaps200x200.h" // 1.54" b/w
//#include "bitmaps/Bitmaps104x212.h" // 2.13" b/w flexible GDEW0213I5F
//#include "bitmaps/Bitmaps128x250.h" // 2.13" b/w
//#include "bitmaps/Bitmaps128x296.h" // 2.9"  b/w
//#include "bitmaps/Bitmaps176x264.h" // 2.7"  b/w
////#include "bitmaps/Bitmaps400x300.h" // 4.2"  b/w // not enough code space
////#include "bitmaps/Bitmaps640x384.h" // 7.5"  b/w // not enough code space
// 3-color
//#include "bitmaps/Bitmaps3c200x200.h" // 1.54" b/w/r
//#include "bitmaps/Bitmaps3c104x212.h" // 2.13" b/w/r
//#include "bitmaps/Bitmaps3c128x296.h" // 2.9"  b/w/r
//#include "bitmaps/Bitmaps3c176x264.h" // 2.7"  b/w/r
////#include "bitmaps/Bitmaps3c400x300.h" // 4.2"  b/w/r // not enough code space
Sketch uses 20506 bytes (63%) of program storage space. Maximum is 32256 bytes.
Global variables use 1497 bytes (73%) of dynamic memory, leaving 551 bytes for local variables. Maximum is 2048 bytes.

Still a bit heavy on code space for AVR Arduino. I don't have a bare minimum example at hand to check.
Don't know how much Adafruit_GFX and FreeFonts contribute.

You can reduce RAM usage by reducing RAM for page buffer:

#if defined(__AVR)
#if defined (ARDUINO_AVR_MEGA2560)
#define MAX_DISPLAY_BUFFER_SIZE 5000 // e.g. full height for 200x200
#else
#define MAX_DISPLAY_BUFFER_SIZE 800 // 
#endif
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))

Hi Jean-Marc

Many thanks for the fast answer. Much appreciated!!

I had already disabled the bitmaps.

I am happy with the buffer size at the moment (my issue is more program/code size than variable/memory size).

The Adafruit font is using quite a bit (2K - I am only using ONE font).
I have found a font editor (Font Converter) that has helped me create a different font that uses a bit less of memory (as the fonts are stored in PROGMEM) even though they do not look as nice. I get 642bytes back that way

I am contemplating editing the adafruit font to remove the characters I don't use so I could keep the look of the font I like and save space (I hope that if I manage to do this, I could save probably 25% of the font size).

Not sure where to look next to save space other than diving in various libraries to look for things to trim out (but I assume that all well written libraries will not include items that are not explicitly used by the calling program).

Regards
H

Hi All,

On another note, I read that it is good practice to refresh the screen in full every now and then when one only does partial refresh for too long (may be once a day?).

Is there a particular function to call in the GxEDP library to do a "refresh"

or

Is this a matter of re-displaying the while screen (in my case a full window of 200x200) with whatever text & graphics I need to show?

Note: I suspect it is the later as the first option (refresh call) would assume that the whole data to display is still present in the buffer/memory ready to be displayed.

Have a great WE.
Regards
H

@hml_2,

GxEPD has no refresh method.

GxEPD2 does have refresh methods.

To get an overview of methods available in GxEPD2 you could take a look at GxEPD2_GFX.h, this is easier to read than the template class header GxEPD2_BW.h

   virtual void refresh(bool partial_update_mode = false) = 0; // screen refresh from controller memory to full screen
    virtual void refresh(int16_t x, int16_t y, int16_t w, int16_t h) = 0; // screen refresh from controller memory, partial screen

If you get ghosting after some time, then it is best to use clearScreen:

    virtual void clearScreen(uint8_t value = 0xFF) = 0; // init controller memory and screen (default white)

Jean-Marc

// GxEPD2_MinimumExample.ino by Jean-Marc Zingg

#include <GxEPD2_BW.h> // including both doesn't hurt
#include <GxEPD2_3C.h> // including both doesn't hurt

// copy constructor for your e-paper from GxEPD2_Example.ino, and for AVR needed #defines
#define MAX_DISPLAY_BUFFER_SIZE 800 // 
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))
GxEPD2_BW<GxEPD2_154_D67, MAX_HEIGHT(GxEPD2_154_D67)> display(GxEPD2_154_D67(/*CS=10*/ SS, /*DC=*/ 8, /*RST=*/ 9, /*BUSY=*/ 7)); // GDEH0154D67

void setup()
{
  display.init();
  display.firstPage();
  do
  {
    display.fillScreen(GxEPD_WHITE);
    // comment out next line to have no or minimal Adafruit_GFX code
    display.print("Hello World!");
  }
  while (display.nextPage());
}

void loop() {};

with Hello World:

Sketch uses 13284 bytes (41%) of program storage space. Maximum is 32256 bytes.
Global variables use 1235 bytes (60%) of dynamic memory, leaving 813 bytes for local variables. Maximum is 2048 bytes.

without Hello World:

Sketch uses 13230 bytes (41%) of program storage space. Maximum is 32256 bytes.
Global variables use 1221 bytes (59%) of dynamic memory, leaving 827 bytes for local variables. Maximum is 2048 bytes.

This result is unexpected. Needs further analysis.

Jean-Marc

// Adafruit_GFX_MinimalExample by Jean-Marc Zingg

#include <Adafruit_GFX.h>

class MiniDisplay : public Adafruit_GFX
{
  public:
    MiniDisplay() : Adafruit_GFX(200, 200) {};
    void drawPixel(int16_t x, int16_t y, uint16_t color) {};
};

MiniDisplay display;

void setup() {};
void loop() {};
Sketch uses 4872 bytes (15%) of program storage space. Maximum is 32256 bytes.
Global variables use 80 bytes (3%) of dynamic memory, leaving 1968 bytes for local variables. Maximum is 2048 bytes.

The minimal code space used by Adafruit_GFX is about 4872 bytes.

The reason for this are the virtual methods. Virtual methods can't be optimised out by the linker, because there needs to be a valid pointer in any object instances vtable for each.

This explains why the code used doesn't change essentially by commenting out Hello World.

The same reason is the cause of the larger amount of code used by GxEPD2 than expected.
All public methods of the driver classes are virtual, to be able to use base class references or pointers, by setting #define ENABLE_GxEPD2_GFX 1, which takes even more code space, by virtualizing the template class methods.

But there are 2 methods in the driver classes that are nearly the same:

    virtual void writeImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0;
    virtual void writeImagePart(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap,
                                int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0;

writeImagePart() is the more general one. So I could reduce code by calling writeImagePart() from writeImage(). (writeImagePart() was added later).

Jean-Marc

Good Morning Jean-Marc,

Merci beaucoup!!!

Thanks for this in depth analysis and explanation about the virtual methods (make sense).
I shall get cracking and have a go at calling writeImagePart() from writeImage().

Have a great WE.
Regards.
H

I just found a post that could help me analyze code space use: Re: Where did all my memory go?

And an older one: Re: Getting the map file

And Arduino tips and tricks

Posted here to remember.

in C:\Users\ZinggJ\AppData\Local\Arduino15\packages\arduino\hardware\avr\1.8.2\platform.txt I now have:

compiler.c.elf.flags={compiler.warning_flags} -Os -g -flto -fuse-linker-plugin -Wl,--gc-sections,-Map,{build.path}/linker.map

Now I have a file linker.map, but I don't know how to interpret it, or find useful information.

Linker Map Files don't seem to be what they used to.

avr-objdump -C -t GxEPD2_MinimumExample.ino.elf>dump.txt

00000656 l     F .text	0000005a Print::write(unsigned char const*, unsigned int)
000006b0 l     F .text	0000001e HardwareSerial::availableForWrite()
000006ce l     F .text	00000028 HardwareSerial::read()
000006f6 l     F .text	0000001c HardwareSerial::peek()
00000712 l     F .text	00000018 HardwareSerial::available()
0000072a l     F .text	00000014 Serial0_available()
008001ba l     O .bss	0000009d Serial
0000073e l     F .text	00000014 serialEventRun()
00000752 l     F .text	00000044 HardwareSerial::_tx_udr_empty_irq()
00000796 l     F .text	0000009a HardwareSerial::write(unsigned char)
00000830 l     F .text	00000040 HardwareSerial::flush()
00000870 l     F .text	00000052 turnOffPWM
000008c2 l     F .text	0000005c digitalWrite
000005ec l     O .text	00000014 digital_pin_to_timer_PGM
000005d8 l     O .text	00000014 digital_pin_to_bit_mask_PGM
000005c4 l     O .text	00000014 digital_pin_to_port_PGM
000005ba l     O .text	0000000a port_to_output_PGM
0000091e l     F .text	00000078 pinMode
000005b0 l     O .text	0000000a port_to_mode_PGM
00000996 l     F .text	0000004a micros
008001b6 l     O .bss	00000004 timer0_overflow_count
000009e0 l     F .text	00000066 delay
00000a46 l     F .text	00000002 Adafruit_GFX::invertDisplay(bool)
00000a48 l     F .text	0000002a Adafruit_GFX::setRotation(unsigned char)
00000a72 l     F .text	0000044a Adafruit_GFX::drawChar(int, int, unsigned char, unsigned int, unsigned int, unsigned char, unsigned char)
000000b0 l     O .text	00000500 font
00000ebc l     F .text	000001a4 Adafruit_GFX::write(unsigned char)
00001060 l     F .text	000000be Adafruit_GFX::drawRect(int, int, int, int, unsigned int)
0000111e l     F .text	000000f0 Adafruit_GFX::drawLine(int, int, int, int, unsigned int)
0000120e l     F .text	0000003a Adafruit_GFX::fillScreen(unsigned int)
00001248 l     F .text	0000008a Adafruit_GFX::fillRect(int, int, int, int, unsigned int)
000012d2 l     F .text	00000072 Adafruit_GFX::drawFastHLine(int, int, int, unsigned int)
00001344 l     F .text	00000072 Adafruit_GFX::drawFastVLine(int, int, int, unsigned int)
000013b6 l     F .text	00000002 Adafruit_GFX::startWrite()
000013b6 l     F .text	00000002 Adafruit_GFX::endWrite()
000013b8 l     F .text	00000020 Adafruit_GFX::writeFillRect(int, int, int, int, unsigned int)
000013d8 l     F .text	00000018 Adafruit_GFX::writeFastHLine(int, int, int, unsigned int)
000013f0 l     F .text	00000018 Adafruit_GFX::writeFastVLine(int, int, int, unsigned int)
00001408 l     F .text	0000000e Adafruit_GFX::writePixel(int, int, unsigned int)
00001416 l     F .text	0000015a Adafruit_GFX::writeLine(int, int, int, int, unsigned int)
00001570 l     F .text	000000de GxEPD2_154::drawNative(unsigned char const*, unsigned char const*, int, int, int, int, bool, bool, bool)
0000164e l     F .text	00000142 GxEPD2_154::drawImagePart(unsigned char const*, unsigned char const*, int, int, int, int, int, int, int, int, bool, bool, bool)
00001790 l     F .text	000000de GxEPD2_154::drawImage(unsigned char const*, unsigned char const*, int, int, int, int, bool, bool, bool)
0000186e l     F .text	00000128 GxEPD2_154::drawImagePart(unsigned char const*, int, int, int, int, int, int, int, int, bool, bool, bool)
00001996 l     F .text	000000b8 GxEPD2_154::drawImage(unsigned char const*, int, int, int, int, bool, bool, bool)
00001a4e l     F .text	00000074 GxEPD2_154::writeImage(unsigned char const*, unsigned char const*, int, int, int, int, bool, bool, bool)
00001a4e l     F .text	00000074 GxEPD2_154::writeNative(unsigned char const*, unsigned char const*, int, int, int, int, bool, bool, bool)
00001ac2 l     F .text	000000ac GxEPD2_154::writeImagePart(unsigned char const*, unsigned char const*, int, int, int, int, int, int, int, int, bool, bool, bool)
00001b6e l     F .text	00000080 GxEPD2_EPD::_reset()
00001bee l     F .text	000001a6 GxEPD2_EPD::init(unsigned long, bool, bool)
008001b0 l     O .bss	00000001 SPIClass::initialized
00001d94 l     F .text	00000018 GxEPD2_EPD::init(unsigned long)
00001dac l     F .text	00000002 SPIClass::endTransaction()
00001dae l     F .text	0000000e SPIClass::transfer(unsigned char)
00001dbc l     F .text	00000006 SPIClass::beginTransaction(SPISettings)
00001dc2 l     F .text	000000a0 GxEPD2_EPD::_writeCommandDataPGM(unsigned char const*, unsigned char)
00001e62 l     F .text	0000003e GxEPD2_EPD::_writeData(unsigned char)
00001ea0 l     F .text	00000056 GxEPD2_EPD::_writeCommand(unsigned char)
00001ef6 l     F .text	000000d8 GxEPD2_154::_setPartialRamArea(unsigned int, unsigned int, unsigned int, unsigned int)
00001fce l     F .text	000000a6 GxEPD2_154::_InitDisplay()
00002074 l     F .text	00000176 GxEPD2_BW<GxEPD2_154, 32u>::drawPixel(int, int, unsigned int)
000021ea l     F .text	0000001c GxEPD2_BW<GxEPD2_154, 32u>::fillScreen(unsigned int)
00002206 l     F .text	00000002 Print::flush()
00002206 l     F .text	00000002 GxEPD2_EPD::setPaged()
00002208 l     F .text	0000006c GxEPD2_EPD::writeImagePartAgain(unsigned char const*, int, int, int, int, int, int, int, int, bool, bool, bool)
00002274 l     F .text	00000038 GxEPD2_EPD::writeImageAgain(unsigned char const*, int, int, int, int, bool, bool, bool)
000022ac l     F .text	0000000e GxEPD2_EPD::writeScreenBufferAgain(unsigned char)
000022ba l     F .text	00000006 Print::availableForWrite()
000022c0 l     F .text	0000001a Print::write(char const*) [clone .part.2] [clone .constprop.49]
000022da l     F .text	00000004 __cxa_pure_virtual
000022de l     F .text	00000198 GxEPD2_EPD::_waitWhileBusy(char const*, unsigned int)
00000068 l     O .text	0000000a port_to_input_PGM
00002476 l     F .text	00000036 GxEPD2_154::_Update_Part()
000024ac l     F .text	00000036 GxEPD2_154::_Update_Full()
000024e2 l     F .text	00000034 GxEPD2_154::_PowerOff()
00002516 l     F .text	0000002a GxEPD2_154::hibernate()
00002540 l     F .text	00000004 GxEPD2_154::powerOff()
00002544 l     F .text	0000003c GxEPD2_154::_PowerOn()
00002580 l     F .text	00000026 GxEPD2_154::_Init_Part()
00000091 l     O .text	0000001f GxEPD2_154::LUTDefault_part
000025a6 l     F .text	000000f8 GxEPD2_154::refresh(int, int, int, int)
0000269e l     F .text	000002e6 GxEPD2_154::writeImagePart(unsigned char const*, int, int, int, int, int, int, int, int, bool, bool, bool)
00002984 l     F .text	00000262 GxEPD2_154::writeImage(unsigned char const*, int, int, int, int, bool, bool, bool)
00002be6 l     F .text	00000084 GxEPD2_154::writeScreenBuffer(unsigned char)
00002c6a l     F .text	00000024 GxEPD2_154::_Init_Full()
00000072 l     O .text	0000001f GxEPD2_154::LUTDefault_full
00002c8e l     F .text	0000004a GxEPD2_154::refresh(bool)
00002cd8 l     F .text	000000f8 GxEPD2_154::clearScreen(unsigned char)
// GxEPD_MinimumExample by Jean-Marc Zingg

#include <GxEPD.h>

// select the display class to use, only one, copy from GxEPD_Example
#include <GxGDEH0154D67/GxGDEH0154D67.h>  // 1.54" b/w

#include <GxIO/GxIO_SPI/GxIO_SPI.h>
#include <GxIO/GxIO.h>

// constructor for AVR Arduino, copy from GxEPD_Example else
GxIO_Class io(SPI, /*CS=*/ SS, /*DC=*/ 8, /*RST=*/ 9); // arbitrary selection of 8, 9 selected for default of GxEPD_Class
GxEPD_Class display(io, /*RST=*/ 9, /*BUSY=*/ 7); // default selection of (9), 7

void setup()
{
  display.init();
  display.eraseDisplay();
  // comment out next line to have no or minimal Adafruit_GFX code
  display.drawPaged(drawHelloWorld); // version for AVR using paged drawing, works also on other processors
}

void drawHelloWorld()
{
  display.print("Hello World!");
}

void loop() {};
Sketch uses 14894 bytes (46%) of program storage space. Maximum is 32256 bytes.
Global variables use 1485 bytes (72%) of dynamic memory, leaving 563 bytes for local variables. Maximum is 2048 bytes.

uses even more than GxEPD2.

without Hello World:

Sketch uses 14672 bytes (45%) of program storage space. Maximum is 32256 bytes.
Global variables use 1471 bytes (71%) of dynamic memory, leaving 577 bytes for local variables. Maximum is 2048 bytes.

Note that support for GDEH0154D67 in GxEPD is not yet released, needs more tests.

Jean-Marc

Good Morning.

I have not yet attempted to save code by following your recommendation (calling writeImagePart() from writeImage() )).

As it will probably take me some time to figure out how to do this without breaking anything else, I am trying to assess the potential benefits.

Do you have an idea as to how much program space this change would/could save?

Regards
H

00002984 l     F .text 00000262 GxEPD2_154::writeImage(unsigned char const*, int, int, int, int, bool, bool, bool)

About less than 610 bytes only. This compilation was for GDEP015OC1 instead of GDEH0154D67, but it's about the same value. I also tried with commenting out methods in GxEPD2_EPD.h that don't need to be virtual, but this reduced by less than 1k bytes. So for now it is not worth the effort, but I will think about this for a later release. But ok, I attach the modified GxEPD2_EPD.h, so you can try.

Jean-Marc

Your change would be:

void GxEPD2_154_D67::_writeImage(uint8_t command, const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm)
{
  _writeImagePart(command, bitmap, 0, 0, w, h, x, y, w, h, invert, mirror_y, pgm);
}

not checked.

GxEPD2_EPD.h (6.95 KB)