Mutiple rotary encoders with shared interruptor

Hi,

I'd like to attach at least 6 rotary encoders to a single Arduino Leonardo - among great many other things.

The mechanical rotary encoders are incremental with 24 PPR and with 24 dents. The datasheet can be found here: PEC16-4220F-S0024
The encoders will be operated by humans, as they were volume knobs.
One and only one encoder will used at a time.

I don't want to poll the digital inputs since the main loop is quite computation heavy (~150ms at its worst) and it would potentially miss a lot of step between two reading.

Since the Leonardo has only 5 interruptable pins, I thought I could share two interruptor pins (A, B) among the encoders, see the image:

(White: A, Black: B, Red: C, Orange: shared interruptors)

It's pretty unortodox, also isn't working.
There's a couple of things:

  • The C pins of the encoders are wired to the +5VC (using INPUT for all the pins) - it was easier to create the logical OR gate from the diodes this way
  • I had to use diodes to prevent an interrupt to travel backward to the other encoders (acting as logical OR)
  • Works alright if only one encoder is hooked in
  • Doesn't work when two encoders are present (the code getting interrupted but the readings on the A/B pins are incorrect)

Goes without saying: but I'm just a beginner with electronics.

Please, if you have any idea how could I wire multiple encoders using only 2 interruptors, or you know what went wront with my pathetic circuit, let me know.

Even if your circuit would have worked, how is the code supposed to know which encoder generated the interrupt?

Yes, this circuit can’t work as one encoder will lock out the other - you’d need XOR gates
to combine interrupts and then use CHANGE in the ISR setup.

If you don’t use delay(), and don’t have any heavy processing to do I dont see why polling
won’t work, loop() should be being called 1 to 1000 times per millisecond in a typical setup,
which is plenty quick enough for human operated switches.

My assumption was that I can read all the digital pins in the interrupt callback to check which one triggered the interrupt.

Let’s say I have two encoders wired as shown on the image in the original post:

  • 1st Encoder: is on PIN8 and PIN9 (for A and B channels)
  • 2nd Encoder: is on PIN10 and PIN11 (for A and B channels)

The pins are also wired to the interrupt pin with that shameless diode thingy I did.
Since only one encoder can be operated at a time, there could be a maximum of 2 pins which’s state differs from its original state - and from the same encoder (for eg.: PIN8 and PIN9 are on LOW now but they were on HIGH previously)

So I keep storing the pins and their states like this (pseudo code just for the sake of it):

#define NumberOfEncoders 2

uint8_t encoderPins[][2] = {
  {8, 9},   // A and B pins of the 1st encoder
  {10, 11}  // A and B pins of the 2nd encoder
};

int8_t lastKnownStates[][2] = {
  {HIGH, HIGH}, // last known A and B pin states of the 1st encoder
  {HIGH, HIGH}  // last known A and B pin states of the 2nd encoder 
};

And updating them on ISR:

void interruptCallback() {
  uint8_t activeEncoderIndex = getActiveEncoderIndex();
  // TODO: act accordingly
  // TODO: update the last known states
}

uint8_t getActiveEncoderIndex() {
  for (uint8_t i = 0; i < NumberOfEncoders && !shouldBreak; i++) {
    for (uint8_t j = 0; j < 2; j++) {  
      
      // Big assumption made here: not sure if it is too late to read the pin
      uint8_t currentState = digitalRead(encoderPins[i][j]); 
      if (currentState != lastKnownStates[i][j]) {
        // We have a winner, it has changed its state
        return i;
      }
    }
  }
}

I made the pretty big assumtion here that I can read the digital pins on interrupt and they will be in the correct state.

For eg.: The 1st encoder’s A pin goes from HIGH → LOW, thus:

  • PIN8 should drop to low too
  • The external interrupted PIN2 should detect the change and trigger the callback
  • I assumed that the PIN8 will be LOW at this point (will be in sync with the PIN2 state)

If that worked, I could identify the affected encoder (given there’s only 1 affected encoder at once).

But I’m not sure if thats the case with the digitalRead in the ISR callback…

If you don’t use delay(), and don’t have any heavy processing to do I dont see why polling won’t work

My loop is a bit slow as of now. I’m updating 8 pieces of TM1637-M4 four digit display which takes something like 80-120ms in total. I was honestly surprised how terribly slow it is.

How have you set up the interrupts? Are they Change, Rising or what? The polarity you've chosen suggest the interrupts should be Rising?

It's not guaranteed the pin will read correctly after the interrupt. It usually will, but not always, because the switches bounce. To cut down the time that passes between the interrupt and the reads, you might look at just reading a port - that's assuming both interrupt pins and all four data pins are in the same port.

You haven't said anything about the logic you will use to interpret the pin changes. Do you need both switches to interrupt, or would just one do it?

From my experience, you have everything backward as to polarity. But it should work the way you have it so long as you have a Rising interrupt option. Well, I guess I need to think about that some more. Switch bounce kinda complicates things.

What I would suggest is that you take the encoders out of the circuit, and replace them with four jumpers - one for each switch. Then you can connect one jumper to Vcc at a time and see what the voltages are, and what your code does. In other words, slow everything way down. Maybe that will show what is going wrong.

That is a very good observation which I ignored/hadn't realized so far! The trigger-mode of the interruptor (CHANGE / FALLING / RISING ...) could be very much a reason for a misread digitalRead!

But first, I'd like to address this:

From my experience, you have everything backward as to polarity.

Yes, it seems so, but with a (not very good) reason. Also it is something that concerns me, so please correct me if I'm wrong with the following reasoning:

As I understood, the regular way to use an encoder is the following:

  • C (Common) pin should be on the common ground
  • The A and B pins should be wired with pull-up resistors (in my mind it's like a normally closed circuit/switch)
  • The interruptor trigger-mode (if it is not a polling implementation) should be CHANGE

My first single-encoder solution worked perfectly just like that, but when I tried to add a second encoder to the same interrupt pins I faced the problem of signal-merging.

I found it easier to simply reverse this normally-closed setup to a normally-open one and use an OR gate with 2 diodes. I was hoping that there would be no serious side-effects or drawbacks to this backward-polarity.

How have you set up the interrupts?

As CHANGE - as far as I know that will trigger both on RISING and on FALLING which is ideal because I need to keep the pin states up-to-date (from LOW -> HIGH and vice versa). And, to be frank it looked like a fool-proof solution because I went with a state machine implementation - worst case scenario: I detect something which is not interesting/not allowed as a next step in the state machine.

But it should work the way you have it so long as you have a Rising interrupt option.

You mean RISING at least? So the CHANGE should do it too? Or it must be RISING, and if it must be, then why do they suggest to use CHANGE in the normal case (where C is GND) and not FALLING?

Do you need both switches to interrupt, or would just one do it

I'm afraid I need both. The polling is pretty inaccurate with my slow loop, and the final device should use no less than 4 encoders. The problem I'd like to solve is monitoring and adjusting/fine tuning some parameters real time so I can't say that there's a "primary" knob and a "secondary".
I'm using the 2 encoder example to simplify the problem as much as I can.

take the encoders out of the circuit, and replace them with four jumpers [..] slow everything way down

That's a great idea! I'll do some testing like this.

The polarity of the setup shouldn't really matter, particularly if you're using Change. I think the normal way you see it with the switches grounding the line when they turn on is because many processors only have internal pullup resistors, not pulldown. Anyway, your circuit would still work if you changed the rail to which each resistor and C pin goes, and reversed the orientation of the diodes. So I guess I don't see why you felt that wouldn't have worked. But maybe I'm missing something.

Your encoder goes through a complete cycle of both switches between each detent, and always ends up with both swithes open at a detent. If we had perfect switches, decoding it could be as simple as triggering a Rising interrupt when A goes high, then reading the state of B to tell you which direction it was. None of the other transitions would matter. But we don't have perfect switches - they bounce.

So from what you say, it seems you are using the state machine method of decoding, which is normally very good at dealing with bounce. But another approach, which would produce fewer interrupts to be serviced, is to turn off the interrupt on the pin that produced the interrupt (in the ISR itself), and enable the interrupt only on the other pin, which at that point should be stable. The idea is to avoid interrupts on all the subsequent bouncing on the pin that just transitioned, hoping that it will have settled down by the time the other pin changes.

Well, this can all get pretty complicated. I suspect the problem is in your code. I don't see any reason why the circuit wouldn't work so long as only one encoder can be turning at a time.

By the way, the Leonardo has 5 external interrupt pins, each with its own interrupt vector, and configurable as Low, Falling, Rising, or Either edge. But it also has all 8 bits of Port B which can be configured as pin change interrupts. One interrupt vector handles all eight bits, and a flags registrer tells you which one interrupted. The coding would be different for these, but using them would give you a total of 13 pins which could be used for the encoder lines, and you wouldn't need to double up with the diodes. Interrupts can be individually enabled or disabled through a mask register. It's all in the datasheet. I believe all bits of Port B sre brought out, but three of them are part of the programming header, and PB0 is somewhere that I can't figure out.

Edit: I was wrong about the flags register telling you which pin interrupted. You would have to read the port and XOR that value with the previous value to see which one changed.