Hardwarenahe Programmierung am Raspberry Pi

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

von Benutzer:Fpgakuechle (Volker Urban)

2015-01-24: 1. Stadium: "Materialsammlung", Grobgliederung

In diesem Artikel soll gezeigt werden wie typische Mikrocontroller-Funktionen wie Pin toggeling, external Interrupt, Nutzung interner timer, etc auf dem Raspberry Pi hardwarenahe realisiert werden.

"Bare metal" oder Betriebssystem?[Bearbeiten]

Der RasPi kann auch wie ein normaler 8-bit Controller ohne Betriebssystem genutzt werden. Dafür hat sich die Bezeichnung "bare metal" etabliert. Auf diese Weise betrieben ist unbeschränkter Zugriff auf alle Register des SoC wie Timer, GPIO-dataIn etc möglich. ?Wechsel von Linux in bare metal mode?

Betriebssysteme beschränken den Zugriff auf die "Hardware". Register sind nicht direkt von user-programmen beschreib- oder lesbar. Lässt man allerdings dieses User-Programm mit root-Rechten laufen und nutzt eine passende Bibliothek, gelingt dieser Zugriff.

Unter http://codeandlife.com/2012/07/03/benchmarking-raspberry-pi-gpio-speed/ findet sich ein benchmark der Schaltraten an den GPIO-Pins misst. Zusammengefasst sind unter Linux - auch in C programmiert - Schaltraten von 2-7 MHz möglich. Bei Scriptsprachen wie perl/python ist schon bei weniger als 50 kHz Schluss.

GPIO Pin Ansteuerung[Bearbeiten]

für einfachste Anwendungen wie Taster oder LED-blinker müssen folgenden hardwarenahe Funktionen realisiert sein.

  • Konfiguration der Richtung der benutzten GPIO's (modus In oder Output)
  • Schalten der Eingangpins in den HIGH oder Low-Signalpegel
  • Lesen des Signalpegels am Eingangpins

Einige Digital-IC treiben die Ausgangpins nicht aktiv, also entweder High oder LOW, sondern nutzen auch die Möglichkeit, den Ausgang "abzuschalten" (hochohmig). Beispielsweise können so mehrer Ausgänge miteinanderkurzgeschlossen werden, und der aktive Pegel HIGH wird über einen Widerstand (PullUp) gegen HIGH-Pegel realisiert (OpenCollector, OpenDrain-Ausgangstreiber). Alternativ sind Widerstände gegen GND möglich, die einen LOW-Zustand auf der Leitung erzeugen. Im Raspberry können interne PullUp oder Down Widerstände aktiviert werden, falls sich auf der Schaltung am Pin nicht ein solcher Widerstand befindet:

  • Modus interne Pull-Up/Down Zustand ändern

Um schnell auf Änderungen an einem Eingangspin zu reagieren, kann dieses als externer Interrupteingäng konfiguriert werden. Bei einem Pegelwechsel wird dann die Interuptserviceroutine ausgeführt.

  • Auslösen Interrupt bei Zustandwechsel am Pin


Alle diese Funktionen stellt die wiredPi Bibliothek zur Verfügung.

Mikrosekundengenaue Pausen und Messungen[Bearbeiten]

Linux/Raspbian auf dem Raspberry ist kein ideales Betriebssystem, um mikrosekundengenaue Aktionen auszulesen oder zu messen, da durch das timeslot basierende Multitasking der Task gerade immer mal wieder inaktiv wird und ein anderer Task tätig ist. Es gibt Möglichkeiten, diese Szenario zu entschärfen, beispielsweise die Priorität hochsetzen.

Zeitmessung[Bearbeiten]

Unter einem nicht dafür ausgelegten Betriebssystem sind kurze Zeitmessungen im Mikrosekundenbereich unvermeidlich ungenau, Linux gehört dazu. Die wiredPi Bibliothek stellt auch eine für die hardwarenahe Programmierung nützliche Funktionen wie einen mikrosekunden genauen Wait zu Verfügung.


In Linux liefert die Funktion gettimeofday einen mikrosekundegenauen Zeitpunkt, der beim Raspberry durch Auslesen des mikrosekundengenauen Timers auf dem Broadcom-Chip ermittelt wird. Diese Angabe kann aber stark schwanken, wohl auch weil die Ausführungszeiten von gettimeofday nicht vernachlässigbar sind.

     struct timeval tiva;
     gettimeofday (&tiva,NULL);
     unsigned int time_dur_us;

     time_dur_us = tiva.tv_usec; // microseconds

In der wiredPi-Bibliothek findet sich eine vereinfachte Funktion, die nur die Mikrosekunden zurückgibt:

unsigned int micros (void);

unsigned int time_dur_us;
time_dur_us = micros();

