Rust之树莓派GPIO篇:从零开始掌控硬件

Rust之树莓派GPIO篇:从零开始掌控硬件

在树莓派上使用Rust进行GPIO编程,兼顾了极致性能(接近C++)与内存安全(无GC、无数据竞争)。Rust生态提供了从底层直接操作寄存器到高层设备抽象的多层库选型,本文带你全面掌握。


第一部分:核心库选型

Rust生态中,针对树莓派GPIO有几种主流选择:

库名方式优点适用场景
rppal直接访问 /dev/gpiomem 寄存器性能最强,功能最全(GPIO/I2C/PWM/SPI/UART)绝大多数场景,首选
gpio-cdevLinux 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物理引脚
2335
47148
15101711
18122713
22152316
24181019
9212522
1123824
726027
128529
6311232
13331935
16362637
20382140

掌握Rust GPIO编程后,你可以构建高性能、内存安全的硬件控制应用。开始你的Rust嵌入式之旅吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王小玗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值