AlarmTest

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

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, 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 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;
  • 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 handler p0 in run 1, and 17 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 Pre-caching

StartTime:       FFFFFF9BH = FFFFFFFFH - 100
FirstRunAfter:   50
TimeBetweenRuns: 25
PreCachedProcs:  Yes   (1579)
 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 1,579 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). Put on to-do list for re-evaluation.

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   (1579)
 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   (1579)
 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   (1579)
 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:
  • lib (v1): this version is no longer being maintained: AlarmTest

  1. Cache line pinning with the RP2350 needs evaluation as an alternative. ↩︎