MIDI für Kirchenorgel – 6

Debugging

Obwohl die Entwicklung gut vorwärts ging, ergaben sich durch die zunehmende Komplexität einige hartnäckige Probleme. Die Anbindung der Manuale an die Midi-In- und Midi-Out-Anschlüsse funktionierte zuverlässig, auch die Wiedergabe komplexer Midi-Dateien auf der Orgel lief störungsfrei, jedoch gab es dabei Fehlermeldungen, dass der MIDI-Out-, teilweise auch der In-Buffer überlief. Dies war sehr irritierend, da man keine fehlenden Töne oder Latenzen hörte; eine Vergrößerung des Puffers auf 256 Byte brachte nichts. Zunächst vermutete ich einen Fehler in der Puffer-Verwaltung.

MIDI-Buffer

Als Ring-Puffer dient ein Byte-Array midiTxBuffer[] mit den Indizes midiTxInIndex (zeigt auf nächste Position zum Schreiben in den Puffer) und midiTxOutIndex (zeigt auf die nächste aus dem Puffer auszugebende Byte. Anfangs sind beide Indizes Null; gleicher Wert für beide zeigt, dass nichts auszugeben ist.

Das Schreiben erfolgt in serial1MIDISend(). Der Index midiTxInIndex wird nach dem Schreiben erhöht. Durch die Und-Verknüpfung mit MIDI_TX_BUFFER_MASK wird der umlaufende Index immer im Rahmen der Arraygröße (die ergo eine 2er-Potenz sein muss) gehalten. Erreicht dieser Schreib-Index jedoch den Ausgabe-Index midiTxOutIndex sozusagen „von hinten“, so liegt ein Überlauf vor. In dem Fall wird der Index nicht erhöht, da sonst der komplette Puffer-Inhalt „gelöscht“ wäre. Falls danach noch ein Byte mit serial1MIDISend() gesendet würde, würde dies das vorige überschreiben (d.h. der Überlauf wird signalisiert, wenn er nur droht, ein Byte „Reserve“ gibt es noch.

void serial1MIDISend(uint8_t data) {
...
midiTxBuffer[midiTxInIndex] = data; 
uint8_t newIndex = (midiTxInIndex+1) & MIDI_TX_BUFFER_MASK; 
if (newIndex == midiTxOutIndex){
    // notify overflow to main by global var
    ...
} else {
    // no overflow
    midiTxInIndex = newIndex;
}
UCSR1B |= (1 << UDRIE1); // Interrupt einschalten für "Senderegister leer"
}

Mit dem Schreiben von UCSR1B wird der Interrupt freigegeben, der ausgelöst wird, wenn das Senderegister leer ist, d. h. nach einem erfolgreichen Sendevorgang. Ist das Senderegister jetzt bereits leer, so wird der Interrupt sofort ausgelöst, falls noch ein Sendevorgang läuft, dann eben etwas später. Die eigentliche Interrupt-Senderoutine sieht dann so aus:

ISR(USART1_UDRE_vect) {
    if (MIDI_TX_BUFFER_NONEMPTY){
        // there ist a byte to sent 
        UDR1 = midiTxBuffer[midiTxOutIndex];
        midiTxOutIndex = (midiTxOutIndex+1) & MIDI_TX_BUFFER_MASK;
        ...
    } else {
        // nothing to send
        UCSR1B &= ~(1 << UDRIE1);
        // Interrupt abschalten - wird in serial1MIDISend wieder gesetzt
    }
}

War in diesem selbst entworfenen Konzept, das ohne jedes Warten in while-Schleifen auskommt, fehlerhaft. Ich konnte bei der Analyse keinen Fehler finden und wandte mich einer anderen Möglichkeit zu.

LCD-Interface

Bereits bei der Entwicklung war mir die aus dem Internet übernommen Routine zur Ansteuerung des LCDs nicht sehr sympathisch. Zur Realisierung des Timings der recht langsamen Schnittstelle zum HD44780-kompatiblen Controller werden vielfach wait()-prozeduren verwendet, die zwar die Interrupt-Verarbeitung nicht blockieren, aber alles was im Hauptprogramm läuft, also v. A. die Verarbeitung des Midi-Eingangs und die Reaktion auf Tastendrücke etc. Allerdings dauert das Schreiben eines Zeichens nur ca. 0,1 ms – das schien ein zu vernachlässigender Wert zu sein. Allerdings wird wegen jeder empfangenen und gesendeten Noten-Information das Display aktualisiert, bei komplexen Stücken könnte das dann doch etwas mehr sein.

Leider erlaubt das einfache Debugging per JTAG keine Echtzeit-Analyse, oder eine Art Profiler, wie lang die CPU in bestimmten Prozeduren läuft. Zur Analyse nutzte ich daher (typisch für solche Mikrocontroller) einige der zahlreichen unbenutzen Port-Pins. Diese werden in bestimmten Routinen auf 1 gesetzt und danach wieder auf 0. Die gelbe Linie gibt die Zeit in der LCD-Routine wieder, blau ist die MIDI-Verarbeitung in main()

Die Sache schien also klar: die CPU ist zu sehr mit der Ausgabe auf dem LCD beschäftigt. Im Log zeigten sich dann massiv Fehlermeldungen, teilweise mehrmals pro Sekunde:

Oben Statuszeile: links: empfangene Note, Mitte: Betriebszeit (und „E“ für Fehler), rechts: gesendete Note
2. Zeile: Menü, 3. Zeile: Text zum Menüpunkte (hier Logeintrag), 4. Zeile: Bedeutung der 4 Softkeys.

Also reduzierte ich die Häufigkeit der Aktualisierung der empfangen bzw. gesendeten Noten in der Statuszeile auf 0,7 Sekunden. Damit waren fast alle Overflows verschwunden – aber nicht alle!

Exakt im Sekundentakt, aber erst nach ca. einer halben Minute erschien weiter ein Overflow, offensichtlich wenn die Uhr aktualisiert wird. Dies dauert nach Messung mit dem Oszilloskop ein paar Millisekunden. In der Zeit müsste ein 256 Byte großer Puffer bei einer 31250-Baud-Schnittstelle (ca 3 Bytes pro Millisekunde) aber locker ausreichen! Im Normalfall sollte der Puffer ja kaum gefüllt sein, da pro Sekunde selbst in schnellen Stücken maximal 70 Note-On/Off Befehle à 3 Byte empfangen bzw. gesendet werden, und das dauert nur 21ms.

Also überprüfte ich zunächst die Verarbeitung einzelner Midi-Befehle und musste überrascht feststellen, dass durch eine falsche Einstellung in der Midi-Konfiguration jeder Befehl 3-fach ausgegeben wurde. Das erklärte eine hohe überflüssige Last, aber nicht den Überlauf.

Die Lösung lag in einer unscheinbaren Einstellung der Midi-Player-Software beim Testen:

Ich hatte versehentlich (dank Software, die nicht mit HDPI umgehen kann) einmal eine Schleife Midi-In-Out in der Software aktiviert. Da aber in meiner Schaltung/Software empfangene Midi-Befehle an die Manuale ausgegeben und dann aber sofort als Tastendrücke registriert werden, wird jedes eingegangener Note-On/Off-Befehl wieder an Midi-Out ausgegeben (sofern der Midi-Ausgang aktiviert ist). Ergebnis ist eine Endlosschleife, die tatsächlich erst durch den Puffer-Überlauf unterbrochen wird! Nachdem ich den Menüpunkt im Player deaktiviert hatte, lief alles ohne Overflow!

Sicherheitshalber richtete ich noch eine Variable ein, in der die maximale Auslastung des Sende- und Empfangspuffers abgefragt werden kann. Sie liegt unter 5 Byte.

Dieser Beitrag wurde unter Allgemein, Mikrocontroller/Arduino veröffentlicht. Setze ein Lesezeichen auf den Permalink.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.