[Elek. GPS fietscomputer] Linker, HAL en huidige boot loader status

Door Anteros op donderdag 16 juli 2015 13:08 - Reacties (6)
Categorie: Technisch, Views: 2.648

Zoals beschreven in mijn introductie blog post ga ik het in deze blog post hebben over o.a. de linker en het gebruik van linker files. Verder wil ik graag een kort stukje schrijven over hardware abstractie om uiteindelijk te eindigen met een overzicht van de huidige boot loader status.

Linker files
Voordat software uitgevoerd kan worden op een target moeten de gecompileerde source-files – genaamd object-files (.o) - gelinkt worden door de linker. Dit linken is nodig om de objecten op de juiste plek in het geheugen van de target te plaatsen en om alle referenties naar functies en datastructuren in te vullen.

Omdat software op verschillende processoren en platformen kan draaien, moet de linker o.a. informatie hebben over de relevante geheugenlocaties en het soort geheugen van de target. Deze informatie verkrijgt de linker doormiddel van een linker-file.

Hieronder een korte omschrijving van wat er nu zoal in een linker-file staat. De voorbeelden geven (stukjes) van de boot loader linker-file weer.

Linker files: geheugen definities
Zoals hierboven al beschreven moet de linker weten wat voor type geheugens er beschikbaar zijn op de target, waar deze beginnen, hoe groot deze zijn en welke toegangsmogelijkheden ze hebben.

http://static.tweakers.net/ext/f/CtedGJhnFvoyY9Y0siIfvSiu/full.png

Met de tag MEMORY { … } kan men geheugensecties definiŽren in de linker-file. De definities die hierboven te zien zijn, komen van de beschreven geheugenindeling in mijn vorige blog-post. Zo begint de boot loader op adres 0x0800 0000 en is 64KB groot. De applicatie begint op adres 0x0800 0000 + 64K + 256B = 0x087B 1300 en omvat de rest van het FLASH geheugen. Men is helemaal vrij om deze adressen en secties te definiŽren, zolang het maar past in de gebruikte processor.

Linker files: Linker variabelen
De linker kan ook variabelen definiŽren die dan weer in de code gebruikt kunnen worden. Dit is af en toe handig, maar soms ook nodig. Ik kom later terug op wanneer het nodig is, maar hieronder alvast een paar voorbeelden:

http://static.tweakers.net/ext/f/MjSmZwoyQnLiFmvObLFnBO69/full.png

De variabele __appRomStart en consorten kunnen dus gebruikt worden in de code mocht dat nodig zijn. Wat goed is om te onthouden is dat de variabele NIET de waarde van ORIGIN(APPROM) krijgt, maar dat de variabele geplaatst wordt op adres ORIGIN(APPROM)! Als men dus in de code de pointer ‘opvraagt’ van de __appRomStart variabele – middels &__appRomStart -, krijgt men een pointer met het adres van ORIGIN(APPROM). Dit is belangrijk om te weten.

Dit maakt het geheel erg krachtig. Je kunt hiermee namelijk in de linker definiŽren waar bepaalde stukken geheugen liggen om deze vervolgens te gebruiken in de code. In mijn geval plaats ik de “boot loader API” in een stuk geheugen en de code kan vervolgens de variabele __bootApiStart gebruiken om de boot loader API functie-tabel erop te mappen. Ook is het mogelijk om bijvoorbeeld het base-adres van externe periferie zoals een FPGA te declareren in de linker, om deze vervolgens met &…. aan te spreken in de code. Mocht het base-adres dan een keer veranderen, dan hoeft de code niet aangepast te worden; de linker hoeft alleen maar opnieuw de boel aan elkaar te linken.

Zoals ik al eerder aangaf is het soms ook nodig om linker variabelen te gebruiken in de code. In mijn geval maak ik geen gebruik van de standaard startup-code en moet ik deze dus zelf schrijven. Het doel van de startup-code is om globale ‘initialized variables’ – zoals uint32_t variable = 0xdeadbeef; - te initialiseren en om de overige globale variabelen een default-waarde te geven (0 in mijn geval).

De ‘initialized variables’ zijn opgeslagen in FLASH en kunnen daarom niet veranderd worden door de code. Dit is uiteraard niet de bedoeling en daarom moeten deze variabelen eerst gekopieerd worden naar RAM voordat ze gebruikt kunnen worden.

Hieronder een stukje C-code die de ‘initialized variables’ kopieert van FLASH naar RAM met behulp van linker variabelen:

http://static.tweakers.net/ext/f/hbocT9ILYj3irIueH7a9RfDF/full.png

De resetHandler is de functie die aangeroepen wordt als de processor opstart. Dit is dus de allereerste code die uitgevoerd wordt op de processor. Deze functie wordt aangeroepen via de reset-entry in de Interrupt Vector Tabel – in de .isr_vector-sectie; zie verder -.

Met ‘extern …’ wordt aan de compiler doorgegeven dat de variabelen ergens gedefinieerd zijn maar nog niet bekend is waar. Deze variabelen zijn (in dit geval) linker variabelen en zijn dus gedefinieerd in de linker:

http://static.tweakers.net/ext/f/ChnmceB9T5G19pytoaq1GgGn/full.png

De twee FOR-loops gebruiken dus het start- en eind-adres van de twee relevante secties om de ‘initialized variables’ te kopiŽren van BOOTROM naar BOOT_RAMVAR en om de ‘non-initialized variables’ in BOOT_RAMVAR te initialiseren met 0U.


Linker files: Linker secties
Hierboven zijn al enkele voorbeelden te zien van linker secties, maar hoe werken die secties nu precies?

http://static.tweakers.net/ext/f/ESdmqtK18c5sKGAMLAU0bYvv/full.png

De SECTIONS { … } tag wordt gebruikt om bepaalde code en/of data toe te wijzen aan vooraf gedefinieerde geheugensecties – met de eerder beschreven MEMORY { … } tag -.

De linker parsed de linker-file van boven naar beneden en komt dus eerst de .isr_vector sectie tegen. Het eind van deze sectie laat } >MIRROR AT>BOOTROM zien. Dit betekent dat de inhoud van deze .isr_vector-sectie geplaatst gaat worden in de MIRROR geheugensectie. Alle referenties naar de .isr_vector-sectie gaan dus verwijzen naar adressen in het MIRROR geheugen. De AT>BOOTROM vertelt de linker dat de inhoud van .isr_vector echter opgeslagen moet worden in BOOTROM. De uiteindelijk gegenereerde binary zal dus vanaf adres BOOTROM data bevatten van .isr_vector. Echter zal de code in deze binary referenties hebben naar MIRROR-adressen. Ik weet het, het kan erg verwarrend zijn.

Overigens is de sectie .isr_vector, wat de interrupt-vectoren bevat, gedefinieerd in code:

http://static.tweakers.net/ext/f/VikngvHufCnxZGjZdiRLVpPK/full.png

Het is dus mogelijk om zelf secties te definiŽren in code om vervolgens de linker te vertellen waar die secties in het geheugen geplaatst moeten worden. De boot loader API is nog een dergelijk voorbeeld:

http://static.tweakers.net/ext/f/IzVTIpJNL4mUvHSkaHTdPf5q/full.png

De linker plaatst vervolgens deze struct met functiepointers vanaf adres BOOTROM_API:

http://static.tweakers.net/ext/f/veGmbdz9qWFoVteyi8wTNEk7/full.png

Om het nog complexer te maken is het ook mogelijk een linker variabele toe te wijzen aan een sectiepointer. Zie onderstaand voorbeeld:

http://static.tweakers.net/ext/f/1uU9tE2ksqct6A6ohpp3Jlps/full.png

De sectie .stack beschrijft een stuk geheugen waarin de stack geplaatst wordt en bevindt zich in het BOOT_RAMVAR geheugen. Zoals te zien is staan daar wat interessante statements in:
  • __stack_start__ = . ; : De linker variabele __stack_start__ wordt geplaatst op het huidige adres van de BOOT_RAMVAR-pointer. Deze pointer begint op adres ORIGIN(BOOT_RAMVAR) en wordt met n-bytes vermeerderd bij elke BOOT_RAMVAR -sectie. Het is dus van te voren niet bekend waar __stack_start__ zich bevindt in het geheugen.
  • . = . + STACK_SIZE; : De BOOT_RAMVAR-pointer wordt vermeerderd met STACK_SIZE bytes. De BOOT_RAMVAR-pointer staat nu dus STACK_SIZE bytes verder.
  • . = ALIGN(4); : Mocht de BOOT_RAMVAR-pointer nu op een adres staan wat niet ge-aligned is met een 4-bytes boundry adres, dan wordt de BOOT_RAMVAR-pointer dusdanig vermeerderd zodat deze weer op een 4-bytes boundry adres staat.
De code kan nu dus het adres van __c_stack_top__ gebruiken om te weten waar in het geheugen de stack begint. In dit geval verwacht de STM32F207 microcontroller de TOP-OF-STACK-pointer op adres 0x0000 0000 zodat het zijn SP-register kan initialiseren:

http://static.tweakers.net/ext/f/F5rthZXeiBq2Paw9HTl6u0fa/full.png

Linker files: Conclusie
Linker files kunnen van heel eenvoudig tot erg complex gaan. Zeker als je er niet dagelijks mee te maken hebt kan het lastig zijn. Ik ben me er van bewust dat bovenstaande incompleet, gefragmenteerd en niet geschikt is om goed te begrijpen hoe linker files werken. Echter kan via een search-engine genoeg informatie gevonden worden om zelf linker files te schrijven en toe te passen. In ieder geval hoop ik dat ik voldoende aanknopingspunten gegeven heb om zelf te gaan spelen met linker files.

Hardware Abstraction Layer
Op dit moment gebruik ik een STM32F207 microcontroller van ST. Dit is een Cortex M3 met diverse leuke periferie. In mijn vorige blog-post kan men lezen waarom ik (voorlopig) voor deze controller kies.

Het is echter waarschijnlijk dat ik op een later tijdstip ga kiezen voor een krachtigere controller. Mocht het zover komen dan wil ik niet de diverse source-files modificeren om de nieuwe controller te ondersteunen. Tevens wil ik later snel kunnen schakelen tussen diverse type controllers en daarom moet er dus een vorm van hardware abstractie komen; de applicatie mag geen kennis hebben van de gekozen controller.

Nu heeft ST wel een soort van hardware abstractie library genaamd STM32F2cube maar deze is helaas niet voldoende. Deze library abstraheert de onderliggende registers van de controller wel, maar de library is nog steeds controller-range/merk specifiek. Het is waarschijnlijk relatief eenvoudig om te switchen van een STM32F2xx naar een STM32F7xx door deze library te vervangen, maar je hebt dan alsnog ST kennis in je applicatie.

Daarom heb ik een echte hardware abstractie laag geÔmplementeerd. Een laag die de applicatie kan gebruiken om controller-periferie aan te sturen zonder kennis te hebben van de gebruikte controller.

Hoe ziet dit er conceptueel uit?

http://static.tweakers.net/ext/f/XfQfEVj3d8Zbyelhomaj1IFV/full.png

En de directorystructuur:

http://static.tweakers.net/ext/f/5zllFpPqpkaY0sqASRtjubzx/full.png

De HAL definieert API-functies die voor elk type controller apart geÔmplementeerd moeten worden. Door nu op linker-niveau de juiste HAL-implementatie te linken met de applicatie, kan eenvoudig geschakeld worden van een controller naar een andere. En zoals hierboven te zien is, gebruikt de applicatie alleen nog de API-functies van de HAL i.p.v. die van de ST32Fxxx-library. Er zijn geen applicatie-dependencies meer voor de STM32Fxxx-library.

Op dit moment heb ik de HAL-implementatie af voor de GPIO, System clock configuratie en de UART. Gaandeweg ga ik meer HAL-componenten voor de STM32F2xx implementeren om uiteindelijk te eindigen met een volledige abstractie van deze controller.

Huidige status van de boot loader
Ten eerste wil ik melden dat mijn “hello world”-applicatie draait op mijn target. Deze applicatie doet niks anders dan enkele LED-jes laten knipperen maar toch geeft dat nog steeds een kick :). De boot loader bouwt en linkt dus goed, mijn HAL werkt en mijn build systeem is ook op orde - binnenkort meer over mijn build systeem in een aparte blog-post -.

Omdat dit een embedded target is heb ik geen standaard console, printf en dergelijke tot mijn beschikking. Omdat deze wel erg handig zijn, zowel met debuggen als configureren/testen, ben ik dus begonnen met het ontwikkelen en implementeren van enkele generieke componenten. Hieronder een korte omschrijving van enkele componenten.

Status BL: UBICOM en VCP
Deze communicatiestack heb ik ontwikkeld om eenvoudig meerdere communicatieadapters en protocollen te kunnen registeren en gebruiken.

http://static.tweakers.net/ext/f/5Mf8Z4IDwM1B3eq3v9iaOgYo/full.png

Het bestaat uit twee delen.

Ubicom implementeert o.a. de mogelijkheid om communicatieadapters te registeren. De ubicom-drivers voor de specifieke adapters/periferie – zoals UART, Bluetooth, WIFI, etc - moet men wel zelf schrijven maar in veel gevallen is dit erg eenvoudig. Men hoeft alleen maar enkele open/close/send/etc callback-functies te implementeren zodat de driver geregistreerd kan worden bij ubicom. Vervolgens kan ubicom data versturen en ontvangen via de geregistreerde adapters. Bovenop deze adapter-manager draait een protocol-manager. Deze is verantwoordelijk om arbitraire protocollen te registreren en om ontvangen data te dispatchen naar de relevante geregistreerde protocol-handlers.

VCP is een dergelijk protocol wat zich registreert bij de protocol-manager. VCP staat voor Virtual Communication Ports en implementeert het concept van poorten. De applicatie kan een poort openen bij VCP en deze ‘binden’ aan een geregistreerde adapter. Vervolgens kan de applicatie data ontvangen en versturen via deze poort.

Het voordeel van deze stack is dat meerdere applicatiecomponenten/-services gescheiden kunnen communiceren met een host via ťťn of meerdere adapters. Ik ga hier gebruik van maken om via mijn Mac met een command-line interpreter op de target te communiceren ťn om tegelijkertijd de target LOG-data via een andere poort te ontvangen op mijn Mac. Alle data gaat dan bi-directioneel via ťťn enkele RS232 verbinding terwijl alles toch netjes gescheiden aankomt op zowel mijn Mac als mijn embedded target.

Status BL: Command-line console
Deze component heb ik ontwikkeld om een command-line console mogelijk te maken op de target, om zo dynamisch commando’s uit te voeren. Deze command-line kan er als volgt gebruikt worden:
  • >> show_version bl;
  • >> enable_encryption;
  • >> show_errorlog -10 -5; clear_errorlog; clr_screen;
  • Etc.
De command-line console is generiek en de applicatie/componenten kunnen zelf extra command-line commando’s registeren. Sterker nog, er kunnen meerdere consoles draaien op de target met ieder hun eigen range van commando’s. De mogelijkheden zijn ‘eindeloos’ :).

http://static.tweakers.net/ext/f/u5h8wCYoj4EP6R1Be5AXdrjR/full.png

Slotwoord
Linker files kunnen soms wat cryptisch zijn en daardoor af en toe wat moeilijker te lezen. Toch zijn ze cruciaal en enige kennis erover is zeker wel handig; vooral als je speciale wensen hebt v.w.b. geheugenindeling op je target.

In ieder geval hoop ik dat het een interessante blog-post was en dat het wellicht nieuwe inzichten heeft gegeven. Mocht je vragen, opmerkingen of kanttekeningen hebben dan hoor ik dat graag. Mijn volgende blog-post is in ieder geval al klaar en gaat over het (CMake) build-systeem.

Tot dan!

Volgende: [Firmware dev] GPS fietscomputer: CMake build systeem 07-'15 [Firmware dev] GPS fietscomputer: CMake build systeem
Volgende: [Elek.]: Ontwikkeling van een GPS fietscomputer – Intro en  de boot loader (fase 1) 06-'15 [Elek.]: Ontwikkeling van een GPS fietscomputer – Intro en de boot loader (fase 1)

Reacties


Door Tweakers user jjust, vrijdag 17 juli 2015 08:15

Wat een interessant blog. Heb nog niet eerder zo een inkijk gehad in het ontwikkelproces van embedded software. Ik blijf het volgen. Veel succes met je project!

Door Tweakers user Hans1990, vrijdag 17 juli 2015 19:54

Erg mooie blog, ben zeker benieuwd hoe de vordering en resultaat zal zijn! Wat meer embedded firmware blogs op tweakers is niet verkeerd :)

