nodecum - lernen

Sprachauswahl

Blinki

Blinky ist das “Hallo Welt” Beispiel Programm der Embedded Welt. Die meisten Entwicklungsboards haben nicht unbedingt eine Anzeige um darauf etwas darzustellen und eine solche anzusteuern ist ja auch schon recht komplex. Viel einfacher und oft vorhanden ist hingegen eine Leuchtdiode - und diese kann man blinken lassen.

Programm Dateien

Wir schauen uns die Dateien an welche im Verzeichniss <prog/blinky> zu finden sind.

prog/blinky
├── CMakeLists.txt
├── prj.conf
└── src
    └── main.c

CMakeLists.txt

1cmake_minimum_required(VERSION 3.20.0)
2find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
3project(blinky)
4target_sources(app PRIVATE src/main.c)

Diese Datei, welche vom cmake Build System gelesen wird beschreibt den Namen des Projektes und wo sich die Quellen des Projektes, hier src/main.c befinden.

prj.conf

1CONFIG_GPIO=y

Die Projektkonfigurationsdatei gibt an, welche Komponenten des Kernels benötigt werden. Diese werden dann mit in die Firmware eingebunden. Hier benötigen wir das GPIO - General Purpose In/Out Subsystem.

main.c

main.c ist die Hauptdatei unseres Programmes, welche in der Sprache C geschrieben ist.

1/*
2 * Copyright (c) 2016 Intel Corporation
3 *
4 * SPDX-License-Identifier: Apache-2.0
5 */

Dies ist der Urheberrechtshinweis. In C beginnt der Schrägstrich und der Stern /* einen Kommentar, welcher durch ein entsprechenden Schrägstrich und Stern */ wieder geschlossen wird. Diese Art von Kommentar kann sich über mehrere Zeilen erstrecken, während ein // Doppelschrägstrich Platz für Kommentare bis zum Ende der Zeile gibt.

1#include <zephyr.h>
2#include <drivers/gpio.h>

Alle Schlüsselwörter in C, die mit # beginnen, sind Präprozessor Direktiven. #include <file.h> lädt eine Header-Datei, welche die Schnittstelle von Deklarationen enthält die wir verwenden möchten. Wenn < und > zur Begrenzung des File Namens verwendet werden, dann werden die dem Compiler übergebenen Such Pfade verwendet um die Datei zu finden. Wird die Datei im Gegensatz dazu mit " umgeben, dann ist der Pfad relativ zu der Datei, welche die Include - Direktive enhält.

1/* 1000 msec = 1 sec */
2#define SLEEP_TIME_MS   1000
3
4/* The devicetree node identifier for the "led0" alias. */
5#define LED0_NODE DT_ALIAS(led0)

Auch hier haben wir eine Präprozessordirektive. #define PLATZHALTER wert erzeugt ein Makro, welches alle Vorkommen von PLATZHALTER durch den angegebenen wert ersetzt. Normalerweise werden die Platzhalter mit Großbuchstaben geschrieben, dies ist jedoch nicht zwingend erforderlich. Es wird so oft gemacht um auf diese Weise die Makros von Funktionen und Variablen zu unterscheiden, welche oft mit Kleinbuchstaben geschrieben werden.

Es können auch Makros definiert werden, welche Argumente haben. Hier verwenden wir ein solches Makro DT_ALIAS mit dem Argument led0 als Wert für unser Makro LED0_NODE. led0 ist ein Alias, der für das Board was wir verwenden angibt, an welchem Port und Pin die LED 0 (LED - light emiting diode - Leuchtdiode) angeschlossen ist.

1/*
2 * A build error on this line means your board is unsupported.
3 * See the sample documentation for information on how to fix this.
4 */
5static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);

Diese C-Deklaration deklariert einen Bezeichner led0 für einen fixen (static) Bereich des “nur lese” (read only - const) Speicherbereiches, der strukturierte Daten (struct) der Form gpio_dt_spec aufnimmt und mit dem Ergebnis des Makroaufrufs GPIO_DT_SPEC_GET(LED0_NODE, gpios) initialisiert wird.

