Author Topic: Having many rotary encoders.  (Read 15176 times)

0 Members and 2 Guests are viewing this topic.

Offline langwadt

  • Super Contributor
  • ***
  • Posts: 5417
  • Country: dk
Re: Having many rotary encoders.
« Reply #75 on: January 31, 2023, 12:35:00 am »
A seperate select button is an option but if you have proportional accelleration (here I go again, I know), you can use an encoder with a lower resolution. This makes it far less likely to turn the encoder enough to change a parameter while you press it.


I guess you could get clever and keep a history of positions and when pressed rollback movement that happened right before a press
 

Offline SiliconWizard

  • Super Contributor
  • ***
  • Posts: 17049
  • Country: fr
Re: Having many rotary encoders.
« Reply #76 on: January 31, 2023, 12:39:46 am »
Very noisy: that's why I usually implement an analog filter, just like what they recommend in 99.9% of encoders' datasheets. The rest if of course a good decoding - there are several ways of decoding and some are definitely worse than others.

On a more general level, the key here is to test, test, test and make sure your implementation will be robust. There are many ways to skin the cat here, as long as it gets the job done.

As a few have mentioned in this thread, as trivial as some make it sound, faulty implementations abound everywhere, so please test before releasing a product - and don't take "looks ok on my desk" as acceptable. Just my 2 cents. ::)
 

Offline Peabody

  • Super Contributor
  • ***
  • Posts: 2548
  • Country: us
Re: Having many rotary encoders.
« Reply #77 on: January 31, 2023, 12:46:23 am »

Indeed.  Just for future reference, below is an Arduino sketch that keeps track of the pins' states, so it doesn't need to trust a read of the interrupting pin when it might be bouncing.  And once a pin interrupts, further interrupts on that pin are disabled, but enabled on the other pin, which should be stable, so bouncing should not trigger interrupts.

The problem with that assumed quadrature flow, is it will not ignore 'wiper noise' bounce properly.
Also, systems that assume a state engine will be always ok (ie bounce cancels), also presumes the state engine nodes not miss any edges.

I agree that when the supposedly stable switch is closed, but actually has two conductive surfaces dragging against each other, there can be noise.  But I've tested my code on some KY-040 encoder modules, which are just cheap stuff from the Far East, and it works quite well with them.  They aren't perfect, but errors are pretty rare.  And if those modules work ok, then a good encoder ought to be pretty foolproof.  If an encoder is noisy enough, it isn't going to work with any software or hardware because at some point you just can't distinguish noise from legitimate transitions.  But in my experience a reasonably well-behaved encoder gives satisfactory performance when using a good servicing routine, whether with interrupts or polling.

Quote
The best design approach seems to be a combination of HW and SW, where the hardware filters the pins so there is a firm lower limit to the edge to edge time (RC filter and schmitt does that ) and the SW is guaranteed to always follow up to that speed.

If you have the budget and board space for the hardware part, that's fine.  But I think for most uses software alone works well enough.  Maybe it's just that my expectations for a mechanical encoder are just not as high as others'.  If you spin the knob as fast as you can, how do you know whether it missed some detents or not, and why would you care?  If you insist that you should always get the same value when you return the knob to the same place, no matter how you abuse it, I think that's asking too much of a mechanical rotary encoder.  It should just give good results if you turn it at a reasonable speed, with infrequent misses.

 

Offline NorthGuy

  • Super Contributor
  • ***
  • Posts: 3391
  • Country: ca
Re: Having many rotary encoders.
« Reply #78 on: January 31, 2023, 02:12:24 am »
The contacts can be very noisy.  It might be impossible to preserve the absolute position alignment without continuously estimating the expected duration of the quadrature pulses.

Looking at the pictures, all the noise is in the low state. I guess there's a weak pull-down keeping it low. If so, a stronger pull-down would eliminate the noise.
 

Offline RoGeorge

  • Super Contributor
  • ***
  • Posts: 7891
  • Country: ro
