Compteur Arduino MEGA 2560

Bonjour,
Je suis en train d'utiliser un compteur sur l'arduino mega 2560. Ce compteur doit me permettre de générer des valeurs de fréquence, via pin output, entre "0" et 3250 Hz.
Le problème est que le compteur ne me permet pas d'atteindre "directement" ces valeurs. Je suis obligé de passer par 2 prédiviseurs.
Le premier à 1024 pour générer les valeurs de "0" Hz à 135Hz,
Le second à 1 pour générer les valeurs de 136Hz à 3250Hz.
Le problème rencontré est que lors du passage d'une plage à l'autre, il s'avère que le compteur passe dans une phase ou il ne génère plus de fréquence.

Question 1 : est-il possible d'utiliser 2 compteurs différents, indépendants pour contourner ce phénomène ?
Question 2 : existe-t'il un moyen de réduire le temps (temps de réinitialisation ?) du compteur ?
Question 3 : est-ce que mon code peut être optimisé afin de réduire ce phénomène ?

voici mon code :

#define SIGNAL4 6                       // ici on definit les sorties qui generent les signaux carres


unsigned char prescaler = 0b00001101;   // variable d'ajustement du prescaler
unsigned valeur = 65500;                    // frequenceInt 
int prescal = 0;


void setup () {
  delay(1000);
  Serial.begin(115200);
  pinMode (SIGNAL4, OUTPUT);  


  // ======================================================================================================================================================
  // Paramétrage du timer1, pour qu'il déclenche une interruption, à chaque fois que sa valeur sera égale à celle qu'on aura indiqué dans le registre OCR1A  
  // ======================================================================================================================================================
  noInterrupts();                 // On désactive les interruptions, pour commencer
  TCCR1A = 0b00000000;            
  TIMSK1 = 0b00000010;
  bitSet(TIMSK1, OCIE1A);         // On met le bit OCIE1A à 1 (contenu dans le registre TIMSK1)
  TCNT1 = 0;            // Mise du timer1 à zéro
  interrupts();         // Et, on ré-active les interruptions
}

// ======================
// Routine d'interruption
// ======================

ISR(TIMER1_COMPA_vect) {
  digitalWrite (SIGNAL4, !digitalRead(SIGNAL4));  
  TCCR1B = prescaler;
  OCR1A = valeur;     // mise à jour de la valeur OCR1A en fonction de la valeur envoyée
}
 
// =================
// Boucle principale
// =================

void loop () 
{
  if (Serial.available())                         // ici on regarde si qqechose vient de l USB
  {
    valeur = Serial.readStringUntil('\n').toInt();
    Serial.println(valeur);
  
    if ((valeur<2461) && (valeur>56)) 
    {
      prescaler = 0b00001101;                         // prescaler 1024
    }
  
    else if ((valeur<59300) && (valeur>2462)) 
    {
      prescaler = 0b00001001;                         // prescaler 1
    }
  
    else 
    {
      prescaler = 0b00001101;              // ici on definit le prescaler pour vitesse nulle
    }
  }
}

:warning:
Post mis dans la mauvaise section, on parle anglais dans les forums généraux. déplacé vers le forum francophone.

Merci de prendre en compte les recommandations listées dans Les bonnes pratiques du Forum Francophone

Pas bien clair ce passage à l'arrêt. Si on veut pouvoir améliorer quelque chose, il faut comprendre le pourquoi cela ne fonctionne pas. Si on change un prescaler, l'effet est immédiat, il n'y a pas de temps mort.

J'observe tout d'abord que pour les valeurs 2461 et 2462 les deux "if" sont faux et on se retrouve avec le prescaler "arrêt" ce qui ne doit pas se voir beaucoup car pour "arrêt" le prescaler vaut 1024. Je ne pense pas que le problème vienne de là. Ne faudrait-il pas des >= et des <= à la place des > et <?

Je ne suis pas certain de la suite. Mais supposons que le couple (prescaler, valeur) passe de (1, 59300) à (1024, 58) c'est à dire, on passe de 135Hz à 134Hz.
− quand le prescaler est à 1, on avait bien les 135Hz (l'horloge est à 16MHz, les inters à 16MHz/59300 et comme il faut deux interruptions par période de signal, on a bien les 135Hz.
− rien ne se passe tant que l'on est en dehors de la routine d'interruption. Il faut attendre la fin d'une demi-période pour que le changement soit effectif.
− le compteur TCNT1 atteint 59300. il repasse à 0 (car il a atteint le TOP) et on démarre la fonction d'interruption.
− l'entrée dans la routine sauvegarde les registres, je crois qu'il faut compter environ 20 cycles. Cela ne change pas grand-chose si c'est 10 ou 30. Suit la ligne:

digitalWrite (SIGNAL4, !digitalRead(SIGNAL4));

cela va durer environ 200 périodes d'horloges. Puis suit les deux lignes suivantes (2 cycles?); le compteur TCNT1 est alors à 230 environ.
− l'inter suivante aura lieu quand le compteur atteint la valeur 58. Mais il est à 230. Il faudra donc attendre qu'il passe presque un tour complet d'incrémentation pour qu'il repasse à 0 et arrive enfin à 58, soit les 65300 coup d'horloge à 16MHz/1024, ce qui dure donc environ 4s.

Si ce que je décris est correct, il ne faut pas que le compteur dépasse OCR1A aux changements.
Cela se passe que quand on passe le prescaler de 1 à 1024. Ce qui me paraît le plus simple est de systématiquement remettre le compteur TCNT1 à 0, ce qui fait que le prochain changement aura lieu dans une demi-période:

ISR(TIMER1_COMPA_vect) {
  digitalWrite (SIGNAL4, !digitalRead(SIGNAL4));  
  TCCR1B = prescaler;
  OCR1A = valeur;     // mise à jour de la valeur OCR1A en fonction de la valeur envoyée
  TCNT1 = 0;
}

Bien entendu, je peux me tromper, ton expérience peut le confirmer ou non.

 


 

C'est ton code, tu as choisi cette méthode. Mais on peut voir les choses différemment. Tu génères un signal carré de la façon suivante: un compteur est en mode CTC et fait un signal carré (que l'on utilise pas) et à chaque période déclenche une interruption qui crée le signal carré. Pourquoi ne pas utiliser directement la sortie du compteur? En plus la broche 6 est précisément une sortie de timer (OC4A). Dans ce cas, on n'a plus besoin d'interruption, le signal est créé avec le matériel.

Il y aura presque le même problème au passage du prescaler à 1024, que l'on peut presque éliminer en mettant le TNCTx à 0. Je dis presque car dans ton programme, le changement est synchrone avec le signal, dans la méthode ci-dessus on ne l'est plus. Si on demande un changement presque à la fin d'une demi-période, comme le changement repart de 0, on pourrait avoir au changement un état haut ou bas deux fois trop long (il doit être en moyenne 1,5 fois trop long).

Bonjour,

Merci pour cette réponse très complète, ça m'a vraiment aidé.
J'ai ainsi ajouté la remise à zéro du compteur TCNT1 et effectivement, si cette ligne n'est pas présente alors j'ai un arrêt du compteur pendant quelques secondes lors du basculement du prédiviseur 1 au prédiviseur 1024, Et en ajoutant la ligne, pas d'arrêt du compteur !

Ce constat est fait avec une simple LED sur la sortie, une visualisation plus à l'oscillo permettrait de bien constater que le changement de prédiviseur et fluide.

J'ai par ailleurs réajusté les valeurs des conditions pour couvrir toutes les plages.

En tout cas merci beaucoup pour toutes ces explications.

Merci du retour, c'était un peu à l'aveuglette.

Bonjour après observation sur oscillo je constate une dérive de la fréquence entre la valeur théorique et la valeur réelle. Je suppose que l'ajout du timer impacte la génération des signaux carrés...
Je me pose la question de savoir si je peux conditionner l'application du zéro compteur " TCNT1 " avec la valeur de passage d'un prédiviseur à un autre ?

  1. La formule théorique de la fréquence de sortie est (voir datasheet)
    image
    Cette fréquence est indépendante du temps passé dans l'interruption par construction du programme. Il y a un petit 1+ devant OCCR1A:
    Pour une valeur de OCR1A de 60, prescaler à 1024, cela devrait donner:
    f= 16HHz /(2x1024x61) soit 128,1Hz
    Bien souvent on oublie ce petit 1+ et on trouve 130,2Hz à la place.
    Cela se sent pour les faibles valeurs de OCR1A.

  2. Comme tu es par interruption, le changement se fait dans un temps fixe après le début de l'interruption. Mais quand vient le moment de l'interruption, le micro va finir son instruction qui peut durer 1, 2 ou 3 cycles. On peut donc avoir une petite fluctuation (jitter) sur les changements de 128ns. C'est peu observable toutefois (128ns devant les 7.000.000ns de la période si on est à 130Hz, mais c'est un peu plus important à 3250Hz

  3. Si tu n'as pas arrêté l'horloge système, une interruption remet l'horloge l'heure et prend de l'ordre de 6µs.Il y a dans ce cas une imprécision supplémentaire de 6µs: Un des fronts peut être retardé. Mais le suivant sera à la bonne place. La remise à l'heure intervient toutes les 1024µs mais on a peu de chances pour qu'elle tombe juste au moment du front. A 3250Hz la période valant 308µs, on a une chance sur 50 de tomber dessus. Il devrait y avoir 49 fronts "bien placés" pour un en retard de 6µs. Pour les fréquences plus basses, cela arrive moins souvent.
    Si tu n'utilises pas l'heure (delay, millis et micros), autant arrêter cette horloge. Cela ne mange pas de pain, le signal sera plus régulier...

  4. Le quartz est donné pour 16MHz et les calculs partent de là. Mais c'est en principe un résonnateur céramique d'une précision "acceptable". La fréquence n'est pas de 16,000000MHz mais peut être légèrement différente. Et en plus cela peut dépendre de la température. Si la fréquence est de 1% plus basse, toutes les fréquences générées seront donc 1% plus basses.

Par interruption, tu peux faire ce que tu veux, cela ne changera pas la fréquence. Rajouter un deuxième timer ne changera absolument pas les fréquences. Cela peut décaler les changements par rapport au temps "idéal", mais ces décalages sont "locaux" à un seul changement et ne sont pas cumulatif. Un peu comme si tu souhaites te coucher toujours à la même heure mais que tu veuille toujours finir le chapitre du livre. Parfois il est plus long, parfois plus court, parfois on t'appelles au moment de te coucher. Si un soir cela traîne, c'est à nouveau synchrone le jour suivant. Ce n'est pas parce que tu décides de lire tous les soirs 5mn de plus que le coucher va se décaler de 5mn puis de 10mn, puis de 15mn... Cela se décale par rapport au temps théorique, mais ce n'est pas cumulatif.

Je me dis qu'il y a peut-être une autre solution. Le problème intervient si on passe du diviseur 1 au diviseur 1024 avec une valeur inférieure à 200 ou 250. Au moment du changement on a dépassé la valeur de OCR1A et le compteur va faire une boucle entière. Il suffit peut-être de faire le changement avant:

ISR(TIMER1_COMPA_vect) {
  TCCR1B = prescaler;
  OCR1A = valeur;     // mise à jour de la valeur OCR1A en fonction de la valeur envoyée
  digitalWrite (SIGNAL4, !digitalRead(SIGNAL4));  
}

A priori, lors du changement de fréquence va intervenir plus tôt après le dernier front, et le temps devrait être plus juste (plus près de la nouvelle valeur). Et comme on ne remet pas le compteur à 0, envoyer deux fois le même ordre ne change strictement rien.

Je te laisse tester!

Bonjour, merci pour les conseils.
Concernant le calcul de la fréquence, c'est bien la formule que j'applique. Le code sans la ligne impliquant la remise à zéro du timer ( TCNT1=0;) permet bien de générer le signal carré avec une assez bonne précision. Par contre quand je l'ajoute, aussi au début ou à la fin du bloc d'interruption alors j'observe un déphasage, assez important car quand je dois théoriquement générer du 3240Hz, cela génère du 2345Hz. Ce déphasage n'est pas linéaire, application d'un coeff de correction pas forcément facile à intégrer.

Concernant l'arrêt de l'horloge systeme, je suis en train de chercher dans la datasheet atmega mais pas réellement trouvé comment la stopper.

J'ai aussi testé ce code, dans l'idée ça pourrait fonctionner car je n'ai pas observé de dérive au niveau de la fréquence, j'ai fait 5 montées en fréquences et 5 descentes et pas de rupture dans la phase de changement de prediviseur. ça semble plutot OK.
Si vous avez des remarques ou améliorations à proposer dans ce code, je suis preneur.
Merci beaucoup.

// VERSION AVEC REMISE ZERO TIMER CONDITIONNE 
// code arduino slave sur COM6 port USB droit
// communication avec Raspberry en USB

#define SIGNAL4 6                       // ici on definit les sorties qui generent les signaux carres

unsigned char prediviseur = 0b00001101;   // variable d'ajustement du prediviseur en fonction de la valeur de frequence
unsigned valeur = 65500;                    // frequenceInt 
int prescal = 0;

void setup () {
  delay(1000);
  Serial.begin(115200);
  pinMode (SIGNAL4, OUTPUT);  


  // ======================================================================================================================================================
  // Paramétrage du timer1, pour qu'il déclenche une interruption, à chaque fois que sa valeur sera égale à celle qu'on aura indiqué dans le registre OCR1A  
  // ======================================================================================================================================================
  noInterrupts();                 // On désactive les interruptions, pour commencer
  TCCR1A = 0b00000000;            
  TIMSK1 = 0b00000010;

  bitSet(TIMSK1, OCIE1A);         // On met le bit OCIE1A à 1 (contenu dans le registre TIMSK1)
  TCNT1 = 0;            // Mise du timer1 à zéro
  interrupts();         // Et, on ré-active les interruptions
}


// ======================
// Routine d'interruption
// ======================

ISR(TIMER1_COMPA_vect) {
  digitalWrite (SIGNAL4, !digitalRead(SIGNAL4));  

  if (TCCR1B!=prediviseur)
    {
      TCNT1 = 0;
    }
  TCCR1B = prediviseur;
  OCR1A = valeur;     // mise à jour de la valeur OCR1A en fonction de la valeur envoyée par arduino master (valeur OCR1A calculée en fonction de frequence horloge, prediviseur)

}
 
// =================
// Boucle principale
// =================

void loop () 
{
  if (Serial.available())                         // ici on regarde si qqechose vient de l USB
  {
    valeur = Serial.readStringUntil('\n').toInt();
    Serial.println(valeur);
  
  
    if ((valeur<2461) && (valeur>=58))                // ici on affine la valeur afin d etre plus proche de la frequence de transition ici 58
    {
      prediviseur = 0b00001101;                         // prediviseur 1024
      //Serial.print("1024");
    }
  
    else if ((valeur<=59230) && (valeur>2462))        // ici on affine la valeur afin d etre plus proche de la valeur de transition ici 59230
    {
      prediviseur = 0b00001001;                         // prediviseur 1
      //Serial.print("1");
    }
  
    else 
    {
      prediviseur = 0b00001101;              // ici on definit le prediviseur pour vitesse nulle
    }
  }
}

Avec le code précédent, mais quand même observé une rupture sur la changement de prediviseur, sur le passage de prediviseur 1 à 1024.

L'horloge système utilise le timer 0 et ce sont les interruptions qui sont utilisées. Il suffit simplement de désactiver l'interruption par
TIMSK0 &= 0xFE;

Si on veut la remettre il suffit alors de valider les interruptions du timer 0 par:
TIMSK0 |= 0x01;

Effectivement il ne faut pas que cette instruction soit systématique car elle modifie le comptage. On peut la mettre comme tu l'a fait uniquement quand c'est besoin.

Mais as-tu essayer de ne mettre que

ISR(TIMER1_COMPA_vect) {
  TCCR1B = prescaler;
  OCR1A = valeur;     // mise à jour de la valeur OCR1A en fonction de la valeur envoyée
  digitalWrite (SIGNAL4, !digitalRead(SIGNAL4));  
}

C'est les digitalRead et digitalWrite qui sont longs. je pense que cela devrait fonctionner.
Que cette instruction soit au début ou à la fin, cela ne change pas la régularité. Si on la met après, elle sera toujours en retard du même temps. Retarder les deux fronts ne change pas le signal.

if ((valeur<=2461) && (valeur>=58)) Non?

C'est en trop.

Pour l'arrêt, c'est possible de mettre une fréquence nulle. Pour cela il suffit de désactiver l'interruption. Comme pour le timer 0:
TIMSK1 &= 0xFE; // Donne une fréquence nulle
Bien entendu il faut le réactiver si valeur est dans les clous.

Bonjour,

Oui j'ai essayé comme ça :

ISR(TIMER1_COMPA_vect) {
  TCCR1B = prescaler;
  OCR1A = valeur;     // mise à jour de la valeur OCR1A en fonction de la valeur envoyée
  digitalWrite (SIGNAL4, !digitalRead(SIGNAL4));  
}

Mais cela n'a rien donné de mieux.

Conditionner le passage d'un prédiviseur à un autre me permet d'avoir le fonctionnement voulu. J'ai testé avec 1000 changements de prédiviseur et je n'ai pas observé d'arrêt de signaux ==> c'est plutot cool !!

Concernant l'arrêt de l'horloge système, je vais tester et voir si cela fonctionne bien.

Merci pour les conseils, ça m'a beaucoup aidé.

Si le fonctionnement est vérifié avec une led, cela ne donnera rien, les 6µs sont invisible pour l'œil.

hello
intrigué, j'ai testé et j'arrive au meme code et résultat que toi, mais sans changer le préscaler
je tape les valeurs au moniteur:
20 me donne 6KHz
5000 me donne 25Hz
65500 me donne 2Hz
à l'oscillo, réponse immédiate

unsigned char prescaler = 0b00001011; //  prescaler = /64
unsigned valeur = 65500;
void setup() {
  Serial.begin(115200);
  DDRB |= 0b00000010;   // D9 passe en sortie
  PORTB |= 0b00000010;  // la led sur D9 est allumée
  SREG &= 0b01111111;   // On désactive les interruptions
  TCCR1A = 0b01000000;  // toggle oca1
  TIMSK1 = 0b00000010;  // valide toggle sur oca1
  OCR1A = valeur;
  TCNT1 = 0;           // Mise du timer1 à zéro
  TCCR1B = prescaler;  // mode ctc et prescaler 1 pas de division
  SREG |= 0b10000000;  // Et, on ré-active les interruptions
}
void loop() {
  if (Serial.available()) {
    valeur = Serial.readStringUntil('\n').toInt();
  }
}

ISR(TIMER1_COMPA_vect) {
  OCR1A = valeur;
}

Non, non, je vous rassure j'ai bien un oscillo sous le coude.

J'ai aussi pensé à ne pas utiliser de prescaler, mais il peut y avoir un problème de résolution pour les fréquences élevées.
Il est vrai aussi que depuis que certains sur cette liste forcent (presque) les demandeurs à utiliser leur code et non pas à corriger le leur, je suis moins enclin à le faire de temps en temps.

Je suppose que c'est le mode CTC 4 qui est utilisé, mais il pose un problème car le "top" est immédiat et si valeur passe de A à B pendant que le compteur est entre les deux, on fait un tour complet.

Si on n'utilise pas le changement de prédiviseur, autant utiliser directement la sortie du timer pour avoir le signal carré.

Dans ton code, @dfgh , tu désactives les interruption pour programmer le timer. C'est ce que je vois la plus part du temps. Mais je me suis toujours posé la question de l'utilité. Il suffit de mettre TIMSK1 à sa bonne valeur en dernier.

Oui effectivement je pense que la résolution ne sera pas optimale. Tout dépend de l'utilité du code. Dans mon cas je dois être capable de générer une fréquence carrée entre 1 et 3400Hz environ, avec un pas de 1Hz. Le code utilisé fonctionne bien mais effectivement nécessite l'utilisation du changement de diviseur, j'aurai pu m'en passer cela aurait été mieux...

ouverture d'une parenthèse:

hello
je suppose que c'est pour moi :thinking: :grinning:
alors oui effectivement, je préfère faire un code et le donner ensuite plutot que de passer des dizaines de posts à expliquer, de ne pas etre compris et de recommencer à expliquer, etc...etc...etc...
je pense que donner un code qui fonctionne est bénéfique pour une personne motivée qui va le désosser et le comprendre. c'est aussi comme ça que l'on peut progresser.
j'arrette là car c'est un sujet qui revient régulièrement...

fermeture de la parenthèse

ou de passer TCCR1B de zéro à 1 (ou autre valeur), en dernier.

Je ne suis pas du tout d'accord avec toi, mais le principal reste d'aider un forumeur d'en le pétrin :+1:

Perdu! Je n'ai pas remarqué d'intervention de ta part non pédagogique.

Aussi, mais cela oblige à 2 écritures du TCCR1B car il est mis à 3 juste avant le setup.

Calcul rapide: cela fait en gros 40 pour 3000Hz et 41 donne 3000*40/41 soit 2926Hz. Résolution de 75Hz seulement pour les "hautes fréquences". Avec le prescaler de 1 cela passe presque.

 


  Une autre idée: avec la routine d'interruption suivante:
ISR(TIMER1_COMPA_vect) {
  if (--decompte == 0)
  {
    digitalWrite (SIGNAL4, !digitalRead(SIGNAL4));  
    decompte=diviseur;
    OCR1A = valeur;     // mise à jour de la valeur OCR1A en fonction de la valeur envoyée
  }
}

TCCR1B étant réglé sur un prescaler de 1.
Pour une valeur donnée:
 - si diviseur = 1 -> on a la même chose qu'avant avec le prescaler de 1
 - si diviseur = 2 -> la fréquence est divisée par 2
 - si diviseur = N -> la fréquence est divisée par N
et donc:
 - on peut donc avoir les même hautes fréquence qu'avant
 - il ne peut plus y avoir de problèmes lors des changements de gamme
 - on peut descendre aussi bas que l'on veut en choisissant bien le type diviseur et decompte correctement (avec des byte, on peut diviser par 255, avec des unsigned long on put diviser la fréquence par 4 milliards.

Ainsi:
 - pour diviseur=1, on sait déjà faire les fréquences de 136Hz à 3250Hz
 - pour diviseur=10, on peut faire les fréquences de 13,6Hz à 325Hz
 - pour diviseur=100, on peut faire les fréquences de 1,36Hz à 32Hz
 - si on utilise des byte, on peut faire les fréquences de 0,5Hz jusqu'à 3250Hz
 - si on utilise des word, on peut faire les fréquences de 0,002Hz jusqu'à 3250Hz
 - si on utilise des unsigned long, on peut faire les fréquences de 0,00000003Hz jusqu'à 3250Hz (la période est de 1 an. Autant dire que c'est l'arrêt)