Interrupts API
Concept:
Interrupt service routines (ISRs) or interrupt handlers are callbacks which are executed on reception of an interrupt or event typically achieved by raising a flag. As the name would suggest an interrupt pauses the execution of the process and jumps to execute the callback (ISR) associated with the particular entry on the interrupt vector table (IV). After the callback finishes executing, the main process is resumed from where it left off. This method is typically used for high priority, event driven code execution on embedded systems. It is often preferable to polling which is slower and more power intensive as it requires the periodic checking of flags whereas interrupts execute immediately and can often be used on a "sleeping processor" to wake up the chip, executing a block of code before checking if any further action is required before falling back asleep.
Applications:
Interrupts are used in various modules including GPIO, timers, SPI, and CAN, among others. They are used for anything event driven and are an important part of the firmware for the car. They used mostly in the context of embedded programming and both the MSP430 and stm32f0xx chips that we use support their use. Emulating them on x86 is the interesting challenge that this API had to overcome.
Critical Sections
Critical Sections protect code that shouldn't be interrupted by blocking and forcing incoming interrupts to wait. On an embedded device there is usually a function that takes care of this for us. Critical sections typically are blocks of code protected by a pair of functions of macros. Nesting critical sections safely is important as ending a nested critical section shouldn't enable interrupts as it is within another critical section.
x86 Emulation
Overview
Emulation of interrupt vectors on x86 presents an interesting challenge. Typically, x86 machines have a single and multi task operating system which has a scheduler that uses separate processes and threads to parallelize operations. Interrupts don't typically occur in x86 systems as when you want a callback to run immediately you simply start a new thread for it. Rarely are x86 machines used for event driven behaviors.
That being said, UNIX like operating systems have a C library that supports interrupt like behaviors <signal.h>. Signals works similar to an interrupt vector table mapping an ID to a callback which is executed immediately upon reception of the the signal. Signals has a few advantages over a typical embedded interrupt system:
- It can queue signals up to a maximum as defined by _POSIX_SIGQUEUE_MAX. (Embedded systems can typically only queue up to one interrupt at a time for any given entry in the IV table).
- The queue is a pqueue prioritized by both time of arrival and minimum ID value. (If multiple signals arrive no time stamp is associated with them on an embedded device so only priority and ID number are considered)
- Signals can be configured to block. This means that callbacks won't be preempted during execution unless the incoming signal is higher priority. Moreover, the queue is still available during critical sections.
These slight differences should not make a huge difference in testing, but are worth noting.
Signals possess 32 regular interrupts including ones commonly used to kill processes via the terminal such as SIGINT, SIGQUIT, SIGTERM or SIGKILL. However, in addition to these 32 pre-defined signals, as of POSIX.1-2001 and Linux 2.2, Linux supports 33 realtime signals. These signals don't have set purposes and are free for application-defined usage. Typically, the range of signals is bounded by SIGRTMIN and SIGRTMAX which define the minimum and maximum entry numbers in the "interrupt vector table" for signals. Because different OS's can use some of these signals on an OS wide level to extend the first 32 signals, we refer to them as SIGRTMIN+n so as to prevent clashes.
Initialization
The x86 Interrupts API initializes three signals with the lowest ID being the highest priority and the largest being the lowest priority (SIGRTMIN+0 to SIGRTMIN+2). The signals are configured to block lower and same priority signals while executing, pushing them onto the queue. Each of these signals owns a callback which dispatches the incoming signal to the correct modules interrupt handler to execute the callback associated with the interrupt. Ex: if a signal intended to trigger a GPIO interrupt is received it is dispatched to the GPIO interrupt handler which then executes the callback accordingly.
It should be noted that these signals only apply to the current process. That is to say each process configures its own set of signals and the signals must be address to the process ID (PID) that is emulating/testing our code.
Interrupt ID System (Tentative)
Interrupts are complicated by the fact there is no standard callback format for interrupts. As a result, each module (GPIO, SPI, CAN, etc.) needs its own interrupt handler which needs to be registered to the core interrupt_mcu library this handler receives a unique handler_id which it needs to send to the interrupt_mcu module whenever it registers an interrupt; this is so the interrupt_mcu module knows which handler a particular interrupt ID it receives is registered to. Handler IDs are purely internal and serve no other purpose than tracking registered handlers and ensuring signals/interrupts get dispatched to the right module. However, interrupt IDs are different and are necessary to trigger any interrupts. When an interrupt is registered with interrupt_mcu it must pass the ID of its handler and is also assigned an ID and that interrupt ID is mapped by interrupt_mcu internally to the handler ID. This interrupt ID that must be sent when triggering an interrupt in order for the callback to get executed.
Ex:
The GPIO interrupt module is initialized and its handler is assigned the handler ID of 2 (Some other module has the handler ID of 1). Next the user registers a callback using the GPIO interrupt module, let's say this interrupt receives an interrupt ID of 3. Internally, interrupt ID 3 is mapped to Handler ID 2. Now if interrupt ID 3 is called, the core interrupt library would look receive interrupt ID 3, looking up its handler ID in the map it dispatches the interrupt to the GPIO interrupt handler passing it the interrupt ID of the interrupt it received a signal for. The handler does its own lookup and finds the callback and executes that callback.
Triggering Interrupts
Interrupts should be triggered in software using sigqueue(pid, sig, sigval).
- pid: the process ID of the main thread that is running the code we are testing/emulating on x86.
- sig: the signal ID you want to run the callback for. In our implementation this is really just the priority. We have 3 signal handlers which run the same callback however, SIGRTMIN is the highest priority handler while SIGRTMIN+2 is the lowest priority.
- sigval: a union whose sival_int should be set to the ID of the interrupt you want to run. When an interrupt is registered it is assigned its own interrupt_id which should be sent in this message to run that interrupt.
Critical Sections
Critical sections are achieved by having the process create a process wide block mask for the three signals we defined which blocks incoming interrupts on these lines forcing them to queue.