diff --git a/src/typestate-programming/design-contracts.md b/src/typestate-programming/design-contracts.md index e6f7bbf..d699a89 100644 --- a/src/typestate-programming/design-contracts.md +++ b/src/typestate-programming/design-contracts.md @@ -1,48 +1,254 @@ # Design Contracts -In +In our last chapter, we wrote an interface that *didn't* enforce design contracts. Let's take another look at our imaginary GPIO configuration register: + +| Name | Bit Number(s) | Value | Meaning | Notes | +| ---: | ------------: | ----: | ------: | ----: | +| enable | 0 | 0 | disabled | Disables the GPIO | +| | | 1 | enabled | Enables the GPIO | +| direction | 1 | 0 | input | Sets the direction to Input | +| | | 1 | output | Sets the direction to Output | +| input_mode | 2..3 | 00 | hi-z | Sets the input as high resistance | +| | | 01 | pull-low | Input pin is pulled low | +| | | 10 | pull-high | Input pin is pulled high | +| | | 11 | n/a | Invalid state. Do not set | +| output_mode | 4 | 0 | set-low | Output pin is driven low | +| | | 1 | set-high | Output pin is driven high | +| input_status | 5 | x | in-val | 0 if input is < 1.5v, 1 if input >= 1.5v | + +If we instead checked the state before making use of the underlying hardware, enforcing our design contracts at runtime, we might write code that looks like this instead: ```rust -use gpio::{InputGpio, OutputGpio}; +/// GPIO interface +struct GpioConfig { + /// GPIO Configuration structure generated by svd2rust + periph: GPIO_CONFIG, +} -struct GpioPin; - -impl GpioPin { - fn into_input(self) -> InputGpio { - self.set_input_mode(); - InputGpio +impl Gpio { + pub fn set_enable(&mut self, is_enabled: bool) { + self.periph.modify(|_r, w| { + w.enable().set_bit(is_enabled) + }); } - fn into_output(self) -> OutputGpio { - self.set_output_mode(); - OutputGpio + + pub fn set_direction(&mut self, is_output: bool) -> Result<(), ()> { + if self.periph.read().enable().bit_is_clear() { + // Must be enabled to set direction + return Err(()); + } + + self.periph.modify(|r, w| { + w.direction().set_bit(is_output) + }); + + Ok(()) + } + + pub fn set_input_mode(&mut self, variant: InputMode) -> Result<(), ()> { + if self.periph.read().enable().bit_is_clear() { + // Must be enabled to set input mode + return Err(()); + } + + if self.periph.read().direction().bit_is_set() { + // Direction must be input + return Err(()); + } + + self.periph.modify(|_r, w| { + w.input_mode().variant(variant) + }); + + Ok(()) + } + + pub fn set_output_status(&mut self, is_high: bool) -> Result<(), ()> { + if self.periph.read().enable().bit_is_clear() { + // Must be enabled to set output status + return Err(()); + } + + if self.periph.read().direction().bit_is_clear() { + // Direction must be output + return Err(()); + } + + self.periph.modify(|_r, w| { + w.output_mode.set_bit(is_high) + }); + + Ok(()) + } + + pub fn get_input_status(&self) -> Result { + if self.periph.read().enable().bit_is_clear() { + // Must be enabled to get status + return Err(()); + } + + if self.periph.read().direction().bit_is_set() { + // Direction must be input + return Err(()); + } + + Ok(self.periph.read().input_status().bit_is_set()) } } ``` -* Use type transitions to enforce setup steps -* Like the builder pattern in "normal" Rust +Because we need to enforce the restrictions on the hardware, we end up doing a lot of runtime checking whch wastes time and resources, and this code will be much less pleasant for the developer to use. + +## Type States + +But what if instead, we used Rust's type system to enforce the state transition rules? Take this example: ```rust -impl LedPin { - fn new(pin: OutputGpio) -> Self { ... } - fn toggle(&mut self) -> bool { ... } +/// GPIO interface +struct GpioConfig { + /// GPIO Configuration structure generated by svd2rust + periph: GPIO_CONFIG, + enabled: ENABLED, + direction: DIRECTION, + mode: MODE, } -fn main() { - let gpio_1 = unsafe { PERIPHERALS.take_gpio_1() }; - // This won't work, the types are wrong! - // let led_1 = LedPin::new(gpio_1); - let mut led_1 = LedPin::new(gpio_1.into_output()); - let _ = led_1.toggle(); +// Type states for MODE in GpioConfig +struct Disabled; +struct Enabled; +struct Output; +struct Input; +struct PulledLow; +struct PulledHigh; +struct HighZ; +struct DontCare; + +/// These functions may be used on any GPIO Pin +impl GpioConfig { + pub fn into_disabled(self) -> GpioConfig { + self.periph.modify(|_r, w| w.enable.disabled()); + GpioConfig { + periph: self.periph, + enabled: Disabled, + direction: DontCare, + mode: DontCare, + } + } + + pub fn into_enabled_input(self) -> GpioConfig { + self.periph.modify(|_r, w| { + w.enable.enabled() + .direction.input() + .input_mode.high_z() + }); + GpioConfig { + periph: self.periph, + enabled: Enabled, + direction: Input, + mode: HighZ, + } + } + + pub fn into_enabled_output(self) -> GpioConfig { + self.periph.modify(|_r, w| { + w.enable.enabled() + .direction.output() + .input_mode.set_high() + }); + GpioConfig { + periph: self.periph, + enabled: Enabled, + direction: Output, + mode: DontCare, + } + } +} + +/// This function may be used on an Output Pin +impl GpioConfig { + pub fn set_bit(&mut self, set_high: bool) { + self.periph.modify(|_r, w| w.output_mode.set_bit(set_high)); + } +} + +/// These methods may be used on any enabled input GPIO +impl GpioConfig { + pub fn bit_is_set(&self) -> bool { + self.periph.read().input_status.bit_is_set() + } + + pub fn into_input_high_z(self) -> GpioConfig { + self.periph.modify(|_r, w| w.input_mode().high_z()); + GpioConfig { + periph: self.periph, + enabled: Enabled, + direction: Input, + mode: HighZ, + } + } + + pub fn into_input_pull_down(self) -> GpioConfig { + self.periph.modify(|_r, w| w.input_mode().pull_low()); + GpioConfig { + periph: self.periph, + enabled: Enabled, + direction: Input, + mode: PulledLow, + } + } + + pub fn into_input_pull_up(self) -> GpioConfig { + self.periph.modify(|_r, w| w.input_mode().pull_high()); + GpioConfig { + periph: self.periph, + enabled: Enabled, + direction: Input, + mode: PulledHigh, + } + } } ``` -* You have to have the right types to have the interfaces you want +Now let's see what the code using this would look like: -## Enforce design contracts +```rust +/* + * Example 1: Unconfigured to High-Z input + */ +let pin: GpioConfig = get_gpio(); -> entirely at compile time -> -> no runtime cost -> -> no room for human error +// Can't do this, pin isn't enabled! +// pin.into_input_pull_down(); + +// Now turn the pin from unconfigured to a high-z input +let input_pin = pin.into_enabled_input(); + +// Read from the pin +let pin_state = input_pin.bit_is_set(); + +// Can't do this, input pins don't have this interface! +// pin_state.set_bit(true); + +/* + * Example 2: High-Z input to Pulled Low input + */ +let pulled_low = input_pin.into_input_pull_down(); +let pin_state = pulled_low.bit_is_set(); + +/* + * Example 3: Pulled Low input to Output, set high + */ +let output_pin = pulled_low.into_enabled_output(); +output_pin.set_bit(false); + +// Can't do this, output pins don't have this interface! +// output_pin.into_input_pull_down(); +``` + +This is defintely a convenient way to store the state of the pin, but why do it this way? Why is this better than storing the state as an `enum` inside of our `GpioConfig` structure? + +## Compile Time Functional Safety + +Because we are enforcing our design constrains entirely at compile time, this incurs no runtime cost. It is impossible to set an output mode when you have a pin in an input mode. Instead, you must walk through the states by converting it to an output pin, and then setting the output mode. Because of this, there is no runtime penalty due to checking the current state before executing a function. + +Also, because these states are enforced by the type system, there is no longer room for errors by consumers of this interface. If they try to perform an illegal state transition, the code will not compile! diff --git a/src/typestate-programming/state-machines.md b/src/typestate-programming/state-machines.md index f7c8f81..1b2fce4 100644 --- a/src/typestate-programming/state-machines.md +++ b/src/typestate-programming/state-machines.md @@ -1,5 +1,98 @@ # Peripherals as State Machines -The peripherals of a microcontroller can be thought of as set of state machines. For example, a simplified [GPIO pin] +The peripherals of a microcontroller can be thought of as set of state machines. For example, the configuration of a simplified [GPIO pin] could be represented as the following tree of states: -[GPIO pin]: https://en.wikipedia.org/wiki/General-purpose_input/output \ No newline at end of file +[GPIO pin]: https://en.wikipedia.org/wiki/General-purpose_input/output + +* Disabled +* Enabled + * Configured as Output + * Output: High + * Output: Low + * Configured as Input + * Input: High Resistance + * Input: Pulled Low + * Input: Pulled High + +If the peripheral starts in the `Disabled` mode, to move to the `Input: High Resistance` mode, we must perform the following steps: + +1. Disabled +2. Enabled +3. Configured as Input +4. Input: High Resistance + +If we wanted to move from `Input: High Resistance` to `Input: Pulled Low`, we must perform the following steps: + +1. Input: High Resistance +2. Input: Pulled Low + +Similarly, if we want to move a GPIO pin from configured as `Input: Pulled Low` to `Output: High`, we must perform the following steps: + +1. Input: Pulled Low +2. Configured as Input +3. Configured as Output +4. Output: High + +## Hardware Representation + +Typically the states listed above are set by writing values to given registers mapped to a GPIO peripheral. Let's define an imaginary GPIO Configuration Register to illustrate this: + +| Name | Bit Number(s) | Value | Meaning | Notes | +| ---: | ------------: | ----: | ------: | ----: | +| enable | 0 | 0 | disabled | Disables the GPIO | +| | | 1 | enabled | Enables the GPIO | +| direction | 1 | 0 | input | Sets the direction to Input | +| | | 1 | output | Sets the direction to Output | +| input_mode | 2..3 | 00 | hi-z | Sets the input as high resistance | +| | | 01 | pull-low | Input pin is pulled low | +| | | 10 | pull-high | Input pin is pulled high | +| | | 11 | n/a | Invalid state. Do not set | +| output_mode | 4 | 0 | set-low | Output pin is driven low | +| | | 1 | set-high | Output pin is driven high | +| input_status | 5 | x | in-val | 0 if input is < 1.5v, 1 if input >= 1.5v | + +We could simple expose the following structure in Rust to control this GPIO: + +```rust +/// GPIO interface +struct GpioConfig { + /// GPIO Configuration structure generated by svd2rust + periph: GPIO_CONFIG, +} + +impl Gpio { + pub fn set_enable(&mut self, is_enabled: bool) { + self.periph.modify(|_r, w| { + w.enable().set_bit(is_enabled) + }); + } + + pub fn set_direction(&mut self, is_output: bool) { + self.periph.modify(|r, w| { + w.direction().set_bit(is_output) + }); + } + + pub fn set_input_mode(&mut self, variant: InputMode) { + self.periph.modify(|_r, w| { + w.input_mode().variant(variant) + }); + } + + pub fn set_output_status(&mut self, is_high: bool) { + self.periph.modify(|_r, w| { + w.output_mode.set_bit(is_high) + }); + } + + pub fn get_input_status(&self) -> bool { + self.periph.read().input_status().bit_is_set() + } +} +``` + +However, this could allow us to modify certain registers that do not make sense. For example, what happens if we set the `output_mode` field when our GPIO is configured as an input? For some hardware, ths may not matter, but on some hardware, it could cause unexpected or undefined behavior. + +This would allow us to reach states not defined by our state machine above: An output that is pulled low, or an input that was set high! + +Although this interface is convenient to write, it doesn't enforce the design contracts set out by our hardware implementation. \ No newline at end of file diff --git a/src/typestate-programming/zero-cost-abstractions.md b/src/typestate-programming/zero-cost-abstractions.md index 89c142e..e1d3cf2 100644 --- a/src/typestate-programming/zero-cost-abstractions.md +++ b/src/typestate-programming/zero-cost-abstractions.md @@ -1,78 +1,42 @@ # Zero Cost Abstractions -## "no runtime cost"? +Type states are also an excellent example of Zero Cost Abstractions - the ability to move certain behaviors to compile time execution or analysis. These type states contain no actual data, and are instead used as markers. Since they contain no data, they have no actual representation in memory at runtime: ```rust use core::mem::size_of; -let _ = size_of::(); // == 0 -let _ = size_of::(); // == 0 -let _ = size_of::(); // == 0 -let _ = size_of::<()>(); // == 0 +let _ = size_of::(); // == 0 +let _ = size_of::(); // == 0 +let _ = size_of::(); // == 0 +let _ = size_of::>(); // == 0 ``` ## Zero Sized Types ```rust -struct GpioPin; +struct Enabled; ``` -> acts real at compile time -> -> doesn't exist in the binary -> -> no RAM, no CPU, no space -> -> Evaporates at compile time +Structures defined like this are called Zero Sized Types, as they contain no actual data. Although these types act "real" at compile time - you can copy them, move them, take references to them, etc., however the optimizer will completely strip them away. -## What if our `OutputGpio` has multiple modes? - -> (it does) - ---- +In this snippet of code: ```rust -pub struct PushPull; // good for general usage -pub struct OpenDrain; // used when multiple devices could drive a bus - -pub struct OutputGpio { - _mode: MODE -} - -impl OutputGpio { - fn default() -> OutputGpio { ... } - fn into_push_pull(self) -> OutputGpio { ... } - fn into_open_drain(self) -> OutputGpio { ... } +pub fn into_input_high_z(self) -> GpioConfig { + self.periph.modify(|_r, w| w.input_mode().high_z()); + GpioConfig { + periph: self.periph, + enabled: Enabled, + direction: Input, + mode: HighZ, + } } ``` ---- +The GpioConfig we return never exists at runtime. Calling this function will generally boil down to a single assembly instruction - storing a constant register value to a register location. This means that the type state interface we've developed is a zero cost abstraction - it uses no more CPU, RAM, or code space tracking the state of `GpioConfig`, and renders to the same machine code as a direct register access. -```rust -/// This kind of LED only works with OpenDrain settings -struct DrainLed { - pin: OutputGpio, -} +## Nesting -impl DrainLed { - fn new(pin: OutputGpio) -> Self { ... } - fn toggle(&self) -> bool { ... } -} -``` +In general, these abstractions may be nested as deeply as you would like. As long as all components used are zero sized types, the whole structure will not exist at runtime. ---- - -```rust -/// This kind of LED works with any output -struct LedDriver { - pin: OutputGpio, -} - -/// Generically support any OutputGpio variant! -impl LedDriver { - fn new(pin: OutputGpio) -> LedDriver { ... } - fn toggle(&self) -> bool { ... } -} -``` - -* Nested zero sized types are still zero sized, no matter how deep you nest them \ No newline at end of file +For complex or deeply nested structures, it may be tedious to define all possible combinations of state. In these cases, macros may be used to generate all implementations.