FreshRSS

🔒
❌ Über FreshRSS
Es gibt neue verfügbare Artikel. Klicken Sie, um die Seite zu aktualisieren.
Vor vorgesternIhre RSS-Feeds

In a Nutshell - Arduino Nano RP2040 Connect

Von heise online

In a Nutshell - Arduino Nano RP2040 Connect

Der Pragmatische Architekt Michael Stal

Als der Raspberry Pi Pico Anfang des Jahres das Licht der Welt erblickte, hat Arduino ein eigenes Board auf Basis des RP2040-Microcontrollers angekündigt. Nach wenigen Monaten ist es nun soweit. Der Arduino Nano RP2040 Connect lässt sich endlich käuflich erwerben. In diesem Beitrag geht es um erste Erfahrungen mit dem neuen Board.

Der Preis ist heiß

Das neue Board Arduino Nano RP2040 Connect
Das neue Board Arduino Nano RP2040 Connect

Billig ist der Nano RP2040 Connect nicht zu haben, zumal er inklusive angelöteter Pin-Leisten mit über 20 Euro zu Buche schlägt. Allerdings erscheint er gegenüber anderen Arduino Nano-Boards wie denen aus der Nano-33-Familie ausgesprochen günstig. Im Vergleich zu den RP2040-Boards von SparkFun, Adafruit oder der Raspberry Pi Foundation wartet das Arduino-Board mit vielen Extras auf, insbesondere der herbeigesehnten WiFi- und Bluetooth-Fähigkeit.

Was verbirgt sich unter der Motorhaube beziehungsweise was sind die wesentlichen Merkmale des Nano RP2040 Connect. Hier zunächst eine Übersicht mit den Eigenschaften und Ingredienzen:

Das neue Arduino Board bietet unter seiner Motorhaube einiges.
Das neue Arduino Board bietet unter seiner Motorhaube einiges. (Bild: Arduino)
  • Das Herzstück des Boards bildet der RP2040/Microcontroller mit seinen zwei Cortex M0+-Kernen, deren Frequenz bei 133 MHz liegt.
  • 264 KB SRAM befinden sich auf dem RP2040-Chip, zu denen sich 16 MB (über einen QSPI-Bus verbundenes) Flash-RAM gesellen.
  • Um für Sicherheit zu sorgen, enthält die Hardware einen kryptografischen Koprozessor des Typs Microchip ATECC608A.
  • Der Buck-Stepdown-Konverter MP2322 regelt die 4.5V bis 21V Eingangsspannung auf die vom Board benötigten 3.3V herunter.
  • Auf dem Nano RP2040 Connect lassen sich mehrere Tiefschlafmodi einsetzen, um den Stromverbrauch zu minimieren.
  • Wie sein Cousin Raspberry Pi Pico enthält auch der Nano RP2040 Connect zwei PIO-Blöcke (PIO = Programmable Input/Output) mit je vier Zustandsmaschinen für programmierbare Ein- und Ausgabe, ebenso wie einen DMA-Controller und zwei Oszillatoren (PLLs) für das Antreiben von System und USB. Natürlich darf auch der interne Temperatursensor nicht in dieser Aufzählung fehlen.
  • Apropos USB: Das Board bietet einen USB 1.1-Controller sowie einen Micro-USB-Anschluss.
  • Eine SWD-Schnittstelle erlaubt Hardware-Debugging.
  • Größenmäßig nimmt der Nano RP2040 nur 18 x 45 mm ein, ist daher kleiner als die meisten RP2040-Boards.

Nano-Formfaktor

Arduino
Die Pin-Belegung des Boards (Bild: Arduino)

Der Formfaktor des neuen Boards entspricht dem anderer Mitglieder der Nano-Familie, angefangen vom Ur-Nano bis hin zu neueren Boards wie Arduino Nano {33 BLE Sense | 33 BLE | 33 IoT | 33 Every}. Nebst Formfaktor stimmen die Nanos auch in der Belegung ihrer Ports überein.

Das Board stellt acht analoge Ports A0…A7 zur Verfügung, von denen allerdings A4 und A5 nur für I2C verwendet werden sollten. Für die Analog-Digital-Wandlung existieren 4 Kanäle mit 12 Bit Auflösung. Die Hälfte dieser Ports (d.h., alle bis auf A4…A7) unterstützt Pulsweitenmodulation (PWM). Wer A6 und A7 für die Verarbeitung analoger Signale einsetzen möchte, sollte wissen, dass diese Ports ausschließlich analoges Lesen erlauben.

Die analogen Ports lassen sich auch als digitale Ports verwenden, sodass insgesamt 22 digitale Anschlüsse zur Verfügung stehen, von denen die meisten PWM-fähig sind.

Weitere kleinere Einschränkungen:

  • Bei der Verwendung von A6 und A7 als digitale Ports ist nur Lesen möglich.
  • Der digitale Pin 3 lässt sich nicht mit INPUT_PULLUP konfigurieren.

Zum Anschluss externer Bus-Komponenten gibt es folgende Möglichkeiten:

  • SPI steht an den Ports D11 (CIPO, früher MOSI), D12 (COPI, früher MISO), D13 (SCK) und einem beliebigen Analog-Pin außer A6/A7 (CS; früher SS) zur Verfügung.
  • I2C: Wie oben bereits angedeutet, fungieren A6 und A7 als I2C-Ports SDA und SDL.
  • UART: Für einen UART-Anschluss gibt es analog zum Raspberry Pi Pico die Ports D0 (RX) und D1 (DX).
  • I2S: Das Board integriert den Bus für Audioströme.
Das Board in voller Pracht
Das Board in voller Pracht


Nach Hause telefonieren

Das Connect im Namen steht für den Chip u-blox NINA-W102, der auch schon auf anderen Arduino-Boards seine Dienste leistet und den ESP32 als Basis nutzt. Mit seiner Hilfe verfügt der Nano RP2040 Connect über WiFi und Bluetooth Low Energy 4.2. Wer die Arduino Cloud abonniert, darf sich darüber freuen, dass die Cloud bereits den Nano RP2040 Connect unterstützt.

Zum Zugriff auf WiFi-Funktionalität müssen Programmierer die entsprechende WiFiNINA-Bibliothek einbinden und in der Funktion setup() initialisieren:

#include <WiFiNINA.h>

void setup() {

WiFi.begin(ssid, pass);

}

In loop() befindet sich der Code für die eigentliche WiFi-Kommunikation:

void loop() {
….
server = “www.heise.de“;
port = 0x80;

if (client.connect(server, port)) {
client.println(
"GET /https://www.heise.de/suche/?q=developer HTTP/1.1");
client.println("Verbindung zu: www.heise.de");
client.println("Connection: close");
client.println();
}
}

Mittels der ArduinoBLE-Bibliothek lassen sich Bluetooth-Dienste anbieten oder konsumieren.

Sensorik

Zusätzlich integriert das Nano RP2040 Connect diverse Sensoren, unter anderem eine Messeinheit zur Trägheitsmessung (ST LSM6DSOXTR ) und ein MEMS-Mikrofon (ST MP34DT06JTR):

  • MEMS steht für Micro ElectroMechanical Systems. MEMS-Mikrofone zeichnen sich durch niedriges Rauschen, einen weiten Dynamikbereich, geringe Verzerrung und große Resilienz gegenüber akustischer Überlast aus, weshalb sie sich sehr gut für Spracherkennung eignen. Zum Integrieren des Mikrofons gibt es die PDM-Bibliothek, die der Board Manager der Arduino IDE (siehe unten) mitinstalliert.
  • Die LSM6DSOXTR von ST Microelectronics implementiert sogenannte Sensor-Fusion, weil er zwei Sensoren kombiniert, einen dreidimensionalen Beschleunigungsmesser und ein dreidimensionales Gyroskop (Kreiselkompass). Hinter den drei Dimensionen verbergen sich räumliche (x,y,z)-Koordinaten, für die der Sensor Beschleunigungen und Drehmomente ermittelt. Durch seinen ML-Kern (Maschine Learning Core) kann der LSM6DSOXTR auch fortgeschrittene Funktionalität anbieten, etwa die Erkennung von freiem Fall. Wollen Entwickler diesen Sensor nutzen, müssen sie die entsprechende LSM6DSOX-Bibliothek einbinden.

Entwicklungshelfer

Das Board lässt sich in beiden Arduino IDEs programmieren, der klassischen 1.8.x-IDE und der neuen 2.0.x-IDE. Da Nano RP2040 Connect den Arduino Mbed OS Nano Boards Core [1] benutzt, genügt zur Installation die Suche nach dieser Phrase im Board Manager der jeweils eingesetzten IDE. Weitere Informationen zur Installation finden sich hier [2].

Das Board in Aktion: es blinkt mal wieder
Das Board in Aktion: es blinkt mal wieder

Für alle Freunde der Online-Entwicklung: Auch der Arduino Web Editor steht für das neue Board bereit.

Das Board implementiert den gleichen Bootloader-Prozess wie der Raspberry Pi Pico, sodass sich der Arduino Nano RP2040 beim Hostsystem als USB-Massenspeicher ausgibt. Auf einem Mac erscheint das Board im Massenspeichermodus sogar als Raspberry Pi Pico. Durch ein schnelles doppeltes Betätigen der Reset-Taste lässt sich der Bootloader aktivieren, sollte einmal beim Hochladen etwas schiefgehen.

Der Nano Nano RP2040 lässt sich sowohl mit einer Arduino 1.8.x  IDE als auch mit der neuen Arduino 2.x-Entwicklungsumgebung programmieren.
Der Nano Nano RP2040 lässt sich sowohl mit einer Arduino 1.8.x IDE als auch mit der neuen Arduino 2.x-Entwicklungsumgebung programmieren.

Für den Fall, dass das Host-Betriebssystem den Nano nicht erkennt, gibt es einen kleinen Workaround: In diesem Fall können Entwickler bei angeschlossenem Board das REC- und das GND-Pin mit einem Verbindungsdraht (Jumper-Wire) zusammenschließen, anschließend die Reset-Taste betätigen, worauf sich der Arduino Nano RP2040 als USB-Speicher zu erkennen gibt, um danach einen Beispielssketch auf das Board hochzuladen.

Grundsätzlich dürften auch andere IDEs und Programmierplattformen demnächst den Arduino Nano RP2040 Connect unterstützen, darunter CircuitPython, MicroPython, Visual Studio Code und PlatformIO.

In einem Selbstversuch konnte ich zumindest validieren, dass sich der Arduino Nano RP2040 Connect mit der MicroPython-Firmware für den Raspberry Pi Pico bespielen lässt. Auch ein einfaches Programm ließ sich zum Ablauf bringen. Nähere Details folgen in zukünftigen Postings.

Angetestet: Dir MicroPython-Firmware für den Raspberry Pi Pico läuft auch auf dem Arduino
Angetestet: Dir MicroPython-Firmware für den Raspberry Pi Pico läuft auch auf dem Arduino

Fazit

Für detaillierte Aussagen ist es selbstverständlich noch zu früh. Trotzdem erscheint es als sicher, dass Arduino mit dem Nano RP2040 Connect ein großer Wurf gelungen ist. Das neue Board bietet das, was Anwender des Raspberry Pi Pico bislang vermisst haben: komplexere Sensoren, die Unterstützung durch das Arduino-Ökosystem, sowie vor allem WiFi- und Bluetooth-Funktionalität. Daher relativiert sich auch der etwas höhere Preis des Produktes deutlich. Vom Preis/Leistungsverhältnis her gesehen ist das neue Board sein Geld wert.

Das nur als kleiner Vorgeschmack. Zukünftige Blog-Postings widmen sich dem Newcomer detaillierter und intensiver.

Referenzen


URL dieses Artikels:
https://www.heise.de/-6051306

Links in diesem Artikel:
[1] https://github.com/arduino/ArduinoCore-mbed
[2] https://docs.arduino.cc/software/ide-v1/installing-mbed-os-nano-boards
[3] https://docs.arduino.cc/hardware/nano-rp2040-connect

Copyright © 2021 Heise Medien

Adblock test (Why?)

  • 21. Mai 2021 um 09:14

Sleepy Pico - ein Raspberry Pi Pico geht mit C/C++ schlafen

Von heise online

Sleepy Pico - ein Raspberry Pi Pico geht mit C/C++ schlafen

Der Pragmatische Architekt Michael Stal

Wer einen Microcontroller wie den RP2040 des Raspberry Pi Pico nutzt, interessiert sich früher oder später für dessen Energieverbrauch. Zum Beispiel bei einer Anwendung für mobile Messdatenerfassung oder Monitoring. Ohne Beachtung der Energiebilanz ist jede Batterie nach wenigen Stunden völlig erschöpft. Dieser Artikel erläutert deshalb, wie Entwickler höhere Energieeffizienz erreichen können.

Nimm zwei - die Schlafmodi

Laut Datenblatt gibt es beim Pico zwei mögliche Schlafmodi, den normalen zeitgesteuerten Schlafmodus (sleep mode) und den von externer Hardware abhängigen Schlafmodus (dormant mode).

Letztere Variante erfordert ein Aufwecken durch einen externen Trigger, der an einem der GPIO-Pins angeschlossen ist. Das könnte zum Beispiel ein Taster sein, ein PIR-Sensor, ein externer Taktgeber, eine NE555-Schaltung, oder ein anderer Microcontroller. Der Dormant-Modus erlaubt den geringstmöglichen Energieverbrauch, benötigt dafür aber einen externen „Wecker“. Während des Dormant-Modus sind alle internen Oszillatoren deaktiviert, ganz im Gegensatz zum Sleep-Modus.

Auf der anderen Seite ist der zeitgesteuerte Schlafmodus nicht ganz so energieeffizient, hat aber den Vorteil, dass der RP2040 in der Lage ist, sich selbst über die eingebaute Echtzeituhr RTC (Real-Time Clock) zu wecken. Der Schlafmodus hält zu diesem Zweck einen Oszillator aktiv, der die Echtzeituhr taktet.

Laut Spezifikation verbraucht der RP2040 bei 25°C im Mittel 0,8 mA im Dormant-Mode und 1.3 mA im Sleep-Mode. Das muss man im Verhältnis zum Normalverbrauch sehen. Unter Volllast können es durchaus 95 mA sein.

Gemäß der Formel P = U*I (P = Leistung, U = Spannung, I = Stromstärke) ergeben sich bei 3.3V Versorgungsspannung theoretische Werte von 313.5 mW (unter Volllast) bis zu sehr niedrigen 4.3 mW (im Sleep Mode) beziehungsweise 2.7 mW (im Dormant Mode).
Nähere Informationen dazu finden sich im Datenblatt des Raspberry Pi Pico [1].
Offensichtlich bieten die beiden Schlafmodi also deutliche Einsparmöglichkeiten. Doch wie lassen sich diese Potenziale programmatisch nutzen?

All you need is code

Der frei verfügbare und unter GPLv3-Lizenz stehende Quellcode für das Beispielsprogramm dieses Beitrags ist auf GitHub abgelegt. Sie finden ihn unter diesem Link [2].

Verwendete Programmiersprachen: C und C++.

C-Beispiele aus der Spielwiese

Für MicroPython hat die Raspberry Pi Foundation leider noch keine API veröffentlicht, mit der sich der Pico über die integrierte Echtzeituhr in Tiefschlaf versetzen ließe. Anders verhält sich die Situation auf der C/C++-Seite.

Der Pico Playground [3] bietet C-Beispiele zur Veranschaulichung, die den Einsatz dieser Modi illustrieren. Alle Beispiele des Playgrounds basieren auf den Pico-Extras [4], bei denen es sich größtenteils um noch unfertige oder unvollständig dokumentierte Bibliotheken und Beispielsanwendungen handelt. Daher sollte man neben dem Pico-Examples-Verzeichnis auch eines für Pico-Extras einrichten, um die dortigen Beispiele zu testen.

Überblick verschaffen

Wer gerne einen Einblick in die aktuellen Taktfrequenzen des Pico haben möchte, die für den Betrieb der verschiedenen Subsysteme verantwortlich sind, kann hierfür die API-Funktion frequency_count_khz() nutzen. Diese ermittelt die aktuellen Taktfrequenzen, etwa pll_usb für die Steuerung der USB-Komponenten oder den Takt des Ringoszillators, der die Echtzeituhr antreibt:

void measure_freqs(void) {
uint f_pll_sys = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_PLL_SYS_CLKSRC_PRIMARY);
uint f_pll_usb = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_PLL_USB_CLKSRC_PRIMARY);
uint f_rosc = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_ROSC_CLKSRC);
uint f_clk_sys = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_SYS);
uint f_clk_peri = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_PERI);
uint f_clk_usb = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_USB);
uint f_clk_adc = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_ADC);
uint f_clk_rtc = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_RTC);

printf("pll_sys = %dkHz\n", f_pll_sys);
printf("pll_usb = %dkHz\n", f_pll_usb);
printf("rosc = %dkHz\n", f_rosc);
printf("clk_sys = %dkHz\n", f_clk_sys);
printf("clk_peri = %dkHz\n", f_clk_peri);
printf("clk_usb = %dkHz\n", f_clk_usb);
printf("clk_adc = %dkHz\n", f_clk_adc);
printf("clk_rtc = %dkHz\n", f_clk_rtc);

uart_default_tx_wait_blocking(); // wait blocking for UART output
}

Ein erster Schritt zum reduzierten Energieverbrauch besteht darin, die System-Taktfrequenz zu reduzieren, wofür eine weitere SDK-Funktion existiert.

Der Aufruf von

set_sys_clock_khz(60000, true);

verändert die Systemfrequenz von 125 MHz auf 60 MHz. Mit Hilfe dieser Funktion ließe sich der RP2040 grundsätzlich auch übertakten, was dementsprechend zu höherem Energieverbrauch führen würde. Aber das ist eine andere Geschichte.

Hürdenlauf

Alles gut, möchte man meinen. Dummerweise legen die Schlafmodi die meisten Taktgeber schlafen. Das Programm kehrt also nach der Schlafphase nicht in den Zustand zurück, der vorher geherrscht hat. Würden Entwickler in dem Code nach Beenden des Schlafmodus eine Methode wie etwa sleep_ms() aufrufen, die zum Funktionieren einen der internen Taktgeber benötigt, bliebe das Programm an dieser Instruktion hängen. Folglich sollten Entwickler alle (benötigten) Oszillatoren nach dem Schlafmodus neu starten beziehungsweise die zugehörigen Register wieder auf die Werte einstellen, die sie vor der Schlafphase aufwiesen. Die Programmbeispiele auf Pico-Playground lassen das notwendige Sichern der Register übrigens unerwähnt.

Um das leisten zu können, muss ein Programm die Registerwerte vor jeder Schlafphase speichern. Das lässt sich durch folgenden Code bewerkstelligen:

// save current values for clocks
scb_orig = scb_hw->scr;
en0_orig = clocks_hw->sleep_en0;
en1_orig = clocks_hw->sleep_en1;

Für den zeitgesteuerten Schlafmodus ist die RTC (Real Time Clock) erforderlich, die mit Datum und Zeit belegt sein sollte:

// Initialize real-time-clock
rtc_init();
rtc_set_datetime(&now);

Schließlich ist der Sleep-Modus von der Echtzeituhr anhängig, die für die Erweckung aus dem Tiefschlaf zu einem vorgegebenen Zeitpunkt sorgen muss.

Natürlich existieren dazu komplementäre Anweisungen für die Zeit unmittelbar nach dem Aufwachen. Diese sorgen für die Inbetriebnahme der im Schlafmodus deaktivierten Komponenten. Der Aufruf von rosc_write() reaktiviert den Ringoszillator ROSC (Ring Oscillator), der dadurch das Regime auf dem Pico wieder übernehmen kann. Dazu gleich mehr. Die gesicherten Register werden ebenfalls wieder hergestellt:

// Re-enable Ring Oscillator control
rosc_write(&rosc_hw->ctrl, ROSC_CTRL_ENABLE_BITS);

// restore clock registers
scb_hw->scr = scb_orig;
clocks_hw->sleep_en0 = en0_orig;
clocks_hw->sleep_en1 = en1_orig;

Starten des Tiefschlafs

Die Taktung der Echtzeituhr kann im Tiefschlaf nicht der Ringoszillator leisten, der sich selbst schlafen legt. Stattdessen soll der Kristalloszillator XOSC das Kommando übernehmen. In Software geschieht dies über den Aufruf von

sleep_run_from_xosc()

Es gibt, wie oben erwähnt, zwei grundsätzliche Varianten des Tiefschlafs. Im Falle des Sleep-Modus gibt der Entwickler einen Weckzeitpunkt vor. Das geschieht mittels des Aufrufs von sleep_goto_sleep_until(). Beim Erreichen dieses Zeitpunkts ruft das System eine vom Programmierer zur Verfügung gestellte Callback-Funktion auf. Diese könnte wie folgt aussehen:

static void onWakeUp(void) {
// put wake up actions here
}

Die notwendigen Anweisungen für das Einschlafen des Raspberry Pi Pico gestalten sich also im Detail folgendermaßen.

  • Der Crystal Oscillator XOSC muss bekanntlich wach bleiben, weil er die Echtzeituhr antreibt.
  • Es muss die Festlegung eines Zeitpunkts (RTC_alarm) erfolgen, an dem die Echtzeituhr den Raspberry Pi Pico aufwecken soll.
  • In den Schlaf versetzen lässt sich der Microcontroller über den Aufruf von sleep_goto_sleep_until(…):

datetime_t RTC_alarm = {
.year = 2021,
.month = 05,
.day = 01,
.dotw = 6, // 0 is Sunday, so 5 is Friday
.hour = 00,
.min = 08,
.sec = 00
}

sleep_goto_sleep_until(&RTC_alarm, &onWakeUp);

Nach dem "Wiedererwecken" ruft das System die benutzerdefinierte Rückrufroutine onWakeUp() auf.

Dormant mode

Für die zweite Schlaf-Variante, den Dormant Modus, ist der entsprechenden Pico-SDK-Funktion sleep_goto_dormant_until_edge_high() ein GPIO-Pin zu übergeben. Im Code unten ist dies der WAKEUP_PIN. Sobald der Pico an diesem Pin eine aufsteigende Signalflanke bzw. ein High-Signal erkennt, erwacht er aus seinem Tiefschlaf. Wie eingangs erläutert, könnte z.B. ein Taster oder ein PIR-Sensor der Auslöser sein:

// Go to sleep until we see a high edge on WAKEUP_PIN
sleep_goto_dormant_until_edge_high(WAKEUP_PIN);

Alternativ gibt es die Methode sleep_goto_dormant_until_pin(), der man neben dem GPIO-Pin auch die boole'schen Variablen edge und active übergibt. Erstere spezifiziert, ob das Aufwachen nach einer steigenden (edge == true) oder fallenden Signalflanke (edge == false) erfolgen soll. Letztere, ob der Pin als Active HIGH (active == true) oder Active LOW (active == false) arbeitet.

Die im vorhergehenden Abschnitt gezeigten Aktionen für den Sleep mode bleiben bis auf die Aufrufe von rtc_init() und rtc_set_datetime() identisch. Auch die Rückrufmethode onWakeUp kann entfallen.

Sensoren und Aktuatoren

Es würde nur wenig nützen, wenn der Raspberry Pi Pico im Tiefschlaf weilt, die angeschlossenen Verbraucher wie Sensoren oder Aktuatoren aber viel Strom konsumieren. Eine Lösung könnte darin bestehen, die entsprechenden Komponenten über ein GPIO zu versorgen und dieses während des Tiefschlafs auf LOW zu setzen. Das hat mehrere Haken: Zum einen erlauben die GPIOs des Pico nur sehr beschränkte Stromstärken, die für viele Anwendungsfälle nicht genügen - alle GPIOs zusammen dürfen nicht mehr als 50 mA ziehen. Zum anderen benötigen einige Komponenten etwas Zeit für Start und Initialisierung beziehungsweise Kalibrierung.

Zum Glück integrieren komplexere Sensoren und Aktuatoren oft eigene Funktionalität, um sie auf Bedarf ebenfalls in den Tiefschlaf zu versetzen. Wie das funktioniert, hängt natürlich von den speziellen Eigenschaften dieser Bausteine ab.

Das KiSS-Prinzip

Da die beschriebenen Aktionen immer wieder neu geschrieben werden müssen, sobald eine energieeffiziente Schaltung mit dem Raspberry Pi Pico geplant ist, liegt die Idee nahe, ein entsprechendes Mini-Framework mit der notwendigen Funktionalität zu realisieren.

Zu diesem Zweck ist die Implementierung einer C++-Klasse empfehlenswert. Diese implementiert das Singleton-Entwurfsmuster. Während der Einsatz von Singletons in vielen Fällen eher kontraproduktiv erscheint, ist es bei der Klasse Sleep ausnahmsweise sinnvoll, zumal hier wirklich nur eine zentrale Instanz vorliegen soll beziehungsweise darf. Die nachfolgend dargestellte Klasse Sleep enthält Datenelemente zum Speichern und Restaurieren der Oszillator-Register vor beziehungsweise nach dem Schlafen, zudem die für den Dormant-Modus benötigte Information bezüglich des GPIO-Pins, an dem der Pico im Dormant-mode ein Weck-Signal registrieren soll, um seinen Tiefschlaf zu unterbrechen. An diesem GPIO-Eingang (wakeup_pin) lassen sich unterschiedliche Signalquellen anschließen.

Für die durch die RTC gesteuerte Schlafenszeit (Sleep-mode) benötigt die Klasse Information über Anfangs- und Alarmzeitpunkt (_init_time, _alarm_time).

Die globale Singleton-Instanz lässt sich entsprechend über die Methoden XXXconfigure() konfigurieren oder rekonfigurieren:

void Sleep::configureDormant(void (*setupfnc)(), void (*loopfnc)(), 
uint8_t WAKEUP_PIN, bool edge, bool active) {
_mode = Sleep:MODE::DORMANT;
_loop = loopfnc;
_setup = setupfnc;
_wakeup_pin = WAKEUP_PIN; _edge = edge; _active = active;
}

Dabei legt der Aufrufer den Modus durch die aufgerufene Konfigurationsmethode fest (configureDormant(), configureSleep(), configureNormal), unter dem die Anwendung ablaufen soll. Das entspricht den beschriebenen Schlafmodi, wobei Normal den Ablauf ohne Schlafenszeiten realisiert. Das ist zum Beispiel bei der Fehlersuche nützlich oder bei der Verbrauchsmessung der unterschiedlichen Modi. Die Methoden before_sleep(), start_sleep() und after_sleep() kapseln die weiter oben erläuterte Funktionalität zum Vorbereiten des Schlafs, Durchführen des Schlafs und Wiederaufsetzen nach der Schlafphase:

class Sleep {
public:
enum MODE { NORMAL = 1, SLEEP = 2, DORMANT = 4 };
// class implemented using the
// Singleton design pattern.
static Sleep& instance() {
static Sleep _instance;
return _instance;
}

// No copy constructor or assignment operator =
Sleep(Sleep const&) = delete;
void operator=(Sleep const&) = delete;

// used to (re-)configure
void configureDormant(void (*setupfnc)(), void (*loopfnc)(),
uint8_t WAKEUP_PIN, bool edge, bool active);
...

// get current mode
MODE get_mode();

// display current frequencies
void measure_freqs();

// saves clock registers
// and initializes RTC for sleep mode
void before_sleep();

// function responsible for sleep
// sleep ends with high edge (DORMANT) or when alarm time is reached (SLEEP)
void start_sleep();

// sleep recovery
void after_sleep();

// kind of run shell: calls _setup once, and
// implements infinite loop where sleep phases
// are initiated and _loop is being called
// in each iteration.
// Actually, this function implements an
// event loop:
void run();

private:
uint _scb_orig; // clock registers saved before DORMANT or SLEEP mode
uint _en0_orig; // ""
uint _en1_orig; // ""
MODE _mode; // can be SLEEP, DORMANT or NORMAL

uint _wakeup_pin; bool _edge; bool _active;
datetime_t _init_time; // initial time set
datetime_t _alarm_time; // alarm time
void (* _setup)(); // user-defined setup function - called once
void (* _loop) (); // user-defined loop function: called in each // iteration
Sleep(){}; // private constructor
};

Ablauf nach Arduino-Art

Einige haben sich wahrscheinlich über die Funktionszeiger _setup und _loop am Ende der Klassendefinition gewundert, die auf benutzerdefinierte Funktionen verweisen.

Wie bei der Arduino-Entwicklung erwartet die Klasse Sleep einen Zeiger auf eine setup()-Funktion und einen auf eine loop()-Funktion.

Die Methode run() ruft zunächst die konfigurierte setup-Funktion auf, die Code für die Initialisierung von Komponenten, benötigte Deklarationen und weitere Teile enthalten kann, welche zu Start der Anwendung einmalig ablaufen sollen.

Danach startet eine Endlosschleife, die in jeder Schleifeni-Ieration zunächst die Methoden before_sleep(), start_sleep(), after_sleep() aufruft, um den gewünschten Schlafmodus, sleep oder dormant, implementieren.
Nach jeder Schlafphase führt run() die benutzerdefinierte Methode loop() aus, in der sich zum Beispiel Messungen vornehmen, Daten verarbeiten, oder Geräte ansteuern lassen. Hier passiert also die eigentliche Routinearbeit:

void Sleep::run() {
_setup(); // called once
while(true) {
if (_mode != MODE::NORMAL) {
before_sleep();
start_sleep();
after_sleep(); // calls _loop in each iteration
}
else {
_loop(); // NORMAL mode =>
// must explicitly call _loop
}
}
}

Das war alles sehr theoretisch. Den Einsatz in der Praxis soll ein konkretes Anwendungsbeispiel illustrieren.

Anwendungsszenario: Wetterdatenerfassung mit dem BME280 und einem OLED-Display

Eine typische Problemstellung ist das Erfassen von Sensorwerten, etwa bei der Messung von Wetter- oder Umweltdaten. Diese findet normalerweise in längeren zeitlichen Abständen statt. Während dieser untätigen Warteperioden lässt sich der RP2040 in den Schlaf versetzen, um die Batterielaufzeit zu verlängern.
Als allseits beliebtes Beispiel soll also wieder einmal eine Wetterstation fungieren, die folgende Schaltung realisiert:

Prototypischer Aufbau der Wetterstation auf einem Steckbrett
Prototypischer Aufbau der Wetterstation auf einem Steckbrett

Als Sensor nutzt die Schaltung den bekannten BME280 von Bosch, der sich über SPI (Serial Peripheral Interface) anschließen lässt. Dieser Sensor misst Temperatur, Luftfeuchtigkeit und Luftdruck, woraus sich per barometrischer Formel die ungefähre Höhe berechnen lässt. Das Datenblatt des Sensors [5] findet sich auf der Webseite von Bosch-Sensortec.

Breakout-Board mit dem BME280 von Bosch-Sensortec
Breakout-Board mit dem BME280 von Bosch-Sensortec


Ein BME280 benötigt zwischen 1.8V und 3.6V Eingangsspannung. Er verbraucht im Schlafmodus in der Regel 0.1 uA (maximal 0.3 uA), während einer Messung 340 uA (Feuchtigkeit), 350 uA (Temperatur) und 740 uA (Druck).

Für die Programmierung der Schaltung kommt Code von der Raspberry-Pi-Organisation zum Einsatz. Die C-Routinen hat der Autor in eine C++-Wrapperklasse BME280 eingebettet, Fehlerkorrekturen vorgenommen, und weitere Funktionalität hinzugefügt. So etwa die Berechnung der Höhe und die Implementierung des sogenannten Forced Mode, bei dem der Sensor nur dann aktiv ist, sobald eine Messung erfolgt. Dadurch lässt sich sein Stromhunger auf ein Minimum reduzieren.

Zur Ausgabe der Messwerte dient ein über I2C angeschlossenes monochromes OLED-Display des Typs SSD1306 (siehe Datenblatt [6]) mit einer Auflösung von 128 x 64 Pixeln. Die Eingangsspannung sollte zwischen 1.65 V und 3.3 V betragen. Während des Schlafs verbraucht die Anzeige etwa 10 uA. Im Betrieb können es hingegen durchschnittlich zwischen 50 uA und 430 uA sein.

Das preisgünstige SSD1306-Display bietet 128 x 64 Pixel
Das preisgünstige SSD1306-Display bietet 128 x 64 Pixel

Als Treiber nutzt das Beispiel eine Bibliothek von Larry Bank.

Zur Stromreduktion gibt es die Möglichkeit, die Anzeige programmatisch ein- und auszuschalten. Dadurch lassen sich die aktiven Zeiten des Displays und damit dessen Stromverbrauch begrenzen.

Ein mit GPIO-Pin 15 (WAKEUP_PIN) und der Versorgungsspannung verbundener Taster dient im Dormant Mode des Raspberry Pi Pico als externer Trigger. Drückt man ihn, erwacht der Pico, führt eine Messung durch, deren Messwerte am OLED-Display für ein paar Sekunden erscheinen, worauf das Ausschalten der Anzeige erfolgt. Den Beginn der Messung zeigt die eingebaute LED an, die zu diesem Zweck kurz blinkt.

Zur Stromversorgung nutzt der Autor einen Batteriehalter für eine Batterie des Typs 18650. Grundsätzlich wäre natürlich jedes andere Setup möglich, etwa der Einsatz von LiPos oder die Verwendung einer Solarzelle.

Bill of Materials (Beispiel)

Für die Schaltung sind folgende Komponenten notwendig, die insgesamt für unter 20 Euro zu haben sind.Die Preise für Batteriehalter plus Batterie oder ein LiPo sind ebenso wie das USB-Kabel zum Programmieren des Raspberry Pi Pico nicht eingerechnet.

SDSD1306                  ca.  5,00 Euro
BME280 (SPI)              ca.  4,60 Euro 
Raspberry Pi Pico         ca.  4,10 Euro
Breadboard                ca.  3,00 Euro
Dupontkabel, Taster       ca.  1,00 Euro
GESAMT:                   ca. 19,70 Euro

Das Anwendungsprogramm

Gleich vorab. Der gesamte Code liegt auf Github (siehe die GitHub-Seite [7]) bereit, um Tipparbeit zu sparen und damit Energieverbrauch zu reduzieren.

Die Funktion setup() enthält den notwendigen Code für die Initialisierung von Sensor und Anzeige. Zudem nutzt die Software die eingebaute LED an GPIO25, um Beginn und Ende einer Sensormessung zu signalisieren. Aufgabe der Funktion welcome() ist einzig die Ausgabe eines Start-Bildschirms:

void setup() {
gpio_init(LED_PIN); // Use built-in LED to signal wake time
gpio_set_dir(LED_PIN, GPIO_OUT);

// ssd1306 OLED is initialized
oled_rc = myOled.init();
myOled.set_back_buffer(ucBuffer);
myOled.fill(0,1);

// Welcome screen
welcome(myOled);

// empty read as a warm-up
myBME280.measure();
sleep_ms(100);
}

In der Funktion loop() findet die Messung und die Ausgabe der
Messergebnisse am SSD1306 statt:

void loop() { 
// get measurement from BME280
gpio_put(LED_PIN, 1);
result = myBME280.measure();
gpio_put(LED_PIN, 0);
draw_on_oled(myOled, result);
}

Aufgabe des Hauptprogrammes (main()) ist im wesentlichen das Konfigurieren der Sleep-Instanz, unter anderem mit Zeigern auf die setup()- & loop()-Funktionen sowie der im Dormant-Modus benötigten Angabe des gewünschten GPIO-Eingangs. Zusätzlich reduziert main() gleich zu Beginn die Systemfrequenz des Pico von 125 MHz auf 60 MHz, um den Energieverbrauch zu minimieren.
Die Wetterstation soll beim Betätigen eines Pushbuttons die Messung starten und deren Ergebnisse anzeigen, weshalb der extern getriggerte Dormant-Modus zum Einsatz kommt. Am Schluss geschieht der Aufruf der run()-Methode, die den gesamten Rest der Verarbeitung steuert.

int main() {
stdio_init_all();
sleep_ms(3000); // required by some OSses to make Pico visible

// Change frequency of Pico to a lower value
printf("Changing system clock to lower frequency: %d KHz\n",
SYSTEM_FREQUENCY_KHZ);
set_sys_clock_khz(SYSTEM_FREQUENCY_KHZ, true);

// configure Sleep instance
// using Dormant mode
// pointers to loop() and setup() functions
// start and end of alarm period
// WAKEUP_PIN where high edges are detected
Sleep::instance().configureDormant(&setup, &loop, WAKEUP_PIN, true, true);
// show clock frequencies
Sleep::instance().measure_freqs();
// start event loop
Sleep::instance().run();
return 0;
}

Mit der Sleep-Klasse lässt sich eine ganze Menge infrastrukturellen Codes einsparen, weshalb Entwickler sich auf die eigentliche fachliche Logik konzentrieren können.

Übrigens findet sich unter folgendem Link "Sleepy Pico - the Movie": Kurzer Video-Clip zum Betrieb der fertigen Schaltung [8]

Fallstricke

Leider funktionieren die Schlafmodi nicht immer problemlos:

  • Nach dem Schlaf ist keine Ausgabe per printf() oder puts() am seriellen Monitor mehr möglich. Zum Debuggen sollten Maker daher die Picoprobe als Hardware-Debugger einsetzen.
  • Es kann passieren, dass der Pico im Schlafmodus „einfriert“.

Das mag an Fehlern in der Pico-SDK oder an der unzureichenden Dokumentation dieser Betriebsarten liegen. Als einfache, aber schmutzige Abhilfe ist es vorläufig ratsam, einen zusätzlichen Taster zwischen RUN-Eingang und einem der GND-Ausgänge des Raspberry Pi Pico anzubringen, um den Microcontroller im Falle des Falles reset-ten zu können. Sobald es weitere sachdienliche Hinweise geben sollte, wie diese Problemzonen zu umschiffen sind, erweitert und ändert der Autor den bereitgestellten Code.

Verbrauchsmessungen

Um den tatsächlichen Stromverbrauch einer Schaltung messen, bietet sich die Serienschaltung eines Multimeters oder Amperemeters zwischen Stromversorgung und Pico an. Einige Labornetzteile ermöglichen ebenfalls die Messung der von dem Verbraucher benötigten Leistung. Leider eignen sich nicht alle Messgeräte für die Erfassung von extrem niedrigen Stromstärken und Verbrauchswerten. Für genaue Aussagen lohnt sich daher der Erwerb eines uCurrent-Boards [9], das sich für Messungen von Strömen in uA-Bereich eignet.

Fazit

Um einen Raspberry Pi Pico für eigene Anwendungen, speziell solche mit Batteriebetrieb, energieeffizient zu nutzen, gibt es zwei Schlaf-Modi. Während der Dormant-Mode für das Aufwachen auf eine externe Quelle angewiesen ist, kann sich der RP2040 im Sleep-Mode selbst über die Echtzeituhr wiedererwecken. Dabei ist allerdings darauf zu achten, dass die Anwendung nach dem Wecken alle Oszillatoren wieder auf ihre ursprünglichen Frequenzen einstellt. Der alleinige Fokus auf den Pico reicht in realen Anwendungen indes nicht aus. Auch der Energiehunger angeschlossener Komponenten, die Umgebungstemperatur, und die Taktfrequenz spielen eine entscheidende Rolle. Wer das berücksichtigt, kann die Zeit bis zum erforderlichen Neuladen der Batterie auf erträgliche Werte verlängern.

Auf Basis des Beispiels einer Wetterstation lassen sich natürlich viele Erweiterungen hinzufügen, angefangen von einem e-Ink-Display zur Anzeige der Messergebnisse bis hin zu einer LiPo-Batterie oder Solarzelle für die Energieversorgung. Hauptsache, die Energiebilanz stimmt.

Viel Spaß mit Ihren eigenen Experimenten im Schlaflabor.


URL dieses Artikels:
https://www.heise.de/-6046517

Links in diesem Artikel:
[1] https://datasheets.raspberrypi.org/pico/pico-datasheet.pdf
[2] https://github.com/ms1963/SleepyPico
[3] https://github.com/raspberrypi/pico-playground/tree/master/sleep
[4] https://github.com/raspberrypi/pico-extras
[5] https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme280-ds002.pdf
[6] https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf
[7] https://github.com/ms1963/SleepyPico
[8] https://www.dropbox.com/s/l9ga78pulkpua55/SleepyPico-Demo.MOV?dl=0
[9] https://www.eevblog.com/projects/ucurrent/

Copyright © 2021 Heise Medien

Adblock test (Why?)

  • 17. Mai 2021 um 14:48

Die Intl-API – Internationalisierung im Browser

Von heise online

Die Intl-API – Internationalisierung im Browser

Tales from the Web side Sebastian Springer

Internationalisierung ist in der Web-Entwicklung ein Thema, das häufig leider immer noch viel zu kurz kommt. Für die meisten endet der Begriff Internationalisierung auch schon bei der Übersetzung von Texten. Das ist jedoch eine Problemstellung, die der Browser nach wie vor den etablierten Bibliotheken überlässt. Wo alle modernen Browser jedoch mittlerweile dank der Intl API durchgängig punkten können, sind Themen wie die lokalisierte Formatierung von Zahlen, Datums- und Zeitwerten. Neben diesen Angaben bringt die Intl API noch Features zum lokalisierten String-Vergleich sowie Regeln für die Behandlung von Ein- und Mehrzahl.

Alle JavaScript-Umgebungen, also die bekannten Browser, Node.js und sogar der Internet Explorer, unterstützen die Intl API in Form des globalen Intl-Objekts. Es enthält neben der getCanonicalLocales-Methode die folgenden Konstruktoren:

  • Collator: Klasse für sprachabhängigen String-Vergleich
  • DateTimeFormat: Formatierung von Datums- und Zeitwerten
  • NumberFormat: Formatierung von Zahlen wie Währungen oder Prozentwerten
  • PluralRules: Unterstützung bei der Behandlung von Ein- und Mehrzahl

Die Intl API setzt durchgängig auf Gebietsschema-Informationen zur Steuerung der einzelnen Features. Zu diesem Zweck akzeptieren die Konstruktoren der Schnittstelle Locale-Strings, die den BCP-47-Vorgaben [1] folgen, also beispielsweise “de” oder “de-DE”. Erzeugt ein Script eine neue Instanz einer der Intl-Klassen, akzeptiert diese entweder ein Array von Locales, eine einzelne Locale oder keinen Wert. Ein ungültiger Wert führt dazu, dass der Browser einen RangeError auswirft.

Die getCanonicalLocales-Methode liefert für eine Eingabe einen gültigen Locale-Wert zurück und verhält sich dabei wie die Konstruktoren der Intl-Unterklassen. Den Wert “DE” wandelt die Methode in “de” um, ein Array mit den Werten “de”, “de-de” und “DE-DE” wird zu “de” und “de-DE” und die Eingabe “de-DEU” führt zu einem RangeError. Bei einem Array entfernt die getCanonicalLocales-Methode alle Duplikate.

Alle Intl-Klassen verfügen über eine statische Methode mit dem Namen supportedLocalesOf. Diese Methode akzeptiert einen Locale-String oder ein Array von Locales und liefert ein Array von Locales zurück, das die jeweilige Klasse direkt unterstützt, ohne auf Standard-Locale zurückzugreifen.

Collator: String-Vergleiche

JavaScript verfügt über die Array.prototype.sort-Methode zur Sortierung von Arrays. Diese Methode sortiert die einzelnen Elemente anhand ihrer UTF-16-Codewerte. Alternativ akzeptiert die Sort-Methode eine Vergleichsfunktion. Diese Vergleichsfunktion erhält bei jedem Aufruf zwei Werte, die miteinander verglichen werden. Ist der erste Wert kleiner als der zweite, gibt die Vergleichsfunktion eine Zahl kleiner als Null zurück, ist der erste Wert größer, wird eine Zahl größer Null zurückgegeben. Sind beide Werte gleich, ist der Rückgabewert der Vergleichsfunktion Null. Je nach Region, in der die Applikation ausgeführt wird, kann das Verhalten dieser Sortierung unterschiedlich sein. Die Collator-Klasse der Intl API nimmt Entwicklern diese Aufgabe ab und sorgt dafür, dass die Sortierung für die gewählte Region passend ist.

Die compare-Methode einer Collator-Instanz hat eine zur sort-Methode der JavaScript Arrays passende Signatur, akzeptiert also zwei Werte und gibt entweder 1, 0 oder –1 zurück. Das folgende Beispiel sortiert das Array mit den Werten “Birnen”, “Äpfel” und “Zitronen” für die Locale “de-DE”.

const col = new Intl.Collator('de');
const arr = [ 'Birnen', 'Äpfel', 'Zitronen' ];
console.log(arr.sort(col.compare)); // ["Äpfel", "Birnen", "Zitronen"]

Wie zu erwarten, ist das Ergebnis “Äpfel, Birnen, Zitronen”. Für die Locale “sv-SE”, also Schwedisch in Schweden, sieht das Ergebnis ganz anders aus, da hier das “Ä” nach dem “Z” einsortiert wird. Und das ist nur ein Beispiel, bei dem die Collator-Klasse hilfreich ist. Ein weiterer typischer Anwendungsfall ist das Sortieren von Zahlenwerten. Standardmäßig sortiert JavaScript folgendermaßen: 1, 132, 22. Die erste Ziffer ist hier also jeweils ausschlaggebend und nicht der Zahlenwert. Die Collator-Klasse sieht eine Lösung vor, nach der numerische Werte korrekt interpretiert werden können. Dieses Verhalten ist jedoch deaktiviert und muss bei der Erzeugung der Collator-Instanz zunächst aktiviert werden. Das erfolgt entweder über die Unicode-Erweiterung “kn” der Locale-Zeichenkette, also in diesem Fall beispielsweise “de-u-kn", oder über die numeric-Option, deren Wert auf true gesetzt wird. Solche Optionen akzeptieren alle Intl-Konstruktoren in Form eines Objekts als zweites Argument bei der Instanziierung.

const col = new Intl.Collator('de', {numeric: true});
const arr = [132, 1, 22 ];
console.log(arr.sort(col.compare)); // [1, 22, 132]

Zusätzlich zu diesen Optionen unterstützt der Collator noch weitere, beispielsweise wie Groß- und Kleinschreibung unterschieden werden sollen. Diese insgesamt flexibele Klasse sorgt dafür, dass für einen Stringvergleich kaum noch benutzerdefinierte Funktionen geschrieben werden müssen.

DateTimeFormat: Formatierung von Datum und Zeit

Eine der großen Schwächen der JavaScript Date-Klasse ist, dass sich die mit ihr erzeugten Objekte kaum formatiert ausgeben lassen. Dafür benötigt eine Applikation immer eine zusätzliche Bibliothek. Die Intl API löst zumindest einen Teil dieser Probleme. Die toString-Methode des Date-Objekts liefert den folgenden Wert: “Fri Jun 05 2020 10:15:00 GMT+0200 (Central European Summer Time)”. In einer Web-Applikation ist eine solche Zeichenkette jedoch wenig sinnvoll. Normalerweise sollen nur Datum oder Zeit in der korrekten Formatierung angezeigt werden – also beim Datum etwa 05.06.2020, 05/06/2020 oder 06/05/2020. Die Date-Klasse unterstützt eine solche Formatierung nicht direkt, sondern nur über einzelne Methoden wie getDay oder getMonth, die allerdings nur einstellige Ausgaben für den 5. beziehungsweise Juni liefern. Entwickler müssen hier also wieder selbst tätig werden. Die DateTimeFormat-Klasse der Intl API liefert mit ihrer format-Methode eine elegantere Lösung:

const dateTime = new Intl.DateTimeFormat('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});

console.log(dateTime.format(date)); // 05.06.2020

Eine Änderung der Locale auf “en-US” führt zur Ausgabe von “06/05/2020” und “en-GB” sorgt für “05/06/2020”. Das Options-Objekt der DateTimeFormat-Klasse entscheidet über das Aussehen und auch den Inhalt der einzelnen Elemente des Date-Objekts. Wird die day-Eigenschaft beispielsweise weggelassen, fällt auch die Tagesangabe bei der Ausgabe weg. Die meisten Eigenschaften des Options-Objekts unterstützen die Werte “2-digit”, also auf zwei Stellen aufgefüllt, und “numeric”, was je nach Wert eine einstellige, zweistellige oder bei der Jahreszahl eine vierstellige Ausgabe produziert. Bei der Formatierung von Zeitwerten verhält sich die DateTimeFormat-Klasse wie das Datum:

const dateTime = new Intl.DateTimeFormat('de-DE', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});

console.log(dateTime.format(date)); //10:15:00

Beim Formatieren von Datum und Uhrzeit hilft die DateTimeFormat-Klasse, allerdings sind die Möglichkeiten eingeschränkt. Eine komplett freie Formatierung ist nicht vorgesehen. Es gibt aber die formatToParts-Methode, die jeden einzelnen Teil des Ergebnisses als ein separates Array-Element enthält:

const dateTime = new Intl.DateTimeFormat('de-DE', {
hour: '2-digit',
minute: '2-digit',
});

console.log(dateTime.formatToParts(date));

/* Ausgabe:
[{type: "hour", value: "10"},
{type: "literal", value: ":"},
{type: "minute", value: "15"}]*/

Mit den einzelnen Teilen des resultierenden Objekts lassen sich beliebige Formate realisieren. Hier ist jedoch umfangreiche Handarbeit erforderlich – es wird jedoch deutlich einfacher als mit dem nativen Date-Objekt von JavaScript.

NumberFormat: Zahlen formatieren

Die NumberFormat-Klasse kümmert sich, wie der Name andeutet, um das Formatieren von Zahlenwerten und zwar in drei verschiedenen Arten. Die style-Eigenschaft bestimmt, ob es sich um eine gewöhnliche Zahl (decimal), einen Währungswert (currency) oder einen Prozentwert (percent) handelt. Der Standardwert für die Option style ist “decimal” und useGrouping weist “true” auf, sodass immer ein Tausendertrennzeichen verwendet wird und die im Beispiel verwendete Option auch weggelassen werden könnte. Der nachfolgende Code gibt die Zahl 3141,59 mit Tausendertrenner und zwei Nachkommastellen aus:

const number = Math.PI * 1000; 
const numberFormatter = new Intl.NumberFormat('de-DE', {
useGrouping: true,
maximumFractionDigits: 2,
});

const formattedNumber = numberFormatter.format(number);

console.log(formattedNumber); // 3.141,59

Mit der Option useGrouping kann gewählt werden, ob der Tausendertrenner angezeigt werden soll. Die Änderung der Locale im Konstruktor auf den Wert “en-US” führt zur Ausgabe von 3,141.59.

Ein weiteres wichtiges Feature der NumberFormat-Klasse ist die Formatierung von Währungswerten. Dieses Feature wird durch die style-Eigenschaft mit dem Wert “currency” aktiviert. Zusätzlich dazu muss die Währung über die currency-Eigenschaft übergeben werden. Erfolgt dies nicht, wirft der Browser einen TypeError aus. Zur Auswahl der Werte stehen die ISO-4217-Währungscodes wie “EUR” oder “USD” bereit. Der nachstehende Code gibt den Wert €3,141.59 aus:

const number = Math.PI * 1000; 
const numberFormatter = new Intl.NumberFormat('en-GB', {
style: "currency",
currency: "EUR",
maximumFractionDigits: 2,
});

const formattedNumber = numberFormatter.format(number);

console.log(formattedNumber); // €3.141,59

Die currencyDisplay-Eigenschaft steuert die Anzeige des Währungssymbols. Der Standardwert “symbol” zeigt in diesem Beispiel das Euro-Symbol an. Weitere mögliche Werte sind “code” für den ISO-Code und “name” für den ausführlichen Namen der Währung.

PluralRules: Einzahl oder Mehrzahl, das ist hier die Frage

Eine Instanz der PluralRules-Klasse gibt mit der select-Methode für eine Zahl eine Zeichenkette zurück, die angibt, ob es sich um Ein- oder Mehrzahl handelt. Zugegebenermaßen ist diese Klasse für die deutsche Locale eher unspektakulär. Bei der type-Option mit dem Standardwert “cardinal” gibt die Methode für die Zahl 1 den Wert “one” und für alle übrigen den Wert “other” zurück. Dieser Typ steht für Mengen. Mit dem “ordinal”-Typ kann gezählt werden, was für die deutsche Locale durchgängig die Werte “other” zurückgibt. Anders sieht die Situation für die Locale “en-US” aus, wie folgendes Codebeispiel zeigt:

const arr = new Array(24).fill('').map((v, i) => i);
const pr = new Intl.PluralRules('en-US', {type: 'ordinal'});

arr.forEach(v => console.log(v, pr.select(v)));

Für alle Werte, die auf 1 enden, gibt die select-Methode “one”, für 2 den Wert “two”, für 3 den Wert “few” aus, und für alle weiteren Eingaben liefert select die Ausgabe “other” zurück. Eine Umstellung auf den “cardinal”-Typ liefert wieder “one” für 1 und “other” für die übrigen Werte zurück.

Fazit: hilft uns die Intl API wirklich?

Die Intl API bietet einige sinnvolle Erweiterungen für den JavaScript-Sprachstandard, um internationale Applikationen zu ermöglichen. Das Thema Übersetzungen spart die Schnittstelle aber komplett aus und überlässt es zusätzlichen Bibliotheken. Aber gerade die Formatierung von Datums-, Zeit- und Zahlenwerten löst eine Reihe von Standardproblemen auf elegante und solide Art.


URL dieses Artikels:
https://www.heise.de/-6040598

Links in diesem Artikel:
[1] https://tools.ietf.org/html/bcp47

Copyright © 2021 Heise Medien

Adblock test (Why?)

  • 07. Mai 2021 um 16:35

I/O on Steroids - PIO, die programmierbare Ein-/Ausgabe des Raspberry Pi Pico

Von heise online

I/O on Steroids - PIO, die programmierbare Ein-/Ausgabe des Raspberry Pi Pico

Der Pragmatische Architekt Michael Stal

Moderne Mikrocontroller-Boards müssen eine ganze Menge von Schnittstellen integrieren wie UARTs, IIS, IIC, SPI. Was aber, wenn eine benötigte Schnittstelle fehlt, etwa 1-Wire oder CAN? Für solche Fälle bietet der Raspberry Pi Pico die PIO (Programmable Input Output). Der vorliegende Blog-Post gliedert sich in zwei Teile: Im ersten ist die Funktionsweise einer PIO Gegenstand der Betrachtung, während der zweite Teil untersucht, wie sich PIOs in der Praxis nutzen lassen.

Motivation

Auf einem Raspberry Pi Pico finden sich keine Komponenten für Bluetooth oder WiFi. Nicht nur deshalb wäre ein Vergleich mit Mikrocontrollern auf Basis von ESP8266 beziehungsweise ESP32 ein Vergleich von Äpfel und Birnen. Eher entspricht der Pico mit seinen Anwendungsmöglichkeiten leistungsfähigen Arduino-Boards.

Was den Pico aber gegenüber anderen Boards auszeichnet, ist seine Möglichkeit, I/O-Pins programmatisch über eine eigene Hardwarekomponente des RP2040-Chips namens PIO (Programmable I/O) zu steuern. Jeder der zwei vorhandenen PIO-Blöcke enthält zu diesem Zweck vier Zustandsmaschinen, auf denen in PIO-Assembler geschriebene Programme ablaufen. Assemblerprogrammierung mag bei manchem ungute Assoziationen wecken. Allerdings bestehen PIO-Programme beim Pico aus lediglich neun relativ einfachen Befehlen.

Vorteil des PIO-Konzepts ist die sich daraus ergebende große Flexibilität und Anwendungsvielfalt. Unter anderem haben Maker damit schon VGA-Bildschirme, WS2812b-LEDs und Schrittmotoren angesteuert oder HW-Schnittstellen wie UART realisiert. Natürlich sind diese Beispiele grundsätzlich auch über ein rein in Software gegossenes Bit-Banging umsetzbar, was aber in der Praxis mit vielen Problemen verbunden ist, etwa bezüglich genauem Timing und Wartbarkeit, sowie der hohen Belastung der Rechnerkerne.

Zustandsmaschinen und I/O-Mappings

Raspberry Pi Foundation
Jeder der beiden PIO-Blocks enthält vier Zustandsmaschinen, die über den Bus mit der Außenwelt verbunden sind (Bild: Raspberry Pi Foundation)

Ein Raspberry Pi Pico enthält zwei PIO-Blöcke (pio0 und pio1) mit je vier Zustandsmaschinen, die sich einen Speicher von 32 Instruktionen teilen. Instruktionen haben eine Länge von 16 Bit. Für jedes Programm auf einer Zustandsmaschine definieren Entwickler ein I/O-Mapping, das alle GPIO-Pins umfasst, die das Programm nutzen möchte. Ein Mapping legt ein Fenster fest, das aus einem GPIO-Start-Pin und der Zahl der im Mapping eingeschlossenen GPIOs besteht. Eigentlich bietet der Raspberry Pi Pico nur 30 GPIOs, aber das Mapping-Register hat zwei virtuelle GPIOs hinzugefügt (GPIO30 und GPIO31). Kein Wunder, besteht das Mapping-Register doch aus 32 Bits. Beispiels-Mappings sind etwa [GPIO4, GPIO5, GPIO6] oder [GPIO 28, GPIO29, GPIO30, GPIO31, GPIO0, GPIO1]. Insofern repräsentieren sie Fenster auf einen Bereich aufeinanderfolgender GPIOs. Wie im zweiten Beispiel zu sehen ist, folgt auf GPIO31 zyklisch wieder GPIO0. Die im Mapping enthaltenen Pins können sowohl zur Eingabe als auch zur Ausgabe dienen. Genau genommen sind es sogar vier Mappings:

Die vier Mappings der PIO
Die vier Mappings der PIO
  • Ein in(put)-Mapping, das aus einem einzelnen Basis-Eingabe-GPIO besteht.
  • Ein out(put)-Mapping, das bis zu 32 GPIO-Pins enthält, wobei die einzelnen GPIOs entweder als Ein- oder Ausgabe-GPIOs fungieren können.
  • Ein set-Mapping mit bis zu 5 GPIOs.
  • Ein side-(set)-Mapping mit bis zu 5 GPIOs.

Das Basis-GPIO jedes Mappings zählt aus Sicht der Zustandsmaschine als Pin 0, die darauffolgenden Pins sind dementsprechend Pin 1, Pin 2, Pin 3, usw. Die vier Mappings dürfen sich sogar überlappen. Es gibt in dieser Hinsicht keinerlei Einschränkungen.

Die Namen der Mappings (in, out, set, side-set) beziehen sich auf die gleichnamigen Kommandos für Einlesen, Auslesen, Setzen von GPIO-Pins, von denen später noch die Rede sein soll.

Interaktion

Zur Kommunikation mit der Außenwelt bieten sich einer PIO-Zustandsmaschine neben I/O zwei Möglichkeiten:

  1. Sie kann einen IRQ nutzen (Interrupt Request), um sich mit den RP2040-Cores oder mit anderen Zustandsmaschinen zu synchronisieren.
  2. Sie kann von einem RP2040-Core Daten erhalten (pull) oder nach dort senden (push).

Da sich alle Zustandsmaschinen eines PIO-Blocks einen gemeinsamen Programmspeicher teilen, können mehrere Zustandsmaschinen dasselbe Programm ausführen.
Beispiel: Jede Zustandsmaschine bedient einen separaten LED-Strip.

Oder sie nehmen unterschiedliche Aufgaben für dieselbe Schnittstelle wahr.
Beispiel: Eine Zustandsmaschine implementiert den RX-Teil (Empfangen von Daten) eines UART, während sich eine andere Zustandsmaschine um die TX-Komponente (Senden von Daten) kümmert.

Sie können natürlich auch unterschiedliche Programme ausführen, um verschiedene Arten von I/O-Schnittstellen parallel zu implementieren.

Raspberry Pi Foundation
Das Innenleben einer Zustandsmaschine mit Schieberegistern ISR und OSR, Scratchregistern X und Y, dem Clock-Divider, den Program Counter, und der Kontrollogik (Bild: Raspberry Pi Foundation)

Eine Zustandsmaschine enthält eine Warteschlange TX-FIFO mit einer Kapazität von vier 32-Bit-Worten, die dem Entgegennehmen von Daten aus der CPU dient. Diese Daten holt sie sich über einen Pull-Befehl in das sogenannte OSR (Output-Shift-Register). Durch Links- oder Rechts-Schiebe-Operationen auf dem OSR lassen sich ein oder mehrere Bits zwecks Ausgabe an die GPIOs senden. Insofern stellt das OSR sowohl die Schnittstelle zwischen Zustandsmaschine und Ausgabe-GPIOs als auch die zwischen Zustandsmaschine und CPU-Cores dar. Durch Pull kann ein PIO-Programm Daten von der CPU über die TX-FIFO lesen.

Raspberry Pi Foundation
Die TX-FIFO dient zum Schreiben von Daten über das OSR auf die Ausgabe-Pins sowie zum Empfangen von Informationen von den CPU-Cores (Bild: Raspberry Pi Foundation)

Es gibt eine weitere Warteschlange namens RX-FIFO, die ebenfalls eine Kommunikation mit der Außenwelt implementiert. Die RX-FIFO-Warteschlange umfasst vier Slots mit 32-Bit-Worten. Ihre Daten enthält sie im Normalfall vom Input-Shift-Register, dem ISR, das dem Einlesen von digitalen Pins dient. Somit bildet das ISR die Schnittstelle zwischen GPIO-Pins und der RX-FIFO. Werte der Eingabe-Pins lassen sich bitweise ins ISR übermitteln, das dazu mit (Left- oder Right-)Shift-Operationen arbeitet. Der push-Befehl überträgt den Inhalt des ISR an die RX-FIFO, von wo sie ein Hauptprogramm in der CPU / MCU übernimmt.

Die TX-FIFO dient zum Ausgeben von Daten an den Ausgabe-Pins sowie zum Empfangen von Informationen von den  CPU-Cores
Die RX-FIFO dient zum Einlesen von Daten aus dem ISR sowie zum Senden von Informationen an die CPU-Cores

Die beiden FIFOs können Entwickler auch zu einer einzelnen Eingabe-Queue (RX-FIFO) oder zu einer einzigen Ausgabe-Queue (TX-FIFO) mit je acht 32-Bit-Worten kombinieren, das sogenannte FIFO-Join.

Raspberry Pi Foundation
Wird nur eine Warteschlange (TX oder RX) benötigt, lassen sich beide Warteschlangen zu einer großen (RX oder TX)-Warteschlange kombinieren (Bild: Raspberry Pi Foundation)

Ein Pull-Befehl auf der TX-FIFO blockiert, solange dort keine Daten vorliegen.

Ein Push-Befehl blockiert, solange die RX-FIFO voll ist, weil keine Daten von der CPU abgeholt wurden.

Des Weiteren besteht die Möglichkeit, für eine Zustandsmaschine Parameter wie AUTOPUSH oder AUTOPULL mit entsprechenden Schwellwerten zu definieren, die automatisiert push- und pull-Operationen beim Einreichen dieser Schwellwerte ausführen.

DMA & PIO

Eine weitere Möglichkeit soll nicht unerwähnt bleiben. Ein Raspberry Pi Pico enthält einen DMA-Controller, der ohne Belastung der Rechnerkerne schnelle Speichertransfers durchführen kann, ein 32-Bit-Wort pro Maschinenzyklus. Diese Daten können als Ziel auch eine Zustandsmaschine haben. Beispielanwendung: Die Zustandsmaschine implementiert eine VGA-Schnittstelle, und enthält die Grafikdaten über DMA. Würde dieser Transfer über die CPU erfolgen, wäre er zum einen langsamer und würde zum anderen die CPU belasten.

Alle Register ziehen

Neben den Registern ISR und OSR existieren noch zwei allgemein nutzbare Register X und Y, die sogenannten Scratch-Register.
Ein ungenutztes ISR oder OSR können Programme nach eigenem Gusto ebenfalls als Register einsetzen.
Zusätzlich gibt es einen PC (Program Counter), der auf den nächsten auszuführenden Befehl im gemeinsamen Befehlsspeicher verweist. Dieser ist sogar programmatisch nutzbar, wie weiter unten erläutert.

Auf Befehl

Programme auf der Zustandsmaschine können aus lediglich 9 Befehlen auswählen, die dafür einige Flexibilität erlauben. Wichtig zu wissen: Die Abarbeitung eines Befehls benötigt stets einen Maschinenzyklus, dessen Zeitdauer sich aus 1 geteilt durch 133.000.000 Hz berechnet. Das ergibt eine Zykluszeit von ungefähr 7,5 Nanosekunden. Wie wir später kennenlernen, lässt sich die Geschwindigkeit der PIO-Blöcke auch auf kleinere Frequenzen beziehungsweise höhere Zykluszeiten einstellen.

Zustandsmaschinen werden über 9 Instruktionen programmiert
Zustandsmaschinen werden über 9 Instruktionen programmiert (Bild: Raspberry Pi Foundation)

In den nachfolgenden Abschnitten folgt nun die komplette Liste der Instruktionen.

PUSH (iffull) (block | noblock)

Raspberry Pi Foundation
(Bild: Raspberry Pi Foundation)

kopiert den Inhalt des ISR (Input-Shift-Register) in die RX-FIFO und löscht danach den Inhalt des ISR. Bei Angabe von iffull geschieht dies ausschließlich dann, wenn das ISR eine konfigurierbare Zahl von Bits enthält. Ist der Befehl mit der Option block versehen, wartet die Zustandsmaschine so lange bis die RX-FIFO Platz für die Daten aus dem ISR vorweist. Hingegen würde bei Angabe von noblock die Zustandsmaschine zum nächsten Befehl springen, sofern das RX-FIFO belegt sein sollte, aber gleichzeitig den Inhalt des ISR löschen. PUSH ohne Argumente ist übrigens gleichbedeutend mit PUSH block.

PULL (ifempty) (block | noblock)

Raspberry Pi Foundation
(Bild: Raspberry Pi Foundation)

überträgt den Inhalt der TX-FIFO in das OSR (Output-Shift-Register). Eine Angabe von ifempty bewirkt, dass dieser Befehl nur durchgeführt wird, wenn das OSR keinen Inhalt aufweist - es ist folglich leer. Bei Angabe von block wartet die Zustandsmaschine solange blockierend bis Daten in der TX-FIFO vorliegen. Bei noblock springt die Zustandsmaschine unverrichteter Dinge zum nächsten Befehl, sollte die TX-FIFO leer sein. PULL ohne Argumente ist gleichbedeutend mit PULL block. C- oder Python-Programme können über die TX-FIFO direkt Instruktionen an die Zustandsmaschine senden.

Ebenso besteht die Option, direkt Instruktionen in das INSTRUCTION-Register der Zustandsmaschine zu schreiben, worauf sie die Instruktion ausführt. Danach setzt sie mit dem nächsten vom PC (Program Counter) adressierten Befehl fort. Achtung: Solche Instruktionen können auch gezielt den PC ändern. Wie das programmatisch aussieht, folgt weiter unten.

JMP (Bedingung) Sprungziel

Raspberry Pi Foundation
(Bild: Raspberry Pi Foundation)

dient, nomen est omen, für bedingte und unbedingte Sprünge an eine Zieladresse. Das Sprungziel kann eine physikalische Adresse von 0 bis 31 sein, oder ein logisches Sprungziel, also ein Label, das der Assembler durch die Physikalische Adresse ersetzt. Die Bedingung ist optional, weshalb etwa

jmp ende 

einen sofortigen Sprung zum Label „ende“ bewirkt.

Der Sprung kann auch abhängig von einer Bedingung erfolgen:

!X bzw. !Y => Sprung erfolgt, falls der Inhalt des X- bzw. Y-Registers gleich 0 ist.

X-- bzw. Y-- => Sprung erfolgt, sofern X beziehungsweise Y ungleich 0 ist. Mit dem Sprung wird gleichzeitig X bzw. Y dekrementiert.

X!=Y => Sprung erfolgt, wenn X und Y unterschiedliche Werte aufweisen.

PIN => Sprung erfolgt, falls Pin auf HIGH.

!OSRE => Sprung erfolgt, falls das OSR (Output-Shift-Register) nicht leer ist. Somit steht OSRE für Output-Shift-Register Empty.

IN Quelle, 1-32

Raspberry Pi Foundation
(Bild: Raspberry Pi Foundation)

bewirkt das Kopieren von 1-32 Bits aus einer Quelle in das Input-Shift-Register. Die Quelle kann sein:

ISR

OSR

PINS ————- PINS verwendet dabei das In-GPIO-Mapping.

X

Y

NULL

OUT Ziel, 1-32

Raspberry Pi Foundation
(Bild: Raspberry Pi Foundation)

kopiert die definierte Zahl an Bits vom OSR zum Ziel, wobei das Ziel sein kann:

PINS —- GPIO-Pins gemäß dem Out-Mapping

X

Y

ISR

NULL

PINDIRS —- Durch Angabe von PINDIRS als Ziel lässt sich für die Pins des Out-Mappings festlegen, ob sie als Ausgabe-Pin dienen sollen (1) oder als Eingabe-Pin (0).

Mit

   out pindirs, 3  

würden somit die ersten 3 Pins des Mappings entweder als Ausgabe-oder Eingabe-GPIOs je nach Belegung der entsprechenden Bits im OSR festgelegt. Auch der Program Counter PC lässt sich mit OUT verändern.

Ein

   out pc, 5

führt dazu, dass der Programmzähler auf den in den entsprechenden 5 Bits des OSR gespeicherten Werts eingestellt wird. Wie erwähnt: Der Programmspeicher besteht aus 32 16-Bit-Worten, weshalb zur Adressierung 5 Bits ausreichen.

Bei

    out exec, 16 

interpretiert die Zustandsmaschine 16 Bits des OSRs als Kommando und führt dieses aus. Der Wert wird im Instruktionsregister abgelegt.

SET Ziel, 0-31

Raspberry Pi Foundation
(Bild: Raspberry Pi Foundation)

kopiert den Wert (0..31) in das Ziel. Ziel kann sein:

PINS, also die im set-Mapping angegebenen GPIO-Pins. Es werden somit die entsprechenden Bits über die bis zu fünf Pins des set-Mappings ausgegeben.

PINDIRS set pindirs, 3 legt beispielsweise die ersten 2 Pins des set-Mappings als Ausgabe-Pins fest.

X Auch die Scratch-Register x und y können als Ziel auftauchen

Y

MOV Ziel, Quelle

Raspberry Pi Foundation
(Bild: Raspberry Pi Foundation)

bewegt, wenig überraschend, den Inhalt der Quelle zum Ziel. Ziele können sein:

  • PINS gemäß out-Mapping
  • EXEC
  • ISR
  • OSR
  • PC
  • X
  • Y

Quellen können sein:

  • PINS
  • X
  • Y
  • ISR
  • OSR
  • NULL
  • STATUS

Bislang blieb die Quelle STATUS unerwähnt. Sie lässt sich gemäß Anforderungen des Entwicklers konfigurieren, etwa als „TX-FIFO leer“ oder „TX-FIFO voll“.

Übrigens: Stellen Programmierer der Quelle ein ! oder ~ voran, kopiert MOV den invertierten Wert der Quelle. Beim Voranstellen von :: kopiert MOV die Bits der Quelle in umgekehrter Reihenfolge.

IRQ (option) IRQ-Nummer (_rel)

Raspberry Pi Foundation
(Bild: Raspberry Pi Foundation)

dient zum Auslösen eines der acht möglichen Interrupts 0 bis 7. Dadurch synchronisieren sich Zustandsmaschinen untereinander (siehe WAIT-Kommando) oder mit der CPU. Mit dem optionalen _rel sind IRQs abhängig von der Zustandsmaschine nutzbar.

Dabei erfolgt folgende Operation: ((0b11 & Nummer-des-IRQ) + Nummer-der Zustandsmachinen) mod 4. Die logische And-Operation extrahiert also die letzten 2 Bits der IRQ-Nummer.

Beispiel: Für IRQ 6 (= binär 0b110) und Zustandsmachine 2 (0b10) ergibt sich 4 % 4 = 0.

Mögliche Optionen:

set nowait IRQnr     => das IRQ Flag ohne Prüfung setzen ohne zu warten.
set wait IRQnr       => das Flag erst dann setzen, nachdem es woanders auf 0 gesetzt wurde. Das ist ein Mechanismus, um sich mit anderen Zustandsmaschinen zu synchronisieren.
set IRQnr clear      => Flag löschen ohne zu warten.

WAIT

Raspberry Pi Foundation
(Bild: Raspberry Pi Foundation)
WAIT Polarität GPIO Nummer 

wartet bis das GPIO mit der angegebenen Nummer den in Polarität angebenen Wert (0 oder 1) erreicht. Mit Nummer ist die tatsächliche GPIO-Nummer auf dem Pico gemeint.

WAIT Polarität PIN Nummer 

verhält sich wie obere Variante, nutzt aber das in-Mapping für die Nummerierung der Pins.

WAIT Polarität IRQ Nummer (_rel)

wartet auf das Setzen eines Interrupts von außen. Achtung! Polarität verhält sich hier anders als bei der ersten Variante: Ist Polarität = 1, löscht der Befehl das IRQ-Flag, sobald es gesetzt wurde. Ist Polarität = 0, bleibt das Flag hingegen unverändert. Das optionale _rel verhält sich wie bei der IRQ-Instruktion.

Teilweise ist in PIOASM-Programmen, etwa in MicroPython oder in C, auch der Befehl NOP zu sehen. Diese Instruktion gibt es nicht wirklich. Stattdessen ersetzt der Assembler diese Pseudoinstruktion durch

mov y,y

Von Delays und Clock Dividern

Wer I/O-Schnittstellen programmiert, ist sich im allgemeinen darüber bewusst, dass Timing-Probleme zu den häufigsten Herausforderungen zählen. Zum Beispiel sind für LEDs des Typs WS2812b genau getaktete Signal-Flanken notwendig, die sich über eine genau festgelegte Anzahl von Zyklen erstrecken. Zustandsmaschinen offerieren dafür Delays gemessen in Maschinenzyklen, die neben Befehlen in eckigen Klammern stehen, etwa:

set pins, 1  [4] 
set pins, 0

Hier wird am ersten im set-Mapping definierten GPIO-Pin ein HIGH-Signal ausgegeben. Dieser Befehl benötigt einen Maschinenzyklus. Die Anweisung [4] rechts daneben veranlasst die Zustandsmaschine, weitere 4 Maschinenzyklen dranzuhängen, wodurch das HIGH-Signal für 5 Maschinenzyklen anliegt, bevor der nächste set-Befehl das GPIO-Pin wieder auf LOW setzt. Da für Delays 5 Bits vorhanden sind, lassen sich zu jeder Instruktion maximal 31 Wartezyklen hinzufügen.

Allerdings nützen Delays alleine nichts, würde die Zustandsmaschine stets mit der vollen Pico-Freqenz von 133 MHz laufen. Zum Glück können Entwickler die Frequenz konfigurieren, indem sie einen 16-Bit Teiler definieren, den Clock Divider.

Die minimal mögliche Taktfrequenz einer Zustandsmaschine liegt daher bei etwa 2029 Hz = 133.000.000 Hz / 65536. Daraus ergibt sich eine maximale Dauer des Zustandsmaschinenzyklus von 0,492 Millisekunden. Es lassen sich zwar niedrigere Frequenzen konfigurieren, die aber zu Instabilität führen beziehungsweise nicht funktionieren. Benötigt der Entwickler längere Zykluszeiten, kann er sie zusätzlich mit Delays kombinieren.

Side-set

Wer sich gefragt hat, was es denn mit dem side-set-I/O-Mapping auf sich hat, erhält nun die Antwort: Wie gesagt, definiert das Mapping bis zu 5 aufeinanderfolgende GPIO-Pins. Mit dem side-set haben Entwickler die Möglichkeit parallel zu einer Instruktionsausführung bis zu 5 Pins auf HIGH oder LOW zu setzen.

Das sieht im Assemblercode folgendermaßen aus:

set pins, 1   side 0;  Side-set erfolgt auch, wenn die Instruktion hängt

Es wird das erste Bit des OSR am GPIO-Pin ausgegeben (gemäß out-Mapping) und danach fünf Wartezyklen eingelegt. Parallel gibt die Zustandsmaschine 0 an allen im side-set-Mapping definierten GPIO-Pins aus. Ein „side 1“ würde an den entsprechenden GPIO-Pins 1 ausgeben.
Im PIO-Programm muss der Entwickler dafür spezifizieren:

.side_set 1

Mittels der Variante

.side_set 1 pindirs

lassen sich die side-Anweisungen alternativ dafür nutzen, um keine Ausgaben an den „side-Pins“ vorzunehmen sondern stattdessen deren Richtung festzulegen. Ein side 0 definiert in der Folge die entsprechenden Pins als Eingabe-Pins, während side 1 sie zu Ausgabe-Pins macht.

Das Einsetzen von side_set hat aber einen Preis, weil die Anweisung dafür 1 Bit vom möglichen Delay stiehlt. Dadurch bleiben nur 4 Bits für Delays übrig, was Werten von 0, 1, 2, ... ,15 entspricht. Zudem ist bei jeder Instruktion verpflichtend ein side 0 oder side 1 anzugeben.

Wer side-Befehle benötigt, aber sie nur bei einzelnen Instruktionen, also optional verwenden möchte, gibt im PIO-Assemblerprogramm .side_set 1 opt an, was aber ein weiteres Bit des Delays kostet, sodass nur noch Delays von 0 bis 7 möglich sind.

Der Wrapper

Ein PIO-Programm, das ein Blinken der eingebauten LED erzeugt, das richtige set-Mapping vorausgesetzt, könnte wie folgt aussehen:

.program blinker
; eventuell weitere Instruktionen
start:
set pins, 1 ; LED ein
set pins, 0 ; LED aus
jmp start ; Let‘s do it again

Da die meisten PIO-Programme mit einer unendlichen Schleife arbeiten, erweist sich der jmp-Befehl am Ende des Programms als notwendig. Diese Instruktion reduziert gleichzeitig die mögliche Zahl nutzbarer Anweisungen in einem PIO-Block um 1. Noch dazu ist das Programm „asymmetrisch“, da die Ausgabe von 1 einen Maschinenzyklus dauert, während die Ausgabe von 0 wegen jmp sich über zwei Maschinenzyklen erstreckt.

Abhilfe könnte natürlich ein Delay schaffen:

.program blinker
; eventuell weitere Instruktionen
start:
set pins, 1 [1] ; LED ein und einen Zyklus dranhängen
set pins, 0 ; LED aus
jmp start ; Let‘s do it again

Jetzt dauern sowohl die Erleuchtung als auch die Verdunklung gleich lang, nämlich zwei Maschinenzyklen.
Da die Endlosschleife einen verbreiteten Anwendungsfall darstellt, haben die Entwickler der PIO hierfür Abhilfe geschaffen:

.program blinker
; eventuell weitere Instruktionen
.wrap_target
set pins, 1 ; LED ein und einen Zyklus dranhängen
set pins, 0 ; LED aus
.wrap ; Let‘s do it again

Mittels .wrap legt der Entwickler einen Sprung fest, der an der mit .wrap_target markierten Stelle landet. Vorteil: Diese Anweisungen beziehungsweise Attribute benötigen keinen eigenen Befehl, sodass die Zahl der benötigten Anweisungen sich um 1 reduziert. Im obigen Beispiel liegt somit für einen Maschinenzyklus 1 und für einen Maschinenzyklus 0 an. Die LED blinkt regelmäßig.

Simples Beispiel in Python

MicroPython macht es Entwicklern leicht, mit den Möglichkeiten der PIO zu experimentieren. Programme für die Zustandsmaschine sind nicht (notwendig) in PIO-Assembler notiert, sondern verwenden Python-Wrapper. Das nachfolgende Beispielprogramm illustriert die Nutzung einer PIO-Zustandsmaschine. Das set-Mapping definiert GPIO25 als Ausgabe-Pin - es handelt sich um die eingebaute LED. Mit set(pins, 1) schaltet sich die LED ein, mit set(pins, 0) wieder aus. Mittels der Pseudoinstruktion nop()[31] legt das Programm 31 Wartezyklen ein (Delays).

# set-Mapping als Ausgabe-Pins
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)

def blink():
wrap_target()
set(pins, 1) [31] # LED an
nop() [31] # Viele Delays
nop() [31]
nop() [31]
nop() [31]
set(pins, 0) [31] # LED aus
nop() [31] # Viele Delays
nop() [31]
nop() [31]
nop() [31]
wrap()

# Zustandsmaschine 0, PIO-Programm = blink, Frequenz = 2029,
# set-Mapping mit 1 Pin: GPIO25 => eingebaute LED
sm = StateMachine(0, blink, freq=2029, set_base=Pin(25))

sm.active(1) # Zustandsmaschine starten
time.sleep_ms(3000) # 3 Sekunden warten
sm.exec('set(pins, 0)') # LED durch Senden einer PIO-Instruktion auf 0 setzen
sm.active(0) # Zustandsmaschine stoppen

Interessant ist der Aufruf von sm.exec(), mit dem sich eine Instruktion direkt an die Zustandsmaschine übertragen lässt. Im vorliegenden Fall schaltet die Instruktion die LED wieder aus, bevor das Hauptprogramm die Zustandsmaschine deaktiviert.

Beispiel Ampelsteuerung

Um das Thema PIO praktisch durchzuspielen, bieten sich vor allem Beispiele an, die weder zu trivial noch zu komplex sind. Eine mögliche Option besteht aus dem Simulieren einer Verkehrsampel, wobei natürlich auch andere Signalanlagen in Frage kämen.

In Deutschland haben Ampelsteuerungen bekanntlich vier verschiedene Phasen:

  1. Rot-Phase: Verkehrsteilnehmer müssen warten.
  2. Rot-Gelb-Phase: Verkehrsteilnehmer müssen bis zur Grün-Phase warten.
  3. Grün-Phase: Verkehrsteilnehmer können fahren.
  4. Gelb-Phase: Verkehrsteilnehmer müssen anhalten.
  5. zurück zu 1.
Die vier Phasen einer Verkehrsampel
Die vier Phasen einer Verkehrsampel

Die Phasen Grün und Rot sind individuell konfigurierbar, etwa abhängig von Verkehrsaufkommen. Die Dauer der Gelbphasen (Gelb und Rot-Gelb) ergibt sich aus der am Aufstellungsort erlaubten Höchstgeschwindigkeit.

Gesucht: ein Pico-Programm, das diese Steuerung implementiert. Die Implementierung erfolgt über C beziehungsweise C++ und PIO-Assemblerinstruktionen. Mögliche Variante: Statt C beziehungsweise C++ ließe sich auch MicroPython verwenden.
Es soll möglich sein, die Rot- beziehungsweise Grünphase von außerhalb des PIO zu konfigurieren.

Die Ampel-Schaltung

Die verschiedenen Leuchten der Ampel ersetzt die sehr einfache Beispielschaltung durch LEDs mit Anschluss an GPIO-Pins des Pico. Diese Schaltung lässt sich später erweitern, etwa durch einen Schalter, der die Ampel außer Betrieb nimmt oder wieder aktiviert. Auf einem Pico stehen zwei PIO-Blocks mit je 4 Zustandsmaschinen zur Verfügung, sodass ebenfalls eine Schaltung mehrerer Ampeln möglich wäre.
Insgesamt ist die Schaltung also sehr einfach. Die LEDs sind an GPIO17 (weisse LED oder andere Empfänger), GPIO18 (rote LED), GPIO19 (gelbe LED), und GPIO20 (grüne LED) über jeweils einen 220 Ohm Widerstand und an GND angeschlossen.

Eine einfache Schaltung für eine Ampel
Eine einfache Schaltung für eine Ampel

Ampelkontrollsoftware

Gleich vorab: Den Code für dieses Beispiel finden Sie auf GitHub [1].

Die eigentliche Steuerungsaufgabe für die vier Ampelphasen übernimmt ein PIO-Programm, dem das Hauptprogramm zu Beginn die Länge der Rot- beziehungsweise Grünphase sowie ein Bit-Pattern übergibt. Die zwei Gelbphasen (Gelb, Rot-Gelb) dauern jeweils wenige Sekunden. Da die LEDs in der Reihenfolge Rot, Gelb, Grün angeschlossen sind, ergeben sich folgende Patterns. Das Bit für die frei verwendbare Ausgabe an GPI017 fehlt in folgenden Bit-Sequenzen:

Phase 1 (Rot) lässt sich durch eine Bitsequenz wie 1-0-0 repräsentieren (rote LED an, gelbe LED aus, grüne LED aus).

Phase 2 (Rot-Gelb) entspricht dann der Bitsequenz 1-1-0.

Phase 3 (Grün) lautet 0-0-1.

Phase 4 (Gelb) entspricht 0-1-0.

Die GPIO-Pins fungieren im PIO-Programm demzufolge als Ausgabe-GPIOs. Damit dies funktioniert, benötigen wir für das entsprechende I/O-Mapping, konkret für das Output-Mapping, benachbarte GPIOs. In dem Mapping sind GPIO-Pins 17, 18 (rot), 19 (gelb), 20 (grün) im Einsatz, wobei GPIO17 mit jeder Rot-Phase ein HIGH-Signal ausgibt. Das könnte man zur Ansteuerung einer weiteren LED benutzen oder für andere Zwecke. Die Bit-Sequenzen von jeweils 4 Bits legt die sm (state machine) per out-Kommando an die LEDs. Für einen vollständigen Durchlauf aller vier Phasen sind somit 16 Bit notwendig. Da die TX-FIFO mit 32 Bit arbeitet, übergeben wir das um einen kompletten Ampelzyklus verdoppelte Pattern als 32-Bit-Wort an das OSR (Output-Shift-Register). Bis das OSR leer ist, vergehen demzufolge zwei vollständige Iterationen durch alle vier Ampelphasen. Da der Entwickler das Pattern nach Belieben definieren kann, lässt sich die Signalisierung am GPIO17 beliebig einstellen, etwa HIGH-Signal in jeder Rot-Phase oder High Signal zu Beginn aller Phasen.

Das Pattern erthält das Programm vom C-Hauptprogramm, das zuvor die Zeit für die Rot- und Grünphase an die Zustandsmaschine übergibt:

        pull       ; Zeit aus der TX-Queue ins OSR holen 
mov x,osr ; und ins Register x
mov isr,x ; sowie ins ungenutzte ISR
; kopieren
pull ; Pattern von CPU-Core aus
; der TX-Queue ins OSR holen
mov y,osr ; und zusätzlich in y speichern

Weil das Programm nach je zwei Ampelzyklen das OSR (Output-Shift-Register) komplett leer geräumt hat, ist das Pattern auch in y abgelegt, um es erneut ins OSR laden zu können:

        jmp !OSRE cont  ; OSR != EMPTY 
; => weiter zu cont
mov osr,y ; Sonst: OSR neu laden
cont:
....

Ebenfalls wird das im Programm unbenutzte ISR (Input-Shift-Register) als Backup für die Zahl der Zeitschleifen benutzt:

        mov x,isr              ; ISR enthält Schleifenzahl für x
...
lgreen:
nop [31]
jmp x-- lgreen [31] ; hier wird x in jeder
; Schleife dekrementiert

Das PIO-Assembler-Programm schiebt das OSR immer um 4 Bit nach rechts, um mit den Bits die 4 Ausgabe-Pins anzusteuern:

        out pins, 4    ; Schiebe 4 Bits aus dem OSR 
; nach rechts zu den LEDs bzw. GPIOs

So entstehen nacheinander und zyklisch die vier Ampelphasen (rot, rotgelb, grün, gelb).

.program trafficlight
pull ; Zeit von CPU-Core holen
mov x,osr ; und in x speichern
mov isr,x ; sowie im ungenutzten ISR
pull ; Pattern von CPU-Core holen
mov y,osr ; und in y speichern
.wrap_target
jmp !OSRE cont; OSR != EMPTY => weiter bei cont
mov osr,y ; Sonst: OSR leer => neu aufladen
cont:
; ROT-PHASE
mov x,isr ; ISR enthält Schleifenzahl für x
out pins,4 ; 4 Bits aus OSR zur Ausgabe
lred:
nop [31]
jmp x-- lred [31]
; ROT-GELB-PHASE
mov x,isr ; ISR enthält Schleifenzahl für x
out pins,4 ; 4 Bits aus OSR zur Ausgabe
lredyellow:
nop [10]
jmp x-- lredyellow [10]
; GRÜN-PHASE
mov x,isr ; ISR enthält Schleifenzahl für x
out pins,4 ; 4 Bits aus OSR zur Ausgabe
lgreen:
nop [31]
jmp x-- lgreen [31]
; GELB-PHASE
mov x,isr ; ISR enthält Schleifenzahl für x
out pins,4 ; 4 Bits aus OSR zur Ausgabe
lyellow:
nop [10]
jmp x-- lyellow [10]
.wrap

% c-sdk {

// Hilfsfunktion, um die Zustandsmaschine zu konfigurieren

void trafficlight_program_init(PIO pio, uint sm, uint offset,
uint pin, uint pincount, uint freq) {

for (uint i = 0; i < pincount; i++) {
pio_gpio_init(pio, (pin+i) % 32); // initialisieren aller Pins
}

// pins als Ausgabe-Pins (true) festlegen
pio_sm_set_consecutive_pindirs(pio, sm, pin, pincount, true);

// Default Configuration holen
pio_sm_config c = trafficlight_program_get_default_config(offset);

// Die vier Pins definieren das out-Mapping:
sm_config_set_out_pins(&c, pin, pincount);

// div <= 65535 (= 2^16-1) - wird hier nicht geprüft
float div = (float)clock_get_hz(clk_sys) / freq;

// Jetzt Clock Divider übergeben
sm_config_set_clkdiv(&c, div);

// Wir kombinieren beide FIFOs zu einer TX_FIFO;
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);

// Rechts-Schieber, kein auto-pull, Schwellwert: 32 Bits
sm_config_set_out_shift(&c, true, false, 32);

// Zustandsmaschine initialisieren
pio_sm_init(pio, sm, offset, &c);

// und starten
pio_sm_set_enabled(pio, sm, true);
}
%}

Am Ende des Assemblercodes befindet sich die mit % c-sdk { beziehungsweise %} geklammerte C-Funktion void trafficlight_program_init(...), die der PIO-Assembler in die generierte xxxxpio.h-Datei übernimmt.
Diese Hilfsfunktion hat mehrere Aufgaben:
Zunächst teilt sie den einzelnen GPIOs über Aufrufe von pio_gpio_init()mit, dass sie unter Kontrolle der PIO stehen.

Die Anweisung

pio_sm_set_consecutive_pindirs() 

legt fest, dass die hintereinander folgenden GPIO-Pins als Ausgabe-Pins fungieren.
Zur Konfiguration der Zustandsmaschine bedarf es einer entsprechenden Datenstruktur (offset ist die Anfangsadresse des Programmes):

pio_sm_config c = trafficlight_program_get_default_config(offset);

Nun erfolgt die Festlegung des out-Mappings:

sm_config_set_out_pins(&c, pin, pincount);

Die Zeitdauern für die verschiedenen Ampelphasen hängen von zwei Faktoren ab:
Der für die Zustandsmaschine gewählten Frequenz und den in Schleifen hinzugefügten Delays.
Die Frequenz einer Zustandsmaschine ist mittels des Clock Dividers beeinflussbar. Nimmt man mit 65535 das Maximum, liegt die Frequenz der Zustandsmaschine, wie bereits erwähnt, bei grob 2000 Hz (eigentlich 2029 Hz), was einer Zykluszeit von grob 0,5 Millisekunden entspricht. Mittels der Wahl der Delays in den PIO-Instruktionen und der von aussen übergebenen Verzögerungszeit können Entwickler die gewünschten Ampelphasen sehr gut annähern.

Um eine gewünschte Frequenz einzustellen, berechnet die Funktion einen Clock-Divider und konfiguriert damit die Zustandsmaschine:

float div = (float)clock_get_hz(clk_sys) / freq;
sm_config_set_clkdiv(&c, div);

Da das Programm die RX-FIFO nicht nutzt, verbinden wir sie mit der TX-Queue zu einer doppelt so großen TX-Queue, was eigentlich im Ampel-Beispiel nicht nötig ist, sondern lediglich zur Illustration dieser Möglichkeit dient:

sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);

Das OSR (Output-Shift-Register) schiebt die Bitmuster für die Ampelsignale nach rechts (erstes true), macht dies manuell statt automatisch (false) und hat einen Schwellwert von 32: Das heißt, nach geschobenen 32 Bits interpretiert die Zustandsmaschine das OSR als leer:

sm_config_set_out_shift(&c, true, false, 32);

Am Ende wird die Zustandsmaschine entsprechend der Konfiguration initialisiert:

pio_sm_init(pio, sm, offset, &c);

und scharf gestellt beziehungsweise gestartet:

pio_sm_set_enabled(pio, sm, true);

Nun folgt noch das in C geschriebene Hauptprogramm:

#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "hardware/clocks.h"
#include "pio_trafficlight.pio.h"

const uint freq = 2029; // Gewünschte Frequenz der Zustandsmaschine
const uint pin = 17; // Start-Pin
const uint pincount = 4; // Zahl der Ausgabe-Pins
// ==> RIGHT SHIFT ==>
// 2 x Ausgabe-Pattern für GPIOs
const uint32_t pattern = 0x48634863; // = 0100 1000 0110 0011 ...

const uint32_t delay = 200; // initial Zeitdauer für Gelb-Phasen

int main() {
setup_default_uart();

// Wir verwenden pio0
PIO pio = pio0;
// Programm zum Programmspeicher hinzufügen => offset
uint offset = pio_add_program(pio, &trafficlight_program);
printf("Programm geladen an Adresse %d\n", offset);
// Frei verfügbare Zustandsmaschine akquirieren
int sm = pio_claim_unused_sm(pio, true);
// Initialisieren und Konfigurieren der Zustandsmaschine
trafficlight_program_init(pio, sm, offset, pin, pincount, freq);

// Delay an TX-FIFO der Zustandsmaschine übergeben
pio_sm_put(pio, sm, delay);
// Bisschen warten
sleep_ms(500);
// Bitmuster zum LED-Schalten an Zustandsmaschine übergeben
pio_sm_put(pio, sm, pattern);
sleep_ms(1000000); // Lange laufen lassen
pio_sm_set_pins(pio, sm, 0); // Alle Pins auf Low
pio_sm_set_enabled(pio, sm, false); // Zustandsmaschine stoppen
}

Das Programm benutzt den Block pio0, fügt das PIO-Programm zu dessen Programmspeicher hinzu, um am Schluss eine freie Zustandsmaschine anzufordern:

PIO pio = pio0;
uint offset = pio_add_program(pio, &trafficlight_program);
int sm = pio_claim_unused_sm(pio, true);

Danach folgt der Aufruf der eben erwähnten Hilfsfunktion:

trafficlight_program_init(pio, sm, offset, pin, pincount, freq);

Erst sendet das Programm die gewünschte Zahl der Warteschleifen zur TX-Queue der Zustandsmaschine:

pio_sm_put(pio, sm, delay);

und danach das Bitmuster für die Ansteuerung der LEDs:

pio_sm_put(pio, sm, pattern);

Wichtig: Das Bit-Muster ist wegen der Right-Shifts zu den 4 GPIOs in 4er-Gruppen von rechts nach links zu lesen. Die 4er-Gruppen steuern jeweils mit dem LSBit GPIO17 (Kontrollausgabe zur beliebigen Verwendung), GPIO18 (Rot), GPIO19 (gelb) und mit dem MSBit GPIO20 (Grün). Das Muster 0x4863.... (= binär 0100 1000 0110 0011) bedeutet also die Abfolge:

  1. 0011 => GPIO17 = HIGH, RED = HIGH, YELLOW = LOW, GREEN = LOW
  2. 0110 => GPIO17 = LOW, RED = HIGH, YELLOW = HIGH, GREEN = LOW
  3. 0001 => GPIO17 = LOW, RED = LOW, YELLOW = LOW, GREEN = HIGH
  4. 0100 => GPIO17 = LOW, RED = LOW, YELLOW = HIGH, GREEN = LOW
  5. ....

Das Programm wartet nach den put()-Operationen für eine festgelegte Zeit von einer Million Millisekunden (etwa 16 Minuten), während die PIO die Ampel steuert:

sleep(1000000);

Nach dieser Wartezeit setzt es alle Ausgabe-Pins auf Low und deaktiviert die Zustandsmaschine wieder:

pio_sm_set_pins(pio, sm, 0); 
pio_sm_set_enabled(pio, sm, false);

PIO-Programmoptimierung

Mit Instruktionen sollten Entwickler gut haushalten, weil für alle vier Zustandsmaschinen eines PIO-Blocks nur insgesamt 32 Slots im gemeinsamen Programmspeicher verfügbar sind.

Wie also ließe sich das Programm optimieren? Im PIO-Teil der Ampelsteuerung durchläuft das Assemblerprogramm die vier Phasen einer Ampel. Eigentlich lässt sich das alles als zweimal zwei Phasen betrachten: je eine lange Phase (rot oder grün), gefolgt von einer kurzen Phase (gelb oder rot & gelb). Somit ist folgende gekürzte Version möglich:

.program trafficlight
pull ; Zeit von CPU-Core holen
mov x,osr ; und in x speichern
mov isr,x ; sowie im ungenutzten ISR
pull ; Pattern von CPU-Core holen
mov y,osr ; und in y speichern
.wrap_target
jmp !OSRE cont ; OSR != EMPTY => weiter bei cont
mov osr,y ; Sonst: OSR leer => neu aufladen
cont:
; ROT oder GRÜN
mov x,isr ; ISR enthält Schleifenzahl für x
out pins,4 ; 4 Bits aus OSR zur Ausgabe
lredgreen:
nop [31]
jmp x-- lredgreen [31]
; ROT-GELB oder Gelb
mov x,isr ; ISR enthält Schleifenzahl für x
out pins,4 ; 4 Bits aus OSR zur Ausgabe
lyellowred:
nop [10]
jmp x-- lyellowred [10]
.wrap

Nachteil:

Jetzt dauern Rot- und Grünphase sowie Rotgelb- und Gelbphase jeweils gleich lange, aber das ist leicht zu verschmerzen.

Die Versuchsschaltung im Einsatz
Die Versuchsschaltung im Einsatz

Fazit

Die Programmierbare Ein-/Ausgabe des Raspberry Pi Pico, kurz PIO, bietet einen echten Mehrwert für Entwickler. Natürlich ließen sich für den gleichen Zweck FPGAs nutzen, aber die sind teuer und benötigen wesentlich mehr Erfahrung in digitaler Schaltungstechnik. Gegenüber dem sonst üblichen Bit-Banging hat PIO den Vorteil, dass Zustandsmaschinen zeitlich genau arbeiten und nicht die beiden CPU-Kerne belasten. Sie offerieren einen kleinen, aber leistungsfähigen Satz von neun Instruktionen, erlauben dabei die Umsetzung auch komplexerer I/O-Schnittstellen, wofür sich im Internet bereits zahlreiche Beispiele finden. Das vorgestellte Beispiel ist bewusst einfach gehalten, sollte aber einen Einblick in die Möglichkeiten der PIO zeigen. Wie heißt es in der Werbung doch so schön: Entdecke die Möglichkeiten!

Referenzen


URL dieses Artikels:
https://www.heise.de/-6018818

Links in diesem Artikel:
[1] https://github.com/ms1963/RaspiPiPico-PIO-Trafficlight-Example
[2] https://datasheets.raspberrypi.org/rp2040/rp2040-datasheet.pdf#page331
[3] https://datasheets.raspberrypi.org/pico/raspberry-pi-pico-c-sdk.pdf#page29
[4] https://datasheets.raspberrypi.org/pico/raspberry-pi-pico-python-sdk.pdf#page18

Copyright © 2021 Heise Medien

Adblock test (Why?)

  • 19. April 2021 um 15:40

Zeit zum Aufräumen: MicroProfile 4.0

Von heise online

Zeit zum Aufräumen: MicroProfile 4.0

Neuigkeiten von der Insel Lars Röwekamp

Das aktuelle MicroProfile 4.0 hat deutlich länger auf sich warten lassen als geplant. Grund hierfür waren vor allem politische Überlegungen. Aber es gab auch die eine oder andere interessante Änderung innerhalb der APIs.

Wie bereits im ersten Teil dieses Blog-Beitrags [1] berichtet, fand im Rahmen der Spezifikation des aktuellen MicroProfile 4.0 ein grundlegendes organisatorisches Alignment statt, das zu einer fast sechsmonatigen Verzögerung des ursprünglich geplanten Erscheinungsdatums geführt hatte.

Da die Mitglieder der einzelnen API-Spezifikationsgruppen für das vorliegende Release mit der konsequenten Migration der internen Abhängigkeiten weg von Java EE 8 hin zu Jakarta EE 8 beschäftigt waren, blieb in MicroProfile 4.0 kaum Zeit und Raum für bahnbrechende Neuerungen. Aber auch wenn sich die Änderungen innerhalb der einzelnen APIs in Grenzen halten, gibt es doch die eine oder andere interessante Neuerung.

Allein an fünf (von acht) der MicroProfile-spezifischen APIs fanden Änderungen statt, die mit der Abwärtskompatibilität brechen und daher einen genaueren Blick wert sind:

  • Config 2.0
  • Open API 2.0
  • Health 3.0
  • Metrics 3.0
  • Fault Tolerance 3.0

MicroProfile Config 2.0

Mit der MicroProfile Config API können zur Laufzeit Konfigurationen aus externen Quellen herangezogen werden. Per Spezifikation muss eine MicroProfile-Config API-Implementierung mindestens die drei Quellen System Properties und Environment Variables und .properties-Datei unterstützen. Weitere externe Konfigurationsquellen, wie Datenbanken oder Cloud-basierte Key-Value-Stores, können durch Implementierung des Interfaces ConfigSource realisiert und eingebunden werden. Da die MicroProfile Config API von nahezu allen anderen MicroProfile APIs als Konfigurationsmechanismus genutzt wird, sind Änderungen an dieser API immer von besonderem Interesse.

Während in den bisherigen Versionen die Key-Value Paare der Konfigurationen jeweils einzeln aus einer der drei vorgegebenen Konfigurationsquellen beziehungsweise einer eigenen Implementierung des ConfigSource-Interfaces eingelesen werden mussten, erlaubt Version 2.0 der Config API zusätzlich einen komfortablen Bulk-Zugriff. Statt also mehrere Attribute einer Klasse mit @ConfigProperty annotieren zu müssen, reicht zukünftig die einmalige Annotation der Klasse mit @ConfigProperties, wie folgendes Beispiel aus der Config-2.0-Spezifikation [2] verdeutlicht.

Stellen wir uns einmal folgende Konfigurationsquelle vor, die unter anderem Detailinformationen zu einer Server-Konfiguration enthält:

...
server.host = localhost
server.port=9080
server.endpoint=query
server.old.location=London
...

Um die oben gezeigten Werte en bloc in eine Konfigurationsklasse einzulesen, muss diese lediglich mit @ConfigProperties annotiert und die Gruppe der einzulesenden Properties via Präfix – in diesem Fall "server" – angegeben werden:

@ConfigProperties(prefix="server")
@Dependent
public class ServerDetails {
// the value of the configuration property server.host
public String host;
// the value of the configuration property server.port

public int port;
//the value of the configuration property server.endpoint
private String endpoint;
//the value of the configuration property server.old.location
public @ConfigProperty(name="old.location")
String location;

public String getEndpoint() {
return endpoint;
}
}

Ebenfalls interessant ist die in Version 2.0 neu hinzugekommene Unterstützung von Property Expressions. Mit ihrer Hilfe ist es möglich, Konfigurationswerte innerhalb einer Konfigurationsquelle mittels Ausdrucks zu parametrisieren. Im folgenden Beispiel würde beim Einlesen der Ausdruck von server.url ausgewertet und durch den Wert von server.host ersetzt werden:

server.url=http://${server.host}/endpoint
server.host=example.org

Das Ergebnis der obigen Konfiguration wäre somit:

server.url=http://example.org/endpoint

Natürlich gelten für die Property Expressions dieselben Regeln, wie für alle anderen Konfigurationen auch. Die Konfigurationswerte können aus beliebigen Konfigurationsquellen stammen und bei Mehrfachvorkommen wird derjenige Wert herangezogen, dessen Konfigurationsquelle die höchste Ordinalität besitzt.

Neben einfachen Ausdrücken sind zusätzlich aneinandergehängte und verschachtelte Ausdrücke erlaubt. Und auch die Angabe von Default-Werten, für den Fall, dass ein Ausdruck nicht ausgewertet werden kann, ist möglich:

server.url=http://${server.host:example.org}:${server.port}/${server.endpoint}
server.port=8080
server.endpoint=${server.endpoint.path.${server.endpoint.path.bar}}
server.endpoint.path.foo=foo
server.endpoint.path.bar=foo

Im obigen Beispiel würde der Ausdruck server.host innerhalb der Konfiguration von server.url nicht aufgelöst werden können und daher durch den mittels ":" angegebenen Default-Wert example.org ersetzt werden. Der Ausdruck server.endpoint dagegen ergibt sich durch die verschachtelte Auswertung von server.endpoint.path.${server.endpoint.path.bar}, was wiederum dem Ausdruck server.endpoint.path.foo entspricht.

http://example.org:8080/foo

Was aber passiert, wenn einer der Ausdrücke nicht ausgewertet werden kann, gleichzeitig aber auch kein Default-Wert angegeben wurde? In dem Fall wird beim Einlesen des Ausdrucks eine NoSuchElementException geworfen beziehungsweise im Falle eines Optional Attribute ein leeres Optional zurückgegeben.

Weitere Details zu den Neuerungen und Änderungen finden sich in der MicroProfile-Config-2.0-Spezifikation [3] sowie im zugehörigen MicroProfile Config 2.0 GitHub Repository [4].

MicroProfile Open API 2.0

Die MicroProfile-Open-API-Spezifikation dient zur Bereitstellung von API-Beschreibungen. Mit ihrer Hilfe lassen sich mit OpenAPI v3 [5] konforme Dokumentationen der anwendungseigenen JAX-RS Schnittstellen generieren.

Innerhalb der neuen Version 2.0 der MicroProfile Open API haben hauptsächlich Aufräumarbeiten stattgefunden. So wurden zum Beispiel die beiden Model Interfaces Scope und ServerVariables, die bereits in der Version 1.1. als "deprecated" markiert wurden, endgültig entfernt. Gleiches gilt für etliche als "deprecated" markierte Methoden der Model Interfaces APIResponses, Callback, Content, OASFactory, OAuthFlow, OpenAPI, Path, PathItem, Schema, SecurityRequirement und Server. Grund für die Markierung als "veraltet" war in der Version 1.1 übrigens eine Vereinheitlichung der Namensgebung, die nun in der Version 2.0 der Open API voll zum Tragen kommt.

Neben den eben beschriebenen Aufräumarbeiten hat es vor allem Neuerungen im Bereich der Schema-Annotationen gegeben, also der Art und Weise, wie die Datentypen von Eingaben und Ausgaben definiert werden können. So können zum Beispiel dank der neu eingeführten Annotation @SchemaProperty die Properties für ein Schema inline definiert werden, was die bisher zusätzlich notwendigen Schema-Annotationen innerhalb der annotierten Klasse erspart:

@Schema(properties={
@SchemaProperty(name="creditCard", required=true),
@SchemaProperty(name="departureFlight", description="... ."),
@SchemaProperty(name="returningFlight")
})
public class Booking {
...
}

Und auch für den Request- und den Response-Body gibt es nun eigene Schema-Annotationen, um so deren Schema-Definition deutlich zu vereinfachen:

@RequestBodySchema(MyRequestObject.class)   
@APIResponseSchema(MyResponseObject.class)

Weitere Details zu den Neuerungen und Änderungen finden sich in der MicroProfile-Open-API-2.0-Spezifikation [6] sowie im zugehörigen MicroProfile Open API 2.0 GitHub Repository [7].

MicroProfile Health 3.0

Die MicroProfile Health API erlaubt die Abfrage des "Gesundheitszustands" einer Anwendung beziehungsweise eines Service mithilfe sogenannter Health Checks. Die Spezifikation unterscheidet dabei zwischen den beiden Varianten Liveness Check und Readiness Check.

Während das Ergebnis eines Liveness Check signalisiert, ob eine Anwendung zum Zeitpunkt der Anfrage läuft, kann mittels Readiness Check zusätzlich abgefragt werden, ob die Anwendung auch bereit ist, Anfragen entgegenzunehmen und zu verarbeiten.

Eine positive (UP) beziehungsweise negative (DOWN) Antwort auf einen Liveness Check hilft somit zum Beispiel, 3rd-Party-Services zu entscheiden, ob eine Anwendung in einem Zustand ist, dass sie ordnungsgemäß – also ohne Verluste von Daten – heruntergefahren beziehungsweise beendet werden kann. Eine positive oder negative Antwort auf einen Readiness Check dagegen hilft 3rd-Party-Services zu entscheiden, ob Anfragen an die Anwendung bezeihungsweise den Service weitergeleitet werden können.

Die MicroProfile-Health-Spezifikation sah bisher vor, dass ein MicroProfile-Container so lange eine negative Antwort, also ein DOWN, als Default für einen Readiness Check zurückliefern muss, bis eine selbstgeschriebene Implementierung eines HealtCheck-Interfaces vom Typ @Readiness diesen auf UP setzt. Mit der neuen Version 3.0 ist nun zusätzlich die Möglichkeit geschaffen worden, diesen Wert via Konfiguration

mp.health.default.readiness.empty.response

zu setzen, wodurch als Standardwert auch ein UP als Rückgabewert möglich ist, ohne dass eine entsprechende Implementierung vorliegen muss.

Darüber hinaus wurde die bereits seit der Version 2.0 als veraltet (deprecated) markierte @Health-Annotation auf optional (pruned) gesetzt. Das bedeutet, dass die eine oder andere Implementierung diese Annotation eventuell auch weiterhin anbieten wird, man aber keinesfalls davon ausgehen darf, dass sie weiterhin zur Verfügung steht. Dieser Schritt ergibt Sinn, da die @Health-Annotation bereits in der Version 2.0 durch die deutlich aussagekräftigeren Pendants @Liveness und @Readiness ersetzt wurde.

Weitere Details zu den Neuerungen und Änderungen finden sich in der MicroProfile-Health-3.0-Spezifikation [8] sowie im zugehörigen MicroProfile Health 3.0 GitHub Repository [9].

MicroProfile Metrics 3.0

Die MicroProfile Metrics API stellt Telemetriedaten einer Anwendung über Zeitreihen hinweg mittels entsprechenden Monitoring-Endpoint /metrics in dem OpenMetrics Text Format (aka Prometheus Exposition Format [10]) oder alternativ in Form von JSON zur Verfügung.

Die wohl augenscheinlichste Änderung innerhalb der MicroProfile-Metrics-3.0-Spezifikation ist die Überführung der ehemals abstrakten Klasse MetricRegistry hin zu einem Interface. Einhergehend mit dieser Änderung sind dem neuen Interface gleich eine ganze Reihe neuer statischer Methoden zur Registrierung beziehungsweise zum Zugriff auf die verschiedenen Metriktypen spendiert worden.

Da der Zugriff auf eine konkrete Implementierung des Interfaces für die verschiedenen Scopes (Type.APPLICATION, Type.BASE, Type.VENDOR) allerdings in der Regel via Injection vollzogen wird, hat sich in der Verwendung durch Entwickler – mit Ausnahme der neuen Methoden – allerdings kaum etwas geändert:

@Inject
@RegistryType(type=MetricRegistry.Type.APPLICATION)
MetricRegistry appRegistry

@Inject
@RegistryType(type=MetricRegistry.Type.BASE)
MetricRegistry baseRegistry;

@Inject
@RegistryType(type=MetricRegistry.Type.VENDOR)
MetricRegistry vendorRegistry;

Eine weitere Änderung innerhalb der Metrics API betrifft die gezielte Wiederverwendung von Metriken. Musste bis zur Version 2.3 eine gewünschte Wiederverwendung einer Metrik – zum Beispiel eines Aufrufzählers – über mehrere Methoden hinweg noch explizit via reusable=true bei der Registrierung der Metrik angegeben werden, ist dieses Verhalten seit der Version 3.0 Standard und kann auch nicht ausgeschaltet werden. Eine Ausnahme bildet hier lediglich die Gauge-Metrik, die weiterhin keine Wiederverwendung ein und derselben Metric-ID über mehrere Methoden hinweg erlaubt und im Falle eines Mehrfachvorkommens automatisch zu einer IllegalArgumentException beim Start der Anwendung führt.

Ebenfalls entfallen ist die Möglichkeit, die @Metric-Annotation zur Registrierung von durch CDI-Producer-Methoden erzeugten Metriken zu nutzen. Diese müssen nun über einen anderen Weg, zum Beispiel durch die Verwendung des MetricRegestry-Interfaces, registriert werden. Die @Metric-Annotation kann somit zukünftig nur noch als Injection-Point im Zusammenspiel mit der @Inject-Annotation auf Feld- oder Parameterebene verwendet werden.

Änderungen gab es auch im Bereich der Timer-Metriken, die jetzt nicht mehr mit Parametern vom Typ long und java.util.concurrent.TimeUnit, sondern stattdessen einfach mit einem Parameter vom Typ java.time.Duration aktualisiert werden können.

Weitere Details zu den Neuerungen und Änderungen finden sich in der MicroProfile-Metrics-3.0-Spezifikation [11] sowie im zugehörigen MicroProfile Metrics 3.0 GitHub Repository [12].

MicroProfile Fault Tolerance 3.0

Die MicroProfile Fault Tolerance API bietet eine Reihe etablierter Resilience-Strategien – Timeout, Retry, Fallback, Bulkhead und Circuit Breaker – zur Behandlung von Fehlersituationen innerhalb einer MicroProfile-basierten Anwendung.

