03-设计契约

设计契约(Design Contracts)

译文 · 基于 The Embedded Rust Book

设计契约

原文链接: https://doc.rust-lang.org/stable/embedded-book/static-guarantees/design-contracts.html

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

名称位编号值含义说明
enable00disabled禁用 GPIO
1enabled启用 GPIO
direction10input将方向设为输入
1output将方向设为输出
input_mode2..300hi-z将输入设为高阻
01pull-low输入引脚下拉
10pull-high输入引脚上拉
11n/a无效状态,请勿设置
output_mode40set-low输出引脚驱动为低
1set-high输出引脚驱动为高
input_status5xin-val输入 < 1.5v 时为 0,输入 >= 1.5v 时为 1

若我们改为在使用底层硬件前检查状态,在运行时强制执行设计契约,代码可能看起来像这样:

/// GPIO 接口
struct GpioConfig {
    /// 由 svd2rust 生成的 GPIO 配置结构
    periph: GPIO_CONFIG,
}

impl GpioConfig {
    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) -> Result<(), ()> {
        if self.periph.read().enable().bit_is_clear() {
            // 必须先启用才能设置方向
            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() {
            // 必须先启用才能设置输入模式
            return Err(());
        }

        if self.periph.read().direction().bit_is_set() {
            // 方向必须是输入
            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() {
            // 必须先启用才能设置输出状态
            return Err(());
        }

        if self.periph.read().direction().bit_is_clear() {
            // 方向必须是输出
            return Err(());
        }

        self.periph.modify(|_r, w| {
            w.output_mode.set_bit(is_high)
        });

        Ok(())
    }

    pub fn get_input_status(&self) -> Result<bool, ()> {
        if self.periph.read().enable().bit_is_clear() {
            // 必须先启用才能获取状态
            return Err(());
        }

        if self.periph.read().direction().bit_is_set() {
            // 方向必须是输入
            return Err(());
        }

        Ok(self.periph.read().input_status().bit_is_set())
    }
}

因为需要强制执行对硬件的限制,我们最终做了大量浪费时间和资源的运行时检查,而且这段代码对开发者来说用起来也没那么愉快。

类型状态

但如果我们改用 Rust 的类型系统来强制执行状态转换规则呢?看这个例子:

/// GPIO 接口
struct GpioConfig<ENABLED, DIRECTION, MODE> {
    /// 由 svd2rust 生成的 GPIO 配置结构
    periph: GPIO_CONFIG,
    enabled: ENABLED,
    direction: DIRECTION,
    mode: MODE,
}

// GpioConfig 中 MODE 的类型状态
struct Disabled;
struct Enabled;
struct Output;
struct Input;
struct PulledLow;
struct PulledHigh;
struct HighZ;
struct DontCare;

/// 这些函数可用于任意 GPIO 引脚
impl<EN, DIR, IN_MODE> GpioConfig<EN, DIR, IN_MODE> {
    pub fn into_disabled(self) -> GpioConfig<Disabled, DontCare, DontCare> {
        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<Enabled, Input, HighZ> {
        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<Enabled, Output, DontCare> {
        self.periph.modify(|_r, w| {
            w.enable.enabled()
             .direction.output()
             .input_mode.set_high()
        });
        GpioConfig {
            periph: self.periph,
            enabled: Enabled,
            direction: Output,
            mode: DontCare,
        }
    }
}

/// 此函数可用于输出引脚
impl GpioConfig<Enabled, Output, DontCare> {
    pub fn set_bit(&mut self, set_high: bool) {
        self.periph.modify(|_r, w| w.output_mode.set_bit(set_high));
    }
}

/// 这些方法可用于任意已启用的输入 GPIO
impl<IN_MODE> GpioConfig<Enabled, Input, IN_MODE> {
    pub fn bit_is_set(&self) -> bool {
        self.periph.read().input_status.bit_is_set()
    }

    pub fn into_input_high_z(self) -> GpioConfig<Enabled, Input, HighZ> {
        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<Enabled, Input, PulledLow> {
        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<Enabled, Input, PulledHigh> {
        self.periph.modify(|_r, w| w.input_mode().pull_high());
        GpioConfig {
            periph: self.periph,
            enabled: Enabled,
            direction: Input,
            mode: PulledHigh,
        }
    }
}

现在看看使用它的代码会是什么样:

/*
 * 示例 1:从未配置到高阻输入
 */
let pin: GpioConfig<Disabled, _, _> = get_gpio();

// 做不到,引脚尚未启用!
// pin.into_input_pull_down();

// 现在把引脚从未配置转为高阻输入
let input_pin = pin.into_enabled_input();

// 从引脚读取
let pin_state = input_pin.bit_is_set();

// 做不到,输入引脚没有这个接口!
// input_pin.set_bit(true);

/*
 * 示例 2:从高阻输入到下拉输入
 */
let pulled_low = input_pin.into_input_pull_down();
let pin_state = pulled_low.bit_is_set();

/*
 * 示例 3:从下拉输入到输出,并置高
 */
let output_pin = pulled_low.into_enabled_output();
output_pin.set_bit(true);

// 做不到,输出引脚没有这个接口!
// output_pin.into_input_pull_down();

这确实是存储引脚状态的便捷方式,但为什么要这样做?为什么这比在 GpioConfig 结构体内部用 enum 存储状态更好?

编译期功能安全

因为我们完全在编译期强制执行设计约束,所以不会产生运行时开销。当你持有输入模式的引脚时,不可能设置输出模式。相反,你必须通过状态逐步转换:先把它转成输出引脚,再设置输出模式。因此,在执行函数前检查当前状态不会带来运行时惩罚。

而且,因为这些状态由类型系统强制执行,该接口的使用者不再有犯错的空间。若他们尝试执行非法状态转换,代码将无法编译!

最后修改 August 11, 2026: 更新 (70a5af133)