Tuesday, November 29, 2011

First instance of sensorless sine commutation!

I won't quite call it sensorless field-oriented control yet, because it's still running open-loop sinusoidal commutation, but I've finally been able to yank the sensor cable:


Here's the proof in data:

(Cap Kart's fancy data logger has certainly come in handy.)
What you see there is a plot (in time) of the motor RPM as measured directly by the Hall effect sensors (yellow) and indirectly by the open-loop flux estimator (blue). At first, both RPM measurements are running simultaneously and they overlap. At the point where the sensor cable is yanked, the RPM measurement from the sensors freezes and then times out to zero, but the flux estimator RPM continues to read properly. The commutation is being controlled by the flux estimator only and the motor continues to run, even under load.

For this test, the motor is being driven by a 12.5% command, which means the peak-to-peak value of the sine waves is 12.5% of the DC voltage. The no-load speed is therefore about 12.5% of maximum. The loaded speed dips to as low as about 6% during the test. This is a good sign that the flux estimator is capable of working at low speeds where the back EMF is small. I haven't written the start-up ramp yet, but this test tells me that I should be able to exit the start-up mode at less than 10% speed.

The properly-functioning flux estimator did not come easy. As I mentioned last time, I converted all the floating-point math to fixed-point 32-bit integer math to get the estimator to run at 10kHz. This led to several instances of both loss-of-precision and overflow problems. The biggest culprit was a low-pass filter being used as a pseudo-integrator on (V-IR). Here's how it looks in floating-point:

vir_a = 0.9996 * vir_a + 0.0004 * vir_a_temp;

Every 100μs, the variable vir_a is decreased by 0.04% and the same percentage of a new value, vir_a_temp, is added in. Since this happens 10,000 times per second, the overall effect is that of a low-pass filter with a time constant of 0.25s. The simple conversion to fixed-point math would be:

vir_a = vir_a * 9996 / 10000 + vir_a_temp * 4 / 10000;

The constants are moved to the opposite side of the variables so that left-to-right order of operations doesn't yield zero all the time. (9996 / 10000 = 0 in integer math.) But that's not the only issue:

[Start of horrible multi-day debugging sequence:]

If vir_a_temp is less than about 10^6, the minimum tolerable precision (~1%) of the second term is lost. Since the physical unit of vir_a and vir_a_temp is Webers, this means working in μWb or even nWb depending on the expected flux.

But, if vir_a is greater than 10^6, then the first multiplication of the first term, vir_a * 9996, overflows a 32-bit signed integer.

Okay, no problem, let's just change the order of operations in the first term:

vir_a = vir_a / 10000 * 9996 + ...

Problem solved? Actually, no. Think about what happens on the first two iterations with vir_a_temp set to 10^6 and vir_a intially at zero.

(1) vir_a = 0 / 10000 * 9996 + 1000000 * 4 / 10000;

The result is 400.

(2) vir_a = 400 / 10000 * 9996 + 1000000 * 4 / 10000;

The results is...400 again. The first term is zero.You can probably see the problem. vir_a will never increase because the divide by 10000 kills off the small initial values completely. So, you can't divide by 10000 first because it kills the small initial values, and you can't multiply by 9996 first because it overflows the large steady-state values. My solution is so horrible that I'm actually ashamed to post it:

if((vir_a <= 214748) && (vir_a >= -214748))
{ vir_a = vir_a * 9996 / 10000; }
else
{ vir_a = vir_a / 10000 * 9996; }

(Note: 214748 is 2^31 / 10000)

There must be a more elegant way to do this...

[End of horrible multi-day debugging sequence.]


After sorting out that mess, I could finally get to the interesting part, which was actually testing the flux estimator to see if it works. Because I opted to paste the flux estimator directly in parallel with my existing sensor-based control code, the testing was greatly simplified:

At all times, the Hall effect sensors are being read and a rotor electrical angle is interpolated from them. Separately, the flux estimator is running on all three phases. At flux zero-crossings, a "virtual Hall effect sensor" transition is generated. The rotor electrical angle based on flux is interpolated from these virtual Hall effect sensor transitions, using the same method as the real sensors. This is where the two methods intersect, and either electrical angle can be fed into the field-oriented control algorithm (or, in this case, directly into the sine wave generator).

Both electrical angles (from flux and from sensors) are recorded and periodically transmitted to the data logger. Because the commutation frequency is much greater than the transmit frequency, plotting either angle against time would not be very informative. But, plotting them against each other tells the whole story:


The rotor electrical angle derived from Hall effect sensors, which were properly sequenced and timed, is nicely correlated to the rotor angle derived from the flux estimator. The "virtual Hall effect sensors" from the flux estimator do not require sequencing or timing, since they are linked to the correct phase variables already. (No more guessing wire combinations!) 

There is a bit of a staircase effect which I have yet to determine a cause for. There are also still a few bugs to track down, including something that is causing the field-oriented control to freak out. And I haven't even begun to write in the start-up and fault detection, but at least progress has finally been made.

5 comments:

  1. Hey Shane, that "if" statement actually made me LOL but if it works it works...and that's all that matters. I can't wait to see the next progress report, your blog is fantastic reading :)

    ReplyDelete
  2. For (X*9996)/10000 try X- ((x*26)/65536) The divide by 65536 can be done with a shift by 16 bits (most modern compilers know how to optimize the crap out of that).

    Another way of dividing by 65536 is to store the 32-bit result of the multiply by 26 in a union variable that is a union of a 32 bit variable and two 16 bit variables (the upper 16 bits and the lower 16 bits). Then pick the upper 16-bit word out of the union. Pay attention to the big-endian/little-endian byte ordering of the processor.

    ReplyDelete
  3. I like that - it avoids the problem of overflowing the first term when multiplying by the larger portion of the complementary fraction.

    I looked up the cycles for the Cortex-M3: 32/32 division is 2-12 cycles, right arithmetic shift should be single cycle. So that should save some time for sure.

    ReplyDelete
  4. yes, so your

    vir_a = 0.9996 * vir_a + 0.0004 * vir_a_temp;

    term becomes:

    vir_a=(vir_a*65510+vir_a_temp*26)>>16;

    in a fixed point math, but beware that vir_a and vir_a_temp need to be represented no more than 16 bits

    ReplyDelete