在嵌入式系統開發中,控制硬體與外部裝置互動至關重要。本文將示範如何使用 Rust 語言在 Raspberry Pi 上控制 GPIO(通用輸入/輸出)介面,並以 LED 閃爍為例說明實作過程。透過 rust_gpiozero 函式庫,簡化 GPIO 操作,並結合防彈跳機制提升程式碼的穩定性。此外,文章也涵蓋了跨平台編譯的設定步驟,讓開發者能在不同平台上編譯程式並佈署到樹莓派上執行,更探討了 Sysfs 和 /dev/gpiomem 兩種底層介面控制 GPIO 的機制,提供更全面的技術理解。
使用 Rust 進行實體運算:控制 GPIO 與 LED 閃爍
在進行實體運算(Physical Computing)時,Rust 語言提供了強大的工具和函式庫,使得開發者能夠輕鬆地控制硬體元件,如 LED、按鈕等。本篇文章將介紹如何在 Raspberry Pi 上使用 Rust 語言控制 GPIO(General Purpose Input/Output)並實作 LED 的閃爍。
GPIO 簡介與 Raspbian OS 的支援
Raspberry Pi 的 GPIO 提供了豐富的介面,用於與外部硬體進行互動。在 Raspbian OS 上,這些 GPIO 暫存器被暴露為裝置檔案(位於 /sys/class/gpio/*)。開發者可以透過讀寫這些虛擬檔案來取得或設定暫存器的值。
使用 rust_gpiozero 函式庫簡化 GPIO 操作
直接操作這些虛擬檔案相對繁瑣,因此可以使用 rust_gpiozero 函式庫來簡化 GPIO 的操作。rust_gpiozero 的設計靈感來自 Python 的 gpiozero 函式庫,提供了易於使用的元件,如 LED 和按鈕,使得控制連線至 GPIO 的硬體元件變得更加簡單。
設定 rust_gpiozero 環境
首先,建立一個新的 Rust 專案,並在 Cargo.toml 中加入 rust_gpiozero 的依賴:
[dependencies]
rust_gpiozero = "0.2.0"
建立 LED 電路
要控制 LED,首先需要了解 LED 的基本特性。LED(Light Emitting Diode)是一種只有在單一方向導通的電子元件,其正極(Anode)通常是較長的腳,負極(Cathode)則是較短的腳。為了保護 LED 免受過大電流的損害,通常會在電路中加入一個電阻來限制電流。
使用麵包板搭建電路
為了方便實驗,可以使用麵包板(Breadboard)來搭建電路。麵包板上的孔洞內部連線著金屬片,可以臨時形成電路連線,非常適合用於原型開發。
連線 LED 電路
按照圖 5-7 的電路圖,將 LED、電阻和跳線連線到 Raspberry Pi 的 GPIO 引腳上。在這個例子中,電流從 GPIO 引腳 2 流向 LED,然後透過電阻到達地線,從而控制 LED 的亮滅。
使用 Rust 控制 GPIO 輸出
開啟 LED
建立一個新的 Rust 程式,使用 rust_gpiozero 函式庫來控制 GPIO 引腳。以下程式碼展示瞭如何開啟連線在 GPIO 引腳 2 上的 LED:
extern crate rust_gpiozero;
use rust_gpiozero::*;
fn main() {
let mut led = LED::new(2);
led.on();
}
執行 cargo run 後,如果電路連線正確,LED 應該會亮起。
閃爍 LED
要實作 LED 的閃爍,可以使用迴圈並在 led.on() 和 led.off() 之間加入延遲。以下是一個簡單的實作:
extern crate rust_gpiozero;
use rust_gpiozero::*;
use std::thread::sleep;
use std::time::Duration;
fn main() {
let led = LED::new(2);
loop {
println!("on");
led.on();
sleep(Duration::from_secs(1));
println!("off");
led.off();
sleep(Duration::from_secs(1));
}
}
或者,可以直接使用 rust_gpiozero 提供的 LED::blink() 方法:
extern crate rust_gpiozero;
use rust_gpiozero::*;
fn main() {
let led = LED::new(2);
led.blink(Duration::from_secs(1), Duration::from_secs(1), None);
}
程式碼解說:
extern crate rust_gpiozero;:宣告使用rust_gpiozero外部 crate。use rust_gpiozero::*;:匯入rust_gpiozero中的所有公開專案。let mut led = LED::new(2);:建立一個新的LED物件,並將其連線到 GPIO 引腳 2。led.on();和led.off();:控制 LED 的開啟和關閉。sleep(Duration::from_secs(1));:在開啟和關閉 LED 之間加入 1 秒的延遲。led.blink(Duration::from_secs(1), Duration::from_secs(1), None);:使 LED 以 1 秒為週期閃爍。
使用 Rust 進行實體運算:控制 LED 與讀取按鈕狀態
在前面的章節中,我們學習瞭如何使用 Rust 控制 GPIO 針腳來輸出訊號。在本章節中,我們將探討如何使用 Rust 進行實體運算,涵蓋控制 LED 燈和讀取按鈕狀態等主題。
控制 LED 燈
要控制 LED 燈,我們需要先初始化一個 LED 物件,並設定其對應的 GPIO 針腳。以下是一個簡單的範例程式碼:
extern crate rust_gpiozero;
use rust_gpiozero::*;
fn main() {
let mut led = LED::new(2);
led.blink(1.0, 1.0);
led.wait(); // 避免程式立即離開
}
內容解密:
LED::new(2):建立一個新的LED物件,並將其對應到 GPIO 針腳 2。led.blink(1.0, 1.0):使 LED 燈每隔 1 秒閃爍一次。第一個引數1.0表示 LED 燈亮起的時間,第二個引數1.0表示 LED 燈熄滅的時間。led.wait():呼叫此函式以避免程式立即離開,使 LED 燈能夠持續閃爍。
讀取按鈕狀態
要讀取按鈕狀態,我們需要組態 GPIO 針腳為輸入模式,並使用內部電阻來穩定針腳的電壓。以下是讀取按鈕狀態的範例程式碼:
extern crate rust_gpiozero;
use rust_gpiozero::*;
fn main() {
let mut button = Button::new(4);
loop {
println!("等待按鈕按下...");
button.wait_for_press(None);
println!("按鈕已按下!");
}
}
內容解密:
Button::new(4):建立一個新的Button物件,並將其對應到 GPIO 針腳 4。此函式預設使用內部上拉電阻。button.wait_for_press(None):阻塞程式直到按鈕被按下。可以選擇性地設定逾時時間,例如Some(1.0)表示 1 秒後逾時。- 按鈕按下後,程式將繼續執行並印出 “按鈕已按下!"。
防彈跳處理
在實際應用中,按鈕可能會因為機械彈跳而觸發多次按下事件。為了避免這種情況,我們需要進行防彈跳處理。以下是一個簡單的防彈跳範例程式碼:
extern crate rust_gpiozero;
use rust_gpiozero::*;
use std::time::{Duration, Instant};
fn main() {
let mut led = LED::new(2);
let mut button = Button::new(4);
let mut last_clicked = Instant::now();
loop {
button.wait_for_press(None);
if last_clicked.elapsed() < Duration::new(1, 0) {
continue;
}
led.toggle();
last_clicked = Instant::now();
}
}
內容解密:
last_clicked變數用於記錄上一次按鈕被按下的時間。- 當按鈕被按下時,檢查距離上一次按下是否已經過了 1 秒。如果沒有,則忽略此次按下事件。
- 如果距離上一次按下已經過了 1 秒,則切換 LED 燈的狀態,並更新
last_clicked時間戳。
跨平台編譯到樹莓派(Raspberry Pi)
在樹莓派上編譯 Rust 程式可能會比較慢,這是因為樹莓派的 CPU 效能不如大多數主流的桌面電腦 CPU。不過,樹莓派的 CPU 在嵌入式世界中已經相當強大。有些應用場合會使用更弱但更節能的 CPU 或微控制器,這些裝置的 CPU、記憶體和電池資源都非常有限,無法在本地編譯程式碼。
跨平台編譯的必要性
這時候就需要使用跨平台編譯(cross-compilation)。跨平台編譯是指在不同的機器(主機)上編譯原始碼,然後在目標機器上執行。例如,可以在強大的 Intel x86 架構的 Linux 桌面電腦上編譯程式碼,然後在樹莓派(ARM 架構)上執行。在這種情況下,編譯器本身執行在 x86 架構的 CPU 上,但它生成的是 ARM 架構的機器碼。
設定跨平台編譯環境
首先,需要在 Linux 桌面電腦或筆電上安裝樹莓派的編譯工具鏈(toolchain)。可以透過 rustup 新增編譯目標:
rustup target add armv7-unknown-linux-gnueabihf
為什麼選擇 armv7 目標?
也許有人會疑惑為什麼要安裝 armv7 目標,而樹莓派的 CPU 是 ARM Cortex-A53,屬於 ARMv8 架構。這是因為 ARM Cortex-A53 CPU 同時支援 32 位元和 64 位元模式,而 Raspbian 預設是 32 位元的 Linux 系統,因此 CPU 會執行在 32 位元模式,只支援 ARMv7 相容的功能。
安裝連結器(Linker)
除了編譯器之外,還需要一個連結器。在 x86 Linux 上工作時,可能不會注意到連結器的存在,因為它通常已經隨著其他程式安裝好了。連結器通常包含在 C 編譯器的套件中,因此可以安裝 GCC(GNU Compiler Collection)來取得 ARM 連結器:
sudo apt-get install gcc-5-multilib-arm-linux-gnueabihf
設定 Cargo 使用連結器
在編譯之前,需要告訴 Cargo 在哪裡找到連結器。可以開啟 ~/.cargo/config 檔案(如果不存在就建立一個),並新增以下設定:
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc-5"
這表示「當為 armv7-unknown-linux-gnueabihf 目標編譯時,使用 arm-linux-gnueabihf-gcc-5 提供的連結器」。
建立新專案並進行跨平台編譯
建立一個新的 Cargo 專案,例如 blink-cross-compile,並將閃爍 LED 的程式碼(見 Listings 5-2)複製到 src/main.rs 中。同時,不要忘記在 Cargo.toml 中新增 rust_gpiozero 的依賴。
要為特定目標編譯 Rust 專案,可以使用 --target 引數,如下所示:
cargo build --target=armv7-unknown-linux-gnueabihf
編譯結果
這將在 target/armv7-unknown-linux-gnueabihf/debug/ 目錄下產生一個名為 blink-cross-compile 的二進位制檔案。注意,這個二進位制檔案位於 target/armv7-unknown-linux-gnueabihf 資料夾中,而不是預設的 target/debug 資料夾。如果嘗試在 x86 Linux 系統上執行這個二進位制檔案,會收到錯誤訊息:
$ ./blink-cross-compile
bash: ./blink-cross-compile: cannot execute binary file: Exec format error
這是因為這個二進位制檔案是為 ARM 架構跨平台編譯的。可以使用 UNIX 的 file 命令來驗證:
$ file ./blink-cross-compile
./blink-cross-compile: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV),
dynamically linked, interpreter /lib/ld-, for GNU/Linux 3.2.0,
BuildID[sha1]=43d4fc4e17539883185e15c3d442986f2fb2f03d, not stripped
在樹莓派上執行跨平台編譯的程式
最後一步是將這個二進位制檔案複製到樹莓派的 SD 卡上,並啟動 Raspbian OS。一旦樹莓派啟動,就開啟終端機,導航到二進位制檔案所在的目錄並執行它。應該可以看到 LED 在閃爍,就像之前一樣。
GPIO 程式碼的工作原理
rust_gpiozero crate 對 GPIO 引腳的操作進行了高度抽象。不過,可以進一步瞭解它是如何在底層工作的。如前所述,GPIO 暫存器透過兩種不同的介面暴露出來:/dev/gpiomem 和 Sysfs。Sysfs 將 GPIO 暫存器暴露為虛擬裝置,更容易理解,因此先從 Sysfs 開始。
使用 Sysfs 虛擬檔案控制 LED
要達到與 Listing 5-1 相同的效果,可以寫一個簡單的 shell 指令碼,使用 Sysfs 虛擬檔案(見 Listing 5-6)。
Listings 5-6:使用 Sysfs 開啟 LED
echo "2" > /sys/class/gpio/export
echo "out" > /sys/class/gpio/gpio2/direction
echo "0" > /sys/class/gpio/gpio2/value
首先,需要將引腳編號寫入 /sys/class/gpio/export 檔案,這告訴 Sysfs 要操作哪個引腳。在匯出引腳之前,虛擬引腳裝置 /sys/class/gpio/gpio2 不存在。因此,首先匯出引腳 2:
echo "2" > /sys/class/gpio/export
然後,一個名為 /sys/class/gpio/gpio2 的新裝置就會出現。可以透過寫入 in 或 out 到 /sys/class/gpio/gpio2/direction 檔案來設定引腳的方向。因為將這個引腳用作輸出裝置,所以寫入 out。這有效地設定了控制 GPIO 2 模式的暫存器。然後,將引腳設為高或低電平就像寫入 1 或 0 到 /sys/class/gpio/gpio2/value 一樣簡單。
這些 Sysfs 檔案基本上是對控制 GPIO 引腳的暫存器的抽象。不過,rust_gpiozero crate 使用了一個更底層的 crate —— rppal,來與 GPIO 引腳互動。出於效能考慮,rppal 不使用 Sysfs 介面,而是直接與 /dev/gpiomem 互動。/dev/gpiomem 是一個虛擬裝置,代表記憶體對映的 GPIO 暫存器。如果呼叫 mmap() 系統呼叫於 /dev/gpiomem,GPIO 暫存器就會被對映到指定的虛擬記憶體位址。然後,可以直接讀寫記憶體中的位元來控制暫存器。
詳細解析:使用Plantuml圖表來說明GPIO控制流程
此圖示說明瞭GPIO控制的基本流程:
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title Rust 實體運算 GPIO 控制 LED 閃爍
package "物聯網架構" {
package "感知層" {
component [感測器] as sensor
component [執行器] as actuator
component [嵌入式裝置] as device
}
package "網路層" {
component [閘道器] as gateway
component [MQTT Broker] as mqtt
component [邊緣運算] as edge
}
package "平台層" {
cloud "IoT Platform" as platform
database [時序資料庫] as tsdb
component [規則引擎] as rules
}
package "應用層" {
component [監控儀表板] as dashboard
component [告警系統] as alert
component [數據分析] as analytics
}
}
sensor --> device : 資料採集
device --> gateway : 資料傳輸
gateway --> mqtt : MQTT 協議
mqtt --> edge : 邊緣處理
edge --> platform : 雲端上傳
platform --> tsdb : 資料儲存
platform --> rules : 規則處理
rules --> alert : 觸發告警
tsdb --> analytics : 資料分析
analytics --> dashboard : 視覺化
@enduml此圖示說明瞭兩種不同的控制GPIO的方式:
透過Sysfs虛擬檔案系統
- 需要匯出GPIO引腳
- 設定引腳方向(輸入/輸出)
- 設定輸出電平(高/低)
透過/dev/gpiomem直接存取
- 需要進行記憶體對映
- 直接讀寫GPIO相關暫存器實作控制
這種雙重控制機制提供了不同的使用場景和效能選擇。