Einen PIR-Bewegungsmelder (HC-SR501) mit dem Raspberry Pi ansteuern

Ich möchte mir, aufbauend auf meinem letzten Projekt LCD-Ansteuerung ein Always-On-Display zusammenbauen, dass mir jederzeit die für mich wichtigen Infos auf mehreren durchschaltbaren Screens anzeigt. Das Durchschalten (oder auch die Direktanwahl einer der bis zu 9 Screens) hatte ich mir per Infrarot-Fernbedienung vorgestellt. Angezeigt werden sollen später einmal
Den ersten Screen habe ich schon zusammen gezimmert. In der ersten Zeile stehen Wochentag, Datum und Uhrzeit.

In der dritten nach einem CPU-Symbol die CPU-Auslastung des Raspberrys, die Taktfrequenz in Gigahertz und die aktuelle CPU-Temperator.

In der vierten Zeile ein RAM-Symbol, dahinter, wieviel MB Speicher noch frei sind und dann noch ein SD-Karten-Symbol und der freie Speicherplatz darauf.

Die zweite Zeile habe ich erstmal frei gelassen für Temperatur, Luftfeuchtigkeit und was mir an Sensoren noch so in die Hänge fällt.


Übrigens ist inzwischen mein Cobbler-Board angekommen, um die Anschlüsse des Raspis über ein 40 poliges Flachbandkabel auf das Breadboard zu bringen. "GPIO Extension Board" haben die Chinesen noch richtig geschrieben bekommen, aber was soll GNL sein? Oder der Pin Nr. 28. Der kann sich anscheinend nicht zwischen ID_SC und GPIO12 entscheiden, steht doch beides dran.

Tolle Hilfe: Pinbeschriftungen, auf die man sich nicht verlassen kann. Der Hohn dabei ist ja, dass auf dem Produktfoto bei eBay alles korrekt gedruckt war.

Ich habe darum meine eigenen Beschriftungen mit Excel gestaltet und ausgedruckt (was für eine Fitzel-Arbeit, das genau passend zu kriegen!) und dann drübergeklebt.

Und ich habe den Cobbler gleich eingesetzt. Dadurch musste ich die Schaltung neu aufbauen und da die Pins nicht mehr von rechts nach links verlaufen, sondern "richtig" herum, haben sich auch die Pins für die Datenleitungen geändert. Im neuen Programm also diese Änderungen berücksichtigen.






Die neue Verdrahtung des LCD sieht nun so aus: LCD Raspi VSS Masse VDD 3.3V oder 5V, je nach Modell V0 über 5 kΩ-Poti an GND, für Kontrast RS an BCM 9, bestimmt ob Kommando oder Zeichen RW GND (wir wollen nur schreiben, also immer Low) E an BCM 11 (Enable-Leitung, Pulse, wenn Sendung fertig) D4 an BCM 5 (Datenleitung 1) D5 an BCM 6 (Datenleitung 2) D6 an BCM 13 (Datenleitung 3) D7 an BCM 19 (Datenleitung 4) A an BCM 26 über 470 Ω-Poti (Backlight an/aus, Helligkeit) K GND
Der LCD-Anschluss über ein 16-poliges Flachbandkabel an mein 20x4-Character-Display ist so nah mit möglich an den Cobbler rangerückt, damit rechts noch Platz für weitere Sensoren bleibt. Ist der auch verbraucht, muss halt mit weiteren Boards oben erweitert werden.

Die einpoligen Seiten der Potis habe ich gleich in die Anschlüsse V0 und A gesteckt, so ist der Platz oberhalb nicht verschwendet und bei der Länge der Potis müssen diese eh beide Boardhälfte (oben und unten) berühren.

Aber die Schaltung, auch wenn die Kabel und Bauteile ein wenig den Platz gewechselt haben, kennen wir ja so bereits aus meinem letzten Projekt.





Heute hinzukommen soll ein sogenannter PIR (für Passive InfraRed)-Sensor, zu deutsch: Infrarot-Bewegungsmelder. Das sind die Dinger, die registrieren, ob jemand an ihnen vorbeiläuft und dann automatisch das Licht anschalten. Die Dinger stecken auch in Alarmanlagen.

Technisch funktioniert das Bauteil so, dass es die abgegeben Wärmestrahlung (die von jedem Menschen, aber auch von Hund und Katze, abgegeben wird) erfasst und dabei Änderungen feststellt. Das signalisiert es durch ein High an OUT-Pin.

