Blinking
Well… this is awkward. The board is already blinking!
But how?
Let’s look line by line to understand how this is working.
Navigate to firmware/src/main.rs
. This is the main file for our firmware.
#![no_std]
#![no_main]
At the top of the file you will find ^ these directives.
#![no_std]
indicates that our program will not have acces to the Rust
standard library. This is because embedded systems are resource constrained
environments with no operating system, so simple things like allocation are
suddenly not so simple. This directive informs the Rust compiler that the
standard library is unavailable, which permits us to target the microcontroller.
#![no_main]
indicates that the entry-point1 to our program is non-trivial.
The Rust compiler typically expects the entry-point to be defined as a top-level2
function named main
, however as we will soon see, the entry-point will not be
defined by us, and will be generated by Embassy to begin async operation and start
the executor3.
Below this you will see:
use embassy_executor::Spawner;
use embassy_time::Timer;
use esp_backtrace as _;
use esp_hal::{
gpio::{Io, Level, Output},
timer::timg::TimerGroup,
};
use esp_println::println;
This looks like a lot, but these are just crates we are importing to use in our program. We will see exactly which crates we use and why as we explore the rest of this file.
#[esp_hal_embassy::main]
async fn main(_spawner: Spawner) {
// ...
}
Next you will see this ^ function.
This function is hosted by Embassy, and is what we will use to kick off our firmware.
You may notice it is async! This means the executor has already started, and we can utilize all of Rust’s concurrency features right away!
#[main]
is an attribute macro4 from Embassy that transforms our async function
definition into the proper structure for starting the executor and configuring the
CPU/static memory.
For funzies, you can use a tool called
cargo-expand
to view what this macro does! Your editor of choice may also support macro expansion inline.
This function provides us with a single parameter of type Spawner
. This type allows
us to spawn tasks, which we don’t need for a simple blink program so we marked the
variable as unused with the leading underscore.
Now let’s look at what exactly we do in this function to configure our microcontroller.
On the first line, we enable logging via the ESP32s JTAG5 controller:
esp_println::logger::init_logger_from_env();
Next, we take ownership of all peripherals. This operation can only be done once, per RAII, we may only have one binding for one resource:
let peripherals = esp_hal::init(esp_hal::Config::default());
Then we initialize the 0th timer group (which we can use to provide Embassy with a means for keeping time):
let timg0 = TimerGroup::new(peripherals.TIMG0);
Embassy needs a impl TimerCollection
(some timer that implements the trait TimerCollection
) to provide monotonics6:
esp_hal_embassy::init(timg0.timer0);
The next section is fairly straight forward:
let io = Io::new(peripherals.GPIO, peripherals.IO_MUX);
let mut led = Output::new(io.pins.gpio17, Level::Low);
loop {
println!("Hello, World!");
led.toggle();
Timer::after_millis(1_000).await;
}
We create a binding to the GPIO
and IO_MUX
(multiplexer for the GPIO) peripherals to gain control of IO.
Then we configure pin 17
to be an output pin with a default level of low.
And finally, in an infinite loop, we print Hello, World!
, toggle the LED, and wait for 1 second!
You may notice as part of that “waiting”, there is a trailing .await
. In async contexts, this indicates
yielding control back to the executor. If we had other tasks waiting to do work, they would get to
while this task waits. This simple handoff of execution is what makes async such a powerful
language feature.
If you have operating system design experience, you may be itching to see some concurrency primitives in action, rest assured we will get to that. You may also be wondering how preemption can be done. The Embassy executor actually does not support preemption, and this was done intentionally. You can, however, create multiple executors of different priorities, and they will preempt each other’s execution. You can learn more at Embassy’s docs.
And that’s it! If you want you could try to get the other LED (pin 18
) blinking as well!
All binaries have an entry-point, which is the symbol to start execution on. ↩︎
A top-level item is defined above all hierarchy of a file. Think of global variable definitions, free-standing functions or type definitions. ↩︎
The executor is the “entity” provided by Embassy that fascilitates the routing of execution between tasks. ↩︎
Attribute macros are procedural macros7 that attatch to structures and directly transform them. ↩︎
JTAG is a proprietary debug interface from SEGGER that exists on top of SWD9. ↩︎
Monotonic means strictly increasing. A monotonic timer is usually at the heart of time-keeping as the assumption that the absolute value of the timer is strictly increasing can be leveraged to conduct time-keeping logic. ↩︎
Procedural macros are an advanced type of macro8 that are mini Rust programs written to be executed by the compiler at compile-time. As opposed to declarative macros which do a direct symbol transformation. ↩︎
Macros are like functions that transform code at compile-time. ↩︎
Sserial Wire Debug (SWD) is a debug protocol created by ARM for debugging ARM CPUs. This is extremely useful as we can remotely debug microcontrollers via this interface. You can use it to transfer log-style information, or via the microcontroller’s debug peripheral, you could even start a GDB (or LLDB) session like any local process. ↩︎