Understanding esp-template

Now that we know how to generate a no_std project, let's inspect what the generated project contains, try to understand every part of it, and run it.

Inspecting the Generated Project

When creating a project from esp-template with the following answers:

  • Which MCU to target? · esp32c3
  • Configure advanced template options? · false

For this explanation, we will use the default values, if you want further modifications, see the additional prompts when not using default values.

It should generate a file structure like this:

├── .cargo
│   └── config.toml
├── src
│   └── main.rs
├── .gitignore
├── Cargo.toml
└── rust-toolchain.toml

Before going further, let's see what these files are for.

  • .cargo/config.toml
    • The Cargo configuration
    • This defines a few options to correctly build the project
    • Contains runner = "espflash flash --monitor" - this means you can just use cargo run to flash and monitor your code
  • src/main.rs
    • The main source file of the newly created project
    • For details, see the Understanding main.rs section below
  • .gitignore
    • Tells git which folders and files to ignore
  • Cargo.toml
    • The usual Cargo manifest declares some meta-data and dependencies of the project
    • Those are the most common licenses used in the Rust ecosystem
    • If you want to use a different license, you can delete these files and change the license in Cargo.toml
  • rust-toolchain.toml
    • Defines which Rust toolchain to use
      • The toolchain will be nightly or esp depending on your target

Understanding main.rs

 1 #![no_std]
 2 #![no_main]
  • #![no_std]
    • This tells the Rust compiler that this code doesn't use libstd
  • #![no_main]
    • The no_main attribute says that this program won't use the standard main interface, which is usually used when a full operating system is available. Instead of the standard main, we'll use the entry attribute from the esp-riscv-rt crate to define a custom entry point. In this program, we have named the entry point main, but any other name could have been used. The entry point function must be a diverging function. I.e. it has the signature fn foo() -> !; this type indicates that the function never returns – which means that the program never terminates.
 4 use esp_backtrace as _;
 5 use esp_println::println;
 6 use esp_hal::{clock::ClockControl, peripherals::Peripherals, prelude::*, timer::TimerGroup, Rtc};
  • use esp_backtrace as _;
    • Since we are in a bare-metal environment, we need a panic handler that runs if a panic occurs in code
    • There are a few different crates you can use (e.g panic-halt) but esp-backtrace provides an implementation that prints the address of a backtrace - together with espflash these addresses can get decoded into source code locations
  • use esp_println::println;
    • Provides println! implementation
  • use esp_hal::{...}
    • We need to bring in some types we are going to use
 8 #[entry]
 9 fn main() -> ! {
10    let peripherals = Peripherals::take();
11    let system = peripherals.SYSTEM.split();
12    let clocks = ClockControl::max(system.clock_control).freeze();
14    println!("Hello world!");
16    loop {}
17 }

Inside the main function we can find:

  • let peripherals = Peripherals::take()
    • HAL drivers usually take ownership of peripherals accessed via the PAC
    • Here we take all the peripherals from the PAC to pass them to the HAL drivers later
  • let mut system = peripherals.SYSTEM.split();
    • Sometimes a peripheral (here the System peripheral) is coarse-grained and doesn't exactly fit the HAL drivers - so here we split the System peripheral into smaller pieces which get passed to the drivers
  • let clocks = ClockControl::max(system.clock_control).freeze();
    • Here we configure the system clocks - in this case, boost to the maxiumum for the chip
    • We freeze the clocks, which means we can't change them later
    • Some drivers need a reference to the clocks to know how to calculate rates and durations
  • println!("Hello world!");
    • Prints "Hello world!"
  • loop {}
    • Since our function is supposed to never return, we just "do nothing" in a loop

Running the Code

Building and running the code is as easy as

cargo run

This builds the code according to the configuration and executes espflash to flash the code to the board.

Since our runner configuration also passes the --monitor argument to espflash, we can see what the code is printing.

Make sure that you have espflash installed, otherwise this step will fail. To install espflash: cargo install espflash

You should see something similar to this:

[2023-04-17T14:17:08Z INFO ] Serial port: '/dev/ttyACM0'
[2023-04-17T14:17:08Z INFO ] Connecting...
[2023-04-17T14:17:09Z INFO ] Using flash stub
[2023-04-17T14:17:09Z WARN ] Setting baud rate higher than 115,200 can cause issues
Chip type:         esp32c3 (revision v0.3)
Crystal frequency: 40MHz
Flash size:        4MB
Features:          WiFi, BLE
MAC address:       60:55:f9:c0:39:7c
App/part. size:    203,920/4,128,768 bytes, 4.94%
[00:00:00] [========================================]      13/13      0x0
[00:00:00] [========================================]       1/1       0x8000
[00:00:01] [========================================]      64/64      0x10000
[2023-04-17T14:17:11Z INFO ] Flashing has completed!
    CTRL+R    Reset chip
    CTRL+C    Exit

Hello world!

What you see here are messages from the first and second stage bootloader, and then, our "Hello world" message!

And that is exactly what the code is doing.

You can reboot with CTRL+R or exit with CTRL+C.

If you encounter any issues while building the project, please, see the Troubleshooting chapter.