Prof. J. Walter - Informationstechnik, Mikrocomputertechnik, Digitale Medien Softwaredoku
Hochschule Karlsruhe Logo Informationstechnik
Drehteller Revisited
Wintersemester 2017/18
Nico (beni1015)
Tobias (hato1024)

Software Dokumentation

An dieser Stelle wird die Funktionsweise des Programms zur Steuerung des Drehtellers beschrieben.

Das Programm muss folgendes leisten:
1. Ansteuerung des Schrittmotors
2. Ansteuerung des LED-Rings
3. Abfragen der Touch-Sensoren und Interpretation der Ergebnisse
4. Übertragung von Daten auf einen Webclient
5. Handling von Eingaben der Clients auf der Weboberfläche

Im Folgenden gehen wir die Punkte der Reihe nach durch. Zum Schluss wird eine Betrachtung des Gesamtablaufs vorgenommen.

1. Ansteuerung des Schrittmotors
Der verwendete Schrittmotortreiber ist lediglich ein Verstärker, d.h. jede Motorwicklung muss einzeln beschaltet werden um ein Drehfeld zu erzeugen.
Für den Schrittmotor wurde eine eigene Klasse geschrieben und eine entsprechende globale Variable erstellt.

class SchrittmotorDaypower
{
public:
SchrittmotorDaypower();
void set_speed(int c_speed);
void set_modus(motor_mode c_modus);
void set_zustand(Zustand_Motor c_zustand);
Zustand_Motor get_zustand();
void run();

private:
const int motorPin1;
const int motorPin2;
const int motorPin3;
const int motorPin4;
Zustand_Motor zustand;
motor_mode modus;
int speed;
};

SchrittmotorDaypower myMotor;

Damit der Motor läuft, muss die Methode run() in einer Dauerschleife - am besten ohne jegliche Verzögerung - aufgerufen werden. Sie wertet die Attribute modus und speed aus und steuert die Ausgänge dann so an, dass der Motor mit einer gewissen Geschwindigkeit nach links oder rechts läuft bzw. anhält.

Das Attribut zustand erlaubt das Umschalten zwischen der Bedienung über die Weboberfläche und dem Handbetrieb.

Die Klasse verfügt über entsprechende getter- und setter-Methoden.

2. Ansteuerung des LED-Rings
Damit der LED-Ring die gewünschte Farbe korrekt (!) anzeigt, benötigt er ein Signal mit exaktem Timing.


Abb.: Ausschnitt Timing-Signal für LED-Ring

Da auf dem ESP32 ein Dual-Core-Prozessor verbaut ist, RTOS und verschiedene Interrups laufen etc., ist es außer bei sehr einfachen Programmen nicht möglich, dieses Timing über die CPU zu gewährleisten. Entsprechend hatten wir lange Probleme mit falschen Farbsignalen.


Abb.: Led-Ring mit falscher Beleuchtung

Das Problem ließ sich erst durch die Verwendung des RMT-Drivers (Remote Control) des ESP32 lösen. Dieser ist eigentlich zum Senden und Empfangen exakter Infrarot-Signale gedacht, lässt sich aber flexibel einsetzen. Das exakte Timing wird dadurch erreicht, dass die CPU zwar die Signaleigenschaften verarbeitet, das eigentliche Senden/Empfangen aber komplett unabhängig durchgeführt wird.

Bei der Verwendung von RMT konnten wir auf eine "halbfertige" Library zurückgreifen und damit eine Klasse für den LED-Ring schreiben. Im Programm wird dann wieder eine entsprechende globale Variable angelegt.

class LEDRing24_ESP32
{
public:
LEDRing24_ESP32();
void init();
void run();
void set_modus(LEDRing_mode c_modus);
void set_zustand(Zustand_LEDRing c_zustand);
Zustand_LEDRing get_zustand();
void set_red(uint8_t c_r);
void set_green(uint8_t c_g);
void set_blue(uint8_t c_b);
int get_refresh();

private:
int refresh;
int counter1;
bool flag1;
strand_t mystrand;
strand_t * mystrandPtr;
uint8_t r, g, b;
LEDRing_mode modus;
Zustand_LEDRing zustand;

//für Regenbogen-Modus (übernommen)
...
};

LEDRing24_ESP32 myLEDRing;

Der LED-Ring erhält durch jeden Aufruf der Methode run() ein neues Farbsignal für alle LEDs.  Sie wertet die Attribute modus und eventuell die Farben r, g, b aus und ruft am Ende die von der Libarary bereitgestellte Funktion

digitalLeds_updatePixels(mystrandPtr);

auf, die das Handling des RMT-Kanals übernimmt. Je nach Modus wird ein Farbsignal alle 5 bis 100ms gesendet.

3. Abfragen der Touch-Sensoren und Interpretation der Ergebnisse
Damit der Drehteller auch ohne Wifi bedienbar ist, sollten wir auch einen Handbetrieb implementieren. Statt der üblichen Taster, entschieden wir uns dabei für die Verwendung von zwei Touch-Sensoren am ESP32. Dazu werden im setup() zwei Interrupts und 10ms-Timer initialisiert.

touchAttachInterrupt(TOUCH8, touch_motor, threshold_on); //Interrupts für die Touch-Sensoren
touchAttachInterrupt(TOUCH9, touch_led, threshold_on);

timer = timerBegin(0, 80, true);             //Timer-Initialisierung
timerAttachInterrupt(timer, &onTimer, true);
timerAlarmWrite(timer, 10000, true);     //Timer alle 10ms
timerAlarmEnable(timer);

