LittlevGL on a Monochrome OLED - JohnHau/mis GitHub Wiki

https://blog.lvgl.io/2019-05-06/oled

Written by costascal on May 06, 2019

This tutorial describes how I got LittlevGL working on a small 128x64 OLED display with a PIC24FJ microcontroller. The OLED display board I used had a SH1106 driver chip, but the code would probably work nealry unchanged with an SSD1306, and should be easily adaptable to other display drivers. You should read the LittlevGL porting guide first as I’ll only cover the extra things you need to do for the monochrome OLED display here.

The SH1106 contains a frame buffer which is arranged into “Pages” which are horizontal 8 pixel stripes. Each page extends the whole width of the display, so has 128 columns. Writes to the display frame buffer are done by selecting a column number and a page number and then sending data bytes containing the pixels for each 8 pixel column. Any number of columns, up to the end of the page can be sent just be sending more data bytes.

The first file to set up is lv_conf.h. The main settings I used are shown below. As the PIC has limitted resources I set LV_MEM_SIZE fairly small. I also set LV_COLOR_DEPTH to 1 bit as the display is monochrome and LV_VDB_PX_BPP to 1 so that the VDB only uses one pit per pixel. I set the VDB size to 2048 pixels which works out to 256 bytes. I also turned off a lot of the fancier effects like shadows and antialiasing (as they don’t make sense in monochrome), and most of the themes except “MONO”.

#define LV_MEM_SIZE (2U * 1024U) /Size memory used by lv_mem_alloc in bytes (>= 2kB)/ #define LV_HOR_RES (128) #define LV_VER_RES (64) #define LV_ANTIALIAS 0 /1: Enable anti-aliasing/

#define LV_COLOR_DEPTH 1 /Color depth: 1/8/16/32/ #define USE_LV_SHADOW 0 /1: Enable shadows/ #define USE_LV_GROUP 0 /1: Enable object groups (for keyboards)/ #define USE_LV_GPU 0 /1: Enable GPU interface/ #define USE_LV_REAL_DRAW 0 /1: Enable function which draw directly to the frame buffer instead of VDB (required if LV_VDB_SIZE = 0)/ #define USE_LV_FILESYSTEM 0 /1: Enable file system (might be required for images/ #define USE_LV_LOG 0 /Enable/disable the log module/ // I turned off all the other themes #define USE_LV_THEME_MONO 1 /Mono color theme for monochrome displays/

Next we need to copy lv_porting/lv_port_disp_templ.c to lv_port_disp.c and add three functions to it:

set_px_cb() - this will be called whenever LittlevGL wants to write a pixel into the VDB and will handle writing the pixel data into bytes in the correct format to send straight to the display. For example if your display accepts vertically aligned pixels (e.g. one byte represents an 8 pixel vertical line) you will use this callback to dispose them correctly. flush_cb() - LittlevGL will use this call to ask your code to fflush an area to the display. rounder_cb() - this will make sure that the area sent to flush_cb() will always be a whole number of pages, as we always have to update the display using pages. Most monochrome displays will only accept data at least 1 byte wide, so it would not make sense for lvgl to try and refresh an area of 3x5 pixels: with this callback you should round buffer areas to the closest page (in this example, a vertical byte boundary).

So here is the set_px_cb() callback function: it just sets the correct bit in the passed in buf, using the page arangement that the SH1103 uses. It receives 7 arguments:

disp_drv: the display driver structure reference; unused in this implementation buf : the buffer where we should change the pixel. This will be a reference to part of a (possibly) bigger buffer indicated while initializing lvgl as usable memory range. buf_w : the width of the area starting at buf, the number of pixel columns that compose it. The number of rows is implicitly defined by the y coordinate. x and y: coordinates of the pixel to change. The buf_w parameter indicates how many columns each row is made of, so we can calculate the actual offset of the buffer. color : the value of the pixel, either 0 or 1. When LV_COLOR_DEPTH is 1 (monochrome) the lv_color_t type collapses to a union with a single significant bit field, the one we check to know wether to set or clear the pixel. opa : color opacity. Unused in monochrome configurations.

Note that it inverts the colors, this was the easiest way to get the effect I wanted:

define BIT_SET(a,b) ((a) |= (1U<<(b))) #define BIT_CLEAR(a,b) ((a) &= ~(1U<<(b))) void set_px_cb(struct _disp_drv_t * disp_drv, uint8_t * buf, lv_coord_t buf_w, lv_coord_t x, lv_coord_t y, lv_color_t color, lv_opa_t opa) { uint16_t byte_index = x + (( y>>3 ) * buf_w); uint8_t bit_index = y & 0x7; // == 0 inverts, so we get blue on black if ( color.full == 0 ) { BIT_SET( buf[ byte_index ] , bit_index ); } else { BIT_CLEAR( buf[ byte_index ] , bit_index ); } }

