Rust之树莓派GPIO篇:从零开始掌控硬件
在树莓派上使用Rust进行GPIO编程,兼顾了极致性能(接近C++)与内存安全(无GC、无数据竞争)。Rust生态提供了从底层直接操作寄存器到高层设备抽象的多层库选型,本文带你全面掌握。
第一部分:核心库选型
Rust生态中,针对树莓派GPIO有几种主流选择:
| 库名 | 方式 | 优点 | 适用场景 |
|---|---|---|---|
| rppal | 直接访问 /dev/gpiomem 寄存器 | 性能最强,功能最全(GPIO/I2C/PWM/SPI/UART) | 绝大多数场景,首选 |
| gpio-cdev | Linux GPIO子系统 (libgpiod) | 内核标准,跨发行版 | 通用应用 |
| embedded-hal | 嵌入式抽象层 | 与嵌入式生态兼容,抽象统一 | 设备驱动开发 |
| 直接mmap | 手动内存映射 | 极致性能,完全控制 | 需深度定制时 |
| sysfs_gpio | /sys/class/gpio 文件系统 | 最简单 | 已过时,不推荐 |
第二部分:rppal库(推荐首选)
rppal是Rust树莓派GPIO的事实标准库,通过直接操作 /dev/gpiomem 实现高性能硬件访问。
2.1 安装rppal
在 Cargo.toml 中添加:
[dependencies]
rppal = "0.17"
2.2 基本用法
闪烁LED示例:
use rppal::gpio::Gpio;
use std::{thread, time::Duration};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 初始化GPIO
let gpio = Gpio::new()?;
// 使用物理引脚编号(也可用BCM编号)
let pin = gpio.get(17)?.into_output(); // 物理引脚11,BCM17
println!("开始闪烁LED(按Ctrl+C退出)");
for i in 0..10 {
pin.set_high();
println!("[{}] 点亮", i + 1);
thread::sleep(Duration::from_millis(500));
pin.set_low();
println!("[{}] 熄灭", i + 1);
thread::sleep(Duration::from_millis(500));
}
Ok(())
}
编译与运行:
cargo build --release
sudo ./target/release/blink
使用物理引脚编号(BOARD模式):
use rppal::gpio::{Gpio, Level, Mode};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let gpio = Gpio::new()?;
// 使用BCM编码(默认)
let pin = gpio.get(17)?.into_output();
// 或者使用物理引脚编号
let pin = gpio.get_with_mode(11, Mode::Output)?; // 物理引脚11,BCM17
Ok(())
}
2.3 读取按钮输入
use rppal::gpio::{Gpio, Level, PullUpDown};
use std::{thread, time::Duration};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let gpio = Gpio::new()?;
// BCM 18(物理引脚12)为输入,启用上拉电阻
let button = gpio.get(18)?.into_input_pullup();
println!("等待按钮按下(连接到GND)...");
loop {
match button.read() {
Level::Low => {
println!("按钮被按下!");
thread::sleep(Duration::from_millis(200)); // 消抖
}
Level::High => {
// 未按下
}
}
thread::sleep(Duration::from_millis(10));
}
}
2.4 按钮控制LED
use rppal::gpio::{Gpio, Level, PullUpDown};
use std::{thread, time::Duration};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let gpio = Gpio::new()?;
let led = gpio.get(17)?.into_output();
let button = gpio.get(18)?.into_input_pullup();
println!("按钮按下LED点亮,松开熄灭");
loop {
match button.read() {
Level::Low => led.set_high(),
Level::High => led.set_low(),
}
thread::sleep(Duration::from_millis(10));
}
}
2.5 自锁开关(每次按下切换状态)
use rppal::gpio::{Gpio, Level, PullUpDown};
use std::{thread, time::Duration};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let gpio = Gpio::new()?;
let led = gpio.get(17)?.into_output();
let button = gpio.get(18)?.into_input_pullup();
let mut led_state = false;
let mut last_state = Level::High;
println!("每次按下按钮切换LED状态");
loop {
let current = button.read();
// 检测下降沿(从高到低)
if last_state == Level::High && current == Level::Low {
led_state = !led_state;
if led_state {
led.set_high();
println!("LED点亮");
} else {
led.set_low();
println!("LED熄灭");
}
thread::sleep(Duration::from_millis(200)); // 消抖
}
last_state = current;
thread::sleep(Duration::from_millis(10));
}
}
第三部分:PWM(脉冲宽度调制)
rppal支持硬件PWM,可精确控制LED亮度或电机转速。
3.1 呼吸灯效果
use rppal::gpio::Gpio;
use std::{thread, time::Duration};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let gpio = Gpio::new()?;
// 硬件PWM引脚:BCM 12, 13, 18, 19
// 这里使用BCM 18(物理引脚12)
let mut pwm = gpio.get(18)?.into_pwm();
// 设置频率100Hz
pwm.set_frequency(100.0, 0.0)?;
println!("呼吸灯效果,按Ctrl+C退出");
loop {
// 渐亮:占空比从0%到100%
for duty in (0..=100).step_by(2) {
let fraction = duty as f64 / 100.0;
pwm.set_duty_cycle(fraction)?;
thread::sleep(Duration::from_millis(20));
}
// 渐暗:占空比从100%到0%
for duty in (0..=100).step_by(2).rev() {
let fraction = duty as f64 / 100.0;
pwm.set_duty_cycle(fraction)?;
thread::sleep(Duration::from_millis(20));
}
}
}
3.2 精确控制占空比
use rppal::gpio::Gpio;
use std::{thread, time::Duration};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let gpio = Gpio::new()?;
let mut pwm = gpio.get(18)?.into_pwm();
// 设置频率1kHz,初始占空比50%
pwm.set_frequency(1000.0, 0.5)?;
println!("PWM输出中,占空比50%");
// 占空比设置为75%
thread::sleep(Duration::from_secs(2));
pwm.set_duty_cycle(0.75)?;
println!("占空比75%");
// 占空比设置为25%
thread::sleep(Duration::from_secs(2));
pwm.set_duty_cycle(0.25)?;
println!("占空比25%");
// 停止PWM输出
thread::sleep(Duration::from_secs(2));
pwm.disable()?;
println!("PWM已停止");
Ok(())
}
第四部分:传感器实战
4.1 超声波测距HC-SR04
使用rppal配合精确计时实现超声波测距:
use rppal::gpio::{Gpio, Level};
use std::{thread, time::{Duration, Instant}};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let gpio = Gpio::new()?;
let trig = gpio.get(23)?.into_output();
let echo = gpio.get(24)?.into_input();
const SPEED_OF_SOUND: f64 = 34300.0; // cm/s
println!("超声波测距开始");
loop {
// 发送10微秒脉冲
trig.set_low();
thread::sleep(Duration::from_micros(2));
trig.set_high();
thread::sleep(Duration::from_micros(10));
trig.set_low();
// 等待ECHO变高
let timeout = Instant::now() + Duration::from_millis(100);
while echo.read() == Level::Low {
if Instant::now() > timeout {
println!("超时");
break;
}
}
let pulse_start = Instant::now();
// 等待ECHO变低
while echo.read() == Level::High {
if Instant::now() > timeout {
println!("超时");
break;
}
}
let pulse_end = Instant::now();
let duration = pulse_end - pulse_start;
let distance = duration.as_secs_f64() * SPEED_OF_SOUND / 2.0;
if distance > 0.0 && distance < 400.0 {
println!("距离: {:.1} cm", distance);
} else {
println!("读数异常: {:.1} cm", distance);
}
thread::sleep(Duration::from_millis(500));
}
}
4.2 DHT11温湿度传感器(使用pigpio)
DHT11对时序要求严格,推荐使用pigpio配合rppal,或使用纯Rust实现的bitbanging。
方案:使用sysfs方式(简化版)
Rust中可以使用 dht-sensor crate:
[dependencies]
dht-sensor = "0.2"
use dht_sensor::dht11;
use std::thread;
use std::time::Duration;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 使用BCM 17
let pin = 17;
loop {
match dht11::read(pin) {
Ok(reading) => {
println!("温度: {:.1}°C 湿度: {:.1}%",
reading.temperature, reading.humidity);
}
Err(e) => {
eprintln!("读取失败: {:?}", e);
}
}
thread::sleep(Duration::from_secs(2));
}
}
4.3 光敏传感器(使用MCP3008 ADC)
通过SPI读取模拟信号:
[dependencies]
rppal = "0.17"
use rppal::spi::{Spi, Bus, SlaveSelect, Mode};
use std::{thread, time::Duration};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 初始化SPI
let mut spi = Spi::new(Bus::Spi0, SlaveSelect::Ss0, 1_000_000, Mode::Mode0)?;
fn read_adc(spi: &mut Spi, channel: u8) -> Result<u16, Box<dyn std::error::Error>> {
if channel > 7 {
return Err("通道必须在0-7之间".into());
}
// MCP3008通信协议
let command = [0x01, (0x08 + channel) << 4, 0x00];
let mut response = [0u8, 0u8, 0u8];
spi.transfer(&mut response, &command)?;
let result = ((response[1] as u16 & 0x03) << 8) | response[2] as u16;
Ok(result)
}
println!("读取光照值");
loop {
let value = read_adc(&mut spi, 0)?;
println!("光照值: {}", value);
thread::sleep(Duration::from_millis(500));
}
}
第五部分:中断与事件检测
rppal支持异步事件检测,通过 AsyncGpio 实现中断。
5.1 使用async/await检测按钮
[dependencies]
rppal = "0.17"
tokio = { version = "1", features = ["full"] }
use rppal::gpio::Gpio;
use std::sync::Arc;
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let gpio = Gpio::new()?;
let button = Arc::new(gpio.get(18)?.into_input_pullup());
let led = Arc::new(gpio.get(17)?.into_output());
let button_clone = button.clone();
let led_clone = led.clone();
// 创建异步任务检测下降沿
let handle = tokio::spawn(async move {
let mut last_state = rppal::gpio::Level::High;
loop {
let current = button_clone.read();
if last_state == rppal::gpio::Level::High && current == rppal::gpio::Level::Low {
// 按钮被按下
led_clone.toggle();
println!("按钮按下,LED切换");
sleep(Duration::from_millis(200)).await; // 消抖
}
last_state = current;
sleep(Duration::from_millis(10)).await;
}
});
println!("等待按钮事件,按Ctrl+C退出");
handle.await?;
Ok(())
}
5.2 使用回调函数检测事件
use rppal::gpio::{Gpio, Level, Trigger};
use std::sync::{Arc, Mutex};
use std::{thread, time::Duration};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let gpio = Gpio::new()?;
let button = gpio.get(18)?.into_input_pullup();
let led = Arc::new(Mutex::new(gpio.get(17)?.into_output()));
// 设置中断检测下降沿
let mut button = button.into_async()?;
let led_clone = led.clone();
// 注册回调
button.set_async_callback(Trigger::FallingEdge, move |level| {
let mut led = led_clone.lock().unwrap();
led.toggle();
println!("按钮中断触发,LED切换");
})?;
println!("中断模式运行中,按Ctrl+C退出");
// 保持主线程运行
loop {
thread::sleep(Duration::from_secs(1));
}
}
第六部分:gpio-cdev库(通用方案)
gpio-cdev使用Linux GPIO子系统,跨发行版兼容性好。
6.1 安装
[dependencies]
gpio-cdev = "0.6"
6.2 基本用法
use gpio_cdev::{Chip, LineRequestFlags};
use std::{thread, time::Duration};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 打开GPIO芯片
let mut chip = Chip::new("/dev/gpiochip0")?;
// 获取GPIO线(BCM 17)
let line = chip.get_line(17)?;
// 请求输出模式
let handle = line.request(
LineRequestFlags::OUTPUT,
0,
"blink",
)?;
println!("开始闪烁LED");
for i in 0..10 {
handle.set_value(1)?;
println!("[{}] 点亮", i + 1);
thread::sleep(Duration::from_millis(500));
handle.set_value(0)?;
println!("[{}] 熄灭", i + 1);
thread::sleep(Duration::from_millis(500));
}
Ok(())
}
6.3 读取输入
use gpio_cdev::{Chip, LineRequestFlags, EventRequestFlags};
use std::{thread, time::Duration};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut chip = Chip::new("/dev/gpiochip0")?;
let line = chip.get_line(18)?;
// 请求输入模式,启用上拉电阻
let handle = line.request(
LineRequestFlags::INPUT | LineRequestFlags::BIAS_PULL_UP,
0,
"button",
)?;
println!("等待按钮按下");
loop {
let value = handle.get_value()?;
if value == 0 {
println!("按钮被按下!");
thread::sleep(Duration::from_millis(200));
}
thread::sleep(Duration::from_millis(10));
}
}
第七部分:embedded-hal抽象层
embedded-hal是Rust嵌入式生态的标准抽象接口,与嵌入式驱动库兼容。
7.1 安装
[dependencies]
rppal = "0.17"
embedded-hal = "0.2"
7.2 使用embedded-hal trait
use embedded_hal::digital::v2::{OutputPin, InputPin};
use rppal::gpio::{Gpio, Level};
struct Led<T: OutputPin> {
pin: T,
}
impl<T: OutputPin> Led<T> {
fn new(pin: T) -> Self {
Self { pin }
}
fn on(&mut self) {
let _ = self.pin.set_high();
}
fn off(&mut self) {
let _ = self.pin.set_low();
}
fn toggle(&mut self) {
if self.pin.is_set_high().unwrap_or(false) {
self.off();
} else {
self.on();
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let gpio = Gpio::new()?;
let pin = gpio.get(17)?.into_output();
let mut led = Led::new(pin);
println!("使用embedded-hal抽象控制LED");
for _ in 0..5 {
led.on();
std::thread::sleep(std::time::Duration::from_millis(500));
led.off();
std::thread::sleep(std::time::Duration::from_millis(500));
}
Ok(())
}
第八部分:直接内存映射(极致性能)
手动操作寄存器,获得最高性能,但风险最高。
8.1 实现GPIO寄存器映射
use std::{fs::File, os::unix::io::AsRawFd};
use std::ptr::copy_nonoverlapping;
use std::slice;
// 使用libc进行mmap
use libc::{mmap, munmap, MAP_SHARED, PROT_READ, PROT_WRITE};
struct GpioRegisters {
base: *mut u32,
size: usize,
}
impl GpioRegisters {
// 树莓派3/4外设基址
const PERIPH_BASE: usize = 0x3F000000;
const GPIO_BASE_OFFSET: usize = 0x200000;
const PAGE_SIZE: usize = 4096;
// 寄存器偏移
const GPFSEL0: usize = 0x00;
const GPFSEL1: usize = 0x04;
const GPFSEL2: usize = 0x08;
const GPSET0: usize = 0x1C;
const GPCLR0: usize = 0x28;
const GPLEV0: usize = 0x34;
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let fd = File::open("/dev/mem")?;
let ptr = unsafe {
mmap(
std::ptr::null_mut(),
Self::PAGE_SIZE,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd.as_raw_fd(),
(Self::PERIPH_BASE + Self::GPIO_BASE_OFFSET) as libc::off_t,
)
};
if ptr == libc::MAP_FAILED {
return Err("内存映射失败".into());
}
Ok(Self {
base: ptr as *mut u32,
size: Self::PAGE_SIZE,
})
}
fn reg(&self, offset: usize) -> &mut u32 {
unsafe { &mut *self.base.add(offset / 4) }
}
pub fn set_mode(&self, pin: u32, mode: u32) {
if pin > 53 {
return;
}
let reg = pin / 10;
let shift = (pin % 10) * 3;
let mask = !(0b111 << shift);
let reg_offset = match reg {
0 => Self::GPFSEL0,
1 => Self::GPFSEL1,
2 => Self::GPFSEL2,
_ => return,
};
let r = self.reg(reg_offset);
*r = (*r & mask) | (mode << shift);
}
pub fn set_output(&self, pin: u32) {
self.set_mode(pin, 0b001);
}
pub fn set_input(&self, pin: u32) {
self.set_mode(pin, 0b000);
}
pub fn write(&self, pin: u32, value: bool) {
if value {
self.reg(Self::GPSET0).set_value(1 << pin);
} else {
self.reg(Self::GPCLR0).set_value(1 << pin);
}
}
pub fn read(&self, pin: u32) -> bool {
(self.reg(Self::GPLEV0).get_value() & (1 << pin)) != 0
}
}
impl Drop for GpioRegisters {
fn drop(&mut self) {
unsafe {
munmap(self.base as *mut libc::c_void, self.size);
}
}
}
// 为u32添加位操作方法
trait BitOps {
fn set_value(&mut self, bits: u32);
fn get_value(&self) -> u32;
}
impl BitOps for u32 {
fn set_value(&mut self, bits: u32) {
*self = bits;
}
fn get_value(&self) -> u32 {
*self
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let gpio = GpioRegisters::new()?;
// BCM 17
gpio.set_output(17);
println!("直接内存映射控制LED");
for i in 0..10 {
gpio.write(17, true);
println!("[{}] 点亮", i + 1);
std::thread::sleep(std::time::Duration::from_millis(500));
gpio.write(17, false);
println!("[{}] 熄灭", i + 1);
std::thread::sleep(std::time::Duration::from_millis(500));
}
Ok(())
}
编译配置(需要在树莓派上运行):
[dependencies]
libc = "0.2"
cargo build --release
sudo ./target/release/gpio_mmap
第九部分:实战项目
9.1 四位数码管(TM1637)
use rppal::gpio::{Gpio, Level};
use std::{thread, time::Duration};
struct Tm1637 {
clk: rppal::gpio::OutputPin,
dio: rppal::gpio::OutputPin,
}
impl Tm1637 {
pub fn new(gpio: &Gpio, clk_pin: u8, dio_pin: u8) -> Result<Self, Box<dyn std::error::Error>> {
let clk = gpio.get(clk_pin)?.into_output();
let dio = gpio.get(dio_pin)?.into_output();
Ok(Tm1637 { clk, dio })
}
fn start(&mut self) {
self.dio.set_high();
self.clk.set_high();
self.dio.set_low();
}
fn stop(&mut self) {
self.dio.set_low();
self.clk.set_high();
self.dio.set_high();
}
fn write_byte(&mut self, data: u8) -> bool {
for i in 0..8 {
self.clk.set_low();
if (data >> i) & 1 == 1 {
self.dio.set_high();
} else {
self.dio.set_low();
}
self.clk.set_high();
thread::sleep(Duration::from_micros(1));
}
// 等待ACK
self.clk.set_low();
self.dio.set_high();
self.clk.set_high();
// 简单ACK检测(简化)
true
}
pub fn display(&mut self, digits: &[u8]) {
const SEG_TABLE: [u8; 16] = [
0x3F, 0x06, 0x5B, 0x4F, 0x66,
0x6D, 0x7D, 0x07, 0x7F, 0x6F,
0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71,
];
self.start();
self.write_byte(0x40); // 自动地址模式
self.stop();
self.start();
self.write_byte(0xC0); // 从地址0开始
for i in 0..digits.len().min(4) {
let idx = digits[i] as usize;
self.write_byte(SEG_TABLE[idx]);
}
self.stop();
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let gpio = Gpio::new()?;
let mut display = Tm1637::new(&gpio, 23, 24)?;
println!("显示数字");
let digits = [1, 2, 3, 4];
display.display(&digits);
thread::sleep(Duration::from_secs(5));
Ok(())
}
第十部分:编译优化与最佳实践
10.1 编译优化
在 Cargo.toml 中添加release优化:
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
10.2 使用cross进行交叉编译
若在非ARM机器上开发,使用 cross 编译:
cargo install cross
cross build --release --target armv7-unknown-linux-gnueabihf
10.3 最佳实践
- 始终使用
sudo运行:访问GPIO需要超级用户权限 - 使用RAII管理资源:确保GPIO资源自动释放
- 添加错误处理:使用
Result处理可能的硬件错误 - 使用线程安全结构:多线程场景使用
Arc<Mutex<>> - 优先使用rppal:功能最全,性能最强
附录:编译命令速查
| 场景 | 命令 |
|---|---|
| 本地编译运行 | cargo build --release && sudo ./target/release/program |
| 仅编译 | cargo build --release |
| 交叉编译(arm) | cross build --release --target armv7-unknown-linux-gnueabihf |
| 代码检查 | cargo check |
| 运行测试 | cargo test |
附录:常用BCM引脚对照表
| BCM | 物理引脚 | BCM | 物理引脚 |
|---|---|---|---|
| 2 | 3 | 3 | 5 |
| 4 | 7 | 14 | 8 |
| 15 | 10 | 17 | 11 |
| 18 | 12 | 27 | 13 |
| 22 | 15 | 23 | 16 |
| 24 | 18 | 10 | 19 |
| 9 | 21 | 25 | 22 |
| 11 | 23 | 8 | 24 |
| 7 | 26 | 0 | 27 |
| 1 | 28 | 5 | 29 |
| 6 | 31 | 12 | 32 |
| 13 | 33 | 19 | 35 |
| 16 | 36 | 26 | 37 |
| 20 | 38 | 21 | 40 |
掌握Rust GPIO编程后,你可以构建高性能、内存安全的硬件控制应用。开始你的Rust嵌入式之旅吧!

320

被折叠的 条评论
为什么被折叠?



