How to use the timers

A simple time-burning delay() function, which just hangs in a loop until the time expires, can work for the most trivial of applications, but in a cooperative multitasking Operating System, that method is simply not useful. Some way to selectively defer program execution until a later time is required, however. This is why MIRTOS has timers.

Only two things are needed for each timer: the duration, or interval, and the starting time. Since the need for processing delays can vary so widely, MIRTOS has five (5) types of timers with different resolutions and ranges:
  Bytes  Timer Range Resolution  System Function
11 to 250mSec msTimerBYTE
21 to 65,530mSec msTimerWORD
41 to 4,294,967,290mSec msTimerULONG
11 to 250Seconds SecTimerBYTE
21 to 65,530Seconds SecTimerWORD

Each of the timers return its results BOTH in Register R24 and in the ZERO flag.

The multitasking, level display, and pump control System Applications each use a single msTimerWORD timer. Each timer, with its duration WORD stored first in SRAM followed immediately by its starting time WORD, is contained within the Application instance's configuration.

The sequencer System Application uses msTimerWORD, the time to wait between calls to the update logic for that step, then SecTimerWORD, the maximum time to remain in that step. Only the durations of each need to be specified in the configuration, since the Operating System initializes each starting time WORD when it begins a new sequencing step.

The scripting logic has the ability to use any of the five (5) types of timers.

Subroutines can call any of the timers. It doesn't make much sense to call a timer subroutine if the called routine was invoked using a timer. There is one instance where embedding a timer call in a subroutine makes a lot of sense, however. It is when using the System Function which calls an assigned subroutine each pass through the main() loop. In this situcation, the subroutine is responsible for finding and assigning its own SRAM memory locations. These can certainly be in the uVars.B[##] range. Here is an example using BYTE timers:

BYTETimer = 0x2FD                 ; Specify an SRAM location for 3 bytes
IbDuration  = 0                   ; Index of the Duration byte at that address
IbLastStart = 1                   ;  then indices of target and configuration
IbOptions   = 2                   ;  bytes which follow it

InlineBYTETimer:
   LDI     R31, BYTETimer >> 8    ; Point 'Z' at the first of the three (3)
   LDI     R30, BYTETimer & 0xFF  ;  BYTE variables defined above
   LDD     R24, Z + IbDuration    ; Load both the interval and the time value
   LDD     R22, Z + IbLastStart   ;  saved the last time the timer fired
   LDD     R20, Z + IbOptions     ; This lets us select which timer to use
   SBRS    R20, 0                 ; If the Low Order Bit is clear (logic 0),
   CALL    AmsTimerBYTE           ;  then the millisecond timer is selected
   SBRC    R20, 0                 ;  otherwise the Seconds timer is desired
   CALL    ASecTimerBYTE          ; All timers return status in both R24 and
   BREQ    AIBT_Exit              ;  ZERO flag (TRUE = not done, FALSE = done)

   SBRS    R20, 0                 ; The timer has timed out; either get the
   CALL    AmillisLowBYTE         ;  milliseconds or System Seconds Low Order
   SBRC    R20, 0                 ;  byte, depending on which timer was being
   CALL    ASystemSecondsLowBYTE  ;  used
   STD     Z + IbLastStart, R22   ; Store the new starting time for next time

   CALL    ATheTimedTask          ; Call the subroutine to do the desired task

IBT_Exit:
   RET
WORD timers are just a little more complicated. In this example, the milliseconds and Seconds selection is made in the Assembly code instead as a configurable option during runtime:
WORDTimer = 0x2FC                    ; Specify 4 SRAM BYTEs: 0x2FC to 0x2FF
IwDuration  = 0                      ; Index constants
IwLastStart = 2

InlineWORDTimer:
   LDI     R31, WORDTimer >> 8       ; Setup the 'Z' pointer
   LDI     R30, WORDTimer & 0xFF
   LDD     R24, Z + IwDuration       ; Read the Low Order BYTE first then the
   LDD     R25, Z + IwDuration + 1   ;  High Order BYTE at the next higher SRAM
   LDD     R22, Z + IwLastStart      ;  address into R25:R24
   LDD     R23, Z + IwLastStart + 1  ; Read the last start time into R23:R22
.IF SELECT_SECONDS == TRUE
   CALL    AmsTimerWORD
   BREQ    AIWT_Exit
   CALL    AmillisLowWORD            ; mSec Low WORD returned in R25:R24
.ELSE
   CALL    ASecTimerWORD
   BREQ    AIWT_Exit
   CALL    ASystemSecondsLowWORD     ; System Seconds LSWord in R25:R24
.ENDIF

   STD     Z + IbLastStart    , R24  ; Store the new starting time, in LSByte
   STD     Z + IbLastStart + 1, R25  ;  then MSByte order

   CALL    ATheTimedTask             ; Call the "workhorse" subroutine

IWT_Exit:
   RET
Finally, for completeness, here is code for an UNSIGNED LONG milliseconds timer. There is no ULONG System Seconds timer.
ULONGTimer = 0x2F8                   ; Specify 8 SRAM BYTEs: 0x2F8 to 0x2FF
IulDuration  = 0                     ; The index constants
IulLastStart = 4

InlineULONGTimer:
   LDI     R31, ULONGTimer >> 8      ; Setup the 'Z' pointer
   LDI     R30, ULONGTimer & 0xFF
   LDD     R22, Z + IulDuration
   LDD     R23, Z + IulDuration + 1  ; Load the duration into R25:R24:R23:R22
   LDD     R24, Z + IulDuration + 2
   LDD     R25, Z + IulDuration + 3
   LDD     R18, Z + IulLastStart     ; Load the last time into R21:R20:R19:R18
   LDD     R19, Z + IulLastStart + 1
   LDD     R20, Z + IulLastStart + 2
   LDD     R21, Z + IulLastStart + 3
   CALL    AmsTimerULONG
   BREQ    AIULT_Exit

   CALL    Amillis
   STD     Z + IulLastStart    , R22 ; Store the new starting time, in LSByte
   STD     Z + IulLastStart + 1, R23 ;  to MSByte order
   STD     Z + IulLastStart + 2, R24
   STD     Z + IulLastStart + 3, R25

   CALL    ATheTimedTask             ; Call the "workhorse" subroutine

IULT_Exit:
   RET
Why have all these types of timers? Two somewhat conflicting aspects of Operating Systems are time efficiency and code efficiency. It is more efficient to check BYTEs instead of WORDs or ULONGs and similarly, it is more efficient to check WORDS rather than ULONGs. This is especially true when it is being done many hundreds or even thousands of times each second. It turns out that although there is a need for ULONG time periods, they really aren't needed that often.

Notes:
   The interval between timer checks isn't really that important. It only matters if the time delay has passed or not. Since the main routine usually loops faster than once each millisecond, it executes over 1,000 timer checks each second. It takes less than 5 microseconds to check a timer when the MCU is being clocked at 8 MHz.
A time delay of zero is considered to ALWAYS be timed out, which means a subroutine can change its own timer to zero to cause it to time out (or "fire") again the very next time it is checked.

See also:
    The BYTE, WORD (2-BYTE), and ULONG (4-BYTE) timers.