Mémo. AVR Buzic


Essence


J'ai déjà créé un certain nombre d'appareils électroniques de loisirs différents, et j'ai une caractéristique étrange: s'il y a un émetteur piézoélectrique sonore (buzzer) sur la carte, moi, après avoir terminé le travail principal sur le projet, je commence à souffrir de bêtises et à lui faire jouer diverses mélodies (autant que possible) ) Il est particulièrement utile d'inclure une mélodie à la fin d'un long processus pour attirer l'attention. Par exemple, je l'ai utilisé lorsque j'ai construit une caméra d'exposition de fortune pour éclairer la résine photosensible, etc.

Mais lorsque j'ai commencé à chercher des exemples de génération de fréquences pour les AVR sur le réseau, pour une raison quelconque, je suis tombé sur des projets monstrueux ou insuffisamment concis qui mettent en œuvre la génération de fréquences sonores de manière purement programmatique. Et puis j'ai décidé de le découvrir moi-même ...

Digression lyrique


Mon hobby comprend la création de divers appareils sur des microcontrôleurs, car cela ne recoupe pas mon prof. activité (développement logiciel), je me considère comme un autodidacte absolu, et en électronique ce n'est pas trop fort. En fait, je préfère les microcontrôleurs PIC, mais il se trouve que j'ai accumulé un certain nombre de microcontrôleurs Atmel AVR (maintenant Microchip). Faites immédiatement une réservation que je n'ai jamais eu AVR entre les mains, c'est-à-dire Il s'agit de mon premier projet sur le MCU Atmel, à savoir Atmega48pa. Le projet lui-même réalise une charge utile, mais je n'en décrirai ici qu'une partie liée à la génération de fréquences sonores. Le test de génération de fréquences que j'ai appelé "buzic", comme abréviation pour la musique de buzzer. Oui, j'ai presque oublié: sur Habr il y a un utilisateur avec le surnom buzic , je voulais immédiatement avertir que ce mémo ne s'applique pas à lui en aucune façon, et juste au cas où, je m'excuse immédiatement d'utiliser la combinaison de lettres "Buzic".

Alors allons-y


J'ai pris connaissance d'un grand nombre d'exemples provenant du réseau - tous sont construits soit sur le cycle le plus simple dans le corps principal du firmware, soit sur l'interruption de la minuterie. Mais ils utilisent tous la même approche pour générer de la fréquence:

  1. alimenter un niveau élevé au pied du microcontrôleur
  2. faire un retard
  3. alimentation basse jusqu'au pied du microcontrôleur

Modification des retards et des paramètres de la minuterie - ajustez la fréquence.

Cette approche ne me convenait pas beaucoup, car Je n'avais aucune envie d'écrire du code pour un contrôle manuel du pied du microcontrôleur. Je voudrais que la "pierre" génère pour moi la fréquence du son, et je règle juste les valeurs de certains registres, la changeant ainsi (fréquence).

Lors de l'étude de la fiche technique (ci-après dénommée DS), j'ai toujours trouvé le mode de minuterie dont j'avais besoin - et ce mode, comme vous l'avez peut-être deviné, est le mode CTC (Clear Timer on Compare Match). Étant donné que la fonction de jouer de la musique est, pour le moins, pas la fonctionnalité principale, j'ai préféré sélectionner la minuterie 2 pour cela (paragraphe 22 de la SD).

Tout le monde sait que pratiquement tous les microcontrôleurs ont un mode de génération de signal PWM qui est implémenté sur des minuteries et il est entièrement matériel. Mais dans cette tâche, PWM ne convient pas car une seule fréquence sera générée dans le matériel. Par conséquent, nous avons besoin de PFM (modulation de fréquence d'impulsion). Une certaine similitude de PFM est le mode de temporisation CTC (clause 22.7.2 LH).

Mode CTC


Le minuteur 2 du microcontrôleur Atmega48pa est de 8 bits, c'est-à-dire qu'il «passe» de 0 à 255, puis passe en cercle. Soit dit en passant, la minuterie peut aller dans une direction différente, mais pas dans notre cas. Le prochain composant requis est l'unité de comparaison. Parlant très grossièrement, ce module est l'initiateur de tout événement lié à la minuterie. Les événements peuvent être différents - tels que les interruptions, les changements de niveau de certaines jambes du microcontrôleur, etc. (Évidemment, nous nous intéressons au second). Comme vous pouvez le deviner, le module de comparaison n'est pas seulement nommé - il compare une valeur spécifique sélectionnée par le développeur du firmware avec la valeur actuelle du minuteur. Si la valeur de la minuterie atteint la valeur que nous avons définie, un événement se produit. Des événements peuvent également se produire lorsque la minuterie déborde ou pendant une réinitialisation. Ok, nous sommes arrivés à la conclusion qu'il est pratique pour nous à certains moments que la minuterie, avec le module de comparaison, change indépendamment le niveau sur le pied du microcontrôleur à l'opposé - générant ainsi des impulsions.

La deuxième tâche consiste à définir les intervalles entre ces impulsions - c'est-à-dire contrôler la fréquence de génération. Le caractère unique du mode CTC réside dans le fait que dans ce mode, le temporisateur n'atteint pas la fin (255), mais est réinitialisé lorsque la valeur définie est atteinte. Par conséquent, en modifiant cette valeur, nous pouvons réellement contrôler la fréquence. Par exemple, si nous définissons la valeur du module de comparaison à 10, le changement de niveau au pied du microcontrôleur se produira 20 fois plus souvent que si nous le définissons (la valeur du module de comparaison) à 200. Maintenant, nous pouvons contrôler la fréquence!



Le fer



Le brochage du microcontrôleur montre que nous devons connecter notre buzzer à la jambe de PB3 (OC2A) ou à la jambe de PD3 (OC2B), car OC2A et OC2B signifient exactement que sur ces jambes, le temporisateur 2 peut générer des signaux.

Le schéma que j'utilise habituellement pour connecter le buzzer est:


Et donc nous avons assemblé l'appareil.

Registres


Dans le paragraphe précédent, nous avons décidé du choix de la jambe - c'est PB3 (OC2A), nous allons travailler avec. Si vous avez besoin de PD3, alors pour elle, tout sera le même, ce qui sera clairement visible de l'histoire.

Nous allons configurer notre timer 2 en changeant 3 registres:
  1. TCCR2A - paramètres de mode et sélection de comportement
  2. TCCR2B - paramètres de mode et diviseur de fréquence de minuterie (également bits FOC - nous ne les utilisons pas)
  3. OCR2A (OCR2B pour le boîtier de jambe PD3) - valeur du module de comparaison


Considérons d'abord les registres TCCR2A et TCCR2B

Comme vous pouvez le voir, nous avons 3 groupes de bits qui sont importants pour nous - ce sont des bits des séries COM2xx, WGM2x et CS2x
La première chose que nous devons changer est WGM2x - c'est la principale chose à choisir le mode de génération - ces bits sont utilisés pour sélectionner notre mode CTC.


note: évidemment en LH la faute de frappe dans "Mise à jour d'OCR0x à" devrait être OCR2x

C'est-à-dire le code sera comme ceci:
TCCR2A = _BV(WGM21) ; 

Comme vous pouvez le voir, TCCR2B n'est pas encore utilisé. WGM22 devrait être nul, mais il est déjà nul.

L'étape suivante consiste à configurer les bits COM2xx, plus précisément COM2Ax - car nous travaillons avec la jambe PB3 (pour PD3 COM2Bx sont utilisés de la même manière). Ce qui va arriver à notre jambe PB3 dépend d'eux.

Les bits COM2xx dépendent du mode que nous avons sélectionné avec les bits WGM2x, nous devrons donc trouver la section correspondante dans le LH. Parce que nous avons le mode CTC, c'est-à-dire pas PWM, alors nous recherchons une plaque "Compare Output Mode, non-PWM", la voici:

Ici, vous devez sélectionner «Basculer» - de sorte que le niveau sur la jambe change à l'opposé lorsque la minuterie atteint la valeur définie. Changement de niveau constant et implémente la génération de la fréquence dont nous avons besoin.

Parce que les bits COM2xx sont également dans le registre TCCR2A - seulement cela change:
 TCCR2A = _BV(COM2A0) | _BV(WGM21) ; 

Naturellement, vous devez également sélectionner le diviseur de fréquence avec des bits CS2x, et bien sûr, régler le pied PB3 sur la sortie ... mais nous ne le ferons pas encore pour que lorsque nous allumons le MK, nous n'obtenions pas un cri perçant à une fréquence incompréhensible, mais lorsque nous faisons tous les autres réglages et tourner le pied pour sortir - sera décrit ci-dessous.

Alors, apportons notre initialisation à un look complet:
 #include <avr/io.h> //set bit - using bitwise OR operator #define sbi(x,y) x |= _BV(y) //clear bit - using bitwise AND operator #define cbi(x,y) x &= ~(_BV(y)) #define BUZ_PIN PB3 void timer2_buzzer_init() { // PB3 cbi(PORTB, BUZ_PIN); // PB3  ,    cbi(DDRB, BUZ_PIN); //  TCCR2A = _BV(COM2A0) | _BV(WGM21) ; //   (      ) OCR2A = 0; } 

J'ai utilisé les macros cbi et sbi (espionné quelque part sur le réseau) pour définir des bits individuels, et je l'ai laissé de cette façon. Ces macros, bien sûr, ont été placées dans le fichier d'en-tête, mais pour plus de clarté, je les mets ici.

Calcul de la fréquence et de la durée des notes


Nous arrivons maintenant à l'essence même de la question. Il y a quelque temps, des connaissances de musiciens ont essayé de transmettre une certaine quantité d'informations sur une équipe musicale dans le cerveau de mon programmeur, mon cerveau a presque bouilli, mais j'ai quand même apporté un grain utile de ces conversations.
Je vous préviens tout de suite - d'énormes inexactitudes sont possibles.
  1. chaque mesure se compose de 4 trimestres
  2. Chaque mélodie a un tempo - c'est-à-dire le nombre de ces trimestres par minute
  3. Chaque note peut être jouée comme une mesure entière, ainsi que sa partie 1/2, 1/3, 1/4, etc.
  4. Chaque note, bien sûr, a une certaine fréquence

Nous avons examiné le cas le plus courant, en fait, tout y est plus compliqué, du moins pour moi, donc je ne discuterai pas de ce sujet dans le cadre de cette histoire.

Et bien, nous allons travailler avec ce que nous avons. Le plus important pour nous est d'obtenir à terme la fréquence de la note (en fait, la valeur du registre OCR2A) et sa durée, par exemple, en millisecondes. En conséquence, il est nécessaire de faire quelques calculs.

Parce que nous sommes dans le cadre d'un langage de programmation, les mélodies sont plus faciles à stocker dans un tableau. La façon la plus logique de définir chaque élément du tableau dans le format est note + durée. Il est nécessaire de calculer la taille de l'élément en octets, car nous écrivons sous le microcontrôleur et avec les ressources ici, c'est serré - cela signifie que la taille de l'élément en octets doit être adéquate.

La fréquence


Commençons par la fréquence. Parce que nous avons le temporisateur 8 bits 2, le registre de comparaison OCR2A est également 8 bits. Autrement dit, notre élément du tableau de mélodies sera déjà d'au moins 2 octets, car vous devez encore enregistrer la durée. En fait, 2 octets est la limite pour ce type d'artisanat. Nous n'obtenons toujours pas un bon son, c'est un euphémisme, et dépenser plus d'octets est déraisonnable. Donc, nous nous sommes arrêtés à 2 octets.

En comptant la fréquence, en fait, un autre gros problème apparaît.
Si vous regardez les fréquences des notes, nous verrons qu'elles sont divisées en octaves.

Pour la plupart des mélodies simples, 3 octaves suffisent, mais j'ai décidé d'esquiver et de mettre en œuvre 6: grandes, petites et les 4 suivantes.

Maintenant, nous allons nous éloigner de la musique et replonger dans le monde de la programmation des microcontrôleurs.
Tout temporisateur dans l'AVR (et la grande majorité des autres MK) est lié à la fréquence du MK lui-même. La fréquence du quartz dans mon circuit est de 16Mhz. La même fréquence est déterminée par le F_CPU "define" pour être égale à 16000000 dans mon cas. Dans le registre TCCR2B, nous pouvons sélectionner des diviseurs de fréquence afin que notre temporisateur 2 ne "coche" pas à une vitesse effrénée de 16000000 fois par seconde, mais un peu plus lentement. Le diviseur de fréquence est sélectionné par des bits CS2x, comme mentionné ci-dessus.


note: évidemment dans LH une faute de frappe au lieu de "CA2x" devrait être CS2x

La question se pose - comment configurer le diviseur?

Pour ce faire, vous devez comprendre comment calculer les valeurs du registre OCR2A. Et le calculer est assez simple:
OCR2A = F_CPU / (diviseur de fréquence à quartz * 2) / fréquence de note
Par exemple, prenez la note AVANT la première octave et le diviseur 256 (CS22 = 1, CS21 = 1, CS20 = 0):
OCR2A = 16000000 / (256 * 2) / 261 = 119

Je vais vous expliquer tout de suite d'où vient la multiplication par 2. Le fait est que nous avons sélectionné le mode "Toggle" avec les registres COM2Ax, ce qui signifie que le changement de niveau sur le pied de bas en haut (ou vice versa) et inversement se fera en 2 passes de la minuterie: d'abord la minuterie atteint la valeur de OCR2A et change le pied du microcontrôleur, par exemple, de 1 à 0, est réinitialisée et seulement au deuxième tour passe de 0 à 1. Par conséquent, 2 tours de la minuterie vont pour chaque onde pleine, respectivement, le diviseur doit être multiplié par 2, sinon nous obtenons seulement la moitié de la fréquence de notre note.

D'où le malheur précité ...

Si on prend la note AVANT la grosse octave et qu'on laisse le diviseur 256:
OCR2A = 16000000 / (256 * 2) / 65 = 480 !!!
480 - ce nombre est clairement supérieur à 255 et ne rentre pas physiquement dans le registre OCR2A 8 bits.

Que faire? Évidemment, changer le diviseur, mais si nous mettons le diviseur 1024, alors avec une grande octave, tout ira bien. Les problèmes vont commencer avec les octaves supérieures:
LA 4ème octave - OCR2A = 16000000 / (1024 * 2) / 3520 = 4
Une quatrième octave nette - OCR2A = 16000000 / (1024 * 2) / 3729 = 4
Les valeurs OCR2A ne sont plus différentes, ce qui signifie que le son cessera également d'être différent.

Il n'y a qu'une seule issue: pour la fréquence des notes, vous devez stocker non seulement les valeurs du registre OCR2A, mais aussi les bits du diviseur de fréquence à quartz. Parce que pour différentes octaves, il y aura une valeur différente du diviseur de fréquence à quartz, que nous devrons régler dans le registre TCCR2B!

Maintenant, tout se met en place - et j'ai finalement expliqué pourquoi nous ne pouvions pas immédiatement remplir la valeur du diviseur dans la fonction timer2_buzzer_init ().

Malheureusement, le diviseur de fréquence est de 3 bits de plus. Et ils devront être pris dans le deuxième octet de l'élément de tableau de mélodie.

Vive les macros
 #define DIV_MASK (_BV(CS20) | _BV(CS21) | _BV(CS22)) #define DIV_1024 (_BV(CS20) | _BV(CS21) | _BV(CS22)) #define DIV_256 (_BV(CS21) | _BV(CS22)) #define DIV_128 (_BV(CS20) | _BV(CS22)) #define DIV_64 _BV(CS22) #define DIV_32 (_BV(CS20) | _BV(CS21)) #define NOTE_1024( x ) ((F_CPU / (1024 * 2) / x) | (DIV_1024 << 8)) #define NOTE_256( x ) ((F_CPU / (256 * 2) / x) | (DIV_256 << 8)) #define NOTE_128( x ) ((F_CPU / (128 * 2) / x) | (DIV_128 << 8)) #define NOTE_64( x ) ((F_CPU / (64 * 2) / x) | (DIV_64 << 8)) #define NOTE_32( x ) ((F_CPU / (32 * 2) / x) | (DIV_32 << 8)) //  #define DOB NOTE_1024( 65 ) #define DO_B NOTE_1024( 69 ) #define REB NOTE_1024 ( 73 ) #define RE_B NOTE_1024 ( 78 ) #define MIB NOTE_1024 ( 82 ) #define FAB NOTE_1024 ( 87 ) #define FA_B NOTE_1024 ( 93 ) #define SOLB NOTE_1024 ( 98 ) #define SOL_B NOTE_1024 ( 104 ) #define LAB NOTE_1024 ( 110 ) #define LA_B NOTE_1024 ( 116 ) #define SIB NOTE_1024 ( 123 ) //  #define DOS NOTE_256( 131 ) #define DO_S NOTE_256( 138 ) #define RES NOTE_256 ( 146 ) #define RE_S NOTE_256 ( 155 ) #define MIS NOTE_256 ( 164 ) #define FAS NOTE_256 ( 174 ) #define FA_S NOTE_256 ( 185 ) #define SOLS NOTE_256 ( 196 ) #define SOL_S NOTE_256 ( 207 ) #define LAS NOTE_256 ( 219 ) #define LA_S NOTE_256 ( 233 ) #define SIS NOTE_256 ( 246 ) //  #define DO1 NOTE_256( 261 ) #define DO_1 NOTE_256( 277 ) #define RE1 NOTE_256 ( 293 ) #define RE_1 NOTE_256 ( 310 ) #define MI1 NOTE_256 ( 329 ) #define FA1 NOTE_256 ( 348 ) #define FA_1 NOTE_256 ( 369 ) #define SOL1 NOTE_256 ( 391 ) #define SOL_1 NOTE_256 ( 414 ) #define LA1 NOTE_256 ( 439 ) #define LA_1 NOTE_256 ( 465 ) #define SI1 NOTE_256 ( 493 ) //  #define DO2 NOTE_128( 522 ) #define DO_2 NOTE_128( 553 ) #define RE2 NOTE_128 ( 586 ) #define RE_2 NOTE_128 ( 621 ) #define MI2 NOTE_128 ( 658 ) #define FA2 NOTE_128 ( 697 ) #define FA_2 NOTE_128 ( 738 ) #define SOL2 NOTE_128 ( 782 ) #define SOL_2 NOTE_128 ( 829 ) #define LA2 NOTE_128 ( 878 ) #define LA_2 NOTE_128 ( 930 ) #define SI2 NOTE_128 ( 985 ) //  #define DO3 NOTE_64( 1047 ) #define DO_3 NOTE_64( 1109 ) #define RE3 NOTE_64 ( 1175 ) #define RE_3 NOTE_64 ( 1245 ) #define MI3 NOTE_64 ( 1319 ) #define FA3 NOTE_64 ( 1397 ) #define FA_3 NOTE_64 ( 1480 ) #define SOL3 NOTE_64 ( 1568 ) #define SOL_3 NOTE_64 ( 1661 ) #define LA3 NOTE_64 ( 1760 ) #define LA_3 NOTE_64 ( 1865 ) #define SI3 NOTE_64 ( 1976 ) //  #define DO4 NOTE_32( 2093 ) #define DO_4 NOTE_32( 2217 ) #define RE4 NOTE_32 ( 2349 ) #define RE_4 NOTE_32 ( 2489 ) #define MI4 NOTE_32 ( 2637 ) #define FA4 NOTE_32 ( 2794 ) #define FA_4 NOTE_32 ( 2960 ) #define SOL4 NOTE_32 ( 3136 ) #define SOL_4 NOTE_32 ( 3322 ) #define LA4 NOTE_32 ( 3520 ) #define LA_4 NOTE_32 ( 3729 ) #define SI4 NOTE_32 ( 3951 ) 



Et pour la durée de la note, il ne nous reste que 5 bits, calculons donc la durée.

La durée


Vous devez d'abord traduire la valeur du tempo en unités temporaires (par exemple, en millisecondes) - je l'ai fait comme ceci:
Durée d'une mesure musicale en ms = (60 000 ms * 4 trimestres) / valeur de tempo.

Par conséquent, si nous parlons de parties de battement, alors cette valeur doit être divisée, et au début, je pensais que le décalage gauche habituel pour les diviseurs serait suffisant. C'est-à-dire le code était le suivant:

 uint16_t calc_note_delay(uint16_t precalced_tempo, uint16_t note) { return (precalced_tempo / _BV((note >> 11) & 0b00111)); } 


C'est-à-dire J'ai utilisé 3 bits (parmi les 5 restants) et obtenu des parties du rythme musical de degrés 2 à 1/128. Mais quand j'ai donné un ami me demandant d'écrire une sorte de sonnerie sur mon morceau de fer, il y avait des questions pour lesquelles il n'y a pas 1/3 ou 1/6 et j'ai commencé à penser ...

Au final, j'ai créé un système délicat pour obtenir de telles durées. Un peu du 2x restant - j'ai passé sur le signe de la multiplication par 3 pour le diviseur d'horloge obtenu après le décalage. Et le dernier bit est d'indiquer s'il est nécessaire de soustraire 1. C'est difficile à décrire, il est plus facile de voir le code:
 uint16_t calc_note_delay(uint16_t precalced_tempo, uint16_t note) { note >>= 11; uint8_t divider = _BV(note & 0b00111); note >>= 3; divider *= ((note & 0b01) ? 3 : 1); divider -= (note >> 1); return (precalced_tempo / divider); } 

Ensuite, j'ai «défini» toutes les notes possibles (sauf celles qui sont inférieures à 1/128).
Les voici
 #define DEL_MINUS_1 0b10000 #define DEL_MUL_3 0b01000 #define DEL_1 0 #define DEL_1N2 1 #define DEL_1N3 (2 | DEL_MINUS_1) #define DEL_1N4 2 #define DEL_1N5 (1 | DEL_MINUS_1 | DEL_MUL_3) #define DEL_1N6 (1 | DEL_MUL_3) #define DEL_1N7 (3 | DEL_MINUS_1) #define DEL_1N8 3 #define DEL_1N11 (2 | DEL_MUL_3 | DEL_MINUS_1) #define DEL_1N12 (2 | DEL_MUL_3) #define DEL_1N15 (4 | DEL_MINUS_1) #define DEL_1N16 4 #define DEL_1N23 (3 | DEL_MUL_3 | DEL_MINUS_1) #define DEL_1N24 (3 | DEL_MUL_3) #define DEL_1N31 (5 | DEL_MINUS_1) #define DEL_1N32 5 #define DEL_1N47 (4 | DEL_MUL_3 | DEL_MINUS_1) #define DEL_1N48 (4 | DEL_MUL_3) #define DEL_1N63 (6 | DEL_MINUS_1) #define DEL_1N64 6 #define DEL_1N95 (5 | DEL_MUL_3 | DEL_MINUS_1) #define DEL_1N96 (5 | DEL_MUL_3) #define DEL_1N127 (7 | DEL_MINUS_1) #define DEL_1N128 7 



Tout mettre ensemble


Au total, nous avons le format suivant pour l'élément de notre réseau de sonneries.

  • 1bit: diviseur de retard - 1
  • 1bit: diviseur de retard * 3
  • 3 bits: décalage du diviseur de retard
  • 3bit: diviseur d'horloge cpu
  • 8 bits: valeur OCR2A

Seulement 16 bits.

Cher lecteur, si vous le souhaitez, vous pouvez imaginer le format vous-même, peut-être que quelque chose de plus vaste que le mien naîtra.

Nous avons oublié d'ajouter une note vide, c'est-à-dire silence. Et enfin, j'ai expliqué pourquoi au tout début, dans la fonction timer2_buzzer_init (), nous avons spécialement défini la jambe PB3 à l'entrée et non à la sortie. En changeant le registre DDRB, nous activerons et désactiverons la lecture du "silence" ou de la composition dans son ensemble. Parce que nous ne pouvons pas avoir de notes avec une valeur de 0 - ce sera une note «vide».

Définissez les macros manquantes et la fonction pour activer la génération du son:
 #define EMPTY_NOTE 0 #define NOTE(delay, note) (uint16_t)((delay << 11) | note) ........ ........ ........ void play_music_note(uint16_t note) { if (note) { TCCR2B = (note >> 8) & DIV_MASK; OCR2A = note & 0xff; sbi(DDRB, BUZ_PIN); } else cbi(DDRB, BUZ_PIN); } 

Je vais maintenant vous montrer à quoi ressemble une sonnerie écrite selon ce principe:

 const uint16_t king[] PROGMEM = { NOTE(DEL_1N4, MI3), NOTE(DEL_1N4, FA_3), NOTE(DEL_1N4, SOL3), NOTE(DEL_1N4, LA3), NOTE(DEL_1N4, SI3), NOTE(DEL_1N4, SOL3), NOTE(DEL_1N2, SI3), NOTE(DEL_1N4, LA_3), NOTE(DEL_1N4, FA_3), NOTE(DEL_1N4, LA_3), NOTE(DEL_1N4, EMPTY_NOTE), NOTE(DEL_1N4, LA3), NOTE(DEL_1N4, FA3), NOTE(DEL_1N2, LA3), NOTE(DEL_1N4, MI3), NOTE(DEL_1N4, FA_3), NOTE(DEL_1N4, SOL3), NOTE(DEL_1N4, LA3), NOTE(DEL_1N4, SI3), NOTE(DEL_1N4, SOL3), NOTE(DEL_1N4, SI3), NOTE(DEL_1N4, MI4), NOTE(DEL_1N4, RE4), NOTE(DEL_1N4, SI3), NOTE(DEL_1N4, SOL3), NOTE(DEL_1N4, SI3), NOTE(DEL_1N2, RE4), NOTE(DEL_1N2, EMPTY_NOTE), }; 


Jouer la sonnerie


Nous avons encore une tâche - jouer la mélodie. Pour ce faire, nous devons "parcourir" le réseau de sonneries, en tenant compte des pauses appropriées et en changeant les fréquences des notes. De toute évidence, nous avons besoin d'un autre minuteur, qui, soit dit en passant, peut être utilisé pour d'autres tâches générales, comme je le fais habituellement. De plus, vous pouvez basculer entre les éléments du tableau soit lors de l'interruption de ce temporisateur, soit dans la boucle principale, et utiliser le temporisateur pour calculer l'heure. Dans cet exemple, j'ai utilisé la 2e option.

Comme vous le savez, le corps de tout programme pour MK comprend une boucle infinie:
 int main(void) { for(;;) { //   } return 0; } 

Dans ce document, nous allons «courir» le long de notre réseau. Mais nous avons besoin d'une fonction similaire à GetTickCount de WinApi, qui renvoie le nombre de millisecondes sur les systèmes d'exploitation Windows. Mais naturellement, dans le monde de MK, il n'y a pas de telles fonctions «prêtes à l'emploi», nous devons donc les écrire nous-mêmes.

Minuterie 1


Pour calculer les intervalles de temps (je n'écris pas intentionnellement des millisecondes, vous comprendrez pourquoi plus tard), j'ai utilisé le temporisateur 1 en conjonction avec le mode CTC déjà connu. Le temporisateur 1 est un temporisateur de 16 bits, ce qui signifie que la valeur du module de comparaison pour lui est déjà indiquée par 2 registres 8 bits OCR1AH ​​et OCR1AL - pour les octets haut et bas, respectivement. Je ne veux pas décrire en détail le travail avec la minuterie 1, car cela ne s'applique pas au sujet principal de ce mémo. Par conséquent, je ne vous le dirai qu'en 2 mots.

Nous avons en fait besoin de 3 fonctions:
  • Initialisation de la minuterie
  • Gestionnaire d'interruption de minuterie
  • fonction qui renvoie le nombre d'intervalles de temps.

Fichier de code C
 #include <avr/io.h> #include <avr/interrupt.h> #include <util/atomic.h> #include "timer1_ticks.h" volatile unsigned long timer1_ticks; //  ISR (TIMER1_COMPA_vect) { timer1_ticks++; } void timer1_ticks_init() { //   // CTC ,     8 TCCR1B |= (1 << WGM12) | (1 << CS11); //     OCR1AH = (uint8_t)(CTC_MATCH_OVERFLOW >> 8); OCR1AL = (uint8_t) CTC_MATCH_OVERFLOW; //    TIMSK1 |= (1 << OCIE1A); } unsigned long ticks() { unsigned long ticks_return; //  ,   ticks_return   //     ATOMIC_BLOCK(ATOMIC_FORCEON) { ticks_return = timer1_ticks; } return ticks_return; } 



Avant d'afficher le fichier d'en-tête avec une certaine constante CTC_MATCH_OVERFLOW, nous devons remonter le temps dans la section "Durée" et déterminer la macro la plus importante pour la mélodie, qui calcule le tempo de la mélodie. J'ai attendu longtemps pour le déterminer, car il est directement connecté au lecteur, et donc au timer 1.
En première approximation, cela ressemblait à cela (voir calculs dans la section "Durée"):
 #define TEMPO( x ) (60000 * 4 / x) 

La valeur que nous obtenons à la sortie, nous devons par la suite substituer le premier argument dans la fonction calc_note_delay . Examinez maintenant de près la fonction calc_note_delay, à savoir la ligne:
 return (precalced_tempo / divider); 

On voit que la valeur obtenue en calculant la macro TEMPO est divisée par un certain diviseur. Rappelons que le diviseur maximum que nous avons défini est DEL_1N128 , c'est-à-dire le diviseur sera 128.

Prenons maintenant la valeur de tempo commune égale à 240 et faisons quelques calculs simples:
60000 * 4/240 = 1000
Oh horreur! Nous n'avons eu que 1000, puisque cette valeur sera toujours divisée par 128, nous courons le risque de glisser à 0, à des taux élevés. Il s'agit du deuxième problème de durée.

Comment le résoudre? De toute évidence, afin d'élargir la plage de valeurs de tempo, nous devons en quelque sorte augmenter le nombre obtenu en calculant la macro TEMPO. Cela ne peut se faire que d'une seule manière: s'éloigner des millisecondes et compter le temps à certains intervalles de temps. Vous comprenez maintenant pourquoi pendant tout ce temps, j'ai évité de mentionner «millisecondes» dans l'histoire. Définissons une autre macro:
  #define MS_DIVIDER 4 

Que ce soit notre diviseur de la milliseconde - divisez la milliseconde, par exemple, par 4 (250 μs).
Ensuite, vous devez modifier la macro TEMPO:
 #define TEMPO( x ) (60000 * MS_DIVIDER * 4 / x) 


Maintenant, avec une conscience claire, je vais donner le fichier d'en-tête pour travailler avec le temporisateur 1:
 #ifndef TIMER1_TICKS_H_INCLUDED #define TIMER1_TICKS_H_INCLUDED #define MS_DIVIDER 4 #define CTC_MATCH_OVERFLOW ((F_CPU / 1000) / (8 * MS_DIVIDER)) void timer1_ticks_init(); unsigned long ticks(); #endif // TIMER1_TICKS_H_INCLUDED 

Maintenant, nous pouvons, en changeant MS_DIVIDER, ajuster la plage de nos tâches - j'en ai 4 dans mon code - c'était suffisant pour mes tâches. Attention: si vous avez encore des tâches «liées» au temporisateur 1, n'oubliez pas de multiplier / diviser les valeurs de contrôle de temps pour elles par MS_DIVIDER.

Plateau tournant


Écrivons maintenant notre lecteur. Je pense que tout sera clair d'après le code et les commentaires.

 int main(void) { timer1_ticks_init(); //   sei(); timer2_buzzer_init(); //    MS_DIVIDER long time_since = ticks(); //       MS_DIVIDER uint16_t note_delay = 0; //     uint16_t note_pos = 0; //  uint16_t length = sizeof(king) / sizeof(king[0]); //     uint16_t tempo = TEMPO(240); for(;;) { unsigned long time_current = ticks(); if (time_current - time_since > note_delay) { //   uint16_t note = pgm_read_word(&king[note_pos]); //   play_music_note(note); //    note_delay = calc_note_delay(tempo, note); //  if (++note_pos >= length) note_pos = 0; time_since = time_current; } } return 0; } 


Conclusion


J'espère que ce mémo sera utile à un lecteur respecté et à moi-même, afin de ne pas oublier toutes les nuances de jouer des morceaux, au cas où je reprendrais les microcontrôleurs AVR.

Eh bien, traditionnellement, la vidéo et le code source (je l'ai développé dans l'environnement Code Blocks, alors n'ayez pas peur des fichiers obscurs):



Code source

Source: https://habr.com/ru/post/fr402329/


All Articles