Memo. AVR Buzic


Essenz


Ich habe bereits eine Reihe verschiedener elektronischer Hobbygeräte entwickelt und habe eine merkwürdige Eigenschaft: Wenn sich auf dem Board ein piezoelektrischer Soundemitter (Summer) befindet, leide ich nach Abschluss der Hauptarbeit an dem Projekt unter Unsinn und lasse ihn verschiedene Melodien spielen (so viel wie möglich) ) Es ist besonders nützlich, am Ende eines längeren Prozesses eine Melodie einzufügen, um die Aufmerksamkeit auf sich zu ziehen. Ich habe es zum Beispiel verwendet, als ich eine provisorische Belichtungskamera gebaut habe, um den Fotolack usw. zu beleuchten.

Als ich jedoch nach Beispielen für die Frequenzerzeugung für AVR im Netzwerk suchte, stieß ich aus irgendeinem Grund auf monströse oder unzureichend präzise Projekte, bei denen die Erzeugung von Schallfrequenzen rein programmatisch implementiert wurde. Und dann habe ich beschlossen, es selbst herauszufinden ...

Lyrischer Exkurs


Mein Hobby umfasst die Erstellung verschiedener Geräte auf Mikrocontrollern, da dies nicht mit meinem prof. Aktivität (Softwareentwicklung) halte ich für einen absoluten Autodidakt und bin in der Elektronik nicht zu stark. Tatsächlich bevorzuge ich PIC-Mikrocontroller, aber es ist einfach so passiert, dass ich eine bestimmte Anzahl von Atmel AVR-Mikrocontrollern (jetzt Microchip) angesammelt habe. Machen Sie sofort eine Reservierung, dass ich nie AVR in meinen Händen hatte, d. H. Dies ist mein erstes Projekt auf der Atmel MCU, nämlich Atmega48pa. Das Projekt selbst führt eine gewisse Nutzlast aus, aber hier werde ich nur einen Teil davon beschreiben, der sich auf die Erzeugung von Schallfrequenzen bezieht. Den Test zur Erzeugung von Frequenzen habe ich "buzic" genannt, als Abkürzung für Summer's Music. Ja, ich hätte es fast vergessen: Auf Habr gibt es einen Benutzer mit dem Spitznamen buzic . Ich wollte sofort warnen, dass dieses Memo in keiner Weise für ihn gilt, und für den Fall, dass ich mich sofort für die Verwendung der Buchstabenkombination "Buzic" entschuldige.

Also lass uns gehen


Ich habe eine große Anzahl von Beispielen aus dem Netzwerk kennengelernt - alle basieren entweder auf dem einfachsten Zyklus im Hauptteil der Firmware oder auf der Unterbrechung des Timers. Aber alle verwenden denselben Ansatz, um Frequenz zu erzeugen:

  1. Führen Sie dem Fuß des Mikrocontrollers einen hohen Füllstand zu
  2. eine Verzögerung machen
  3. Zuführen Sie den Fuß des Mikrocontrollers tief

Ändern der Verzögerungen und Timer-Einstellungen - Passen Sie die Frequenz an.

Dieser Ansatz passte mir nicht sehr, weil Ich hatte keine Lust, Code für die manuelle Steuerung des Fußes des Mikrocontrollers zu schreiben. Ich möchte, dass der „Stein“ die Schallfrequenz für mich erzeugt, und ich stelle nur die Werte bestimmter Register ein und ändere sie dadurch (Frequenz).

Beim Studium des Datenblattes (im Folgenden als DS bezeichnet) habe ich immer noch den benötigten Timer-Modus gefunden - und dieser Modus ist, wie Sie vielleicht vermutet haben, der CTC-Modus (Clear Timer on Compare Match). Da die Funktion der Musikwiedergabe, gelinde gesagt, nicht die Hauptfunktionalität ist, habe ich es vorgezogen, Timer 2 dafür auszuwählen (Absatz 22 der SD).

Jeder weiß, dass praktisch jeder Mikrocontroller über einen PWM-Signalerzeugungsmodus verfügt, der auf Timern implementiert ist und vollständig aus Hardware besteht. Aber für diese Aufgabe ist PWM nicht geeignet, weil In der Hardware wird nur eine Frequenz generiert. Deshalb brauchen wir PFM (Pulsfrequenzmodulation). Eine gewisse Ähnlichkeit von PFM besteht im CTC-Timer-Modus (Abschnitt 22.7.2 LH).

CTC-Modus


Timer 2 im Atmega48pa-Mikrocontroller ist 8-Bit, dh er „tickt“ von 0 bis 255 und geht dann in einem Kreis. Der Timer kann übrigens in eine andere Richtung gehen, in unserem Fall jedoch nicht. Die nächste erforderliche Komponente ist die Vergleichseinheit. Dieses Modul ist der Initiator aller Ereignisse, die sich auf den Timer beziehen. Ereignisse können unterschiedlich sein - wie Unterbrechungen, Änderungen des Pegels bestimmter Beine des Mikrocontrollers usw. (Offensichtlich interessieren wir uns für die zweite). Wie Sie sich vorstellen können, wird das Vergleichsmodul nicht nur benannt, sondern es vergleicht einen bestimmten vom Firmware-Entwickler ausgewählten Wert mit dem aktuellen Timer-Wert. Wenn der Timer-Wert den von uns festgelegten Wert erreicht, tritt ein Ereignis auf. Ereignisse können auch auftreten, wenn der Timer überläuft oder während eines Zurücksetzens. Ok, wir sind zu dem Schluss gekommen, dass es für uns zu bestimmten Zeiten günstig ist, dass der Timer zusammen mit dem Vergleichsmodul den Pegel am Fuß des Mikrocontrollers unabhängig in die entgegengesetzte Richtung ändert und so Impulse erzeugt.

Die zweite Aufgabe besteht darin, die Intervalle zwischen diesen Impulsen einzustellen - d.h. Steuern Sie die Häufigkeit der Erzeugung. Die ganze Einzigartigkeit des CTC-Modus liegt in der Tatsache, dass in diesem Modus der Timer nicht bis zum Ende (255) läuft, sondern zurückgesetzt wird, wenn der eingestellte Wert erreicht ist. Dementsprechend können wir durch Ändern dieses Werts tatsächlich die Frequenz steuern. Wenn wir beispielsweise den Wert des Vergleichsmoduls auf 10 setzen, tritt die Pegeländerung am Fuß des Mikrocontrollers 20-mal häufiger auf als wenn wir ihn (den Wert des Vergleichsmoduls) auf 200 setzen. Jetzt können wir die Frequenz steuern!



Eisen



Die Pinbelegung des Mikrocontrollers zeigt, dass wir unseren Summer entweder an das Bein von PB3 (OC2A) oder an das Bein von PD3 (OC2B) anschließen müssen, weil OC2A und OC2B bedeuten genau, dass Timer 2 auf diesen Beinen Signale erzeugen kann.

Das Schema, das ich normalerweise verwende, um den Summer anzuschließen, ist:


Und so haben wir das Gerät zusammengebaut.

Register


Im vorherigen Absatz haben wir uns für die Wahl des Beins entschieden - dies ist PB3 (OC2A), wir werden damit arbeiten. Wenn Sie PD3 benötigen, ist für sie alles gleich, was aus der Geschichte deutlich hervorgeht.

Wir werden unseren Timer 2 konfigurieren, indem wir 3 Register ändern:
  1. TCCR2A - Moduseinstellungen und Verhaltensauswahl
  2. TCCR2B - Moduseinstellungen und Timer-Frequenzteiler (auch FOC-Bits - wir verwenden sie nicht)
  3. OCR2A (OCR2B für den PD3-Beinfall) - Wert des Vergleichsmoduls


Betrachten Sie zuerst die Register TCCR2A und TCCR2B

Wie Sie sehen können, haben wir 3 Gruppen von Bits, die für uns von Bedeutung sind - dies sind Bits der Serien COM2xx, WGM2x und CS2x
Das erste, was wir ändern müssen, ist WGM2x - dies ist die Hauptsache für die Auswahl des Generierungsmodus - diese Bits werden zur Auswahl unseres CTC-Modus verwendet.


Hinweis: In LH sollte der Tippfehler in "Update von OCR0x at" offensichtlich OCR2x sein

Das heißt, Der Code sieht folgendermaßen aus:
TCCR2A = _BV(WGM21) ; 

Wie Sie sehen können, wird TCCR2B noch nicht verwendet. WGM22 sollte Null sein, ist aber bereits Null.

Der nächste Schritt ist die Konfiguration der COM2xx-Bits, genauer gesagt COM2Ax - weil Wir arbeiten mit dem Bein PB3 (für PD3 werden COM2Bx auf die gleiche Weise verwendet). Was mit unserem PB3-Bein passieren wird, hängt von ihnen ab.

Die COM2xx-Bits hängen von dem Modus ab, den wir mit den WGM2x-Bits ausgewählt haben, daher müssen wir den entsprechenden Abschnitt in der linken Seite finden. Weil wir haben den CTC-Modus, d.h. nicht PWM, dann suchen wir nach einer Platte "Compare Output Mode, non-PWM", hier ist es:

Hier müssen Sie „Toggle“ auswählen, damit sich der Pegel am Bein in das Gegenteil ändert, wenn der Timer den eingestellten Wert erreicht. Ständige Pegeländerung und implementiert die Erzeugung der Frequenz, die wir benötigen.

Weil Die COM2xx-Bits befinden sich ebenfalls im TCCR2A-Register - nur es ändert sich:
 TCCR2A = _BV(COM2A0) | _BV(WGM21) ; 

Natürlich müssen Sie auch den Frequenzteiler mit CS2x-Bits auswählen und natürlich den PB3-Fuß auf den Ausgang setzen ... aber wir werden es noch nicht tun, damit wir beim Einschalten des MK kein durchdringendes Kreischen bei einer unverständlichen Frequenz bekommen, sondern wenn wir alle anderen Einstellungen und Einstellungen vornehmen Zum Verlassen den Fuß einschalten - wird unten beschrieben.

Lassen Sie uns also unsere Initialisierung vollständig betrachten:
 #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; } 

Ich habe die Makros cbi und sbi (irgendwo im Netzwerk ausspioniert) verwendet, um einzelne Bits zu setzen, und habe es so belassen. Diese Makros wurden natürlich in die Header-Datei eingefügt, aber der Übersichtlichkeit halber habe ich sie hier eingefügt.

Berechnung der Häufigkeit und Dauer von Noten


Jetzt kommen wir zum Kern des Problems. Vor einiger Zeit versuchten Bekannte von Musikern, eine bestimmte Menge an Informationen über ein musikalisches Personal in das Gehirn meines Programmierers zu bringen, mein Gehirn kochte fast, aber ich brachte dennoch ein nützliches Korn aus diesen Gesprächen heraus.
Ich warne Sie sofort - große Ungenauigkeiten sind möglich.
  1. Jede Maßnahme besteht aus 4 Vierteln
  2. Jede Melodie hat ein Tempo - d.h. die Anzahl solcher Viertel pro Minute
  3. Jede Note kann als ganzer Takt sowie als Teil 1/2, 1/3, 1/4 usw. gespielt werden.
  4. Jede Note hat natürlich eine bestimmte Frequenz

Wir haben den häufigsten Fall untersucht, tatsächlich ist dort zumindest für mich alles komplizierter, so dass ich dieses Thema im Rahmen dieser Geschichte nicht diskutieren werde.

Okay, wir werden mit dem arbeiten, was wir haben. Für uns ist es am wichtigsten, die Frequenz der Note (tatsächlich den Wert des OCR2A-Registers) und ihre Dauer beispielsweise in Millisekunden zu ermitteln. Dementsprechend müssen einige Berechnungen durchgeführt werden.

Weil Wir befinden uns im Rahmen einer Programmiersprache. Melodien lassen sich am einfachsten in einem Array speichern. Die logischste Möglichkeit, jedes Element des Arrays im Format festzulegen, ist Note + Dauer. Es ist notwendig, die Größe des Elements in Bytes zu berechnen, da wir unter dem Mikrocontroller schreiben und es hier mit Ressourcen eng ist - das bedeutet, dass die Größe des Elements in Bytes angemessen sein muss.

Frequenz


Beginnen wir mit der Frequenz. Weil Wir haben 8-Bit-Timer 2, das OCR2A-Vergleichsregister ist ebenfalls 8-Bit. Das heißt, unser Element des Melodie-Arrays besteht bereits aus mindestens 2 Bytes, da Sie die Dauer noch speichern müssen. Tatsächlich sind 2 Bytes die Grenze für diese Art von Handwerk. Wir bekommen immer noch keinen guten Sound, um es milde auszudrücken, und mehr Bytes auszugeben ist unvernünftig. Also haben wir bei 2 Bytes angehalten.

Beim Zählen der Frequenz tritt tatsächlich ein weiteres großes Problem auf.
Wenn Sie sich die Frequenzen der Noten ansehen, werden Sie feststellen, dass sie in Oktaven unterteilt sind.

Für die meisten einfachen Melodien reichen 3 Oktaven aus, aber ich habe mich entschlossen, 6 auszuweichen und zu implementieren: groß, klein und die nächsten 4.

Lassen Sie uns nun von der Musik abschweifen und zurück in die Welt der Mikrocontroller-Programmierung eintauchen.
Jeder Timer im AVR (und die überwiegende Mehrheit der anderen MK) ist an die Frequenz des MK selbst gebunden. Die Frequenz des Quarzes in meiner Schaltung beträgt 16 MHz. Die gleiche Frequenz wird von der F_CPU "define" als gleich 16000000 in meinem Fall bestimmt. Im TCCR2B-Register können wir Frequenzteiler so auswählen, dass unser Timer 2 nicht mit einer rasenden Geschwindigkeit von 16000000 Mal pro Sekunde "tickt", sondern etwas langsamer. Der Frequenzteiler wird wie oben erwähnt durch CS2x-Bits ausgewählt.


Hinweis: Offensichtlich sollte in LH ein Tippfehler anstelle von "CA2x" CS2x sein

Es stellt sich die Frage, wie der Teiler konfiguriert werden soll.

Dazu müssen Sie wissen, wie die Werte für das OCR2A-Register berechnet werden. Und die Berechnung ist ganz einfach:
OCR2A = F_CPU / (Quarzfrequenzteiler * 2) / Notenfrequenz
Nehmen Sie zum Beispiel die Notiz VOR der ersten Oktave und dem Divisor 256 (CS22 = 1, CS21 = 1, CS20 = 0):
OCR2A = 16000000 / (256 * 2) / 261 = 119

Ich werde gleich erklären, woher die Multiplikation mit 2 stammt. Tatsache ist, dass wir den Umschaltmodus mit den COM2Ax-Registern ausgewählt haben. Dies bedeutet, dass die Änderung der Pegel am Fuß von niedrig nach hoch (oder umgekehrt) und zurück in zwei Durchgängen des Timers erfolgt: zuerst Der Timer erreicht den Wert von OCR2A und ändert den Fuß des Mikrocontrollers, z. B. von 1 auf 0, wird zurückgesetzt und ändert erst in der zweiten Runde 0 zurück auf 1. Daher müssen 2 Runden des Timers für jede volle Welle jeweils den Divisor mit 2 multipliziert werden, andernfalls erhalten wir nur die Hälfte der Häufigkeit unserer Notiz.

Daher das oben erwähnte Unglück ...

Wenn wir die Note VOR der großen Oktave nehmen und den Divisor 256 verlassen:
OCR2A = 16000000 / (256 * 2) / 65 = 480 !!!
480 - Diese Zahl ist eindeutig größer als 255 und passt physikalisch nicht in das 8-Bit-OCR2A-Register.

Was tun? Natürlich den Teiler ändern, aber wenn wir den Teiler 1024 setzen, dann wird mit einer großen Oktave alles in Ordnung sein. Probleme beginnen mit den oberen Oktaven:
LA 4. Oktave - OCR2A = 16000000 / (1024 * 2) / 3520 = 4
Eine scharfe vierte Oktave - OCR2A = 16000000 / (1024 * 2) / 3729 = 4
Die OCR2A-Werte unterscheiden sich nicht mehr, was bedeutet, dass auch der Klang nicht mehr anders ist.

Es gibt nur einen Ausweg: Für die Frequenz der Noten müssen Sie nicht nur die Werte des OCR2A-Registers speichern, sondern auch die Bits des Quarzfrequenzteilers. Weil Für verschiedene Oktaven gibt es einen unterschiedlichen Wert des Quarzfrequenzteilers, den wir im TCCR2B-Register einstellen müssen!

Jetzt passt alles zusammen - und ich erklärte schließlich, warum wir den Divisorwert in der Funktion timer2_buzzer_init () nicht sofort eingeben konnten.

Leider ist der Frequenzteiler 3 weitere Bits. Und sie müssen im zweiten Byte des Elements des Melodie-Arrays aufgenommen werden.

Es lebe die Makros
 #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 ) 



Und für die Dauer der Note haben wir nur noch 5 Bits, also berechnen wir die Dauer.

Dauer


Zuerst müssen Sie den Tempowert in temporäre Einheiten übersetzen (z. B. in Millisekunden) - ich habe es so gemacht:
Dauer eines musikalischen Taktes in ms = (60.000 ms * 4 Viertel) / Tempowert.

Wenn wir also über Beat-Parts sprechen, muss dieser Wert geteilt werden, und zuerst dachte ich, dass die übliche Linksverschiebung für die Teiler ausreichen würde. Das heißt, Der Code war folgender:

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


Das heißt, Ich habe 3 Bits (von den restlichen 5) verwendet und Teile des musikalischen Beats von Grad 2 bis zu 1/128 bekommen. Aber als ich einen Freund gab, der mich bat, einen Klingelton auf mein Stück Eisen zu schreiben, gab es Fragen, warum es kein 1/3 oder 1/6 gibt, und ich fing an zu überlegen ...

Am Ende habe ich ein kniffliges System entwickelt, um solche Dauern zu erhalten. Ein Bit von den verbleibenden 2x - Ich habe das Vorzeichen der Multiplikation mit 3 für den nach der Verschiebung erhaltenen Taktteiler ausgegeben. Und das letzte Bit gibt an, ob es notwendig ist, 1 zu subtrahieren. Dies ist schwer zu beschreiben, es ist einfacher, den Code zu sehen:
 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); } 

Dann habe ich alle möglichen (außer denen, die weniger als 1/128 sind) Noten „definiert“.
Hier sind sie
 #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 



Alles zusammenfügen


Insgesamt haben wir das folgende Format für das Element unseres Klingelton-Arrays.

  • 1 Bit: Verzögerungsteiler - 1
  • 1 Bit: Verzögerungsteiler * 3
  • 3bit: Verzögerungsteilerverschiebung
  • 3bit: CPU-Taktteiler
  • 8 Bit: OCR2A-Wert

Nur 16 Bit.

Lieber Leser, wenn Sie möchten, können Sie sich das Format selbst einfallen lassen, vielleicht wird etwas Großartigeres als meins geboren.

Wir haben vergessen, eine leere Notiz hinzuzufügen, d. H. Stille. Und schließlich habe ich erklärt, warum wir in der Funktion timer2_buzzer_init () ganz am Anfang das PB3-Bein speziell auf den Eingang und nicht auf den Ausgang gesetzt haben. Wenn Sie das Register DDRB ändern, wird die Wiedergabe von "Stille" oder der gesamten Komposition ein- und ausgeschaltet. Weil Wir können keine Noten mit dem Wert 0 haben - es wird eine "leere" Note sein.

Definieren Sie die fehlenden Makros und die Funktion zum Aktivieren der Klangerzeugung:
 #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); } 

Jetzt zeige ich Ihnen, wie ein nach diesem Prinzip geschriebener Klingelton aussieht:

 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), }; 


Klingelton abspielen


Wir haben noch eine Aufgabe - die Melodie zu spielen. Dazu müssen wir das Klingelton-Array "durchlaufen", den entsprechenden Pausen standhalten und die Frequenzen der Noten umschalten. Natürlich brauchen wir einen anderen Timer, der übrigens für andere allgemeine Aufgaben verwendet werden kann, wie ich es normalerweise tue. Darüber hinaus können Sie entweder in der Unterbrechung dieses Timers oder in der Hauptschleife zwischen Array-Elementen wechseln und den Timer zur Berechnung der Zeit verwenden. In diesem Beispiel habe ich die 2. Option verwendet.

Wie Sie wissen, enthält der Hauptteil eines Programms für MK eine Endlosschleife:
 int main(void) { for(;;) { //   } return 0; } 

Darin werden wir entlang unseres Arrays "rennen". Wir benötigen jedoch eine ähnliche Funktion wie GetTickCount von WinApi, die die Anzahl der Millisekunden unter Windows-Betriebssystemen zurückgibt. Aber natürlich gibt es in der Welt von MK keine solchen Funktionen "out of the box", also müssen wir sie selbst schreiben.

Timer 1


Zur Berechnung der Zeitintervalle (ich schreibe absichtlich keine Millisekunden, später werden Sie verstehen, warum) habe ich Timer 1 in Verbindung mit dem bereits bekannten CTC-Modus verwendet. Timer 1 ist ein 16-Bit-Timer, was bedeutet, dass der Wert des Vergleichsmoduls dafür bereits durch 2 8-Bit-Register OCR1AH ​​und OCR1AL angezeigt wird - für die hohen bzw. niedrigen Bytes. Ich möchte die Arbeit mit Timer 1 nicht im Detail beschreiben, da dies nicht für das Hauptthema dieses Memos gilt. Deshalb werde ich Ihnen nur in 2 Worten sagen.

Wir brauchen eigentlich 3 Funktionen:
  • Timer-Initialisierung
  • Timer-Interrupt-Handler
  • Funktion, die die Anzahl der Zeitintervalle zurückgibt.

Code C-Datei
 #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; } 



Bevor ich die Header-Datei mit einer bestimmten Konstante CTC_MATCH_OVERFLOW zeige, müssen wir in der Zeit zurück zum Abschnitt „Dauer“ gehen und das für die Melodie wichtigste Makro bestimmen, das das Tempo der Melodie berechnet. Ich habe lange gewartet, um es festzustellen, da es direkt mit dem Player verbunden ist und daher mit Timer 1.
In erster Näherung sah es so aus (siehe Berechnungen im Abschnitt "Dauer"):
 #define TEMPO( x ) (60000 * 4 / x) 

Den Wert, den wir am Ausgang erhalten, müssen wir anschließend das erste Argument in die Funktion calc_note_delay einsetzen . Schauen Sie sich nun die Funktion calc_note_delay genauer an, nämlich die Zeile:
 return (precalced_tempo / divider); 

Wir sehen, dass der durch Berechnung des TEMPO-Makros erhaltene Wert durch einen bestimmten Divisor geteilt wird. Denken Sie daran, dass der maximale Divisor, den wir definiert haben, DEL_1N128 ist , d. H. Der Divisor wird 128 sein.

Nehmen wir nun den gemeinsamen Tempo-Wert von 240 und führen einige einfache Berechnungen durch:
60000 * 4/240 = 1000
Oh Horror! Wir haben nur 1000, da dieser Wert immer noch durch 128 geteilt wird, laufen wir Gefahr, bei hohen Raten auf 0 zu rutschen. Dies ist die zweite Ausgabe der Dauer.

Wie kann man es lösen? Um den Bereich der Tempowerte zu erweitern, müssen wir natürlich die Anzahl erhöhen, die durch die Berechnung des TEMPO-Makros erhalten wird. Dies kann nur auf eine Weise erfolgen - um Millisekunden zu vermeiden und die Zeit in bestimmten Zeitintervallen zu zählen. Jetzt verstehen Sie, warum ich es die ganze Zeit vermieden habe, „Millisekunden“ in der Geschichte zu erwähnen. Definieren wir ein anderes Makro:
  #define MS_DIVIDER 4 

Sei es unser Teiler der Millisekunde - dividiere die Millisekunde zum Beispiel durch 4 (250 μs).
Dann müssen Sie das TEMPO-Makro ändern:
 #define TEMPO( x ) (60000 * MS_DIVIDER * 4 / x) 


Jetzt werde ich mit gutem Gewissen die Header-Datei für die Arbeit mit Timer 1 geben:
 #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 

Jetzt können wir durch Ändern von MS_DIVIDER den Bereich für unsere Aufgaben anpassen - ich habe 4 in meinem Code - dies war genug für meine Aufgaben. Achtung: Wenn Sie noch Aufgaben haben, die an Timer 1 gebunden sind, vergessen Sie nicht, die Zeitsteuerungswerte für diese mit MS_DIVIDER zu multiplizieren / zu dividieren.

Plattenspieler


Jetzt schreiben wir unseren Player. Ich denke, aus dem Code und den Kommentaren wird alles klar.

 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; } 


Fazit


Ich hoffe, dass dieses Memo für einen angesehenen Leser und mich selbst nützlich sein wird, um nicht alle Nuancen des Musikspielens zu vergessen, falls ich die AVR-Mikrocontroller wieder in die Hand nehme.

Nun, traditionell der Video- und Quellcode (ich habe ihn in der Code Blocks-Umgebung entwickelt, also keine Angst vor obskuren Dateien):



Quellcode

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


All Articles