-
Notifications
You must be signed in to change notification settings - Fork 830
Getting Started
To begin, you should:
- Have an MCU that embassy supports. We currently support nrf, stm32f0, stm32f4, stm32g0, stm32h7, stm32l0, stm32l1, stm32l4, stm32wb and stm32wl series MCUs. We aim to support all common stm32 families.
- Know how to compile, load, and debug a rust program on an embedded target.
- Have an embedded probe that your IDE supports.
- Have the nightly rust compiler with the applicable eabi target, for example
thumbv7em-none-eabi
.
TODO: add links to tutorials when applicable in the list above
If you're not familiar with embedded development, consult the rust embedded development book.
Because embassy uses some unstable features to support async, embassy requires the rust nightly compiler. Even though embassy currently requires the nightly compiler, you can be confident that software written with embassy will continue to be supported into the future as the required compiler features become stabilized.
First, let's start with an example from the embassy
project. To download it:
git clone --recurse-submodules https://github.com/embassy-rs/embassy.git
You need
git
to run this command
Then go into the embassy/examples
folder and check the folder matching the MCU
you want to use. For instance, if your chip is an STM32F407VGT6, choose the
stm32f4/
folder.
cd embassy/examples/stm32f4/
The folder has the following structure:
.
├── .cargo
│ └── config.toml
├── Cargo.toml
└── src
├── bin
│ ├── blinky.rs
│ ├── button_exti.rs
│ ├── button.rs
│ ├── can.rs
│ ├── hello.rs
│ ├── spi_dma.rs
│ ├── spi.rs
│ ├── usart_dma.rs
│ └── usart.rs
└── example_common.rs
3 directories, 12 files
As in every Rust/Cargo
project, the file Cargo.toml
contains the library
dependencies for the project. Notice the hidden .cargo/config.toml
file. It
contains some additional configuration used to build the binary, upload it into
the chip and debug it. The code is, as usual, into the src/
directory. There
are several examples, each of them into a file in the bin/
directory. Before
diving into the code, let's setup the project for your specific chip.
First, to take into account its specific memory layout, edit the following line
in Cargo.toml
, in the [dependencies]
block:
embassy-stm32 = { version = "0.1.0", path = "../../embassy-stm32", features = ["defmt", "defmt-trace", "stm32f429zi", "unstable-pac", "memory-x", "time-driver-tim2"] }
As you may have noticed, this line contains a chip name: stm32f429zi
. Change
it to match your chip (eg: stm32f407vg
for STM32F407VGT6). This is used by
"memory-x"
feature to generate a memory.x
file describing the memory of the
chip, so that the linker knows where to put each part of your program.
Advanced users: you can also supply a custom memory.x
file by putting it in
your project root (where Cargo.toml
sits) - in this case remove the memory-x
feature.
Another component needs to know about the chip: probe-run
, which interacts
with a debugger to upload the program into the chip and get debug informations.
Open .cargo/config.toml
and edit the following line to match your chip (eg:
STM32F407VGTx
for STM32F407VGT6).
runner = "probe-run --chip STM32F429ZITx"
Now let's check that the blinky.rs
program runs on the chip:
cargo run --bin blinky
This may download things, then it compiles the code and finally:
Finished dev [unoptimized + debuginfo] target(s) in 1m 56s
Running `probe-run --chip STM32F407VGTx target/thumbv7em-none-eabi/debug/blinky`
(HOST) INFO flashing program (71.36 KiB)
(HOST) INFO success!
────────────────────────────────────────────────────────────────────────────────
0 INFO Hello World!
└─ blinky::__embassy_main::task::{generator#0} @ src/bin/blinky.rs:18
1 INFO high
└─ blinky::__embassy_main::task::{generator#0} @ src/bin/blinky.rs:23
2 INFO low
└─ blinky::__embassy_main::task::{generator#0} @ src/bin/blinky.rs:27
3 INFO high
└─ blinky::__embassy_main::task::{generator#0} @ src/bin/blinky.rs:23
4 INFO low
└─ blinky::__embassy_main::task::{generator#0} @ src/bin/blinky.rs:27
It works! Press ctrl-c
to stop the program and the debugger. Now let's switch
to the funniest part: let's take a look at the code!
Open src/bin/blinky.rs
.
#![no_std]
#![no_main]
To put it in a nutshell, this tells the compiler this program has no access to
std
and that there is no main
function (because it is not run by an OS).
Then some other macros are used to tell the compiler to use some of its unstable
features.
Then it imports some stuff used in this program. Notice that it uses
example_common
, which is not a library: it is another example file. Actually
for this specific example program the example_common
module can be replaced
by:
use defmt::{info, unwrap}; // macros to log messages
use defmt_rtt as _; // indicates that the log target is the debugger
use panic_probe as _; // defines behavior on panic
There is also a macro call,
defmt::timestamp!
, which prepends some text to all the messages that will be printed via the debugger.
Then, there is the main function, which is the function run when the chip starts receiving power or on reset:
#[embassy::main]
async fn main(_spawner: Spawner, p: Peripherals) {
It is declared as such using the embassy::main
macro. It is conventional to
name this function main
but actually you can give it the name you want. This
function receives a Spawner
, which can be used to spawn tasks, and a
Peripherals
structure to use the peripherals of the chip.
This function is
async
, to enable the use of asynchronous things inside it.
This main
function first prints Hello World! through the debugger.
info!("Hello World!");
Then it defines an output on pin PB7
. The initial logical level is set to
High
, and the speed of this peripheral is not critical here so its speed is
set to Low
.
let mut led = Output::new(p.PB7, Level::High, Speed::Low);
This variable has to be mutable to be able to change the pin state later, to make the led blink.
Then, there is an infinite loop.
loop {
info!("high");
unwrap!(led.set_high());
Timer::after(Duration::from_millis(300)).await;
info!("low");
unwrap!(led.set_low());
Timer::after(Duration::from_millis(300)).await;
}
On each iteration, it prints some text, updates the LED state and waits for
300 ms. .await
indicates that other things can be done during this delay.
But... Wait... For now, there is nothing else to be done!
Let's asynchronously:
- print through the debugger
- blink the LED
First, write the two functions:
async fn blink_led(mut led: Output<'_, embassy_stm32::peripherals::PB7>) {
const DURATION: Duration = Duration::from_millis(300);
loop {
unwrap!(led.set_high());
Timer::after(DURATION).await;
unwrap!(led.set_low());
Timer::after(DURATION).await;
}
}
async fn blink_info() {
const DURATION: Duration = Duration::from_millis(1000);
loop {
info!("high");
Timer::after(DURATION).await;
info!("low");
Timer::after(DURATION).await;
}
}
The durations are not the same to be able to notice asynchronism: the LED will blink faster than the prints.
Now let's use these functions in main
:
#[embassy::main]
async fn main(_spawner: Spawner, p: Peripherals) {
let led = Output::new(p.PB7, Level::High, Speed::Low);
futures::join!(blink_led(led), blink_info());
}
You can run this program and notice asynchronism.
If you have already used async
Rust, you already know the join!
macro. It
runs all the futures (~ async function calls) asynchronously until all of them
end. In our case, none of them end so there is another way to write this code:
we can spawn the tasks. This is a way to start doing something and do not wait
for it to end.
First, add #[embassy::task]
before both the two tasks:
#[embassy::task]
async fn blink_led(mut led: Output<'static, embassy_stm32::peripherals::PB7>) {
The caller does not wait for the task to end so the borrowed data it receives must have
'static
lifetime
#[embassy::task]
async fn blink_info() {
Now, let's update main
to spawn these tasks:
#[embassy::main]
async fn main(spawner: Spawner, p: Peripherals) {
let led = Output::new(p.PB7, Level::High, Speed::Low);
spawner.spawn(blink_led(led)).unwrap();
spawner.spawn(blink_info()).unwrap();
}
That's it! embassy
makes embedded asynchronous Rust as simple as that. Now
you can create your own project.
TODO: Write section based on https://embassy.dev/book/dev/new_project.html
This is the old version of this tutorial
Embassy has two options for creating a project: the basic and advanced API. The basic API assumes that every task runs at the same priority and that delays will be needed. The advanced API allows multiple-priority tasks and usage with other frameworks such as RTIC, but is more complex to set-up. Here, we will begin with the basic API.
Though embassy does not require a particular HAL, the stm32-rs
HAL is used to
configure clocks if the basic API is used. Other HALs may be used with other
peripherals, provided that they also use the standard svd2rust PAC.
To begin, let's create our main
function:
#[embassy::main(use_hse = 48)]
async fn main(spawner: Spawner) {
let (dp, clocks) = embassy_stm32::Peripherals::take().unwrap();
spawner.spawn(run1()).unwrap();
}
First, we the write main function and add an embassy::main
attribute. The
embassy::main
attribute sets-up a simple clock and executor
configuration
using the specified arguments. In this case we use a high-speed external
oscillator running at 48 Mhz. Then, we take our device peripherals with
embassy_stm32::Peripherals::take()
. This replaces the standard
pac::Peripherals::take()
call, because embassy takes some of the peripherals
during set-up. If the basic set-up is used, pac::Peripherals::take()
will
return None
, though cortex_m::Peripherals::take()
can still be used as
normal. Finally, we use the spawner to spawn a task.
Every task is declared with the task macro, as such. Every task is a standard
async
function defined with the task
attribute. Only functions defined with
the task
attribute can be spawned with the spawner
. Tasks can accept any
number of arguments, but they cannot be defined with generic parameters.
#[task]
async fn run1() {
loop {
info!("tick");
Timer::after(Duration::from_ticks(13000 as u64)).await;
}
}