Low-overhead rotary encoder servicing

I've been trying to develop a very low-overhead servicing routine for rotary encoders without having to add hardware debouncing. What I've done so far is found in a new "Arduino" folder in an old Github repo I started some years ago:

Included there are two Arduino demonstration sketches which use pin-change interrupts. The first uses the standard lookup table method where the previous and current pin states form an index into a table of all possible transitions. Interrupts are enabled on both encoder pin lines. In addition to blinking a CW or CCW LED for each tick, the sketch now prints to the serial monitor the total number of interrupts which have been serviced since the last tick, which provides an indication of how bouncy or noisy the encoder switches are.

An alternate sketch works the same way, but it enables only one interrupt at a time. When an interrupt occurs, the ISR disables further interrupts on that pin, and enables interrupts on the other pin. So none of the bouncing that may take place on the interrupting pin will trigger further interrupts because interrupts are no longer enabled on that pin. The other pin changed state some time ago and is presumed to be stable.

There's a PDF file in that folder that provides more detail, but what I've found so far is that the standard method can produce a very large number of interrupts - dozens to hundreds - for a single step from one detent to the next. Ideally, it would be four interrupts for a typical encoder. But the standard method deals very successfully with all that bouncing, and almost always comes up with the right result.

In contrast, the alternate method produces far fewer interrupts, but is a bit more error-prone, including producing some false direction ticks. The PDF includes some trial run information on both methods.

I was surprised at how many interrupts might be triggered in the standard method. I don't see any ringing on my scope, but the test Nano is running at 16MHz, so it's possible I'm not seeing what's really going on. Only the internal GPIO pullup resistors are used.

If anyone has an Arduino and an encoder laying around, I'm hoping maybe someone with more expertise than me could take a run at these sketches. No libraries are required.

The attached picture shows my "Minty" single-step encoder which I use to make sure my code produces the correct result for all possible transitions. The Minty produces a consistent run of 4's with the alternate sketch, but with the standard sketch I get this:

52 53 130 73 234 46 15 42 75 30

Assuming my code is ok, I would not have expected the Minty switches to bounce that much. They are genuine Radio Shack switches, not cheap imitations. :slight_smile: But the PDF shows I get similar results with a real encoder. So I'm a bit puzzled by this result, and would like to know if others see the same thing, or if I'm doing something wrong and the number of interrupts reported isn't valid.

I’ve not read all your post but I thought you might like this for inspiration. The code below was written for a PIC, but being C can easily be modified for an Arduino. To work you need to have an interrupt call RPE_read(unsigned char p) every 1 millisecond, this timing is critical, you need to arrange a timer with a 1 millisecond time out to call this function. I would not think it would work very well if you just relied on millis().

Although it is called every millisecond it does not do much, so the overhead is low.

struct RPE_data {
    char index;				//Position index and rotation direction while being read
    float RPE_input_delta;		//Offset from 0 for RPE since last data read
};
struct RPE_data RPE[zone_count];	//Data for each RPE

//Reads rotary position encoders connected to port p and leaves delta in associated RPE_input_delta. 1 change for each encoder step.
void RPE_read(unsigned char p) {
    const char p_mask[4] = {0xfc, 0xf3, 0xcf, 0x3f};
    const char RPE_input_mask[4][4] = {{0xfe, 0xfd, 0xfc, 0xff}, {0xfb, 0xf7, 0xf3, 0xff}, {0xef, 0xdf, 0xcf, 0xff}, {0xbf, 0x7f, 0x3f, 0xff}};
    char p_masked; static char p_last; char i;
    if (p != p_last) {
        p_last = p;
        for (i = 0; i < zone_count; i++) {
            p_masked = p | p_mask[i];
            if (p_masked == 0xff) {
                RPE[i].index = 0;
            }
            switch (RPE[i].index) {
                case 0:
                    if (p_masked == RPE_input_mask [i][0]) {
                        RPE[i].index = 1;
                    }
                    if (p_masked == RPE_input_mask [i][1]) {
                        RPE[i].index = 4;
                    }
                    break;
                case 1:
                    if (p_masked == RPE_input_mask [i][2]) {
                        RPE[i].index = 2;
                    }
                    break;
                case 2:
                    if (p_masked == RPE_input_mask [i][1]) {
                        RPE[i].index = 3;
                        RPE[i].RPE_input_delta++;
                    }
                    break;
                case 3:
                    if (p_masked == RPE_input_mask [i][3]) {
                        RPE[i].index =0;
                    }
                    break;
                case 4:
                    if (p_masked == RPE_input_mask [i][2]) {
                        RPE[i].index = 5;
                    }
                    break;
                case 5:
                    if (p_masked == RPE_input_mask [i][0]) {
                        RPE[i].index = 6;
                        RPE[i].RPE_input_delta--;
                    }
                    break;
                case 6:
                    if (p_masked == RPE_input_mask [i][3]) {
                        RPE[i].index = 0;
                    }
            }
        }
    }
}

As for debouncing, this code does it automatically because of the way it works, that said, when I was developing it I noticed that checking with an oscilloscope showed little or no bounce on the output of the encoders I have.

Using mechanical switches for a rotary encoder is calling for trouble. Are you paid for the time you spend with experiments?

Thanks Perry, but it doesn’t look like your encoders are the type I’m working with. Mine are just the cheap mechanical incremental quadrature encoders. I’m afraid I don’t know what a rotary position encoder is.

I might have used the wrong terminology when I wrote that, they are quadrature encoders, I mean 2 outputs with a square wave 90 degrees out of phase from each other.

DrDiettrich:
Using mechanical switches for a rotary encoder is calling for trouble. Are you paid for the time you spend with experiments?

The mechanical switches just let me single-step through all the transitions to make sure my code produces the right results, particularly for a change in direction.

As for payment, I'm not sure what point you're making, but no, of course I don't get paid for anything here. Does anyone?

Your idea of using toggles to test the software is not a good one. It is not possible to achieve the kind of speeds that you would get from rotating a knob. Testing should be able to explore worst case behaviour.

Why do you think that e.g. light barriers do not perform as well as mechanical switches?