(3 intermediate revisions by the same user not shown)
Line 12: Line 12:
 
Since this radio module is for handheld units that transmit audio, I needed some sort of modem for converting data to an analog format and vice-versa.  One of the methods of modulation I investigated was DTMF, which is used commonly in amateur radio and is built in to many consumer units.  However, DTMF modems tend to be rather large pin-wise and aren't compatible with most digipeaters, so I decided to build one from scratch using my favorite microcontroller, the ATTiny85.  For starters, I need to modulate and demodulate incoming data (known as `symbols` in communications) so they can be transmitted over the air.  In this case, most digipeaters use a standard known as `Bell-202` (also known as AFSK-1200), which is a simple modulation scheme where a logic 1 (mark) is represented by a 1200 Hz tone and a logic 0 (space) is represented by a 2200 Hz tone.  I accomplished this by calculating an 8-point DTFT at 1200 Hz and 2200 Hz at a sample rate of 9600 Hz (determined through testing).  I didn't use the FFT here because I'm only interested in the spectral power at two points.
 
Since this radio module is for handheld units that transmit audio, I needed some sort of modem for converting data to an analog format and vice-versa.  One of the methods of modulation I investigated was DTMF, which is used commonly in amateur radio and is built in to many consumer units.  However, DTMF modems tend to be rather large pin-wise and aren't compatible with most digipeaters, so I decided to build one from scratch using my favorite microcontroller, the ATTiny85.  For starters, I need to modulate and demodulate incoming data (known as `symbols` in communications) so they can be transmitted over the air.  In this case, most digipeaters use a standard known as `Bell-202` (also known as AFSK-1200), which is a simple modulation scheme where a logic 1 (mark) is represented by a 1200 Hz tone and a logic 0 (space) is represented by a 2200 Hz tone.  I accomplished this by calculating an 8-point DTFT at 1200 Hz and 2200 Hz at a sample rate of 9600 Hz (determined through testing).  I didn't use the FFT here because I'm only interested in the spectral power at two points.
  
