Tutorial für Fortgeschrittene - Teil 2
In diesem Teil werden wir in Meta Genius ein neues EmStaMaG Modell erstellen und diesem einige Units und zwei Zustandsmaschinen hinzufügen und damit alle wesentliche Teile der erforderlichen Softwarefunktionen realisieren um am Ende daraus mit EmStaMaG C Code zu generieren.
Ziel von Teil 2
Wir wollen mit dem ATtiny 1607 Curiosity Nano Evaluation Kit eine Temperatur-Überwachung mit optischer Übertemperatur-Anzeige entwickeln.
Die Software-Implementierung dieser Funktionen wird dabei weitestgehend durch die Modellierung von Zustandsmaschinen erfolgen mit anschließender Codegenerierung. Als Tools kommen dabei Meta Genius und der Embedded Statemachine Codegenerator EmStaMaG zum Einsatz.
Im ersten Teil dieses Tutorials wurde bereits ein Atmel Studio Projekt aufgesetzt und für das ATtiny Curiosity Board konfiguriert. In diesem Teil werden wir nun in Meta Genius ein neues EmStaMaG Modell erstellen und diesem einige Units und zwei Zustandsmaschinen hinzufügen und damit alle wesentliche Teile der erforderlichen Softwarefunktionen realisieren. Am Ende werden wir daraus mit EmStaMaG Code in der Programmiersprache C generieren, den wir im nächsten Teil dieses Tutorials in das Atmel Studio Projekt integrieren und manuell erweitern werden.
Einführung
Um mit diesem Tutorial auch einen hilfreichen Leitfaden für größere Anwendungen zu geben, werden wir die Software bewusst gut strukturiert aufbauen und verschiedene funktionale Einheiten in Form von Units bilden (zur Erinnerung: Als Unit wird eine zusammengehörende Einheit aus einem *.h
und einem *.c
File bezeichnet).
Units müssen in der Modellierung übrigens nicht zwangsläufig eine Zustandsmaschine enthalten, sondern können z. B. auch nur Actions und/oder Conditions für andere Zustandsmaschinen bereitstellen. Dadurch wird bereits bei der Modellierung eine funktionale Gliederung ermöglicht, die später die manuelle Implementierung und auch das Interfacing zu nativem Code vereinfacht.
Schritt #2.1: Meta Genius Modell erstellen
Das Erstellen und Vorbereiten eines neuen EmStaMaG Modells wurde bereits im Anfänger Tutorial in den Schritten #1 bis #3 erläutert.
- Erstellen Sie in Meta Genius ein neues EmStaMaG Modell.
- Führen Sie in EmStaMaG die „Meta Genius Preparation“ aus.
Schritt #2.2: Generierungsverzeichnisse festlegen
Auch das Festlegen der Generierungsverzeichnisse wurde schon im Schritt #4 des Anfänger Tutorials prinzipiell behandelt. Hier müssen wir nun jedoch die Verzeichnisse berücksichtigen, die wir im Schritt #1.6 und #1.7 dieses Tutorials beim Aufsetzen des Atmel Start Projektes angegeben hatten. Zur Erinnerung:
Project Name: | AsPrjAdc |
Location: | C:\EmStaMaG\Tutorial-Adc |
Solution Name: | AsSolAdc |
User Code Verzeichnis: | Code |
Daraus macht Atmel Studio einen verschachtelten Verzeichnispfad:
C:\EmStaMaG\Tutorial-Adc\AsSolAdc\AsPrjAdc\Code |
In unserem Modell müssen wir unter GenerationOptions nun einige Verzeichnisse festlegen, die mit diesem Verzeichnispfad übereinstimmen müssen.
In den DiagramFolder werden von EmStaMaG die *.graphml
Zustandsdiagramme generiert, die von uns dann weiter mit yEd editiert werden können. Diese Diagramme gelten als Dokumentation zum generierten Source Code. Ein sinnvoller Ort hierfür wäre also ein Verzeichnis neben dem eigentlichen Source Code Projekt Verzeichnis:
DiagramFolder | C:\EmStaMaG\Tutorial-Adc\AsSolAdc\Diagrams |
In die Verzeichnisse IncCodeFolder und SrcCodeFolder werden von EmStaMaG die *.h
und *.c
Dateien für alle Units generiert.
Aus dem Anfänger Tutorial wissen wir noch, dass der Inhalt dieser Verzeichnisse beim Generieren immer vollständig gelöscht wird. Wir verfolgen aber das Ziel, Source Code zu generieren, der nachträglich noch manuell modifiziert werden kann. Das kann unter diesen Umständen natürlich nicht im gleichen Verzeichnis erfolgen.
Die unkomplizierteste Lösung zu diesem Dilemma ist die Verwendung von zwei verschiedenen Verzeichnissen, deren Inhalt manuell über ein geeignetes Diff-Tool synchronisiert werden muss (es gibt hierfür auch eine deutlich elegantere Lösung unter Verwendung von GIT, dies würde aber hier zu weit führen).
Wir verwenden hier den Weg über ein eigenes Generierungsverzeichnis:
IncCodeFolder | C:\EmStaMaG\Tutorial-Adc\AsSolAdc\GeneratedCode |
SrcCodeFolder | C:\EmStaMaG\Tutorial-Adc\AsSolAdc\GeneratedCode |
Nun fehlt noch der MgrArchivationFolder in den bei jeder Codegenerierung eine Kopie des aktuellen Modells gespeichert wird. Diese Funktionalität wird eigentlich erst bei gleichzeitiger Verwendung eines Versionsverwaltungssystems wie z. B. GIT interessant. Da beim Speichern aber immer auch Backup Kopien der vorangegangenen Datei-Version angelegt werden, entsteht auch ohne GIT eine Modellierungs-Historie, die manchen Fällen durchaus hilfreich sein kann.
MgrArchivationFolder | C:\EmStaMaG\Tutorial-Adc\AsSolAdc\RepositoryArchive |
Nun sind aber Sie an der Reihe:
- Legen Sie die Generierungsverzeichnisse in Modell fest.
Schritt #2.3: Units hinzufügen
Wir werden dem Modell in diesem Tutorial schon vorab alle Units hinzufügen um gleich einen gewissen Überblick über die angestrebte Software-Architektur zu gewinnen. In realen Entwicklungsprojekten können diese jedoch natürlich auch zu beliebigen späteren Zeitpunkten hinzugefügt werden.
Als erstes werden wir eine Unit benötigen für die Ansteuerung des ADCs. Durch die unbekannte Konvertierungszeit eines ADCs ist das eine klassische Anwendung für eine ereignisgesteuerte Zustandsmaschine. Diese Unit nennen wir AdcSm.
Wie schon in der Einführung erläutert, werden wir zur Temperatur-Überwachung mit Übertemperatur-Anzeige eine weitere Statemachine benötigen, um eine Hysterse-Funktion zu realisieren. Diese Unit nennen wir TempSm.
Um das Interfacing zu den API Funktionen unseres Atmel Start Framworks zu kapseln und zu abstrahieren, werden wir eine weitere Unit namens Hal
(Hardware Abstraction Layer) verwenden. Diese wird jedoch nur Actions aber keine Statemachine beinhalten.
In Embedded Software Projekten ist es früher oder später nahezu immer erforderlich mindestens einen Timer zu verwenden. Es empfiehlt sich hierfür schon von Anfang an eine ordentliche Lösung einzuführen. Und obwohl wir für unsere angestrebte Funktionialität nicht zwangläufig einen Timer benötigen würden, werden wir dennoch einen einführen. Das soll Ihnen eine Anleitung geben, wie man auch Timerbasiertes Verhalten mit EmStaMaG modellieren kann. Hierfür verwenden wir eine eigene Unit namens Timer.
Zu guter letzt werden wir noch eine weitere Unit vorsehen, in der unspezifische allgemeine Funktionen angesiedelt werden. Diese Unit nennen wir Common.
- Fügen Sie dem Modell folgende Units hinzu:
AdcSm, TempSm, Hal, Timer, Common.
Schritt #2.4: Zustandsmaschine für ADC modellieren
Die Ansteuerung eines ADCs ist in unserem Fall recht einfach. Im Schritt #1.3 aus Teil 1 dieses Tutorials wurde der ADC bereits vollständig konfiguriert, um damit den internen Temperatur Sensor des ATtiny auszulesen. Bei der Konfiguration wurde auch schon aktiviert, dass immer ein Interrupt ausgelöst wird, wenn eine Konvertierung abgeschlossen wurde.
Wir müssen in unseren Modellierung jetzt nur Vorkehrungen dafür treffen, dass der Interrupt auch mit einer geeigneten Callback Funktion verknüpft werden kann. Hierfür sehen wir eine Action namens init()
in der Unit Hal
vor.
Dann benötigen wir noch eine startConversion()
Action um eine A/D-Konvertierung anzustoßen. Und eine Action processResult()
um das Ergebnis zu verarbeiten und zu speichern. Diese sind auch in der Unit Hal
richtig aufgehoben.
Werfen wir mal einen Blick auf die erste Version einer Zustandsmaschine, die unsere Analog/Digital-Konvertierung realisieren könnte:
Bei der Initialisierung der Zustandsmaschine wird die initiale Transition durchlaufen. Dabei wird die Hal.init()
Action aufgerufen, in der später dann der Interrupt mit einer Callback-Funktion Hal.convCompletedCb()
verknüpft werden wird.
Ziel der initalen Transition ist der Conversion
Zustand, bei dessen Betreten immer die Hal.startConversion()
Action aufgerufen wird.
Die Konvertierung benötigt dann eine gewisse Zeit, in der die Zustandsmaschine im State Conversion
verbleibt.
Sobald die A/D-Konvertierung abgeschlossen ist, wird vom ATtiny ein Interrupt ausgelöst, für den wir schon eine entsprechende Behandlung vorgesehen haben. In der Interrupt-Callback-Funktion werden wir dann der Statemachine ein Trigger-Event namens ADC_COMPLETED
senden.
Dieses Event löst dann eine Transition aus, bei der der State Conversion
verlassen und das Ergebnis der Konvertierung in der Action Hal.processResult()
verarbeitet wird. Danach wird der State Conversion
wieder neu betreten und eine neue Konvertierung wird gestartet. Das Spiel beginnt von vorne.
- Fügen Sie der Unit
Hal
folgende Actions hinzu:init, startConversion, convCompletedCb, processResult.
(Implementieren werden wir diese später) - Fügen Sie der Unit
Common
folgende Action hinzu:handleFatalError.
(Implementieren werden wir diese später)
- Fügen Sie der Unit
AdcSm
eine neue Statemachine hinzu. - Referenzieren Sie für diese Statemachine als FatalErrorHandler die Action
handleFatalError
aus der UnitCommon.
- Fügen Sie der Statemachine einen neuen State
Conversion
hinzu. - Fügen Sie der Statemachine eine InitialTransition hinzu und referenzierien Sie dort als Target den State
Common.
- Fügen Sie der InitialTransition eine neue Action hinzu und referenzieren Sie hierfür die Action
init
aus der UnitHal.
- Fügen Sie dem State
Conversion
eine neue ActionOnEntering hinzu und referenzieren Sie hierfür die ActionstartConversion
aus der UnitHal.
- Fügen Sie der Unit
AdcSm
eine neue EventGroupEvent
hinzu. - Fügen Sie der EventGroup
Event
die folgenden Events hinzu:INVALID, ADC_COMPLETED.
- Referenzieren Sie das Event
INVALID
als InvalidEvent. - Fügen Sie dem State
Conversion
eine neue StateTransition hinzu und referenzieren Sie als Target den StateConversion.
- Referenzieren Sie bei dieser Transition als TriggerEvent den Event
ADC_COMPLETED.
Die Modellierung der Zustandsmaschine für die ADC Ansteuerung ist damit erst mal abgeschlossen, sie wird später noch erweitert werden. Um aber auch zwischendurch mal zu kontrollieren, ob Sie auch alle Modell-Elemente korrekt erstellt und miteinander verknüpft haben, sollten Sie jetzt eine erste Generierung starten (mehr dazu hier) und dann das generierte Diagramm mit yEd öffnen und das Ergebnis überprüfen. Mit der Auto-Layout Funktion „Flowchart“ sollte das Ergebnis schon mehr oder weniger wie obiges Zustandsdiagramm aussehen.
Hier lässt sich auch schon ein ganz entscheidender Vorteil dieses modellbasierten Entwicklungsansatzes erkennen. Mit dem generierten Diagramm kann man schon zu einem sehr frühen Zeitpunkt seine Modellierung überprüfen, kann darin gegebenenfalls auch schon sehr früh Fehler erkennen und diese schon korrigieren, bevor man die erste Zeile Source Code geschrieben hat.
Schritt #2.5: Timerbasierte Pause in ADC Messung einführen
In der bisher modellierten Zustandsmaschine wird eine A/D-Konvertierung gestartet und sobald diese abgeschlossen ist, wird das Ergebnis ausgewertet und eine neue Konvertierung gestartet. Aus rein funktionaler Sicht, wäre dies für unsere Zwecke ausreichend. Da es aber nicht notwendig ist, die Temperatur pro Sekunde mehrere hunter Mal zu messen, werden wir nun eine timerbasierte Pause einführen:
Timer werden in Embedded Software häufig benötigt. Diese werden üblicherweise mit Hilfe von Hardware-Timern realisiert, die am Ende einen Interrupt auslösen. Und bei der Verarbeitung von Interrupts bietet sich grundsätzlich immer eine Implementierung mit ereignisgesteuerten Zustandsmaschinen an.
Wir müssen also unsere Zustandsmaschine um einen Pause-Zustand erweitern. Beim Betreten des Zustands einen Timer starten und am Ausgang des Zustands darauf warten, dass der Timer abläuft.
- Fügen Sie der Unit
Timer
eine neue Action namesstartIdleWaiting
hinzu.
(Implementieren werden wir diese später) - Fügen Sie in der Unit
AdcSm
der EventGroup ein neues EventIDLE_WAITING_EXPIRED
hinzu.
- Fügen Sie der Statemachine einen neuen State
IdleWaiting
hinzu. - Fügen Sie dem State eine neue ActionOnEntering hinzu und referenzieren Sie hierfür die Action
startIdleWaiting
aus der UnitTimer.
- Fügen Sie dem State eine neue StateTransition hinzu und referenzieren Sie als Target den State
Conversion.
- Referenzieren Sie für diese Transition als TriggerEvent das Event
IDLE_WAITING_EXPIRED.
- Ändern Sie bei der Transition von State
Conversion
das Target aufIdleWaiting.
Hier wäre wieder eine gute Gelegenheit, die Arbeit im Zustandsdiagramm zu prüfen.
- Starten Sie eine neue Generierung.
- Öffnen Sie das generierte Diagramm
AdcSm.graphml
mit yEd. - Wenden Sie in yEd die Funktion Layout > Flowchart an.
Schritt #2.6: Event Queuing aktivieren
Die Verwendung von Hardware-Timern bringt einerseits viele Vorteile, um damit recht einfach zeitgesteuerte Funktionalitäten realisieren zu können. Timer-Interrupts werfen allerdings sofort auch die Frage nach der Reentrancy des generierten Zustandsmaschinen Codes auf.
Es würde zu weit führen, alle Details dieser Problematik hier zu erörtern. Aber so viel kann hier schon verraten werden:
Wird eine Zustandsmaschine ohne EventObjectQueue modelliert, dann kann es im Zusammenhang mit der Verwendung von Hardware-Interrupts zu Reentrancy-Problemen kommen. Diese werden jedoch vom generierten Code erkannt und es würde dann der FatalErrorHandler aufgerufen werden.
Um diesem Problemen entgegenzuwirken, kann man für jede Statemachine auch die Verwendung einer EventObjectQueue aktivieren.
Sollte sich eine Zustandsmaschine mit EventObjectQueue bei der Verarbeitung eines Events gerade in einem transienten Übergang befinden, während durch einen Hardware-Interrupt ein neuer Event erzeugt und eingespeist wird, würde dieser in der EventObjectQueue zwischengespeichert werden. Sobald die Zustandsmaschine mit der Verarbeitung des vorangegangenen Events fertig ist, würde dann erst der nächste Event aus der Queue verarbeitet werden.
Bei der Arbeit mit Zustandsmaschinen kann man übrigens grundsätzlich empfehlen eine EventObjectQueue zu verwenden, sobald Interrupts mit im Spiel sind.
- Fügen Sie der Statemachine eine EventObjectQueue hinzu und übernehmen Sie den Defaultwert für die Size.
Schritt #2.7: Neue Zustandsmaschine zur Temperaturüberwachung hinzufügen
Die Modellierung der Zustandsmaschine zur Temperaturmessung über den ADC hatten wir ja bereits abgeschlossen. Nur erzeugen wir eine neue Zustandsmaschine zur Temperatur-Überwachung. Und zur Übertemperatur-Anzeige verwenden wir die User LED des ATtiny 1607 Curiosity Nano Evaluation Kit.
Ist eine Zustandsmaschine für diese simple Funktionalität nicht zuviel des Guten? Nun, es besteht das Problem, dass die ADC-Messung natürlich einem gewissen Rauschen unterworfen ist. Dies würde im Temperatur-Übergangsbereich zwischen kalt und warm zu einem unschönen Flackern der LED führen. Mit einer Zustandsmaschine können wir auf eine sehr einfache und elegante Weise eine Hysterse-Funktion realisieren, mit der man dieses Flackern effektiv unterbinden kann.
Eine Hysterse-Funktion ist im Allgemeinen dadurch gekennzeichnet, dass sich die Schwellwerte der Übergangsbedingungen zwischen den Zuständen deutlich voneinander unterscheiden müssen – also um mehr als die mathematisch/logische Inversion.
In unserem speziellen Fall könnte man z. B. festlegen:
isHot() ... temp > 30
isCold() ... temp < 27
Nun aber genug der Theorie. Lassen Sie uns diese Zustandsmaschine schrittweise aufbauen.
- Fügen Sie zu der Unit
TempSm
die folgenden Conditions hinzu:isHot, isCold.
(Implementieren werden wir diese später) - Fügen Sie zu der Unit
Hal
die folgenden Actions hinzu:switchLedOn, switchLedOff.
(Implementieren werden wir diese später)
- Fügen Sie der Unit
TempSm
eine neue Statemachine hinzu. - Referenzieren Sie als FatalErrorHandler die Action
handleFatalError
aus der UnitCommon.
- Fügen Sie der Statemachine folgende States hinzu:
Cold, Hot.
- Fügen Sie der Statemachine eine Choice hinzu und benennen Sie diese
HotOrCold?
- Fügen Sie der Choice eine ChoiceTransition hinzu und referenzieren Sie hierfür als Target den State
Hot
und als Condition die ConditionisHot.
- Fügen Sie zu der Choice
HotOrCold?
eine Else Transition hinzu und referenzieren Sie hierfür als Target den StateCold.
- Führen Sie mit EmStaMaG eine Codegenerierung durch und überprüfen Sie das generierte Diagramm.
Achtung! Hierbei wird EmStaMaG Modellierungsprobleme melden, aber das Diagramm dennoch generieren. Es sollte an dieser Stelle so aussehen:
Der erste Teil der Zustandsmaschine ist damit schon mal fertig. Nun ergänzen wir noch die Zustandsübergänge zwischen den Zuständen Cold
und Hot
um folgende Zustandsmaschine zu erhalten:
- Fügen Sie der Statemachine eine neue EventGroup
Event
hinzu. - Fügen Sie der EventGroup folgende Events hinzu:
INVALID, TEMP_CHANGED.
- Referenzieren Sie für die EventGroup als InvalidEvent den Event
INVALID.
- Fügen Sie dem State
Cold
eine neue ActionOnEntering hinzu und referenzieren Sie hierfür die ActionswitchLedOff
aus der UnitHal.
- Fügen Sie dem State
Cold
eine neue StateTransition hinzu und referenzieren Sie hierfür als Target den StateHot.
- Referenzieren Sie für diese Transition als TriggerEvent den Event
TEMP_CHANGED.
- Referenzieren Sie für diese Transition als Condition die Condition
isHot.
- Fügen Sie dem State
Hot
eine neue ActionOnEntering hinzu und referenzieren Sie hierfür die ActionswitchLedOn
aus der UnitHal.
- Fügen Sie dem State
Hot
eine neue StateTransition hinzu und referenzieren Sie hierfür als Target den StateCold.
- Referenzieren Sie für diese Transition als TriggerEvent den Event
TEMP_CHANGED.
- Referenzieren Sie für diese Transition als Condition die Condition
isCold.
- Führen Sie mit EmStaMaG eine Codegenerierung durch und prüfen Sie das generierte Diagramm.
Dieses Mal sollten beim Generieren alle Warnung verschwinden und Sie sollten einen Graphen erhalten, der sich mit ein bisschen Hin- und Herschieben zu folgenden Zustandsdiagramm anordnen lässt:
Teil 2 dieses Tutorials ist damit abgeschlossen.
Im nächsten Teil werden wir den generierten Source Code in das Atmel Studio Projekt aus Teil 1 integrieren und mit etwas manueller Codierung vervollständigen.
Navigation
Adc Tutorial Teil 1 – Atmel Studio Projekt aufsetzen
Adc Tutorial Teil 2 – Zustandsmaschinen in Meta Genius modellieren und mit EmStaMaG generieren
AdcTutorial Teil 3 – Source Code integrieren und manuell vervollständigen