Thursday, December 10, 2009

3ph Duo: Codefest

These days, most people are surprised when I tell them that my background is in mechanical engineering, not electrical, since more often then not I am troubleshooting some circuit or winding a giant inductor or something. But probably the thing that nobody knows is that my real background is in software. Okay, not in the educational sense, but I've been programming things since I was like...ten. I won't make any claims about my actual skill, but I will say that on more than one occasion I have been saved by a bit of software fidgeting that I probably couldn't have done if I hadn't been writing all those games (e.g. Pokemon Hunter and Pokemon Hunter II) when I was too little to use power tools.

In one of the saddest tragedies of my life, the original source code for Pokemon Hunter (written in QBASIC!) has been lost forever, but I assure you the graphics looked roughly like this.

Oh yeah, this is a post about my brushless motor controller. Or, the latest of them, anyway. If you've lost track, here is a side-by-side comparison of the three real iterations it has gone through:

Left-to-Right: Newest, Middle, Oldest


First was the double-stack IRFB3077-based controller (right), my very first shot at three-phase brushless motor control. While it has the distinction of MOSFETs so powerful they caused the aluminum bus bars to desolder themselves (Yes, you can solder aluminum.), it was impractically large for the scooter because each one only controlled one motor. Next was the greatly compacted 3ph Duo, so named because it controlled two motors from a single board using two IXYS GWM 100-01X1 six-pack MOSFET bridges as the inverter stages. And it works. But as I described in the last post, that won't stop me from making yet another one.

The most obvious visible change is the lack of LEM current sensors. These were large and expensive and have been replaced by the ACS714 surface-mount sensors, two each per motor. Two each so that I can effectively measure three-phase currents, instead of assuming that only two phases conduct current at a given time. If you want to know more about why I am concerned with this, this post sort-of sums it up. The goal is to implement full sine wave commutation with the possibility of phase adjustment, something that would separate this controller from other similarly sized and priced small vehicle brushless motor controllers on the market. Turns out from a hardware standpoint, this is very easy. The microcontroller I use, the TI MSP430F2274 already has six independent PWM output channels (three per motor). So the circuit board is essentially the same, with a few signals re-routed to make room for the six PWM pins.

The real challenge is the software. This is not a 32-bit processor with native floating-point. It is a regular old 16-bit mixed-signal processor that runs at a relatively poky 16Mhz and doesn't even have hardware multiply. Your cell phone probably has 10x more processing power. To roughly lay out the challenge: the PWMs are refreshed at a rate of 15kHz, setting the upper limit on the resolution of the sine waves generated. (You get 15,000 points per second with which to draw the sine function.) But to use all 15,000 points per second, six PWM values must be generated and scaled appropriately at every refresh. The clock speed is 16MHz, so all this has to happen in less than 1,000 clock cycles. To give you an idea of how hard this is, just multiplying two integer numbers together takes about 70 clock cycles on this chip. Forget about fractions and definitely forget about trigonometric functions. The only way to generate a sine wave is to use a look-up table, in this case 256 bytes in memory that store the value of the sine function for various angles. To set a baseline, this is how I originally implemented a sine-wave look-up on six channels:
aidx_int += aspeed;
aidx = aidx_int >> 8;
atemp = SIN8LUT[aidx];
btemp = SIN8LUT[(unsigned char)(aidx - TWOTHIRDSPI)];
ctemp = SIN8LUT[(unsigned char)(aidx + TWOTHIRDSPI)];
atemp = 65023 - ((atemp * amag) >> 6) + ((127 * amag) >> 6);
btemp = 65023 - ((btemp * amag) >> 6) + ((127 * amag) >> 6);
ctemp = 65023 - ((ctemp * amag) >> 6) + ((127 * amag) >> 6);

