Homepage von Peter Rachow Startseite - Home

VFO mit DDS-Baustein AD9832

(C) 2014 Peter Rachow
Die folgende Schaltung zeigt, wie ein AD9835 als Frequenzsynthesebaustein von einem ATMega128 angesteuert wird. Das Projekt ist gedacht als Frequenzaufbereitung für einen Amateurfunktransceiver. Die Schaltung liefert ein hochfrequenzstabiles und extrem genaues Ausgangssignal, welches in diesem Falle variabel zwischen 5 und 5.5 MHz liegt.

Projektüberblick

Die Schaltung besteht aus mehreren Funktionsgruppen:

a) Der Frequenzeinstellung,
b) dem Mikrocontroller ATMega128 mit einem LCD-Textdisplay,
c) dem DDS-Baustein AD9835,
d) einem Ausgangsverstärker.

Einzelheiten

a) Die Frequenzeinstellung ist unkonventionell gelöst. Ziel war es, einen Drehregler zu emulieren, wie er an Transceivern Standard ist. Dazu wäre grundsätzlich ein optischer Drehgeber denkbar gewesen, der über den uC abgefragt wird. Ich habe stattdessen foglenden Weg gewählt:

Ein kleiner Gleichstrommotor, wie er in Modellbahnloks oder anderweitig verwendet wird, wird als Generator betrieben. Die Höhe der Spannung, welche er liefert, hängt ab von der Winkelgeschwindigkeit mit der die Achse in Drehung versetzt wird. Die Polarität der Spannung ist zusätzlich abhängig von der Drehrichtung.

Der als Generator "missbrauchte" Motor liefert sein Signal an einen OP vom Typ LM358. Wird der Motor nicht bewegt, so erzeugt dieser OP eine konstante Spannung von ungefähr 2,5 V. Wird der Motor gedreht steigt oder fällt die Spannung als Funktion von Winkelgeschwindigkeit und Drehrichtung um einen bestimmten Betrag. Sie pendelt dabei zwischen 0 und 5 Volt und lässt sich so über den ADC des ATMega präzise auswerten. Diese Daten steuern die Frequenzeinstellung des Controllers und damit des DDS-Bausteins AD9835.

Achtung: Bürstenlose Motoren eignen sich hierfür nicht!

b) Die Beschaltung des ATMega128 mit dem 2 x 16-Zeilen-Display zeigt keine Besonderheiten, so dass hier keine weiteren Ausführungen vonnöten sind.

c) Der AD9835 wird nur im TSSOP-SMD-Package geliefert. Empfehlenswert ist eine Adapterplatine, wenn man in konventioneller Technik mit bedrahteten Komponenten arbeiten will. Die Leitungen insbesondere nach Masse sind kurz zu halten. Die von mir aufgebaute Schaltung besitzt ein Ausgangsfilter (Pi-Schaltung), welches unbedingt erforderlich ist, um eine sinusförmige Ausgangsspannung zu erhalten. Der AD9835 kann theoretisch bis ca. 20 MHz betrieben werden, allerdings ist dann die Signalqualität nicht mehr optimal. Der Chip benötigt als Referenz einen Quarzoszillator. Hier sind max. 50 MHz zulässig.

d) Der Ausgangsverstärker liefert ein Signal von ca. 2VSS. Statt des 2SC1975 (in alten CB-Geräten immer mehrfach enthalten) kann jeder andere NPN-Transistor mit einer fT von > 30 MHz verwendet werden.

Die Schaltung:
VFO mit DDS-Chip AD9835 und ATMega128
(DDS-VFO mit AD9835 - bitte klicken zur Vollbilddarstellung!)

Hier ein Beispielcode für den ATMega128 zur direkten Synthese der Frequenz mit dem AD9835. Es werden eigene SPI-Routinen verwendet. Das genaue Timing steht im Datenblatt.


/*****************************************************************/
/* DDS mit ATMega 128 und AD 9835 */
/* ************************************************************ */
/* Mikrocontroller: ATMEL AVR ATmega128, 8 MHz */
/* */
/* Compiler: GCC (GNU AVR C-Compiler) */
/* Autor: Peter Rachow */
/* Letzte Aenderung: 01.09.2014 */
/*****************************************************************/

#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <avr/eeprom.h>
#include <util/delay.h>

char *appstr = "AVR DDS V15";

unsigned long runseconds = 0;

/*******************/
// SPI
/*******************/
//Belegung
//FSYNC: PC2 (4)
//SCLK: PC3 (8)
//SDATA: PC4 (16)

void spi_start(void);
void spi_send_bit(int);
void spi_send_byte(unsigned int);
void spi_send_word(unsigned int);
void spi_stop(void);

void set_frequency(unsigned long);

/***************/
/* LCD-Display */
/***************/
//Daten: PD4..PD7
//E: PC0
//RS: PC1

#define LCD_INST 0x00
#define LCD_DATA 0x01

void lcd_write(char, unsigned char, int);
void set_rs(char);
void set_e(char);
void lcd_init(void);
void lcd_cls(void);
void lcd_linecls(int, int);
void lcd_putchar(int, int, unsigned char);
void lcd_putstring(int, int, char*);
int lcd_putnumber(int, int, long, int, int, char, char);
void lcd_display_test(void);


int main(void);

//************
// SPI
//************
void spi_start(void)
{

//FSYNC lo
PORTC &= ~(4); // Bit PC2 löschen

}

void spi_send_bit(int sbit)
{
//Bit setzen oder löschen
if(sbit)
{
PORTC |= 16; //Bit PC4 setzen
}
else
{
PORTC &= ~(16); //Bit PC4 löschen
}

//SCLK hi
PORTC |= 8; //Bit PC3 setzen

//SCLK lo
PORTC &= ~(8); //Bit PC3 löschen

}

void spi_send_byte(unsigned int sbyte)
{
int t1, x = 128;

for(t1 = 0; t1 < 8; t1++)
{
spi_send_bit(sbyte & x);
x /= 2;
}

PORTC |= 16; //SDATA hi

}

void spi_send_word(unsigned int sbyte)
{
unsigned int t1, x = 32768;

for(t1 = 0; t1 < 16; t1++)
{
spi_send_bit(sbyte & x);
x /= 2;
}

PORTC |= 16; //SDATA hi
}


void spi_stop(void)
{
//FSYNC hi
PORTC |= 4; // Bit PC2 setzen

}


//***************************************
// TIMER 2
//***************************************
// Timer 2 Ereignisroutine (autom. Aufruf 1/s)
ISR(TIMER0_OVF_vect)
{
runseconds++;
TCNT0 = 0; // Timerregister auf 0
}

/**************************************/
/* Funktionen und Prozeduren fuer LCD */
/**************************************/
//Anschlussbelegeung am uC:
//LCD-Data: PD4..PD7
//E: PC0
//RS: PC1

/* Ein Byte (Befehl bzw. Zeichen) zum Display senden */
void lcd_write(char lcdmode, unsigned char value, int waitcycles)
{
set_e(0);

if(!lcdmode)
set_rs(0); /* RS=0 => Befehl */
else
set_rs(1); /* RS=1 => Zeichen */

_delay_ms(waitcycles * 2);

set_e(1);
PORTD = value & 0xF0; /* Hi byte */
set_e(0);

set_e(1);
PORTD = (value & 0x0F) * 0x10; /* Lo byte */
set_e(0);

}

/* E setzen */
void set_e(char status) /* PORT PC0 = Pin 6 am LCD */
{
if(status)
{
PORTC |= 1;
}
else
{
PORTC &= ~(1);
}
}


/* RS setzen */
void set_rs(char status) /* PORT PC1 = Pin 4 am LCD */
{
if(status)
{
PORTC |= 2;
}
else
{
PORTC &= ~(2);
}
}


/* Ein Zeichen (Char) zum Display senden, dieses in */
/* Zeile row und Spalte col positionieren */
void lcd_putchar(int row, int col, unsigned char ch)
{
lcd_write(LCD_INST, col + 128 + row * 0x40, 1);
lcd_write(LCD_DATA, ch, 1);
}


/* Eine Zeichenkette direkt in das LCD schreiben */
/* Parameter: Startposition, Zeile und Pointer */
void lcd_putstring(int row, int col, char *s)
{
unsigned char t1;

for(t1 = col; *(s); t1++)
{
lcd_putchar(row, t1, *(s++));
}
}


/* Display loeschen */
void lcd_cls(void)
{
lcd_write(LCD_INST, 1, 5);
}


/* Display loeschen (eine Zeile) */
void lcd_linecls(int displine, int chars)
{
unsigned char t1;

for(t1 = 0; t1 <= chars; t1++)
lcd_putchar(displine, t1, 32);
}


/* LCD-Display initialisieren */
void lcd_init(void)
{
/* Grundeinstellungen: 2 Zeilen, 5x7 Matrix, 4 Bit */
lcd_write(LCD_INST, 40, 5);
lcd_write(LCD_INST, 40, 5);
lcd_write(LCD_INST, 40, 5);

lcd_write(LCD_INST, 2, 5);
lcd_write(LCD_INST, 8, 5);

/* Display on, Cursor off, Blink off */
lcd_write(LCD_INST, 12, 5);

lcd_cls();

/* Entrymode !cursoincrease + !displayshifted */
lcd_write(LCD_INST, 4, 5);
}


/* Eine n-stellige Zahl direkt in das LCD schreiben */
/* Parameter: Startposition und Zeile; Zahl, */
/* darzustellende Ziffern, Position des Dezimalpunktes, (l)links- oder (r)echtsbuendig */
int lcd_putnumber(int row, int col, long num, int digits, int dec, char orientation, char showplussign)
{
char cl = col, minusflag = 0;
unsigned char cdigit[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, digitcnt = 0;
long t1, t2, n = num, r, x = 1;

if(num < 0)
{
minusflag = 1;
n *= -1;
}

/* Stellenzahl automatisch bestimmen */
if(digits == -1)
{
for(t1 = 1; t1 < 10 && (n / x); t1++)
x *= 10;
digits = t1 - 1;
}

if(!digits)
digits = 1;

for(t1 = digits - 1; t1 >= 0; t1--)
{
x = 1;
for(t2 = 0; t2 < t1; t2++)
x *= 10;
r = n / x;
cdigit[digitcnt++] = r + 48;

if(t1 == dec)
cdigit[digitcnt++] = 46;
n -= r * x;
}

digitcnt--;
t1 = 0;

/* Ausgabe */
switch(orientation)
{
case 'l':
cl = col;
if(minusflag)
{
lcd_putchar(row, cl++, '-');
digitcnt++;
}
else
{
if(showplussign)
{
lcd_putchar(row, cl++, '+');
digitcnt++;
}
}


while(cl <= col + digitcnt) /* Linksbuendig */
lcd_putchar(row, cl++, cdigit[t1++]);

break;

case 'r':
t1 = digitcnt; /* Rechtsbuendig */
for(cl = col; t1 >= 0; cl--)
lcd_putchar(row, cl, cdigit[t1--]);
if(minusflag)
lcd_putchar(row, --cl, '-');
}

if(dec == -1)
return digits;
else
return digits + 1;
}

void set_frequency(unsigned long freq)
{

double fxtal = 50000; //fQuarz in Khz
double fword1;
unsigned long hiword, loword;
unsigned char hmsb, lmsb, hlsb, llsb;

fword1 = freq / fxtal * 0xFFFFFFFF;

//Aufspalten der 32 Bit in 2 * 16 Bit
hiword = (unsigned long) fword1 / 65536;
loword = (unsigned long) fword1 - hiword * 65536;

//Aufspalten der 1. 16 Bit in 2 * 8 Bit
hmsb = hiword / 256;
lmsb = hiword - hmsb * 256;

//Aufspalten der 2. 16 Bit in 2 * 8 Bit
hlsb = loword / 256;
llsb = loword - hlsb * 256;

//Initialisierung, AD9835 in Sleepmode setzen
spi_start();
spi_send_word(0xF800);
spi_stop();

//Senden der 4 * 16 Bit
spi_start();
spi_send_word(0x33 * 0x100 + hmsb);
spi_stop();

spi_start();
spi_send_word(0x22 * 0x100 + lmsb);
spi_stop();

spi_start();
spi_send_word(0x31 * 0x100 + hlsb);
spi_stop();

spi_start();
spi_send_word(0x20 * 0x100 + llsb);
spi_stop();

//Sequenzende AD9835 aus Sleepmode wecken
spi_start();
spi_send_word(0xC000);
spi_stop();
}


int main()
{
unsigned long runseconds_old = 0;
unsigned int freq = 5000;

/* Ports einrichten */
/* OUTPUT */
DDRC = 0xFF; //LCD (RS und E) an PC0, PC1 / SPI PC2, PC3, PC4
DDRD = 0xF0; //LCD (Daten) an PD4...PD7

//JTAG abschalten
MCUCSR |= (1<<JTD);
MCUCSR |= (1<<JTD);

// Timer 0 fuer Sekundenzaehlung initialisieren, wird getaktet vom 32.768 kHz-Uhrenqurz
TIMSK &=~((1<<TOIE0)|(1<<OCIE0)); //TC0 Interrupt unterbinden
ASSR |= (1<<AS0); //Timer/Counter0 im Asynchronmodus mit Uhrenquarz.
TCNT0 = 0x00;
TCCR0 = 0x05; //Abgeleitet von f.xtal (CLK / 128) Taktrate
//für f=1/s definieren
while(ASSR&0x07); //Warten bis REgister Update durchlaufen hat
TIMSK |= (1<<TOIE0); //8-bit Timer/Counter0 Overflow Interrupt einschalten

sei();

usart_init(51); //9600 Bd 8N1

lcd_init();
_delay_ms(50);
lcd_putstring(0, 0, appstr);

runseconds_old = runseconds;

for(;;)
{
if(runseconds >= runseconds_old + 3)
{
set_frequency(freq);
lcd_putnumber(0, 0, freq, -1, -1, 'l', 0);
lcd_putstring(0, 4, " kHz OK.");
freq += 100;
if(freq > 5500)
{
freq = 5000;
}
runseconds_old = runseconds;
}
}


return 0;
}