Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Last updated: Wednesday 29 April 2026 @ 11:05:00

GPIO

Warning

Here we are going to continue working on the Rust crate bbb_io.

In the earlier C implementation, GPIO access was performed using libgpiod . The Rust implementation in this chapter follows the same design:

Physical pin name -> chip + offset -> libgpiod -> Linux kernel -> GPIO pin

For example:

P9_12 -> gpiochip0, line 28

This is the same model used by commands such as:

gpioset gpiochip0 28=1

This GPIO chapter uses libgpiod, not sysfs.

The earlier sysfs.rs module will be used for ADC and PWM-style file interfaces, but GPIO on this system is handled through:

/dev/gpiochip0
/dev/gpiochip1
/dev/gpiochip2
/dev/gpiochip3

rather than:

/sys/class/gpio/...

1. Creating the pin.rs

The first step is to create a pin map. This is the Rust equivalent of the PINMAP[] table from the C library/GPIOlibrary.

  1. Open src/pin.rs and add the following:

    Code: pin.rs [60 lines]

    use crate::{BbbError, BbbResult};
    
    #[derive(Debug, Clone, Copy)]
    pub struct PinMap {
        pub name: &'static str,
        pub chip: u32,
        pub offset: u32,
    }
    
    const PINMAP: &[PinMap] = &[
        PinMap { name: "P8_3", chip: 0, offset: 6 },
        PinMap { name: "P8_4", chip: 0, offset: 7 },
        PinMap { name: "P8_5", chip: 0, offset: 2 },
        PinMap { name: "P8_6", chip: 0, offset: 3 },
        PinMap { name: "P8_7", chip: 1, offset: 2 },
        PinMap { name: "P8_8", chip: 1, offset: 3 },
        PinMap { name: "P8_9", chip: 1, offset: 5 },
        PinMap { name: "P8_10", chip: 1, offset: 4 },
        PinMap { name: "P8_11", chip: 0, offset: 13 },
        PinMap { name: "P8_12", chip: 0, offset: 12 },
        PinMap { name: "P8_13", chip: 3, offset: 23 },
        PinMap { name: "P8_14", chip: 3, offset: 26 },
        PinMap { name: "P8_15", chip: 0, offset: 15 },
        PinMap { name: "P8_16", chip: 0, offset: 14 },
        PinMap { name: "P8_17", chip: 3, offset: 27 },
        PinMap { name: "P8_18", chip: 1, offset: 1 },
        PinMap { name: "P8_19", chip: 3, offset: 22 },
        PinMap { name: "P8_20", chip: 0, offset: 31 },
        PinMap { name: "P8_21", chip: 0, offset: 30 },
        PinMap { name: "P8_22", chip: 0, offset: 5 },
        PinMap { name: "P8_23", chip: 0, offset: 4 },
        PinMap { name: "P8_24", chip: 1, offset: 1 },
        PinMap { name: "P8_25", chip: 0, offset: 0 },
        PinMap { name: "P8_26", chip: 0, offset: 29 },
        PinMap { name: "P8_35", chip: 3, offset: 8 },
        PinMap { name: "P9_11", chip: 3, offset: 30 },
        PinMap { name: "P9_12", chip: 0, offset: 28 },
        PinMap { name: "P9_13", chip: 3, offset: 31 },
        PinMap { name: "P9_15", chip: 0, offset: 16 },
        PinMap { name: "P9_17", chip: 3, offset: 5 },
        PinMap { name: "P9_18", chip: 3, offset: 4 },
        PinMap { name: "P9_22", chip: 3, offset: 2 },
        PinMap { name: "P9_23", chip: 0, offset: 17 },
        PinMap { name: "P9_24", chip: 3, offset: 15 },
        PinMap { name: "P9_25", chip: 2, offset: 21 },
        PinMap { name: "P9_26", chip: 3, offset: 14 },
        PinMap { name: "P9_27", chip: 2, offset: 19 },
        PinMap { name: "P9_29", chip: 2, offset: 15 },
        PinMap { name: "P9_30", chip: 2, offset: 16 },
        PinMap { name: "P9_31", chip: 2, offset: 14 },
        PinMap { name: "P9_41", chip: 2, offset: 20 },
    ];
    
    pub fn lookup(pin: &str) -> BbbResult<PinMap> {
        PINMAP
            .iter()
            .copied()
            .find(|entry| entry.name == pin)
            .ok_or_else(|| BbbError::InvalidPin(pin.to_string()))
    }
    

    Explanation

    The PinMap struct

    pub struct PinMap {
        pub name: &'static str,
        pub chip: u32,
        pub offset: u32,
    }
    
    • This struct stores one entry in the pin table.

      • name

        • the BeagleBone header pin name, for example "P9_12"
      • chip

        • which /dev/gpiochipN device to use
        • chip: 0 means /dev/gpiochip0
      • offset

        • the line number inside that GPIO chip
        • offset: 28 means line 28
    • So:

      PinMap { name: "P9_12", chip: 0, offset: 28 }
      
    • which means:

      P9_12 -> /dev/gpiochip0, line 28
      

    Why &'static str?

    • The pin names are fixed strings written directly into the program.

      pub name: &'static str
      
      • 'static means:

        this string lives for the whole lifetime of the program

        This is appropriate because entries like "P9_12" are stored in the compiled binary.

        Read more:


    Why #[derive(Debug, Clone, Copy)]?

    • PinMap only contains a string reference and integers, so it is cheap and safe to copy.

      #[derive(Debug, Clone, Copy)]
      

      This asks the compiler to automatically implement useful behaviour.

      • Debug

        • allows developer-style printing:

          println!("{:?}", pin);
          
      • Clone

        • allows explicit copying:

          let b = a.clone();
          
      • Copy

        • allows automatic copying for small simple values

      Read more:


    The pin table

    • The following creates a fixed, read-only list of pins.

      const PINMAP: &[PinMap] = &[ ... ];
      
      • const

        • the value is known at compile time
      • &[PinMap]

        • a slice of PinMap entries
    • This is similar in purpose to the C table:

      static const PinMap PINMAP[] = { ... };
      

    The lookup function

    • This function searches the table for a pin name.

      pub fn lookup(pin: &str) -> BbbResult<PinMap>
      
      • Example:

        let pin = pin::lookup("P9_12")?;
        
        • If the pin exists, it returns:

          Ok(PinMap { name: "P9_12", chip: 0, offset: 28 })
          
        • If the pin does not exist, it returns:

          Err(BbbError::InvalidPin(...))
          

    Understanding the iterator chain

    • Here we are refering to:

      PINMAP
          .iter()
          .copied()
          .find(|entry| entry.name == pin)
      
      • This means:

        1. .iter()

          • visit each entry in the table
        2. .copied()

          • copy the PinMap value out of the table
        3. .find(...)

          • return the first entry where the condition is true
      • The condition is:

        entry.name == pin
        

        So the table is searched until it finds the requested pin.

      Read more:


    Why ok_or_else(...)?

    • Another very useful feature in rust is ok_or_else()

      .ok_or_else(|| BbbError::InvalidPin(pin.to_string()))
      
      • find(...) returns an Option:

        Some(pin)    // found
        None         // not found
        
        • But our function returns a Result:

          Ok(pin)
          Err(error)
          
        • ok_or_else(...) converts:

          Some(value) -> Ok(value)
          None        -> Err(...)
          

      Read more:

