Mit Blazor ist Microsoft als letzter der großen Player auf den Markt der Single-Page-App-Frameworks vorgedrungen. Dort konkurriert Blazor gegen alteingesessene Platzhirsche wie Googles Angular. Für welche Zielgruppen Angular und Blazor jeweils interessant sind, soll dieser Artikel klären.
Im März 2018 stellte Microsoft Blazor vor, ein von Steve Sanderson initiiertes Experiment, um .NET (zurück) in den Browser zu bringen: Statt auf JavaScript können Entwickler auf C# und die Templatesprache Razor zurückgreifen, um Webanwendungen zu entwickeln. Damit knüpft Blazor an das serverseitig ausgeführte ASP.NET MVC an.
Zunächst müssen wir differenzieren zwischen den beiden Geschmacksrichtungen von Blazor: Da gibt es zunächst das serverseitig ausgeführte Blazor Server. Der Zustand der Anwendung wird hierbei vom Server verwaltet, Interaktionen der Nutzer führen zu einer Kommunikation mit dem Server über SignalR, auch HTML-Fragmente werden darüber ausgetauscht. Wenn besonders viele Nutzer die Anwendung gleichzeitig verwenden, wird der Server damit zum Bottleneck. Eine langsame Verbindung führt zu einem schlechten Antwortverhalten der Benutzeroberfläche.
Demgegenüber steht Blazor WebAssembly. Hier wird die Anwendung einschließlich der .NET-Runtime komplett clientseitig im Browser ausgeführt. Hier braucht es im einfachsten Fall lediglich einen statischen Webserver; der Browser muss die Technologie WebAssembly unterstützen. Einmal geladen kann die Anwendung auch offline ausgeführt werden. Wenn in diesem Artikel von Blazor die Rede ist, ist immer von der WebAssembly-Variante gemeint, die auch die von vornherein geplante Zielausbaustufe war. Blazor Server ist eher als eine Zwischenlösung anzusehen.
WebAssembly (Wasm) ist ein Bytecode für das Web, der JavaScript ergänzen soll. Der große Vorteil von Wasm gegenüber dem menschenlesbaren JavaScript besteht darin, dass der Code weder geparst noch interpretiert werden muss. In anderen Sprachen geschriebener Programmcode (z.B. Rust, C#, Java) kann nach Wasm kompiliert und dann im Webbrowser ausgeführt werden.
Die Technologie soll JavaScript dabei nicht ersetzen, sondern für spezielle Anwendungsfälle ergänzen. WebAssembly-Code wird von derselben Engine ausgeführt, die auch JavaScript antreibt. Daher ist die Laufzeitperformance von Wasm-Code nicht zwingend besser als die von JavaScript-Anwendungen (siehe auch: Is WebAssembly magic performance pixie dust? [1]). WebAssembly-Code unterliegt zudem derselben Sandbox wie JavaScript. Somit können nicht beliebige native Schnittstellen aufgerufen werden, sondern nur diejenigen, für die es auch passende APIs im Web gibt.
Um Blazor-Apps im Webbrowser auszuführen, wird zunächst die .NET-Runtime in Wasm heruntergeladen und gestartet. Als nächstes wird die passende Dynamic Link Library (DLL) mit der .NET-Assembly bezogen und auf der Runtime ausgeführt (Just in Time, ab .NET 6 soll Ahead-of-Time-Kompilierung möglich werden).
WebAssembly ist standardisiert und wird von den vier großen Evergreen-Browsern Firefox, Edge, Safari und Chrome plattformübergreifend seit geraumer Zeit von Haus aus unterstützt. Damit grenzt sich Blazor auch von Silverlight ab, dem schon lange abgekündigten, proprietären und Browser-Plug-in-basierten Ansatz, .NET-Anwendungen im Browser auszuführen.
Angular wurde im Jahr 2016 von Google veröffentlicht. Es basiert auf den Erfahrungen von Google mit dem Vorgängerframework AngularJS, das 2009 erstmals herausgegeben wurde. Dieses wiederum entstand auf Basis der Erfahrungen bei der Implementierung großer Webanwendungen wie Google Mail. Angular ist das meistverwendetste Framework bei Google selbst.
Die ab 2016 herausgegebene Variante basiert auf der Sprache TypeScript. Vergleichbar zu Razor erweitert Angular den HTML-Sprachumfang um eine Templatingsyntax. Vor der Ausführung im Browser müssen die TypeScript-Quelldateien nach JavaScript übersetzt werden. JavaScript wiederum kann direkt im Browser ausgeführt werden, weswegen Angular-Apps deutlich „webnäher“ sind als Blazor-Anwendungen.
Während bei Angular mittlerweile jede Version eine Long-Term-Support-Version (LTS) mindestens 18 Monate Unterstützung erhält, gibt es bei Blazor noch keine solche Version. Ferner sind Community und Ökosystem bei Angular bis jetzt größer als bei Blazor.
Blazor ist vor allem für die Teams interessant, die über Wissen im .NET- und C#-Bereich verfügen, im Umgang mit JavaScript aber eher weniger erfahren sind und die kein Wissen in diesem Bereich aufbauen können oder wollen: Ein großer Vorteil in der Verwendung von Blazor besteht darin, dass bestehender Quelltext eventuell ins Web mitgenommen werden kann. Frühere Investitionen in eine Codebasis bleiben somit erhalten.
Wie oben angemerkt ist das aber nur dann möglich, wenn die verwendeten Funktionen und Schnittstellen im Webbrowser zur Verfügung stehen, also in JavaScript implementiert werden könnten: Ein wahlfreier Zugriff auf das Dateisystem oder Geräteschnittstellen ist auch mit WebAssembly nicht möglich. Weiterhin kann C#-Code zwischen Server und Client geteilt werden, etwa Validierungslogik. Die bekannten Komponentenhersteller bieten auch für Blazor passende Komponenten an, eventuell sind diese in bestehenden Abonnements sogar schon enthalten und können einfach mitverwendet werden.
Angular ist deutlich älter und damit auch reifer als Blazor. Viele Features, die Angular schon seit geraumer Zeit mitbringt und dort robust funktionieren, muss Blazor erst nachliefern: Das Lazy Loading von Anwendungsbestandteilen kam bei Blazor etwa erst mit .NET 5 hinzu. Andere Features sind noch gar nicht verfügbar. Vorne liegt Angular etwa bei Ahead-of-Time-Kompilierung, was für deutlich kleinere Bundlegrößen sorgt oder bei Live beziehungsweise Hot Reloading. Hier wird die Anwendung nach einer Änderung im Quelltext automatisch neu geladen, was die Entwicklerproduktivität deutlich steigert. Beides soll in Blazor erst mit .NET 6 im November diesen Jahres nachgereicht werden. Da Angular-Anwendungen vor Ausführung im Browser nach JavaScript übersetzt werden, kommen diese Apps ohne schwergewichtige Runtime aus: Eine einfache Hallo-Welt-Anwendung ist in Angular gerade einmal 170 Kilobyte groß, bei Blazor starten Anwendungen mit 2 Megabyte Dateigröße. JavaScript-basierte Browserschnittstellen können in Angular direkt aufgerufen werden, bei Blazor braucht es hingegen ein passendes NuGet-Paket oder die Verwendung der Interop-Brücke.
Blazor bietet sich für die Entwickler an, die über Kenntnisse oder Bestandscode im .NET-Umfeld verfügen, bei denen ein Wechsel auf einen anderen Technologiestack nicht in Frage kommt und die höhere Bundlegrößen tolerieren können. Wichtig ist dabei, dass Blazor keine magische .NET-zu-Web-Übersetzungsmaschine ist: Auch Blazor-Entwicklern bleibt die Beschäftigung mit HTML, CSS, REST-APIs, CORS oder Betriebsmodellen in der Cloud nicht erspart, Berührungspunkte mit Webtechniken wird es also definitiv geben.
Selbst für .NET-Entwickler kann das auf TypeScript aufsetzende Angular interessant sein: Mit Anders Hejlsberg zeichnet sich derselbe Sprachdesigner für TypeScript verantwortlich wie auch für C#. Beide Sprachen beeinflussen sich oft gegenseitig und sind syntaktisch sehr ähnlich. Das Framework hat sich seit Jahren im Einsatz bewährt und aufgrund des weiterverbreiteten Einsatzes bei Google selbst dürfte die Weiterentwicklung noch auf Jahre gesichert sein. Entwickler ohne .NET-Hintergrund sind bei Angular vermutlich besser aufgehoben.
Schließlich ist festzustellen, dass sich beide Ansätze funktional nicht unterscheiden: Derselbe Funktionsumfang lässt sich mit Angular wie auch in Blazor umsetzen und plattformübergreifend zur Ausführung bringen.
URL dieses Artikels:https://www.heise.de/-6197138
Links in diesem Artikel:[1] https://surma.dev/things/js-to-asc/
Copyright © 2021 Heise Medien
![]()
In c’t 18/2021 gehts um die Technik in E-Bikes. Wir befassen uns mit dem Schutz und den Tücken von VPN, retten verschlammte Festplatten und drehen bunte Würfel.
Hallo aus Hannover,
hier bei c’t kommen etliche Leute mit dem Fahrrad zur Arbeit und bewegen sich in ihrer Freizeit gern auf zwei Rädern. Ich gehöre auch dazu und nutze das Rad außerdem als Sportgerät an Wochenenden und im Urlaub. Mein Interesse an E-Bikes ist deshalb allenfalls technischer Natur.
Unser Schwerpunkt im aktuellen Heft hat mich trotzdem angefixt, denn darin gehts genau um die Technik von E-BIkes – wir sind schließlich kein Fahrradmagazin, sondern eines für IT. Die Kollegen haben die unterschiedlichen Konzepte und Finessen aktueller E-Bikes bis hin zum Tuning unter die Lupe genommen, das finde ich total spannend. Und ich hätte nie erraten, welche E-Bike-Modelle im vergangenen Jahr besonders oft gekauft wurden: Dass elektrische Rennräder nicht so gut laufen, war klar. Aber mehr Mountainbikes mit als ohne E-Antrieb? No Way.
Wie stehen Sie zu E-Bikes? Erkennen Sie sich in einer der beiden Befragten-Gruppen aus der Grafik wieder?


Häufig finde ich in c’t Themen, mit denen ich nicht gerechnet hätte. Diesmal wars ein Artikel meines Kollegen Wilhelm Drehling: Er hat einen Rubik's Cube getestet. Ich dachte, Zauberwürfel sind längst out, von vorgestern, 1980er sozusagen. Falsch gedacht, es werden weiterhin Würfel gebaut, heute allerdings smarte Versionen. Und die sind sogar für Profis interessant. Weil ich weiß, dass meine Nichte als Kind sehr gern mit dem Zauberwürfel gespielt hat, konnte ich nicht anders: Ich hab ihr einen bestellt. Jetzt bin ich gespannt, ob sie die Variante "Der mit der App spricht" auch so gut findet, wie ich nach Lesen des c’t-Tests:
Zauberwürfel spricht mit App: GAN356 i Carry [13]
Mir war gar nicht klar, dass es sogar richtige Turniere fürs Arrangieren der bunten Würfelflächen gibt. Das muss ein irres Spektakel sein. Beherrschen Sie die Finessen des Magic Cube? Wenn ja, wie lange brauchen Sie, um die Flächen zu ordnen? Schreiben Sie mir an , ich bin gespannt!

Bestellen Sie unsere Heftvorschau als Newsletter und erhalten Sie alle 2 Wochen einen Überblick über die aktuelle c’t per Mail:
URL dieses Artikels:https://www.heise.de/-6160398
Links in diesem Artikel:[1] https://www.heise.de/select/ct/2021/18/2114109114267327767[2] https://www.heise.de/select/ct/2021/18/2118913314466014605[3] https://www.heise.de/select/ct/2021/18/2118913341442463451[4] https://www.heise.de/select/ct/2021/18/2118913370844478797[5] https://www.heise.de/select/ct/2021/18/2118314232583378987[6] https://www.heise.de/select/ct/2021/18/2121007270279521682[7] https://www.heise.de/select/ct/2021/18/2120411105449144072[8] https://www.heise.de/select/ct/2021/18/2118207383140197497[9] https://www.heise.de/select/ct/2021/18/2120300402760007086[10] https://www.heise.de/select/ct/2021/18/2120212262046232274[11] https://www.heise.de/select/ct/2021/18/2104314470145316514[12] https://www.heise.de/select/ct/2021/18/2112417523874083746[13] https://www.heise.de/select/ct/2021/18/2119612275149569355[14] mailto:uk@ct.de[15] https://www.heise.de/select/ct/2021/18/2031014552154148829[16] https://www.heise.de/select/ct/2021/18/2031014552307432676[17] https://www.heise.de/select/ct/2021/18/2031014552606769863[18] https://www.heise.de/select/ct/2021/18/2031014550480465643[19] https://www.heise.de/select/ct/2021/18[20] https://www.heise.de/newsletter/manage/ct-magazin
Copyright © 2021 Heise Medien
Tabs oder Spaces? Diese Frage spaltet Entwicklerinnen und Entwickler seit Jahren, wenn nicht seit Jahrzehnten. Dabei ist die Frage, ob mit Tabs oder Spaces eingerückt werden sollte, eigentlich ganz leicht zu beantworten.
Die Frage nach der Einrückung ist annähernd so alt wie die Softwareentwicklung selbst. Für den Einsatz von Tabs und auch für den von Spaces gibt es unzählige Argumente – und jeweils mindestens genauso viele Argumente dagegen.
Eigentlich könnte man sich auf den Standpunkt stellen, dass es letztlich keine Rolle spielt, wie eingerückt wird – doch praktisch jede Entwicklerin und jeder Entwickler hat eine eigene Meinung dazu, die ausführlich begründet werden kann.
Was spricht also wirklich für den Einsatz von Tabs, was für den von Spaces – und was ist wann warum zu bevorzugen? Das folgende Video versucht, die Frage zu beantworten:
URL dieses Artikels:https://www.heise.de/-6153563
Copyright © 2021 Heise Medien
![]()
In c't 17/2021 erfahren Sie, wie Sie mit dem Raspi alte Hardware wieder zum Laufen kriegen oder sogar ins Netz holen. Außerdem gehts um Online-Wahlwerbung, Phishing und das Homeoffice im Grünen.
Moin aus Hannover,
in den 90er Jahren habe ich einen ganzen Sommer lang geschuftet, um mir eine Stereoanlage zu kaufen: Verstärker, Radioreceiver, Decks für Kassetten und CDs – alles dabei. Nur WLAN kannte sie damals noch nicht. Und so wanderte das gute Stück irgendwann, nachdem MP3s die CD und später Streaming die MP3s ablöste, in den Keller – und einen Umzug später auf den Recyclinghof. Sollten Sie Ihre Anlage von damals noch nicht abgegeben haben, werfen Sie doch ein Blick in die aktuelle c't: Mein Kollege Dennis Schirrmacher zeigt, wie sich Spotify und andere Streamingdienste per WLAN über einen Raspi mit alten Hifi-Türmen verbinden.
Mit dem Raspi lassen sich auch noch allerlei andere alte Hardware netzfähig machen oder deren Sicherheits- und Treiberprobleme beseitigen: den alten, aber immer noch hervorragend funktionierenden Scanner zum Beispiel, oder teure Mess- oder Medizingeräte:

Es ist 2021, und immer noch schafft es weder mein Mailanbieter, noch die Software auf meinem Server, allen Spam effektiv in meinen Konten herauszufischen. Heute zum Beispiel habe ich schon einen "TESLA Model 3" gewonnen und wurde ausgewählt für die Teilnahme an einer Umfrage zur Erfahrung in Supermärkten, zu der mir der Spammer auch noch mit einem "Herzlichen Glückwunsch!" gratulierte. Mit Pflastern könnte ich in 30 Tagen 14,8 kg abnehmen und mit einem Spray meine "Standfestigkeit" erhöhen. Damit ich mir Pflaster und Spray leisten kann, könnte ich mit Amazon unverschämt viel Geld verdienen, heißt es in einer anderen Mail. Sehr verlockend – bisher hat ja nur Amazon unverschämt viel Geld an mir verdient.
Das klingt zwar amüsant, ist aber auch ziemlich nervig – und gefährlich. Immer mal wieder mischt sich darunter eine saugut gefälschte Rechnung für einen Dienst, den ich nutze, oder ein vermeintlicher Tracking-Link genau dann, wenn ich etwas bestellt habe. Ein falscher Klick trennt mich dann von einer Phishing-Attacke.
Mein Kollege Jan Mahn war nach einem Hinweis eines c't-Lesers einem dieser Phishing-Spammer auf der Spur. Er hat seinen Namen herausgefunden und bis in Telegram-Channels und Microsoft-Clouds verfolgt – und ihm dann selbst eine Phishing-Nachricht geschickt. Die ganze Geschichte lesen Sie in der aktuellen c't:
Wie wir einem unachtsamen Phisher auf die Schliche kamen [12]
Welches waren die absurdesten – oder geschicktesten – Spam-Mails, die Sie je bekommen haben? Schreiben Sie mir an (aber forwarden Sie mir auf keinen Fall Ihren Spam ;) ).

