Abstraktionsebenen in der MikrocontrollerprogrammierungZur Verteidigung der Hardware Abstraction Layer (HAL)
Von
Herbert Paulis (FH Campus Wien), Eveline Prochaska (TU Dresden) und Julia Teissl (FH Campus Wien)
11 min Lesedauer
Die Verwendung von Abstraktionsebenen setzt sich auch im Bereich der Programmierung von Mikrocontrollern immer stärker durch. Doch der Einsatz der Hardware Abstraction Layer hat auch Kritiker. Welche Vorteile kann deren Einsatz sowohl in der Lehre als auch bei der praktischen industriellen Verwendung bieten? Dieser Beitrag beleuchtet das Thema am Beispiel der Microcontrollerserie STM32.
Abstraktionsschichten (Symbolbild)
(Bild: KI-generiert / DALL-E)
Noch bis vor wenigen Jahren lagen die Programmspeicher von Mikrocontrollern im ein- bis maximal zweistelligen KByte-Bereich und die Datenspeicher im Bereich von einigen wenigen hundert Bytes. Moderne Mikrocontroller aus der STM32-Serie können heute Werte von 512KByte Programm- und 265 KByte Datenspeicher (STM32F4-Serie) bis zu 2GByte Programm- und 512 KByte Datenspeicher (STM32F7-Serie) vorweisen. Damit einher gehen auch die höheren CPU-Taktfrequenzen (STM32H725/735: 550 MHz) und die Vielzahl der Peripheriekomponenten, die die schier unübersichtliche Vielfalt der Einsatzmöglichkeiten moderner Mikrocontroller maßgeblich mitbestimmen.
Diese Steigerung der Leistungsfähigkeit führt logischerweise auch dazu, dass die Aufgaben, die Mikrocontrollern übertragen werden, immer umfangreicher und komplexer werden. Daher ist es unumgänglich, dass verstärkt moderne Softwarestrukturen und höhere Programmiersprachen zum Einsatz kommen und es trotz prinzipiell hardwarenaher Programmierung zu einer (bis zu einem gewissen Grad) Entfernung von der Hardware kommt. Dies ist unumgänglich, da für komplexere Problemstellungen ein höherer Grad an Abstraktion erforderlich wird. Nichtsdestotrotz ist der Mikrocontroller durch seine Peripherie unmittelbar an die ihn umgebenden Hardware angebunden.
Abstraktionsschichten
Eine Lösung dieses scheinbaren Dilemmas ist das Einziehen von zusätzlichen Schichten, die zwischen der Programmlogik und der eigentlichen Hardware liegen. Diese werden in der Regel als Abstraktionsschichten bezeichnet, auf Englisch auch Hardware Abstraction Layer oder kurz HAL. Ein weiterer Vorteil eines solchen Layers, der sich dem Lernenden oder dem Hobbyanwender oftmals nicht voll erschließt, liegt in der wesentlich leichteren Wiederverwendbarkeit solcher Codestrukturen, eine Eigenschaft, die in der Industrie ein absolutes Muss ist. Wenn Code für eine andere Anwendung wiederverwendet werden soll, der aber eine andere Hardware zugrunde liegt, kann durch einfaches Austauschen der dazwischenliegenden Abstraktionsschichten eine leichtere Anpassung an eine geänderte Hardware erfolgen, als wenn Hardwareeigenschaften tief in funktionalen Strukturen verankert sind.
Sowohl in der Literatur als auch in verschiedenen Foren wird allerdings immer wieder Kritik am HAL-System von ST für den STM32 geübt. Die Argumente erstrecken sich in den geäußerten Kritiken vor allem auf Bereiche wie fehlerhaft, unübersichtlich, ineffizient, erschwertes Verständnis, nicht wirklich abstrahierend und HW-unabhängig oder auch versteckte Bindung an einen Hersteller. Diese Punkte sollen nun hier in der Folge diskutiert werden.
Zuvor wird es aber notwendig sein, einige grundlegende Punkte zu betrachten. Viele Einsteiger in die Mikrocontroller-Programmierung nutzen die Arduino Plattform. Diese ist für viele einfache Anwendungen vollkommen ausreichend, aber in den meisten Varianten eine 8-Bit-Mikrocontroller-Familie mit überschaubarem Befehlssatz ohne komplexe FDE-Architektur basierend auf der Atmel-AVR-Familie mit 16 MHz Taktfrequenz und (bis auf wenige Ausnahmen) mit 16 bis 256 KByte Flash und 2 bis 32 KByte RAM. Außerdem gibt es ein Programmiersystem, die ARDUINO-IDE, basierend auf C++ und einer Vielzahl von Klassenbibliotheken, die die eigentliche Low-Level-Hardware vollkommen von Anwender abschotten. Peripheriekomponenten wie z.B. I²C und ähnliches existieren zwar, sind aber ebenfalls abgekoppelt über Bibliotheken in die IDE eingebettet. Genau genommen ist diese Abschottung auch nichts anderes als ein Abstraktionslayer, der nur nicht als ein solcher bezeichnet wird.
Im Vergleich dazu sind die Mikrocontroller der STM32-Familie, die auf der ARM Cortex-M-Serie beruhen, hoch funktonale komplexe High End Mikrocontroller, die wesentlich umfangreichere Peripheriekomponenten in großer Zahl bieten. Dementsprechend komplex gestalten sich auch Anwendungen, in denen STM32 zur Verwendung kommen, was auch zum Teil gänzlich andere Anforderungen an die zum Einsatz gebrachte Software stellt. Im professionellen industriellen Einsatz reicht es nicht, dass etwas bloß „funktioniert“, Absicherungen gegen Fehler aller möglichen Art sind ebenso Standard wie Flexibilität, Portabilität, Refactoring (und daraus folgend Modularisierung in so vielen Bereichen der SW, wie nur möglich) und Wiederverwendbarkeit des verwendeten Codes. Auf diesen Punkt wird später noch zurückzukommen sein.
Herstellerabhängigkeit
Betrachtet man jetzt die vielen „Anschuldigungen“ der HAL-Gegner, so soll zuerst das Argument der versteckten Bindung an einen Hersteller untersucht werden. Genau genommen gibt es bei Mikrocontrollern keine echte Herstellerunabhängigkeit, nimmt man die Struktur und den inneren Aufbau eines Controllers und seiner Peripherie als Grundlage. Selbst die verschiedenen ARM-basierten Prozessoren unterscheiden sich in ihrer Peripherie deutlich, was auf der Registerebene definitiv zu nicht wirklich kompatiblen Methoden führt, wenn man etwa einen GPIO oder einen Timer manuell konfigurieren und betreiben möchte. Als Beispiel sei hier nur angeführt, wie ein GPIO-Pin gesetzt oder rückgesetzt wird, einmal bei einem STM32F407 und einmal bei einem Infineon XMC1000, beides ARM-basierte Mikrocontroller. Beim Infineon lauten die entsprechenden Codefolgen:
Stand: 08.12.2025
Es ist für uns eine Selbstverständlichkeit, dass wir verantwortungsvoll mit Ihren personenbezogenen Daten umgehen. Sofern wir personenbezogene Daten von Ihnen erheben, verarbeiten wir diese unter Beachtung der geltenden Datenschutzvorschriften. Detaillierte Informationen finden Sie in unserer Datenschutzerklärung.
Einwilligung in die Verwendung von Daten zu Werbezwecken
Ich bin damit einverstanden, dass die Vogel Communications Group GmbH & Co. KG, Max-Planckstr. 7-9, 97082 Würzburg einschließlich aller mit ihr im Sinne der §§ 15 ff. AktG verbundenen Unternehmen (im weiteren: Vogel Communications Group) meine E-Mail-Adresse für die Zusendung von redaktionellen Newslettern nutzt. Auflistungen der jeweils zugehörigen Unternehmen können hier abgerufen werden.
Der Newsletterinhalt erstreckt sich dabei auf Produkte und Dienstleistungen aller zuvor genannten Unternehmen, darunter beispielsweise Fachzeitschriften und Fachbücher, Veranstaltungen und Messen sowie veranstaltungsbezogene Produkte und Dienstleistungen, Print- und Digital-Mediaangebote und Services wie weitere (redaktionelle) Newsletter, Gewinnspiele, Lead-Kampagnen, Marktforschung im Online- und Offline-Bereich, fachspezifische Webportale und E-Learning-Angebote. Wenn auch meine persönliche Telefonnummer erhoben wurde, darf diese für die Unterbreitung von Angeboten der vorgenannten Produkte und Dienstleistungen der vorgenannten Unternehmen und Marktforschung genutzt werden.
Meine Einwilligung umfasst zudem die Verarbeitung meiner E-Mail-Adresse und Telefonnummer für den Datenabgleich zu Marketingzwecken mit ausgewählten Werbepartnern wie z.B. LinkedIN, Google und Meta. Hierfür darf die Vogel Communications Group die genannten Daten gehasht an Werbepartner übermitteln, die diese Daten dann nutzen, um feststellen zu können, ob ich ebenfalls Mitglied auf den besagten Werbepartnerportalen bin. Die Vogel Communications Group nutzt diese Funktion zu Zwecken des Retargeting (Upselling, Crossselling und Kundenbindung), der Generierung von sog. Lookalike Audiences zur Neukundengewinnung und als Ausschlussgrundlage für laufende Werbekampagnen. Weitere Informationen kann ich dem Abschnitt „Datenabgleich zu Marketingzwecken“ in der Datenschutzerklärung entnehmen.
Falls ich im Internet auf Portalen der Vogel Communications Group einschließlich deren mit ihr im Sinne der §§ 15 ff. AktG verbundenen Unternehmen geschützte Inhalte abrufe, muss ich mich mit weiteren Daten für den Zugang zu diesen Inhalten registrieren. Im Gegenzug für diesen gebührenlosen Zugang zu redaktionellen Inhalten dürfen meine Daten im Sinne dieser Einwilligung für die hier genannten Zwecke verwendet werden. Dies gilt nicht für den Datenabgleich zu Marketingzwecken.
Recht auf Widerruf
Mir ist bewusst, dass ich diese Einwilligung jederzeit für die Zukunft widerrufen kann. Durch meinen Widerruf wird die Rechtmäßigkeit der aufgrund meiner Einwilligung bis zum Widerruf erfolgten Verarbeitung nicht berührt. Um meinen Widerruf zu erklären, kann ich als eine Möglichkeit das unter https://contact.vogel.de abrufbare Kontaktformular nutzen. Sofern ich einzelne von mir abonnierte Newsletter nicht mehr erhalten möchte, kann ich darüber hinaus auch den am Ende eines Newsletters eingebundenen Abmeldelink anklicken. Weitere Informationen zu meinem Widerrufsrecht und dessen Ausübung sowie zu den Folgen meines Widerrufs finde ich in der Datenschutzerklärung, Abschnitt Redaktionelle Newsletter.
// Set Port P0.0 in output register PORT0->OMR = PORT0_OMR_PS0_Msk; // Reset Port P0.0 in output register PORT0->OMR = PORT0_OMR_PR0_Msk;
Beim STM32 sieht das entsprechende Gegenstück folgendermaßen aus:
// Set Port A, Pin 0 GPIOA->BSRR = GPIO_PIN_0; // Reset Port A, Pin 0 GPIOA->BSRR = (uint32_t)GPIO_PIN_0 << 16U;
Während hier sogar noch gewisse strukturelle Ähnlichkeiten vorhanden sind, so sind bei beiden Systemen alleine schon die Anweisungen zur Konfiguration des jeweils angeführten Ports als GPIO-Output-Pin komplett unterschiedliche Folgen von vielen Anweisungen , auf deren volle Wiedergabe hier aus Platzgründen aber verzichtet wird.
Auf den Punkt gebracht kann man sagen, dass man, sobald man sich für die Prozessorlinie eines Herstellers entschieden hat, an diese aus einer Vielzahl unterschiedlicher Gründe mehr oder weniger gebunden ist, daher ist dieses Gegenargument eigentlich keines. Im Gegenteil, der HAL erlaubt es, problemlos zwischen verschiedenen STM32-Modellen zu wechseln, ohne dass die eigentlichen Funktionsaufrufe geändert werden müssen. Durch ein einfaches Auswechseln der HAL-Bibliotheken wird erreicht, dass auch bei Unterschieden auf der Registerebene der Code der Anwendung weiterverwendet werden kann. Damit ist aber auch ein vor allem für den industriellen Anwendungsbereich sehr wichtiger Punkt optimal abgedeckt, nämlich die erleichterte Wiederverwendbarkeit (Code Reuse) von Softwarekomponenten. Bezüglich der vielfältigen anderen Probleme, die bei Code Reuse natürlich auch noch auftreten können, unabhängig von der verwendeten HW-Plattform, sei hier aber nur auf die zahlreiche Literatur dazu verwiesen, z.B. dieser BloGByteeitrag zu Best Practices bei Wiederverwendung von Code, oder das Buch "Rapid Development: Taming Wild Software Schedules" von Steve McConnell.
Was die fehlenden Abstraktionsebenen betrifft, die ebenfalls als Argument gegen den HAL vorgebracht werden, so muss man sich die Frage stellen, welche Art von Abstraktion erwartet man sich vom einem Hardware Abstraction Layer eines Mikrocontrollers? Vergleicht man etwa die im obigen STM-Codebeispiel gezeigten Portbefehle mit dem zugehörigen HAL-Aufrufen
so ist nach Meinung der Autoren hier sehr wohl eine deutliche Abstraktion festzustellen, ganz abgesehen von der erhöhten Lesbarkeit („Puristen“ ersetzen auch noch gerne die definierte Konstante GPIO_PIN_0 durch ((uint16_t)0x0001)).
Es ist klar ersichtlich, welche Aktion gesetzt wird (Port A Pin 0 wird gesetzt bzw. rückgesetzt), aber die eigentliche Aktion, welches Register wie beschrieben wird, ist hier nicht mehr ersichtlich. Sie ist auch weder für das Verständnis der Funktion noch der gesetzten Aktion notwendig. Die Aktion und die betroffenen Komponenten sind klar und ohne Verständnisproblem oder Verwechslungsgefahr zu erkennen. Noch offensichtlicher wird das, wenn man untersucht, wie eine DMA-gesteuerte periodische Abfrage von ADC-Werten gestartet wird, nämlich mit dem einfachen Aufruf
Von der zugrundeliegenden Funktion wird hier nur ein Auszug gezeigt (die volle Funktion ist 126 Zeilen lang und ruft ihrerseits selbst wieder mehrere andere HAL-Funktionen auf):
… if (READ_BIT(hadc->Instance->CR1, ADC_CR1_JAUTO) != RESET) { ADC_STATE_CLR_SET(hadc->State, HAL_ADC_STATE_INJ_EOC, HAL_ADC_STATE_INJ_BUSY); } /* State machine update: Check if an injected conversion is ongoing */ if (HAL_IS_BIT_SET(hadc->State, HAL_ADC_STATE_INJ_BUSY)) { /* Reset ADC error code fields related to conversions on group regular */ CLEAR_BIT(hadc->ErrorCode, (HAL_ADC_ERROR_OVR | HAL_ADC_ERROR_DMA)); } else { /* Reset ADC all error code fields */ ADC_CLEAR_ERRORCODE(hadc); }...
Die Abstraktionsebene des HAL-Aufrufes ist offensichtlich und erleichtert es der Anwendung deutlich, die Übersicht zu bewahren und sich auf das eigentlich zu lösende Problem zu fokussieren. Damit ist wohl auch das Argument, dass der HAL unübersichtlich sei, mit entkräftet.
HAL in der Lehre
Ebenso ist es unklar, warum die Verwendung des HAL das Verständnis der Funktionen des Mikrocontrollers erschweren sollte; eine Kritik, die vor allem im deutschsprachigen Entwicklerraum weit verbreitet zu sein scheint. Für eine Anwendung ist es vollkommen ausreichend, zu verstehen, was es bedeutet, dass ein GPIO-Port gesetzt oder rückgesetzt wird und was das für logische und elektrische Folgen für mit ihm verbundene externe Hardware-Komponenten damit verbunden sind. Wie soll es für das Verständnis der Funktion noch förderlich sein, zu wissen, dass im BSRR („Bit Set and Reset Register“) eines GPIO-Ports die unteren 16 Bits dazu dienen, den zugehörigen Pin zu setzten und die oberen 16 Bits dazu, den jeweiligen Pin zurückzusetzen? Für Timer, DMA, ADC oder noch komplexere Funktionen, wie etwa einem USB-Port, können in einer Vielzahl von Registern die unterschiedlichsten Bits Funktionalitäten beeinflussen. Die Art und Weise, wie diese Bits strukturiert sind, ist oft verwirrend, da sie durch die innere Hardwarestruktur des Controllers bestimmt und nicht immer leicht verständlich ist. Es ist doch wesentlich logischer und klarer, wenn das korrekte Behandeln dieser Register in Funktionen mit klar verständlichen Bezeichnungen und Parametern mit sinnvollen Namen ausgelagert ist, wie sie der HAL bietet.
Die Autoren haben auch die Erfahrung gemacht, dass es Anfängern wesentlich leichter fällt, die ersten Schritte bei der STM32-Programmierung mit dem HAL zu unternehmen. Das Lernen und Verstehen der vielen Funktionen und Peripheriekomponenten wird dadurch deutlich erleichtert und erlaubt es den Studierenden, sich auf die eigentlichen Funktionen zu konzentrieren. Vor allem Programmieranfängern fällt der Umgang mit dem Mikrocontroller dadurch wesentlich leichter.
Etwas schwieriger wird es, wenn man die Vergangenheit mitbetrachtet, das Argument vieler HAL-Gegner zu entkräften, dass der HAL fehlerbehaftet sei. Die erste Version des HAL von ST stammt aus dem Jahr 2014. Es war zu Beginn eine deutliche Schwäche des HAL, dass die ersten Versionen oftmals in der Tat einige Fehler beinhaltet haben. Einerseits ist das bei einem so umfangreichen SW-Paket, wie es der HAL darstellt, natürlich nicht gänzlich zu vermeiden, andererseits war die Kritik vieler Anwender hier berechtigt. Auch war die Dokumentation am Anfang unklar und unübersichtlich und Änderungen wurden oftmals ohne Dokumentation und überraschend ausgeliefert, was ein intensives Studium der HAL-Sourcen notwendig machte*. Allerdings ist der HAL mittlerweile nahezu allen seinen Kinderkrankheiten entwachsen und stellt heute in der Version 7 eine stabile Basis mit vollständiger und ausgereifter Dokumentation dar, mit jeweils eigener Version für alle Mikrocontroller-Familien der STM32-Reihe.
Effizienzüberlegungen
Ein subtiler Punkt und Gegenstand vieler Diskussionen ist die Frage, wie es beim HAL um die Effizienz bestellt ist. Im Vergleich zu den vielfach vor allem im Hobby- und Heimanwenderbereich sowie teilweise auch im akademischen Lehrbetrieb Verwendung findenden 8-Bit-Controllern von AVR sind 32-Bit-Mikrocontroller für wesentlich höhere Verarbeitungsgeschwindigkeiten und Speichergrößen ausgelegt. Anstatt hier mit „raffinierten“ und undurchsichtigen Programmiertricks das letzte Quäntchen Leistung aus einer leistungsmäßig vielleicht gerade noch passenden MPU herauszuholen (und sich dabei jede Menge potenzielle Probleme bei zukünftigen Funktionserweiterungen einzuhandeln), ist es doch wesentlich vernünftiger, bei erkennbaren Problemen mit der Performance in der Entwicklungs- oder Weiterentwicklungsphase auf ein größeres STM32-Modell umzusteigen, ohne große Kosten und Aufwände zu verursachen. Das würde auch gleichzeitig Reserven für spätere Erweiterungen der Funktionalität einer Anwendung mit sich bringen. Spätere HW-Änderungen sind vor allem im industriellen und Automotive-Bereich nicht gerne gesehen, da sie in der Regel mit hohen Kosten für Dokumentation, Ausbildung und Wartung verbunden sind.
Außerdem lässt sich oft sehr viel zusätzliche Effizienz automatisiert einbringen, moderne Compiler verfügen über vielfältige Optimierungsmöglichkeiten. Da alle HAL-Module als Source Code in ein Projekt eingebunden und auch gemeinsam mit dem Projekt kompiliert werden, wird die eingestellte Compileroptimierung natürlich auch auf diese angewendet.
Ein wesentlicher und elementarer Punkt in Hinblick auf SW-Effizienz beginnt vielmehr schon im Design, nämlich bei der Auswahl der für eine Anwendung optimalen Algorithmen. Das sind Entscheidungen, die den Designern und späteren Programmieren ein Compiler weder abnehmen noch später einmal durch auch noch so schlaue Tricks verbessern kann. Man kann also getrost sagen, die Auswahl der richtigen Algorithmen ist jedenfalls „mission critical“ für den Erfolg einer Anwendung. Kommt man später im Projekt darauf, dass man die falschen Algorithmen ausgewählt hat, so ist eine solche Fehlentscheidung oftmals nur mit sehr großen Aufwänden korrigierbar, wenn überhaupt, da diese Algorithmen in der Regel tief im Code verankert sind. Auch hier muss aus Platzgründen auf weiterführende Literatur verwiesen werden (z.B. die Fachbücher Weniger schlecht programmieren von Katrin Passig und Johannes Jander (2013), oder Besser coden: Clean Code und Best Practices für professionelle Software-Projekte, von Uwe Post (2017)).
Ebenso sind gute Kenntnisse gewisser Programmiertechniken und die Art und Weise, wie bestimmte Strukturen einer Programmiersprache vom Compiler übersetzt werden (in C etwa switch-case-Konstrukte versus if-else if-Ketten) hier von Vorteil.
Abschließend kann man sagen, dass der HAL seinen Designzweck optimal erfüllt. Die Benutzung der STM32-Peripherie wird abstrahiert und die Verwendung dadurch vereinfacht. Wenn man für die Systemkonfiguration Tools wie die STM32CubeIDE oder STM32CubeMX verwendet, dann ist auch die Initialisierung der Peripherie einfach und ohne große Probleme zu bewerkstelligen. Unter entsprechender Anleitung ist damit auch für Einsteiger und Studierende eine deutliche Steigung der Lernkurve ersichtlich und diese können schnell zu ersten Erfolgen beim Verständnis der Funktionalitäten eines Mikrocontrollers kommen, aber auch ohne allzu großen Frust erste einfachere Projekte mit dem STM32 schnell realisieren. Profis und Experten erleichtert der HAL einen raschen Umstieg von einem STM32-Modell auf ein anderes sowie durch vereinfachtes Code Reuse eine effizientere Realisierung vieler komplexer Projekte. (sg)
* Die Autor:innen erinnern sich, als in einem HAL-Update überraschend und undokumentiert der Start des Independent Watchdogs IWDG aus einer eigenen Start-Funktion in die Initialisierungsroutine verlegt wurde, was zur Folge hatte, dass Projekte, die die Komponente verwendeten, plötzlich nicht mehr funktionierten (weil die berechneten Watchdog-Ablaufzeiten nun auf einmal falsch waren)