Run at Full Speed
According to the datasheet, STM32F103 MCU can run at the maximum frequency of 72 MHz. But by default it runs at only 8 MHz. To achieve the full potential of the chip, the system frequency should be raised in the run-time.
There are three options for the system clock source:
-
HSI (High Speed Internal) - an RC oscillator running at constant 8 MHz and sitting inside the MCU chip. It is the default source for the system clock selected at the start-up.
-
HSE (High Speed External) - an optional external resonator component in the range from 4 to 16 MHz. A Blue Pill board has a 8 MHz crystal connected to the MCU (the component in a metal case right beside the MCU marked as Y2.)
-
PLL (Phase-Locked Loop) - a peripheral inside the MCU that can be used as a multiplier for HSI or HSE. The maximum multiplier for HSI is 8, which can give us 64 MHz, and for HSE - 16, which can theoretically result in 128 MHz, but the output frequency of PLL shouldn't exceed 72 MHz.
Given the above, in order to achieve 72 MHz, we should take the following steps:
- Start the HSE oscillator and wait for it to stabilize.
- Start the PLL with the HSE input and the multiplier of 9. Wait for it to stabilize.
- Select the PLL as the source for the system clock.
For a start, let's create a module for our project-level constants. Create a new
file at src/consts.rs
with the following content:
#![allow(unused)] fn main() { //! Project constants. /// HSE crystal frequency. pub const HSE_FREQ: u32 = 8_000_000; /// PLL multiplication factor. pub const PLL_MULT: u32 = 9; /// System clock frequency. pub const SYS_CLK: u32 = HSE_FREQ * PLL_MULT; }
And register the module in the src/lib.rs
:
#![allow(unused)] fn main() { pub mod consts; }
When the application will need to wait for HSE and PLL clocks stabilization, we don't want it to be constantly checking the flags wasting CPU cycles and energy, but rather to subscribe for an interrupt and sleep until it is triggered. We will use the RCC interrupt for this purpose:
From the table above, which can be found in the Reference Manual, we only need
the position of the RCC interrupt. Let's put this interrupt to the application
Vector Table. For this you need to edit thr::nvic!
macro in src/thr.rs
. By
default it looks like this:
#![allow(unused)] fn main() { thr::nvic! { // ... The header is skipped ... threads => { exceptions => { /// All classes of faults. pub hard_fault; }; }; } }
There is only a HardFault handler defined. Note that according the above table, HardFault doesn't have a position number, therefore it is referred only by its name. We need to add a new interrupt handler at the position of 5:
#![allow(unused)] fn main() { thr::nvic! { // ... The header is skipped ... threads => { exceptions => { /// All classes of faults. pub hard_fault; }; interrupts => { /// RCC global interrupt. 5: pub rcc; }; }; } }
Since the new handler has a numeric position, the name can be arbitrary.
Let's open the root task handler at src/tasks/root.rs
. By default it looks
like this:
#![allow(unused)] fn main() { //! The root task. use crate::{thr, thr::ThrsInit, Regs}; use drone_cortexm::{reg::prelude::*, thr::prelude::*}; /// The root task handler. #[inline(never)] pub fn handler(reg: Regs, thr_init: ThrsInit) { let thr = thr::init(thr_init); thr.hard_fault.add_once(|| panic!("Hard Fault")); println!("Hello, world!"); // Enter a sleep state on ISR exit. reg.scb_scr.sleeponexit.set_bit(); } }
In Drone OS the very first task with the lowest priority named root. Its
function handler is called by the program entry point at
src/bin/bluepill-blink.rs
, after finishing unsafe initialization routines. The
root handler receives two arguments of types Regs
and ThrsInit
. Both are
zero-sized types implementing Token
trait, which permits existence of only one
instance at a time. Instantiating a Token
type is unsafe, that is why it is
done inside the unsafe entry point at src/bin/bluepill-blink.rs
.
The reg
argument is a set of tokens for all available memory-mapped
registers. It is supposed to be destructured into individual register tokens or
register field tokens within the root handler.
The second thr_init
argument's purpose is to pass it to thr::init
function. The function runs an initialization routine for threading system and
returns an instance of Thrs
type. Thrs
is also a zero-sized type similar to
Regs
, but for thread tokens.
The root handler adds a one-shot fiber to the HardFault thread. The fiber body
is just a call to the panic!
macro. Drone handles panics by writing the panic
message to the log output at port #1, issuing a self-reset request, and blocking
until it's executed.
Let's add a new async
function that will be responsible for raising the system
clock frequency to 72 MHz. It will need some registers from RCC and FLASH
peripherals, as well as the RCC thread token.
#![allow(unused)] fn main() { //! The root task. use crate::{ consts::{PLL_MULT, SYS_CLK}, thr, thr::ThrsInit, Regs, }; use drone_core::log; use drone_cortexm::{fib, reg::prelude::*, swo, thr::prelude::*}; use drone_stm32_map::reg; /// The root task handler. #[inline(never)] pub fn handler(reg: Regs, thr_init: ThrsInit) { let thr = thr::init(thr_init); thr.hard_fault.add_once(|| panic!("Hard Fault")); raise_system_frequency( reg.flash_acr, reg.rcc_cfgr, reg.rcc_cir, reg.rcc_cr, thr.rcc, ) .root_wait(); println!("Hello, world!"); // Enter a sleep state on ISR exit. reg.scb_scr.sleeponexit.set_bit(); } async fn raise_system_frequency( flash_acr: reg::flash::Acr<Srt>, rcc_cfgr: reg::rcc::Cfgr<Srt>, rcc_cir: reg::rcc::Cir<Srt>, rcc_cr: reg::rcc::Cr<Srt>, thr_rcc: thr::Rcc, ) { // TODO raise the frequency to 72 MHz } }
An async
function is a syntax sugar for a function returning a custom type
implementing Future
trait. We execute the returned future using the
.root_wait()
method. The root_wait
method is supposed to be used inside a
thread with the lowest priority, e.g. in the root task context. Otherwise the
threads that are currently preempted may be stalled. Another option for
executing futures is to use exec
or add_exec
methods on thread tokens.
It's good to check that the program still works:
$ just flash
$ just log
Let's start filling out the raise_system_frequency
function. First, we need to
enable the RCC interrupt in NVIC (Nested Vectored Interrupt Controller), and
allow the RCC peripheral to trigger the interrupt when HSE or PLL is stabilized:
#![allow(unused)] fn main() { thr_rcc.enable_int(); rcc_cir.modify(|r| r.set_hserdyie().set_pllrdyie()); }
Then we're enabling the HSE clock and waiting until it's stabilized:
#![allow(unused)] fn main() { // We need to move ownership of `hserdyc` and `hserdyf` into the fiber. let reg::rcc::Cir { hserdyc, hserdyf, .. } = rcc_cir; // Attach a listener that will notify us when RCC_CIR_HSERDYF is asserted. let hserdy = thr_rcc.add_future(fib::new_fn(move || { if hserdyf.read_bit() { hserdyc.set_bit(); fib::Complete(()) } else { fib::Yielded(()) } })); // Enable the HSE clock. rcc_cr.modify(|r| r.set_hseon()); // Sleep until RCC_CIR_HSERDYF is asserted. hserdy.await; }
And similarly enable the PLL:
#![allow(unused)] fn main() { // We need to move ownership of `pllrdyc` and `pllrdyf` into the fiber. let reg::rcc::Cir { pllrdyc, pllrdyf, .. } = rcc_cir; // Attach a listener that will notify us when RCC_CIR_PLLRDYF is asserted. let pllrdy = thr_rcc.add_future(fib::new_fn(move || { if pllrdyf.read_bit() { pllrdyc.set_bit(); fib::Complete(()) } else { fib::Yielded(()) } })); rcc_cfgr.modify(|r| { r.set_pllsrc() // HSE oscillator clock selected as PLL input clock .write_pllmul(PLL_MULT - 2) // output frequency = input clock × PLL_MULT }); // Enable the PLL. rcc_cr.modify(|r| r.set_pllon()); // Sleep until RCC_CIR_PLLRDYF is asserted. pllrdy.await; }
The flash memory settings should be tweaked because of increased frequency:
#![allow(unused)] fn main() { // Two wait states, if 48 MHz < SYS_CLK <= 72 Mhz. flash_acr.modify(|r| r.write_latency(2)); }
Before increasing the frequency, we should wait until the currently ongoing SWO
transmission is finished if any. And also update the SWO prescaler to maintain
the fixed baud-rate defined at the project's Drone.toml
. Note that if a debug
probe is not connected, this will be a no-op, thus it's safe to keep this in the
release binary.
#![allow(unused)] fn main() { swo::flush(); swo::update_prescaler(SYS_CLK / log::baud_rate!() - 1); }
And finally switch the source for the system clock to PLL:
#![allow(unused)] fn main() { rcc_cfgr.modify(|r| r.write_sw(0b10)); // PLL selected as system clock }
Here is the final listing of the raise_system_frequency
function:
#![allow(unused)] fn main() { async fn raise_system_frequency( flash_acr: reg::flash::Acr<Srt>, rcc_cfgr: reg::rcc::Cfgr<Srt>, rcc_cir: reg::rcc::Cir<Srt>, rcc_cr: reg::rcc::Cr<Srt>, thr_rcc: thr::Rcc, ) { thr_rcc.enable_int(); rcc_cir.modify(|r| r.set_hserdyie().set_pllrdyie()); // We need to move ownership of `hserdyc` and `hserdyf` into the fiber. let reg::rcc::Cir { hserdyc, hserdyf, .. } = rcc_cir; // Attach a listener that will notify us when RCC_CIR_HSERDYF is asserted. let hserdy = thr_rcc.add_future(fib::new_fn(move || { if hserdyf.read_bit() { hserdyc.set_bit(); fib::Complete(()) } else { fib::Yielded(()) } })); // Enable the HSE clock. rcc_cr.modify(|r| r.set_hseon()); // Sleep until RCC_CIR_HSERDYF is asserted. hserdy.await; // We need to move ownership of `pllrdyc` and `pllrdyf` into the fiber. let reg::rcc::Cir { pllrdyc, pllrdyf, .. } = rcc_cir; // Attach a listener that will notify us when RCC_CIR_PLLRDYF is asserted. let pllrdy = thr_rcc.add_future(fib::new_fn(move || { if pllrdyf.read_bit() { pllrdyc.set_bit(); fib::Complete(()) } else { fib::Yielded(()) } })); rcc_cfgr.modify(|r| { r.set_pllsrc() // HSE oscillator clock selected as PLL input clock .write_pllmul(PLL_MULT - 2) // output frequency = input clock × PLL_MULT }); // Enable the PLL. rcc_cr.modify(|r| r.set_pllon()); // Sleep until RCC_CIR_PLLRDYF is asserted. pllrdy.await; // Two wait states, if 48 MHz < SYS_CLK <= 72 Mhz. flash_acr.modify(|r| r.write_latency(2)); swo::flush(); swo::update_prescaler(SYS_CLK / log::baud_rate!() - 1); rcc_cfgr.modify(|r| r.write_sw(0b10)); // PLL selected as system clock } }