rust-embedded / embedded-hal

A Hardware Abstraction Layer (HAL) for embedded systems
Apache License 2.0
2.03k stars 205 forks source link

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

Open PTaylor-us opened 4 years ago

PTaylor-us commented 4 years ago

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.

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 commented 4 years ago

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

PTaylor-us commented 4 years ago

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.

PTaylor-FluenTech/embedded-time#15

PTaylor-us commented 4 years ago

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

burrbull commented 4 years ago

Thank you for your work very much.

therealprof commented 4 years ago

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

PTaylor-us commented 4 years ago

@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 commented 4 years ago

@therealprof Added with v0.5.2

PTaylor-us commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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:

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 commented 4 years ago

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

Added

Timers

Changed

PTaylor-us commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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

PTaylor-us commented 4 years ago

Some new releases of embedded-time

0.9.1 - 2020-08-07

Changed

0.9.0 - 2020-08-05

Added

Changed

Removed

sourcebox commented 3 years ago

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:

jeancf commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 6 months ago

@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 commented 6 months ago

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 commented 6 months ago

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 commented 6 months ago

I've put together an RFC in the wg repo: https://github.com/rust-embedded/wg/pull/762

I believe I've addressed most of the points: