Hardware
Zum Testen habe ich einen ESP32 und einen VL53L0X / MPU6050 über die Default I2C GPIO Pins verbunden (D21 und D22).
Da ich mich hier aber nicht so sehr mit der Hardware beschäftigen möchte werde ich das mal nicht weiter ausführen. Details dazu findet man überall im Internet. Einfach mal nach ESP32 I2C und VL53L0X / MPU6050 suchen.
Software
Als nächstes Habe ich die Adafruit_VL53L0X Bibliothek in der Arduino-IDE installiert (Bibliotheken verwalten -> etc.) und das Adafruit VL43L0X Beispiel geladen (Beispiele -> Adafruit_VL53L0X) und den Sketch auf dem ESP32 installiert.
#include "Adafruit_VL53L0X.h"
Adafruit_VL53L0X lox = Adafruit_VL53L0X();
void setup() {
Serial.begin(115200);
// wait until serial port opens for native USB devices
while (! Serial) {
delay(1);
}
Serial.println("Adafruit VL53L0X test");
if (!lox.begin()) {
Serial.println(F("Failed to boot VL53L0X"));
while(1);
}
// power
Serial.println(F("VL53L0X API Simple Ranging example\n\n"));
}
void loop() {
VL53L0X_RangingMeasurementData_t measure;
Serial.print("Reading a measurement... ");
lox.rangingTest(&measure, false); // pass in 'true' to get debug data printout!
if (measure.RangeStatus != 4) { // phase failures have incorrect data
Serial.print("Distance (mm): "); Serial.println(measure.RangeMilliMeter);
} else {
Serial.println(" out of range ");
}
delay(100);
}
PS: Der Sensor hat eigentlich eine Reichweite von 2 Metern, die er aber nur erreicht, wenn man die folgende Zeile noch ergänzt.
lox.configSensor(Adafruit_VL53L0X::VL53L0X_SENSE_LONG_RANGE);
Die Konsole hat dann direkt angefangen, die gemessenen Distanzen auszugeben.
Reading a measurement... Distance (mm): 529
Reading a measurement... Distance (mm): 517
Reading a measurement... Distance (mm): 497
Reading a measurement... Distance (mm): 506
Reading a measurement... Distance (mm): 508
Wozu soll ich mich mit denn dem I2C Bus beschäftigen?
Alles ganz einfach und man hat innerhalb weniger Minuten einen funktionierenden Distanz-Sensor für wenige Euro realisiert.
Normalerweise gibt es keinen Grund, sich mit den Details der I2C Schnittstelle zu beschäftigen. Als „Maker“ inkludiert man einfach Adafruit_VL53L0X.h und erstelle eine Instanz der Klasse Adafruit_VL53L0X. Schon kann man der Methode rangingTest die Distanz auslesen. Das ist so unglaublich einfach, aber das geht nur so lange gut, bis man vor einem Problem steht und nicht weiß, woran es liegt.
Dann kann es sehr hilfreich sein, sich die Schnittstelle mal genauer anzuschauen.
Das Basis Wissen über die I2C Schnittstelle kann unter https://de.wikipedia.org/wiki/I2C nachgelesen werden.
Das Werkzeug
Wie sieht die Kommunikation zwischen dem ESP32 und dem VL43L0X auf dem I2C Bus wirklich aus? Um die Schnittstelle detailliert analysieren zu können, ist ein Logic-Analyzer oder ein vergleichbares Tool unverzichtbar.
Der Logic Analyzer den ich verwende ist von AZ-Delivery. Er kostet um die 11€ und ist sein Geld auf jeden Fall wert. Es gibt vergleichbare (identische) Kisten bei Aliexpress, Ebay und Co. die sogar noch ein paar Euro günstiger sind aber ich hatte keine Lust so lange zu warten und habe meinen hier in Deutschland bestellt.
AZ-Delivery hat auch eine sehr gute Einführung auf der Webseite, die man sich auf jeden Fall mal ansehen sollte.
Mit Sigrok PulseView kann man die Signale dann am PC anzeigen und weiterverarbeiten. Dank der vielen verfügbaren Decoder muss man sich nicht mal die Arbeit machen, die Pegel High/Low selbst in Bits und Bytes zu zerlegen.
Die Liste der verfügbaren Decoder ist sehr lang und ich habe bisher noch immer den Dekoder gefunden, den ich gesucht habe. Liste der verfügbaren Sigrok PulseView Decoder. https://sigrok.org/wiki/Protocol_decoders
Der I2C Decoder (https://sigrok.org/wiki/Protocol_decoder:I2c) kann einfach an die beiden Leitungen (SDA, SCL) gehängt werden und schon geht es los.
Zur Analyse zeichnet man ein paar Sekunden der Kommunikation auf und sieht sich dann die dekodierten Signale an. Am besten nimmt man sich noch das Datasheet dazu (https://www.st.com/resource/en/datasheet/vl53l0x.pdf) und vergleicht die Interface-Spezifikation mit den aufgezeichneten Daten.
Behind the Scenes
In meinem konkreten Beispiel ist ein Block zum Auslesen der Distanz ca. 40ms lang und enthält eine Unmenge an Daten.
Vergrößert man das Bild etwas, kann man erkennen, dass es unterschiedliche Datenblöcke gibt. „Data read“ und „Data write“. Bei den „Data write“ Blöcken handelt es sich um die Daten, die der Master (Mikrokontroller) auf den Bus schreibt und bei den „Data read“ handelt es sich um die Daten vom Slave. (Sorry, aber die Schnittstelle ist von 1982 und damals hat sich noch niemand um rassistisch konnotierte Begriffe gekümmert)
Der erste Block in der Kommunikation zwischen VL53L0X und ESP32 hat folgenden Aubau:
Die vom Sensor empfangenen Daten werden in ein Register geschrieben. Jedes Datenbyte wird mit einem ACK quittiert. Die Daten werden dann in dem internen Register gespeichert, das durch den aktuellen Index adressiert ist.
Das ganze kann man jetzt Byte für Byte durchgehen und Block für Block untersuchen. Da das aber immer noch viel zu aufwändig ist, kann man die Daten einfach speichern und auf der Konsole damit weiter arbeiten.
Dazu kann man unter https://sigrok.org/wiki/Downloads#Binaries_and_distribution_packages das Programm sigrok-cli herunterladen. Das Programm steht unter der GNU GENERAL PUBLIC LICENSE und kann kostenlos verwendet werden.
Nach der Installation kann man eine Bash oder eine Powershell öffnen und das Kommando sigrok-cli aufrufen.
$ ./sigrok-cli.exe
Usage:
sigrok-cli.exe [OPTION...]
Help Options:
-h, --help Show help options
Application Options:
-V, --version Show version
-L, --list-supported List supported devices/modules/decoders
--list-supported-wiki List supported decoders (MediaWiki)
-l, --loglevel Set loglevel (5 is most verbose)
-d, --driver The driver to use
-c, --config Specify device configuration options
-i, --input-file Load input from file
-I, --input-format Input format
-o, --output-file Save output to file
-O, --output-format Output format
-T, --transform-module Transform module
-C, --channels Channels to use
-g, --channel-group Channel groups
-t, --triggers Trigger configuration
-w, --wait-trigger Wait for trigger
-P, --protocol-decoders Protocol decoders to run
-A, --protocol-decoder-annotations Protocol decoder annotation(s) to show
-M, --protocol-decoder-meta Protocol decoder meta output to show
-B, --protocol-decoder-binary Protocol decoder binary output to show
--protocol-decoder-samplenum Show sample numbers in decoder output
--protocol-decoder-jsontrace Output in Google Trace Event format (JSON)
--scan Scan for devices
-D, --dont-scan Don't auto-scan (use -d spec only)
--show Show device/format/decoder details
--time How long to sample (ms)
--samples Number of samples to acquire
--frames Number of frames to acquire
--continuous Sample continuously
--get Get device options only
--set Set device options only
--list-serial List available serial/HID/BT/BLE ports
Example use, typical options:
-d <driver> --scan
-d <driver> { --samples N | --frames N | --time T | --continuous }
{ -d <driver> | -I <format> | -O <format> | -P <decoder> } --show
See the manpage or the wiki for more details.
Note: --samples/--frames/--time/--continuous is required for acquisition.
Mit dem folgenden Befehl kann man nun die gespeicherten Daten (SCL=D1 SDA=D0) auf der Konsole anzeigen.
$ ./sigrok-cli -i data/vl53l0x-i2c-data.sr -P i2c:scl=D1:sda=D0:address_format=unshifted -A i2c=address-read:address-write:data-read:data-write
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 80
i2c-1: Data write: 01
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: FF
i2c-1: Data write: 01
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 00
i2c-1: Data write: 00
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 91
i2c-1: Data write: 3C
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 00
i2c-1: Data write: 01
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: FF
i2c-1: Data write: 00
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 80
i2c-1: Data write: 00
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 00
i2c-1: Data write: 01
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 00
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 00
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 44
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 14
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 31
i2c-1: Data read: 05
i2c-1: Data read: B6
i2c-1: Data read: 04
i2c-1: Data read: 00
i2c-1: Data read: B0
i2c-1: Data read: 00
i2c-1: Data read: 58
i2c-1: Data read: 00
i2c-1: Data read: 20
i2c-1: Data read: 1F
i2c-1: Data read: FE
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: FF
i2c-1: Data write: 01
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: B6
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 09
i2c-1: Data read: C9
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: FF
i2c-1: Data write: 00
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 0B
i2c-1: Data write: 01
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 0B
i2c-1: Data write: 00
i2c-1: Write
i2c-1: Address write: 52
i2c-1: Data write: 13
i2c-1: Read
i2c-1: Address read: 53
i2c-1: Data read: 40
Dieser ganze Datenblock wird zwischen dem ESP32 und dem VL53L0X innerhalb von 40ms ausgetauscht und damit der Befehl rangingTest der Adafruit_VL53L0X Bibliothek ausgeführt. Zum Glück funktioniert die Schnittstelle und ich habe keinen Grund, die Daten Byte für Byte mit der Spezifikation abgleichen. (Und im Vergleich zur Initialisierungs-Routine ist dieser Datenblock noch verhältnismäßig klein)
Doch nicht alle I2C Schnittstellen sind so komplex. Z.B. die Schnittstelle des MPU-6050 ist wesentlich einfacher zu verstehen.
Bei dem MPU-6050 handelt es sich um einen 6-Achsen Sensor (3-Achsen-Gyroskop, 3-Achsen-Beschleunigung) der z.B. auch im „Arduino Programmable Open-Source Flight Controller and Mini Drone using SMT32 and MPU6050 for Drone Enthusiasts“ (https://circuitdigest.com/news/arduino-programmable-open-source-flight-controller-and-mini-drone-using-smt32-and-mpu6050-drone-enthusiasts) verbaut ist.
Schaut man sich die Daten auf der Schnittstelle an, die zum Auslesen der 6 Werte nötig sind, stellt man schnell fest, da ist nicht viel Overhead drin.
Exakt 2565 µs sind nötig, um einmal die Werte der Temperatur, Gyroskop und Beschleunigungssensoren auszulesen.
Ein sehr schönes Video in dem das Prinzip der Sensoren erklärt wird ist das folgende (Etwas Off-Topic aber egal)
Auf der Konsole sieht dass dann wie folgt aus:
$ ./sigrok-cli -i data/mpu-6050.sr -P i2c:scl=D1:sda=D0:address_format=shifted -A i2c=address-read:address-write:data-read:data-write
Schritt 1:
Adresse 0x38 des MPU6050 (ID 0x68) senden. 38 ist hier „base address for sensor data reads“. Es wird mit einer 14 Byte großen Sequenz geantwortet.
i2c-1: Write
i2c-1: Address write: 68
i2c-1: Data write: 3B
Schritt 2:
Schritt 2: Einlesen der 14 Bytes der Sensordaten. Die Daten sind wie folgt sortiert:
ACCEL_XOUT_H
ACCEL_XOUT_L
ACCEL_YOUT_H
ACCEL_YOUT_L
ACCEL_ZOUT_H
ACCEL_ZOUT_L
TEMP_OUT_H R
TEMP_OUT_L R
GYRO_XOUT_H
GYRO_XOUT_L
GYRO_YOUT_H
GYRO_YOUT_L
GYRO_ZOUT_H
GYRO_ZOUT_L
Quelle: https://invensense.tdk.com/wp-content/uploads/2015/02/MPU-6000-Register-Map1.pdf
i2c-1: Read
i2c-1: Address read: 68
i2c-1: Data read: 08
i2c-1: Data read: 53
i2c-1: Data read: 00
i2c-1: Data read: 05
i2c-1: Data read: FE
i2c-1: Data read: 7F
i2c-1: Data read: ED
i2c-1: Data read: 07
i2c-1: Data read: 00
i2c-1: Data read: 0C
i2c-1: Data read: FF
i2c-1: Data read: EC
i2c-1: Data read: FF
i2c-1: Data read: F1
Schritt 3:
0x1C senden. Adresse 1C bedeutet: „Accelerometer specific configration register“
i2c-1: Write
i2c-1: Address write: 68
i2c-1: Data write: 1C
Schritt 4:
Daten der Adresse 0x1c werden zurückgegeben. 0x18 bedeutet hierbei:
i2c-1: Read
i2c-1: Address read: 68
i2c-1: Data read: 18
Das Gleiche wird nun noch einmal für das Gyrometer durchgeführt. Adresse 0x18 = „Gyro specfic configuration register“
i2c-1: Write
i2c-1: Address write: 68
i2c-1: Data write: 1B
i2c-1: Read
i2c-1: Address read: 68
i2c-1: Data read: 18
Nach ca. 2,5 Millisekunden sind alle Daten ausgelesen. Sowohl die Sensoren-Daten als auch die Skalierungsfaktoren (+/- 2g, +/- 4g, +/- 8g, +/- 16g und +/- 250, +/- 500, +/- 1000 oder 2000°/s) der Konfiguration. Im Sourcecode der Adafruit_MPU6050 Bibliothek sieht dass dann wie folgt aus:
void Adafruit_MPU6050::_read(void) {
// get raw readings
Adafruit_BusIO_Register data_reg =
Adafruit_BusIO_Register(i2c_dev, MPU6050_ACCEL_OUT, 14);
uint8_t buffer[14];
data_reg.read(buffer, 14);
rawAccX = buffer[0] << 8 | buffer[1];
rawAccY = buffer[2] << 8 | buffer[3];
rawAccZ = buffer[4] << 8 | buffer[5];
rawTemp = buffer[6] << 8 | buffer[7];
rawGyroX = buffer[8] << 8 | buffer[9];
rawGyroY = buffer[10] << 8 | buffer[11];
rawGyroZ = buffer[12] << 8 | buffer[13];
temperature = (rawTemp / 340.0) + 36.53;
mpu6050_accel_range_t accel_range = getAccelerometerRange();
float accel_scale = 1;
if (accel_range == MPU6050_RANGE_16_G)
accel_scale = 2048;
if (accel_range == MPU6050_RANGE_8_G)
accel_scale = 4096;
if (accel_range == MPU6050_RANGE_4_G)
accel_scale = 8192;
if (accel_range == MPU6050_RANGE_2_G)
accel_scale = 16384;
// setup range dependant scaling
accX = ((float)rawAccX) / accel_scale;
accY = ((float)rawAccY) / accel_scale;
accZ = ((float)rawAccZ) / accel_scale;
mpu6050_gyro_range_t gyro_range = getGyroRange();
float gyro_scale = 1;
if (gyro_range == MPU6050_RANGE_250_DEG)
gyro_scale = 131;
if (gyro_range == MPU6050_RANGE_500_DEG)
gyro_scale = 65.5;
if (gyro_range == MPU6050_RANGE_1000_DEG)
gyro_scale = 32.8;
if (gyro_range == MPU6050_RANGE_2000_DEG)
gyro_scale = 16.4;
gyroX = ((float)rawGyroX) / gyro_scale;
gyroY = ((float)rawGyroY) / gyro_scale;
gyroZ = ((float)rawGyroZ) / gyro_scale;
}
Alles in allem ist es schon erstaunlich, wie viel Logik in einem 2 € Sensor steckt und wie viel Programmcode (z.B. https://github.com/adafruit/Adafruit_VL53L0X/ & https://github.com/adafruit/Adafruit_MPU6050) hier kostenlos zur Verfügung gestellt wird. Ohne dieses Engagement würde die Maker-Szene heute wohl ganz anders aussehen – Vielen Dank dafür
Fazit
Die I2C (Inter-Integrated Circuit) Schnittstelle ist heute in vielen digitalen ICs ein Standard. Es gibt zwar einige Limitierungen in Bezug auf Sicherheit, Geschwindigkeit und Stabilität in störanfälligen Umgebungen, aber er ist aus der IoT Welt eigentlich nicht mehr wegzudenken und hat sich seit 40 Jahren in Millionen (vermutlich sogar Milliarden) ICs bewährt da er unglaublich flexibel einsetzbar ist und bis zu 128 Sensoren (oder andere ICs) mit nur 2 Leitungen (SCL und SDA) miteinander verbinden kann. Darüber hinaus kann man die Signale sehr einfach auslesen und analysieren. Ich bin ein I2C Bus Fanboy