Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Setup

  1. Software setup
  2. Hardware information

Setup

1. Rust

Go to https://www.rust-lang.org/tools/install

💡 Windows users: prefer to install Rust natively, rather than using WSL.

2. Toolchain

To compile Rust code for out ESP32-C6 microcontroller, we need to install the appropriate toolchain.

rustup toolchain install stable --component rust-src --target riscv32imac-unknown-none-elf

3. Tools

Additionally, we will want tools to help us generate projects and program the firmware.

cargo install cargo-espflash espflash esp-generate

Hardware information

Board pinout

board pinout

Documents

Links

Hello World

Create the project

Let's create our first embedded Rust project. For this, we will be using esp-generate tool installed earlier.

To begin with, run the following command:

esp-generate --headless --chip=esp32c6 -o log -o unstable-hal hello_world

This will create a new project in hello_world directory. Go ahead and open it with: code hello_world.

💡 You can run esp-generate --chip=esp32c6 to open the tool in interactive mode and explore the available options.

Run

With the ESP board plugged into your computer, you should be able to run the firmware with:

cargo run

You may have to select the appropriate device. If this is the case, the tool will prompt for it.

If everything goes well, your device will boot and start printing the "hello world" message:

INFO - Hello world!
INFO - Hello world!
INFO - Hello world!
INFO - Hello world!
INFO - Hello world!

Experiment

Experiment with this example a little bit.

  • Change the message to print something else.
  • Change the log level (e.g. to warn).
  • Change the printing frequency by adjusting the sleep duration.

Bonus

The way the delay is currently written is a bit verbose. We can simplify the code by using Delay from the esp_hal crate.

  1. Add the following line next to the existing use declarations:
    #![allow(unused)]
    fn main() {
    use esp_hal::delay::Delay;
    }
  2. Create a delay variable:
    #![allow(unused)]
    fn main() {
    let delay = Delay::new();
    }
  3. Replace the two lines using delay_start with the following one line:
    #![allow(unused)]
    fn main() {
    delay.delay_millis(1000);
    }

Blinky

We have created a typical "hello world" application, but in embedded systems, the equivalent of "hello world" is blinky: simply blinking an LED.

We will continue working in the hello_world project.

Change the program

In order to blink an LED, we need to define an output pin and make a couple of other changes.

Firstly, let's rename the _peripherals variable to peripherals.

💡 In Rust, a variable can be prefixed with an underscore (_) to indicate that it is unused. Since we'll be using it, let's remove the leading underscore.

Next, we will define our LED:

#![allow(unused)]
fn main() {
let mut led = Output::new(
    peripherals.GPIO19,
    Level::Low,
    OutputConfig::default().with_drive_mode(DriveMode::OpenDrain),
);
}

The code above will not compile as is, since the compiler won't know where Output and other types are coming from. To fix that, import the required types like so:

#![allow(unused)]
fn main() {
use esp_hal::gpio::{DriveMode, Level, Output, OutputConfig};
}

💡 Whenever a you have a missing declaration error, you can use rust-analyzer to help you fix it.

Place the cursor on the missing type name and press Ctrl + . on the keyboard. In the context menu, select Import <Type> or Qualify <Type>.

"Import" will add the required use declaration, while "Qualify" will prefix the module path in-place.

Finally, add the following line to the body of the loop to toggle the LED every cycle:

#![allow(unused)]
fn main() {
led.toggle();
}

Experiment

Output has other methods than toggle. Try using set_high and set_low methods to change the LED blinking pattern.

Working with inputs

We have worked through an example of driving a GPIO output. Now it's time for you to try and extend the program to read the GPIO input state.

  • Use the esp_hal crate documentation to create and initialize a GPIO Input.
  • The button is connected to GPIO9.
  • Write a program that turns the LED on only while the button is pressed.
Solution
  1. Add new imports:
    #![allow(unused)]
    fn main() {
    use esp_hal::gpio::{Input, InputConfig};
    }
  2. Declare the button:
    #![allow(unused)]
    fn main() {
    let button = Input::new(peripherals.GPIO9, InputConfig::default());
    }
  3. Modify the loop code:
    #![allow(unused)]
    fn main() {
     if button.is_high() {
         led.set_high();
     } else {
         led.set_low();
     }
    }

Explanation

You may be tempted to think that high means ON and low means off, however, the logic of both the button and the LED is inverted.

Button has a pull-up resistor and connects the pin to ground when pressed.

The LED has its anode connected to the 3.3 V supply and the cathode is connected to the open-drain output. When the output is set to low, it is connected to ground, allowing the current to flow and thereby turning the LED on.

Interrupts

In the last exercise, we learnt how to read to read a GPIO pin state. However, it's not always desirable to poll the pin state in software. This is where interrupts come it. We can ask hardware to notify us whenever the pin state changes.

Background

Because interrupts can happen at any time during the execution of our program, we can think of them as if they're running on a separate thread. As such, when working with interrupts, we apply the same principles as when working in a multi-threaded environment.

Additionally, because the interrupt routine is called by hardware, there is no way to pass parameters or return values from the interrupt handler.

One common way to pass data between the application and interrupt contexts is using global state. This is mostly straightforward in C, as you can just read and write any global variable at any time. Whether you like it or not, this "feature" opens up questions about what happens when the variable is modified while some other code is reading it?

Consider this code:

volatile int counter = 0;

int main() {
    while (true) {
        app_task();
        ++counter; // Same as: counter = counter + 1
    }
}

void timer_interrupt() {
    interrupt_task(counter);
    counter = 0;
}

In Rust, however, using a mutable global state is explicitly unsafe. While it's possible to bypass the compiler rules, the language strongly encourages developers to write correct code.

Atomics & Mutexes

When working with primitive types we can use one of the types provided in core::sync::atomic. Atomic types are the most fundamental way to ensure exclusive access -- these opperations are provided by the CPU.

For more complex types, we have to resort to higher level synchronization primitives, such as Mutexes and other types of locks. Note that Mutex is only available in the std library, as is requires operating system support.

No worries though, for embedded systems we can use Mutex provided by the critical_section crate.

GPIO Interrupt