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.
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.
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
.
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.
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
.
Same as the HAL config struct, the RCC config struct exposes lots of fields.
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:
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:
, andapb2_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
).
- Use the internal 32kHz oscillator as the clock source (
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
).
Documentation for using the 32.768 kHz oscillator Link to heading
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.
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 typeHertz
mode
with a typeLseMode
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
:
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)
.
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.
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);
}
Getting the 100 MHz system clock running Link to heading
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.
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.