Timers et ordonnancement

Candençage

Comme nous l'avons vu précédemment, le microcontrôleur est candencé à une certaine fréquence, qui dépend très souvent d'un élément hardware externe, le quartz. Cette fréquence est appelée fréquence d'horloge.

Par exemple, notre ATmega328P est cadencé à 16MHz.


Souvenez vous de la définition -DF_CPU=16000000UL dans le fichier Makefile. C'est cette dernière qui permet au programme C de savoir à quelle fréquence le microcontrôleur est cadencé.

Fonctionnement

Un timer est un module hardware qui permet de compter le nombre de cycles d'horloge. Il est incrémenté à une certaine période \(T\), et réinitialisé lorsqu'il dépasse un certain plafond \(P\) (on parle d'overflow).

La valeur du timer peut être lue et utilisée directement par le code. Par exemple, le registre TCNT0 contient la valeur du timer TIMER0 de l'ATmega328P.

Réglage de la période

La période d'un timer peut se régler à l'aide d'un registre, appelé prescaler. Ce dernier permet de diviser la fréquence d'horloge par un certain nombre \(N\), on peut alors calculer la période \(T\) à l'aide de la formule:

$$T = \frac{N}{f_{CPU}}$$

Où \(f_{CPU}\) est la fréquence d'horloge du microcontrôleur.

En général, le prescaler \(N\) est une valeur que l'on peut choisir parmis une liste de valeurs prédéfinies. Par exemple, la liste des valeurs possibles pour le prescaler du TIMER0 de l'ATmega328P est: 1, 8, 64, 256 ou 1024.

Page 87 de la datasheet

Réglage du plafond

En fonction du mode d'utilisation du timer, le plafond peut être sa valeur maximale (par exemple 0xFF pour un timer 8 bits), ou une valeur arbitraire fixée par un registre.

Page 86 de la datasheet

Par exemple, le TIMER0 de l'ATmega peut être réglé en mode CTC (Clear Timer on Compare Match), où le plafond est fixé par le registre OCR0A.

La fréquence à laquelle le timer overflow (c'est à dire repasse à zéro) est alors:

$$f = \frac{f_{CPU}}{N \times (P + 1)}$$

Où \(f_{CPU}\) est la fréquence d'horloge du microcontrôleur, \(N\) est le prescaler, et \(P\) le plafond.

Exemple

Si un microcontrôleur est candencé à 20Mhz, qu'on définit le prescaler à 256 et le plafond à 124, on obtiendra alors une fréquence de 20Mhz / (256 * (124 + 1)) = 625Hz, soit une période de 1.6ms.

Modulation en longueur d'impulsion (PWM)

Définition

Dans le TD1, nous avons fait briller une LED plus ou moins fort en jouant sur le ratio \(t_1\) entre le temps où la LED est allumée et le temps \(t_0\) où elle est éteinte.

On nomme ce type de signal modulation en longueur d'impulsion (ou PWM pour Pulse Width Modulation).

Le ratio suivant:

$$r = \frac{t_1}{t_0 + t_1}$$

S'appelle le rapport cyclique.

Si la fréquence du signal est suffisament élevée, ce type de signal est équivalent à une tension moyenne proportionnelle au rapport cyclique.

Produire un un signal de ce type est possible à l'aide de délai (en "software"), mais en monopolisant le microcontrôleur. En pratique, il est possible d'implémenter la même chose en utilisant des timers (en "hardware")

Utilisation de timers en mode PWM

En ajoutant un autre registre de comparaison, il est possible de produire ce signal en utilisant un timer en mode PWM:

Ici:

  • Lorsque le timer atteint son plafond, il est réinitialisé à zéro, et une broche correspondante est mise à l'état haut (5V),
  • Lorsque le timer atteint la valeur du registre de comparaison, la broche correspondante est mise à l'état bas (0V).

Il est ainsi possible de régler le rapport cyclique en faisant varier OCR0A de 0 à 255 (si il s'agit d'un timer 8 bits avec le plafond maximum)

Interruptions

Les timers permettent de déclencher des interruptions à chaque overflow (c'est à dire à chaque fois que le timer revient à zéro), ainsi qu'à chaque fois que le timer atteint la valeur du registre de comparaison.

En jouant sur le prescaler et sur le plafond, il est alors possible d'obtenir des interruptions à des fréquences arbitraires et régulières.

Ordonnancement

Introduction

Comme vous l'avez remarqué, nous programmons actuellement sans système d'exploitation.

En particulier, il n'y a pas de concept de tâche (ou thread), et le programme s'exécute de manière séquentielle.

Question: Comment faire plusieurs choses "en même temps" ?

Ordonnancement coopératif

La solution la plus simple est de pratiquer un ordonnancement coopératif. C'est à dire que le programme est découpé en plusieurs tâches, et que chacune de ces tâches est exécutée à tour de rôle.

Par exemple, le code suivant qui fait clignoter une LED:

DDRB |= (1 << PB5);

while (1) {
    PORTB |= (1 << PB5);
    _delay_ms(1000);
    PORTB &= ~(1 << PB5);
    _delay_ms(1000);
}

Est bloquant à cause des appels à _delay_ms.

Supposons que l'on dispose d'une méthode uint32_t system_clock() qui utilise un timer et nous retourne le temps écoulé depuis le démarrage du microcontrôleur en millisecondes.

On pourrait alors ré-écrire le code de la manière suivante:

uint32_t last_change = 0;
uint8_t led_state = 0;

void blink_init() {
    DDRB |= (1 << PB5);
}

void blink_tick() {
    if (system_clock() - last_change >= 1000) {
        last_change = system_clock();
        led_state = !led_state;
        if (led_state) {
            PORTB |= (1 << PB5);
        } else {
            PORTB &= ~(1 << PB5);
        }
    }
}

Ainsi, la fonction main ressemblerait alors à:

int main() {
    blink_init();
    // Initialisation d'autre tâches

    while (1) {
        blink_tick();
        // Exécution d'autre tâches
    }
}

Cette méthode repose sur le fait que les fonctions *_tick ne sont pas bloquantes, elles ne font que vérifier si il est temps de faire quelque chose et rendent la main immédiatement.


L'ordonnancement coopératif est une approche simple et efficace pour réaliser plusieurs tâches en même temps sans système d'exploitation.

Ordonnancement préemptif

Une autre approche consiste à interrompre les tâches afin de donner du temps de calcul à d'autres tâches.

Une manière d'implémenter cela est d'utiliser un timer pour déclencher une interruption à une fréquence donnée, et d'écrire du code (un peu technique) qui permet de changer le contexte de l'exécution (état des registres, de la pile, etc.) lors de l'interruption.

Au retour de l'interruption, une autre tâche peut alors être exécutée.

Ce type de mécanisme est appelé ordonnancement préemptif. Il est plus complexe à mettre en oeuvre, et la meilleure solution est d'utiliser un système d'exploitation, dont une des principale valeur ajoutée est préciement de gérer ce type de mécanisme.