Heutige Embedded Softwareentwicklung beinhaltet Themen wie IoT, Multitouch-fähige Benutzeroberflächen, Updatefähigkeit und natürlich die traditionellen Gebiete, das Steuern und Überwachen von Systemen und ihren Prozessen.
Neben den oben genannten gibt es auch noch weitere Herausforderungen: die Produkte haben einen Lifecycle von bis zu 15 Jahren. Auch die dazugehörige Embedded Software muss in diesem Zeitraum wartbar sein und allenfalls weiterentwickelt werden können. Folglich muss auch die dazugehörige Entwicklungsumgebung über den ganzen Lifecycle zur Verfügung stehen.
Bis heute findet man häufig den Ansatz, die Entwicklungsumgebung in eine Virtuelle Maschine (VM) zu verpacken, da sich natürlich auch die für die Entwicklung benötigte Software und die Betriebssysteme weiterentwickeln. So kann es zum Beispiel sein, dass ein verwendeter Compiler oder eine IDE auf einem moderneren Betriebssystem ein anderes Verhalten zeigt oder gar nicht mehr unterstützt wird. Aber auch das Installieren und Konfigurieren einer Entwicklungsumgebung ist komplex und zeitaufwändig. Es sind Arbeiten, die jedes Teammitglied immer wieder durchführen muss.
In diesem Blogbeitrag möchte ich euch zeigen, diese Problemstellung mit VSCode DevContainer zu lösen. Nachstehend findet ihr ein Tutorial, in dem ich die verwendeten Tools kurz erkläre und die notwendigen Schritte zum Erstellen eines DevContainer zeige.
Die VSCode DevContainer Extension
Was sind VSCode DevContainer eigentlich? VSCode an sich ist sicherlich bestens bekannt. Es handelt sich um einen modernen, leichtgewichtigen Quelltext-Editor. Mit Hilfe von Erweiterungen (folgend als Extensions bezeichnet) erhält man die Möglichkeit VSCode zu einer vollwertigen IDE (Integrated Development Environment) auszubauen.
Die VSCode DevContainter Extension ist eine Erweiterung, die es einem auf einfache Weise ermöglicht, die Entwicklungsumgebung in einem Docker Container zu konfigurieren und auszuführen. Die generelle Architektur ist in der folgenden Abbildung dargestellt:
Das hat den Vorteil, dass alle Entwickler in einer vorkonfigurierten und konsistenten virtuellen Umgebung arbeiten können. Das beschleunigt den Onboarding Prozess und vereinfacht die Verwaltung und Wartung der Infrastruktur für Projekte mit unterschiedlichen Konfigurationen.
Verwendete Tools und Hardware
- Docker: Docker verwenden wir, um unseren DevContainer auszuführen.
- WSL: Das «Windows Subsystem for Linux», kurz WSL, wird Windows-Benutzern empfohlen. Es vereinfacht und unterstützt das Ausführen von Docker Container unter Windows.
- VSCode: Wird als Entwicklungsumgebung eingesetzt.
- VSCode Extension: Die Entwicklungsumgebung ergänzen wir mit folgenden Extensions:
- Remote Development Extension Pack: eine Sammlung von Microsoft mit nützlichen Extensions für Remote Development unter VSCode, beinhaltet die DevContainer Extension.
- C/C++ Extensions Pack: eine Sammlung von Extension die einem unterstützt bei der Entwicklung von C/C++ Applikationen.
- Cortex-Debug Extension: eine Erweiterung die Unterstützung beim On-Target-Debugging von ARM-basierten Microcontrollern.
- CMake und ninja-build: Buildsysteme die im Beispielprojekt eingesetzt werden um das Projekt zu kompilieren.
- Arm GNU Toolchain: wird für das Kompilieren und Linken verwendet und beinhaltet einen Debugger.
- Git: Revisionsmanagementsystem zur Versionierung und Verwaltung des Beispielprojekts.
- STLink GDB Server / STLink Programmer: Standardtools zum Programmieren und Debuggen von STM Hardware.
- STM32F3Discovery-Board: eingesetzte Hardware, die als Target für unsere Applikation im Beispielprojekt dient.
Installation und Konfiguration
Nun folgt die Installation der Tools, die Erläuterung der wichtigsten Konfigurationsschritte und natürlich die Inbetriebnahme aus Sicht eines Softwareentwicklers. Das verwendete Beispielprojekt kann man unter https://github.com/knsig/DevContainerEmbeddedTestApp einsehen.
Die Installation erfolgt in zwei Schritten. Im ersten Teil installieren wir die Tools, die auf jedem Host installiert und ausgeführt werden müssen. Sie bilden die Basis unseres Setups. Dabei handelt es sich um Docker, WSL, VSCode, STLink GDB Server und STLink Programmer.
Der zweite Teil der Installation beinhaltet das Definieren und Erzeugen des Docker-Container. In diesem Schritt wählen wir ein Docker-Image als Basis und installieren einige allgemeine Tools, die uns bei der Installation unterstützen, sowie die oben aufgeführten Tools: CMake, ninja-build, die Arm GNU Toochain und Git. Das muss nur einmal erstellt werden und kann anschliessend in einem Git-Repository abgelegt und beliebig geteilt werden.
Installation auf dem Arbeitsplatz
Da es im Internet bereits zahlreiche Anleitungen zur Installation von Docker, WSL, VSCode und VSCode Extensions gibt, erlaube ich mir, auf die offizielle Dokumentation der Hersteller zu verweisen:
Docker Engine: https://docs.docker.com/engine/install/
Installation WSL (für Windows-Benutzer): https://learn.microsoft.com/en-us/windows/wsl/install
Konfiguration WSL (für Windows-Benutzer): https://docs.docker.com/desktop/wsl/
VSCode: https://code.visualstudio.com/download
STLink GDB Server und STLink Programmer
Die beiden Programme verwende ich in meinem Setup, um das Discovery-Board zu programmieren und zu debuggen.
Der STLink GDB Server führen wir auf dem Host aus, damit wir eine Verbindung per USB zum Discovery-Board herstellen können. Weiter ist der Server die Gegenstelle für den GDB-Debugger der im Docker-Container für eine Debug-Session gestartet wird. Damit können wir nun im DevContainer mit GDB eine Debug-Session starten, die auf dem Discovery-Board ausgeführt wird.
Ich habe ein Windows System im Einsatz und die STM CubeIDE installiert. In diesem Fall findet man die beiden Anwendungen im Installationsverzeichnis der CubeIDE unter
c:\STM32CubeIDE_1.7.0\STM32CubeIDE\plugins\com.st.stm32cube.ide.mcu.externaltools.stlink-gdb-server.win32_2.0.0.202105311346\tools\bin
Wenn sich der Pfad in der PATH-Systemvariable befindet, dann startet man die Anwendung in einem Terminal wie folgt:
ST-LINK_gdbserver.exe -p 2000 -e -d -v -cp C:\ST\STM32CubeIDE_1.7.0\STM32CubeIDE\plugins\com.st.stm32cube.ide.mcu.externaltools.cubeprogrammer.win32_2.0.0.202105311346\tools\bin
Es können auch die Tools von Segger verwendet werden. Da Hardwaredebugging zum Alltag beim Entwickeln von Embedded Software gehört, gehe ich davon aus, dass ihr euch zurechtfindet und möchte nicht weiter auf die Installation eingehen.
Erstellen und Konfigurieren des DevContainers
Nun geht es weiter mit dem Einrichten der Entwicklungsumgebung an sich. Zuerst brauchen wir ein C/C++ Beispielprojekt. Ich habe es mir einfach gemacht und mithilfe der CubeIDE ein Beispielprojekt für das Discovery-Board generieren lassen. Anschliessend habe ich die nötigen Sourcefiles und Makefiles aus dem Eclipse-Workspace exportiert, in einen neuen Ordner abgelegt und den mit VSCode geöffnet.
Als nächstes müssen wir einige Verzeichnisse und Dateien erstellen:
- Verzeichnis «.devcontainer»: Standardverzeichnis für die Konfiguration der DevContainer Extension und das Dockerfile. In diesem Verzeichnis erstellen wir auch gleich die beiden Dateien «Dockerfile» und «devcontainer.json».
- Verzeichnis «.vscode»: Standardverzeichnis für die Konfiguration von VSCode Extensions. In diesem Verzeichnis erstellen wir die Datei «launch.json», die von der Extension «cortex-debug» verwendet wird um die Debug-Konfiguration zu definieren.
- Die Datei «settings.json»: Datei für Einstellungen der Extension «cortex-debug»
Schliesslich sollte die Struktur ähnlich wie in der folgenden Abbildung aussehen:
Dockerfile
Nun definieren wir das Docker-Image, von dem aus dann der Docker-Container erzeugt wird.
# this points to the lts version FROM ubuntu:latest # install basics ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update --fix-missing && apt-get install -y \ unzip \ libncurses5 \ build-essential \ wget \ make \ git \ cppcheck \ && rm -rf /var/lib/apt/lists/* # add the arm toolchain RUN wget https://developer.arm.com/-/media/Files/downloads/gnu-rm/10.3-2021.10/gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2 \ -O gcc-arm-none-eabi.tar.bz2 \ && mkdir /opt/gcc-arm-none-eabi-10.3-2021.10 \ && tar xjfv gcc-arm-none-eabi.tar.bz2 \ -C /opt/gcc-arm-none-eabi-10.3-2021.10 --strip-components 1 \ && rm gcc-arm-none-eabi.tar.bz2 \ && ln -s /opt/gcc-arm-none-eabi-10.3-2021.10/bin/* /usr/local/bin # add cmake ARG CMAKE_VERSION=3.27.7 RUN wget https://github.com/Kitware/CMake/releases/download/v3.27.7/cmake-3.27.7-linux-x86_64.sh \ -q -O /tmp/cmake-install.sh \ && chmod u+x /tmp/cmake-install.sh \ && mkdir /opt/cmake-3.27.7 \ && ./tmp/cmake-install.sh --skip-license --prefix=/opt/cmake-3.27.7 \ && rm /tmp/cmake-install.sh \ && ln -s /opt/cmake-3.27.7/bin/* /usr/local/bin # add ninja-build RUN wget https://github.com/ninja-build/ninja/releases/download/v1.11.1/ninja-linux.zip \ -O /tmp/ninja-build.zip \ && mkdir /opt/ninja-build-1.11.1 \ && unzip /tmp/ninja-build.zip -d /opt/ninja-build-1.11.1 \ && rm /tmp/ninja-build.zip \ && ln -s /opt/ninja-build-1.11.1/* /usr/local/bin # add a vscode user RUN useradd -m vscode && chsh -s /bin/bash vscode
Basierend auf einem aktuellen Ubuntu Image installieren wir zuerst einige Basisanwendungen wie unzip (Entpacken von komprimierten Daten), wget (Download von Inhalten aus dem Internet) und Git. Anschliessend wird die Arm GNU Toolchain heruntergeladen, entpackt, im Verzeichnis «/opt» abgelegt und Softlinks im Verzeichnis «/usr/local/bin» angelegt, um die Tools systemweit verfügbar zu machen. Die gleichen Schritte werden für «CMake» und «ninja-build» ausgeführt.
Zum Schluss wird der Benutzer «vscode» angelegt. Das ist wichtig für Linux-Benutzer, damit im Docker-Container mit der gleichen UID/GID gearbeitet werden kann, wie auf dem Host. Ist das nicht der Fall, kann der User auf dem Host möglicherweise die im Container erzeugten Files nicht löschen, weil der Container ohne Angabe eines Users mit Root-Rechten ausgeführt wird.
devcontainer.json
Fahren wir fort mit dem Inhalt von der Datei «devcontainer.json». Wie schon erwähnt beinhaltet diese Datei die Konfiguration für die DevContainer Extension.
{ "name": "EmbSwDevContainer", "build": { "dockerfile": "Dockerfile" }, "runArgs": [ "--hostname=devcontainer", "--network=host" ], // Set *default* container specific settings.json values on container create. // "settings": {}, // Add the IDs of extensions you want installed when the container is created. "customizations": { "vscode": { "extensions": [ "mhutchie.git-graph", "ms-vscode.cpptools-extension-pack", "spmeesseman.vscode-taskexplorer", "mcu-debug.debug-tracker-vscode", "marus25.cortex-debug", "ms-vscode.cmake-tools", "twxs.cmake" ] } }, // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [ 61234 ], // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "uname -a", // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode", "containerUser": "vscode", "updateRemoteUserUID": true }
Zu erwähnen ist, wenn unter «dockerfile» nur der Name angegeben wird, sucht die Extension im Verzeichnis «.devcontainer» danach. Es kann jedoch auch ein relativer Pfad angegeben werden, an dem das Dockerfile liegt.
Die Extensions, die im Container installiert und ausgeführt werden sollen, definieren wir unter «customizations». Wie ihr sicher bemerkt habt, habe ich in der Zwischenzeit noch ein paar mehr installiert, als nur die oben erwähnten.
Das Port-Forwarding benutzen wir für die Verbindung vom GDB-Debugger auf den STLink GDB Server.
launch.json
Die Einstellungen für die «cortex-debug» Extension, die uns beim Debuggen auf dem Discovery-Board unterstützt, werden in «launch.json» vorgenommen. Wichtige Einstellungen sind die Felder «type», «device», «executable», «loadFiles», «serverType» und «gdbTarget».
{ // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Debug Inside Docker - EmbeddedApplication", "type": "cortex-debug", "cwd": "${workspaceFolder}", "request": "launch", "device": "STM32F303VCT6", "svdFile": "${workspaceFolder}/STM32F303.svd", "executable": "${workspaceFolder}/build/EmbeddedApplication.elf", "loadFiles": [ "${workspaceFolder}/build/EmbeddedApplication.elf" ], "servertype": "external", "gdbTarget": "10.35.0.31:2000", "postLaunchCommands":[ "monitor reset" ], "runToEntryPoint": "main", "showDevDebugOutput": "raw", }, ] }
settings.json
Zuletzt müssen wir den Pfad auf den GDB-Debugger für die «cortex-debug» Extension setzten, damit diese den Debugger auch findet. In meinem Fall benutze ich auch eine CMake Extension von Microsoft. Für diese habe ich auch zwei Einstellungen vorgenommen.
{ "cmake.generator": "Ninja", "cmake.configureOnOpen": true, "cortex-debug.gdbPath": "/opt/gcc-arm-none-eabi-10.3-2021.10/bin/arm-none-eabi-gdb", "cortex-debug.registerUseNaturalFormat": false, "cortex-debug.variableUseNaturalFormat": true, }
Wenn ihr alle Dateien erstellt und eure Umgebung eingerichtet habt, vergesst nicht, alles in einem Git-Repository abzulegen und mit euren Arbeitskollegen zu teilen.
Arbeiten mit der DevContainer Entwicklungsumgebung
Geschafft! Wir haben alle notwendigen Installationen und Konfigurationen vorgenommen, um endlich unserer wirklichen Leidenschaft nachzugehen – dem Entwickeln von Embedded Software.
Ein neuer Projektmitarbeiter muss nun die Tools aus dem ersten Schritt bei sich installieren. Anschliessen kann er das Projekt-Repository klonen mit den Dateien und Konfigurationen aus dem zweiten Teil. Und nun ist er bereits bereit, den Container zu starten, um mit der Arbeit am Projekt zu beginnen.
Um den Container mit DevContainer zu starten, könnt ihr am einfachsten die Tastenkombination «CTRL + SHIFT + P» verwenden. Anschliessend könnt ihr oben in der Eingabe «DevContainer: Rebuild and Reopen in Container» eingeben und mit Enter bestätigen.
Der Container wird automatisch erzeugt und gestartet. All eure Befehle, die ihr eingebt, werden nun im Container ausgeführt. Ihr könnt wie gewohnt CMake ausführen, und eure Projekt Kompilieren und mit Git eueren Sourcecode verwalten.
Für den Start einer Debug-Session führt ihr zuerst auf eurem Host den STLink GDB Server aus. Den startet ihr wie weiter oben beschrieben. Wenn der STLink GDB Server gestartet ist und eine Verbindung zum Discovery-Board besteht, wechselt ihr zurück zu VSCode. In VSCode wechselt ihr in die «Run and Debug»-Ansicht und wählt oben die Debug-Session die wir in «launch.json» konfiguriert haben und klickt auf «Start Debugging».
Den Container stoppt ihr, indem ihr einfach VSCode schliesst oder wiederum die Tastenkombination «CTRL + SHIFT + P» benutzt und «DevContainer: Reopen Folder Locally» eingebt.
Fazit
Wir haben jetzt eine vollwertige Entwicklungsumgebung für Embedded Software, die virtuell in einem Docker-Container ausgeführt wird. Wenn man das Setup zum ersten Mal erstellt, gibt es sicherlich vieles neu zu Entdecken und es braucht ein wenig Gewohnheit, zwischen Host und Container zu unterscheiden. Die Konfiguration zum Debuggen mag auch ein wenig aufwendiger sein. Das Beispiel zeigt auch nur das absolute Minimum und kann beliebig mit euren favorisierten Extensions und Tools erweitert werden.
Abschliessend finde ich, dass das Arbeiten sehr angenehm ist. Das Setup können wir einfach zwischen Mitarbeitern teilen und die Wartung der Entwicklungsumgebung ist weniger aufwendig und um ein vielfaches übersichtlicher.