TaskEval

Introduction

This example program TaskEval introduces the task system with microseconds (us) timing mentioned as the motivation for the CodeLoading test program. Module Tasks is provided together with the example program, not yet in the source library, as it’s a version that is instrumented with test code that will be removed from the library version.

The purpose of this document and the test program are:

  • introduce the task system concepts
  • outline the timing arithmetic used, including the challenges of doing 32-bit unsigned integer calculations and comparisons using signed INTEGERs
  • describe different runs and results of the test program
    • demonstrate the correct timing arithmetic
    • explore the minimum time between tasks
    • explore the minimum set-up time for a task
  • experimental: explore a way to force a procedure to run from flash cache

Task System Basics

Refer to module Tasks.mod in the example directory. As said, it’s an unwieldy version heavily instrumented with test code.

Each core gets allocated one of the timer alarms (see below). A control program can schedule a Task to be executed at a certain time in the future, specified in microseconds. For each core, and timer alarm, more than one such Task can be scheduled, eg. to start a measurement in 100 us and to stop it after 150 us. All scheduled tasks are held in a linked list, ordered by alarm trigger time (run-queue).

When the currently active alarm is triggered, the corresponding Task will be executed, and the alarm time of next Task in the run-queue will be written to the corresponding timer alarm register, which is called arming. Care must be taken to not arm an alarm with a timer value in the past, else it will take about a full “rotation” of the 32-bit register until it’s triggered, ie. more than 71 minutes, not the intended few microseconds. This could happen if the Task procedure takes longer to execute than the alarm time for the next Task in the list allows. That is, the two Task were scheduled to close to each other. In the task system, in this case the next Task in the list will be executed immediately (directly). Better execute a Task with some delay than not execute it. Obviously, this situation must only be an exceptional case, not by program design. It’s a band-aid for an error situation.

The same care about not inadvertently setting an alarm with a time in the past must be taken when putting a Task in the run-queue. In the task system, the same solution is applied: execute the Task immediately, without even setting an alarm for it. Again, this must be an exceptional case, to resolve an error situation.

The task system as introduced here cannot control how long a scheduled Task will take to execute, that’s the responsibility of the control program designer. But we can look at the basic overhead of the above scheduling and execution mechanisms, which we do with this test program. The other purpose is to check the alarm timing behaviour around the 32-bit roll-over and 2-complement overflow points, explained below.

Timer and Timing

Refer to document Timer and Timing.

The Example Program

The example program consist of

  • TaskEval.mod, the main program module
  • Tasks.mod, the work-in-progress library module, instrumented for testing

TaskEval creates a series of eight Task and schedules them for execution. The Task are scheduled to run in the order 0 to 7, in equidistant time intervals. However, in the first test, they are not put on the run-queue in this order, but as 2, 4, 6, 1, 3, 5, 7, 0, so we can check the correct insertion and ordering in the run-queue. For the timing tests, the tasks will be put on the run-queue in their intended execution time, in order to avoid side effects by already firing alarms while still at work with scheduling.

After the eight test tasks, an additional one is scheduled to print the resulting data collected during the test runs. Obviously, we cannot do any serial text output during timing runs in a microseconds range, as this would add milliseconds.

Collecting the data adds some additional run time, so the results with the “final” non-instrumented module Tasks should then be somewhat better.

For testing purposes, we set the global timer close to 0FFFFFFFFH to evaluate the roll-over.

Now, finally, let’s run the example program for different test cases.

Task Scheduling: Timing Comparisons (Run-queue Ordering)

As stored in the repo, the test code is configured for this test case. As outlined above, it puts the Task in the order 2, 4, 6, 1, 3, 5, 7, 0 on the run-queue, and we can check if the insertions in the queue work, ie. the timing comparison using INTEGER, but actually with CARDINAL values.

The initial test settings are, in case you want to restore the initial conditions:

  • In module TaskEval
    • CONST PrintQueue = TRUE
    • CONST TimeBetweenTasks = TimeBetweenTasks0,
    • CONST FirstTaskAfter = FirstTaskAfter0,
    • CONST TimerStart = TimerStart0,
  • in module Tasks
    • CONST AlarmInRam = FALSE,
    • CONST PutInRam = FALSE,

Build and run TaskEval, which prints to the serial terminal:1

   2   FFFFFFC0H

   2   FFFFFFC0H
   4   00000024H

   2   FFFFFFC0H
   4   00000024H
   6   00000088H

   1   FFFFFF8EH
   2   FFFFFFC0H
   4   00000024H
   6   00000088H

   1   FFFFFF8EH
   2   FFFFFFC0H
   3   FFFFFFF2H
   4   00000024H
   6   00000088H

   1   FFFFFF8EH
   2   FFFFFFC0H
   3   FFFFFFF2H
   4   00000024H
   5   00000056H
   6   00000088H

   1   FFFFFF8EH
   2   FFFFFFC0H
   3   FFFFFFF2H
   4   00000024H
   5   00000056H
   6   00000088H
   7   000000BAH

   0   FFFFFF5CH
   1   FFFFFF8EH
   2   FFFFFFC0H
   3   FFFFFFF2H
   4   00000024H
   5   00000056H
   6   00000088H
   7   000000BAH

   0   FFFFFF5CH
   1   FFFFFF8EH
   2   FFFFFFC0H
   3   FFFFFFF2H
   4   00000024H
   5   00000056H
   6   00000088H
   7   000000BAH
  13   0007A07CH
done

We see how the run-queue is built, with the tasks’ id and their alarm trigger time, which cross the 32-bit roll-over point. Task 13 is the task that will print the results. It just prints done for now. We schedule the tasks around the roll-over point, hence 000000024H is later than 0FFFFFFF2H. With hexadecimal time output it is easier to follow the roll-over.

Refer to Traps.slotInHandler to inspect the logic of creating the above run-queue.

Timing Baseline

Now let’s reconfigure the test for timing measurements:

  • In module TaskEval
    • set CONST PrintQueue = FALSE (keep for all runs below)
    • set CONST TimeBetweenTasks = TimeBetweenTasks0,
    • set CONST FirstTaskAfter = FirstTaskAfter1,
    • set CONST TimerStart = TimerStart1,
  • in module Tasks
    • set CONST AlarmInRam = FALSE,
    • set CONST PutInRam = FALSE,

Build and run TaskEval, which prints to the serial terminal:

task    sch at      q at q-del   sch for run    int at    run at r-del    ord
   0 FFF0BD8CH FFF0BDB2H    38 FFFFFF61H   0 FFFFFF62H FFFFFF6AH     9   0 ok
   1 FFF0BDB4H FFF0BDBAH     6 FFFFFF93H   0 FFFFFF93H FFFFFF93H     0   1 ok
   2 FFF0BDBAH FFF0BDBCH     2 FFFFFFC5H   0 FFFFFFC5H FFFFFFC5H     0   2 ok
   3 FFF0BDBCH FFF0BDBEH     2 FFFFFFF7H   0 FFFFFFF7H FFFFFFF7H     0   3 ok
   4 FFF0BDBFH FFF0BDC1H     2 00000029H   0 00000029H 00000029H     0   4 ok
   5 FFF0BDC1H FFF0BDC4H     3 0000005BH   0 0000005BH 0000005BH     0   5 ok
   6 FFF0BDC4H FFF0BDC7H     3 0000008DH   0 0000008DH 0000008DH     0   6 ok
   7 FFF0BDC8H FFF0BDCBH     3 000000BFH   0 000000BFH 000000BFH     0   7 ok
  • task: task id
  • sch at: time when putting the task on the run-queue started
  • q at: time when the task was put on the run-queue
  • q-del: delay between the two values above, ie. the set-up time
  • sch for: time for when the task was scheduled to be triggered by the alarm timer
  • run: run mode: 0 if run normally via alarm interrupt, 1 if run directly due to the scheduling time already being in the past (error)
  • int at: time when the alarm timer interrupt occurred
  • run at: time when the Task procedure was run
  • r-del: delay between the scheduled alarm trigger time and when the task was run
  • ord: order in which the tasks were executed: if the order index equals task id print ok

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).

The q-del value for task 0 shows the caching of the set-up code, the r-del for task 0 the caching of the alarm handler code. All tasks are executed correctly as alarm interrupt handlers (run = 0).

Timing Evaluation: Minimum Consecutive Task Time

Decrease Time Between Consecutive Tasks

Now let’s put some pressure on the alarm handler by decreasing the time between consecutive tasks:

  • In module TaskEval
    • change CONST TimeBetweenTasks = TimeBetweenTasks1,
    • leave CONST FirstTaskAfter = FirstTaskAfter1,
    • change CONST TimerStart = TimerStart2,
  • in module Tasks
    • leave CONST AlarmInRam = FALSE,
    • leave CONST PutInRam = FALSE,

Build and run TaskEval, and we get:

task    sch at      q at q-del   sch for run    int at    run at r-del    ord
   0 FFFFFDF7H FFFFFE1DH    38 FFFFFFE4H   0 FFFFFFE5H FFFFFFEDH     9   0 ok
   1 FFFFFE1FH FFFFFE25H     6 FFFFFFEEH   1 --        FFFFFFFAH    12   1 ok
   2 FFFFFE25H FFFFFE27H     2 FFFFFFF8H   1 --        00000001H     9   2 ok
   3 FFFFFE27H FFFFFE29H     2 00000002H   1 --        00000002H     0   3 ok
   4 FFFFFE2AH FFFFFE2CH     2 0000000CH   0 0000000CH 0000000CH     0   4 ok
   5 FFFFFE2DH FFFFFE2FH     2 00000016H   0 00000016H 00000016H     0   5 ok
   6 FFFFFE30H FFFFFE32H     2 00000020H   0 00000020H 00000020H     0   6 ok
   7 FFFFFE33H FFFFFE36H     3 0000002AH   0 0000002AH 0000002AH     0   7 ok

Now we get cases with run = 1, ie. the alarm handler (see Tasks.alarmHandler) was late when trying to schedule the next task in the run-queue, meaning the current time was already past the task’s planned alarm trigger time, so the task’s procedures were run directly, ie. immediately during the same alarm handler execution as task 0. Consequently, tasks 1 and 2 were executed with a delay (r-del), but in the right order. Task 3 was also executed directly (run = 1), but just in time.

Hence, with the time between consecutive tasks of TimeBetweenTasks1 = 10 microseconds2 we have crossed the limit when executing everything from flash memory. Not shown here, with this test program, 25 microseconds are OK.

Since the tasks execution time eats into the available time between tasks, this result cannot be generalised: it really depends on the specific use case. This is true for all the following tests.

Same Timing, Run from SRAM

Next, we load the alarm handler into SRAM:

  • In module TaskEval
    • leave CONST TimeBetweenTasks = TimeBetweenTasks1,
    • leave CONST FirstTaskAfter = FirstTaskAfter1,
    • leave CONST TimerStart = TimerStart2,
  • in module Tasks
    • change CONST AlarmInRam = TRUE,
    • leave CONST PutInRam = FALSE,

Build and run TaskEval, and we get:

task    sch at      q at q-del   sch for run    int at    run at r-del    ord
   0 FFFFFDF4H FFFFFE10H    28 FFFFFFE1H   0 FFFFFFE1H FFFFFFE1H     0   0 ok
   1 FFFFFE12H FFFFFE18H     6 FFFFFFEBH   0 FFFFFFEBH FFFFFFEBH     0   1 ok
   2 FFFFFE18H FFFFFE1AH     2 FFFFFFF5H   0 FFFFFFF5H FFFFFFF5H     0   2 ok
   3 FFFFFE1BH FFFFFE1DH     2 FFFFFFFFH   0 FFFFFFFFH FFFFFFFFH     0   3 ok
   4 FFFFFE1DH FFFFFE20H     3 00000009H   0 00000009H 00000009H     0   4 ok
   5 FFFFFE20H FFFFFE23H     3 00000013H   0 00000013H 00000013H     0   5 ok
   6 FFFFFE23H FFFFFE26H     3 0000001DH   0 0000001DH 0000001DH     0   6 ok
   7 FFFFFE26H FFFFFE29H     3 00000027H   0 00000027H 00000027H     0   7 ok

As expected, when the alarm handler is run from SRAM, all Task run on time, normally triggered as alarm handlers, and the r-del of task 0 has disappeared, since no caching needs to take place.

As an aside, the set-up delay (q-del) of task 0 has gone from 38 down to 28 microseconds, since the set-up and the alarm handler share some code, which is now in SRAM.

With this test program, running from SRAM, we can achieve about 10 microseconds between tasks (not shown here, see section Pulling All Registers below).

Timing Evaluation: Minimum Task Set-up Time

To check out the minimum set-up time:

  • In module TaskEval
    • change CONST TimeBetweenTasks = TimeBetweenTasks0,
    • change CONST FirstTaskAfter = FirstTaskAfter2,
    • change CONST TimerStart = TimerStart3,
  • in module Tasks
    • change CONST AlarmInRam = FALSE,
    • leave CONST PutInRam = FALSE,

Build and run TaskEval, and we get:

task    sch at      q at q-del   sch for run    int at    run at r-del    ord
   0 FFFFFF35H FFFFFF64H    47 FFFFFF42H   1 --        FFFFFF53H    17   0 ok
   1 FFFFFF66H FFFFFF6BH     5 FFFFFF74H   0 FFFFFF75H FFFFFF7DH     9   1 ok
   2 FFFFFF6BH FFFFFF71H     6 FFFFFFA6H   0 FFFFFFA6H FFFFFFA6H     0   2 ok
   3 FFFFFF71H FFFFFF73H     2 FFFFFFD8H   0 FFFFFFD8H FFFFFFD8H     0   3 ok
   4 FFFFFF73H FFFFFF89H    22 0000000AH   0 0000000AH 0000000AH     0   4 ok
   5 FFFFFF89H FFFFFF8CH     3 0000003CH   0 0000003CH 0000003CH     0   5 ok
   6 FFFFFF8CH FFFFFF8EH     2 0000006EH   0 0000006EH 0000006EH     0   6 ok
   7 FFFFFF8FH FFFFFF91H     2 000000A0H   0 000000A0H 000000A0H     0   7 ok

Task 0 could not be scheduled in time, as FirstTaskAfter2 = 20 is too short after when module TaskEval tries to put it onto the run-queue. It is run directly, so the set-up time q-del includes its execution time.

For task 4, we see an additional set-up delay due to the first firing of an alarm during the time TestEval sets up all tasks.

Same Timing, Run from SRAM

If we put the set-up code in module Tasks into SRAM:

  • In module TaskEval
    • leave CONST TimeBetweenTasks = TimeBetweenTasks0,
    • leave CONST FirstTaskAfter = FirstTaskAfter2,
    • leave CONST TimerStart = TimerStart3,
  • in module Tasks
    • leave CONST AlarmInRam = FALSE,
    • change CONST PutInRam = TRUE,

We get

task    sch at      q at q-del   sch for run    int at    run at r-del    ord
   0 FFFFFF34H FFFFFF3AH     6 FFFFFF41H   0 FFFFFF42H FFFFFF4AH     9   0 ok
   1 FFFFFF3CH FFFFFF3DH     1 FFFFFF73H   0 FFFFFF73H FFFFFF73H     0   1 ok
   2 FFFFFF3EH FFFFFF40H     2 FFFFFFA5H   0 FFFFFFA5H FFFFFFA5H     0   2 ok
   3 FFFFFF40H FFFFFF5AH    26 FFFFFFD7H   0 FFFFFFD7H FFFFFFD7H     0   3 ok
   4 FFFFFF5BH FFFFFF5DH     2 00000009H   0 00000009H 00000009H     0   4 ok
   5 FFFFFF5DH FFFFFF60H     3 0000003BH   0 0000003BH 0000003BH     0   5 ok
   6 FFFFFF60H FFFFFF63H     3 0000006DH   0 0000006DH 0000006DH     0   6 ok
   7 FFFFFF63H FFFFFF66H     3 0000009FH   0 0000009FH 0000009FH     0   7 ok

With the same timing for the first task 0, the set-up is now in time due to the code being in SRAM, ie. all tasks are run by the alarm interrupt. We see caching of the alarm handler for task 0 (r-del).

Decrease Time Before First Task, Run from SRAM

Let’s put some pressure on the set-up time by decreasing the time before the first task:

  • In module TaskEval
    • leave CONST TimeBetweenTasks = TimeBetweenTasks0,
    • change CONST FirstTaskAfter = FirstTaskAfter3,
    • leave CONST TimerStart = TimerStart3,
  • in module Tasks
    • leave CONST AlarmInRam = FALSE,
    • leave CONST PutInRam = TRUE,

We get

task    sch at      q at q-del   sch for run    int at    run at r-del    ord
   0 FFFFFF37H FFFFFF43H    12 FFFFFF3AH   1 --        FFFFFF3BH     1   0 ok
   1 FFFFFF45H FFFFFF47H     2 FFFFFF6CH   0 FFFFFF6DH FFFFFF75H     9   1 ok
   2 FFFFFF48H FFFFFF49H     1 FFFFFF9EH   0 FFFFFF9EH FFFFFF9EH     0   2 ok
   3 FFFFFF4AH FFFFFF4CH     2 FFFFFFD0H   0 FFFFFFD0H FFFFFFD0H     0   3 ok
   4 FFFFFF4CH FFFFFF4EH     2 00000002H   0 00000002H 00000002H     0   4 ok
   5 FFFFFF4EH FFFFFF51H     3 00000034H   0 00000034H 00000034H     0   5 ok
   6 FFFFFF51H FFFFFF54H     3 00000066H   0 00000066H 00000066H     0   6 ok
   7 FFFFFF54H FFFFFF57H     3 00000098H   0 00000098H 00000098H     0   7 ok

With FirstTaskAfter3 = 10 we have again crossed the line for the first set-up and get a direct execution, if only delayed by one microsecond.

With this test program we can achieve about 15 microseconds first task set-up time (see next section).

Pulling All Registers

Let’s run with the minimum times for set-up and between consecutive tasks as evaluated above, all relevant code in module Tasks in SRAM:

  • In module TaskEval
    • set CONST TimeBetweenTasks = TimeBetweenTasks1,
    • set CONST FirstTaskAfter = FirstTaskAfter4,
    • set CONST TimerStart = TimerStart4,
  • in module Tasks
    • set CONST AlarmInRam = TRUE,
    • set CONST PutInRam = TRUE,

We get

task    sch at      q at q-del   sch for run    int at    run at r-del    ord
   0 FFFFFFD4H FFFFFFDAH     6 FFFFFFDCH   0 FFFFFFDCH FFFFFFDCH     0   0 ok
   1 FFFFFFDCH FFFFFFE5H     9 FFFFFFE6H   0 FFFFFFE6H FFFFFFE6H     0   1 ok
   2 FFFFFFE5H FFFFFFE9H     4 FFFFFFF0H   0 FFFFFFF0H FFFFFFF1H     1   2 ok
   3 FFFFFFEAH FFFFFFEBH     1 FFFFFFFAH   0 FFFFFFFAH FFFFFFFAH     0   3 ok
   4 FFFFFFECH FFFFFFEEH     2 00000004H   0 00000004H 00000004H     0   4 ok
   5 FFFFFFEEH FFFFFFF2H     4 0000000EH   0 0000000EH 0000000EH     0   5 ok
   6 FFFFFFF3H FFFFFFF5H     2 00000018H   0 00000018H 00000018H     0   6 ok
   7 FFFFFFF5H FFFFFFF7H     2 00000022H   0 00000022H 00000022H     0   7 ok

Experimental: Forcing Code into the Flash Cache

To close, let’s look at an experimental way of forcing a procedure to be in the flash cache memory, as an alternative to loading them into SRAM.

  • In module TaskEval
    • set CONST TimeBetweenTasks = TimeBetweenTasks1,
    • set CONST FirstTaskAfter = FirstTaskAfter4,
    • set CONST TimerStart = TimerStart4,
  • in module Tasks
    • set CONST AlarmInRam = FALSE,
    • set CONST PutInRam = FALSE,
    • set CONST ForceCache = TRUE.

We get

task    sch at      q at q-del   sch for run    int at    run at r-del    ord
   0 FFFFFFD2H FFFFFFD8H     6 FFFFFFDAH   0 FFFFFFDAH FFFFFFDAH     0   0 ok
   1 FFFFFFDAH FFFFFFE3H     9 FFFFFFE4H   0 FFFFFFE4H FFFFFFE4H     0   1 ok
   2 FFFFFFE3H FFFFFFE7H     4 FFFFFFEEH   0 FFFFFFEEH FFFFFFEFH     1   2 ok
   3 FFFFFFE8H FFFFFFE9H     1 FFFFFFF8H   0 FFFFFFF8H FFFFFFF8H     0   3 ok
   4 FFFFFFEAH FFFFFFECH     2 00000002H   0 00000002H 00000002H     0   4 ok
   5 FFFFFFECH FFFFFFF0H     4 0000000CH   0 0000000CH 0000000CH     0   5 ok
   6 FFFFFFF1H FFFFFFF3H     2 00000016H   0 00000016H 00000016H     0   6 ok
   7 FFFFFFF3H FFFFFFF5H     2 00000020H   0 00000020H 00000020H     0   7 ok

We get set-up and run times just like when loading the code into SRAM, see the Pulling All Registers test case.

Check out procedure MemoryExt.CacheProc and its use in Tasks.Install.

Bottom Line

We could put more code into SRAM, including the Task procedures, to further improve the situation, if the first run requires it. In general, the caching does quite a good job, in particular if our timing requirements are above 100+ microseconds. In case we need to be below, a specifically customised program, including module Tasks and its clients, is advisable, including thorough testing. Module Tasks, with its design to run tasks directly in case their scheduled time falls behind (before) the current time, ensures that all tasks are run, but maybe with some delay. This can be acceptable or not.

Not sure yet how to implement the library module Tasks. I don’t like highly parameterised modules – and neither procedures for that matter –, but customised variants coded straight for the use case. Maybe the concept of maintaining a list of Task to run, and use only one alarm timer per core, is just too “heavy” for low microseconds timing.

Of course it’s nice to be able to schedule more tasks than we have alarm timers available (four), but an approach with restricting the number of tasks to run to the number of alarms, and run the corresponding task procedures directly as alarm interrupt handlers, hence avoiding the list management overhead, might be better for really “tight” timing requirements. I guess I’ll need to implement and test that as well.

Output Terminal

See Set-up, one-terminal set-up.

Build and Run

Build module TaskEval 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. As usual, there can be minor time differences with your MCU/board. ↩︎

  2. At the time of this writing. Maybe some timing details will have changed when you read this. ↩︎