PIC Micro Controller Interrupt Introduction

Via the 16F877 by William Chops Westfield

On a PIC, and especially a PIC16, I think the basic principles of interrupts are obscured by the complexities of how they work on PIC16s; details about saving state, handling paging, Read-only vectors, and so on. On some other CPUs, things are much simpler.

So, stepping backwards...

"Hardware Interrupts" are a mechanism for providing faster (more immediate) software servicing of particular hardware events. Typically the CPU hardware will check some hardware status after each CPU instruction (for CISC machines for potentially long-running single instructions, sometimes even with a single instruction.) If the hardware needs attention, the CPU will transfer execution to a special piece of software called "the Interrupt Service Routine" (ISR). The ISR can do whatever is necessary to service the hardware, and then it returns to the point where the main software was interrupted, and continues whatever it was doing before.

So what has to happen in order for that to work?

Well, at the hardware level, we want to be able to turn this sort of thing on and off at a global level. Some things should just not be interrupted! Other things shouldn't cause interrupts before all the pieces associated with them are set up properly. On the PIC (16F877), this global enable/disable of the interrupt function is controlled by the GIE bit of the INTCON register.

But before that can work, we have to route the signals that we want to cause interrupts to the appropriate gates, and others that we don't want to cause interrupts need to be set up NOT to. In this case, this is also done via setting or clearing other bits in the INTCON register. See figure 12-9 of the 877 manual for the cascade of gates involved.

And we have to set up the destination address(es) of the ISR code functions. On PICxx877 this is easy; it goes to location 4. Other processors may have multiple locations for multiple interrupt sources, or have external interrupt controllers than you need to poke at...

So if we want RB0 and TM0 to cause interrupt, somewhere in our code initialization we will have code that looks something like:

   bsf INTCON,T0IE  ;enable TMR0 interrupt
   bsf INTCON,INTE  ;enable B0 interrupt
   bsf INTCON,GIE   ; enable interrupt globally.

Now we're ready to do anything we want, and hope we get interrupted by important events!

But wait; how is this going to work from a SW point of view, if the ISR has to continue what we were doing when the ISR is done? The interrupt as a whole will have to preserve everything about the state of the CPU that was relevant to what the main code was doing at the time the interrupt occurred. PC address. Register contents. Status flags for the multi-precision math or conditional jump/skip I was about to take, and so on. All of that will need to be saved when an interrupt occurs, and restore to the previous state when the ISR finishes. This is not TOO much different than a subroutine call, but it is less voluntary than a subroutine, and so must preserve things more exactly (your subroutine can say "I trash the flags values", and you're supposed to deal with that.)

The Saving of State is divided up between what the hardware does, and what the ISR software you write has to do. On many CPUs, the interrupt hardware will push both the return address and the status flags onto the stack, for example. On some CPUs, it'll do that AND switch you to another set of registers reserved just for used by ISRs. However, on the PIC16, the stack is only for return addresses, so that's all that gets saved for you automatically. You suddenly arrive at location 4, and you're NOT ALLOWED TO MAKE ANY CHANGES THAT YOU CAN'T REVERT on exit. And you don't have a general purpose stack. The other thing that has been done for you is that interrupts are now globally OFF, which prevents things like new interrupts showing up and wanting to save (now different) state in the same variables. Here is where this bit of code comes in:

 org 0x4
ContextSave
   movwf          temp_w       ;save w in temp.  movwf does not
change STATUS
                               ;; NOTE However, that temp_w must
exist in all RAM banks.
   swapf          STATUS,w    ;put unchanged STATUS in w with nibbles
reversed.
   clrf           STATUS      ;now use RAM page 0
   movwf          temp_s       ;save STATUS in temp_s
   movf           PCLATH,W     ; save PCLATH as well
   movwf          temp_pclath
   clrf           PCLATH       ; now on code page 0 for sure too.
   ;;; Now the important and hard-to-do-without registers have been
   ;;; saved and reset as appropriate for continuing to run the ISR 
   ;;; code here in low memory.

Now, since all interrupts came to location 4, we have to check which one it might have been and jump to appropriate smaller bits of code. this is usually done by examining the INTCON register(s) some more:

Dispathc_int
   btfsc INTCON, RBIE  ;; check for RB0 interrupt
    goto  RB0INTCODE
   btfsc INTCON, T0IF  ;; check for timer interrupt
    goto  TIMERINT
;;; Shouldn't be here; just get out by falling through

INTEXIT
  MOVF     temp_pclath, W ;Restore PCLATH
  MOVWF    PCLATH         ;Move W into PCLATH
  SWAPF    temp_s,W       ;Swap STATUS_TEMP register into W
                            ;(sets bank to original state)
  MOVWF    STATUS        ;Move W into STATUS register
  SWAPF    temp_w,F      ;Swap temp_w
  SWAPF    temp_w,W      ;Swap temp_w into W
   retfie

RB0INTCODE   ;; we got a signal on RB0.  Do the right things!
    ;  :  Blah
    ;  :  Blah
    goto INTEXIT  ;get out

TIMERINT
       incfz TIMERTICS_LOW
        goto INTEXIT
       inc TIMERTICS_HIGH
        goto INTEXIT

The ISR code has to have SOME state (memory variable) it can manipulate somewhere in the processor. That's fine as long as it's not state that the main program also manipulates. If there is data that has to be shared between ISR code and main Code, you have to be VERY CAREFUL about how and when it is manipulated. The easiest thing to do is surround the main code that touches this data with instructions that turn off interrupts (globally) while the main code is working with them (and then turns them on again when its done.)

Since our ISR code is done, we now know where the startup code can go:

StartupCode:
     ;; blah

And hopefully we can set up the reset vector to go there:

   org 0x0
   movlw  HIGH(StartupCode)  ; 0
   movwf  PCLATH             ; 1
   goto   StartupCode        ; plenty of room; won't run into our ISR code.

(mind you this probably has errors; I haven't actually tried this, and it's been a while since I coded a PIC in assembler. But I hope the explanation helps some...)

BillW