🪁

Qt 6 を Zephyr で動くようにしました

に公開

Qt が Zephyr で動いた。しかも、マイコンの上で

Qt には様々なサンプルアプリが含まれています。

そのデモを、ソースコードを 1 行も変更せずに、Linux も Android も載っていないマイコンの上で動かしました。

https://x.com/task_jp/status/2054252558198382857

https://doc.qt.io/qt-6/gallery.html

https://x.com/task_jp/status/2054387392375853292

https://doc.qt.io/qt-6/qtdoc-demos-coffee-example.html

今回は、Qt 6.11.0 を Zephyr RTOS 上に移植し、NXP MIMXRT1170-EVKB という Cortex-M7 マイコンボード上で、Qt Quick / Qt Widgets / QML / QPainter などを実際に動かしました。

これは「Qt っぽい別フレームワークを MCU で動かした」という話ではありません。

Qt そのものを、Zephyr の上で、マイコンに載せた という話です。

なぜこれが面白いのか

組み込みの世界で「Qt が動く」と言うとき、これまではほとんどの場合、Linux や QNX が対象でした。

Cortex-A クラスの SoC に Embedded Linux を載せ、その上で Qt を動かす。これは長年使われてきた構成で、実績もあります。産業機器、車載、医療機器、計測器、デジタルサイネージなど、Qt は組み込み Linux の UI では非常に強い選択肢です。

一方で、その下のハードウェアも存在します。

Cortex-M のような、MMU を持たず、RAM も数百 KB から数十 MB 程度で、Linux が載らないマイコンの世界です。

ここで Qt は動くのか。動くとしてどの程度ちゃんと動くんだろうというのがここ数年の疑問でした。

The Qt Company には商用製品として Qt for MCUs があります。ただし、Qt for MCUs は内部的には Qt Quick (の劣化版)のレンダラーであり、QtCore / QtGui / QtQml / QtQuick / QtWidgets といった標準 Qt ライブラリそのものではなく、Qt とは別系統の MCU 向け UI フレームワークです。

今回挑戦したのは、QtCore、QtGui、QtWidgets、QtQml、QtQuick を含む標準 Qt 6 を、Zephyr RTOS 上で動かす という試みです。

Qt アプリケーションのソース互換性をできるだけ保ち、Qt SDK に最初から入っている公式サンプルを、そのまま Zephyr 上で動くようにして、実機でも動作確認をしました。

Zephyr という土台

今回のもう一つの主役は Zephyr RTOS です。

Zephyr は Linux Foundation がホストしているオープンソースの組み込み RTOS で、NXP、ST、Nordic、Espressif、Intel、Raspberry Pi など、主要なシリコンベンダーが対応しています。

従来の MCU 開発では、ボードやチップごとにベンダー SDK を選び、その SDK の作法に合わせてドライバ、ビルド、設定、RTOS を組み合わせる必要がありました。

Zephyr はそこに、Linux 的な統一 OS 抽象を持ち込もうとしています。

Device tree、Kconfig、ドライバモデル、Display subsystem、Input subsystem、POSIX サブセット、west によるモジュール管理。こうした仕組みの上に、ベンダーやアーキテクチャをまたいだ開発基盤を作っています。

ここに Qt を載せられるということは、単に「NXP のこのボードで Qt が動いた」という意味に留まりません。

Zephyr が対応している多数のボードに対して、同じ Qt アプリケーションを展開できる可能性が出てきます。

Cortex-M、RISC-V、ARC。NXP、STM32、Nordic、ESP32-S3。ボードごとに UI フレームワークを選び直すのではなく、Zephyr の上に Qt を載せる、という考え方ができます。

実際に動かしたハードウェア

今回確認したハードウェアは NXP MIMXRT1170-EVKB です。

  • Cortex-M7
  • 1 GHz
  • 64 MB SDRAM
  • MMU なし
  • MPU あり
  • 720 × 1280 MIPI-DSI パネル
  • GT911 静電タッチコントローラ

