Thursday, 21 May 2015

WSPR with RTC timing

Transmitting WSPR has to be done exactly on time. It means a sequence of symbols has to start on an even minute, within the first second! How to do this?

There are four possible ways as far as I can see, get time from your computer if you are using a computer program and you have accurately set your computer's time; get time from a GPS receiver or a DFC77 receiver or a Real Time Clock chip which has been set correctly.

RTC solution

So far I have looked at only the RTC solution using an IC the DS1307, one of the simplest RTCs, but not one of the most accurate.

DS1307

This is a module from Adafruit:

Screen Shot 2015 05 21 at 11 00 12

It has the DS1307 IC, a couple of I2C bus pull-up resistors, the XTAL at 32768Hz and a back-up battery to maintain the circuit in operation when the +5V supply is off. The connections are

GND - ground

5V - +5V supply

SDA & SCL - the I2C bus

SQW - a pulse output which can be programmed to be 1Hz, 4.096kHz, 8.192kHz or 32.768kHz, HIGH or LOW output

Software

There seems to be a multitude of libraries for the RTC on the web, notably RTClib from Adafruit. But I believe most are not necessary to read the time from the chip as its operation is quite simple. For setting the time there is a clever program from Adafruit to set the initial time - see their web site. This only need be run once, or as frequently as your clock needs calibrating, it transfers your computer time to the DS1307).

DS1307 registers

The DS1307 has its main I2C address at 0x68h, this allows you to address 7 registers. The time is available in these registers in BCD (binary coded decimal) format (e.g. 15 = 0001 1001).

0x00	Seconds, b7 = 1 is halt oscillator, so mask this if writing time to the IC
0x01	Minutes
0x02	Hours, b6 = HIGH = 12, LOW = 24. In 12 mode, b6 reads AM = LOW/PM = HIGH
0x04	Date
0x05	Month
0x06 	Year


A further register can be addressed at 0x07h, this is a control register like this

b0	RS0 set freq output on SQWE
b1	RS1 “ - “
b2&3	-
b4	SQWE/OUT, square wave SQW enable, set SQWE = HIGH
b5&6	-
b7	OUT output level of pin OUT = 1, HIGH, out = 0 LOW

	RS0		RS1		SQWE
	0		0		1Hz
	0		1		4kHz (4096Hz)
	1		0		8kHz (8092Hz)
	1		1		32kHz (32768Hz)

This control register is set to 0x00h (0000 0000b) on power up, it should not need to be set for normal use.

Here is a small example of how to read the time, and convert it from BCD to decimal and display it on an LCD.

Code

#include "Wire.h"
#include "RTClib.h"
#include "LiquidCrystal_I2C.h"

// LCD
#define LCDADDR 0x27
#define LCDCOLS 16
#define LCDROWS 2

#define RTCADDR 0x68

// LCD object
LiquidCrystal_I2C lcd(LCDADDR, LCDCOLS, LCDROWS);

RTC_DS1307 rtc;

byte sec, mts, hr, wkday, mthday, mth, yr;

void setup()
{
  // init LCD & backlight on
  lcd.init();
  lcd.backlight();

  // init time from computer
  rtc.adjust(DateTime(__DATE__, __TIME__));
}

void loop() {
  // read registers and load bytes with time
  getTime();
  dispTime(4, 1);
}

void getTime() {
  // Reset the register pointer
  Wire.beginTransmission(RTCADDR);
  byte zero = 0x00;
  Wire.write(zero);
  Wire.endTransmission();
  
  // request 7 bytes from the RTC address
  Wire.requestFrom(RTCADDR, 7);

  // get the time data
  sec = bcdToDec(Wire.read());
  mts = bcdToDec(Wire.read());
  hr = bcdToDec(Wire.read() & 0b111111); //24 hour time
  wkday= bcdToDec(Wire.read()); //0-6 -> Sunday - Saturday
  mthday = bcdToDec(Wire.read());
  mth = bcdToDec(Wire.read());
  yr = bcdToDec(Wire.read());
}

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

// display time at col, row
void dispTime(byte c, byte r) {
  lcd.setCursor(c, r);
  if(hr < 10)
    lcd.print("0");
  lcd.print(hr);
  lcd.print(":");
  if(mts < 10)
    lcd.print("0");
  lcd.print(mts);
  lcd.print(":");
  if(sec < 10)
    lcd.print("0");
  lcd.print(sec);
}


The main steps are to reset the internal address pointer to point to the first register (reg0), then to sequentially read the registers to get the data, and convert it to decimal.

Following this idea I think that this code would work to trigger a WSPR transmission at an even minute.

Code

// Universal_WSPR_RTC transmits WSPR
// use WSPR_Symbol_Generator sketch to make symbol tone table
// here it is hard coded for msgtxt " M6KWH IO92 20", note leading space, 3rd char must be number
// insert symbols and msgtxt below
// step in 50Hz steps, giving 5 frequencies per band
// output on CLK0, Universal VFO shield "VFO" pin
// Interface by I2C bus to RTC DS1307
// DS1307 must be intitialised with time before use, button selects repeat time 2, 4, 6, 8 min

