Conversie Arduino- naar PlatformIO-project

[ 388 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:

  1. Dit is de genoemde PlatformIO directory (deze map en alle onderliggende mappen en bestanden zijn niet zichtbaar in de Arduino IDE)
  2. Dit is de PlatformIO project Folder
  3. 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!

This entry was posted in Aandewiel, Arduino, Computer, Firmware, Scripts and tagged , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

The maximum upload file size: 4 MB. You can upload: image, other. Links to YouTube, Facebook, Twitter and other services inserted in the comment text will be automatically embedded. Drop file here

This site uses Akismet to reduce spam. Learn how your comment data is processed.