ダイレクトメモリアクセス (DMA)
このセクションでは、DMA 転送を扱うメモリ安全な API を構築するための 中核的な要件を説明します。
DMA ペリフェラルは、プロセッサの処理(メインプログラムの実行)と並行して
メモリ転送を行うために使われます。DMA 転送は、おおむね memcpy を行う
スレッドを生成すること(thread::spawn を参照)に相当します。
ここでは、メモリ安全な API の要件を説明するために、フォーク・ジョインモデルを
用います。
次の DMA プリミティブを考えてみましょう:
#![allow(unused)]
fn main() {
/// A singleton that represents a single DMA channel (channel 1 in this case)
///
/// This singleton has exclusive access to the registers of the DMA channel 1
pub struct Dma1Channel1 {
// ..
}
impl Dma1Channel1 {
/// Data will be written to this `address`
///
/// `inc` indicates whether the address will be incremented after every byte
/// transfer
///
/// NOTE this performs a volatile write
pub fn set_destination_address(&mut self, address: usize, inc: bool) {
// ..
}
/// Data will be read from this `address`
///
/// `inc` indicates whether the address will be incremented after every byte
/// transfer
///
/// NOTE this performs a volatile write
pub fn set_source_address(&mut self, address: usize, inc: bool) {
// ..
}
/// Number of bytes to transfer
///
/// NOTE this performs a volatile write
pub fn set_transfer_length(&mut self, len: usize) {
// ..
}
/// Starts the DMA transfer
///
/// NOTE this performs a volatile write
pub fn start(&mut self) {
// ..
}
/// Stops the DMA transfer
///
/// NOTE this performs a volatile write
pub fn stop(&mut self) {
// ..
}
/// Returns `true` if there's a transfer in progress
///
/// NOTE this performs a volatile read
pub fn in_progress() -> bool {
// ..
false
}
}
}
Dma1Channel1 は、シリアルポート(別名 UART または USART)#1 である
Serial1 と、ワンショットモード(つまり循環モードではない)で動作するよう
静的に設定されているとします。Serial1 は次の ブロッキング API を提供します:
#![allow(unused)]
fn main() {
/// A singleton that represents serial port #1
pub struct Serial1 {
// ..
}
impl Serial1 {
/// Reads out a single byte
///
/// NOTE: blocks if no byte is available to be read
pub fn read(&mut self) -> Result<u8, Error> {
// ..
Ok(0)
}
/// Sends out a single byte
///
/// NOTE: blocks if the output FIFO buffer is full
pub fn write(&mut self, byte: u8) -> Result<(), Error> {
// ..
Ok(())
}
}
}
Serial1 の API を拡張して、(a) バッファを非同期に送信し、(b) バッファを
非同期に埋めるようにしたいとします。
まずはメモリ安全でない API から始めて、完全にメモリ安全になるまで段階的に 改善していきます。各段階で、その API がどのように破られるかを示し、 非同期メモリ操作を扱う際に対処しなければならない問題を明らかにします。
最初の試み
手始めに、Write::write_all API を参考にしてみましょう。単純化のため、
エラーハンドリングはすべて無視します。
#![allow(unused)]
fn main() {
/// A singleton that represents serial port #1
pub struct Serial1 {
// NOTE: we extend this struct by adding the DMA channel singleton
dma: Dma1Channel1,
// ..
}
impl Serial1 {
/// Sends out the given `buffer`
///
/// Returns a value that represents the in-progress DMA transfer
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 }
}
}
/// A DMA transfer
pub struct Transfer<B> {
buffer: B,
}
impl<B> Transfer<B> {
/// Returns `true` if the DMA transfer has finished
pub fn is_done(&self) -> bool {
!Dma1Channel1::in_progress()
}
/// Blocks until the transfer is done and returns the buffer
pub fn wait(self) -> B {
// Busy wait until the transfer is done
while !self.is_done() {}
self.buffer
}
}
}
NOTE:
Transferは、上に示した API の代わりに、future ベースまたは generator ベースの API を公開してもかまいません。これは API 設計の問題であり、 API 全体のメモリ安全性にはほとんど影響しないため、この文書では立ち入りません。
Read::read_exact の非同期版も実装できます。
#![allow(unused)]
fn main() {
impl Serial1 {
/// Receives data into the given `buffer` until it's filled
///
/// Returns a value that represents the in-progress DMA transfer
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) {
// fire and forget
serial.write_all(b"Hello, world!\n");
// do other stuff
}
}
こちらは read_exact API の使用例です:
#![allow(unused)]
fn main() {
fn read(mut serial: Serial1) {
let mut buf = [0; 16];
let t = serial.read_exact(&mut buf);
// do other stuff
t.wait();
match buf.split(|b| *b == b'\n').next() {
Some(b"some-command") => { /* do something */ }
_ => { /* do something else */ }
}
}
}
mem::forget
mem::forget は safe な API です。私たちの API が本当に安全であれば、
この 2 つを組み合わせて使っても未定義動作に陥らないはずです。しかし実際には
そうではありません。次の例を見てください:
#![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];
// start a DMA transfer and forget the returned `Transfer` value
mem::forget(serial.read_exact(&mut buf));
}
#[inline(never)]
fn bar() {
// stack variables
let mut x = 0;
let mut y = 0;
// use `x` and `y`
}
}
ここでは start 内で DMA 転送を開始し、スタック上に割り当てられた配列を
埋めようとした後、返された Transfer 値に対して mem::forget を呼んでいます。
その後 start からリターンし、関数 bar を実行します。
この一連の操作は未定義動作を引き起こします。DMA 転送はスタックメモリへ
書き込みますが、そのメモリは start がリターンすると解放され、その後 bar が
x や y のような変数を割り当てるために再利用されます。実行時には、このために
変数 x と y の値がランダムなタイミングで変化する可能性があります。DMA 転送は、
関数 bar のプロローグによってスタックに積まれた状態(たとえばリンクレジスタ)を
上書きしてしまう可能性もあります。
mem::forget の代わりに mem::drop を使っていれば、Transfer の
デストラクタで DMA 転送を停止させることができ、その場合プログラムは安全に
できたはずです。しかし、メモリ安全性を保証するためにデストラクタが実行される
ことへ 頼ることはできません。なぜなら mem::forget とメモリリーク
(Rc の循環参照を参照)は Rust では safe だからです。
(mem::forget safety を参照してください。)
この特定の問題は、両方の API でバッファのライフタイムを 'a から
'static に変更することで解決できます。
#![allow(unused)]
fn main() {
impl Serial1 {
/// Receives data into the given `buffer` until it's filled
///
/// Returns a value that represents the in-progress DMA transfer
pub fn read_exact(&mut self, buffer: &'static mut [u8]) -> Transfer<&'static mut [u8]> {
// .. same as before ..
}
/// Sends out the given `buffer`
///
/// Returns a value that represents the in-progress DMA transfer
pub fn write_all(mut self, buffer: &'static [u8]) -> Transfer<&'static [u8]> {
// .. same as before ..
}
}
}
前の問題を再現しようとしても、mem::forget がもはや問題を引き起こさない
ことがわかります。
#![allow(unused)]
fn main() {
#[allow(dead_code)]
fn sound(mut serial: Serial1, buf: &'static mut [u8; 16]) {
// NOTE `buf` is moved into `foo`
foo(&mut serial, buf);
bar();
}
#[inline(never)]
fn foo(serial: &mut Serial1, buf: &'static mut [u8]) {
// start a DMA transfer and forget the returned `Transfer` value
mem::forget(serial.read_exact(buf));
}
#[inline(never)]
fn bar() {
// stack variables
let mut x = 0;
let mut y = 0;
// use `x` and `y`
}
}
これまでと同様に、Transfer 値を mem::forget したあとも DMA 転送は
継続します。今回は buf がスタック上ではなく静的に割り当てられている
(たとえば static mut 変数)ため、問題にはなりません。
重複利用
この API では、DMA 転送の進行中にユーザーが Serial インターフェースを
使うことを防げません。これにより、転送が失敗したりデータが失われたりする
可能性があります。
重複利用を防ぐ方法はいくつかあります。1 つの方法は、Transfer が
Serial1 の所有権を受け取り、wait が呼ばれたときにそれを返すようにすること
です。
#![allow(unused)]
fn main() {
/// A DMA transfer
pub struct Transfer<B> {
buffer: B,
// NOTE: added
serial: Serial1,
}
impl<B> Transfer<B> {
/// Blocks until the transfer is done and returns the buffer
// NOTE: the return value has changed
pub fn wait(self) -> (B, Serial1) {
// Busy wait until the transfer is done
while !self.is_done() {}
(self.buffer, self.serial)
}
// ..
}
impl Serial1 {
/// Receives data into the given `buffer` until it's filled
///
/// Returns a value that represents the in-progress DMA transfer
// NOTE we now take `self` by value
pub fn read_exact(mut self, buffer: &'static mut [u8]) -> Transfer<&'static mut [u8]> {
// .. same as before ..
Transfer {
buffer,
// NOTE: added
serial: self,
}
}
/// Sends out the given `buffer`
///
/// Returns a value that represents the in-progress DMA transfer
// NOTE we now take `self` by value
pub fn write_all(mut self, buffer: &'static [u8]) -> Transfer<&'static [u8]> {
// .. same as before ..
Transfer {
buffer,
// NOTE: added
serial: self,
}
}
}
}
ムーブセマンティクスにより、転送の進行中は 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
// .. do stuff ..
let (serial, buf) = t.wait();
// .. do more stuff ..
}
}
重複利用を防ぐ方法はほかにもあります。たとえば、DMA 転送が進行中かどうかを
示す(Cell の)フラグを Serial1 に追加できます。フラグが設定されている場合、
read、write、read_exact、write_all はすべて実行時にエラー
(たとえば Error::InUse)を返すようにできます。このフラグは
write_all / read_exact の使用時に設定され、Transfer.wait でクリアされます。
コンパイラの(誤った)最適化
コンパイラは、プログラムをよりよく最適化するために、非 volatile なメモリ操作を 並べ替えたり統合したりすることが自由にできます。現在の API では、この自由が 未定義動作につながる可能性があります。次の例を考えてみましょう:
#![allow(unused)]
fn main() {
fn reorder(serial: Serial1, buf: &'static mut [u8]) {
// zero the buffer (for no particular reason)
buf.iter_mut().for_each(|byte| *byte = 0);
let t = serial.read_exact(buf);
// ... do other stuff ..
let (buf, serial) = t.wait();
buf.reverse();
// .. do stuff with `buf` ..
}
}
ここでコンパイラは、buf.reverse() を t.wait() より前に移動させることが
自由にできます。そうなるとデータ競合が発生します。つまり、プロセッサと DMA の
両方が同じ時点で buf を変更することになるためです。同様に、コンパイラは
ゼロ埋めの操作を read_exact の後へ移動させることもでき、それもデータ競合を
引き起こします。
このような問題のある並べ替えを防ぐために、compiler_fence を使えます。
#![allow(unused)]
fn main() {
impl Serial1 {
/// Receives data into the given `buffer` until it's filled
///
/// Returns a value that represents the in-progress DMA transfer
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());
// NOTE: added
atomic::compiler_fence(Ordering::Release);
// NOTE: this is a volatile *write*
self.dma.start();
Transfer {
buffer,
serial: self,
}
}
/// Sends out the given `buffer`
///
/// Returns a value that represents the in-progress DMA transfer
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());
// NOTE: added
atomic::compiler_fence(Ordering::Release);
// NOTE: this is a volatile *write*
self.dma.start();
Transfer {
buffer,
serial: self,
}
}
}
impl<B> Transfer<B> {
/// Blocks until the transfer is done and returns the buffer
pub fn wait(self) -> (B, Serial1) {
// NOTE: this is a volatile *read*
while !self.is_done() {}
// NOTE: added
atomic::compiler_fence(Ordering::Acquire);
(self.buffer, self.serial)
}
// ..
}
}
read_exact と write_all では Ordering::Release を使い、それに先行する
すべてのメモリ操作が、volatile write を行う self.dma.start() の 後 に
移動されないようにします。
同様に、Transfer.wait では Ordering::Acquire を使い、それに後続する
すべてのメモリ操作が、volatile read を行う self.is_done() の 前 に
移動されないようにします。
フェンスの効果をよりわかりやすくするために、前のセクションの例を少し調整した 版を以下に示します。コメントにはフェンスとそのオーダリングを追加してあります。
#![allow(unused)]
fn main() {
fn reorder(serial: Serial1, buf: &'static mut [u8], x: &mut u32) {
// zero the buffer (for no particular reason)
buf.iter_mut().for_each(|byte| *byte = 0);
*x += 1;
let t = serial.read_exact(buf); // compiler_fence(Ordering::Release) ▲
// NOTE: the processor can't access `buf` between the fences
// ... do other stuff ..
*x += 2;
let (buf, serial) = t.wait(); // compiler_fence(Ordering::Acquire) ▼
*x += 3;
buf.reverse();
// .. do stuff with `buf` ..
}
}
ゼロ化操作は、Release フェンスがあるため、read_exact の後に 移動することは できません。同様に、reverse 操作は、Acquire フェンスがあるため、wait の前に 移動することは できません。両方のフェンスの 間 にあるメモリ操作は、フェンスをまたいで自由に並べ替えることが できます が、それらの操作はいずれも buf に関与しないため、そのような並べ替えによって未定義動作が生じることは ありません。
なお、compiler_fence は必要とされるものより少し強すぎます。たとえば、buf が x とオーバーラップしないことがわかっていても(Rust のエイリアシング規則による)、これらのフェンスは x に対する操作がマージされるのを防ぎます。しかし、compiler_fence より粒度の細かい intrinsic は存在しません。
メモリバリアは必要ないのでしょうか?
それはターゲットアーキテクチャに依存します。Cortex M0 から M4F までのコアの場合、AN321 には次のようにあります。
3.2 典型的な使用法
(..)
Cortex-M プロセッサでは、メモリトランザクションの並べ替えを行わないため、
DMBの使用が必要になることはまれです。ただし、ソフトウェアを他の ARM プロセッサ、特にマルチマスターシステムで再利用する場合には必要です。たとえば:
- DMA コントローラの設定。CPU のメモリアクセスと DMA 操作の間にはバリアが必要です。
(..)
4.18 マルチマスターシステム
(..)
47 ページの Figure 41 および Figure 42 の例において
DMBまたはDSB命令を省略しても、Cortex-M プロセッサではエラーは発生しません。なぜなら:
- メモリ転送を並べ替えない
- 2 つの書き込み転送が重なることを許可しない
からです。
Figure 41 では、DMA トランザクションを開始する前に DMB(メモリバリア)命令が使われていることが示されています。
Cortex-M7 コアの場合、データキャッシュ(DCache)を使用しているなら、DMA が使用するバッファを手動で無効化しない限り、メモリバリア(DMB/DSB)が必要になります。データキャッシュを無効にしていても、ストアバッファでの並べ替えを避けるために、依然としてメモリバリアが必要になる場合があります。
ターゲットがマルチコアシステムであれば、メモリバリアが必要になる可能性は非常に高いです。
実際にメモリバリアが必要であれば、compiler_fence ではなく atomic::fence を使う必要があります。Cortex-M デバイスでは、これにより DMB 命令が生成されるはずです。
アトミックは必要ないのでしょうか?
フェンスのドキュメントには、フェンスはアトミックと組み合わせた場合にのみ機能すると書かれています。
(少なくとも)
Release順序付けセマンティクスを持つフェンス ‘A’ は、(少なくとも)Acquireセマンティクスを持つフェンス ‘B’ と同期します。ただしそれは、あるアトミックオブジェクト ‘m’ に対して動作する操作 X と Y が存在し、A が X の前に順序付けられ、Y が B の前に順序付けられ、かつ Y が m への変更を観測する場合に限られます。
同じことは compiler_fence にも当てはまります。
fenceと同様に、同期には依然として両方のスレッドでアトミック操作を使う必要があることに注意してください。フェンスと非アトミック操作だけで完全に同期を行うことはできません。
では、別のスレッドではなく DMA エンジンのようなハードウェアとやり取りしている場合、これはどのように機能するのでしょうか?答えは、現行実装では、volatile 操作がたまたま relaxed atomic 操作と同じように動作するということです。将来の Rust のバージョンでこの振る舞いを実際に保証するための作業が進められています。
ジェネリックなバッファ
この API は必要以上に制約が厳しくなっています。たとえば、次のプログラムは有効であるにもかかわらず受け入れられません。
#![allow(unused)]
fn main() {
fn reuse(serial: Serial1, msg: &'static mut [u8]) {
// send a message
let t1 = serial.write_all(msg);
// ..
let (msg, serial) = t1.wait(); // `msg` is now `&'static [u8]`
msg.reverse();
// now send it in 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 {
/// Receives data into the given `buffer` until it's filled
///
/// Returns a value that represents the in-progress DMA transfer
pub fn read_exact<B>(mut self, mut buffer: B) -> Transfer<B>
where
B: AsMutSlice<Element = u8>,
{
// NOTE: added
let slice = buffer.as_mut_slice();
let (ptr, len) = (slice.as_mut_ptr(), slice.len());
self.dma.set_source_address(USART1_RX, false);
// NOTE: tweaked
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,
}
}
/// Sends out the given `buffer`
///
/// Returns a value that represents the in-progress DMA transfer
fn write_all<B>(mut self, buffer: B) -> Transfer<B>
where
B: AsSlice<Element = u8>,
{
// NOTE: added
let slice = buffer.as_slice();
let (ptr, len) = (slice.as_ptr(), slice.len());
self.dma.set_destination_address(USART1_TX, false);
// NOTE: tweaked
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,
}
}
}
}
NOTE:
AsRef<[u8]>(AsMut<[u8]>) をAsSlice<Element = u8>(AsMutSlice<Element = 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]> {
// array allocated in this frame
let buffer = [0; 16];
serial.read_exact(buffer)
}
#[inline(never)]
fn bar() {
// stack variables
let mut x = 0;
let mut y = 0;
// use `x` and `y`
}
}
read_exact 操作は、start 関数のローカル変数 buffer のアドレスを使います。そのローカル変数 buffer は start が返ると解放され、read_exact で使われるポインタは無効になります。結果として、unsound の例に似た状況になります。
この問題を避けるため、私たちの API で使うバッファには、移動されてもメモリ上の位置を保持することを要求します。Pin newtype はそのような保証を提供します。そこで API を更新して、すべてのバッファがまず「ピン留め」されていることを要求できます。
#![allow(unused)]
fn main() {
/// A DMA transfer
pub struct Transfer<B> {
// NOTE: changed
buffer: Pin<B>,
serial: Serial1,
}
impl Serial1 {
/// Receives data into the given `buffer` until it's filled
///
/// Returns a value that represents the in-progress DMA transfer
pub fn read_exact<B>(mut self, mut buffer: Pin<B>) -> Transfer<B>
where
// NOTE: bounds changed
B: DerefMut,
B::Target: AsMutSlice<Element = u8> + Unpin,
{
// .. same as before ..
}
/// Sends out the given `buffer`
///
/// Returns a value that represents the in-progress DMA transfer
pub fn write_all<B>(mut self, buffer: Pin<B>) -> Transfer<B>
where
// NOTE: bounds changed
B: Deref,
B::Target: AsSlice<Element = u8>,
{
// .. same as before ..
}
}
}
NOTE:
Pinnewtype の代わりにStableDerefトレイトを使うこともできましたが、標準ライブラリで提供されているため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 境界
ピン留めすれば、スタックに確保された配列を安全に使えるのでしょうか?答えは いいえ です。次の例を考えてみましょう。
#![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];
// pin the `buffer` to this stack frame
// `buffer` now has type `Pin<&mut [u8; 16]>`
pin_mut!(buffer);
mem::forget(serial.read_exact(buffer));
}
#[inline(never)]
fn bar() {
// stack variables
let mut x = 0;
let mut y = 0;
// use `x` and `y`
}
}
これまで何度も見てきたように、上のプログラムはスタックフレームの破壊により未定義動作に陥ります。
この API は、'a が not 'static である Pin<&'a mut [u8]> 型のバッファに対しては健全ではありません。この問題を防ぐには、いくつかの箇所に 'static 境界を追加する必要があります。
#![allow(unused)]
fn main() {
impl Serial1 {
/// Receives data into the given `buffer` until it's filled
///
/// Returns a value that represents the in-progress DMA transfer
pub fn read_exact<B>(mut self, mut buffer: Pin<B>) -> Transfer<B>
where
// NOTE: added 'static bound
B: DerefMut + 'static,
B::Target: AsMutSlice<Element = u8> + Unpin,
{
// .. same as before ..
}
/// Sends out the given `buffer`
///
/// Returns a value that represents the in-progress DMA transfer
pub fn write_all<B>(mut self, buffer: Pin<B>) -> Transfer<B>
where
// NOTE: added 'static bound
B: Deref + 'static,
B::Target: AsSlice<Element = u8>,
{
// .. same as before ..
}
}
}
これで問題のあるプログラムは拒否されます。
デストラクタ
API が Box などのデストラクタを持つ型を受け入れるようになったので、Transfer が早期に drop されたときにどうするかを決める必要があります。
通常、Transfer 値は wait メソッドを使って消費されますが、転送が終わる前に暗黙的または明示的にその値を drop することも可能です。たとえば、Transfer<Box<[u8]>> 値を drop すると、バッファは解放されます。転送がまだ進行中であれば、DMA が解放済みメモリに書き込むことになってしまうため、これは未定義動作につながる可能性があります。
このような状況では、選択肢の 1 つは Transfer.drop に DMA 転送を停止させることです。もう 1 つの選択肢は、Transfer.drop に転送の完了を待たせることです。ここでは、コストが低いため前者を選びます。
#![allow(unused)]
fn main() {
/// A DMA transfer
pub struct Transfer<B> {
// NOTE: always `Some` variant
inner: Option<Inner<B>>,
}
// NOTE: previously named `Transfer<B>`
struct Inner<B> {
buffer: Pin<B>,
serial: Serial1,
}
impl<B> Transfer<B> {
/// Blocks until the transfer is done and returns the buffer
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() {
// NOTE: this is a volatile write
inner.serial.dma.stop();
// we need a read here to make the Acquire fence effective
// we do *not* need this if `dma.stop` does a RMW operation
unsafe {
ptr::read_volatile(&0);
}
// we need a fence here for the same reason we need one in `Transfer.wait`
atomic::compiler_fence(Ordering::Acquire);
}
}
}
impl Serial1 {
/// Receives data into the given `buffer` until it's filled
///
/// Returns a value that represents the in-progress DMA transfer
pub fn read_exact<B>(mut self, mut buffer: Pin<B>) -> Transfer<B>
where
B: DerefMut + 'static,
B::Target: AsMutSlice<Element = u8> + Unpin,
{
// .. same as before ..
Transfer {
inner: Some(Inner {
buffer,
serial: self,
}),
}
}
/// Sends out the given `buffer`
///
/// Returns a value that represents the in-progress DMA transfer
pub fn write_all<B>(mut self, buffer: Pin<B>) -> Transfer<B>
where
B: Deref + 'static,
B::Target: AsSlice<Element = u8>,
{
// .. same as before ..
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) ▲
// ..
// this stops the DMA transfer and frees memory
mem::drop(t); // compiler_fence(Ordering::Acquire) ▼
// this likely reuses the previous memory allocation
let mut buf = Box::new([0; 16]);
// .. do stuff with `buf` ..
}
}
まとめ
要するに、メモリ安全な DMA 転送を実現するには、次の点をすべて考慮する必要があります:
-
移動不能なバッファと間接参照を使用する:
Pin<B>。あるいは、StableDerefトレイトを使用することもできます。 -
バッファの所有権は DMA に渡さなければなりません:
B: 'static。 -
メモリ安全性について、デストラクタが実行されることに依存してはいけません。
mem::forgetがあなたの API で使われた場合に何が起こるかを考えてください。 -
DMA 転送を停止するか、その完了を待機するカスタムデストラクタを必ず追加してください。
mem::dropがあなたの API で使われた場合に何が起こるかを考えてください。
このテキストでは、本番品質の DMA 抽象化を構築するために必要ないくつかの詳細を省いています。たとえば、DMA チャネルの設定(例: ストリーム、循環モードかワンショットモードか、など)、バッファのアラインメント、エラーハンドリング、抽象化をデバイス非依存にする方法などです。これらの側面はすべて、読者 / コミュニティへの演習課題として残されています(:P)。