0
\$\begingroup\$

Simply put, I'm controlling a DC-motor with a dual-channel encoder with a microcontroller for a personal project, and I'm trying to find the "best" software-based implementation of the following state-machine given in this TI reference manual:

enter image description here

Before doing any actual research on the topic, I, being a hot-headed idiot, quickly tried to use a (software implementation of a) D-flip-flop to get the motor direction, and then increment or decrement a counter according to the direction. Half C, half pseudo-code here:

uint16_t counter = 0;
uint8_t direction = 0;

void interruptEncoderA(void){
    if(getInput(encoder_a) == 1){
        direction = getPinValue(encoder_b);
    }
    direction ? counter++ : counter--;
}

void interruptEncoderB(void){
    direction ? counter++ : counter--;
}

The interrupts happen on both edges of the corresponding channel. This naive implementation has some very clear problems. For instance, when the motor is going back and forth between the edges of encoder B, it will keep counting on the same direction.

I know that this question may actually be somewhat subjective, or maybe even dependent on hardware architecture. What I'm really looking for are your takes on efficient, elegant and simple solutions. If possible, provide a short description of the pros and cons of your answer, or any other insights which you think might prove useful.

Also, feel free to change how the interrupts work. If it is more suitable to only have one interrupt that fires at each clock edge of either encoder A or B, or maybe if you prefer 4 interrupts (one per edge, per channel), go for it. Just make sure it's obvious.

Also, just to make it clear, this question is about a microcontroller implementation, not HDLs.

This question actually has a really nice solution to this problem. My take on this implementation will be in the answers.

\$\endgroup\$
0
\$\begingroup\$

Here goes my own implementation.

Using a LUT, one can store the increment that must go into a counter, which determines the position of the rotor.

The index of the LUT is a combination of the current state, the last known state, and the last known direction:

Bits 0 and 1 (the 2 LSBs) are the current state (A and B values, respectively), while bits 3 and 2 are the previous state.

Bit 4 is the previous direction. If a state is missed, the motor is assumed to be going in the same direction as in the last state, thus recovering from a missed step.

Pros: No need for signal debouncing. Small code footprint. Will recover from missed steps.

Cons: CPU time wasted when motor is stopped or rotating slowly. Polling frequency needs to be higher than 4x the maximum encoder frequency (2x per edge, 2 channels).

Pseudo-code here:

int32_t counter_lut[32] = {
        //Direction = 1
        0, // 00 to 00
       -1, // 00 to 01
       +1, // 00 to 10
       +2, // 00 to 11

       +1, // 01 to 00
        0, // 01 to 01
       +2, // 01 to 10
       -1, // 01 to 11

       -1, // 10 to 00
       +2, // 10 to 01
        0, // 10 to 10
       +1, // 10 to 11

       +2, // 11 to 00
       +1, // 11 to 01
       -1, // 11 to 10
        0, // 11 to 11

        //Direction = 0
        0, // 00 to 00
       -1, // 00 to 01
       +1, // 00 to 10
       -2, // 00 to 11

       +1, // 01 to 00
        0, // 01 to 01
       -2, // 01 to 10
       -1, // 01 to 11

       -1, // 10 to 00
       -2, // 10 to 01
        0, // 10 to 10
       +1, // 10 to 11

       -2, // 11 to 00
       +1, // 11 to 01
       -1, // 11 to 10
        0, // 11 to 11
};

uint32_t counter = 0,  direction = 0, lut_index = 0;

void encoderInterrupt(void){

    lut_index |= getInput(encoder_A)<<1 | getInput(encoder_B);
    counter += counter_lut[lut_index];

    if (counter_lut[lut_index] != 0){
        direction = (counter_lut[lut_index] > 0) ? 1 : 0;
    }

    //Prepare for next iteration by shifting current state
    //bits to old state bits and also the direction bit
    lut_index = ((lut_index << 2) & 0b1100) | (direction<<4);
}

Tested to be working perfectly on a 100 counts per revolution encoder, with a gearbox of 12.5:1 and maximum 260RPM (after gearbox), making it 5000 state transitions per revolution (100 counts * 12.5 * 4 encoder signal edges), for a maximum of ~22000 state transitions per second.

The sample frequency is set to 100kHz, and the interrupt priority is above all other interrupts, which is not a huge deal considering how little code the CPU actually has to execute. This was tested on a 144MHz Cortex-M4 microcontroller.

\$\endgroup\$
  • 1
    \$\begingroup\$ Why no need for denouncer? \$\endgroup\$ – Gregory Kornblum Mar 9 '18 at 7:01
  • \$\begingroup\$ Besides, in a microcontroller there should be quadrature encoder peripheral, and the op seems to be talking about fpga. \$\endgroup\$ – Gregory Kornblum Mar 9 '18 at 7:05
  • \$\begingroup\$ If you poll the signals at a constant frequency, even if your motor stops right near the edge of one of the channels, you will just oscillate between 2 states, which is fine, because as soon as the motor moves away from that position, the next iteration will have no problems knowing in which state you are. If you were to implement this using interrupts and the motor stopped in the same way, the CPU would register a bunch of state transitions so fast that it might lose some. \$\endgroup\$ – Chi Mar 9 '18 at 7:09
  • \$\begingroup\$ Ok, but LPF is still required. \$\endgroup\$ – Gregory Kornblum Mar 9 '18 at 7:11
  • 2
    \$\begingroup\$ @GregoryKornblum, you want a denouncer? :-) \$\endgroup\$ – TonyM Mar 9 '18 at 7:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.