ARM TrustZone CryptoCell 310という名称の疑問

はじめに

nRF52840は、Cortex-M4Fが搭載されたSoCです。 また、CryptoCell 310という暗号化アクセラレータが搭載されています。

Nordic Documentation LibraryのCryptoCellのページを見ると、「ARM TrustZone CryptoCell 310」と書かれています。

www.nordicsemi.com

しかし、Cortex-M4自体は、TrustZoneをサポートしていませんし、これはどういう意味なんだろう、と思っていました。 先日、他人にも突っ込まれたので、自分のために整理しておこうと思います。

TrustZoneサポートのあるCortex-Mシリーズ

Armv8-M命令セットのシリーズに、オプションでTrustZoneを実装することができます。 下記ページの下の方に一覧表があります。

developer.arm.com

Cortex-M23, M33, M35PがOptionのFeatureとして、TrustZone for Armv8-Mをサポートしています。

CryptoCell

再度、CryptoCellの話に戻ります。 ARMのWebページでの紹介をみると、次のように書いてあります。

developer.arm.com

CryptoCell complements TrustZone for Armv8-M and together these solutions form a Trusted Execution Environment (TEE).

ということで、TrustZone for Armv8-Mを補完するのが、CryptoCellの役割のようです。 下図が一番最初に掲載されていますが、TrustZoneからアクセスすることもできるし、そうでない場合でも、専用インタフェースを通してしかアクセスできないので、(多少は)安全です、ということなのでしょう。

結局のところ

nRF52840でARM TrustZone CryptoCell 310、という文言は、少しミスリーディングな気がしますね。

Zephyr×Rustのインテグレーションにチャレンジ!⑥~RustでDriverを書く!~

はじめに

ZephyrとRustのインテグレーションに挑戦しています。

これまでで、アプリケーションをRustで書いてきました。 ここからは、Driverを書く方法を調査していきます。

前回までのあらすじ

RustでZephyrのDriverを書くためには、INIT_DEVICEマクロ相当のことをすればよいことがわかりました。 具体的には、.devconfig.init.init_<priority level>に、device_configdeviceオブジェクトを配置します。

bindgenで自動されたbindingは次の通りでした。これをグローバル領域に置いていきます。

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct device_config {
    pub name: *const cty::c_char,
    pub init: ::core::option::Option<unsafe extern "C" fn(device: *mut device) -> cty::c_int>,
    pub config_info: *const cty::c_void,
}

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct device {
    pub config: *mut device_config,
    pub driver_api: *const cty::c_void,
    pub driver_data: *mut cty::c_void,
}

Syncの洗礼

とりあえず、雑に試してみます。

#[link_section = ".init_POST_KERNEL40"]
static __DEVICE_MY_DEVICE: zephyr::device = zephyr::device {
    config: core::ptr::null_mut(),
    driver_api: core::ptr::null_mut(),
    driver_data: core::ptr::null_mut()
};

ビルドしてみます。

error[E0277]: `*mut bindings::device_config` cannot be shared between threads safely
  --> src/lib.rs:31:1
   |
31 | / static __DEVICE_MY_DEVICE: zephyr::device = zephyr::device {
32 | |     config: core::ptr::null_mut(),
33 | |     driver_api: core::ptr::null_mut(),
34 | |     driver_data: core::ptr::null_mut()
35 | | };
   | |__^ `*mut bindings::device_config` cannot be shared between threads safely
   |
   = help: within `bindings::device`, the trait `core::marker::Sync` is not implemented for `*mut bindings::device_config`
   = note: required because it appears within the type `bindings::device`
   = note: shared static variables must have a type that implements `Sync`

あー、生ポインタ型なので、Syncトレイトが実装されていない、と…。 どちらにしても、bindgenから自動生成した構造体を使うのは無理そうですね。

真面目にやるのであれば、生ポインタを包むNewtype型を作って、Syncトレイトを実装する方法があります。

pub struct MY_PTR(*mut bindings::device_config);

unsafe impl Sync for MY_PTR {}

こうなると、自分で構造体を定義する必要があるので、一回動くまでは、楽をします。 device_configおよびdeviceと等価になる構造体を用意します。

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct DeviceConfig {
    pub name: usize,
    // 初期化関数の関数ポインタ
    pub init: unsafe extern "C" fn(device: *mut Device) -> cty::c_int,
    pub config_info: usize,
}

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct Device {
    pub config: &'static DeviceConfig,
    pub driver_api: usize,
    pub driver_data: usize,
}

unsafe impl Sync for DeviceConfig {}
unsafe impl Sync for Device {}

これでバイナリ上の辻褄は合うはずです。

実装

かなり力技で、褒められた実装ではないですが、次のように実装します。

unsafe extern "C" fn my_init(_device: *mut Device) -> cty::c_int {
    println!("Hello from My Driver!\n");
    0
}

#[link_section = ".devconfig.init"]
static __CONFIG_MY_DEVICE: DeviceConfig = DeviceConfig {
    name: 0,
    init: my_init,
    config_info: 0
};

#[link_section = ".init_POST_KERNEL40"]
static __DEVICE_MY_DEVICE: Device = Device {
    config: &__CONFIG_MY_DEVICE,
    driver_api: 0,
    driver_data: 0
};

結果

$ ninja run
Recompacting log...
[1/1] To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: cortex-m3
qemu-system-arm: warning: nic stellaris_enet.0 has no peer
Hello from My Driver!
***** Booting Zephyr OS v1.13.99-ncs2-10-gdad14f4 *****
Hello from Rust!

Rustで書いたDriverの初期化関数から、Hello from My Driver!が出力されました!

Zephyr×Rustのインテグレーションにチャレンジ!⑤~続RustでDriverを書くための調査~

はじめに

ZephyrとRustのインテグレーションに挑戦しています。

これまでで、アプリケーションをRustで書いてきました。 ここからは、Driverを書く方法を調査していきます。

DEVICE_INITマクロ

tomo-wait-for-it-yuki.hatenablog.com

前回記事で、DEVICE_INITマクロさえ呼べれば、アプリケーションプロジェクト内でdriverが書けることがわかりました。 そこで、DEVICE_INITマクロを少し深堀りします。

DEVICE_INITは、DEVICE_AND_API_INITのラッパーです。

|> zephyr/include/device.h

#define DEVICE_INIT(dev_name, drv_name, init_fn, data, cfg_info, level, prio) \
   DEVICE_AND_API_INIT(dev_name, drv_name, init_fn, data, cfg_info,      \
               level, prio, NULL)

DEVICE_AND_API_INITは、CONFIG_DEVICE_POWER_MANAGEMENTが有効か無効か、で定義が異なります。 今回は、無効な場合を見ます。

