ISRContext

There are a few excellent sources of information regarding the topic of interrupt context, and those are the Swordfish Manual / IDE help (see sections on Interrupts, Events, Frame Recycling, and Context Saving), and the PIC 18F Interrupts in Swordfish article by RadioT. If your device has a Vectored Interrupt Controller module (VIC) then you should also check out the article Using Vectored Interrupts with Swordfish for more details on vectored interrupts.

This article will discuss some additional information and hints to expand on those resources.

Note: To follow some of the the discussion below you may have to examine the asm output of the compiler. After compiling, open an IDE assembler view (F2 function key), or just open the .asm or .lst file with a text editor. Scroll down/search to find org 0x00, which is the program reset location. The statements following org 0x08 and org 0x18 show the vector locations for the high-priority and low-priority interrupt routines respectively.

    ORG 0X00                ; program reset  vector
    GOTO SBCDSTD            ; main program startup
    ORG 0X08                ; high-priority intr vector
    BRA ISR_HIGHP_ISR_2     ; goto ipHigh handler
    ORG 0X18                ; low-priority intr vector
    BRA ISR_LOWP_ISR_5      ; goto ipLow handler

With regards to interrupts and context saving, there are a few topics to address. These are:

  • processor registers
  • save/restore blocks
  • SF system variables/library functions
  • subroutines and frame recycling
  • events
  • stack depth

Processor Registers

Interrupt handlers must save and restore all processor registers used in an ISR.

The standard 18F series devices have one level of hardware shadow registers for context saving. If a single ISR is declared in your program, the STATUS, WREG, and BSR are saved and restored in hardware shadow registers. If both high and low priority ISRs are declared within the same program, the high priority interrupt will save and restore STATUS, WREG, and BSR using the hardware shadow registers, but the low priority interrupt must save and restore STATUS, WREG, and BSR in software since there is only a single set of shadow registers.

The 18XV core devices (K42, K83, and most of the Q family) improve on this. In these devices there are two levels of hardware shadow registers, one each for high and low priority ISRs, and the shadow registers are greatly expanded to include: STATUS, WREG, BSR, FSR0/1/2, PRODL/H and PCLATH/U. You can identify the 18XV core devices from the device 18F*.bas include file entry #define _xv18 = 1.

The hardware shadow registers are restored at the end of an ISR via an asm RETFIE 1 instruction (aka RETFIE FAST), whereas an asm RETFIE instruction just re-enables interrupts and returns without restoring the shadow registers, which assumes that the registers were saved/restored in software.

ISR_SHADOW options

You can force the compiler to perform context saving in software for both high and low priority interrupts, rather than using the hardware shadow registers.

For standard 18F devices this is achieved via the ISR_SHADOW option:

  // enable hardware shadow register usage... ipHIGH uses shadow registers,
  // ipLOW saved in software (this is the default setting if not specified) 
  #option ISR_SHADOW = true

  // disable all hardware shadow register usage... ipHIGH and ipLOW saved in software
  #option ISR_SHADOW = false

For 18XV core devices, ISR_SHADOW has been augmented with two individual options- ISR_SHADOW_HIGH and ISR_SHADOW_LOW, which can be used to select the context save method for high and low priority interrupts respectively:

true = use hdw shadow registers (return via RETFIE 1)
false = use software context save (save using MOVFF/MOVFFL and return via RETFIE)

The default setting for these two options is the same as using #option ISR_SHADOW=true (ISR_SHADOW_HIGH=true, ISR_SHADOW_LOW=false), so for best performance with 18XV devices you should set both #option ISR_SHADOW_HIGH=true and #option ISR_SHADOW_LOW=true

  #option ISR_SHADOW_HIGH = true        // enables ipHIGH hardware shadow register usage
  #option ISR_SHADOW_LOW = true         // enables ipLOW hardware shadow register usage

Here's an example of a typical asm output for a standard 18F low-priority intr using software context save. As you can see, it starts out with saving the STATUS, WREG, and BSR registers.

ISR_LOWP_ISR_5
    MOVFF STATUS,F0_U08
    MOVFF WREG,F1_U08
    MOVFF BSR,F2_U08

Save/Restore blocks

