Bevat blog-posts over mijn traject van idee->design->ontwikkeling->testen->? van een GPS fietscomputer met navigatie.

[Firmware dev] GPS fietscomputer: CMake build systeem

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

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.630

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!

[Elek.]: Ontwikkeling van een GPS fietscomputer – Intro en de boot loader (fase 1)

Door Anteros op zaterdag 27 juni 2015 18:20 - Reacties (20)
Categorie: Technisch, Views: 4.284

Introductie

Mijn eerste blog op Tweakers. Een probeersel om te zien of het wat voor mij is. Op zich ben ik niet zo'n schrijver maar het leek me leuk om het eens te proberen.

Anyway, waarom een blog over het ontwikkelen van een GPS fietscomputer? Wel, in mijn vrije tijd pak ik regelmatig mijn racefiets om er op uit te trekken. Het is voor mij een stuk ontspanning en ik kan erg genieten van de snelheid en de vrijheid die het me geeft. Omdat ik regelmatig op nieuwe plekken kom die niet (erg) bekend zijn voor mij, gebruik ik een Garmin Edge 810 GPS fietscomputer met navigatie. Ideaal om thuis een route uit te stippelen en deze vervolgens te volgen met behulp van de fietscomputer.

Maar helaas, het ding doet niet alles wat Garmin beweerd. Althans, niet stabiel. Als datalogger of als map-viewer werkt deze prima, maar de echte navigatiemogelijkheden zoals actieve begeleiding laten het vaak afweten. Sterker nog, soms laat heel de Edge 810 het afweten. Wat er ook nog eens bij moet komen is dat Garmin erg, erg slecht omgaat met klachten en softwareproblemen. De laatste firmware versie voelt nog steeds aan als BETA terwijl de 810 al ruim 1,5 jaar op de markt is.

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

Dat heeft mijn o.a. doen besluiten om zelf iets te gaan ontwikkelen. De discutabele software is echter niet de enige reden. Een andere reden is dat er diverse (motiverende) features niet, of niet goed geÔmplementeerd zijn. In mijn ogen kan dat (veel) beter en ik heb enkele features in mijn hoofd zitten die het fietsen – en sporten in het algemeen – extra kunnen stimuleren en nog leuker kunnen maken. Een laatste reden is dat ik graag embedded software ontwerp/ontwikkel en graag met elektronica bezig ben. De stap om het zelf te proberen was daarom snel gemaakt.

Ik werk ruim tien jaar als professioneel embedded software engineer en kreeg meteen kriebels om enkele interessante concepten verder uit te werken. Niet alleen op functioneel niveau, maar ook op architectuur niveau. Ook de bijbehorende hardwareontwikkeling lijkt me een leuke uitdaging.

Nu weet ik (van mezelf) dat vele (hobby-)projecten vroegtijdig stranden. En om eerlijk te zijn, vind ik dat helemaal niet erg. Je leert er altijd van en het houdt je scherp. Het is in mijn ogen dus nooit een nutteloze exercitie. Mocht het echter wel zover komen dat er een werkend prototype is, dan ben ik voornemens om serieuze stappen te maken voor een vervolgtraject. Dit zou dan een soort van kickstarter-traject kunnen zijn met als eerste doel een echt produceerbaar product te maken. Het tweede (voor mij utopisch) doel zou zijn om te groeien tot een bedrijfje wat meer onderscheidende producten voor de sportwereld gaat maken.

Overigens weet ik heel goed wat er allemaal bij komt kijken als je een product wilt maken vanuit een prototype: verificatietesten, schok-/vibratie-/emc-/IP67-/etc-testen, normeringen, maakbaarheid, re-designs, issues, lead-time, fine-tunen, handleidingen, reduceren van component-costs, (design-)documentatie, backend, support, etc, etc. Teveel om op te noemen. Het is in ieder geval een grote, moeizame en kostbare stap om van prototype naar product te gaan. Maar goed, zover is het nog (lang) niet :).

Wat kan je verwachten van deze blogs?

