Cadencer des mesures

Bonjour,

J'utilise une carte Arduino Uno comme petite console d'acquisition.
J'ai créé une fonction pour paramétrer l'acquisition : période d'échantillonnage, durée d'acquisition.
Dans ma boucle principale loop, je lance une mesure lorsque le temps courant (avec millis()) dépasse le temps précédent (qui a été incrémenté de la période d'échantillonnage lors de la mesure précédente).
Globalement, cela fonctionne pas si mal mais j'ai parfois quelques erreurs : même temps affiché deux fois consécutivement, écart entre deux mesures un peu supérieur à la valeur de la période d'échantillonnage.
Je viens vers pour améliorer mon code et essayer d'être plus précis (ou rigoureux) sur la fréquence des mesures

Mon code :


// Déclaration des variables du programme
unsigned long fluxSerie = 250000; // flux de la liaison série

// Variables d'acquisition
int tempsEchant ; // période d'échantillonnage en ms, saisi par l'utilisateur
int dureeAcquisition ; // durée totale de l'acquisition  en s, saisie par l'utilisateur

unsigned long tempsZero;
unsigned long tempsCourant;
int topDepart = 0; //variable pour lancer l'acquisition
String envoi;

unsigned long tempsPrec;

// Broches de branchement au circuit
byte mesureTension = 0; //broche analogique de mesure de la tension aux bornes de C
byte capteur = 3; //broche pour décharger initialement le condensateur


void setup() {
  Serial.begin(fluxSerie);
  tempsPrec = millis();
  tempsZero = millis();
  envoi = "0";

  //configure la broche commandePin en tant que sortie
  pinMode(capteur, OUTPUT) ;

}

//  Boucle principale
void loop()
{
  tempsCourant = millis() - tempsZero;
  if (  topDepart == 0)
  {
    reglages();
  }
  else
  {
    if (tempsCourant <= dureeAcquisition)
    {
      if (tempsCourant >= tempsPrec)
        lecture();
    }
  }
}

void reglages()
{
  while (topDepart == 0)
  {
    envoi = Serial.readString();

    Serial.print("Saisir la période d'échantillonnage (en ms) : ");
    while (Serial.available() == 0 );
    {
      tempsEchant = Serial.parseInt(); //on enregistre la valeur saisie dans tempsEchant
      Serial.println(tempsEchant);
      delay(100); //délai de temporisation
    }

    Serial.print("Saisir la durée d'acquisition (en ms) : ");
    while (Serial.available() == 0 );
    {
      dureeAcquisition = Serial.parseInt(); //on enregistre la valeur saisie dans dureeAcquisition
      Serial.println(dureeAcquisition);
      delay(100); //délai de temporisation
    }

    Serial.println("Saisir S pour lancer l'acquisition : ");
    while (Serial.available() == 0 );
    {
      topDepart = 1;
      tempsPrec = 0;
      tempsZero = millis();
    }
  }
}

void lecture()
{
  float tension;
  // Lecture et conversion des données
  tension = analogRead(mesureTension) * 5.0 / 1024; // mesure analogique et conversion en tension

  // Envoie les données
  Serial.print(tempsCourant / 1000.0, 3); // donne la date en seconde
  Serial.print('\t');
  Serial.println(tension, 2); // donne la valeur recalculée de tension aux bornes de C
  tempsPrec += tempsEchant;
}

Merci pour votre aide

Tout peut dépendre de tempsEchant. Si l'échantillonnage est irrégulier c'est peut être que ce temps est très court. millis() s'incrémente de 1 toutes les 1/1,024ms et en compensation s'incrémente 24 fois sur 1024 de 2 unités pour rattraper. Cela peut jouer si tempsEchant est petit. Dans ce cas si le temps est inférieur à 16ms, il vaut mieux employer micros().

Sinon, plus loop est long, plus les temps auxquels peuvent se faire les échantillonnages s'espacent. Supposons qu'une boucle de loop dure 10ms. Si le temps est écoulé au bon moment, il sera pris immédiatement. Si il arrive un chouilla plus tard, il est pris 10ms plus tard. L'instant de déclenchement réel se fait donc avec une erreur de 10ms. Il faut donc réduire la boucle loop au maximum.

  • j'ai l'impression que topDepart une fois mis à 1 ne repasse plus par 0, et donc le réglage ne se fait qu'une fois. Si c'est le cas, mettre le réglage dans le setup ce qui allège loop.
  • il y a deux tests pour déclencher lecture(), je n'en comprends pas bien le sens. Dans le programme classique "blink without delay", on montre qu'un seul test suffit
  • perso, j'aurais tendance à passer les durées en int et pas en unsigned long. Les calculs sur les entiers 16 bits vont plus vite. Pourquoi pas passer sur un octet? Mais j'ai l'impression que les opérations sur deux octets se font sur 16 bits, ce qui ne gagnerait rien au contraire.

L'échantillonnage peut être de quelques ms : je passe donc de millis() à micros()

Pour déclencher la lecture, il faut que la durée d'acquisition totale ne soit pas dépassée (paramétrer par l'utilisateur dans la fonction reglages) et que le temps courant ait avancé de la période d'échantillonnage.

En terme de calcul, est-ce qu'il vaut mieux faire un test if (tempsCourant <= dureeAcquisition) ou un test if (tempsCourant - dureeAcquisition <= 0) ?

Je n'ai pas compris la dernière partie de ta réponse :

Pourquoi pas passer sur un octet? Mais j'ai l'impression que les opérations sur deux octets se font sur 16 bits, ce qui ne gagnerait rien au contraire.

merci pour ton aide

OK j'avais pas compris l'arrêt (fin de l’acquisition). Cela fait deux conditions, soit deux tests.

Je reprends l'idée de diminuer le temps de la boucle. C'est surtout le temps quand il n'y a pas d'acquisition. Quand une acquisition se présente, elle peut durer longtemps, cela n'a pas trop d'importance (tant que cela ne retarde pas l'acquisition suivante). Du coup, il est plus judicieux de tester la fin des toutes les acquisition en fin de lecture. Il est alors possible de combiner les deux valeurs tempsEchant et dureeAcquisition, par exemple en laissant le test sur tempsCourant >= tempsPrec et en mettant tempsEchant au maximum (il n'y aura plus d'échantillonage avant 50 jours), ou de faire une boucle infinie...

loop() deviendrait alors a peu près:

void loop()
{
  tempsCourant = millis() - tempsZero;
  if (tempsCourant >= tempsPrec)
        lecture();
}

Le réglage étant mis dans setup.
Et pour éviter de ranger et de ressortir tempsCourant, on peut laisser:

void loop()
{
  if (millis() - tempsZero >= tempsPrec)
        lecture();
}

La deuxième forme fonctionne sauf au moment ou le compteur de temps déborde. Il faut donc utiliser la première forme. C'est bon pour toutes les chronométrages.

Si on fait:

unsigned long tempsZero;
unsigned long tempsPrec;
...
if (millis() - tempsZero >= tempsPrec)

La soustraction et la comparaison se font en 32 bits. Comme Arduino est un micro 8 bits, les opérations doivent se faire en 4 fois. Si on fait:

word tempsZero;
word tempsPrec;
...
if (word(millis()) - tempsZero >= tempsPrec)

On n'a plus que des opérations sur 16 bits. C'est donc deux fois plus rapide.
Cela fonctionne si il est possible de passer en 16 bits, c'est à dire si on utilise micros(), on ne peut dépasser 65000µs soit 65ms.

Pour vérifier ce que je dis, voici un "blink without delay" en utilisant des word:

const word DEMI_PERIODE = 500;
word dernierChangement;

void setup()
{
  pinMode(LED_BUILTIN, OUTPUT); // Configuration de la broche de la led
}

void loop()
{
  if (word(millis()) - dernierChangement >= DEMI_PERIODE)
  {
    // Changement de l'état de la LED
    if (digitalRead(LED_BUILTIN) == HIGH) // Si la LED est allumée
      digitalWrite(LED_BUILTIN, LOW); // Éteint la led
    else // Si la LED est éteinte
      digitalWrite(LED_BUILTIN, HIGH); // Allume la led
    dernierChangement += DEMI_PERIODE; // Nouveau départ
  }
}

On peut même utiliser des byte:

const byte DEMI_PERIODE = 250;
byte dernierChangement;

void setup()
{
  pinMode(LED_BUILTIN, OUTPUT); // Configuration de la broche de la led
}

void loop()
{
  if (byte(byte(millis()) - dernierChangement) >= DEMI_PERIODE)
  {
    // Changement de l'état de la LED
    if (digitalRead(LED_BUILTIN) == HIGH) // Si la LED est allumée
      digitalWrite(LED_BUILTIN, LOW); // Éteint la led
    else // Si la LED est éteinte
      digitalWrite(LED_BUILTIN, HIGH); // Allume la led
    dernierChangement += DEMI_PERIODE; // Nouveau départ
  }
}

Je crois que la soustraction se fait en 16 bits, d’où la présence d'un deuxième byte(). Le seul intérêt est d'utiliser moins de mémoire

Bonjour,

Merci pour toutes ces précisions.
Je n'ai pas bien compris comment tester la fin de l'acquisition : si c'est testé à la fin de la lecture, comment cela stoppe la boucle loop du programme ?

J'ai testé la solution d'utiliser micros() au lieu de millis() mais cela ne me paraît pas possible (ou alors ai-je sauté une étape ?) car effectivement mon temps d'échantillonnage peut être de l'ordre de la ms mais par contre la durée d'acquisition est de l'ordre de la centaine de ms (voire même de quelques secondes) : si je passe en int au lieu de unsigned long, ça dépasse !

Il est possible de cadencer l’échantillonnage avec micros() plus des unsigned long et la durée d'acquisition avec millis(), et des words. Ce sont deux choses indépendantes.

Si le programme ne doit pas recommencer comme le code du départ, si la condition de fin arrive, on arrête tout, par exemple en mettant une boucle infinie: while (true);

Comme millis() s'incrémente parfois de 1 parfois de 2 et que la fréquence d'échantillonnage est faible, dans ce cas une bonne solution est de passer par un timer qui va cadencer l'ensemble. L'avantage est que quand l'interruption arrive, elle est prise en compte à la fin de l'instruction, ce qui peut ne faire qu'une centaine de ns de différence.

Avantage de passer par interruption: c'est l'interruption qui fait l'échantillonnage, et éventuellement fait le traitement. Du coup loop peut faire autre chose, par exemple gérer les réglages, les reprise d' séquence d'échantillonnage...
Inconvénient: c'est un peu plus compliqué à écrire.

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.