Siglent SDG2042X + IMD Measurement

SDG2042X

I recently acquired a relatively inexpensive dual channel signal generator: the Siglent SDG2042X. I’d been looking for a dual channel signal generator for a while to allow IMD measurements and this one was particularly attractive for a couple of reasons:

  • DAC sample rate of 1.2GSPS meant that any image frequencies on the output would be a long way from the signals of interest. This was the main attraction to be honest.
  • Easily software upgradeable to 120MHz max frequency using instructions here: https://www.eevblog.com/forum/testgear/siglent-sdg2042x-hack-door-closed/msg2409867/#msg2409867 to 120MHz version (at least on the verison I bought in Sepetember 2021)
  • Lots of other options including modulation / arbitrary waveform generation that I’ve not even started to have a look at yet.

First thing I did was to apply the software “upgrade” and confirmed I could get 120MHz fmax. I then added drivers for this new piece of kit to my Python suite (https://github.com/M0WUT/Automated-Testing) for driving all my bits of test equipment. I then swept the power from -50dBm to 0dBm with the output of the signal generator fed straight into my spectrum analyser. Note that I had not calibrated out the cable loss or the DC block I had between the two:

Initial validation setup for SDG2042X

This gave the following very promising results:

Output power
Measured error in output power

This certainly seems more than good enough for testing at home. IMD3 grows 3x faster (in dB) than fundamental power so a +- 0.3dB error in fundamental power equates to roughly +-1dB in measurement of IIP3. Depending on whether the measured errors are random or inherent to the instrument, when a straight line is fitted during the IMD3 calculation, this error may reduce significantly.

SDG2042X performance

First, it was important to check the IMD performance of the signal generator on it’s own. If it’s performance is significantly worse that that of the DUT, then the measurement is almost worthless. Please forgive the phone images of spectrum analyser screen – it’s not modern enough for any of that fancy USB stuff and I don’t have a floppy drive to hand!

The above screen shows 3 measurements:

  • Yellow: Signal generator output set to 0dBm, 120MHz
  • Blue (barely visible behind pink trace): Signal generator output disabled
  • Pink: Spectrum analyser noise floor when disconnected from the signal generator

It’s reassuring that the blue and pink traces align, this suggests that the leakage of the signal generator when the output is disabled is less than the noise floor of the spectrum analyser. This is easy to achieve in modern DAC based signal generators but couldn’t always be taken for granted in older units where the analog oscillator was always running and an RF switch was used to disable the output. Three tones are visible above the noise floor:

ToneFrequency (MHz)Power (dBm)Power (dBc)
1120-0.070
2240-56.7-56.7
3360-56.6-56.6

It’s a good idea to work out whether these are harmonics generated within the signal generator or IMD products within the spectrum analyser. To measure this, a 10dB attenuator was placed at the input of the spectrum analyser. If the harmonics are generated by the signal generator, then they will drop by 10dB, if they are IMD products within the spectrum analyser, they’ll drop by 30dB as IMD scales with the cube of the input power. Repeating with this attenuator installed gave the following:

ToneFrequency (MHz)Power (dBm)Power (dBc)
1120-9.850
2240-64.9-55
3360-65.4-55.5
Measured tone power with 10dB attenuator installed

The tone power relative to the carrier stayed roughly constant so this suggests that the tones are being generated within the signal generator. However, they are low enough to not be considered an issue for this testing.

IMD Measurement Board

To measure IMD, we inject two closely spaced tones and measure the intermod products generated by our DUT – I intend to write an intro to IMD which will be linked here but haven’t got around to it yet, sorry. The schematic is very simple:

IMD Measurement board

The circuit is incredibly simple: a DC blocking capacitor, spaces for a filter and attenuator should I feel it’s needed, then a Minicircuits combiner and 20dB directional coupler. The use of an expensive (£9) coupler rather than resistively combining is resitive combination always wastes 3dB of power in the combiner and I didn’t want that. The directional coupler is allowing the output signal to be measured after combination – this is much easier to calibrate the relationship between the signal at J4 and J3 rather than all the cables and components upstream of the combiner plus the generator itself.

IMD combiner

I initially didn’t fit any filtering or attenuation and measured the performance as is.

Blue = SIG 1 to Output, Green = SIG 2 to Output

Both channels are well matched, insertion loss is well aligned with the datasheet values for the components used. Checking the isolation also looked promising up to 500MHz:

Isolation between two input ports

Measuring the difference between the sample port and the output is not super simple. The coupler is directional, we can’t just inject signal into the output port and measure the sample port as that will give the wrong answer due to the directionality of the coupler. Instead, we have to measure the input -> output, and the input -> sample port and subtract the two to convert what is measured at the sample port to what that means is at the input and then convert that to what signal level that produces at the output. We’ve already measured input -> output, so measuring input -> sample port gives:

Blue = SIG 1 to Sample, Green = SIG 2 to Sample

It’s important here that the two traces overlap closely in the operating region – once a signal has made it to the sample port, we don’t know which input it came from so life is much easier if we don’t have to care. There would be a way around this by comparing the sample signal to what’s being generated currently but this is a problem I’m glad to avoid. Calculating the conversion between level at the sample port and the output port gives the following:

Sample port to Output port conversion
Sample port to Output port conversion (0-250MHz)

In practical terms, it’s probably reasonable to just add 19.8dB as a constant, the error introduced by this is small enough compared to the rest of the test setup. It’s good that both channels perform identically, for the reasons discussed above.

I received some excellent advice from Carlos EB4FBZ to watch out for IMD generated by ferrite combiners / couplers (https://twitter.com/eb4fbz/status/1448700376245485572) as I have done here so we shall measure the performance of the new board in isolation first to see whether this is an issue. First a quick indicative measurement, I wanted this be a good distance away from fmax to prevent any LPF filtering in the ouput having a significant effect. Following plot is two tones, at 50MHz +- 0.5MHz with both tones set to 0dBm on the signal generator:

Output of IMD measurement board
ToneFrequency (MHz)Power (dBm)Power (dBc)
149.5-3.260
250.5-3.310
348.5-70.2-66.9
451.5-74.5-71.2
550-64.9-61.6
Measured two-tone output of IMD measurement board

At the point that these become limiting in any measurement I’m doing at home, I think I’ll be very happy.

Software

Writing an IMD measurement test is pretty straightforward – combine signals, feed into DUT and measure power at the expected frequencies for IMD3 / 5 etc. I initially wrote a test script that didn’t worry about sampling the signal and had calibration constants fed into it to convert from signal generator setting to power at DUT – these were based on the measurements taken above. Nearly all the effort was in the best-fit line generation and creation of the Excel workbook. While it’s generally much easier to plot using matplotlib from Python – I like Excel as I can edit the graph afterwards if my code fails to generate the graph as I’d like it or I want a different view of the results.

Given most receivers have an OIP3 of ~0dBm and high performance RF components (excluding high power PAs) have an OIP3 of ~30dBm, I’m very happy with that. I’ll try to get some amplifiers to test another day. If you have any thoughts, please comment here or message me on Twitter @m0wut.

Phase Accumulator / CORDIC part 1

Disclaimer: I am not claiming any competency in signal processing / Verilog. This is just a documentation of what I’ve done (primarily for myself when I’ve forgotten what I did in 6 months!) If it helps you, great but I’m not brave enough to claim this is a tutorial.

Demodulating an AM signal can be done by multiplication by a cosine wave of the same frequency as the AM signal’s carrier frequency. (If this statement is not obvious, I suggest reading my intro to AM page link TODO before proceeding)

However, performing this the naive way by calculating the cosine of ωt and performing a floating-point multiplication for every value of t (bearing in mind a new sample comes in every clock cycle) is almost impossible or is, at best, going to be stupidly resource intensive. As such, one way used in SDRs is to use a phase accumulator to generate an angle θ which is equal to ωt and using an algorithm called CORDIC to calculate RF*cos(θ).

Phase Accumulator

ωt is just an angle that changes every timestep by an amount such that it performs f (=ω/2π) complete revolutions per second. In a discrete time system, t can only take values of n/fs where n is some integer and fs is the sampling frequency and t will increment by 1/fs in each timestep. This means that the angle (which I can going to call θ from now on) will increment by ω/fs = 2π*f/fs. This means that we could calculate θ at time t by knowing the value of θ at the previous timestep (t-1) and adding 2π*f/fs. The only reason for the 2π to be present is that the standard cosine function in most programming languages expects the angle to be specified in radians which have 2π radians per revolution. We can actually use any system we want to divide a revolution up, lets say we divide a circle into x steps, θ will increment by x * f / fs in each timestep. The convention in most mathmatics is to use radians and divide a circle into 2π. Radians are preferably mathematically as they have the simplest behaviour when differentiated / integrated and a couple of other nice properties. But, given that we are not differentiating / using any functions that expect angles in radians, we can define an arbitrary number of steps in a revolution as long as we are consistent. This is particularly important in the case of Verilog which, as far as I know, can only handle integers. Dividing a circle into 6 (closest integer to 2π) doesn’t seem like a recipe for accuracy to me! I initially chose to use 32 bit angles (so divided a complete revolution into just over 4 billion sections) at which point our angle increments by 2^32*f/fs on every clock cycle. The Verilog module for this is fairly simple, it has a 32 bit register (o_phaseAngle) which increments by some constant amount (i_phaseDelta) every clock edge. The reason for taking the increment as an input rather than calculating it from the desired frequency and the sampling frequency is that I wasn’t sure if asking for a multiplication and division by such large numbers in a single clock cycle was a great idea. Given I have an ARM core attached to the FPGA fabric, I think I’ll let that handle the maths. If someone understands this better than me, I’d be interested in your thoughts.


module phase_accumulator(
    input wire i_adcClock,
    input wire [31:0] i_phaseDelta,
    input wire i_resetn,
    output reg [31:0] o_phaseAngle
);

always @(posedge i_adcClock or negedge i_resetn) begin
    if(~i_resetn) begin
        o_phaseAngle <= 0;
    end else begin
        o_phaseAngle <= o_phaseAngle + i_phaseDelta;
    end
end

endmodule

Setting f/fs = 1/80 gave a phase increment of 53,687,091 (0x3333333). Simulating this with a 2ns clock period (not at all realistic, just the defaults) shows the angle increase and then overflow (as one full cycle was completed) after 160ns or 80 clock cyles. Result!

CORDIC Basics

For a excellent video on CORDIC that explains it much better than I can, check this video on Youtube https://youtu.be/TJe4RUYiOIg but in essence:

If we feed 0 in as y0, our RF signal as x0 and have θ=ωt (as done by the phase accumulator) , our output is [RF * cos(ωt), RF * sin(ωt)], the first element of which is our demodulated signal!

Unfortunately, calculating tan(θ) is still tricky to do for all values of θ. So what if we restrict tan(θ) to be a negative power of 2 (1, half, quarter, 1/8, 1/16 etc)? Computers can easily divide by 2 for an integer number – it’s equivalent to a bit-shift to the right. This means if we constrain tan(θ) to be 2-i where i is 0 or a positive integer, our rotation has become:

Ignoring K as it is just a constant gain for each value of i, we now have an addition/subtraction and some bit-shifts. Much better! This calculation also works for a clockwise rotation, just with some sign changes. The only problem is “how do we calculate this for an angle which does not satisfy the magic requirement of tan(θ) = 2-i?” Well, there’s nothing stopping us combining multiple rotations that are easy to calculate until we get close enough to the answer we want. As i increases, θ decreases so we can increase the number of iterations of this algorithm, tweaking our approximation by an increasingly fine amount, until our series of rotations adequately represent the angle we want. Here are the first 5 angles that satisfy our criteria:

iθ (degrees)
045.0
126.56505117707799
214.036243467926477
37.125016348901798
43.5763343749973515
First angles that satisy the CORDIC criteria

Say we wanted an angle of 13° (angles are defined following the standard convention of x-axis = 0°, positive rotations are counter-clockwise). Our first stage has a starting error (target angle – current angle) of 13° so we need to perform a postive rotation. We rotate positively by the first CORDIC angle (45°), our second stage has a starting error of -32° so we perform a negative rotation of the second CORDIC angle (26.56°), our third stage has a starting error of -5.44° etc. and so the algorithm homes in on the correct answer – shown below. The downside is that it takes has a processing delay of the number of iterations but I think I can live with that. Extending this example to many iterations gives the following:

Error in CORDIC approximation vs iteration

How good is “good enough”?

I now have two parameters that I can play with to adjust my accuracy, how many bits I specify my angle to and how many iterations of the CORDIC I run before deciding it is good enough. If I can’t specify my angle accurately enough, I’ll lose a lot of accuracy just trying to express the angle. e.g. if I only used 2 bits, I could only divide a cicle into 4 – this means expressing angles to the nearest 90° so I have an average of 22.5° degrees error (max error is 45°) that I can’t do anything about before I’ve even started. If I can specify my angle more accurately, I need more iterations of the CORDIC to achieve 0 error. The CORDIC is capable of reaching zero error, given enough iterations, as the angle is expressed using a fixed number of bits. As long as we have enough iterations such that tan(θ) becomes a value of 1 LSB, then we can adjust the rotation to be perfect as far as the CORDIC algorithm sees it. At this point, the remaining error is whatever error was introduced expressing the original angle as a finite number of bits (known as quantisation error). This is shown below, after 14 iterations, the CORDIC algorithm has an error of 0 and the Total error in expressing the angle comes purely from quantising the angle.

I hacked together some Python to plot the average angle error vs number of iterations / number of bits I’d specified the angle to.

The point where the error doesn’t improve for increasing iterations is where the error is entirely due to quantisation noise. I also realised this should be roughly the point where the number of iterations is equal to the number of bits the angle is expressed to. The result of the arctan function is scaled such that a full circle is 232 units.

When i is much greater than angleWidth, θ = 0 as all of the data has been right shifted out of the number! It also holds that the number of iterations can’t exceed the data width. Recalling our expression for the rotation:

If y0 or x0 are shifted more bits then they are wide, the second term on each line will be 0 and the rotation will just apply a scaling factor. I only have a 16 bit wide data bus so plotted error against number of how many bits I used to specify the angle for 16 iterations of the CORDIC algorithm.

RMS error vs angle precision for 16 iterations (due to 16 bit data bus)

This confirmed using the 32 bit angle will work just fine. I don’t know how an RMS error of 0.001 degree affects the signal quality but I’ll work with it. Also not sure if you can pad the data bus to get greater angular resolution (in theory, I should be able to perform more iterations before hitting the quantisation noise limit) but that’s a thought for another day.

Power Supply Hacking – Part 1

I’ve really liked my Tenma 72-10480 power supply which I’ve been using for the last couple of years. Main plus points for me have been:

  • Fairly cheap – currently £60 on CPC
  • Amazing voltage accuracy (especially for the price)- setting the output to 20V still reads 20.00V on my Fluke 87V. Numerous checks over using it have me pretty convinced that the voltage it says is actually what it is putting out. This reassurance is worth a lot when connecting to expensive stuff and is something I don’t get with eBay specials.
  • Current limiting – saved me on more than one occasion!
  • Nice user interface – it’s really simple and “just works™”

The main downside is the obnoxiously loud fan which I’ve been meaning to replace since I bought it.

Recently, I decided that I needed another supply in the shack, I have been running with just 1 for a long time and it’s been slightly limiting on several occasions. Given how much I’ve liked the Tenma, I basically decided to buy the same thing again. This time, I splashed a little bit more cash (£84) and get the programmable version (Tenma 72-2535). Same power supply inside (I suspect) but now has a USB / Serial port on the back to allow the power supply to be configured by a computer. This will be a requirement in a future project to convert a lot of my testing to be handled by a Python script rather than me manually pressing buttons and taking readings. Given everything else in my shack supports remote control, it felt lacking to not be able to adjust power conditions and turn things on/off as required. The fan is still loud (though does pump a lot of air through the unit) and they’ve changed the binding posts from the “sticky-out” (apologies for technical language!) kind that could have a wire directly inserted to insert sockets that are still fine for banana plugs but useless to shove a wire in. Solution long-term is probably just to buy a load of banana leads off eBay and cannibalise as needed.

Old power supply (Left), New power supply (Right)

Given how similar they look, I was hopeful that they had the same hardware inside and making my original PSU programmable was just a case of finding the right header / bit of firmware prodding. I was made more confident by having a working programmable unit as a reference. IF not (assuming nothing gets broken in the process!) I haven’t lost anything.

Hello World

Step 1 was to ensure I was able to communicate with the new PSU before I started breaking anything. I was a bit surprised that Tenma haven’t jumped onto the bandwagon of GPIB, instead offering a normal COM port with a command set that looks supiciously like GPIB but just not quite: *IDN? is still a thing but other than that, they’ve gone a bit rogue. Datasheet on Farnell is here. Anyway, once I’d got over being deafened by the fan preparing for take-off, a few minutes of Python and everything looked to be working:

import serial
PORT = "COM3"

BAUDRATE = 9600
TIMEOUT = 1

with serial.Serial(PORT, BAUDRATE, timeout=TIMEOUT) as ser:

ser.write(b'*IDN?')
print(ser.readline())
resulted in:
b'TENMA 72-2535 V2.1'
The unit beeps when it transitions from local (buttons) to remote (USB) control and takes roughly 5s to return to local control once the serial port has been closed. The buttons are disabled while the unit is under remote control, however the main power button is a physical switch that does allow the unit to be shutdown instantly when the unit is under remote control. This is a definite plus if something goes wrong and your device under test starts smoking.

Don’t turn it on, take it apart!

Usual disclaimer about working on stuff that has mains electricity in it: Don’t work on any electrical device, especially ones containing high voltage, unless you are suitably trained and competent. You do any activity entirely at your own risk and high voltage can be fatal. This is not a guide, I will not document every step I take and I do (surprisingly) know what I’m doing!

Boring stuff aside, I wanted to open up both of these units and see how different they looked inside. I did have the slight extra complication of one unit being 5 years older than the other as well as being different models so wasn’t really sure what to expect.

Overall, both look incredibly similar. What is very interesting is that the old unit has a unpopulated 4 pin header just above (what I’m guessing) is the microcontroller. In the programmable unit, this header has a cable going to the rear USB port with a small adapter board. Ooh, this just got exciting! I’m also going to stick my neck out and guess its running UART. If this is the case, a cheap USB-UART adapter should allow us to test if the old unit can be programmable or if the firmware doesn’t support it. Firstly, let’s remove the USB adapter board and have a look.

The two pin header is clearly power, it comes straight from the transformer. I’m not quite sure what the coil on the board does but a bridge rectifier and voltage regulator are obvious. Sorry but some of this “just spotting what they are” is experience and I don’t know how to explain how I guessed certain things. The two 4 leg SMD parts under the 4 pin communications header are most likely optocouplers based on how they look. It’s not normal for a 4 pin device to be so physically large. The text FPT2 can also be seen on these parts, small amount of Googling returns that these are Isocom IS181 optocouplers – datasheet here. This makes some sense, USB board is floating compared to everything else and prevents conducted USB noise getting in.

Pin connections

Bit of tracing later and I appear to be correct. Combing what is traced out above with the pinout for a IS181GB:

Optocoupler Pinout

This gives Pin 1 on the header goes through the LED on the left optocoupler (pale blue line), through a 510R resistor and back to pin 2 (orange line). The output emitter is connected to 0V (black circle) and the collector goes off somewhere (purple line) but has that resistor marked 30B (I think but it measures 2k exactly) going up to the supply voltage on the USB board. The opposite happens in the TX path, a trace comes out from the IC, through a 510R resistor (dark blue trace), into pin 2 of the right hand optocoupler with the LED anode connected to V+ on the USB board (brown circle). Pin 4 of the optocoupler goes to pin 4 of the header (green trace), pin 3 goes to the collector (red trace) with a 2k pullup to pin 1. This would suggest pin 1 is the logic supply voltage on the other board. That nicely means that circuit is symmetrical which is a nice design touch. This is quite hard to convey in words so schematically:

Schematic form of optoisolators

Fairly nice, quite elegant and means that both RX and TX paths always have normal polarity. Standard feeding of the data signal into the anode means the UART gets inverted as a high into the LED causes the output to be pulled low but this problem doesn’t exist here. It does mean that we should find normal polarity UART signals at the point marked TX and RX above. Now I’m fairly happy that this USB board should not damage the old power supply, the acid test is plugging the USB board into the old supply’s control board and seeing if we can talk to it. There is the slight issue of there being no extra winding on the transformer but that’s nothing a bit of Frankensteining can’t fix. Note, what is shown in the next picture is seriously dangerous. PLEASE DON’T DO THIS YOURSELF.

Old PSU (left) getting the data feed while the new PSI (right) powers the USB board

Oh it’s a good job I spent loads on my education, right? Incidentially, I did replace the fan in the old PSU with a random one I found but it didn’t solve fan noise. Most power supplies don’t come looking as trashy as this.
However, this was vaguely successful. Running the same script as above, the old PSU did not return an identity string but did beep to indicate it was being controlled remotely and locked all the front panel buttons while the script was running. Control was then nicely set back to manual once the script finished. Think we need to write a bit more code to see if we can actually do something as it’s not surprising that a PSU without remote control doesn’t have an identity string. So let’s query the voltage on channel 1. Same code as before but send “VSET?” instead of “*IDN?” and… proper success this time, b'09.00'and it was actually set to 9V! This is excellent, cheap FTDI board, couple of optocouplers to do it nicely and we’re in business and I’ll have 2 programmable supplies! For someone that doesn’t already have a power supply, this probably isn’t worth doing as it’s only £20 difference but it saved me buying another £80 supply should I need a second programmable one so worth it for me 😀 Part 2 will be soldering something up to put into the old PSU.