Interrupts informieren die CPU über externe Ereignisse und im Kern warten passende Interrupt-Service-Routinen. Programmieren und testen lässt sich das Ganze prima auf einem Raspberry Pi.
Betriebssysteme gehen einer ureigenen Aufgabe nach, wenn sie exklusiv die Hardware direkt ansprechen und Applikationen vor Zugriffen auf Registerebene abhalten. Denn der Durchgriff auf die Peripherie ist sicherheitskritisch und paralleles Nutzen von Ressourcen kann zu Race Conditions (unerwünschten Systemzuständen) führen. Die Applikationssoftware sollte zudem nicht von speziellen Hardware-Eigenschaften abhängig sein, sondern portierbar bleiben.
Soweit die Theorie. Beim Raspberry Pi nutzen fast alle Hardwareprojekte das Bastelinterface Wiring Pi [1], das die Peripherieregister des Controllers per »mmap()« in den Adressraum einer Applikation einblendet. Damit bekommen die Programme am Betriebssystem vorbei Zugriff auf die Hardwareregister.
Dabei haben mehrere Kern-Technik-Artikel gezeigt, wie professionell ein Programmierer die Peripherie eines Raspberry Pi ansprechen kann ([2], [3], [4]). Der Clou dabei ist, ihm ein einfach zu bedienendes Interface über einen Gerätetreiber zur Verfügung zu stellen. Der Treiber nutzt die Linux-internen Schnittstellen, die den Schutz vor parallelen Zugriffen gewährleisten und zudem nicht nur auf dem Raspberry Pi funktionieren. Der Treiber eröffnet zugleich die Möglichkeit der nativen Interruptverarbeitung, die innerhalb von Applikationen nicht vorgesehen ist, jedoch erhebliche Geschwindigkeitsvorteile bietet.
Auch wenn das Userland – richtigerweise – keine Interrupts kennt, arbeiten Applikationen ereignisgesteuert: Die Applikation legt sich schlafen bis beispielsweise ein Interrupt eintritt. Das implementieren Programme klassisch über die Funktion »select()« oder moderner über »poll()« . Die Bibliothek Wiring Pi benutzt übrigens »poll()« in Kombination mit dem Sys-Filsystem für ihre Interrupt- beziehungsweise Ereignissteuerung.
Die Unterstützung von »poll()« im Kernel in Form einer Treiberfunktion (»driver_poll()« ) ist scheinbar einfach. Im Detail erweist sie sich wegen der Behandlung paralleler Zugriffe als ein rechtes Hexenwerk. Moderner ist es ohnehin, einen eigenen Thread aufzuziehen, der sich schlafen legt und bei eintreffenden Interrupts aufwacht. Das ist Applikations- und Treiber-seitig leicht implementierbar, es realisiert das Schlafenlegen und Wecken über einen blockierenden Lesezugriff durch die Applikation und verschmerzt den Verzicht auf »poll()« leicht.
Für diesen Ansatz hat der Entwickler des Treibers neben der Initialisierung eine Funktion »driver_read()« zu schreiben, die sich durch Aufruf der Kernelfunktion »wait_event_interruptible()« schlafen legt (Abbildung 1). Eine Applikation, die einen Lese-Aufruf (»read()« ) absetzt, schläft dadurch erst mal. Der nächst Interrupt weckt die Applikation.
Im Detail: Der Interrupt startet die Interrupt-Service-Routine (ISR), die wiederum ruft die Funktion »wake_up()« auf. Damit die richtige, zugehörige Applikation aufwacht, verwenden sowohl »wait_event_interruptible()« als auch »wake_up()« als Parameter zur Identifikation der Zusammengehörigkeit eine Wait-Queue, die »init_waitqueue_head()« initialisiert.
Interrupts ankoppeln
Nach dem Userinterface und dem Kontrollfluss mit Schlafenlegen und Aufwecken bleibt noch die eigentliche Ankopplung an die Interruptverarbeitung im Linux-Kernel übrig. Deren Zentrum bildet die Funktion »request_irq()« , die vier Parameter erwartet: die Interruptnummer, das Auslöse-Ereignis für den Interrupt, die Adresse der aufzurufenden Interrupt-Service-Routine und einen Parameter, der die ISR beim Aufruf übergeben bekommt. Die Interruptnummer lässt sich über die Funktion »gpio_to_irq()« beim Kernel erfragen. Sie hängt von der Hardware ab, ist aber dankenswerterweise im Devicetree hinterlegt, beim Raspberry Pi haben sie die Entwickler sogar fest im Kernel einprogrammiert.
Moderne Prozessoren unterstützen so viele Interruptquellen, dass jeder GPIO-Pin seine eigene Interruptnummer hat. So liefert die Funktion für GPIO-17 (Pin 11 am Stecker P1) auf dem Raspberry Pi 1 die Interruptnummer 187, auf einem Raspberry 2 die Nummer 273 zurück. Bevor ein GPIO als Interruptquelle verwendbar ist, müssen ihn – wie in [2] beschrieben – die Kernelfunktion »gpio_request()« reservieren und »gpio_direction_input()« als Eingabepin konfigurieren.
Als Auslöse-Ereignisse hat der Linux-Kernel »IRQF_TRIGGER_RISING« , »IRQF_TRIGGER_FALLING« , »IRQF_TRIGGER_HIGH« und »IRGF_TRIGGER_LOW« implementiert, die Entwickler auch kombinieren dürfen (Abbildung 2). Der Raspberry Pi unterstützt vier Ereignistypen: Das Auslösen eines Interrupts bei steigender Flanke (Wechsel von 0 zu 3,3 Volt am GPIO-Pin), bei fallender Flanke (von 3,3 auf 0 Volt), Pegel-getriggert auf 3,3 Volt oder – viertens – anliegende 0 Volt.
Die ISR, deren Adresse »request_irq()« per drittem Parameter erfährt, bekommt nach einem Interrupt neben der Interruptnummer auch den vierten Parameter von »request_irq()« übergeben. Durch diesen im Kernel häufig angewandten Trick kann der Programmierer denselben Code für mehrere ISRs verwenden.
Interruptsharing
Bei der Programmierung der ISR gibt es drei Dinge zu beachten: Erstens muss sie prüfen, ob es wirklich die zugehörige Hardware war, die den Interrupt ausgelöst hat. Viele Architekturen nutzen Interruptleitungen nämlich für mehrere Geräte. War die aufgerufene ISR die falsche, meldet sie »IRQ_NOT_HANDLED« zurück. Hat dagegen der Interrupt den für die Hardware richtigen Treiber erwischt, muss dieser den Interrupt gegenüber der Hardware quittieren, zumeist durch Löschen eines Bits in einem Peripherieregister. Das Device setzt daraufhin den Interrupt zurück. Es überrascht nicht, dass die Ausgestaltung dieses Vorgangs völlig Hardware-abhängig ist.
Als Zweites findet die eigentliche Behandlung des Interrupts statt, nach der die ISR als Drittes den Wert »IRQ_HANDLED« zurückgibt. Falls nicht explizit anders bestimmt, unterbricht Linux eine laufende ISR nicht.
Dennoch gilt es zu bedenken, dass auf einem Multicore-System, der Raspberry 2 ist ein solches, ISRs auf anderen Cores laufen könnten. Dieses Wissen ist relevant für den Schutz kritischer Abschnitte, etwa beim Zugriff auf gemeinsame Daten.
Der gerade beschriebene Schritt eins fällt für den Raspberry Pi weg. Der leistet sich für jeden GPIO eine eigene Interruptleitung beziehungsweise einen eigenen Interrupt. Daher muss der Code weder die Interruptquelle verifizieren noch den Interrupt selber quittieren. Es bleiben die Behandlung des Interrupts und die Rückgabe von »IRQ_HANDLED« übrig.
Innerhalb eines Kernelmoduls erfolgt das Reservieren eines Interrupts häufig in der Modul-Initialisierungsfunktion (»mod_init()« ). Das Freigeben des Interrupts erfolgt beim Entladen des Moduls (Funktion »mod_exit()« ) durch den Aufruf von »free_irq()« . Diese Funktion bekommt als Parameter die Interruptnummer und den vierten Aufrufparameter übergeben.
Widerstand, Drahtbrücken und ein Makefile
Um die Interruptverarbeitung zu testen, benötigen Raspberry-Pi-Bastler nur einen 10-Kiloohm-Widerstand und zwei Drahtbrücken (female-female). Sie verbinden die beiden Drahtbrücken über den Widerstand miteinander. Dann stecken sie das eine Ende in GPIO-17, der auf dem Stecker P1 am Pin 11 anliegt (Abbildung 3). Das andere Ende gehört in GPIO-4, Pin 7. Wer mag, kann auch andere GPIOs verwenden und passt den Quellcode in Listing 1 entsprechend an.
Den Quellcode in Listing 1 kompilieren Anwender auf dem Raspberry Pi mit Hilfe des Makefile aus Listing 2. Das erweist sich möglicherweise als der schwierigste Teil. Die Himbeere macht einem unter Raspbian das Anfertigen von Kernelbinaries unnötig schwer, da die zum Kernel passenden Headerdateien im Repository fehlen. Hinzu kommt, dass die Distribution unterschiedliche Compilerversionen zum Übersetzen des Kernels und auf dem Raspberry Pi selbst einsetzt.
Der Kasten “Invalid module format” verrät, wie es trotz aller Misslichkeiten gelingt. Ist der Treiber kompiliert und per »insmod gpio_irq.ko« geladen, kann es losgehen.
Listing 1
gpio_irq.c
001 #include <linux/module.h>
002 #include <linux/fs.h>
003 #include <linux/cdev.h>
004 #include <linux/device.h>
005 #include <linux/gpio.h>
006 #include <asm/uaccess.h>
007 #include <linux/interrupt.h>
008 #include <linux/sched.h>
009
010 static dev_t gpio_dev_number;
011 static struct cdev *driver_object;
012 static struct class *gpio_class;
013 static struct device *gpio_dev;
014 static int rpi_irq_17;
015 static char *devname = "gpio_irq";
016 static wait_queue_head_t sleeping_for_ir;
017 static int interrupt_arrived;
018
019 static irqreturn_t rpi_gpio_isr( int irq, void *data )
020 {
021 printk("rpi_gpio_isr( %d, %p )\n", irq, data );
022 interrupt_arrived += 1;
023 wake_up( &sleeping_for_ir );
024 return IRQ_HANDLED;
025 }
026
027 static int config_gpio( int gpionr )
028 {
029 int err, rpi_irq;
030 char name[20];
031
032 snprintf( name, sizeof(name), "rpi-gpio-%d", gpionr );
033 err = gpio_request( gpionr, name );
034 if (err) {
035 printk("gpio_request failed %d\n", err);
036 return -1;
037 }
038 err = gpio_direction_input( gpionr );
039 if (err) {
040 printk("gpio_direction_input failed %d\n", err);
041 gpio_free( gpionr );
042 return -1;
043 }
044 rpi_irq = gpio_to_irq( gpionr );
045 printk("gpio_to_irq returned %d\n", rpi_irq);
046 if (rpi_irq < 0) {
047 printk("gpio_to_irq failed %d\n", rpi_irq);
048 gpio_free( gpionr );
049 return -1;
050 }
051 err = request_irq( rpi_irq, rpi_gpio_isr,
052 IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
053 devname, driver_object);
054 printk("driver_object: %p\n", driver_object);
055 if (err) {
056 printk("request_irq failed with %d\n", err);
057 gpio_free( gpionr );
058 return -1;
059 }
060 printk("gpio %d successfull configured\n", gpionr);
061 return rpi_irq;
062 }
063
064 static ssize_t driver_read( struct file *instanz, char __user *user,
065 size_t count, loff_t *offset )
066 {
067 unsigned long not_copied, to_copy;
068
069 interrupt_arrived = 0;
070 wait_event_interruptible( sleeping_for_ir, interrupt_arrived );
071 to_copy = min( count, sizeof(interrupt_arrived) );
072 not_copied = copy_to_user(user, &interrupt_arrived, to_copy);
073 return to_copy-not_copied;
074 }
075
076 static struct file_operations fops = {
077 .owner= THIS_MODULE,
078 .read= driver_read,
079 };
080
081 static int __init mod_init( void )
082 {
083 dev_info(gpio_dev, "mod_init");
084 init_waitqueue_head( &sleeping_for_ir );
085 if( alloc_chrdev_region(&gpio_dev_number,0,1,"gpioirq17")<0 )
086 return -EIO;
087 driver_object = cdev_alloc(); /* Anmeldeobjekt reservieren */
088 if( driver_object==NULL )
089 goto free_device_number;
090 driver_object->owner = THIS_MODULE;
091 driver_object->ops = &fops;
092 if( cdev_add(driver_object,gpio_dev_number,1) )
093 goto free_cdev;
094 gpio_class = class_create( THIS_MODULE, "gpioirq17" );
095 if( IS_ERR( gpio_class ) ) {
096 pr_err( "gpioirq17: no udev support\n");
097 goto free_cdev;
098 }
099 gpio_dev = device_create( gpio_class, NULL, gpio_dev_number,
100 NULL, "%s", "gpioirq17" );
101 if ( IS_ERR(gpio_dev) )
102 goto free_class;
103 rpi_irq_17 = config_gpio( 17 );
104 if (rpi_irq_17 < 0) {
105 goto free_device;
106 }
107 return 0;
108 free_device:
109 device_destroy( gpio_class, gpio_dev_number );
110 free_class:
111 class_destroy( gpio_class );
112 free_cdev:
113 kobject_put( &driver_object->kobj );
114 free_device_number:
115 unregister_chrdev_region( gpio_dev_number, 1 );
116 return -EIO;
117 }
118
119 static void __exit mod_exit( void )
120 {
121 dev_info(gpio_dev, "mod_exit");
122 device_destroy( gpio_class, gpio_dev_number );
123 class_destroy( gpio_class );
124 cdev_del( driver_object );
125 unregister_chrdev_region( gpio_dev_number, 1 );
126 free_irq(rpi_irq_17, driver_object);
127 gpio_free( 17 );
128 return;
129 }
130
131 module_init( mod_init );
132 module_exit( mod_exit );
133 MODULE_LICENSE("GPL");
Listing 2
Makefile
01 ifneq ($(KERNELRELEASE),) 02 obj-m := gpio_irq.o 03 04 else 05 KDIR := /lib/modules/$(shell uname -r)/build 06 PWD := $(shell pwd) 07 08 default: 09 $(MAKE) -C $(KDIR) M=$(PWD) modules 10 endif 11 12 clean: 13 rm -rf *.o *.ko *.cmd *.mod.* modules.order 14 rm -rf Module.symvers .tmp_versions
Invalid module format
Um Kernelcode auf dem Raspberry Pi zu entwickeln, bedarf es der Kernelheader und Konfigurationsdateien. Diese lassen sich, wie [5] erläutert, über die Webseite [6] installieren. Unglücklicherweise ist aber der Kernel der aktuellen Raspbian-Version mit einer anderen Compilerversion übersetzt als derjenige, der auf der Himbeere installiert ist. Dadurch lassen sich auf dem Raspberry Pi generierte Module trotz passender Kernelheader nicht laden (Meldung: »Invalid module format« ).
Die Autoren des Artikels empfehlen daher anhand der Anleitung [7] den Kernel selbst zu übersetzen und zu installieren. Sehr zeitintensiv, aber mit den wenigsten Problemen verbunden gelingt das auf dem Raspberry Pi selbst. Wer das als Cross-Entwicklung zu praktizieren beabsichtigt, muss auf dem Raspberry Pi unter Umständen zuvor den Compiler aktualisieren. Die GCC-Version erfragt übrigens die Option »-v« . Die beiden Befehle
sudo apt-get install gcc-4.8 make CC=gcc-4.8 V=1
installieren einen aktuellen Compiler und erzeugen damit gleich das Kernelmodul.
Zwei praktische Tests
Zu Beginn öffnet der Tester auf dem Raspberry Pi ein zweites Terminal. Ein »cat /proc/interrupts« zeigt die Interruptquellen und deren Auftrittshäufigkeit. Auf dem Raspberry Pi 1 erscheint unter der Interruptnummer 187 – auf einem Pi 2 unter 273 – ein neuer Eintrag, der mit dem Namen des Treibermoduls »gpio_irq« verbunden ist.
Jetzt tippt der Tester in dem einen Terminal »sudo tail -f /var/log/messages« ein und verfolgt alle Kernelnachrichten. Im zweiten Terminal löst er einen Interrupt aus, indem er die in Listing 3 gezeigten Shellkommandos eintippt.
Zum Abschluss stellt Listing 4 eine einfache Applikation vor, die auf Interrupts reagiert und mit »make waitforinterrupt« schnell übersetzt ist. Sie greift auf die vom Treiber bereitgestellte Gerätedatei »/dev/gpioirq17« zu und schläft ab dem Aufruf von »read()« in Zeile 14. Mit jedem Interrupt, den jemand beispielsweise per »echo 0 >value« auslöst, wacht die Applikation auf (Abbildung 4).
Listing 3
Shellkommandos lösen Interrupt aus
01 quade@raspberrypi ~ $ sudo su 02 [sudo] password for quade: 03 root@raspberrypi:/home/quade# cd /sys/class/gpio 04 root@raspberrypi:/sys/class/gpio# echo 4 >export 05 root@raspberrypi:/sys/class/gpio# cd gpio4/ 06 root@raspberrypi:/sys/class/gpio/gpio4# echo out >direction 07 root@raspberrypi:/sys/class/gpio/gpio4# echo 0 >value ; echo 1 >value
Listing 4
waitforinterrupt.c
01 #include <stdio.h>
02 #include <fcntl.h>
03
04 int main( int argc, char **argv, char **envp )
05 {
06 int fd, interrupts=0;
07
08 fd = open( "/dev/gpioirq17", O_RDONLY );
09 if (fd<0) {
10 perror("/dev/gpioirq17");
11 return -1;
12 }
13 while (1) {
14 read( fd, &interrupts, sizeof(interrupts) );
15 printf("interrupts: %d\n", interrupts );
16 }
17 return 0;
18 }
Fazit
Der Artikel macht klar, dass Interrupt-gesteuerte Applikationen zu schreiben keinen großen Aufwand verursacht. Gerade wenn es im eigenen Programm zeitlich eng wird, bieten sich der Einsatz von Interrupts und die Programmierung einer Interrupt-Service-Routine geradezu an. Da Interrupts das Zeitverhalten des ganzen Systems beeinflussen, achten Entwickler auf eine professionelle Realisierung (Kasten “Professionelle Interruptprogrammierung”).
Professionelle Interruptprogrammierung
Gerade wenn enge zeitliche Anforderungen vorliegen, kann eine Interrupt-Service-Routine hilfreich sein. Bei der Kodierung sind jedoch einige Richtlinien zu beachten:
- Die ISR soll kurz sein. Andernfalls entstehen lange Interrupt-Latenzzeiten, da während der Abarbeitung der Routine andere Interrupts auf derselben CPU gesperrt sind.
- Eine ISR darf nicht schlafen gelegt werden. Kernelfunktionen wie »wait_event_interruptible()« innerhalb der ISR sind daher tabu.
- Innerhalb der ISR lassen sich keine Daten mit dem Userland austauschen. Kernelfunktionen wie »copy_to_user()« oder »copy_from_user()« sind in der ISR ebenfalls verboten.
Pfiffige Programmierer teilen die im Rahmen eines Interrupts abzuarbeitenden Aufgaben in zwei Teile: Im ersten lassen sie nur die notwendigsten Aktionen ausführen, etwa das Quittieren des Interrupts in der Hardware. Im zweiten Teil folgen die zeitintensiveren Aufgaben. Klassisch ist dieser zweite Teil als Soft-IRQ ausgeprägt und hieß in den Urzeiten von Linux “Bottom Half”.
Etwas moderner ist die Variante “Threaded Interrupts”, bei der der zweite Teil als Kernelthread ausgeführt ist. Kernelthreads sind zwar nicht ganz so effizient wie Soft-IRQs, aber priorisierbar. Wer niedrige Latenzzeiten benötigt, wählt also diese Realzeit-Architektur. Threaded Interrupts lassen sich entweder per Kommandozeile für alle Interrupts aktivieren oder aber explizit für den Treiber mit der Funktion »request_threaded_irq()« . Listing 5 zeigt die elementaren Komponenten.
Listing 5
Geänderter Code für Threaded Interrupts
01 static irqreturn_t rpi_gpio_isr( int irq, void *data )
02 {
03 printk("rpi_gpio_isr( %d, %p )\n", irq, data );
04 interrupt_arrived += 1;
05 wake_up( &sleeping_for_ir );
06 return IRQ_HANDLED;
07 }
08
09 static irqreturn_t hard_isr( int irq, void *dev_id )
10 {
11 return IRQ_WAKE_THREAD;
12 }
13
14 static int config_gpio( int gpionr )
15 {
16 [...]
17 err = request_threaded_irq( rpi_irq, hard_isr, rpi_gpio_isr,
18 IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
19 devname, driver_object);
Infos
- Gordon, “WiringPi, GPIO Interface library for the Raspberry Pi”: http://wiringpi.com
- Quade, Kunst, “Kern-Technik – Folge 70” (GPIO): Linux-Magazin 10/13, S. 102
- Quade, Kunst, “Kern-Technik – Folge 72” (I2C): Linux-Magazin 02/14, S. 90
- Quade, Kunst, “Kern-Technik – Folge 77” (SPI): Linux-Magazin 12/14, S. 98
- Quade, Kunst, “Kern-Technik – Folge 80” (Performance Monitoring Unit): Linux-Magazin 05/15, S. 84
- Archiv für Raspbian-Kernel-Headerdateien: http://www.niksula.hut.fi/~mhiienka/Rpi/linux-headers-rpi/
- Quade, Kunst, “Kern-Technik – Folge 69” (Raspberry-Pi-Kernel): Linux-Magazin 08/13, S. 86










