Friday 29 December 2017

Final PA RXTX and LPF band control - the Caprice system

The Caprice system

I previously gave a set of sketch code for the VFO or QRP to control the PA, to switch it from RX to TX and set the LPF to a band, 40, 30. 20m. The control signals are carried on two wires of a 4-way jack. The two control bits were connected to Arduino pins 12 & 13. But these pins, I realise are used in the QRP for TX & RX switching and so cannot be used for PA control. So I have changed to A0 & A1 pins.

This has meant a change also to the GPS sketches so the TX and RX serial data is read from these pins.

So the new code is as described below:

Caprice System

The Caprice system uses a 4 wire bus to supply power and signals. These are carried on a 4-way 3.5mm jack. The signals connect to Arduino A0 & A1.

Jack	Power/Signal	Socket Wire
Body		GND		Brown (B)
Ring		PIN A1	Yellow (Y)
Ring  	PIN A0	Orange (O)
Tip		5V OUT	Red (R)

OLED	Power/Signal	Wire
GND				Black (K)
VDD				Brown (B)
SCK				Red (R)
SDA				Orange (O)

PA Connections (top)
OLED
________________
o o o o o o o o |
O R B K
SDA VDD
  SCK GND

Jack
o o o o o o o o |
Y O R B     - -
A1  5V      12V
  A0  GND     GND

Outputs from GPS
A GPS uses the following connections:

GND
A0 - RX from GPS
A1 - TX to GPS
5V

Output from sketches
In the Caprice system sketches use the following connections:

GND
A0 - LSB of command (was 12)
A1 - MSB of command (was 13)
5V out

The output signals encode the signals to set the PA into one of 4 modes (0, 1, 2 & 3):

// modes, 00, 01, 10, 11 on A1, A0
#define RX 0
#define TX40 1
#define TX30 2
#define TX20 3

// SETUP
// include in setup...
  // jack PA control outputs
  pinMode(A0, OUTPUT);
  pinMode(A1, OUTPUT);

  // init band, RX
  setPA(TX40);                    // mode 40m, set PA LPF
  setPA(RX);                      // back to mode RX

This function outputs the modes to the PA.

// SET PA MODE
// modes 0, 1, 2, 3 output on A1, A0, active LOW
void setPA(int m) {
  // set mode
  switch (m) {
    case 0:
      digitalWrite(A1, HIGH);
      digitalWrite(A0, HIGH);    // RX
      break;
    case 1:
      digitalWrite(A1, HIGH);
      digitalWrite(A0, LOW);    // TX40
      break;
    case 2:
      digitalWrite(A1, LOW);
      digitalWrite(A0, HIGH);   // TX30
      break;
    case 3:
      digitalWrite(A1, LOW);
      digitalWrite(A0, LOW);    // TX20
      break;
  }
}

Relay wiring

		o------- 40m
in/Out---o/             /B6
		o-----|
	|----------|
	|	o------- 30m
	|---o/             /B7
		o------- 20m

PA Input
The PA inputs the signals and decodes them to modes (0, 1, 2 or 3):

// CONNECTIONS
// relay outputs (active HIGH)
#define PTT 5
#define B6 6
#define B7 7

// PARAMETERS
// modes
#define RX 0
#define TX40 1
#define TX30 2
#define TX20 3

// GLOBAL VARIABLES
char disp[][4] = {"", "40m", "30m", "20m"};    // display
byte mode, band;                               // mode 0-3, band 1-3

SETUP includes

  pinMode(A0, INPUT_PULLUP);     // bus LSB
  pinMode(A1, INPUT_PULLUP);     // bus MSB
  pinMode(PTT, OUTPUT);   // T/R relay
  pinMode(B6, OUTPUT);    // BAND 6
  pinMode(B7, OUTPUT);    // BAND 7

  // init 40m RX
  swPA(TX40);            // mode 40m, set LPF
  swPA(RX);              // back to mode RX


The mode input is read from A1 & A0

  // read mode input
  mode = getMode(digitalRead(A1), digitalRead(A0)); // read 0000 00xx

/ GET MODE
// A1 & A0 gets mode, returns 0-3 (RX-TX20)
byte getMode(bool b1, bool b0) {
  if (b1 == HIGH && b0 == HIGH) return RX;     // 0 RX
  if (b1 == HIGH && b0 == LOW)  return TX40;   // 1 TX 40m
  if (b1 == LOW && b0 == HIGH)  return TX30;   // 2 TX 30m
  if (b1 == LOW && b0 == LOW)   return TX20;   // 3 TX 20m
}

The mode is used to switch the TX/TX PTT relay (Antenna to PA or RX) and the LPF relays.

// SWITCH PA
// set PTT & LPF relays (HIGH = on), set band
void swPA(byte m) {
  // first check RX or TX
  if (m == RX)
    digitalWrite(PTT, LOW);              // RX
  else {
    digitalWrite(PTT, HIGH);             // TX

    // chose LPF freq
    switch (m) {
      case TX40:
        digitalWrite(B6, LOW);           // 40m
        digitalWrite(B7, LOW);
        break;
      case TX30:
        digitalWrite(B6, HIGH);          // 30m
        digitalWrite(B7, LOW);
        break;
      case TX20:
        digitalWrite(B6, HIGH);          // 20m
        digitalWrite(B7, HIGH);
        break;
    }
    band = m;                            // set band 1-3 for display
  }
}

The PA display shows UL Band when in RX mode, and "TX" when transmitting.

//  PICTURE LOOP
// Display band or "TX"
void dispUpdate() {
  oled.firstPage();
  do {
    dispMsg(60, 0, "PA");
    if (mode == RX) {                 // if RX
      dispMsgUL(30, 15, disp[band]); // display band
      dispMsgL(50, 50, "RX");
    }
    else {
      dispMsgUL(45, 15, "TX");       // otherwise show "TX"
      dispMsgL(50, 50, disp[band]);
    }
  } while (oled.nextPage());
}

Friday 8 December 2017

ICOM718 Digital INterface

PTT circuit

Here is the working ICOM "SEND" or PTT circuit. The PC or Mac is connected to the interface using a serial USB <-> RS232/TTL interface cable - costing just a few pounds on Amazon... The signals output from the adaptor that I bought are 3.3V logic, and there is a 5V power supply. I use the negative logic (active LOW) RTS signal to drive the gate of the BS170 MOSFET, when HIGH the 3.3V turns it on and a LOW level turns it off. The output of the BS170 stage is used to drive the diode input on the 1N35 optocoupler, thus giving an active LOW output to the ICOM SEND (PTT) line.

RTS PTT

The Audio circuits have yet to be tested and will be published here later. The box that has been made is shown below, Front and back.

IMG 1639

IMG 1640

The front has a switch which will be used to disable the computer PTT/RTS signal to prevent it being accidentally activated during system set-up, along with volume controls for the IN and OUT audio channels. The back has a cable with USB plug (containing the USB <-> RS232/TTL adaptor), the DIN connector to the ICOM718 (using a custom made cable to the rear ICOM 13 pin Input/Output, a Jack for CV-I control signals and two audio jacks for connections to the soundcard of the PC or Mac (I use a small plug-in USB sound interface).

Monday 27 November 2017

LATEST Sketches and Libraries

Here is the latest update of my sketches and libraries.

IMPORTANT

Previous sketches have output HIGH/LOW signals on a one connection on the 4-way jack on the VFO hardware. This signal was coded from D12 to switch the first PA I made from RX (HIGH) to TX (LOW).
The new sketches output active LOW codes on two pins of the 4-way jack, with Arduino UNO connections body=GND, ring=D12, ring=D13, tip=+5V.

These connections were chosen to also support a GPS receiver connection to the box.

For the PA-2 switching D12 is used as the LSB and D13 as the MSB of a two bit binary code. This code is then used as,

HH = RX

HL = TX 40m LPF

LH = TX 30m LPF

LL = TX 20m LPF

The codes are used to switch the three LPFs in the PA-2. And also to switch the Antenna connection from RX to TX.

Sunday 19 November 2017

Slow build QRP PA-2

I've been at this project for a while. But its slowly coming together. It is a QRP PA (2-3W output) for 40, 30 & 20m. It uses a MMIC pre-built module running on 12-15V, a set of three LPFs and an Arduino Nanocomputer to run things.

It provides TX RX switching and LPF switching from a two bit binary input on the rings of a a 4-way jack (the other connections are body GND & tip +5V. This input is compatible with my VFO output jack- this jack is used for GPS inputs when finding time and Maidenhead Locator, and as a TXRX/band command to the PA running other sketches which generate WSPR, JT65, CW, PSK, RTTY signals.

IMG 1694

PA

The display is simple, just the title "PA, the band big in the middle and TX or RX at the bottom.

Tuesday 7 November 2017

BARGAIN SDR

I just noticed that Elektor has cut it's prices in a sale! The Elektor SDR broad band receiver can now be bought for €30!!! This will tune from 100kHz to over 50MHz using an Si5351 DDS. programmed from an Arduino.

Put this together with an Arduino UNO at around €9, a small OLED display and a rotary encoder for around another €9, and you can build a very decent SDR for less then €50!!!

Check it out.

Screen Shot 2017 11 07 at 11 47 26

I built one, which I use with a low cost 96kHz ADC and HDSDR running on either on my MacBook or a PC.

Screen Shot 2017 11 07 at 11 52 08

Screen Shot 2017 11 07 at 11 56 30

I have put the software on this blog, look for my latest download of my sketches.

Friday 3 November 2017

Quicky on new FT8 mode

Some slides I have presented to BARS Tech Group

FT 8 001FT 8 002FT 8 004FT 8 005FT 8 006FT 8 007

Thursday 26 October 2017

Weather station - Failed

I found on the web this project ESP8266 Weather Station by Daniel Eichhorn. It uses an ESP8266 processor and WiFi to download weather from Wunderground and display it on an OELD.

It is wired up like this

Screen Shot 2017 10 26 at 18 06 10

This is the result of the example code they provide working

Screen Shot 2017 10 26 at 17 59 41



Which was great. But the next day when I switched it on, it did not work. I removed everything, re-installed everything on my MacbBook, no go. I installed everything on a different computer (my iMac), no go. I found it would connect to my router and was given an IP, but would not connect to any site on the internet. I have given up. Maybe my router is somehow blocking it??? Tried to check the router but login admin/password does not work, is the router defunct?

FAIL.

Saturday 14 October 2017

BARS Tech Group - Digital mode interface for the ICOM 718

This is the latest status from the BARS Constructors group.

Digital

Our aim is to interface our ICOM 718 to a PC so that we can transmit the digital FSK (audio) on USB SSB from the ICOM, and receive audio for decoding digital FSK on the PC. Using for example the new WSJT-X software which supports many new digital modes.

FT-8

One digital mode that was introduced only in July 2017 is called FT-8. This has taken the amateur fraternity by storm, and hundreds of QSO are pouring across the ether using this mode. Attached is a short presentation I gave last week about FT-8. Have listen on 7074kHz to hear the signals.

BASIC DIGITAL COMMS

We are making progress in the development and design of a digital interface for our ICOM 718.  I have read the manual twice, and tried to understand the connections, modes and numerous front panels buttons. One thing we did discover is that our ICOM needed a RESET before it was in the mode to output/input audio and provide more than 10W output!

Digital Mode Interfaces do three things:

Audio IN/OUT
PTT ON/OFF
CAT Data IN/OUT

1. Pass audio in and out between the ICOM and the Soundcard of the PC (if it has one, if not an external Soundcard is used) via isolating transformers and volume controls to avoid ground loops and set the levels for TX & RX.

We have tested this by connecting an audio output tone from my MacBook via a Soundcard - the STARTECH one we all purchased when we built the SRD receiver two years ago, to the ICOM. It needed 200mV p-p to output 10W RF on 40m (into a dummy load of course). We also tested the audio output which provided up to 500mV peak, by connecting to an external small Amp+LS. We also tested the use of the ICOM SEND connection (PTT) and this switched us to TX with no problems.

2. We need a cable on a 23 pin DIN plug to connect to the ICOM ACC port on the back of the Transceiver. This ACC plug provides some signals we need:

+8V
GND
AFO - audio output, 200-500mV
MOD - audio input, 100-200mV
SEND - PTT, low = TX

John tackled this, bought a 13 pin DIN and built a cable. I have to apologise to him as I told him we need only four wires, I forgot the +8V supply wire, sorry John… we need to add this.

In order to connect these signals to our interface we will need a multi-pin plug on the other end I have ordered a 5pin DIN plug and socket for this. Connections to be defined...

3. We need to connect to a USB port on the PC and derive from this virtual COM Port the RS232 signals (as you would get from a DB9 connector in the PC if it had one, but most PCs today do not support a DB9 RS232 connection). So a USB<->RS232 convertor has to be used, this is best implemented using a FT232 chip (a driver must be installed on Windows, the MacOS has one already). A cable can be purchased for a few pounds as a USB plug incorporating the chip and providing the six signals we need:

+5V
GND
TXD - used for CAT data
RXD - used for CAT data
RTS - used for PTT/SEND
CST

I have a cable in purchase. Note: the signals are not RS232 (+/-13V positive logic) but TTL 0/3V3 or 0/5V negative logic, active LOW.

4. Develop an isolated interface between the RS232/TTL RTS signal (active LOW) and the PTT/SEND connection (also active LOW) on the back of the ICOM. The RTS/TTL signal from the RS232 convertor is inverted by a BS170 MOSFET and drives the input to an optical encoder, the output pulls don the SEND line (PTT) of the ICOM718.

BOXING IT UP

After we have done this initial development, which is quite simple, it will be time to wire up a PCB and put the stuff in a box. This will give us basic digital comms.

CAT or CI-V

Then we can tackle the CAT system. CAT is called CI-V by ICOM (Computer Interface V-five). It is a single wire serial bus used for two way communication between the PC and the ICOM. The bus line is normally pulled HIGH by a resistor in the ICOM, and is pulled low by either our interface or the ICOM itself. When the interface pulls it LOW it is transmitting data to the ICOM, and when ICOM pulls it LOW it is sending data to the PC.

The USB to RS232 convertor (above) has two signals /TXD & /RXD to send data in/out of the PC from/to any digital software we are using for controlling the ICOM. Over the link a lot of the operating modes of the ICOM can be controlled, the most common of which is to switch bands and change frequency. Data is sent at a baud rate which can be programmed in the ICOM (I think/hope) as I read it the default is 19200baud which has to be set up in the PC software.

This hardware interface has yet to be designed.

SOFTWARE - volunteers please

We need a group of us to investigate, download and learn about the many software packages that are on the web so we know how they interface for Audio, PTT and CAT. And so we know what they can do (RTTY, PSK31, digital modes like WSPR, FT-8, JT-65, SSTV…).

So that’s where we are right now, progress. See you in a couple of weeks for more for the nest step - checking out our audio isolation transformers, both the ones I found on eBay or the one’s that Franks has kindly given us, to see which is best. We need to check also the audio signal levels coming out of the PC sound card (both Mac and PC I suggest).

Sunday 1 October 2017

IMPORTANT for MacOS users

Whatever you do, do NOT install the FTDI drivers from FTDIChip on your MacOS Sierra. They block the use of the Apple Driver, which works just fine, and you will not be able to acces your Arduino board!

Sunday 10 September 2017

The fight to read an ASCII string into Arduino

If you have a terminal program sending characters of a string input at 9600 baud to the USB port of an Arduino, then they arrive in just over 1ms per character. In 1ms the Arduino can do a lot of things.

So a program like this won't work,

//  GET  MESSAGE
// Try to get a message in from the terminal
void getMsg(char *m) }
  int p;
  
  p = 0;
  while (Serial.available() > 0) {
   m[p++] = Serial.read();
  }
}

Why? Because after the first character arrives and Serial.Available() says it has done so, the next line Serial.read() happens very fast, a long time before the next character at 9600 baud arrives. So the next pass of the while loop fails, as there is no character in the RAM buffer.

What to do? Well wait for the next character before trying to read it, of course. Like this

// GET MESSAGE
// get line m (EOL '\n')
bool getMsg(char *m) {
  int p;

  if (Serial.available() > 0) {                 // data available ?
    p = 0;
    while ((m[p] = Serial.read()) != '\n') {    // end of line?
      p++;
      while (!Serial.available());              // wait for next char
    }
    m[p] = '\0';                                // terminate string
    return true;                                // data received
  }
  return false;                                 // no data
}

Here you can see that after the first iteration of the while loop and the Serial.read() we wait for the next character to arrive.

The next problem is to know when all the string's characters have been read, and this is done by marking the end of the input with a special character - in this case the program expects a LF or '\n'. You must make sure your terminal is set up to end the line it sends with this character.

Screen Shot 2017 09 10 at 17 13 15

Saturday 2 September 2017

Got a new toy - USB Scope

Elektor magazine just offered me a deal I could not refuse. A SmartScope from LabNation. This is a 100ms/s sampling scope with software for PC & MacOS. Here the display of a 7.1MHz output from my VFO.

IMG 1453

Screen Shot 2017 09 02 at 14 57 14

Thursday 24 August 2017

LATEST Sketches and libraries

Here's the 24 Aug 2017 update of all my current sketches and libraries. Mainly tidying up and checking functionality on both AD9851 & Si5351 synthesiser platforms.

Download

Tuesday 22 August 2017

NEW NEW NEW!!! New GPS read code, display Lat/Lon or Maidenhead Locator, set RTC

I have a couple of new Arduino code sketches for GPS. The problem with my previous code was that it repeatedly read the GPS time, and set the RTC. This took too long and so the time display of the seconds jumped to every two seconds!

So I have completely re-written the code, it now waits for the GPS "fix", programs the RTC, gets the lat & Lon and calculates the Maidenhead Locator, then displays these, but displays the time from the programmed RTC, which then goes by seconds. Phew!!

My header file "Oled_128X64_I2C.h" has been published previously, look back and you will find it.

Just to remind you I use a VK-163 GPS (look on Amazon or ebay) which connects by a 4-way 3.5mm jack, and provides NMEA GPS data at 9600 baud. The block digram of the "VFO" I use is below, it uses an Arduino UNO with an AD9851 synthesiser and MMIC output amplifier giving up to 50mW output. The Arduino also drives an OLED 128x64 pixel display, using a U8g2lib library (look up on GitHUb). This is a great box of tricks and I can use it for many different sketches from GPS to WSPR, JT65, RTTY, PSK31, QRSS and DFCW...

Screen Shot 2017 08 31 at 09 31 29

Download here

CODE LAT & LON DISPLAY

// GPS_DATE_TIME_LATLON
// V1.0 22-8-17 after fix, set RTC, then display RTC Time & Date
// jack
// TIP +5V
// ring 13 (RX LOW, FROMGPS)
// ring 12 (TX LOW, TOGPS)
// GND

// HEADRERS & LIBRARIES
#include "Oled_128X64_I2C.h"
#include "SoftwareSerial.h"

// CONNECTIONS
#define FROMGPS 13
#define TOGPS 12
#define SW 4

// I2C COMMS DATA
// RTC address
#define RTCADDR 0x68

// OBJECTS
SoftwareSerial gps(FROMGPS, TOGPS);

// GLOBAL VARIABLES
char gpsbuf[200];   // GPS input buffer

char tm[20];        // time HHMMSS
char fix[5];        // fix A|V, init void
char dt[20];        // date YYMMDD
char la[15];        // latitude
char ns[2];         // NS
char lo[15];        // longitude
char ew[2];         // EW

// data converted to numeric (decimal)  bytes
byte hrs, mns, sec;
byte yr, mth, dy;
byte dow;
double lat, lon;

// Maidenhead Locator
char mh[10] = "";


// SETUP
void setup() {
  // pin modes
  pinMode(FROMGPS, INPUT);
  pinMode(TOGPS, OUTPUT);
  pinMode(SW, INPUT_PULLUP);

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

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

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

// LOOP
void loop() {
  dispUpdate();                // init display AWTG GPS

  do {                         // read sentence until fix confirmed
    getGPS("$GPRMC", gpsbuf);    // get $GPRMC
    fieldsGPS(gpsbuf);           // dig out ASCII fields
  } while (strcmp(fix, "A") != 0);

  convert();                 // convert Time & Date to numeric values
  dow = calcDow(yr, mth, dy);  // calulate day of week
  setRTC();                    // program RTC with GPS time

  lat = convertPos(la, ns);    // convert Lat & Lon to numeric
  lon = convertPos(lo, ew);
  findMH(mh, lat, lon);        // find Maidenhead Locator

  while (!button()) {          // update Time & Date display from RTC
    readRTC();                 // read RTC
    dispUpdate();
  }
  strcpy(fix, "V");            // button pressed restart
}

// button pressed?
bool button() {
  if (digitalRead(SW) == LOW) { // button pressed?
    while (!digitalRead(SW)); // wait for release
    return true;
  }
  else {
    return false;
  }
}

// GPS FUNCTIONS
// get sentence into buffer, for example
//   $GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62
void getGPS(char *sntc, char *buf) {
  do {                            // find sntc
    getline(buf);
  } while (strncmp(buf, sntc, 6) != 0);
}

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

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

// find GPS fields
void fieldsGPS(char *buf) {
  // extract strings from $GPRMC fields
  xtract(buf, 1, tm);          // time HHMMSS
  xtract(buf, 2, fix);         // fix A or V
  xtract(buf, 9, dt);          // date YYMMDD

  xtract(buf, 3, la);          // latitude
  xtract(buf, 4, ns);          // NS
  xtract(buf, 5, lo);          // longitude
  xtract(buf, 6, ew);          // EW
}

// 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 GPS ASCII Time and Date to decimal bytes
void convert() {

  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
}

// convert ASCII field starting at field pointer, to byte
byte strtob(char *field, int fp) {
  char out[20];

  strncpy(out, field + fp, 2);   // copy 2 char
  return (byte)atoi(out);        // return byte
}

// convert Lat, Lon strings to decimal +/-NS|EW
double convertPos(char *pos, char *d) {
  double pp, mm, ans;
  int dd;

  pp = atof(pos);                        // get in decimal ddmm.mmmmmmm
  dd = (int)pp / 100;                    // get degrees part
  mm = pp - (100 * dd);                  // get minutes
  ans = dd + (double)mm / 60.0;          // calc decimal degrees

  if (strcmp(d, "N") == 0 || strcmp(d, "E") == 0)  // if positive
    return ans;
  else
    return - ans; // negative
}

// DATE OF WEEK CALC
// calc dow, return decimal byte, Sun = 0
byte calcDow(byte year, byte month, byte day)
{
  unsigned long days;
  unsigned int febs;
  unsigned int months[] =
  {
    0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 // days until 1st of month
  };

  days = year * 365;   // days up to year

  febs = year;
  if (month > 2) febs++; // number of completed Februaries

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

  days += months[month - 1] + day;

  return (byte)(((days + 5) % 7)); // sun = 0
}

// FIND MAIDENHEAD LOCATOR
void findMH(char *dst, double fa, double fo) {
  int a1, a2, a3;
  int o1, o2, o3;
  double rd;

  // Latitude
  rd = fa + 90.0;
  a1 = (int)(rd / 10.0);
  rd = rd - (double)a1 * 10.0;
  a2 = (int)(rd);
  rd = rd - (double)a2;
  a3 = (int)(24.0 * rd);

  // Longitude
  rd = fo + 180.0;
  o1 = (int)(rd / 20.0);
  rd = rd - (double)o1 * 20.0;
  o2 = (int)(rd / 2.0);
  rd = rd - 2.0 * (double)o2;
  o3 = (int)(12.0 * rd);

  dst[0] = (char)o1 + 'A';
  dst[1] = (char)a1 + 'A';
  dst[2] = (char)o2 + '0';
  dst[3] = (char)a2 + '0';
  dst[4] = (char)o3 + 'A';
  dst[5] = (char)a3 + 'A';
  dst[6] = '\0';
}

// SET RTC
// set date and time bytes to RTC BCD
void setRTC() {
  // 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) );
}

// READ RTC
void readRTC() {
  // Reset the RTC register pointer
  Wire.beginTransmission(RTCADDR);
  Wire.write(0x00);
  Wire.endTransmission();

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

  // get the time date
  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 = Sunday
  dy  = bcdToDec(Wire.read()); // 1 - 31
  mth = bcdToDec(Wire.read()); // 0 = jan
  yr  = bcdToDec(Wire.read()); // ..yy
}

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

// PICTURE LOOP
// this version displays lat/lon
void dispUpdate() {
  oled.firstPage();
  do {
    dispMsg(30, 0, "GPS Lat/Lon");

    if (strcmp(fix, "A") != 0) {   // no fix?
      dispMsgL(30, 15, "AWTG GPS");
    }
    else {
      dispNum(10, 15, lat, 2);
      dispMsg(50, 15, ns);
      dispNum(70, 15, lon, 2);
      dispMsg(110, 15, ew);
      dispDate(15, 32, dow, dy, mth, yr);
      dispTimeL(25, 47, hrs, mns, sec);
    }
  } while ( oled.nextPage() );
}


CODE MAIDENHEAD LOCATOR DISPLAY
// GPS_DATE_TIME_MH
// V1.0 22-8-17 after fix, set RTC, then display RTC Time & Date
// jack
// TIP +5V
// ring 13 (RX LOW, FROMGPS)
// ring 12 (TX LOW, TOGPS)
// GND

// HEADERS & LIBRARIES
#include "Oled_128X64_I2C.h"
#include "SoftwareSerial.h"

// CONNECTIONS
#define FROMGPS 13
#define TOGPS 12
#define SW 4

// I2C COMMS DATA
// RTC address
#define RTCADDR 0x68

// OBJECTS
SoftwareSerial gps(FROMGPS, TOGPS);


// GLOBAL VARIABLES
char gpsbuf[200];   // GPS input buffer

char tm[20];        // time HHMMSS
char fix[5];        // fix A|V, init void
char dt[20];        // date YYMMDD
char la[15];        // latitude
char ns[2];         // NS
char lo[15];        // longitude
char ew[2];         // EW

// data converted to numeric (decimal)  bytes
byte hrs, mns, sec;
byte yr, mth, dy;
byte dow;
double lat, lon;

// Maidenhead Locator
char mh[10] = "";

// SETUP
void setup() {
  // pin modes
  pinMode(FROMGPS, INPUT);
  pinMode(TOGPS, OUTPUT);
  pinMode(SW, INPUT_PULLUP);

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

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

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

// LOOP
void loop() {
  dispUpdate();                // init display AWTG GPS

  do {                         // read sentence until fix confirmed
    getGPS("$GPRMC", gpsbuf);    // get $GPRMC
    fieldsGPS(gpsbuf);           // dig out ASCII fields
  } while (strcmp(fix, "A") != 0);

  convert();                 // convert Time & Date to numeric values
  dow = calcDow(yr, mth, dy);  // calulate day of week
  setRTC();                    // program RTC with GPS time

  lat = convertPos(la, ns);    // convert Lat & Lon to numeric
  lon = convertPos(lo, ew);
  findMH(mh, lat, lon);        // find Maidenhead Locator

  while (!button()) {          // update Time & Date display from RTC
    readRTC();                 // read RTC
    dispUpdate();
  }
  strcpy(fix, "V");            // button pressed restart
}

// button pressed?
bool button() {
  if (digitalRead(SW) == LOW) { // button pressed?
    while (!digitalRead(SW)); // wait for release
    return true;
  }
  else {
    return false;
  }
}

// GPS FUNCTIONS
// get sentence into buffer, for example
//   $GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62
void getGPS(char *sntc, char *buf) {
  do {                            // find sntc
    getline(buf);
  } while (strncmp(buf, sntc, 6) != 0);
}

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

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

// find GPS fields
void fieldsGPS(char *buf) {
  // extract strings from $GPRMC fields
  xtract(buf, 1, tm);          // time HHMMSS
  xtract(buf, 2, fix);         // fix A or V
  xtract(buf, 9, dt);          // date YYMMDD

  xtract(buf, 3, la);          // latitude
  xtract(buf, 4, ns);          // NS
  xtract(buf, 5, lo);          // longitude
  xtract(buf, 6, ew);          // EW
}

// 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 GPS ASCII Time and Date to decimal bytes
void convert() {

  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
}

// convert ASCII field starting at field pointer, to byte
byte strtob(char *field, int fp) {
  char out[20];

  strncpy(out, field + fp, 2);   // copy 2 char
  return (byte)atoi(out);        // return byte
}

// convert Lat, Lon strings to decimal +/-NS|EW
double convertPos(char *pos, char *d) {
  double pp, mm, ans;
  int dd;

  pp = atof(pos);                        // get in decimal ddmm.mmmmmmm
  dd = (int)pp / 100;                    // get degrees part
  mm = pp - (100 * dd);                  // get minutes
  ans = dd + (double)mm / 60.0;          // calc decimal degrees

  if (strcmp(d, "N") == 0 || strcmp(d, "E") == 0)  // if positive
    return ans;
  else
    return - ans; // negative
}

// DATE OF WEEK CALC
// calc dow, return decimal byte, Sun = 0
byte calcDow(byte year, byte month, byte day)
{
  unsigned long days;
  unsigned int febs;
  unsigned int months[] =
  {
    0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 // days until 1st of month
  };

  days = year * 365;   // days up to year

  febs = year;
  if (month > 2) febs++; // number of completed Februaries

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

  days += months[month - 1] + day;

  return (byte)(((days + 5) % 7)); // sun = 0
}

// FIND MAIDENHEAD LOCATOR
void findMH(char *dst, double fa, double fo) {
  int a1, a2, a3;
  int o1, o2, o3;
  double rd;

  // Latitude
  rd = fa + 90.0;
  a1 = (int)(rd / 10.0);
  rd = rd - (double)a1 * 10.0;
  a2 = (int)(rd);
  rd = rd - (double)a2;
  a3 = (int)(24.0 * rd);

  // Longitude
  rd = fo + 180.0;
  o1 = (int)(rd / 20.0);
  rd = rd - (double)o1 * 20.0;
  o2 = (int)(rd / 2.0);
  rd = rd - 2.0 * (double)o2;
  o3 = (int)(12.0 * rd);

  dst[0] = (char)o1 + 'A';
  dst[1] = (char)a1 + 'A';
  dst[2] = (char)o2 + '0';
  dst[3] = (char)a2 + '0';
  dst[4] = (char)o3 + 'A';
  dst[5] = (char)a3 + 'A';
  dst[6] = '\0';
}

// SET RTC
// set date and time bytes to RTC BCD
void setRTC() {
  // 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) );
}

// READ RTC
void readRTC() {
  // Reset the RTC register pointer
  Wire.beginTransmission(RTCADDR);
  Wire.write(0x00);
  Wire.endTransmission();

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

  // get the time date
  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 = Sunday
  dy  = bcdToDec(Wire.read()); // 1 - 31
  mth = bcdToDec(Wire.read()); // 0 = jan
  yr  = bcdToDec(Wire.read()); // ..yy
}

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

// PICTURE LOOP
// this version displays MH
// this version displays lat/lon
void dispUpdate() {
  oled.firstPage();
  do {
    dispMsg(40, 0, "GPS MH");
    
    if (strcmp(fix, "A") != 0) {   // no fix?
      dispMsgL(30, 15, "AWTG GPS");
    }
    else {
      dispMsgL(35, 15, mh);
      dispDate(15, 32, dow, dy, mth, yr);
      dispTimeL(25, 47, hrs, mns, sec);
    }
  } while ( oled.nextPage() );
}

Saturday 19 August 2017

New! ADS9851 library update, QRSS & QRSS DFCW for AD9851 VFO

Been a bit busy today with some updates and a couple of re-writes. First off I noticed a problem with the library ADS9851 which I wrote for the AD9851 Analog Digital Synthesiser. When the "down" function was called it did not completely turn off the output! It continued to output a low level signal 500Hz below the programmed one... I think this was due to the reset pulse applied being too short - it has to be at least 5 system cycles long. So I have included a small 2us delay in the "pulse" function to give a longer LOW-HIGH-LOW pulse. The same pulse is used for "init" so will serve both functions.

Here's the ".h" and ".cpp" code for the updated library,

CODE for ADS9851.h
// Arduino Library for AD9851 frequency synthesiser module, with 30MHz clock
// V1.1 18-8-17 Antony Watts, M0IFA
// frequency in Hz and cHz
// W_CLK, FQ_UD, DATA, RESET to any pins
// void begin(int W_CLK, int FQ_UD, int DATA, int RESET); intialise pins and reset AD9850
// void setFreq(double Hz, double Chz, uint8_t p); set frequency(Hz) and centi-freq(Chz)
// void calibrate(double calHz); change xtal frequency from standard 125MHz to new value
// void down(); power down, power up with setFreq()
// phase coding, 0-180 in 11.25deg steps 0x00, 0x01, 0x02, 0x04, 0x08, 0x10
// REFCLK = 1 for x6 multiplier

#ifndef ADS9851_H
#define ADS9851_H

#include "Arduino.h"

#define ADS_XTAL 180000000.0

class ADS9851 {

	public:
		ADS9851();

		void begin(int W_CLK, int FQ_UD, int DATA, int RESET);
		void setFreq(double Hz, double Chz, uint8_t phase);
		void calibrate(double calHz);
                void down();

	private:
		int _W_CLK;
		int _FQ_UD;
		int _DATA;
		int _RESET;

		double _calFreq;

                void update(uint32_t d, uint8_t p);
		void pulse(int _pin);
                void init();

};

#endif


And the CODE for ADS9851.cpp
// Arduino Library for AD9851 frequency synthesiser module, with 30MHz clock
// V1.1 19-8-17 Antony Watts, M0IFA, pulser code update
// frequency in Hz and cHz
// W_CLK, FQ_UD, DATA, RESET to any pins
// void begin(int W_CLK, int FQ_UD, int DATA, int RESET); init, reset, serial mode
// void setFreq(double Hz, double Chz, uint8_t p); set f(Hz) and cHz(Chz), phase
// void calibrate(double calHz); change xtal frequency from standard 180MHz (30MHz x6)
// void down(); power down, power up with setFreq()
// phase in steps 0x00, 0x01, 0x02, 0x04, 0x08, 0x10 (11.5, 22.5, 45. 90, 180deg)
// REFCLK = 1 for x6 multiplier

#include "Arduino.h"
#include "ADS9851.h"

// constructor
ADS9851::ADS9851() {

}

// init calFreq, pins, reset & serial mode
void ADS9851::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 d2-d0
        pulse(_FQ_UD);

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

void ADS9851::update(uint32_t fW, uint8_t cP) {
    for (int i=0; i <4 ; i++, fW >>= 8) {
       shiftOut(_DATA, _W_CLK, LSBFIRST, fW); // output freq byte
    }
    shiftOut(_DATA, _W_CLK, LSBFIRST, cP); // output control & phase byte

    pulse(_FQ_UD);
}

// 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
        bitSet(p, 0); // REFCLK on, = ppppp001
	update(delta, p);
}

// turn off, zero freq
void ADS9851::down() {
        update(0, 0);
}

// clear freq to zero, set PD bit, set REFCLK bit
void ADS9851::init() {

	pulse(_FQ_UD);
	update(0, 0b00000101); // fW=0, PD=1 (pwr dwn), REFCLK=1 (on)
}

// pulse a pin LOW-HIGH-LOW
void ADS9851::pulse(int _pin) {
        digitalWrite(_pin, LOW);
	digitalWrite(_pin, HIGH);
        delayMicroseconds(2); // 2us pulse for init & down
	digitalWrite(_pin, LOW);
}

// load a new value for _calFreq
void ADS9851::calibrate(double calXtal) {
	_calFreq = calXtal;
}


NEW QRSS_CW and QRSS_DFCW sketches

Two very interesting slow CW modes are QRSS and QRSS DFCW. QRSS sends morse code at a very low speed, in this case it is 3sec/dot known as QRSS3. The DFCW mode sends both dots and dashes with the same lenght, but at different frequencies, in this case 5Hz apart, dash higher, dot lower.

CODE for QRSS_CW
// QRSS_CW, sends 40m QRSS3 message from the KB
// V1.0 18-8-17
// AD9851
// RESET 8
// DATA 9
// FQ_UD 10
// W_CLK 11
// OLED 128x64
// SDA = A4
// SCL = A5


// OLED, AD9851 libraries
#include "Oled_128X64_I2C.h"
#include "ADS9851.h"

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

// 3sec dot time
#define DOT 3000

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

// ASCII input
char msg[30];

// frequency settings - fixed for now
volatile double freqHz = 7000800; // (Hz) start frequency 7000.8kHz
volatile double freqChz = 0;      // (cHz) additional 0cHz
uint8_t phase = 0;                // init phase

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

// morse code strings, _ = dot space, 0-9 numbers, 10-36 A..Z
// table from 0 - 36
char morse[][8] = {
  "-----_", // 0
  ".----_", // 1-9
  "..---_",
  "...--_",
  "....-_",
  "....._",
  "-...._",
  "--..._",
  "---.._",
  "----._",
  ".-_",   // A
  "-..._", // B
  "-.-._", // C
  "-.._",  // D
  "._",    // E
  "..-._", // F
  "--._",  // G
  "...._", // H
  ".._",   // I
  ".---_", // J
  "-.-_",  // K
  ".-.._", // L
  "--_",   // M
  "-._",   // N
  "---_",  // O
  ".--._", // P
  "--.-_", // Q
  ".-._",  // R
  "..._",  // S
  "-_",    // T
  "..-_",  // U
  "...-_", // V
  ".--_",  // W
  "-..-_", // X
  "-.--_", // Y
  "--.._", // Z
  "__",    // word space
};

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

  // 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.down();

  msg[0] = '\0'; // init message
  
  dispUpdate(); //  init display
}

void loop()
{
  // get message, send
  if (getMsg(msg)) {
    qrssOut(msg);
  }
}

// get input message
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();            // get char
      if (ch == '\n' || ch == '\r') ch = '\0';     // end of text
      m[n++] = ch;
      delay(20);                     // let USB catch up
    }
    return true;                     // got input
  }
  return false;                      // no input
}

// look up morse string, send char by char
void qrssOut(char *m)
{
  static byte ndx;
  byte n;
  char c;

  dispUpdate(); // display msg

  // step along msg chraracters
  ndx = 0;
  while (m[ndx] != '\0')
  {
    // convert SPACE
    if (m[ndx] == 32)
      c = m[ndx] + 4;

    // convert ASCII
    else if (m[ndx] >= 48 && m[ndx] <= 57) // table 0-9
      c = m[ndx] - 48;
    else if (m[ndx] >= 65 && m[ndx] <= 90) // table A-Z (uc)
      c = m[ndx] - 55;
    else if (msg[ndx] >= 97 && msg[ndx] <= 122) // table a-z (lc)
      c = m[ndx] - 87;

    // output morse, up to SPACE
    n = 0;
    while (morse[c][n] != '_')
    {
      if (morse[c][n] == '.')  dotOut(); // dot out
      else if (morse[c][n] == '-')  dashOut(); // dash out
      n++;
    }
    spaceOut(); // end of char
    ndx++;
  }

  m[0] = '\0'; // clear message
  dispUpdate();
}

// send a dot for DOT time
void dotOut()
{
  unsigned long t;
  ads.setFreq(freqHz, freqChz, phase);
  t = millis();
  while (millis() < t + DOT);
  ads.down();
  t = millis();
  while (millis() < t + DOT);
}

// send a dash for 3* DOT time
void dashOut()
{
  unsigned long t;
  ads.setFreq(freqHz, freqChz, phase);
  t = millis();
  while (millis() < t + DOT * 3);
  ads.down();
  t = millis();
  while (millis() < t + DOT);
}

// word space for 2 * DOT time (each character has its own one DOT space
void spaceOut()
{
  unsigned long t;
  t = millis();
  while (millis() < t + DOT * 2);
}

// picture loop, display init data
void dispUpdate() {
  oled.firstPage();
  do {
      dispMsg(50, 0, "QRSS3");
      dispFreq(15, 20, freqHz, freqChz, 2);
      dispMsg(5, 40, msg);
  } while ( oled.nextPage() );
}


And the CODE for QRSS_DFCW
// QRSS_DFCW sends a DFCW3 message from the KB
// V1.0 18-8-17 like QRSS but with equal time for dot and dash
// with DFCW dot/dash spacing of 10Hz
// tunable in 10Hz steps
// 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 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

// encoder
#define CLK 2
#define DT 3

// 3sec dot time
#define DOT 3000

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

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

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

// frequency settings
volatile double freqHz = 7000800; // (Hz) start frequency 7000.8kHz
volatile double dotChz = 0;       // (cHz) zero
volatile double dashChz = 500;    // (cHz) additional 5Hz
volatile double freqStep = 10;    // (Hz) freqHz +/- step
uint8_t phase = 0;                // init phase

// ASCII input
char msg[30];

// morse code strings, _ = dot space, 0-9 numbers, 10-36 A..Z
// table from 0 - 36
char morse[][8] = {
  "-----_", // 0
  ".----_", // 1-9
  "..---_",
  "...--_",
  "....-_",
  "....._",
  "-...._",
  "--..._",
  "---.._",
  "----._",
  ".-_",   // A
  "-..._", // B
  "-.-._", // C
  "-.._",  // D
  "._",    // E
  "..-._", // F
  "--._",  // G
  "...._", // H
  ".._",   // I
  ".---_", // J
  "-.-_",  // K
  ".-.._", // L
  "--_",   // M
  "-._",   // N
  "---_",  // O
  ".--._", // P
  "--.-_", // Q
  ".-._",  // R
  "..._",  // S
  "-_",    // T
  "..-_",  // U
  "...-_", // V
  ".--_",  // W
  "-..-_", // X
  "-.--_", // Y
  "--.._", // Z
  "__",    // word space
};

// interrupt freq change flag
bool freqChange;

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

  // for msg input/display
  Serial.begin(9600);

  // 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.down();

  msg[0] = '\0'; // init message

  dispUpdate();
}

void loop()
{
  if (freqChange) {
    freqChange = false;
    dispUpdate();
  }
  if (getMsg(msg)) {
    dfcwOut(msg);
  }
}

// get input message
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();            // get char
      if (ch == '\n' || ch == '\r') ch = '\0';     // end of text
      m[n++] = ch;
      delay(20);                     // let USB catch up
    }
    return true;                     // got input
  }
  return false;                      // no input
}

// 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;
  }
}

// look up morse string, send char by char
void dfcwOut(char *m)
{
  static byte ndx;
  byte n;
  char c;

  dispUpdate();

  // step along msg chraracters
  ndx = 0;
  while (m[ndx] != '\0')
  {
    // convert to position in morse table
    // convert SPACE
    if (msg[ndx] == 32)
      c = m[ndx] + 4;

    // convert ASCII
    else if (m[ndx] >= 48 && m[ndx] <= 57) // table 0-9
      c = m[ndx] - 48;
    else if (m[ndx] >= 65 && m[ndx] <= 90) // table A-Z (uc)
      c = m[ndx] - 55;
    else if (m[ndx] >= 97 && m[ndx] <= 122) // table a-z (lc)
      c = m[ndx] - 87;

    // output morse, up to SPACE
    n = 0;
    while (morse[c][n] != '_')
    {
      if (morse[c][n] == '.')  dotOut(); // dot out
      else if (morse[c][n] == '-')  dashOut(); // dash out
      n++;
    }
    spaceOut(); // end of char
    ndx++;
  }
  m[0] = '\0';
  dispUpdate();
}


// send a dot for DOT time
void dotOut()
{
  unsigned long t;

  ads.setFreq(freqHz, dotChz, phase); //  send dot
  t = millis();
  while (millis() < t + DOT);
  ads.down();                             // small space
  t = millis();
  while (millis() < t + DOT / 2);
}

// send a dash for 3* DOT time
void dashOut()
{
  unsigned long t;

  ads.setFreq(freqHz, dashChz, phase); // send dash, 3x dot
  t = millis();
  while (millis() < t + DOT);
  ads.down();                              // small space
  t = millis();
  while (millis() < t + DOT / 2);
}

// word space for 2 * DOT time (each character has its own one DOT space
void spaceOut()
{
  unsigned long t;
  t = millis();
  while (millis() < t + DOT * 2);
}

// picture loop, display init data
void dispUpdate() {
  oled.firstPage();
  do {
    dispMsg(50, 0, "DFCW3");
    dispFreq(15, 20, freqHz, dotChz, 2);
    dispMsg(5, 40, msg);
  } while ( oled.nextPage() );
}


This is the transmitter and the received signal in DFCW using the ARGO software,

IMG 1434

IMG 1433

Wednesday 16 August 2017

Some Arduino routines for GPS

There are some very cheap GPS receivers on the market today, notably GPS originally designed for plugging into dash-cams. These receivers have a 4-way 3.5mm jack connection, and send out 9600 baud serial data in ASCII format.

You can also buy very cheap Arduino Nano boards and cheap OLED 128x64 pixel displays.

Putting this all together you can have a useful tool for amateur radio to display your latitude, longitude, Maidenhead Locator, date and time. Here are few of the code snippets useful for handling the data

HEADER definitions, libraries, defines and variables

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

// GPS connections,
#define FROMGPS 12
#define TOGPS 13
#define SW 4

// GPS data buffer
char gpsbuf[200];

// data extracted from $GPRMC, ACSII
char tm[20];        // time HHMMSS
char fix[5];        // fix A|V, init void
char dt[20];        // date YYMMDD
char la[15];        // latitude
char ns[2];         // NS
char lo[15];        // longitude
char ew[2];         // EW

// Maidenhead Locator
char mh[10] = "";

// Date, Time Lat & lon decimal
byte hrs, mns, sec;
byte yr, mth, dy;
byte dow;
double lat, lon;

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


CODE for setup
void setup() {
  // pins
  pinMode(FROMGPS, INPUT);
  pinMode(TOGPS, OUTPUT);
  pinMode(SW, INPUT_PULLUP);

  // I2C init
  Wire.begin();

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

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

  strcpy(fix, "V");
  dispUpdate();
}


The basic loop CODE
void loop() {
  getGPS();                      // get GPS, extract data

  if (strcmp(fix, "A") == 0) {   // when GPS Aquired
    getDateTime();
    getMH();
    dispUpdate();
  }
}

And finally the functions
// get RMC line data
void getGPS() {
  do {
    getline(gpsbuf);
  } while (strncmp(gpsbuf, "$GPRMC", 6) != 0);

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

  xtract(gpsbuf, 3, la);          // latitude
  xtract(gpsbuf, 4, ns);          // NS
  xtract(gpsbuf, 5, lo);          // longitude
  xtract(gpsbuf, 6, ew);          // EW
}

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

  p = 0;                         // buffer pointer
  do {
    if (gps.available() > 0) {   // data?
      c = gps.read();            // read character
      out[p++] = c;              // put in buffer
    }
  } while ( c != '\n' );          // stop on /n
  out[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
}

// ================ Date & Time, Dow =================
void getDateTime() {
  // get GPS data in bytes, calc dow
  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(yr, mth, dy);

}

// convert 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
}

// calc dow
byte calcDow(byte year, byte month, byte day)
{
  unsigned long days;
  unsigned int febs;
  unsigned int months[] =
  {
    0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 // days until 1st of month
  };

  days = year * 365;   // days up to year

  febs = year;
  if (month > 2) febs++; // number of completed Februaries

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

  days += months[month - 1] + day;

  return (byte)(((days + 5) % 7)); // sun = 0
}

// ====================Maidenhead functions================
// calculate maideng=head locator from lat & lon
void getMH() {
  // extract lat * lon from GPS data
  xtract(gpsbuf, 3, la);
  xtract(gpsbuf, 4, ns);
  xtract(gpsbuf, 5, lo);
  xtract(gpsbuf, 6, ew);

  lat = convertPos(la, ns);
  lon = convertPos(lo, ew);

  calcMH(mh, lat, lon);
}

// convert Lat, Lon strings to decimal +/-NS|EW
double convertPos(char *pos, char *d) {
  double pp, mm, ans;
  int dd;

  pp = atof(pos);                        // get in decimal ddmm.mmmmmmm
  dd = (int)pp / 100;                    // get degrees part
  mm = pp - (100 * dd);                  // get minutes
  ans = dd + (double)mm / 60.0;          // calc decimal degrees

  if (strcmp(d, "N") == 0 || strcmp(d, "E") == 0)  // if positive
    return ans;
  else
    return - ans; // negative
}

// calc MH from lat & lon
void calcMH(char *dst, double fa, double fo) {
  int a1, a2, a3;
  int o1, o2, o3;
  double rd;

  // Latitude
  rd = fa + 90.0;
  a1 = (int)(rd / 10.0);
  rd = rd - (double)a1 * 10.0;
  a2 = (int)(rd);
  rd = rd - (double)a2;
  a3 = (int)(24.0 * rd);

  // Longitude
  rd = fo + 180.0;
  o1 = (int)(rd / 20.0);
  rd = rd - (double)o1 * 20.0;
  o2 = (int)(rd / 2.0);
  rd = rd - 2.0 * (double)o2;
  o3 = (int)(12.0 * rd);

  dst[0] = (char)o1 + 'A';
  dst[1] = (char)a1 + 'A';
  dst[2] = (char)o2 + '0';
  dst[3] = (char)a2 + '0';
  dst[4] = (char)o3 + 'A';
  dst[5] = (char)a3 + 'A';
  dst[6] = '\0';
}


// ============Picture Display ===============
// picture loop
void dispUpdate() {
  oled.firstPage();
  do {
    dispMsg(55, 0, "GPS");

    if (strcmp(fix, "V") == 0) {
      dispMsgL(10, 25, "NO GPS");
    }
    else if (strcmp(fix, "A") == 0) {
      dispMsg(10, 12, ns);
      dispNum(25, 12, lat, 2);
      dispMsg(75, 12, ew);
      dispNum(90, 12, lon, 2);

      dispMsg(45, 25, mh);
      dispDate(15, 37, dow, dy, mth, yr);
      dispTime(35, 52, hrs, mns, sec);
    }
  } while ( oled.nextPage() );
}

Saturday 29 July 2017

The meArm built and working

So I have finalised the build of a meArm robot arm, and I'm quite impressed by it. Here it is about the pick up an object:

IMG 1396
I publish below three pieces of Arduino software I have written to get it going. I tried the so-called "meArm" IK stuff I found on GitHub, but was disappointed in it and found it unusable to get it going, maybe through complete lack of documentation. It claimed to create a cartesian or cylindrical space for giving commands to the meArm in order to make programming easier. But I found this to not help at all.

So I have remained with a concept of determining the minimum and maximum movements of the arm rotation, shoulder and elbow servos and equating them to 0-100% of a possible movement. Now it is possible to write 'movement" sketches describing the required movement in numbers from zero to one hundred.

To do this I first wrote a library of functions listed below,

README

README meArm.h

1 arm positions expressed as 0-100% of travel
  arm 0 = left, 50 = centre, 100 = right
  left 0 = up, 100 = down
  right 0 = in, 100 = out
  grip 0 = open, 100 = closed
  
2 servos attached on PWM pins

  arm(11), left(10), right(9), grip(6)

  and set to start positions
    arm = centre
    left = up
    right = in
    grip = open

2 functions

void armBegin(); // attach servoe & start positions
// move  to pos (0-100%) in ms/step
void armPos(pos, ms);   // arm
void leftPos(pos, ms);  // left
void rightPos(pos, ms); // roght
void gripPos(pos, ms);  // grip
void goPos(a, r, l, g, s); // s = ms/step


LIBRARY meArm.h
// meArm.h
// v 1.0 24-7-17

#include "Servo.h"

Servo arm, left, right, grip;


// arm deg max = max, home = mid point, left = min
#define ARM 11
#define ARMMAX 180
#define ARMHOME 98
#define ARMMIN 0

// right deg min = in, max = out
#define RIGHT 10
#define RIGHTMIN 50
#define RIGHTHOME 90
#define RIGHTMAX 145

// left deg min = in, max = out
#define LEFT 9
#define LEFTMIN 45
#define LEFTHOME 90
#define LEFTMAX 150

// grip close = in, open = out
#define GRIP 6
#define GRIPIN 70
#define GRIPOUT 0


// ============= BEGIN =============
void armBegin() {
  arm.attach(ARM);
  right.attach(RIGHT);
  left.attach(LEFT);
  grip.attach(GRIP);

  // start positions
  arm.write(ARMHOME);
  right.write(RIGHTHOME);
  left.write(LEFTHOME);
  grip.write(GRIPOUT);
}


// ============= ARM MOVEMENTS =============
// ARM steadily move arm to pos 0-100 in ms steps
void armPos(int pos, int ms) {
  int m, r;

  pos = map(pos, 0, 100, ARMMIN, ARMMAX);
  r = arm.read();
  if (pos > r) {
    for (m = r; m != pos; m += 1) {
      arm.write(m);
      delay(ms);
    }
    return;
  }
  else if (pos < r) {
    for (m = r; m != pos; m -= 1) {
      arm.write(m);
      delay(ms);
    }
  }
  return;
}

// LEFT steadily move left to pos in ms steps
void leftPos(int pos, int ms) {
  int m, r;

  pos = map(pos, 0, 100, LEFTMIN, LEFTMAX);
  r = left.read();
  if (pos > r) {
    for (m = r; m != pos; m += 1) {
      left.write(m);
      delay(ms);
    }
    return;
  }
  else if (pos < r) {
    for (m = r; m != pos; m -= 1) {
      left.write(m);
      delay(ms);
    }
  }
  return;
}


// RIGHT steadily move left to pos in ms steps
void rightPos(int pos, int ms) {
  int m, r;

  pos = map(pos, 0, 100, RIGHTMIN, RIGHTMAX);
  r = right.read();
  if (pos > r) {
    for (m = r; m != pos; m += 1) {
      right.write(m);
      delay(ms);
    }
    return;
  }
  else if (pos < r) {
    for (m = r; m != pos; m -= 1) {
      right.write(m);
      delay(ms);
    }
  }
  return;
}

// GRIP move open/close in ms steps
void gripPos(int pos, int ms) {
  int m, r;

  pos = map(pos, 0, 100, GRIPOUT, GRIPIN);
  r = grip.read();
  if (pos > r) {
    for (m = r; m != pos; m += 1) {
      grip.write(m);
      delay(ms);
    }
    return;
  }
  else if (pos < r) {
    for (m = r; m != pos; m -= 1) {
      grip.write(m);
      delay(ms);
    }
  }
  return;
}

// ========== GO to POS =======
// go to a position a, r, l, g, ms
void goPos(int a, int r, int l, int g, int s) {
  armPos(a, s);  // position arm, right andleft
  leftPos(l, s);
  rightPos(r, s);
  gripPos(g, s);
}


Note the hidden drawback of this library, the goPos() function always carries out movement in the order arm, left, right, grip servos, which maybe not what you want...

Next I wrote a couple of sketches, the first to allow you to manually set the arm position by three linear potentiometers connected to the Arduino Uno for ARM, SHOULDER and ELBOW servos.

IMG 1397

This allows you to plan, move and record the steps needed for a specific set of movements which are displayed on the Serial Monotor. Here is the sketch,

DEFINE
// Define
// v0.5 20-7-17
// see also library mearm.h
// servo pins
// arm   11
// right 10
// left  9
// grip  6
// movement %0 = in/Left, 100% = out/Right

#include "meArm.h"

void setup() {
  Serial.begin(9600);
  
  armBegin();     // attaches servos, sets start positions, HOME, IN, IN, OPEN
  delay(2000);
}

void loop() {
  int a, r, l, g;

  a = map(analogRead(A0), 0, 1023, 0, 100);
  r = map(analogRead(A1), 0, 1023, 0, 100);
  l = map(analogRead(A2), 0, 1023, 0, 100);

  Serial.print("Arm\t");
  Serial.print(a);
  Serial.print("\tRight\t");
  Serial.print(r);
  Serial.print("\tLeft\t");
  Serial.println(l);

  // random positions - test, grip open
  goPos(a, r, l, 0, 10);


  delay(50);

}


This Define sketch is used to move the arm to any place you want then, movement by movement, write down the Arm, Shoulder, and Elbow "%" values 0-100, and add the grip 0 (open) or 100% (close) command and the speed of each movement in small steps in milliseconds which controls the speed, finally to program a sequence of moves. Remember that the meARm.h library executes the goPos(a, r, l, g, ms) commands in the order ARM-LEFT-RIGHT-GRIP when you are planning and recording moves.

So Finally here is a demo sketch using the functions from the library to make a simple move of picking up an object, swinging across and putting it down, then reversing the move,

MOVE

// move
// v0.5 20-7-17
// see also library mearm.h
// servo pins
// arm   11
// right 10
// left  9
// grip  6
// movement %0 = in/L/open, 100% = out/R/close

#include "meArm.h"

#define SPEED 20

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

  armBegin();     // attaches servos, sets start positions, HOME, IN, IN, OPEN
  delay(2000);
}

void loop() {

  // routine positions a, r, l, g, ms

  goPos(28, 79, 30, 100, SPEED); // pick up
  goPos(28, 40, 30, 100, SPEED); //  rotate
  goPos(85, 79, 30, 0, SPEED);  // out & drop

  goPos(85, 40, 30, 0, SPEED);   // out
  goPos(50, 40, 30, 0, SPEED);   // home

  delay(2000);                // pause

  goPos(85, 79, 30, 0, SPEED);  // out
  goPos(85, 79, 30, 100, SPEED);  // pickup
  goPos(85, 40, 30, 100, SPEED); // up
  goPos(28, 40, 30, 100, SPEED); //  rotate
  goPos(28, 79, 30, 0, SPEED); // drop

  goPos(28, 40, 30, 0, SPEED);
  goPos(50, 40, 30, 0, SPEED);   // home


  delay(2000);



}

Thursday 20 July 2017

PLAYING with the kids

Away, sort of, from radio things I have indulged myself with a robot arm kit, the meArm. But there is a tiny nagging thought at the back of my mind - that is to control a set of these arms by radio, maybe using the ubiquitous 433MHz RX TX modules, and make them dance in harmony, fun eh?

IMG 1374

The meArm sitting on my desk

So far I have done a little bit of Arduino programming to try it out, and written a library file to keep some of the functions in.

CODE "meArm.h"

// mearm.h
// v 0.5 20-7-17

#include "Servo.h"

Servo arm, left, right, grip;


// arm deg max = max, home = mid point, left = min
#define ARMMAX 180
#define ARMHOME 98
#define ARMMIN 0

// right deg min = in, max = out
#define RIGHTMIN 90
#define RIGHTMAX 180

// left deg min = in, max = out
#define LEFTMIN 50
#define LEFTMAX 140

// grip close = in, open = out
#define GRIPIN 65
#define GRIPOUT 10


// ============= BEGIN =============
void armBegin() {
  arm.attach(11);
  right.attach(10);
  left.attach(9);
  grip.attach(6);

  // start positions
  arm.write(ARMHOME);
  right.write(RIGHTMIN);
  left.write(LEFTMIN);
  grip.write(GRIPOUT);
}


// ============= ARM MOVEMENTS =============
// ARM steady move arm to pos 0-100 in ms steps
void armpos(int pos, int ms) {
  int m, r;

  pos = map(pos, 0, 100, ARMMIN, ARMMAX);
  r = arm.read();
  if (pos > r) {
    for (m = r; m != pos; m += 1) {
      arm.write(m);
      delay(ms);
    }
    return;
  }
  else if (pos < r) {
    for (m = r; m != pos; m -= 1) {
      arm.write(m);
      delay(ms);
    }
  }
  return;
}

// LEFT steady move left to pos in ms steps
void leftpos(int pos, int ms) {
  int m, r;

  pos = map(pos, 0, 100, LEFTMIN, LEFTMAX);
  r = arm.read();
  if (pos > r) {
    for (m = r; m != pos; m += 1) {
      left.write(m);
      delay(ms);
    }
    return;
  }
  else if (pos < r) {
    for (m = r; m != pos; m -= 1) {
      left.write(m);
      delay(ms);
    }
  }
  return;
}


// RIGHT steady move left to pos in ms steps
void rightpos(int pos, int ms) {
  int m, r;

  pos = map(pos, 0, 100, RIGHTMIN, RIGHTMAX);
  r = arm.read();
  if (pos > r) {
    for (m = r; m != pos; m += 1) {
      right.write(m);
      delay(ms);
    }
    return;
  }
  else if (pos < r) {
    for (m = r; m != pos; m -= 1) {
      right.write(m);
      delay(ms);
    }
  }
  return;
}

// GRIP move open close in ms steps
void grippos(int pos, int ms) {
  int m, r;

  pos = map(pos, 0, 100, GRIPOUT, GRIPIN);
  r = arm.read();
  if (pos > r) {
    for (m = r; m != pos; m += 1) {
      grip.write(m);
      delay(ms);
    }
    return;
  }
  else if (pos < r) {
    for (m = r; m != pos; m -= 1) {
      grip.write(m);
      delay(ms);
    }
  }
  return;
}

CODE FOR TESTING SERVOS

// move
// v0.5 20-7-17
// viewed from behind, range
// arm mid xx/98/xx
// right 80/xx/140
// left xx/xx/xx

#include "meArm.h"

void setup() {
  armBegin();     // attach servos, goto start positions
  delay(2000);
}

void loop() {
  armpos(80, 50); // move to 80%, 50ms steps%
  delay(5000);
  armpos(20, 50); // move to 60% in 100 ms steps
  delay(5000);

}

Tuesday 4 July 2017

LATEST Sketches and Libraries

4 July 2017. Here's the latest update, downloadable zip file M0IFA SKetches

Screen Shot 2017 07 04 at 09 45 47

Sketches

Screen Shot 2017 07 04 at 09 47 37

Libraries used

Watch out for updates on this blog.

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.

Screen Shot 2017 07 03 at 10 21 52

VFO with time and date set from GPS

Power up the VFO by USB. Then plug in the GPS. push the button and there is a GPS FIX the RTC will be programmed. The display will show the RTC date time and the Maidenhead Locator. WSPR or JT65 sketches can now be loaded on the VFO and will transmit at the correct time (on UTC minute)

It may be possible to integrate this GPS calibration into the WSPR and JT65 sketches, but there may not be enough memory to do this - I have a vague plan to store the MH in EEPROM so it accessible by WSPR programs.

CODE

// DATE_TIME_MH_SET_GPS
// V2.5 read GPS time, fix & date, calc dow, set RTC, store MH
//      display RTC date & time, MH
// GPS VK-163 jack connections
// 1 tip  VCC
// 2      TX GPS  output
// 3      RX GPS  input
// 4 ring GND

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

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

// RTC address
#define RTCADDR 0x68

// GPS data buffer
char gpsbuf[200];

// data extracted from $GPRMC, ACSII
char tm[20];        // time HHMMSS
char fix[5];        // fix A|V, init void
char dt[20];        // date YYMMDD
char la[15];        // latitude
char ns[2];         // NS
char lo[15];        // longitude
char ew[2];         // EW

// Maidenhead Locator
char mh[10];

// RTC data, decimal
byte hrs, mns, sec;
byte yr, mth, dy;
byte dow;
double lat, lon;

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

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

  // I2C init
  Wire.begin();

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

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

  // init GPS void
  strcpy(fix, "V");
}

// ============== loop ===============
void loop() {
  dispUpdate();

  // if buuton pressed, get GPS data
  if (digitalRead(SW) == LOW) {
    strcpy(fix, "V");              // set V for display update
    dispUpdate();
    
    getGPS();                      // get GPS, extract data
    
    if (strcmp(fix, "A") == 0) {   // when GPS Aquired
      setRTC();                    // set RTC date time dow
      setMH();                     // calculate MH Locator
    }
  }
  
  getRTC();                        // read RTC
}

// ========= GPS funcitons =========
// get RMC line data
void getGPS() {
  do {
    getline(gpsbuf);
  } while (strncmp(gpsbuf, "$GPRMC", 6) != 0);

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

  xtract(gpsbuf, 3, la);          // latitude
  xtract(gpsbuf, 4, ns);          // NS
  xtract(gpsbuf, 5, lo);          // longitude
  xtract(gpsbuf, 6, ew);          // EW
}

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

  p = 0;                         // buffer pointer
  do {
    if (gps.available() > 0) {   // data?
      c = gps.read();            // read character
      out[p++] = c;              // put in buffer
    }
  } while ( c != '\n' );          // stop on /n
  out[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
}

// ================ RTC functions  SET =================
// set date and time bytes to RTC BCD
void setRTC() {
  // get GPS data in bytes, calc dow
  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(yr, mth, dy);

  // 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 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
}

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

// calc dow
byte calcDow(byte year, byte month, byte day)
{
  unsigned long days;
  unsigned int febs;
  unsigned int months[] =
  {
    0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 // days until 1st of month
  };

  days = year * 365;   // days up to year

  febs = year;
  if (month > 2) febs++; // number of completed Februaries

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

  days += months[month - 1] + day;

  // now we have day number such that 0000-01-01(Sat) is day 1

  return (byte)(((days + 5) % 7));
}

// ====================Maidenhead functions================
// calculate maideng=head locator from lat & lon
void setMH() {
  // extract lat * lon from GPS data
  xtract(gpsbuf, 3, la);
  xtract(gpsbuf, 4, ns);
  xtract(gpsbuf, 5, lo);
  xtract(gpsbuf, 6, ew);

  lat = convertPos(la, ns);
  lon = convertPos(lo, ew);

  findMH(mh, lat, lon);

  saveMH(); // save in EEPROM
}

// convert Lat, Lon strings to decimal +/-NS|EW
double convertPos(char *pos, char *d) {
  double pp, mm, ans;
  int dd;

  pp = atof(pos);                        // get in decimal ddmm.mmmmmmm
  dd = (int)pp / 100;                    // get degrees part
  mm = pp - (100 * dd);                  // get minutes
  ans = dd + (double)mm / 60.0;          // calc decimal degrees

  if (strcmp(d, "N") == 0 || strcmp(d, "E") == 0)  // if positive
    return ans;
  else
    return - ans; // negative
}

// find MH from lat & lon
void findMH(char *dst, double fa, double fo) {
  int a1, a2, a3;
  int o1, o2, o3;
  double rd;

  // Latitude
  rd = fa + 90.0;
  a1 = (int)(rd / 10.0);
  rd = rd - (double)a1 * 10.0;
  a2 = (int)(rd);
  rd = rd - (double)a2;
  a3 = (int)(24.0 * rd);

  // Longitude
  rd = fo + 180.0;
  o1 = (int)(rd / 20.0);
  rd = rd - (double)o1 * 20.0;
  o2 = (int)(rd / 2.0);
  rd = rd - 2.0 * (double)o2;
  o3 = (int)(12.0 * rd);

  dst[0] = (char)o1 + 'A';
  dst[1] = (char)a1 + 'A';
  dst[2] = (char)o2 + '0';
  dst[3] = (char)a2 + '0';
  dst[4] = (char)o3 + 'A';
  dst[5] = (char)a3 + 'A';
  dst[6] = '\0';
}

// save mh in EEPROM bytes 0-6
void saveMH() {
  int addr;
  byte i;

  addr = 0;
  i = 0;
  while(i < strlen(mh))                    // write MMnnMM to EEPROM
    EEPROM.write(addr++, mh[i++]);
  EEPROM.write(addr, '\0');                // terminate string
}

// ====================RTC functions READ =================
// 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 date
  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 = Sunday
  dy  = bcdToDec(Wire.read()); // 1 - 31
  mth = bcdToDec(Wire.read()); // 0 = jan
  yr  = bcdToDec(Wire.read()); // ..yy
}


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

// ============Picture Display ===============
// picture loop
void dispUpdate() {
  oled.firstPage();
  do {
    dispMsg(55, 0, "GPS");
    if (strcmp(fix, "V") == 0) {            // no fix V
      dispMsgL(30, 15, "GET GPS");
      dispMsg(25, 45, "Press Button");
    }
    else {
      dispMsgL(30, 15, mh);                 // fix A
      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 = 1) 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. I could then also use the same connection in other sketches to input or output a couple of signals to any external device when the GPS is not used...

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 way 3.5mm jack plug. After a considerable time fussing about with it I discovered the connections, which are,

Jack tip = VCC 3.6-5V

1st ring = GPS TX output (NMEA output data strings)

2nd ring = GPS RX Input (configuration input commands)

Jack shaft = Ground

And I wired it up to an Arduino UNO, using pin 12 as UNO RX to GPSTX, and pin 13 as UNO TX to GPS RX. />
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 may 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 haven't used pin 13 in my sketch.

The results, when I insert Serial.print() commands, 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
// 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
  }
}