Zweikanal-DDS-Generator          


Elektronik-Labor   Projekte   AVR 


Auszug aus dem Arduino Messlabor Kap. 4.

Das zentrale Ziel ist, dass alle Funktionen des Messlabors gleichzeitig genutzt werden können. Man kann die einzelnen Ein- und Ausgänge also so verwenden, als gehörten sie zu eigenständigen Geräten. Später sollen dann noch weitere Funktionen hinzukommen.

Frequenzzähler bis 16 MHz
Zwei Voltmeter bis 5 V
Rechteckgenerator bis 8 MHz

 4.1 Zweikanal-DDS-Generator

Das MSR-Labor soll mit zwei unabhängig einstellbaren DDS-Generatoren ausgestattet werden. Insbesondere die Auswahl der Timer erfordert einige Vorüberlegungen, weil es hier verschiedene Einschränkungen gibt. Insbesondere teilen sich Timer 0 und Timer 1 den Anschluss am Port D5. Wenn Timer 1 später als Frequenzzähler eingesetzt werden soll, verbietet sich die Verwendung des PWM-Ausgangs OC0B. Die beiden PWM-Ausgänge für die DDS-Generatoren verwenden deshalb nun den Timer 2. OC2A liegt an PD3 und OC2B an PB3. Damit kann der Eingang T1 (an PD6) für den Frequenzzähler verwendet werden. Und OC0A bleibt für einen Rechteckgenerator bis 8 MHz einsetzbar.

Timer 2 kann in ähnlicher Weise für PWM-Ausgänge initialisiert werden wie bisher der Timer 0. Die PWM-Frequenz beträgt wieder 62,5 kHz. Mit gleicher Frequenz wird eine Timer-2-Interrupt-Funktion aufgerufen, in der die DDS-Signale erzeugt und die Analogwerte für das Oszilloskop gemessen werden. Allerdings gibt es leider keine Möglichkeit, den ADC-Start automatisch vom Timer 2 zu triggern. Es muss also ein zusätzliches Startkommando eingefügt werden. Das geht jedoch ohne Probleme, weil die Rechenzeit innerhalb der Interrupt-Funktion noch nicht knapp wird. Später werden hier sogar noch weitere Elemente eingefügt.

In der Interrupt-Funktion werden nun über beide PWM-Ausgänge gleich zwei Sinussignale erzeugt. Dazu gibt es zwei getrennte Phasenakkus a1 und a2. Sie werden diesmal nicht durch Konstanten erhöht, sondern durch die Variablen ph1 und ph2, über die man extern die Frequenz vorgeben kann. Außerdem werden zwei Rechtecksignale mit gleicher Frequenz an den Ausgängen D4 und D7 erzeugt, indem jeweils das höchtwertige Bit des Phasenakkus auf den Ausgang kopiert wird. Damit gibt es insgesamt vier DDS-Ausgänge und einen Hilfsausgang:

Sinus 1 (OC2A) an PB3 (D11)
Sinus 2 (OC2B) an PD3 (D3)
Rechteck 1 an PD4 (D4)
Rechteck 2 an PD7 (D7)
Rechteck 62,5 kHz an PB0 (D8)

Der Zweikanal-Generator mit Tiefpassfiltern

ISR (TIMER2_OVF_vect)
{
  PORTB |= 1;
  a1+=ph1;
  a2+=ph2;
  if (ds1) OCR2A=(dds[a1>>8]);
  if (ds2) OCR2B=(dds[a2>>8]);
  PORTD = (PORTD & ~128)|((a1>>8)&128);
  PORTD = (PORTD & ~16)|((a2>>11)&16);
  if (adn<628){
       UDR0 = ADCH;
       adn++;
  }
  ADCSRA |= 0x40;  //AD Start 
  PORTB &= ~1;
}

Außerdem wird in der Interrupt-Funktion die Messung über den AD-Wandler durchgeführt. Anders als zuletzt wird das Messergebnis nicht zwischengespeichert, sondern direkt an den UART übergeben (UDR0 = ADCH;). Der Zeitaufwand für dieses Kopieren eines einzelnen Bytes ist extrem gering. Mit dem Schreibzugriff wird zugleich der UART gestartet. Das eigentliche Senden des Datenbytes dauert bei 1 Mbaud etwa 10 µs und wird durch die Hardware geleistet, ohne den Prozessor zu belasten. Entscheidend ist nur, dass die Übertragungszeit kürzer ist als die Interrupt-Periode von 16 µs. So werden 628 Bytes gemessen und gesendet. Das User-Programm muss in der Lage sein, Einzelbytes zu empfangen und darzustellen. Um eine neue Serie zu starten, muss der Zähler adn von außen auf Null gesetzt werden.

Gegen Ende der Funktion wird der AD-Wandler über einen direkten Startbefehl neu angestoßen (ADCSRA = 0xD2;), weil die automatische Triggerung durch den Timer 2 nicht möglich ist. Auch dieser Befehl besteht nur aus einem einzelnen Byte-Transfer. Der AD-Wandler hat dann genügend Zeit bis zum nächsten Interrupt.

 

4.2 Binäre serielle Übertragung

Die Firmware verzichtet ganz auf die Übertragung von Text und verwendet stattdessen binäre Daten in Form von Einzelbytes. Deshalb können die Arduino-typischen Befehle zur Datenübertragung nicht benutzt werden. Für die Byte-Übertragung gibt es nun die Funktionen  USART_Transmit und USART_Receive. In beiden Richtungen wird letztlich nur ein Byte in UDR0  geschrieben oder von dort gelesen. Vorher wird jeweils gewartet, bis der USART bereit ist bzw. ein Byte empfangen hat. Die Byte-Übertragung aus der Interrupt-Funktion konnte auf diese Wartezeit verzichten, weil bei einem Sendetakt von 16 µs klar ist, dass der USART für ein neues Byte bereit ist.  

In der Main-Funktion werden zunächst der USART, der Timer 2, der AD-Wandler und die Ports initialisiert. Der Timer 2 liefert zwei PWM-Ausgänge mit 62,5 kHz und löst bei jedem Überlauf einen Timer-Interrupt aus. Die PWM-Pulsbreiten werden zunächst mit 128 initialisiert. Der AD-Wandler arbeitet left-adjusted, sodass die oberen 8 Bit mit einem Byte-Zugriff im Register ADCH gelesen werden können.

Der Wandler-Takt wird auf die höchste mögliche Geschwindigkeit eingestellt. Der AD-Takt wird auf Clk/16, also 1 MHz eingestellt. Das Datenblatt empfiehlt 200 kHz für beste Genauigkeit. Weil hier jedoch nur 8 Bit der verfügbaren Auflösung verwendet werden, kann der schnellere Takt ohne Verluste verwendet werden. Der AD-Wandler benötigt 13 Takte für eine Messung. Die Wandlungszeit ist daher mit 13 µs kürzer als die Interrupt-Periode von 16 µs. Die serielle Übertragung des letzten Messwerts und die Messung des folgenden Werts laufen gleichzeitig mit ihren getrennten Hardware-Blöcken, ohne die Rechenzeit des Controllers zu belasten.

void USART_Transmit(unsigned char data){
  while ( !( UCSR0A & (1<<UDRE0)));
  UDR0 = data;

unsigned char USART_Receive( void ){
while ( !(UCSR0A & (1<<RXC0)) );
return UDR0;
}

int main()
{
  UBRR0H = 0;    //USART
  UBRR0L = 0;     //1 MBaud
  UCSR0B = 0x18;  //TX, RX an
  UCSR0C = 0x06;  //8N1

  TCCR2A = 0xA3;  //Timer2 Fast PWM an PD3/PB3
  TCCR2B = 0x01;  //16 MHz
  TIMSK2 = 0x01;  //Timer0 Overflow Interrupt
  OCR2A = 128;
  OCR2B = 128;

  ADMUX = 0x60;   //Ref = VCC, Left adjusted
  ADCSRA = 0xC4;  //ADC Enable, Start, 1 MHz 

  DDRB = 255;
  DDRD = 254; 

  for (n=0;n<256;n++)  dds[n]= 128+127.9*sin(PI*n/128.0);
 
ph1=1049;  //1 kHz
  ph2=2098;  //2 kHz
  sei(); 

  while(true) {
    c=USART_Receive();
    if (c==65) adn=0;     //Oszi Start
}

 

Zusätzlich zur Initialisierung der Controller-Peripherie wird in der Main-Funktion auch die Sinustabelle dds[n] mit ihren 256 Stützwerten vorbereitet. Und schließlich werden die beiden Ausgangsfrequenzen auf 1 kHz und 2 kHz eingestellt. Für 1000 Hz muss ph1=1049 eingestellt werden. Man hat also eine Frequenzauflösung von rund einem Hertz. Später werden auch andere Frequenzen über serielle Kommandos zugewiesen. Die beiden Sinus- und die beiden Rechtecksignale erscheinen ohne Verzögerung und dauerhaft an ihren Ausgängen. Messwerte werden aber erst nach einer Aufforderung gesendet.

In einer Endlosschleife wartet das Programm auf Kommandos in Form einzelner Bytes. In der ersten Ausbaustufe wird nur ein Kommando erkannt. Wenn ein Byte 65 empfangen wurde, wird eine Messung mit 628 Punkten gestartet, indem adn=0 gesetzt wird. Das Kommando kann auch als einzelnes großes A gesendet werden. Beide Lesarten der Daten können von dem einfachen seriellen Terminalprogramm Ternimal.exe verarbeitet werden. Die empfangenen Daten zeigen das mit einem RC-Filter geglättete Sinussignal. Man erkennt deutlich das Maximum bei 240 und das Minimum bei 15.

 Start und Empfang der Messwerte

 
Das VB-Programm löst die Messung in einer Timer-Funktion aus, die nach jeweils einer Sekunde wiederholt wird. Auf diese Weise bekommt man ausreichend lange stehende Bilder. Sobald Daten empfangen werden, wird ein neues Diagramm mit grünen Hilfslinien im Zeichenbereich Picture1 vorbereitet. Dann werden insgesamt 626 Bytes empfangen und in 625 Linienabschnitten geplottet. Bei einer Abtastfrequenz von 62,5 kHz hat man damit eine Zeit von 10 ms auf der x-Achse, also 1 Millisekunde pro Skalenteil (Ablenkzeit 1 ms/div).

 

Private Sub Timer2_Timer()
busy = 1
CLEARBUFFER
 SENDBYTE 65
 Do
    n = n + 1
    If n > 20000 Then Exit Do
 Loop Until INBUFFER > 0
 Picture5.Cls
 For n = 1 To 9
    Picture1.Line (62.5 * n, 2)-(62.5 * n, 258), vbGreen
 Next
 For n = 0 To 10
    Picture5.Line (0, 258 - n * 25.5)-(625, 258 - n * 25.5), vbGreen
 Next
 alast = 258 - READBYTE
 alast = 258 - READBYTE
 For n = 0 To 625
     Ain = 258 - READBYTE
     Picture5.Line (n - 1, alast)-(n, Ain), vbBlack
     alast = Ain
 Next n
End Sub

 

4.3 Frequenzeinstellung

Die nächste Stufe der Erweiterung betrifft die Einstellung der DDS-Frequenzen. Dazu müssen die Inhalte von ph1 und ph2 neu zugewiesen werden. Hierfür wurden die Kommandos 67 und 68 definiert. Weil beide Variablen eine Breite von 16-Bit haben, müssen jeweils zwei Datenbytes übertragen werden.

while(true) {
    c=USART_Receive();
    //cli();
    if (c==65) adn=0;     //Oszi Start
    if (c==80){           //DDS Freq 1
      ph1 = USART_Receive()<<8;
      ph1 = ph1+ USART_Receive();
    }
    if (c==81){           //DDS Freq 2
      ph2 = USART_Receive()<<8;
      ph2 = ph2+ USART_Receive();
   
}
}

 
Ein Beispiel: Mit einem Wert von 512  erzeugt man ca. 500 Hz. Um diese Frequenz für den zweiten DDS-Kanal einzustellen, lautet das Kommando und seine Daten: 80, 2, 0. Im Terminal erkennt man den verlangsamten Fortgang der Sinusfunktion.

Frequenzzuweisung und Start einer Messung

Im User-Programm gibt es zwei Schieberegler für die beiden DDS- Frequenzen. Beide rechnen die einstellbare Frequenz von 0 bis 5000 Hz in Frequenzparameter von 0 bis 5242 um, die dann nach dem Steuerkommando 66 oder 67 als Highbyte und Lowbyte gesendet werden. Die Grenze von 5 kHz ist willkürlich gesetzt. Man könnte auch bis zu 10 kHz erzeugen, Aber dann hätte jede Schwingung nur noch sechs Stützwerte, was noch wesentlich bessere Tiefpassfilter erfordern würde.

Private Sub HScroll1_Change()
  Data = HScroll1.Value
  f1 = Round(Data * 0.65536 * 1.6)
  Label1.Caption = Str$(Data) + " Hz"
  SENDBYTE 80
  Hi = Int(f1 / 256)
  Lo = f1 And 255
  SENDBYTE Hi
  SENDBYTE Lo
End Sub

Bisher gibt es zwar zwei DDS-Ausgänge, aber nur einen Analog-Eingang. Zum Test bietet es sich an, beide DDS-Ausgänge mit jeweils 10 kΩ oder auch unterschiedlichen Widerständen auf den gemeinsamen Eingang A0 zu führen und mit 10 nF zu glätten. Das Oszilloskop zeigt dann die Summe beider Signale. Wenn man leicht unterschiedliche Frequenzen einstellt, erscheinen die typischen Schwebungen.

 Addition beider DDS-Kanäle

Überlagerung zweier Sinussignale

Zusätzlich zu den beiden Sinus-Ausgängen gibt es zwei Rechteck-Ausgänge mit der gleichen Frequenz. DDS1 liefert ein Signal an D7, DDS2 an D4. Auch diese Signale können direkt mit dem Oszilloskop untersucht werden. Schickt man sie durch ein Tiefpassfilter, entstehen dabei die typischen Haifischzähne mit einem exponentiellen Anstieg und Abfall der Flanken.

Rechtecksignal mit 500 Hz an D7

Rechtecksignal mit 800 Hz nach Tiefpassfilterung mit 10 kΩ und 10 nF



Elektronik-Labor   Projekte   AVR