QPSK Mod and Demod
This tutorial updates and replaces the Guided Tutorial PSK Demodulation.
- Understand issues of signal distortion and channel effects.
- Recognize the stages required to transmit and receive QPSK signals.
The student should study each of the sections under the "Flowgraph Fundamentals" heading in Tutorials before attempting to do this tutorial.
- The ARRL Handbook, "Quadrature Modulation" section (any recent edition)
- f. j. harris and M. Rice, "Multirate Digital Filters for Symbol Timing Synchronization in Software Defined Radios", IEEE Selected Areas in Communications, Vol. 19, No. 12, Dec., 2001. 
- J. Feigin, "Practical Costas loop design: Designing a simple and inexpensive BPSK Costas loop carrier recovery circuit," RF signal processing, pp. 20-36, 2002
- Our Suggested Reading list
This tutorial has been revised and tested with GNU Radio version 184.108.40.206. It strives to provide consistent flowgraphs where each stage builds on the previous one, maintaining the same parameters from one stage to the next.
It is intended that the reader study the flowgraphs and resulting output, but not necessarily build each one. However, links to GRC source files are included. Clicking the link of a '.grc' file will cause it to be downloaded. That file then can be opened by gnuradio-companion.
Transmitting a QPSK Signal
The first stage is transmitting the QPSK signal. We generate a stream of bits and modulate it onto a complex constellation. To do this, we use the Constellation Modulator block, which uses a Constellation Rect. Object and other settings to control the transmitted signal. The Constellation parameter of the Constellation Modulator is the id of the Constellation Rect. Object (qpsk), even though it shows on the flowgraph as something else.
The constellation object specifies how the symbols are coded. The modulator block can then use this modulation scheme with or without differential encoding. The constellation modulator expects packed bytes, so we have a random source generator providing bytes with values of 0 to 255.
When dealing with the number of samples per symbol, we want to keep this value as small as possible (minimum value of 2). Generally, we can use this value to help us match the desired bit rate with the sample rate of a hardware device. But since we're using simulation, the samples per symbol is only important in making sure we match this rate throughout the flowgraph. We'll use 4 here, which is greater than what we need, but useful to visualize the signal in the different domains.
The constellation modulator uses a root raised cosine (RRC) pulse shaping filter to control the bandwidth of the transmit signal. That parameter is called "Excess BW" (excess bandwidth).
The flowgraph below, Media:Qpsk_rrc_rolloff.grc, generates the following figure showing different values of the excess bandwidth. Typical values used are between 0.2 (red trace) and 0.35 (green trace). We will use 0.35 in this tutorial.
Matched Filters and ISI
The example flowgraph, Media:Qpsk_stage1.grc, transmits a QPSK constellation. It plots both the transmitted signal and part of the receiver chain in time, frequency, and the constellation plot. The variable
rrc_taps value is
In the constellation plot below, we see the effects of the up-sampling (generating 4 samples per symbol) and filtering process. The RRC filter limits the transmit bandwidth so the signal is within our desired bandwidth. If we didn't put a shaping filter on the signal, we would be transmitting square waves which produce a lot of energy in the adjacent channels.
A side effect of the RRC filter is to create inter-symbol interference (ISI). ISI is bad for a received signal because it blurs the symbols together. We'll look into this in-depth during the timing recovery section.
On the receive side, we get rid of ISI by using another filter. Basically, what we've done is used a filter on the transmitter, the RRC filter, which creates the ISI. But when we convolve two RRC filters together, we get a raised cosine filter (which is a form of a Nyquist filter). So, knowing this property of the transmit RRC filter, we can use another RRC filter at the receiver to minimize ISI.
The first stage example only dealt with the mechanics of transmitting a QPSK signal. We'll now look into the effects of the channel and how the signal is distorted between when it was transmitted and when we see the signal in the receiver. The first step is to add a channel model, which is done using the example Media:Qpsk_stage2.grc below. We'll use the basic Channel Model block of GNU Radio.
This block allows us to simulate a few main issues that we have to deal with. The first issue with receivers is noise. Thermal noise in our receiver causes noise that we know of as Additive White Gaussian Noise (AWGN). We set the noise power by adjusting the noise voltage value of the channel model. We specify the voltage here instead of power because we need to know the bandwidth of the signal in order to calculate the power properly. We can calculate the noise voltage from a desired power level knowing the other parameters of the simulation.
Another significant problem between two radios is different clocks, which drive the frequency of the radios. The clocks are, for one thing, imperfect, and therefore different between radios. One radio transmits nominally at fc (say, 450 MHz), but the imperfections mean that it is really transmitting at fc + f_delta_1. Meanwhile, the other radio has a different clock and therefore a different offset, f_delta_2. When it's set to fc, the real frequency is at fc + f_delta_2. In the end, the received signal will be f_delta_1 + f_delta_2 off where we think it should be (these deltas may be positive or negative).
Related to the clock problem is the ideal sampling point. We've up-sampled our signal in the transmitter and shaped it, but when receiving it, we need to sample the signal at the original sampling point in order to maximize the signal power and minimize the inter-symbol interference. Like in our stage 1 simulation after adding the second RRC filter, we can see that among the 4 samples per symbol, one of them is at the ideal sampling point of +1, -1, or 0. But again, the two radios are running at different speeds, so the ideal sampling point is an unknown.
The second stage of our simulation allows us to play with these effects of additive noise, frequency offset, and timing offset. When we run this graph we have added a bit of noise (0.2), some frequency offset 0.025), and some timing offset (1.0005) to see the resulting signal.
The constellation plot shows us a cloud of samples, far worse that what we started off with in the last stage. From this received signal, we now have to undo all of these effects.
Receiving a QPSK signal
Polyphase Clock Sync
The Polyphase Clock Sync provides three functions. First, it performs the clock recovery. Second, it provides the receiver matched filter to remove the ISI. Third, it down-samples the signal (reduces the samples per symbol).
The example flowgraph Media:Qpsk_stage3.grc takes the output of the channel model and passes it through a Polyphase Clock Sync block. This block is setup with 32 filters and a loop bandwidth of 2pi/100. The block also takes in a value for the expected samples per symbol.
When running this script, we see the constellation is still a little noisy as a result of the ISI after the 32 filters, but is quickly absorbed by noise once we adjust the channel Noise Voltage setting to be more than 0.
Multipath results from that fact that in most communication environments, we don't have a single path for the signal to travel from the transmitter to the receiver. Like the drawing below shows, any time there is an object that is reflective to the signal, a new path can be established between the two nodes. Surfaces like buildings, signs, trees, people, etc. can all produce signal reflections. Each of these reflective paths will show up at the receiver at different times based on the length of the path. Summing these together at the receiver causes distortions, both constructively and destructively.
The Adaptive Algorithm has a CMA algorithm type, or Constant Modulus Algorithm. It is a blind equalizer, but it only works on signals that have a constant amplitude, or modulus. This means that digital signals like QPSK are good candidates since they have points only on the unit circle. The Media:Qpsk_stage4.grc flowgraph illustrates this point.
We can watch the CMA algorithm converge. Note, too, that since we have both a clock sync and equalizer block, they are converging independently, but the one stage will affect the next stage. So there is some interaction going on here while both are locking on to the signal. In the end, though, we can see the effect of the time-locked multipath signal before and after the equalizer. Before the equalizer, we have a very ugly signal, even without noise. The equalizer nicely figures out how to invert and cancel out this channel so that we have a nice, clean signal again. We can also see the channel itself and how it flattens out nicely after the equalizer.
Phase and Frequency Correction
Given that we've equalized the channel, we still have a problem of phase and frequency offset. Equalizers tend not to adapt quickly, and so a frequency offset easily can be beyond the ability of the equalizer to keep up. Also, if we're just running the CMA equalizer, all it cares about is converging to the unit circle. It has no knowledge of the constellation, so when it locks, it will lock at any given phase. We now need to correct for any phase offset as well as any frequency offset.
Two things about this stage. First, we'll use a second order loop so that we can track both phase and frequency (which is the derivative of the phase) over time. Second, the type of recovery we'll deal with here assumes that we are doing fine frequency correction. So we must be sure that we are already within a decent range of the ideal frequency. If we are too far away, our loop here won't converge and we'll continue to spin. There are ways to do coarse frequency correction, such as the FLL_Band-Edge block, but we won't get getting into those here.
For this task, we're going to use the Costas Loop in example Media:Qpsk_stage5.grc. The Costas Loop block can synchronize BPSK, QPSK, and 8PSK. Like all of our others, it uses a second order loop and is therefore defined with a loop bandwidth parameter. The other thing it needs to know is the order of the PSK modulation, so 2 for BPSK, 4 for QPSK, and 8 for 8PSK.
After the equalizer, the symbols are all on the unit circle, but rotating due to the frequency offset. At the output of the Costas loop block, we can see the locked constellation like we started with (plus the extra noise).
Media:Qpsk_stage6.grc contains our final flowgraph to decode the signal. First, we insert a Constellation Decoder after the Costas loop, but our work is not quite done. At this point, we get our symbols from 0 to 3 because this is the size of our alphabet in a QPSK scheme. But, of those 0-3 symbols, how do we know for sure that we have the same mapping of symbols to constellation points that we did when we transmitted? Notice in our discussion above that nothing we did had any knowledge of the transmitted symbol-to-constellation mapping, which means we might have an ambiguity of 90 degrees in the constellation. Luckily, we avoided this problem by transmitting differential symbols. We didn't actually transmit the constellation itself, we transmitted the difference between symbols of the constellation by setting the Differential setting in the Constellation Modulator block to True.
The flowgraph uses the Differential Decoder to translate the differential coded symbols back to their original symbols due to the phase transitions, not the absolute phase itself. But even out of here, our symbols are not exactly right. This is the hardest part about demodulation. In the synchronization steps, we had basic physics and math on our side. Now, though, we have to interpret some symbol based on what someone else said it was. Basically we just have to know this mapping. And luckily we do, so we use the Map block to convert the symbols from the differential decoder to the original symbols we transmitted. At this point, we now have the original symbols from 0-3, so lets unpack those 2 bits per symbol into bits using the unpack bits block. Now we have the original bit stream of data!
But how do we know that it's the original bit stream? We'll compare the received stream to the input stream, which we can do because this is a simulation and we have access to the transmitted data. But of course, the transmitter produced packed bits, so we again use the unpack bit block to unpack from 8-bits per byte to 1-bit per byte. We then convert these streams to floating point values of 0.0 and 1.0 simply because our time sinks only accept float and complex values. Comparing these two directly would show us... nothing. Why? Because the receiver chain has many blocks and filters that delay the signal, so the received signal is some number of bits behind. To compensate, we have to delay the transmit bits by the same amount using the Delay block. You can then adjust the delay to find the correct value and see how the bits synchronize. Note: wait a few seconds after each change of the delay value. Hint: the correct value is 58.
Using the Symbol Sync block
The Polyphase_Clock_Sync block will be deprecated in a future GNU Radio version (after v220.127.116.11). The replacement for it is a Symbol_Sync block. There is a good explanation of how the Symbol Sync block works in the block documentation, including a link to a GRCon17 presentation about it.
The stage6 source code using the Symbol Sync block is here. The flowgraph is shown below.