Zephyr×Rustのインテグレーションにチャレンジ!⑫~Driver APIリファクタリングの旅1~

はじめに

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

Rustで最低限動作するZephyrのUART Driverを作りました。 力技で無理矢理実装したところも多いです。負債を返済するときが来ました。

バイス登録

まずは、デバイスおよびデバイスドライバをZephyr kernelに認識させるために、次のように実装していました。

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

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

特定のセクションに、Device / DeviceConfig構造体のオブジェクトを配置しています。

とりあえずで解決したい問題は2つです。

  • 使い方が面倒
  • 型がUART Driver固定

使い方が面倒

Device / DeviceConfig構造体を知っていなければならないのと、それ以上にリンカセクションを意識しなければならないのがダメダメです。

マクロで簡素化

雑にマクロを書いて、もう少し簡単に書けるようにします。 次のように、必要な情報をマクロの引数に渡すと、上述したものと同等になるようにします。

device_config!(
    __CONFIG_MY_DEVICE,
    zephyr::CONFIG_UART_CONSOLE_ON_DEV_NAME,
    my_init,
    0
);
device_init!(__DEVICE_MY_DEVICE, &__CONFIG_MY_DEVICE, &UART_API, 0);

少しAPIを簡単にするだけなので、マクロもそれほど複雑になりません。 与えられた引数から、Device / DeviceConfig構造体の静的なオブジェクトを作成し、特定のリンカセクションを指定します。

#[macro_export]
macro_rules! device_config {
    ($dev_name:ident, $name:expr, $init:expr, $info:expr) => {
        #[link_section = ".devconfig.init"]
        static $dev_name: DeviceConfig = DeviceConfig {
            name: $name,
            init: $init,
            config_info: $info
        };
    }
}

#[macro_export]
macro_rules! device_init {
    ($dev_name:ident, $config:expr, $api:expr, $data:expr) => {
        #[link_section = ".init_POST_KERNEL40"]
        static $dev_name: Device = Device {
            config: $config,
            driver_api: $api,
            driver_data: $data
        };
    }
}

現状、device_init!の方は、.init_POST_KERNEL40と固定になっています。 おそらく、これ以上は手続きマクロが必要なので、これから勉強します。

いずれにせよ、これで少し使うのが楽になりました。

型がUART Driver固定

Device構造体のdriver_apiの型がuart_driver_api固定になっています。これではUARTでしか使えません。

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

ジェネリクスで汎化!

driver_apiの型をジェネリックにします。

pub struct Device<T: 'static + DriverApi> {
    pub config: &'static DeviceConfig<T>,
    pub driver_api: &'static T,
    pub driver_data: usize,
}

単純にジェネリックにするだけでは、どんな型も指定できてしまうため、DriverApi traitの実装を制約にします。

DriverApi traitは、ただのマーカーです。何か必要になったら追加しましょう。

pub trait DriverApi {}
impl DriverApi for bindings::uart_driver_api {}

これで、少しはマシになりました。 が、まだまだ負債はたくさんあるので、少しずつ解消していきます。

Zephyr×Rustのインテグレーションにチャレンジ!⑪~UART Driver 受信編~

はじめに

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

RustでデバイスDriverを作っていきます。 まずは、QEMUのUARTをターゲットにします。 QEMUのUARTデバイスガバガバで、初期化を行わなくてもデータ送受信ができるので、敷居が低いです。

UART受信

基本は、以前の記事でUART Driverの受信処理を作ったときと同じです。

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

動作確認も、ZephyrのShellが動くかどうか、を見ていきましょう。

実装

まず、APIpoll_inを埋めます。

static UART_API: uart_driver_api = uart_driver_api {
    poll_out: Some(rust_poll_out),
-    poll_in: None,
+    poll_in: Some(rust_poll_in),
    err_check: None,
    configure: None,
    config_get: None,
};

poll_inを実装します。あとでどちゃくそリファクタリングしましょう。 (と言ってもCからのコールバックなので、それほどきれいにできないかもしれませんが)

// Flag register
const FR: u32 = 0x18;
const UARTFR_RXFE: u32 = 0x00000010;