2. GPIOD

  1. Next we create the low-level libgpiod interface.

    Note

    This is the most advanced part of this chapter because Rust is calling functions from a C library libgpiod.

    There is a rust implmentation gpiod, in the future you may want to convert to this one. We are not so you can see how rust can use c too.

  2. Create src/gpiod.rs:

    Code: gpiod.rs [178 lines]

    use std::ffi::CString;
    use std::os::raw::{c_char, c_int};
    
    use crate::{pin, BbbError, BbbResult};
    
    #[repr(C)]
    struct gpiod_chip {
        _private: [u8; 0],
    }
    
    #[repr(C)]
    struct gpiod_line {
        _private: [u8; 0],
    }
    
    #[link(name = "gpiod")]
    unsafe extern "C" {
        fn gpiod_chip_open(path: *const c_char) -> *mut gpiod_chip;
        fn gpiod_chip_close(chip: *mut gpiod_chip);
    
        fn gpiod_chip_get_line(chip: *mut gpiod_chip, offset: c_int) -> *mut gpiod_line;
    
        fn gpiod_line_request_input(
            line: *mut gpiod_line,
            consumer: *const c_char,
        ) -> c_int;
    
        fn gpiod_line_request_output(
            line: *mut gpiod_line,
            consumer: *const c_char,
            default_val: c_int,
        ) -> c_int;
    
        fn gpiod_line_get_value(line: *mut gpiod_line) -> c_int;
        fn gpiod_line_set_value(line: *mut gpiod_line, value: c_int) -> c_int;
    
        fn gpiod_line_release(line: *mut gpiod_line);
    }
    
    #[derive(Debug, Clone, Copy)]
    pub enum Direction {
        In,
        Out,
    }
    
    pub struct Gpio {
        chip: *mut gpiod_chip,
        line: *mut gpiod_line,
        chip_index: u32,
        offset: u32,
    }
    
    impl Gpio {
        pub fn open(pin_name: &str, direction: Direction, initial_value: u8) -> BbbResult<Self> {
            if initial_value > 1 {
                return Err(BbbError::InvalidValue(initial_value.to_string()));
            }
    
            let pin = pin::lookup(pin_name)?;
    
            let path = format!("/dev/gpiochip{}", pin.chip);
            let c_path = cstring(&path)?;
    
            let chip = unsafe { gpiod_chip_open(c_path.as_ptr()) };
    
            if chip.is_null() {
                return Err(BbbError::InvalidValue(format!(
                    "failed to open {path}"
                )));
            }
    
            let line = unsafe { gpiod_chip_get_line(chip, pin.offset as c_int) };
    
            if line.is_null() {
                unsafe {
                    gpiod_chip_close(chip);
                }
    
                return Err(BbbError::InvalidValue(format!(
                    "failed to get line {} on {path}",
                    pin.offset
                )));
            }
    
            let consumer = cstring("bbb_rust")?;
    
            let ret = unsafe {
                match direction {
                    Direction::In => gpiod_line_request_input(line, consumer.as_ptr()),
                    Direction::Out => {
                        gpiod_line_request_output(line, consumer.as_ptr(), initial_value as c_int)
                    }
                }
            };
    
            if ret < 0 {
                unsafe {
                    gpiod_chip_close(chip);
                }
    
                return Err(BbbError::InvalidValue(format!(
                    "failed to request {pin_name} as {direction:?}"
                )));
            }
    
            Ok(Self {
                chip,
                line,
                chip_index: pin.chip,
                offset: pin.offset,
            })
        }
    
        pub fn open_output(pin_name: &str, initial_value: u8) -> BbbResult<Self> {
            Self::open(pin_name, Direction::Out, initial_value)
        }
    
        pub fn open_input(pin_name: &str) -> BbbResult<Self> {
            Self::open(pin_name, Direction::In, 0)
        }
    
        pub fn write(&self, value: u8) -> BbbResult<()> {
            if value > 1 {
                return Err(BbbError::InvalidValue(value.to_string()));
            }
    
            let ret = unsafe { gpiod_line_set_value(self.line, value as c_int) };
    
            if ret < 0 {
                return Err(BbbError::InvalidValue(format!(
                    "failed to write value {value} to gpiochip{} offset {}",
                    self.chip_index, self.offset
                )));
            }
    
            Ok(())
        }
    
        pub fn read(&self) -> BbbResult<u8> {
            let ret = unsafe { gpiod_line_get_value(self.line) };
    
            if ret < 0 {
                return Err(BbbError::InvalidValue(format!(
                    "failed to read gpiochip{} offset {}",
                    self.chip_index, self.offset
                )));
            }
    
            Ok(ret as u8)
        }
    
        pub fn set_high(&self) -> BbbResult<()> {
            self.write(1)
        }
    
        pub fn set_low(&self) -> BbbResult<()> {
            self.write(0)
        }
    }
    
    impl Drop for Gpio {
        fn drop(&mut self) {
            unsafe {
                if !self.line.is_null() {
                    gpiod_line_release(self.line);
                }
    
                if !self.chip.is_null() {
                    gpiod_chip_close(self.chip);
                }
            }
        }
    }
    
    fn cstring(value: &str) -> BbbResult<CString> {
        CString::new(value)
            .map_err(|_| BbbError::InvalidValue(format!("string contains NUL byte: {value:?}")))
    }
    

    Example

    This file is a safe Rust wrapper around unsafe C functions.

    The C library already knows how to talk to the Linux GPIO character device API. Rust does not call libgpiod automatically, so we declare the C functions we want to use.


    Imports

    • Another useful rust feature is the Foreign Function Interface

      use std::ffi::CString;
      use std::os::raw::{c_char, c_int};
      
      • CString

        • converts Rust strings into C-compatible strings
      • c_char

        • Rust representation of C char
      • c_int

        • Rust representation of C int

      These are needed because libgpiod is a C library.

      Read more:


    Opaque C structs

    • Rust does not need to know what is inside these structs. It only needs to hold pointers to them.

    • That is why we define them as opaque types and use repr (representation) attribute to control how structs and enums are laid out in memory.

      #[repr(C)]
      struct gpiod_chip {
          _private: [u8; 0],
      }
      

      and:

      #[repr(C)]
      struct gpiod_line {
          _private: [u8; 0],
      }
      
    • In C, libgpiod provides types such as:

      struct gpiod_chip;
      struct gpiod_line;
      
    • #[repr(C)]

      • tells Rust to use C-compatible layout
    • _private: [u8; 0]

      • prevents users from constructing the type directly
      • communicates that the internal layout is hidden

    Read more:


    Linking to libgpiod

    • The #[link] attribute used in FFI (Foreign Function Interface) to specify native libraries to link against

      #[link(name = "gpiod")]
      
      • This tells Rust to link against:

        libgpiod.so
        
      • This is equivalent to using the compiler flag:

        -l gpiod
        

    The unsafe extern block

    • unsafe in is a keyword that unlocks “superpowers” allowing developers to bypass compiler safety checks for low-level tasks like dereferencing raw pointers, accessing mutable statics, implementing unsafe traits or in our case calling FFI functions:

      unsafe extern "C" {
          fn gpiod_chip_open(...);
          ...
      }
      
      • This declares C functions that Rust is allowed to call.

        • extern "C" means:

          use the C calling convention

        • unsafe means:

          Rust cannot guarantee these functions are safe

        • This is expected when calling C libraries.

      Read more:


    Raw pointers

    • Like C, rust can use pointers * and addressing, though they are called raw pointers and these operations must be placed inside unsafe

      *mut gpiod_chip
      
      • Rust normally avoids raw pointers, but they are required when interacting with C.

      • The important rule is:

        raw pointer operations must be placed inside unsafe


    Direction enum

    • enums give you a way of saying a value is one of a possible set of values

      pub enum Direction {
          In,
          Out,
      }
      
      • This is a safer replacement for passing arbitrary integers.

      • Instead of:

        output = 1
        output = 0
        
      • Rust code can say:

        Direction::Out
        Direction::In
        
      • This makes code easier to read and reduces invalid states.

      Read more:


    The Gpio struct

    • The strucs in rust are dedined differently to that in C:

      pub struct Gpio {
          chip: *mut gpiod_chip,
          line: *mut gpiod_line,
          chip_index: u32,
          offset: u32,
      }
      

      as seen here in C:

      typedef struct {
          struct gpiod_chip *chip;
          struct gpiod_line *line;
          unsigned chip_index;
          unsigned offset;
      } BbbGpio;
      

      It stores:

      • the opened chip pointer
      • the requested line pointer
      • the chip index
      • the offset

      This means a Gpio value represents an open GPIO resource.


    Why are there null checks?

    • C functions often signal failure by returning NULL.

    • Rust does not use NULL in safe code, so we check:

      if chip.is_null()
      

      and:

      if line.is_null()
      
      • If either call fails, we convert it into a Rust error.

      • This is one of the main roles of gpiod.rs:

        convert unsafe C-style errors into safe Rust Result values


    Writing a value

    pub fn write(&self, value: u8) -> BbbResult<()>
    

    This writes 0 or 1 to an already-open GPIO line.

    Why &self?

    • The GPIO handle already owns the line
    • Writing does not need to move or destroy the handle
    • We can call write repeatedly

    Example:

    led.write(1)?;
    led.write(0)?;
    

    Reading a value

    pub fn read(&self) -> BbbResult<u8>
    

    This reads the current value of the line.

    If libgpiod returns a negative value, we treat it as an error.


    Convenience methods

    pub fn set_high(&self) -> BbbResult<()> {
        self.write(1)
    }
    
    pub fn set_low(&self) -> BbbResult<()> {
        self.write(0)
    }
    

    These improve readability.

    Instead of:

    led.write(1)?;
    

    we can write:

    led.set_high()?;
    

    Drop and RAII

    impl Drop for Gpio {
        fn drop(&mut self) {
            ...
        }
    }
    

    This is one of the most important Rust ideas in this chapter.

    In C, the programmer must remember to close resources manually:

    bbb_close(h);
    

    In Rust, we attach cleanup to the lifetime of the value.

    When Gpio goes out of scope, Rust automatically calls drop.

    This releases:

    • the GPIO line
    • the GPIO chip

    This pattern is called RAII:

    Resource Acquisition Is Initialisation
    

    It means:

    create object -> acquire resource
    object ends   -> release resource
    

    Read more:

    Drop trait


    Why keep unsafe code here?

    Only gpiod.rs contains unsafe FFI calls.

    The rest of the crate can use safe functions:

    led.set_high()?;
    

    This is a common Rust systems pattern:

    unsafe low-level wrapper
    safe high-level API
    

