i want to get accuracy angle with rotary encoder by interrupt

before explain my problem, i will tell you about my arduino data.

my arduino board is uno and avr is atmega328p u.
my reference data is here.

http://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf

i want to get accuracy angle with rotary encoder so i was write code like this first.

int _count = 0;

void setup()
{
    pinMode(2, INPUT);
    attachInterrupt(digitalPinToInterrupt(2), tick, CHANGE);
}

void loop()
{
    if(Serial.available())
    {
        Serial.read();
        Serial.println(_count);
    }
}

void tick()
{
    _count++;
}

when i run this code, it is ok with very slow rotation speed, but if i rotate it with little faster, then it return a little error. i thought that error will occurs by interrupt when interrupt. my rotary encoder PPR is 6000.

so i was study about pin change interrupt and i change my code like this.

#include <avr\interrupt.h>

void setup()
{
  Serial.begin(9600);

  PCICR |= (1 << PCIE0);
  PCMSK0 |= (1 << PCINT0);
}

void loop() { }

ISR(PCINT0_vec)
{
  Serial.println("a");
}

in this time, i connect the rotary encoder at 5v, gnd and A phase at pin number 8.

i thought that setting Pin Change Interrupt Control Register(PCICR) to 0b00000001 and it means i will use B port to interrupt.

and Pin Change Mask Register 0(PCMSK0) is setted 0b00000001 so it will interrupted when digital pin 8 is changed.

so when i rotate the encoder then phase A will pulsed, and my Serial will show "a" on Serial monitor, but it won't.

is there any problem on my code? or should i compile at atmel studio?(i was compile and upload this code at Arduino IDE.) or my reference data is wrong?

please give me some help.

your counter should be volatile and you should not print from an ISR.

Printing uses interrupts and they are disabled within your ISR, that can lead to a lock up if the Serial buffer is full.

also dealing with anything that is not just 1 byte and coming from an interrupt is error prone. Your int is 2 bytes long, one of the byte could be modified whilst trying to print without you knowing it. You should duplicate in the loop — in a interrupt protected section of your code — the variables that might get modified if an interrupt were to come at that moment.

  noInterrupts();
  // critical, time-sensitive code here
  int tmpCnt = _count;
  interrupts();
  // now you can safely use tmpCnt as you froze its value to whatever it was

that being said, unless this is for exploration and learning, if I were you I would use the excellent encoder library from Paul Stoffregen & co, it's proven to work at pretty high speed.

sorry, i was too fast to reply before read full context of your post.

I think that you want to say if in interrupt valiables should be volatile and size should be 1byte.

so i change my code like this, but it didn't work.

did i understand wrong?

#include <avr\interrupt.h>

volatile int _count = 0;
long _time = 0;

void setup()
{
  Serial.begin(9600);

  PCICR |= (1 << PCIE0);
  PCMSK0 |= (1 << PCINT0);
}

void loop()
{
  if(Serial.available())
  {
    Serial.read();
    Serial.println(_count);
  }
}

ISR(PCINT0_vec)
{
  noInterrupts();
  char tmp = 0;
  tmp++;
  _count = tmp;
  interrupts();
}

i'm appreciate your recommend library. i will try as possible as i can and if i failed, i will use that library.

Yes for println() outside interrupts but I also suggested a critical section.

I'd suggest you get it working without playing with registers, just use the high level construct provided by the Arduino environment - may be something like this based on your first code.

volatile long _count = 0;
const byte encoderPin = 2;

void tick() {_count++;}

void setup()
{
  Serial.begin(115200); // *** SET SERIAL CONSOLE TO 115200 BAUDS AS WELL ***
  pinMode(encoderPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(encoderPin), tick, CHANGE);
}

void loop()
{
  if(Serial.read() != -1) { // -1 means there was nothing to read 
    noInterrupts();
    long tmpCnt = _count;  // local copy to be safe
    interrupts();
    Serial.println(tmpCnt);
  }
}

Of course if your button is bouncing — which is very likely — you'll get plenty of ticks per rotation.
adding a cap next to the rotary (between output of rotary and GND) will solve this bouncing.

Rotary encoders I know do have 2 output though - so not sure why you only care about one.

Once you get this working to your satisfaction, you can explore how to set pins and registers to do the right thing if you want to be closer to the silicon.

ok i change my code like this.

volatile long _count = 0;
const byte phaseA = 2;
const byte phaseB = 8;
const int ppr = 12000;
const float delta = 360.0/ppr;//12000 is PPR

void tick()
{
  if(digitalRead(phaseA) ^ digitalRead(phaseB))
    _count++;
  else
    _count--;

  if(_count < 0)
    _count += ppr;
  else
    _count  = _count % ppr;
}

void setup()
{
  Serial.begin(115200); // *** SET SERIAL CONSOLE TO 115200 BAUDS AS WELL ***
  pinMode(phaseA, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(phaseA), tick, CHANGE);
}

void loop()
{
  if(Serial.read() != -1) { // -1 means there was nothing to read
    noInterrupts();
    long tmpCnt = _count;  // local copy to be safe
    interrupts();
    Serial.println(tmpCnt * delta);
  }
}

i think this code type will completely what i want to do.

and if rotate encoder, stop, and write something to print angle, it print some error when i rotate faster the error go increase.

i think that's because interrupt tick occurs when interrupt tick was not finished.

so i want to make interrupt routine faster to make doesn't happend like that.

but, i have an idea when you said about cap. when encoder get little impact, the count increase when it really didn'r rotated. and when i check interrupt routine time, it return 28 micro seconds if i calculate it exactly. and it means interrupt routine is possible almost 35,000 hz which is faster than i rotate it. my encoder's maximum speed is 5000 rpm, and it means 7rpm at arduino, but i know i didn't rotate it like that.

but.. i think it doesn't matter about it, because when i test encoder which is didn't broken with myrio, it's currectly measure it.

but i will consider about cap. but what is cap you mean?

and before i apply to my encoder, i want to know why error occurs. i think that's because interrupt routine is too long to check every pulse. so i want to apply pin change interrupt on my code.

how can i do this?

what about PhaseB ?

  pinMode(phaseA, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(phaseA), tick, CHANGE);

regarding the capacitor, read about button bouncing and how to remove that.

Modulo takes some time, you could keep the ISR to the strict minimum which is incrementing or decrementing the counter and do the math when you need to use the counter in the main loop.

Rotary encoders I know do have 2 output though - so not sure why you only care about one.

phase A and B is just two output of rotary encoder.

then you mean that debouncing like this?
https://www.arduino.cc/en/tutorial/debounce

i will consider about it. thanks.

and i think your recommendation is like this.

void tick()
{
    if(digitalRead(phaseA) ^ digitalRead(phaseB))
    _count++;
  else
    _count--;
}

void loop()
{
    if(Serial.read() != -1) { // -1 means there was nothing to read
        noInterrupts();

        while(_count < 0)
            _count += ppr;


        _count  = _count % ppr;

        long tmpCnt = _count;  // local copy to be safe
        interrupts();
        Serial.println(tmpCnt * delta);
  }
}

thank you for your advise. i will consider that structure.

by the way, while you were offline i was done pin change interrupt.
it's because "PCINT0_vect". i was wrote it first "PCINT_vec" and when i change like "PCINT0_vect", it works. but it still make some error, too.

now i want to make sure that this happen occurs by repeat of interrupt. i guess this happen because rotary pulse that making interrupt pulse is too fast so interrupt occurs before same interrupt finished. but i don't know how to check it.

is there any way to check this? or this problem is another cause? i have no idea about this.

if you turn your rotary encoder manually interrupts are faster than you... way faster, should not be a worry

i know.. i was check it before that interrupt routine cost only 28 micro seconds. i guess interrupt will finish before next interrupt, too.

but is there any other reason i can guess why error has occur.

i just only assume that nested interrupt problem. sorry.

do you have any idea about this?

post the full program, explain the wiring and how things are powered and explain what "error" you see.

here is my full code

const byte phaseA = 2;
const byte phaseB = 8;
const int ppr = 12000;
const float delta = 360.0/ppr;//12000 is PPR

volatile long _count = 0;

void tick()
{
  if(digitalRead(phaseA) ^ digitalRead(phaseB))
    _count++;
  else
    _count--;
}

void setup ()
{
  Serial.begin(115200); // *** SET SERIAL CONSOLE TO 115200 BAUDS AS WELL ***
  pinMode(phaseA, INPUT_PULLUP);
  pinMode(phaseB, INPUT_PULLUP);
 
  attachInterrupt(digitalPinToInterrupt(phaseA), tick, CHANGE);
}

void loop ()
{
  if(Serial.read() != -1) { // -1 means there was nothing to read
    noInterrupts();
    while(_count < 0)
      _count += ppr;

    _count  = _count % ppr;

    long tmpCnt = _count;  // local copy to be safe
    interrupts();
    Serial.println(tmpCnt * delta);
  }
}

i try to count encoder in two way which one is counting by attachInterrupt

const byte phaseA = 8;
const byte phaseB = 7;
const int ppr = 12000;
const float delta = 360.0/ppr;//12000 is PPR

volatile long _count = 0;

ISR(PCINT0_vect)
{
  if(digitalRead(phaseA) ^ digitalRead(phaseB))
    _count++;
  else
    _count--;
}

void setup ()
{
  Serial.begin(115200); // *** SET SERIAL CONSOLE TO 115200 BAUDS AS WELL ***
  pinMode(phaseA, INPUT_PULLUP);
  pinMode(phaseB, INPUT_PULLUP);
 
  uint8_t  oldSREG = SREG;
  PCICR |= B00000001;
  PCMSK0 |= B00000001;
  SREG = oldSREG;
}

void loop ()
{
  if(Serial.read() != -1) { // -1 means there was nothing to read
    noInterrupts();
    while(_count < 0)
      _count += ppr;
      
    _count  = _count % ppr;
    long tmpCnt = _count;  // local copy to be safe
    interrupts();
    Serial.println(tmpCnt * delta);
  }
}

and pin change interrupt way.

both are most same and able to activate, and has same error occurs.

the error means error.

when i rotate 360 degree the output should be 0
and when i rotate it very slowly the output is 1~359(which means -1)

but when i rotate it little fast like 1 rpm / s
the output is 7~8 or 352~353 like this. or sometimes 120degree like this.
output didn't return 0 or 1 or 359.

so i assume that's because interrupt reiteration. if not i didn't assume else.

you should keep the time with interrupt blocked as short as possible, so not do

    noInterrupts();
    while(_count < 0)
      _count += ppr;

    _count  = _count % ppr;

    long tmpCnt = _count;  // local copy to be safe
    interrupts();

and prefer

    noInterrupts();
    long tmpCnt = _count;  // local copy to be safe
    interrupts();
    while(tmpCnt < 0) tmpCnt += ppr;
    tmpCnt  = tmpCnt % ppr;

Then remember that rotary encoder you buy for ~ a dollar are not top notch quality. You get bouncing, the quality of the contacts might be average, which all starts playing and degrading the ticks as you turn faster.

what model do you have? is it graded for being attached to a motor and be accurate?

ok i will remember that keep interrupt block as short as possible.

here is my encoder datasheet

s66-5-6000VL is my encoder.

i don't know how much is this because i just found this from our lab, and nobody know how to control this.
i think encoder is quite well.

and if it occurs by bouncing i think that would affect only 2~3 degree..not quite much.

