Arduino关于旋转编码器程序的介绍(Reading Rotary Encoders)--by Markdown

Posted 轩辕泪1020

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Arduino关于旋转编码器程序的介绍(Reading Rotary Encoders)--by Markdown相关的知识,希望对你有一定的参考价值。

介绍

旋转或编码器是一个角度测量装置. 他用作精确测量电机的旋转角度或者用来控制控制轮子(可以无限旋转,而电位器只能旋转到特定位置)。其中有一些还安装了一个可以在轴上按的按钮,就像音乐播放器的控制按钮。Some of them are also equipped with a pushbutton when you press on the axis (like the ones used for navigation on many music controllers). 它们的精度多种多样,有每圈16步到1024步的各种,价格也从2到200欧元不等。
我写了一个小例子去读旋转编码器,并且使将读数通过RS232显示。我们很容易实现当编码器每走一步更新一下计数,并且将它通过串口显示在电脑上(通过串口监视器)。这个程序在ALPS STEC12E08编码器(每圈有24步)上运行良好。但是我认为当它使用在一个有更高精度的编码器上时有可能就会失效或者当电机旋转很快,或者你拓展这个程序以适应多个编码器。请先试试他吧。
我在Arduino distribution(AVRLib的一部分)的encoder.h中学会了怎样操作编码器。谢谢作者:Pascal Stang,感谢他对每一个函数友好而详细的解释。如下:

Example 1

/* Read Quadrature Encoder
  * Connect Encoder to Pins encoder0PinA, encoder0PinB, and +5V.
  *
  * Sketch by max wolf / www.meso.net
  * v. 0.1 - very basic functions - mw 20061220
  *
  */  


 int val; 
 int encoder0PinA = 3;
 int encoder0PinB = 4;
 int encoder0Pos = 0;
 int encoder0PinALast = LOW;
 int n = LOW;

 void setup() { 
   pinMode (encoder0PinA,INPUT);
   pinMode (encoder0PinB,INPUT);
   Serial.begin (9600);
 } 

 void loop() { 
   n = digitalRead(encoder0PinA);
   if ((encoder0PinALast == LOW) && (n == HIGH)) {//上升沿
     if (digitalRead(encoder0PinB) == LOW) {
       encoder0Pos--;
     } else {
       encoder0Pos++;
     }
     Serial.print (encoder0Pos);
     Serial.print ("/");
   } 
   encoder0PinALast = n;
 } 

要注意的几点:
encoder0Pos会一直记数,那也就意味着如果电机一直向一个方向进行旋转,那么串口消息会变的很长(最多6个字符),这样的话就会画更多的时间去转换。你需要保证当encoder0Pos溢出的时候,在你的PC端不会发生bugs-如果它的值大于INT的最大值(32,767)时,它会突变为-32,768!反之亦然。改进建议: 仅当上位机需要读数的时候,将计数相加,即只计数发送周期之间的数据。当然,如果你在loop()中添加更多的代码,或者使用更高精度的编码器,就有可能丢失某一步(少计数)。更好的解决办法是使用中断(当检测到信号的突变时)。我上面提到库文件就是这么去做的,但是现在(2006-12) 无法再Arduino环境下编译,或者我不知道怎么去做…… 。


更深的解释, 包括编码器时序图

我对于编码器的时序的原理不怎么确定,但是还是将这些内容添加到这里吧. Paul Badger
下图,是 编码器A & B两通道的时序图。
编码器A B两通道的时序图
下面的描述会更好的解释编码器是怎么运行的。当代码检测到A通道一个上升沿的时候,他会接着去检查B通道是高电平或者是低电平,因为方向不同,接着它会将计数增加或者减少。为了检测到波形,编码器必须旋转。
上面代码的优势是,他只检测了关于编码器的一种可能性(共4种分别是检测A(上升沿,下降沿)(B+,B-),检测B(上升沿,下降沿)(A+,A-))。为了方便解释,不管红色还是绿色虚线标注的变换,都是根据编码器的旋转方向变化而变化的。

中断的例子

使用一个中断的例子

下面是使用中断的代码. 当Arduino检测到A通道有变化(上升沿或下降沿), 它立刻跳转到 “doEncoder” 函数, 中断函数会在上升沿和下降沿都会被调用,所以每个一步都会被计两次。我不想使用另一个中断去检查B通道的2个变换 ( 上图紫色和青色线标注处),但是即使调用了,也不会很麻烦.
使用中断去操作旋转编码器比较不错,因为中断响应时间很快,因为它不用操作很多任务。
I used the encoder as a “mode selector” on a synthesizer made solely from an Arduino chip(译者作者可能是说他将这个旋转编码器用作一个模式选择用途,就是如同按钮形式的东西). 这是一个比较随意的程序,因为用户对单片机丢失一些脉冲并不在意. 中断方法比较重要的应用是在伺服电机或者机器人的轮子上,在这些应用中,单片机不能丢失任何一个脉冲,不然运动的精度就无法保证了。
要注意的另一点是: I used the Arduino’s pullup resistors to “steer” the inputs high when they were not engaged by the encoder. Hence the encoder common pin is connected to ground. (译者作者使用Arduino内部上拉电阻使输入端的常态是高电平,因此编码器的公共端是连接到地上)上面的程序没有提到的一点是:输入端需要串联上拉电阻(10K欧就好),因为编码器的公共端连接是+5V.

/* read a rotary encoder with interrupts
   Encoder hooked up with common to GROUND,
   encoder0PinA to pin 2, encoder0PinB to pin 4 (or pin 3 see below)
   it doesn't matter which encoder pin you use for A or B  

   uses Arduino pullups on A & B channel outputs
   turning on the pullups saves having to hook up resistors 
   to the A & B channel outputs 

*/ 

#define encoder0PinA  2
#define encoder0PinB  4

volatile unsigned int encoder0Pos = 0;

void setup() { 


  pinMode(encoder0PinA, INPUT); 
  digitalWrite(encoder0PinA, HIGH);       // turn on pullup resistor
  pinMode(encoder0PinB, INPUT); 
  digitalWrite(encoder0PinB, HIGH);       // turn on pullup resistor

  attachInterrupt(0, doEncoder, CHANGE);  // encoder pin on interrupt 0 - pin 2
  Serial.begin (9600);
  Serial.println("start");                // a personal quirk

} 

void loop(){
// do some stuff here - the joy of interrupts is that they take care of themselves
}

void doEncoder() {
  /* If pinA and pinB are both high or both low, it is spinning
   * forward. If they're different, it's going backward.
   *
   * For more information on speeding up this process, see
   * [Reference/PortManipulation], specifically the PIND register.
   */
  if (digitalRead(encoder0PinA) == digitalRead(encoder0PinB)) {
    encoder0Pos++;
  } else {
    encoder0Pos--;
  }

  Serial.println (encoder0Pos, DEC);
}

/* See this expanded function to get a better understanding of the
 * meanings of the four possible (pinA, pinB) value pairs:
 */
void doEncoder_Expanded(){
  if (digitalRead(encoder0PinA) == HIGH) {   // found a low-to-high on channel A
    if (digitalRead(encoder0PinB) == LOW) {  // check channel B to see which way
                                             // encoder is turning
      encoder0Pos = encoder0Pos - 1;         // CCW
    } 
    else {
      encoder0Pos = encoder0Pos + 1;         // CW
    }
  }
  else                                        // found a high-to-low on channel A
  { 
    if (digitalRead(encoder0PinB) == LOW) {   // check channel B to see which way
                                              // encoder is turning  
      encoder0Pos = encoder0Pos + 1;          // CW
    } 
    else {
      encoder0Pos = encoder0Pos - 1;          // CCW
    }

  }
  Serial.println (encoder0Pos, DEC);          // debug - remember to comment out
                                              // before final program run
  // you don't want serial slowing down your program if not needed
}

/*  to read the other two transitions - just use another attachInterrupt()
in the setup and duplicate the doEncoder function into say, 
doEncoderA and doEncoderB. 
You also need to move the other encoder wire over to pin 3 (interrupt 1). 
*/ 

BY:dskv 注意!!!
在中断程序中使用Serial.Print要特别注意,大多数情况下它会失败, 但是有时它会成功, 这是个程序比较糟糕的bugs. 下面连接是解释文档:
https://groups.google.com/a/arduino.cc/forum/#!topic/developers/HKzEcN6gikM
http://forum.arduino.cc/index.php?topic=94459.0
http://forum.jeelabs.net/node/1188.html


中断例程 (编码器中断主线程). 使用两个中断端口

