I started learning Rust a few months ago and quickly fell in love with it. After spending some time reading the Book and doing Exercism exercises, I thought Rust’s strengths like safety and high performance were a good fit for embedded systems.

So, equipped with basic knowledge of Rust, I dived into the sea of embedded Rust world. The tide was rough and it was dark and scary underwater. It seemed pretty difficult to navigate at first. However, thanks to the strict yet friendly compiler and the awesome Rust community, I enjoyed the learning process and managed to write a simple device driver in Rust.

max6955 Driver

max6955 is a device driver to communicate with MAX6955 LED Display Driver through I2C interface. I chose MAX6955 for my first project because if a blinking LED is exciting, a scrolling text is a hundred times more exciting. I dug through my toy box and found an 8 digits x 2 display that I designed and built many years ago. Back then, I wrote a driver in C.

if a blinking LED is exciting, a scrolling text is a hundred times more exciting

Since the device was pretty simple, it didn’t take much time to develop the driver in Rust once I had properly configured the development environment. Of course, I had some difficulties along the way. But, the compiler was nice enough to suggest possible solutions.

Project Pages

Example

Here is a scrolling Text application example using max6955 driver! RUST RUST RUST!

#![no_std]
#![no_main]

extern crate panic_halt;
pub use cortex_m::{asm::bkpt, iprint, iprintln, peripheral::ITM};
pub use cortex_m_rt::entry;
use f3::hal::gpio::gpiob::{PB6, PB7};
use f3::hal::gpio::AF4;
use f3::hal::stm32f30x::I2C1;
pub use f3::hal::{delay::Delay, prelude};
use f3::hal::{i2c::I2c, prelude::*, stm32f30x};

use max6955::*;
pub type MAX6955 = max6955::Max6955<I2c<I2C1, (PB6<AF4>, PB7<AF4>)>>;

#[entry]
fn main() -> ! {
    let (mut max6955, mut delay) = init();
    max6955.powerup().unwrap(); // power up the top row
    max6955.set_global_intensity(4).unwrap(); // set intensity

    max6955.set_address(0x61); // choose the bottom row
    max6955.powerup().unwrap();
    max6955.set_global_intensity(4).unwrap();

    let txt = [
        "       R", "      RU", "     RUS", "    RUST", "   RUST", "  RUST", " RUST", "RUST",
        "UST", "ST", "T", "        ",
    ];

    loop {
        max6955.set_address(0x60); // chose the top row
        for t in txt.iter() {
            max6955.write_str(t).unwrap();
            delay.delay_ms(250_u16);
        }

        max6955.set_address(0x61); // choose the bottom row
        for t in txt.iter() {
            max6955.write_str(t).unwrap();
            delay.delay_ms(250_u16);
        }
    }
}

pub fn init() -> (MAX6955, Delay) {
    let cp = cortex_m::Peripherals::take().unwrap();
    let dp = stm32f30x::Peripherals::take().unwrap();

    let mut flash = dp.FLASH.constrain();
    let mut rcc = dp.RCC.constrain();

    let clocks = rcc.cfgr.freeze(&mut flash.acr);

    let mut gpiob = dp.GPIOB.split(&mut rcc.ahb);
    let scl = gpiob.pb6.into_af4(&mut gpiob.moder, &mut gpiob.afrl);
    let sda = gpiob.pb7.into_af4(&mut gpiob.moder, &mut gpiob.afrl);

    let i2c = I2c::i2c1(dp.I2C1, (scl, sda), 400.khz(), clocks, &mut rcc.apb1);

    let max6955 = Max6955::new(i2c).unwrap(); // instantiate max6955 with default address of 0x60

    let delay = Delay::new(cp.SYST, clocks);

    (max6955, delay)
}

What I have learned

“I can do this!”

First of all, embedded Rust is not that bad! Since the community has already figured out the most difficult parts like HAL, I basically read the datasheet and translated registers and expected values to enums and worked on high-level APIs like powerup(), set_global_intensity() and write_str().

enum

enum seems really powerful in Rust. I guess I only scratched its surface and haven’t seen the full potential of it. But, I am already liking it.

Here is how I handled the register map in C many years ago.

#define reg_noOp 0x00
#define reg_decodeMode 0x01
#define reg_globalIntensity 0x02
#define reg_scanLimit 0x03
#define reg_configuration 0x04
...

And in Rust this time.

pub enum Register {
    NoOp = 0x00,
    DecodeMode = 0x01,
    GlobalIntensity = 0x02,
    ScanLimit = 0x03,
    Configuration = 0x04,
    ...
}

impl Register {
    /// return register address as u8
    pub fn addr(self) -> u8 {
        self as u8
    }
}

Because of Rust’s type system, the compiler makes sure that I don’t pass a wrong value and refuses to compile if I pass a different enum or u8. For example, here is how to set Digit type of LED display.

pub enum DecodeMode {
    /// No decode for digit pairs 7 to 0.
    NoDecode = 0x00,
    /// Hexadecimal decode for digit pair 0, no decode for digit pairs 7 to 1.
    HexD0 = 0x01,
    /// Hexadecimal decode for digit pairs 2 to 0, no decode for digit pairs 7 to 3.
    HexD0D2 = 0x07,
    /// Hexadecimal decode for digit pairs 7 to 0.
    Hex = 0xFF,
}

impl DecodeMode {
    /// return enum value as u8
    pub fn value(self) -> u8 {
        self as u8
    }
}

/// Configure Decode Mode
/// # Arguments
/// * `mode` - `DecodeMode`
pub fn set_decode_mode(&mut self, mode: DecodeMode) -> Result<(), E> {
    self.write_register(Register::DecodeMode, mode.value())
}

The public method set_decode_mode only takes DecodeMode enum. If I try to pass u8, I would get en error at compile time. Similarly, the private method write_register called in set_decode_mode takes Register enum only.

Trait

Trait is pretty cool too. This driver can take whatever I2C instance that implements WriteRead and Write. Below is how I construct a driver with an instance of I2C.

impl<I2C, E> Max6955<I2C>
where
    I2C: WriteRead<Error = E> + Write<Error = E>,
{
    pub fn new(i2c: I2C) -> Result<Self, E> {
        let max6955 = Max6955 {
            i2c,
            addr: DEFAULT_SLAVE_ADDR,
        };
        Ok(max6955)
    }
}

I2C is generic here. The method expects something that implements WriteRead and Write. I tested this driver with stm32f30x-hal and stm32f3xx_hal and it worked with both as expected! I think I can even make a fake interface with WriteRead and Write for unit testing. I will explore that in future projects.

Documentation

Lastly, I found Rust’s built-in documentation feature fantastic. It is great to be able to generate documentation without external tools.

Final Thoughts

I am still an infant Rustacean just starting to explore the language and its ecosystem. But, I have a strong intuitive feeling that Rust is great for embedded systems. I really think that Rust’s type system and zero-cost abstractions help me write safe and clean readable code without sacrificing performance.