The save statement can be used to save a variable, register, subroutine, or function context. Multiple items can be included as part of the save statement, and specifying a constant '0' will save the compilers system variables. Note that in addition to saving the system variables, save(0) will also save the FSR0, FSR1, and PRODH/PRODL processor registers. The save statement should typically be used on entry to the ISR, and restore called prior to exit. examples:

    save(0)                     // save system registers, FSR0, FSR1, and PROD
    save(FSR2)                  // save FSR2 register (FSR2H/FSR2L)
    save(FSR0, FSR1, mysub)     // save local variables for 'mysub' (see subroutine discussion below)
    save(0, FSR2, mysub)        // save all of the above

SF Register Usage

PROD (PRODH/PRODL) - hardware multiply instruction result

  • used by SF multiply statements (ie 'b = a * c'), but may also be used in computing the address of array elements, especially with arrays of structures (ie 'myStruct[ix].b = 3').
  • some libraries may also use this as a temp variable.
  • asm references: MULLW, MULWF instructions

FSR0/FSR1/FSR2 (FSRxH/FSRxL) - indirect RAM address pointer registers

  • used with array, string, and structure variables located in RAM
  • FSR0 and FSR1 may also be used when saving/restoring the interrupt context (system registers, subs/functions)
  • these registers are also aliased as A0/A0H (FSR0), A1/A1H (FSR1), and A2/A2H (FSR2) by the compiler
  • FSR2 is not typically used by the compiler itself, but it may be used by library modules.
  • asm references: LFSR instruction and FSRxH/xL, INDFx, POSTINCx, POSTDECx, PREINCx, PLUSWx registers

TBLPTR (TBLPTRU/TBLPTRH/TBLPTRL) and TABLAT - table read/write address and data registers

  • used to access const data/strings in PFM (program flash memory)
  • lower 16-bits (TBLPTRH/TBLPTRL) aliased as TABLEPTR
  • may also be used on some devices to access other special areas such as SAF and CONFIG data
  • asm references: TBLRD, TBLWR instructions

PCLAT (PCLATH/PCLATU) - used by the SF event() mechanism to invoke/call an event handler

  • PCLATH/PCLATU registers will automatically be saved in software if events are used, so these registers can be safely ignored.

The device include file defines aliases for the common 16-bit registers:

// sfr alias...
public dim
   PROD as PRODL.AsWord,        // PRODL, PRODH
   FSR0 as FSR0L.AsWord,        // FSR0L, FSR0H
   FSR1 as FSR1L.AsWord,        // FSR1L, FSR1H
   FSR2 as FSR2L.AsWord,        // FSR2L, FSR2H
   TABLEPTR as TBLPTRL.AsWord   // TBLPTRL, TBLPTRH
public dim
   PRODW as word absolute $0FF3,
   FSR0W as word absolute $0FE9,
   FSR1W as word absolute $0FE1,
   FSR2W as word absolute $0FD9,
   TABLEPTRW as word absolute $0FF6

The table pointer registers (TBLPTRU/TBLPTRH/TBLPTRL) are used to access flash memory space. The 24-bit value can point to the full 16M address space of the 18F, although typically only the first 64K or 128K is used for the program memory space. Table read and write operations use the TABLAT register as the data register.

Normally, SF places const data and strings in the lower 64K of program memory and treats addresses as 16-bit values assuming TBLPTRU=0. Setting #option large_code_model=true can be used to extend this to the full 128K range, and the compiler will set TBLPTRU as appropriate. TABLEPTR is an alias for the lower 16-bits of the table pointer (TBLPTRH/TBLPTRL) and can be used accordingly.

For the larger 128K devices, or to access special areas of the flash memory space the full 24-bit address must be used. If #option large_code_model=true then you should use save(TBLPTRU, TBLPTRH, TBLPTRL, TABLAT), otherwise you can typically just use save(TABLEPTR, TABLAT) to save the TBLPTRH/TBLPTRL pointer and data registers.

Performing table operations is a two-step process, so it is important to save the TABLAT data register to avoid issues with being interrupted in the middle of a table read operation such as the following:

TBLRD*          ; read into TABLAT
MOVF TABLAT, W  ; put TABLAT data previously read into WREG

