DDS mit dem Arduino Due

Microcontroller eignen sich zwar nicht besonders gut für wirklich schnelle digitale Signalverarbeitung, d.h. für Sampling-Frequenzen oberhalb einiger 100 kHz, aber sie sind leichter und einfacher und vor allem wesentlich schneller zu programmieren als FPGAs.

Wenn es also nicht um Geschwindigkeit geht, sondern um das Ausprobieren und Herumspielen mit dem DDS-Verfahren und seinen Eigenschaften, dann ist ein FPGA unnötig und eher hinderlich dafür und man wendet sich lieber einem Microcontroller zu.

Um einen NCO (numerically controlled oszillator) nach dem DDS-Verfahren für Frequenzen im Audiobereich zu bauen, sind die Voraussetzungen auch nicht allzu groß. Man braucht:

  • Einen Microcontroller, möglichst 32 bit, damit die eigentlichen DDS-Operationen nur wenige Prozessorzyklen brauchen
  • Einen DAC, extern oder intern, aber am einfachsten ist eine interne Version, also wenn der Microcontroller einen DAC eingebaut hat
  • Eine Programmierumgebung, die unkompliziert zu bedienen ist

Die Arduino-Programmierumgebung in Kombination mit dem Arduino DUE erfüllen diese Voraussetzungen, zumindest hardwareseitig. Auch der Zero und die MKR-Familie haben eingebaute DACs, aber den Arduino DUE hatte ich zuhause, die anderen nicht. Auf dem DUE ist ein Microcontroller von Microchip (SAM3X8E, früher Atmel) eingebaut, in dem ein ARM-Core Cortex-M3 mit einer Taktfrequenz von 84 MHz läuft, der einen 2-Kanal-DAC mit 12 bit Auflösung als Peripherie hat.

So weit super.

Bei der Programmierumgebung schaut es nicht ganz so toll aus:

  • Die Arduino-Umgebung ist zwar einfach zu bedienen und einfache Dinge sind auch einfach umzusetzen, aber wenn es nur ein klein bisschen komplizierter wird, dann muss man entweder Glück haben und eine passende Bibliothek finden, die jemand programmiert hat, oder Prozessor- und Peripherie-Register direkt programmieren, was eigentlich der Arduino-Philosophie (Code-Portabilität!) widerspricht
  • Mit Arduino-Bordmitteln ist es anscheinend nicht möglich, Timer-Interrupts oder Timer-Events des SAM3X8E zu benutzen, und schon gar nicht DMA-Funktionen
  • Es ist in der Arduino-Welt anscheinend auch nicht vorgesehen, Interrupt-Prioritäten zu verändern. Das kommt wahrscheinlich daher, dass der Prozessor auf dem Arduino UNO, also quasi auf dem ‚ursprünglichen‘ Arduino, gar keine veränderlichen Interrupt-Prioritäten hardwareseitig vorgesehen hat. Dort ist die Priorität durch die Adresse des Interrupt-Vektors festgelegt und damit unveränderbar

Wozu würde man die obigen Anforderungen überhaupt brauchen?

Die Antwort dazu ist, dass man sicherstellen muss, dass die digitalen Werte, die man an den DAC schickt, zeitlich möglichst äquidistant als Spannungen oder Stromwerte ausgegeben werden.

Wenn der eine oder andere DAC-Wert etwas später oder früher als der vorige Wert ausgegeben wird, dann führt das zu Jitter im Ausgangssignal.

Dieser Jitter im Ausgangssignal führt im Ausgangsspektrum des DACs zu einem unter Umständen eklatant erhöhten Phasenrauschen, das durch das Rekonstruktionsfilter nicht mehr herausgefiltert werden kann, weil es – je nach genauer Zeitstruktur des Jitters – typischerweise das Ausgangsspektrum sehr nah an der gewünschten Ausgangsfrequenz (‚Träger‘ genannt) beeinflusst.

So was will man meistens überhaupt nicht haben!

Die Entwickler des Cortex-M3 haben deswegen dafür gesorgt, dass es exakt 12 Prozessortakte braucht bis zur ersten Instruktion in einer Interrupt-Service-Routine, unter der Voraussetzung, dass der Prozessor nicht gerade einen höher priorisierten Interrupt abarbeitet. (Martin, T: The Designer’s Guide to the Cortex-M Processor Family, 2nd Edition)

Man muss also dafür sorgen, dass der Timer-Interrupt, der benutzt wird, um einen DAC-Wert auszugeben, eine möglichst hohe Priorität hat und nicht durch andere Interrupts unterbrochen werden kann.

Alternativ dazu könnte man den DAC mit einem Hardware-Triggersignal takten, das von einem Timer-Ausgang erzeugt wird und damit praktisch nahezu jitterfrei wäre. Der DAC besäße dafür den Eingang DATRG/PA10.

Aber, wie gesagt, Funktionen für solche Betriebsmodi habe ich nirgends gefunden und Register-Programmierung ist mir für dieses Projekt zu aufwändig.

Vielleicht später mal.

Den Code zu diesem Projekt findet man hier:

https://github.com/papamidas/DDS_due_simple_DM1CR

Die Datei für das Hauptprogramm heißt „DDS_due_simple_DM1CR.ino“:

In der Setup()-Funktion wird zunächst eine Sinus-Tabelle angelegt, dann wird die serielle Schnittstelle initialisiert und dann wird in den letzten 4 Zeilen (48-51) eine Interrupt-Funktion an den Timer3 angehängt und der Timer wird gestartet:

void setup() {
  analogWriteResolution(12);  // set the analog output resolution to 12 bit (4096 levels)
  for (int i = 0; i < pow(2, DDS_SINETABLELEN_BITS); i++) {
    dds_sinetable[i] = sin(2.0 * M_PI * i/pow(2, DDS_SINETABLELEN_BITS));
  }
  // initialize serial:
  Serial.begin(115200);
  // reserve 200 bytes for the inputString:
  inputString.reserve(200);
  Serial.print("Enter Freq: ");
  Timer3.attachInterrupt(timerHandler);
  Timer3.start(DDS_WANTEDSAMPLEPERIOD_US);   // desired: INT every DDS_WANTEDSAMPLEPERIOD_US us
  dds_samplePeriod_us = Timer3.getPeriod();  // actual time between INTs
  dds_sampleFreq_Hz = Timer3.getFrequency(); // actual INT frequency
}

Jedes Mal nach Ablauf des Timers, in Mikrosekunden ausgedrückt durch die Variable dds_samplePeriod_us, wird ein Timer-Interrupt ausgelöst und der Prozessor ruft diese Unterroutine auf:

void timerHandler(){
  dds_phase += dds_phaseinc;
  dds_out = dds_amplitude_adcsteps * dds_sinetable[dds_phase >> (32-DDS_SINETABLELEN_BITS)] + dds_offset_adcsteps;
  analogWrite(DAC1, dds_out); // write next sample to DAC
}

Das ist alles!

In der ersten Zeile dieser Funktion (139) wird der Wert des Phasenregisters um den Phaseninkrement-Wert erhöht.

In der zweiten Zeile (140) werden die obersten 14 Bit des Phasenwerts herausgefischt und als Index für die Sinustabelle verwendet. Der Sinuswert wird mit der Amplitude dds_amplitude_adcsteps multipliziert und der Offset-Wert dds_offset_adcsteps wird addiert.

In der dritten Zeile (141) wird der DAC-Wert zum DAC geschickt.
Mehr ist es nicht!

Der Rest des Codes ist Beiwerk und dient zum Eingeben und Verändern von Variablen.

Zu einem DDS gehört auch ein Tiefpassfilter dazu, der idealerweise dafür sorgt, dass Signale oberhalb der halben Sampling-Frequenz völlig unterdrückt werden.

So was gibt es natürlich nicht und man muss einen sinnvollen Kompromiss finden.

Aus diesem Grund habe ich mich für einen 2-stufigen Sallen-Key-Filter entschieden, der nur ein paar Bauteile benötigt, die ich zudem alle zuhause hatte.

Den Filter kann man auf einem einfachen Steckbrett aufbauen, siehe Foto:

Nach einigen Anfangsschwierigkeiten mit einem geschlachteten DAC-Ausgang und einem LM358 mit seiner altertümlichen Ausgangsstufe bin ich dann bei diesem Design (unterer Teil des Schaltplans mit U3 und U4) gelandet:

Mit dem ADALM2000, einem USB-Gadget von Analog Devices, das ich als Oszilloskop, Spektrumanalysator und Logikanalysator benutzen kann, kann ich auch Übertragungsfunktionen messen.

Wie man im folgenden Diagramm der gemessenen Übertragungsfunktion sieht, macht der Filter genau das, was erwartet wird (oberhalb von 30 kHz ist die Übertragungsfunktion bereits -60 dB; darüber ist das Eingangssignal des ADCs im ADALM2000 so klein, dass die Messwerte unbrauchbar werden):

Als Nächstes kann man jetzt das Filter an den DAC-Ausgang anschließen und mit einem 2-Kanal-Oszilloskop gleichzeitig den DAC-Ausgang und den Filter-Ausgang beobachten.

In meinem Fall mache ich das natürlich wieder mit meinem ADALM2000:

ADALM2000, 5-kHz-Filter auf der Steckplatine und Arduino DUE

Im Arduino Serial Monitor kann man die gewünschte Ausgangsfrequenz, die Samplingrate, Ausgangsamplitude und -offset einstellen:

Arduino Serial Monitor

Wenn man die Ausgangsfrequenz auf 4000 Hz und die Sampling-Frequenz auf 20000 Hz einstellt, dann erhält man folgendes Oszilloskopbild:

Ausgangsfrequenz 4 kHz, Samplingfrequenz 20 kHz

Was zunächst auffällt ist, dass der Ausgangsspannungsbereich des DACs nur von etwa 0,55V bis 2,55V reicht, obwohl der DAC voll ausgesteuert wird.
Das ist aber leider richtig so; der Ausgangsspannungsbereich des DACs reicht laut Datenblatt (Seite 1412) https://www.microchip.com/wwwproducts/en/ATSAM3X8E
tatsächlich nur von 1/6 * Vref bis zu 5/6 * Vref und das ist 1/6*3.3 V = 0,55 V bzw. 5/6*3.3 V = 2,75 V, und das sind auch nur MIN- und MAX-Angaben!

Was man außerdem sieht, ist die Phasenverzögerung des Filters, die grob geschätzt zwischen 90 und 180 Grad zu liegen scheint. Was man nicht sieht, ist, wie viel von den unerwünschten Störsignalen oberhalb der gewünschten Frequenz von 4 kHz übrig geblieben ist. Das Ausgangssignal des Filters schaut zwar ziemlich sinusförmig aus, aber das kann täuschen. Im Zeitbereich kann man das nur schwer beurteilen.

Das nächste Diagramm zeigt daher das DAC-Signal im Frequenzbereich und das übernächste Diagramm den Filter-Ausgang:

DAC-Ausgang, Ausgangsfrequenz 4 kHz, Samplingfrequenz 20 kHz
Filter-Ausgang, Ausgangsfrequenz 4 kHz, Samplingfrequenz 20 kHz

Man sieht, dass der Filter die Alias-Frequenz bei 16 kHz um mehr als 40 db unterdrückt. Das passt auch zur vorher gemessenen Übertragungsfunktion.
Höherfrequente Störungen sind auch kaum zu sehen. Die stärksten Störer liegen bei 8 kHz und (20-8) kHz und könnten die 1. Oberwelle der Ausgangsfrequenz sein.

Jetzt wähle ich die gleiche Ausgangsfrequenz von 4 kHz, aber eine Samplingfrequenz von nur 10 kHz. Die Ausgangsfrequenz liegt jetzt bereits bei 40% von der Samplingfrequenz. Wie sieht es dann aus?

Ausgangsfrequenz 4 kHz, Samplingfrequenz 10 kHz

Wie man bereits rein optisch merkt, hat das Ausgangssignal keine Ähnlichkeit mehr mit einer Sinusfunktion. Im Frequenzbereich kann man auch sofort sehen, warum das so ist:

DAC-Ausgang, Ausgangsfrequenz 4 kHz, Samplingfrequenz 10 kHz
Filter-Ausgang, Ausgangsfrequenz 4 kHz, Samplingfrequenz 10 kHz

Die Alias-Frequenz liegt jetzt bei (10-4) kHz = 6 kHz, und bei 6 kHz ist die Dämpfung des Filters nur so um die 5-6 dB.
Das Filter ist für dieses geringe Verhältnis zwischen Sampling-Frequenz und gewünschter Ausgangsfrequenz einfach nicht steilflankig genug!

Wegen dieser hohen Anforderungen an Rekonstruktionsfilter ist es deswegen meistens günstiger, die Samplingfrequenz zu erhöhen und bei einem einfach realisierbaren Filter zu bleiben.

Genau das können wir jetzt ausprobieren, indem wir die Samplingfrequenz auf 40 kHz einstellen.

Die Signale sehen dann so aus:

Ausgangsfrequenz 4 kHz, Samplingfrequenz 40 kHz

Wie man sofort sieht, wird die Form des Sinus jetzt durch mehr Samples angenähert und es ist zu erwarten, dass dadurch der Oberwellengehalt gegenüber einer Samplefrequenz von 20 kHz, wie das in der ersten Einstellung war, noch weiter sinkt.

Im Frequenzbereich kann man nachschauen, ob dem auch wirklich so ist:

Ja, es ist wirklich so, die Anzahl der Störer ist geringer geworden. Der höchste Störer liegt allerdings immer noch in etwa auf dem gleichen Niveau wie im ersten Beispiel. In der Praxis könnte man sich jetzt überlegen, doch nur mit 20 kHz zu samplen oder bei 40 kHz zu bleiben und das Filter noch etwas einfacher zu gestalten, weil es für 40 kHz eigentlich unnötig gut ist.

Was kann man mit dem Arduino-DDS noch machen?

  • Man könnte verifizieren, ob sich die Frequenzabhängigkeit der Amplitude des Ausgangssignals verhält wie sie soll, nämlich proportional zu sin(X)/X wobei X zu ersetzen ist durch Pi * (f_out/f_sample). Der Grund dafür ist, dass bei beliebigen Frequenzen, die nicht gerade zufällig ein gerader Teiler der Samplingfrequenz sind, bei jedem Zyklus ein anderer Phasenwert getroffen wird. Wenn die Ausgangsfrequenz der Samplingfrequenz nahe kommt, dann werden bei jedem Sampling-Zeitpunkt praktisch dieselben Phasenwerte ausgegeben und die Amplitude der Ausgangsspannung wird sehr klein. Exakt bei der Samplingfrequenz muss es daher eine Nullstelle der Amplitude geben
  • Man könnte Eigenschaften von amplitudenmodulierten Signalen beobachten, indem man den Wert dds_amplitude_adcsteps zeitlich ändert, zum Beispiel innerhalb eines zweiten Timer-Interrupts
  • Man könnte Eigenschaften von phasen- oder frequenzmodulierten Signalen beobachten, indem man den Wert des Phaseninkrement-Registers, d.h. der Variable dds_phase_inc zeitlich ändert, zum Beispiel innerhalb eines zweiten Timer-Interrupts
  • Man könnte mit ein paar zusätzlichen Programmzeilen einen Zweiton-Generator draus machen, wie man ihn zum Testen von Verstärkern verwendet
  • Man könnte mit ein paar zusätzlichen Programmzeilen auch einen Audio-Sweep-Generator draus machen
  • Man könnte den zweiten DAC in Betrieb nehmen und damit IQ-Signale oder Stereo-Signale erzeugen

Viel Spaß beim Ausprobieren!

73 de Christian DM1CR

Kommentare?

One Reply to “DDS mit dem Arduino Due”

Schreibe einen Kommentar

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