在處理嵌入式系統時常會需要控制 GPIO。所謂的 GPIO(General-Purpose I/O),就是 IC 的某些 pin 腳,可以透過軔體設定為「input(偵測外部的訊號是高電位還是低電位)」或是「output(對外輸出高電位或低電位)」狀態。

理論上來說,當 GPIO 設定為 input 狀態時,叫它「輸出高電位」是沒有意義的。反之,設定為 output 時,去讀取 pin 的電位狀態可能也是沒意義的。因此在設計存取 GPIO 的 API 時,最好能有適當的防呆機制。

有一種做法,是建立一個 struct,以 data 的方式儲存每根 pin 目前的狀態。然後每次要呼叫 set_bit() 或是 is_bit_set() 之類的函式時,進行 run-time checking。但一來 run-time checking 需要執行時間,二來儲存狀態需要用到記憶體,因此不算完美的解決方案。

而 Rust Embedded Book 中提出一個方案(也是目前相關 crates 中採用的方案),是利用 generic type,將 GPIO 的不同狀態設定為不同的型別參數,產生不同的型別。然後再針對特化版的型別,提供不同的 methods,以防止 GPIO 呼叫到不該呼叫到的 methods。

直接看程式碼可能比較清楚:

/// GPIO interface
struct GpioConfig<ENABLED, DIRECTION> {
    periph: GPIO_CONFIG,    // 實際用來控制硬體暫存器的界面
    enabled: ENABLED,       // 這根 GPIO 是否啟用?
    direction: DIRECTION,   // 輸入還是輸出?
}

// 可以用在 GpioConfig 欄位上的狀態型別
struct Disabled;
struct Enabled;
struct Output;
struct Input;
struct DontCare;

/// 不管 GPIO 的狀態為何,都可以使用這個版本的實作
impl<EN, DIR> GpioConfig<EN, DIR> {
    // 關閉這根 GPIO 的功能
    pub fn into_disabled(self) -> GpioConfig<Disabled, DontCare> {
        /*.. 控制硬體暫存器,略。 ..*/
        GpioConfig {                // 回傳新的 GpioConfig 實體
            periph: self.periph,
            enabled: Disabled,      // enabled 的型別設為 Disabled.
            direction: DontCare,    // 反正都是 Disabled 了,所以不重要。
        }
    }

    // 打開這根 GPIO 的功能,並切換到 Input 模式
    pub fn into_enabled_input(self) -> GpioConfig<Enabled, Input> {
        /*.. 控制硬體暫存器,略。 ..*/
        GpioConfig {                // 回傳新的 GpioConfig 實體
            periph: self.periph,
            enabled: Enabled,       // enabled 的型別設為 Enabled.
            direction: Input,       // direction 的型別設為 Input.
        }
    }

    // 打開這根 GPIO 的功能,並切換到 Output 模式
    pub fn into_enabled_output(self) -> GpioConfig<Enabled, Output> {
        /*.. 控制硬體暫存器,略。 ..*/
        GpioConfig {                // 回傳新的 GpioConfig 實體
            periph: self.periph,
            enabled: Enabled,       // enabled 的型別設為 Enabled.
            direction: Output,      // direction 的型別設為 Ouput.
        }
    }
}

/// 只有特化為 Output 模式的 GPIO 才能使用這些函式
impl GpioConfig<Enabled, Output> {
    // 設定輸出電位為 high 或 low
    pub fn set_bit(&mut self, set_high: bool) {
        /*.. 控制硬體暫存器,略。 ..*/
    }
}

/// 只有特化為 Input 模式的 GPIO 才能使用這些函式
impl GpioConfig<Enabled, Input> {
    // 檢查輸入電位為 high 或 low
    pub fn bit_is_set(&self) -> bool {
        /*.. 控制硬體暫存器,略。 ..*/
    }
}

fn gpio_example() {
    // 取得某個尚未設定的 GPIO
    let pin: GpioConfig<Disabled, _> = get_gpio();

    // 下面這行編譯不會過,因為 pin 還是 Disabled 的狀態。
    // let pin_status = pin.bit_is_set();

    // 把 pin 轉成 Input。執行完這行後 pin 就不能用了。
    let input_pin = pin.into_enabled_input();

    // 此時就可以透過 input_pin 檢查狀態。
    let pin_status = input_pin.bit_is_set();

    // 試著透過 input_pin 設定輸出電位。編譯不會過。
    // input_pin.set_bit(true);

    // 把 input_pin 轉成 Output。執行完後 input_pin 也不能用了。
    let output_pin = input_pin.into_enabled_output();

    // 透過 output_pin 設定輸出電位。
    output_pin.set_bit(true);
}

By closer

發表迴響