Qt for embedded systems
Qt runs on Linux-based embedded systems (i.MX6, i.CORE, Pi) via Qt for Device Creation and the eglfs or linuxfb backends — no X11 required. It also supports bare-metal MCUs (STM32, NXP) via Qt for MCUs (QUL), though QUL uses a subset of the API.
This article focuses on Qt for Linux-based embedded HMIs: an industrial panel, a touchscreen controller, or an embedded PC. The signal-slot architecture and threading patterns are the same as desktop Qt.
Signal-slot fundamentals
Qt’s signal-slot mechanism is the primary communication channel between objects. Unlike callbacks or function pointers, signals and slots decouple sender from receiver — the sender doesn’t know who’s listening.
1// sensor_worker.h
2class SensorWorker : public QObject {
3 Q_OBJECT
4public:
5 explicit SensorWorker(QObject* parent = nullptr);
6
7public slots:
8 void startReading(); // callable from any thread via QMetaObject::invokeMethod
9
10signals:
11 void readingReady(float temperature, float humidity);
12 void error(QString message);
13};
14
15// sensor_worker.cpp
16void SensorWorker::startReading() {
17 float temp, humid;
18 if (!readI2cSensor(&temp, &humid)) {
19 emit error("Sensor read failed");
20 return;
21 }
22 emit readingReady(temp, humid);
23}
Connection in the UI:
1SensorWorker* worker = new SensorWorker;
2MainWindow* window = new MainWindow;
3
4QObject::connect(worker, &SensorWorker::readingReady,
5 window, &MainWindow::updateDisplay);
6
7QObject::connect(worker, &SensorWorker::error,
8 window, &MainWindow::showError);
MainWindow::updateDisplay(float, float) is called whenever readingReady is
emitted. Neither object holds a pointer to the other; the connection is the only
coupling.
Keeping the UI responsive — background workers
Never run blocking I/O, sensor reads, or serial communication on the main thread. The Qt event loop must stay unblocked to process user input and repaint.
Pattern: QObject worker on a QThread
1// main.cpp or mainwindow.cpp
2QThread* workerThread = new QThread;
3SensorWorker* worker = new SensorWorker;
4
5worker->moveToThread(workerThread); // worker now lives on workerThread
6
7// Start the worker when the thread starts
8QObject::connect(workerThread, &QThread::started, worker, &SensorWorker::start);
9
10// Clean up
11QObject::connect(workerThread, &QThread::finished, worker, &QObject::deleteLater);
12QObject::connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater);
13
14workerThread->start();
After moveToThread, any slot invoked on worker runs on workerThread,
not on the calling thread. Signal-slot connections across threads use
Qt::QueuedConnection automatically — the call is posted to the receiver’s
thread event queue rather than called directly.
1// This is safe — readingReady is emitted on workerThread,
2// updateDisplay runs on the main thread via queued connection
3QObject::connect(worker, &SensorWorker::readingReady,
4 window, &MainWindow::updateDisplay);
Periodic sensor reads with QTimer
1class SensorWorker : public QObject {
2 Q_OBJECT
3 QTimer* timer_ = nullptr;
4
5public:
6 void start() {
7 timer_ = new QTimer(this);
8 connect(timer_, &QTimer::timeout, this, &SensorWorker::readSensor);
9 timer_->start(250); // 250 ms interval
10 }
11
12private slots:
13 void readSensor() {
14 float temp = readI2cSensor();
15 emit readingReady(temp);
16 }
17
18signals:
19 void readingReady(float temperature);
20};
QTimer lives on the worker thread (created in start() which runs on the
worker thread). Its timeout signal fires on the worker thread, readSensor
runs on the worker thread — no cross-thread issues, no manual sleep loops.
Sending commands to the worker
To send data from the UI thread to the worker, use a queued connection or
QMetaObject::invokeMethod:
1// UI button press — set fan speed
2connect(fanSlider, &QSlider::valueChanged, this, [this, worker](int value) {
3 // Posts setFanSpeed(value) call to workerThread's event queue
4 QMetaObject::invokeMethod(worker, "setFanSpeed",
5 Qt::QueuedConnection, Q_ARG(int, value));
6});
Or with a lambda signal-slot:
1// Declare in SensorWorker
2public slots:
3 void setFanSpeed(int percent);
4
5// Connect — Qt automatically uses QueuedConnection across threads
6connect(ui->fanSlider, &QSlider::valueChanged,
7 worker, &SensorWorker::setFanSpeed);
The slider fires on the main thread; setFanSpeed executes on the worker
thread — safe, no manual synchronisation.
Safe shared data — avoid it
The temptation when sharing data between threads: std::mutex around a shared
variable. This works but creates contention and couples the threads.
Prefer signal-slot copies: pass data by value through signals. Qt copies the arguments into the queued call. For small types (floats, ints, structs) this is cheaper than locking.
1signals:
2 void readingReady(float temp, float humid); // two floats — 8 bytes
3 void frameReady(QByteArray data); // Qt copies the byte array
For large data: use std::shared_ptr — Qt queued connections transfer
shared ownership atomically:
1signals:
2 void frameReady(std::shared_ptr<DataFrame> frame);
The producer emits a shared_ptr — the consumer receives it. No raw pointer
lifetime issues, no copy of large buffers.
QML for the display layer
For modern embedded HMIs, QML separates the visual layer from C++ logic:
1// main.qml
2import QtQuick 2.15
3
4Rectangle {
5 width: 800; height: 480
6 color: "#1a1a1a"
7
8 Text {
9 id: tempLabel
10 text: sensorModel.temperature.toFixed(1) + "°C"
11 color: "#f0a030"
12 font.pixelSize: 48
13 }
14
15 Text {
16 text: sensorModel.humidity.toFixed(0) + "%"
17 color: "#80c0ff"
18 font.pixelSize: 32
19 }
20}
The sensorModel is a C++ QObject exposed to QML:
1class SensorModel : public QObject {
2 Q_OBJECT
3 Q_PROPERTY(float temperature READ temperature NOTIFY temperatureChanged)
4 Q_PROPERTY(float humidity READ humidity NOTIFY humidityChanged)
5
6public:
7 float temperature() const { return temperature_; }
8 float humidity() const { return humidity_; }
9
10public slots:
11 void updateReading(float temp, float humid) {
12 temperature_ = temp;
13 humidity_ = humid;
14 emit temperatureChanged();
15 emit humidityChanged();
16 }
17
18signals:
19 void temperatureChanged();
20 void humidityChanged();
21
22private:
23 float temperature_ = 0.0f;
24 float humidity_ = 0.0f;
25};
Expose to QML in main.cpp:
1SensorModel model;
2SensorWorker worker;
3
4// Worker signal → model slot (both on correct threads via queued connection)
5QObject::connect(&worker, &SensorWorker::readingReady,
6 &model, &SensorModel::updateReading);
7
8QQmlApplicationEngine engine;
9engine.rootContext()->setContextProperty("sensorModel", &model);
10engine.load(QUrl("qrc:/main.qml"));
QML binds to sensorModel.temperature — when temperatureChanged fires,
QML re-evaluates the binding and updates the Text label. The UI thread,
C++ model, and worker thread are fully decoupled.
Touchscreen input handling
Qt handles touchscreen input via QTouchEvent and its abstraction through
QQuickItem::MouseArea (QML) or QWidget::mousePressEvent:
1// QML touch button
2Rectangle {
3 width: 120; height: 60
4 color: pressed ? "#f0a030" : "#333333"
5 radius: 8
6
7 property bool pressed: false
8
9 MouseArea {
10 anchors.fill: parent
11 onPressed: { parent.pressed = true; backend.onFanToggle(); }
12 onReleased: { parent.pressed = false; }
13 }
14}
backend.onFanToggle() is a Q_INVOKABLE slot on a C++ object exposed to
QML — it runs on the main thread and can post to the worker thread via
QMetaObject::invokeMethod.
Deployment on embedded Linux
1# Cross-compile Qt for your target
2./configure -device linux-rasp-pi4-oe-g++ -device-option CROSS_COMPILE=arm-linux-gnueabihf-
3make -j4
4
5# Deploy your app (rsync or scp)
6rsync -avz myapp root@192.168.1.10:/home/user/
7
8# Run on target with eglfs backend (no X11)
9./myapp -platform eglfs
For production, add a systemd service that starts the HMI on boot and restarts
it on crash. Pass -platform eglfs or configure it in qt.conf.
Summary
- Signal-slot: decouple sensor/device code from UI — emitter knows nothing about receivers
- Worker thread:
moveToThread— sensor reads and serial I/O off the main thread QTimerin worker: periodic reads with no sleep loops- Queued connections: cross-thread signal-slot is safe by default — Qt posts to the event queue
- Pass data by value or
shared_ptrthrough signals — avoid shared mutable state - QML +
Q_PROPERTY+NOTIFY: reactive UI that updates when C++ data changes
What’s next
- Command pattern — command objects as the payload of UI→device signals
- Active object pattern — the worker thread pattern generalised
- Event-driven architecture — event bus for multi-widget HMIs