If you've arrived here, you've arrived at an old post that has been superseded!
To go to the new FPGA SDR Transceiver posts, please click
(Thanks! Jeff, K6JCA)
—•— —•••• •——— —•—• •—
A digital down-converter (DDC) converts a digitized, band limited signal to a lower frequency signal at a lower sampling rate in order to simplify the subsequent radio stages.Below is an illustration of a DDC stage (don't worry about the math -- it will be explained later in this post):
//------------------------------------------------
//
Author: Jeff Anderson, K6JCA
//
// Rev:
B_160314a
//
//------------------------------------------------
#include <LiquidCrystal_I2C.h>
#include <SoftwareSerial.h>
#include <PWM.h>
#include <Wire.h>
#include <stdlib.h>
// Compiler Directives
//#define ECHO_NMEA
//#define
PRINT_GPSDO_STATES
//#define PRINT_ACCUM_TOTAL
#define
PRINT_CSV
//#define PRINT_DELTA
//#define HOLD_DAC
//
Constant Defines
#define MAX5217 0x1C // DAC I2C
address
#define INT0_PIN 2 // NANO pin,
INTERRUPT 0
#define GPS_RST 7 //
NANO pin, reset GNSS-2 receiver
#define TX_IN
10 // NANO pin, software UART RX
#define
ICP1 8 // NANO pin,
T1's ICP1 input
#define T1_CLK
5 // NANO pin, T1's clock input
//
Starting Values
#define MAX_DAC_STEP
2816 // 16-second DAC step
#define
GAIN
15.0
//#define
GAIN
0.0
#define START_DAC
0x8000
#define
OSC_PPB_DRIFT_PER_HOUR 0.02
// measured osc aging
//#define
OSC_PPB_DRIFT_PER_HOUR
0.0 // DON'T CORRECT FOR AGING
#define
PPB_CORRECTION_PER_DAC_STEP 0.0044 // from osc. measurements
#define
IDEAL_DELTA 19264 // for a 1 second period.
//
gpsdo states:
#define WAIT 0
#define START
1
#define SETTLE 2
#define GO 3
#define
SETTLE_2 4
// set the LCD address to 0x27
LiquidCrystal_I2C
lcd(0x27,16,2);
// Define software UART, Rx pin 10, Tx pin
11
SoftwareSerial mySerial(10, 11);
int
byteGPS;
char gps_time[9];
char gps_date[9];
char
sats_used[3];
int
nmea_decode_state; // state of NMEA decode process (to
extract date, time, and SV)
boolean date_flag;
boolean
time_flag;
boolean sv_flag;
boolean lcd_update_flag;
boolean
first_lcd_update;
boolean nmea_start_flag; // signals start
of nmea packet
boolean nmea_end_flag; //
signals end of nmea packet
boolean
one_pps_flag; // signals leading-edge of
one_pps_pulse
unsigned int prev_t1_count;
unsigned int
t1_count;
unsigned int t1_delta;
int
number_of_sats;
byte gpsdo_state;
unsigned int
dac;
long
dac_delta;
unsigned long accum_seconds;
unsigned long
long_dac;
int dac_direction; // +1 =
positive change, -1 = neg. change, 0 = no change.
char
tick_count;
boolean display_countdown;
unsigned int
prev_countdown;
unsigned long zero_count;
unsigned
long prev_count;
boolean one_flag;
boolean
minus_one_flag;
float float_zero_count;
int
last_direction;
long run_error;
long accum_error;
long
prev_accum_error;
float drift_corr_per_hour; // drift
correction, per hour
void setup()
{
Serial.begin(57600); // init serial port
lcd.init();
// initialize the lcd
InitTimersSafe(); //initialize all timers
except for 0, to save time keeping functions
pinMode(INT0_PIN, INPUT);
pinMode(TX_IN, INPUT);
pinMode(ICP1, INPUT);
pinMode(T1_CLK, INPUT);
pinMode(GPS_RST, OUTPUT);
// Reset for 15 msec
digitalWrite(GPS_RST,HIGH);
delay(15);
digitalWrite(GPS_RST,LOW);
date_flag = false;
time_flag = false;
sv_flag
= false;
lcd_update_flag = false;
nmea_start_flag = false;
nmea_end_flag = false;
one_pps_flag = false;
first_lcd_update = true;
nmea_decode_state = 0;
number_of_sats = 0;
tick_count = 0;
display_countdown = false;
prev_countdown = 0;
zero_count = 0;
accum_seconds = 0;
dac
= START_DAC;
dac_direction = 0;
gpsdo_state = WAIT;
dac_delta = 0;
long_dac = long(dac);
one_flag = false;
minus_one_flag = false;
last_direction = 0;
run_error = 0;
prev_count
= 0;
accum_error = 0;
drift_corr_per_hour =
OSC_PPB_DRIFT_PER_HOUR/PPB_CORRECTION_PER_DAC_STEP;
write_to_dac(dac);
// Print a message to the
LCD...
//
lcd.backlight();
lcd.print(" Hi, I'm your");
lcd.setCursor(0,1);
lcd.print(" K6JCA GPSDO!");
mySerial.begin(9600);
mySerial.println("Hello Jeff");
delay(2000);
lcd.clear();
lcd.print("
Waiting for ");
lcd.setCursor(0,1);
lcd.print(" NMEA stream... ");
//
interrupt on falling edge of one_pps signal.
attachInterrupt(digitalPinToInterrupt(INT0_PIN), one_pps,
FALLING);
// Timer 1: external clock.
Neg. Edge capture. No noise cancellation.
timer1_setup (0x00, -1, 0x00, 0x00, 0x00);
#ifdef PRINT_CSV
Serial.println("Total
Seconds,DAC,DacDirection,ZeroCount,RunError,PrevAccumError,DacDelta,New-AccumError,SV");
#endif
mySerial.println("Total
Seconds,DAC,DacDirection,ZeroCount,RunError,PrevAccumError,DacDelta,New-AccumError,SV");
}
void
loop()
{
// Read a byte of the serial
port,
// (returns -1 if no byte, I believe).
// Byte available. Get it and decode NMEA time,
//
date, and SV info.
byteGPS =
mySerial.read();
if (byteGPS != -1)
{
#ifdef ECHO_NMEA
Serial.write(byteGPS);
#endif
decode_nmea();
}
// only update LCD (and do
other calcs)
// if not receiving NMEA packet
AND
// if 1pps pulse has happened.
if
(nmea_end_flag && one_pps_flag) {
lcd_update_flag = true;
nmea_start_flag =
false;
nmea_end_flag = false;
one_pps_flag = false;
}
if (lcd_update_flag)
{
// convert ascii to number
number_of_sats = atoi(sats_used);
if
(number_of_sats >= 4) {
// 1
pps seems to be present if satellites >= 4
// so call the gpsdo state machine.
gpsdo_state_machine();
}
update_lcd();
lcd_update_flag = false;
#ifdef PRINT_GPSDO_STATES
Serial.println(t1_delta);
Serial.print("state: ");
Serial.print(gpsdo_state);
#endif
}
}
void decode_nmea()
{
// The NMEA packet from the Quectel L76 receiver starts
//
with $GNRMC and ends with $GNGLL, and
// they arrive in
the following order:
// $GNRMC
// $GPVTG
// $GPGGA
// $GNGSA (might be more than one GNGSA line)
// $GPGSV (might be more than one line)
// $GLGSC (might be more than one line)
// $GNGLL
//
// From this
packet of information, I want to:
// o
Decode 3 numbers from the gps receiver's
// NMEA stream:
// o Time
// o Date
// o SV (number of
satellites used for fix)
// o Flag when
the packet has started, and also
// flag when it ends.
//
// Extract Time and Date from the $GxRMC command
// (decode states start at state 20).
// This
command will also set the nmea_start_flag.
//
// Extract SV from the $GnGGA command
//
(decode states start at state 3)
//
//
When we have all the data we want, we can write
//
to the LCD, but don't do this until the entire
// NMEA packet has been received.
//
Otherwise, writing to the lcd
// might cause NEMA
info to be missed
// (and not echoed to serial
port).
//
// The last line sent in a
Quectel L76 NMEA
// packet seems to be the
$GNGLL line
// (the first is the $GxRMC
line.
// The line-feed is the last character
in that
// line, and therefore the last
character in
// the data packet.
// (Decode of packet end starts at state 50).
// Otherwise, return false if haven't yet
//reached the end of packet.
switch
(nmea_decode_state) {
case 0:
if (byteGPS == 'G') nmea_decode_state = 1;
else {
if (byteGPS ==
'R') nmea_decode_state = 20;
else nmea_decode_state = 0;
}
break;
case 1:
if (byteGPS == 'G') nmea_decode_state = 2;
else {
if (byteGPS ==
'L') nmea_decode_state = 50;
else nmea_decode_state = 0;
}
break;
case 2:
if (byteGPS == 'A')
nmea_decode_state = 3;
else
nmea_decode_state = 0;
break;
case 3: // 'GGA' found,
// now start counting commas to the SV field
if (byteGPS == ',') nmea_decode_state = 4;
else nmea_decode_state = 0;
break;
case 4: // found one comma
if (byteGPS == ',') nmea_decode_state = 5;
break;
case 5: // found 2
commas
if (byteGPS == ',')
nmea_decode_state = 6;
break;
case 6: // found 3 commas
if (byteGPS == ',') nmea_decode_state = 7;
break;
case 7: // found 4
commas
if (byteGPS == ',')
nmea_decode_state = 8;
break;
case 8: // found 5 commas
if (byteGPS == ',') nmea_decode_state = 9;
break;
case 9: // found 6
commas, look for 7th...
if
(byteGPS == ',') nmea_decode_state = 10;
break;
case 10:
// found 7 commas,
// next
characters should be SV field...
sats_used[0] = byteGPS;
nmea_decode_state = 11;
break;
case 11:
if (byteGPS != ',') {
sats_used[1] = byteGPS;
sats_used[2] = '\0';
}
else sats_used[1] = '\0';
sv_flag
= true;
nmea_decode_state = 0;
break;
case 20:
// found an 'R'.
// Now
let's check if the next characters
//are M and C...
if (byteGPS ==
'M') nmea_decode_state = 21;
else
nmea_decode_state = 0;
break;
case 21:
// found an
'M'.
//Now let's check if
the next character is C...
if
(byteGPS == 'C') {
nmea_decode_state = 22;
// found 'RMC' -- so set nmea_start_flag
nmea_start_flag = true;
}
else nmea_decode_state = 0;
break;
case 22:
// found a
'C'.
// Now let's check if
the next character
// is a
comma
if (byteGPS == ',')
nmea_decode_state = 23;
else
nmea_decode_state = 0;
break;
case 23: // found a comma.
//Next characters will be time...
gps_time[0] = byteGPS;
nmea_decode_state = 24;
break;
case 24: // time...
gps_time[1] = byteGPS;
gps_time[2]
= ':';
nmea_decode_state = 25;
break;
case 25: // time...
gps_time[3] = byteGPS;
nmea_decode_state = 26;
break;
case 26: // time...
gps_time[4] = byteGPS;
gps_time[5]
= ':';
nmea_decode_state = 27;
break;
case 27: // time...
gps_time[6] = byteGPS;
nmea_decode_state = 28;
break;
case 28: // time...
gps_time[7] = byteGPS;
gps_time[8]
= '\0';
nmea_decode_state = 29;
time_flag = true;
break;
case 29:
// Now let's start
counting commas to
// the
date field (there should be 8)
if
(byteGPS == ',') nmea_decode_state = 30;
break;
case 30:
// found a 1st comma.
//
check if the next character is a comma
if (byteGPS == ',') nmea_decode_state = 31;
break;
case 31:
// found a 2nd comma.
//
check if the next character is a comma
if (byteGPS == ',') nmea_decode_state = 32;
break;
case 32:
// found a 3rd comma.
//
check if the next character is a comma
if (byteGPS == ',') nmea_decode_state = 33;
break;
case 33:
// found a 4th comma.
//
check if the next character is a comma
if (byteGPS == ',') nmea_decode_state = 34;
break;
case 34:
// found a 5th comma.
//
check if the next character is a comma
if (byteGPS == ',') nmea_decode_state = 35;
break;
case 35:
// found a 6th comma.
//
check if the next character is a comma
if (byteGPS == ',') nmea_decode_state = 36;
break;
case 36:
// found a 7th comma.
//
check if the next character is a comma
if (byteGPS == ',') nmea_decode_state = 37;
break;
case 37:
// found 8 commas.
// Now
get the date. It's in ddmmyy format,
// but I'll display it as mm/dd/yy
gps_date[3] = byteGPS;
nmea_decode_state = 38;
break;
case 38:
// found 8
commas. Now get the date
gps_date[4] = byteGPS;
gps_date[5]
= '/';
nmea_decode_state = 39;
break;
case 39:
// found 8 commas. Now get the date
gps_date[0] = byteGPS;
nmea_decode_state = 40;
break;
case 40:
// found 8
commas. Now get the date
gps_date[1] = byteGPS;
gps_date[2]
= '/';
nmea_decode_state = 41;
break;
case 41:
// found 8 commas. Now get the date
gps_date[6] = byteGPS;
nmea_decode_state = 42;
break;
case 42:
// found 8
commas. Now get the date
gps_date[7] = byteGPS;
gps_date[8]
= '\0';
date_flag = true;
nmea_decode_state = 0;
break;
case 50:
// 'GL' found, now
see if there's
// another
'L'...
if (byteGPS == 'L') {
nmea_decode_state = 51;
//Serial.println("s");
}
else nmea_decode_state = 0;
break;
case 51:
// 'GLL'
found.
// Now look for end
of $GnGLL
// packet (Line
Feed).
if (byteGPS == 10)
{
nmea_end_flag =
true;
nmea_decode_state = 0;
}
break;
default:
nmea_decode_state = 0;
break;
}
}
void
update_lcd()
{
// first, get magnitude of run_error
and
// magnitude of accum_error
long
mag_run_error;
long mag_accum_error;
if
(run_error < 0) mag_run_error = -run_error;
else
mag_run_error = run_error;
if (accum_error < 0)
mag_accum_error = -accum_error;
else mag_accum_error =
accum_error;
// first time into this
call, clear the screen.
// Otherwise, just write w/o
clearing.
if (first_lcd_update == true) {
lcd.clear();
first_lcd_update = false;
}
tick_count++;
if (tick_count == 6)
{
tick_count = 0;
}
lcd.setCursor(0,0);
lcd.print(gps_time);
lcd.print(" ");
lcd.setCursor(10,0);
lcd.print("SV: ");
// compensate if number of sats <
10.
if(sats_used[1] == '\0') lcd.print(" ");
lcd.print(sats_used);
lcd.setCursor(0,1);
lcd.print(gps_date);
lcd.print(" ");
lcd.setCursor(9,1);
switch(tick_count) {
case 2:
// Print current value
Zero count
if (zero_count <
10000) lcd.print(" ");
if
(zero_count < 1000) lcd.print(" ");
if (zero_count < 100) lcd.print(" ");
if (zero_count < 10) lcd.print(" ");
lcd.print(zero_count);
break;
case 3:
// Print previous
zero-count
if (prev_count <
10000) lcd.print(" ");
if
(prev_count < 1000) lcd.print(" ");
if (prev_count < 100) lcd.print(" ");
if (prev_count < 10) lcd.print(" ");
lcd.print(prev_count);
break;
case 4:
// Print magnitude of
previous run-error.
lcd.print("rER");
if
(mag_run_error < 1000) lcd.print(" ");
if (mag_run_error < 100) lcd.print(" ");
if (mag_run_error < 10) lcd.print(" ");
lcd.print(mag_run_error);
break;
case 5:
// Print magnitude of
current accumulated_error.
lcd.print("aER");
if
(mag_accum_error < 1000) lcd.print(" ");
if (mag_accum_error < 100) lcd.print(" ");
if (mag_accum_error < 10) lcd.print(" ");
lcd.print(mag_accum_error);
break;
default:
// Print DAC value and
sign of last delta.
// print
leading zeroes (normally suppressed
lcd.print(" ");
if (dac < 4096)
lcd.print("0");
if (dac < 256)
lcd.print("0");
if (dac < 16)
lcd.print("0");
lcd.print(dac,HEX);
lcd.print("
");
dac_direction_to_lcd();
break;
}
}
void one_pps()
{
one_pps_flag = true;
}
void timer1_setup (byte mode,
int prescale, byte outmode_A, byte outmode_B, byte capture_mode)
{
// NOTE: This code found at:
//
http://sphinx.mythic-beasts.com/~markt/ATmega-timers.html
// enforce field widths for sanity
mode &= 15 ;
outmode_A &= 3 ;
outmode_B &= 3 ;
capture_mode &= 3 ;
byte clock_mode = 0 ; // 0
means no clocking - the counter is frozen.
switch
(prescale)
{
case 1: clock_mode =
1 ; break ;
case 8: clock_mode = 2 ; break
;
case 64: clock_mode = 3 ; break ;
case 256: clock_mode = 4 ; break ;
case 1024:
clock_mode = 5 ; break ;
default:
if (prescale < 0)
clock_mode = 7 ; // external clock
}
TCCR1A =
(outmode_A << 6) | (outmode_B << 4) | (mode & 3) ;
TCCR1B = (capture_mode << 6) | ((mode & 0xC) << 1) |
clock_mode ;
}
void gpsdo_state_machine()
{
switch (gpsdo_state) {
case WAIT:
gpsdo_state = START;
break;
case START:
#ifdef PRINT_CSV
Serial.print(accum_seconds);
Serial.print(",");
Serial.print(dac);
Serial.print(",");
#endif
mySerial.print(accum_seconds);
mySerial.print(",");
mySerial.print(dac);
mySerial.print(",");
write_to_dac(dac);
gpsdo_state =
SETTLE;
accum_seconds++;
break;
case SETTLE:
// DAC has been set. Let's let it settle.
gpsdo_state = SETTLE_2;
accum_seconds++;
break;
case SETTLE_2:
// DAC has been
set. Let's let it settle.
//
Meanwhile, get current count and
// set to previous count.
t1_count
= ICR1;
prev_t1_count =
t1_count;
gpsdo_state =
GO;
accum_seconds++;
break;
case GO:
accum_seconds++; // another second has passed
prev_t1_count = t1_count; // prev. counter snapshot
t1_count = ICR1;
// new counter snapshot
//
compensate for counter wraparound
if (t1_count < prev_t1_count) {
t1_delta = 65536 - (prev_t1_count - t1_count);
}
else {
t1_delta = t1_count - prev_t1_count;
}
#ifdef PRINT_DELTA
Serial.print(t1_count);
Serial.print(" - ");
Serial.print(prev_t1_count);
Serial.print(" = ");
Serial.println(t1_delta);
#endif
// a positive delta
following a negative delta
//
cancel. So run of zeroes is not yet over.
// continue counting zeroes!
if
(t1_delta > IDEAL_DELTA) {
if (minus_one_flag) {
// +1 and -1 cancel so
// continue counting
dac_direction = 0;
zero_count = zero_count + 1;
minus_one_flag = false;
}
else {
if (!one_flag) {
// Previous delta was 0, set set flag
// that this delta is a 1.
// Don't stop counting zeroes yet.
one_flag = true;
zero_count = zero_count + 1;
dac_direction = 0;
}
else
{
// Two positive deltas in a row.
// Run of zeroes is over!
// Reset flag and indicate direction to
// change dac.
// Positive deltas mean increase dac
// to lower the oscillator frequency.
dac_direction = +1;
one_flag = false;
}
}
}
else {
// same idea as above, but instead with a
// negative deltampreceeding a pos. delta.
if (t1_delta < IDEAL_DELTA) {
if (one_flag) {
// +1 and -1 cancel, so continue with zeroes
dac_direction = 0;
zero_count = zero_count + 1;
one_flag = false;
}
else
{
if (!minus_one_flag) {
minus_one_flag = true;
zero_count = zero_count + 1;
dac_direction = 0;
}
else {
// two minus deltas in a row.
// Run of zeroes is over! Reset flag
// and identify direction to move DAC.
// Negative deltas mean decrease dac
// to raise the oscillator frequency.
dac_direction = -1;
minus_one_flag = false;
}
}
}
else { // the
run of zeroes continues...
zero_count++;
}
}
if (dac_direction != 0) {
// Run of zeroes has ended. From run length
// determine dac correction factor (= run_error).
// Will also add to that value a "drift" offset
// (proportional to run length)
prev_count = zero_count; // for lcd display purposes
float_zero_count = float(zero_count);
prev_accum_error = accum_error; // for print purposes
accum_error = long(drift_corr_per_hour * float_zero_count / 3600);
run_error = long(MAX_DAC_STEP * (GAIN / (float_zero_count +
1))); // add one so don't divide by 0.
if (dac_direction == -1) {
run_error = -run_error;
last_direction = -1; // For lcd display purposes
}
else {
last_direction = +1;
}
dac_delta =
run_error + accum_error;
#ifdef PRINT_CSV
Serial.print(dac_direction);
Serial.print(",");
Serial.print(zero_count);
Serial.print(",");
Serial.print(run_error);
Serial.print(",");
Serial.print(prev_accum_error);
Serial.print(",");
Serial.print(dac_delta);
Serial.print(",");
Serial.print(accum_error);
Serial.print(",");
Serial.println(number_of_sats);
#endif
mySerial.print(dac_direction);
mySerial.print(",");
mySerial.print(zero_count);
mySerial.print(",");
mySerial.print(run_error);
mySerial.print(",");
mySerial.print(prev_accum_error);
mySerial.print(",");
mySerial.print(dac_delta);
mySerial.print(",");
mySerial.print(accum_error);
mySerial.print(",");
mySerial.println(number_of_sats);
// Calculate new dac value.
#ifdef HOLD_DAC
dac_delta = 0;
#endif
long_dac = long(dac) + dac_delta;
if (long_dac > 65535) long_dac = 65535;
if (long_dac < 0) long_dac = 0;
dac = uint16_t(long_dac);
// dac updated. Start counting 0's anew...
zero_count = 0;
dac_direction = 0;
gpsdo_state = START;
}
break;
default:
break;
}
}
void write_to_dac(unsigned int
dac_value)
{
// writing 16-bit word to Maxim
MAX5217
byte msbyte;
byte
lsbyte;
msbyte = byte((dac_value &
0xFF00)>>8);
lsbyte = byte(dac_value &
0x00FF);
Wire.beginTransmission(MAX5217);
Wire.write(0x01); // control word
Wire.write(msbyte); // MS Byte
Wire.write(lsbyte); // LS Byte
Wire.endTransmission();
}
void
dac_direction_to_lcd()
{
if (last_direction == +1)
lcd.print("+");
else {
if
(last_direction == -1) lcd.print("-");
else
lcd.print(".");
}
}