Description
NOTE I don't think the idea presented here should live in this crate, but it could live in a crate maintained by the HAL team. In any case, the only reason I'm posting this here is that it's more likely to be seen by authors of HAL crates, but feel free to move this thread elsewhere.
Goal
Expose the core clock frequency in a device agnostic manner for use in device agnostic APIs. This piece of information is required to translate human time (e.g. milliseconds) into "core clock cycles" which is what time-related peripherals, like timers, work with.
Motivation
The cortex-m
crate provides an asm::delay(x)
API to perform a delay of x
clock cycles. Application authors usually don't (directly or indirectly) use that API because they want an API that works with human time like milliseconds.
Although asm::delay
is device agnostic (but not architecture agnostic) it's not possible to create a device agnostic asm::delay_ms
on top of it because the core clock (CPU) frequency can't be accessed in a device agnostic manner. The result? Code duplication: HAL crates end up providing their own delay_ms
API on top of asm::delay
or cortex_m::SYST
that takes or interacts with some struct that represents the clock configuration of the device.
With this proposal we could create a cortex-m-hal
crate that depends on cortex-m
and provides a device agnostic function like delay_ms(x: u32)
.
Design
We use the global singleton pattern to implement a crate with zero dependencies that provides access to the core clock frequency.
// crate: core-frequency
use core::sync::atomic::{AtomicU32, Ordering};
#[macro_export]
macro_rules! initial {
($initial:expr) => {
// TODO use a symbol name less likely to cause a collision
#[no_mangle]
static CORE_FREQUENCY: core::sync::atomic::AtomicU32 =
core::sync::atomic::AtomicU32::new($initial);
// IMPORTANT never expose this to application authors; they shouldn't be
// directly manipulating the core clock frequency
pub fn set_core_frequency(freq: u32) {
CORE_FREQUENCY.store(freq, core::sync::atomic::Ordering::Release)
}
};
}
/// Returns the core clock frequency in Hz
pub fn get() -> u32 {
extern "C" {
static CORE_FREQUENCY: AtomicU32;
}
unsafe { CORE_FREQUENCY.load(Ordering::Acquire) }
}
Then we can create a crate like cortex-m-hal
to provide methods like delay_ms
.
pub fn delay_ms(ms: u32) {
let cycles = (core_frequency::get() / 1_000) * ms;
cortex_m::asm::delay(cycles);
}
pub struct AsmDelay;
impl embedded_hal::DelayMs for AsmDelay {
pub fn delay_ms(ms: u32) {
delay_ms(ms);
}
}
pub struct SystDelay(SYST);
impl embedded_hal::DelayMs for SystDelay {
pub fn delay_ms(ms: u32) {
let cycles = (core_frequency::get() / 1_000) * ms;
// ..
}
}
HAL crates would integrate with core_frequency
like this:
// crate: stm32fxyz-hal
// the core clock frequency at reset (usually an internal RC clock)
core_frequency::initial!(8_000_000);
//~^ if the device needs an external oscillator then this need to be called by
// the end user (application author)
// Singleton proxy for the clock peripheral
pub struct ClockPeripheral {
_0: (),
}
// omitted: passing configuration like prescalers as arguments
//
// IMPORTANT: here you *need* a mutable (unique) reference to the clock
// peripheral to ensure that `CORE_FREQUENCY` will be set "atomically" with all
// the clock registers.
//
// "Why?" Imagine what would happen if you get preempted at spot (A) by an
// interrupt and the handler calls `configure_clocks`. If `configure_clocks`
// takes `&mut ClockPeripheral` that can't happen unless someone breaks Rust
// aliasing rules
pub fn configure_clocks(clock: &mut ClockPeripheral) {
// .. do stuff with registers ..
// (A)
// then
set_core_frequency(frequency_computed_above);
}
Other thoughts
This only uses atomic stores / loads so it also works with architectures that don't have CAS instructions, like ARMv6-M and MSP430.
A few HAL APIs could be simplified with this API: for example the clocks
argument could be removed from stm32f1xx::MonoTimer
constructor.
The core_frequency
crate plus a few more atomics (for the prescalers) could replace the stm32f1xx::Clocks
struct. This change would reduce the number of arguments of many constructors by one.
// APB1 pre-scaler
static PPRE1: AtomicU8 = AtomicU8::new(1);
impl Serial1 {
pub fn new(
usart1: USART1,
// clocks: Clocks, // no longer required
// ..
) {
// APB1 clock frequency
let apb1 = core_clock::get() / u32::from(PPRE1.load(Ordering::Acquire));
// ..
}
}
RTFM could use this to provide a variation of the schedule
API that works with human time (the current API works with core clock cycles).
Suggestion: do not make the cortex-m
crate depend on core-frequency
. That would make users of cortex-m
run into weird linker errors ("undefined reference to CORE_FREQUENCY
") if they are not using a HAL crate that integrates with core-frequency
. Also it would make the versioning of the cortex-m
crate harder: bumping the minor of version core-frequency
would require bumping the minor version of the cortex-m
crate.
Suggestion: do not make embedded-hal
depend on core-frequency
. embedded-hal
is a pure crate that mainly contains interfaces; making it depend on core-frequency
would add "impurity" to it making it depend on a symbol defined elsewhere. It also would make versioning harder.