Microcontroller Units (MCUs) are the heart of countless embedded systems, powering devices from consumer electronics to industrial machinery. With modern applications demanding user interfaces, many of the electronics devices have graphical UI. Qt, a popular cross-platform framework for GUI development, offers a robust solution for building graphical user interfaces on MCUs with its variant called “Qt for MCUs”. While developing applications for MCUs often involves navigating the complexities of hardware constraints and real-time requirements, doing one for complex UI application with Qt needs more care and effort. Today, we'll delve into the process of creating Qt applications for MCUs, comparing development approaches for both bare-metal and Real-Time Operating Systems (RTOS).
Understanding Qt for MCU
Before diving into Qt application development, it's essential to understand the architecture of Qt for MCUs. With a development history of over 23 years, Qt is a C++ based library for backend integration with QML frontend development. It comes with a rich set of widgets, efficient memory management, modular architecture and portability. All these points keep the QT as one of the preferred options for any GUI product.
While this is suitable for processor-based systems, MCUs are far more limited in terms of memory and features. A heavy run time could not be supported and hence Qt launched the Qt for MCU variant targeted for smaller MCUs and applications.
The tech stack of Qt is captured along with that of Qt for MCU.

QT for MCU does not support multimedia, filesystem, Networking etc. It supports only graphical interface design.
In QT for MCU, all the design and assets are converted to cpp and compiled together to get a single elf file.

Run Time environment: Bare-Metal vs RTOS
QT for MCU can be run on embedded firmware both on bare-metal and RTOS modes. Let us understand the differences the same.
Bare-Metal: In a bare-metal environment, applications run directly on the MCU hardware without an operating system layer. Developers have direct control over hardware peripherals and system resources, allowing for maximum efficiency and minimal overhead. It will be a single threaded application with only one job running at any point in time. Bare-metal development requires careful management of application architecture to maintain the application responses to external world.
RTOS: In contrast, Real-Time Operating Systems (RTOS) provide a layer of abstraction on top of the hardware, enabling multitasking and deterministic behaviour. While there is a direct control over peripherals, RTOSes offer task scheduling, synchronization primitives, and communication mechanisms, making it easier to develop complex applications with strict timing requirements. However, RTOSes introduce overhead, and complexity compared to bare-metal development.
Creating a Qt Application for MCU: Bare-Metal Approach
Developing a Qt application for an MCU in a bare-metal environment involves understanding how QT renders the content on screen and how UI changes in run time.

In QT all the UI layouts are defined in QML language, for a project with multiple layouts and screens there will be multiple QML files. So, in QT project, we need to define one of the QML as an entrypoint for QT application by setting it as root item for QT Application. All the QML files will be converted to cpp and header files during compilation. Include necessary assets and resources in the qmlproject or cmake.
Example (Test.qml)
import QtQuick 2.15
import QtQuick.Controls 2.15
import DataModels
Rectangle{
width: 800
height: 480
visible: true
Image {
id: testImage
visible: true
source: TestModel.image1 => QT Object variables
}
}
QML widget properties(source) are updated using QT Object variables.
Example(TestModel.qml)
pragma Singleton
import QtQuick 2.15
QtObject {
id: TestModel
property string image1: '' => QT Object Variable
}
Main.cpp shall be created and define the entrypoint of qml file and timer to update the QT object variable dynamically.
Example(main.cpp)
#include 'test.h' => default entry QML file header file
#include
#include
#include
int main()
{
Qul::initHardware(); => Initialize hardware
Qul::initPlatform(); => Initialize QT
Qul::Platform::getPlatformInstance();
Qul::Application app;
static test item; => Create an instance of default QML file(test.qml)
Qul::Timer timer; => Timer instance to handle application requirement
timer.onTimeout([]() { timer_call(); }); => Register application handler for the timer
timer.start(1000); => Configure timer for 1ms
app.setRootItem(&item); => Configure QT application with root element as our default QML file.
app.exec(); => QT application idle loop to redraw UI on need basis.
return 0;
}
As an example in the below timer routine, we are updating the display image path dynamically.
Example(timer.cpp)void timer_handler ()
{
::TestModel &model = < Model URI >:: TestModel::instance(); => Fetch instance of a singleton qtobject class
/*Process business logic and update data*/
/*Update QT Object interface variables to QML accordingly*/
if (condition)
Model.image1.setValue(“background.png”);
else
Model.image1.setValue(“”);
}
This is a trivial approach which might be suitable for very small applications. But when we are working for a complex use case like an industrial HMI or automotive instrument cluster, there is a lot of business logic involved more than that can be handled over a simple timer handler. In this case the following approach can be used, where we keep the execution in application control and update the Qt screens as and when necessary.
Example(main.cpp)
#include 'test.h'
#include
#include
int main()
{
Qul::initHardware();
Qul::initPlatform();
Qul::Application app;
static test item; => Create an instance of default QML file(test.qml)
app.setRootItem(&item);
while (true) {
uint64_t now = Qul::Platform::getPlatformInstance()->currentTimestamp();
//
uint64_t nextUpdate = app.update();
//Do business logic here
if (event1)
{
Model.image1.setValue(“background.png”);
}
else
{
Model.image1.setValue(“”);
}
}
return 1;
}
Creating a Qt Application for MCU: RTOS Approach
Developing a Qt application for an MCU with an RTOS, the process is similar to the bare-metal approach, with some additional considerations.