3. gpio.rs

  1. Now create the public GPIO API layer. This file is intentionally small as it acts as the public user-facing GPIO module.

  2. Open src/gpio.rs and add:

    Code: gpio.rs [27 lines]

    pub use crate::gpiod::Direction;
    
    use crate::gpiod::Gpio;
    use crate::BbbResult;
    
    pub fn open_output(pin: &str, initial_value: u8) -> BbbResult<Gpio> {
        Gpio::open_output(pin, initial_value)
    }
    
    pub fn open_input(pin: &str) -> BbbResult<Gpio> {
        Gpio::open_input(pin)
    }
    
    pub fn set_high(pin: &str) -> BbbResult<()> {
        let gpio = Gpio::open_output(pin, 1)?;
        gpio.set_high()
    }
    
    pub fn set_low(pin: &str) -> BbbResult<()> {
        let gpio = Gpio::open_output(pin, 0)?;
        gpio.set_low()
    }
    
    pub fn read(pin: &str) -> BbbResult<u8> {
        let gpio = Gpio::open_input(pin)?;
        gpio.read()
    }
    

    Explanation

    Re-exporting Direction

    • The Direction is available through gpio.

      pub use crate::gpiod::Direction;
      
      • So you can write, when using it in an implementation:

        use bbb_io::gpio::Direction;
        
      • instead of:

        use bbb_io::gpiod::Direction;
        
      • This keeps the public API tidier.

      Read more:


    Opening output

    • This forwards the request to the lower-level Gpio wrapper.

      pub fn open_output(pin: &str, initial_value: u8) -> BbbResult<Gpio> {
          Gpio::open_output(pin, initial_value)
      }
      
    • Example:

      let led = gpio::open_output("P9_12", 0)?;
      
    • This means:

      open P9_12 as an output pin and start LOW
      

    One-shot helpers

    • These are convenient, but they open and close the line each time.

      pub fn set_high(pin: &str) -> BbbResult<()>
      

      and:

      pub fn set_low(pin: &str) -> BbbResult<()>
      
      • That means this:

        gpio::set_high("P9_12")?;
        

        does:

        open -> request -> write HIGH -> drop -> release
        

    Why return BbbResult<Gpio>?

    • Opening a GPIO can fail:

      • invalid pin name
      • missing /dev/gpiochipN
      • permission denied
      • line already in use
    • So the function returns:

      BbbResult<Gpio>
      

      rather than assuming success.