Linux が動く Cortex-A ボードではありません。あくまで Cortex-M7 のマイコンです。

この上で、Qt SDK に付属する公式サンプルを動かしました。

確認済みのサンプルは以下です。

サンプル ソースコード 内容
Coffee Machine qtdoc//examples/demos/coffee QtQuick + QtQuick.Controls.Basic + Effects + Labs.synchronizer
Imagine Automotive qtdeclarative/examples/quickcontrols/imagine/automotive QtQuick.Controls.Imagine、9-patch PNG スタイル
QuickControls Gallery qtdeclarative/examples/quickcontrols/gallery QtQuick.Controls + QtCore + QtQml.Labs.Models
Widgets Gallery qtbase/examples/widgets/gallery QtWidgets 一式。QTabWidget / QListWidget / QPushButton / QSlider など
Colliding Mice qtbase/examples/widgets/graphicsview/collidingmice QtWidgets + QGraphicsScene + JPEG 背景
Vector Deformation qtbase/examples/widgets/painting/deform QtWidgets + QPainter によるベクター変形

Qt Quick だけではありません。Qt Widgets も、QGraphicsView も、QPainter も動いています。

https://x.com/task_jp/status/2054419294919008666

何が変わるのか

一番大きいのは、既存の Qt の知識と資産を MCU に持ち込めることです。

たとえば、これまで組み込み Linux 向けに Qt で UI を作ってきたチームがあるとします。

上位モデルでは Cortex-A + Linux + Qt を使う。しかし下位モデルでは、コスト、起動時間、消費電力、構成の単純さなどの理由で Linux を載せたくない。そこで自然に候補に上がるのが Qt for MCUs です。既存の Qt 資産を活かしたまま MCU に展開できるなら、それが一番きれいです。ところが実際には、Qt for MCUs は標準 Qt とは互換性がなく、QtCore / QtGui / QtQml / QtQuick / QtWidgets をそのまま持ち込めるものではありません。既存の Qt アプリケーションや開発資産を前提にすると、ここで採用を諦めざるを得ないケースがありました。

Zephyr 上で標準 Qt が動くなら、状況が変わります。

能力も機能も異なるハードウェアでOSも違うので、製品であれば100%同じコードが動くということはないでしょうが、それでも既存のソースコードへの設計の変更や、実装の追加などで対応は可能で、多くのソースコードを共通で使用できるでしょう。

また、マルチコア SoC の構成でも面白くなります。

Linux が動く AP コアと、Zephyr が動くリアルタイムコアが同居する製品で、両方に Qt の API を持ち込む。上位 UI は Linux 側で、リアルタイム性の必要な小さな UI や診断画面は Zephyr 側で、という構成も見えてきます。

Zephyr 側には GUI は必要ないけど Qt(Core) を使いたい、というエンジニアもいるかもしれません。

全体構成

ランタイムのレイヤ構造はこうなっています。

┌──────────────────────────────────────────────────────────────────┐
│  ユーザーアプリケーション                                          │
│    Qt 6 標準 API で書かれた QML / C++ コード                        │
│    公式 Examples: Coffee Machine / Gallery / Colliding Mice など   │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│  Qt 6 標準ライブラリ                                                │
│    QtCore / QtGui / QtWidgets / QtQml / QtQuick / QtSvg            │
│    Qt 公式ソースを Cortex-M7 用 static library としてクロスビルド   │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│  qzephyr QPA プラグイン                                             │
│    Qt の OS 抽象層である QPlatformIntegration の Zephyr 実装        │
│    Window / Backingstore / Screen / EventDispatcher /              │
│    NativeInterface / Font database を提供                           │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│  Zephyr RTOS                                                       │
│    Kernel / Display subsystem / Input subsystem /                  │
│    POSIX subset / Device tree / Kconfig                            │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│  ハードウェア                                                       │
│    NXP MIMXRT1170 + RK055HDMIPI4MA0 panel + GT911 touch            │
└──────────────────────────────────────────────────────────────────┘