In het kort, niks :). Ik ga het traject in zonder bestaande code of architectuur en ik wil dus alles zelf ontwerpen, programmeren en testen. Uiteraard ga ik ook gebruik maken van bestaande, open-source, free-to-use software. Dit gehele ontwikkelproces wil ik af en toe delen middels enkele blog-posts om anderen wellicht te inspireren. Het kan ook zijn dat ik juist een blog-post schrijf om mijn frustratie weg te typen of om advies te vragen (je bent nooit te oud om te leren :)). Omdat ik de meeste tijd wil besteden aan het project, heb ik niet de tijd om de gehele context met alle ins en outs te beschrijven. Het kan dus goed mogelijk zijn dat er bepaalde informatie afwezig is, welke cruciaal is voor de context of het begrip voor de gemaakte keuzes. In dat geval, op voorhand mijn excuses. Uiteraard kan ik later het een en ander nader toelichten mocht dat nodig zijn.

Dus: verwacht niks, maar mocht er een blog-post verschijnen dan hoop ik dat het interessant genoeg is om te lezen of vragen te stellen.

Tot zover de introductie, op naar het interessante gedeelte :P

Projectinformatie en huidige status

Een lijstje met hardware eigenschappen welke ik voor ogen heb voor dit project:
  • GPS voor snelheid, locatie, logging en navigatie
  • BT voor communicatie met sensoren (BLE) en een telefoon
  • ANT+ ondersteuning voor sensoren
  • Transflective color LCD met een nog onbekende resolutie
  • Micro SD-card interface voor kaarten, opgeslagen ritten en statistieken, etc
  • WiFi
  • Diverse interne sensoren zoals luchtdruk, temperatuur, etc
  • Er is nog meer maar dit schiet me zo ff te binnen
Omdat hardwareontwikkeling tijdrovend en duur is, ga ik eerst gebruikmaken van een ontwikkelbord in combinatie met externe modules zoals WIFI, BT (LE), ANT+, GPS en een LCD. Zo'n ontwikkelbord bevat verder vaak een RS232 poort, USB, EEPROM, I2C, GPIO, SDCARD, etc. en is dus ideaal om te gebruiken voor het ontwikkelen van een prototype.

Gezien de toepassing, een GPS fietscomputer met navigatie mogelijkheid, is een energiezuinige maar relatief krachtige microcontroller vereist. Omdat zo'n fietscomputer normaliter klein moet zijn, is het niet handig om een grote print met diverse externe componenten te hebben. Integratie is key en daarom kies ik voor een microcontroller met geÔntegreerde FLASH, RAM en diverse periferie. Daarvan zijn er tig op de markt, maar aangezien ik de meeste ervaring heb met ARM ťn omdat ik toevallig al een ARM ontwikkelbord heb liggen, valt voorlopig mijn keuze op een Cortex M3 van ST. Een STM32F207 om precies te zijn. Deze microcontroller heeft de mogelijkheid om extern geheugen aan te spreken mocht de hoeveelheid interne RAM niet voldoende zijn.

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

De software, volledig geschreven in C, wordt gelaagd ontwikkeld met diverse abstracties. Dit werkt hergebruik van software componenten in de hand en maakt het relatief eenvoudig om later voor een meer performante microcontroller te kiezen, mocht dat nodig zijn.

De eerste stap was het inrichten van de ontwikkelomgeving waar ik nu bijna klaar mee ben. De gebruikte tooling is allemaal freeware: Gevirtualiseerde Linux als ontwikkel-host, Eclipse voor de IDE, ARM cross compiler voor compilatie en linken, MAKE/MAKEFILE voor de build environment en GIT als VCS. Voor code documentatie maak ik gebruik van Doxygen terwijl ik LibreOffice gebruik voor mijn design documentatie.

Omdat ik bottom-up ga ontwikkelen is de volgende stap het ontwerpen en programmeren van de boot loader. Dit is een cruciaal stukje software welke diverse verantwoordelijkheden en mogelijkheden krijgt. De boot loader maakt het tevens voor mij mogelijk om snel nieuwe firmware te flashen zonder gebruik te maken van speciale apparatuur.

Boot loader – High Level

De boot loader is het eerste stukje software wat uitgevoerd gaat worden als de microcontroller spanning krijgt. Deze heeft daarom een primaire taak om de microcontroller en eventuele andere componenten zoals het externe geheugen te initialiseren. Na deze initialisatie geeft de boot loader de controle over aan de applicatie.

