Make Magazin 5/2017
S. 40
Digital-Analog-Wandler
Aufmacherbild

Wertewandel

In einigen unserer Make-Projekte wie der Uhr aus Messinstrumenten, der Musik mit dem Arduino oder der Laserharfe sind wir bereits auf einzelne Techniken zur Umwandlung digitaler Signale in analoge Spannungen eingegangen. Grund genug nun einmal in einem Übersichtsartikel alle gängigen Verfahren zur Digital-Analog-Wandlung zusammenzufassen.

Viele moderne Mikrocontroller haben bereits Wandler integriert, um analoge Signale in digitale Werte umzusetzen. Auch der Arduino hat einen solchen Wandler eingebaut. Wie diese funktionieren wurde bereits in der Make 4/2015 ausführlich beschrieben. Sind analoge Signale jedoch erst einmal digitalisiert, so lassen sie sich nicht ohne Weiteres wieder zurück in analoge Spannungsverläufe umwandeln. Daür benötigt man Digital-Analog-Wandler/Umsetzer (DAW/DAU; engl. Digital Analog Converter, DAC).

PWM

Beim Arduino liest der Befehl analogRead(Port) ein analoges Signal zwischen 0 und einer Referenzspannung (meist den internen 5 V) von einem Analogport ein und wandelt es in einen digitalen Wert zwischen 0 und 1024 um (10 Bit). Prinzipiell könnte man nun annehmen, dass der Befehl analogWrite(Port, Wert) diesen Wert anschließend wieder als analoge Spannung über einen Pin ausgeben kann. Leider hat der Arduino (respektive der im Kern verwendete ATmega328) jedoch keine echte DA-Wandler-Funktion. Vielmehr erzeugt analogWrite ein PWM-Signal (PWM = Pulsweitenmodulation), aus dem sich durch Glättung ein analoger Spannungswert ableiten lässt.

Da alle Mikrocontroller letztlich nur mit 1 und 0, also Spannung ein und Spannung aus umgehen können, wird beim PWM ein digitaler Wert zwischen 0 und 255 in eine schnelle Folge von HIGH- und LOW-Zuständen umgesetzt. Das Verhältnis zwischen den An- und Auszeiten wird als Pulsweite oder Pulsdauer bezeichnet und entscheidet über die resultierende Spannung. Hier muss man zwischen dem Gleichrichtwert (beziehungsweise dem arithmetischen Mittel) unterscheiden, der sich aus tan/T berechnet, sowie dem Effektivwert, der sich aus der Quadratwurzel der Verhältnisse berechnet. Er ist in der Praxis für die Leistungsberechnungen wichtig.

Unten das Oszilloskop-Bild einer analogen Sinus(halb)welle; oben das gleiche Signal als PWM
PWM-Frequenzen von einem Arduino Uno

Mit PWM lässt sich über einen Transistor oder MOSFET problemlos die Leistung von Gleichstrommotoren oder die Helligkeit von Lampen steuern. Das funktioniert dort besonders gut, weil die Verbraucher das gepulste Signal selbst durch ihre elektrischen Eigenschaften (Induktivität etc.) glätten.

Bei einem echten DA-Wandler liegt der analoge Wert konstant am Ausgang an, man muss nicht mehr glätten. Die schnellen An- und Ausimpulse haben bei der Steuerung von Gleichstrommotoren allerdings einen entscheidenden Vorteil: Bei kleinen Impulsbreiten bekommt der Motor kurze Impulse mit der vollen Spannung. Dadurch lässt sich die Geschwindigkeit wesentlich leichter und genauer regeln, als wenn er nur eine dauerhafte, dafür aber kleine Spannung erhält. Dies ermöglicht etwa bei einer Modelleisenbahn das langsame Anfahren der Züge oder Schleichfahrten. Auch die Motoren von Elektroautos werden oft mit Pulsweitenmodulation angesteuert.

Die in einem Arduino Uno oder Nano integrierte PWM arbeitet standardmäßig mit einer Frequenz von 490 Hz an den Pins 3, 9, 10, 11 und 976 Hz an den Pins 5, 6. Für die Steuerung eines flinken Galvo-Scanners wie er in der Laserharfe in Make 1/17 zu finden ist, ist diese Frequenz jedoch zu niedrig. Der Spiegel würde dann immer noch versuchen, den schnellen An- und Ausimpulsen zu folgen, was in unschönen Zuckungen endet.

