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

Solution for dealing with clocks and time in embedded systems #211

Open
PTaylor-us opened this issue May 22, 2020 · 35 comments
Open

Solution for dealing with clocks and time in embedded systems #211

PTaylor-us opened this issue May 22, 2020 · 35 comments

Comments

@PTaylor-us
Copy link

PTaylor-us commented May 22, 2020

I have just released embedded-time to handle clocks, instants, durations more easily in embedded contexts. It follows a similar idea to the C++ std::chrono library. I'd love to get some feedback, suggestions, PRs, etc.

embedded-time provides a comprehensive library for implementing abstractions over hardware and work with clocks, instants, durations, periods, and frequencies in a more intuitive way.

  • Clock trait allowing abstraction of hardware timers for timekeeping.
  • Work with time using milliseconds, seconds, hertz, etc. rather than cycles or ticks.
  • Includes example for the nRF52_DK board
  • Conversion to/from core::time::Duration

Example Usage:

struct SomeClock;

impl Clock for SomeClock {
    type Rep = i64;

    fn now() -> Instant<Self> {
        // read the count of the clock
        // ...
        Instant::new(count as Self::Rep)
    }
}

impl Period for SomeClock {
    // this clock is counting at 16 MHz
    const PERIOD: Period = Period::raw(1, 16_000_000);
}

// read from a Clock
let instant1 = SomeClock::now();

// ... some time passes

let instant2 = SomeClock::now();
assert!(instant1 < instant2);    // instant1 is *before* instant2

// duration is the difference between the instances
let duration: Option<Microseconds<i64>> = instant2.duration_since(&instant1);    

// add some duration to an instant
let future_instant = instant2 + Milliseconds(23);
// or
let future_instant = instant2 + 23.milliseconds();

assert(future_instant > instant2);

Related:
#122 #207 #24 #46 #201 #129 #103 #59 #186

@burrbull
Copy link
Member

I see only duration types. What about frequencies and baudrates?

@PTaylor-us
Copy link
Author

PTaylor-us commented Jun 11, 2020

I see only duration types. What about frequencies and baudrates?

@burrbull There is a Period type that is an i32 rational::Ratio. I use it in the durations to specify the "magnitude" of the value. I'm assuming these types would be whole numbers, so no need for a Ratio. If you can help me with some use-cases, I could certainly add those types.

FluenTech/embedded-time#15

@PTaylor-us
Copy link
Author

@burrbull Initial frequency-type functionality has been added in v0.5.0

@burrbull
Copy link
Member

Thank you for your work very much.

@therealprof
Copy link
Contributor

Looks excellent. One thing that could be useful is a bridge to core::time, which is only Duration at the moment.

@PTaylor-us
Copy link
Author

@therealprof While I don't think that core::time is particularly usable in embedded systems, I do recognize the benefit of having that bridge when it's needed. I'll add it to my list, and thank you very much for the suggestion.

@PTaylor-us
Copy link
Author

@therealprof Added with v0.5.2

@PTaylor-us
Copy link
Author

I'm curious whether it would be appropriate to using embedded-time in the embedded-hal crate or if that would make it too non-general. If it wouldn't be appropriate, I would love any feedback in ways I can make embedded-time usage with embedded-hal more seamless.

@therealprof
Copy link
Contributor

Looks good to me in general, appreciate the effort. I'll need to find some time to check it out in detail though.

@Ben-Lichtman
Copy link

Ben-Lichtman commented Jun 28, 2020

This looks really great - are there any plans to slowly integrate this into embedded-hal through PRs? I don't think this will see widespread adoption otherwise since most of the downstream embedded crates are already using their own time primitives etc. It would be nice to see your crate added as an embedded-hal dependency and then exposed through the embedded-hal public API at very least. Though I suspect people would be more comfortable just cloning the functionality to embedded-hal to keep the control within the embedded org...

@therealprof
Copy link
Contributor

embedded-time is trying to solve a different problem than embedded-hal so I don't think integration here makes too much sense. What we can do is encourage HAL impls to use embedded-time rather than rolling their own types and this is probably best done by someone doing PRs to do the switcheroo. I happen to be the maintainer of a few HAL impls so if anyone is looking for crates which could serve as the first stepping stone to general adoption feel free to use do PRs against https://github.com/stm32-rs/stm32f0xx-hal or https://github.com/stm32-rs/stm32f4xx-hal and I will help driving that to completion.

@PTaylor-us
Copy link
Author

