Timer
Last updated
Last updated
It is customary in bare metal development to flash leds in the first application (instead of writing "Hello world"). However most tutorials show how to do it synchronously using loops to wait some time before changing state of the led. I'm going to describe how to do it asynchronously using timer interrupt in conjunction with Event Loop.
Almost every embedded platform has usually one or two timer peripherals. One such peripheral can be programmed to provide an interrupt after some period of time. However, there may be a need to have multiple timers that can be activated independently at the same time. It is quite clear that there should be an entity that receives all the wait requests from various Components in non-interrupt context, then queues the wait requests internally, programs the timer peripheral to provide an interrupt after some time, and finally reports the completion to appropriate Component via callback also in non-interrupt (event loop) context.
Such entity can be a generic (platform independent) Driver, if it is provided with platform specific Device object, that exposes some predefined public interface and controls the actual platform specific hardware.
The asynchronous timer event handling follows the same pattern described in Device-Driver-Component chapter.
Just like described in Device-Driver-Component chapter the Driver needs to provide the "Wait Complete" callback object to be called when timer interrupt occurs. The assignment is usually performed during initialisation/construction stage of the Driver:
The Driver must be able to support multiple wait requests from various Components and manage the internal queue accordingly. In the chart above the timer peripheral activated on the first asyncWait()
request. When the second request is issued (assuming timeout1 < timeout2
and existing wait mustn't be stopped), the Driver must prevent the completion of the currently scheduled timer countdown being reported in interrupt context while interfering with an update to internal data structures. The interrupts are disabled by calling suspendWait()
member function of the Device. The call to the suspendWait()
returns true
, which means the interrupts are successfully disabled and it is safe to update internal data structures. If the call to suspendWait()
returns false
, it means that the interrupt has already occurred and there is no existing wait in progress, i.e. the second asyncWait()
actually becomes a first one in the new sequence.
There also may be a case when timeout2 < timeout1
which means the order of the timeout requests must be re-evaluated, and new wait re-programmed.
The Driver must be able to cancel the existing timer countdown, evaluate how much time has passed since the first request, evaluate the new values to reprogram the timer Device countdown again.
Due to the fact that Driver may receive multiple independent wait requests, it must reprogram the next wait (if such exists) while running in interrupt mode. Please pay attention to InterruptCtx()
tag parameter passed to the startWait()
member function of the Device. It indicates that the request is executed in interrupt context, while the same request used EventLoopCtx()
as the tag parameter to specify that the call was performed in event loop (non-interrupt) context.
If there is a request to cancel the currently executed wait, the Driver must receive the information about the elapsed time and reprogram the next wait if such exists.
If the cancellation request to some other wait, that hasn't been forwarded to the Device, the Driver just needs to update its internal data structures without canceling currently performed timer countdown.
The unsuccessful attempts to cancel wait is performed in exactly the same way as described in Device-Driver-Component chapter.
There is obviously a need to have some kind of identification of the wait requests in order to be able to cancel some specific request while keeping the rest in waiting queue. One approach would be to have some kind of a handle which can be used during the cancellation request:
Another one is to hide the handle in some wrapper class, which makes it a bit safer to use:
The Driver itself has only one public function allocTimer()
. It is used to allocate the Timer
object. All the wait and/or cancel requests are issued to this timer object directly, which is declared to be a friend
of the Driver class, i.e. it is able to call private functions of the latter using the handle it has. The destructor of the Timer
makes sure that the handle is properly invalidated.
The second approach is a bit safer than the first one and it is used in the implementation of such generic "Timer Management Driver" in embxx library.
The timer Device is platform specific. Some platforms may support wait duration granularity of a microsecond, others can achieve only a millisecond. It usually depends on the system clock speed. However, when using generic Driver and/or Component there is a need to be able to write platform independent code that performs wait of the specified duration regardless of the Device in use. The Standard Template Library (STL) of C++11 standard provides convenient Date and Time Utilities that make such usage possible.
In case the Device declares a minimal wait duration unit using std::chrono::duration type, the Driver may use std::chrono::duration_cast to convert the requested wait duration to supported duration units.
In the example above the minimal supported duration unit (WaitTimeUnitDuration
) is declared to be 1 millisecond. Please note that startWait()
member function expects to receive number of wait units, i.e. milliseconds as its first parameter.
Then the definition of the asyncWait()
member function of the Driver may be defined like this:
In the example above the call below will perform correct adjustment of the duration and will measure the same timeout with any Device whether the latter expects milliseconds or microseconds in its startWait()
member function.
In case the developer tries to execute a wait of several microseconds when Driver supports only milliseconds granularity, the compilation will fail.
The timer management Driver is a generic layer. It must work on any platform with any timer Device object that exposes the right interface.
Such Driver is already implemented in embxx library as embxx::driver::TimerMgr
and resides in embxx/driver/TimerMgr.h while platform specific (Raspberry Pi) peripheral control object is implemented in embxx_on_rpi project as device::Timer
and resides in src/device/Timer.h.
The detailed documentation for embxx::driver::TimerMgr
can be found here.
The embxx::driver::TimerMgr
is defined like this:
The TDevice
template parameter is Platform specific control class for timer peripheral.
The TEventLoop
template parameter is the class of the Event Loop.
The TMaxTimers
template parameters specifies the maximal number of timer objects the TimerMgr
will be able to allocate. This parameter is required because embxx::driver::TimerMgr
was designed to be used in the systems without dynamic memory allocation. If dynamic memory allocation is allowed, then it is quite easy to implement similar functionality without this limitation.
The TTimeoutHandler
template parameter specifies type of the timeout callback object. This object must have void (const embxx::error::ErrorStatus&)
signature and expose similar interface to std::function or embxx::util::StaticFunction.
The embxx::driver::TimerMgr
exposes the following public interface:
The reader may notice that embxx::driver::TimerMgr
exposes only one public function: Timer allocTimer();
. This function returns simple TimerMgr::Timer
object which can be used to schedule new wait as well as cancel the previous wait request. Also note that TimerMgr::Timer
class is declared to be a friend
of TimerMgr
. This is required to allow seamless delegation of the wait/cancel request from TimerMgr::Timer
to TimerMgr
which is responsible for managing multiple simultaneous wait requests and delegating them one by one to the the actual hardware control object.
Then the led flashing application (implemented in src/app/app_led_flash) can be as simple as the code below:
As it was already mentioned earlier, the embxx::driver::TimerMgr
is a generic Driver class that does most of the work of managing and scheduling independent wait requests. It requires support from low level timer Device object to program the actual hardware of the platform the code runs on. The embxx::driver::TimerMgr
is defined to receive the Device class as template parameter as well as reference to the Device timer object in the constructor. The Driver doesn't know the exact Device type, but expects it to expose certain public interface:
The timer control Device class must expose the following public interface: 1. Define WaitTimeUnitDuration
type as variation of std::chrono::duration that specifies duration of single wait unit supported by the Device.
Function to set the callback object to be invoked from timer interrupt:
Functions to start timer countdown in both event loop (non-interrupt) and interrupt contexts:
Function to cancel timer countdown in event loop (non-interrupt) context. The function must return true in case the wait was actually canceled and false when there is no wait in progress.
Function to suspend countdown (disable interrupts while the actual wait countdown is not stopped) in event loop (non-interrupt) context. The function must return true in case the wait was actually suspended and false when there is no wait in progress. The call to this function will be followed either by resumeWait()
or by cancelWait()
.
Function to resume countdown in event loop (non-interrupt) context.
Function to retrieve elapsed time of the last executed wait. It will be called right after the cancelWait()
.
The definition and implementation of such timer device for Raspberry Pi platform can be found in src/device/Timer.h file of embxx_on_rpi project.