1void main(void)
2{

Hier definieren wir unsere Hauptroutine des Programms, void main(void) ist der so genannte Einstiegspunkt, der aufgerufen wird, um die Anwendung zu starten. void bezeichnet einen Nullwert, daher wird diese Funktion keinen Wert zurückgeben und auch kein Argument haben. Das Schreiben von void main() ist gleichbedeutend mit der vorherigen Form mit einem void Argument. Der Körper der Funktion wird eingeschlossen in die geschweiften Klammern {}.

1int ret;

Diese Zeile besagt, dass wir Speicher für die Aufnahme eines ganzzahligen (Integer - int ) Wertes benötigen, auf den mit dem Namen ret zugegriffen werden kann. Dies ist ein uninitialisierter Speicherplatz, d.h. es wurde bisher kein bestimmter Wert dorthin geschrieben.

1if (!device_is_ready(led.port)) {
2  return;
3}

Hier haben wir eine Bedingungsklausel namens if. Wenn die Bedingung in der Klammer wahr ist, dann werden die Anweisungen in den geschweiften Klammern ausgeführt. Bedingte Klauseln werden auf Seite 79 in “Die Programmiersprache C” beschrieben. Zephyr folgt mindestens der C99-Spezifikation in welcher die Unterstützung von booleschen Werten mit dem Typ bool in die Sprache aufgenommen hat. (Siehe C-Sprachunterstützung) Das Ausrufezeichen ! ist ein Negationsoperator welcher bewirkt dass, wenn die Funktion device_is_ready false zurückgibt, was bedeutet, dass das Gerät nicht einsatzbereit ist, die direkt folgende Anweisung (hier der Block { return; } ausgeführt wird. Siehe hier für die Beschreibung der device_is_ready Funktion. Mit dem Aufruf von return wird unsere main Funktion verlassen, also unser Programm beendet. Das heißt, falls das Gerät nicht korrekt initialisiert werden konnte, wird unser Programm hier beendet.

1ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
2if (ret < 0) {
3  return;
4}

Aufruf der Funktion gpio_pin_configure_dt und abspeichern des Integer-Ergebnisses in der Variablen ret. Ein Blick in die Dokumentation von gpio_pin_configure_dt in GPIO Driver APIs zeigt, dass die Funktion bei Erfolg Null und bei Mißerfolg negative Werte zurückgibt, welche die aufgetretenen Fehlerbedingungen beschreiben. GPIO_OUTPUT_ACTIVE konfiguriert den angegebenen GPIO-Pin als Ausgang und initialisiert ihn auf eine logische 1. Auch hier wird das Programm durch den Aufruf von return beendet, falls ein Fehler aufgetreten sein sollte.

1while (1) {
2  ret = gpio_pin_toggle_dt(&led);
3  if (ret < 0) {
4    return;
5  }
6  k_msleep(SLEEP_TIME_MS);
7}

Das Schlüsselwort while beschreibt eine ‘while’-Schleife, solange die Bedingung in den geschweiften Klammern ( 1 ) wahr ist ( d.h. nicht Null ist ), wird die folgende Anweisung, { ... } ausgeführt werden. Hier ist die Bedingung immer wahr, also auf den ersten Blick eine nie endende Schleife programmiert worden. Aber wenn wir weiter schauen dann sehen wir, dass wenn gpio_pin_toggle_dt eine Fehlerbedingung zurückgibt (ret < 0 ), dann wird return aufgerufen, was zum die Beenden der main Funktion führt. Der Aufruf von gpio_pin_toggle_dt verändert den Zustand des Pins zwischen high und low – die LED wird ein oder ausgeschaltet.

k_msleep versetzt den aktuellen Thread für die in Millisekunden angegebene Zeit in den Schlaf. Das bedeutet, dass die Ausführung für die angegebene Zeit angehalten wird. Danach wird der Pin wieder umgeschaltet werden und so weiter.

1}

Die abschließende Klammer den Körper der main Funktionsdeklaration.

Hier ist das vollständige Programms:

 1/*
 2 * Copyright (c) 2016 Intel Corporation
 3 *
 4 * SPDX-License-Identifier: Apache-2.0
 5 */
 6#include <zephyr.h>
 7#include <drivers/gpio.h>
 8/* 1000 msec = 1 sec */
 9#define SLEEP_TIME_MS   1000
10
11/* The devicetree node identifier for the "led0" alias. */
12#define LED0_NODE DT_ALIAS(led0)
13/*
14 * A build error on this line means your board is unsupported.
15 * See the sample documentation for information on how to fix this.
16 */
17static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
18void main(void)
19{
20    int ret;
21    if (!device_is_ready(led.port)) {
22      return;
23    }
24    ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
25    if (ret < 0) {
26      return;
27    }
28    while (1) {
29      ret = gpio_pin_toggle_dt(&led);
30      if (ret < 0) {
31	return;
32      }
33      k_msleep(SLEEP_TIME_MS);
34    }
35}