-
Notifications
You must be signed in to change notification settings - Fork 99
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
base: master
Are you sure you want to change the base?
Changes from 1 commit
1cae262
f8ee374
fb1e268
8359971
29ddfd8
304ebba
ec2c659
84d29a4
cdcae5e
e4bdb37
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
- 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). | ||
|
||
# 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would this interface work well with shared access? e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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`: Just as this crate makes no impositions on specific implementation, but rather, encapsulates interfaces that | ||
represent the most fundamental of behavior. | ||
- `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: | ||
```rust | ||
// I would rename this to `TickCount`. 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
// 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If A There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would be up to the implementor. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that's reasonable! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you mean " 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:
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:
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) | ||
} | ||
} | ||
``` | ||
|
||
Up to this point, I've taken the concept of `Clock` and generalised it to `Counter`. I beleive that the `Clock` trait fits | ||
in by being a trait extension of a `Counter` trait. Such an extension would further constrain the type that is being measured | ||
with an aditional requirement that it is a representation of time (e.g. MilliSecs<...>), and perhaps add methods that are | ||
clock-specific. | ||
|
||
```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) | ||
} | ||
} | ||
``` | ||
|
||
# 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As a note: embassy-time is fully usable in non-async contexts. There is no requirement to use async at all. There are certainly other drawbacks you could mention (requirements to use u64, separate externally defined driver methods, mandated timer wheel/queue implementation, etc.), but these are fully separate from "async or not". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I should add that embassy time is not intended to be a universally applicable encapsulation of a counter, or something along those lines. |
||
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. | ||
|
||
|
||
# Unresolved questions | ||
[unresolved]: #unresolved-questions | ||
|
||
How to move forward? I propose an `embedded-count` added to the rust-embedded repository, initially just a placeholder, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned, it is generally required to first get a working system, usually with at least 2-3 platform implementations, EXTERNALLY first, so that they can be evaluated in working operation, prior to merging anything or creating new repos in the rust-embedded space. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Understood, but I'm concerned about the chicken-egg paradox. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, however the HAL team is just as concerned about having/owning a crate that it isn't able to consider the design of. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see that there is much context I do not account for in my original intent. I'll educate myself on the context/standards/expectations and update my declared thoughts on moving forward. |
||
I'll fork it, and begin initial work. At an appropriate time, it will be upstreamed, and v0.0.2 will be released. If all goes | ||
well, its form will be representative of this RFC. | ||
|
||
Are we comfortable with using `typenum` instead of const generics. This will remove the limitations of const generics | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the underlying counter ticks are truly decoupled from the concept of time, it would be good to discuss how this would work in practice. Many platforms don't have hardware floating point support, and still many platforms (Cortex-M0, M0+, likely others) don't necessarily have hardware division support. Will it be reasonable for these platforms to perform the tick->time conversions? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would be up to each platform implementation. Fugit does everything using integers and in cost time with minor exceptions, and I wouldnt be surprised if platforms gravitated towards fugit primitives when counting for time. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not saying this is a deal breaker - but something to actively address and engage with. If the base timer is 1MHz, it'll require integer division to render milliseconds or seconds. It would be good to discuss, even if the answer is "fugit does it like this, it is this cheap, reasonably priced for the features provided". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One way of interpreting this comment, is that it's a proposal to include a canonical expression of time when writing portable embedded. Either as part of this RFC, or in a seperate, but closely related one. Here are my thoughts so far, from most, to least portable:
For me, the easiest answer to what is most portable is "whatever type you choose", but that's not much use, I think this is a void that needs filling. My take is that at least the following properties are needed in order to be considered maximumly embedded-portable:
I feel the closest is
I would be 100% behind setting up some sort of e-hal encapsulation of measuring time, and other things that counters... count, but there's some work involved to get there, imho. |
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be good to fully demonstrate what the typenum-based interface (or any alternatives) would look like for users. See my top level notes wrt providing examples for discussion. |
||
|
||
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. | ||
|
||
|
There was a problem hiding this comment.
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:
u64
is not a mandatory representation of the counter valueu32
andu64
based counter valuesI'd also really really like to see:
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.