BPSK Transmitter Implementation
In the previous post we discussed BPSK Transmit Theory. This post will discuss the implementation of it. This implementation will be in the C programming language and it will also be a command line tool.
PSK Series
I’m doing a series on PSK here is where we are:
- BPSK Transmit Theory - This post shows graphs and has audio files of what this tool does.
- BPSK Implementation - This Post.
- BPSK Receiver Theory - Coming Soon
- BPSK Receiver Theory - Coming Soon
- QPSK - Coming Soon
Using the Command Line Tool
I like to start out with how the tool is used as it gives insight about the implementation. It’s quite simple, provide text input as a program argument and the tool will output a .wav
file with the content. The example below converts the text hello world
to the psk31.wav
audio file.
$ ./bpsk31tx --wavfile psk31.wav "hello world"
hello world -> 1010110011001101100110110011100100110101100111001010100110110010110100
$ ls -l psk31.wav
-rw-r--r-- 1 lloydroc lloydroc 4500044 Jan 24 14:38 psk31.wav
What the Implementation Doesn’t Do
The scope here could be very large. This is a blog post so we need to keep it simple. We will merely be converting text into PCM data. I’ve chosen to put this PCM data into an audio .wav
file as this makes the program portable. Interfacing with a sound card and other audio formats is something I’d like to write about later.
Also, it would be nice to have this program run in the background and have clients connect to it. This way you don’t need to write in C and other programs could merely send it text. The background process would handle transmission of BPSK out of the sound interface. This is something that could be built in later.
Program Options
I’ve built in a number of options. Most of these options deal with tweaking the transmission characteristics and debugging. When building a transmitter, especially in C, you need to be able to graph waveforms along the way. I dump out data to .csv
and use gnuplot
to graph them.
$ ./bpsk31tx
Usage: psk31tx [OPTIONS] TEXT
Options:
-h, --help Print this menu
--wavfile FILE Output .wav file [psk31.wav]
--csvfile FILE Output .csv file
--symbol-frequency DECIMAL Symbol Frequency [31.25]
--sample-frequency INTEGER Sample Frequency [8000]
--cycles-per-symbol INTEGER Cycles Per Symbol [16]
--bits-per-sample INTEGER Bits for Every Sample [16]
--matched-filter TYPE Matched filter: none or rrc for Root Raised Cosine [rrc]
Debugging Options:
--debug-transmission-csv FILE Write samples going to the sound card to CSV
--debug-carrier-float-csv FILE Write the Carriers for Each Symbol to CSV
--debug-carrier-integer-csv FILE Write the Carriers quantized to Integers to CSV
--debug-matched-float-csv FILE Write the matched filter to CSV
The Overall Implementation Process
The overall implementation process for a BPSK Transmitter will be the following:
- Take program input as text
- Map the text to our code alphabet of 0’s and 1’s
- Convert 0’s and 1’s to our symbols
- Convert symbols into PCM Data
- Write the PCM Data to a file
The PSK Structure
The heart of the program is a structure that contains all the information to create a BPSK transmission.
/* Handle BPSK and QPSK */
struct PSK
{
/* characteristics of PSK */
struct options *opts;
int phases; // 2 for bpsk and 4 for qpsk
int bytes_per_sample;
int samples_per_symbol;
float ts;
float symbol_duration;
float carrier_amplitude;
float carrier_frequency;
/* coding from text to binary */
const char (*alphabet)[11];
char *text_encoded;
size_t symbols_len;
char *symbols;
/* symbol construction */
struct cosine cos_i;
struct cosine cos_q;
struct cosine sin_i; // unused for bpsk
struct cosine sin_q; // unused for bpsk
float *matched_filter;
size_t matched_filter_length;
float* carrier_0i;
float* carrier_1i;
float* carrier_0q; // unused for bpsk
float* carrier_1q; // unused for bpsk
/* quantized carrier data */
int8_t* carrier_0i_q;
int8_t* carrier_1i_q;
int8_t* carrier_0q_q; // unused for bpsk
int8_t* carrier_1q_q; // unused for bpsk
/* PCM Data for the final output */
int8_t *transmission; // the int8_t can support 8, 16 and 24 bit quantization
ssize_t transmission_num_samples;
ssize_t transmission_num_bytes;
};
We will make function calls to operate on the struct PSK
and use it to take our input text, encode it, and phase key it. To initialize this structure we have a function called bpsk31_init
in the bpsk31.h
and bpsk31.c
files.
Program Options
We need a number of options to control the characteristics of PSK. Here is our structure to do so:
enum MATCHED_FILTER
{
NONE,
ROOT_RAISED_COSINE
};
struct options {
int help;
char *wavfile;
char *debug_transmission_csvfile;
char *debug_carrier_float_csvfile;
char *debug_carrier_integer_csvfile;
char *debug_matched_float_csvfile;
float symbol_frequency;
uint32_t sample_frequency;
float cycles_per_symbol;
uint8_t bits_per_sample;
uint8_t num_phases;
char *text;
enum MATCHED_FILTER matched_filter;
int error;
};
We keep a pointer to these options in the struct PSK
. Note, that BPSK31 is just a usage of the options.
BPSK Alphabet
Here is the BPSK Alphabet in C. It is is used to convert text to 0’s and 1’s. I had originally stored the varicode in binary, however, string matching becomes easier when using char
arrays.
For this alphabet we don’t have in the “rest”, or “delimiter” which is a double 00
after each code word. The order of the alphabet is the same as ASCII so a character in c
directly maps to the index in the alphabet.
const char ALPHABET[128][11] = {
"1010101011", // NUL
"1011011011", // SOH
"1011101101", // STX
"1101110111", // ETX
"1011101011", // EOT
"1101011111", // ENQ
"1011101111", // ACK
"1011111101", // BEL
"1011111111", // BS
"11101111", // HT
"11101", // LF
"1101101111", // VT
"1011011101", // FF
"11111", // CR
"1101110101", // SO
"1110101011", // SI
"1011110111", // DLE
"1011110101", // DC1
"1110101101", // DC2
"1110101111", // DC3
"1101011011", // DC4
"1101101011", // NAK
"1101101101", // SYN
"1101010111", // ETB
"1101111011", // CAN
"1101111101", // EM
"1110110111", // SUB
"1101010101", // ESC
"1101011101", // FS
"1110111011", // GS
"1011111011", // RS
"1101111111", // US
"1", // SP
"11111111", // !
"101011111", // "
"111110101", // #
"111011011", // $
"1011010101", // %
"1010111011", // &
"101111111", // '
"11111011", // (
"11110111", // )
"101101111", // *
"111011111", // +
"1110101", // ,
"110101", // -
"1010111", // .
"110101111", // /
"10110111", // 0
"10111101", // 1
"11101101", // 2
"11111111", // 3
"101110111", // 4
"101011011", // 5
"101101011", // 6
"110101101", // 7
"110101011", // 8
"110110111", // 9
"11110101", // :
"110111101", // ;
"111101101", // <
"1010101", // =
"111010111", // >
"1010101111", // ?
"1010111101", // @
"1111101", // A
"11101011", // B
"10101101", // C
"10110101", // D
"1110111", // E
"11011011", // F
"11111101", // G
"101010101", // H
"1111111", // I
"111111101", // J
"101111101", // K
"11010111", // L
"10111011", // M
"11011101", // N
"10101011", // O
"11010101", // P
"111011101", // Q
"10101111", // R
"1101111", // S
"1101101", // T
"101010111", // U
"110110101", // V
"101011101", // W
"101110101", // X
"101111011", // Y
"1010101101", // Z
"111110111", // [
"111101111", // back slash
"111111011", // ]
"1010111111", // ^
"101101101", // _
"1011011111", // `
"1011", // a
"1011111", // b
"101111", // c
"101101", // d
"11", // e
"111101", // f
"1011011", // g
"101011", // h
"1101", // i
"111101011", // j
"10111111", // k
"11011", // l
"111011", // m
"1111", // n
"111", // o
"111111", // p
"110111111", // q
"10101", // r
"10111", // s
"101", // t
"110111", // u
"1111011", // v
"1101011", // w
"11011111", // x
"1011101", // y
"111010101", // z
"1010110111", // {
"110111011", // |
"1010110101", // }
"1011010111", // ~
"1110110101" // DEL
};
Encoding Text from the Alphabet
We need to use the alphabet to encode our text using the alphabet. The result will be 0’s and 1’s. We will insert the rest between characters.
int
bpsk31_encode_text(struct PSK *bpsk31, char *text)
{
const size_t N = strlen(text);
// generously allocate memory for the symbols
// each symbol is 10 chars plus 2 0's for a rest and a NULL
bpsk31->text_encoded = malloc(N*13);
if(bpsk31->text_encoded == NULL)
{
fprintf(stderr, "out of memory\n");
return 1;
}
// make string zero length
bpsk31->text_encoded[0] = '\0';
for(int i=0; i<N; i++)
{
int index = text[i];
const char *alpha = bpsk31->alphabet[index];
strcat(bpsk31->text_encoded, alpha);
// insert the rest between characters
strcat(bpsk31->text_encoded, "00");
}
// allocate memory for the in-phase symbols
bpsk31->symbols_len = strlen(bpsk31->text_encoded);
bpsk31->symbols = malloc(bpsk31->symbols_len);
if(bpsk31->symbols == NULL)
{
fprintf(stderr, "out of memory\n");
return 1;
}
// convert ascii to numbers that are chars
for(int i=0; i<bpsk31->symbols_len; i++)
{
if(bpsk31->text_encoded[i] == '0')
bpsk31->symbols[i] = 0;
else
bpsk31->symbols[i] = 1;
}
return 0;
}
BPSK Symbol Creation
Now we have 1’s and 0’s encoded from our text. We then need to map our symbol waveforms to each of those. Each symbol waveform spans multiple symbol durations since the we’re using a Root Raised Cosine. Thus, we have account for overlap which makes things more complex.
It’s easiest to create a memory buffer set to zero and use pointers to move the symbol waveforms in. Since each symbol overlaps it makes it more complex where we need to perform addition on the overlap case.
int
bpsk31_make_transmission(struct PSK *bpsk31)
{
size_t num_symbols;
int8_t* dest_ptr;
int8_t* source_ptr;
// how many bytes are in one symbol period
// this excludes if a matched filter bleeds
// into another period
size_t bytes_per_symbol;
int overlaps, overlap_samples;
num_symbols = bpsk31->symbols_len;
if(num_symbols < 1)
return 0;
bytes_per_symbol = bpsk31->samples_per_symbol*bpsk31->bytes_per_sample;
overlaps = bpsk31->symbols_len-1;
overlap_samples = bpsk31->cos_i.num_samples - bpsk31->samples_per_symbol;
//overlap = overlap_samples*bpsk31->bytes_per_sample;
bpsk31->transmission_num_samples = bpsk31->cos_i.num_samples*num_symbols-overlaps*overlap_samples;
bpsk31->transmission_num_bytes = bpsk31->transission_num_samples * bpsk31->bytes_per_sample;
bpsk31->transmission = calloc(bpsk31->transmission_num_samples, bpsk31->bytes_per_sample);
dest_ptr = bpsk31->transmission;
if(bpsk31->opts->matched_filter)
memset(dest_ptr, 0, bpsk31->transmission_num_bytes);
for(int i=0; i<num_symbols; i++)
{
if(bpsk31->symbols[i] == 0)
source_ptr = bpsk31->carrier_0i_q;
else
source_ptr = bpsk31->carrier_1i_q;
if(bpsk31->opts->matched_filter)
{
// TODO assuming 2 bytes per sample
int16_t *dest_ptr_16 = (int16_t *) dest_ptr;
int16_t *source_ptr_16 = (int16_t *) source_ptr;
for(int j=0; j<bpsk31->cos_i.num_bytes/bpsk31->bytes_per_sample; j++)
{
dest_ptr_16[j] += source_ptr_16[j];
}
dest_ptr += bytes_per_symbol;
}
else
{
memcpy(dest_ptr, source_ptr, bpsk31->cos_i.num_bytes);
dest_ptr += bpsk31->cos_i.num_bytes;
}
}
return 0;
}
Source
I’m going to hold off on providing the full source until we work on the receive side of BPSK. I have a number of code clean-up items at this point to complete the series with the receiver. Drop a comment or email me if you need it.