uidx_int += uspeed;
uidx = uidx_int >> 8;
utemp = SIN8LUT[uidx];
vtemp = SIN8LUT[(unsigned char)(uidx - TWOTHIRDSPI)];
wtemp = SIN8LUT[(unsigned char)(uidx + TWOTHIRDSPI)];
utemp = 65023 - ((utemp * umag) >> 6) + ((127 * umag) >> 6);
vtemp = 65023 - ((vtemp * umag) >> 6) + ((127 * umag) >> 6);
wtemp = 65023 - ((wtemp * umag) >> 6) + ((127 * umag) >> 6);
One motor is ABC, the other is UVW. This steps through the sine table at some speed, looking up values with an independent index for each motor. Then, it scales those values by some magnitude and shifts them such that their PWM-average outputs are sine wave voltages centered at half the DC voltage:


The PWM outputs, normalized to the DC (battery) voltage, for full-command (top) and half-command (bottom). In both cases, they are centered at half the battery voltage.


Well, this almost worked. It worked with 1, 2, 3, and 4 channels. But with 5 or 6 channels, the interrupts starting running into each other because it took longer than 1,000 clock cycles to get through that block of code. Luckily, it's not a very efficient way of doing things, so the next few paragraphs will describe how it was trimmed down. First, the zero-sequence hack. I don't know of anyone who refers to it as such, but this is my favorite motor code hack of all time. A lot of the code above is being used simply to offset the PWM values so that they are always centered at 50% duty cycle. This is...stupid. (Maybe not, but I declare it to be so.) So I ditched that drive method in favor of one that always puts the peak of the sine waves at +Vbat, like this:


The modified drive signals at full-command (top) and half-command (bottom).


The difference is that, except at full command, the three sine waves are shifted up to higher voltages with respect to the battery. But what really matters, as far as the motor is concerned, is the relative voltage across the three phases, which is the same in either case. The same amount of current will come out of or go into each wire, and the bulk shift does not change the motor operation. It does, however, greatly simplify the code:

aidx_int += aspeed;
aidx = aidx_int >> 8;
atemp = SIN8LUT[aidx];
btemp = SIN8LUT[(unsigned char)(aidx - TWOTHIRDSPI)];
ctemp = SIN8LUT[(unsigned char)(aidx + TWOTHIRDSPI)];
atemp = -((atemp * amag) >> 6);
btemp = -((btemp * amag) >> 6);
ctemp = -((ctemp * amag) >> 6);

uidx_int += uspeed;
uidx = uidx_int >> 8;
utemp = SIN8LUT[uidx];
vtemp = SIN8LUT[(unsigned char)(uidx - TWOTHIRDSPI)];
wtemp = SIN8LUT[(unsigned char)(uidx + TWOTHIRDSPI)];
utemp = -((utemp * umag) >> 6);
vtemp = -((vtemp * umag) >> 6);
wtemp = -((wtemp * umag) >> 6);
The extra multiplication of the magnitude to get the sine waves in the right place is gone. So is the bulk offset. All that's left is a negative of the magnitude times the sine value. (Don't ask why it's negative...you don't want to know.) This manipulation gets the interrupt routine down to a size where it can actually run at 15kHz...barely. The processor utilization is up near 60%:


This signal is high when the processor is in a PWM interrupt executing the above block of code.

Of that, roughly half of the time is spent just doing the six multiplications. There are tricks for fast software multiplication, but only if one of the operands is known a priori (not the case here). The sine table look-ups, as fast as they are, also take up some time. The adds and shifts are relatively small contributions. Amazingly, though, this actually works. Try getting your computer to do anything when its processor is being utilized by background processes 60% of the time. (Okay, dual core...I know...) But this can still execute a slow loop with control and data acquisition functions and read in more interrupts from the hall-effect sensors. It's still a little quirky, but it didn't collapse in a mess of horrible interrupt stack Jenga blocks like I thought it might.

But...it can actually be even more efficient. The key observation is in the nature of the outputs, a balanced three-phase set of voltages. This type of output only really has two degrees of freedome...magnitude and phase. So you should really only have to look up two numbers, right? (Can you see it coming?) If you add the sine of any three angles separated by 120ยบ each, you get zero. Try it. Or have Wolfram Alpha try it. Here's my proposal: Look up and scale two sine values per motor. The third is the sum of the first two, negated. C = -(A+B). W = -(U+V). It's a bit more complicated because of magnitude offsets, but if this offset is calculated based on the magnitude in the slow loop, it can be simply added to the third value:
uidx_int += uspeed;
uidx = uidx_int >> 8;
utemp = SIN8LUT[uidx];
vtemp = SIN8LUT[(unsigned char)(uidx - TWOTHIRDSPI)];
utemp = -((utemp * umag) >> 6);
vtemp = -((vtemp * umag) >> 6);
wtemp = utemp + vtemp - woffset;

aidx_int += aspeed;
aidx = aidx_int >> 8;
atemp = SIN8LUT[aidx];
btemp = SIN8LUT[(unsigned char)(aidx - TWOTHIRDSPI)];
atemp = -((atemp * amag) >> 6);
btemp = -((btemp * amag) >> 6);
ctemp = atemp + btemp - coffset;
That's two fewer table look-ups and two fewer multiplies. The resulting interrupt should run 30% faster, giving back valuable clock time. Again, don't ask why everything is negated...

That's a far stretch in the name of efficient computation, but I assure you it makes a difference. Although everything seemed to be more-or-less working even with the bulkier interrupt. It's always good to write simple code, for many reasons. Which reminds me of another good one:



Yep, finally ran out of program memory. And I sure as hell am not going to buy the $N,000 full version of IAR Embedded Workbench. For the record, no compiler is worth that much money. It's not like Solidworks or some specialized simulation program that costs a lot of money to develop. It's a f*cking compiler. It takes C code and makes assembly code based on a set of well-understood standards. Even if I were using it for a commercially, it would be more worth my time to get a free GCC compiler and learn how to use Eclipse. Screw you, IAR. But I love your free version. :) So I will just have to keep my code size down.

Now might be a good time to step back and ask what the heck I am doing. Optimizing this one timy bit of code is a long way from making a new sine-commutated motor controller, and I have a long way to go before I can say I've finished the latter. But with the sine wave generator running at 15kHz in the background, all that's really left is to integrate the Hall effect sensors and some sort of master control loop.

The Hall effect sensors are really easy...dare I say easier than they are with the normal six-step commutation scheme where they drive a state machine. When a sensor hit comes in, you abandon whatever position you were at before and jump to the "correct" place in the sine table. "Correct" is tricky, and this is where a phase offset can be added in. But for now, I will rely on the scooter's moveable Hall sensors to make this work. The other piece of the puzzle is to set the speed of advance through the sine table, aspeed and uspeed in the code above. This is done by (carefully) estimating the time of one electrical cycle, which is six Hall effect transitions or in this case 1/7th of a revolution. I say carefully because this breaks down at low speed and also can be "glitchy". So there is more software work to be done here.

Lastly, the master control loop. This is where the high-level implementation happens. Eventually, this will hopefully measure and control both the quadrature- and the direct-axis current. The quadrature-axis current is the current that actually pulls on the magnets, creating torque. The direct-axis current can be used to change the torque-speed curve by countering the magnets, but I doubt this will make much difference for the scooter motors. Anyway, there is still some work to be done before this method of control is fully functional. For now, it is easy enough to create something that works by just controlling some semi-arbitrary current measurement, or even just running it open loop. This is where I am now...testing the subsystems to make sure they are working reliably before I integrate everything in the master loop.

But that didn't stop me from trying to ride it. The most noticeable difference (besides the fear that it might jump out of the program loop and short the motor at any moment) is the reduced torque ripple. You can actually hear the difference between DC (six-step) operation and AC (sine wave) operaiton. Okay, maybe you can't really hear the difference...but that's mostly because my microphone is terrible and the sensors were still not quite in the right place in either case. But there is definitely less high-frequency noise. If you don't believe me, believe MATLAB:

I promise I will do a better job capturing the results in a future update. That is, assuming I don't go crazy from debugging software. It's usually not my favorite thing to do. There's some fun in squeezing every last drop of computational power out of this thing, but I would still rather be making something tangible. Although in the end that's what this is for, so that's one plus. And if things go downhill, the old controller worked perfectly fine. Which...hrm...why am I doing this again?

I also promise a massive technical write-up for anyone who is interested in how this thing actually works. Ha...funny...nobody cares about controller. But there will be a write-up nonetheless.


No comments:

Post a Comment