millis() and micros() timer routines
This module is a SF port of the arduino millis() and micros() functions.
The functions return a 32-bit longword count, so the max range for each is:
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)