What is a Hardware Abstraction Layer (HAL) and why do I need it? [German]
by Marco Wuelser
Die Aufgabe des HAL besteht darin, alle Zugriffe auf die Zielhardware (Target) zu abstrahieren. Dazu bietet der HAL nach Peripherie gruppierte Methoden (z.B. USART, GPIO, RTC, ...). Der Applikations-Code (z.B. der Active Part Handler) ruft die HAL Methoden auf. Für die HAL Methoden werden zwei Implementierungen zur Verfügung gestellt:
- Target HAL
- Mock HAL
Im Target HAL wird der eigentliche Hardware Zugriff implementiert. Meist beinhaltet dies das lesen und/oder schreiben eines Registers. Die Register einer Embedded Hardware sind auf bestimmte Adressen gemappt und können so wie ganz normale variablen geschrieben und gelesen werden. Ein solcher zugriff auf eine feste Speicheradresse würde aber auf einem anderen Rechner, z.B. eine PC der die Unit Tests eines Active Parts ausführt, nicht funktionieren und zu einer Zugriffsverletzung führen -> der Test würde mit einer Ausnahme (Exception) fehlschlagen.
Dazu gibt es die Mock Implementation des HAL. Hier wird der HAL zugriff in einer Datenstruktur im Arbeitsspeicher gespeichert. Auch das Verhalten, z.B. ein allfälliger Rückgabewert kann vorgegeben werden. Dadurch kann die Hardware im Unit Test simuliert werden.
Beispiel
if (m_ledEnable) {
GPIO::writePin(GPIOModuleAddress::GPIOB, GPIO::Pin::Pin0, GPIO::PinState::High);
}
else {
GPIO::writePin(GPIOModuleAddress::GPIOB, GPIO::Pin::Pin0, GPIO::PinState::Low);
}
void GPIO::writePin(const GPIOModuleAddress::Id module, const Pin::Id pin,
const PinState::Id pinState) {
if (pinState == PinState::Low) {
resetPinValue(module, pin);
}
else {
setPinValue(module, pin);
}
}
void GPIO::setPinValue(const GPIOModuleAddress::Id module, const Pin::Id pin) {
GPIORegisters& GpioRegister = GPIORegisters::getInstance(module);
switch (pin) {
case Pin::Pin0:
GpioRegister.BSRR.BS0 = 1U;
break;
case Pin::Pin1:
GpioRegister.BSRR.BS1 = 1U;
break;
// …
}
}
// Mock:
void GPIO::writePin(const GPIOModuleAddress::Id moduleAddress, const Pin::Id pinSource,
PinState::Id pinState) {
STM32F767Mock::getSingle().getMockGPIO().writePin(moduleAddress, pinSource, pinState);
}
void writePin(const GPIOModuleAddress::Id moduleAddress, const GPIO::Pin::Id pinSource,
GPIO::PinState::Id pinState) {
m_pinLevelMap[moduleAddress][pinSource] = pinState;
}
Sonderfall: Bit Banding
Gewisse Prozessoren (z.B. die STM32 Cortex M3 Serie) erlauben einen vereinfachten Zugriff auf GPIO Pins. Dafür wird für jeden Pin eine eigene, 32 Bit Adresse reserviert. Die Anwendung kann nun einfach die Adresse lesen oder schreiben, um den Pin zu setzten oder auszulesen. Meist wird 0 als LOW und alles andere als HIGH interpretiert. Dies kann nicht über eine Methode im HAL abstrahiert werden. Stattdessen wird ein Symbol verwendet, das für die Zielhardware als Speicher Adresse des GPIO Pins definiert wird und im Unit Test als 32 Bit integer variable (uint32_t).
Beispiel
#ifndef UNIT_TEST
#define LD2 GPIOA_5_WRITE_ADDR
#elseif
uint32_t LD2 = 0U;
#endif
Zugriff:
LD2 = m_ledEnable ? 1 : 0;
Test:
LD2 = 0U;
Assert.AreEqual(1U, LD2);
HAL Distribution
Aktuell gibt es keine separaten HAL Packages. Der HAL ist im Runtime Package für die jeweilige Zielhardware enthalten (siehe Runtime Distribution).
Der HAL der der DATAFLOW Runtime beiliegt ist nicht in jedem Fall vollständig. Wir erweitern unsere HAL laufend und implementieren nur Peripherien, die wir auch auf der Zielhardware getestet haben. Damit fehlende Peripherien hinzugefügt werden können, werden alle DATAFLOW HAL Implementation immer als Source Code ausgeliefert.