Interrupts

Currently, CMRX doesn’t intervene into interrupt handling. Processes are expected to define their own interrupt service handlers and these will be executed same as in any bare-metal firmware. This approach was chosen to provide lowest-latency path. This means that interrupt handlers are running with kernel privileges and should do only the absolutely most critical work needed to handle the interrupt.

Interrupts vs. kernel

CMRX architecture is designed in a way where kernel services are not callable from interrupt service handlers. On architectures where interrupt subsystem is used to switch from usermode to kernel mode (such as ARM), interrupt which does so should have priority lower than any hardware IRQ. In such setup, hardware will raise an error if IRQ handler is trying to call kernel. Otherwise the result of calling kernel from ISR is considered user error and will most probably lead to crash or sever corruption.

This design is deliberate. Interrupt service routines (ISRs) are executed with privileges equal to kernel and are not a subject to memory protection, thus we want to make live in there as hard as possible. ISRs have no access to kernel syscalls, can’t use mutexes and are generally isolated from the rest of firmware by lacking any way of communication except of two:

  • Interrupts can notify an object via special version of notify_object() callable from ISR
  • Interrupts can send signal via special version of kill() callable from ISR

It is highly discouraged to disable interrupts globally in interrupt service handlers as this would prevent higher-priority interrupts from preempt lower-priority interrupts. Kernel itself is designed as lock-free, thus it is not using interrupt disable as a way to implement critical sections. CMRX kernel ever only disables interrupts for very short period of time to perform atomic updates to internal counters of transactioning subsystem.

Interrupts vs. userspace

ISR handler developers are expected to use ISR handlers just to do the absolutely unavoidable work due to latency requirements and then dispatch the work down to userspace thread. This can be done by queueing received data into queue and notifying thread, which will process the data the next time CPU is free for task of this priority. This approach creates minimum-latency environment where no single event is able to block all threads from CPU.

Typical pattern of handling interrupt in CMRS is as follows:

typedef struct {
    const struct PeripheralAPI * vtable;
    /* ... internal state of driver ... */
    bool running;
}  PeripheralDriver;

PeripheralDriver peripheral;

void IRQ_Handler()
{
    /* ... process peripheral request ... */
    isr_notify_object(&peripheral);
}

static int peripheral_workqueue(void * data)
{
    PeripheralDriver * driver = (PeripheralDriver *) data;
    
    do {
        wait_for_event(&driver, 0);
        /* ... process deferred event ... */
    } while (driver->running);
}

OS_THREAD_CREATE(driver, peripheral_workqueue, &peripheral, 32);

Structure peripheral is an instance of RPC service (driver instance) which will provide driver API for this driver. IRQ_Handler is a function that provides ISR handler for IRQ this peripheral generates. Function naming is determined by the HAL used in your project.

Note: Many HALs implement approach where HAL provides generic no-op ISR handler that does nothing and immediately returns and weak symbol aliases which alias all interrupt handlers to this no-op handler. Developer is then free to define their own handlers which are given priority before weak symbols provided by HAL. Unfortunately the way how weak symbols are handled by linker causes that ISR handlers coming from static libraries are not given precedence over weak symbols in HAL and they won’t be used. To address this inconvenience either source files containing process ISR handlers have to be linked directly with main binary and not with process library or weak symbol in HAL has to be disabled.

This handler will handle the latency sensitive part, such as reading data from registers before they get overwritten, re-enabling peripheral for next action and as very last action, it will use isr_notify_object() to notify the driver instance object. The “isr_” prefixed version expects to be called from privileged mode, does not perform switch to kernel mode and thus is only callable from ISRs.

Driver also defines one worker thread which processes deferred requests. This thread will wait for notification using wait_for_object(). As the worker thread is a standard CMRX thread, no ISR-specific version of the call is used here. The developer is free to set any priority for this thread to facilitate latency of data processing with respect to other load running on the CPU.

Interrupt control

As of now, CMRX does not provide or support any way to globally disable interrupts from anywhere in the userspace. We believe this feature is seldomly needed (although we identified one very specific use case where its use is legitimate). Given the fact that bulk of driver code runs in userspace, it means that not even drivers are able to globally disable interrupts.

Userspace code is able to enable or disable IRQ lines though, effectively enabling or disabling hardware-generated interrupts. This is a legitimate duty of peripheral driver. It can be done via irq_enable and irq_disable system calls.