Den Raspberry Pi über die WiringPi-API in C programmieren

Python ist eine angenehm zu programmierende Sprache auf dem Raspberry Pi und hat zudem den Vorteil, dass gerade Anfänger leichter mit ihr zurechtkommen als z. B. mit C, das mit seinen vielen Klammern und Pointer etc. verwirren kann.

Aber Python ist ein Just-In-Time (JIT) - Compiler, also ein halber Interpreter. Wie z. B. auch Java nutzt Python dabei eine Technik, den Source in einen Zwischencode, den sogenannten Byte-Code zu übersetzen und dann diesen Byte-Code in einer Virtual Machine auszuführen. Das hat den Vorteil, dass man ein .py-Script direkt mit dem Befehl Python programm.py ausführen kann ohne vorher alles kompilieren zu können. Außerdem kann die VM immer ein Blick auf die Code-Ausführung haben und die Ausführung stoppen (und die fehlerhafte Codezeile anzeigen), sollte es während der Laufzeit z. B. zu einem Speicherzugriffsfehler kommen.

Dieser Komfort kostet allerdings Zeit. Sehr viel Zeit. Ein in C geschriebenes und mit GCC kompiliertes Programm ist ungefähr 100 mal schneller. Das ist schon eine Hausnnummer. Die Geschwindigkeit wird erreicht, indem direkt in Maschinensprache kompiliert wird, die die CPU direkt versteht. Im Prinzip sind das nur noch Zahlen nach dem Muster "führe den 42. Befehl aus der Liste der dir bekannter Befehle aus und benutze dazu das Byte mit dem Wert 170.". Es gibt keine höhere Instanz mehr, die beobachtend zur Seite steht und eingreift, wenn etwas schiefgeht - der Code ist betoniert und wird auf Gedeih und Verderb ausgeführt. Das macht die Sache natürlich sehr schnell - aber auch gefährlich. Ich weiß noch, als ich damals in MS-DOS 3.3 in C programmiert habe und bei einem Fehler der ganze Rechner neu hochfuhr. Irgendwo hatte ich im Programm einen Zeiger falsch gesetzt und so in Speicherbereiche außerhalb des Programmspeiches geschrieben, vielleicht ins Betriebssystem. Abgegrenzte Speicherbereiche kannte man damals noch nicht - als C-Programmierer war kein Bit in den Weiten des Rechners vor einem sicher.

Auf dem Raspberry Pi läuft ein modernes Linux mit Multitasking-Umgebung. Schreibe ich hier aus Versehen in einen Speicherbereich, der meinem Programm nicht gehört, wird das Programm mit einem lapidaren "Speicherzugriffsfehler" beendet. Kein "Neuhochfahren, Programmeditor starten, Programm laden, Fehler suchen und hoffentlich finden, neu kompilieren, Programm starten - Doh! Schon wieder ein HArdware-Reset!" mehr - Terror mehr, der einen schier in den Wahnsinn treiben konnte. Das nimmt C den großen Schrecken. Aber man muss trotzdem wissen, was man tut. Für Trial and Error-Programmierung fehlt der Interpreter, der sagt, was falsch war. Außer "Speicherzugriffsfehler" Null Informationen.

Braucht man aber Geschwindigkeit, die Python nicht mehr bieten kann, muss man zwangsweise auf C umsteigen. Bei meinem letzten Projekt, bei dem ich eine 8x8-LED-Matrix ansteuere wollte ich das Darstellen von Bildern zuerst dadurch erreichen, indem ich jeweils eine der 64 LEDs kurz erleuchte und das für die anderen, bis zu 63 weiteren LEDs ebenfalls tue. Tue ich das schnell genug, "Multiplexing" genannt, dann wird das Auge getäuscht und es entsteht z. B. ein ruhiger 8x8 Block mit 64 leuchtenden LEDs.

Python war da einfach zu langsam. Statt das alle 64 LEDs leuchteten, lief immer eine Zeile nach der anderen durch. Selbst wo es gerade in der Zeile leuchtete konnte man noch sehen. Ein Durchlauf dauerte vielleicht eine Sekunde oder mehr. Mit C hatte ich dann 100 Durchläufe pro Sekunde und einen stabil und ruhig aussehenden Block. Kurz darauf bin ich dann auf die Idee gekommen mehrere LEDs pro Spalte gleichzeitig leuchten zu lassen, wobei die Python-Performance dann doch noch ausreichte.

Trotzdem hätte ich nicht gedacht, dass ich so schnell auf ein Beispiel stoße, indem Python einfach zu langsam ist. Und hätte diese Tests nicht gemacht und erstmal nicht erfahren, wie viel schneller C doch ist.

Den GCC installieren

Der GCC (steht für GNU Compiler Collection oder auch GNU C Compiler) sollte auf dem Raspi eigentlich schon vorhanden sein. Das lässt sich ganze einfach durch ein gcc -v herausfinden. Kommt hier ein "Kommando nicht gefunden" muss GCC zuerst mit sudo apt-get install gcc installiert werden.

Die WiringPi-API installieren

Im Grunde kann man jetzt schon den Editor öffnen, ein C-Sourcecode schreiben, in dem man auf die GPIO-Handles im Betriebssystem zugreift, es kompilieren und so den GPIO manipulieren. Man kann sich es aber auch einfacher machen und eine bereits fertige Library verwenden. In dieser Bibliothek befindet sich schon Code zum Zugriff auf die GPIO, die in Funktionen gekapselt sind, die wir dann einfach verwenden können.

Durch das Installieren der GPIO-Tools im Teil "Erste Schritte mit GPIO-Ansteuerung und LED" habe ich bereits die entsprechenden Libraries auf dem Rechner. Für diejenige, die das noch nicht getan haben, ist die Sache schnell mit einem sudo apt-get install wiringpi erledigt. Alternativ kann man die API auch von der Site des Autors Gordon downloaden.

Außer der WiringPi-API gibt es noch andere APIs, die man zum Zugriff auf die GPIO-Ports benutzen kann. Mir hat aber an der WiringPi-API sehr gefallen, dass die Befehle gut lesbar sind und im Prinzip die gleichen wie sie im Arduino-C verwendet werden. Das macht es sehr leicht, C-Code vom Raspberry Pi zum Arduino zu portieren und umgekehrt.

WiringPi-Api-Befehle

Um die WiringPi-Befehle im eigenen C-Code benutzen zu können werden sie mit #include <wiringPi.h> importiert. Danach stehen einem die Funktionen zur Verfügung, von denen ich die wichtigsten hier kurz erwähnen möchte. Die komplette Funktionsreferenz findet sich auf der oben genannten Site.

Funktionen zur GPIO-Manipulation: weitere Funktionen: zusätzliche Funktionen: Nicht erschlagen lassen vom Funktionsumfang. Für einfache Programme brauchen wir eigentlich nur die Befehle wiringPiSetupGpio, pinMode, digitalWrite und digitalRead.

Springen wir gleich ins kalte Wasser. Nachfolgend das Programm zur 8x8-Matrix-Ansteuerung in C aus dem Raspi. Wer Python mit C vergleichen möchte, sollte jetzt ein zweites Browserfenster mit dem Python-Code öffnen und dann den Code Zeile für Zeile vergleichen. So wird schnell klar, wie der Python-Code in C übersetzt lautet.

