Das Intel Hex-Object Format dient zum Übertragen von binären Daten in der Regel über eine serielle Schnittstelle. Mir persönlich ist es seit Ende der 1990er Jahre geläufig, wo ich mich erstmals intensiv mit Mikroprozessoren beschäftigt habe. Laut einer Revision der orignalen Spezifikation von Intel 1988, handelt es sich bei dem hexadezimalen Objektdateiformat "um eine Möglichkeit, eine absolut binäre Objektdatei in ASCII darzustellen. Da die Datei in ASCII anstatt in Binärform vorliegt, ist es möglich, die Datei auch auf Medien wie z.B. Papierstreifen, Lochkarten usw. zu speichern". Wie man anhand der Formulierung sieht, ist das Format also schon recht alt, Lochkarten und dergleichen spielen zum Glück schon eine ganze Weile keine Rolle mehr in der Informatik.
Intel HEX-Dateien werden auch heute noch hauptsächlich im Umfeld der Embedded-Programmierung z.B. bei 8-Bit- und 16-Bit-Prozessoren verwendet, um generierte Programmierdaten der erstellten Programme für diese Prozessoren, EPROMS und ähnliche Bausteine zu speichern. Durch spätere Erweiterungen der Spezifikation wurden sogar noch 32-Bit-Prozessoeren der x86er-Serie von Intel mit berücksichtigt.
Warum HEX-Dateien?
Im Embedded-/IoT-Bereich ist es heute noch gängige Praxis, dass beim Programmiervorgang der Mikroprozessoren (Microcontroller) zunächst nur ein Bootloaderprogramm mit einem passenden Programmiergerät auf die CPU "gebrannt" wird. Das eigentliche Programm, welches später zur Laufzeit ausgeführt werden soll, wird anschliessend über den zuvor einprogrammierten Bootloader in binärer Form in den dafür vorgesehenen Speicherbereich programmiert. Die Daten der Applikation werden in der Regel über eine serielle USB-Schnittstelle übertragen, wobei hier gerne immer wieder noch der gute alte RS232 COM-Port getunnelt wird.
Die ausführbare Appilkation wird vom verwendeten Compiler für den Microcontroller in Form von Intel-HEX-Dateien bereitgestellt und ist praktisch vergleichbar mit einem ausführbaren Programm (EXE-Datei) unter einem Windows-System. Aufgrund des in der Regel stark begrenzten Speichers bei 8-Bit- und 16-Bit-Microcontrollern, darf auch der Bootloader selbst nur möglichst wenig Speicher verbrauchen. Deshalb wird die HEX-Datei der Applikation entsprechend auf PC-Seite geparst und dort im Speicher in binärer Form zur späteren Übertragung zum Microcontroller aufbereitet.
Allgemeine Darstellung
Die hexadezimale Darstellung von Binärdaten ist beim Intel HEX-Format in alphanumerischen ASCII-Zeichen kodiert. Zum Beispiel ist der 8-Bit-Binärwert 0011-1111 0x3F in hexadezimal. Um dies in ASCII zu kodieren, wird ein 8-Bit-Byte mit dem ASCII-Code für das Zeichen'3' (0011-0011 oder 0x33) und einem 8-Bit-Byte mit dem Symbol ASCII-Code für das Zeichen'F' (0100-0110 oder 0x46) benötigt. Für jeden Bytewert ist die höherwertige Hexadezimalziffer immer die erste Ziffer des Hexadezimalziffernpaares. Diese Darstellung (ASCII hexadezimal) benötigt demnach also doppelt so viele Bytes wie die Binärdarstellung.
HEX-Datenblöcke
Eine Intel-HEX Datei ist in Datenblöcke aufgeteilt, die neben den Daten jeweils den Datensatz-Typ, Länge, die Speicheradresse und die Prüfsumme enthalten. Es existieren sechs verschiedene Typen von Datensätzen, welche definiert sind, aber nicht alle Kombinationen dieser Typen sind sinnvoll. Die spätere Übertragung der Daten erfolgt ebenfalls blockweise. Der Größe des Blocks beziehungsweise einer Flash-Page, welche zu einem Bootloader übertragen wird, kann sich jedoch von CPU zu CPU unterscheiden, dazu später mehr.
Das allgemeine Datensatz-Format für jede Zeile in einer Intel-HEX-Datei beginnt immer mit der sogenannten Startmakrierung ":", also dem ASCII-Code für den Doppelpunkt. Danach wird mit einem Byte die jeweilige Länge der Nutzdaten in Bytes angegeben. Anschliessend folgt der Datensatz-Typ mit einem Byte. Der maximale Wert des Feldes für die Nutzdatenlänge sind 255 Bytes (0xFF).
Danach wird über ein 2 Byte langes Adressfeld der 16Bit-Offset innerhalb des Speichers zur Ablage der Nutzdaten angegeben. Dieses Feld wird nur für Datensätze mit Nutzdaten (sog. Data records) verwendet. Bei Datensätzen, wo dieses Feld nicht verwendet wird, empfiehlt die Spezifikation hier vier ASCII-Nullzeichen zu verwenden ('0000'). Nach dem Adressfeld verfügt jeder Datensatz noch 1 Byte zur Angabe des entsprechenden Typen (siehe oben).
Anhand eines konkreten Beispiels lässt sich das nun für das generelle Format eines HEX-Datenblocks leicht darstellen:
: | 10 | A910 | 00 | 03300895E7E3F5E3 E00DF11D089520E0 | 2D |
Start-Markierung (1 Byte) | Länge der Nutzdaten (1 Byte) | Adresse (2 Byte) | Record-Typ (1 Byte) | Info oder Nutzdaten (hier 16 Bytes) | Prüfsumme (1 Byte) |
Vor der Startmarkierung dürfen theoretisch beliebig viel Zeichen stehen - in der Praxis tut das jedoch in der Regel niemand, bis auf die Angabe der Steuerzeichen CR (Carriage Return) und LF (Line Feed).
Datensatz-Typen (Record types)
Wie bereits erwähnt, verfügt jeder Datensatz über ein Feld Record-Typ, welches eben den Typen angibt. Dieses Feld dient zu Interpretation der folgenden Informationen:
- 00 Standard-Datensatz (8-, 16- oder 32-Bit-Format).
- 01 End-of-File Record zur Angabe des letzten Datensatzes der laufenden Übertragung (8-, 16- oder 32-Bit-Format).
- 02 Erweiterter Segmentadressensatz (16- oder 32-Bit-Format).
Der angegebene Address-Offset ist 0 und die Daten geben eine Segment-Adresse an, wie sie bei x86 CPUs verwendet wird. Die nachfolgenden Daten-Records sind in dieses Segment zu laden. - 03 Start Segment Address Record (16- oder 32-Bit-Format).
Der angegebene Address-Offset ist 0 und die Daten geben den Einsprungpunkt für die Programmausführung als CS:IP an. - 04 Erweiterte lineare Adressaufzeichnung (nur 32-Bit-Format). Der angegebene Address-Offset ist 0 und die Daten geben die oberen 16 Bit einer 32-bit Linear-Adresse an, ab der die folgenden Daten-Records zu laden sind.
- 05 Start Linear Address Record (nur 32Bit-Format). Der angegebene Adress-Offset ist 0 und die Daten geben den Einsprungpunkt für die Programmausführung als EIP an.
Die Record-Typen 03 und 05 werden für die Arbeit mit klassischen Microcontrollern in Havard-Architektur z.B. von Atmel, respektive Microchip grundsätzlich nicht benötigt. Die Spezifikation des Intel-HEX Object Formats sieht noch weitere Datensatztypen vor wie der Extended Linear Address Record und Extended Segment Address Record. Diese werden jedoch vor allem im Kontext von Intel 32-Bit x86-CPUs verwendet. Wer noch mehr detailiierte Informationen über die hier aufgeführten Record-Typen benötigt, verweise ich gern auf die originale Spezifikation des HEX-Formats von Intel [2].
Sollten beispielsweise beim Kompilieren und anschliessenden Linken einer Firmware Records wie z.B. :0400000300000000F9 mit in der HEX-Datei ausgegeben worden sein, können diese beim Einladen oder der Weiterverarbeitung der Datei ignoriert oder gelöscht werden, da sich am Beispiel dieses Eintrags (Typ 03) nur ein externer Verweis auf eine andere Adresse ergibt, welche aber selbst wieder 0 ist. Da genau dieser Datenblock wahrscheinlich als vorletzter Eintrag in der HEX-Datei zu finden ist, bevor diese mit dem wohldefinierten End-Record abgeschlossen wird, ist der Record dann allein aus diesem Grund schon überflüssig und kann in jedem Fall ignoriert oder gegebenenfalls gelöscht werden.
Berechnung der Prüfsumme
Jeder Record endet zum Schluss mit einer 1-Byte Prüfsumme, welche die ASCII-Hexadezimaldarstellung des Zweierkomplements der aller Bytes außer der Startmarkierung enthält, die sich aus der Umwandlung jedes ASCII-Hexadezimalziffernpaares in ein Binärbyte ergeben. Aus dem obigen Beispiel :10A9100003300895E7E3F5E3E00DF11D089520E0 wird die Prüfsumme also wie folgt berechnet:
10+A9+10+00+03+30+08+95+E7+E3 + F5+E3+E0+0D+F1+1D+08+95+20+30+E0 = 0x08D3 0x08D3 modulo 0x100 = 0xD3 0xD3 = 1101 0011b |
Zur Überprüfung einer korrekten Übertragung eines Records zum Microcontroller, bildet man nun einfach die Modulo-256-Summe über alle Bytes, logischerweise außer der Startmarkierung. Konkret bedeutet das, dass man sich dazu 1 Prüfsummenbyte allokiert und addiert die betroffenen Bytes, inklusive des angegebenen Prüfsummenbytes für den Record auf. Bei korrekter Übertragung bzw. Generierung muss das Ergebnis 0 sein, ansonsten ist ein Fehler aufgetreten.
Der EOF-Record
Der End-Of-File Record dient speziell zur Angabe eines Parsers, dass nun das Ende des Programms erreicht wurde. Theoretisch ist es ja denkbar, dass meherere Programme in ein und derselben Datei angegeben werden, somit stellt die folgende Zeile das Ende dar, wobei der Record-Typ 01 entsprechend für die Endmarkierung zuständig ist: :00000001FF
Übertragung der Programmdaten (Firmware) zum Gerät
Nachdem die Programmdaten aus der Intel-HEX-Datei korrekt geparst wurden, liegen die Daten binär in Form einer entsprechenden Memory-Map vor. Da in der Regel bekannt ist, wie groß die zu übertragenden Programmdaten sind, genügt einfach das statische Erzeugen eben eines Byte-Arrays in ausreichender Größe bei Initialisierung. Je nach verwendeter Microcontroller-CPU, sind die zu übertragenden Bytes pro Flash-Page unterschiedlich groß. Diese Info bekommt man entsprechend im Datenblatt vom Hersteller. Beim ATxmega128A1 von Atmel ist die Größe einer Flash-Page beispielsweise 256 Byte. Wenn der Linker entsprechend eingestellt ist, werden beim Kompilieren ausgegebene Intel-HEX-Dateien so erzeugt, dass diese entsprechende Offsets berücksichtigen, sodass die Programmdaten oberhalb des Bootloader-Speichers abgelegt werden. Dies hängt aber im Wesentlichen vom Bootloader-Konzept ab, welches man fährt.
Für die Speicherung der Programmdaten in den Flashspeicher, empfehle ich diese telegrammbasiert zu übertragen. Ein Telegramm sollte sämtliche Nutzdaten für eine Flash-Page für die jeweilige CPU enthalten, sodass der laufende Bootloader auf der Microcontroller-CPU diese Daten mit einem Aufruf ins Flash abspeichern kann. Es versteht sich von selbst, dass die Übertragung der Telegramme selbst mindestens nochmal mit entsprechenden Prüfsummen (z.B. CRC16) versehen und diese auf Microcontrollerseite entsprechend geprüft werden sollten, bevor das Abspeichern erfolgt.
Download / Links
Anbei habe ich einen Parser und Synthetisierer für das beschriebene Intel HEX-Dateiformat zum Download beigefügt. Der Parser kann entsprechende HEX-Dateien einladen, im Speicher ablegen und daraus auch wieder HEX-Dateien erzeugen. Darüber hinaus bietet die Implementation Funktionen zum Einladen und Speichern von Programmdaten bzw. der Firmware als Binärformat an. Implementiert habe ich den Parser schon vor einiger Zeit in C# 4.0 auf Basis des .NET-Frameworks 4.0 und kann frei verwendet werden. Zu Demo-Zwecken habe ich alle benötigten Hilfsfunktionen integriert und als kleine Konsolen-App verpackt.
[2] Intel Hexadecimal Object File Format Specification