RTOS Integration: Choose an RTOS that is suitable for the target and configure task scheduling, memory management, and communication mechanisms provided by the RTOS. Integrate application business case excluding UI as one or more task and configure priority of the tasks as required for the use case.
QT Integration: Create a dedicated task for LCD initialization and asset initialization. Post initialization qt EXEC will be called. QT exec is a QT idle loop routine, which repaints the UI based on dirty flag status. Dirty flag get set based on any widget property changes.
Synchronization and Communication: Utilize RTOS primitives for synchronization and communication between tasks, ensuring thread safety and deterministic behavior. QT widget properties update and repaint need to happen in the same thread.
Use case:Let's take a typical example of a firmware where the business logic runs one task and Qt on another. Here the scheduler contact-switches both periodically and the business logic determines the value to be shown.
Via RTOS mechanisms these changes are to be indicated to the QT task from where the Qt related API's can be called.
Example(main.cpp)
#include “qul_run.h”
Void main(){
Platform_init(); => Initialize hardware
RTOS_init(); => Initialize stack and heap for rtos
/*Create task for QT*/ => Create QT task and call qul_run()
RTOS_Task_Start (qt_task);
/*Create task for business logic and data processing */
RTOS_Task_Start (app_task);
}
#include
#include
#include
extern RTOSEvent Event1;
int qt_task()
{
Qul::initHardware();
Qul::initPlatform();
Qul::Application app;
static test item; => Create an instance of default QML file(test.qml) app.setRootItem(&item);
while (true) {
uint64_t now = Qul::Platform::getPlatformInstance()->currentTimestamp();
//
uint64_t nextUpdate = app.update();
if (nextUpdate > now) {
::TestModel &model = < Model URI >:: TestModel::instance(); => Fetch instance of a singleton qtobject class
/*Process LIN/CAN data and do business logic*/
/*Update QT Object interface variables to QML accordingly*/
if (Rtos_IsEventSet (&Event1))
Model.image1.setValue(“background.png”); => Draw background image
else
Model.image1.setValue(“”); => Turn off background image
}
}
return 0;
}
#include 'app_task.h'
RTOSEvent Event1;
int app_task()
{
/*Process business logic and derive information*/
if (/*condition*/){
Rtos_SetEvent (&Event1);
}
else {
Rtos_ClearEvent (&Event1);
}
}
An essential aspect to consider in the design is ensuring that any modifications to UI elements via models are executed in a sequential order to prevent potential race conditions in the QT processing pipeline. This sequential execution is critical for maintaining the integrity and consistency of the user interface.
This can be achieved by ensuring the UI elements are updated only in the Qt task. By adhering to this, we can mitigate the risk of concurrent access to UI elements, thereby avoiding conflicts that could arise if multiple threads attempt to modify the interface simultaneously.
Conclusion
Qt for MCU provides a powerful framework for developing GUI applications on MCUs, offering a high level of abstraction and cross-platform compatibility. Whether developing in a bare-metal or RTOS environment, Qt enables developers to create rich and responsive user interfaces for embedded systems. By understanding the differences and considerations of each approach, developers can choose the best fit for their project's requirements, balancing efficiency, real-time performance, and development complexity. Whether opting for a bare-metal or RTOS approach, Qt empowers developers to unlock the full potential of MCUs in a wide range of applications.