Working with Microchip PIC 8-bit Interrupts
Welcome to the fifth and final post of the getting started with Microchip's 8-bit family of processors series. This post ties together the concepts covered in the first four posts with the implementation of a 4-bit binary counter whose value is output to the four LEDs on Curiosity HPC Development Board. The counter is incremented once per second. To accomplish code relies on basic process configuration, general purpose input and output (GPIO) and timer peripherals as well as the newly introduced topic of PIC interrupts.
The completed code in MPLAB X project format can be downloaded from GitHub.
PIC®, MPLAB® X, PICkit™ 4 On-Board, and PICkit™ 4 are trademarks of Microchip Technology Inc.
All data sheet references are to Microchip document PIC18F27_47Q10-data-sheet-40002043C, © 2019 - Microchip Technology Inc, available here at the time this post was published.
All compiler manual references are to Microchip document DS-50002737J - MPLAB XC8 C Compiler User’s Guide available in the v3.00 compiler (Windows) install folder (in the doc subfolder).
Background
Previous posts in this series are available here:
- Getting Started with the Microchip PIC Microcontroller
- Understanding Microchip 8-bit PIC Configuration
- Working with Microchip PIC 8-bit GPIO
- Introduction to PIC Timers
Some concepts covered in previous posts including basic timer behavior, PIC 8-bit configuration, using the MPLAB X IDE and project structure are not discussed in this post; please refer to one or more of the previous posts in the series for additional detail. The development board is the previous introduced Microchip Curiosity HPC Development Board. As that development board supports various 8 bit PIC processors, the post also continues using the PIC18F47Q10 in the 40 pin PDIP package. The development environment (and related screen shots) is MPLAB X v6.25 on Microsoft Windows.
PIC Program Execution
Prior to delving into interrupts, a brief, high level discussion of how programs execute on PIC high end 8-bit processors provides context. A previous post discussed memory organization of the processor family, specifically how dedicated registers, known as Special Function Registers (SFRs) manage the configuration and operation of various processor peripherals, e.g. serial ports, timers, GPIO ports, etc. A similar set of dedicated registers exist for managing processor operation. These registers are documented in the processor data sheet under Section 10 Memory Organization, specifically 10.7 - Register Summary: Memory and Status and 10.8 - Register Definitions: Memory and Status .
The Program Counter (PC) in the PIC18 family is 21 bits wide (allowing the PC to address up to a 2Mbyte program memory space) realized through three 8-bit registers: PCL, the low byte comprising PC<7:0>, PCH, the high byte comprising PC <15:8> and PCU, the upper byte comprising PC<20:16>. The PC contains the address, in the program memory space, of the instruction to fetch for execution. While PCL is readable / writable, there is typically no reason for applications to write to this register. PCH and PCU are not directly readable / writable. On a processor reset, the PC is reset to program memory address 00 0000h (also known as the reset vector, a vector being a term for a memory location or address) where processor start up code resides. During the normal course of program execution the PC is either incremented to load the next sequential instruction or in the case of the program calling a function or altering linear program flow due to a conditional result (if block) or a loop (while, for) the instruction may load another address into the PC. As detailed further below, the occurrence and servicing of an interrupt loads the appropriate address for processing the interrupt into the PC.
PIC Interrupts
Consider you are listening to music. Someone knocks on your front door. You pause the music, go to the front door, get a package from the delivery person, and return to listening to music. This sequence of events provides a good analogy of microprocessor interrupts . The processor is running an application (equivalent to your listening to music) when a peripheral generates an interrupt (the knock at your door). The processor then pauses the current running application, saving the current state such as the PC, stack contents, registers, etc (similar to pausing the music) to run the specific interrupt handling code (answering the door). Once interrupt processing is complete (you get your package) the processor restores the previously saved state and resumes from where the application was paused (you return to listening to music).
The benefits of interrupts is that the processor can perform other tasks without miss an important event, such as a timer match. As illustrated in the previous post on PIC timers without interrupts the code needed to continually check for a timer bit to toggle to in order to toggle the state of the LEDs. With interrupts, the application no longer needs to poll a bit within a timer register to determine when to toggle the LED state but rather waits until notification of the match is sent via an interrupt to update the LED state.
Interrupt Types
Interrupts can be implemented in many ways. A very limited set of the PIC18 family of High Performance processors (specifically only the PIC18FK42 family) as well as many of the 16-bit PIC families implement vectored interrupts. Other members of the PIC18 High Performance family, including the PIC18F47Q10 used in this article, support two interrupt vectors supporting prioritization. Mid-range devices typically have a single interrupt vector. Many devices in the baseline family do not have any interrupt capabilities.
Applications implement Interrupt Service Routines (ISRs) to handle interrupts. These are specially annotated functions which the compiler and linker place at specific locations within the application space based on the interrupt architecture and implementation for the target processor. ISRs are typically compiled with additional instructions such as disabling interrupts on entry and re-enabling interrupts on exit added by the compiler for proper behavior of interrupts.
Vectored Interrupts
A vectored interrupt allows the application to implement a specific function for each interruptdource, e.g., one interrupt handler exists to handle the interrupt sent when the serial receive buffer is full and another interrupt handler when a timer match occurs. Returning to the earlier analogy, the answering of the door would be a vectored interrupt. You would have a separate set of discrete actions when the alarm clock rings in the morning (interrupting you sleep) or when the smoke alarm goes off. Vectored interrupts are a bit more complicated to implement within the actual processor silicon. The controlling logic (implemented in a Vectored Interrupt Controller, VIC) needs to maintain a mapping between the available interrupt types and the address of the handlers (known as the interrupt vector table) as well as logic to find and branch to the address of the handler within that map when an interrupt occurs. In a vectored interrupt implementation the application developer writes an ISR for each of the interrupts to be handled using compiler specific annotations. The compiler and linker then build out the interrupt vector table. Various implementation of vectored interrupts allows the developer to assign and manage priorities to individual interrupts allowing more urgent events to take priority. An in-depth investigation of vectored interrupts is outside the context of this post as the PIC18F47Q10 processor does not support vectored interrupts.
Single Vector Interrupt
A single interrupt vector implementation provides for a single memory location (vector) for handling all interrupts regardless of the source of the interrupt. This implementation, while simpler to implement in the processor, is less efficient as conditional logic needs to be written in the single ISR to determine which interrupt(s) occurred. Once the interrupt source is determined, logic can branch within the ISR to the appropriate block of logic for handling the specific interrupt. While some architectures and implementations of single vector interrupts allow nesting of interrupts, that is interrupting an existing interrupt, the 8 bit PICs with single interrupt vectors are not designed to supporting nested interrupts. If there is a need for certain interrupts to take priority over processing of other interrupts, one option is moving to a PIC family (such as the PIC18 family of which the PIC18F47Q10 is a member) supporting prioritized interrupts.
Priority Interrupt Vectors
Priority Interrupt Vectors are very similar in implementation to the Single Vector Interrupt in that there is still a single vector called regardless of the interrupt. However, each interrupt can be assigned to one of two priorities, high or low, each with a unique vector and ISR. When a high priority interrupt occurs during the processing of a low priority interrupt, the lower priority interrupt will suspend while the higher priority interrupt is processed. In other words, a high priority interrupt interrupts a lower priority interrupt. This approach provides the application developer a means, albeit somewhat limited, to provide precedence for certain interrupts.
PIC18F47Q10 Interrupts
Operating Modes
The PIC18F47Q10 implements Prioritized Interrupts with two priorities: low and high. The low priority interrupt vector address is 0008h while the high priority interrupt vector address is 0018h The PIC18F47Q10 can also be configured to operate in Midrange Compatibility mode which provides only a single interrupt priority with all interrupts branching to the high priority interrupt vector address. Midrange Compatibility mode is enabled by setting the IPEN (Interrupt Priority ENable) bit (bit 5) of the INTCON (INTerrupt CONtrol) SFR register to 0.
The value of the IPEN bit in the INTCON SFR also determines the behavior of both the GIE/GIEH (bit 7) and the PEIE/GIEL (bit 6) bits in the INTCON SFR. When IPEN = 1 (priorities enabled) bit 7 acts as the GIEH (Global Interrupt Enable High priority) flag - enabling / disabling all unmasked interrupts. Likewise, bit 6 acts as the GIEL (Global Interrupt Enable Low priority) flag enabling all low priority interrupts. (An unmasked interrupt is one in which the interrupts’s flag in the respective PIE SFR is enabled, or unmasked.)
As detailed in the subsequent Interrupt Execution section, hardware manages the interrupt enabled flag when an interrupt is processed. When interrupt priorities are enabled, hardware clears the GIEH bit only for high priority interrupts. The occurrence of a low priority interrupt does not clear this bit thereby allowing the occurrence of higher priority interrupt during the processing of the low priority interrupt to preempt the execution of the lower priority ISR. If a high priority interrupt occurs, this flag is disabled by hardware such that the high priority executing ISR is not preempted by either a new high or low priority interrupt. In a similar manner, when a low priority interrupt occurs, hardware clears the GIEL bit ensuring any subsequent low priority interrupt will not preempt execution of the ISR but as the GIEH bit is not cleared, the ISR is preempted on the occurrence of a high priority interrupt.
When IPEN = 0, bit 7 of the INTCON SFR acts as the GIE ( Global Interrupt Enable) bit - enabling / disabling all unmasked interrupts and bit 6 acts as the PEIE (PEripheral Interrupt Enable) bit enabling / disabling all unmasked peripheral interrupts. PEIE is only enabling peripheral interrupts as there are interrupt sources other than peripherals. When operating in Midrange Compatibility interrupt mode ( IPEN = 0) both the GIE and PEIE bits need to be set to enable peripheral interrupts. There is one exception to needing to set the PEIE bit for peripheral interrupts. When using Timer0 (a peripheral), the PEIE bit does not need to be set to enable the Timer0 interrupt, only the GIE bit needs to be set as shown in data sheet:
Interrupt Execution
A peripheral sets its related Interrupt Request Flag in an SFR. When this occurs and interrupts are configured and enabled, the following sequence of events occurs:- Global Interrupts are disabled
- Current program context is saved
- Current PC value is stored on the return stack
- Program control transfers to the appropriate interrupt vector address (high or low priority)
- ISR executes (and returns)
- Global Interrupts are enabled
- Previous saved program context is retired
- Previously saved PC value is loaded
- Execution resumes from the point at which the interrupt occurred
Concurrent Interrupt Execution
What happens when a second peripheral sets an Interrupt Request Flag while an ISR is executing? The answer is dependent upon whether the processor is configured to operate in Prioritized or Midrange Compatibility Mode.Midrange Compatibility Mode Interrupts
When operating in legacy mode and a second peripheral sets an Interrupt Request Flag, the flag remains set until cleared by the application. When the ISR returns, the processor detects the new Interrupt Request Flag and calls the ISR again.
Priority Interrupts
When priority interrupts are enabled and a second peripheral sets an Interrupt Request Flag, if the interrupt being processed is of lower priority than the new interrupt request, application control is transferred to the high priority interrupt vector address. When the higher priority ISR completes, control is returned to the low priority ISR. If the new interrupt request is of the same or lower priority, the new request flag remains set while the current ISR executes. When the ISR exits the processor detects the new Interrupt Request Flag and calls the ISR again.
Implementing Interrupts
The steps for implementing interrupts on the PIC18F47Q10 processor is dependent on the mode - either Priority Mode or Midrange Compatibility Mode.Configuration - Midrange Compatibility Mode
- Clear the IPEN bit (bit 5 - INTCON SFR)
- Enable the specific interrupts by setting the enable bit for the peripheral in the appropriate PIEx ( Peripheral Interrupt Enable) SFR - there are seven PIEx registers - PIE0 - PIE6. As an example, the datasheet documents PIE3 as follows:
- Set the PEIE bit (bit 6 INTCON SFR) to enable peripheral interrupts
- Set the GIE bit (bit 7 INTCON SFR) to enable all interrupts.
Configuration - Priority Mode
- Set the IPEN bit (bit 5 - INTCON SFR)
- Enable the specific interrupts by setting the enable bit for the peripheral in the appropriate PIEx (Peripheral Interrupt Enable) SFR - there are seven PIEx registers - PIE0 - PIE6. As an example, the datasheet documents PIE3 as follows:
- Assign the priority for the enabled interrupts in the appropriate IPRx ( Interrupt PRiority) SFR(s). As an example, the data sheet documents IPR3 as:
- Set the GIEL bit (bit 6 INTCON SFR) to enable low priority interrupts.
- Set the GIEH bit (bit 7 INTCON SFR) to enable high priority interrupts.
Handling Interrupts
One or two Interrupt Service Routines (ISR) need to be implemented to process interrupts depending on the interrupt operation mode selected. The XC8 compiler has a specific format / signature for designating an ISR:
void __interrupt(type) interruptHandler(void)
where (for XC8 - PIC18F47Q10) type is one of two values
specifying the priority of interrupts handled by the service routine:
- high_priority
- low_priority
See the XC 8 Compiler User’s Guide referenced in the introduction for additional details about ISRs.
Recall that a single interrupt vector is used to handle all interrupts of the specified priority. The first thing the ISR implementation typically does is determine which peripheral invoked the interrupt. The peripheral invoking the interrupt has its interrupt flag set in the associated PIRx (Peripheral Interrupt Request) SFR. As an example, the data sheet documents PIR3 as:
Once the peripheral invoking the interrupt is determined, the necessary logic to handle the interrupt can be implemented. As part of that logic it is important to clear the PIRx flag once the interrupt is processed. The data sheet will indicate if the hardware clears the flag - such as in the case of the RC2IF, RC1F, TX2IF and TX1IF flags - however, typically code within the ISR directly clears the flag.
Additional best practices for ISR implementations are included in Microchip’s MPLAB XC8 C Compiler User’s Guide for PIC MCU v2.46, Section 5.9.1:
- Do not re-enable interrupts inside the ISR body. This is performed automatically when the ISR returns.
- Keep the ISR as short and as simple as possible. Complex code will typically use more registers that will increase the size of the context switch code.
Interrupt Application
The solution worked through in the last post used a polling
mechanism with the PIC18F47Q10’s Timer0 module to flash the
4 LEDs on the Curiosity HPC Development Board tied to the
upper nibble of the GPIO port A. The solution
continually reads (polls) the T0CON0bits.T0OUT bit comparing
it to the last toggled value to determine when the timer
match has occurred. Upon detection of the match, the
local toggle state is updated and the four LEDs are toggled
as well via the LATA (LATch A) SFR. The code implementing
this logic appears as:
while(1) { if(currentT0OutputState != T0CON0bits.T0OUT) { LATA ^= 0xF0; currentT0OutputState ^= 1; } }
Polling is rarely a first / best choice for monitoring a state - the processor cannot be doing anything else while polling. Of course other activities could be included in the above loop to occur between polling attempts. Depending on timing as well as duration of such tasks, the actually detection of the T0OUT toggle could be delayed resulting in inconsistencies in the period of the events the timer is triggering. A few milliseconds different would certainly not be noticed in the blinking of an LED, however if the timer was being used to drive a communication protocol on one of the GPIO pins with tight timing tolerances, the altering of the period could be more consequential. Additionally, a local variable needs to be dedicated for holding the last known value of the T0OUT bit of the T0CON0 register so the comparison can be made.
Interrupts solve this polling problem. The above LED management logic can be handled via interrupts. The ISR requires some additional logic to validate it is in fact Timer0 causing the interrupt and some clean up of the relevant interrupt flag, however, the main loop of the application can perform other completely unrelated tasks while the LED state code is executed at the appropriate time in the ISR.
Project Set Up
A new project can be started in MPLAB. Please see any of the previous posts in this series to follow this path as it covers standing up a new project. A fully implemented project is available in GitHub. The project built in the Introduction to PIC Timers post can also be continued which is what the remainder of this post uses.
A new Interrupt module is introduced to mange general system interrupt configuration. The following highlights the basic configuration (excluding interrupt configuration) covered in the first four posts in this series:
- System clock is configured to use the internal high speed oscillator with clock divider resulting in a processor clock speed of 8MHz.
- Port A upper nibble is configured as output GPIO.
- Timer0 is configured with a period of one second.
Timer Module
The approach taken is to place the peripheral specific interrupt configuration code with the actual peripheral configuration; tasks such as setting priority and enabling the specific peripheral interrupt. All peripheral interrupt code could also be implemented within the new Interrupt module introduced subsequently, thereby centralizing all interrupt configuration. I elected to place the peripheral specific interrupt configuration code with the peripheral instead of in the Interrupt module so that there is a single source of truth for peripheral behavior definition.
New code to the configureTimer0() (timer.c) method enables the Timer0 interrupt and clears the interrupt flag:
void configureTimer0(void) { T0CON0 = 0; T0CON1 = ((0b010 << _T0CON1_T0CS_POSITION) | (0b1110 << _T0CON1_T0CKPS_POSITION)); TMR0H = 121; // enable TMR0 interrupt and clear the flag PIE0bits.TMR0IE = 1; PIR0bits.TMR0IF = 0; }
Setting the TMR0IE bit in PIE0 SFR enables the Timer0 interrupt and clearing TMR0IF bit in the PIR0 SFR ensures the interrupt flag is cleared. See the Introduction to PIC Timers for a detailed explanation of the other elements of the function.
Interrupt Module
Following the previous established project structure of using modules, a new module, interruptmodule, is added to the previous modules: gpiomodule, systemmodule, and timermodule. With the addition of the interruptmodule folder and project module along with the C source file, interruptmodule.c and the header file, interrupt.h, the projects tab looks like:
and the files tab appears as:
The interrupt.h header file forward declares a new function for configuring interrupts:
#ifndef INTERRUPT_H #define INTERRUPT_H #include <xc8.h> void configureInterrupts(void); #endif /* INTERRUPT_H */
The interruptmodule.c file provides the implementation for configuring interrupts to operate in Midrange Compatibility Mode in the configureInterrupts() method. Note as only the Timer0 interrupt is being used, there is no need to Set the PEIE bit in the INTCON SFR.
#include "interrupt.h" void configureInterrupts(void) { // no priority INTCONbits.IPEN = 0; }
As the comments indicate and previously discussed the
IPEN bit
in the Interrupt Control Register (
INTCON)
disables
interrupt priorities. As the application is only
handling a single interrupt from Timer0 there is no need to prioritize interrupts.
Application Code
The remaining steps:
- call to configure interrupts
- enabling global interrupts
- implementing an ISR
all occur in the application code (main.c):
/* * File: main.c * Author: lstanton * * Created on December 26, 2024, 10:47 AM */ #include "system.h" #include "timer.h" #include "gpio.h" #include "interrupt.h" /* * */ void main(void) { initializeSystem(); configureGPIO(); configureTimer0(); configureInterrupts(); ei(); startTimer0(); while(1) { ; // NO-OP } return; } void __interrupt() interruptHandler(void) { if(PIR0bits.TMR0IF == 1) { if((LATA & 0xF0) == 0xF0) { LATA&= 0x0F; } else { LATA += 0x10; } PIR0bits.TMR0IF = 0; } return; }
The interrupt configuration occurs with the call to configureInterrupts().
The processor specific header (pic18.h) provides a convenient macro for enabling interrupts. The macro, ei(), simply sets the GIE bit of the INTCON register.
Note that there is now no LED state management or polling code in the main application loop. The application can now handle other logic in that loop without concern of missing the Timer0 match.
The final bit of logic is the ISR itself; the function definition with the __interrupt() specifier. This ensures this function will be encoded (by the compiler) to specifically handle the interrupt such as disabling and re-enabling interrupts on entry and exit of the function respectively and placed at the correct memory address. Also note that the priority is not specified as high_priority as when the system is operating in Midrange Compatibility Model (the IPEN bit of the INTCON register is cleared), This per the latest version of the compiler documentation.
This is a good opportunity to mention the importance of always installing the latest tools - such as the XC8 compiler - as well as reviewing any new guides and relevant documentation installed with the new version of the tools. Over the course of writing this post Microchip released v3.00 (Windows) of the XC8 compiler. I started putting the post together with the 2.50 version (Windows) of the compiler. In the 2.50 version of the compiler documentation, there was no specific mention of how to declare the ISR with respect to the type (priority) parameter. As we will be operating in Midrange compatibility mode, the guidance from section 5.9 of the guide states:
The priority scheme implemented by PIC18 devices can also be disabled by clearing the IPEN SFR bit. Such devices are then said to be operating in Mid-range compatibility mode and utilize only one interrupt vector, located at address 0x8.
Further guidance:
For Enhanced Baseline devices with interrupts, Mid-range devices, or PIC18 devices operating in Mid-range compatibility mode:
• Write one interrupt function to process all interrupt sources.
And then in section 5.9.1:
If interrupt priorities are being used but an ISR does not specify a priority, it will default to being high priority. It is recommended that you always specify the ISR priority to ensure your code is readable.
While the application is not using priorities, technically there should be no need to specify the type parameter as high_priority and if type is not specified it defaults to high_priority. We also know from previous guidance that when operating in Midrange compatibility mode only the interrupt vector at residing at 0x08 (which is the high priority interrupt vector) will be used. However, there is also the suggestion to always specify the ISR priority to ensure readable code. It is of course specific to using priority interrupts, but seems like it would be a good idea to specify the type as high_priority for readability. I did this and everything worked.
After installing the latest compiler, the new document, DS-50002737J, introduces a new section - 5.9.1.1 – with specific implementation guidance for writing Single Vector or Compatibility Mode ISRs:
Write only one ISR using the __interrupt() specifier. No arguments should be passed to this specifier. Use void as the ISR return type and for the parameter specification.
As observed in the code above, the newest guidance from the latest XC 8 Compiler's User Guide is followed.
The logic within the ISR first determines if the Timer0 interrupt flag ( TMR0IF bit in PIR0) is set which indicates at least one source causing the ISR execution is Timer0. If the TMR0IF bit is set, execution of the ISR continues with evaluating the upper nibble of LATA (recall the four LEDs on the Curiosity HPC development board are connected to the upper nibble of LATA) and if all four bits are set the upper nibble is reset to 0, otherwise the upper nibble is incremented by one. The logic is written to preserve any values in the lower nibble of LATA. Finally, the TImer0 interrupt flag ( TMR0IF) is cleared and the ISR exits.
Running the Application
With the above updates, the application is complete and should build without errors. Once uploaded, the four LEDs on the Curiosity HPC development board will display a binary counter output, from 0h to Fh, updating by 1h every second.
Summary
This post examined the important embedded development / micro-processor concept of interrupts. Interrupts allow both external and peripheral events to be handled on demand as they occur. The alternative, polling for specific events of interest, makes application harder to develop and maintain as well as making application much less efficient. Specifically this post extended a previous implementation where polling was implemented to determine when a Timer0 match occurred to manage application UI in the form of LED manipulation on the development board. By using interrupts the resulting application is more efficient as the UI is updated on demand when the timer match occurs; the application and processor is now able to perform other tasks in the main application loop.
Also with this post, the series of posts looking at getting started with Microchip 8-bit development wraps up. I hope for those following this series I have provided a solid foundation for your continuing journey of developing on Microchip's 8-bit PIC processors.
- Comments
- Write a Comment Select to add a comment
To post reply to a comment, click on the 'reply' button attached to each comment. To post a new comment (not a reply to a comment) check out the 'Write a Comment' tab at the top of the comments.
Please login (on the right) if you already have an account on this platform.
Otherwise, please use this form to register (free) an join one of the largest online community for Electrical/Embedded/DSP/FPGA/ML engineers: