Accessing GPIO on the beaglebone black with rust
Published: 2019-09-09 18:51:05


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.

```rust

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:

```rust
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.

```rust
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::Input => bits & !(1 << bit_index),
            PinMode::Output => bits | (1 << bit_index),
        };
        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:

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