QRP SSB handheld transceiver for 14 MHz (20 Meter)

On my radiotransmitter-blog I’ve postet an article about my latest project: A compact handheld QRP transceiver for SSB with self-containing battery pack for portable use:

QRP SSB handheld transceiver by Peter Rachow (DK7IH) (C) 2015

QRP SSB handheld transceiver by Peter Rachow (DK7IH) (C) 2015

Read more…

Peter Rachow, DK7IH: A 14 MHz (20 meter) SSB transceiver

Today I’d like to present my new homemade 14-MHZ-SSB-Transceiver to my fellow radio amateurs. Some basic information should be given first before details and circuits are to be discussed:
Frequency range: 14,000 kHz to 14,350 kHz
Transmit power: 15 to 20 Watts max.
Mode: SSB only.

The main units of this transceiver are:

  • SSB-Generator: LM358 operational amplifier as microfone amplifier and MC1496 Gilbert-cell mixer for signal production.
  • TX-Mixer: MC1496
  • Transmitter consisting of 4 stages: 1 preamplifier, 1 predriver, 1 driver, 1 final (push-pull), all equipped with bipolar transistors
  • Receiver: single conversion, interfrequency 9.785 MHz, 1 rf preamplifier (dual gate MOSFET) followd by  mixer (dual gate MOSFET), SSB-filter, if-pre and if-main-amplifier (MC1350), product detector (CA3028A), audio preamp, audio final amp (LM 386)
  • Frequency generation: DDS with ATMega32 driving an AD 9835
  • S-Meter and AGC-circuitry: ATMega8 driving led-chain for S and RF presentation an r2r-network with op for generating agc voltage.

Block diagram:

SSB Transceiver for 14 Mhz (20mtr) by Peter Rachow (DK7IH) - Block diagram

SSB Transceiver for 14 Mhz (20mtr) by Peter Rachow (DK7IH) – Block diagram

Front view:

SSB Transceiver for 14 Mhz (20mtr) by Peter Rachow (DK7IH) - Front view

SSB Transceiver for 14 Mhz (20mtr) by Peter Rachow (DK7IH) – Front view (OK, it’s not 100% QRP… 😉 )

Interior view:

SSB Transceiver for 14 Mhz (20mtr) by Peter Rachow (DK7IH) - Interior view

SSB Transceiver for 14 Mhz (20mtr) by Peter Rachow (DK7IH) – Interior view

To be continued…

(C) 2014 by Peter Rachow, DK7IH

Programmierung des AD9835 DDS Chips

Nachdem ich mich nun vertiefter mit der Programmierung des DDS-Bausteins AD9835 von Analog Devices beschäftigte, habe ich auf meiner Webseite unter dem URL

http://peterrachow.scienceontheweb.net/programming-ad9835.htm

eine genauere Anleitung abgelegt, wie die Frequenzeinstellung bei diesem Bauteil vorgenommen wird.

After having explored the AD9835 DDS chip more intesively I posted a detailed description of the neceessary programming procdures for setting frequencies with this device on my website

http://peterrachow.scienceontheweb.net/programming-ad9835.htm

including code examples in C programming language. Unfortunately the article is in German only but code examples are commented in English language.

So, have fun using this essay!

Variabler Frequenzoszillator mit DDS AD9835 (Peter Rachow 2014)

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. Peter Rachow  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 Peter Rachow 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 (C) 2014 Peter Rachow

VFO mit DDS-Chip AD9835 (C) 2014 Peter Rachow

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

}
// Peter Rachow 
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   */
//Peter Rachow 
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;
}