[Firmware dev] GPS fietscomputer: CMake build systeem

Door Anteros op maandag 20 juli 2015 16:59 - Reacties (7)
Categorie: Technisch, Views: 2.489

Een goed onderhoudbaar softwareproject begint bij een begrijpbare opzet van de projectstructuur en het build systeem. Er zijn diverse manieren om dit te bewerkstelligen en ik heb de volgende structuur gekozen:

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

De root van het project bestaat uit diverse subdirectory’s:
  • buildtools : Hierin staat de cross-compiler voor het compileren en linken van de sources
    • Voor zowel Linux als Windows is een aparte cross-compiler gebruikt en afhankelijk van de host wordt de relevante cross-compiler aangeroepen
  • firmware : Hierin staan alle sub-systemen en sub-componenten, inclusief de twee executables (boot loader en de applicatie)
    • Sub-systemen behoren tot het project en zijn in principe niet 1:1 herbruikbaar
    • Sub-systemen bestaan uit een inc en src directory. De inc directory bevat alle openbare header-files die de API-functies beschrijven. De src directory bevat een of meerdere source-files en lokale header-files.
    • Sub-componenten behoren ook tot het project maar zijn wel 1:1 herbruik- en configureerbaar. De cfg directory in de ./firmware/ directory bevat daarom een subdirectory voor elk sub-component wat een zogenaamde ‘config’ header-file bevat – bv een vcp_cfg.h file -

      http://static.tweakers.net/ext/f/T4JoJLqL9I1XxNEwm1klc7nB/full.png
    • Sub-componenten bestaan verder ook uit een inc en een src directory.
    • Hieronder de huidige directory-structuur van mijn project en zoals te zien is, zijn cmdLine, configman, eventlog, stm32f2cube, ubicom en vcp herbruikbare componenten.

      http://static.tweakers.net/ext/f/QQ3Im8Jbq2kpNg5ON1w1AYak/full.png
  • build : Deze subdirectory wordt gebruikt voor het bouwen en linken van de executable(s)
  • refInfo : Hierin staat allerlei documentatie, voorbeeldcode, etc. wat gebruikt kan worden voor naslag tijdens de ontwikkeling

    http://static.tweakers.net/ext/f/5HDmiXxAg98MblkX5j9HVjjw/full.png
  • .git : De standaard GIT subdirectory
Voor het build systeem maak ik gebruik van Makefiles in combinatie met CMake. CMake parsed speciale CmakeLists.txt bestanden en genereert daaruit de Makefiles. Deze Makefiles kunnen vervolgens afgetrapt worden voor het daadwerkelijk bouwen van de executable(s).

Omdat er relatief weinig informatie beschikbaar is voor het gebruik van CMake in combinatie met een cross-compiler, wil ik graag het een en ander samenvatten om hiermee hopelijk wat mensen op weg te helpen met CMake.

CMake in embedded development
CMake werkt met het concept van executables en libraries. Een executable is een applicatie en in mijn geval zijn dat de boot loader (boot) en de hoofdapplicatie (applic). Alle sub-systemen en sub-componenten in mijn project zijn gedefinieerd als statische libraries. Ze worden dus gelinkt als .a bestanden om later weer gelinkt te worden met de executable(s).

Mijn CMake-configuratiebestanden staan als volgt in mijn project – overigens hebben nog niet alle libraries CMakeLists.txt bestanden - :

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

Omdat CMake er normaliter vanuit gaat dat de software gecompileerd gaat worden voor de local-host, moet je CMake nu vertellen dat je wilt compileren voor een embedded target. Dit kan middels een zogenaamd toolchain-bestand.

En toolchain CMake bestand wordt aangemaakt en daarin staat vermeldt welke compiler en linker CMake moet gebruiken. Dit bestand wordt geplaatst in de root van het CMake project (./firmware/ in mijn geval).

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

De inhoud van mijn toolchain file arm.toolchain.cmake is als volgt:

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

Korte uitleg:
  • ${PROJECT_SOURCE_DIR} : Is het absolute pad waar, in dit geval, arm.toolchain.cmake staat.
  • CMAKE_FORCE_C_COMPILER : Vertelt CMake dat CMake geen compiler check moet uitvoeren en dat de aangegeven compiler gebruikt moet worden.
Nu CMake weet welke compiler te gebruiken, is het ook nodig om CMake te vertellen wat de compiler- en linkeropties zijn. Die opties zijn nodig om de compiler o.a. te vertellen wat het optimalisatieniveau is, welke warnings en errors deze moet genereren, enz. De linker moet o.a. weten welke linker-file gebruikt moet worden om de objecten en libraries te linken, of de linker standaard libraries zoals libc mee moet nemen, enz.

Deze compiler- en linkeropties worden gezet in de root CMakeLists.txt en in een executable - ‘boot’ of ‘applic’ in mijn geval - CMakeLists.txt.

De inhoud van de root CmakeLists.txt is in mijn geval als volgt:

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

Korte uitleg:
  • set(LIBS …) : Dit zijn alle project libraries en executables die CMake moet gebruiken
  • add_definitions(…) : Hiermee kan je definities toevoegen die je vervolgens kan gebruiken in de code – #ifdef <MIJN_DEFINITIE> … #endif
  • set(C_MAKE_C_FLAGS …) : Dit zijn de compiler opties die CMake meegeeft aan de compiler
  • include_directories(…) : Hiermee vertel je CMake waar deze de verschillende header-files kan vinden
  • add_subdirectory(…) : Hiermee vertel je CMake dat een library of executable toegevoegd moet worden aan het project. CMake gaat dan de CMakeLists.txt-file in die subdirectory parsen en uitvoeren

Zoals hierboven beschreven staat, worden de linkeropties gedefinieerd in een executable. In mijn boot loader geval staat deze CMakeListst.txt dus in de ./firmware/boot/ directory en ziet er als volgt uit:

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

Korte uitleg:
  • set(FILE_MEMORY_LAYOUT …) : Deze CMake variabele bevat een absoluut pad naar de boot loader linker-file
  • set(CMAKE_EXE_LINKER_FLAGS …) : De linker opties die CMake meegeeft wanneer de linker aangeroepen wordt
  • add_custom_command(…) : Hiermee vertel je CMake dat dit commando uitgevoegd moet worden als de bestanden vectors.c en/of vectors.h benodigd zijn. Deze bestanden worden dynamisch door AWK gegeneerd om de Interrupt Vector Tabel te definiŽren
  • set(LIBDEPENDS …) : Hierin staan libraries die benodigd zijn om de boot loader executable te kunnen linken
Laatste stap
Nu alles klaar is kan CMake afgetrapt worden om de Makefiles te genereren. Dit wordt gedaan in de ./build/ directory met het volgende commando:

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

De output ziet er als volgt uit:

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

Nu kunnen we het project bouwen met make:

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

Als alles goed gaat staat er nu een .elf executable in ./build/boot/. Met het commando readelf <executable> -h kan je wat informatie opvragen:

http://static.tweakers.net/ext/f/3RTCU3wT3S9e3yWwe3XHt9k5/full.png

En de output:

http://static.tweakers.net/ext/f/4zuMlW2EFGrvIP2lbRVPCxHz/full.png

Slotwoord
CMake maakt het (voor mij) makkelijker om een build systeem op te zetten wat leesbaar en goed onderhoudbaar is. Niet alleen dat, het is nu een fluitje van een cent om een nieuwe library toe te voegen of speciale varianten van de software te bouwen. Deze en nog enkele andere voordelen maken het voor mij in ieder geval een perfecte keus!

Ik hoop dat het met deze blog-post nu wat duidelijker is hoe je CMake kan gebruiken in embedded softwareprojecten. Wellicht een overweging waard voor jouw (embedded) softwareproject?

Referenties

[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!