Bestellen Sie unsere Heftvorschau als Newsletter und erhalten Sie alle 2 Wochen einen Überblick über die aktuelle c't per Mail:
URL dieses Artikels:https://www.heise.de/-6148051
Links in diesem Artikel:[1] https://www.heise.de/select/ct/2021/17/2115912325966703636[2] https://www.heise.de/select/ct/2021/17/2115912341181956736[3] https://www.heise.de/select/ct/2021/17/2115912375249708287[4] https://www.heise.de/select/ct/2021/17/2115312330005159731[5] https://www.heise.de/select/ct/2021/17/2119310071989299936[6] https://www.heise.de/select/ct/2021/17/2116711360296623124[7] https://www.heise.de/select/ct/2021/17/2117410351882465724[8] https://www.heise.de/select/ct/2021/17/2116609511633046882[9] https://www.heise.de/select/ct/2021/17/2116008564237539335[10] https://www.heise.de/select/ct/2021/17/2116008595413429691[11] https://www.heise.de/select/ct/2021/17/2116708124887893691[12] https://www.heise.de/select/ct/2021/17/2118708502587724805[13] mailto:acb@ct.de[14] https://www.heise.de/select/ct/2021/17/2031014542585999188[15] https://www.heise.de/select/ct/2021/17/2031014542784320586[16] https://www.heise.de/select/ct/2021/17/2031014542991089020[17] https://www.heise.de/select/ct/2021/17/2031014542265054262[18] https://www.heise.de/select/ct/2021/17[19] https://www.heise.de/newsletter/manage/ct-magazin
Copyright © 2021 Heise Medien
Software-Entwicklung ist voller spannender Herausforderungen, und es gibt immer etwas Neues zu lernen: Software ist schließlich eine der komplexesten Dinge, die von Menschen gebaut wird. Aber die Ergebnisse in der Realität sind manchmal mehr als enttäuschend – sie sind beschämend.
In der aktuellen c't ist ein Artikel [1], dessen Untertitel eigentlich schon der erste Aufreger sein könnte: Es geht um "typische" Datenlecks in Software für die Test- und Impfterminvergabe. Mit anderen Worten: Software in diesem Bereich ist so unsicher, dass es typische Fehler gibt – und diese Software ist auch in Produktion. Die im Artikel diskutierte Software ist medizinische Software. Nehmen wir an, wir würden über andere Medikamente sprechen. Wäre es akzeptabel, dass es "typische" vermeidbare Sicherheitsprobleme bei Medikamenten gäbe, die auch tatsächlich verordnet und eingenommen werden?
Im Übrigen ist die c’t in diesem Bereich nicht alleine. Die Gruppe zerforschung beschäftigt sich auch intensiv mit der Sicherheit solcher Lösungen und schreibt [2]: “Wir haben mittlerweile das vierte Testzentrums-Datenleck in drei Monaten gefunden und sind einfach nur noch genervt.” Was würden wir sagen, wenn es das vierte Problem in drei Monaten mit verschiedenen Medikamenten gäbe? Und Experten anschließend von der Sicherheit der Medikamente “einfach nur noch genervt” sind?
Der Vergleich zwischen Software und Medikamenten scheint auf den ersten Blick weit hergeholt zu sein. Mittlerweile ist es vielen gleichgültig, wo überall Daten leaken. Es passiert einfach so häufig, und bestimmte Unternehmen leben von dem Ausspionieren ihrer Kunden. Aber eine Schnelltest-Software [3]machte es sehr einfach Testergebnisse zu fälschen. Solche Testergebnisse werden verlangt, um Personen vor einer Ansteckung durch Kontakt mit Infizierten zu schützen. Wenn die Testergebnisse gefälscht werden können, stellt das ein Gesundheitsrisiko dar – wie ein fehlerhaftes Medikament auch. Die Luca App zur Kontaktverfolgung konnte sogar zu Angriffen auf Gesundheitsämter [4] genutzt werden. Über die Konsequenzen eines erfolgreichen Angriffs will ich nicht spekulieren.
Die Einleitung des sehr lesenswerten c’t Artikels berichtet dann, dass bei der c't eine Vielzahl von Meldungen über Datenlecks bei Software für Test- und Impfterminvergabe eingegangen ist. Die c't verfolgt die Probleme und informiert die Hersteller. Hier ist der nächste Aufreger: Die c't, zerforschung und viele andere machen sicher einen super Job – aber offensichtlich muss eine Zeitschrift oder eine Gruppe von Expert:innen sich dieser Probleme annehmen. Nehmen wir als Analogie wieder Medikamente. Wäre es akzeptabel, wenn eine Zeitschrift routinemäßig über mögliche Probleme mit Medikamenten informieren würde, und diese Zeitschrift dann anschließend die Fälle näher untersucht und den Herstellern mit Rat und Tat zur Seite steht – und nur so die Probleme wirklich behoben werden? Sicher gibt es vereinzelt Medikamenten-Skandale, die über die Presse bekannt werden, und Fachzeitschrift, die wissenschaftliche Studien über Medikamente verbreiten. Aber eklatante Probleme mit Medikamenten sind eben nicht Routine. Und in der Branche und bei den Behörden sind Maßnahmen etabliert, damit das so bleibt.
Das Fazit ist dann der nächste Aufreger: "Ärzte und Apotheker sollten sich die Software sehr gewissenhaft ansehen (oder einen unabhängigen IT-Experten zu Rate ziehen)." Man kann kaum ernsthaft von Ärzt:innen und Apotheker:innen die Kompetenz verlangen, die Sicherheit einer Software zu bewerten. Ich arbeite im Bereich Software-Entwicklung und würde mir das selber nicht wirklich zutrauen. Vor kurzem bin ich privat um Rat in Bezug auf ein Sicherheitsthema gebeten worden und habe auch einen Rat gegeben. Allerdings bleibt das schlechte Gefühl, dass ich gegebenenfalls ein Detail übersehen habe, das am Ende dann doch große Auswirkungen hat. Daher ist der Hinweis, Expert:innen zu Rate zu ziehen, auch sehr sinnvoll. Aber auch hier bemühe ich den Vergleich zu einem Medikament: Kann man von einem Patienten verlangen, sich selber mit der Sicherheit eines Medikaments zu beschäftigen? Ich erwarte eigentlich, dass Medikamente, die ich in der Apotheke kaufe oder die mir ein Arzt verordnet, sicher sind. Üblicherweise werde ich sogar von Apotheker:innen und Ärzt:innen über die Details und mögliche Probleme proaktiv beraten.
Die Verantwortung für den Datenschutz auf die Nutzer:innen zu verlagern, kann desaströse Konsequenzen haben. Einige Lehrer:innen, die auf Eigeninitiative Videokonferenz-Lösungen in der Pandemie für ihren Unterricht genutzt haben, mussten die Sicherheit und den Datenschutz dieser Lösungen selber bewerten, weil sie oft dazu keine klaren Produktempfehlungen bekommen haben. Am Ende müssen die Lehrer:innen mit den Konsequenzen leben, wenn ihre Beurteilung falsch war – oder sie unterlassen es, sich dem Risiko auszusetzen und machen eben keinen Unterricht per Videokonferenz. Beide Konsequenzen sind nicht akzeptabel.
Aber die Empfehlung, sich die Lösungen anzuschauen, ist skurrilerweise sinnvoll: Tatsächlich können c't -Leser:innen meiner Einschätzung nach die geschilderten Herausforderungen nachvollziehen und auch Software auf diese Schwächen untersuchen. Dann sind diese Probleme aber auch für Software-Entwickler:innen nachvollziehbar – und das wirft die Frage auf, warum sie in Software überhaupt vorhanden sind. Mit anderen Worten: Wir sprechen nicht über subtile Herausforderungen, sondern solche, die man nach dem Studium einiger Seiten einer Zeitschrift verstehen kann und vermutlich auch abstellen können müsste – und daran scheitern die Hersteller dieser Software-Lösungen.
Es ist gut, dass die c’t und viele Sicherheitsforscher:innen und Expert:innen sich kümmern. Aber es ist erschreckend, dass unsere Branche der Softwareentwicklung solche fehlerhaften Produkte auf den Markt bringt. Als Ergebnis gibt es keinen Aufschrei, sondern eben einen Artikel, der davon handelt, wie Nutzer:innen sich mit der Situation arrangieren können und wie sie selber herausfinden können, ob die Software ausreichend sicher ist. Sind wir so abgestumpft, dass wir diesen Zustand akzeptieren? Ist unsere Branche wirklich so unprofessionell, dass sich Nutzer:innen eben mit diesen Mängeln unserer Produkte arrangieren müssen? Ist es unser Qualitätsanspruch, das unserer Produkte Fehler haben, die man als interessierter Laie nach dem Lesen einiger Seiten in einer Zeitschrift diagnostizieren kann?
Besonders bedenklich ist, dass diese Anwendungen personenbezogene Gesundheitsdaten verwalten. Diese Daten sind sehr sensibel und besonders schützenswert. Auch die Luca App [5] zur Kontaktverfolgung geht mit Gesundheitsdaten um, weil über die App ja gerade infizierte Personen verfolgt werden soll. Dazu muss der Infektionsstatus bekannt sein – und das sind Gesundheitsdaten. Auch für diese App gibt es sehr viel Kritik und Sicherheitslücken [6] bis hin zu den schon erwähnten möglichen Angriffen auf Gesundheitsämter. Dazu hat der Sicherheitsexperten Marcus Mengs zunächst ein Dokument mit fast 21.000 Wörtern geschrieben und dann die Arbeit an der Luca App eingestellt. Der Grund steht im Dokument [7]: “Der Hersteller gibt mir keine Zeit die Zusammenhänge zu dokumentieren, welche zu Sicherheitslücken im Code führen und patcht stattdessen ständig Code mit neuen Fehlern nach.”
Mit mindestens 25 Mio € Einnahmen [8] muss man die Luca App einen kommerziellen Erfolg nennen. Und die App hat bekanntermaßen prominente Unterstützung durch den Musiker Smudo [9]. Seine Band “Die fantastischen Vier” ist “begeistert” von dem Produkt und sieht sich als Teil des Teams [10]. Das zeigt eine Erklärungsmöglichkeit für den Zustand unserer Branche: Wenn der Applaus der Prominenz und der kommerzielle Erfolg von der Sicherheit der Anwendungen entkoppelt ist, dann ist es unlogisch, sich mit Sicherheit zu beschäftigen, weil sie für den Erfolg egal ist – außer man hat ethische Prinzipien, die dem Ignorieren entgegenstehen.
Die Spitze der Absurdität ist aber das immer wieder zu hörende Argument, dass der Datenschutz einer effektiven und effizienten IT im Wege steht. Der "Datenschutz", der in dem c’t-Artikel beschrieben wird oder der bei der Luca App “umgesetzt” wurde, kann damit zumindest nicht gemeint sein. So ist der Datenschutz auf der einen Seite nicht gewährleistet, kann aber gleichzeitig als Entschuldigung für weitere Mängel unserer Branche dienen.
Aber es gibt auch Lichtblicke. Ich war an Softwareprojekten beteiligt, die unter anderem auch personenbezogene Gesundheitsdaten verwaltet haben. Dort war sehr präsent, dass diese Daten besonders geschützt werden müssen und dementsprechend wurden diese Herausforderungen adressiert. Auch in vielen anderen Kontexten habe ich beispielsweise die Anforderungen durch die Datenschutzgrundverordnung DSGVO als wesentlich für Architektur-Diskussionen erlebt. Und natürlich gibt es für die Kontaktverfolgung-Anwendungen wie die Corona-Warn-App, die so konsequent auf Datenschutz ausgerichtet ist, dass sogar der Chaos Computer Club sie lobt [11]. Und den bereits erwähnten Sicherheitsexpert:innen und vielen anderen, die hier gar nicht erwähnt werden, ist die Situation offensichtlich nicht egal, sondern sie arbeiten aktiv daran sie zu verbessern - wofür ich mich an dieser Stelle bedanken möchte.
Am Ende bleibt aber die unangenehme Frage, ob unsere Branche im Vergleich zu anderen Branchen verantwortungsloser handelt. So oder so sollten wir uns alle anstrengen, in Zukunft Sicherheitsprobleme in Software zu vermeiden. Das ist besser, als sich zu schämen. Und das Studium des Artikels in der c't oder die Werke der Sicherheitsexpert:innen kann ein guter erster Schritt sein, um Sicherheitslücken kennenzulernen und zukünftig zu vermeiden. Vielleicht wird unsere Branche dann endlich erwachsener und verantwortungsvoller.
Leider ist die Qualität von Software-Lösungen oft mehr als schlecht. Sich dafür zu schämen ist aber nicht so effektiv, wie die Situation zu verbessern.
Vielen Dank an Anja Kammer, Tammo van Lessen, Tanja Maritzen, Joachim Praetorius, Max Schröter, Stefan Tilkov und Jan Seeger für die Kommentare zu einer früheren Version des Artikels.
URL dieses Artikels:https://www.heise.de/-6116235
Links in diesem Artikel:[1] https://www.heise.de/select/ct/2021/14/2113009092122225886[2] https://zerforschung.org/posts/medicant/[3] https://zerforschung.org/posts/medicus/[4] https://de.wikipedia.org/wiki/Luca_(App)#Angriff_auf_Gesundheits%C3%A4mter_und_Datenklau_durch_Code-Injection[5] https://www.heise.de/thema/Luca_App[6] https://de.wikipedia.org/wiki/Luca_(App)#Kritik[7] https://github.com/mame82/LucaAppIssues/blob/main/abandoned_issues.md[8] https://de.wikipedia.org/wiki/Luca_(App)#Einkauf_der_App_trotz_schwerer_M%C3%A4ngel,_ohne_staatliche_Ausschreibung[9] https://de.wikipedia.org/wiki/Luca_(App)#Entwicklung[10] https://www.luca-app.de/uber-uns/[11] https://www.zdf.de/nachrichten/politik/corona-app-launch-100.html
Copyright © 2021 Heise Medien
Der ECMAScript-Standard wird stetig weiterentwickelt und JavaScript damit stetig verbessert. Eine der größten Baustellen war bisher der Umgang mit Datum und Zeit. Das ändert sich jedoch mit der neuen Temporal API.
JavaScript, Datum und Zeit, das ist so eine Geschichte für sich und sie nimmt leider kein gutes Ende. Jedes Mal, wenn ich ein neues Date-Objekt erzeuge oder noch schlimmer mit Datum und Zeit rechnen muss, frage ich mich, wie es eine Sprache mit einer so schlechten Datums- und Zeitimplementierung so weit bringen konnte. Aber seien wir mal ehrlich: Bei diesem Thema bekleckern sich auch andere Sprachen nicht mit Ruhm. Aber jetzt sprechen wir erstmal von JavaScript und einer sehr erfreulichen Entwicklung.
JavaScript erhält eine Spracherweiterung mit dem Namen Temporal, die sich mit Datum und Zeit beschäftigt und eine deutlich angenehmere Schnittstelle als die bisherige Date-API hat, die sich übrigens an einer der ersten Date-APIs von Java orientiert. Die neue Schnittstelle behebt nicht nur einige Schwächen der bisherigen, sondern gestaltet den Umgang mit Zeit und Datum deutlich moderner und greift einige Aspekte von modernen Bibliotheken wie beispielsweise die Unveränderbarkeit von Objekten auf. Aktuell befindet sich die API zwar noch als ECMAScript Proposal in der Stage 3. Das ist jedoch auch beinahe das Ende des Standardisierungsprozesses mit seinen insgesamt vier Stufen, die ein Feature durchlaufen muss, bevor es in den Standard aufgenommen wird. Üblicherweise gibt es in der dritten Stufe keine größeren Änderungen mehr an der Schnittstelle, sodass das, was wir jetzt in der Temporal API finden, mit großer Wahrscheinlichkeit auch das sein wird, was schließlich die Browserhersteller in ihre JavaScript Engines aufnehmen.
Für alle Ungeduldigen, die der Temporal API bereits jetzt eine Chance geben möchten, existiert ein Polyfill, das die Schnittstelle in aktuellen Browsern nachbildet. Das NPM-Paket mit dem Namen proposal-temporal enthält es und wird mit dem Kommando npm install proposal-temporal installiert. Die Temporal-Schnittstelle ist, wie alle nativen Sprachfeatures von JavaScript, plattformunabhängig. Das bedeutet, dass es sowohl clientseitig im Browser als auch serverseitig in Node.js zum Einsatz kommen kann. Das gleiche gilt für das Polyfill, es ist außerdem kompatibel mit allen gängigen Modulsystemen, sodass es in allen Umgebungen getestet werden kann.
Es gibt mehrere Anwendungsfälle, denen man in JavaScript-Applikationen immer wieder begegnet und diese werden wir uns als Erstes ansehen. Beginnen wir mit dem aktuellen Zeitpunkt. Das Temporal-Objekt bietet für diesen Zweck das now-Objekt mit der plainDateTime-ISO-Methode. Gerade für den Austausch zwischen Systemen arbeiten Entwickler nicht nur mit einer Datumszeichenkette, sondern mit UNIX-Zeitstempeln, also den Sekunden, die seit dem 1.1.1970 00:00 vergangen sind. Hierfür bietet das now-Objekt die Methode instant, deren Rückgabewert die epochSeconds-Eigenschaft aufweist, die wiederum den UNIX-Zeitstempel enthält. Die letzte Aufgabe im Schnellstart mit der neuen Temporal-Schnittstelle ist das Erzeugen eines beliebigen Zeitpunkts. Dies wird über die PlainDateTime.from-Methode erreicht. Die from-Methode akzeptiert sowohl ein Konfigurationsobjekt als auch eine Zeichenkette, die das gewünschte Datum angibt. Das nachfolgende Listing zeigt die verschiedenen Szenarien.
import { Temporal } from "proposal-temporal";const currentTime = Temporal.now.plainDateTimeISO();
console.log(currentTime.toString()); // Ausgabe:
2021-06-03T08:22:18.381338378const timeStamp = Temporal.now.instant().epochSeconds;
console.log(timeStamp); // Ausgabe: 1622701338
const christmas = Temporal.PlainDateTime.from({
day: 24,
month: 12,
year: 2021,
hour: 20,
minute: 15,
second: 32
});
console.log(christmas.toString()); // Ausgabe: 2021-12-24T20:15:32
Bestehende Applikationen werden es bei der Umstellung von der Date API auf die neue Temporal-Schnittstelle erforderlich machen, dass die Datums- und Zeitobjekte zwischen beiden Schnittstellen konvertiert werden. Listing 2 zeigt, wie eine solche Umwandlung funktionieren kann.
import { Temporal } from 'proposal-temporal';const date = new Date('2020-12-24T20:15:32');
const temporal = new Temporal.ZonedDateTime(
BigInt(date.getTime() * 10 ** 6),
'Europe/Berlin');
console.log(temporal.toString());
//2020-12-24T20:15:32+01:00[Europe/Berlin]
const temporal2 = Temporal.ZonedDateTime.from({
year: 2020,
month: 12,
day: 24,
hour: 20,
minute: 15,
second: 32,
timeZone: 'Europe/Berlin'
});
const date2 = new Date(temporal2.epochMilliseconds);
console.log(date2.toLocaleString()); // Ausgabe: 24/12/2020, 20:15:32
Dieses Codebeispiel zeigt ein grundlegendes Problem der Date API von JavaScript sehr schön: Es ist nicht möglich, ein Date-Objekt zu erzeugen, das unabhängig von einer Zeitzone ist. Das Date-Objekt hat die lokale Zeitzone der Umgebung, in meinem Fall ist das die mitteleuropäische Zeit. Die Entsprechung dieses Konzepts in Temporal sind Objekte vom Typ ZonedDateTime. In einigen Fällen ist jedoch die Information über die Zeitzone in einem Datumsobjekt gar nicht notwendig. Dieses Problem löst die Temporal-Schnittstelle, indem es verschiedene Objekte gibt, um eine Datums- und Zeit-Kombination zu repräsentieren. Die folgende Abbildung aus dem Temporal Proposal fasst diesen Sachverhalt grafisch zusammen.

Die umfassendste Klasse der Schnittstelle ist ZonedDateTime, sie enthält die Information über den Zeitpunkt sowie die Zeitzone. Im Gegensatz dazu haben die Plain-Typen wie PlainDateTime oder PlainTime keine Information über Zeitzonen und stellen lediglich einen generischen Zeitpunkt dar. Alle Typen, außer Instant, arbeiten außerdem auf einem bestimmten Kalender. Standardmäßig nutzt Temporal den Gregorianischen Kalender, der in der ISO 8601-Norm standardisiert ist. Weitere Beispiele für Kalender, die an dieser Stelle zum Einsatz kommen können, sind der Buddhistische, der Persische oder der Japanische Kalender.
Ein großer Vorteil der Temporal API gegenüber der bisherigen Implementierung ist, dass sie in der Lage ist, mit Datumswerten zu rechnen und mit Zeitspannen umzugehen. Die Grundlage hierfür bildet die Duration-Klasse beziehungsweise Objekte, die der Struktur entsprechen, die diese Klasse vorgibt. Beim Rechnen mit Datumswerten kommt eine weitere Besonderheit der Temporal Schnittstelle zum Tragen: Die Schnittstelle arbeitet mit immutable Objekten, modifiziert das Ausgangsobjekt beispielsweise bei einer Addition nicht direkt, sondern gibt ein neues Objekt zurück. Wie das im Code funktioniert, zeigt folgendes Beispiel:
import { Temporal } from 'proposal-temporal'; const now = Temporal.now.plainDateTimeISO();
console.log(now.toString()); // Ausgabe: 2021-06-03T19:26:42.949202945
const future = now.add({days: 30});
console.log(future.toString()); // Ausgabe: 2021-07-03T19:26:42.949202945
console.log(now.toString()); // Ausgabe: 2021-06-03T19:26:42.949202945
Alle Zoned- und Plain-Klassen der Temporal API unterstützen die add- und subtract-Methoden. Diese ermöglichen, wie die Namen schon verraten, das Hinzufügen beziehungsweise Abziehen von Zeitspannen zu einem bestimmten Zeitpunkt. Beide Methoden akzeptieren einfache JavaScript-Objekte mit den Angaben, wie viele Zeiteinheiten, also beispielsweise Monate, Tage oder Stunden, abgezogen oder addiert werden sollen. Alternativ kann auch ein Duration-Objekt eingesetzt werden.
Ein weiterer Einsatzzweck für das Duration-Objekt ist die Bestimmung der Zeitspanne, die zwischen zwei Datumswerten liegt. Soll beispielsweise herausgefunden werden, wie viele Tage zwischen dem Beginn und dem Ende der Weihnachtsferien liegen, kann der folgende Quellcode eingesetzt werden.
import { Temporal } from 'proposal-temporal'; const start = Temporal.PlainDate.from({
day: 24,
month: 12,
year: 2021
});
const end = Temporal.PlainDate.from({
day: 8,
month: 1,
year: 2022
});
const diff = start.until(end);
const diff2 = end.since(start);
console.log(diff.toString()); // Ausgabe: P15D
console.log(diff2.toString()); // Ausgabe: P15D
Sowohl der Start- als auch der Endpunkt werden durch ein PlainDate-Objekt repräsentiert, da in diesem Fall weder Zeitzone noch Uhrzeit benötigt wird. Neben der hier verwendeten until-Methode gibt es mit der since-Methode auch noch das Gegenstück, das die Zeitspanne vom Ende bis zum Start misst. Das Ergebnis ist in beiden Fällen ein Duration-Objekt, das 15 Tage umfasst. Ein solches Objekt kann dann wieder in Kombination mit weiteren Temporal-Objekten verwendet werden, um beispielsweise eine Addition oder Subtraktion durchzuführen. Die Formatierung der Ausgabe, P15D, erscheint auf den ersten Blick etwas gewöhnungsbedürftig, folgt jedoch einem klaren Format. Die Zeichenkette wird durch das Zeichen P eingeleitet, danach folgen Jahre, Monate, Wochen und Tage. Anschließend werden Datum und Uhrzeit durch das Zeichen T getrennt gefolgt von Stunden, Minuten und Sekunden.
Wären in diesem Beispiel nicht nur Datumswerte, sondern auch Uhrzeiten beteiligt, gibt es mit der round-Methode die Möglichkeit, eine Zeitspanne auf ganze Tage zu runden. Die Methode akzeptiert ein Konfigurationsobjekt, dessen wichtigste Eigenschaften largestUnit, smallestUnit und roundingMode sind. Die ersten beiden geben die kleinste beziehungsweise größte Einheit an, auf die gerundet werden soll. Mit der roundingMode-Eigenschaft können Entwickler festlegen, wie gerundet wird. Der Code in folgendem Listing rundet beispielsweise auf volle Tage ab:
import { Temporal } from 'proposal-temporal'; const start = Temporal.PlainDateTime.from({
day: 24,
month: 12,
year: 2021,
hour: 7,
minute: 30
});
const end = Temporal.PlainDateTime.from({
day: 8,
month: 1,
year: 2022,
hour: 20,
minute: 15
});
console.log(start.until(end).toString()); // Ausgabe: P15DT12H45M
const diff = start.until(end).round({
smallestUnit: 'day',
roundingMode: 'floor'
});
console.log(diff.toString()); // Ausgabe: P15D
Das Ergebnis der round-Methode ist wiederum ein Duration-Objekt, das die Applikation weiterverwenden kann. Das Prinzip der Immutability bleibt also auch in diesem Fall gewahrt.
Mit der Temporal API trägt das TC39 der Tatsache Rechnung, dass die Date-Funktionalität von JavaScript einige Schwächen aufweist und für die Umsetzung moderner Applikationen nicht die beste Wahl ist. Rund um den Umgang mit Datum und Zeit sind im Laufe der Zeit eine Vielzahl von Bibliotheken entstanden. Das Temporal Proposal greift viele der Ideen auf, die diese Bibliotheken verfolgen und integriert sie in den Sprachkern von JavaScript. Das hat den Vorteil, dass diese Features, sobald die Temporal API in den Standard aufgenommen wurde, nativ vom Browser unterstützt werden. Das bedeutet, dass es keinen zusätzlichen Overhead durch Bibliotheken mehr gibt und die Funktionalität potenziell performanter ist, da sie direkt im Browser umgesetzt ist. Diese Schnittstelle ist ein sehr gutes Beispiel dafür, dass sich JavaScript stetig weiterentwickelt und dabei auf die Anforderungen der Entwickler eingeht.
URL dieses Artikels:https://www.heise.de/-6069626
Links in diesem Artikel:[1] https://tc39.es/proposal-temporal/docs/[2] https://tc39.es/proposal-temporal/docs/cookbook.html
Copyright © 2021 Heise Medien
Note: Last release before dropping support for PHP 5 and Internet Explorer 11.
pdo_sqlite is optional except for export/import SQLite #3545ORIG_PATH_INFO #3560latest tag in git for the latest FreshRSS release #3524Microsoft Paint ist der Dino unter den Produktivitätsanwendungen. Das 1985 erstmals erschienene Programm brachte Kinderaugen zum Leuchten und machte viele Anwender mit der Erstellung einfacher Computergrafiken vertraut. Nun kehrt die Windows-95-Variante in Form eines webbasierten Remakes in den Browser zurück.
Ja, es stimmt. Microsoft Paint war nie zu vergleichen mit Programmen wie GIMP oder Photoshop. Keine Ebenen, in einigen Versionen nur drei Undo-Schritte, je nach Version mehr oder weniger Dateiformate. Dennoch implementiert Paint exakt den Workflow, auf den jede Produktivitätsanwendung auf dem Desktop setzt:
Jetzt feiert Paint mit all diesen Funktionen eine Renaissance im Browser: paint.js.org [1] ist ein Remake von Microsoft Paint – als Progressive Web App (PWA), gebaut mit Web Components und unter Verwendung moderner Webschnittstellen zum Zugriff auf die Zwischenablage, das Dateisystem und die lokalen Schriftarten. Darüber hinaus unterstützt das Paint-Remake auch den Dark Mode. Das Programm lässt sich direkt im Browser ausführen und auf Wunsch auf dem Gerät installieren.

