GPIO
- Make sure you have completed the following chapters:
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.
-
Open
src/pin.rsand 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
PinMapstructpub 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"
- the BeagleBone header pin name, for example
-
chip- which
/dev/gpiochipNdevice to use chip: 0means/dev/gpiochip0
- which
-
offset- the line number inside that GPIO chip
offset: 28means 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-
'staticmeans: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)]?-
PinMaponly 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
PinMapentries
- a slice of
-
-
This is similar in purpose to the C table:
static const PinMap PINMAP[] = { ... };
The
lookupfunction-
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:
-
.iter()- visit each entry in the table
-
.copied()- copy the
PinMapvalue out of the table
- copy the
-
.find(...)- return the first entry where the condition is true
-
-
The condition is:
entry.name == pinSo 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 anOption: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
-
Next we create the low-level libgpiod interface.
-
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
- Rust representation of C
-
c_int- Rust representation of C
int
- Rust representation of C
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
-
unsafein 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
-
unsafemeans: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 insideunsafe*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
-
enumsgive you a way of saying a value is one of a possible set of valuespub 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
Gpiovalue represents an open GPIO resource.
Why are there null checks?
-
C functions often signal failure by returning
NULL. -
Rust does not use
NULLin 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
Resultvalues
-
Writing a value
pub fn write(&self, value: u8) -> BbbResult<()>This writes
0or1to 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
writerepeatedly
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
Gpiogoes out of scope, Rust automatically callsdrop.This releases:
- the GPIO line
- the GPIO chip
This pattern is called RAII:
Resource Acquisition Is InitialisationIt means:
create object -> acquire resource object ends -> release resourceRead more:
Why keep unsafe code here?
Only
gpiod.rscontains 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
-
Now create the public GPIO API layer. This file is intentionally small as it acts as the public user-facing GPIO module.
-
Open
src/gpio.rsand 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
Directionis available throughgpio.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
Gpiowrapper.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
-
Run:
cargo clean && cargo buildIf successful, you should see a compiled library under:
target/debug/libbbb_io.rlibThe
libbbb_io.rlibis 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
-
Create an examples directory if it does not already exist:
mkdir -p examples -
Create
examples/blink.rs: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)); } } -
Save and exit the script. Once done you can compile the rust program like this:
rustc examples/blink.rs \ --edition=2021 \ -L target/debug \ --extern bbb_io=target/debug/libbbb_io.rlib \ -l gpiod \ -o blinkExplanation
-
--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.rlibWithout
-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 fileThis 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 nameThis 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)
-
-
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

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