Ok seems not a $0,50 cheap encoder :), likely offers 6000 pulse per rotation. You probably get 4 pulses per tick.

There are many things to read from your spec

1/ the "VL" in your s66-5-6000VL model means you have a line driver model with 6 outputs

model.png

2/ You want to take A and B

Signal.png

3/ so that should be the Green and White cables. Are those the ones you picked ?

4/ the current draw can be 150mA max. How are you powering it ?

————

Why don't you first check if you can get it to work with the library I mentioned above. Once that works, you can dig into it more if you want to (or just move on to your real project)

Can you test this code - open the console at 115200 bauds

Make sure you have a stable power supply able to deliver 150mA. Don't use the 5V pin of your Arduino Uno (the 5V pin may supply less than five volts if you are on USB - or use a Jack power supply at 9V 1A for example). If you do not power directly from your Arduino, join grounds.

#define ENCODER_OPTIMIZE_INTERRUPTS
#include <Encoder.h> // install https://github.com/PaulStoffregen/Encoder

Encoder rotaryEncoder(2, 3); // Green cable on 2, White cable on 3
int32_t oldPosition = 0;

void setup() {
  Serial.begin(115200); // open the Serial Console at 115200 bauds
  Serial.println(F("Testing Rotary Encoder s66-5-6000VL"));
  rotaryEncoder.write(oldPosition);
}

void loop() {
  int32_t newPosition = rotaryEncoder.read();
  if (newPosition != oldPosition) {
    oldPosition = newPosition;
    Serial.println(newPosition);
  }
}

model.png

Signal.png

i powered it directly to uno board. i know that board can power 500mA and nothing else connected it, so i think it can supply enough current to it.

=============================================================================

wow, i'm very impressived. it has some error, but it's very accuate better than my code.
i should applied this as fast as possible when you recommend.

i'm very appriciate with your help, thanks!.

but i have one question. if i analyize that library, then is it possible to know why my code has an error even if i use pin change interrupt, but library is very accurate even if use loop with serial?

sinsin63:
i powered it directly to uno board. i know that board can power 500mA and nothing else connected it, so i think it can supply enough current to it.

USB can provide indeed 500mA but the 5V pin when powered through USB is not exactly 5V. it can be more or less depending on your USB quality and it can vary as you draw current. if your rotary is too sensitive (here seems OK to up to 5% ripple) that can lead to disfunction too.

You could indeed look into the library (prune out the code that does not pertain to AVR) and you'll see how they use interrupts.

How many pulses do you get per tick ? if you get 2 you could modify the code to

#define ENCODER_OPTIMIZE_INTERRUPTS
#include <Encoder.h> // install https://github.com/PaulStoffregen/Encoder

Encoder rotaryEncoder(2, 3); // Green cable on 2, White cable on 3
int32_t oldPosition = 0;

void setup() {
  Serial.begin(115200); // open the Serial Console at 115200 bauds
  Serial.println(F("Testing Rotary Encoder s66-5-6000VL"));
  rotaryEncoder.write(oldPosition);
}

void loop() {
  int32_t newPosition = rotaryEncoder.read() >> 1; // divide by 2 as 2 pulses per tick
  if (newPosition != oldPosition) {
    oldPosition = newPosition;
    Serial.println(newPosition);
  }
}

if you get 4 pulses then you can do

  int32_t newPosition = rotaryEncoder.read() >> 2; // divide by 4 as 4 pulses per tick

if you spin really really fast, then increase the Baud rate and don't print as often (may be every 50 ticks) as that will generate extra interrupts.

Note as well that digitalRead is somewhat slow and thus you are not reading both pins at the exact same time of the interrupt - that would also lead to inaccurate result at fast RPM

Using PORTs to read the status of the pins is way much faster (1 clock cycle to get both pins if they are in the same port plus a bit mask)

sorry to late for reply.

thanks! i will remember your advise.