Introduction
With example program AlarmEval we had evaluated module Alarms
for different timing and recovery test cases. AlarmEval
uses a module Alarms
that provides additional features for timing recovery. This module can be found in the example program directory.
This example program AlarmTest
re-runs a few of the same tests relevant to the library module Alarms
. The library module used here focuses on the “retiming” recovery method. See AlarmEval, section Module Alarms, option 2, in sub-section Recovery: Alarm Times in the Past.
Some of the text below is adopted and adapted from AlarmEval.
The test results were gained by re-running the tests with module Alarms
after porting it to libv2
, on the RP2040/Pico. While the results with the RP2350/Pico2 show different numbers, due to the MCU’s better performance, the basic behaviour is the same.
Module Alarms
Alarm = Device
In module Alarms
, each of the four alarms per timer 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 a run-queue. 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.
In Alarms.mod, this error situation is handled by adjusting the alarm time to the future from the point where the arming takes place (retiming).
Time Arithmetic
See Timers and Timing.
A relevant takeaway is that the maximum alarm time can be the current time plus 2^31 - 1 microseconds, ie. about 35 minutes.
The Example Program
Structure
The program module AlarmTest
creates a series of alarms, which are serviced by two interrupt handlers, AlarmTest.p0
and AlarmTest.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, AlarmTest.p3
, which is armed to be activated by another alarm device after the runs. The test sequence is initiated by AlarmTest.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 (test parameter
AlarmTest.TimeBetweenRuns
), or - for set-up times, ie. how closely to the current time an alarm can be set (test parameter
AlarmTest.FirstRunAfter
).
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 nonetheless allow some generalised observations and conclusions.
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 used global timer close to 0FFFFFFFFH
to evaluate the roll-over. In a control program, the timers should not be changed thusly, in order to get a uniform time basis for all threads, or parts of a monolithic “big loop” program. The timers are 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.
Pre-caching is selected via test parameter AlarmTest.PreCacheProcs
.
Timing Baseline
The first runs establish a timing baseline, using “unproblematic” set-up values, and allow to introduce the output structure. I know these unproblematic values as I have run the tests before. :)
No Pre-caching
StartTime: FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter: 50
TimeBetweenRuns: 25
PreCachedProcs: No
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFF9FH FFFFFFA4H FFFFFFB0H 12 FFFFFFB0H 5 17
1 T FFFFFFD1H FFFFFFD1H 1 FFFFFFD2H FFFFFFDEH FFFFFFE6H 8 FFFFFFE8H 14 22
2 T FFFFFFEAH FFFFFFEAH 0 FFFFFFEAH FFFFFFEAH FFFFFFEBH 1 FFFFFFEBH 0 1
3 T 00000003H 00000003H 0 00000003H 00000003H 00000004H 1 00000004H 0 1
4 T 0000001CH 0000001CH 0 0000001CH 0000001DH 00000021H 4 00000022H 2 6
5 T 00000035H 00000035H 1 00000036H 00000042H 00000046H 4 00000047H 13 17
6 T 0000004EH 0000004EH 0 0000004EH 0000004EH 0000004FH 1 0000004FH 0 1
7 T 00000067H 00000067H 0 00000067H 00000067H 00000068H 1 00000068H 0 1
8 T 00000080H 00000080H 0 00000080H 00000080H 0 0
The first lines list the test parameters:
- 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);
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, ie. with the intended alarm time,R
the handler was run using retiming error recovery;
- al-arm: the intended alarm time, ie. for when the alarm should be armed;
- al-run: the actual, possibly adjusted alarm time after retiming;
- 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
; - 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: 22 us for the first handlerp0
inrun 1
, and 17 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 Pre-caching
StartTime: FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter: 50
TimeBetweenRuns: 25
PreCachedProcs: Yes (737)
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFF9DH FFFFFF9EH FFFFFFA1H 3 FFFFFFA1H 1 4
1 T FFFFFFCFH FFFFFFCFH 0 FFFFFFCFH FFFFFFCFH FFFFFFD1H 2 FFFFFFD1H 0 2
2 T FFFFFFE8H FFFFFFE8H 0 FFFFFFE8H FFFFFFE8H FFFFFFE9H 1 FFFFFFE9H 0 1
3 T 00000001H 00000001H 0 00000001H 00000001H 00000002H 1 00000002H 0 1
4 T 0000001AH 0000001AH 0 0000001AH 0000001AH 0000001CH 2 0000001CH 0 2
5 T 00000033H 00000033H 0 00000033H 00000035H 00000036H 1 00000036H 2 3
6 T 0000004CH 0000004CH 0 0000004CH 0000004CH 0000004DH 1 0000004DH 0 1
7 T 00000065H 00000065H 0 00000065H 00000065H 00000066H 1 00000066H 0 1
8 T 0000007EH 0000007EH 0 0000007EH 0000007EH 0 0
Observation: pre-caching works well. It takes 737 us to load the code into the flash memory cache. This is substantially longer than it took before making the change in procedure MemoryExt.CacheProc
to use the meta data in lieu of scanning the memory (see change note; it was below 150 us before). But memory scanning can lead to false positives, which the meta data lookup method cannot.
Decreasing Consecutive Alarm Times: AlarmTest.TimeBetweenRuns = 15
No Pre-caching
StartTime: FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter: 50
TimeBetweenRuns: 15
PreCachedProcs: No
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFF9FH FFFFFFA4H FFFFFFB0H 12 FFFFFFB1H 6 18
1 T FFFFFFD1H FFFFFFD1H 1 FFFFFFD2H FFFFFFDEH FFFFFFE6H 8 FFFFFFE8H 14 22
2 R FFFFFFE0H FFFFFFEAH 10 FFFFFFEAH FFFFFFEAH FFFFFFEBH 1 FFFFFFECH 1 2
3 R FFFFFFEFH FFFFFFF2H 3 FFFFFFF2H FFFFFFF2H FFFFFFF3H 1 FFFFFFF3H 0 1
4 T FFFFFFFEH FFFFFFFEH 0 FFFFFFFEH FFFFFFFFH 00000003H 4 00000004H 2 6
5 T 0000000DH 0000000DH 1 0000000EH 0000001AH 0000001EH 4 0000001FH 13 17
6 R 0000001CH 00000024H 8 00000024H 00000024H 00000025H 1 00000026H 1 2
7 R 0000002BH 0000002CH 1 0000002CH 0000002CH 0000002DH 1 0000002DH 0 1
8 T 0000003AH 0000003AH 0 0000003AH 0000003AH 0 0
Observations: with AlarmTest.TimeBetweenRuns = 15
being below the run-time of the uncached handlers, we now see retiming taking place, before caching starts to save the day.
With Pre-caching
StartTime: FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter: 50
TimeBetweenRuns: 15
PreCachedProcs: Yes (737)
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFF9CH FFFFFF9DH FFFFFFA0H 3 FFFFFFA0H 1 4
1 T FFFFFFCEH FFFFFFCEH 0 FFFFFFCEH FFFFFFCEH FFFFFFD0H 2 FFFFFFD0H 0 2
2 T FFFFFFDDH FFFFFFDDH 0 FFFFFFDDH FFFFFFDDH FFFFFFDEH 1 FFFFFFDEH 0 1
3 T FFFFFFECH FFFFFFECH 0 FFFFFFECH FFFFFFECH FFFFFFEDH 1 FFFFFFEDH 0 1
4 T FFFFFFFBH FFFFFFFBH 0 FFFFFFFBH FFFFFFFBH FFFFFFFDH 2 FFFFFFFDH 0 2
5 T 0000000AH 0000000AH 0 0000000AH 0000000CH 0000000DH 1 0000000DH 2 3
6 T 00000019H 00000019H 0 00000019H 00000019H 0000001AH 1 0000001AH 0 1
7 T 00000028H 00000028H 0 00000028H 00000028H 00000029H 1 00000029H 0 1
8 T 00000037H 00000037H 0 00000037H 00000037H 0 0
Observations: Or call on pre-caching to save the day. :)
Further Decreasing Consecutive Alarm Times: AlarmTest.TimeBetweenRuns = 5
No Pre-caching
StartTime: FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter: 50
TimeBetweenRuns: 5
PreCachedProcs: No
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFF9EH FFFFFFA3H FFFFFFAFH 12 FFFFFFB0H 6 18
1 T FFFFFFD0H FFFFFFD0H 1 FFFFFFD1H FFFFFFDDH FFFFFFE5H 8 FFFFFFE7H 14 22
2 R FFFFFFD5H FFFFFFE9H 20 FFFFFFE9H FFFFFFE9H FFFFFFEAH 1 FFFFFFEBH 1 2
3 R FFFFFFDAH FFFFFFF1H 23 FFFFFFF1H FFFFFFF1H FFFFFFF2H 1 FFFFFFF3H 1 2
4 R FFFFFFDFH FFFFFFF9H 26 FFFFFFF9H FFFFFFFAH FFFFFFFEH 4 FFFFFFFFH 2 6
5 R FFFFFFE4H 00000004H 33 00000005H 00000011H 00000015H 4 00000016H 13 17
6 R FFFFFFE9H 0000001BH 50 0000001BH 0000001BH 0000001CH 1 0000001DH 1 2
7 R FFFFFFEEH 00000023H 53 00000023H 00000023H 00000024H 1 00000025H 1 2
8 R FFFFFFF3H 0000002BH 56 0000002BH 0000002BH 0 0
Observations: with AlarmTest.TimeBetweenRuns = 5
, caching cannot catch up, we have retiming from top to bottom.
With Pre-caching
StartTime: FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter: 50
TimeBetweenRuns: 5
PreCachedProcs: Yes (737)
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFF9DH FFFFFF9EH FFFFFFA0H 2 FFFFFFA0H 1 3
1 T FFFFFFCFH FFFFFFCFH 0 FFFFFFCFH FFFFFFCFH FFFFFFD1H 2 FFFFFFD1H 0 2
2 T FFFFFFD4H FFFFFFD4H 0 FFFFFFD4H FFFFFFD4H FFFFFFD5H 1 FFFFFFD5H 0 1
3 T FFFFFFD9H FFFFFFD9H 0 FFFFFFD9H FFFFFFD9H FFFFFFDAH 1 FFFFFFDAH 0 1
4 T FFFFFFDEH FFFFFFDEH 0 FFFFFFDEH FFFFFFDEH FFFFFFE0H 2 FFFFFFE0H 0 2
5 T FFFFFFE3H FFFFFFE3H 0 FFFFFFE3H FFFFFFE5H FFFFFFE6H 1 FFFFFFE6H 2 3
6 T FFFFFFE8H FFFFFFE8H 0 FFFFFFE8H FFFFFFE8H FFFFFFE9H 1 FFFFFFE9H 0 1
7 T FFFFFFEDH FFFFFFEDH 0 FFFFFFEDH FFFFFFEDH FFFFFFEEH 1 FFFFFFEEH 0 1
8 T FFFFFFF2H FFFFFFF2H 0 FFFFFFF2H FFFFFFF2H 0 0
Observations: even with AlarmTest.TimeBetweenRuns = 5
, pre-caching however works again, no retiming is taking place.
Decreasing Alarm Set-up Time: AlarmTest.FirstRunAfter = 5
Now switching to testing set-up times.
No Pre-caching
StartTime: FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter: 5
TimeBetweenRuns: 25
PreCachedProcs: No
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFF9EH FFFFFFA3H FFFFFFC7H 36 FFFFFFC8H 6 42
1 R FFFFFFA3H FFFFFFB0H 15 FFFFFFB2H FFFFFFBEH FFFFFFC4H 6 FFFFFFC6H 14 20
2 R FFFFFFBCH FFFFFFC9H 13 FFFFFFC9H FFFFFFCAH FFFFFFCBH 1 FFFFFFCBH 1 2
3 T FFFFFFD5H FFFFFFD5H 0 FFFFFFD5H FFFFFFD5H FFFFFFD6H 1 FFFFFFD6H 0 1
4 T FFFFFFEEH FFFFFFEEH 0 FFFFFFEEH FFFFFFEFH FFFFFFF3H 4 FFFFFFF4H 2 6
5 T 00000007H 00000007H 1 00000008H 00000014H 00000018H 4 00000019H 13 17
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: with AlarmTest.FirstRunAfter = 5
we get retiming for the first two runs.
With Pre-caching
StartTime: FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter: 5
TimeBetweenRuns: 25
PreCachedProcs: Yes (737)
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFF9DH FFFFFF9EH FFFFFFA1H 3 FFFFFFA1H 1 4
1 T FFFFFFA2H FFFFFFA2H 0 FFFFFFA2H FFFFFFA3H FFFFFFA4H 1 FFFFFFA4H 1 2
2 T FFFFFFBBH FFFFFFBBH 0 FFFFFFBBH FFFFFFBBH FFFFFFBCH 1 FFFFFFBCH 0 1
3 T FFFFFFD4H FFFFFFD4H 0 FFFFFFD4H FFFFFFD4H FFFFFFD5H 1 FFFFFFD5H 0 1
4 T FFFFFFEDH FFFFFFEDH 0 FFFFFFEDH FFFFFFEDH FFFFFFEFH 2 FFFFFFEFH 0 2
5 T 00000006H 00000006H 0 00000006H 00000008H 00000009H 1 00000009H 2 3
6 T 0000001FH 0000001FH 0 0000001FH 0000001FH 00000020H 1 00000020H 0 1
7 T 00000038H 00000038H 0 00000038H 00000038H 00000039H 1 00000039H 0 1
8 T 00000051H 00000051H 0 00000051H 00000051H 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
run rm al-arm al-run rdel run-beg arm armed atm run-end htm rtm
0 FFFFFFA0H FFFFFFA5H FFFFFFC9H 36 FFFFFFCAH 6 42
1 R FFFFFF9BH FFFFFFB2H 24 FFFFFFB3H FFFFFFBFH FFFFFFC6H 7 FFFFFFC8H 14 21
2 R FFFFFF96H FFFFFFCBH 53 FFFFFFCBH FFFFFFCBH FFFFFFCCH 1 FFFFFFCDH 1 2
3 R FFFFFF91H FFFFFFD3H 66 FFFFFFD3H FFFFFFD4H FFFFFFD5H 1 FFFFFFD5H 1 2
4 R FFFFFF8CH FFFFFFDBH 79 FFFFFFDBH FFFFFFDCH FFFFFFE1H 5 FFFFFFE1H 1 6
5 R FFFFFF87H FFFFFFE6H 96 FFFFFFE7H FFFFFFF3H FFFFFFF8H 5 FFFFFFF8H 12 17
6 R FFFFFF82H FFFFFFFDH 123 FFFFFFFDH FFFFFFFDH FFFFFFFEH 1 FFFFFFFFH 1 2
7 R FFFFFF7DH 00000005H 136 00000005H 00000005H 00000006H 1 00000007H 1 2
8 R FFFFFF78H 0000000DH 149 0000000DH 0000000DH 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. Keeping things running under error conditions is important for a control system. The actual alarm 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 retiming error recovery method implemented, and tested here, is not fundamentally necessary, it provides 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 push 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. Better shutting that RCS thruster off a few microseconds too late than leave it burning. Since the actual alarm 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
, without any support by the compiler, it requires specific coding if procedures are called from the handler, 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.1
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 or milliseconds 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 (as done in the test program here). 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.
A Lesson Learned Aside
As an aside, what I have learned is that, when running directly 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 about 400 nanoseconds per load, and a typical SYSTEM.PUT
comes at three instructions. It can be difficult to add testing and debugging code in time-critical sections, even only for toggling a LED.
The serial off-chip flash memory allows to design different memory configurations on the board, with the same MCU. The Execute in Place (XIP) 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.
Output Terminals
See Set-up, one-terminal set-up.
Build and Run
Repository
- libv2:
- for RP2040/Pico and RP2350/Pico2: AlarmTest.mod
- for RP2040/Pico and RP2350/Pico2: Alarms.mod
- lib (v1): this version is no longer being maintained: AlarmTest
-
Cache line pinning with the RP2350 needs evaluation as an alternative. ↩︎