PIC16F882: 14us interrupt overhead when using TMR0

Thread Starter

Robin66

Joined Jan 5, 2016
275
Hi guys, I've spent 2 night trying to suss this and am still perplexed. I've a PIC 16F882 with an external 20MHz crystal giving 5Mips. I've confirmed this with the RB0 output. I want to create an interrupt every ~20us, so I thought 100x 200ns instruction period == 20us. However when I set TMR0 = 255-100, I get an interrupt period of 34us. If I set TMR0=255-200, I get a period of 54us so the marginal change is correct ie. 20us per 100 change in TMR0 seed. But where's the fixed 14us overhead coming from? I expected to lose a few instructions, but 14us is 70 instructions! I've tried using a prescaler on TMR0 but it didn't fix the missing instructions. I've confirmed the interrupt period by observing the RB1 output. Code attached.

Code:
;**********************************************************************
; Li-ion balancer
; PIC16F882, 20MHz, origin
;**********************************************************************

    list      p=16f882           ; list directive to define processor
    #include <p16F882.inc>       ; processor specific variable definitions
    ;errorlevel  -302              ; suppress message 302 from list file
   
__config _CONFIG1, h'20f2'  ;
            ; 0010 - nyi,nyi,debug disabled,LVP off
            ; 0000 - Failsafe disabled, I/E switchover disabled, BOR disabled
            ; 1111 - Dataprotection disabled,Codeprotection disabled, MCLR, INTOSC on
            ; 0010 - Powerup timer ON, WDT off, 20Mhz crystal on OSC1-2

    cblock 0x20    ; Begin General Purpose-Register
    countLED
    nCycles ; cycles per interrupt
    w_temp
    status_temp
    endc

    ;***** VARIABLE DEFINITIONS
;w_temp        EQU     0x71        ; variable used for context saving
;status_temp   EQU     0x72        ; variable used for context saving


;**********************************************************************
    ORG     0x000             ; processor reset vector
    goto    setup              ; go to beginning of program
   
    ORG     0x004             ; interrupt vector location
    movwf   w_temp            ; save off current W register contents
    movf    STATUS,w          ; move status register into W register
    movwf    status_temp       ; save off contents of STATUS register

    call isr
       
    ; set TMR0 so that we have nCycles per interrupt
    movfw nCycles
    subwf h'FF',w
    movwf TMR0   
    bcf INTCON,T0IF
    bsf INTCON, T0IE ; enable TMR0 interrupt
   
    movf    status_temp,w     ; retrieve copy of STATUS register
    movwf    STATUS            ; restore pre-isr STATUS register contents
    swapf   w_temp,f
    swapf   w_temp,w          ; restore pre-isr W register contents
    retfie                    ; return from interrupt

;**********************************************************************

setup ; init PIC16F882

    movlw    b'00000000'        ; Turn off comparator 1
    movwf    CM1CON0
   
    banksel ANSEL
    clrf    ANSEL ; 0 = all pins digital ie. analog off
    clrf    ANSELH
   
    banksel TRISA    ; BSF    STATUS,RP0 Jump to bank 1 use BANKSEL instead
    movlw b'00000000'             ; B: all outputs
    movwf TRISB                           
    movwf TRISA                
    movwf TRISC   
   
    banksel INTCON ; back to bank 0
    clrf    PORTA ; all outputs low
    clrf    PORTB

    ; setp TMR0 interrupts
    banksel OPTION_REG ; Reg. 0x81 BANK1
    movlw b'00001000' ; instruction clock, assign prescale to WDT
    movwf OPTION_REG
    banksel INTCON ;
    clrf INTCON
    bsf INTCON, GIE ; enable global interrupt
    bcf INTCON, PEIE ; enable all unmasked interrupts
    bsf INTCON, T0IE ; enable TMR0 interrupt
    clrf TMR0
    movlw d'1'  ; 100 instructions per interrupt = 20us
    movwf nCycles
    goto main

main

    movlw b'00000001' ; toggle 0th bit
    xorwf PORTB,f   
    goto main

;************************************************************************
       
isr
    movlw b'00000010' ; toggle 1st bit every interupt
    xorwf PORTB,f   
    return
   
END  ; directive 'end of program'
Mod edit: added code tags
 

Attachments

Last edited by a moderator:

atferrari

