Skip to content

Latest commit

 

History

History
722 lines (550 loc) · 28.2 KB

dma.md

File metadata and controls

722 lines (550 loc) · 28.2 KB

Direct Memory Access (DMA)

このセクションは、DMA転送周りのメモリ安全なAPI構築における主要な要件について、説明します。

DMAペリフェラルは、プロセッサの動作(メインプログラムの実行)と並行してメモリ転送を行うために使用されます。 DMA転送は、memcpyを実行するためにスレッドを生成すること(thread::spawnを参照)とほぼ同等です。 メモリ安全なAPIの要件を説明するために、fork-joinのモデルを使用します。

次のDMAプリミティブを考えます。

{{#include ../ci/dma/src/lib.rs:6:57}}
{{#include ../ci/dma/src/lib.rs:59:60}}

Dma1Channel1は、Serial1というシリアルポート(別名UARTまたはUSART)#1と1ショットモード(つまりサーキュラーモードでない)でやり取りするように、 静的に設定されていると想定して下さい。 Serial1は次のようなブロッキングするAPIを提供します。

{{#include ../ci/dma/src/lib.rs:62:72}}
{{#include ../ci/dma/src/lib.rs:74:80}}
{{#include ../ci/dma/src/lib.rs:82:83}}

例えば、(a)非同期にバッファを送信し、(b)非同期にバッファを埋めるように、Serial1 APIを拡張したいとしましょう。

メモリアンセーフなAPIから出発し、完全にメモリ安全になるまで繰り返し改善していきます。 各ステップで、非同期メモリ操作を扱う際に対処すべき問題を理解するために、 APIがどのように壊れる可能性があるか、を説明します。

最初の挑戦

初心者向けに、Write::write_allを参考に使ってみましょう。 単純化のため、全てのエラー処理を無視します。

{{#include ../ci/dma/examples/one.rs:7:47}}

注記 Transferは、上述のAPIの代わりに、フューチャーやジェネレータベースのAPIとして公開できるでしょう。 それは、API設計の問題で、API全体のメモリ安全性にはほとんど関係がありません。 そのため、このテキストでは、詳しく説明しません。

Read::read_exactの非同期バージョンも実装できます。

{{#include ../ci/dma/examples/one.rs:49:63}}

write_all APIの使い方は次のとおりです。

{{#include ../ci/dma/examples/one.rs:66:71}}

そして、read_exact APIの使用例です。

{{#include ../ci/dma/examples/one.rs:74:86}}

mem::forget

mem::forgetは安全なAPIです。もし私達のAPIが本当に安全なら、 未定義動作を起こさずに両方のAPIを同時に使えるはずです。 しかしながら、そうではありません。次の例を考えます。

{{#include ../ci/dma/examples/one.rs:91:103}}
{{#include ../ci/dma/examples/one.rs:105:112}}

ここで、スタック上に確保された配列を埋めるために、fooからDMA転送を開始します。 そして、戻り値のTransfermem::forgetします。 その後、fooから戻り、bar関数を実行します。

この一連の操作は、未定義動作を引き起こします。DMA転送はスタックメモリに書き込みますが、 そのメモリはfooから戻った時に解放され、barxyのような変数を確保するために再利用されます。 実行時、xyの値は、ランダムなタイミングで書き換わる可能性があります。 DMA転送はbar関数のプロローグによりスタックにプッシュされた状態(例えば、リンクレジスタ)を上書きする可能性もあります。

mem::forgetを使わずに、mem::dropを使うと、TransferのデストラクタはDMA転送を停止し、 プログラムを安全にすることができることに留意して下さい。 しかし、メモリ安全性を強制するためにデストラクタの実行に頼ることはできません。 なぜなら、mem::forgetとメモリリーク(RCサイクルを参照)はRustでは安全だからです。

両APIのバッファのライフタイムを'aから'staticに変更することで、この問題を解決できます。

{{#include ../ci/dma/examples/two.rs:7:12}}
{{#include ../ci/dma/examples/two.rs:21:27}}
{{#include ../ci/dma/examples/two.rs:35:36}}

もし前と同じ問題を再現しようとすると、mem::forgetはもはや問題になりません。

{{#include ../ci/dma/examples/two.rs:40:52}}
{{#include ../ci/dma/examples/two.rs:54:61}}

前回同様、Transferの値をmem::forgetした後も、DMA転送は続いています。 今回は、これは問題になりません。なぜならbufは静的に確保されており(例えば、static mut変数)、 スタック上にないからです。

オーバーラップして使う

私達のAPIは、DMA転送を行っている間、ユーザーがSerialインタフェースを使えてしまいます。 これは、DMA転送が失敗するか、データロスを発生させる可能性があります。

オーバーラップしての利用を防ぐ方法は、いくつかあります。 1つの方法は、TransferSerial1の所有権を取得し、waitが呼ばれた時に所有権を返すことです。

{{#include ../ci/dma/examples/three.rs:7:32}}
{{#include ../ci/dma/examples/three.rs:40:53}}
{{#include ../ci/dma/examples/three.rs:60:68}}

ムーブセマンティクスは、DMA転送を行っている間、Serial1へのアクセスを静的に防ぎます。

{{#include ../ci/dma/examples/three.rs:71:81}}

オーバーラップして利用できないようにする方法が他にもいくつかあります。 例えば、Serial1にDMA転送中かどうかを示す(Cell)フラグを追加できます。 もしフラグがセットされている時は、read, write, read_exactおよびwrite_allは、 実行時にエラー(例えば、Error::InUse)を返します。 このフラグはwrite_all / read_exactが使われた時にセットし、Transfer.waitでクリアします。

コンパイラの(誤った)最適化

コンパイラは、よりプログラムを最適化するため、non-volatileなメモリ操作の順番を入れ替えたり、結合する自由があります。 現在のAPIでは、この自由が未定義動作を引き起こします。 次の例を考えます。

{{#include ../ci/dma/examples/three.rs:84:97}}

ここで、コンパイラは、自由にt.wait()の前にbuf.reverse()を移動することができます。 この移動は、プロセッサとDMAが同時にbufを修正するデータ競合を起こします。 同様に、コンパイラはゼロクリア操作をread_exactの後に移動するかもしれません。 それもデータ競合を起こします。

これらの問題ある順番の入れ替えを起こさないために、compiler_fenceを使えます。

{{#include ../ci/dma/examples/four.rs:9:65}}

volatileな書き込みをするself.dma.start()後ろに先行するメモリ操作が移動されないように、 read_exactwrite_allではOrdering::Releaseを使います。

同様に、volatileな読み込みをするself.is_done()前に後続のメモリ操作が移動されないように、 Transfer.waitではOrdering::Acquireを使います。

フェンスの効果をより理解しやすくするために、前回セクションの例を少し修正したバージョンを示します。 フェンスを追加しており、メモリ操作の順序はコメントで記述しています。

{{#include ../ci/dma/examples/four.rs:68:87}}

Releaseフェンスのおかげで、ゼロクリアする操作は、read_exactより後ろに動かすことができません。 同様に、Acquireフェンスのおかげで、reverse操作はwaitより前に動かすことができません。 両フェンスのにあるメモリ操作は、フェンスを超えて自由に順序を入れ替えることができますが、 bufに関わるような操作はありません。そのため、順序の入れ替えは、未定義動作を起こしません

compiler_fenceは求められているものより少し強いことに注意して下さい。例えば、 このフェンスは、bufxとがオーバーラップしない(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は要件よりも制約が強いです。例えば、 次のプログラムは正しいですが、対応できません

{{#include ../ci/dma/examples/five.rs:67:85}}

このようなプログラムに対応するため、バッファの引数をジェネリックにできます。

{{#include ../ci/dma/examples/five.rs:9:65}}

注記AsSlice<Element = u8> (AsMutSlice<Element = u8)の代わりに、 AsRef<[u8]> (AsMut<[u8]>)を使うことができます。

これで、reuseプログラムに対応できます。

固定バッファ

この修正でAPIは値として配列(例えば、[u8; 16])を受け取れるようになります。 しかし、配列を使用すると、ポインタが不正になる可能性があります。 次のプログラムを考えます。

{{#include ../ci/dma/examples/five.rs:88:103}}
{{#include ../ci/dma/examples/five.rs:105:112}}

read_exact操作は、start関数にあるbufferのアドレスを使います。 このローカルbufferは、startから戻った時に解放され、read_exactで使われているポインタは不正になります。 unsoundの例と似たような状況になるでしょう。

この問題を避けるため、APIで使用するバッファに、ムーブされてもメモリの位置を保ち続けることを要求します。 Pinニュータイプは、このような保証を提供します。 全てのバッファがあらかじめ「pin」されていることを要求するように、APIを更新します。

注記: 以降のプログラムをコンパイルするためには、Rust 1.33.0以上が必要です。 執筆時点(2019-01-04)では、nightlyチャネルの使用を意味します。

{{#include ../ci/dma/examples/six.rs:16:33}}
{{#include ../ci/dma/examples/six.rs:48:59}}
{{#include ../ci/dma/examples/six.rs:74:75}}

注記: Pinニュータイプの代わりにStableDerefトレイトを使うことができますが、 Pinは標準ライブラリで提供されるため、Pinを選びました。

この新しいAPIでは、&'static mut参照、Box化したスライス、Rc化されたスライスなどを使えます。

{{#include ../ci/dma/examples/six.rs:78:89}}
{{#include ../ci/dma/examples/six.rs:91:101}}

'static境界

Pinを使うことで、スタックに割り当てられた配列を安全に使えるのでしょうか? 答えは、ノーです。次の例を考えます。

{{#include ../ci/dma/examples/six.rs:104:123}}
{{#include ../ci/dma/examples/six.rs:125:132}}

これまで何回も見た通り、スタックフレームの破壊により、上記のプログラムは未定義動作に陥ります。

このAPIは、Pin<&'a mut [u8]>(ここで'astaticではありません)の型を持つバッファに対して、 安全ではありません。 この問題を解決するため、どこかに'static境界を追加しなければなりません。

{{#include ../ci/dma/examples/seven.rs:15:25}}
{{#include ../ci/dma/examples/seven.rs:40:51}}
{{#include ../ci/dma/examples/seven.rs:66:67}}

これで問題のプログラムは拒絶されます。

デストラクタ

これでAPIはBoxやデストラクタを持つ型を受け入れることができます。 Transferが早めにドロップされたときに何をすべきか決める必要があります。

通常、Transferの値は、waitメソッドを使って消費されます。しかし、転送が完了する前に、 暗黙的もしくは明示的に、値をdropすることも可能です。 例えば、Transfer<Box<[u8]>>の値をドロップすると、バッファは解放されます。 これは、まだ転送中であれば、DMAが解放済みのメモリに書き込むため、未定義動作を引き起こします。

このような状況では、Transfer.dropでDMA転送を止めることが1つの選択肢です。 他の選択肢は、Transfer.dropが転送完了を待つことです。 より簡単なので、前者を選びます。

{{#include ../ci/dma/examples/eight.rs:18:72}}
{{#include ../ci/dma/examples/eight.rs:82:99}}
{{#include ../ci/dma/examples/eight.rs:109:117}}

これで、バッファが解放される前にDMA転送が中断されます。

{{#include ../ci/dma/examples/eight.rs:120:134}}

まとめ

まとめると、メモリ安全なDMA転送を行うために、これら全てを考えなければなりません。

  • Pin<B>という固定バッファと間接参照を使います。あるいは、StableDerefトレイトを使用できます。
  • B: 'staticというバッファの所有権をDMAに渡す必要があります。
  • メモリ安全性をデストラクタの実行に頼ってはいけません。 APIとmem::forgetが一緒に使われるとどうなるか、考えて下さい。
  • DMA転送を中断するカスタムデストラクタを追加、もしくは、転送完了まで待機、するようにして下さい。 APIとmem::dropが一緒に使われるとどうなるか、考えて下さい。

このテキストでは製品レベルのDMA抽象を構築するために要求される詳細を省略しています。 例えば、DMAチャネルの設定(ストリーム、サーキュラー vs ワンショットモードなど)、バッファのアライメント、 エラー処置、デバイスに依存しない抽象の作り方などについてです。 これらの点は、読者 / コミュニティの演習とします (:P)。