Monday, 26 June 2017

BASIC Tech Group - MyNews - 48 GPS working

In the last post I described how I wanted to build a GPS input to my VFO, and use this to display my Locator, from the Lat & Lon data, and calibrate the time and date of my RTC. There is a little progress I can report. the VFO has a new 3.5mm socket on the back to connect to the GPS. After a mixup, I have solved the problems of getting the right connections to the GPS and to the Arduino. Arduino pin 12 is the data input from the GPS, connected to the GPS TX output, and pin 13 is connected to the GPS RX input.

IMG 1221

VFO with 3.5mm socket for GPS input

I have some code working - see below.

Power up the VFO by USB. Then plug in the GPS. Press the front button, and if there is a GPS FIX the RTC will be programmed. The display will show the RTC date and time.

CODE

// DATE_TIME_SET_GPS
// V1.5 read GPS time, fix & date. Convert to hh mm ss & yy mm dd bytes
// if fix, calculate dow & program RTC.
// display GPS date & time
// GPS VK-163 jack wiring
// 1 tip  VCC (Y)
// 2      TX GPS (R)  output
// 3      RX GPS (OR) input
// 4 ring GND (BWN)

#include "Oled_128X64_I2C.h"
#include "SoftwareSerial.h"
#include "Wire.h"

// Arduino pins TX GPS -> RX(12), RX GPS <- TX(13)
#define RX 12
#define TX 13

// button
#define SW 4

// RTC address
#define RTCADDR 0x68

// GPS data buffer
char gpsbuf[200];

// fields extracted from GPS message ($GPRMC)
// tm 1, fix 2, date 9
char tm[20];        // time HHMMSS
char fix[5];        // fix A|V, init void
char dt[20];        // date YYMMDD

// RTC data
byte hrs, mns, sec;
byte yr, mth, dy;
byte dow;

// Serial object
SoftwareSerial gps(RX, TX);

// ========= setup =========

void setup() {
  pinMode(RX, INPUT);
  pinMode(TX, OUTPUT);
  pinMode(SW, INPUT_PULLUP);

  // I2C init
  Wire.begin();

  // OLED init, sets I2C addr to 0x3C
  oled.begin();

  // GPS software serial init
  gps.begin(9600);

  // init no fix
  strcpy(fix, "V");
}

// ========= loop =========
void loop() {
  // no fix until GPS valid read
  if (digitalRead(SW) == LOW) {
    getGPS();
    // if fix
    if (strcmp(fix, "A")) { // fix
      setRTC();
    }
    else
      strcpy(fix, "V"); // still no fix
  }
  getRTC();
  dispUpdate();
}

// ========= functions =========
void getGPS() {
  do {
    getline(gpsbuf);
  } while (strncmp(gpsbuf, "$GPRMC", 6) != 0);

  // extract time, fix and date strings from $GPRMC fields
  xtract(gpsbuf, 1, tm);          // time HHMMSS
  xtract(gpsbuf, 2, fix);         // fix A or V
  xtract(gpsbuf, 9, dt);          // date YYMMDD
}

// get a line from the GPS, inc /r/n, add /0
// if no data return false
void getline(char *buf) {
  char c;
  int p;

  p = 0;                         // buffer pointer
  do {
    if (gps.available() > 0) {   // data?
      c = gps.read();            // read character
      buf[p++] = c;              // put in buffer
    }
  } while ( c != '\n');          // stop on /n
  buf[p] = '\0';                 // terminate string
}

// extract field and return string in outbuf
void xtract(char *in, int field, char *out) {
  int ip = 0;                    // input buffer pointer
  int op = 0;                    // output buffer pointer
  int f = 0;                     // field counter

  while (f < field) {            // find start of field, ip
    while (in[ip++] != ',');
    f++;
  }

  while (in[ip] != ',')  {      // scan to next ','
    out[op++] = in[ip++];       // copy in to out
  }
  out[op] = '/0';               // terminate out string
}

// convert 2 char ASCII (0-99), starting at bp, to byte
byte strtob(char *in, int bp) {
  char out[20];

  strncpy(out, in + bp, 2);     // copy 2 char
  return (byte)atoi(out);       // return byte
}

// calculate dow from GPS date string "YYMMDD"
byte calcDow(char *str) {
  int yy, mm, dd, n;
  uint32_t days;
  uint16_t febs;
  uint16_t months[] = {
    0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 // days until 1st of month
  };
  char buf[5];

  // extract yy, mm dd strings, convert to int
  strncpy(buf, str, 2);
  yy = 2000 + atoi(buf);                 // need yyyy
  strncpy(buf, str + 2, 2);
  mm = atoi(buf);
  strncpy(buf, str + 4, 2);
  dd = atoi(buf);

  // calc dow
  days = yy * 365;                      // days from 0000 to current year, ignoring leap years

  febs = yy;
  if (mm > 2) febs++;                   // number of completed Februaries

  // add in the leap days
  days += ((febs + 3) / 4);
  days -= ((febs + 99) / 100);
  days += ((febs + 399) / 400);

  // add in today
  days += months[mm - 1] + dd;

  // now we have day number such that 0000-01-01(Sat) is day 1, add 1 for Sun = 1
  return ((dd + 5) % 7) + 1; // +1 for Sun = 1 ... Sat = 7
}

// set date and time bytes to RTC BCD
void setRTC() {
  // get GPS data in bytes
  hrs = strtob(tm, 0);           // HH....
  mns = strtob(tm, 2);           // ..MM..
  sec = strtob(tm, 4);           // ....SS
  dy  = strtob(dt, 0);           // DD....
  mth = strtob(dt, 2);           // ..MM..
  yr  = strtob(dt, 4);           // ....YY
  dow = calcDow(dt);

  // program RTC
  Wire.beginTransmission(RTCADDR);
  Wire.write(0);               // next input at sec register

  Wire.write(decToBcd(sec));   // set seconds
  Wire.write(decToBcd(mns));   // set minutes
  Wire.write(decToBcd(hrs));   // set hours
  Wire.write(decToBcd(dow));   // set day of week
  Wire.write(decToBcd(dy));    // set date (1 to 31)
  Wire.write(decToBcd(mth));   // set month (1-12)
  Wire.write(decToBcd(yr));    // set year (0 to 99)
  Wire.endTransmission();
}

// Convert decimal to BCD
byte decToBcd(byte dec)
{
  return ( (dec / 10 * 16) + (dec % 10) );
}

// get time from RTC, convert bcd to decimal
void getRTC() {
  // Reset the RTC register pointer
  Wire.beginTransmission(RTCADDR);
  Wire.write(0);
  Wire.endTransmission();

  // request 7 bytes from the RTC address
  Wire.requestFrom(RTCADDR, 7);

  // get the time data
  sec = bcdToDec(Wire.read()); // 0 - 59
  mns = bcdToDec(Wire.read()); // 0 - 59
  hrs = bcdToDec(Wire.read() & 0b111111); // mask 12/24 bit
  dow = bcdToDec(Wire.read()); //0 - 6 = Sunday - Saturday
  dy  = bcdToDec(Wire.read()); // 1 - 31
  mth = bcdToDec(Wire.read()); // 0 = jan
  yr  = bcdToDec(Wire.read()); // 20xx
}


// Convert binary coded decimal to normal decimal numbers
byte bcdToDec(byte val) {
  return ( (val / 16 * 10) + (val % 16) );
}

// picture loop
void dispUpdate() {
  oled.firstPage();
  do {
    dispMsg(55, 0, "GPS");
    if (strcmp(fix, "V") == 0) {                  // no fix
      dispMsgL(30, 15, "NO FIX");
    }
    else {
      dispMsgL(30, 15, "GPS OK"); // acquired disp date time
      dispDate(15, 32, dow, dy, mth, yr);
      dispTimeL(25, 45, hrs, mns, sec);
    }
  } while ( oled.nextPage() );
}

Saturday, 24 June 2017

BASIC Tech Group - MyNews - 47 GPS time and locator

My ADS (analog Digital Synthesiser) built using an Arduino UNO and an AD9851 chip includes a RTC DS3231 with a back up battery. The intention is to have UTC time available to software running on the Arduino UNO. So that it can generate correctly timed WSPR and JT65 output. It also includes a MMIC amplifier to output up to 10mW into 50R.

IMG 1084

Internal view of the VFO

TIME

At the moment I have a special Arduino sketch "DATE_TIME_SET_OLED" - see below - which I use to set the date and time in the RTC, it needs you to enter the date & time in the format YYMMDDWHHMMSS (W = day of week, Sunday = 0) in the Arduino Monitor window, then hit "Send" at exactly the right moment to set the correct time. Obviously this is a bit hit-and-miss, and also relies on your Computer displaying the right time to the second (my MacBook does this automatically by reading time from time.apple.com, but I have had trouble with my Windows PC which loses lock).

Anyway it seemed to me that by providing a new input/output connection to the Arduino UNO in the VFO I could send in information from a GPS receiver, extract the date and time and automatically calibrate the internal RTC, at maybe a long push of the Encoder Button. I could then also use the same connection to output a couple of signals to any external device when the GPS is not connected...

GPS RECEIVER

First a GPS receiver, I searched the internet and found a very low cost solution - GPS receivers that are targeted at car navigation and dash board cameras market. Like this one, VK-163 G-Mouse Headphone Wire Interface Navigation GPS,

Screen Shot 2017 06 24 at 14 50 36

GPS Receiver

It has a 4 pin 3.5mm jack plug. After a considerable time fussing about with it I discovered the connections,

Jack tip = VCC 3.6-5V

ring = GPS RX Input (configuration input commands)

ring = GPS TX output (NMEA output data strings)

Jack shaft = Ground

And I wired it up to an Arduino UNO. />
IMG 1220

GPS wiring to Arduino UNO

The Arduino connections I used can be read in the sketch below. Basically pin 12 for data coming in, and pin 13 for any commands I want to send out. Though I found the GPS works out of the box with 1 second updated outputs without giving any new commands, so I won't use pin 13 in my software.

The results are that I can read the NMEA message "$GPRMC" on the serial monitor like this,

Screen Shot 2017 06 24 at 14 34 11

GPS NMEA ASCII string data for the $GPRMC message ID

Below are the sketches code for the manual set time and the GPS reception. Now all I have to do is extract the time and date info from the GPS string and program the RTC...

CODE

// DATE_TIME_SET_OLED
// V1.0 9-5-17 does not use DS3231 library
// enter YYMMDDwHHMMSS, reset/reload to repeat
// w = day-of-week 1 = mon, 01 = Jan 17 = 2017, 24 hour clock
// RTC
// SDA A4
// SCL A5
// SW 4

#include "Wire.h"
#include "Oled_128X64_I2C.h"