Re: Having many rotary encoders.
« Reply #79 on: January 31, 2023, 03:10:48 am »
Low for that encoder was when a contact is closed.  When the contact is open, there is a 10k pull up resistor.  The noise during a low level means the contact is not firmly closed to GND.

That is visible when the encoder is turning very, very fast.  That model has 20 clicks per full turn and did them in about 25ms (it was turning at 2400 RPM :scared:).

Offline pqass

  • Super Contributor
  • ***
  • Posts: 1087
  • Country: ca
Re: Having many rotary encoders.
« Reply #80 on: January 31, 2023, 03:36:23 am »
If anyone is interested...
here is my final version of my rotary encoder debouncer code. It's fairly simple and works well. It can sense turning speed such that faster turns change by 5 steps instead of 1.  It only needs a 1KHz timer interrupt (no pin change interrupts) to support as many encoders as you'll want; just add an encoders[] array element and call UpdateEncoder() with your encoder switch-MCU pin mapping in the timer interrupt.

Athough I had only one physical encoder in this test, I used its data 10 times as if 10 encoders existed (with the same switch changes occurring). It consumes worst case 1.75% of available MCU time per encoder or 17.5% for 10 encoders (ATMega328@16MHz).  I'm sure it'll go unnoticed on >100MHz ARM MCU.

See the waveforms below for the overall and zoomed in views. YELLOW/RED are the debounced quadrature output which shows that it can keep up with 2.7 turns/second (15ms pulse period on a 24 pulse/rev encoder).  Zoomed-in it shows the BLUE in-interrupt time with GREEN blips indicating when the an encoder's current value integer has changed (in responce to cw/ccw rotation).

Code: [Select]
/*
 * Encoder Debounce Test
 *
 * Debounce testing of 10 rotary encoders (with momentary push button).
 * Each encoder maintains a .value integer which is inc/decremented (turned cw/ccw).
 * We sense turning speed such that faster turns change .value by 5 instead of 1.
 * Using a 1KHz timer interrupt and no pin change interrupts, polling and debouncing all 10 encoder pins
 * has a worst case load of 17.5% of MCU time on a 16MHz ATMega328 (1.75% per encoder).
 * Idle load (no switch changes) is 14.1%.
 * The encoder (Bourns PEC11-4015K-S0024) used in this test was mechanical with 24 pulses per rotation,
 * momentary push switch, and no detents.
 *
 *
 * Author: PQASS
 * Date: Jan 30, 2023
 *
 * Hardware:
 *  ATmega328; Arduino UNO with 16MHz xtal
 *  output pins: PB5, PB4, PB3, PB2; these are optional for debugging (viewing via o-scope). they represent:
 *               one debounced switch state, the other debounced switch state,
 *               in-interrupt indicator, when encoder value is changed indicator, respectively.
 *  input  pins: PD7, PD6, PD5 with internal pullup to Vcc;
 *               encoder switches connecting to ground when rotated or momentary is pressed.
 *               we only have one physical encoder in this test but we use the same data
 *               10 times as if 10 encoders existed (with the same switch changes occurring).
 *               multiplexing/routing 10 real encoders is left as an exercise for the diligent student.
 *
 * IDE Tools menu settings:
 *  Board: "Arduino/Genuino UNO 16Mhz"
 *  Port: "/dev/ttyACM0"
 *  Programmer: via optiboot bootloader
 *  Fuses: default for Arduino UNO
 */

#define DEBUG 1

void timer_setup() {
  cli();
  TCCR1A = 0;
  TCCR1B = 0;
                          // 16000000 / <prescaler> = x    then set OCR1A = x / <desired Hz>
  OCR1A  = 14;            // 15624=1s 78=4.99ms 7=448us 14=896us with 1024 prescaler
  TCNT1H = 0;             // must clear count otherwise we'll wait till it overflows before the first interrupt will occur!
  TCNT1L = 0;
  TCCR1B |= _BV(WGM12);   // turn on CTC mode
  // Set CS10 and CS12 bits for 1024 prescaler
  TCCR1B |= _BV(CS10);    // set CS10
  TCCR1B &= ~_BV(CS11);   // clear CS11
  TCCR1B |= _BV(CS12);    // set CS12
  TIMSK1 |= _BV(OCIE1A);  // enable timer compare interrupt
  sei();
}

// see bottom of this page: [url]http://www.ganssle.com/debouncing-pt2.htm[/url]
// state is changed only after 3.6ms passes (4 checks * 900us timer interval) after the last time the switch changed state.
bool DebouncePin(uint8_t current_pin_state, volatile uint8_t *tmpp, volatile uint8_t *statep) {
  uint8_t hasChanged = false;
  *tmpp = *tmpp << 1 | (current_pin_state & 0x01);
  if ((*tmpp|0xf0) == 0xff) {
    if (*statep != HIGH) {
      *statep = HIGH;
      hasChanged = true;
    }
  } else if ((*tmpp&0x0f) == 0x00) {
    if (*statep != LOW) {
      *statep = LOW;
      hasChanged = true;
    }
  }
  return hasChanged;
}

// tickle pins to signal ep->value has changed, "in-interrupt" indicator.
#ifdef DEBUG
#define CHANGE_IND      PORTB |= _BV(PB2); PORTB &= ~_BV(PB2)
#define ENTER_INTR_IND  PORTB |= _BV(PB3)
#define LEAVE_INTR_IND  PORTB &= ~_BV(PB3)
#else   /* DEBUG */
#define CHANGE_IND
#define ENTER_INTR_IND
#define LEAVE_INTR_IND
#endif  /* DEBUG */

struct debounce_t {
  uint8_t tmp;      // holds last n states of bouncing switch (most current in lsb)
  uint8_t state;    // debounced state of switch
};

struct encoder_t {
  struct debounce_t sw[3];  // encoder pinA sw, pinB sw, and momentary push button
  uint16_t value;           // cw debounced state increments, ccw debounced state decrements
  unsigned long chgms;      // last time (in millis()) that .value was changed
};

static volatile struct encoder_t encoders[] = {
  // add one line per [3 switch] encoder; .state initialized to inactive state (pulledup to Vcc).
  { .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 },
  { .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 },
  { .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 },
  { .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 },
  { .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 },
  { .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 },
  { .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 },
  { .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 },
  { .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 },
  { .sw = { { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH }, { .tmp = 0, .state = HIGH } }, .value = 0, .chgms = 0 }
};

void UpdateEncoder (uint8_t sw0, uint8_t sw1, uint8_t sw2, unsigned long currms, volatile struct encoder_t *ep) {
  volatile uint8_t *tp0 = &(ep->sw[0].tmp);
  volatile uint8_t *sp0 = &(ep->sw[0].state);
  volatile uint8_t *tp1 = &(ep->sw[1].tmp);
  volatile uint8_t *sp1 = &(ep->sw[1].state);
  volatile uint8_t *tp2 = &(ep->sw[2].tmp);
  volatile uint8_t *sp2 = &(ep->sw[2].state);
  if (DebouncePin(sw0, tp0, sp0)) {
    if (*sp0 == HIGH && *sp1 == LOW )  {
      // fast turns change value by 5 instead of 1.
      if ((currms - ep->chgms) < 30) ep->value+=5; else ep->value++;
      ep->chgms = currms;
      CHANGE_IND;
    }
  }
  if (DebouncePin(sw1, tp1, sp1)) {
    if (*sp0 == LOW  && *sp1 == HIGH)  {
      // fast turns change value by 5 instead of 1.
      if ((currms - ep->chgms) < 30) ep->value-=5; else ep->value--;
      ep->chgms = currms;
      CHANGE_IND;
    }
  }
  DebouncePin(sw2, tp2, sp2); // momentary
}

