Ansteuerung eines Abstandssensors

Um mit dem Roboterprojekt Fuß zu fassen und eine robuste softwareseitige Grundlage zu schaffen, wurden parallel zur Planung der Chassis bereits Sensoren angesteuert. Die schwierigste Aufgabe ist der autonome Modus namens Lava Palaver. Hierbei muss der Roboter einer weißen Linie hinterherfahren.

 

Sicherlich ist es möglich, diese Aufgabe mit zwei Liniensensoren und einem durchdachten Algorithmus zu lösen. Dennoch bietet es sich an, weitere Sensoren zu verwenden, um das Fahrverhalten optimieren zu können.

 

In diesem Fall werden zur Ermittlung des seitlichen Abstands zu den Wänden sogenannte Abstandssensoren verbaut. Diese ermitteln durch das Aussenden und Empfangen von Infrarotlicht den Abstand zu einem Objekt. Leider besteht genau hier das Problem: Der Abstand wird als analoger Spannungswert an einem Pin in Referenz zur Masse angelegt. Leider kann der Raspberry Pi dieses Signal nicht direkt weiterverarbeiten.

 

Die technische Lösung bringt ein ADC. ADC steht für Analog-Digital-Konverter. Wie der Name bereits andeutet, wird das analog anliegende Spannungssignal durch Abtastung in ein digitales, zeitdiskretes Signal konvertiert.

 

Gewählt wird für dieses Projekt der IC MCP3204, welcher eine Auflösung von 12bit bietet. Dies bedeutet, dass zwischen der positiven Spannung V_DD und der negativen Referenzspannung V_SS insgesamt 2^12 = 4096 unterschiedliche Werte ausgelesen werden können. In unserem Fall gilt V_DD = 3,3V sowie V_SS = 0V. Damit beläuft sich die Feinheit auf Spannungsschritte von 0.0008V.

Dies scheint zunächst sehr präzise, dennoch ist es wichtig zu beachten, dass bereits eine geringfügige Spannungsänderung eine große Änderung für die Abstandsfunktion mit sich bringen kann.

 

Das ist sicherlich erstmal viel Theorie. Wichtig ist jetzt erstmal zu verstehen, wie der analog ausgelesene Wert in einen Spannungswert und dieser anschließend in eine Distanz umgerechnet werden kann.

Die Lösung dafür ist relativ einfach: Es bedarf mathematischer Funktionen.

 

Zunächst aber wollen wir erst einmal ein Programm schreiben, welches uns den analogen Spannungswert ausliest. Dazu wird das folgende Programm verwendet.

from spidev import SpiDev


class MCP3204:

def __init__(self, bus=0, device=0):
self.bus, self.device = bus, device
self.spi = SpiDev()
self.open()

def open(self):
self.spi.open(self.bus, self.device)

def read(self, channel=0) -> int:
if channel < 0 or channel > 3:
return -1
adc = self.spi.xfer2([0b00000110, channel << 6, 0])
data = ((adc[1] & 0b00001111) << 8) + adc[2]
return data

def close(self):
self.spi.close()

Sicherlich erscheint der Code zunächst recht überschaubar. Doch gerade die read()-Funktion verlangt einiges an Wissen über den verwendeten Analog-Digital-Konverter.

 

Zunächst muss ein Python-Objekt erstellt werden, welches die Hardware repräsentiert. Für dieses Projekt haben wir uns entschieden, dass jeder verbaute IC eine eigene Pythonklasse enthält und diese durch Erzeugung eines Objekts instanziiert wird.

 

Nachdem in Python ein Objekt erstellt wurde, wird zunächst die Funktion open() aufgerufen. Diese eröffnet die Verbindung zum IC über den SPI-Bus. Somit ist nun die Kommunikation möglich.

 

Möchte man nun endlich den analogen Wert auslesen, so muss die read()-Funktion aufgerufen werden. Hierbei ist zunächst relevant, dass abgefragt wird, welcher Kanal ausgelesen werden soll. Der MCP3204-ADC verfügt über vier Kanäle mit logischer Nummerierung von 0...3. Um Fehlverhalten zu vermeiden und gegebenenfalls falsche Rückgabewerte zu erhalten, wird der IC nicht angesprochen, wenn eine falsche Kanalnummer übergeben wird. In diesem Fall wird einfach der Wert -1 zurückgegeben.

Im Grunde genommen ist es egal, was die Funktion zurückgibt, wenn der IC nicht ausgelesen wird. Es ist jedoch wichtig, dass dadurch die Bijektivität der Funktion nicht verloren wird.

 

Was ist denn jetzt schon wieder Bijektivität und wieso ist diese wichtig?

Bijektivität bedeutet nur die Eindeutigkeit eines Ergebnisses. Wird ein analoger Wert ausgelesen, so ist dieser immer im Intervall I:=[0, 4096]. Auffallend ist hierbei, dass der Wert -1 nicht ausgelesen werden kann -  und genau deshalb wird dieser im Fehlerfall zurückgegeben. Die Bijektivität wäre nicht mehr gegeben, wenn beispielsweise der Wert 42 zurückgegeben wird. Hierbei lässt es sich nicht mehr nachvollziehen, ob nun ein Wert ausgelesen wurde, oder ein Fehlerfall eingetreten ist.

 

Anschließend werden mit dem Funktionsaufruf spi.xfer2(...) mehrere Bytes an den ADC gesendet. Doch was haben diese Bytes und jedes einzelne Bit zu bedeuten?

