Framerate Timing Resolved

February 08, 2008 | 6 Minute Read

I solved my timing issue, but I have to admit, I’m still confused as to exactly what caused it.

My basic timing functions rely on an asynchronous wallclock with millisecond resolution. I implement this using Timer 0 in 16-bit mode, setting its initial value then incrementing a tick count during the interrupt it generates on roll-over from 0xffff to 0x0000. Since Timer 0 is incremented every instruction cycle, the correct initial value is simply 0xffff - Count, where Count is the number of instructions that can be executed in 1ms.

For some reason I originally turned on prescaling for Timer 0. I think I was thinking there would be less overhead if the timer incremented fewer times before processing (the prescalar causes every 2nd, 4th, 8th, etc. instruction cycle to be counted, instead of every one—in my case, I chose every 256th). This is wrong. In fact, since it’s counting instruction cycles (not actual time intervals), it’s up to you to ensure the total duration of the cycles you count equals your desired delay.

For example, if you want to delay 50μs and each instruction cycle takes 5μs, you need to count 10 instruction cycles. If you want to delay 33μs, however, you’re out of luck. 33 is not a multiple of 5. At 24MHz, my project executes 6,000,000 instructions per second, which means I needed to count 6,000 instruction cycles to delay exactly 1ms. Because I applied a prescalar of 1:256, however, that count changed to 6,000 ÷ 256 = 23.4375, rounded to 23.

That rounding caused a timing error, to be sure—my wall clock would appear to be running slightly fast—but I still didn’t see how it could cause the shortened pulses from the trace. Regardless, removing the prescalar (and making the clock ISR isochronous for good measure) eliminated the problem, so the question is moot.

;; ---------------------------------------------------------------------------
;;  PIC Framework
;;  Copyright © 2006-8  Peter Heinrich
;;  All Rights Reserved
;;  $URL: svn:// $
;;  $Revision: 288 $
;;  Provides a general-purpose wallclock with millisecond resolution.
;; ---------------------------------------------------------------------------
;;  $Author: Peter $
;;  $Date: 2008-02-07 07:23:15 +0000 (Thu, 07 Feb 2008) $
;; ---------------------------------------------------------------------------
   #include ""
   ; Variables
   global   Clock.Alarm
   global   Clock.Ticks
   ; Methods
   global   Clock.init
   global   Clock.isAwake
   global   Clock.isr
   global   Clock.setWakeTime
   global   Clock.sleep
kMIPS                   equ   kFrequency >> 2
kTickPrescalarLog2      equ   0
kInstructionsPerMS      equ   (kMIPS >> kTickPrescalarLog2) / 1000
kTickDelay              equ   0xffff - kInstructionsPerMS
;; ---------------------------------------------------------------------------
;; ---------------------------------------------------------------------------
Clock.Alarm             res   4
Clock.Ticks             res   4
;; ---------------------------------------------------------------------------
.clock                  code
;; ---------------------------------------------------------------------------
;; ----------------------------------------------
;;  void Clock.init()
;;  Initializes Timer0 to be a general-purpose wallclock with millisecond
;;  resolution.
   bcf      PORTC, RC1
   lfsr     FSR0, Clock.Alarm
   movlw    0x08
   ; Clear the block.
   clrf     POSTINC0
   decfsz   WREG, F
     bra    $-4
   ; Install the isr at the correct frequency.
   bra      restart
;; ----------------------------------------------
;;  STATUS<C> Clock.isAwake()
;;  Compares the current tick count to the wake time stored in Clock.Alarm.
;;  This method returns with the STATUS<C> set if the wake time is in the past,
;;  otherwise it will be clear.
   ; Compare the 32-bit alarm value to the 32-bit tick count.
   movf     Clock.Alarm, W
   subwf    Clock.Ticks, W          ; first byte (LSB)
   movf     Clock.Alarm + 1, W
   subwfb   Clock.Ticks + 1, W      ; second byte
   movf     Clock.Alarm + 2, W
   subwfb   Clock.Ticks + 2, W      ; third byte
   movf     Clock.Alarm + 3, W
   subwfb   Clock.Ticks + 3, W      ; fourth byte (MSB)
   ; If the current tick count has passed the wake time, the subtraction above
   ; will set the carry flag.
