Library LCDGFX by Alexey Dynda: Poor documentation

Hi there,

I'm trying to use a ST7735-Display with an arduino nano and was really dissapointed about the adafruit-ST7735- and adafruit gfx-library, being so slow. However, I found Alexey Dynda's ssd1306-library which seems to be a lot faster. It has support for the ST7735-Display and can fluently animate sprites, using framebuffers.

The developer claims his library being so fast, it was even possible writing games with it.

Has anybody experience with this library? The documentation is very poor. Search engines always give me the same website.

My only sources are the official website and the few examples coming with the library.

The more I try to understand them the more questions come up:

For instance:

-How does the canvas work?
-Why are there several kinds of canvas-objects?
-what kinds of objects do exist (for instance: there is engine.canvas.fillRect, canvas.fillRect, ssd1306_fillRect (this time with underscore!)..and maybe other versions of the same function)?
-How does the nano-engine work?
-Why do some sketches need to call canvas.blt() to put the canvas on screen, while others don't seem to use it at all?
-Why can some sketches (e.g. nano-engine.ino) draw the whole screen with canvas, when the size is so limited (e.g. 32x32 px)?
-Is there really no circle-drawing-function?
-What is the difference between double-buffering,sprites and canvas?

When I try to draw a rectangle, it's not filled with solid color. Instead I'm getting a stripes-pattern. Don't know why.

Would be glad for some support here. Thanks in advance.

After intensive study of the library and its examples I can answer a few questions myself:

  1. The canvas is a small rectangle of the screen buffered in memory. Things drawn into the canvas are super-fast. On arduino nano the size is max. 32x32 pixels only, because nano has only 1 KB of SRAM.

  2. Depending on the amount of colors, a pixel needs a different amount of space. An 8-bit-pixel needs one byte per pixel in memory (canvas8), a monochrome pixel needs only on bit (canvas1) and a 16 Bit-pixel needs 2 bytes per pixel (canvas16).
    So before using canvas, someone has to decide which colorspace to use. I prefer 8-bit since calculation is easiest.

  3. The library seems to have three drawing levels:

  • Directly sending drawing-commands to the display (e.g. with ssd1306_fillRect), sending drawing-commands to the canvas in sram (canvas.fillRect) and letting the enginge manage the drawing (engine.canvas.fillRect).
    Because the canvas is so small in size, canvas.fillRect can only draw inside the 32x32-pixel-box. So this is why the nano-engine of the library is getting interesting...

4+5) ...the nano-engine can manage the whole screen and the drawing: It divides the screen into many canvas-tiles and updates only needed ones (tiles where changes occur). How the nano-engine is doing this is a little bit secret. However this makes the whole thing very fast.

  1. There really is no circle-drawing-function. However, there is adafruit-gfx-support somehow. It's possible to import/connect the library somehow. I didn't test it, since I made my own (very slow) circle-drawing-function.

  2. Double-buffering: Data is buffered in program memory and sram (I think). The engine seems to manage the whole screen-drawing and can copy the proper tile of the screen into a canvas in sram, dividing even the the drawing commands in tiles. At least I think this is how it works.
    Sprites: Small bitmaps in canvas. They can be moved around easier with special commands. The engine can automatically update alle pixels, a sprite is touching on its moving-path.
    canvaz: A tile of the screen buffered in sram, which is super-fast to draw.

However, I'm still hitting dead-ends: The engine doesn't seem to work as intended.

In theory, a basic nano-engine-sketch always looks like this:

#include "ssd1306.h"
#include "nano_engine.h"

NanoEngine8 engine;	//Define an 8-Bit nano-engine-object

bool drawAll(){
		
	// This is the main-function for the nano-engine
	// All drawing-commands come in here
	// The engine is calling this function in a non-regular
	// way, to do its secret magic.
	// It's like the backbuffer of the whole screen

	// Drawing some stuff:
	
	engine.canvas.clear();	//clearing the sram-buffer
	engine.canvas.setColor(RGB_COLOR8(255,255,255)); // pick white color for the next drawing-command
    	engine.canvas.fillRect(30, 39, 100, 90); // draw a box

	return true; //the function always has to end with this
		     //for the engine to work properly.
} //endfunce drawAll

void setup (){
	// Initialize Display:
	st7735_128x160_spi_init(8 , 10 , 9); // My LCD-connection: DC: D9, CS: D10, Reset: D8, DIN: D11, CLK: D13 
					     // (DIN and CLK are always the same, because pins for MOSI and SCK are
					     // defined in arduino-hardware)
	ssd1306_setMode( LCD_MODE_NORMAL );  // Normal-mode is standard for color
  	st7735_setRotation(2); 		    //screen rotation 0 - normal, 1 - 90 CW, 2 - 180 CW, 3 - 270 CW
  	ssd1306_setFixedFont(ssd1306xled_font6x8_German); // standard font was: ssd1306xled_font6x8
  	ssd1306_fillScreen8(RGB_COLOR8(255,255,255)); // After power-on display is full of noise if this command is omitted

	//Initialize Nano-Engine
  	engine.begin();
  	engine.setFrameRate(30); //Nano-engine will try to refresh the screen with 30 fps  
  	engine.drawCallback( drawAll ); /* Set callback to draw parts, when NanoEngine8 asks */
					// This is the main function for all drawings !!!	

} // endfunc setup

void loop {
 
	//Running the nano-engine always needs these three commands in a row:
	if (!engine.nextFrame()) return;  // nano-engine is updating with fixed fps, as defined in setup-function
   	engine.refresh();  // Mark whole screen-content for refresh (to mark 
			   // a specific range only: refresh(rect.p1.x, 
			   // rect.p1.y, rect.p2.x, rect.p2.y), found this 
			   // command in the sourcecode, never used in the examples. 
			   // They always refresh whole screen for unknown reasons.)
   	engine.display();  // Do the refresh of marked parts of the screen    

} //end of main loop

My problem now: Conditions in the drawAll-function seem to get ignored. For instance, I want to draw a symbol in grey and it shall switch to black when a signal is received. The drawing-function never draws the black version of the symbol for some reason:

bool drawAll(){   
    engine.canvas.clear();
    engine.canvas.setColor(RGB_COLOR8(255,255,255));
    engine.canvas.fillRect(0, 0, 128, 160);         // White Box as Background

 if (signal==false){
      engine.canvas.setColor(RGB_COLOR8(180,180,180)); //Grey Symbol
    }else{
      engine.canvas.setColor(RGB_COLOR8(0,0,0)); //Black Symbol
    } //endif signal 
    engine.canvas.setMode(CANVAS_MODE_TRANSPARENT); // Transparent. Alternatives: CANVAS_MODE_TRANSPARENT, CANVAS_TEXT_WRAP, CANVAS_TEXT_WRAP_LOCAL 
    engine.canvas.drawXBitmap1(0, 0, Symbol_width, Symbol_height, Symbol); // Draw Symbol
 return true; // if to return false, the engine will skip this part of screen update
}//endfunc drawAll

Here is another example from the README-file, provided with the library (ssd1306/src/nano_engine/README.md). This
sketch doesn't use a drawAll-function. Instead, all objects can be drawn in the main-loop of the sketch. I had to
modify one line to circumvent a compile-error:

//## What if not to use draw callbacks

// If you don't want to use draw callbacks in your application, but still need a power of NanoEngine, then there is one 
// way for you: to use full-screen double-buffering with NanoEngine. The example, you will find below, shows how to 
// use full-screen double buffering for monochrome 128x64 ssd1306 oled display. This example can be run on Atmega328p 
// and more powerful micro controllers. It clears back-buffer every time engine says to redraw the frame. But you can 
// preserve previously prepared image by removing call to `engine.canvas.clear()`.

#include "ssd1306.h"
#include "nano_engine.h"

//NanoEngine<BUFFER_128x64_MONO> engine; // compile-error: BUFFER_128x64_MONO not found 
NanoEngine8 engine; //Sketch compiles with this

void setup()
{
    // Init SPI 128x64 monochrome oled.
    // 3 - RESET, 4 - CS (can be omitted, oled CS must be pulled down), 5 - D/C
   //  ssd1306_128x64_spi_init(3, 4, 5); // deactivated becaus I use another screen
   st7735_128x160_spi_init (8, 10, 9);

    engine.begin();
    engine.setFrameRate(30);
}

void loop()
{
    if (!engine.nextFrame()) return;
    engine.canvas.clear();    // This step can be removed, if you don't want to clear buffer
    engine.canvas.drawRect(15,12,70,55);
    engine.display();
}

This is just not working! The drawing-commands in the main-loop don't get executed!

This is an official arduino-library, available in the library-manager!

ssd1306 is completely different library. You provided example with "ssd1306.h" header. And that's wrong, since the issue, you're talking about, is submitted for lcdgfx library.

Can you point, which example from examples folder of lcdgfx library doesn't work?

Regarding wiki pages, the example from here works: Using NanoEngine for systems with low resources2 · lexus2k/lcdgfx Wiki · GitHub

This is just not working! The drawing-commands in the main-loop don't get executed!

This is an official arduino-library, available in the library-manager!

This is open source non-commercial project under MIT license. You're free to fix and to do pull request.
Please, be more careful, when submitting issues.

Poor documentation

Again, you're free to help, of course if you're interested in

AFAIK lcdgfx and ssd3306 is the very same library. It's the first time I see the word "lcdgfx.h" included in the header. All examples and even the readme file include "ssd1306.h".

So when opening the arduino library-manager, you search for lcdgfx and ssd1306 will show up. This is the one I installed.

BTW, lcdgfx.h doesn't exist at all in the library-folder.

Your example is the very same than the one in the README.md-file, with the exception of the different header-file, at the top of the code

lexus2k:
This is open source non-commercial project under MIT license. You're free to fix and to do pull request.
Please, be more careful, when submitting issues.
Again, you're free to help, of course if you're interested in

Well this i what I do here, right now - and I'm the only one! So be glad someone did do all the work trying to understand the library. There is not a single tutorial out there. I think it's because usage is so difficult to understand.

Do you have further hints on how to get the library to work? I placed two examples, in my previous post. All examples from the example-folder seem to run fine (btw, all use "ssd1306.h" in header).

AFAIK lcdgfx and ssd3306 is the very same library.

As I mentioned on github, these are 2 different libraries. ssd106 is C-style, while lcdgfx is C++ style. It's up to you, which library to use.

Please, intall lcdgfx library to run lcdgfx examples. If you experience some problems with installing lcdgfx library, let me know.

So be glad someone did do all the work trying to understand the library.

Examples are the good way to understand the library.

Going back to your project, you have color TFT 128x160 16-bit display, and you use Atmega328p. So, for full-screen buffer you need 128x160x2 ~ 40KiB of RAM, while Atmega328p has only 2KiB.
The only way out here for you is to use drawAll() callback function and NanoEngine, which allows to redraw only part of display content, when necessary.

Thank you, that's good to know. So I did get that right, that the engine's drawing-commands (e.g. engine.canvas.fillRect) can subdivide drawing-commands into tiles?
I ask this because I wrote a small circle-drawing function, drawing circles with dots. It is very slow of course and don't know what will happen when I try to call it with the drawAll-function and nano-engine, since my function doesn't divide anything into tiles.

If you are interested, here is my circle-function:

void drawCircle (uint8_t xpos, uint8_t ypos, uint8_t radius, uint16_t farbe=RGB_COLOR8(0,0,0)){

  float winkel=0;                //Angle in degree
  float winkelschritt=1.2;    //Length of  liear pieces on the circle in degrees. Choose 1 or 2 when using dots, 5 when using lines
  uint8_t xAnfang=0;
  uint8_t xEnde=0;
  uint8_t yAnfang=0;
  uint8_t yEnde=0;  
   
  engine.canvas.setColor(farbe);
  
  //Draw circle:
  for (winkel=0; winkel <= (360-winkelschritt); winkel+=winkelschritt){
    xAnfang=xpos+round(radius*cos((winkel-90)*pi/180));
    xEnde=xpos+round(radius*cos((winkel+winkelschritt-90)*pi/180));
    yAnfang=ypos+round(radius*sin((winkel-90)*pi/180));
    yEnde=ypos+round(radius*sin((winkel+winkelschritt-90)*pi/180));    
    
    //Draw Line:
    //engine.canvas.drawLine(xAnfang, yAnfang, xEnde, yEnde);  //draw circle using lines -> Circle looks "uneven"
    engine.canvas.putPixel(xAnfang, yAnfang); //  draw with dots -> slower, looks better
    engine.canvas.putPixel(xEnde, yEnde);    // draw with dots -> slower, looks better
    //Formula for Points on a circle: P(x;y)=(r*cos(alpha);r*sin(alpha))       
    
  }//end for

  
}//endfunc drawCircle

If you are interested, here is my circle-function:

Good, thank you. This is too slow algorithm. You can try midpoint circle algorithm or bresenham circle algorithm. Those ones are much faster.

So I did get that right, that the engine's drawing-commands (e.g. engine.canvas.fillRect) can subdivide drawing-commands into tiles?

In general, yes. But NanoEngine uses refresh() functions to understand, which area of the dispay needs to be updated. Thus, in main loop you need to use refresh() group functionsto tell NanoEngine, which parts of the diplay should be updated. Then NanoEngine will split all areas to be updated into tiles, and for each tile it will call draw() callback with specially configured canvas.

PS. Let move our discussion to github, since it's closer to the library and the issues related to the library.
PPS. I added drawCircle method on development branch.

A circle-function! Thank you!

Unforuntately, I don't get the lcdgfx-example to work. Maybe it's because I'm using a different display. Here is the example with my changes in the first 9 lines:

#include "lcdgfx.h"

// TFT-Pins:
#define TFT_CS     10       // TFT CS pin is connected to arduino pin 10
#define TFT_DC     9        // TFT DC pin is connected to arduino pin 9
#define TFT_RST    8        // TFT Reset-Pin is connected to arduino pin 8

DisplayST7735_128x160x16_SPI display(TFT_RST,{-1, TFT_CS,TFT_DC,0,-1,-1});;
NanoEngine8<DisplayST7735_128x160x16_SPI> engine(display);

bool drawAll()
{
    engine.getCanvas().clear();
    engine.getCanvas().setColor(RGB_COLOR8(255,255,0));
    engine.getCanvas().drawRect(15,12,70,55);
    return true;   // if to return false, the engine will skip this part of screen update
}

void setup()
{
    /* Init SPI 96x64 RBG oled. 3 - RESET, 4 - CS (can be omitted, oled CS must be pulled down), 5 - D/C */
    display.begin();

    engine.begin();
    engine.setFrameRate(30);
    /* Set callback to draw parts, when NanoEngine8 asks */
    engine.drawCallback( drawAll );
}

void loop()
{
    if (!engine.nextFrame()) return;
    engine.refresh();  // Makes engine to refresh whole display content
    engine.display();
}

Is my pin-assignment correct? Also, in my old sketch, I propably need to update a lot of things to lcdgfx's new functions. I'm getting a lot of compile errors, e.g.

.../libraries/lcdgfx/src/v2/nano_engine/tiler.h:450:7: note: declared private here

or

'NanoCanvas<16, 16, 8> NanoEngineTiler<NanoCanvas<16, 16, 8>, DisplayST7735_128x160x16_SPI>::canvas' is private within this context

I guess the syntax of commands is different in lcdgfx. Have to figure them out when I have time. You said already, lcdgfx' design was completely different.

Please, can you give me the syntax of DisplayST7735_128x160x16_SPI ? It's not in the documentation and also not in the sourcecode, since this function inherits from other classes. So there is no simple list of variables.

This is the class-definition from lcd_st7735.h:

