Basic OFDM Tutorial: Difference between revisions

From GNU Radio
Jump to navigation Jump to search
No edit summary
(add Example loopback flowgraph)
Line 1: Line 1:
[[Category:Tutorials]]
<!-- Basic_OFDM_Tutorial.mediawiki -->
== Introduction ==
== Introduction ==


In the following, we assume the reader is familiar with [https://en.wikipedia.org/wiki/Orthogonal_frequency-division_multiplexing Orthogonal Frequency Division Multiplexing (OFDM)] and how it works.  
In the following, we assume the reader is familiar with [https://en.wikipedia.org/wiki/Orthogonal_frequency-division_multiplexing Orthogonal Frequency Division Multiplexing (OFDM)] and how it works. For an introduction to OFDM, refer to standard textbooks on digital communication such as found in [[SuggestedReading|Suggested Reading]].
For an introduction to OFDM refer to standard textbooks on digital communication.  


GNU Radio provides blocks to transmit and receive OFDM-modulated signals. These blocks are designed in a very generic fashion. As a developer, this means that
GNU Radio provides blocks to transmit and receive OFDM-modulated signals. These 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 one's own functionality is possible with very little effort.
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 one's own functionality is possible with
very little friction.


== Conventions and Notations ==
== Conventions and Notations ==
Line 15: Line 10:
=== FFT Shifting ===
=== FFT Shifting ===


In all cases where OFDM symbols are passed between blocks, the default behaviour
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 floor(N/2) where N is the FFT length and carrier indexing starts at 0).
is to FFT-Shift these symbols, i.e. that the DC carrier is in the middle (to be
precise, it is on carrier floor(N/2) 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
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.
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.


=== Carrier Indexing ===
=== Carrier Indexing ===
Line 40: Line 28:
=== Carrier and Symbol Allocation ===
=== Carrier and Symbol Allocation ===


Many blocks require knowledge of which carriers are allocated, and whether they
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).
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
Every one of these objects is a vector of vectors. occupied_carriers and
Line 49: Line 34:
symbols are stored, respectively.
symbols are stored, respectively.


occupied_carriers[0] identifies which carriers are occupied on the first
'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.
OFDM symbol, 'occupied_carriers[1]' does the same on the second OFDM symbol etc.


Here's an example:
Here's an example:
Line 57: Line 42:
   pilot_carriers = ((-3, 2), (-2, 3))
   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.
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.
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
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]).
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 parameterization:
But how are the pilot symbols set? This is a valid parameterization:
Line 70: Line 50:
   pilot_symbols = ((-1, 1j), (1, -1j), (-1, 1j), (-1j, 1))
   pilot_symbols = ((-1, 1j), (1, -1j), (-1, 1j), (-1j, 1))


The position of these symbols are those in pilot_carriers. So on the first OFDM
The position of these symbols are those 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].
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].


== Detection and Synchronization ==
== Detection and Synchronization ==


Before anything happens, an OFDM frame must be detected, the beginning of OFDM
Before anything happens, an OFDM frame must be detected, the beginning of OFDM symbols must be identified, and frequency offset must be estimated.
symbols must be identified, and frequency offset must be estimated.


== Transmitting ==
== Transmitting ==
Line 91: Line 66:
The first block is the carrier allocator (gr::digital::ofdm_carrier_allocator_cvc).
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
This sorts the incoming complex scalars onto OFDM carriers, and also places the
pilot symbols onto the correct positions.
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.
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).
The carrier allocator outputs OFDM symbols (i.e. complex vectors of FFT length).
Line 101: Line 73:
treated in the shifted form, the IFFT block must be shifting as well.
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
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).
can also perform pulse shaping on the OFDM symbols (raised cosine flanks in the
time domain).


== Receiving ==
== Receiving ==
Line 130: Line 100:


The last block in the frequency domain is the gr::digital::ofdm_serializer_vcc,
The last block in the frequency domain is the gr::digital::ofdm_serializer_vcc,
which is the inverse block to the carrier allocator.
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.
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
== Example loopback flowgraph ==
to a forward error correction decoder.
 
This example uses an [[OFDM_Transmitter]] block and an [[OFDM_Receiver]] block to send a file to a receiver.
 
=== Building the flowgraph ===
 
You can download the ofdm_example.grc flowgraph below by clicking [[Media:Ofdm_example.grc|here]].
 
[[File:Ofdm_example_fg.png|800px]]
 
The 'EPB: File Source to Tagged Stream' block is an Embedded Python Block which takes the place of a [[File_Sink]] block, a [[Stream_to_Tagged_Stream]] block, and parts of a [[Burst_Shaper]] block. The Python block performs the following functions:
* Send a preamble to allow the receiver to synchronize.
* Send the selected file with "packet_len" tags.
* Send a post-file filler to assure that any buffers have been flushed.
 
