Picture:Little Green is Teensy 3.1, Blue one is "generic" AD9850 board.Yes, this is Geegy Little Machine.

KISSB transmitter


Direct digital synthesis of SSB-voice transmission, with AD9850 and Teensy 3.1
SSBwithAD9850.JPG

Summary: Code for producing SSB-transmission with AD9850-dds board and Teensy 3.1.
Be warned: this is at early test stage. But certainly interesting

While KISSing Pig, I found thiscode fragment from Guido, PE1NNZ. I tried it first with AD9850-board and Teensy 3.1. Thanks Guido for sharing this!

Guido's idea was to phase modulate signal by altering frequency of "carrier". I first tested this idea with AD9850, and it worked, but it seemed to produce a bit noisy and wide signal. Then I modified code to directly shift phase in AD9850 (because AD9850 have ability for this. Guido's original work is meant for different hardware without this possibility). And it started to sound like transmitter. So here comes KISSB.

As Guido's original work, KISSB also uses "constant amplitude", in other words, no amplitude varying circuit for signal is needed. Also if rf-amplifier is used, it does not require high linearity like "true" SSB.
Quite surprisingly (for me), received constant amplitude SSB sounds just like normal voice. What a heck?! Why then is amplitude of normal SSB varied? Maybe answer is, it is not purposely varied, it varies because it is made by filtering AM-signal?
(I have some faint memory, that AM-component of SSB is beneficial on hard circumstances, something to do with "synchronous detection"...have to check that later.)

Hardware

You simply need Teensy 3.1 , cost about 20 USD (maybe Teensy 3 will do also?), "generic" AD9850 board (about 10 USD) and some kind of audio source, among with some obvious little parts (wires, power source, breadboard, and add what ever you are going to add: lcd, buttons, a hundred feet tall tower...)
You can test KISSB by just connecting short wire to output pin of AD9850. Tx power without amplification is around 10 mW.
To see what is AD9850 board, and how to connect wires etc, go to webshed of David Mill. It shows connection to Arduino, for Teensy 3.1 it is about the same.
You can see what pins of Teensy I used for connections to AD9850 at #defines on start of KISSB code. Modify these if needed.
With Teensy 3.1 you can use this simple serial connection like in webshed, it is fast enough. (I tested once, it will change frequency of AD9850 max 200000 times/second. With usual Arduino it is much much slower, not fast enough for FM-modulation for example (but if SPI is used you can do FM with ordinary Arduino...))

Audio is taken in from pin5. Modify constant AudioInPin if needed.
For testing, I have inserted audio from phone jack of FM-broadcast receiver, throw capacitor. I think it have voltage swing of roughly 2 volts. I am currently thinking of easy method to connect microphone, Teensy's analog in is versatile and constant amplitude transmission should make job relatively easy, there is no trouble of AM pickup on mic circuit.


Technical notes for fellow software amateurs


There is possibility to set sample rate...it should act like adjustable lowpass-filter...maybe. If KISSB is used at air, I suggest you to filter off high frequencies from audio signal before inserting it to KISSB.( And all the usual warnings about low-pass filtering and licences etc.)
(And ADC settings at setup() affects a lot. These are just ones.)

Generally, KISSB starts transmitting when AD9850 is initialized and timer1 is started with:
timer1.begin(timer1_callback, period1);
To stop modulating, call timer1.end(). You also have to shut down AD9850, otherwise it will continue producing unmodulated carrier. You can see example of how at kiSSB().
Timer will produce interrupt, which launches ADC measurement in certain periods. This period is derived from variable sample rate:
int period1 = int((1000000 / samplerate));
So if you change samplerate after scetch is started, you have to let period1 calculated again.
Varibale "samplerate" is needed for calculating phase at ssb(). Period1 is needed for timer setting. They must correspond to each ohers.

CPU load

