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)
J’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 ».