Embedded Rust is cool!
February 7, 2024
I've started to work on modifying an old CCTV camera pan-tilt mount to use as a satellite tracking device for receiving radio signals (and maybe for a telescope?), and during this process I have needed to write some code to perform various functions such as moving the motors to point in a particular direction. I was doing most of this work in the Arduino IDE using the version of C++ usually used on Arduinos, and I was moderately successful in making functions to do what I wanted. Despite my lack of experience with C and C++, I was able to make the code do what I wanted to.
Then I fried the solid state relays on the control board for the camera mount, and I was back to waiting for parts to arrive. In the meantime, I decided to try using the Raspberry Pi Pico and the BNO055 sensor I was using for altitude and azimuth detection with Rust, as I had not used Rust for very much embedded programming before. To start with, there were a few libraries which caught my eye, including an awesome library (by eupn on GitHub) for the BNO055 sensor, and Embassy, a pretty much fully featured framework for embedded Rust development.
Most of the setup after that was what I expected, although having a template repository for using the RP2040 with Embassy would be nice (I might make one). The BNO055 library worked flawlessly, and Embassy made it extremely easy to get up and running with what I wanted to do, with built in support for time, serial logging with embassy_usb_logger, and a very easy to use hardware abstraction layer (HAL). Then, just for fun, I tried out Embassy's task capabilities, and I was blown away by how easy it was to make concurrent code on bare metal.
For example, if we define a task blink_led() which contains the following:
#[embassy_executor::task]
async fn blink_led(mut led: Output<'static, AnyPin>) {
loop {
led.set_high();
Timer::after_secs(1).await;
led.set_low();
Timer::after_secs(1).await;
}
}
This will turn a pin defined by led on and off every second, indefinitely. If we then create a main function which does something else, like reading the data from the BNO055, we can end up with something like the following:
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_rp::init(Default::default());
// Set the LED to pin 25, and spawn a new instance of `blink_led()`
// which runs it
let led = Output::new(AnyPin::from(p.PIN_25), Level::Low);
spawner.spawn(blink_led(led)).unwrap();
// Set up i2c
let sda = p.PIN_0;
let scl = p.PIN_1;
let i2c = i2c::I2c::new_blocking(p.I2C0, scl, sda, Config::default());
// Set up and initialize the BNO055
let mut imu = bno055::Bno055::new(i2c).with_alternative_address();
imu.init(&mut Delay).expect("Couldn't init the sensor!");
imu.set_mode(bno055::BNO055OperationMode::NDOF, &mut Delay).expect("Failed to set mode");
// Get a new [quaternion](https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation) from the sensor every 100ms
loop {
let quaternion = imu.quaternion().unwrap();
Timer::after_millis(100).await;
}
}
Then when this code is run on the Pico, it will begin blinking the LED, and at the same time it will be grabbing quaternions from the sensor! It's like magic! It doesn't use an RTOS or more than one core, it just schedules tasks to run so they are always guaranteed to have time to run, as long as no one task blocks indefinitely. You can even define different priority levels for different tasks, make them wake up when an interrupt is received, and a bunch of other stuff. Check out the Embassy documentation for more information about its features and functionality, I only covered an extremely limited portion of what it is capable of here.
Also more updates about the satellite tracker mount will come eventually!