How to properly code rotary encoder - Page 1
Forum section: electronics / general

author: strawberry
author_rank: Super Contributor
timestamp: December 23, 2019, 09:15:02 pm
content: Don't know why this code in Arduino works fine but not in plain c/c++Inputs are clean.Code:char rotary_enc(){unsigned char State1;unsigned char State2;unsigned char State1_prev;unsigned char State2_prev;State1=(PINB & (1 << Enc1));State2=(PINB & (1 << Enc2));if(State1 == State2 ){State1_prev = State1;State2_prev = State2;return 0;}else{if(State1_prev == 1 ){if (State2 == 0){return -1;}else {return 1;}}if (State1_prev == 0 ){if (State2 == 1){return 1;}else {return -1;}}}

author: andersm
author_rank: Super Contributor
timestamp: December 23, 2019, 09:37:43 pm
content: What do you meanby not working?

author: SiliconWizard
author_rank: Super Contributor
timestamp: December 23, 2019, 09:39:00 pm
content: Quote from: strawberry on December 23, 2019, 09:15:02 pmMe neither. State1_prev and State2_prev are declared as plain (non-static) local variables, and won't retain their value across calls of this function. I have no clue what would make it appear to work in "Arduino", but it's certainly not a behavior you can count on.

author: GromBeestje
author_rank: Frequent Contributor
timestamp: December 25, 2019, 09:50:18 am
content: How do you initialise your pins in arduino vs normal code? Are they configured as input?

author: Siwastaja
author_rank: Super Contributor
timestamp: December 25, 2019, 09:56:53 am
content: Add-Wallto the compiler command line (assuming GCC) to emit a warning of reading an uninitialized variable, and possibly other helpful warnings to catch mistakes.

author: Renate
author_rank: Super Contributor
timestamp: December 27, 2019, 04:07:27 pm
content: Erm, this code is pretty bad.Starting with the smallest problem, you aren't sampling the two phase at the same time, i.e. the same instruction.Okay, on a gazillion Hertz processor that hardly makes a difference, but it is sloppy.Oh, and you're only getting half the transitions, half the resolution.Code:state=(PINB>>whatever)&3; // get two bits, presuming that they are adjacent.state=(state>>1)^state; // convert from Gray code to binary code so we can do useful thingsdelta=(state-oldstate)&3; // we can do simple math since it's all binaryoldstate=state; // update, since we got what we wanted, deltaswitch(delta){case 0: break; // no changecase 1: break; // move forward onecase 3: break; // move backward onecase 2: break; // Hmm, we seemed to have missed polling fast enough.// We either moved forward or backwards two.// If we had kept track of which direction we were going before we could probably guess which one.Of course, we also want to add a bit of backlash so that a single transition (or contact bounce) won't change anything.

author: bolting.gnome
author_rank: Newbie
timestamp: December 28, 2019, 10:49:55 pm
content: Couple of things1. Lack of static on XXXX_prev variables2. XXXX_prev variables not initialised (probably should be 0)3. I suspect the Arduino was using bool rather than unsigned char so reading State1/2 should be something like thisState1=(PINB & (1 << Enc1)) != 0;State2=(PINB & (1 << Enc2)) != 0;otherwise State1 will never be equal to State2 unless Enc1 and Enc2 are both 0 which makes no sense for the obvious reason you are trying two read to separate inputs.

author: Jan Audio
author_rank: Frequent Contributor
timestamp: December 29, 2019, 03:51:17 pm
content: Quote from: Renate on December 27, 2019, 04:07:27 pmJust code it in the loop instead of interrupts, if it is slow enough you dont need this.

author: sokoloff
author_rank: Super Contributor
timestamp: December 29, 2019, 04:25:15 pm
content: Quote from: Renate on December 27, 2019, 12:07:27 pmFor a quadrature encoder, you don't need to worry about contact bounce.Any contact bounce between two logical states will just toggle the logical position between those two states.In the stream of bits above, you want to change the counter as follows on the state transitions:ABChangeHighRises+1LowFalls+1FallsHigh+1RisesLow+1HighFalls-1LowRises-1RisesHigh-1FallsLow-1Imagine the user is turning the encoder clockwise, A and B both start High. The first transition that can happen is a falling edge on A. If A bounces during this transition, each bounce down will +1 the counter and each bounce up will -1 the counter. When A finally settles Low, the counter will be increased by 1.

author: Renate
author_rank: Super Contributor
timestamp: December 30, 2019, 01:15:22 am
content: Quote from: sokoloff on December 29, 2019, 04:25:15 pmWell, ok, if you don't want to.If we are talking about the standard twiddle knob...They have 24 detents per rotation, 15 degrees a detent.For each click you have 4 transitions.Yes, of course +1-1+1-1+1-1+1-1+1-1+1-1+1-1+1-1+1-1+1-1+1-1+1-1+1-1+1-1+1-1+1-1+1-1+1=1The problem comes if you react to each transition.Do you really want to update your OLED from "Apples" to "Bananas" and back in a flurry of updates as a user slowly turns a (gratchy) knob between two indents?I add hysterisis (or backlash) of two transitions, that is, a fraction of a detent.It works fine for me.

author: bingo600
author_rank: Super Contributor
timestamp: December 30, 2019, 10:26:51 am
content: Peter D made som nice routines for avr (Use google xlate - German)https://www.mikrocontroller.net/articles/Drehgeber#Beispielcode_in_CAdditionalhttps://www.mikrocontroller.net/topic/112603#newhttps://www.mikrocontroller.net/topic/drehgeber-auslesen#new/Bingo

author: chris_leyson
author_rank: Super Contributor
timestamp: December 30, 2019, 11:30:14 am
content: If you want to use pin change interrupts then there is a good article by Oleg Mazurov, here:-https://chome.nerpa.tech/mcu/reading-rotary-encoder-on-arduino/And a follow up article here:-https://chome.nerpa.tech/mcu/rotary-encoder-interrupt-service-routine-for-avr-micros/I've used it on PIC and AVR microcontrollers without any problems.

author: ehughes
author_rank: Frequent Contributor
timestamp: December 31, 2019, 06:31:51 pm
content: https://www.mkesc.co.uk/ise.pdfThese is the cleanest way to read an encoder. Simple, effective and tolerant to about anything you can throw at it.

author: newbrain
author_rank: Super Contributor
timestamp: January 01, 2020, 12:52:10 pm
content: I normally use, for "full step" encoders, an FSM based approach.It's not - if you look at it squinting a bit - very different from the one ehughes suggested...It works very well, both in polling and interrupt mode - even without any HW debouncing.I generally use it polling, wrapped by code for velocity implementation.Code:// Read a full step encoder with negative logic// Based on a simple state machine// Licence: WTFPL - http://www.wtfpl.net/typedef enum{ENCIDLE = 0,CW__INI,CW__MID,CW__END,CCW_INI,CCW_MID,CCW_END,ENC_ERR,CW__OUT = 0x10u, /* The actual state is IDLE, action encoded in high nibble */CCW_OUT = 0x20u, /* The actual state is IDLE, action encoded in high nibble */} State;#define ENC_STATE_MASK 0x0Fu/** As the new state for the encoder FSM depends only on the phase inputs and* the previous state, a simple state table is enough.* Clockwise step input sequence: 11, 10, 00, 01, 11, 11 is rest position* CounterCW step input sequence: 11, 01, 00, 10, 11* Unexpected transitions goto an error state, ENC_ERR, then to idle, ENC_IDLE.* The error state persists until a 11 is read (return to rest position).* If the unexpected transition is to the rest position, ENC_ERR is skipped.**/unsigned char const nextState = {/* Previous stateENCIDLE CW__INI CW__MID CW__END CCW_INI CCW_MID CCW_END ENC_ERR */{ENCIDLE, ENCIDLE, ENCIDLE, CW__OUT, ENCIDLE, ENCIDLE, CCW_OUT, ENCIDLE}, // Input 11{CW__INI, CW__INI, CW__INI, ENC_ERR, ENCIDLE, CCW_END, CCW_END, ENC_ERR}, // Input 10{CCW_INI, ENC_ERR, CW__END, CW__END, CCW_INI, CCW_INI, ENC_ERR, ENC_ERR}, // Input 01{ENC_ERR, CW__MID, CW__MID, CW__MID, CCW_MID, CCW_MID, CCW_MID, ENC_ERR}, // Input 00};typedef struct{GPIO_Type *encGPIO;const uint32_t ph1Pin;const uint32_t ph2Pin;const int16_t kInc;const int16_t kDec;State FSMstate;} Encoder;#define ENCODERS 1static Encoder enc = {{.encGPIO = ENC_PH1_GPIO,.ph1Pin = ENC_PH1_PIN,.ph2Pin = ENC_PH2_PIN,.kInc = +1,.kDec = -1,.FSMstate = ENCIDLE},};// Read full step encoderint PollEncoder(Encoder *enc){int key = 0;bool ph1 = GPIO_PinRead(enc->encGPIO, enc->ph1Pin);bool ph2 = GPIO_PinRead(enc->encGPIO, enc->ph2Pin);uint32_t FSMinput = (ph1 | (ph2 << 1)) ^ 0x03u; /* Remove the XOR if rest position is 00 *//* Advance the state machine */State newState = nextState[enc->FSMstate];/* Check if we have a full step */if (newState == CW__OUT)key = enc->kInc;else if (newState == CCW_OUT)key = enc->kDec;/* Clean up the state from the ouput indication */enc->FSMstate = newState & ENC_STATE_MASK;return key;}

author: chris_leyson
author_rank: Super Contributor
timestamp: January 01, 2020, 08:40:40 pm
content: Using the FSM approach, either outlined by Oleg Mazurov or the paper linked by ehughs, they're both the same, it's easy to add a second encoder channel.In the PIC24 example below, encoder_1 is wired to port pins B0 and B1 and encoder_2 is wired to port pins B2 and B3. Pin change interrupt is set to detect a change on B0, B1, B2 and B3.When a pin change interrupt occursFor encoder_1 the two LSBs in old_state_1 are left shifted and then ORed with port pins B0 and B1 to generate the 4-bit index for the enc_state lookup table. The value fetched from the enc_state table is then added to encval_1, this is then checked for over and underflow >3 or <-3 respectively and increments or decrememts encoder count_1.For encoder_2, bits 2 and 3 of old_state_2 are right shifted and then ORed with port pins B2 and B3 to generate the 4-bit index for the enc_state lookup table. However, because we've swapped around the two pairs of index bits for the enc_state table -1 becomes +1 and similarly +1 becomes -1, so instead of adding the table value we now have to subtract it from encval_2 because of the sign change. encval_2 is now checked for over and underflow the same way as encval_1.Code:// Change interrupt:- Incremental encodersvoid __attribute__((__interrupt__, no_auto_psv)) _CNInterrupt(void){static int enc_state[] = {0,1,-1,0,-1,0,0,1,1,0,0,-1,0,-1,1,0};old_state_1 = old_state_1 << 2;old_state_1 |= (PORTB & 0x0003);encval_1 = encval_1 + (enc_state[old_state_1 & 0x0f]);if(encval_1 > 3){ count_1++; encval_1 = 0;}else if(encval_1 < -3){ count_1--; encval_1 = 0;}// because old_state_2 bit positions are swapped// enc_state has opposite signold_state_2 = old_state_2 >> 2;old_state_2 |= (PORTB & 0x000c);encval_2 = encval_2 - (enc_state[old_state_2 & 0x0f]);if(encval_2 > 3){ count_2++; encval_2 = 0;}else if(encval_2 < -3){ count_2--; encval_2 = 0;}IFS1bits.CNIF = 0;}I think if encval were unsigned then that would speed up things a bit.

author: Jan Audio
author_rank: Frequent Contributor
timestamp: January 13, 2020, 04:30:53 pm
content: Yo, you dont have accelleration.

author: free_electron
author_rank: Super Contributor
timestamp: January 13, 2020, 04:48:41 pm
content: Quote from: Jan Audio on January 13, 2020, 04:30:53 pmthe question is : do you want that ?when used as a motor/ position encoder you absolutely do not want acceleration !when used as a scroller in a user interface you may want that.Rohde & Schwarz came up with the , in my opinion, best solution. they simply added a large flywheel to the back of their encoders. you could just whip that thing and it would keep running for a while. that combined with some clever software allowed very fast but very smooth ( the flywheel dampened the finger jiggle) scrolling.

author: Jan Audio
author_rank: Frequent Contributor
timestamp: January 13, 2020, 04:54:49 pm
content: Do you have a part for that at mouser ?thanks

author: Bud
author_rank: Super Contributor
timestamp: January 13, 2020, 05:47:17 pm
content: Quote from: free_electron on January 13, 2020, 04:48:41 pmPerhaps this was a novel thing in test equipment but this has been employed in Ham radio for decades , for frequency dials. The fast scroll wheel at the back is called Jog Dial or Shuttle Dial.

author: edigi
author_rank: Regular Contributor
timestamp: January 15, 2020, 02:33:08 pm
content: Yet another rotary encoder (ugly code from me; not using const, sparse commenting etc.). It's FSM and interrupt based, debouncing is purely SW, and it works very well even with crappy KY-040 (that I use a lot for my hobby projects) and has velocity (speed) control as option. Originally designed for UI (for frequency dial).Code example is for Arduino.Code:/*This code is free software; you can redistribute it and/ormodify it under the terms of the CC BY-NC-SA 3.0 license.THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND...*/#define ROTE_CLK GPIO_NUM_xx#define ROTE_DT GPIO_NUM_xy#define ROTE_SPCTM 50000 // speed control time limit, not defined no velocity controlvolatile int32_t rotval = 0;void IRAM_ATTR isrrot() {volatile static uint8_t pinsta = 0x3, cwi = 0, ccwi = 0;volatile static uint8_t cwexp[] = {0xD, 0x4, 0x2, 0xB};volatile static uint8_t ccwexp[] = {0xE, 0x8, 0x1, 0x7};int32_t rvchg;#ifdef ROTE_SPCTMvolatile static uint32_t tc = 0, tm = 0;uint32_t ctm, td;#endifpinsta = (pinsta << 2) & 0xf;if (digitalRead(ROTE_DT)) pinsta |= 0x2;if (digitalRead(ROTE_CLK)) pinsta |= 0x1;if (pinsta == cwexp) cwi++;else if (pinsta == ccwexp) ccwi++;if (cwi == 0x4 || ccwi == 0x4){if (cwi == 4) rvchg = 1;else rvchg = -1;pinsta = 0x3; cwi = 0; ccwi = 0;#ifdef ROTE_SPCTMctm = micros();td = ctm - tm;tm = ctm;if (td < ROTE_SPCTM / 2) rvchg *= 7;else if (td < (ROTE_SPCTM * 2) / 3) rvchg *= 4;else if (td < ROTE_SPCTM) rvchg *= 2;#endifrotval += rvchg;}} // isrrotint16_t getrotv() {static int32_t lval = 0;int32_t cval = rotval;int16_t rotc = 0;if (lval != cval) {rotc = cval - lval;lval = cval;}return (rotc);} // getrotvvoid inirotein(gpio_num_t clk, gpio_num_t dt) {pinMode(clk, INPUT);pinMode(dt, INPUT);attachInterrupt(digitalPinToInterrupt(clk), isrrot, CHANGE);attachInterrupt(digitalPinToInterrupt(dt), isrrot, CHANGE);} // inirotein...inirotein(ROTE_CLK, ROTE_DT);...

author: Jan Audio
author_rank: Frequent Contributor
timestamp: January 16, 2020, 05:14:59 pm
content: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND...No and i can see why.When i post my code i give full warranty and guarantee.Need to remember to bring the code at the right moment.

author: Jan Audio
author_rank: Frequent Contributor
timestamp: January 17, 2020, 04:23:47 pm
content: This code has full warranty.It works for PIC16 @ 32MHz = 8MHz instruction clock.Code:bit lastC0;// accelleration timersunsigned short encodertime = 0;unsigned short temptime;// init main loopif( PORTCbits.RC0 != lastC0 )lastC0 = PORTCbits.RC0;// main loopif( PORTCbits.RC0 != lastC0 ){lastC0 = PORTCbits.RC0;changeparam = curpage * 4 + 3;if( PORTCbits.RC0 ){temptime = encodertime ? 2 + encodertime >> 7 : 1;if( !PORTCbits.RC1 ){SetValue( parametervalue[ changeparam ] + temptime );}else{SetValue( parametervalue[ changeparam ] - temptime );}encodertime = 1023;}}elseif( encodertime )encodertime--;Please leave a comment.You have to take out some line of code and make your own.I left it in for your inspiration.