Random Quote Board
How to Create a Basic Spectral Display Using Gnu Radio Companion
You've seen'em. You love'em. Spectral displays! They're colorful, they can do all kinds of great things and, if you understand what they're showing you, can tell you all kinds of great things.
This post is essentially a written version of a Youtube video I created to cover the basics of how a spectral display works. As with the video, I'll be using Gnu Radio Companion to build this spectral display. Just as I did in the video, I'll be building this display one block at a time in order to explain what each component does.
The Signal Source
For a signal source, I'll be using a File Source with a Throttle block. (NOTE: I did a quick check, and I have almost a terabyte of IQ samples in various files. Might as well make use of them.) This particular file consists of complex, 8-bit signed samples. I used the "IChar to Complex" block to transform the samples into the 32-bit floating point samples that GRC uses for most of its processing.
Creating the Time Record
The fast Fourier transform (FFT) is a block-based algorithm. We need to pack a group of samples together into one of these blocks. For this, I'm using a variable "N" to set the block size. In GRC, I'm also going to use the "Stream to Vector" block to create the collection of samples. As I'm also going to continue to use the Time Sink for displaying the various stages, I'm adding a follow-on "Vector to Stream" as the Time Sink block can only process samples, not vectors of samples. The last part is to set the "Number of Points" in the Time Sink to the block size of "N".
For the rest of this post, assume that any new blocks added will need to have their "Vector Size" set to "N".
The result is not something that is really viewable by the end user. It will not appear any different than the initial flowgraph.
Windowing the Data
This is where we talk about a not-well-understood-by-many concept with respect to the FFT. The FFT operates on data that is sampled in the time domain. It then creates a spectrum that is sampled in the frequency domain. This is where we discuss the basic concept of:
\[ \Large Sampling <==> Repetition \]
What this means is that sampling in one domain (time or frequency) imposes repetition in the other domain. For example, since the data fed into the FFT is sampled time domain data, the frequency domain will repeat. This leads to the concept of "Nyquist zones" (the repeating pattern of the spectrum between +/- 1/2 of the sample rate).
The exact, same concept applies for a sampled frequency domain. A sampled frequency domain imposes repetition in the time domain. To demonstrate this, I created a flowgraph that calculates the FFT using a rectangular window (effectively does not window), zeropads the FFT output, then calculates the inverse FFT (IFFT).
This leads to the window, a set of weights typically applied to the time record. This window has a shape that is similar to that shown below.
To add the windowing to the flowgraph, I used a "Variable" block and a "Multiply Const" block, as shown below. The "Variable" block contains the window weights, and its applied to the samples using the "Multiply Const" block. (NOTE: I realize that the "FFT" block in Gnu Radio Companion has an entry for the window, but I prefer to window separately so that you can see the effect that windowing has on the data. So there.)
As you can see, the window "pinches" each end of the time record to zero. (NOTE: Not all windows will go to zero. The Hamming window, for example, only goes down to 0.08.) This alleviates the spectral leakage problem, as well as a good portion of the scalloping loss (depending on the window used).
Applying the FFT
Now that we've created our block of data (the time record of samples) and windowed it, it's time to apply the actual FFT transform. Here's where I'm going to take my time as I want to explain how I view the FFT (with a shout-out to Andrei, a viewer of my Youtube channel, for his insight).
The FFT transforms the time domain samples into the frequency domain. What, exactly, is it doing? The way that I look at it is that it is correlating the time domain samples with a series of frequency-shifted samples. Let's start by looking at the equation for calculating the discrete Fourier transform.
\[ \LARGE DFT = X[ m ] = \sum_{n=0}^{N-1} x[n] e^{ {-j 2 \pi n m / N}} \]
where:
- N = number of points in the time domain record (typically a power-of-2).
- m = frequency point counter = 0 ... N-1
- n = time point counter = 0 ... N-1
Each frequency value X[m] is a complex sample. Here's how each point is calculated (again, in my mind):
- At some frequency point m, we create a complex sinusoid waveform. This waveform will be an integer number (m) of cycles.
- The time domain samples will be multiplied on a point-by-point basis with complex sinusoid. This will frequency shift the spectrum of the time domain data for each frequency point such that that point will be shifted to 0.
- The frequency-shifted data will be summed, creating a correlation value for both the real and imaginary component at that frequency point.
- As each frequency point is represented by a real and imaginary component, this is equivalent to being represented by a magnitude and phase.
We can look at that complex sample as either a real and imaginary component, or as a magnitude and phase. These are equivalent representations.
\[ \LARGE X + jY = M \angle \theta \]
where:
- \( M = \sqrt {X^2 + Y^2} \)
- \( \theta = tan^{-1}(Y/X) \)
To summarize, the complex sinusoid is shifting the spectrum of the data (represented by x[n]) so that each frequency point is shifted to DC (0 Hz). It then sums the data, creating an average (correlation value) at that frequency point. This is how the DFT works. The FFT is nothing more than a very efficient algorithm for calculating the DFT.
Now its time to actually add the FFT block to our flowgraph.
The difference between this and the "normal" spectral display is that:
- Each point is represented by a complex sample, which means it represents both the magnitude and phase at that point. A normal spectral display only shows the magnitude.
- The vertical scale is linear. Normal spectral displays use a logarithmic scale (decibel or dB).
Calculating the Magnitude
Spectral displays show the magnitude of the signal at each frequency point. This is one of those, "Well, actually..." moments, too. They don't show the magnitude but the square of the magnitude or the magnitude squared. There are two ways we can do this. We can use the "Complex to Mag" block, or the "Complex to Mag^2" block. Either will work if you're not going to average the spectral data, but the "Complex to Mag^2" block is the better one to use. There are two reasons it is better.
- Since the magnitude value should be the magnitude squared, there's no reason to calculate the square root. Just calculate the value X2+Y2, and you're done. Calculating the square root is computationally expensive, anyway. So calculating the magnitude, then squaring it, is far more processing intensive and is unneeded.
- Calculating the average using the magnitude squared provides a more precise value than if using the magnitude value.
Transforming to Log Scale
The last step for creating a basic display is to transform the magnitudes from a linear scale to a logarithmic (decibel) scale. For this, we're going to use the "Log10" block. Adding this block and changing the "n" value to "10" (and, of course, setting the "Vector Size" to "N"), we transform the magnitude values to decibels (dB).
Adding the Vector Sink
Note that, up til now, we've not made use of the sample rate in any of the blocks except for the last one. And it didn't really apply. That's because the FFT does not care about sample rates. All the FFT knows is that you're feeding it a certain number of samples, which it is then performing calculations on. If the data is time domain data, then the output will be the frequency domain data. But, again, it has no idea of "sample rates" or "frequency" or anything like that. Those are scales that are applied afterwards. For this flowgraph, we'll use the "QT GUI Vector Sink". It will allow us to add a proper frequency scale on the horizontal axis.
This final result is equivalent to the QT GUI Frequency Sink.
Summary
And that, my friends, is how you create a basic spectral display. In the next post, I'll dive into making the vertical scale "normalized" so that you can, at the very least, get an idea of how well your SDR quantizer works by measuring its ENOB. I'll also go into how to properly perform averaging on the spectrum, including both an exponential and block average.
Til next time!