olikraus / u8g2

U8glib library for monochrome displays, version 2
Other
5.04k stars 1.04k forks source link

Only update the changed pixels in full buffer mode #736

Closed cornedoggen closed 5 years ago

cornedoggen commented 5 years ago

Hi all,

I have a question about the update procedure of the u8g2 library. When you use the full buffer mode (f extension in constructor), can it be configured to only update the pixels/bytes which are changed since last time? For example: I have some text down in the display and an audio level meter in the top. The level meter should be updated very fast to give a smooth response for the user. If you need to redraw the entire display for every audio metering update, then the meter will behave too slow... :(. Therefore i think it is possible to only update the region / area which is changed. In other library which i used in the past ( RAMTEX - https://www.ramtex.dk ), they call this the 'dirty area'. It is very very fast! because if you change only a specific item/widget on the screen it updates that area!. The dirty area is a rectangle with (x0,y0, x1,y1) which automatically scales when you call the library functions for writing text, or drawing lines. When you call the send-buffer() function it should send the data bytes which are surrounded by the dirty area. What do you think about this feature?

Kind Regards,

Corné Doggen

olikraus commented 5 years ago

Thanks for comparing with RAMTEX library.

With respect to the dirty window: Such a feature would be thinkable with some restrictions. It can be implemented for full buffer mode only and it will increase flash ROM size.

I the size and position of this window is already known (should be true in your case), then this should be possible right now. hmmm in fact this should have no RAM/ROM impact for users, who do not use this function...

The use case would be this: You update a part of the screen say x0, y0, x1, y1. Then there would be a function, which sends only this part to the display: U8G2:sendWindow(x0, y0, x1, y1) Would this be a solution? I mean, there would be no automatic scaling of the dirty window, so comfort is lesser, but I think this is not required in your case (because you know your dirty window in advance).

Another impact is this: Such an update is only possible for displays with support the u8x8 API (ok, should be true for most of the displays). But to ensure that this works for your: What is your display type? One more restriction is the fact, that the coordinates of the dirty window, must be dividable by 8. This means, the actual window has to be U8G2:sendTileWindow(x0/8, y0/8, (x1+7)/8, (y1+7)/8) due to the memory layout of these black/white displays.

olikraus commented 5 years ago

I have implemented the above mentioned sub block transfer function here: https://github.com/olikraus/u8g2/blob/master/sys/sdl/full_buffer_send_window/main.c#L15 and here: https://github.com/olikraus/u8g2/blob/master/sys/sdl/full_buffer_send_window/main.c#L42

Here is the code which can be copied into your project.

/*
  Limitations:
    - Tile positions and sizes (pixel position divided by 8)
    - Any display rotation/mirror is ignored
    - Only works with displays, which support U8x8 API
    - Only available in full buffer mode
*/
u8g2_SendTileSubBuffer(u8g2_t *u8g2, uint8_t x, uint8_t y, uint8_t w, uint8_t h)
{
  uint16_t page_size;
  uint8_t *ptr;

  /* the page size in bytes is equal to the pixel width in bits */
  page_size = u8g2->pixel_buf_width;

  ptr = u8g2_GetBufferPtr(u8g2);
  ptr += x*8;
  ptr += page_size*y;

  while( h > 0 )
  {
    u8x8_DrawTile( u8g2_GetU8x8(u8g2), x, y, w, ptr );
    ptr += page_size;
    y++;
    h--;
  }  
}

/*
  sub buffer window in pixel coordinates
  lower left corner is NOT included, this means the transfer range is from
  x0..x1-1
  y0..y1-1
*/
void u8g2_SendWindow(u8g2_t *u8g2, u8g2_uint_t x0, u8g2_uint_t y0, u8g2_uint_t x1, u8g2_uint_t y1)
{
  x0 /= 8;
  y0 /= 8;
  x1 += 7;
  x1 /= 8;
  y1 +=7;
  y1 /= 8;
  u8g2_SendTileSubBuffer(u8g2, x0, y0, x1-x0, y1-y0);
}

For calling these functions within Arduino/C++ environment: Get the pointer to the u8g2 structure with U8G2:getU8g2(), so it will look like this:

u8g2_SendWindow(u8g2.getU8g2(), x0, y0, x1, y1); 
olikraus commented 5 years ago

Can we apply rotation/mirror for the pixel values? --> no ToDo: Copy example code to production code. --> only copy u8g2_SendTileSubBuffer ToDo: Make functions available in the C++ interface ToDo: Documentation

cornedoggen commented 5 years ago

Hi Oli,

Thank you so much for this update Oli! great!

In my case i have to update only the area on top of the display (128x128 SSD1327). There are the audio level meters located, and i know the coordinates of that area. So , the missing autoscale function is not a problem for me at the moment.

As a software developer i try to fully understand your implementation of the library. I have a few questions about it:

In the constructor it allocates memory with:

What are the 16, 16 telling me?? the 'f' is full buffer, thats clear to me. Is it 16 by 16 pixels/bytes? is 16x16 the Tile size? Are Tiles in horizontal or vertical direction? So, main question is.. how do the terms Page, Tile, and Row look like in a graphical representation in my mind?

Kind Regards, Corné

olikraus commented 5 years ago

What is the representation of: Page

This comes from the display controller datasheets. The memory of most black/white displays is organized as a list of pages. So for a full understanding, it is probably required to read the datasheets, like the SSD1306 datasheet.

But simply spoken, a page is a memory area on the display, which has a height of 8 pixel and a width which is identical to the width of the display. So for example a 128x64 pixel display, usually has a memory, which is organized into 8 pages of 8x128 pixel each.

Tile

A tile a quadratic area on the display, which contains 8x8 = 64 pixel. A page is a sequence of tiles, organized from left to right. So, the above page with 8x128 pixel is composed of 16 tiles.

I would love to see a picture of display and indicate what is a page, tile, and row.

There is a brief description in the reference manual. The table depicts the left part of the first (topmost) page of the display: https://github.com/olikraus/u8g2/wiki/u8g2reference#memory-structure-for-controller-with-u8x8-support

Row

Well, not so clearly defined, but usually a tile or buffer row. A buffer contains the memory for one page (1-constructor), two pages (2-constructor) or all pages (f-constructor)

So for a 128x64 display and the 1-constuctor, there are 8 rows (0..7). For the same display with the 2-constructor, there will be only 4 rows (0..3).

For the f-constructor (like in your case), the whole page and row topic does not make much sense, because the buffer will contain the complete frame buffer for the display.

u8g2_m_16_16_f()

U8g2 does not use malloc(), so it uses several different memory allocation procedures. U8g2 depends on the linker garbage collector: It is assumed, that functions, which are not called, are removed during linking stage.

The name includes the size of the memory in tiles. So in this case, a memory with 16x16 tiles are allocated. These are 16*8 x 16*8 = 128x128 pixel = 128x128/8 Bytes = 2048 Bytes: https://github.com/olikraus/u8g2/blob/master/csrc/u8g2_d_memory.c#L126

Are Tiles in horizontal or vertical direction?

Yes, tiles are horizontals blocks of 8x8 pixel each

how do the terms Page, Tile, and Row look like in a graphical representation in my mind?

The sdl u8g2 emulation will show the tiles as checkerboard (maybe little bit difficult to see). The topmost row of this checkerboard is one page.

screenshot

hope this helps...

cornedoggen commented 5 years ago

Excellent explanation!!

I did a git pull to update my u8g2 sources, but i can't find the new functions you added:

Or is it intentionally leaved out of the repo ? and i need to copy the code manually to my project?

In total i have connected 8 displays to my microcontroller via a SPI buss with 8 separate CS lines. So to initialize all the displays i create 8 instances of u8g2_t in an array:

struct displayInfo[8]

u8g2_t display[8]

To initialize the displays i call the corresponding constructor with the 'f' for all the displays. for every function call of the constructor i give the same callback references as the parameters. So all the calls from your library end in 2 callback functions, the gpio_and_delay_cb, and byte_cb. And now the problem arises: In the callback function i have no idea about which displays i have to control. For example.. i need to control the CS line of the display, but how do i know which one??

In most libraries this issue is solved by an extra void *userdata parameter in the callback function. This pointer is also available in the constructor and is referenced to an instance / object in your code.

void u8g2_Setup_ssd1327_midas_128x128_f(u8g2_t *u8g2, const u8g2_cb_t *rotation, u8x8_msg_cb byte_cb, void *byte_cb_userdata, u8x8_msg_cb gpio_and_delay_cb, void *gpio_and_delay_cb_userdata)

uint8_t *gpio_and_delay_cb(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr, void *user_data);

uint8_t *byte_cb(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr, void *user_data);

In my case it would be a reference to (void*)&displayInfo[0] for the first display. In the displayInfo struct the CS line pin can be found. which result to something like this in the callback:

`uint8_t gpio_and_delay_cb(u8x8_t u8x8, uint8_t msg, uint8_t arg_int, void arg_ptr, void user_data) { struct displayInfo info = (struct displayInfo )user_data;

switch(msg)
{
    case U8X8_MSG_GPIO_CS:              // CS (chip select) pin: Output level in arg_int
        ioport_set_pin_level( info->cs_pin, arg_int );
        break;
}
return 1;

}`

Kind Regards,

Corné

olikraus commented 5 years ago

Or is it intentionally leaved out of the repo ? and i need to copy the code manually to my project?

The functions are planed, but not part of the csrc directory. I added the functions to the sdl emulation directory: https://github.com/olikraus/u8g2/blob/master/sys/sdl/full_buffer_send_window/main.c#L15

So the code is in "sys/sdl/full_buffer_send_window" as part of the repo already.

And now the problem arises: In the callback function i have no idea about which displays i have to control.

You need to create 8 different gpio&delay procedures. Actually these gpio&delay procedures are custom user procedures, which need to be adjusted to your needs. In fact I do not know which and from where you took these procedures, but in general, you need to catch the msg (given as agument) and react accordingly. Here is an example for a gpio&delay procedure for display 3:

uint8_t *gpio_and_delay_cb_disp_3(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
switch(msg)
{
    case U8X8_MSG_GPIO_CS:              // CS (chip select) pin: Output level in arg_int
        ioport_set_pin_level( disp-3-cs-pin, arg_int );
        break;
    default:                // CS (chip select) pin: Output level in arg_int
        return gpio_and_delay_cb(u8x8, msg, arg_int, arg_ptr);
}
return 1;
}

So, instead of passing gpio_and_delay_cb to the constructor, use gpio_and_delay_cb_disp_3.

Details are documented here: https://github.com/olikraus/u8g2/wiki/Porting-to-new-MCU-platform

Ok, the above mentioned option allows you to customize u8g2 for all 8 displays without additional user pointer. U8x8 (and u8g2) also support a custom user pointer. However, you need to enable this in u8x8.h: https://github.com/olikraus/u8g2/blob/master/csrc/u8x8.h#L95

Once defined, you can use set and get the user pointer: https://github.com/olikraus/u8g2/blob/master/csrc/u8x8.h#L358

You can set the user pointer to a variable (uint16_t in the example below) with the pin number, and get the number in the gpio&delay procedure. I think it would look like this:

uint8_t *my_gpio_and_delay_cb(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
switch(msg)
{
    case U8X8_MSG_GPIO_CS:              // CS (chip select) pin: Output level in arg_int
        ioport_set_pin_level( *((uint16_t *)u8x8_GetUserPtr(u8x8)), arg_int );
        break;
    default:                // CS (chip select) pin: Output level in arg_int
        return gpio_and_delay_cb(u8x8, msg, arg_int, arg_ptr);
}
return 1;
}

The advantage will be, that you need to create only one gpio and delay procedure for all displays.

Then there is a third method: U8x8 can be compiled with an array, which can be used to store pin numbers. This is required for the Arduino environment, but could be used also by you. You need to compile with -DU8X8_USE_PINS:

https://github.com/olikraus/u8g2/blob/master/csrc/u8x8.h#L352

Once the array is added, you can assign the value of the cs pin to u8x8->pins[U8X8_PIN_CS]. Then the gpio&delay procedure will look like this:

uint8_t *my_gpio_and_delay_cb(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
switch(msg)
{
    case U8X8_MSG_GPIO_CS:              // CS (chip select) pin: Output level in arg_int
        ioport_set_pin_level( u8x8->pins[U8X8_PIN_CS], arg_int );
        break;
    default:                // CS (chip select) pin: Output level in arg_int
        return gpio_and_delay_cb(u8x8, msg, arg_int, arg_ptr);
}
return 1;
}

In my case it would be a reference to (void*)&displayInfo[0] for the first display. In the displayInfo struct the CS line pin can be found. which result to something like this in the callback:

If you already plan to use a displayInfo struct, then the use of the USER_PTR might be the best approach.

  1. Add -DU8X8_WITH_USER_PTR to your compiler call.
  2. Create a custom gpio&delay procedure:
uint8_t *my_gpio_and_delay_cb(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
struct displayInfo *info = (struct displayInfo *)u8x8_GetUserPtr(u8x8);

switch(msg)
{
    case U8X8_MSG_GPIO_CS:              // CS (chip select) pin: Output level in arg_int
        ioport_set_pin_level( info->cs_pin, arg_int );
        break;
    default:                // CS (chip select) pin: Output level in arg_int
        return gpio_and_delay_cb(u8x8, msg, arg_int, arg_ptr);
}
return 1;
}
  1. During init phase, assign the pointer to displayinfo struct via u8x8_SetUserPtr()
struct displayInfo[8]
u8g2_t display[8]
for( i = 0; i < 8; i++ )
{
    u8g2_Setup_ssd1327_midas_128x128_f( &(display[i]), U8G2_R0, byte_cb, my_gpio_and_delay_cb);
    u8x8_SetUserPtr(u8g2_GetU8x8(&(display[i])), (void *)&(display[i]) );
}
cornedoggen commented 5 years ago

Hi Oli,

Thank you so much again for the fast and comprehensive reply! Really appreciate that! :)

The hole idea behind asking for a user pointer was offcourse avoiding to have 8x times the same callback. I did not mention that, im sorry, but you understood already. i didn't knew the u8x8 object was designed to hold a reference user pointer. I will use that to point to my info struct!

Thumbs up for u8g2! Great and well designed library!

p.s. The only thing i am gonna change is the memory allocation. I will do it with malloc, because i have an external SRAM memory connected to the AVR and this is assigned to the heap. Therefore, i need to use malloc() to allocate memory for the 8 (full buffer) displays on the ext. SRAM. The avr (AT90CAN128) isn't sufficient to store all 8 display buffers into internal RAM. And ofcourse i can use the 1 or 2 constructor, but that consumes more time when writing data to the display(writing part by part)... and is not what i want.. i want fast display updates!.

Corné

olikraus commented 5 years ago

I will do it with malloc

Sure, just update u8g2_m_16_16_f(): Call malloc and return a pointer to the allocated memory.

to store all 8 display buffers into internal RAM

With the current default behavior, the memory would be shared among all 8 displays.

cornedoggen commented 5 years ago

to store all 8 display buffers into internal RAM

With the current default behavior, the memory would be shared among all 8 displays

? This last sentence i don't understand, since if you call the function u8g2_m_16_16_f() it allocates everytime you call it new memory.. right? and it should, because every display has other content. can you explain what you mean with 'sharing the memory among 8 displays' ?

is the buf pointer a global in your library , so all the functions refer to the global buf, and therefore share memory ? is that what you mean? I hoped the pointer to the buffer is stored in the u8x8 handle, so every function uses that reference. Another option is to let your global buf pointer every time point to one of my 8x created memory buffers with malloc BEFORE i call any of your functions from the library. But that requires an extra step every time i want to write to display.. 1- make sure buf points to the correct memory space. (one of the 8 display buffers ) 2 - call u8g2 lib functions.

I prefer the first solution whereby the buf pointer is part of the u8x8 handle. Just like the userdata pointer.

olikraus commented 5 years ago

u8g2_m_16_16_f() it allocates everytime you call it new memory.. right?

No. There is only one static memory buffer. u8g2_m_16_16_f() will always return the same address of the same buffer.

is the buf pointer a global in your library , so all the functions refer to the global buf, and therefore share memory ? is that what you mean?

yes.

I hoped the pointer to the buffer is stored in the u8x8 handle, so every function uses that reference.

U8x8 does not use any buffer. The U8x8 API writes directly to the display without buffer. If you mean u8g2 instead of ux88, then yes: Each u8g2 structure has a reference to its personal buffer. However the buffer is the same in your case for all the displays, because u8g2_m_16_16_f() will return the same address with each call. If you change

uint8_t *u8g2_m_16_16_f(uint8_t *page_cnt)
{
  static uint8_t buf[2048];
  *page_cnt = 16;
  return buf;
}

to

uint8_t *u8g2_m_16_16_f(uint8_t *page_cnt)
{
  *page_cnt = 16;
  return malloc(2048);
}

then each u8g2 object will get its individual buffer.

Please note: Each display contains its own internal memory. There is no need to store the content of each display in the memory of the controller. In fact this would be redundant to the content of the memory in the display. Of course it still might be required to improve performance to have a copy of the display content in the RAM of the microcontroller.

ghost commented 5 years ago

There is no need to store the content of each display in the memory of the controller

Offcourse this IS needed, Since the content of Every display is different. If all the displays would use the same memory, then writing to a display requires a full drawn of the display. This is not what i want. I want to update only the Area in the buffer what is charged en witte Those changes to the display.

I dont understand why You say buffering is nit needed.... since the 3 constructor methods do use buffering! What do You mean with this?

Regarding the buf pointer in u8x8, i ment the u8g2. You are right.

olikraus commented 5 years ago

Offcourse this IS needed,

It is needed in your case, because you do not want to do a full redraw.

e-music commented 5 years ago

Hello Everyone!

Well, in a nutshell, that is what I wanted exactly for my application and I have the very same scenario as the OP. My uC/SPI is fast enough to refresh the entire display at an excellent performance, but when it comes to audio applications where you need to update certain portions of the display at a very short intervals, like for FFT bars and audio meters, the "dirty area" update of LCD as the OP quoted from the Ramtex library comes into place. I can't wait to implement the u8g2_SendTileSubBuffer() function into my project.

I was thinking about changing the whole display for something else that can write on pixel boundaries, but I knew that this could be done in the library somehow. The SSD1322 with a 256*64 OLED display provides a great resolution and I still find it amazing for many projects over full color TFTs.

Will be back shortly!

Thanks

e-music commented 5 years ago

I have added the functions, but not sure how the calculation is done to convert coordinates from pixels to tiles. For example, suppose I want to draw a symbol at a certain coordinate like this: u8g2_DrawStr(&u8g2, 220, 40, "\xD9");

How to calculate the coordinates (in tiles) to update the area used by that symbol then pass that to the u8g2_SendWindow() function?

olikraus commented 5 years ago

Precondition: R0 is used in the constructor

To get the tile coordinates from the pixel position, divide x and y by 8. So in your case the tile position is x=27 and y=5.

If the rotation is anything else than R0, then the rotation as to be applied to the tile position manually (but the division by 8 is still required).

e-music commented 5 years ago

Rotation is R0, and I did exactly as you said for the x0,y0 coordinates, but nothing is being displayed on the LCD. I did also apply the same calculations for the x1,y1 coordinates as you indicated earlier "(x1+7)/8, (y1+7)/8)", but still nothing happens. Playing/stretching coordinates solves the issue and the symbol is displayed, but I guess I'm refreshing an area much bigger than used by the symbol concerned.

By the way, since the symbol is 16*16 pixels, I had to add 16 to each coordinate before division by 8.

olikraus commented 5 years ago

Playing/stretching coordinates solves the issue and the symbol is displayed, but I guess I'm refreshing an area much bigger than used by the symbol concerned.

True, the area is bigger. A tile contains 8*8 pixel.

By the way, since the symbol is 16*16 pixels, I had to add 16 to each coordinate before division by 8.

yes, also note, that glyphs have there reference point in the lower left corner by default.

e-music commented 5 years ago

Not sure if I understand what do you mean by "reference point" of glyphs. Anyways, could you please provide an example that shows how to draw a small filled box/rectangle/whatever and have that area only updated using the added functions?

olikraus commented 5 years ago

The sdl example is linked above. Arduino .ino example is planed. But this implementation is anyways at the beginning.

What exactly does not work? Can you post a more complete example?

e-music commented 5 years ago

Thank you very much for the quick follow up. Of course, I will explain exactly what I'm trying to do and what it doesn't work and what it does.

I'm trying to draw a symbol on an existing frame and have that displayed by refreshing the "dirty area" used by that symbol using the recently added function u8g2_SendWindow().

Here is the drawing function: u8g2_SetFont(&u8g2, u8g2_font_open_iconic_all_2x_t); u8g2_DrawStr(&u8g2, 200, 40, "\xE1");

According to the tile/pixel calculations provided above, the maximum coordinates for x0, y0 should not exceed 32, 8 respectively. Same also apply to the x1, y1 coordinates.

Given the calculations above, and adding 16 pixels for each coordinate (taken by the symbol), I suppose the call for the u8g2_SendWindow function has to be like this: u8g2_SendWindow(&u8g2, 25, 5, 28, 8);

But that doesn't work. I mean, the symbol is not shown on LCD. Instead, the following has worked for me so far: u8g2_SendWindow(&u8g2, 200, 30, 210, 34);

Any ideas?

Thanks again!

olikraus commented 5 years ago

ok, then there is a missunderstanding. The above mentioned SendWindow command already does the division by 8 for you:

void u8g2_SendWindow(u8g2_t *u8g2, u8g2_uint_t x0, u8g2_uint_t y0, u8g2_uint_t x1, u8g2_uint_t y1)
{
  x0 /= 8;
  y0 /= 8;
  x1 += 7;
  x1 /= 8;
  y1 +=7;
  y1 /= 8;
  u8g2_SendTileSubBuffer(u8g2, x0, y0, x1-x0, y1-y0);
}

Most of the code just divides the x and y values by 8. My missunderstanding was, that you want to call SendTileSubBuffer directly. One more point: SendWindow requires the upper left and lower right position to describe the window.

So, let us discuss u8g2_DrawStr(&u8g2, 200, 40, "\xE1"); The reference point should be the lower left corner of the icon, so 200, 40 is the lower left corner. The glyph size is 16x16, so the upper left corner is 200, 40-16 and the lower right is 200+16,40 (actually i am not sure whether it is 16 or 15... it depense on the exact location of the reference point, but with 16 you are on the safe side...) So the SendWindow command should be: u8g2_SendWindow(&u8g2, 200, 40-16, 200+16, 40);

e-music commented 5 years ago

Thank you very much. Worked flawlessly!

olikraus commented 5 years ago

Note: In U8g2 page buffer mode (_1_ constructor), individual rows (pages) can be updated independently. I have updated the reference manual for this: https://github.com/olikraus/u8g2/wiki/u8g2reference#setbuffercurrtilerow

https://github.com/olikraus/u8g2/blob/master/sys/arduino/u8g2_page_buffer/UpdatePartly/UpdatePartly.ino

olikraus commented 5 years ago

ToDo:

asfarley commented 5 years ago

This function was very helpful - I just integrated it into my ST7789 stuff and it's a huge improvement.

olikraus commented 5 years ago

:)

