STM32F4 Embedded Rust at the PAC: svd2rust
Tags: rust, tutorial, iot, embedded
Published: 2023-02-06 21:21:27

## Introduction

When I set out on learning embedded Rust, I recall one of my struggles is material spanning multiple abstraction levels. As might be known in embedded Rust, the abstraction level sitting directly on top of the controller is the peripheral access crate (PAC). The PAC gives access to the controller registers to configure and control controller functions. On top of the PAC sits the hardware abstraction layer (HAL), providing higher-level abstractions and safe code assurances as well. Although some resources would mention that they target for example the HAL, the material would mix in code from other levels like the PAC. The ease with which one can mix abstraction levels in Rust is something to admire, though, in the context of learning, it was a confusing factor. It really made me wonder if I correctly understood the abstractions.

In a prior blog series I wrote, "[STM32F4 with Embedded Rust at the HAL](https://apollolabsblog.hashnode.dev/series/stm32f4-embedded-rust-hal)", I covered examples of various peripherals sticking to the HAL. In this post, I will be starting a new series doing something similar but rather sticking to the PAC. This would mean something a bit different in which context about the STM32 peripheral registers is required. At the HAL, this wasn't required as we used methods that described configurations and functions. Register manipulations happened under the hood, and other than knowing controller features, not much understanding of the registers was required.

Developing code at the PAC, well, requires a PAC crate for the targeted controller. For the STM32 there exists a [repo](https://github.com/stm32-rs/stm32-rs) for all the supported PACs. These PACs are all generated using a command line tool called [svd2rust](https://docs.rs/svd2rust/latest/svd2rust/). svd2rust grabs what is called an svd file and converts it into a PAC exposing API allowing access to peripheral registers. An SVD file is an Extensible Markup Language (XML) formatted file describing the hardware features of a device, listing all the peripherals and the registers associated with them. SVD files typically are released by microcontroller manufacturers.

In the context of STM32F4, there is already a PAC available publicly, so it can be leveraged directly in the project dependencies. However, there are cases one might run into for a newer controller or one that does not have an existing PAC. As such, one would have to go through generating a PAC using svd2rust. In this post, I go through an example of the steps to generate a PAC from an SVD file for the STM32F401 device. The steps should be more or less the same for any other device as long as an SVD file exists.

#### If you find this post useful, and if Embedded Rust interests you, stay in the know and skyrocket your learning curve by subscribing to The Embedded Rustacean newsletter:

{% cta  http://www.theembeddedrustacean.com %} Subscribe Now to The Embedded Rustacean{% endcta%} 

## Step 1 - Install svd2rust 💾

svd2rust can be easily installed in the command line using the following cargo command:

```bash
$ cargo install svd2rust
```

## Step 2 - Create a Library Package 📚

The files generated by svd2rust need to be contained in a library package. This is done through cargo using the `new` command and named the crate `stm32f401_pac`:

```bash
$ cargo new stm32f401_pac --lib
```

## Step 3 - Locate and Download SVD File 🗂️

ST microelectronics keeps a full list of zip files containing SVDs for different families of the STM32 on their own [website](https://www.st.com/content/st_com/en/search.html#q=svd-t=resources-page=1). Consequently, I downloaded the STM32F4 System View Description zip file and unzipped it. After that, I navigated to find the STM32F401.svd file and placed it in the library package folder I created in step 2.

## Step 4 - Generate Rust Files ⚙️

In this step, I execute all the commands as indicated by the svd2rust [documentation](https://docs.rs/svd2rust/latest/svd2rust/). Here there are two things to note. First, the controller core architecture needs to be known so that the correct target can be specified (Cortex-M for the STM32F4). Second, if the `form` command line tool needs to be installed if it's not, this is done through cargo as follows:

```bash
$ cargo install form
```

Next, one needs to execute the commands as indicated by svd2rust documentation:

```bash
$ svd2rust -i STM32F30x.svd
$ rm -rf src
$ form -i lib.rs -o src/ && rm lib.rs
$ cargo fmt
```

Additionally, in the same library package `Cargo.toml` the necessary dependencies and features need to be added:

```rust
[dependencies]
critical-section = { version = "1.0", optional = true }
cortex-m = "0.7.6"
cortex-m-rt = { version = "0.6.13", optional = true }
vcell = "0.1.2"

[features]
rt = ["cortex-m-rt/device"]
```

At this point, we essentially have a PAC that can be imported into other projects!

## Step 5 - Import PAC into Project 📥

Now we would need to create a new binary project and import the PAC we just created so that we can use it. I navigated to the same folder I placed the `stm32f401_pac` folder in and ran the following command:

```bash
$ cargo new stm32f401_pactest --bin
```

In `Cargo.toml` of the new binary I included the necessary dependencies:

```rust
[package]
name = "stm32401_pactest"
version = "0.1.0"
edition = "2021"

[dependencies]
cortex-m = { version = "0.7.7", features = ["critical-section-single-core"] }
stm32f401_pac = { path = "../stm32f401_pac", features = ["rt", "critical-section"] }
panic-halt = "0.2.0"
cortex-m-rt = "0.7.2"
```

There are a few things to note here. In the `stm32f401_pac` dependency, a path is provided to the `stm32f401_pac` package that was created earlier. Also, note the `features` included. The svd2rust documentation states that the `take` method used to get an instance of the device peripherals needs a `critical-section` implementation provided. As such, the implementation of `critical-section` is provided through the `cortex-m` dependency through `critical-section-single-core`.

After that, I wanted to do a test to make sure that everything builds ok. In `main.rs` I wrote the code below. The code doesn't necessarily do anything useful. It only obtains a handle for the peripherals and then loops forever.

```rust
#![no_std]
#![no_main]

use cortex_m_rt::entry;
use panic_halt as _;
use stm32f401_pac::Peripherals;

#[entry]
fn main() -> ! {
    let per = Peripherals::take().unwrap();
    loop {}
}
```

The code is then built with cargo specifying the target architechture:

```bash
$ cargo build --target thumbv7em-none-eabihf
```

> 📝 **Note:** At first I tried to achieve step 5 by naievly placing a `main.rs` in the `stm32f401_pac` folder thinking I can build my code from there. This created a conflict where the features specified in the cargo.toml did not get recognized.

## The PAC API

In Rust a singleton pattern is adopted. This means that only one instance of a device peripherals can exist. As such, access to peripherals is obtained through the `take` method. In the earlier code, this was done in the `let per = Peripherals::take().unwrap();` line. The `take` method provides an instance to the `Peripherals` struct and returns an `Option` . Due to the singleton pattern, any subsequent calls beyond the first one will return a `None` . The `per` handle is later used to create instances to specific peripherals.

After obtaining access to the peripherals, the PAC provides `read`, `modify`, and `write` methods to manipulate device registers. Essentially giving access to individual bits. The type of manipulation allowed in a register depends on what is specified in the datasheet. More detail on this will follow in example posts. Generally, as will be seen going forward, PAC code takes the following form:

```rust
[Peripheral Handle].[Peripheral Register Name].[Operation]
```

`[Operation]` being one of the `read`, `modify`, and `write` methods. In the following posts, examples will be demonstrated for using this PAC access API to control and configure peripherals.

## Conclusion

The peripheral access crate (PAC) is a lower lever of abstraction in embedded Rust. PACs provide type-safe access to peripheral registers through API that allows manipulation of individual bits. PACs can also be generated using manufacturer SVD files that describe controllers and a command line tool called svd2rust. In this post, I walk through the steps of creating a PAC using the svd2rust tool. This post is also the first part of a series experimenting with STM32 peripherals using PAC access API. Have any questions/comments? Share your thoughts in the comments below 👇.

#### If you found this post useful, and if Embedded Rust interests you, stay in the know and skyrocket your learning curve by subscribing to The Embedded Rustacean newsletter:

{% cta  http://www.theembeddedrustacean.com %} Subscribe Now to The Embedded Rustacean{% endcta%}