SOLID未分類 SOLID for Raspberry Pi 4 (連載20)

SOLID for Raspberry Pi 4 (連載20)

(2023/3/20)

20.Rust, SOLID-OSで割り込み処理[実装編]

前回、SOLID-OS上で割り込み処理をRustで書くために必要なこと、について調べた結果をもとに、実装していきます。

一点、訂正があります。

前回、
「今回発生させたい割り込みは、SPI受信・送信時の割り込みです。」
と書きましたが、正しくはBCM2711のSPIでは以下2つの割り込みがあります。
・転送が完了したことを示すDONEビットに連動した割り込み
・受信FIFOにデータが3/4以上たまってきたことを示す割り込み

今回たくさんのデータを受信するわけではないため、「転送完了を示す割り込み」のみ使用することにします。

1.準備

現状、spi_adxl345クレートに、加速度センサへのアクセスをすべて入れ込んでいます。

今回、SPI送受信方法として割り込みを用いた方法に変更するため、spi_adxl345クレートの中身を変更する必要があります。

筆者だけかもしれないのですが、クレートは「閉じたライブラリ」という意識があって
クレートのままいろいろ変更するのは、ちょっとしっくりきません。
なので、一旦、spi_adxl345クレートの中を、main関数のあるlib.rsに持ってくることにします。

持ってくる、といっても、大それたことをするわけではありません。

 

①Cargo.tomlの[dependencies]から、spi_adxl345クレート参照部分を削除する。

[dependencies]
tock-registers = "0.7.0"
itron = { version = "= 0.1.9", features = ["unstable", "nightly", "solid_fmp3"] }
bcm2711_pac.path = "../../bcm2711_pac"
#bcm2711_pac = { git = "https://github.com/KyotoMicrocomputer/solid-rapi4-examples.git" } 
#spi_adxl345.path = "../../spi_adxl345"
env_logger = { version = "0.9", default-features = false }
log = "0.4"

#spi_adxl345.path = "../../spi_adxl345"
の部分です。コメントアウトします。

②spi_adxl345クレートのlib.rs内容をすべてmain関数のあるlib.rsにコピーし、モジュール化する。
モジュール名はmod spi_adxl345。
ファイル最後にぺたーっと貼り付ければOK。

ここで一度ビルドして実行してみたところ、特に今までと相違なく動きました。

 

2.割り込みの登録

次に、登録した割り込みを許可する方法について調べていきます。

前回はRustで記述されたタイマサンプルを参考にして調査しました。
https://github.com/KyotoMicrocomputer/solid-rapi4-examples/blob/main/rust-blinky-pac-cs/rustapp/src/lib.rs

ここで、大変有益な情報が!

interruptを使ったサンプルがありました!
https://github.com/KyotoMicrocomputer/solid-rapi4-examples/blob/main/rust-blinky-pac-ap804/rustapp/src/lib.rs

こちらを参考にしながら、実装を進めていきます。

 

2.1 solidクレートを使えるようにする ― 依存関係の設定

SOLID-OSのAPIをコールできるようにするために、solidクレートを依存関係として定義します。

操作方法は、以下のURLに書かれています。
https://github.com/KyotoMicrocomputer/solid-rapi4-examples/tree/main/common/solid

2つの操作が必要です。

・Cargo.tomlに以下を書く
[dependencies]
solid = { git = "https://github.com/KyotoMicrocomputer/solid-rapi4-examples.git" , features = ["std"] }

・rustソース側の.ptrsproj ファイル(tomlファイルと同じフォルダにあります)に「CargoEnvironmentVariables 」要素を追加する

 

2.2 solidクレートを使えるようにする ― スコープ内に取り込む

今回、mod spi_adxl345内で、solidクレートのinterruptオブジェクトを使用するので、以下のように定義し、使えるようにします。

use solid::{interrupt, singleton::pin_singleton, thread::CpuCx};

ここで一旦Buildしました。
(singleton::pin_singleton, thread::CpuCxについては、今回は「おまじない」という事でご容赦ください。。。)

 

2.3 割り込み登録

いよいよ割り込みを登録する処理を書いていきます。
spi_init()関数でSPIに関する初期設定をしているので、ここに追加するのが良いです。

①割り込みハンドラ登録用構造体の準備
割り込みを登録するための情報を持った構造体を準備します。

typedef struct _SOLID_INTC_HANDLER_ {
    int intno;
    int priority;
    int config;
    int (*func)(void*, SOLID_CPU_CONTEXT*);
    void* param;
} SOLID_INTC_HANDLER;

これに具体的な数値を入れ、準備します。

以下のnewで、intnoとpriorityを登録します。

SPI割り込みは、GIC-400の 96 + 54 = 150 番に割り振られていますので、intnoは150とします。[2023/3/23 記述追記]

