Pfad: Home => AVR-Überblick => R/2R-DAC
DAC8 Tutorial zum Erlernen der AVR Assembler-Sprache von
AVR-Einchip-Prozessoren AT90Sxxxx
von ATMEL anhand praktischer Beispiele.

Einfacher 8-Bit-Digital-zu-Analog-Wandler mit einem R/2R-Netzwerk

Zweck

Die Umwandlung von digitalen Werten in eine Analogspannung kann durch integrierte Schaltungen bewerkstelligt werden. Eine billigere und weniger anspruchsvolle Lösung ist ein selbstgebautes R/2R-Widerstandsnetzwerk, gefolgt von einem Operationsverstärker.

R/2R-NetzwerkEin R/2R-Netzwerk ist wie im Bild gezeigt aus Widerständen aufgebaut. Die einzelnen Eingangsbits liegen entweder auf Null Volt oder auf der Betriebsspannung und speisen über doppelt so große Widerstände ein wie der vertikale Teil des Netzwerks. Jedes Bit trägt so seinen spezifischen Teil zur resultierenden Ausgangsspannung bei. Das funktioniert wirklich, und ziemlich gut! Kommerzielle Digital-Analog-Wandler haben solche R/2R-Netzwerke im IC integriert.

Der Ausgang eines AVR liefert nicht sehr viel Strom an seinen Portausgängen, wenn die Spannungen in der Nähe der Versorgungsspannungen bleiben sollen. Daher sollten die Widerstände des R/2R-Netzwerks größer als einige 10 Kiloohm sein. Um das Netzwerk möglichst gering zu belasten entkoppelt ein Operationsverstärker das Netzwerk vom weiteren Verbraucher.

Die Widerstandswerte sollten so genau eingehalten werden wie es vom gesamten Netzwerk erwartet wird. Abweichungen von Widerstandswerten sind besonders bei den höherwertigen Bits relevant. Die folgende Tabelle zeigt einige Beispiele für die schrittweise Spannungssteigerung eines R/2R-Netzwerks mit einer 51k/100k-Kombination. (Die Berechnungen wurden mit einem Free-Pascal-Programm durchgeführt, der freie Quellcode kann hier gedowngeloaded) werden.


R2R-Netzwerk Berechnungsprogramm, (C)2004 info !at! avr-asm-tutorial.net
------------------------------------------------------------------------

Bits Aufloesung: nr=8[bits], Bits=00000000
Spannungen: ub=5.000[V], ul=0.000[V], uh=5.000[V]
Widerstaende: R1= 51k0, R2=100k0

Eingabekombinationen und Ausgabespannungen
00000000: 0.000[V]
00000001: 0.019[V] (Delta=   18.69[mV])
00000010: 0.038[V] (Delta=   19.06[mV])
00000011: 0.056[V] (Delta=   18.69[mV])
00000100: 0.076[V] (Delta=   19.62[mV])
00000101: 0.095[V] (Delta=   18.69[mV])
00000110: 0.114[V] (Delta=   19.06[mV])
00000111: 0.132[V] (Delta=   18.69[mV])
00001000: 0.153[V] (Delta=   20.67[mV])
00001001: 0.172[V] (Delta=   18.69[mV])
00001010: 0.191[V] (Delta=   19.06[mV])
00001011: 0.210[V] (Delta=   18.69[mV])
00001100: 0.229[V] (Delta=   19.62[mV])
00001101: 0.248[V] (Delta=   18.69[mV])
00001110: 0.267[V] (Delta=   19.06[mV])
00001111: 0.286[V] (Delta=   18.69[mV])
00010000: 0.308[V] (Delta=   22.72[mV])
00010001: 0.327[V] (Delta=   18.69[mV])
00010010: 0.346[V] (Delta=   19.06[mV])
00010011: 0.365[V] (Delta=   18.69[mV])
00010100: 0.384[V] (Delta=   19.62[mV])
00010101: 0.403[V] (Delta=   18.69[mV])
00010110: 0.422[V] (Delta=   19.06[mV])
00010111: 0.441[V] (Delta=   18.69[mV])
00011000: 0.462[V] (Delta=   20.67[mV])
00011001: 0.480[V] (Delta=   18.69[mV])
00011010: 0.499[V] (Delta=   19.06[mV])
00011011: 0.518[V] (Delta=   18.69[mV])
00011100: 0.538[V] (Delta=   19.62[mV])
00011101: 0.556[V] (Delta=   18.69[mV])
00011110: 0.575[V] (Delta=   19.06[mV])
00011111: 0.594[V] (Delta=   18.69[mV])
00100000: 0.621[V] (Delta=   26.83[mV])
00100001: 0.640[V] (Delta=   18.69[mV])
00100010: 0.659[V] (Delta=   19.06[mV])
00100011: 0.677[V] (Delta=   18.69[mV])
00100100: 0.697[V] (Delta=   19.62[mV])
00100101: 0.716[V] (Delta=   18.69[mV])
00100110: 0.735[V] (Delta=   19.06[mV])
00100111: 0.753[V] (Delta=   18.69[mV])
00101000: 0.774[V] (Delta=   20.67[mV])
00101001: 0.793[V] (Delta=   18.69[mV])
00101010: 0.812[V] (Delta=   19.06[mV])
00101011: 0.830[V] (Delta=   18.69[mV])
00101100: 0.850[V] (Delta=   19.62[mV])
00101101: 0.869[V] (Delta=   18.69[mV])
00101110: 0.888[V] (Delta=   19.06[mV])
00101111: 0.906[V] (Delta=   18.69[mV])
00110000: 0.929[V] (Delta=   22.72[mV])
...
01111110: 2.446[V] (Delta=   19.06[mV])
01111111: 2.465[V] (Delta=   18.69[mV])
10000000: 2.517[V] (Delta=   51.72[mV])
10000001: 2.535[V] (Delta=   18.69[mV])
10000010: 2.554[V] (Delta=   19.06[mV])
10000011: 2.573[V] (Delta=   18.69[mV])
...

Man beachte den Sprung, wenn Bit 7 High wird! Die Spannung springt dann um mehr als zwei Digits. Das ist für ein 8-Bit-Netzwerk zu groß, aber akzeptabel für ein 4-Bit-Netzwerk.

Benötigte Hardware

Die Hardware ist einfach zu bauen, es ist ein wahres Widerstands-Grab.

Gepuffertes R/2R-Netzwerk

Der CA3140 ist ein Operationsverstärker mit einer FET-Eingangsstufe. Er arbeitet auch bei Eingangsspannungen in der Nähe der negativen Versorgungsspannung. Man kann auch einen 741 verwenden, aber das hat Konsequenzen (siehe unten).
Die Betriebsspannung von 5 Volt wird hier über den zehnpoligen Steckverbinder bezogen. Dieser passt direkt zu einem STK200- oder STK500-Entwicklungsboard. Es ist auch möglich, die Betriebsspannung des Operationsverstärkers aus einer externen Spannungsquelle zu beziehen. Das hat einige Vorteile, ist aber nicht zwingend.
Anstelle der Parallelschaltung zweier gleich großer Widerstände kann man natürlich auch ähnliche Paare verwenden, aber das verringert die Genauigkeit bei einem 8-Bit-Netzwerk immens (siehe oben).

Zum Anfang dieser Seite

Anwendung des R/2R-Netzwerks

Einen Sägezahn erzeugen

Das folgende Programm erzeugt eine Sägezahnspannung am R/2R-Netzwerk-Ausgang. Den Quellcode gibt es zum Download here.


; *************************************************************
; * R/2R-Netzwerk erzeugt eine Saegezahnspannung ueber Port D *
; * (C)2005 by info!at!avr-asm-tutorial.net                   *
; *************************************************************
;
.INCLUDE "8515def.inc"
;
; Register Definitionen
;
.DEF rmp = R16 ; Multipurpose Register
;
	ldi rmp,0xFF;  Alle Pins von Port D als Ausgang
	out DDRD,rmp ; in Datenrichtungsregister
sawtooth:
	out PORTD,rmp ; Inhalt von rmp an Port D ausgeben
	inc rmp ; erhöhen
	rjmp sawtooth ; und weiter fuer immer


Sägezahn oben Das Ergebnis ist etwas enttäuschend. Sieht nicht wie ein Sägezahn aus, eher wie eine Holzsäge, mit der Stahl gesägt worden ist.

Der Grund dafür liegt nicht beim R/2R-Netzwerk sondern beim Operationsverstärker. Er arbeitet nicht so ganz gut in der Nähe der positiven Betriebsspannung.

Die maximale Ausgangsspannung des R/2R-Netzwerks muss also auf etwa 2.5 Volt begrenzt werden. Das wird per Software erledigt (Quellcode steht unter diesem Link zum Download).


; *****************************************************
; * R/2R-Netzwerk als Saegezahn ueber Port D          *
; * (C)2005 by info!at!avr-asm-tutorial.net              *
; *****************************************************
;
.INCLUDE "8515def.inc"
;
; Register Definitionen
;
.DEF rmp = R16 ; Multipurpose Register
;
	ldi rmp,0xFF;  Alle Pins von Port D auf Ausgang
	out DDRD,rmp
sawtooth:
	out PORTD,rmp ; Inhalt des Registers an Port ausgeben
	inc rmp ; um Eins erhoehen
	andi rmp,0x7F ; Bit 7 auf Null setzen
	rjmp sawtooth ; und so weiter
Sägezahn Das sieht etwas besser aus.

Man beachten, dass wir jetzt Bit 7 des Ports eigentlich nicht mehr benötigen, er kann fest auf Null gezogen werden, weil er sowieso Null ist.

Sägezahn 741 Hier das Ergebnis, wenn der Operationsverstärker CA3140 mit einem billigeren 741 ersetzt wird. Der 741 arbeitet weder in der Nähe der negativen noch in der Nähe der positiven Betriebsspannung. Der Spannungsbereich des R/2R-Netzwerks müsste entweder weiter eingeschränkt werden (auf ca. 2 bis 4 Volt) oder es muss eine symmetrische Versorgung des Opamp her.

Zum Anfang dieser Seite

Eine Dreieckspannung

Eine Dreieckspannung ist ähnlich einfach zu erzeugen: nur hoch und runter zählen. Der Quellcode kann wieder unter diesem Link gedowngeloaded werden. Die Software gestattet die Einstellung der Frequenz durch Änderung der Konstanten "delay" und die maximale Amplitude durch "maxAmp".


; ***********************************************************
; * R/2R-Netzwerk produziert eine Dreieckspannung an Port D *
; * (C)2005 by info!at!avr-asm-tutorial.net                    *
; ***********************************************************
;
.INCLUDE "8515def.inc"
;
; Register Definitionen
;
.DEF rmp = R16 ; Multipurpose Register
.DEF rdl = R17 ; Verzoegerungszaehler
;
; Konstanten
;
.EQU maxAmp = 127 ; Maximum Amplitude
.EQU delay = 1 ; Verzoegerung, hoehere Werte machen niedrigere Frequenz
;
	ldi rmp,0xFF;  Alle Port-D-Pins als Ausgang
	out DDRD,rmp
triangle:
	clr rmp ; bei Null anfangen
loopup:
	out PORTD,rmp ; an Port ausgeben
	ldi rdl,delay ; Verzoegerung einstellen
delayup:
	dec rdl ; Zaehler herunterzaehlen
	brne delayup ; weiter mit zaehlen
	inc rmp ; naechstgroesserer Wert
	cpi rmp,maxAmp ; mit maximaler Amplitude vergleichen
	brcs loopup ; wenn noch nicht erreicht, weiter hoch
loopdwn:
	out PORTD,rmp ; Ausgabe rueckwaerts
	ldi rdl,delay ; wieder verzoegern
delaydwn:
	dec rdl ; herunterzaehlen
	brne delaydwn ; weiter verzoegern
	dec rmp ; naechstniedrigeren Wert einstellen
	brne loopdwn ; wenn noch nicht Null, dann weiter
	rjmp triangle ; und wieder von vorne fuer immer


Dreieck Dreieck zu hoch Links wurde die maximale Amplitude auf 2,5 V begrenzt, rechts sind die vollen 5 Volt ausgesteuert.


Zum Anfang dieser Seite

Einen Sinus erzeugen

Mindestens ein Sinus muss jetzt her. Wer denkt, ich schreibe jetzt in Assembler ein Programm zur Sinusberechnung, den muss ich enttäuschen. Ich mache das auf dem PC in einem kleinen Programm in Free-Pascal (Download hier) und importiere die resultierende Tabelle (Download hier) in mein Programm (Download hier).


; **********************************************************
; * Produziert einen Sinus an einem R/2R-Netzwerk an PORTD *
; * (C)2005 by avr-asm-tutorial.net                        *
; **********************************************************
;
.INCLUDE "8515def.inc"
;
; Register Definition
;
.DEF rmp = R16 ; Multipurpose Register
;
; Beginn des Programms
;
	ldi rmp,0xFF ; Alle Pins von Port D sind Ausgang
	out DDRD,rmp
	ldi ZH,HIGH(2*SineTable) ; Z auf Tabelle im Flash
	ldi ZL,LOW(2*SineTable)
	clr rmp
loop1:
	nop
	nop
	nop
loop2:
	lpm ; Lesen aus der Tabelle
	out PORTD,R0 ; Tabellenwert an Port D
	adiw ZL,1 ; naechster Tabellenwert
	dec rmp ; Ende der Tabelle erreicht?
	brne loop1 ; nein
	ldi ZH,HIGH(2*SineTable) ; Z wieder auf Tabellenanfang
	ldi ZL,LOW(2*SineTable)
	rjmp loop2 ; weiter so
;
; Ende Instruktionen
;
; Include der Sinustabelle
;
.INCLUDE "sine8_25.txt"
;
; Ende Programm
;
;
; Sinustabelle fuer 8 Bits D/A
; Tabellenlaenge = 256 Werte
; VCC=5.000V, uLow=0.000V, uHigh=2.500V
; (mit sinewave.pas erzeugt)
;
Sinetable:
.DB 64,65,67,68,70,72,73,75
.DB 76,78,79,81,82,84,85,87
.DB 88,90,91,92,94,95,97,98
.DB 99,100,102,103,104,105,107,108
.DB 109,110,111,112,113,114,115,116
.DB 117,118,118,119,120,121,121,122
.DB 123,123,124,124,125,125,126,126
.DB 126,127,127,127,127,127,127,127
.DB 128,127,127,127,127,127,127,127
.DB 126,126,126,125,125,124,124,123
.DB 123,122,121,121,120,119,118,118
.DB 117,116,115,114,113,112,111,110
.DB 109,108,107,105,104,103,102,100
.DB 99,98,97,95,94,92,91,90
.DB 88,87,85,84,82,81,79,78
.DB 76,75,73,72,70,68,67,65
.DB 64,62,61,59,58,56,54,53
.DB 51,50,48,47,45,44,42,41
.DB 39,38,36,35,34,32,31,30
.DB 28,27,26,25,23,22,21,20
.DB 19,18,17,15,14,13,13,12
.DB 11,10,9,8,8,7,6,5
.DB 5,4,4,3,3,2,2,2
.DB 1,1,1,0,0,0,0,0
.DB 0,0,0,0,0,0,1,1
.DB 1,2,2,2,3,3,4,4
.DB 5,5,6,7,8,8,9,10
.DB 11,12,13,13,14,15,17,18
.DB 19,20,21,22,23,25,26,27
.DB 28,30,31,32,34,35,36,38
.DB 39,41,42,44,45,47,48,50
.DB 51,53,54,56,58,59,61,62


Sinus Das war es schon. Macht einen schönen Sinus. Man glaubt kaum, dass hier eine digitale Maschinerie am Werk ist und kein sauberer LC-Oszillator.

Unglücklicherweise kann man mit dieser Methode keinen Sinus mit mehr als 1800 Hz machen, weil vier MHz Takt durch 256 schon nur noch 15.625 ergeben. Und zum Tabellelesen auch der eine oder andere Takt gebraucht wird.


Zum Anfang dieser Seite

Musiknoten mit dem R/2R-Netzwerk spielen

Das folgende Programm benutzt das R/2R-Netzwerk zum Spielen von Musiknoten mit den Tasten des STK200-Boards. Es arbeitet ohne Änderung mit einem AT90S8515 mit 4 MHz Takt, den acht Tastenschaltern am Port D und dem R/2R-Netzwerk an Port B angeschlossen (Download hier).


; ******************************************************************
; * Musik mit dem STK200 und einem R/2R-Netzwerk                   *
; * PortD hat acht Tasten (active low), PortB generiert die Ausgabe*
; * fuer das R/2R-Netzwerk, spielt Noten wenn die Tasten betaetigt *
; * werden, fuer ATMEL AT90S8515 bei 4 MHz                         *
; * (C)2005 by info!at!avr-asm-tutorial.net                           *
; ******************************************************************
;
.NOLIST
.INCLUDE "8515def.inc"
.LIST
;
; Konstanten
;
.EQU clock = 4000000 ; Processortakt
.EQU cNSine = 32 ; Tabellelaenge Sinustabelle
;
.DEF rLen = R1 ; Register fuer Dauer der Ausgabe
.DEF rCnt = R2 ; Zaehler fuer die Verzoegerung
.DEF rmp = R16 ; Multipurpose Register
.DEF rTab = R17 ; Zaehler fuer Tabellenlaenge
;
	ldi rmp,0xFF ; Alle Bits von Port B Ausgang => R/2R Netzwerk
	out DDRB,rmp ; an Datenrichtungsregister
wtloop:
	in rmp,PIND ; lese die Tasten
	cpi rmp,0xFF ; alle Tasten inaktiv?
	breq wtloop ; je, weiter warten bis aktiv
	ldi ZH,HIGH(2*MusicTable) ; Z auf Tonhoehentabelle setzen
	ldi ZL,LOW(2*MusicTable)
tabloop:
	rol rmp ; Rotiere naechstes Bit in Carry
	brcc tabfound ; gedrueckte Taste gefunden
	adiw ZL,1 ; Z auf nachsten Tabellenwert
	rjmp tabloop ; Pruefe naechstes Bit
tabfound:
	lpm ; Lese Tonhoehenwert aus Tabelle in R0
	mov rlen,R0 ; Kopiere in delay, R0 wird anderweitig benutzt
;
; Spiele einen Ton, bis die Tasten alle inaktiv sind
;
startsine:
	ldi ZH,HIGH(2*SineTable) ; Z auf die Sinustabelle setzen
	ldi ZL,LOW(2*SineTable)
	ldi rTab,cNSine ; Laenge der Sinustabelle
;
; Der folgende Code ist timing-maessig optimiert, damit alle
; Teilschritte gleich lang dauern, die benoetigten Taktzyklen
; sind angegeben, wenn die Instruktion abgearbeitet ist,
; Taktzyklen waehrend Tabellenlesen / Taktzyklen am Tabellenende
;
loopsine:
	in rmp,PIND ; 1/- Pruefe ob Tasten noch aktiv ist
	cpi rmp,0xFF ; 2/-
	breq wtloop ; 3/- Taste nicht mehr aktiv, gehe zurueck
	nop ; 4/- verzoegern fuer Synchronisation
loopnext:
	lpm ; 7/3 Lese Sinustabelle in R0
	out PORTB,R0 ; 8/4 Kopiere zum R/2R-Netzwerk
	mov rCnt,rLen ; 9/5 Setze Verzoegerungszaehler
; Verzoegerungsschleife, braucht 3*rLen-1 Taktzyklen
loopdelay:
	dec rCnt ; (1) naechster Zaehlerwert
	brne loopdelay ; (2/1) noch nicht Null
; 3*rLen+8/3*rLen+4 am Ende der Verzoegerung
	adiw ZL,1 ; 3*rLen+10/3*rLen+6 naechster Tabellenwert
	dec rTab ; 3*rLen+11/3*rLen+7 Anzahl Tabellenwerte
	brne loopsine ; 3*rLen+13/3*rLen+8 naechster Tabellenwert
	ldi ZH,HIGH(2*SineTable) ; -/3*rLen+9 Neuanfang Tabelle
	ldi ZL,LOW(2*SineTable) ; -/3*rLen+10
	ldi rTab,cNSine ; -/3*rLen+11 Laenge der Sinustabelle
	rjmp loopnext ; -/3*rLen+13 Neustart (ohne Tasten!)
;
; Tabelle fuer Verzoegerung zur Tonerzeugung der 8 Frequenzen
;
; Frequenz = clock / Tabellenlaenge / ( 3 * rLen + 13 )
; rLen = ( clock /Tabellenlaenge / Frequenz - 13 ) / 3
;
MusicTable:
; f=261.6 293.7 329.6 349.2 392.0 440.0 493.9 523.2 (Sollwert)
.DB 155,  138,  122,  115,  102,  90,   80,   75
; f=261.5 292.7 329.8 349.2 391.9 441.7 494.1 525.2 (Istwert)
; Unterschiede zwischen Soll und Ist wegen Rundung und Aufloesung)
;
; Sinustabelle fuer 8 Bits D/A
; Tabellenlaenge = 32 Werte
; VCC=5.000V, uLow=0.000V, uHigh=2.500V
; (mit sinewave.pas erzeugt)
;
Sinetable:
.DB 64,76,88,99,109,117,123,126
.DB 128,126,123,117,109,99,88,76
.DB 64,51,39,28,19,11,5,1
.DB 0,1,5,11,19,28,39,51
;
; Ende des Programms
;


Natürlich muss man einen kleinen Lautsprecher oder Ohrhöhrer an den Bufferausgang anschließen. Ohne das bleiben die schönen Sinuswellen stumm.

Zum Anfang dieser Seite

©2005 by http://www.avr-asm-tutorial.net