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++

Что дальше