olikraus commented 5 years ago

current status: added updateDisplayArea command u8g2_full_buffer/UpdateArea/UpdateArea.ino Maybe it should be renamed to sendBufferArea to be more consistent. Documentation is missing

keeping the the name void u8g2_UpdateDisplayArea(u8g2_t *u8g2, uint8_t tx, uint8_t ty, uint8_t tw, uint8_t th)

UpdateDisplayArea has more limitations

AndyPeacock commented 5 years ago

Hi, the updateDisplayArea command is just what I need in my Arduino project. When is it likely to make it into the build available through the Arduino IDE library manager? Is there any way to build my own temporary version that I can use until an official update is released? Sorry for the questions but I'm not sure exactly how all this side of it works. Awesome work on the library by the way.

Thanks

olikraus commented 5 years ago

I have created a new beta release. It also includes the above mentioned example. Documentation is still not there...

You can download the latest U8g2 beta release from here: https://github.com/olikraus/U8g2_Arduino/archive/master.zip

  1. Remove the existing U8g2_Arduino library (https://stackoverflow.com/questions/16752806/how-do-i-remove-a-library-from-the-arduino-environment)
  2. Install the U8g2_Arduino Zip file via Arduino IDE, add zip library menu (https://www.arduino.cc/en/Guide/Libraries).
olikraus commented 5 years ago

https://github.com/olikraus/u8g2/wiki/u8g2reference#updateDisplay

olikraus commented 5 years ago

closing...