Chris Thompson - AC2CZ - Amateur Radio Station

Software Defined Radio for the Radio Amateur

Prev Tutorial | Index | Next Tutorial

2018-Mar-7 - Tutorial 6: IQ Mixing

The files for this tutorial are available on github.com/ac2cz/SDR

It was harder to analyze our Complex Oscillator than expected, but the techniques we learnt will be beneficial each time we want to display or analyze our signals. In fact we will implement the Blackman Window in our FFTPanel during this tutorial and we will use similar test signals with noise to make sure our mixing and demodulation are working.

If you have built homebrew radios with transistors or ICs and tried to understand how mixing really works, then you will know that nothing about mixing signals is easy to understand. Fortunately the actual mixing is relatively easy, in hardware and software. So we can implement and use it even if we can't do the math. If you want to understand how it works I suggest Understanding Digital Signal Processing by Richard Lyons. I have it on my kindle and refer to it regularly. It is another great book, though at a text book price. For hardware, I suggest Experimental Methods in RF Design which is a landmark book on radio design that should be in every amateur electronics lab.

Our mixer multiplies our complex oscillator with the complex IQ signal from our SDR hardware. If you remember how to multiply complex numbers it is easy, if you do not, then it works like this:

[a + jb] * [c + jd] = ac + jad + jcb - bd

Remember that "j" is the square root of -1, so jb * jd is -bd.

Our signal after mixing therefore still has a real (I) and complex (Q) component. The complex component is the part that still has a "j" multiplied by it in the formula above. We calculate them as follows using the formula above, where i/q are the input signal and i_osc/q_osc are the signal from our oscillator. Note that in our Java code we do not store the "j" operator in any way, but it is part of the calculation and results in the minus sign in the equations below:

I = i * osc_i - q*osc_q
Q = i * osc_q + q*osc_i

The Mixer

Our mixer is therefore just two lines of code, implementing the two equations above. IQbuffer holds our input i/q values in alternating samples and c is a complex number returned from our oscillator. We put the output in a new buffer called IQbuffer2, so we can graph it alongside the original input.


	IQbuffer2[2*d] = IQbuffer[2*d]*c.geti() + IQbuffer[2*d+1]*c.getq();
	IQbuffer2[2*d+1] = IQbuffer[2*d+1]*c.geti() - IQbuffer[2*d]*c.getq();

SDR MainWindow Layout

Let's add some code to test this. Our MainWindow class holds our FFTPanel. Let's give it two FFTPanels and an output audio panel so we can see what is going on. One FFTPanel will show the "IF" which will be our spectrum after the mixing. That requires us to know a bit about Layout Managers. Follow the link if you want to learn more. If your're already an expert, or just here for the DSP code, then keep reading instead.

We leverage the underlying BorderLayout that the JFrame has. We put our IQ FFT in the North. Our IF will go in the Center. Then when we stretch the window horizontally the FFT displays stretch.

We add a new method to receive the data and pass it to the new FFTPanel.


	public class TestWindow extends JFrame {

	FFTPanel rfFftPanel;
	FFTPanel rfFftPanel2;

	public TestWindow(String title, int sampleRate, int samples) {
		super(title);
		this.setDefaultCloseOperation(EXIT_ON_CLOSE);
		setBounds(100, 100, 900, 600);
		
		rfFftPanel = new FFTPanel(sampleRate, 7200000, samples);
		rfFftPanel.setPreferredSize(new Dimension(1200,300));
		add(rfFftPanel, BorderLayout.NORTH);
		
		rfFftPanel2 = new FFTPanel(sampleRate, 7200000, samples);
		rfFftPanel2.setPreferredSize(new Dimension(600, 200));
		add(rfFftPanel2, BorderLayout.CENTER);
	}

	public void setRfData(double[] data) {
		rfFftPanel.setData(data);
	}
	public void setRfData2(double[] data) {
		rfFftPanel2.setData(data);
	}
}

Testing the Mixer

We just need a simple main() method to call our mixer and display the results in the two panels of our updated MainWindow. This builds on the main() method we used in Tutorial 4 when we played the audio through the speaker and implemented some filtering.

We add a complex oscillator and add the mixer lines to the main loop. We store the mixed result in a new buffer. As before we take just one channel into the audio buffer, which we filter. We are not running an FFT on the audio, so we don't need to set half the values to zero. This time we add the I and Q values together because that gives a slightly better result. Try it with just I and with the values added together when you test it.


		...
		ComplexOscillator localOsc = new ComplexOscillator(sampleRate, -20000);

		double[] audioBuffer = new double[FFT_LENGTH]; // just one mono channel
		double[] IQbuffer2 = new double[FFT_LENGTH*2];
		boolean readingData = true;
		
		while (readingData) {
			double[] IQbuffer = soundCard.read();
			if (IQbuffer != null) {
				for (int d=0; d < IQbuffer.length/2; d++) {					
					//NCO Frequency
					Complex c = localOsc.nextSample();
					c.normalize();
					// Mix 
					IQbuffer2[2*d] = IQbuffer[2*d]*c.geti() + IQbuffer[2*d+1]*c.getq();
					IQbuffer2[2*d+1] = IQbuffer[2*d+1]*c.geti() - IQbuffer[2*d]*c.getq();

					double audio = IQbuffer2[2*d] + IQbuffer2[2*d+1] 

					double fil = audioLowPass.filter(audio);
					audioBuffer[d] = fil;
				}
				sink.write(audioBuffer);

				window.setRfData(IQbuffer);
				window.setRfData2(IQbuffer2);
				window.setVisible(true); // causes window to be redrawn
			} else 
				readingData = false;
		} 
		...

I tested this with my ECARS recording. I call it that because the ECARS net control station is sitting just to the left of zero Hz in the center. If you run this with the ComplexOscillator set to zero Hz you will hear that net control station. If you put in a negative frequency value (-21000 is a good example, because there is a signal there) you should clearly see the IF rotated to the right. It will rotate left if you put in a positive frequency, pulling the signal at that positive frequency to zero Hz.

When you put in a negative offset then any signals that disappear off the right of the FFT now appear in the negative frequencies on the left. Remember in an earlier tutorial I said that the frequency spectrum repeats. The signals that come in from the left are a copy of the spectrum that have now been shifted into our FFT range.

A better FFT Panel

What we really want to be able to do is click on a signal and listen to it. Then we can sit back and explore the band. So let's update our FFTPanel to do that. It would also be nice if we could run either the Real FFT or the Complex FFT. We will use the Real FFT for final audio and the Complex FFT for IQ signals. You can see the complete FFTPanel.java class on github. I will just describe the changes here and any of the theory you need to understand what we are doing.

First we make some changes to the constructor. We pass in a name, so we know which panel is which once we have drawn them. We also pass in a boolean to indicate if this should be the complex FFT or not. Those are stored in class variables. We also add a MouseListener and set it to "this" which means this class. Finally we add a small method to set the FFT length. We may need to use this in our SDR if the length changes at run time. Notice how the calculation for binBandwith casts both the values to a double before doing the calculation. This is a subtle but easy to make Java bug. If we don't do this then Java will do integer division and then cast the result to a double for us. That is not what we want. We want the precise value. fftLength and sampleRate are integers, so we cast them to double first. Without this our binBandwidth is just slightly off and we don't tune to quite the right frequency later. In future we will cast any integer value that we use in a floating point calculation, just to be sure.


	public FFTPanel(String name, int rate, long freq, int length, boolean complexOrReal) {
		sampleRate = rate;
		centerFrequency = freq;
		fftLength = length;
		this.name = name;
		setFFTLength(length);
		addMouseListener(this);
		this.complexOrReal = complexOrReal;
	}

	public void setFFTLength(int len) {
		this.fftLength = len;
		psdBuffer = new double[fftLength+1];
		fft = new DoubleFFT_1D(fftLength);
		binBandwidth = (double)sampleRate/(double)fftLength;
	}

If we add "this" as a MouseListener, then we need to change the class definition slightly. It has to be a MouseListener. Otherwise we get a compile error. We modify the first line of the class with the statement "implements MouseListener".


public class FFTPanel extends JPanel implements MouseListener {

In Java, when we say a class "implements" something we mean it implements an interface. An interface is the way that Java defines APIs between classes. In this case the interface requires us to support four methods. If we implement those four methods then we are a MouseListener because we can handle any of the events associated with listening to the mouse.

If you are using an IDE like Eclipse it will automatically add the empty methods. Otherwise you can lookup the MouseListener interface or cut/paste them from below. We then populate the method that is called whenever the mouse is clicked on our component. The others are left blank.

In "mouseClicked" we get the X co-ordinate from the MouseEvent that is passed to us. This is the horizontal pixel that was clicked on our JPanel. We then turn it into the offset frequency we want to use to tune our NCO.

Our pixel to frequency algorithm first scales the pixel position to the length of the FFT based on the width of the window. This is just the inverse of the code to plot a pixel based on an FFT bin. That calculates the bin that we clicked on. This is stored in "selectedBin", which we add as a class variable. We will use that to show the current tuning point so it needs to exist outside of this method. We then call a method, binToFrequency(), which we need to write. This converts the bin into the frequency it represents. Our tuning offset needs to be in the opposite direction to support our mixer because it will shift the position we click on to 0Hz.


	public void mouseClicked(MouseEvent e) {
		int x=e.getX();
	    x = x - BORDER;
		int selection = LineChart.getRatioPosition(0, getWidth()-BORDER*2, x, fftLength );
		if (selection >= fftLength/2) 
			selectedBin = selection - fftLength/2;
		else
			selectedBin = selection + fftLength/2;
		System.out.println(x+" is fft bin "+selectedBin);//these co-ords are relative to the component
		System.out.println("Tuned to: " + (-1*binToFrequency(selectedBin)));
		if (nco != null)
			nco.setFrequency(-1*binToFrequency(selectedBin)); // flip the sign
	}

	public void mouseEntered(MouseEvent arg0) {}
	public void mouseExited(MouseEvent arg0) {}
	public void mousePressed(MouseEvent arg0) {}
	public void mouseReleased(MouseEvent arg0) {}
The method to return the selected frequency is like this:


	private long binToFrequency(int bin) {
		long freq = 0;
		if (bin < fftLength/2) {
			freq = (long)(bin*binBandwidth);
		} else {
			freq = (long)( -1* (fftLength-bin)*binBandwidth);
		}
		return freq;
	}

We also add some utility functions to set and read the frequencies and bins. You can see those in the full class.

When we run the FFT in the setData method, we check the "complex" boolean and perform the appropriate FFT, with simple logic like below.


		if (complexOrReal == COMPLEX) {
			fft.complexForward(buffer);
		} else
			fft.realForward(buffer);
		...

We sprinkle similar logic throughout the paintComponent() method to show only half the bins for the Real FFT, although that is not required. There are some other changes to the paint routine to display the name and draw the tuning point. You can see them in FFTPanel.java.

Final Testing

Our main() method needs just one line added. It goes straight after we create the ComplexOscillator. This is so the GUI can call back to the NCO and change the frequency.


window.setNco(localOsc);

Our MainWindow needs the calls to FFTPanel to be updated to include the name of each panel and a boolean to indicate if it is the Complex or Real FFT that we run. Note that the FFT Length is half for the real FFT. I'll let you update the others, but as an example here is the new audio Panel, which we put in the EAST part of the BorderLayout. Look at the full class on github if you get stuck.


        audioFftPanel = new FFTPanel("Audio", sampleRate, 0, samples/2, FFTPanel.REAL);
		audioFftPanel.setPreferredSize(new Dimension(300, 200));
		add(audioFftPanel, BorderLayout.EAST);

If everything is working, here is our new SDR display:

I sat down in the shack early one March evening to run this. I set the center frequency to 7200kHz using another SDR. It was dark outside and the 40 meter Amateur Radio band was alive with strong signals. I clicked just to the right of the signal at about 7176kHz and there was November Echo Two Quebec calling CQ. Loud and clear. That is fantastic!

I could click to the right of any signal and there it was. If I clicked just to the left of a signal I could hear it too, but it was garbled. It sounds like I had the rig set to the wrong sideband by mistake. Which is true. We are then listening to the upper sideband but it is filled with a lower sideband signal. This is still a Double Sideband Receiver. In the next tutorial we will see how that get's sorted out, but it will need a bit more DSP theory, some analysis, and a new tool.

Also, if you play back my ECARS recording you can see one or two AM transmission at about +36kHz. We will also have a go at demodulating those :)

Prev Tutorial | Index | Next Tutorial


Enter Comments Here:

Name:

HTML Markup can be included in the comments

Answer this question to help prevent spam (one word, not case sensitive):
The third planet from the sun is called what?


Comments on this post

On: 11/01/20 10:10 Claude KE6DXJ said:
I've been working on this tutorial and have come up with a question. My version of the software and the IQSdrTest version agree so far with each other for RF and IF processing. My question involves the audio signal processing and the "readonly" flag check in FFTPanel class "setData" method. The flag is initialized as "false" and does not appear to be set "true" within IQSdrTest project. Does this affect the calculation of Power Spectral Density for the audio portion of the signal? If the audio portion of the signal makes use of the realForward FFT, should the Power Spectral Density calculation also make use of only the real portion of the FFT? Have I missed the point again as I did with the average PSD calculations. As an aside, it appears that my decision to use javaFX rather than swing, may have impacted the realtime response when generating the actual audio playback. Plus javaFX may have come come to a deadend with regard to support.
On: 11/05/20 13:13 Chris G0KLA said:
Hi Claude, great to see you have made it this far. Looking back at my code I could have made this clearer! So thanks for the comment. As a summary, you can ignore those flags. They are not used.

In case you are interested, I added these to plot the FFT results in three different ways. Those flags are public and you can set them from outside the panel if you want, say with a button on the GUI. They will change the plot to show just the real part of the FFT or just the imaginary part. Feel free to set one of them to true and see what gets plotted. It is very different from the PSD.

On: 11/05/20 13:13 Chris G0KLA said:
On JavaFX, it would be a shame if it dies. But don't lose heart. It is still supposed to be the replacement for Swing and is part of the OpenJDK that Oracle has released. So it should be around for some time to come.
On: 03/19/22 15:15Dario Greggio said:
thank you very much for these tutorials about SDR, Chris! You explain quite well and in a nice manner :)

I've been fond of SDR for decades now, and I came out with its overall idea even before I saw that "SDR" actually existed... 2-3 years ago I implemented my own approach, using only one MCU (PIC32) and its 4Msamples ADC, so that I could receive and decode AM band - you can see it here
https://www.youtube.com/watch?v=NyVVpvgAsnA

I was missing the "mixing" part (which should've been kinda obvious, since I've been working with analogic radios for many years) and that's where you're helping!! Dario

On: 03/19/22 19:19 Chris G0KLA said:
Dario, thanks for the kind comments. Great to see your experimentation with the PIC32. I never thought of doing that. I'm glad the tutorials have been helpful.
On: 05/30/22 22:22 said:

Copyright 2001-2021 Chris Thompson
Send me an email