#![allow(unused)]fnmain() {
/// 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 1pubstructDma1Channel1 {
// ..
}
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 writepubfnset_destination_address(&mutself, 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 writepubfnset_source_address(&mutself, address: usize, inc: bool) {
// ..
}
/// Number of bytes to transfer////// NOTE this performs a volatile writepubfnset_transfer_length(&mutself, len: usize) {
// ..
}
/// Starts the DMA transfer////// NOTE this performs a volatile writepubfnstart(&mutself) {
// ..
}
/// Stops the DMA transfer////// NOTE this performs a volatile writepubfnstop(&mutself) {
// ..
}
/// Returns `true` if there's a transfer in progress////// NOTE this performs a volatile readpubfnin_progress() -> bool {
// ..
}
}
}
#![allow(unused)]fnmain() {
/// A singleton that represents serial port #1pubstructSerial1 {
// ..
}
impl Serial1 {
/// Reads out a single byte////// NOTE: blocks if no byte is available to be readpubfnread(&mutself) -> Result<u8, Error> {
// ..
}
/// Sends out a single byte////// NOTE: blocks if the output FIFO buffer is fullpubfnwrite(&mutself, byte: u8) -> Result<(), Error> {
// ..
}
}
}
#![allow(unused)]fnmain() {
/// A singleton that represents serial port #1pubstructSerial1 {
// 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 transferpubfnwrite_all<'a>(mutself, buffer: &'a [u8]) -> Transfer<&'a [u8]> {
self.dma.set_destination_address(USART1_TX, false);
self.dma.set_source_address(buffer.as_ptr() asusize, true);
self.dma.set_transfer_length(buffer.len());
self.dma.start();
Transfer { buffer }
}
}
/// A DMA transferpubstructTransfer<B> {
buffer: B,
}
impl<B> Transfer<B> {
/// Returns `true` if the DMA transfer has finishedpubfnis_done(&self) -> bool {
!Dma1Channel1::in_progress()
}
/// Blocks until the transfer is done and returns the bufferpubfnwait(self) -> B {
// Busy wait until the transfer is donewhile !self.is_done() {}
self.buffer
}
}
}
#![allow(unused)]fnmain() {
impl Serial1 {
/// Receives data into the given `buffer` until it's filled////// Returns a value that represents the in-progress DMA transferpubfnread_exact<'a>(&mutself, buffer: &'amut [u8]) -> Transfer<&'amut [u8]> {
self.dma.set_source_address(USART1_RX, false);
self.dma
.set_destination_address(buffer.as_mut_ptr() asusize, true);
self.dma.set_transfer_length(buffer.len());
self.dma.start();
Transfer { buffer }
}
}
}
这里是write_all API的用法:
#![allow(unused)]fnmain() {
fnwrite(serial: Serial1) {
// fire and forget
serial.write_all(b"Hello, world!\n");
// do other stuff
}
}
这是使用read_exact API的一个例子:
#![allow(unused)]fnmain() {
fnread(mut serial: Serial1) {
letmut 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 */ }
}
}
}
#![allow(unused)]fnmain() {
impl Serial1 {
/// Receives data into the given `buffer` until it's filled////// Returns a value that represents the in-progress DMA transferpubfnread_exact(&mutself, buffer: &'staticmut [u8]) -> Transfer<&'staticmut [u8]> {
// .. same as before ..
}
/// Sends out the given `buffer`////// Returns a value that represents the in-progress DMA transferpubfnwrite_all(mutself, buffer: &'static [u8]) -> Transfer<&'static [u8]> {
// .. same as before ..
}
}
}
如果我们尝试复现先前的问题,我们注意到mem::forget不再引起问题了。
#![allow(unused)]fnmain() {
#[allow(dead_code)]fnsound(mut serial: Serial1, buf: &'staticmut [u8; 16]) {
// NOTE `buf` is moved into `foo`
foo(&mut serial, buf);
bar();
}
#[inline(never)]fnfoo(serial: &mut Serial1, buf: &'staticmut [u8]) {
// start a DMA transfer and forget the returned `Transfer` value
mem::forget(serial.read_exact(buf));
}
#[inline(never)]fnbar() {
// stack variablesletmut x = 0;
letmut y = 0;
// use `x` and `y`
}
}
#![allow(unused)]fnmain() {
/// A DMA transferpubstructTransfer<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 changedpubfnwait(self) -> (B, Serial1) {
// Busy wait until the transfer is donewhile !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 valuepubfnread_exact(mutself, buffer: &'staticmut [u8]) -> Transfer<&'staticmut [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 valuepubfnwrite_all(mutself, buffer: &'static [u8]) -> Transfer<&'static [u8]> {
// .. same as before ..
Transfer {
buffer,
// NOTE: added
serial: self,
}
}
}
}
移动语义静态地阻止了当传输在进行时对Serial1的访问。
#![allow(unused)]fnmain() {
fnread(serial: Serial1, buf: &'staticmut [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 ..
}
}
#![allow(unused)]fnmain() {
fnreorder(serial: Serial1, buf: &'staticmut [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` ..
}
}
#![allow(unused)]fnmain() {
impl Serial1 {
/// Receives data into the given `buffer` until it's filled////// Returns a value that represents the in-progress DMA transferpubfnread_exact(mutself, buffer: &'staticmut [u8]) -> Transfer<&'staticmut [u8]> {
self.dma.set_source_address(USART1_RX, false);
self.dma
.set_destination_address(buffer.as_mut_ptr() asusize, 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 transferpubfnwrite_all(mutself, buffer: &'static [u8]) -> Transfer<&'static [u8]> {
self.dma.set_destination_address(USART1_TX, false);
self.dma.set_source_address(buffer.as_ptr() asusize, 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 bufferpubfnwait(self) -> (B, Serial1) {
// NOTE: this is a volatile *read*while !self.is_done() {}
// NOTE: added
atomic::compiler_fence(Ordering::Acquire);
(self.buffer, self.serial)
}
// ..
}
}
#![allow(unused)]fnmain() {
/// A DMA transferpubstructTransfer<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 transferpubfnread_exact<B>(mutself, 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 transferpubfnwrite_all<B>(mutself, buffer: Pin<B>) -> Transfer<B>
where// NOTE: bounds changed
B: Deref,
B::Target: AsSlice<Element = u8>,
{
// .. same as before ..
}
}
}
注意: 我们可以使用 StableDeref 特质而不是 Pin
newtype but opted for Pin since it's provided in the standard library.
With this new API we can use &'static mut references, Box-ed slices, Rc-ed
slices, etc.
#![allow(unused)]fnmain() {
fnstatic_mut(serial: Serial1, buf: &'staticmut [u8]) {
let buf = Pin::new(buf);
let t = serial.read_exact(buf);
// ..let (buf, serial) = t.wait();
// ..
}
fnboxed(serial: Serial1, buf: Box<[u8]>) {
let buf = Pin::new(buf);
let t = serial.read_exact(buf);
// ..let (buf, serial) = t.wait();
// ..
}
}
Does pinning let us safely use stack allocated arrays? The answer is no.
Consider the following example.
#![allow(unused)]fnmain() {
fnunsound(serial: Serial1) {
start(serial);
bar();
}
// pin-utils = "0.1.0-alpha.4"use pin_utils::pin_mut;
#[inline(never)]fnstart(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)]fnbar() {
// stack variablesletmut x = 0;
letmut y = 0;
// use `x` and `y`
}
}
As seen many times before, the above program runs into undefined behavior due to
stack frame corruption.
The API is unsound for buffers of type Pin<&'a mut [u8]> where 'a is not'static. To prevent the problem we have to add a 'static bound in some
places.
#![allow(unused)]fnmain() {
impl Serial1 {
/// Receives data into the given `buffer` until it's filled////// Returns a value that represents the in-progress DMA transferpubfnread_exact<B>(mutself, 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 transferpubfnwrite_all<B>(mutself, buffer: Pin<B>) -> Transfer<B>
where// NOTE: added 'static bound
B: Deref + 'static,
B::Target: AsSlice<Element = u8>,
{
// .. same as before ..
}
}
}
Now that the API accepts Box-es and other types that have destructors we need
to decide what to do when Transfer is early-dropped.
Normally, Transfer values are consumed using the wait method but it's also
possible to, implicitly or explicitly, drop the value before the transfer is
over. For example, dropping a Transfer<Box<[u8]>> value will cause the buffer
to be deallocated. This can result in undefined behavior if the transfer is
still in progress as the DMA would end up writing to deallocated memory.
In such scenario one option is to make Transfer.drop stop the DMA transfer.
The other option is to make Transfer.drop wait for the transfer to finish.
We'll pick the former option as it's cheaper.
#![allow(unused)]fnmain() {
/// A DMA transferpubstructTransfer<B> {
// NOTE: always `Some` variant
inner: Option<Inner<B>>,
}
// NOTE: previously named `Transfer<B>`structInner<B> {
buffer: Pin<B>,
serial: Serial1,
}
impl<B> Transfer<B> {
/// Blocks until the transfer is done and returns the bufferpubfnwait(mutself) -> (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> Dropfor Transfer<B> {
fndrop(&mutself) {
ifletSome(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 operationunsafe {
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 transferpubfnread_exact<B>(mutself, 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 transferpubfnwrite_all<B>(mutself, buffer: Pin<B>) -> Transfer<B>
where
B: Deref + 'static,
B::Target: AsSlice<Element = u8>,
{
// .. same as before ..
Transfer {
inner: Some(Inner {
buffer,
serial: self,
}),
}
}
}
}
Now the DMA transfer will be stopped before the buffer is deallocated.
#![allow(unused)]fnmain() {
fnreuse(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 allocationletmut buf = Box::new([0; 16]);
// .. do stuff with `buf` ..
}
}
To sum it up, we need to consider all the following points to achieve memory
safe DMA transfers:
Use immovable buffers plus indirection: Pin<B>. Alternatively, you can use
the StableDeref trait.
The ownership of the buffer must be passed to the DMA : B: 'static.
Do not rely on destructors running for memory safety. Consider what happens
if mem::forget is used with your API.
Do add a custom destructor that stops the DMA transfer, or waits for it to
finish. Consider what happens if mem::drop is used with your API.
This text leaves out up several details required to build a production grade
DMA abstraction, like configuring the DMA channels (e.g. streams, circular vs
one-shot mode, etc.), alignment of buffers, error handling, how to make the
abstraction device-agnostic, etc. All those aspects are left as an exercise for
the reader / community (:P).
Overlapping use