The TLDR version Link to heading

This will be a long post, so if you are looking for the quick solution, here it is:

This code has been tested against rust version 1.75.0

[dependencies]
embassy-stm32 = { version = "0.1.0", features = ["defmt", "stm32f411ce", "unstable-pac", "memory-x", "time-driver-any", "exti", "chrono"]  }
embassy-sync = { version = "0.5.0", features = ["defmt"] }
embassy-executor = { version = "0.5.0", features = ["task-arena-size-32768", "arch-cortex-m", "executor-thread", "executor-interrupt", "defmt", "integrated-timers"] }
embassy-time = { version = "0.3.0", features = ["defmt", "defmt-timestamp-uptime", "tick-hz-32_768"] }
defmt = "0.3"
defmt-rtt = "0.4"
cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] }
cortex-m-rt = "0.7.0"
embedded-hal = "1.0.0"
panic-probe = { version = "0.3", features = ["print-defmt"] }
let mut device_config = embassy_stm32::Config::default();

device_config.rcc.hsi = false;
device_config.rcc.hse = Some(Hse {
    freq: mhz(25),
    mode: HseMode::Oscillator,
});

device_config.rcc.pll_src = PllSource::HSE;
device_config.rcc.pll = Some(Pll {
    prediv: PllPreDiv::DIV25,
    mul: PllMul::MUL400,
    divp: Some(PllPDiv::DIV4),
    divq: None,
    divr: None,
});

device_config.rcc.sys = Sysclk::PLL1_P;

device_config.rcc.ahb_pre = AHBPrescaler::DIV1;
device_config.rcc.apb1_pre = APBPrescaler::DIV2;
device_config.rcc.apb2_pre = APBPrescaler::DIV1;

let device_peripherals = embassy_stm32::init(device_config);

But if you want to know how it all works, read on.

Background Link to heading

The default settings in embassy for the STM32F411 lets it run at 16MHz as shown in the debug output, e.g. sys: Hertz(16000000).

# default configuration options
let mut device_config = embassy_stm32::Config::default();
let device_peripherals = embassy_stm32::init(device_config);

# debug output
0.000000 DEBUG flash: latency=0
└─ embassy_stm32::rcc::_version::init @ /home/brendan/.cargo/registry/src/index.crates.io-6f17d22bba15001f/embassy-stm32-0.1.0/src/fmt.rs:130 
0.000000 DEBUG rcc: Clocks { sys: Hertz(16000000), pclk1: Hertz(16000000), pclk1_tim: Hertz(16000000), pclk2: Hertz(16000000), pclk2_tim: Hertz(16000000), hclk1: Hertz(16000000), hclk2: Hertz(16000000), hclk3: Hertz(16000000), plli2s1_q: None, plli2s1_r: None, pll1_q: None, rtc: Some(Hertz(32000)) }

Let’s get it running faster. The ‘Black Pill’ board that I’m using has an external 25MHz crystal which means we should be able to run the board at 100MHz. But to do that, we need to change some of the clock configuration options.

A quick look at the 844 page datasheet shows us that the clocks of the STM32 are controlled by the Reset and clock control (RCC) registers (page 90 of RM0383 Rev 3). We can set these registers as we start the board to configure how the clocks work. These registers are set to some defaults as we start the board.

Thankfully, there are hardware abstraction layers that make this process a bit easier.

Info

Before going into this, I am going to make a few assumptions:

  • that you know what STM32 (chipset), rust (programming language), and embassy (framework) are.
  • that you have rust installed
  • that you have an STM32 board
  • that you have been able to get some example code using the embassy framework running on your STM32 board Omar Hiari has put together some very detailed walkthroughs on how to do these things. He uses a slightly different chip to what I’m using, but the overall approach is similar.

Finding the default settings Link to heading

Now we start to manually configure how the board works.

If you’ve read Omar Hiari’s work you’ll notice that it loads the default configuration when the STM32 board starts.

let p = embassy_stm32::init(Default::default());

The first thing I’ve done is break up that command into two steps. First, load the default configuration into a struct. Then, use the values of that struct to initialise the board. This lets us modify the configuration before we use it.

// Load the default configuration as a variable
let mut device_config = embassy_stm32::Config::default();

// We can make some changes to the `device_config` here

// Use the configuration to initialise the board
let device_peripherals = embassy_stm32::init(device_config);

After we go to the Embassy STM32 HAL documentation page and select our chip, we can go to the page for the “config” struct.

“Embassy STM32 HAL documentation page”

The documentation for the config struct shows the fields that are part of the struct (rcc, enable_debug_during_sleep, and dma_interrupt_priority) and where we can find out what is implemented in Config::default(). Both links go to the same piece of source code. The code for the impl is just a wrapper for the function.

Embassy STM32 HAL config struct documentation

Looking at the source code for Config::default(), we can see that it actually exposes five fields, not just the three that we saw previously.

Embassy STM32 HAL config struct source code

This must be because the STM32F411 does not have bdma or gpdma. Recalling that the clocks are controlled by the rcc register, we can see that the rcc field in the struct is another struct that has its own set of defaults. Going back to the STM32 config struct, we can click on the Config link to see what is in the config struct.

Embassy STM32 HAL config struct documentation

Same as the HAL config struct, the RCC config struct exposes lots of fields.

RCC config struct documentation

These fields correspond to the configurable bits in the RCC registers section 6.3 of the 844 page manual

The source code tells us what values are loaded into the rcc register by default:

Source code for embassy STM32 HAL rcc defaults

These defaults tell the STM32F411 to:

  • use the 16MHz high speed internal (HSI) oscillator (hsi: true),
  • ignore the the high speed external (HSE) clock source (hse: None),
  • use the HSI as the system clock (sys: Sysclk::HSI),
  • use the HSI as the input for the phase lock loop (PLL) (pll_src: PllSource::HSI),
  • but don’t use the PLL (pll: None) or PLL for the I2S system (plli2s: None),
  • and don’t divide the system clock (ahb_pre:, apb1_pre:, and apb2_pre: APBPrescaler::DIV1), and
  • use the default settings for the low speed clock (ls: Default::default())
    • Use the internal 32kHz oscillator as the clock source (rtc: RtcClockSource::LSI),
    • Use the internal 32kHz oscillator (lsi: true),
    • Don’t use an external low speed clock (lse: None).

The three lines that start with #[cfg(any tell us that certain configuration options are only available on certain chip. For example, the STM32F411 has a plli2s but not a pllsai.

This matches what we saw when we first started the board. The debug output (sys: Hertz(16000000)) shows us that the board is running at 16MHz.

Getting the board to run at 100MHz Link to heading

So now that we know how the default settings are applied to the clock, we can start to work out what we need to adjust to get the board running at 100MHz.

Figure 12 (p. 93) of the manual shows the “clock tree”. This is just a diagram of how the ‘clock sources’ (squares on the left of the page) are modified (smaller squares, e.g. /M) and switches (trapezoids) are used to select the ‘clock output’ (arrows pointing to the right). Examining the diagram, we can see that the labels of parts of the clock tree diagram match up with the fields in the rcc register we look at previously. For example:

  • LSI RC 32kHz = Low Speed Internal 32kHz clock which is enabled (lsi: true) and used (rtc: RtcClockSource::LSI),
  • 16MHz HSI RC = High Speed Internal 16MHz clock which is enabled (hsi: true) and used as the system clock (sys: Sysclk::HSI),
  • PLL = Phase Locked Loop which is set to take input from the HSI (pll_src: PllSource::HSI) but is not used (pll: None).

STM32F411 Clock Tree

Documentation for using the 32.768 kHz oscillator Link to heading

Tip
Before we get the system clock running at 100 Mhz, we’ll walk through an slightly easier example of how to get the system to use the external 32.768 kHz oscillator as the low speed clock source.

The Black Pill board comes with an external 32.768 kHz oscillator, but the embassy framework disables it by default and uses the internal oscillator. We want to:

  • disable the internal low speed clock source (red cross on the diagram),
  • enable the external low speed clock source (left red arrow), and
  • tell the RTCSEL to use the external low speed clock source (right red arrow). The blue arrow just shows the flow of the clock source.