I tried Teensy's CPU load with sample rate 10000 ("Normal" is 5000). Signal started to degrade after lowering CPU speed setting down to "24 Mhz optimized". (Max is 96 Mhz). So load is surprisingly low. Or tiny Teensy 3.1 is surprisingly strong.
So transmitting is interrupt-driven. As coarse CPU-load test suggest, there is lot of processing power available while producing SSB. To update sceen etc. from loop() while transmitting, or something else, you can modify code to be non-blocking. Now it blocks at example while() at kiSSB(). (It blinks a led as an example task when transmitting.)
To clarify: kiSSB() is just exampe for starting and stopping SSB-transmission. Do not hesitate to discard it..

Frequency range

Entire HF range, from bottom to 10 meter area should be fine. In other words, from 0-30 Mhz. Maybe up to 40 Mhz, but there is not much targets for using SSB between 30 and 40 Mhz.

LSB

For LSB (lower side band) you can find commented line at adc_isr(). Line itself is obvious, it just turns phase value to be negative. But handling negative phase at AD9850 driver is not much tested. (I have tried LSB with KISSB, it seems to work. What I mean, this is not much tested.)

Adding FM

Frequency modulation is "natively" amplitude constant, so adding it is easy. FM is achieved by chancing carrier frequency with variable "data":
  SetFrequency(txFrequency + data);//FOR FM
Reason this line is not in kiSSB(), is that you should first check that your audio is not producing too wide signal. If value of "data" (audio sample) varies too much, signal will be too wide. Ideally it should swing roughly between -1700 and + 1700 with maximum audio signal. In KISSB example code, variable data is available also inside loop().

Constant amplitude AM?

No, this is not a joke. Constant amplitude AM would be adorable. In reality it would be FM signal optimized for loop detection on AM-receivers. To avoid family distortion caused by FM squelch. And to avoid AM troubles on transmitter side, and FM troubles on receiver side. I think, with loop detection, only half of FM-signal is used. So why to transmit whole signal? Must try some day!

(Traditional) AM

You can use Analog Out of Teensy 3.1 for modulating carrier. Just write value of "data" to analog out and use signal for modulating your driver circuit. (Of course, you can ask why. Teensy just converts audio to digital and then back to analog. Well you could for example want to compress your mic signal. BTW you can use this analog out to debugging, listening your own audio signal with speakers.)
There is also application note available how to easily AM modulate AD9850. You can find this note easily by searching from net.

Trying out KISSB

Just copy-paste scetch below to Arduino Ide (with Teensyduino installed). Load to Teensy board, Insert audio signal and if You are lucky, you can hear sound at USB 29.000.000. Some important variables are defined above loop() for easy start.
This example code will transmit about 10 second, then shut down for 3 second, and this sequence will be repeated 10 times. When you code your own PTT-button code, you can start by putting it in while() inside kiSSB():

All the best!

Tarmo, OH6ECF


/*
This is KISSB, tiny SSB ("Single sideband") transmitter code (or signal source) from Tarmo Huttunen, OH6ECF.
KISSB is for Teensy 3.1 and "generic" AD9850 -board.
This is meant to be a starting point for fellow amateurs, to develop simply software defined transmitter.
There is lot of room for improvements. If you develop this further, please share you work.
Use this code as you like (but in legal and kind ways only).
 
Massive thanks to Guido, PE1NNZ, for his blogpost at http://pe1nnz.nl.eu.org/2013/05/direct-ssb-generation-on-pll.html
Code for produsing from audio SSB is derived from his code.
Note: This version uses "true" phase-altering on AD9850, not the "altering phase with altering frequency", which was Guidos idea on his blog.
(Because AD9850 have ability for that, Guidos original code is for a bit different use)
 
Also Thanks for Pedro Villanueva for Teensy ADC.library, audio in in this code is derived from ADC library example "AnalogReadIntervallTimer",
and for David Mills, G7UVW, for sharing driver code for AD9850, I just added phase-modulation function to his original work.
 
Done on Arduino Ide 1.6.4 (with Teensyduino loader 1.23)
*/
//AD9850 pins
#define DDS_CLOCK 125000000
//pin connections for AD9850 board
//Modify to suit pin numbers in connections you have
#define  CLOCK  18
#define  LOAD 17
#define  DATA  16
#define  RESET 15
 
#include "ADC.h"
#include <IntervalTimer.h>
 
ADC *adc = new ADC();
IntervalTimer timer1;
 
int startTimerValue1 = 0;
const int ledPin = LED_BUILTIN;
//Modify to suit pin you are using for ADC in
const int audioInPin = A5;
 
 
 
void setup() {
  //AD9850 pins
  pinMode(CLOCK, OUTPUT); digitalWrite(CLOCK, LOW);
  pinMode(LOAD, OUTPUT); digitalWrite(LOAD, LOW);
  pinMode(DATA, OUTPUT); digitalWrite(DATA, LOW);
  pinMode(RESET, OUTPUT); digitalWrite(RESET, LOW);
  pinMode(audioInPin, INPUT);
  pinMode(ledPin, OUTPUT);
  Serial.begin(9600);
  //delay(10);
  adc->setAveraging(8); // set number of averages
  adc->setResolution(16); // set bits of resolution
  // it can be ADC_VERY_LOW_SPEED, ADC_LOW_SPEED, ADC_MED_SPEED, ADC_HIGH_SPEED_16BITS, ADC_HIGH_SPEED or ADC_VERY_HIGH_SPEED
  // see the documentation for more information
  adc->setConversionSpeed(ADC_HIGH_SPEED); // change the conversion speed
  // it can be ADC_VERY_LOW_SPEED, ADC_LOW_SPEED, ADC_MED_SPEED, ADC_HIGH_SPEED or ADC_VERY_HIGH_SPEED
  adc->setSamplingSpeed(ADC_HIGH_SPEED); // change the sampling speed
  adc->enableInterrupts(ADC_0);
}
 
unsigned long txFrequency = 29000000;//set tx-frequency
float samplerate = 5000; // set "sample-rate"
int period1 = int((1000000 / samplerate)); // calculates interrupt timing (us) based on "samplerate"
int data;//audio sample value used in adc0_isr(), declared here to be used in loop() if needed
 
 
void loop() {
  //This while() only to demonstrate function
  int x;
  while (x < 10){
  kiSSB();
  x++;
  delay(3000);//While this delay, TX should be off. Check that carrier stops also
  }
}
 
 
 
float df, amp;
#define ln(x) (log(x)/log(2.718281828459045235f))
static long xv[45];
 
void filter(int val, long* i, long* q)
{
    int j;
    for (j = 0; j < 44; j++) {
        xv[j] = xv[j+1];
    }
    xv[44] = val;
    *i = xv[22];
    int _q = (xv[1] + xv[3] + xv[5] + xv[7]+xv[7] + 4*xv[9] + 6*xv[11] \
        + 9*xv[13] + 14*xv[15] + 23*xv[17] + 41*xv[19] + 127*xv[21] \
        - (127*xv[23] + 41*xv[25] + 23*xv[27] + 14*xv[29] + 9*xv[31] \
        + 6*xv[33] + 4*xv[35] + xv[37]+xv[37] + xv[39] + xv[41] + xv[43]) ) / 202;
  *q = _q;
}
 
int arctan2(int y, int x)
{
   long abs_y = abs(y);
   long angle;
   if(x >= 0){
      angle = 45 - 45 * (x - abs_y) / ((x + abs_y)==0?1:(x + abs_y));
   } else {
      angle = 135 - 45 * (x + abs_y) / ((abs_y - x)==0?1:(abs_y - x));
   }
   return (y < 0) ? -angle : angle; // negate if in quad III or IV
}
 
 
 static float t;
 //static float prev_f;
 static float prev_phase;
 //static float acc;
 float pphase;
 
void ssb(float in, float fsamp, float* amp, float* df)
{
   long i, q;
   t++;
   filter((in * 128), &i, &q);
   *amp = sqrt( i*i + q*q) / 128.0f;
   if(*amp > 1.0){
     //printf("amp overflow %f\n", *amp);
     *amp = 1.0;
   }
   pphase = M_PI + ((float)arctan2(q,i)) * M_PI/180.0f;
   float dp = pphase - prev_phase;
   if(dp < 0) dp = dp + 2*M_PI;
   prev_phase = pphase;
   *df = dp*fsamp/(2.0f*M_PI);
}
 