ISR(TIMER1_COMPA_vect) {    // called 1100 times per second; every 900us; see timer_setup()
  ENTER_INTR_IND;

  unsigned long currms = millis();
  uint8_t data = PIND;

  // 'scope readings; worst case duration of interrupt (every 1040Hz)
  //                  processing one switch change for each of the 10 encoders being updated
  //     113us when no switch was changed (w/o  speed sensing).            113us*1040Hz*100 = 11.7% of MCU time used
  //     131us when a switch per encoder was changed (w/o  speed sensing). 131us*1040Hz*100 = 13.6% of MCU time used
  //          12.8us processing time per encoder + 3us interrupt call overhead.
  //     136us when no switch was changed (with speed sensing).            136us*1040Hz*100 = 14.1% of MCU time used
  //     169us when a switch per encoder was changed (with speed sensing). 169us*1040Hz*100 = 17.5% of MCU time used
  //          16.4us processing time per encoder + 5us interrupt call overhead.

  // normally, you'd get the data from different pins
  //    or multiplex 5 "rows" of 2 [3 switch] encoders on the same input port (or any other mapping).
  //    but we only have one physical encoder in this test so we use the same data
  //    10 times as if 10 encoders existed (with the same switch changes occurring).
  UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders[0]);
  UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders[1]);
  UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders[2]);
  UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders[3]);
  UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders[4]);
  UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders[5]);
  UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders[6]);
  UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders[7]);
  UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders[8]);
  UpdateEncoder(data>>PD7, data>>PD6, data>>PD5, currms, &encoders[9]);

  LEAVE_INTR_IND;
}

void setup() {
  DDRD  &= ~(_BV(PD7)|_BV(PD6)|_BV(PD5));   // enable input on encoder pins.
  PORTD |=   _BV(PD7)|_BV(PD6)|_BV(PD5);    // enable internal pull-up on input pins.

#ifdef DEBUG
  DDRB  |= _BV(PB5);   // enable output on LED pin (Arduino pin 13).
  PORTB &= ~_BV(PB5);  // turn off LED. LED shows debounced rotary encoder movement.
  DDRB  |= _BV(PB4);   // enable output on another pin for the other encoder switch.
  PORTB &= ~_BV(PB4);  // turn off pin. pin shows debounced rotary encoder movement.
  DDRB  |= _BV(PB3);   // enable output.
  PORTB &= ~_BV(PB3);  // default is cleared. shows when control is in the timer interrupt routine.
  DDRB  |= _BV(PB2);   // enable output.
  PORTB &= ~_BV(PB2);  // default is cleared. shows when control has changed .value.
#endif  /* DEBUG */

  Serial.begin(38400);

  timer_setup();   // should be last to setup since it starts the timer interrupt.
}

unsigned long prevMillis = 0;
unsigned long currMillis = 0;

void loop() {
  volatile struct encoder_t *ep = &encoders[0];

#ifdef DEBUG
  // check switch state var anywhere in your code.
  // it reflects the debounced state of one encoder switch at any moment.
  // but you don't really need this as your code would just use .value.
  // it's really only useful to check momentary switches' state.
  if (ep->sw[0].state == HIGH) {
    PORTB |= _BV(PB5);      // turn on an LED as a response to the debounced switch.
  } else if (ep->sw[0].state == LOW) {
    PORTB &= ~_BV(PB5);     // turn off an LED as a response to the debounced switch.
  }
  if (ep->sw[1].state == HIGH) {
    PORTB |= _BV(PB4);      // turn on an LED as a response to the debounced switch.
  } else if (ep->sw[1].state == LOW) {
    PORTB &= ~_BV(PB4);     // turn off an LED as a response to the debounced switch.
  }
#endif  /* DEBUG */

  currMillis = millis();
  if (currMillis > (prevMillis + 250)) {  // print quantity value and momentary sw status every quarter-second
    prevMillis = currMillis;
    Serial.print(ep->value);
    if (ep->sw[2].state == HIGH) {
      Serial.println(" r");
    } else if (ep->sw[2].state == LOW) {
      Serial.println(" p");
    }
  }
}
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf