Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Introducing a common interface for counters. #762

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
284 changes: 284 additions & 0 deletions text/0000-common-counter-interface.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate you writing this up! In particular, I would very much like to see the following items, which I don't see in the current draft:

  • discussion of potential wraparound conditions, particularly if u64 is not a mandatory representation of the counter value
  • discussion of how you expect platforms with limited range timers (e.g. 16-bit only timers) to participate for u32 and u64 based counter values
  • If necessary (particularly for increasing smaller timers to larger ranges), any external resources, such as interrupts, that may be necessary for implementation
  • How you expect sharing of these clock/counter resources to work, for example if multiple drivers need to use them to provide timing capabilities
  • Any additional API surface, such as how "wait for timer to complete" would look

I'd also really really like to see:

  • The concrete traits you are proposing, for example in a a published crate (on GitHub or crates-io)
  • At least one example of implementing these traits, for your hardware platform or HAL of choice, likely in a fork of an existing HAL
  • At least one example of a downstream driver crate, that uses this trait
  • At least one example of an application, that links the HAL-provided trait impl and the trait-consuming driver, e.g. an "end to end example"

These examples are vitally necessary for evaluation - both for you to determine if the proposed interface(s) are reasonable in practice, as well as for others to comprehend the full extent of what you are proposing.

I'm not on the HAL team, but I imagine all the items I have listed here (and in comments) will be strong points of discussion, so for your RFC to be accepted it should likely address or dismiss these concerns.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your first group of points illustrate the need to ensure that everything that is implementation detail be left to the implementor. If a hal author of a specific platform or usecase is in any way hampered in their ability for their implementation to be encapsulated appropriately, then that is a failure of the trait definitions. For example, platforms with 16 bit counters could track wrappings themselves, or they could just say "out counter wraps" or whatever they choose to do.

The proposal in this RFC is to standardise an interface, with the only restriction on implementation detail being at the type constraint level.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, however, it is necessary to at least demonstrate that your proposed interface is practically usable before agreeing on that - and the best way we have today is by providing at least one (ideally a few diverse) implementations.

We've had many designs in embedded-hal that SEEMED like good ideas, or were good in isolation, but did not interact well beyond basic "hello world" interactions. This is particularly true in SPI and I2C, where it became clear between 0.2 and 1.0 that it was necessary to address sharing of a bus, as many practical systems had more than one device on the bus.

Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
- Feature Name: Common Interface for reading Clocks and other counters
- Start Date: 2024-05-22
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)

# Summary
[summary]: #summary

The embedded-hal traits `InputPin`, `OutputPin`, `I2c`, etc. key elements on the journey towards rust becoming
a first-class-citizen in the embedded devolopment space. Of notable absence is such a trait for reading an incremental counter,
and coupling that read to a unit being counted. This RFC calls for the development of such a trait.

**Example 1: Clock**
The trait would be implemented such that it reads from a clocks tick-count register, and assosciated type(s)/const(s) would allow
this to be expressed as a measure of time. This implementation can be used wherever one needs a clock to get a measure of time, and
`embedded_hal::counters::Clock` trait is in scope.

**Example 2: Usage Tracking**

A embedded dev wishes to track total movement of a machine. The couple an AB encoder to a ++ only pulse-count peripheral.
They implement counter such that a `read` will read from the pulse-count peripheral. They define their own trait extension internally
named `LifetimeMoveCount: embedded_hal::count::Counter`, and constrain it such that Counter must specify a distance for each count.
Their `LifetimeMoveCount` defers to `Counter` to get a read of movement, but its own impls defines long-term caching of data (e.g.
non volatile storage, or send reports to a server, etc).

**Core Principal**

> the goal of embedded-hal is to make it possible to write drivers that are portable across HALs.



# Motivation
[motivation]: #motivation

`esp-hal` has one implementation of representing time. `embassy-time` is another. If you look through any platform specific hal,
you are sure to find two things:

- Comprehensive implementation of the embedded-hal common interface.
- Their own implementation of getting time.
- Where applicable, their own interface for reading and interpreting counters, such as pulse counters.

One can put together a library that is platform agnostic by means of using common interfaces. Much like the way
`where LedP: SetDutyCycle` clause in a method makes it usable across the board, such as with this e.g....:

```rust
fn set_brightness<LedP: SetDutyCycle>(pin: LedP, brightness_pcnt: u8) -> Encoder {
pin.set_duty_cycle_percent(brightness_pcnt)
}
```

...embedded developers ought to have access to a common counter interface

```rust
// rotate brightness between 50 and 100% every 15 seconds using a clock-specific extension of a counting interface.
fn time_rotated_brightness<LedP: SetDutyCycle, ClkSrc: ReadClock>(led_pin: LedP, clk: ClkSrc) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this interface work well with shared access? e.g. &ClkSrc?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why that should be difficult, and if it was, why it should not be overcome.

One thing to consider, is the question of how this should differ from, say, an InputPin implementor. conceptually, it's no different. instead of reading an inherently boolean state, it's a count, with the addition of a specification of what's being counted.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This typically becomes challenging if you need to have more complex inner state - e.g. expanding a 16-bit timer to larger ranges. Something to keep in mind if you need to change other parts of the design.

let age: MiliSecs<_> = clk.read_count().time_scaled();
let age: u16 = age.into();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting that this probably need to be fallible, for example if we are more than 66 seconds into the runtime.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed. This specific code is useless outside of the scope of illustrating a point. Key ideas is to communicate a potential idiom.

let modulod = age.integer() % 15_000;
let numer = // math to map and scale the modulod to a triange pattern between 7_500 and 15_000
led_pin.set_duty_cycle_fraction(numer, 15_000)
}
```

This example is quite contrived, but for every situation in which someone needs to read an oscilator count, there is no core
standard for this, the way there is for things like gpio pins, i2c, etc.

# Detailed design
[design]: #detailed-design

The core influence comes from two sources:

- `embedded-hal`: This RFC aims to make no impositions on specific implementation, but rather, make it easy to write highly portable embedded-rust
- `embedded-time`: An independant endeavour that does a great job of already encapsulating this core idea.

This is the code defining the `embedded_time::Clock` trait. I have changed the comments for the context of this RFC:

<details>
<summary> Expand to view annotated summary `embedded_time::Clock` trait </summary>

```rust
// I would rename this. A consistent oscilator is not necisarily a clock. It could be a PWM at 50% duty cycle,
// or an ab encoder tracking total distance moved, etc. The only assumption is that it is an incremental counter, and that
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this address platforms where only down-counting timers exist? Are implementors expected to handle this (e.g. u32::MAX - raw_value?)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point no. A good design would be mindful of behavior where countdown is a posibility, but for now, I'm thinking in torms of strictly incrementing counts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha - something to keep in mind. I have go figure out what platform it is, but IIRC a fairly typical platform only has down-counting timers available.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the assosciated type I'm going with at this point (might drop the Copy):

    /// Typically u32 or u64 (or held within a type-wrapper) that corresponds with the
    /// data type held within the peripheral register being read.
    type RawData: Copy;

I've been ruminating on this question a bit, and I can't find any technical, development, portability, etc. reason not to have this on the forefront. Right now, the only thing stopping me, is finding a chance to do a bit of a case-study of making a hal/driver/etc portable through this trait, and needs this behavior yo describe.

I anticipate it being (relatively) trivial. My guess is a hal impl would implement try_read_raw, and do the mapping you describe. They would set the raw-data type to be u32, do the u32::max - $REGISTER_READ (or similar), then provide their own mappings to a data type that encapsulates the thing they are measuring.

// each increment has a specific meaning (be it a meanure of time, distance, etc.).
pub trait Clock: Sized {

// This `T` assosciated type is constrained to u32 or u64. I agree with constraining to an unsigned integer, but I am of the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If u32 is used, is there an opinion of what to do in the case of wraparounds? For example, 1MHz based timers are somewhat common and desired for timing, but this will wrap around a u32 in approximately 71 minutes. Even a 32.768kHz time will wrap a u32 in 1.5 days.

A u64 can probably practically be considered infinite, even at 1GHz, it would take ~584 years to wrap a u64.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be up to the implementor.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"that would be up to the implementor" - but this affects downstream users (e.g. drivers consuming the trait). Can drivers rely that the number always goes up? If so, how is this behavior ensured if the implementor provides a 1MHz 32 bit timer that works for the first 71 minutes, but not afterwards?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nailing down this kind of three-way contract between the HAL implementor, the trait interface, and the trait-using driver is pretty much the whole challenge of stabilizing these APIs, there's only so much that can be left to the implementor, while still providing a useful contract to the consuming driver.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a draft of a concept where you can extend a time-counter:

/// There are many scenarios where a time-based tick-counter needs to handle the case
/// where the counter overflows. This trait is intended to provide a HAL with the means
/// of providing a `TimeCount` implementor with specialised interupt-based overflow
/// tracking. 
pub trait TimeWrap: TimeCount {
    /// A HAL implementation might register the provided method to handle an overflow
    /// interupt and increment a field that tracks overflow.
    ///
    /// META: I honestly don't know the best way to do this. This might prove to be
    ///       a key technical challenge
    fn register_wrap_interupt(self, ???);
}

Is it the good way? dunno. Keen to hear thoughts.

// opinion that it can be _any_ unsigned integer; It shall be the decision of the implementor to choose the type.
//
// In addition, maybe integers-only as too limiting, as exotic types like `Wrapper(u32)` should be available. The guiding
// principal here is "You are reading a thing that counts the number of occurances of an event." Unsigned integers make
// the most sense on what to reach for, but that should be a decision for the implementor.
type T: TimeInt + Hash;

// The original code runs its own fraction implementation. Strictly with respect to time, I feel using `fugit` is more
// prudent.
//
// More broadly, I feel this should be an encapsulation of an interval. An interval could be a measure of time, a distance,
// angle, and so on, depending on what is being counted.
//
// I feel this should remain a compile-time defined statement.
const SCALING_FACTOR: Fraction;

// With respect to assosciated types for errors, the question that defines the design choices made should be "What is best
// for creating a standardised interface?"

// I would rename this to `try_read`. I would also like to raise the idea of mirroring the standard `Read` pattern, where
// it's not a return value that's used, but rather a passid in `&mut`. At its core, this interface should encapsulate
// the reading of a count that increments, tightly coupled to an encapsulation of what is being counted. This implies:
// - it can read time: An implementation of this trait would define a duration-per-count
// - it can read absolute movement: each increment would correspond to a distance.
// - it is a _reader_: but is it different enough to `Read` implementors that it merits different patterns?
// - it reads _generic_ units: the notion of `now` and `time` are implementation dependant
fn try_now(&self) -> Result<Instant<Self>, Error>;

// Not exactly sure about how to read into this, or what should be done. Will update this part of RFC in response to
// questions, feedback, and any further insight I gather. My thinking is anything that respembles construction or
// initialisation is out, much the same as any other trait in embedded-hal.
fn new_timer<Dur: Duration>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this RFC propose a mechanism for multiplexing multiple timers, e.g. if new_timer is called multiple times? Is this Timer interface specified anywhere?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have to think about this, but with respect to constructors, I anticipate not including it, much the same as with all the other traits in embedded-hal

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's reasonable!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you mean "new_timer returns a new instance, but sometimes each instance just uses the same timer under the hood. What approach to allowing this is being proposed?", I see this as addressing the challenge of "shared state, not necisarily immutable", and using a multitude of multiplexed objects as one particular strategy.

I'm of the mind that the primary domain of portability sits within the the scope of how a thing is used. There could be a case for a "soft-standard" of construction, but the driving mandate should always be "portability in its use".

Some thoughts I've been reminating on:

  • Object exclusivity contradicts the premise of portability of count-readers. Time-based, or otherwise.
  • Shared mutable state is to be avoided, especially at the interface level.
  • Not being able to multiplex, or use other means to share a counter that's potentially mutable, is a deal-breaker.
  • Interior mutability patterns/idioms/lang-features/etc. at impl level should be able to resolve this contradiction nicely. Their use must not be limited in any way.
  • In particular, w.r.t. embedded, interior mutability must be available in the context of juggling interrupts, soft/hard-real-time use-cases, etc.

My gut is to use patterns of shared immutable state at the interface level, and ensure no restrictions are imposed to allow for sharing something that is imutable under the hood

To whit:

  • &self in the interface
  • Investigate the various multiplexing approaches used, and ensure that e-hal Just Works to make their solutions usable in a portable fashion.
  • Validate this in the context of embedded: interupts, real-time, etc.

If a particular implementation chooses to use multiple, unshared, mutable objects that multiplex a single thing under the hood, I don't see how that would be incompatible with the pattern I just proposed at first glance, but I expect to be proven wrong on that.

&self,
duration: Dur,
) -> Timer<param::OneShot, param::Armed, Self, Dur>
where
Dur: FixedPoint,
{
Timer::<param::None, param::None, Self, Dur>::new(&self, duration)
}
}
```

</details>

Below is an initial draft design.

<details>
<summary>Expand to view pseudo-rust illustrative concept. </summary>

```rust
/// Base encapsulation for reading a clock perihpereal. Intended typical use-case is
/// a HAL implementation adds this trait to a HAL type, and uses this interface to
/// read a clock/timer register. Through implementation of this trait, this object
/// will carry with it the means to read a clocks tick counter, and communicate the
/// total duration this represents.
pub trait TimeCount: Sized {
/// Typically u32 or u64 (or held within a type-wrapper) that corresponds with the
/// data type held within the peripheral register being read.
type RawData;

/// The length of time that each clock-tick measures. Setting up a clock with a 80MHz,
/// a HAL might opt to set this as `fugit::TimerDuration<Self::RawData, 1, 80_000_000>`.
///
/// Another option might something that expresses the measure as a frequency, in which
/// case, something that encapsulates "eighty million" (as opposed to "one eighty
/// millionth").
///
/// META: This might merit being a const instead.
/// META: Should it also be documented about the Mul constraint?
type TickMeasure: Duration + Mul<Self::RawData, Output = Self::TimeMeasure> + Default;

/// A combinasion of raw count and measure. 80k ticks with 80MHz => 1 second.
type TimeMeasure: Duration;


/// Intended as an interface to a raw-register read.
fn try_now_raw(&self) -> Result<Self::RawData, crate::clock::Error>;

/// Interprates the tick-count of `try_now_raw`, and scales based on `TickMeasure`
fn try_now(&self) -> Result<Self::TimeMeasure, crate::clock::Error> {
Self::TickMeasure::default() * self.try_now_raw()?
}
}
```

</details>

And here is an initial thought as to how to handle the need to address wrapping.

<details>
<summary>`/// There are many sc...</summary>

```rust
/// There are many scenarios where a time-based tick-counter needs to handle the case
/// where the counter overflows. This trait is intended to provide a HAL with the means
/// of providing a `TimeCount` implementor with specialised interupt-based overflow
/// tracking.
pub trait TimeWrap: TimeCount {
/// A HAL implementation might register the provided method to handle an overflow
/// interupt and increment a field that tracks overflow.
///
/// META: I honestly don't know the best way to do this. This might prove to be
/// a key technical challenge
fn register_wrap_interupt(self, ???);
}
```
</details>

This RFC initially proposed a base `Counter` trait, where a `Clock` trait would extend
it in a manner to handle time. This has changed. For background context, the initial
illustration of this concept is included here.

```rust
pub trait Clock: Counter
where <Self as Counter>::Output: Into<Seconds<...>>
{
fn read_time(&self) -> Seconds<...> {
let ticks = <Self as Counter>::read_ticks(self);
Seconds::from(ticks)
}
}
```

### Upstreamed feedback

James Munn shared a valuable summary of their thoughts following a lengthy discussion in DMs:

> you need to provide an interface that is reasonable for all parties, within reasonable use case [expectations] of all of them

<details>
<summary>It is expected that...</summary>

- ..a timer/counter will need to be shared - there are relatively few of them on many chips
- ..users will want high precision in some cases, 1k-1M are very reasonable number
- ..some timers have a limited range - 16/24/32 bit are common, but many chips only have 16/24
- ..programs will be expected for months to years at a time
- ..some users want to use the lowest possible power design they can
- ..not all chips have atomics, and may need to use critical sections
- ..users may have other interrupts, some of which may have higher priority than your timer/counter interrupt
- ..users may want to use a global timer inside of other interrupts

</details>


# How We Teach This
[how-we-teach-this]: #how-we-teach-this

Personally, I intend to make PRs into embedded-hal reverse dependencies, and update their encapsulation of time-tracking
to include implementations that are officially supported by the embedded working group. I also see the need to add examples
of both implementation, and use (such as the time-rotating e.g. I hypothesised above).

# Drawbacks
[drawbacks]: #drawbacks

I'm mostly basing my thoughts on the models that I see throughout the embedded-hal crate. The drawbacks assosciated with the
various modules that define a common interface for things like gpio pins, I2c, etc would ideally apply in the same manner here.

# Alternatives
[alternatives]: #alternatives

- `embassy-time` has an encapsulation of time, however that lives in an async-first context. It also forces design
choices onto reverse dependencies, such as forced into using u64 for their `Duration` and `Instant` primitives.
- many MCU hals have their own way of encapsulating time, and other incremental counts.
- fugit is an embedded-first encapsulation of mapping time-oscilator counts to a measurement of time in SI units.
- `fugit` covers a lot of other base-functionalities in terms of embedded-first time-encapsulation.


# Unresolved questions
[unresolved]: #unresolved-questions


How should we approach assosciated error types?

What is the wish-list of industry, such as the various teams writing the hal crates for specific MCUs/platforms?

What constraits make sense? I feel that `Ord` ought to be required for the raw data being read. In my mind, a type that can
be used to count something, would necissarily have an ordering between any two possible values. I also think some thought should
be put into providing the maths where compatable. in pseudo-rust: `impl<A: Counter, B: Counter> Add<A::Output> for for B::Output`
with where-clauses that constrain that the output types can do the Add.

### How to move forward?

Initially, it was thought to add `embedded-count` added to the rust-embedded repository, initially just a placeholder, and so on.

This has been updated. I have concerns about the chicken-egg dilema, and counsil is sought to mitigate this. At time of writing:

1. Create out-of-workinggroup repo and build up the interface(s)
2. Fork some hals and update to use the interface(s)
3. Fork existing projects, update to clocks portably.
4. Continue working on my `SimpleFOC-rs` project, writing clocks in a portable manner

### Typenum

Are we comfortable with using `typenum` instead of const generics. This will remove the limitations of const generics
entirely, including the need for nightly features, at the risk of interface ergonomics, and compromising user familiarity.
I wish to explore the use of this crate, though I feel it's a core requirement that the change in UX to be trivial. I.e. other
than theneed to use `typenum::U5` where `5u32/64/size/8` would be used to set a generic const.