Als Progressive Web App [2] ist die Anwendung per se auch plattformübergreifend einsetzbar: In Chrome, Edge, Firefox und Safari, auf Android und iOS, unter Windows, Chrome OS, macOS und Linux. Die PWA ist nicht nur offline verfügbar, sondern erlaubt auch den überwiegenden Großteil der aus Microsoft Paint bekannten Aktionen: Bilddateien laden, speichern, Ausschnitte kopieren und einfügen, einfache Zeichenoperationen. Realisiert wurde das Remake mit dem Canvas-Element [3], das sich für 2-D- und 3-D-Inhalte im Web eignet. Mithilfe der im Zuge der Entwicklung von Windows 8 spezifizierten Pointer Events [4] werden Eingaben über die Maus, per Finger sowie Stift unterstützt. Auch kommen einige moderne Webschnittstellen aus dem Umfeld von Project Fugu [5] zum Einsatz.

Neben der Plattformunabhängigkeit ist ein weiterer Vorteil webbasierter Anwendungen, dass diese oftmals kleiner ausfallen als ihre nativen Gegenstücke. Für das Paint-Remake müssen derzeit 276 KByte übertragen werden (Brotli-komprimierter Quellcode), während die Binärdatei des Originals 340 KByte groß ist.
Die Anwendung setzt bewusst nicht auf ein größeres Anwendungsframework, sondern benutzt die Bibliothek Lit [6]. Dabei handelt es sich um eine schlanke Bibliothek zum (einfacheren) Verfassen von Web Components [7]. Diese wiederum lassen sich zu größeren Anwendungen zusammensetzen. Als Buildtool kommt das leichtgewichtige Snowpack [8] zum Einsatz. Für die Offlineunterstützung generiert das Toolkit Workbox [9] den passenden Service Worker. Die Codebase selbst ist in TypeScript [10] implementiert.

Das Remake setzt eine ganze Reihe von Schnittstellen ein, die im Rahmen von Project Fugu eingeführt wurden: So etwa die Async Clipboard API [11], die asynchronen Zugriff auf die Zwischenablage erlaubt. Diese Schnittstelle unterstützen sämtliche aktuellen Browser – mit Ausnahme von Firefox. Andere Funktionen sind noch relativ neu und werden lediglich in Chromium-basierten Browsern (Chrome, Edge, Opera, Brave, Samsung Internet etc.) angeboten: Mithilfe der File System Access API [12] (generell verfügbar ab Chromium 80) können Dateien direkt vom Dateisystem geöffnet und wieder dorthin gespeichert werden. Eine einmal geöffnete Datei lässt sich direkt aus der Anwendung heraus überschreiben. Darüber hinaus lässt sich eine Datei per Drag-and-Drop ins Browserfenster ziehen. Auch dann erhält der Browser Zugriff auf das Dateihandle und kann die Datei manipulieren.

Die "Send"-Funktion nutzt die Web Share API [13], um das gezeichnete Bild mit einer anderen installierten App zu teilen.
Auch ganz frische APIs sind dabei, die derzeit noch über about://flags gezielt vom Benutzer aktiviert werden müssen: Mit der Local Font Access API [14] kann sich Paint die komplette Liste der installierten Schriftarten abholen, sofern der Benutzer dem zustimmt. Über die File Handling API [15] kann sich eine Anwendung als Handler für eine bestimmte Dateierweiterung registrieren. Das Paint-Remake registriert sich bei Installation für die Dateierweiterung "png". Damit steht die Webanwendung für Dateien mit dieser Erweiterung über das Menü "Öffnen mit" zur Auswahl. Ist die Anwendung die einzige, die sich für eine Erweiterung registriert, übernimmt sie direkt die Rolle des Standardprogramms. Andernfalls kann der Benutzer sie als solches definieren. Dann wird die Anwendung per Doppelklick auf eine Datei mit der registrierten Dateierweiterung wieder gestartet.

Soweit möglich wird in allen Fällen das Konzept des Progressive Enhancement [16] angewendet: Sollte eine API auf dem Zielsystem nicht vorhanden sein, wird eine Fallback-Schnittstelle gewählt, sofern verfügbar. Andernfalls wird die Funktion deaktiviert oder gar nicht erst angeboten.
Die einzige Paint-Funktion, die auch in den Chromium-basierten Browsern nicht nachgebildet werden kann, ist das Festlegen der aktuellen Zeichnung als Desktophintergrund. Diese Funktion steht im Web nicht zur Verfügung. Sämtliche andere Funktionen lassen sich wie im Original oder leicht abgewandelt implementieren.
Das Paint-Remake unterstützt derzeit noch nicht alle Aktionen und Werkzeuge, die das Original mitbringt. Funktional vollständiger und besser auf unterschiedlichen Plattformen getestet ist das Remake jspaint.app [17], das jedoch nicht auf die neuen Schnittstellen zur Umsetzung von Dateisystemzugriff oder Offlinefähigkeit zurückgreift. Der Quellcode für das Paint-Remake ist auf GitHub zu finden [18].
Der Funktionsumfang des klassischen Paint war sicherlich nicht überragend. Und dennoch besitzt die Anwendung alle Eigenschaften einer typischen Produktivitätsanwendung. Das Remake zeigt, dass dank Project Fugu und zumindest auf Chromium-basierenden Browsern alle relevanten Schnittstellen für solche Anwendungen auch im Web verfügbar sind. Damit nimmt die Notwendigkeit nativer Anwendungen oder Wrapper-Ansätzen wie Electron (z.B. Slack, Visual Studio Code, Discord) zusehends ab. Das dürfte weiteren Geschäftsanwendungen, Büroprogrammen sowie Bild- und Videobearbeitungssoftware den Weg ins Web ebnen.
URL dieses Artikels:https://www.heise.de/-6058723
Links in diesem Artikel:[1] https://paint.js.org[2] https://www.heise.de/ratgeber/Progressive-Web-Apps-Write-once-run-anywhere-4839505.html[3] https://developer.mozilla.org/docs/Web/API/Canvas_API/Tutorial/Basic_usage[4] https://developer.mozilla.org/docs/Web/API/Pointer_events[5] https://www.heise.de/hintergrund/Project-Fugu-Brueckenschlag-zwischen-progressive-Web-und-nativen-Apps-4684369.html[6] https://lit.dev/[7] https://www.heise.de/developer/artikel/Single-Page-Applications-ohne-Framework-Web-Components-als-Ersatz-fuer-React-Co-4850959.html[8] https://www.snowpack.dev/[9] https://developers.google.com/web/tools/workbox[10] https://www.heise.de/developer/artikel/App-Entwicklung-mit-JavaScript-Teil-3-Aber-bitte-mit-Typen-3623957.html[11] https://webkit.org/blog/10855/async-clipboard-api/[12] https://web.dev/file-system-access/[13] https://web.dev/web-share/[14] https://web.dev/local-fonts/[15] https://web.dev/file-handling/[16] https://blog.tomayac.com/2020/01/23/progressive-enhancement-in-the-age-of-fugu-apis/[17] https://jspaint.app/[18] https://github.com/christianliebel/paint
Copyright © 2021 Heise Medien
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.

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:


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:
INPUT_PULLUP konfigurieren. Zum Anschluss externer Bus-Komponenten gibt es folgende Möglichkeiten:

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.
Zusätzlich integriert das Nano RP2040 Connect diverse Sensoren, unter anderem eine Messeinheit zur Trägheitsmessung (ST LSM6DSOXTR ) und ein MEMS-Mikrofon (ST MP34DT06JTR):
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].

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.

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.

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.
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
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.
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?
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++.
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.
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.
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;
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.
RTC_alarm) erfolgen, an dem die Echtzeituhr den Raspberry Pi Pico aufwecken soll. 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.
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.
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.
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
};
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.
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:

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.

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.

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.
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
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]
Leider funktionieren die Schlafmodi nicht immer problemlos:
printf() oder puts() am seriellen Monitor mehr möglich. Zum Debuggen sollten Maker daher die Picoprobe als Hardware-Debugger einsetzen.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.
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.
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
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-VergleichDateTimeFormat: Formatierung von Datums- und ZeitwertenNumberFormat: Formatierung von Zahlen wie Währungen oder ProzentwertenPluralRules: Unterstützung bei der Behandlung von Ein- und MehrzahlDie 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.
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.
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.
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.
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.
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
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.
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.

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:

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.
Zur Kommunikation mit der Außenwelt bieten sich einer PIO-Zustandsmaschine neben I/O zwei Möglichkeiten:
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.

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.

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 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.

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.
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.
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.
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.

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

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.