Die Liste der Änderungen innerhalb der neuen Version 3.0 der MicroProfile Fault Tolerance API ist recht überschaubar und erstreckt sich im Wesentlichen auf zwei Punkte:

  • Zum einen hat es eine Klärung bezüglich des Lifecycle der beiden Fault-Tolerance-Startegien Circuit Breaker und Bulkhead gegeben. Dies Klärung sieht vor, dass beide Varianten bezüglich der zugehörigen CDI Bean als Singleton implementiert werden müssen. Ist also zum Beispiel eine Methode innerhalb einer @RequestScoped Bean mit @CircuitBreaker annotiert, so muss sichergestellt werden, dass alle Aufrufe der Methode sich – trotz unterschiedlicher Bean Instanzen – ein und denselben State des Circuit Breaker teilen.
  • Die zweite Änderung betrifft die, durch die Fault Tolerance Annotationen @Retry, @Timeout, @CircuitBreaker, @Bulkhead und @Fallback automatisch generierten Metriken. Diese finden sich zukünftig aus Gründen der Konsistenz mit den anderen MicroProfile APIs nicht mehr im Scope application: sondern im Scope base: und werden entsprechend über den Endpunkt /metrics/base statt /metrics/application zur Verfügung gestellt.

Darüber hinaus nutzen die Metriken von Fault Tolerance 3.0 zukünftig die in MicroProfile Metrics 2.0 eingeführten Tags zur genaueren Spezifizierung der jeweiligen Metrik.

Eine annotierte Klasse

package com.exmaple;

@Timeout(1000)
public class MyClass {

@Retry
public void doWork() {
// do some very important work
}

}

würde somit aufgrund der eben genannten Änderungen im Bereich der Scopes und Tags unter anderem zu folgender Metrik führen:

base:ft.timeout.calls.total{method=“com.example.MyClass.doWork“, timedOut=”true”}

statt wie bisher zu

application:ft.com.example.MyClass.doWork.timout.callsTimedOut.total

In Konsequenz bedeutet dies, dass bestehende Dashboards und Abfragen, die auf die verschiedenen Metriken der Fault-Tolerance-3.0-Annotationen zugreifen, entsprechend angepasst werden müssen.

Weitere Details zu den Neuerungen und Änderungen finden sich in der MicroProfile-Fault-Tolerance-3.0-Spezifikation [13] sowie im zugehörigen MicroProfile Fault Tolerance 3.0 GitHub Repository [14].

(M)Ein persönliches Fazit

Auch wenn die Neuerungen und Änderungen innerhalb der MicroProfile 4.0 APIs eher als überschaubar zu bezeichnen sind, haben die verschiedenen API-Spezifikationsgruppen doch die Chance des anstehenden Major-Release-Sprungs genutzt, um ein wenig innerhalb ihrer API aufzuräumen.

Besonders positiv fällt dabei ins Auge, dass alle Spezifikationsgruppen anscheinend großen Wert auf eine MicroProfile-weite und somit API-übergreifende Vereinheitlichung zu setzen. Das spiegelt sich zum Beispiel in der Umbenennung von Methoden oder aber der Namensgebung erzeugter Metriken wieder.

Ebenfalls großer Wert wurde auf eine gute Developer Experience gelegt, was sich unter anderem an der neu eingeführten @ConfigProperties-Annotation zum Bulk-Einlesen von Konfigurationsdaten innerhalb der Config-API-Spezifikation oder aber der vereinfachten Schema-Definition innerhalb der Open-API-Spezifikation zeigt.

Aus der Praxis für die Praxis. Diesem Motto ist man trotz der Einführung von Working Group und Specification Process treu geblieben – und das ist auch gut so.


URL dieses Artikels:
https://www.heise.de/-6006676

Links in diesem Artikel:
[1] https://www.heise.de/developer/artikel/MicroProfile-4-0-ein-ueberfaelliges-politisches-Statement-5064494.html
[2] https://download.eclipse.org/microprofile/microprofile-config-2.0/microprofile-config-spec-2.0.html
[3] https://download.eclipse.org/microprofile/microprofile-config-2.0/microprofile-config-spec-2.0.html
[4] https://github.com/eclipse/microprofile-config
[5] https://swagger.io/specification/
[6] https://download.eclipse.org/microprofile/microprofile-open-api-2.0/microprofile-openapi-spec-2.0.html
[7] https://github.com/eclipse/microprofile-open-api
[8] https://download.eclipse.org/microprofile/microprofile-health-3.0/microprofile-health-spec-3.0.html
[9] https://github.com/eclipse/microprofile-health
[10] https://prometheus.io/docs/instrumenting/exposition_formats
[11] https://download.eclipse.org/microprofile/microprofile-metrics-3.0/microprofile-metrics-spec-3.0.html
[12] https://github.com/eclipse/microprofile-metrics
[13] https://download.eclipse.org/microprofile/microprofile-fault-tolerance-3.0/microprofile-fault-tolerance-spec-3.0.html
[14] https://github.com/eclipse/microprofile-fault-tolerance

Copyright © 2021 Heise Medien

Adblock test (Why?)

  • 16. April 2021 um 08:22

MicroProfile 4.0 – ein überfälliges politisches Statement

Von heise online

MicroProfile 4.0 – ein überfälliges politisches Statement

Neuigkeiten von der Insel Lars Röwekamp

Einen Tag vor Weihnachten, und somit noch gerade eben so im alten Jahr, wurde MicroProfile 4.0 veröffentlicht. Offiziell war dieses Release bereits für den Sommer 2020 angekündigt. Ein grundlegendes organisatorisches Alignment sorgte aber für eine fast sechsmonatige Verzögerung der Java-Technik. Hat sich das Warten gelohnt?

Ein erster Blick auf die verschiedenen APIs von MicroProfile 4.0 zeigt zwar die eine oder andere sinnvolle Ergänzung, wirklich Neues oder Innovatives dagegen sucht man vergebens. Trotzdem musste die Community deutlich länger als sonst auf die neue Version warten. Und gleichzeitig gab es einen Sprung in der Major-Release-Version. Warum das?

Zum einen wurden die vier Java EE 8 APIs – CDI 2.0, JSON-P 1.1, JAX-RS 2.1 und JSON-B 1.0 – durch ihre gleichnamigen Pendants aus Jakarta EE 8 ersetzt. Gleiches gilt auch für die intern verwendete Annotation API 1.3. Parallel dazu wurden alle Abhängigkeiten der anderen MicroProfile APIs zu den eben genannten APIs aktualisiert.

Zum anderen, und das ist sicherlich deutlich gewichtiger, ist die aktuelle Version des MicroProfile das erste Release, das unter der Regie der neu geformten MicroProfile Working Group [1] veröffentlicht wurde und somit dem ebenfalls neu definierten MicroProfile Specification Process folgt.

Eine Initiative wird erwachsen

Als sich vor gut fünf Jahren eine Gruppe von "Interessierten" – vornehmlich Hersteller von Enterprise-Java-Application-Servern – zusammenschloss und die Initiative MicroProfile.io [2] ins Leben rief, hat wohl kaum einer von ihnen geahnt, wie erfolgreich die Reise in den kommenden Jahren werden sollte. Angetreten mit dem Ziel, einen herstellerunabhängigen De-facto-Standard für Microservices im Enterprise-Java-Universum zu etablieren, erfreut sich das MicroProfile mittlerweile einer großen Fangemeinde.

Von Beginn an zeichnete sich die Initiative MicroProfile.io durch einen leichtgewichtigen Spezifikationsprozess, Transparenz in der Kommunikation, Herstellerunabhängigkeit und einen hohen Innovationscharakter aus. Aus der Praxis für die Praxis, so die Devise, die sich unter anderem auch in dem gelebten Implementation-First-Spezifikationsansatz widerspiegelt. Das Resultat dieses Ansatzes kann sich durchaus sehen lassen: Dreizehn Releases (inkl. MicroProfile 4.0) und somit im Schnitt zwei bis drei Releases pro Jahr, umgesetzt in bis zu zehn verschiedenen Implementierungen!

Aber mit dem wachsenden Erfolg stieg auch die Verantwortung. Reichte es anfangs noch aus, die konkreten Inhalte eines Release extrem agil, das heißt mit einem minimalen Prozess-Overhead und sehr späten Entscheidungen zu definieren, wuchs mit zunehmender Verbreitung der Wunsch nach mehr Planbarkeit innerhalb der stetig wachsenden Anwender-Community.

So kommt es nicht von ungefähr, dass sich die MicroProfile.io-Initiative in den letzten Wochen vor dem Release 4.0 – nicht auch zuletzt auf den eindringlichen Wunsch der Eclipse Foundation hin – vornehmlich mit organisatorischen Herausforderungen auseinandergesetzt hat und weniger mit der Weiterentwicklung von APIs. Im Fokus standen dabei insbesondere eine Reorganisation der ursprünglichen Initiative hin zu einer Eclipse Working Group (MicroProfile Working Group, kurz MPWG), sowie die Ausarbeitung eines Spezifikationsprozesses (MicroProfile Specification Process, kurz MPSP) für alle aktuellen und zukünftigen APIs.

MicroProfile Working Group

Die Wesentliche Aufgabe der MPWG besteht darin, die fortwährende Weiterentwicklung des MicroProfile-Projekts für die kommenden Jahre sowohl technologisch als auch organisatorisch und finanziell zu sichern. Die Institutionalisierung der Working Group ist somit ein eindeutiges Signal der Eclipse Foundation an die Enterprise-Java-Community in Richtung Zukunftssicherheit und damit einhergehend eine Aufforderung, auch weiterhin auf den De-facto-Standard MicroProfile zur Entwicklung, zum Deployment und zum Management Coud-nativer Microservices zu setzen.

Den Kern der MPWG bildet ein Lenkungsausschuss, der sich aus verschiedenen Unternehmen und Java User Groups zusammensetzt (Atlanta JUG, IBM, Jelastic, Garden State JUG, Oracle, Payara, Red Hat, Fujitsu und Tomitribe sowie bald auch der iJUG). Zu seinen Aufgaben gehören neben der generellen zukünftigen Ausrichtung des MicroProfile insbesondere auch die Koordination der Weiterentwicklung der verschiedenen Spezifikationen.

Neben Unternehmen (Coorporate Members) und individuellen Committern (Commit Members) können auch durch den Lenkungsausschuss geladene Organisation (Guest Members) wie JUGs, R&D-Partner oder Universitäten temporär Teil der MPWG werden. Letztere werden dabei für ein Jahr eingeladen, um in dieser Zeit dedizierte Problemstellungen anzugehen oder Aktivitäten voranzutreiben. Anders als Cooperate Members und Commit Members haben Guest Members allerdings niemals Stimmrechte. Durch diesen Schritt soll es zukünftig möglich werden, punktuell Partner für bestimmte Fragestellungen mit ins Boot zu nehmen, ohne dabei die Working Group langfristig unnötig groß werden lassen zu müssen.

Weitere Details zur MPWG und deren Vision finden sich in der MicroProfile Working Group Charter [3].

MicroProfile Specification Process

Ein wichtiges Instrument für die Schaffung von mehr Transparenz bei der Spezifikation neuer APIs beziehungsweise der Weiterentwicklung bestehender APIs ist der zugehörige, formalisierte Prozess. Mehr Transparenz schafft eine verbesserte Planbarkeit und somit eine erhöhte Akzeptanz innerhalb der Community. Das gilt sowohl für die überschaubare Gruppe der Hersteller von MicroProfile-Implementierungen als auch für die deutlich größere Gruppe der Anwender eben dieser Implementierungen, also die Entwickler.

Während die Spezifikation neuer APIs beziehungsweise die Weiterentwicklung bestehender APIs bisher bis zum Release 3.3 eher pragmatisch und vor allem innerhalb der einzelnen APIs selbst geregelt wurden, setzt man ab Version 4.0 bewusst auf den etablierten Eclipse Foundation Specification Process v1.2 [4] (kurz: EFSP). So verwundert es auch nicht, dass der eigentliche MPSP – in gedruckter Form – mit weniger als einer DIN-A4-Seite auskommt und neben dem Verweis auf den EFSP im Wesentlichen nur die Angabe der Zeiträume zwischen den einzelnen Prozessschritten angibt (s. Abb.).

Eclipse Foundation Specification Process
Überblick "Eclipse Foundation Specification Process"

(M)Ein persönliches Fazit

Noch einmal zurück zur Ausgangsfrage: Hat sich das verhältnismäßig lange Warten auf die Version 4.0 des MicroProfile denn nun gelohnt oder eher nicht?

Betrachtet man nur die politische Komponente, dann stellt das aktuelle Release wahrscheinlich einen der wichtigsten, wenn nicht sogar den wichtigsten Meilenstein seit der Einführung des MicroProfile in der Version 1.0 im September 2016 dar.

Dank neu gegründeter MicroProfile Working Group inklusive zugehörigem Spezifikationsprozess ist das MicroProfile endgültig den Kinderschuhen entwachsen und ins Lager der Erwachsenen gewechselt. Der Prozess wurde dabei bewusst so flexibel ausgelegt, dass die bisherige, erfreuliche hohe Frequenz an Releases pro Jahr nahezu unverändert beibehalten werden kann, ohne dabei an Qualität zu verlieren.

Dass der Prozess am Ende tatsächlich auch funktionieren wird, zeigt das aktuelle Release, das als eine Art internes Proof of Concept gesehen werden kann. Denn auch wenn der Fokus des MicroProfile 4.0 eher auf dem organisatorischen Alignment lag, gibt es natürlich auch die eine oder andere Änderung an den APIs. Dazu aber mehr in meinem nächsten Blog-Beitrag.


URL dieses Artikels:
https://www.heise.de/-5064494

Links in diesem Artikel:
[1] https://microprofile.io/workinggroup/
[2] https://microprofile.io
[3] https://www.eclipse.org/org/workinggroups/microprofile-charter.php
[4] https://www.eclipse.org/projects/efsp/?version=1.2

Copyright © 2021 Heise Medien

Adblock test (Why?)

  • 09. April 2021 um 07:41

IT-Projekte: Kostenfaktor statt Wettbewerbsvorteil

Von heise online

IT-Projekte: Kostenfaktor statt Wettbewerbsvorteil

Continuous Architecture Eberhard Wolff

Lange genug hat es geheißen, dass IT-Projekte einen Wettbewerbsvorteil versprechen. Schaut man der Realität ins Auge, wird ein IT-Projekt jedoch wie ein Kostenfaktor behandelt – nicht anders als Strom oder Miete. Dabei könnte es anders sein.

Wohl alle in der IT denken, dass eine effiziente IT einen Wettbewerbsvorteil verschaffe. Daten sind bekanntermaßen das neue Öl. Digitalisierung macht alles zu Software, und wer Software beherrscht, beherrscht den Markt. IT-Projekte sind offensichtlich notwendig, um diese Vorteile tatsächlich zu realisieren.

Schauen wir uns ein typisches IT-Projekt an. Die Kosten sind praktisch immer bekannt. Die laufenden Personalkosten lassen sich relativ einfach ermitteln, die Projektlaufzeit ist ebenfalls bekannt und auch Faktoren wie Hardwarekosten werden meistens sehr genau ermittelt. Zeiten und Termine werden geschätzt, Budgets festgelegt, und wenn sie überschritten werden, wird das natürlich gemanagt. Und deutliche Überschreitungen haben ernsthafte Konsequenzen.

Business-Ziele?

Wenn die IT-Projekte einen Wettbewerbsvorteil erzeugen sollen, dann müssen die Business-Ziele ebenfalls gemanagt werden – sei es das Erschließen neuer Märkte oder das Optimieren bekannter Prozesse. Die Ziele müssen eigentlich sogar im Mittelpunkt stehen. Natürlich basieren Projekte oft auf einem Business Case, der beispielsweise in einer Präsentation festgehalten worden ist und dazu dient, ein Projekt zu rechtfertigen und zu starten. Aber das bedeutet noch lange nicht, dass die Ziele tatsächlich das Geschehen in den laufenden Projekten bestimmen.

In den verschiedenen Projekten, die man als Berater sieht, sind den Teams die Business-Ziele der Projekte nicht immer klar. In einigen Projekten gibt es beispielsweise Ziele wie einen Nutzen für Endkunden, aber sie sind dem Team nicht kommuniziert oder bestimmen nicht die tägliche Arbeit. Bei anderen Projekten bleiben die Ziele dauerhaft unklar.

Werkzeuge wie Burn-Down-Charts, die den noch verbliebenen Aufwand und damit letztlich die verbliebene Zeit und das verbleibende Budget zeigen und an alle klar kommunizieren, sind hingegen üblich. Wie gesagt: Kosten und Budgets werden eigentlich immer gemanagt. Interessanterweise ist ein Burn-Down-Chart gerade bei agilen Projekten üblich, die eigentlich versprechen, Business-Ziele besser zu erreichen.

Ein Beispiel für die Vernachlässigung von Business-Zielen können Projekte für die Migration auf eine neue Technologie sein. An den Funktionalitäten ändert sich nichts. So wird zwar ein Business-Ziel wie niedrigere Betriebskosten oder ein langfristig stabiler Betrieb ermöglicht, aber solche Projekte könnten oft zusätzlich andere Business-Ziele durch Änderungen an der Logik recht einfach umsetzen. Schließlich wird das System ja sowieso komplett umgestellt. Man kann diese Chance nutzen, um das System zu verbessern und so mehr Business-Werte zu schaffen. Das Projekt kann dadurch aber auch zu komplex und risikoreich werden. Eine solche Abwägung zwischen zusätzlichem Business-Wert und Risiko erfolgt oft aber nicht – die Kosten hingegen werden auch bei diesen Projekten überwacht.

Business-Wert?

Wenn man IT als Wettbewerbsvorteil leben will, muss man nicht nur die Business-Ziele kennen, sondern den Business Case als Geldbetrag schätzen. Wer beispielsweise durch ein Projekt mehr Umsatz oder Gewinn erwartet, kann ausrechnen, wie viel Geld das wert ist. Der Wert eines Unternehmens wird auch anhand finanzieller Kenngrößen ermittelt – warum also bei einem Projekt nicht genauso vorgehen?

Nun kann man argumentieren, dass der Business-Wert eines Projekts viel schwieriger zu ermitteln ist als das verbrauchte und das übrige Budget. Aber der Aufwand für ein Softwareprojekt ist ebenfalls schwierig zu schätzen. Eigentlich ist das sogar unmöglich, weil Softwareprojekte so komplex sind, dass sie sich einer detaillierten Planung entziehen. Deswegen nutzt man iterativ-inkrementelle Methoden, die in einzelnen kleinen Inkrementen vorgehen, die wegen der geringeren Größe einfacher abzuschätzen sind. Softwareentwicklungsteams müssen sich also darin bewähren, trotz widriger Umstände Aufwände und Budgets abzuschätzen. Ist es tatsächlich schwieriger, den Business-Wert der Projekte abzuschätzen? Zumindest probieren könnte man es, aber selbst ein solcher Versuch bleibt meistens aus. Strukturierte Ansätze, solche Werte zu ermitteln, existieren – man muss sie nur anwenden.

Außerdem kann man gerade beim Business-Wert große Überraschungen erleben: Ein Produkt, dass keine Kunden findet, ist wertlos. Daher ist es eigentlich noch wichtiger, den Business-Wert zu ermitteln und als das Budget nachzuhalten. Denn wenn man Geld für etwas Wertloses ausgibt, ist das schlicht unsinnig. Wenn man hingegen die Chance verpasst, Projekte, die bei Kunden sehr beliebt und daher wertvoll sind, besser zu unterstützen, ist das ebenso schlecht.

Der Wert eines Projekts kann dabei erheblich sein. Mir ist ein Projekt in Erinnerung, das sich bereits vor dem Start amortisiert hatte, weil der Auftraggeber in Erwartung der neuen Software andere Finanzgeschäfte abschließen konnte und dadurch das Projekt-Budget erwirtschaften konnte. Und weil Software immer wichtiger wird, steigt auch der Wert guter Software.

Hätte man eine solche finanzielle Bewertung, würden sich die Bedingungen in den Projekten ändern. Statt die Frage zu stellen, wo man Aufwand sparen kann, wird die Frage, wo man mehr Werte schaffen kann, plötzlich genauso wichtig. Es geht nicht nur um Budgetüberschreitungen, sondern auch um die geschaffenen Werte. Ohne eine solche finanzielle Kenngröße für den Business-Wert liegt es nahe, die Projekte nach den Kosten zu beurteilen und zu optimieren, weil es die einzige finanziellen Kenngrößen sind, die eigentlich immer bekannt sind.

Durchlaufzeit?

Neben dem Business-Wert eines Projekts gibt es eine weitere Kenngröße, die angeblich wichtig für IT ist: Durchlaufzeit, bis ein Feature tatsächlich in Produktion ist. Aber auch hier ist stellt sich die Frage, ob diese Größe wirklich aktiv gemanagt wird. Projekte wissen sicherlich, wie schnell sie eine Änderung in Produktion bringen können. Ob sie diese Zahl reporten und danach gemessen werden, ist eine ganz andere Frage. Außerdem gilt auch hier: Einen Wert sollte man als Geldbetrag ausdrücken können. Dazu bietet sich eine Größe an: Wie viele Kosten entstehen, wenn ein Feature sich verzögert? Man spricht vom "Cost of Delay".

Im "State of DevOps 2018 [1]"-Report gibt es dazu eine interessante Grafik von Maersk (Seite 46). Sie zeigt, dass es drei Features gibt, deren Verzögerung um eine Woche 7 Millionen US-Dollar kosten würde – vermutlich, weil Kunden abspringen oder Prozesse nicht rechtzeitig optimiert werden können. Solche Grafiken oder Aufstellungen haben in Projekten Seltenheitswert. Mit anderen Worten: Durchlaufzeit ist so unwichtig, dass man sich noch nicht einmal die Mühe macht, den möglichen Vorteil überhaupt finanziell zu bewerten.

Fazit

Es ist also üblich, die Kosten eines Projekts finanziell zu bewerten und im Auge zu behalten. Business-Ziele sind oft schlecht kommuniziert. Und es ist sehr ungewöhnlich, den Business-Wert eines Projekts tatsächlich als Geldbetrag auszudrücken. So verschiebt sich der Fokus von dem Schaffen von Werten auf die Optimierung der Kosten, die als einzige Größe wirklich bekannt sind. IT wird dann unfreiwillig und ungeplant zu einem Kosten- statt zu einem Wettbewerbsfaktor. Das muss natürlich nicht so sein: Dazu müssen aber Business-Ziele, der Business-Wert, der Business Case und damit die erzeugten Werte bekanntund idealerweise als finanzielle Kenngrößen etabliert sein.

tl;dr

Die Kosten von IT-Projekten werden typischerweise gemanagt, aber der potenziell geschaffene Wert noch nicht einmal als Geldbetrag ermittelt. So wird die IT zu einem reinen Kostenfaktor.

Vielen Dank an meine Kolleg:innen Gerrit Beine, Matthias Déjà, Anja Kammer und Stefan Tilkov für die Kommentare zu einer früheren Version des Artikels.


URL dieses Artikels:
https://www.heise.de/-6007620

Links in diesem Artikel:
[1] https://services.google.com/fh/files/misc/state-of-devops-2018.pdf

Copyright © 2021 Heise Medien

Adblock test (Why?)

  • 08. April 2021 um 11:36

Less Power, more Fun – das ESP32-trigBoard v8 von Kevin Darrah

Von heise online

Less Power, more Fun – das ESP32-trigBoard v8 von Kevin Darrah

Der Pragmatische Architekt Michael Stal

Bei mobilen eingebetteten Lösungen spielt der Energieverbrauch oft eine entscheidende Rolle. Je höher der Verbrauch, desto schneller muss das Gerät wieder an die "Tankstelle". Der ESP32-Microcontroller ist diesbezüglich nicht gerade als die sparsamste Option bekannt. Kann man trotzdem ein ESP32-Board mit Niedrigenergieverbrauch bauen? Man kann! Das trigBoard v8 lässt grüßen.

Kevin Darrah hat das trigBoard V8 entwickelt, damit ein ESP32 auch Anwendungen unterstützen kann, die niedrigen Stromverbrauch erfordern. Ursprünglich war das trigBoard für solche IoT-Geräte konzipiert, die via WiFi melden, sobald sie über einen angeschlossenen Sensor das Öffnen oder Schließen eines Fensters oder einer Tür feststellen. Das Board hat sich inzwischen als Lösung herauskristallisiert, die sich durchaus auch für Problemstellungen mit anderen Sensoren eignet.

Das trigBoard v8 hatte diverse Vorgänger, die auf dem ESP8266 basierten. Es ist aber mit dem ESP32 nicht nur ein neuer Microcontroller an Bo(a)rd, sondern diverse Verbesserungen gegenüber früheren Versionen.

Als Grundlage verwendet das trigBoard v8 ein ESP32-Modul des Typs ESP32-WROOM-32D mit 16-MByte-Flash. Um den Stromverbrauch zu minimieren, erfolgt die Spannungsversorgung nicht direkt von der Versorgungsspannung zum ESP32, sondern nimmt den Umweg über eine "Wake Circuitry"(Aufweckschaltkreis).

Vorderansicht des trigBoard v8


Rückansicht des trigBoard v8.
Rückansicht des trigBoard v8.


Die Stromstärke im Standby-Modus beträgt mit einer 3V-Spannungsquelle gerade einmal 1,5 uA. Zum Vergleich: ein handelsüblicher Rauchmelder kommt auf circa 7 uA. Eine exemplarische Modellrechnung beim Einsatz einer LiPo-Batterie mit 3,7 V und 1200 mAh Kapazität fällt folgendermaßen aus: Angenommen, das trigBoard verbraucht pro Stunde etwa fünf Sekunden lang 50 mA und wartet dazwischen im Tiefschlaf auf neue Ereignisse. In diesem Fall kommt die Batterie mehr als 1,5 Jahre ohne Neuaufladung aus.

Abgebildet sind Vorder- und Rückseite des trigBoard. Anschließen lassen sich über JST-Adapter eine Spannungsquelle und ein Sensor.
Abgebildet sind Vorder- und Rückseite des trigBoard. Anschließen lassen sich über JST-Adapter eine Spannungsquelle und ein Sensor.


Wie erreicht das trigBoard dieses Ziel? Grundsätzlich verfügt das Board neben dem ESP32 über sechs Funktionsblöcke:

  • eine Echtzeituhr des Typs RV-8263-C7 mit sehr niedrigem Energiebedarf, die nach einem Timeout prüfen kann, ob der Sensorkontakt geöffnet oder geschlossen wurde. Des Weiteren hilft sie bei der periodischen Überprüfung der Batteriespannung und kann in diesem Zusammenhang auch Meldungen versenden. Sie lässt sich zudem einsetzen, um regelmäßig Sensordaten zu lesen und zu versenden.
  • eine geschaltete Spannungsversorgung, die ausschließlich bei erkannten Ereignissen/Triggern auf 3,3 V schaltet, wodurch der Stromverbrauch gering bleibt.
  • eine ständig aktive 4,2-V-Betriebsspannung, die Sensorschaltkreise und Echtzeituhr versorgt.
  • ein pulsierend betriebener Überwachungsschaltkreis, der die Sensorzustände beobachtet.
  • ein Auto-Reset-Schaltkreis, der das einfache Programmieren des trigBoard über ein USB-Serial-Board ermöglicht – hierfür bietet Kevin Darrah eine eigene, ebenfalls energieeffiziente Lösung.
  • eine Batteriespannungsmessung, die bei der Fabrikation des Boards getestet und mit einem Korrekturoffset versehen wird.
Die sechs Funktionsblöcke des trigBoard v8
Die sechs Funktionsblöcke des trigBoard v8

Die folgenden Pins sind Teil dieser Funktionalitäten und lassen sich vom Nutzer für eigene Zwecke konfigurieren:

  • IO36-AINPUT-Batterie-Pin: dient zur analogen Messung der Batteriespannung.
  • IO0-OUTPUT-LED-Pin: schaltet die LED bei einem Low-Signal ein. Sollte mit Bedacht genutzt werden, da auch der Auto-Reset-Schaltkreis den Pin verwendet.
  • IO16-OUTPUT-Power-Latch-Pin: Dieser Pin lässt sich auf High schalten, um die Spannungsversorgung des ESP32 aufrechtzuerhalten. Sobald das Board aufwacht, schaltet das System den Pin hoch, um die Versorgung zu gewährleisten, während das Board die Ursache des Erwachens ermittelt. Sobald das trigBoard wieder in den Tiefschlaf fallen soll, lässt sich das Signal am Pin auf Low setzen.
  • IO17-OUTPUT-Kill-Power-Pin beziehungsweise UnLatch-Pin: Der Sensoreingang nutzt zwei Kontakte für Schalter (normal geöffnet, normal geschlossen). Wacht das Board auf, kann die Firmware die Signale an den Kontakten einlesen, um sie dann wieder durch Schreiben eines High-Signals zu deaktivieren.
  • IO18-INPUT-Contact-Opened-Latch-Pin: Ist das Signal Low, hat sich der Schalter am Sensoreingang von geschlossen auf offen geändert.
  • IO19-INPUT-Contact-Closed-Latch-Pin: Ist das Signal Low, hat sich der Schalter am Sensoreingang von offen auf geschlossen geändert.
  • IO23-INPUT-Contact-Status-Pin: Echtzeitstatus des Kontakts (HIGH => geöffnet, LOW => geschlossen).
  • IO27-INPUT-Wake-Button-Pin: LOW => Knopf wurde betätigt.
  • IO21 & IO22: Genutzt von der an I2C-Adresse 0x51 liegenden Echtzeituhr.

Spannungsseitig läuft das Board mit Versorgungsspannungen zwischen 1,8 V und 5 V. Die empfohlene Betriebsspannung beträgt jedoch zwischen 2,5 V und 5 V. Das trigBoard prüft beim Einlegen von Batterien deren Polarität und erleidet in den meisten Fällen keinen Schaden, sollten Batterien falsch herum angeschlossen werden.

Wichtige Pins des trigBoard sind nach außen gelegt. Durch Anlöten von Header-Leisten können Entwickler das Board prototypisch auf einem Breadboard einsetzen oder alternativ Anschlussklemmen installieren. Auf der Webseite für das trigBoard [1] gibt es außerdem STL [2]-Dateien, um per 3D-Drucker ein geeignetes Gehäuse zu erzeugen.

Firmware

Die bereitgestellte Firmware [3] unterstützt mehrere Internetdienste, um Push-Meldungen zu versenden, unter anderem IFTTT, Pushsafer und Pushover. Ebenso sind eigene Lösungen möglich, die auf den Kommunikationsprotokollen TCP, UDP oder MQTT basieren. Meldungen können dabei auch mehrere Wege nehmen, zum Beispiel einen über MQTT, einen anderen über IFTTT. Notiz am Rande: Sogar der "Weck"-Knopf am Board ist optional in der Lage, das Versenden von Push-Meldungen zu initiieren.

Für das Bereitstellen neuer Firmware implementiert das Board OTA-Downloads (Over the Air). Das ist sinnvoll, weil niemand willens wäre, alle vorhandenen Boards mit einem Notebook auf den neuesten Stand zu bringen, obwohl auch das problemlos machbar ist.

Der Weckdienst

Was muss genau passieren, damit das trigBoard aus seinem Dornröschenschlaf erwacht? Entsprechende Trigger lassen sich rein softwareseitig konfigurieren. Ein auslösender Trigger könnte beispielsweise darin bestehen, dass ein Kontakt von offen auf geschlossen wechselt oder dass der Kontakt sich von geschlossen auf offen ändert. Möglich ist auch die Kombination beider Fälle, etwa "Garagentor hat sich geöffnet" und später "Garagentor wurde geschlossen". Das Aufwecken des Boards erfordert einen Abfall oder Anstieg der Spannung am Sensoreingang um 3 V. Der Zielzustand muss mindestens 200 Millisekunden anliegen, um das Board zu wecken. Da sich dieses Verhalten nicht für alle Sensoren realisieren lässt, beispielsweise für CO/Rauchdetektoren oder PIR-Sensoren, können sich Entwickler mit einem MOS-FET-Transistor oder anderen Schaltungen behelfen (sogenannter Hair-Trigger).

Eine möglichst exakte Zeitnehmung erweist sich in vielen Anwendungsfällen als relevant, weshalb das ESP32-Board eine genaue Echtzeituhr (RTC) integriert, die das Aufwecken des Boards aus dem Tiefschlaf erlaubt. Dabei kann das Board zum Beispiel periodisch die Spannungsquelle auf Restkapazität prüfen und gegebenenfalls eine Alarmmeldung versenden, sobald die Spannung unter einen bestimmten Pegel fällt.

Ein Problem durch das Design für niedrigen Energieverbrauch resultiert aus Szenarien wie dem folgenden: Jemand läuft aus dem Haus, öffnet die Tür und schließt sie gleich wieder hinter sich. In diesem Fall könnte es sein, dass das trigBoard zwar das Öffnen der Tür bemerkt, nicht aber das Schließen. Als Ausweg kann ein periodischer Alarm der Echtzeituhr fungieren, der den aktuellen Status der Tür detektiert, also ob sie nach wie vor offen oder geschlossen ist.

Konfiguration

Für die Anpassung des trigBoards an eigene Bedürfnisse kombinieren Nutzer Google Chrome mit einem Konfigurationswerkzeug, um alle Boardparameter und -einstellungen auf einem per Bluetooth angeschlossenen trigBoard durchzuführen. Als Basis fungiert dabei ein Host mit macOS, Linux oder Windows 10. Das Konfigurationsprogramm [4] ist über GitHub beziehbar.

Der hauptsächlich in Processing-Code geschriebene Configurator läuft auf mac OS, Linux oder Windows 10 unter dem Chrome-Browser.
Der hauptsächlich in Processing-Code geschriebene Configurator läuft auf mac OS, Linux oder Windows 10 unter dem Chrome-Browser.


Der Entwickler oder Nutzer kann im Configurator jede Änderung der Konfiguration vornehmen.
Der Entwickler oder Nutzer kann im Configurator jede Änderung der Konfiguration vornehmen.


Softwareentwicklung für das trigBoard

Die Software für das trigBoard entstand unter der Arduino IDE mit dem entsprechenden Boardsmanager [5]. Es handelt sich um ein ESP32 Dev Module mit folgenden Einstellungen in der IDE:

  • Upload Speed: 921600
  • CPU Frequency: 80 MHz (WiFi/BT)
  • Flash Frequeny: 80 Mhz
  • Flash Mode: QIO
  • Flash Size: 16 MByte (128 Mb)
  • Partition Scheme: Minimal SPIFFS (1,9 MByte App with OTA/190 KB SPIFFS)
  • Core Debug Level: None
  • PS-RAM: Disabled

Benutzt haben Firmware-Entwickler die Arduino IDE Version 1.8.10. Zusätzlich benötigt die Firmware

  • ESP32 Arduino Core v1.0.4
  • die MQTT-Bibliothek PubSubClient v2.7.0
  • die Arduino-Json-Bibliothek v6.13.0
  • die PushSafer-Arduino-Library auf GitHub
Eigene Adaptionen der Firmware sind über die Arduino-IDE möglich.
Eigene Adaptionen der Firmware sind über die Arduino-IDE möglich.

Testequipment

Um eigene Experimente durchzuführen, habe ich folgende Komponenten eingesetzt:

  • trigBoard v8
  • Batteriehalter für zwei AAA-Batterien zur Stromversorgung des trigBoards (sowohl der Batteriehalter als auch der Kontakt/Magnet sind für den Anschluss an das trigBoard mit JST-Steckverbindern ausgestattet)
  • eine FTDI-Schnittstelle, um das trigBoard über USB mit dem Computer (macOS, Linux, Windows 10) zu verbinden
  • ein USB-A nach Micro-USB-Kabel
  • einen Aleph-PS-1523WG-Kontakt plus Magnet (siehe Bild unten), was erlaubt, das Öffnen oder Schließen von Türen, Fenstern und Ähnlichem zu testen

Der folgende Versuchsaufbau stellt fest, ob eine Tür (oder ein Fenster) geöffnet oder geschlossen wird:

Bei einem eigenen Experiment prüft das trigBoard mittels Magnetkontakten, ob eine Tür geöffnet oder geschlossen wird.
Bei einem eigenen Experiment prüft das trigBoard mittels Magnetkontakten, ob eine Tür geöffnet oder geschlossen wird.

Das trigBoard ist so konfiguriert, dass es Türöffnen oder Türschließen über den Dienst Pushover meldet:

Der Dienst Pushover läuft auf iOS- und Android-Apps. Er empfängt die Meldungen des trigBoard und leitet sie weiter.
Der Dienst Pushover läuft auf iOS- und Android-Apps. Er empfängt die Meldungen des trigBoard und leitet sie weiter.

Zusammenfassung

Es geht also doch, wenn ein Elektronikexperte Hand anlegt. Das trigBoard v8 verbindet die Vorteile des ESP32 mit denen des Niedrigenergiebetriebs. Sicherlich ist das trigBoard kein Mittel für alle Fälle, sondern speziell darauf getrimmt, Lösungen mit geringem Stromverbrauch zu ermöglichen. Genau das macht es, und zwar nicht nur richtig, sondern richtig gut. Mit umgerechnet 25,29 Euro (US-$ 29,99) gehört das Board mit Sicherheit nicht zu den Schnäppchen (Bezugsquelle: tindie [6]), aber – den entsprechenden Einsatzzweck vorausgesetzt – ist es auf jeden Fall sein Geld wert.

Ich möchte Kevin Darrah an dieser Stelle dafür danken, dass er diesem Blog ein Testexemplar des trigBoards bereitgestellt hat.


URL dieses Artikels:
https://www.heise.de/-6005505

Links in diesem Artikel:
[1] https://trigboard-docs.readthedocs.io/en/latest/index.html#
[2] https://de.wikipedia.org/wiki/STL-Schnittstelle
[3] https://github.com/krdarrah/trigBoardv8_basefirmware/
[4] https://github.com/krdarrah/trigBoardConfigurator/
[5] https://dl.espressif.com/dl/package_esp32_index.json
[6] https://www.tindie.com/products/kdcircuits/trigboard-ultra-low-power-esp32-iot-platform/

Copyright © 2021 Heise Medien

Adblock test (Why?)

  • 06. April 2021 um 08:00

Des Prozessors Kern – Parallelisierung auf dem Raspberry Pi Pico mit C und C++

Von heise online

Des Prozessors Kern – Parallelisierung auf dem Raspberry Pi Pico mit C und C++

Der Pragmatische Architekt Michael Stal

Leistungsstarke Prozessoren wie der im Pico verwendete ARM-M0+ haben mehrere Kerne. Dadurch ist die parallele Abarbeitung verschiedener Aufgaben durch Threads möglich. Von sehr seltenen Situationen abgesehen, müssen sich Threads miteinander synchronisieren, sobald sie auf gemeinsame Ressourcen zugreifen. Der vorliegende Beitrag erläutert, welche Mechanismen das Pico-SDK dafür bereitstellt und wie sie Entwickler einsetzen können.

Der Mikrocontroller des Raspberry Pi Pico besitzt zwei Rechenkerne, genannt Core0 und Core1, die eine Parallelisierung über Threads ermöglichen.

Während das Hauptprogramm, egal ob bei Verwendung von Python oder C beziehungsweise C++, im Kern "Core0" läuft, steht der Kern "Core1" für einen zweiten Thread zur Verfügung. Dafür gibt es im Pico-SDK die Funktion multicore_launch_core1(), die als Argument eine Funktion ohne Rückgabewert erwartet. Diese Funktion enthält dementsprechend den vom zweiten Thread durchlaufenen Code. In MicroPython erfolgt der Start eines zweiten Threads übrigens über die Bibliotheksfunktion _thread.start_new_thread().

Das Pico-SDK erlaubt nur einen Thread pro Rechenkern, insgesamt also zwei in einem Programm. Um mehrere Threads pro Kern zu verwalten, bräuchte es ein geeignetes Betriebssystem, das Kernel-Komponenten für Task-Scheduling und Prioritätsmanagement umfasst.

Für Pico-Entwickler könnte sich alles durch das Echtzeitbetriebssystems FreeRTOS ändern, das in Zukunft auch den RP2040 unterstützen soll.

FIFO-Schlangen

Multithreading macht im Allgemeinen nur dann Sinn, wenn Threads in der Lage sind, sich miteinander zu koordinieren. Zu diesem Zweck existiert für jeden Kern eine Warteschlange (FIFO), in die ein Thread eines Kerns Nachrichten platziert (push) und von der der Thread des anderen Kerns Nachrichten lesen (pop) kann. Folglich gibt es insgesamt zwei Warteschlangen. Lesen geschieht über Funktionen wie multicore_fifo_pop_blocking(), Schreiben über Funktionen wie multicore_fifo_push_blocking().

Das "blocking" besagt, dass der jeweilige Thread so lange warten muss, bis er eine Nachricht lesen kann – das heißt, es sind Nachrichten vorhanden – beziehungsweise eine Nachricht schreiben kann – das heißt, die Warteschlange ist nicht voll.

Das C-Programmbeispiel unten instanziiert zwei Threads, nämlich den im Kern laufenden Main-Thread, und einen im Kern 1 laufenden Thread, der die Funktion playerOne() abarbeitet. Beide Threads spielen miteinander ein simples Würfelspiel mit zwei Würfeln. Die Regeln sind denkbar einfach: Wer die höhere Augensumme erzielt, gewinnt. Dafür nutzt das Programm die rand()-Funktion zur Würfelsimulation. Der Ausdruck rand() % 6 + 1 liefert einen ganzzahligen Wert zwischen 1 und 6. Der initiale Seed lautet im Programmbeispiel 42, was im Normalfall keine gute Idee ist, aber für den illustrativen Zweck genügt.

Das Ergebnis als Summe ihrer zwei Würfel übermitteln die Threads blockierend über die Warteschlange an den Gegenspieler. In jeder Spielrunde passiert Folgendes: Spieler 2 (main()-Funktion im Kern 0) würfelt, und schreibt sein Ergebnis blockierend in die Warteschlange. Spieler 1 liest das Ergebnis ebenfalls blockierend aus derselben Warteschlange.

Dann dreht sich das Verfahren um: Jetzt schreibt Spieler 1 (Kern 1) die eigene Augenzahl blockierend, während sie Spieler 2 (Kern 0) blockierend liest. Nur durch diese Reihenfolge lässt sich ein Deadlock verhindern.

#include <stdio.h>
#include "pico/stdlib.h"
#include "pico/multicore.h"

void playerOne() {
uint32_t my_result; // own sum of dice
uint32_t opponent_result; // opponent's result
uint32_t dice1, dice2;
while (true) {
dice1 = rand() % 6 + 1;
dice2 = rand() % 6 + 1;
printf("Player 1: I got %d and %d\n", dice1, dice2);
my_result = dice1 + dice2;
opponent_result = multicore_fifo_pop_blocking(); // obtain result from player 1
sleep_ms(1000);
multicore_fifo_push_blocking(my_result); // send own result to player 1
sleep_ms(1000);
if (my_result > opponent_result) {
puts("Player 1: I won :-)");
}
else
if (my_result < opponent_result) {
puts("Player 1: I lost :-(");
}
else
puts("Player 1: Draw :-!");

}
}

int main() { // player 2
stdio_init_all();
int32_t seed = 42; // note: this is a bad seed in general. Use time()-function instead
srand(seed); // initialize random number generator
uint32_t dice1, dice2; // own dice
multicore_launch_core1(playerOne); // start player One as opponent
uint32_t my_result; // own sum of dice
uint32_t opponent_result; // opponent's result
while (true) {
dice1 = rand() % 6 + 1;
dice2 = rand() % 6 + 1;
my_result = dice1 + dice2;
printf("Player 2: I got %d and %d\n", dice1, dice2);
multicore_fifo_push_blocking(my_result); // send own result to player 2
sleep_ms(1000);
opponent_result = multicore_fifo_pop_blocking(); // obtain result from player 2
sleep_ms(1000);
if (my_result > opponent_result) {
puts("Player 2: I won :-)");
}
else
if (my_result < opponent_result) {
puts("Player 2: I lost :-(");
}
else
puts("Player 2: Draw :-!");

}
return 0;
}

Zum Beobachten des Spielverlaufs kommt ein serieller Monitor zum Einsatz. Auf dem Mac benutze ich dafür die (kostenpflichtige) App Serial 2 von decisivetactics. Natürlich tut es jeder andere serielle Monitor auch.

Die Würfel sind gefallen
Die Würfel sind gefallen

Das Pico-Würfelspiel-Programm definiert einen fest vorgegebenen Seed für die Zufallsverteilung. Für eine echte Anwendung würde man natürlich einen zufälligen Seed nutzen. Andernfalls wäre die Zufallsverteilung vorhersagbar. Um den Seed zu ermitteln, können Entwickler zum Beispiel die Echtzeituhr (RTC) des Pico einsetzen. Im nachfolgenden Programmbeispiel geben Entwickler das aktuelle Datum und die aktuelle Zeit fest in den Code ein (Datenstruktur datetime_t) und können damit die Echtzeituhr initialisieren.

Aus der von rtc_get_datetime() zurückgelieferten Struktur lässt sich damit ein geeigneter Seed berechnen, etwa durch Addieren aller Datums- und Zeitwerte bei vorhergehender Normalisierung des Jahres (nur die letzten zwei Ziffern des Jahres finden Berücksichtigung).

#include <stdio.h>
#include "hardware/rtc.h"
#include "pico/stdlib.h"
#include "pico/util/datetime.h"

int main() {
stdio_init_all();

char datetime_buf[256];
char *datetime_str = &datetime_buf[0];

// Start jetzt: am 23.3.2021 um 16:29
datetime_t t = {
.year = 2021,
.month = 03,
.day = 23,
.dotw = 2, // 0 = Sonntag, 1 = Montag, 2 = Dienstag, ...
.hour = 16,
.min = 29,
.sec = 00
};
// Initialisieren der Echtzeituhr
rtc_init();
rtc_set_datetime(&t);

// ……………… weiterer Code ………………

rtc_get_datetime(&t);
datetime_to_str(datetime_str, sizeof(datetime_buf), &t);
printf("\r%s ", datetime_str);

// ……………… weiterer Code ………………

}

Die bisherigen Funktionen für Multithreading beziehungsweise Thread-Management stellen nur die Spitze des Eisbergs dar. In der zugehörigen Bibliothek gibt es weitere Möglichkeiten.

Die in der Pico-SDK angebotenen Funktionen für Multithreading
Die in der Pico-SDK angebotenen Funktionen für Multithreading

Das Würfel-Programm nutzt eine der Funktionen zum Start eines Threads in Kern 1 namens multicore_launch_core1().

Zusätzlich existiert die Möglichkeit, den Thread auch mittels multicore_launch_core1_with_stack() zu starten, wobei diese Funktion als Argumente einen Zeiger auf die auszuführende Funktion sowie die Adresse eines benutzerdefinierten Laufzeitstacks und dessen Startadresse erwartet. Eine weitere Funktion multicore_launch_core1_raw() startet Kern 1 neu, führt darauf die als erstes Argument angegebene Funktion aus. Wichtig: Hier gibt es allerdings keinen Stack Guard.

Es liegt in der Verantwortung der Programmierer, dass der Stack nicht überläuft. Schließlich existiert noch eine Funktion multicore_reset_core1(), die Kern 1 zurücksetzt.

FIFO-Queues

FIFO-Warteschlangen zur Inter-Core-Kommunikation besitzen acht Einträge zu je 32 Bit. Für das Verwenden dieser FIFO-Warteschlangen gibt es ebenfalls noch weitere Funktionen in der Bibliothek.

multicore_fifo_rvalid() prüft nichtblockierend, ob Daten zum Lesen bereits sind. Falls ja, kann ein Thread beim Vorliegen von Einträgen mit multicore_fifo_pop_blocking() die Daten beziehen. Liegen keine Daten vor, kann er sich inzwischen um andere Angelegenheiten kümmern und später erneut den Zugriff versuchen.

Für das Schreiben von Daten gibt es analog das nichtblockierende multicore_fifo_wready(), dem ein multicore_fifo_push_blocking() folgt. Mittels multicore_fifo_status() ermitteln Programmierer, ob die Warteschlange nicht voll (->1) oder nicht leer ist (-> 0) beziehungsweise ob es einen Versuch gab, die leere FIFO zu lesen (->3) oder auf die volle FIFO zu schreiben (->2).

Funktionalität für FIFOs
Funktionalität für FIFOs

Mr. and Mrs. Queue

Übrigens liegt in der Bibliothek pico_util (Pico-SDK) auch der Datentyp Queue vor (siehe hier [1]), der allgemeiner als die oben verwendete FIFO-Queue, aber ebenfalls Thread-sicher ist. "Allgemeiner" soll heißen, dass Queues keine Längenbeschränkung besitzen und beliebige Arten von Elementen enthalten können. Dieser Datentyp aus der Bibliothek lässt sich entsprechend als Alternative zu den systemnahen FIFO-Queues nutzen.

Hier sehen wir das auf die Thread-sicheren Queues umgestellte Programmbeispiel. Gegenüber obigen Beispiel hat sich nicht viel geändert. Es läuft, semantisch gesehen, analog ab:

#include <stdio.h>
#include "pico/stdlib.h"
#include "pico/util/queue.h"
#include "pico/multicore.h"

typedef struct
{
uint32_t dice1;
uint32_t dice2;
} queue_entry_t;

queue_t playerOneQueue; // queue used by playerOne to send results
queue_t playerTwoQueue; // queue used by playerTwo to send results

void playerOne() {
queue_entry_t playerTwoResult, playerOneResult;

while (true) {
sleep_ms(1000);
playerOneResult.dice1 = rand() % 6 + 1;
playerOneResult.dice2 = rand() % 6 + 1;

printf("Player 1: I got %d and %d\n", playerOneResult.dice1, playerOneResult.dice2);
queue_remove_blocking(&playerTwoQueue, &playerTwoResult); // obtain result from player 2
sleep_ms(1000);
queue_add_blocking(&playerOneQueue, &playerOneResult);
sleep_ms(1000);
if ((playerTwoResult.dice1 + playerTwoResult.dice2) < (playerOneResult.dice1 + playerOneResult.dice2)) {
puts("Player 1: I won :-)");
}
else
if ((playerTwoResult.dice1 + playerTwoResult.dice2) > (playerOneResult.dice1 + playerOneResult.dice2)) {
puts("Player 1: I lost :-(");
}
else
puts("Player 1: draw :-!");
}
}

void playerTwo() { // player 2
queue_entry_t playerTwoResult, playerOneResult;

while (true) {
playerTwoResult.dice1 = rand() % 6 + 1;
playerTwoResult.dice2 = rand() % 6 + 1;

printf("Player 2: I got %d and %d\n", playerTwoResult.dice1, playerTwoResult.dice2);
queue_add_blocking(&playerTwoQueue, &playerTwoResult);
sleep_ms(1000);
queue_remove_blocking(&playerOneQueue, &playerOneResult); // obtain result from player 1
sleep_ms(1000);

if ((playerTwoResult.dice1 + playerTwoResult.dice2) > (playerOneResult.dice1 + playerOneResult.dice2)) {
puts("Player 2: I won :-)");
}
else
if ((playerTwoResult.dice1 + playerTwoResult.dice2) < (playerOneResult.dice1 + playerOneResult.dice2)) {
puts("Player 2: I lost :-(");
}
else
puts("Player 2: draw :-!");
}
}

int main() {
stdio_init_all();

int32_t seed = 42; // note: this is a bad seed in general. Use time()-function instead
srand(seed); // initialize random number generator
queue_init(&playerOneQueue, sizeof(queue_entry_t), 2); // initialize queues
queue_init(&playerTwoQueue, sizeof(queue_entry_t), 2);
multicore_launch_core1(playerOne); // start player One as opponent
playerTwo(); // start player Two

return 0;
}

Ein Hinweis, wenn statt C die Programmiersprache C++ zum Einsatz kommen sollte: In diesem Fall ergeben sich beim abgedruckten Beispiel Fehlermeldungen während des Kompilierens und Bindens. Daher müssen die betreffenden Include-Anweisungen im Code wie etwa

#include "pico/util/queue.h"

folgendermaßen "umklammert" werden:

extern "C" {
#include "pico/util/queue.h"
}

Queues besitzen nicht nur die oben benutzten Funktionen, sondern auch weitere, etwa solche zum nichtblockierenden Prüfen wie queue_is_empty(), queue_is_full(), queue_try_peek() und Funktionalität zum nichtblockierenden Hinzufügen oder Entnehmen von Elementen wie queue_try_add() oder queue_try_remove().

Der abstrakte Datentyp Queue aus dem Pico-SDK ist thread-sicher
Der abstrakte Datentyp Queue aus dem Pico-SDK ist thread-sicher

Do it yourself

Falls Entwickler selbst Thread-sichere Datentypen wie Queues erstellen wollen, benötigen sie dafür geeignete Mechanismen. Zur Synchronisation von Threads stellt die Bibliothek pico_sync des Pico-SDKs verschiedene Primitive bereit, insbesondere Mutexes, Semaphores und Critical Sections. Ein wichtiger Hinweis in diesem Zusammenhang: Die vorgestellten Mechanismen schützen bei oberflächlicher Betrachtung Teile des Programmes vor parallelem Betreten. Das ist aber nicht ihr eigentlicher Zweck. Ihr Zweck ist es vielmehr, gemeinsame Ressourcen vor inkonsistenten Änderungen abzusichern und inkonsistente Sichten zu verhindern. Unter Ressourcen sind Speicherbereiche zu verstehen, aber auch andere gemeinsam genutzte Entitäten wie serielle Schnittstellen, Sensoren oder Aktuatoren.

Kritische Bereiche

Kritische Bereiche (Critical Sections) basieren auf Spinlocks. Diese sind im PICO-SDK systemnah implementiert und nutzen ARM-Maschineninstruktionen. Sie stellen akquirierbare Ressourcen dar, auf die ein Thread in einer Schleife so lange aktiv warten muss, bis der Spinlock nicht mehr belegt ist. Daher auch der Name – der Thread "spint" also in gewisser Weise. Nach Beendigung seiner Tätigkeit gibt er den Spinlock wieder frei. Weil die Akquisition eines Spinlocks wie die jedes Synchronisationsprimitive mit aktivem Warten verbunden ist, sollte ihn jeder Thread nur sehr kurz behalten und auch schnell wieder freigeben. Insbesondere sollten alle hier vorgestellten Synchronisationsmechanismen nicht oder nur in sehr wenigen und vor allem nur in berechtigten Ausnahmefällen innerhalb von Interrupt-Handlern auftauchen.

Kritische Bereiche definieren Programmteile, die immer nur ein Thread zu einem Zeitpunkt exklusiv durchlaufen kann, nämlich der, der gerade im Besitz des zugehörigen Spinlocks ist. Sie erinnern sich sicher noch an den Begriff Mutual Exclusion (= gegenseitiger Ausschluss), der diesen Sachverhalt beschreibt. Sinn des kritischen Bereichs ist der exklusive Zugriff auf eine geschützte Ressource durch maximal einen Thread. Das Betreten eines kritischen Bereichs erfolgt mittels

critical_section_enter_blocking()

und dessen Verlassen mit

critical_section_exit()

In diesen Funktionen läuft dementsprechend die Akquisition eines Spinlocks beziehungswesie dessen Freigabe ab. Das Beispiel der Critical Sections zeigt, dass sich letztlich alle Synchronisationsmechanismen tief im Inneren auf atomare Operationen wie das exklusive Prüfen und Schreiben (Test-and-Set) von Variablen zurückführen lassen. Innerhalb des kritischen Bereichs greift der jeweilige Thread exklusiv auf gemeinsame Ressourcen zu. Insofern fasst der kritische Bereich die in ihm durchgeführten Aktivitäten zu einer atomaren Operation zusammen.

Die Funktionen der Critical Sections gestatten das Anlegen geschützter kritischer Codeabschnitte
Die Funktionen der Critical Sections gestatten das Anlegen geschützter kritischer Codeabschnitte

Im dazugehörigen Programmbeispiel schreiben zwei Threads über die Methode changeText() immer denselben Text in die Variable buffer. Der Thread in Core 0 (Funktion: entry_core0()) schreibt 00000, während der Thread in Core 1 (Funktion: entry_core1()) 11111 schreibt. Ohne den kritischen Bereich zwischen

critical_section_enter_blocking()

und

critical_section_exit() (siehe changeText())

kommen sich die Threads beim Schreiben von buffer in die Quere, sodass dessen Inhalt bisweilen auch Werte wie 00110 oder 01000 statt immer nur entweder 00000 oder 11111 annimmt. Probieren Sie das einfach mal selbst aus, indem Sie die Funktionen für kritische Abschnitte in Kommentare setzen und an interessanten Stellen Ausgaben auf die serielle stdio-Schnittstelle schreiben.

Erklärung für das beobachtete Phänomen: Die Buchstaben des Texts werden in der Funktion changeText() einzeln geschrieben und jeweils mit einer zufälligen Wartezeit abgeschlossen, um reale Situationen zu simulieren. In der "echten" Welt würde jeder Thread hier nützliche Schritte durchführen, etwa das Einlesen von Sensorwerten oder die Ansteuerung angeschlossener Komponenten. Der Sachverhalt bleibt der gleiche. Nur durch Schutzmechanismen ist zu gewähleisten, dass ein Thread seine Aktivitäten ausführen kann, ohne dass ihm ein anderer Thread dazwischenfunkt. Viele Köche verderben den Brei.

Das Initialisieren eines kritischen Bereichs erfordert entweder die Funktion critical_section_init() oder critical_section_init_with_lock_num(). In ersterer Funktion stellt das Laufzeitsystem implizit einen unsichtbaren Spinlock zur Verfügung. Der letzteren Funktion können Programmierer explizit einen eigenen Spinlock übergeben.

#include "pico/stdlib.h"
#include "pico/multicore.h"
#include "pico/sync.h"
#include <string.h>
#include <stdio.h>

// Declaration of a critical section:
critical_section_t cs1;

// buffer containing 5 characters:
char buffer[5] = "-----";

// The function used to overwrite buffer:
void changeText(char *text) {
// Wait until door opens:
critical_section_enter_blocking(&cs1);
// Overwrite the buffer char by char
for (uint16_t i = 0; i < 5; i++) {
buffer[i] = text[i];
// random delay:
sleep_ms(100 + rand() % 100);
}
// leave room:
critical_section_exit(&cs1);
}

void core1_entry() {
// core 1 thread writes 5 ones to the buffer
while (1) {
changeText("11111");
}
}

void core0_entry() {
// core 0 thread writes 5 zeros to the buffer
while(1) {
changeText("00000");
}
}

int main() {
stdio_init_all();
// initialize random number generator:
srand(42);
// Initialize critical section:
critical_section_init(&cs1);
// Start new thread in core 1:
multicore_launch_core1(core1_entry);
// Start main routine of core 0 thread:
core0_entry();

return 0;
}

Mutexes

Genau genommen basieren kritische Abschnitte auf Mutexes (Mutex = Mutual Exclusion). Daher können Programmierer Mutexe anstelle von Critical Sections verwenden. Im oberen Programm ließe sich ein Mutex mittels

mutex_t mx1;

vereinbaren, um es über

mutex_init(&mx1);

zu initialisieren.

Um das Mutex blockierend zu akquirieren, ist der Aufruf

mutex_enter_blocking(&mx1);

notwendig, während für die Freigabe ein

mutex_exit(&mx1);

genügt.

Das Pico-SDK bietet beim Einsatz von Mutexes wesentlich mehr Möglichkeiten als bei kritischen Abschnitten. Beispielsweise erlaubt der Aufruf von

mutex_enter_timeout_ms(&mx1, timeout)

ein zeitlich beschränktes Warten auf die Verfügbarkeit des Mutex, während

mutex_enter_block_until(&mx1, abstime);

das Warten bis zu einer absoluten Zeit erlaubt. Durch den Rückgabewert erfahren die Aufrufer, ob sie den Mutex erfolgreich akquiriert haben oder nicht. Auch Deadlocks sind dadurch vermeidbar. Es gibt noch weitere Funktionen im Pico-SDK, aber das soll es zum Thema Mutex gewesen sein.

Mutexes bieten ähnliche Möglichkeiten wie Critical Sections, sind aber flexibler
Mutexes bieten ähnliche Möglichkeiten wie Critical Sections, sind aber flexibler

Semaphores

Semaphore (altgriechisch für Signalgeber) dienen zum Signalisieren von Zuständen. Im Gegensatz zu Mutexes erlauben sie mehrere parallele Zugänge zu kritischen Bereichen, die sogenannten permits (Zugangsberechtigungen). Theoretisch könnte man binäre Semaphore, also solche mit einer einzigen Zugangsberechtigung, als Mutex betrachten – in manchen Echtzeitbetriebssystemen wie FreeRTOS basieren Mutex und Semaphor sogar auf einer gemeinsamen Implementierung. Allerdings unterscheiden sich beide Abstraktionen in der Praxis, was aber nicht Gegenstand der Diskussion sein soll (für nähere Details siehe Wikipedia-Artikel zu Semaphore [2]).

Auf dem Raspberry Pi Pico erlaubt das Pico-SDK ohnehin nur einen Thread pro Kern, weshalb die volle Flexibilität von Semaphores nur beschränkt nutzbar ist.

Was sind die wichtigen Semaphore-Funktionen in der Bibliothek?

Über

semaphore_t mySignal; 

sem_init(&mySignal, 1, 2);

erfolgt eine Definition eines Semaphore und seine Initialisierung. Im vorliegenden Fall besitzt das Semaphore initial eine einzige Zugangsberechtigung, kann aber maximal zwei vergeben. Die Zahl der noch nicht vergebenen Berechtigungen errechnet sich durch Aufruf von

sem_available(mySignal);

Um ein Semaphore blockierend zu akquirieren, nutzt man:

sem_acquire_blocking(&mySignal);

Soll keine Blockierung bei Nichtverfügbarkeit erfolgen, ist ein Timeout festzulegen:

sem_acquire_timeout_ms(&mySignal, 1000); // Timeout == 1000 Millisekunden

Soll sich die Zahl der Berechtigungen zur Laufzeit ändern, lässt sich folgende Funktionalität einsetzen:

sem_reset(&mySignal, newNumberOfPermits);

Zur Freigabe einer Berechtigung reicht der Aufruf:

sem_release(&mySignal);

Semaphore eignen sich übrigens auch für das Verwenden in Interrupt-Handlern, ganz im Gegensatz zu ihren Pendants wie FIFO-Queues, Queues, kritische Abschnitte und Mutexes.

Semaphores haben meist andere Einsatzgebiete als Mutexes
Semaphores haben meist andere Einsatzgebiete als Mutexes

Das illustrative Anwendungsbeispiel für Semaphore stammt von der Stanford University (siehe Seite 8 des Dokuments [3]). Es handelt sich um ein Erzeuger-Verbraucher-Problem. Der Erzeuger (Thread in Core 0) erzeugt zufällige Großbuchstaben in der Funktion Writer() mittels der Funktion PrepareData(). Ihm steht der Verbraucher (Thread in Core 1) gegenüber, der die Buchstaben mittels der Funktion Reader()ausliest. Zur temporären Speicherung der Buchstaben existiert ein Buffer (buffers) mit einer Kapazität von NUM_TOTAL_BUFFERS (im Beispiel auf 5 gesetzt). Für das Signalisieren der Lese- beziehungsweise Schreibmöglichkeit definiert das Programm zwei Semaphore:

    emptyBuffers signalisiert, ob der Buffer geschrieben werden kann, und fullBuffers, ob der Buffer gelesen werden kann. Beide Semaphore vergeben fünf Berechtigungen. Zum Beginn der Programmausführung verfügt emptyBuffers über fünf Berechtigungen, fullBuffers über 0.

Der Writer akquiriert bei jedem Schreibvorgang blockierend eine Berechtigung von emptyBuffers und gibt nach dem Schreiben eines Buchstabens eine Berechtigung von fullBuffers frei. Der Reader akquiriert umgekehrt bei jedem Lesevorgang blockierend eine Berechtigung von fullBuffers und gibt nach jedem Buchstabenlesen eine Berechtigung von emptyBuffers frei.

Da das Programm mit Berechtigungen von fullBuffers startet, ist garantiert, dass der lesende Thread erst dann Zugriff auf den Buffer erhält, sobald der schreibende Thread dort etwas hineinschreiben konnte. Die vom Reader nach jedem Lesevorgang aufgerufene Funktion ProcessData() symbolisiert eine mögliche Verarbeitung der gelesenen Daten. Im Beispiel verbringt sie lediglich eine zufällige Zeitdauer mit Warten.

Analoges gilt für PrepareData(), das die Vorverarbeitung beziehungsweise das Heranschaffen der Daten illustriert. Im Beispiel erzeugt sie einfach einen zufälligen Großbuchstaben, nachdem sie zuvor eine zufällige Zeitspanne untätig gewartet hat:

/**
* readerWriter.c
* --------------
* The canonical consumer-producer example. This version has just one reader
* and just one writer (although it could be generalized to multiple readers/
* writers) communicating information through a shared buffer. There are two
* generalized semaphores used, one to track the num of empty buffers, another
* to track full buffers. Each is used to count, as well as control access.
*/
#include "pico/stdlib.h"
#include "pico/multicore.h"
#include "pico/sync.h"
#include <stdio.h>

#define NUM_TOTAL_BUFFERS 5
#define DATA_LENGTH 20

char buffers[NUM_TOTAL_BUFFERS]; // the shared buffer
semaphore_t emptyBuffers, fullBuffers; // semaphores used as counters

/**
* ProcessData
* -----------
* This just stands in for some lengthy processing step that might be
* required to handle the incoming data. Processing the data can be done by
* many reader threads simultaneously since it doesn't access any global state
*/
static void ProcessData(char data)
{
sleep_ms(rand() % 500);
// sleep random amount
}

/**
* PrepareData
* -----------
* This just stands in for some lengthy processing step that might be
* required to create the data. Preparing the data can be done by many writer
* threads simultaneously since it doesn't access any global state. The data
* value is just randomly generated in our simulation.
*/
static char PrepareData(void)
{
sleep_ms(rand() % 500);
return (65 + rand() % 26); // random uppercase letter
// sleep random amount
// return random character
}

/**
* Writer
* ------
* This is the routine forked by the Writer thread. It will loop until
* all data is written. It prepares the data to be written, then waits
* for an empty buffer to be available to write the data to, after which
* it signals that a full buffer is ready.
*/
static void Writer()
{
int i, writePt = 0;
char data;
for (i = 0; i < DATA_LENGTH; i++) {
data = PrepareData();
sem_acquire_blocking(&emptyBuffers);
buffers[writePt] = data;
printf("%s: buffer[%d] = %c\n", "Writer", writePt, data);
writePt = (writePt + 1) % NUM_TOTAL_BUFFERS;
sem_release(&fullBuffers);
// announce full buffer ready
}
}

/**
* Reader
* ------
* This is the routine forked by the Reader thread. It will loop until
* all data is read. It waits until a full buffer is available and the
* reads from it, signals that now an empty buffer is ready, and then
* goes off and processes the data.
*/
static void Reader()
{
int i, readPt = 0;
char data;
for (i = 0; i < DATA_LENGTH; i++) {
sem_acquire_blocking(&fullBuffers); // announce empty buffer
// wait til something to read
data = buffers[readPt]; // pull value out of buffer
printf("\t\t%s: buffer[%d] = %c\n", "Reader", readPt, data);
readPt = (readPt + 1) % NUM_TOTAL_BUFFERS;
sem_release(&emptyBuffers); // announce empty buffer
ProcessData(data); // now go off & process data
}
}

/**
* Initially, all buffers are empty, so our empty buffer semaphore starts
* with a count equal to the total number of buffers, while our full buffer
* semaphore begins at zero. We create two threads: one to read and one
* to write, and then start them off running. They will finish after all
* data has been written & read.
*/

int main()
{
stdio_init_all();

sem_init(&emptyBuffers, NUM_TOTAL_BUFFERS, NUM_TOTAL_BUFFERS);
sem_init(&fullBuffers, 0, NUM_TOTAL_BUFFERS);

multicore_launch_core1(Reader);

Writer(); // runs in this thread
sem_release(&fullBuffers);
sem_release(&emptyBuffers);
printf("All done!\n");
while(true);
return 0;
}

In der Ausgabe des Beispiels am seriellen Monitor ist Folgendes zu beobachten:

    Alles, was der “Writer" in den Buffer hineinschreibt, liest der “Reader” in exakt derselben Reihenfolge aus. Der "Writer" kann in einem Schreibvorgang maximal bis zu fünf Daten hintereinander schreiben, der Reader entsprechend maximal bis zu fünf Daten hintereinander lesen. Nach 20 Iterationen (konfiguriert in DATA_LENGTH) beendet sich das Programm.
Das Beispielsprogramm zeigt in der Bildschirm-Ausgabe, dass Semaphores richtig zur Synchronisation verwendet werden
Das Beispielsprogramm zeigt in der Bildschirm-Ausgabe, dass Semaphores richtig zur Synchronisation verwendet werden

Das alles funktioniert nur deshalb so gut und abgestimmt, weil die Semaphore dafür sorgen, dass der Buffer als kritische Resource keine Inkonsistenzen aufweist. Die Semaphore dienen in diesem Kontext als Signale für "Du darfst jetzt (nicht) lesen" oder "Du darfst jetzt (nicht) schreiben".

Fazit

Im Pico-SDK ist Parallelisierung nur eingeschränkt möglich. Das Hauptprogramm läuft in einem Thread auf Core und kann zusätzlich einen Thread erzeugen, der auf Core 1 läuft. Erst die Verfügbarkeit von Echtzeitbetriebssystemen auf dem Pico überwindet diese Beschränkung. Trotzdem ergibt auch Multithreading mit zwei Threads Sinn. Wichtig ist dabei allerdings, die vorhandenen Synchronisationsmechanismen zu verstehen. Das gilt für die Hardware-basierte FIFO-Queue, den abstrakten und Thread-sicheren Datentyp Queue aus der Pico-Bibliothek und selbstredend auch für die bereitgestellten Synchronisationsprimitive Critical Sections, Mutexes, und Semaphores, mit denen sich eigene Bibliotheken Thread-sicher machen lassen.

Manche Entwickler scheuen sich vor dem Einsatz von Multithreading. Zu schwierig erscheint das Aufspüren von Fehlerquellen in Problemcode. Durch die Berücksichtigung bekannter Best Practices überwiegt der potenzielle Nutzen aber weit die möglichen Herausforderungen.

Damit wäre der kurze Rundgang durch Multithreading und Koordination von Threads beendet, und das Rüstzeug bereitgestellt, um responsive und effiziente Mikrocontroller-Anwendungen für den Pico zu erstellen.


URL dieses Artikels:
https://www.heise.de/-6000386

Links in diesem Artikel:
[1] https://raspberrypi.github.io/pico-sdk-doxygen/group__queue.html
[2] https://en.m.wikipedia.org/wiki/Semaphore_(programming)
[3] https://see.stanford.edu/materials/icsppcs107/23-Concurrency-Examples.pdf

Copyright © 2021 Heise Medien

Adblock test (Why?)

  • 30. März 2021 um 08:07

Dependency Injection in JavaScript

Von heise online

Dependency Injection in JavaScript

Tales from the Web side Sebastian Springer

Bibliotheken und Frameworks setzen auf Dependency Injection, um mehr Flexiblität im Umgang mit Abhängigkeiten innerhalb einer Applikation zu erhalten.

In Programmiersprachen wie Java und PHP ist Dependency Injection als Entwurfsmuster seit vielen Jahren etabliert. Und es hat in den letzten Jahren auch Einzug in die JavaScript-Welt gehalten. Der wohl bekannteste Vertreter der Frameworks, der dieses Entwurfsmuster nahezu durchgängig einsetzt, ist Angular. Aber auch serverseitig wird Dependency Injection (DI) beispielsweise von Nest verwendet. Stellt sich nur die Frage, warum viele andere etablierte Bibliotheken wie React dieses Entwurfsmuster komplett links liegen lassen.

Bevor wir uns im nächsten Schritt mit der konkreten Umsetzung von DI in JavaScript beschäftigen, sehen wir uns zunächst die Theorie hinter dem Entwurfsmuster an. Bei DI geht es darum Abhängigkeiten zur Laufzeit zur Verfügung zu stellen. Im Gegensatz zum Modulsystem, bei dem die einzelnen Elemente schon zum Compile- beziehungsweise Startzeitpunkt festgelegt werden, erlaubt DI eine deutlich feinere Kontrolle über die Applikation. Außerdem erleichtert DI das Testen von Objekten, indem Abhängigkeiten für die Testumgebung ausgetauscht werden. Damit DI funktioniert, stellen DI umsetzende Bibliotheken einen Mechanismus bereit, über den die injizierbaren Klassen registriert werden. Eine Klasse in der Applikation gibt dann nur noch an, welche Abhängigkeiten sie benötigt, und die Bibliothek kümmert sich darum, dass die Klasse eine Instanz der Abhängigkeit erhält. Das klingt recht abstrakt, sehen wir uns also ein konkretes Beispiel in Nest an.

DI in Nest

Nest ist ein serverseitiges JavaScript-Framework [1], das Entwickler bei der Umsetzung von Web-Schnittstellen unterstützt. Die Funktionalität wird meist in ein oder mehrere Module aufgeteilt. Jedes Modul verfügt über Controller, die die Endpunkte der Schnittstelle enthalten. Die eigentliche Businesslogik liegt in Providern, die über DI geladen werden. Für unser DI-Beispiel setzen wir eine Schnittstelle um, die die Distanz zwischen zwei GPS-Koordinaten berechnet. Die Grundlage bildet ein fachliches Modul, das alle Belange kapselt, die mit dem Thema GPS zu tun haben, in unserem Fall ist das die Distanzberechnung. Das Modul ist im weitesten Sinn ein Container für die DI, enthält aber auch die Einstiegspunkte in das Modul. Nest arbeitet, wie auch Angular im Frontend, mit TypeScript und Decorators. Ein solcher Decorator fügt Metainformationen zu einer Klasse oder Funktion hinzu. Der nachfolgende Code zeigt das Modul, in dem bereits der Controller und der Service registriert sind:

import { Module } from '@nestjs/common';
import { DistanceController } from './distance/distance.controller';
import { DistanceService } from './distance/distance.service';

@Module({
controllers: [DistanceController],
providers: [DistanceService]
})
export class GpsModule {}

Den Einstieg stellen die Controller dar. Sie sind, wie auch Module, einfache TypeScript-Klassen, die mit Dekoratoren erweitert werden. Das folgende Listing enthält den Code des Controllers:

import { Body, Controller, Get } from '@nestjs/common';
import { Point } from 'src/point.interface';
import { DistanceService } from './distance.service';

@Controller('gps/distance')
export class DistanceController {
constructor(private readonly distanceService: DistanceService) {}

@Get() distance(@Body() coordinates: Point[]): number {
const a = coordinates[0];
const b = coordinates[1];

return this.distanceService.calculate(a, b);
}
}

Die Decorators des Controllers bestimmen das Verhalten der Schnittstelle. Der Decorator der Klasse legt den URL-Pfad fest und der Decorator der distance-Methode definiert, dass diese Methode ausgeführt wird, wenn Benutzer den Pfad gps/distance mit einem HTTP GET-Request aufrufen. Über den Body-Decorator kann die Methode direkt auf den Request-Body zugreifen. Der Controller kümmert sich lediglich um die Schnittstelle, die Extraktion der Daten aus dem Request und die Validierung der eingehenden Daten. Die eigentliche Businesslogik liegt dann nicht mehr im Controller, sondern in einem Service. Der Vorteil dieser Aufteilung ist, dass die Logik getrennt vom Request- und Response-Handling getestet werden kann.

Der Austausch des Algorithmus ist vergleichsweise einfach, da der Controller selbst nicht modifiziert werden muss, sondern lediglich die injizierte Abhängigkeit angepasst wird. Den Schlüssel bildet die sogenannte Constructor Injection. Hierbei enthält der Constructor einen Parameter, der vom Injector von Nest ausgewertet wird. Erzeugt Nest beim Start der Applikation eine Instanz des Controllers, wird der Injector tätig und generiert anhand der Konfiguration des Moduls eine Instanz des angegebenen Services. Dieser steht dann innerhalb der Controller-Klasse über die distanceService-Eigenschaft zur Verfügung. Der Code des Services selbst ist relativ unspektakulär. Er berechnet anhand zweier übergebener GPS-Koordinaten die Distanz in Metern und gibt sie zurück:

import { Injectable } from '@nestjs/common';
import { Point } from 'src/point.interface';

@Injectable()
export class DistanceService {
calculate(a: Point, b: Point): number {
const earthRadius = 6371e3;
const latARad = a.lat * Math.PI/180;
const latBRad = b.lat * Math.PI/180;
const deltaLatRad = (b.lat - a.lat) * Math.PI/180;
const deltaLonRad = (b.lon - a.lon) * Math.PI/180;

const x = Math.sin(deltaLatRad/2) * Math.sin(deltaLatRad/2) + Math.cos(latARad) * Math.cos(latBRad) * Math.sin(deltaLonRad/2) * Math.sin(deltaLonRad/2);
const c = 2 * Math.atan2(Math.sqrt(x), Math.sqrt(1-x));

return earthRadius * c;
}
}

Im einfachsten Fall sind der Name des Services, den der Injector verwendet, und der Klassenname gleich. Angenommen, der Distanz-Algorithmus soll jetzt durch eine optimierte Version ersetzt werden, kann dies entweder durch ein Ersetzen der Klasse oder durch eine Konfigurationsänderung am Modul erfolgen. Der Service wird in diesem Fall nicht mehr direkt referenziert, sondern in Form eines Objekts, wie im nachfolgenden Code zu sehen ist. In diesem Fall wird die OptimizedDistanceService-Klasse verwendet:

@Module({
controllers: [DistanceController],
providers: [{
provide: DistanceService,
useClass: OptimizedDistanceService,
}]
})
export class GpsModule {}

Neben der Flexibilität zum Austausch von Algorithmen bringt die DI gerade auch beim Testen deutliche Vorteile mit sich. Auch hier werfen wir wieder einen Blick auf die konkrete Implementierung in Nest:

import { Test, TestingModule } from '@nestjs/testing';
import { DistanceController } from './distance.controller';
import { DistanceService } from './distance.service';

const newYork = { lat: 40.6943, lon: -73.9249 };
const tokyo = { lat: 35.6897, lon: 139.6922 };
const distance = 10853705.986613212;

describe('DistanceController', () => {
let controller: DistanceController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [DistanceController],
providers: [DistanceService]
}).overrideProvider(DistanceService).useValue({
calculate() { return distance; }
}).compile();

controller = module.get<DistanceController>(DistanceController);
});

it('should calculate the distance between New York and Tokyo', () => {
const result = controller.distance([newYork, tokyo]);
expect(result).toBe(distance);
});
});

Die Nest CLI legt für alle Strukturen, die mit ihr generiert werden, standardmäßig einen Test an. So auch für den DistanceController. Die Setup-Routine, die mit der beforeEach-Funktion definiert wird, sorgt dafür, dass vor jedem Test ein neues Test-Modul erzeugt wird. Hier können Entwickler alle Strukturen registrieren, die für einen Testlauf erforderlich sind. In unserem Fall sind das der DistanceController und der DistanceService. Wobei die Tests nicht den ursprünglichen Service, sondern einen speziell für den Test definierten Mock-Service nutzen.

Die Injector-Komponente von Nest weiß aufgrund dieser Konfiguration, dass bei jeder Anforderung nach der DistanceService-Abhängigkeit der unter useValue angegebene Wert verwendet werden soll. Dieser Mock-Service gibt für die calculate-Methode immer einen konstanten Wert für die Distanz zurück, unabhängig davon, wie er aufgerufen wird. Das hat den Vorteil, dass der Controller-Test nur den Controller selbst, nicht aber den Service testet. Die Konsequenz daraus ist, dass der Test auch nur wegen Fehlern im Controller fehlschlagen kann. Der Test selbst ruft die distance-Methode des Controllers mit einem Array aus Koordinatenpunkten auf, die an die Stelle des Request-Bodys treten. Da die Rückmeldung des Controllers synchron erfolgt, sind keine weiteren Maßnahmen wie das Einfügen von async/await erforderlich. Bleibt nur noch die Überprüfung des Ergebnisses mittels Aufrufs der expect-Funktion.

Alternativen zur integrierten DI

Bisher haben wir uns mit der DI von Nest nur eine integrierte Lösung angesehen. Es gibt allerdings auch Bibliotheken, die DI unabhängig vom verwendeten Framework anbieten. Diese eignen sich vor allem für individuelle Lösungen und eigene Framework-Ansätze. Ein recht populärer Vertreter dieser Art von Bibliotheken ist Inversify [2]. Die Umsetzung ist etwas aufwendiger als bei einer integrierten Lösung wie bei Nest oder Angular:

  1. Interfaces und Typen deklarieren: Inversify folgt der Empfehlung, dass die Verweise auf Abhängigkeiten nicht auf konkrete Klassen, sondern auf Abstraktionen erfolgen sollen.
  2. Dependencies angeben: Inversify arbeitet, wie auch Nest, mit Decorators. Der @injectable-Decorator kennzeichnet Klassen, die mit DI arbeiten. Der @inject-Decorator markiert die Stellen im Code, an denen die Abhängigkeiten eigensetzt werden.
  3. DI-Container erzeugen: Die Container-Instanz bringt die abstrakten Abhängigkeiten mit den konkreten Klassen zusammen.
  4. Dependencies auflösen: Die get-Methode der Container-Instanz erlaubt das Auflösen der Abhängigkeiten und somit die Arbeit mit den konkreten Objekten.

Warum haben manche Bibliotheken und Frameworks keine DI?

React ist ein prominenter Vertreter einer verbreiteten Bibliothek, die komplett auf DI verzichtet. Im Gegensatz zu Frameworks wie Nest arbeitet React viel weniger mit Klassen. Der Großteil der Komponenten in modernen React-Applikationen sind als Funktionen umgesetzt. Diese Funktionen erhalten Informationen über die sogenannten Props in Form von Argumenten. Die Props können bei der Einbindung der Komponente beeinflusst werden, was das Testen der Komponenten einfach macht. Services in der Form, wie es sie in Nest oder Angular gibt, existieren in React nicht. In den meisten Fällen greifen Entwickler für diesen Zweck auf reine Funktionen zurück, die wiederum über die übergebenen Argumente gesteuert werden.

Beim Testen geht React auch einen anderen Weg und empfiehlt, die Komponenten mehr aus der Benutzerperspektive in Form eines Blackbox-Tests zu überprüfen. Die Testumgebung rendert die Komponenten, wie sie auch in der produktiven Applikation gerendert werden, und interagiert dann wie ein Benutzer damit, löst verschiedene Events aus und überprüft deren Auswirkungen auf die gerenderten Strukturen. Aufgaben, die typischerweise von Services abstrahiert werden, wie die Kommunikation mit einer Backend-Schnittstelle, können an den Systemgrenzen durch einen Mock des HTTP-Clients überprüft werden.

Und was lernen wir daraus?

Dependency Injection ist ein Entwurfsmuster, das sich dank Frameworks wie Angular oder Nest auch in der JavaScript-Welt etabliert hat. Es eignet sich jedoch nicht in jeder Umgebung. Wo sie in klassenorientierten Applikationen ihre Stärken sehr gut ausspielen kann, ist der Einsatz von DI in einem funktionalen Ansatz eher fragwürdig. In der passenden Umgebung verwendet, erleichtert DI das Testen einzelner Einheiten deutlich und sorgt für eine Entkopplung der Strukturen, was sich wiederum förderlich beim Austausch von einzelnen Teilen der Applikation auswirkt.


URL dieses Artikels:
https://www.heise.de/-5992734

Links in diesem Artikel:
[1] https://nestjs.com/
[2] https://inversify.io/

Copyright © 2021 Heise Medien

Adblock test (Why?)

  • 22. März 2021 um 08:54

Raspberry Pi Pico und C/C++ – eine gute Kombination

Von heise online

Raspberry Pi Pico und C/C++ – eine gute Kombination

Der Pragmatische Architekt Michael Stal

Bislang war in diesem Blog MicroPython das Mittel der Wahl für Projekte mit dem Raspberry Pi Pico. In der Embedded-Entwicklung spielen noch immer C beziehungsweise C++ eine herausragende Rolle. Der vorliegende Artikel illustriert deshalb, wie sich C/C++-Entwicklung für den Pico durchführen lässt.

Nach den Empfehlungen für die Installation der benötigten Werkzeugkette kommt der Beitrag auf ein kleines Beispielprojekt zu sprechen.

Bereitstellen von Python 3 und Git

Nutzer können unterschiedliche Betriebssysteme einsetzen, wenn sie für den Pico als Zielsystem programmieren wollen:

  • macOS für Intel oder ARM.
  • Linux.
  • Windows 10 nativ.
  • Windows 10 plus Version 1 vom WSL, dem Windows Subsystem for Linux. WSL 2 eignet sich momentan nicht, weil es im Gegensatz zu WSL 1 nur über komplexe Winkelzüge einen Durchgriff auf die seriellen Ports gestattet.

Es gibt somit unterschiedliche Host-Umgebungen. Daher sind die nachfolgenden Beschreibungen bewusst allgemein formuliert. Wo nötig, erfolgen Details zu den jeweiligen Betriebssystemen. Voraussetzung für den weiter unten beschriebenen Installationsprozess ist die Verfügbarkeit von Python 3.x und Git auf dem jeweiligen Betriebssystem.

Git und Python

Auf Linux-Distributionen wie Debian oder Ubuntu sind folgende Schritte nötig:

sudo apt install git-all

gefolgt von:

sudo apt-get install python3 python3-pip

Mac-Besitzer benutzen Homebrew:

brew install git 
brew install python3

Sofern sich bereits Xcode auf dem System befindet, ist der erste Schritt nicht notwendig, zumal Xcode eine Git-Implementierung mitbringt.

Windows-Nutzer holen sich die Git-Installation über die Downloadseite [1]. Für die Installation von Python 3 existiert ebenfalls eine Downloadseite [2].

Installation der Entwicklerwerkzeuge

Als Erstes ist an dieser Stelle zu erwähnen, dass sich C/C++-Programmierer unbedingt die Dokumentation zum Pico-SDK für C/C++ besorgen sollten, die sich hier [3] befindet. Dort stehen alle wesentlichen Informationen über das SDK für den Pico bereit. Als gute Einführung eignet sich darüber hinaus das Dokument "Getting started with Raspberrry Pi Pico – C/C++ development with Raspberry Pi Pico and other RP2040-based microcontroller boards [4]". [5]

Im ersten Schritt geht es um die Inbetriebnahme der notwendigen Werkzeugkette. Zunächst ist die Installation des Pico-SDKs notwendig. Dieses kopieren Entwickler über Git in ein eigenes Verzeichnis:

git clone -b master —-recurse-submodules https://github.com/raspberrypi/pico-sdk.git

Anschließend bedarf es folgender Anweisung:

cd /path/to/pico-sdk git submodule update --init

Es existiert zusätzlich ein GitHub-Repository mit Beispielen, dessen Herunterladen lohnt:

git clone -b master https://github.com/raspberrypi/pico-examples.git

Damit die Werkzeuge das SDK finden, lässt sich die Umgebungsvariable PICO_SDK_PATH definieren:

PICO_SDK_PATH="/path/to/pico-sdk“

Als nächstes sind CMake und die Entwicklungswerkzeuge von ARM erforderlich, deren Installation sich beispielsweise auf macOS wie folgt gestaltet:

brew install cmake 
brew tap ArmMbed/homebrew-formulae 
brew install arm-none-eabi-gcc

Für Windows 10 gibt es die notwendigen Binärdateien zum Herunterladen auf der entsprechenden CMake [6]- beziehungsweise auf der ARM-Webseite [7]. Zusätzlich installieren Windows-Nutzer die Build-Tools for Visual Studio 2019 [8].

Linux-Nutzer verwenden stattdessen:

sudo apt-get install cmake 
sudo apt install gcc-arm-none-eabi

An dieser Stelle soll eine (optionale) IDE zum Einsatz kommen. Hinsichtlich C/C++ bieten sich CLion, Eclipse oder Visual Studio Code als Alternativen an. Die meisten Seiten im Internet konzentrieren sich auf Visual Studio Code, weshalb im Folgenden ausschließlich davon die Rede sein soll. Im Anschluss an die eigentliche Installation von Visual Studio Code [9] suchen Entwicklerinnen in der IDE nach der Extension CMake Tools und installieren diese zusätzlich.

Um Visual Studio Code (wie bei Windows oder Linux ohnehin der Fall) auf der Kommandozeile über „code“ aufrufen zu können, starten macOS-Nutzer die IDE, wählen mit Cmd + Shift + p oder über das View-Command Palette-Menü die Command-Palette.

Der Suchbegriff "Shell Command" führt zu einem Eintrag "Shell Command: Install 'code' command in PATH", dessen Aktivierung die IDE aus der Kommandozeile aufrufbar macht. Durch Eingabe von code öffnet sich fortan Visual Studio Code auch über die Shell. Als Argument lässt sich optional der Pfad zum gewünschten Projektordner angeben.

Um einen Build von Pico-Projekten durchführen zu können, bedarf es in der IDE verschiedener Erweiterungen, insbesondere Python, CMake und CMake Tools. Nach deren Installation in Visual Studio Code müssen In den Einstellungen für die CMake Tools noch diverse Einträge erfolgen:

Cmake: Build Environment: Hier ist als Schlüssel PICO_SDK_PATH und als Wert der entsprechende Pfad einzugeben.

Cmake: Configure Environment beziehungsweise Cmake: Environment: hier erfolgt derselbe Eintrag.

CMake Tools benötigen die Angabe des Pico-SDK-Pfads
CMake Tools benötigen die Angabe des Pico-SDK-Pfads

Cmake: Generator: An dieser Stelle bittet CMake um die Angabe des verwendeten Build-Werkzeugs. Auf Unix-Systemen ist dies für gewöhnlich Unix Makefiles, unter Windows NMake Makefiles. Bei Nutzung von ninja als Generator ergeben sich Probleme, weshalb Entwickler lieber auf die vorgenannten Werkzeuge vertrauen sollten.

Unter CMake Generator ist das genutzte make-Tool einzutragen
Unter CMake Generator ist das genutzte make-Tool einzutragen

Projekt anlegen

Nun lässt sich auf einem beliebigen Ordner ein neues Projekt anlegen, und die Datei /path/to/pico-sdk/external/pico_sdk_import.cmake in den jeweiligen Ordner kopieren. Zusätzlich ist im Projektverzeichnis die Datei CMakeLists.txt bereitzustellen, die folgendermaßen aussehen muss:

# Minimal zulässige Version von CMake: 
cmake_minimum_required(VERSION 3.15)
# Inkludieren des Pico-SDK:

include(pico_sdk_import.cmake)
# Name und Version des Projekts:

project(MeinPicoProjekt VERSION 1.0.0)
# Das Projekt mit einer Quelldatei verknüpfen, das das Hauptprogramm enthält:

add_executable(MeinPicoProjekt MeinPicoProjekt.c)
# Angabe der benötigten Bibliothek(en):

target_link_libraries(MeinPicoProjekt hardware_i2c pico_stdlib)
# Initialisieren des SDK:

pico_sdk_init()
# Zugriff auf USB und UART ermöglichen (1 = enable, 0 = disable):

pico_enable_stdio_usb(MeinPicoProjekt 1)
pico_enable_stdio_uart(MeinPicoProjekt 1)
# Definition notwendiger Extra-Zieldateien. Hieraus generiert das Tooling
# die für die Übertragung auf den Pico benötigte UF2-Datei:

pico_add_extra_outputs(MeinPicoProjekt)

Natürlich kann die Implementierung auch mehrere Quelldateien a.c, b.c, z.c enthalten, die alle in add_executable() auftauchen müssen.

Im Projektordner sollten nun die Dateien MeinPicoProjekt.h und MeinPicoProjekt.c (sowie eventuell andere C-/C++-Programmdateien) bereitgestellt werden. In macOS/Linux zum Beispiel über:

touch MeinPicoProjekt.h 
echo '#include "MeinPicoProjekt.h"' > MeinPicoProjekt.c

Noch eine kleine Information am Rande: Falls in target_link_libraries() innerhalb von CMakeLists.txt eine Bibliothek wie hardware_i2c oder pico_stdlib als Argument auftaucht, gibt es dazu Header-Dateien, die Anwender in ihren Quelltextdateien einfügen müssen, etwa folgendermaßen:

#include "hardware/i2c.h" 
#include "pico/stdlib.h"

Sie erkennen jetzt sicher das Namensschema sowie die Korrelation zwischen Bibliotheksnamen und Header-Dateien. Die oben beschriebene Datei CMakeLists.txt enthält bereits die benötigten Bibliotheken hardware_i2c und pico_stdlib.

Es empfiehlt sich zusätzlich die Bereitstellung des Werkzeugs Doxygen, das aus Quelldateien Kommentare entnimmt und daraus eine Dokumentation erzeugt. Näheres dazu findet sich auf der Doxygen-Webseite [10].

Sobald Visual Studio Code nach dem verwendeten Kit für das Projekt fragt – gemeint sind die C/C++-Compiler-Werkzeuge –, ist GCC for arm-none-eabi ?.?.? anzugeben. Auch in der unteren Statusleiste ist die Eingabe des Kits möglich (Icon mit gekreuzten Werkzeugen). Dort finden sich ebenfalls Icons zur Auswahl der gewünschten Ausgabedateien (Icon CMake mit den Optionen Debug, Release, MinSizeRel, RelWithDebInfo) sowie ein Build-Icon mit Zahnrad, das den Build-Prozess anstößt. Über die Command Palette (Submenü von View) lässt sich der Build-Prozess für das Projekt ebenfalls initiieren (Kommando CMake : Build).

Auf der unteren Statusleiste präsentiert CMake Tools verschiedene Kommandos
Auf der unteren Statusleiste präsentiert CMake Tools verschiedene Kommandos

Ist alles erfolgreich eingerichtet lassen sich Projekte kompilieren, übertragen und debuggen. Mit dem Kommando CMake : Build in der Command Palette lässt sich der Prozess anstoßen.

Der Build-Prozess läuft ...
Der Build-Prozess läuft ...

Raspberry Pi Pico Generator

Weil diese Schritte viel Mühe machen, müsste man sie bei jedem Projekt erneut durchlaufen, hat die Raspberry Pi Foundation dafür ein spezielles Werkzeug bereitgestellt, den Raspberry Pi Pico Project Generator [11].

Dieses Werkzeug ist in Python geschrieben, lässt sich mit einer GUI starten oder auch über die Kommandozeile. Jedenfalls ersparen sich Entwickler dadurch die oberen Schritte für das Anlegen eines neuen Projekts. Dieser hilfreiche Generator findet leider viel zu selten Erwähnung.

Project Generator mit Gui. Dazu ist der Parameter —gui an das Werkzeug pico_project.py zu übergeben
Project Generator mit Gui. Dazu ist der Parameter —gui an das Werkzeug pico_project.py zu übergeben

Achtung: Der Code des Pico-Python-Generators geht davon aus, dass die ARM-Werkzeuge auf dem Pfad /usr/bin/arm-none-eabi-gcc liegen, was aber nicht stimmen muss. In meinem Fall befinden sich die Werkzeuge unter macOS auf /usr/local/bin/arm-none-eabi.gcc. Es reicht, den richtigen Pfad in der Python-Datei einzutragen oder diesen Vorgang mit einem sed-Kommando zu automatisieren.

Auf Kommando(ebene)

Natürlich ist nicht zwingend eine IDE wie Visual Studio Code, CLion oder Eclipse notwendig, um C/C++-Entwicklung für den Pico durchzuführen. Information über das Arbeiten mit diesen IDEs findet sich übrigens im Getting Started Manual [12] ab Seite 31, und zwar in den Kapiteln 8, 9 und 10.

Die Arbeit auf der Kommandozeile ist mitunter sehr hilfreich, die dafür notwendigen Schritte relativ einfach – die Mithilfe des Project Generators vorausgesetzt.

Entwickler können unter der Kommandozeile beziehungsweise Shell in den Unterordner build des Projektordners wechseln, dort

cmake .. 

aufrufen, und anschließend das jeweilige Make-Tool des Hostsystems, etwa make unter Unix oder nmake unter Windows, starten. CMake generiert zu diesem Zweck die passenden Makefiles. Für CMake sollten Entwickler natürlich den Pfad (Umgebungsvariable PATH) mit dem Verzeichnis der CMake-Werkzeuge ergänzen.

Zur Beobachtung des USB-Ports, an dem der Pico hängt, empfehlen sich bei kommandozeilenorientierter Entwicklung Werkzeuge wie screen (macOS), minicom (macOS, Linux), CoolTerm (macOS, Windows, Linux) oder PuTTY (Windows).

Picoprobe – der Hardware Debugger

Ein Hardware-Debugger für die Pico-Entwicklung lässt sich übrigens mit Hilfe eines zweiten Pico leicht und kostengünstig realisieren. Entsprechende Anleitungen finden sich auf verschiedenen Webseiten, etwa hier [13] oder hier [14] oder hier [15].

Die Dokumentation für den Raspberry Pi Pico spricht in diesem Zusammenhang von Picoprobe. Dabei werden die SWD-Pins des Entwicklungsboards (im unteren Bild rechts) mit dem zur Probe umfunktionierten zweiten Pico verbunden (im Bild unten links). Die Firmware für die Picoprobe basiert auf OpenOCD (Open On-Chip Debugger), das ein Debuggen verschiedenster Zielsysteme ermöglicht (siehe die OpenOCD-Webseite [16]).

Raspberry Pi Foundation
Der Schaltungsaufbau für das Picoprobe (Bild: Raspberry Pi Foundation)

Die notwendige Software für die Picoprobe lässt sich über GitHub beziehen (Webseite [17]). Es genügt, die Software mit cmake und (n)make zu generieren, um sie dann als UF2-Binärdatei auf den als Hardware-Debugger genutzten "Zweit"-Pico zu übertragen. Nähere Information über Picoprobe finden sich auf dem "Getting Started"-Dokument der Raspberry Pi Foundation im Anhang A [18].

Um in Visual Studio Code die Picoprobe einzusetzen, bedarf es einzig der Extension Cortex-Debug und einer speziellen Konfigurationsdatei launch.json, die sich im Unterverzeichnis .vscode des Projektordners befinden muss. Nähere Information und den Code für Cortex-Debug sind auf dem GitHub-Repository von Marus [19] einsehbar.

Hardware-Debugging mit Picoprobe unter Visual Studio Code
Hardware-Debugging mit Picoprobe unter Visual Studio Code

Ein Mini-Projekt

Zum Schluss präsentiert dieser Beitrag ein kleines C/C++-Projekt für den Pico. Dieses Projekt kratzt nur an der Oberfläche des Pico und dient allein der Illustration. In dem Beispiel prüft ein mit Infrarot arbeitender Bewegungssensor des Typs HC-SR501 die Umgebung auf Bewegungen. Es handelt sich um einen sogenannten PIR-Sensor, wobei PIR für Passive Infrared steht.

Die Grundlagen dieser Sensoren und ein ähliches Programm für den Arduino gab es übrigens schon einmal in diesem Blog. Wen es interessiert, kann sich das damalige Posting [20] gerne noch einmal zu Gemüte führen.

Beim Erkennen einer Bewegung aktiviert das System eine LED. Statt der LED ließe sich natürlich auch eine über ein Relais geschaltete Lampe nutzen.

Bill of Material:
Komponente                                Preis
HC-SR501                                  2 Euro                         
LED,Drähte,220 Ohm-Widerstand             1 Euro                    
RPi Pico                                  5 Euro
Gesamt                                    8 Euro

Die zugehörige Schaltung sieht folgendermaßen aus:

Das Fritzing-Schaltungsdiagramm für das PIR-Beispiel
Das Fritzing-Schaltungsdiagramm für das PIR-Beispiel

Der PIR-Sensor wird durch GPIO-Port 10 des Pico eingelesen, die LED durch GPIO-Port 11 angesteuert. Versorgungsspannung und Erde erhält der Sensor durch den 3.3V-Ausgang des Pico sowie einen seiner GND-Pins. Ein 220-Ohm-Widerstand sorgt dafür, dass die LED nicht durchbrennt.

Alle Projektdateien befinden sich auf einem eigenen GitHub-Repository [21]. Sollten Entwickler Picoprobe zum Hardware-unterstützten Debuggen benutzen, finden sie im Repository unter .vscode auch die notwendige und dazu passende Konfigurationsdatei namens launch.json. Das Projektverzeichnis hat der Raspberry Pi Project Generator erzeugt.

Das Programm benutzt einen Interrupthandler pir_irq_handler(), der auf steigende oder fallende Flanken des Eingangspins PIR_PIN reagiert – an diesem ist der Ausgang des PIR-Sensors angeschlossen. Die Registrierung des Handlers passiert in der Methode gpio_set_irq_enabled_with_callback(), der wir unter anderem den jeweiligen Pin und die zu behandelnden Ereignisse übergeben.

Bei einer ansteigenden Signalflanke (0 => 1) hat der Sensor eine Bewegung entdeckt, weshalb das Programm die Lampe (= LED) einschaltet. Deren Ausschaltung erfolgt bei einer fallenden Flanke (1 => 0). Die Methode calibrate() wartet, bis der Sensor etwas "eingeschwungen" ist, und lässt solange die eingebaute LED des Pico im Sekundentakt blinken.

Am PIR-Sensor ist es möglich, über zwei Potentiometer jeweils Empfindlichkeit der Erkennung und die Art beziehungsweise Dauer der Erkennung einstellen. Wer mag, kann hier etwas experimentieren.

Die zwei Potis am PIR-Sensor erlauben eine Feinjustierung
Die zwei Potis am PIR-Sensor erlauben eine Feinjustierung

Listing

#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/irq.h"
#include "hardware/gpio.h"
#include "pico/time.h"

const uint PIR_PIN = 10;
const uint LED_PIN = 11; // LED that signals motion detection
const uint BUSY_PIN = 25; // Built-in LED of Pico
const uint CALIBRATION_TIME = 6; // Calibration time in seconds

//
// IRQ handler called when rising or falling edge is detected
//

void pir_irq_handler(uint gpio, uint32_t event) {
if (event == GPIO_IRQ_EDGE_RISE) // rising edge => detection of movement
gpio_put(LED_PIN, 1); // turn LED on
else // falling edge
gpio_put(LED_PIN, 0);
}

//
// function used to calibrate PIR sensor
//

void calibrate () {
for (uint counter = 0; counter < CALIBRATION_TIME; counter++){
gpio_put(BUSY_PIN, 1);
sleep_ms(500);
gpio_put(BUSY_PIN, 0);
sleep_ms(500);
}
puts("Calibration completed");
}

int main()
{
stdio_init_all();
gpio_init(LED_PIN); // init LED Pin: used to signal motion detection
gpio_set_dir(LED_PIN, GPIO_OUT); // LED Pin is an output pin
gpio_init(BUSY_PIN); // init BUSY Pin: used to blink during calibration
gpio_set_dir(BUSY_PIN, GPIO_OUT); // BUSY Pin is an output pin

// Calibrate PIR for CALIBRATION_TIME seconds
calibrate();

// Enable interrupt handling for PIR Pin:
// Interrupt handling for rising or falling edges

gpio_set_irq_enabled_with_callback(PIR_PIN, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true, &pir_irq_handler);
while(true); // wait forever
return 0;
}

Das System im Einsatz
Das System im Einsatz

Fazit

Das initiale Aufsetzen von C/C++-Werkzeugen und -Projekten für den Raspberry Pi Pico erfordert einen hohen Aufwand. Sind die Hürden erst einmal gemeistert, geht das Entwickeln von Lösungen zügig voran. Es wäre schön, wenn auch "Embedded"-IDEs wie PlatformIO und die Arduino IDE bald eine Pico-Unterstützung anbieten, was nur noch eine Frage der Zeit sein dürfte, zumal Arduino bereits entsprechende Schritte für das hauseigene Board Arduino Nano RP2040 Connect angekündigt hat.

Vorläufig stürzen sich viele Autoren und Firmen im Internet eher auf das Programmieren des Pico mit MicroPython oder CircuitPython, was daran liegen könnte, dass die MicroPython-Bibliotheken viel Komplexität des SDKs verbergen, während dem C/C++-Entwickler der raue Wind der Embedded-Entwicklung ins Gesicht bläst. Allerdings bieten sich Anhängern von C/C++ dadurch auch mehr Flexibilität und Potenziale beim Ausschöpfen der Möglichkeiten des RP2040-Microcontrollers.

Konnte das vorliegende Posting nur mit einem kleinen Projekt aufwarten, erkunden zukünftige Folgen komplexere Beispiele.

Bis dahin viel Spaß bei den eigenen Explorationen.


URL dieses Artikels:
https://www.heise.de/-5991042

Links in diesem Artikel:
[1] https://git-scm.com/download/win
[2] https://www.python.org/downloads/windows/
[3] https://datasheets.raspberrypi.org/pico/raspberry-pi-pico-c-sdk.pdf
[4] https://datasheets.raspberrypi.org/pico/getting-started-with-pico.pdf
[5] https://datasheets.raspberrypi.org/pico/getting-started-with-pico.pdf
[6] https://cmake.org/download/
[7] https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm/downloads
[8] https://visualstudio.microsoft.com/de/thank-you-downloading-visual-studio/?sku=BuildTools&rel=16
[9] https://code.visualstudio.com/download
[10] https://www.doxygen.nl/index.html
[11] https://github.com/raspberrypi/pico-project-generator
[12] https://datasheets.raspberrypi.org/pico/getting-started-with-pico.pdf#page31
[13] https://wiki.freepascal.org/ARM_Embedded_Tutorial_-_Raspberry_Pi_Pico_Setting_up_for_Development
[14] https://hackaday.io/project/177198-pi-pico-picoprobe-and-vs-code
[15] https://smittytone.wordpress.com/2021/02/05/how-to-debug-a-raspberry-pi-pico-with-a-mac-swd/
[16] http://openocd.org/
[17] https://github.com/raspberrypi/picoprobe
[18] https://datasheets.raspberrypi.org/pico/getting-started-with-pico.pdf#page58
[19] https://github.com/Marus/cortex-debug
[20] https://www.heise.de/developer/artikel/Bewegungserkennung-durch-Infrarot-Strahlung-3221542.html
[21] https://github.com/ms1963/PirSensorRPiPico

Copyright © 2021 Heise Medien

Let's block ads! (Why?)

  • 19. März 2021 um 08:03

master

Von Alkarex

Prepare 1.18.1-dev

  • 15. März 2021 um 00:08

FreshRSS 1.18.0

Von Alkarex

Changelog:

  • Features
    • Allow parallel requests #3096
      • Much faster manual feeds refresh
    • Reload full article content when an article has changed #3506
    • New share article link to clipboard #3330
    • Improved OPML import of feeds with multiple categories #3286
    • Add a content action parameter to work with CSS selector #3453
    • New cURL options per feed: proxy, cookie, user-agent #3367, #3494, #3516
    • Do not import feeds causing database errors (e.g. due to conflicting HTTP redirections) ##3347
  • UI
    • New option to remember open categories #3185
    • Remember the scroll position of the sidebar #3231
    • Feedback messages are now properly attached to a request, in case multiple tabs are open #3208
    • New user query configuration page #3366
    • Allow sorting and drag & drop in the list of user queries #3346, #3355
    • Change layout to add a subscription #3289
    • Change integration configuration page #3372
    • Improve author search when clicking on an author #3315
    • Allow typing a label name instead of selecting it #3213
    • Use same behaviour for labels than the option Show all articles in favourites #3472
    • Change naming from Tag management to Label management #3446
    • Sort options alphabetically in share menu #3331
    • Case-insensitive sort order of feeds in category settings #3466
    • Better compression of the images #3184
    • Fix minor jaggy motion of the sidebar #3266
    • Remove useless reset action in sharing configuration page #3365
    • Add autofocus on subscription page #3334
    • Fix contrast issue by enforcing black text in base theme #3196
    • Adjust brightness & contrast of images and videos in dark themes #3356
    • Improve menu bar of several themes for mobile view #3480, #3491
    • Fix dropdown menu for user queries with BlueLagoon and Screwdriver themes #3485
    • Upgrade to jQuery 3.6.0 for statistics #3501
  • Bug fixing
    • Fix the reloading of full article content with SQLite #3461
    • Fix the caching of an SQL prepared statement affecting the read state of updated articles #3500
    • Better handle expected article conflicts in database #3409
    • Fix SQL syntax error/warning when deleting temporary articles #3357
    • Fix login and refresh bugs in anonymous mode #3305
    • Fix i18n init #3249
    • Fix tag management #3292
    • Fix user queries with labels #3285
    • Fix loading of default actions for shortcuts #3394
    • Fix extensions when using CLI #3443
    • Fix translation CLI #3364
    • Allow searching for + sign #3489
    • Fix cURL version detection in install script #3519
  • Compatibility
    • Support PHP 8+ #3186, #3207, #3459, #3487
      • Note: needed for MySQL 8+ with default authentication settings
      • Change ZIP-handling method #3470
  • API
    • Supported by FocusReader (Android) #3478
    • Implement OPML import/export #3424
    • Add canonical field for fluent-reader, better compatibility InoReader #3391
    • Better compatibility with BazQux API #3320
    • Fix continuation token by ensuring it is a string (for Reeder) #3247
  • CLI
    • Add requirements check in relevant CLI #3410
  • Deployment
    • Possibility to auto-install via e.g. docker-compose #3353
      • New tolerance when the database is not available / up (yet) by trying a few times to connect
    • Docker: use Apache remoteip to log the client remote IP instead of the local proxy IP #3226
    • Docker: new environment variable LISTEN to change the internal Apache port when running in host network mode #3343
    • Docker: Alpine image updated to 3.13 with PHP 8.0.3 and Apache 2.4.46 #3375
      • Note: needed for MySQL 8+ with default authentication settings #3191
    • Docker default image (Debian 10 Buster) updated to PHP 7.3.27
    • New Docker development image based on alpine:edge to test the latest PHP 8+ version #3294
    • New Docker development image based on alpine:3.4 to test oldest supported PHP 5.6.36 version with Apache 2.4.43 #3274
    • Disable built-in update mechanism for new installations using Docker #3496
    • Check that PHP session is working during install #3430
  • Security
    • Auto-renew login cookie #3287
    • Remove referer check, already replaced by CSRF token #3432
    • Increase login cookie duration to 3 months by default #3170
    • Prevent click on login button until JavaScript is fully loaded #3275
    • Add toggle password visibility button on login form UI #3205
    • Better sanitize feed description #3222
    • Allow @-sign in database username (for Azure) #3241
    • Add username hint for permissions during install #3373
  • SimplePie
    • SimplePie prevent cache pollution #3502
    • Update to SimplePie 1.5.6 with PHP8 support #3469, #3206
    • Reduce memory consumption to handle very big feeds #3416
    • Replace ceil() by intval() for edge cases with PHP8 #3404
    • Strip HTML comments in feeds #3288
    • Minor fix of return type for broken feeds #3423
    • Fix images in enclosures without MIME type #3361
  • Extensions
    • New user maintenance hooks #3440
    • New hooks js_vars and nav_menu #3342
    • Add class autoload for extensions #3350
    • Add support for extension user files #3433
    • Add user configuration in extensions #3397
    • Add a method to retrieve a configuration value #3422
    • Search files for both core and third-party extensions #3413
    • Updated base extension class #3333, #3335
    • Refactor extension file script #3399
  • i18n
  • Code improvements
    • Improve FreshRSS system initialisation #3070
    • Improve session code during install #3276
    • Enforce phpcs (PHP_CodeSniffer) line length + whitespace #3488
      • Improve settings and applies to *.phtml, *.css, *.js as well
    • Fix superfluous Minz check during install #3302
    • Extract some classes to their own files #3301, #3298, #3297
    • Explicit git declaration of .png files as binary #3211
    • Remove Minz validation #3439
    • Explicit PDO::ERRMODE_SILENT #3048
    • Add constant for minimal PHP version #3369
    • Refactor requirements check during install #3368
  • Misc.
    • Check access rights to temp folder during install #3312
    • Ensure maximum integer for a date to avoid some database issues #3259
    • Upgrade PHPMailer to 6.3.0 #3457
    • Make our Travis greener by testing only our oldest and newest supported PHP versions #3492
  • 14. März 2021 um 18:51

latest

Von Alkarex

Revert PHP version

  • 08. Dezember 2022 um 11:36

Das Adapter-Pattern in JavaScript

Von heise online

Das Adapter-Pattern in JavaScript

Tales from the Web side Philip Ackermann

Zugegeben, die Relevanz einiger GoF-Entwurfsmuster für JavaScript hält sich in Grenzen, wurden diese Entwurfsmuster doch ursprünglich dafür konzipiert, Rezepte für objektorientierte Programmiersprachen beziehungsweise das objektorientierte Programmierparadigma zu definieren.

Das Command-Pattern beispielsweise dient in erster Linie dazu, in der Objektorientierung Funktionen als Objekte zu behandeln und sie beispielsweise als Parameter einer anderen Funktion (bzw. im Kontext von Objekten: Methode) übergeben zu können. Dieses "Feature" ist in funktionalen Programmiersprachen bereits Teil des Programmierparadigmas: Funktionen sind hier "First-Class Citizens" und können wie Objekte behandelt werden, lassen sich beispielsweise Variablen zuweisen, einer anderen Funktion als Parameter übergeben oder ihr als Rückgabewert dienen.

Andere Entwurfsmuster hingegen wie das im Folgenden vorgestellte Adapter-Pattern ergeben durchaus auch in JavaScript Sinn. Der Einsatz des Adapter-Patterns in JavaScript ist vor allem dann in Betracht zu ziehen, wenn man es mit externem Code, sprich mit Third-Party-Bibliotheken zu tun hat.

Es war einmal ein Entwicklerteam ...