读编码器,使用2个中断 pin 2 & pin3
注意:下面的程序使用两个中断来使用编码器的最高精度(就像步进电机的细分. 上文的程序使用1个中断. 它仅仅读取一半精度,通过检测EncoderPin A的位置,但是它省一个中断程序。

#define encoder0PinA 2
#define encoder0PinB 3
volatile unsigned int encoder0Pos = 0;
void setup() {
  pinMode(encoder0PinA, INPUT); 
  pinMode(encoder0PinB, INPUT); 

// encoder pin on interrupt 0 (pin 2)
  attachInterrupt(0, doEncoderA, CHANGE);

// encoder pin on interrupt 1 (pin 3)
  attachInterrupt(1, doEncoderB, CHANGE);  
  Serial.begin (9600);
}

void loop(){ //Do stuff here }

void doEncoderA(){

  // look for a low-to-high on channel A
  if (digitalRead(encoder0PinA) == HIGH) { 
    // check channel B to see which way encoder is turning
    if (digitalRead(encoder0PinB) == LOW) {  
      encoder0Pos = encoder0Pos + 1;         // CW
    } 
    else {
      encoder0Pos = encoder0Pos - 1;         // CCW
    }
  }
  else   // must be a high-to-low edge on channel A                                       
  { 
    // check channel B to see which way encoder is turning  
    if (digitalRead(encoder0PinB) == HIGH) {   
      encoder0Pos = encoder0Pos + 1;          // CW
    } 
    else {
      encoder0Pos = encoder0Pos - 1;          // CCW
    }
  }
  Serial.println (encoder0Pos, DEC);          
  // use for debugging - remember to comment out
}

void doEncoderB(){

  // look for a low-to-high on channel B
  if (digitalRead(encoder0PinB) == HIGH) {   
   // check channel A to see which way encoder is turning
    if (digitalRead(encoder0PinA) == HIGH) {  
      encoder0Pos = encoder0Pos + 1;         // CW
    } 
    else {
      encoder0Pos = encoder0Pos - 1;         // CCW
    }
  }
  // Look for a high-to-low on channel B
  else { 
    // check channel B to see which way encoder is turning  
    if (digitalRead(encoder0PinA) == LOW) {   
      encoder0Pos = encoder0Pos + 1;          // CW
    } 
    else {
      encoder0Pos = encoder0Pos - 1;          // CCW
    }
  }
}

中端例子(编码器中断主线程). 用一个中断端口, 将代码包装进C++类.

类包装 by mikkoh [01/2010]
将上面的例子包装 (一个中断的例程) 进类并且减少一点doEncoder 函数代码的体积(希望代码对你们还是可读的). 在这个类的文档中一个例子.

#ifndef __ENCODER_H__
#define __ENCODER_H__
#include "WProgram.h"
class Encoder {
  /*  
    wraps encoder setup and update functions in a class
    !!! NOTE : user must call the encoders update method from an
    interrupt function himself! i.e. user must attach an interrupt to the
    encoder pin A and call the encoder update method from within the 
    interrupt

    uses Arduino pullups on A & B channel outputs
    turning on the pullups saves having to hook up resistors 
    to the A & B channel outputs 

    // ------------------------------------------------------------------------------------------------
    // Example usage :
    // ------------------------------------------------------------------------------------------------
        #include "Encoder.h"

        Encoder encoder(2, 4);

        void setup() { 
            attachInterrupt(0, doEncoder, CHANGE); 
            Serial.begin (115200);
            Serial.println("start");
        } 

        void loop(){
            // do some stuff here - the joy of interrupts is that they take care of themselves
        }

        void doEncoder(){
            encoder.update();
            Serial.println( encoder.getPosition() );
        }    
    // ------------------------------------------------------------------------------------------------
    // Example usage end
    // ------------------------------------------------------------------------------------------------
  */
public:

    // constructor : sets pins as inputs and turns on pullup resistors

    Encoder( int8_t PinA, int8_t PinB) : pin_a ( PinA), pin_b( PinB ) {
        // set pin a and b to be input 
        pinMode(pin_a, INPUT); 
        pinMode(pin_b, INPUT); 
        // and turn on pullup resistors
        digitalWrite(pin_a, HIGH);    
        digitalWrite(pin_b, HIGH);                 
    };

    // call this from your interrupt function

    void update () {
        if (digitalRead(pin_a)) digitalRead(pin_b) ? position++ : position--;
        else digitalRead(pin_b) ? position-- : position++;
    };

    // returns current position

    long int getPosition () { return position; };

    // set the position value

    void setPosition ( const long int p) { position = p; };

private:

    long int position;

    int8_t pin_a;

    int8_t pin_b;
};

#endif // __ENCODER_H__

译者 上面的代码使用long int类型,关于Arduino 数据类型Arduino 数据类型

中断例子(编码器中断主线程),使用两个中断

使用两个外部中断,仅仅计算一个方向的脉冲.
注意: 尽管代码感觉比较有效率, 但是由于使用了digitalRead()这个库函数,根据 Pin I/O performance译者地址Arduino 端口性能讲解)上说的它会比直接读端口要慢50倍。
最优效率的旋转编码器计数 by m3tr0g33k
Paul Badger 的工作和原博客很有启发性并且很有用,在看代码之前,请理解他们是怎么说的(希望你能看明白)。
My project is a data loger where three analogue inputs are sampled each time a rotary encoder pulse steps clockwise. On an Arduino, time is of the essence to get this data sampled and saved somewhere (I have not included the ‘saving somewhere’ part of this project yet.) (译者大体意思是当旋转编码器旋转一周将有3个输入量出现,但是在Arduino中处理器资源是有限的)为了节约一些处理器资源,我对终端系统做了稍微的修改,去维持在中断循环外的一对布尔声明。
我的想法是改变一个A或B的布尔变量,当在A或B口收到一个有效的跳变沿. 当你因A端口中断,并且是有效的,你将A_set=true. 然后检测B_set是不是false.如果是, 那么 A 比 B 的相位靠前,这表明是顺时针 (计数++)。同理,当你收到有效的因端口B中断, 你会改变set B_set=true. 然后你检查 A_set 是否是 false. 如果是, 那么 B 比A的相位靠前 , 那么意味着是逆时针旋转 (计数–)。
此代码与以前的代码不同的地方是:当A或B发生中断的时候(中断标志是CHANGE),检测A和B口的状态,如果A或B是0则设置A_set或B_set为false,其他的工作就不需要了,减少了中断的占用时间(因为以前在一次中断需要读取A和B口的状态,而这个仅仅需要读取一个口的状态)。
不管如何,在代码中有足够的解释,代码如下:

#define encoder0PinA 2
#define encoder0PinB 3

volatile unsigned int encoder0Pos = 0;
unsigned int tmp_Pos = 1;
unsigned int valx;
unsigned int valy;
unsigned int valz;

boolean A_set;
boolean B_set;


void setup() {

  pinMode(encoder0PinA, INPUT); 
  pinMode(encoder0PinB, INPUT); 

// encoder pin on interrupt 0 (pin 2)
  attachInterrupt(0, doEncoderA, CHANGE);

// encoder pin on interrupt 1 (pin 3)
  attachInterrupt(1, doEncoderB, CHANGE);

  Serial.begin (9600);
}


void loop(){ 
//Check each second for change in position
  if (tmp_Pos != encoder0Pos) {
    Serial.print("Index:"); Serial.print(encoder0Pos, DEC); Serial.print(", Values: "); 
    Serial.print(valx, DEC); Serial.print(", ");
    Serial.print(valy, DEC); Serial.print(", ");
    Serial.print(valz, DEC); Serial.println();
    tmp_Pos = encoder0Pos;
  }
  delay(1000);
}


// Interrupt on A changing state
void doEncoderA(){

  // Low to High transition?
  if (digitalRead(encoder0PinA) == HIGH) { 
    A_set = true;
    if (!B_set) {
      encoder0Pos = encoder0Pos + 1;
      valx=analogRead(0);
      valy=analogRead(1);
      valz=analogRead(2);
    }        
  }

  // High-to-low transition?
  if (digitalRead(encoder0PinA) == LOW) {
    A_set = false;
  }

}

// Interrupt on B changing state
void doEncoderB(){

  // Low-to-high transition?
  if (digitalRead(encoder0PinB) == HIGH) {   
    B_set = true;
    if (!A_set) {
      encoder0Pos = encoder0Pos - 1;
    }
  }

  // High-to-low transition?
  if (digitalRead(encoder0PinB) == LOW) {
    B_set = false;
  }
}

环绕中断代码的其他部分仅仅是为了展示它是怎么工作的。就像我说的,我仅仅希望演示电机正传(在我的电车例子中就是向前走)。当检测到电机是反转的时候,我仅仅是更新那个计数变量。在 loop{} 循环体中,我们每一秒显示一次当前编码器的位置和相应引脚读数数据。但是仅当编码器位置发生改变的时候才去更新,如果位置没有发生变化,则就不更新。你可以试试一秒钟你能转你的编码器多少圈。我的记录是300步或者是1.5圈每秒。
这个代码有一个逻辑问题,如果你频繁改变方向,那么你也许会想知道在每步的中间位置是否改变了方向,而且在那个位置,你的计数参数是不发生改变的。这就是半步滞后现象。当然,在绝大多数情况下,这并不能观察得到或者是重要的,但是思考一下这个问题是比较重要的。
希望这个运行速度的提升能够帮助一些人!m3tr0g33k
中断例子(编码器中断主线程),使用两个中断,简化上面代码的中断响应函数
简化中断程序
按照A_set == B_set来判断滞后或提前,你可以简化相当一部分中断程序的代码。
中断程序变为:

// Interrupt on A changing state
void doEncoderA(){
  // Test transition
  A_set = digitalRead(encoderPinA) == HIGH;
  // and adjust counter + if A leads B
  encoderPos += (A_set != B_set) ? +1 : -1;
}

// Interrupt on B changing state
void doEncoderB(){
  // Test transition
  B_set = digitalRead(encoderPinB) == HIGH;
  // and adjust counter + if B follows A
  encoderPos += (A_set == B_set) ? +1 : -1;
}

其基本原理为:当当前的引脚的改变的状态和另一个引脚的状态一致,那么,这个引脚就比另一个引脚滞后。如果状态不一致,那么当前引脚就超前。
最终结果:两行代码便构成了中断程序。
全部代码为:

enum PinAssignments {
  encoderPinA = 2,
  encoderPinB = 3,
  clearButton = 8
};

volatile unsigned int encoderPos = 0;
unsigned int lastReportedPos = 1;

boolean A_set = false;
boolean B_set = false;

void setup() {

  pinMode(encoderPinA, INPUT); 
  pinMode(encoderPinB, INPUT); 
  pinMode(clearButton, INPUT);
  digitalWrite(encoderPinA, HIGH);  // turn on pullup resistor
  digitalWrite(encoderPinB, HIGH);  // turn on pullup resistor
  digitalWrite(clearButton, HIGH);

// encoder pin on interrupt 0 (pin 2)
  attachInterrupt(0, doEncoderA, CHANGE);
// encoder pin on interrupt 1 (pin 3)
  attachInterrupt(1, doEncoderB, CHANGE);

  Serial.begin(9600);
}


void loop(){ 
  if (lastReportedPos != encoderPos) {
    Serial.print("Index:");
    Serial.print(encoderPos, DEC);
    Serial.println();
    lastReportedPos = encoderPos;
  }
  if (digitalRead(clearButton) == LOW)  {
    encoderPos = 0;
  }
}

// Interrupt on A changing state
void doEncoderA(){
  // Test transition
  A_set = digitalRead(encoderPinA) == HIGH;
  // and adjust counter + if A leads B
  encoderPos += (A_set != B_set) ? +1 : -1;
}

// Interrupt on B changing state
void doEncoderB(){
  // Test transition
  B_set = digitalRead(encoderPinB) == HIGH;
  // and adjust counter + if B follows A
  encoderPos += (A_set == B_set) ? +1 : -1;
}

其他关于编码器的链接:
a good explanation of grey codes and absolute encoders
this code did work better for me than most others, with good explanation

中断例子(编码器中断主线程),使用两个外部中断,初始化读后,就不再读引脚的状态

更快读编码器:仅使用中断
我也在应用中使用到了读旋转编码器,经过多次尝试,我很高兴高速大家有一个新的方式处理他们。根据前任的建议,我在AMT旋转编码器上使用了它并且运行真的很好。不幸的是,其他的方法失败了,因为计数太快了。为了避免读Arduino引脚的,我想也许有更快的方法,通过仅仅使用中断。下面是它的代码:

//PIN's definition
#define encoder0PinA  2
#define encoder0PinB  3


volatile int encoder0Pos = 0;
volatile boolean PastA = 0;
volatile boolean PastB = 0;

void setup() 
{

  pinMode(encoder0PinA, INPUT);
  //turn on pullup resistor
  //digitalWrite(encoder0PinA, HIGH); //ONLY FOR SOME ENCODER(MAGNETIC)!!!! 
  pinMode(encoder0PinB, INPUT); 
  //turn on pullup resistor
  //digitalWrite(encoder0PinB, HIGH); //ONLY FOR SOME ENCODER(MAGNETIC)!!!! 
  PastA = (boolean)digitalRead(encoder0PinA); //initial value of channel A;
  PastB = (boolean)digitalRead(encoder0PinB); //and channel B

//To speed up even more, you may define manually the ISRs
// encoder A channel on interrupt 0 (arduino's pin 2)
  attachInterrupt(0, doEncoderA, RISING);
// encoder B channel pin on interrupt 1 (arduino's pin 3)
  attachInterrupt(1, doEncoderB, CHANGE); 

}


void loop()
{  
 //your staff....ENJOY! :D
}

//you may easily modify the code  get quadrature..
//..but be sure this whouldn't let Arduino back! 
void doEncoderA()
{
     PastB ? encoder0Pos--:  encoder0Pos++;
}

void doEncoderB()
{
     PastB = !PastB; 
}

希望能帮助到大家! by carnevaledaniele [04/2010]

旋转编码器库,在loop()中使用,没有示例代码

下面是完整的旋转编码器操作库代码(译者下面的代码没有使用中断,将占用较多的处理器资源):

#include <inttypes.h>
#include "HardwareSerial.h"

// 12 Step Rotary Encoder with Click //
// http://www.sparkfun.com/products/9117 //
#define EncoderPinA 20  // Rotary Encoder Left Pin //
#define EncoderPinB 19  // Rotary Encoder Right Pin //
#define EncoderPinP 21  // Rotary Encoder Click //

// ======================================================================================= //
class Encoder
{
public:
    Encoder() 
    { 
        pinMode(EncoderPinA, INPUT);
        digitalWrite(EncoderPinA, HIGH);
        pinMode(EncoderPinB, INPUT);
        digitalWrite(EncoderPinB, HIGH);
        pinMode(EncoderPinP, INPUT);
        digitalWrite(EncoderPinP, HIGH);

        Position = 0; 
        Position2 = 0; 
        Max = 127; 
        Min = 0;
        clickMultiply = 10;
    }

    void Tick(void)
    { 
        Position2 = (digitalRead(EncoderPinB) * 2) + digitalRead(EncoderPinA);;
        if (Position2 != Position)
        {
            isFwd = ((Position == 0) && (Position2 == 1)) || ((Position == 1) && (Position2 == 3)) || 
                ((Position == 3) && (Position2 == 2)) || ((Position == 2) && (Position2 == 0));
            if (!digitalRead(EncoderPinP)) { if (isFwd) Pos += clickMultiply; else Pos -= clickMultiply; }
                else { if (isFwd) Pos++; else Pos--; }
            if (Pos < Min) Pos = Min;
            if (Pos > Max) Pos = Max;
        }
        Position = Position2;
    }

    int getPos(void)
    {
        return (Pos/4);
    }

    void setMinMax(int _Min, int _Max) 
    { 
        Min = _Min*4; 
        Max = _Max*4; 
        if (Pos < Min) Pos = Min;
        if (Pos > Max) Pos = Max;
    }

    void setClickMultiply(int _clickMultiply)
    {
        clickMultiply = _clickMultiply;
    }

private:
    int clickMultiply;
    int Max;
    int Min;
    int Pos;
    int Position;
    int Position2;
    int isFwd;
};

中断例子(编码器中断主线程)–>这个没看

使用两个外部中断端口,主要根据Circuits@home代码,不防抖动。 注意: Serial.print()函数会占用几毫秒,所以此代码中的中断程序运行时间会相对比较长,当将这些声明去掉的时候,那么就会提升程序的性能。(因为当中断程序占用足够长的时间,那么信号抖动甚至一些信号将会被丢失)。-ED
代码:

/*
  RotaryInterrupts - a port-read and interrupt based rotary encoder sketch
  Created by Joshua Layne (w15p), January 4, 2011.
  based largely on: http://www.circuitsathome.com/mcu/reading-rotary-encoder-on-arduino
  Released into the public domain.
*/

#define ENC_A 2
#define ENC_B 3
#define ENC_PORT PIND
uint8_t bitShift = 2; // change to suit your pins (offset from 0,1 per port)
// Note: You need to choose pins that have Interrupt capability.

int counter;
boolean ticToc;

void setup()
{
  pinMode(ENC_A, INPUT);
  digitalWrite(ENC_A, HIGH);
  pinMode(ENC_B, INPUT);
  digitalWrite(ENC_B, HIGH);
  Serial.begin (115200);
  Serial.println("Start");
  counter = 0;
  ticToc = false;
  // Attach ISR to both interrupts
  attachInterrupt(0, read_encoder, CHANGE);
  attachInterrupt(1, read_encoder, CHANGE);
}

void loop()
{
// do some stuff here - the joy of interrupts is that they take care of themselves
}

void read_encoder()
{
  int8_t enc_states[] = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};
  static uint8_t encoderState = 0;
  static uint8_t stateIndex = 0;
  static uint8_t filteredPort = 0;
  uint8_t filter = 0x03; // base filter: 0b00000011
  filter <<= bitShift;

  Serial.print("raw port value: ");
  Serial.println(ENC_PORT, BIN);

  Serial.print("filter bitmask: ");
  Serial.println(filter, BIN);

  filteredPort = ENC_PORT & filter;
  Serial.print("filtered port state: ");
  Serial.println(filteredPort, BIN);

  Serial.print("old encoder state: ");
  Serial.println(encoderState, BIN);

  encoderState &= filter; // filter out everything except the rotary encoder pins
  Serial.print("filtered old encoder state: ");
  Serial.println(encoderState, BIN);

  encoderState <<= 2; // shift existing value two bits to the left
  Serial.print("filtered and shifted (<<2) old encoder state: ");
  Serial.println(encoderState, BIN);

  encoderState |= filteredPort; // add filteredport value
  Serial.print("old encoder state + port state: ");
  Serial.println(encoderState, BIN);

  stateIndex = encoderState >> bitShift;
  Serial.print("encoder state index: ");
  Serial.println(stateIndex, DEC);

  if (ticToc) {
  Serial.print("counter tic: ");
  Serial.println(enc_states[stateIndex], DEC);
  counter += enc_states[stateIndex];
  Serial.print("counter: ");
  Serial.println(counter, DEC);
  }
  ticToc = !ticToc;

  Serial.println("----------");
}

中断例子(旋转编码器中断主线程)

使用1个中断,这样就丢失了一半的精度,作者说速度比较快但是却用了digitalRead()函数而不是直接去操作引脚。(具体请看: http://jeelabs.org/2010/01/06/pin-io-performance/))<< 2011/06 >>
通过尝试所有的例子,我觉得我的代码对于SaprkFun的旋转编码器(SaprkFunRotary encoder)操作地比较快了。但是请注意:需要在编码器的中间引脚和B引脚之间串连一个电容(译者就是在输出口和GND之间串联一个电容)。
程序:

#include <LiquidCrystal.h>

LiquidCrystal lcd(12,11,5,4,8,7);

#define encoder0PinA  2
#define encoder0PinB  3


volatile int encoder0Pos = 0;
volatile boolean PastB = 0;
volatile boolean update = false;

void setup() 
{
  lcd.begin(16,2);
  lcd.print("Pos:");

  pinMode(encoder0PinA, INPUT);
  //turn on pullup resistor
  digitalWrite(encoder0PinA, HIGH);
  pinMode(encoder0PinB, INPUT); 
  //turn on pullup resistor
  digitalWrite(encoder0PinB, HIGH);

  attachInterrupt(1, doEncoderB, FALLING); 
}

void loop()
{  
  if (update){
    update = false;
    PastB? encoder0Pos++:encoder0Pos--;
    lcd.setCursor(7,0);
    lcd.print("     ");
    lcd.setCursor(7,0);    
    lcd.print(encoder0Pos,DEC);
  }
}

void doEncoderB()
{
  PastB=(boolean)digitalRead(encoder0PinA);
  update = true; 
}

去抖动电路

去抖动电路1

7.8.2011 deif
通过许多尝试,我建立了一个电路消除大多数的抖动。
去抖动电路图
下面是我最简形式的代码:

ISR(INT0_vect){
    int a;
    a = PIND & 0x0c;

    if ((a == 0x0c) || (a == 0)){
        encoderCount++;
    }
    else {
        encoderCount--;
    }
}


void setupPinInterrupt(){
  EICRA = 0x01; //int0 on pinchange
  EIMSK = 0x01; //enable interrupt 0
  EIFR = 0; //clear flags
}

去抖动电路2

2011-12-11 by _bse_
去抖动电路2
和上拉电阻一起使用(还是添加电容)


中断例子(旋转编码器中断主进程)。使用两个外部中断

注意:在中断程序中,使用digitalRead()而不是直接操作端口。
使用XOR(异或)的方法。
通过异或的方法,将当前B口的状态和前一次端口A的状态进行比较,得出是应该减少还是增加计数(即正反转)。较少的代码,编码器的全精度并且达到了Arduino最大采集速率。
使用愉快!
15 August 2011 by Bruno Chaparro

#define encoder0PinA 2
#define encoder0PinB 3
volatile unsigned int encoder0Pos = 0;
unsigned int tmp = 0;
unsigned int Aold = 0;
unsigned int Bnew = 0;
void setup() {
  pinMode(encoder0PinA, INPUT); 
  pinMode(encoder0PinB, INPUT);
// encoder pin on interrupt 0 (pin 2)
  attachInterrupt(0, doEncoderA, CHANGE);
// encoder pin on interrupt 1 (pin 3)
  attachInterrupt(1, doEncoderB, CHANGE);
// set up the Serial Connection 
  Serial.begin (115200);
}
void loop(){
  //Check each changes in position
  if (tmp != encoder0Pos) {
    Serial.println(encoder0Pos, DEC);
    tmp = encoder0Pos;
  }
  delay(500);
}
// Interrupt on A changing state
void doEncoderA(){
  Bnew^Aold ? encoder0Pos++:encoder0Pos--;
  Aold=digitalRead(encoder0PinA);
}
// Interrupt on B changing state
void doEncoderB(){
  Bnew=digitalRead(encoder0PinB);
  Bnew^Aold ? encoder0Pos++:encoder0Pos--;
}

我不知道上面代码是怎么运行的(就是XOR方法)(译者其实没啥神秘的就是将是否相等变成使用异或的方法进行实现,但是我不知道这样是否会对代码的运行速度带来提升。),但是当我将代码和PinChangeIntlibrary(这样我可以使用所有的I/O口作为中断口)进行联合使用时,我在的小车上运行了两个增量编码器而且运行良好。
I am using a Solarbotics SB Freeduino board with an ATMEGA 328P.(译者没使用过这个板子,不清楚)。
我希望我对上面的XOR代码是怎么运行了解更多。我不得写点什么在其他时间,但是这个代码很棒。
谢谢你!


中断例子使用上面的“XOR 方法”

使用XOR方法驱动一个LCD和一个LED
使用上面Bruno Chaparro所提到XOR方法,我将一个驱动LCD bargraph和LED的例子和它结合到了一起。
29 December 2011 Mark Amos

// increments/decrements a counter based on the movement of a rotary encoder and 
// displays the results on an LCD in digital and  bargraph form.
// rotary encoder is a 5 pin. Pins, left to right:
//   Encoder pin B - connect to D2 (interrupt 0)
//   +5 VDC
//   Encoder pin A - connect to D3 (interrupt 1)
//   NC
//   Ground
// Pin D5 is used to light up an LED connected to ground with a 1K resistor
// using PWM with brightness proportional to the encoder position.

#include <LiquidCrystal.h>
#define encoder0PinA 3
#define encoder0PinB 2
#define analogOutPin 5

LiquidCrystal lcd(13,12,11,10,9,8,7);
volatile unsigned int encoder0Pos = 0;
unsigned int tmp = 0;
unsigned int Aold = 0;
unsigned int Bnew = 0;

void setup() {
  pinMode(encoder0PinA, INPUT); 
  pinMode(encoder0PinB, INPUT);

  lcd.begin(20,2);
  lcd.clear();

  // encoder A on interrupt 1 (pin 3)
  attachInterrupt(1, doEncoderA, CHANGE);
  // encoder B on interrupt 0 (pin 2)
  attachInterrupt(0, doEncoderB, CHANGE);
  // set up the Serial Connection 
  Serial.begin (115200);
  Serial.println("Starting");
}

void loop(){
  //if position has changed, display it on serial and bargraph
  if (tmp != encoder0Pos) {
    tmp = encoder0Pos;
    Serial.println(tmp, DEC);
    lcd.setCursor(0,0);
    lcd.print(tmp);
    lcd.print("    ");
    lcd.setCursor(0,1);
    //scale the range of the LCD bargraph from 0-1023 to 0-20 by dividing by 50
    for (int loopCnt = 0; loopCnt < (tmp / 50) +1 ; loopCnt++) {
      lcd.write(1);
    }
    lcd.print("                   ");
    //scale the encorer0Pos from 0 - 1023 to the range of the PWM (0 - 255) by dividing by 4.
    analogWrite(analogOutPin, tmp / 4);
  }
}

// Interrupt on A changing state
void doEncoderA(){
  // if Bnew = Aold, increment, otherwise decrement
  Bnew^Aold ? encoder0Pos++:encoder0Pos--;
  Aold=digitalRead(encoder0PinA);
  // check for underflow (< 0) 
  if (bitRead(encoder0Pos, 15) == 1) encoder0Pos = 0;
  // check for overflow (> 1023)
  if (bitRead(encoder0Pos, 10) == 1) encoder0Pos = 1023;
  constrain(encoder0Pos, 0, 1023);
}

// Interrupt on B changing state
void doEncoderB(){
  Bnew=digitalRead(encoder0PinB);
  // if Bnew = Aold, increment, otherwise decrement
  Bnew^Aold ? encoder0Pos++:encoder0Pos--;
  // check for underflow (< 0) 
  if (bitRead(encoder0Pos, 15) == 1) encoder0Pos = 0;
  // check for overflow (> 1023)
  if (bitRead(encoder0Pos, 10) == 1) encoder0Pos = 1023;
}

中断库(旋转编码器中断主线程)

使用PinChangeInt库来利用ATmega328p的所有的引脚作为中断口。by GreyGnome

AdaEncoder库适用于2个基本端口的增量旋转编码器,就像下面链接中所示:
https://www.adafruit.com/products/377
http://www.sparkfun.com/products/9117
通过介绍我们知道:
下面翻译是2016年9月6日15:03:10后翻译,所以有可能翻译没有以前那么准确了。(时间太长了)-译者
这个库适用于2端口编码器(一个A口一个B口还有一个公共口C口)。这个库不会通知每一个状态的改变,而是仅当编码器从一个定位点变化到另一个的时候才会通知用户(这个没有看懂)。 It does not indicate every state change, rather, it reports only when the decoder is turned from one detent position to the next. 它是通过中断触发的并且设计的尽量反应快和好用。 The interrupt routine is lightweight, and the programmer is then able to read the direction the encoder turned at their leisure (within reason; what’s important is that the library is reasonably forgiving). The library is designed to be easy to use (it bears repeating :-) ) and it is reasonably immune to switch bounce.
See the project page at: http://code.google.com/p/adaencoder/
See a speed discussion at: http://code.google.com/p/adaencoder/wiki/Speed
See the PinChangeInt library project page at: http://code.google.com/p/arduino-pinchangeint/
Here’s an example with two encoders connected. Encoder a is connected to pins 2 and 3, b is connected to 5 and 6:

#include <PinChangeInt.h> // necessary otherwise we get undefined reference errors.
#include <AdaEncoder.h>

#define a_PINA 2
#define a_PINB 3
#define b_PINA 5
#define b_PINB 6

int8_t clicks=0;
char id=0;

void setup()
{
  Serial.begin(115200);
  AdaEncoder::addEncoder('a', a_PINA, a_PINB);
  AdaEncoder::addEncoder('b', b_PINA, b_PINB);  
}

void loop()
{
  encoder *thisEncoder;
  thisEncoder=AdaEncoder::genie(&clicks, &id);
  if (thisEncoder != NULL) {
    Serial.print(id); Serial.print(':');
    if (clicks > 0) {
      Serial.println(" CW");
    }
    if (clicks < 0) {
       Serial.println(" CCW");
    }
  }
}

另外一个中断库(绝对能用)(旋转编码器中断主线程并且防抖动–>完美!!)

by rafbuff
译者这个有点吹了,他在中断程序中使用delay(1),这也就是说计数的周期应该远大于1ms,但是别人也有小于1ms的啊!!)
我尝试了上面大多数的程序,但是发现它们的计数不是那么的可靠,大多数不能防抖动。同时我尝试了许多技巧性的功能,如中断的使用和提高效率的办法,当要求精确计数的时候,我发现下面的这个运行最好。

/* interrupt routine for Rotary Encoders
   tested with Noble RE0124PVB 17.7FINB-24 http://www.nobleusa.com/pdf/xre.pdf - available at pollin.de
   and a few others, seems pretty universal

   The average rotary encoder has three pins, seen from front: A C B
   Clockwise rotation A(on)->B(on)->A(off)->B(off)
   CounterCW rotation B(on)->A(on)->B(off)->A(off)

   and may be a push switch with another two pins, pulled low at pin 8 in this case
   raf

以上是关于Arduino关于旋转编码器程序的介绍(Reading Rotary Encoders)--by Markdown的主要内容,如果未能解决你的问题,请参考以下文章

Arduino关于旋转编码器程序的介绍(Reading Rotary Encoders)--by Markdown

Arduino旋转编码器测试

Arduino与Processing交互-旋转编码器控制播放音乐

Arduino与Processing交互-旋转编码器控制播放音乐

ESP32 + EC11旋转编码器调节PWM占空比输出

用于虚拟 DJ 的 Arduino Usb MIDI 控制器