Rust on bare metal RISC V microcontroller

Published:

I have been working with Rust for quite some time, but I haven’t yet worked with Rust on embedded platforms. I am also interested to gain some experience with the relatively new RISC V processor architecture.

I bought the SeeedStudio GD32 RISC-V kit with LCD development kit several years ago, but it was just gathering dust in a drawer. I now decided to actually play around with it. It has a 32bit RISC V microcontroller and comes with a small LCD display. The development board has a USB C connector that you can use to interface it with a PC.

Let’s first make a small plan:

  1. Get everything connected and see if it actually boots and the LCD works.
  2. Check if there is Rust library for this board and build a small Rust demo app

Getting started

I followed the steps on the Seeedstudio Wiki page to connect the LCD to the board. I installed platformIO and compiled a small C++ demo app and uploaded it to the board. In order to upload an application to the development board, you need a DFU(Device Firmware Updates) tool provided by GigaDevices, the company that developed this RISC V microcontroller. A link is provided on the Seeedstudio Wiki page. There are also some websites that suggest that it should be possible to use OpenOCD debugger via the USB interface, but I couldn’t get it to work. The board also has a JTAG interface, but I don’t have a JTAG debugger , so that’s no option either. So I have to keep it simple, so on chip debugging is not required. Anyway, the C++ application worked, so let’s move on to Rust.

Deploy Rust applications

I was pleasantly surprised to find that there was actually a Rust support crate for this particular board: https://github.com/riscv-rust/seedstudio-gd32v. It hasn’t be updated in years, but the board is also several years old, so that makes sense.

The following steps were required to deploy Rust apps to this development board: ( I already have the basics installed like Rust and VS Code)

  1. Install a new Rust target
rustup target add riscv32imac-unknown-none-elf
  1. In order to create a binary file that can be deployed to the microcontroller from the executable created by the Rust compiler, a tool from the RISC V toolchain is required: objcopy. I got the RISC V toolchain from https://nucleisys.com/ and used only the objcopy util.
  2. I used the Cargo.toml and config file as-is from the support crate.

Development of Rust applications for microcontrollers

One of the main differences between developing Rust applications for PC’s compared to microcontrollers is that you cannot use the Rust standard library. You can specify this by stating #![no_std] in your main source file. The Rust compiler will then link to libcore: the platform agnostic parts of the Rust standard library. Due to the fact that we don’t have access to the standard library, we also don’t have features like dynamic memory allocation. There are ways to solve that, but for this simple demo app I should be fine with static memory allocation.

In order to stay away from reading and writing to registers directly, it is quite common for embedded systems software to use a HAL(hardware abstraction layer). For many microcontrollers, such a HAL implementation is also available for Rust. For this particular microcontroller , this is implemented in https://github.com/riscv-rust/gd32vf103xx-hal. It offers abstractions for many of the features of this particular microcontroller, such as the GPIO, DMA, I2C interface,etc. The LCD is connected to the parallel interface on the microcontroller and this is also covered in this HAL (EXMC). In https://github.com/riscv-rust/seedstudio-gd32v there are good examples on how to use for example the LCD and toggle the LED’s.

Build a demo

I decided to build a basic demo to get some feeling for how easy it is to use Rust on an embedded system. In the demo I will draw a button on the display and when you then touch the display, the 3 LED’s on the board will be toggled in sequence. For this I reuse some code from the other demo’s in this repo, so I completed in a few hours.

Due to the abstractions provided by the HAL and LCD driver, I didn’t need any unsafe Rust code on the application level. If you prevent unsafe code in Rust, the Rust compiler will make sure that your code doesn’t have any memory related bugs. This is quite a big improvement compared to C/C++ on embedded platforms.

Check out the final result in this short YouTube movie:

I am happy with how easy it was to create embedded software for this microcontroller using Rust. I am planning to make a more sophisticated and useful embedded system in the future. Stay tuned !

P.S. This is the sourcecode for this demo:

#![no_std]
#![no_main]

use core::convert::TryInto;
use panic_halt as _;
use embedded_graphics::pixelcolor::Rgb565;
use embedded_graphics::prelude::*;
use embedded_graphics::primitives::Circle;
use embedded_graphics::primitives::Rectangle;
use embedded_graphics::primitives::Triangle;
use embedded_graphics::style::PrimitiveStyle;
use gd32vf103xx_hal::pac;
use gd32vf103xx_hal::prelude::*;
use gd32vf103xx_hal::delay::McycleDelay;
use riscv_rt::entry;
use seedstudio_gd32v::lcd::{ili9341::Orientation, Lcd};
use seedstudio_gd32v::touch::TouchController;
use seedstudio_gd32v::{lcd_pins, touch_pins};
use seedstudio_gd32v::led::Led;

struct LcdWrapper {
    lcd: Lcd,
}

#[entry]
fn main() -> ! {
    //init variables
    let mut state: usize = 0;
    let mut update: bool = true;
    let mut i = 0;

    //init hardware
    let dp = pac::Peripherals::take().unwrap();
    let mut rcu = dp
        .RCU
        .configure()
        .ext_hf_clock(8.mhz())
        .sysclk(108.mhz())
        .freeze();

    let gpiod = dp.GPIOD.split(&mut rcu);
    let gpioe = dp.GPIOE.split(&mut rcu);
    let gpiob = dp.GPIOB.split(&mut rcu);

    let mut led1 = gpiob.pb5.into_push_pull_output();
    led1.off();
    let mut led2 = gpiob.pb1.into_push_pull_output();
    led2.off();
    let mut led3 = gpiob.pb0.into_push_pull_output();
    led3.off();

    let leds: [&mut dyn Led; 3] = [&mut led1, &mut led2, &mut led3];

    let mut delay = McycleDelay::new(&rcu.clocks);

    let lcd_pins = lcd_pins!(gpiod, gpioe);
    let mut lcd = Lcd::new(dp.EXMC, lcd_pins, &mut rcu);
    lcd.set_orientation(Orientation::Landscape).unwrap();
    let mut wrapper = LcdWrapper { lcd };

    let touch_pins = touch_pins!(gpiod, gpioe);
    let mut touch_controller = TouchController::new(touch_pins, &mut rcu);
    touch_controller.set_orientation(Orientation::Landscape);

    //start infinite loop
    loop {
        let t = touch_controller.get_touch();
        //state management
        if state == 0 {
            //if screen is touched, go to next state
            if let Some(_t) = t {
                state = 1;
                update = true;
            }
        } else {
            //blink LED's in sequence
            let inext = (i + 1) % leds.len();
            leds[i].off();
            leds[inext].on();
            delay.delay_ms(500);
            i = inext;
        }
        //update display if required
        if update {
            draw(&mut wrapper, state);
            update = false;
        }
        
    }
}

fn draw(wrapper: &mut LcdWrapper, state: usize) {
    draw_background(wrapper);
    if state == 0 {
        draw_start_button(wrapper);
    } 
}

fn draw_background(wrapper: &mut LcdWrapper) {
    //draw black background
    let p = Point::new(
        wrapper.lcd.width() as i32 - 1,
        wrapper.lcd.height() as i32 - 1,
    );
    Rectangle::new(Point::new(0, 0), p)
        .into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK))
        .draw(&mut *wrapper.lcd)
        .unwrap();
}

fn draw_start_button(wrapper: &mut LcdWrapper) {
    //draw a simple blue button with white triangle
    let screen_width: usize = wrapper.lcd.width();
    let screen_height: usize = wrapper.lcd.height();
    let diameter: usize = 60;
    Circle::new(
        Point::new(
            (screen_width / 2).try_into().unwrap(),
            (screen_height / 2).try_into().unwrap(),
        ),
        diameter.try_into().unwrap(),
    )
    .into_styled(PrimitiveStyle::with_fill(Rgb565::BLUE))
    .draw(&mut *wrapper.lcd)
    .unwrap();

    let triangle_size: usize = 40;
    Triangle::new(
        Point::new(
            (screen_width / 2 - triangle_size / 2).try_into().unwrap(),
            (screen_height / 2 - triangle_size / 2).try_into().unwrap(),
        ),
        Point::new(
            (screen_width / 2 - triangle_size / 2).try_into().unwrap(),
            (screen_height / 2 + triangle_size / 2).try_into().unwrap(),
        ),
        Point::new(
            (screen_width / 2 + triangle_size).try_into().unwrap(),
            (screen_height / 2).try_into().unwrap(),
        ),
    )
    .into_styled(PrimitiveStyle::with_fill(Rgb565::WHITE))
    .draw(&mut *wrapper.lcd)
    .unwrap();
}