Informatikprojekt: Drahtlose Sensornetzwerke mit ZigBee
Einleitung
Vor wenigen Jahren, als Student der Informatik an der Frankfurt University of Applied Sciences habe ich mich für das Thema für das Informatikprojekt von Professor Krauße entschieden.
Es war passend zu meinen Erwartungen, als es hatte:
- echte Hardware, die ich mit meinem Code erleben kann
- und Sensoren für eine möglichst praktische Anwendung.
Das Teamprojekt kam in zwei Varianten und diese im Post beschrieben kam mit zwei BNO055 smart Sensoren, die Gyroskop, Magnetometer- und Beschleuinigungssensoren kombinieren. Aus den drei Sensoren wir haben nur den Gyroskop für Auslesen der Eulerwinkel und Quaternionen benutzt. Dazu haben wir die Entwicklungsplatte der ZigBee Geräte über VPython Funktionsbibliothek in der Echtzeit visualisiert. Diese beiden Anforderungen waren verpflichtend, aber um unsere Kollegen von anderen Teams zu beeindrucken, war meine persönliche Berührung für die Visualisierung die Entwicklung eines Labyrinthspiels mit einer Kugel, die versucht zu entkommen (gesteuert durch den Sensor), jedes Mal mit einem zufällig generierten Labyrinth.
Der BNO055 Smart-Sensor bietet Gyroskop-Messwerte sowohl in Euler-Winkeln als auch in Quaternionen und wir waren verpflichtet, beides zu lesen.
Zuerst lassen Sie uns das Rezept für dieses Projekt überfliegen.
Projektkomponenten
Software
- BitCloud Embedded SW Stack von Atmel Version 3.3.0 (C/C++)
- Python Version 3.8.5
- VPython - Visual Python graphische Funktionsbibliothek Version 7
Hardware
- 3 x ZigBee ATMEGA256RFR2 Radiomodulen
- 1 x PAN Koordinator
- 2 x ZigBee Endgeräte mit jeweils einem BNO055 9-axis Sensor
- 2 x Akkupacks mit jeweils 2 AA Batterien (für Endgeräte)
- 2 x Bosch BNO055 9-Achsen Smart Sensoren
- 1 x UART Brücke
- 1 x USB mini zu USB 2.0 Kabel für die Verbindung mit dem PC
Verbindung und Sensorein-/ausgabe
Das Ziel war, ein Personal Area Network (PAN) zwischen ZigBee-Geräten einzurichten und Sensordaten von zwei Endgeräten über einen Coordinator (Master-Knoten) an den PC zu übertragen.
Das Hauptkommunikationsprotokoll zwischen den ZigBee-Endgeräten und den 9-Achsen-Sensoren auf den Platinen ist das Inter-Integrated-Circuit (I2C) oder der Two Wire Interface (TWI).
Für dieses Projekt hatten wir insgesamt nur drei ZigBee-Geräte, daher konnte der Adressbereich ziemlich klein sein. Da jeder einen 8-Bit-Prozessor an Bord hatte, war die einzige Option der 7-Bit-Adressbereich für die Geräte, die innerhalb von I2C kommunizieren, es gibt jedoch auch eine 10-Bit-Adressbereich-Option.
Das bedeutet theoretisch, dass es bis zu 127 (27-1) Geräte geben kann, die über eine I2C-Verbindung kommunizieren (mindestens ein Master-Knoten muss vorhanden sein).
Die Clock/Data-Rate für diese Verbindung wurde auf 125 KB/s gewählt, da sie die Geschwindigkeit, mit der die Sensoren gelesen werden können, ausreichend ist.
Auch zu beachten ist, dass wir alle unseren Code (außer Konstanten) in eine riesige app.c
-Datei eingefügt haben, die von Atmel Studio generiert wurde. Am Ende war die Datei fast 1000 Zeilen Code.app.c
file generated by Atmel Studio.
Die Definition der I2C-Verbindung wird mithilfe eines HAL_I2cDescriptor_t
-Descriptors zusammengefasst:
HAL I2C Descriptor
static HAL_I2cDescriptor_t i2cdescriptor={ .tty = TWI_CHANNEL_0, .clockRate = I2C_CLOCK_RATE_125, .id = BNO055_I2C_ADDRESS, }
Auf der Seite 102 der BNO055-Sensordokumentation, die speziell für I2C vorgesehen ist, werden einige Dinge zum Lesen/Schreiben der Register erwähnt:
- Um ein Sensorregister zu schreiben, benötigen Sie 2 Byte:
- Wählen Sie durch Festlegen des ersten Bytes auf die Adresse des Zielregisters aus, in das Sie schreiben möchten und
- Das zweite Byte für die Daten, die Sie schreiben.
- Um ein Sensorregister zu lesen, benötigen Sie 1 Byte:
- Wählen Sie ein Register auf dem Sensor aus, das Sie lesen möchten, indem Sie die Adresse des Registers senden, das Sie lesen.
Nachdem der Descriptor für die Zwei-Draht-Schnittstelle definiert wurde, können die BitCloud-API-Funktionen zur Kommunikation über die Schnittstelle verwendet werden:
HAL_OpenI2cPacket(HAL_I2cDescriptor_t *descriptor)
,HAL_WriteI2cPacket(HAL_I2cDescriptor_t *descriptor)
,HAL_ReadI2cPacket(HAL_I2cDescriptor_t *descriptor)
undHAL_CloseI2cPacket(HAL_I2cDescriptor_t *descriptor)
.
Dies sind wichtige Bausteine für die benutzerdefinierten Lese-/Schreibfunktionen für die Sensoreregister. Schauen wir uns die benutzerdefinierte Schreibfunktion an, die mithilfe der oben genannten BitCloud-API-Funktionen geschrieben wurde:
Write sensor registers
int writeBno055(uint8_t numOfBytes, uint8_t regAddr, uint8_t val){ if(-1 == HAL_OpenI2cPacket(&i2cdescriptor)){ printLineToUSART(ERROR_I2C_OPEN); printLineToUSART("Error in writeBno055!"); appstate=APP_NOTHING; SYS_PostTask(APL_TASK_ID); return -1; }else{ // set isPacketClosed to false since it's open isPacketClosed=false; } if(numOfBytes==1) //register setzen { writeOneByteData[0]=regAddr; i2cdescriptor.f = writeDone; i2cdescriptor.data = writeOneByteData; i2cdescriptor.length = 1; }else if(numOfBytes==2){ // Wert reinschreiben writeTwoBytesData[0]= regAddr; writeTwoBytesData[1]= val; i2cdescriptor.f = writeDone; i2cdescriptor.data = writeTwoBytesData; i2cdescriptor.length = 2; }else{ // invalid numOfBytes argument return -2; } if(-1 == HAL_WriteI2cPacket(&i2cdescriptor)){ printLineToUSART(ERROR_I2C_WRITE); printLineToUSART(" When writing to a register using writeBno055 function!"); appstate = APP_NOTHING; SYS_PostTask(APL_TASK_ID); return -1; } // the packet is closed in the callback function writeDone return 0; }
Lassen Sie uns die Funktion aufbrechen. Zuerst versuchen wir, die Ressource I2C durch Aufruf der API-Funktion HAL_OpenI2cPacket(&i2cdescriptor)
zu öffnen. Damit stellen wir sicher, dass die Ressource zum Lesen/Schreiben verfügbar ist und wenn nicht, wird als Fehlercode -1 zurückgegeben.
Im Allgemeinen kann der erste Parameter der Schreibfunktion uint8_t numOfBytes entweder 1 oder 2 sein:
- 1, wenn nur das Register für späteres Lesen gesetzt wird und
- 2, wenn tatsächlich in das Register geschrieben wird.
- Jede andere Zahl gibt als Fehlercode -2 zurück.
Je nach Anzahl der Bytes ist entweder i2cdescriptor.length
1 oder 2, i2cdescriptor.data
enthält entweder 1 oder 2 Bytes am Ende. Das war's, jetzt kann die HAL_WriteI2cPacket(&i2cdescriptor)
verwendet werden, da der Descriptor eingerichtet ist und wir in eines der BNO055-Register schreiben können.
Sie werden feststellen, dass wir das I2C-Paket hier nicht schließen. Dies geschieht, indem die Callback-Funktion i2cdescriptor.f
auf writeDone(bool result)
gesetzt wird und die Callback-Funktion es schließt, da zu diesem Zeitpunkt, wenn die Callback ausgeführt wird, das Lesen/Schreiben bereits abgeschlossen ist oder es fehlgeschlagen ist und es keine Timingprobleme gibt.
Das ist der Callback für die Schreibfunktion:
Write callback function
static void writeDone(bool result){ if(result == false){ printLineToUSART(ERROR_I2C_WRITE); appstate = APP_NOTHING; }else{ // succesful write, now close the packet if(-1 == HAL_CloseI2cPacket(&i2cdescriptor)){ printLineToUSART(ERROR_I2C_CLOSE); appstate = APP_NOTHING; }else{ // set isPacketClosed to true isPacketClosed=true; } } SYS_PostTask(APL_TASK_ID); }
Der Callback für die Lesefunktion ist genau dasselbe, aber mit einer anderen USART-Druckzeile im ersten if
-Satz.
Jetzt schauen wir uns die benutzerdefinierte Funktion readBno055
an:
Read sensor registers
int readBno055(uint8_t numOfBytes, uint8_t* readData){ if(-1 == HAL_OpenI2cPacket(&i2cdescriptor)){ printLineToUSART(ERROR_I2C_OPEN); printLineToUSART("Error in readBno055!"); return -1; }else{ // set isPacketClosed to false since it's open isPacketClosed=false; } i2cdescriptor.f = readDone; i2cdescriptor.data = readData; i2cdescriptor.length = numOfBytes; if(-1 == HAL_ReadI2cPacket(&i2cdescriptor)){ printLineToUSART(ERROR_I2C_READ); printLineToUSART("Error in readBno055!"); appstate = APP_NOTHING; SYS_PostTask(APL_TASK_ID); return -1; } return 0; }
Ähnlich wie bei der Schreibfunktion beginnt die Lesefunktion damit, die I2C-Ressource zu öffnen und die Callback-Funktion festzulegen, aber was anders ist, ist der Datenpuffer (i2cdescriptor.data
), der mit Daten aus dem zuvor ausgewählten Register gefüllt wird, anstatt als Daten für das Schreiben in ein Register verwendet zu werden. Schließlich wird die Anzahl der gelesenen Bytes angegeben, im Gegensatz zur Schreibfunktion kann dies jede positive Zahl sein.
Programmzustände
Nun, da wir den Weg für die Kommunikation mit Sensoren vorgezeichnet haben, besprechen wir die Zustände des Programms, durch die das Programm läuft.
APP_NOTHING
- Dieser Zustand wird verwendet, wenn das Programm aufgrund eines Fehlers gestoppt werden muss.APP_INIT
- In diesem Initialisierungszustand werden die booleschen Variablen eingerichtet, die entscheiden, ob die Euler-Winkel oder Quaternionen gelesen werden sollen.APP_STARTJOIN_NETWORK
- Ein unbedingt erforderlicher ZigBee-Zustand. Hier startet der Coordinator (Master-Knoten) ein PAN, während die beiden anderen ZigBee-Endgeräte diesem Netzwerk in diesem Zustand beitreten.APP_INIT_ENDPOINT
- Ein unbedingt erforderlicher ZigBee-Zustand, in dem der Endpunkt initialisiert wird.APP_INIT_TRANSMITDATA
- Ein unbedingt erforderlicher ZigBee-Zustand, in dem die Datenstruktur der Nachricht im PAN definiert wird. In diesem Fall sieht die Struktur so aus:
Payload structure
typedef struct _AppMessagePayloadEnddevice_t{ uint8_t moduleName; int32_t eulerX, eulerY, eulerZ; // all values are 1000 times int32_t quatW, quatX, quatY, quatZ; // all values are 1000 times uint32_t voltage; } PACK AppMessagePayload_t;
Wie die Kommentare sagen, werden diese Werte mit 1000 multipliziert und dann durch die korrekte Ausgabe über die UART-Brücke-Schnittstelle am PC geteilt.
APP_SET/READ_ID_REG
sind zwei Zustände zum Setzen und Lesen des ID-Registers am Sensor, um die I2C-Verbindung zu testen.APP_SET_PAGEID
setzt die Seiten-ID am Sensor auf 0. Dadurch können Sensordaten gelesen werden.APP_SET_OP_MODE_NDOF
- hier wird der Betriebsmodus des Sensors auf NDOF gesetzt, der einer der Fusionsmodi ist, bei dem alle drei Sensorsignale von Beschleunigungssensor, Magnetometer und Gyroskop mit einer absoluten Orientierung der Fusionsdatenausgabe vorhanden sind. Dieser Zustand ist erforderlich, da der Standardbetriebsmodus nach dem Einschalten des Sensors derCONFIGMODE
ist und Sensoren in diesem Modus noch nicht angeschaltet sind.APP_SET/READ_CALIBRATION_REG
sind Zustände, in denen die Kalibrierung der Sensormodule aktiviert wird. Die Sensordaten für unsere Anwendung waren ausreichend kalibriert, aber mit diesem Modus sind die Daten sogar noch korrekter.APP_READ_EULER/QUATERNION
- in diesen Zuständen werden die Sensordaten gelesen, mit 1000 multipliziert und später als int32_t-Variablen gespeichert.APP_READ_BATTERY
wie der Name schon sagt, liest die aktuelle Spannung des an die ZigBee-Endgeräte angeschlossenen Akkupacks aus. Der Coordinator ist über die UART-Brücke mit dem PC verbunden und versorgt sich selbst auf diese Weise.APP_TRANSMIT
- mit Sensor Daten von entwederAPP_READ_EULER
oderAPP_READ_QUATERNION
initialisierte Pakete werden über PAN an den Coordinator gesendet.
Lesen/Schreiben von Sensoren in der Praxis
Wie bereits erwähnt, muss ein Register, das Sensordaten gespeichert hat, zunächst durch ein I2C-Schreiben ausgewählt werden, um es zu lesen. Aus diesem Grund gibt es die APP_SET...
-Zustände.
Unten ist der Code zum Setzen eines Quaternionsstartregisters zum Lesen. Das ist nichts, was wir noch nicht erklärt haben. Im Falle eines Fehlers geht das Programm direkt in den Zustand APP_NOTHING
über.
APP_SET_QUATERNION_REG State
case APP_SET_QUATERNION_REG: if(debugMode){ printLineToUSART("------------------------------"); printLineToUSART("STATE: APP_SET_QUAT_REG"); } if(-1 == writeBno055(1, QUATERNION_DATA_W_LSB_ADDR, 0x00)){ printLineToUSART(ERROR_SEL_REG_QUA); appstate=APP_NOTHING; SYS_PostTask(APL_TASK_ID); } appstate=APP_READ_QUATERNION; SYS_PostTask(APL_TASK_ID); break;
Jetzt kann das eigentliche Lesen der in Quaternionsregistern gespeicherten Daten beginnen. In diesem Fall werden 8 Byte Daten gelesen (8 Register) und dann in 4 int16_t
Variablen w, x, y, z
gespeichert. Die Daten müssen auf den korrekten Bereich skaliert und mit 1000 multipliziert werden, um die 3 Gleitkommastellen zu behalten.
APP_READ_QUATERNION State
case APP_READ_QUATERNION: if(debugMode){ printLineToUSART("--------------------------------"); printLineToUSART("STATE: APP_READ_QUATERNION"); } readBno055(8, readDataQ); { int16_t w, x, y, z; w = x = y = z = 0; w = (((uint16_t)readDataQ[1]) << 8 ) | ((uint16_t)readDataQ[0]); x = (((uint16_t)readDataQ[3]) << 8 ) | ((uint16_t)readDataQ[2]); y = (((uint16_t)readDataQ[5]) << 8 ) | ((uint16_t)readDataQ[4]); z = (((uint16_t)readDataQ[7]) << 8 ) | ((uint16_t)readDataQ[6]); // convert the values to appropriate range const double scale = (1.0/(1<<14))*DEC_POINT_PRECISION; // (1/(1<<14))*1000 int32_t wThousand = (w*scale); int32_t xThousand = (x*scale); int32_t yThousand = (y*scale); int32_t zThousand = (z*scale); voltage = (float)adcData * 0.00625*VOLTAGE_RATIO; // 0.00625 = 1.6 (Referenzspannung) / 256 (2 hoch 8 bit Auflösung) uint32_t intVoltage=(uint16_t)(voltage*DEC_POINT_PRECISION); // store these values in network payload message networkMessagePayload=(AppMessagePayload_t){ .moduleName=0x02, .quatW=wThousand, .quatX=xThousand, .quatY=yThousand, .quatZ=zThousand, .voltage=intVoltage }; float floatW=(float)wThousand; float floatX=(float)xThousand; float floatY=(float)yThousand; float floatZ=(float)zThousand; floatW=floatW/DEC_POINT_PRECISION; floatX=floatX/DEC_POINT_PRECISION; floatY=floatY/DEC_POINT_PRECISION; floatZ=floatZ/DEC_POINT_PRECISION; printDoubleToUSART(floatW); printTextToUSART(", "); printDoubleToUSART(floatX); printTextToUSART(", "); printDoubleToUSART(floatY); printTextToUSART(", "); printDoubleToUSART(floatZ); printTextToUSART(", "); printDoubleToUSART(voltage); printLineToUSART(" "); } appstate = APP_READ_BATTERY; SYS_PostTask(APL_TASK_ID); break;
Jetzt können Sie in Sensor-Registern lesen/schreiben und auch die Datenausgabe des BNO055 Smart Sensors auswerten.
Das war's für den Backend-Teil, jetzt sehen wir uns den Frontend-Teil des Programms an.
VPython UI
Da die Pflichtanforderung war, die Bewegungen von zwei ZigBee-Endgeräten aufgrund der Sensormessungen auf dem Bildschirm zu visualisieren, sehen wir uns an, wie das umgesetzt wurde.
Dieser Teil wurde von meinem Teamkollegen gemacht und ich habe nicht an der Entwicklung dieser Schnittstelle teilgenommen. So sieht die Schnittstelle aus:
Beide Szenen verarbeiten Live-Feeds von den Sensoren und die Boards werden gedreht, wenn sie im echten Leben gedreht werden.
Das gab mir die Idee, daraus ein Spiel zu machen und es in der finalen Demo für das Projekt vorzustellen. Zu der Zeit des Projekts war ich von Algorithmen für die Generierung von Labyrinthen fasziniert, die lösbare aber zufällige Labyrinthe erzeugen. Die Punkte waren verbunden und ich wollte Labyrinthe erstellen, die sich basierend auf meiner Eingabe von den Sensoren drehen und neigen.
Als Nächstes sehen wir uns meinen Beitrag zur Visualisierung der Daten in Form eines Spiels an.
Das Marble Maze Game
Das Spielkonzept ist sehr einfach; man startet an einer Startposition in einer Ecke des Labyrinthes und das Ziel ist es, den Ball zu rollen zur Endposition, die in rot auf der diagonalen Ecke des Labyrinthes markiert ist. Das Spiel hatte aufgrund der Zeitbeschränkung Wochen vor dem Ende des Projekts kein Menüsystem, keinen Spielabschlussbildschirm und kein ernsthaftes Belohnungssystem.
Das Board war in drei verschiedenen Größen erhältlich: klein (3x3 Zellen), mittel (6x6) und groß (9x9) und das oben abgebildete ist das mittelgroße Labyrinth, das für die Demo verwendet wurde.
Um ein Labyrinth zu generieren, wird der zufällige rekursive Tiefensuche -Algorithmus verwendet.
Schlussgedanken
Dank des Professors und seinem anständigen Assistenten konnten wir im Verlauf des Projekts viel über drahtlose Sensornetze und Mikrocontrollerprogrammierung lernen. Es hat mir auch geholfen, meine Teamkollegen zu motivieren und zu großen Leistungen zu führen.