Porting LittlevGL for a monochrome display

Leveraging LvGL’s capabilities on a low resource project

My line of work brings me close to small embedded systems with minimal UI: a few LEDs or some digits displays. From time to time however I end up working with small monochrome screens, sparking the need for a working albeit simple GUI library.

Microchip’s attempt at GUI support. Needless to say, it’s barely functional.

Monochrome Configuration

LittlevGL is written entirely in C and uses a macro system to configure its settings and features. There is a template configuration on the root of the repository that needs to be copied into a file named lv_conf.h and to be included by all other sources. There are a lot of options, but for the sake of simplicity we can focus on just a few of them:

/* Maximal horizontal and vertical resolution to support by the library.*/
#define LV_HOR_RES_MAX (240)
#define LV_VER_RES_MAX (128)
/* Color depth:
* - 1: 1 byte per pixel
* - 8: RGB233
* - 16: RGB565
* - 32: ARGB8888
#define LV_COLOR_DEPTH 1
/* Enable anti-aliasing (lines, and radiuses will be smoothed) */
#define LV_ANTIALIAS 0
/*1: Enable the Animations */
/* Enable GPU optimization */
#define USE_LV_GPU 0

Library Initialization

Let’s look at how to actually initialize the library. The following code manages all the necessary operations:

    static uint8_t gbuf[8*240];
static lv_disp_buf_t disp_buf;

lv_disp_buf_init(&disp_buf, gbuf, NULL, 8*240);

lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv); /*Basic initialization*/
disp_drv.buffer = &disp_buf; /*Set an initialized buffer*/
disp_drv.flush_cb = my_flush_cb; /*Callback*/
disp_drv.set_px_cb = my_set_px_cb; /*Callback*/
disp_drv.rounder_cb = my_rounder; /*Callback*/
lv_disp_t * disp;
disp = lv_disp_drv_register(&disp_drv); /*Register the driver and save the created display objects*/
/*Input device*/
lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv); /*Basic initialization*/
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = my_input_read;
/*Register the driver in LittlevGL and save the created input device object*/
lv_indev_t * my_indev = lv_indev_drv_register(&indev_drv);
In this example pixels are grouped by vertical bytes; each “segment” draws an 8-bit column.
bool my_input_read(lv_indev_drv_t * drv, lv_indev_data_t*data)
data->point.x = Touch_Coord[0];
data->point.y = Touch_Coord[1];
data->state = f_touch_detected ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL;
return false; /*No buffering now so no more data read*/


Other than booting up, LvGL also requires two function to be called periodically to work: lv_task_handler and lv_tick_inc.

