Beaglebone Blue is an all-in-one linux computer for robotics. Based on the Beaglebone Black with specific integrations for robotics its the perfect dev board. In this post we wrap the extensive C libraries available to make them callable from Rust.
Generate C Bindings
We need to generate C bindings to librobotcontrol. Thankfully Rust has fantastic tooling to autogenerate FFI C bindings with rust-bindgen. However we are not going to use the library feature as that requires the C libraries to be present at build. Instead we will generate the binding file ahead of time an statically link against the compiled C lib.
Boot up the beaglebone blue and login to a shell (mosh debian@beaglebone.local
).
Install bindgen.
Git clone librobotcontrol, running the following:
$ git clone git@github.com:StrawsonDesign/librobotcontrol.git
$ cd librobotcontrol
$ bindgen include/robotcontrol.h -o bindings.rs -- -Iinclude/
If successful we now have a new file bindings.rs
!
Now we have bindings lets create the library to link against. Compilation is easy and all setup with the default Beaglebone Blue distribution. Compile with make
. Then create an archive with ar
:
$ make
$ ar rcs librobotcontrol.a build/**.o
NB: librobotcontrol is already complied in your Beaglebone distribution
Copy these files back to your main computer and lets move on to compiling our Rust program against this.
Linking
To compile our C libraries in rust we will need a build script.
Placing the file build.rs
in the root of a package will cause Cargo to compile the script and execute it just before building.
With a file layout:
.
├── build.rs
├── librobotcontrol.a
├── src
│ ├── main.rs
│ ├── bindings.rs
. .
Where bindings.rs
and librobotcontrol.a
were generated on the Beaglebone Blue.
The build script provides the static link:
// build.rs
use std::env;
use std::path::Path;
fn main() {
let dir = env::var("CARGO_MANIFEST_DIR").unwrap();
println!("cargo:rustc-link-lib=static=robotcontrol");
println!(
"cargo:rustc-link-search=native={}",
Path::new(&dir).display()
);
}
In main.rs
we can now include our new bindings:
// main.rs
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
include!("bindings.rs");
#[cfg(all(target_os = "linux"))]
fn main() {
let c_str = unsafe {
let s = rc_version_string();
assert!(!s.is_null());
CStr::from_ptr(s)
};
let r_str = c_str.to_str().unwrap();
println!("Success version: {}!", r_str);
}
#[cfg(any(not(target_os = "linux")))]
fn main() {
println!(r#"Invalid compile target!"#);
}
Cross Compile
Our build target is the AM335x 1GHz ARM® Cortex-A8. To easily compile natively the Rust tools team provides cross. This encapsulates the required environment in docker making cross compilation easy. Install cross and build with:
$ cross build --target armv7-unknown-linux-gnueabihf
Calling back to Rust from C
A core part of the Robotics feature is the IMU_MPU wich includes accelerometers, gyros and barometers. The provided C library has a callback function with the following C signature:
int rc_mpu_set_dmp_callback(void(*)(void) func)
In Rust, bingen translates this to:
extern "C" {
pub fn rc_mpu_set_dmp_callback(
func: ::std::option::Option<unsafe extern "C" fn()>,
) -> ::std::os::raw::c_int;
}
To call back into Rust from C code we need to provide a function which compiles to C's ABI. Unfortunately closures aren't able to provide this without a trampoline function. However, the provided C API doesn't allow this trampolining as the arguments aren't passed in the callback. We therefore resort to using global state, breaking the Rust thread safety guarantees. Each function that touches this memory will need to be wrapped in unsafe.
static mut mpu_data: rc_mpu_data_t = rc_mpu_data_t {
accel: [0.0; 3usize],
gyro: [0.0; 3usize],
mag: [0.0; 3usize],
temp: 0.0,
...
};
unsafe extern "C" fn mpu_callback() {
// Read access is only safe in this function.
println!("Acceleration: {}!", mpu_data.accel);
}
Where the function can be registered with:
unsafe { rc_mpu_set_dmp_callback(Some(mpu_callback)) };
Monitoring
As a quick demonstration we can expose these metrics via Promethues. Create a GuageVec for acceleration. On each callback update the guage parameters for all 3 dimensions x, y and z:
lazy_static! {
static ref ACCELERATION: GaugeVec =
register_gauge_vec!("acceleration", "Acceleration in m/s^2", &["dimension"]).unwrap();
}
unsafe extern "C" fn mpu_callback() {
ACCELERATION
.with_label_values(&["x"])
.set(mpu_data.accel[0]);
ACCELERATION
.with_label_values(&["y"])
.set(mpu_data.accel[1]);
ACCELERATION
.with_label_values(&["z"])
.set(mpu_data.accel[2]);
}
NB: MPU acceleration values should be filtered.
# HELP acceleration Acceleration in m/s^2
# TYPE acceleration gauge
acceleration{dimension="x"} 8.169016064453125
acceleration{dimension="y"} -0.3399766357421875
acceleration{dimension="z"} 5.616797094726562
We now have our Rust program wrapping an ARM C library enabling easy access to all Beaglebone Blues peripherals!