Image for post
Image for post

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.

Image for post
Image for post
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 */
#define LV_USE_ANIMATION 0
/* 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);
Image for post
Image for post
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*/
}

Runtime

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

while (1) {
lv_task_handler();
lv_tick_inc(1);
delay_ms(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

flush_cb

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;
}

lv_disp_flush_ready(disp_drv);
}
  • 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).

rounder_cb

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.

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

set_px_cb

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)
{
ConfigureOscillator();
InitializeSystem();

lv_init();

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);
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;
lv_indev_drv_init(&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 */
lv_theme_set_current(th);

/*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) {
lv_task_handler();
lv_tick_inc(1);
delay_ms(1);
}
}
Image for post
Image for post

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