1.3 Zustandsdiagramm

Benötigte Hilfsmittel: TGI-Formelsammlung, Software Umlet, Anleitung zu Umlet:

Tipp: Um Diagramme automatisch in der passenden Größe dar zu stellen: style=autoresize

Fußgängerampel mit UML-Zustandsdiagramm entwickeln

Die [Fußgängerampel] wird mit UML-Zustandsdiagramm erneut entwickelt. Schauen Sie sich den alten Lösungs-Quellcode noch einmal an.

Aus der Digitaltechnik kennen Sie ja bereits Zustandsdiagramme, sie sind ein praktisches Werkzeug um Automaten systematisch zu entwicklen. In der UML steht uns dieses praktische Werkzeug auch für Software zur Verfügung. Sogar wesentlich mächtiger als bei Hardware. Z.B. kann man darstellen, dass eine Aktion genau einmal ausgeführt wird, wenn in den Zustand gesprungen wurde usw. In der [Formelsammlung] stehen Beispiele ab Seite 4.

Die Möglichkeiten ein Verhalten dar zu stellen sind zunächst überwältigend. Keine Panik, nach kurzer Eingewöhnung wird es ganz einfach und dieses Werkzeug erleichtert die Entwicklung von Software erheblich.

Zustandsdiagramme Darstellungen
Zustandsdiagramme Darstellungen

UML-Zustandsdiagramm zur Fußgängerampel

Hier eine Möglichkeit die Fußgängerampel mit einem UML-Zustandsdiagramm dar zu stellen.

  • setup() nach Start ausführen
  • Die grüne Ampelphase[0] ausgeben
  • Auf Tastendruck von PA1 warten
  • Bei Tastendruck erst 6 Sec Fußgänger nerven
  • Die gelbe Ampelphase[1] ausgeben
  • usw.
Fußgängerampel Zustandsdiagramm
Fußgängerampel Zustandsdiagramm

Ist diese Lösung falsch?

❓ Überprüfen Sie den Ablauf.

Lösung

Der Ablauf stimmt, aber das Diagramm ist nicht so übersichtlich.

Fußgängerampel Zustandsdiagramm falsch?
Fußgängerampel Zustandsdiagramm falsch?

Implementieren des Zustandsdiagrammes

In der Formelsammlung auf Seite 7 findet sich ein Beispiel für die Umsetzung in Code, hier eine Lösungsidee für Arduino:

Implementierung des Zustandsdiagramms
Implementierung des Zustandsdiagramms

Damit in Quellcode nach dem case ein aussagekräftiger Name und keine Zahl stehen kann brauchen wir dafür einen neuen Datentyp, die Aufzählung engl. enumeration.

Aufzählungsdatentyp enum

Die Zustände müssen codiert werden. Der Quellcode ist besser lesbar wenn statt Zahlen 0,1… für zustand aussagekräftige Namen vergeben werden. Mit dem Aufzählungsdatentyp enum ist dies möglich. Die Symbole {GRUEN,GELB,ROT,ROT_GELB} sollten dabei aber Groß geschrieben werden. Hier die Umsetzung mit Arduino:

typedef enum {GRUEN,GELB,ROT,ROT_GELB} zustandstyp;  // Aufzaehlungstyp
zustandstyp zustand = GRUEN; // Startzustand
void setup() {
  pinMode(PC0,OUTPUT);
  pinMode(PC2,OUTPUT);
  pinMode(PC4,OUTPUT);
  pinMode(PC6,OUTPUT);
  pinMode(PC7,OUTPUT);
  pinMode(PA1,INPUT_PULLDOWN);
}
typedef enum {GRUEN,GELB,ROT,ROT_GELB} zustandstyp;  // Aufzaehlungstyp
zustandstyp zustand = GRUEN; // Startzustand

unsigned char ampelphasen[4]={0b01010000,0b01000100,0b10000001,0b01000101};

void loop() {
  switch(zustand){
    case GRUEN:
      if(digitalRead(PA1)){  // Waechterbedingung
        zustand = GELB;      // Folgezustand
        delay(6000);         // exit Aktion
        GPIOC->ODR=ampelphasen[1]; // GELB entry Aktion
      }
    break;
    case GELB:
      zustand = ROT;         // Folgezustand
      delay(2000);           // exit Aktion
      GPIOC->ODR=ampelphasen[2]; // ROT entry Aktion
    break;
    case ROT:
      zustand = ROT_GELB;     // Folgezustand
      delay(6000);           // exit Aktion
      GPIOC->ODR=ampelphasen[3]; // ROTGELB entry Aktion
    break;
    case ROT_GELB:
      zustand = GRUEN;       // Folgezustand
      delay(3000);           // exit Aktion
      GPIOC->ODR=ampelphasen[0]; // GRUEN entry Aktion
    break;
  }
}

⁈ In der Umsetzung ist ein kleiner Fehler: Werden die LED nach dem Start eingeschaltet? Wie kann das repariert werden?

Lösung

Nach dem Start wird bei GRUEN kein entry ausgeführt. Ein extra GRUEN_ENTRY Zustand kann das reparieren:

void setup() {
  pinMode(PC0,OUTPUT);
  pinMode(PC2,OUTPUT);
  pinMode(PC4,OUTPUT);
  pinMode(PC6,OUTPUT);
  pinMode(PC7,OUTPUT);
  pinMode(PA1,INPUT_PULLDOWN);
}
typedef enum {GRUEN_ENTRY,GRUEN,GELB,ROT,ROT_GELB} zustandstyp; // Aufzaehlungstyp
zustandstyp zustand = GRUEN_ENTRY; // Initalisierung

unsigned char ampelphasen[4]={0b01010000,0b01000100,0b10000001,0b01000101};

