1# Design Contracts 2 3In our last chapter, we wrote an interface that *didn't* enforce design contracts. Let's take another look at our imaginary GPIO configuration register: 4 5| Name | Bit Number(s) | Value | Meaning | Notes | 6| ---: | ------------: | ----: | ------: | ----: | 7| enable | 0 | 0 | disabled | Disables the GPIO | 8| | | 1 | enabled | Enables the GPIO | 9| direction | 1 | 0 | input | Sets the direction to Input | 10| | | 1 | output | Sets the direction to Output | 11| input_mode | 2..3 | 00 | hi-z | Sets the input as high resistance | 12| | | 01 | pull-low | Input pin is pulled low | 13| | | 10 | pull-high | Input pin is pulled high | 14| | | 11 | n/a | Invalid state. Do not set | 15| output_mode | 4 | 0 | set-low | Output pin is driven low | 16| | | 1 | set-high | Output pin is driven high | 17| input_status | 5 | x | in-val | 0 if input is < 1.5v, 1 if input >= 1.5v | 18 19If 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: 20 21```rust,ignore 22/// GPIO interface 23struct GpioConfig { 24 /// GPIO Configuration structure generated by svd2rust 25 periph: GPIO_CONFIG, 26} 27 28impl GpioConfig { 29 pub fn set_enable(&mut self, is_enabled: bool) { 30 self.periph.modify(|_r, w| { 31 w.enable().set_bit(is_enabled) 32 }); 33 } 34 35 pub fn set_direction(&mut self, is_output: bool) -> Result<(), ()> { 36 if self.periph.read().enable().bit_is_clear() { 37 // Must be enabled to set direction 38 return Err(()); 39 } 40 41 self.periph.modify(|r, w| { 42 w.direction().set_bit(is_output) 43 }); 44 45 Ok(()) 46 } 47 48 pub fn set_input_mode(&mut self, variant: InputMode) -> Result<(), ()> { 49 if self.periph.read().enable().bit_is_clear() { 50 // Must be enabled to set input mode 51 return Err(()); 52 } 53 54 if self.periph.read().direction().bit_is_set() { 55 // Direction must be input 56 return Err(()); 57 } 58 59 self.periph.modify(|_r, w| { 60 w.input_mode().variant(variant) 61 }); 62 63 Ok(()) 64 } 65 66 pub fn set_output_status(&mut self, is_high: bool) -> Result<(), ()> { 67 if self.periph.read().enable().bit_is_clear() { 68 // Must be enabled to set output status 69 return Err(()); 70 } 71 72 if self.periph.read().direction().bit_is_clear() { 73 // Direction must be output 74 return Err(()); 75 } 76 77 self.periph.modify(|_r, w| { 78 w.output_mode.set_bit(is_high) 79 }); 80 81 Ok(()) 82 } 83 84 pub fn get_input_status(&self) -> Result<bool, ()> { 85 if self.periph.read().enable().bit_is_clear() { 86 // Must be enabled to get status 87 return Err(()); 88 } 89 90 if self.periph.read().direction().bit_is_set() { 91 // Direction must be input 92 return Err(()); 93 } 94 95 Ok(self.periph.read().input_status().bit_is_set()) 96 } 97} 98``` 99 100Because we need to enforce the restrictions on the hardware, we end up doing a lot of runtime checking which wastes time and resources, and this code will be much less pleasant for the developer to use. 101 102## Type States 103 104But what if instead, we used Rust's type system to enforce the state transition rules? Take this example: 105 106```rust,ignore 107/// GPIO interface 108struct GpioConfig<ENABLED, DIRECTION, MODE> { 109 /// GPIO Configuration structure generated by svd2rust 110 periph: GPIO_CONFIG, 111 enabled: ENABLED, 112 direction: DIRECTION, 113 mode: MODE, 114} 115 116// Type states for MODE in GpioConfig 117struct Disabled; 118struct Enabled; 119struct Output; 120struct Input; 121struct PulledLow; 122struct PulledHigh; 123struct HighZ; 124struct DontCare; 125 126/// These functions may be used on any GPIO Pin 127impl<EN, DIR, IN_MODE> GpioConfig<EN, DIR, IN_MODE> { 128 pub fn into_disabled(self) -> GpioConfig<Disabled, DontCare, DontCare> { 129 self.periph.modify(|_r, w| w.enable.disabled()); 130 GpioConfig { 131 periph: self.periph, 132 enabled: Disabled, 133 direction: DontCare, 134 mode: DontCare, 135 } 136 } 137 138 pub fn into_enabled_input(self) -> GpioConfig<Enabled, Input, HighZ> { 139 self.periph.modify(|_r, w| { 140 w.enable.enabled() 141 .direction.input() 142 .input_mode.high_z() 143 }); 144 GpioConfig { 145 periph: self.periph, 146 enabled: Enabled, 147 direction: Input, 148 mode: HighZ, 149 } 150 } 151 152 pub fn into_enabled_output(self) -> GpioConfig<Enabled, Output, DontCare> { 153 self.periph.modify(|_r, w| { 154 w.enable.enabled() 155 .direction.output() 156 .input_mode.set_high() 157 }); 158 GpioConfig { 159 periph: self.periph, 160 enabled: Enabled, 161 direction: Output, 162 mode: DontCare, 163 } 164 } 165} 166 167/// This function may be used on an Output Pin 168impl GpioConfig<Enabled, Output, DontCare> { 169 pub fn set_bit(&mut self, set_high: bool) { 170 self.periph.modify(|_r, w| w.output_mode.set_bit(set_high)); 171 } 172} 173 174/// These methods may be used on any enabled input GPIO 175impl<IN_MODE> GpioConfig<Enabled, Input, IN_MODE> { 176 pub fn bit_is_set(&self) -> bool { 177 self.periph.read().input_status.bit_is_set() 178 } 179 180 pub fn into_input_high_z(self) -> GpioConfig<Enabled, Input, HighZ> { 181 self.periph.modify(|_r, w| w.input_mode().high_z()); 182 GpioConfig { 183 periph: self.periph, 184 enabled: Enabled, 185 direction: Input, 186 mode: HighZ, 187 } 188 } 189 190 pub fn into_input_pull_down(self) -> GpioConfig<Enabled, Input, PulledLow> { 191 self.periph.modify(|_r, w| w.input_mode().pull_low()); 192 GpioConfig { 193 periph: self.periph, 194 enabled: Enabled, 195 direction: Input, 196 mode: PulledLow, 197 } 198 } 199 200 pub fn into_input_pull_up(self) -> GpioConfig<Enabled, Input, PulledHigh> { 201 self.periph.modify(|_r, w| w.input_mode().pull_high()); 202 GpioConfig { 203 periph: self.periph, 204 enabled: Enabled, 205 direction: Input, 206 mode: PulledHigh, 207 } 208 } 209} 210``` 211 212Now let's see what the code using this would look like: 213 214```rust,ignore 215/* 216 * Example 1: Unconfigured to High-Z input 217 */ 218let pin: GpioConfig<Disabled, _, _> = get_gpio(); 219 220// Can't do this, pin isn't enabled! 221// pin.into_input_pull_down(); 222 223// Now turn the pin from unconfigured to a high-z input 224let input_pin = pin.into_enabled_input(); 225 226// Read from the pin 227let pin_state = input_pin.bit_is_set(); 228 229// Can't do this, input pins don't have this interface! 230// input_pin.set_bit(true); 231 232/* 233 * Example 2: High-Z input to Pulled Low input 234 */ 235let pulled_low = input_pin.into_input_pull_down(); 236let pin_state = pulled_low.bit_is_set(); 237 238/* 239 * Example 3: Pulled Low input to Output, set high 240 */ 241let output_pin = pulled_low.into_enabled_output(); 242output_pin.set_bit(true); 243 244// Can't do this, output pins don't have this interface! 245// output_pin.into_input_pull_down(); 246``` 247 248This is definitely 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? 249 250## Compile Time Functional Safety 251 252Because we are enforcing our design constraints 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. 253 254Also, 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! 255