Schnelle Entwicklung von Echtzeitanwendungen auf Mikrocontroller-Basis mithilfe von MicroPython

Von Jacob Beningo

Zur Verfügung gestellt von Nordamerikanische Fachredakteure von Digi-Key

Eingebettete Echtzeitsysteme werden gerade äußerst komplex. Sie setzen ein umfassendes Wissen über die komplizierten 32-Bit-Mikrocontroller sowie Sensoren, Algorithmen, Internetprotokolle und eine Vielzahl von Endbenutzer-Anwendungen voraus. Angesichts kürzerer Entwicklungszyklen und eines wachsenden Funktionsumfangs müssen Entwicklungsteams Wege finden, den Entwicklungsprozess zu beschleunigen und gleichzeitig ihren Code auf neue Produkte portieren zu können: Das wiederum setzt eine integrierte und flexible Entwicklungsplattform voraus.

Es gibt mehrere mikrocontroller-spezifische Plattformen, die dabei helfen, den Entwicklungsprozess zu beschleunigen. Das Problem mit diesen Lösungen ist jedoch, dass sie die Entwickler an einen Anbieter von Mikrocontrollern binden. Die Portierung von Software von einer Plattform auf die nächste kann zeit- und kostenaufwendig sein.

Eine einzigartige und neue Lösung, die breite Akzeptanz findet und eine große Dynamik entfaltet, ist die Kopplung der Low-Level-Mikrocontrollerhardware mit einer höheren Programmiersprache wie Python. Eine solche Lösung ist MicroPython. Die Sprache läuft auf Mikrocontrollern unterschiedlicher Anbieter und folgt dem Open-Source-Prinzip. Dadurch können Entwickler sie nutzen und an die eigenen Erfordernisse anpassen.

MicroPython.org beschreibt sie als schlanke und effiziente Implementierung der Programmiersprache Python 3 mit einer kleinen Teilmenge der Python-Standardbibliothek, die für die Ausführung auf Mikrocontrollern und in eng begrenzten Umgebungen optimiert ist. MicroPython begann als Kickstarter-Projekt, das nicht nur mit Erfolg finanziert wurde, sondern viele Anhänger gewann. Inzwischen wurde MicroPython erfolgreich in Projekten in mehreren Branchen – z. B. bei Industrie- und Raumfahrtsystemen – eingesetzt.

Auswahl des richtigen Mikrocontrollers

MicroPython läuft auf unterschiedlichen Mikrocontrollern, und es gibt keine größeren Beschränkungen, was die Portierung von MicroPython auf mehr Mikrocontroller angeht. Voraussetzung dafür ist, dass sie über genügend RAM, Flash-Speicher und Prozessorleistung verfügen, um den Interpreter auszuführen. Vor diesem Hintergrund gibt es verschiedene wichtige Anforderungen, auf die ein Entwickler einen Mikrocontroller prüfen muss, der für die Ausführung von MicroPython eingesetzt werden soll:

  • mindestens 256 KB Flash-Speicher
  • mindestens 16 KB RAM
  • mindestens einen mit 80 MHz getakteten Prozessor

Das sind allgemeine Empfehlungen, und Entwickler können je nach den Erfordernissen ihrer Anwendungen und Zeit, die sie mit dem Anpassen des MicroPython-Kernels zubringen wollen, von diesen Werten abweichen. So lässt sich MicroPython beispielsweise so modifizieren, dass es weniger als 256 KB Flash-Speicher nutzt. Diese Empfehlungen stellen Erfahrungswerte dar, die dem Entwickler Raum für das Wachstum seines Anwendungscodes lassen sollen.

MicroPython wurde bereits auf verschiedene Mikrocontroller-Baureihen portiert, die sich als guter Ausgangspunkt für die Portierung auf eine neue Plattform oder die Auswahl eines bereits unterstützten Mikrocontrollers anbieten. Das Hauptverzeichnis des MicroPython-Quellcodes ist in Abbildung 1 dargestellt. Hier kann der Leser verschiedene unterstützte Mikrocontroller-Geräte sehen:

Abbildung der Beispielordner-Verzeichnisstruktur

Abbildung 1: Beispiel der Ordnerverzeichnisstruktur mit den verfügbaren Mikrocontroller-Plattformen, die gegenwärtig MicroPython unterstützen. Dazu gehören ARM, CC3200, esp8266, Microchip PIC und STM32. (Bildquelle: Beningo Embedded Group)

Jeder im Stammverzeichnis aufgelistete Ordner ist ein High-Level-Ordner, der die allgemeinen Treiber und Support für diese Chip-Familie enthält. Die einzelnen Ordner können verschiedene unterstützte Entwicklungskarten oder Prozessoren enthalten. So unterstützt der Ordner „stmhal“ beispielsweise Entwicklungskarten wie das STM32F429 Discovery Board von STMicroelectronics und den STM32 IoT Discovery Node (STM32L) – neben weiteren wie das STM32F405 pyboard von Adafruit Industries. Der Ordner ESP8266 enthält Support für die Huzzah-Breakout-Karte für den ESP8266 und das Feather Huzzah Stack Board von Adafruit.

Die Entwicklungskarten, die MicroPython ausführen können, sind nicht teuer. Für den Entwickler bietet es sich daher an, mehrere zu kaufen, um herauszufinden, wie viel Arbeitsspeicher, Speicherplatz und Prozessorleistung sie für eine Anwendung benötigen. So könnte ein Entwickler beispielsweise mit dem STM32F405 pyboard anfangen und zugunsten zukunftssicherer Funktionen und Upgrades dann beschließen, im endgültigen Produkt auf den STM32F429 umzusteigen. Die STM32F429 hat 2 MB Flash-Speicher, 256 KB RAM und einen speziellen 0-wait-state-RAM mit der Bezeichnung CCM.

Der MicroPython-Anwendungscode, den ein Entwickler schreibt, muss nicht zwangsläufig im internen Flash-Speicher des Mikrocontrollers gespeichert werden. Der MicroPython-Kernel muss sich auf dem Mikrocontroller befinden, aber der Anwendungscode kann auf einem externen Speichermedium wie der 8GB-microSD-Karte von Panasonic gespeichert sein. Die Nutzung eines externen Speichergerätes für die Speicherung des Anwendungscodes bietet die Möglichkeit, einen Mikrocontroller mit weniger Speicher zu verwenden und so möglicherweise Gesamtsystemkosten zu sparen.

Schnell loslegen – mit MicroPython

MicroPython wird auf dem Adafruit STM32F405 pyboard vorinstalliert. Bei jedem anderen Entwicklungskit oder jeder anderen proprietären Hardware muss der Entwickler den MicroPython-Code jedoch herunterladen, den Code für die Zielkarte erstellen und den Mikrocontroller dann mit der Software flashen. Der Zugriff auf den MicroPython-Quellcode ist unproblematisch, weil alles auf GitHub gehostet wird. Der Entwickler muss einige Schritte ausführen, um die Toolkette einzurichten und die Umgebung so zu konfigurieren, dass ein Build von MicroPython erstellt wird. In diesem Beispiel werden wir MicroPython für die STM32F429 Discovery-Karte erstellen.

Dazu muss der Entwickler zunächst entweder eine virtuelle Maschine auf Linux-Basis erzeugen oder mit einer nativen Linux-Installation arbeiten. Sobald Linux über ein Terminal verfügbar ist, kann der Entwickler mit folgendem Befehl die ARM-Compiler-Toolkette installieren:

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

 

Wenn die Linux-Installation frisch ist, kann es sein, dass das System zur Versionssteuerung (git) nicht installiert ist. Git lässt sich über folgenden Befehl vom Terminal installieren:

 

sudo apt-get install git

 

