Wie das Datenblatt des APDS9960 zeigt, erfolgt die Steuerung dieses Bausteins über einige Dutzend von Registern; häufig finden wir dabei auch Register, bei denen die einzelnen Bits ganz unterschiedliche Bedeutung haben. Es wäre sehr mühsam und unübersichtlich, zur Programmierung des APDS9960 jeweils direkt auf diese Register bzw. deren einzelne Bits zuzugreifen. Deswegen benutzen wir ein Modul, welches solche Zugriffe einfacher und übersichtlicher macht.
Ich benutze hier das Modul APDS9960_ALS_PROX.py. Dieses geht auf das Modul APDS9960LITE.py von Rune Langøy zurück, welches ich ein wenig überarbeitet habe.
Dieses Modul stellt zunächst eine Klasse I2CEX zur Verfügung; diese Klasse beinhaltet Methoden für den I2C-Zugriff auf die einzelnen Register des APDS9960. Die Klasse I2CEX vererbt diese Eigenschaften an die Klassen APDS9960, PROX und ALS.
Die Klasse PROX beinhaltet Eigenschaften und Methoden zur Abstandsmessung; von diesen werden wir in diesem Beitrag Gebrauch machen. Bei der Klasse ALS geht es um Lichtmessungen; darauf werden wir in einem anderem Beitrag zu sprechen kommen. Die Klasse APDS9960 bietet selbst nur wenige, aber wichtige “Funktionen” an:
- Initialisierung der I2C-Schnittstelle (mit Hilfe ihrer Oberklasse I2CEX)
- das Attribut prox, welches eine Instanz der Klasse PROX ist
- das Attribut als, welches eine Instanz der Klasse ALS ist.
Code: Alles auswählen
import APDS9960_ALS_PROX # Modul importieren
i2c = I2C(1, sda = Pin(22), scl = Pin(21)) # i2c-Instanz erzeugen
apds9960=APDS9960_ALS_PROX.APDS9960(i2c) # apds-Instanz erzeugen
s = apds9960.prox # Instanz von PROX (als Eigenschaft von apds9960)
s.enableSensor() # Proximity-Sensor aktivieren
Erste Abstandsmessungen
Kommen wir nun zu ersten Messungen! Dazu verbinden wir das APDS9960-Modul mit dem TTGO wie in dem Beitrag Der APDS9960-Sensor: Grundlagen dargestellt; zusätzlich schließen wir den Anschluss VL des Moduls an den 3V-Pin des TTGO an; dadurch wird auf dem Modul eine IR-LED mit Strom versorgt.
Die Stromversorgung kann auch direkt durch das APDS9960-Modul erfolgen; dazu muss man die beiden Lötstellen bei PS mit Lötzinn verbinden. In der folgenden Abbildung sind sie im rechten Bereich mit der Kennung SJ2 versehen. Dort kann man übrigens auch im linken Teil die Lötstellen erkennen, mit denen man einen Pullup für SCL und SDA aktivieren kann.
Die Abstandsmessung erfolgt nun so: Wenn der Proximity-Sensor mit dem Befehl s.enableSensor(True) aktiviert worden ist, dann sendet das Modul kurze IR-Lichtimpulse aus. Befindet sich ein Gegenstand oberhalb des Moduls, reflektiert er das Licht; dieses wird nun von mehreren Photodioden registriert (vgl. Abb. 2 von Der APDS9960-Sensor: Grundlagen). Wieviel Licht auf diese Dioden gelangt, hängt von dem Abstand des Gegenstandes zum Modul ab. Das Modul verstärkt jetzt die Signale und berechnet daraus einen Proximity-Wert. Diesen kann man mit dem Befehl
print(s.proximityLevel)
auf dem Terminal anzeigen lassen. Hier das gesamte Programm:
Code: Alles auswählen
from machine import Pin, I2C
from time import sleep_ms
import APDS9960_ALS_PROX
# Init I2C
i2c = I2C(1, sda = Pin(22), scl = Pin(21))
apds9960=APDS9960_ALS_PROX.APDS9960(i2c) # apds9960-Instanz erstellen; auch: Power-ON
s = apds9960.prox # mit apds9960 werden auch apds9960.prox (und apds9960.als) erzeugt
s.enableSensor(True) # Proximity-Sensor einschalten
sleep_ms(7) # EXIT SLEEP (vgl. "ASPD9960: Ein Blick hinter die Kulissen"
s.eProximityGain = 1 # Verstärkungsgrad des empfangenen Signals, Standardwert: 0
while True:
sleep_ms(200) # auf nächsten Messwert warten
print(s.proximityLevel) # proximity-Wert ermitteln und am Terminal anzeigen
Bemerkungen:
1. Die Methode enableSensor() erwartet als Parameter Wahrheitswerte, also True oder False. Ist der Parameter True, dann wird der Sensor eingeschaltet, ist er False, dann wird er ausgeschaltet. Wird kein Parameter (in den Klammern!) angegeben, dann wird automatisch der Default-Wert True angenommen.
2. Auch wenn es so aussieht, es handelt sich bei s.eProximityGain und s.proximityLevel NICHT einfach um Attribute von der Instanz s. Vielmehr handelt es sich in diesen Fällen um so genannte Properties. Von einem solchen Property können wir zwar wie bei einem Attribut Werte erhalten, z. B. durch
wert = s.proximityLevel
Im Gegensatz zu Attributen steht dieser Wert aber nicht schon im Speicher des ESP32 zur Verfügung, sondern wird erst durch eine so genannte getter-Funktion besorgt. (Diese wird in dem Modul APDS9960_ALS_PROX.py definiert). In unserem Fall holt sich diese getter-Funktion mittels eines I2C-Lesevorgangs den Wert aus einem bestimmten Registers des APDS9960-Bausteins.
Bei der Programmzeile
s.eProximityGain = 1
ist es ganz ähnlich: Hier wird der Wert 1 nicht einfach in einem Speicherplatz des ESP32 abgelegt; vielmehr wird er mit Hilfe einer setter-Funktion durch einen I2C-Schreibvorgang in ein Register des APDS9960 geschrieben.
Mit dem Property eProximityGain können wir uns den Verstärkungsgrad auch wieder aus dem APDS9960 beschaffen:
g = s.eProximityGain
Hierbei kommt nun die getter-Funktion von dem eProximityGain-Property zum Einsatz.
Das Schöne an den Properties ist: Wir können Sie beim Schreiben eines Programms wie einfache Attribute behandeln und müssen uns nicht darum kümmern, dass hier in Wirklichkeit mehr oder weniger komplexe Aktionen durchgeführt werden.
Wer etwas mehr über Properties erfahren möchte, der sei auf https://realpython.com/python-property/ und https://www.programiz.com/python-programming/property verwiesen. Auch kann es hilfreich sein, sich einige der Property-Definitionen in dem Modul APDS9960_ALS_PROX.py anzuschauen.
3. Es ist nicht garantiert, dass zu jedem Zeitpunkt ein Proximity-Wert zur Verfügung steht. Man kann dies aber mit Hilfe des PVALID-Bits überprüfen. Mit Hilfe dieses Bits kann man ein Programm so lange warten lassen, bis ein Proximity-Wert zur Vergügung steht. Mehr dazu in dem Beitrag Der APDS9960-Sensor: Ein Blick hinter die Kulissen.
Abstands-Messungen mit Interrupt
Die Situation: Ein Gegenstand soll in einem bestimmten Abstandbereich bleiben. Wenn er diesen Bereich verlässt, soll das APDS9960-Modul ein Interrupt-Signal an den TTGO senden. Dies geschieht mit Hilfe des Ausgangs INT unten rechts am APDS9960-Modul. Dieser hat normalerweise den Zustand High. Mit wenigen Zeilen können wir dafür sorgen, dass dieser Ausgang auf Low geht, wenn unser Gegenstand diesen Bereich verlässt:
Code: Alles auswählen
# Interrupt konfigurieren: Ein Interrupt-Signal wird gegeben, wenn der Abstandswert außerhalb des Bereichs [8; 24] ist
min = 8
max = 24
s.clearInterrupt() # Löscht alle Non-Gesture Interrupts
s.setInterruptThreshold(low = min, high = max) # löst Interrupt aus (fallende Flanke an INT), wenn der Wert max überschritten oder der Wert von min unterschritten wird
s.enableInterrupt() # aktiviert Proximity-Interrupts, d. h. bei jedem Verlassen des Bereichs wird INT auf Low gezogen.
- die LED wieder ausgeschaltet werden,
- das Interrupt-Register wieder zurückgesetzt werden, damit INT wieder auf High geht und ein neuer Interrupt ausgelöst werden kann.
Code: Alles auswählen
# APDS9960_Proximity-Interrupt_Test.py
# letzte Änderung am 06.03.2023
# Wenn der Messwert unterhalb des low-Wertes oder oberhalb des High-Wertes liegt,
# wird eine LED (an Pin25) eingeschaltet.
#########################
# APDS9960 | TTGO #
# --------------------- #
# GND | G #
# VCC | 3V (3.3 V) #
# SDA | Pin22 #
# SCL | Pin21 #
# INT | Pin38 #
#########################
# LED an Pin25 #
#########################
from machine import Pin, I2C
from time import sleep_ms
import APDS9960_ALS_PROX
# Init I2C
i2c = I2C(1, sda = Pin(22), scl = Pin(21))
apds9960=APDS9960_ALS_PROX.APDS9960(i2c) # apds9960-Instanz erstellen; auch: Power-ON
s = apds9960.prox # Proximity Sensor
s.eProximityGain = 1 # Standardwert: 0
s.enableSensor() # Enable Light/Proximity Sensor depending on s
sleep_ms(7) # EXIT SLEEP
# Interrupt konfigurieren: Ein Interrupt-Signal wird gegeben, wenn der Abstandswert außerhalb des Bereichs [8; 24] ist...
min = 8
max = 24
LED = Pin(25, Pin.OUT) # LED an Pin25
s.clearInterrupt() # Löscht alle Non-Gesture Interrupts
s.setInterruptThreshold(low = min, high = max) # löst Interrupt aus (fallende Flanke an INT), wenn der Wert max überschritten oder Wert von min unterschritten wird
s.enableInterrupt() # aktiviert Proximity-Interrupts, d. h. bei jedem Verlassen des Bereichs wird INT auf Low gezogen
# Interrupt-Handler:
def handleMyPinInterrupt(myPin):
print('IR bei', myPin, 'wurde ausgelöst.')
LED.on() # LED einschalten
s.clearInterrupt() # Interrupt-Register muss zurückgesetzt werden, damit INT wieder auf High geht und ein neuer Interrupt ausgelöst werden kann
sleep_ms(10)
LED.off() # LED ausschalten
interrupt = Pin(38, Pin.IN) # Pin-Bezeichner wird automatisch als Parameter an den Interrupt-Handler übergeben
interrupt.irq(trigger = Pin.IRQ_FALLING, handler = handleMyPinInterrupt) # Interupt bei fallender Flanke auslösen
# Irgendetwas Anderes tun auch auch gar nichts...
while True:
pass
# sleep_ms(100)
# print('Messwert:',s.proximityLevel) # proximityLevel ausgeben