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

BBB-Rust

Warning

We are going to build our a crate for BBB, probably the first for many of you!

1. Initialise the crate

  1. In the terminal, remember to be in a ssh session on the beaglebone and run the following:

    Terminal

    cargo init bbb_io
    

    Success

        Creating binary (application) package
    note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
    
  2. cd into the newly created crate bbb_io. In this directory you should see the following if you run ls:

    Output

    Cargo.toml  src
    
  3. Lets start off with our first modification, use nano Cargo.toml and you should see the following:

    Code

    [package]
    name = "bbb_io"
    version = "0.1.0"
    edition = "2024"
    
    [dependencies]
    

    modify like this:

    Code

    [package]
    name = "bbb_io"
    version = "0.1.0"
    edition = "2024"
    
    [lib]
    name = "bbb_io"
    path = "src/lib.rs"
    

    Note

    Cargo.toml is the manifest file for Rust’s package manager, cargo . This file contains metadata such as name, version, and dependencies for packages, which are call “crates” in Rust.

    We have removed [dependencies] as we will not be using any third-party crate. We have told the cargo that we are building a library.

  4. Save and exit, and reproduce the following directory structure:

    Terminal

    bbb_io/
    ├── Cargo.toml
    ├── src/
    │   ├── adc.rs                    # ADC raw value, voltage, buffer helpers
    │   ├── error.rs                  # simple crate error type
    │   ├── gpio.rs                   # GPIO open/read/write/close
    │   ├── gpiod.rs                  # GPIO open/read/write/close
    │   ├── lib.rs                    # public crate API, equivalent to libioctrl.h
    │   ├── pin.rs                    # Pin map
    │   ├── pwm.rs                    # PWM init/period/duty/enable/disable
    │   └── sysfs.rs                  # shared read/write helpers
    ├── examples/
    │   ├── blink.rs                  
    │   ├── adc_read.rs               
    │   ├── adc_continous_read.rs     
    │   ├── pwm_test.rs               
    │   └── adc2pwm.rs                
    
  5. Modify the lib.rs so that it provides

    Code

    pub mod adc;
    pub mod error;
    pub mod gpio;
    pub mod pin;
    pub mod pwm;
    pub mod sysfs;
    
    pub use error::{BbbError, BbbResult};
    

2. error.rs

  1. Something we didn’t do in the C implementation, which is something really nice to implement in rust is Error types. We are going to modify the src/error.rs like this:

    Code

    use std::fmt;
    use std::io;
    
    pub type BbbResult<T> = Result<T, BbbError>;
    
    #[derive(Debug)]
    pub enum BbbError {
        Io(io::Error),
        InvalidPin(String),
        InvalidValue(String),
    }
    
    impl From<io::Error> for BbbError {
        fn from(err: io::Error) -> Self {
            BbbError::Io(err)
        }
    }
    
    impl fmt::Display for BbbError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            match self {
                BbbError::Io(err) => write!(f, "I/O error: {err}"),
                BbbError::InvalidPin(pin) => write!(f, "invalid pin: {pin}"),
                BbbError::InvalidValue(value) => write!(f, "invalid value: {value}"),
            }
        }
    }
    

    Explanation

    • pub type BbbResult<T> = Result<T, BbbError>;
      • Rust functions oftern return Result<T,E>
        • T = the successful result
        • E = the error type
      • This line creates a shortcut:
        • Instead of writing Result<T, BbbError> everywhere, we write BbbResult<T>
      • Read more here

    • Here we define possible errors (this will grow as the library develops):

      pub enum BbbError {
          Io(io::Error),
          InvalidPin(String),
          InvalidValue(String),
      }
      
      • Each line is a different kind of error:

        • Io(io::Error) -> something went wrong with the system (file not found, permission denied, etc.)

        • InvalidPin(String) -> the user gave a bad pin name

        • InvalidValue(String) -> the user gave a bad value

          So instead of returning just -1, you return meaningful information.


    • Next we have allowed for automatic conversions using From as a wrapper around io:error

      impl From<io::Error> for BbbError {
          fn from(err: io::Error) -> Self {
              BbbError::Io(err)
          }
      }
      
      • This allows Rust to automatically convert:
        • io::Error -> BbbError
        • This is what makes the ? operator work seamlessly
        • Without this, we would need to manually convert errors every time

    • In the next impl we implement the functionality to display the errors to stdErr

      impl fmt::Display for BbbError {
          fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
              match self {
                  BbbError::Io(err) => write!(f, "I/O error: {err}"),
                  BbbError::InvalidPin(pin) => write!(f, "invalid pin: {pin}"),
                  BbbError::InvalidValue(value) => write!(f, "invalid value: {value}"),
              }
          }
      }
      

      For example:

      • BbbError::InvalidPin("P9_99".into()) -> println!("{}", error); -> invalid pin: P9_99

    • The attribute #[derive(Debug)] enables debug printing:

      • println!("{:?}", error); -> InvalidPin("P9_99")
      • Note:
        • {} -> uses Display (user-friendly)
        • {:?} -> uses Debug (developer-focused)

