Prof. J. Walter - Informationstechnik, Mikrocomputertechnik, Digitale Medien Softwaredoku
Hochschule Karlsruhe Logo Informationstechnik
Drehteller - V3
Wintersemester 2018/2019
Brilliant Thomas
Tindo Samudra

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 über BLE-Schnittstelle

5*. Übertragung von Daten auf einen Webclient
6*. Handling von Eingaben der Clients auf der Weboberfläche

*Die Fokus unserer Arbeit liegt darin, den Drehteller über eine BLE-Schnittstelle anzusteuern. Für die WebSocket-Anwendung, bitte schauen Sie auf den Link von den vorherigen Gruppen.

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.

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.

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 über BLE-Schnittstelle

In diesem Abschnitt werde ich auf einige wichtige Dinge hinweisen, die diese BLE-Anwendung möglich machen. Am Anfang des Skizzenbuchs befinden sich die notwendigen Bibliotheken, damit der Code ausgeführt werden kann:

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>


Hier definieren wir auch den Service und die Charakteristischen UUID's für TX und RX (Zweiwegekommunikation). Auch wenn der ESP32 nur lesen muss, was der Client (Handy) gesendet hat, wird hier die bidirektionale Kommunikation für die zukünftige Anwendung eingesetzt.:

#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" // UART service UUID
#define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"


Als nächstes schauen wir uns die Callback-Funktion an, die den Status der Bluetooth-Verbindung behandelt. Man kann zusätzliche Befehle einfügen, die beim connected oder disconnected aufgerufen sein müssen:

class MyServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
        deviceConnected = true;
        Serial.println("BLE-Device connected.");

    myLEDRing.set_red(0);
    myLEDRing.set_green(100);
    myLEDRing.set_blue(0);
    myLEDRing.run();                 //wenn BLE-Verbindung, dann zeige kurz grün
    delay(1000);
    myLEDRing.set_red(0);
    myLEDRing.set_green(0);
    myLEDRing.set_blue(0);
    myLEDRing.run();

    myMotor.set_zustand(motor_BLEgesteuert);
    myLEDRing.set_zustand(LEDRing_BLEgesteuert);
    };

    void onDisconnect(BLEServer* pServer) {
        deviceConnected = false;
        Serial.println("BLE-Device disconnected.");
        Serial.println("Nur Handbetrieb moeglich, bitte die Touch-Sensoren verwenden.");
        myMotor.set_zustand(motor_Handbetrieb_aus);
        myLEDRing.set_zustand(LEDRing_Handbetrieb_aus);
        myLEDRing.set_red(0);
        myLEDRing.set_green(0);
        myLEDRing.set_blue(0);
        myLEDRing.run();
    }
};


Dabei wird das Flag "deviceConnected" auf true oder false gesetzt, wenn Sie sich mit dem ESP32 verbinden oder von ihm trennen. Man kann zusätzliche Befehle einfügen, die beim connected oder disconnected aufgerufen sein müssen. Ebenso gibt es eine weitere Callback-Funktion, die den Empfang von Werten, die vom Client gesendet werden, über den RX-Kanal verarbeitet.

class MyCallbacks: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
    rxValue = pCharacteristic->getValue();

    if (rxValue.length() > 0) {
        Serial.println("*********");
        Serial.print("Received Value: ");

        for (int i = 0; i < rxValue.length(); i++) {
        Serial.print(rxValue[i]);
    }  

    Serial.println();
    }



In der loop1()-Funktion prüfen wir, ob ein Gerät über BLE angeschlossen ist oder nicht (über die Callback-Funktion), und wenn ja, dann wird die Nachricht über Kontrollstrukturen (switch, if/else) ausgewertet und eine entsprechende Änderung für den Motor bzw. den LED-Ring (siehe unten) vorgenommen. Hier ein Beispiel:

if (rxValue[0] == '*') //Event f?r Motorstop
{
    myMotor.set_zustand(motor_BLEgesteuert);
    Serial.printf("SocketEvent: Stop\n");
    myMotor.set_modus(stop);
}


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 BLE-Schnittstelle, der Handbetrieb und die LED-Ring-Steuerung erledigt.

void loop1(void * pvParameters) {
...
while (true) //Dauerschleife
{

if (deviceConnected) {
    if (rxValue.length() > 0) {
    if (rxValue[0] == '#') //Event f?r RGB-Werte
    {
        myLEDRing.set_zustand(LEDRing_BLEgesteuert);

    .
    .
    .
}

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.





  Mit Unterstützung von Prof. J. Walter Wintersemester 2018/2019