For 16-bit registers such as FSRx and PROD you can specify either the individual 8-bit registers (ie 'PRODH, PRODL') or the 16-bit alias name ('PROD'), in which case you'll see references to the alias names ('PROD__PRODL#M0_U16H, PROD__PRODL#M0_U16') instead of the actual PRODH, PRODL registers.

To save ALL hardware registers, you can use save(FSR0, FSR1, FSR2, PROD, TBLPTRU, TBLPTRH, TBLPTRL, TABLAT). For the 18XV core devices this can be reduced to save(TBLPTRU, TBLPTRH, TBLPTRL, TABLAT) if using the hardware shadow registers.

SF system variables/library functions

SF reserves approx 25 bytes of ram as system variables for use by various internal library functions, such as multiply and divide routines. These variables are defined in the system.bas include file, and all begin with the prefix 'SB_SV'. System library functions themselves can be identified by the prefix 'SB_'. If your ISR uses any system libraries or calls routines that use them these system variables must be saved as part of the context save/restore via save(0).

Example of a system library call

dim a, b, c as longword     // module-level static variables
a = b * c                   // 32-bit multiply uses SF library call  (SB_MULT32X32)

// ASM output
?I000000_F000_000003_M000000 ; L#MK a = b * c
    MOVFF M4_U32HHH,SB_SV0HHH            // 'SB_SVxxx' system library variable
    MOVFF M4_U32HH,SB_SV0HH
    MOVFF M4_U32H,SB_SV0H
    MOVFF M4_U32,SB_SV0
    MOVFF M8_U32HHH,SB_SV2HHH
    MOVFF M8_U32HH,SB_SV2HH
    MOVFF M8_U32H,SB_SV2H
    MOVFF M8_U32,SB_SV2
    RCALL SB_MULT32x32                  // 'SB_xxxx' system library call
    MOVFF SB_SV0HHH,M0_U32HHH
    MOVFF SB_SV0HH,M0_U32HH
    MOVFF SB_SV0H,M0_U32H
    MOVFF SB_SV0,M0_U32

When viewing the asm output you will notice that the variable names do not seem to match up with the source code. The compiler changes variable names to ensure they are unique, and to allow frame variable placement. Variable names that begin with 'M' are module-level static variables, variable names beginning with 'F' are frame variables, and 'SB_SV' variables are SF system variables.

To aid in understanding some of the variable references, adding #option _showvar = true to your program will prefix the 'M' and 'F' names with your variable name, making it easier to see what's going on. This can, however, make it more difficult to distinguish module-level variables from frame variables.

#option _showvar = true     // add variable name prefixes to the asm output
dim a, b, c as longword     // module-level static variables
a = b * c                   // 32-bit multiply uses SF library call  (SB_MULT32X32)

// ASM output
?I000002_F000_000010_M000000 ; L#MK a = b * c       // 32-bit multiply uses SF library call  (SB_)
    MOVFF B_M4_U32HHH,SB_SV0HHH            // variable names added as a prefix
    MOVFF B_M4_U32HH,SB_SV0HH
    MOVFF B_M4_U32H,SB_SV0H
    MOVFF B_M4_U32,SB_SV0
    MOVFF C_M8_U32HHH,SB_SV2HHH
    MOVFF C_M8_U32HH,SB_SV2HH
    MOVFF C_M8_U32H,SB_SV2H
    MOVFF C_M8_U32,SB_SV2
    RCALL SB_MULT32x32
    MOVFF SB_SV0HHH,A_M0_U32HHH
    MOVFF SB_SV0HH,A_M0_U32HH
    MOVFF SB_SV0H,A_M0_U32H
    MOVFF SB_SV0,A_M0_U32

Module-level vs local variables (subroutines/functions) and frame recycling