printf is op een dergelijke uC waarschijnlijk wel beschikbaar (ook zelfs op kleine PIC16's), maar vreet zomaar paar kB code ruimte weg. Ook moet je vaak even puzzelen waar je de stdout kan re-directen naar een COM poort of zoiets. Vaak zijn de stdlib functies write() e.d. weak geimplementeerd en kan je die overschrijven. Soms moet je het via putch() doen.

Het kan wel verrekte handig zijn om in je code iets te printen (hoewel je ook debuggers hebt tegenwoordig). Ik gebruik het o.a. om gefaalde assert() conditions in functies over COM poort (of soms telnet) te spugen, zodat ik kan zien of er overduidelijke eigenaardigheden in de code verstopt zitten.

Wat moet ik mij voorstellen bij je ubicom deel? Is dat een standaard device driver voor meerdere data streams over 1 COM poort oid?

Door Tweakers user Anteros, vrijdag 17 juli 2015 20:33

Dank jullie voor jullie reacties!

@Hans1990: Het klopt dat printf, malloc, etc beschikbaar zijn op embedded targets. De standaard libc-library die ik gebruik roept diverse 'helper'-functies aan om de printf output te re-directen of om geheugen te reserveren. Deze 'helper'-functies moet je zelf implementeren in je applicatie. Een voorbeeld van zo'n functie is _sbrk (wordt gebruikt voor malloc). Een nadeel is, en jij hebt het al aangegeven, dat je kostbare FLASH-ruimte kwijtraakt als je de standaard library meelinkt. Daarom doe ik dat (voorlopig) niet en implementeer ik mijn eigen printf-functies. Overigens wil ik dynamisch geheugen zo lang mogelijk vermijden en ik denk dat ik er wel een heel eind mee kom.

Ubicom is een framework wat ik ontwikkeld heb om communicatie-adapters en protocollen te registeren. Dit gedeelte is generiek en gebruik ik als basis om de diverse communicatie-adapters zoals UART, WIFI, BT en het VCP-protocol te registeren. Overigens moet ik de high-level WIFI, BT en UART drivers wel zelf schrijven (maar dat is erg eenvoudig).

Het VCP-protocol maakt het dan weer mogelijk om meerdere datastreams gescheiden over 1 communicatie-adapter te versturen. De multiplexing/demultiplexing van de datastreams zit dus in het VCP-protocol.

Ik heb deze stack+drivers ontwikkeld om het mogelijk te maken dat ik bv mijn event- en assert-log kan versturen via poort A over adapter X en mijn debug-log via poort B over adapter Y.

Door Tweakers user Sissors, zaterdag 18 juli 2015 10:11

Ik ben even te lui om op te zoeken hoeveel flash je MCU precies heeft, maar dat is sowieso een flinke hoop voor een MCU. Gewoon standaard printf zou toch niet zoveel geheugen moeten gebruiken? Ik weet ook niet of je dat met opties bij libc nog verder kan terugbrengen (bijvoorbeeld van ARM heb je microlib die nog een stuk kleiner is).

Als je het doet op een MCU met 16kB flash, ja dan wordt het weer een heel ander verhaal.

Door Tweakers user Anteros, zaterdag 18 juli 2015 14:40

De uC die ik gebruik heeft 1MB FLASH geheugen. Dat lijkt veel, maar ik denk dat ik meer nodig ga hebben als de applicatie wat groter wordt. Als ik zonder de libc-library kan, dan is dat wel mooi meegenomen.

Ik geef toe, nu ik mijn blog-post terug lees, dat de reden voor Ubicom+VCP niet helemaal duidelijk wordt. Het is iig niet bedoeld om printf en dergelijke te vervangen, maar meer om de mogelijkheid te bieden om diverse input/output zoals debug-output/eventlog-output/console-input/etc via verschillende poorten tegelijk te kunnen gebruiken over een of meerdere fysieke interfaces. Het kan dus goed mogelijk zijn dat ik later een library toevoeg die (o.a.) printf implementeert.

Door Tweakers user Sissors, zaterdag 18 juli 2015 20:37

Elke kB kan relevant zijn, aan de andere is de paar kB die printf en aanverwanten kosten waarschijnlijk een heel stuk relevanter op een MCU met 16kB flash dan eentje met 1MB flash.

Zelf gebruik ik mbed (http://mbed.org/) en dat merk je daar ook: mbed voegt overhead toe, en bij de kleinste ondersteunde MCUs, die hebben 4kB SRAM en denk 16kB flash uit mijn hoofd, is dat wel significant. Bij een K64F met 1MB RAM is dat eigenlijk allemaal verwaarloosbaar. Net als gebruik van strings ipv char arrays: Bij de meeste MCUs een doodzonde, maar bij de grootste valt het wel mee.

De overhead heeft natuurlijk ook nadelen, vooral als je toch zelf je eigen implementatie wil doen. Iets als een standaard interface is wel heel makkelijk te maken daar: Makkelijkste is een class maken die Stream class inherit, en dan hoef je enkel _putc, _getc en nog een paar vergelijkbare functies te implementeren, en dan inherit hij alle printf en soortgelijken functies.

Reageren is niet meer mogelijk