blob: 468b9226f9666aed4cf240a22a38d40da4d77ea8 [file] [log] [blame]
.. _module-pw_chrono:
=========
pw_chrono
=========
Pigweed's chrono module provides facilities for applications to deal with time,
leveraging many pieces of STL's the ``std::chrono`` library but with a focus
on portability for constrained embedded devices and maintaining correctness.
.. note::
This module is still under construction, the API is not yet stable.
-------------------------------
``duration`` and ``time_point``
-------------------------------
Pigweed's time primitives rely on C++'s
`<chrono> <https://en.cppreference.com/w/cpp/header/chrono>`_ library to enable
users to express intents with strongly typed real time units through
`std::chrono::duration <https://en.cppreference.com/w/cpp/chrono/duration>`_
and
`std::chrono::time_point <https://en.cppreference.com/w/cpp/chrono/time_point>`_
.
What are they?
==============
At a high level, durations and time_points at run time are tick counts which
are wrapped in templated metadata which is only used at compile time.
The STL's
`std::chrono::duration <https://en.cppreference.com/w/cpp/chrono/duration>`_
class template represents a time interval. It consists of a count of ticks of
type ``rep`` and a tick ``period``, where the tick period is a ``std::ratio``
representing the time in seconds from one tick to the next.
The only data stored in a duration is a tick count of type ``rep``. The
``period`` is included as part of the duration's type, and is only used when
converting between different durations.
Similarly, the STL's
`std::chrono::time_point <https://en.cppreference.com/w/cpp/chrono/time_point>`_
class template represents a point in time (i.e. timestamp). It consists of a
value of type ``duration`` which represents the time interval from the start of
the ``clock``'s epoch.
The ``duration`` and ``time_point`` class templates can be represented with the
following simplified model, ignoring most of their member functions:
.. code-block:: cpp
namespace std::chrono {
template<class Rep, class Period = std::ratio<1, 1>>
class duration {
public:
using rep = Rep;
using period = Period;
constexpr rep count() const { return tick_count_; }
static constexpr duration zero() noexcept {
return duration(0);
}
// Other member functions...
private:
rep tick_count_;
};
template<class Clock, class Duration = typename Clock::duration>
class time_point {
public:
using duration = Duration;
using rep = Duration::rep;
using period = Duration::period;
using clock = Clock;
constexpr duration time_since_epoch() const { return time_since_epoch_; }
// Other member functions...
private:
duration time_since_epoch_;
};
} // namespace std::chrono
What ``rep`` type should be used?
=================================
The duration's ``rep``, or tick count type, can be a floating point or a signed
integer. For most applications, this is a signed integer just as how one may
represent the number of ticks for an RTOS API or the number of nanoseconds in
POSIX.
The ``rep`` should be able to represent the durations of time necessary for the
application. Pigweed recommends that the duration's ``rep`` used for a clock use
``int64_t`` in order to trivially avoid integer underflow and overflow risks by
covering a range of at least ±292 years. This matches the STL's requirements
for the duration helper types which are relevant for a clock's tick period:
* ``std::chrono::nanoseconds duration</*signed integer type of at least 64 bits*/, std::nano>``
* ``std::chrono::microseconds duration</*signed integer type of at least 55 bits*/, std::micro>``
* ``std::chrono::milliseconds duration</*signed integer type of at least 45 bits*/, std::milli>``
* ``std::chrono::seconds duration</*signed integer type of at least 35 bits*/>``
With this guidance one can avoid common pitfalls like ``uint32_t`` millisecond
tick rollover bugs when using RTOSes every 49.7 days.
.. warning::
We recommend avoiding the ``duration<>::min()`` and ``duration<>::max()``
helper member functions where possible as they exceed the ±292 years duration
limit assumption. There's an immediate risk of integer underflow or overflow
for any arithmetic operations. Consider using ``std::optional`` instead of
priming a variable with a value at the limit.
Helper duration types and literals
==================================
The STL's ``<chrono>`` library includes a set of helper types based on actual
time units, including the following (and more):
* ``std::chrono::nanoseconds``
* ``std::chrono::microseconds``
* ``std::chrono::milliseconds``
* ``std::chrono::seconds``
* ``std::chrono::minutes``
* ``std::chrono::hours``
As an example you can use these as follows:
.. code-block:: cpp
#include <chrono>
void Foo() {
Bar(std::chrono::milliseconds(42));
}
In addition, the inline namespace ``std::literals::chrono_literals`` includes:
* ``operator""ns`` for ``std::chrono::nanoseconds``
* ``operator""us`` for ``std::chrono::microseconds``
* ``operator""ms`` for ``std::chrono::milliseconds``
* ``operator""s`` for ``std::chrono::seconds``
* ``operator""min`` for ``std::chrono::minutes``
* ``operator""h`` for ``std::chrono::hours``
As an example you can use these as follows:
.. code-block:: cpp
using std::literals::chrono_literals::ms;
// Or if you want them all: using namespace std::chrono_literals;
void Foo() {
Bar(42ms);
}
For these helper duration types to be compatible with API's that take a
`SystemClock::duration` either an :ref:`implicit<Implicit lossless conversions>`
or :ref:`explicit lossy<Explicit lossy conversions>` conversion must be done.
Converting between time units and clock durations
=================================================
So why go through all of this trouble instead of just using ticks or instead
just using one time unit such as nanoseconds? For example, imagine that you have
a 1kHz RTOS tick period and you would like to express a timeout duration:
.. code-block:: cpp
// Instead of using ticks which are not portable between RTOS configurations,
// as the tick period may be different:
constexpr uint32_t kFooNotificationTimeoutTicks = 42;
bool TryGetNotificationFor(uint32_t ticks);
// And instead of using a time unit which is prone to accidental conversion
// errors as all variables must maintain the time units:
constexpr uint32_t kFooNotificationTimeoutMs = 42;
bool TryGetNotificationFor(uint32_t milliseconds);
// We can instead use a defined clock and its duration for the kernel and rely
// on implicit lossless conversions:
#include <chrono>
#include "pw_chrono/system_clock.h"
constexpr SystemClock::duration kFooNotificationTimeout =
std::chrono::milliseconds(42);
bool TryGetNotificationFor(SystemClock::duration timeout);
void MaybeProcessNotification() {
if (TryGetNotificationFor(kFooNotificationTimeout)) {
ProcessNotification();
}
}
.. _Implicit lossless conversions:
Implicit lossless conversions
-----------------------------
Wait, but how does this work? Is there a hidden cost? The ``duration`` type
comes with built in implicit lossless conversion support which is evaluated at
compile time where possible.
If you rely on implicit conversions then the worst case cost is multiplication,
there is no risk of a division operation.
If the implicit conversion cannot be guaranteed at compile time to be lossless
for all possible tick count values, then it will fail to compile.
As an example you can always convert from ``std::chrono::seconds`` to
``std::chrono::milliseconds`` in a lossless manner. However, you cannot
guarantee for all tick count values that ``std::chrono::milliseconds`` can be
losslessly converted to ``std::chrono::seconds``, even though it may work for
some values like ``0``, ``1000``, etc.
.. code-block:: cpp
#include <chrono>
constexpr std::chrono::milliseconds this_compiles =
std::chrono::seconds(42);
// This cannot compile, because for some duration values it is lossy even
// though this particular value can be in theory converted to whole seconds.
// constexpr std::chrono::seconds this_does_not_compile =
// std::chrono::milliseconds(1000);
.. _Explicit lossy conversions:
Explicit lossy conversions
--------------------------
Although we recommend sticking to implicit lossless conversions, what if for
some reason a lossy conversion is required? For example what if we're using a
128Hz RTOS tick clock?
The 128Hz ``period`` can be perfectly represented with a ``std::ratio<1,128>``.
However you will not be able to implicitly convert any real time unit durations
to this duration type. Instead explicit lossy conversions must be used. Pigweed
recommends explicitly using:
* `std::chrono::floor <https://en.cppreference.com/w/cpp/chrono/duration/floor>`_
to round down.
* `std::chrono::round <https://en.cppreference.com/w/cpp/chrono/duration/round>`_
to round to the nearest, rounding to even in halfway cases.
* `std::chrono::ceil <https://en.cppreference.com/w/cpp/chrono/duration/ceil>`_
to round up.
* `pw::chrono::SystemClock::for_at_least` to round up using the `SystemClock::period`,
as a more explicit form of std::chrono::ceil.
.. Note::
Pigweed does not recommend using ``std::chrono::duration_cast<>`` which
truncates dowards zero like ``static_cast``. This is typically not the desired
rounding behavior when dealing with time units. Instead, where possible we
recommend the more explicit, self-documenting ``std::chrono::floor``,
``std::chrono::round``, and ``std::chrono::ceil``.
Now knowing this, the previous example could be portably and correctly handled
as follows:
.. code-block:: cpp
#include <chrono>
#include "pw_chrono/system_clock.h"
// We want to round up to ensure we block for at least the specified duration,
// instead of rounding down. Imagine for example the extreme case where you
// may round down to zero or one, you would definitely want to at least block.
constexpr SystemClock::duration kFooNotificationTimeout =
std::chrono::ceil(std::chrono::milliseconds(42));
bool TryGetNotificationFor(SystemClock::duration timeout);
void MaybeProcessNotification() {
if (TryGetNotificationFor(kFooNotificationTimeout)) {
ProcessNotification();
}
}
This code is lossless if the clock period is 1kHz and it's correct using a
division which rounds up when the clock period is 128Hz.
.. Note::
When using ``pw::chrono::SystemClock::duration`` for timeouts we recommend
using its ``SystemClock::for_at_least()`` to round up timeouts in a more
explicit, self documenting manner which uses ``std::chrono::ceil`` internally.
Use of ``count()`` and ``time_since_epoch()``
=============================================
It's easy to escape the typesafe chrono types through the use of
``duration<>::count()`` and ``time_point<>::time_since_epoch()``, however this
increases the risk of accidentally introduce conversion and arithmetic errors.
For this reason, we recommend avoiding these two escape hatches until it's
absolutely necessary due to I/O such as RPCs or writing to non-volatile storage.
Discrete Timeouts
=================
We briefly want to mention a common pitfall when working with discrete
representations of time durations for timeouts (ticks and real time units)
on systems with a continously running clock which is backed by discrete time
intervals (i.e. whole integer constant tick periods).
Imagine an RTOS system where we have a constant tick interval. If we attempt to
sleep for 1 tick, how long will the kernel actually let us sleep?
In most kernels you will end up sleeping somewhere between 0 and 1 tick periods
inclusively, i.e. ``[0, 1]``, if we ignore scheduling latency and preemption.
**This means it can randomly be non-blocking vs blocking!**
This is because internally kernels use a decrementing timeout counter or a
deadline without taking the current current progression through the existing
tick period into account.
For this reason all of Pigweed's time bound APIs will internally add an extra
tick to timeout intents when needed to guarantee that we will block for at least
the specified timeout.
This same risk exists if a continuously running hardware timer is used for a
software timer service.
.. Note::
When calculating deadlines based on a timeout when using
``pw::chrono::SystemClock::timeout``, we recommend using its
``SystemClock::TimePointAfterAtLeast()`` which adds an extra tick for you
internally.
------
Clocks
------
We do not recomend using the clocks provided by ``<chrono>`` including but not
limited to the ``std::chrono::system_clock``, ``std::chrono::steady_clock``, and
``std::chrono::high_resolution_clock``. These clocks typically do not work on
embedded systems, as they are not backed by any actual clocks although they
often do compile. In addition, their APIs miss guarantees and parameters which
make them difficult and risky to use on embedded systems.
In addition, the STL time bound APIs heavily rely on templating to permit
different clocks and durations to be used. We believe this level of template
metaprogramming and the indirection that comes with that can be confusing. On
top of this, accidental use of the wrong clock and/or conversions between them
is a frequent source of bugs. For example using a real time clock which is not
monotonic for a timeout or deadline can wreak havoc when the clock is adjusted.
For this reason Pigweed's timeout and deadline APIs will not permit arbitrary
clock and duration selection. Outside of small templated helpers, all APIs will
require a specific clock's duration and/or time-point. For almost all of Pigweed
this means that the ``pw::chrono::SystemClock`` is used which is usually backed
by the kernel's clock.
PigweedClock Requirements
=========================
``pw_chrono`` extends the C++ named
`Clock <https://en.cppreference.com/w/cpp/named_req/Clock>`_ and
`TrivialClock <https://en.cppreference.com/w/cpp/named_req/TrivialClock>`_
requirements with the ``PigweedClock Requirements`` to make clocks more friendly
for embedded systems.
This permits the clock compatibility to be verified through ``static_assert`` at
compile time which the STL's requirements do not address. For example whether
the clock continues to tick while interrupts are masked or whether the clock is
monotonic even if the clock period may not be steady due to the use of low power
sleep modes.
For a type ``PWC`` to meet the ``PigweedClock Requirements``:
* The type PWC must meet C++14's
`Clock <https://en.cppreference.com/w/cpp/named_req/Clock>`_ and
`TrivialClock <https://en.cppreference.com/w/cpp/named_req/TrivialClock>`_
requirements.
* The ``PWC::rep`` must be ``int64_t`` to ensure that there cannot be any
overflow risk regardless of the ``PWC::period`` configuration.
This is done because we do not expect any clocks with periods coarser than
seconds which already require 35 bits.
* ``const bool PWC::is_monotonic`` must return true if and only if the clock
can never move backwards.
This effectively allows one to describe an unsteady but monotonic clock by
combining the C++14's Clock requirement's ``const bool PWC::is_steady``.
* ``const bool PWC::is_free_running`` must return true if and only if the clock
continues to move forward, without risk of overflow, regardless of whether
global interrupts are disabled or whether one is in a critical section or even
non maskable interrupt.
* ``const bool PWC::is_always_enabled`` must return true if the clock is always
enabled and available. If false, the clock must:
+ Ensure the ``const bool is_{steady,monotonic,free_running}`` attributes
are all valid while the clock is not enabled to ensure they properly meet
the previously stated requirements.
+ Meet C++14's
`BasicLockable <https://en.cppreference.com/w/cpp/named_req/BasicLockable>`_
requirements (i.e. provide ``void lock()`` & ``void unlock()``) in order
to provide ``std::scoped_lock`` support to enable a user to enable the
clock.
+ Provide ``const bool is_{steady,monotonic,free_running}_while_enabled``
attributes which meet the attributes only while the clock is enabled.
* ``const bool PWC::is_stopped_in_halting_debug_mode`` must return true if and
only if the clock halts, without further modification, during halting debug
mode , for example during a breakpoint while a hardware debugger is used.
* ``const Epoch PWC::epoch`` must return the epoch type of the clock, the
``Epoch`` enumeration is defined in ``pw_chrono/epoch.h``.
* The function ``time_point PWC::now() noexcept`` must always be thread and
interrupt safe, but not necessarily non-masking and bare-metal interrupt safe.
* ``const bool PWC::is_non_masking_interrupt_safe`` must return true if and only
if the clock is safe to use from non-masking and bare-metal interrupts.
The PigweedClock requirement will not require ``now()`` to be a static function,
however the upstream façades will follow this approach.
SystemClock facade
==================
The ``pw::chrono::SystemClock`` is meant to serve as the clock used for time
bound operations such as thread sleeping, waiting on mutexes/semaphores, etc.
The ``SystemClock`` always uses a signed 64 bit as the underlying type for time
points and durations. This means users do not have to worry about clock overflow
risk as long as rational durations and time points as used, i.e. within a range
of ±292 years.
The ``SystemClock`` represents an unsteady, monotonic clock.
The epoch of this clock is unspecified and may not be related to wall time
(for example, it can be time since boot). The time between ticks of this
clock may vary due to sleep modes and potential interrupt handling.
``SystemClock`` meets the requirements of C++'s ``TrivialClock`` and Pigweed's
``PigweedClock``.
This clock is used for expressing timeout and deadline semantics with the
scheduler in Pigweed including pw_sync, pw_thread, etc.
C++
---
.. cpp:class:: pw::chrono::SystemClock
.. cpp:type:: rep = int64_t;
.. cpp:type:: period = std::ratio<PW_CHRONO_SYSTEM_CLOCK_PERIOD_SECONDS_NUMBERATOR, PW_CHRONO_SYSTEM_CLOCK_PERIOD_SECONDS_DENOMINATOR>;
The period is specified by the backend.
.. cpp:type:: duration = std::chrono::duration<rep, period>;
.. cpp:type:: time_point = std::chrono::time_point<SystemClock>;
.. cpp:member:: static constexpr Epoch epoch = backend::kSystemClockEpoch;
The epoch must be provided by the backend.
.. cpp:member:: static constexpr bool is_monotonic = true;
The time points of this clock cannot decrease.
.. cpp:member:: static constexpr bool is_steady = false;
However, the time between ticks of this clock may slightly vary due to sleep
modes. The duration during sleep may be ignored or backfilled with another
clock.
.. cpp:member:: static constexpr bool is_free_running = backend::kSystemClockFreeRunning;
The now() function may not move forward while in a critical section or
interrupt. This must be provided by the backend.
.. cpp:member:: static constexpr bool is_stopped_in_halting_debug_mode = true;
The clock must stop while in halting debug mode.
.. cpp:member:: static constexpr bool is_always_enabled = true;
The now() function can be invoked at any time.
.. cpp:member:: static constexpr bool is_nmi_safe = backend::kSystemClockNmiSafe;
The now() function may work in non-masking interrupts, depending on the
backend. This must be provided by the backend.
.. cpp:function:: static time_point now() noexcept;
This is thread and IRQ safe.
.. cpp:function:: template <class Rep, class Period> static constexpr duration for_at_least(std::chrono::duration<Rep, Period> d);
This is purely a helper, identical to directly using std::chrono::ceil, to
convert a duration type which cannot be implicitly converted where the
result is rounded up.
.. cpp:function:: static time_point TimePointAfterAtLeast(duration after_at_least);
Computes the nearest time_point after the specified duration has elapsed.
This is useful for translating delay or timeout durations into deadlines.
The time_point is computed based on now() plus the specified duration
where a singular clock tick is added to handle partial ticks. This ensures
that a duration of at least 1 tick does not result in [0,1] ticks and
instead in [1,2] ticks.
Example in C++
--------------
.. code-block:: cpp
#include <chrono>
#include "pw_chrono/system_clock.h"
void Foo() {
const SystemClock::time_point before = SystemClock::now();
TakesALongTime();
const SystemClock::duration time_taken = SystemClock::now() - before;
bool took_way_too_long = false;
if (time_taken > std::chrono::seconds(42)) {
took_way_too_long = true;
}
}
Protobuf
========
Sometimes it's desirable to communicate high resolution time points and
durations from one device to another. For this, ``pw_chrono`` provides protobuf
representations of clock parameters (``pw.chrono.ClockParameters``) and time
points (``pw.chrono.TimePoint``). These types are less succinct than simple
single-purpose fields like ``ms_since_boot`` or ``unix_timestamp``, but allow
timestamps to be communicated in terms of the tick rate of a device, potentially
providing significantly higher resolution. Logging, tracing, and system state
snapshots are use cases that benefit from this additional resolution.
This module provides an overlay proto (``pw.chrono.SnapshotTimestamps``) for
usage with ``pw_snapshot`` to encourage capture of high resolution timestamps
in device snapshots. Simplified capture utilies and host-side tooling to
interpret this data are not yet provided by ``pw_chrono``.
---------------
Software Timers
---------------
SystemTimer facade
==================
The SystemTimer facade enables deferring execution of a callback until a later
time. For example, enabling low power mode after a period of inactivity.
The base SystemTimer only supports a one-shot style timer with a callback.
A periodic timer can be implemented by rescheduling the timer in the callback
through ``InvokeAt(kDesiredPeriod + expired_deadline)``.
When implementing a periodic layer on top, the user should be mindful of
handling missed periodic callbacks. They could opt to invoke the callback
multiple times with the expected ``expired_deadline`` values or instead saturate
and invoke the callback only once with the latest ``expired_deadline``.
The entire API is thread safe, however it is NOT always IRQ safe.
The ExpiryCallback is either invoked from a high priority thread or an
interrupt. Ergo ExpiryCallbacks should be treated as if they are executed by an
interrupt, meaning:
* Processing inside of the callback should be kept to a minimum.
* Callbacks should never attempt to block.
* APIs which are not interrupt safe such as pw::sync::Mutex should not be used!
C++
---
.. cpp:class:: pw::chrono::SystemTimer
.. cpp:function:: SystemTimer(ExpiryCallback callback)
Constructs the SystemTimer based on the user provided
``pw::Function<void(SystemClock::time_point expired_deadline)>``. Note that
The ExpiryCallback is either invoked from a high priority thread or an
interrupt.
.. note::
For a given timer instance, its ExpiryCallback will not preempt itself.
This makes it appear like there is a single executor of a timer instance's
ExpiryCallback.
.. cpp:function:: ~SystemTimer()
Cancels the timer and blocks if necssary if the callback is already being
processed.
**Postcondition:** The expiry callback is not in progress and will not be
called in the future.
.. cpp:function:: void InvokeAfter(chrono::SystemClock::duration delay)
Invokes the expiry callback as soon as possible after at least the
specified duration.
Scheduling a callback cancels the existing callback (if pending).
If the callback is already being executed while you reschedule it, it will
finish callback execution to completion. You are responsible for any
critical section locks which may be needed for timer coordination.
This is thread safe, it may not be IRQ safe.
.. cpp:function:: void InvokeAt(chrono::SystemClock::time_point timestamp)
Invokes the expiry callback as soon as possible starting at the specified
time_point.
Scheduling a callback cancels the existing callback (if pending).
If the callback is already being executed while you reschedule it, it will
finish callback execution to completion. You are responsible for any
critical section locks which may be needed for timer coordination.
This is thread safe, it may not be IRQ safe.
.. cpp:function:: void Cancel()
Cancels the software timer expiry callback if pending.
Canceling a timer which isn't scheduled does nothing.
If the callback is already being executed while you cancel it, it will
finish callback execution to completion. You are responsible for any
synchronization which is needed for thread safety.
This is thread safe, it may not be IRQ safe.
.. list-table::
* - *Safe to use in context*
- *Thread*
- *Interrupt*
- *NMI*
* - ``SystemTimer::SystemTimer``
- ✔
-
-
* - ``SystemTimer::~SystemTimer``
- ✔
-
-
* - ``void SystemTimer::InvokeAfter``
- ✔
-
-
* - ``void SystemTimer::InvokeAt``
- ✔
-
-
* - ``void SystemTimer::Cancel``
- ✔
-
-
Example in C++
--------------
.. code-block:: cpp
#include "pw_chrono/system_clock.h"
#include "pw_chrono/system_timer.h"
#include "pw_log/log.h"
using namespace std::chrono_literals;
void DoFoo(pw::chrono::SystemClock::time_point expired_deadline) {
PW_LOG_INFO("Timer callback invoked!");
}
pw::chrono::SystemTimer foo_timer(DoFoo);
void DoFooLater() {
foo_timer.InvokeAfter(42ms); // DoFoo will be invoked after 42ms.
}