Building a Linux system for the STM32MP1: developing a Qt5 graphical application

After showing how to build a minimal Linux system for the STM32MP157 platform, how to connect and use an I2C based pressure/temperature/humidity sensor and how to integrate Qt5 in our system, how to set up a development environment to write our own Qt5 application, we are finally going to write our Qt5 application.

List of articles in this series:

  1. Building a Linux system for the STM32MP1: basic system
  2. Building a Linux system for the STM32MP1: connecting an I2C sensor
  3. Building a Linux system for the STM32MP1: enabling Qt5 for graphical applications
  4. Building a Linux system for the STM32MP1: setting up a Qt5 application development environment
  5. Building a Linux system for the STM32MP1: developing a Qt5 graphical application

Disclaimer

Before we get started in this blog post, it is important to mention that it is not meant to be a full introduction to programming applications with Qt5. This would require much more than a blog post, and the Qt web site has extensive documentation.

Also, we want to make it clear that Bootlin’s core expertise is in low-level embedded Linux development, not in Qt application development. Therefore, our example application may not show the best practices in terms of Qt development. We welcome comments and suggestions from our readers to improve the example used in this blog post.

Reading sensor data

As we’ve seen in a previous article, the sensor data is available by reading the following files:

  • /sys/bus/iio/devices/iio:device2/in_temp_input for the temperature
  • /sys/bus/iio/devices/iio:device2/in_pressure_input for the pressure
  • /sys/bus/iio/devices/iio:device2/in_humidityrelative_input for the humidity

So what we will do is writing a new class called DataProvider, which will read those files once per second, and emit a signal with the 3 values every second. Slots and signals is a fundamental mechanism in Qt, which allows to connect emitters of events to receivers for those events. In our case, the DataProvider class will emit a signal when new sensor values are read, while another class in charge of the graphical UI will receive those signals.

At this step, we don’t yet have a graphical UI, so we’ll simply add a few debugging messages in the DataProvider to make sure it works as expected.

Let’s start by adding a data-provider.h file to our project:

#ifndef DATA_PROVIDER_H
#define DATA_PROVIDER_H

#include <QtCore/QTimer>

class DataProvider: public QObject
{
    Q_OBJECT

public:
    DataProvider();

private slots:
    void handleTimer();

signals:
    void valueChanged(float temp, float pressure, float humidity);

private:
    QTimer timer;
};

#endif /* DATA_PROVIDER_H */

It creates a very simple class than inherits from QObject, with:

  • A constructor
  • A private slot handleTimer which will be used internally by the class QTimer’s instance to notify that a timer has expired. This is what will allow us to poll the sensor values every second.
  • A valueChanged signal, which will be emitted by the class every time new sensor values are available.

Then, the implementation of this class in data-provider.cpp is fairly straight-forward:

#include <QtCore/QFile>
#include <QDebug>
#include "data-provider.h"

DataProvider::DataProvider()
{
    QObject::connect(&timer, &QTimer::timeout,
		     this, &DataProvider::handleTimer);
    timer.setInterval(1000);
    timer.start();
}

void DataProvider::handleTimer()
{
    QFile temp_f("/sys/bus/iio/devices/iio:device2/in_temp_input");
    QFile pressure_f("/sys/bus/iio/devices/iio:device2/in_pressure_input");
    QFile humidity_f("/sys/bus/iio/devices/iio:device2/in_humidityrelative_input");

    if (!temp_f.open(QIODevice::ReadOnly | QIODevice::Text))
        return;
    if (!pressure_f.open(QIODevice::ReadOnly | QIODevice::Text))
        return;
    if (!humidity_f.open(QIODevice::ReadOnly | QIODevice::Text))
        return;

    float temp = QString(temp_f.readAll()).toDouble() / 1000;
    float pressure = QString(pressure_f.readAll()).toDouble() * 10;
    float humidity = QString(humidity_f.readAll()).toDouble() / 1000;

    qDebug() << "Temperature: " << temp << "Pressure: " << pressure << "Humidity: " << humidity;

    emit valueChanged(temp, pressure, humidity);
}

The constructor of the class connects the QTimer::timeout signal of the QTimer to this class handlerTimer slot, sets the timer interval to 1000 milliseconds, and starts the timer. This is what will ensure the handleTimer method gets called every second.

In the handleTimer method, we open the 3 files in sysfs, read their value and convert them to meaningful units: the temperature in Celcius, the pressure in hPA, and the humidity in percent. We then print a debugging message and emit the signal with the three values.

With this in place, we need to make sure those two files are properly taken into account by our project, by changing the .pro file as follows:

QT += widgets
SOURCES = main.cpp data-provider.cpp
HEADERS = data-provider.h
INSTALLS += target
target.path = /usr/bin

The data-provider.cpp file was added to SOURCES, while data-provider.h was added to the new HEADERS.

Now, we just need to change main.cpp to instantiate one DataProvider object:

#include <QApplication>
#include <QPushButton&ht;
#include "data-provider.h"

int main(int argc, char* argv[])
{
    QApplication app(argc, argv);
    QPushButton hello("Hello world!");
    DataProvider dp;
    hello.resize(100,30);
    hello.show();
    return app.exec();
}

With this, you can now build and run the application, and you should see every second the debugging message showing the temperature, pressure and humidity values:

# qt-sensor-demo -platform linuxfb
Temperature:  28.12  Pressure:  1003.08  Humidity:  32.235
Temperature:  28.12  Pressure:  1003.07  Humidity:  32.246
Temperature:  28.12  Pressure:  1003.06  Humidity:  32.256
Temperature:  28.12  Pressure:  1003.08  Humidity:  32.267

Displaying sensor data

We now want to display the sensor data. For this, we'll create a UI with two panels, one to display the numeric value of the temperature, humidity and pressure, and another panel with a chart of the temperature. At the bottom of the screen, two buttons Values and Chart will allow to switch between both panels.

So, we'll create a Window class to encapsulate the overall window layout and behavior, and a Values class providing the widget showing the 3 values. We'll leave the chart implementation to the next section. To help you follow the code in this section, here is a diagram that shows the different widgets and how they will be grouped together in our user interface:

Qt Sensor demo UI

Let's start by implementing the Values widget, which will be used to show the 3 numeric values, one below each other. The values.h file will look like this:

#ifndef VALUES_H
#define VALUES_H

#include <QWidget>

class QLabel;

