AlarmEval

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:

  1. 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
  2. retime: adjust the alarm time to the future from the point where the arming takes place, or
  3. 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 and run 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 or Direct, 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 for rm[0], al-arm[0], and al-run[0]; the last run does not arm any further alarm, so there are not values for arm[8], armed[8], and atm[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 and run-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 handler p0 in run 1, and 15 us for the second one p1 in run 5, and
    • just of the handler code outside module Alarms in htm;
  • 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 for run 1 and 15 us for run 5, and their spillover effects
  • inspect Alarms.ArmRetime: al-run[2] must be in the range of arm[1] to armed[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 for run 1, run 6 by the handler for run 5
  • inspect Alarms.ArmDirect: the time from run-beg[2] to run-end[2] should be inside the time from arm[1] and armed[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 from run 1, which is armed by the test start-up code after FirstRunAfter = 50, ie. there’s no impact of TimeBetweenRuns
  • there is a noticeable delay rdel, which is caused by the run times rtm[1] and rtm[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 (for run 1 see above)
  • compared to retiming, we observe smaller delays rdel, since with Alarms.ArmDirect the handlers are executed directly from code as soon as we detect an alarm time before the current time, while with Alarms.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 before run 1
    • in fact, all run times run-beg[2] to rtm[2], run-beg[3] to rtm[3], etc. are within the arming time arm[1] to armed[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 and run 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] and al-run[2] are smaller than run-end[0], ie. the first two alarm interrupts fired before run 0 had finished: Alarms.start runs in thread mode, hence the two recovered and thus delayed alarm interrupt handlers of run 1 and run 2 pre-empted run 0, resulting in the long rtm[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


  1. 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. ↩︎

  2. 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. ↩︎

  3. If I recall correctly, in C this is the other way round. ↩︎