Mit dem linken der zwei Potis (gesehen mit Blick auf die Unterseite, wenn der gelbe Jumper links ist) kann man die Empfindlichkeit des Bauteils einstellen: Je weiter rechts, desto unempfindlicher reagiert der Sensor auf Bewegungen. Aber wie groß muss die zum Auslösen nötige Bewegung sein? Auf einen Käfer soll es wohl eher nicht reagieren. Aber wir wollen ja auch nicht vor dem Sensor stehen wollen und wild mit den Armen wedeln, bis er mal registriert, dass sich da etwas bewegt hat.

Mit dem rechten Poti regelt man die Haltezeit, also die Zeitspanne, wie lange das Signal nach einer Bewegungserkennung auf HIGH geht. Ist das Poti links am Anschlag, dann beträgt sie eins bis zwei Sekunden, rechts am Anschlag dauert es 5 Minuten, bis der PIR wieder auf LOW geht. Die Haltezeit ist z. B. dafür gut, das Flurlicht so und so lange anzulassen, wenn das schaltene Relais sich am High des PIR orientiert. Nach ein paar Sekunden oder Minuten geht es dann wieder automatisch aus.

Mit dem Jumper (soweit er denn vorhanden ist) kann man schließlich einstellen, obwohl ein langes, einzelnes (Stellung L) oder ein gepulstes (Stellung H) HIGH haben will. Das gepulste HIGH macht nur Sinn für Schaltungen, die auf Flankenveränderungen prüfen. Da wir direkt auf den Zustand prüfen (also ob LOW oder HIGH) ist L für uns die richtige Stellung.


Auf der Unterseite der Platine befindet sich auch der Header zum Anschluss des PIR-Sensors an den Raspi. Mehr als Versorgunsspannung, Masse und einem Kabel, dass an OUT angeschlossen wird, braucht es nicht.

Die technischen Daten lauten: Versorgungsspannung: 5-20 V Gleichstrom Stromaufnahme: 65 mA Output Level High: 3.3V, Low: 0V Haltezeit: 5 ... 300 Sekunden Betriebstemperatur: -15 ... +70 °C Operationswinkel: 120 °, 7 Meter Da der Spannung, die an OUT anliegt, auf 3.3V begrenzt ist, können wir den PIR auch bedenkenlos an +5V betreiben. Damit funktionieren sie sicher. Manche meiner Modelle haben aber auch klaglos mit 3.3V Versorgungsspannung funktioniert.

Das OUT-Kabel schließen wir an BCM 10 des Raspi an, GND dürfte klar sein.

BCM 10 wird dann natürlich als Eingang definiert. Außerdem schalten wir den internen PullDown-Widerstand dazu, um immer einen klar definierten Zustand zu haben.


So sieht der PIR-Sensor übrigens ohne die Plastikhaube aus. Das kleine rechteckige Fenster wertet die eintreffende Infrarotstrahlung aus.

Wozu die Plastikhaube genau gut ist und warum sie genau diese Form hat, weiß ich auch nicht genau.

Ich schätze mal, damit kann man einen größeren Betrachtungswinkel realisieren. Vielleicht wird damit auch zusätzlich ungewollte Strahlung gefiltert.






Der Aufbau funktioniert wunderbar. Ich habe das Timing so eingestellt, dass das Display ausgeht, wenn ich mich länger als 2 Minuten davor nicht bewegt habe, was eigentlich nie der Fall ist, wenn ich vorm Computer sitze.

Und wenn ich den Platz verlasse, geht der Bildschirm automatisch nach etwa 2 Minuten aus und schont die LED der Hintergrundbeleuchtung (und spart ein wenig Strom).

Sobald ich wiederkomme, geht die Hintergrundbeleuchtung wieder an und das Display ist wieder gut ablesbar.


Hier der erweiterte und angepasste Code aus dem letzten Projekt:

