Timers and Timing

The Timer Device

The RP2040 and the RP2350 provide one or two timer peripheral devices, first, allowing to read the current time in micro-seconds since reset, and second, to trigger an interrupt at a given alarm time, using one of four alarm timers. The timer registers get incremented with a one micro-second clock from the watchdog (RP2040) or tick (RP2350) device.

The time since reset is counted in 64 bits, ie. via two 32-bit counter registers, which gives a 2^64 micro-seconds = 585e3 years runway. However, alarms only make use of the lower 32 bits of this timer: an alarm interrupt gets triggered when the lower 32 bits of the 64-bit timer become equal to the alarm time value. Consequently, it’s possible to set an alarm maximally 2^32 micro-seconds into the future, that is, 71.58 minutes.

Time Arithmetic

Overview

The alarm timer, ie. the lower 32 bits counter of the 64-bit “global” timer, can be read via a register address as 32-bit value as INTEGER. Unlike INTEGER, though, its full value range represents positive values, like the CARDINAL type in Modula-2. At 0FFFFFFFFH, it will roll over to zero. From a timing perspective, a value of, say, 0AH, can represent a later time than 0FFFFFFFAH. At 07FFFFFFFH, it will increment and overflow to 080000000H at the next micro-seconds tick, ie. the latter represents a later time than the former, but since INTEGERs are 2-complement values, 080000000H < 07FFFFFFFH. We cannot use simple comparisons to decide what is earlier or later.

Setting an Alarm: Addition

The easy part is to set an alarm in the future. The addition operation on a 32-bit CPU register using INTEGERs will overflow the 07FFFFFFFH “positive to negative” 2-complement boundary, as well as roll over across the 0FFFFFFFFH “negative to positive” boundary, respectively. There are no corresponding (compiler-inserted) run-time checks, and correctly so, otherwise lots of low-level operations on the hardware would be problematic (apart from the run-time overhead). Same for the CPU itself, which just sets its overflow flags (C and V).

All hardware timer values represent positive times, so it obviously will cross the 07FFFFFFFH “positive to negative” boundary, since this notion does not even exist in the timer hardware, and at 0FFFFFFFFH it will roll over to zero – just as the addition operation in the CPU.

So since the interrupt trigger is based on the simple equality of the binary values in the hardware, we can always set the future alarm trigger time as current timer value + delay.

Time Value Comparison: Before or After?

A challenge arises when comparing timer values for bigger or smaller, as outlined above. Time comparisons are needed for the scheduling of Task: we need to know if a certain time value is before or after a reference time. Simply comparing INTEGERs, which is all we got with Oberon, will not work.

A solution is to use subtraction. There, we need to know if a certain time value read as INTEGER is after a reference time as INTEGER. This code snippet exemplifies the algorithm:

  IF t - refTime > 0 THEN
    Out.String("after")
  ELSE
    Out.String("before or equal")
  END
With this comparison algorithm

  • time values refTime + 2^31 - 1 are after refTime
  • other time values are before (or equal to) refTime

This limits us, consequently, to setting alarm times to 2^31 - 1 micro-seconds into the future, ie. half the 71 minutes range of the full 32-bit value of the timer.

Test Program TestWrap

Since I am bad with all this roll-over and overflow stuff, there’s a test program TestWrap to play with the boundaries, and different timer values, in order to see that things work as intended.