Module-level variables (those declared outside any subroutine or function) are static in nature... they exist for the lifetime of the program, are not shared, and are safe to access in an ISR (ignoring the fact that you may still need to protect them from non-atomic access, but that's a different topic). These variables are identified by the 'M' prefix in their names.

Subroutine/function parameters, local, and temp variables are local to the routine and are part of the frame variables... they are accessible only while the procedure is executing. Frame recycling may result in these local variable locations being shared by other routines. If you call subroutines or functions in an ISR then these frame variables must be saved on entry to the ISR. To do this, add the name of the sub/function to the save list. You only need to specify the procedures that are called directly from the ISR... the compiler will account for any additional frame variables used by routines that they may call. However, if you have nested subroutine calls you must examine ALL the routines for processor register and/or system library usage and account for these in the save list. Look for any CALL or RCALL instructions in the ISR asm listing, or references to variable names beginning with 'F' (frame variables).

When adding a subroutine/function to the save list, you should also be aware that saving/restoring the local context of a procedure may involve the use the FSR0 and FSR1 registers. For small amounts of data (up to about 6 bytes) the context is saved directly. However, larger amounts of context data will use the FSR registers, which results in something like this to save the subroutine context:

    LFSR 0,F11_U56              // FSR0 (dest)
    LFSR 1,F0_U56               // FSR1 (src)
    MOVLW 0X07                  // number of bytes to copy
    MOVFF POSTINC1,POSTINC0     // loop copying src to dest
    DECFSZ WREG,1,0
    BRA $ - 6

In this case, instead of just save(mysub) you would also have to save the FSR registers using either save(FSR0, FSR1, mysub) or save(0, mysub)

Events

SF events use an indirect call mechanism by modifying the PCLAT registers (PCLATU/PCLATH). If you use events in your program, SF will automatically add saving the PCLATU/PCLATH registers as part of the ISR entry so no further action is required.

ISR_LOWP_ISR_6
    MOVFF STATUS,F0_U08         // software processor register context save
    MOVFF WREG,F1_U08
    MOVFF BSR,F2_U08
    MOVFF PCLATH,F3_U08         // PCLATx registers saved due to event usage
    MOVFF PCLATU,F4_U08

Any local variables defined in an event() routine are not shared with regular frame variables, even though they are identified as 'F'. They are effectively the same as a module-level static variable ('M').

Stack Depth

This isn't really related to context issues, but you should be aware that standard 18F devices limit the hardware stack to 31 entries, after which the stack 'overflows' and wraps (see device file #define _hwstackdepth). 18XV core devices extend this to a 127 level stack. If your ISR calls procedures, events, or uses system library calls you must account for this, especially if you have both high and low priority interrupts enabled. The sum total of the main code call depth + high_priority depth (+ low_priority depth) cannot exceed this, and the interrupt(s) counts as one additional level. The default device file setting CONFIG STVREN = ON will generate a reset if the stack overflows.

Conclusion

As you can see, saving the interrupt context can be a complex topic. When in doubt, the safest thing to do is just to save everything... save(0, FSR2, TBLPTRU, TBLPTRH, TBLPTRL, TABLAT, <list of all subroutines called>) However, this comes at a performance penalty that will impact interrupt latency. As a general rule of thumb, saving the system registers takes approx 6 cycles/byte, and saving the additional hdw registers could take another 32 cycles for a total of 182 instruction cycles. The restore is the same, so that's 364 cycles to save/restore the above, and that's not accounting for any user subroutines you might add to the list. Even at 64MHz that's an additional 22us penalty just for the context operations!

Code Examples

Here are some common operations and the typical assembler code output to demonstrate what resources might be used and what to watch out for. Note: in some of the examples I have removed asm output to make it easier to see the instructions of interest.

example 1 - array in ram using a variable as index

dim barray(32) as byte
dim b, ix as byte

ix = 1
b = barray(ix)

// ASM output
?I000000_F000_000111_P000006 ; L#MK ix = 1
    MOVLW 0X01
    MOVWF IX_M33_U08,0
?I000001_F000_000112_P000006 ; L#MK b = barray(ix)
    LFSR 1,M0_U32              // FSR1
    MOVF IX_M33_U08,0,0
    ADDWF A1,1,0               // A1 is an alias for FSR1
    MOVLW 0X00
    ADDWFC A1H,1,0
    MOVFF INDF1,B_M32_U08      // INDF1 uses FSR1

Used in an ISR, the context would require save(FSR1)

example 2 - const data array in program memory using a variable as index

const carray(4) as byte = (1, 2, 3, 4)
dim b, ix as byte

ix = 1
b = carray(ix)

// ASM output
?I000000_F000_000119_P000006 ; L#MK ix = 1
    MOVLW 0X01
    MOVWF IX_M1_U08,0
?I000001_F000_000120_P000006 ; L#MK b = carray(ix)
    <code removed>
    MOVFF F0_ADRHH,TBLPTRU     // const data access uses TBLPTR registers
    MOVFF F0_ADRH,TBLPTRH
    MOVFF F0_ADR,TBLPTRL
    TBLRD*+                    // TBLRD use TBLPTR, TABLAT
    MOVFF TABLAT,B_M0_U08

Used in an ISR, the context would require save(TBLPTRU, TBLPTRH, TBLPTRL, TABLAT)

example 3 - string array in ram

dim sarray(10) as string
dim ix as byte
dim s as string

ix = 1
s = sarray(ix)

// ASM output
?I000000_F000_000130_P000006 ; L#MK ix = 1
    MOVLW 0X01
    MOVWF IX_M72_U08,1
?I000001_F000_000131_P000006 ; L#MK s = sarray(ix)
    <code removed>
    MULLW 0X18                 // MUL instruction uses PRODH, PRODL
    MOVFF PRODL,F28_U16        // PRODL
    MOVFF PRODH,F28_U16H       // PRODH
    <code removed>
    MOVFF F24_ADRH,A0H         // A0 is an alias for FSR0
    MOVFF F24_ADR,A0
    MOVFF F30_ADRH,A1H         // A1 is an alias for FSR1
    MOVFF F30_ADR,A1
    MOVF POSTINC1,0,0          // POSTINC1 uses FSR1
    BZ $ + 6
    MOVWF POSTINC0,0           // POSTINC0 uses FSR0
    BRA $ - 6
    CLRF INDF0,0               // INDF0 uses FSR0

Used in an ISR, the context would require save(PRODH, PRODL, FSR0, FSR1)

example 4 - const string array in program memory

const csarray(5) as string = ("this ", "is ", "a ", "const ", "array")
dim ix as byte
dim s as string

s = csarray(2)      // fixed index
ix = 1
s = csarray(ix)     // variable index

// ASM output
?I000000_F000_000141_P000006 ; L#MK s = csarray(2)    // fixed index
    LFSR 0,M1_U192                          // FSR0
    MOVLW ((_DB_CSARRAY_0#16 >> 16) & 255)
    MOVWF TBLPTRU,0                         // TBLPTRU, TBLPTRH, TBLPTRL
    MOVLW ((_DB_CSARRAY_0#16 >> 8) & 255)
    MOVWF TBLPTRH,0
    MOVLW (_DB_CSARRAY_0#16 & 255)
    MOVWF TBLPTRL,0
    RCALL SB_LOAD_MEMSTRL                   // system library call (uses TBLPTR, TABLAT and FSR0)
    CLRF INDF0,0                            // INDF0 uses FSR0

?I000001_F000_000142_P000006 ; L#MK ix = 1
    MOVLW 0X01
    MOVWF IX_M0_U08,0
?I000002_F000_000143_P000006 ; L#MK s = csarray(ix)    // variable index
    <code removed>
    MOVFF F24_ADRH,A0H              // A0 is an alias for FSR0
    MOVFF F24_ADR,A0
    MOVFF F30_ADRHH,TBLPTRU         // TBLPTRU, TBLPTRH, TBLPTRL
    MOVFF F30_ADRH,TBLPTRH
    MOVFF F30_ADR,TBLPTRL
    RCALL SB_LOAD_MEMSTRL           // system library call (uses TBLPTR, TABLAT and FSR0)
    CLRF INDF0,0                    // INDF0 uses FSR0

Used in an ISR, the context would require save(0, TBLPTRU, TBLPTRH, TBLPTRL, TABLAT)

example 5 - array of structures using a variable as index

structure mystruct_t
    b as byte
    w as word
    lw as longword
end structure

dim struct_array(10) as mystruct_t
dim mystruct as mystruct_t
dim ix as byte

ix = 1
mystruct = struct_array(ix)
mystruct.w = struct_array(ix).w

// ASM output
?I000001_F000_000160_P000007 ; L#MK ix = 1
    MOVLW 0X01
    MOVWF IX_M81_U08,1
?I000002_F000_000161_P000007 ; L#MK mystruct = struct_array(ix)
    MOVF IX_M81_U08,0,1
    MULLW 0X07                 // MUL instruction uses PRODH, PRODL
    MOVFF PRODL,F38_U16        // PRODL
    MOVFF PRODH,F38_U16H       // PRODH
    LFSR 1,M4_U32              // FSR1
    MOVF F38_U16,0,0
    ADDWF A1,1,0               // A1 is an alias for FSR1
    MOVF F38_U16H,0,0
    ADDWFC A1H,1,0
    LFSR 0,MYSTRUCT_M74_U56    // FSR0
    MOVLW 0X07
    MOVFF POSTINC1,POSTINC0    // POSTINC1 uses FSR1, POSTINC0 uses FSR0
    DECFSZ WREG,1,0
    BRA $ - 6
?I000003_F000_000162_P000007 ; L#MK mystruct.w = struct_array(ix).w
    MOVF IX_M81_U08,0,1
    MULLW 0X07                 // MUL instruction uses PRODH, PRODL
    MOVFF PRODL,F38_U16
    MOVFF PRODH,F38_U16H
    LFSR 1,M4_U32              // FSR1
    MOVF F38_U16,0,0
    ADDWF A1,1,0               // A1 is an alias for FSR1
    MOVF F38_U16H,0,0
    ADDWFC A1H,1,0
    INFSNZ A1,1,0
    INCF A1H,1,0
    MOVFF POSTINC1,W_M75_U16   // POSTINC1 uses FSR1
    MOVFF POSTINC1,W_M75_U16H

Used in an ISR, the context would require save(PRODH, PRODL, FSR0, FSR1)

example 6 - functions with local frame variables and system lib calls

function mult_lw(pA, pB as longword) as longword
    // 32-bit multiply uses SF library call (SB_MULT32X32) which uses the hdw MUL
    result = pA * pB    // frame variables
end function

function mult_b(pA, pB as byte) as word
    // 8-bit multiply is typically done inline (no library call)
    result = pA * pB    // frame variables
end function

// ASM output
PROC_MULT_LW_0
?I000000_F000_000006_P000001 ; L#MK result = pA * pB    // frame variables
    MOVFF F0_U32HHH,SB_SV0HHH       // 'SB_SVxxx' system library variable
    MOVFF F0_U32HH,SB_SV0HH
    MOVFF F0_U32H,SB_SV0H
    MOVFF F0_U32,SB_SV0
    MOVFF F4_U32HHH,SB_SV2HHH
    MOVFF F4_U32HH,SB_SV2HH
    MOVFF F4_U32H,SB_SV2H
    MOVFF F4_U32,SB_SV2
    RCALL SB_MULT32x32              // 'SB_xxxx' system library call (uses hdw MUL)
    MOVFF SB_SV0HHH,F8_U32HHH
    MOVFF SB_SV0HH,F8_U32HH
    MOVFF SB_SV0H,F8_U32H
    MOVFF SB_SV0,F8_U32
?I000001_F000_000007_P000001 ; L#MK end function
    RETURN 0

PROC_MULT_B_2
?I000002_F000_000011_P000002 ; L#MK result = pA * pB    // frame variables
    MOVF F0_U08,0,0
    MULWF F1_U08,0                  // MUL instruction uses PRODH, PRODL
    MOVFF PRODL,F2_U16
    MOVFF PRODH,F2_U16H
?I000003_F000_000012_P000002 ; L#MK end function
    RETURN 0

The function 'mult_lw()' uses a system library call and variables (SB_MULT32x32 and SB_SVxxx), a library call which uses the hdw MUL instruction (PRODH/PRODL registers), and local frame variables ('F'). If 'mult_lw' is called from an ISR, the context would require save(0, mult_lw)

The function 'mult_b()' does not use any system library calls or variables, but it does use hdw MUL instruction (PRODH/PRODL registers), and local frame variables ('F'). If 'mult_b' is called from an ISR, the context would require save(PRODH, PRODL, mult_b)