Übersicht

Ultranet wird von zahlreichen digitalen Audiogeräten der Marke Behringer, KlarkTeknik und Midas (alles Sub-Firmen der Marke MusicTribe) eingesetzt, um 16 digitale Audiokanäle mit 24 Bit Audio und 48kHz Samplerate zu übertragen. Sehr schön ist das ganze im Personal-Monitoring-System Powerplay P16-X (X für zahlreiche Subgeräte) umgesetzt.

An einem defekten P16-I (einem Analog-zu-Digital-Wandler für 16 einzelne Kanäle) habe ich mich mal dem Protokoll gewidmet und unter Verwendung eines Arduino MKR Vidor 4000 Boards mit FPGA einen einfachen digitalen Audiomixer gebaut. Dieser Mixer kann sowohl über die eingebaute USB-Schnittstelle, als auch über Ethernet (wenn man einen WIZ5500-Chip zusätzlich einbaut) steuern. Besonders schön ist, dass man aufgrund des FPGAs sehr einfach beliebige Funktionen nachrüsten oder ändern kann und ein unglaublich flexibles DIY-Audiosystem erhält, was sogar analoge Signale ausgeben kann. Wie? Das erfahrt ihr entweder im Youtube-Video im Überblick, oder auf den nachfolgenden Seiten im Detail. Der Code und alle notwendigen Design-Teile sind schließlich auf Github verfügbar.

 

Youtube-Video

 

Übersicht über das verwendete P16-I von Behringer

P16-I

 

Verwendete Toolchain

Bevor wir in die eigentliche Thematik einsteigen, hier ein paar Informationen zur verwendeten Toolchain: die Arduino-Platine hat zwei voneinander unabhängige Systeme: den SAMD21-Mikrocontroller und den Intel Cyclone 10LP-FPGA. Den Mikrocontroller kann man direkt über die Arduino-IDE v2.x ganz herkömmlich in C programmieren.

Den FPGA hingegen kann man nicht mit der Arduino-Toolchain direkt beschreiben, sondern benötigt von der Intel-Website die kostenfreie Intel Quartus Prime-Software. Diese Software erlaubt das synthetisieren von VHDL- und Verilog-Dateien und ermöglicht die Zusammenschaltung mehrerer einzelner VHDL-Blöcke über eine grafische Oberfläche. Nach ein wenig Einarbeitungszeit lässt sich mit Quartus sehr gut digitale Logik entwerfen und auch komplexere VHDL-Blöcke aus dem Netz verwenden.

Nachdem man VHDL in Quartus zu einem Bitstream synthetisiert hat, erhält man bei korrekten Projekteinstellungen auch eine sogenannte "Tabular Text File" mit der Endung *.ttf (nein, keine Truetype-Font). Diese TTF-Datei enthält den eigentlichen Bitstream für den FPGA, der konzeptbedingt bei einem Spannungsabfall jegliche Logik schlagartig verliert und eine harte Demenz erleidet. Daher muss man diesen Bitstream bei jedem Neustart des Systems dem FPGA wieder zuführen. Dies kann man entweder über einen separaten Flash tun - oder über den SAMD21... letztere Methode ist aus Programmierersicht sogar deutlich einfacher, da wir ja eine direkte Verbindung zwischen SAMD21 und dem Computer haben.

Daher habe ich im Arduino-Projekt eine entsprechende Header-Datei vorbereitet, die über ein Batchscript den Bitstream enthält. Diesen Bitstream lade ich dann während des Bootvorgangs via JTAG direkt auf den FPGA. Somit kann ich mit einem Firmwareupdate des SAMD21 auch gleich den Bitstream des FPGA mitsenden. Den TTF-Bitstream kann man mit der Software vidorcvt.exe automatisiert in eine Header-Datei konvertieren. Eine Anleitung findet ihr auf Github in meinem Projekt: Ultranet Receiver

 

So sieht der Weg vom VHDL-Design bis zum FPGA aus:

Quartus Prime Lite -> Bitstream (TTF) -> VidorCvt.exe -> Bitstream.h -> Arduino IDE v2.x -> SAMD21-Binary-File -> bossac.exe -> USB -> SAMD21 -> JTAG -> FPGA

 

Ultranet-Signale empfangen

Ultranet nutzt als Grundlage ein unidirektionales Datenformat, das bereits 1985 standardisiert wurde: AES/EBU. Im AES/EBU-Standard sind dabei Sampleraten für ein Stereosignal zwischen 32kHz und 192kHz, und Bitraten zwischen 16 und 24 Bit spezifiziert. Ultranet selbst nutzt im konkreten Fall pro Kanal 48kHz bei 24 Bit. Da die Spezifikation bis zu 192kHz erlaubt, bekommt man also 4 Stereo-Kanäle übertragen (=8 Audiokanäle). Als elektrisches Medium kommt Ethernet zum Einsatz, allerdings lediglich im Layer1, sodass man zwar die elektrischen Eigenschaften von Ethernet einsetzt, nicht aber die eigentliche Ethernet-Kommunikation:

OSI-Layer-Definition
Quelle: Wikipedia

Ultranet nutzt dabei die Pins 1/2 für die Übertragung der Kanäle 1 bis 8 und die Pins 3/6 für die Kanäle 9-16. Pins 4/5 und 7/8 sind für die Bereitstellung von +15V Gleichspannung zur Versorgung angeschlossener Geräte vorgesehen:

Ethernet
Quelle: Wikipedia

 

Das besondere an AES/EBU ist, dass es keine Rückkanäle benötigt und inhärent eine Taktrate enthält. Somit kann man Zweidraht-Verbindungen oder auch optische Verbindungen (TOSLINK) verwenden, um AES/EBU-Signale zu übertragen.

Aufgrund der hohen Datenraten bietet es sich nicht wirklich an, einen Mikrocontroller direkt zum Dekodieren zu verwenden. Allerdings hat der Arduino MKR Vidor 4000 einen FPGA an Board, mit dem man digitale Logik recht gut umsetzen kann. Auf OpenCores.org hat Petr Nohavica einen AES3-Receiver-Block bereitgestellt, mit dem man AES3 bei verschiedenen Sample-Raten dekodieren kann. Taktet man diesen Block mit 200MHz, so kann man auch die 192kHz des Ultranet-Signals gut dekodieren. Im Youtube-Video habe ich zum Empfang einfach eine TX+ Eingangsleitung des Oktal-Buffer-ICs direkt an den Arduino angeschlossen:

Oktal-Buffer

Der AES3-Receiver wandelt die empfangenen AES/EBU (Ultranet) Signale dann in ein herkömmliches I2S-Signal um. Hierzu müssen wir uns kurz das eigentliche AES/EBU-Format etwas genauer ansehen. Da der Takt inhärent im Signal eingebettet ist, kommt hier - bis auf eine Ausnahme - eine Biphase-Mark-Encodierung zum Einsatz. Nachfolgende Grafik zeigt die Erzeugung dieses Signals:

Biphase-Mark-Encoding
Quelle: NTI Audio Application Note

AES/EBU enthält gemäß AppNote entsprechend folgende Daten:

AESEBU

Das 4-bit-SYNC-Signal enthält in Realität kein Biphase-Mark encodiertes Signal, sondern "reale" Bits, sodass wir hier 8 Bits empfangen können (Biphase-Mark-Encodierung erzeugt einen Datenoverhead des Faktors 2). Dieses SYNC-Signal kennt laut Spezifikation drei Zustände: X-, Y- und Z-Zustand (genannt Preamble). Alle drei 8-Bit-Zustände haben ein eindeutiges Muster, auf das man Synchroniseren kann. Genau dies macht der AES-Receiver und gibt die empfangenen 24 Nutzdaten (20 Audiobits + 4 Aux-Bits) als I2S-Signal aus.

I2S ist im Prinzip das Audio-Gegenstück zu I2C (Inter-Integrated Circuit), welches zur Kommunikation zwischen ICs zum Einsatz kommt. I2S steht für Inter-IC Sound und beschreibt ein serielles Datenformat, bei welchem neben einer Bitclock eine Wordclock als Synchronisationssignal und L/R-Pointer neben dem eigentlichen Datensignal eingesetzt wird.

Der Receive-Block benötigt lediglich eine ausreichend hohe Clock (hier 200MHz) und die Eingangsdaten, die vom P16-I kommen. Am Ausgang gibt es schließlich die drei I2S-Signale Bitclock (bclk), Wordclock (lrck) und die seriellen Daten (sdata). BSYNC wird bei jedem Empfang einer Z-Preamble auf 1 gesetzt und markiert den Empfang von Kanal 1. Der "active"-Ausgang liegt auf HIGH, wenn gültige Ultranet-Daten empfangen werden:

AES-Receiver

 

I2S-Signale zu einzelnen Bitvektoren wandeln

Auch für den Empfang von I2S-Signalen gibt es auf OpenCores.org einen fertigen Block: Geir Drange hat den I2S Interface-Block bereitgestellt, mit dem man serielle I2S-Signale in 24-Bit-Logikvektoren wandeln kann. Ich habe diesen Block entsprechend umgeändert und als reinen Receive-Block angepasst. Zudem habe ich einen Counter eingefügt, der bei jedem Empfang einer Z-Preamble (BSYNC = HIGH) auf 0 gesetzt wird und ansonsten bei Wechsel der Wordclock (LRCLK) inkrementiert wird:

I2S_Rx

Am Ausgang des Blocks stehen die Audiosamples dann nach jedem gültigen Empfang für einen kurzen Moment zur Verfügung. Ein Erhalt wird mit einem "HIGH" auf dem Ausgang "new_data" mitgeteilt. Da wir aber Zugriff auf alle 16 Kanäle haben möchten, habe ich noch einen Demultiplexer-Block geschrieben, der bei jedem new_data passend zum Channel-Counter die einzelnen Audiosamples auf einen von 8 Ausgängen routet:

Ultranet_Demuxer

Von nun an kann man mit den empfangenen Audiosamples machen was man möchte. Die gesamte Signalkette sieht im Überblick wie folgt aus:

Signalverlauf

 

Audiomixing-Funktionen im FPGA

Was wäre dieses ganze Projekt ohne eine grundlegende Mixing-Funktion? Fügt man eine einfache Signed-Integer-Multiplikation in den Signalpfad der Audiosamples, so kann man die Lautstärke einstellen, da die Audiosamples im Prinzip nichts anderes als die Auslenkung der Lautsprecher-Kalotte beinhalten. Senkt man also entsprechend die Aussteuerung der einzelnen Bits der Audiosamples, reduziert sich auch die Lautstärke. Damit ich keine Bruchzahlen verrechnen muss, habe ich eine einfache Integer-Multiplikation implementiert und das Lautstärkesignal zwischen 0 und 256 laufen lassen. 256 deshalb, da es 8-Bit entspricht:

Multiplication

Die Division durch 256 kann man in der Digitaltechnik (und somit auch im FPGA) dabei sehr einfach durch ein Bitshift um 8 Bit nach Rechts realisieren, was die ganze Berechnung sehr einfach macht. Fertig ist schon unsere Lautstärkeregelung.

 

Nachgelagert an die einzelnen Multiplikationsblöcke habe ich zwei Summen-Blöcke gepackt, die die Audiosumme aller angeschlossener Kanäle bilden. Hier verwende ich derzeit eine ganz einfache Integer-Addition, sodass bei zu hohem Pegel der Einzelsignale auch ein Signal-Clipping entstehen kann:

FPGA_Volume

Nach den beiden Summenblöcken habe ich jeweils noch einmal eine Lautstärkeregelung zur Abbildung der beiden Main-Fader für Links und Rechts implementiert.

 

Wo kommen aber die Lautstärkesignale eigentlich her? Hier kommt nun endlich der SAMD21-Mikrocontroller auf dem Board ins Spiel. Im C-Code des Controllers habe ich einen Empfang über die serielle Schnittstelle implementiert, der die einzelnen Kanalwerte empfängt. Alternativ kann man bei einem angeschlossenen WIZ5500-Ethernet-Chip die Steuerung auch über eine Terminalschnittstelle oder bei Bedarf auch einen Webserver einstellen.

Die Lautstärkesignale werden dann als Integer-Wert zum FPGA gesendet. Derzeit können 256 einzelne Lautstärke-Kommandos übertragen werden, da ich ein 8-Bit-Kommando zur Unterscheidung der 32-Bit-Nutzdaten einsetze:

Procotol

Über das Protokoll habe ich zudem auch das Links-/Rechts-Balancing realisiert. Man stellt über die Befehle einen Wert für die Balance zwischen 0% (ganz links) und 100% (ganz rechts) ein. Im Hintergrund wird aber lediglich die Lautstärke für links und rechts berechnet und dann an den FPGA weitergegeben. Der Mikrocontroller speichert für jeden Kanal die aktuelle absolute Lautstärke und die Balance und errechnet darüber die notwendigen Lautstärken für links und rechts.

 

Digitale Audioausgabe

Vom FPGA kann man sich recht einfach SPDIF-Signale (AES/EBU) ausgeben lassen, sodass man einen im Handel erhältlichen SPDIF -> Analog-Konverter einsetzen kann. Hier hat Geir Drange wieder einen sehr schönen SPDIF-Transmitter-Block auf OpenCores.org bereitgestellt. Diesen Block schließt man einfach an zwei der 16 24-Bit-Vektoren an und kann mit einer passenden Clock von 6,144MHz wieder Biphase-Mark-Encodierte Daten ausgeben:

SPDIF_Tx

Warum eigentlich jetzt 6,144MHz? Nun, aufgrund des Biphase-Mark-Encodierens und der zu übertragenen Bits, müssen wir entsprechend schnell die Bits über die Leitung bekommen:

Clock

Als weitere Option kann man die Audiosignale auch einfach wieder als I2S-Signal für einen externen DAC verwenden. Hierzu müssen die Audiosamples mit geeigneten Bit- und Wordclocks seriell ausgegeben werden:

I2S_Tx

 

Analoge Audioausgabe

Auch ein digitales System kann analoge Daten ausgeben, man muss nur ausreichend schnell den digitalen Ausgang ein- und ausschalten und kann damit ein entsprechendes Analogsignal emulieren. Bei LEDs nutzt man eine PWM (Puls-Weiten-Modulation), um die Helligkeit der LED einzustellen. Bei Audiosignalen bietet sich eher eine Puls-Dichte-Modulation (PDM = Pulse-Density-Modulation) an:

PDM
Quelle: Wikipedia

 

Bei diesem Verfahren können bei ausreichend hoher Samplerate des Digitalpins die Audiosamples direkt umgewandelt und seriell am Digitalpin ausgegeben werden. Um die beabsichtigten 48kHz Samplerate zu erreichen, müssen wir die Clockrate der PDM auf 4,8MHz stellen:

PDM-Clock
Quelle: Application Note InvenSense

 

Anschließend kann man ein einfaches Tiefpass-Filter verwenden, um aus dem seriellen Digitalsignal ein analoges Signal mit Line-Pegel zu erstellen:

LPF

 

Das resultierende Audiosignal ist überraschend gut und ist für alltägliche Aufgaben mehr als ausreichend. Eine Ausgabe einer MP3 auf Kopfhörer hätte mich nicht vermuten lassen, dass da lediglich ein Tiefpassfilter am Werke ist. Audiophile schlagen sicherlich jetzt gerade die Hände über dem Kopf zusammen.

 

Ausblick

Was hat dieses kleine Projekt nun gebracht? Nun, ich konnte mein Wissen über digitale Signalverarbeitung ein wenig vertiefen. Als ich vor 20 Jahren im Studium über Digitaltechnik etwas gehört habe, hatten wir nur die Grundlagen durchgenommen, nicht aber die Verarbeitung innerhalb von FPGAs. Das dürfte heute im Studium wieder anders aussehen.

Des Weiteren kann man diese Codebasis zur Erstellung eigener Mischpulte verwenden, um vor allem digitale Eingangssignale zu verarbeiten. Man kann mit diesem System tatsächlich mehrere Digitalsignale unabhängig voneinander einlesen, gemeinsam verarbeiten und wieder ausgeben. Und das mit einem kleinen Arduino-Board auf einem Steckbrett... also ich finde das klasse ;-)