unsafe extern "C" fn rust_poll_in(_dev: *mut device, p_char: *mut cty::c_uchar)
        -> cty::c_int
{
    let flags = *((zephyr::UART_0_BASE_ADDRESS + FR) as  *const u32);
    if (flags & UARTFR_RXFE) != 0 {
        return -1;  // don't have RX data.
    }

    *p_char = *(zephyr::UART_0_BASE_ADDRESS as *mut u32) as cty::c_uchar;
    return 0;
}

Rustの実装はこれだけです!

動作確認

ZephyrのShellをinterrupt drivenを無効化した上で、有効化します。

$ ninja run
 ninja run
[0/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 from Rust!

uart:~$ 
  clear    help     history  resize   shell
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.

Rustで書いたZephyrのDriverが動いています!感動!

Zephyr×Rustのインテグレーションにチャレンジ!⑩~UART Driver 送信編~

はじめに

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

RustでデバイスDriverを作っていきます。 まずは、QEMUのUARTをターゲットにします。 QEMUのUARTデバイスガバガバで、初期化を行わなくてもデータ送受信ができるので、敷居が低いです。

やることのおさらい

前回、Rustで次のようにDriver登録に必要な情報を作成しました。

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

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

やることは2つです。

  • poll_outを実装して、driver_apiに登録する
  • nameUART_0を登録する

bindingから使えるものを探す

UART_0は、下の参照を引っ張ってくれば良さそうです。

pub const CONFIG_UART_CONSOLE_ON_DEV_NAME: &'static [u8; 7usize] = b"UART_0\0";

UART0のベースアドレスは、下記の定義を使いましょう。

pub const UART_0_BASE_ADDRESS: u32 = 1073790976;

uart_driver_apiのbindingは次の通り生成されています。

#[doc = " @brief Driver API structure."]
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct uart_driver_api {
    #[doc = " Console I/O function"]
    pub poll_in: ::core::option::Option<
        unsafe extern "C" fn(dev: *mut device, p_char: *mut cty::c_uchar) -> cty::c_int,
    >,
    pub poll_out:
        ::core::option::Option<unsafe extern "C" fn(dev: *mut device, out_char: cty::c_uchar)>,
    #[doc = " Console I/O function"]
    pub err_check: ::core::option::Option<unsafe extern "C" fn(dev: *mut device) -> cty::c_int>,
    #[doc = " UART configuration functions"]
    pub configure: ::core::option::Option<
        unsafe extern "C" fn(dev: *mut device, cfg: *const uart_config) -> cty::c_int,
    >,
    pub config_get: ::core::option::Option<
        unsafe extern "C" fn(dev: *mut device, cfg: *mut uart_config) -> cty::c_int,
    >,
}

いらないAPINoneにしておけば良さそうです。 パーツは揃っていそうなので、早速やっていきましょう。

uart_driver_api

use bindings::zephyr::{self, device, uart_driver_api};

unsafe extern "C" fn rust_poll_out(_dev: *mut device, _out_char: cty::c_uchar)
{
    ()
}

static UART_API: uart_driver_api = uart_driver_api {
    poll_out: Some(rust_poll_out),
    poll_in: None,
    err_check: None,
    configure: None,
    config_get: None,
};

このような実装でビルドが通ります。

deviceとdevice_config

ひとまず、具体的な型を愚直に書きます。トレイト、ジェネリクス、関連型を使えば、もっと良くできると思います。が、それは今後の課題、ということで。

pub struct Device {
    pub config: &'static DeviceConfig,
-    pub driver_api: usize,
+    pub driver_api: &'static zephyr::uart_driver_api,
    pub driver_data: usize,
}

pub struct DeviceConfig {
-    pub name: usize,
+    pub name: &'static [u8],
    pub init: unsafe extern "C" fn(device: *mut Device) -> cty::c_int,
    pub config_info: usize,
}
#[link_section = ".init_POST_KERNEL40"]
static __DEVICE_MY_DEVICE: Device = Device {
    config: &__CONFIG_MY_DEVICE,
-    driver_api: 0,
+    driver_api: &UART_API,
    driver_data: 0
};

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

一度動作確認

さて、ここまでで空の実装ができました。 ビルドして実行しても、HardFaultにはならないはずです。

$ ninja run
[0/1] To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: cortex-m3
qemu-system-arm: warning: nic stellaris_enet.0 has no peer
# 予想通りここでブロックします

rust_poll_outの実装

ターゲットのUART (stellaris-uart) では、ベースアドレスに送信用レジスタがあります。 お行儀が悪いですが、次のように実装して、本当に動くかどうか、確かめてみましょう。

unsafe extern "C" fn rust_poll_out(_dev: *mut device, out_char: cty::c_uchar) {
    *(zephyr::UART_0_BASE_ADDRESS as *mut u32) = out_char as u32;
}

動作確認

$ ninja run
[0/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 from Rust!

動いちゃいました!

Zephyr×Rustのインテグレーションにチャレンジ!⑨~UART Driver 準備編~

はじめに

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

前回は、RustでDriverの初期化関数を書いて、動作させることに成功しました。

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

今回から、もう少しちゃんとデバイスDriverを作っていきます。 まずは、QEMUのUARTをターゲットにします。QEMUのUARTデバイスガバガバで、初期化を行わなくてもデータ送受信ができるので、敷居が低いです。

下準備

専用のdevice tree bindingを準備

既存のstellaris UART Driverの実装を修正しない方法でやりたいため、Rustで書くDriver用に、device treeのbindingを作成します。

|> zephyr/dts/bindings/serial

cp ti,stellaris-uart.yaml rust,simple-uart.yaml

ベースとなるstellarisのbindingをコピーして、compatibleを変更するだけです。

---
title: TI Stellaris UART Driver Bindings Written in Rust
version: 0.1

description: >
    This binding gives a base representation of the TI Stellaris UART

inherits:
    !include uart.yaml

properties:
    compatible:
-      constraint: "ti,stellaris-uart"
+      constraint: "rust,simple-uart"

device tree修正

&uart0 {
+   compatible = "rust,simple-uart";
    status = "ok";
    current-speed = <115200>;
};

ちなみに、uart0には、UART_0というラベルがついています。

|> zephyr/dts/arm/ti/lm3s6965.dtsi

                uart0: uart@4000c000 {
                        compatible = "ti,stellaris-uart";
                        reg = <0x4000c000 0x4c>;
                        interrupts = <5 3>;
                        status = "disabled";
                        label = "UART_0";
                };

kernel config修正

uart0を間借りするので、本来のuart0をconfigで無効化します。 これをしないと、device treeで必要なマクロが生成されていない状態で、stellaris Driverのインスタンスを作ろうとするため、コンパイルエラーになります。

|> prj.conf

CONFIG_UART_STELLARIS_PORT_0=n

一度動作確認

ビルドができて、実行はできない、ことを確認します。

$ mkdir build && cd $_
$ cmake -GNinja -DBOARD=qemu_cortex_m3 ..
$ ninja run
[0/1] To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: cortex-m3
qemu-system-arm: warning: nic stellaris_enet.0 has no peer
qemu: fatal: Lockup: can't escalate 3 to HardFault (current priority -1)

QEMUでHardFaultが発生しました。 UART Driverで必要とされるAPIを実装していないので、一旦、これでOKです。

Zephyrのdriverを作ってみよう④~device tree~

はじめに

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

|> zephyr/drivers/serial/uart_stellaris.c

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

前回までで、ポーリングベースの送受信ができるUART Driverを実装しました。

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

今回は、ハードウェアを定義するためのdevice treeを作成していきます。 device treeは他のモジュールとのインタフェースにも使われます。UARTの場合はshell subsystemがどのUARTデバイスを入出力に利用するか、の指定はdevice treeの記述を元に行われます。

device tree

Linuxでハードウェア構成を記述するための形式です。主にARMなど組込みで用いられるアーキテクチャで利用されます。

組込みLinuxでplatform deviceを記述する3つの方法(board file/device tree/ACPI DSDT)

Linux Foundation傘下のプロジェクトのためか、Zephyrもハードウェア構成の記述にdevice treeを用います。 文法は同じですが、扱われ方は、Linuxとはかなり違います。

Linuxでは、device treeは単独でblobを形成し、Linux kernelの外に置かれます。そして、Linux kernelはランタイムでこのblobにある情報を参照します。

一方、Zephyrでは、device treeを、ビルド時にheaderファイルを生成するために利用します。 ソフトウェアが扱うハードウェア構成は、ビルド時に固定されます。

UART device tree nodeの使われ方

まず、写経対象であるstellaris-uartのdevice tree nodeがどのように記述されるか、を見ます。

|> build/zephyr/qemu_cortex_m3.dts_compiled

                uart0: uart@4000c000 {
                        compatible = "ti,stellaris-uart";
                        reg = <0x4000c000 0x4c>;
                        interrupts = <0x05 0x03>;
                        status = "ok";
                        label = "UART_0";
                        current-speed = <0x1c200>;
                };

このdevice treeノードから生成されるheaderファイルは次のようになります。

|> build/zephyr/include/generated/generated_dts_board.h

/* uart@4000c000 */
#define CONFIG_UART_CONSOLE_ON_DEV_NAME                 "UART_0"
#define DT_TI_STELLARIS_UART_4000C000_BASE_ADDRESS      0x4000c000
#define DT_TI_STELLARIS_UART_4000C000_CURRENT_SPEED     115200
#define DT_TI_STELLARIS_UART_4000C000_IRQ_0             5
#define DT_TI_STELLARIS_UART_4000C000_IRQ_0_PRIORITY    3
#define DT_TI_STELLARIS_UART_4000C000_LABEL             "UART_0"
#define DT_TI_STELLARIS_UART_4000C000_SIZE              76
#define DT_TI_STELLARIS_UART_UART_0_BASE_ADDRESS        DT_TI_STELLARIS_UART_4000C000_BASE_ADDRESS
#define DT_TI_STELLARIS_UART_UART_0_CURRENT_SPEED       DT_TI_STELLARIS_UART_4000C000_CURRENT_SPEED
#define DT_TI_STELLARIS_UART_UART_0_IRQ                 DT_TI_STELLARIS_UART_4000C000_IRQ_0
#define DT_TI_STELLARIS_UART_UART_0_IRQ_PRIORITY        DT_TI_STELLARIS_UART_4000C000_IRQ_0_PRIORITY
#define DT_TI_STELLARIS_UART_UART_0_LABEL               DT_TI_STELLARIS_UART_4000C000_LABEL
#define DT_TI_STELLARIS_UART_UART_0_SIZE                DT_TI_STELLARIS_UART_4000C000_SIZE
#define UART_0_BASE_ADDRESS                             DT_TI_STELLARIS_UART_4000C000_BASE_ADDRESS
#define UART_0_CURRENT_SPEED                            DT_TI_STELLARIS_UART_4000C000_CURRENT_SPEED
#define UART_0_IRQ                                      DT_TI_STELLARIS_UART_4000C000_IRQ_0
#define UART_0_IRQ_PRIORITY                             DT_TI_STELLARIS_UART_4000C000_IRQ_0_PRIORITY
#define UART_0_LABEL                                    DT_TI_STELLARIS_UART_4000C000_LABEL
#define UART_0_SIZE                                     DT_TI_STELLARIS_UART_4000C000_SIZE

Driverでは、このheaderファイルで定義されたマクロを使って、デバイスパラメータの設定や、デバイス名の参照を行います。

static const struct uart_device_config uart_my_uart_dev_cfg_0 = {
        .base = (u8_t *)DT_TI_STELLARIS_UART_4000C000_BASE_ADDRESS,
        .sys_clk_freq = DT_UART_MY_UART_CLK_FREQ,
};

UART device tree binding

対応するdevice treeのbindingを見てみます。

|> zephyr/dts/bindings/serial

# at dts/bindings/serial
$ ls
altera,jtag-uart.yaml    nxp,kinetis-uart.yaml
arm,cmsdk-uart.yaml      nxp,lpc-usart.yaml
atmel,sam0-uart.yaml     sifive,uart0.yaml
atmel,sam-uart.yaml      silabs,gecko-leuart.yaml
atmel,sam-usart.yaml     silabs,gecko-uart.yaml
cypress,psoc6-uart.yaml  silabs,gecko-usart.yaml
intel,qmsi-uart.yaml     st,stm32-lpuart.yaml
microsemi,coreuart.yaml  st,stm32-uart.yaml
nordic,nrf-uarte.yaml    st,stm32-usart.yaml
nordic,nrf-uart.yaml     ti,cc32xx-uart.yaml
ns16550.yaml             ti,msp432p4xx-uart.yaml
nxp,imx-uart.yaml        ti,stellaris-uart.yaml
nxp,kinetis-lpsci.yaml   uart-device.yaml
nxp,kinetis-lpuart.yaml  uart.yaml

Zephyrでは、device treeのbindingはyaml形式で記述します。 pythonで解析しやすいですし、人間にとっても見やすいので、これはLinuxから進化しているところだと思います。

早速、これまで写経してきたstellaris-uartのbindingを見てみます。

|> ti,stellaris-uart.yaml

---
title: TI Stellaris UART
version: 0.1

description: >
    This binding gives a base representation of the TI Stellaris UART

inherits:
    !include uart.yaml

properties:
    compatible:
      constraint: "ti,stellaris-uart"

    reg:
      type: array
      description: mmio register space
      generation: define
      category: required

    interrupts:
      type: array
      category: required
      description: required interrupts
      generation: define
...

ファイル名と、compatibleが対応していそうです。後程、自分のDriverのbindingを作るときに試してみます。

UARTデバイスで共通して必要となるプロパティは、uart.yamlで定義されており、それを継承しています。

|> uart.yaml

---
title: Uart Base Structure
version: 0.1

description: >
    This binding gives the base structures for all UART devices

child:
    bus: uart

properties:
    compatible:
      type: string
      category: required
      description: compatible strings
      generation: define
    clock-frequency:
      type: int
      category: optional
      description: Clock frequency information for UART operation
      generation: define
    current-speed:
      type: int
      category: required
      description: Initial baud rate setting for UART
      generation: define
...

自作Driverのbinding作成

stellarisのbindingをコピーして、compatibleだけ変更します。

# at zephyr/dts/bindings/serial
cp ti,stellaris-uart.yaml tn,my-uart.yaml
properties:
    compatible:
-      constraint: "ti,stellaris-uart"
+      constraint: "tn,my-uart"

device tree nodeの修正

compatibleを上書きします。

|> qemu_cortex_m3.dts

&uart0 {
+   compatible = "tn,my-uart";
    status = "ok";
    current-speed = <115200>;
};

UART driverの修正

device treeから生成されるマクロを使用している部分を修正します。 例えば、次のような箇所です。

static const struct uart_device_config uart_my_uart_dev_cfg_0 = {
-   .base = (u8_t *)DT_TI_STELLARIS_UART_4000C000_BASE_ADDRESS,
+   .base = (u8_t *)DT_TN_MY_UART_4000C000_BASE_ADDRESS,
    .sys_clk_freq = DT_UART_MY_UART_CLK_FREQ,
};

ビルド

ビルドしてみます。

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

[107/112] Linking C executable zephyr/zephyr_prebuilt.elf
Memory region         Used Size  Region Size  %age Used
           FLASH:       23548 B       256 KB      8.98%
            SRAM:        8196 B        64 KB     12.51%
        IDT_LIST:           8 B         2 KB      0.39%
[112/112] Linking C executable zephyr/zephyr.elf

通りましたね…。

生成されたheaderを確認してみます。

|> build/zephyr/include/generated/generated_dts_board.h

/* uart@4000c000 */
#define CONFIG_UART_CONSOLE_ON_DEV_NAME         "UART_0"
#define DT_TN_MY_UART_4000C000_BASE_ADDRESS     0x4000c000
#define DT_TN_MY_UART_4000C000_CURRENT_SPEED    115200
#define DT_TN_MY_UART_4000C000_IRQ_0            5
#define DT_TN_MY_UART_4000C000_IRQ_0_PRIORITY   3
#define DT_TN_MY_UART_4000C000_LABEL            "UART_0"
#define DT_TN_MY_UART_4000C000_SIZE             76
...

ちゃんとマクロが生成されています。

動作確認

$ 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:~$ 
  clear    help     history  resize   shell

問題ありません。

実験

compatibleから対応するbindingを見つけるのを、どうやっているか調べます。 まず、単純にyamlのファイル名とcompatibleをマッチングしている可能性があります。 実験してみます。

$ mv tn,my-uart.yaml ti,my-uart.yaml

これは、普通にビルドが通ります。

ということで、ちゃんとyamlcompatibleまで見に行っているみたいですね。

ざっと、pythonを見てみると、device treeからすべてのcompatibleを取得した後、対象のyamlディレクトリ内からcompatibleで引っかけているみたいです。

$ find . -name *.yaml | wc -l
232

全ファイル舐めても232しかないので、計算時間はあまり気にならなさそうです。

Zephyr×Rustのインテグレーションにチャレンジ!⑧~続システムコール調査~

はじめに

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

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

前回までのあらすじ

アプリケーションに公開するDriver APIを作るため、システムコールの実装を調査しています。 システムコール実装のために必要な要素のうち、__syscallプレフィックスのついたプロトタイプ宣言について調査しました。

  • __syscallプレフィックスのついたプロトタイプ宣言。ビルドプロセス内でgen_syscall.pyによって処理されます。
  • 実装関数。実際のシステムコール実装です。
  • ハンドラ関数。引数を検証してから、実装関数に渡すためのラッパーです。

実装関数

実装関数を作る際に気を付けることは、名前の付け方です。 _impl_プレフィックスシステムコールAPI、にする必要があります。

プロトタイプ宣言は自動生成されるため、自分でプロトタイプ宣言を作る必要はありません。

ハンドラ関数

ハンドラ関数は、ユーザースレッドがシステムコールを発行した場合、kernel側で動作します。 ユーザースレッドがスーパーバイザモードに移行するため、ソフトウェア割り込みを発生させると、共通のシステムコールエントリポイントは、システムコールIDを使って、適切なハンドラ関数を見つけて、ジャンプします。

ハンドラ関数の目的は、与えられた引数を検証することです。

  • kernel objectポインタが与えられた場合、システムコールを呼び出したスレッドが、パーミッションを持っているかどうか、検証します。
  • ユーザー空間から与えられたバッファの場合、システムコールを呼び出したスレッドが、読み/書きのパーミッションを持っているかどうか、検証します。
  • そのほかの引数では、値が制限範囲内にあるかどうか、検証します。

引数検証

引数を検証するためのマクロが定義されています。 いくつか紹介します。

  • Z_SYSCALL_OBJ()。kernel objectが期待した通りの型か、呼び出しスレッドは適切なパーミッションを持っているか、初期化されているか、を検証します。
  • Z_SYSCALL_MEMORY_READ()。指定サイズのメモリバッファ全体に対して、読み込み権限があるかどうか、を検証します。
  • Z_SYSCALL_MEMORY_WRITE()Z_SYSCALL_MEMORY_READ()と似ていますが、書き込み権限があるかどうか、を検証します。

検証に失敗した場合、Z_OOPS()が呼び出され、呼び出しスレッドをkillします。

ハンドラの宣言

_SYSCALL_HANDLER()マクロを使って宣言できます。

Z_SYSCALL_HANDLER(k_sem_init, sem, initial_count, limit)
{
    ...
}

ということで、既存のシステムコールを利用するならともかく、新しいシステムコールをRustで作ろうとすると、苦労しそうですね…。

Zephyr×Rustのインテグレーションにチャレンジ!⑦~システムコール調査~

はじめに

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

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

前回までのあらすじ

Rustで書いたDriverの初期化関数から、文字が出力できました。

前回のDriverでは、ユーザーアプリケーションから、Driver APIを使う方法がありません。 そこで、ユーザーアプリケーションからDriver APIを利用できるように、システムコールを実装します。 実装する前には、調査が必要です。

Zephyrでどのようにシステムコールが実装されているか、見ていきます。

System Calls

少し古いドキュメントですが、下記にシステムコールについて書かれています (1.13以降はZephyr Kernel Primerの章が削除されています…)。

docs.zephyrproject.org

Components

  • __syscallプレフィックスのついたプロトタイプ宣言。ビルドプロセス内でgen_syscall.pyによって処理されます。
  • 実装関数。実際のシステムコール実装です。
  • ハンドラ関数。引数を検証してから、実装関数に渡すためのラッパーです。

プロトタイプ宣言

システムコールにする関数は、__syscallを付けて宣言されています。

__syscall void k_sem_init(struct k_sem *sem, unsigned int initial_count,
                          unsigned int limit);

__syscall属性は、非常に特殊です。コンパイラは、単純にstatic inlineに展開します。 post-buildで実行されるparse_syscalls.pyは、__syscall属性がついたAPIを解析します。細かい制限はありますが、一旦置いておきます。

呼び出しコンテキスト

システムコールを呼び出しているソースコードが、ユーザーモードで実行されるか、スーパーバイザモードで実行されるか、でシステムコールの呼び出し方を変更します。 基本、スーパーバイザモードからのシステムコール呼び出しは、単純に実装関数を呼び出す簡易の実装になります。

Cソースファイルごとに実行モードを判定するために、ビルドプロセスで、__ZEPHYR_SUPERVISOR____ZEPHYR_USER__というマクロをコンパイラフラグとして追加します。

  • CONFIG_USERSPACEが有効になっていない場合、全てのシステムコールAPIは、直接、実装関数を呼び出します。
  • __ZEPHYR_SUPERVISOR__が定義されていると、直接、実装関数を呼び出します。
  • __ZEPHYR_USER__が定義されていると、システムコールが無条件に発行されます。

実装の詳細

__syscallを使ってAPIを宣言すると、scripts/gen_syscalls.pyによって、Cソースファイルとヘッダファイルが生成されます。 生成されたファイルは、include/generated下にあります。

  • システムコールシステムコールID型で番号付けされます。include/generated/syscall_list.hK_SYSCALL_から始まるAPIとして表現されます。
  • include/generated/syscall_list.hにハンドラ関数のプロトタイプが生成されます。
  • include/generated/syscall_dispatch.cシステムコール発行テーブル (_k_sycall_table) にエントリが生成されます。
  • ハンドラ関数は、未実装のシステムコールハンドラのエイリアスである、weakハンドラ関数として宣言されます。

k_sem_init()は、build/zephyr/include/generated/syscalls/kernel.hで次のようになっています。

K_SYSCALL_DECLARE3_VOID(K_SYSCALL_K_SEM_INIT, k_sem_init, struct k_sem *,
                        sem, unsigned int, initial_count,
                        unsigned int, limit);

この場合のマクロは、次のように実装されています。上述した通り、システムコールの呼び出しコンテキストによって実装が変わります。

#if !defined(CONFIG_USERSPACE) || defined(__ZEPHYR_SUPERVISOR__)

#define K_SYSCALL_DECLARE3_VOID(id, name, t0, p0, t1, p1, t2, p2) \
        extern void _impl_##name(t0 p0, t1 p1, t2 p2); \
        static inline void name(t0 p0, t1 p1, t2 p2) \
        { \
                _impl_##name(p0, p1, p2); \
        }

#elif defined(__ZEPHYR_USER__)
#define K_SYSCALL_DECLARE3_VOID(id, name, t0, p0, t1, p1, t2, p2) \
        static inline void name(t0 p0, t1 p1, t2 p2) \
        { \
                _arch_syscall_invoke3((u32_t)p0, (u32_t)p1, (u32_t)p2, id); \
        }

#else /* mixed kernel/user macros */
#define K_SYSCALL_DECLARE3_VOID(id, name, t0, p0, t1, p1, t2, p2) \
        extern void _impl_##name(t0 p0, t1 p1, t2 p2); \
        static inline void name(t0 p0, t1 p1, t2 p2) \
        { \
                if (_is_user_context()) { \
                        _arch_syscall_invoke3((u32_t)p0, (u32_t)p1, (u32_t)p2, id); \
                } else { \
                        compiler_barrier(); \
                        _impl_##name(p0, p1, p2); \
                } \
        }
#endif

あー、やっと、アーキテクチャ固有のシステムコール呼び出しが見えました。

だいぶ、長くなってきたので、次回に続きます。