🪁

Zephyr LLEXT EDKの使いこなし

に公開

はじめに

Zephyr OSで動作するプログラムは通常、一般的なRTOSと同様、ビルド時に静的リンクされます。LLEXT(Linkable Loadable Extensions)は、Zephyrにおけるビルド済みプログラムの動的リンク・ロードをサポートしています。
対応アーキテクチャは、本記事の執筆時点でRISC-V、ARM、ARM64、ARC(experimental)、となっています[1]
本記事では、LLEXT EDK(Extension Development Kit)を使ったビルドについて得た知見を書いています。
なお、本書におけるZephyrのバージョンは4.1.99、westのバージョンは1.3.0です。

LLEXTを使う場面

そもそもLLEXTを使うのはどういう時でしょうか。
例えば、shellにLLEXTのローダーを実装した公式サンプルのように、シリアル通信で受け取ったライブラリ内の関数をコールすることが可能です。
これの何が嬉しいかというと、わざわざカーネルと密結合したビルドをしなくて済む上、フラッシュを書き換えずにプログラムを追加・変更できるわけです。
なお、LLEXTはカーネル拡張も可能です。ユースケースによってはこちらも有用でしょう。

LLEXT EDKを使う場面

エクステンション自体は、メインのプロジェクト内に含めてビルドするということも可能です。
一方で、メインとは別の開発部隊がエクステンションを開発するパターンがあるかと思います。
そのような場合にEDKを使用することで、並行して開発を進めることができます。

LLEXT EDKセットアップ

基本的には公式の手順に従えば良いのですが、サンプルはARM Cortex-R5のQEMUエミュレーションを対象としています。
Zephyrをサポートしているボード向けにEDKとエクステンションをビルドしようと思うと、どうも当該サンプルをそのまま使用できなかったので、その辺りを中心に解説していこうと思います。

EDKのビルド

EDKのビルドは、メインのビルドシステムごとに行う必要があります。これは、アプリケーションで使用するヘッダやコンパイルフラグを同一にするためです。
EDKの公式サンプルはCortex-R5 QEMU向けにプロジェクトが構築されてしまっているので、例えばRaspberry Pi pico向けにEDKビルドを行おうと思ってもエラーを吐かれてしまいます。
したがってここでは、Raspberry Pi pico向けにビルド可能な別のサンプルをもとにして、EDKをビルドしてみましょう。
以降の手順は、公式のスタートガイドと同様にvenvをアクティベートした前提で示します。

まずはshellローダーのサンプルプロジェクトをコピーします。コピー先はご自身の環境に合わせて適宜変更してください。

cp -r samples/subsys/llext/shell_loader ~/

なおshell_loader/prj.confを覗いてみると、CONFIG_LLEXT=yとなっていると思います。このプロジェクト設定により、ビルドターゲットとしてllext-edkが指定可能になります。

メインのプロジェクトをビルドし、EDKを生成します。

west build -b rpi_pico -p=always ~/shell_loader
west build -t llext-edk

build/zephyr/llext-sdk.tar.xzが生成されているため、これを任意のディレクトリに解凍します。

mkdir ~/edk
cp build/zephyr/llext-edk.tar.xz ~/edk/
cd ~/edk
tar -xf llext-edk.tar.xz

環境変数にLLEXT EDKとZephyr SDKのパスを通しておきます。
ちなみにZephyr SDKのインストール方法はこちらになります。

export LLEXT_EDK_INSTALL_DIR=$HOME/edk/llext-edk
export ZEPHYR_SDK_INSTALL_DIR=</path/to/zephyr-sdk>

以上で、エクステンションをビルドする環境が整いました。

エクステンションのプログラムとビルド

EDKのセットアップが完了したら、エクステンションを作成していきます。

エクステンションのプロジェクト作成

任意の場所に、エクステンション用のディレクトリを作成してください。
配置は下記のようになります。各ファイルについては続けて説明します。

hello_llext
├── CMakeLists.txt
├── src
│   └── main.c
└── toolchain.cmake

CMakeLists.txtは、EDKの公式サンプルのext1を参考にしています。
元からほとんど変更していませんが、プロジェクトやファイルの名前を修正し、不要な出力処理(.inc)を削除しました。

CMakeLists.txt
# SPDX-License-Identifier: Apache-2.0

cmake_minimum_required(VERSION 3.20.0)