The content of the Embedded Python Block is shown here:
<pre>
"""
Embedded Python Block: File Source to Tagged Stream
"""
 
import numpy as np
from gnuradio import gr
import pmt
import os.path
import sys
 
class blk(gr.sync_block):
    def __init__(self, FileName='None', Pkt_len=256):
        gr.sync_block.__init__(
            self,
            name='EPB: File Source to Tagged Stream',
            in_sig=None,
            out_sig=[np.uint8])
        self.FileName = FileName
        self.Pkt_len = Pkt_len
        self.state = 0
        self.pre_count = 0
        self.indx = 0
        if (os.path.exists(self.FileName)):
            # open input file
            self.f_in = open (self.FileName, 'rb')
            self._eof = False
        else:
            print(FileName, 'does not exist')
            self._eof = True
        self.char_list = [37,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85, 85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85, 85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85, 85,85,85,93]
        self.c_len = len (self.char_list)
        # print (self.c_len)
 
    def work(self, input_items, output_items):
        if (self.state == 0):
            # send phasing filler
            key1 = pmt.intern("packet_len")
            val1 = pmt.from_long(self.c_len)
            self.add_item_tag(0, # Write to output port 0
                self.indx,  # Index of the tag
                key1,  # Key of the tag
                val1    # Value of the tag
                )
            self.indx += self.c_len
            i = 0
            while (i < self.c_len):
                output_items[0][i] = self.char_list[i]
                i += 1
            self.pre_count += 1
            if (self.pre_count > 19):
                self.state = 1
            return (self.c_len)
        elif (self.state == 1):
            while (not (self._eof)):
                buff = self.f_in.read (self.Pkt_len)
                b_len = len(buff)
                if b_len == 0:
                    print ('End of file')
                    self._eof = True
                    self.f_in.close()
                    self.state = 2
                    self.pre_count = 0
                    break
                key0 = pmt.intern("packet_len")
                val0 = pmt.from_long(b_len)
                self.add_item_tag(0, # Write to output port 0
                    self.indx,  # Index of the tag
                    key0,  # Key of the tag
                    val0    # Value of the tag
                    )
                self.indx += b_len
                i = 0
                while (i < b_len):
                    output_items[0][i] = buff[i]
                    i += 1
                return (b_len)
        elif (self.state == 2):
            # send idle filler
            key1 = pmt.intern("packet_len")
            val1 = pmt.from_long(self.c_len)
            self.add_item_tag(0, # Write to output port 0
                self.indx,  # Index of the tag
                key1,  # Key of the tag
                val1    # Value of the tag
                )
            self.indx += self.c_len
            i = 0
            while (i < self.c_len):
                output_items[0][i] = self.char_list[i]
                i += 1
            self.pre_count += 1
            if (self.pre_count > 3):
                self.state = 3
            return (self.c_len)
        return (0)
</pre>
 
The preamble is composed of the '%' character, followed by 50 'U's, followed by a ']'. It is repeated nineteen times to allow the receiver to synchronize. The post-file filler is sent three times.
 
=== Testing ===
 
The file source can be any text file of any length. A packet length of 52 has worked well in testing, and should be less than the FFT length of 64.
 
After executing the flowgraph, the "output.txt" file will contain a partial preamble followed by several more, depending on how long it takes the receiver to synchronize; then the file itself; then the post-file filler.
 
=== Sending binary data ===
 