priorityについては、許可されている最大値としようかと思ったのですが、すみません、手っ取り早く 10 とします。(サンプルと同じにした)

let spi_handler_option = interrupt::HandlerOptions::new(interrupt::Number(150),10);

 

② ハンドラの登録
割り込み発生時のコールバック関数名を「spi_trans_handler()」とし、ハンドラを登録します。

let spi_handler = pin_singleton!(: Handler<_> = interrupt::Handler::new(|_: CpuCx<'_>| spi_trans_handler())).unwrap(); 

[2023/3/23 ソースコード一部修正]

(重ね重ねですが、、、pin_singleton, CpuCxについては、今回は「おまじない」という事でご容赦ください。。。)

ここで、以下のunstable featureが必要だったので(エラーが出た)、以下をlib.rsファイル先頭に書きました。

[2023/3/23 記述一部修正]

#![feature(type_alias_impl_trait)]

spi_trans_handlerは、まだ空にしておきます。
とりあえず以下のコードで、コールバック関数に飛んで来たら割り込みを禁止するようにしておきます。

fn spi_trans_handler() {
	spi_regs().cs.modify(spi::CS::INTD::CLEAR);
}

 

③割り込みハンドラ登録用構造体の登録
先ほど準備したハンドラ登録用構造体を登録します。

spi_handler.register_static(&spi_handler_option);

これでビルドは通ったのですが、Warningが発生しました。
note: this `Result` may be an `Err` variant, which should be handled

という事で、.expect()つけてエラー処理が必要でした。[2023/3/23 記述一部修正]

サンプルから、真似してとってきます。

assert!(
	spi_handler.register_static(&spi_handler_option)
	.expect("unable to register interrupt handler"),
	"interrupt handler was already registered"
);

以上で割り込みを発生させるための準備ができました。

 

2.4 割り込み許可をする

2操作が必要です。
・SPI:割り込み許可ビットをセットする。
・GIC-400(割り込みコントローラ):割り込みの番号を指定して有効化する。

[2023/3/23 記述一部修正]

① SPIへの設定
今回使用するのは、SPI転送完了割り込みですので、CSレジスタのINTDビットをセットすればOKです。
spi_regs().cs.modify(spi::CS::INTD::SET);

② 割り込みEnable

以下のように設定すればOKです。[2023/3/23 記述一部修正]

interrupt::Number(150).enable().expect("unable to enable interrupt line");

ここまで、SPIを初期化する関数のソースコードは以下になりました。
println!("Register SPI Interrupt.");
以降のコードが該当します。

   fn spi_init() {
        const CLKSET: u32 = 0x00cb;

        gpio_init();

        //SPIのCLKレジスタにSPI周波数を設定
        spi_regs().clk.write(spi::CLK::CDIV.val(CLKSET));

        //CS : 00 = Chip select 0  -> CS.bit0 and 1
        //CPOL: Clock Polarity 1 = Rest state of clock = High -> CS.bit3
        //CPHA: Clock Phase 1 = First SCLK transition at beginning of data bit. -> CS.bit2
        spi_regs().cs.modify(spi::CS::CS::ChipSelect0);
        spi_regs().cs.modify(spi::CS::CPOL::RestStateIsHigh);
        spi_regs()
            .cs
            .modify(spi::CS::CPHA::FirstSclkTransitionAtBeginningOFDataBit);

        println!("Register SPI Interrupt.");
        let spi_handler_option = interrupt::HandlerOptions::new(interrupt::Number(150),10);
        let spi_handler = pin_singleton!(: Handler<_> = interrupt::Handler::new(move |_: CpuCx<'_>| spi_trans_handler())).unwrap(); 
    
        assert!(
            spi_handler.register_static(&spi_handler_option)
            .expect("unable to register interrupt handler"),
            "interrupt handler was already registered"
        );

        spi_regs().cs.modify(spi::CS::INTD::SET);  //割り込み発生要因:DONE = 1

        interrupt::Number(150).enable().expect("unable to enable interrupt line");

    }

 

2.5 割り込みに飛ぶかどうか確認

割り込みの登録と許可が終わりました。
では、ここでビルド&実行してみましょう。

実行する前にコールバック関数にブレークポイントを設定しておきましょう。
このブレークポイントで停止すればOK、という事ですね。

では、実行!

無事、飛んできました。

何の割り込みが発生しているか確認してみましょう。

CSレジスタの値を見ればわかるはずです。
PARTNER Command Windowから以下のようにコマンド発行します。

16ビット目が1だから転送完了を示す割り込みですね。

意図した通りに動いているようです。

 

3.SPI転送完了割り込みを使って通信する

という事で、割り込みを発生させることができました。第一関門突破です。
この割り込みを使った送受信ルーチンを書いて、動作確認ができたので、ご紹介します。

今までは、送信FIFOにデータを書く前に、毎回送信FIFOの空きを示すビットの状態を確認して丁寧に1バイトずつ書いていました。
今回は、送信FIFOに、送信したいデータを一気に書きます。

割り込みを使用しなくても、そのように一気に書けるのではないかと思いますが、そこを深く調査することは目的ではないので、先に進みます。

 

3.1 コールバック関数で行う事

まずレジスタライト処理についてご紹介します。
波形で見るとこうなります。

 

(1)のタイミングでは、レジスタライト関数によりCSレジスタのTAビットが1にされ、SPI転送が開始します。

①、③のタイミングで割り込みが発生し、コールバック関数が呼ばれます。

②のタイミングで最初の1バイトを受信完了、③のタイミングで次の1バイトを受信完了します。しかし今回は受信データを読み取る必要はないため、無視します。

コールバック関数では以下の処理を行います。
①起因の割り込み時(初回割り込み):
送信FIFOに、ライト対象レジスタアドレスとライトデータを格納。
③起因の割り込み時(送受信完了):
CSレジスタのTAビットを0としSPI転送完了。
レジスタライト関数に対し共有フラグで転送完了を通知。

[2023/3/23 記述一部修正]

 

次に、レジスタリード処理についてご紹介します。
波形で見るとこうなります。

(1)のタイミングでは、レジスタリード関数によりCSレジスタのTAビットが1にされ、SPI転送が開始します。

①、④のタイミングで割り込みが発生し、コールバック関数が呼ばれます。

②のタイミングで最初の1バイトを受信完了、③のタイミングで次の1バイトを受信完了、④のタイミングで最後の1バイトを受信完了します。
③の受信データが、指定レジスタの下位1バイト値です。
④の受信データが、指定レジスタの上位1バイト値です。

コールバック関数では以下の処理を行います。
①起因の割り込み時(初回割り込み):
送信FIFOに、リード対象レジスタアドレスを格納。
④起因の割り込み時(送受信完了):
受信FIFOの値を読み取り、CSレジスタのTAビットを0としSPI転送完了。
レジスタリード関数に対し共有フラグで転送完了を通知。

[2023/3/23 記述一部修正]

 

3.2 コールバック関数との共有変数

コールバック関数と、レジスタライト関数、レジスタリード関数の間で、共有しないといけない変数が出てきました。

・転送完了フラグ:指定したバイト数すべてが転送完了したかどうか、を示す。
・送信データ格納バッファ
・受信データ

順番に見ていきます。

①転送完了フラグ

普通に、staticで以下のように定義したら、「unsafeだ!」と怒られました。

static mut SPI_DONE: bool = false;

前回に引き続き、変数の共用方法を検討しないといけません。

せっかくなので、使ったことがない方法を試してみました。
Mutexは前回使ったので、アトミックでstaticなブール変数としてみました。

定義:

static SPI_DONE: AtomicBool = AtomicBool::new(false);

値を入れるとき:

SPI_DONE.store(true, Ordering::SeqCst);

値を読むとき:

SPI_DONE.load(Ordering::SeqCst);

Orderingはメモリ操作を順序付けするための指定ですが、複雑なトピックなのでここでは説明せず、SeqCstを指定することにします。[2023/3/23 記述一部修正]

https://doc.rust-lang.org/std/sync/atomic/enum.Ordering.html

 

② 送信データ格納バッファ
同じく、アトミックでstaticなタプル形式の変数としてみました。
アトミックにするために、配列でなくタプル形式を採りました。
(どうやって配列をアトミックにするのか、ちょっと調べてもわからずじまいで。。。)

static S_DATA: SendData = SendData(AtomicU8::new(0),AtomicU8::new(0), AtomicU8::new(0), AtomicBool::new(false),  AtomicBool::new(false));

③ 受信データ
16bitのデータなので、アトミックでstaticなi32変数にしてみました。
i32にしたのは、上位関数で都合が良かったからで、深い意味はありません。

static R_DATA: AtomicI32 = AtomicI32::new(0);

以上の共有変数を用いて、コールバック関数を以下のように書きました。

	fn spi_trans_handler() {
		//DONEビットが1でFIFO書き込みデータがあるなら、送信FIFOに書き込みデータをライトする。
		//DONEビットが1でFIFO書き込みデータがないなら、受信FIFOからデータをリードし割り込み発生フラグを1にするClearTA
		//DONEビットが0であれば何もしない
		if spi_regs().cs.is_set(spi::CS::DONE) {
			let transfer_type = S_DATA.4.load(Ordering::SeqCst);
			let first_flag = S_DATA.3.load(Ordering::SeqCst);

			if transfer_type == true && first_flag == true //regread初回割り込み
			{
				spi_regs().fifo.write(spi::FIFO::DATA.val(S_DATA.0.load(Ordering::SeqCst).into()));
				spi_regs().fifo.write(spi::FIFO::DATA.val(S_DATA.1.load(Ordering::SeqCst).into()));
				spi_regs().fifo.write(spi::FIFO::DATA.val(S_DATA.2.load(Ordering::SeqCst).into()));
				S_DATA.3.store(false, Ordering::SeqCst);
			}
			else if transfer_type == false && first_flag == true //regwrite初回割り込み
			{
				spi_regs().fifo.write(spi::FIFO::DATA.val(S_DATA.0.load(Ordering::SeqCst).into()));
				spi_regs().fifo.write(spi::FIFO::DATA.val(S_DATA.1.load(Ordering::SeqCst).into()));
				S_DATA.3.store(false, Ordering::SeqCst);
			}
			else 
			{
				if transfer_type == true //regread: 3回転送分のデータがRX FIFOに入っているという決め打ち。(Dummy, 下位1バイト、上位1バイト)
				{
					let retdata_h: u32;
					let retdata_l: u32;

					spi_regs().fifo.read(spi::FIFO::DATA); //Dummy read

					retdata_l = spi_regs().fifo.read(spi::FIFO::DATA);
					retdata_h = spi_regs().fifo.read(spi::FIFO::DATA);
					R_DATA.store(i32::from(i16::from_le_bytes([retdata_l as u8, retdata_h as u8])),Ordering::SeqCst);
				}

				spi_regs().cs.modify(spi::CS::TA::CLEAR);
				SPI_DONE.store(true, Ordering::SeqCst);
			}
		}
		else {}
	}

[2023/3/23 ソースコード一部修正]

レジスタライト関数は以下のようになりました。

    pub fn regwrite(regaddr: u8, writeval: u8) {
        //(1)転送完了フラグをクリアし、FIFO書き込みデータ準備
        SPI_DONE.store(false, Ordering::SeqCst);
        S_DATA.0.store(regaddr, Ordering::SeqCst);
        S_DATA.1.store(writeval, Ordering::SeqCst);
        S_DATA.3.store(true, Ordering::SeqCst);
        S_DATA.4.store(false, Ordering::SeqCst);

        //(2)CSレジスタのCLEARビットに0x03を書いてTX-FIFO, RX-FIFOクリア
        spi_regs().cs.modify(spi::CS::CLEAR_TX::SET + spi::CS::CLEAR_RX::SET);

        //(3)CSレジスタのTAビットを1にする。
        spi_regs().cs.modify(spi::CS::TA::SET);
        //割り込み発生しFIFOに値が書かれているはず。

        //(4)転送完了割り込み発生フラグがtrueになるまで待つ
        while !SPI_DONE.load(Ordering::SeqCst){}
    }

[2023/3/23 ソースコード一部修正]

レジスタリード関数は以下のようになりました。

fn regread(regval: u8) -> i32 {
        //(1)転送完了フラグをクリアし、FIFO書き込みデータ準備
        SPI_DONE.store(false, Ordering::SeqCst);
        S_DATA.0.store(regval, Ordering::SeqCst);
        S_DATA.1.store(0, Ordering::SeqCst);
        S_DATA.2.store(0, Ordering::SeqCst);
        S_DATA.3.store(true, Ordering::SeqCst);
        S_DATA.4.store(true, Ordering::SeqCst);

        //(2)CSレジスタのCLEARビットに0x03を書いてTX-FIFO, RX-FIFOクリア
        spi_regs().cs.modify(spi::CS::CLEAR_TX::SET + spi::CS::CLEAR_RX::SET);

        //(3)CSレジスタのTAビットを1にする。
        spi_regs().cs.modify(spi::CS::TA::SET);
        //割り込み発生しFIFOに値が書かれているはず。

        //(4)転送完了割り込み発生フラグがtrueになるまで待つ
        while !SPI_DONE.load(Ordering::SeqCst){}

        return R_DATA.load(Ordering::SeqCst);
    }

[2023/3/23 ソースコード一部修正]

[2023/3/23 追記]
最後の return R_DATA.load(Ordering::SeqCst); は、"return"と";"を削除できる、ということをすっかり忘れていて、ついついC言語ライクに書いてしまいました。

これで、SOLID-OSの持つC言語で記載された割り込み関連APIを、Rustからコールし、無事、割り込みを発生させることができました!
そのコールバック関数もRustで書けました!

 

以上、さらっと書いてきましたが、今回は特にコールバック関数を書き上げる際の共有変数の書き方についてドハマりしました。

それでもサンプルを参考にしたり、公式サイトで勉強したりすれば、いろいろ調べて自力で書くことができました。
本連載開始当初とは違い、Rustで何か書くにしても、なんとかなりそうな感じが見えてきた気がします。

今回はここまで。
次回はこのコードを有識者の方に見て頂く予定です。どういうFeedbackを頂けるか楽しみにしています。