In veel deep-embedded applicaties wordt de boven genoemde initialisatie uitgevoerd door de applicatie zelf en is er geen boot loader. Dat is een ontwerpkeuze maar in mijn ogen niet altijd de juiste. Waarom? Wel, een boot loader biedt namelijk vele mogelijkheden:
  • Het voorkomen van het bricken van een product als er een firmware update uitgevoerd wordt
  • Hardware en platform abstractie van een of meerdere componenten en periferie
  • Eenvoudige manier om een nieuwe applicatie firmware te flashen
  • Een applicatie interface beschikbaar stellen voor primaire functies (soort van BIOS)
  • Data 'hiding' door bepaalde data alleen via een interface beschikbaar te stellen aan de applicatie (secure firmware update, device profile data, key management, hardware revisie, etc)
  • Etc.
Het is eerlijk om ook enkele nadelen te noemen:
  • Start proces is wat complexer
  • Boot loader neemt wellicht kostbare FLASH ruimte in
  • Opstarttijd kan wat langer zijn (maar scheelt vaak maar enkele ms)
  • Build systeem is wat complexer (aparte linker files, makefiles, etc)
  • Van tevoren goed nadenken over de geheugenindeling
Doordat het hebben van een boot loader echter zoveel meer mogelijkheden biedt, wegen de nadelen niet op tegen de voordelen. Een boot loader gaat er dus komen :-)

Omdat de boot loader en applicatie apart gelinkt worden zijn het twee aparte entiteiten – ze delen geen object code –. Om ervoor te zorgen dat ze elkaar niet in de weg gaan zitten wanneer ze gedeelde resources zoals het geheugen aanspreken, moet van tevoren goed nagedacht worden hoe de boot loader en applicatie gebruik gaan maken van deze resources. Ook moet duidelijk zijn hoe beide images opgeslagen worden in het FLASH geheugen.

Voor mijn ontwikkelplatform gaat die geheugenindeling er voorlopig als volgt uitzien:

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

Ik begrijp dat het bovenstaande plaatje wellicht enige uitleg behoeft.

De boot loader en applicatie hebben ieder een aparte sectie gereserveerd gekregen in het interne FLASH geheugen van de microcontroller. Deze secties liggen vast en zijn gedefineerd in de twee zelfgeschreven linker files. Omdat de boot loader in principe nooit geŁpdatet wordt in het veld, moet goed nagedacht worden over deze secties. Een te grote boot loader sectie zorgt ervoor dat er minder over blijft voor de applicatie sectie. Omgekeerd hetzelfde. Een goede balans vinden is dus cruciaal om eventuele problemen in de toekomst te voorkomen.

De boot loader heeft een klein gedeelte genaamd “BL API”. Dit is tabel met functie-pointers welke verwijzen naar functies in de boot loader code. Dit gedeelte staat altijd aan het eind van de boot loader sectie, ongeacht de lengte van de boot loader code. De ruimte tussen de “BL API” en de boot loader code wordt opgevuld met filler bytes zoals 0xFF. Door het op deze manier te implementeren 'weet' de applicatie altijd de “BL API” te vinden ongeacht de werkelijke grote van de boot loader code. De applicatie hoeft dan ook niet geŁpdatet te worden als de boot loader geŁpdatet wordt.

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

De applicatie kan gebruik maken van de “BL API” om bepaalde services van de boot loader aan te spreken. Denk dan b.v. aan asymmetrische decryptie (handtekening verificatie), toegang tot (gedeelde) externe permanente opslag zoals een EEPROM – hardware abstractie voor de applicatie –, uitlezen van de hardwarerevisie, producttype, het uitvoeren van een firmware update (FWU), toekennen van boot loader extensies, etc. Deze tabel is opgeslagen in de boot loader sectie maar zowel de boot loader linker file als de applicatie linker file bevatten verwijzingen er naartoe.

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

Wat nog opvalt aan de boot loader en applicatie images zijn de IVT's (Interrupt Vector Tabellen). Elk image heeft zo'n tabel. In deze tabel staan functie-pointers naar ISR (Interrupt Service Routine) functies voor de relevante (externe) interrupts. Ook de reset-vector staat erin. Omdat zowel de boot loader als de applicatie hun eigen interrupt handlers hebben, krijgen ze dus ook ieder een eigen IVT. Voordat de boot loader de applicatie image aanroept, wordt de microcontroller eerst geherconfigureerd zodat deze de applicatie IVT gebruikt als er zich een (externe) interrupt voordoet.

Als laatste de check-sums. Zowel de applicatie image als de boot loader image hebben een check-sum veld. Deze check-sums worden berekend na het genereren van de binaries door het build systeem om vervolgens eraan toegevoegd te worden. Het doel ervan is om tijdens het booten te kunnen bepalen of er code-corruptie is ontstaan in ťťn of beide images. Het kan namelijk voorkomen dat er door b.v. straling ťťn of meerdere bits omvallen in het flash geheugen. Dit kan zich dan uiten in willekeurig gedrag van de microcontroller of zelfs een complete crash. De boot loader berekent run-time de check-sums van zichzelf en de applicatie image om deze vervolgens te vergelijken met de opgeslagen check-sums. Zijn ze hetzelfde dan gaat boot proces verder. Zijn ze niet hetzelfde, dan stopt het boot proces en wordt er een error gegenereerd. Mocht de applicatie image corrupt zijn dan kan de gebruiker deze opnieuw flashen. Is de boot loader echter corrupt, dan houdt het op.

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

Nu het RAM gedeelte. Bovenaan is een stukje “exclusief BL” te zien. Dit is een gedeelte van het RAM geheugen wat exclusief gereserveerd is voor de boot loader. Het is de bedoeling dat de applicatie hier niet van leest of in schrijft – echter kan de gekozen microcontroller dit niet hardwarematig voorkomen (geen MMU) –. Dit stukje RAM geheugen wordt middels een linker file geconfigureerd. De boot loader gebruikt dit om tijdelijke runtime data in op te slaan als de applicatie gebruik maakt van de BL API – de zogenaamde 'statische'-variabelen –.

Als laatste het mirrored RAM gedeelte. Normaliter is deze 'mirror' mode niet nodig, ook niet voor een standaard applicatie firmware update (FWU). Moet echter de boot loader geŁpdatet worden, dan moet deze speciale mogelijkheid wel aangesproken worden. Het is een optie van de STM32F2xx en deze maakt het mogelijk om het interne RAM geheugen te mappen op dezelfde adressen als het interne FLASH geheugen – dit ligt iets genuanceerder maar voor deze blog is het voldoende –. Dit maakt het mogelijk voor de boot loader om eerst zichzelf te kopiŽren naar het interne RAM geheugen om vervolgens de mirror-mode aan te zetten. Executie van de boot loader gaan dan onverstoord vanuit het RAM geheugen verder. Hiermee komt het interne FLASH beschikbaar om gewist en opnieuw geprogrammeerd te worden. Op deze manier kan je dus zowel de boot loader als de applicatie opnieuw flashen. Merk op dat wanneer de boot loader opnieuw geflashed wordt, er wel een kans op bricken bestaat.

Wat verder….

Zoals ik al eerder aangegeven heb, is mijn ontwikkelomgeving en build systeem op orde en kan de volgende stap genomen worden. Deze stap behelst het creŽren van de linker-files en het opzetten van de boot loader architectuur. Mijn volgende blog gaat dan ook wat dieper in op de linker-files en hoe deze gebruikt worden in zowel code als de linker. Tevens zal ik wat dieper ingaan op de boot loader architectuur en de eerste resultaten van de implementatie.

Slot

Uiteindelijk is het toch meer type-werk geworden dan gedacht maar desalniettemin hoop ik dat het (een beetje) interessant was. Mijn streven is om de volgende keer meer plaatjes en minder tekst te plaatsen :)

Anyway, bedankt voor het lezen.

References en links:
Eclipse C/C++
ARM cross compiler
GIT
STM32F2xx BSP+FreeRTOS+CMSIS (STM cube)
STM32F2xx reference manual – Cortex M3 reference manual