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転送を開始します。 そして、戻り値のTransfermem::forgetします。 その後、fooから戻り、bar関数を実行します。

この一連の操作は、未定義動作を引き起こします。DMA転送はスタックメモリに書き込みますが、 そのメモリはfooから戻った時に解放され、barxyのような変数を確保するために再利用されます。 実行時、xyの値は、ランダムなタイミングで書き換わる可能性があります。 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つの方法は、TransferSerial1の所有権を取得し、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_exactwrite_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は求められているものより少し強いことに注意して下さい。例えば、 このフェンスは、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は要件よりも制約が強いです。例えば、 次のプログラムは正しいですが、対応できません


#![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]>(ここで'astaticではありません)の型を持つバッファに対して、 安全ではありません。 この問題を解決するため、どこかに'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)。