SOLID未分類 SOLID for Raspberry Pi 4 (連載25)
(2023/5/12)
今回は、組み込み開発でよく使う、「しかるべき場所にしかるべきデータを配置する」をいくつか試してみたいと思います。
良くあるケース:
・UART、BLE、ネットワーク等、通信用パケットを配置する
・構造体を共用体の中に入れて、異なる方法で同じデータをアクセスする
・メモリのアライメントを意識した配置にする
・Volatile
・ビットフィールド
・とあるプログラムのかたまりを、しかるべきアドレスに配置する
これら、試してみました。
ついでに、インラインアセンブラについても試してみました。
以前の記事で試してみた通り、C/C++言語で書く時のように宣言したメンバの順にメモリに配置されるためには、#[repr(C)]アトリビュートが必要でした。
ここではUARTからデータを受信すると仮定し、受信データのパケット仕様は以下としてみます。
パケット内容 | Byte 0 | Byte 1 | Byte 2 | Byte 3 |
---|---|---|---|---|
ヘッダー(固定) | ||||
コマンド(0x00-0xff) | - | - | - | |
Data1(4bytes) | ||||
Data2(2bytes)、Data3(1byte) | - |
構造体はこのようになります。
#[repr(C)]
struct TestPacktRecvData
{
header: u32,
command: u8,
data1: u32,
data2: u16,
data3: u8,
}
インスタンス化して使ってみます。
今回はstaticにしてみます。排他制御が必要そうなコードですが、ややこしくなるので今回は省略で。
static mut PACKETBUF:TestPacktRecvData = TestPacktRecvData{
header:0x11223344, command:0x0a, data1:0x55555555, data2:0x1111, data3:0xFF
};
#[no_mangle]
pub extern "C" fn slo_main() {
println!("Hello world from Rust lib!");
changedata();
showdata();
}
fn changedata(){
unsafe{ PACKETBUF.header = 0x11111111 };
}
fn showdata(){
unsafe{
println!("header={:#X}, command={:#X}, data1={:#X}, data2={:#X}, data3={:#X}"
,PACKETBUF.header, PACKETBUF.command, PACKETBUF.data1, PACKETBUF.data2, PACKETBUF.data3);
}
}
unsafeだらけになってしまいました。
unsafeを付けないと、以下のようなエラーが出ます。
|
22 | PACKETBUF.header = 0x11111111;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ use of mutable static
|
= note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior
排他制御を無視したからですね。
今回はメモリ配置について試してみる回、ですので、unsafeだらけでもまぁ良しとしましょう。
ちなみに、排他制御を付けるにあたっては、以前の記事で書いた記憶があります。
https://note.com/yn_2022/n/nc24f46a9274d#882361d9-38f5-48f3-9db6-beff5047b9e7
複数スレッドからアクセスできる共有変数を付けてみた回、ですね。
では仕切りなおして。
メモリ配置を見てみましょう。
期待通りに割りついています。
ではこの構造体に、受信したと想定する仮データをがさっと入れて、それを構造体経由で読んでみましょう。
大体のケースではDMAで直接がさっと入るのでしょうが、ここはCPUでやってみましょう。
ポインタで場所を把握して、書き込みます。
またまたunsafeですね。
おっと、間違えてデータを上書きしすぎてしまいました。
unsafeなので、こういうことにも普通になります。C/C++と同じです。
悪い見本です。
for i in 0..4に変更し、先に進みます。
動きました。
DebugビルドでもReleaseビルドでも同様でした。
次に、共用体を使ってみます。
先程のパケットを、共用体を使って配列としてもアクセスできるようにしてみます。
use std::mem::ManuallyDrop;
#[repr(C)]
struct TestPacktRecvData
{
header: u32,
command: u8,
data1: u32,
data2: u16,
data3: u8,
}
union TestPacktRecvDataUnion{
packet: ManuallyDrop,
data: [u8; 15],
}
ManuallyDropを付けないとエラーになります。
以下のコードで、
「共用体を初期化 ⇒ すべて0x11に上書き ⇒ 表示」
をしてみます。
#[no_mangle]
pub extern "C" fn slo_main() {
println!("Hello world from Rust lib!");
let mut u = TestPacktRecvDataUnion{ data: [0x00 ; 15] };
unsafe {
for i in 0..15 {
u.data[i] = 0x11;
}
println!("header={:#X}, command={:#X}, data1={:#X}, data2={:#X}, data3={:#X}"
,u.packet.header, u.packet.command, u.packet.data1, u.packet.data2, u.packet.data3);
}
}
ちなみに共用体にアクセスするのはunsafeだそうです。
共用体初期化直後に止めてみます。
共用体のdata[]配列に、forループで値を埋めていった後で止めてみます。
さらに実行。
意図通り動きました。
DebugビルドでもReleaseビルドでも同様でした。
組み込みプログラムでは、しばしば、データブロックをアライメントしたい場合があります。
例えばキャッシュラインを意識して、効率的にデータブロックをキャッシュに載せたい場合。
もしキャッシュラインのサイズが64バイトであれば、データブロックを64バイト境界に配置して、一気にキャッシュフィルしてもらうようにし、プログラムの高速化を図る時があります。
gccであれば以下のような感じになります。
int datablock[8] __attribute((aligned(64));
Rustでは以下のように書けばよいようです。
#[repr(align(64))]
struct BlockData {
data: [u8; 32],
}
もっと大きな、例えばフレームバッファを4kバイト境界に!というのも大丈夫です。
#[repr(align(4096))]
struct FrameData {
data: [u8; 4096],
}
動かしてみます。
以下の4つのデータブロックを定義してみました。
static BLOCKDATABUF:BlockData = BlockData{
data:[1; 32]
};
static BLOCKDATABUF1:BlockData = BlockData{
data:[2; 32]
};
static FLAMEDATABUF:FrameData = FrameData{
data:[3; 4096]
};
static FLAMEDATABUF1:FrameData = FrameData{
data:[4; 4096]
};
意図通りに割ついていました。
メモリ配置、とは異なりますが、組み込みでよく使いますよね。
コンパイラの最適化の時に、必要ないコードだと認識されてコードがなくなってしまう事を防ぐアレです。
Rustでも同じことが可能です。
https://doc.rust-lang.org/std/ptr/fn.read_volatile.html
https://doc.rust-lang.org/std/ptr/fn.write_volatile.html
ただ、やはりunsafeでくくらないといけないようです。
次に、ビットフィールドについて見てみます。
良いサンプルがあります。
https://github.com/KyotoMicrocomputer/solid-rapi4-examples/blob/main/common/bcm2711_pac/src/spi.rs
GPIO、SPIペリフェラルレジスタ操作を行うときに使った、tock_registersクレートをuseしています。
37行目から。
register_bitfields! {u32,
pub CS [
/// Chip select
CS OFFSET(0) NUMBITS(2) [
ChipSelect0 = 0b00,
ChipSelect1 = 0b01,
ChipSelect2 = 0b10,
],
といった感じに、ビットフィールドで書かれています。
書き方のお作法が違えど、C/C++と同じような感じで使えそうです。
ここで作成者の方からコメント頂きました。
tock_registersはペリフェラルレジスタ操作用のもので、一般の構造体では使われることはありません。読み書きがvolatileアクセスで行われるため、最適化の妨げにもなります。
ビットフィールド自体Rustではあまり使われるものではありませんが、定番の方法として以下があります。
- 真偽値フィールドのみを含む場合、bitflags::bitflags! を使う。
- #[bitfield_struct::bitfield] のようなマクロを使う。
- 手動でビット操作を行う。
ところでC/C++のビットフィールドの場合は、並び順などは処理系依存で言語仕様として規定されていません。
Rustの場合はどうでしょうか。
「Rustのビットフィールド機能」はまだ存在しません。#[repr(C)] 属性のある型は「Cと互換」かつ「レイアウトアルゴリズムがコンパイラ実装非依存」であること意味しますが、ビットフィールドは標準的なレイアウトが存在しないというのが理由の一つです。
bitflags::bitflags!, #[bitfield_struct::bitfield], tock_registers::register_bitfields! は全てサードパーティーパッケージが提供するマクロです。これらは整数型をラップする型を定義するため、ネイティブエンディアン表現となります。ビット位置は明確に仕様化されています。
次は、組み込みでは外せない、セクション定義についてです。
この関数は内蔵RAMに割り付けたい、や、ベアメタルであればベクタ関数をしかるべきアドレスから開始しないといけない、このバッファはこのアドレスでないといけない、等、ありますよね。
C/C++と同じような感じで、link_sectionアトリビュート、というのを付けて指定することができます。
https://doc.rust-lang.org/reference/abi.html#the-link_section-attribute
この例では、
#[link_section = ".example_section"]
と指定しています。
当然、".example_section"が、リンカスプクリプトに定義されている必要があります。
最後に、インラインアセンブラの書き方について見てみました。
本記事タイトル「Rustのメモリ配置」からだんだん離れてきました。
以下の方法で記述可能です。
(asm! は1.59.0でstabilizeされました。)
#[cfg(all(
target_arch = "arm",
target_feature = "v7",
target_feature = "thumb-mode",
))]
pub unsafe fn testasm() {
use core::arch::asm;
asm!(
"mov r0, #0",
"isb",
);
}
[注]
ビルドができることを確認しただけのコードです。
r0レジスタを壊すので、実行することはしません。
以上、組み込みでよく使うアレはどうなの?のシリーズでした。
Unsafeのオンパレードになってしまった事が、何かを言い表しているようです。
1章のunsafeは、staticの構造体に対してきちんと排他制御を施せば、unsafeから脱出できます。
2章のunsafeは、そもそも共用体をあまり使わないのかな、という気がしました。
3章、5章、6章はunsafeではありません。
4章と7章のunsafeは、volatileとアセンブラなので仕方がないですね。
unsafeでくくらないといけない、とは言え、ハードウェアに近い低レベルのプログラムを書くための仕組みはそろっています。
unsafe部を下位レイヤーに押し込めれば、その上からはunsafeのない安全なコードを書くことができます。
すなわち、組み込みプログラミングでもRustの恩恵を十分受けることができる、という事がわかりました。
さて、半年にわたり組み込み×Rustの連載をしてきました。
次回は今までの記事を振り返ってみる予定です。