Raspberry Pi Pico Kurs – #10 – Arrays, Strukturen und Zustandsautomaten

Lesezeit 19 min.

Im letzten Tutorial haben wir gemeinsam das inzwischen wohlbekannte blink led-Projekt gestartet, das jedoch keine CPU-blockierende sleep_ms-Funktion enthielt. Außerdem haben wir zum ersten Mal Indikatoren und eine Struktur verwendet, die wir, wie versprochen, in diesem Artikel behandeln werden. Außerdem erkläre ich Ihnen, wie Sie Zustandsautomaten in der Sprache C erstellen können.

Kaufen Sie ein Set, um das Programmieren mit dem Raspberry Pi Pico W zu erlernen, und nutzen Sie die Vorteile des Kurses, der im Botland Blog verfügbar ist!

Im Set enthalten: Raspberry Pi Pico W-Modul, Kontaktplatte, Leitungen, LEDs, Widerstände, Tasten, Fotowiderstände, digitale Licht-, Temperatur-, Feuchtigkeits- und Drucksensoren, OLED-Display und ein USB-microUSB-Kabel.

Vor dem Start sollte das Team zusammengestellt werden

Ein Satz von Komponenten für den Raspberry Pi Pico Kurs.

Wer das Programmieren anhand von realen Projekten erlernen möchte, braucht natürlich die richtige Ausrüstung, aber keine Sorge – Sie müssen jetzt nicht von einem Artikel zum nächsten springen und eine Liste der benötigten elektronischen Komponenten erstellen. Im Botland-Shop ist ein fertiges Set erhältlich, das alle notwendigen Komponenten enthält, um die in der Tutorial-Reihe beschriebenen Projekte mit dem Raspberry Pi Pico durchzuführen.

In dem fertigen Set von Elementen finden Sie:

  • Raspberry Pi Pico W,
  • MicroUSB-Kabel,
  • Kontaktplatte,
  • Ein Set von Anschlusskabeln in drei Ausführungen,
  • Ein Set von LEDs in drei Farben,
  • Ein Set der in der Elektronik am häufigsten verwendeten Widerstände,
  • Tact Switch-Tasten,
  • Fotowiderstände,
  • Digitaler Lichtsensor,
  • Digitaler Feuchtigkeits-, Temperatur- und Drucksensor,
  • OLED-Display.

Einzelne und mehrdimensionale Arrays

Zunächst wäre es sinnvoll, auf das Thema der Strukturen zurückzukommen, das bereits bei dem kürzlich diskutierten Projekt zur Sprache kam. Bevor wir jedoch weitermachen, werden wir versuchen, einen Code auszuführen, der auf etwas einfacheren Konstrukten basiert, nämlich Arrays.

Bisher haben wir ganz allgemein verschiedene Arten von Variablen verwendet; sie können ganze Zahlen, Fließkommazahlen und logische Zustände speichern. Es ist jedoch erwähnenswert, dass eine einzelne Variable nur einen einzigen Wert speichern kann. Wenn wir die Temperatur vom eingebauten Sensor des RP2040 ablesen, wird immer der letzte Wert in der Variable gespeichert und es gibt keine Möglichkeit, ihn mit einer früheren Messung zu vergleichen.

Ein Vergleich von Variablen und Arrays.

Wenn wir davon ausgehen, dass eine einzelne Variable ein Blatt Papier sein könnte, auf dem der geschriebene Text sein Wert ist, dann könnte ein Array ein Buch sein. Mit anderen Worten: Arrays in C sind Sammlungen von Variablen desselben Typs, deren Wert variieren kann. Die Verwendung von Arrays kann sehr nützlich sein, insbesondere wenn wir viele Werte desselben Typs speichern müssen. Die Deklaration einzelner Variablen ist in einem solchen Fall einfach unnötig und verkompliziert den Code unnötig. Darüber hinaus können mit Arrays so genannte Puffer erstellt werden, die für verschiedene Arten der Kommunikation besonders nützlich sind, sowie Zeichenfolgen von Text, so genannte Strings (von dem englischen Wort string). Es sollte auch erwähnt werden, dass Arrays mehrere Dimensionen haben können, aber mit diesem Thema werden wir uns gleich beschäftigen. Nehmen wir als ersten Schritt ein einfaches Programm, das ein eindimensionales Array verwendet.

Ein Schaltkreis, der aus früheren Ratgebern bekannt ist.

An diesem Punkt brauchen wir die vorbereitete Schaltung nicht zu ändern. Im ersten Projekt, das ich als arrays_test beschrieben habe, werden wir die LEDs steuern. Dieses Mal bestimmt jedoch der im Array gespeicherte Wert, ob die Diode ausgelöst wird oder nicht. Außerdem werden wir versuchen, einen bestimmten Wert im Array zu ändern, also gehen wir zum ersten Programm über.

				
					#include "pico/stdlib.h"

#define GREEN_LED 0
#define YELLOW_LED 1
#define RED_LED 2

bool led_state_array[3] = {1, 0, 1}; // array declaration

int main() {
    stdio_init_all(); //initialization of the stdio.h library

    gpio_init(GREEN_LED);
    gpio_init(YELLOW_LED);
    gpio_init(RED_LED);
    gpio_set_dir(GREEN_LED, GPIO_OUT);
    gpio_set_dir(YELLOW_LED, GPIO_OUT);
    gpio_set_dir(RED_LED, GPIO_OUT);

    while(true) {

            gpio_put(GREEN_LED, led_state_array[0]);
            gpio_put(YELLOW_LED, led_state_array[1]);
            gpio_put(RED_LED, led_state_array[2]);

            sleep_ms(3000);

            led_state_array[0] = 0; // changing the value of an element with index zero

    }
}

				
			

