Calcul de vitesse d'un objet par interruption

Bonjour,

Je suis actuellement bloqué sur un petit projet de mesure de vitesse. Mon système observe en plusieurs points le passage de mon objet. Chaque passage provoque un passage de LOW à HIGHT qui provoque un interruption et un enregistrement du temps à son passage :

...
const byte In1 = digitalPinToInterrupt(19); 
...
const byte I1 = 19;
...
unsigned int t1 = 0;
...
void loop (){
...
  pinMode(In1, INPUT_PULLUP);
  attachInterrupt(In1,TEMPS1,RISING);
...
void TEMPS1(){
  t1 = millis ();
  detachInterrupt(In1);
}
...

Si je veut être capable de mesure une vitesse max de 500 km/h entre deux points de mesure espacés de 30 cm (fixe et non modifiable), mon interruption doit prendre moins de 2,16ms, si je compte bien. Je me suis donc équipé d'un Arduino Mega, pour avoir assez d'interruption direct, mais mon système n'arrive pas à prendre les temps correctement.

Ma question est donc d'où pensez vous que mon problème puisse provenir (matériel ou programmation) ?

2ms c’est une éternité pour un processeur même à 16MHz

Pour commencer il faut déclarer les variables correctement.

Tout ce qui est partagé en lecture ou écriture entre le code principal et les interruptions doit être déclaré volatile. Ensuite millis retourne un unsigned long. Avec un int vous n’irez pas plus loin que 65 secondes de fonctionnement. Il faut aussi de la précision dans la mesure. millis() n’est pas adapté, il vous donne une granularité de 50% du temps à mesurer...il faut prendre micros().

Enfin il faut caractériser la mesure. Si vous avez 2 points de mesure il vous faut deux variables pour mémoriser le passage et calculer le delta T dT qui, connaissant la distance d entre les deux points de mesure, vous donnera la vitesse (v = d / dT)

Si ça va à 500km/h et que vos deux points de mesure sont proches, il va falloir être précis dans la distance séparant les deux points de détection et donc la qualité/finesse (laser ?) du capteur est importante si vous voulez avoir de la précision sur la vitesse. 1cm d’erreur si vous avez un cône de mesure aura un impact de +/-3% d’erreur soit 15km/h d’incertitude à 500km/h.

Pour le câblage, les 2 détecteurs vont sur la même pin ?

Utiliser une interruption n'est pas toujours la solution la plus efficace.
Le traitement d'une interruption n'est pas instantané, il y a la sauvegarde/restauration du contexte, les opérations que tu fais pour traiter ton problème.
Pendant le temps passé dans la routine d'interruption millis() ou micros() n'incrémente plus. Ce qui va introduire une erreur dans le temps mesuré.

Si ton programme ne fait rien d'autre que tester des entrées et mémoriser des temps de passage. Une boucle de polling sera certainement plus efficace et plus précise.
Pour optimiser cette boucle, tu peux t'arranger pour limiter le nombre de lecture de registres en affectant toutes les entrées que tu veux superviser dans le même port d'entrées/sorties et ne pas utiliser digitalRead mais lire directement le registre d'I/O.

Merci pour vos réponses.

Pour la partie micro()/unsigned long, j'avais changé pour une variable t1 ( int ) = millis() pour voir si j'arrivais à gagner du temps.
Par rapport aux points de mesure, j'en ai 4 sur 4 entrées d'interruption différentes, je peux donc faire une moyenne pour palier au manque de précision.
Par contre, j'avais oublié que l'incrémentation du temps ne se faisait pas lors de l'interruption. Je vais donc partir sur une boucle de surveillance comme conseillé par fdufnews

fdufnews:
Pendant le temps passé dans la routine d'interruption millis() ou micros() n'incrémente plus. Ce qui va introduire une erreur dans le temps mesuré.

pas sûr de vous suivre. Je trouve que c'est pas mal en fait, on a le temps au moment exact de l'interruption, donc on n'est pas impacté par quoi que ce soit.

Vous aurez des interruptions séparées pour les deux détecteurs et micros sera figé au moment de l’interruption donc à mon c’est jouable. (Le polling a lui aussi son coût à cause de la loop)

Quelques corrections par rapport au post original sont fait en italique (ajouts) ou en barré (retrait)

Pour avoir joué avec 1à 4 interruptions avec les timers et relevé et interprété pas mal de chronogrammes (super merci à ceux qui ont conseillé d'acheter un analyseur logique à 10 balles)...

Petit rappel: les fonctions delay(), millis() et micros() utilisent le timer 0 (8 bits). La résolution sur une MEGA est de 4µs. Pour compter le temps, on utilise une variable 42 bits non signée dont les 2 bits de poids faibles sont toujours à 0, les 8 bits de poids milieu sont dans le timer0 (registre TCNT0) et les 32 bits de poids forts sont dans la RAM. le registre TCNT0 s'incrémente toutes les 4µs, et toutes les 256*4µs (1ms environ) le timer0 interrompt le fonctionnement normal pour incrémenter les 32 bits de poids fort. Une correction permet de n'incrémenter les 32 bits que toutes les ms. Cette interruption doit durer environ dure au minimum 5,5µs (je pense que le temps est constant, je n'ai pas analysé le code).
L'appel à micros() retourne en gros les 32 bits de poids faibles de la fameuse variable 42 bits. Pour millis() on retourne cette variable 42 bits divisé par 1000 les 32 bits de poids forts. Les 32 bits de poids faibles débordent toutes les 4294967296 µs (232) 4194304000µs (222*1000) c'est à dire toutes les 70mn, les 42 bits font le tour en 50 jours.

Pendant le temps passé dans la routine d'interruption millis() ou micros() n'incrémente plus

Ce n'est pas aussi simple. Pendant la routine d'interruption TCNT0 ne va continuer de s'incrémenter, mais pas les 32 bis de poids fort. Quand on appelle micros(), les 10 bits de poids faibles sont donc remis à jour, mais pas les poids forts. Si la routine d'interruption est en dehors de l'incrémentation des poids forts de ma variable de 42 bits, micros() fonctionne correctement et s'incrémente!
Si on arrive au mauvais moment, l'incrémentation ne se faisant pas, on fait alors une erreur de 1024 ou 1000 en moins.

Pour les septiques, exécutez le programme

void setup()
{
// Preparation de l'affichage
Serial.begin(115200);

// Mettre en route le timer 1 (16 bits)
TCCR1A = 0b00000000; // Pas de comparaisons, mode CTC
TCCR1B = 0b00011101; // Mode CTC ICR (12), base de temps 64µs
ICR1 = 15624; // Interruption toutes les secondes
TIMSK1 = 0b00100000; // Autoriser les interruptions du timer 1
}

 
ISR(TIMER1_CAPT_vect)
{
Serial.println(micros()); // Ecriture de 10 échantillons
Serial.println(micros());
Serial.println(micros());
Serial.println(micros());
Serial.println(micros());
Serial.println(micros());
Serial.println(micros());
Serial.println(micros());
Serial.println(micros());
Serial.println(micros());
Serial.println(); // Saut de ligne pour séparer les paquets d'échantillons
}

void loop()
{
}

On obtient pour le premier paquet écrit pendant la première interruption:

999872
1000168
1000508
1000848
1001188
1000508
1000848
1001188
1000816
1000676

On peut d'abord voir que micros() varie pendant la procédure d'interruption. Si il n'y a pas entre deux valeurs le temps utile pour l’incrémentation, les deux valeurs sont correctes et la différence est de 340µs ce qui correspond au temps mis pour faire l'instruction Serial.println(micros()); . Sinon, le résultat est faux de la valeur de l'incrémentation, c'est à dire de 1024. Il y a quelque valeurs qui ne donnent pas 340µs ou 340µs-1024µs, je n'ai pas encore analysé pourquoi.

Pour millis(), cela revient presque à utiliser les 32 bits de poids forts qui ne s'incrémente plus. Comme la division est pas 1000, il devrait être possible que deux appels à millis() donne deux valeurs différentes, mais je n'ai pas réussi à le montrer. Pour millis(), comme on retourne les 32 bits de poids forts qui ne s'incrémentent jamais, deux appels à millis() donne donc toujours la même valeur. [/i] On doit alors dire:

Pendant le temps passé dans la routine d'interruption millis()n'incrémente plus
Pendant le temps passé dans la routine d'interruption micros()n'incrémente mais ce n'est pas fiable, il peut y avoir débordement

fdufnews: Pendant le temps passé dans la routine d'interruption millis() ou micros() n'incrémente plus. Ce qui va introduire une erreur dans le temps mesuré.
J-M-L: Pas sûr de vous suivre. Je trouve que c'est pas mal en fait, on a le temps au moment exact de l'interruption, donc on n'est pas impacté par quoi que ce soit.

La mesure du temps se fait entre deux appels successifs à la routine d'interruption. Si cette dernière est inférieure à 1ms, il se peut que cela retarde l'incrémentation de la variable 42 bits, mais pas des suivantes. Il y a une incrémentation qui sera décalée (l'interruption est mémorisée et sera déclenchée avec du retard, c'est à dire en sortant de la routine de mesure). Le temps est donc, en dehors de la routine , correct. Désolé fdufnews

Mais aussi désolé pour J-M-L si on utilise micros():

  • Si on a du pot et que l'incrémentation des 42 bits se fait en dehors des routines de mesure, le compteur aura la bonne valeur pendant tout le temps de la routine, et le temps pris ne sera pas pris au moment exact de l'interruption, mais au moment de l'appel à micros(). Toutefois ce décalage interviendra aussi la deuxième fois, et la mesure sera correcte.
  • Si par contre l'interruption du timer 0 se faisait pendant la routine d'interruption de mesure, on pourrait avoir un des temps faux de 1024µs

Bon, on fait quoi du coup?
Si on veut faire MA meilleure mesure (c'est ce que je ferais, il y a peut être mieux), c'est utiliser une routine d'interruption avec un timer, mais en évitant le débordement. Comme le timer 0n'est pas sympa, on peut utiliser le timer 1 (avec une MEGA, on peut aussi prendre le timer 3, 4 ou 5). C'est un timer 16 bits, ce qui permet une bonne résolution. On peut aussi choisir le rapport de division pour avoir une résolution de 62,5ns si le temps à mesurer ne dépasse pas 4ms, jusqu'à une résolution de 64µs pour une mesure ne dépassant pas les 4s. Pour être sûr de ne pas avoir le débordement du timer pendant la mesure, on met le compteur du timer à 0 ( TCNT1 = 0 ; ) au début de la mesure, et on lit la valeur de TCNT1 en fin de mesure (on remet le compteur TCNT1 à 0 pour la mesure d'après si on fait des paquets de mesures).

Le gros intérêt de procéder ainsi est que d'une part la mesure est bonne, mais aussi qu'elle se fait en masqué, ce qui permet de faire l'affichage sans se préoccuper si ce dernier est bloquant ou non.

Après avoir vainement cherché a voir la valeur de millis() changer dans une routine d'interruption, et que cela ne se passe pas exactement comme je l'ai écrit, j'ai été trifouiller du coté du code pour savoir pourquoi. Sans aucune garantie, il semblerait que sur le mot de 42 bits de la valeur du temps système, les 32 bits de poids forts sont incrémentes toutes les ms; il y a une correction faite. Du coup la fonction millis() retourne directement la valeur de 32 bits stockée.

Ce qui change surtout, c'est que dans une routine d'interruption millis() retournera toujours la même valeur.

Je corrige le post précédent.

Okay Vileroi, merci pour ton explication claire et précise, je vois qu'il me reste beaucoup à apprendre.

Je vais donc partir sur l'utilisation du Timer 1 dans des interruptions pour éviter d'avoir une boucle de surveillance dans un boucle de surveillance qui est mon propre à mon goût (j'en avais déjà une de base durant l'attente du passage de l'objet).

Ce qui change surtout, c'est que dans une routine d'interruption millis() retournera toujours la même valeur.

oui
Mais sa résolution est inadaptée pour le cas présent ou il veut pouvoir mesurer un intervalle de l’ordre de 2ms

Il me semble que lire micros() juste à l’entrée de l’ISR au pire sera en retard de 3 microsecondes, non ? ( on se fiche de lire millis() qui n’est pas assez précis ) et on a le même coût dû à la gestion de l’interruption dans les deux cas donc la différence de temps gomme ce retard dans la mesure. Bien sûr il faut que l’interruption soit super courte pour ne pas perdre une ms.

Je me trompe ?

sinon utiliser print dans une interruption c'est pas top. la classe HardwareSerial a été modifiée il y a quelques temps pour tester si les interruptions sont bloquées (ie on fait print dans une interruption, chose que font bcp de débutants) et dans ce cas le code fait du polling sur le flag de byte émis (register empty flag) ce qui rend le temps d'exécution de l'ISR dépendant du baud rate et de la longueur de la chaîne dès que l'on dépasse la taille du buffer... C'est pas génial à mon avis.

Et si on parlait cahier des charges au lieu de partir bille en tête sur des solutions logicielles ?

Vitesse de 500 km/h
Mon système observe en plusieurs points le passage de mon objet.

Première impression de "Top secret" :smiling_imp: .

Un objet qui se déplace à 500km/h c'est une balle de fusil ?, une flêche d'un arc ou plutôt un carreau d'arbalète ?

Plusieurs point de passages :
Un point de passage c'est quoi ? Un dispositif optique ?
Quel est son temps de réponse ? S'il y a un photo transistor des vérifications sont à faire il n'est pas assuré que ce soit adapté.
Point de passage = point de mesure,
"Plusieurs" c'est combien ?
Deux me paraîtrait logique, est bien cela ? S'il y en a plus de deux le logiciel va se compliquer.

Une mesure s'obtient toujours avec une précision, quelle précision faut-il sur la valeur de 500 km/h ? C'est loin d'être anodin, le principe même du choix de mesure peut-être remis en cause.

500km/h ok c'est la vitesse maximale, mais qu'elle est la vitesse minimale ?
Cette valeur à "peut-être" une importance dans la détermination de la taille des variables ?
Si on prend un pas petit pour avoir de la précision à 500 km/h et si la mesure min est de 50 km/h cela fera un très grand nombre de pas à sauvegarder.

Personnellement j'aurai tendance à ne pas utiliser les fonctions Wiring/arduino qui sont loin d'être rapides, sans parler de la soupe des interruptions qui est loin d'être claire. De plus comme l'a écrit J-M-L millis est complètement inadapté ici (millis n'est qu'une sur-couche à micro pour mesurer des temps plus long il ne faudrait pas l'oublier).

Je pense revenir à la base et utiliser directement le compteur d'un timer.
Delay, micro et millis le font mais avec tout un environnement logiciel qui n'est pas, sauf démonstration contraire, indispensable ici.
Un timer c'est un bloc d'électronique indépendant du reste du micro.

Dans un avr les timers sont soit 8 bits soit 16 bits.
Contrairement à ce qu'on lit le timer0 n'est pas sanctuarisé, il sert à la PWM sur 2 pins bien précises (voir datasheet du micro) et aux fonctions wiring/arduino : delay, micro et millis.
Si on trouve une solution pour ne pas utiliser micro et millis il reste delay MAIS bien avant wiring/arduino Atmel fournissait déjà une macro _delay() qui n'est pas basée sur un timer mais sur la parfaite connaissance qu'un concepteur à sur son produit.

Le risque de non fonctionnement que je vois est dans l'amplitude de la gamme de vitesse.
Faire une mesure à 500km/h ± 10% ne devrait pas poser de problème, il suffira de bien choisir la valeur du diviseur d'horloge du timer choisi, par contre une mesure entre 50 et 500 km/h me parait difficilement faisable avec cette solution.

D'où mes questions du début du message.

mariuso31:
Je vais donc partir sur l'utilisation du Timer 1 ...

68tjs:
Je pense revenir à la base et utiliser directement le compteur d'un timer.

Il te faut alors regarder le mode normal, (pas CTC), le timer tourne en permanence, et quand on veut deux points de mesures, on met à 0 le compteur, et sur le deuxième, on lit sa valeur.
La lecture des deux valeurs se font alors par interruption, sur front des broches (pin interrupt). Dans mon test j'avais pris le mode CTC parce que c'est le timer qui interrompait et pas les pins.

mariuso31:
Je vois qu'il me reste beaucoup à apprendre.

A moi aussi, je viens de voir comment fonctionne millis()!

mariuso31:
Okay Vileroi, merci pour ton explication claire et précise,

C'est le but du forum, et pour les fonctions d'interruption, ne as hésiter à poster de nouveau. Ce serait bien exceptionnel que cela fonctionne du premier coup, et que tu comprennes toute la doc du timer!

vileroi:
Ce qui change surtout, c'est que dans une routine d'interruption millis() retournera toujours la même valeur.

C’était surtout pour montrer que l'on peut utiliser millis() dans une routine d'interruption.

J-M-L:
Il me semble que lire micros() juste à l’entrée de l’ISR au pire sera en retard de 3 microsecondes, non ? ( on se fiche de lire millis() qui n’est pas assez précis ) et on a le même coût dû à la gestion de l’interruption dans les deux cas donc la différence de temps gomme ce retard dans la mesure.

La différence fait effectivement que que la mesure n'est pas faussée. Et si il y a une différence ente les instructions, on peut toujours corriger.
Mais le problème n'est pas là; c'est qu'entre les deux évènements il peut y avoir le débordement et on se retrouve avec t2-t1 négatif; Mais apriori (je dis à priori, mais je n'ai pas vérifier), en rajoutant 1024, la mesure devrait être juste.

J-M-L:
Bien sûr il faut que l’interruption soit super courte pour ne pas perdre une ms.

Inférieure à 1ms pour ne pas perdre un incrémentation du compteur du timer 1, mais 1ms ce n'est pas vraiment très court ("2ms c'est une éternité pour un processeur même à 16MHz")!

J-M-L:
Sinon utiliser print dans une interruption c'est pas top.

C'est pour monter que micros() est encore vivant dans une routine d'interruption, et pour voir que la valeur peut présenter des erreurs toutes les ms. Dans mon cas même si la fonction durait 10mn cela fonctionnerait quand même. Le but aussi est que la fonction dure plusieurs ms pour voit les erreurs dues au manque de comptage du temps. Bien entendu, à chaque appel, l'horloge système se retarde, mais je n'ai pas besoin de l'utiliser.
D'habitude j'essaie de soigner particulièrement mon code et les fonctions d'interruptions.
C'est pour cela que j'avais fait des suppositions sur le fonctionnement de l'horloge qui sont fausses. Je trouve ce code vraiment bizarre. Si l'on veut compter les ms, pourquoi avoir pris un timer qui compte jusqu'à 256 et pas 250? Cela aurait eu une influence sur le PWM? Cela aurait été si simple avec une fonction d'interruption très courte, sans ajustements.... :frowning:

68tjs:
Contrairement à ce qu'on lit le timer0 n'est pas sanctuarisé, il sert à la PWM sur 2 pins bien précises (voir datasheet du micro) et aux fonctions wiring/arduino : delay, micro et millis

C'est pas ça? Réservé pour ces cas? A un moment j'ai voulu utiliser le timer0, mais on ne peut pas utiliser ISR(TIMER0_OVF_vect) qui est pris par l'horloge système.

68tjs:
Je pense revenir à la base et utiliser directement le compteur d'un timer.

Je pense que c'est une bonne solution, mais à y réfléchir, cela revient à se faire la fonction micros() un peu plus adaptée.

Pour la précision en utilisant un timer on peut avoir une résolution sur la mesure du temps de 62,5ns. et pour les basses vitesses, on utilise la résolution de 64µs... Après comme les instructions durent de 1 à 3 cycles d'horloge, la précision maximale va dépendre de l'instruction qui est interrompue et que l'on doit terminer avant, et devra se situer de l'ordre de 400ns.

68tjs:
Le risque de non fonctionnement que je vois est dans l'amplitude de la gamme de vitesse.
Faire une mesure à 500km/h ± 10% ne devrait pas poser de problème, il suffira de bien choisir la valeur du diviseur d'horloge du timer choisi, par contre une mesure entre 50 et 500 km/h me parait difficilement faisable avec cette solution

Pour l'instant je ne parle que de mesure de temps. Pour la mesure de vitesse, il faut la mécanique.... Faire une mesure de temps précise avec une variation de 10 fois, ne me semble pas poser de problèmes. Si on sait mesurer 500km/h ± 10% soit 500km/h ± 50km/h soit encore temps ± temps/10, si la vitesse est 10 fois plus faible, le temps est 10 fois plus long, et comme l'erreur sur le temps est quasi constant, on fera une mesure 10 fois plus précise soit 50km/h ± 0.5km/h
Ceci à condition que la mesure du temps utilise la même base de temps (pas de débordement). Sinon, en changeant de base de temps, on aura moins de précision.

Pour avoir une précision de 10% sur la vitesse avec un timer, je peux générer des implusions entre 0,4s et 6µs. On devrait bien pouvoir mesurer une vitesse entre 5 et 500km/h.

68tjs:
Point de passage = point de mesure,
"Plusieurs" c'est combien ?
Deux me paraîtrait logique, est bien cela ? S'il y en a plus de deux le logiciel va se compliquer.

Pas forcément, une fonction d'interruption qui se déclenche pour tous les points de mesure, et qui met dans un tableau la valeur du compteur. Avec micros() c'est sans doute plus compliqué, et encore.

Reste le top secret pour savoir si on peut mettre des capteurs...

Mais le problème n'est pas là; c'est qu'entre les deux évènements il peut y avoir le débordement et on se retrouve avec t2-t1 négatif; Mais apriori (je dis à priori, mais je n'ai pas vérifier), en rajoutant 1024, la mesure devrait être juste.

là où je suis d'accord avec vous c'est la construction de micros():

Timer0 est sur un prescale de 64 donc on a un tick toutes les 64/16m = 4µs et donc effectivement le registre TCNT0 (8 bits) s'incrémente de 1 toutes les 4µs.

Comme Timer0 est un timer 8 bits il va faire son overflow à 256, soit toutes les 1024 µs. Cet overflow déclenche l'interruption TIMER0_OVF_vect et dans cette ISR, on fait le cumul du nombre d'overflow dans la variable timer0_overflow_count (32 bits).

Comme on est dans une ISR, elle ne peut pas être interrompue. On en ressort avec timer0_overflow_count incrémenté ET TCNT0 à 0.

On a donc deux variables, une de 32 bits et une de 8 bits = 40 bits et la fonction micros nous retourne sous forme d'une variable de 32 bits le résultat de la construction suivante:

  • les 32 bits de timer0_overflow_count décalés de 8 positions à gauche
  • auxquels on on colle dans l'octet de poids faible les 8 bits de TCNT0
  • puis enfin on décale le tout de 2 bits à gauche pour avoir le bon nombre de microsecondes (puisque 1 tick = 4µs)

ça ressemble en gros à cela:

Là où je ne vous suis pas: je ne vois pas comment la différence des deux mesures (en calcul entier non signés) peut être négatif par définition.

Comme indiqué sur le graphique, on ne peut appeler micros() que avant ou après l'overflow, pas pendant puisque l'on est dans l'ISR. et quand on est dans micros(), les interruptions sont bloquées aussi (il y a un cli()). les deux parties des 42 bits sont donc "stables" et dans la plupart des cas on aura t1 plus grand que t0 et la soustraction nous donne le bon résultat.

Il y a un cas où t1 peut être inférieur à t0 c'est quand la formule qui donne micros() fait son overflow.

Imaginons que timer0_overflow_count vaut 0x003FFFFF (3F c'est 0011 1111) quand on décale de 8 bits on est à 0x3FFFFF00, quand on ajoute TCNT0 au max on est à 0x3FFFFFFF et quand on décale de 2 bits de plus on est à t0 = 0xFFFFFFFC

au tick du timer suivant on fait l'overflow, l'ISR est appelée et timer0_overflow_count vaut 0x00400000, TCNT0 vaut 0x00.

Disons qu'on appelle micros() juste après le déclenchement de l'ISR. On aura alors
t1 = (( 0x400000 << 8 ) + 0x00) << 2 ce qui nous donne t1 = 0x00000000

On a bien t1 plus petit que t0. Mais magie de la soustraction en nombres non signés: t1-t0 = 4. donc ça fonctionne très bien (on est dans la résolution des 4µs).

Est-ce que je rate quelque chose dans votre explication ??

Là où je ne vous suis pas: je ne vois pas comment la différence des deux mesures (en calcul entier non signés) peut être négatif par définition.

Effectivement la différence de deux mesures sera toujours un nombre positif, mais qui n'est pas correct. Il peut être négatif si on le range dans un entier non signé;
Le problème soulevé était "Pendant le temps passé dans la routine d'interruption millis() ou micros() n'incrémente plus". Pour millis(), c'est vrai. Mais pour micros() c'est faux. micros() continue de bouger, le programme du post #5 montre le contraire. Quand on est dans une routine d'interruption micro() n'est pas constant mais la partie 32 bits hors de TCNT0 ne bouge pas vu qu'en général on a bloqué les interruptions notamment celle qui la mettait à jour.
micros.gif
Ma routine est en bleu. Pendant ma routine, je mesure le temps t0 et t1. Si je n'ai pas de chance, TCNT0 repasse à 0 entre les deux mesures. Comme je suis dans la routine d'interruption, timer0_overflow_count n'est pas mis à jour, les interruptions sont désactivées. Quand je sortirai de ma routine, l'interruption sera prise en compte (un peu tard) et timer0_overflow_count prendra la valeur qu'il faut. Mais pendant ma routine, si je fais les calcul (sur 8 bits non signés) t2-t1, j'obtiens bien le bon résultat. Il y a un débordement, mais cela ne gêne pas, car je suis sur 8 bits.

Par exemple, le temps de mesure est de 0x50*4µs:

  • si t0 vaut 0x20, t1 vaudra 0x70, t1-t0 vaut bien 0x50
  • si t0 vaut 0xF0, t1 vaudra 0x40, t1-t0 vaut bien 0x40-0xF0=0x50

Si on utilise TCNT0 tout va bien. Mais pas si on utilise micros()! car il est complété avec 22 bits devant et 00 derrière. si micros0 et micros1 sont les valeurs de micros() à t0 et t1
Par exemple, le temps de mesure est de 0x50*4µs:

  • si t0 vaut 0x20, micros0 lu sera par exemple à 0x00000080, t1 vaudra 0x70, micros1 vaudra 0x000001C0, micros1-micros0 vaut bien 0x00000050
  • si t0 vaut 0xF0, micros0 lu sera par exemple à 0x00000200, t1 vaudra 0x40, micros1 vaudra 0x000000C0, mais micros1-micros0 vaut 0xFFFFFEC0 donc pas du tout la bonne valeur

On peut donc faire des mesures avec micros() si on a la chance de ne pas tomber au moment de l'overflow du timer, ou si on utilise des entiers SIGNES!

Quand on reprend le programme du post #4 qui permet de mesurer le temps mis par Serial.println(micros()); on obtient la succession de valeurs

999872
1000168
1000508
1000848
1001188
1000508
1000848
1001188
1000816
1000676

Pour avoir le temps d'une instruction, il suffit de faire la différence d'une ligne moins la précédente. et on voit bien que les valeurs n'étant pas toujours croissantes, on va obtenir ces valeurs "négatives".
On peut donc parfaitement utiliser micros() dans une routine d'interruption et faire des mesures de temps a l'intérieur cette routine. Précautions:

  • la routine ne doit pas dépasser 1ms (pour ne pas perdre un mise à jour de timer0_overflow_count
  • il faut des entiers longs SIGNES
    Maintenant si on utilise micros() à l'intérieur d'une routine d'interruption pour faire des calculs à l'extérieur, on peut
  • soit avoir la bonne valeur de micros() si l'overflow du timer 0 n'intervient pas entre le début de la routine et l'appel à micros()
  • soit avoir une valeur trop petite de 1000 dans le cas contraire.
    Et si on fait deux mesures dans deux appels à une ISR, l'une ou l'autre des valeurs peut être trop petite de 1000. Le résultat n'est pas bon.
    C'est pour cela que l'on dit de ne pas utiliser millis() ou micros() dans une ISR.
    Pareil pour delay().

Comme on est dans une ISR, elle ne peut pas être interrompue

Pas tout à fait exact non plus, cela dépend de qui a écrit le programme, pourquoi faire...
Supposons que ce soit moi. Je cherche à écrire un programme qui sous interruption donne les temps espacés d'une Serial.println(micros()); et que je veuille faire des mesures correctes. Je sais que l'instruction durant environ 340µs, ma routine sous interruption va durer 3,4µs (elle est appelée toutes les secondes, j'ai donc du temps libre devant moi). Si le timer0 est suspendu je vais perdre 3 ou 4 incrémentations. Pour éviter cela je vais réactiver les interruptions. En particulier celle du timer0, mais aussi la mienne qui ne peut être appelée récursivement car elle dure trop peu. Et puis comme j'ai du temps, j'ai envie de rajouter un delay(10); rien que pour montrer que l'on peut utiliser cette fonction dans une ISR. Voici le programme (c'est celui du #4 sur lequel j'ai rajouté la réactivation du timer0 et les delay():

void setup()
{
  // Preparation de l'affichage
  Serial.begin(115200);

  // Mettre en route le timer 1 (16 bits)
  TCCR1A = 0b00000000; // Pas de comparaisons, mode CTC
  TCCR1B = 0b00011101; // Mode CTC ICR (12), base de temps 64µs
  ICR1 = 15624; // Interruption toutes les secondes
  TIMSK1 = 0b00100000; // Autoriser les interruptions du timer 1
}


ISR(TIMER1_CAPT_vect)
{
  sei(); // Permettre au timer de faire son boulot
  Serial.println(micros()); // Ecriture de 10 échantillons
  delay(10);
  Serial.println(micros());
  delay(10);
  Serial.println(micros());
  delay(10);
  Serial.println(micros());
  delay(10);
  Serial.println(micros());
  delay(10);
  Serial.println(micros());
  delay(10);
  Serial.println(micros());
  delay(10);
  Serial.println(micros());
  delay(10);
  Serial.println(micros());
  delay(10);
  Serial.println(micros());
  Serial.println(); // Saut de ligne pour séparer les paquets d'échantillons
}

void loop()
{
}

Comme j'ai réactivé les interruptions delay() fonctionne et millis() ne donne plus de résultats incorrects. Cela donne en sortie:
......

3093024

3999872
4010224
4020572
4030916
4041268
4051616
4061964
4072316
4082672
4093024

4999872
5010224

.....

Et on a bien des valeurs croissante. Tout fonctionne... sous ISR!
Bien évidemment, je peux le faire car ma routine est appelée moins souvent que sa durée. Dans le cas contraire, je deviens récursif, et la pile déborde. autre solution: je désactive le timer1 puis je réactive les interruptions.

micros.gif

Il peut être négatif si on le range dans un entier non signé;

si on met des bugs et qu'on code avec les pieds alors tout est perdu.... :slight_smile: :wink:

problème soulevé était "Pendant le temps passé dans la routine d'interruption millis() ou micros() n'incrémente plus"

euh non... je ne crois pas... je contestais le point que vous m'adressiez:

  • Si par contre l'interruption du timer 0 se faisait pendant la routine d'interruption de mesure, on pourrait avoir un des temps faux de 1024µs

Mais bon - en fait, je crois qu'on ne discute pas de la même chose: Dans mon schéma les petites étoiles marquent les mesures de t1 et t2. Ce sont des évènements ponctuels. Vous décrivez une attente active dans une ISR...

L'approche que vous décrivez n'est pas ce qui est recommandé avec une interruption. Une interruption doit être hyper courte. on ne fait pas de l'attente active de plusieurs ms dans une ISR. Donc effectivement ça conduit à toutes sortes de misères dont celle que vous décrivez (ie "coder avec les pieds" comme dit ci dessus)

Donc pour moi, on a 2 capteurs de passage de l'objet qui déclenchent des interruptions ponctuelles où l'on capture le temps de passage et la loop fait la vérification d'acquisition pour impression à l'écran ou autre chose - un truc du genre:

const byte pinT0 = 2;
const byte pinT1 = 3;
const float distance = 0.30; // 30cm entre les capteurs

double vitesse;
volatile uint32_t t0;
volatile uint32_t t1;
volatile bool passageSurCapteur2 = false;

void passageT0() {
  t0 = micros();
}

void passageT1() {
  t1 = micros();
  passageSurCapteur2 = true;
}

void setup() {
  Serial.begin(115200);
  pinMode(pinT0, INPUT_PULLUP);
  pinMode(pinT1, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(pinT0), passageT0, FALLING);
  attachInterrupt(digitalPinToInterrupt(pinT1), passageT1, FALLING);
}

void loop()
{
  if (passageSurCapteur2) {
    noInterrupts();
    if (t0 != t1) vitesse = distance / (t1 - t0);
    else vitesse = NAN;
    Serial.print(F("Vitesse = "));
    if (isnan(vitesse))
      Serial.println(F("erreur de lecture"));
    else
      Serial.println(vitesse);
    passageSurCapteur2 = false;
    interrupts();
  }
}

(tapé ici, il y a peut-être des fautes de frappe c'est juste le principe de base sans traitement des exceptions comme le bolide passe devant 2 mais pas devant 1 par exemple. ça se règle avec des états.)

avec cette approche je pense qu'il n'y a aucun des risques que vous décrivez (en supposant que les capteurs fonctionnent et les détections se font bien uniquement du premier vers le second)

PS/ oui tout à fait d'accord sur votre point des ISR non interruptibles;

Pas tout à fait exact non plus, cela dépend de qui a écrit le programme, pourquoi faire...

J'ai pris un petit raccourci de simplification pour ce cas: je parlais de ce qui se pratique le plus souvent mais si on veut, on peut.

L'approche que vous décrivez n'est pas ce qui est recommandé avec une interruption. Une interruption doit être hyper courte. on ne fait pas de l'attente active de plusieurs ms dans une ISR. Donc effectivement ça conduit à toutes sortes de misères dont celle que vous décrivez (ie "coder avec les pieds" comme dit ci dessus)

C'est pas des misères, ce sont des challenges. Quand j'entends dire que l'on ne peut pas utiliser millis() ou micros() dans une fonction d'interruption sans aucune preuve, je montre le contraire. Je n'ai pas de misères puisque cela fonctionne. Il n'en est pas de même pour ceux qui essaient d'utiliser millis() dans la boucle loop() bien souvent (au vu des posts).

Maintenant, dire qu'une interruption doit être hyper courte, est pour moi un questionnement. Suffisamment courte pour que cela fonctionne, d'accord, mais pas plus. J'utilise un code actuellement (pour piloter des moteurs pas à pas) qui peuvent passer 60% du temps dans la routine d'interruption. C'est aussi une façon de masquer le code. la gestion est faite par interruption. Quand je donne un ordre pour tourner, la suite des pas est invisible; le moteur fait ses pas dans son coin, et je peux faire autre chose. Bien sûr il me mange du temps, mais je n'ai rien à gérer. Je peux aussi écrire du code bloquant pendant que mon moteur tourne...

Au passage dans le programme du post #4 il y a des Serial.print mais la fonction est quand même hyper courte: elle n'occupe que 35ms toutes les 1000ms soit 0,35% du temps.

Encore une fois je parlais d’une manière générale. Clairement vous savez ce que vous faites, maîtrisez très bien votre sujet et Comprenez tous les détails et conséquences de vos choix. Donc pourquoi pas.

Mais ce n’est pas ce que je recommanderais à un débutant. Le bout de code (ou du polling sur les PORTs si compatible avec la vitesse) serait ma proposition. Maintenant ce n’est que mon avis, vous pouvez ne pas le partager, je ne m’en offusquerai pas. Il y a toujours plusieurs façons de faire.

Clairement vous savez ce que vous faites, maîtrisez très bien votre sujet

Pas vraiment, sinon je n'aurai pas corrigé le post... Et j'ai pas encore pris le temps de comprendre la correction du timer0.

Mais ce n'est pas ce que je recommanderais à un débutant. Le bout de code (ou du polling sur les PORTs si compatible avec la vitesse) serait ma proposition

Je ne le conseille pas non plus, mais pour avoir une bonne mesure, le polling est nettement moins bon. En tout cas il skie vite le bonhomme!

Bonjour, pour apporter un peu de détaille pour 68tjs, mon projet est de rénover la partie mesure de vitesse d'un banc de tir. On regarde donc la vitesse d'un projectile qui vient sectionner des fils électriques à 4 endroits différents espacés de 30 cm chacun. On observe donc 0V quand le fil est en place et 5v quand le fil est sectionné.

Comme vitesse maximum, je me suis donné 500 km/h et 50 km/h en V min. On a donc un écart de 2.1 ms (pour 500 km/h ), jusqu'à 21.5ms (pour 50 km/h).
J'ai choisi de diviser mon timer 1 par 64, ce qui me donne un pas de 4 microsecondes et une valeur max de 262 ms, ce qui me semble bon.

...
  TCCR1A = B00000000;
  TCCR1B = B00000011;
...

void TEMPS1(){
  TCNT1 = 0;
  //Serial.println(t1);
  detachInterrupt(In1);
}

void TEMPS2(){
  t2 = TCNT1;
  //Serial.println(t2);
  detachInterrupt(In2);
}

void TEMPS3(){
  t3 = TCNT1;
  //Serial.println(t3);
  detachInterrupt(In3);
}

void TEMPS4(){
  t4 = TCNT1;
  //Serial.println(t4);
  detachInterrupt(In4);
}

Je suis encore entrain de revoir mon code, mais, durant mes essais, j'ai l'impression d'avoir un timer sur 8 bits, car je n'observe aucun relevé au dessus de 250. Je retombe même en dessous pour le dernier relevé.

vileroi:
Pas vraiment, sinon je n'aurai pas corrigé le post...

Soyez pas modeste, le simple fait d’aller voir et de lire et comprendre le code source fait partie de la démarche “pro”. Et corriger en admettant une imprécision c’est aussi une preuve de maturité !

:slight_smile:

Pour millis() en fait il savent que 1024 ce n’est pas 1000 et donc ils font à la main la gestion en entier d’une addition avec des petits bouts après la virgule qui se cumulent dans timer0_fract et quand cette partie fractionnaire dépasse (en représentation binaire adaptée) la ms ils ajoutent 1 à millis pour rattraper la dérive

 m += MILLIS_INC;
	f += FRACT_INC;
	if (f >= FRACT_MAX) {
		f -= FRACT_MAX;
		m += 1;
	}

	timer0_fract = f;
	timer0_millis = m;

la ruse pour que ce soit efficace c’est de traiter cela sur 1 octet seulement d’où le décalage de 3 bits à droite. 1000 est normalement représenté par 0b1111101000 et on ne perd rien en décalant, il est alors représenté par 0b1111101 et ça fonctionne car les 24 microsecondes manquantes à chaque appel sont ajoutées en étant aussi décalées de 3 bits (24 c’est 0b11000 et donc on ne perd pas d’infos en abandonnant les 3 zéros à droite)

Je suis encore entrain de revoir mon code, mais, durant mes essais, j'ai l'impression d'avoir un timer sur 8 bits, car je n'observe aucun relevé au dessus de 250. Je retombe même en dessous pour le dernier relevé.

t2, t3 et t4 sont bien définis comme uint16_t?