Timer Wrapper - UWCubeSat/DubSat1 GitHub Wiki
Overview
The timer wrapper is designed to abstract away the complicated bits of using timers on the MSP430FR5994 (and eventually, the MSP432, used in the ADCS system).
General Requirements
A successful, complete timer wrapper will include the following:
- A given subsystem module (MSP43x) must be able to track different time durations at the same time
- Only one timer peripheral instance (e.g. TimerA2) should be used to service ALL durations
- The user code should be able to refer to the different durations it cares about with an opaque "handle", NOT a timer value
- A maximum of 8 separate durations should be supported by the wrapper
- The timer wrapper must be optimized such that:
- Only durations that are actively in-use - i.e. have been requested by user code and not released - will contribute to the runtime overhead of the wrapper.
- Interrupt call frequency must be managed carefully so that interrupts are triggered with only as high a frequency as necessary, and no more
- As with all code relying on interrupts, care must be taken to minimize the calculations or other work performed while running at "interrupt level" (in other words, in the actual interrupt handler, or functions called directly or indirectly from the interrupt handler)
- Two separate APIs (function or set of functions meant to be called by the user code) should be provided:
- Polling-based timer
- Resolution required: 1 millisecond
- Acceptable error: -0ms / +10ms
- Callback timer
- Resolution required: 0.1 millisecond (100 us)
- Acceptable error: -0us / + 300us
- Polling-based timer
Note that the resolution and acceptable error margins in generally SHOULD allow the use of the 32.768kHz ACLK on the MSP43x device, which in turn should keep interrupt call rates to a reasonable level. The asymmetry to the error bounds are important to remember, however: timers in general must NOT "go off" early, under any circumstances.
Also, note that the "callback" timer is also known as the "interrupt timer", but both APIs will need to use interrupts, so the language has been changed to be more clear.
API Details
General
Both sets of APIs should exhibit several characteristics, detailed here.
Handles
A handle-based design is used in non-object-oriented programming environments like C. A handle is basically a "reference" to a particular instance of something the user cares about, and while the reference alone doesn't grant them special access to anything, it can be "handed to" APIs that use the handles, which will in turn use the handles to look up, inside internal (hidden) data tables, the relevant information for the particular instance that maps to the handle.
From an implementation perspective, the easiest way to deal with handles is usually to just have a handle be an unsigned 8-bit value, that is actually just the index into an array of "information" structs that the timer wrapper has hidden away. So, something like this would give you the initial setup:
typedef enum {
Timer_Polling,
Timer_Callback,
} duration_type;
typedef struct {
BOOL inUse;
duration_type type;
uint16_t durationMS; // Always indicate what the units are in struct names
timer_callback callbackPtr;
} duration_info;
FILE_STATIC duration_info durinfos[NUM_SUPPORTED_DURATIONS];
Now, when the user first wants a duration tracked, they'd call into the APIs, and based on which API they call and/or what they pass into the APIs, they get a handle back which is actually just the index into the durinfos
array, indicating which struct instance holds the information the user passed in. So, something like:
#include "core/utils.h"
...
hDev hPPTChargeTimer = timerPollInit( ... some stuff ...); // or other API options ... see below
...
if (timerDurElapsed(hPPTChargeTimer)) ... go do stuff if the timer elapsed
...
timerDurRelease(hPPTChargeTimer); // release the tracked duration
There are other variations on the handle theme, but for DS-1 we'll stick with something close to that described here.
Common Shape
In general, an API like this needs to provide ways to:
- initialize the whole system (i.e. configure Timer A2 to run with the right clock, enable GENERAL interrupts, etc.)
- specify a particular duration that the user cares about
- indicate that the duration should start "counting" NOW
- check (or be notified) when that duration has elapsed
- tell the timer system that a duration should no longer be tracked (i.e. release the duration)
There are lots of devils lurking in the details about these, but in general, a common use sequence would be #1-5. Also, it's possible that some could be combined into a single function, though the only combination that immediately comes to mind as sensible is combining #2 and #3 (e.g. timerInitBlahblah creates the duration AND starts it) ... though it is likely better to keep them separated.
Functions for #1, #3, and #5 could easily be the exact same between the two timer APIs, but it's not critical that that be so. Functions for #2 and #4 would almost certainly differ, and more details are provided below.
One interesting option to consider is whether to provide a way for users to indicate whether a timer is "single-shot" or "repeating" - i.e. whether the timer should keep going for another duration. There are a number of places where this would be useful. It would introduce another parameter into the initialization function.
Polling-Based Timer
Once the polling timer has been initialized as such (see above), the idea is that the user code is saying that they will check back in with the timer polling API at whatever rate they want, and would like to be told each time they check in whether the time has elapsed or not. Note that this requires very little interrupt handling for the timer's internals (pretty much just watching for rollover on the timer register(s) in use). Even more importantly, it says that in most cases, the biggest delay in being notified will be on the user code side. However, the test case should be user code, using the timer API to poll in a tight loop where they do nothing else. That is the scenario to compare against the acceptable error range listed above, i.e.:
while (TRUE)
{
if (timerDurElapsed(hMyHandle))
break;
}
The test would then place this "busy-wait" in a larger loop that flashes an output pin, that can in turn be used to determine the accuracy of the timing.
The singleshot vs. repeat behavior for the polling API could be implemented by having the timerDurElapsed() (or whatever it's called) function return an integer that is the number of times the duration has elapsed since it was last queried. This would be a nice feature, to be able to detect by users when their loop is running too slowly to actually snag all the individual durations.
Callback-Based Timer
This is the solution for users who want to be sure they are notified IMMEDIATELY upon the duration elapsed, and hence why the acceptable error and resolution bounds are more stringent than the polling API. When a timer is running in this mode, interrupts are used both for monitoring clock register roll-over (as in the polling method), but also an interrupt when the capture-compare register (CCR) on the timer "goes off", indicating that it's time to notify the user. Note that the timer is only looking at the CCR register after the the full rollovers have occurred for a given duration (i.e. if the user wants a duration that equates to 2.5 "register's worth" of ticks, then two rollovers must go by before putting the "stop value" in the CCR for that last tick-up.
The callback should be implemented with a function pointer. The syntax for function pointers in C is somewhat obnoxious, but there are examples both in our code (e.g. see debugtools.c/.h) and in every C reference. Make the callback signature (the arguments that get passed back to the user) as small as possible - maybe just a handle that identifies which timer is giving the callback. You will be calling their function on an "interrupt thread", so it's important everything moves as fast as possible.
Naming
The DS-1 project doesn't unfortunately have hard and fast rules around naming functions, variables, enums, and the like, but at least one pattern that has emerged is that entities that are grouped together (like the I2C functions and data structures, or the UART functions and data structures) have a leading, lower-case "tag" at the front of all those names, like "i2cInit()". For the timers, please use "timerXxxYyy()" for all the public functions that users will use. Functions that are internal to the timer implementation can be named whatever you want, but be sure they are pre-pended with FILE_STATIC (which ends up equating to the C keyword 'static') so that nobody can get to them outside of the timers.c/.h file.