Wenn git installiert ist, kann die MicroPython-Quelle durch Ausführung des folgenden Befehls im Terminal aus dem Repository ausgecheckt werden:

 

git clone https://github.com/micropython/micropython.git

Die Ausführung des Vorgangs kann einige Minuten dauern, aber der Entwickler sollte eine Demonstration des Ablaufs sehen (Abbildung 2).

Bild vom Klonen des MicroPython-Repository auf ein lokales Dateisystem

Abbildung 2: Klonen des MicroPython-Repository auf das lokale Dateisystem, auf dem der Entwickler dann MicroPython für die Zielkarte erstellen oder den Kernel für eigene Anwendungen anpassen kann. (Bildquelle: Beningo Embedded Group)

Sobald die MicroPython-Quelle auf das lokale Dateisystem geklont wurde, sollte in das Verzeichnis gewechselt werden. Dann muss das Terminal einen „cd stmhal“ durchführen. Das stmhal-Verzeichnis enthält das Makefile für MicroPython für die STM32-Mikrocontroller. Darüber hinaus gibt es einen Ordner „boards“, in dem der Entwickler sehen kann, welche STM32-Karten gegenwärtig unterstützt werden. Über das Terminal kann der Entwickler dann einen Build für jede Karte erstellen, die im Ordner „boards“ aufgeführt ist. Durch Eingabe des folgenden Befehls lässt sich beispielsweise der Code für die STM32F4 Discovery-Karte erzeugen:

make BOARD=STM32F4DISC

Das Erzeugen des MicroPython-Builds dauert einige Minuten. Währenddessen kann der Entwickler das DFU-Tool (Device Firmware Update) installieren, mit dem MicroPython über USB auf einen Mikrocontroller programmiert wird. Das Tool muss nur einmal installiert werden. Dazu wird in ein Terminal der folgende Befehl eingegeben:

sudo apt-get install dfu-util

Wenn der MicroPython-Build fertig ist und die dfu-util installiert wurde, kann der Entwickler MicroPython auf seinen Mikrocontroller laden. Dazu muss der Entwickler seinen Mikrocontroller zunächst in den DFU-Bootloader-Modus bringen. Das kann erfolgen, indem die Boot-Pins für das Laden des internen Bootloaders auf Reset gesetzt werden, statt den Code per Flash auszuführen.

Befindet sich der Mikrocontroller im Bootloader-Modus und ist per USB an den Hostcomputer angeschlossen, kann mit dfu-util und folgendem Befehl MicroPython heruntergeladen werden:

dfu-util -a 0 -d 0483:df11 -D build-STM32F4DISC/firmware.dfu

Die dfu-util nutzt die dfu-Datei, die im Zuge der Kompilierung ausgegeben wird. Der Vorgang dauert mehrere Minuten, weil der Mikrocontroller komplett gelöscht und dann neu programmiert wird. Der Vorgang ähnelt weitgehend dem in Abbildung 3 gezeigten Prozess. Wenn das Tool fertig ist, müssen die Boot-Jumper so eingestellt werden, dass vom internen Flash-Speicher geladen wird. Dann kann der Mikrocontroller aus- und wieder eingeschaltet werden. Jetzt läuft MicroPython auf dem Ziel-Mikrocontroller.

Abbildung vom Laden von MicroPython auf einen Mikrocontroller mittels dfu-util

Abbildung 3: MicroPython wird mittels dfu-util auf einen Mikrocontroller geladen. (Bildquelle: Beningo Embedded Group)

Erzeugung von Schnittstellen zwischen Sensoren und angeschlossenen Geräten