@therealprof Thanks for clearing up the embedded-hal question, I wasn't at all sure whether it would be a good fit there. I will do whatever I can do to support PRs for individual HAL impls including opening them up myself unless I have updates needed in the embedded-time crate itself.

@PTaylor-us
Copy link
Author

After some feedback from a HAL implementor, I created the this PR (FluenTech/embedded-time#22) which would change all the "inner" types to unsigned. I would love to get some feedback about this change before I merge it.

@PTaylor-us
Copy link
Author

PTaylor-us commented Jun 28, 2020

@therealprof I am a little confused, actually, about what you said about embedded-hal. I'm still trying to wrap my head around what the objective of embedded-hal is. Device-specific HALs impl embedded-hal so that other code (primarily device drivers) can be written abstracted from the actual device being used. The goal (in my opinion) would be to provide complete, generic, access to the high-level functionality. Obviously, there will be very device-specific details that would be impractical to include. I think that for device drivers to work with time-related features (clocks, timers, durations, instants, frequency, etc) in a generic way, embedded-hal would be the place it needs to reside. Perhaps, I misunderstand the purpose of embedded-hal?

@therealprof
Copy link
Contributor

The purpose of embedded-hal is to facilitate the interfacing between hardware and drivers/applications. As you've said yourself embedded-time is mostly hardware independent.

I think that for device drivers to work with time-related features (clocks, timers, durations, instants, frequency, etc) in a generic way, embedded-hal would be the place it needs to reside.

I don't see it why it needs to be an integral part of embedded-hal instead of a dependency. But requirements and expectations can change...

@PTaylor-us
Copy link
Author

PTaylor-us commented Jun 28, 2020

I don't see it why it needs to be an integral part of embedded-hal instead of a dependency. But requirements and expectations can change...

Sorry, I wasn't commenting on where the code should be, but rather saying that (whether as a dependency or direct integration), it seems to me that embedded-hal is where it should be "used".

The purpose of embedded-hal is to facilitate the interfacing between hardware and drivers/applications. As you've said yourself embedded-time is mostly hardware independent.

Would it make sense to split it up? The only hardware-abstraction part of it is the Clock trait. However, it does depend on the TimeInt and Duration traits as well as Instant and Period types -- so basically everything. Being relatively new to Rust, I can't visualize what ideal outcome would look like.

I guess as long as embedded-hal exposes the interfaces necessary to impl the Clock trait, drivers could still utilize embedded-time. However, I'm fairly certain that it currently does not.

@therealprof
Copy link
Contributor

Would it make sense to split it up? The only hardware-abstraction part of it is the Clock trait. However, it does depend on the TimeInt and Duration traits as well as Instant and Period types -- so basically everything.

I'd wait and see where this goes. I can totally see this being useful for a lot of applications, if it gets picked up by other crates and turns out to be useful I'd have no regrets integrating it more deeply.

At the moment I see most of the usefulness in the HAL impls themselves with a hint of use in the timer related stuff in e-h.

I guess as long as embedded-hal exposes the interfaces necessary to impl the Clock trait, drivers could still utilize embedded-time. However, I'm fairly certain that it currently does not.

We could also do it the other way around and provide all the fun clock, timer and countdown traits via embedded-time and phase it out from embedded-hal. They're only borderline useful as-is and could be much more powerful with an proper time calculations, basically we're only using simple units (plus some copy and paste simple conversion in the HAL impls).

Also it would be great if we could have something like a monotonic system clock, real time clock, alarms, scheduling...

@PTaylor-us
Copy link
Author

Also it would be great if we could have something like a monotonic system clock, real time clock, alarms, scheduling...

As things sit right now, the Clock trait can be implemented for any monotonic source. That could be a peripheral timer, RTC, etc. I have an implementation for the nrf52 with two of the 32-bit peripheral timers chained together and that is my "system clock". I've also done an example using the RTC.

The next release will probably be with Timers added and I'm hoping to be able to use those to schedule tasks executed by the Clock impl's interrupt.

@therealprof I'm certain you have spent much more time thinking about where embedded rust is right now, where we want to go, and how to get there, so I would greatly appreciate any guidance.

@Ben-Lichtman
Copy link

Ben-Lichtman commented Jul 3, 2020

I dont think embedded time is trying to solve a different problem at all - Just look at all the issues noted in the original post mentioning the need for some crucial clock traits like periods and frequencies, and to tie them to the std lib. Could the traits and Period struct at least be moved into embedded-hal to ensure comparability?

I don't think it's wise to expect the ecosystem to depend on a 3rd party crate to ensure cross-ecosystem compatibility for timings - that should be something that embedded-hal does since it is the "blessed" crate for the normal hardware abstractions one would expect no?

@ryankurte
Copy link
Contributor

ryankurte commented Jul 3, 2020

Hey thanks for working on this! There's lots of useful stuff here, and how / what we integrate is an interesting question.

I think i'd echo @therealprof in that for now I would continue this as a separate project, and to try out integrating it with HALs and other components. This gives us space to experiment a bit (as we require for new hal additions) and means we're not tied to the HAL for releases (you may have noticed there's a lot going on at the moment).

To me it seems that the possible steps from there are:
a) we could review and bless embedded-time, and take over ownership (and maintenance) of the repo / crate (so it's separate but, first party).
b) we could look to integrate the validated traits with the hal directly
c) we could do a bit of both and move the types directly applicable to hardware in but keep the less-general ones separate.

