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à !

Une réflexion au sujet de « Faire chanter un ATtiny85 »

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

*