I threw together a scheme procedure which dumps FFT data to Gnuplot.
You see, however, that I have more work to do as properly labeling the graph, as well as implementing averaging for a more practical spectrum display. The 512 points on the x-axis correspond to the 512 complex numbers in the FFT output buffer, but center frequency in this case is actually 103Mhz, at 8 msps.
scheme@(guile-user)> (define d (hackrf-open))
scheme@(guile-user)> (hackrf-sensible-defaults d)
scheme@(guile-user)> (hackrf-set-freq d 103000000)
scheme@(guile-user)> (hackrf-set-baseband-filter-bandwidth d 2000000)
scheme@(guile-user)> (hackrf-enable-amp d)
scheme@(guile-user)> (define c (cb-fft-to-gnuplot))
scheme@(guile-user)> (hackrf-start-rx d c)
scheme@(guile-user)> (hackrf-stop-rx d)
It was mostly easy to write the scheme -> Gnuplot interface as that is just sending text commands to Gnuplot standard input. However, it took me a while to fix an elusive bug in my C FFT function.
I spent quite a while troubleshooting a bug in which RX would mysteriously not restart, if you did a hackrf-stop-rx followed by another hackrf-start-rx. The problem actually was not in my code, but due to some old libhackrf bugs that had not been patched in the old Debian 9 version of libhackrf which I am using.
This is a serious enough annoyance that you won’t want to be using HackRF Shell with the unpatched version. So, I added instructions to my git repo (debian9-libhackrf-patch directory, see commit 346c50e) on how to get a patched version of the Debian 9 packages. I think that the Debian 10 library version is actually not new enough to avoid all the bugs, either, so the info I have provided might be of value to Debian 10 users as well.
I would like to switch my home system from Debian to Gnu Guix, and use Guix package management for development, but I’m not sure how soon that will happen.
I added in the baseband-filter-bandwidth control procedure, which is something I forgot to do earlier. This was critical for picking up the weaker stations, such as KJNP 100.3Mhz, which is around 20 or 30 miles away, I think. I coded some simple helper functions (in Scheme) to start and stop receiving data according to time parameters, which I will use to record my favorite radio program each morning. This example records data for one minute from 8:43pm to 8:44pm (code checkout a992f67).
scheme@(guile-user)> (define d (hackrf-open))
scheme@(guile-user)> (load "hackrf-shell-lib.scm")
scheme@(guile-user)> (hackrf-sensible-defaults d)
scheme@(guile-user)> (hackrf-set-baseband-filter-bandwidth d 2000000)
scheme@(guile-user)> (hackrf-enable-amp d)
scheme@(guile-user)> (timed-read d "out.bin" 20 43 20 44)
This still just dumps the floating point signal data to a file, rather that doing any demodulation, so the file size is very large, and I must feed it into GnuRadio. Yet, it is progress.
I need to go over the RX start/stop code again as I get an error if I try to start RX again after stopping it. I coded that part of the device management rather quickly so I am not surprised.
I started playing around with merging in FFT functionality. I added an fft-512 procedure which does FFT on a 512 byte buffer using libfftwf. I think it works, but I haven’t added any procedures yet to do anything useful with fft-512 so I don’t really know yet. I was going to code something which feeds data to GnuPlot for a spectrum analysis display, in the usual fashion like all the SDR software does:
I have been learning a lot lately about Fourier transform and DFT (Discrete Fourier Transform) and I think I have a mostly clear understanding of the basic math and concepts involved now. For fun, I did a DFT operation manually in Emacs Calc on a length 8 data sample, and the results came out making sense. This article is a nice introduction to the Fourier transform, though you need to have a good understanding of complex numbers to fully grasp the DFT equation:
As expected, the bottle neck has disappeared at the floating point conversion. At least, I can say that I didn’t have trouble pulling 8 million samples per second (msps), which with 32 bit floating point (and 2 floating point numbers per sample) is 64 MB/sec. (I am short on time this evening, so I haven’t had a chance yet to try 20 msps.) As before, I fed the data into a Gnu Radio FM demodulator and got clean FM radio station audio out of it.
GCC 6.3 with -O3 appears to do some SSE optimization on the byte buffer to float buffer conversion:
The next thing, perhaps, should be to create a little demo program where it captures the data at certain times of day. Or I could work on the next stages of an FM receiver, i.e., frequency multiplication, a low pass filter, and the FM demodulator.
The shell is functioning, including RX functionality (not TX). Currently the process looks like so:
christopher@nightshade:~/Repos/hackrf-shell$ ./hackrf-shell
GNU Guile 2.2.3
Copyright (C) 1995-2017 Free Software Foundation, Inc.
Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'.
This program is free software, and you are welcome to redistribute it
under certain conditions; type `,show c' for details.
Enter `,help' for help.
scheme@(guile-user)> (load "hackrf-shell-lib.scm")
scheme@(guile-user)> (define d (hackrf-open))
scheme@(guile-user)> (hackrf-sensible-defaults d)
scheme@(guile-user)> (define c (hackrf-cb-rx-to-file "out.bin"))
scheme@(guile-user)> (hackrf-start-rx d c)
scheme@(guile-user)> (hackrf-stop d c)
hackrf-sensible-defaults is simply an alias for
(hackrf-set-freq d 99500000)
(hackrf-set-sample-rate d 8000000)
(hackrf-disable-amp d)
(hackrf-set-lna-gain d 16)
(hackrf-set-vga-gain d 16))
The hackrf-start-rx procedure must receive a callback function which will handle the data each time a block of data is received through libhackrf. The callback function receives a pointer object to the data buffer, which can be converted to a bytevector with pointer->bytevector, and also the byte length of the buffer (an int). User can put together their own callback functions, though I was planning to add more for common use cases. Here are the ones included now:
(define (hackrf-cb-rx-to-stream out)
(lambda (b bl)
(let ([bv (pointer->bytevector b bl)])
(put-bytevector out bv))))
(define (hackrf-cb-rx-to-file filename)
(hackrf-cb-rx-to-stream
(open-output-file filename)))
I could expand this by adding more such scheme functions for common use cases (e.g., FM demodulation or frequency analysis) with helper functions written in C to handle intensive mathematical operations (e.g., FFT). You can see here the power of Guile scheme, allowing user to load in any Guile scheme code they want, while I can provide helper functions coded in C to allow for maximum efficiency in critical mathematical operations.
The current code simply provides the raw 8-bit IQ data, but the user may want to receive the IQ data as 32-bit floating point numbers. This should be an easy function to add, which I will code in C to allow for SSE/AVX style compiler optimizations. It still remains to be seen if I will run into a performance bottle neck with the byte to float conversions, but since I do not need to do any IPC with hackrf-shell, I do not expect this to be a problem.
To confirm received data is not junk, I saved about 1 GB of IQ data to “out.bin”, converted the data to floating point values using Gnu Radio, and then ran the data through an FM demodulator Gnu Radio program. I was able to tune into several different radio stations recorded in the save data, such as a local country music station, and I did not hear any distortion or skips.