rust_gpiozero 套件簡化了 Rust 在 Raspberry Pi 上的 GPIO 控制,讓開發者能輕鬆操作硬體。透過 LED 與按鈕的範例,展示瞭如何設定 GPIO 腳位為輸出或輸入模式,並控制 LED 狀態和讀取按鈕輸入。文章也說明瞭如何使用內部上拉/下拉電阻來穩定輸入訊號,以及如何透過軟體去抖動處理來避免按鈕彈跳造成的誤觸發。此外,文章還深入 rust_gpiozero 底層,解釋了 Sysfs 虛擬檔案系統和 /dev/gpiomem 記憶體對映的運作方式,以及 rppal 套件如何利用這些機制來提升效能。最後,也提到了如何在 x86 架構主機上進行跨平台編譯,以縮短在 Raspberry Pi 上的編譯時間。
使用 Rust 控制 Raspberry Pi 的 GPIO 輸出與輸入
本章節將介紹如何使用 Rust 程式語言控制 Raspberry Pi 的 GPIO(General Purpose Input/Output)輸出與輸入。我們將使用 rust_gpiozero 這個 crate 來簡化 GPIO 的操作。
設定專案與安裝依賴
首先,在 Raspberry Pi 的桌面環境中開啟終端機,並建立一個新的 Rust 專案:
$ cargo new physical-computing
接著,進入專案目錄並新增 rust_gpiozero 這個依賴:
$ cd physical-computing
$ cargo add rust_gpiozero
你的 Cargo.toml 檔案應該會像這樣:
[dependencies]
rust_gpiozero = "0.1"
控制 LED 燈的輸出
首先,我們來控制一個 LED 燈的輸出。假設我們已經將 LED 連線到 Raspberry Pi 的 GPIO Pin 2。
// src/main.rs
use rust_gpiozero::*;
fn main() {
let led = LED::new(2);
led.on();
}
你可以執行 cargo run 來編譯並執行這個程式。如果電路連線正確,你應該會看到 LED 燈亮起來。
程式碼解析:
LED::new(2)建立了一個新的 LED 物件,並將其連線到 GPIO Pin 2。在建立的過程中,rust_gpiozero會自動將該 Pin 設定為輸出模式。led.on()將 GPIO Pin 2 的電壓設為高(3.3V),使 LED 燈亮起來。
切換 LED 燈的狀態
要讓 LED 燈閃爍,可以使用迴圈並在 led.on() 和 led.off() 之間加入延遲。
// src/main.rs
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));
}
}
程式碼解析:
- 使用
loop迴圈讓程式無限執行。 sleep(Duration::from_secs(1))使程式暫停 1 秒鐘,實作閃爍效果。
或者,你可以使用 rust_gpiozero 提供的 LED::blink() 方法來簡化閃爍的實作:
// src/main.rs
use rust_gpiozero::*;
fn main() {
let mut led = LED::new(2);
led.blink(1.0, 1.0);
led.wait(); // 防止程式立即離開
}
程式碼解析:
led.blink(1.0, 1.0)使 LED 燈每隔 1 秒鐘閃爍一次。led.wait()確保程式不會在blink方法執行完後立即離開。
讀取按鈕輸入
接下來,我們將介紹如何使用 GPIO 讀取按鈕的輸入。首先,我們需要了解 GPIO 的輸入模式以及內部上拉/下拉電阻的組態。
GPIO 輸入模式與內部電阻
當 GPIO Pin 設定為輸入模式時,它可以檢測電壓變化。然而,如果沒有適當的組態,GPIO Pin 可能會處於浮動狀態,導致誤觸發。Raspberry Pi 提供內部上拉和下拉電阻來穩定輸入電壓。
- 內部下拉電阻:將 GPIO Pin 連線到地(GND),預設電壓為 0V。當按鈕被按下時,GPIO Pin 被連線到 3.3V 電源,電壓升高。
- 內部上拉電阻:將 GPIO Pin 連線到 3.3V 電源,預設電壓為 3.3V。當按鈕被按下時,GPIO Pin 被連線到地(GND),電壓降低。
按鈕電路設計
這裡我們使用內部上拉電阻的組態。按鈕的一端連線到 GPIO Pin 4,另一端透過限流電阻連線到地。當按鈕被按下時,GPIO Pin 4 的電壓會降低到 0V。
// src/main.rs
use rust_gpiozero::*;
fn main() {
let mut led = LED::new(2);
let mut button = Button::new(4);
loop {
println!("wait for button");
button.wait_for_press(None);
println!("button pressed!");
led.toggle();
}
}
程式碼解析:
Button::new(4)建立了一個新的按鈕物件,並將其連線到 GPIO Pin 4。button.wait_for_press(None)使程式等待按鈕被按下。- 當按鈕被按下時,
led.toggle()切換 LED 的狀態。
實作按鈕控制LED並解決按鈕彈跳問題
在嵌入式系統中,控制LED燈的開關狀態是一個基本的實作練習。而當加入按鈕控制後,如何處理按鈕的機械彈跳(bounce)成為了一個重要的課題。本章節將介紹如何使用Rust語言來實作按鈕控制LED,並探討如何解決按鈕彈跳的問題。
初始化LED和按鈕結構
首先,我們需要初始化一個LED結構和一個按鈕結構。按鈕結構的初始化函式Button::new()會將指定的GPIO腳位組態為使用上拉電阻。如果需要使用下拉電阻,可以使用Button::new_with_pulldown()函式。
use rust_gpiozero::*;
fn main() {
let mut led = LED::new(2);
let mut button = Button::new(4);
loop {
button.wait_for_press(None);
led.toggle();
}
}
在這個範例中,程式會無限期地等待按鈕被按下。當按鈕被按下後,button.wait_for_press(None)函式會傳回並繼續執行下一行程式碼,進而切換LED的開關狀態。
解決按鈕彈跳問題
然而,在實際操作中,按鈕的機械結構可能會導致短時間內的多重觸發。這是因為當按鈕被按下時,內部的金屬片可能會振動並反覆接觸,從而觸發多次button.wait_for_press()。為瞭解決這個問題,我們需要對電路進行去抖(debounce)處理。
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變數用於記錄上一次按鈕被按下的時間。- 當
button.wait_for_press(None)傳回時,程式會檢查自上次按鈕被按下以來是否已經過了至少1秒。如果時間間隔太短,則認為是按鈕彈跳並忽略此次事件。 - 如果時間間隔足夠長,則切換LED的狀態並更新
last_clicked為當前時間。
跨平台編譯到樹莓派
由於樹莓派的CPU效能相對於主流桌面電腦較弱,直接在樹莓派上編譯Rust程式可能會比較慢。因此,我們需要在效能更強的主機上進行跨平台編譯。
首先,需要在x86架構的Linux主機上安裝ARM架構的編譯工具鏈,包括新增編譯目標和安裝連結器。
rustup target add armv7-unknown-linux-gnueabihf
sudo apt-get install gcc-10-multilib-arm-linux-gnueabihf
然後,需要在~/.cargo/config檔案中指定連結器:
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc-10"
最後,可以使用以下指令進行跨平台編譯:
cargo build --target=armv7-unknown-linux-gnueabihf
內容解密:
rustup target add armv7-unknown-linux-gnueabihf指令用於新增ARMv7架構的編譯目標。- 由於樹莓派OS預設是32位元系統,因此即使CPU支援64位元,也需要使用ARMv7相容的編譯目標。
gcc-10-multilib-arm-linux-gnueabihf套件提供了ARM架構的連結器。- 在
~/.cargo/config中指定連結器,以確保Cargo使用正確的連結器進行編譯。 - 使用
--target引數指定編譯目標,以進行跨平台編譯。
8.4 GPIO程式碼運作原理
rust_gpiozero 套件抽象化了大部分設定GPIO腳位的複雜度,但你可能會好奇它在底層是如何運作的。如第8.2節所述,GPIO暫存器透過兩種不同的介面暴露:/dev/gpiomem 和 Sysfs。首先,讓我們來看看Sysfs是如何運作的。
Sysfs介面
Sysfs將GPIO暫存器以虛擬檔案的形式暴露出來。要開啟一個LED,你可以寫一個簡單的shell指令碼來使用Sysfs虛擬檔案:
# led-on.sh
echo "2" > /sys/class/gpio/export
echo "out" > /sys/class/gpio/gpio2/direction
echo "1" > /sys/class/gpio/gpio2/value
首先,你需要將腳位編號寫入檔案 /sys/class/gpio/export,這告訴Sysfs你想要使用指定的腳位:
echo "2" > /sys/class/gpio/export
一個新的檔案 /sys/class/gpio/gpio2 將會出現。然後,你可以透過寫入 in 或 out 到檔案 /sys/class/gpio/gpio2/direction 來設定腳位的方向。在這個例子中,我們將其設定為輸出模式:
echo "out" > /sys/class/gpio/gpio2/direction
這有效地設定了控制GPIO 2模式的暫存器。將腳位設為高或低電平就像寫入1或0到 /sys/class/gpio/gpio2/value 檔案一樣簡單。
內容解密:
echo "2" > /sys/class/gpio/export:將GPIO 2匯出,使其可用於使用者空間程式。echo "out" > /sys/class/gpio/gpio2/direction:設定GPIO 2為輸出模式。echo "1" > /sys/class/gpio/gpio2/value:將GPIO 2設為高電平,從而開啟LED。
rppal套件與/dev/gpiomem
rust_gpiozero 套件在底層使用了一個更低階的套件 rppal 來與GPIO腳位互動。出於效能考慮,rppal 套件並不使用Sysfs介面,而是直接與 /dev/gpiomem 互動。/dev/gpiomem 是一個虛擬裝置,代表記憶體對映的GPIO暫存器。如果你對 /dev/gpiomem 呼叫 mmap() 系統呼叫,GPIO暫存器將被對映到指定的虛擬記憶體位址。你可以直接讀寫記憶體中的位元來控制暫存器。
為什麼使用/dev/gpiomem?
在 /dev/gpiomem 出現之前,你只能透過 /dev/mem 來存取與GPIO相關的記憶體位址。然而,/dev/mem 暴露了整個系統記憶體,需要root許可權才能存取。這使得每個與GPIO互動的程式都需要使用root許可權,帶來了安全隱患。因此,/dev/gpiomem 被建立出來,只暴露與GPIO相關的記憶體部分,而不需要特殊許可權。
rppal原始碼解析
在 rppal 的原始碼中,你可以看到它首先嘗試使用 /dev/gpiomem。如果出現任何錯誤,它會退回到使用 /dev/mem,但這需要root許可權。
程式碼範例:rppal的src/gpio/mem.rs
const PATH_DEV_GPIOMEM: &str = "/dev/gpiomem";
const GPFSEL0: usize = 0x00;
const GPSET0: usize = 0x1c / std::mem::size_of::<u32>();
const GPCLR0: usize = 0x28 / std::mem::size_of::<u32>();
const GPLEV0: usize = 0x34 / std::mem::size_of::<u32>();
pub struct GpioMem {}
impl GpioMem {
// ...
fn map_devgpiomem() -> Result<*mut u32> {
// ...
// 將/dev/gpiomem記憶體對映到偏移量0
let gpiomem_ptr = unsafe {
libc::mmap(
ptr::null_mut(),
GPIO_MEM_SIZE,
PROT_READ | PROT_WRITE,
MAP_SHARED,
gpiomem_file.as_raw_fd(),
0,
)
};
Ok(gpiomem_ptr as *mut u32)
}
#[inline(always)]
fn write(&self, offset: usize, value: u32) {
unsafe {
ptr::write_volatile(
self.mem_ptr.add(offset),
value
);
}
}
#[inline(always)]
pub(crate) fn set_high(&self, pin: u8) {
let offset = GPSET0 + pin as usize / 32;
let shift = pin % 32;
self.write(offset, 1 << shift);
}
#[inline(always)]
pub(crate) fn set_low(&self, pin: u8) {
let offset = GPCLR0 + pin as usize / 32;
let shift = pin % 32;
self.write(offset, 1 << shift);
}
pub(crate) fn set_mode(&self, pin: u8, mode: Mode) {
let offset = GPFSEL0 + pin as usize / 10;
let shift = (pin % 10) * 3;
// ...
let reg_value = self.read(offset);
self.write(
offset,
(
reg_value & !(0b111 << shift)) |
((mode as u32) << shift
),
);
}
}
內容解密:
map_devgpiomem():將/dev/gpiomem記憶體對映到虛擬記憶體中,使得可以直接操作GPIO暫存器。write():向指定的偏移量寫入值,用於控制GPIO暫存器。set_high()和set_low():分別用於將指定的GPIO腳位設為高或低電平。set_mode():設定指定GPIO腳位的模式,例如輸入或輸出模式。