Core concepts of GNU Radio
This is a very basic introductory tutorial to GNU Radio. You should definitely read this before reading anything else. Even if you've heard of all of this, at least skim over to see if there's something you've missed.
Flow graphs -- and what they're made of
Before we're going anywhere, first we need to understand the most basic concepts about GNU Radio: flow graphs and blocks.
Flow graphs are graphs (as in theory) through which data flows. Many GNU Radio applications contain nothing other than a flow graph. The nodes of such a graph are called blocks, and the data flows along the edges.
Any actual signal processing is done in the blocks. Ideally, every block does exactly one job - this way GNU Radio stays modular and flexible. Blocks are usually written in C++ (might also be Python); writing new blocks is not very difficult.
In order to illuminate this diffuse topic a little, let's start with an example (all these examples were created with the GNU Radio companion (GRC), a graphical user interface to GNU Radio).
Here, we have three blocks (the rectangles). Data flows from left to right in this example, meaning that it originates in the audio source, passes through the low pass filter and ends in a file that gets written to hard disk.
The blocks are connected at ports. The first block has no input port, it produces samples. Such a block, with only output ports, is called a source. In an analog fashion, the final block, with no outputs, is called a sink.
Sometimes, this is confusing: from the user's perspective, the audio block (which takes samples from the sound card) is just a part of the processing. When we're talking about sinks and sources, we always mean from the flow graph's perspective.
So what's happening here. The audio source block is connected to the sound card driver and outputs audio samples. These samples are connected to the low pass filter, which processes them further. Finally, the samples are passed to a block which writes them to a WAV file.
In general, we call whatever a block outputs an item. In the previous example, one item was a float value representing one sample produced by the audio driver. However, an item can be anything that can be represented digitally. The most common types of samples are real samples (as before), complex samples (the most common type in software defined radio), integer types, and vectors of these scalar types.
To understand this latter concept, consider an FFT analysis. Say we want to perform an FFT to a signal before we save it a file. Of course, we need a certain number of samples at a time to calculate an FFT; unlike the filters, it does not work sample-wise.
Here's how it works:
There's a new block in here called 'Stream to vector'. What's special about this is that its input type is different to its output type. This block takes 1024 samples (i.e. 1024 items) and outputs them as one vector of 1024 samples (which is one item). The complex FFT outputs are then converted to their magnitude squared; which is a real data type (note how we use different colors at the ports to indicate different data types).
So remember: an item can be anything, a sample, a bunch of bits, a set of filter coefficients or whatever.
Here's what you should know by now:
- All signal processing in GNU Radio is done through flow graphs.
- A flow graph consists of blocks. A block does one signal processing operation, such as filtering, adding signals, transforming, decoding, hardware access or many others.
- Data passes between blocks in various formats, complex or real integers, floats or basically any kind of data type you can define.
- Every flow graph needs at least one sink and source.
So what does GNU Radio do?
First, this is what you do: you design the flow graph, choose the blocks, define the connections and tell GNU Radio about all of this. GNU Radio comes in twice, here: first, it supplies you with a multitude of blocks. Once the flow graph is defined, it executes the flow graph by calling the blocks one after another and makes sure the items are passed from one block to another.
Of course, if you're reading this, you know what sampling rates are (if you don't, go figure that out). Let's see how these relate to flow graphs. In the first example, the audio source has a fixed sampling rate of 32ksps. Because the filter doesn't change the sampling rate, the same sampling rate is used through the entire flow graph.
In the second example, the second block (stream to vector) produces one item for every 1024 input items. So, the rate at which it produces items is 1024 times smaller then the rate at which it consumes items (the fact that it actually produces bytes at the same rate it consumes them is irrelevant here). Such a block is called a decimator, well, because it decimates the item rate. A block which outputs more items than it receives is called an interpolator. If it produces and consumes at the same rate, it's a sync block.
Now, let's return to the second example. As mentioned, it has different sampling rates throughout the flow graph. But what's the base sampling rate?
OK, get ready: there's no such thing. As long as there is no hardware clock present which fixes the rate, sampling rate is meaningless--only relative rates (i.e. input to output rates are important. Your computer may handle the samples as fast as it wants (note that this can cause your computer to lock up by allocating 100% of CPU cycles to your signal processing).
Here's another example:
First of all, what's new here is that the sink has two inputs. Each port goes to one channel (left, right) of the sound card, which runs at a fixed sampling rate.
More on blocks (and atomicity)
Let's go back to the blocks. The biggest part of GNU Radio is the large number of blocks that GNU Radio provides. When you start using GNU Radio, you'll be connecting block after block. Sooner or later, you'll need a block that's not available in GNU Radio, which you can write yourself. It's not hard.
The question is, how much do you put into one block? Ideally, blocks are as atomic as possible; every block does exactly one job - this way GNU Radio stays modular and flexible. However, sometimes this just doesn't work. Some blocks do a lot of work at once. You'll probably find there's a performance vs. modularity tradeoff.
Simulink-users: Ignore all you know about frame- vs. sample-based processing.
This is a special section only for Simulink-users. In Simulink, flow graphs can be configured to run either frame-based, or sample-based. The difference is that in the sample-based model, samples are passed from block to block individually, and therefore the control over the signal processing stream is maximal. However, this comes at a performance loss, and therefore Simulink introduces the frame-based processing.
Now, when you're coming from Simulink: forget all of this. It just doesn't make sense in GNU Radio. In GNU Radio, there is only item-based, and, quite often, an item is a sample (but it could also be a vector). The item sizes are a logical description of what you get at the input ports.
The reason GNU Radio does not have this performance loss (although it deals with individual items) is because it tries to process as many items as possible at once. In a sense, it is both sample-based and frame-based.
The downside of the GNU Radio approach is that it is not trivial to introduce recursive flow graphs. The upside is a massive performance improvement.
A stream of samples is much more interesting when there is parsable metadata connected to that stream, such as the time of reception, centre frequency, sampling rate or even protocol-specific information such as node identification.
In GNU Radio, adding metadata to sample streams is done via a mechanism called stream tags. A stream tag is an object that is connected to a specific item (e.g. a sample). This can be a scalar value of any type, a vector, a list, a dictionary or whatever the user specifies.
When saving streams to disk, the metadata can be saved as well (see also the | Metadata Documentation Page).
Streams vs. Messages: Passing PDUs
All the blocks presented so far operate as "infinite stream" blocks, i.e., they simply continue working as long as items are fed into their inputs. The low pass filter is a good example: Every new item is interpreted as a new sample, and the output is always the low-pass filtered version of the input. It does not care about the content of the signal, be it noise, data or whatever.
When handling packets (or PDUs, protocol data units), such a behaviour is not enough. There must be a way to identify PDU boundaries, i.e. tell which byte is the first of this packet and how long it is.
GNU Radio supports two ways to do this: Message passing and tagged stream blocks.
The first is an asynchronous method to directly pass PDUs from one block to another. On a MAC layer, this is probably the preferred behaviour. A block could receive a PDU, add a packet header, and pass the entire packet (including the new header) as a new PDU to another block (see also the [message passing manual page]).
Tagged stream blocks are regular streaming blocks which use stream tags to identify PDU boundaries (see the tagged stream blocks manual page]). This allows mixing blocks which know about PDUs and blocks that don't care about them. There are also blocks to switch between message passing and tagged stream blocks.