I spent all day Saturday and Sunday (except for a few hours grading) working on the lab handout for the class-D power amplifier lab. It was going great on Saturday, but I decided to include a portion of the lab on doing an LC output filter, rather than just directly connecting the speaker between ground and the FETs of the output stage as I had originally planned. That opened a big can of worms.

First, the standard design method calls for adding a “Zobel network” to compensate the speaker: a resistor and capacitor in series, that is put in parallel with the loudspeaker. I came up with a cute trick for using gnuplot’s fit to optimize the network given the loudspeaker model that I had developed, and wrote that up. Here is the gnuplot code for designing the Zobel network:

set logscale xy set xlabel "frequency [Hz]" set xrange[10:1e6] j=sqrt(-1) Zc(f,C) = 1/(j*2*pi*f*C) # f= frequency [Hz], C=cap [farads] Zl(f,L) = j*2*pi*f*L Zpar(z1,z2) = z1*z2/(z1+z2) #impedance of loudspeaker (complicated model) Zloud(f) = Rnom+Zl(f,Lhi)+Zpar(Rs,Zpar(Zl(f,Ls),Zc(f,Cs))) + Zpar(Rex,Zl(f,Lex)) Rnom=7.67 Lhi=34.6e-6 Ls=2.9e-3; Cs=394e-6; Rs=15.5 Lex=112e-6; Rex=31.9 # impedance of whole circuit with RC compensator Zcomp(f,Rcomp,Ccomp) = Zpar(Rcomp+Zc(f,Ccomp), Zloud(f)) #Guess some initial values Rcomp=Rnom; Ccomp=0.1e-6 # Kluge: we need points at which to do the fitting. # Instead of creating a data file, we can have a short python program that # generates points, one per line. # The following string, when interpreted as a datafile, # generates 101 points from 400 to 4e6, with uniform ratio between them fit_points = "< python -c 'for i in range(101): print 400.*(10.**(i*0.04))'" # Pick a target value for the constant impedance at high frequency # (say the resistance at 1kHz, which is in the flat region) Rtarget=abs(Zloud(1e3)) # Alternately fit Ccomp and Rcomp to try to keep the impedance near Rtarget # (the scales are too different to fit both at once). fit abs(Zcomp(x,Rcomp,Ccomp)) fit_points using 1:(Rtarget) via Ccomp fit abs(Zcomp(x,Rcomp,Ccomp)) fit_points using 1:(Rtarget) via Rcomp fit abs(Zcomp(x,Rcomp,Ccomp)) fit_points using 1:(Rtarget) via Ccomp fit abs(Zcomp(x,Rcomp,Ccomp)) fit_points using 1:(Rtarget) via Rcomp set ylabel "impedance [ohms]" set yrange[*:*] plot abs(Zloud(x)) title "uncompensated loudspeaker",\ abs(Zcomp(x,Rcomp,Ccomp)) title sprintf("loudspeaker || (%.3g ohm + %.3g F)",Rcomp,Ccomp), \ abs(Zcomp(x,8.2,2.2e-6)) title sprintf("loudspeaker || (%.3g ohm + %.3g F)",8.2,2.2e-6)

The code produces the following plot, showing good compensation (I added the last plot statement with standard values for Rcomp and Ccomp, to show that the exact values were not that crucial, and standard values would work almost as well).

But then I realized that I’d need a 10W resistor for the Zobel network, since with no input the class-D amplifier puts out about 10W of power at the PWM frequency, almost all of which would be dissipated in the resistor. Even with a good LC filter blocking the PWM frequency, we could still have a lot of power dissipated in the resistor for frequencies in the audio range (like almost 8W at 10kHz). We don’t have power resistors in our kit, and I don’t like such an inefficient solution.

The point of the Zobel network is mainly to simplify the design of the LC low-pass filter, but with the detailed loudspeaker model, we can use gnuplot to plot the gain of a voltage divider, and fit L and C to the desired gain as a function of frequency.

It turns out to make a quite a difference how you specify the desired gain, and whether you use a linear or a log model for the errors. I wrote several gnuplot scripts, some of which did optimization, and some of which just explored the effect of choosing several standard values. Here is one of the optimization scripts and its output:

set title "Low-pass LC filter for loudspeaker without Zobel network" set logscale xy set xlabel "frequency [Hz]" set xrange[1:1e6] set ylabel "gain" set yrange [0.005: 2] set key bottom left Left set samples 1000 j=sqrt(-1) Zc(f,C) = 1/(j*2*pi*f*C) # f= frequency [Hz], C=cap [farads] Zl(f,L) = j*2*pi*f*L Zpar(z1,z2) = z1*z2/(z1+z2) divider(Zup,Zdown) = Zdown/(Zup+Zdown) #impedance of loudspeaker Zloud(f) = Rnom+Zl(f,Lhi)+Zpar(Rs,Zpar(Zl(f,Ls),Zc(f,Cs))) + Zpar(Rex,Zl(f,Lex)) Rnom=7.67 Lhi=34.6e-6 Ls=2.9e-3; Cs=394e-6; Rs=15.5 Lex=112e-6; Rex=31.9 # voltage divider with Lfilt and Cfilt gain(f,Lfilt,Rfilt,Cfilt) = divider(Zl(f,Lfilt)+Rfilt, Zpar(Zloud(f),Zc(f,Cfilt))) # total impedance, in case we want to plot that also Ztotal(f,Lfilt,Rfilt,Cfilt) = Zl(f,Lfilt)+Rfilt + Zpar(Zloud(f),Zc(f,Cfilt)) desired_gain(f) = f<cutoff? low_gain:="" 0.01="" #="" a="" simple="" <span="" class="hiddenSpellError" pre="">step function # desired_gain(f) = low_gain*(f<cutoff? 1: (cutoff/f)**2) # f^-2 rolloff # desired_gain(f) = low_gain*(f<cutoff? 1: 0.3*(cutoff/f)**2) # step then f^-2 rolloff cutoff =10000 # what cutoff frequency do we want? low_gain = 0.95 # we expect some loss a low frequencies---we want it to be constant # error_model(y)=log(y) # use this one to fit high frequencies well error_model(y)=y # use this one to keep passband ripple small # As before, we make up a bunch of points from 400Hz on up to 4MHz fit_points = "< python -c 'for i in range(101): print 400.*(10.**(i*0.04))'" Lfilt=220e-6 ; Rfilt=0.252 # AIUR-06-221 inductor (from spec sheet) Cfit=1e-6 # here we fit a capacitor for a fixed impedance fit error_model(gain(x,Lfilt,Rfilt,Cfit)) fit_points \ using 1:(error_model(desired_gain($1))) via Cfit Lfit=Lfilt # here we fit an inductor for a fixed capacitance, resistance proportional to inductance # The resistance is about right for the AIUR-06 series of # inductors---air-core coils with 24-gauge wire would be more like # 2500*Lfilt to 4500*Lfilt. fit error_model(gain(x,Lfit,1200*Lfit,2.2e-6)) fit_points \ using 1:(error_model(desired_gain($1))) via Lfit # Try fitting both: Cboth=Cfit Lboth=Lfit fit error_model(gain(x,Lboth,1200*Lboth,Cboth)) fit_points \ using 1:(error_model(desired_gain($1))) via Lboth fit error_model(gain(x,Lboth,1200*Lboth,Cboth)) fit_points \ using 1:(error_model(desired_gain($1))) via Cboth fit error_model(gain(x,Lboth,1200*Lboth,Cboth)) fit_points \ using 1:(error_model(desired_gain($1))) via Lboth fit error_model(gain(x,Lboth,1200*Lboth,Cboth)) fit_points \ using 1:(error_model(desired_gain($1))) via Cboth plot desired_gain(x), \ low_gain*sqrt(0.5) title "half-power level", \ abs(gain(x,Lboth,1200*Lboth,Cboth)) title sprintf("L=%.3guH C=%.3guF", Lboth*1e6, Cboth*1e6), \ abs(gain(x,Lfit,1200*Lfit,2.2e-6)) title sprintf("L=%.3guH C=%.3guF", Lfit*1e6, 2.2), \ abs(gain(x,Lfilt,Rfilt,Cfit)) title sprintf("L=%.3guH C=%.3guF", Lfilt*1e6, Cfit*1e6), \ abs(gain(x,Lfilt,Rfilt,2.2e-6)) title sprintf("L=%.3guH C=%.3guF", Lfilt*1e6, 2.2)

Unfortunately, I don’t think that the students in my class are up to doing this much exploration with gnuplot, so I may have to fix at least one parameter for them—probably the size of the inductor, since I’ll have to buy them for the students. I had initially thought that a small inductor that they could hand-wind would do, as the design with the Zobel network only needs a small inductor. But without the Zobel network, I needed much bigger inductors, and hand-winding a coil with 50–100 turns is likely to be tedious and error-prone (also costing more in wire than a 55¢ 200µH ferrite-core inductor rated at 1.9A). I’m going to have to buy inductors and try the lab out again, since I’m not sure whether the parasitic impedance in the electrolytic capacitors that they have for 1µF, 2.2µF, and 3.3µF will cause problems. They have 0.1µF and 4.7µF ceramic capacitors, but nothing in between—they would probably work in a pinch, but with poor performance over 3kHz. The 0.1µF capacitor would also need a very high PWM frequency, which may produce too much heat in the FETs.

Since I need to do more experimenting with the class-D design to find the problems before handing it over to the students, I’ve moved that lab from week 8 to week 9, giving me another week to finish the handout (which is already 12 pages long, and not finished yet). That means that I have 2 days to write the new lab 8: the instrumentation amplifier for the strain-gauge pressure sensor. I’ve built and tested such an amplifier (even demoed it on the first day of class), so there aren’t any surprises waiting for me, but I have to write up not only an explanation of instrumentation amps, but also how to do layout for the protoboard.

Hmm, I just realized that I don’t have any posts on the blog for the rev3.0 instrumentation amp protoboard that I designed using the MCP6004 quad op-amp chip instead of the MCP6002 dual op-amp chip (still with the INA126P instrumentation amp chip). My old test of the pressure sensor lab was with the old protoboard. Maybe I need to redo the pressure-sensor lab with the new board, to make sure that there aren’t any problems with the PC board design.

So I’ve got today and tomorrow to redo the pressure-sensor lab and write up the lab handout, plus finish the grading and figure out how I’m going to present sampling and aliasing on Wednesday, before the Thursday lab. (I plan to use my son’s stroboscope, but I’ve not figured out what motion we’ll sample with it.)

May I ask a detail question? I had NO idea that gnuplot could be scripted like this! What are the advantages of scripting in gnuplot, as opposed to generating data in MATLAB (Octave) or SciPy, then graphing the results? Is gnuplot particularly fast or efficient at things like this? Is it common to use gnuplot like this in your research community? (I’m interested in scripting practices, and am fascinated to see a tool that I use (gnuplot) being used in a way that I wouldn’t have guessed at.)

Comment by Mark Guzdial — 2013 February 18 @ 17:31 |

I’m not sure what part of the scripting you find unusual. The use of a script to set all the properties of the plot is completely standard, so is plotting functions. For those of use who do curve fitting, the use scripts to do the fit commands is also common, as is the use of functions to build the model to be fit.

About the only weird thing in this particular example was my use of the “< " pipe redirection to generate data, rather than just to reformat it. (Most of the time the redirection is used only for things like database queries or reformatting programs.)

I find gnuplot plotting and model fitting a lot easier to work with than the more general-purpose packages in Python or Matlab, which have very powerful operations but incredibly difficult to decipher user interfaces. I also find switching back and forth between programming languages a bit of a nuisance, as I start typing the wrong syntax (a particular problem when rapidly switching between Python and c++), so I prefer to work in just one language at a time. Having everything in one file also makes it easier for me to distribute the code (like posting on my blog or putting on a class web page).

Gnuplot is not a particularly powerful programming language (no looping and the only conditionals are conditional expressions), but it produces good plots for a variety of different output formats, and is pretty easy to use. If you set up your Mac right (getting the command-line tools that Apple now omits by default and downloading X windows) it is not too difficult to install (though I still wish that the gnuplot community would put together a proper Mac binary installer—I've spent several hours this quarter helping students install gnuplot on their Macs).

Comment by gasstationwithoutpumps — 2013 February 18 @ 22:26 |

[…] All weekend and handouts still not written, I […]

Pingback by Pressure-sensor lab handout written « Gas station without pumps — 2013 February 19 @ 22:32 |