Next is the flush_cb() code. This works out the page range that needs to be written (row1 -> row2), sends the OLED command for the starting row and column, then sends the bytes out (one per 8 pixel column) and finally ends the transfer. It takes 3 arguments:

disp_drv : the display driver structure reference; unused in this implementation. area : the coordinates for the screen area to refresh packed in a lv_area_t structure. They should have been rounded by the rounder callback. color_p : the buffer containing the actual data to flush. Its dimentions are indicated by coordinates in area. It is an lv_color_t array, a data type that can be safely converted to uint8_t in case of monochrome displays; it is the same memory range we drew onto with the set_px_cb callback. static void flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { uint8_t row1 = area->y1>>3; uint8_t row2 = area->y2>>3; uint8_t buf = (uint8_t) color_p;

for(uint8_t row = row1; row <= row2; row++) {
  oledStartSend( row, area->x1 );
  for(uint16_t x = area->x1; x <= area->x2; x++) {
    oledByteOut(*buf);
    buf++;
  }
  oledEndSend();
}
/* IMPORTANT!!!
 * Inform the graphics library that you are ready with the flushing*/
lv_flush_ready();

}

Finally the simplest function, the round_cb() callback, it just increases the y limits of an area to the nearest page boundaries (in this case, the lower and bigger multiple of 8, or byte boundary):

void rounder_cb(struct _disp_drv_t * disp_drv, lv_area_t *area) { area->y1 = (area->y1 & (~0x7)); area->y2 = (area->y2 & (~0x7)) + 7; } Then to hook it all up you need to add some code into the lv_port_disp_init( ) function:

lv_refr_set_round_cb( round_cb );

disp_drv_p->vdb_wr = vdb_wr;
disp_drv_p->disp_flush = disp_flush;

The last low level task is to implement the OLED communication functions, these are very specific to the microcontroller and interface used, but here are mine for the PIC24FJ and SPI, this could get much fancier using the Enhanced FIFO or DMA, but I haven’t got that far yet:

#define SPI_DC LATAbits.LATA1 #define SPI_DATA() SPI_DC = 1 #define SPI_COMMAND() SPI_DC = 0 void oledByteOut( uint8_t data ) { SPI1BUF = data; while(SPI1STATbits.SPIRBF == 0); data = SPI1BUF; // have to read as well }

void oledCommand( uint8_t data ) { SPI_COMMAND(); oledByteOut(data); }

void oledStartSend( uint8_t row, uint8_t col ) { oledCommand( 0xB0 | row); // goto first column col += 2; // +2 for SH1106 - remove for SSD1306 oledCommand( 0x00 | (col & 0xF) ); // col LS 4 bits oledCommand( 0x10 | ((col>>4) & 0xF) ); // col MS 4 bits SPI_DATA(); } void oledEndSend() { } void oledInit() { TRISBbits.TRISB7 = 0; RPOR3bits.RP7R = 9; // RP7 is pin 16 on GB002, 9 selects SS1OUT to drive Chip Select

SPI1CON1 = 0; // Clear the content of SPI1CON1 register 
SPI1CON1bits.CKP = 1;   // Idle state for clock is a high level; active state is a low level
SPI1CON1bits.CKE = 0;   // Serial output data changes on transition from idle clock state to active clock state
SPI1CON1bits.MSTEN = 1; // Enable master mode
// oLed says 600ns -> 1.67Mhz, so use 4:1,4:1 = 1MHz (pg180)
//  measured period was 750ns, which is 1.3MHz - don't know why
SPI1CON1bits.PPRE = 2;  // primary prescaler 3 = 1:1, 2 = 4:1
SPI1CON1bits.SPRE = 5;  // secondary prescaler 5 = 4:1   
SPI1STATbits.SPIEN = 1; // Enable SPI peripheral

}

And then the last extra piece you need is to select the MONO theme after initializing the display in your main application:

oledInit();

static lv_disp_buf_t disp_buf;    /*Descriptor of a display buffer */
static lv_disp_drv_t disp_drv;    /*Descriptor of a display driver*/
static lv_color_t gbuf[DISP_BUF_SIZE];  /*Memory area used as display buffer */

lv_init();
lv_disp_buf_init(&disp_buf, gbuf, NULL, DISP_BUF_SIZE); /* DISP_BUF_SIZE can be smaller than the actual display */
lv_port_disp_init(&disp_drv);
// set mono theme - maybe this is the default for 1 bit/pixel anyway?
//  hue doesn't seem to make a difference
lv_theme_mono_init(0 /* hue */,NULL /* use LV_FONT_DEFAULT */);
lv_theme_set_current( lv_theme_get_mono() );

When initializing the display buffer you can use any size you see fit. lvgl will use this memory range to draw widgets to be refreshed: if a widget does not fit into the provided range it will be drawn and flushed in more than one go. To maximize performances one would use the entire screen size (in bytes, so pixels divided by 8):

#define DISP_BUF_SIZE (128*64/8)

⚠️ **GitHub.com Fallback** ⚠️