Die Struktur des Codes ist eigentlich recht einfach. Hier haben wir die Standardbibliothek eingebunden, die Definition der Pins, an die die LEDs angeschlossen sind, die dann am Anfang der Hauptfunktion main initialisiert werden. Die ganze Magie im Zusammenhang mit Arrays geschieht innerhalb der Endlosschleife und oberhalb der Hauptfunktion.

Erklärung der Arrays.

Wenden wir uns zunächst der Erklärung des Arrays selbst zu. Es sieht dem Code, der eine Variable zum Leben erweckt, sehr ähnlich. Hier können wir auch den Typ des Arrays unterscheiden, genauer gesagt den Typ der Daten, die sich in dem Array befinden werden. In unserem Beispiel ist es bool, da wir logische Werte speichern werden, die dem Zustand entsprechen, in dem sich die LED befindet. Als Nächstes folgt der Name des Arrays, hier ist es einfach led_state_array. In eckige Klammern setzen wir die Zahl, die der Anzahl der Elemente im Array entspricht. Wir werden drei Dioden ansteuern, deshalb habe ich hier die Zahl drei eingesetzt. Wichtig ist, dass wir bei der Deklaration eines Arrays die Größe des Arrays nicht angeben müssen. Die eckige Klammer kann leer gelassen werden und der Compiler würde die Anzahl der später deklarierten Elemente selbst überprüfen. Nach dem Gleichheitszeichen werden die Standardwerte aller Elemente im Array in geschweifte Klammern gesetzt. Jedes wird mit dem Index [0], [1] und [2] bezeichnet. Wichtig ist, dass der Indexwert von links nach rechts ansteigt. Das kann kontraintuitiv sein, insbesondere für diejenigen, die sich mit digitaler Elektronik beschäftigt haben, bei der das jüngste Bit mit einem Null-Index normalerweise ganz rechts platziert wird.

Das so deklarierte Array speichert drei Variablen vom Typ bool, deren Werte wir bereits zu Beginn angegeben haben.

In der while-Schleife sehen wir drei Funktionen, die aufeinanderfolgende LEDs steuern. In diesem Fall bestimmen wir den Zustand der leuchtenden Struktur durch ein bestimmtes Element des Arrays, indem wir dessen Namen und Index in eckigen Klammern angeben. So entspricht der Zustand der grünen LED dem ersten Element im Array, die gelbe LED nimmt den Zustand des zweiten Elements an und der roten LED wird der Wert des letzten Elements im Array zugewiesen. Wie Sie sich denken können, sollten beim Starten des Programms die beiden äußersten LEDs aufleuchten, d.h. grün und rot.

Der Einfachheit halber werden wir jedoch den Wert des ersten Elements im Array ändern, nachdem wir drei Sekunden gewartet haben. Der Verweis darauf ist identisch mit dem in der LED-Steuerungsfunktion. Zuerst geben wir den Namen des Arrays an und in eckigen Klammern den Index des entsprechenden Elements. Wie bei Variablen weisen wir dank des Gleichheitszeichens einen Wert von Null zu.

Nachdem wir das Programm ausgeführt haben, können wir den erwarteten Effekt sehen. Im ersten Schritt wurden die beiden äußersten LEDs – grün und rot – aktiviert. Anschließend, nach einer Wartezeit von drei Sekunden, änderte der Mikrocontroller das erste Element im Array und kehrte an den Anfang der while-Schleife zurück, um die LEDs erneut anzusteuern. Auf diese Weise wurde das grüne Lichtelement ausgeschaltet, da sein Zustand durch das Element im Array mit dem Index Null beschrieben wurde.

Deklaration eines zweidimensionalen Arrays.

Im ersten Beispiel haben wir ein eindimensionales Array verwendet. Nachfolgende Daten darin wurden durch einen einzelnen Index beschrieben, aber es ist wissenswert, dass auch mehrdimensionale Arrays in C erstellt werden können, und das ist der Fall, den wir dieses Mal untersuchen werden. Doch bevor wir uns dem Programm zuwenden, lassen Sie uns anhand des Beispiels, das wir gleich verwenden werden, sehen, wie Arrays dieses Typs deklariert werden.

Im Vergleich zur letzten Einsetzung des Arrays ist die Struktur dieses Befehls leicht erweitert worden. Beachten Sie zunächst, dass auf den Namen des Arrays zwei eckige Klammern folgen, in denen wir die Dimensionen des Arrays angeben, also die Anzahl der Zeilen und Spalten. In unserem Programm werden wir immer noch drei LEDs ansteuern, also wird das Board drei Spalten haben. Die Anzahl der Zeilen wird dagegen acht betragen, um alle möglichen Zustände der drei Elemente zu nutzen. Wenn Sie sich die Zustände ansehen, werden Sie in den folgenden Zeilen des Arrays feststellen, dass sie fortlaufenden Zahlen im Binärcode entsprechen. In einem auf diese Weise vorbereiteten Array wird jedes Element durch seinen eigenen Index beschrieben, der der Zeile und Spalte entspricht. Spalten werden, wie im vorherigen Beispiel erwähnt, von links nach rechts angegeben, während Zeilen von oben nach unten angegeben werden.

				
					#include "pico/stdlib.h"

#define GREEN_LED 0
#define YELLOW_LED 1
#define RED_LED 2

// declaration of a multidimensional array
bool led_state_array[8][3] = {{0, 0, 0},
                              {0, 0, 1},
                              {0, 1, 0},
                              {0, 1, 1},
                              {1, 0, 0},
                              {1, 0, 1},
                              {1, 1, 0},
                              {1, 1, 1}};

