C/C++ für Mikrocontroller - Eigener HAL

Um die C/C++ für Mikrocontroller verwenden zu können ist ein hardware-spezifischer HAL notwendig. Falls für die Zielplattform kein HAL verfügbar ist, kann mittels dieser Anleitung ein eigener geschrieben werden.

Bemerkung

Falls Du einen HAL für eine neue Hardware-Plattform geschrieben hast und es möglich ist diesen unter der CC0-Lizenz zu veröffentlichen, bitten wir um eine Mail an info@tinkerforge.com. Wir haben immer Interesse daran, HALs für weitere Plattformen in die Bindings aufzunehmen.

Hardware-Anforderungen

Um die Bindings auszuführen, wird ein Mikrocontroller benötigt, der in etwa vergleichbar zum, oder besser als der ATmega328 (Arduino Uno) ist. Der Bindings-Code benötigt mindestens 2k RAM und 16k Flash. Außerdem muss die Host-Hardware SPI mit mindestens 400 kHz kommunizieren können. Für maximale Performance wird eine SPI-Taktfrequenz von 2 MHz empfohlen.

Übersicht

Ein HAL ist für die Initialisierung und Kontrolle der SPI-Hardware der Zielplattform verantwortlich. Außerdem abstrahiert er andere hardwarespezifische Funktionen wie Zeitmessung und Logging.

Um einen HAL zu implementieren müssen die folgenden Schritte durchgeführt werden:

  • Definition der TF_HAL- und TF_Port-Strukturen
  • Implementierung der tf_hal_create und tf_hal_destroy-Funktionen für die definierte Struktur
  • Implementierung der notwendigen HAL-Funktionen

Die folgenden Funktionen aus bindings/hal_common.h werden verwendet:

int tf_hal_common_create(TF_HAL *hal)

Initialisiert die TF_HALCommon-Instanz die zum übergebenen TF_HAL gehört. Diese Funktion muss bei der HAL-Initialisierung so früh wie möglich aufgerufen werden.

int tf_hal_common_prepare(TF_HAL *hal, uint8_t port_count, uint32_t port_discovery_timeout_us)

Schließt die Initialisierung der TF_HALCommon-Instanz, die zum übergebenen TF_HAL gehört, ab. Das ist typischerweise der letzte Schritt der HAL-Initialisierung. SPI-Kommunikation muss hier bereits möglich sein. Diese Funktion erwartet die Anzahl verwendbarer Ports, sowie einen Timeout in Mikrosekunden, der angibt, wie lange die Bindings versuchen sollen, ein Gerät an einem der Ports zu erreichen. tf_hal_common_prepare() baut dann eine Liste der erreichbaren Geräte und speichert diese in der TF_HALCommon-Instanz.

Definition einer TF_HAL-Struktur

Die TF_HAL-Struktur hält alle Informationen, die für die SPI-Kommunikation notwendig sind. Sie speichert außerdem eine Instanz von TF_HALCommon, einem internen Typen, der pro HAL-Instanz einmal von den Bindings benötigt wird, sowie einen Pointer auf ein Array von Port-Mapping-Informationen (TF_Port).

Die TF_Port-Struktur kann komplett auf den HAL angepasst werden, um zum Beispiel den Chip-Select-Pin eines Ports, sowie dessen Namen, zu verwendende SPI-Einheit usw. abzuspeichern. Beispiele finden sich in hal_arduino_esp32/hal_arduino_esp32.h und hal_linux/hal_linux.h.

Bricklets werden anhand ihrer UID und des Ports unter dem sie erreichbar sind identifiziert. Ein Port mappt typischerweise auf den Chip-Select-Pin der gesetzt werden muss, damit Daten über SPI an das Bricklet übertragen werden. Manche HAL-Funktionen bekommen eine Port-ID übergeben. Diese ist typischerweise der Index in ein TF_Port-Array.

Implemetierung der tf_hal_create und tf_hal_destroy-Funktionen

Nachdem die TF_HAL-Funktion definiert wurde, muss deren Initialisierungsfunktion tf_hal_create implementiert werden. Sie hat folgende Aufgaben:

  • Die TF_HAL-Struktur in einen definierten Zustand bringen
  • Die TF_HALCommon-Instanz mit tf_hal_common_create() initialisieren
  • Vorbereiten der SPI-Kommunikation. Wenn die Initialisierungs-Funktion beendet ist, muss die SPI-Kommunikation mit allen angeschlossenen Geräten möglich sein. Alle Chip-Select-Pins müssen auf HIGH (also deaktiviert) stehen. Siehe hier für Details über die SPI-Kommunikation.
  • Aufrufen von tf_hal_common_prepare(). Das ist typischerweise der letzte Initialisierungsschritt. SPI-Kommunikation muss hier möglich sein.

Nach Konvention gibt tf_hal_create einen int zurück, der bei Erfolg auf TF_E_OK gesetzt ist. Falls die Initialisierung fehlschlägt, kann ein anderer Fehlercode aus bindings/errors.h zurückgegeben werden. Es ist außerdem möglich eigene Fehlercodes für den HAL in dessen Header zu definieren. Die Fehlercodes von -99 bis -1 sind allerdings für die Bindings reserviert. Der erste valide Fehlercode ist also -100.

Nachdem tf_hal_create implementiert wurde, kann jetzt tf_hal_destroy implementiert werden. Es sollte möglich sein, einen HAL mit tf_hal_create zu erstellen, zu verwenden, ihn dann mit tf_hal_destroy zu zerstören und danach mit tf_hal_create wieder zu erstellen. Der neu erstellte HAL muss dann wieder funktionsfähig sein.

Implementierung der benötigten HAL-Funktionen

Als letzter Schritt müssen die folgenden Funktionen implementiert werden, die in bindings/hal_common.h zwischen // BEGIN - To be implemented by the specific HAL und // END - To be implemented by the specific HAL definiert sind. Alle Funktionen, die einen int zurückgeben, sollten TF_E_OK zurückgeben, wenn kein Fehler aufgetreten ist.

int tf_hal_chip_select(TF_HAL *hal, uint8_t port_id, bool enable)

Wenn enable true ist, wählt diese Funktion den Port mit der übergebenen ID für die folgende SPI-Kommunikation aus. Wenn enable false ist, wird der Port nicht mehr ausgewählt.

Abhängig von der Plattform müssen hier mehrere Schritte durchgeführt werden. Zum Beispiel muss auf einem Arduino begin/endTransaction aufgerufen werden um sicherzustellen, dass die SPI-Konfiguration angewendet wird.

Die Bindings stellen sicher, dass immer nur ein Port gleichzeitig ausgewählt wird.

int tf_hal_transceive(TF_HAL *hal, uint8_t port_id, const uint8_t *write_buffer, uint8_t *read_buffer, uint32_t length)

Überträgt length Bytes an Daten aus dem write_buffer zum Bricklet und empfängt währenddessen die selbe Menge an Bytes vom Bricklet in den read_buffer (da SPI bidirektional ist). Die übergebenen Buffer sind immer groß genug um length Bytes zu lesen oder zu schreiben.

Diese Funktion wird nur aufgerufen, wenn zuvor tf_hal_chip_select() mit der selben Port-ID und enable=true aufgerufen wurde.

Falls die Zielplattform DMA unterstützt, kann hier ein Transfer initiiert werden, es muss aber blockiert werden bis die Daten übertragen wurden.

Falls die Zielplattform kooperatives Multitasking unterstützt, kann, nachdem ein Transfer initiiert wurde, yield o.Ä. aufgerufen werden. Um sicherzustellen, dass während die Bindings während des Transfers nicht verwendet werden, sollten sie wie folgt gesperrt werden:

TF_HALCommon *common = tf_hal_get_common(hal);
common->locked = true

Nachdem der Transfer abgeschlossen ist, sollten die Bindings wieder entsperrt werden, damit sie weiter verwendet werden können.

