Getting started with the ESP32-C3 and Rust

Date: 13.05.2022

So, you got a shiny new esp32-c3 dev-board and want to get started programming it. But you also want to use the best™ programming language to do so?

Boy do I got a quick start for you!

Before we are starting, keep in mind that it is mid-2022. This information might be outdated at some point in the future. Currently, it works and also uses only the latest tech from the rust embedded world.

Overview

In the rust embedded world most hardware programming is done through HAL (Hardware Abstraction Layer) crates. They provide common peripherals based on the embedded-hal crate. This allows sensor implementations to share only the ebedded-hal crate. Drivers are only implemented against this embedded-hal once and can be shared across multiple architectures and chips.

Finding the prefect abstraction for hardware however is not always easy. This is one of the reasons why at least two HALs exist for esp32 controllers. One is esp32-hal and one is esp-hal.

esp32-hal is the old HAL. We don’t want to use old stuff. The new esp-hal however is still in development, so we can’t find it on crates.io yet. At some point it will be released there.

Setting up the environment

The first thing you’ll have to do whenever you are starting out with new hardware and rust-embedded dev is installing a toolchain for the hardware you are using. In our case this is done via

$ rustup target add riscv32imc-unknown-none-elf

Now we are able to build a project. However, we still have to flash our board. Luckily there is espflash, which is similar to the similarly named python script, but in rust.

You can install it via

$ cargo install espflash

It can be called either via espflash, or, if you have the cargo subcommand installed via cargo espflash.

If you don’t want to use screen for reading back output via USB (or you are on Windows), you can also install espmonitor.

Setting up a project

The one time setup is done at this point. Setting up a rust project is nearly as easy as it usually is. We start out with

$ cargo new --bin new_project

If you start to program using esp-hal and embedded-hal you would get an error telling you that esp-hal is emitting wrong (RiscV) instructions. So we need a way to tell the compiler to use the correct toolchain.

We do this by creating:

new_project/.cargo/config.toml

with the following content:

[target.riscv32imc-unknown-none-elf]
runner = "espflash --monitor"
rustflags = [
  "-C", "link-arg=-Tlinkall.x"
]

# for testing: you can specify this target to see atomic emulation in action
[target.riscv32imac-unknown-none-elf]
runner = "espflash --monitor"
rustflags = [
  "-C", "link-arg=-Tlinkall.x"
]

[build]
target = "riscv32imc-unknown-none-elf"

[unstable]
build-std = [ "core" ]

The last thing to do is setting up our new_project/src/main.rs file.

A really simple main.rs might look like this:

#![no_std]
#![no_main]

#[entry]
fn main() -> {
    //your code
}

For completeness, the hello_world example looks like this:

#![no_std]
#![no_main]

use core::fmt::Write;

use esp32c3_hal::{pac::Peripherals, prelude::*, RtcCntl, Serial, Timer};
use nb::block;
use panic_halt as _;
use riscv_rt::entry;

#[entry]
fn main() -> ! {
    let peripherals = Peripherals::take().unwrap();

    let mut rtc_cntl = RtcCntl::new(peripherals.RTC_CNTL);
    let mut serial0 = Serial::new(peripherals.UART0).unwrap();
    let mut timer0 = Timer::new(peripherals.TIMG0);
    let mut timer1 = Timer::new(peripherals.TIMG1);

    // Disable watchdog timers
    rtc_cntl.set_super_wdt_enable(false);
    rtc_cntl.set_wdt_enable(false);
    timer0.disable();
    timer1.disable();

    timer0.start(10_000_000u64);

    loop {
        writeln!(serial0, "Hello world!").unwrap();
        block!(timer0.wait()).unwrap();
    }
}

Flashing and monitoring

Usually we use cargo run to execute our program. In this case, it is a two-step procedure. First we flash the program to the board, and second, monitoring its serial output.

Make sure to have the board connected. Then check /dev/tty* for a USB connect. Usually this is /dev/ttyUSB0. Assuming it is USB0 the flash command looks like this

$ cargo espflash --release /dev/ttyUSB0

Now you can use screen or espmonitor to check the serial output of your board on USB0 via

$ cargo espmonitor /dev/ttyUSB0

At this point you are ready to program using your esp32-c3. Have fun, maybe you need a flow sensor?