The Cortex M0+ processor supports handling multiple interrupt sources using a single interrupt service routine (ISR). This can help reduce code size by avoiding duplicated ISR code. The key is configuring the Nested Vectored Interrupt Controller (NVIC) to route multiple interrupt sources to the same ISR function.
Benefits of Sharing an ISR
There are several benefits to using a single ISR handler for multiple interrupt sources on Cortex M0+:
- Reduces overall code size since the ISR code only needs to be defined once.
- Simplifies interrupt handling code.
- Allows flexibility to add/remove interrupt sources easily by modifying NVIC routing only.
- Avoids duplicated effort when handling similar peripheral interrupts.
For resource constrained Cortex M0+ devices, minimizing code size is often a priority. Sharing an ISR can help reduce Flash usage and leave more room for application code.
Configuration Steps
To configure multiple interrupts to share an ISR handler on Cortex M0+ involves a few simple steps:
- Define a single ISR handler function that will be shared. This takes the standard form:
- In the NVIC peripheral registers, configure the interrupt sources to route to the same IRQ channel.
- Enable interrupts for the desired sources by setting the enable bits in the NVIC peripheral.
- Write the shared ISR handler code to check which source caused the interrupt, and handle appropriately.
As long as the interrupts are routed to the same IRQ channel, the shared ISR will execute when any of the enabled sources trigger.
Identifying the Interrupt Source
Within the shared ISR, we need a way to identify which source triggered the interrupt. There are a few ways to achieve this on Cortex M0+:
Check Peripheral Interrupt Flags
Many peripherals have pending interrupt flags that get set when an interrupt occurs. Within the ISR, check the status of each peripheral’s interrupt flags:
void Shared_ISR(void) {
if (TIMER1_IRQFlag) {
// Handle Timer1 Interrupt
TIMER1_ClearIRQFlag();
}
if (ADC_IRQFlag) {
// Handle ADC Interrupt
ADC_ClearIRQFlag();
}
}
This approach works well if the peripheral flags are directly accessible or easy to check.
Use Device Vector Table
The Cortex M0+ vector table contains the address of the active interrupt service routine. Within the shared handler, this can be compared to known ISR addresses to identify the source:
#define TIMER1_ISR ((void*) 0x20004) // Timer1 ISR Address
#define ADC_ISR ((void*) 0x20008) // ADC ISR Address
void Shared_ISR(void) {
if (ACTIVE_ISR_ADDRESS == TIMER1_ISR) {
// Handle Timer1 Interrupt
}
if (ACTIVE_ISR_ADDRESS == ADC_ISR) {
// Handle ADC Interrupt
}
}
This takes advantage of the vector table routing to identify interrupts. However, it requires knowing the specific ISR addresses.
Use Interrupt Priority Level
The NVIC allows setting unique priority levels for each IRQ channel. Inside the shared handler, reading the current priority can help distinguish sources:
void Shared_ISR(void) {
if (CURRENT_PRIO == TIMER1_PRIO) {
// Handle Timer1 Interrupt
}
if (CURRENT_PRIO == ADC_PRIO) {
// Handle ADC Interrupt
}
}
This requires configuring the NVIC priority registers differently for each interrupt source during setup.
Associating Data with Interrupt Sources
Some applications require passing data or context along with an interrupt. There are a few techniques that can achieve this with a shared Cortex M0+ ISR:
Access Global Variables
Define global variables that can be set by main application code before servicing each interrupt:
volatile uint32_t int_data;
void main() {
int_data = ADC_values;
NVIC_TriggerADCInt();
int_data = Timer_count;
NVIC_TriggerTimerInt();
}
void Shared_ISR() {
if(ACTIVE_ISR == ADC_ISR) {
// Use int_data
}
}
This allows passing data to the ISR through a global variable. Context is determined based on which interrupt triggers.
Use Function Pointers
Set a global function pointer from the main code before servicing each interrupt. The ISR can then call the function:
void (*int_handler)(void);
void main() {
int_handler = HandleADCInt;
NVIC_TriggerADCInt();
int_handler = HandleTimerInt;
NVIC_TriggerTimerInt();
}
void Shared_ISR() {
int_handler(); // Call function based on context
}
This is useful for passing entire functions or code callbacks to the ISR.
Create Interrupt Context Structures
For more complex data, structure context variables can be defined:
typedef struct {
uint32_t* data;
uint16_t length;
void (*callback)(void);
} ISR_Context;
ISR_Context ctx_adc, ctx_timer;
void main() {
ctx_adc.data = ADC_values;
ctx_adc.length = 100;
ctx_timer.data = Timer_ticks;
ctx_timer.length = 50;
}
void Shared_ISR() {
if (ACTIVE_ISR == ADC_ISR) {
// Use ctx_adc
}
}
This allows customization of the context data provided to the ISR.
Prioritizing Interrupts
Using the NVIC priority registers, we can configure unique priorities for each interrupt routed to the shared ISR. This ensures time-sensitive interrupts get handled before lower priority sources.
Some key points when prioritizing shared interrupts on Cortex M0+:
- Set priority levels during NVIC configuration before enabling interrupts.
- Higher priority values correspond to lower logical priorities.
- Must clear pending bits for same or lower priorities during each interrupt.
- To preserve priorities, use nested interrupts sparingly.
Properly prioritizing interrupts is key to avoiding missed events or timing issues when sharing an ISR between multiple sources.
Interrupt Latency Considerations
Sharing an interrupt handler can add additional latency for lower priority interrupts. For time sensitive applications, ensure the worst case latency introduced by other sources is acceptable.
Some ways to help manage interrupt latency when sharing an ISR:
- Minimize runtime of high priority interrupt handling routines.
- Drop lower priority events if their latency window expires.
- Use interrupt locking or masking to create critical sections if needed.
- Adjust NVIC priorities to match application requirements.
Testing and measurement is important to verify real-time deadlines are met when using shared ISRs.
Usage Examples
Here are some examples of how sharing an ISR can be useful on the Cortex M0+:
UART + Timer Interrupts
A UART and periodic timer interrupt can trigger the same handler. The UART is set to low priority while the timer gets high priority using the NVIC. This services time critical events quickly while handling UART in the background.
Multiple ADC Channel Conversion
The ADC peripheral may have multiple channel interrupts. Sharing an ISR allows consolidating the handling logic for each channel into one place.
Motor Control ISR
For a multi-phase motor driver, a shared ISR can handle interrupts from multiple comparators or timers to update the motor drive signals.
Conclusion
Sharing an interrupt service routine between multiple sources can optimize code size on the Cortex M0+. With careful NVIC configuration and priority management, it provides flexibility to handle several peripheral interrupts from a single handler.
Key considerations include properly identifying interrupt sources, managing context and data transfer, prioritizing timing critical events, and verifying worst-case interrupt latency. Used judiciously, shared ISRs enable efficient utilization of the Cortex M0+ interrupt capabilities.