uint32_t tf_hal_current_time_us(TF_HAL *hal)

Gibt die aktuelle Zeit in Mikrosekunden zurück. Diese Zeit muss keine Relation zu einer "echten" Zeit haben, aber monoton außer bei Überläufen sein.

void tf_hal_sleep_us(TF_HAL *hal, uint32_t us)

Blockiert für die übergebene Zeit in Mikrosekunden. Falls die Plattform kooperatives Multitasking unterstützt, können die Bindings hier gesperrt und danach durch yield pausiert werden. Siehe tf_hal_transceive() für Details.

TF_HALCommon *tf_hal_get_common(TF_HAL *hal)

Gibt die TF_HALCommon-Instanz zurück, die zum übergebenen TF_HAL gehört.

char tf_hal_get_port_name(TF_HAL *hal, uint8_t port_id)

Gibt den Port-Namen (typischerweise ein Buchstabe zwischen 'A' and 'Z') für die übergebene Port-ID zurück. Der Name wird in get_identity-Rückgaben eingefügt, falls das Gerät direkt mit dem Host verbunden ist.

void tf_hal_log_message(const char *msg, size_t len)

Loggt die übergebene Nachricht. Die Nachricht hat eine Länge von len und ist nicht null-terminiert. Abhängig von der Plattform kann hier z.B. eine serielle Konsole (Arduino) oder die Standardausgabe (Linux) verwendet werden. Es kann auch in eine Log-Datei geschrieben werden.

void tf_hal_log_newline()

Loggt das/die plattformspezifischen Zeilenumbruchszeichen.

const char *tf_hal_strerror(int e_code)

Gibt eine Fehlerbeschreibung für den übergebenen Fehlercode zurück. Um so platzeffizient wie möglich zu sein, kann diese Funktion komplett entfernt werden, falls TF_IMPLEMENT_STRERROR nicht in bindings/config.h definiert ist.

Fehlercodes die von den Bindings verwendet werden können durch Einbinden von bindings/error_cases.h behandelt werden.

Zur Implementierung kann die folgende Vorlage verwendet werden:

#if TF_IMPLEMENT_STRERROR != 0
const char *tf_hal_strerror(int e_code) {
    switch(e_code) {
        #include "../bindings/error_cases.h"
        /* Add HAL specific error codes here, for example:
        case TF_E_OPEN_GPIO_FAILED:
            return "failed to open GPIO";
        */
        default:
            return "unknown error";
    }
}
#endif
char tf_hal_get_port_name(TF_HAL *hal, uint8_t port_id)

Gibt den 1-Zeichen Namen zurück, der zur übergebenen Port ID gehört.

TF_PortCommon *tf_hal_get_port_common(TF_HAL *hal, uint8_t port_id)

Gibt die TF_PortCommon-Instanz zurück, die zur übergebenen Port ID gehört.

Details über die SPI-Kommunikation

Die Kommunikation zwischen dem Host und den Bricks/Bricklets verwendet SPI Modus 3:

  • CPOL=1: Clock-Polarität ist invertiert, high wenn inaktiv
  • CPHA=1: Clock-Phase ist verschoben, Daten werden zur fallenden Taktflanke gelesen

Daten werden mit dem MSB (most significant bit) zuerst übertragen. Die Standardtaktfrequenz ist 1,4 MHz, Bricks und Bricklets unterstützen aber Taktfrequenzen zwischen 400 kHz und 2 MHz. Der Logikpegel aller Signale beträgt 3,3V.

Aufgrund eines Bugs des auf den Bricklets verwendeten XMC-Mikrocontrollers von Infineon trennt das Bricklets sich nicht korrekt vom SPI-Bus, wenn das Chip-Select-Signal deaktiviert wird. Es treibt dann weiterhin auf MISO einen Wert, was dazu führt, dass sich mehrere Bricklets am selben SPI-Bus gegenseitig stören. Falls mehrere Bricklets eingesetzt werden sollen, müssen deshalb vom Chip-Select-Signal kontrollierte Trenner-Chips eingesetzt werden.