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が動くかどうか、を見ていきましょう。
実装
まず、APIのpoll_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
に登録するname
にUART_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, >, }
いらないAPIはNone
にしておけば良さそうです。
パーツは揃っていそうなので、早速やっていきましょう。
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
これは、普通にビルドが通ります。
ということで、ちゃんとyamlのcompatible
まで見に行っているみたいですね。
ざっと、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の章が削除されています…)。
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.h
にK_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
あー、やっと、アーキテクチャ固有のシステムコール呼び出しが見えました。
だいぶ、長くなってきたので、次回に続きます。