int main() {
    stdio_init_all(); //initialization of the stdio library

    gpio_init(GREEN_LED);
    gpio_init(YELLOW_LED);
    gpio_init(RED_LED);
    gpio_set_dir(GREEN_LED, GPIO_OUT);
    gpio_set_dir(YELLOW_LED, GPIO_OUT);
    gpio_set_dir(RED_LED, GPIO_OUT);

    while (true) {
        for (int i = 0; i < 8; i++) {
            // Set the diode states according to the current row of the table
            gpio_put(GREEN_LED, led_state_array[i][0]);
            gpio_put(YELLOW_LED, led_state_array[i][1]);
            gpio_put(RED_LED, led_state_array[i][2]);

            // delay
            sleep_ms(1000);
        }
    }
}

				
			

Dieses Mal wurde eine neue Array-Deklaration, die Sie bereits gesehen haben, in den zu testenden Code eingefügt. Die unendliche while-Schleife wurde ebenfalls geändert. Sie enthielt eine for-Schleife, die acht Sprünge durchführte, so dass die LEDs von einem Element aus jeder aufeinanderfolgenden Zeile des Arrays angesteuert wurden. Die Schleife wird achtmal ausgeführt, mit den Befehlen, die die Dioden darin steuern. Der Zustand jeder Diode entspricht einem Element aus dem Array, das sich bei Index [i] und [0], [1] und [2] befindet. Bei jedem weiteren Schleifendurchlauf wird das “i” durch eine andere Zahl von null bis acht ersetzt, so dass den LEDs Werte aus den folgenden Zeilen des Arrays zugewiesen werden. Darüber hinaus wurde eine Verzögerung von einer Sekunde in den Code eingebaut, damit Änderungen im Licht der LEDs sichtbar werden.

Nachdem wir den Code ausgeführt haben, können wir die wechselnden Zustände der LEDs sehen. Wenn wir genauer hinsehen, können wir feststellen, dass sie mit den im Array gespeicherten Werten übereinstimmen.

Es ist wichtig zu wissen, dass wir in der Sprache C nicht nur zwei- und eindimensionale Arrays erstellen können, sondern dass uns nichts daran hindert, ein dreidimensionales oder sogar ein vierdimensionales Array zu initialisieren. Solche Beispiele sind jedoch schon etwas fortgeschrittener und wir werden sie hier nicht analysieren. Zwei Grundformen von Arrays werden uns für den Moment genügen.

Strukturen, oder Pseudo-Objektivität in C

Anhand von einfachen Beispielen lernten wir Arrays kennen, die als kleinere oder größere Sammlungen von Variablen desselben Typs beschrieben werden können, die wir mit Hilfe von Indexen voneinander unterscheiden können. Eine Einschränkung bei dieser Art von Konstruktion ist jedoch der gerade erwähnte Datentyp. In Arrays haben wir nicht die Möglichkeit, mehrere verschiedene Variablen zu speichern, die ein etwas größeres Objekt beschreiben würden. Nehmen wir für dieses Beispiel einen Mann, der einen Namen, ein Alter und eine Größe hat. Der Vorname ist eine Zeichenfolge, das Alter ist eine ganze Zahl und die Größe ist eine Zahl mit einem Komma. Solche Daten können auf keinen Fall in einem Array untergebracht werden, wir müssten drei separate Variablen erstellen und das werden wir auch tun, aber wir werden sie in einer Struktur unterbringen. In der Sprache C gibt es eine bestimmte komplexe Konstruktion, die Struktur genannt wird. Sie ermöglicht es, verschiedene Arten von Variablen zu gruppieren und mit ihnen größere Objekte zu erstellen, die viele Attribute haben, wie der oben erwähnte Mensch. Im folgenden Beispiel werden wir eine solche Struktur zur Beschreibung einer Person erstellen, in der Informationen über ihren Namen, ihr Alter und ihre Größe gespeichert werden. Diese Daten werden, sobald sie gespeichert sind, an den Computer gesendet und auf dem Monitor der seriellen Schnittstelle angezeigt. Für dieses Programm habe ich ein weiteres Projekt mit dem Namen struct_test erstellt, aber bevor wir uns damit befassen, sollten wir uns die Deklaration der Struktur, die wir gleich verwenden werden, genauer ansehen.

Erklärung der Struktur.

Wir erwecken Strukturen in der Sprache C auf eine recht einfache und intuitive Weise zum Leben. Wir beginnen mit den Schlüsselwörtern ‘typdef struct’, dann setzen wir in geschweifte Klammern die so genannte Liste der Struktur-Variablen, die einfach eine Liste von Elementen ist, die den Objekten auf der Grundlage dieser Struktur zugewiesen werden. In unserem Beispiel werden wir die gewünschte Person beschreiben, also verwenden wir ein Array vom Typ char, um den Namen zu speichern, eine uint8_t-Variable, um das Alter zu speichern und eine Fließkommazahl, um die Größe der beschriebenen Person darzustellen. Wir schließen die Definition der Struktur ab, indem wir ihr einen Namen geben. In diesem Fall heißt sie einfach Person.

Eine auf diese Weise vorbereitete Struktur ermöglicht es uns, eine Art ‘Objekt’ im Code zu erstellen, aber dazu kommen wir gleich. An dieser Stelle muss ich erwähnen, dass diese Art der Beschreibung nicht wirklich der C-Sprachtheorie entspricht. In dem “reinen” Beispiel kommt das Schlüsselwort typedef nicht vor und der Name der Struktur steht unmittelbar nach dem Wort struct. Diese etwas komplizierte Definition der Struktur, die ich bereitgestellt habe, folgt jedoch den ungeschriebenen Regeln der Sprache C. Mit ihr müssen wir das Wort struct nicht jedes Mal verwenden, wenn wir uns auf Objekte beziehen, denn dank typedef wird es zu einer Art “festen Typdefinition”.

				
					#include "pico/stdlib.h"
#include 