Once I generated my DTFT coefficients, I used the [https://en.wikipedia.org/wiki/Q_%28number_format%29 Q number format], which is a method of handling signed fractional numbers within fixed point datatypes. I wrote a bit of code to do the conversion.
+
Once I generated my DTFT coefficients, I used the [https://en.wikipedia.org/wiki/Q_%28number_format%29 Q number format], which is a method of handling signed fractional numbers within fixed point datatypes, to convert the coefficients to a form that can be used on a microcontroller.   I wrote a bit of code to do the conversion.
  
 
== Calculating coefficients ==
 
== Calculating coefficients ==
Line 60: Line 60:
 
== Simulation ==
 
== Simulation ==
 
----
 
----
Before implementing my code on the ATTiny, I wanted to do some verification and evaluate performance.  Fortunately, Octave has a [https://www.gnu.org/software/octave/doc/v4.0.1/Getting-Started-with-Oct_002dFiles.html C/C++ api] which allowed me to test my code with the same bit-precision as I would have on the microcontroller.
+
Before implementing my code on the ATTiny, I wanted to do some verification and evaluate performance.  Fortunately, Octave has a [https://www.gnu.org/software/octave/doc/v4.0.1/Getting-Started-with-Oct_002dFiles.html C/C++ api] which allowed me to test my code with the same bit-precision as I would have on the microcontroller. Below you can see the 4 arrays of DTFT coefficients that were generated in Q.7 number format (1 sign bit, 7 data bits).
 +
 
 +
    <div style="background-color: #ddf5eb; border-style: dotted;">
 +
  // Evan Widloski - FSK Computation
 +
    // Intended to be called from Octave
 +
    #include <octave/oct.h>
 +
    #include <stdint.h>
 +
    #include <math.h>
 +
 
 +
    // Input: 8 element row-vector containing time samples, 9600 samp/s
 +
    // Output: 1 element. DTFT(2200) - DTFT(1200)
 +
 
 +
    DEFUN_DLD(compute_cpp, args, , "FSK"){
 +
    // get input vector from args
 +
    int8NDArray arg = args(0).vector_value();
 +
    int8_t in[8] = {arg(0), arg(1), arg(2), arg(3), arg(4), arg(5), arg(6), arg(7)};
 +
    printf("input:%d,%d,%d,%d,%d,%d,%d,%d\n",in[0],in[1],in[2],in[3],in[4],in[5],in[6],in[7]);
 +
 
 +
    // define coefficient vectors
 +
    int8_t coeff_1200_real[8] = {127,  90,    0,  -90, -127,  -90,    0,  90};
 +
    int8_t coeff_1200_imag[8] = {  0,  -90, -127,  -90,    0,  90,  127,  90};
 +
    int8_t coeff_2200_real[8] = {127,  16, -123,  -49,  110,  77,  -90, -101};
 +
    int8_t coeff_2200_imag[8] = {  0, -126,  -33,  117,  63, -101,  -90,  77};
 +
 
 +
    int16_t dtft_1200_real, dtft_1200_imag, dtft_2200_real, dtft_2200_imag;
 +
    dtft_1200_real =  dtft_1200_imag =  dtft_2200_real =  dtft_2200_imag = 0;
 +
 
 +
    for (int i = 0; i < 8; i++){
 +
      dtft_1200_real += (int8_t)(((int16_t)in[i]*(int16_t)coeff_1200_real[i])>>7);
 +
      dtft_1200_imag += (int8_t)(((int16_t)in[i]*(int16_t)coeff_1200_imag[i])>>7);
 +
      dtft_2200_real += (int8_t)(((int16_t)in[i]*(int16_t)coeff_2200_real[i])>>7);
 +
      dtft_2200_imag += (int8_t)(((int16_t)in[i]*(int16_t)coeff_2200_imag[i])>>7);
 +
    }
 +
    dtft_1200_real = dtft_1200_real>>3;
 +
    dtft_1200_imag = dtft_1200_imag>>3;
 +
    dtft_2200_real = dtft_2200_real>>3;
 +
    dtft_2200_imag = dtft_2200_imag>>3;
 +
 
 +
    int16_t dtft_1200 = dtft_1200_real*dtft_1200_real + dtft_1200_imag*dtft_1200_imag;
 +
    int16_t dtft_2200 = dtft_2200_real*dtft_2200_real + dtft_2200_imag*dtft_2200_imag;
 +
    return octave_value(dtft_2200 - dtft_1200);
 +
    return octave_value(dtft_2200);
 +
    }
 +
    </div>
  
 
Initially, I generated my test signals incoherently, meaning that the phase resets each time the signal transitions from a mark to a space and vice versa.  This creates discontinuities and adds noise to the spectrum in the transition regions.  From the results below, it was clear that my implementation really needed to be coherent.
 
Initially, I generated my test signals incoherently, meaning that the phase resets each time the signal transitions from a mark to a space and vice versa.  This creates discontinuities and adds noise to the spectrum in the transition regions.  From the results below, it was clear that my implementation really needed to be coherent.
Line 67: Line 110:
 
     [[File:spectrogram_incoherent.png|thumb|center|800px|Spectrogram view of incoherent test signal.  There is noticeable fuzziness in the transition regions.]]
 
     [[File:spectrogram_incoherent.png|thumb|center|800px|Spectrogram view of incoherent test signal.  There is noticeable fuzziness in the transition regions.]]
 
     [[File:modulation_coherent.png|thumb|center|800px|Coherent test signal.  No more discontinuities.]]
 
     [[File:modulation_coherent.png|thumb|center|800px|Coherent test signal.  No more discontinuities.]]
     [[File:spectrogram_coherent.png|thumb|center|800px|Spectrogram view of coherent test signal.  cleaner transitions between frequencies.]]
+
     [[File:spectrogram_coherent.png|thumb|center|800px|Spectrogram view of coherent test signal.  Cleaner transitions between frequencies.]]
  
 
== Bit synchronization ==
 
== Bit synchronization ==
 
----
 
----
  
At this point, the modem has no awareness of bits.  The modem simply modulates at 1200 or 2200 Hz if it sees a logic 1 or 0 and vice versa for demodulation.  If you look carefully at the thresholded output, you'll notice that the widths of the pulses are not exactly the same.  This was problematic because UARTs operate asynchronously (without a clock line) and depend on the timing and width of bits to be precise.  To solve this problem, I wrote some simple code that detects the phase of the thresholded output and samples it at regular intervals to ensure that the UART is receiving equally spaced bits.
+
At this point, the modem had no awareness of bits.  The modem simply modulated at 1200 or 2200 Hz if it saw a logic 1 or 0 and vice versa for demodulation.  If you look carefully at the thresholded output, you'll notice that the widths of the pulses are not exactly the same.  This was problematic because UARTs operate asynchronously (without a clock line) and depend on the timing and width of bits to be precise.  To solve this problem, I wrote some simple code that detects the phase of the thresholded output and samples it at regular intervals to ensure that the UART is receiving equally spaced bits.
  
 
     [[File:bit_clock.png|thumb|center|800px|Phase detect and bit synchronization.  Sample intervals shown in green]]
 
     [[File:bit_clock.png|thumb|center|800px|Phase detect and bit synchronization.  Sample intervals shown in green]]
Line 85: Line 128:
 
     [[File:pcb.jpg|thumb|center|800px|PCB Received]]
 
     [[File:pcb.jpg|thumb|center|800px|PCB Received]]
 
     [[File:pair.jpg|thumb|center|800px|Finished pair]]
 
     [[File:pair.jpg|thumb|center|800px|Finished pair]]
 +
 +
The modem and radio were fully functional, but there still needed to be some higher level logic to handle commands sent between devices.  I designed a simple 7 byte packet with addressing, payload and a [https://en.wikipedia.org/wiki/Cyclic_redundancy_check CRC8 checksum].
 +
 +
== Packetization ==
 +
----
 +
 +
{| border="1" style="border-collapse:collapse"
 +
!colspan="7"|Packet Specification
 +
|-
 +
|Start Byte
 +
|Address Byte
 +
|colspan="2"|Payload Bytes
 +
|colspan="2"|Checksum Bytes
 +
|End Byte
 +
|-
 +
|1
 +
|2
 +
|3
 +
|4
 +
|5
 +
|6
 +
|7
 +
|}
 +
 +
I was able to issue commands from a master device and control an LED (and other GPIO) on a slave device as well as receive information back from the slave.  From limited range tests I performed, the radios are able to transmit at least a mile with line-of-sight, but quite possibly much more than that.  I ran out of space before the signal began dropping.
 +
 +
== Antenna tuning ==
 +
----
 +
 +
Antennas are easily the point of greatest loss in range and throughput in amateur radio, and any mismatches between transmitter and antenna impedance cause reflections and loss of transmission power.  I used a network analyzer to measure and compensate my crappy dipole antenna so that it matched the <math>50 \Omega</math> output impedance of the radio module for greater range.
 +
 +
From the first Smith chart, we see that antenna has an impedance of <math>57 + 45j \Omega</math>  at <math>144</math> MHz.  If we can add some capacitance in series with the antenna, we can kill off the reactive component and get <math>57 \Omega</math>, which is "close enough". 
 +
 +
<math>\frac{1}{C 2 \pi j \omega} = -45j \Rightarrow C = 24pF</math>
 +
 +
So a 24 pF capacitor should be added in series to improve matching.
 +
 +
    [[File:setup.jpg|thumb|center|800px]]
 +
    [[File:smith_unmatched.jpg|thumb|center|800px|Approximately 57+45j Ohms]]
 +
    [[File:matching_network.jpg|thumb|center|800px|Compensation]]
 +
    [[File:smith_matched.jpg|thumb|center|800px|Approximately 50 Ohms]]
 +
 +
== Final thoughts ==
 +
- very comprehensive
 +
  - 438, 307, 270
 +
-
  
 
== Source code ==
 
== Source code ==

Latest revision as of 01:18, 30 November 2016

Problem


This is a prototype for a high altitude balloon project I worked on with the Purdue Orbital rocketry team. I designed this board to test the range and power draw of our comms system and threw on a relay and microcontroller so that it can be used for triggering things on a real flight.

One of the FAA requirements is that balloons over a certain weight have four cutdown systems (two on the balloon envelope and two on the payload tether) and that these systems must be independent, meaning they are electrically disconnected from each other and have separate batteries. Many amateurs balloonists overlook this rule, but due to our close work with the FAA it is important that we follow the regulations.

APRS and Modem Selection


To save on cost, I avoided the higher end radio specific devices on the market and stuck to hobby-tier components. First was the Baofeng DRA818V. This is cheap Chinese VHF transceiver that costs less than $20 on Amazon and has a decent amount of power. Some users claim that these devices bleed onto other channels and should have a lowpass filter on the output (mine is inline with the coax). There is also a UHF band available, but I chose VHF for potential future compatibility with the APRS system. APRS will be an essential part of the system, because most radios in this power level (0.5 - 1W transmission) can only achieve a few dozen miles tops transmitting from ground to balloon. The incorporation of APRS will greatly extend this range by taking advantage of amateur repeating stations called `digipeaters`.

Since this radio module is for handheld units that transmit audio, I needed some sort of modem for converting data to an analog format and vice-versa. One of the methods of modulation I investigated was DTMF, which is used commonly in amateur radio and is built in to many consumer units. However, DTMF modems tend to be rather large pin-wise and aren't compatible with most digipeaters, so I decided to build one from scratch using my favorite microcontroller, the ATTiny85. For starters, I need to modulate and demodulate incoming data (known as `symbols` in communications) so they can be transmitted over the air. In this case, most digipeaters use a standard known as `Bell-202` (also known as AFSK-1200), which is a simple modulation scheme where a logic 1 (mark) is represented by a 1200 Hz tone and a logic 0 (space) is represented by a 2200 Hz tone. I accomplished this by calculating an 8-point DTFT at 1200 Hz and 2200 Hz at a sample rate of 9600 Hz (determined through testing). I didn't use the FFT here because I'm only interested in the spectral power at two points.

Once I generated my DTFT coefficients, I used the Q number format, which is a method of handling signed fractional numbers within fixed point datatypes, to convert the coefficients to a form that can be used on a microcontroller. I wrote a bit of code to do the conversion.

Calculating coefficients


     #!/bin/octave
     ## Evan Widloski - 2016-10-15
     ## calculate DTFT coefficients and express as binary fractions
     ## ----- Convert decimal numbers to n bit signed binary fractions -----
     function out = dec2binfrac(x,n)
       ## round input array to nearest 1/(2^n)
       x = round(x * 2^n)/(2^n);
       k = [1:n-1];
       if (x < 0)
         x = 1+x;
         out = -2^(n-1);
       else
         out = 0;
       endif
       twos_complement = mod(abs(x),.5.^(k-1)) >= .5.^k;
       bin_values = 2.^[n-2:-1:0];
       out += sum(bin_values .* twos_complement);
     endfunction
     ## ----- Create coefficients for implementation in C -----
     N = 8
     ## generate DFT coefficients
     Xd1 = e.^(-i*2*pi*(1200/9600)*[0:N-1]);
     Xd2 = e.^(-i*2*pi*(2200/9600)*[0:N-1]);
     ## scale down coefficients by 1 bit, since `1` can't be expressed as binary fraction
     ## e.g. scale 1 to 127/128
     Xd1 = Xd1*((2^(N-1) - 1)/2^(N-1));
     Xd2 = Xd2*((2^(N-1) - 1)/2^(N-1));
     ## express coefficients as signed N-bit binary fractions (Q7)
     arrayfun(@(x) dec2binfrac(x,N),real(Xd1))
     arrayfun(@(x) dec2binfrac(x,N),imag(Xd1))
     arrayfun(@(x) dec2binfrac(x,N),real(Xd2))
     arrayfun(@(x) dec2binfrac(x,N),imag(Xd2))

This yielded 4 arrays which would calculate the DFT at 1200 and 2200 Hz with a 9600 Hz sample rate.

Simulation


Before implementing my code on the ATTiny, I wanted to do some verification and evaluate performance. Fortunately, Octave has a C/C++ api which allowed me to test my code with the same bit-precision as I would have on the microcontroller. Below you can see the 4 arrays of DTFT coefficients that were generated in Q.7 number format (1 sign bit, 7 data bits).

  // Evan Widloski - FSK Computation
   // Intended to be called from Octave
   #include <octave/oct.h>
   #include <stdint.h>
   #include <math.h>
   // Input: 8 element row-vector containing time samples, 9600 samp/s
   // Output: 1 element. DTFT(2200) - DTFT(1200)
   DEFUN_DLD(compute_cpp, args, , "FSK"){
   // get input vector from args
   int8NDArray arg = args(0).vector_value();
   int8_t in[8] = {arg(0), arg(1), arg(2), arg(3), arg(4), arg(5), arg(6), arg(7)};
   printf("input:%d,%d,%d,%d,%d,%d,%d,%d\n",in[0],in[1],in[2],in[3],in[4],in[5],in[6],in[7]);
   // define coefficient vectors
   int8_t coeff_1200_real[8] = {127,   90,    0,  -90, -127,  -90,    0,   90};
   int8_t coeff_1200_imag[8] = {  0,  -90, -127,  -90,    0,   90,  127,   90};
   int8_t coeff_2200_real[8] = {127,   16, -123,  -49,  110,   77,  -90, -101};
   int8_t coeff_2200_imag[8] = {  0, -126,  -33,  117,   63, -101,  -90,   77};
   int16_t dtft_1200_real, dtft_1200_imag, dtft_2200_real, dtft_2200_imag;
   dtft_1200_real =  dtft_1200_imag =  dtft_2200_real =  dtft_2200_imag = 0;
   for (int i = 0; i < 8; i++){
     dtft_1200_real += (int8_t)(((int16_t)in[i]*(int16_t)coeff_1200_real[i])>>7);
     dtft_1200_imag += (int8_t)(((int16_t)in[i]*(int16_t)coeff_1200_imag[i])>>7);
     dtft_2200_real += (int8_t)(((int16_t)in[i]*(int16_t)coeff_2200_real[i])>>7);
     dtft_2200_imag += (int8_t)(((int16_t)in[i]*(int16_t)coeff_2200_imag[i])>>7);
   }
   dtft_1200_real = dtft_1200_real>>3;
   dtft_1200_imag = dtft_1200_imag>>3;
   dtft_2200_real = dtft_2200_real>>3;
   dtft_2200_imag = dtft_2200_imag>>3;
   int16_t dtft_1200 = dtft_1200_real*dtft_1200_real + dtft_1200_imag*dtft_1200_imag;
   int16_t dtft_2200 = dtft_2200_real*dtft_2200_real + dtft_2200_imag*dtft_2200_imag;
   return octave_value(dtft_2200 - dtft_1200);
   return octave_value(dtft_2200);
   }

Initially, I generated my test signals incoherently, meaning that the phase resets each time the signal transitions from a mark to a space and vice versa. This creates discontinuities and adds noise to the spectrum in the transition regions. From the results below, it was clear that my implementation really needed to be coherent.

Incoherent test signal. Notice jumps in the generated signal
Spectrogram view of incoherent test signal. There is noticeable fuzziness in the transition regions.
Coherent test signal. No more discontinuities.
Spectrogram view of coherent test signal. Cleaner transitions between frequencies.

Bit synchronization


At this point, the modem had no awareness of bits. The modem simply modulated at 1200 or 2200 Hz if it saw a logic 1 or 0 and vice versa for demodulation. If you look carefully at the thresholded output, you'll notice that the widths of the pulses are not exactly the same. This was problematic because UARTs operate asynchronously (without a clock line) and depend on the timing and width of bits to be precise. To solve this problem, I wrote some simple code that detects the phase of the thresholded output and samples it at regular intervals to ensure that the UART is receiving equally spaced bits.

Phase detect and bit synchronization. Sample intervals shown in green

Construction


Direct modem-to-modem comms testing
Radio breadboarding and testing
Schematic
Board
PCB Received
Finished pair

The modem and radio were fully functional, but there still needed to be some higher level logic to handle commands sent between devices. I designed a simple 7 byte packet with addressing, payload and a CRC8 checksum.

Packetization


Packet Specification
Start Byte Address Byte Payload Bytes Checksum Bytes End Byte
1 2 3 4 5 6 7

I was able to issue commands from a master device and control an LED (and other GPIO) on a slave device as well as receive information back from the slave. From limited range tests I performed, the radios are able to transmit at least a mile with line-of-sight, but quite possibly much more than that. I ran out of space before the signal began dropping.

Antenna tuning


Antennas are easily the point of greatest loss in range and throughput in amateur radio, and any mismatches between transmitter and antenna impedance cause reflections and loss of transmission power. I used a network analyzer to measure and compensate my crappy dipole antenna so that it matched the $ 50 \Omega $ output impedance of the radio module for greater range.

From the first Smith chart, we see that antenna has an impedance of $ 57 + 45j \Omega $ at $ 144 $ MHz. If we can add some capacitance in series with the antenna, we can kill off the reactive component and get $ 57 \Omega $, which is "close enough".

$ \frac{1}{C 2 \pi j \omega} = -45j \Rightarrow C = 24pF $

So a 24 pF capacitor should be added in series to improve matching.

Setup.jpg
Approximately 57+45j Ohms
Compensation
Approximately 50 Ohms

Final thoughts

- very comprehensive

 - 438, 307, 270

-

Source code


File:Source.zip

  • modem simulation - simulation.m
  • DTFT coefficient generation - compute_coefficients.m
  • binary fraction converter - dec2binfrac.m
  • DTFT computation - compute_cpp.cpp
  • ATTiny85 modem - main.c

Alumni Liaison

Followed her dream after having raised her family.

Ruth Enoch, PhD Mathematics