Direct Memory Access (DMA)
このセクションは、DMA転送周りのメモリ安全なAPI構築における主要な要件について、説明します。
DMAペリフェラルは、プロセッサの動作(メインプログラムの実行)と並行してメモリ転送を行うために使用されます。
DMA転送は、memcpy
を実行するためにスレッドを生成すること(thread::spawn
を参照)とほぼ同等です。
メモリ安全なAPIの要件を説明するために、fork-joinのモデルを使用します。
次のDMAプリミティブを考えます。
#![allow(unused)] fn main() { /// 1つのDMAチャネル(ここではチャネル1)を表すシングルトンです /// /// このシングルトンは、DMAチャネル1のレジスタへの排他アクセスを持ちます pub struct Dma1Channel1 { // .. } impl Dma1Channel1 { /// データは`address`に書かれます /// /// `inc`は、各転送の後にアドレスをインクリメントするかどうか、を意味します /// /// 注記 この関数はvolatileな書き込みを行います pub fn set_destination_address(&mut self, address: usize, inc: bool) { // .. } /// データは`address`から読まれます /// /// `inc`は、各転送の後にアドレスをインクリメントするかどうか、を意味します /// /// 注記 この関数はvolatileな書き込みを行います pub fn set_source_address(&mut self, address: usize, inc: bool) { // .. } /// 転送するバイト数です /// /// 注記 この関数はvolatileな書き込みを行います pub fn set_transfer_length(&mut self, len: usize) { // .. } /// DMA転送を開始します /// /// 注記 この関数はvolatileな書き込みを行います pub fn start(&mut self) { // .. } /// DMA転送を停止します /// /// 注記 この関数はvolatileな書き込みを行います pub fn stop(&mut self) { // .. } /// 転送中なら`true`を返します /// /// 注記 この関数はvolatileな読み込みを行います pub fn in_progress() -> bool { // .. } } }
Dma1Channel1
は、Serial1
というシリアルポート(別名UARTまたはUSART)#1と1ショットモード(つまりサーキュラーモードでない)でやり取りするように、
静的に設定されていると想定して下さい。
Serial1
は次のようなブロッキングするAPIを提供します。
#![allow(unused)] fn main() { /// シリアルポート#1を表すシングルトンです pub struct Serial1 { // .. } impl Serial1 { /// 1バイト読み込みます /// /// 注記:読み込めるバイトがないとブロックします pub fn read(&mut self) -> Result<u8, Error> { // .. } /// 1バイト送信します /// /// 注記:出力FIFOバッファに空きがなければブロックします pub fn write(&mut self, byte: u8) -> Result<(), Error> { // .. } } }
例えば、(a)非同期にバッファを送信し、(b)非同期にバッファを埋めるように、Serial1
APIを拡張したいとしましょう。
メモリアンセーフなAPIから出発し、完全にメモリ安全になるまで繰り返し改善していきます。 各ステップで、非同期メモリ操作を扱う際に対処すべき問題を理解するために、 APIがどのように壊れる可能性があるか、を説明します。
最初の挑戦
初心者向けに、Write::write_all
を参考に使ってみましょう。
単純化のため、全てのエラー処理を無視します。
#![allow(unused)] fn main() { /// シリアルポート#1を表すシングルトンです pub struct Serial1 { // 注記:DMAチャネルシングルトンを追加することで、このstructを拡張します dma: Dma1Channel1, // .. } impl Serial1 { /// 与えられた`buffer`を送信します /// /// DMA転送中であることを意味する値を返します pub fn write_all<'a>(mut self, buffer: &'a [u8]) -> Transfer<&'a [u8]> { self.dma.set_destination_address(USART1_TX, false); self.dma.set_source_address(buffer.as_ptr() as usize, true); self.dma.set_transfer_length(buffer.len()); self.dma.start(); Transfer { buffer } } } /// 1回のDMA転送です pub struct Transfer<B> { buffer: B, } impl<B> Transfer<B> { /// DMA転送が完了すると`true`を返します pub fn is_done(&self) -> bool { !Dma1Channel1::in_progress() } /// 転送が完了するまでブロックし、バッファを返します。 pub fn wait(self) -> B { // 転送が完了するまでビジーウェイトします while !self.is_done() {} self.buffer } } }
注記
Transfer
は、上述のAPIの代わりに、フューチャーやジェネレータベースのAPIとして公開できるでしょう。 それは、API設計の問題で、API全体のメモリ安全性にはほとんど関係がありません。 そのため、このテキストでは、詳しく説明しません。
Read::read_exact
の非同期バージョンも実装できます。
#![allow(unused)] fn main() { impl Serial1 { /// 与えられた`buffer`が埋められるまでデータを受信します /// /// DMA転送中であることを意味する値を返します pub fn read_exact<'a>(&mut self, buffer: &'a mut [u8]) -> Transfer<&'a mut [u8]> { self.dma.set_source_address(USART1_RX, false); self.dma .set_destination_address(buffer.as_mut_ptr() as usize, true); self.dma.set_transfer_length(buffer.len()); self.dma.start(); Transfer { buffer } } } }
write_all
APIの使い方は次のとおりです。
#![allow(unused)] fn main() { fn write(serial: Serial1) { // 転送を開始して、忘れます serial.write_all(b"Hello, world!\n"); // 他のことをやります } }
そして、read_exact
APIの使用例です。
#![allow(unused)] fn main() { fn read(mut serial: Serial1) { let mut buf = [0; 16]; let t = serial.read_exact(&mut buf); // 他のことをやります t.wait(); match buf.split(|b| *b == b'\n').next() { Some(b"some-command") => { /* 何かやります */ } _ => { /* 何か他のことをやります */ } } } }
mem::forget
mem::forget
は安全なAPIです。もし私達のAPIが本当に安全なら、
未定義動作を起こさずに両方のAPIを同時に使えるはずです。
しかしながら、そうではありません。次の例を考えます。
#![allow(unused)] fn main() { fn unsound(mut serial: Serial1) { start(&mut serial); bar(); } #[inline(never)] fn start(serial: &mut Serial1) { let mut buf = [0; 16]; // DMA転送を開始し、戻り値の`Transfer`をforgetします mem::forget(serial.read_exact(&mut buf)); } #[inline(never)] fn bar() { // スタック変数です let mut x = 0; let mut y = 0; // `x`と`y`を使います } }
ここで、スタック上に確保された配列を埋めるために、foo
からDMA転送を開始します。
そして、戻り値のTransfer
をmem::forget
します。
その後、foo
から戻り、bar
関数を実行します。
この一連の操作は、未定義動作を引き起こします。DMA転送はスタックメモリに書き込みますが、
そのメモリはfoo
から戻った時に解放され、bar
でx
とy
のような変数を確保するために再利用されます。
実行時、x
とy
の値は、ランダムなタイミングで書き換わる可能性があります。
DMA転送はbar
関数のプロローグによりスタックにプッシュされた状態(例えば、リンクレジスタ)を上書きする可能性もあります。
mem::forget
を使わずに、mem::drop
を使うと、Transfer
のデストラクタはDMA転送を停止し、
プログラムを安全にすることができることに留意して下さい。
しかし、メモリ安全性を強制するためにデストラクタの実行に頼ることはできません。
なぜなら、mem::forget
とメモリリーク(RCサイクルを参照)はRustでは安全だからです。
両APIのバッファのライフタイムを'a
から'static
に変更することで、この問題を解決できます。
#![allow(unused)] fn main() { impl Serial1 { /// 与えられた`buffer`が埋められるまでデータを受信します /// /// DMA転送中であることを意味する値を返します pub fn read_exact(&mut self, buffer: &'static mut [u8]) -> Transfer<&'static mut [u8]> { // .. 以前と同じです .. } /// 与えられた`buffer`を送信します /// /// DMA転送中であることを意味する値を返します pub fn write_all(mut self, buffer: &'static [u8]) -> Transfer<&'static [u8]> { // .. 以前と同じです .. } } }
もし前と同じ問題を再現しようとすると、mem::forget
はもはや問題になりません。
#![allow(unused)] fn main() { #[allow(dead_code)] fn sound(mut serial: Serial1, buf: &'static mut [u8; 16]) { // 注記 `buf`は`foo`にムーブされます foo(&mut serial, buf); bar(); } #[inline(never)] fn foo(serial: &mut Serial1, buf: &'static mut [u8]) { // DMA転送を開始し、戻り値の`Transfer`を忘れます mem::forget(serial.read_exact(buf)); } #[inline(never)] fn bar() { // スタック変数です let mut x = 0; let mut y = 0; // `x`と`y`を使います } }
前回同様、Transfer
の値をmem::forget
した後も、DMA転送は続いています。
今回は、これは問題になりません。なぜならbuf
は静的に確保されており(例えば、static mut
変数)、
スタック上にないからです。
オーバーラップして使う
私達のAPIは、DMA転送を行っている間、ユーザーがSerial
インタフェースを使えてしまいます。
これは、DMA転送が失敗するか、データロスを発生させる可能性があります。
オーバーラップしての利用を防ぐ方法は、いくつかあります。
1つの方法は、Transfer
がSerial1
の所有権を取得し、wait
が呼ばれた時に所有権を返すことです。
#![allow(unused)] fn main() { /// 1回のDMA転送です pub struct Transfer<B> { buffer: B, // 注記:追加しました serial: Serial1, } impl<B> Transfer<B> { /// 転送が完了するまでブロックし、バッファを返します。 /// 注記:戻り値が変わっています pub fn wait(self) -> (B, Serial1) { // 転送が完了するまでビジーウェイトします while !self.is_done() {} (self.buffer, self.serial) } // .. } impl Serial1 { /// 与えられた`buffer`が埋められるまでデータを受信します /// /// DMA転送中であることを意味する値を返します // 注記 今回は、`self`を値として受け取ります pub fn read_exact(mut self, buffer: &'static mut [u8]) -> Transfer<&'static mut [u8]> { // .. 以前と同じです .. Transfer { buffer, // 注記:追加しました serial: self, } } /// 与えられた`buffer`を送信します /// /// DMA転送中であることを意味する値を返します // 注記 今回は、`self`を値として受け取ります pub fn write_all(mut self, buffer: &'static [u8]) -> Transfer<&'static [u8]> { // .. 以前と同じです .. Transfer { buffer, // 注記:追加しました serial: self, } } } }
ムーブセマンティクスは、DMA転送を行っている間、Serial1
へのアクセスを静的に防ぎます。
#![allow(unused)] fn main() { fn read(serial: Serial1, buf: &'static mut [u8; 16]) { let t = serial.read_exact(buf); // let byte = serial.read(); //~ ERROR: `serial` has been moved // .. 何かやります .. let (serial, buf) = t.wait(); // .. さらに何かやります .. } }
オーバーラップして利用できないようにする方法が他にもいくつかあります。
例えば、Serial1
にDMA転送中かどうかを示す(Cell
)フラグを追加できます。
もしフラグがセットされている時は、read
, write
, read_exact
およびwrite_all
は、
実行時にエラー(例えば、Error::InUse
)を返します。
このフラグはwrite_all
/ read_exact
が使われた時にセットし、Transfer.wait
でクリアします。
コンパイラの(誤った)最適化
コンパイラは、よりプログラムを最適化するため、non-volatileなメモリ操作の順番を入れ替えたり、結合する自由があります。 現在のAPIでは、この自由が未定義動作を引き起こします。 次の例を考えます。
#![allow(unused)] fn main() { fn reorder(serial: Serial1, buf: &'static mut [u8]) { // バッファをゼロクリアします(特別な理由はありません) buf.iter_mut().for_each(|byte| *byte = 0); let t = serial.read_exact(buf); // ... 何か別のことをやります .. let (buf, serial) = t.wait(); buf.reverse(); // .. `buf`で何かやります .. } }
ここで、コンパイラは、自由にt.wait()
の前にbuf.reverse()
を移動することができます。
この移動は、プロセッサとDMAが同時にbuf
を修正するデータ競合を起こします。
同様に、コンパイラはゼロクリア操作をread_exact
の後に移動するかもしれません。
それもデータ競合を起こします。
これらの問題ある順番の入れ替えを起こさないために、compiler_fence
を使えます。
#![allow(unused)] fn main() { impl Serial1 { /// 与えられた`buffer`が埋められるまでデータを受信します /// /// DMA転送中であることを意味する値を返します pub fn read_exact(mut self, buffer: &'static mut [u8]) -> Transfer<&'static mut [u8]> { self.dma.set_source_address(USART1_RX, false); self.dma .set_destination_address(buffer.as_mut_ptr() as usize, true); self.dma.set_transfer_length(buffer.len()); // 注記:追加しました atomic::compiler_fence(Ordering::Release); // 注記:これはvolatileな*書き込み*です self.dma.start(); Transfer { buffer, serial: self, } } /// 与えられた`buffer`を送信します /// /// DMA転送中であることを意味する値を返します pub fn write_all(mut self, buffer: &'static [u8]) -> Transfer<&'static [u8]> { self.dma.set_destination_address(USART1_TX, false); self.dma.set_source_address(buffer.as_ptr() as usize, true); self.dma.set_transfer_length(buffer.len()); // 注記:追加しました atomic::compiler_fence(Ordering::Release); // 注記:これはvolatileな*書き込み*です self.dma.start(); Transfer { buffer, serial: self, } } } impl<B> Transfer<B> { /// 転送が完了するまでブロックし、バッファを返します。 pub fn wait(self) -> (B, Serial1) { // 注記: これはvolatileな*読み込み*です while !self.is_done() {} // 注記:追加しました atomic::compiler_fence(Ordering::Acquire); (self.buffer, self.serial) } // .. } }
volatileな書き込みをするself.dma.start()
の後ろに先行するメモリ操作が移動されないように、
read_exact
とwrite_all
ではOrdering::Release
を使います。
同様に、volatileな読み込みをするself.is_done()
の前に後続のメモリ操作が移動されないように、
Transfer.wait
ではOrdering::Acquire
を使います。
フェンスの効果をより理解しやすくするために、前回セクションの例を少し修正したバージョンを示します。 フェンスを追加しており、メモリ操作の順序はコメントで記述しています。
#![allow(unused)] fn main() { fn reorder(serial: Serial1, buf: &'static mut [u8], x: &mut u32) { // バッファをゼロクリアします(特別な理由はありません) buf.iter_mut().for_each(|byte| *byte = 0); *x += 1; let t = serial.read_exact(buf); // compiler_fence(Ordering::Release) ▲ // 注記:プロセッサはフェンスの間、`buf`にアクセスできません // ... 何か別のことをやります .. *x += 2; let (buf, serial) = t.wait(); // compiler_fence(Ordering::Acquire) ▼ *x += 3; buf.reverse(); // .. `buf`で何かやります .. } }
Release
フェンスのおかげで、ゼロクリアする操作は、read_exact
より後ろに動かすことができません。
同様に、Acquire
フェンスのおかげで、reverse
操作はwait
より前に動かすことができません。
両フェンスの間にあるメモリ操作は、フェンスを超えて自由に順序を入れ替えることができますが、
buf
に関わるような操作はありません。そのため、順序の入れ替えは、未定義動作を起こしません。
compiler_fence
は求められているものより少し強いことに注意して下さい。例えば、
このフェンスは、buf
とx
とがオーバーラップしない(Rustのエイリアス規則のため)ことが分かっているにも関わらず、
x
に対する操作が結合されないようにします。しかしながら、
compiler_fence
より細かい粒度のintrinsicは存在していません。
メモリバリアは不要なのですか?
ターゲットアーキテクチャによります。Cortex M0とM4Fコアについて、AN321は次のように言っています。
3.2 一般的な使い方
(..)
DMBの使用はCortex-Mプロセッサではほとんど必要ありません。なぜならCortex-Mプロセッサは メモリトランザクションの順序を変更しないからです。しかし、ソフトウェアが他のARMプロセッサ、 特に複数のマスターがあるシステム、で再利用される場合は必要です。例えば、
- DMAコントローラ設定。バリアは、CPUのメモリアクセスとDMA操作との間で必要です。
(..)
4.18 複数のマスターがあるシステム
(..)
47ページの図41や図42でDMBやDSB命令を除去すると、何らかのエラーが発生します。なぜなら、Cortex-Mプロセッサは
- メモリ転送の順序を入れ替えない
- オーバーラップした2つの書き込み転送を許可しない
ここで、図41は、DMAトランザクションを開始する前に使用されるDMB(メモリバリア)命令を示しています。
Cortex-M7コアの場合、データキャッシュ(DCache)を使っていれば、 DMAで使用されるバッファを手動で無効化しない限り、メモリバリア(DMB/DSB)が必要になります。
もしターゲットがマルチコアシステムの場合、メモリバリアが必要になる可能性が非常に高いです。
もしメモリバリアが必要な場合、compiler_fence
の代わりにatomic::fence
を使わなければなりません。
これは、Cortex-MデバイスではDMB命令を生成するはずです。
ジェネリックバッファ
私達のAPIは要件よりも制約が強いです。例えば、 次のプログラムは正しいですが、対応できません
#![allow(unused)] fn main() { fn reuse(serial: Serial1, msg: &'static mut [u8]) { // メッセージを送信します let t1 = serial.write_all(msg); // .. let (msg, serial) = t1.wait(); // `msg`は現在`&'static [u8]`です msg.reverse(); // 今度は、逆順に送ります let t2 = serial.write_all(msg); // .. let (buf, serial) = t2.wait(); // .. } }
このようなプログラムに対応するため、バッファの引数をジェネリックにできます。
#![allow(unused)] fn main() { // as-slice = "0.1.0" use as_slice::{AsMutSlice, AsSlice}; impl Serial1 { /// 与えられた`buffer`が埋められるまでデータを受信します /// /// DMA転送中であることを意味する値を返します pub fn read_exact<B>(mut self, mut buffer: B) -> Transfer<B> where B: AsMutSlice<Element = u8>, { // 注記:追加しました let slice = buffer.as_mut_slice(); let (ptr, len) = (slice.as_mut_ptr(), slice.len()); self.dma.set_source_address(USART1_RX, false); // 注記:微妙に変更しました self.dma.set_destination_address(ptr as usize, true); self.dma.set_transfer_length(len); atomic::compiler_fence(Ordering::Release); self.dma.start(); Transfer { buffer, serial: self, } } /// 与えられた`buffer`を送信します /// /// DMA転送中であることを意味する値を返します fn write_all<B>(mut self, buffer: B) -> Transfer<B> where B: AsSlice<Element = u8>, { // 注記:追加しました let slice = buffer.as_slice(); let (ptr, len) = (slice.as_ptr(), slice.len()); self.dma.set_destination_address(USART1_TX, false); // 注記:微妙に変更しました self.dma.set_source_address(ptr as usize, true); self.dma.set_transfer_length(len); atomic::compiler_fence(Ordering::Release); self.dma.start(); Transfer { buffer, serial: self, } } } }
注記:
AsSlice<Element = u8>
(AsMutSlice<Element = u8
)の代わりに、AsRef<[u8]>
(AsMut<[u8]>
)を使うことができます。
これで、reuse
プログラムに対応できます。
固定バッファ
この修正でAPIは値として配列(例えば、[u8; 16]
)を受け取れるようになります。
しかし、配列を使用すると、ポインタが不正になる可能性があります。
次のプログラムを考えます。
#![allow(unused)] fn main() { fn invalidate(serial: Serial1) { let t = start(serial); bar(); let (buf, serial) = t.wait(); } #[inline(never)] fn start(serial: Serial1) -> Transfer<[u8; 16]> { // このフレームで確保された配列です let buffer = [0; 16]; serial.read_exact(buffer) } #[inline(never)] fn bar() { // スタック変数です let mut x = 0; let mut y = 0; // `x`と`y`を使います } }
read_exact
操作は、start
関数にあるbuffer
のアドレスを使います。
このローカルbuffer
は、start
から戻った時に解放され、read_exact
で使われているポインタは不正になります。
unsound
の例と似たような状況になるでしょう。
この問題を避けるため、APIで使用するバッファに、ムーブされてもメモリの位置を保ち続けることを要求します。
Pin
ニュータイプは、このような保証を提供します。
全てのバッファがあらかじめ「pin」されていることを要求するように、APIを更新します。
注記: 以降のプログラムをコンパイルするためには、Rust
1.33.0以上
が必要です。 執筆時点(2019-01-04)では、nightlyチャネルの使用を意味します。
#![allow(unused)] fn main() { /// 1回のDMA転送です pub struct Transfer<B> { // 注記:変更しました buffer: Pin<B>, serial: Serial1, } impl Serial1 { /// 与えられた`buffer`が埋められるまでデータを受信します /// /// DMA転送中であることを意味する値を返します pub fn read_exact<B>(mut self, mut buffer: Pin<B>) -> Transfer<B> where // 注記:境界を変更しました B: DerefMut, B::Target: AsMutSlice<Element = u8> + Unpin, { // .. 以前と同じです .. } /// 与えられた`buffer`を送信します /// /// DMA転送中であることを意味する値を返します pub fn write_all<B>(mut self, buffer: Pin<B>) -> Transfer<B> where // 注記:境界を変更しました B: Deref, B::Target: AsSlice<Element = u8>, { // .. 以前と同じです .. } } }
注記:
Pin
ニュータイプの代わりにStableDeref
トレイトを使うことができますが、Pin
は標準ライブラリで提供されるため、Pinを選びました。
この新しいAPIでは、&'static mut
参照、Box
化したスライス、Rc
化されたスライスなどを使えます。
#![allow(unused)] fn main() { fn static_mut(serial: Serial1, buf: &'static mut [u8]) { let buf = Pin::new(buf); let t = serial.read_exact(buf); // .. let (buf, serial) = t.wait(); // .. } fn boxed(serial: Serial1, buf: Box<[u8]>) { let buf = Pin::new(buf); let t = serial.read_exact(buf); // .. let (buf, serial) = t.wait(); // .. } }
'static
境界
Pinを使うことで、スタックに割り当てられた配列を安全に使えるのでしょうか? 答えは、ノーです。次の例を考えます。
#![allow(unused)] fn main() { fn unsound(serial: Serial1) { start(serial); bar(); } // pin-utils = "0.1.0-alpha.4" use pin_utils::pin_mut; #[inline(never)] fn start(serial: Serial1) { let buffer = [0; 16]; // `buffer`をこのスタックフレームにピン留めします // `buffer`は`Pin<&mut [u8; 16]>`の型を持ちます pin_mut!(buffer); mem::forget(serial.read_exact(buffer)); } #[inline(never)] fn bar() { // スタック変数 let mut x = 0; let mut y = 0; // `x`と`y`を使います } }
これまで何回も見た通り、スタックフレームの破壊により、上記のプログラムは未定義動作に陥ります。
このAPIは、Pin<&'a mut [u8]>
(ここで'a
はstatic
ではありません)の型を持つバッファに対して、
安全ではありません。
この問題を解決するため、どこかに'static
境界を追加しなければなりません。
#![allow(unused)] fn main() { impl Serial1 { /// 与えられた`buffer`が埋められるまでデータを受信します /// /// DMA転送中であることを意味する値を返します pub fn read_exact<B>(mut self, mut buffer: Pin<B>) -> Transfer<B> where // 注記:'static境界を追加しました B: DerefMut + 'static, B::Target: AsMutSlice<Element = u8> + Unpin, { // .. 以前と同じです .. } /// 与えられた`buffer`を送信します /// /// DMA転送中であることを意味する値を返します pub fn write_all<B>(mut self, buffer: Pin<B>) -> Transfer<B> where // 注記:'static境界を追加しました B: Deref + 'static, B::Target: AsSlice<Element = u8>, { // .. 以前と同じです .. } } }
これで問題のプログラムは拒絶されます。
デストラクタ
これでAPIはBox
やデストラクタを持つ型を受け入れることができます。
Transfer
が早めにドロップされたときに何をすべきか決める必要があります。
通常、Transfer
の値は、wait
メソッドを使って消費されます。しかし、転送が完了する前に、
暗黙的もしくは明示的に、値をdrop
することも可能です。
例えば、Transfer<Box<[u8]>>
の値をドロップすると、バッファは解放されます。
これは、まだ転送中であれば、DMAが解放済みのメモリに書き込むため、未定義動作を引き起こします。
このような状況では、Transfer.drop
でDMA転送を止めることが1つの選択肢です。
他の選択肢は、Transfer.drop
が転送完了を待つことです。
より簡単なので、前者を選びます。
#![allow(unused)] fn main() { /// 1回のDMA転送です pub struct Transfer<B> { // 注記:常に`Some`ヴァリアントです inner: Option<Inner<B>>, } // 注記:以前は、`Transfer<B>という名前でした struct Inner<B> { buffer: Pin<B>, serial: Serial1, } impl<B> Transfer<B> { /// 転送が完了するまでブロックし、バッファを返します。 pub fn wait(mut self) -> (Pin<B>, Serial1) { while !self.is_done() {} atomic::compiler_fence(Ordering::Acquire); let inner = self .inner .take() .unwrap_or_else(|| unsafe { hint::unreachable_unchecked() }); (inner.buffer, inner.serial) } } impl<B> Drop for Transfer<B> { fn drop(&mut self) { if let Some(inner) = self.inner.as_mut() { // 注記:これはvolatileな書き込みです inner.serial.dma.stop(); // Acquireフェンスを有効化するため、ここで読み込みが必要です // `dma.stop`がRMW操作をするのであれば、これは*不要*です unsafe { ptr::read_volatile(&0); } // `Transfer.wait`と同じ理由でフェンスが必要です。 atomic::compiler_fence(Ordering::Acquire); } } } impl Serial1 { /// 与えられた`buffer`が埋められるまでデータを受信します /// /// DMA転送中であることを意味する値を返します pub fn read_exact<B>(mut self, mut buffer: Pin<B>) -> Transfer<B> where B: DerefMut + 'static, B::Target: AsMutSlice<Element = u8> + Unpin, { // .. 以前と同じです .. Transfer { inner: Some(Inner { buffer, serial: self, }), } } /// 与えられた`buffer`を送信します /// /// DMA転送中であることを意味する値を返します pub fn write_all<B>(mut self, buffer: Pin<B>) -> Transfer<B> where B: Deref + 'static, B::Target: AsSlice<Element = u8>, { // .. 以前と同じです .. Transfer { inner: Some(Inner { buffer, serial: self, }), } } } }
これで、バッファが解放される前にDMA転送が中断されます。
#![allow(unused)] fn main() { fn reuse(serial: Serial1) { let buf = Pin::new(Box::new([0; 16])); let t = serial.read_exact(buf); // compiler_fence(Ordering::Release) ▲ // .. // これはDMA転送を中断し、メモリを解放します mem::drop(t); // compiler_fence(Ordering::Acquire) ▼ // これは、前のメモリ割り当てを再利用する可能性が高いです let mut buf = Box::new([0; 16]); // `buf`で何かやります } }
まとめ
まとめると、メモリ安全なDMA転送を行うために、これら全てを考えなければなりません。
Pin<B>
という固定バッファと間接参照を使います。あるいは、StableDeref
トレイトを使用できます。
B: 'static
というバッファの所有権をDMAに渡す必要があります。
- メモリ安全性をデストラクタの実行に頼ってはいけません。
APIと
mem::forget
が一緒に使われるとどうなるか、考えて下さい。
- DMA転送を中断するカスタムデストラクタを追加、もしくは、転送完了まで待機、するようにして下さい。
APIと
mem::drop
が一緒に使われるとどうなるか、考えて下さい。
このテキストでは製品レベルのDMA抽象を構築するために要求される詳細を省略しています。
例えば、DMAチャネルの設定(ストリーム、サーキュラー vs ワンショットモードなど)、バッファのアライメント、
エラー処置、デバイスに依存しない抽象の作り方などについてです。
これらの点は、読者 / コミュニティの演習とします (:P
)。