Méthodes de mesure de fréquence et résultats imprécis

Bonjour à tous,

Il y a un bout de temps, j'avais sollicité votre aide pour réaliser un compte-tours (moteur 2 temps à 2 étincelles par tour).
Après moult expérimentations, je me trouve confronté à un problème logiciel.
Avec ce code donné par l'un d'entre vous, dont l'entrée est simulée par une pwm fixée à 500 Hz (ce qui doit me donner 15000 rpm), la valeur affichée est environ 13700 rpm et fluctue presque à chaque mesure d'environ 300 rpm.

volatile byte rpmcount;
volatile unsigned int rpm;
unsigned long timeold;



void setup()
{
  Serial.begin(57600);
  attachInterrupt(0, rpm_pulse, FALLING);
  digitalWrite(2, HIGH); 
  
}

void loop()
{
  
    noInterrupts(); // interdit les interruptions / la modification des compteurs
    uint32_t t_now = micros();  // lit l'heure

    
    uint32_t rpmc = rpmcount;
    rpmcount = 0;// lit et réinitialise les compteurs

    interrupts(); // autorise les interruptions / la modification des compteurs
    uint32_t delta_t = t_now - timeold; // calcule la durée du comptage effectué
    timeold = t_now; // mémorise l'heure de départ du nouveau comptage
    rpm = rpmc * 6.0e7 / delta_t;
    rpm = rpm / 2 ;
    Serial.println(rpm);
    delay(200);   
}

void rpm_pulse()
{
  rpmcount++;
}

J'ai donc tenté l'autre méthode qui mesure le temps entre chaque impulsion.
Cette fois-ci ça fonctionne j'ai un beau 15000 rpm rafraichi toutes les 200ms mais disons une fois toutes les secondes, il m'affiche 9990 rpm.

volatile unsigned int freq;
volatile unsigned int rpm;
float start;
float elapsed;



void setup()
{
  Serial.begin(57600);
  attachInterrupt(0, rpm_pulse, FALLING);
  digitalWrite(2, HIGH); 
  
}

void loop()
{
    Serial.println(rpm);
    delay(200);   
}

void rpm_pulse()
{
  elapsed = millis() - start;
  start = millis();
  freq = 1000UL / elapsed;
  rpm = freq * 30;
}

Même si je commence à avoir un peu de recul sur les arduinos, je reste un débutant, j'ai cru comprendre que gérer des timers (ce que je n'ai encore pas assimilé) permet d'alléger la charge de travail du microcontroleur, est-ce la solution ?
Le reste de mon code implique déjà beaucoup de choses donc je cherche la solution moins gourmande en ressources sachant que je n'ai besoin d'une précision qu'à 100 rpm.

Une explication à mon problème ?

bonjour,
je dirais, normal les fluctuations.
le delay est une fonction bloquante, donc tu vas sauter des tours.

de plus

float start;
float elapsed;

pour du millis = pas glop, un millis() n'est pas à "virgule"
unsigned long serait plus approprié pour millis

Bonne remarque infobarquee.

inryjo, les méthodes sont bonnes à priori (j'ai survolé tes codes), c'est à dire:
soit on mesure le temps entre 2 interruptions
soit on compte le nombre d'interruptions tout les x temps

Je ne sais pas si tu travailles sur l'arduino uno ? Si c'est le cas:
Si mes souvenirs sont bons, l'ATmega328P est capable de détecter 1 interruption tous les 2 cycles d'horloge, si le microcontrôleur est cadencé à 16Mhz, cela nous donne 8Mhz max.

Ensuite les timers, et la fonction millis() du code arduino:
Cette fonction millis() est dépendante d'une fonction plus bas niveau (avr interrupt) qui compte les microsecondes, enfin, pas exactement, qui compte toutes les 4 microsecondes, parce que compter toutes les microsecondes signifie mettre le timer 0 à 1000000Hz, et ça ce n'est pas possible sans avoir des bugs et lags divers parce que les interruptions sont appelées trop de fois par seconde et que le code dans l'interruption (même si c'est un vulgaire incrément genre time++), n'a pas le temps de s’exécuter entièrement avant qu'une autre interruption pointe déjà son nez...

Bon bref, c'est un peu le coté technique, mais saches que les interruptions et les timers c'est ce qu'il faut utiliser, mais que ça peut poser problème à haute fréquence.

Par contre pour ton banal 500Hz, pour ma part j'ai fait des tests à 1000Hz, impeccable il me détecte bien 1000Hz, 10000Hz, impeccable aussi, en revanche 100000Hz j'obtiens quelque chose de l'ordre de 60000Hz mesuré, ce qui peut être est normal puisque j'utilisais pour mesurer cela ma fonction qui utilise des pin change interrupt de l'ATmega328P, c'est à dire ma fonction pour lire plusieurs pwm rc (récepteurs de modélisme).

Je peux éventuellement te montrer le code que j'utilise pour mesurer mais c'est pas de l'arduino, c'est ma propre bibliothèque, donc je ne sais pas si ça te serait utile?

Enfin quand je vois cela:

rpm = rpmc * 6.0e7 / delta_t;

Je trouve ça quand même un peu chelou non?

Et effectivement la fonction delay fait faire des tours dans le vide au microcontrôleur, ce qui n'est pas top niveau précision, bien qu'on arrive à générer un pwm correct à basse fréquence avec.

Bonjour,

Comment tu génère ton 500Hz?
Je viens d'essayer ton premier programme et je trouve des résultats entre 14930 et 15080 rpm.

Comme tu mesures toutes les 200ms tu as environ 100 tops. Selon la synchro tu peux te tromper de 1 ou 2 tops ce qui te donnes une erreur de 1 à 2%.

Je ne suis pas d'accord avec les deux remarques précédentes sur delay(), celui ci sert uniquement à cadencer la mesure. Sa précision n'a pas d'importance, le temps est mesuré par la différence des deux micros ().

tout d'abord merci pour toutes ces réponses
J'ai viré les float de la fonction millis, et essayé avec divers temps de delay sans aucune influence sur les résultats.
Pour générer du PWM j'utilise la sortie GPIO d'un raspberry, mes 9900 récurrents avec le contage du delta t peuvent peut etre venir d'une imprécision du RPi.
Avec quelle méthode de comptage as-tu testé Kamill ?
Je suis sur arduino Nano donc oui Atmega 328P.

Sachant que ce que je vous ai montré est juste mon code d'expérimentation de la partie rpm et que ça s'intègre dans un code qui fait déjà beaucoup de choses (utilisation d'autres interruptions), y a-t-il un intérêt dans l'allègement des ressources à juste lui dire "d'écouter" juste le delta T entre 2 interruptions une fois à chaque boucle ? Si oui, comment faire ? DetachInterrupt ?

inryjo:
Avec quelle méthode de comptage as-tu testé Kamill ?

J'ai utilisé ton 1er programme avec un générateur de fréquence

inryjo:
Pour générer du PWM j'utilise la sortie GPIO d'un raspberry, mes 9900 récurrents avec le contage du delta t peuvent peut etre venir d'une imprécision du RPi.

Utilise plutôt une sortie PWM de ton Arduino. Une fois définie dans le setup, tu n'auras plus besoin de mettre quoique ce soit à jour dans la partie "loop" et ce PWM ne consommera pas de temps CPU qui pourrait perturber le reste.
Bien sûr, la méthode de Kamill est encore meilleure, mais on n'a pas forcément un générateur de fréquence sous la main.

3Sigma:
Utilise plutôt une sortie PWM de ton Arduino. Une fois définie dans le setup, tu n'auras plus besoin de mettre quoique ce soit à jour dans la partie "loop" et ce PWM ne consommera pas de temps CPU qui pourrait perturber le reste.
Bien sûr, la méthode de Kamill est encore meilleure, mais on n'a pas forcément un générateur de fréquence sous la main.

Je viens de tester et j'obtiens les mêmes résultats avec la méthode du delta T entre deux impulsions.
Il m'affiche 15000 rpm et toutes les 10/15 mesures me sort un 9990 qui sort d'on ne sait où.

Certes cette méthode me donne les résultats les plus précis mais ça reste étrange qu'avec un code aussi simple j'aie ce nombre qui vient s'intercaler.

Faudrait que tu isoles tout ça et que tu fait fonctionner avec le code le plus minimaliste qui soit, histoire d'être sûr du bug.

3Sigma à raison, essaye avec du pwm sans appeler de gpio.

Bien que quand cela est bien fait (notamment avec des variables volatile), ça ne pose aucun problème, pour avoir testé cela avec 3 timers qui font des choses très différentes sur le 328p (et non pas le raspberry (que je ne connais pas)).

inryjo:
Il m'affiche 15000 rpm et toutes les 10/15 mesures me sort un 9990 qui sort d'on ne sait où.

Moi je sais d'ou ça sort.

A 500 Hz, la période est de 2ms. Suivant la synchro entre ton signal et l'horloge de millis() tu peux avoir une mesure de 1, 2 ou 3ms

1ms->30000 rpm
2ms->15000 rpm
3ms->9990 rpm

Je te conseille fortement d'utiliser la fonction micros()

sylvainmahe:
Faudrait que tu isoles tout ça et que tu fait fonctionner avec le code le plus minimaliste qui soit, histoire d'être sûr du bug.

3Sigma à raison, essaye avec du pwm sans appeler de gpio.

Bien que quand cela est bien fait (notamment avec des variables volatile), ça ne pose aucun problème, pour avoir testé cela avec 3 timers qui font des choses très différentes sur le 328p (et non pas le raspberry (que je ne connais pas)).

Oui j'utilise désormais comme conseillé la sortie pwm de l'arduino pour tester.

kamill:
Moi je sais d'ou ça sort.

A 500 Hz, la période est de 2ms. Suivant la synchro entre ton signal et l'horloge de millis() tu peux avoir une mesure de 1, 2 ou 3ms

1ms->30000 rpm
2ms->15000 rpm
3ms->9990 rpm

Je te conseille fortement d'utiliser la fonction micros()

Merci pour cette explication claire et précise. J'ai donc modifié mon code avec la PWM dans le setup qui m'affiche 500 Hz soit 15000 rpm avec ces fameux 330 Hz / 9900 rpm.
J'ai donc testé avec micros au lieu de millis, qui me donne une bien plus faible variation (et parfaitement acceptable dans mon cas) entre 14760 rpm et 14730 rpm. Soit 490/491 Hz.
On est donc loin du compte mais serait-ce lié au fait que la PWM est fixée à 490 Hz selon cette source ?
Si c'est le cas, pourquoi avec millis j'ai un 500Hz tout rond ?

volatile unsigned int freq;
volatile unsigned int rpm;
unsigned long start;
unsigned long elapsed;

void setup()
{
  Serial.begin(57600);
  attachInterrupt(0, rpm_pulse, FALLING);
  digitalWrite(2, HIGH); 
  analogWrite(10, 127);
  
}

void loop()
{
   Serial.println(rpm);
   delay(100);   
}

void rpm_pulse()
{
  elapsed = micros() - start;
  start = micros();
  freq = 1E6 / elapsed;  
  rpm = freq * 30;
  
}

Oui, le pwm est par défaut à 490Hz (980 sur certaines pins)
Tu trouve 500 Hz avec millis() car pour 490Hz, la période est de 2.04 ms, comme avec millis() tu as une résolution de 1ms tu trouves 2ms -> 500 Hz

Grillé par Kamill mais j'avais détaillé alors je poste.

millis() comme micro() utilisent le timer 0 qui compte de 0 à 255. Ces fonctions ajoutent le traitement des débordements du compteur du timer 0 pour pouvoir compter au delà de 255.
Ce traitement n'est pas anodin et prend le temps qu'il faut.
Le code des fonctions est disponible dans l'IDE.

millis() est configuré pour utiliser un diviseur d'horloge système (préscaleur) égal à 64.
En conséquence le pas de mesure de millis est 64/16 000 = 4µs.
C'est sans doute ce pas, qui n'est pas infiniment petit, qui explique qu'il n'est pas possible d'obtenir la valeur exacte.

Information :
Les prescalers des 3 timers de l'atmega328p sont configurés dans la fonction init() (voir les fichiers de l'IDE).

Question :
De quel timer dépend la sortie PWM que tu utilise ?
Le pourqoi de cette question est qu'il me semble bien me rappeler que les 3 timers ne sont pas réglés pareil et aussi que les modes de PWM ne sont pas les mêmes sur les 3 timers.
Il,me semble que le calcul devrait être plus précis si la sortie PWM utilisée dépendait du timer 0.

Remarque :
Un micro-contrôleur à 1€ n'est pas aussi simple à utiliser qu'un fréquencemètre à plusieurs kilo €.
Cela ne veut pas dire que c'est infaisable, cela veut dire qu'il faut prendre en compte les caractéristiques réelles du micro et celle de la configuration Wiring/arduino et non pas celle souhaitées.
Pour cela il faut lire la datasheet du micro.
Pour un usage particulier il est tout à fait envisageable de ne pas rester clampé sur les réglages Wiring/arduino et de modifier les réglages des timers.
Cela ne présente aucun danger car la fonction init() est exécutée à chaque démarrage du micro.

Vos réponses sont claires, nettes, précises et font la lumière sur bien des choses que je bidouillais sans vraiment savoir ce que je faisais, merci beaucoup.
J'ai simplement utilisé analogWrite pour définir une PWM sans passer par les timers, aspect sur lequel je dois encore bûcher...

Vu que mon code complet englobe plein de librairies différentes, pour ne pas interférer avec le reste du programme, j'ai intégré la chose en activant les interruptions en début de boucle, je lis ma fréquence, je désactive les interruptions et la boucle continue. Dans ma logique, dites moi si je me trompe, c'est ce qui consomme le moins de ressources sachant que derrière, il faut rérer une rtc, les interruptions du compteur kilometrique, les interruptions d'une barrière infrarouge, et la gestion d'un ecran tft de type HMI (nextion via altsoftserial).

Je ne vois pas bien ce que tu veux faire en dévalidant les interruptions dans la loop. Tu va perdre des front et tu risque de mesurer plusieurs période à la fois.

Actuellement tu calcules la fréquence toutes les 2ms (à 500Hz), c'est sans doute inutile d'avoir un calcul si souvent.
Ce que tu peux faire c'est par exemple mesurer 10 périodes consécutives, ce qui nécessite des calculs beaucoup moins fréquent et en plus améliore la résolution.

void rpm_pulse()
{
 static int cpt=0;
 if (++cpt>=10)
 {
   unsigned long end=micros();
   elapsed = end - start;
   start = end;
   freq = 10000000.0 / elapsed;
   rpm = freq * 30;
   cpt=0;
 }
}

J'ai simplement utilisé analogWrite pour définir une PWM sans passer par les timers, aspect sur lequel je dois encore bûcher...

On ne s'est pas compris parce que tu manquais de connaissances dans la PWM sur 328p

  1. analogWrite manipule les timers pour toi
  2. sur un 328p 1 timer peut contrôler 2 sorties (bien définies)

Donc le timer 0 qui sert au fonctions millis, micro et delay contrôle aussi 2 sorties pour faire de la PWM.
Au passage il peut faire autre chose que de la PWM . Mais n'est pas géré par les fonctions Wiring/arduino, c'est un autre sujet.

En pj je te mets une anti-sèche perso qui, par timer, donne les sorties contrôlées avec leur nom officiel Atmel et le pseudo donné par Wiring/arduino.

Donc je te propose de refaire la manip en utilisant 1 des deux sorties contrôlée par le timer 0.

timers_compteurs.pdf (35.5 KB)

68tjs:
On ne s'est pas compris parce que tu manquais de connaissances dans la PWM sur 328p

  1. analogWrite manipule les timers pour toi
  2. sur un 328p 1 timer peut contrôler 2 sorties (bien définies)

Donc le timer 0 qui sert au fonctions millis, micro et delay contrôle aussi 2 sorties pour faire de la PWM.
Au passage il peut faire autre chose que de la PWM . Mais n'est pas géré par les fonctions Wiring/arduino, c'est un autre sujet.

En pj je te mets une anti-sèche perso qui, par timer, donne les sorties contrôlées avec leur nom officiel Atmel et le pseudo donné par Wiring/arduino.

Donc je te propose de refaire la manip en utilisant 1 des deux sorties contrôlée par le timer 0.

J'obtiens 980Hz sur mes sorties liées au timer 0, ce qui d'après la littérature est normal. Où veux-tu en venir ?

J'obtiens 980Hz sur mes sorties liées au timer 0, ce qui d'après la littérature est normal. Où veux-tu en venir ?

Puisque tu trouvais 500 Hz au lieu de 490 avec millis()

Si c'est le cas, pourquoi avec millis j'ai un 500Hz tout rond ?

et comme tu n'avais pas répondu précisément à ma proposition d'utiliser le même timer pour la fonction millis et la PWM et que la documentation Arduino est obscure et incomplète j'en ai remis une couche avec un peu de doc personnelle.

Par contre j'ai des doutes sur la mesure de 980 Hz, avec une sortie liée au timer 0 c'est normalement 490 Hz. Je pense plutôt à une sortie liée au timer 1.

Salut à tous.

Sans trop rentrer dans les détails (je vais être en retard au boulot), le 328 a une pin faite pour la mesure de fréquence : ICP1 (la N°8 si je ne me trompe). Cela utilise le timer 1 (comptage en 16 bits) en hard, et la mesure du delta t devient donc une simple soustraction 16 bits à faire dans une interruption (TIMER1_ICP_vect). qui dit interruption dit donc traitement "caché".

Bref, la variable delta_t sera mise à jour automatiquement à chaque impulsion, avec une excellente précision, et accessible tout le temps. Le calcul de la vitesse de rotation restera quand même une division entière à faire dans le loop(), mais j'aurais tendance à dire que c'est la solution la plus économique, puisque cette pin a été mise là pour ça.

Se référer au datasheet, voire même sur le site d'ATMEL qui propose (en cherchant bien) des AN (Application Notes) très explicites sur l'utilisation des ressources internes de leurs processeurs...

Bonne journée!

Il y a ces librairies qui pourraient t'intéresser:
http://interface.khm.de/index.php/lab/interfaces-advanced/frequency-measurement-library/
http://interface.khm.de/index.php/lab/interfaces-advanced/arduino-frequency-counter-library/