// -*- encoding: utf-8 -*- ////////////////////////////////////////////////////// // (C) 2018 by Oliver Kuhlemann // // Bei Verwendung freue ich mich um Namensnennung, // // Quellenangabe und Verlinkung // // Quelle: http://cool-web.de/raspberry/ // ////////////////////////////////////////////////////// #include <stdio.h> #include <stdint.h> #include <unistd.h> #include <wiringPi.h> #include <time.h> #include <string.h> #define HIGH 1 #define LOW 0 #define False 0 #define True 1 // Es gelten die BCM-Pinnummern! #define PinData 13 #define PinClockStore 19 #define PinClockShift 26 #define ClockPulseLen 1 // in Microsekunden void storeTick() { // einen Puls auf die ClockStore-Leitung schicken digitalWrite(PinClockStore, HIGH); delayMicroseconds (ClockPulseLen); digitalWrite(PinClockStore, LOW); delayMicroseconds (ClockPulseLen); } void shiftTick() { // einen Puls auf die ClockShift-Leitung schicken digitalWrite(PinClockShift, HIGH); delayMicroseconds (ClockPulseLen); digitalWrite(PinClockShift, LOW); delayMicroseconds (ClockPulseLen); } void clearDots() { // alle Punkte löschen for (int i=0;i < 16;i++) { digitalWrite(PinData, LOW); shiftTick(); // nächstes Bit } storeTick(); // alle Bits fertig. Speichern } void lightCol(uint8_t col,uint8_t colByte) { // Vorwiderstände hängen an den Row-Ausgängen, sonst keine gleichmäßige Helligkeit // zuerst das rowByte einschieben for (uint8_t b=0; b<8; b++){ uint8_t w = (colByte & 1); digitalWrite(PinData,w); // w = 0 | 1 shiftTick(); // nächstes Bit colByte = colByte >> 1; } // dann das colByte invertiert for (uint8_t b=0; b<8; b++) { uint8_t w = (b == (7-col)); w^=1; // invertieren, weil wird müssen auf Low und damit zu GND durchschalten digitalWrite(PinData,w); // w = 0 | 1 shiftTick(); // nächstes Bit } storeTick(); // alle Bits fertig. Speichern } void lightDot(uint8_t x, uint8_t y) { lightCol(x, 1 << (7-y)); } uint8_t countSetBits(uint8_t byte) { uint8_t cnt=0; for (uint8_t i=0; i<8; i++) { if (byte & 1) cnt++; byte = byte >> 1; } return cnt; } void lightMatrix(uint8_t *rowArray, uint16_t msecs) { uint8_t colArray[8] = {0, 0, 0, 0, 0, 0, 0, 0}; for (uint8_t col=0; col<8; col++) { for (uint8_t row=0; row<8; row++) { colArray[col] += (rowArray[7-row] & 128)/128 * (1 << row); //printf ("col %d, row %d, bit=%d, macht %d\n",col,row,(rowArray[row] & 128)/128,(rowArray[row] & 128)/128 * (1 << row)); rowArray[7-row] = rowArray[7-row] << 1; } // for (int i=0;i<8;i++) printf("col %d: %d\n",col, colArray[col]); } for (uint16_t i=0; i < msecs/6.6; i++) { for (uint8_t col=0; col<8; col++) { lightCol(col, colArray[col]); delayMicroseconds(500); //clearDots(); //delayMicroseconds(10); } } clearDots(); } void scrollShow (uint8_t colArray[8], uint16_t verzoegerung) { for (uint16_t i=0; i < verzoegerung/6.6; i++) { // und anzeigen for (uint8_t col=0; col<8; col++) { lightCol(col, colArray[col]); delayMicroseconds(500); } clearDots(); } } void scroll (char *text[8], uint16_t cols, uint16_t verzoegerung) { // cols=Anzahl von Spalten gesamt uint8_t colArray[8] = {0, 0, 0, 0, 0, 0, 0, 0}; //ersten Screen reinschieben for (uint8_t c=0; c<7; c++) { for (uint8_t i=0; i<7;i++) { colArray[i]=colArray[i+1]; } uint8_t colByte=0; for (uint8_t row=0; row<8; row++) { if (text[row][c] == '#') colByte += 1 << (7-row); } colArray[7]=colByte; scrollShow(colArray,verzoegerung); } // gesamten Text durchscrollen for (uint16_t col=0; col < cols-8; col++) { for (uint16_t c=col; c < col+8; c++) { // 8 ZeilenBytes holen uint8_t colByte=0; for (uint8_t row=0; row<8; row++) { if (text[row][c] == '#') colByte += 1 << (7-row); } colArray[c-col] = colByte; } scrollShow(colArray,verzoegerung); } // letzten Screen auch noch rausschieben for (uint8_t c=0; c < 8; c++) { for (uint8_t i=0; i < 7;i++) { colArray[i]=colArray[i+1]; } colArray[7]=0; scrollShow(colArray,verzoegerung); } } // ------------------------------------------------------ int main(void) { if (wiringPiSetupGpio() == -1) return 1; piHiPri(50); // max. Priorität, funktioniert nur, wenn exe mit sudo aufgerufen wird pinMode(PinData, OUTPUT); pinMode(PinClockStore, OUTPUT); pinMode(PinClockShift, OUTPUT); clearDots(); // die Dots der Reihe nach durchschalten for (int i=0; i<64; i++) { uint8_t x=i%8; uint8_t y=i/8; lightDot(x,y); delay (25); } clearDots(); delay (500); // kleine Warte-Animation /* 0 1 2 3 4 5 6 7 . . # # # # . . 0 . # . . . . # . 8 # . . . . . . # 16 # . . . . . . # 24 # . . . . . . # 32 # . . . . . . # 40 . # . . . . # . 48 . . # # # # . . 56 */ uint8_t dots[20] = {2,3,4,5, 14, 23, 31, 39, 47, 54, 61,60,59,58, 49, 40, 32, 24, 16, 9}; for (uint8_t w=0; w < 10; w++) { for (int i=0; i<20; i++) { uint8_t x=dots[i]%8; uint8_t y=dots[i]/8; lightDot(x,y); delay (10); } } clearDots(); delay (500); // ein 8x8-Matrix-Bild anzeigen uint8_t pic[8] = { 0b01100110, 0b10011001, 0b10000001, 0b10000001, 0b10000001, 0b01000010, 0b00100100, 0b00011000 }; lightMatrix (pic,2000); clearDots(); delay (500); // einen Scroll-Text durchlaufen lassen char *scrollText[8] = { "###...##.##....####....####....####...#..............#...#...#####..####.......####....#####", ".#...#..#..#..#....#..#....#..#....#..#..............#...#...#......#...#......#...#...#....", ".#...#.....#..#.......#....#..#....#..#..............#...#...#......#...#......#....#..#....", ".#...#.....#..#.......#....#..#....#..#......#####...#...#...####...####.......#....#..####.", ".#...#.....#..#.......#....#..#....#..#..............#...#...#......#...#......#....#..#....", ".#....#...#...#.......#....#..#....#..#..............#.#.#...#......#...#......#....#..#....", ".#.....#.#....#....#..#....#..#....#..#..............##.##...#......#...#..##..#...#...#....", "###.....#......####....####....####...#####..........#...#...#####..####...##..####....#####" }; scroll (scrollText,92,100); delay (500); clearDots(); } So unähnlich ist der C-Code vom Python-Code doch gar nicht. Es gibt ein paar Abweichungen, aber die sind eigentlich immer die selben: C ist in der Syntax strenger und will alles haarklein geklammert und deklariert haben. Dadurch wird aber auch eine klare Marschrichtung spezifiziert und der Weg zum Ziel ist dadurch geradlinig und schneller als in Pyhton. Wie bereits erwähnt: mit C ist man 100 mal schneller am Ziel (bei der Ausführung, nicht beim Programmieren).

Nachdem der Source eingegeben wurde, muss er nun kompiliert werden. Das geschieht durch: gcc -o 8x8-matrix-2x595.exe 8x8-matrix-2x595.c -lwiringPi -o steht für output und gibt an, wohin die exe geschrieben werden soll. Aus reiner Windows-Gewohnheit und um es selbst schneller zu erkennen, hat ich der exe die Endung .exe verpasst. Linux kommt aber auch ohne dieser Endung aus. Dann folgt die Textdatei mit dem C-Code, in diesem Fall 8x8-matrix-2x595.c und hinter dem -l folgt dann die Library wiringPi, die zusätzliche mit eingebunden werden muss, weil wir ja Funktionen daraus benutzen wollen.

Der Compiler wird nun haarklein jeden Syntax-Fehler ankreiden, den wir im Source-Code haben, oder - was uns natürich lieber ist - einfach schweigen. Dann gab es nichts zu meckern und die fertige exe liegt vor. Diese können wir mit ./8x8-matrix-2x595.exe starten. Wollen wir z. B. den Priorisierungsbefehl aus der WiringPi-API benutzen, dann müssen wir noch ein sudo davorsetzen, sonst bleiben Root-Funktionsaufrufe wirkungslos oder führen zu Fehlern, je nachdem wie wir das programmiert haben.

Auf ein Video vom Ergebnis verzichte ich. Denn das sieht genau so aus wie das beim Python-Code.