I think the key component delineation to me is whether this is intended to be std compatible and how they can be implemented:

  • if it were to provide an abstraction over std or no_std then it would be better in the separate library (ie. using features and type aliasing, Instant::now() if we elected to provide this)
  • if it is directly implementable on hardware (or used in this, for example Timer::start(), Timer::now()) it should be in the hal
  • and if it's implementable using hardware components, but does not reflect physical hardware (for example, Timer::instant() might use but not reflect a hardware primitive), it's a bit ambiguous but i would lean towards having it in the library

However I can also see the benefits to going in either direction.

In terms of moving traits into the hal, it is reasonably straightforward to add a trait and then import and re-export that from the original crate, so we can move traits into the hal later should this be desired, without breaking any external dependencies.

@PTaylor-us
Copy link
Author

I just release v0.6.0 of embedded-time. With feedback and advice from @eldruin and @TheZoq2. I made a number of changes.

Added

  • Fallible Clock implementations

Timers

  • Software timers spawned from a Clock impl object.
  • One-shot or periodic/continuous
  • Blocking delay
  • Poll for expiration
  • Read elapsed/remaining duration

Changed

  • Switched to unsigned inner integer types
  • Changed the Clock trait to allow stateful implementations (added &self) to methods

@PTaylor-us
Copy link
Author