Zusätzlich werden vier globale Flag-Variablen definiert (hier nur zwei, weil nur der Motor betrachtet wird):

bool touch_motor_detected = false;     //Flag für den Motor-Touch-Sensor    
bool touch_motor_store = false;         //Flag zur Erkennung von Veränderung ("Flanke") am Motor-Touch-Sensor

Löst der Touch-Interrupt bei einer gewissen Schwelle aus, wird touch_motor_detected = true; gesetzt.

Ist das Flag true, schaut die Timer-ISR alle 10ms nach, ob aufgrund einer anderen Schwelle nicht wieder zurückgesetzt werden müsste.

if (touch_motor_detected) {
if (touchRead(TOUCH8) > threshold_off) {
touch_motor_detected = false;
touch_motor_store = false;
}
}

Zusammen mit dem zweiten Flag lässt sich so in einer Funktion handbetrieb() leicht eine Flankenerkennung für die Touch-Sensoren programmieren:

void handbetrieb()         //Idee: Erkennung einer "positiven Flanke" an den Touch Sensoren führt zum Übergang in den nächsten Zustand
{
//Touch-Sensor Motor
if (touch_motor_detected == true && touch_motor_store == false)
{
touch_motor_store = true;
int temp = (int)myMotor.get_zustand();
temp++;
if (temp == motor_letzter) { temp = 1; }
myMotor.set_zustand((Zustand_Motor)temp);
}
...
}

handbetrieb() muss regelmäßig aufgerufen werden.

4. Übertragung von Daten auf einen Webclient
Das Programm verwendet eine passende Library für einen Webserver und erstellt eine entsprechende globale Variable.

#include <WebServer.h>
WebServer server = WebServer(80);

Im setup() wird festgelegt, was passiert, wenn eine bestimmte Datei angefragt wird. Anschließend wird der Server gestartet.

server.on("/", HTTP_GET, handle_index);
server.on("/jquery.js", HTTP_GET, handle_jquery);
server.on("/jscolor.js", HTTP_GET, handle_jscolor);
server.begin();

Die Dateien werden in einem separaten Flash-Vorgang (siehe Quellcode) in Form eines Spiffs-Image auf den ESP32 übertragen. Für den Umgang mit Spiffs wird ebenfalls eine Library "SPIFFS.h" eingebunden. Das Senden der Daten geschieht in der separaten Funktion send_file(...) mit dem Befehl

server.streamFile(file, contentType);

Damit der Server läuft, wird regelmäßig

server.handleClient();

aufgerufen.

5. Handling von Eingaben der Clients auf der Weboberfläche
Das Programm verwendet eine passende Library für einen Websocket-Server und erstellt eine entsprechende globale Variable.

#include <WebSocketsServer.h>
WebSocketsServer webSocket = WebSocketsServer(81);

Im setup() wird festgelegt, was passiert, wenn ein Event eintritt und der Server wird gestartet.

webSocket.begin();
webSocket.onEvent(webSocketEvent);

In der Funktion webSocketEvent(...) wird das Event über Kontrollstrukturen (switch, if/else) ausgewertet und eine entsprechende Änderung für den Motor bzw. den LED-Ring (siehe unten) vorgenommen. Hier ein Beispiel:

else if (payload[0] == '+') //Event für Motorrechtslauf
{
myMotor.set_zustand(motor_wifigesteuert);
Serial.printf("SocketEvent: Rechts\n");
myMotor.set_modus(turnRight);
}

Damit der Server läuft, wird regelmäßig

webSocket.loop();

aufgerufen.

Betrachtung des Gesamtablaufs
Wie schon erwähnt, ist die Ansteuerung des LED-Rings äußerst zeitkritisch, allerdings konnten wir durch die Verwendung von RMT diesen Engpass komplett beseitigen. Darüber hinaus ist auch die Ansteuerung des Motors zeitkritisch, wenn man voraussetzt, dass alle Motorschritte immer genau gleich lang sein sollen, d.h. der Motor nicht stocken soll.

Vor diesem Hintergrund entschieden wir uns, den Standard-loop() zu deaktivieren und selber im setup() zwei Tasks mit Dauerschleifen zu starten, wobei der eine auf Core 0 und der andere auf Core 1 des Prozessors läuft.

xTaskCreatePinnedToCore(loop1, "loop_1", 4096, NULL, 1, NULL, 0);
xTaskCreatePinnedToCore(loop2, "loop_2", 4096, NULL, 1, NULL, 1);

Im loop1 wird das Webhandling, der Handbetrieb und die LED-Ring-Steuerung erledigt.

void loop1(void * pvParameters) {
...
while (true) //Dauerschleife
{
webSocket.loop();
server.handleClient();
handbetrieb();

if (millis() >= tickTime) //rufe die run-Methode frühestens alle [myLEDRing.get_refresh()] ms auf
{
tickTime = millis() + myLEDRing.get_refresh();
myLEDRing.run();
}

delay(5);
}
}

Im loop2 läuft ganz allein der Motor.

void loop2(void * pvParameters) {
while (true)
{
myMotor.run();
}
}

Diese Verteilung der Aufgaben führt dazu, dass sowohl der Motor als auch der LED-Ring optimal angesteuert werden und zudem im loop1 noch jede Menge Rechenzeit (beachte den delay) für zukünftige Aufgaben bereitstehen.

Zum Abschluss noch ein Timing-Diagramm im Regenbogen-Modus bei maximaler Geschwindigkeit:


Abb.: Timing Diagramm




  Mit Unterstützung von Prof. J. Walter Wintersemester 2017/18