// RTC address
#define RTCADDR 0x68

// RTC time and date
byte doW, date, month, year;
byte hrs, mns, sec;

bool gotString;

void setup() {
  Serial.begin(9600);
  
  Wire.begin();

  oled.begin();

  gotString = false;
  
  dispUpdate();
}

void loop() {
  char inString[20] = "";
  byte j = 0;

  while (!gotString) {
    if (Serial.available()) {
      inString[j] = Serial.read();

      if (inString[j] == '\n') {
        gotString = true;

        // convert ASCII codes to bytes
        year = ((byte)inString[0] - 48) * 10 + (byte)inString[1] - 48;
        month = ((byte)inString[2] - 48) * 10 + (byte)inString[3] - 48;
        date = ((byte)inString[4] - 48) * 10 + (byte)inString[5] - 48;
        doW = ((byte)inString[6] - 48);
        hrs = ((byte)inString[7] - 48) * 10 + (byte)inString[8] - 48;
        mns = ((byte)inString[9] - 48) * 10 + (byte)inString[10] - 48;
        sec = ((byte)inString[11] - 48) * 10 + (byte)inString[12] - 48;

        setRTC();
      }
      j += 1;
    }
  }

  getRTC(); // get time
  
  dispUpdate();
}
  
// set the time int he RTC
void setRTC()
{
  // sets time and date data to DS3231
  Wire.beginTransmission(RTCADDR);
  Wire.write(0); // set next input to start at the sec register
  
  Wire.write(decToBcd(sec)); // set seconds
  Wire.write(decToBcd(mns)); // set minutes
  Wire.write(decToBcd(hrs)); // set hours
  Wire.write(decToBcd(doW)); // set day of week (1=Sunday, 7=Saturday)
  Wire.write(decToBcd(date)); // set date (1 to 31)
  Wire.write(decToBcd(month)); // set month
  Wire.write(decToBcd(year)); // set year (0 to 99)
  Wire.endTransmission();
}

// get time from RTC, convert bcd to decimal
void getRTC() {
  // Reset the RTC register pointer
  Wire.beginTransmission(RTCADDR);
  Wire.write(0x00); // output start at sec register
  Wire.endTransmission();

  // request 7 bytes from the RTC address
  Wire.requestFrom(RTCADDR, 7);

  // get the time data
  sec = bcdToDec(Wire.read()); // 0 - 59
  mns = bcdToDec(Wire.read()); // 0 - 59
  hrs = bcdToDec(Wire.read() & 0b111111); //mask 12/24 bit
  doW = bcdToDec(Wire.read()); //0 - 6 = Sunday - Saturday
  date = bcdToDec(Wire.read()); // 1 - 31
  month = bcdToDec(Wire.read()); // 0 = jan
  year = bcdToDec(Wire.read()); // 20xx
}

// Convert normal decimal numbers to binary coded decimal
byte decToBcd(byte val)
{
  return( (val/10*16) + (val%10) );
}

// Convert binary coded decimal to normal decimal numbers
byte bcdToDec(byte val)
{
  return( (val/16*10) + (val%16) );
}

// picture loop, display init data
void dispUpdate() {
  oled.firstPage();
  do {
    if (gotString == true) {
      dispDate(15, 15, doW, date, month, year);
      dispTimeL(25, 40, hrs, mns, sec);
    }
    else {
      dispMsg(0, 15, ">> YYMMDDwHHMMSS");
    }
  } while ( oled.nextPage() );
}


MORE CODE
// GPS_READ_MSG_PRINT
// V0.4 basics of read GPS & locator
// Jack plug/socket wiring
// tip  VCC (Y)
// 2    TX GPS (R)
// 3    RX GPS (OR)
// ring GND (BWN)

#include "SoftwareSerial.h"

// connections GPSRX -> RX, GPSTX <- TX
#define RX 12
#define TX 13

// GPS data buffer, gps message ID
char gpsbuf[200];
char MSGID[10] = "$GPRMC";

// jack GPS 3(TX) -> RX, GPS 2(RX) <- TX
SoftwareSerial gps (RX, TX);

void setup() {
  pinMode(RX, INPUT);
  pinMode(TX, OUTPUT);

  Serial.begin(9600);

  gps.begin(9600); // start GPS serial

  Serial.println("Start");
}

void loop() {
  // read MSGID line
  do {
    getline(gpsbuf);
  } while (strncmp(gpsbuf, MSGID, 6) != 0);
  
  Serial.print(gpsbuf);
}

// get a line from the GPS, inc /r/n, add /0
void getline(char *buf) {
  char c;
  int p = 0;

  do {
    if (gps.available() > 0) {
      c = gps.read();
      buf[p++] = c;
    }
  } while ( c != '\n');
  buf[p] = '\0';
}

Friday, 9 June 2017

BASIC Tech Group - MyNews - 46 THE PIXIE CHALLENGE

The PIXIE CHALLENGE

This should be fun! On eBay you will find lots of very low cost kits for a 40m Transceiver called the "Pixie". This is a simple two transistor - Oscillator and PA, and a receiver - using the PA transistor as an amplifier followed by a diode detector and LM386 amplifier IC. It is a cute and interesting design.

It took me about 3 hours to sort out the components and identify the resistors, capacitors and coils (looking like RF chokes), then to build the board. It needs a morse key, a headphone or external amplifier and loudspeaker, a 9-12V supply (I used 6 x AA batteries, you can also use a simple PP3) and an antenna. I attached a 2m length of wire as an antenna - as the challenge is intended to make a contact over a short distance - a few tens of metres. It is also better to have ground connection or radial.

Take note that from my measurements the TX on 7023kHz has lots of harmonics, for example the second harmonic is less than 30dB down, which is poor and probably not legal. The RX also seems to radiate a low level signal at the RIT higher frequency.

THE CHALLENGE

The members of the Banbury Amateur Radio Society (BARS) will be challenged to take two of our "Constructor" evenings to each build a Pixie, get it going and make a CW QSO - minimum exchange of call signs and reports with acknowledgements. First couple to make a QSO will get a prize. Simple QSO might be:

CQ DE G3YWX K
G3YWX DE G3QAB KN
G3QAB DE G3YWX UR 599 K
R UR RST 599 K
R 599 SK
To set up this challenge I purchased one of the Pixie kits here. and it arrived in a couple of weeks. The circuit is a xtal oscillator RIT tuneable a kHz or so from the XTAL frequency of 7023kHz by a varicap diode. I built it and first tested the RX using my Arduino AD9851 VFO on a frequency of 7023.00kHz.

IMG 1197

Battery (6 x AA), morse key connection, audio output and antenna. And my VFO in the small blackbox.

The RX seems to be reasonably sensitive, but an external audio amplifier is a good idea. Next I tested the TX, and connected the antenna output to my RF Meter capable of measuring RF power from a few mW to 10W.

IMG 1199

The output was around 780mW into a 50R dummy load.

Arduino keyer

Now I am lazy about morse code (and terrible at it, as are other members of BARS - thus the challenge), but I wrote a short sketch for an Arduino Uno to send a fixed short text message or a message you type in, automatically. The Arduino controls a relay from one of its outputs which in turn keys the Pixie TX.

IMG 1203

The reception was by my Elektor SDR feeding the HDSDR software, with its audio output fed to the Argo spectrum display software.

IMG 1205

IMG 1204

You can read the morse message in the Argo window.

Both software programs are running on my very low cost (£180) Windows 10 PC! I used a low cost 96kHz USB analog/digital convertor.

CODE
// PIXIE_MORSE - relay driver for sending morse message
// V1.1 9-5-17
// thanks to F0GOJ for some of the varicode
// Output to a relay, HIGH = TX
// board LED also on pin
// RELAY < PTT (5)

// relay pin
#define RELAY 5

//speed WPM
#define WPM 5

int repeat = 10000; // erpeat in 10 secs

// message to send
char msg[] = "SECRET MESSAGE GOES HERE";

// morse varicode MSB 1st, and length
byte morseVaricode[2][59] = {
  { 0, 212, 72, 0, 144, 0, 128, 120, 176, 180,
    0, 80, 204, 132, 84, 144, 248, 120, 56, 24,
    8, 0, 128, 192, 224, 240, 224, 168, 0, 136,
    0, 48, 104, 64, 128, 160, 128, 0, 32, 192,
    0, 0, 112, 160, 64, 192, 128, 224, 96, 208,
    64, 0, 128, 32, 16, 96, 144, 176, 192
  },
  { 7, 6, 5, 0, 4, 0, 4, 6, 5, 6,
    0, 5, 6, 6, 6, 5, 5, 5, 5, 5,
    5, 5, 5, 5, 5, 5, 6, 6, 0, 5,
    0, 6, 6, 2, 4, 4, 3, 1, 4, 3,
    4, 2, 4, 3, 4, 2, 2, 3, 4, 4,
    3, 3, 1, 3, 4, 3, 4, 4, 4
  }
};

void setup() {
  // relay output
  pinMode(RELAY, OUTPUT);

  // delay before start
  delay(repeat);
}

void loop() {
  sendMsg(msg);            // send CW message
  delay(repeat);           // repeat
}

// send message at wpm
void sendMsg(char *m) {
  bool val;
  byte c, n, ndx, bits, vCode;;
  int dotTime, dashTime;

  // calculate dot time
  dotTime = 1200 / WPM;                           // Duration of 1 dot
  dashTime = 3 * dotTime;                         // and dash

  //send msg in morse code
  c = 0;
  while (m[c] != '\0') {
    m[c] = toupper(m[c]);                        // u.c.just in case

    if (m[c] == ' ') {                           // catch ASCII SP
      delay(7 * dotTime);
    }
    else if (m[c] > ' ' && m[c] <= 'Z') {
      ndx = m[c] - ' ';                         // index to varicode 0-58

      vCode = morseVaricode[0][ndx];            // get CW varicode data
      bits = morseVaricode[1][ndx];             // get CW varicode length

      if (bits != 0) {                          // if not characters # % < >
        for (n = 7; n > (7 - bits); n--) {      // Send CW character, MSB(bit 7) 1st
                                                // 0 for dot, 1 for dash
          val = bitRead(vCode, n);              // look up varicode bit

          digitalWrite(RELAY, HIGH);            // send dot or dash
          if (val == 1) delay(dashTime);
          else delay(dotTime);
          digitalWrite(RELAY, LOW);

          delay(dotTime);                       // for 1 dot space between dots|dashes
        }
      }
      delay(dashTime);                          // 1 dash space between characters in a word
    }
    c++;                                        // next character in string
  }
}

The next code needs the Arduino to be connected to a serial terminal program, you can use the "serial monitor" of the Arduino IDE or your own terminal program - I use "iSerialTerm" on my MacBook.

MORE CODE
// PIXIE_MORSE_TEXT - relay driver for sending morse message
// V1.1 16-6-17
// thanks to F0GOJ for some of the varicode
// Output to a relay, HIGH = TX
// board LED also on pin
// RELAY 5 PTT

// relay pin
#define RELAY 5

//speed WPM
#define WPM 5

// message to send
char msg[40];

// morse varicode MSB 1st, and length
byte morseVaricode[2][59] = {
  { 0, 212, 72, 0, 144, 0, 128, 120, 176, 180,
    0, 80, 204, 132, 84, 144, 248, 120, 56, 24,
    8, 0, 128, 192, 224, 240, 224, 168, 0, 136,
    0, 48, 104, 64, 128, 160, 128, 0, 32, 192,
    0, 0, 112, 160, 64, 192, 128, 224, 96, 208,
    64, 0, 128, 32, 16, 96, 144, 176, 192
  },
  { 7, 6, 5, 0, 4, 0, 4, 6, 5, 6,
    0, 5, 6, 6, 6, 5, 5, 5, 5, 5,
    5, 5, 5, 5, 5, 5, 6, 6, 0, 5,
    0, 6, 6, 2, 4, 4, 3, 1, 4, 3,
    4, 2, 4, 3, 4, 2, 2, 3, 4, 4,
    3, 3, 1, 3, 4, 3, 4, 4, 4
  }
};

void setup() {
  // Serial
  Serial.begin(9600);

  // relay output
  pinMode(RELAY, OUTPUT);
}

void loop() {
  if (getMsg(msg) == true) {
    Serial.println(msg);
    sendMsg(msg);            // send CW message
  }
  clearBuf(msg);
}

// get input msg[] U.C.
bool getMsg(char *m)
{
  char ch;
  int n;

  n = 0;
  if (Serial.available() > 0) {      // if input
    delay(20);                       // let USB catch up
    while (Serial.available() > 0) { // get input
      ch = Serial.read();            // use upper case as input
      if (ch == '\n') ch = '\0';     // end of text
      m[n++] = ch;
      delay(20);                     // let USB catch up
    }
    return true;                     // got input
  }
  return false;                      // no input
}

// clear msg and buffer
void clearBuf(char *m) {
  m[0] = '\0';
  while (Serial.available() > 0) Serial.read();
}

// send message at wpm
void sendMsg(char *m) {
  bool val;
  byte c, n, ndx, bits, vCode;;
  int dotTime, dashTime;

  // calculate dot time
  dotTime = 1200 / WPM;                           // Duration of 1 dot
  dashTime = 3 * dotTime;                         // and dash

  //send msg in morse code
  c = 0;
  while (m[c] != '\0') {
    m[c] = toupper(m[c]);                        // u.c.just in case

    if (m[c] == ' ') {                           // catch ASCII SP
      delay(7 * dotTime);
    }
    else if (m[c] > ' ' && m[c] <= 'Z') {
      ndx = m[c] - ' ';                         // index to varicode 0-58

      vCode = morseVaricode[0][ndx];            // get CW varicode data
      bits = morseVaricode[1][ndx];             // get CW varicode length

      if (bits != 0) {                          // if not characters # % < >
        for (n = 7; n > (7 - bits); n--) {      // Send CW character, MSB(bit 7) 1st
          // 0 for dot, 1 for dash
          val = bitRead(vCode, n);              // look up varicode bit

          digitalWrite(RELAY, HIGH);            // send dot or dash
          if (val == 1) delay(dashTime);
          else delay(dotTime);
          digitalWrite(RELAY, LOW);

          delay(dotTime);                       // for 1 dot space between dots|dashes
        }
      }
      delay(dashTime);                          // 1 dash space between characters in a word
    }
    c++;                                        // next character in string
  }
}

Friday, 26 May 2017

BASIC Tech Group - MyNews - 45 Testing PA, WSPR, more on SWR

Finally the 20dB attenuator I ordered from China has arrived. I need this as even the lowest output setting of my VFO is too much drive power for the small 3W PA amplifier I have. So now I can turn it down to get the PA operating in its linear range. So I have been trying agin to send WSPR weak signals and am looking for a result on WSPRnet.org.

IMG 1095

Here's the set up, from right to left

- Windows PC (I am a Mac man, but this seems not so bad) running HDSDR and WSTJ software, using VB to pipe audio from one to the other.

- The SDR receiver, which is an Elektor SDR Arduino shield and an Arudino Uno.

- The VFO, which has an AD9851, RTC (using a DS3231) and a buffer amp. Software is loaded to run WSPR on 40m at 7040.1kHz, the 20dB attenuator is plugged in the back of the VFO.

The small PA amplifier module, from eBay, running 2W output from a supply of 13.8V

IMG 1097

More on SWR

In the last post I talked about an SWR meter using a normal bridge circuit. The output from the bridge FWD & REF RF was handled by two AD9307 log amplifiers and fed to the analog inputs of an Arduino Nano. This drove an OLED display showing FWD power and SWR. Fiddle as I might I could not get stable results, probably as a lot of the circuitry was on a plug-and-play breadboard. So I am having a rethink.

I have browsed the web for ages and this is what I have come up with. I will wire this up on a proper PCB to test it:

IMG 1096

I have abandoned the AD8307s and gone back to what most people use - a couple of germanium low drop diodes. The PA amplifier this will be used with is 2-3W output, so their should be enough volts to run the thing. Calibration can be done in software.

As you can see I want to retain the measurement of RF output - just the voltage on the line, which can be shown as power when the system is matched to a 50R load. For this I am using a tried and trusted AD8307 circuit which will display 10mW to 10W. This AD8307 output will also be used to trigger the auto RX/TX switching of the PA.

Wish me luck.

Monday, 15 May 2017

BASIC Tech Group - MyNews - 44 The SWR meter

As part of my planned QRP PA, giving 3.2W output on 40, 30 & 20m, I am including an SWR meter. These look simple but are far from it.

The circuit I am trying to build is based on an Arduino Nano to do the calculations and drive a OLED display, and a couple of AD8307 log detectors to measure the Forward and Reflected power outputted from a conventional circuit using tow 1:10 transformers to measure the load line current and voltage.

Screen Shot 2017 05 15 at 12 46 10

When a wave travels from TRX to ANT a forward voltage FWD is output, when a reflected wave travels from ANT to TRX a reflected voltage REF is output. That's the theory anyway. (see this plagiarism article) The actual circuit looks like this

Screen Shot 2017 05 14 at 14 42 57

So far so good.

PROGRESS SO FAR

What I have done so far is to buikd the AD8307s and Nano circuit,

IMG 1086

and write some software to display a couple of bars for FWD power, SWR, display the values and say which band I am operating on. This involves detecting (over an average of ten measurements) the AD8307 outputs in millivoltss, converting this to dBm according to the ICs slope of 25mV/dB, converting this to dBm and then milliwatts across the 50R

IMG 1085resistors.

I have found that individual calibration is needed to get the same sensitivity from each AD8307, see code below. The slopes seem to be equal, but the intercepts different. Anyway I am now getting roughly the correct SWR for a 20dB Return Loss.

And at the moment I am stuck as my SWR transformers do not seem to giving the right outputs. I am driving the TXR end from my AD9851 VFO buffer outputs with 10mW and using a load of 50R at the output, built-into my RF Meter box. And the results are WRONG - Negative SWR! Reflected power bigger than Forward power... Why?

Code

// PA_LPF_TRX_OLED
// V0.95 16-5-17 need h/w for testing/calibTFMRLOSSn
// to add TRX

#include "Oled_128X64_I2C.h"

// analog reference (mV), A/D count, read avg
// slope and TX/display trigger (mW)
#define AREF 3300
#define ADCOUNT 1023
#define READAVG 10
#define TXTRIG 2

// analog inputs, band button, band relays, TXRX relay (PTT)
#define FWDPIN A0
#define REFPIN A1
#define SW 4
#define BAND1 7
#define BAND2 6
#define PTT 5

// display variables
double mwFwd, mwRef, swr;

// display text for bands
char bandTxt[][4] = {
  "40m", "30m", "20m"
};

byte band;

bool txFlag;

void setup() {

  Serial.begin(9600);

  pinMode(SW, INPUT_PULLUP);
  pinMode(BAND1, OUTPUT);
  pinMode(BAND2, OUTPUT);
  pinMode(PTT, OUTPUT);

  // analog ref AREF
  analogReference(EXTERNAL);

  oled.begin();

  // start on 40m
  band = 0;

  // TX off
  txFlag = false;
}

void loop() {
  int aFwd, aRef, n;
  double mV, dBm;

  // read SWR bridge inputs and average
  aFwd = 0;
  aRef = 0;
  for (n = 0; n < READAVG; n++) {
    aFwd += analogRead(FWDPIN); // typ +20dB level, gives 0dB at AD8307, or 2.5V (775 d/a)
    delay(50);
    aRef += analogRead(REFPIN); // typ -10dB level, gives -30dB at AD8307, or 1.5V (465 d/a)
  }
  aFwd /= READAVG;
  aRef /= READAVG;

  // 1. Adj slope for 20dB drop (470/50R), plot
  // 2. Adj intercept for correct mW
  // aFwd to mW, slope & intercept
  mwFwd = convert(aFwd, 25.0, -86);
  mwRef = convert(aRef, 25.0, -88);

  Serial.print("F: ");
  Serial.print(aFwd);
  Serial.print("\t");
  Serial.print(mwFwd, 3);

  Serial.print("\t R: ");
  Serial.print(aRef);
  Serial.print("\t");
  Serial.println(mwRef, 3);


  // calc SWR
  swr = ( 1 + sqrt(mwRef / mwFwd) ) / (1 - sqrt(mwRef / mwFwd) ); // calc SWR

  // band change
  if (digitalRead(SW) == LOW) {
    while (!digitalRead(SW));
    if (band < 2) band++;
    else band = 0;
    bandSw();
  }

  // switch to TX at 100mW
  if (mwFwd > TXTRIG) {
    digitalWrite(PTT, HIGH); // TX = HIGH
    txFlag = true;
  }
  else {
    digitalWrite(PTT, LOW);
    txFlag = false;
  }

  dispUpdate();

  delay(50); // loop stability
}

// convert A/D count, to mW
double convert(int aIn, double sl, double cal) {
  double mV, dBm;

  mV = (double)(map(aIn, 0, ADCOUNT, 0, AREF));
  dBm = (mV / sl) + cal;
  return pow(10.0, (dBm / 10.0));

}

