[ 365 keer bekeken / views ]
Voor een vriend heb ik hardware ontwikkeld om de vloerverwarming van zijn huis effectiever en efficiënter te regelen. Om deze hardware te kunnen testen schreef ik software met de Arduino IDE. Voor mij de enige manier die ik kende.
Here you can find an English version of this post
Deze vriend, een professionele software engineer, ging met mijn software aan de haal om de functionaliteit aan te passen aan zijn wensen en eisen. Maar door zijn ervaring heeft hij nooit aan de manier waarop je met de Arduino IDE software ontwikkelt kunnen wennen. Voor hem was het allemaal erg “on-natuurlijk”.
Op enig moment ontdekte hij Visual Studio Code en met die IDE in samenwerking met de “PlatformIO” extensie kon hij software voor zijn meet-en-regel-systemen ontwikkelen op een manier die voor hem wél “natuurlijk” was.
Ik heb ook naar Visual Studie Code met de PlatformIO extensie (vanaf nu gewoon “PlatformIO” genoemd) gekeken maar liep al snel vast door de vrij stijle leer curve.
Uiteindelijk heb ik, voor nieuwe projecten, de overstap toch gemaakt en ben érg enthousiast geworden. Ook omdat ik Visual Studio Code (VSC) nu niet alleen gebruik voor het ontwikkelen van software voor MCU’s maar ook voor de ontwikkeling van Python en openSCAD programma’s. Alles dus met één IDE (met verschillende “extensies”) wat de productiviteit ten goede komt omdat je niet steeds van IDE hoeft te wisselen.
Eén van de grootste problemen die ik met de Arduino IDE en Arduino projecten heb, is dat het lastig is om vast te leggen welke versie van bibliotheken je voor een project gebruikt en wat voor type MCU met de daarbij horende instellingen je moet selecteren om een project te kunnen compileren en flashen. Omdat ik veel projecten op deze website publiceer en deze nagebouwd worden door meer- of minder ervaren makers blijkt toch iedere keer dat ik zaken niet goed (genoeg) heb beschreven en/of dat makers niet het geduld hebben om alles goed te lezen. Maar een groter probleem is dat in de loop der tijd core software en bibliotheken óók vernieuwd worden en dat core software opeens hele andere instellingen en mogelijkheden krijgt (die nu niet meer aansluiten bij mijn beschrijving) en dat nieuwere bibliotheken niet altijd compatibel zijn met de versie die ik voor het project heb gebruikt.
En daar heeft PlatformIO een briljante oplossing voor door voor ieder project een ‘platformio.ini’ bestand te gebruiken waarin al dit soort zaken vastgelegd zijn. Het platform (in Arduino taal de gebruikte core), het type board (NodeMCU, Arduino UNO, ATtiny85 enz.) en welke libraries gebruikt worden in een project ligt vast in het platformio.ini bestand waarbij je ook nog kunt opgeven dat je een specifieke versie van een library wilt gebruiken. Het “platformio.ini” bestand is onderdeel van het hele project waarbij je ook nog eens niets zelf hoeft te installeren of uit te zoeken. Eenvoudig een project folder openen en compileren zorgt ervoor dat alle benodigde (externe) core-bestanden en bibliotheken met de goede versie worden gedownload (dat gaat overigens razend snel en hoeft alleen de eerste keer dat je een project compileert te gebeuren).
Voorbeeld van een “platformio.ini” bestand
; PlatformIO Project Configuration File ; ; Build options: build flags, source filter ; Upload options: custom upload port, speed and extra flags ; Library options: dependencies, extra library storages ; Advanced options: extra scripting ; ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html [platformio] workspace_dir = .pio.nosync default_envs = myBoard [env:myBoard] platform = atmelavr board = uno upload_protocol = usbtiny framework = arduino monitor_speed = 19200 upload_speed = 19200 upload_port = #select port like "/dev/cu.usbserial-3224144" build_flags = -D DEBUG lib_ldf_mode = deep+ lib_deps = marcoschwartz/LiquidCrystal_I2C@^1.1.4 chris--a/Keypad@^3.1.1 monitor_filters =
Nu ik PlatformIO voor nieuwe projecten gebruik wil ik mijn bestaande Arduino projecten het liefst óók omzetten naar de PlatformIO structuur. Maar dat blijkt minder eenvoudig dan je zou denken (of hopen).
Er zijn op internet wel handleidingen die dit proces beschrijven, maar deze zijn vaak onvolledig en gaan bijna allemaal uit van een eenvoudig project met maar één ‘.ino’ bestand.
Ik dacht: dat moet makkelijker kunnen.
Om een Arduino project om te kunnen zetten naar een PlatformIO structuur is het belangrijk te begrijpen hoe de Arduino IDE “werkt” en wat deze IDE doet om een project te compileren.
De Arduino IDE doet achter de schermen (of onder de motorkap) een aantal zaken met de ‘.ino’ bestanden waar het project uit bestaat.
Samenvoegen van .ino Bestanden
De Arduino IDE begint het compilatieproces door alle ‘.ino’ bestanden in je project achter elkaar te plakken tot één groot gecombineerd bestand. Dit gebeurt in alfabetische volgorde van de bestandsnamen. Stel je voor dat je drie bestanden hebt: ‘mijnProgramma.ino’, ‘sensor.ino’ en ‘display.ino’. De IDE voegt deze samen in de volgorde ‘display.ino’, ‘mijnProgramma.ino’ en ‘sensor.ino’. Dit betekent dat de volgorde van de bestanden invloed kan hebben op het gedrag van je code, vooral als functies in verschillende bestanden elkaar aanroepen.
Aanmaken van Functieprototypes
Nadat de bestanden zijn samengevoegd, scant de Arduino IDE het gecombineerde bestand op zoek naar alle functies die je hebt gedefinieerd. Vervolgens maakt de IDE automatisch prototypes voor deze functies en zet deze bovenaan het gecombineerde bestand. Een functieprototype is een declaratie van een functie die aangeeft wat voor type waarde de functie teruggeeft en welke argumenten de functie verwacht, zonder de volledige functie-implementatie. Dit stelt de compiler in staat om functies aan te roepen die verderop in het bestand gedefinieerd zijn, wat normaal gesproken niet mogelijk zou zijn zonder prototypes (standaard kan in C of C++ een functie pas gebruikt worden als hij vóór de aanroep gedefinieerd is).
Globale variabelen
Als je een variabele buiten een functie declareert (dus niet binnen de accolades ‘{}’ van een functie), dan is, door het samenvoegen van alle ‘.ino’ bestanden door de Arduino IDE, deze variabele globaal en beschikbaar voor alle functies in alle samengevoegde ‘.ino’ bestanden. Bijvoorbeeld, een variabele ‘int sensorValue;’ die in ‘sensor.ino’ wordt gedeclareerd, kan zonder problemen worden gebruikt in ‘mijnProgramma.ino’ of ‘display.ino’.
Inclusie van Bibliotheken met ‘#include’
Bij het gebruik van externe bibliotheken in een Arduino-project moet je deze importeren met de ‘#include’ directive. Als je een bibliotheek in één ‘.ino’ bestand importeert, geldt dit, na samenvoeging door de IDE, voor het hele gecombineerde bestand.
Dubbele ‘#include’ directives
Het is niet noodzakelijk, maar ook niet schadelijk, om dezelfde ‘#include’ directive in meerdere ‘.ino’ bestanden te plaatsen. De compiler zorgt ervoor dat een bibliotheek maar één keer wordt toegevoegd, zelfs als de ‘#include’ directive, door het samenvoegen van alle ‘.ino’ bestanden, meerdere keren voorkomt. Dit komt doordat de meeste bibliotheken zichzelf beschermen tegen dubbele inclusie door middel van ‘#ifndef’ en ‘#define’ (zogenaamd ‘Header Guards’) directives in hun header bestanden.
Compilatie als één Programma
Nu de bestanden zijn samengevoegd en de prototypes zijn aangemaakt, behandelt de IDE het resultaat als één enkel bestand. Dit betekent dat de code nu wordt gecompileerd alsof het een enkel ‘.ino’ bestand is. De compiler doorloopt de code, vertaalt deze naar machine-instructies en controleert op syntaxis- en semantische fouten. Omdat alles nu in één bestand staat waarbij alle functies als “prototype” aan het begin van het samengevoegde bestand staan, kunnen functies in verschillende bestanden probleemloos met elkaar communiceren, zonder dat de gebruiker zich zorgen hoeft te maken over de volgorde van de functie-definities.
En hoe ziet een PlatformIO project er dan uit?
Als je een project start met PlatformIO, krijg je een gestructureerde map waarin je al je code, instellingen en andere bestanden kunt opslaan. Deze structuur helpt je om overzicht te houden, vooral als je project groter wordt.
De project folder bestaat minimaal uit een ‘platformio.ini’ bestand en twee folders waarin alle code staat. Deze folders zijn “src” waar alle ‘.c’ en ‘.cpp’ bestanden van het project staan en “include” waarin alle header bestanden van het project staan.
Het ‘platformio.ini’ bestand
Dit bestand is als de “gebruiksaanwijzing” voor je project. Hierin staat welke microcontroller je gebruikt, welke bibliotheken nodig zijn, en hoe alles geconfigureerd moet worden.
De “src” map
Dit is de plek waar je de belangrijkste code van je project bewaart. Hierin staan bestanden met de extensie ‘.c’ voor C code en ‘.cpp’ voor C++ code.
De naam “src” staat voor “source”, wat betekent dat dit de broncode van je project is.
Omdat PlatformIO niet, zoals de Arduino IDE doet, alle code bestanden achter elkaar zet is ieder ‘.c’ of ‘.cpp’ bestand een afzonderlijk object. Zonder speciale instructies weten deze zelfstandige objecten niets van elkaar.
De “include” map
In deze map bewaar je bestanden met “voorbereidende” code, ook wel header-bestanden genoemd. Een header-bestand wordt meestal gebruikt om de “interface” van je code tussen verschillende objecten te definiëren, vooral als je functies, variabelen of klassen in andere bestanden wilt gebruiken. Header bestanden hebben de extensie ‘.h’.
Voorbeeld van hoe zo’n project er in PlatformIO uitziet
Stel je voor dat je een project hebt genaamd ‘MyProject’. Dit is hoe de structuur er dan uit zou kunnen zien:
‘main.cpp’ bevat de code van je project.
‘my_header.h’ is een header-bestand met code die je op meerdere plekken in je project kunt gebruiken.
‘platformio.ini’ is het bestand dat PlatformIO de instructies geeft over hoe het project moet worden gebouwd en uitgevoerd.
Alles wat er in ‘my_header.h’ wordt gedefinieerd of vastgelegd komt beschikbaar of is van invloed op alle andere .c of .cpp bestanden die dit header bestand via de ‘#include “my_header.h”
’ invoegen.
Het conversie programma
Nu we weten wat de Arduino IDE doet om een complex project te compileren én we weten hoe een project er in PlatformIO uit ziet moet het mogelijk zijn om hiervoor een automatisch conversie programma te schrijven. Ik zet op een rijtje welke stappen dit conversie programma moet doorlopen om deze taak uit te kunnen voeren.
Stap 1
In deze stap moet de directory structuur voor een PlatformIO project worden opgezet. Ik heb ervoor gekozen om in een Arduino project (waarvan de top-directory altijd dezelfde naam heeft als het “hoofd” .ino bestand) een nieuwe map “PlatformIO” aan te maken. Onder deze map komt een tweede map met de naam van het arduino project. Zie onderstaande plaatje van het originele Arduino project “pulsGenerator”:
Zoals je kunt zien bestaat dit project uit drie ‘.ino’ bestanden. In de Arduino IDE ziet dat er zo uit:
Goed om te weten is dat de Arduino IDE alle bestanden die niet de extensie ‘.ino’, ‘.c’, ‘.cpp’ of ‘.h’ hebben negeert.
Het ‘README.md’ bestand die wel degelijk in de project directory staat wordt dus niet ‘gezien’ door de Arduino IDE. Hetzelfde geldt voor de map ‘PlatformIO’ die het conversie programma gaat aanmaken.
Na Stap 1 ziet de bestands structuur er zo uit:
- Dit is de genoemde PlatformIO directory (deze map en alle onderliggende mappen en bestanden zijn niet zichtbaar in de Arduino IDE)
- Dit is de PlatformIO project Folder
- De “src” en “include” folder en een template voor het platformio.ini bestand
Stap 2
In deze stap worden alle aanwezige ‘.ino’, ‘.c’ en ‘.cpp’ bestanden naar de “src” map gekopieerd en worden eventueel aanwezige ‘.h’ bestanden naar de “include” map gekopieerd.
Nu worden alle globaal gedefinieerde variabelen, constant pointers, functie definities en “#include” statements in tabellen gezet. De in de ‘.ino’ bestanden gevonden “#include” statements worden daar tot commentaar aangepast.
//#include <Wire.h> //-- moved to arduinoGlue.h //#include <LiquidCrystal_I2C.h> //-- moved to arduinoGlue.h
In eventueel al aanwezige header bestanden worden Header Guards aangebracht (als die niet al aanwezig zijn).
Vervolgens worden alle gegevens uit de eerder aangemaakte tabellen in een nieuw “arduinoGlue.h” bestand gestopt waarbij globale variabelen het voorvoegsel “extern” krijgen. Dit heeft hetzelfde effect als “prototypes”. De compiler weet dat zo’n variabel ‘ergens” gedeclareerd gaat worden maar hoeft niet te weten wáár die declaratie plaats gaat vinden. Dat is een taak die de “linker” gaat uitzoeken. Dit header bestand bevat nu dus alle gegevens voor de overige bestanden om foutloos te kunnen compileren.
#ifndef ARDUINOGLUE_H #define ARDUINOGLUE_H #define SETBIT(a,b) ((a) |= _BV(b)) #define CLEARBIT(a, b) ((a) &= ~_BV(b)) #define SET_LOW(_port, _pin) ((_port) &= ~_BV(_pin)) #define SET_HIGH(_port, _pin) ((_port) |= _BV(_pin)) #define pinA 8 // PB0 #define pinAbit 0 // PB0 #define pinB 9 // PB1 #define pinBbit 1 // PB1 #define pinA 11 // PB3 #define pinAbit 3 // PB3 #define pinB 12 // PB4 #define pinBbit 4 // PB5 #define POTMETER A0 // PC0 #define LED_PULSE_ON A1 #define LED_POTMETER A2 #define LED_SWEEPMODE A3 #define _CLOCK 16000000 #define _MAXFREQCHAR 20 #define _HYSTERESIS 5 //-- dict_all_includes --- #include <Wire.h> #include <LiquidCrystal_I2C.h> #include <Keypad.h> //-- dict_global_variables --- extern int32_t diffFrequency; //-- from pulsGenerator extern int32_t endSweepFreq; //-- from pulsGenerator extern uint8_t freqKeyPos; //-- from pulsGenerator extern volatile int32_t frequency; //-- from pulsGenerator extern char inputKey; //-- from pulsGenerator extern uint32_t ledBuiltinTimer; //-- from pulsGenerator extern int32_t newFrequency; //-- from pulsGenerator extern char newInputChar[_MAXFREQCHAR]; //-- from pulsGenerator extern uint16_t newPotValue; //-- from pulsGenerator extern uint16_t potSaved; //-- from pulsGenerator extern uint16_t potValue; //-- from pulsGenerator extern bool potmeterActive; //-- from pulsGenerator extern int32_t startSweepFreq; //-- from pulsGenerator extern float stepFrequency; //-- from pulsGenerator extern bool sweepModeActive; //-- from pulsGenerator extern uint32_t sweepTime; //-- from pulsGenerator extern uint32_t sweepTimer; //-- from pulsGenerator extern volatile int8_t togglePin; //-- from pulsGenerator //-- dict_prototypes --- //-- from displayStuff.ino ----------- void setupLCD(); void initLCD(); void updateLCD(); void easterLCD(); //-- from pulsGenerator.ino ----------- void explanation(); void readPotmeter(); void calculateSweep(); void sweep(); //-- from timer1Stuff.ino ----------- int32_t calculateTimer1(int32_t freqAsked, uint8_t &newTCCR1B); void setupTimer1(int32_t newFrequency); #endif // ARDUINOGLUE_H
Stap 3
In deze stap wordt voor alle ‘.ino’ bestanden een header bestand aangemaakt voorzien van Header Guards en een ‘#include “arduinoGlue.h”’ (dit gebeurt niet voor ‘.c’ en ‘.cpp’ bestanden omdat deze, als ze in het originele Arduino project gebruikt worden, al een ‘.h’ bestand moeten hebben).
Stap 4
In het “hoofd-project” bestand (in ons voorbeeld “pulsGenerator.ino”) wordt een “#include” statement voor alle andere header bestanden ingevoegd zodat de ‘.cpp’ objecten met dezelfde naam worden ingevoegd. In dit voorbeeld project zijn dat:
#include “arduinoGlue.h” #include “displayStuff.h” //-- voor ‘displayStuff.cpp’ #include “timer1Stuff.h” //--voor ‘timer1Stuff.cpp’
Stap 5
Nu worden alle ‘.ino’ bestanden in de “src” directory hernoemd tot ‘.cpp’ bestanden.
Na de conversie ziet de directory structuur er zo uit:
Wat rest
Wat rest en niet geautomatiseerd kan worden is het aanpassen van het ‘platformio.ini’ bestand.
Waar kun je het conversie programma vinden?
In deze repo staat het “arduinoIDE2platformIO.py” conversie programma. In deze repo staan in de map “testproject” twee voorbeeld Arduino projecten die je, als test, met dit programma kunt omzetten in een PlatformIO project.
En wat als je van een PlatformIO project een Arduino IDE versie wilt maken?
Die conversie is een stuk eenvoudiger. In deze repo vind je het “platformIO2arduinoIDE.py” programma die dit automatisch voor je doet!