Accessing GPIO on the beaglebone black with rust
For a small project, I needed to be able to control some GPIO with Linux. I
had some experience with the raspberry pi, but I wanted something more open
(hardware wise). I tried the beaglebone black wireless, and was pleased with the
experience. I had a minor issue, is that accessing GPIO with sysfs
was slow
(like 3Khz), and I wanted much faster GPIO.
I toyed a bit with it, and found a lower level solution I explain in this post. It's in rust because I am learning the language, but it's low level, so you should be able to do it in C.
Register overview
To access the GPIO at the hardware level, we need to access some registers. On the beaglebone black wireless uses an AM335x serie processor. To get the documentation, you need to download the Technical Reference Manual from texas instrument, the document number is SPRUH73P.
The first step is to find where the GPIO registers live in memory. For this, we
need to look at the ARM Cortex-A8 Memory Map table. If we look in the table,
there are multiple GPIO registers, the first one, GPIO0 lives at the address
0x44E0_7000
and is 4KB long. Each GPIO registers has the same layout and
are documented in the GPIO Registers section of the document. There are
numerous sub-registers. What interest us most is the GPIO_OE
which enables
output, GPIO_DATAIN
for reading data and GPIO_DATAOUT
for writing data.
Memory mapping
To access the registers from Linux, we need to access the memory directly. The
way to do this in Linux is to access /dev/mem
.
The following rust code map /dev/mem
in memory for direct access to the
registers.
const GPIO_BASE_REGISTERS: [off_t; 3] = [0x44E0_7000, 0x4804_C000, 0x481A_C000];
const GPIO_REGISTER_SIZE: size_t = 0xFFF;
const GPIO_OE_REGISTER: isize = 0x134;
const GPIO_DATAOUT_REGISTER: isize = 0x13C;
const GPIO_DATAIN_REGISTER: isize = 0x138;
let index = 0;
let path = CString::new("/dev/mem").unwrap();
let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDWR) };
if fd < 0 {
panic!("Cannot open memory device");
}
let base = unsafe {
libc::mmap(
std::ptr::null_mut(),
GPIO_REGISTER_SIZE,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_SHARED,
fd,
GPIO_BASE_REGISTERS[index],
)
};
if base.is_null() {
panic!("Cannot map GPIO");
}
let oe: *mut u32 = unsafe { base.offset(GPIO_OE_REGISTER) as *mut u32 };
let dataout: *mut u32 =
unsafe { base.offset(GPIO_DATAOUT_REGISTER) as *mut u32 };
let datain: *mut u32 =
unsafe { base.offset(GPIO_DATAIN_REGISTER) as *mut u32 };
This code can access the three GPIO registers (by changing index
). From here,
we can access the three sub-registers as u32
.
Accessing the registers
If we access oe
, dataout
, datain
the hardware will pickup the changes
immediatly, but we need to ensure the compiler doesn't do anything smart with
out access. In C we need to use the volatile keyword when declaring the
variables. In rust we can use std::ptr::read_volatile
and
std::ptr::write_volatile
.
For example, doing:
std::ptr::write_volatile(oe, 0);
std::ptr::write_volatile(dataout, 0xffffffff);
Would enable output on all GPIO pins and set them all to HIGH
.
Sample code
For my needs, I wrote a quick&dirty library.
use libc::{c_int, c_void, off_t, size_t};
use std::ffi::CString;
const GPIO_BASE_REGISTERS: [off_t; 3] = [0x44E0_7000, 0x4804_C000, 0x481A_C000];
const GPIO_REGISTER_SIZE: size_t = 0xFFF;
const GPIO_OE_REGISTER: isize = 0x134;
const GPIO_DATAOUT_REGISTER: isize = 0x13C;
const GPIO_DATAIN_REGISTER: isize = 0x138;
#[derive(Debug)]
pub enum PinValue {
Low = 0,
High = 1,
}
#[derive(Debug)]
pub enum PinMode {
Output,
Input,
}
#[derive(Debug)]
pub enum BitOrder {
LSBFirst,
MSBFirst,
}
pub struct Gpio {
fd: c_int,
base: *mut c_void,
oe: *mut u32,
dataout: *mut u32,
datain: *mut u32,
}
pub struct Pin<'a> {
gpio: &'a Gpio,
index: usize,
}
impl Drop for Gpio {
fn drop(&mut self) {
unsafe {
libc::munmap(self.base, GPIO_REGISTER_SIZE);
libc::close(self.fd);
};
}
}
impl Gpio {
pub fn open(index: usize) -> Gpio {
let path = CString::new("/dev/mem").unwrap();
let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDWR) };
if fd < 0 {
panic!("Cannot open memory device");
}
let base = unsafe {
libc::mmap(
std::ptr::null_mut(),
GPIO_REGISTER_SIZE,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_SHARED,
fd,
GPIO_BASE_REGISTERS[index],
)
};
if base.is_null() {
panic!("Cannot map GPIO");
}
let oe: *mut u32 = unsafe { base.offset(GPIO_OE_REGISTER) as *mut u32 };
let dataout: *mut u32 =
unsafe { base.offset(GPIO_DATAOUT_REGISTER) as *mut u32 };
let datain: *mut u32 =
unsafe { base.offset(GPIO_DATAIN_REGISTER) as *mut u32 };
Gpio {
fd,
base,
oe,
dataout,
datain,
}
}
pub fn pin_mode(&self, bit_index: usize, mode: PinMode) {
let mut bits = unsafe { std::ptr::read_volatile(self.oe) };
bits = match mode {
PinMode::Output => bits & !(1 << bit_index),
PinMode::Input => bits | (1 << bit_index),
};
unsafe { std::ptr::write_volatile(self.oe, bits) };
}
pub fn digital_write(&self, bit_index: usize, value: PinValue) {
let mut bits = unsafe { std::ptr::read_volatile(self.dataout) };
bits = match value {
PinValue::Low => bits & !(1 << bit_index),
PinValue::High => bits | (1 << bit_index),
};
unsafe { std::ptr::write_volatile(self.dataout, bits) };
}
pub fn digital_read(&self, bit_index: usize) -> PinValue {
let bits = unsafe { std::ptr::read_volatile(self.datain) };
if bits & (1 << bit_index) != 0 {
PinValue::High
} else {
PinValue::Low
}
}
}
impl Pin<'_> {
pub fn open(gpio: &Gpio, index: usize) -> Pin {
Pin { gpio, index }
}
pub fn digital_read(&self) -> PinValue {
self.gpio.digital_read(self.index)
}
pub fn digital_write(&self, value: PinValue) {
self.gpio.digital_write(self.index, value);
}
pub fn mode(&self, mode: PinMode) {
self.gpio.pin_mode(self.index, mode);
}
pub fn shift_out(
data_pin: &Pin,
clock_pin: &Pin,
bit_order: BitOrder,
value: u8,
) {
for i in 0..8 {
let value = match bit_order {
BitOrder::LSBFirst => {
if (value & (1 << i)) != 0 {
PinValue::High
} else {
PinValue::Low
}
}
BitOrder::MSBFirst => {
if (value & (1 << (7 - i))) != 0 {
PinValue::High
} else {
PinValue::Low
}
}
};
data_pin.digital_write(value);
clock_pin.digital_write(PinValue::High);
clock_pin.digital_write(PinValue::Low);
}
}
}
This can be used like this:
let gpio0 = Gpio::open(0);
let clk = Pin::open(&gpio0, 26);
clk.mode(Output);
loop {
clk.digital_write(Low);
clk.digital_write(High);
}
This would generate a signal on pin 14 of header P8 (look at the beaglebone black schematic).