The Missing Bit

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).