Real-time Linux (Xenomai)

 Radboud University Nijmegen

Exercise #9: Interrupt Service Routines

Note: The last part of this exercise cannot be done on a simulated version of Xenomai in VMware, but only on a real hardware platform.


Introduction

Interrupt handling is important in real-time operating systems, because via the interrupt mechanism the system becomes aware of external events. The speed of the system's response to interrupts is an important characteristic of many real-time systems. 


Objectives

The following are the primary objectives of this exercise:

  • To get some experience with interrupt handling in Xenomai.

Description

We consider two sources of interrupts: interrupts generated by a press on the keyboard and interrupts from the parallel port of the PC.

Interrupts from the keyboard

A key press leads to an interrupt with interrupt request (IRQ) line 1. By default, keyboard interrupts are enabled.

Interrupts from the parallel port

Extensive documentation on programming and using the parallel port of the PC for data acquisition and control can be found on the Web. We give a brief overview.

  • A parallel port can generate interrupts when the voltage level on a certain input pin changes. The parallel port has to be instructed explicitly to generate interrupts and it is also possible to inhibit interrupts (e.g., while serving a previous interrupt).
  • The parallel port consists of three bytes in the I/O address space of the PC:
    1. The first byte is used as data port: 8 bits of output, often labelled D0 to D7, with D0 being the least significant bit and D7 the most significant.
    2. The second byte is used as status port: 5 bits of input (S3 - S7), 3 bits unused (S0 - S2)
    3. The third byte is used as control port:  4 bits of output (C0 - C3), 4 bits setup/unused (C4 - C7)
  • Traditionally, IBM PC systems have allocated their first two parallel ports according to the configuration in the table below:
 PORT NAME      Interrupt #     Starting I/O    Ending I/O
  LPT1           IRQ 7           0x378           0x37f
  LPT2           IRQ 5           0x278           0x27f
  • In this exercise we use LPT1 with IRQ 7 and the following three bytes: 
    1. 0x378 : data port
    2. 0x379 : status port
    3. 0x37A : control port
  • The parallel port standard states that setting bit C4 of the control port (0x37A) enables interrupt reporting.

The pins of the 25 pin female D type parallel port connector are shown in the following picture

  • In this picture all green pins (numbered 18 - 25) are grounded at 0 Volt.
  • When having interrupts enabled (i.e., bit C4 has been set), an interrupt will be generated by the parallel port when the voltage on input line S6 (pin 10) is raised from 0 Volt to +5 Volt. By default the input line is kept high to +5 Volt when nothing is connected. Observe that when pin 10 is shortly shorted to one of the pins 19-25, i.e., to ground, an interrupt on IRQ line 7 will be generated by the parallel port. The interrupt is generated at exactly the moment that you undo the shortage, since at that exact moment the line is raised from 0 to +5 Volt.

Setting up the parallel port

The process of setting up a hardware device and writing an ISR is mainly determined by the particular characteristics of the device. Xenomai only provides the facilities to connect a particular interrupt to a particular ISR.
To enable interrupts on the parallel, simply set bit C4 of the control port and clear it to disable them.

  • Add the following include for inb and outb:
#include  <sys/io.h>
  • To set a bit at a particular I/O address, first read the byte, next set the bit using the bitwise-or operation '|', and finally write the byte back. In this way, the other bits are not affected (there is no x86 instruction for individual bit-setting). In our case,
ioperm(0x37A, 1, 1);
byte = inb(0x37A);
byte = byte | 0x10; /* hex 10 = binary 00010000 */
outb(byte, 0x37A);

After these instructions, shorting pin 10 to ground will generate an interrupt.

  • To clear the bit and disable interrupts, follow a similar procedure using the bitwise-and operation '&':
byte = inb(0x37A);
byte = byte & 0xEF; /* hex EF = binary 11101111 */
outb(byte, 0x37A);

After these instructions, shorting pin 10 to ground will have no effect.

Defining an Interrupt Service Routine (ISR)

An Interrupt Service Routine (ISR) is simply a function that mediates between interrupts of a hardware device and a real-time application (in fact, it may be the entire real-time application itself).

  • Suppose an interrupt happens when we are inside an ISR. Then the ISR itself would be interrupted and called again.This is called "re-entrancy". This is often useful, but care needs to be taken when coding re-entrant functions.
  • Note that when a function is interrupted, its arguments and local variables are written safely to the stack. Note that "static" and "global" variables are persistent and are written to the heap instead of the stack.
  • One has to be very careful with non-atomic actions such as
    • writing global data structures
    • writing hardware registers
    • calling other non-re-entrant functions

As a simple example, consider an ISR which keeps track of the number of interrupts that have been serviced and writes this integer to pipe. Hence, the main idea is to have an ISR of the form:

static int isr_code(xnintr_t *intr)
{
  static int interrupts = 0; /* cumulative count of interrupts */
  interrupts++;
  rt_pipe_write(&mypipe, &interrupts, sizeof(interrupts),P_NORMAL);
  return RT_INTR_HANDLED;
}

There are two problems which make the code above non-re-entrant: the persistent counter "interrupts" and the call to "rt_pipe_write()":

  • Incrementing "interrupts" means reading its value, adding 1 to it, and writing it back. This sequence could be split and we could lose a count by an interrupt within this sequence.
  • The "rt_pipe_write()" operation manipulates some global data structure associated with the pipe. If the sequence of writing data and updating any counts or pointers is interrupted, the pipe will be corrupted.

Our solution is to disable interrupts as soon as the ISR runs. When interrupts are disabled, new arriving interrupts are put on hold in the hardware until the interrupts are enabled again.

This leads to following re-entrant ISR code:

static int isr_code(xnintr_t *intr)

{

  static int interrupts = 0; /* cumulative count of interrupts */

  /* Disable physical interrupts in parallel port hardware

     while we're servicing this one.

     It's to prevent we don't re-enter this code! */

  outb(inb(0x37A) & 0xEF, 0x37A);

  /* now we do our work */

  interrupts++;

  rt_pipe_write(&mypipe, &interrupts, sizeof(interrupts),P_NORMAL);

  /* and re-enable physical interrupts in parallel port hardware */

  outb(inb(0x37A) | 0x10, 0x37A); /* enable interrupt */

  return RT_INTR_HANDLED;

}

What happens if the instructions to disable interrupts are interrupted themselves?

  • In our case, these instructions are idempotent, meaning doing them twice or redoing them in the middle gives the same net effect as doing them once.
  • Note that on multiple-processor hardware it may be more difficult to obtain an idempotent sequence of instructions to disable interrupts. In this case, mutual exclusion techniques such as spin locking can be used. This is a technique where the CPU that tries to get the lock does this by a busy wait which keeps checking whether the lock is freed. For normal code which is not part of an interrupt handler, we can also use semaphores to do locking on multiprocessor machines, but note that blocking on a semaphore is not allowed in an interrupt handler!

Installing interrupt handlers

The Interrupt management services of the Native Xenomai API Module of the Xenomai API contains a number of functions to deal with interrupts, such as create, enable, disable, and delete operations.

Usually, writing an interrupt handler can only be done in kernel mode and not in user mode. The reason is that interrupts are close to the low level hardware and, to drive hardware in a stable and secure way, modern operating systems do not want user programs to be able to mess around with the hardware at that low level. User programs are only allowed to interact with the hardware driver which is tested for stability and security. For instance, in MS DOS and MS Windows 95/98 user programs could directly interact with the hardware. Since Windows XP, however, Windows does not allow that anymore. Instead, one has to use drivers for hardware that are certified by Microsoft.

Xenomai, however, does also allow interrupt handling in user space. We describe the differences between kernel space and user space.

Interrupt handling in kernel space

In kernel space the "rt_intr_create" function with six arguments is used to associate an ISR to a particular interrupt:

int rt_intr_create (RT_INTR *intr, const char* name, unsigned irq,
                    rt_isr_t isr, rt_iack_t iack, int mode)

As an example, consider the installation of an ISR for the parallel port.

/* declare interrupt descriptor */

static RT_INTR myinterrupt;

/* assign our ISR 'isr_code' to interrupt 7 */

rt_intr_create(&myinterrupt, "myinterrupt", PARPORT_IRQ, isr_code, NULL, 0);

/* enable xenomai to listen to our interrupt */

rt_intr_enable(&myinterrupt);

/* activate the parallel port's interrupt (7) hardware */

outb(inb(0x37A) | 0x10, 0x37A);

At the end of the program, we disconnect the ISR:

/* disable interrupt */

outb(inb(0x37A) & 0xEF, 0x37A);

/* disable  xenomai to listen to our interrupt */

rt_intr_disable(&myinterrupt);

/* delete our interrupt */

rt_intr_delete(&myinterrupt);

Interrupt handling in user space

In user space, the interrupt handler can be declared as a real-time task which waits for the next interrupt event by means of the special "rt_intr_wait" operation:

int rt_intr_wait (RT_INTR *intr, RTIME timeout)

This operation can only be called in user space. The association between the interrupt and an IRQ line is defined by a simplified create function, with four arguments, which can only be called by a user space task:

int rt_intr_create (RT_INTR *intr, const char* name, unsigned irq, int mode)

If the mode is I_PROPAGATE the interrupt is forwarded to Linux after it has been processed by the Xenomai task.

Hence, in user space an interrupt handler is a real-time task which might look like :

 void irq_server (void *cookie)
 {
     for (;;) {
        /* Wait for the next interrupt */
        nr_interrupts = rt_intr_wait(&intr_desc,TM_INFINITE);
        if (nr_interrupts>0) {
            /* Process interrupt. */
        }
     }
 }

The "rt_intr_wait" operation allows a task to suspend execution until the associated interrupt event triggers. The priority of this task is raised above all other Xenomai tasks - except those also undergoing an interrupt or alarm wait (see rt_alarm_wait()) - which ensures that when an interrupt occurs it preempts any other task under normal circumstances.

Interrupt receipts are logged if they cannot be delivered immediately to some interrupt server task. Hence, a call to rt_intr_wait() might return immediately if an interrupt is already pending on entry of the service.

Note that by using "rt_intr_wait" we cannot have nested interrupts, but then also the re-entrancy problems mentioned above are avoided, so there is no need to disable interrupts in the handler.


Exercises

Similar to the other exercises, we will work in user space in this exercise.

Exercise 9a.

  1. This part of the exercise can be done in VMware.
    Write a program which catches keyboard interrupts; it should count the number of key presses and print this number after each change. Note that it is very important to propagate these interrupts to Linux, because otherwise no keyboard commands are possible anymore!

      Describe and explain the output of the program.

  1. To investigate the interrupt mechanism, add a dummy task which periodically executes an "rt_timer_spin" with a long period (say 3 seconds) and which has a higher priority than the interrupt handler. In addition, let the interrupt handler print its priority, for instance using:
        rt_task_inquire(curtask,&curtaskinfo);
        rt_printf("Current priority of handler task: %d \n", curtaskinfo.cprio);
      Describe and explain the output of the program.
 

Exercise 9b.

This part of the exercise cannot be done on WMware. It uses a special connector for the parallel port which connects D0 (pin 2) to S6 (pin 10) when we press a button. Modify the program of exercise 9a1 such that it counts the number of interrupts on the parallel port and prints the value of the counter after each interrupt. Describe and explain the output of the program.


Last Updated: 28 September 2011 (Jozef Hooman)
Created by: Harco Kuppens
h.kuppens@cs.ru.nl