GFSK Demodulator on Raspberry Pi

V. Hunter Adams

Background

This webpage describes the radio receiver station that I built to conduct the experiments described on the vineyard and cattle monitoring Monarch page. One of these receiver stations is pictured below, with all of the components labeled. This particular station was deployed at the vineyard to gather the Monarch data over 5 weeks, as described on the webpage linked previously. The station is powered by two 17W solar panels from Voltaic system, which interface with their 24,000mAh V88 Laptop Battery. This battery powers a Raspberry Pi, which is demodulating packets being received from an RTL-SDR software-defined radio. This SDR is attached to a Yagi antenna through a low noise amplifier.

With this ground station, I was able to consistently gather packets (at 50bps) from Monarchs up to a quarter-mile away (with no forward error correction, matched filtering, or anything). Those distances were sufficient for the tests that I was conducting.

missing
Raspberry-Pi-based GFSK demodulator for use with CC1310

Background Math

The background math for this project and for the Verilog-based GFSK demodulator project are the same, so I've copied the relevant sections from that webpage below.

Finite Impulse Response Filter

A finite impulse response filter is essentially just a weighted average of the $N$ most recent samples, where the weights associated with each of those samples are chosen in order to get a desired frequency response in the filter. For example, the output $y[n]$ of an FIR filter of order $N$ is shown below, where $b_i$ is the weight associated with sample $x_i$.

\begin{align} y[n] &= \sum_{i=0}^N b_0x[n] + b_1x[n-1] + \cdots + b_N x[n-N] \end{align}

This filters have the advantage of requiring no feedback, being inherently stable, and being capable of being designed such that they are linear phase. This is accomplished by simply making the coefficient sequence symmetric, and is a desirable property for phase-sensitive applications like GFSK demodulation.

Many methods exist for coming up with a set of coefficients that meet requirements on passband ripple, stopband ripple, and transition width. Some of these include the window design method, frequency sampling method, and Parks-McClellan method. I used a Matlab tool that implements the Parks-McClellan method in order to come up with a set of coefficents for a 10-tap FIR filter that low-passes the raw RF samples down to 50 kHz. Such tools did not exist for any of the other aspects of this project.

GFSK Demodulation