;; ----------------------------------------------
;;  void Clock.isr()
;;  Updates the millisecond counter whenever Timer0 rolls over.  We reset the
;;  timer at the end of every update to ensure this method is called by the
;;  interrupt service routine every millisecond.
   ; Determine if it's time for us to update the counter.
   btfss    INTCON, TMR0IE          ; is the TMR0 interrupt enabled?
     return                         ; no, we can exit
   btfss    INTCON, TMR0IF          ; yes, did TMR0 roll over?
     return                         ; no, we can exit
   ; Increment the millisecond tick counter, a 32-bit value.
   subwf    WREG                    ; W = 0, STATUS<C> = 1
   addwfc   Clock.Ticks + 0, F
   addwfc   Clock.Ticks + 1, F
   addwfc   Clock.Ticks + 2, F
   addwfc   Clock.Ticks + 3, F
   ; Toggle "heartbeat" I/O pin at ~1 Hz.
   btfss    Clock.Ticks + 1, 1
     bcf    PORTC, RC1
   btfsc    Clock.Ticks + 1, 1
     bsf    PORTC, RC1
   bra      restart
;; ----------------------------------------------
;;  void Clock.setWakeTime()
;;  Adds the current time to the 32-bit value in Clock.Alarm, computing a
;;  tick count (probably) in the future.  We'll compare that value to the
;;  actual tick count to effect simple delays with millisecond precision.
   ; Add the alarm value to the current tick count, creating a "target" tick count
   ; to match.  Once the actual tick count reaches the target value, the delay is
   ; complete.
   movf     Clock.Ticks, W
   addwf    Clock.Alarm, F          ; first byte (LSB)
   movf     Clock.Ticks + 1, W
   addwfc   Clock.Alarm + 1, F      ; second byte
   movf     Clock.Ticks + 2, W
   addwfc   Clock.Alarm + 2, F      ; third byte
   movf     Clock.Ticks + 3, W
   addwfc   Clock.Alarm + 3, F      ; fourth byte (MSB)
;; ----------------------------------------------
;;  void Clock.sleep()
;;  Enters a busy loop (suspends normal execution) until the tick count equals
;;  a specified alarm value, settable by updating Clock.Alarm directly via
;;  Clock.setWakeTime() or by using the SetAlarmMS macro.  On entry, this
;;  routine expects the alarm registers to hold the target wake time.
;;  Note that interrupts must not be disabled when this routine runs, since it
;;  depends on Clock.Ticks being volatile and updated asynchronously by the
;;  interrupt service routine.
   ; Compare the current time to the wake time.
   rcall    Clock.isAwake        ; has the wake time passed?
   bnc      Clock.sleep          ; no, keep checking
   return                        ; yes, we can exit
;; ----------------------------------------------
;;  void restart()
;;  Reset the countdown period for the millisecond timer.
   ; Set up the basic timer operation.
   movlw    b'00001000'
            ; 0------- TMR0ON       ; turn off timer
            ; -0------ T08BIT       ; use 16-bit counter
            ; --0----- T0CS         ; use internal instruction clock
            ; ---X---- TOSE         ; [not used with internal instruction clock]
            ; ----1--- PSA          ; do not prescale timer output
            ; -----XXX T0PSx        ; [not used when prescaler inactive]
   movwf    T0CON
   ; Establish the countdown based on calculated MIPS.
   movlw    kTickDelay >> 8
   movwf    TMR0H
   movlw    kTickDelay & 0xff
   movwf    TMR0L
   ; Clear the timer interrupt flag.
   bcf      INTCON, TMR0IF
   btfsc    INTCON, TMR0IF          ; is the flag clear now?
     bra    $-2                     ; no, wait for it to change
   ; Unmask the timer interrupt and turn on the countdown timer.
   bsf      INTCON, TMR0IE
   bsf      T0CON, TMR0ON