Because sending random binary data can cause errors in the data transmission, it has been found that converting the binary file to [https://en.wikipedia.org/wiki/Base64 Base64 encoding] first works reliably. A post-processing program can be used to strip the preambles and convert the Base64 code back to binary data.
 
=== Notes ===
 
One approach to post processing the received file would be to replace the [[File_Sink]] with a [[ZMQ_PUB_Sink]]. A separate Python program then could receive the data and process it. For example code, see [[Understanding_ZMQ_Blocks#Python_Program_to_Process_Flowgraph_Data]].
 
[[Category:Tutorials]]

Revision as of 14:01, 14 April 2023

Introduction

In the following, we assume the reader is familiar with Orthogonal Frequency Division Multiplexing (OFDM) and how it works. For an introduction to OFDM, refer to standard textbooks on digital communication such as found in Suggested Reading.

GNU Radio provides blocks to transmit and receive OFDM-modulated signals. These 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 one's own functionality is possible with very little effort.

Conventions and Notations

FFT Shifting

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 floor(N/2) 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.

Carrier Indexing

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.

Carrier and Symbol Allocation

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 parameterization:

 pilot_symbols = ((-1, 1j), (1, -1j), (-1, 1j), (-1j, 1))

The position of these symbols are those 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].

Detection and Synchronization

Before anything happens, an OFDM frame must be detected, the beginning of OFDM symbols must be identified, and frequency offset must be estimated.

Transmitting

Core elements of an OFDM transmitter

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).

Receiving

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.

Core elements of an OFDM receiver

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.

Example loopback flowgraph

This example uses an OFDM_Transmitter block and an OFDM_Receiver block to send a file to a receiver.

Building the flowgraph

You can download the ofdm_example.grc flowgraph below by clicking here.

Ofdm example fg.png

The 'EPB: File Source to Tagged Stream' block is an Embedded Python Block which takes the place of a File_Sink block, a Stream_to_Tagged_Stream block, and parts of a Burst_Shaper block. The Python block performs the following functions:

  • Send a preamble to allow the receiver to synchronize.
  • Send the selected file with "packet_len" tags.
  • Send a post-file filler to assure that any buffers have been flushed.

The content of the Embedded Python Block is shown here:

"""
Embedded Python Block: File Source to Tagged Stream
"""

import numpy as np
from gnuradio import gr
import pmt
import os.path
import sys

class blk(gr.sync_block):
    def __init__(self, FileName='None', Pkt_len=256):
        gr.sync_block.__init__(
            self,
            name='EPB: File Source to Tagged Stream',
            in_sig=None,
            out_sig=[np.uint8])
        self.FileName = FileName
        self.Pkt_len = Pkt_len
        self.state = 0
        self.pre_count = 0
        self.indx = 0
        if (os.path.exists(self.FileName)):
            # open input file
            self.f_in = open (self.FileName, 'rb')
            self._eof = False
        else:
            print(FileName, 'does not exist')
            self._eof = True
        self.char_list = [37,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85, 85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85, 85,85,85,85,85,85,85,85,85,85,85,85,85,85,85,85, 85,85,85,93]
        self.c_len = len (self.char_list)
        # print (self.c_len)

    def work(self, input_items, output_items):
        if (self.state == 0):
            # send phasing filler
            key1 = pmt.intern("packet_len")
            val1 = pmt.from_long(self.c_len)
            self.add_item_tag(0, # Write to output port 0
                self.indx,   # Index of the tag
                key1,   # Key of the tag
                val1    # Value of the tag
                )
            self.indx += self.c_len
            i = 0
            while (i < self.c_len):
                output_items[0][i] = self.char_list[i]
                i += 1
            self.pre_count += 1
            if (self.pre_count > 19):
                self.state = 1
            return (self.c_len)
        elif (self.state == 1):
            while (not (self._eof)):
                buff = self.f_in.read (self.Pkt_len)
                b_len = len(buff)
                if b_len == 0:
                    print ('End of file')
                    self._eof = True
                    self.f_in.close()
                    self.state = 2
                    self.pre_count = 0
                    break
                key0 = pmt.intern("packet_len")
                val0 = pmt.from_long(b_len)
                self.add_item_tag(0, # Write to output port 0
                    self.indx,   # Index of the tag
                    key0,   # Key of the tag
                    val0    # Value of the tag
                    )
                self.indx += b_len
                i = 0
                while (i < b_len):
                    output_items[0][i] = buff[i]
                    i += 1
                return (b_len)
        elif (self.state == 2):
            # send idle filler
            key1 = pmt.intern("packet_len")
            val1 = pmt.from_long(self.c_len)
            self.add_item_tag(0, # Write to output port 0
                self.indx,   # Index of the tag
                key1,   # Key of the tag
                val1    # Value of the tag
                )
            self.indx += self.c_len
            i = 0
            while (i < self.c_len):
                output_items[0][i] = self.char_list[i]
                i += 1
            self.pre_count += 1
            if (self.pre_count > 3):
                self.state = 3
            return (self.c_len)
        return (0)

The preamble is composed of the '%' character, followed by 50 'U's, followed by a ']'. It is repeated nineteen times to allow the receiver to synchronize. The post-file filler is sent three times.

Testing

The file source can be any text file of any length. A packet length of 52 has worked well in testing, and should be less than the FFT length of 64.

After executing the flowgraph, the "output.txt" file will contain a partial preamble followed by several more, depending on how long it takes the receiver to synchronize; then the file itself; then the post-file filler.

Sending binary data

Because sending random binary data can cause errors in the data transmission, it has been found that converting the binary file to Base64 encoding first works reliably. A post-processing program can be used to strip the preambles and convert the Base64 code back to binary data.

Notes

One approach to post processing the received file would be to replace the File_Sink with a ZMQ_PUB_Sink. A separate Python program then could receive the data and process it. For example code, see Understanding_ZMQ_Blocks#Python_Program_to_Process_Flowgraph_Data.