Schreiben von Hardware Abstraction Layers (HALs) in C
Jakob von Benin | 19. Mai 2023
Hardware-Abstraktionsschichten (HALs) sind eine wichtige Schicht für jede eingebettete Softwareanwendung. Ein HAL ermöglicht es einem Entwickler, die Hardwaredetails vom Anwendungscode zu abstrahieren oder zu entkoppeln. Durch die Entkopplung der Hardware entfällt die Abhängigkeit der Anwendung von der Hardware, was bedeutet, dass sie in einer perfekten Position ist, um außerhalb des Ziels, also auf dem Host, geschrieben und getestet zu werden. Entwickler können die Anwendung dann viel schneller simulieren, emulieren und testen, Fehler beseitigen, schneller auf den Markt kommen und die Gesamtentwicklungskosten senken. Lassen Sie uns untersuchen, wie eingebettete Entwickler in C geschriebene HALs entwerfen und verwenden können.
Relativ häufig findet man eingebettete Anwendungsmodule, die direkt auf die Hardware zugreifen. Dies erleichtert zwar das Schreiben der Anwendung, stellt aber auch eine schlechte Programmierpraxis dar, da die Anwendung eng an die Hardware gekoppelt wird. Sie denken vielleicht, dass das keine große Sache ist – denn wer muss schon eine Anwendung auf mehr als einem Hardwaresatz ausführen oder den Code portieren? In diesem Fall würde ich Sie an alle verweisen, die in letzter Zeit unter Chipmangel gelitten haben und nicht nur ihre Hardware neu entwerfen, sondern auch ihre gesamte Software neu schreiben mussten. Es gibt ein Prinzip, das viele in der objektorientierten Programmierung (OOP) als Abhängigkeitsinversionsprinzip kennen und das zur Lösung dieses Problems beitragen kann.
Das Abhängigkeitsinversionsprinzip besagt, dass „High-Level-Module nicht von Low-Level-Modulen abhängen sollten, sondern beide von Abstraktionen abhängen sollten.“ Das Prinzip der Abhängigkeitsinversion wird in Programmiersprachen häufig mithilfe von Schnittstellen oder abstrakten Klassen implementiert. Wenn ich beispielsweise eine digitale Ein-/Ausgabeschnittstelle (Dio) in C++ schreiben würde, die eine Lese- und Schreibfunktion unterstützt, könnte sie etwa so aussehen:
Klasse dio_base {
öffentlich:
virtual ~dio_base() = default;
// Klassenmethoden
virtual void write(dioPort_t port, dioPin_t pin, dioState_t state) = 0;
virtual dioState_t read(dioPort_t port, dioPin_t pin) = 0;
}
Diejenigen unter Ihnen, die mit C++ vertraut sind, können sehen, dass wir virtuelle Funktionen verwenden, um die Schnittstelle zu definieren, was erfordert, dass wir eine abgeleitete Klasse bereitstellen, die die Details implementiert. Mit dieser Art abstrakter Klasse können wir dynamischen Polymorphismus in unserer Anwendung verwenden.
Aus dem Code ist schwer zu erkennen, wie die Abhängigkeit umgekehrt wurde. Schauen wir uns stattdessen ein kurzes UML-Diagramm an. Im Diagramm unten ist ein led_io-Modul durch Abhängigkeitsinjektion von einer Dio-Schnittstelle abhängig. Wenn das led_io-Objekt erstellt wird, wird ihm ein Zeiger auf die Implementierung für digitale Ein-/Ausgänge bereitgestellt. Die Implementierung für jeden Mikrocontroller dio muss auch der dio-Schnittstelle entsprechen, die durch dio_base definiert ist.
Wenn Sie sich das UML-Klassendiagramm oben ansehen, denken Sie vielleicht, dass dies zwar großartig für das Entwerfen einer Anwendung in einer OOP-Sprache wie C++ ist, dies jedoch nicht für C gilt. Allerdings können Sie diese Art von Verhalten tatsächlich in C erzielen kehrt die Abhängigkeiten um. Es gibt einen einfachen Trick, der in C mithilfe von Strukturen angewendet werden kann.
Entwerfen Sie zunächst die Schnittstelle. Sie können dies tun, indem Sie einfach die Funktionssignaturen schreiben, die Ihrer Meinung nach von der Schnittstelle unterstützt werden sollten. Wenn Sie beispielsweise entschieden haben, dass die Schnittstelle das Initialisieren, Schreiben und Lesen des digitalen Ein-/Ausgangs unterstützen soll, könnten Sie die Funktionen einfach so auflisten:
void write(dioPort_t const port, dioPin_t const pin, dioState_t const state);
dioState_t read(dioPort_t const port, dioPin_t const pin);
Beachten Sie, dass dies den Funktionen, die ich zuvor in meiner abstrakten C++-Klasse definiert habe, sehr ähnlich sieht, nur ohne das Schlüsselwort „virtual“ und die reine abstrakte Klassendefinition (= 0).
Als nächstes kann ich diese Funktionen in eine Typedef-Struktur packen. Die Struktur verhält sich wie ein benutzerdefinierter Typ, der die gesamte Dio-Schnittstelle enthält. Der anfängliche Code sieht in etwa wie folgt aus:
typedef struct {
void init (DioConfig_t const * const Config);
void write (dioPort_t const port, dioPin_t const pin, dioState_t const state);
dioState_t read (dioPort_t const port, dioPin_t const pin);
} god_base;
Das Problem mit dem obigen Code besteht darin, dass er nicht kompiliert werden kann. Sie können in C keine Funktion in eine Struktur einbinden. Sie können jedoch einen Funktionszeiger einbinden! Der letzte Schritt besteht darin, die Dio-HAL-Funktionen in der Struktur in Funktionszeiger umzuwandeln. Die Funktion kann konvertiert werden, indem ein * vor den Funktionsnamen gesetzt und dann () darum herum geschrieben wird. Die Struktur sieht jetzt beispielsweise wie folgt aus:
typedef struct {
void (*init) (DioConfig_t const * const Config);
void (*write) (dioPort_t const port, dioPin_t const pin, dioState_t const state);
dioState_t (*read) (dioPort_t const port, dioPin_t const pin);
} god_base;
Nehmen wir nun an, dass Sie den Dio HAL in einem led_io-Modul verwenden möchten. Sie könnten eine LED-Init-Funktion schreiben, die einen Zeiger auf den Typ dio_base akzeptiert. Auf diese Weise fügen Sie die Abhängigkeit ein und entfernen die Abhängigkeit von der Low-Level-Hardware. Der C-Code für das LED-Init-Modul würde etwa wie folgt aussehen:
void led_init(dio_base * const dioPtr, dioPort_t const portInit, dioPin_t const pinInit){
dio = dioPtr;
port = portInit;
pin = pinHeat;
}
Intern im LED-Modul kann ein Entwickler die HAL-Schnittstelle nutzen, ohne etwas über die Hardware zu wissen! Sie könnten beispielsweise in einer led_toggle-Funktion wie folgt in das Dio-Peripheriegerät schreiben:
void led_toggle(void){
bool state = (dio->read(port, pin) == dio->HIGH) ? dio->LOW : dio->HIGH);
dio->write(port, pin, state};
}
Der LED-Code wäre vollständig portierbar, wiederverwendbar und von der Hardware abstrahiert. Keine wirklichen Abhängigkeiten von der Hardware – nur von der Schnittstelle. An dieser Stelle benötigen Sie noch eine Implementierung für die Hardware, die auch die Schnittstelle implementiert, damit der LED-Code nutzbar ist. Um dies zu erreichen, würden Sie ein Dio-Modul mit Funktionen implementieren, die der Schnittstellensignatur entsprechen. Anschließend würden Sie diese Funktionen mithilfe von C-Code wie dem folgenden der Schnittstelle zuweisen:
god_base god_hal = {
Dio_Init,
Dio_Write,
Dio_Read
}
Das LED-Modul würde dann wie folgt initialisiert werden:
led_init(dio_hal, PORTA, PIN15);
Das ist es! Wenn Sie diesem Prozess folgen, können Sie Ihren Anwendungscode durch eine Reihe von Hardware-Abstraktionsschichten von der Hardware entkoppeln!
Hardware-Abstraktionsschichten sind eine entscheidende Komponente, die jeder Entwickler eingebetteter Software nutzen muss, um die Kopplung an die Hardware zu minimieren. Wir haben eine einfache Technik zum Definieren einer Schnittstelle und deren Implementierung in C untersucht. Wie sich herausstellt, benötigen Sie keine OOP-Sprache wie C++, um die Vorteile von Schnittstellen und Abstraktionsschichten zu nutzen. C verfügt über genügend Fähigkeiten, um dies zu ermöglichen. Beachten Sie, dass diese Technik im Hinblick auf Leistung und Speicher ein wenig kostenintensiv ist. Sie verlieren höchstwahrscheinlich die Leistung eines Funktionsaufrufs und genügend Speicher, um die Funktionszeiger Ihrer Schnittstellen zu speichern. Am Ende des Tages ist dieser geringe Aufwand es wert!
Weitere Informationen zu Textformaten