7. Watching time elapse
Time is a complex topic. In computing systems, there are generally several notions of time. Digital circuits have clock sources that tick at different rates to drive circuits. These don't even have a notion of elapsed time, they simply trigger at a predictable rate that's within the range that allows signals to propagate across the circuit in one cycle.
The simplest notion of elapsed time comes from simply counting the number of times one of these clocks has ticked. Assuming that this clock rate is constant, this gives a way of measuring elapsed time but not a notion of points in time that are relevant to anyone else (or any other computer). There may be multiple such clock sources that operate at different rates, including ones specific to peripherals (for example, an analogue-to-digital converter may have its own clock that it uses for quantisation).
Most computers expose something that they refer to as a real-time clock or a wall clock. CHERIoT RTOS prefers the latter term because this is not the clock used in real-time systems and the overloaded term is confusing. The wall clock time is the time that you might find on a clock on the wall: a time where zero is some consensus value. These are usually defined in terms of UTC. For example, the UNIX epoch, specified in POSIX, is 00:00:00 UTC on 1 January 1970 (i.e. the beginning of 1970, UTC). UNIX time is the number of seconds since that date.
This is a useful coordination point but times exposed to humans often also need to take time zones into account. If you're creating a user interface in, say, Australia, then a clock that presents UTC will be very confusing. You need to apply a displacement to calculate the local time. This is normally a number of hours (and may change for winter or summer), but a few are some, such as Nepal, that are offset by quarter of an hour. Embedded systems can often avoid this complexity and leave user interfaces to some external component, or provide minimal support for the time zones in their target markets.
7.1. Understanding the monotonic clock
CHERIoT RTOS requires a monotonic clock source. This is a single clock that increments at a fixed rate. The RISC-V specification defines the mtime register to provide this source but implementations may choose to expose this as a memory-mapped I/O device or via some other mechanism (Chapter 14. Adding a new board describes how to define this interface).
This clock source has two requirements. It must increment at a constant rate and it must be possible to get an interrupt relative to this clock. The latter requirement can also be satisfied by having an interrupt source that counts down from a user-specified value at the same rate as the monotonic clock increments.
Interfaces to the scheduler all use these units, or ticks, which are defined relative to the monotonic clock. For more discussion of ticks, see Section 7.3. Converting to ticks.
The lowest-level interface to the monotonic clock is the platform_monotonic_time_read function, implemented in the platform-time.h header, which is provided by the board's platform integration code.
uint64_t platform_monotonic_time_read()Platform-specific hook for reading the current monotonic time. This should be implemented to return the time that increments with the rate defined by the CPU_TIMER_HZ macro and must be the same timer used by the scheduler.
This API returns an unsigned 64-bit integer, which is intended to allow any plausible time, without overflow. If the clock increments at the CPU cycle speed, a 200 MHz microcontroller will elapse around 6.3 quadrillion ticks per year. That's enough to overflow a 52-bit counter. At 2 GHz, that's 55 bits. Even at 2 GHz, a 64-bit counter would last for 512 years without overflow. Although CHERIoT aims to support long device lifetimes, supporting several centuries of uptime is probably unusual.
This function is used to implement the POSIX-compatible clock_gettime call when used to read the monotonic clock.
int clock_gettime(clockid_t clockID, struct timespec * outTime)Retrieve the time from the specified clock as a timespec.
If clockID is CLOCK_REALTIME, the returned value is meaningful only if clock_update_wall_clock has been called at least once and there is at least one working clock source in the system.
This API avoids the need to understand the tick rate of clocks and instead returns a structure (struct timespec) containing seconds and nanoseconds.
Prior to C23, the nanoseconds (tv_nsec) field of struct timespec was defined to be long but it was undefined behaviour if the value was not between 0 and 999,999,999. C23 made this a user-defined type capable of storing that range. This precision requires a 29-bit value, so CHERIoT RTOS uses uint32_t for this field: negative out-of-range values are not expressible and overflow is well defined.
7.2. Mapping between clocks
POSIX defines several clocks, of which CHERIoT RTOS implements a subset. CLOCK_MONOTONIC is the monotonic clock, as previously discussed. This is zero at boot at a stable rate.
These two properties mean that, if you knew the monotonic time and wall-clock time for some point in the past, you can compute the wall-clock time for the present. This is precisely what the clocks subsystem does. It stores a pair of a monotonic time and a wall-clock time that corresponded to the same time. When you ask for the current wall-clock time, it subtracts the snapshot monotonic time from the current monotonic time and adds it to the snapshot wall-clock time.
The clock_gettime function is implemented in the clock_helpers shared library. If you call clock_gettime with CLOCK_REALTIME, this library does the calculation described above.
If you query the wall-clock time, you may not get a meaningful value unless something has set it. Section 7.4. Providing a wall-clock source describes how to plug in a time source. If you're using the network stack, you can use NTP to set the time (see Section 13.2. Synchronising time with SNTP).
POSIX also defines a CLOCK_THREAD_CPUTIME_ID as the time that the current thread has spent running. This is the value that should be returned from the clock function. The CHERIoT RTOS scheduler does not track this by default, but can if built with the --scheduler-accounting=y configure-time option.
clock_t clock()Returns the amount of CPU time (in units defined by CLOCKS_PER_SEC) that are accounted to the current thread (POSIX specifies 'process' here, but CHERIoT RTOS does not have an directly analogous abstraction).
Note: If scheduler accounting is not enabled, this API will return the total elapsed uptime instead. Add --scheduler-accounting=y to your build configuration line to enable this.
The other clock defined by POSIX relates to processes. The CLOCK_PROCESS_CPUTIME_ID clock is supposed to count the amount of time elapsed by the current process, including all threads, but CHERIoT RTOS does not have an abstraction that corresponds directly to a process.
Listing 55 shows how to query ech of the clocks. If you run this in a simulator, you will likely see very similar values for each, for example output such as this:
Clocks Example: Monotonic time: 2 seconds, 37220000 nanoseconds Clocks Example: Wall-clock time: 2 seconds, 83160000 nanoseconds Clocks Example: Thread CPU time: 2 seconds, 128640000 nanoseconds
This is because the wall-clock time offset has not been set, the wall-clock time is set to the same as the monotonic clock time. Similarly, there is a single thread, so the pre-thread CPU time will be the same as the monotonic time.
timespec ts;clock_gettime(CLOCK_MONOTONIC, &ts);Debug::log("Monotonic time: {} seconds, {} nanoseconds",ts.tv_sec,static_cast<int>(ts.tv_nsec));clock_gettime(CLOCK_REALTIME, &ts);Debug::log("Wall-clock time: {} seconds, {} nanoseconds",ts.tv_sec,static_cast<int>(ts.tv_nsec));clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts);Debug::log("Thread CPU time: {} seconds, {} nanoseconds",ts.tv_sec,static_cast<int>(ts.tv_nsec));
Listing 55. Querying the various clocks.examples/clocks/clocks.cc
7.3. Converting to ticks
CHERIoT RTOS provides a tick abstraction that mirrors that of FreeRTOS. This originated with old scheduler designs, which scheduled a regular timer interrupt (for example, every 10 ms) and made scheduling decisions when that interrupt fired. Both CHERIoT RTOS and FreeRTOS started with this design and both moved away. This is a simple model but it doesn't interact well with power management because the scheduler is always taking periodic interrupts, even when it has a single runnable thread.
The CHERIoT RTOS scheduler is a tickless scheduler. Rather than scheduling a periodic tick, it requests a timer interrupt at the time that the next thread of a higher priority will wake. This reduces jitter and means that the core can spend longer in a wait-for-interrupts state. Ticks are still used for scheduling threads at the same priority. If two (or more) threads are at the same priority and are both runnable, the tick defines the amount of time that each will run for before being preempted. This is configurable in the board file (or via an override), allowing individual deployments to trade latency and throughput. Longer tick times mean that is less overhead from context switching but threads spend longer between opportunities to run.
The standard CLOCKS_PER_SEC constant in time.h defines the number of monotonic-clock increments per second. The CHERIoT-specific ticks.h header defines a MS_TO_TICKS() macro that takes a number of milliseconds as an argument and returns the number of ticks that are provided. Both of these are defined by two pre-defined macros that the build system sets.
The CPU_TIMER_HZ macro defines the rate at which the monotonic clock increments. The TICK_RATE_HZ macro defines the number of scheduler ticks per second.
7.4. Providing a wall-clock source
The wall-clock compartment manages a set of clock sources. These are intentionally pluggable because they may relate to both the available hardware (for example, a battery-backed clock) or to the specific system. For example, the network stack's Simple Network Time Protocol (SNTP) compartment (see Section 13.2. Synchronising time with SNTP) provides a time source that fetches the time over the network.
Each clock source advertises two high-level pieces of information. The first defines the behaviour: Does this simply provide a time value, or does it also record the time? For example, a battery-backed clock may give an initial time but then be possible to set with something more precise. The second defines whether this time source is 'cheap' to check. Cheap time sources are polled every time the wall-clock compartment is instructed to update the time, expensive sources are checked at most once a minute (this frequency will likely be configurable in a future version).
When clock_update_wall_clock is called, the wall-clock compartment will iterate over the provided clock sources and query each in turn (possibly skipping expensive ones). Each clock source can then provide a wall-clock time, a monotonic time corresponding to that time, and a priority. The source that provides the highest priority will be treated as the new reference. It will then be written back to any clock sources that support setting the time and
On more complex systems, setting a new time will slowly skew the clock, adjusting the rate at which the clock advances until it converges with the desired value. CHERIoT RTOS's wall-clock compartment does not do this, it simply updates the time. This can lead to abrupt jumps in the wall-clock time.
int clock_update_wall_clock(TimeoutArgument timeout)Update the wall-clock time from available time sources.
Listing 56 shows a simple example of a clock source. This is designed to work in simulators that don't have a real clock source. Instead, it parses the __DATE__ and __TIME__ built-in macros that define the build time (hidden here in the build_time call). This time is then set as associated with monotonic time zero.
struct ExampleWallClockSource{/// This example source does not support setting the time.static constexpr bool SupportsTimeSetting = false;/// This example source is quick to computestatic constexpr bool IsCheap = true;/// Calculate the time, assume that boot time is build/// timeint get_time(TimeoutArgument,clock_t &outRealTime,clock_t &outMonotonicTime,int &outPriority){outMonotonicTime = 0;// Compute the time when this image was builtoutRealTime = build_time();// This is a terrible time sourceoutPriority = -5000;return 0;}};
Listing 56. A trivial example of a clock source.examples/wall_clock/include/example_rtc.hh
This example sets a priority of -5,000 because almost any other time is probably better than this one, which becomes more wrong the firmware image is deployed. For comparison, the fallback clock source, which simply sets both to zero (so the boot time is the POSIX Epoch time, the start of 1970) sets its priority as the smallest possible value. The SNTP compartment sets its priority at 1000. A battery-backed clock would probably set its priority somewhere below the NTP time, but above zero. The value of -5,000 means better than nothing but worse than anything sensible. This value does not need to be constant. For example, a clock source that has some known (but not fully quantified) drift may decay its priority the longer it's been since it was last set.
The build system integration for this is shown in Listing 57. The registration happens in xmake's after_load phase. This runs after all of the individual targets have been loaded and resolved but before any of them have been built. In this example, this hook is implemented on the firmware target but it can be on any target that is loaded as part of the build. For example, the SNTP compartment does the same in the compartment's build target so that any firmware image that builds the SNTP compartment also uses it as a clock source automatically.
-- Register the wall-clock sourceafter_load(function(target)import("core.project.project")local wall_clock = project.target("wall_clock")wall_clock:add("includedirs", path.join(target:scriptdir(), "include"))wall_clock:add("cheriot.clock_source_includes", "example_rtc.hh")wall_clock:add("cheriot.clock_source_types", "ExampleWallClockSource")end)
Listing 57. Registering the clock source in the build system.examples/wall_clock/xmake.lua
This build-system snippet is setting three properties on the wall-clock compartment's target. The first is adding the include/ directory relative to the example to the include search path for the wall-clock compartment. This allows it to find the header. The next specifies the name of the header to include. Finally, it adds the name of the type that the header defines. There is no requirement to have one type per header, you can provide a header that implements multiple clock sources.
7.5. Reading the wall-clock time
The interfaces for reading the wall-clock time are intended to follow POSIX. Listing 58 shows an example that prints the human-readable time once per second.
int update = clock_update_wall_clock(TimeoutWaitForever);Debug::Assert(update == 0,"Failed to update wall-clock time: {}",update);while (true){timeval tv;int ret = gettimeofday(&tv, nullptr);Debug::Assert(ret == 0, "Failed to get time of day: {}", ret);auto *timeUTC = gmtime(&tv.tv_sec);Debug::log("Current UNIX epoch time: {} {}-{}-{} {}:{}:{} UTC",tv.tv_sec,timeUTC->tm_year + 1900,timeUTC->tm_mon + 1,timeUTC->tm_mday,timeUTC->tm_hour,timeUTC->tm_min,timeUTC->tm_sec);Timeout t(MS_TO_TICKS(1000));thread_sleep(&t, ThreadSleepNoEarlyWake);}
Listing 58. Read and print the wall-clock time.examples/wall_clock/clocks.cc
This starts by calling clock_update_wall_clock, which instructs the wall-clock compartment to set the time using any of the available clock sources, as described in the previous section. It then loops, calling gettimeofday to get the current time and gmtime to convert it from a number of seconds since the POSIX epoch to a human-readable date. Finally, it sleeps for a second before retrying.
If you run this example in a simulator then you will see that its internal notion of time is probably wrong. Simulators do not advance time at a rate that matches anything outside of their simulated world. Depending on the speed of your computer, this example may advance time much more quickly or slowly than one second per second.
If your time zone is not UTC, then you will notice that the time is offset because the __TIME__ macro that the clock source in this example does not include a time zone. When you run this example, it should repeatedly print time, starting at the build time of the project and elapsing as the simulator models time.