Der größte Vorteil bei der Verwendung einer höheren Programmiersprache wie MicroPython für die Entwicklung eingebetteter Echtzeitsoftware ist der, dass die Software nicht von der zugrunde liegenden Hardware abhängt. Das heißt, dass ein Entwickler ein MicroPython-Skript zur Ausführung auf einem pyboard schreiben und es mit geringen oder gar keinen Modifikationen auch auf der ESP8266- oder der STM32F4 Discovery-Karte ausführen kann. Schauen wir uns an, wie ein MicroPython-Basisskript aussehen könnte, das eine Schnittstelle zwischen dem Luftdruck- und Temperatursensor Bosch Sensortec BMP280 und einem I2C-Bus herstellt und die Daten dann per serieller Bluetooth-Schnittstelle unter Verwendung des Bluetooth-Moduls RN-42 von Microchip Technology überträgt.

Der BMP280 ist ein Absolut-Luftdruck- und Temperatursensor auf Basis von I2C mit der standardmäßigen I2C-Slave-Adresse von Dezimal 119. Der einfachste Weg, die Schnittstelle zum pyboard herzustellen, ist die Verwendung der Gravity-Karte von DFRobot. Sie bietet einen robusten Anschluss für die einfache Spannungsversorgung des Geräts und den Zugriff auf I2C. Der Entwickler kann für den Anschluss an die Gravity-Karte den I2C1- oder den I2C2-Bus wählen. Sobald die Karten verbunden sind, ist das MicroPython-Skript unproblematisch.

Zunächst importiert der Entwickler die I2C-Klasse aus der pyb-Bibliothek. Die pyb-Bibliothek bietet Zugang zu den Peripheriegeräte-Funktionen des Mikrocontrollers wie SPI, I2C und UART. Bevor ein Peripheriegerät verwendet werden kann, muss der Entwickler die Peripheriegeräteklasse instanziieren, um ein Objekt zu erzeugen, das für die Steuerung des Peripheriegeräts verwendet werden kann. Sobald die Peripheriegeräteklasse initialisiert wurde, kann der Entwickler alle anderen Initialisierungen vornehmen (z. B. vor Aufruf der primären Anwendungsschleife überprüfen, ob Geräte vorhanden sind). Der primäre Anwendungscode fragt dann einmal pro Sekunde den Sensor ab. Ein Beispiel dafür, wie das realisiert werden kann, zeigt folgendes Skript (Code-Listing 1).

Copy
from pyb import I2C
 
GlobalTemp = 0.0
GlobalBarometer = 0.0
 
# Initialize and Instantiate I2C peripheral 2
I2C2 = I2C(2,I2C.MASTER, baudrate=100000)
 
while True:
            SensorSample()
            pyb.delay(1000)
 
def SensorSample():
            #Read the Temperature Data
            TempSample = I2C2.readfrom_mem(119, 0xFA,3)
 
            #Read the Pressure Data
            PressureSample = I2C2.readfrom_mem(119, 0xF7,3)

Code-Listing 1: MicroPython-Skript, das das I2C-Peripheriegerät initialisiert und mit der Gravity-Karte von DFRobot kommuniziert, um Temperatur- und Luftdruck-Sensordaten abzufragen. (Codequelle: Beningo Embedded Group)

Das Abfragen von Sensordaten, ohne etwas mit ihnen anzufangen, ist keine besonders überzeugende Demonstration des Potenzials von MicroPython für Entwicklungsteams. Viele Entwicklungsteams stehen vor technischen Herausforderungen, die die Anbindung ihrer Sensorgeräte an das Internet oder einen lokalen Sensor-Hub über Bluetooth mit sich bringt.

Mit dem RN-42-Modul lässt sich ein Projekt auf einfache Art um Bluetooth-Funktionen ergänzen. Das RN-42 kann in einen Modus gesetzt werden, in dem der Mikrocontroller einfach die UART-Daten sendet, die über Bluetooth übertragen werden sollen. Das RN-42 wickelt dann die gesamte Bluetooth-Verarbeitung ab (Abbildung 4).

Abbildung vom pyboard, auf dem MicroPython ausgeführt wird, vernetzt mit einem RN-42-Bluetooth-Modul über UART

