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).
/*
* 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");
}
}
}