So there's a hierarchy of configurations a pin could use:
- Input
- Floating (default)
- PullUp
- PullDown
- Output
- PushPull
- OpenDrain
- AF0 (alternate function 0)
- AF1
- ...
A classical way to model this is using an enum, storing the state somewhere, and checking the state at runtime (or just trusting the programmer):
There are a couple of downsides with this approach:#[derive(PartialEq, Clone, Copy)] pub enum Mode { InputFloating, InputPullUp, Output, // more features left out for brevity } pub struct Pin { mode: Mode, // just for checking } impl Pin { pub fn new() -> Self { Pin { mode: Mode::InputFloating, /* just for checking */ } } pub fn configure(&mut self, mode: Mode) { self.mode = mode; // just for checking // configure the hardware match mode { Mode::InputFloating => {} Mode::InputPullUp => {} Mode::Output => {} } } pub fn read(&self) -> bool { assert!(self.mode != Mode::Output); // just for checking false // bit is read from hardware } pub fn write(&self, value: bool) { assert!(self.mode == Mode::Output); // just for checking // bit is written to hardware } }
- If we want checks, the value is stored twice: once in the struct and once in a register of the microcontroller
- If we want checks, we also slow down the actual program.
- If we trust the programmer instead, we can remove all the "just for checking" lines/items, but now we're trusting the programmer.
- Even with checks, we only see errors at runtime, and there could be lots of code paths that lead to writing or reading a pin.
Here, every configuration has its own struct type, and we use into_... methods to configure the pin and convert between the associated struct type. The struct definitions with (()) at the end look a little funny, that's for visibility reasons: the () is a 0-byte-sized private field that prevents the struct from being constructed outside the declaring module. Clients have to go through InputFloatingPin::new(), meaning they have to start with the reset configuration.pub struct InputFloatingPin(()); pub struct InputPullUpPin(()); pub struct OutputPin(()); impl InputFloatingPin { pub fn new() -> Self { InputFloatingPin(()) } pub fn into_input_pull_up(self) -> InputPullUpPin { // configure the hardware InputPullUpPin(()) } pub fn into_output(self) -> OutputPin { // configure the hardware OutputPin(()) } pub fn read(&self) -> bool { false // bit is read from hardware } } impl InputPullUpPin { // pub fn into_... pub fn read(&self) -> bool { false // bit is read from hardware } } impl OutputPin { // pub fn into_... pub fn write(&self, value: bool) { // bit is written to hardware } }
So we have a bunch of structs with no values in them, which seems kind of stupid. But as noted above, the configuration is in hardware anyway. Also, creating structs that don't need memory is not only cheap, it has no cost at all! We get all the checks from above, but at compile time without slowing the program. We can even use these types to specify that a function requires a pin in a certain configuration.
There is a downside however: code duplication. There's the into_... methods I left out, and there's the read method that's written twice.
So let's fix that with generics. The idea is to define a struct Pin<MODE>; all the into_... methods would be defined on Pin<_>, thus available in any configuration. Pin<Output> is trivial, but crucially, we model the proper configuration hierarchy for inputs: read is implemented for Pin<Input<_>>! So no matter what kind of input we have, we can read from it:
use std::marker::PhantomData; pub struct Pin<MODE>(PhantomData<MODE>); pub struct Input<MODE>(PhantomData<MODE>); pub struct Floating(()); pub struct PullUp(()); pub struct Output(()); impl Pin<Input<Floating>> { pub fn new() -> Self { Pin(PhantomData) } } impl<MODE> Pin<MODE> { pub fn into_input_floating(self) -> Pin<Input<Floating>> { // configure the hardware Pin(PhantomData) } pub fn into_input_pull_up(self) -> Pin<Input<PullUp>> { // configure the hardware Pin(PhantomData) } pub fn into_output(self) -> Pin<Output> { // configure the hardware Pin(PhantomData) } } impl<MODE> Pin<Input<MODE>> { pub fn read(&self) -> bool { false // bit is read from hardware } } impl Pin<Output> { pub fn write(&self, value: bool) { // bit is written to hardware } }
No comments:
Post a Comment