Quadrature Amplitude Modulation in MATLAB
In this post I am hoping to provide an explanation of Quadrature Amplitude Modulation and work through an implementation using MATLAB. I'll attempt to make comparisons between an electrical implementation and specific chunks of code. To be completely specific, the code below implements QAM 256, which works nicely with ASCII.
Quadrature Amplitude Modulation (QAM) is a modulation technique which allows transmission and reception of two separate signals. These two signals each have their own carrier of the same frequency, but are out of phase from one another by 90 degrees. These two carriers are known as I and Q, for in-phase and quadrature. The modulation used for our original two signals is not actually of importance to QAM - we could use binary phase shift keying, frequency modulation, amplitude modulation, or any other kind of modulation you can dream of.
Figure 1 shows a simple block diagram of the modulation process. As input it accepts a carrier signal, and two signals, A and B. The carrier frequency is not particularly important, but it must be fast enough to provide a good number of samples for signals A and B. The carrier is split into two paths, one of which is shifted by 90 degrees.
For now, let's begin by setting up our environment. We'll begin by setting a carrier frequency at 1KHz - this will be in audible range so we can play it back and hear what it sounds like. We set a sample rate of 16 times the carrier frequency to ensure we are taking enough samples per wave. Next, we define our baud rate. This will be the number of state transitions per second modulated onto the carrier.
F = 1e3; % carrier frequency fs = 16 * F; % samples rate - 16 samples per wave. baud = 50; % baud rate of data
Next we'll start generating our signals. We define a phrase of arbitrary length and convert this to binary. We take the high and low order bits and store them in
phraseQ. As these are four bits each, we have a 16 possible states, and so centre this data by subtracting 8 after converting back to a decimal value. Once we know the length of this data, we can figure out how long it'll take to transmit, based on the baud rate, and then define a carrier signal
c of the correct length for this transmission.
phrase = 'Having fun in MATLAB. '; phrase_bin = dec2bin(phrase, 8); phraseI = phrase_bin(:,1:4); phraseQ = phrase_bin(:,5:8); symsI = bin2dec(phraseI)-8; symsQ = bin2dec(phraseQ)-8; % generate the base for a carrier based on the length of the phrase. time = numel(symsI) / baud; c = 2*pi*F*(0:(1/fs):time+(1/fs));
Let's explain how this data will look when phrase is set to something short, like "Hello",
Looks pretty ordinary and we can just think of
symsQ as analogue voltage levels for Signal A and Signal B. I'd plot them but they'll look pretty boring, so let's save that until later. Since the length of our carrier
c is fundamentally based on the length of our string, we can cheat and stretch out our two signals using nearest neighbour interpolation. For this, I'm going to use imresize. Yes, yes, there are 1d linear interpolation functions available, but give me one good reason why I need to look up how to use them for nearest neighbour interpolation ;)
symsI = imresize(symsI, [numel(c), 1]', 'nearest')'; symsQ = imresize(symsQ, [numel(c), 1]', 'nearest')';
Okay, now we can take a look at these two signals in Figure 2.
If we refer back to Figure 1, we can see that our two signals are "mixed" with the I and Q carriers. What does mixing mean? Electrically this can be implemented in a variety of ways, for example by using a pair of transformers, or even a single transistor. Numerically though, what we are essentially doing is multiplying the carrier by our signal.
ca = sin(c) .* (symsI/50); cb = cos(c) .* (symsQ/50);
We implement our phase shifting by passing the carrier vector
c through either
cos. Additionally, we apply some attenuation to our analogue signals to prevent them from making our carrier too large - we'll do the inverse during the demodulation. Figure 3 shows us what these two signals looks like. This figure also shows why it's important to have a carrier which is much higher than our baud rate. We need to provide a suitable "envelope" for our two signals.
Finally, we can combine these two separate streams by simply adding them together. Before we do that though, let's try and understand what happens when we add these two carriers together. Figure 4 shows
sin in blue,
cos in red, and the sum of the two in orange. As you can see, we increase the amplitude slightly when
sin is 90 degrees ahead of
cos. Our actual combined signals are shown in Figure 5.
And that's it! For modulation, it's really that simple. By doing it this way, we are able to transmit a single character with each transition in I and Q. This may be easier to understand a little later when I introduce constellation diagrams. For now, let's take a listen to what this sounds like. To make it a little easier to hear, I've repeated the string eight times.
Right, let's move onto the receiving end. We'll begin by adding some noise to our signal so it has a Signal to Noise Radio (SNR) of 40dB. Matlab makes this very easy!
rx = awgn(tx, 40);
Demodulation is essentially the inverse of the modulation step. We use the same carriers, one out of phrase from the other, and multiply it by our received signal. Honestly, I can't remember why we need to scale this by 2.
rx_a = 2 * sin(c) .* rx; rx_b = 2 * cos(c) .* rx;
This will look almost identical to what we had in Figure 3, but remember that was not our original signal because it had already been mixed with the carrier. To remove the carrier, and revert back to what we had in Figure 2, we need to use a low pass filter. This essentially chops off all frequencies above our baud rate. Electrically, this would just be a resistor and a capacitor. We are multiplying by 50 because we divided by 50 earlier. This number is fairly arbitrary.
rx_a = lowpass(rx_a, baud, fs) * 50; rx_b = lowpass(rx_b, baud, fs) * 50;
Figure 6 shows a plot of
rx_b. These are our symbols. What we have essentially done is by modulating the two signals together (high and low order bits) is define a vector pointing to a particular symbol. ASCII 0 would be 0,0 on this chart. The constellation diagram for the full ASCII table using smaller points, can be seen in Figure 7. Doesn't it look awesome?!?!
Finally, now we have removed the carrier using a low pass filter, we need to match these voltage levels back up to our original 16 per signal. We also want to define some points which sit in between each transition, so we can retrieve our original message. I feel like I am cheating slightly by using k-nearest neighbour.
rx_a2 = knnsearch((0:15)', rx_a'+8) - 1; rx_b2 = knnsearch((0:15)', rx_b'+8) - 1; points = round((fs/baud/2):(fs/baud):(fs*time)); cc = [dec2bin(rx_a2(points)) dec2bin(rx_b2(points))]; s = char(bin2dec(cc))'
So, here we go. We sent a string very efficiently at 50 baud.
>> qam s = 'Having fun in MATLAB. ' >>
Hopefully I didn't make too many mistakes while trying to explain this. I've tried to approach it from a very practical perspective. Hope you found it interesting! :-)
<2020-05-03 Sun 16:49> I wanted to play around with a phase difference between the transmitter and receiver. Below is an animation showing what happens to the constellation as the local oscillator phase differs to the phase of the carrier.