Problème de démarrage de l'Arduino UNO avec un programme multi-fichiers [RESOLU]

Bonjour à tous.
J'ai un drôle de soucis avec un programme multi-fichiers, qui bien qu'il compile et se transfert sans problème, ne se lance pas.

J'ai créé une classe qui utilise la librairie Adafruit PWMServoDriver:

#include "Adafruit_PWMServoDriver.h"

class ServoDriverPCA9685 {

public:

    /**
     * Constructor
     * @param addr address of the PCA9685 on the I2C bus - 0x40 by default.
     * @param frequence PWM frequency for the entire chip - by default 50.0.
     * @param oscillatorFrequency the internally tracked oscillator used for freq calculations - by default 25000000.
     */
    explicit ServoDriverPCA9685(uint8_t addr = 0x40, float frequence = 50.0, uint32_t oscillatorFrequency = 25000000);

    /**
     * Copy constructor
     * @param other ServoDriverPCA9685 object.
     */
    ServoDriverPCA9685(const ServoDriverPCA9685 &other);
.....

et j'ai donc deux fichiers (ServoDriverPCA9685.cpp et ServoDriverPCA9685.h).

Dans le fichier principal (.ino) j'inclus ServoDriverPCA9685.h et aussi Adafruit_PWMServoDriver.h tel qu'il est recommandé de faire lorsque l'on utilise des librairies dans les fichiers "externes".

ServoDriverPCA9685 servoDriverPCA9685(0x40, 50.0, 27000000);
bool initBreakout = false;

void setup() {

  Serial.begin(19200);

  initBreakout = servoDriverPCA9685.initDriver();

  if (initBreakout) {
    Serial.println(F("Init arm"));
    initArm();
  } else {
    Serial.println(F("Erreur d'initialisation du breakout."));
  }
}

void loop() {...}

Lorsque le programme est transféré dans la carte il ne démarre pas, un peu comme si la carte était bloquée. On ne passe ni dans le setup() et ni dans loop(). Si je commente le code faisant référence à ServoDriverPCA9685, cela refonctionne. Le problème doit je pense venir du fait d'utiliser une librairie dans un fichier externe même si cela est faisable. Ce qui m'ennuie c'est que je n'ai aucune erreur de compilation.

Si quelqu'un a une petite idée, je suis preneur.

Jocelyn

il n'y a aucun souci à utiliser une librairie dans un fichier externe.

➜ postez le code du constructeur de votre classe - ou mieux encore, tout le code.

(compilez aussi en mode "verbose" avec tous les warnings)

Bonjour J-M-L et merci pour ton conseil.
J'avais effectivement un petit "problème" que j'ai solutionné: plusieurs versions de la librairie
Adafruit PWMServoDriver, mais cela n'a rien changé. Au niveau des logs lors de la compilation il n'y a aucune erreur, ni lors du transfert.
Je continue donc de chercher.

Pour info voici le code du constructeur.

/**
 * Constructor
 * @param addr address of the PCA9685 on the I2C bus.
 */
ServoDriverPCA9685::ServoDriverPCA9685(const uint8_t addr, float frequence, uint32_t oscillatorFrequency) {

    if (addr != 0x40) {
        pwmPCA9685 = Adafruit_PWMServoDriver(addr);
    } else {
        pwmPCA9685 = Adafruit_PWMServoDriver();
    }

    frequence_ = frequence;
    pwmPCA9685.setPWMFreq(frequence_);
    pwmPCA9685.setOscillatorFrequency(oscillatorFrequency);
}

Jocelyn

Je ferai ces opérations dans une nouvelle méthode begin() que vous appelleriez depuis le setup().

Les constructeurs sont appelés très tôt et vous n’êtes pas sûr que la carte ait été proprement initialisee

Dans le constructeur je ne ferai que stocker ces valeurs dans des variables d’instance

1 Like

J'interviens pour avoir des explication et comprendre le langage en temps que débutant sur arduino

1/ Que fait le void setup dans le fichier externe ServoDriverPCA9685?

2/ Pourquoi cette condition alors que le constructeur est initialisé Par défaut?

3/ Que représente cette syntaxe d'affectation pour un constructeur?

4/ Pourquoi, c'est différent du C++?

Non c’est la spec C++ qui dit cela

Les constructeurs sont appelés avant le main() et c’est le main qui configure la carte, les timers, les pins ou l’I2C avant d’appeler setup() ou loop()

Donc il vaut mieux éviter de mettre dans son constructeur des appels à des méthodes qui pourraient avoir des dépendances matérielles

1 Like

Non c'est faux

Précisez votre pensée.. .Dire "c'est faux" sans plus de détail n'apporte rien au débat.

Je vais être plus précis s'il le faut : les constructeurs des variables globales j'entends (ça semblait évident puisque sinon ils sont dans le code et appelé donc plus tard)

Je vous renvoie à la spec qui dit bien

Non-local variables

All non-local variables with static storage duration are initialized as part of program startup, before the execution of the main function begins (unless deferred, see below). All non-local variables with thread-local storage duration are initialized as part of thread launch, sequenced-before the execution of the thread function begins. For both of these classes of variables, initialization occurs in two distinct stages:...

je vous laisse faire vos pirouettes pour vous expliquer...

pourquoi des pirouettes?

Si je reprend l'exemple initial de la class Adafruit_PWMServoDriver
cette classe à trois constructeurs un par défaut avec une liste d'initialisation, un avec le paramètre addr plus un avec le paramètre addr et un objet pointé sur la classe TWI.
Il faut donc créer un objet sans paramètres pour appel du premier constructeur, avec un paramètre pour l'appel du second constructeur ou avec 2 pour l'appel du 3eme constructeur.

regardez le code du setup et je suis sûr qu'ils appellent une méthode begin() sur leur instance dans le setup()

c'est ce begin() qui se charge de faire le nécessaire - ce qui est risqué de faire tant que la carte n'est pas initialisée.

Ce n'est pas très claire.
Il ne faut rien, si un objet à 3 constructeurs, dans ton code tu appels l'un des trois constructeurs, lorsque tu instancie ton objet.
Tu peux (re)instancier ton objet à différent endroit, dans la déclaration des variables globales ou dans n'importe quel fonction.

Dans quel setup?

un objet est un non unique dans la même portée
Adafruit_PWMServoDriver monObjet();
1er objet: Appel au 1er constructeur pour un premier PCA qui sera à l'adresse 0x040, inutile de préciser la variable est initialisée à 0x040 par le constructeur
Adafruit_PWMServoDriver monObjet2(0x050);
2eme objet: Appel au 2eme constructeur pour un second PCA qui sera à l'adresse 0x050

des exemples de la bibliothèque

...

➜ vous instanciez la variable globale, son constructeur est appelé mais le gros du boulot de configuration qui nécessite d'avoir le hardware qui est prêt est fait plus tard, dans le setup() du sketch par l'appel à cette fonction begin()

Là vous êtes dans des fichiers .ino pas dans la configuration initiale demandée de faire un .h et un .c

Je ne connais pas ce genre de syntaxe pour créer un objet
// called this way, it uses the default address 0x40
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();

Je connais la façon dont vous l'aviez fait dans l'exemple que vous m'avez donné pour le DS18B20 puisque c'est la même que je vous donne
OneWire oneWire(onewireBusPin);

Dans les deux cas on ne fait que construire l'objet. Le choix de l'endroit où est app la méthode begin est du ressort du programmeur

je vais essayez d'expliquer mieux ce que je disais:

@jakob_1109 a une classe spécifique ServoDriverPCA9685

il crée une variable globale de ce type dans son sketch

ServoDriverPCA9685 servoDriverPCA9685(0x40, 50.0, 27000000); // <=== ICI
bool initBreakout = false;

void setup() {
...

cette classe a un constructeur

ServoDriverPCA9685::ServoDriverPCA9685(const uint8_t addr, float frequence, uint32_t oscillatorFrequency) {

    if (addr != 0x40) {
        pwmPCA9685 = Adafruit_PWMServoDriver(addr);
    } else {
        pwmPCA9685 = Adafruit_PWMServoDriver();
    }

    frequence_ = frequence;
    pwmPCA9685.setPWMFreq(frequence_);
    pwmPCA9685.setOscillatorFrequency(oscillatorFrequency);
}

qui instancie (on le ferait dans une liste d'initialisation normalement) la variable d'instance pwmPCA9685 qui est de type Adafruit_PWMServoDriver.

jusque là dans l'absolu ça va.

par contre ensuite il appelle

    pwmPCA9685.setPWMFreq(frequence_);
    pwmPCA9685.setOscillatorFrequency(oscillatorFrequency);

hors si vous regardez les exemples comme proposés par adafruit

ils appellent begin() avant de faire les 2 autres appels de configuration (setPWMFreq() et setOscillatorFrequency() ) puisque ces appels ont besoin que l'I2C soit configuré pour fonctionner...

Cet appel à begin() manque dans le constructeur de @jakob_1109 mais s'il le rajoutait

ServoDriverPCA9685::ServoDriverPCA9685(const uint8_t addr, float frequence, uint32_t oscillatorFrequency) {

    if (addr != 0x40) {
        pwmPCA9685 = Adafruit_PWMServoDriver(addr);
    } else {
        pwmPCA9685 = Adafruit_PWMServoDriver();
    }
    pwmPCA9685.begin(); // <===== RAJOUTER LE BEGIN ICI
    frequence_ = frequence;
    pwmPCA9685.setPWMFreq(frequence_);
    pwmPCA9685.setOscillatorFrequency(oscillatorFrequency);
}

il se trouverait dans le cas ou ce begin() est appelé dans la phase de construction de sa variable globale, qui est comme je le rappelais une phase qui se produit avant que main() soit appelé et donc avant que la carte ne soit configurée (par exemple l'I2C ou le port série, les timers etc).

la façon habituelle de faire c'est que le constructeur instancie effectivement le Adafruit_PWMServoDriver et que la classe de @jakob_1109 contienne une méthode begin() qui va faire la partie qui doit être effectuée une fois que tout est configuré

    pwmPCA9685.begin(); 
    pwmPCA9685.setPWMFreq(frequence_);
    pwmPCA9685.setOscillatorFrequency(oscillatorFrequency);

son code deviendrait alors

ServoDriverPCA9685 servoDriverPCA9685(0x40, 50.0, 27000000); 
bool initBreakout = false;

void setup() {
  servoDriverPCA9685.begin(); // <<=== ici on est après l'appel de main(), la carte est configurée

c'est plus clair ?


pour résumer : On ne met rien qui concerne le matériel dans un constructeur d'une classe qui a vocation a être potentiellement utilisée pour créer des instances globales.

je ne comprends donc pas votre post laconique

1 Like

Pour ce qui est purement arduino je n'ai pas assez regardé et pas pratiqué donc je ne vois pas pourquoi je douterais de ce que vous dites. J'ai compris de quoi vous parliez au post3

Dire c'est faux est certes laconique mais il est possible d'appeler le constructeur de n'importe où. rendre des variables globales ne présentent généralement aucune utilité.

J'ai suivi ce vous m'avez dit ca m'a permis au moins de comprendre sur quoi me documenter avant d'utiliser ma carte.

Jquest ce qui est faux dans ce que j’ai dit

(En faisant référence à ses constructeurs dans le code posté)
???

Qui a dit qu’il fallait rendre les variables globales ?

Et sur petit Microcontrôleur on a plutôt tendance à allouer statistiquement les variables plutôt que dynamiquement…

Vous me parlez trop spécifiquement Arduino pour moi avant la fonction main c'est global. C'est vrai que j'ai plus l'habitude que les allocations soient dynamiques mais je ne pense pas que cela me pose des problèmes.
Il faut que je regarde quelques tuto Arduino et je pourrais alors vous demander plus précisément ce qui m'échappe, là j'ai souvent du mal à vous suivre entre le main que je ne vois pas et les fichiers setup et loop qui n'existent pas en C++

@J-M-L Merci de votre réponse, ce que vous dites est complètement cohérent. Je vais tout sortir du constructeur hormis l'initialisation des variables d'instance.
Ensuite je vais créer une méthode public init() pour l'initialisation.

Le setup() est dans le fichier .ino et c'est ici qu'il doit être.

Si vous regardez bien la dernière valeur des paramètres du constructeur est changée.
Je suis donc obligé de mettre les autres.
Si j'écris:

ServoDriverPCA9685 servoDriverPCA9685();

Alors effectivement j'utilise complètement le constructeur avec ses paramètres par défaut.

Si j'écris:

ServoDriverPCA9685 servoDriverPCA9685(0x41);

Alors seul les deux derniers paramètres seront par défaut et je rentrerai dans ce if:

if (addr != 0x40) {
        pwmPCA9685 = Adafruit_PWMServoDriver(addr);
    }

En c++ il y a deux façons d'initialiser un objet:
Adafruit_PWMServoDriver *pwmPCA9685 = new Adafruit_PWMServoDriver(addr);
Cette façon va créer un objet sur le tas (mémoire globale au programme en gros). C'est ce que l'on appelle de l'allocation dynamique, ce qui est fortement déconseillé avec les microcontrôleurs car il y a très peu de mémoire. Un new implique obligatoirement un delete pour libérer la mémoire alloué à l'objet.

Adafruit_PWMServoDriver pwmPCA9685 = Adafruit_PWMServoDriver(addr);
Celle-ci va créer un objet dans la stack et il sera automatiquement détruit lorsqu'il sera hors de portée.

Merci à tous pour vos réponses.

Jocelyn

Vous êtes sur le forum Arduino… si vos avis laconiques partagés à l’emporte pièce n’ont rien à voir ni avec la discussion ni avec le monde arduino, je ne sais pas pourquoi vous jugez pertinent de poster ce genre de réponse :man_shrugging: .

C’est la deuxième fois que vous affirmez une contre vérité, n’en faites pas une habitude…

Que ce soit sur petit MCU ou gros ordinateur, le constructeur d’une variable globale (sauf à déférer sa création) est toujours appelé avant que le main() - d’où qu’il vienne - soit appelé. Vous n’êtes pas non plus maître de l’ordre dans lequel ces constructeurs sont appelés.

En conséquence il est évident qu’il ne faut pas mettre dans ce constructeur quoi que ce soit qui dépende de ce que le main fait plus tard ou d’autres instances globales.

C’est la norme qui le dit. C’est comme cela. Faut faire avec.

Donc quand vous créez une classe a vocation générale, prévoyez le constructeur et un init() ou begin() pour finaliser les détails nécessaires au run time.