// structure declaration
typedef struct {
    char name[20];
    u_int8_t age;
    float height;
} Person;

int main() {
    stdio_init_all(); //initialization of the stdio library

    // Create an instance of the Person structure
    Person Rafal;
    Person Kuba;

    // Assigning values to structure fields
    snprintf(Rafal.name, sizeof(Rafal.name), "Rafal");
    Rafal.age = 24;
    Rafal.height = 180.5;

    snprintf(Kuba.name, sizeof(Kuba.name), "Kuba");
    Kuba.age = 28;
    Kuba.height = 176.0;



    while (true) {
    printf("Name: %s\n", Rafal.name);
    printf("Age: %d\n", Rafal.age);
    printf("Height: %.1f cm\n", Rafal.height);

    printf("Name: %s\n", Kuba.name);
    printf("Age: %d\n", Kuba.age);
    printf("Height: %.1f cm\n", Kuba.height);

    // delay
    sleep_ms(1000);
        
    }
}

				
			

Das Programm, mit dem wir strukturelle Objekte zum Leben erwecken können, beginnt mit der inzwischen klassischen Implementierung von Bibliotheken. Im zweiten Schritt definieren wir, wie im obigen Beispiel beschrieben, eine Struktur, mit der es möglich ist, eine Person mit ihrem Namen, ihrem Alter und ihrer Größe zu “generieren”. Beachten Sie im Zusammenhang mit den zuvor besprochenen Arrays, dass der Vorname genau ein 20-elementiges char-Array ist, in dem wir den Vornamen unterbringen, der aus Sicht des Mikrocontrollers einfach eine Zeichenfolge ist.

In der Hauptfunktion main erstellen wir nach der Initialisierung der Standardbefehle der Studiobibliothek Objekte, d.h. Personen, die auf der beschriebenen Struktur basieren. Es werden Rafal und Kuba sein. Die Erstellung des strukturierten Objekts selbst ist äußerst einfach und besteht darin, den Strukturnamen ‘Person’ und einen imaginären Objektnamen anzugeben. Hätten wir die theoretische Methode der C-Sprache zur Definition einer Struktur verwendet, ohne “typedef”, dann hätten wir an dieser Stelle den zusätzlichen Befehl struct verwenden müssen, und so besteht keine Notwendigkeit.

Sobald die Objekte, d.h. Personen, ernannt wurden, können ihnen die entsprechenden Attribute zugewiesen werden. Dies ist die Aufgabe, die in den folgenden Anweisungen ausgeführt wird. Sie gelangen zu einer einzelnen strukturierten Variablen, indem Sie den Namen des Objekts und eine einzelne Variable in der Struktur getrennt durch einen Punkt angeben. Um das Alter von Rafal hinzuzufügen, verwenden wir also Rafal.age und dank des Zuweisungsoperators wird 24 in die Variable “age” des Objekts “Rafal” gesetzt. Wir fügen die Größe auf ähnliche Weise hinzu, obwohl wir hier eine präzisere Zahl verwenden können, da diese durch eine Float-Variable beschrieben wird. Der bei weitem komplexeste Prozess ist die Namensvergabe selbst. Obwohl wir wissen, dass der Name eine Zeichenfolge ist, ist dies für den Mikrocontroller nicht so offensichtlich und wir können nicht einfach z.B. “Kuba.name = ”Kuba”;” verwenden. Um einen Namen hinzuzufügen, verwenden wir den Befehl snprintf, der die entsprechende Zeichenfolge erstellt. Diese Funktion benötigt drei Parameter. Der erste ist der Ort, an dem der generierte Puffer abgelegt werden soll. In unserem Fall erwarten wir, dass er in einer strukturierten Variablen gespeichert wird, die dem betreffenden Objekt entspricht. Der zweite Parameter ist die Größe des Speicherplatzes. Wir könnten sie dauerhaft als zwanzig schreiben, da dies die Größe von char-Arrays ist, aber es ist besser, hier einen Befehl zu verwenden, der dies automatisch für uns erledigt. So funktioniert sizeof und gibt die Größe des in Klammern angegebenen Elements zurück. Das letzte Argument von snprintf ist die Zeichenfolge, die wir an die zuvor angegebene Stelle setzen. Es handelt sich dabei einfach um die Namen Rafal und Kuba, die als Zeichenfolgen geschrieben werden.

In einer unendlichen while-Schleife verwenden wir das inzwischen bekannte printf und senden in strukturierten Variablen gespeicherte Informationen in jedem Sekundenintervall an den Monitor der seriellen Schnittstelle.

Überwachung der seriellen Schnittstelle nach dem Programmstart.

Nach dem Hochladen des Codes auf den Raspberry Pi Pico im seriellen Monitor sollten wir die Beschreibungen der im Code erstellten Personen sehen.

Eine Kleinigkeit ist bei diesem Projekt erwähnenswert. Die Struktur, die wir erstellt haben, ist nur eine, während die Objekte zwei sind und sich sozusagen auf dieselben Variablen beziehen, und es spricht nichts dagegen, noch mehr Objekte hinzuzufügen. Das ist die Magie von Strukturen, die in C manchmal als Pseudo-Objektkonstrukte bezeichnet werden. Das liegt daran, dass die Arbeit mit ihnen ein wenig an die Programmierung erinnert, die aus objektorientierten Sprachen wie C++ oder Python bekannt ist. Die Struktur ermöglicht es, bestimmte Daten in etwas anspruchsvollere Pakete zu gruppieren, was recht nützlich sein kann. Stellen Sie sich vor, Sie arbeiten mit einem beliebigen Sensor (dazu mehr im nächsten Artikel), dessen Handhabung auf Variablen oder Konstanten basieren könnte. Das Programm würde gut funktionieren, aber es könnte schwierig sein, jegliche Arten von Änderungen vorzunehmen. Es ist viel besser, einen solchen Sensor zu einem Objekt mit eigenen Attributen zu machen. Dann sieht der Code viel besser aus, und auch die Kommunikation selbst wird überdurchschnittlich gut realisiert. Für den Moment reicht jedoch das Grundwissen über die Erstellung und Verwendung der Strukturen aus; wir werden uns in Zukunft mit komplexeren Beispielen beschäftigen.

