Выбрать главу

   • pub fn into_input<N: InputState>(self, input: N) -> Pin<N>

   • pub fn into_output<N: OutputState>(self, output: N) -> Pin<N>

   • pub fn with_input_state<N: InputState, R>(

&mut self,

input: N,

f: impl FnOnce(&mut PA1<N>) -> R,

) -> R

   • pub fn with_output_state<N: OutputState, R>(

&mut self,

output: N,

f: impl FnOnce(&mut PA1<N>) -> R,

) -> R

Pin state should be bounded by sealed traits. Users of the HAL should have no need to add their own state. The traits can provide HAL-specific methods required to implement the pin state API.

Example:

#![allow(unused)]

fn main() {

use std::marker::PhantomData;

mod sealed {

pub trait Sealed {}

}

pub trait PinState: sealed::Sealed {}

pub trait OutputState: sealed::Sealed {}

pub trait InputState: sealed::Sealed {

// ...

}

pub struct Output<S: OutputState> {

_p: PhantomData<S>,

}

impl<S: OutputState> PinState for Output<S> {}

impl<S: OutputState> sealed::Sealed for Output<S> {}

pub struct PushPull;

pub struct OpenDrain;

impl OutputState for PushPull {}

impl OutputState for OpenDrain {}

impl sealed::Sealed for PushPull {}

impl sealed::Sealed for OpenDrain {}

pub struct Input<S: InputState> {

_p: PhantomData<S>,

}

impl<S: InputState> PinState for Input<S> {}

impl<S: InputState> sealed::Sealed for Input<S> {}

pub struct Floating;

pub struct PullUp;

pub struct PullDown;

impl InputState for Floating {}

impl InputState for PullUp {}

impl InputState for PullDown {}

impl sealed::Sealed for Floating {}

impl sealed::Sealed for PullUp {}

impl sealed::Sealed for PullDown {}

pub struct PA1<S: PinState> {

_p: PhantomData<S>,

}

impl<S: PinState> PA1<S> {

pub fn into_input<N: InputState>(self, input: N) -> PA1<Input<N>> {

todo!()

}

pub fn into_output<N: OutputState>(self, output: N) -> PA1<Output<N>> {

todo!()

}

pub fn with_input_state<N: InputState, R>(

&mut self,

input: N,

f: impl FnOnce(&mut PA1<N>) -> R,

) -> R {

todo!()

}

pub fn with_output_state<N: OutputState, R>(

&mut self,

output: N,

f: impl FnOnce(&mut PA1<N>) -> R,

) -> R {

todo!()

}

}

// Same for `PA` and `Pin`, and other pin types.

}

This chapter collects a variety of tips that might be useful to experienced embedded C developers looking to start writing Rust. It will especially highlight how things you might already be used to in C are different in Rust.

In embedded C it is very common to use the preprocessor for a variety of purposes, such as:

   • Compile-time selection of code blocks with #ifdef

   • Compile-time array sizes and computations

   • Macros to simplify common patterns (to avoid function call overhead)

In Rust there is no preprocessor, and so many of these use cases are addressed differently. In the rest of this section we cover various alternatives to using the preprocessor.

The closest match to #ifdef ... #endif in Rust are Cargo features. These are a little more formal than the C preprocessor: all possible features are explicitly listed per crate, and can only be either on or off. Features are turned on when you list a crate as a dependency, and are additive: if any crate in your dependency tree enables a feature for another crate, that feature will be enabled for all users of that crate.

For example, you might have a crate which provides a library of signal processing primitives. Each one might take some extra time to compile or declare some large table of constants which you'd like to avoid. You could declare a Cargo feature for each component in your Cargo.tomclass="underline"

[features]

FIR = []

IIR = []

Then, in your code, use #[cfg(feature="FIR")] to control what is included.

#![allow(unused)]

fn main() {

/// In your top-level lib.rs

#[cfg(feature="FIR")]

pub mod fir;

#[cfg(feature="IIR")]

pub mod iir;

}

You can similarly include code blocks only if a feature is not enabled, or if any combination of features are or are not enabled.

Additionally, Rust provides a number of automatically-set conditions you can use, such as target_arch to select different code based on architecture. For full details of the conditional compilation support, refer to the conditional compilation chapter of the Rust reference.

The conditional compilation will only apply to the next statement or block. If a block can not be used in the current scope then the cfg attribute will need to be used multiple times. It's worth noting that most of the time it is better to simply include all the code and allow the compiler to remove dead code when optimising: it's simpler for you and your users, and in general the compiler will do a good job of removing unused code.

Rust supports const fn, functions which are guaranteed to be evaluable at compile-time and can therefore be used where constants are required, such as in the size of arrays. This can be used alongside features mentioned above, for example:

#![allow(unused)]