Now it's time to discuss interrupts. If you work with microcontrollers, you know interrupts are crucial to real-time systems and the fastest way to handle events. When using an RTOS, you must consider how the kernel handles interrupts and how those interrupts interact with context switching between tasks. I’m going to focus specifically on the Cortex-M architecture for this occasion and assume you already understand how interrupts work and how the NVIC peripheral dispatches interrupt service routines and handles priorities, if not, well take a look at some videos on youtube or ask to ChatGPT.
Interrupts in Zephyr default to a single function, called _isr_wrapper, defined in the isr_wrapper.c file; all vectors point to it. For instance, if a microcontroller has 32 vector interrupts, Zephyr directs all of them to the same _isr_wrapper function. This defeats the purpose of a nested vectored interrupt controller, which can dispatch different handlers for each vector, but its done like this for platform portability purposes with other architectures. For instance this is how we set-up a custom interrupt routine.
#include <zephyr/kernel.h>
#define DUMMY_IRQ 1
void my_own_isr(void *param)
{
printk( "Interrupt routine\n\r" );
}
int main( void )
{
IRQ_CONNECT( DUMMY_IRQ, 1, my_own_isr, 0, 0 );
irq_enable( DUMMY_IRQ );
....
return 0;
}
Inside _isr_wrapper function, the Interrupt service Rutine that triggered the call is decoded using the _sw_isr_table array, which holds each ISR routine's address and an optional argument address for the routine. At the end of the day the same NVIC from Cortex-M architecture is emulated by software. But that is not all, it is important to notice the _isr_wrapper does a few extra things before and after dispatch the interrupts.
void _isr_wrapper(void)
{
...
/* _sw_isr_table does not map the core system exceptions,
* which take first 16 interrupt numbers, only the external
* interrupts.
*/
irq_number -= 16;
const struct _isr_table_entry *entry = &_sw_isr_table[irq_number];
(entry->isr)(entry->arg);
...
}The table _sw_isr_table is generated in files isr_tables.c and filled at pre-build time with all the ISR rutines registered using the macro IRQ_CONNECT
Each entry holds two values: the first is the parameter passed to the handler, and the second is the ISR address. The ISR address always has its LSB set because that bit does not form part of the address; it indicates the routine must execute in interrupt mode (a Cortex-M architecture detail)
const struct _isr_table_entry __sw_isr_table _sw_isr_table[31] = {
{(const void *)0x0, (ISR)z_irq_spurious}, /* 0 */
{(const void *)0x0, (ISR)0x800033d}, /* 1 my_own_isr function address*/
{(const void *)0x0, (ISR)z_irq_spurious}, /* 2 */
...
};Pause your reading and build the following example on GitHub. Then review the previously mentioned files and the software ISR table, and experiment by generating additional interrupts.
Direct Interrupts
Direct interrupts are called “directly“ by the NVIC, meaning the ISR to call wont be called by the _isr_wrapper common ISR instead of, an address is place directly in the _irq_vector_table this is not the ISR address we declare in code, for instance the example below uses the mandatory macro ISR_DIRECT_DECLARE to declare our direct ISR, the name of our function is my_own_isr
/*this is the function that will be called when the interrupt occurs */
ISR_DIRECT_DECLARE(my_own_isr)
{
printk( "Interrupt routine\n\r" );
ISR_DIRECT_PM();
return 1;
}
The address you will find in the vector table is for an inline function my_own_isr in our case, which is not the address of the ISR_DIRECT_DECLARE(my_own_isr)code, ??? and why you will wonder
static inline int my_own_isr_body(void);
__attribute__ ((interrupt ("IRQ"))) void my_own_isr(void)
{
int check_reschedule;
ISR_DIRECT_HEADER();
check_reschedule = my_own_isr_body();
ISR_DIRECT_FOOTER(check_reschedule);
}
static inline int my_own_isr_body(void)
{
printk( "Interrupt routine\n\r" );
ISR_DIRECT_PM();
/*it is up to the application to return a 1 or 0*/
return 1;
}The ISR_DIRECT_DECLARE (in file irq.h and irq.c) is actually a macro that does a few things it creates an inline function with the same name and the posfix _body wich in turn will have the code we place, also add two macros ISR_DIRECT_HEADER and ISR_DIRECT_FOOTERbefore and after calling the actual ISR my_own_isr_body function, this is because the kernel needs to know if a desition needs to be made (preemption of a task for instance), this how looks like the macro resolve for our example.
static inline int my_own_isr_body(void);
__attribute__ ((interrupt ("IRQ"))) void my_own_isr(void)
{
int check_reschedule;
ISR_DIRECT_HEADER();
check_reschedule = my_own_isr_body();
ISR_DIRECT_FOOTER(check_reschedule);
}
static inline int my_own_isr_body(void)
{
printk( "Interrupt routine\n\r" );
ISR_DIRECT_PM();
/*it is up to the application to return a 1 or 0*/
return 1;
}Shared Interrupts
One interrupt line can be shared by more than one interrupt service routine, it is only needed to enable this feature using the following Kconfig options
CONFIG_SHARED_INTERRUPTS=y
CONFIG_SHARED_IRQ_MAX_NUM_CLIENTS=2The following code shows how to implement this in code
#include <zephyr/kernel.h>
#define DUMMY_ISR 1
void my_first_isr( void *p );
void my_second_isr( void *p );
int main( void )
{
IRQ_CONNECT( DUMMY_ISR, 1, my_first_isr, NULL, 0 );
IRQ_CONNECT( DUMMY_ISR, 1, my_second_isr, NULL, 0 );
irq_enable( DUMMY_ISR );
while(1){
NVIC_SetPendingIRQ( DUMMY_ISR );
k_msleep( 1000 );
}
return 0;
}
void my_first_isr( void *p ){
printk( "First interrupt routine\n\r" );
}
void my_second_isr( void *p ){
printk( "Second interrupt routine\n\r" );
}What happens is that a new table called z_shared_sw_isr_table is generated in the file isr_tables.c whin in turn has the address to parameter and ISr corresponding to the two functions, these are called clients, for the same IRQ
const struct z_shared_isr_table_entry __shared_sw_isr_table z_shared_sw_isr_table[31] = {
{ },
{
.client_num = 2,
.clients = {
{
.isr = (ISR)0x800033d, .arg = (const void *)0x0 /*my_first_isr address*/
},
{
.isr = (ISR)0x8000359, .arg = (const void *)0x0 /*my_second_isr address*/
},
},
},
{ },
...While in the _sw_isr_table there are references to z_shared_sw_isr_table and a reference to a new wrapper function called z_shared_isr in a file called shared_irq.c which in turn will call both ISR declared for this specific shared IRQ
const struct _isr_table_entry __sw_isr_table _sw_isr_table[31] = {
{(const void *)0x0, (ISR)z_irq_spurious}, /* 0 */
{(const void *)&z_shared_sw_isr_table[1], (ISR)z_shared_isr}, /* 1 */
{(const void *)0x0, (ISR)z_irq_spurious}, /* 2 */
...This is the subroutine where you can see all the clients registered will be dispatch starting in the same order they where registered using the IRQ_CONNECT macro or the irq_connect_dynamicfunction trough a simple for loop
void z_shared_isr(const void *data)
{
size_t i;
const struct z_shared_isr_table_entry *entry;
const struct _isr_table_entry *client;
entry = data;
for (i = 0; i < entry->client_num; i++) {
client = &entry->clients[i];
if (client->isr) {
client->isr(client->arg);
}
}
}OK, good enough for today. Most of this knowledge will be used when writing low-level drivers for microcontroller peripherals that use interrupts, but not when the driver already exists in Zephyr, such as GPIO or UART; then you only need to handle callbacks, But it is always good to know how these kind of sensitive stuffs are handled by the operative system, you never know. We still need to discuss zero-latency interrupts and RAM-based handlers