// CONNECTIONS
// RTC DS1307
// SCL = A5
// SDA = A4
// I2C address 0x68
// -----
// DDS I2C SI5351
// SCL = A5
// SDA = A4
// I2C address 0x60
// ------
// display I2C LCD 16 * 2
// o A5 SCL (y)
// o A4 SDA (or)
// o +5     (r)
// o GND    (bwn)
// I2C address 0x27
// -----
// encoder KY-040
// o D2 DT  (y)
// o D3 CLK (g)
// o D4 SW  (or)
// o +5     (r)
// o GND    (bwn)
// -----

// libraries
#include "Wire.h"
#include "si5351.h"
#include "LiquidCrystal_I2C.h"
#include "Rotary.h"
#include "TimerOne.h"

// RTC I2C address
#define RTCADDR 0x68

// LCD
#define LCDADDR 0x27
#define LCDCOLS 16
#define LCDROWS 2

// rotary Encoder pins 2 & 3 (DT & CLK), button 4 (SW)
#define DT 2
#define CLK 3
#define SW 4

// receive & transmit enable output pins
#define RX 13
#define TX 12

// dds object
Si5351 dds;

// LCD object
LiquidCrystal_I2C lcd(LCDADDR, LCDCOLS, LCDROWS);

// rotary sncoder object
Rotary rot = Rotary(DT, CLK);

uint32_t freq;
uint8_t tone_ptr = 0; // Pointer to the current symbol
volatile uint8_t next_tone = 0; // Incremented by the ISR
uint8_t next_tone2 = 0; // Local store, to compare against.
uint8_t step_tone = 0; // Flag used to signify we need to move to the next symbol
uint8_t b; // band 0 - 26

// band frequencies (cHz)
uint32_t band[15] =
{ 704000000, 704005000, 704010000, 704015000, 704020000,
  1014010000, 1014015000, 1014020000, 1014025000, 1014030000,
  1409700000, 1409705000, 1409710000, 1409715000, 1409720000
}; // 40, 30, 20m

// ====================== insert your message text & symbol
char msgtxt[] = {" M6KWH IO92 20  "};
// insert symbols - use WSSPR Symbol Generator program to create
uint8_t msg[162] =
{ 3, 1, 2, 0, 0, 0, 2, 2, 1, 2, 0, 2, 3, 1, 3, 0, 2, 2, 1, 2, 0, 3, 0, 1, 3, 1, 1, 0, 2, 0, 0, 2,
  0, 0, 1, 0, 0, 1, 0, 1, 2, 0, 0, 0, 0, 2, 1, 2, 1, 3, 0, 0, 1, 3, 0, 1, 2, 0, 0, 1, 3, 2, 1, 0,
  2, 2, 0, 3, 3, 2, 3, 2, 3, 0, 3, 0, 3, 2, 0, 3, 2, 0, 1, 2, 1, 3, 0, 0, 0, 3, 1, 0, 1, 0, 1, 0,
  2, 2, 3, 0, 0, 0, 0, 0, 1, 0, 2, 1, 0, 2, 1, 3, 1, 2, 1, 1, 0, 0, 3, 3, 2, 1, 2, 0, 0, 1, 1, 1,
  2, 2, 2, 0, 0, 3, 0, 3, 2, 0, 3, 1, 2, 2, 2, 0, 2, 0, 2, 3, 1, 0, 1, 0, 1, 1, 2, 0, 0, 3, 1, 0,
  2, 0
};
// ======================

// tone out at tuned freq + tone (cHz above)
uint32_t tones[4] = {0, 146, 293, 440};

// RTC time sec & mins, sec = 1 means no WSPR TX, yr = 0 means RTC not running
byte sec, mns, hr;

// repeat frequency
uint8_t repeat;

void setup()
{
  // initialise the wire library for I2C comms
  Wire.begin();

  // init LCD & backlight on
  lcd.init();
  lcd.backlight();

  // init dds
  dds.init(SI5351_CRYSTAL_LOAD_8PF, 0); // default 25MHz XTAL

  // all CLK disabled
  dds.output_enable(SI5351_CLK0, 0);
  dds.output_enable(SI5351_CLK1, 0);
  dds.output_enable(SI5351_CLK2, 0);

  // encoder, button, RX & TX pins
  pinMode(DT, INPUT_PULLUP);
  pinMode(CLK, INPUT_PULLUP);
  pinMode(SW, INPUT_PULLUP);
  pinMode(RX, OUTPUT);
  pinMode(TX, OUTPUT);

  digitalWrite(RX, LOW); // RX
  digitalWrite(TX, HIGH); // no TX

  b = 7; // band, middle 30m
  repeat = 2; // init to send every 2 mins

  dispMsg(0, 0, "WSPR            ");
  dispFreq(5, 0, band[b], 2); // display freq b
  dispMsg(1, 1, " min");
  dispNum(0, 1, repeat);
}

void loop()
{
  tune(); // tune freq?
  event(); // set repeat timing 2,4,6,8 min?

  getRTC(); // read RTC, display time

  // send WSPR now?
  if (mns % repeat == 0 && sec == 0)
  {
    // display message
    dispMsg(0, 0, msgtxt);
    dispMsg(0, 1, " Symbol       TX"); // show symbol number & transmit

    txWspr(b); // transmit on freq b

    // restore display
    dispMsg(0, 0, "WSPR           ");
    dispFreq(5, 0, band[b], 2); // display freq
    dispMsg(1, 1, " min");
    dispNum(0, 1, repeat);
  }
}

void tune()
{
  unsigned char dir; // tuning direction CW/CCW

  dir = rot.process(); // read encoder
  if (dir != DIR_NONE) // turned?
  {
    if (dir == DIR_CW && b > 0) b += 1; // increment band freq +/- 1
    if (dir == DIR_CCW && b < 15) b -= 1;

    dispFreq(5, 0, band[b], 2); // display freq
  }
}

// set event time 4,6,8,10 min
void event()
{
  if (digitalRead(SW) == LOW)
  {
    while (!digitalRead(SW)); // wait for release
    if (repeat == 8) repeat = 2;
    else repeat += 2;
    dispMsg(1, 1, " min");
    dispNum(0, 1, repeat);
  }
}

// transmit msg at freq fTable[b]
void txWspr(uint8_t b)
{
  // Start Transmitter
  digitalWrite(RX, HIGH);
  digitalWrite(TX, LOW);

  // Start the timer interrupt, is called every 8192/12000 seconds.
  Timer1.initialize(682666UL);
  Timer1.attachInterrupt(wspr_isr);

  // Transmit!
  while (1)
  {
    // do the increment checking without the interrupts, in case it gets
    // modified while we are checking.
    noInterrupts();
    if (next_tone > next_tone2)
    {
      step_tone = 1;  // next_tone has incremented, raise flag
      next_tone2 = next_tone;
    }
    interrupts();

    if (step_tone)
    {
      // Got a call from the ISR to increment the tone, reset flag
      step_tone = 0;

      // We enable the clock here to avoid having it be active before the first tone
      // is due to be transmitted.
      dds.output_enable(SI5351_CLK0, 1);

      // output freq
      freq = band[b] + tones[msg[tone_ptr]];
      freqOut(freq);

      dispNum(8, 1, tone_ptr); // display tone number
      tone_ptr++; // next tone

      // If we're at the end of the symbol array, disable the timer and break.
      if (tone_ptr == 162)
      {
        Timer1.detachInterrupt();
        next_tone = 0;
        next_tone2 = 0;
        tone_ptr = 0;
        break;
      }
    }
  }

  // Disable clock, stop Tx
  dds.output_enable(SI5351_CLK0, 0);
  digitalWrite(TX, HIGH);
  digitalWrite(RX, LOW);
}

// Timer ISR.
void wspr_isr(void)
{
  next_tone += 1;
}

// frequency (in cHz) for VFO, on CLK0
void freqOut(uint32_t f)
{
  dds.set_freq(f, 0ULL, SI5351_CLK0); // f cHz
}

// 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 7 bytes from the RTC address
  Wire.requestFrom(RTCADDR, 3);

  // get the s/m/h time data
  sec = bcdToDec(Wire.read());
  mns = bcdToDec(Wire.read());
  hr = bcdToDec(Wire.read() & 0b111111); //24 hour time
  dispTime(8, 1);
}

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

// display char msg at col c, row r
void dispMsg(uint8_t c, uint8_t r, char *m)
{
  lcd.setCursor(c, r);
  lcd.print(m);
}

// display a number at col c, row r
void dispNum(uint8_t c, uint8_t r, uint16_t n)
{
  lcd.setCursor(c, r);
  lcd.print(n);
}

// display freq in kHz,col c, row r, d decimal places
void dispFreq(uint8_t c, uint8_t r, uint32_t f, uint8_t d)
{
  lcd.setCursor(c, r); // clear last freq display
  lcd.print((float)f / 100000, d); // convert to float for print function

  lcd.print("kHz  "); // + trailing spaces to clear previous display
}

// display time at col, row
void dispTime(byte c, byte r) {
  lcd.setCursor(c, r);
  if(hr < 10)
    lcd.print("0");
  lcd.print(hr);
  lcd.print(":");
  if(mns < 10)
    lcd.print("0");
  lcd.print(mns);
  lcd.print(":");
  if(sec < 10)
    lcd.print("0");
  lcd.print(sec);
}


I have not verified this actually transmits yet, when I build the Concept stack (RTC + VFO) I will let you know

No comments: