Ein DSP-FX benötigt jitter-freie Verarbeitung der Audiodaten in Echtzeit. Echtzeit ist ein theoretischer und dehnbarer Begriff, der die erforderliche Systemlatenz beschreibt. Beim Audio beruht diese Feststellung auf die Latenzwahrnehmung des Menschen. Wir nehmen die Latenzen unter 10ms als Echtzeit wahr. Die Latenz der gesamten Kette (Roundtriplatenz) ist die Zeit zwischen dem Eintreten des Signals in die Codec-Eingänge bis zur Bereitstehung des verarbeiteten Signals an Codec-Ausgängen. Diese Zeit darf 10ms nicht überschreiten. Am besten soll diese Zeit sogar noch unter 10ms liegen, da auch der Schall zwischen den Lautsprechern und dem Ohr zusätzlich große Latenz erzeugt und Echtzeiterlebnis schnell beeinflusst werden kann.
Systemarchitektur
Die Übersicht der Systemarchitektur des DSPs vom Flex 500 ist im folgenden Diagramm gezeigt:
Codec
Ein Audio- Codec (Coder, decoder) ist die Komponente, die die analogen Audio-Signale ins Digitale wandelt und die digitalen Audio-Signale ins Analoge wandelt. (Sampling) Er besteht aus einem oder mehreren Analog-Digital-Wandlern (ADC) und ein Digital-Analog-Wandlern (DAC). Nach diesem Schritt liegen die Audio-Signale in einem digitalen Audio-Format vor, im vorliegenden Fall als I2S-Format (Intersound).
Die Codecs müssen konfiguriert und initialisiert werden. Das erfolgt über eine andere serielle Schnittstelle, üblicherweise SPI oder I2C. Das heißt, der Codec hat auch eine Steuerschnittstelle zum DSP. Beim Flex 500 stehen beide Schnittstellen zur Verfügung.
SAI
SAI (Serial Audio Interface) ist eine Schnittstelle, über die digitale Audio-Daten ausgetauscht werden können. Der Codec kommuniziert mit der SAI-Schnittstelle vom DSP-Chip, in dem Fall STM32H743. Diese Schnittstelle serialisiert und deserialisiert die Audiodaten, D.h. er schreibt/liest die in den bzw. von dem Arbeitsspeicher.
DMA
Das Schreiben bzw. Lesen muss über eine DMA (DIrect memory access)-Hardware-Komponente erfolgen. DMA ist eine einfache Hardware, die die Aufgabe hat, ein Register in das andere zu kopieren. Die Startaddresse, FIFO, IRQs und die Länge müssen dabei konfiguriert werden. Dadurch dass DMA die Datenübertragungsaufgabe übernimmt, kann sich DSP auf die Datenverarbeitung konzentrieren.
DMA muss so konfiguriert werden, dass er ein Interrupt auslöst, wenn die Puffer
- halb voll und
- ganz voll
sind. Dadurch können die Flags der Zustandsmaschine (State machine) gesetzt werden.
Puffer
Das vom Audio-Codec ins digitale I2S-Format gewandelte Audio-Signal muss in einem Eingangspuffer zwischengespeichert werden. Dann wird dieses Puffer vom DSP verarbeitet und das Ergebnis in ein Ausgangspuffer geschrieben. Die Größe der Puffer ergibt sich aus dem Kompromiss aus zwei Anforderungen:
- Die Puffer muss so klein wie möglich sein, um eine nicht-wahrnehmbare Latenz zu erreichen.
- Die Puffer müssen so groß wie möglich sein, um eine effiziente blockweise Datenverarbeitung zu ermöglichen (Overhead muss reduziert werden)
Bei den Anforderungen
- Roundtrip-Latenz = 10ms
- Abtastrate = 48kHz
- Bittiefe = 32bit
ergibt sich eine Puffergröße von 240 für jeweils Eingangs- und Ausgangspuffer mit 32bit Registern, da Eingangslatenz und Ausgangslatenz 5ms betragen müssen.
Für die Verarbeitung mit DMA-Interrupts wird ein Doppelpuffer der Größe 480 verwendet. Für genaue Erkläreung, siehe unten.
Zustandsmaschine
Die Zustandsmaschine ist die Hauptsteuerungskomponente in der Software. Durch die Interrupts von DMA wird der Software mitgeteilt, dass das Puffer halb oder ganz voll ist. Nun kann die Zustandsmaschine, die in Endlosschleife läuft, entscheiden, ob der Prozess getriggert werden soll.
Die auf STM32 eingesetzter DMA unterstützt Double-buffering. Das heißt, er kann auf der Hälfte und am Ende der Übertragung ein Interrupt auslösen. Deshalb müssen wir das Doppelpuffer nicht selbst managen.
Der Ablauf sieht folgendermaßen aus:
- Erste Hälfte vom RX fertig ( Ab nun beschreibt DMA die zweite Hälfte)
- Erste Hälfte vom TX fertig ( Ab nun beschreibt DMA die zweite Hälfte)
- Zustandsmaschine löst die Verarbeitung der ersten Hälfte aus. Jetzt liest DSP von der ersten Hälfte von RX und beschreibt die erste Hälfte von TX.
- Zweite Hälfte vom RX fertig ( Ab nun beschreibt DMA die erste Hälfte)
- Zweite Hälfte vom TX fertig ( Ab nun beschreibt DMA die erste Hälfte)
- Zustandsmaschine löst die Verarbeitung der zweiten Hälfte aus. Jetzt liest DSP von der zweiten Hälfte von RX und beschreibt die zweite Hälfte von TX.
- Zurück zu 1.
Man erkennt, dass zwischen den RX und TX interrupts ein kleiner Versatz ist. Zwar synchronisiert der Codec die ADCs und DACs aber trotzdem entstehen ein kleines Offset von ein paar Samples. Um den Jitter zu verhindern, müssen beide Interrupts ausgewertet werden, um sicherzugehen, dass in der zu verarbeitenden Hälfte wirklich nichts mehr beschrieben bzw. gelesen wird.
Implementierung
Zuerst müssen die Stati initialisiert werden.
1 2 3 4 5 6 7 8 9 10 11 |
/*Init routine*/ void c_ser::init(void){ tx_status=0; rx_status=0; //Initialize DSP dsp.init(); } |
Die Hauptroutine, die Endlosschleife der Verarbeitung wird folgendermaßen implementiert:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
/* * Main loop for the state machine * */ void c_ser::start(void){ bool old_pos=1; unsigned i; //Endless main loop while(1){ if((tx_status && rx_status)&&old_pos){ //When pointer is on the second half of the buffer for(i=0;i<buf_size*2;i+=2){ tx_buf[i]=dsp.process(&rx_buf[i]); //(mono right) } old_pos=0; }else if(!(tx_status || rx_status)&&!old_pos){//When pointer is on the first half of the buffer for(i=buf_size*2;i<buf_size*4;i+=2){ tx_buf[i]=dsp.process(&rx_buf[i]); //(mono right) } old_pos=1; }else{ //Can measure idle here } } } |
Die Flags tx_status und rx_status wurden in Interrupt Routinen gesetzt und hier (nach der Verarbeitung) wieder geresettet.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void HAL_SAI_TxCpltCallback(SAI_HandleTypeDef *hsai) { tx_status=0; } void HAL_SAI_TxHalfCpltCallback(SAI_HandleTypeDef *hsai){ tx_status=1; } void HAL_SAI_RxCpltCallback(SAI_HandleTypeDef *hsai){ rx_status=0; } void HAL_SAI_RxHalfCpltCallback(SAI_HandleTypeDef *hsai){ rx_status=1; } |
Wichtig: Die Interruptroutine muss so schnell wie möglich ablaufen, da diese höchste Priorität hat und alles pausiert. Hier nichts verarbeiten, sondern nur Flags setzen, die dann in der Hauptschleife verarbeitet werden.
Schreibe einen Kommentar