The low-power transmitters (TI-CC1310's) use Gaussian Frequency Shift Keying (GFSK) to encode information at the carrier frequency. With GFSK, a logical 1 is encoded by increasing the frequency of the transmission to slightly greater than the carrier frequency and a logical zero is encoded by decreasing the frequency of the transmission to slightly less than the carrier frequency. This is in contrast to Amplitude Modulation which obviously modulates the amplitude in order to encode 1's and 0's, and Phase Modulation, which modulates the phase of the transmission (while keeping the frequency constant) in order to encode 1's and 0's. A good introductory article on these modulation schemes can be found here: https://www.allaboutcircuits.com/textbook/radio-frequency-analysis-design/radio-frequency-demodulation/quadrature-frequency-and-phase-demodulation/.

A discussion of the demodulation method requires a brief discussion of how the RTL-SDR sampling works. The RTL-SDR has two voltage-controlled oscillators that oscillate at precisely the carrier frequency of the transmitter (915 MHz). One of these oscillators is 90 degrees out of phase from the other. The RF transmissions received by the antenna are mixed with these local oscillators in order to get the baseband transmission. By mixing the received transmissions with both the in-phase oscillator and the out-of-phase oscillator, the RTL-SDR is able to represent the received transmission as the sum of two out-of-phase 915 MHz waves. One of these waves is in-phase (I) and the other is out-of-phase (or "quadrature", Q). This I/Q data is a nice way to represent the received transmissions because it is independent of the carrier frequency, and it includes phase information (which would be impossible to recover with just one local oscillator).

With the I/Q data, one has all of the information necessary to demodulate any of the modulation schemes mentioned above. For Amplitude Modulation, the relevant quantity would be the amplitude of the received transmission ($\sqrt{I^2 + Q^2}$). For phase modulation, the relevant quantity is the phase of the received signal relative to the local oscillators $\left(\text{atan2}\left(\frac{I}{Q}\right)\right)$. For Frequency Modulation, the information is encoded on the derivative of the phase. A procedural way to approximate this quantity is to find the conjugate product of the $n^{th}$ and $(n-1)^{st}$ samples (a complex number), and then to find the argument of the resulting complex number. If these two samples have the same phase, then the product will be a real number with argument 0. If these two samples are 90 degrees out of phase, then the product will be a purely imaginary number with argument $\frac{\pi}{2}$. The I/Q plot for a frequency-modulated signal ends up forming a circle, since the phase of the received transmission moves continually around the complex plane. For phase-modulated signals, the I/Q plots look like a collection of dots. Letting $\tilde{x}[n-1]$ be the complex conjugate of sample $x[n-1]$, this is represented by the below equation.

\begin{align} y[n] = \text{arg}\left(x[n]\overline{x}[n-1]\right) \end{align}

When no transmission is being received, the output of this demodulation method is white noise, since two consecutive samples may be any amount of phase separated from one another. During a transmission, however, this demodulation method is capable of recovering the logical waveform (the 1's and 0's) of the transmission.

Code

The code below gathers samples from the RTL-SDR through a TCP/IP socket, decimates, demodulates, filters, binary slices, searches for a sync word, gathers a packet (when it sees the sync word), performs a checksum, and then stores the packet in a local file.

There is some code commented out that will alternatively send the packet to some server address rather than just storing it locally.

To use this:

  1. Install the RTL-SDR command line tools on the RPi
  2. Plug the RTL-SDR into the Raspberry Pi
  3. Start sending radio samples to the code below through a TCP/IP socket by running:
    rtl_tcp -a 127.0.0.1 -p 1234 -s 900001 -f 915000000
    This starts rtl_tcp, sends to the local address at which the C code below is looking, port no. 1234, sample rate of 900001 samples/sec (at 50kpbs, that's 18 samples/bit), at a frequency of 915 MHz
  4. Run the C-code below with ./demod
///////////////////////////////////////
/// Hunter Adams (vha3@cornell.edu)
/// test VGA with hardware video input copy to VGA
// compile with
// gcc demodulate_fpga.c -o demod -lm -lpthread
//////////////////////////////////////
#define _GNU_SOURCE 

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ipc.h> 
#include <sys/shm.h> 
#include <sys/mman.h>
#include <sys/time.h> 
#include <math.h> 
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h> 
#include <sched.h>
#include <time.h>

#include <errno.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>

// threads
#include <pthread.h>
#include <semaphore.h>  

// Semaphores for flow control
sem_t tcpip_semaphore;
sem_t slice_semaphore;
sem_t search_semaphore;
sem_t check_semaphore;
sem_t start_semaphore;
sem_t exfiltrate_semaphore;

// TCPIP buffer from RTL-SDR
unsigned char tcpip_buffer[14400];

// 50 bytes at a time
double filtered_zero[400];
double filtered_one[400];
double filtered_two[400];

// Store 128 bytes
char received_test_0[128];
char received_test_1[128];
char received_test_2[128];

// Sync word
char address[4] = {0x93, 0x0b, 0x51, 0xde};

// Count received packets
int counter = 0;

// 128-50
char packet[78];
int packetlen;
int numclocks;
uint16_t checksum;

#define PORT "3490"  // the port users will be connecting to
#define BACKLOG 10   // how many pending connections queue will hold

void clock_array(char array[128]) {
    int i;
    for(i=0; i<127; i++) {
        array[i] = ((array[i] << 1) & 0xff) | ((array[i+1] & 0x80) >> 7);
    }
    array[127] = ((array[127] << 1) & 0xff);
}

#define CRC16_POLY 0x8005
#define CRC_INIT 0xFFFF
uint16_t culCalcCRC(char crcData, uint16_t crcReg) {
    int i;
    for (i = 0; i < 8; i++) {
        if (((crcReg & 0x8000) >> 8) ^ (crcData & 0x80)) {
            crcReg = (crcReg << 1) ^ CRC16_POLY;
        } else {
          crcReg = (crcReg << 1);
        }
        crcData <<= 1;
    }
    return crcReg;
}// culCalcCRC

void sigchld_handler(int s)
{
    (void)s; // quiet unused variable warning

    // waitpid() might overwrite errno, so we save and restore it:
    int saved_errno = errno;

    while(waitpid(-1, NULL, WNOHANG) > 0);

    errno = saved_errno;
}

// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa)
{
    if (sa->sa_family == AF_INET) {
        return &(((struct sockaddr_in*)sa)->sin_addr);
    }

    return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

char slice_n_dice(int count) {
    if (count == 0) return 0x7f;
    if (count == 1) return 0xbf;
    if (count == 2) return 0xdf;
    if (count == 3) return 0xef;
    if (count == 4) return 0xf7;
    if (count == 5) return 0xfb;
    if (count == 6) return 0xfd;
    if (count == 7) return 0xfe;
}

void * read_tcpip() {

    sem_wait(&start_semaphore);

    // =======================================================================
    // ========== Opens TCP/IP socket to RTL-SDR for I/Q sampling ============
    // =======================================================================
    int sockfd, portno, n;
    struct sockaddr_in serv_addr;
    struct hostent *server;
    char frequency;
    unsigned int length;
    portno = atoi("1234");
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) 
        fprintf(stderr,"ERROR opening socket\n");
    // server = gethostbyname("192.168.1.145");
    server = gethostbyname("127.0.0.1");
    if (server == NULL) {
        fprintf(stderr,"ERROR, no such host\n");
        exit(0);
    }
    bzero((char *) &serv_addr, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    bcopy((char *)server->h_addr, 
         (char *)&serv_addr.sin_addr.s_addr,
         server->h_length);
    serv_addr.sin_port = htons(portno);
    if (connect(sockfd,(struct sockaddr *) &serv_addr,sizeof(serv_addr)) < 0) 
        fprintf(stderr,"ERROR connecting\n");
    //printf("Please enter the message: ");
    bzero(tcpip_buffer,14400);
    length=sizeof(struct sockaddr_in);
    // =======================================================================
    // =======================================================================
    // =======================================================================


    // variables to hold raw I/Q samples
    double first_i, first_q, second_i, second_q;
    // variables for re/im part of complex conjugate
    double real, imag;
    // variable for argument of complex number
    double output[3];
    // finite impulse response filter taps
    double tap1 = 0.0584283;
    double tap2 = 0.88314341;
    double tap3 = 0.0584283;


    while(1) 
    {

        sem_wait(&tcpip_semaphore);
        bzero(tcpip_buffer,14400);

        n = recvfrom(sockfd, tcpip_buffer,
            14400, MSG_WAITALL,(struct sockaddr *)&serv_addr, &length);

        if (n > 0) {
            // variables for sample rates in TCPIP buffer
            int i=0, filterdex=0, sorter=0;
            for (i=0; i<n; i+=12) {
                // Decimate
                first_q =    tcpip_buffer[i] - 128;
                first_i =    tcpip_buffer[i+1] - 128;
                second_q = -(tcpip_buffer[i+6] - 128);
                second_i =   tcpip_buffer[i+6+1] - 128;
                // Demodulate
                real = first_i*second_i - first_q*second_q;
                imag = first_i*second_q + first_q*second_i;
                output[2] = atan2(imag, real);
                // Filter
                switch (sorter) {
                    case 0:
                        filtered_zero[filterdex] = (tap1*output[0]) + (tap2*output[1]) + (tap3*output[2]);
                        sorter = 1;
                        break;
                    case 1:
                        filtered_one[filterdex] = (tap1*output[0]) + (tap2*output[1]) + (tap3*output[2]);
                        sorter = 2;
                        break;
                    case 2:
                        filtered_two[filterdex] = (tap1*output[0]) + (tap2*output[1]) + (tap3*output[2]);
                        sorter = 0;
                        filterdex += 1;
                }
                // Iterate
                output[0] = output[1];
                output[1] = output[2];
            }
            sem_post(&slice_semaphore);
            n = 0;
        } // end if
    } // end while(1)
} // end task


void * slice() {

    while(1) {

        sem_wait(&slice_semaphore);

        int index=0, tracker=0;
        char temp0=0xff, temp1=0xff, temp2=0xff;
        int bit_index = 78;

        for (index = 0; index<400; index++){
            int test = tracker % 8;
            char mask = slice_n_dice(test);
            if (filtered_zero[index] <= 0) {temp0 &= mask;}
            if (filtered_one[index]  <= 0) {temp1 &= mask;}
            if (filtered_two[index]  <= 0) {temp2 &= mask;}
            if (test == 7) {
                received_test_0[bit_index] = temp0;
                received_test_1[bit_index] = temp1;
                received_test_2[bit_index] = temp2;
                temp0=0xff, temp1=0xff, temp2=0xff;
                bit_index += 1;
            }
            tracker += 1;
        }
        // you'll pop bitstrings over to FPGA here
        //
        numclocks = 0;
        sem_post(&search_semaphore);
    }
}


void * search() {

    while(1) {

        sem_wait(&search_semaphore);

        int i;
        while (numclocks < 400) {

            if ((received_test_0[0]==address[0]) && (received_test_0[1]==address[1]) && 
                (received_test_0[2]==address[2]) && (received_test_0[3]==address[3])){
                packetlen = received_test_0[4] + 6; 
                for (i=0; i<=packetlen; i++){
                    packet[i] = received_test_0[i];
                }
                sem_post(&check_semaphore);
                sem_wait(&search_semaphore);
            }

            if ((received_test_1[0]==address[0]) && (received_test_1[1]==address[1]) && 
                (received_test_1[2]==address[2]) && (received_test_1[3]==address[3])){
                packetlen = received_test_1[4] + 6;
                for (i=0; i<=packetlen; i++){
                    packet[i] = received_test_1[i];
                }
                sem_post(&check_semaphore);
                sem_wait(&search_semaphore);
            }

            if ((received_test_2[0]==address[0]) && (received_test_2[1]==address[1]) && 
                (received_test_2[2]==address[2]) && (received_test_2[3]==address[3])){
                packetlen = received_test_2[4] + 6;
                for (i=0; i<=packetlen; i++){
                    packet[i] = received_test_2[i];
                }
                sem_post(&check_semaphore);
                sem_wait(&search_semaphore);
            }

            clock_array(received_test_0);
            clock_array(received_test_1);
            clock_array(received_test_2);
            bzero(packet, 78);
            numclocks += 1;
        }
        sem_post(&tcpip_semaphore);
    }
}

void * check() {

    while (1) {
        sem_wait(&check_semaphore);

        int i;
        checksum = CRC_INIT; // Init value for CRC calculation
        for (i = 4; i < (packetlen-1); i++) {
          checksum = culCalcCRC(packet[i], checksum);
        }


        if(((checksum & 0xff)==packet[packetlen]) &&
            ((checksum>>8) & 0xff) == packet[packetlen-1]){
            sem_post(&exfiltrate_semaphore);
        }
        else{
            bzero(packet, 78);
            sem_post(&search_semaphore);
        }
    }
}

int connection = -1;
void * exfiltrate() {
    // =======================================================================
    // ========== Opens TCP/IP socket for packet exfiltration ================
    // =======================================================================
    // // struct sockaddr_in address; 
    // int new_fd = 0, valread; 
    // struct sockaddr_in serv_addr; 
    // if ((new_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) 
    // { 
    //     printf("\n Socket creation error \n"); 
    //     // return -1; 
    // } 
    // memset(&serv_addr, '0', sizeof(serv_addr)); 
    // serv_addr.sin_family = AF_INET; 
    // serv_addr.sin_port = htons(3490); 
    // //19265 
    // // Convert IPv4 and IPv6 addresses from text to binary form 
    // // 3.19.114.185
    // if(inet_pton(AF_INET, "169.254.113.120", &serv_addr.sin_addr)<=0)  
    // { 
    //     printf("\nInvalid address/ Address not supported \n"); 
    //     // return -1; 
    // } 
    // // int connection = -1;
    // // if (connect(new_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) 
    // while (connection < 0)
    // { 
    //  connection = connect(new_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    //     printf("\nRetrying Connection \n"); 
    //     // return -1; 
    // } 
    // =======================================================================
    // =======================================================================
    // =======================================================================

    FILE *fp;
    time_t now;

    int i;

    sem_post(&start_semaphore);

    while(1) {

        sem_wait(&exfiltrate_semaphore);

        counter += 1;
        printf("%d: ", counter);
        fp = fopen("stored_data.txt", "a+");
        time(&now);
        for (i=0; i<=packetlen; i++) {
            printf("%02x ", packet[i]);
            fprintf(fp, "%02x ", packet[i]);
        }
        fprintf(fp, "%s", ctime(&now));
        // fputs("\n", fp);
        fclose(fp);
        printf("\n");
        // if (send(new_fd, packet, packetlen+1, 0) == -1)
        //  perror("send");
        for (i=0; i<(packetlen*8); i++) {
            clock_array(received_test_0);
            clock_array(received_test_1);
            clock_array(received_test_2);
        }
        bzero(packet, 78);
        numclocks += packetlen*8;
        sem_post(&search_semaphore);
    }
}


int main(void)
{

    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);  
    // put two processsors into the list
    CPU_SET(0, &cpuset);
    CPU_SET(1, &cpuset);
    CPU_SET(2, &cpuset);
    CPU_SET(3, &cpuset);

    // the thread identifiers
    pthread_t thread_read, thread_demodulate, thread_filter, thread_search, thread_slice, thread_check, thread_exfiltrate ;

    // the semaphore inits
    sem_init(&tcpip_semaphore, 0, 1);
    sem_init(&search_semaphore, 0, 0);
    sem_init(&slice_semaphore, 0, 0);
    sem_init(&check_semaphore, 0, 0);
    sem_init(&start_semaphore, 0, 0);
    sem_init(&exfiltrate_semaphore, 0, 0);

    //For portability, explicitly create threads in a joinable state 
    // thread attribute used here to allow JOIN
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);

    // now the threads
    pthread_create(&thread_read,NULL,read_tcpip,NULL);
    pthread_create(&thread_search,NULL,search,NULL);
    pthread_create(&thread_slice,NULL,slice,NULL);
    pthread_create(&thread_check,NULL,check,NULL);
    pthread_create(&thread_exfiltrate,NULL, exfiltrate,NULL);

    // for efficiency, force  threads onto separate processors
    pthread_setaffinity_np(thread_read, sizeof(cpu_set_t), &cpuset);
    pthread_setaffinity_np(thread_search, sizeof(cpu_set_t), &cpuset);
    pthread_setaffinity_np(thread_slice, sizeof(cpu_set_t), &cpuset);
    pthread_setaffinity_np(thread_check, sizeof(cpu_set_t), &cpuset);
    pthread_setaffinity_np(thread_exfiltrate, sizeof(cpu_set_t), &cpuset);

    // In this case the thread never exit
    pthread_join(thread_read,NULL);
    pthread_join(thread_search,NULL);
    pthread_join(thread_slice,NULL);
    pthread_join(thread_check,NULL);
    pthread_join(thread_exfiltrate,NULL);
    return 0;
} // end main