// band relays, wiring HIGH = relay ON
void bandSw() {
  switch (band) {
    case 0:
      digitalWrite(BAND1, HIGH);  // 40m
      digitalWrite(BAND2, HIGH);
      break;
    case 1:
      digitalWrite(BAND1, HIGH); // 30m
      digitalWrite(BAND2, LOW);
      break;
    case 2:
      digitalWrite(BAND1, LOW); // 20m
      digitalWrite(BAND2, LOW);
      break;
  }
}

//=====PICTURE LOOP
void dispUpdate() {
  oled.firstPage();
  do {
    dispMsg(20, 0, "QRP POWER AMP"); // title

    dispMsgS(0, 15, "FWD");
    dispMsgS(0, 25, "SWR");
    dispMsg(50, 35, "FWD");
    dispMsg(50, 50, "SWR");
    dispMsgL(10, 40, bandTxt[band]); // band "40m" "30m" "20m" large font

    // values display active only on TX
    if (txFlag == false) {
      dispBar(20, 15, 5, 0); // blank bars
      dispBar(20, 25, 5, 0);
    }
    else {
      dispBar(20, 15, 5, mwFwd / 50 ); // 0-5000mW = 0-100bar
      dispBar(20, 25, 5, (swr - 1) * 25); // 1-5 = 0-100bar
      if (mwFwd > 1000) {
        dispNum(80, 35, mwFwd / 1000, 1);
        dispMsg(110, 35, "W");
      }
      else {
        dispNum(80, 35, mwFwd, 0);
        dispMsg(110, 35, "mW");
      }
      dispNum(80, 50, swr, 2);
    }
  } while (oled.nextPage());
}



Thursday, 11 May 2017

BASIC Tech Group - MyNews - 43 Next project, a QRP PA

The final box in my chain of QRP station items for digital transmissions is to be a 3.2W QRP PA. The block diagram for this is:

IMG 1082

I have found a 3.2W RF amplifier module on eBay which I will use. It has a gain of 35dB and works from a +12V supply (I tried another one to play with and it worked well, but was only 2W). To drive this from my VFO I will use an external 20dB attenuator and adjust the output with the variable gain buffer of the VFO.

Next in the chain are a set of 3 switched LPFs for 40, 30 & 20m followed by a SWR bridge. All is controlled by an Arduino Nano microcomputer. This switches the LPF relays, reads the SWR bridge and displays FWD forward power and SWR on an OLED display. RX/TX switching is automatic when ever the PA forward output is above 100mW. So far I have developed some provisional software, you can see the OLED display, and a wired up an SWR detection board. Now to breadboard the AD8307s for detection of FWD & REF outputs from the bridge, and drive the Nano analog inputs for the display...

IMG 1081

By the way in the background you can see my SDR-ELEKTOR, RF_POWER_METER and VFO running Hellschreiber S-MT software transmitting a HELL_S-MT beacon signal, displayed on the screen in Argo software:

Screen Shot 2017 05 11 at 14 31 28

Wednesday, 26 April 2017

BASIC Tech Group - MyNews - 42 Boxing them up

I have, finally, got to the point of putting my projects in boxes. Here you can see, from top to bottom,

The Elektor SDR, with OLED display and a rotary encoder to select 80, 40 & 20m centre frequencies in 50kHz steps.

The RF meter, displaying the RF output from the VFO, it can display 0.1uW up to 10W power and has a switchable 50R dummy load inside.

The VFO, which is programmable as a simple VFO 1-70MHz, in steps of 10Hz to 1MHz. But can also be programmed with various Arduino sketches as a Beacon for CW, PSK & RTTY, as a JT65 transmistter and a WSPR transmitter. The output is -5 to +15dBm adjustable.

IMG 1036 Box stack

Sketches

The current range of sketches I have are:

Screen Shot 2017 05 04 at 14 55 44

These sketches all use the OLED display, and are written for the AD9851 synthesiser - except the SDR which uses the Si5351 sythesiser.

From the top these are for

BCN_CW_OLED - a CW beacon transmitting a short programable message at regular selectable intervals, uses an OLED display

IMG 1055 Argo view on PC

BCN_PSK_OLED - same for PSK31

IMG 1076 PSK31 Beacon running on VFO

Screen Shot 2017 05 09 at 11 57 02 PSK reception on HDSDR on MacBook

Screen Shot 2017 05 09 at 11 56 26 PSK31 decode on MultiMode Cocoa on MacBook

BNC_RTTY_OLED - same for RTTY

DATE_TIME_OLED - a simple date and time display that runs on the Arduino in any of the boxes as they have the same OLED connections and display libraries.

DATE_TIME_SET_OLED - an important sketch to set the time of the RTC in the VFO, this has to be set to 1sec accuracy for WSPR & JT65 transmissions

HELL_FELD_7x14_OLED - Hellschreiber message transmitter, with a standard 7x14 font

HELL_S-MT_5x7_OLED - another version of Hellschreiber using sequencial multi tone transmission and a 5x7 font

IMG 1054 Argo on PC

JT65_ADS51_TEXT_OLED - JT65 transmission of simple messages, input via a serial comms program over USB

IMG 1058 VFO running JT65

IMG 1069 HDSDR on PC

IMG 1070 WSJT-X on PC (Mac version crashes... need to sort out memory shared size)

RF_METER_OLED - a sketch for the RF Meter box, displaying RF bar graph 0.1uW to 10W, mV, dBm and power (when internal 50R dummy load active)

SDR_ELEKTOR_TUNE_OLED - a sketch for the Si5351 synthesiser used on the Elektor SDR board, with frequency selection in 50kHz steps over the 80, 40 & 20m bands

VFO_ADS51_OLED - a general purpose VFO 1-70MHz in steps of 10Hz to 1MHz.

VOLT_OLED - not a sketch for these boxes, but a simple DC voltmeter (0-5V) usng the Arduino analog input A0, and displaying on the OLED display. It also calculates the dB of the input and reads dBm if measurements are made across a 50R load. Good for a very low cost voltmeter using an Arduino Nano...

WSPR_ADS51_OLED - programmable WSPR transmitter, currently set to 40m (7040.1kHz, dial 7038,6kHz reception).



That's a full 2 years on and off work but I am nearing completion of my station. Planned are two more boxes - a complete QRP SDR transceiver and a 2-3W PA with 80, 40, 20m LPF and SWR display.

All Code and Libraries download.

Wednesday, 19 April 2017

BASIC Tech Group - MyNews - 41 Moving from LCD to OLED displays - a new header file

I am planning to mount a number of my projects in some, quite small, instrument boxes. I will build a VFO, an RF METER, an SDR, and a PA/LPF/SWR. These boxes will use 1.3" OLED displays. (Note: works also for the smaller 0.96" OLEDs).

IMG 0992

I plan to tidy up the sketches i have for driving OLED displays and consolidate them in a new header file, I call Oled_128X64_I2C.h. This header includes six functions used for displaying the data I need across many applications. They are

OLED Functions dispBar - display a bar graph at x, y, height h and length l, with a surrounding box
dispFreq - display, in a larger font, the frequency at x, y, in d decimal places
dispMsg - display a general text message at x, y
dispNum - display a general number at x, y, d decimal places
dispDate- display the date day-of-week, day, month, year dispTime - display the time at x, y, hours, minute, seconds
dispStep - display the VFO frequency step when tuning, at x, y

The variables used are shown in the functions' outlines above.

Example

Here is an example of the use of the new header file

// test OLED_128x64_I2C with OLED SH1106 display

#include "Oled_128X64_I2C.h"

// display values - global values used to pass them to dispUpdate() function
double a = 7100000;   // freq
double b = 0;          // centi freq
unsigned int c = 1000; // step
byte hr = 12;          // time
byte mn = 3;
byte sc = 33;
double n = 345.6;      // a number
byte hT = 5;          // bar height and length
byte bL = 45;

void setup() {
  oled.begin();
}

void loop() {
  // put your main code here, to run repeatedly:
  dispUpdate();
}

// picture loop, display definition data, what, where
void dispUpdate() {
  oled.firstPage();
  do {
    dispMsg(0, 0, "RF");
    dispBar(15, 3, hT, bL);
    dispFreq(10, 15, a, b, 2);
    dispNum(45, 35, n, 2);
    dispTime(10, 50 , hr, mn, sc);
    dispStep(80, 50, c);
  } while ( oled.nextPage() );
}

You can see how simple it makes writing the sketch. Every thing needed, including the constructor for the 1.3" I2C OLED display is in the header file.

IMG 0991 The result of the code above.

IMG 0983 My desk full of OLED displays for my projects.

Code
// oled_128X64_I2C.h
// V1.5 21-4-17 added small & large versions, and date display
// defines oled pins, creates "oled" object for 128x64 SH1106 display
// step now an unsigned int
// functions usage
// void dispBar(u8g2_uint_t x, u8g2_uint_t y, u8g2_uint_t h, u8g2_uint_t l)
// void dispFreq(u8g2_uint_t x, u8g2_uint_t y, double f, double cf, uint8_t d)
// void dispMsgS(u8g2_uint_t x, u8g2_uint_t y, char *m)
// void dispMsg(u8g2_uint_t x, u8g2_uint_t y, char *m)
// void dispMsgL(u8g2_uint_t x, u8g2_uint_t y, char *m)
// void dispNum(u8g2_uint_t x, u8g2_uint_t y, double n, uint8_t d)
// void dispNumL(u8g2_uint_t x, u8g2_uint_t y, double n, uint8_t d)
// void dispDate(u8g2_uint_t x, u8g2_uint_t y, byte dw, byte da, byte mo, byte yr)
// void dispTime(u8g2_uint_t x, u8g2_uint_t y, byte h, byte m, byte s)
// void dispTimeL(u8g2_uint_t x, u8g2_uint_t y, byte h, byte m, byte s)
// void dispStep(u8g2_uint_t x, u8g2_uint_t y, unsigned int s)


#include "U8g2lib.h"
#include "Wire.h"

// oled object, SH1106 controller, 128X64, HW I2C and normal orientation R0
U8G2_SH1106_128X64_NONAME_1_HW_I2C oled(U8G2_R0);

//=====FUNCTIONS
// display bar at x, y, h)eight, l)ength (0-128 pixels)
void dispBar(u8g2_uint_t x, u8g2_uint_t y, byte h, byte l) {
  byte n;

  oled.drawFrame(x, y, 100, h+1);
  for ( n = 0; n < l; n++) {
    oled.drawLine(x + n, y, x  + n, y + h);
  }
}

// display freq at x, y, f (Hz), cf (cHz), d)ecimal places (max 3)
void dispFreq(u8g2_uint_t x, u8g2_uint_t y, double f, double cf, byte d) {

  // sets font, cursor position and displays freq
  oled.setFont(u8g2_font_10x20_tf); // font
  oled.setFontPosTop(); // origin top

  f = f / 1000.0;
  cf = cf / 100000.0;

  oled.setCursor(x, y);
  oled.print(f + cf, d);
  oled.print("kHz");
}

// fonts github.com/olikraus/u8g2/wiki/fntgrpx11

// display small message at at x), y), *m)essage
void dispMsgS(u8g2_uint_t x, u8g2_uint_t y, char *m) {
  // sets font, cursor position and displays message
  oled.setFont(u8g2_font_5x8_tf); // font
  oled.setFontPosTop();
  oled.setCursor(x, y);
  oled.print(m);
}

// display message at at x), y), *m)essage
void dispMsg(u8g2_uint_t x, u8g2_uint_t y, char *m) {
  // sets font, cursor position and displays message
  oled.setFont(u8g2_font_7x13_tf); // font
  oled.setFontPosTop();
  oled.setCursor(x, y);
  oled.print(m);
}
// display large message at at x), y), *m)essage
void dispMsgL(u8g2_uint_t x, u8g2_uint_t y, char *m) {
  // sets font, cursor position and displays message
  oled.setFont(u8g2_font_10x20_tf); // font
  oled.setFontPosTop();
  oled.setCursor(x, y);
  oled.print(m);
}

// display number at x), y), n)umber (double), d)ecimal places
void dispNum(u8g2_uint_t x, u8g2_uint_t y, double n, byte d) {
    // sets font, cursor position and displays number
  oled.setFont(u8g_font_7x14); // fix font for now
  oled.setFontPosTop();
  oled.setCursor(x, y);
  oled.print(n, d);
}

// display number large at x), y), n)umber (double), d)ecimal places
void dispNumL(u8g2_uint_t x, u8g2_uint_t y, double n, byte d) {
    // sets font, cursor position and displays number
  oled.setFont(u8g2_font_10x20_tf); // font
  oled.setFontPosTop();
  oled.setCursor(x, y);
  oled.print(n, d);
}

// display date
void dispDate(u8g2_uint_t x, u8g2_uint_t y, byte dw, byte da, byte mo, byte yr) {
 // sets font, cursor position and displays message
  oled.setFont(u8g_font_7x14); // fix font for now
  oled.setFontPosTop();
  oled.setCursor(x, y);
  
  switch (dw) {
    case 1:
      oled.print("Mon");
      break;
    case 2:
      oled.print("Tue");
      break;
    case 3:
      oled.print("Wed");
      break;
    case 4:
      oled.print("Thu");
      break;
    case 5:
      oled.print("Fri");
      break;
    case 6:
      oled.print("Sat");
      break;
    case 7:
      oled.print("Sun");
      break;
  }

  oled.print(" ");
  oled.print(da);

  oled.print(" ");
  switch (mo)
  {
    case 1:
      oled.print("Jan");
      break;
    case 2:
      oled.print("Feb");
      break;
    case 3:
      oled.print("Mar");
      break;
    case 4:
      oled.print("Apr");
      break;
    case 5:
      oled.print("May");
      break;
    case 6:
      oled.print("Jun");
      break;
    case 7:
      oled.print("Jul");
      break;
    case 8:
      oled.print("Aug");
      break;
    case 9:
      oled.print("Sep");
      break;
    case 10:
      oled.print("Oct");
      break;
    case 11:
      oled.print("Nov");
      break;
    case 12:
      oled.print("Dec");
      break;
  }
  oled.print(" ");
  oled.print("20");
  oled.print(yr);
}

// display time HH:MM:SS at x), y)
void dispTime(u8g2_uint_t x, u8g2_uint_t y, byte h, byte m, byte s) {
  // sets font, cursor position and displays message
  oled.setFont(u8g_font_7x14); // fix font for now
  oled.setFontPosTop();
  oled.setCursor(x, y);
  if (h < 10)
    oled.print("0");
  oled.print(h);
  oled.print(":");
  if (m < 10)
    oled.print("0");
  oled.print(m);
  oled.print(":");
  if (s < 10)
    oled.print("0");
  oled.print(s);
}

// display time HH:MM:SS at x), y)
void dispTimeL(u8g2_uint_t x, u8g2_uint_t y, byte h, byte m, byte s) {
  // sets font, cursor position and displays message
  oled.setFont(u8g2_font_10x20_tf); // font
  oled.setFontPosTop();
  oled.setCursor(x, y);
  if (h < 10)
    oled.print("0");
  oled.print(h);
  oled.print(":");
  if (m < 10)
    oled.print("0");
  oled.print(m);
  oled.print(":");
  if (s < 10)
    oled.print("0");
  oled.print(s);
}

// display step at x) y) s)tep
void dispStep(u8g2_uint_t x, u8g2_uint_t y, unsigned int s) {
  // set font, cursor position and display step
  oled.setFont(u8g_font_7x14); // fix font for now
  oled.setFontPosTop();
  
  oled.setCursor(x, y);
  switch (s) // display freqStep
  {
    case 10:
      oled.print(" 10Hz");
      break;
    case 100:
      oled.print("100Hz");
      break;
    case 1000:
      oled.print(" 1kHz");
      break;
    case 10000:
      oled.print(" 10kHz");
      break;
    case 100000:
      oled.print("100kHz");
      break;
    case 1000000:
      oled.print(" 1MHz");
      break;
  }
}

Sunday, 16 April 2017

BASIC Tech Group - MyNews 40 - VFO using AD9851 with OLED display

This code has been updated - see later posting!!

Here's the code for a VFO using the AD9851, up to 70MHz. The AD9851 code is here

// VFO_ADS51_OLED
// V1.0 16-4-17 VFO for the AD9851, 30MHz external xtal, x6 180MHz internal clock
// freq steps 10Hz, 100Hz, 1kHz, 10kHz, 100kHz, 1MHz
// AD9851
// RESET 8
// DATA 9
// FQ_UD 10
// W_CLK 11
// OLED 128x64
// SDA = A4
// SCL = A5
// rotary encoder pins
// CLK = 2
// DT = 3
// SW = 4


// OLED, AD9851, Rotary Encoder & I2C libraries
#include "Oled_128X64_I2C.h"
#include "ADS9851.h"
#include "Rotary.h"

// AD9851 pins
#define RESET 8
#define DATA 9
#define FQ_UD 10
#define W_CLK 11

// xtal calibration (30MHz external x6 REFCLK = 180MHz internal
#define CALIBRATE 180002300 // cal against SDR (cal at 7070 against CORRA)

// encoder
#define CLK 2
#define DT 3
#define SW 4

// ads (analog-output digital synthesiser) object
ADS9851 ads;

// Encoder object
Rotary enc = Rotary(DT, CLK);

// phase coding, 0-180 [0-5]
uint8_t ph[] = {0x00, 0x01, 0x02, 0x04, 0x08, 0x10};

// initial settings
volatile double freqHz = 7100000; // (Hz) start frequency 7.1MHz
volatile double freqChz = 0;  // (cHz) additional 0cHz
volatile double freqStep = 10; // (Hz) init 10Hz freqStep
uint8_t phase = ph[0]; // init phase

// freq change flag
volatile bool freqChange;

void setup() {
  // encoder, button pins
  pinMode(DT, INPUT_PULLUP);
  pinMode(CLK, INPUT_PULLUP);
  pinMode(SW, INPUT_PULLUP);

  // setup interrupts from DT or CLK for tuning
  attachInterrupt(digitalPinToInterrupt(DT), freqTune, CHANGE);
  attachInterrupt(digitalPinToInterrupt(CLK), freqTune, CHANGE);
  interrupts(); // enable

  // oled init, sets I2C addr to 0x3C
  oled.begin();

  // init ads, executes down() to flush buffers
  ads.begin(W_CLK, FQ_UD, DATA, RESET);

  // calibrate to xtal actual frequency
  ads.calibrate(CALIBRATE);

  ads.setFreq(freqHz, freqChz, phase);

  freqChange = false;

  dispUpdate();
}

// check button & freq tune, update display
void loop() {
  if (button()) {
    dispUpdate();
  }

  if (freqChange) {
    // freq updated
    ads.setFreq(freqHz, freqChz, phase);
    freqChange = false;
    dispUpdate();
  }
}


// ISR - encoder interrupt service routine
void freqTune() {
  unsigned char result;

  result = enc.process();
  if (result == DIR_CW ) {
    freqHz += freqStep;
    freqChange = true;
  }
  else if (result == DIR_CCW) {
    freqHz -= freqStep;
    freqChange = true;
  }
}

// change freqStep, 10Hz to 1MHz
bool button() {
  if (digitalRead(SW) == LOW) { // button pressed?
    while (!digitalRead(SW)); // wait for release
    if (freqStep == 1000000) freqStep = 10; // back to 10Hz
    else freqStep = freqStep * 10; // or increase by x10
    return true;
  }
  else {
    return false;
  }
}

// picture loop, display init data
void dispUpdate() {
  oled.firstPage();
  do {
    dispMsg(50, 0, "VFO");
    dispFreq(15, 25, freqHz, freqChz, 2);
    dispMsg(15, 50, "<--");
    dispStep(45, 50, freqStep);
    dispMsg(95, 50, "-->");

  } while ( oled.nextPage() );
}

Getting it straight - AD985x IC Arduino library "ADS9850 & 51"

I’ve seen a lot of libraries on the web for the AD9850 and AD9851 Analog Digital Synthesisers. And as far as I can see none of them meet the data sheet description of how to program the devices.

Most people use the AD985x devices mounted on modules made in China.

IMG 0993

These all seem to have the same circuit diagram. The device is wired for either parallel or serial data inputs, and two sine and two square wave outputs.

Screen Shot 2017 04 17 at 11 26 00

Large board

Screen Shot 2017 04 17 at 11 25 09

Small board

But ONLY one sine output has a filter to filter the DAC jitters, a 70MHz LPF, this is on the IOUT pin. So this is the output you should use.

ARDUINO

Lets start with the Arduino, and assume we are going to send data to the device in serial mode. We need 4 pins for this, for the signals W_CLK, FQ_UP, DATA and RESET. So we set up 4 Arduino pins in OUTPUT mode. On the Arduino this defaults the output to LOW level, which is what we want.

The first thing to do is to reset the device, this is a bit difficult as it starts in parallel mode and could cause a problem. With the device in parallel mode a reset (a LOW-HIGH-LOW pulse on RESET) will zero the phase accumulator & offset, set the output IOUT to zero mA, set the internal address pointer to the first of the five programming bytes, set power down PD off and, for the AD9851 set the internal x6 xtal frequency REFCLK  multiplier off. But it won’t clear the frequency registers or the control and phase set registers. So they could contain random bits…  this is chance you have to take. They will anyway be overwritten by the following code, but could briefly put the device  in an illegal factory control mode...

The large modules from China are hard wired with pins D0 & D1 to VCC and D2 via a jumper J1 to GND, but the small modules should be wired externally to be safe. This gives the input code required to the parallel input D0-D7 of xxxxx011 which will switch the device to serial mode by a pulse on W_CLK followed by FQ_UD.

Next thing to do for the AD985x is to place the device in power down mode, which will clear all the registers, and for the AD9851 set the PD and the REFCLK registers. We need the REFCLK set to ‘1’ for the AD9851 to enable the x6 xtal frequency multiplier, as all AD9851 modules from China seem to come with a lower cost 30MHz xtal, not an external 180MHz one. But we need an internal 180MHz for correct frequency outputs. (This seems to be a BIG mistake many users make as AD9850 modules come with a 125MHz xtal...).

So the first call is to the library function “begin":

// init calFreq, pins, reset & serial mode
void ADS9850::begin(int W_CLK, int FQ_UD, int DATA, int RESET) {
	_W_CLK = W_CLK;
	_FQ_UD = FQ_UD;
	_DATA = DATA;
	_RESET = RESET;

	_calFreq = ADS_XTAL;

	pinMode(_W_CLK, OUTPUT); // outputs default to LOW
	pinMode(_FQ_UD, OUTPUT);
	pinMode(_DATA, OUTPUT);
	pinMode(_RESET, OUTPUT);

	pulse(_RESET); // reset, parallel mode, ptr to W0
        
        pulse(_W_CLK); // switch to serial mode, xxxxx011 wired on d0-d2
        pulse(_FQ_UD);

        down(); // clear freq and phase registers, REFCLK=1 (x6 en), PD=1 (pwd dn)
}

To set an output frequency we need to calculate the 32 bit value to serially transfer into the first 4 bytes of the 40 bit control register. This we do like this for the AD9850:

// calculate 4 freq bytes, convert double to to uint32_t
void ADS9850::setFreq(double f, double cf, uint8_t p) {
        uint32_t delta;

	delta = (uint32_t)((f + cf/100.0) * 4294967296.0 / _calFreq);
	p = p << 3; // = ppppp000
	update(delta, p);
}
or for the AD9851
// calculate 4 freq bytes, convert double to to uint32_t
void ADS9851::setFreq(double f, double cf, uint8_t p) {
     uint32_t delta;

	delta = (uint32_t)((f + cf/100.0) * 4294967296.0 / _calFreq);
	p = p << 3; // PD off = ppppp000
    setBit(p, 0); // REFCLK on, = ppppp001
	update(delta, p);
}

We start with frequencies f (Hz) and cf (cHz) in double floating point, calculate the "delta" value then convert the result to a 32 bit uint32_t value. And we calculate the phase register contents, setting the REFCLK flag for the AD9851

Next we have to transfer the uint32_t frequency, and the byte p for the control and phase of the ouptut. This is done by the function update().

The update is made like this (same for the AD9851):

// load the 4 delta & the control/phase registers
void ADS9850::update(uint32_t d, uint8_t c) {
    for (int i=0; i <4 ; i++, d >>= 8) {
       shiftOut(_DATA, _W_CLK, LSBFIRST, d); // output freq byte
    }
    shiftOut(_DATA, _W_CLK, LSBFIRST, c); // output control & phase byte

    pulse(_FQ_UD);
}

First "shiftOut" the four delta frequency bytes then the control/phase byte with the previous settings for PD and REFCLK. The function "shiftOut" generates the W_CLK signal for the transfer.

So here are the complete libraries.

Study them both to understand the differences between the AD9850 and 51.

Monday, 10 April 2017

BASIC Tech Group - MyNews 39 - Updated VFO with OLED display

Here’s the updated sketch for my AD9850 based VFO, to use an OLED display. The VFO tunes in 10Hz to 1MHz steps. I have in development a buffer amplifier - probably it will be based on an MMIC (ERA2-SM)maybe  to give up to +10dBm output.

Screen Shot 2017 04 11 at 17 02 50

Code

// VFO_ADS_OLED
// V1.5 9-4-17 update to U8g2lib
// output G0
// freq steps 10Hz, 100Hz, 1kHz, 10kHz, 100kHz, 1MHz
// AD9850
// W_CLK 8
// FQ_UD 9
// DATA 10
// RESET 11
// OLED 128x64
// SDA = A4
// SCL = A5
// rotary encoder pins
// DT = 2
// CLK = 3
// SW = 4


// ADS9850, OLED and Rotary Encoder, I2C libraries
#include "ADS9850.h"
#include "U8g2lib.h"
#include "Rotary.h"
#include "Wire.h"

// AD9850 pins
#define W_CLK 8
#define FQ_UD 9
#define DATA 10
#define RESET 11

// chose pin 2,3 or 3,2 for encoder
#define DT 2
//#define DT 3
#define CLK 3
//#define CLK 2
#define SW 4

// xtal calibration
#define CALIBRATE 125000000

// ads (analog-output digital synthesiser) object
ADS9850 ads;

// oled object
U8G2_SH1106_128X64_NONAME_1_HW_I2C oled(U8G2_R0, U8X8_PIN_NONE, SCL, SDA);

// Encoder object
Rotary enc = Rotary(DT, CLK);

// phase coding, 0-180 [0-5] in 11.25deg steps
uint8_t ph[] = {0x00, 0x01, 0x02, 0x04, 0x08, 0x10};

// initial settings
volatile double freqHz = 7100000; // (Hz) start frequency 7.1MHz
volatile double freqChz = 0; // (cHz) additional 0cHz
volatile double freqStep = 10; // (Hz) init 10Hz freqStep
uint8_t phase = ph[0]; // init phase

// freq change flag
volatile bool freqChange;

void setup() {
// encoder, button pins
pinMode(DT, INPUT_PULLUP);
pinMode(CLK, INPUT_PULLUP);
pinMode(SW, INPUT_PULLUP);

// setup interrupts from DT or CLK for tuning
attachInterrupt(digitalPinToInterrupt(DT), freqTune, CHANGE);
attachInterrupt(digitalPinToInterrupt(CLK), freqTune, CHANGE);
interrupts(); // enable

// oled init, sets I2C addr to 0x3C
oled.begin();

// init AD9850
ads.begin(W_CLK, FQ_UD, DATA, RESET);

// calibrate to xtal actual frequency
ads.calibrate(CALIBRATE);

ads.setFreq(freqHz, freqChz, phase);

freqChange = false;

dispUpdate();
}

// check button & freq tune, update display
void loop() {
if (button()) {
dispUpdate();
}

if (freqChange) {
// freq updated
ads.setFreq(freqHz, freqChz, phase);
freqChange = false;

dispUpdate();
}
}


// ISR - encoder interrupt service routine
void freqTune() {
unsigned char result;

result = enc.process();
if (result == DIR_CW ) {
freqHz += freqStep;
freqChange = true;
}
else if (result == DIR_CCW) {
freqHz -= freqStep;
freqChange = true;
}
}

// change freqStep, 10Hz to 1MHz
bool button() {
if (digitalRead(SW) == LOW) { // button pressed?
while (!digitalRead(SW)); // wait for release
if (freqStep == 1000000) freqStep = 10; // back to 10Hz
else freqStep = freqStep * 10; // or increase by x10
return true;
}
else {
return false;
}
}

// picture loop, display init data
void dispUpdate() {
oled.firstPage();
do {
dispMsg(40, 0, "VFO_ADS");
dispFreq(15, 25, freqHz, freqChz, 2);
dispMsg(30, 50, "Step");
dispStep(70, 50, freqStep);

} while ( oled.nextPage() );
}

// display freq at x, y, f (Hz), cf (cHz), d)ecimal places
void dispFreq(u8g2_uint_t x, u8g2_uint_t y, double f, double cf, uint8_t d) {
double fd;
char buf[100];

// sets font, cursor position and displays freq
oled.setFont(u8g2_font_10x20_tf); // font
oled.setFontPosTop(); // origin top

fd = f + cf / 100; // calc freq

oled.setCursor(x, y);
oled.print(fd / 1000, d);
oled.print("kHz");
}

void dispStep(u8g2_uint_t x, u8g2_uint_t y, uint64_t s) {
// set font, cursor position and display step
oled.setFont(u8g2_font_7x13_tf); // font
oled.setFontPosTop();

oled.setCursor(x, y);
switch (s) // display freqStep
{
case 10:
oled.print("10Hz ");
break;
case 100:
oled.print("100Hz ");
break;
case 1000:
oled.print("1kHz ");
break;
case 10000:
oled.print("10kHz ");
break;
case 100000:
oled.print("100kHz ");
break;
case 1000000:
oled.print("1MHz ");
break;
}
}

// display message at at x), y), *m)essage
void dispMsg(u8g2_uint_t x, u8g2_uint_t y, char *m) {
// sets font, cursor position and displays message
oled.setFont(u8g2_font_7x13_tf); // font
oled.setFontPosTop();
oled.setCursor(x, y);
oled.print(m);
}

BASIC Tech Group - MyNews 38 - Updated SDR ELEKTOR with OLED display

Here’s the code for an update to the sketch for the Elektor SDR RX. It updates the display to OLED and adds a tuning capability to select 50kHZ segments of 80, 40 & 20m bands.

Code

// SDR_ELEKTOR_OLED_TUNE
// V4.5 9-4-17 update to U8g2lib
// Output 80, 40, 20m in 50kHz steps x4 CLK1
// Si5351 I2C bus
// SDA = A4
// SCL = A5
// OLED 128x64
// SDA = A4
// SCL = A5

// Si5351 V2, LCD libraries
#include "si5351.h"
#include "U8g2lib.h"
#include "Rotary.h"

// chose pin 2,3 or 3,2 for encoder
#define DT 2
//#define DT 3
#define CLK 3
//#define CLK 2
#define SW 4

// xtal correction for Elektor DDS
#define CORRE 180000

// dds object
Si5351 dds;

// oled object
U8G2_SH1106_128X64_NONAME_1_HW_I2C oled(U8G2_R0, U8X8_PIN_NONE, SCL, SDA);

// Encoder object
Rotary enc = Rotary(DT, CLK);

// table of tuning frequencies
uint64_t freqTable[] = {
350000000, 355000000, 360000000, 365000000, 370000000, 375000000,
705000000, 710000000, 715000000,
1405000000, 1410000000, 1415000000, 1420000000, 1425000000
};

// index into freq table, startup freq
byte ndx = 7;

// initial frequency
uint64_t freq = freqTable[ndx];

// freq change flag
volatile bool freqChange;

void setup() {
// encoder, button pins
pinMode(DT, INPUT_PULLUP);
pinMode(CLK, INPUT_PULLUP);
pinMode(SW, INPUT_PULLUP);

// setup interrupts from DT or CLK for tuning
attachInterrupt(digitalPinToInterrupt(DT), freqTune, CHANGE);
attachInterrupt(digitalPinToInterrupt(CLK), freqTune, CHANGE);

// enable interrupts
interrupts();

// oled init, sets I2C addr to 0x3C
oled.begin();

// init dds si5351 module, "0" = default 25MHz XTAL, correction
dds.init(SI5351_CRYSTAL_LOAD_8PF, 0, CORRE);

// set 8mA output drive
dds.drive_strength(SI5351_CLK1, SI5351_DRIVE_8MA);

// enable VFO output CLK1, disable CLK0 & 2
dds.output_enable(SI5351_CLK0, 0);
dds.output_enable(SI5351_CLK1, 1);
dds.output_enable(SI5351_CLK2, 0);

freqOut(freq); // cHz, output freq
freqChange = false;

dispUpdate();
}

void loop() {
if (freqChange) {
freqOut(freq);
freqChange = false;
dispUpdate();
}
}

// ISR - encoder interrupt service routine
void freqTune() {
unsigned char result;

result = enc.process();
if (result == DIR_CW && ndx < 13) {
freq = freqTable[++ndx];
freqChange = true;
}
else if (result == DIR_CCW && ndx > 0) {
freq = freqTable[--ndx];;
freqChange = true;
}
}

// picture loop, display init data
void dispUpdate() {
oled.firstPage();
do {
dispMsg(25, 0, "SDR_ELEKTOR");
dispFreq(25, 25, 0, freq, 0); // freq is in cHz
dispMsg(20, 50, "<-- Freq -->");
} while ( oled.nextPage() );
}

// Output Freq for Elektor SDR, f (cHz) x 4 on CLK1
void freqOut(uint64_t f) {
dds.set_freq(f * 4ULL, SI5351_CLK1);
}

// display freq at x, y, f (Hz), cf (cHz), d)ecimal places
void dispFreq(u8g2_uint_t x, u8g2_uint_t y, double f, double cf, uint8_t d) {
double fd;
char buf[100];

// sets font, cursor position and displays freq
oled.setFont(u8g2_font_10x20_tf); // font
oled.setFontPosTop(); // origin top

fd = f + cf / 100; // calc freq

oled.setCursor(x, y);
oled.print(fd / 1000, d);
oled.print("kHz");
}

// display message at at x), y), *m)essage
void dispMsg(u8g2_uint_t x, u8g2_uint_t y, char *m) {
// sets font, cursor position and displays message
oled.setFont(u8g2_font_7x13_tf); // font
oled.setFontPosTop();
oled.setCursor(x, y);
oled.print(m);
}

BASIC Tech Group - MyNews 37 - Updated JT65 with OLED display

Here’s the code for JT65 with OLED display and text input via a USB connected terminal program, such as MacOS iSerialTerm or the Arduino IDE Serial Monitor window.

Code

// JT65_ADS_TEXT_OLED
// V2.5 9-4-17 serial input/output, update to U8g2lib
// Code based on Feld Hell beacon for Arduino by K6HX
// Timer setup code by LA3PNA.
// TX 7060000, Dial 7074830
// AD9850
// W_CLK 8
// FQ_UD 9
// DATA 10
// RESET 11
// OLED 128x64
// SDA = A4
// SCL = A5
// RTC I2C bus
// SDA = A4
// SCL = A5

#include "ADS9850.h"
#include "U8g2lib.h"
#include "Wire.h"

// RTC address
#define RTCADDR 0x68

// Stuff specific to the general (integer) version of the Reed-Solomon codecs
#define MODNN(x) modnn(rs,x)
#define MM (rs->mm)
#define NN (rs->nn)
#define ALPHA_TO (rs->alpha_to)
#define INDEX_OF (rs->index_of)
#define GENPOLY (rs->genpoly)
#define NROOTS (rs->nroots)
#define FCR (rs->fcr)
#define PRIM (rs->prim)
#define IPRIM (rs->iprim)
#define PAD (rs->pad)
#define A0 (NN)

#define TONE_SPACING            269           // ~2.6917 Hz
#define SUBMODE_A               5812         // CTC value for JT65A
#define SYMBOL_COUNT            126

// AD9850 pins
#define W_CLK 8
#define FQ_UD 9
#define DATA 10
#define RESET 11

typedef unsigned int data_t;

/* Reed-Solomon codec control block */
struct rs {
  int mm;              /* Bits per symbol */
  int nn;              /* Symbols per block (= (1<<mm)-1) */
  data_t *alpha_to;     /* log lookup table */
  data_t *index_of;     /* Antilog lookup table */
  data_t *genpoly;      /* Generator polynomial */
  int nroots;     /* Number of generator roots = number of parity symbols */
  int fcr;        /* First consecutive root, index form */
  int prim;       /* Primitive element, index form */
  int iprim;      /* prim-th root of 1, index form */
  int pad;        /* Padding bytes in shortened block */
};

// RTC time
byte sec, mns, hrs;

// trigger TX (sec)
uint8_t trigger;
bool tx;

// Frequency variables
double freqHz = 7076000; // nominal dial is 1270Hz lower
double freqChz = 0;
uint8_t phase = 0;

// message to send
char message[20] = "";

// encode variables
uint8_t tx_buffer[SYMBOL_COUNT];
static void *rs;

// timer flag
volatile bool proceed = false;

// AD9850 object
ADS9850 ads;

// oled object
U8G2_SH1106_128X64_NONAME_1_HW_I2C oled(U8G2_R0, U8X8_PIN_NONE, SCL, SDA);

void setup()
{
  Serial.begin(9600);
  
  // init I2C
  Wire.begin();

  // oled init, sets I2C addr to 0x3C
  oled.begin();

  // init AD9850, init freq and off to start
  ads.begin(W_CLK, FQ_UD, DATA, RESET);
  ads.setFreq(freqHz, freqChz, phase);
  ads.down();

  // Set up Timer1 for interrupts every symbol period.
  noInterrupts();          // Turn off interrupts.
  TCCR1A = 0;              // Set entire TCCR1A register to 0; disconnects
  //   interrupt output pins, sets normal waveform
  //   mode.  We're just using Timer1 as a counter.
  TCNT1  = 0;              // Initialize counter value to 0.
  TCCR1B = (1 << CS12) |   // Set CS12 and CS10 bit to set prescale
           (1 << CS10) |          //   to /1024
           (1 << WGM12);          //   turn on CTC
  //   which gives, 64us ticks
  TIMSK1 = (1 << OCIE1A);  // Enable timer compare interrupt.
  OCR1A = SUBMODE_A;          // Set up interrupt trigger count;
  interrupts();            // Re-enable interrupts.

  trigger = 1; // trigger on each minute
  tx = false;

  // Initialize the Reed-Solomon encoder
  rs = (struct rs *)(intptr_t)init_rs_int(6, 0x43, 3, 1, 51, 0);
}

void loop() {
  // get time
  getRTC();
  dispUpdate();

  // message?
  if (getMsg(message) == true) {
    // echo message
    Serial.println(message);
    // display message & time, trigger time?
    do {
      getRTC();
      dispUpdate();
    } while (mns % trigger != 0 || sec != 0);
    // send
    tx = true; // indicate TX on display
    encode(message);
    tx = false;
    // clear message & buffer
    clearBuf(message);
  }
}

// picture loop, display init data
void dispUpdate() {
  oled.firstPage();
  do {
    dispMsg(15, 0, "JT65_ADS_TEXT");
    dispMsg(15, 15, message);
    dispFreq(15, 30, freqHz, freqChz, 2);
    if (tx == false) dispTime(30, 50);
    else dispMsg(40, 50, "TX");
  } while ( oled.nextPage() );
}

// Loop through the string, transmitting one character at a time.
void encode(char * tx_string)
{
  uint8_t i;

  // encode the message
  jt65_encode(tx_string, tx_buffer);

  // Now do the rest of the message
  for (i = 0; i < SYMBOL_COUNT; i++)
  {
    ads.setFreq(freqHz, freqChz + (tx_buffer[i] * TONE_SPACING), phase);
    proceed = false;
    while (!proceed);
  }

  // Turn off the output
  ads.down();
}

static inline int modnn(struct rs *rs, int x) {
  while (x >= rs->nn) {
    x -= rs->nn;
    x = (x >> rs->mm) + (x & rs->nn);
  }
  return x;
}


uint8_t jt_code(char c)
{
  /* Validate the input then return the proper integer code */
  // Return 255 as an error code if the char is not allowed

  if (isdigit(c))
  {
    return (uint8_t)(c - 48);
  }
  else if (c >= 'A' && c <= 'Z')
  {
    return (uint8_t)(c - 55);
  }
  else if (c == ' ')
  {
    return 36;
  }
  else if (c == '+')
  {
    return 37;
  }
  else if (c == '-')
  {
    return 38;
  }
  else if (c == '.')
  {
    return 39;
  }
  else if (c == '/')
  {
    return 40;
  }
  else if (c == '?')
  {
    return 41;
  }
  else
  {
    return 255;
  }
}

// timer interrupt veector
ISR(TIMER1_COMPA_vect)
{
  proceed = true;
}

// Reed Soloman encoder by KA9Q
void encode_rs_int(void *p, data_t *data, data_t *parity)
{
  struct rs *rs = (struct rs *)p;

#undef A0
#define A0 (NN) /* Special reserved value encoding zero in index form */


  int i, j;
  data_t feedback;

  memset(parity, 0, NROOTS * sizeof(data_t));

  for (i = 0; i < NN - NROOTS - PAD; i++) {
    feedback = INDEX_OF[data[i] ^ parity[0]];
    if (feedback != A0) {    /* feedback term is non-zero */
#ifdef UNNORMALIZED
      /* This line is unnecessary when GENPOLY[NROOTS] is unity, as it must
         always be for the polynomials constructed by init_rs()
      */
      feedback = MODNN(NN - GENPOLY[NROOTS] + feedback);
#endif
      for (j = 1; j < NROOTS; j++)
        parity[j] ^= ALPHA_TO[MODNN(feedback + GENPOLY[NROOTS - j])];
    }
    /* Shift */
    memmove(&parity[0], &parity[1], sizeof(data_t) * (NROOTS - 1));
    if (feedback != A0)
      parity[NROOTS - 1] = ALPHA_TO[MODNN(feedback + GENPOLY[0])];
    else
      parity[NROOTS - 1] = 0;
  }
}


void free_rs_int(void *p)
{
  struct rs *rs = (struct rs *)p;

  free(rs->alpha_to);
  free(rs->index_of);
  free(rs->genpoly);
  free(rs);
}

// init RS enc, symb size, poly coeff, first root, primative, no roots, padding
void *init_rs_int(int symsize, int gfpoly, int fcr, int prim, int nroots, int pad) {
  struct rs *rs;

  //#undef NULL
  //#define NULL ((void *)0)

  int i, j, sr, root, iprim;

  rs = ((struct rs *)0);
  /* Check parameter ranges */
  if (symsize < 0 || symsize > 8 * sizeof(data_t)) {
    goto done;
  }

  if (fcr < 0 || fcr >= (1 << symsize))
    goto done;
  if (prim <= 0 || prim >= (1 << symsize))
    goto done;
  if (nroots < 0 || nroots >= (1 << symsize))
    goto done; /* Can't have more roots than symbol values! */
  if (pad < 0 || pad >= ((1 << symsize) - 1 - nroots))
    goto done; /* Too much padding */

  rs = (struct rs *)calloc(1, sizeof(struct rs));
  if (rs == NULL)
    goto done;

  rs->mm = symsize;
  rs->nn = (1 << symsize) - 1;
  rs->pad = pad;

  rs->alpha_to = (data_t *)malloc(sizeof(data_t) * (rs->nn + 1));
  if (rs->alpha_to == NULL) {
    free(rs);
    rs = ((struct rs *)0);
    goto done;
  }
  rs->index_of = (data_t *)malloc(sizeof(data_t) * (rs->nn + 1));
  if (rs->index_of == NULL) {
    free(rs->alpha_to);
    free(rs);
    rs = ((struct rs *)0);
    goto done;
  }

  /* Generate Galois field lookup tables */
  rs->index_of[0] = A0; /* log(zero) = -inf */
  rs->alpha_to[A0] = 0; /* alpha**-inf = 0 */
  sr = 1;
  for (i = 0; i < rs->nn; i++) {
    rs->index_of[sr] = i;
    rs->alpha_to[i] = sr;
    sr <<= 1;
    if (sr & (1 << symsize))
      sr ^= gfpoly;
    sr &= rs->nn;
  }
  if (sr != 1) {
    /* field generator polynomial is not primitive! */
    free(rs->alpha_to);
    free(rs->index_of);
    free(rs);
    rs = ((struct rs *)0);
    goto done;
  }

  /* Form RS code generator polynomial from its roots */
  rs->genpoly = (data_t *)malloc(sizeof(data_t) * (nroots + 1));
  if (rs->genpoly == NULL) {
    free(rs->alpha_to);
    free(rs->index_of);
    free(rs);
    rs = ((struct rs *)0);
    goto done;
  }
  rs->fcr = fcr;
  rs->prim = prim;
  rs->nroots = nroots;

  /* Find prim-th root of 1, used in decoding */
  for (iprim = 1; (iprim % prim) != 0; iprim += rs->nn)
    ;
  rs->iprim = iprim / prim;

  rs->genpoly[0] = 1;
  for (i = 0, root = fcr * prim; i < nroots; i++, root += prim) {
    rs->genpoly[i + 1] = 1;

    /* Multiply rs->genpoly[] by  @**(root + x) */
    for (j = i; j > 0; j--) {
      if (rs->genpoly[j] != 0)
        rs->genpoly[j] = rs->genpoly[j - 1] ^ rs->alpha_to[modnn(rs, rs->index_of[rs->genpoly[j]] + root)];
      else
        rs->genpoly[j] = rs->genpoly[j - 1];
    }
    /* rs->genpoly[0] can never be zero */
    rs->genpoly[0] = rs->alpha_to[modnn(rs, rs->index_of[rs->genpoly[0]] + root)];
  }
  /* convert rs->genpoly[] to index form for quicker encoding */
  for (i = 0; i <= nroots; i++)
    rs->genpoly[i] = rs->index_of[rs->genpoly[i]];
done:;

  return rs;
}

uint8_t gray_code(uint8_t c)
{
  return (c >> 1) ^ c;
}

void rs_encode(uint8_t * data, uint8_t * symbols)
{
  unsigned int dat1[12];
  unsigned int b[51];
  unsigned int i;

  // Reverse data order for the Karn codec.
  for (i = 0; i < 12; i++)
  {
    dat1[i] = data[11 - i];
  }

  // Compute the parity symbols
  encode_rs_int(rs, dat1, b);

  // Move parity symbols and data into symbols array, in reverse order.
  for (i = 0; i < 51; i++)
  {
    symbols[50 - i] = b[i];
  }

  for (i = 0; i < 12; i++)
  {
    symbols[i + 51] = dat1[11 - i];
  }
}

void jt65_encode(char * message, uint8_t symbols[SYMBOL_COUNT])
{
  uint8_t i, j, k;

  // Convert all chars to uppercase
  for (i = 0; i < 13; i++)
  {
    if (islower(message[i]))
    {
      message[i] = toupper(message[i]);
    }
  }

  // Collapse multiple spaces down to one

  // Pad the message with trailing spaces
  uint8_t len = strlen(message);
  if (len < 13)
  {
    for (i = len; i < 13; i++)
    {
      message[i] = ' ';
    }
  }

  // Bit packing
  // -----------
  uint8_t c[12];
  uint32_t n1, n2, n3;

  // Find the N values
  n1 = jt_code(message[0]);
  n1 = n1 * 42 + jt_code(message[1]);
  n1 = n1 * 42 + jt_code(message[2]);
  n1 = n1 * 42 + jt_code(message[3]);
  n1 = n1 * 42 + jt_code(message[4]);

  n2 = jt_code(message[5]);
  n2 = n2 * 42 + jt_code(message[6]);
  n2 = n2 * 42 + jt_code(message[7]);
  n2 = n2 * 42 + jt_code(message[8]);
  n2 = n2 * 42 + jt_code(message[9]);

  n3 = jt_code(message[10]);
  n3 = n3 * 42 + jt_code(message[11]);
  n3 = n3 * 42 + jt_code(message[12]);


  // Pack bits 15 and 16 of N3 into N1 and N2,
  // then mask reset of N3 bits
  n1 = (n1 << 1) + ((n3 >> 15) & 1);
  n2 = (n2 << 1) + ((n3 >> 16) & 1);
  n3 = n3 & 0x7fff;

  // Set the freeform message flag
  n3 += 32768;

  c[0] = (n1 >> 22) & 0x003f;
  c[1] = (n1 >> 16) & 0x003f;
  c[2] = (n1 >> 10) & 0x003f;
  c[3] = (n1 >> 4) & 0x003f;
  c[4] = ((n1 & 0x000f) << 2) + ((n2 >> 26) & 0x0003);
  c[5] = (n2 >> 20) & 0x003f;
  c[6] = (n2 >> 14) & 0x003f;
  c[7] = (n2 >> 8) & 0x003f;
  c[8] = (n2 >> 2) & 0x003f;
  c[9] = ((n2 & 0x0003) << 4) + ((n3 >> 12) & 0x000f);
  c[10] = (n3 >> 6) & 0x003f;
  c[11] = n3 & 0x003f;

  // Reed-Solomon encoding
  // ---------------------
  uint8_t s[63];
  k = 0;

  rs_encode(c, s);

  // Interleaving
  // ------------
  uint8_t d[63];
  uint8_t d1[7][9];

  // Fill temp d1 array
  for (i = 0; i < 9; i++)
  {
    for (j = 0; j < 7; j++)
    {
      d1[i][j] = s[(i * 7) + j];
    }
  }

  // Interleave and translate back to 1D destination array
  for (i = 0; i < 7; i++)
  {
    for (j = 0; j < 9; j++)
    {
      d[(i * 9) + j] = d1[j][i];
    }
  }

  // Gray Code
  // ---------
  uint8_t g[63];

  for (i = 0; i < 63; i++)
  {
    g[i] = gray_code(d[i]);
  }

  // Merge with sync vector
  // ----------------------
  const uint8_t sync_vector[126] =
  { 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0,
    0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1,
    0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1,
    0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1,
    1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1,
    0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1,
    1, 1, 1, 1, 1, 1
  };

  j = 0;

  for (i = 0; i < 126; i++)
  {
    if (sync_vector[i])
    {
      symbols[i] = 0;
    }
    else
    {
      symbols[i] = g[j] + 2;
      j++;
    }
  }

}

// get time from RTC, convert bcd to decimal
void getRTC()
{
  // Reset the register pointer
  Wire.beginTransmission(RTCADDR);
  byte zero = 0x00;
  Wire.write(zero);
  Wire.endTransmission();

  // request 1st 3 bytes from the RTC address
  Wire.requestFrom(RTCADDR, 3);

  // get the s/m/h time data
  sec = bcdToDec(Wire.read());
  mns = bcdToDec(Wire.read());
  hrs = bcdToDec(Wire.read() & 0b111111); // mask 24 hour time bit
}

// Convert binary coded decimal to normal decimal numbers
byte bcdToDec(byte val)
{
  return ( (val / 16 * 10) + (val % 16) );
}

// get input message[] U.C.
bool getMsg(char *m)
{
  char ch;
  int n;

  n = 0;
  if (Serial.available() > 0) {      // if input
    while (Serial.available() > 0) { // get input
      ch = Serial.read();            // use upper case as input
      if (ch == '\n') ch = '\0';     // end of text
      m[n++] = ch;
      delay(20);                     // let USB catch up
    }
    return true;                     // got input
  }
  return false;                      // no input
}

// clear msg and buffer
void clearBuf(char *m) {
  m[0] = '\0';
  while (Serial.available() > 0) Serial.read();
}

// display freq at x, y, f (Hz), cf (cHz), d)ecimal places
void dispFreq(u8g2_uint_t x, u8g2_uint_t y, double f, double cf, uint8_t d) {
  double fd;
  char buf[100];

  // sets font, cursor position and displays freq
  oled.setFont(u8g2_font_10x20_tf); // font
  oled.setFontPosTop(); // origin top

  fd = f + cf / 100; // calc freq

  oled.setCursor(x, y);
  oled.print(fd / 1000, d);
  oled.print("kHz");
}

// display message at at x), y), *m)essage
void dispMsg(u8g2_uint_t x, u8g2_uint_t y, char *m) {
  // sets font, cursor position and displays message
  oled.setFont(u8g2_font_7x13_tf); // font
  oled.setFontPosTop();
  oled.setCursor(x, y);
  oled.print(m);
}

// display time HH:MM:SS at x), y)
void dispTime(u8g2_uint_t x, u8g2_uint_t y) {
  // sets font, cursor position and displays message
  oled.setFont(u8g_font_7x14); // fix font for now
  oled.setFontPosTop();
  oled.setCursor(x, y);
  if (hrs < 10)
    oled.print("0");
  oled.print(hrs);
  oled.print(":");
  if (mns < 10)
    oled.print("0");
  oled.print(mns);
  oled.print(":");
  if (sec < 10)
    oled.print("0");
  oled.print(sec);
}