Skip to content

[Idea] Device agnostic access to the core clock frequency #129

Open
@japaric

Description

@japaric

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions