(WS19-01)Wetterballon Design

Aus Verteilte Systeme - Wiki
Wechseln zu: Navigation, Suche

Hardware-Architektur

HW Architecture.png

Als zentrale Steuereinheit haben wir einen Pi-Zero beutzt, um Gewicht und Strom zu sparen. Ein selbst gebauter HAT, der uns zur Verfügung getellt wurde, stellte u.a. die notwendigen I²C-Anschlüsse bereit, um die Sensoren mit dem Pi zu verbinden. Auf dem HAT befanden sich außerdem Module für die Lokalisierung per GPS und das Versenden von Daten über LoRaWAN, sowie GPIO-Anschlüsse die wir für einen Buzzer verwendet haben, der die Bergung unterstützen sollte. Über ein USB-Hub wurden eine zusätzliche Kamera und ein Modul für die Datenübertragung per GSM angeschlossen. Der Zusammenschluss von Pi, HAT, USB-Hub und Speicherkarte wurde "master" getauft. Es befand sich außerdem ein GPS-Tracker im Payload, der unabhängig vom eigentlichen System war und bei einem Stromausfall die Bergung ermöglichen sollte.

Verbaute Sensoren und Geräte

Messung Anzahl Name
Temperatur innen 1 MCP9808
Temperatur außen 1 TMP 117 Breakout
Luftfeuchtigkeit außen 1 SHT 35
Luftfeuchtigkeit außen 1 SHT 31
Luftfeuchtigkeit innen 1 SHT 35
Luftdruck außen 1 MS8607
Luftdruck außen 1 MS5611 auf GY-86
Luftdruck innen 1 MS8607
9-Achsen (3-Achsen-Beschl., -Gyro., -Magnet.) 1 IMU 10DOF mit MPU-9250 & BME280
UV-A und UV-B 1 GYML8511 an Grove I2C-ADC
Kamera Bild 1 WaveShare IMX179 USB-Kamera
Kamera Video 1 Raspicam V2
GPS 1 u-blox NEO-M8U
Buzzer 1 Grove Buzzer

Aufbau des Payloads

Auf Basis der Hardware-Achritektur wurde ein Design des Payloadaufbaus in FreeCAD erzeugt.


Payload1.jpg Payload3.jpg Payload5.jpg Payload6.jpg


Aus dem Entwurf wurde noch eine 3D-PDF-Datei erzeugt, die sich nur mit dem Acrobat Reader öffnen und betrachten lässt.

Datei:Payload3D.pdf

In diesem Design fehlen noch die Sensoren sowie die Status-LEDs, da deren Position aufgrund der flexiblen Befestigungsmethoden zu dem Zeitpunkt noch unbekannt war.

Die Sensoren für den Innenraum wurden nur mit Klebeband an der Innenseite befestigt. Die äußeren Sensoren wurden mit Draht in der Styroporbox verankert. Für die LEDs wurde jeweils ein passendes Loch in die Box gebohrt durch das sie nach außen geführt und nicht separat befestigt wurden.

Der Designvorschlag wies im Laufe des Projekts einige Schwächen auf.

  1. Durch die passgenaue Form der Bodenplatte konnte das Gestell nicht mehr ohne das Entfernen der inneren Sensoren aus der Styroporbox entfernt werden. Dies war notwendig, um bspw. ein Wärmepad zu platzieren oder die USB-Kamera nachzujustieren.
  2. Die Halterung des Pis wurde zu nah an der Innenwand platziert, sodass das Flachbandkabel der Kamera sehr stark geknickt und oft aus dem Port gezogen wurde.


Anbindung

I2C

Fast alle Sensoren benutzen zur Kommunikation I2C, ein Bus zur synchronen Datenübertragung, der auf einem Master-Slave-Prinzip basiert. Die Slaves sind über Adressen ansprechbar und antworten auf Anfragen vom Master.

In unserem Fall stellt das master-Modul auch den Master des Busses dar, an dem die Sensoren als Slaves angeschlossen sind. Dieser sendet Signale aus, um Messungen anzustoßen oder Daten auszulesen. Diese Signale werden sensorabhängig als akute Befehle oder als Lese-/Schreibbefehle für Register auf dem Sensor interpretiert.

Mehr dazu: Wikipedia.

Seriell

Der GPS-Empfänger, der auf dem Hat verbaut ist, benutzt die serielle Schnittstelle zur Kommunikation. Das master-Modul kann also auf die Daten zugreifen, indem es aus der entsprechenden Datei im /dev/-Verzeichnis ließt. Sobald die Daten einmal ausgelesen werden, sind sie jedoch nicht mehr verfügbar.

Digital

Da der Buzzer nur an- oder ausgeschaltet werden kann, wird er direkt über die GPIOs gesteuert, indem deren Modus auf Output gesetzt wird und die Spannung abwechsend angelegt und abgenommen wird.

Probleme

Arbeitsbereiche

Bei den vorhandenen Sensoren sind wir auf kleinere Probleme gestoßen. Alle Sensoren sind bis zu einer Minimaltemperatur von -40°C funktionsfähig. Auf dem Flug werden diese allerdings deutlich unterschritten. Wir haben uns dennoch für den Einsatz dieser Sensoren entschieden, da auf dem Markt keine geeignerten Sensoren zu finden waren. Um Messungenauigkeiten oder gar Ausfälle zu kompensieren, werden wir die Sensoren für den Außeneinsatz mehrfach verbauen.

Adresskonflikte

Da manche Sensoren dieselben Adressen besitzen, haben wir einen Multiplexor verwendet, der vor Ansteuerung der Sensoren mit identischen Adressen auf den korrekten Ausgang geschaltet werden muss. Dazu hat unser Betreuer Kai Beckmann eine zum Grove-System passende Platine entworfen und fertigen lassen.

Multiplexor PCB.pdf

Multiplexor Schematic.pdf

Software-Architektur

Bei der Software-Architektur stellte sich die Frage, wie die unterschiedlichen Messzyklen der Sensoren am besten zu verwalten sind. Hier gab es zwei grundsätzliche Ideen.

Parallel

Eine parallele Implementierung mit Threads bietet den Vorteil, dass jeder Sensor autonom läuft, also auch in keiner Weise von den anderen Sensoren abhängig ist. Jeder Sensor muss nur eine (oder für jede Phase eine) run-Methode implementieren, die vom Master gestartet wird. Der Zugriff auf den Bus kann per Mutex synchronisiert werden. Beim Wechsel des Modus kann der Master dann jeden Thread beenden.

Die Nachteile dieser Methode sind größtenteils erst nach einer Probeimplementierung aufgefallen. Dazu zählen neben der ohnehin höheren Komplexität paralleler Programme, dass die Abgrenzung der Sensoren untereinander auch dazu führt, dass sie weitestgehend vom Master entkoppelt sind. Das ist vor allem im Bezug auf die Live-Datenauswertung ein Problem, da ein einzelner Sensor keinen simplen Weg hat, mit dem Master zu kommunizieren. Des weiteren führt die Anforderung der Unterbrechbarkeit in Verbindung mit der Synchronisierung per Mutex zum Problem von Race-Conditions, die, wenn nicht korrekt behandelt, das komplette Programm lahmlegen könnten.

Aus diesen Gründen wurde sich gegen die Implementierung mit Threads entschieden.

Monolithisch

Eine monolithische Implementierung mit einer einzigen Hauptfunktion, die alle Sensoren und andere Funktionalitäten, wie das Schreiben der Daten in Dateien, überwacht, sah zunächst nach einem uneleganteren Weg aus, da ihr Hauptvorteil im Vergleich zu Threads lediglich die einfachere Programmierung zu sein schien.

Neben der Lösung für die Problematik der Live-Datenverarbeitung bietet dieser zentralisierte Ansatz allerdings noch einige andere Vorteile. Die Komplexität des Codes wird verringert und die Implementierung der Wartungsschnittstelle wird vereinfacht. Außerdem eröffnet dieser Ansatz die Möglichkeit für weitere Dateninteraktionen. Des Weiteren müssen gleiche Abläufe nicht mehr für jeden Sensor redundant programmiert, sondern nur einmal im Master geschrieben werden.

Durch die engere Bindung der einzelnen Komponenten wurde eine größere, gut durchdachte und klar definierte Schnittstelle für die Sensoren nötig, die hilft, den Master algorithmisch übersichtlich zu halten, die aber auch auf den Eigenheiten der unterschiedlichen Sensoren gerecht wird.

Software-Architektur

Komponentendiagramm.jpg

Messdatenstruktur

Die Messdaten der Sensoren müssen über Programmteile hinweg kommuniziert werden können, z. B. zur internen Verarbeitung oder zur Serialisierung. Um eine solch vielseitige Nutzung zu ermöglichen, braucht es eine Datenstruktur, die nicht nur Zahlenwerte speichert sondern ihnen auch einen Kontext gibt. Daher ist für jeden Messwert der zugehörige Messdatentyp gespeichert.

Sensorschnittstelle

Alle Methoden dieser Schnittstelle sind mit Rückgabewerten definiert, die eindeutig Fehlercodes darstellen können. Das erlaubt von außen zu entscheiden, ob eine Fehlermeldung relevant ist und z. B. zum Programmabbruch führen soll, oder ob anderweitig bzw. überhaupt auf sie reagiert werden muss.

init()
Vor allem komplexere I²C-Sensoren bieten die Möglichkeit, sie über ihre Register zu konfigurieren. Für solche Sensoren existiert diese Funktion. Dies ist die einzige Funktion, die kein informelles Zeitlimit hat. Dauert die Konfiguration länger, stellt das kein Problem dar.
init_data(measurement*)
Um möglichst hohe Flexibilität zu wahren, wird der Speicher für die Messdaten nicht durch den Sensor selbst verwaltet. Dieser bietet Lediglich die Funktionalität, Speicherplatz in der für ihn korrekten Größe und Struktur anzulegen. Alle weiteren Methoden erwarten eine solche Struktur als Parameter.
free_data(measurement*)
Gibt eine allokierte Messdatenstruktur wieder frei.
measure(measurement*)
Für Sensoren, z. B. aufgrund eines Analog-Digital-Converters etwas Zeit brauchen, um nach dem Messen Daten bereit zu stellen, ist diese Funktion gemacht. Es wird eine Messdatenstruktur erwartet, um den Zeitpunkt, zu dem die tatsächliche Messung ausgeführt wurde, festzuhalten.
get_data(measurement*)
In dieser Funktion werden die Sensordaten abgefragt und in die Messdatenstruktur gespeichert. Je komplexer der Sensor, desto mehr passiert hier. Es ist trotzdem wichtig, dass diese Funktion nicht übermäßig viel Zeit braucht.

Master

Erster Entwurf

Der Aufbau des Master-Programms war zunächst so gestaltet, dass in einem ersten Schritt alle Bibliotheken geladen, Persistenzdaten gelesen, Sensoren und deren Messdatenstrukturen initialisiert und alle weiteren einmaligen Vorbereitungen durchgeführt werden. Nach den Vorbereitungen wurde die Hauptschleife betreten, die das periodische Aufrufen der Sensoren übernahm. Dazu wurde eine feste Laufzeit für einen Schleifendurchlauf festgelegt, welche mit passivem Warten nach der Sensorabarbeitung umgesetzt wurde. Das Timing jedes Sensors konnte somit in feste Schleifenintervalle umgerechnet werden. Falls die Schleife ihre für Testzwecke eingebaute Abbruchbedingung erreich hat, folgte der Ressourcenabbau in umgekehrter Reihenfolge der Initialisierung.

Nach der anfänglichen Entwicklung mit dieser Programmstruktur fiel auf, dass das Konzept für eine zufriedenstellende Implementierung der benötigten Funktionalitäten unzureichen war und ein flexibleres Scheduling-Verfahren akkurater und fehlertoleranter wäre. Zusätzlich sind weiter Aufgaben, wie das Ansprechen der Status-LEDs, angefallen, die sich nicht sauber über die Sensorschnittstelle hätten implementieren lassen können und fest in das master-Programm hätten eingearbeitet werden müssen. Es war also ein weiterentwickeltes Design nötig.

Vereinfachte Hauptschleife des ersten Entwurfs:

// Main loop.
for (unsigned long iteration = 0; QUIT != active_phase; ++iteration) {
	// Take starting time.
	clock_gettime(CLOCK_MONOTONIC, &iteration_start);

	// Check every sensor.
	for (size_t i = 0; SENSOR_AMT > i; ++i) {
		if (0 <= errors_g[i] && SENSORS[i].active_phase & active_phase) {
			// Block all signals.
			sigprocmask(SIG_BLOCK, &all, &normal);

			// Take measurements from all sensors active on this iteration.
			if (0 == iteration % frequency_to_iteration(SENSORS[i].frequency * multipliers_g[i])) {
				SENSORS[i].measure(&data[i]);
			}

			// Get data from all sensors which have available data on this iteration.
			if (frequency_to_iteration(SENSORS[i].measure_delay * multipliers_g[i]) == iteration % frequency_to_iteration(SENSORS[i].frequency * multipliers_g[i])) {
				SENSORS[i].get_data(&data[i]);
			}

			// Reset blocked signals.
			sigprocmask(SIG_SETMASK, &normal, NULL);
		}
	}
}

Zweiter Entwurf

Das Kernkonzept der Neuerung war das o.g. Scheduling-Verfahren und die Diversifizierung der auszuführenden Aufgaben. Das Problem der unterschiedlichen Aufgaben ließ sich relativ einfach lösen, indem nun nicht mehr nur Sensoren, sondern allgemeine Task verschiedener Typen verwaltet werden können. Sensoren sind damit zu Sensor-Tasks geworden.

Als Scheduling-Verfahren wurde eine eine Art Queue gewählt, in der jedoch zu jedem Zeitpunkt alle Tasks stehen. Die Auswahl der als nächstes auszuführenden Aufgabe erfolgt per Zeitstempel, der im Task gespeichert ist und vom Task selbst verwaltet wird. Dieser Zeitstempel zeigt den nächsten gewünschten Ausführungstermin an. Es wir immer der Task mit dem am nächsten in der Zukunft liegenden Zeitstempel ausgewählt. Sollte ein Task in Verzug geraten, wird sein Zeitstempel vom Scheduler in die Gegenwart korrigiert.

Die Rahmenstruktur mit Laden und Initialisieren der Komponenten zu Beginn und dem Aufräumen der Ressourcen am Ende blieb weitestgehend bestehen und wurde lediglich durch Auslagerung in eigene Teil-Funktionen modularisiert. Ergänzt wurde die Initialisierung der Sensor- und anderweitiger Tasks.

Vereinfachte Initialisierung eines Tasks:

// Master LED
tasks[s_con_amt].type = LED_MASTER;
tasks[s_con_amt].state = RUNNING;
tasks[s_con_amt].active_phases = START;
clock_gettime(CLOCK_MONOTONIC, &(tasks[s_con_amt].next_time));

Vereinfachte Hauptschleife des zweiten Entwurfs:

// Main loop
while (QUIT != active_phase && EXIT_SUCCESS == task_get_next(tasks, tasks_amt, &task)) {
	// Is task ready? If not, sleep remaining time
	clock_gettime(CLOCK_MONOTONIC, &cur_time);
	if (timespec_gt(task->next_time, cur_time)) {
		diff_time = timespec_sub(task->next_time, cur_time);
		clock_nanosleep(CLOCK_MONOTONIC, 0, &diff_time, &diff_time);
	}

	switch (task->type) {
		case SENSOR:
			sensor = (i2c_sensor *) (task->p);
			sigprocmask(SIG_BLOCK, &all, &normal);

			switch (sensor->state) {
				case INIT:
					// ...
				// ...
			}

			sigprocmask(SIG_SETMASK, &normal, NULL);
			break;
		case LED_MASTER:
			// ...
		// ...
	}
}