4. Build

  1. Run:

    cargo clean && cargo build
    

    If successful, you should see a compiled library under:

    target/debug/libbbb_io.rlib
    

    The libbbb_io.rlib is the Rust library that our standalone scripts can link against.

    This is similar in purpose to a C static library such as:

    libioctrl.a
    

5. Example: blink.rs

Now we can make our blink program in rust referencing the libbbb_io.rlib

  1. Create an examples directory if it does not already exist:

    mkdir -p examples
    
  2. Create examples/blink.rs:

    Code

    use std::thread;
    use std::time::Duration;
    
    use bbb_io::gpio;
    
    fn main() -> bbb_io::BbbResult<()> {
        let led = gpio::open_output("P9_12", 0)?;
    
        loop {
            println!("P9_12 HIGH");
            led.set_high()?;
            thread::sleep(Duration::from_millis(500));
    
            println!("P9_12 LOW");
            led.set_low()?;
            thread::sleep(Duration::from_millis(500));
        }
    }
    
  3. Save and exit the script. Once done you can compile the rust program like this:

    Terminal

    rustc examples/blink.rs \
    --edition=2021 \
    -L target/debug \
    --extern bbb_io=target/debug/libbbb_io.rlib \
    -l gpiod \
    -o blink
    

    Explanation

    • --edition=2021

      • Tells the compiler which Rust language edition to use

      Read more:

    • -L target/debug

      • -L = “library search path”

        • This tells the compiler:

          Look in target/debug/ for compiled libraries
          
        • Why we need this:

          • We already ran cargo build

          • That produced:

            target/debug/libbbb_io.rlib
            

            Without -L, the compiler would not know where to find it.

    • --extern bbb_io=target/debug/libbbb_io.rlib

      • This links our crate into the program.

        bbb_io = name used in `use bbb_io::...`
        
        • So this line means:

          When code says `use bbb_io`, use this compiled library file
          

          This is the manual version of Cargo dependency resolution.

    • -l gpiod

      • This links against a system C library:

        libgpiod.so
        
        • Why we need this:

          • Our Rust code uses FFI:

            #![allow(unused)]
            fn main() {
            unsafe extern "C" {
                fn gpiod_chip_open(...);
            }
            }
          • These functions are implemented in C, not Rust

            So we must tell the linker:

            "Include the libgpiod library"
            

            Equivalent to:

            gcc ... -lgpiod
            
    • -o blink

      • -o = output file name

        This produces in the current directory:

        ./blink
        
    • So now we have:

      blink.rs (our program)
          v
      bbb_io (our Rust library)
          v
      gpiod.rs (FFI layer)
          v
      libgpiod (C library)
          v
      Linux kernel
          v
      Hardware (GPIO pin)
      
  4. Wire up the circuit, remember we have an active-high pinout on P9_12 (see figure below).

    Figure: Active High LED cirucit

    As a reminder here is the bbb headers map

    BBB Headers

  5. Now run the script with ./blink, remember ./ is telling the shell to run the excutable, I am in the same directory as the executable.

Success

If you got here, then you have made your first crate and used it!