Tutorial for creating multi cpp file arduino project

I can see where you are both coming from, but I tend to agree more with @bobcroft is saying… solely because of the “Why does it work on the Arduino IDE but doesn’t work with PlatformIO” question that gets thrown around, with the viewpoint of the poster that there must be something wrong with PlatformIO (understandable, considering their level of knowledge).

But the real question then is what to do about it, because something does need to be done… even if it is a page/post saying do this (read this link for more detail), do that (read that link to understand why), etc, etc… Because if that exists, it can be linked to the next time the question is inevitably asked, and anyone google searching will find it, and it might make more people make the jump to PlatformIO… always a good thing! But certainly, yes… it will never be ‘complete’ … as that would require a lot of nights of lost sleep, frustration, and result in a 300 page book on C++!! :laughing:

3 Likes

Ok, let’s give this a try…
After some googling I found this. It is based on the implementation of an arduino-capable VS-extension. First cite, no offense :stuck_out_tongue::

In a nutshell:

  • If you don’t want to learn about C++ declarations and header files, stay with .INO files
  • If you have a more complex sketch with many separate source files that use classes and methods from each other, you will have more control and overview if you use “classic” C++ .CPP and .H files

Nevertheless based on the implementation choosen in this extension you could mimic the standard Arduino-IDE behaviour by edediting files like this:

  1. rename your “main”-file with the setup and loop functions to *.cpp and add #include "Arduino.h" at the very top of this file
  2. rename all other *.ino-files that are part of your project to *.h
  3. At the end of your “main”-*.cpp-file write #include "foo.h", #include "bar.h", etc. for every *.h-file you have where foo and bar are the actual names of the needed files
  4. create function prototypes in a seperate file. To do this create a new file called prototypes.h and add a prototype for each function defined in one of your other *.h-files there.
  5. include the created prototypes.h (#include "prototypes.h") at the top of your “main”-*.cpp-file right below #include "Arduino.h"

Now you are ready to go…
Example:

main.ino:

void setup() {
    ;
}

void loop() {
    ;
}

library.ino:

int add(int a, int b) {
    return (a+b);
}

Those get turned into
main.cpp:

#include "Arduino.h"
#include "prototypes.h"

void setup() {
    ;
}

void loop() {
    ;
}

#include "library.h"

library.h:

int add(int a, int b) {
    return (a+b);
}

prototypes.h:

int add(int a, int b); // This is a function prototype - basically the function-head without a {}-block ended by a semicolon, you need one of those for each function you want to use

I really want to point out that this style isn’t particulary beautiful… no, it really is bad, very bad as it could cause a lot of errors… but it seems this is the “easy” way to convert a *.ino-project quickly.

  • Why does this work?
    #include is a so called preprocessor directive. Before you code gets translated to something your Arduino can work with (by a compiler and a linker), the preprocessor does some simple stuff (like text replacement). By telling the preprocessor to include something (everything in your source-code beginning with an # is a preprocessor directive) you basically tell him to simply copy/insert the text from the included file at that point. The file created is simply a text-file with all the included files copied together into one…
  • Why do you need those strange function prototypes?
    The compiler translating your sourcecode works top-to-bottom. When you include the *.h at the end of your “main”-file the moment you might call the function add() in your loop the compiler doesn’t know there is a add()-function defined; you get an error. To prevent that you have a prototype at the top so the compiler knows that somewhere there is a definition of that function and he doesn’t care about it anymore, no matter where the actuall definition is.
void add(int a, int b); // A function prototype or declaration to "please the compiler"

void add(int a, int) { // The actual function definition with the logic
    return (a+b);
}
  • Why not just #include all those *.h-files at the top of my “main” so I don’t need prototypes?
    Well, idk, this is a bad style in eather way, I just stuck with the explenation given here. Just make sure you include other files AFTER the Arduino.h so all following files inserted “have access” to it.

In the end I still recommend:
Just do it the proper way and learn splitting into multi-file projects the “normal” C/C++ style; it’s not even that big of a step from what I just wrote… You can not just place functions in other files but global variables and classes too, not to mention that it provides a way clearer structure for large projects, maybe even projects with multiple people.

10 Likes

Is there any movement on this? I am really new to PlatformIO and I am looking for this exact type of tutorial with no luck. I have read the links that were posted here, but it didn’t help me figure out the problem.

I find the tutorial by @Thomseeen above to be well-explained and the links (example, example) helpful. If you didn’t understand something specifically in these you should ask about that.

It might also be possible that your problem is of an entirely different nature. If you feel like that, you need to post a new topic.

2 Likes

I know it may seem that it is well explained from your perspective, but it hasn’t answered the OP’s call. Is there a tutorial for taking an existing Arduino project with or without multiple files and importing it into platformio? Something along the lines of “here is the source code”, “here is how you import it”, “here is the button you click”. All of the links provided do not show how they are implemented in PlatformIO. I am looking for the equivalent of “blink” with multiple files.

1 Like

Because it’s not PlatformIO-specific, it’s only related to C++. What PIO / VSCode gives you is a “new file” button and a source folder. I can show you a blink example with multiple files.

src\main.cpp

#include <Arduino.h>
#include "blinky.h"

void setup() {
   blinky_init();
}

void loop() {
  do_blink(500);
}

src\blinky.h

#ifndef BLINKY_H /* include guards */
#define BLINKY_H

/* blinky initialization function */
void blinky_init();

/* blinks the LED once for an length of <ms> milliseconds. */
void do_blink(int ms);

#endif /* BLINKY_H */

src\blinky.cpp

#include "blinky.h"
#include <Arduino.h> /* we need arduino functions to implement this */

void blinky_init() {
   pinMode(LED_BUILTIN, OUTPUT);
}

void do_blink(int ms) {
  digitalWrite(LED_BUILTIN, LOW);
  delay(ms);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(ms);
}
5 Likes

Yes it does. Simply add more files in the respective src and include dirs.

The way I split code in Platformio is:

create:

  • code.h
  • code.cpp

code.h:

// pragma once prevents the file to be included twice
#pragma once
// #include ...
// all other includes stays here in the .h file

// vars need to be extern
extern String myName;

// prototypes don't need named attributes in the .h file
void sayMyName(String);

code.cpp:

// the only include in the .cpp file
#include "code.h"

String myName = "John";

void sayMyName(String name){

  Serial.print(name);
}

main.cpp

#include <Arduino.h>
#include "code.h"

void setup(){
  
  sayMyName(myName);
}

void loop(){

}
3 Likes

Hi, It’s really nice but… How we can use it when we try to work with external libraries at main which must be used by splited files?

I’m not sure what you mean by that. It means the external lib has some header file and it needs to be used in multiple source files in the src/ folder? Can you give a concrete example?

Buenas noches.

Creo que lo que intenta decir Poleg, es algo parecido a lo que me pasaba a mí, cuando hice la transformación de los programas del ROV, del entorno Arduino al entorno VSC, que hay bastantes librerías que se utilizan con normalidad en el entorno Arduino, que aun no están convalidadas a VSC + PIO. Sin embargo Poleg ya tiene los archivos divididos.

Supongo que quiere decir, que tiene que haber una manera de decirle al programa que busque las librerías en alguna carpeta, incluso cuando el programa está dividido.

Maxgerhardt, me dio la solución, para poder utilizar las bibliotecas de Arduino, pero mi programa no estaba dividido.

Si Poleg pudiera ser un poco más preciso, e indicar con que librerías concretas tiene problemas, a lo mejor sería más fácil poder ayudarle.

Esta es la respuesta que me diste, supongo que abra que adaptarla a cuando un programa ya esta dividido.

Blockquote

maxgerhardt

3d

asesorplaza1:

src \ main.cpp: 154: 54: error fatal: PS2X_lib.h: No existe tal archivo o directorio

La biblioteca PS2X aún no está registrada en PlatformIO: le sugiero que vaya a GitHub - simondlevy/PS2X_lib: Arduino Playstation2 library with compiler warnings fixed , descargue el archivo ZIP ( https://github.com/simondlevy/PS2X_lib/archive/master.zip ) , a continuación, crear una nueva carpeta en la lib/ llamada PS2X_lib donde se coloca en los archivos del archivo ZIP de modo que la estructura es …

lib/
   PS2X_lib/ 
      PS2X_lib.cpp
      
PS2X_lib.h
      ...

luego agrega la línea

lib_deps = 
   PS2X_lib

en el platformio.ini para que PlatformIO busque la biblioteca.

Blockquote

Un saludo.

Good night.

I think what Poleg is trying to say is something like what happened to me, when I did the transformation of the ROV programs, from the Arduino environment to the VSC environment, that there are quite a few libraries that are used normally in the Arduino environment, which are not yet validated to VSC + PIO. However Poleg already has the files split.

I suppose it means, there has to be a way to tell the program to look for libraries in some folder, even when the program is split.

Maxgerhardt, gave me the solution, to be able to use the Arduino libraries, but my program was not divided.

If Poleg could be a little more accurate, and indicate which specific libraries have problems with, it might be easier to help you.

This is the answer you gave me, I suppose I open it to adapt it to when a program is already divided.

maxgerhardt

3d

asesorplaza1:

src\main.cpp:154:54: fatal error: PS2X_lib.h: No such file or directory

The PS2X library is not yet registered with PlatformIO – I suggest you go to GitHub - simondlevy/PS2X_lib: Arduino Playstation2 library with compiler warnings fixed, download the ZIP file (https://github.com/simondlevy/PS2X_lib/archive/master.zip), then create a new folder in lib/ called PS2X_lib where you put in the files from the ZIP file so that the structure is …

lib/
   PS2X_lib/ 
      PS2X_lib.cpp
      PS2X_lib.h
      ...

then add the line

lib_deps = 
   PS2X_lib

into the platformio.ini so that the library is looked for by PlatformIO.

Blockquote

Greetings.

Pues os comento, el proyecto en sí es un combinación de usos para la utlización del MFRC522 (Lector RFID)

La idea es tener el main en el que estén el setup y el loop. En el loop aparece la petición de modo de trabajo, y tras la introducción por parte del usuario se hace la llamada a uno de los archivos divididos.

El problema es que los objetos creados en el main y deben usarse en los dividos da error

So you have created global variables in your main.cpp and need to use them in your solucion1.cpp.

Your problem is that these global variables are not known to your main.cpp and also that you #include a CPP FILE in another cp file. That will lead to double-definition behaviours.

To resolve it, do:

  • remove #include solucion1.cpp from main.cpp
  • create the new file src/global_vars.h with the content
#ifndef GLOBAL_VARS_H
#define GLOBAL_VARS_H

/* types for global vars */
#include <MFRC522.h>
/* global vars */
extern MFRC522 rfid;
extern bool momento;
extern bool contador1;

#endif
  • in solucion1.cpp, add a #include "global_vars.h"

Now your global variables have been declared extern in a header file which every other cpp file can include.

This is the exact same technique as outlined in all the posts above.

1 Like

Mmmmmm I thought in that, but I didn’t know the concept “extern” thanks a lot…

But, where I initialize extern variables and objets?

And… I should make an #include "solucion1.cpp" at main? Because I need some functions that are coded there

You do not include the implementation (the source code which implements the function), only the function or variable declaration. So no #include "solucion1.cpp".

In one .cpp file only. The extern keyword makes a simple declaration “the exists a variable of this type and name somewhere”. It doesn’t create the variable or define its value. In fact, if you declare the existence of a variable which is never defined, you get an undefined reference error.

See e.g. variable declaration - When to use extern in C++ - Stack Overflow

2 Likes

So how I should do it? I cant see it, sorry

The post above shows you how to create a header file which declares all your global variables and use them in your other cpp file. That’s the solution.

1 Like

ok, thanks for your time

Hi alll, i’m quite a beginner in programming world; i would like to show here an example code to understand if i have well learned splitting principles:
main.cpp

#include "Arduino.h"
#include "main.h"

int delay_ms= 250;

// Set LED_BUILTIN if it is not defined by Arduino framework
// #define LED_BUILTIN 13

void setup()
{
  // initialize LED digital pin as an output.
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop()
{
  blink();
}

main.h

#ifndef MAIN_H
#define MAIN_H

/* types for global vars */

/* global vars */

extern int delay_ms;

// prototypes don't need named attributes in the .h file

void blink();

#endif

blink.cpp

#include "Arduino.h"
#include "main.h"

void blink(void)
{
  // turn the LED on (HIGH is the voltage level)
  digitalWrite(LED_BUILTIN, HIGH);

  // wait for a second
  delay(delay_ms);

  // turn the LED off by making the voltage LOW
  digitalWrite(LED_BUILTIN, LOW);

   // wait for a second
  delay(delay_ms);
}

This code works but i have a doubt: is it all ok? Syntax and form are correct? There is something that can be done in a better way?