PWM-Tiefbandfilter.ino

 1 int PWMPin = 5;    
 2 int pause=64;  // Die Pause ist erforderlich, :
   .damit der Tiefbandfilter funktioniert
 3 
 4 void setup() 
 5 {
 6   pinMode(PWMPin, OUTPUT);
 7 
 8   // Hier kann die PWM Frequenz angepasst werden
 9   // 0x01; ==> 62500 Hz
10   // 0x03; ==> 976 Hz (default)
11   // 0x05; ==> 61 Hz
12   TCCR0B = TCCR0B & 0b11111000 | 0x01; 
13 }
14 
15 
16 void loop() { 
17   for (int i = 0 ; i < 256 ; i++)
18   {
19     analogWrite(PWMPin, i);
20     delay(pause);
21   }
22 }

Die Erhöhung der PWM-Frequenz ist erfreulicherweise beim Arduino problemlos möglich. Letztlich genügt im Arduinosketch folgende Code-Zeile, um die internen Timer und Counter neu zu konfigurieren:

TCCRnB = TCCRnB & 0b11111000 | Wert; 

Dabei müssen die beiden Ziffern n gegen die Nummer des Timers und Wert gegen den Wert aus der Tabelle ausgetauscht werden. Ein Beispiel zeigt Listing PWM-Tiefbandfilter.ino.

Dabei gilt es jedoch zu beachten, dass bei einer Änderung der PWM-Frequenz an den Pins 5 und 6 auch andere Zeitfunktionen wie die Befehle delay(), millis() und micros() in Mitleidenschaft gezogen werden. Doch das lässt sich meist kompensieren: Bei einer Erhöhung der PWM-Frequenz um den Faktor 8 wartet der Befehl delay (8000) wieder genau eine Sekunde.

Tiefpassfilter

Trotz Erhöhung der Frequenz haben wir aber immer noch kein richtiges analoges Signal. Um das Signal zu glätten, bedient man sich einer sehr einfachen Schaltung aus einem Kondensator und einem Widerstand. Dieses sogenannte RC-Glied kann man auf zweierlei Wegen betrachen, einmal als Tiefpassfilter, einmal als Integrierglied.

Ein Tiefpassfilter erster Ordnung

Der Tiefpassfilter sperrt Frequenzanteile oberhalb einer Grenzfrequenz, also die schnelle PWM-Frequenz. Frequenzen unterhalb der Grenzfrequenz können annähernd ungehindert passieren. Daraus ergibt sich eine Spannung. Betrachtet man das RC-Glied als Integrierglied, so integriert es die Spannung über die Zeit, während der Puls High ist. Wir betrachten das RC-Glied im folgenden als Tiefpassfilter.

Die Dimensionierung der beiden Bauteile ist jedoch nicht ganz einfach und immer auch von dem benötigten Ergebnis abhängig. Die Formel zur Ermittlung der Grenzfrequenz lautet:

Mal angenommen, wir verwenden einen Widerstand von 100 KOhm und einen Kondensator mit 22 uF, so erhalten wir eine Grenzfrequenz von 72 Hz.

Beim Blick aufs Oszilloskop lässt sich nun schon eher ein analoger Spannungsverlauf erkennen. Es bleibt jedoch immer noch eine deutlich zu erkennende Schwankung von circa 150 mV, die sogenannte Rippelspannung.

Vergrößert man den Kondensator auf 100 nf, so verkleinert sich zwar die Rippelspannung, jedoch wird bei Spannungsänderungen mehr Zeit für das Laden des Kondensators benötigt. Dies wird auf dem Oszilloskop durch den abgerundeten Verlauf der Flanken deutlich. Je höher die Kapazität des Kondensators gewählt wird, desto langsamer kann das System auf Spannungsänderungen reagieren.

Die Rippelspannungen in der vergrößerten Darstellung
Zweistufiger Tiefpassfilter