Dazu ein kurzes fiktives Beispiel: Angenommen, wir haben eine Applikation, in der an verschiedensten Stellen HTTP-Anfragen ausgeführt werden müssen. Statt selbst einen HTTP-Client zu implementieren, machen wir uns auf die Suche nach einer entsprechenden Bibliothek. Schließlich soll man das Rad nicht immer neu erfinden. Zum Glück gibt es im JavaScript-Universum recht viele Bibliotheken beziehungsweise Packages, und wir werden schnell fündig: Die Wahl fällt auf das Package request [1] (Spoiler: Unser Beispiel beginnt 2018 und wir wissen noch nicht, dass diese Wahl eine weniger gute war).

Wann immer wir in unserer Applikation nun eine HTTP-Anfrage stellen möchten, binden wir request wie folgt ein:

request('https://www.heise.de', (error, response, body) => {
// usw.
});

Insgesamt machen wir das an 32 Stellen. Und weil wir so große Fans von HTTP-Anfragen sind, erhöht sich die Anzahl schon nach wenigen Wochen auf 128. Das geht einige Zeit gut, doch nach ein paar Monaten erhalten wir schlechte Nachrichten (es ist jetzt der 30. März 2019): Mikeal Rogers [2], der Hauptentwickler hinter request, markiert die Bibliothek als "deprecated" und verkündet, dass sie zukünftig nicht mehr aktiv weiterentwickelt werde [3]. Nach einigem Hin und Her und mehrstündigen Diskussionen im Team entscheiden wir uns, unseren Code auf eine andere Bibliothek zu migrieren.

Schnell fällt die Wahl dabei auf axios [4]. Über 82.000 Stargazer bei GitHub? Das ist schon mal ein gutes Zeichen. Dass wir jetzt 128 Stellen in der eigenen Applikation anpassen müssen, nehmen wir zähneknirschend in Kauf, sind dieses Mal aber schlauer. Einer im Team bringt kleinlaut hervor, dass wir vielleicht eines der GoF-Entwurfsmuster einsetzen könnten. Er habe da neulich was in einem Buch über JavaScript gelesen. Einige der Patterns seien wohl auch in JavaScript sinnvoll. Nach anfänglicher Skepsis hören wir uns den Vorschlag an.

Das Adapter-Pattern in JavaScript

Statt externe Bibliotheken wie request oder axios direkt zu verwenden, definieren wir uns einfach eine eigene API, gegen die wir innerhalb unserer Applikation entwickeln. Im Fall der HTTP-Client-Funktionalität könnte die API also beispielsweise wie folgt aussehen (ja, in Form einer Klasse!). Die API definiert die Methoden, die Parameter, die Rückgabewerte und in unserem Fall auch, dass es sich um eine asynchrone API auf Basis von Promises (und nicht etwa auf Basis von Callbacks) handelt. In Bezug auf das Adapter-Pattern ist diese API also die "Adapter"-Komponente, die wir innerhalb unserer Applikation (als "Client") verwenden.

class HTTPClient {

constructor() {
// ...
}

async request(url, method, headers, body, config) {
return Promise.reject('Please implement');
}

async get(url, headers, body, config) {
return this.request(url, 'GET', headers, body, config)
}

async post(url, headers, body, config) {
return this.request(url, 'POST', headers, body, config)
}

// ...
}

Neben dieser internen API kommt jetzt die zweite (externe) API ins Spiel, die durch die externe Bibliothek (jetzt: axios) vorgegeben wird. Im Kontext des Adapter-Patterns handelt es sich bei dieser API um die "Adaptee"-Komponente. Das Bindeglied zwischen den beiden APIs (also zwischen "Adapter" und "Adaptee") kommt nun in Form eines "ConcreteAdapters", also einer konkreten Implementierung von HTTPClient. Für axios als Adaptee sähe eine – wohlgemerkt nur skizzierte – Implementierung wie folgt aus:

import HTTPClient from './HTTPClient';

class AxiosAdapter extends HTTPClient {

constructor(axios) {
this._axios = axios;
}

async request(url, method, headers, body, config) {
// hier Aufruf der "axios"-Bibliothek
// Adaptation der Parameter plus
// Adaptation des Rückgabewertes
}

async get(url, headers, body, config) {
// ...
}

// ...

}

Die Erzeugung der Adapter-Klasse lagern wir dabei aus, beispielsweise mit Hilfe einer Factory-Klasse (hey, noch ein GoF-Pattern!):

import axios from 'axios';
import AxiosAdapter from './AxiosAdapter';

class HTTPClientFactory {

static createHTTPClient() {
return new AxiosAdapter(axios);
}

}

Auf diese Weise können wir HTTP-Client-Instanzen in unserer Applikation wie folgt erzeugen und haben nichts direkt mit der externen API von axios zu tun:

import HTTPClientFactory from 'my-http-client';

const client = HTTPClientFactory.createHTTPClient();

Ein Jahr später ...

Knapp ein Jahr später – wir schreiben jetzt das Jahr 2020 – können wir uns glücklich schätzen, auf unseren Kollegen gehört zu haben. Mittlerweile wird unsere Klasse nicht mehr nur an 128 Stellen verwendet, sondern an 256! Diese würden wir nun wirklich nicht mehr alle ändern wollen.

Dass der Plan mit dem Adapter-Pattern aufgeht, merken wir auch, als es wieder soweit ist, die HTTP-Client-Bibliothek auszuwechseln: Nach einem Tweet [5] von Matteo Collina [6] werden wir auf die Bibliothek undici [7] aufmerksam. Zweimal so schnell wie der native HTTP-Client von Node.js soll sie sein. Das klingt sehr gut (zumal axios intern ja genau den nativen HTTP-Client verwendet). Sollten wir vielleicht wechseln? Klar, warum nicht?

Ist ja nicht viel Aufwand. Einfach eine neue Adapter-Klasse implementieren (hier wieder nur skizziert) ...

import HTTPClient from './HTTPClient';

class UndiciAdapter extends HTTPClient {

constructor(undici) {
this._undici = undici;
}

async request(url, method, headers, body, config) {
// hier Aufruf der "undici"-Bibliothek
// Adaptation der Parameter plus
// Adaptation des Rückgabewertes
}

async get(url, headers, body, config) {
// ...
}

// ...

}

... die Factory-Klasse anpassen ...

import undici from 'undici';
import UndiciAdapter from './UndiciAdapter';

class HTTPClientFactory {

static createHTTPClient() {
return new UndiciAdapter(undici);
}

}

... fertig!

Und falls wir doch wieder zur axios-Bibliothek zurückwechseln wollen, brauchen wir nur wenige Zeilen in der Factory ändern.

... und die Moral von der Geschicht'

"Hätten wir aber auch ohne Adapter-Pattern lösen können", meint einer im Team. Ja, hätten wir. Wir hätten auch alles nur mit Funktionen funktional lösen können. Das macht die Sprache JavaScript ja gerade so vielseitig. Und ja, hinter der Klassensyntax stecken keine echten Klassen, wie man sie aus Sprachen wie Java kennt. Und ja, wegen fehlender Interfaces in JavaScript sind viele GoF-Patterns umständlich zu realisieren. Dennoch kann man nicht abstreiten, dass das objektorientierte Programmierparadigma inklusive der objektorientierten Denkweise (auch der Denkweise in Patterns) unter Entwicklern sehr verbreitet ist und sich auf diese Weise schnell ein gemeinsames Verständnis vom Code schaffen lässt.

In diesem Sinne bleibt in Bezug auf die Integration externer APIs festzuhalten:

"Objektorientiert oder funktional,
Hauptsache es passt, der Rest ist egal."

(Unbekannter Entwickler im Team)


URL dieses Artikels:
https://www.heise.de/-5073865

Links in diesem Artikel:
[1] https://github.com/request/request
[2] https://twitter.com/mikeal
[3] https://github.com/request/request/issues/3142
[4] https://github.com/axios/axios
[5] https://twitter.com/matteocollina/status/1298148085210775553
[6] https://twitter.com/matteocollina
[7] https://github.com/nodejs/undici

Copyright © 2021 Heise Medien

Adblock test (Why?)

  • 08. März 2021 um 11:08

Enter at Your Own RISC! – Intermezzo mit RISC-V und dem ESP32-C3

Von heise online

Enter at Your Own RISC! – Intermezzo mit RISC-V und dem ESP32-C3

Der Pragmatische Architekt Michael Stal

In den letzten Folgen war vom Raspberry Pi Pico die Rede. Bevor sich das Blog weiterhin dem Pico widmet, adressiert dieses Extra-Posting den neuen Microcontroller ESP32-C3 von Espressif. Da der Chip auf der offenen RISC-V-Architektur basiert, geht der Artikel auch auf deren Grundlagen ein.

Die intensive Nutzung von Open-Source-Software gehört für uns längst zum Alltag, egal ob Linux, OpenOffice, etliche IDEs, Bibliotheken oder Beispiele für Embedded-Entwicklung. Offene Hardware existiert zwar auch schon länger, ist aber weitaus weniger verbreitet. Mit der offenen RISC-V-Architektur könnte Open-Source-Hardware mehr Fahrt aufnehmen.

Der vorlegende Beitrag stellt zunächst die Grundlagen von RISC-V vor, um sich danach dem neuen Microcontroller ESP32-C3 von Espressif zu widmen, der RISC-V implementiert.

Dominanz von ARM

In der IT-Community kennt jeder die Systemfamilie der ARM-Architekturen, die Hersteller nach Zahlung von Lizenzgebühren zur Eigenentwicklung von Prozessoren nutzen können. Im Bereich der Embedded-Systeme, Mobilgeräte und SBCs (Einplatinencomputer) hat sich ARM längst durchgesetzt. Und Apple hat neuerdings begonnen, eigen entwickelte ARM-Chips sogar in Notebook- und Desktopsystemen zu verbauen.

Einer der Nachteile des ARM-Geschäftsmodells sind die Lizenzgebühren, was speziell für kleinere Hersteller zutrifft. Nicht alle Unternehmen können es sich schließlich leisten, Prozessoren zu entwerfen, die sich dem eigenen Anwendungsfall optimal anpassen. Mit der kostenfreien RISC-V-Architektur (ausgesprochen: “RISC-Five”) könnte sich daher ernsthafte Konkurrenz am Markt etablieren.

RISC statt CISC

Während meines Studiums spielte RISC (Reduced Instruction Set Computer) eine wichtige Rolle. Den Anfang meines Arbeitslebens verbrachte ich folgerichtig vor RISC-basierten Sun-Workstations, unter deren Motorhaube SPARC-Chips steckten. RISC zeichnet sich gemäß der 80:20-Regel unter anderem dadurch aus, dass es sich auf wenige, häufig verwendete Maschineninstruktionen beschränkt, einen großen Satz an Registern besitzt und Instruktionen in der Regel mit der Breite von einem Maschinenwort, zum Beispiel mit 32 Bits oder 64 Bits, verwendet, deren Abarbeitung in einem CPU-Zyklus erfolgt. Das macht die Entwicklung solcher CPUs einfacher, den Bau von Compilern aber komplexer. Im Gegensatz dazu weisen CISC-CPUs (CISC = Complex Instruction Set Computer) wie die von AMD und Intel einen sehr mächtigen und dafür komplexen Befehlssatz auf. Weiterer positiver “Nebeneffekt”: Hersteller können sehr energieeffiziente RISC-CPUs bauen, womit sich Halbleiterriesen wie Intel oder AMD erfahrungsgemäß schwer tun.

Wesentliche Ingredienz jeder Prozessorfamilie ist der verwendete Befehlssatz (Instruction Set), weil er die Schnittstelle zwischen Hardwarearchitektur und Software vorgibt. Das betrifft Softwareentwickler zwar normalerweise wenig oder nicht, zumal sich durch Hardwareabstraktionsschichten wie dem Arduino Core, der JVM, der CLR oder einem Python-Interpreter die Hardware gut ausblenden lässt. Es betrifft aber die Entwickler von Compilern oder Interpretern, die für jede Hardware entsprechende Werkzeuge bereitstellen müssen. Daher wäre es von Vorteil, gäbe es einen einheitlichen Befehlssatz für Prozessoren. Das ist natürlich unrealistisch, aber ließe sich zumindest teilweise realisieren, womit wir bei RISC-V angelangt wären.

Was ist RISC-V

RISC-V [1]definiert einen offenen Befehlssatz, der nicht patentiert und stattdessen über eine BSD-Lizenz frei nutzbar ist. Seine Entwicklung begann 2010 an der University of California at Berkeley. Ziel war unter anderem das Bereitstellen einer eigenen Architektur für Lehrzwecke, speziell für die Vermittlung von Parallelisierung im Unterricht. Als die amerikanische DARPA und Unternehmen wie Microsoft und ST Microelectronics das Potenzial erkannten, hat sich aus dem akademischen Konzept eine Lösung für Universitäten und Industrie entwickelt.

Hauptgrund ist die Flexibilität: Die ISA (Instruction Set Architecture) von RISC-V lässt sich sowohl für die Kreierung von 32-Bit- als auch für die von 64-Bit-Prozessoren nutzen. Sogar 128-Bit-Prozessorerarchitekturen sind damit machbar, was die Zukunftssicherheit von RISC-V gewährleistet. Darüber hinaus eignet sich der zugrunde liegende Befehlssatz sehr gut für die Virtualisierung über Hypervisor, für Bare-Metal-Systeme oder als ideale Plattform für Betriebssysteme wie Linux.

Eine Hardwareplattform auf Basis von RISC-V enthält verschiedene Prozessorkerne, worunter sich auch solche befinden können, die nicht RISC-V-konform sind. Dazu stoßen gegebenenfalls Koprozessoren, die den Befehlssatz erweitern und sich in den RISC-V-Befehlsstrom integrieren. Zusätzlich existieren Accelerators (Beschleuniger), die eine abgeschlossene Funktion oder einen eigenen Kern definieren, und autonome Aufgaben leisten, etwa die der I/O-Verarbeitung.

Mithilfe dieses modularen Konzepts sind Hardwareimplementierungen vom Einkern-Microcontroller, über Many-Core-Systeme bis hin zu zu großen Clustern von Shared-Memory-basierten Netzwerkknoten denkbar.

Modularität ist Trumpf

Das Fundament von RISC-V bildet ein Befehlssatz für ganze Zahlen, der sich stark an die früheren RISC-Architekturen anlehnt. Eigentlich sind es sogar vier Befehlssätze, von denen sich einer auf 32-Bit-, ein anderer auf 64-Bit-Verarbeitung spezialisiert. Ein weiterer Befehlssatz reduziert die Zahl der Register für den Einsatz in Embedded-Controllern, während der vierte 128-Bit-Architekturen unterstützt. Alle genannten Basis-Befehlssätze sind erweiterbar. Sogar Befehle mit variablen Längen sind damit möglich (siehe RISC-V-Spezifikationen [2]).

Zu den Standarderweiterungen gehören unter anderem:

  • M: Multiplikation und Division von ganzen Zahlen
  • A: Atomare Operationen
  • F: Gleitkommazahlen mit einfacher Genauigkeit
  • D: Gleitkommazahlen mit doppelter Genauigkeit
  • Q: Gleitkommazahlen mit vierfacher Genauigkeit
  • L: Dezimale Gleitkommazahlen
  • B: Instruktionen für Bit-Manipulation
  • J: Instruktionen für dynamisch übersetzte Programmiersprachen
  • T: Transaktionaler Speicher
  • V: Befehle für Vektoroperationen

Obere Liste ist im übrigen nicht vollständig.

Wer den Befehlssatz einmal genauer unter die Lupe nehmen will, findet ihn zu Beispiel hier [3].

Die Komplexität eines RISC-V-Prozessorkerns, der alle Standard-Erweiterungen implementiert, kann also durchaus die luftigen Höhen einer General Purpose CPU erreichen.

Es gibt noch eine weitere spannende Dimension von RISC-V. Neben einer "Unprivileged"-Architektur, von der bislang die Rede war, existiert eine "Privileged"-Architektur, die einen Stack verschiedener Privilegierungsebenen definiert, angefangen von der Maschine über den Supervisor hin zu Anwendungen und Anwendern. Entwickler schreiben Code meistens für eine dieser Schichten. Code auf der Maschinenebene (M-mode) gilt üblicherweise als vertrauenswürdig und genießt die höchsten Privilegien. Dahingegen läuft Code auf der Supervisorebene (S-mode) mit weniger Privilegien ab. Hier tummeln sich zum Beispiel Betriebssysteme. Ganz oben – oder sollte ich lieber sagen ganz unten?! – laufen Anwendungen im "unterprivilegierten" U-Mode (U=User). Volume II des RISC-V Manuals zur Instruction Set Architecture adressiert die Privileged Architecture und führt entsprechende Erweiterungen ein, um Maschineninstruktionen mit verschiedenen Privilegierungsstufen zu unterstützen.

Theorie und Praxis

Da RISC-V "bloß" eine Spezifikation repräsentiert, heißt die Frage, ob an deren Umsetzung überhaupt jemand Interesse zeigt. Natürlich lautet die Antwort ja, weil sich dieses Posting sonst damit nicht beschäftigen würde. Beispielsweise entwickeln Western Digital und Nvidia entsprechende Implementierungen. Von der ETH Zürich stammt das RISC-V-basierte PULPino-Board.

Darf es etwas mehr sein? Zahlreiche weitere Produkte und Entwicklungen finden sich auf der Website von RISC-V. Daraus wird ersichtlich, dass RISC-V schon jetzt viele Anhänger besitzt, sowohl im universitären als auch im industriellen Bereich (siehe Liste [4]). Zum Üben mit dem RISC-V-Befehlssatz existiert eine Visual-Studio-Code-Erweiterung, die den Venus-Simulator verwendet (siehe hier [5]).

RISC-V goes Embedded

Der Embedded Bereich bietet inzwischen sogar einige Produkte für den "Hausgebrauch" wie:

  • Espressif's ESP32-C3
  • diverse Boards von SiFive
  • der M5StickV von M5Stack
  • die SiPEED-Boards Maix-Bit, Maixduino, Longan Nano
SiFive
Das HiFive-Board von SiFive enthält ebenfalls einen Microcontroller auf Basis von RISC-V (Bild: SiFive)

Das Longan Nano lässt sich über Aliexpress für Preise ab rund 2,70 Euro erwerben und ist noch dazu kompatibel zu Arduino-Boards (siehe SiPEED-Webseite [6]).

Entwickler können folglich bereits entsprechende Produkte für bezahlbare und attraktive Preise erwerben. Daraus folgt: RISC-V ist inzwischen Realität und nicht bloß eine Sammlung von Spezifikationen. Das Explorieren kann somit beginnen.

RISC-V International
Schon jetzt unterstützen viele Firmen und Universitäten RISC-V. Hier nur ein kleiner Ausschnitt. (Bild: RISC-V International)

Espressif ESP32-C3

Dankenswerterweise hat mir Espressif ein Vorserienmodell seines Boards mit dem Namen ESP32-C3-DevKitM-1 zur Verfügung gestellt. Das soll hier im Fokus stehen. Trotz des ESP32 in seinem Namen sollte man den Microcontroller ESP32-C3 eher als Evolution des ESP8266 betrachten. Genauer gesagt, positioniert er sich bezüglich Leistung und Fähigkeiten zwischen dem ESP8266 und dem ESP32.

Espressif
Espressif's ESP32-C3-DevkitM-1 (Bild: Espressif)

Er soll auch zu sehr wettbewerbsfähigen Preisen auf den Markt kommen und zeigt sich wie seine Geschwister dank Bluetooth 5.0 und 2.4 GHz WiFi überaus kommunikativ. Der in 40-nm-Technik gefertigte RISC-V-Prozessor besitzt einen einzelnen Kern und erlaubt bis zu 160 MHz Taktfrequenz. Neben 384 KBytes ROM stehen 400 KBytes SRAM, darunter 16 KBytes als Cache zur Verfügung. Die Echtzeituhr verfügt darüber hinaus über 8 KByte Speicher.

Des Weiteren implementiert ein ESP32-C3 folgende Ports und Schnittstellen:

  • 22 × programmierbare GPIOs
  • 2 × 12-bit SAR ADCs, mit bis zu sechs Kanälen
  • 1 × Temperatursensor
  • 3 × SPI
  • 2 × UART
  • 1 × I2C
  • 1 × I2S
  • Einheit zur Remote-Steuerung mit zwei Übertragungskanälen und zwei Empfangskanälen
  • LED-PWM-Controller, mit bis zu sechs Kanälen
  • DMA-Controller, mit drei Übertragungskanälen und drei Empfangskanälen
  • 1 × CAN-Bus-Controller (kompatibel mit ISO 11898-1)
  • eingebaute Sicherheitshardware
Blockdiagramm des RISC-V-basierten ESP32-C3
Blockdiagramm des RISC-V-basierten ESP32-C3

Eine integrierte PMU (Power Management Unit) sorgt mit fünf Energiestufen für die Anpassung an eigene Bedürfnisse, etwa für einen Tiefschlafmodus mit geringstmöglichem Energieverbrauch. Wer mehr Details über den ESP32-C3 erfahren möchte, findet im Web ein entsprechendes Datasheet [7].

Programmierwerkzeuge

Zum Experimentieren mit dem ESP32-C3 beziehungsweise dem Board ESP32-C3-DevKitM-1 gibt es eine gute Dokumentation [8] von Espressif. Programmieren können interessierte Entwickler entweder kommandozeilenorientiert über die ESP-IDF-Werkzeuge (siehe meinen früheren Artikel hier [9]) oder mit entsprechenden Plug-ins in Eclipse oder Visual Studio Code.

Programmbeispiel

Das nachfolgende Beispiel führt zum Blinken der eingebauten LED. Das Programm nutzt die Multithreading-Unterstützung der FreeRTOS-Firmware, um den einzigen Thread jeweils gezielt für eine Sekunde anzuhalten. Der Rest der Anwendung definiert den GPIO für die eingebaute LED als Ausgang (gpio_set_direction())), setzt ihn mit reset()in einen definierten Zustand, um dann abwechselnd über ein gpio_set_level() erst ein 0- und dann ein 1-Signal auszugeben. Die printf()-Aufrufe sorgen für Textausgaben auf dem seriellen Terminal.

/* Blink Beispiel */
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "sdkconfig.h"

/* Genutzt wird der konfigurierte GPIO für die eingebaute LED */
#define BLINK_GPIO CONFIG_BLINK_GPIO

void app_main(void) {
/* GPIO zurücksetzen */
gpio_reset_pin(BLINK_GPIO);
/* GPIO für die LED ist ein Ausgang */
gpio_set_direction(BLINK_GPIO, GPIO_MODE_OUTPUT);
while(1) {
/* LED aus */
printf("LED ausgeschalten");
/* das heisst 0 am GPIO ausgeben */
gpio_set_level(BLINK_GPIO, 0);
/* 1 Sekunde Wartezeit des Threads */
vTaskDelay(1000 / portTICK_PERIOD_MS);
/* LED ein */
printf("LED eingeschalten");
/* das heisst 1 am GPIO ausgeben */
gpio_set_level(BLINK_GPIO, 1);
/* 1 Sekunde Wartezeit des Threads */
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}

Fazit

RISC-V hat großes Potenzial, sich zu einer Erfolgsgeschichte zu entwickeln. Gerade Halbleiterschmieden ergreifen die Chance der offenen Befehlsarchitektur, aber auch Embedded-Hersteller nutzen die Gunst der Stunde. Ob sich die Architektur auch auf Desktops oder Notebooks durchsetzen kann, bleibt abzuwarten. Für Entwickler ist es zunächst egal, welche Hardware ein System anbietet. Indirekt hätte aber eine einheitliche Befehlsarchitektur den Vorteil eines großen Software- und Hardware-Ökosystems. Und daher bleibt die weitere Entwicklung von RISC-V spannend.

In der nächsten Folge setzt sich dieser Blog wieder mit IDEs und Projekten für den Raspberry Pi Pico auseinander.


URL dieses Artikels:
https://www.heise.de/-5072875

Links in diesem Artikel:
[1] https://riscv.org/
[2] https://riscv.org/specifications/
[3] https://6004.mit.edu/web/_static/test/resources/references/6004_isa_reference.pdf
[4] https://github.com/riscv/riscv-cores-list
[5] https://marketplace.visualstudio.com/items?itemName=hm.riscv-venus
[6] https://longan.sipeed.com/en/
[7] https://www.espressif.com/sites/default/files/documentation/esp32-c3_datasheet_en.pdf
[8] https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/get-started/index.html
[9] https://www.heise.de/developer/artikel/ESP32-to-go-4452689.html

Copyright © 2021 Heise Medien

Let's block ads! (Why?)

  • 05. März 2021 um 16:08

Gutes Wetter mit Raspberry Pi Pico, MicroPython und Visual Studio Code

Von heise online

Gutes Wetter mit Raspberry Pi Pico, MicroPython und Visual Studio Code

Der Pragmatische Architekt Michael Stal

Es geht weiter mit der Reise durch das Pico-Land. Während in der letzten Folge die Python-IDE Thonny zur Sprache kam, konzentrieren sich die jetzige und die kommende Folge auf Visual Studio Code für die Pico-Entwicklung unter Python und C beziehungsweise C++.

Die Thonny-IDE eignet sich sowohl für den Einstieg in die Programmiersprache Python beziehungsweise MicroPython als auch für Experimente mit dem Raspberry Pi Pico. Deshalb war meine Andeutung in der vergangenen Folge etwas despektierlich, Thonny könnte bei (semi-)professionellen Entwicklern keinen Blumentopf gewinnen. Immerhin stellt es eine ideale Spielwiese zur Verfügung.

Für größere Projekten mit komplexerem Code und mehreren Beteiligten skaliert eine einfache IDE allerdings nicht. Dort benötigen Entwickler beispielsweise Werkzeuge für Sourcecode-Verwaltung, ausgereiftere Testmöglichkeiten und leistungsfähiges Management von Ressourcen wie Bibliotheken.

Benötigte Software-Ingredienzen

Doch woher nehmen und nicht stehlen? Vier Dinge brauchen anspruchsvollere Python-Entwickler, um auf einem Linux-, macOS- oder Windows-Host Anwendungen für den Raspberry Pi Pico zu entwickeln, wobei ausschließlich 64-Bit-Betriebssysteme infrage kommen:

  • das SDK von Python in einer aktuellen Version,
  • Visual Studio Code (VS Code) als modulare Programmierumgebung (siehe diesen Blogartikel [1]),
  • Node.js und
  • für VS Code eine Erweiterung (engl. Extension) namens Pico-Go. Diese ist eine Ableitung aus der PyMakr-Erweiterung von Pycom.

Übrigens: Besitzer eines Computers mit Apple Silicon dürfen sich darüber freuen, dass alle Werkzeuge auch unter einem M1-basierten System zur Verfügung stehen.

Weitere gute Nachricht: Die genannten Anwendungen sind kostenlos verfügbar.

Pico-Go

Für die Installation der Combo liefert der Entwickler von Pico-Go, Chris Wood, bereits eine ausführliche englischsprachige Dokumentation auf seiner Webseite [2]. Deshalb an dieser Stelle nur eine grobe Zusammenfassung der entsprechenden Referenzen:

Sind alle aufgezeigten Pakete betriebsbereit, lässt sich in VS Code die Erweiterung Pico-Go von Chris Wood installieren.

Die Extension Pico-Go hilft bei der MicroPython-Entwicklung für den Raspberry Pi Pico unter Visual Studio Code
Die Extension Pico-Go hilft bei der MicroPython-Entwicklung für den Raspberry Pi Pico unter Visual Studio Code

Der Spaß beginnt

Zunächst empfiehlt es sich, einen Ordner für Python-Projekte zu kreieren, diesen unter VS Code zu öffnen, mit Ctrl + Shift + p (Windows, Linux) beziehungsweise Command + Shift + p (Mac) die Kommandopalette zu laden, um das Kommando Pico-Go | Configure project via Palette auszuführen. Dadurch sind unter anderem Code-Vervollständigung und Syntaxprüfung (Lint) nutzbar.

Sobald Nutzer einen Raspberry Pi Pico an das Hostsystem anschließen, erkennt ihn Pico-Go und teilt dies über die untere Statusleiste der IDE mit.

Jetzt können Entwickler unter VS Code eine Programmdatei (Endung .py) anlegen, um dort MicroPython-Quellcode einzufügen. Das fertige Programm lässt sich über das Run-Kommando (untere VS-Code-Statusleiste!) auf dem Pico ausführen, oder mittels Upload auf das Board kopieren, worauf der Pico einen Reset durchführt und danach das Programm automatisch startet. Wichtig: Vor dem Upload einer Programmdatei sollten Entwickler sie in main.py umtaufen, weil der Pico andernfalls nur das Programm ablegt, ohne es weiter zu beachten.

Das Beispielprojekt "Heise Wetter"

Nach erfolgreicher Installation und Konfiguration von Pico-Go ist es an der Zeit, sich dem eigentlichen Thema zu widmen, einem Embedded-Projekt für den Pico. Entstehen soll mithilfe des Sensors BME280 eine kleine Wetterstation, die periodisch Temperatur, Luftfeuchtigkeit und Luftdruck misst.

Der Wettersensor BME280 bildet das Herz der Schaltung
Der Wettersensor BME280 bildet das Herz der Schaltung

Grundsätzliche Idee: Über den Pico erfolgt der Zugriff auf den BME280-Sensor, der für die Erfassung der Messwerte für Temperatur, Luftfeuchtigkeit und Luftdruck zuständig ist. Die entsprechenden Werte stellt die Wetterstation auf dem SSD1306-Display dar. Zugleich gibt es eine kodierte Regel über das augenblickliche "Klima". Liegen die Werte im angenehmen Bereich, aktiviert das Programm die grüne LED. Sind Feuchtigkeit und Temperatur jenseits der eigenen Komfortzone, leuchtet die rote LED. Liegen die Werte irgendwo dazwischen, teilt die Anwendung dies über die gelbe LED mit. Diese Regel ist als eigene Funktion kodiert und lässt sich dementsprechend anpassen. Auch die Zeitdauer zwischen den Abfragen (SLEEPTIME ist konfigurierbar).

Das günstige OLE-Display mit 128 x 64 Pixeln Auflösung reicht für den vorgesehenen Zweck
Das günstige OLE-Display mit 128 x 64 Pixeln Auflösung reicht für den vorgesehenen Zweck

Übrigens können durch Auflegen einer Fingerspitze auf den BME280 Temperatur und Feuchtigkeit stark nach oben steigen. Insofern lässt sich die Klimaregel manuell testen.

Hardware BoM (Bill of Material)

Für das Projekt benötigen wir folgende Stückliste:

  • Raspberry Pi Pico: ca. 5 Euro
  • Zwei Reihen 20er-Header-Pins: ca. 0,50 Euro
  • Breadboard für 3,50 Euro
  • Kabel zum Verbinden des Pico Micro-USB mit der USB-Schnittstelle des Hosts: ca. 3 Euro
  • Je eine rote, gelbe, grüne LED: ca. 0,30 Euro
  • 3 Widerstände (z. B. 120 Ohm, 150 Ohm oder 220 Ohm): ca. 0,10 Euro
  • Bosch Sensortec BME280 Breakout-Board mit I2C-Bus: ca. 6 Euro
  • SSD1306-OLED-Display mit 128x64 Pixeln und I2C-Bus: ca. 6 Euro
  • 14 Verbindungen Jumper-Wire männlich auf männlich: ca. 5 Euro im 120er-Pack

Gesamt: ca. 29 Euro

Wer bei den üblichen Verdächtigen in China zuschlägt, kann die Anschaffungssumme noch weiter reduzieren.

Die Schaltung

Auf dem mit Fritzing gezeichneten Schaltungsdiagramm sind rechts unten das OLED-Display SSD1306 mit 128x68 Punkten Auflösung abgebildet und rechts oben der Umweltsensor BME280. Beide sind mit dem I2C-Bus des Pico verbunden, SDA liegt dort auf Pin 6 (= GPIO 4) und SCL auf Pin 7 (= GPIO 5). GPIO steht für General Purpose IO.

Die Schaltung der Wetterstation als Fritzing-Diagramm
Die Schaltung der Wetterstation als Fritzing-Diagramm

Achtung: Die physikalischen Pins haben keinen direkten Bezug zu ihrer logischen Bezeichnung. Der physikalische Pin 6 entspricht zum Beispiel dem logischen Pin GPIO 4, der physikalische Pin 25 dem logischen Pin GPIO 19. In Anwendungen für den Pico oder einem anderen Raspberry Pi Board sind in der Regel die logischen Namen gemeint.

  • Die Spannungsversorgung der Komponenten durch den Pico erfolgt mit 3,3 V über dessen Vcc-Ausgang (Pin 36) und einem der verschiedenen GND-Ports (im Diagramm: Pin 38).
  • Für den Anschluss der LEDs über jeweils einen 150-Ohm-Widerstand sind Pin 25 (rot), Pin 26 (gelb) und Pin 27 (grün) vorgesehen. Das entspricht den GPIO-Ports 19, 20 und 21.
Das Pin-Layout des Pico. Achtung: Logische Pins sind ungleich physikalischen Pins
Das Pin-Layout des Pico. Achtung: Logische Pins sind ungleich physikalischen Pins

Damit ist schon alles Wesentliche über die Schaltung gesagt.

Die Programmierung

Kommen wir zur Software. Im ersten Schritt ist ein Treiber für das OLED-Display SSD1306 notwendig. Den hat bereits Stefan Lehmann vom Kunststoff-Zentrum Leipzig implementiert. Der Treiber lässt sich über seine Github-Seite [7] importieren.

Für die Ansteuerung des BME280 von Bosch habe ich einige Beispielimplementierungen unter die Lupe genommen und als Vorlage für einen eigenen Treiber kodiert. Bosch Sensortec stellt zu diesem Zweck ein hilfreiches Dokument über den Sensor auf seiner Webseite [8] zur Verfügung.

Der Sensor misst neben Temperatur (in Grad Celsius) und Feuchtigkeit (in Prozent) auch den Luftdruck (in mbar beziehungsweise HectoPascal). Wer auf die Messung der Luftfeuchtigkeit verzichten kann, sollte auf die billigere Variante BMP280 zugreifen. Dieser Sensor lässt sich teilweise bereits für unter einem Euro im Internet erwerben.

Damit niemand mühsam das Beispielprogramm abtippen oder mit Copy&Paste zusammenschneiden muss, habe ich den Code auf einer Github-Seite bereitgestellt. Hier der Link für das GitHub-Repository [9].

Im Folgenden kommen hauptsächlich einige wichtige Fragmente zur Sprache, die dem Verständnis dienen.

Für die Implementierung des Programmes sind einige Bibliotheken notwendig, die meisten davon aus den Pico- beziehungsweise Micropython-SDKs. Die Bibliothek für die Ansteuerung des OLED-Displays ist, wie bereits erwähnt, auf GitHub zu finden.

from machine import Pin, I2C    # Wir brauchen Pin und I2C des Pico
from micropython import const
from ustruct import unpack as unp
from ssd1306 import SSD1306_I2C # Modul für SSD1306
import utime # Zwei Zeit-Bibliotheken
import time

Für die Entscheidung, ob das Wetter gut, schlecht, mittel ist, fungiert die Funktion condition(). Diese enthält eine eigene Regel, die jeder für sich selbst ändern kann:

#--Condition ----------------------
COND_RED = 1 # Schlechtes Klima
COND_GREEN = 2 # Angenehmes Klima
COND_YELLOW = 3 # Mittleres Klima
#----------------------------------

ComfortZoneTemp = (15,25) # Meine Komfortzone für Temperatur liegt zwischen 15 und 25 Grad
ComfortZoneHumi = (10,40) # Meine Komfortzone für Feuchtigkeit liegt zwischen 10 und 40%
#
def condition(temperature, humidity, pressure):
niceTemperature = temperature >= ComfortZoneTemp[0] and temperature <= ComfortZoneTemp[1]
niceHumidity = humidity >= ComfortZoneHumi[0] and humidity <= ComfortZoneHumi[1]
if niceHumidity and niceTemperature:
return COND_GREEN
elif (niceHumidity != niceTemperature): # XOR
return COND_YELLOW
else:
return COND_RED
Die Anwendung initialisiert den I2C-Bus zum Zugriff auf BME280 und SSD1306:#

Die Anwendung initialisiert den I2C-Bus zum Zugriff auf BME280 und SSD1306:

sda = Pin(4) # BME280 und SSD1306 sind an GPIO 4 und 5 angeschlossen
scl = Pin(5)
i2c = I2C(0,sda=sda,scl=scl,freq=400000) # I2C-Bus 0
i2c_addr_bme = 0x76 # Ich gehe davon aus, der BME280 liegt an 0x76

Die LEDs befinden sich an den GPIO-Ports 19, 20, 21:

GreenLED  = Pin(21, Pin.OUT) # Grüne LED an GPIO 21
YellowLED = Pin(20, Pin.OUT) # Gelbe LED an GPIO 20
RedLED = Pin(19, Pin.OUT) # Rote LED an GPIO 19

Die Variable SLEEPTIME legt die Zeit zwischen zwei Messungen fest:

SLEEPTIME = 5

Hinter der Klasse BMX280 verbirgt sich der Treiber für den Umweltsensor. Deren Konstruktor nimmt etliche Initialisierungen vor. Interessant für Anwender ist hauptsächlich die Methode measure(), weil sie Druck, Feuchtigkeit und Temperatur zurückliefert.

class BMX280:
# Im Konstruktor werden primäre Datenmember und Konstanten belegt
def __init__(self, i2c, i2c_addr_bme)
def measure(self)
# ... diverse Hilfsmethoden ...

Das Hauptprogramm der Software sucht zunächst nach Sensoren und Aktoren am I2C-Bus – optional, weil abhängig von der Belegung der boole’schen Variable debug.

if debug:
print('Ich habe an folgenden Adressen Komponenten am I2C-Bus entdeckt:')
devices = i2c.scan()
if devices:
for i in devices:
print(hex(i))
utime.sleep_ms(2000)

Gilt debug == True, erfolgt an weiteren Stellen die Bildschirmausgabe am Terminal der IDE.

Anschließend initialisiert die Anwendung die Treiber für SSD1306 (Klasse SSD1306_I2C) und BME280 beziehungsweise BMP280 (Klasse BMX280).

In der Hauptschleife liest der Code über measure() die Wetterwerte ein, aktiviert abhängig von den Messwerten entweder die rote, grüne oder gelbe LED, und stellt die Werte am OLED-Display dar. Nach einer Wartezeit folgt der Übergang zur nächsten Messrunde:

oled = SSD1306_I2C(128,64,i2c)
bme = BMX280(i2c = i2c, i2c_addr_bme = i2c_addr_bme)

oled.fill(0)
oled.show()

# HAUPTSCHLEIFE #
while True:
temperature, humidity, pressure = bme.measure()
#......................
currentState = condition(temperature, humidity, pressure)
if currentState == COND_GREEN:
GreenLED.value(1)
YellowLED.value(0)
RedLED.value(0)
if debug:
print("Angenehmes Klima")
elif currentState == COND_YELLOW:
GreenLED.value(0)
YellowLED.value(1)
RedLED.value(0)
if debug:
print("Geht so")
elif currentState == COND_RED:
GreenLED.value(0)
YellowLED.value(0)
RedLED.value(1)
if debug:
print("Unangenehmes Klima")
# ......................
oled.fill(0)
oled.text("Heise Wetter", 5, 10)
# Formatierte Ausgabe mit 7 Ziffern bei 2 Nachkommastellen
oled.text(str('% 7.2f' % temperature) + " Grad", 5,20)
oled.text(str('% 7.2f' % humidity) + " %",5,30)
oled.text(str('% 7.2f' % pressure) + " HPa",5,40)
# Und jetzt enthuellen
oled.show()
utime.sleep(SLEEPTIME) # Schlafen bis zur nächsten Messung

Der fertige und funktionierende Versuchsaufbau sieht folgendermaßen aus:

Das fertige Projekt in einem prototypischen Aufbau
Das fertige Projekt in einem prototypischen Aufbau

Fazit

Das Beispielprojekt hat erste Einblicke in die MicroPython-Programmierung des Pico unter Visual Studio Code gegeben. Ein Problem, das sich momentan noch zeigt, sind fehlende Module für die Ansteuerung einiger Bauteile, insbesondere von Breakout-Boards und komplexeren Sensoren. Da der Pico noch sehr neu ist, dürfte sich dies in den nächsten Monaten ändern und dadurch ein ähnlich breites Ökosystem entstehen, wie es bei Arduino, Espressif (ESP32, ESP8266), Raspberry Pi Single-Board-Computer, Adafruit, Sparkfun und ST Microelectronics bereits der Fall ist.

Natürlich gibt es für das gezeigte Beispiel diverse Optimierungsmöglichkeiten, etwa:

  • Das Programm kümmert sich nicht um Energieeffizienz. Wer eine Wetterstation unabhängig vom Stromnetz betreiben möchte, könnte zum Beispiel die verschiedenen Schlafmöglichkeiten des Pico und der verwendeten Bauteile nutzen.
  • Die Schaltung ist weiter ausbaubar, etwa durch Hinzunahme von Sensoren für Gewittererkennung, Regen, Bodenfeuchtigkeit, Luftqualität, Feinstaub und CO2-Belastung.
  • Für Wettermessungen gibt es neben den BME280 viele Alternativen, die sich einsetzen lassen, zum Beispiel der DHT22.
  • Manche Breakout-Boards für BME280 oder SSD1306 benutzen statt des I2C-Bus den SPI-Bus, der eine andere Verdrahtung erfordert.
  • Es wären auch andere Anzeigen als der monochrome SSD1306 denkbar, etwa Displays mit höherer Auflösung und/oder mehreren Farben, das Vorhandensein oder die Eigenentwicklung entsprechender Treibersoftware vorausgesetzt.
  • Anstelle des BME280 ließe sich auch der neuere Sensor BME680 nutzen, was sich aber preislich auswirkt. Die Kosten belaufen sich in diesem Fall auf um die 20 Euro. Außerdem habe ich dafür noch keinen Treiber entdeckt. Vorteil des BME680 wäre die Berechnung der Luftqualität.

Interessierte Leser haben also noch genug Möglichkeiten, sich weiter auszutoben.

Der Fokus dieses Artikels lag auf MicroPython. Übrigens existiert auch CircuitPython (entwickelt von Adafruit) als Alternative, aber davon soll nicht weiter die Rede sein, da die konzeptionellen Unterschiede eher marginal sind. In der nächsten Folge geht es um die Programmierung mit C beziehungsweise C++.

Bis dahin viel Spaß beim Picomentieren.


URL dieses Artikels:
https://www.heise.de/-5066047

Links in diesem Artikel:
[1] https://www.heise.de/developer/artikel/VS-Code-lebt-Entwicklung-von-Embedded-Software-mit-PlatformIO-4464669.html
[2] http://pico-go.net/docs/start/quick/
[3] https://www.python.org/downloads/
[4] https://nodejs.org/en/download/
[5] https://code.visualstudio.com/download
[6] https://code.visualstudio.com/insiders/
[7] https://github.com/stlehmann/micropython-ssd1306
[8] https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme280-ds002.pdf
[9] https://github.com/ms1963/DerPragmatischeArchitekt-WetterStation

Copyright © 2021 Heise Medien

Let's block ads! (Why?)

  • 26. Februar 2021 um 15:41

Erste Schritte mit dem Raspberry Pi Pico – die Thonny-IDE

Von heise online

Erste Schritte mit dem Raspberry Pi Pico – die Thonny-IDE

Der Pragmatische Architekt Michael Stal

Der letzte Artikel [1]hat das neue Board Raspberry Pi Pico vorgestellt. In diesem und den nachfolgenden Teilen ist von den verschiedenen Möglichkeiten die Rede, wie sich das Board zum Leben erwecken lässt. Die Reise beginnt mit der kostenlosen Python-IDE Thonny.

Die Thonny-IDE bietet eine Python-Shell, die eher einfachen und mittleren Ansprüchen genügt. Für höhere Anforderungen haben ihre Schöpfer sie allerdings auch nicht entwickelt. Ihre Entwicklung erfolgte durch die University of Tartu in Estland, um einen einfachen Einstieg in das Programmieren mit Python zu ermöglichen.

Um die Programmierung für das Pico-Board zu illustrieren, ist das aber mehr als ausreichend. Bevor die Artikelreihe auf ausgefeilte IDEs für Pros zu sprechen kommt, verhilft Thonny-IDE zu ersten Erfahrungen mit dem Raspberry Pi Pico. Darüber hinaus ist die freie IDE auf Raspberry Pi OS bereits vorinstalliert, sodass Raspi-Nutzer nichts weiter zu tun haben und sich jetzt gemütlich zurücklehnen können. Zu guter Letzt sei angemerkt, dass die Programmierumgebung eine vollständige Implementierung von Python 3.7 mitbringt, weshalb sich keine weiteren Installationsorgien als notwendig erweisen.

Auf der Thonny-Webseite [2] lassen sich Versionen für Windows, macOS oder Linux-Distributionen herunterladen:

Website der Python-IDE thonny.org
Website der Python-IDE thonny.org

Installation der IDE

Nach Durchführung des Downloads auf Windows oder macOS starten Anwender die Installation durch Ausführung des Installationspakets. Linux-Anwender nutzen entweder ihren jeweiligen Paketmanager oder andere Möglichkeiten zur Installation (siehe Thonny auf GitHub [3]).

Exemplarisch die Illustration für Windows-PCs. Nach Start der Installation ergibt sich folgendes Bild:

Installation der Thonny IDE auf Windows
Installation der Thonny IDE auf Windows

Danach findet sich das Programm auf dem Zielsystem. Bei Start der fertig installierten IDE öffnet das schlichte IDE-Fenster von Thonny, das in der oberen Hälfte einen Editor und in der unteren Hälfte eine REPL-Shell (Read Eval Print Loop) enthält. Fürs interaktive Experimentieren bietet die Shell eine ideale Spielwiese.

Der Start-Bildschirm  von Thonny
Der Start-Bildschirm von Thonny

Unten rechts im Fenster zeigt Thonny den aktuell genutzten Python-Interpreter textuell an, im vorliegenden Fall also Python 3.7.9. Allerdings laufen auf den meisten Embedded Boards nur abgespeckte Python-Varianten wie CircuitPython oder MicroPython, wobei der Pico per default Letzteres unterstützt.

Um die geeignete Variante zu erhalten, genügt ein Mausklick auf das Textfeld "Python 3.7.9". Anschließend bietet Thonny ein Auswahlmenü mit allen unterstützten Interpretern, unter anderem "MicroPython (Raspberry Pi Pico)":

Auswahl von Python-Interpretern in der Thonny IDE
Auswahl von Python-Interpretern in der Thonny IDE

Vor der Selektion von MicroPython sollten Entwickler das Raspberry-Pi-Pico-Board mit gedrückter BOOTSEL-Taste an einen USB-Eingang des Hostrechners anschließen. Dadurch identifiziert sich das Board gegenüber dem Host als Speichergerät, etwa als Speicherstick. Anschließend wählen Entwickler wie oben erwähnt in Thonny die Option "MicroPython (Raspberry Pi Pico)" als zu installierenden Interpreter, worauf folgender Dialog erscheint:

Installation der MicroPython-Firmware auf ein Pico-Board
Installation der MicroPython-Firmware auf ein Pico-Board

Klicken Anwender auf den Install-Button, startet die Installation der MicroPython-Firmware. Nach Beendigung der Firmware-Installation müssen Anwender nur noch das Dialogfenster schließen:

Dialog nach beendeter MicroPython-Firmware-Installation
Dialog nach beendeter MicroPython-Firmware-Installation

Die Firmware besteht aus einem lauffähigen Programm mit der Endung .UF2. Sie lässt sich auch manuell auf den Pico übertragen.

Bereit für alle Schandtaten

Nun sind Host und Pico miteinander verbunden, und Entwickler können entweder über die REPL-Shell direkt mit dem MicroPython-Interpreter arbeiten oder alternativ ein Programm in den Editor eingeben. Wichtig ist an dieser Stelle, dass nach Übertragen eines MicroPython-Programms auf das Board (Endung: .py) das Board erst herunter- und dann wieder hochfährt, worauf das Laufzeitsystem automatisch das Python-Programm ausführt.

Befinden sich allerdings mehrere Programmdateien auf dem Board, weiß der Pico zunächst nicht, welche er ausführen soll. In diesem Fall sucht er nach einer Datei namens main.py, um sie auszuführen oder verharrt regungslos, sollte er diese Datei nicht finden.

Programmieren mit dem MicroPython SDK

Jetzt ist es endlich an der Zeit, ein einfaches MicroPython-Programm zu schreiben, um den Zugriff auf das I/O-System des Boards zu testen. Wie traditionell üblich, soll das erste Programm die interne LED des Pico zum Blinken bringen, bevor wir in nachfolgenden Folgen weitere Möglichkeiten des Pico kennenlernen:

Das übliche Blink-Programm in einer Pico-Version
Das übliche Blink-Programm in einer Pico-Version

Nun sezieren wir das Programm Schritt für Schritt:

from machine import Pin, Timer

Die import-Anweisung nutzt aus der machine-Bibliothek die Komponenten Pin und Timer. Erstere erlaubt den Zugriff auf die I/O-Ports des Pico, Letztere stellt Funktionen zur zeitlichen Steuerung von Aktionen zur Verfügung. Überhaupt enthält – nomen est omen – machine diverse Komponenten zum Zugriff auf die Pico-Hardware.

onboardLED = Pin(25, Pin.OUT)

Nun erfolgt die Initialisierung der Variablen onboardLED. Sie soll GPIO-Port 25 repräsentieren, an dem sich die eingebaute LED befindet. Wichtig: Die Zahlen beziehen sich nicht auf das physikalische Pin 25, sondern auf den GPIO-Port 25, für den es noch nicht einmal einen physikalischen Pin gibt.

Das Pin-Layout des Raspberry Pi Pico - physikalische und logische Pins
Das Pin-Layout des Raspberry Pi Pico - physikalische und logische Pins

Der zweite Parameter im Konstruktor namens Pin teilt dem Interpreter mit, wie der I/O-Port verwendet werden soll, nämlich im vorliegenden Fall als Ausgabe-Port. Daher die Konfiguration mit Pin.OUT.

timer = Timer()

Hier initialisiert das Programm die Variable timer. Dazu gleich mehr.

def blinker(timer):

onboardLED.toggle()

Die Methode blinker erhält als Argument ein initialisiertes timer-Objekt. Immer wenn der timer dazu die Initiative gibt, erfolgt der Aufruf der toggle-Methode. Die setzt den Ausgang abhängig von deren momentanen Zustand entweder von 0 auf 1 oder von 1 auf 0, was folglich zum Blinken der LED führt.

timer.init(freq = 2.5, mode = Timer.PERIODIC, callback = blinker)

In der Initialisierungsmethode init definiert das Programm einen periodisch feuernden Timer (Timer.PERIODIC) mit einer Frequenz von 2,5. Daraus folgt eine Periode von 400 Millisekunden. timer ruft zu diesem Zweck alle 400 Millisekunden eine Callback-Funktion auf (callback = blinker). In unserem Fall ist das die blinker-Methode, die dementsprechend alle 400 Millisekunden das Ausgangssignal für die LED invertiert.

Gut gesichert

Nach getaner Arbeit können Entwickler entweder für die direkte Programmausführung auf dem Pico sorgen, indem sie auf das grüne Icon oder den entsprechenden Menüpunkt klicken. Oder sie sichern das Programm auf das Pico-Board, worauf das Board resettet, um anschließend mit der Programmausführung zu beginnen. Selbstredend lässt sich die Programmdatei auch auf dem Hostrechner abspeichern:

Das Speichern der Programmdatei kann entweder auf dem Host oder dem Pico erfolgen
Das Speichern der Programmdatei kann entweder auf dem Host oder dem Pico erfolgen

Wenig überraschend sollte sich jetzt folgendes Geschehen zeigen:

Der Pico-Blinker in Aktion
Der Pico-Blinker in Aktion

Damit wäre das erste MicroPython für den Raspberry Pi Pico geschafft. Zwar ein triviales Beispiel, aber ein lehrreicher erster Schritt.

Fazit

Dieser Artikel hat sich in seichtem Wasser bewegt. Die Thonny-IDE taugt als ideale Spielwiese, um den Pico interaktiv kennenzulernen. Als professionelle Entwicklungsumgebung kann sie hingegen nicht fungieren. Sie macht allerdings den Einstieg sehr leicht, was schließlich der Sinn des vorliegenden Artikels war. Ich hoffe, Sie haben jetzt noch viel Spaß mit eigenen Experimenten. In den nächsten Teilen wird das Fahrwasser etwas rauer, aber auch unterhaltsamer.

Hilfreiche Referenzen


URL dieses Artikels:
https://www.heise.de/-5057160

Links in diesem Artikel:
[1] https://www.heise.de/developer/artikel/Ein-Picobello-Microcontroller-Raspberry-Pi-Pico-Board-5045274.html
[2] https://thonny.org
[3] https://github.com/thonny/thonny/wiki/Linux
[4] https://datasheets.raspberrypi.org/pico/raspberry-pi-pico-python-sdk.pdf
[5] https://micropython.org
[6] https://micropython.org/download/rp2-pico/

Copyright © 2021 Heise Medien

Let's block ads! (Why?)

  • 17. Februar 2021 um 14:44

Micro Frontends – die Microservices im Frontend

Von heise online

Micro Frontends – die Microservices im Frontend

Tales from the Web side Sebastian Springer

Micro Frontends verfolgen einen ähnlichen Ansatz wie Microservices, nur in einer völlig unterschiedlichen Umgebung. Daher ergeben sich auch andere Vor- und Nachteile sowie eine veränderte Herangehensweise bei der Konzeption und Umsetzung.

Serverseitig hat sich das Architekturmuster der Microservices gerade für umfangreiche Applikationen als Alternative zu einem monolithischen Ansatz etabliert. Einen ähnlichen Zweck verfolgen Entwickler mittlerweile auch im Frontend mit Micro Frontends. Die Idee dahinter ist, einige der Vorteile von Microservices auch im Frontend nutzbar zu machen. Doch bevor wir uns der Frage widmen, ob Micro Frontends der neue Standard für die Entwicklung von Web-Frontends werden, werfen wir einen Blick auf die Microservice-Architektur.

Microservices und was sie mit dem Frontend zu tun haben

Die Idee hinter Microservices ist, ein großes Problem in mehrere Teilprobleme zu zerlegen und sie getrennt voneinander zu bewältigen. Dieser Architekturansatz geht noch einen Schritt weiter als eine gewöhnliche Modularisierung von Applikationen, da hier nicht der Schnitt an fachlichen Grenzen innerhalb einer Applikation gemacht wird, sondern das Ganze noch weiter geht und die Bereiche in einzelne, unabhängige Systeme teilt, die durch Schnittstellen miteinander verbunden sind. Die Vorteile, die durch eine solche Microservice-Architektur entstehen, sind:

  • Skalierbarkeit: Im Gegensatz zu einem Monolithen können die einzelnen Microservices unabhängig voneinander skaliert werden. Steht ein Service unter Last, werden zusätzliche Instanzen hochgefahren, alle übrigen Services bleiben von dieser Änderung unberührt.
  • Robustheit: Der Ausfall eines Services führt nicht zwangsläufig zum Ausfall des Gesamtsystems. Da die Services sauber voneinander getrennt sind, besteht die Möglichkeit, den ausgefallenen Service wieder hochzufahren oder Alternativen anzubieten.
  • Einfachheit: Die Komplexität eines kleinen Services ist deutlich geringer als die eines Monolithen, der den gesamten Funktionsumfang einer Applikation umfasst. Entwickler können so die einzelnen Services deutlich besser überblicken, als es bei einem großen zusammenhängenden Konstrukt möglich ist.
  • Flexibilität: Nicht zu vernachlässigen ist die Möglichkeit, in einer Microservice-Architektur das jeweils beste Werkzeug für eine Aufgabe auszuwählen. Da die Services unabhängig voneinander sind, spricht nichts dagegen, einen Service in Go, einen anderen in JavaScript und einen dritten beispielsweise in Kotlin umzusetzen.

Sicherlich hat eine Microservice-Architektur nicht nur ihre Vorteile. Die Komplexität in der Kommunikation zwischen den einzelnen Services steigt deutlich an. Hinzu kommt, dass es sich hierbei um Kommunikation zwischen eigenständigen Systemen handelt. Die Entwickler müssen sich also um Problemstellungen wie potenziellen Nachrichtenverlust, synchrone- und asynchrone Kommunikation kümmern. Entwickeln die einzelnen Service-Teams unabhängig voneinander, erfordert dies einen zusätzlichen Koordinationsaufwand, wenn es um die Versionierung der Services geht. Hierbei muss sichergestellt sein, dass die Services der Applikation untereinander kompatibel bleiben.

Micro Frontends verfolgen einen ähnlichen Ansatz wie Microservices, jedoch in einer völlig anderen Umgebung. Daraus ergibt sich eine andere Bewertung der Architekturform. Wo sich eine Microservice-Architektur für eine Vielzahl von Applikationen lohnt, sind die Anwendungsfälle für Micro Frontends deutlich eingeschränkter.

Doch werfen wir zunächst einen Blick auf die wesentlichen Unterschiede zwischen Frontend und Backend und wie sie sich auf die Architektur auswirken. Der Browser führt eine Applikation als einzelne Instanz aus. Im Gegensatz dazu sind Backend-Services unabhängig voneinander (man hat also mehrere Instanzen). Das wirkt sich vor allem auf die Skalierbarkeit der Applikation aus. Bei einer Lastspitze fährt die Backend-Infrastruktur zusätzliche Instanzen der betroffenen Services hoch und verteilt die Last entsprechend. Ist der Browser unter Last, bringt es zugegebenermaßen wenig, ein weiteres Browserfenster mit einer neuen Instanz der Applikation zu öffnen. Der Browser kann nur auf Worker-Prozesse zugreifen, die allerdings vom Rendering abgekapselt sind und lediglich für Berechnungen und Serverkommunikation eingesetzt werden können.

Das Skalierungsverhalten von Front- und Backend unterscheidet sich also grundlegend. Auch beim Thema Robustheit gibt es gravierende Unterschiede: Hier wird häufig das Bild von Pets vs. Cattle aufgegriffen, also der Unterschied zwischen Haustieren und Nutztieren. In einer Microservice-Architektur sind die einzelnen Instanzen wie Nutztiere, zu denen die Betreiber der Plattform ein eher distanziertes Verhältnis pflegen sollten. Stürzt ein Service ab, wird er durch eine neue Instanz ersetzt. Im Browser lässt sich eine solche Vorgehensweise nicht realisieren, denn es gibt nur den einen Hauptprozess. Stürzt dieser ab, war's das. Die Entwickler sollten ihn also mit umfangreicher Fehlerbehandlung hegen und pflegen wie ein Haustier.

Das Thema Einfachheit betrifft sowohl Front- als auch Backend. Kleinere Einheiten, die für sich stehen, sind in beiden Welten deutlich einfacher zu handhaben als ineinander verflochtene, umfangreiche Konstrukte. Die Flexibilität, die eine Microservice-Architektur im Backend hinsichtlich der Wahl der Sprachen und Technologien bietet, muss im Frontend auch wieder differenziert betrachtet werden. Im Backend können die Entwickler die für die jeweilige Problemstellung passende Kombination aus Programmiersprache, Frameworks und Bibliotheken auswählen. Der Browser schränkt diese Auswahl im Frontend deutlich ein. Neben JavaScript und zu einem geringen Teil WebAssembly lässt er keine weiteren Programmiersprachen direkt zu. Eine Ausnahme bilden hier nur Sprachen, die in JavaScript oder WebAssembly übersetzt werden, wie es mit TypeScript, CoffeeScript oder C# in Form von Blazor der Fall ist.

Bleibt also nur noch die Auswahl an Bibliotheken und Frameworks, die in diesem Bereich zugegebenermaßen sehr umfangreich ist. Besteht eine Applikation aus mehreren kleinen Teilen und sind diese mithilfe verschiedener Bibliotheken und Frameworks umgesetzt, lädt der Browser die erforderlichen Ressourcen zur Darstellung der Applikation vom Server, was einen erheblichen Overhead im Vergleich zu einer traditionell monolithisch aufgebauten Applikation bedeuten kann.

Der Aufbau einer Micro-Frontend-Applikation

Bei einer Micro-Frontend-Architektur setzt sich die Applikation aus mehreren voneinander unabhängigen Micro Frontends zusammen. Diese einzelnen Bestandteile werden in einer Integrationsschicht zusammengefügt. Das wird nötig, da es für den Browser immer ein Dokument gibt, das er initial vom Server lädt. Beim Aufbau einer Micro-Frontend-Architektur gilt Ähnliches wie bei den Microservices im Backend: Es gibt nicht den einen richtigen Weg, sie umzusetzen, vielmehr existieren zahlreiche Facetten dieser Architekturform.

Die einfachste Möglichkeit besteht darin, die einzelnen Micro Frontends über iFrames einzubetten. Diese Variante ist zwar nicht sonderlich elegant, stellt jedoch sicher, dass die einzelnen Teile der Applikation tatsächlich unabhängig voneinander sind. Weitere Möglichkeiten sind die Kapselung der Micro Frontends in Web Components oder der Einsatz von zusätzlichen Bibliotheken wie single-spa, die die friedliche Koexistenz verschiedener Frontend-Frameworks in einer Applikation erlauben.

Ein großes Problem bei der Umsetzung von Micro Frontends mit verschiedenen Bibliotheken ist die bereits erwähnte Paketgröße, da im schlechtesten Fall mehrere vollwertige Frameworks parallel ausgeliefert werden müssen. Dieses Problem lässt sich nur auf zwei Arten lösen: Entweder werden keine größeren Bibliotheken eingesetzt oder die Entwicklerteams der einzelnen Micro Frontends einigen sich auf gewisse Konventionen. So kann die Verwendung nur eines Frameworks festgelegt werden, und die Teams entwickeln dann nicht komplett eigenständige Applikationen, sondern Module, die die Integrationsschicht zusammenführt. Der Entwicklungsprozess einer solchen Architektur ist also geprägt von Kompromissen und einer Gratwanderung zwischen völliger Flexibilität und Einschränkungen durch Konventionen. Wie die Vorgehensweise konkret aussieht, hängt stark vom Einsatzzweck und den Rahmenbedingungen der Gesamtapplikation ab.

Micro Frontends oder keine Micro Frontends, das ist hier die Frage

Wie über so ziemlich jedes Thema in der Entwicklung, werden auch über Micro Frontends leidenschaftliche Diskussionen geführt. Und wie so oft sollte jeder auch dieser Architekturform seine Daseinsberechtigung zusprechen. Natürlich sind Micro Frontends nicht die Silver Bullet, die alle Probleme in der Web-Entwicklung lösen. Aber gerade für umfangreiche Applikationen mit mehreren beteiligten Teams kann eine Micro-Frontend-Architektur eine potenzielle Lösung sein.

Ein Szenario, in dem Micro Frontends ihre Stärke wirklich ausspielen, sind Migrationen. Hat eine bestehende Applikation ihren Zenit überschritten, steigen die Kosten für die Wartung und Weiterentwicklung deutlich an und für die Entwickler ist die Arbeit an einem solchen gewachsenen Stück Software kein wirkliches Vergnügen mehr. Also steht irgendwann die Frage eines Rewrites im Raum. Das Neuschreiben einer Applikation birgt jedoch beträchtliche Risiken, die bis hin zum Scheitern des Unterfangens reichen.

Eine Migration der alten Software mithilfe einer Micro-Frontend-Architektur auf die neue Version ist eine Möglichkeit, die Risiken in den Griff zu bekommen. Hierfür schaffen die Entwickler zunächst die Integrationsschicht, in die die bestehende Software eingebettet wird, sodass sie funktioniert wie bisher. Anschließend können sie sich schrittweise daran machen, die Features durch neue Versionen zu ersetzen. Die Integrationsschicht sorgt dann dafür, dass die Benutzer die neue Version erhalten und die alte Version wird abgeschaltet.

Sobald alle Features migriert sind, kann die alte Applikation gelöscht und auch die Integrationsschicht kann, falls sie nicht mehr benötigt wird, zurückgebaut werden. Der Vorteil dieser Vorgehensweise ist, dass es keinen Big-Bang-Release gibt, die Migration schleichend erfolgt und es zu keinem Stillstand bei der Entwicklung neuer Features kommen muss.

Abschließend bleibt nur noch zu sagen, dass jedes Team die Entscheidung für die Architektur seiner Applikation selbst treffen muss. Es ist dabei allerdings hilfreich, möglichst viele verschiedene Alternativen zu kennen und die jeweils beste auszuwählen. Ob es also ein sauber modularisierter Monolith oder eine Micro-Frontend-Architektur wird, hängt von der Problemstellung und den Vorlieben der Entwickler ab.


URL dieses Artikels:
https://www.heise.de/-5057255

Copyright © 2021 Heise Medien

Adblock test (Why?)

  • 17. Februar 2021 um 12:13

Zugriff auf die Zwischenablage: synchron und asynchron

Von heise online

Zugriff auf die Zwischenablage: synchron und asynchron

Tales from the Web side Philip Ackermann

Für die Arbeit mit der Systemzwischenablage stellt die W3C-Spezifikation "Clipboard API and events" zwei APIs zur Verfügung: die synchrone "Clipboard Event API" und die asynchrone "Asynchronous Clipboard API".

Der programmatische Zugriff auf die Zwischenablage innerhalb einer Webanwendung hat in den letzten Jahren einen Wandel durchgemacht. Noch vor ein paar Jahren griff man gerne auf Bibliotheken wie ZeroClipboard [1] zurück, die unter der Haube Flash voraussetzten. Mit dem Verschwinden von Flash in die Bedeutungslosigkeit verloren allerdings auch diese Bibliotheken an Bedeutung. Die execCommand API [2] wiederum, über die man unter anderem programmatisch Befehle wie das Kopieren oder Einfügen ausführen kann, leidet unter Interoperabilitätsproblemen, gilt mittlerweile als veraltet [3] und wird aller Voraussicht auch nicht mehr über den Status eines "Unofficial Draft" hinauskommen. Hinzu kommt, dass hierüber Daten nur aus dem DOM gelesen und auch nur in das DOM geschrieben werden können:

// Achtung: veraltet! 

// Kopieren
const source = document.querySelector("#source");
source.select();
document.execCommand("copy");

// Und Einfügen
const target = document.querySelector("#target");
target.focus();
document.execCommand("paste");

Beim W3C liegt der Fokus der Entwicklung daher auf der Spezifikation "Clipboard API and events" [4], die aktuell als Working Draft vorliegt. Sie definiert für die Arbeit mit der Systemzwischenablage zwei verschiedene APIs: die Clipboard Event API [5] (bzw. Synchronous Clipboard API) und die relativ neue Asynchronous Clipboard API [6].

Synchroner Zugriff

Die Clipboard Event API [7] bietet zum einen die Möglichkeit, sich in die gängigen Operationen auf der Zwischenablage "einzuhaken", sprich auf Events zu reagieren, die beim Ausschneiden, Kopieren und Einfügen ausgelöst werden sowie zum anderen synchron schreibend und lesend auf die Zwischenablage zuzugreifen.

Für Ersteres definiert die API vier verschiedene Events:

  • "copy": wird ausgelöst, wenn Daten in die Zwischenablage kopiert werden.
  • "paste": wird ausgelöst, wenn Daten aus der Zwischenablage eingefügt werden.
  • "cut": wird ausgelöst, wenn Daten "ausgeschnitten" und dabei in die Zwischenablage kopiert werden.
  • "clipboardchange": wird ausgelöst, wenn der Inhalt der Zwischenablage geändert wird. Dies betrifft im Übrigen auch Änderungen, die außerhalb des Browsers ausgelöst werden. In diesem Fall wird das Event genau dann ausgelöst, wenn der Browser wieder den Fokus erhält.

Auf diese Events kann man, wie in folgendem Listing gezeigt, über entsprechende Event-Listener reagieren. Der EventListener für das copy-Event definiert, was passieren soll, wenn innerhalb der Webseite eine Kopieraktion durchgeführt wird. Über die Eigenschaft clipboardData des entsprechenden Events gelangt man an ein Objekt vom Typ DataTransfer. Zur Erinnerung: Dieses Objekt repräsentiert ein Objekt, das ursprünglich für den Datentransfer bei Drag&Drop-API-Aktionen [8] zuständig ist. Über die Methode setData() können an diesem Transferobjekt die zu übertragenden Daten definiert werden, wobei der Methode als erster Parameter das Format der Daten (in Form des MIME-Typen) und als zweiter Parameter die eigentlichen Daten übergeben werden. Auf der Gegenseite (innerhalb des Event-Listeners, der für die Einfügeoperation zuständig ist) werden analog über die Methode getData() unter Angabe des MIME-Typen die Daten aus dem Transferobjekt gelesen.

document.addEventListener('copy', (event) => {
const { clipboardData } = event;
clipboardData.setData(
'text/plain',
'Dieser Text kommt synchron in die Zwischenablage.'
);
event.preventDefault();
});

document.addEventListener('paste', (event) => {
const { clipboardData } = event;
const data = clipboardData.getData('text/plain');
console.log(data);
// => "Dieser Text kommt synchron in die Zwischenablage."
event.preventDefault();
});

Grundsätzlich kann ein Transferobjekt Daten für verschiedene Formate enthalten. Auf diese Weise lassen sich in einer einzelnen Kopier- beziehungsweise Einfügeoperation die Daten direkt in mehreren Formaten übertragen. Das ist beispielsweise dann praktisch, um abhängig vom Ziel der Einfügeoperation die Daten entweder in dem einen oder dem anderen Format zu verarbeiten beziehungsweise. einzufügen. Folgendes Listing zeigt, wie auf diese Weise gleichzeitig Daten als einfacher Text, als formatierter HTML-Text und als JSON übertragen werden können (da sich die Daten nur als Zeichenkette übergeben lassen, muss im Fall von JSON entsprechend serialisiert (JSON.stringify()) und deserialisiert (JSON.parse()) werden).

document.addEventListener('copy', (event) => {
const { clipboardData } = event;
console.log(clipboardData);
clipboardData.setData(
'text/plain',
'Dieser Text kommt synchron in die Zwischenablage.'
);
clipboardData.setData(
'text/html',
'Dieser <strong>Text</strong> kommt synchron in die Zwischenablage.'
);
clipboardData.setData(
'application/json',
JSON.stringify({
message: 'Dieser Text kommt synchron in die Zwischenablage.'
}));
event.preventDefault();
});

document.addEventListener('paste', (event) => {
const { clipboardData } = event;
const data = clipboardData.getData('text/plain');
console.log(data);
// => "Dieser Text kommt synchron in die Zwischenablage."

const dataHTML = clipboardData.getData('text/html');
console.log(dataHTML);
// => "<meta charset='utf-8'>Dieser <strong>Text</strong> \
// kommt synchron in die Zwischenablage."

const dataJSON = JSON.parse(clipboardData.getData('application/json'));
console.log(dataJSON);
// => { message: "Dieser Text kommt synchron in die Zwischenablage." }
event.preventDefault();
});

Asynchroner Zugriff

Der oben beschriebene synchrone Zugriff auf die Zwischenablage eignet sich – weil die Ausführung der Webanwendung während der Kopier- beziehungsweise Einfügeoperation blockiert wird – nur für Daten, die eine überschaubare Größe haben. Möchte man dagegen komplexere oder größere Daten wie Bilddaten in die Zwischenablage kopieren oder aus der Zwischenablage in eine Webanwendung einfügen, greift man besser auf die Asynchronous Clipboard API [9] zurück. Wie der Name vermuten lässt, können Daten hierüber asynchron in die Zwischenablage geschrieben und aus ihr gelesen werden, ohne dass die entsprechenden Operationen dabei den Browser blockieren. Allerdings müssen Nutzer den Zugriff der entsprechenden Webanwendung auf die Zwischenablage erlauben, damit der Einsatz der Asynchronous Clipboard API überhaupt möglich ist.

Der Zugriff auf die API erfolgt über das Objekt navigator.clipboard, das entsprechende Methoden für das Schreiben (write()) sowie das Lesen (read()) bereitstellt. Beide Methoden liefern als Rückgabewert ein Promise-Objekt, sodass sie sich bequem sowohl über die Promise API als auch über async/await verwenden lassen.

Beim Schreiben werden der Methode write() die zu kopierenden Daten in Form eines Arrays von ClipboardItem-Objekten übergeben. Jedem einzelnen Objekt übergibt man die Daten mit einem Konfigurationsobjekt als Schlüssel/Wert-Paare, wobei der Schlüssel das Format repräsentiert und der Wert die Daten als Blob enthält:

const copy = async () => { 
try {
const data = [
new ClipboardItem(
{
'text/plain': new Blob(
['Dieser Text kommt asynchron in die Zwischenablage.'],
{ type: 'text/plain' }
)
}) ];
await navigator.clipboard.write(data);
} catch (error) {
console.error(error);
}
}

Der lesende Zugriff geschieht über die Methode read(), über die man an die entsprechenden Daten in Form eines Arrays von ClipboardItem-Objekte gelangt. Die Eigenschaft types jedes einzelnen ClipboardItem-Objekts enthält die verschiedenen Datenformate, die mit dem Objekt übertragen wurden:


const paste = async () => {
try {
const items = await navigator.clipboard.read();
for (const item of items) {
for (const type of item.types) {
const data = await item.getType(type);
const text = await data.text();
console.log(text);
}
}
} catch (error) {
console.error(error);
}
}

Mit write() und read() können derzeit Texte und Bilder übertragen werden. Grundsätzlich sind weitere Formate denkbar, beispielsweise solche für Audio- oder Videodaten. Diese werden momentan aber in Bezug auf die Übertragung über die Zwischenablage noch nicht von allen Browsern unterstützt.

Für die Arbeit mit Texten stehen neben write() und read() noch die beiden Convenience-Methoden writeText() und readText() zur Verfügung, über die der Code von eben etwas einfacher gestaltet werden kann, weil man sich nicht mit ClipboardItem- und Blob-Objekten herumschlagen muss:

const copyText = async () => { 
try {
await navigator.clipboard.writeText(
"Dieser Text kommt asychron in die Zwischenablage."
);
} catch (error) {
console.error(error);
}
}

const pasteText = async () => {
try {
const text = await navigator.clipboard.readText();
console.log(text);
} catch (error) {
console.error(error);
}
}

Fazit

Mit der Asynchronous Clipboard API wird der Zugriff auf die Zwischenablage um einen asynchronen Kommunikationskanal erweitert. Alle "großen" Browser bieten Support für die Clipboard API an, sowohl für die Synchronous Clipboard API [10] als auch für die Asynchronous Clipboard API, wobei der Support für Erstere auch ältere Browserversionen umschließt. Wer auf Nummer sicher gehen möchte, dass die neuere asychrone Version im jeweiligen Browser zur Verfügung steht, kann dies über Feature Detection [11] anhand des navigator.clipboard-Objekts überprüfen. Alternativ kann man auf eine Polyfill-Bibliothek wie clipboard-polyfill [12] oder auf beliebte Alternativen wie clipboard.js [13] zurückgreifen.


URL dieses Artikels:
https://www.heise.de/-5048275

Links in diesem Artikel:
[1] https://github.com/zeroclipboard/zeroclipboard
[2] https://w3c.github.io/editing/docs/execCommand/
[3] https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
[4] https://w3c.github.io/clipboard-apis
[5] https://w3c.github.io/clipboard-apis/#clipboard-event-api
[6] https://w3c.github.io/clipboard-apis/#async-clipboard-api
[7] https://w3c.github.io/clipboard-apis/#clipboard-event-api
[8] https://html.spec.whatwg.org/multipage/dnd.html#datatransfer
[9] https://w3c.github.io/clipboard-apis/#async-clipboard-api
[10] https://caniuse.com/clipboard
[11] https://en.wikipedia.org/wiki/Feature_detection_(web_development)
[12] https://github.com/lgarron/clipboard-polyfill
[13] https://github.com/zenorocha/clipboard.js

Copyright © 2021 Heise Medien

Let's block ads! (Why?)

  • 08. Februar 2021 um 11:09
❌