Skip to content

Cross-Compiling Qt Embedded Applications with QtCreator and CMake

Build, deploy and run Qt application on embedded device by pressing the Run button in QtCreator.

We change the code of our Qt application in QtCreator and press the Run button to try the changes on an embedded device. QtCreator cross-compiles the application, deploys it to the device and runs it on the device. QtCreator performs these steps in a breeze, because we spent quite some time to define a QtCreator Kit. The fairly unknown script configure-qtcreator.sh from the Yocto layer meta-boot2qt automates most of the kit definition.

Starting Point

We have built a Linux image and an SDK for an embedded device, e.g., a Raspberry Pi 3B. In QtCreator, we have defined a Device so that we can log in the Raspberry Pi with SSH using public key authentication. Hence, we need not enter a password every time we deploy the application to the device.

We use QtCreator not older than version 4.11 and CMake not older than version 3.14. Actually, I am not aware of any reasons, why we cannot work with the latest released versions of QtCreator and CMake. In the unlikely event that we must fall back to older versions, my post Deploying Qt Projects to Embedded Devices with CMake explains a workaround.

Nearly Automatic Configuration of QtCreator Kits

When I prepared this post as a talk for Qt Day 2020, I was hard pressed for time. So, I decided to use my Qt Commercial license for small businesses and to install the QBSP (Qt Board Support Package) for the Raspberry Pi 3B. When I started QtCreator after the installation, I was positively surprised by a complete and correct kit definition. I could immediately build, run and deploy my example application (an Internet radio). Using Qt Commercial was a real time saver.

This is in stark contrast to my normal experience with Qt LGPLv3. Even after having set up many kits, I still spend 1-2 hours for defining a kit. In the beginning, it was more like 4-6 hours. The integration of QtCreator, CMake and Yocto has been wanting for quite some time. I was curious, how Qt Commercial made the setup so easy and what we could use for Qt LGPLv3.

The crucial part of the easy setup with Qt Commercial is the script configure-qtcreator.sh. As the script is available under GPLv3, we can use it like QtCreator or GCC. For convenience, we copy the script to a directory contained in $PATH (e.g., $HOME/bin). We first quit QtCreator and then call the script:

$ configure-qtcreator.sh --config <env-setup> \
      --qtcreator <creator-path> --name RPi3-XXX
Configured Qt Creator with new kit: RPi3-XXX

When we build an application against a Qt SDK, we must source a shell script like

environment-setup-cortexa7t2hf-neon-vfpv4-poky-linux-gnueabi

to set environment variables like OECORE_TARGET_SYSROOT, OECORE_NATIVE_SYSROOT, OE_CMAKE_TOOLCHAIN_FILE, CXX and CXXFLAGS for cross-compilation. The value <env-setup> of the option --config is the file path to this environment setup script.

The QtCreator configuration script sources the environment setup script and assigns the environment variables to their CMake counterparts. For example, it assigns OE_CMAKE_TOOLCHAIN_FILE to CMAKE_TOOLCHAIN_FILE and CXX to CMAKE_CXX_COMPILER.

The value <creator-path> of the option --qtcreator points to the directory path, which contains the QtCreator installation. The configuration script needs <creator-path> to find the sdktool utility, which is located in <creator-path>/libexec/qtcreator/sdktool and which is completely undocumented.

sdktool creates an entry for each tab – Kits, Qt Versions, Compilers, Debuggers and CMake – on the dialog page Tools | Options | Kits. Without the QtCreator configuration script, we would have to create these entries manually. For example, sdktool generates the entry for the C++ compiler with this command:

${SDKTOOL} addTC \
    --id "ProjectExplorer.ToolChain.Gcc:${BASEID}.g++" \
    --name "G++ (${NAME})" \
    --path "$(type -p ${CXX})" \
    --abi "${ABI}" \
    --language 2

sdktool writes these entries to files like profiles.xml for the kit and toolchain.xml for the compilers. These files are located in <creator-path>/share/qtcreator/QtProject/qtcreator.

We restart QtCreator now. Qt reads the XML configuration files and displays them in the auto-detected Kit RPi3-XXX on the dialog page Tools | Options | Kits.

Autodetected kit created by QtCreator configuration setup script

The results look promising. Except for the Device type and the Device, the settings look OK. As we cannot change the settings of an auto-detected kit, we clone the kit and give it the name RPi3-Qt5.15.1.

Auto-dected kit adjusted for the correct embedded device

We select Generic Linux Device as the Device type and Pi3B as the Device. If we haven’t defined a device yet, we can still do it now. The section Setup: Device of my post Docker Builds from QtCreator describes how.

Sysroot, C Compiler, C++ Compiler and Debugger have the values OECORE_TARGET_SYSROOT, CC, CXX and GDB, respectively, from the Yocto environment setup script. Qt mkspec is always devices/linux-oe-generic-g++ for Yocto builds. The Qt version is defined by the path to the QMake binary. As we use CMake instead of QMake, the Qt version is None.

The combo box of CMake Tool lists all the CMake executables we have made known to QtCreator. I see three CMake executables, for example:

  • CMake RPi3-XXX (v3.15.3) is provided by the Qt SDK.
  • System CMake (v3.10.2) is the version coming with Ubuntu 18.04.
  • System CMake (v3.17.2) is the latest version I install in /usr/local/bin from time to time.

As pointed out in Starting Point above, CMake 3.10.2 is too old and would complicate deployment unnecessarily. The other two versions 3.15.3 and 3.17.2 are both OK. We choose CMake RPi3-XXX (v3.15.3).

The CMake generator should use Unix Makefiles as the Generator and <none> as the Extra generator. The correct settings look as follows.

CMake Generator dialog

The CMake configuration – the last property on the kit page – is the most difficult part during manual configuration. If QtCreator shows any errors when running CMake, the CMake configuration is often at fault. Getting the CMake configuration right is where we can lose many hours.

Fortunately, the QtCreator configuration script frees us from this tedious job and generates an immaculate CMake configuration.

# <sdk-path> = $OECORE_NATIVE_SYSROOT
# <bin-path> = <sdk-path>/usr/bin/arm-poky-linux-gnueabi

CMAKE_CXX_COMPILER:FILEPATH=<bin-path>/arm-poky-linux-gnueabi-g++
CMAKE_C_COMPILER:FILEPATH= =<bin-path>/arm-poky-linux-gnueabi-gcc
CMAKE_MAKE_PROGRAM:FILEPATH=<sdk-path/usr/bin/make
CMAKE_TOOLCHAIN_FILE:FILEPATH=<sdk-path>/usr/share/cmake/OEToolchainConfig.cmake

QtCreator shows the very long paths <sdk-path> and <bin-path> in their full glory. It cannot use environment variables like OECORE_TARGET_SYSROOT and CXX, because it doesn’t know them. sdktool resolves the environment variables and writes their values verbatim into the QtCreator configuration files.

The first three CMake variables are crucial to pass the compiler detection tests. An Internet search lists dozens of pages “fixing” compiler detection errors. The advice is at best partially right. The QtCreator configuration script shows us the right solution.

CMAKE_CXX_COMPILER and CMAKE_C_COMPILER are assigned the full path of the compiler executables, but do not contain any compiler flags. The compiler flags are stored in CMAKE_CXX_FLAGS and CMAKE_C_FLAGS by the CMake toolchain file. With compiler flags present, the compiler detection tests fail. CMAKE_MAKE_PROGRAM is assigned the full path to make. If not set, the compiler detection tests fail.

CMAKE_TOOLCHAIN_FILE enables QtCreator to cross-compile Qt applications for the embedded device. It maps variables from the Qt SDK build environment to CMake variables. It sets CMAKE_CXX_FLAGS, CMAKE_C_FLAGS, CMAKE_LD_FLAGS, CMAKE_FIND_ROOT_PATH* and some other variables.

The toolchain file includes all the CMake files found in the directory

<sdk-path>/usr/share/cmake/OEToolchainConfig.cmake.d
  • OEQt5Toolchain.cmake: The CMake files of the Qt libraries refer to OE_QMAKE_PATH_EXTERNAL_HOST_BINS, although the SDK environment defines OE_QMAKE_PATH_HOST_BINS. Without assigning the value of the latter to the former variable, the cross-builds of Qt applications fail. This long-standing error is fixed by this toolchain extension.
  • raspberrypi3.cmake: This toolchain extension sets CMAKE_SYSROOT and CMAKE_PREFIX_PATH and adds some Pi3-specific flags to the standard compiler flags (e.g., the machine options -mthumb -mfpu=neon-vfpv4 -mfloat-abi=hard -mcpu=cortex-a7).

If we must set up QtCreator for cross-compilation manually, the CMake configuration and the toolchain files above are an excellent guide. We must get them right before we can move on the build, deployment and run settings.

Build, Deployment and Run Settings

The Build Settings should not need any changes, as they are derived from the CMake configuration and the toolchain files. We must not define an install step in the Build Steps, as the installation is done during deployment.

The Deployment Settings should look as follows.

Deployment Settings

The Deployment Settings define four steps:

  • Install into temporary host directory. QtCreator runs make install through CMake and copies all files specified by install commands in CMakeLists.txt files to the Install root directory. We should tick the box Clean install root first to get rid of stale files from previous runs. The table Files to deploy shows the mapping from local files on the PC to remote files on the device.
  • Check for free disk space. We adapt the Required disk space to a value greater than the total size of all files to deploy.
  • Kill current application instance. This step runs the equivalent to the command killall <executable-name>.
  • Deploy files via rsync. QtCreator uses rsync by default to deploy all the specified files to the embedded device.

We can add deploy steps executing any Linux command (including shell scripts) remotely on the device. For example, we could kill all applications, whoses names contain app, with the command killall -r app || true. If the regular expression app doesn’t match any running executables, || true makes sure that the deploy step succeeds and that deployment continues.

The Run Settings are shown in the next screenshot.

QtCreator Run Settings

We tick the box Use this command instead to specify the correct executable. We could also specify a shell script, which sets some environment variables and starts some applications and services.

Running the Qt Application on the Embedded Device

We have started our application on the Raspberry Pi manually from an SSH shell. The Internet radio application is running and playing the radio station Antenne Bayern.

Manually started Internet radio application

Our product owner decided that the background behind the station name Antenne Bayern should be red instead of yellow. We change the QML code accordingly and press the Run button or hit Ctrl+R in QtCreator.

Deploy Steps: Messages in Compile pane and black screen (killed app)

The Compile Output pane of QtCreator shows the last message of the build step and the messages of the four deploy steps: installing the build files in the temporary install root, checking whether there is enough space on the device, killing the application on the device causing the black screen, and copying all files to the device.

Finally, QtCreator starts the application on the device and shows the messages in the Application Output pane. The radio station name is now shown on a red background according to our last code change.

Run Step: Messages in Application pane and modified app on embedded device

Leave a Reply

Your email address will not be published. Required fields are marked *