STM32F411 RTC clock diagram

Earlier, we looked at the defaults for the low speed clocks. The LsConfig struct contains three fields:

  • rtc
  • lsi
  • lse

By default, they are set as follows:

  • rtc: RtcClockSource::LSI
  • lsi: true
  • lse: None

The type of each field and the default settings should give us a clue as to how they are set. rtc seems to be set by selecting from the RtcClockSource list. lsi is set to true, so it is probably a boolean value. lse is set to None and its type is Option<LseConfig> so there’s a few options that need to be set.

RTC Link to heading

To set the RTC clock source, we select from an enum that offers four possibilities:

  • DISABLE = 0
  • LSE = 1
  • LSI = 2
  • HSE = 3

We want to use LSE, so we would simply change RtcClockSource::LSI to RtcClockSource::LSE to get the chip to use the external oscillator.

LSI Link to heading

Disabling the LSI is as simple as changing true to false.

LSE Link to heading

Setting up the LSE requires a bit more reading. By default, the LSE is set to None. To set up the LSE, were going to need to change None to Some(LseConfig {}) and set its fields.

LseConfig is a struct that contains two fields:

  • frequency with an associated type Hertz
  • mode with a type LseMode

Putting these into LSE config (or getting the rust language server to do the work) gives us:

Some(LseConfig {
    frequency: (),
    mode: ()
})

The field frequency needs to be set to something that provides the type Hertz. Going to the documentation for Hertz:

Link to Hertz documentation

We can see that it is a struct provided by the embassy_stm32::time module. This struct provides functions that take a number and turn it into a frequency. The external low speed oscillator on the board runs at 32.768 kHz, so we can set frequency: hz(32768).

Embassy STM32 Hertz Documentation

This means that we need to make sure we import the embassy_stm32::time module with use embassy_stm32::time::*;

Our LSE config is now:

Some(LseConfig {
    frequency: hz(32768),
    mode: ()
})

Looking at LseMode shows us that we have another enum with two choices.

LseMode Documentation

We want to choose oscillator which means that we then have to set the LseDrive which is one of:

  • Low = 0
  • MediumLow = 1
  • MediumHigh = 2
  • High = 3

I couldn’t find much information about this option on the internet beyond this STM32 Application Note. In short, I picked Low.

So our final LseConfig is:

Some(LseConfig {
    frequency: hz(32768),
    mode: (LseMode::Oscillator(LseDrive::Low)),
})

The code for the external 32.768 kHz oscillator Link to heading

After all of that, we have out final code to tell the Embassy framework to use the external 32.768 kHz oscillator on the Black pill board:

#![no_std]
#![no_main]

use defmt::*;
use embassy_stm32::rcc::{LseConfig, LseDrive, LseMode, RtcClockSource};
use embassy_stm32::time::*;
use embassy_executor::Spawner;
use {defmt_rtt as _, panic_probe as _};

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let mut device_config = embassy_stm32::Config::default();

    device_config.rcc.ls.rtc = RtcClockSource::LSE;
    device_config.rcc.ls.lsi = false;
    device_config.rcc.ls.lse = Some(LseConfig {
        frequency: hz(32768),
        mode: (LseMode::Oscillator(LseDrive::Low)),
    });

    let device_peripherals = embassy_stm32::init(device_config);
}
Warning
And that was the easy part.

Getting the 100 MHz system clock running Link to heading

Info
The previous example went into a lot of detail looking at how to read the documentation and work out how to set each option. It was a lot just to write three measly statements over six lines of code. I can’t go into that much detail when outlining how to set the system clock, but I’ll outline what I needed to adjust.

To get the board to run at 100 MHz we need to:

  • Disable the HSI oscillator
  • Enable the HSE oscillator
  • Tell the PLL to take input from the HSE
  • Use the PLL to convert the HSE 25 MHz to 100 MHz output
  • Tell the SYSCLK to take its input from the PLL
  • Set the AHB prescaler to output 100 MHz
  • Set the APB1 prescaler to output 50 MHz
  • Set the APB2 prescaler to output 100 MHz

We know that we have to go through these steps if we follow the blue line in the screen shot below.

Clock Tree

Disabling the HSI is very easy:

device_config.rcc.hsi = false;

Enabling the HSE is quite similar to how we enabled the LSE earlier:

device_config.rcc.hse = Some(Hse {
    freq: mhz(25),
    mode: HseMode::Oscillator,
});

Telling the PLL to take input from the HSE:

device_config.rcc.pll_src = PllSource::HSE;

Configuring the PLL is a bit trickier and requires you to read section 6.3.2 of the reference manual. The output of the PLL is: input frequency, divided by M, multiplied by N, divided by P. But there are a few constraints:

  • The HSE frequency needs to be divided by M so that the output is between 1 Mhz and 2 MHz,
  • The output of M needs to be multiplied by N to that the output is between 100 MHz and 432 MHz
  • The output of N needs to be divided by P to that the output of the PLL is 100 MHz

There are probably a dozen ways to convert a 25 MHz clock source to a 100 MHz output. This worked for me:

device_config.rcc.pll = Some(Pll {
    prediv: PllPreDiv::DIV25,
    mul: PllMul::MUL400,
    divp: Some(PllPDiv::DIV4),
    divq: None,
    divr: None,
});

Telling the system clock to use the PLL output:

device_config.rcc.sys = Sysclk::PLL1_P;

And finally, setting various prescalers:

device_config.rcc.ahb_pre = AHBPrescaler::DIV1;
device_config.rcc.apb1_pre = APBPrescaler::DIV2;  // Needs to be 50 MHz max
device_config.rcc.apb2_pre = APBPrescaler::DIV1;

The code Link to heading

Putting this all together, we get the following minimal working code:

#![no_std]
#![no_main]

use embassy_executor::Spawner;
use {defmt_rtt as _, panic_probe as _};

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

    let mut device_config = embassy_stm32::Config::default();

    {
        use embassy_stm32::rcc::{
            AHBPrescaler, APBPrescaler, Hse, HseMode, LseConfig, LseDrive, LseMode, Pll, PllMul,
            PllPDiv, PllPreDiv, PllSource, RtcClockSource, Sysclk,
        };
        use embassy_stm32::time::*;
        device_config.enable_debug_during_sleep = true;
        device_config.rcc.hsi = false;
        device_config.rcc.hse = Some(Hse {
            freq: mhz(25),
            mode: HseMode::Oscillator,
        });
        device_config.rcc.pll_src = PllSource::HSE;
        device_config.rcc.pll = Some(Pll {
            prediv: PllPreDiv::DIV25,
            mul: PllMul::MUL400,
            divp: Some(PllPDiv::DIV4),
            divq: None,
            divr: None,
        });
        device_config.rcc.sys = Sysclk::PLL1_P;
        device_config.rcc.ahb_pre = AHBPrescaler::DIV1;
        device_config.rcc.apb1_pre = APBPrescaler::DIV2;
        device_config.rcc.apb2_pre = APBPrescaler::DIV1;

        device_config.rcc.ls.rtc = RtcClockSource::LSE;
        device_config.rcc.ls.lsi = false;
        device_config.rcc.ls.lse = Some(LseConfig {
            frequency: hz(32768),
            mode: (LseMode::Oscillator(LseDrive::Low)),
        });
    }

    let device_peripherals = embassy_stm32::init(device_config);
}

And running this code, we get the following output showing that we are running the main clock at 100 MHz and the RTC at 32.768 kHz:

DEBUG rcc: Clocks { 
    sys: Hertz(100000000),
    pclk1: Hertz(50000000), 
    pclk1_tim: Hertz(100000000), 
    pclk2: Hertz(100000000), 
    pclk2_tim: Hertz(100000000), 
    hclk1: Hertz(100000000), 
    hclk2: Hertz(100000000), 
    hclk3: Hertz(100000000), 
    plli2s1_q: None, 
    plli2s1_r: None, 
    pll1_q: None, 
    rtc: Some(Hertz(32768)) 
    }

Success.