void kiSSB()
{
  timer1.begin(timer1_callback, period1);//This starts TX
  AD9850_reset();
  AD9850_init();
  Serial.println("TX (hopefully) on");
  byte seconds;
  //This 10 second "timer" is only for test tx start and stop. Surely you want to change this something that suits your usage
  while(seconds < 10) {
    if(startTimerValue1==false) {
      Serial.println("Timer1 setup failed");
    }
    digitalWriteFast(LED_BUILTIN, !digitalReadFast(LED_BUILTIN) );
    delay(1000);//Speed of led blinking
    seconds++;
  }
  timer1.end();//This stops modulation TX
  AD9850_reset();//This stops carrier from AD9850
}
 
 
 
// when the ADC measurement finishes, this will be called
//Generally, isr-functions should be very compact, this could be better.
void adc0_isr() {
      //I just get better signal when multiplying audio sample ("data2) with value 4 or 8. badBooster is simply multiplier for this
      //play with badBooster value to suit your use. I quess with ideal audio in signal, this should be 1 (and badBooster could be removed from code)
      byte badBooster = 4;
      //Value for audio in "center voltage"
      //When no audio inserted, in my case, "data" was around -5400
      int adcCenter = 5400;
      // It would be better to adjust your audio in way that no badBooster and adcCenter are needed, and remove these variables
      uint8_t pin = ADC::sc1a2channelADC0[ADC0_SC1A&ADC_SC1A_CHANNELS]; // the bits 0-4 of ADC0_SC1A have the channel
      data = adc->readSingle();
      ssb((float)(badBooster * (data + adcCenter)/32767.0), samplerate, &amp, &df);
 
     /*If you need measure power for amplitude variation (this from original Guido's code):
      float A = 87.7f; // compression parameter
      amp = (fabs(amp) < 1.0f/A) ? A*fabs(amp)/(1.0f+ln(A)) : (1.0f+ln(A*fabs(amp)))/(1.0f+ln(A)); //compand
      int ampval = (int)(round(amp * 8.0f)) - 1;
     */
 
       SetPhase(txFrequency, pphase);//FOR USB
       //SetPhase(txFrequency, -pphase);//FOR LSB
 
}
// This function will be called with the desired interrupt frequency
// start the measurement
void timer1_callback(void) {
    adc->startSingleRead(audioInPin, ADC_0);
}
 
 
 
///////////////AD9850 DRIVER CODE/////////
//Driver code is derived from example shown at http://webshed.org/wiki/AD9850_Arduino
//Thanks to David Mills, G7UVW, for information shared on his webshed!
//I (OH6ECF) have simply added frequency fine tuning and phase modulation functions to David's code
 
//Basic frequency set function
void SetFrequency(unsigned long hertz)
{
  unsigned long tuning_word = (hertz * pow(2, 32)) / DDS_CLOCK;
 
  digitalWrite (LOAD, LOW);
  shiftOut(DATA, CLOCK, LSBFIRST, tuning_word);
  shiftOut(DATA, CLOCK, LSBFIRST, tuning_word >> 8);
  shiftOut(DATA, CLOCK, LSBFIRST, tuning_word >> 16);
  shiftOut(DATA, CLOCK, LSBFIRST, tuning_word >> 24);
  shiftOut(DATA, CLOCK, LSBFIRST, 0x0);
  digitalWrite (LOAD, HIGH);
}
 
//Frequency set with added precicion. Will try set frequency to hertz + subhertz
//For example SetFrequency(25000000, 0.5) will set near 25000000.5 hz.
void SetFrequency(unsigned long hertz, float subhertz)
 
{
  unsigned long tuning_hertz = (hertz * pow(2, 32)) / DDS_CLOCK;
  unsigned long tuning_subhertz = (subhertz * pow(2, 32)) / DDS_CLOCK;
  unsigned long tuning_word = tuning_hertz + tuning_subhertz;
 
  digitalWrite (LOAD, LOW);
  shiftOut(DATA, CLOCK, LSBFIRST, tuning_word);
  shiftOut(DATA, CLOCK, LSBFIRST, tuning_word >> 8);
  shiftOut(DATA, CLOCK, LSBFIRST, tuning_word >> 16);
  shiftOut(DATA, CLOCK, LSBFIRST, tuning_word >> 24);
  shiftOut(DATA, CLOCK, LSBFIRST, 0x0);
  digitalWrite (LOAD, HIGH);
}
//For setting phase of AD9850
//Float phase = phase in radians
 
void SetPhase(unsigned long hertz, float phase)
{
  //Most likely, when modulating phase, frequency will be same, so first check if there is need to calculate tuning word
  static unsigned long tuning_word;
  static unsigned long previous_hertz;
  if (previous_hertz != hertz){
    tuning_word = (hertz * pow(2, 32)) / DDS_CLOCK;
    previous_hertz = hertz;
  }
  //If phase is negative (doing LSB) , we convert it to be corresponding "Upsidedown" positive
  //negative over 360 degree might be problem?
  if (phase < 0){
    phase = (2 * M_PI + phase);
  }
  //Calculate AD9850 phase shifting bits
 //This is in case we get over 360 deg phase shift
  while (phase > (2 * M_PI)){
    phase -= 2 * M_PI;
  }
 
 
  byte phaseBits = 0;
 
  if (phase > M_PI){
    bitSet(phaseBits,7);
    phase -= M_PI;
  }
  if (phase > (M_PI / 2)){
    bitSet(phaseBits,6);
    phase -= (M_PI / 2);
  }
  if (phase > (M_PI / 4)){
    bitSet(phaseBits,5);
    phase -= (M_PI / 4);
  }
  if (phase > (M_PI / 8)){
    bitSet(phaseBits,4);
    phase -= (M_PI / 8);
  }
  if (phase > (M_PI / 16)){
    bitSet(phaseBits,3);
  }
 
  digitalWrite (LOAD, LOW);
  shiftOut(DATA, CLOCK, LSBFIRST, tuning_word);
  shiftOut(DATA, CLOCK, LSBFIRST, tuning_word >> 8);
  shiftOut(DATA, CLOCK, LSBFIRST, tuning_word >> 16);
  shiftOut(DATA, CLOCK, LSBFIRST, tuning_word >> 24);
  shiftOut(DATA, CLOCK, LSBFIRST, phaseBits);
  digitalWrite (LOAD, HIGH);
}
 
void AD9850_init()
{
  digitalWrite(RESET, LOW);
  digitalWrite(CLOCK, LOW);
  digitalWrite(LOAD, LOW);
  digitalWrite(DATA, LOW);
}
 
void AD9850_reset()
{
  //reset sequence is:
  // CLOCK & LOAD = LOW
  //  Pulse RESET high for a few uS (use 5 uS here)
  //  Pulse CLOCK high for a few uS (use 5 uS here)
  //  Set DATA to ZERO and pulse LOAD for a few uS (use 5 uS here)
 
  // data sheet diagrams show only RESET and CLOCK being used to reset the device, but I see no output unless I also
  // toggle the LOAD line here.
 
  digitalWrite(CLOCK, LOW);
  digitalWrite(LOAD, LOW);
 
  digitalWrite(RESET, LOW);
  delay(5);
  digitalWrite(RESET, HIGH);  //pulse RESET
  delay(5);
  digitalWrite(RESET, LOW);
  delay(5);
 
  digitalWrite(CLOCK, LOW);
  delay(5);
  digitalWrite(CLOCK, HIGH);  //pulse CLOCK
  delay(5);
  digitalWrite(CLOCK, LOW);
  delay(5);
  digitalWrite(DATA, LOW);    //make sure DATA pin is LOW
 
    digitalWrite(LOAD, LOW);
  delay(5);
  digitalWrite(LOAD, HIGH);  //pulse LOAD
  delay(5);
  digitalWrite(LOAD, LOW);
  // Chip is RESET now
}
 
////////////////AD9850 DRIVER CODE END/////////////
 
//KEEP IT KISS


[[code]]
 
 

code


Placeholde
r