设计协约

在我们的上个章节中,我们写了一个接口,其没有强制执行设计协约。让我们再看下我们假想的GPIO配置寄存器:

名字位数(s)含义注释
使能00关闭关闭GPIO
1使能使能GPIO
方向10输入方向设置成输入
1输出方向设置成输出
输入模式2..300hi-z输入设置为高阻抗
01下拉下拉输入管脚
10上拉上拉输入管脚
11n/a无效模式。不要设置
输出模式40拉低拉低输出管脚
1拉高拉高输出管脚
输入状态5xin-val如果输入 < 1.5v 为0,如果输入 >= 1.5v 为1

如果我们在使用底层硬件之前检查状态,在运行时强制遵守我们的设计协约,我们写的代码可能像这一样:

  1. /// GPIO接口
  2. struct GpioConfig {
  3. /// 由svd2rust生成的GPIO配制结构体
  4. periph: GPIO_CONFIG,
  5. }
  6. impl GpioConfig {
  7. pub fn set_enable(&mut self, is_enabled: bool) {
  8. self.periph.modify(|_r, w| {
  9. w.enable().set_bit(is_enabled)
  10. });
  11. }
  12. pub fn set_direction(&mut self, is_output: bool) -> Result<(), ()> {
  13. if self.periph.read().enable().bit_is_clear() {
  14. // 必须被使能配置方向
  15. return Err(());
  16. }
  17. self.periph.modify(|r, w| {
  18. w.direction().set_bit(is_output)
  19. });
  20. Ok(())
  21. }
  22. pub fn set_input_mode(&mut self, variant: InputMode) -> Result<(), ()> {
  23. if self.periph.read().enable().bit_is_clear() {
  24. // 必须被使能配置输入模式
  25. return Err(());
  26. }
  27. if self.periph.read().direction().bit_is_set() {
  28. // 方向必须被设置成输入
  29. return Err(());
  30. }
  31. self.periph.modify(|_r, w| {
  32. w.input_mode().variant(variant)
  33. });
  34. Ok(())
  35. }
  36. pub fn set_output_status(&mut self, is_high: bool) -> Result<(), ()> {
  37. if self.periph.read().enable().bit_is_clear() {
  38. // 设置输出状态必须被使能
  39. return Err(());
  40. }
  41. if self.periph.read().direction().bit_is_clear() {
  42. // 方向必须是输出
  43. return Err(());
  44. }
  45. self.periph.modify(|_r, w| {
  46. w.output_mode.set_bit(is_high)
  47. });
  48. Ok(())
  49. }
  50. pub fn get_input_status(&self) -> Result<bool, ()> {
  51. if self.periph.read().enable().bit_is_clear() {
  52. // 获取状态必须被使能
  53. return Err(());
  54. }
  55. if self.periph.read().direction().bit_is_set() {
  56. // 方向必须是输入
  57. return Err(());
  58. }
  59. Ok(self.periph.read().input_status().bit_is_set())
  60. }
  61. }

因为我们不需要强制遵守硬件上的限制,所以我们最后做了很多运行时检查,它浪费了我们很多时间和资源,对于开发者来说,这个代码用起来就没那么愉快了。

类型状态(Type states)