3. sysfs.rs

  1. As a reminder that the BeagleBone Black file system is like this:

    /sys/class/gpio/gpio60/value  # if sysfs is used
    /sys/class/pwm/...
    /sys/bus/iio/...
    
  2. What we are going to do is have central behaviour, instead of repeating file handling everywhere:

    adc.rs   ┐
             ├── -> sysfs.rs -> std::fs -> Linux kernel
    pwm.rs   ┘
    
  3. So we need to build our sysfs.rs

    Code

    use std::fs;
    use std::path::Path;
    
    use crate::BbbResult;
    
    /// Write a string value to a sysfs file.
    pub fn write_string(path: impl AsRef<Path>, value: &str) -> BbbResult<()> {
        fs::write(path, value)?;
        Ok(())
    }
    
    /// Read a string value from a sysfs file.
    ///
    /// Sysfs files usually include a trailing newline, so this trims it.
    pub fn read_string(path: impl AsRef<Path>) -> BbbResult<String> {
        Ok(fs::read_to_string(path)?.trim().to_string())
    }
    
    /// Write an unsigned integer to a sysfs file.
    pub fn write_u32(path: impl AsRef<Path>, value: u32) -> BbbResult<()> {
        write_string(path, &value.to_string())
    }
    
    /// Read an unsigned integer from a sysfs file.
    pub fn read_u32(path: impl AsRef<Path>) -> BbbResult<u32> {
        let text = read_string(path)?;
    
        match text.parse::<u32>() {
            Ok(value) => Ok(value),
            Err(_) => Err(crate::BbbError::InvalidValue(text)),
        }
    }
    

    Explanation

    • This module provides low-level helpers for interacting with Linux sysfs.

    • On embedded Linux systems (e.g. BeagleBone Black), hardware is controlled via files:

      • Writing to a file -> sends a command to the kernel
      • Reading from a file -> retrieves state

      So in practice:

      • GPIO, ADC, PWM -> all reduce to file read/write operations

    • write_string(...)

      pub fn write_string(path: impl AsRef<Path>, value: &str) -> BbbResult<()> {
          fs::write(path, value)?;
          Ok(())
      }
      
      • Writes a string to a sysfs file

      • Equivalent to:

        echo VALUE > PATH
        

      Example:

      write_string("/sys/class/gpio/export", "60")?;
      
      • The ? operator:
        • returns early if an error occurs
        • automatically converts io::Error -> BbbError

    • Why impl AsRef<Path>?

    • Allows flexible input types:

      • &str
      • Path
      • PathBuf
    • Think of it as:

      • “anything that behaves like a file path”

    • read_string(...)

      pub fn read_string(path: impl AsRef<Path>) -> BbbResult<String> {
          Ok(fs::read_to_string(path)?.trim().to_string())
      }
      
      • Reads a file and returns its contents as a String
      • .trim() removes trailing newline (\n), which sysfs files often include

    • write_u32(...)

      pub fn write_u32(path: impl AsRef<Path>, value: u32) -> BbbResult<()> {
          write_string(path, &value.to_string())
      }
      
      • Converts a number into a string
      • Reuses write_string(...)

    • read_u32(...)

      pub fn read_u32(path: impl AsRef<Path>) -> BbbResult<u32> {
          let text = read_string(path)?;
      
          match text.parse::<u32>() {
              Ok(value) => Ok(value),
              Err(_) => Err(crate::BbbError::InvalidValue(text)),
          }
      }
      
      • Reads a string
      • Attempts to convert it into a number

    • Architectural role

           adc.rs / pwm.rs       -> what we want to do
                  v
              sysfs.rs           -> how we talk to Linux
                  v
              std::fs            -> actual file operations
                  v
              Linux kernel       -> hardware control
      
  4. We are now going to try and build a our library, run cargo build - there should be no errors.

    Terminal

    cargo build
    
  5. Now you can move on to Rust_Crate/GPIO