Pour créer le NoRobo, j’ai dû me pencher sur la notion d’interruptions (Wikipedia). Il s’agit d’un moyen de faire faire plusieurs choses différentes par un même microprocesseur. C’est un concept essentiel dès que l’on veut faire exécuter des activités et les ajuster selon des données entrées par le biais d’une interface utilisateur (la commande bluetooth d’un robot par exemple) ou par un capteur du système.

Les ondes PWM (MLI en français)

Pwm 5stepsJ’ai commencé à m’intéresser aux PWM lorsque j’ai cherché comment contrôler la vitesse d’un moteur à courant continu. Le code dont je m’inspirais utilisait des ondes PWM. J’ai finalement décidé d’utiliser AnalogWrite(x), avec x, de 0 à 255, représentant la vitesse souhaitée.

J’ai découvert à ce moment là que AnalogWrite() envoie aussi des ondes PWM, mais dont la fréquence n’est pas contrôlée. Et par ailleurs, AnalogWrite() n’est pas exécuté si l’arduino fait autre chose. Il est donc fort probable qu’il faille que j’ai recours aux PWM lorsque je vais finaliser le NoRobo (cf la série d’article à ce sujet ici).

Mes sources principales d’information ont été « Secrets of Arduino PWM » et « Changing PWM Frequency on the Arduino« . Ca m’a permis de comprendre les réglages d’horloge que l’on peut faire sur un arduino.

Seules les broches 3,5,6, 9, 10 et 11 de l’arduino uno peuvent être configurées en sorties PWM. Elles sont repérées par un petit symbole ∼.

On notera que la fréquence d’onde PWM à utiliser est fonction des moteurs. Chacun a ses spécificités. On peut seulement dire que la fréquence est comprise entre  1 et 15 kHz.

Les Interruptions (interrupt en anglais)

Elles sont utilisées pour réaliser des activités à une fréquence régulière. Tout le monde connaît delay(), qui permet d’interrompre le sketch arduino pendant un certain temps avant de reprendre. C’est très bien pour des activités simples, mais cette fonction a un énorme problème : elle arrête tout le reste du sketch ! On la réserve donc à des sketch très simples, avec une seule activité.

L’article d’Hobbytronics (en anglais) sur les interruptions Arduino propose un sketch qui permet d’allumer et éteindre une led toutes les secondes, sans utiliser delay(). La led clignote et on peut faire faire ce qu’on veut à l’arduino en même temps. Adafruit y consacre aussi un tutoriel.

Nick Gammon donne des informations sur les interruptions dans les arduino. J’ai également lu attentivement cet article, qui m’a été très utile.

Enfin, la notice du microprocesseur ATMEGA 328 de l’arduino UNO contient plein de détails sur les horloges  :

  • Chapitre 15. sur les « 8-bit Timer/Counter0 with PWM » en page 93;
  • Chapitre 16. sur les « 16-bit Timer/Counter1 with PWM » en page 111;
  • Chapitre 17. sur les « Timer/Counter0 and Timer/Counter1 Prescalers » en page 138;
  • Chapitre 18. sur les « 8-bit Timer/Counter2 with PWM and Asynchronous Operation » en page 141;

Cas pratique : examen d’un morceau de code

Dans les différentes versions de sketch pour le NoRobo (par exemple celle-ci : NoRobo-Joystick-BT-commander-2016-04-12B.ino), il y a plusieurs éléments liés aux horloges. Dans les lignes ci-dessous, je n’ai laissé que les lignes qui me paraissent liées aux horloges :

uint16_t freqCounter = 0;
uint16_t oldfreqCounter = 0;
uint16_t loop_time = 0;         //how fast is the main loop running

int motorG_enable = 11; //pwm
int motorD_enable = 3; //pwm

void setup()
{

  Bl_Setup();
   
}

/****************************************************************************************************
 * LOOP
 ****************************************************************************************************/
 
void loop()
{
  //run main loop every ~4ms
  if ((freqCounter & 0x07f) == 0)
  {
    // record when loop starts
    oldfreqCounter = freqCounter;

    // every ~1s
	if ((freqCounter & 0x7FFF) == 0)
    {
      // Do something
    }

    // every ~0.13s
    if ((freqCounter & 0xFFF) == 0)
    {
      // Do something else
    }

    //calculate loop time
    if (freqCounter > oldfreqCounter)
    {
      if (loop_time < (freqCounter - oldfreqCounter)) loop_time = freqCounter - oldfreqCounter;
    }
  }
}

void Bl_Setup()
{
  cli();//stop interrupts

  //timer setup for 31.250KHZ phase correct PWM
  TCCR0A = 0;
  TCCR0B = 0;
  TCCR0A = _BV(COM0A1) | _BV(COM0B1) | _BV(WGM00);
  TCCR0B = _BV(CS00);
  TCCR1A = 0;
  TCCR1B = 0;
  TCCR1A = _BV(COM1A1) | _BV(COM1B1) | _BV(WGM10);
  TCCR1B = _BV(CS10);
  TCCR2A = 0;
  TCCR2B = 0;
  TCCR2A = _BV(COM2A1) | _BV(COM2B1) | _BV(WGM20);
  TCCR2B = _BV(CS20);

  // enable Timer 1 interrupt
  TIMSK1 = 0;
  TIMSK1 |= _BV(TOIE1);
  // disable arduino standard timer interrupt
  TIMSK0 &= ~_BV(TOIE1);

  sei(); // Start Interrupt

  //turn off all PWM signals
  OCR2A = 0;  //11  APIN
  OCR2B = 0;  //D3
  OCR1A = 0;  //D9  CPIN
  OCR1B = 0;  //D10 BPIN
  OCR0A = 0;  //D6
  OCR0B = 0;  //D5

  // switch off PWM Power
  motorPowerOff();
}

//--------------------------------------------------------------
// code loop timing---------------------------------------------
//--------------------------------------------------------------
// minimize interrupt code length
// is called every 31.875us (510 clock cycles)  ???????
ISR( TIMER1_OVF_vect )
{
  //every 32 count of freqCounter ~1ms
  freqCounter++;

  if ((freqCounter & 0x01) == 0)
  {
    // do another thing
  } 
}

Durant le setup, les horloges sont réglées

cli() arrête les interruptions pour procéder aux réglages.

Les trois horloges sont réglées pour fournir des fréquences de 31.25 KHz « phase correct ».

   //timer setup for 31.250KHZ phase correct PWM
  TCCR0A = 0;
  TCCR0B = 0;
  TCCR0A = _BV(COM0A1) | _BV(COM0B1) | _BV(WGM00);
  TCCR0B = _BV(CS00);
  TCCR1A = 0;
  TCCR1B = 0;
  TCCR1A = _BV(COM1A1) | _BV(COM1B1) | _BV(WGM10);
  TCCR1B = _BV(CS10);
  TCCR2A = 0;
  TCCR2B = 0;
  TCCR2A = _BV(COM2A1) | _BV(COM2B1) | _BV(WGM20);
  TCCR2B = _BV(CS20);

Ici on règle les trois horloges, 0, 1 et 2.

Les horloges 0 et 2 ont des compteurs 8 bits et sont très semblables. L’horloge 0 gère delay() and millis()  et tout réglage modifie ces deux fonctions.

L’horloge 1 dispose d’un compteur 16 bits (valeurs de 0 à 65535). La librairie servo.h s’en sert et il vaut mieux ne pas l’utiliser lorsqu’on a des servomoteurs.

Pour configurer une horloge, on doit modifier les registres de réglage correspondants. Ces registres, au nombre de 2 par horloge s’appellent des « Timer/Counter Control Registers ». On les appelle donc TCCRxA et TCCRxB, où x est le numéro de l’horloge. Chaque registre fait 8 bits et stocke une valeur de configuration.

Pour l’horloge 1, les bits les plus importants sont les trois derniers de TCCR1B (CS12, CS11 et CS10). En les modifiant, on peut changer la vitesse d’horloge.

PAr défaut, lorsque CS10, 11 et 12 sont à 0, l’horloge 1 tourne à 16 MHZ. C’est la même vitesse lorsque seul CS10 est à 1. On fera donc un cycle d’horloge toutes les (1/16*10ˆ6) seconde, soit 6.25*10-8 s. Notre compteur passera donc de 0 à 65535 en (65535 * 6.25*10-8s), soit toutes les 0.0041 seconds.

_BV(bit) est une macro qui convertit un numéro de bit en un octet. En écrivant TCCR1B = _BV(CS10); , on dit de régler CS10 à 1, CS11 et CS12 restant à 0. On a donc un « prescaler » fixé à 1, selon le tableau ci-dessous :

CS12 CS11 CS10 Description
0 0 0 No clock source (Timer/Counter stopped)
0 0 1 clki/o/1 (No prescaling)
0 1 0 clki/o/8 (From Prescaler)
0 1 1 clki/o/64 (From Prescaler)
1 0 0 clki/o/256 (From Prescaler)
1 0 1 clki/o/1024 (From Prescaler)
1 1 0 External clock source on T1 pin. Clock on falling edge
1 1 1 External clock source on T1 pin. Clock on rising edge

Ce prescaler permet de ralentir le compteur. Si le prescaler est à 1, le compteur fera un overflow tous les 0.004 secondes (4 millisecondes) comme vu précédemment.

Si le prescaler est à 256 par exemple (  TCCR1B = _BV(CS12); ), la vitesse d’horloge passe à 1/(16*10⁶/256), ou 0.000016 secondes (62500 Hz). L’horloge aura un overflow toutes les (65535 *0.000016=) 1.04856 secondes.

Si on ecrivait TCCR1B = _BV(CS10) | _BV(CS12), le prescaler serait 1024. On aurait donc une vitesse d’horloge de 1/(16*10⁶ / 1024), soit 0.000064 seconds (15625 Hz). Maintenant on aura un overflow toutes les (65535 * 6.4*10-5s), or 4.194s

 Dans notre cas, on aura un overflow toutes les 4 millisecondes. C’est donc toutes les 4 millisecondes que se déclenchera l’ISR liée au timer 1. 

Pour tout savoir sur les réglages du timer 0, voir l’arduino timer cheat sheet.

Enfin, la ligne suivante règle le timer 1 en mode

TCCR1A = _BV(COM1A1) | _BV(COM1B1) | _BV(WGM10);

TCCR1A est réglé en Phase-correct PWM.

Les deux moteurs sont connectés aux broches du timer 2

Les broches 11 et 3 correspondent au timer 2. Les 9 et 10 au timer 1 et les 5 et 6 au timer 0.

Une interruption est réalisée grâce au timer 1

  // enable Timer 1 interrupt
  TIMSK1 = 0;
  TIMSK1 |= _BV(TOIE1);
  // disable arduino standard timer interrupt
  TIMSK0 &= ~_BV(TOIE1);

  sei(); // Start Interrupt

TIMSK1 |= _BV(TOIE1);  définit que lors de l’overflow, il faut déclencher une interruption. Et comme on a mis à 1 le bit CS10, ce sera le vecteur  (TIMER1_OVF_vect) qui sera déclenché à chaque overflow.

Les horloges fonctionnent en incrémentant un compteur. Il compte de 0 à 255 (si le registre fait 8 bits, 0 à 65535 s’il fait 16 bits). Lorsque le compteur atteint sa valeur maximale, il fait un « overflow » et se remet à 0. Lorsqu’un overflow se produit, on peut déclencher une interruption grâce à une routine de service d’interruption (ISR). c’est ce qui se passe dans l’ISR ci-dessous.

Pour voir la liste des interruptions, voir le chapitre « 12.4 Interrupt Vectors in ATmega328 and ATmega328P », en page 65 de la « datasheet » du ATmega328 (de l’arduino Uno).

Une ISR, Interruption Service Routine

//--------------------------------------------------------------
// code loop timing---------------------------------------------
//--------------------------------------------------------------
// minimize interrupt code length
// is called every 31.875us (510 clock cycles)  ???????
ISR( TIMER1_OVF_vect )
{
  //every 32 count of freqCounter ~1ms
  freqCounter++;

  if ((freqCounter & 0x01) == 0)
  {
    // do another thing
  }
}

ISR( TIMER1_OVF_vect )  est une « interrupt service routine », qui s’éxécute lorsque TIMER1_OVF_vect  se déclenche.

La boucle suit le temps

void loop()
{
  //run main loop every ~4ms
  if ((freqCounter & 0x07f) == 0)
  {
    // record when loop starts
    oldfreqCounter = freqCounter;

    // every ~1s
	if ((freqCounter & 0x7FFF) == 0)
    {
      // Do something
    }

    // every ~0.13s
    if ((freqCounter & 0xFFF) == 0)
    {
      // Do something else
    }

    //calculate loop time
    if (freqCounter > oldfreqCounter)
    {
      if (loop_time < (freqCounter - oldfreqCounter)) loop_time = freqCounter - oldfreqCounter;
    }

  }
}

Les broches 2 et 3 pour des external interrupts…

voir le chapitre 13 « External Interrupts », en page 70 de la « datasheet » du ATmega328 (de l’arduino Uno).

Les broches « INT0 » et « INT1 » (respectivement 2 et 3 sur l’arduino Uno) servent à déclencher des interruptions externes.

Exemple suivant issu de cet article en français de Michael Bouvy.

par exemple sur le pin INT0 (soit D2) nous attachons une interruption, qui appellera la fonction « myInterrupt » lors d’un passage du pin à l’état haut :

attachInterrupt(0, myInterrupt(), RISING);

Bien que le pin Arduino soit « D2 », nous indiquons ici « 0 » qui est le n° de pin d’interruption (0 pour INT0 / D2, 1 pour INT1 / D3).

Ensuite, une fonction exécutera ce qui doit être fait lorsque la broche passe à l’état haut :

void myInterrupt() {
  // do something ...
}

Noter que dans le programme du balancing robot, les moteurs utilisent les broches 3,5,6,9,10 et 11, donc toutes les broches connectées aux horloges de l’arduino.  INT du gyroscope ne peut donc pas être relié à ces horloges…

Communication I2C

Dans la documentation du ATmega328 (de l’arduino Uno), le chapitre 22, en page 206 est intitulé « 2-wire Serial Interface » (TWI). C’est la partie qui correspond à I2C.

C’est la librairie wire.h qui gère le protocole I2C sur l’arduino.

http://www.robot-electronics.co.uk/i2c-tutorial

La ligne writeTo(MPU6050, PWR_MGMT_1, 0);  de la fonction angle_setup() dit au gyroscope de se « réveiller » et d’utiliser l’horloge interne à 8MHz. D’après ce que je comprends, c’est l’horloge interne du gyroscope, pas une des horloges de l’arduino.

Mais si INT du gyroscope est connecté à une broche de l’arduino, que se passe-t-il ?

Et maintenant ?

Je suis loin d’avoir tout compris, mais le brouillard s’éclaircit. Je retourne donc à mon NoRobo, avec un article dans la série « un robot arduino ».

Print Friendly, PDF & Email
5 1 vote
Évaluation de l'article
0
Nous aimerions avoir votre avis, veuillez laisser un commentaire.x