Das erste Bit besteht zunächst aus fünf Nullbits gefolgt von einem Einsbit, welches gleichzeitig das Startbit darstellt. Das nächste gesetzte Bit bedeutet, dass die Spannung eines Kanals in Referenz zur negativen Versorgungsspannung V_SS ausgelesen werden soll. Gemäß Datenblatt kann man ebenfalls die Differenz zweier Kanäle auslesen. In diesem Fall müsste das entsprechende Bit auf 0 gesetzt sein. Das niederwertigste Bit des ersten übermittelten Bytes beinhaltet das höchstwertigste Bit der Adresse.

Hier ist es wichtig zu verstehen, dass das Protokoll ebenfalls kompatibel ist für den MCP3208, welcher acht Kanäle bietet. Logischerweise benötigt man zur binären Kodierung der Zahl 4 lediglich zwei Bits, weshalb das höchstwertige Bit der Adresse einfach auf 0 gesetzt wird.

 

Das zweite übermittelte Byte beinhaltete lediglich nur noch die zwei niederwertigen Bits der Kanaladresse als höchstwertiger Position. Darum ist es wichtig, die übergebene Kanalnummer sechs Bits nach links zu shiften. Dies wird mit dem Operator << erreicht. Die restlichen Bits des zweiten Bytes sind irrelevant und werden deshalb auf 0 gesetzt.

 

Das dritte und zeitgleich letzte übermittelte Byte beinhaltet ebenfalls keine relevanten Daten mehr, weshalb lediglich ein Nullbyte übersendet wird.

 

Nach erfolgreichem Versenden der einzelnen Bytes wird die Antwort des ADC in Form eines Bytearrays entgegengenommen. Auch hier ist ein Blick in das Datenblatt zwingend notwendig, um zu verstehen, wo genau die 12 Bit-Zahl versteckt ist.

 

Das erste empfangene Byte, in obigem Codebeispiel also adc[0] beinhaltet keine relevanten Daten und wird deshalb nicht gelesen. Das zweite Byte enthält zunächst drei irrelevante Bits gefolgt von einem Nullbit. Lediglich die vier niederwertigsten Bits stellen die vier höchstwertigen Bits der 12bit-Zahl dar. Um die fragwürdigen Bits auszublenden, ist eine UND-Verknüpfung mit der Zahl 00001111 erforderlich. Somit bleiben nur die letzten vier Bits erhalten. Da noch acht weitere Bits folgen, müssen die vier Bits um acht Positionen nach links geshiftet werden.

Abschließend muss noch das letzte empfangene Byte aufaddiert werden.

 

Das Ergebnis ist ein ganzzahliger Wert im Bereich von 0 und 4096.

 

Wie berechnet man daraus die Spannung?
Generell gilt:

V = ADC / 4096 * V_DD

 

Da in diesem Fall eine Spannung von 3,3V anliegt, folgt somit

V = ADC/4096 * 3,3V

 

Dies bedeutet, dass bei einem Wert von 42 eine Spannung von

V = 42/4096 * 3,3V = 0.0338V

am ausgelesenen Kanal anliegt.

 

Das war jetzt bereits eine Menge Theorie, doch ein großer Teil der Arbeit ist bereits geschehen!

 

Nun muss lediglich ein Abstandswert, am besten in Millimetern, berechnet werden. Dazu ist es notwendig zu wissen, welcher Abstandssensor verwendet wird. In diesem Fall handelt es sich um den Sharp Distance Sensor GP2Y0A41SK0F. Dieser kann Distanzen zwischen 4 und 30cm erfassen. Leider enthält das Datenblatt keine Funktion, die durch Einsetzen der Spannung eine Distanz zurückliefert. Dennoch ist ein Diagramm enthalten, welches mehrere Messpunkte beinhaltet.

Mithilfe dieser Messwerte ist es möglich, mittels Polynominterpolation eine Funktion zu konstruieren, die nur eine minimale Abweichung zum Datenblatt bietet.

 

Polynominterpolation klingt vielleicht zunächst nicht ganz leicht, doch schwierig ist es nicht: Es werden zunächst Messpunkte ausgelesen. In unserem Fall wurden 15 Punkte aus dem Datenblatt ausgelesen. Mithilfe dieser Messpunkte ist es möglich ein Gleichungssystem aufzustellen und somit ein Polynom vierzehnten Grades zu definieren. Nach mehreren Versuchen wurde jedoch ersichtlich, dass ein Polynom sechsten Grades zu einer geringen Abweichung führt.

 

Die ermittelte Funktion lautet wie folgt:

d(x) = 4.6398 - 42.8394x + 160.9256x^2 - 320.3166x^3 + 367.7317x^4 - 245.9917x^5 + 88.7889x^6

mit Spannungswert x.

 

Wie im folgenden Diagramm zu sehen, liegen die roten Messwerte aus dem Datenblatt relativ präzise auf der entwickelten Polynomkurve (gelb).

 

Der Rückgabewert der Funktion d(x) wird in Zentimetern angegeben. Eine entsprechende Umrechnung in andere Längeneinheiten kann durch Abändern der Funktion erreicht werden.

Nachdem all diese Schritte korrekt ausgeführt wurden, können sinnvolle Abstände ausgelesen werden.

 

Wer weitere Präzision benötigt, kann nun die Sensoren einzeln auslesen bei vorgegebenen Abständen. Die Differenzpunkte zwischen gemessener Distanz und richtiger Distanz können anschließend erneut mittels Polynominterpolation zu einem Polynom berechnet werden, welches anschließend mit der obigen Funktion verknüpft wird.

 

Eine Lektion, die wir beim Experimentieren mit den hier beschriebenen Bauteilen gemacht haben ist, dass bereits wenige Zeilen eines Programms eine hohe Komplexität verbergen und analysiert werden müssen, bevor ein Programm verwendet werden kann.

 

Denn keiner kann ein (gutes) System entwickeln, wenn dieses nicht vollständig verstanden wurde!