重要な点は以下のとおりです。

  1. Qt 標準ライブラリ層は upstream そのもの
    QObjectQStringQPainterQQmlApplicationEngine など、通常の Qt API をそのまま使います。MCU 向けに似た API を作ったのではなく、Qt 自体のソースコードの Zephyr 対応をし、Cortex-M7 / Zephyr 向けにクロスビルドして動かしています。

  2. Zephyr 対応の中心は QPA プラグイン
    Qt には QPA、Qt Platform Abstraction という描画のバックエンドの抽象層があります。xcb、Wayland、Linux fb、Windows、macOS などの platform plugin と同じ位置に、Zephyr 向けの platform plugin を追加して、Zephyr の API を呼ぶようにしています。

qt-zephyr-port モジュール

Zephyr には module 機構があります。

module.ymlzephyr/Kconfigzephyr/CMakeLists.txt を持つディレクトリは west から自動的に discover され、外部モジュールとしてプロジェクトに組み込めます。

今回は qt-zephyr-port という Zephyr module を作り、Qt と Zephyr を接続するためのコードをここにまとめました。

zephyr-module/
├── zephyr/
│   ├── module.yml
│   ├── Kconfig
│   └── CMakeLists.txt

├── qt-app/
│   ├── CMakeLists.txt
│   ├── prj.conf
│   └── boards/
│       └── mimxrt1170_evk_mimxrt1176_cm7.conf

├── cmake/
│   └── QtZephyrApp.cmake

└── src/
    ├── qzephyr_display_zephyr.cpp
    ├── qzephyr_touch_zephyr.cpp
    ├── qzephyr_plugin_import.cpp
    ├── qzephyr_font_loader.cpp
    └── embedded_font.c

ビルドは大きく 2 段階です。

git clone --recursive -b zephyr git@github.com:signal-slot/qt6-for-zephyr.git
cd qt6-for-zephyr

# Stage 1: Qt を Cortex-M7 / Zephyr 向けにビルドしてインストール
./build-qt-zephyr-rt1170.sh

# Stage 2: 任意の Qt アプリを Zephyr アプリとして取り込む
export ZEPHYR_BASE=~/zephyrproject/zephyr
export QT_ZEPHYR_PREFIX=$HOME/qt-zephyr-rt1170
export ZEPHYR_EXTRA_MODULES=$PWD/zephyr-module

west build -b mimxrt1170_evk@B/mimxrt1176/cm7 \
    $ZEPHYR_EXTRA_MODULES/qt-app \
    -- -DSHIELD=rk055hdmipi4ma0 \
       -DQT_APP_DIR=/path/to/Qt-6.11.0/demos/coffee

west flash --runner linkserver

Qt をビルドしてから、Zephyr (とアプリ)のビルド時にその Qt のライブラリをリンクするように工夫しています。

技術的なハイライト

ここからは、実際に何をやったのかを紹介します。

1. Qt アプリの CMakeLists.txt を変更せず Zephyr アプリに混ぜる

Qt のアプリは通常、次のような CMake 構成になっています。

cmake_minimum_required(VERSION 3.16)
project(CoffeeMachine LANGUAGES CXX)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Qml Quick)
qt_add_executable(coffeemachine main.cpp)
qt_add_qml_module(coffeemachine URI demos.coffee QML_FILES ...)
target_link_libraries(coffeemachine PRIVATE Qt6::Core Qt6::Gui Qt6::Qml Qt6::Quick)

一方、Zephyr のアプリは find_package(Zephyr)project()target_sources(app PRIVATE ...) という作法です。

この 2 つはそのままでは噛み合いません。

そこで qt_zephyr_app() では、Qt アプリを読み込む前に CMake 関数を差し替えています。

  • qt_add_executable() を override し、実行ファイルではなく static library を作る
  • qt_add_qml_module() を override し、NO_PLUGIN を強制する
  • add_subdirectory(${QT_APP_DIR}) でアプリの CMakeLists.txt をそのまま実行する
  • 作られた static library に Zephyr の zephyr_interface を再帰的に伝播する
  • qt_import_qml_plugins() で必要な QML plugin を自動 link する
  • 最後に Zephyr の app--whole-archive で吸収する

--whole-archive が必要なのは、Zephyr 側に __weak main(void) があるためです。

2. QML JavaScript エンジン V4 を MMU なし Cortex-M7 で動かす

Qt Quick の QML は、内部で V4 という JavaScript エンジンを使います。

V4 は通常、mmap(MAP_ANONYMOUS) を使って匿名メモリ領域を確保します。しかし、Zephyr の POSIX サブセットでは、MMU のない Cortex-M7 上でこの使い方は成立しません。

そこで qtdeclarative/src/3rdparty/masm/wtf/OSAllocatorPosix.cppOS(ZEPHYR) 分岐を追加し、mmap の代わりに以下の方式で確保するようにしました。

  • posix_memalign(&p, 4096, bytes)
  • memset(p, 0, bytes)

また、qv4stacklimits.cpp では Zephyr 向けの StackProperties を返すようにし、V4 のスタック範囲判定が MCU 上でも破綻しないようにしています。

本物のスタックオーバーフローは Zephyr の MPU stack guard に任せます。

この対応で、QML の評価が通るようになりました。

3. 64-bit __atomic_* libcalls を割り込みマスクで実装する

Cortex-M7 は 32-bit の LDREX/STREX は持っていますが、64-bit atomic をハードウェアだけでは処理できません。

そのため、std::atomic<int64_t> などを使うと、GCC は __atomic_load_8__atomic_fetch_add_8 などの libatomic 関数呼び出しを生成します。

ところが Zephyr SDK には、この用途で期待される libatomic が入っていません。未定義シンボルが残ると、最悪の場合 0 番地呼び出しになり、即フォルトします。

そこで qzephyr_libc_stubs.cpp に、__atomic_*_8 ファミリの weak 実装を追加しました。

シングルコア Cortex-M なので、PRIMASK で IRQ をマスクすれば 64-bit 操作中の原子性を確保できます。

static inline unsigned int qzephyr_atomic_lock(void)
{
    unsigned int key;
    __asm__ volatile ("mrs %0, PRIMASK\n\tcpsid i"
                      : "=r" (key) :: "memory");
    return key;
}

実装したのは、__atomic_load_8store_8exchange_8compare_exchange_8fetch_add_8 などの 64-bit atomic libcalls 一式です。

4. QPA プラグインと Stage 2 hook

Zephyr 向け QPA プラグインは、Stage 1 で Qt 側の static library としてビルドされます。

ただし、Stage 1 の時点では Zephyr アプリ本体の build context がありません。そこで、Zephyr の display driver や input subsystem に直接触る部分は Stage 2 側に分離しています。

Stage 1 の QPA プラグインは weak extern hook を呼びます。

Stage 2 では <zephyr/drivers/display.h> などを include し、strong definition を提供します。

weak hook Stage 2 の strong definition
qzephyr_make_event_dispatcher() QEventDispatcherZephyr を生成する
qzephyr_display_query_caps(w,h,fmt) display_get_capabilities() を呼ぶ
qzephyr_display_write(x,y,w,h,...) display_write() を呼ぶ
qzephyr_drain_qpa_events() QWindowSystemInterface::sendWindowSystemEvents() を呼ぶ

この分離により、Qt の Stage 1 build と Zephyr アプリの Stage 2 build をきれいに分けられます。

5. PXP DMA とハードウェアローテーション

RK055HDMIPI4MA0 パネルはポートレート、評価ボード上の見せ方はランドスケープです。

Qt 側は 1280 × 720 のランドスケープとして描画し、NXP の PXP 2D アクセラレータが DMA 転送中に 90 度回転して、720 × 1280 のスキャンアウトバッファに書き込みます。

CONFIG_DMA=y
CONFIG_MCUX_ELCDIF_PXP=y
CONFIG_MCUX_ELCDIF_PXP_ROTATE_90=y

6. 埋め込みフォント

Zephyr 上では通常の system font directory が存在しないため、そのままだと QPainter::drawText() が豆腐になります。

そこで Roboto-Regular.ttf を C 配列に変換し、起動時に QFontDatabase::addApplicationFontFromData() で登録しています。

Q_COREAPP_STARTUP_FUNCTION(qzephyrLoadEmbeddedFont)
static void qzephyrLoadEmbeddedFont()
{
    QByteArray bytes(reinterpret_cast<const char *>(Roboto_Regular_ttf),
                     Roboto_Regular_ttf_len);
    int id = QFontDatabase::addApplicationFontFromData(bytes);
    if (id >= 0) {
        const QStringList families = QFontDatabase::applicationFontFamilies(id);
        if (!families.isEmpty())
            QGuiApplication::setFont(QFont(families.first()));
    }
}

CONFIG_QT_EMBEDDED_ROBOTO_FONT=y で有効化し、不要であれば外せるようにしています。

7. GT911 タッチを QPA イベントに流す

GT911 のタッチ入力は Zephyr の input subsystem 経由で受け取ります。

INPUT_BTN_TOUCHINPUT_ABS_X/Y を組み立て、Qt 側には QWindowSystemInterface::handleMouseEvent<AsynchronousDelivery> で渡します。

ここで重要なのは AsynchronousDelivery です。

同期 delivery にすると、Zephyr の input thread 上で QGuiApplication::notify() の連鎖が走ります。input thread のスタックは小さいため、すぐに MPU stack guard に引っかかります。

AsynchronousDelivery なら、input thread では QPA キューにイベントを積むだけで戻り、実際の処理はメインスレッドの processEvents() が拾います。


どこまで動くのか

実機で確認済みの範囲は以下です。

  • QtCore

    • QObject
    • signals / slots
    • QString
    • QTimer
    • QEventLoop
  • QtGui

    • QPainter
    • QImage
    • QFont
    • QFontDatabase
  • QtWidgets

    • QGraphicsScene
    • QGraphicsView
    • QStyle
    • QPushButton など
  • QtQml / QtQuick

    • V4
    • scene graph software renderer
  • QtQuick.Controls

    • Basic
    • Imagine
    • Fusion
    • Material
    • Universal
  • QtQuick.Effects

  • QtQuick.Layouts

  • QtQuick.Templates

  • QtQuick.Window

  • QtQuick.Shapes

  • QtQml.Models

  • QtQml.Labs.Models

  • QtQuick.Particles

  • Qt.labs.synchronizer

  • QtSvg

  • PNG / JPEG / GIF / ICO / SVG / SVGZ などの image format

一方で、まだ未対応のものもあります。

  • QtSql / QtQuick.LocalStorage
  • QtNetwork
  • QtSerialPort
  • ...

リポジトリ

GitHub で公開しています。

ベースは Qt 6.11.0 です。

各 repository は zephyr branch に、移植用の差分を載せています。

.gitmodules は、修正済み repository については ../<name>-for-zephyr.git を参照し、未変更の Qt module については upstream の code.qt.io を参照する構成です。

そのため、git clone --recursive で必要なものをまとめて取得できます。


おわりに

「MCU で Qt を動かす」という試みは、実はこれが初めてではありません。

過去に Qt を T-Kernel に移植したり、μITRON に移植したり、RTEMS に移植したりということがありました。

クローズドソースな Qt for MCU の登場以降、MCU でオープンソースの Qt を動かすということはビジネス的な理由で非現実的になってしまいましたが、機会があれば人生で一度くらい挑戦したいなと思ってたことに取り組めて、とても楽しかったです。

Qt の公式サンプルが、手元のマイコンボードのタッチパネルでそのまま動いた瞬間の「あ、これいける」という感覚は、かなり強烈です。

ぜひ、みなさんも試してみてください。

Discussion