Freitag, 31. Oktober 2014

Performance optimizations of the demodulation code of RF Analyzer

When I started implementing AM and FM demodulation for the RF Analyzer, I first built a receiver in GNU Radio Companion and then tried to rebuild it in Java. The basic blocks in the receiver are pretty simple and soon I had working code. But I had to recognize how poorly it performs on an Android device with limited CPU power. I was far from performing demodulation in real time and so I had to re-build many parts and optimize them.

Here's a little post about some of the things I did to optimize the demodulation process and get it running in real time. By the way: The version I am talking about is RF Analyzer 1.07. Available on Google Play (https://play.google.com/store/apps/details?id=com.mantz_it.rfanalyzer) or, if you want to have a look into the source code, on GitHub (https://github.com/demantz/RFAnalyzer).


<UPDATE>
I've got a hint from Michael Ossmann to use single precision floating point variables instead of doubles. This this turned out to be a very significant optimization since it speeds up every operation performed on the signal samples. Version 1.08 contains these changes along with some other, rather minor optimizations.
</UPDATE>

Channel Selection

In order to shift the interesting signal down to base band it has to be multiplied by a complex cosine. This has to be done before any sample rate decimation is possible and therefore this will always run at the highest sample rate in our receive chain.

I was already using a lookup table to convert the signed interleaved IQ bytes from the HackRF to floating point values. So I decided to extend this table to also include the mixed values:

2-dimensional array as lookup table for samples multiplied by a cosine

By using this lookup table I don't have to do any multiplication to mix the signal down to baseband. The table has to be recreated as soon as the user changes the channel frequency:

cosineRealLookupTable = new float[bestLength][256];
cosineImagLookupTable = new float[bestLength][256];
float cosineAtT;
float sineAtT;
for (int t = 0; t < bestLength; t++) {
  cosineAtT = (float) Math.cos(2 * Math.PI * mixFrequency * t / (float) sampleRate);
  sineAtT = (float) Math.sin(2 * Math.PI * mixFrequency * t / (float) sampleRate);
  for (int i = 0; i < 256; i++) {
  cosineRealLookupTable[t][i] = (i-128)/128.0f * cosineAtT;
  cosineImagLookupTable[t][i] = (i-128)/128.0f * sineAtT;
  }
}

This lookup table strategy effectively gets rid of any multiplications needed for downmixing and speeds things up a lot.


Sample rate decimation

The next block is a decimating low pass filter used to slice out the interesting signal and decimate the sample rate. It enables the actual demodulation process to be performed in real-time at a much lower sample rate. However, the decimating low pass will still run at the high sample rate and is therefore our next target for optimization.

I tried many different things to speed up this low pass filter and I ended up with splitting it into a cascade of decimating low pass filters. Each filter decimates the sample rate by two which means only the first filter will run at highest sample rate. Decimation by two enables us to implement the low pass filter as a half-band filter. A half-band filter is a low pass filter with cut-off frequency at fs/4 and the positive characteristic of having every second filter tap equal to zero:

A half-band low pass filter. Graphic from Richard G. Lions "Understanding Signal Processing"

Because every second filter tap is zero we can implement this filter to require only half the multiplications needed for a standard FIR filter. We will also take advantage of the symmetry of the filters taps, effectively reducing the number of multiplications again by factor two!

Finally I implemented the filter in a very un-flexible way by hard-coding the filter tap values into the filter method. This gets rid of some conditional structures and array lookups in the code and makes it very fast at the cost of very ugly, un-flexible code.

By cascading a variable number of these filters we can decimate the sample rate by every integer power of 2. Note that the last filter in the line should always be a regular FIR filter with cut-off frequency < fs/4 to avoid aliasing.


Multithreading

It is obvious that we should take advantage of today's smartphones having multi-core CPUs. That's why we separate each block in our receiving chain into its own thread.

Biggest problem for multi-threading is always synchronization. I chose to use ArrayBlockingQueues to connect the threads together (sorry for the over wide image^^):

Every blue rectangle is a separate thread. The blocking queues help to synchronize them.
It is very important that we reuse the buffers by passing them back to the previous block every time we finished processing them. This will avoid memory allocations at runtime and the garbage collector going crazy.


Conclusion

The above design and implementation choices helped me to get demodulation running in real time. But this is only true if you run it on a device with a decent quad-core CPU. I don't have an old phone to test it on a weak CPU but I'm pretty sure it won't work on a dual- or single-core device.

If you tested the application on your phone, please leave a comment and tell me the device type and your experience.

I will try to further optimize the demodulation chain in order to get it running on older devices too! And of course to save battery! Any tips or hints are very welcome!

Sonntag, 19. Oktober 2014

RF Analyzer - Explore the frequency spectrum with the HackRF on an Android device

Over the last week I've been working on a new project, trying to build a spectrum analyzer for Android that works with my hackrf_android library. Now I finally reached the point were it is stable enough to be useful and I created the GitHub repository today:

https://github.com/demantz/RFAnalyzer

It is still very basic and I have a lot of ideas to extend its functionality, but I thought it's better to have testers involved as early as possible. Eventually it should evolve in something similar to GQRX, supporting different modes and devices. But that will take some time!

<UPDATE>

The new version of RF Analyzer (1.07) has now support for AM/FM demodulation! It is now also available on Google Play:

https://play.google.com/store/apps/details?id=com.mantz_it.rfanalyzer



See the readme on GitHub for a more detailed description!

</UPDATE>

RF Analyzer running on a Nexus 5

In this blog I'm going to show what you can do with the app and in the end I explain how it is working internally for those who like to play with the source code. I also tried to document the code as good as possible, but it is always easier if the basic flow of the program is explained before looking at it.

What you can do with it

Right now there aren't many fancy features. The app will present you with a simple UI showing the frequency spectrum including a waterfall plot. Here is a list of what you can do right now with version 1.00:
  • Browse the spectrum by scrolling horizontally
  • Zoom in and out, both horizontally and vertically
  • Adjust the sample rate and center frequency to match the current view of the screen by double tapping
  • Auto scale the vertical axis
  • Jump directly to a frequency
  • Adjust the gain settings of the HackRF
  • Select a pre-recorded file as source instead of a real HackRF
  • Change the FFT size
  • Setting the frame rate either to a fixed value or to automatic control
  • Activate logging and showing the log file
I'm planning to also support the rtl-sdr in the future and of course I want to include the actual demodulation for common analog modes like AM, FM, SSB, ... But so far you can only browse the spectrum. Here is how you get it to work:

Plug the HackRF into your Android device using an OTG (on-the-go) cable. You can get those cables for around 3$ and you can also find them as Y-version which enables external powering the HackRF, for those phones/tablets that don't deliver enough power. After you start RF Analyzer you can hit the start button in the action bar and it should prompt you for the permission to access the USB device. Once you did that the FFT will start:

FFT at 20 Msps showing FLEX pagers at 931 MHz

Use common gestures to zoom and scroll both vertically and horizontally. Note that the vertical axis of the FFT plot also affects the colors of the waterfall plot:

Zoomed in (both vertical and horizontal) view

If you scroll outside the current range of the FFT or if you zoom so that the resolution of the FFT is too low you can simply double tap the screen. RF Analyzer will re-tune the HackRF to the frequency currently centered on the screen and also ajust the sample rate so that the FFT covers exactly the frequency range that is currently visible:

The resolution of the FFT is too low when zoomed in too closely. And we scrolled to far right that we can see the end of the FFT on the right site

After double tapping the HackRF is tuned to 931,61 MHz (note the DC offset peak!) and the sample rate is now adjusted to about 2.5 Msps so that we see the full FFT resolution again

You can also use the autoscale button in the action bar to adjust the vertical scale so that it ranges from the minimum to the maximum of the currently visible values of the FFT:



If you want to jump to a certain frequency, use the 'set frequency' button and it will prompt you to enter a new frequency:



The gain settings of the HackRF (both VGA and LNA gain) can be accessed through the 'set gain' button in the overflow menu:



In the settings activity you can:
  • Select other source types (currently only HackRF or file source)
  • Set the FFT size
  • Set the screen orientation (auto / landscape / portrait)
  • Turn autostart on and off (so that you don't have to hit the start button every time)
  • Set the frame rate to auto or a fixed value (useful if you want a linear time axis in the waterfall plot)
  • Deactivate vertical zoom and scrolling (so that you don't accidentally alter the vertical scale while scrolling through frequencies)
  • Turn on logging and set the location of the log file.
  • Show the log file
Settings Activity of RF Analyzer on a Nexus 7

Implementing the file source was helpful for debugging the application. It is also a way to test the app if you don't have an OTG cable or your phone/tablet doesn't output enough power for the HackRF. Selecting the file source type will allow you to use RF Analyzer with recorded samples from hackrf_transfer or Test_HackRF. I've uploaded a short capture of some FLEX pager signals for testing: FLEX Pager at 931MHz (2Msps)

How it works

For those who want to play with the sources of RF Analyzer (GPLv2) I want to quickly explain the internal structure of the app:

(Uncomplete) class diagram of RF Analyzer. Underlined classes are running in seperate threads. Gray elements are external modules.

To support different devices I defined a common interface that is implemented by all classes which represent sources of IQ samples. The Scheduler will continuously read samples from the source to prevent the receive buffers of the device to fill up. It forwards samples in packets of the size of the FFT to the AnalyzerProcessingLoop by inserting them in a queue. If the queue is full, the samples are thrown away in order to not block the input device. The AnalyzerProcessingLoop also runs in a separate thread and reads the sample packets from the queue, processes them with the help of the FFT class and then calls draw() on the AnalyzerSurface. This method draws the given FFT samples on a SurfaceView and also draws a new line of the waterfall plot as well as the horizontal and vertical axis.

For a more detailed impression of how the app works, have a look into the sources on GitHub. I tried my best to add helpful comments to understand the flow of the program.

If you have any questions, comments or any other input, don't hesitate to leave a comment or contact me directly on Twitter:  @dennismantz

Have fun testing it! ;)

Here is the video were I demonstrate the old version of RF Analyzer:



Dienstag, 7. Oktober 2014

hackrf_android - Using the HackRF with a Android device

Since I received my HackRF last month I wanted to use it with an Android device. I own a Nexus 7 and a Nexus 5 and both are able to act as USB host. I also have an OTG cable to connect other USB devices to them. So I started to port Michael Ossmann's libhackrf library to Android and now I want to present the first alpha version. There is still a lot to do and to implement, but at least it's possible to get some samples out of the air and into the phone/tablet ;)

