ZephyrでRTTコンソール
はじめに
久々のZephyrです。
JLinkには、JTAG/SWD経由でシリアル入出力をする機能があります。 UARTを使ったコンソールも良いのですが、物理配線が増えたり、簡単にUARTを引き出せない場合、RTTが便利です。
Zephyrコンフィグ
NordicのZephyrフォークを参考に、必要そうなコンフィグを有効化していきます。
- CONFIG_USE_SEGGER_RTT=y
- CONFIG_SHELL_BACKEND_RTT=y
上の2つあたりが必須そうです。 次の内容で設定ファイルを作成します。
|> zephyr/samples/subsys/shell/shell_module/prj_minimal_rtt.conf
CONFIG_SHELL=y CONFIG_KERNEL_SHELL=n CONFIG_SHELL_BACKEND_SERIAL=n CONFIG_OBJECT_TRACING=y CONFIG_THREAD_MONITOR=y CONFIG_INIT_STACKS=y CONFIG_BOOT_BANNER=n CONFIG_THREAD_NAME=y CONFIG_LOG=n CONFIG_SHELL_HISTORY=n CONFIG_SHELL_STACK_SIZE=1024 CONFIG_SHELL_CMD_BUFF_SIZE=128 CONFIG_SHELL_WILDCARD=n CONFIG_SHELL_HELP_ON_WRONG_ARGUMENT_COUNT=n CONFIG_SHELL_STATS=n CONFIG_SHELL_CMDS=n CONFIG_CONSOLE=y #enable RTT shell CONFIG_USE_SEGGER_RTT=y CONFIG_SHELL_BACKEND_RTT=y
ビルドしてターゲットボードに書き込みます。
mkdir build && cd $_ cmake -GNinja -DBOARD=nrf52840_pca10056 -DCONF_FILE="prj.conf prj_minimal_rtt.conf" .. ninja flash
RTTクライアントを立ち上げます。 まず、JLinkの接続から。
JLinkExe -device nrf52840_xxaa -speed 4000 -if SWD J-Link>connect
RTTクライアントを起動します。
$ JLinkRTTClient ###RTT Client: ************************************************************ ###RTT Client: * SEGGER Microcontroller GmbH * ###RTT Client: * Solutions for real time microcontroller applications * ###RTT Client: ************************************************************ ###RTT Client: * * ###RTT Client: * (c) 2012 - 2016 SEGGER Microcontroller GmbH * ###RTT Client: * * ###RTT Client: * www.segger.com Support: support@segger.com * ###RTT Client: * * ###RTT Client: ************************************************************ ###RTT Client: * * ###RTT Client: * SEGGER J-Link RTT Client Compiled Mar 27 2019 17:12:05 * ###RTT Client: * * ###RTT Client: ************************************************************ ###RTT Client: ----------------------------------------------- ###RTT Client: Connecting to J-Link RTT Server via localhost:19021 Connected. llss^?^?###RTT Client: Connection lost. Going to reconnect. ###RTT Client: Connecting to J-Link RTT Server via localhost:19021 .. Connected. SEGGER J-Link V6.44d - Real time terminal output J-Link OB-SAM3U128-V2-NordicSemi compiled Jan 7 2019 14:07:15 V1.0, SN=683516743 Process: JLinkExe rtt:~$
Zephyrのshell rtt:~$
が立ち上がっています。
Tabでの補完も効きます。
rtt:~$ demo device dynamic gpio log_test version
version情報を見てみましょう。
rtt:~$ vvveeerrrsssiiiooonnn Zephyr version 1.13.99
あん?何故かキー入力に対して、3回文字が出力されます。 Zephyr側はコマンドを受け付けているので、RTTClientの問題っぽいですね…。
どうもどこかのバージョンからバグったみたいですね…。
Zephyrのブートログが出力されていません。 上記設定では、printkが出力されていません。
UARTコンソールが有効になっているせいかと思いきや、そうでもないようです。
LOG_PRINTK
を設定しないとダメ?
お試して、LOG機能を有効化して、バックエンドをRTTにします。
ダメですね…。
1.14のリリースノートで解決済みになっているようですが、1.14でもprintkでの出力は出ていないような…?
下のサンプルだと、printkが出力されることがわかりました。
|> zephyr/samples/subsys/logging/logger
Process: JLinkExe ***** Booting Zephyr OS zephyr-v1.14.0-1039-gc3ccbbbdc3ca ***** Module logging showcase. [00:00:00.125,030] <inf> sample_module: log in test_module 11 [00:00:00.125,061] <inf> sample_module: Inline function. Disabling logging in the sample_module module Function called again but with logging disabled. Instance level logging showcase. [00:00:00.125,213] <inf> sample_instance.inst1: Inline call. [00:00:00.125,213] <inf> sample_instance.inst1: counter_value: 0 [00:00:00.125,213] <wrn> sample_instance.inst1: Example of hexdump: 01 02 03 04 |.... [00:00:00.125,213] <inf> sample_instance.inst2: Inline call. [00:00:00.125,213] <inf> sample_instance.inst2: counter_value: 0 [00:00:00.125,244] <wrn> sample_instance.inst2: Example of hexdump: 01 02 03 04 |.... Changing filter to warning on sample_instance.inst1 instance. [00:00:00.125,305] <wrn> sample_instance.inst1: Example of hexdump: 01 0m
設定ファイルを見てみます。
$ cat prj_rtt.conf CONFIG_LOG=y CONFIG_LOG_RUNTIME_FILTERING=y CONFIG_LOG_BUFFER_SIZE=2048 CONFIG_LOG_PRINTK=y CONFIG_LOG_PROCESS_TRIGGER_THRESHOLD=0 CONFIG_LOG_BACKEND_RTT=y CONFIG_USE_SEGGER_RTT=y CONFIG_COVERAGE=n
ふむふむ。LOG_BACKEND_RTT
を有効にするようですね。
では、これをshellの方の設定に継ぎ足して…
$ ninja zephyr/subsys/shell/shell_rtt.c:7: ../../../../../include/toolchain/gcc.h:28:37: error: static assertion failed: "Conflicting log RTT backend enabled on the same channel" #define BUILD_ASSERT_MSG(EXPR, MSG) _Static_assert(EXPR, MSG) ^~~~~~~~~~~~~~ zephyr/subsys/shell/shell_rtt.c:12:1: note: in expansion of macro 'BUILD_ASSERT_MSG' BUILD_ASSERT_MSG(!(IS_ENABLED(CONFIG_LOG_BACKEND_RTT) && ^~~~~~~~~~~~~~~~ [64/130] Building C object zephyr/CMak...phyr.dir/subsys/logging/log_core.c.obj ninja: build stopped: subcommand failed.
ああん? まさか、shellとログ、どちらかしか使えないってこと…?
Rust製組込みOSのTockを触ってみる②
はじめに
諸事情によりまとまった時間を取るのが難しいので、入門系やります。
今回は、Rust製のRTOSであるTockを触ってみます。 githubのTock organizationはこちらです。
前回はC言語で書かれたサンプルアプリケーションを動かしたので、今回はRustで書かれたサンプルアプリケーションを動かしてみます。
libtock-rs
Rustでアプリケーションを書くためのライブラリレポジトリは、libtock-rsです。 examplesは、nRF52-DKでなら動作するようです。 nRF52840-DKとは別物ですが、まぁ動くでしょう。やってみましょう。
rustup target add thumbv7em-none-eabi git clone https://github.com/tock/libtock-rs.git cd libtock-rs ./run_example.sh blink
無事書き込めて、動いています。
+ elf_file_name=target/tab/blink/cortex-m4.elf + tab_file_name=target/tab/blink.tab + tockloader_flags='--jlink --arch cortex-m4 --board nrf52dk --jtag-device nrf52 --app-address 0x20000' + hail_defined= + '[' -n '' ']' + mkdir -p target/tab/blink + cp target/thumbv7em-none-eabi/release/examples/blink target/tab/blink/cortex-m4.elf + elf2tab -n blink -o target/tab/blink.tab target/tab/blink/cortex-m4.elf --stack 2048 --app-heap 1024 --kernel-heap 1024 --protected-region-size=64 + '[' 1 -ge 2 ']' + tockloader uninstall --jlink --arch cortex-m4 --board nrf52dk --jtag-device nrf52 --app-address 0x20000 Preparing to uninstall apps... Using known arch and jtag-device for known board nrf52dk No apps are installed on the board + true + tockloader install --jlink --arch cortex-m4 --board nrf52dk --jtag-device nrf52 --app-address 0x20000 target/tab/blink.tab Installing apps on the board... Using known arch and jtag-device for known board nrf52dk Finished in 1.961 seconds
なにやら色々やっていますね。 事前にアプリケーションがインストールされていると、アンインストールするみたいです。
examplesチラ見
BLEのサンプルがあって気になります。
|> exmaples/ble_scanning.rs
#![no_std] use libtock::ble_parser; use libtock::led; use libtock::simple_ble; use libtock::simple_ble::BleCallback; use libtock::simple_ble::BleDriver; use libtock::syscalls; use serde::Deserialize; #[derive(Deserialize)] struct LedCommand { pub nr: u8, pub st: bool, } // Prevents the compiler from dropping the subscription too early. #[allow(unreachable_code)] fn main() { let mut shared_buffer = BleDriver::create_scan_buffer(); let mut my_buffer = BleDriver::create_scan_buffer(); let shared_memory = BleDriver::share_memory(&mut shared_buffer).unwrap(); let mut callback = BleCallback::new(|_: usize, _: usize| { shared_memory.read_bytes(&mut my_buffer[..]); ble_parser::find(&my_buffer, simple_ble::gap_data::SERVICE_DATA as u8) .and_then(|service_data| ble_parser::extract_for_service([91, 79], service_data)) .and_then(|payload| corepack::from_bytes::<LedCommand>(&payload).ok()) .and_then(|msg| led::get(msg.nr as isize).map(|led| led.set_state(msg.st))); }); let _subscription = BleDriver::start(&mut callback).unwrap(); loop { syscalls::yieldk(); } }
あー、serdeビルドしているの、なぜだろうと思ってましたが、ここで使っていたのですね…。 割とビルド時間長いのは、気になりますね。
BLE Advertising driverもkernel側に実装されています。
libtock-rsにBleAdvertisingDriver
の実装がありますが、基本はシステムコールを呼ぶだけで、実態はkernel側のようですね。
おもむろにDRIVER_NUMBER
が使用されていますが、これはkernel側のシステムコールで定義されています。
|> libtock-rs/src/simple_ble.rs
const DRIVER_NUMBER: usize = 0x30000; pub const MAX_PAYLOAD_SIZE: usize = 9; pub const BUFFER_SIZE_ADVERTISE: usize = 39; pub const BUFFER_SIZE_SCAN: usize = 39; mod ble_commands { pub const START_ADVERTISING: usize = 0; pub const ALLOW_ADVERTISMENT_BUFFER: usize = 0; pub const BLE_PASSIVE_SCAN_SUB: usize = 0; pub const ALLOW_SCAN_BUFFER: usize = 1; pub const PASSIVE_SCAN: usize = 5; }
|> tock/doc/syscalls/README.md
Radio
1.0 | Driver Number | Driver | Description |
---|---|---|---|
0x30000 | BLE | Bluetooth Low Energy | |
0x30001 | 802.15.4 | IEEE 802.15.4 | |
0x30002 | UDP | UDP / 6LoWPAN Interface |
なるほどね〜。
Rust製組込みOSのTockを触ってみる①
はじめに
諸事情によりまとまった時間を取るのが難しいので、入門系やります。
今回は、Rust製のRTOSであるTockを触ってみます。 githubのTock organizationはこちらです。
Getting started
Getting startedを参考に、何か動かしてみます。
Requirements
- nightly Rust (開発グループは
nightly-2019-04-11
を使用) - make
- tockloader
Rustとmakeはインストールされているので、tockloaderをインストールします。
pip3 install --upgrade tockloader --user
ttyUSBを管理者権限なしで使えるようにします。
grep -q dialout <(groups $(whoami)) || sudo usermod -a -G dialout $(whoami)
Compile kernel
git clone https://github.com/tock/tock.git cd tock/
kernelをビルドする前に、まずボードを選びます。 手元にnRF52840-DKがあるので、それにします。
kernelをビルドして、ボードに書き込みます。
cd boards/nordic/nrf52840dk/ make flash
JLinkで何やら書き込んでいるようです。
Finished release [optimized + debuginfo] target(s) in 2.36s text data bss dec hex filename 71676 1940 260204 333820 517fc target/thumbv7em-none-eabi/release/nrf52840dk tockloader flash --address 0x00000 --jlink --board nrf52dk target/thumbv7em-none-eabi/release/nrf52840dk.bin Flashing binar(y|ies) to board... Using known arch and jtag-device for known board nrf52dk Finished in 5.365 seconds
build example application
C言語のユーザーアプリケーションを実行してみましょう。
git clone https://github.com/tock/libtock-c.git cd libtock-c/ cd examples/blink/ make DIR ../../libtock/build/cortex-m0 CC ../../libtock/internal/alarm_internal.c /bin/sh: 1: arm-none-eabi-gcc: not found ../../TockLibrary.mk:149: recipe for target '../../libtock/build/cortex-m0/alarm_internal.o' failed make: *** [../../libtock/build/cortex-m0/alarm_internal.o] Error 127
む?C言語のアプリケーション実行には、GCCが必要みたいです。 (そういえばPC新しくしてからインストールしてなかったな)
sudo apt install gcc-arm-none-eabi
改めて、今度はビルドが通ります。
make
アプリケーションを書き込みます。
tockloader install --jlink --board nrf52dk
無事、Lチカできました!
参考
Rustコンパイラ組込みのlintについて調べる
はじめに
Rustコンパイラであるrustc
には組込みのlintがあります。
Rustのlintツールでは、clippyが有名ですが、rustc組込みの方もソースコードの改善に役立ちます。
7月の技術書同人誌博覧会のネタ探しの一環だったのですが、思ったより世の中の役に立ちそうだったので、ドラフトを公開します。 原稿落とさなければ、「組込み / ベアメタルRustテクニック集」的な薄い本を出す予定です。
ちなみにここに書いている内容は、全てThe rustc bookに書いてあります。
lint
lint
はソースコードをコンパイラより厳密なルールに則り、検査するためのツールです。
Rustコンパイラには、様々なlintルールが組み込まれています。
ソースコードをコンパイルする時、自動的にlintによる検査が行われます。
プロジェクトの運用ルールに合わせて、適切なlintルールを設定することで、ソースコードの品質をより向上できるでしょう。
lintレベル
rustcのlintレベルは、4つに分類されます。
- allow (許可)
- warn (警告)
- deny (拒絶)
- forbid (禁止)
各lintルールには、デフォルトのlintレベルがあり、コンパイルオプションかアトリビュートで上書きできるようになっています。 まず、lintレベルについて説明します。
allow (許可)
lintルールを適用しません。 例えば、次のコードをコンパイルしても、警告は発生しません。
pub fn foo() {}
$ rustc lib.rs --crate-type=lib
しかし、このコードはmissing_docs
ルールを違反しています。
lintレベルを上書きしてコンパイルすると、コンパイルエラーになったり、警告が出力されるようになります。
warn (警告)
lintルール違反があった場合、警告を表示します。
fn main() { let x = 5; }
このコードは次の警告が報告されます。
warning: unused variable: `x` --> src/main.rs:2:9 | 2 | let x = 5; | ^ help: consider prefixing with an underscore: `_x` | = note: #[warn(unused_variables)] on by default
deny (拒絶)
lintルール違反があった場合、コンパイルエラーになります。
fn main() { 100u8 << 10; }
このコードは、exceeding_bitshifts
ルールに違反しており、コンパイルエラーになります。
error: attempt to shift left with overflow --> src/main.rs:2:5 | 2 | 100u8 << 10; | ^^^^^^^^^^^ | = note: #[deny(exceeding_bitshifts)] on by default
forbid (禁止)
lintルール違反があった場合、コンパイルエラーになります。 forbidは、denyより強いレベルで、上書きができません。
下のコードは、アトリビュートでmissing_docs
ルールをallowに上書きしています。
#![allow(missing_docs)] pub fn foo() {}
missing_dogs
ルールを、denyレベルに設定してコンパイルすると、このコードはコンパイルできます。
$ rustc lib.rs --crate-type=lib -D missing-docs
一方、forbidレベルに設定してコンパイルすると、コンパイルエラーになります。
$ rustc lib.rs --crate-type=lib -F missing-docs error[E0453]: allow(missing_docs) overruled by outer forbid(missing_docs) --> lib.rs:1:10 | 1 | #![allow(missing_docs)] | ^^^^^^^^^^^^ overruled by previous forbid | = note: `forbid` lint level was set on command line
lintレベルの設定方法
コンパイラフラグで設定
コンパイルオプションで、-A
, -W
, -D
, -F
のいずれかを指定して、lintレベルを設定できます。
$ rustc lib.rs --crate-type=lib -W missing-docs
もちろん、複数のフラグを同時に設定することも可能です。
$ rustc lib.rs --crate-type=lib -D missing-docs -A unused-variables
Cargoの設定ファイル内で、lintレベルを設定することも可能です。
$ cat .cargo/config
[build] rustflags = ["-D", "unsafe-code"]
アトリビュートで設定
ソースコード内のアトリビュートで、allow
, warn
, deny
, forbid
のいずれかを指定して、lintレベルを設定できます。
$ cat lib.rs #![warn(missing_docs)] pub fn foo() {}
1つのアトリビュートに、複数のlintルールを指定できます。
#![warn(missing_docs, unused_variables)] fn main() { pub fn foo() {} }
複数のアトリビュートを組み合わせて使うこともできます。
#![warn(missing_docs)] #![deny(unused_variables)] pub fn foo() {}
lintルール
次のコマンドでlintルールと、デフォルトレベルの一覧が取得できます。
$ rustc -W help
デフォルトレベルごとに、サンプルコード付きでlintルールが説明されています。
知られざるlintルールたち
ちらっと目について、へー、と思ったルールを独断と偏見で紹介します。
ちょっとマニアック
- unsafe-code:
unsafe
ブロックがある。デフォルトallow。unsafe
ブロックを使わせたくないアイツの.cargo/config
にこっそりどうぞ。 - illegal-floating-point-literal-pattern: パターン内で浮動小数点が使われている。デフォルトwarn。
- no-mangle-generic-items: ジェネリック関数にno_mangleアトリビュートをつける。デフォルトwarn。
warn
なんだ?強制的にマングルされるってことかしら?
自分が知らんかった枠
- variant-size-differences: enumのヴァリアントのサイズが違いすぎる。デフォルトallow。
- improper-ctypes:
extern "C"
の中などFFIでRustの型を使う。デフォルトwarn。 - no-mangle-const-items: シンボルを持たない
const
変数にno_mangleアトリビュートをつける。デフォルトdeny。言われると納得。やったことないけど。
RustのRTFM (Real Time For the Masses)を試してみる⑤
はじめに
組込みRust界の伝説japaric
氏が実装しているReal Time For the Masses (RTFM) framework for ARM Cortex-M microcontrollersを試してみます。
サンプルコードをマクロ展開して心が折れそうです。 とりあえず、当面サンプルコードの表面をなぞります。
タスク
RTFMでは、割り込みと例外をハードウェアタスクとして扱います。 RTFMでは、ソフトウェアタスクも提供しています。
ソフトウェアタスクは、割り込みハンドラから、優先度を指定してディスパッチできます。
ソフトウェアタスクをディスパッチする割り込みハンドラは、extern
ブロックの中で宣言しておく必要があります。
ソフトウェアタスクになる関数は、task
アトリビュートをつけます。
ソフトウェアタスクをspawnする関数は、contextアトリビュートにspawn
引数をつけます。
#[app(device = lm3s6965)] const APP: () = { #[init(spawn = [foo])] fn init() { spawn.foo().unwrap(); } #[task(spawn = [bar, baz])] fn foo() { hprintln!("foo").unwrap(); // spawns `bar` onto the task scheduler // `foo` and `bar` have the same priority so `bar` will not run until // after `foo` terminates spawn.bar().unwrap(); // spawns `baz` onto the task scheduler // `baz` has higher priority than `foo` so it immediately preempts `foo` spawn.baz().unwrap(); } #[task] fn bar() { hprintln!("bar").unwrap(); debug::exit(debug::EXIT_SUCCESS); } #[task(priority = 2)] fn baz() { hprintln!("baz").unwrap(); } // Interrupt handlers used to dispatch software tasks extern "C" { fn UART0(); fn UART1(); } };
spawn
の結果は、Result
で返ってきます。
これは、タスクが起動できる数 (capacity) が指定できるようになっており、その数以上のタスクを起動できないからです。
capacityはデフォルトでは、1になっています。
#[interrupt(spawn = [foo, bar])] fn UART0() { spawn.foo(0).unwrap(); spawn.foo(1).unwrap(); spawn.foo(2).unwrap(); spawn.foo(3).unwrap(); spawn.bar().unwrap(); // runtime error spawn.bar().unwrap(); } #[task(capacity = 4)] fn foo(x: u32) { hprintln!("foo({})", x).unwrap(); } #[task] fn bar() { hprintln!("bar").unwrap(); debug::exit(debug::EXIT_SUCCESS); }
ソフトウェアタスクには、メッセージを渡すことができます。 メッセージは、タスクの引数として、シグネチャに含める必要があります。
#[app(device = lm3s6965)] const APP: () = { #[init(spawn = [foo])] fn init() { spawn.foo(/* no message */).unwrap(); } #[task(spawn = [bar])] fn foo() { static mut COUNT: u32 = 0; hprintln!("foo").unwrap(); spawn.bar(*COUNT).unwrap(); *COUNT += 1; } #[task(spawn = [baz])] fn bar(x: u32) { hprintln!("bar({})", x).unwrap(); spawn.baz(x + 1, x + 2).unwrap(); } #[task(spawn = [foo])] fn baz(x: u32, y: u32) { hprintln!("baz({}, {})", x, y).unwrap(); if x + y > 4 { debug::exit(debug::EXIT_SUCCESS); } spawn.foo().unwrap(); } }
foo
のコードで、static mut COUNT
を使用する際、デリファレンスしているのが気になりました。
fn foo(c: foo::Context) { static mut COUNT: u32 = 0; // ... c.spawn.bar(*COUNT).unwrap(); *COUNT += 1; }
どうも、static mut
を参照にマクロ展開しているようです。
次の手順でマクロ展開したコードを確認しました。
$ cargo rustc --example message -- -Z unstable-options --pretty=expanded > message_expanded.rs # フォーマットしないとツラいです $ rustfmt message_expanded.rs
foo
は次のように展開されます。
#[allow(non_snake_case)] fn foo(__locals: foo::Locals, c: foo::Context) { use rtfm::Mutex as _; let COUNT = __locals.COUNT; ::cortex_m_semihosting::export::hstdout_str("foo\n").unwrap(); c.spawn.bar(*COUNT).unwrap(); *COUNT += 1; }
foo::Locals
という型で引数を受け取っており、そこからCOUNTを取得しています。
foo::Locals
は、次の通りです。
#[allow(non_snake_case)] #[doc(hidden)] pub struct fooLocals<'a> { COUNT: &'a mut u32, __marker__: core::marker::PhantomData<&'a mut ()>, }
COUNT
が参照になっていますね。
試しに、COUNT
をデリファレンスせずに使うと、次のコンパイルエラーになります。
error[E0308]: mismatched types --> examples/message.rs:25:21 | 25 | c.spawn.bar(COUNT).unwrap(); | ^^^^^ | | | expected u32, found &mut u32 | help: consider dereferencing the borrow: `*COUNT` | = note: expected type `u32` found type `&mut u32`
いやー、知らないと意味がわからないですよ、これ。
RustのRTFM (Real Time For the Masses)を試してみる④
はじめに
組込みRust界の伝説japaric
氏が実装しているReal Time For the Masses (RTFM) framework for ARM Cortex-M microcontrollersを試してみます。
面白いので、引き続き触っていきます。
リソース管理続き
RTFMのstatic
変数は、通常のstatic
変数と異なり、実行時に初期化できます。
このように実行時に初期化するリソースをlate resourcesと呼んでいます。
補足すると、lazy_static
とは、また異なる仕組みのようです。Optionで包んで云々、ということはやっていません。
リソースの初期化は、init
関数で行う必要があり、init
関数は、初期化したリソースを戻り値にしなければなりません。
何を言っているのかわからないと思うので、サンプルコードを見てみましょう。
use heapless::{ consts::*, spsc::{Consumer, Producer, Queue}, }; #[app(device = lm3s6965)] const APP: () = { // Late resources `()`を初期値にします static mut P: Producer<'static, u32, U4> = (); static mut C: Consumer<'static, u32, U4> = (); #[init] fn init() -> init::LateResources { // NOTE: we use `Option` here to work around the lack of // a stable `const` constructor static mut Q: Option<Queue<u32, U4>> = None; *Q = Some(Queue::new()); let (p, c) = Q.as_mut().unwrap().split(); // late resourcesを初期化して、関数の戻り値とします init::LateResources { P: p, C: c } } #[idle(resources = [C])] fn idle() -> ! { loop { if let Some(byte) = resources.C.dequeue() { hprintln!("received message: {}", byte).unwrap(); debug::exit(debug::EXIT_SUCCESS); } else { rtfm::pend(Interrupt::UART0); } } } #[interrupt(resources = [P])] fn UART0() { resources.P.enqueue(42).unwrap(); } };
まず、()
で初期化されたlate resourcesを宣言します。
static mut P: Producer<'static, u32, U4> = (); static mut C: Consumer<'static, u32, U4> = ();
次に、init
関数のシグネチャが、init::LateResources
を返すものに変わります。
このinit::LateResources
は、late resourcesのP
とC
とのタプルです。
#[init] fn init() -> init::LateResources {
idle
はC、UART0
はPを、それぞれ専有して利用するため、ロックなしでリソースを利用することができます。
このサンプルでは、`heaphessクレートのqueueが使われています。
このheaplessクレートは、文字通り、ヒープがない環境でコレクションを使うためのクレートです。
queueは、single producer / single consumerのモデルになっています。
このqueueの初期化をinit
で行い、ProducerとConsumerのオブジェクトを共有リソースとして管理することで、うまいこと安全な機構になっているように見えます。
リソースがただのstatic
で、可変参照にならない場合も、ロックなしでリソースにアクセスできます。
これは、late resourcesにも適用されます。
#[app(device = lm3s6965)] const APP: () = { static KEY: u32 = (); #[init] fn init() -> init::LateResources { rtfm::pend(Interrupt::UART0); rtfm::pend(Interrupt::UART1); init::LateResources { KEY: 0xdeadbeef } } #[interrupt(resources = [KEY])] fn UART0() { hprintln!("UART0(KEY = {:#x})", resources.KEY).unwrap(); debug::exit(debug::EXIT_SUCCESS); } #[interrupt(priority = 2, resources = [KEY])] fn UART1() { hprintln!("UART1(KEY = {:#x})", resources.KEY).unwrap(); } };
RustのRTFM (Real Time For the Masses)を試してみる③
はじめに
組込みRust界の伝説japaric
氏が実装しているReal Time For the Masses (RTFM) framework for ARM Cortex-M microcontrollersを試してみます。
軽い気持ちで触り始めたのですが、読み進めるうちに、手続きマクロでゴリゴリ静的解析している変態実装であることがわかってきました。 少しずつフレームワークの実装にも突っ込んでいきたいですね。
リソース管理
cortex-m-rt
クレートを使用する場合、割り込みとメイン関数間でのデータ共有に制限があります。
常に、全ての割り込みを無効化するcortex_m::interrupt::Mutex
を使わなければなりません。
しかし、全ての割り込みを無効化することは、常に求められる条件ではありません。
例えば、2つの割り込みハンドラがデータを共有する場合、両者の優先度が同じで、プリエンプションが発生しないとすると、ロックは不要です。
RTFMでは、ソースコードを静的に解析することで、不要なロックをせずに、共有データにアクセスできるようになっています。
このような解析が可能な理由は、app
アトリビュート内にアプリケーションの実装を、全て書くためです。
また、RTFMでは、動的な割り込み優先度の変更をサポートしていません。 そのため、全ての割り込みハンドラ間の優先度は静的に決定します。 これがうまいこと生きており、ある共有データを使用する割り込みハンドラ同士で、最も優先度の高いハンドラはロックを取得しなくても共有データにアクセスできます。
そろそろサンプルコードを見てみましょう。
共有データは、static
変数として宣言します。
#[app(device = lm3s6965)] const APP: () = { // A resource static mut SHARED: u32 = 0; #[init] fn init() { rtfm::pend(Interrupt::UART0); rtfm::pend(Interrupt::UART1); } #[idle] fn idle() -> ! { debug::exit(debug::EXIT_SUCCESS); // error: `SHARED` can't be accessed from this context // SHARED += 1; loop {} } // `SHARED` can be access from this context #[interrupt(resources = [SHARED])] fn UART0() { *resources.SHARED += 1; hprintln!("UART0: SHARED = {}", resources.SHARED).unwrap(); } // `SHARED` can be access from this context #[interrupt(resources = [SHARED])] fn UART1() { *resources.SHARED += 1; hprintln!("UART1: SHARED = {}", resources.SHARED).unwrap(); } };
共有データにアクセスする関数は、アトリビュート内でresources = [<変数名>]
を引数にします。
ここで、2つの割り込みハンドラUART0
とUART1
は共に共有データSHARED
を使用します。
2つの割り込みは、割り込み優先度が同じで、プリエンプションも発生しません。
そのため、2つの割り込みハンドラでは、Mutexなしで共有データにアクセスできます。
割り込み優先度を設定する場合、同じ共有データを使用する割り込みハンドラのうち、優先度が最も高いハンドラだけはロックが不要です。
//! examples/lock.rs #![deny(unsafe_code)] #![deny(warnings)] #![no_main] #![no_std] extern crate panic_semihosting; use cortex_m_semihosting::{debug, hprintln}; use lm3s6965::Interrupt; #[rtfm::app(device = lm3s6965)] const APP: () = { static mut SHARED: u32 = 0; #[init] fn init(_: init::Context) { rtfm::pend(Interrupt::GPIOA); } // when omitted priority is assumed to be `1` #[interrupt(resources = [SHARED])] fn GPIOA(mut c: GPIOA::Context) { hprintln!("A").unwrap(); // the lower priority task requires a critical section to access the data c.resources.SHARED.lock(|shared| { // data can only be modified within this critical section (closure) *shared += 1; // GPIOB will *not* run right now due to the critical section rtfm::pend(Interrupt::GPIOB); hprintln!("B - SHARED = {}", *shared).unwrap(); // GPIOC does not contend for `SHARED` so it's allowed to run now rtfm::pend(Interrupt::GPIOC); }); // critical section is over: GPIOB can now start hprintln!("E").unwrap(); debug::exit(debug::EXIT_SUCCESS); } #[interrupt(priority = 2, resources = [SHARED])] fn GPIOB(mut c: GPIOB::Context) { // the higher priority task does *not* need a critical section *c.resources.SHARED += 1; hprintln!("D - SHARED = {}", *c.resources.SHARED).unwrap(); } #[interrupt(priority = 3)] fn GPIOC(_: GPIOC::Context) { hprintln!("C").unwrap(); } };
ドキュメントによると、このプログラムの実行結果は、次のようになるようです。
$ cargo run --example lock A B - SHARED = 1 C D - SHARED = 2 E
note: 手元の環境で実行すると、次の結果になります。要調査。レポジトリのCIは通っているようなので、手元環境の問題?
A D - SHARED = 2 B - SHARED = 2 C E
GPIOAの共有データアクセスは、クリティカルセクションのMutexを取得しており、割り込み禁止状態で行われます。
少し上記プログラムを改変して、どうなるか試してみます。
const APP: () = { static mut SHARED: u32 = 0; // ... #[interrupt(resources = [SHARED])] fn GPIOA() { hprintln!("A").unwrap(); // GPIOBと同じ方法でアクセスしてみます *c.resources.SHARED += 1; hprintln!("E").unwrap(); debug::exit(debug::EXIT_SUCCESS); } #[interrupt(priority = 2, resources = [SHARED])] fn GPIOB(mut c: GPIOB::Context) { *c.resources.SHARED += 1; hprintln!("D - SHARED = {}", *c.resources.SHARED).unwrap(); } // ... };
ビルドすると次のコンパイルエラーになります。
$ cargo run --example lock error[E0614]: type `resources::SHARED<'_>` cannot be dereferenced --> examples/lock.rs:41:9 | 41 | *c.resources.SHARED += 1; | ^^^^^^^^^^^^^^^^^ error: aborting due to previous error For more information about this error, try `rustc --explain E0614`. error: Could not compile `cortex-m-rtfm`. To learn more, run the command again with --verbose.
どうも無名の型になっているようで、詳細がつかみにくいですが、GPIOA()
とGPIOB
()とで、
resource::SHARED`の型が違う、ということがわかります。