Abbildung 4: Vernetzung des pyboard, auf dem MicroPython ausgeführt wird, mit einem RN-42-Bluetooth-Modul über UART. (Bildquelle: Beningo Embedded Group)

Sobald die Bluetooth-Karte angebunden ist, kann der Entwickler ein sehr einfaches Skript schreiben, das die empfangenen Sensordaten über Bluetooth an ein mobiles Gerät überträgt, das diese Daten dann zur weiteren Analyse speichern oder an die Cloud weiterleiten kann. Das Beispielsskript ist unter Code-Listing 2 aufgeführt. In diesem Beispiel ist UART1 für 115200 Bit/s, 8-Bit-Übertragung, keine Parität und ein Stopp-Bit konfiguriert.

Copy
from pyb import uart
from pyb import I2C
 
GlobalTemp = 0.0
GlobalBarometer = 0.0
 
# Initialize and Instantiate I2C peripheral 2
I2C2 = I2C(2,I2C.MASTER, baudrate=100000)
 
# Configure Uart1 for communication
Uart1 = pyb.UART(1,115200)
Uart1.init(115200, bits=8, parity=None, stop=1)
 
while True:
            SampleSensor()
            pyb.delay(1000)
 
def SensorSample():
            #Read the Temperature Data
            TempSample = I2C2.readfrom_mem(119, 0xFA,3)
 
            #Read the Pressure Data
            PressureSample = I2C2.readfrom_mem(119, 0xF7,3)
 
            #Convert Sample data to string
            data = “#,temperature=”str(TempSample)+”,pressure”+str(PressureSample)+”,#,\n\r”
 
            #Write the data to Bluetooth
            Uart1.write(data)

Code-Listing 2: Beispiel-MicroPython-Skript initialisiert UART1 und kommuniziert mit dem externen Gerät. (Codequelle: Beningo Embedded Group)

Der Python-Anwendungscode ist nicht nur einfach auf andere Plattformen portierbar, sondern die Anwendung nutzt zudem gemeinsame Bibliotheken und Funktionen, die bereits implementiert sind. Das hilft Entwicklern bei der Beschleunigung ihrer Entwicklungsarbeit. Die obige Anwendung lässt sich in maximal einer Stunde erstellen. Müsste ein Entwickler auf den untersten Softwareebenen starten und sich nach oben durcharbeiten, würde ihn das wahrscheinlich eine Woche kosten.

Tipps und Tricks für die Entwicklung von Echtzeit-Software

Die Entwicklung eingebetteter Anwendungen mit MicroPython ist einfach. Vom System Echtzeit-Performance zu bekommen, ist hingegen nicht ganz so simpel, wie manch einer denken mag. Während MicroPython große Vorteile bezüglich der Vereinfachung und Wiederverwendung von Code bietet, kann es eine Herausforderung sein, vom System ein vorhersagbares und einheitliches Timing zu bekommen, wenn der Entwickler einige interessante Fakten und Bibliotheken nicht kennt.

So umfasst MicroPython beispielsweise einen Garbage Collector, der im Hintergrund läuft und den Heap sowie weitere Speicherressourcen verwaltet. Der Garbage Collector ist nicht deterministisch. Entwickler, die ein deterministisches Verhalten erwarten, können in Schwierigkeiten kommen, wenn die Ausführung des Garbage Collector in einem zeitkritischen Abschnitt gestartet wird. Entwickler müssen bestimmte Empfehlungen befolgen, um sicherzustellen, dass dieser Fall nicht eintritt.

Erstens können sie die Garbage Collection-Bibliothek (gc) importieren und mit der enable- und disable-Methode steuern, wann der Garbage Collector aktiviert bzw. deaktiviert wird. Die Garbage Collection lässt sich vor kritischen Abschnitten deaktivieren und danach wieder aktivieren (siehe dazu Code-Listing 3).

Copy
import gc
 
gc.disable()
 
#My time critical code
 
gc.enable()

Code-Listing 3: Deaktivierung des MicroPython Garbage Collector vor einem zeitkritischen Code-Abschnitt. (Codequelle: Beningo Embedded Group)

Zweitens kann der Entwickler den Garbage Collection-Prozess von Hand steuern. Wenn er Objekte erzeugt und verwirft, belegen sie Speicher im Heap. Der Garbage Collector läuft und gibt ungenutzten Speicher frei. Weil das in unregelmäßigen Abständen erfolgt, können Entwickler mit der collect-Methode den Garbage Collection-Prozess in periodischen Abständen laufen lassen und so sicherstellen, dass der Heap nicht mit gelöschten Inhalten überfüllt wird. Wenn das erfolgt ist, können die einzelnen Garbage Collection-Vorgänge von 10 Millisekunden bis zu weniger als einer Millisekunde pro Vorgang dauern. Der manuelle Aufruf der Garbage Collection garantiert auch, dass die Anwendung des Entwicklers Kontrolle über den nicht deterministisch getimten Code hat. Dadurch kann er entscheiden, wann die Garbage Collection erfolgen soll, und so sicherstellen, dass seine Anwendung Echtzeit-Performance hat.

Es gibt verschiedene weitere Praktiken, die Entwickler befolgen können, die Echtzeitcode schreiben wollen. Zu diesen zählen:

  • Nutzung vorher zugewiesener Speicher für Kommunikationskanäle
  • Verwendung der readinto-Methode bei Nutzung von Kommunikationsperipheriegeräten
  • Vermeidung traditioneller Python-Dokumentation mittels ###
  • Minimierung der Objekterstellung und -verwerfung während der Laufzeit
  • Überwachung der Ausführungszeiten der Anwendung

Entwickler, die mehr über „Best Practices“ wissen wollen, können sich hier die Dokumentation zur Optimierung von MicroPython anschauen.

Fazit

MicroPython ist eine interessante Plattform für Entwickler, die eingebettete Echtzeitanwendungen implementieren wollen, die keine Abhängigkeit von der zugrunde liegenden Mikrocontroller-Hardware aufweisen. Entwickler können ihre High-Level-Python-Skripts mithilfe der Standardbibliotheken von MicroPython schreiben und diese auf jedem unterstützten Mikrocontroller ausführen. Das bietet Entwicklern zahlreiche Vorteile wie:

  • bessere Wiederverwendbarkeit von Anwendungen
  • schnellere Markteinführung
  • Entkopplung der Anwendung von der Hardware

MicroPython eignet sich nicht für jede Anwendung perfekt, wurde aber bereits erfolgreich in Anwendungen für Industrie- und Raumfahrtsysteme und zudem für das Rapid Prototyping und Machbarkeitsstudien eingesetzt. 

Haftungsausschluss: Die Meinungen, Überzeugungen und Standpunkte der verschiedenen Autoren und/oder Forumsteilnehmer dieser Website spiegeln nicht notwendigerweise die Meinungen, Überzeugungen und Standpunkte der Digi-Key Electronics oder offiziellen Politik der Digi-Key Electronics wider.

Über den Autor

Jacob Beningo

Jacob Beningo ist ein Berater für eingebettete Software, der derzeit mit Kunden in mehr als einem Dutzend Ländern zusammenarbeitet, um ihr Unternehmen durch die Verbesserung von Produktqualität, Kosten und Markteinführungszeit dramatisch zu transformieren. Er hat mehr als 200 Artikel über Entwicklungstechniken für eingebettete Software veröffentlicht, ist ein gefragter Redner und technischer Trainer und verfügt über drei Abschlüsse, darunter einen Masters of Engineering der University of Michigan. Bei Interesse können Sie ihn unter jacob@beningo.com kontaktieren oder besuchen Sie seine Website www.beningo.com und melden Sie sich für seinen monatlichen Embedded Bytes Newsletter an.

Über den Verlag

Nordamerikanische Fachredakteure von Digi-Key