Qt для встраиваемых систем
Qt работает на встраиваемых Linux-системах (i.MX6, i.CORE, Pi) через Qt for Device Creation и бэкенды eglfs или linuxfb — X11 не требуется. Также поддерживаются MCU без ОС (STM32, NXP) через Qt for MCUs (QUL), хотя QUL использует подмножество API.
Эта статья сфокусирована на Qt для встраиваемых HMI на Linux: промышленная панель, контроллер сенсорного экрана или встраиваемый ПК. Архитектура сигнал-слот и паттерны многопоточности те же, что и в настольном Qt.
Основы сигнал-слот
Механизм сигнал-слот Qt — основной канал коммуникации между объектами. В отличие от callback-ов или указателей на функции, сигналы и слоты развязывают отправителя от получателя — отправитель не знает, кто слушает.
1// sensor_worker.h
2class SensorWorker : public QObject {
3 Q_OBJECT
4public:
5 explicit SensorWorker(QObject* parent = nullptr);
6
7public slots:
8 void startReading(); // вызывается из любого потока через 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("Ошибка чтения сенсора");
20 return;
21 }
22 emit readingReady(temp, humid);
23}
Подключение в 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) вызывается каждый раз при испускании
readingReady. Ни один объект не хранит указатель на другой; соединение — единственная связь.
Отзывчивость UI — фоновые рабочие потоки
Никогда не выполняйте блокирующий I/O, чтение сенсоров или последовательную коммуникацию в главном потоке. Цикл событий Qt должен оставаться незаблокированным для обработки пользовательского ввода и перерисовки.
Паттерн: объект QObject в QThread
1// main.cpp или mainwindow.cpp
2QThread* workerThread = new QThread;
3SensorWorker* worker = new SensorWorker;
4
5worker->moveToThread(workerThread); // worker теперь живёт в workerThread
6
7// Запустить worker при старте потока
8QObject::connect(workerThread, &QThread::started, worker, &SensorWorker::start);
9
10// Очистка
11QObject::connect(workerThread, &QThread::finished, worker, &QObject::deleteLater);
12QObject::connect(workerThread, &QThread::finished, workerThread, &QObject::deleteLater);
13
14workerThread->start();
После moveToThread любой слот, вызванный на worker, выполняется в workerThread,
а не в вызывающем потоке. Соединения сигнал-слот через потоки автоматически используют
Qt::QueuedConnection — вызов помещается в очередь событий потока-получателя вместо
прямого вызова.
1// Это безопасно — readingReady испускается в workerThread,
2// updateDisplay выполняется в главном потоке через очередное соединение
3QObject::connect(worker, &SensorWorker::readingReady,
4 window, &MainWindow::updateDisplay);
Периодическое чтение сенсора с 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 мс
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 живёт в рабочем потоке (создаётся в start(), который выполняется в рабочем потоке).
Его сигнал timeout срабатывает в рабочем потоке, readSensor выполняется в рабочем
потоке — нет межпоточных проблем, нет ручных циклов sleep.
Отправка команд рабочему потоку
Для передачи данных из потока UI в рабочий поток используйте очередное соединение
или QMetaObject::invokeMethod:
1// Нажатие кнопки в UI — установить скорость вентилятора
2connect(fanSlider, &QSlider::valueChanged, this, [this, worker](int value) {
3 // Помещает вызов setFanSpeed(value) в очередь событий workerThread
4 QMetaObject::invokeMethod(worker, "setFanSpeed",
5 Qt::QueuedConnection, Q_ARG(int, value));
6});
Или с лямбда-соединением:
1// Объявить в SensorWorker
2public slots:
3 void setFanSpeed(int percent);
4
5// Подключить — Qt автоматически использует QueuedConnection между потоками
6connect(ui->fanSlider, &QSlider::valueChanged,
7 worker, &SensorWorker::setFanSpeed);
Слайдер срабатывает в главном потоке; setFanSpeed выполняется в рабочем потоке —
безопасно, без ручной синхронизации.
Безопасные разделяемые данные — избегайте их
Соблазн при разделении данных между потоками: std::mutex вокруг разделяемой переменной.
Работает, но создаёт конкуренцию и связывает потоки.
Предпочтительнее копии через сигнал-слот: передавайте данные по значению через сигналы. Qt копирует аргументы в очередной вызов. Для маленьких типов (float, int, структуры) это дешевле блокировки.
1signals:
2 void readingReady(float temp, float humid); // два float — 8 байт
3 void frameReady(QByteArray data); // Qt копирует массив байт
Для больших данных: используйте std::shared_ptr — очередные соединения Qt передают
разделённое владение атомарно:
1signals:
2 void frameReady(std::shared_ptr<DataFrame> frame);
Производитель испускает shared_ptr — потребитель получает его. Никаких проблем
с временем жизни сырого указателя, никаких копий больших буферов.
QML для слоя отображения
Для современных встраиваемых HMI QML разделяет визуальный слой от логики C++:
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}
sensorModel — C++ QObject, открытый для 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};
Открываем для QML в main.cpp:
1SensorModel model;
2SensorWorker worker;
3
4// Сигнал worker → слот model (оба в правильных потоках через очередное соединение)
5QObject::connect(&worker, &SensorWorker::readingReady,
6 &model, &SensorModel::updateReading);
7
8QQmlApplicationEngine engine;
9engine.rootContext()->setContextProperty("sensorModel", &model);
10engine.load(QUrl("qrc:/main.qml"));
QML привязывается к sensorModel.temperature — при срабатывании temperatureChanged
QML переоценивает привязку и обновляет метку Text. Поток UI, модель C++ и рабочий
поток полностью развязаны.
Обработка ввода с сенсорного экрана
Qt обрабатывает ввод с сенсорного экрана через QTouchEvent и его абстракцию через
QQuickItem::MouseArea (QML) или QWidget::mousePressEvent:
1// Кнопка QML с сенсорным управлением
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() — слот Q_INVOKABLE на C++ объекте, открытом для QML —
выполняется в главном потоке и может отправлять в рабочий поток через
QMetaObject::invokeMethod.
Развёртывание на встраиваемом Linux
1# Кросс-компиляция Qt для целевой платформы
2./configure -device linux-rasp-pi4-oe-g++ -device-option CROSS_COMPILE=arm-linux-gnueabihf-
3make -j4
4
5# Развернуть приложение (rsync или scp)
6rsync -avz myapp root@192.168.1.10:/home/user/
7
8# Запустить на целевой платформе с бэкендом eglfs (без X11)
9./myapp -platform eglfs
Для производства добавьте systemd-сервис, запускающий HMI при загрузке и перезапускающий
при сбое. Передайте -platform eglfs или настройте в qt.conf.
Итоги
- Сигнал-слот: развязать код сенсора/устройства от UI — эмиттер ничего не знает о получателях
- Рабочий поток:
moveToThread— чтение сенсоров и последовательный I/O вне главного потока QTimerв рабочем потоке: периодическое чтение без циклов sleep- Очередные соединения: межпоточный сигнал-слот безопасен по умолчанию — Qt помещает в очередь событий
- Передавайте данные по значению или
shared_ptrчерез сигналы — избегайте разделяемого изменяемого состояния - QML +
Q_PROPERTY+NOTIFY: реактивный UI, обновляющийся при изменении данных C++
Что дальше
- Паттерн «команда» — объекты команд как полезная нагрузка сигналов UI→устройство
- Паттерн «активный объект» — обобщённый паттерн рабочего потока
- Событийная архитектура — шина событий для многовиджетных HMI