GNU Radio 3.7.1 C++ API
|
GNU Radio provides some blocks to transmit and receive OFDM-modulated signals. In the following, we assume the reader is familiar with OFDM and how it works, for an introduction to OFDM refer to standard textbooks on digital communication.
The blocks are designed in a very generic fashion. As a developer, this means that often, a desired functionality can be achieved by correct parametrization of the available blocks, but in some cases, custom blocks have to be included. The design of the OFDM components is such that adding own functionality is possible with very little friction.
Packet Data Transmission has an example of how to use OFDM in a packet-based receiver.
In all cases where OFDM symbols are passed between blocks, the default behaviour is to FFT-Shift these symbols, i.e. that the DC carrier is in the middle (to be precise, it is on carrier where N is the FFT length and carrier indexing starts at 0).
The reason for this convention is that some blocks require FFT-shifted ordering of the symbols to function (such as gr::digital::ofdm_chanest_vcvc), and for consistency's sake, this was chosen as a default for all blocks that pass OFDM symbols. Also, when viewing OFDM symbols, FFT-shifted symbols are in their natural order, i.e. as they appear in the pass band.
Carriers are always index starting at the DC carrier, which has the index 0 (you usually don't want to occupy this carrier). The carriers right of the DC carrier (the ones at higher frequencies) are indexed with 1 through N/2-1 (N being the FFT length again).
The carriers left of the DC carrier (with lower frequencies) can be indexed -N/2 through -1 or N/2 through N-1. Carrier indices N-1 and -1 are thus equivalent. The advantage of using negative carrier indices is that the FFT length can be changed without changing the carrier indexing.
Many blocks require knowledge of which carriers are allocated, and whether they carry data or pilot symbols. GNU Radio blocks uses three objects for this, typically called occupied_carriers
(for the data symbols), pilot_carriers
and pilot_symbols
(for the pilot symbols).
Every one of these objects is a vector of vectors. occupied_carriers
and pilot_carriers
identify the position within a frame where data and pilot symbols are stored, respectively.
occupied_carriers
[0] identifies which carriers are occupied on the first OFDM symbol, occupied_carriers
[1] does the same on the second OFDM symbol etc.
Here's an example:
occupied_carriers = ((-2, -1, 1, 3), (-3, -1, 1, 2)) pilot_carriers = ((-3, 2), (-2, 3))
Every OFDM symbol carries 4 data symbols. On the first OFDM symbol, they are on carriers -2, -1, 1 and 3. Carriers -3 and 2 are not used, so they are where the pilot symbols can be placed. On the second OFDM symbol, the occupied carriers are -3, -1, 1 and 2. The pilot symbols must thus be placed elsewhere, and are put on carriers -2 and 3.
If there are more symbols in the OFDM frame than the length of occupied_carriers
or pilot_carriers
, they wrap around (in this example, the third OFDM symbol uses the allocation in occupied_carriers
[0]).
But how are the pilot symbols set? This is a valid parametrization:
pilot_symbols = ((-1, 1j), (1, -1j), (-1, 1j), (-1j, 1))
The position of these symbols are thos in pilot_carriers
. So on the first OFDM symbol, carrier -3 will transmit a -1, and carrier 2 will transmit a 1j. Note that pilot_symbols
is longer than pilot_carriers
in this example-- this is valid, the symbols in pilot_symbols
[2] will be mapped according to pilot_carriers
[0].
Before anything happens, an OFDM frame must be detected, the beginning of OFDM symbols must be identified, and frequency offset must be estimated.
This image shows a very simple example of a transmitter. It is assumed that the input is a stream of complex scalars with a length tag, i.e. the transmitter will work on one frame at a time.
The first block is the carrier allocator (gr::digital::ofdm_carrier_allocator_cvc). This sorts the incoming complex scalars onto OFDM carriers, and also places the pilot symbols onto the correct positions. There is also the option to pass OFDM symbols which are prepended in front of every frame (i.e. preamble symbols). These can be used for detection, synchronisation and channel estimation.
The carrier allocator outputs OFDM symbols (i.e. complex vectors of FFT length). These must be converted to time domain signals before continuing, which is why they are piped into an (I)FFT block. Note that because all the OFDM symbols are treated in the shifted form, the IFFT block must be shifting as well.
Finally, the cyclic prefix is added to the OFDM symbols. The gr::digital::ofdm_cyclic_prefixer can also perform pulse shaping on the OFDM symbols (raised cosine flanks in the time domain).
On the receiver side, some more effort is necessary. The following flow graph assumes that the input starts at the beginning of an OFDM frame and is prepended with a Schmidl & Cox preamble for coarse frequency correction and channel estimation. Also assumed is that the fine frequency offset is already corrected and that the cyclic prefix has been removed. The latter can be achieved by a gr::digital::header_payload_demux, the former can be done using a gr::digital::ofdm_sync_sc_cc.
First, an FFT shifts the OFDM symbols into the frequency domain, where the signal processing is performed (the OFDM frame is thus in the memory in matrix form). It is passed to a block that uses the preambles to perform channel estimation and coarse frequency offset. Both of these values are added to the output stream as tags; the preambles are then removed from the stream and not propagated.
Note that this block does not correct the OFDM frame. Both the coarse frequency offset correction and the equalizing (using the initial channel state estimate) are done in the following block, gr::digital::ofdm_frame_equalizer_vcvc. The interesting property about this block is that it uses a gr::digital::ofdm_equalizer_base derived object to perform the actual equalization.
The last block in the frequency domain is the gr::digital::ofdm_serializer_vcc, which is the inverse block to the carrier allocator. It plucks the data symbols from the occupied_carriers
and outputs them as a stream of complex scalars. These can then be directly converted to bits, or passed to a forward error correction decoder.