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.
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.
Ist diese Lösung falsch?
❓ Überprüfen Sie den Ablauf.
Lösung
Der Ablauf stimmt, aber das Diagramm ist nicht so übersichtlich.
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:

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:
enum zustandstyp {GRUEN,GELB,ROT,ROT_GELB}; // Aufzaehlungstyp
enum 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);
}
enum zustandstyp {GRUEN,GELB,ROT,ROT_GELB}; // Aufzaehlungstyp
enum 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 sauber reparieren:
void setup() {
pinMode(PC0,OUTPUT);
pinMode(PC2,OUTPUT);
pinMode(PC4,OUTPUT);
pinMode(PC6,OUTPUT);
pinMode(PC7,OUTPUT);
pinMode(PA1,INPUT_PULLDOWN);
}
enum zustandstyp {GRUEN_ENTRY,GRUEN,GELB,ROT,ROT_GELB}; // Aufzaehlungstyp
enum zustandstyp zustand = GRUEN_ENTRY; // Startzustand
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
PA1 | PA6 | PA10 |
---|---|---|
An <-> Aus | langsamer | schneller |
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.
Stufe | PC3 | PC2 | PC1 | PC0 |
---|---|---|---|---|
Aus | 0 | 0 | 0 | 0 |
Stufe 0 | 0 | 0 | 0 | 1 |
Stufe 1 | 0 | 0 | 1 | 1 |
Stufe 2 | 0 | 1 | 1 | 1 |
Stufe 3 | 1 | 1 | 1 | 1 |
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;
}
}
}
enum zustandstyp {AUS,AN}; // Aufzaehlung der Zustaende
enum 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;
}
}
}
enum zustandstyp {AUS,AN}; // Aufzaehlung der Zustaende
enum 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
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)
enum zustandstyp {AUS,AN}; // Aufzaehlung der Zustaende
enum 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
Autoblinker
PA1 | PA6 | PA10 |
---|---|---|
Nach links | Nach rechts | Warnblinker |
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.
Hier Code und Zustandsdiagramm zum Start:
Hinweis auf faulen Lehrer: Ich hatte keine Lust 8* pinMode(PCx, OUTPUT) für x ∊ {0..7} zu schreiben und habe dieses Wissen verwendet: PC0..PC7 als Ausgänge einstellen
void setup() {
pinMode(PC0, OUTPUT); // ohne diese Zeile klappts nicht
GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang (zu faul für 8* pinMode(..))
pinMode(PA1,INPUT_PULLDOWN);
pinMode(PA6,INPUT_PULLDOWN);
pinMode(PA10,INPUT_PULLDOWN);
}
enum zustandstyp {AUS,LINKS,RECHTS,WARNBLINKEN}; // Aufzaehlungstyp
enum zustandstyp zustand=AUS;
#define LEUCHTDAUER 70
#define BLINKDAUER 400
Diesmal wird die Arduino-Funktion digitalRead(..) verwendet.
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 (zu faul für 8* pinMode(..))
pinMode(PA1,INPUT_PULLDOWN);
pinMode(PA6,INPUT_PULLDOWN);
pinMode(PA10,INPUT_PULLDOWN);
}
enum zustandstyp {AUS,LINKS,RECHTS,WARNBLINKEN}; // Aufzaehlungstyp
enum zustandstyp zustand=AUS;
#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.

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 (zu faul für 8* pinMode(..))
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
}
enum zustandstyp {CHILLEN_ENTER,CHILLEN,STUFEUP,STUFEDOWN};
enum 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
}
enum zustandstyp {CHILLEN_ENTER,CHILLEN,STUFEUP,STUFEDOWN};
enum 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);
}
Beim Drücken einer Taste bleibt die Ausgabe allerdings stehen. Auch bei StufeUp und StufeDown sollte ausgegeben werden.
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 zustandstyp {CHILLEN,STUFEUP,STUFEDOWN};
enum zustandstyp 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.


PA1 | PA6 | PA10 |
---|---|---|
Zu | Auf | Handsender |
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 zustandstyp {STOPP,ZU,AUF}; // Aufzaehlungstyp
enum zustandstyp zustand=STOPP;
#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
Naiver Code dazu
int tor = 1;
enum zustandstyp {STOPP,ZU,AUF}; // Aufzaehlungstyp
enum zustandstyp zustand=STOPP;
#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
enum zustandstyp {STOPP,ZU,AUF}; // Aufzaehlungstyp
enum zustandstyp zustand = STOPP;
enum zustandstyp 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
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
enum zustandstyp {STOPP,ZU,AUF}; // Aufzaehlungstyp
enum zustandstyp zustand = STOPP;
enum zustandstyp 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
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;
}
enum zustandstyp {ZAEHLEN,HALT};
enum zustandstyp zustand = ZAEHLEN;
void loop(){
switch(zustand){
case ZAEHLEN:
break;
case HALT:
break;
}
}
- 🖌 Zeichen Sie ein Zustandsdiagramm.
- 🖥 Vervollständigen Sie den Code.
Lösungsvorschlag Zustandsdiagramm
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;
}
enum zustandstyp {ZAEHLEN,HALT};
enum 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
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;
}
enum zustandstyp {ZAEHLEN,HALT};
enum 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;
}
}