lcd-2004a-pir.py (klicken, um diesen Abschnitt aufzuklappen)
# -*- encoding: utf-8 -*- # (C) 2018 by Oliver Kuhlemann # Bei Verwendung freue ich mich über Namensnennung, # Quellenangabe und Verlinkung # Quelle: http://cool-web.de/raspberry/ import RPi.GPIO as GPIO # Funktionen für die GPIO-Ansteuerung laden from time import sleep # damit müssen wir nur noch sleep() statt time.sleep schreiben import time import datetime from sys import exit # um das Programm ggf. vorzeitg zu beenden import os from subprocess import PIPE, Popen GPIO.setmode(GPIO.BCM) # die GPIO-Pins im BCM-Modus ansprechen pinLcdRS=9 # OUT LCD Command / Character Switch Leitung pinLcdE=11 # OUT LCD Enable-Leitung pinLcdData=[5,6,13,19] # OUT wir benutzen den 4-Pin-Data-Mode für das LCD pinLcdBL=26 # OUT LCD lcdBacklight pinPIR=10 # IN PIR-Bewegungsmelder # GPIO-Ports initialisieren GPIO.setup(pinLcdRS, GPIO.OUT) GPIO.setup(pinLcdE, GPIO.OUT) GPIO.setup(pinLcdBL, GPIO.OUT) for pin in pinLcdData: GPIO.setup(pin, GPIO.OUT) GPIO.setup(pinPIR, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) lcdPulseDur = 0.001 lcdDelayDur = 0.001 # Defnitionen für ein 1602-Display #lcdWidth = 16 #lcdHeight= 2 #addrLines=[0x00,0x40] # Defnitionen für ein 2004-Display lcdWidth = 20 lcdHeight= 4 addrLines=[0x00,0x40,0x14,0x54] # Dictionary der Kommandos cmds={"clear":0x01, # löscht alle Zeichen "home":0x02, # springt an den Anfang (line 0, col 0) "off":0b1000, # schaltet LCD aus (merkt sich aber den Text) "on":0b1100, # schaltet LCD wieder ein mit Text (praktisch zum blinken) "curon":0b1111, # cursor an "curoff":0b1100 # cursor wieder aus } # Folgende ASCII-Zeichen werden auf das richtig LCD-Zeichen übersetzt zeichen="°äöüÄÖÜßµ€" ersatz=[223,225,239,245,225,239,245,226,228,227] def lcdBacklight(onOff): # 1 schaltet die Hintergrundbeleuchtung an GPIO.output(pinLcdBL, onOff) def pulseEnable(): # Einen Pulse auf die Enable-Leitung schicken sleep(lcdDelayDur) GPIO.output(pinLcdE, 1) sleep(lcdPulseDur) GPIO.output(pinLcdE, 0) sleep(lcdDelayDur) def lcdByte(byte): # ein Byte im 4-Bit-Mode senden # höherwertiges Halbbyte senden for bnr in range (4,8): bit=(byte & 2**bnr) / 2**bnr GPIO.output(pinLcdData[bnr-4], bit) pulseEnable() # niederwertiges Halbbyte senden for bnr in range (0,4): bit=(byte & 2**bnr) / 2**bnr GPIO.output(pinLcdData[bnr], bit) pulseEnable() def lcdCmd(cmd): # ein Kommando ans LCD senden try: byte=cmds[cmd] except KeyError: print "Das Kommando " + char + " ist nicht definiert." return lcdCmdByte(byte) def lcdCmdByte(byte): # Kommando als Byte-Wert senden GPIO.output(pinLcdRS, 0) # 0=command, 1=character lcdByte(byte) def lcdMsg(line, col, msg): # line ist die x. Zeile (gezählt ab 0) # col die Spalte (ab 0) lcdCmdByte(addrLines[line]+0x80+col) # Speicheradresse für Line/Col adressieren GPIO.output(pinLcdRS, 1) # 0=command, 1=character za=col+1 for char in msg: byte=(ord(char)) # ord gibt ASCII-Code eines Zeichens zurück p=zeichen.find(char) # einige zeichen sind nicht ASCII-konform, diese übersetzen if p >-1: byte=ersatz[p] lcdByte(byte) if za % lcdWidth == 0: line+=1 # Zeile zu lang, Zeilenumbruch und in nächster Zeile weiter try: lcdCmdByte(addrLines[line]+0x80) GPIO.output(pinLcdRS, 1) # und wieder zurück in den Zeichenmodus except IndexError: # Display voll break za+=1 def lcdInit(): # LCD initialisieren lcdCmdByte(0x33) # 110011 Initialize lcdCmdByte(0x32) # 110010 Initialize lcdCmdByte (0b101000) # Function Set Command # ^----- DL: 4-bit-mode (optional 8-bit-Mode) # ^---- N: 2-line-mode (optional 1-line-mode) # ^--- F: 5x8 font (optional 5x11 font) lcdCmdByte (0b1100) # Display on/off Command # ^----- D: Display on # ^---- C: Cursor off # ^--- B: Cursor Pos. off lcdCmdByte (0b110) # Cursor or Display Shift Command # ^----- S/C: 1=Shift, 0=Cursor # ^---- R/L: 1=Right, 0=Left # lcdCmd("clear") def showZs(): # zeigt die eingebauten Zeichen an lcdCmd("clear") beg=32 c=beg line=0 lcdCmdByte(addrLines[0]+0x80) while c <256: GPIO.output(pinLcdRS, 1) lcdByte (c) c+=1 if (c-beg) % (lcdHeight*lcdWidth) == 0: sleep(3) lcdCmd("clear") line=0 elif (c-beg) % lcdWidth == 0: line+=1 lcdCmdByte(addrLines[line]+0x80) GPIO.output(pinLcdRS, 1) def showZsCGRAM(): # zeigt die selbst definierten Zeichen an lcdMsg(0,0,"own CGRAM chars:") lcdCmdByte(addrLines[1]+0x80) GPIO.output(pinLcdRS, 1) for c in range (0,8): lcdByte (c) for c in range (0,8): lcdByte(32) def saveCharToCGRAM(charNr, arrMuster): # speichert ein eigenes Zeichen lcdCmdByte(charNr*8+0x40) GPIO.output(pinLcdRS, 1) for byte in arrMuster: lcdByte(byte) sleep (lcdDelayDur) def loadCustomChars(): # definiert 8 eigene Zeichen (ansprechbar über chr(0) bis chr(7) muster = [ 0b10000 , # Strich Breite 1 0b10000 , 0b10000 , 0b10000 , 0b10000 , 0b10000 , 0b10000 , 0b10000 ] saveCharToCGRAM (0, muster) muster = [ 0b11000 , # Balken Breite 2 0b11000 , 0b11000 , 0b11000 , 0b11000 , 0b11000 , 0b11000 , 0b11000 ] saveCharToCGRAM (1, muster) muster = [ 0b11100 , # Balken Breite 3 0b11100 , 0b11100 , 0b11100 , 0b11100 , 0b11100 , 0b11100 , 0b11100 ] saveCharToCGRAM (2, muster) muster = [ 0b11110 , # Balken Breite 4 0b11110 , 0b11110 , 0b11110 , 0b11110 , 0b11110 , 0b11110 , 0b11110 ] saveCharToCGRAM (3, muster) muster = [ 0b01110 , # Batterie-Symbol 0b11111 , 0b10001 , 0b10001 , 0b10001 , 0b10001 , 0b10001 , 0b11111 ] saveCharToCGRAM (4, muster) muster = [ 0b00000 , # CPU-Symbol 0b11111 , 0b01110 , 0b11111 , 0b01110 , 0b11111 , 0b00000 , 0b00000 ] saveCharToCGRAM (5, muster) muster = [ 0b01110 , # RAM-Symbol 0b11011 , 0b01010 , 0b11011 , 0b01010 , 0b11011 , 0b01110 , 0b00000 ] saveCharToCGRAM (6, muster) muster = [ 0b00000 , # SD-Karten-Symbol 0b11110 , 0b10001 , 0b10001 , 0b10001 , 0b10001 , 0b11111 , 0b00000 ] saveCharToCGRAM (7, muster) def loadCustomSmiley (charNr): muster = [ 0b00000 , # Smiley 0b01010 , 0b01010 , 0b00000 , 0b10001 , 0b10001 , 0b01110 , 0b00000 ] saveCharToCGRAM (charNr, muster) def loadCustomCheckmark (charNr): muster = [ 0b00001 , # Checkmark 0b00001 , 0b00010 , 0b00010 , 0b10100 , 0b10100 , 0b01000 , 0b00000 ] saveCharToCGRAM (charNr, muster) def loadCustomEuro(charNr): muster = [ 0b00011 , # Euro-Zeichen 0b00100 , 0b11111 , 0b01000 , 0b11111 , 0b00100, 0b00011 , 0b00000 ] saveCharToCGRAM (charNr, muster) def showProzentBalken(line,startCol,prozent): # zeigt einen fein abgestuften Prozent-Balken # benutzt selbstdefinierte Chars if prozent == 0: # es gibt nichts anzuzeigen return(0) breiteGes=lcdWidth*5-startCol*5 breite=int(breiteGes*prozent/100)+1 if breite > breiteGes: breite = breiteGes voll=int(breite/5) rest=breite % 5 # die vollen 5-Balken darstellen (char Nr. 255) lcdCmdByte(addrLines[line]+0x80+startCol) GPIO.output(pinLcdRS, 1) for n in range (0,voll): lcdByte (255) # gefolgt vom Rest if rest > 0: lcdByte(rest-1) # --- Ende Funktionen --- Beginn Hauptprogramm ------------------------------------------- try: lcdInit() # wichtig. Ohne diesen Befehl macht das LCD nur Müll. loadCustomChars() # selbst definierte Zeichen laden screen=1 # voreingestellter Screen weekdays=["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"] lcdBacklight(1) # lcdBacklight anschalten seksPerMove = 2*60 # wieviele Sekunden bleibt das Display pro Bewegung an? lastMove = time.time() # Variablen zum Wertevergleich, um unnötige Ausgaben auf LCD zu sparen altDatum="" altSdGb="" altScreen="" altRamMbFree="" altTakt="" za=0 # wird in der Schleife hochgezählt, so kann z. B. nur alle 100 Sek. etwas gemacht werden while 1: ts=time.time() if GPIO.input(pinPIR) == GPIO.HIGH: # Es wurde eine Bewegung festgestellt lastMove = time.time() lcdBacklight(1) seksUnbewegt = int(ts - lastMove) if seksUnbewegt > seksPerMove: lcdBacklight(0) # print GPIO.input(pinPIR), seksUnbewegt if screen ==1: # Datum, Zeit (mit Wochentag) ts = datetime.datetime.now() weekday= int(ts.strftime("%w")) datum=weekdays[weekday] + ts.strftime(" %d.%m.%y") zeit=ts.strftime("%H:%M:%S") if datum <> altDatum: lcdMsg (0,0,datum) lcdMsg (0,12,zeit) altDatum=datum # CPU-Auslastung proc=os.popen("ps -eo pcpu") lines=proc.readlines() proc.close() cpu=0.0 for i in range (1,len(lines)): cpu += float(lines[i]) if cpu > 100: cpu = 100 # weil mehr nicht sein kann msg= chr(5)+ str(cpu) + "% " lcdMsg (2,0,msg) # Takt proc=os.popen(" vcgencmd measure_clock arm | awk -F '=' '{print $2}'") takt=round(float(proc.readline())/1000000000,1) proc.close() takt=str(takt)+"GHz " if takt <> altTakt: lcdMsg (2,8,takt) altTakt=takt # Temperatur proc=os.popen("cat /sys/class/thermal/thermal_zone*/temp") temp=float(proc.readline()) proc.close() msg=str(round(temp/1000,1)) + "°" lcdMsg (2,15,msg) # RAM frei proc=os.popen(" free --mega | awk '{print $4}'") lines=proc.readlines() ramMbFree=lines[1].replace(chr(10),"") proc.close() ramMbFree=chr(6)+""+ ramMbFree + "MB " if ramMbFree <> altRamMbFree: lcdMsg (3,0,ramMbFree) altRamMbFree = ramMbFree # GB auf der SD free proc=os.popen("df -h / | awk '{print $3}'") lines=proc.readlines() sdGb=lines[1].replace(",",".").replace("G","").replace(chr(10),"") proc.close() sdGb=chr(7)+""+ sdGb + "GB " if sdGb <> altSdGb: lcdMsg (3,8,sdGb) altSdGb=sdGb if altScreen <>"1/1": lcdMsg (3,17,"1/1") altScreen="1/1" sleep (1) za += 1 except KeyboardInterrupt: pass lcdCmd("clear") lcdCmd("off") GPIO.cleanup() # Programm sauber verlassen und Ressourcen wieder freigeben

Der bezüglich des PIR-Sensors hinzugekommene Code: pinPIR=10 # IN PIR-Bewegungsmelder GPIO.setup(pinPIR, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) ... seksPerMove = 2*60 # wieviele Sekunden bleibt das Display pro Bewegung an? ... if GPIO.input(pinPIR) == GPIO.HIGH: # Es wurde eine Bewegung festgestellt lastMove = time.time() lcdBacklight(1) seksUnbewegt = int(ts - lastMove) if seksUnbewegt > seksPerMove: lcdBacklight(0) ... Die Leitung für den PIR-Sensor wird wie angeschlossen als Pin BCM 10 definiert und als Input mit PullDown gesetupt. In der Schleife wird dann der Input abgefragt. Bei jeder Bewegung geht der Sensor für ca. 3 Sekunden auf High. Dann wird sofort das Backlight eingeschaltet (bzw. bleibt an). Wurde die definierte Zeit von 120 Sekunden keine Bewegung festgestellt, wird die Hintergrundbeleuchtung abgeschaltet.