while (1) {

Porting LvGL

One of LvGL’s most endearing aspects is how well it manages integration with the display driver. One cannot expect to find a common API in embedded systems, so the best solution is simply to leave to the developer the task to connect hardware and GUI: LVGL exposes a handful of callbacks that signal when and what to show on the screen; how to do it is up to you.

  • rounder_cb : a callback asking you to “round” the area that needs to be updated. More on this later.
  • set_px_cb : asks to draw a pixel into the buffer


Possibly the most important callback for LvGL, it signals what must actually be sent on the screen. LvGL optimizes drawing by only refreshing the areas that changed, and flush_cb is its way to tell the driver to proceed.

  • lv_area_t *area : a tuple of coordinates that limit the screen area that should be updated
  • lv_color_t *color_p: a pointer to the buffer memory where the graphical objects are drawn. This contains the data that should be flushed to the display
void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
int row = area->y1, col;
unsigned int address = (unsigned int)row * HW_COLUMNS + area->x1/8;
uint8_t *buffer;
int linelen = (area->x2 - area->x1)/8;

buffer = (uint8_t*) color_p;

for (row = area->y1; row <= area->y2; row++) {
flush_one_row(address, buffer, linelen);
buffer += linelen+1;
address += HW_COLUMNS;

  • color_p will point to an area inside the buffer we used to initialize the LvGL display (gbuf). There is no guarantee however on where this will be: widgets are not drawn in gbuf in any particular order or position, it works similarly to an allocation heap.
  • area on the other hand tells me exactly where the flushing should refresh the screen, so I know the address that updates the LCD’s RAM.
  • When the function is done flushing lv_disp_flush_ready should be called. Here it is used at the end of the callback, but it could signal that a longer procedure has been completed (for example, a DMA transfer or a delayed refresh).


This one is a little tricky. I mentioned before that in monochrome LCDs pixels are often grouped in bytes; other than that, they might accept updates to their RAM only in special formats (like in specific batches, or by writing to fixed-size memory pages). Because of that LvGL may use areas that don’t make sense during the updating process.

void my_rounder(struct _disp_drv_t * disp_drv, lv_area_t *a)
a->x1 = a->x1 & ~(0x7);
a->x2 = a->x2 | (0x7);


The biggest shortcoming of LvGL (for my purposes) is that it does not natively manage single-bit pixel values. It is mainly used for color display where every pixel is at least one byte wide, but it would be absurd to use a buffer 8 times bigger than necessary with a monochrome LCD. Instead of integrating this functionality, LvGL delegates it to you.

void my_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)
buf += buf_w/8 * y;
buf += x/8;
if(lv_color_brightness(color) > 128) {(*buf) |= (1 << (7 - x % 8));}
else {(*buf) &= ~(1 << (7 - x % 8));}
  • buf is the memory range where LvGL wants you to update the pixel
  • buf_w is the width of said range; the y coordinate tells you its height
  • x and y are the coordinates of the pixel to update
  • color is, in a monochrome setting, either full or clear
  • opa is the opacity value, unused here for obvious reasons

A small example

LvGL provides many practical tutorials, examples and demos. Unfortunately, many of them will fail or malfunction in a monochrome environment: for me either the screen or the MCU’s available memory were too small. This is a small example with just a click button and a changing label:

lv_obj_t *label2, *label1;
static void btn_event_cb(lv_obj_t * btn, lv_event_t event)
if(event == LV_EVENT_RELEASED) {
lv_label_set_text(label1, "RELEASED");
} else if (event == LV_EVENT_PRESSED) {
lv_label_set_text(label1, "CLICKED");
int main (void)


static lv_disp_buf_t disp_buf;
lv_disp_buf_init(&disp_buf, gbuf, NULL, 8*240);
lv_disp_drv_t disp_drv;
disp_drv.buffer = &disp_buf;
disp_drv.flush_cb = my_flush_cb;
disp_drv.set_px_cb = my_set_px_cb;
disp_drv.rounder_cb = my_rounder;

lv_disp_t * disp;
disp = lv_disp_drv_register(&disp_drv);

lv_indev_drv_t indev_drv;
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb =my_input_read;
lv_indev_t * my_indev = lv_indev_drv_register(&indev_drv);

lv_obj_t * scr = lv_disp_get_scr_act(NULL);
lv_theme_t * th = lv_theme_mono_init(0, NULL);
/* Set the mono system theme */

/*Create a Label on the currently active screen*/
label1 = lv_label_create(scr, NULL);
lv_label_set_text(label1, "");
lv_obj_set_pos(label1,30, 30);// position, position);
/*Create a button on the currently loaded screen*/
lv_obj_t * btn1 = lv_btn_create(scr, NULL);
lv_obj_set_event_cb(btn1, btn_event_cb); /*Set function to be called when the button is released*/
//lv_obj_align(btn1, label2, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 20); /*Align below the label*/
lv_obj_set_pos(btn1, 30, 50);

/*Create a label on the button (the 'label' variable can be reused)*/
label2 = lv_label_create(btn1, NULL);
lv_label_set_text(label2, "Click me!");
lv_obj_set_size(btn1, 100, 20);
while (1) {

Additional Notes

As I’ve mentioned in the introduction, at first I could not use LvGL because of its memory occupation. The MCU I used for this tutorial is a PIC24EP512GP206, which sports 512 KB of flash memory: LvGL alone takes up half of that.

Computer Science Master from Alma Mater Studiorum, Bologna; interested in a wide range of topics, from functional programming to embedded systems.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store