SOLID未分類 SOLID for Raspberry Pi 4 (連載4)
(2022/11/16)
さて、とうとうRustでプログラムを書いてみます。
前回まで、C言語やC++言語でLチカを動かしてみました。
これがRustになるとどういう感じになるのか。。。
有識者の導きの下、やってみます。
C/C++をずっと使ってきたとはいえ、Rust初心者の筆者としては、ちょっとドキドキです、、、
最初に、とりあえず動かしてみます。
Rustライブラリを作成するためのワークスペースを作成し、サンプルコードをそのままコピー&ペーストし、ビルドして実行してみます。
サンプル一式は以下URLにあります。
https://github.com/KyotoMicrocomputer/solid-rapi4-examples
これから使用するサンプルコードは以下のものです。
https://github.com/KyotoMicrocomputer/solid-rapi4-examples/tree/main/rust-blinky-raw-rtos
lib.rsはこちら。
Rawなプログラム、と書きましたが、Rustコード上で直接GPIOレジスタを操作する、というニュアンスです。
筆者もそうですが、従来のC/C++民のイメージでは、Low LevelのLow、のニュアンスの方が近いかもしれません。
が、Rust民からすると、Rawだそうです。
こんなところから、感覚が違うんだなーと思いつつ。。。次に行きます。
まず、C++で新規ワークスペースを作成します。
①プロジェクトを作成します
②Rustライブラリを選択し、「ソリューションに追加」を選択します。
「ソリューションに追加」は、以下の赤枠の部分から選択できます。
以下のように作成できました。
③メインプロジェクトのリンカ設定で、 追加のライブラリファイル にライブラリ名を指定します。
④メインプロジェクトのビルド依存関係にRustのライブラリを追加します。
①main.cppを空にします。
②lib.rsに以下のように書きます。
#[no_mangle]
pub extern "C" fn slo_main() {
unsafe { ffi::SOLID_LOG_printf(b"Starting LED blinker\n\0".as_ptr().cast()) };
// Configure the LED port
green_led::init();
loop {
// Turn on the LED
green_led::update(true);
unsafe { ffi::dly_tsk(200_000) };
// Turn off the LED
green_led::update(false);
unsafe { ffi::dly_tsk(200_000) };
}
}
/// FFI declarations used in this application
#[allow(non_camel_case_types)]
mod ffi {
use std::os::raw::{c_char, c_int};
pub type int_t = c_int;
pub type RELTIM = u32;
pub type ER = int_t;
extern "C" {
pub fn SOLID_LOG_printf(format: *const c_char, ...);
pub fn dly_tsk(dlytim: RELTIM) -> ER;
}
}
mod green_led {
const GPIO_BASE: usize = 0xFE200000;
const GPIO_NUM: usize = 42;
pub fn init() {
unsafe {
let reg = (GPIO_BASE + (GPIO_NUM / 10) * 4) as *mut u32; // GPFSEL4
let mode = 1; // output
reg.write_volatile(
reg.read_volatile() & !(7 << (GPIO_NUM % 10 * 3)) | (mode << (GPIO_NUM % 10 * 3)),
);
}
}
pub fn update(new_state: bool) {
unsafe {
let reg = (GPIO_BASE
+ (GPIO_NUM / 32) * 4
+ if new_state {
0x1c /* GPSET1 */
} else {
0x28 /* GPCLR1 */
}) as *mut u32; // GPFSEL4
reg.write_volatile(1 << (GPIO_NUM % 32));
}
}
}
ビルドして実行をすると、LEDが点滅します。
デバッグ機能が普通に動きます。
・ブレークポイント
・ステップイン
GPIOレジスタの操作にPAC(peripheral access crate)を使用するプログラムです。
また、TOPPERSカーネル関数 dly_tsk を itron パッケージの itron::task::delay 関数を経由して使用します。
先程作成したlib.rsを、以下サンプルコードのものに置き換え、ビルドして実行してみます。
先程作成したlib.rsの内容を、以下プログラムに変更します。
use itron::{task::delay, time::duration};
#[no_mangle]
pub extern "C" fn slo_main() {
println!("Starting LED blinker");
// Configure the LED port
green_led::init();
loop {
// Turn on the LED
green_led::update(true);
delay(duration!(ms: 200)).unwrap();
// Turn off the LED
green_led::update(false);
delay(duration!(ms: 200)).unwrap();
}
}
mod green_led {
use bcm2711_pac::gpio;
use tock_registers::interfaces::{ReadWriteable, Writeable};
const GPIO_NUM: usize = 42;
fn gpio_regs() -> &'static gpio::Registers {
// Safety: SOLID for RaPi4B provides an identity mapping in this area, and we don't alter
// the mapping
unsafe { &*(gpio::BASE.to_arm_pa().unwrap() as usize as *const gpio::Registers) }
}
pub fn init() {
// Configure the GPIO pin for output
gpio_regs().gpfsel[GPIO_NUM / gpio::GPFSEL::PINS_PER_REGISTER].modify(
gpio::GPFSEL::pin(GPIO_NUM % gpio::GPFSEL::PINS_PER_REGISTER).val(gpio::GPFSEL::OUTPUT),
);
}
pub fn update(new_state: bool) {
if new_state {
gpio_regs().gpset[GPIO_NUM / gpio::GPSET::PINS_PER_REGISTER]
.write(gpio::GPSET::set(GPIO_NUM % gpio::GPSET::PINS_PER_REGISTER));
} else {
gpio_regs().gpclr[GPIO_NUM / gpio::GPCLR::PINS_PER_REGISTER].write(gpio::GPCLR::clear(
GPIO_NUM % gpio::GPCLR::PINS_PER_REGISTER,
));
}
}
}
次に、GitHub上に存在する、BCM2711 SoC向けのperipheral access crateを、このプログラムで使えるようにします。
Cargo.toml の[dependencies]に次の記述を追加してください。
・Cargo.toml
追加すると以下のようになります。
ビルドして実行をすると、LEDが点滅します。
デバッグ機能も先程と同様、普通に動きます。
・ブレークポイント
ブレークしたついでに、RTOS Viewerを見てみましょう。
特にタスクは作っていないので、root_taskがRunningですね。
ここで、二つのプログラムのうち、代表してinit()関数を見比べてみましょう。
その1:
const GPIO_BASE: usize = 0xFE200000;
const GPIO_NUM: usize = 42;
:
:
pub fn init() {
unsafe {
let reg = (GPIO_BASE + (GPIO_NUM / 10) * 4) as *mut u32; // GPFSEL4
let mode = 1; // output
reg.write_volatile(
reg.read_volatile() & !(7 << (GPIO_NUM % 10 * 3)) | (mode << (GPIO_NUM % 10 * 3)),
);
}
}
その2:
const GPIO_NUM: usize = 42;
:
:
pub fn init() {
// Configure the GPIO pin for output
gpio_regs().gpfsel[GPIO_NUM / gpio::GPFSEL::PINS_PER_REGISTER].modify(
gpio::GPFSEL::pin(GPIO_NUM % gpio::GPFSEL::PINS_PER_REGISTER).val(gpio::GPFSEL::OUTPUT),
);
}
その1で作成したinit()関数は、全体的にunsafeで囲われています。
unsafeとは、その名の通り、「安全でない」部分です。
これで囲われている部分は、プログラマが自分自身で安全性を確保する必要があります。
例えば、GPIO_BASEという定数の定義を間違っていると、最悪の場合はリセットになったりします。
そうならないために、正しい値を定義してね、自分でちゃんとしてね、です。
百聞は一見に如かず。
その1のRawなプログラムで、GPIOのアドレスを0番地と定義して、アクセス例外を発生させてみる、という事が簡単にできてしまいます。
これをビルドして実行すると、、、
アクセス例外に飛んでいきました。
って、C/C++だと、その感覚はめっちゃ普通。
一方、その2はunsafeを一か所しか使っていません。
GPIOのレジスタ定義は、PAC(peripheral access crate)であらかじめ定義されているので、アドレス定義のミスが起因での例外発生はありません。(0が一つ足らなかった!とか、ない。)
正しいアドレスが定義されてるされていることは前提です。
プログラマはアドレスが正しいか特に意識する必要ないです。
それに、人によって、命名するレジスタ名っていろいろバリエーションありますよね。
GPIO_BASEだったり、
GPIO_regだったり、
RegGPIOだったり。
プログラマが増えるたびにいろんなレジスタ名が命名されてしまうこともない。
(コーディング規則で、「レジスタ名には _Reg をつけること」として、守ってくれない人にキーキー怒ることもなくなる。)
これって、、、やっぱり、便利じゃない?
Rustは安全な言語、とよく言われますが、具体的に何かどう安全でどう嬉しいのか、を有識者にヒアリングし、その場で思いついたことを羅列してもらいました。
まず、一つ目。
C言語とは違い、デフォルトが「安全側」。
等など。。。
そして、危険な因子を排除するためなのか、規則がいろいろあり、書き方と手続きがCに比べると複雑です。例えば、
等など。。。
これらのことが間違っているとコンパイルエラーや警告になります。
ポインタはただ場所をポイントしてくれる便利なもの、と筆者は思っていたのですが、、、Rustでは所有権、参照、借用等という概念があり、きっちりと守られています。
C++ ではスマートポインタというのがあり、所有権や参照カウンタ等でdeleteのタイミングを自動で判断してくれるので、Cの場合よりは安全性が向上しているのですが、Rustは、さらに上を行くイメージです。
上記のことが間違っているとコンパイルエラーになります。
加えて、先程の事例でも紹介しましたが、できるだけunsafeは使うべきではありません。本当に、もう、どうしようもない時以外は。。。
Rustはプログラマ側で生じる危険因子をできるだけ排除した安全な言語、とはいえ、ハードルが低くはないのも事実です。
・見てきたように言語仕様が堅苦しく自由にできない
→言語仕様としての安全性の裏返し。やっているうちに慣れる。
逆に、コンパイル時にあれだけチェックしてくれていたら、コンパイルが通った暁には期待する動作ができるまでの時間が短縮されるかもしれない。
・Cargoとパッケージ、という概念が慣れない
新言語なのである程度は新しい概念があるのは仕方ない
・.oとソースの関係
コンパイル単位は、コンパイラが決めているようで、どのオブジェクトにそのソースコードが入っているのか、パッと見てわからない。
→そのうち慣れる。
まぁ、やってたらそのうち慣れるもんですよね。
あくまでも筆者の意見ですが、メンテナンス性や安全性にあまりとらわれずに、ささっとPOCを自分一人で作るのであれば、使い慣れたC/C++で、今まで気を付けていたことを忘れないよう注意して、動かせばいいと思います。
しかし、多人数で継続性のあるプロジェクトの場合、自由度を持ったプログラミング言語では、意思疎通の齟齬がどうしてもできてしまいます。
生まれも育ちも、価値観も全く違う人間同士がグループになり、一緒にシステムを作り上げていくことを想像してみると、堅固に規定された同じルール(言語仕様)にのっとって作業をすることが大事になります。
筆者の場合、国際色豊かなチームで開発を行っていた際、コーディング規則を細かく厳しくしていくことによって、それを実現していましたが、正直規則を遵守しているかどうか細かくチェックできず、苦労した経験があります。
Rustの場合は、かなりの部分それが言語仕様として規定され、遵守しないとビルドができない、というチェック機能が働いているのは正直なところ有難いです。
今回はここまでです。
次回はRustの特徴である「メモリ安全性」について、もう少し詳しく調べてみます。