#define DEVICE_AND_API_INIT(dev_name, drv_name, init_fn, data, cfg_info,  \
               level, prio, api)                 \
   static struct device_config _CONCAT(__config_, dev_name) __used     \
   __attribute__((__section__(".devconfig.init"))) = {        \
       .name = drv_name, .init = (init_fn),              \
       .config_info = (cfg_info)                 \
   };                                \
   static struct device _CONCAT(__device_, dev_name) __used    \
   __attribute__((__section__(".init_" #level STRINGIFY(prio)))) = { \
       .config = &_CONCAT(__config_, dev_name),          \
       .driver_api = api,                    \
       .driver_data = data                   \
   }

パッと見た感じ、特定のセクション (.devconfig.init.init_<priority level>) に、device_configdeviceオブジェクトを配置していることがわかります。 この時のオブジェクト名は、それぞれ、__config_<dev_name>__device_<dev_name>になります。 (わかりにくい…)

バイナリを確認してみましょう。前回、次のようにデバイスを定義しました。

DEVICE_INIT(my_device,
            "MY_DRIVER",
            my_init,
            NULL,
            NULL,
            POST_KERNEL,
            CONFIG_KERNEL_INIT_PRIORITY_DEFAULT );

zephyr.elfを覗いてみると、該当するシンボルがありません。あれ??

予想を外してしまったので、中間生成物のlibapp.aを覗いてみます。

Disassembly of section .devconfig.init:

00000000 <__config_my_device>:
        ...

Disassembly of section .init_POST_KERNEL40:

00000000 <__device_my_device>:
        ...

ありますね。最終リンク時にシンボル情報が消えるのかもしれません。 リンカのマップ情報を見ます。

devconfig       0x000000000000183c       0x6c
                0x000000000000183c                __devconfig_start = .
 *(.devconfig.*)
 .devconfig.init
                0x000000000000183c        0xc libapp.a(main.c.obj)
 .devconfig.init
                0x0000000000001848        0xc zephyr/libzephyr.a(soc.c.obj)

居ますね。サイズも12バイト (name, init, config_info) なので良さそうです。 deviceの方も、ちゃんといます。

 .init_POST_KERNEL40
                0x0000000020000fa0        0xc libapp.a(main.c.obj)
                0x0000000020000fac                __device_APPLICATION_start = .

Rustでやることは?

まとめると、device_config構造体とdevice構造体のオブジェクトを、ユニークな名前で、特定セクション (.devconfig.init.init_<priority level>) に置けば良いわけです。

ZephyrのRust bindingを作ったときに、上記の構造体はbindingが生成されています。

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct device_config {
    pub name: *const cty::c_char,
    pub init: ::core::option::Option<unsafe extern "C" fn(device: *mut device) -> cty::c_int>,
    pub config_info: *const cty::c_void,
}

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct device {
    pub config: *mut device_config,
    pub driver_api: *const cty::c_void,
    pub driver_data: *mut cty::c_void,
}

ということで、恐らく、次のようにRustでdriverを書けるはずです。

#[no_mangle]
pub unsafe extern "C" fn my_init(device: *mut device) -> cty::c_int {
...
}

#[link_section = ".devconfig.init"]
#[no_mangle]
pub static __CONFIG_MY_DEVICE: unsafe extern "C" zephyr::device_config {
    .drv_name = b"my_driver",
    .init = Some(my_init),
    .config_info = core::ptr::null
};

#[link_section = ".init_POST_KERNEL40"]
#[no_mangle]
pub static __DEVICE_MY_DEVICE: unsafe extern "C" zephyr::device {
    .config = core::ptr::null,
    .driver_api = core::ptr::null,
    .driver_data = core::ptr::null
};

次は、実際に試してみましょう。

Zephyr×Rustのインテグレーションにチャレンジ!④~RustでDriverを書くための調査~

はじめに

ZephyrとRustのインテグレーションに挑戦しています。

tomo-wait-for-it-yuki.hatenablog.com

これまでで、アプリケーションをRustで書いてきました。 ここからは、Driverを書く方法を調査していきます。

最低限必要なことは何か?

まずは、ここからです。 Zephyrのビルドプロセスを解析し、device driverのライブラリを作るプロセスがあることが分かっています。

tomo-wait-for-it-yuki.hatenablog.com

理想的には、このプロセスにRustで書いたdriverがビルドできると良いです。 ですが、いきなりこれをやるのはハードルが高そうです。

そこで、まずアプリケーションプロジェクト内device driverが作れるかどうか、試してみます。

external driver project

hello_worldサンプルアプリケーションをコピーして、device driverを作ってみます。

driverを登録するマクロは、DEVICE_INITです。

docs.zephyrproject.org

DEVICE_INIT(dev_name, drv_name, init_fn, data, cfg_info, level, prio)

このマクロ(と引数に使うマクロ)を使うために、init.hdevice.hとをincludeします(init.h内でdevice.hをincludeしているので、device.hは明示的にincludeしなくても大丈夫です)。

#include <init.h>
#include <device.h>

最低限、初期化関数は必要なので、初期化関数内で文字を表示するようにしておきます。

static int my_init(struct device *dev)
{
    printk("Hello from MY_DRIVER.\n");

    return 0;
}

DEVICE_INIT(my_device,
            "MY_DRIVER",
            my_init,
            NULL,
            NULL,
            POST_KERNEL,
            CONFIG_KERNEL_INIT_PRIORITY_DEFAULT );

ビルドしてみます。

$ mkdir build && cd $_
$ cmake -GNinja -DBOARD=qemu_cortex_m3 ..
$ ninja
[3/100] Preparing syscall dependency handling

[95/100] Linking C executable zephyr/zephyr_prebuilt.elf
Memory region         Used Size  Region Size  %age Used
           FLASH:        7680 B       256 KB      2.93%
            SRAM:        4032 B        64 KB      6.15%
        IDT_LIST:           8 B         2 KB      0.39%
[100/100] Linking C executable zephyr/zephyr.elf

いけるやん。実行してみましょう。

$ ninja run
[1/1] To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: cortex-m3
qemu-system-arm: warning: nic stellaris_enet.0 has no peer
Hello from MY_DRIVER.  # !!!!!!!
***** Booting Zephyr OS 1.13.99 *****
Hello World! qemu_cortex_m3

いけるやん。

Discovery翻訳の宣伝

はじめに

Discoveryを翻訳したので、マイコンでRustを動かす体験をお楽しみください。

tomoyuki-nakabayashi.github.io

Discoveryでは、STM32F3DISCOVERY開発ボードを使って、次のようなアプリケーションを実装します。

  • LEDルーレット
  • LEDコンパス
  • シリアル通信
  • パンチングマシーン

F3DISCOVERYは、チップワンストップなどで購入できます。 いつの間にか秋月では品切れ状態ですね…。 秋葉原をうろついても見つからなかったので、最初から通販するのが安定かと思います。

レポジトリは、↓です。誤植や改善案があれば、気軽にフィードバック頂けると嬉しいです。

github.com

他愛ない翻訳裏話

と言っても大した裏話はありません。

コミットログを見てみると、翻訳を開始したのが、2月9日でした。

f:id:tomo-wait-for-it-yuki:20190328203143p:plain
discovery_translation_start

3月25日に一通り翻訳が完了したので、1ヶ月半くらいで翻訳が完了したことになります。 The Embedded Rust Bookと違って、普段から組込みとRustをやっている人間からすると、高度な内容はありません。 割と、粛々とやるだけ!という感じでした。

組込みの入門書として、丁寧に書かれているので、あまり組込み開発の経験がない方でも、読み進められると思います。 Discoveryでマイコンを動かしてみて、より深いことを知りたい場合は、The Embedded Rust BookやEmbedonomiconへ進むと良いです。

予告

これで、ずっと翻訳したかったEmbedonomiconの和訳を始める準備が整いました。

tomoyuki-nakabayashi.github.io

github.com

まだ、翻訳開始したばかりですが、ウォッチして頂けると嬉しいです。

Zephyrのdriverを作ってみよう③~単純なシリアル受信~

はじめに

Zephyrのdriverを作る方法を学びます。 まずは、qemu_cortex_m3をターゲットとした場合の、UART device driverを写経します。

|> zephyr/drivers/serial/uart_stellaris.c

全体で700行くらいなので、全てを単純に写経する、というよりポイントを押さえて行きたいと思います。

今回は、割込みなしのUART (polling) で、データを受信する部分を写経していきます。

実装するAPI

前回、poll_outを実装しました。今回は、poll_inを実装します。

|> ${ZEPHYR_BASE}/include/uart.h

/** @brief Driver API structure. */
struct uart_driver_api {
    /** Console I/O function */
    int (*poll_in)(struct device *dev, unsigned char *p_char);
    void (*poll_out)(struct device *dev, unsigned char out_char);
...
}

シリアル受信

uart_driver_api構造体のpoll_in APIを実装します。

static const struct uart_driver_api uart_stellaris_driver_api = {
    .poll_in = uart_stellaris_poll_in,  // これから実装
    .poll_out = uart_stellaris_poll_out,  // 実装済み
...
}

UARTのpoll_in APIでは、UARTデバイスの受信バッファにデータが入っているかどうかを確認します。 データがない場合は-1を返し、データがある場合はパラメータのcに読み込んだ文字を詰めて0を返します。

static int uart_my_uart_poll_in(struct device *dev, unsigned char *c)
{
    volatile struct _uart *uart = UART_STRUCT(dev);

    if (uart->fr & UARTFR_RXFE)
        return (-1);

    /* got a character */
    *c = (unsigned char)uart->dr;

    return 0;
}

動作確認

受信処理を実装する前に、Zephyrのshellを有効にすると、poll_in API呼び出しで例外が発生してしまいます。

$ ninja run
[1/1] To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: cortex-m3
qemu-system-arm: warning: nic stellaris_enet.0 has no peer
***** Booting Zephyr OS 1.13.99 *****
Hello World! qemu_cortex_m3


uart:~$ 
***** USAGE FAULT *****
  Illegal use of the EPSR
***** Hardware exception *****
Current thread ID = 0x20000400
Faulting instruction address = 0x0
Fatal fault in essential thread! Spinning...
QEMU 3.0.50 monitor - type 'help' for more information
(qemu) QEMU: Terminated

受信処理を実装した後、同じ設定でZephyrを起動すると、無事shellが動きます。

$ ninja run
[1/1] To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: cortex-m3
qemu-system-arm: warning: nic stellaris_enet.0 has no peer
***** Booting Zephyr OS 1.13.99 *****
Hello World! qemu_cortex_m3

uart:~$ help
Please press the <Tab> button to see all available commands.
You can also use the <Tab> button to prompt or auto-complete all commands or its subcommands.
You can try to call commands with <-h> or <--help> parameter for more information.

おまけ

UART poll_in APIの実装と、shell subsystemから利用する方法を見てみましょう。 poll_inはsystem callとして提供され、shell subsystemのどこかからタイマ割り込みで、処理が開始されているはずです。

system call

まずは、UARTのAPIを深堀りしていきます。

|> zephyr/include/uart.h

/**
 * @brief Poll the device for input.
 *
 * @param dev UART device structure.
 * @param p_char Pointer to character.
 *
 * @retval 0 If a character arrived.
 * @retval -1 If no character was available to read (i.e., the UART
 *            input buffer was empty).
 * @retval -ENOTSUP If the operation is not supported.
 */
__syscall int uart_poll_in(struct device *dev, unsigned char *p_char);

static inline int _impl_uart_poll_in(struct device *dev, unsigned char *p_char)
{
    const struct uart_driver_api *api =
        (const struct uart_driver_api *)dev->driver_api;

    return api->poll_in(dev, p_char);
}

uart_poll_in()システムコールとして宣言されています。 ここでは宣言だけですが、uart_poll_in()の定義で、_impl_uart_poll_in()が呼び出されると推測できます。

_impl_uart_poll_in()では、driverのpoll_in() (さきほど実装したAPI) が呼ばれています。

uart_poll_in()の定義は、下記です (多分) 。

|> zephyr/drivers/serial/uart_handlers.c

Z_SYSCALL_HANDLER(uart_poll_in, dev, p_char)
{
        Z_OOPS(Z_SYSCALL_DRIVER_UART(dev, poll_in));
        Z_OOPS(Z_SYSCALL_MEMORY_WRITE(p_char, sizeof(unsigned char)));
        return _impl_uart_poll_in((struct device *)dev,
                                  (unsigned char *)p_char);
}

Z_SYSCALL_HANDLERは、可変長引数のハンドラ登録マクロです。引数に応じたハンドラ登録マクロが呼ばれる仕組みになっています。

|> zephyr/kernel/include/syscall_handler.h

#define Z_SYSCALL_HANDLER(...) \
        _SYSCALL_CONCAT(__SYSCALL_HANDLER, \
                        _SYSCALL_NARG(__VA_ARGS__))(__VA_ARGS__)

subsys/shell

次にsubsystemのshellを見ていきます。 今回は割込み駆動の設定を行っていないため、定期的にpoll_inを呼び出すような実装になっているはずです。

|> zephyr/subsys/shell/shell_uart.c

早速、それっぽいタイマハンドラが見つかりました。

static void timer_handler(struct k_timer *timer)
{
    u8_t c;
    const struct shell_uart *sh_uart = k_timer_user_data_get(timer);

    while (uart_poll_in(sh_uart->ctrl_blk->dev, &c) == 0) {  // ★
        if (ring_buf_put(sh_uart->rx_ringbuf, &c, 1) == 0) {
            /* ring buffer full. */
            LOG_WRN("RX ring buffer full.");
        }
        sh_uart->ctrl_blk->handler(SHELL_TRANSPORT_EVT_RX_RDY,
                       sh_uart->ctrl_blk->context);
    }
}

上記ソースコードの★の部分で、uart_poll_in()システムコールを行っています。 uart_poll_in()では、受信バッファにデータがあった場合に、0を返す実装になっていました。 UARTに受信データがあった場合、shellのリングバッファに受信したデータを詰めています。

タイマの初期化は、同ファイル内のinit()で行われています。

static int init(const struct shell_transport *transport,
        const void *config,
        shell_transport_handler_t evt_handler,
        void *context)
{
    const struct shell_uart *sh_uart = (struct shell_uart *)transport->ctx;
...
    if (IS_ENABLED(CONFIG_SHELL_BACKEND_SERIAL_INTERRUPT_DRIVEN)) {
        uart_irq_init(sh_uart);
    } else {
        // 今回はこちら
        k_timer_init(sh_uart->timer, timer_handler, NULL);
        k_timer_user_data_set(sh_uart->timer, (void *)sh_uart);
        k_timer_start(sh_uart->timer, RX_POLL_PERIOD, RX_POLL_PERIOD);
    }

    return 0;
}

参考

docs.zephyrproject.org

ZephyrのIRQ_CONNECTメモ

はじめに

Zephyrのdriverを書いています。IRQハンドラ登録の処理を調査したので、メモを残しておきます。

IRQ_CONNECT

Zephyrで割り込みハンドラを登録する場合、IRQ_CONNECTというマクロを使用します。 まず、Zephyrのドキュメントに掲載されている利用例です。

#define MY_DEV_IRQ  24       /* device uses IRQ 24 */
#define MY_DEV_PRIO  2       /* device uses interrupt priority 2 */
/* argument passed to my_isr(), in this case a pointer to the device */
#define MY_ISR_ARG  DEVICE_GET(my_device)
#define MY_IRQ_FLAGS 0       /* IRQ flags. Unused on non-x86 */

void my_isr(void *arg)
{
   ... /* ISR code */
}

void my_isr_installer(void)
{
   ...
   IRQ_CONNECT(MY_DEV_IRQ, MY_DEV_PRIO, my_isr, MY_ISR_ARG, MY_IRQ_FLAGS);
   irq_enable(MY_DEV_IRQ);
   ...
}

素直な作りで、IRQ番号、プライオリティ、IRQハンドラといった情報を指定します。 このマクロは次のように定義されています。

|> zephyr/include/irq.h

#define IRQ_CONNECT(irq_p, priority_p, isr_p, isr_param_p, flags_p) \
        _ARCH_IRQ_CONNECT(irq_p, priority_p, isr_p, isr_param_p, flags_p)

ターゲットアーキテクチャごとに実装が変わります。ここでは、Cortex-Mの実装を見ていきます。

#define _ARCH_IRQ_CONNECT(irq_p, priority_p, isr_p, isr_param_p, flags_p) \
({ \
        _ISR_DECLARE(irq_p, 0, isr_p, isr_param_p); \
        _irq_priority_set(irq_p, priority_p, flags_p); \
        irq_p; \
})

コメントで次のように書かれている通り、Cortex-Mでは、静的に割り込みが設定できます。

/**
 * Configure a static interrupt.
 *
 * All arguments must be computable by the compiler at build time.
 *
...
 */

Zephyrで面白いのは、ここからです。

 * _ISR_DECLARE will populate the .intList section with the interrupt's
 * parameters, which will then be used by gen_irq_tables.py to create
 * the vector table and the software ISR table. This is all done at
 * build-time.

_ISR_DECLAREマクロは、.intListセクションに割り込みパラメータを置きます。これを、pythonスクリプトで解析して、ISRテーブルを作ります。

_ISR_DECLAREマクロは、次のように定義されています。

|> zephyr/include/sw_isr_table.h

/* Create an instance of struct _isr_list which gets put in the .intList
 * section. This gets consumed by gen_isr_tables.py which creates the vector
 * and/or SW ISR tables.
 */
#define _ISR_DECLARE(irq, flags, func, param) \
        static struct _isr_list _GENERIC_SECTION(.intList) __used \
                _MK_ISR_NAME(func, __COUNTER__) = \
                        {irq, flags, &func, (void *)param}

さて、別言語から、静的にIRQを登録したいのですが、どうしましょうかね…。

参考

docs.zephyrproject.org