BBB-Rust
- Make sure you have completed the following chapters:
We are going to build our a crate for BBB, probably the first for many of you!
1. Initialise the crate
-
In the terminal, remember to be in a
sshsession on the beaglebone and run the following: -
cdinto the newly created cratebbb_io. In this directory you should see the following if you runls: -
Lets start off with our first modification, use
nano Cargo.tomland you should see the following:modify like this:
[package] name = "bbb_io" version = "0.1.0" edition = "2024" [lib] name = "bbb_io" path = "src/lib.rs"Cargo.tomlis 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. -
Save and exit, and reproduce the following directory structure:
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 -
Modify the
lib.rsso that it provides
2. error.rs
-
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.rslike this: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}"), } } }pub type BbbResult<T> = Result<T, BbbError>;- Rust functions oftern return
Result<T,E>T= the successful resultE= the error type
- This line creates a shortcut:
- Instead of writing
Result<T, BbbError>everywhere, we writeBbbResult<T>
- Instead of writing
- Read more here
- Rust functions oftern return
-
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 valueSo instead of returning just
-1, you return meaningful information.
-
-
-
Next we have allowed for automatic conversions using
Fromas a wrapper aroundio:errorimpl 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
- This allows Rust to automatically convert:
-
In the next
implwe implement the functionality to display the errors tostdErrimpl 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
-
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/... -
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 ┘ -
So we need to build our
sysfs.rsuse 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)), } }-
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:
&strPathPathBuf
-
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
- Reads a file and returns its contents as a
-
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
-
-
We are now going to try and build a our library, run
cargo build- there should be no errors. -
Now you can move on to Rust_Crate/GPIO