class Values : public QWidget
{
    Q_OBJECT

public:
    Values();

public slots:
    void handleValueChanged(float temp, float pressure, float humidity);

private:
    QLabel *temperature_v;
    QLabel *pressure_v;
    QLabel *humidity_v;
};

#endif /* VALUES_H */

So it has a simple constructor, a slot to be notified of new values available, and 3 text labels to display the 3 values. The implementation in values.cpp is:

// SPDX-License-Identifier: MIT
#include <QtWidgets>
#include "values.h"

Values::Values()
{
    QVBoxLayout *layout = new QVBoxLayout;

    QLabel *temperature_l = new QLabel(tr("Temperature (°C)"));
    QLabel *pressure_l = new QLabel(tr("Pressure (hPa)"));
    QLabel *humidity_l = new QLabel(tr("Humidity (%)"));

    temperature_v = new QLabel();
    pressure_v = new QLabel();
    humidity_v = new QLabel();

    QFont f = temperature_v->font();
    f.setPointSize(28);
    f.setBold(true);
    temperature_v->setFont(f);
    pressure_v->setFont(f);
    humidity_v->setFont(f);
    temperature_v->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
    pressure_v->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
    humidity_v->setAlignment(Qt::AlignRight | Qt::AlignVCenter);

    layout->addWidget(temperature_l);
    layout->addWidget(temperature_v);
    layout->addWidget(pressure_l);
    layout->addWidget(pressure_v);
    layout->addWidget(humidity_l);
    layout->addWidget(humidity_v);

    setLayout(layout);
}

void Values::handleValueChanged(float temp, float pressure, float humidity)
{
    temperature_v->setText(QString::number(temp, 'f', 2));
    pressure_v->setText(QString::number(pressure, 'f', 1));
    humidity_v->setText(QString::number(humidity, 'f', 1));
}

The constructor creates 3 text labels for the legends ("Temperature (°C)", "Pressure (hPA)" and "Humidity (%)"), then instantiates the 3 text labels for the values themselves. It sets up the font and text alignment properties for those labels, and then adds all widgets in a QVBoxLayout so that they all appear vertically below each other.

The handleValueChanged slot simply updates the text labels contents with the new sensor values, doing the proper text formatting on the way.

With the Values class implemented, we can now implement the main Window class. The window.h will contain:

#ifndef WINDOW_H
#define WINDOW_H

#include <QWidget>

class Values;

class Window : public QWidget
{
    Q_OBJECT

public slots:
    void handleValueChanged(float temp, float pressure, float humidity);

public:
    Window();

private:
    Values *values;
};

#endif

Beyond a simple constructor, it has a slot to receive new sensor values, and a reference to a Values widget instance.

The implementation in window.cpp is as follows:

#include <QtWidgets>

#include "window.h"
#include "values.h"

Window::Window()
{
    values = new Values;
    QVBoxLayout *layout = new QVBoxLayout;
    QHBoxLayout *buttons = new QHBoxLayout;

    QPushButton *values_button = new QPushButton("Values");
    QPushButton *chart_button = new QPushButton("Chart");

    buttons->addWidget(values_button);
    buttons->addWidget(chart_button);

    layout->addWidget(values);
    layout->addLayout(buttons);

    setLayout(layout);

    setWindowTitle(tr("Sensors"));
}

void Window::handleValueChanged(float temp, float pressure, float humidity)
{
    values->handleValueChanged(temp, pressure, humidity);
}

The constructor creates a horizontal layout QHBoxLayout with two buttons: Values and Chart. Those will be used in the next section to switch between the Values panel and the Chart panel. For now, they don't do anything.

Then, the constructor adds the Value widget, and the horizontal layout box with the buttons into a vertical box layout, assigns the main window layout and defines the window title.

The handleValueChanged slot implementation just forwards the call to the Values::handleValueChanged method.

Now, obviously main.cpp needs to be changed: instead of creating a button, we'll create our window, and do a bit of additional setup:

#include <QApplication>
#include "window.h"
#include "data-provider.h"

int main(int argc, char* argv[])
{
    QApplication app(argc, argv);
    DataProvider dp;
    Window window;

    QObject::connect(&dp, &DataProvider::valueChanged,
		     &window, &Window::handleValueChanged);

    window.setFixedSize(480, 800);
    window.setStyleSheet("background-color: white;");
    window.show();
    return app.exec();
}

So, not only we create the Window, but more importantly, we connect the valueChanged signal of DataProvider to the handleValueChanged slot of Window. We define the window size (which is fixed, to match the STM32MP15 Discovery board panel) and set the background color of the application.

Obviously, the qt-sensor-demo.pro file needs to be adjusted to build our new files. It now looks like this:

QT += widgets
SOURCES = main.cpp data-provider.cpp window.cpp values.cpp
HEADERS = data-provider.h window.h values.h
INSTALLS += target
target.path = /usr/bin

With this done, we can run the Qt5 application on our target, and see:

Qt5 application showing the I2C sensor data

Graphing the temperature

The final part of developing our application is to implement a graph showing the evolution of temperature over time. For this, we are going to use the very convenient Qt Charts module, which is available in a separate Qt module from the base of Qt.

To implement the graph widget itself, we'll create a new Chart class:

#ifndef CHART_H
#define CHART_H

#include <QtCharts/QChart>

QT_CHARTS_BEGIN_NAMESPACE
class QSplineSeries;
class QValueAxis;
QT_CHARTS_END_NAMESPACE

QT_CHARTS_USE_NAMESPACE

class Chart: public QChart
{
    Q_OBJECT

public:
    Chart(QGraphicsItem *parent = 0, Qt::WindowFlags wFlags = 0);

public slots:
    void handleValueChanged(float temp, float pressure, float humidity);

private:
    QSplineSeries *m_series;
    QStringList m_titles;
    QValueAxis *m_axisX;
    QValueAxis *m_axisY;
    int xpos;
};

#endif /* CHART_H */

This class inherits from the QChart class provided by Qt. It provides a constructor and destructor, a slot that allows to receive notification of new sensor values, and it has a number of private variables to manage the chart itself.

Let's go through the implementation of this class now:

#include "chart.h"
#include <QtCharts/QAbstractAxis>
#include <QtCharts/QSplineSeries>
#include <QtCharts/QValueAxis>

Chart::Chart(QGraphicsItem *parent, Qt::WindowFlags wFlags):
    QChart(QChart::ChartTypeCartesian, parent, wFlags),
    m_series(0),
    m_axisX(new QValueAxis()),
    m_axisY(new QValueAxis()),
    xpos(0)
{
    m_series = new QSplineSeries(this);
    QPen pen(Qt::red);
    pen.setWidth(2);
    m_series->setPen(pen);
    m_series->append(xpos, 30);

    addSeries(m_series);

    addAxis(m_axisX,Qt::AlignBottom);
    addAxis(m_axisY,Qt::AlignLeft);
    m_series->attachAxis(m_axisX);
    m_series->attachAxis(m_axisY);
    m_axisX->setTickCount(5);
    m_axisX->setRange(0, 60);
    m_axisY->setRange(0, 50);

    QFont f = m_axisX->labelsFont();
    f.setPointSize(8);
    m_axisX->setLabelsFont(f);
    m_axisY->setLabelsFont(f);

    setMargins(QMargins(0,0,0,0));

    setTitle("Temperature (°C)");
    legend()->hide();
}

void Chart::handleValueChanged(float temp, float pressure, float humidity)
{
    Q_UNUSED(pressure);
    Q_UNUSED(humidity);
    m_series->append(xpos, temp);
    xpos++;
    if (xpos >= 60)
      scroll(plotArea().width() / 60, 0);
}

The constructor simply sets up the QChart we inherit from: defining the axis, their range, the pen width and color, etc. On the X axis (time), we are going to show 60 measurements, and since our handleValueChanged slot is going to be called every second, it means our graph will show the last 60 seconds of temperature measurement. On the Y axis (temperature), we can show temperatures from 0°C to 50°C. Of course, this is all very hardcoded in this example, for simplicity.

The handleValueChanged slot appends the new temperature value to the graph, and then updates the area displayed by the graph so that always the last 60 seconds are visible.

Now, we need to integrate this to our existing Window class, so that we can display the chart, and switch between the numeric values and the chart. First, we need to do some changes in window.h, and below we'll show only the diff to make the differences very clear:

diff --git a/window.h b/window.h
index 3d63d38..05d1f39 100644
--- a/window.h
+++ b/window.h
@@ -3,8 +3,12 @@
 #define WINDOW_H
 
 #include <QWidget>
+#include <QtCharts/QChartView>
+
+QT_CHARTS_USE_NAMESPACE
 
 class Values;
+class Chart;
 
 class Window : public QWidget
 {
@@ -13,11 +17,17 @@ class Window : public QWidget
 public slots:
     void handleValueChanged(float temp, float pressure, float humidity);
 
+private slots:
+    void chartButtonClicked();
+    void valuesButtonClicked();
+
 public:
     Window();
 
 private:
     Values *values;
+    QChartView *chartView;
+    Chart *chart;
 };
 
 #endif

So, we're defining two private slots that will be used for the two buttons that allow to switch between the numeric values and the chart, and then we add two variables, one for the chart itself, and one for the QChartView (which basically renders the graph into a widget).

Then, in window.cpp, we do the following changes:

diff --git a/window.cpp b/window.cpp
index aba2862..d654964 100644
--- a/window.cpp
+++ b/window.cpp
@@ -3,28 +3,54 @@
 
 #include "window.h"
 #include "values.h"
+#include "chart.h"
 
 Window::Window()
 {
     values = new Values;
+    chart = new Chart;
     QVBoxLayout *layout = new QVBoxLayout;
     QHBoxLayout *buttons = new QHBoxLayout;
 
     QPushButton *values_button = new QPushButton("Values");
     QPushButton *chart_button = new QPushButton("Chart");
 
+    QObject::connect(chart_button, &QPushButton::clicked,
+                    this, &Window::chartButtonClicked);
+    QObject::connect(values_button, &QPushButton::clicked,
+                    this, &Window::valuesButtonClicked);
+
     buttons->addWidget(values_button);
     buttons->addWidget(chart_button);
 
+    chartView = new QChartView(chart);
+    chartView->setRenderHint(QPainter::Antialiasing);
+
     layout->addWidget(values);
+    layout->addWidget(chartView);
     layout->addLayout(buttons);
 
     setLayout(layout);
 
+    chartView->hide();
+
     setWindowTitle(tr("Sensors"));
 }
 
 void Window::handleValueChanged(float temp, float pressure, float humidity)
 {
     values->handleValueChanged(temp, pressure, humidity);
+    chart->handleValueChanged(temp, pressure, humidity);
+}
+
+void Window::chartButtonClicked()
+{
+    values->hide();
+    chartView->show();
+}
+
+void Window::valuesButtonClicked()
+{
+    values->show();
+    chartView->hide();
 }

So, in the constructor we are connecting the clicked signals of the two buttons to their respective slots. We create the Chart object, and then the QChartView to render the graph. We add the latter as an additional widget in the QVBoxLayout, and we hide it.

The existing handleValueChanged slot is modified to also update the Chart object with the new sensor values.

Finally, the new chartButtonClicked and valuesButtonClicked slots implement the logic that is executed when the buttons are pressed. We simply hide or show the appropriate widget to display either the numeric values or the chart. There is probably a nicer way to achieve this in Qt, but this was good enough for our example.

Now that the source code is in place, we of course need to adjust the build logic in qt-sensor-demo.pro:

--- a/qt-sensor-demo.pro
+++ b/qt-sensor-demo.pro
@@ -1,6 +1,6 @@
 # SPDX-License-Identifier: MIT
-QT += widgets
-SOURCES = main.cpp data-provider.cpp window.cpp values.cpp
-HEADERS = data-provider.h window.h values.h
+QT += widgets charts
+SOURCES = main.cpp data-provider.cpp window.cpp values.cpp chart.cpp
+HEADERS = data-provider.h window.h values.h chart.h
 INSTALLS += target
 target.path = /usr/bin

Besides the obvious addition of the chart.cpp and chart.h file, the other important addition is charts to the QT variable. This tells qmake that our application is using the Qt Charts, and that we therefore need to link against the appropriate libraries.

Building the application

At this point, if you try to build the application, it will fail because QtCharts has not been built as part of our Buildroot configuration. In order to address this, run Buildroot's make menuconfig, enable the BR2_PACKAGE_QT5CHARTS option (in Target packages -> Graphic libraries and applications -> Qt5 -> qt5charts).

Then, run the Buildroot build with make, and reflash the resulting SD card image.

Now, you can build again your application, either with Qt Creator if you've been using Qt Creator, or manually. If you build it manually, you'll have to run qmake again to regenerate the Makefile, and then build with make.

When you run the application on the target, the GUI will display the same numeric values as before, but now if you press the Chart button, it will show something like:

Qt5 application with chart

Adjusting the Buildroot package

We have for now been building this application manually, but as explained in our previous blog post, we really want Buildroot to be able to build our complete system, including our application. For this reason, we had created a qt-sensor-demo package, which gets our application source code, configures it with qmake, builds it and installs it.

However, with the new use of Qt Charts, our qt-sensor-demo package needs a few adjustements:

  • The Config.in file needs an additional select BR2_PACKAGE_QT5CHARTS, to make sure Qt Charts are enabled in the Buildroot configuration
  • The qt-sensor-demo.mk file needs an additional qt5charts in the QT_SENSOR_DEMO_DEPENDENCIES variable to make sure the qt5charts package gets built before qt-sensor-demo

With this in place, you can run:

make qt-sensor-demo-rebuild
make

And you have an SD card image that includes our application!

Starting the application automatically at boot time

The next and almost final step for this blog post is to get our application automatically started at boot time. We can simply add a small shell script on the target in /etc/init.d/: the default Buildroot configuration for the init system will execute all scripts named Ssomething in /etc/init.d/. We'll add a file named package/qt-sensor-demo/S99qt-sensor-demo with these contents:

#!/bin/sh

DAEMON="qt-sensor-demo"
DAEMON_ARGS="-platform linuxfb"
PIDFILE="/var/run/qt-sensor-demo.pid"

start() {
	printf 'Starting %s: ' "$DAEMON"
	start-stop-daemon -b -m -S -q -p "$PIDFILE" -x "/usr/bin/$DAEMON" -- $DAEMON_ARGS
	status=$?
	if [ "$status" -eq 0 ]; then
		echo "OK"
	else
		echo "FAIL"
	fi
	return "$status"
}

stop () {
	printf 'Stopping %s: ' "$DAEMON"
	start-stop-daemon -K -q -p "$PIDFILE"
	status=$?
	if [ "$status" -eq 0 ]; then
		rm -f "$PIDFILE"
		echo "OK"
	else
		echo "FAIL"
	fi
	return "$status"
}

restart () {
	stop
	sleep 1
	start
}

case "$1" in
        start|stop|restart)
		"$1";;
	reload)
		# Restart, since there is no true "reload" feature.
		restart;;
        *)
                echo "Usage: $0 {start|stop|restart|reload}"
                exit 1
esac

This is the canonical init script used in Buildroot to start system daemons and services, and is modeled after the one in package/busybox/S01syslogd. It uses the start-stop-daemon program to start our application in the background.

Then, to get this init script installed, we need to adjust package/qt-sensor-demo/qt-sensor-demo.mk with the following additional lines:

define QT_SENSOR_DEMO_INSTALL_INIT_SYSV
        $(INSTALL) -D -m 755 package/qt-sensor-demo/S99qt-sensor-demo \
                $(TARGET_DIR)/etc/init.d/S99qt-sensor-demo
endef

This ensures that the init script gets installed in /etc/init.d/S99qt-sensor-demo as part of the build process of our qt-sensor-demo package. Note that an init script works fine if you're using the Busybox init implementation or the sysvinit init implementation (we're using the default Buildroot setup here, which uses the Busybox init implementation). If you want to use systemd as an init implementation, then a different setup is necessary.

With this done, you simply need to reinstall the application and regenerate the SD card image

$ make qt-sensor-demo-reinstall
$ make

You can now test your SD card image on you board, and you should see the application being started automatically, with the following messages at boot time

Starting dropbear sshd: OK
Starting qt-sensor-demo: OK

Welcome to Buildroot

Avoid unnecessary logging on the display panel

In our current setup, the kernel messages are being sent to both the serial port and the framebuffer console, which means they appear on the display panel. This is not very pretty, and we would like the display to remain black until the application starts, while keeping the kernel messages on the serial port for debugging purposes. Also, we would like the framebuffer console text cursor to not be displayed, to really have a fully black screen. To achieve this we will add two arguments on the Linux kernel command line:

  • console=ttySTM0,115200, which will tell the Linux kernel to only use the serial port as the console, and not all registered consoles, which would include the framebuffer console. This option will make sure the kernel messages are not displayed on the screen.
  • vt.global_cursor_default=0, which will tell the Linux kernel to not display any cursor on the framebuffer console.

So, to add those options, we simply modify board/stmicroelectronics/stm32mp157-dk/overlay/boot/extlinux/extlinux.conf in Buildroot as follows:

label stm32mp15-buildroot
  kernel /boot/zImage
  devicetree /boot/stm32mp157c-dk2.dtb
  append root=/dev/mmcblk0p4 rootwait console=ttySTM0,115200 vt.global_cursor_default=0

Of course, rebuild the SD card image with make, reflash and test the result on your STM32MP1 platform.

Conclusion

In this blog post, we have seen how to write a real (but admittedly very simple) Qt application, how to make it read and display sensor data, and how to integrate this application so that it gets started at boot time.

You can find the Buildroot changes corresponding to this blog post in the 2019.02/stm32mp157-dk-blog-5 branch of our repository. The qt-sensor-demo application code can be found in the blog-5 branch of this application Git repository.

Stay tuned for our next blog post about factory flashing and OTA update!

Building a Linux system for the STM32MP1: setting up a Qt5 application development environment

After showing how to build a minimal Linux system for the STM32MP157 platform, how to connect and use an I2C based pressure/temperature/humidity sensor and how to integrate Qt5 in our system, in this blog post, we are going to see how to set up a development environment to write our own Qt5 application, with QtCreator.

List of articles in this series:

  1. Building a Linux system for the STM32MP1: basic system
  2. Building a Linux system for the STM32MP1: connecting an I2C sensor
  3. Building a Linux system for the STM32MP1: enabling Qt5 for graphical applications
  4. Building a Linux system for the STM32MP1: setting up a Qt5 application development environment
  5. Building a Linux system for the STM32MP1: developing a Qt5 graphical application

A minimal Qt5 application

We’ll call our application qt-sensor-demo, so create a directory with this name, outside of Buildroot. It’s important to not mix up your application code with your build system: you could very well decide to use another build system one day, while keeping your application code. To keep things simple, create this qt-sensor-demo side-by-side with Buildroot, as this will be important for a future step in this blog post.

In this directory, create a main.cpp file with the following code:

#include <QApplication>
#include <QPushButton>

int main(int argc, char* argv[])
{
    QApplication app(argc, argv);
    QPushButton hello("Hello world!");
    hello.resize(100,30);
    hello.show();
    return app.exec();
}