Receiving at 15 Msps on a Nexus 5
OTG cable (available for ~ 3$)

JNI vs. Pure Java

So the hackrf_android library is entirely written in Java. I thought about using Java Native Interface (JNI) to just reuse the original code from hackrf.c without modifications, but I decided not to do so. The advantage of a pure Java library is, that it is very easy to use (no need to care about NDK and JNI stuff). However, using the JNI approach would have advantages too, e.g. the hackrf.c code is completely tested and it fully implements all features. So maybe in the future I will give it a try.

The hackrf_android library

You can find the library on github: https://github.com/demantz/hackrf_android
The repository contains the library sources and an example application. Binaries of both are also in the rep if you don't want to build them on your own.

Basically the library consists only of the main class: Hackrf. In addition there is an HackrfUsbException class and a HackrfCallbackInterface which both don't contain much code. The Hackrf class has a static method called initHackrf(). This method will try to enumerate and open the HackRF on the USB port. This will require the user to hit OK when he is asked for the permission to do so. Therefore initHackrf() works asynchronously. It takes an instance of HackrfCallbackLibrary as argument and will call the onHackrfReady() method of this instance as soon as the device is ready to use. This call contains an instance of the Hackrf class which is then used for all operations on the device.

There are methods to set and get parameters of the device. Right now it is possible to:

  • Read Board ID from HackRF
  • Read Version from HackRF
  • Read Part ID and Serial Number from HackRF
  • Set Sample Rate of HackRF
  • Set Frequency of HackRF
  • Set Baseband Filter Width of HackRF
  • Compute Baseband Filter Width for given Sample Rate
  • Set VGA Gain (Rx/Tx) of HackRF
  • Set LNA Gain of HackRF
  • Set Amplifier of HackRF
  • Set Antenna Port Power of HackRF
  • Set Transceiver Mode of HackRF

Receiving Samples

The Hackrf class will put the samples into an ArrayBlockingQueue as they arrive over the USB connection. A reference to this queue will be returned to the application when it calls startRx(). The application can then grab the packets from the queue. Each queue element is a byte array containing the samples. The packets (arrays) all have a fixed length, which can be determined by calling getPacketSize(). These packets are the raw packets that are received through the USB connection. Make sure you pass packets you don't need anymore back to the buffer pool of the hackrf library. Otherwise the performance will drop because of the many memory allocations and garbage collection runs. Inside the packets the samples are stored as signed 8-bit values in pairs of two (first the quadrature sample then the in-phase sample - took me some time to find that out^^). Right now I set the packet size to 16KB. This might not be ideal. If it is to low (I started with 512B) the samples aren't getting fast enough from the HackRF to the Android device. If it is to high (I tried 256KB - same as in hackrf.c) then most of the bytes in the packets will be zero. Feel free to experiment with this, all you have to do is change the packetSize attribute in Hackrf.java.

Note that the size of the queue (measured in bytes!) is defined by the application as an argument to initHackrf(). Make sure you choose a large enough queue to buffer the samples between the hackrf library and your application. If the application doesn't pull the samples fast enough out of the queue, it will ran full pretty quickly and the hackrf will stop receiving. What I noticed on the Nexus 7: When writing the samples to a file (using a BufferedOutputStream), you can't set the sample rate to values higher than 2Msps. The write procedure to file will be too slow to get the samples fast enough out of the queue. Can anyone confirm this? It seems strange to me and I don't have this problem with my Nexus 5 (15Msps to a file - works like a charm!).

How to use it - a quick example

The example application I want to show you is also in the git repository. I used Eclipse with the ADT plugin to write it. If you want to do so too, just create two new Android Projects from existing sources and choose the root directory of the repository for the library and the example directory for the example App. Make sure that the example App is either linked against the library project (this should be the default case) or against the hackrf_android.jar file.

The example App shows the user this screen:


After opening the Hackrf the user can show some information about the device (this is equivalent to run the hackrf_info command from the hackrf tools) and he can start receiving. Right now the only parameters that can be adjusted through the GUI is sample rate and frequency. I will extend this as soon as I have time for it. The received samples will be written to the external memory (/storage/sdcard0/Test_HackRF/hackrf_receive.iq). You can view a nice fft of this file by copying it on a linux machine and running baudline with the following settings (Don't forget to adjust the samplerate and baseband frequency):

 cat hackrf_receive.io | baudline -stdin -quadrature -channels 2 -flipcomplex -format s8  -overlap 100 -memory 512 -fftsize 4096 -record -basefrequency 97000000 -samplerate 2000000


Baudline showing the fft of the recorded samples


Now I just want to point out the basic code snippets which are showing how to use the library in your App. Try to compare them to the example App in the repository, but notice, that I stripped off most of the code that is not related to using hackrf_android. So here is a minimal working example of how to receive samples in an App:

public class MainActivity extends Activity implements Runnable,
                                    HackrfCallbackInterface {

private boolean stopRequested = false;

// Called when the button "Open HackRF" is clicked
public void openHackrf(View view) {
    // at 2 Msps we will buffer for 1 second
    int queueSize = 2000000 * 2;    // each sample is 2 bytes
 
    if (!Hackrf.initHackrf(view.getContext(), this, queueSize))
    {
        System.out.println("No HackRF could be found!\n");
    }
}

// Called when the button "RX" is clicked
public void rx(View view) {
    stopRequested = false;
    // Run RX in separate thread to keep GUI responsive:
    new Thread(this).start();
}

// Called by hackrf_android library when device was opened successfully
public void onHackrfReady(Hackrf hackrf) {
    System.out.println("HackRF is ready!\n");
    this.hackrf = hackrf;
}

// Called by hackrf_android library when device could not be opened
public void onHackrfError(String message) {
    System.out.println("Error while opening HackRF: "+message+"\n");
}

// Runs in a separate thread
public void run() {
    try {
        System.out.print("Setting Sample Rate to 2000000 Sps...");
        hackrf.setSampleRate(2000000, 1);
        System.out.print("ok.\nSetting Frequency to 97000000 Hz...");
        hackrf.setFrequency(97000000);
        int bbFilter = Hackrf.computeBasebandFilterBandwidth(
                                            (int)(0.75*2000000));
        System.out.print("ok.\nSet BB Filter to "+bbFilter+" Hz...");
        hackrf.setBasebandFilterBandwidth(basebandFilterWidth);
        System.out.print("ok.\nSetting RX VGA Gain to 20...");
        hackrf.setRxVGAGain(20);
        System.out.print("ok.\nSetting LNA Gain to 8...");
        hackrf.setRxLNAGain(8);
        System.out.print("ok.\nSetting Amplifier to 'off'...");
        hackrf.setAmp(false);
        System.out.print("ok.\nSetting Antenna Power to 'off'...");
        hackrf.setAntennaPower(false);
        System.out.print("ok.\n\n");
     
        File file = new File(Environment.
                getExternalStorageDirectory(), "hackrf_receive.io");
        BufferedOutputStream bufferedOutputStream =
                new BufferedOutputStream(new FileOutputStream(file));
     
        // This starts receiving:
        ArrayBlockingQueue<byte[]> queue = hackrf.startRX();
     
        while(!this.stopRequested)
        {
            // Grab a packet from the queue:
            byte[] packet = queue.poll(1000, TimeUnit.MILLISECONDS);
            bufferedOutputStream.write(packet); // write it to file
            hackrf.returnBufferToBufferPool(receivedBytes);
        }
        bufferedOutputStream.close();
        System.out.print( String.format("Finished! (Avg Rate: " +
            "%4.1f MB/s)\n", hackrf.getAverageTransceiveRate()/1000000.0));
    } catch (HackrfUsbException e) {
        System.out.print("error (USB communication)!\n");
    } catch (IOException e) {
        System.out.print("error (File IO)!\n");
    } catch (InterruptedException e) {
        System.out.print("error (Queue Interrupt)!\n");
    }
}

Porting the hackrf library was fun and easier than I thought. I also learned a lot about USB :) At this point I want to say thank you to Michael Ossmann for creating such a great open source SDR platform. I hope hackrf_android helps to make it even more useful / mobile than it already is!

Let me know if you have issues with the library or if you successfully used it in your own App! Doing DSP on Android devices won't be easy without something like GNU Radio, but I'm curious what you guys will implement ;)

UPDATE:
I made a short video presenting the example application: