Close

Hypothesizing about init-state...

A project log for anaQuad!

Reliably increase your quadrature-encoder's resolution using an ADC (or a bunch of comparators?)

eric-hertzEric Hertz 03/04/2016 at 13:510 Comments

(Update: Adding init-state testing results for the 2x case, at the bottom)

-------------

The "no-Mult" method for4x resolution, from the last log, has a "bug" of sorts... Rather, it's not exactly a bug, but an annoyance, anyhow.

Here it is, again...

If you power-up the system, and, say, it assumes you're at position A, but you're really just right of position H... the system will stick at position A. Whereas, in the 2x method, I think (but have not fully verified) that if you start at any position, it will automatically "catch up" to the actual position.

So, I've some hypothesizing...

In the 4x "no-mult" case, there's an 'eye' where both endpoints are tested... and these two endpoints are *closer* than 1/2 period. I'm not sure why, but for some reason my intuition is telling me that's somehow related, or maybe responsible, for the inability to "catch up."

I highlighted the wrong "eye" here, for my 'H example'... but looking between the A state and the H state, it's quite apparent that the transition from A->B is not met because there exists an 'eye' between B and H. The transition from A->B requires testing that the black line is greater-than the green line, but to the right of the H-case, they've already crossed-over again. Their sampled-values show that the black line is less than the green line, so as far as "State A" is concerned, the B-crossover hasn't happened. Similar is true in the other direction, between P and J, preventing it from "catching up" in the reverse-direction.

Plausibly, the "eye" thing isn't the factor, but the "dead-zone" between them...?

Here's the 2x case, again...

Note that no "eyes" are less than one half of a period. So, I think, it's plausible to use the simple comparisons in the switch() statement to automatically iterate through from an assumed position to an actual position at least as far as 1/2 period away (maybe even a full period?), or "auto-align", or "catch-up".

Here's a 4x case I did early-on (with intentional "noise" to simulate offset and magnitude calibration-error)...

This one doesn't have any <1/2 period "eyes" either... so, again, I've a vague idea it might be possible for it to "catch up."

The problem with this one is it's centered around zero, whereas my ADC readings will not be. The scaling/shifting math necessary to zero it would add up quite quickly... multiplications of the signal, multiplications of the offset...

Now here's a case I was experimenting with just-now which gave me the "eye"dea (heh).

That "eye" is wonkey-shaped, but also <1/2 period.

This technique's not so great, anyhow, as it requires multiplications of fractions... so there's division or floating-point involved...

One might say "why you goin' to all that trouble to detect crossovers when alls you really need is to measure several threshold values...?"

Well, don't doubt that I've thought about it... I think I explained it earlier, but briefly:

The crossovers are *much* more noise-immune, for one thing. Also, as can be seen in the second-to-last image, it's relatively immune to things like calibration-error... if the offsets or amplitudes vary slightly between the two channels, or if the amplitudes aren't exactly the value I hard-code into the software, it should still work. There may be some positional accuracy error, but there shouldn't be a case where, for instance, a really large amplitude-difference (beyond whatever hardcoded-thresholds I might put in) would cause steps to go undetected. Also, note that I'm going out of my way not to look for "peaks", wherever there's a peak there's also a cross-over. That's *much* more detectable.

But, anyways, back to the "eye" thing... Again, I don't really know for certain that the 2x case even auto-aligns as I think it does... and actually I can't really think of a way to test (or otherwise verify) it, without testing every possible case. But somehow that "eye" sticks out to me as some sort of indication that it wouldn't be possible to auto-align in all (or as many) cases.

I've some other vague idea of something like riding a wave, or magnetic vs electric fields oscillating, or a few other such things... maybe traversing a circle. In the 2x case, it could be imagined that there are four (or two?) points on a circle which constantly stay the same angular-distance apart... Maybe a spring's attached between them, or something... The tension on that spring will never change. The 4x case centered at zero similarly. Or, how 'bout like the piston on a steam-engine... yeah. But the other cases are kinda like springs attached to separate gears... or planets with really weird orbits... I dunno, the intuition's there, the explanation's lacking.

------------------------

Init-state experiments with the 2x case:

Again, here's the graph:

The theory, again, is that this one is capable of automatically detecting the initial state via a few calls to the update() function.

It seems to be working, with a bit of "slop" inherent to the hysteresis of the system...

Locating our initial position from angle 224...
aE: adcVal_A=108, adcVal_B=610
aE:  lastPhaseState='A'
aE:   test1 = adcVal_A = 108
aE:   test2 = INVERT_CHANNEL(adcVal_B) = 101
aE:   test3 = adcVal_A = 108
aE:   test4 = adcVal_B = 610
aE:    test1 > test2. Advancing.
aE: adcVal_A=108, adcVal_B=610
aE:  lastPhaseState='B'
aE:   test1 = adcVal_B = 610
aE:   test2 = INVERT_CHANNEL(adcVal_B) = 101
aE:   test3 = adcVal_A = 108
aE:   test4 = INVERT_CHANNEL(adcVal_A) = 603
aE:    test1 > test2. Advancing.
aE: adcVal_A=108, adcVal_B=610
aE:  lastPhaseState='C'
aE:   test1 = adcVal_B = 610
aE:   test2 = adcVal_A = 108
aE:   test3 = adcVal_A = 108
aE:   test4 = INVERT_CHANNEL(adcVal_B) = 101
aE:    test1 > test2. Advancing.
aE: adcVal_A=108, adcVal_B=610
aE:  lastPhaseState='D'
aE:   test1 = INVERT_CHANNEL(adcVal_A) = 603
aE:   test2 = adcVal_A = 108
aE:   test3 = adcVal_B = 610
aE:   test4 = INVERT_CHANNEL(adcVal_B) = 101
aE:    test1 > test2. Advancing.
aE: adcVal_A=108, adcVal_B=610
aE:  lastPhaseState='E'
aE:   test1 = INVERT_CHANNEL(adcVal_A) = 603
aE:   test2 = adcVal_B = 610
aE:   test3 = adcVal_B = 610
aE:   test4 = adcVal_A = 108
aE:    No Change.
...found at 4 = 180 deg



Locating our initial position from angle 225...
aE: adcVal_A=103, adcVal_B=606
aE:  lastPhaseState='A'
aE:   test1 = adcVal_A = 103
aE:   test2 = INVERT_CHANNEL(adcVal_B) = 105
aE:   test3 = adcVal_A = 103
aE:   test4 = adcVal_B = 606
aE:    test3 < test4. Decrementing.
aE: adcVal_A=103, adcVal_B=606
aE:  lastPhaseState='H'
aE:   test1 = adcVal_A = 103
aE:   test2 = INVERT_CHANNEL(adcVal_A) = 608
aE:   test3 = INVERT_CHANNEL(adcVal_B) = 105
aE:   test4 = adcVal_B = 606
aE:    test3 < test4. Decrementing.
aE: adcVal_A=103, adcVal_B=606
aE:  lastPhaseState='G'
aE:   test1 = adcVal_A = 103
aE:   test2 = adcVal_B = 606
aE:   test3 = INVERT_CHANNEL(adcVal_A) = 608
aE:   test4 = adcVal_B = 606
aE:    No Change.
...found at -2 = -90 deg

So, it thinks 225deg is 180deg, and it thinks 226deg is -90deg... That's not so bad, right...? I figured it would be capable of detecting the position within 1/2 period, but of course some preference is given to the increasing direction since its test is run in the if() while the decreasing direction's test is run in an else(). And, though 225 -> -90 seems far-off (why not at least -135?) it makes sense in the sense that it's detecting in the reverse-direction, and 225 isn't far enough away from 360 to actually see the -135deg crossover. This is the hysteresis...

Again, this whole functionality is a nice side-effect that I wasn't at all expecting... but now that I know it's *possible*, it's something I'd like to keep in the higher-resolution versions... It might be a bit of a tradeoff I'll have to decide on. In the 'no-mult' case, it should run quite a bit quicker, but won't be able to auto-align like this. So, then, detecting the initial position at power-up might require an init-routine that involves actually moving the motor. (And, realistically, I'm not even sure how that routine would work). Also, it would mean that the update() function *must* be called at least once between *every* transition, there's no (or at least not as much) room for a missed transition to be caught-up-to. That isn't such a big deal, since, again, this whole system was intended from the start to be lock-stepped like that such that every transition would be detected in realtime. So, again, this whole thing is a bit of "would be nices" based on "completely unexpecteds" that are nice. But am I willing to sacrifice real-time speed-improvement over avoiding a complicated initialization-routine...? hmm...

Further, this auto-aligning experiment was run with "ideal" sine-waves with *known* magnitudes and offsets... what when they're thrown into a real-world system...?

On the plus-side of this experiment... I think I cleaned up the code quite a bit, and it should still be almost as fast...

The old switch-statement looked like:

//From A -> B: cA crosses c~B:  A > ~B
//From A -> H: cA crosses cB:   A < B
case PHASESTATE_A:
   //Advancing Right to B
   if( adcVal_A > INVERT_CHANNEL(adcVal_B) )
   {
      anaEnc_position++;
      anaEnc_lastPhaseState = PHASESTATE_B;
   }
   //Advancing Left to H
   else if( adcVal_A < adcVal_B )
   {
      anaEnc_position--;
      anaEnc_lastPhaseState = PHASESTATE_H;
   }
   //else, wait here...
   break;
which resulted in a lot of redundant-code (every case has a position++/-- and a PhaseState++/--). So why not move that stuff to the end...?

So now it looks like:

//From A -> B: cA crosses c~B:  A > ~B   test1 > test2
//From A -> H: cA crosses cB:   A < B    test3 < test4
case PHASESTATE_A:
   //test1 = adcVal_A;
   SET_TEST(1, adcVal_A);
   SET_TEST(2, INVERT_CHANNEL(adcVal_B));
   SET_TEST(3, adcVal_A);
   SET_TEST(4, adcVal_B);
   break;
SET_TEST(num, var) is pretty much nothing more than "test<num> = var", but also makes it quite nice for debug-printout with some good 'ol preprocessing...:
#define SET_TEST(num, var) \
({ \
   test##num = var; \
   AE_DPRINT("aE:   test" #num " = " #var " = %d\n", var); \
   {};   \
 })

Discussions