It should be fairly straight-forward to understand that this program creates a QApplication object, a push button with the Hello world! label, sets the button size to 100 by 30 pixels, shows the button, and enters the application event loop. It is obviously a very basic application, because it doesn’t do anything useful, but that’s good enough as a starting point.

Now, we need to build this application. Building Qt applications by hand is definitely not reasonable, as Qt may need to run several tools on the source code before it gets built, and requires a number of compiler and linker flags. So, we won’t write a Makefile by hand, but instead use a build tool that generates the Makefile for us. We have a number of options here:

In this blog post, we’ll simply stick to qmake, which is good enough for a number of Qt-based applications. qmake takes as input one or several .pro files describing the project, and uses that to generate Makefiles (on Linux systems).

In our case, the qt-sensor-demo.pro file will be as simple as:

QT += widgets
SOURCES = main.cpp

Building our application

We have two ways to build our application:

  1. Manually outside of Buildroot. In this case, we’ll use the Buildroot-provided compiler and tools, but we will trigger the build of our application separately from the Buildroot build.
  2. Using Buildroot. In this case, our application would have a corresponding Buildroot package, that would automate building the application as part of the complete system build process.

Ultimately, we definitely want to have a Buildroot package for our application, to make sure the entire build is fully automated. However, during the active development of the application, it may be useful to build it manually outside of Buildroot, so we are going to see both solutions, which are not mutually exclusive: you can have a Buildroot package for your application, and still build it manually when you’re doing active development/debugging.

Building manually outside of Buildroot

To build manually, we simply need to first invoke Buildroot’s provided qmake:

/path/to/buildroot/output/host/bin/qmake

This will generate a Makefile, that we can use to build our application:

make

At this point, you should have:

$ ls
main.cpp  main.o  Makefile  qt-sensor-demo  qt-sensor-demo.pro

The qt-sensor-demo executable is compiled for ARM, and linked against the various libraries built by Buildroot.

Now, we need this executable on our STM32MP15 target. For now, we’ll simply add it to the SD card image:

cp qt-sensor-demo /path/to/buildroot/output/target/usr/bin/
cd /path/to/buildroot/
make

Hello World Qt application running on the STM32MP15 Discovery platform
Hello World Qt application running on the STM32MP15 Discovery platform
This will copy the executable to the output/target folder, which contains the root filesystem produced by Buildroot. Then invoking Buildroot’s make will ensure that the root filesystem and SD card images get re-generated. Of course, beware that if you run a Buildroot make clean, all the contents of output/, including output/target/ get removed. So this technique is only suitable for temporary changes. This is fine since anyway as discussed above, ultimately we’ll have a proper Buildroot package to build our qt-sensor-demo application.

Reflash your SD card with the new image, and on the target, run the demo:

# qt-sensor-demo -platform linuxfb

Setting SSH for communication with the board

Regenerating the SD card image and reflashing the entire SD card every time we want to change our application is not going to be very efficient during the application development/debugging. So instead, we’ll set up networking communication with the board, and use SSH to transfer files. This will also be useful for Qt Creator, as it uses SFTP to deploy files to the target.

Let’s start by enabling a small SSH client/server, called Dropbear. Go in Buildroot menuconfig, and enable the BR2_PACKAGE_DROPBEAR option (in Target packages, Networking applications, dropbear). While Dropbear provides SSH access, it does not support SFTP which will be needed by Qt Creator, so we’ll also enable an SFTP server, gesftpserver. So, we’ll enable BR2_PACKAGE_GESFTPSERVER as well (in Target packages, Networking applications, gesftpserver).

Then, in order to log in through SSH as root, we must have a non-empty root password, so set BR2_TARGET_GENERIC_ROOT_PASSWD (in System configuration, Root password) to a value you like.

You can now exit menuconfig, as we have enabled all features we needed. Before restarting the build, we need to do one last thing: set up a network configuration file so that our STM32MP15 system configures an IP address. To do this, we’ll create a /etc/network/interfaces file, and add it to the root filesystem using the root filesystem overlay mechanism, which was presented in the first post of this series. So, in your Buildroot sources, just create a file board/stmicroelectronics/stm32mp157-dk/overlay/etc/network/interfaces, with the following contents:

auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
      address 192.168.42.2
      netmask 255.255.255.0

This will ensure the eth0 interface of our target gets configured with the 192.168.42.2 IP address. Of course, feel free to use a different IP address.

Then, run make in Buildroot, reflash your SD card, and boot your system. At boot time, you should see:

Starting dropbear sshd: OK

You can also run ip addr show dev eth0 to check the IP address of the eth0 interface:

2: eth0:  mtu 1500 qdisc mq qlen 1000
    link/ether 00:80:e1:42:4d:e3 brd ff:ff:ff:ff:ff:ff
    inet 192.168.42.2/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::280:e1ff:fe42:4de3/64 scope link 
       valid_lft forever preferred_lft forever

So the IPv4 address is properly set to 192.168.42.2, as expected.

Now, on your workstation, we need to configure the 192.168.42.1 static IP address so that you can connect to your board. It is very likely that the Linux system on your workstation is using NetworkManager. Let’s add a connection:

$ nmcli con add con-name buildroot-target type ethernet ifname enp57s0u1u3 ip4 192.168.42.1/24
Connection 'buildroot-target' (234e0d9a-5c4f-4eac-9277-c3587bbd370d) successfully added.

Make sure to replace enp57s0u1u by the name of your PC wired interface, to which the board is connected. We of course assume you have an Ethernet cable directly connecting your PC to the board.

Finally, enable the connection:

$ nmcli con up id buildroot-target
Connection successfully activated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/10)

We can now ping our target:

$ ping 192.168.42.2
PING 192.168.42.2 (192.168.42.2) 56(84) bytes of data.
64 bytes from 192.168.42.2: icmp_seq=1 ttl=64 time=1.33 ms

Log-in over SSH:

$ ssh root@192.168.42.2
root@192.168.42.2's password: 
# uname -a
Linux buildroot 4.19.26 #1 SMP PREEMPT Wed Aug 28 15:54:58 CEST 2019 armv7l GNU/Linux
# cat /etc/issue 
Welcome to Buildroot
# 

And verify that SFTP is working:

$ sftp root@192.168.42.2
root@192.168.42.2's password: 
Connected to root@192.168.42.2.
sftp> ls /
/bin            /boot           /dev            /etc            
/lib            /lib32          /linuxrc        /lost+found     
/media          /mnt            /opt            /proc           
/root           /run            /sbin           /sys            
/tmp            /usr            /var            
sftp> 

So, now we can make a change to our Qt5 application, for example changing the label of the button, recompile by running make in the application directory, and directly copy the application using scp, and run it over ssh:

$ make
[...]
$ scp qt-sensor-demo root@192.168.42.2:/usr/bin/
root@192.168.42.2's password: 
qt-sensor-demo                                    100%   12KB 634.7KB/s   00:00
$ ssh root@192.168.42.2
root@192.168.42.2's password: 
# qt-sensor-demo -platform linuxfb

Much nicer, we don’t have to reflash our SD card every time we want to test a change in our application!

Note that we could create a public/private key pair, with the public key on our target, and this way not have to enter our password every time we want to transfer a file or log-in to the target. Since this blog post is already very long, we’ll live that as an exercise for the reader, there are plenty of resources on the Web about this topic.

Setting up Qt Creator

Some people (such as your author) are happy with using a powerful text editor (such as Vim or Emacs) and a terminal to do their application development. But others are sometimes more comfortable with an integrated development environment (IDE). So in this section, we’ll see how to set up Qt Creator to write, build, deploy and debug a Qt5 application.

Installing Qt Creator

First of all, you’ll have to install Qt Creator, which you can do using the package management system of your distribution. On Fedora systems, this would be:

$ sudo dnf install qt-creator

On Debian/Ubuntu systems:

$ sudo apt install qtcreator

The following instructions have been written and tested against Qt Creator version 4.9.2.

Creating a kit

After starting Qt Creator, the first thing to do is to create a kit, which describes the cross-compiler and Qt installation provided by Buildroot. Go to Tools -> Options, and the first item should be Kits:

Click on Add, and fill in the different fields as follows:

  • Name: Buildroot ARM
  • Device type: Generic Linux Device
  • Sysroot: /path/to/buildroot/output/host/arm-buildroot-linux-gnueabihf/sysroot/. Of course, replace /path/to/buildroot/ with the appropriate path on your system.
  • For the compiler, click on Manage, then in the Compiler panel:
    • Add one GCC C compiler, with the name Buildroot GCC and pointing to /path/to/buildroot/output/host/bin/arm-linux-gnueabihf-gcc
    • Add one GCC C++ compiler, with the name Buildroot G++ and pointing to /path/to/buildroot/output/host/bin/arm-linux-gnueabihf-g++
  • Back in the Kits panel, select Buildroot GCC and Buildroot G++ as the C and C++ compilers, respectively.
  • For the debugger, click on Manage, then in the debugger panel add one debugger named Buildroot GDB, and pointing to /path/to/buildroot/output/host/bin/arm-linux-gnueabihf-gdb. Back in the Kits panel, select Buildroot GDB as our debugger.
  • For the Qt version, click on Manage, then on Add, and point to the qmake binary in /path/to/buildroot/output/host/bin/. It will auto-detect that Buildroot has built Qt 5.11.3. You may want to adjust the version name from Qt %{Qt:Version} (host) to Qt %{Qt:Version} (Buildroot), as this Qt version is clearly not built for our host PC. Then back in the Kits panel, select this new Qt version.
  • For the Qt mkspec, enter devices/linux-buildroot-g++, which is the name of the mkspec configuration Buildroot generates.

You’ll find below screenshots of the various panels, with the details related to the Buildroot cross-compiler, cross-debugger and Qt installation:

Qt Creator Kits panel
Qt Creator Kits panel, filled in with the details of the Buildroot cross-compiler, cross-debugger and Qt installation
Qt Creator compiler panel, C compiler
Qt Creator compiler panel, filled in with the details of the Buildroot C compiler
Qt Creatoer compiler panel, C++ compiler
Qt Creator compiler panel, filled in with the details of the Buildroot C++ compiler
Qt Creator debugger panel
Qt Creator debugger panel, filled in with the details of the Buildroot cross-debugger
Qt Creator Qt version panel
Qt Creator Qt version panel, filled in with the details of the Buildroot Qt installation

We’re now done configuring a Kit!

Creating a device

In order to allow Qt Creator to deploy our application to the device, run it and debug it, we need to create a Device. Go again in Tools -> Options, and this time go to the Devices panel.

In the first window, select Generic Linux Device.

Qt Creator device creation, step 1

Then, for the device name, use STM32MP15 Discovery board for example, for the IP address, 192.168.42.2 and for the user, root, which should give:

Qt Creator device creation, step 2

In the next step about Key deployment, simply skip to the next section, as we have not created a private/public key pair, as explained previously in this blog post. You can then finalize the device creation. Qt Creator will now test that it can communicate as expected with our device:

Qt Creator testing our new device

As you can see, it doesn’t find rsync on the target, because we have not installed it. It will use sftp instead, which is fine.

Back in the Device panel, you should see our device definition as follows:

Qt Creator device panel
Qt Creator device panel, filled in with the details of our STM32MP15 Discovery board

You can click on Open Remote Shell to directly open a shell over SSH to your target, or Show Running processes.

Our device is now set up correctly, time to create our first application!

Importing our project

We now want to import our qt-sensor-demo project in Qt Creator. To do so, go in File -> Open File or Project, then browse to the directory containing our qt-sensor-demo application, and select both the main.cpp and qt-sensor-demo.pro files, and click Open. Qt Creator should now switch to a Configure project window, where it asks you to select the Kit to use for this project. Obviously, select the Buildroot ARM kit we have just created, and validate by clicking Configure Project:

Qt Creator configure project

You should now see our project imported, with both of its files, and main.cpp is opened by default:

Qt Creator project

If we now use Build -> Build All, and then go in the Compile Output panel, we see:

13:11:58: Running steps for project qt-sensor-demo...
13:11:59: Starting: "/home/thomas/projets/outputs/st/host/bin/qmake" /home/thomas/qt-sensor-demo/qt-sensor-demo.pro -spec devices/linux-buildroot-g++ CONFIG+=debug CONFIG+=qml_debug
Info: creating stash file /home/thomas/build-qt-sensor-demo-Buildroot_ARM-Debug/.qmake.stash
13:11:59: The process "/home/thomas/projets/outputs/st/host/bin/qmake" exited normally.
13:11:59: Starting: "/usr/bin/make" -f /home/thomas/build-qt-sensor-demo-Buildroot_ARM-Debug/Makefile qmake_all
make: Nothing to be done for 'qmake_all'.
13:11:59: The process "/usr/bin/make" exited normally.
13:11:59: Starting: "/usr/bin/make" -j4
/home/thomas/projets/outputs/st/host/bin/arm-linux-gnueabihf-g++ -c -pipe -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 -Os -Og --sysroot=/home/thomas/projets/outputs/st/host/arm-buildroot-linux-gnueabihf/sysroot -g -Wall -W -D_REENTRANT -fPIC -DQT_QML_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -I../qt-sensor-demo -I. -I../projets/outputs/st/host/arm-buildroot-linux-gnueabihf/sysroot/usr/include/qt5 -I../projets/outputs/st/host/arm-buildroot-linux-gnueabihf/sysroot/usr/include/qt5/QtWidgets -I../projets/outputs/st/host/arm-buildroot-linux-gnueabihf/sysroot/usr/include/qt5/QtGui -I../projets/outputs/st/host/arm-buildroot-linux-gnueabihf/sysroot/usr/include/qt5/QtCore -I. -I../projets/outputs/st/host/mkspecs/devices/linux-buildroot-g++ -o main.o ../qt-sensor-demo/main.cpp
/home/thomas/projets/outputs/st/host/bin/arm-linux-gnueabihf-g++ --sysroot=/home/thomas/projets/outputs/st/host/arm-buildroot-linux-gnueabihf/sysroot -o qt-sensor-demo main.o   -lQt5Widgets -lQt5Gui -lQt5Core -lrt -ldl -latomic -lpthread 
13:12:00: The process "/usr/bin/make" exited normally.
13:12:00: Elapsed time: 00:02.

So we see that it is invoking qmake from Buildroot, and then running make, which builds our application, with the appropriate cross-compiler provided by Buildroot!

The application has been built in /home/thomas/build-qt-sensor-demo-Buildroot_ARM-Debug, which contains:

-rw-rw-r-- 1 thomas thomas 620760 30 août  13:12 main.o
-rw-rw-r-- 1 thomas thomas  31522 30 août  13:11 Makefile
-rwxrwxr-x 1 thomas thomas 516504 30 août  13:12 qt-sensor-demo

Running the application on the target

In order for Qt to deploy our application on the target, we need to adjust our .pro file so that it has directives to install the application. We’ll simply make our .pro file look like this:

QT += widgets
SOURCES = main.cpp
INSTALLS += target
target.path = /usr/bin

We invite you to read the relevant part of the Qt documentation to get details about the INSTALLS directive and the special target keyword.

Before we can really deploy on your target, we need to adjust the Run configuration, so click on the Project icon in the left bar, which should bring you to:

Qt Creator project build settings

We’re seeing the Build settings, so click on Run to see the Run settings. Everything should already be auto-detected: we want to deploy qt-sensor-demo to /usr/bin on the target, the target is STM32MP15 Discovery board. The only thing we need to change is to set Command line arguments to -platform linuxfb. Your settings should then look like this:

Qt Creator run settings

Now, you can finally do Build -> Run. Qt Creator will prompt you for the root password of your target, and automatically deploy and run the application!

Just to test it, make a change to the QPushButton label, and do Build -> Run again. You’ll see the new version of your application running!

Debugging your application

The last part in setting up our development environment is to be able to debug our application from Qt Creator. This involves remote debugging, where the debugger runs on your workstation, while the program being debugged runs on a separate target. As part of the Kit definition done previously, we have already told Qt Creator where the cross debugger provided by Buildroot is.

Now, we need to have gdbserver on the target, which is the program with which the cross-debugger will communicate to control the execution of our application on the target. To achieve this, go to the Buildroot menuconfig, and enable the option BR2_TOOLCHAIN_EXTERNAL_GDB_SERVER_COPY, in Toolchain -> Copy gdb server to the Target. With this done, we now need to have Buildroot take this change into account. Unfortunately simply running make will not take this change into account (see here for more details). We could do a full clean rebuild of Buildroot (make clean all), but that would take quite some time, so we’ll ask Buildroot to only reinstall the toolchain package and regenerate the root filesystem image:

make toolchain-external-arm-arm-reinstall all

Reflash your SD card, and reboot the system. You should now have gdbserver available on the target:

# ls -l /usr/bin/gdbserver 
-rwxr-xr-x    1 root     root        355924 Aug 29  2019 /usr/bin/gdbserver

We’ll now change a bit our program with some additional dummy code to play around with the debugger:

#include 
#include 

int main(int argc, char* argv[])
{
    int a = 42;
    QApplication app(argc, argv);
    QPushButton hello("Hello world!");
    a++;
    qDebug("Test 1");
    a++;
    qDebug("Test 2");
    hello.resize(100,30);
    hello.show();
    return app.exec();
}

Place a breakpoint on the line QApplication app(argc, argv) by clicking to the left of this line, it should show a red dot, like this:

Qt Creator breakpoint

Then you can start debugging by clicking on the following button in the left bar:

Qt Creator debug button

It will switch to the debug view, with the program stopped at our breakpoint:

Qt Creator debug view

At the bottom of the screen, click on Application Output so that we can see the stdout of the application running on the target. Now hit F10 to step through our code line by line. You should then see the value of the variable a updated in the top right panel, and the Test 1 and then Test 2 messages printed in the application output:

Qt Creator debugging

So, as expected, we are able to debug our application! This concludes the setup of Qt Creator, which allows us to very easily make a change to our application, build it, deploy it on the target and debug it.

Building using a Buildroot package

Before we conclude this article, we want to see how to integrate the build of our application with Buildroot. Indeed, building the application manually or through Qt Creator is perfectly fine during the active development of the application. But in the end, we want Buildroot to be able to build our complete system, including all the applications and libraries we have developed, in a fully automated and reproducible fashion.

To achieve this, in this section, we’ll create a Buildroot package for our qt-sensor-demo application. A package in Buildroot speak is a small set of metadata that tells Buildroot how to retrieve and build a particular piece of software.

To learn how to create a Buildroot package, we suggest you to read the relevant section of the Buildroot manual, or to read the slides of our Buildroot training course. The following steps will however guide you in the process of creating our qt-sensor-demo package.

First, in the Buildroot source tree, create a package/qt-sensor-demo/ directory. Then, create a file named package/qt-sensor-demo/Config.in, which describes one configuration option to be able to enable/disable this package from Buildroot’s menuconfig:

config BR2_PACKAGE_QT_SENSOR_DEMO
        bool "qt-sensor-demo"
        depends on BR2_PACKAGE_QT5
        select BR2_PACKAGE_QT5BASE_WIDGETS
        help
          This is the qt-sensor-demo application.

Note that the bool, depends on, select and help keywords need to be prefixed with a tab (not spaces), and that the BR2_PACKAGE_QT_SENSOR_DEMO string should be exactly as-is, as it needs to match the name of the directory qt-sensor-demo.

This Config.in file basically creates a boolean option which will appear as qt-sensor-demo in menuconfig. The depends on BR2_PACKAGE_QT5 definition ensures that our option will only be selectable if Qt5 is available, while select BR2_PACKAGE_QT5BASE_WIDGETS makes sure Qt5 will be built with QtWidgets support, as we use them.

Now, edit the existing package/Config.in file, and at a relevant place (perhaps Graphic libraries and applications, submenu Graphic applications), you need to add:

source "package/qt-sensor-demo/Config.in"

So that Buildroot’s menuconfig properly includes and reads our new package Config.in file. Now, if you run make menuconfig in Buildroot, you should be able to see our new option and enable it. Of course for now, it doesn’t do anything useful.

The next step is to create a qt-sensor-demo.mk file in package/qt-sensor-demo/ to teach Buildroot how to build our package. This .mk file is a Makefile, which uses a number of Buildroot-specific variables and macros, to a point where it doesn’t really look like a typical Makefile. In our case, qt-sensor-demo.mk will look like this:

################################################################################
#
# qt-sensor-demo
#
################################################################################

QT_SENSOR_DEMO_SITE = $(TOPDIR)/../qt-sensor-demo
QT_SENSOR_DEMO_SITE_METHOD = local

QT_SENSOR_DEMO_DEPENDENCIES = qt5base

define QT_SENSOR_DEMO_CONFIGURE_CMDS
	(cd $(@D); $(QT5_QMAKE))
endef

define QT_SENSOR_DEMO_BUILD_CMDS
	$(TARGET_MAKE_ENV) $(MAKE) -C $(@D)
endef

define QT_SENSOR_DEMO_INSTALL_TARGET_CMDS
	$(INSTALL) -D -m 0755 $(@D)/qt-sensor-demo $(TARGET_DIR)/usr/bin/qt-sensor-demo

endef

$(eval $(generic-package))

The first two variables, QT_SENSOR_DEMO_SITE and QT_SENSOR_DEMO_SITE_METHOD tell Buildroot how to retrieve the source code for this application. Most Buildroot packages retrieve tarballs of source from HTTP servers, or clone source code from Git repositories. But in the case of our package, we are simply taking the source from the qt-sensor-demo directory, located just one level up from the main Buildroot source directory.

The QT_SENSOR_DEMO_DEPENDENCIES variable tells Buildroot that the qt5base package needs to be built before qt-sensor-demo gets built.

The QT_SENSOR_DEMO_CONFIGURE_CMDS variable describes the commands to run to configure our package. Here, we simply call Qt’s qmake utility, using the Buildroot-provided variable QT5_QMAKE.

The QT_SENSOR_DEMO_BUILD_CMDS variable describes the commands to run to build our package. In our case, we invoke make in the application directory, $(@D), passing appropriate variables in the environment ($(TARGET_MAKE_ENV)).

Then, the QT_SENSOR_DEMO_INSTALL_TARGET_CMDS variable describes the commands to run to install our package. We simply copy the qt-sensor-demo executable from the build directory ($(@D)) to usr/bin in the target directory.

Finally, the generic-package macro invocation is what triggers the Buildroot machinery to create a package. Read the Buildroot manual and/or our Buildroot training slides for more details.

With this in place, if you have already enabled qt-sensor-demo in menuconfig, when you run make in Buildroot, you should see:

>>> qt-sensor-demo  Syncing from source dir /home/thomas/qt-sensor-demo
rsync -au --chmod=u=rwX,go=rX --exclude .svn --exclude .git --exclude .hg --exclude .bzr --exclude CVS  /home/thomas/qt-sensor-demo/ /home/thomas/buildroot/output/build/qt-sensor-demo
>>> qt-sensor-demo  Configuring
(cd /home/thomas/buildroot/output/build/qt-sensor-demo; /home/thomas/buildroot/output/host/bin/qmake -spec devices/linux-buildroot-g++)
>>> qt-sensor-demo  Building
PATH="..." /usr/bin/make -j5 -C /home/thomas/buildroot/output/build/qt-sensor-demo
/home/thomas/buildroot/output/host/bin/arm-linux-gnueabihf-g++ --sysroot=/home/thomas/buildroot/output/host/arm-buildroot-linux-gnueabihf/sysroot -Wl,-O1 -o qt-sensor-demo main.o   -L/home/thomas/buildroot/output/host/arm-buildroot-linux-gnueabihf/sysroot/usr/lib -lQt5Widgets -lQt5Gui -lQt5Core -lrt -ldl -latomic -lpthread 
>>> qt-sensor-demo  Installing to target
/usr/bin/install -D -m 0755 /home/thomas/buildroot/output/build/qt-sensor-demo/qt-sensor-demo /home/thomas/buildroot/output/target/usr/bin/qt-sensor-demo

You’re seeing:

  1. Buildroot copying the source code from its original location to the Buildroot build directory
  2. Buildroot configuring the build of our package by invoking qmake
  3. Buildroot building our application
  4. Buildroot installing our application

The application being installed in Buildroot’s target directory, it is automatically part of the root filesystem image, and consequently the SD card image sdcard.img. You can flash it again, and see that you have the same application.

Now, if you want to change the source code of your application, you can simply change it in its original location, the qt-sensor-demo directory, and issue the following command in Buildroot:

$ make qt-sensor-demo-rebuild all

Buildroot will synchronize again the source code from its original directory to Buildroot’s build directory, and rebuild the application. It will only transfer the files that have changed, and only rebuild the files that have changed. The all target ensures that the root filesystem and SD card images get regenerated with the new version of the code.

Conclusion

In this article, we’ve seen many things:

  • How to create and manually build our first Qt5 application
  • How to deploy our application to the target by adding it to the SD card
  • How to set up network communication, and SSH, to deploy our application more efficiently during development
  • How to set up Qt Creator as a development environment to write, build, deploy and debug our application
  • How to create a Buildroot package to automate the build of our application

You can find the Buildroot changes corresponding to this blog post in the 2019.02/stm32mp157-dk-blog-4 branch of our repository. The qt-sensor-demo application code can be found in the blog-4 branch of this application Git repository.

In our next blog post, we’ll extend our qt-sensor-demo application to make it really useful!