Yes, that's the best way for user-operated knobs. They are very slow.
I guess I am using a 24 position encoder with detent, so precision is already about as high as usable for human interaction. I probably misspoke about the information to be had from the bouncy pin- with what I am doing, the bouncy pin still cannot be read reliably, but one can deduce its state by knowing its last state (which was a stable read). So the bouncy pin state can be deduced by simply inverting its stored last state value in its isr (we are in its isr because the pin changed).
const int aPIN = 4; // encoder pins
const int bPIN = 5;
/* This section defines the port being used for the encoder pins, along with the PCI registers
and interrupt vector. The values shown here are for Port D (D0 - D7), but Port B (D8 - D13)
or Port C(A0 - A5) could also be used.
*/
#define pinZero 0 // data pin name of PD0 (0). PB0 (8) PC0 (A0) for ports B and C
#define PORT PIND // port input register (port D includes D4 and D5)
#define portVECT PCINT2_vect // The ISR vector for port D pin change interrupt
#define portINT PCIE2 // used to enable pin change interrupts on port D as a whole
#define ENABLE PCMSK2 // individual pin interrupt enabled = 1
#define FLAG PCIF2 // only one flag for whole port
const byte encoderType = 0; // encoder with equal # of detents & pulses per rev
//const byte encoderType = 1; // encoder with pulses = detents/2. pick one, commment out the other
const int THRESH =(4-(2*encoderType)); // transitions needed to recognize a tick - type 0 = 4, type 1 = 2
const byte CWPIN = bit(aPIN - pinZero); // bit value for switch that leads on CW rotation
const byte CCWPIN = bit(bPIN - pinZero);// bit value for switch that leads on CCW rotation
const byte PINS = CWPIN + CCWPIN; // sum of bit values of the encoder pins
const byte ZEERO = 0x80; // "byte" data type doesn't do negative, so make 128 = zero
byte EDGE; // the edge direction of the next pin change interrupt
byte CURRENT; // the current state of the switches
byte TOTAL = ZEERO; // accumulated transitions since last tick (0x80 = none)
byte INDEX = 0; // Index into lookup state table
int Setting = 0; // current accumulated value set by rotary encoder
volatile byte tickArray[256]; // circular buffer of ticks
volatile byte beginTICK = 0; // pointers to beginning and ending of circular buffer
volatile byte endTICK = 0;
// The table is now 32 bytes long so as to include the identity of the pin currently interrupting.
// The +2 and -2 entries are for a change of direction.
int ENCTABLE[] = {0,1,0,-2,-1,0,2,0,0,2,0,-1,-2,0,1,0,0,0,-1,2,0,0,-2,1,1,-2,0,0,2,-1,0,0};
void setup() {
Serial.begin(115200);
pinMode(aPIN, INPUT); // set up encoder pins as INPUT. Assumes external 10K pullups
pinMode(bPIN, INPUT);
EDGE = PINS; // identifies next pin-change interrupt as falling or rising
// assume current state is low, so any change will be rising
if(PORT & PINS) { // but if actual current state is already high,
EDGE = 0; // make EDGE low
INDEX = 3; // and make "current" bits of INDEX match the current high state
}
ENABLE |= CWPIN; // enable only CWPIN interrupt in mask register
PCIFR |= bit(FLAG); // clear interrupt flag if any
PCICR |= bit(portINT); // enable interrupts on Port D
}
void loop() {
if(beginTICK != endTICK) { // if anything in circular buffer
beginTICK++;
if(tickArray[beginTICK] == 1) Setting ++;
else Setting--;
Serial.println(Setting);
}
}
ISR (portVECT) { // pin change interrupt service routine. interrupts
// automatically disabled during execution
CURRENT = PORT & PINS; // read the entire port, mask out all but our pins
/* Time passes between the interrupt and the reading of the port. So if there is bouncing,
the read value of the interrupting pin may be wrong. But we know it must be the EDGE state.
So in CURRENT, we clear the bit that just caused the interrupt, and replace it with the EDGE
value. The non-interrupting pin is assumed to be stable, with a valid read. */
CURRENT &= ~ENABLE; // clear the bit that just caused the interrupt
CURRENT |= (EDGE & ENABLE); // OR the EDGE value back in
INDEX = INDEX << 2; // Shift previous state left 2 bits (0 in)
if(CURRENT & CWPIN) bitSet(INDEX,0); // If CW is high, set INDEX bit 0
if(CURRENT & CCWPIN) bitSet(INDEX,1); // If CCW is high, set INDEX bit 1
INDEX &= 15; // Mask out all but prev and current. bit 4 now zero
if(ENABLE & CCWPIN) bitSet(INDEX,4); // if CCWPIN is the current enabled interrupt, set bit 4
// INDEX is now a five-bit index into the 32-byte ENCTABLE state table.
TOTAL += ENCTABLE[INDEX]; // Accumulate transitions
if((CURRENT == PINS) || ((CURRENT == 0) && encoderType)) { // A valid tick can occur only at a detent
// If we have a valid number of TOTAL transitions at a detent, add the tick direction
// to the circular buffer. The MAIN loop will update the Setting value and print it.
if(TOTAL == (ZEERO + THRESH)) {
endTICK++;
tickArray[endTICK] = 1;
}
else if(TOTAL == (ZEERO - THRESH)) {
endTICK++;
tickArray[endTICK] = 0xFF;
}
TOTAL = ZEERO; // Always reset TOTAL to 0x80 at detent
}
if(CURRENT == EDGE) EDGE ^= PINS; // if both pins have reached EDGE state, switch EDGE to opposite
ENABLE ^= PINS; // always switch interrupt to other pin
PCIFR |= bit(FLAG); // clear flag if any by writing a 1
} // end of ISR - interrupts automatically re-enabled
SPI bus chaining, I have seen it in some advanced SPI use cases with the MOSI leaves the master to the first slave, which at the same time, in it's slot sends the last buffer out it's while receiving the current on the MISO. Each in turn along the line do this. I can see this means you only need 1 SS line. It also adds latency.
Tangent question, but what is the next logical step up from SPI? I know there are parallel GPIO connection approaching, but is/are there many established/standard protocols for linking multiple devices more tightly that SPI?
What other options lie between SPI and PCI/ISA or custom full bus?
Yes, that's the best way for user-operated knobs. They are very slow.
Tell that to the nearly every designer of rotary encoder UIs that poll too slowly. There is very little that bothers me more than using a $200,000 VNA or a $50,000 car and spinning the menu knob only to have the cursor move backwards. That isn't really acceptable on $5 IoT crap, but it's pervasive on devices in every class.
That's not to say that you can't do it with polling, only that polling too slow is pervasive, and most advice on the topic does not address this.
Yes, that's the best way for user-operated knobs. They are very slow.
Tell that to the nearly every designer of rotary encoder UIs that poll too slowly. There is very little that bothers me more than using a $200,000 VNA or a $50,000 car and spinning the menu knob only to have the cursor move backwards. That isn't really acceptable on $5 IoT crap, but it's pervasive on devices in every class.
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.
Yes, that's the best way for user-operated knobs. They are very slow.
Tell that to the nearly every designer of rotary encoder UIs that poll too slowly. There is very little that bothers me more than using a $200,000 VNA or a $50,000 car and spinning the menu knob only to have the cursor move backwards. That isn't really acceptable on $5 IoT crap, but it's pervasive on devices in every class.
This is not because rotary encoders in the knobs are overly fast. Those people have RTOS, gazillion of interrupts which they often disable with and without need, use high levels of abstraction, "don't reinvent the wheel", "fast way to market", and all that. They have no idea what their timing is. That's why you see skipped points.
Marking 0 on the knob with a dot then going apeshit on the encoder, randomly turning it and flicking it as fast as i can..etc then turning it back to the marked 0 and checking the count is 0
You really start realizing you're doing it wrong when using encoders with detent. Those without detent are a lot more forgiving as users don't have the same expectations with these.
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.
Yes, that's the best way for user-operated knobs. They are very slow.
Tell that to the nearly every designer of rotary encoder UIs that poll too slowly. There is very little that bothers me more than using a $200,000 VNA or a $50,000 car and spinning the menu knob only to have the cursor move backwards. That isn't really acceptable on $5 IoT crap, but it's pervasive on devices in every class.
This is not because rotary encoders in the knobs are overly fast. Those people have RTOS, gazillion of interrupts which they often disable with and without need, use high levels of abstraction, "don't reinvent the wheel", "fast way to market", and all that. They have no idea what their timing is. That's why you see skipped points.
36 points per revolution rotated by hand? If you think this is fast, I don't know what is slow. Even PIC16 can handle several of these and never skip a point, and this will consume only a fraction of the CPU time.
Compare this to a 1000-point encoder on a shaft of a motor rotating at 1500 rpm. See the difference?
For this reason, I suggested the use of Microchip microcontrollers with integrated Configurable Logic Cells. See how it looks on an example
The only requirements to implement the quadrature decoder (with no physical outputs or indicators) are 2 CLCs, TMR1, TMR2, and TMR3.
You really start realizing you're doing it wrong when using encoders with detent.
You really start realizing you're doing it wrong when using encoders with detent. Those without detent are a lot more forgiving as users don't have the same expectations with these.Using an encoder with a detent isn't bad perse but expecting not to miss a step is. IMHO any decent encoder implementation has proportional speed. So the faster you turn it, the bigger the steps it takes. This means that the user never has to turn it really fast to begin with and thus the software doesn't need to support crazy high speeds.
QuoteFor this reason, I suggested the use of Microchip microcontrollers with integrated Configurable Logic Cells. See how it looks on an exampleYou only need to give up 2 CLC's and 3 timers-QuoteThe only requirements to implement the quadrature decoder (with no physical outputs or indicators) are 2 CLCs, TMR1, TMR2, and TMR3.Any timers left to anything else?
You really start realizing you're doing it wrong when using encoders with detent. Those without detent are a lot more forgiving as users don't have the same expectations with these.Using an encoder with a detent isn't bad perse but expecting not to miss a step is. IMHO any decent encoder implementation has proportional speed. So the faster you turn it, the bigger the steps it takes. This means that the user never has to turn it really fast to begin with and thus the software doesn't need to support crazy high speeds.
This is exactly what I am ranting about. Tweaking acceleration parameters on an encoder that can't be relied on to count properly is dumb.
If I can turn the encoder knob by hand fast enough that it looses pulses, your product is defective.
The shaft encoder is recognized as "fast" so people do it correctly.
The other two could, again be done with 1. Rotate to highlight parameter, press to select, rotate to change, press to commit. I still think it would be much easier, faster to use with 2. I have 2 hands for a start!