但是,如果我们使用Rust的类型系统去强制状态转换的规则会怎样?看下这个例子:

  1. /// GPIO接口
  2. struct GpioConfig<ENABLED, DIRECTION, MODE> {
  3. /// 由svd2rust产生的GPIO配置结构体
  4. periph: GPIO_CONFIG,
  5. enabled: ENABLED,
  6. direction: DIRECTION,
  7. mode: MODE,
  8. }
  9. // GpioConfig中MODE的类型状态
  10. struct Disabled;
  11. struct Enabled;
  12. struct Output;
  13. struct Input;
  14. struct PulledLow;
  15. struct PulledHigh;
  16. struct HighZ;
  17. struct DontCare;
  18. /// 这些函数可能被用于所有的GPIO管脚
  19. impl<EN, DIR, IN_MODE> GpioConfig<EN, DIR, IN_MODE> {
  20. pub fn into_disabled(self) -> GpioConfig<Disabled, DontCare, DontCare> {
  21. self.periph.modify(|_r, w| w.enable.disabled());
  22. GpioConfig {
  23. periph: self.periph,
  24. enabled: Disabled,
  25. direction: DontCare,
  26. mode: DontCare,
  27. }
  28. }
  29. pub fn into_enabled_input(self) -> GpioConfig<Enabled, Input, HighZ> {
  30. self.periph.modify(|_r, w| {
  31. w.enable.enabled()
  32. .direction.input()
  33. .input_mode.high_z()
  34. });
  35. GpioConfig {
  36. periph: self.periph,
  37. enabled: Enabled,
  38. direction: Input,
  39. mode: HighZ,
  40. }
  41. }
  42. pub fn into_enabled_output(self) -> GpioConfig<Enabled, Output, DontCare> {
  43. self.periph.modify(|_r, w| {
  44. w.enable.enabled()
  45. .direction.output()
  46. .input_mode.set_high()
  47. });
  48. GpioConfig {
  49. periph: self.periph,
  50. enabled: Enabled,
  51. direction: Output,
  52. mode: DontCare,
  53. }
  54. }
  55. }
  56. /// 这个函数可能被用于一个输出管脚
  57. impl GpioConfig<Enabled, Output, DontCare> {
  58. pub fn set_bit(&mut self, set_high: bool) {
  59. self.periph.modify(|_r, w| w.output_mode.set_bit(set_high));
  60. }
  61. }
  62. /// 这些方法可能被用于使能一个输入GPIO
  63. impl<IN_MODE> GpioConfig<Enabled, Input, IN_MODE> {
  64. pub fn bit_is_set(&self) -> bool {
  65. self.periph.read().input_status.bit_is_set()
  66. }
  67. pub fn into_input_high_z(self) -> GpioConfig<Enabled, Input, HighZ> {
  68. self.periph.modify(|_r, w| w.input_mode().high_z());
  69. GpioConfig {
  70. periph: self.periph,
  71. enabled: Enabled,
  72. direction: Input,
  73. mode: HighZ,
  74. }
  75. }
  76. pub fn into_input_pull_down(self) -> GpioConfig<Enabled, Input, PulledLow> {
  77. self.periph.modify(|_r, w| w.input_mode().pull_low());
  78. GpioConfig {
  79. periph: self.periph,
  80. enabled: Enabled,
  81. direction: Input,
  82. mode: PulledLow,
  83. }
  84. }
  85. pub fn into_input_pull_up(self) -> GpioConfig<Enabled, Input, PulledHigh> {
  86. self.periph.modify(|_r, w| w.input_mode().pull_high());
  87. GpioConfig {
  88. periph: self.periph,
  89. enabled: Enabled,
  90. direction: Input,
  91. mode: PulledHigh,
  92. }
  93. }
  94. }

现在让我们看下代码如何用这个API:

  1. /*
  2. * 案例 1: Unconfigured to High-Z input
  3. */
  4. let pin: GpioConfig<Disabled, _, _> = get_gpio();
  5. // 不能这么做,pin没有被使能
  6. // pin.into_input_pull_down();
  7. // 现在从unconfigured to a high-z input打开管脚
  8. let input_pin = pin.into_enabled_input();
  9. // 从管脚读取
  10. let pin_state = input_pin.bit_is_set();
  11. // 不能这么做,输入管脚没有这个接口
  12. // input_pin.set_bit(true);
  13. /*
  14. * 案例 2: High-Z 输入到下拉输入
  15. */
  16. let pulled_low = input_pin.into_input_pull_down();
  17. let pin_state = pulled_low.bit_is_set();
  18. /*
  19. * 案例 3: 下拉输入到输出, 拉高
  20. */
  21. let output_pin = pulled_low.into_enabled_output();
  22. output_pin.set_bit(true);
  23. // 不能这么做,输出管脚没有这个接口
  24. // output_pin.into_input_pull_down();

这绝对是存储管脚状态的便捷方法,但是为什么这么做?为什么这比把状态当成一个enum存在我们的GpioConfig结构体中更好?

编译时功能安全(Functional Safety)

因为我们在编译时完全强制我们遵守我们的设计约定,这造成了没有运行时开销。当你有一个在输入模式的管脚时,是不可能去设置一个输出模式的。你必须先把它设置成一个输出管脚,然后再设置输出模式。因为在执行一个函数前会检查现在的状态,因此没有运行时消耗。

也因为这些状态被类型系统强制遵守,因此没有为这个接口的使用者留太多的犯错空间。如果它们尝试执行一个非法的状态转换,代码将不会编译!