Joined Jan 6, 2004
4,764
Hi guys, I've spent 2 night trying to suss this and am still perplexed. I've a PIC 16F882 with an external 20MHz crystal giving 5Mips. I've confirmed this with the RB0 output. I want to create an interrupt every ~20us, so I thought 100x 200ns instruction period == 20us. However when I set TMR0 = 255-100, I get an interrupt period of 34us. If I set TMR0=255-200, I get a period of 54us so the marginal change is correct ie. 20us per 100 change in TMR0 seed. But where's the fixed 14us overhead coming from? I expected to lose a few instructions, but 14us is 70 instructions! I've tried using a prescaler on TMR0 but it didn't fix the missing instructions. I've confirmed the interrupt period by observing the RB1 output. Code attached.
Had I that problem I would simulate to verify that there is not an undesired change of some registers.

Could you test just the minimal amount of code to narrow down the problematic part?
 

dannyf

Joined Sep 13, 2015
2,197
But where's the fixed 14us overhead coming from?
14 ticks (=3us in your case) sound about right. I typically budget 20us for (C) overhead.

I've tried using a prescaler on TMR0 but it didn't fix the missing instructions.
That sounds wrong - you either didn't implement the prescaler right or there is something else wrong.

I've confirmed the interrupt period by observing the RB1 output.
100 ticks sounds plenty to get an isr done. so I think something else is wrong.
 

jpanhalt

Joined Jan 18, 2008
11,087
Hi guys, I've spent 2 night trying to suss this and am still perplexed. I've a PIC 16F882 with an external 20MHz crystal giving 5Mips. I've confirmed this with the RB0 output. I want to create an interrupt every ~20us, so I thought 100x 200ns instruction period == 20us. However when I set TMR0 = 255-100, I get an interrupt period of 34us. If I set TMR0=255-200, I get a period of 54us so the marginal change is correct ie. 20us per 100 change in TMR0 seed. But where's the fixed 14us overhead coming from? I expected to lose a few instructions, but 14us is 70 instructions! I've tried using a prescaler on TMR0 but it didn't fix the missing instructions. I've confirmed the interrupt period by observing the RB1 output. Code attached.
That's your error. Try (255-155). That is, it counts 100 and rolls over.

John
 

Thread Starter

Robin66

Joined Jan 5, 2016
275
That's your error. Try (255-155). That is, it counts 100 and rolls over.

John
255-155 = 100. If TMR0 is set to 100 then the overflow will occur 155 instructions later since TMR0 increments +1 per instruction and overflows at 255.

I've demonstrated that my incremental changes to nCycles give the desired change in interrupt period ie. +100 nCycles = 20us change in interrupt period
 

jpanhalt

Joined Jan 18, 2008
11,087
I thought you wanted 100 counts. What you actually posted gave 155 counts.

John

Edit: Oops. Too much sun today cleaning up dead ash trees. Sorry.
 
Last edited:

JohnInTX

Joined Jun 26, 2012
4,787
Keep in mind that writing to TMR0 clears the prescaler and you lose the counts in it. Also, setting the timer by moving W to it will overwrite any accumulated counts in the timer itself. That can be mitigated by ADDING the set value to TMR0 but you still lose the prescaler counts. @ErnieM has a better idea.
FWIW, the context save in your interrupt routine should use swapf to get STATUS, not movf. From the Midrange Family Reference Manual:
Example 8-1: Saving the STATUS and W Registers in RAM
(for Devices with Common RAM)
MOVWF W_TEMP ; Copy W to a Temporary Register
; regardless of current bank
SWAPF STATUS,W ; Swap STATUS nibbles and place
; into W register
MOVWF STATUS_TEMP ; Save STATUS to a Temporary register
; in Bank0
:
: (Interrupt Service Routine (ISR) )
:
SWAPF STATUS_TEMP,W ; Swap original STATUS register value
; into W (restores original bank)
MOVWF STATUS ; Restore STATUS register from
; W register
SWAPF W_TEMP,F ; Swap W_Temp nibbles and return
; value to W_Temp
SWAPF W_TEMP,W ; Swap W_Temp to W to restore original
; W value without affecting STATUS
Finally, you don't need to reenable TOIE each time. Clearing TOIF and executing RETFIE is sufficient.
 
Last edited:

dannyf

Joined Sep 13, 2015
2,197
I've demonstrated that my incremental changes to nCycles give the desired change in interrupt period
That doesn't meant the code is right. It just means that the changes you made didn't impact the error(s). ie. after the changes, your code is consistently wrong.

I would go back and stare at your code a little bit more - the price you pay for coding in assembly.
 

Thread Starter

Robin66

Joined Jan 5, 2016
275
Ok fantastic work fellas. Good spot by NorthGuy. I replaced subwf with sublw. That changed the period but it was constant regardless of what nCycles was set to. Then I enacted JohnIn TX's suggestion: "Finally, you don't need to reenable TOIE each time....". Removing this now means the interrupt period IS a function of nCycles and in exactly the way I originally wanted ie. nCycles=100 --> 23us period. Changes to nCycles result in +20us change to interrupt period per +100 nCycles.

I understand NorthGuy's fix. I don't understand JohnIn TX's fix tho. I guess setting TOIE may interfer with the TMR0 value.

Thx for the help. I've taken onboard all of the suggestions
 

Thread Starter

Robin66

Joined Jan 5, 2016
275
That actually is exactly the wrong answer.

But I can recreate it as well and you can see the difference between this execution and the prior execution.

The difference (and the fix) is easy to spot.
Ok I made the change below and the period is now 20.6us

; movfw nCycles ; this old code introduces 3us period error
; sublw h'FF'
; movwf TMR0

movfw nCycles ; this new code is much closer
subwf TMR0, f
 

JohnInTX

Joined Jun 26, 2012
4,787
Your arithmetic seems to be fixed? Here's a snippet of code (with simplified TMR0 setting) that illustrates ADDING the setting to TMR0 to account for accumulated counts in the timer PLUS the 3 cycle write latency present on TMR0 writes. This runs at 20us on the dot in MPSIM.
See line 6 (addwf TMR0,F). Setting an MPSIM breakpoint at this location (just before the addwf happens) shows that TMR0 has already accumulated 6 counts after rollover. If you MOVWF to TMR0 with your calculated value, you lose those 6 counts and your time will be longer than expected.

Code:
    ORG     0x004             ; interrupt vector location
    movwf   w_temp            ; save off current W register contents
    swapf    STATUS,W          ; swap status register into W (does not affect flags)
    movwf    status_temp       ; save swapped STATUS register
    movlw   .256 -.97         ; reset timer to 100 tiks-to-rollover less 3 tik latency (200ns * 100 = 20uS)
    addwf TMR0,F              ; see Midrange Family Reference Manual re: latency

    call isr                  ; this should be inline instead of called

    bcf INTCON,T0IF
    ;bsf INTCON, T0IE ; enable TMR0 interrupt NOT NECESSARY


    swapf   status_temp,W     ; swap STATUS register back to W
    movwf    STATUS            ; restore pre-isr STATUS register contents
    swapf   w_temp,f
    swapf   w_temp,w          ; restore pre-isr W register contents without changing flags
    retfie                    ; return from interrupt
From the manual:
Any write to the TMR0 register will cause a 2 instruction cycle (2TCY) inhibit. That is, after the
TMR0 register has been written with the new value, TMR0 will not be incremented until the third
instruction cycle later (Figure 11-2). When the prescaler is assigned to the Timer0 module, any
write to the TMR0 register will immediately update the TMR0 register and clear the prescaler
. The
incrementing of Timer0 (TMR0 and Prescaler) will also be inhibited 2 instruction cycles
(TCY). So
if the prescaler is configured as 2, then after a write to the TMR0 register TMR0 will not increment
for 4 Timer0 clocks (Figure 11-3). After that, TMR0 will increment every prescaler number of
clocks later.
That's why using TMR2 / PR2 is easier.
 
Last edited:

Thread Starter

Robin66

Joined Jan 5, 2016
275
Thx a lot JohnIn TX. I'll read up more about TMR2 config tomorrow while I'm at work. I've only worked with a 16F628a previously so I'm still learning about the extra peripherals+config for the 882. I'll be using the interrupts to control the charging of an inductor for charge transfer between cells in an Li-ion array so I'm not worried about timing precision. But the TMR0 errors I was first experiencing were so large that I clearly didn't understand how my code was working.

Once again. thx a lot guys
 
Top