Introduction
This example program AlarmEval
introduces using the RP2040 timer device’s alarms to trigger handler procedures with microseconds (us) timing resolution, using module Alarms
in the example program directory.
Update: note that the library module Alarms
is a conceptually and technically simplified version of the module used here (see section Bottom Line).
In TaskEval, we had looked at a general purpose task system, which is flexible, but comes with some complexity and many possible ways and dimensions for the configuration of the memory access for the program code, in particular regarding the effect of caching the flash memory contents. As outlined in the section Bottom Line, there’s probably too much flexibility and variability for a library module, considering the application area of embedded control systems. Also, since every thread could potentially schedule a task at any time, there’s the conceptual and design question how to avoid timing issues and “collisions” between the threads.
Module Alarms
Alarm = Device
Module Alarms
takes a different approach. Each of the four alarm timers is handled as a separate device, just as a UART or SPI peripheral. A program or thread “acquires” the device, and uses it to schedule its alarm procedures, directly as interrupt handlers. Any sharing of the device between threads needs to be coordinated as with any other device, eg. a UART for serial output.
The programmer allocates the alarm devices by design, and is responsible for the coordinated use between threads, or the exclusive use by one thread. Importantly, each alarm is a separate entity regarding its data, there’s no shared data in the library module, such as the run-queue in the task system. Of course, the threads using an alarm must ensure coordinated access to any data shared with its own alarm handlers, but that is true for any interrupt. Also, since an alarm device is exclusive to one thread at a given time, assuming a correct coordination, there cannot be timing issues and “collisions” between the threads.
Interrupt Handlers
This approach is lightweight, but puts more responsibility on the programmer. For example, the scheduled handler procedures must be programmed as interrupt handlers, and they are responsible for de-asserting the interrupt signals from the alarm device.
A “handler chaining” feature allows to easily schedule follow-up handlers, based on the time base of the current handler, to implement a sequence of actions in a precisely timed fashion, eg. for measurement or actuator control.
Recovery: Alarm Times in the Past
A question arises what to do in case an intended alarm time is before the the point in time where the arming takes place. This situation can occur for different reasons, such as some processing taking longer than usual, eg. due to high prio interrupts being served frequently, or relevant program code having been evicted from flash memory cache. As demonstrated in CodeLoading, according to my measurements, reading from flash memory can take about twenty times (!) as long as reading from the flash cache – 400 nanoseconds per load compared to under 20 nanoseconds. With microseconds alarm timing, this can be make or break.
Basically, this error situation can be handled in the library module in three different ways:
- ignore: just use the scheduling time as given by the client program, which puts the full responsibility on the programmer, but is the most straightforward way, or
- retime: adjust the alarm time to the future from the point where the arming takes place, or
- direct: execute the alarm handler directly from the arming procedure, bypassing the alarm timer.
Alarm retiming and direct execution are referred to as (error) recovery in this document.
Module Alarm
implements all three options for testing and measuring purposes. See section Bottom Line for the corresponding conclusions.
The alarm device RECORD Alarms.Device
refers to either Alarms.ArmRaw
, Alarms.ArmRetime
or Alarms.ArmDirect
, which implement options 1, 2 and 3, respectively, set via Alarms.Init
.
Regarding option 3, see also section Calling an Interrupt Handler from Code? at the end of this document.
Time Arithmetic
See Timers and Timing.
A relevant takeaway is that the maximum alarm time can be the current time plus 2^31 - 1, ie. about 35 minutes.
The Example Program
Structure
The program module AlarmEval
creates a series of alarms, which are serviced by two interrupt handlers, AlarmEval.p0
and AlarmEval.p1
. The first handler re-arms itself a few times, then actives the second handler, which does the same, then terminates. The handlers collect the run-time data, which will be printed by a third handler, AlarmEval.p3
, which is armed to be activated by another alarm device after the runs. The test sequence is initiated by AlarmEval.start
, which runs in thread mode, not handler mode.
This allows to explore and test timing recovery, for different times between alarms, ie. how closely to each other can alarms be set, as well as for set-up times, ie. how closely to the current time an alarm can be set.
The run times of the alarm handlers presented below are composed of two parts,
- their time in module
Alarms
, plus - their time in the test program specific part.
That is, the results are specific for this test program, but should nonetheless allow some generalised observations and conclusions, mutatis mutandis.
Timing Results
As usual, there can be minor time differences with your MCU/board. Also, there’s always a one microsecond uncertainty when reading the time: we can happen to read the current time from the hardware just before or after the microseconds tick, ie. get a difference of one even if the actual times were virtually identical.
Global Timer
For testing purposes, we set the global timer close to 0FFFFFFFFH
to evaluate the roll-over. In a control program, the timer should not be changed thusly, in order to get a uniform time basis for all threads, or parts of a monolithic program. It’s set to zero upon reset of the MCU.
Pre-caching
In lieu of storing code in SRAM, we’ll lazily use and test the feature MemoryExt.CacheProc
to pre-load a procedure into flash memory cache. Of course, storing code in SRAM is always a possibility for ultimate guaranteed performance, but entails a specific design, explained in CodeLoading
. Pre-caching requires no code changes, and any run-time errors info and stack traces are correct.
Test Parameters
With this example test program, I’ll simply print the relevant CONST parameter values with each test result, in lieu of listing their settings here in the description text as I did for TaskEval.
Now, finally, let’s run the example program for different test cases.
Or jump down to the Bottom Line.
Timing Baseline
The first runs establish a timing baseline, using “unproblematic” set-up values, and allow me to introduce the output structure. I know these unproblematic values as I have run the tests before. :)
With AlarmEval.Recovery = RecoveryNone
Build and run AlarmEval
, which prints to the serial terminal:
StartTime: FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter: 50
TimeBetweenRuns: 25
PreCachedProcs: No
Recovery: None
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFFA0H FFFFFFA5H FFFFFFB1H 12 FFFFFFB2H 6 18
1 T FFFFFFD2H FFFFFFD2H 1 FFFFFFD3H FFFFFFDEH FFFFFFE5H 7 FFFFFFE7H 13 20
2 T FFFFFFEBH FFFFFFEBH 0 FFFFFFEBH FFFFFFEBH FFFFFFECH 1 FFFFFFECH 0 1
3 T 00000004H 00000004H 0 00000004H 00000004H 00000005H 1 00000005H 0 1
4 T 0000001DH 0000001DH 0 0000001DH 0000001EH 00000022H 4 00000022H 1 5
5 T 00000036H 00000036H 1 00000037H 00000042H 00000045H 3 00000046H 12 15
6 T 0000004FH 0000004FH 0 0000004FH 0000004FH 00000050H 1 00000050H 0 1
7 T 00000068H 00000068H 0 00000068H 00000068H 00000069H 1 00000069H 0 1
8 T 00000081H 00000081H 0 00000081H 00000081H 0 0
The first lines list the test parameters, as defined in AlarmEval
:
- StartTime: the start time of the global timer, adjusted to get the alarm times around the timer’s lower 32 bit roll-over point;
- FirstRunAfter: the time of the first alarm after the test run timing starts, ie. basically between
run 0
andrun 1
;run 0
is the start procedure run from the main program; - TimeBetweenRuns: the alarm time between all runs from and including
run 1
; - PreCachedProcs: if procedure pre-caching was used; if yes, the value in brackets is the time it took for pre-caching (see below);
- Recovery: the recovery method used in case an alarm time is before the current time, either
None
,Retime
orDirect
, for options 1, 2 or 3, respectively, as outlined above.
The columns for the run data:
- run: run number;
run 0
does not run as alarm handler, hence there are no values forrm[0]
,al-arm[0]
, andal-run[0]
; the last run does not arm any further alarm, so there are not values forarm[8]
,armed[8]
, andatm[8]
; - rm: run-mode, with
T
the handler was run on time, as alarm handler, ie. with the intended alarm time,R
the handler was run using recovery, either with an adjusted alarm time (retiming), or called directly;
- al-arm: the intended alarm time, ie. for when the alarm should be armed;
- al-run: the actual, adjusted alarm time for retiming, or the time just before the direct call;
- rdel: run delay, ie. the time between
al-arm
andrun-beg
; - run-beg: start time of the handler run, without the exception entry time (stacking etc.);
- arm: time when the arming is initiated; this is the time of entry into module
Alarms
; - armed: time when the arming is done, ie. exit from module
Alarms
; if the handler was called directly, this will include the latter’s run time; - atm: the time spent in module
Alarms
, ie.armed - arm
; - run-end: end time of the handler run, without the exception exit time;
- htm: the time spent in the handler, without the time inside module
Alarms
, ie.rtm - atm
; - rtm: total handler run time, ie.
run-end - run-beg
, including the time spent in the library module (atm
).
All times are in microseconds. They are printed as hexadecimal numbers, as they are CARDINAL values, not (signed) INTEGER (CARDINAL as with Modula-2, ie. 0 to 2^32 - 1).
I’ll write, for example, al-arm[0]
to refer to the value al-arm
for run 0
.
Observations:
- all handlers are triggered normally on time by the alarm (
rm = T
); - we see the caching of the two handlers
- total in the
rtm
column: 20 us for the first handlerp0
inrun 1
, and 15 us for the second onep1
inrun 5
, and - just of the handler code outside module
Alarms
inhtm
;
- total in the
- we also see the caching of the library code in
atm
; - as expected, the performance increase due to the caching “as we go” is substantial.
With AlarmEval.Recovery = RecoveryRetime
StartTime: FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter: 50
TimeBetweenRuns: 25
PreCachedProcs: No
Recovery: Retime
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFFA0H FFFFFFA5H FFFFFFB5H 16 FFFFFFB6H 6 22
1 T FFFFFFD2H FFFFFFD2H 1 FFFFFFD3H FFFFFFDEH FFFFFFE6H 8 FFFFFFE8H 13 21
2 T FFFFFFEBH FFFFFFEBH 0 FFFFFFEBH FFFFFFEBH FFFFFFECH 1 FFFFFFECH 0 1
3 T 00000004H 00000004H 0 00000004H 00000004H 00000005H 1 00000005H 0 1
4 T 0000001DH 0000001DH 0 0000001DH 0000001EH 00000022H 4 00000022H 1 5
5 T 00000036H 00000036H 1 00000037H 00000042H 00000045H 3 00000046H 12 15
6 T 0000004FH 0000004FH 0 0000004FH 0000004FH 00000050H 1 00000050H 0 1
7 T 00000068H 00000068H 0 00000068H 00000068H 00000069H 1 00000069H 0 1
8 T 00000081H 00000081H 0 00000081H 00000081H 0 0
Observation: unsurprisingly, we get the same values, with the exception of the time spent inside module Alarms
, ie. atm
, since Alarms.ArmRetime
is a bit more complex than Alarms.ArmRaw
.
With AlarmEval.Recovery = RecoveryDirect
StartTime: FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter: 50
TimeBetweenRuns: 25
PreCachedProcs: No
Recovery: Direct
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFFA0H FFFFFFA5H FFFFFFB6H 17 FFFFFFB7H 6 23
1 T FFFFFFD2H FFFFFFD2H 1 FFFFFFD3H FFFFFFDEH FFFFFFE6H 8 FFFFFFE7H 12 20
2 T FFFFFFEBH FFFFFFEBH 0 FFFFFFEBH FFFFFFEBH FFFFFFECH 1 FFFFFFECH 0 1
3 T 00000004H 00000004H 0 00000004H 00000004H 00000005H 1 00000005H 0 1
4 T 0000001DH 0000001DH 0 0000001DH 0000001EH 00000022H 4 00000022H 1 5
5 T 00000036H 00000036H 1 00000037H 00000042H 00000045H 3 00000046H 12 15
6 T 0000004FH 0000004FH 0 0000004FH 0000004FH 00000050H 1 00000050H 0 1
7 T 00000068H 00000068H 0 00000068H 00000068H 00000069H 1 00000069H 0 1
8 T 00000081H 00000081H 0 00000081H 00000081H 0 0
Observation: same story.
With Pre-caching AlarmEval.PreCachedProcs = TRUE
StartTime: FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter: 50
TimeBetweenRuns: 25
PreCachedProcs: Yes (131)
Recovery: Retime
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFF9EH FFFFFF9FH FFFFFFA1H 2 FFFFFFA1H 1 3
1 T FFFFFFD0H FFFFFFD0H 0 FFFFFFD0H FFFFFFD0H FFFFFFD1H 1 FFFFFFD1H 0 1
2 T FFFFFFE9H FFFFFFE9H 0 FFFFFFE9H FFFFFFE9H FFFFFFEAH 1 FFFFFFEAH 0 1
3 T 00000002H 00000002H 0 00000002H 00000002H 00000003H 1 00000003H 0 1
4 T 0000001BH 0000001BH 0 0000001BH 0000001BH 0000001CH 1 0000001CH 0 1
5 T 00000034H 00000034H 1 00000035H 00000036H 00000037H 1 00000037H 1 2
6 T 0000004DH 0000004DH 0 0000004DH 0000004DH 0000004EH 1 0000004EH 0 1
7 T 00000066H 00000066H 0 00000066H 00000066H 00000067H 1 00000067H 0 1
8 T 0000007FH 0000007FH 0 0000007FH 0000007FH 0 0
Observation: pre-caching works well. It takes 131 us to load the code into the flash memory cache (see AlarmEval.preCache
).
The results are the same for all recovery modes (none, retime, direct).
Summary So Far
The above tests show the normal cases, where all alarm times are used and set correctly to allow all handlers to run on time from flash with subsequent caching, without their run times interfering with subsequent alarms.
Remember that the run times presented here are composed of two parts, their time in module Alarms
, plus the time in the test program specific part. In a real control program, the handler run times could also be worsened by handlers servicing interrupts at higher priority.
Decreasing Consecutive Alarm Times: AlarmEval.TimeBetweenRuns = 15
If we look at the handler run times (without pre-caching) above, we see that times between alarms (ie. test parameter CONST TimeBetweenRuns
) of 20 or fewer microseconds will cause problems: the handler will attempt to re-arm the consecutive handler for an alarm time before the current time.
With AlarmEval.Recovery = RecoveryNone
StartTime: FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter: 50
TimeBetweenRuns: 15
PreCachedProcs: No
Recovery: None
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFF9FH FFFFFFA4H FFFFFFB1H 13 FFFFFFB1H 5 18
1 T FFFFFFD1H FFFFFFD1H 1 FFFFFFD2H FFFFFFE6H 20 20
Observations:
- oops, but expected, since we have no recovery: the run time of 20 us for
run 1
caused the subsequent alarm time to be in the past when re-arming was attempted run 2
will be executed if we wait ~70 minutes, though :)
Consequently, we’ll not use AlarmEval = RecoveryNone
(option 1, above) anymore for error cases.
With AlarmEval.Recovery = RecoveryRetime
StartTime: FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter: 50
TimeBetweenRuns: 15
PreCachedProcs: No
Recovery: Retime
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFF9FH FFFFFFA5H FFFFFFB5H 16 FFFFFFB5H 6 22
1 T FFFFFFD1H FFFFFFD1H 1 FFFFFFD2H FFFFFFDDH FFFFFFE5H 8 FFFFFFE7H 13 21
2 R FFFFFFE0H FFFFFFE9H 9 FFFFFFE9H FFFFFFE9H FFFFFFEAH 1 FFFFFFEAH 0 1
3 R FFFFFFEFH FFFFFFF1H 2 FFFFFFF1H FFFFFFF1H FFFFFFF2H 1 FFFFFFF2H 0 1
4 T FFFFFFFEH FFFFFFFEH 0 FFFFFFFEH FFFFFFFFH 00000003H 4 00000003H 1 5
5 T 0000000DH 0000000DH 1 0000000EH 00000019H 0000001DH 4 0000001DH 11 15
6 R 0000001CH 00000022H 6 00000022H 00000022H 00000023H 1 00000023H 0 1
7 T 0000002BH 0000002BH 0 0000002BH 0000002BH 0000002CH 1 0000002CH 0 1
8 T 0000003AH 0000003AH 0 0000003AH 0000003AH 0 0
Observations:
- we now see retimed runs (
rm = R
), due to the run times of 21 us forrun 1
and 15 us forrun 5
, and their spillover effects - inspect
Alarms.ArmRetime
:al-run[2]
must be in the range ofarm[1]
toarmed[1] + Alarms.dev.margin
- the other runs were on time (
rm = T
), thanks to the caching
With AlarmEval.Recovery = RecoveryDirect
StartTime: FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter: 50
TimeBetweenRuns: 15
PreCachedProcs: No
Recovery: Direct
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFF9FH FFFFFFA4H FFFFFFB5H 17 FFFFFFB6H 6 23
1 T FFFFFFD1H FFFFFFD1H 1 FFFFFFD2H FFFFFFDDH FFFFFFEBH 14 FFFFFFEBH 11 25
2 R FFFFFFE0H FFFFFFE2H 5 FFFFFFE5H FFFFFFE6H FFFFFFE8H 2 FFFFFFEAH 3 5
3 T FFFFFFEFH FFFFFFEFH 0 FFFFFFEFH FFFFFFEFH FFFFFFF0H 1 FFFFFFF0H 0 1
4 T FFFFFFFEH FFFFFFFEH 0 FFFFFFFEH FFFFFFFFH 00000003H 4 00000003H 1 5
5 T 0000000DH 0000000DH 1 0000000EH 00000019H 00000020H 7 00000020H 11 18
6 R 0000001CH 0000001BH 0 0000001CH 0000001CH 0000001EH 2 0000001FH 1 3
7 T 0000002BH 0000002BH 0 0000002BH 0000002BH 0000002CH 1 0000002CH 0 1
8 T 0000003AH 0000003AH 0 0000003AH 0000003AH 0 0
Observations::
- again, we have recovered runs (
rm = R
), this time by direct execution of the handler from code run 2
was executed directly by the handler forrun 1
,run 6
by the handler forrun 5
- inspect
Alarms.ArmDirect
: the time fromrun-beg[2]
torun-end[2]
should be inside the time fromarm[1]
andarmed[1]
Further Decreasing Consecutive Alarm Times: AlarmEval.TimeBetweenRuns = 5
Let’s push the limits a bit, with AlarmEval.TimeBetweenRuns = 5
.
With AlarmEval.Recovery = RecoveryRetime
StartTime: FFFFFFB9H = FFFFFFFFH - 70
FirstRunAfter: 50
TimeBetweenRuns: 5
PreCachedProcs: No
Recovery: Retime
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFFBEH FFFFFFC3H FFFFFFD3H 16 FFFFFFD4H 6 22
1 T FFFFFFF0H FFFFFFF0H 1 FFFFFFF1H FFFFFFFCH 00000004H 8 00000006H 13 21
2 R FFFFFFF5H 00000008H 19 00000008H 00000008H 00000009H 1 00000009H 0 1
3 R FFFFFFFAH 00000010H 22 00000010H 00000010H 00000011H 1 00000011H 0 1
4 R FFFFFFFFH 00000018H 25 00000018H 00000019H 0000001DH 4 0000001EH 2 6
5 R 00000004H 00000023H 32 00000024H 0000002FH 00000033H 4 00000033H 11 15
6 R 00000009H 00000038H 47 00000038H 00000038H 00000039H 1 00000039H 0 1
7 R 0000000EH 00000040H 50 00000040H 00000040H 00000041H 1 00000041H 0 1
8 R 00000013H 00000048H 53 00000048H 00000048H 0 0
Observations:
- all handlers are executed with recovery
rm = R
, apart fromrun 1
, which is armed by the test start-up code afterFirstRunAfter = 50
, ie. there’s no impact ofTimeBetweenRuns
- there is a noticeable delay
rdel
, which is caused by the run timesrtm[1]
andrtm[5]
(plus some caching) - all handlers are properly triggered by an alarm, compare to the next test with direct execution recovery
With AlarmEval.Recovery = RecoveryDirect
StartTime: FFFFFFB9H = FFFFFFFFH - 70
FirstRunAfter: 50
TimeBetweenRuns: 5
PreCachedProcs: No
Recovery: Direct
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFFBDH FFFFFFC2H FFFFFFD3H 17 FFFFFFD4H 6 23
1 T FFFFFFEFH FFFFFFEFH 1 FFFFFFF0H FFFFFFFBH 00000026H 43 00000027H 12 55
2 R FFFFFFF4H 00000000H 15 00000003H 00000004H 00000026H 34 00000026H 1 35
3 R FFFFFFF9H 00000004H 12 00000005H 00000005H 00000025H 32 00000026H 1 33
4 R FFFFFFFEH 00000006H 8 00000006H 00000007H 00000023H 28 00000023H 1 29
5 R 00000003H 0000000AH 9 0000000CH 00000017H 00000021H 10 00000021H 11 21
6 R 00000008H 00000019H 18 0000001AH 0000001AH 00000021H 7 00000021H 0 7
7 R 0000000DH 0000001BH 14 0000001BH 0000001CH 00000021H 5 00000021H 1 6
8 R 00000012H 0000001CH 11 0000001DH 0000001EH 1 1
Observations:
- as with retiming, all handlers are executed with recovery
rm = R
(forrun 1
see above) - compared to retiming, we observe smaller delays
rdel
, since withAlarms.ArmDirect
the handlers are executed directly from code as soon as we detect an alarm time before the current time, while withAlarms.ArmRetime
we need to add some safety margin to the current time in order to allow the alarm arming code to execute - however, check out some of the times:
run 8
ends beforerun 1
- in fact, all run times
run-beg[2]
tortm[2]
,run-beg[3]
tortm[3]
, etc. are within the arming timearm[1]
toarmed[1]
- that is, all recovered handlers are actually executed by the alarm-triggered handler of
run 1
- this is also reflected in
rtm[1] = 55
- compare to the retiming case, where all handlers are executed triggered by an alarm, in sequence
- hence, depending on the priority of the alarm handlers, the long uninterrupted run time with the direct recovery case, doing direct execution, could prevent other interrupts from being triggered; this is not the case for alarm retiming, which can result in better responsiveness
With Pre-caching AlarmEval.PreCachedProcs = TRUE
StartTime: FFFFFFB9H = FFFFFFFFH - 70
FirstRunAfter: 50
TimeBetweenRuns: 5
PreCachedProcs: Yes (131)
Recovery: Retime
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFFBCH FFFFFFBCH FFFFFFBDH 1 FFFFFFBDH 0 1
1 T FFFFFFEEH FFFFFFEEH 0 FFFFFFEEH FFFFFFEFH FFFFFFF0H 1 FFFFFFF0H 1 2
2 T FFFFFFF3H FFFFFFF3H 0 FFFFFFF3H FFFFFFF3H FFFFFFF4H 1 FFFFFFF4H 0 1
3 T FFFFFFF8H FFFFFFF8H 0 FFFFFFF8H FFFFFFF8H FFFFFFF9H 1 FFFFFFF9H 0 1
4 T FFFFFFFDH FFFFFFFDH 0 FFFFFFFDH FFFFFFFDH FFFFFFFEH 1 FFFFFFFEH 0 1
5 T 00000002H 00000002H 0 00000002H 00000003H 00000004H 1 00000004H 1 2
6 T 00000007H 00000007H 0 00000007H 00000007H 00000008H 1 00000008H 0 1
7 T 0000000CH 0000000CH 0 0000000CH 0000000CH 0000000DH 1 0000000DH 0 1
8 T 00000011H 00000011H 0 00000011H 00000011H 0 0
Observations: with pre-caching all relevant procedures (see AlarmEval.preCache
), the seemingly hopeless case with TimeBetweenRuns = 5
becomes feasible: no recovery needed.
Summary So Far
Both recovery methods – retiming and direct execution – achieve the objective to run all handlers even if the alarm times between the handlers is smaller than their run times. Or the other way round, should a handler run for too long, for whatever reason, the alarm system will cope with this error situation.
Procedure pre-caching works well, as the last test case demonstrates.
Decreasing Alarm Set-up Time: AlarmEval.FirstRunAfter = 5
Up to now, we have kept the first alarm time (run 1
) sufficiently far into the future, so that this first handler never required recovery. Let’s change that now.
Let’s be bold and go right down to FirstRunAfter = 5
.
But first, as a reminder, if we keep all test parameter times outside any recovery:
StartTime: 0FFFFFFFFH - 100
FirstRunAfter: 25
TimeBetweenRuns: 25
PreCachedProcs: No
Recovery: Retime
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFFA0H FFFFFFA5H FFFFFFB5H 16 FFFFFFB6H 6 22
1 T FFFFFFB9H FFFFFFB9H 2 FFFFFFBBH FFFFFFC5H FFFFFFCDH 8 FFFFFFCEH 11 19
2 T FFFFFFD2H FFFFFFD2H 0 FFFFFFD2H FFFFFFD2H FFFFFFD3H 1 FFFFFFD3H 0 1
3 T FFFFFFEBH FFFFFFEBH 0 FFFFFFEBH FFFFFFEBH FFFFFFECH 1 FFFFFFECH 0 1
4 T 00000004H 00000004H 0 00000004H 00000005H 00000009H 4 00000009H 1 5
5 T 0000001DH 0000001DH 1 0000001EH 00000029H 0000002CH 3 0000002DH 12 15
6 T 00000036H 00000036H 0 00000036H 00000036H 00000037H 1 00000037H 0 1
7 T 0000004FH 0000004FH 0 0000004FH 0000004FH 00000050H 1 00000050H 0 1
8 T 00000068H 00000068H 0 00000068H 00000068H 0 0
Run 0
, representing the set-up procedure AlarmEval.start
, has a run time of 22 us. Hence, reducing FirstRunAfter
below this value should result in recovery.
With AlarmEval.Recovery = RecoveryRetime
StartTime: FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter: 5
TimeBetweenRuns: 25
PreCachedProcs: No
Recovery: Retime
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFFA0H FFFFFFA5H FFFFFFCCH 39 FFFFFFCFH 8 47
1 R FFFFFFA5H FFFFFFB5H 17 FFFFFFB6H FFFFFFC1H FFFFFFC9H 8 FFFFFFCAH 12 20
2 R FFFFFFBEH FFFFFFCDH 15 FFFFFFCDH FFFFFFCDH FFFFFFCEH 1 FFFFFFCEH 0 1
3 T FFFFFFD7H FFFFFFD7H 0 FFFFFFD7H FFFFFFD7H FFFFFFD8H 1 FFFFFFD8H 0 1
4 T FFFFFFF0H FFFFFFF0H 0 FFFFFFF0H FFFFFFF1H FFFFFFF5H 4 FFFFFFF5H 1 5
5 T 00000009H 00000009H 1 0000000AH 00000015H 00000018H 3 00000019H 12 15
6 T 00000022H 00000022H 0 00000022H 00000022H 00000023H 1 00000023H 0 1
7 T 0000003BH 0000003BH 0 0000003BH 0000003BH 0000003CH 1 0000003CH 0 1
8 T 00000054H 00000054H 0 00000054H 00000054H 0 0
Observations:
run 1
andrun 2
run with recovery, then the handler chain has caught up, not least thanks to the caching effects- compare the run time
rtm[0]
in this error case with the case above: 47 us vs. 22 us - inspecting the times reveals:
al-run[1]
andal-run[2]
are smaller thanrun-end[0]
, ie. the first two alarm interrupts fired beforerun 0
had finished:Alarms.start
runs in thread mode, hence the two recovered and thus delayed alarm interrupt handlers ofrun 1
andrun 2
pre-emptedrun 0
, resulting in the longrtm[0]
With AlarmEval.Recovery = RecoveryDirect
StartTime: FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter: 5
TimeBetweenRuns: 25
PreCachedProcs: No
Recovery: Direct
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFF9FH FFFFFFA5H FFFFFFD0H 43 FFFFFFD0H 6 49
1 R FFFFFFA4H FFFFFFAEH 16 FFFFFFB4H FFFFFFBEH FFFFFFCDH 15 FFFFFFCDH 10 25
2 R FFFFFFBDH FFFFFFC4H 7 FFFFFFC4H FFFFFFC5H FFFFFFCBH 6 FFFFFFCCH 2 8
3 T FFFFFFD6H FFFFFFD6H 0 FFFFFFD6H FFFFFFD7H FFFFFFD8H 1 FFFFFFD8H 1 2
4 T FFFFFFEFH FFFFFFEFH 0 FFFFFFEFH FFFFFFF0H FFFFFFF4H 4 FFFFFFF4H 1 5
5 T 00000008H 00000008H 1 00000009H 00000014H 00000017H 3 00000018H 12 15
6 T 00000021H 00000021H 0 00000021H 00000021H 00000022H 1 00000022H 0 1
7 T 0000003AH 0000003AH 0 0000003AH 0000003AH 0000003BH 1 0000003BH 0 1
8 T 00000053H 00000053H 0 00000053H 00000053H 0 0
Observations: same behaviour as with retiming, with slightly less run delays rdel[1]
and rdel[2]
for the same reason outlined above.
With Pre-caching AlarmEval.PreCachedProcs = TRUE
StartTime: FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter: 5
TimeBetweenRuns: 25
PreCachedProcs: Yes (131)
Recovery: Retime
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFF9EH FFFFFF9EH FFFFFF9FH 1 FFFFFF9FH 0 1
1 T FFFFFFA3H FFFFFFA3H 1 FFFFFFA4H FFFFFFA5H FFFFFFA6H 1 FFFFFFA6H 1 2
2 T FFFFFFBCH FFFFFFBCH 0 FFFFFFBCH FFFFFFBCH FFFFFFBDH 1 FFFFFFBDH 0 1
3 T FFFFFFD5H FFFFFFD5H 0 FFFFFFD5H FFFFFFD5H FFFFFFD6H 1 FFFFFFD6H 0 1
4 T FFFFFFEEH FFFFFFEEH 0 FFFFFFEEH FFFFFFEEH FFFFFFEFH 1 FFFFFFEFH 0 1
5 T 00000007H 00000007H 0 00000007H 00000008H 0000000AH 2 0000000AH 1 3
6 T 00000020H 00000020H 0 00000020H 00000020H 00000021H 1 00000021H 0 1
7 T 00000039H 00000039H 0 00000039H 00000039H 0000003AH 1 0000003AH 0 1
8 T 00000052H 00000052H 0 00000052H 00000052H 0 0
Observations: when running with pre-caching, all alarms are on time.
Faulty “Impossible” Values
To conclude, let’s run with clearly faulty values: FirstRunAfter = -5
and TimeBetweenRuns = -5
, without pre-caching.
StartTime: FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter: -5
TimeBetweenRuns: -5
PreCachedProcs: No
Recovery: Retime
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFFA0H FFFFFFA5H FFFFFFCDH 40 FFFFFFCEH 6 46
1 R FFFFFF9BH FFFFFFB6H 29 FFFFFFB8H FFFFFFC2H FFFFFFCAH 8 FFFFFFCBH 11 19
2 R FFFFFF96H FFFFFFCFH 57 FFFFFFCFH FFFFFFCFH FFFFFFD1H 2 FFFFFFD1H 0 2
3 R FFFFFF91H FFFFFFD7H 70 FFFFFFD7H FFFFFFD7H FFFFFFD9H 2 FFFFFFD9H 0 2
4 R FFFFFF8CH FFFFFFDFH 83 FFFFFFDFH FFFFFFE0H FFFFFFE4H 4 FFFFFFE5H 2 6
5 R FFFFFF87H FFFFFFEAH 100 FFFFFFEBH FFFFFFF6H FFFFFFFAH 4 FFFFFFFBH 12 16
6 R FFFFFF82H 00000000H 126 00000000H 00000000H 00000001H 1 00000001H 0 1
7 R FFFFFF7DH 00000008H 139 00000008H 00000008H 00000009H 1 00000009H 0 1
8 R FFFFFF78H 00000010H 152 00000010H 00000010H 0 0
Observations: of course, all runs need to be recovered, but they are executed despite the anomalous values, which could be the result of a faulty calculation, or disturbed sensor reading. I think keeping things running under error conditions is important for a control system. The actual alarm or direct execution times are reported back, so the thread can check and decide if the handler’s results can be trusted.
Bottom Line
Overview
Module Alarms
as presented and tested is a useful approach to microseconds timing of one-shot tasks. With its device concept, with little overhead, it requires the programmer to carefully design the control program, but that’s not different from other peripherals such as UARTs or SPIs. Each alarm and its data are separate entities, both regarding data and hardware.
Recovery
While the recovery methods implemented, and tested here, are not fundamentally necessary, they provide a useful safety net for control programs. Depending on the overall timing requirements of a control program, in particular when using different interrupts and microseconds timing of tasks (or handlers), their run times could get alarm times into the past compared to the current time. Not each and every state of a system can be predicted and tested when it comes to timing. (Trying to) Keeping the system going in any case is a good trait of a control program. Since the actual alarm or direct execution times are reported back, the client module (or thread) can always check and decide if the handler’s results can be trusted.
Pre-caching
Installing time-critical handler code in SRAM is always a possibility. As explained in CodeLoading
, it requires specific coding if procedures are called from the handler,1 but the resulting performance is good. SRAM-based code is installed once and for all, it cannot be evicted from the cache, we know it’s available.
As an alternative, as presented above, pre-caching works well, too. The performance is good, comparable to loading into SRAM. We just need to find the microseconds inside the control program to load the time-critical procedures into the cache, sufficiently close to the time they are used. Depending on the frequency of their use, pre-cached procedures will remain in the cache “automatically”, or need to be pre-cached again. There’s simply the general uncertainty with the flash memory cache that we cannot know for sure if a certain slice of code is stored there.
The advantage of pre-caching compared to loading into SRAM is that there are no specific code changes necessary, and a client module can even pre-cache a provider module’s procedures, unbeknownst to the latter, if they are accessible via API. With the current implementation of catching and reporting run-time errors (RuntimeErrors.mod
), we also get correct error messages regarding addresses, and correct stack traces, something that’s lost (for now) when loading a procedure into SRAM.
The Library Module Alarms
For the library module Alarms, I have decided to simplify and cut complexity, both conceptual and technical. Meaning, it always uses recovery, and in particular retiming. It’s the conceptually more consistent recovery variant. You can find a corresponding example and test program here.
Module Alarms
as presented and tested here will remain to be available in the example program directory.
The Task System
For the time being, I will not put any more efforts into the task system.
Lessons Learned
As an aside, what I have learned is that, when running from flash memory, ie. executing code for the first time, even a single SYSTEM.PUT
can add “substantial” run time when single microseconds count – it uses 400 nanoseconds per load, and a typical SYSTEM.PUT
comes at three instructions. Difficult to add testing and debugging code in time-critical sections.
The serial off-chip flash memory allows to design different memory configurations on the board, with the same MCU. The Execute in Place (XIO) functionality and the caching mechanism makes this transparent in most cases. But we have to be aware of the impact on code loading and thus execution times in specific cases.
Calling an Interrupt Handler from Code?
If a procedure is compiled as interrupt handler, can it be called as any other procedure?
An interrupt handler is basically a parameterless procedure, marked using [0]
(or any other constant value) for the corresponding compilation.
A “normal” parameterless procedure compiles thusly:
PROCEDURE proc;
END proc;
. 8 08H 0B500H push { lr }
. 10 0AH 0BD00H pop { pc }
An interrupt handler compiles thusly (simplest version):
PROCEDURE handler[0];
END handler;
. 12 0CH 0B500H push { lr }
. 14 0EH 0BD00H pop { pc }
No difference here so far. Of course, the meaning of these operations is a bit different when executed as interrupt handler. The hardware will set the link register to a specific EXC_RETURN
value when entering the interrupt handling. This value will be pushed onto the stack, and then when exiting the handler popped into the program counter. The hardware detects the special EXC_RETURN
value, which causes the hardware to reverse the stacking of registers r0 to r3 and the other registers pushed there upon entry, among them the program counter, so the execution continues after the instruction where the interrupt occurred.2
However, when called from code, in lieu of the interrupt hardware, this simple interrupt handler will behave exactly as a “normal” procedure. The link register will contain the return address as put there by the BL
instruction. The prologue pushes the link register onto the stack, and the epilogue pops it into the program counter, and the execution continues after the BL
instruction.
Note that in Oberon it’s the responsibility of the calling procedure to save all relevant registers before the call, so the called procedure can use them freely.3
The compilation as handler will show as soon as the handler code modifies registers r4 to r7, in which case these registers are pushed onto the stack flexibly as needed. Remember that registers r0 to r3 are pushed onto the stack by the hardware upon exception entry.
To exemplify, in case the interrupt handler calls a procedure, we get (stack trace disabled):
PROCEDURE test;
END test;
. 4 04H 0B500H push { lr }
. 6 06H 0BD00H pop { pc }
PROCEDURE handler[0];
BEGIN
. 16 010H 0B5F0H push { r4, r5, r6, r7, lr }
test
END handler;
. 18 012H 0F7FFFFF7H bl.w -18 -> 4
. 22 016H 0BDF0H pop { r4, r5, r6, r7, pc }
Since the compiler cannot “know” which registers will be mutated by the called procedure, and possibly any procedures further down the chain, the prologue pushes all registers r4 to r7 onto the stack, and the epilogue restores them upon exit.
Again, when called from code, the handler procedure will execute normally, if with the overhead of unnecessarily pushing r4 to r7, which does no logical harm though, just extends the run-time of the prologue and epilogue.
Conclusion: an interrupt handler can basically be called as any other procedure, allowing for the option of the direct call in case an alarm time is before the current time.
Of course, care must be given to the side effects of the handler, in particular on the hardware, for example when de-asserting an interrupt from a device. With the alarm device it’s OK to de-assert an interrupt any time, even if it wasn’t even asserted in the first place. But this might not be true for other hardware.
Output Terminal
See Set-up, one-terminal set-up.
Build and Run
Build module AlarmEval
with Astrobe, and create and upload the UF2 file using abin2uf2
.
Set Astrobe’s memory options as listed, and the library search path as explained.
Repository
- AlarmEval.mod
- Alarms.mod, the module in the example directory
- Alarms.mod, the library module
-
At least with the current version of Astrobe, which does not yet “officially” support the RP2040, so I can blame only myself here for being impatient. Maybe the official release will provide SRAM-loading and execution of code in a better way. ↩︎
-
Things can be a bit more complex if a load-multiple or store-multiple instruction such as
push
was interrupted: it will be replayed from the start. ↩︎ -
If I recall correctly, in C this is the other way round. ↩︎