SOLID未分類 SOLID for Raspberry Pi 4 (連載21)
(2023/3/31)
前回書いたプログラムや内容について、有識者の方に読んでいただき、フィードバックを頂きました。
今回はその内容について記載します。
前回、コールバック関数との共有変数をタプル形式で定義しましたが、読みにくくなりました。
共有変数すべてを一つの構造体でまとめる書き方について教えて頂きましたので、紹介します。
コードの書き方は色々あると思いますが、筆者の場合は以下を共有変数としています。
・送信データ格納バッファ
(前回は3バイトだったが、よく考えると2バイトで大丈夫。)
・受信データ
・転送開始直後のTX-FIFOエンプティ割り込みなのかどうか
・レジスタリード関数なのかライト関数なのか
・SPI転送完了したかどうか
これらを構造体にまとめます。
struct XferState {
send_data: [AtomicU8; 2],
recv_data: AtomicI32,
is_first: AtomicBool,
is_read: AtomicBool,
is_done: AtomicBool,
}
そして、グローバル変数として使えるよう、静的インスタンス化します。
static XFER_STATE: XferState = XferState{
send_data: [AtomicU8::new(0),AtomicU8::new(0)],
recv_data: AtomicI32::new(0),
is_first: AtomicBool::new(false),
is_read: AtomicBool::new(false),
is_done: AtomicBool::new(false)
};
この構造体を、コールバック関数とSPI転送(リード・ライト)関数で共有することにしました。
実は先週、タプル形式で書く前に、構造体で書こうとして、あれこれ試行錯誤して失敗していました。
構造体の中の配列を初期化するところで、
[AtomicU8::new(0),AtomicU8::new(0)]
と書く事を思いつけずにウンウン唸っていました。
言われてみれば、あぁそうか、と思えるのですが、、、
#ソースコードは別途ダウンロードできるようにします。
お気づきの方も多いかと思いますが、筆者の書いた割り込みハンドラは、「割り込み」の特色を全く生かしていないものです。
目的 ⇒「RustでSOLID-OSの割り込み機能を使うこと!」
であったため、「割り込みハンドラの在り方」については気にしていませんでした。
これで終わってしまっては、なんだかイマイチです。
(まぁ指摘受けたからそう言っていますが、、別にいいかなぁ♪なんて思ってました。)
では、どうあるべきか。
せっかくリアルタイムOS上で動いているんです。
待ってる間は自タスクをスリープにすべきでしょう。
具体的に書きます。
SPI転送(リード・ライト)関数は、転送開始後、転送完了フラグをポーリングすることで転送終了を待っています。これでは、CSレジスタのDONEビットをポーリングするのと大して違いありません。
せっかく転送終了したら割り込みハンドラが起動するのだから、その起動した割り込みハンドラによって起こしてもらうようにすべきです。
SPI転送(リード・ライト)関数:
SPI転送を開始(TAビットを1)したら寝る。
割り込みハンドラ:
転送終了したら、SPI転送(リード・ライト)関数を起こす。
実装方法は以下のような感じになりそうです。
タスクスリープ対応SPI転送(リード・ライト)関数:
①自タスクIDを取得する。
②取得したIDをアトミックなグローバル変数に格納する
③既存のSPI転送開始処理を行う
④スリープする
⑤タスクIDを取り除く
割り込みハンドラ:
①既存の処理を行う
②転送が終了していれば、
・アトミックなグローバル変数から対象のタスクID値を取得
・対象のタスクをwakeupさせる。
ひとつひとつ、コード例を教えてもらったので、見ていきます。
アトミックとして、共有できるようにします。
/// アクティブなSPI転送の発行元タスクIDを格納するグローバル変数。
/// 制御機構を独占するためのミューテックスの役割も兼ねる。
static XFER_ACTIVE_TID: AtomicI32 = AtomicI32::new(0);
itron関数を使用してIDを取得します。
let mut tid = 0;
assert_eq!(unsafe { itron::abi::get_tid(&mut tid) }, itron::abi::E_OK);
assert_ne!(tid, 0);
先程準備したグローバル変数に格納します。
XFER_ACTIVE_TID.compare_exchange(0, tid, Ordering::AcqRel, Ordering::Relaxed)
.expect("transfer already in progress");
itron関数のslp_tsk()を使用します。
assert_eq!(unsafe { itron::abi::slp_tsk() }, itron::abi::E_OK);
先程準備したグローバル変数に格納されたタスクIDを取り除きます。
XFER_ACTIVE_TID.store(0, Ordering::Release);
グローバル変数から対象のタスクIDを取得し、itron関数のwup_tsk()を使用しwakeupさせます。
let tid = XFER_ACTIVE_TID.load(Ordering::Acquire);
if tid != 0 {
unsafe { itron::abi::wup_tsk(tid) };
}
コード例っていうか。。。
そのまま動くやん!
無事、動作確認ができました!
割り込みハンドラで、対象タスクのスリープ解除をする直前でブレークしてみました。
SOLID-IDEのRTOSビューアで見てみると、スリープになっている事がわかります。
#ご紹介した以外にも、イベントフラグを用いる方法もあります。
前回、SPI転送完了割り込みの優先度について、とりあえず 10 固定にしました。
今回は、SOLID-OSのSOLID_INTC_GetPriorityLevel()関数を使って、システムで許されている最大値を取得し、その付近の値を優先度として設定してみます。
SOLID_INTC_GetPriorityLevel()関数は、solidクレートの中で、C言語の関数をコールするためのWrapperが定義されています。。
https://github.com/KyotoMicrocomputer/solid-rapi4-examples/blob/main/common/solid/src/abi.rs
solidクレートの中の、abiの中にあるので、
solid::abi::SOLID_INTC_GetPriorityLevel()
で使用できます。
さらに、C言語なので戻り値がc_int型です。
i32型に変換するため、
.into()
を付けます。
すると、今度は unsafeだよ!と怒られました。
という事で、unsafe{ }で囲んだところ、エラーがなくなりました。
let spi_handler_option = interrupt::HandlerOptions::new(interrupt::Number(150), unsafe{solid::abi::SOLID_INTC_GetPriorityLevel().into()});
このコードを実行してみます。
max_priorityには何が入っているでしょうか。
16ですね。
– 2 として、優先順位14あたりがいいかもしれません。
let max_priority: i32 = unsafe{solid::abi::SOLID_INTC_GetPriorityLevel().into()};
let spi_handler_option = interrupt::HandlerOptions::new(interrupt::Number(150), max_priority - 2);
前回、
「singleton::pin_singleton, thread::CpuCxについては、今回は「おまじない」という事でご容赦ください。。。」
でごまかしたご容赦頂いた部分ですが、フィードバック頂きました。
ものすごく簡単にイメージだけで言うと、
・pin_singleton!
プログラムを実行中に1回だけ走らせるという事ができ(シングルトン)、かつ、割り込み構造体等ずっと同じメモリに留め置く(ピン)型のみ受け付けるマクロ。ハードウェアの初期設定時に使うと便利。
・CpuCx<'a>
SOLID_CPU_CONTEXTのラッパー。
以下、下手に自分の言葉で書こうとせず、フィードバック頂いた内容をそのまま引用することにします。
[solid::singleton::pin_singleton!]
指定した式でグローバル変数を初期化し、変数への参照を Pin<&'static mut _> (pinned reference) として返すマクロです。プログラムを実行中に1回だけこれができ、2回目以降は Err(_) を返します。
ポイントを挙げると以下の通りです。
• グローバル変数を定義する
• 変数を実行時に初期化して、'static mutable 参照を返す
• 2回目以降は失敗する
• 単なる参照 (unpinned reference) ではなくpinned referenceを返すsolid::interrupt::Handler と solid::timer::Timer を静的割当てする実装アプローチを取る場合に、コードを簡潔にすることが目的です。実行時に初期化するので、ローカル変数をハンドラ関数 (クロージャ―) にmoveすることも可能です (rust-blinky-pac-ap804の例)。
動作イメージとしては以下のようになります (シングルスレッド、非リエントラント仮定)。// let foo = pin_singleton!(: Handler<_> = Handler::new(HandlerFn)); let foo = unsafe { static mut VAR: Handler; static mut INIT: bool; if !INIT { INIT = true; VAR = Handler::new(HandlerFn); Ok(Pin::static_mut(&mut VAR)) } else { Err(todo!()) } };
solid::interrupt::Handler に含まれる SOLID_INTC_HANDLER 構造体は、SOLID_INTC_Register 関数のAPI規約上、割込みハンドラが登録されている間はずっと同じメモリ番地に存在し続けなければなりません (i.e., Safe Rustからこれを違反できてはいけません)。
この制約を型で表現するのが、 Pin<&'static mut Handler> のようなpinned referenceです。solid::timer::Timer も同様で、これらの型のメソッドはpinned referencesのみを受け付けます。Pinned referencesを作るには次の方法があります。
1. 最も簡単な方法は Box::pin で値をヒープに移して Pin<Box> スマートポインタを得ることです (rust-blinky-pac-csの古い例)。
2. あるいは Box::new から Box::leak して得られる &'static mut _ を Pin::static_mut に渡せば、 Pin<&'static mut _> が得られます (rust-blinky-pac-ap804の古い例)。
3. どちらの方法も動的メモリ割当てのオーバヘッドが掛かります。静的メモリ割当てすることは可能ですが、やや冗長なボイラープレートコードが必要になります (rust-server-gothamの古い例)。変数の動的な初期化が必要な場合、さらに冗長になります。
4. Pin::new_unchecked はpinned referenceの性質をプログラマが自らの責任で保証しなければならないため、unsafeコードからしか呼び出せません。
このマクロでは3つ目の方法を簡潔なコードで実現できるようにしています。
[solid::thread::CpuCx<'a>]
これは SOLID_CPU_CONTEXT のラッパーです。現時点では何もラッパーとしての付加機能はありませんが、ラッパーを介して渡すことにより追加の余地を残しています。
割込み発生時点のCPUレジスタの値を取得するのに使用できます (rust-server-gothamの例)。
以上ここまでで、Rust×SOLID-OS×SPI加速度センサとの格闘はひと段落です。
今までのソースコードを、以下からダウンロードできます。
何かのご参考になれば幸いです。
PC側C#プログラム(Visual Studio 2019系)
(bin\Releaseフォルダ内にexecutableファイルがあります)
加速度センサ制御部はクレート化しました。
SPI割り込みハンドラ等の部分については、spi_acxelrl; クレートのソースコードに入っています。
次回は、SOLID-OSの持つ他のAPIをRustからコールしてみます。
C言語で作られたAPIですが、今回使用してみたSOLID_INTC_GetPriorityLevel()関数のように、solidクレートとしてRust側に解放されているので、それらを使用してみつつ、RustとC言語の橋渡しの部分について理解を深められるといいなと思っています。