Jeu des 5 LEDs, version AVR

La couche logicielle Arduino a le mérite de simplifier grandement la programmation sur micro-controleur AVR ; mais le prix de cette simplicité est une grosse perte de performances et le passage sous silence de nombreuses fonctionnalités. Étant curieux, j´ai décidé d´apprendre à programmer un Arduino directement en C, sans les petites roulettes sur les cotés.

Pour me faire la main, j´ai réécrit le jeu des cinq LEDs en C. Sans le buzzer, dans un premier temps.

Toolchain

La programmation en C nécessite quelques outils. Par chance, ils sont tous libres et des gens biens ont déjà préparé tout ce qu´il faut pour nous simplifier la vie.

Les outils utilisés :

  • pour compiler : avr-gcc
  • pour écrire le programme sur l´arduino : avrdude
  • pour ne pas réinventer la roue : avr-libc
  • pour se simplifier la vie : avr-template
  • pour écrire le code : vim, bien sûr !

Avec tout ça, il ne reste plus qu´à écrire le code dans le fichier main.c

Le code

#define F_CPU 16000000

#include <avr/io.h>
#include <util/delay.h>

#define MASKD 0b01111100
#define MASKB 0b00011111


uint8_t toggle_mask(uint8_t pos){
    switch(pos) {
        case 1  : return 0b10011;
        case 2  : return 0b00111;
        case 4  : return 0b01110;
        case 8  : return 0b11100;
        case 16 : return 0b11001;
        default : return 0; // any other case is due to multiple keypress - ignore
    }
}

int main (void)
{
    uint8_t save, read;
    // set ports 2-6 for output
    DDRD |= MASKD;
    // set ports 8-12 for input
    DDRB &= ~MASKB;
    // toggle pull-ups
    PORTB = MASKB;
    // save buttons state
    save = PINB & MASKB;
    // read inputs
    for (;;) {
        read = PINB & MASKB;
        // something has changed
        if (read != save) {
            // toggle the leds corresponding to the bits that have changed from 1 to 0
            PORTD ^= toggle_mask((read ^ save) & ~read) << 2;
            save = read;
            _delay_ms(200);
        }
    }
    return 0;
}

Explications

Les ports et les registres

Sur un microcontroleur, les IO sont regroupées en ports. Pour l´atmega328p qu´on trouve sur un arduino, il y a 3 ports :

  • B (pins 8 à 13)
  • C (pins A0 à A5)
  • D (pins 0 à 7)

Chaque port est accessible via un ensemble de registres accessibles en lecture/écriture. Pour les ports regroupant les IO numériques (ce qui nous intéresse dans le cas présent) :

  • DDRD : pour configurer le mode de chaque pin, INPUT ou OUTPUT
  • PORTD : pour écrire sur les pins (digitalWrite)
  • PIND : pour lire (digitalRead)

Comme chaque registre fait un octet, chaque pin est associé à un bit du registre.

pins      76543210
PORTD   0b01000000

Cette configuration du registre PORTD indique que tous les pins du port D sont à l´état LOW, a l´exception du pin 6. En écrivant une nouvelle valeur dans le registre, on change l´état des 8 pins simultanément.

Dans notre montage, les LEDs sont branchées sur les pin 2 à 6 (port D), et les boutons sur les pins 8 à 12 (port B). On changera donc l´état des 5 LEDs par le biais du registre PORTD, et on lira l´état des 5 boutons par le biais du registre PINB.

Mais dans l´affaire, tous les pins ne nous intéressent pas. Par exemple, comment lire les pins 8 à 12 et ignorer le pin 13 ? Il est temps de sortir les «bitmasks» et les «bitwise operators» …

Les bitmasks et bitwise operators

Nous avons dit que notre montage utilise les pins 2-6 pour contrôler les LEDs. Nous devons donc ignorer les pins 0,1 et 7 du port D. Pour ça, on construit un bitmask :

#define MASKD 0b01111100

Connaissant la correspondance entre l´emplacement des bits et les pins du port D, on marque d´un 1 les bits qui nous intéressent, et d´un 0 les autres. Cette représentation (un bit par pin) ouvre la porte à de nombreuses manipulations.

Les «bitwise operators» permettent de manipuler les données bit-à-bit. Par exemple, l´opérateur «et» :

PORTD           0b10010110
MASKD           0b01111100
PORTD & MASKD   0b00010100

Au passage, ceci correspond à la lecture de l´état des LEDs, en masquant les pins qui ne nous intéressent pas.

En combinant les opérateurs (et, ou, ou exclusif, négation, décalage à gauche/droite), on peut réaliser des opérations complexes très efficacement - le prix a payer étant la simplicité et la lisibilité, bien entendu.

La fonction toggle_mask

Cette fonction est un bon exemple de manipulation de bits.

uint8_t toggle_mask(uint8_t pos){
    switch(pos) {
        case 1  : return 0b10011;
        case 2  : return 0b00111;
        case 4  : return 0b01110;
        case 8  : return 0b11100;
        case 16 : return 0b11001;
        default : return 0; // any other case is due to multiple keypress - ignore
    }
}

Le paramètre est un bitmask indiquant quels boutons sont pressés. A chaque cas pertinent, on associe un bitmask indiquant les LEDs dont l´état doit être modifié. C´est plus clair si on écrit en binaire :

uint8_t toggle_mask(uint8_t pos){
    switch(pos) {
        case 0b00001 : return 0b10011;
        case 0b00010 : return 0b00111;
        case 0b00100 : return 0b01110;
        case 0b01000 : return 0b11100;
        case 0b10000 : return 0b11001;
        default      : return 0; 
    }
}

Lecture des boutons et actualisation des LEDs

Comme à son habitude, la fonction main assemble les morceaux. Comme dans la première version du projet, on tourne en boucle et on bgit si l´état des boutons à l´instant t est différent de l´état en t-1.

La partie la plus intéressante est sans conteste l´expression suivante :

PORTD ^= toggle_mask((read ^ save) & ~read) << 2;

Rappelons que read contient l´état actuel des boutons, tandis que save contient l´état précédent. Prenons un exemple :

save                  0b11110
read                  0b11101

Traduction : l´utilisateur appuie sur le 2ème bouton et relâche le 1er simultanément (hautement iprobable, mais ce n´est pas important). Rappel : sur notre montage, les interrupteurs sont connectés pour exploiter les résistances pull-up. On lit donc 0 quand on appuie sur le bouton.

read ^ save           0b00011

Le ou exclusif nous permet de repérer les bits qui ont changé d´état.

(read ^ save) & ~read 0b00010

Cet ajout nous permet d´ignorer les bits qui sont passés de 0 à 1 - ce qui correspond au relâchement d´un bouton. On a alors un bitmask indiquant les boutons qui ont été pressés. Ce résultat est passé à la fonction toggle_mask, qui nous retourne un bitmask indiquant les LEDs à modifier.

Pour finir, PORTD ^= ... permet de changer l´état des bits désignés par le bitmask retourné par toggle_mask, et << 2 permet de prendre en compte le fait que les LEDs sont connectées à partir du pin 2, et non 0.

Conclusion

Au final, on a exprimé le gros de la logique du programme en une expression, qui vient remplacer une boucle et une dizaine d´appels de fonction.

Bien entendu, le gain en terme de mémoire et de performance n´est pas pertinent pour un projet comme celui-ci - mais quand il s´agira de traiter du son en temps réel (mon objectif à terme), cer gains seront précieux !

Ressources