Dadurch wird sie schneller und genauer ausgeführt. Der zurückgebende Wert entspricht den Mikrosekunden seit Aufruf der Funktion wiringPiSetup. Die Zählung beginnt etwa aller 71 Minuten neu.

Pausen[Bearbeiten]

Im Raspian gibt es die Pausen:

  • usleep(int usec) - für mikrosekunden genauen Pause
  • nanosleep(int nanosec) - für Nanosekunden genauen Pause

beide Funktion garantieren aber nur, dass die Pause mindestens so lang wie angefordert ist. Es kann aber auch deutlich länger sein.

Die wiredPi hat ihre eigene Funktion für Mikrosekunden, die aber einen messbaren Sprung zwischen 99 und 100 aufweist:

void delayMicroseconds (unsigned int howLong)

delayMicroseconds(100);

Die Ursache dafür findet sich in der Implementierung. Bis 99 wird eine hart codierte (polling-)Schleife durchlaufen, die kontinuirlich die Systemzeit abtestet. Ab 100 wird stattdessen die systemfunktion nanosleep() verwendet.


Priorität hochsetzen[Bearbeiten]

Durch ein Hochsetzen der Taskpriorität kann man versuchen, die multitasking-bedingten Schwankungen bei kurzen Pausen und Messungen auszugleichen. Die wiredPi Bibliothek bietet dazu die Funktion:

int piHiPri (int priority);
/*99 :max priority - 0 :default */
piHiPri(40);

unsigned int time_dur_us;
time_dur_us = micros();

Verzögerungen[Bearbeiten]

  • usleep
  • nanosleep()

CPU-Core und Assembler[Bearbeiten]

Sich aus Gründen der Optimierung den aus dem C-Code generierten Assemblercode anzuschauen ist eine gute Idee. Für den GNU-compiler gelingt das mit

gcc -o {output_filename} -S  -fverbose_asm   {source-files}

Entscheidend ist der Schalter -S. Dieser veranlasst den Compiler, vor dem Linken anzuhalten und den Assembler-Quelltext in die Datei outpu_filename.S zu schreiben. Die Option -fverbose_asm bringt den Compiler dazu, "verständlicheren" Assemblercode zu schreiben. Natürlich müssen auch die für einen normale C-compilation nötigen Include-Pfade, Optimierungsvarianten etc. angegeben werden.

Wer bereits Assembler-Code für den ARM-Cortex kennt, wird überrascht sein: der für den Raspberry mit einem ARM11 erzeugte Code schaut deutlich anders aus. Ursache dafür ist, dass der ARM-11 mit der Version v6 des ARM's Befehlssatz erstellt wurde, die Cortex-Reihe dagegen mit v7. Hinzu kommt, dass auch THUMB-Befehle im Code eingestreut sein könnten.

.L25:
	ldr	r7, [r6, #-4]!	@ D.3058, MEM[base: D.3166_19, offset: 0B]
	mov	r1, #1	@,
	mov	r0, r7	@, D.3058
	bl	digitalWrite	@
	mov	r0, #100	@,
	bl	delayMicroseconds	@
	mov	r0, #1	@,
	bl	digitalRead	@
	cmp	r0, #0	@,
	addeq	r4, r4, r5	@ intervall_low_bound, intervall_low_bound, intervall_halfwidth
	beq	.L24	@,
	mov	r0, r7	@, D.3058
	mov	r1, #0	@,
	add	r5, r5, r5, lsr #31	@, tmp155, intervall_halfwidth, intervall_halfwidth,
	bl	digitalWrite	@
	cmp	r6, r8	@ ivtmp.68, C_PIN_2POW.76
	mov	r5, r5, asr #1	@ intervall_halfwidth, tmp155,

"Fallgruben" für 8/16 bit Programmierer[Bearbeiten]

Wer bisher mit den kleineren Controllern gearbeitet hat und sich stur an Mikrocontroller-Routinen hält, verliert bei ARM an Performance. Folgende Hinweise sollte man daher beherzigen:

  • Für lokale Variablen 32 bit Datentypen (integer) verwenden

Der ARM muss bei 8 bit Operationen extra Befehle (UXTB) zum korrekten Setzen des Carry-bits einfügen

  • Nicht jede 32 bit Zahl kann als Direktoperand (immediate operand) benutzt werden.

Da die Befehle des ARM-Cores immer nur 32 bit lang sind, kann nicht jeder 32 bit Integer in den Befehl "gepackt" werden. Dafür stehen nur 12 bit im OPcode zur Verfügung: 8 bit als Direktoperand und 4 bit die angeben, um wie viel bit die 8 bits nach rechts rotiert (ROR) werden, wobei die korrekte Verschiebeweite das doppelte der 4 bit Zahl ist. Genaueres dort: [1].

Interessante Links[Bearbeiten]

Tips für "bessere Echtzeit" mit Linux bzw. Raspi