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://saphum.com/PIC/framework/trunk/clock.asm $
;; $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 "private.inc"
; 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
;; ---------------------------------------------------------------------------
udata_acs
;; ---------------------------------------------------------------------------
Clock.Alarm res 4
Clock.Ticks res 4
;; ---------------------------------------------------------------------------
.clock code
;; ---------------------------------------------------------------------------
;; ----------------------------------------------
;; void Clock.init()
;;
;; Initializes Timer0 to be a general-purpose wallclock with millisecond
;; resolution.
;;
Clock.init:
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.
;;
Clock.isAwake:
; 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.
return
;; ----------------------------------------------
;; 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.
;;
Clock.isr:
; 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.
;;
Clock.setWakeTime:
; 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)
return
;; ----------------------------------------------
;; 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.
;;
Clock.sleep:
; 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.
;;
restart:
; 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
return
end