millis() and micros() timer routines

This module is a SF port of the arduino millis() and micros() functions.

millis() - returns the number of msecs since the timer started
micros() - returns the number of usecs since the timer started

The functions return a 32-bit longword count, so the max range for each is:

millis() wraps in approx 49.7 days (1194 hours)
micros() wraps in approx 71.5 minutes (4294 secs)

The module uses a 16-bit timer (TMR1) along with a Capture/Compare/PWM peripheral (CCP1) and CCP1 interrupt to provide accurate and low-overhead time related functions. The code is written for an 18F26K22, but it is compatible with almost all 18F devices (just a few don't have CCP). However, you may have to adjust the TMR, CCP, and CCP INTR settings to match your device (see "**DEVICE DEPENDENT SECTION**" in the code below).

The TMR is set to provide a 1us count rate (or 500ns @ 64MHz) for the micros() function. The CCP peripheral is set to Compare mode:Special Event Trigger for a 1msec period of TMR1, after which it automatically resets the TMR and generates an interrupt. The CCP interrupt handler takes care of incrementing the msec and usec counter variables. There is one restriction: due to the 1us count rate and available timer prescaler settings, the module only works with system clock settings of 4, 8, 16, 32, and 64MHz.

Using the TMR+CCP combination allows the module to avoid the errors inherent in the original arduino method, which actually uses a 1.024ms tick interrupt and constantly tries to correct the value.

Both the millis() and micros() functions are interrupt safe and require no further special considerations. The millis() function provides access to the count by disabling/enabling interrupts, but the micros() function is a little more complicated. To keep things simple (and accurate), the usec count variable keeps track of 1000us intervals, which must then be combined with the realtime count of the TMR. See the comments in the micros() section below for how that's accomplished.

Here's the code for millimicros.bas:

//
//------------------------------------------------------------------------------
//  Name    : millimicros.bas
//  Author  : Jerry Messina
//  Date    : 27 FEB 2023
//  Version : 1.1
//
// SF port of the arduino millis() and micros() functions
// uses TMR1, CCP1, and high or low priority CCP1 interrupt
//
// this module improves on the arduino versions as it does not 
// suffer the inaccuracies/drift of the millis() timer, and 
// provides 1us vs 4us resolution for micros() 
//
// note: TMR1, CCP1, and INTR settings vary with device, 
// so be sure to check the DEVICE DEPENDENT SECTION below
//
//  ver 1.1  27 FEB 2023
//      - change millis() to use PRODL for temp
//      - add timer status flags to allow tracking if a timer wraps (tmr_flags_t)
//      - add #option MILLIMICROS_WRAP to enable/disable wrap check
//  ver 1.0  26 FEB 2023
//      - Initial release
//------------------------------------------------------------------------------
//
module millimicros

// check for currently supported system clock settings 
#if not (_clock in (4, 8, 16, 32, 64))
  #error "CLOCK setting unsupported"
#endif

// device must support some form of CCP peripheral
#if (_ccp = 0) and (_eccp = 0)
  #error _device + " does not contain a CCP/ECCP module"
#endif

// TIMER16 macros (read_tmr16, write_tmr16)
include "timer16.bas"

// select CCP intr priority
// ipLow=1, ipHigh=2 (default)
#option MILLIMICROS_PRIORITY = ipHigh
#if not(MILLIMICROS_PRIORITY in (ipHigh, ipLow))
  #error MILLIMICROS_PRIORITY, "invalid MILLIMICROS_PRIORITY setting"
#endif  
const INTR_PRIORITY = MILLIMICROS_PRIORITY

// enable/disable timer wrap check
//  true=wrap check enabled, false=wrap check disabled (default)
#option MILLIMICROS_WRAP = false

// 
//-----------------------------------------------------------------------------
// **DEVICE DEPENDENT SECTION** (TMRx and CCPx settings)
// **ADJUST AS REQUIRED FOR YOUR DEVICE AND TMR/CCP SELECTIONS**
//-----------------------------------------------------------------------------
// settings below are for an 18F26K22 device using TMR1 and CCP1
#if (_device <> 18F26K22)
  #warning "verify T1CON, CCP1CON, and CCP1 intr settings for " + _device
#endif

//
//-----------------------------------------------------------------------------
// TMR1 setup
//-----------------------------------------------------------------------------
// TMR1 must provide either a 1us (4, 8, 16, 32MHz) or 500ns count rate (64MHz)
// set to count instruction cycles(FOSC/4), 16-bit R/W mode, prescaler setting
//  varies depending on _clock
// T1CON 
//  TMR1 Clock Source Select = instruction clock (FOSC/4)
//  TMR1 Input Clock Prescale = 1:8 (32MHz, 64MHz), 1:4 (16MHz), 1:2 (8MHz), 1:1 (4MHz)
//  TMR1 RD16 = 1 (enable 16-Bit Read/Write Mode)
//  TMR1 ON = 1  (turn on timer, disabled to start)
// 
const TMR = 1                   // TMR number (for timer16.bas)
// T1CON settings
#if (_clock = 64)
  const TMR_TICKS = 2           // two 500ns counts (due to max prescaler of 1:8)
  const TxCKPS = %11<<4         // prescaler = 1:8 T1CON[5:4]
#elseif (_clock = 32)
  const TMR_TICKS = 1           // single 1us count
  const TxCKPS = %11<<4         // prescaler = 1:8 T1CON[5:4]
#elseif (_clock = 16)
  const TMR_TICKS = 1           // single 1us count
  const TxCKPS = %10<<4         // prescaler = 1:4 T1CON[5:4]
#elseif (_clock = 8)
  const TMR_TICKS = 1           // single 1us count
  const TxCKPS = %01<<4         // prescaler = 1:2 T1CON[5:4]
#elseif (_clock = 4)
  const TMR_TICKS = 1           // single 1us count
  const TxCKPS = %00<<4         // prescaler = 1:1 T1CON[5:4]
#endif
const TMRxCS = %00<<6           // TMRxCS = instruction clock T1CON[7:6]
const TxRD16 = %1<<1            // TxRD16 = 1 enable 16-bit mode T1CON[1]
const TMRxON = 0                // TMRxON bit number T1CON[0]
const TMR1_T1CON = TMRxCS + TxCKPS + TxRD16 // T1CON register settings

dim TMRON as T1CON.bits(TMRxON)     // alias for TMR1ON bit

// TMR1 init 
public sub tmr_init()
    TMR1_PIE.bits(TMR1_IE) = 0      // TMR1 intr not used (definitions from timer16.bas)
    T1CON = TMR1_T1CON              // setup timer control reg
    write_tmr16(TMR, 0)             // init TMRH:TMRL = 0
end sub

//
//-----------------------------------------------------------------------------
// CCP1 setup
//-----------------------------------------------------------------------------
// CCP1 must be set to Compare mode: Special Event Trigger with source = TMR1
// this will generate an interrupt when TMR1 matches the CCP1 period reg and
// automatically reset the TMR1 count
// the CCP1 period registers should be set to provide a 1ms intr based on
// the TMR1 count rate
//
const CCP = 1
// CCP1CON settings
const CCP1M = %1011                 // CCP1M[3:0] = Compare mode: Special Event Trigger 
const CCP1_CCP1CON = CCP1M          // CCP1CON register settings
// CCP1 intr bits
dim CCP1IF as PIR1.bits(2),         // CCP1 intr flag
    CCP1IE as PIE1.bits(2),         // CCP1 intr enable
    CCP1IP as IPR1.bits(2)          // CCP1 intr priority
// CCP1 timer select register 
dim CCP1TMRS as CCPTMRS0            // reg alias for CCP1 timer select reg (name can change)

dim CCPIF as CCP1IF,                // alias for CCP1IF
    CCPIE as CCP1IE                 // alias for CCP1IE

// CCP 1ms period based on TMR count rate (1000 @ 1us, 2000 @ 500ns)
const CCP_PERIOD as word = (1000*TMR_TICKS)-1

// CCP1 init
public sub ccp_init()
    CCP1CON = CCP1_CCP1CON          // setup CCP1 control reg
    CCP1TMRS.bits(1) = 0            // CCP1 Timer Selection C1TSEL[1:0] = use TMR1
    CCP1TMRS.bits(0) = 0
    CCPR1H = CCP_PERIOD >> 8        // CCP1 period, high byte      
    CCPR1L = CCP_PERIOD and $FF     // CCP1 period, low byte

    // intr setup
  #if (MILLIMICROS_PRIORITY = ipHigh)
    '#warning "CCP IP = ipHigh"
    CCP1IP = 1                      // intr priority = high (IRPx)
  #else
    '#warning "CCP IP = ipLow"
    CCP1IP = 0                      // intr priority = low (IRPx)
  #endif
    CCP1IF = 0                      // clear intr flag (PIRx)
    CCP1IE = 1                      // enable intr (PIEx)
end sub

//-----------------------------------------------------------------------------
// **end of device dependent section**
//-----------------------------------------------------------------------------

// tmr status flags
public structure tmr_flags_t
    b as byte
    tmr_intr    as b.bits(0)        // counters have updated (ccp_interrupt)
    ms_wrapped  as b.bits(1)        // msec counter has wrapped
    us_wrapped  as b.bits(2)        // usec counter has wrapped
end structure

public dim
    tmr_flags as tmr_flags_t        // tmr status

// module level static variables
dim
    tmr_ms as longword = 0,         // msec counter
    tmr_1000us as longword = 0      // usec counter

// CCP interrupt - occurs every 1ms
// execution time: approx 20 cycles (~1.25us @ 64MHz)
public interrupt ccp_interrupt(INTR_PRIORITY)
    dim CARRY as STATUS.bits(0)     // STATUS reg Carry flag

    CCPIF = 0                       // clear intr flag
    tmr_ms = tmr_ms + 1             // increment msec count
  #if (MILLIMICROS_WRAP)            // wrap check enabled...
    if (CARRY = 1) then             // tmr_ms has wrapped
        tmr_flags.ms_wrapped = 1
    endif        
  #endif
    tmr_1000us = tmr_1000us + 1000  // increment usec count (1ms=1000us)
  #if (MILLIMICROS_WRAP)            // wrap check enabled...
    if (CARRY = 1) then             // tmr_us has wrapped
        tmr_flags.us_wrapped = 1
    endif        
  #endif
    tmr_flags.tmr_intr = 1          // signal an update has occurred
end interrupt

// init hdw peripherals, start timer, and enable intr
public sub init()
    CCPIE = 0                       // disable CCP intr
    tmr_init()                      // init tmr hdw
    ccp_init()                      // init cpp hdw
    TMRON = 1                       // start TMR running...
    enable(ccp_interrupt)           // and enable the intr
end sub

// millis()
//  returns the number of msecs since the timer started
//  the msec timer wraps in approx 49.7 days (1194 hours)
// execution time: ~1us @ 64MHz
public function millis() as longword
    dim t_intcon as PRODL       // use PRODL to avoid any banking
    const GIE = 7

    // get current intr enable state and disable intrs
    // while we access the global msec variable
    t_intcon = INTCON
    INTCON.bits(GIE) = 0

    // read global variable to get return value
    result = tmr_ms

    // restore intr state
    if (t_intcon.bits(GIE) = 1) then
        INTCON.bits(GIE) = 1
    endif
end function

// micros()
//  returns the number of usecs since the timer started
//  the usec timer wraps in approx 71.5 minutes (4294 secs)
//
// this function combines the value of the free-running hdw timer 
//  with a multi-byte variable that's incremented in the ISR. 
// for this to be correct the two values must be in sync...
//  if it gets interrupted, or if you disable interrupts and 
//  the ISR doesn't get a chance to keep tmr_1000us current,
//  then the values will be out of sync and will be invalid
// use tmr_intr flag to determine if we get interrupted and 
//  if so re-read the tmr
// execution time: ~1.5us @ 64MHz
public function micros() as longword
    repeat
        tmr_flags.tmr_intr = 0          // clear 'has updated' flag set by ISR
        read_tmr16(TMR, PRODW)          // get current tmr count into PRODH:PRODL
        if (TMR_TICKS = 2) then         // adjust count (this const test will get optimized out)
            PRODW = PRODW >> 1          // convert 500ns ticks -> 1us ticks
        endif
        result = tmr_1000us + PRODW     // add current TMR to global usec counter
    until (tmr_flags.tmr_intr = 0)      // if interrupted then re-read        
end function

// module initialization
tmr_flags.b = 0

end module

Here are some examples of using it...

// example millis(), micros()

device = 18F26K22
clock = 64

include "intosc.bas"
include "millimicros.bas"

dim startTime, elapTime, interval as longword
dim LED as PORTB.0

low(LED)

// start millis-micros timer and enable intr
millimicros.init()

// repeat until interval has expired
interval = 500     // 500ms
startTime = millis()
while (millis()-startTime < interval)
    // do something
    toggle(LED)
end while

interval = 500     // 500us
startTime = micros()
repeat
    elapTime = micros() - startTime
    toggle(LED)
until (elapTime >= interval)

// repeat every 'interval' usecs
interval = 500
startTime = micros()
while (true)
    if (micros()-startTime >= interval) then
        toggle(LED)
        startTime = micros()
    endif        
end while

One thing that might not be immediately apparent is that even though the counters are 32-bit, you can use 'byte' or 'word' sized variables if appropriate and save considerable code. If used in a conditional statement you should cast the return of millis()/micros() to match.

// for values up to 255 use 'byte'
dim startTime, interval as byte

interval = 250
startTime = millis()
while (byte(millis())-startTime < interval)		// cast return value
    // do something
    toggle(LED)
end while

// for values up to 65535 use 'word'
dim wStart as word

wStart = micros()
repeat
    toggle(LED)
until (word(micros())-wStart >= 800)