class DisplayST7735_128x160x16_SPI: public DisplayST7735_128x160x16<InterfaceST7735<PlatformSpi>>
{
public:
    /**
     * @brief Inits 128x160x16 lcd display over spi (based on ST7735 controller): 16-bit mode.
     *
     * Inits 128x160x16 lcd display over spi (based on ST7735 controller): 16-bit mode
     * @param rstPin pin controlling LCD reset (-1 if not used)
     * @param config platform spi configuration. Please refer to SPlatformSpiConfig.
     */
    DisplayST7735_128x160x16_SPI( int8_t rstPin, const SPlatformSpiConfig &config = { -1, { -1 }, -1, 0, -1, -1 } )
        : DisplayST7735_128x160x16(m_spi, rstPin)
        , m_spi( *this, config.dc,
                 SPlatformSpiConfig{ config.busId,
                                     { config.cs },
                                     config.dc,
                                     config.frequency ?: 8000000,
                                     config.scl,
                                     config.sda } ) {}

However, in interface.h, I found this struct, which is used by the class, mentioned before:

typedef struct
{
    /**
     * bus id number. this parameter is valid for Linux, ESP32.
     * If -1 is pointed, it defaults to platform specific i2c bus (Linux spidev1.X, esp32 VSPI_HOST).
     */
    int8_t busId;

    /**
     * parameter is optional for all platforms, except Linux.
     * If chip select pin is not used, it should be set to -1
     * For Linux platform devId should be pointed, if -1, it defaults to spidevX.0
     */
    union
    {
        int8_t cs;
        int8_t devId;
    };

    /**
     * Data command control pin number. This pin assignment is mandatory
     */
    int8_t dc;

    /**
     * Frequency in HZ to run spi bus at. If 0, it defaults to max frequency, supported
     * by platform
     */
    uint32_t frequency;

    /**
     * Optional - spi clock pin number. -1 if to use default spi clock pin.
     * This is required for ESP32 platform only.
     */
    int8_t scl; // clk

    /**
     * Optional - spi data MOSI pin number. -1 if to use default spi MOSI pin.
     * This is required for ESP32 platform only.
     */
    int8_t sda; // mosi
} SPlatformSpiConfig;

So..was this syntax correct for the class DisplayST7735_128x160x16 ?:

DisplayST7735_128x160x16 (busId, TFT_CS-Pin, TFT_DC-Pin, frequency, clock, mosi)

busId was always -1 for default
frequency was always 0 for max speed
clock was always -1 for default pin
and mosi alwas -1 for default pin?

Well, than my line of code seems to be correct:

DisplayST7735_128x160x16_SPI display(TFT_RST,{-1, TFT_CS,TFT_DC,0,-1,-1});

...still not sure with the reset-pin, though...and why there are two curly braces...

I feel like a detective now, could you please...answer? I really believe your library is the answer to all my arduino-problems. It's propably the best graphics-library out there...if it was documented properly! Fast like hell with small memory-usage. That's perfect! Adafruit's graphics-library is causing flickering, ucg-lib needs over 80% of program-memory. Your library is the only alternative!

Update:
Still playing around with the line for the display-object. My display doesn't even turn-on. So there must be a problem with the initialization already.

Update 2:
OK, got it working now. It was my fault: I powered my display's backlight with a pwm-pin and forgot to set that up correctly. I can confirm the demo-script is running properly.

So, I finally found out how to modify my sketch to make it compatible with lcdgfx.:

In drawAll:
engine.canvas.drawXBitmap1 is now engine.getCanvas().drawBitmap1
engine.canvas.printFixed is now engine.getCanvas().printFixed
engine.canvas.setColor(RGB_COLOR8(0,0,0)) is now engine.getCanvas().setColor(RGB_COLOR8(0,0,0))

In Setup:

st7735_setRotation(2) is now display.getInterface().setRotation(2)
ssd1306_setFixedFont(ssd1306xled_font6x8_German) is now display.setFixedFont(ssd1306xled_font6x8_German)

ssd1306_setMode( LCD_MODE_NORMAL ) doesn't exist anymore, as well as
st7735_128x160_spi_init(TFT_RST , TFT_CS , TFT_DC ). Display-initialization now is done with a simple display.begin()

However, I still can't print text. In the drawAll-function, conditional statements don't get executed. I just don't know how to actually draw something on the screen, when something is happening.

I don't know if this was actually possible, but I think your library just fried two of my arduinos. I have no other explanation: I didn't change wiring of my project for three weeks and did a lot of tests, in this time-period. Now, when trying the new library, the program just crashed and I can't upload anything anymore.
I soldered everything to a new arduino, uploaded the code several times, played around with the code and suddenly again: bam, display goes dark, arduino not working anymore. No sketch-uploads possible.

Was this possible with a faulty library?

I'll make a question out of it:

Is the engine in your library writing things to the flash-memory or the EEPROM?

I don't know if this was actually possible, but I think your library just fried two of my arduinos.

No, that's not possible.

Is the engine in your library writing things to the flash-memory or the EEPROM?

No.
Just check carefully, what you do with hardware.

Just recovered one of the two arduinos successfully with re-burning the bootloader, using a programmer. So the bootloader got overwritten...but how and why? Here is a thread about this very question in general, without a sufficient answer, though. As I said, I didn't change the wiring for a long time. The sketch just ran in background while I was doing something else. It sent values to the serial-monitor...suddenly the program crashed and the bootloader was overwritten...strange thing.

Read westfw's posts in your link. Very carefully.

The IDE "Burn Bootloader" will do everything correctly. i.e. unlock, program boot flash, fuses. then set lockbits

Punters that choose to do it "by hand" e.g. in AS7.0 often omit one of these steps.

It is always wise to use the IDE "Burn Bootloader"

If you can corrupt the Uno Bootloader with an Arduino sketch (uploaded via bootloader) I will award you a medal.

Yes, you can erase memory and lockbits with an external programmer. Which is why you should remove programmer after the "Burn Bootloader" operation.

David.

Did you mean this part?

The AVR chip has some "memory protection" bits that would prevent the non-bootloader code from writing the bootloader space, but the arduino environment does not seem to set these, so that means that an inappropriate instruction in the sketch could overwrite parts of the bootloader...

Do you think after re-burning the bootloader, my arduino was now "save" because the fuses are set-up correctly now?

It's more likely that something like a buffer overflow causes the user sketch to "jump" to the middle of the bootloader code, just where it is about to do something like, oh, erase a page... The memory protection bits could still fix this...

This is possible of course, since the graphics-library is memory-hungry.

BTW, I'm not using an UNO but a arduino nano. Usually I use the USB-connection to upload sketches. This was the firtst and only time I used a programmer to burn the bootloader with the arduino IDE. Meanwhile, I also recovered my second nano with this procedure successfully.

I just ordered an ESP32 now since it is faster and has more memory. Still, I hope development of lcdgfx will be kept up. It's propably the fastest gfx-library out there and it is hardly known. Using arduino nano, micro or pro mini, driving a tft-display is just not possible with other libraries. The refreshes are just to slow with other graphics libraries.

I hope the developers will recognize the need for good documentation. Tutorials are needed to make lcdgfx more known in the world.

Do you think after re-burning the bootloader, my arduino was now "save" because the fuses are set-up correctly now?

Yes.

Note that you must be honest about your board. e.g. if you have a Nano, select Nano "old" or "new"

Old Nanos have a big wasteful bootloader. You have less Flash for your program.

The IDE checks whether you have enough Flash and SRAM when you compile.
The Bootloader will not let you upload a program that is too big.

David.

Thanks for the info. I'm indeed using old nanos (guess they were cheaper when I bought them).

You can change "Old Nano" to "New Nano" by Burning the "New Nano" bootloader.

If you have different types of Nano, I would convert them to the same type e.g. New.

If you only have Old Nano, leave as they are. (unless your programs don't fit)

Seriously. Any graphics project can easily use lots of Flash e.g. images, fonts, ...

David.