Interrupts¶
Learning objectives: Interrupts and their use in embedded devices.
In a computer, an interrupt is an internal or external signal sent to the processor that, as the name suggests, interrupts the processor's ongoing task. When an interrupt occurs, the processor saves its state (program execution state, registers, etc.) and switches to the execution of the interrupt handler. Once the handler finishes, the processor restores its saved state and continues executing the program exactly from where it left off.
The image below shows the pin configuration of the ATmel ATtiny2313 microcontroller, with two external hardware interrupt pins (INT0 and INT1) marked in red. These pins can be connected to the interrupt line from a peripheral device, allowing it to capture the interrupt signals sent by the device. In general, microcontrollers have dedicated pins for external interrupts, physically connected to the interrupt management logic of the chip. The data sheets of peripherals/components define exactly what types of interrupts they can generate.
Interrupts come in two types. An interrupt can be hardware-based (either internal to the CPU or from a peripheral) and is typically caused by an asynchronous event (independent of the program's timing) inside the CPU or a peripheral. Thus, from the program's perspective, a hardware interrupt can occur at any moment. Hardware interrupts can be triggered by events inside the CPU, such as a divide by zero error (an error condition), or externally, for example, when a peripheral device signals that it needs attention because new data is available or an error occurred.
Software interrupts are triggered by a special machine language interrupt instruction, such as the
INT
instruction on Intel x86 processors. A software interrupt signals the CPU, and the operating system/firmware handles it the same way as hardware interrupts, executing the corresponding handler function.Interrupts have a priority. Higher-priority interrupts are handled first. Priority becomes important when multiple interrupts occur simultaneously or while a lower-priority interrupt handler is executing. In such cases, a higher-priority interrupt will preempt the lower-priority handler and execute first.
In embedded systems, the RESET pin hardware interrupt has the highest priority. Next are hardware interrupts, which are typically time-critical, followed by software interrupts, for which the programmer can set the desired priority. Software interrupts are therefore controlled according to how the programmer defines them (and they can also preempt each other).
An interrupt handler (handler) is almost like a function, but it is never called from the running program. This is important because before executing the interrupt handler, the processor state must be saved, and calling the handler as a function would not perform this save. The handler also does not return any values, but it can use global variables or registers to pass information. In C, however, the handler is implemented as a function, and the RTOS must be informed that this function is now an interrupt handler.
Handlers are time-critical for two reasons: they interrupt the running program, and they can preempt each other. This means that it is easy (incorrectly) to block the entire program's execution with a high-priority handler that takes too long to execute. Now, TI-RTOS specifies that a hardware interrupt should last a maximum of 5 microseconds and a software interrupt should last around 100 microseconds. Therefore, only minimal code can be executed in the handler. For this reason, handlers often only modify the values of registers or global state variables. A good practice, for example, is to use a state variable in the handler to indicate that the peripheral has new data, and then perform the actual read operation outside the handler in the main program. Again, these are priceless free tips. Well, more on this in the State Machines lecture materials.
SensorTag Interrupts¶
Next, we will go through different ways to use interrupts in RTOS with an example. We will demonstrate both external hardware interrupts (pin and peripheral). Internal hardware interrupt is described in the sarjaliikenne when we discuss about UART serial communication.
Pin Interrupt¶
In the previous chapter, we introduced an interrupt that responds to changes in pin state. Let’s revisit this topic...
...
// Pin configuration
PIN_Config buttonConfig[] = {
Board_BUTTON0 | PIN_INPUT_EN | PIN_PULLUP | PIN_IRQ_NEGEDGE,
PIN_TERMINATE
};
...
// Interrupt handler
void buttonFxn(PIN_Handle handle, PIN_Id pinId) {
...
}
Int main() {
...
// Register the buttonFxn handler for the pin
if (PIN_registerIntCb(buttonHandle, &buttonFxn) != 0) {
System_abort("Error registering button callback function");
}
...
}
We can see in the
Pin_Config
table that the pin is now set with the constant PIN_IRQ_NEGEDGE
, which allows the pin to trigger an interrupt. In this case, the interrupt occurs when the pin’s state changes on the falling edge, meaning from HIGH (operating voltage) -> LOW (ground level). A pin interrupt can also be set to trigger on the rising edge when the state changes from LOW -> HIGH, using the constant PIN_IRQ_POSEDGE
, or even on both edges with PIN_IRQ_BOTHEDGES
.We assign the interrupt handler function with the call
PIN_registerIntCb
, where we specify the buttonFxn
function. The interrupt-specific functionality is then implemented in the function definition.Peripheral Interrupts¶
As an example of an external interrupt from a peripheral in the SensorTag, we introduce the versatile MPU9250 sensor (42-page datasheet). This sensor integrates a gyroscope, accelerometer, and magnetometer. It can also be used as a compass.
The MPU9250 may work via interrupt by connecting its external interrupt line
Board_MPU_INT
(found in the CC2650STK.h header file) to the SensorTag. The interrupt is enabled in the same way as the pin interrupt described above.In the following example, we will learn how to enable and disable external interrupts.
// RTOS variables for MPU9250 pins
static PIN_Handle MpuHandle;
static PIN_State MpuState;
// MPU9250 pin configuration
static PIN_Config MpuConfig[] = {
Board_MPU_INT | PIN_INPUT_EN | PIN_PULLDOWN | PIN_IRQ_DIS | PIN_HYSTERESIS,
PIN_TERMINATE
};
// Handler function
Void MpuFxn(PIN_Handle handle, PIN_Id pinId) {
...
}
Void sensorTask(UArg arg0, UArg arg1) {
// Enable MPU9250 interrupt on the rising edge
PIN_setInterrupt(MpuConfig, PIN_ID(Board_MPU_INT) | PIN_IRQ_POSEDGE);
...
// Disable MPU9250 interrupts
PIN_setInterrupt(MpuConfig, PIN_ID(Board_MPU_INT) | PIN_IRQ_DIS);
}
int main(void) {
...
// Enable MPU interrupt pin
MpuHandle = PIN_open(&MpuState, MpuConfig);
if (MpuHandle == NULL) {
System_abort("Pin open failed!");
}
// Register the interrupt handler function
PIN_registerIntCb(MpuHandle, &MpuFxn);
...
}
Let’s break down this example. In the
main
function, we activate the interrupt line in the program and assign the interrupt handler function MpuFxn
.In the pin configuration
MpuConfig
, there is a new constant PIN_IRQ_DIS
(disable), which indicates that the pin-triggered interrupts are initially disabled. Therefore, when we want to receive interrupts (e.g., in a task), we use the PIN_setInterrupt
function to enable the interrupts with the constant PIN_IRQ_POSEDGE
. Typically, it's a good practice to enable interrupts at the latest possible moment in the code to prevent them from interfering with the program’s execution before they're actually needed.In the example, the implementation of
sensorTask
also includes enabling the interrupt with the PIN_setInterrupt
function. This way, we can programmatically enable and disable interrupts as needed.Timers¶
The RTOS also provides a timer through the
Clock
library, which allows us to implement scheduled events, i.e., interrupts at specific intervals. For example, we could use a timer to read sensor data once per second, communicate with a peripheral, or blink an LED. In the previous state machine material, there was already an example of this, where we changed the state of the state machine once per second, which resulted in reading sensor data and printing the new measurements to the screen.An example tells us more than a thousand words:
...
#include <ti/sysbios/knl/Clock.h>
...
// Clock interrupt handler
Void clkFxn(UArg arg0) {
// Example style: don't do this, as it's very slow
sprintf(str,"System time: %.5fs\n", (double)Clock_getTicks() / 100000.0);
System_printf(str);
System_flush();
}
int main(void) {
...
Board_initGeneral();
// RTOS clock variables
Clock_Handle clkHandle;
Clock_Params clkParams;
// Initialize the clock
Clock_Params_init(&clkParams);
clkParams.period = 1000000 / Clock_tickPeriod;
clkParams.startFlag = TRUE;
// Enable the clock in the program
clkHandle = Clock_create((Clock_FuncPtr)clkFxn, 1000000 / Clock_tickPeriod, &clkParams, NULL);
if (clkHandle == NULL) {
System_abort("Clock create failed");
}
...
}
Once again, we use a configuration structure, in this case
Clock_Params
. In the period
member, we set the desired time in clock cycles. Let's recall the previous material, where it was mentioned that one tick corresponds to about 10 microseconds in our time.Now we set the period to
100000
, which gives us a timer interrupt approximately once per second. The inaccuracy here comes from the fact that the clock in the Clock
library is implemented in software, and thus its interrupts are software interrupts. When we run the example program, we can see that the time calculation fluctuates by a few tens of microseconds in either direction. Well, this level of accuracy is sufficient for us humans.The
startFlag
member in the configuration structure can be used to start the clock immediately from the Clock_create
call (startFlag=TRUE) or to start it later with the Clock_start
call (startFlag=FALSE). The clock is stopped with the Clock_stop
call. These calls require the clock handle as a parameter.The
Clock_create
call introduces something new to us. The first parameter of the call is the interrupt handler, in this case, the clkFxn
function. The purpose of this function is to demonstrate how a software-implemented clock works. It's important to remember that printing to the console is a very slow operation (from the MCU's perspective), so in this example, we are clearly violating the rule that an interrupt handler's execution time should not be too long. Well, we'll forgive it this time, as we wanted to measure the inaccuracy of the software interrupt. (A better way to achieve the same result would be to store the clock time in a global variable and print it to the console in a task using the state variable.)The second parameter of the
Clock_create
call, timeout
(in this case, 1 second), tells the timer how many ticks it should wait before the first interrupt event. The idea here is that we can also implement one-time (one-shot) timers. In these clocks, the period
member of the Clock_params
structure is set to zero, and the desired time delay is passed to the timeout
argument.The figure shows two different types of clocks in the Clock library.
The library allows us to create multiple simultaneous clocks in our program. We only need to define a handle for each clock, set the parameters, and create the clocks using the
Clock_create
call. However, this is not particularly efficient; a better approach would be to implement a single clock interrupt and count multiple time delays at once.Additionally, the library introduces us to the familiar
Clock_tickPeriod
variable, which tells us how many microseconds one tick represents. The Clock_getTicks
call provides the elapsed time in ticks since the program started.Real-time Clock¶
On top of all this, the RTOS also offers a library for a real-time clock called
Seconds
, which operates in human-readable time. The only drawback is that we need to manually initialize the clock by setting it to the correct time before using it.Once again, let's rely on the power of an example:
#include <ti/sysbios/hal/Seconds.h>
...
Void clkFxn(UArg arg0) {
time_t now = time(NULL);
struct tm *timeinfo = localtime(&now);
System_printf("The time is %02d:%02d:%02d\n", timeinfo->tm_hour+3, timeinfo->tm_min, timeinfo->tm_sec);
System_flush();
}
Int main() {
...
// Set the real-time clock start time
Seconds_set(1475578882); // A more interesting argument..
...
}
First, we notice the
Seconds_set
call in the main
function, which sets the clock to the desired time. Now things get interesting (well... for some people, it surely does!).Here, the time is provided in Unix time, which is the number of seconds that have passed since January 1, 1970, 00:00:00 UTC, represented as a 32-bit integer. There are several websites available online that calculate the elapsed seconds for you, such as the Epoch converter. The number 1475578882 used in the above example corresponds to the time October 4, 2016, 11:01:22 GMT.
In the clkFxn function, we encounter some new things as we use the time.h standard library. The library provides the
time(NULL)
function, which returns the current real-time as a 32-bit integer. In addition, the time library provides several functions that convert the seconds into a more readable format. In the above example, the localtime
call fills in the struct tm
structure, allowing us to extract the hours, minutes, and seconds separately.In the example, we also have to set the time zone in a somewhat awkward way using
tm_hour+3
.In Conclusion¶
The RTOS abstracts the use of interrupts to something as simple as writing a handler as a function. We don't even necessarily know whether the library is interrupt-based or just a regular program function. To clarify this, it's important to read the documentation of the specific library. Otherwise, we might unknowingly create too heavy a handler for an interrupt.
Give feedback on this content
Comments about this material