I think i'd echo @therealprof in that for now I would continue this as a separate project, and to try out integrating it with HALs and other components. This gives us space to experiment a bit (as we require for new hal additions) and means we're not tied to the HAL for releases (you may have noticed there's a lot going on at the moment).

I like that idea. I think embedded-hal (as well as embedded-time) are both still pretty fluid. It's my intention to continue development in a way that serves my purposes, but also keeps an eye toward maybe being suitable for incorporation at some later date.

I think the key component delineation to me is whether this is intended to be std compatible and how they can be implemented:

  • if it were to provide an abstraction over std or no_std then it would be better in the separate library (ie. using features and type aliasing, Instant::now() if we elected to provide this)
  • if it is directly implementable on hardware (or used in this, for example Timer::start(), Timer::now()) it should be in the hal
  • and if it's implementable using hardware components, but does not reflect physical hardware (for example, Timer::instant() might use but not reflect a hardware primitive), it's a bit ambiguous but i would lean towards having it in the library

There is only one part that must implemented in a hardware-specific manner and that's the Clock trait. Everything else is hardware-agnostic.

Here's a rough snapshot of some of the clock and instant interfaces:

struct ClockImpl {
    <hardware-specific state goes here>
}
impl embedded_time::Clock for ClockImpl {
    fn now(&self) -> Instant {
        <reading of hardware goes here>
    }
}

let my_clock = ClockImpl
let instant_1 = my_clock.now()
let instant_2 = instant_1 + 10.seconds()
let some_timer = my_clock.new_timer(2.seconds()).into_oneshot().start()
...
let remaining = some_timer.remaining()
let elapsed = some_timer.elapsed()
some_timer.wait()    // blocks until expiration

let elapsed_time = my_clock.duration_since(instant_1)
let dur_until_instant_2 = my_clock.duration_until(instant_2)

And here is an actual Clock implementation from the example:

pub struct SysClock {
    low: nrf52::pac::TIMER0,
    high: nrf52::pac::TIMER1,
    capture_task: nrf52::pac::EGU0,
}
impl SysClock {
    pub fn take(
        low: nrf52::pac::TIMER0,
        high: nrf52::pac::TIMER1,
        capture_task: nrf52::pac::EGU0,
    ) -> Self {
        Self {
            low,
            high,
            capture_task,
        }
    }
}
impl time::Clock for SysClock {
    type Rep = u64;
    const PERIOD: time::Period = <time::Period>::new(1, 16_000_000);
    type ImplError = Infallible;

    fn now(&self) -> Result<time::Instant<Self>, time::clock::Error<Self::ImplError>> {
        self.capture_task.tasks_trigger[0].write(|write| unsafe { write.bits(1) });

        let ticks =
            self.low.cc[0].read().bits() as u64 | ((self.high.cc[0].read().bits() as u64) << 32);

        Ok(time::Instant::new(ticks as Self::Rep))
    }
}

@ryankurte
Copy link
Contributor

time::Clock looks super useful hey, are there any soundness issues with reading from the two timers sequentially? (ie. what happens if high rolls over while you're reading the low section)?

@PTaylor-us
Copy link
Author

PTaylor-us commented Jul 16, 2020

@ryankurte, Thanks for taking a look at the crate. There are a lot of changes happening at the moment.

With this chip, I can trigger a signal from software that causes both timers to capture atomically (capture_task), then I just read the captured values out of each.

@ryankurte
Copy link
Contributor

ahh nice, it looks like a similar approach is possible on ST cores but, might require checking for overflow.

@PTaylor-us
Copy link
Author

Some new releases of embedded-time

0.9.1 - 2020-08-07

Changed

  • Re-export Fraction type in the duration and rate module
    • Allows for a single import (eg use duration::*) to support Generic usage which uses Fraction
  • Unify readme's and crate-level documentation
    • The GitHub README.md and crates.io crates-io.md are now generated using cargo-readme

0.9.0 - 2020-08-05

Added

  • From implementations for infallible intra-rate and intra-duration conversions
  • Implementations of common traits to types
  • More frequency types: mebihertz (MiHz) and kibihertz(KiHz)
  • More test coverage

Changed

  • Fallible intra-rate and intra-duration conversions use try_from()/try_into() rather than the try_convert_ methods
  • Replaced try_into_generic() with to_generic()
  • Replaced try_from_(duration/rate) with to_duration() and to_rate()
  • Return Options from checked_ methods rather than Results
  • Remove impl-specific Clock error type (ImplError)

Removed

  • trait module
    • duration- and/or rate-specific functionality can be imported separately by using duration::* and rate::*

@sourcebox
Copy link
Contributor

As a developer who is quite new to Rust but has a decent amount of experience with C/C++ and STM32 development, my thoughts on this topic when developing a device driver:

  • I want as less dependencies as possible, ideally embedded-hal only. Dependency hell is something that fears a lot of embedded developers since they are used to have control over the whole code except from a few vendor libraries.
  • The core::time::Duration type seems strange in the embedded world because we are mostly dealing with microseconds and milliseconds and u32 for nanoseconds overflow too quickly.
  • I don't need any "fancy stuff" but a unified way to get a monotonic instant that does not overflow too quickly, e.g. a simple u64 representing nanoseconds. Ticks as a generic time unit would also be fine unless there are some clear rules how to convert them.

@jeancf
Copy link

jeancf commented Aug 17, 2022

I just started with embedded development in rust and I want to say that I am in line with @sourcebox's points above.

I am trying to write a simple embedded-hal driver on the ESP32C3. I need to measure how long a GPIO pin stays high. For that I do not need a sophisticated timer: access to a monotonic timer (tick count + frequency) is sufficient. It would be especially interesting on the ESP32C3 where there are only 2 general purpose timers. With access to the system clock I would not need to consume a timer.

For what it's worth, I think basic time access functionalities should be intergrated in embedded-hal. Having them in a separate crate just makes life more difficult for the implementer of hardware-specific HAL that would have to track and implement traits from multiple crates. And if they don't, the ecosystem will fragment itself with partial HAL implementations that do not deliver the expected hardware abstraction.

I see that this discussion started years ago. Is there still life in it? How does it move forward?

@sourcebox
Copy link
Contributor

I never wanted to write my own crate for dealing with time, but to have a solution for my own projects until something "official" is released:

https://github.com/sourcebox/emtick-rs

I don't know if the general concept behind it is good enough as solution within e-h, maybe it could be discussed.
And yes: the code for the conversions between ticks/ms/µs looks scary and expensive, but at least in my own testing, the compiler did a decent job of optimizing it.

@romancardenas
Copy link

Hi, @sourcebox , what are the main differences between your implementation and fugit? I saw that yours does not have any dependency, and that is always desirable (I believe). However, it would be great to have a performance comparative. Also, memory footprint should be studied.

I agree with you, we just need a decent toolbox for dealing with time and timers in embedded systems. It would be great if e-h adopts/integrates either fugit or your new crate. The main drawback of your solution (I think) is that several HALs already use fugit, and maybe it is worth it to adopt it in e-h as well. I'll take a look to your code later with more time.

@sourcebox
Copy link
Contributor

As far as I understand fugit, it tries to eleminate runtime cost completely. This however leads to the fact that you have to deal with types like Duration::<u32, 1, 1_000> and pass them around, which I'm personally not a big fan of.

My solution is a more pragmatic one. It should be easier to use but has some runtime overhead when converting from/to natural time units. So doing this should be kept minimal. The amount of impact on overall performance is hard to decide. In my code, time calculations are typically used rarely compared to the overall coverage. Optimizing this to the max would not have too much effect globally. But this may vary depending on the use case.

I would suggest that you do a real world example using fugit, my crate, maybe embedded-time etc. and do some comparision in terms of ease-of-use and performance.

@Ben-PH
Copy link

Ben-PH commented May 21, 2024

@PTaylor-us I've been using the Clock trait, and I am very grateful for the existance of embedded-time. Thank you for your work.

The project seems stale: I haven't seen any updates, or activity in the issues/PR space. Would you be happy to see this taken over by someone and/or absorbed into the embedded-hal crate?

@Ben-PH
Copy link

Ben-PH commented May 21, 2024

For what it's worth, I think basic time access functionalities should be intergrated in embedded-hal

@sourcebox 100%. I would add that the more advanced functionalities can be incrementally added as the community asks for them. Starting with a trait with an API that sources a tick-count and mapping to seconds (I say seconds because it's the standard unit of time, but ideally, this would be generic between seconds, us, ms, etc). The more advanced stuff can be added down the line.

@eldruin
Copy link
Member

eldruin commented May 22, 2024

Quoting @Dirbaio in the chat for a comparison of embedded-time vs. fugit:

there's definitely interest indeed, the problem is it's very unclear how to do it. Decisions that must be done is:

  • Bit width. Hardcoded, configurable via Cargo features, configurable via generics?
  • Tick rate: Hardcoded, configurable via Cargo features, configurable via generics? Const generics? typenum? Just a frequency value? or a full NUM/DENOM fraction?
  • Who gets to choose these settings? the HAL implementing the trait, or the driver using the trait?
  • Single global clock, or multiple clocks? If multiple clocks, can they have different settings?

Example 1: embassy-time:

  • bit width: hardcoded to u64, so it never overflows. (prioritizing convenience over efficiency)
  • tick rate: configurable via cargo features. Some HALs choose it, some let the end user choose.
  • Single global clock, so you can do Instant::now() from anywhere without having to pass around stuff.

Example 2: fugit

  • bit width: configurable with generics
  • tick rate: full NUM/DENOM fraction, configurable with const generics.
  • Who chooses? the HAL implementation (I think the HAL can choose to be generic so the end user chooses? but either way the driver can't choose)
  • Multiple clocks, each can have its own settings.

as you can see they're polar opposites.
So, adding some Clock/Instant/Duration traits to embedded-hal means we have to make SOME choice on these questions and if we choose X then use cases that would be better suited by Y would suffer

@Ben-PH
Copy link

Ben-PH commented May 22, 2024

I've put together an RFC in the wg repo: rust-embedded/wg#762

I believe I've addressed most of the points:

  • Bit width. That is to be determined by the implementor. I am of the opinion that the value being read should be entirely up to the implementor, so long as it encapsulates a type that is suitable for step-counting. This could be u8, Wrapper(u8), u128, SomeTypeThatCounts, and so on
  • Tick rate: again, implementor dependant. embedded-hal makes no assumptions about what an InputPin actually is. for all it matters, it could just be a struct that just randomly chooses high or low. What matters is that a) the trait encapsulates the act of reading something that is asumed to be increasing one at a time and b) a specification of what is being counted, be it a measurement of time, distance moved (e.g. with ab-encoders), etc
  • Who gets to choose settings? Same as with the I2c trait. The implementor.
  • Single or global clock? Again, implementor.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants