Generates RTTY from a GPS receiver

Dependencies:   mbed-dsp mbed-rtos mbed

main.cpp

Committer:
adwiens
Date:
2014-01-11
Revision:
5:dcf952c7e6b3
Parent:
4:ed77fd381cc5

File content as of revision 5:dcf952c7e6b3:

// Author:  Andrew Wiens (adwiens.com)
// Date:    11 Jan 2014
//
// ==========================================================================
// 
// GPS2RTTY
//
// Using the mbed RTOS and DSP libraries, GPS2RTTY generates RTTY messages
// containing raw NMEA sentences from a GPS receiver.
//
// No extra hardware needed! Audio is generated with the mbed's built-in DAC.
//
// GPS ---> mbed ---> Radio
//
// ==========================================================================

#include "mbed.h"
#include "rtos.h" // mbed real time os library
#include "dsp.h" // mbed digital signal processing library
#include <cstring>
#include <string>
#include <iostream>
#include <sstream>
#include <map>

#define GPS_CB_SIZE (16) /* size of gps circular buffer in characters */
#define RTTY_CB_SIZE (2048) /* characters in rtty buffer */
#define GPS_BAUD (9600) /* gps serial port speed in bps */
#define RADIO_TX_WAIT (1000) /* time between radio transmissions in ms */
#define PRINT_NMEA_WAIT (2000) /* time between console debug messages in ms */
#define CALLSIGN_WAIT (600) /* time between sending amateur radio callsign in s */
#define AUDIO_FS (8000) /* audio sample rate in hz */
#define RTTY_BAUD (110) /* rtty bit rate in bps */
#define MARK_FREQ (2850) /* mark frequency (1) in hz */
#define SPACE_FREQ (2000) /* space frequency (0) in hz */
#define AUDIO_VOL (1) /* range 0-1 */
#define RTTY_NUM_ZEROS_BEFORE (25) /* number of empty characters to append before each rtty message. helps receivers sync. */
#define RTTY_NUM_ZEROS_AFTER (8) /* number of empty characters to append after each rtty message. */
#define CALLSIGN "KC0WYS" /* my amateur radio callsign */
#define CALLSIGN_TAG "HAMID" /* 5-character tag used in the fake nmea sentence */

using namespace std;

// mbed ports:
Serial pc(USBTX, USBRX); // pc serial port (via usb)
Serial gps(p9, p10); // gps serial port (uart3)
AnalogOut dac(p18); // mbed built-in digital to analog converter
DigitalOut ptt(p17); // radio push to talk button

// Status leds:
DigitalOut gps_led(LED1); // gps status led
DigitalOut rtty_led(LED2); // tx status led

// GPS variables:
char cb[GPS_CB_SIZE]; // c-string circular buffer for gps rx isr
int cb_isr_i = 0; // gps isr index
int cb_thr_i = 0; // gps thread index
stringstream rxbuf; // gps receive buffer
map<string,string> nmea_data; // most recent nmea sentences
Mutex nmea_data_mutex; // nmea data lock

// RTTY variables:
char r_cb[RTTY_CB_SIZE]; // c-string circular buffer for rtty tx isr
int r_cb_isr_i = 0; // rtty isr index
int r_cb_thr_i = 0; // rtty thread index
float angle = 0.0; // current sine angle
int ifreq = MARK_FREQ; // instantaneous frequency
int bitn = -1; // current bit number
bool txen = false; // tx enable flag

// Interrupt service routine for the GPS.
// It is called when the serial port receives a character,
// and it puts the character into a circular buffer for the gps thread.
void gps_rx_isr()
{
    cb[cb_isr_i] = LPC_UART3->RBR; // avoid mutex lockup (https://mbed.org/forum/bugs-suggestions/topic/4217/)
    if(++cb_isr_i >= GPS_CB_SIZE) cb_isr_i = 0; // loop circular buffer index
}

// Reads new characters from the GPS circular
// buffer when the circular buffer is about 25 percent full
// and adds the new characters to a string containing the current
// nmea sentence. Each time a complete NMEA sentence is received
// this function updates a map containing K,V pairs. (The sentence
// type is the key and the value is the NMEA sentence.)
void gps_rx_thread(void const *argument)
{
    while(1) {
        gps_led = 0;
        Thread::wait((GPS_CB_SIZE)*1000*8/4/GPS_BAUD); // wait until cb 25% full
        gps_led = 1;
        while(cb_thr_i != cb_isr_i) {
            char c = cb[cb_thr_i];
            rxbuf << c; // add to string buffer
            if(c == '\n') { // clear string buffer
                const char *c = rxbuf.str().c_str();
                const char *l = strchr(c, '$'), *r = strchr(c, ',');
                if(l != NULL && r != NULL && r > l) { // check valid limits
                    char ctype[6];
                    memcpy(ctype,l+1,5);
                    ctype[5] = 0;
                    string type = ctype;
                    nmea_data_mutex.lock();
                    nmea_data[type] = rxbuf.str(); // update map
                    nmea_data_mutex.unlock();
                }
                rxbuf.str("");
                rxbuf.clear();
            }
            if(++cb_thr_i >= GPS_CB_SIZE) cb_thr_i = 0; // incr/loop circular buffer index
        }
    }
}

// Writes individual audio samples to the dac. It uses the
// instantaneous frequency from the bit ticker.
void rtty_sample_tick()
{
    if(txen) {
        angle += 2 * PI * ifreq / AUDIO_FS;
        if(angle > 2 * PI) angle -= 2*PI;
        dac = (arm_sin_f32(angle) + 1.0) / 2.0 * AUDIO_VOL; // write sample to dac
        ptt = 0;
        rtty_led = !rtty_led;
    } else {
        dac = 0;
        ptt = 1;
        rtty_led = 0;
    }
}

// Handles whether the current rtty bit is a mark or a space.
// It reads one character at a time from the rtty circular buffer and sets
// the instantaneous frequency of the sample ticker for each bit. (1==mark,
// 0==space.)
void rtty_bit_tick()
{
    if(bitn < 0) {
        txen = (r_cb_isr_i != r_cb_thr_i);
        if(txen) {
            ifreq = SPACE_FREQ; // start bit
            if(++r_cb_isr_i >= RTTY_CB_SIZE) r_cb_isr_i = 0; // incr/loop circular buffer index
            ++bitn;
        }
    } else if(bitn < 8 && txen) {
        ifreq = ((r_cb[r_cb_isr_i] & (1<<(bitn++))) == 0) ? SPACE_FREQ : MARK_FREQ; // data bit
    } else if(txen) {
        ifreq = MARK_FREQ; // stop bit
        bitn = -1;
    }
}

// Adds NMEA sentences periodically to a buffer for the other RTTY functions to process.
void rtty_tx_thread(void const *argument)
{
    while(1) {
        Thread::wait(RADIO_TX_WAIT); // wait for a certain amount of time between transmissions
        stringstream txbuf;
        nmea_data_mutex.lock();
        for (map<string,string>::iterator iter = nmea_data.begin(); iter != nmea_data.end(); ++iter) {
            txbuf << (iter->second); // fill the packet with the most recent nmea sentences
        }
        nmea_data.clear(); // empty the map
        nmea_data_mutex.unlock();
        for(int i = 0; i < RTTY_NUM_ZEROS_BEFORE; ++i) { // add empty characters
            if(++r_cb_thr_i >= RTTY_CB_SIZE) r_cb_thr_i = 0; // incr/loop circular buffer index
            r_cb[r_cb_thr_i] = 0; // append a zero character
        }
        for(const char* it = txbuf.str().c_str(); *it; ++it) { // add all characters to buffer
            if(++r_cb_thr_i >= RTTY_CB_SIZE) r_cb_thr_i = 0; // incr/loop circular buffer index
            r_cb[r_cb_thr_i] = *it;
        }
        for(int i = 0; i < RTTY_NUM_ZEROS_AFTER; ++i) { // add empty characters
            if(++r_cb_thr_i >= RTTY_CB_SIZE) r_cb_thr_i = 0; // incr/loop circular buffer index
            r_cb[r_cb_thr_i] = 0; // append a zero character
        }
        while(!txen) Thread::wait(100); // wait for transmission to start
        while( txen) Thread::wait(100); // wait for transmission to end
    }
}

// Periodically inserts HAM radio callsign into NMEA sentence map.
// Required by FCC to send callsign at least every 30 minutes.
void insert_callsign_thread(void const *argument)
{
    while(1) {
        nmea_data_mutex.lock();
        nmea_data[CALLSIGN_TAG] = "$" CALLSIGN_TAG "," CALLSIGN "," CALLSIGN "," CALLSIGN ",*00\r\n"; // dummy nmea sentence
        nmea_data_mutex.unlock();
        for(int i = 0; i < CALLSIGN_WAIT; ++i) Thread::wait(1000); // wait for CALLSIGN_WAIT seconds
    }
}

// Writes NMEA sentences to the console. Useful for debug.
void print_nmea_thread(void const *argument)
{
    while(1) {
        nmea_data_mutex.lock();
        for (map<string,string>::iterator iter = nmea_data.begin(); iter != nmea_data.end(); ++iter) {
            cout << (iter->second);
        }
        nmea_data_mutex.unlock();
        cout << endl;
        Thread::wait(PRINT_NMEA_WAIT);
    }
}

int main()
{
    Ticker sample_tick, bit_tick;
    Thread gps_thread(gps_rx_thread); // gps receive thread
    Thread rtty_thread(rtty_tx_thread); // rtty transmit thread
    Thread print_thread(print_nmea_thread); // debug print thread
    Thread callsign_thread(insert_callsign_thread); // amateur callsign thread
    gps.baud(GPS_BAUD); // set gps bit rate
    gps.attach(&gps_rx_isr); // set up gps receive interrupt service routine
    sample_tick.attach_us(&rtty_sample_tick,1000000/AUDIO_FS); // begin generating audio
    bit_tick.attach_us(&rtty_bit_tick,1000000/RTTY_BAUD); // begin sending characters
    while(1); // idle forever
}