void loop() {
  switch(zustand){
    case GRUEN_ENTRY:
      GPIOC->ODR=ampelphasen[0]; // GRUEN entry Aktion
      zustand = GRUEN;
    break;
    case GRUEN:
      if(digitalRead(PA1)){  // Waechterbedingung
        zustand = GELB;      // Folgezustand
        delay(6000);         // exit Aktion
        GPIOC->ODR=ampelphasen[1]; // GELB entry Aktion
      }
    break;
    case GELB:
      zustand = ROT;         // Folgezustand
      delay(2000);           // exit Aktion
      GPIOC->ODR=ampelphasen[2]; // ROT entry Aktion
    break;
    case ROT:
      zustand = ROT_GELB;     // Folgezustand
      delay(6000);           // exit Aktion
      GPIOC->ODR=ampelphasen[3]; // ROTGELB entry Aktion
    break;
    case ROT_GELB:
      zustand = GRUEN_ENTRY; // Folgezustand
      delay(3000);           // exit Aktion
    break;
  }
}

Zustandsdiagramm für Lüftersteuerung

PA1PA6PA10
An <-> Auslangsamerschneller

Ein Lüfter soll mit den Tasten PA1 (An/Aus) PA6 (Lüfter langsamer) und PA10 (Lüfter schneller) gesteuert werden. Es gibt 4 Lüfterstufen 0..3. Bei Stufe 0 leuchtet nur LC0 an PC0 als Betriebsanzeige.
Die letzte Lüfterstufe soll beim Ausschalten gemerkt werden und beim Anschalten wieder eingestellt sein. Bei ausgeschaltetem Lüfter leuchtet keine LED und die Taster PA6 und PA10 sind ohne Funktion.
Bei der Verwendung von digitalRead(..) müsste Prellen berücksichtigt und auf das Loslassen der Tasten gewartet werden. Deshalb steht die Funktion buttonCheck() zur prellfreien Erkennung von Tastenbetätigungen zur Verfügung.

StufePC3PC2PC1PC0
Aus0000
Stufe 00001
Stufe 10011
Stufe 20111
Stufe 31111
Anzeige der Lüfterstufen
Zustandsdiagramm
Zustandsdiagramm

Codevorgabe für Zustandsdiagramm der Lüftersteuerung

void setup() {
  pinMode(PC0,OUTPUT);
  pinMode(PC1,OUTPUT);
  pinMode(PC2,OUTPUT);
  pinMode(PC3,OUTPUT);
  pinMode(PA1,INPUT_PULLDOWN);
  pinMode(PA6,INPUT_PULLDOWN);
  pinMode(PA10,INPUT_PULLDOWN);
}

#define BUTTONREADER GPIOA->IDR & 0b10001000010 // nur die relevanten Tastenbits
int buttonOld = 0;              // alter Tasten-Zustand 
int buttonEnter,buttonExit;     // gedrueckte und losgelassene Tasten 

void buttonCheck(){             // Tastaturabfrage mit Flankendedektion  
  int buttonTest,tmp; 
  buttonEnter = 0, buttonExit = 0; 
  buttonTest = BUTTONREADER;       // Einlesen
  if (buttonOld != buttonTest){    // hat sich was getan 
    delay(5);                      // 5ms Prellen abwarten 
    tmp = BUTTONREADER;            // noch mal Einlesen
    if (tmp == buttonTest){        // ist es stabil? 
      buttonEnter = (~buttonOld) & buttonTest; // steigende Flanke !alt und neu 
      buttonExit = buttonOld & (~buttonTest);  // fallende Flanke alt und !neu 
      buttonOld = buttonTest; 
    } 
  } 
}

typedef enum {AUS,AN} zustandstyp; // Aufzaehlung der Zustaende
zustandstyp zustand = AUS;  // Startzustand
int stufe = 0;

void ausgebenStufe(){     // Luefterstufen als Leuchtband ausgeben
  int ausgabe=1;
  for(int i=0; i<stufe; i++){
    ausgabe = (ausgabe<<1)+1;
  }
  GPIOC->ODR = ausgabe;
}
void ausschaltenLED(){
  GPIOC->ODR = 0;
}
void minusLuefter(){
  if(stufe>0) stufe--;
}
void plusLuefter(){
  if(stufe<3) stufe++;
}
void loop() {
  buttonCheck();               // in jedem Zustand do Aktion
  switch (zustand){.           // je nach Zustand
    case AUS:                  // Zustand Aus
      
      break;
    case AN:                   // Zustand An
      
      break;
  }
}

🖥 Vervollständigen Sie den Code.

Lösung
void setup() {
  pinMode(PC0,OUTPUT);
  pinMode(PC1,OUTPUT);
  pinMode(PC2,OUTPUT);
  pinMode(PC3,OUTPUT);
  pinMode(PA1,INPUT_PULLDOWN);
  pinMode(PA6,INPUT_PULLDOWN);
  pinMode(PA10,INPUT_PULLDOWN);
}

#define BUTTONREADER GPIOA->IDR & 0b10001000010 // nur die relevanten Tastenbits
int buttonOld = 0;              // alter Tasten-Zustand 
int buttonEnter,buttonExit;     // gedrueckte und losgelassene Tasten 

void buttonCheck(){             // Tastaturabfrage mit Flankendedektion  
  int buttonTest,tmp; 
  buttonEnter = 0, buttonExit = 0; 
  buttonTest = BUTTONREADER;       // Einlesen
  if (buttonOld != buttonTest){    // hat sich was getan 
    delay(5);                      // 5ms Prellen abwarten 
    tmp = BUTTONREADER;            // noch mal Einlesen
    if (tmp == buttonTest){        // ist es stabil? 
      buttonEnter = (~buttonOld) & buttonTest; // steigende Flanke !alt und neu 
      buttonExit = buttonOld & (~buttonTest);  // fallende Flanke alt und !neu 
      buttonOld = buttonTest; 
    } 
  } 
}

typedef enum {AUS,AN} zustandstyp; // Aufzaehlung der Zustaende
zustandstyp zustand = AUS;  // Startzustand
int stufe = 0;

void ausgebenStufe(){     // Luefterstufen als Leuchtband ausgeben
  int ausgabe=1;
  for(int i=0; i<stufe; i++){
    ausgabe = (ausgabe<<1)+1;
  }
  GPIOC->ODR = ausgabe;
}
void ausschaltenLED(){
  GPIOC->ODR = 0;
}
void minusLuefter(){
  if(stufe>0) stufe--;
}
void plusLuefter(){
  if(stufe<3) stufe++;
}
void loop() {
  buttonCheck();
  switch (zustand){.           // je nach Zustand
    case AUS:                  // Zustand Aus
      if(buttonEnter&(1<<1)){  // PA1 gedrueckt?
        ausgebenStufe();
        zustand = AN;
      }
      break;
    case AN:                   // Zustand An
      if(buttonEnter&(1<<1)){  // PA1 gedrueckt?
        ausschaltenLED();
        zustand = AUS;
      }
      if(buttonEnter&(1<<6)){  // PA6 gedrueckt?
        minusLuefter();
        ausgebenStufe();
      }
      if(buttonEnter&(1<<10)){ // PA10 gedrueckt?
        plusLuefter();
        ausgebenStufe();
      }
      break;
  }
}

Lösung verschönern mit #define

Es gibt noch “hässliche” Ausdrücke wie buttonEnter&(1<<1) im Projekt. Vor dem Kompilieren kann der C-Prä-Prozessor Texte ersetzen. Z.B T_ANAUS durch buttonEnter&(1<<1). D.h. wir könnten das Zustandsdiagramm und das Programm damit lesbarer machen! Hier mein Vorschlag: Das T_ steht für Taste. Die Ersetzungen sollten Groß geschrieben werden.

#define T_ANAUS buttonEnter&(1<<1)
#define T_LANGSAMER buttonEnter&(1<<6)
#define T_SCHNELLER buttonEnter&(1<<10)

🖌 🖥 Verschönern Sie das Zustandsdiagramm und den Quellcode damit!

Lösung Zustandsdiagramm
Zustandsdiagramm Lüftersteuerung
Lösung Quellcode
void setup() {
  pinMode(PC0,OUTPUT);
  pinMode(PC1,OUTPUT);
  pinMode(PC2,OUTPUT);
  pinMode(PC3,OUTPUT);
  pinMode(PA1,INPUT_PULLDOWN);
  pinMode(PA6,INPUT_PULLDOWN);
  pinMode(PA10,INPUT_PULLDOWN);
}

#define BUTTONREADER GPIOA->IDR & 0b10001000010 // nur die relevanten Tastenbits
int buttonOld = 0;              // alter Tasten-Zustand 
int buttonEnter,buttonExit;     // gedrueckte und losgelassene Tasten 

void buttonCheck(){             // Tastaturabfrage mit Flankendedektion  
  int buttonTest,tmp; 
  buttonEnter = 0, buttonExit = 0; 
  buttonTest = BUTTONREADER;       // Einlesen
  if (buttonOld != buttonTest){    // hat sich was getan 
    delay(5);                      // 5ms Prellen abwarten 
    tmp = BUTTONREADER;            // noch mal Einlesen
    if (tmp == buttonTest){        // ist es stabil? 
      buttonEnter = (~buttonOld) & buttonTest; // steigende Flanke !alt und neu 
      buttonExit = buttonOld & (~buttonTest);  // fallende Flanke alt und !neu 
      buttonOld = buttonTest; 
    } 
  } 
}

#define T_ANAUS buttonEnter&(1<<1)
#define T_LANGSAMER buttonEnter&(1<<6)
#define T_SCHNELLER buttonEnter&(1<<10)

typedef enum {AUS,AN} zustandstyp; // Aufzaehlung der Zustaende
zustandstyp zustand = AUS;  // Startzustand
int stufe = 0;

void ausgebenStufe(){     // Luefterstufen ausgeben
  int ausgabe=1;
  for(int i=0; i<stufe; i++){
    ausgabe = (ausgabe<<1)+1;
  }
  GPIOC->ODR = ausgabe;
}
void ausschaltenLED(){
  GPIOC->ODR = 0;
}
void minusLuefter(){
  if(stufe>0) stufe--;
}
void plusLuefter(){
  if(stufe<3) stufe++;
}
void loop() {
  buttonCheck();
  switch (zustand){
    case AUS:          // Zustand Aus
      if(T_ANAUS){     // AnAus gedruekt?
        ausgebenStufe();
        zustand = AN;
      }
      break;
    case AN:          // Zustand An
      if(T_ANAUS){    // Taste AnAus gedruekt?
        ausschaltenLED();
        zustand = AUS;
      }
      if(T_LANGSAMER){  // Taste langsamer gedrueckt?
        minusLuefter();
        ausgebenStufe();
      }
      if(T_SCHNELLER){ // Taste schneller gedrueckt?
        plusLuefter();
        ausgebenStufe();
      }
      break;
  }
}

Lösung verschönern mit isEnter()

Wem die #define -Lösung nicht gefällt könnte sich ja eine Hilfsfunktion isEnter(n:int):boolean wobei n die Pin-Nummer des Tasters ist ausdenken:

boolean isEnter(int n){
  return buttonEnter&(1<<n);
}

Einschalten auch mit Tasten PA6 und PA10

Der Lüfter soll nun auch mit den Tasten PA6 und PA10 eingeschaltet werden können.
🖌 🖥 Modifizieren Sie das Zustandsdiagramm und das Programm entsprechend, verwenden Sie diesmal isEnter() zur Tastenabfrage.

Lösung Zustandsdiagramm
Zustandsdiagramm Lüftersteuerung

Autoblinker

PA1PA6PA10
Nach linksNach rechtsWarnblinker
Tasterbelegung

Wenn PA1 gedrückt wird soll sich ein Leuchtband von LC0 bis LC7 aufbauen mit Zeitabstand LEUCHTDAUER. Bei Betätigen von PA6 wird das Leuchtband von LC7 bis LC0 aufgebaut. Solange PA10 gedrückt ist sollen alle LED blinken mit Zeitabstand BLINKDAUER.
Diesmal wird die Arduino-Funktion digitalRead(..) verwendet. Hier Code und Zustandsdiagramm zum Start:

void setup() {
  pinMode(PC0, OUTPUT);  // ohne diese Zeile klappts nicht
  GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
  pinMode(PA1,INPUT_PULLDOWN);
  pinMode(PA6,INPUT_PULLDOWN);
  pinMode(PA10,INPUT_PULLDOWN);
}
enum {AUS,LINKS,RECHTS,WARNBLINKEN} zustand=AUS; // Aufzaehlungstyp
#define LEUCHTDAUER 70
#define BLINKDAUER 400
Zustandsdiagramm Autoblinker

Wenn alle Tasten gedrückt werden sollte der Warnblinker Vorrang haben.
🖥 Entwickeln Sie aus dem Zustandsdiagramm den restlichen Code. Bin gespannt ob das Experiment klappt 🤠.

Lösung
void setup() {
  pinMode(PC0, OUTPUT);  // ohne diese Zeile klappts nicht
  GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
  pinMode(PA1,INPUT_PULLDOWN);
  pinMode(PA6,INPUT_PULLDOWN);
  pinMode(PA10,INPUT_PULLDOWN);
}

enum {AUS,LINKS,RECHTS,WARNBLINKEN} zustand=AUS; // Aufzaehlungstyp
#define LEUCHTDAUER 70
#define BLINKDAUER 400
void loop() {
  switch (zustand){
    case AUS:                   // Zustand Aus
      delay(BLINKDAUER);
      if(digitalRead(PA1)){     // Links blinken?
        zustand = LINKS;
      }
      if(digitalRead(PA6)){     // Rechts blinken?
        zustand = RECHTS;
      }
      if(digitalRead(PA10)){    // Warnblinken?
        zustand = WARNBLINKEN;
      }
      break;
    case LINKS:                 // Zustand Links
      if(GPIOC->ODR>=0xff){     // Alle leuchten?
        zustand = AUS;
        GPIOC->ODR = 0;
      }
      else{
        GPIOC->ODR = (GPIOC->ODR<<1)+1;
        delay(LEUCHTDAUER);
      }
      break;
    case RECHTS:               // Zustand rechts
      if(GPIOC->ODR>=0xff){    // Alle leuchten?
        zustand = AUS;
        GPIOC->ODR = 0;
      }
      else{
        GPIOC->ODR = (GPIOC->ODR>>1)+0x80;
        delay(LEUCHTDAUER);
      }
      break;
    case WARNBLINKEN:          // Zustand Warnblinken
      GPIOC->ODR = 0xff;
      delay(BLINKDAUER);
      GPIOC->ODR = 0;
      zustand = AUS;
      break;
  }
}

❓ Warum ist ein Entprellen diesmal nicht notwendig?

UpDown-Zähler mit 7Segmentanzeige

Mit den Tasten PA6 (Up) und PA1 (Down) soll eine Zahl auf der 7-Segmentanzeige ausgegeben werden.

Zustandsdiagramm
Zustandsdiagramm
Sieben Segment Anzeige
Sieben Segment Anzeige

Die Anoden der 7Segmentanzeige sind mit PC0..PC7 verbunden und die gemeinsame Kathode wird mit einem Transistor bei PC11 = 1 auf GND geschaltet. D.h. die Segmente leuchten bei log. 1 an PC0..PC7 wenn PC11 = 1 ist. Und die Ausgabe einer Ziffer einfach zu gestalten benötigen wir ein Umrechnungsarray int bcd_7seg[]={0x3f,…} für die Ziffern 0..9.

✍️ Vervollständigen Sie den Code für int bcd_7seg[]={0x3f,…}; // Umrechnung. Tipp: Erstellen Sie eine Tabelle mit PC7..PC0 und übersetzen die Binärmuster nach Hex.

Beim Eintritt in den Chillen-Zustand muss wegen enter/ausgeben() die Operation ausgeben() aus den Transitionen Start -> setup(), von StufeUp und StufeDown aufgerufen werden. Kriegt man das eleganter hin, ohne ausgebenEiner() drei mal aufrufen zu müssen? Wie wäre es mit einem Zustand CHILLEN_ENTER, der dann in den Zustand CHILLEN wechselt?

Codevorgabe

void setup(){   // Einmalige Ausführung => Initialisierungen...
    pinMode(PC0, OUTPUT);  // ohne diese Zeile klappts nicht
    GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
    pinMode(PC11,OUTPUT); // Einer
    pinMode(PC12,OUTPUT); // Zehner
    pinMode(PA1,INPUT_PULLDOWN);
    pinMode(PA6,INPUT_PULLDOWN);
}

int bcd_7seg[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; // Umrechnung
int stufe=0;

void ausgebenEiner(){
  GPIOC->ODR = bcd_7seg[stufe] | (1<<11); // Einer einschalten
}

typedef enum {CHILLEN_ENTER,CHILLEN,STUFEUP,STUFEDOWN} zustandstyp;
zustandstyp zustand=CHILLEN_ENTER;

void loop(){
  switch(zustand){
    case CHILLEN_ENTER:
      
      break;
    case CHILLEN:
      
      break;
    case STUFEUP:
      
      break;
    case STUFEDOWN:
      
      break;
  }
}

❓ Wozu ist der Zustand CHILLEN_ENTER bei der Implementierung notwendig?

🖥 Vervollständigen Sie den Quellcode.

Lösung
void setup(){   // Einmalige Ausführung => Initialisierungen...
    pinMode(PC0, OUTPUT);  // ohne diese Zeile klappts nicht
    GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
    pinMode(PC11,OUTPUT); // Einer
    pinMode(PC12,OUTPUT); // Zehner
    pinMode(PA1,INPUT_PULLDOWN);
    pinMode(PA6,INPUT_PULLDOWN);
}

int bcd_7seg[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; // Umrechnung
int stufe=0;

void ausgebenEiner(){
  GPIOC->ODR = bcd_7seg[stufe] | (1<<11); // Einer einschalten
}

typedef enum {CHILLEN_ENTER,CHILLEN,STUFEUP,STUFEDOWN} zustandstyp;
zustandstyp zustand=CHILLEN_ENTER;

void loop(){
  switch(zustand){
    case CHILLEN_ENTER:
      ausgebenEiner();
      zustand = CHILLEN;
      break;
    case CHILLEN:
      if(digitalRead(PA6) && stufe<9){
        stufe++;
        zustand = STUFEUP;
        delay(5); // Entprellen
      }
      if(digitalRead(PA1) && stufe>0){
        stufe--;
        zustand = STUFEDOWN;
        delay(5); // Entprellen
      }
      break;
    case STUFEUP:
      if(!digitalRead(PA6)){
        zustand = CHILLEN_ENTER;
        delay(5);
      }
      break;
    case STUFEDOWN:
      if(!digitalRead(PA1)){
        zustand = CHILLEN_ENTER;
        delay(5);
      }
      break;
  }
}

Zweistellige Ausgabe

Die Segmentanzeigen werden im Zeitmultiplex betrieben. Einerstelle ausgeben, warten, Zehnerstelle ausgeben, warten und wieder von vorne und das die ganze Zeit. Ersetze enter/ausgebenEiner() durch do/ausgeben().

#define AZEIT 20 // 20ms Stelle anzeigen, Flackern ist sichtbar
void ausgeben(){
  GPIOC->ODR = bcd_7seg[stufe%10] | (1<<11); // Einer einschalten
  delay(AZEIT);
  GPIOC->ODR = bcd_7seg[stufe/10] | (1<<12); // Zehner einschalten
  delay(AZEIT);
}
Zustandsdiagramm
Zustandsdiagramm

Beim Drücken einer Taste bleibt die Ausgabe allerdings stehen. Auch bei StufeUp und StufeDown sollte ausgegeben werden.

Zustandsdiagramm
Zustandsdiagramm

Da es eh für alle Zustände gilt, kann ausgeben() auch vor der Switch-Anweisung aufgerufen werden.
🖥 Modifizieren Sie Ihren Code entsprechend.

Lösungsvorschlag
void setup(){   // Einmalige Ausführung => Initialisierungen...
    pinMode(PC0, OUTPUT);  // ohne diese Zeile klappts nicht
    GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
    pinMode(PC11,OUTPUT); // Einer
    pinMode(PC12,OUTPUT); // Zehner
    pinMode(PA1,INPUT_PULLDOWN);
    pinMode(PA6,INPUT_PULLDOWN);
}

int bcd_7seg[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; // Umrechnung
int stufe=0;

#define AZEIT 10 
void ausgeben(){
  GPIOC->ODR = bcd_7seg[stufe%10] | (1<<11); // Einer einschalten
  delay(AZEIT);
  GPIOC->ODR = bcd_7seg[stufe/10] | (1<<12); // Zehner einschalten
  delay(AZEIT);
}

enum {CHILLEN,STUFEUP,STUFEDOWN} zustand=CHILLEN;
void loop(){
  ausgeben();
  switch(zustand){
    case CHILLEN:
      if(digitalRead(PA6) && stufe<99){
        stufe++;
        zustand = STUFEUP;
        delay(5); // Entprellen
      }
      if(digitalRead(PA1) && stufe>0){
        stufe--;
        zustand = STUFEDOWN;
        delay(5); // Entprellen
      }
      break;
    case STUFEUP:
      if(!digitalRead(PA6)){
        zustand = CHILLEN;
        delay(5);
      }
      break;
    case STUFEDOWN:
      if(!digitalRead(PA1)){
        zustand = CHILLEN;
        delay(5);
      }
      break;
  }
}

Garagentorsteuerungssimulation

Die Simulation einer Garagentorsteuerung sollen Sie entwickeln. Der Zustand des Tors wird durch Leuchtband L0..L6 angezeigt. Wenn das Tor auf ist, leuchtet nur L0, ist es zu ist leuchten L0..L6. L7 soll später blinken, wenn sich das Tor bewegt. Zunächst wird nur der Schlüsselschalter mit PA1 (zu) und PA6 (auf) und L0..L6 berücksichtigt.

Ausgabe- und Eingabe-Belegung
Ausgabe- und Eingabe-Belegung
Schlüsselschalter
Schlüsselschalter
PA1PA6PA10
ZuAufHandsender
Tasterbelegung

Teil 1: Schlüsselschalter steuert Tor, noch kein Blinken und kein Handsender

Die Taster sind bekanntermaßen nicht entprellt, wir tun aber naiv zunächst so, als ob sie es wären. Durch Betätigen von PA1 fährt das Tor zu, ein kurzes Betätigen reicht, um die Fahrt in Gang zu setzen. Dabei wird in der Simulation nach der Zeit LEUCHTDAUER jeweils eine weitere LED eingeschaltet, bis L0..L6 leuchten, dann stoppt das Tor. Entsprechend der Ablauf zum Öffnen des Tors mit PA6. Wird während des Laufs der Schalter in die Gegenrichtung bewegt, stoppt das Tor auch.
Hier ein wenig Quelltext-Vorgabe:

int tor = 1;
enum {STOPP,ZU,AUF} zustand=STOPP; // Aufzaehlungstyp
#define LEUCHTDAUER 300

void setup() {
  pinMode(PC0, OUTPUT);  // ohne diese Zeile klappts nicht
  GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
  pinMode(PA1,INPUT_PULLDOWN); // Zu
  pinMode(PA6,INPUT_PULLDOWN); // Auf
  pinMode(PA10,INPUT_PULLDOWN);// Handsender
  GPIOC->ODR = tor;
}

void loop() {
}

🖌 🖥 Erstellen Sie ein Zustandsdiagramm und entwickeln Sie daraus das Programm. Verwenden Sie zunächst digitalRead(). Ich bin auf Ihre Lösungen gespannt!

Naives Zustandsdiagramm
Zustandsdiagramm
Zustandsdiagramm
Naiver Code dazu
int tor = 1;
enum {STOPP,ZU,AUF} zustand=STOPP; // Aufzaehlungstyp
#define LEUCHTDAUER 300

void setup() {
  pinMode(PC0, OUTPUT);  // ohne diese Zeile klappts nicht
  GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
  pinMode(PA1,INPUT_PULLDOWN); // Zu
  pinMode(PA6,INPUT_PULLDOWN); // Auf
  pinMode(PA10,INPUT_PULLDOWN);// Handsender
  GPIOC->ODR = tor;
}

void loop() {
  switch (zustand){         // in Abhaengigkeit des Zustands
      case STOPP:
        if(digitalRead(PA1)){  // Zu?
          zustand=ZU;
        }
        if(digitalRead(PA6)){  // Auf?
          zustand=AUF;
        }
        break;
      case ZU:
        if(digitalRead(PA6) || tor>=0b1000000){  // Auf oder Tor zu
          zustand=STOPP;
        }
        else{                               // do Aktionen
          tor = (tor<<1) + 1;
          GPIOC->ODR = tor;
          delay(LEUCHTDAUER);
        }
        break;
      case AUF:
        if(digitalRead(PA1) || tor<=0b1){ // Zu oder Tor offen
          zustand=STOPP;
        }
        else{                               // do Aktionen
          tor = tor>>1;
          GPIOC->ODR = tor;
          delay(LEUCHTDAUER);
        }
      break;
    }
}

❓ Bei den naiven Lösungsvorschlägen wurde das Prellen der Tasten nicht beachtet, sehen Sie eine Auswirkung?

Teil 2: Handsender verwenden können

Durch Betätigen von PA10 (Handsender) soll das Tor in den jeweils nächsten Zustand schalten:
ZU->STOPP->AUF->STOPP->ZU->STOPP usw.
Um das Prellen in den Griff zu kriegen und eine “schönere” Lösung zu erhalten ist wieder buttonCheck() vorgegeben.

#define BUTTONREADER GPIOA->IDR & 0b10001000010 // nur die relevanten Tastenbits
int buttonOld = 0;              // alter Tasten-Zustand 
int buttonEnter,buttonExit;     // gedrueckte und losgelassene Tasten 

void buttonCheck(){             // Tastaturabfrage mit Flankendedektion  
  int buttonTest,tmp; 
  buttonEnter = 0, buttonExit = 0; 
  buttonTest = BUTTONREADER;       // Einlesen
  if (buttonOld != buttonTest){    // hat sich was getan 
    delay(7);                      // 7ms Prellen abwarten 
    tmp = BUTTONREADER;            // noch mal Einlesen
    if (tmp == buttonTest){        // ist es stabil? 
      buttonEnter = (~buttonOld) & buttonTest; // steigende Flanke !alt und neu 
      buttonExit = buttonOld & (~buttonTest);  // fallende Flanke alt und !neu 
      buttonOld = buttonTest; 
    } 
  } 
}

#define T_ZU   (buttonEnter&1<<1)  // PA1
#define T_AUF  (buttonEnter&1<<6)  // PA6
#define T_HAND (buttonEnter&1<<10) // PA10

typedef enum {STOPP,ZU,AUF} ZustandTyp;  // Aufzaehlungstyp
ZustandTyp zustand = STOPP;
ZustandTyp letzteRichtung = ZU;

🖌 🖥 Erweitern Sie Ihr Zustandsdiagramm entsprechend und testen Sie ihre Lösung. Tipp: Schauen Sie sich in der Forsa die Zustandsdiagramm-Entscheidungsraute an.

Mögliches Zustandsdiagramm
Zustandsdiagramm
Zustandsdiagramm
Möglicher Code
int tor = 1;
#define LEUCHTDAUER 300

void setup() {
  pinMode(PC0, OUTPUT);  // ohne diese Zeile klappts nicht
  GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
  pinMode(PA1,INPUT_PULLDOWN); // Zu
  pinMode(PA6,INPUT_PULLDOWN); // Auf
  pinMode(PA10,INPUT_PULLDOWN);// Handsender
  GPIOC->ODR = tor;
}

#define BUTTONREADER GPIOA->IDR & 0b10001000010 // nur die relevanten Tastenbits
int buttonOld = 0;              // alter Tasten-Zustand 
int buttonEnter,buttonExit;     // gedrueckte und losgelassene Tasten 

void buttonCheck(){             // Tastaturabfrage mit Flankendedektion  
  int buttonTest,tmp; 
  buttonEnter = 0, buttonExit = 0; 
  buttonTest = BUTTONREADER;       // Einlesen
  if (buttonOld != buttonTest){    // hat sich was getan 
    delay(5);                      // 5ms Prellen abwarten 
    tmp = BUTTONREADER;            // noch mal Einlesen
    if (tmp == buttonTest){        // ist es stabil? 
      buttonEnter = (~buttonOld) & buttonTest; // steigende Flanke !alt und neu 
      buttonExit = buttonOld & (~buttonTest);  // fallende Flanke alt und !neu 
      buttonOld = buttonTest; 
    } 
  } 
}

#define T_ZU   (buttonEnter&1<<1)  // PA1
#define T_AUF  (buttonEnter&1<<6)  // PA6
#define T_HAND (buttonEnter&1<<10) // PA10

typedef enum {STOPP,ZU,AUF} ZustandTyp;  // Aufzaehlungstyp
ZustandTyp zustand = STOPP;
ZustandTyp letzteRichtung = ZU;

void loop() {
  buttonCheck();
  switch (zustand){         // in Abhaengigkeit des Zustands
      case STOPP:
        if(T_ZU){  // Zu?
          zustand=ZU;
        }
        if(T_AUF){  // Auf?
          zustand=AUF;
        }
        if(T_HAND){
          if(letzteRichtung==ZU){
            zustand=AUF;
            letzteRichtung=AUF;
          }
          else{
            zustand=ZU;
            letzteRichtung=ZU;
          }
        }
        break;
      case ZU:
        if(T_HAND || T_AUF || tor>=0b1000000){  // Auf oder Tor zu
          zustand=STOPP;
        }
        else{                               // do Aktionen
          tor = (tor<<1) + 1;
          GPIOC->ODR = tor;
          delay(LEUCHTDAUER);
        }
        break;
      case AUF:
        if(T_HAND || T_ZU || tor<=0b1){ // Zu oder Tor offen
          zustand=STOPP;
        }
        else{                               // do Aktionen
          tor = tor>>1;
          GPIOC->ODR = tor;
          delay(LEUCHTDAUER);
        }
      break;
    }
}

Teil 3: Besser reagieren und mit Blinken

Zwei Dinge sollen besser werden, durch das Delay für die Leuchtdauer werden manche Tastendrücke nicht wahr genommen und beim Lauf des Tors soll L7 blinken.
Ein Unterprogramm fahren() ersetzt den do/-Code nach buttonCheck() in ZU und AUF:

  • L7 soll immer nach 100ms umschalten (An/Aus)
  • Nach 4 Zyklen jeweils eine LED des Leuchtbandes mehr- bzw. weniger leuchten lassen (zustand beachten).

✍️ Entwerfen Sie den Quellcode für fahren().

Lösungsvorschlag für fahren()
void fahren(){
  static int blinken =0; // persistente lokale Variable
  blinken++;
  if(blinken%4==0){
    if(zustand == ZU){
      tor = (tor<<1) + 1;
    }
    else{
      tor = tor>>1;
    }
    GPIOC->ODR = tor;
  }
  if(blinken%2) GPIOC->BSRR = 1<<7;
  else GPIOC->BSRR = 1<<7+16;
  delay(100);
}

🖌 🖥 Modifizieren Sie das Zustandsdiagramm und implementieren Sie die Lösung.

Mögliches Zustandsdiagramm
Zustandsdiagramm
Zustandsdiagramm
Möglicher Quellcode
int tor = 1;
#define LEUCHTDAUER 100

void setup() {
  pinMode(PC0, OUTPUT);  // ohne diese Zeile klappts nicht
  GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
  pinMode(PA1,INPUT_PULLDOWN); // Zu
  pinMode(PA6,INPUT_PULLDOWN); // Auf
  pinMode(PA10,INPUT_PULLDOWN);// Handsender
  GPIOC->ODR = tor;
}

#define BUTTONREADER GPIOA->IDR & 0b10001000010 // nur die relevanten Tastenbits
int buttonOld = 0;              // alter Tasten-Zustand 
int buttonEnter,buttonExit;     // gedrueckte und losgelassene Tasten 

void buttonCheck(){             // Tastaturabfrage mit Flankendedektion  
  int buttonTest,tmp; 
  buttonEnter = 0, buttonExit = 0; 
  buttonTest = BUTTONREADER;       // Einlesen
  if (buttonOld != buttonTest){    // hat sich was getan 
    delay(7);                      // 7ms Prellen abwarten 
    tmp = BUTTONREADER;            // noch mal Einlesen
    if (tmp == buttonTest){        // ist es stabil? 
      buttonEnter = (~buttonOld) & buttonTest; // steigende Flanke !alt und neu 
      buttonExit = buttonOld & (~buttonTest);  // fallende Flanke alt und !neu 
      buttonOld = buttonTest; 
    } 
  } 
}

#define T_ZU   (buttonEnter&1<<1)  // PA1
#define T_AUF  (buttonEnter&1<<6)  // PA6
#define T_HAND (buttonEnter&1<<10) // PA10

typedef enum {STOPP,ZU,AUF} ZustandTyp;  // Aufzaehlungstyp
ZustandTyp zustand = STOPP;
ZustandTyp letzteRichtung = ZU;

void fahren(){
  static int blinken =0; // persistente lokale Variable
  blinken++;
  if(blinken%4==0){
    if(zustand == ZU){
      tor = (tor<<1) + 1;
    }
    else{
      tor = tor>>1;
    }
    GPIOC->ODR = tor;
  }
  if(blinken%2) GPIOC->BSRR = 1<<7;
  else GPIOC->BSRR = 1<<7+16;
  delay(LEUCHTDAUER);
}

void loop() {
  buttonCheck();
  switch (zustand){         // in Abhaengigkeit des Zustands
      case STOPP:
        if(T_ZU){  // Zu?
          zustand=ZU;
        }
        if(T_AUF){  // Auf?
          zustand=AUF;
        }
        if(T_HAND){
          if(letzteRichtung==ZU){
            zustand=AUF;
            letzteRichtung=AUF;
          }
          else{
            zustand=ZU;
            letzteRichtung=ZU;
          }
        }
        break;
      case ZU:
        if(T_HAND || T_AUF || tor>=0b1000000){  // Auf oder Tor zu
          zustand=STOPP;
          GPIOC->BSRR = 1<<7+16; // L7 aus
        }
        else{                               // do Aktionen
          fahren();
        }
        break;
      case AUF:
        if(T_HAND || T_ZU || tor<=0b1){ // Zu oder Tor offen
          zustand=STOPP;
          GPIOC->BSRR = 1<<7+16; // L7 aus
        }
        else{                               // do Aktionen
          fahren();
        }
      break;
    }
}

Skigondel

Die Skigondel hat einen Fassungsvermögen von 6 Personen. Die Personen gehen durch ein Drehkreuz in einen Einstiegsbereich und steigen alle in die Gondel ein. Eine Anzeige zeigt an, wie viele das Drehkreuz zum Einstiegsbereich passiert haben, im Bild sind es 3 Personen (L3 leuchtet). Das Drehkreuz wird bei 6 Personen blockiert (HALT-Signal PC7). Nach Start der Gondel ist der Einstiegsbereich wieder leer und die Anzeige L0 leuchtet. Die Gondel kann auch mit weniger als 6 Personen losfahren.

  • Die Anzeige ist an PC6..PC0 angeschlossen.
  • Das HALT-Signal wird an PC7 ausgegeben (PC7 und PC6 sind 1).
  • Das Drehkreuz gibt ein kurzes 1-Signal (DK an PA6) wenn eine Person durchgegangen ist.
  • Wenn die Gondel ablegt gibt sie ein kurzes 1-Signal (RESET an PA1).
  • Die Signallängen der kurzen Signale DK und RESET sind <400ms, somit könnte das Entprellen der Tasten und das Warten bis die Tasten wieder 0 sind mit jeweils einem delay(400) vereinfacht werden.
void setup(){   // Einmalige Ausführung => Initialisierungen...
    pinMode(PC0, OUTPUT);  // ohne diese Zeile klappts nicht
    GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
    pinMode(PA1,INPUT_PULLDOWN);
    pinMode(PA6,INPUT_PULLDOWN);
    GPIOC->ODR=1;
}
typedef enum {ZAEHLEN,HALT} zustandstyp;
zustandstyp zustand=ZAEHLEN;

void loop(){
  switch(zustand){
    case ZAEHLEN:
      break;
    case HALT:
      break;
  }
}
Skigondel Einstiegsbereich
Skigondel Einstiegsbereich
  1. 🖌 Zeichen Sie ein Zustandsdiagramm.
  2. 🖥 Vervollständigen Sie den Code.
Lösungsvorschlag Zustandsdiagramm
Zustandsdiagramm Skigondel
Lösungsvorschlag Code
void setup(){   // Einmalige Ausführung => Initialisierungen...
    pinMode(PC0, OUTPUT);  // ohne diese Zeile klappts nicht
    GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
    pinMode(PA1,INPUT_PULLDOWN);
    pinMode(PA6,INPUT_PULLDOWN);
    GPIOC->ODR=1;
}
typedef enum {ZAEHLEN,HALT} zustandstyp;
zustandstyp zustand=ZAEHLEN;

void loop(){
  switch(zustand){
    case ZAEHLEN:
      if(GPIOC->ODR>=0x40){
        GPIOC->ODR=0xC0;
        zustand=HALT;
      }
      if(digitalRead(PA6)){ // DK-Signal?
        GPIOC->ODR=GPIOC->ODR<<1;
        delay(400);
      }
      if(digitalRead(PA1)){ // RESET-Signal?
        GPIOC->ODR=1;
        delay(400);
      }
      break;
    case HALT:
      if(digitalRead(PA1)){ // RESET-Signal?
        GPIOC->ODR=1;
        delay(400);
        zustand=ZAEHLEN;
      }
      break;
  }
}
Lösungsvorschlag2 Zustandsdiagramm
Zustandsdiagramm2 Skigondel
Lösungsvorschlag2 Code
void setup(){   // Einmalige Ausführung => Initialisierungen...
    pinMode(PC0, OUTPUT);  // ohne diese Zeile klappts nicht
    GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
    pinMode(PA1,INPUT_PULLDOWN);
    pinMode(PA6,INPUT_PULLDOWN);
    GPIOC->ODR=1;
}
typedef enum {ZAEHLEN,HALT} zustandstyp;
zustandstyp zustand=ZAEHLEN;

void loop(){
  switch(zustand){
    case ZAEHLEN:
      if(digitalRead(PA6)){ // DK-Signal?
        GPIOC->ODR=GPIOC->ODR<<1;
        delay(400);
        if(GPIOC->ODR>=0x40){
          GPIOC->ODR=0xC0;
          zustand=HALT;
        }
      }
      if(digitalRead(PA1)){ // RESET-Signal?
        GPIOC->ODR=1;
        delay(400);
      }
      break;
    case HALT:
      if(digitalRead(PA1)){ // RESET-Signal?
        GPIOC->ODR=1;
        delay(400);
        zustand=ZAEHLEN;
      }
      break;
  }
}

2 Kommentare

Kommentare sind geschlossen.