AVR – Chapitre 9 – Créer son périphérique USB

L’usine à gaz USB

L’Universal Serial Bus est une norme de transfert de données en série (c’est à dire que les données sont transmises les unes à la suite des autres ; à opposer au mode parallèle où les données sont transmises via plusieurs canaux). Créé en 1994 par un consortium de constructeurs, l’USB est maintenant incontournable depuis le début des années 2000. Tout fonctionne maintenant par USB : claviers, souris, stockages de données, caméras, imprimantes, protocoles sans fil,.. L’idée de pouvoir rejoindre cette communauté doit vous sembler excitante !

Il est important de saisir certains concepts avant de se jeter à l’eau. Par ailleurs, je ne traiterai ici que des périphériques de version USB 1.1. Cette limitation est due aux caractéristiques techniques des micro-contrôleurs.

Les caractéristiques du bus

L’USB 1 et 2, se déploit sur quatre fils :

  • L’alimentation +5V VCC en rouge (parfois orange)
  • Le câble « données » D- en blanc (parfois jaune)
  • Le câble « données » D+ en vert
  • et la masse GND en noir (parfois bleu)

Communément, un périphérique USB 1.x peut être alimenté par un courant de 150mA (500mA pour l’USB 2.0). Si vous avez besoin de plus de puissance, il sera nécessaire de prévoir votre propre source de courant (batteries, alimentation, …).

Les connecteurs à chaque bout peuvent avoir différentes formes, les plus courants étant les suivantes :

Broches Type A Type B Mini-A Mini-B Micro-A Micro-B
VCC 1 1 1 1 1 1
D- 2 2 2 2 2 2
D+ 3 3 3 3 3 3
GND 4 4 5 5 5 5

La topologie

Le bus se dessine suivant une topologie en arbre, l’hôte étant la racine. Il est possible de connecter jusqu’à 127 périphériques, sur 5 niveaux (derrière des hubs).

L’USB peut fonctionner jusqu’à 3 mètres et même au-delà en utilisant des répétiteurs alimentés (ceci est néanmoins déconseillé).

Les identifiants

Chaque périphérique s’identifie grâce à deux codes :

  • le code fournisseur (vendor ID) sur 2 octets (soit 65536 possibilités). Ce code est distribué par l’USB Implementers Forum. Il est nécessaire de payer entre 2500 et 5000 US$ pour disposer du sien de façon officielle. Heureusement, il existe des codes pour les « bricoleurs » comme nous.
  • le code produit (product ID) sur 2 octets  (soit 65536 possibilités). Ce code est défini par le fournisseur à sa guise. Généralement, tous les produits d’une même génération partagent le même code produit.

On les représente communément sous forme hexadécimale, sur 4 chiffres, en les séparant par un deux-points. Voici quelques exemples :

  • Appareil photo Sony NEX-5R : 054c:066e
  • Téléphone Nexus 4 : 18d1:4ee1

Ce couple ne permettent pas d’identifier de manière unique un individu, seulement sa nature. Pour l’identifier précisément, il sera nécessaire de le prévoir de manière logicielle, ou en utilisant son chemin de connexion dans le réseau (par exemple, hub 2 > hub 2.1> hub 2.1.4 > port 2.1.4.5).

Par ailleurs, il existe une identification textuelle représentée par une chaîne de caractères.

Les classes USB

Vue la myriade de possibilités qu’offre la norme USB, il est nécessaire de regrouper les périphériques par classes, afin de factoriser l’effort à fournir pour le support de nouveaux matériels.

Ainsi, les souris, claviers, joysticks, etc. sont syndiqués sous la classe « Human interface device » (HID): 0x03. Les périphériques de stockage sont sous la classe « Mass storage » : 0x08. Cette page (en anglais) résume les classes officiellement déclarées.

Suivant la classe, les systèmes d’exploitation ont bâti des pilotes (drivers) afin de rendre plus ou moins transparente l’intégration des périphériques USB. Ainsi, la plupart des périphériques de classe HID sont supportés sans la nécessité de fournir des pilotes. C’est cette classe qui va nous intéresser, car il s’agit de la plus simple à mettre en œuvre.

Le « Report Descriptor »

Dans le cas de la classe HID, il est nécessaire de fournir un « report descriptor ». Il s’agit d’une chaîne d’entiers qui décrit le format de données que le périphérique va retourner à l’hôte.

Prenons l’exemple d’une souris ; celle-ci doit retourner les informations suivantes :

  • l’état de ses boutons (gauche, droite, milieu, molette vers le haut et molette vers le bas)
  • un mouvement sur l’axe X
  • un mouvement sur l’axe Y

La classe HID « mouse »

Voici une façon de représenter ces données, sur 3 octets :

octet bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0
Boutons -     -     -     molette bas molette haut clic droit clic milieu clic gauche
Axe X entier relatif entre -128 et 127
Axe Y entier relatif entre -128 et 127

Voici à quoi correspond le rapport :

 

Pour aider à la rédaction de ce rapport, il est possible d’utiliser HID Descriptor Tool disponible ici. Voici à quoi cela ressemble :

HIDtoolJe ne vais pas  vous faire un cours sur le contenu détaillé de ces rapports. Il suffit simplement de savoir qu’ils existent et à quoi ils servent. Pour plus de détails, voici une bonne introduction (en anglais).

La classe HID « Custom »

Dans mon tutoriel, je vais utiliser une version allégée qui n’utilise pas de donnée formatée :

L’utilisation d’un tel rapport permet de s’affranchir des limitations du format HID, tout en étant facile à « installer » sur l’hôte. Cependant, il devient nécessaire d’utiliser un logiciel sur l’hôte pour se servir du périphérique.

La bibliothèque V-USB

AVR-GCC ne fournit pas de méthode pour utiliser l’USB, et ce n’est pas son but.

Il est donc nécessaire de charger une bibliothèque (library) tierce. V-USB est une bibliothèque qui propose une implémentation logicielle de la norme USB 1.1 pour les micro-contrôleurs AVR.

Celle-ci est proposée sous double licence : GPL version 2 ou commerciale. Cela implique que vos projets qui incluent cette bibliothèque doivent être publiés en accord avec la GPL (et donc avec distribution des sources), ou nécessite l’achat d’une licence commerciale (9.90€ pour une licence « Hobbyiste », 500€ pour une licence professionnelle).

Il existe des alternatives, mais la plupart nécessitent des micro-contrôleurs « puissants » ou des contrôleurs USB matériel. V-USB est utilisable sur des micro-contrôleurs modestes, dont l’ATTiny85.

Que contient-elle ?

La version actuelle (20121206) contient le répertoire usbdrv, dans lequel vous trouverez (entre autres) :

usbdrv.h les définitions nécessaires
usbdrv.c le cœur du pilote
usbdrvasm.S code assembleur utilisé par le pilote
usbdrvasm*.inc fichier appelé depuis usbdrvasm.S
asmcommon.inc fichier appelé depuis usbdrvasm.S
oddebug.c fournit plusieurs fonction de débogage
oddebug.h Les définitions requises par oddebug.c
usbportability.h Des définitions utilisés par le compilateur
usbconfig-prototype.h un exemple de fichier de configuration

Pour le bon fonctionnement du projet, il est important de copier l’intégralité du répertoire dans vos sources.

Comment fonctionne la bibliothèque ?

La configuration

Toute la configuration se trouve dans le fichier usbconfig.h, que vous devez ajouter à vos sources. Pour cela, vous pouvez copier le fichier usbconfig-prototype.h. Voici les points importants à adapter à votre projet :

Macro Description Exemple
USB_CFG_IOPORTNAME Port du microcontrôleur utilisé pour la communication en USB B
USB_CFG_DMINUS_BIT Numéro de broche utilisé par D- 0
USB_CFG_DPLUS_BIT Numéro de broche utilisé par D+ 2
USB_CFG_IS_SELF_POWERED Indique si votre périphérique dispose de sa propre source (0 : Non/ 1 : Oui) 0
USB_CFG_MAX_BUS_POWER Courant maximum que consomme votre périphérique (en mA) 50
USB_CFG_IMPLEMENT_FN_WRITE Indique si vous souhaitez utiliser la fonction usbFunctionWrite() ou non 0
USB_CFG_IMPLEMENT_FN_READ Indique si vous souhaitez utiliser la fonction usbFunctionRead() ou non 0
USB_CFG_LONG_TRANSFERS Indique si vous souhaitez faire des transferts de plus de 254 octets 0
USB_CFG_VENDOR_ID Octet faible et octet fort qui définissent l’identifiant fournisseur 0xc0, 0x16
USB_CFG_PRODUCT_ID Octet faible et octet fort qui définissent l’identifiant produit 0xdc, 0x05
USB_CFG_VENDOR_NAME Nom du fournisseur (il est demandé d’y mettre un nom de domaine) ‘c’,’i’,’c’,’a’,’t’,’r’,’i’,’c’,’e’,’.’,’e’,’u’
USB_CFG_VENDOR_NAME_LEN Taille de la macro fournisseur 12
USB_CFG_DEVICE_NAME Nom du produit ’I’,’R’,’-‘,’L’,’e’,’d’
USB_CFG_DEVICE_NAME_LEN Taille de la macro produit 6

Bien entendu, il reste beaucoup de macros, je vous laisse découvrir leur utilité dans les commentaires du code.

Les callbacks

V-USB implémente plusieurs « callbacks » (fonctions de retour), qui sont appelées à la suite d’événements précis. Cela ressemble à des interruptions, sauf que celles-ci sont purement logicielles :

  • usbFunctionSetup  : appelée à l’arrivée d’une requête ; c’est ici que vous définirez ce qu’il doit se passer au niveau du micro-contrôleur.
  • usbFunctionWrite  (nécessite USB_CFG_IMPLEMENT_FN_WRITE à 1) Cette fonction est appelée quand la fonction usbFunctionSetup retourne USB_NO_MSG et si la requête est de type USBRQ_HID_SET_REPORT (0x09).
  • usbFunctionRead  (nécessite USB_CFG_IMPLEMENT_FN_READ à 1) Cette fonction est appelée quand la fonction usbFunctionSetup retourne USB_NO_MSG et si la requête est de type USBRQ_HID_GET_REPORT (0x01).

Il y en a d’autres mais ce sont les principales. Ce diagramme résume l’utilisation que vous pouvez faire de ces méthodes.

USBlibs

La bibliothèque LibUSB

Comme je l’ai indiqué lors du chapitre sur les « report descriptors », nous allons avoir besoin d’un programme sur la machine hôte qui se chargera de dialoguer avec le micro-contrôleur branché en USB. Je vais utiliser ici libusb.

Ne voulant pas m’embêter avec la signature des pilotes sous Windows (obligatoire depuis la version 8) j’ai décidé de tout faire sous Linux (j’utilise une Fedora 20).

LibUSB est une bibliothèque dont le but est de fournir des méthodes de gestion et d’accès aux périphériques branchés en USB.

Le programme se découpe en deux parties :

  • une partie d’initialisation, où l’on cherche le périphérique USB qui nous intéresse
  • une partie d’exécution, où l’on effectue les actions nécessaires, notamment grâce à la méthode  libusb_control_transfer()  que vous avez pu apercevoir dans mon diagramme plus haut.

La structure du projet

Pour mon exemple, je vais utiliser un petit projet qui permet à mon PC de servir de télécommande pour une bande de LED RGB.

Aperçu

Voici comment doit se présenter votre projet :

  \dirtree{% .1 projet. .2 commands. .3 send.c. .3 Makefile. .2 usbdrv. .3 .... .2 Makefile. .2 remote.c. .2 requests.h. .2 usbconfig.h. }

Vous verrez dans beaucoup d’exemples que les fichiers remote.c , requests.h ,  usbconfig.h  et Makefile (celui du micro-contrôleur) se trouvent dans un répertoire firmware. Vous pouvez ranger ces fichiers comme bon vous semble, l’important est que cette structure soit claire pour vous.

Le schéma

Avant de parler du code, voici un exemple de branchements :ir_rgb_schémaIl n’y a pas grand chose à dire sur ce schéma. Les deux diodes Zener doivent limiter la tension à 3.3V.

 

Les définitions

usbconfig.h

Comme décrit plus tôt, il s’agit de la configuration pilote V-USB. Celui est chargé automatiquement, à partir du moment où votre projet inclus le fichier usbdrv.h . Il sera également inclus lors de la compilation du programme hôte.

 

requests.h

Ce fichier contient les requêtes USB personnalisées. Comme pour le fichier précédent, il sera inclus lors de la compilation.

 

Le programme du micro-contrôleur

remote.c

Il s’agit du code principal, qui sera envoyé sur le micro-contrôleur.

 

Makefile

Ici il faut faire quelques ajouts, pour prendre en compte la bibliothèque. J’ai mis en surbrillance les lignes qui diffèrent par rapport à d’habitude.

 

Le programme hôte

Ici, tout se passe sur une machine sous Fedora 20.

commande-hote.c

Comme vous pouvez le voir, ce code à besoin d’inclure les fichiers  ../requests.h  et ../usbconfig.h .

Makefile

Je ne sais pas ce qu’il en ait quant à l’outil pkg-config en dehors de Fedora, mais celui est plutôt pratique puisqu’il génère les options tout seul.

Utiliser un ATTiny85

Le problème que pose l’utilisation d’un ATTiny, est qu’il faut utiliser une horloge cadencée au minimum à 12MHz. Or l’ATTiny ne fonctionne correctement à 8MHz maximum. On va utiliser une astuce pour le passer à 16.5MHz sans cristal externe : la PLL (Phase-Locked Loop : boucle à verrouillage de phase).

Sans entrer dans les détails, il s’agit d’un dispositif qui permet d’augmenter la fréquence d’un micro-contrôleur (basée sur un cristal), au prix de sa stabilité. Il est ainsi possible de faire fonctionner un ATTiny au delà de 80MHz ! Mais dans notre cas, 16.5MHz suffiront.

Pour cela il est nécessaire d’harmoniser la fréquence au début de l’exécution. Ceci se fait en définissant un « hook » dans le ficher usbconfig.h , en y ajoutant la fonction hadUsbReset() , qui s’occupera d’appeler une autre fonction ( calibrateOscillator() ), qui calibrera l’oscillateur PLL.

L’exemple donné dans cet article prend déjà en compte ces modifications.

Dans le cas d’un micro-contrôleur capable d’utiliser une horloge à 16.5MHz, il n’y a pas besoin de ce dispositif, et donc des fonctions hadUsbReset() ,  calibrateOscillator()  ainsi que des lignes suivantes dans usbconfig.h  :

 

 Et voilà !

Voici les sources du projet : RGB remote (fichier 7z).

Lorsque vous branchez votre périphérique, les messages suivants doivent apparaitre dans dmesg.

Une fois le programme hôte compilé, vous pouvez envoyer des commandes au micro-contrôleur avec les commandes suivantes :

 

AVR – Chapitre 6 – Lire des valeurs analogiques (ADC)

Analogique vs. numérique

Les valeurs analogiques sont à opposer aux valeurs discrètes et logiques. Voici quelques comparaisons entre les deux mondes, avec des exemples pratiques :

Vision analogique Vision logique (numérique)
la porte est à peine ouverte (0.01) la porte est ouverte (1)
le réservoir est presque vide (0.1) le réservoir contient de l’essence (1)
le premier a parcouru 99% du chemin (0.99) le premier n’est pas encore arrivé (0)
il reste 17% de batterie (0.17) il reste de la batterie (1)

Oui, il est frustrant de ne pas atteindre le niveau de détails qui nous semble trivial au quotidien, au sein d’un programme purement logique.

L’ADC

Pour palier à ce manque, il a été ajouté aux microcontrôleurs un ou plusieurs canaux convertisseurs analogique vers numérique. Ceux-ci sont représentés par les broches ADC d’un microcontrôleur, pour « Analog to Digital Converter ».

L’ATTiny85 peut supporter jusqu’à 4 canaux ADC, chacun ayant une résolution de 10 bits, soit 1024 valeurs. Comme pour les PWM (Modulation de signal pulsé), leur utilisation dépend d’une horloge et d’un diviseur.

La théorie

Du point de vue de l’exécution, il s’agit d’approximer une tension inconnue (Vin) contenue entre 0 et une tension de référence (Vref).

Ainsi, le résultat est retourné sous la forme d’un entier entre 0 et 1023 :

ADC = [\frac{V_{IN} \times 1024}{V_{REF}}]

La configuration

Les registres de configuration de l’ADC sont ADMUX, ADCSRA et ADCSRB. Je vais faire l’impasse sur le registre ADCSRB, il permet des options avancées non essentielles pour le moment. Bien entendu, vous retrouverez tous ces registres dans la documentation (À partir de la page 134).

Le registre ADMUX

Bit 7 6 5 4 3 2 1 0
ADMUX REFS1 REFS0 ADLAR REFS2 MUX3 MUX2 MUX1 MUX0

Les bits REFS 2 à 0 déterminent la tension de référence Vref utilisée pour la comparaison.

REFS2 REFS1 REFS0 Vref
0 0 0 Vcc (i.e. la tension d’alimentation fournie au microcontrôleur)
0 0 1 Aref (i.e. tension externe fournie sur la broche Aref/PB0 du microcontrôleur)
0 1 0 1.1V
0 1 1 réservé (ne pas utiliser)
1 1 0 2.56V sans condensateur de découplage sur Aref/PB0
1 1 1 2.56V avec condensateur de découplage sur Aref/PB0

Les bits MUX 3 à 0 indiquent quel canal sélectionné (je ne parlerai ici que des canaux simples).

MUX3 MUX2 MUX1 MUX0 Canal
0 0 0 0 ADC0/PB5
0 0 0 1 ADC1/PB2
0 0 1 0 ADC2/PB4
0 0 1 1 ADC3/PB3
1 1 1 1 ADC4 il s’agit d’un capteur de température interne. Surprise !

Enfin le bit ADLAR indique l’alignement du résultat (1 pour gauche, 0 pour droite).

Le registre ADCSRA

Il s’agit du registre de contrôle, qui va permettre de démarrer la conversion et de savoir quand celle-ci est terminée.

Bit 7 6 5 4 3 2 1 0
ADCSRA ADEN ADSC ADATE ADIF ADIE ADPS2 ADPS1 ADPS0

Les bits ADPS 2 à 0 configure le diviseur (page 136 de la documentation). Le bit ADEN doit être à 1 pour que la conversion puisse fonctionner, mais ceci ne la démarre pas.

Passer le bit ADSC à 1 démarre la conversion. Il repasse à zéro dès que celle-ci est finie.

Le bit ADIE permet de déclencher une interruption lors de la fin d’une conversion, et le bit ADIF témoigne de la fin de la conversion. Dans le cas d’une utilisation sans interruption il est important de le positionner à 1 à la fin de la boucle, sinon la lecture restera bloquée sur la même valeur. Lors de l’utilisation de l’interruption, le chargement de sa procédure réinitialisera automatiquement ADIF, il n’y a donc plus besoin d’y toucher.

Le résultat

Puisqu’il s’agit d’un convertisseur 10 bits, il n’est pas possible de rendre le résultat sur un seul registre de 8 bits. Il y a donc deux façons d’organiser le résultat :

  • aligné à gauche (ADLAR = 1) : les 8 bits les plus forts sont dans le registre haut, et les 2 derniers bits dans le registre bas
    Bit 7 6 5 4 3 2 1 0
    ADCH ADC9 ADC8 ADC7 ADC6 ADC5 ADC4 ADC3 ADC2
    ADCL ADC1 ADC0 -   -     -    -     -      -
  • aligné à droite (ADLAR = 0) : les 2 bits les plus forts sont dans le registre haut, et les 8 derniers bits dans le registre bas
    Bit 7 6 5 4 3 2 1 0
    ADCH -   -     -    -     -      -    ADC9 ADC8
    ADCL ADC7 ADC6 ADC5 ADC4 ADC3 ADC2 ADC1 ADC0

Pour choisir quelle format vous sied le mieux, l’alignement à gauche permet de comparer rapidement le résultat sur 8 bit avec une précision moyenne, alors que l’alignement à droite offre une meilleure précision mais contraint à l’utilisation d’un entier codé sur 16 bits (cf. exemple plus loin).

 Un exemple simple : la photorésistance

Comme vous le savez surement, la photorésistance est un composant électronique à deux pôles, dont la résistance dépend de la luminosité ambiante. Voici un schéma pour la mettre en œuvre :

adc_schem

La résistance R3 est à prévoir en fonction de la photorésistance R1. Celles-ci étant montées en diviseur de tension, on mesurera donc une tension comprise entre :

\frac{R_3}{R_{1}_{MAX} + R_{3}} \times V_{REF} et \frac{R_3}{R_{1}_{MIN} + R_{3}} \times V_{REF}

Pour ma part, j’ai utilisé une résistance R3 de 4,7kΩ et une résistance R1 de 20kΩ, sur un circuit de 5V soit :

\frac{4.7k}{20k + 4.7k} \times 5 \simeq 1V et \frac{4.7k}{0 + 4.7k} \times 5 = 5V

Ainsi, la valeur retournée par l’ADC devrait être comprise entre 205 et 1023.

Le but ici est d’activer la diode dès que la photorésistance R1 atteint un certain seuil (i.e. dès qu’il fait trop sombre), et de l’éteindre dès qu’on y voit suffisamment clair, à la manière de l’éclairage public qui s’allume à la tombée de la nuit, et s’éteint quand le soleil brille. On laissera une marge entre les deux pour éviter un va-et-vient lors de la pénombre. Il faudra veiller également à ce que la diode ne soit pas orienté vers la résistance pour ne pas fausser sa mesure.

 

Pour aller plus loin

Je n’ai présenté ici l’utilisation que pour un seul canal simple, mais il est assez facile d’en utiliser plusieurs à la suite ; le choix du canal se faisant avec le registre ADMUX, il est facile de reconfigurer les bits 3 à 0 en fonction du canal désiré, à la fin de l’interruption (juste avant la ligne ADCSRA |= (1 << ADSC); ).

Il est également possible de ne pas utiliser les interruptions. Dans ce cas, il est nécessaire d’ajouter la ligne suivante une fois la valeur lue, au risque de voir la conversion retourner toujours le même résultat :

 

AVR – Chapitre 7 – Passer à l’ATMega

L’ATTiny85 est une plateforme parfaite pour bien débuter en programmation AVR, mais ses fonctionnalités et ses broches sont en nombre limité. Je vous propose maintenant de passer à une série de microcontrôleur AVR plus performante : l’ATMega.

Présentation de l’ATMega

Il s’agit toujours d’un microcontrôleur fabriqué par Atmel. Il y a beaucoup de variantes d’ATMega, la version que je vais présenter ici est l’ATMega168. Si vous disposez d’un ATMega328 (comme les cartes Arduino), la configuration sera quasi identique.

L’ATMega168 dispose d’une horloge fonctionnant jusqu’à 20Mhz, de 16ko de mémoire Flash, 512octets d’EEPROM, 1ko de RAM, et 28 broches.

Coté fonctionnalités, l’ATMega168 fournis deux compteurs 8bits, un compteur 16bits, et surtout une liaison USART (Universal Synchronous & Asynchronous Receiver & Transmitter : Récepteur/Émetteur Universel, Synchrone & Asynchrone), nous verrons cela plus tard.

Il fonctionne avec une tension comprise entre 1,8 et 5,5V.

Comment programmer un ATMega168 ?

Les branchements

Comme pour l’ATTiny85, il est facile de créer une carte de programmation pour l’ATMega168, à partir d’un Arduino. Je vous conseille d’utiliser un support ZIF ; ce sera plus facile à manipuler qu’un simple support pour circuit intégré.

arduino_as_ISP_atmega_bb

Notez que le quartz et les deux condensateurs en bas à droite du schéma ne sont utiles que si l’on souhaite utiliser une horloge externe.

Les ajustements

Rien de bien compliqué ici, il suffit simplement d’indiquer au compilateur et au logiciel de programmation la nouvelle cible. Voici les modifications que j’ai apportées à mon Makefile :

 

 

Faire chanter un ATtiny85

Si vous avez suivi mes tutoriels sur la programmation de microcontrôleur AVR, vous n’aurez pas de difficulté à saisir le fonctionnement de cet exemple.

Du fichier MIDI à l’EEPROM

Le MIDI est un format standard binaire (à opposer au fichier texte) utilisé dans industrie musicale, qui contient une ou plusieurs pistes, où sont décrites les notes et leur durée. Ces fichiers peuvent être générés depuis certains équipements (clavier,…) ou en utilisant des logiciels dédiés. Le son produit par les instruments n’est pas directement enregistrés ; seuls sont enregistrés des événements, et des informations sur l’instrument.

Lors de la lecture, les sons sont générés en fonction de ces informations. Ainsi le résultat peut être légèrement différent selon l’équipement de lecture.

Ici, nous allons nous intéresser à une seule piste, et nous allons nous assurer qu’elle est monophonique ; puisqu’il sera difficile de lire plusieurs notes en même temps.

La sélection de la piste, et l’édition

Pour cela, j’ai utilisé le logiciel Aria Maestosa, un logiciel libre qui permet d’importer des fichiers MIDI et de les modifier.

Voici le fichier MIDI pour Greensleeves avant édition :

Piste MIDI avant édition

Et voici le même fichier après avoir supprimé les notes basses, pour garder seulement la mélodie :

Piste MIDI après édition

Nous avons maintenant un fichier MIDI avec une seule piste, mais celui-ci reste encore trop gros pour tenir dans les 512 octets disponibles dans l’EEPROM.

Un petit script pour nettoyer tout ça

Je souhaite donner comme format pour chaque note, trois valeurs :

  • l’index de la note (cf. plus bas)
  • le délai de silence avant la note
  • la durée de la note

J’ai donc décider d’écrire un petit script en Ruby pour obtenir une série de nombres, qui tiennent chacun sur 8bit. Après avoir installé l’interpréteur à partir de cet installeur, j’ai ajouté la bibliothèque midilib disponible en gem :

[cce_dos]gem install midilib[/cce_dos]

Puis j’ai écrit ce script :

[cce_ruby]

require ‘midilib/sequence’
require ‘midilib/event’

diviseur = ARGV[1].to_i unless ARGV[1].nil?
diviseur ||= 1
notes = []
current_note=[]

sequence = MIDI::Sequence.new()

File.open(ARGV[0], ‘rb’) do |file|
sequence.read(file)
end

sequence.each do |track|
track.each do |e|
current_note << e.note.to_i if e.is_a? MIDI::NoteOn
current_note << (e.delta_time/diviseur).to_i unless e.note_to_s.empty? if e.is_a? MIDI::NoteEvent
notes << current_note if e.is_a? MIDI::NoteOff
current_note = [] if e.is_a? MIDI::NoteOff
end
end

notes.flatten!
$stderr.puts « \n notes > 255\n » unless notes.select{|n| n>255}.empty?
puts notes.to_s.gsub(‘[‘,'{‘).gsub(‘]’,’}’)

[/cce_ruby]

Voici comment l’utiliser ; si j’ajoute seulement le nom du fichier en paramètre, je n’utilise pas de diviseur :

[cce_dos]

D:\projects\attiny\music>ruby mid2data.rb d:\Downloads\greensleeves-mono.mid

notes > 255
{69, 0, 256, 72, 0, 512, 74, 0, 256, 76, 0, 384, 77, 0, 128, 76, 0, 256, 74, 0, 512, 71, 0, 256, 67, 0, 384, 69, 0, 128, 71, 0, 256, 72, 0, 512, 69, 0, 256, 69, 0, 384, 68, 0, 128, 69, 0, 256, 71, 0, 512, 68, 0, 256, 64, 0, 470, 69, 42, 256, 72, 0, 512, 74, 0, 256, 76, 0, 384, 77, 0, 128, 76, 0, 256, 74, 0, 512, 71, 0, 256, 67, 0, 384, 69, 0, 128, 71, 0, 256, 72, 0, 384, 71, 0, 128, 69, 0, 256, 68, 0, 384, 66, 0, 128, 68, 0, 256, 69, 0, 768, 69, 0, 726, 79, 42, 768, 79, 0, 384, 77, 0, 128, 76, 0, 256, 74, 0, 512, 71, 0, 256, 67, 0, 384, 69, 0, 128, 71, 0, 256, 72, 0, 512, 69, 0, 256, 69, 0, 384, 68, 0, 128, 69, 0, 256, 71, 0, 512, 68, 0, 256, 64, 0, 726, 79, 42, 768, 79, 0, 384, 77, 0, 128, 76, 0, 256, 74, 0, 512, 71, 0, 256, 67, 0, 384, 69, 0, 128, 71, 0, 256, 72, 0, 384, 71, 0, 128, 69, 0, 256, 68, 0, 384, 66, 0, 128, 68, 0, 256, 69, 0, 768, 69, 0, 726}

[/cce_dos]

Certaines notes utilisent une durée supérieure à 255, je vais donc ajouter un paramètre pour diviser cette durée ; elle sera restituée à l’exécution par le microcontrôleur :

[cce_dos]

D:\projects\attiny\music>ruby mid2data.rb d:\Downloads\greensleeves-mono.mid 4
{69, 0, 64, 72, 0, 128, 74, 0, 64, 76, 0, 96, 77, 0, 32, 76, 0, 64, 74, 0, 128, 71, 0, 64, 67, 0, 96, 69, 0, 32, 71, 0,64, 72, 0, 128, 69, 0, 64, 69, 0, 96, 68, 0, 32, 69, 0, 64, 71, 0, 128, 68, 0, 64, 64, 0, 117, 69, 10, 64, 72, 0, 128, 74, 0, 64, 76, 0, 96, 77, 0, 32, 76, 0, 64, 74, 0, 128, 71, 0, 64, 67, 0, 96, 69, 0, 32, 71, 0, 64, 72, 0, 96, 71, 0, 32, 69, 0, 64, 68, 0, 96, 66, 0, 32, 68, 0, 64, 69, 0, 192, 69, 0, 181, 79, 10, 192, 79, 0, 96, 77, 0, 32, 76, 0, 64, 74, 0, 128, 71, 0, 64, 67, 0, 96, 69, 0, 32, 71, 0, 64, 72, 0, 128, 69, 0, 64, 69, 0, 96, 68, 0, 32, 69, 0, 64, 71, 0, 128, 68, 0, 64, 64, 0, 181, 79, 10, 192, 79, 0, 96, 77, 0, 32, 76, 0, 64, 74, 0, 128, 71, 0, 64, 67, 0, 96, 69, 0, 32, 71, 0,64, 72, 0, 96, 71, 0, 32, 69, 0, 64, 68, 0, 96, 66, 0, 32, 68, 0, 64, 69, 0, 192, 69, 0, 181}

[/cce_dos]

Parfait ! On a maintenant un tableau formaté pour le C, prêt à être envoyé sur un microcontrôleur ; voyons maintenant comment faire.

Préparer le code pour l’EEPROM

Pour enregistrer ce tableau au sein du microcontrôleur, il faut déclarer une variable au tout début du script, et lui ajouté la directive EEMEM. Je créé donc un fichier « greensleeves.h » que j’appellerai au début de mon code, celui-ci contenant ces lignes :

[cce_c]

//pour le tempo, je multiplie le diviseur par 1000. L’augmenter ralentira le tempo, le baisser l’accélérera.
#define TEMPO 4000L

//j’ajoute également une constante contenant la taille de la piste, ce sera plus commode pour parcourir la mémoire.
#define SONG_LENGTH 216

uint8_t EEMEM song[SONG_LENGTH] = {69, 0, 64, 72, 0, 128, 74, 0, 64, 76, 0, 96, 77, 0, 32, 76, 0, 64, 74, 0, 128, 71, 0, 64, 67, 0, 96, 69, 0, 32, 71, 0,64, 72, 0, 128, 69, 0, 64, 69, 0, 96, 68, 0, 32, 69, 0, 64, 71, 0, 128, 68, 0, 64, 64, 0, 117, 69, 10, 64, 72, 0, 128, 74, 0, 64, 76, 0, 96, 77, 0, 32, 76, 0, 64, 74, 0, 128, 71, 0, 64, 67, 0, 96, 69, 0, 32, 71, 0, 64, 72, 0, 96, 71, 0, 32, 69, 0, 64, 68, 0, 96, 66, 0, 32, 68, 0, 64, 69, 0, 192, 69, 0, 181, 79, 10, 192, 79, 0, 96, 77, 0, 32, 76, 0, 64, 74, 0, 128, 71, 0, 64, 67, 0, 96, 69, 0, 32, 71, 0, 64, 72, 0, 128, 69, 0, 64, 69, 0, 96, 68, 0, 32, 69, 0, 64, 71, 0, 128, 68, 0, 64, 64, 0, 181, 79, 10, 192, 79, 0, 96, 77, 0, 32, 76, 0, 64, 74, 0, 128, 71, 0, 64, 67, 0, 96, 69, 0, 32, 71, 0,64, 72, 0, 96, 71, 0, 32, 69, 0, 64, 68, 0, 96, 66, 0, 32, 68, 0, 64, 69, 0, 192, 69, 0, 181};
[/cce_c]

Voilà ! Nous avons exporter un fichier MIDI en tableau utilisable en C. Nous verrons un tout petit peu plus tard comment enregistrer ce tableau dans l’EEPROM.

De la note aux registres

Une note de musique n’est qu’une vibration à une fréquence précise. Par exemple, un LA est une vibration à 440Hz.

Les notes et leur fréquence

Wikipédia nous donne une liste de notes et de leur fréquence :

Fréquences des hauteurs (en hertz) dans la gamme tempérée
Note\octave 0 1 2 3 4 5 6 7
Do 32,70 65,41 130,81 261,63 523,25 1046,50 2093,00 4186,01
Do♯ ou Ré♭ 34,65 69,30 138,59 277,18 554,37 1108,73 2217,46 4434,92
36,71 73,42 146,83 293,66 587,33 1174,66 2349,32 4698,64
Ré♯ ou Mi♭ 38,89 77,78 155,56 311,13 622,25 1244,51 2489,02 4978,03
Mi 41,20 82,41 164,81 329,63 659,26 1318,51 2637,02 5274,04
Fa 43,65 87,31 174,61 349,23 698,46 1396,91 2793,83 5587,65
Fa♯ ou Sol♭ 46,25 92,50 185,00 369,99 739,99 1479,98 2959,96 5919,91
Sol 49,00 98,00 196,00 392,00 783,99 1567,98 3135,96 6271,93
Sol♯ ou La♭ 51,91 103,83 207,65 415,30 830,61 1661,22 3322,44 6644,88
La 55,00 110,00 220,00 440,00 880,00 1760,00 3520,00 7040,00
La♯ ou Si♭ 58,27 116,54 233,08 466,16 932,33 1864,66 3729,31 7458,62
Si 61,74 123,47 246,94 493,88 987,77 1975,53 3951,07 7902,13

On peut donc créer un fichier contenant pour chaque note et chaque octave, la valeur de la fréquence.

Coder les notes sur 8bit

Le petit souci que vous remarquerez rapidement, est qu’il n’est pas possible d’aller au delà de 255 avec 8 bit. Et il n’est pas question de supprimer les notes supérieures à 255Hz. Nous allons donc associer un index à chaque fréquence.

Pour cela, nous allons utiliser l’index utilisé par le format MIDI ; celui-ci obéit à une formule simple : Index = Octave_{note} \times 12 + Index_{note dans l'octave}.

Nous allons utiliser deux fichiers pour mettre en place cette association. Tout d’abord, un fichier « notes.h » qui va contenir la liste des notes, ainsi que leur fréquence et leur index :

[cce_c]
long get_note_frequency(int note_index);

/*
*
* Frequencies of notes (in Hz) from C0 to B7
*
*/
#define F_C0 33
#define F_CS0 35
#define F_D0 37
#define F_DS0 39
#define F_E0 41
#define F_F0 44
#define F_FS0 46
#define F_G0 49
#define F_GS0 52
#define F_A0 55
#define F_AS0 58
#define F_B0 62
#define F_C1 65
#define F_CS1 69
#define F_D1 73
#define F_DS1 78
#define F_E1 82
#define F_F1 87
#define F_FS1 93
#define F_G1 98
#define F_GS1 104
#define F_A1 110
#define F_AS1 117
#define F_B1 123
#define F_C2 131
#define F_CS2 139
#define F_D2 147
#define F_DS2 156
#define F_E2 165
#define F_F2 175
#define F_FS2 185
#define F_G2 196
#define F_GS2 208
#define F_A2 220
#define F_AS2 233
#define F_B2 247
#define F_C3 262
#define F_CS3 277
#define F_D3 294
#define F_DS3 311
#define F_E3 330
#define F_F3 349
#define F_FS3 370
#define F_G3 392
#define F_GS3 415
#define F_A3 440
#define F_AS3 466
#define F_B3 494
#define F_C4 523
#define F_CS4 554
#define F_D4 587
#define F_DS4 622
#define F_E4 659
#define F_F4 698
#define F_FS4 740
#define F_G4 784
#define F_GS4 831
#define F_A4 880
#define F_AS4 932
#define F_B4 988
#define F_C5 1047
#define F_CS5 1109
#define F_D5 1175
#define F_DS5 1245
#define F_E5 1319
#define F_F5 1397
#define F_FS5 1480
#define F_G5 1568
#define F_GS5 1661
#define F_A5 1760
#define F_AS5 1865
#define F_B5 1976
#define F_C6 2093
#define F_CS6 2217
#define F_D6 2349
#define F_DS6 2489
#define F_E6 2637
#define F_F6 2794
#define F_FS6 2960
#define F_G6 3136
#define F_GS6 3322
#define F_A6 3520
#define F_AS6 3729
#define F_B6 3951
#define F_C7 4186
#define F_CS7 4435
#define F_D7 4699
#define F_DS7 4978
#define F_E7 5274
#define F_F7 5588
#define F_FS7 5920
#define F_G7 6272
#define F_GS7 6645
#define F_A7 7040
#define F_AS7 7459
#define F_B7 7902

/*
*
* Frequencies of notes (in Hz) from Do0 to Si7 (Roman style)
*
*/
#define F_DO0 F_C0
#define F_REM0 F_CS0
#define F_RE0 F_D0
#define F_MIM0 F_DS0
#define F_MI0 F_E0
#define F_FA0 F_F0
#define F_SOLM0 F_FS0
#define F_SOL0 F_G0
#define F_LAM0 F_GS0
#define F_LA0 F_A0
#define F_SIM0 F_AS0
#define F_SI0 F_B0
#define F_DO1 F_C1
#define F_REM1 F_CS1
#define F_RE1 F_D1
#define F_MIM1 F_DS1
#define F_MI1 F_E1
#define F_FA1 F_F1
#define F_SOLM1 F_FS1
#define F_SOL1 F_G1
#define F_LAM1 F_GS1
#define F_LA1 F_A1
#define F_SIM1 F_AS1
#define F_SI1 F_B1
#define F_DO2 F_C2
#define F_REM2 F_CS2
#define F_RE2 F_D2
#define F_MIM2 F_DS2
#define F_MI2 F_E2
#define F_FA2 F_F2
#define F_SOLM2 F_FS2
#define F_SOL2 F_G2
#define F_LAM2 F_GS2
#define F_LA2 F_A2
#define F_SIM2 F_AS2
#define F_SI2 F_B2
#define F_DO3 F_C3
#define F_REM3 F_CS3
#define F_RE3 F_D3
#define F_MIM3 F_DS3
#define F_MI3 F_E3
#define F_FA3 F_F3
#define F_SOLM3 F_FS3
#define F_SOL3 F_G3
#define F_LAM3 F_GS3
#define F_LA3 F_A3
#define F_SIM3 F_AS3
#define F_SI3 F_B3
#define F_DO4 F_C4
#define F_REM4 F_CS4
#define F_RE4 F_D4
#define F_MIM4 F_DS4
#define F_MI4 F_E4
#define F_FA4 F_F4
#define F_SOLM4 F_FS4
#define F_SOL4 F_G4
#define F_LAM4 F_GS4
#define F_LA4 F_A4
#define F_SIM4 F_AS4
#define F_SI4 F_B4
#define F_DO5 F_C5
#define F_REM5 F_CS5
#define F_RE5 F_D5
#define F_MIM5 F_DS5
#define F_MI5 F_E5
#define F_FA5 F_F5
#define F_SOLM5 F_FS5
#define F_SOL5 F_G5
#define F_LAM5 F_GS5
#define F_LA5 F_A5
#define F_SIM5 F_AS5
#define F_SI5 F_B5
#define F_DO6 F_C6
#define F_REM6 F_CS6
#define F_RE6 F_D6
#define F_MIM6 F_DS6
#define F_MI6 F_E6
#define F_FA6 F_F6
#define F_SOLM6 F_FS6
#define F_SOL6 F_G6
#define F_LAM6 F_GS6
#define F_LA6 F_A6
#define F_SIM6 F_AS6
#define F_SI6 F_B6
#define F_DO7 F_C7
#define F_REM7 F_CS7
#define F_RE7 F_D7
#define F_MIM7 F_DS7
#define F_MI7 F_E7
#define F_FA7 F_F7
#define F_SOLM7 F_FS7
#define F_SOL7 F_G7
#define F_LAM7 F_GS7
#define F_LA7 F_A7
#define F_SIM7 F_AS7
#define F_SI7 F_B7

/*
*
* Notes list, to be stored in EEPROM. It will save a lot of space.
*
*/

#define N_C0 0
#define N_CS0 1
#define N_D0 2
#define N_DS0 3
#define N_E0 4
#define N_F0 5
#define N_FS0 6
#define N_G0 7
#define N_GS0 8
#define N_A0 9
#define N_AS0 10
#define N_B0 11
#define N_C1 12
#define N_CS1 13
#define N_D1 14
#define N_DS1 15
#define N_E1 16
#define N_F1 17
#define N_FS1 18
#define N_G1 19
#define N_GS1 20
#define N_A1 21
#define N_AS1 22
#define N_B1 23
#define N_C2 24
#define N_CS2 25
#define N_D2 26
#define N_DS2 27
#define N_E2 28
#define N_F2 29
#define N_FS2 30
#define N_G2 31
#define N_GS2 32
#define N_A2 33
#define N_AS2 34
#define N_B2 35
#define N_C3 36
#define N_CS3 37
#define N_D3 38
#define N_DS3 39
#define N_E3 40
#define N_F3 41
#define N_FS3 42
#define N_G3 43
#define N_GS3 44
#define N_A3 45
#define N_AS3 46
#define N_B3 47
#define N_C4 48
#define N_CS4 49
#define N_D4 50
#define N_DS4 51
#define N_E4 52
#define N_F4 53
#define N_FS4 54
#define N_G4 55
#define N_GS4 56
#define N_A4 57
#define N_AS4 58
#define N_B4 59
#define N_C5 60
#define N_CS5 61
#define N_D5 62
#define N_DS5 63
#define N_E5 64
#define N_F5 65
#define N_FS5 66
#define N_G5 67
#define N_GS5 68
#define N_A5 69
#define N_AS5 70
#define N_B5 71
#define N_C6 72
#define N_CS6 73
#define N_D6 74
#define N_DS6 75
#define N_E6 76
#define N_F6 77
#define N_FS6 78
#define N_G6 79
#define N_GS6 80
#define N_A6 81
#define N_AS6 82
#define N_B6 83
#define N_C7 84
#define N_CS7 85
#define N_D7 86
#define N_DS7 87
#define N_E7 88
#define N_F7 89
#define N_FS7 90
#define N_G7 91
#define N_GS7 92
#define N_A7 93
#define N_AS7 94
#define N_B7 95

#define N_DO0 N_C0
#define N_REM0 N_CS0
#define N_RE0 N_D0
#define N_MIM0 N_DS0
#define N_MI0 N_E0
#define N_FA0 N_F0
#define N_SOLM0 N_FS0
#define N_SOL0 N_G0
#define N_LAM0 N_GS0
#define N_LA0 N_A0
#define N_SIM0 N_AS0
#define N_SI0 N_B0
#define N_DO1 N_C1
#define N_REM1 N_CS1
#define N_RE1 N_D1
#define N_MIM1 N_DS1
#define N_MI1 N_E1
#define N_FA1 N_F1
#define N_SOLM1 N_FS1
#define N_SOL1 N_G1
#define N_LAM1 N_GS1
#define N_LA1 N_A1
#define N_SIM1 N_AS1
#define N_SI1 N_B1
#define N_DO2 N_C2
#define N_REM2 N_CS2
#define N_RE2 N_D2
#define N_MIM2 N_DS2
#define N_MI2 N_E2
#define N_FA2 N_F2
#define N_SOLM2 N_FS2
#define N_SOL2 N_G2
#define N_LAM2 N_GS2
#define N_LA2 N_A2
#define N_SIM2 N_AS2
#define N_SI2 N_B2
#define N_DO3 N_C3
#define N_REM3 N_CS3
#define N_RE3 N_D3
#define N_MIM3 N_DS3
#define N_MI3 N_E3
#define N_FA3 N_F3
#define N_SOLM3 N_FS3
#define N_SOL3 N_G3
#define N_LAM3 N_GS3
#define N_LA3 N_A3
#define N_SIM3 N_AS3
#define N_SI3 N_B3
#define N_DO4 N_C4
#define N_REM4 N_CS4
#define N_RE4 N_D4
#define N_MIM4 N_DS4
#define N_MI4 N_E4
#define N_FA4 N_F4
#define N_SOLM4 N_FS4
#define N_SOL4 N_G4
#define N_LAM4 N_GS4
#define N_LA4 N_A4
#define N_SIM4 N_AS4
#define N_SI4 N_B4
#define N_DO5 N_C5
#define N_REM5 N_CS5
#define N_RE5 N_D5
#define N_MIM5 N_DS5
#define N_MI5 N_E5
#define N_FA5 N_F5
#define N_SOLM5 N_FS5
#define N_SOL5 N_G5
#define N_LAM5 N_GS5
#define N_LA5 N_A5
#define N_SIM5 N_AS5
#define N_SI5 N_B5
#define N_DO6 N_C6
#define N_REM6 N_CS6
#define N_RE6 N_D6
#define N_MIM6 N_DS6
#define N_MI6 N_E6
#define N_FA6 N_F6
#define N_SOLM6 N_FS6
#define N_SOL6 N_G6
#define N_LAM6 N_GS6
#define N_LA6 N_A6
#define N_SIM6 N_AS6
#define N_SI6 N_B6
#define N_DO7 N_C7
#define N_REM7 N_CS7
#define N_RE7 N_D7
#define N_MIM7 N_DS7
#define N_MI7 N_E7
#define N_FA7 N_F7
#define N_SOLM7 N_FS7
#define N_SOL7 N_G7
#define N_LAM7 N_GS7
#define N_LA7 N_A7
#define N_SIM7 N_AS7
#define N_SI7 N_B7

[/cce_c]

Puis un second fichier « notes.c », contenant une fonction associant chaque index à chaque fréquence :

[cce_c]

long get_note_frequency(int note_index){
switch(note_index){
case N_C0: return F_C0;
case N_CS0: return F_CS0;
case N_D0: return F_D0;
case N_DS0: return F_DS0;
case N_E0: return F_E0;
case N_F0: return F_F0;
case N_FS0: return F_FS0;
case N_G0: return F_G0;
case N_GS0: return F_GS0;
case N_A0: return F_A0;
case N_AS0: return F_AS0;
case N_B0: return F_B0;
case N_C1: return F_C1;
case N_CS1: return F_CS1;
case N_D1: return F_D1;
case N_DS1: return F_DS1;
case N_E1: return F_E1;
case N_F1: return F_F1;
case N_FS1: return F_FS1;
case N_G1: return F_G1;
case N_GS1: return F_GS1;
case N_A1: return F_A1;
case N_AS1: return F_AS1;
case N_B1: return F_B1;
case N_C2: return F_C2;
case N_CS2: return F_CS2;
case N_D2: return F_D2;
case N_DS2: return F_DS2;
case N_E2: return F_E2;
case N_F2: return F_F2;
case N_FS2: return F_FS2;
case N_G2: return F_G2;
case N_GS2: return F_GS2;
case N_A2: return F_A2;
case N_AS2: return F_AS2;
case N_B2: return F_B2;
case N_C3: return F_C3;
case N_CS3: return F_CS3;
case N_D3: return F_D3;
case N_DS3: return F_DS3;
case N_E3: return F_E3;
case N_F3: return F_F3;
case N_FS3: return F_FS3;
case N_G3: return F_G3;
case N_GS3: return F_GS3;
case N_A3: return F_A3;
case N_AS3: return F_AS3;
case N_B3: return F_B3;
case N_C4: return F_C4;
case N_CS4: return F_CS4;
case N_D4: return F_D4;
case N_DS4: return F_DS4;
case N_E4: return F_E4;
case N_F4: return F_F4;
case N_FS4: return F_FS4;
case N_G4: return F_G4;
case N_GS4: return F_GS4;
case N_A4: return F_A4;
case N_AS4: return F_AS4;
case N_B4: return F_B4;
case N_C5: return F_C5;
case N_CS5: return F_CS5;
case N_D5: return F_D5;
case N_DS5: return F_DS5;
case N_E5: return F_E5;
case N_F5: return F_F5;
case N_FS5: return F_FS5;
case N_G5: return F_G5;
case N_GS5: return F_GS5;
case N_A5: return F_A5;
case N_AS5: return F_AS5;
case N_B5: return F_B5;
case N_C6: return F_C6;
case N_CS6: return F_CS6;
case N_D6: return F_D6;
case N_DS6: return F_DS6;
case N_E6: return F_E6;
case N_F6: return F_F6;
case N_FS6: return F_FS6;
case N_G6: return F_G6;
case N_GS6: return F_GS6;
case N_A6: return F_A6;
case N_AS6: return F_AS6;
case N_B6: return F_B6;
case N_C7: return F_C7;
case N_CS7: return F_CS7;
case N_D7: return F_D7;
case N_DS7: return F_DS7;
case N_E7: return F_E7;
case N_F7: return F_F7;
case N_FS7: return F_FS7;
case N_G7: return F_G7;
case N_GS7: return F_GS7;
case N_A7: return F_A7;
case N_AS7: return F_AS7;
case N_B7: return F_B7;
default: return 0;
}
}

[/cce_c]

Maintenant, comment restituer ces fréquences depuis un microcontrôleur ? Grâce aux compteurs, comme nous avons pu le voir.

Déterminer le meilleur diviseur, et le meilleur registre TOP

Nous avons un microcontrôleur dont l’horloge oscille à 1Mhz, et nous souhaitons obtenir une fréquence entre 30 et 8000Hz. Pour cela nous avons deux paramètres :

  • le diviseur, qui peut aller de 1 à 16384 (par puissance de deux)
  • le registre TOP, qui doit être compris entre 2 (il faut au moins 2 pas, sinon il est impossible de faire vibrer le haut-parleur) et 255.

La formule est donc la suivante :

Registre_{TOP} = \frac{fr\'{e}quence_{Horloge}}{2 \times fr\'{e}quence_{son} \times diviseur}

L’idée est de garder un diviseur le plus bas possible, pour gagner en résolution, tant que le registre ne dépasse pas 255.

Pour voir ce que ça donne,j’ai compilé les résultats dans un fichier Excel.

On peut faire le choix de coder chaque pair diviseur / registre TOP, pour chaque fréquence de note. Mais dans ce cas, on se prive de toutes les autres fréquences, alors j’ai décidé d’écrire un morceau de code qui s’occupe de cela. Voilà l’idée :

  1. On regarde si la fréquence à écrire est strictement positive ; dans le cas contraire, on désactive le compteur, ce qui annulera le son
  2. On définit le diviseur à 1, et on applique la formule vue plus haut
  3. Si le diviseur est plus grand que 16384, on abandonne, la fréquence est trop basse pour être lue correctement
  4. Si le résultat est supérieur à 255, on multiplie le diviseur par deux, et on recommence.
  5. Dès qu’on a un résultat inférieur à 255, on l’utilise comme registre TOP
  6. on divise ce registre par deux, pour définir le registre de comparaison (comme pour le PWM)
  7. enfin, on configure le registre du diviseur avec celui trouvé lors de l’exécution de l’algorithme.

Maintenant passons à la pratique !

Le code, la compilation et la programmation

Codez…

[cce_c]

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

#include « notes.h »
#include « notes.c »
#include « songs/greensleeves.h »

void set_tone(long frequency){
int prescaler = 1 ;
long tone_ocr;
if (frequency <= 0){
TCCR1 = 0;
return;
}
while( (tone_ocr = F_CPU / ( frequency * (1<<prescaler)) ) > 0xFF){
prescaler++;
if(prescaler>0x0F) break;
}
if( prescaler > 0x0F) return; //prescaler too high
OCR1C = 0xFF & tone_ocr;
OCR1A = 0xFF & tone_ocr / 2;
TCCR1 = (0x0F & prescaler) | (1<<PWM1A) | (1<<COM1A1) | (1<<CTC1);
}

void tone(long frequency, long duration){
set_tone(frequency);
_delay_us(duration);
}

int main(void)
{
DDRB= (1<<DDB1);

while(1){
uint8_t i=0;
uint8_t note_to_play;
uint8_t silence_before;
uint8_t note_duration;
while(i<SONG_LENGTH){
note_to_play=eeprom_read_byte((uint8_t*) i);

i++;
silence_before=eeprom_read_byte((uint8_t*) i);
i++;
note_duration=eeprom_read_byte((uint8_t*) i);
i++;
set_tone(0);
_delay_us(TEMPO*silence_before);
tone(get_note_frequency(note_to_play),TEMPO*note_duration);
}
i = 0;

}
}

[/cce_c]

…compilez…

J’utilise toujours un Makefile, c’est le plus pratique :

[cce_dos]

make

[/cce_dos]

… et chargez tout ça !

Après la compilation, il est possible de générer le fichier mémoire tel qu’il sera interprété par le microcontrôleur :

[cce_dos]

make eeprom

[/cce_dos]

Cela va créer un fichier « note_eeprom.hex ». Maintenant, on peut exécuter la commande suivante pour programmer la mémoire EEPROM :

[cce_dos]

avrdude -p t85 -c avrisp -P COM4 -b 19200 -v  -U eeprom:w:note_eeprom.hex

[/cce_dos]

On charge également le code principal :

[cce_dos]

make install

[/cce_dos]

C’est terminé ! Branchez maintenant la broche 6 de votre microcontrôleur à un petit haut parleur piézoélectrique, relier l’autre partie du haut-parleur à la broche VCC. Puis alimentez votre microcontrôleur, et voilà !

AVR – Chapitre 4 – Fabriquer des valeurs analogiques (PWM)

D’un point de vue numérique, l’analogique n’existe pas. Le courant est là, ou n’est pas là. La diode est allumée, ou éteinte. Il n’y a pas d’état intermédiaire. Si cela vous frustre, pas de panique ; il est toujours possible de simuler des valeurs analogiques, à condition d’avoir un bon compteur, avec une haute fréquence.

L’illusion

L’idée est plutôt simple : prenons le cas d’une diode que l’on veut allumer progressivement de 0 à 100%. Pour l’allumer à 25%, nous allons l’allumer sur 25% des cycles du microcontrôleur.
Soit un tic d’horloge allumée, et 3 tics d’horloge éteinte.

Rendered by QuickLaTeX.com

À première vue, on peut s’inquiéter d’un effet de scintillement, mais pas de souci à se faire : quand l’horloge tourne à une fréquence de l’ordre du méga-hertz, l’œil humain, lui ne perçoit ce phénomène qu’en deçà de 30 Hz. Nous avons donc une marge plus que confortable pour ces jeux d’éclairage.

Par contre, il n’est pas question de monopoliser un microcontrôleur de cette façon ; il a donc été créé un mécanisme : le PWM (Pulse Width Modulation), soit la modulation de largeur d’impulsion.

La modulation de largeur d’impulsion : PWM

Il s’agit d’un compteur, couplé à des comparateurs. L’ATtiny85 peut fonctionner suivant deux modes :

  • Fast PWM : modulation rapide
  • Phase Correct PWM : modulation réelle

 

Mode de modulation rapide

Un peu d’explications…

Il s’agit de définir un registre de référence, et d’incrémenter un compteur de 0 à 255 (dans le cas d’un compteur 8bit). On réalise à chaque pas une comparaison entre la référence et le compteur.

Tant que le compteur est inférieur à la référence, la sortie est à 1, puis une fois qu’on a dépassé le registre, la sortie passe à 0.

Une fois arrivé au maximum (TOP) on redescend directement au niveau bas (BOTTOM) et on recommence.

Ce diagramme résume la situation, pour un registre de référence qui correspond à 25% de la taille du compteur, soit {25\% \times {2}^{8}} = 64

Rendered by QuickLaTeX.com

Plus intéressant maintenant, il est possible de configurer le compteur pour se réinitialiser non plus après {2}^{8}-1 = 255 mais à une valeur arbitraire entre 0 et 255. Puisque l’on est habitué au décimal, pourquoi ne pas choisir 99 ? Dans ce cas, si l’on souhaite obtenir une valeur de 42%, elle sera plus facile à définir qu’en utilisant un maximum de 255 : 42\% \times \frac{100}{256} = 107.52.

Rendered by QuickLaTeX.com

La pratique

Dans ce microcontrôleur, il y a plusieurs registres à prendre en compte pour utiliser la modulation rapide ; il nous faut :

  • le registre de configuration du compteur 1 :  TCCR1
  • le registre de référence du comparateur du compteur 1 : OCR1A
  • le registre TOP du comparateur du compteur 1 : OCR1C
  • le registre de configuration du port B : DDRB

Reprenons le dernier exemple ; on cherche à allumer une diode, connectée à la broche 1 du port B (broche 6), à 42%.

 

Voilà ! Votre diode « clignote » maintenant à 80kHz, restant allumée 42 fois sur 100.

Mode de modulation réelle

Ce mode de fonctionnement fonctionne presque de la même façon que le précédent, à ceci près qu’une fois arrivé au TOP, on redescend vers BOTTOM, est on applique le même procédé de comparaison. Ceci à pour avantage d’augmenter la résolution, au détriment de la fréquence maximale, qui est divisée par deux.

Rendered by QuickLaTeX.com

Reprenons maintenant le premier exemple ; le code change peu, mais on utilisera ici le compteur 0 :

 

On sait maintenant utiliser les compteurs avec une application interessante : générer des valeurs analogiques. Une fois configurée, l’utilisation dans votre code est trivial ; il suffit d’affecter la valeur souhaitée au registre de référence (OCR0A ou OCR1A). Il est également possible de définir un second registre de référence sur chaque compteur (OCR0B et OCR1B), chacun utilisant son horloge respective (compteur 0 ou 1).

Et il est également possible d’utiliser un diviseur (prescaler) afin de réduire nettement la fréquence. Ceci peut vous permettre de créer des signaux oscillants au niveau du kilo-hertz, soit la possibilité de créer des sons !

AVR – Annexe – Tutoriel sur la récuperation d’un ATTiny bloqué

Comment bloqué son microcontrôleur ?

Si vous êtes aussi pressé que moi de tester un montage, il a pu vous arriver de faire des boulettes. Il y a les classiques : la surtension, le court-circuit, le claquage de diode…

Mais il peut vous arriver également de vous tromper lors des réglages des fusibles de votre microcontrôleur.Pour ma part, j’ai mal configuré l’horloge de l’ATTiny…

Et cela se solde en général par une indisponibilité pour toute essai de programmation de sa mémoire. Pire, vous n’aurez même plus accès aux-dits fusibles. Dur !

Au secours !

Après avoir bloqué (on dit aussi brické) quatre ATTiny, je décide d’arrêter le massacre et de me renseigner.

Je suis tombé sur énormément de tutoriels qui proposaient d’utiliser l’environnement de développement officiel fourni par Atmel (AVR Studio). Mais celui-ci requiert du matériel officiel, et non un programmateur bricolé avec un Arduino. L’AVR ISP MKII est un boitier de programmation, qui coûte une quarantaine d’euros, et qui n’est pas aussi convivial à utilisé qu’un Arduino, du point de vue montage. Il y a aussi les copies chinoises sur eBay, mais qui restent tout de même assez chères (25€ environ).

En cherchant un peu plus loin, j’ai trouvé une méthode dite « High Voltage ». L’idée d’injecter une haute dose de courant dans mon circuit ne me plaisait pas, même s’il s’agissait d’un courant de 12V, ce qui finalement n’est pas bien haut.

Mais puisque je ne trouvais rien de plus convaincant sur le sujet, j’ai tenté l’expérience.

Allons-y !

Matériel et logiciels

Il vous faudra donc :

  • un PC
  • un Arduino et son câble USB
  • une plaque de prototypage
  • 6 résistances de 1kΩ
  • une pile A23 (trouvable en grande surface) et son support (à bricoler avec du papier alu et un support LR3 ou LR6)
  • un transistor NPN 2N3904 ou BC547B (d’autres références feront surement l’affaire)
  • quelques câbles
  • et un ATTiny souffrant (ça marche aussi avec un microcontrôleur sain, mais c’est moins utile)

Coté logiciel, il vous faudra :

  • l’environnement de développement Arduino, ici
  • ce sketch pour Arduino

Montage

Si vous optez pour une petite plaque de prototypage, les composants seront un peu serrés, mais ça passe :arduino_attiny_fuse_reset_bb

Faites quand même attention avec le 12V, il serait dommage de griller votre Arduino en plus de votre ATTiny !

Commencez les incantations !

Maintenant que votre montage est prêt, vous pouvez brancher votre Arduino, et envoyer le sketch fourni plus haut vers celui-ci.

Une fois qu’il tourne, connectez le terminal série, en prenant soin de régler la vitesse sur 19200 bauds. Tapez n’importe quel caractère (ASCII de préférence) et envoyez-le.

Si tout s’est bien passé, vous devriez avoir une réponse dans la foulée, précisant les nouveaux fusibles configurés pour votre microcontrôleur.

Bravo, il est reparé !

Comment trouver les bons fusibles ?

Maintenant que vous avez retrouvé votre microcontrôleur, il peut être bien de savoir comment changer ses fusibles. Le plus simple est d’utiliser [cci]avrdude[/cci], au travers du Makefile dont je parle ici. Et pour savoir comment choisir les valeurs pour ces fusibles, utilisez un calculateur comme celui-ci.

Le calculateur vous donnera des détails sur chaque bit des fusibles, et se chargera même de créer les arguments à transmettre à la commande [cci]avrdude[/cci].

 

 

AVR – Chapitre 5 – Lire et écrire les mémoires

Un microcontrôleur AVR contient trois mémoires :

  • la mémoire vive statique, aussi appelée SRAM, est un espace occupé par les programmes en cours d’exécution. Elle est généralement petite (512 octets pour l’ATtiny85), et volatile ; la mise hors-tension du microcontrôleur vide son contenu.
  • la mémoire programmable EEPROM, parfois appelée « mémoire morte » car son contenu survit à l’exécution du programme. Elle est utilisée pour stocker des données, même après une mise hors-tension du microcontrôleur. Sa taille est de 512 octets pour l’ATtiny85.
  • la mémoire programmable Flash, qui contient le code compilé directement en instructions AVR. Il peut contenir jusqu’à 8192 octets.

La mémoire vive

Il n’y a rien de plus simple pour écrire et lire la mémoire vive, puisque c’est exactement ici que ce déroule l’exécution du programme. Déclarer une variable et y accéder suffisent à réaliser cette opération.

Cependant, sa taille étant réduite, nous allons voir comment palier ce défaut en utilisant la mémoire morte.

La mémoire programmable EEPROM

Cette mémoire est intéressante pour sa persistance lorsque le microcontrôleur est mis hors tension. Elle peut supporter 100 000 cycles d’écriture, soit une durée de vie minimum d’un an si on y écrit toutes les cinq minutes. Bien entendu ceci est un minimum théorique et il est fort possible que le microcontrôleur résiste bien plus longtemps.

Comment y écrire ?

Afin d’utiliser la mémoire EEPROM, il faut inclure la bibliothèque correspondante :

Nous pouvons maintenant déclarer une variable directement dans l’EEPROM :

Et il est possible d’écrire en brut dans la mémoire morte :

 

Et comment lire mes variables ?

L’accès à cette variable se fait de manière naturelle :

Il est également possible de lire directement le secteur qui nous intéresse de cette façon :

Pour un exemple pratique, je vous invite à lire cet article, sur la création d’un lecteur de musique minimaliste.

La mémoire programmable Flash

Celle-ci est beaucoup plus large que la mémoire vive, et son but est de contenir le programme à exécuter.

Cette mémoire a cependant un cycle de vie limité (environ 10 000 écritures) et il est déconseillé de l’utiliser sauf dans des cas extrêmes, comme le stockage de larges variables (chaine de caractères, tableau lourd, …).

Cet article (dans la documentation des bibliothèques AVR pour GCC) décrit en détail comment et pourquoi utiliser la mémoire Flash. Je ne rentrerai pas dans les détails ici, je vais simplement vous montrer comment écrire de lourdes variables dans la mémoire Flash :

 

AVR – Chapitre 3 – Utiliser les compteurs

Un  compteur (timer en anglais), en électronique, est une fonction qui permet de générer un tic (tick) à une fréquence définie. C’est fonction est cruciale pour beaucoup d’applications comme la gestion du temps, la communication, mais aussi pour générer des valeurs analogiques (dans le prochain chapitre).

En théorie…

un peu de maths

Pour l’ATtiny, le concept est simple : un compteur est réglé pour incrémenter un registre de 8 bits à chaque tic en fonction :

  • de l’horloge du processeur
  • d’un diviseur

Ce registre, une fois arrivé au maximum (255 pour 8 bits) va déclencher une interruption de dépassement de compteur au prochain incrément.

Ainsi, pour une fréquence d’horloge de 8 MHz, avec un diviseur configuré à 256, on peut s’attendre à une interruption de dépassement toutes les 8,192 ms. \displaystyle t = \frac{{2}^{taille\ du\ registre\ du\ compteur} \times diviseur}{frequence} = \frac{{2}^{8}\times 256}{8\times {10}^{6}} = 8,192\ ms.

8 millisecondes c’est court !

La documentation de l’ATtiny85 nous donne les diviseurs disponibles (page 80) et la configuration du registre TCCR0B en conséquence (les bits CS00, CS01 et CS02). On peut donc utiliser un de ces diviseurs :

  • 1 (pas de diviseur)
  • 8
  • 64
  • 256
  • 1024

Il est également possible d’utiliser une horloge externe, mais celle disponible ici suffira.

Dans notre cas il s’agit de compteur 8 bits, et la manière la plus élégante d’arriver à compter des secondes est de compter un certain nombre d’interruptions, et de n’agir qu’à une seule de ces interruptions. Dans le cas des compteurs 16 bits, il est possible d’utiliser une autre méthode que je ne décrirai pas pour le moment.

En laissant passer 122 interruptions pour dépassement, on peut approximer la seconde :
\displaystyle 122 \times 8,192 = 999,424\ ms

 … et en pratique !

Reprenons rapidement les étapes pour utiliser un compteur pour faire clignoter une diode :

  • on note la fréquence de l’horloge du microcontrôleur
  • on définit un diviseur pour le compteur 0, à la valeur de 256
  • on prépare une section qui va gérer l’interruption pour dépassement du compteur 0
  • on y déclare une condition qui ne changera l’état de la diode toutes les 122 interruptions, soit toutes les secondes

maintenant, le code !

 

 

Nous voici donc capable d’exécuter du code à intervalle régulier, ce qui va avoir une multitude d’utilités pour la suite !

 

AVR – Annexe – Tutoriel sur l’utilisation d’un Arduino comme programmateur AVR

Je suis convaincu que la théorie vous ennuie autant que moi sans pratique, alors voici de quoi essayer vos programmes fraîchement écrit, sur un microcontrôleur.

Faites votre propre programmateur avec un Arduino

Liste du materiel et des logiciels

Il vous faudra :

  • un Arduino (peu importe la version, tant que vous savez l’utiliser)
  • un condensateur de 10 μF
  • une plaque de prototypage
  • quelques câbles

Pour la partie logiciel :

  • L’environnement de développement Arduino, disponible ici
  • WinAVR, par là, qui contient le compilateur AVR GCC mais qui a aussi la délicatesse d’installer une série d’outils provenant du monde Unix, dont Make
  • Un bon éditeur de code, j’utilise pour ma part Vim
  • Un Makefile, voici le mien  (je l’ai adapté d’un que j’ai trouvé sur le net, mais je ne retrouve plus la source)

Configuration de l’Arduino

Avant de procéder au montage, il est important de configurer l’Arduino pour qu’il agisse comme un programmateur (ISP). Branchez le donc seul (sans rien connecter sur les broches), et démarrez l’environnement de développement.

Dans le menu, sélectionnez Fichier > Exemples > ArduinoISP. Je ne m’attarderai pas sur ce code, il est documenté et long. Programmez votre Arduino avec celui-ci. Une fois terminé, vous n’aurez plus besoin de l’environnement de développement, quittez-le.

Votre Arduino est maintenant utilisable en tant que programmateur, à moins bien sûr que vous le reprogrammiez.

Montage

Ensuite il vous suffit de faire les connexions suivantes :

  • les broches 10, 11, 12 et 13 de l’Arduino respectivement sur les broches 1, 7, 6 et 5 de l’ATtiny85 (RESET, MOSI, MISO, SCK)
  • les broches VCC de l’ATtiny85 (broche 8) et de l’Arduino
  • les broches masse (GND) de l’ATtiny85 (broche 4) et l’Arduino
  • le condensateur entre GND et RESET sur l’Arduino, le (-) vers la masse.

arduino_as_ISP_bb

Voilà, c’est prêt ! Cependant, l’utilisation d’une plaque de prototypage n’est pas le plus pratique, je vous conseille donc de réaliser une «carapace» Arduino (appelé shield). Attaquons maintenant la programmation.

Préparez votre environnement

Si vous avez installer WinAVR, celui-ci a dû configurer votre variable d’environnement PATH afin d’y ajouter ses commandes (en plus de quelques commandes Unix).

Créer le projet

Décider d’un nom

Commencez par créer un dossier avec un petit nom assez distinctif mais pas trop long. Dans mon exemple, je l’appelle «blink», car il s’agit de faire clignoter une diode.

Ajouter un Makefile

Copier le fichier Makefile (enlevez le .txt) et éditez le. Les premières lignes servent à configurer la compilation et la programmation. Voici un tableau qui vous aidera à comprendre comment il fonctionne.

PRG Un nom court pour le programme
OBJ Le nom du fichier que le compilateur créera
PROGRAMMER Le type de programmateur utilisé. Ici un ISP basé sur AVR donc avrisp
PORT le port série sur lequel est branché l’Arduino.
MCU_TARGET le type de microcontrôleur pour le compilateur. Pour avoir la liste des valeurs disponibles : [cci]avr-gcc –target-help[/cci]
AVRDUDE_TARGET le type de microcontrôleur pour le programmateur. Pour avoir la liste des valeurs disponibles : [cci]avrdude -p ?[/cci]
OPTIMIZE Une série d’options dédiées au compilateur
DEFS Des options pour le compilateur
LIBS Des options d’ajout de bibliothèques pour le compilateur
FUSE la configuration des fusibles du microcontrôleur. On y jettera un œil plus tard.
HZ la fréquence de l’horloge du microcontrôleur.

Ajustez en fonction de votre configuration.

Écrivez votre code

Il vous suffit d’ajouter un fichier nommé avec le nom de votre projet, suivi de « .c ». Ici, mon code source est enregistré dans le fichier blink.c.

Compilez le code et programmez votre microcontrôleur

La compilation

L’utilisation d’un Makefile facilite grandement les opérations ; entrez seulement [cci]make[/cci]. Vous noterez qu’une série de fichiers viennent d’apparaître dans le répertoire de votre projet.

La préparation du microcontrôleur

Les fusibles du microcontrôleur permettent de configurer la vitesse de l’horloge, l’utilisation de la broche RESET ou le chien de garde (watchdog). La gestion des fusibles est sensible, car une mauvaise configuration peut endommager votre matériel (même s’il est possible de rétablir les fusibles ; j’ajouterai un tutoriel pour cela).

Par défaut, j’utilise une horloge cadencée à 8Mhz, je désactive le chien de garde, et je laisse la broche RESET tranquille. Pour régler les fusibles, utilisez la commande [cci]make fuse[/cci].

La programmation

Vous y êtes ! La dernière étape consiste à envoyer le programme vers votre ATtiny85. Entrez [cci]make install[/cci]. Une fois les barres de progression terminées, vous pouvez débrancher votre microcontrôleur et l’assembler dans votre projet.

 

AVR – Chapitre 2 – Comprendre les registres et accéder aux broches

De quoi parle-t’on ?

Du point de vu d’un processeur, les registres sont un espace de stockage particulier ; où des valeurs vitales pour l’exécution d’un programme sont enregistrées pour être exploitées de façon très rapide. En effet, les registres étant internes au processeur, il n’y a pas besoin de communiquer sur un bus relativement lent.

Les registres sont généralement réservés à la configuration et à l’exploitation des composants du microcontrôleur : les ports, les horloges, les interruptions, … Cet espace, bien que rapide, est de taille très limitée. Il n’est donc pas destiné à stocker des valeurs pour notre propre utilisation.

Ça va servir à quoi ?

L’ATtiny85 dispose de 64 registres de 8bits adressés de 0x00  à 0x3f . Dans ce document, vous trouverez à la page 7 la liste des registres du microcontrôleur, avec le nom de chaque bit.

On y trouve ainsi les registres PORTB, DDRB et PINB. Ces 3 registres seront les plus utilisés tout au long de ces tutoriels, car il s’agit des registres relatifs aux entrées et sorties du microcontrôleur.

Les registres d’entrées et sorties

Les entrées et sorties (Input/Output, souvent abrégé I/O) du microcontrôleur sont groupés par port. Pour l’ATtiny85, il n’y a qu’un seul port, le port B. Ainsi, la lettre B à la fin des noms des registres indique que ceux-ci sont destinés aux broches du port B.

DDRB : le registre «directions» du port B

Bit 7 6 5 4 3 2 1 0
Nom -      -      DDB5 DDB4 DDB3 DDB2 DDB1 DDB0
Défaut 0 0 0 0 0 0 0 0

Le registre DDRB indique les directions (entrée ou sortie) de chaque broche. Par défaut, les broches sont configurés en entrée (valeur 0). La valeur 1 correspond donc à une utilisation en sortie. Dans l’exemple ici, la ligne suivante configure la broche 0 du port B en sortie : DDRB = (1<<DDB0); .

PORTB : le registre «données» du port B

Bit 7 6 5 4 3 2 1 0
Nom -      -      PORTB5 PORTB4 PORTB3 PORTB2 PORTB1 PORTB0
Défaut 0 0 0 0 0 0 0 0

Une fois la direction définie, on peut attribuer la valeur souhaitée à la broche en utilisant ce registre. Par défaut, elle est desactivée ; pour l’activer on peut utiliser le code suivant PORTB |= (1<<PORTB0); .

 

PINB : le registre «entrée» du port B

Bit 7 6 5 4 3 2 1 0
Nom -      -      PINB5 PINB4 PINB3 PINB2 PINB1 PINB0
Défaut -      -     -      -     -      -     -      -

Maintenant que l’on sait attribuer une valeur à la broche, voyons comment lire une valeur depuis le registre PINB. Imaginons un circuit connectant la broche PINB1 du microcontrôleur avec un bouton poussoir. Si celui-ci est pressé, alors je souhaite faire changer l’état de ma diode branchée sur PORTB0. Voici un exemple de code : if(PINB & (1<<PINB1)) PORTB ^= (1<<PORTB0);

Vous voici donc capable de jouer avec les broches de votre ATtiny !  Pour fêter celà, faites clignoter toutes les broches de votre microcontrôleur de cette façon : PORTB ^= 0x3F; .

Le registre d’interruption

Les interruptions sont un moyen de déclencher une action suite à un événement dans le microcontrôleur. Il peut s’agir d’un changement d’état d’une broche ou de l’épuisement d’une horloge (aussi appelé Timeout).

GIMSK : le registre général des interruptions

Bit 7 6 5 4 3 2 1 0
Nom -      INT0 PCIE -     -     -     -     -
Défaut -      0 0 -     -      -     -      -

Ce registre configure le comportement des interruptions du microcontrôleur. Le bit INT0 est utilisé pour les interruptions externes, nous ne nous y intéresserons pas pour le moment. Le bit PCIE active le vecteur d’interruption sur les broches. Le registre suivant détaille son utilisation.

PCMSK : le registre des interruptions des broches

Bit 7 6 5 4 3 2 1 0
Nom -      -      PCINT5 PCINT4 PCINT3 PCINT2 PCINT1 PCINT0
Défaut -      -     0 0 0 0 0 0

Reprenons l’exemple du bouton qui va changer l’état de la broche de la diode, voici un moyen de le faire avec des interruptions. Cette méthode a l’avantage d’être fonctionnelle dans n’importe quelle partie d’un programme plus complexe, puisqu’elle interrompra l’exécution courante.

Pour la suite

J’espère vous avoir aidé à comprendre comment fonctionnent les registres de l’ATtiny85. La meilleure façon de travailler avec reste encore de garder la documentation à portée de main. En effet, tous les tableaux de ce chapitre sont issus de la documentation du microcontrôleur. Referez vous simplement à la page 7 du résumé de la documentation, qui indexe chaque registre.