Un sketch, plusieurs fichiers ?

Bonjour
J'ai un sketch un peu gros que j'aimerais diviser en plusieurs fichiers. J'ai essayé de mettre en pratique le tuto d'Eskimon, mais je n'arrive pas à compiler : les variables globales ne sont pas reconnues, les type de variables ne sont pas reconnus.
Pouvez-vous m'indiquer un tuto vraiment bien expliqué, qui mentionne les points durs, en anglais ou en français? Merci

pour tes variables globales, tu déclares :

  • dans le fichier .c principal:
int Machin;
float Truc;
  • dans les autres fichiers .c
extern int Machin;
extern float Truc;

Si tu as beaucoup de variables globales, tu peux organiser le truc.
Tu déclares toutes tes variables globales dans un fichier globales.h, de cette façon:

/* mes variables glolales */
EXTERN  int Etat;
EXTERN  float Valeur;
EXTERN  const char* Titre;
/* etc... */

Note bien que

  • j'ai écrit EXTERN en majuscules,
  • conseil : mes variables globales commencent toutes par une majuscule, ce n'est pas obligatoire, c'est juste une méthode pour aider le programmeur (c-àd : toi)

Puis dans ton fichier principal, tu mets:

#define EXTERN
#include "globales.h"

et dans tes autres fichiers .c ou .cpp, tu mets

#define EXTERN extern
#include "globales.h"

et le tour est joué !

1 Like

Bonjour,
intéressant biggil, merci !
cependant tu dis comment faire, mais comme cela doit être évident pour toi, tu n'expliques pas pourquoi.
AMHA il serait bien que tu mettes un petit commentaire à la fin des lignes pour que les débutants (et tant d'autres) comprennent le mécanisme

La 1ère façon de faire qui vient à l'esprit, quand on commence à diviser son logiciel en plusieurs fichiers sources, c'est de re-déclarer ses variables globales dans tous les fichiers C qui en ont besoin.
Là le compilateur va gueuler - en fait c'est le linker (éditeur de liens) qui va couiner : il va dire que des variables sont définies plusieurs fois, ce qui ne lui plaît pas.

Alors on ajoute les mot-clé extern devant les déclarations, ce qui veut dire au compilateur que la variable existe, qu'elle est connue, mais qu'il ne fait pas la créer.

Rapidement ça devient le souk : le programmeur recopie ses déclarations extern un peu partout, selon les besoins, commence par déclarer les globales dans le fichier principal, avec des extern dans les autres fichiers, puis c'est l'inverse (var. créée dans un fichier secondaire et mise en extern dans le principal), etc...

Or une bonne partie du succès d'une programmation complexe dépend de la capacité du programmeur à maintenir de l'ordre. D'où l'idée d'une méthode générale par traiter ce problème.

Ce qui se passe dans la méthode que je suggère :

  • dans le fichier principal, avant d'inclure le fichier globales.h qui contient les déclarations, on défiit la macro EXTERN comme ... rien du tout. Donc c'est comme si on n'avait pas écrit EXTERN dans les déclarations, et les variables sont déclarées pleinement. Le compilateur les crée.
  • dans les autres fichiers, on définit-la macro EXTERN comme valant "extern". Donc toutes les variables sont déclarées externes, et le compilateur ne le re-crée pas.

Un inconvenient de cette méthode est qu'on ne peut pas initialiser les variables globales. On ne peut pas écrire EXTERN int Etat = 0; car le comilateur n'acceptera pas cette ligne quand EXTERN signifie extern.

Bonsoir,

Il y a une autre façon de faire, mais elle n'est pas académique et ne va pas plaire aux puristes : découper le programme en 'modules' et les placer dans des fichiers .h qui contiennent les définitions des variables et le code.
Ensuite, il ne reste qu'à inclure les fichiers .h au bon endroit et dans le bon ordre. Si nécessaire, utiliser les 'prototypes' comme d'habitude pour éviter les références en avant.

Ce n'est pas académique, mais c'est simple et ça marche très bien :

  • Pas de problème avec les variables globales, pas besoin de macros pour ėviter les définitions/déclarations multiples,
  • Pas de problème avec les progmem, beaucoup plus difficiles à gérer que les globales en compilation séparée,
  • En prime, au total, il y a deux fois moins de fichiers qu'avec la méthode académique !

Désolé d'avoir choqué les puristes et bonne bidouille...

Microquettas

je dois dire que j'ai pas bien compris ...

Si tu places des définitions de fonctions dans les fichiers .h, et que tu inclus ces .h dans plusieurs fichier.c, le linker va couiner sur des déclarations multiples d'une même fonction.
Pareil avec les variables globales...

Dans la façon "puriste", si tout est bien fait, il n'y a besoin de se préoccuper de l'ordre d'inclusion des .h !

D'ailleurs la façon "puriste" n'est pas tirée d'un principe religieux ou d'un besoin de pureté dans l'absolu.
Elle résulte de l'expérience de milliers de gens, et a été adoptée comme la mieux adaptée à résoudre un maximum de problèmes.

Salut
Je ne sais pas si je vais mettre tout le monde d'accord mais voici la méthode que j'utilise.
Je précise que je suis développeur embarqué depuis 30 ans et que je développe aussi bien sur des STM32, des MSP, ou sur des applications LINUX, et aussi du kernel LINUX ou OpenBSD.

Imaginons un module funcs.

On écrit déjà le code (.c ou .ino dans votre monde).

#include "funcs.h"

int var1 = 10;
static int var2 = 100;

int funcMachin(void)
{
 return var1++;
}

Ensuite on écrit un header (.h)

#ifndef __FUNC__
#define __FUNC__

extern int var1;

int funcMachin(void);

#endif

Ensuite l'application principale.

#include "funcs.h"

void setup() {
  Serial.begin(115200);
}

void loop() {
  Serial.println(funcMachin());
  delay(1000);
}

Remarques:
Les variables externes sont déclarées dans le .h en "extern".
Le .h est protégé contre les inclusions multiples par une macro FUNC. Le fichier sera donc inclus une seule fois dans les .c
Le source funcs.c inclut funcs.h, ce qui permet au compilateur de vérifier la concordance des types et des paramètres.
L'application principale inclut également funcs.h, ce qui permet également au compilateur de faire les mêmes vérifications.
Vous remarquerez une variable statique var2 dans funcs.h. Elle ne figure pas dans funcs.h bien entendu. Elle appartient exclusivement à funcs.c

La méthode qui consiste à déclarer les extern dans chaque .c est à proscrire, car rien ne permet au compilateur de vérifier que ces déclarations sont identiques. Seule le linker pourra le faire, grâce aux signatures des variables et des fonctions. De plus, si vous changez le prototype d'une fonction ou le type d'une variable, il faut ensuite le faire dans chaque.c. Avec la méthode ci-dessus, on modifie le .c et le .h et c'est fini.

Je ne décris pas ici mes propres méthodes. Je ne fais qu'énoncer des principes qui paraîtraient évidents à n'importe quel développeur expérimenté.
Vous remarquerez facilement que les bibliothèques ARDUINO sont construites de la même manière.
Et si vous mettez le nez dans le kernel LINUX, vous verrez qu'il en est de même.
Je pars du principe que pour bien développer, il faut adopter les méthodes des meilleurs -> donc LINUX pour moi.

Cela dit, j'adore votre monde ARDUINO et c'est devenu le mien depuis quelques années.
Pour m'en convaincre il m'a suffit d'acheter un afficheur LCD, un capteur de température I2C, un module NRF24L01 et de réussir à mettre tout ça en route en moins d'une heure sur une breadboard. Tout ceci est rendu possible grâce au travail bénévole d'un tas de gens qui travaillent bien et fort.

En espérant ne pas vous avoir trop ennuyés ni exaspérés.
merci @+

On peut corser la chose avec deux module sur deux niveaux et un peu de c++ :
un module funcs, bas-niveau
un module anotherfunc, niveau intermédiaire, qui utilise le premier.
Une application qui utilise les deux.

funcs.h

#ifndef __FUNC__
#define __FUNC__

extern int var1;

extern int funcMachin(void);

#endif

funcs.ino

#include "funcs.h"

int var1 = 10;

int funcMachin(void)
{
  return var1++;
}

anotherfunc.h

#ifndef __ANOTHERFUNC__
#define __ANOTHERFUNC__

class Truc
{
 private:
   int m_truc;
 public:
   Truc();
   int monTrucEnPlumes(void);
};

// le compilateur de couine pas : les deux protos sont identiques
extern int anotherFuncMachin(void);
extern int anotherFuncMachin(void);

#endif

anotherfunc.ino

#include "funcs.h"
#include "anotherfunc.h"

// constructeur
Truc::Truc(void)
{
  m_truc = 0;
}

// méthode
int Truc::monTrucEnPlumes(void)
{
  return m_truc++;
}

// une fonction C
int anotherFuncMachin(void)
{
  return funcMachin();
}

application

#include "funcs.h"
#include "anotherfunc.h"

Truc MonTruc;

void setup() {
  Serial.begin(115200);
}

void loop() {
  // put your main code here, to run repeatedly:
  Serial.print("machin=");
  Serial.println(anotherFuncMachin());
  Serial.print("truc=");
  Serial.println(MonTruc.monTrucEnPlumes());
  delay(1000);
}

Chaque variable globale est déclarée dans le module qui l'utilise directement, pas dans l'application.
Le C++ est le meilleur moyen pour bien associer les fonctions (méthodes) et les variables (attributs).

Encore une fois, ces remarques s'adressent aux débutants, qui feraient bien d'ailleurs de consacrer du temps à se former, c'est le meilleur moyen.

Le seul problème est que souvent les cours et les tutos n'apprennent pas à modulariser et à hiérarchiser. Le C++ peut aider à y parvenir plus facilement.

J'arrête pour aujourd'hui.
N'hésitez pas à y revenir.
@+

hbachetti:
Je ne décris pas ici mes propres méthodes. Je ne fais qu'énoncer des principes qui paraîtraient évidents à n'importe quel développeur expérimenté. [...]
Je pars du principe que pour bien développer, il faut adopter les méthodes des meilleurs -> donc LINUX pour moi.

D'accord à 100% !

De toutes façon, dans un gros projet il faut éviter au maximum les variables globales, qui déstructurent la conception modulaire.
Et puis les variables globales sont instanciées avant de rentrer dans la fonction main(), donc si une erreur se produit (déclaration d'un tableau immense, échec de la construction d'un objet avec levée d'exception...), bonjour l'angoisse pour trouver d'ou ça vient !

Et bien, merci de tous ces bons conseils, que je vais m'empresser de mettre en pratique.
A suivre...

Ben, ça couine pas mal... :confused:

J'ai d'abord créé un fichier 'Horloge.h' dans lequel j'ai déclaré mes variables et mes fonctions, par exemple

/* 
 * Les définitions
 */

#ifndef __FUNC__
#define __FUNC__

// paramètres horloge
extern int fuseau ;  // Fuseau horaire de Paris
extern int dst ;  // Daylight saving time
extern bool change ;

...

// Paramètres animations
extern const byte NO_ANIM ;
extern const byte COLOR_PULSE ;

...

void FillLEDsFromPaletteColors( uint8_t colorIndex);


#endif

Ensuite, j'ai mis mes fonctions (pas toutes, j'en ai laissé quelques unes dans le fichier .ino) dans un fichier 'Horloge.cpp'

/* 
 * Les fonctions
 */

#include "Horloge.h"

...

void FillLEDsFromPaletteColors( uint8_t colorIndex)
{
  uint8_t brightness = 100;
  for ( int i = 0; i < NUM_LEDS; i++) {
    leds[i] = ColorFromPalette( currentPalette, colorIndex, brightness);
    colorIndex += 3;
  }
}

Et dans le programme principal, le fichier .ino, j'ai inséré

#include "Horloge.h"

et j'ai laissé les initialisations des variables quand c'est nécessaire :

// paramètres horloge
int fuseau = 1;  // Fuseau horaire de Paris
int dst = 0;  // Daylight saving time
bool change = false;

Et clic ! je lance la compilation... et ça couine !!
Les erreurs sont toutes similaires à ceci :

sketch\Horloge.h:12:8: error: 'byte' does not name a type

Il ne trouve pas les types... J'ai loupé un épisode ?

En ajoutant au début du fichier .h la ligne
#include "Arduino.h"ça règle une partie des erreurs.

Maintenant j'ai beaucoup moins d'erreurs : on dirait juste qu'il a du mal à trouver les bibliothèques que j'appelle.

Par exemple, j'utilise la bibliothèque Streaming, bien pratique pour simplifier les affichages sur la console. Par exemple, elle permet de faire ça :

Serial << "MaJ_heure = " << heures << ":" << minutes << ":" << secondes << endl ;

Quand tout était dans un seul fichier, je n'avais aucun problème, maintenant il couine :

sketch\Horloge.cpp: In function 'void MaJ_heure()':

Horloge.cpp:47: error: no match for 'operator<<' (operand types are 'HardwareSerial' and 'const char [13]')

   Serial << "MaJ_heure = " << heures << ":" << minutes << ":" << secondes << endl ;

          ^

Horloge.cpp:47: error: 'endl' was not declared in this scope

   Serial << "MaJ_heure = " << heures << ":" << minutes << ":" << secondes << endl ;

On dirait que a bibliothèque n'est pas reconnue ainsi que ses paramètres.

Même chose avec l'appel à WifiClient, issu de la bibli déclarée avec #include "CayenneWiFiClient.h" dans le programme principal .ino

Faut-il déclarer les bibliothèques dans le fichier .h ?

Ca avance : j'ai mis les déclarations des bibliothèques dans le fichier .h et ça compile sans rien dire !

Youpi...

Mais, c'est le linker qui couine maintenant... :angry:

Je recopie les messages ci-dessous, parce que ça me semble un peu compliqué

sketch\Horloge_LED3.ino.cpp.o: In function `CLEDController::getMaxRefreshRate() const':

C:\Users\Chuwi\Documents\Arduino\libraries\FastLED/controller.h:117: multiple definition of `leds'

sketch\Horloge.cpp.o:(.bss.leds+0x0): first defined here

sketch\Horloge_LED3.ino.cpp.o: In function `CLEDController::getMaxRefreshRate() const':

C:\Users\Chuwi\Documents\Arduino\libraries\FastLED/controller.h:117: multiple definition of `currentPalette'

sketch\Horloge.cpp.o:(.bss.currentPalette+0x0): first defined here

sketch\Horloge_LED3.ino.cpp.o: In function `CLEDController::getMaxRefreshRate() const':

C:\Users\Chuwi\Documents\Arduino\libraries\FastLED/controller.h:117: multiple definition of `currentBlending'

sketch\Horloge.cpp.o:(.bss.currentBlending+0x0): first defined here

sketch\Horloge_LED3.ino.cpp.o: In function `CLEDController::getMaxRefreshRate() const':

C:\Users\Chuwi\Documents\Arduino\libraries\FastLED/controller.h:117: multiple definition of `Blynk'

sketch\Horloge.cpp.o:(.bss.Blynk+0x0): first defined here

sketch\Horloge_LED3.ino.cpp.o: In function `CLEDController::getMaxRefreshRate() const':

C:\Users\Chuwi\Documents\Arduino\libraries\FastLED/controller.h:117: multiple definition of `Cayenne'

sketch\Horloge.cpp.o:(.bss.Cayenne+0x0): first defined here

sketch\Horloge_LED3.ino.cpp.o:(.text.setup+0x3c): undefined reference to `TlastHorloge'

sketch\Horloge_LED3.ino.cpp.o:(.text.setup+0x40): undefined reference to `TlastAnim'

collect2.exe: error: ld returned 1 exit status

exit status 1
Erreur de compilation pour la carte NodeMCU 0.9 (ESP-12 Module)
[right][/right]

Toutes ces erreurs se ressemblent : 'multiple definition' ou 'undefined reference' mais j'ai bien défini dans le fichier .h

extern long TlastHorloge ;
extern long TlastAnim ;

ce sont les deux seules variables que j'initialise dans le setup du fichier .ino (et pas avant)

  TlastHorloge = millis();

Je peux régler ce problème en initialisant ces variables avant le setup.

Quant aux multiple definitions, j'ai vérifié : leds, currentBlending et currentPalette sont bien définies dans le .h et une seule fois:

CRGB leds[NUM_LEDS];
CRGBPalette16 currentPalette( CRGB::Black);
TBlendType    currentBlending;

Est-ce un problème lié aux structures ? (là je commence à atteindre mes limites...)

Il ne faut pas instancier tes variables dans les .h

CRGB leds[NUM_LEDS];
CRGBPalette16 currentPalette( CRGB::Black);
TBlendType    currentBlending;

Ces lignes doivent être dans un .cpp ou .ino
Dans le .h tu les déclares seulement en "extern"

extern CRGB leds[NUM_LEDS];
extern CRGBPalette16 currentPalette( CRGB::Black);
extern TBlendType    currentBlending;

CRGB
Il faudrait poster le code complet pour y voir plus clair.
@+

Merci, j'avais en effet oublié les extern. Maintenant, ces erreurs ont disparu. Il me reste uniquement les 'multiple definitions'...

sketch\Horloge_LED3.ino.cpp.o: In function `CLEDController::getMaxRefreshRate() const':

C:\Users\Chuwi\Documents\Arduino\libraries\FastLED/controller.h:117: multiple definition of `Blynk'

sketch\Horloge.cpp.o:(.bss.Blynk+0x0): first defined here

sketch\Horloge_LED3.ino.cpp.o: In function `CLEDController::getMaxRefreshRate() const':

C:\Users\Chuwi\Documents\Arduino\libraries\FastLED/controller.h:117: multiple definition of `Cayenne'

sketch\Horloge.cpp.o:(.bss.Cayenne+0x0): first defined here

collect2.exe: error: ld returned 1 exit status

exit status 1
Erreur de compilation pour la carte NodeMCU 0.9 (ESP-12 Module)

Je pensais éviter de poster les codes, mais là je pense qu'il n'y a plus d'autre solution...

Merci de ton aide. Pour info, le projet est de faire une horloge avec un miroir infini, dont je puisse contrôler les animations via une interface Cayenne depuis un smartphone.

Quand ce premier code compilera, je vais le séparer en plusieurs fichiers : un avec les fonctions horaires, un avec les commandes Cayenne, un avec les animations, etc. Il faudra un seul fichier .h ?

Horloge_LED3.ino (7.36 KB)

Horloge.cpp (3.22 KB)

Horloge.h (2.36 KB)

A mon avis tu as déclaré dans Horloge.cpp deux variables Blynk et Cayenne alors qu'il te suffirait d'inclure controller.h de la librairie FastLED.
Évite de redéclaré dans tes sources les variables de tes bibliothèques. Il est préférable d'inclure leurs .h

Bizarre, j'ai bien installé une librairie FastLED mais je n'ai pas ces variables. Pas la bonne lib sans doute ...
Est-ce bien celle-ci : GitHub - FastLED/FastLED: The FastLED library for colored LED animation on Arduino. Please direct questions/requests for help to the FastLED Reddit community: http://fastled.io/r We'd like to use github "issues" just for tracking library bugs / enhancements.

Tu commets une autre erreur :

#ifndef __FUNC__
#define __FUNC__

Donner de référence à cette directive le même nom que le fichier, en majuscules.

// Pour chaque fichier .h une directive différente
// pour horloge.h
#ifndef __HORLOGE__
#define __HORLOGE__
// ou
#ifndef __HORLOGE_H__
#define __HORLOGE_H__

// pour controller.h
#ifndef __CONTROLLER__
#define __CONTROLLER__
// ou
#ifndef __CONTROLLER_H__
#define __CONTROLLER_H__

// etc.

// #endif à la fin de chaque fichier bien sûr
#endif

Il faudra un seul fichier .h ?

Non, de préférence un fichier .h pour chaque fichier .ccp et portant le même nom.
Ainsi, si tu transformes chaque module en librairie ils auront chacun leur propres fonctionnalités.
Attention de bien déclarer les variables propres à chaque module dans son .h, aucune variable globale appartenant à un module ne doit être déclarée dans l'application. De plus ces variables sont de préférence initialisées avec une fonction ayant comme suffixe "begin" ou "init" (horloge_begin, cayenne_init, etc.) et accédées au travers de fonctions.
Cela conduit souvent à déclarer dans les .h uniquement les fonctions, et à rendre les variables globales de chaque module statiques, c'est à dire privées au module.

@+

Merci hbachetti, mais j'ai du mal à comprendre ce que tu me dis. Alors, allons lentement STP...

Dans mon fichier horloge.h, il y a

#include <Streaming.h>
#define CAYENNE_DEBUG         // Uncomment to show debug messages
#define CAYENNE_PRINT Serial  // Comment this out to disable prints and save space
#include "CayenneDefines.h"
#include "BlynkSimpleEsp8266.h"
#include "CayenneWiFiClient.h"

car Cayenne fait référence à Blynk et utilise une ou plusieurs de ses bibliothèques. Mais je ne vois pas où j'ai pu déclarer Blynk et Cayenne à la fois.

Tu dis

A mon avis tu as déclaré dans Horloge.cpp deux variables Blynk et Cayenne alors qu'il te suffirait d'inclure controller.h de la librairie FastLED.

Je ne vois pas le rapport. Je devrais mettre #include "controller.h" à la place de #include "FastLED.h" ?

Évite de redéclarer dans tes sources les variables de tes bibliothèques. Il est préférable d'inclure leurs .h

Je ne comprends pas : peux-tu me donner un exemple ?

La bibliothèque FastLED que tu as trouvée est bien la bonne. Que veux-tu dire par "je n'ai pas ces variables" ?

Ensuite, je ne comprends pas la suite de ta réponse (désolé)

Je pense qu'il vaut mieux poster tes sources, cela facilitera les choses.

@+

Ils sont attachés au message 15