In embedded programming, our software needs to interact directly with the hardware peripherals of the microcontroller. For example, we use UART to produce log messages and I2C to communicate with the HTS221 humidity and temperature sensor. These peripherals need to be initialized and configured before the application can use them. The STM32 itself also requires device-specific initialization, including its clock configuration.

Rust provides the language and its standard libraries, but these do not provide STM32-specific code for configuring clocks, GPIO pins, UART, I2C, or other microcontroller peripherals. In this project, we use Embassy to provide that hardware-specific support.

Embassy is a set of crates for embedded Rust development. The embassy-stm32 crate provides the STM32 hardware abstraction layer, or HAL. It gives us Rust types and peripheral drivers for configuring and controlling STM32 hardware without writing the register-level peripheral code ourselves.

For example, instead of manually configuring the STM32 registers associated with USART1, I2C2, or GPIO pin PH7, our program works with Embassy types such as UartTx, I2c, and Output. Embassy performs the STM32-specific operations required by those drivers, while our application specifies which peripherals and pins to use and how they should be configured.

Embassy also provides the asynchronous infrastructure used by this program. embassy-executor runs asynchronous tasks, while embassy-time provides the timer used by Timer::after_millis(...).await. Our UART and I2C drivers in this example are configured in blocking mode, while the heartbeat delay uses an asynchronous timer.

In our program, Embassy sits between the application and the STM32 hardware:

Embassy HAL

At the lowest level, STM32 peripherals are controlled by reading and writing memory-mapped hardware registers. A Peripheral Access Crate (PAC), such as stm32-metapac, provides Rust definitions and access to those registers. If we used the PAC directly, our code would still need to know which USART, I2C, or GPIO registers and fields must be configured for each operation.

embassy-stm32 builds a higher-level hardware abstraction layer on top of this lower-level register access. For example, instead of configuring the USART registers directly, our application creates a UartTx driver and calls methods such as blocking_write(). The same applies to I2c and Output. Embassy performs the STM32-specific register-level work required by those driver operations, while our application works with peripheral types and methods.

Embassy vs PAC

STM32 HAL Initialisation

The main hardware-specific crate is embassy-stm32. The following call initializes the HAL and returns the STM32 peripheral instances used by the application:

let peripherals = embassy_stm32::init(Config::default());

Those peripheral instances are then passed to Embassy driver types. For example, the program configures PH7 as a GPIO output:

let mut led = Output::new(
peripherals.PH7,
Level::High,
Speed::Low,
);

The LED can then be controlled through the Output methods:

led.set_low();
led.set_high();

The same approach is used for the UART and I2C peripherals. Embassy creates a UART transmitter around USART1:

let uart =
UartTx::new_blocking(
peripherals.USART1,
peripherals.PA9,
UartConfig::default(),
)
.unwrap();

It also creates an I2C driver around the STM32 I2C2 peripheral and the selected pins:

let mut i2c = I2c::new_blocking(
peripherals.I2C2,
peripherals.PH4,
peripherals.PH5,
i2c_config,
);

The application can then communicate with the HTS221 sensor through that driver:

i2c.blocking_write_read(
HTS221_ADDRESS,
&[HTS221_WHO_AM_I_REGISTER],
&mut who_am_i,
)

Embassy is also used for asynchronous execution. The program declares its main task with embassy-executor:

#[embassy_executor::main]
async fn main(_spawner: Spawner) {

embassy-executor runs this asynchronous task and polls it whenever it is ready to make progress. This allows the program to await operations such as the heartbeat timer:

Timer::after_millis(500).await;

Timer comes from embassy-time. The timer future becomes ready after the requested interval, allowing the task to wait without implementing its own timer handling.

The UART and I2C operations in this example are still blocking operations because they are created with new_blocking(). Embassy supports asynchronous programming, but an application can also use its blocking peripheral drivers where that is appropriate.

For this project, Embassy therefore provides the STM32 peripheral interface, the asynchronous executor, and the timing facilities used by the application. The application code can concentrate on operations such as reading the HTS221, writing diagnostic messages, and controlling the LED while Embassy handles the STM32-specific driver layer underneath those operations.

Part of the series Secure STM32 home monitoring with Rust STM32 IoT development board

Discover more from Tech For Talk

Subscribe to get the latest posts sent to your email.

Leave a Reply