1
\$\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\$

2 Answers 2

2
\$\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\$
14
  • 1
    \$\begingroup\$ Why no need for denouncer? \$\endgroup\$
    – user76844
    Mar 9, 2018 at 7:01
  • \$\begingroup\$ Besides, in a microcontroller there should be quadrature encoder peripheral, and the op seems to be talking about fpga. \$\endgroup\$
    – user76844
    Mar 9, 2018 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, 2018 at 7:09
  • 2
    \$\begingroup\$ @GregoryKornblum, you want a denouncer? :-) \$\endgroup\$
    – TonyM
    Mar 9, 2018 at 7:32
  • 1
    \$\begingroup\$ @GregoryKornblum, but you could simply detail and explain the method plus areas of concern and how to resolve them, like we do on this site. C'mon, it's really not that big a problem at all. I've done a good few of these and they've worked first time and every time. Can I ask did you understand all the reasons for the problems you had? Sounds like you had more trouble than I'd expect. \$\endgroup\$
    – TonyM
    Mar 9, 2018 at 9:40
1
\$\begingroup\$

This is a decoder I wrote years back, and have used successfully in many personal and commercial applications.

It is designed to efficiently decode anywhere between 1 and 16 encoder inputs in parallel, but compile efficiently regardless of how many are used.

The example below targets 32-bit MCUs - to adapt for efficiency on 8-/16-bit architectures change data type accordingly and reduce maximum parallel decoders to bits/2.

Attributes are in place to optimize for GNU compilers and may need to be modified for other compilers.

It will count up/down only when the correct quadrature signals are given in sequence - it will not respond if both A and B signals change simultaneously as direction is then undeterminate.

It can be used in interrupt-driven or polled architectures, but I personally prefer polling as it puts a deterministic load on the CPU and will not be affected by faults that result in high-frequency noise on the quadrature inputs, which are often closely coupled with PWM signals that could generate this exact fault and result in a locked-up processor due to excessive interrupting.

// This example is for Kinetis MCU with 4 encoders connected to PTC0-PTC7
// Decoder should be run at well above the maximum flank frequency when polled

// Any fast method of obtaining the input data
// Each individual encoder is a pair of bits 0-1, 2-3, etc.
// Odd bits A, even bits B - optimally use GPIOs accordingly
#define QUADRATURE_BITS (GPIOD_PDIR & 0b11111111)

// Expected initial signal levels
#define QUADRATURE_INIT 0b11111111

// Number of decoders to implement
#define QUADRATURE_COUNT 4

// Current quadrature count
int32_t quadrature[QUADRATURE_COUNT];

// Parallel quadrature decoder
__attribute__((optimize("unroll-loops")))
static __INLINE
void decode_quadratures() {
  uint32_t curr = QUADRATURE_BITS;
  static uint32_t prev = QUADRATURE_INIT;
  uint32_t xora = (curr ^ (prev >> 1)) & 0X55555555;
  uint32_t xorb = (prev ^ (curr >> 1)) & 0X55555555;
  uint32_t decoded = ((xora ^ xorb) << 1) | xora;
  for(int id = 0; id < QUADRATURE_COUNT; id++, decoded >>= 2) {
    if(decoded & 2) {
      quadrature[id] += (decoded & 1) ? +1 : -1;
    }
  }
  prev = curr;
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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