Übermittlung von Daten an eine Funktion über einen Indikator

Ein Thema, das wir bereits angesprochen haben, sind Indikatoren. Bisher haben wir einfache Beispiele entwickelt, in denen wir die Adressen der im Speicher des Mikrocontrollers gespeicherten Variablen lesen. Nun ist es jedoch an der Zeit, sich ein Programm anzusehen, das die Essenz dieser Elemente der Sprache C tatsächlich nutzt, nämlich die Übermittlung von Daten an eine Funktion über einen Indikator. Auch wenn es sich etwas kompliziert anhört, ist das Verfahren überhaupt nicht übermäßig kompliziert und bietet einen großen Vorteil.

Für dieses Beispiel habe ich ein weiteres Projekt mit dem Namen increase_variable erstellt, in dem wir eine Funktion schreiben werden, die die Variable bei jedem Aufruf um eins erhöht. Dies ist eine einfache Aufgabe, da die Erhöhung des Wertes um eins bereits aus dem vorherigen Material bekannt ist, aber dieses Mal werden wir es auf eine etwas anspruchsvollere Weise realisieren. Lassen Sie uns also direkt zum Code kommen.

				
					#include 
#include "pico/stdlib.h"

uint8_t number = 0;

// A function that increases the value of the indicated variable by 1
void increase_variable(uint8_t *num) {
    (*num)++;
}

int main() {
    stdio_init_all();

    while (true) {
        
        printf("Before incrementation: %d\n", number);
        increase_variable(&number);
        printf("After inctementation: %d\n", number);
        sleep_ms(500);
    }

}

				
			

Das Programm beginnt wie üblich mit der Einbindung von Bibliotheken und der Erstellung der Variablen “number”, deren Anfangswert Null ist und die wir später im Code ändern wollen. Ich habe außerdem eine Funktion increase_variable vom Typ void erstellt. Auch sie wird uns keinen Wert zurückgeben, aber in den Argumenten erwartet sie einen Indikator auf Daten vom Typ uint8_t, die wir im Folgenden als “num” bezeichnen werden. Die Funktion führt tatsächlich nur eine Anweisung aus, um den Wert unter dem im Argument angegebenen Zeiger zu erhöhen. Wir können also davon ausgehen, dass beim Aufruf der Funktion die Daten, auf die wir einen Zeiger übergeben, um eins erhöht werden.

Der Hauptteil des Programms wurde in eine while-Schleife eingefügt. Wir zeigen den aktuellen Zustand der Variablen number an, rufen dann die Funktion increase_variable auf und geben dank des Adressenextraktionsoperators im Argument den Speicherplatz an, an dem number gespeichert wurde. Sobald dieser Vorgang abgeschlossen ist, sendet das Programm erneut den in unserer Variablen gespeicherten Wert an den seriellen Monitor.

Zahlenwerte, die im Monitor der seriellen Schnittstelle sichtbar sind.

Nach der Ausführung des Programms sollte der Computerbildschirm aufeinanderfolgende Werte der variablen Zahl anzeigen, die sich jede halbe Sekunde um eins erhöhen. Mit diesem einfachen Programm haben wir die Übergabe von Daten an eine Funktion über einen Zeiger implementiert. Das mag wie nichts Besonderes aussehen, aber der größte Vorteil dieser Lösung ist etwas, das wir nicht sehen können.

Lassen Sie uns überlegen, was passieren würde, wenn wir uns entschließen würden, eine gewöhnliche Variable in das Argument einer Funktion zu setzen. Wenn in diesem Fall eine Variable an increase_variable übergeben wird, wird eine Kopie der Variable erzeugt, an der die Inkrementierungsoperation durchgeführt wird. Und nur aus der Kopie können wir den um eins erhöhten Wert entnehmen. Ich denke, Sie wissen bereits, worum es hier geht. In diesem Fall erstellen wir völlig unnötigerweise eine zusätzliche Variable für uns, die wir nicht sehen können, die aber Platz im Speicher beansprucht. Mit dem Zeiger können wir dies vermeiden, denn wir geben nur die Adresse an, so dass die gesamte Operation mit dem ursprünglichen Wert durchgeführt wird. Diese Lösung hat eine Reihe von Vorteilen. Zunächst einmal können wir die Werte der Variablen direkt manipulieren, was auch die Effizienz des Codes erhöht. Außerdem ist eine solche Lösung in Zukunft sicherer, denn wenn wir unsere eigenen Bibliotheken schreiben, die von anderen Benutzern verwendet werden sollen, sind wir immun gegen die Eingabe falscher Daten durch diese Benutzer. Unser Code erwartet nur eine Adresse, wir führen unsere eigenen Operationen durch und kümmern uns nicht darum, wie sich dies auf den Code Dritter auswirkt, wir waschen sozusagen unsere Hände in Unschuld. Ob der Benutzer den richtigen Indikator angibt, hängt allein von ihm ab. Allerdings muss ich auch erwähnen, dass in der Sprache C so etwas wie ein Typkonflikt, der entstehen kann, wenn Daten über einen Zeiger übergeben werden, durch das so genannte Casting gelöst wird, aber mit diesem Thema werden wir uns ein anderes Mal beschäftigen.

Entwurf eines Zustandsautomaten

Bisher haben wir unsere Programme vor allem entwickelt, um bestimmte Funktionen zu testen, RPI-Leitungen zu steuern, ADCs zu bedienen oder über USB zu kommunizieren. Wir haben diese Codes im Hinblick auf ihre Funktion, aber nicht unbedingt auf ihre Leistung als Ganzes analysiert, also ist es an der Zeit, dies zu ändern. In diesem Unterabschnitt werden wir ein etwas größeres Projekt vorbereiten, das auf dem bekannten Konzept des Zustandsautomaten in C basiert. Da drei LEDs an den Raspberry Pi Pico angeschlossen sind, können wir ein Programm vorbereiten, das eine Ampel simuliert. Neben dem klassischen Wechsel von Rot auf Grün und umgekehrt werden wir jedoch auch einen speziellen Servicemodus einführen, bei dem nur die gelbe LED blinkt. Für diese Übung habe ich ein weiteres Projekt mit dem Namen state_machine vorbereitet, aber bevor wir uns mit dem Code befassen, sollten wir uns das Konzept eines Zustandsautomaten genauer ansehen.

Grafische Darstellung der aufeinanderfolgenden Ampelzustände.

Gemäß der definition können wir einen Zustandsautomaten als ein mathematisches Berechnungsmodell definieren, das ein System durch eine endliche Anzahl von Zuständen, Übergängen zwischen ihnen und den durch diese Übergänge ausgelösten Aktionen darstellt. Das klingt etwas kompliziert und es ist viel besser zu sagen, dass ein Zustandsautomat einfach ein Modell ist, in der Regel ein grafisches, das Schritt für Schritt den Betrieb eines bestimmten Programms oder Geräts beschreibt. Mit einem solchen Modell lässt sich auch der Betrieb einer Straßenverkehrsampel beschreiben. Zunächst ist das rote Licht aktiv, dann wechselt es zu Gelb, so dass nach einer Weile das grüne Signal aktiviert wird, während der letzte Zustand vor der Rückkehr zu Rot das gelbe Licht ist (dies ist ein dreiphasiges Ampelsystem, das unter anderem aus den USA und Frankreich bekannt ist; in Polen wird ein vierphasiges Modell verwendet). Mit einer solchen Beschreibung ist es viel einfacher, ein Programm zu erstellen, das dieser Aufgabe gerecht wird. Natürlich könnten wir einen einfachen Code schreiben, der die entsprechenden LEDs, die der oben erwähnten Signalleuchte entsprechen, eine nach der anderen aktiviert, aber in diesem Projekt werden wir die viel interessantere Funktion switch…case verwenden. Außerdem werden wir der Einfachheit halber versuchen, einen speziellen Modus vorzubereiten, bei dem es dem potenziellen Verkehrsteilnehmer überlassen bleibt, den vertikalen Schildern zu folgen. In diesem Fall blinkt die Ampel gelb.

Bevor wir uns jedoch dem Code zuwenden, müssen Sie eine weitere Neuheit der Sprache C kennenlernen, den so genannten Aufzählungstyp, der es uns ermöglicht, die Zustände zu beschreiben, in denen sich eine Signalleuchte befinden kann.

Deklaration eines Aufzählungstyps.

Genauso wie wir Variablen in Programmen platzieren, müssen wir auch die Zustände, in denen sich eine Ampel befinden kann, irgendwie benennen und definieren. Am einfachsten ist es, sich auf die Farbe zu beziehen, die gerade aktiv ist. Wenn eine grüne LED leuchtet, bezeichnen wir diesen Zustand als grün, das Gleiche gilt für rotes Licht. Es ist erwähnenswert, dass das gelbe Licht in zwei Fällen leuchtet, nämlich beim Übergang von Grün zu Rot oder umgekehrt, so dass wir auch solche Zustände beschreiben müssen. Darüber hinaus müssen wir auch an den speziellen Servicemodus denken.

Wir könnten die verschiedenen Zustände in verschiedenen Variablen speichern, aber es gibt einen viel besseren Weg, den Aufzählungstyp. Bis jetzt waren die Variablen, die wir verwendet haben, bestimmten Datentypen gewidmet, z.B. float speichert immer Fließkommazahlen. Enum, auch bekannt als Aufzählungstyp, ermöglicht es uns, eine eigene Variable zu erstellen, die unsere eigenen Werte speichert.

Wir deklarieren den Aufzählungstyp auf ähnliche Weise wie Strukturen. Wir beginnen mit den Schlüsselwörtern “typedef enum” und setzen in geschweiften Klammern unsere eigenen Wertetypen ein, die die durch die enum beschriebene Variable akzeptieren kann. Ganz am Ende fügen wir auch einen Namen hinzu, den wir im Rest des Codes verwenden werden.

Auf diese Weise haben wir sozusagen unseren eigenen Variablentyp namens traffic_light_state_t geschaffen, der den aktuellen Zustand unseres Zustandsautomaten speichert, mit anderen Worten, den Zustand, in dem sich die Ampel befindet. Dies ist z.B. STATE_GREEN, was einer grünen Signalleuchte entspricht.

In diesem Fall, wie auch bei der Struktur, habe ich mich entschieden, Ihnen sofort eine etwas aufwendigere Enum-Deklaration zu präsentieren, mit dem Wort typedef am Anfang. Die Motivation war hier identisch mit der vorhergehenden, und daher wird das enum-Präfix im restlichen Code nicht benötigt.

				
					#include "pico/stdlib.h"

#define GREEN_LED 0
#define YELLOW_LED 1
#define RED_LED 2
#define BUTTON_PIN 16

// States of the state machine for traffic lights
typedef enum {
    STATE_GREEN,
    STATE_YELLOW_TO_RED,
    STATE_RED,
    STATE_YELLOW_TO_GREEN,
    STATE_SERVICE
} traffic_light_state_t;

// Function to set the status of the LED
void set_traffic_light(traffic_light_state_t state) {
    gpio_put(GREEN_LED, state == STATE_GREEN);
    gpio_put(YELLOW_LED, state == STATE_YELLOW_TO_RED || state == STATE_YELLOW_TO_GREEN);
    gpio_put(RED_LED, state == STATE_RED);
}

int main() {
    stdio_init_all();

    gpio_init(GREEN_LED);
    gpio_set_dir(GREEN_LED, GPIO_OUT);

    gpio_init(YELLOW_LED);
    gpio_set_dir(YELLOW_LED, GPIO_OUT);

    gpio_init(RED_LED);
    gpio_set_dir(RED_LED, GPIO_OUT);

    gpio_init(BUTTON_PIN);
    gpio_set_dir(BUTTON_PIN, GPIO_IN);
    gpio_pull_up(BUTTON_PIN);  // Włączenie wewnętrznego rezystora podciągającego

    // Initial signaling status
    traffic_light_state_t state = STATE_GREEN;
    bool service_mode = false;

    while (true) {
        // Checking the status of the button
            if (gpio_get(BUTTON_PIN) == 0) {
                service_mode = !service_mode;
                while (gpio_get(BUTTON_PIN) == 0);  // Waiting for the button to be released

                if (service_mode) {
                    state = STATE_SERVICE;
                } else {
                    state = STATE_GREEN; // Return to normal state after service mode
                }
        }

        
        switch (state) {
            case STATE_GREEN:
                set_traffic_light(STATE_GREEN);
                sleep_ms(5000); // Zielone światło świeci przez 5 sekund
                if (!service_mode) state = STATE_YELLOW_TO_RED;
                break;

            case STATE_YELLOW_TO_RED:
                set_traffic_light(STATE_YELLOW_TO_RED);
                sleep_ms(2000); // Yellow light illuminates for 2 seconds
                if (!service_mode) state = STATE_RED;
                break;

            case STATE_RED:
                set_traffic_light(STATE_RED);
                sleep_ms(5000); // Red light illuminates for 5 seconds
                if (!service_mode) state = STATE_YELLOW_TO_GREEN;
                break;

            case STATE_YELLOW_TO_GREEN:
                set_traffic_light(STATE_YELLOW_TO_GREEN);
                sleep_ms(2000); // Yellow light illuminates for 2 seconds
                if (!service_mode) state = STATE_GREEN;
                break;

            case STATE_SERVICE:
                gpio_put(GREEN_LED, 0);
                gpio_put(RED_LED, 0);
                // Blinking yellow light
                gpio_put(YELLOW_LED, 1);
                sleep_ms(500);
                gpio_put(YELLOW_LED, 0);
                sleep_ms(500);
                break;
        }
    }

}

				
			

Wie üblich beginnen wir das Programm, indem wir die Bibliothek einbinden und die Pins definieren, an denen die LEDs und die Taste angeschlossen sind. Im nächsten Schritt wird eine Aufzählungstyp-Deklaration platziert, die ich oben beschrieben habe, sowie eine Funktion, die den entsprechenden LED-Status setzt, aber dazu kommen wir gleich.

Zu Beginn der Hauptfunktion haben wir die von uns verwendeten Raspberry Pi Pico-Kabel initialisiert und den Anfangszustand unserer Signalisierung festgelegt. Hier erstellen wir zwei Variablen state und service_mode. Der erste ist ein Wert unseres speziellen Typs, so dass wir ihm nur ausgewählte Zustände zuweisen können. Der Standardwert ist STATE_GREEN, also das aktive grüne Licht. Wir werden eine Variable vom Typ bool mit der Bezeichnung service_mode verwenden, um den Service-Modus für die Signalisierung zu aktivieren. Ihr Standardwert ist false, da es keinen Grund gibt, diesen Zustand gleich zu Beginn des Programms zu aktivieren.

Wir werden den Betrieb der Ampel in einer while-Schleife beschreiben. Darin muss es einen Tastendienst geben, der den Service-Modus aktiviert, d.h. den Wert des Zustands auf STATE_SERVICE ändert, und eine Abhängigkeit, um die nachfolgenden Zustände der Ampel zu ändern und entsprechend zu reagieren. Nehmen wir zunächst den Tastendienst, der mit einem kurzen Stück Code am Anfang einer unendlichen while-Schleife implementiert wird. Ganz am Anfang müssen wir den Zustand des Knopfes mit der Funktion gpio_get überprüfen; wenn der gelesene Wert Null ist, d.h. der Knopf ist gedrückt und der RPI-Pin ist mit Masse kurzgeschlossen, können wir den Wert der Variablen service_mode in das Gegenteil ändern. Hier verwenden wir den Operator “!”, der der Variablen den entgegengesetzten Zustand zuweist. Im nächsten Schritt müssen wir darauf warten, dass der Knopf losgelassen wird. Hier verwenden wir eine clevere Konstruktion, die auf der while-Schleife basiert. Wir überprüfen den Zustand der Taste ein zweites Mal, und wenn die Taste immer noch gedrückt ist, fallen wir in die Schleife. Innerhalb der Schleife weisen wir mit einer bedingten Funktion, die von der Variable service_mode abhängt, state den entsprechenden Zustand zu. Wenn die Variable eine logische Variable ist, aktivieren wir STATE_SERVICE, andernfalls aktivieren wir den Standardzustand des grünen Lichts. Diese Schleife ist so lange aktiv, bis die Taste losgelassen wird; danach befinden wir uns immer im Service- oder Standardmodus.

Das Prinzip der Verwendung der Funktion switch...case.

Die Funktion, die tatsächlich die Funktionsweise unserer Signalisierung definiert, ist die bereits erwähnte switch…case. Dies ist eine interessante Konstruktion, die wir in gewisser Weise als bedingt bezeichnen könnten, abhängig von einer einzigen Variable. Nach dem Schlüsselwort “switch” platzieren wir eine Variable, von der der als nächstes auszuführende Code abhängt. In unserem Fall wird dies ‘state’ sein, denn hier wird der aktuelle Status der Ampel gespeichert. Im Folgenden wird auf jeden möglichen Wert von “state” reagiert. Standardmäßig wird der unten beschriebene Codeschnipsel case STATE_GREEN beim Start des Programms ausgeführt. In diesem Zustand rufen wir die Funktion set_traffic_light auf und setzen STATE_GREEN in ihr Argument. Wenn wir zum Anfang des Programms zurückgehen, wo die Funktion beschrieben wird, stellen wir fest, dass die Aktivierung der LED genau von dem im Argument übergebenen Wert abhängt. Wenn STATE_GREEN vorhanden ist, ist die grüne LED aktiviert. Nach dem Aufruf der Funktion, die den entsprechenden Status am RPI-Pin einstellt, wartet das Programm fünf Sekunden lang und prüft dann, ob der Servicemodus aktiviert wurde. Falls nicht, wird dem Statuswert der Wert STATE_YELLOW_TO_RED zugewiesen, da dies der Status ist, in dem sich die Signalisierung jetzt befinden sollte. Beachten Sie, dass ich hier eine interessante Konstruktion verwendet habe, bei der der if-Operator von geschweiften Klammern befreit wurde. Wenn es sich bei dem in der Bedingung ausgeführten Code nur um eine einzige Funktion handelt, können wir sie in derselben Zeile wie die if-Anweisung unterbringen, was unser Programm etwas verkürzt und seine Lesbarkeit verbessert. Wenn dieser Code ausgeführt wird, wird die Funktion switch…case durch das Schlüsselwort ‘break’ unterbrochen und das Programm kehrt an den Anfang der while-Schleife zurück.

Hier wird der Zustand der Taste erneut überprüft und wenn die Taste nicht gedrückt wurde, geht das Programm zu switch…case über, aber dieses Mal führt der Mikrocontroller den Code unter dem case-Wert STATE_YELLOW_TO_RED aus, je nach dem Zustand der State-Variablen, der sich während des vorherigen Schleifendurchlaufs geändert hat. Die Anweisungen hier sehen analog zu den vorherigen aus und wenn die aktive LED wechselt, wird ‘state’ ein anderer Zustand zugewiesen, so dass der mit STATE_RED verbundene Code beim nächsten Schleifendurchlauf ausgeführt werden kann. So funktioniert die Funktion switch…case, mit der Sie auf verschiedene Zustände derselben Variablen reagieren können.

Die letzte switch…case-Bedingung beschreibt die Antwort auf STATE_SERVICE. Wenn die Taste gedrückt wird, ist dies der Wert, der der Statusvariablen zugewiesen wird und die gelbe LED blinkt dann.

Nachdem wir den Code ausgeführt haben, können wir unsere Miniatur-Ampel in Betrieb sehen. Der erste Zustand ist ein grünes Licht, das nacheinander in gelb und rot übergeht, wie es auch im wirklichen Leben der Fall ist. Wenn Sie die Taste einen Moment lang gedrückt halten, wird der Service-Modus aktiviert, in dem die Leuchte gelb blinkt. Wenn Sie die Taste erneut drücken, kehren Sie in den Standardmodus zurück.

Ich weiß, dass das Projekt des Zustandsautomaten etwas kompliziert erscheinen kann. Wenn Sie sich also nicht 100-prozentig sicher sind, sollten Sie es noch einmal durchgehen und selbst experimentieren, indem Sie bestimmte Werte und Zustände ändern und dann sehen, wie das Programm reagiert. Denken Sie daran, dass alles von der Statusvariable abhängt. Sie definiert den aktuellen Status der Signalleuchte und den nächsten Status. Und auf dieser Grundlage funktioniert die Funktion switch…case, die durch den Aufruf des Befehls set_traffic_light die LEDs entsprechend ausschaltet.

Ein paar Worte zum Schluss...

Einer der definitiv größeren Artikel in dieser Serie liegt hinter uns. Wir haben heute eine Menge über neue Dinge gelernt – Arrays, Strukturen, Zeigerübergabe und wir haben auch ein Ampelprojekt auf der Grundlage eines Zustandsautomaten gestartet. Nach zehn Teilen verfügen wir bereits über ein solides Fundament der Sprache C, das wir mit realen Beispielen getestet haben. Im nächsten Artikel werden wir also echte Experimente starten und einen digitalen Lichtsensor an unseren Raspberry anschließen.

Quellen:

  • https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf
  • https://datasheets.raspberrypi.com/picow/pico-w-datasheet.pdf
  • https://www.raspberrypi.com/products/rp2040/
  • https://www.raspberrypi.com/documentation/microcontrollers/raspberry-pi-pico.html

Wie hilfreich war dieser Beitrag?

Klicke auf die Sterne um zu bewerten!

Durchschnittliche Bewertung 5 / 5. Stimmenzahl: 4

Bisher keine Bewertungen! Sei der Erste, der diesen Beitrag bewertet.

Teilen:

Picture of Rafał Bartoszak

Rafał Bartoszak

Ein mit Botland kooperierender Elektroniker, der sein Wissen im Internet teilt. Enthusiast für digitale Systeme, programmierbare Schaltungen und Mikroelektronik. Leidenschaftlich für Geschichte, mit besonderem Schwerpunkt auf dem 20. Jahrhundert.

Siehe mehr:

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Aus Sicherheitsgründen ist die Verwendung des reCAPTCHA-Dienstes von Google erforderlich, der den Google-Datenschutzbestimmungen und Nutzungsbedingungen unterliegt..