Does LVGL need (or can it work with) another event loop?

Apologies for the vague question, but I’m not quite familiar enough with LVGL to ask something more specific.

Typically when I write embedded code for a “low-level” environment (ie. no OS bigger than FreeRTOS), I’ll try to separate things by having eg. one component emit events I define (eg. USER_OPERATION_START) and another component that listens for these events and does the appropriate thing. Typically these components will be separated by role eg. network functions is one, serial console is another.

For example, with the ESP32 I’d use esp_event_handler_register(...) and esp_event_post(...) (which uses FreeRTOS under the hood). So a network component might post a DATA_REFRESH event that the UI registers to handle. Or the UI might post a OPERATION_CANCEL event that the network stack handles.

Now that I’m using LVGL on an embedded Linux system, I’m not sure how this all relates to LVGL’s own event loop ie. lv_tick_inc(...), lv_task_handler(...), lv_send_event(...). It looks like these are purely for UI objects ie. I should not be trying to overload them to process my own events that are not LVGL-related. So perhaps I need to use another event library like libev, and perhaps even hook LVGL into that instead of using pthreads/nanosleep etc (which is, in a way, using the kernel’s event loop), and note the caveats in the LVGL docs about thread safety.

If this is right, can anyone offer advice for a light-ish event library that might comfortably run on a router chipset with OpenWRT? (As in, if you’ve used one and it was good, I’d love to know about it.)

Or am I wrong about this, and it would be sensible to use LVGL’s event system for my own non-UI stuff? And if that’s right, are there examples or tutorials that go into more detail on such use?

Hi @detly

I haven’t worked with Linux very much at all so please excuse my lack of knowledge of the specific functions/semantics for Linux…

If I understand what your asking correctly I don’t think you need any third party libraries to achieve your goal.

I would create a native thread for LVGL which initialises it and then periodically calls lv_task_handler() I would then create a global message queue using the native environment to receive messages/events from other parts of the system to update the GUI at runtime. Next I would create a task function for LVGL which is used to process the message queue and is executed by the LVGL task scheduler, this is registered with a call to lv_task_create() This task is called periodically by the LVGL scheduler I have found in my systems a period of 30mS usually gives a good performance, but depending on your CPU and other system load you may need to change this. One thing to make sure is that you never call any blocking functions from the GUI message/event queue to avoid disrupting the responsiveness of the GUI. To deal with calls to blocking activities triggered from the GUI I normally create a second native thread which monitors a similar global message/event queue that LVGL can post non blocking requests to it then carries out access to things like the file system, flash, nework etc. I then run this thread at a lower priority than the GUI which keeps the GUI nice and responsive at all times.

Here are some code snippets for the first part of my description, hopefully this makes sense:

declare linux message queue  your_queue; (Global)

void gui_thread(void *p) {

	lv_disp_drv_t	disp_drv;
	lv_disp_buf_t	disp_buf;

	// Initialise Video Hardware
	set_video_prams( ... );
	// Initialise GUI
	lv_init();
	lv_disp_drv_init(&disp_drv);
	lv_disp_buf_init(&disp_buf, (void*)LV_VDB_ADR, (void*)LV_VDB2_ADR, (LV_HOR_RES_MAX*LV_VER_RES_MAX));
	disp_drv.flush_cb = your_disp_flush;
	disp_drv.buffer = &disp_buf;
	lv_disp_drv_register(&disp_drv);
    gui_create();  // Your code to create GUI
	create and initialise your_queue;
	while(1) {
		lv_task_handler();
		// Linux non blocking delay call for say 5mS (usleep() or nanosleep() I expect)
	}
}


void gui_create( void ) {

	// Create your objects and place them on the screen etc...
    lv_task_create((void*)process_msg_q, 30, LV_TASK_PRIO_LOW, NULL );  // Create a task in LVGL to listen to a message queue and perform GUI related requests
 	
}


static void process_msg_q ( void ) {

	uint16_t 			msg;

	while( ( linux_message_queue_receive( your_queue, &msg ) ) ) { // IMPORTANT: THIS MUST BE A NON-BLOCKING RECEIVE FUNCTION

			switch( msg ) {

				case UPDATE_PART1_OF_GUI:			//  Your own requests to update parts of the gui
					update_gui_part1();
					break;

				case UPDATE_PART2_OF_GUI:
					update_gui_part2();
					break;

				case UPDATE_PART3_OF_GUI:
					update_gui_part3();
					break;

				.....
				
				
				case UPDATE_GUI_THEME:
					update_gui_theme();
					break;

				default:
					break;
			}
		}
	}
}

I hope this all makes sense, and is helpful.

I you have any questions please ask…

Kind Regards,

Pete

Thanks for the detailed response!

Ah see this is the sort of thing where I can’t be bothered rolling my own, so maybe I do want a library to handle this for me, especially with blocking activities etc. (which I will definitely have). But your example code prompted me to look into Linux’s native message queues, which is a great starting point for what I need. (Typically I’ve used cross platform event loops like glib which are a little heavy for my use case.)

Thanks again!

1 Like

I’ve more or less settled on using libuv — it has a nice interface and extremely good documentation. But I do have one more question: according to the porting docs, I need to have a mutex around the lv_task_handler() call and around any other lv_...() calls from other tasks or threads. But

  1. Is there a built-in lv_...() function that does this? I know lv_tick_inc() doesn’t need this protection, what about say, lv_task_create()?
  2. Failing that, if I only call lv_ functions from the thread that runs lv_task_handler(), will that suffice? For example, in libuv, I might do:
uv_timer_t timer_lvgl_handler;
uv_timer_init(&loop, &timer_lvgl_handler);
uv_timer_start(&timer_lvgl_handler, lvgl_handler_cb, LVGL_TICK_MS/2, LVGL_TICK_MS);

To run lv_task_handler() in a timer task (offset from lv_tick_inc() by half the period). A simple version of that might be:

void lvgl_handler_cb(uv_timer_t * timer)
{
    lv_task_handler();
}

But what if I had something like:

void lvgl_handler_cb(uv_timer_t * timer)
{
    // bit of pseudocode here - store an event queue
    // or whatever as user data
    event_queue = timer->data;
    while (next_event = non_blocking_pop(timer->data))
    {
        lv_task_create(something_based_on_next_event);
    }
    lv_task_handler();
}

The way I’m thinking about it is that this exploits whatever mutexes/protection libuv or queue is using, right? Calls from within LVGL code could also post events to a similar queue for other libs to process. Does that make sense?

lv_tick_inc is the only function that has built-in protection (because you would often want to call it from an interrupt handler, etc.). All other functions need manual protection added.

Yes, because in that case, all LVGL code is running on one OS thread.

Yes; if libuv has mutexes already and you do everything through its own APIs, that should be sufficient.

2 Likes