
Der vorgeschlagene Player benötigt keine Speicherkarte, sondern speichert eine MIDI-Datei mit einer Länge von bis zu 6.000 Byte direkt im ATtiny85-Mikrocontroller (im Gegensatz zu
diesem klassischen Design, das WAV-Dateien wiedergibt, ist natürlich eine Speicherkarte erforderlich). Die Vier-Wege-Wiedergabe mit Dämpfung unter Verwendung von PWM ist in der Software implementiert. Ein Beispiel für das Klingen ist
hier .
Das Gerät wird nach dem Schema hergestellt:

Der Elektrolytkondensator zwischen dem Mikrocontroller und dem dynamischen Kopf verfehlt die konstante Komponente nicht, wenn aufgrund eines Softwarefehlers eine logische Einheit am Ausgang von PB4 erscheint. Die Induktivität des Kopfes überschreitet nicht die PWM-Frequenz. Wenn Sie das Gerät an den Verstärker anschließen möchten, müssen Sie wie
hier ein Tiefpassfilter hinzufügen, um eine Überlastung des Geräts mit einem PWM-Signal zu vermeiden.
Die MIDI-Datei muss als Array des Formulars in der Firmware-Quelle abgelegt werden:
const uint8_t Tune[] PROGMEM = { 0x4d, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x01, 0x03, 0xc0, 0x4d, 0x54, 0x72, 0x6b, 0x00, 0x00, 0x0a, 0x7e, 0x00, 0xff, ... 0x50, 0xb0, 0x5b, 0x00, 0x00, 0xff, 0x2f, 0x00 };
Es gibt eine vorgefertigte Lösung zum Konvertieren einer Datei in dieses Format unter UNIX-ähnlichen Betriebssystemen - das Dienstprogramm xxd. Wir nehmen die MIDI-Datei und durchlaufen dieses Dienstprogramm wie folgt:
xxd -i musicbox.mid
Die Konsole zeigt Folgendes an:
unsigned char musicbox_mid[] = { 0x4d, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x01, 0x03, 0xc0, 0x4d, 0x54, 0x72, 0x6b, 0x00, 0x00, 0x0a, 0x7e, 0x00, 0xff, ... 0x50, 0xb0, 0x5b, 0x00, 0x00, 0xff, 0x2f, 0x00 }; unsigned int musicbox_mid_len = 2708;
2708 ist die Länge in Bytes. Es stellte sich heraus, dass es weniger als 6000 waren - das heißt, es passt. Die Folge von Hexadezimalzahlen durch die Zwischenablage wird anstelle des Standardarrays in die Skizze übertragen (denken Sie daran:
in der Konsole - keine Strg + C ). Oder machen Sie das alles nicht, wenn wir es verlassen wollen.
Der Timer-Zähler 1 arbeitet mit einer Frequenz von 64 MHz von der PLL:
PLLCSR = 1<<PCKE | 1<<PLLE;
Wir übertragen diesen Timer in den PWM-Modus, um als DAC zu arbeiten. Der Arbeitszyklus hängt vom Wert von OCR1B ab:
TIMSK = 0; // Timer interrupts OFF TCCR1 = 1<<CS10; // 1:1 prescale GTCCR = 1<<PWM1B | 2<<COM1B0; // PWM B, clear on match OCR1B = 128; DDRB = 1<<DDB4; // Enable PWM output on pin 4
Die Frequenz der Rechteckimpulse hängt vom Wert von OCR1C ab. Wir belassen ihn auf 255 (standardmäßig), dann wird die Frequenz von 64 MHz durch 256 geteilt und wir erhalten 250 kHz.
Der Timer-Zähler 0 erzeugt Interrupts:
TCCR0A = 3<<WGM00; // Fast PWM TCCR0B = 1<<WGM02 | 2<<CS00; // 1/8 prescale OCR0A = 19; // Divide by 20 TIMSK = 1<<OCIE0A; // Enable compare match, disable overflow
Eine Taktfrequenz von 16 MHz wird durch einen Teiler durch 8 und dann durch einen OCR0A-Wert von 19 + 1 geteilt, und 100 kHz werden erhalten. Der Player ist vierstimmig, für jede Stimme werden 25 kHz erhalten. Bei Unterbrechung wird eine ISR-Verarbeitungsroutine aufgerufen (TIMER0_COMPA_vect), die Töne berechnet und ausgibt.
Der Watchdog-Timer ist so konfiguriert, dass alle 16 ms ein Interrupt generiert wird, der zum Empfangen von Notenfrequenzen erforderlich ist:
WDTCR = 1<<WDIE | 0<<WDP0; // Interrupt every 16ms
Um Schwingungen einer gegebenen Form zu erhalten, wird eine direkte digitale Synthese verwendet. In ATtiny85 gibt es keine Hardware-Multiplikation, daher nehmen wir Rechteckimpulse und multiplizieren die Amplitude der Hüllkurve mit 1 oder -1. Die Amplitude nimmt linear ab, und um sie zu einem bestimmten Zeitpunkt zu berechnen, reicht es aus, den Zählerstand linear zu verringern.
Für jeden Kanal stehen drei Variablen zur Verfügung: Frequenz [] - Frequenz, Acc [] - Phasenbatterie, Amp [], Hüllkurvenamplitudenwert. Die Werte von Freq [] und Acc [] werden summiert. Das höherwertige Bit Acc [] wird verwendet, um Rechteckimpulse zu erhalten. Je mehr Freq [], desto höher die Frequenz. Die fertige Wellenform wird mit der Hüllkurve Amp [] multipliziert. Alle vier Kanäle werden gemultiplext und dem Analogausgang zugeführt.
Ein wichtiger Teil des Programms ist die Prozedur zur Verarbeitung des Interrupts vom Timer-Zähler 0, der die Schwingungen an den Analogausgang ausgibt. Dieses Verfahren wird bei einer Frequenz von etwa 95 kHz aufgerufen. Für den aktuellen Kanal c werden die Werte von Acc [c] und Amp [c] aktualisiert und der Wert der aktuellen Note berechnet. Das Ergebnis wird an das OCR1B-Vergleichsregister des OCR1B-Zeitzählers gesendet, um ein analoges Signal an Pin 4 zu erhalten:
ISR(TIMER0_COMPA_vect) { static uint8_t c; signed char Temp, Mask, Env, Note; Acc[c] = Acc[c] + Freq[c]; Amp[c] = Amp[c] - (Amp[c] != 0); Temp = Acc[c] >> 8; Temp = Temp & Temp<<1; Mask = Temp >> 7; Env = Amp[c] >> Volume; Note = (Env ^ Mask) + (Mask & 1); OCR1B = Note + 128; c = (c + 1) & 3; }
String
Acc[c] = Acc[c] + Freq[c];
addiert die Frequenzfrequenz [c] zur Batterie Acc [c]. Je größer Freq [c] ist, desto schneller ändert sich der Acc [c] -Wert. Dann Linie
Amp[c] = Amp[c] - (Amp[c] != 0);
verringert den Amplitudenwert für einen bestimmten Kanal. Das Fragment (Amp [c]! = 0) wird benötigt, damit es nach Erreichen von Null nicht weiter abnimmt. Jetzt Linie
Temp = Acc[c] >> 8;
überträgt die hohen 9 Bits von Acc [c] an Temp. Und die Linie
Temp = Temp & Temp<<1;
belässt das höherwertige Bit dieser Variablen gleich eins, wenn zwei höherwertige Bits gleich eins sind, und setzt das höherwertige Bit auf null, wenn dies nicht der Fall ist. Das Ergebnis sind Rechteckimpulse mit einem Ein / Aus-Verhältnis von 25/75. In einer der vorherigen Konstruktionen hat der Autor einen Mäander angewendet, während mit der neuen Methode Harmonische etwas mehr erhalten werden. String
Mask = Temp >> 7;
überträgt die Werte des höchstwertigen Bits auf die verbleibenden Bits des Bytes. Wenn beispielsweise das höchstwertige Bit 0 war, wird 0x00 erhalten, und wenn 1 0xFF ist. String
Env = Amp[c] >> Volume;
überträgt das durch den Volume-Wert angegebene Amp [c] -Bit an Env, standardmäßig das ältere, da Volume = 8. String
Note = (Env ^ Mask) + (Mask & 1);
All dies vereint. Wenn Maske = 0x00, wird Note der Wert Env zugewiesen. Wenn Mask = 0xFF, wird Note zusätzlich zu Env + 1 ein Wert zugewiesen, dh Env mit einem Minuszeichen. Hinweis enthält jetzt die aktuelle Wellenform, die von positiven zu negativen Stromamplitudenwerten wechselt. String
OCR1B = Note + 128;
fügt Note 128 hinzu und schreibt das Ergebnis in OCR1B. String
c = (c + 1) & 3;
gibt vier Kanäle entsprechend den entsprechenden Interrupts aus und multiplext die Stimmen am Ausgang.
Das Array enthält zwölf Notenfrequenzen:
unsigned int Scale[] = { 10973, 11626, 12317, 13050, 13826, 14648, 15519, 16442, 17419, 18455, 19552, 20715};
Die Notenfrequenzen anderer Oktaven werden durch Teilen durch 2
n erhalten . Zum Beispiel teilen wir 10973 durch 2
4 und erhalten 686. Das obere Bit Acc [c] schaltet mit einer Frequenz von 25000 / (65536/685) = 261,7 Hz.
Zwei Variablen beeinflussen den Klang: Lautstärke - Lautstärke von 7 bis 9 und Abklingdämpfung von 12 bis 14. Je höher der Abklingwert, desto langsamer die Dämpfung.
Der einfachste MIDI-Interpreter achtet nur auf die Werte von Note, Tempo und Divisionskoeffizient und ignoriert andere Daten. Die Routine readIgnore () überspringt die angegebene Anzahl von Bytes in dem von der Datei empfangenen Array:
void readIgnore (int n) { Ptr = Ptr + n; }
Die Routine readNumber () liest eine Zahl aus einer bestimmten Anzahl von Bytes mit einer Genauigkeit von 4:
unsigned long readNumber (int n) { long result = 0; for (int i=0; i<n; i++) result = (result<<8) + pgm_read_byte(&Tune[Ptr++]); return result; }
Die Routine readVariable () liest eine Zahl mit variabler MIDI-Genauigkeit. Die Anzahl der Bytes kann in diesem Fall zwischen eins und vier liegen:
unsigned long readVariable () { long result = 0; uint8_t b; do { b = pgm_read_byte(&Tune[Ptr++]); result = (result<<7) + (b & 0x7F); } while (b & 0x80); return result; }
Von jedem Byte werden sieben Bits genommen, und das achte ist gleich eins, wenn Sie ein weiteres Byte weiter lesen müssen, oder null, wenn nicht.
Der Interpreter ruft die Routine noteOn () auf, um die Note auf dem folgenden verfügbaren Kanal abzuspielen:
void noteOn (uint8_t number) { uint8_t octave = number/12; uint8_t note = number%12; unsigned int freq = Scale[note]; uint8_t shift = 9-octave; Freq[Chan] = freq>>shift; Amp[Chan] = 1<<Decay; Chan = (Chan + 1) & 3; }
Die Variable Ptr gibt das nächste zu lesende Byte an:
void playMidiData () { Ptr = 0; // Begin at start of file
Der erste Block in der MIDI-Datei ist ein Header, der die Anzahl der Spuren, das Tempo und das Teilungsverhältnis angibt:
// Read header chunk unsigned long type = readNumber(4); if (type != MThd) error(1); unsigned long len = readNumber(4); unsigned int format = readNumber(2); unsigned int tracks = readNumber(2); unsigned int division = readNumber(2); // Ticks per beat TempoDivisor = (long)division*16000/Tempo;
Der Teilungskoeffizient ist normalerweise gleich 960. Nun lesen wir die angegebene Anzahl von Blöcken:
// Read track chunks for (int t=0; t<tracks; t++) { type = readNumber(4); if (type != MTrk) error(2); len = readNumber(4); EndBlock = Ptr + len;
Lesen Sie sequentielle Ereignisse bis zum Ende des Blocks:
// Parse track while (Ptr < EndBlock) { unsigned long delta = readVariable(); uint8_t event = readNumber(1); uint8_t eventType = event & 0xF0; if (delta > 0) Delay(delta/TempoDivisor);
In jedem Fall wird Delta angegeben - die Verzögerung in Zeiteinheiten, die durch den Teilungskoeffizienten bestimmt wird und vor diesem Ereignis auftreten muss. Für Ereignisse, die hier de auftreten sollten, ist Delta Null.
Meta-Ereignisse sind Ereignisse vom Typ 0xFF:
// Meta event if (event == 0xFF) { uint8_t mtype = readNumber(1); uint8_t mlen = readNumber(1); // Tempo if (mtype == 0x51) { Tempo = readNumber(mlen); TempoDivisor = (long)division*16000/Tempo; // Ignore other meta events } else readIgnore(mlen);
Die einzige Art von Meta-Ereignis, die uns interessiert, ist Tempo, der Wert des Tempos in Mikrosekunden. Standardmäßig sind es 500.000, dh eine halbe Sekunde, was 120 Schlägen pro Minute entspricht.
Die verbleibenden Ereignisse sind MIDI-Ereignisse, die durch die erste hexadezimale Ziffer ihres Typs definiert sind. Wir sind nur an 0x90 - Note On interessiert und spielen Noten auf dem folgenden verfügbaren Kanal:
// Note off - ignored } else if (eventType == 0x80) { uint8_t number = readNumber(1); uint8_t velocity = readNumber(1); // Note on } else if (eventType == 0x90) { uint8_t number = readNumber(1); uint8_t velocity = readNumber(1); noteOn(number); // Polyphonic key pressure } else if (eventType == 0xA0) readIgnore(2); // Controller change else if (eventType == 0xB0) readIgnore(2); // Program change else if (eventType == 0xC0) readIgnore(1); // Channel key pressure else if (eventType == 0xD0) readIgnore(1); // Pitch bend else if (eventType == 0xD0) readIgnore(2); else error(3); } } }
Wir ignorieren den Velocity-Wert, aber wenn Sie möchten, können Sie die anfängliche Amplitude der Note darauf einstellen. Wir überspringen den Rest der Ereignisse, ihre Länge kann unterschiedlich sein. Wenn in der MIDI-Datei ein Fehler auftritt, leuchtet die LED auf.
Der Mikrocontroller arbeitet mit einer Frequenz von 16 MHz, sodass kein Quarz erforderlich ist. Sie müssen die integrierte PLL ordnungsgemäß konfigurieren. Damit der Mikrocontroller Arduino-kompatibel wird, wird
diese Erfahrung von Spence Konde angewendet. Wählen Sie im Board-Menü das Untermenü ATtinyCore und dort ATtiny25 / 45/85. Wählen Sie in den folgenden Menüs Folgendes aus: Timer 1 Clock: CPU, BOD Disabled, ATtiny85, 16 MHz (PLL). Wählen Sie dann Bootloader brennen und füllen Sie das Programm aus. Der Programmierer wird wie das Tiny AVR Programmer Board von SpinyFun verwendet.
Die Firmware für CC-BY 4.0, die bereits eine Bach-Fuge in d-Moll hat, ist
hier , die ursprüngliche MIDI-Datei ist
hier aufgenommen .