Besser ist da schon ein zweistufiger Tiefpassfilter. Dazu werden einfach zwei Tiefbandfilter in Reihe geschaltet. Bei einem zweistufigen Filter mit jeweils 100 KOhm und 10 nF ist bei immer noch relativ steilen Flanken kaum noch Restwelligkeit (Rippel) messbar.

Dies kann prinzipiell noch beliebig gesteigert werden, indem weitere Filter in Reihe geschaltet werden. Idealerweise würde man dann Rn+1 immer etwas größer dimensionieren als Rn.

Steile Flanken und kaum eine Rippelspannung erkennbar

Im Internet gibt es unter http://www.electronicdeveloper.de/FilterPassivTiefpassRCPWM1.aspx ein recht gutes Tool zur Berechnung von Tiefpassfiltern.

Bei allen Filtern gilt es jedoch zu beachten, dass sie nur sehr schwach belastet werden können, soll heißen, es darf kein großer Strom aus ihnen herausfließen, um etwas anzusteuern. Abhilfe schafft ein sogenannter Spannungsfolger, der den Filter von weiteren Stufen entkoppelt. Dazu eignen sich Operationsverstärker wie der nur wenige Cent teure LM358.

Alle drei hier abgebildeten Verstärkerschaltungen lassen sich selbstverständlich auch in Verbindung mit allen weiteren DA-Wandlern einsetzen.

Die Erzeugung einer analogen Spannung aus einem kontinuierlichem PWM-Signal ist etwas trickreich und mit Fallstricken verbunden, aber hardwaretechnisch leicht und damit für Hersteller von Mikrocoontrollern günstig zu implementieren. Es gibt aber noch andere Lösungen, wie wir im folgenden zeigen.

Noch flexibler ist man mit der folgenden Schaltung, die beide Kanäle des LM 358 ausnutzt. Hier lassen sich Spannungen von –16 V bis +16 V ausgeben. An den beiden Potentiometern lassen sich die Verstärkung (Gain) und die Lage (Offset) der Spannung einstellen.
Achtung: Wird die hier gezeigte Schaltung an 5 V betrieben, so liegen am Ausgang maximal ca. 3,7 V an. Es lässt sich somit nur der analogWrite-Wertebereich von 0 bis 190 nutzen. Will man den kompletten Spannungsbereich zwischen 0 V und 5 V ausgeben, so muss der LM358 mit mindesten 6 V VCC versorgt werden.
Spendiert man der obigen Schaltung noch einen Potentiometer, so lassen sich Ausgangsspannungen zwischen 1 V bis zu 32 V realisieren.

R2R-Netzwerk

Findige Köpfe haben sich das Ohmsche Gesetz und die Mathematik zunutze gemacht und einzelne Spannungsteiler so miteinander vernetzt, dass sich prinzipiell DA-Wandler mit beliebig vielen digitalen Eingängen konstruieren lassen, an deren Ausgang ein analoges Signal herauskommt.

Solche Wandler heißen R2R-Netzwerke und benötigen pro Eingang nur zwei Widerstände, wobei der Wert des einen Widerstands immer exakt doppelt so groß ist, wie der des anderen, also beispielsweise R = 10 KOhm und 2R = 20 KOhm. Aufgrund der markanten Form des R2R-Netzwerks in Schaltplänen wird es auch als R2R-Leiter bezeichnet.

Die Eingänge des R2R-Netzwerks repräsentieren die Bits einer Binärzahl und bei N Eingangsbits berechnet sich die Ausgangsspannung A für die Binärzahl x nach der Formel A = 5 V * x / 2 N. Bei acht Eingangsbits gilt also A = 5 V * x / 256. Für x = 0 ergeben sich 0 V und x = 255 führt zu 4,98 V, was im Toleranzbereich liegt.

Auf dem Bild wurde ein 8-Bit-R2R-Netzwerk an den PortD eines Arduino Nano angeordnet. Achtung: Beim Arduino Nano ist Pin0 der TX-Pin und Pin 1 der RX-Pin. Aus diesem Grund kreuzen sich die beiden Widerstände auf dem Bild. Die grüne Leitung (rechts unten) ist der analoge Ausgang.

R2R-Netzwerk.ino

 1 void setup() 
 2 {
 3  // Alle Pins am PortD als Ausgang definieren
 4  DDRD = B11111111;    
 5 }
 6 
 7 void loop() {
 8  for (int i = 0 ; i < 256 ; i++ )
 9  { 
10   PORTD = i;
11  }
12 }
Zwei DA-Kanäle mit je 12 Bit Auflösung an einem Arduino Mega. Es waren dafür immerhin 48 Widerstände nötig.

Wird PD7 am Arduino auf HIGH gesetzt (siehe Bild oben), also auf 5 V geschaltet, so liegen am Ausgang des R2R-Netzwerks 2,5 V an, und so weiter.

Wird PD6 auf HIGH geschaltet, so liegen 1,25 V an. Werden PD7 und PD6 auf HIGH geschaltet liegt die Summe der beiden Spannungen also 3,75 V an.

Für einen DA-Wandler mit 8 Bit Auflösung werden insgesamt 16 Widerstände benötigt. Aus diesem Grund wird ein R2R-Netzwerk oft auch als Widerstandsgrab bezeichnet.

Werden die Widerstände an einen Port des Mikrocontrollers angeordnet, so genügt ein einziger Befehl an diesen Port, um alle 8 Pins gleichzeitig zu schalten und damit die Spannung zu generieren.

Um nun eine Spannung zu generieren, genügt im Arduino-Sketch (siehe Listing PWM-Tiefbandfilter.ino) der Befehl PORTD = Wert; (z. B. PORTD = 125). Die Spannung ergibt sich dann aus der Formel Spannung = 5 V * Wert/255. Vorher müssen im Setup-Bereich alle Pins am PortD als Ausgang definiert werden: DDRD = B11111111;. DDR steht für Data Direction Register und bezeichnet ein Konfigurationsregister des im Arduino verwendeten Mikrocontrollers. Im Sketch ist sie zugleich eine bereits in den internen Bibliotheken reservierte Konstante.

Bei der Auswahl der Widerstände ist es besonders bei den höherwertigen Bits sehr wichtig, auf eine möglichst kleine Toleranz zu achten. Brauchbare Ergebnisse erreicht man mit Metallfilmwiderständen ab einer Toleranz von 1 % oder weniger.

Auf diese Weise lassen sich DA-Konverter mit nahezu jeder beliebigen Auflösung herstellen. In der Regel setzt aber die Toleranz der Widerstände und das zunehmende Quantisierungsrauschen der Auflösung ein Ende. Aber probieren Sie es selbst aus!

Der TLC7524 wird parallel mit Daten versorgt. An der grünen Leitung rechts unten im Bild liegt das analoge Ausgangssignal an.

MCP-4921-Hardware-SPI.ino

 1 #define SLAVESELECT 10 // CS
 2 #define DATAOUT     11 // DIN
 3 #define SPICLOCK    13 // SCLK 
 4 
 5 
 6 void setup() {
 7   pinMode(DATAOUT, OUTPUT);
 8   pinMode(SPICLOCK,OUTPUT);
 9   pinMode(SLAVESELECT,OUTPUT);
10   setup_hardware_spi();
11 }
12 
13 void loop() { 
14   for (int i = 0 ; i < 4096 ; :     
    .i += 16)
15   {
16     SetVoltage(i);
17   }
18 }

DAC-ICs

Wem der Aufwand mit den vielen Widerständen zu viel ist, kann aus einem reichhaltigem Angebot an DAC-ICs auswählen.

Im Inneren von DAC-ICs verbirgt sich wiederum nichts anderes als ein R2R-Netzwerk, das allerdings hochpräzise, lasergetrimmte Widerstände (durch laserstrahlinduzierte Materialveränderungen) aufweist. Je nach Wunsch gibt es ICs mit 8 Bit, 10 Bit oder 12 Bit Auflösung, sowie mit einem oder mehreren Kanälen. Ebenso enthalten die meisten ICs bereits einen Operationsverstärker.

Der entscheidende Unterschied bei modernen ICs liegt in der Ansteuerung der ICs: Je nach Ausführung erfolgt diese entweder parallel über eine Schaltlogik, seriell über integrierte Schieberegister oder über serielle Busse wie I2C oder SPI.

Der 2-Kanal-12-Bit-DAC MCP4922
Der DAC MCP4921 an einem Arduino Nano. Er besitzt einen DAC-Kanal mit einer Auflösung von 12 Bit. Die Ansteuerung auf dem Bild erfolgt per Hardware SPI. Die gelbe Leitung rechts unten im Bild ist der analoge Ausgang.

SPI.ino

 1 void setup_hardware_spi(void)
 2 {
 3      byte clr;
 4   digitalWrite(SLAVESELECT,HIGH); //disable device
 5   SPCR = (1<SPE)|(1<MSTR)|(1<CPHA);
 6   clr=SPSR;
 7   clr=SPDR;
 8   SPSR |= _BV(SPI2X);               // Letztes Byte zuerst senden 
 9 }
10 
11 char spi_transfer(volatile char data)
12 {
13   SPDR = data; // Start the transmission
14   while (!(SPSR & (1<SPIF))) {};// Wait the end of :
     .the transmission
15 }
16 
17 void SetVoltage(short Voltage)
18 {
19   Voltage = 24576 + 2*Voltage ;  //24576 = '0110000000000000'
20   digitalWrite(SLAVESELECT,LOW);
21     spi_transfer( highByte(Voltage) );
22     spi_transfer( lowByte(Voltage) );
23   digitalWrite(SLAVESELECT,HIGH);
24 }

Parallel angesteuerte ICs wie der TLC7524 oder der DAC0800 punkten durch ein sehr genaues R2R-Netzwerk und sehr hohe Geschwindigkeit. Je nachdem wie sie beschaltet werden, können sie sowohl zur Spannungs- als auch zur Stromsteuerung eingesetzt werden. Sie belegen jedoch immer noch sehr viele Pins des Mikrocontrollers respektive des Arduinos .

Serielle ICs wie der MCP4921 oder der TLV5618 benötigen hingegen nur noch 3 Pins (SPI), der über I2C angesteuerte PCF8591 sogar nur 2 Pins.

Damit ein digitaler Wert in ein analoges Signal umgewandelt wird, erwarten die meisten seriellen ICs die Übermittlung von 2 Bytes, die den digitalen Stellwert und das Kommando für den Chip enthalten.

Beim MCP4922 teilen sich die 2 Bytes in 12 Bits für den Wert und 4 Bits für Chip-Anweisungen auf. Dabei entscheidet Bit 15 (A/B), welcher DAC-Kanal (Ausgang) den Wert ausgeben soll, Bit 14 (BUF) ob der Wert in den Zwischenspeicher geschrieben werden soll, Bit 13 (Output Gain Selection), ob die volle oder die halbe Spannung als Referenzspannung dient und Bit 12 (SHDN), ob der Ausgang nun aktiv oder inaktiv sein soll.

Zur Übertragung wird zuerst der CS-Pin auf LOW gesetzt (CS, Chip Select; schaltet den IC quasi an), anschließend werden die 16 Bit seriell von links nach rechts (MSBFIRST) übertragen und anschließend der CS-Pin wieder auf HIGH gesetzt. Listing MCP4921-ShiftOut.ino zeigt, wie das funktioniert. Zum Übersetzen muss man zusätzlich SPI.ino in die Arduino-IDE laden, da dort weitere Funktionen definiert sind. Alle Codebeispiele finden Sie übrigens unter den Downloads zu diesem Artikel.

Der Vorteil von DAC-ICs liegt in ihrer hohen Genauigkeit und dies sogar bei einer Auflösung von 12 Bit (Wertebereich von 0 bis 4096). Außerdem belegen DAC-Chips lediglich 3 Mikrocontrollerpins, I2C Konverter sogar nur 2 Pins.

Zur Ansteuerung muss jedoch ein etwas höherer Programmieraufwand betrieben werden. Durch die bitweise serielle Datenübertragung ist die Geschwindigkeit jedoch um ein Vielfaches langsamer als bei einem reinen R2R-Netzwerk.

Tabelle: Geschwindigkeitsvergleich DAC
Ein SMD vom Type PCF8591, der vier Analog-zu-digital-Kanäle und einen Digital-zu-analog-Kanal besitzt. Angesteuert wird er über das I2C-Protokoll. Ein Beispielprogramm ist in Listing PCF8591.ino zu finden. Zum Experimentieren sind auf diesem Bord bereits ein lichtempfindlicher Widerstand (LDR), ein temperaturabhängiger Widerstand (PTC) und ein Potentiometer aufgelötet. Wir kauften es bei Ebay für 3,50 Euro.

MCP4922-ShiftOut.ino

 1 #define SLAVESELECT 9 // CS
 2 uint8_t dataPin =   8; // DIN
 3 #define clockPin    7 // SCLK 
 4 
 5 #define pause 5
 6 
 7 
 8 void setup() {
 9   pinMode(dataPin, OUTPUT);
10   pinMode(clockPin,OUTPUT);
11   pinMode(SLAVESELECT,OUTPUT);
12 }
13 
14 void loop() { 
15   for (int i = 0 ; i < 4096 ; i += 16)
16   {
17     SetVoltage(i);
18   }
19 }
20 
21 void SetVoltage(short Voltage)
22 {
23   Voltage = 28672 + Voltage ;  //24576 = '0110000000000000'
24   digitalWrite(SLAVESELECT,LOW);
25     shiftOut(dataPin, clockPin, MSBFIRST, (Voltage > 8)); 
26     shiftOut(dataPin, clockPin, MSBFIRST, Voltage); 
27 
28   digitalWrite(SLAVESELECT,HIGH);
29 }
Der DAC MCP4922 an einem Arduino Nano. Er besitzt 2 DAC-Kanäle mit je 12 Bit Auflösung. Die Ansteuerung auf dem Bild erfolgt per ShiftOut. In Listing MCP4922-ShiftOut.ino ist die grundsätzliche Funktion zu sehen. An der grünen und der gelben Leitung (im Bild unten) liegt jeweils ein analoges Signal an.

Geschwindigkeitsvergleich

Um eine Aussage über die Geschwindigkeiten der einzelnen DAC-Methoden treffen zu können, ließen wir jeweils eine Sägezahnkurve mit jeweils 256 Einzelwerten generieren.

PCF8591-I2C.ino

 1 #include "Wire.h"
 2 #define PCF8591 (0x90 > 1) // I2C bus address
 3 
 4 void setup()
 5 {
 6  Wire.begin();
 7 }
 8 
 9 void loop()
10 {
11  for (int i = 0 ; i < 256 ; i++)
12  {
13   Wire.beginTransmission(PCF8591); // PCF8591 aufwecken
14   Wire.write(0x40);                // DAC einschalten (binär 1000000)
15   Wire.write(i);                   // Wert senden
16   Wire.endTransmission();          // end tranmission
17  }
18 }

Klarer Spitzenreiter war dabei das R2R-Netzwerk mit einer Frequenz von 7800 Hz (mit den Arduino-Lib). Auch der TLC7524 erreichte durch die parallele Ansteuerung diesen Spitzenwert. Beim MCP4921 hängt die Geschwindigkeit vom Tempo der Datenübertragung ab. Per Arduino-Hardware SPI erreichte er immerhin noch 181 Hz. Der MCP4921 mit ShiftOut angesteuert und der PCF8591 mit I2C lagen dicht beieinander bei rund 13 Hz. Das PWM-Modell in Kombination mit einem zweistufigen Tiefbandfilter mussten wir bis auf 3,8 Hz drosseln, um noch einen halbwegs steilen Sägezahn zu bekommen.

Den PCF8591 gibt es auch einzeln in einem DIP-16-Gehäuse.
Der PCF8591P wird mit I2C angesteuert. Die gelbe Leitung rechts unten ist der analoge Ausgang.

Fazit

PWM ist bei Aufgaben wie der Helligkeitsregelung von Lampen oder der Geschwindigkeitssteuerung von Motoren das Mittel der Wahl. An Geschwindigkeit ist ein R2R-Netzwerk nicht zu übertreffen, insbesondere wenn man die Port-Register direkt programmiert, belegt aber zahlreiche Portleitungen. DAC ICs punkten bei der Auflösung und Genauigkeit des analogen Signals. dab