set(CMAKE_TOOLCHAIN_FILE toolchain.cmake)
set(CMAKE_C_COMPILER_FORCED TRUE)
set(CMAKE_CXX_COMPILER_FORCED TRUE)

project(hello_llext)

# Include EDK CFLAGS
if(NOT DEFINED LLEXT_EDK_INSTALL_DIR)
    set(LLEXT_EDK_INSTALL_DIR $ENV{LLEXT_EDK_INSTALL_DIR})
endif()
include(${LLEXT_EDK_INSTALL_DIR}/cmake.cflags)

# Add LLEXT_CFLAGS to our flags
add_compile_options(${LLEXT_CFLAGS})
add_compile_options("-c")

# Get flags from COMPILE_OPTIONS
get_property(COMPILE_OPTIONS_PROP DIRECTORY PROPERTY COMPILE_OPTIONS)

add_custom_command(
    OUTPUT
        ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.llext
    COMMAND ${CMAKE_C_COMPILER} ${COMPILE_OPTIONS_PROP}
        -o ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.llext
        ${PROJECT_SOURCE_DIR}/src/main.c
)

add_custom_target(hello_llext ALL DEPENDS ${PROJECT_BINARY_DIR}/hello_llext.llext)

toolchain.cmakeはサンプルそのままです。
エクステンションのビルドに使用するコンパイラとそのパスを設定しています。

toolchain.cmake
# SPDX-License-Identifier: Apache-2.0

set(CMAKE_C_COMPILER   arm-zephyr-eabi-gcc)
set(CMAKE_FIND_ROOT_PATH $ENV{ZEPHYR_SDK_INSTALL_DIR}/arm-zephyr-eabi)

プログラムの作成

エクステンションのプログラムを作成します。
シンプルにprintk()しているだけといえばだけですが、エクステンション側からZephyrのAPIを呼べる点がミソかと思います。

main.c
#include <zephyr/kernel.h>

void greeting(void)
{
	printk("hello llext\n");
}

ビルド

エクステンションのプロジェクトフォルダに移動して、ビルドを実施します。

cd hello_llext
cmake -B build
make -C build

プロジェクトフォルダにbuild/hello_llext.llextが生成されます。
なお拡張子が.llextとなっていますが、実態はELFフォーマットです。

うまくビルドができたら、後ほどシリアル通信でプログラムを送りつけるため、次のコマンドで出力される数字列を控えておいてください。
これは、ファイルを16進数でダンプしたものです。

xxd -p build/hello_llext.llext | tr -d '\n'
出力
7f454c460101010000000000000000000...(以下略)

メインのフラッシュ書き込みと実行

いよいよ動作確認をしていきます。
shellローダーのサンプルをまずRaspberry Pi pico上で動作させて、シリアル通信でそのshellとインタラクションします。
llextというコマンドがshellに含まれているので、そのload_hexというサブコマンドにエクステンションのダンプを入力することで、RAM上にエクステンションのプログラムが読み込まれます。

書き込み

EDKのビルド時、メインのプロジェクトもビルドできていると思います。
Raspberry Pi picoの場合、書き込み方法はいくつかありますが、ちょっと試すだけであればuf2を使用するのが最も楽だと思います。
ご自身の開発環境に合わせて書き込みを実施してください。

実行

screenコマンドなどでボードとシリアル通信で接続します。
なおボーレートは115200のはずです。

エンターを送信するとシェルが反応するので、下記のコマンドに倣って、先ほどxxdコマンドで出力された数字列をペーストして送りつけてください。

screen
uart:~$ llext load_hex hello_llext <Paste your llext>
# (略)
Successfully loaded extension hello_llext, addr 0x20007ab4

上記のようにロードが成功したメッセージが出るかと思います。
それではエクステンションに実装したgreeting関数を実行してみましょう。

screen
uart:~$ llext call_fn hello_llext greeting
hello llext

hello llextがprintk()されれば成功です。

おわりに

本記事では、ZephyrのLLEXTとそれを開発するEDKについて解説しました。
MCUBootを使ったファームウェアアップグレードのようなことをせずに、機能を更新したいといった場合に有用だと思います。
実用的にはエクステンションにどのくらいメモリを与えてあげるんだとか、細かいところを気にする必要があると思います。他にもいろいろ気になる点があるので、引き続き調査を進めていこうと思います。
記事の内容について、お気づきの点がありましたらコメントをお願いします。

脚注
  1. よって例えばホストがx86系の場合、native_simでzephyrを動かすと、動的リンクや実行の際に躓きます ↩︎

Discussion