ü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.

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.

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

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.

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

bewegt, wenig überraschend, den Inhalt der Quelle zum Ziel. Ziele können sein:
PINS gemäß out-Mapping EXEC ISR OSR PC X YQuellen können sein:
PINSXYISROSRNULLSTATUS 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.

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 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
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.
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.
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.
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.
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:

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 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.

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:
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);
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:

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!
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
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:
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.portpublic 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].
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].
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].
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].
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:
@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. @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].
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
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.
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.
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].
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.).

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
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.
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.
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.
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.
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.
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
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).


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.

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

Die folgenden Pins sind Teil dieser Funktionalitäten und lassen sich vom Nutzer für eigene Zwecke konfigurieren:
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.
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.
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.
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.


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:
Benutzt haben Firmware-Entwickler die Arduino IDE Version 1.8.10. Zusätzlich benötigt die Firmware

Um eigene Experimente durchzuführen, habe ich folgende Komponenten eingesetzt:
Der folgende Versuchsaufbau stellt fest, ob eine Tür (oder ein Fenster) geöffnet oder geschlossen wird:

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

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
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.
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.

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.

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-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).

Ü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().

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 (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.

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;
}
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.

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.

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:
DATA_LENGTH) beendet sich das Programm.
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".
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
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.
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.
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:
@injectable-Decorator kennzeichnet Klassen, die mit DI arbeiten. Der @inject-Decorator markiert die Stellen im Code, an denen die Abhängigkeiten eigensetzt werden.get-Methode der Container-Instanz erlaubt das Auflösen der Abhängigkeiten und somit die Arbeit mit den konkreten Objekten.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.
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
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.
Nutzer können unterschiedliche Betriebssysteme einsetzen, wenn sie für den Pico als Zielsystem programmieren wollen:
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.
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].
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: 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.

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).

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.

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.

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.
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).
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]).

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.

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:

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.

#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 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