Arduino關於旋轉編碼器程序的介紹(Reading Rotary Encoders)

介紹

旋轉或編碼器是一個角度測量裝置. 他用作精確測量電機的旋轉角度或者用來控制控制輪子(可以無限旋轉,而電位器只能旋轉到特定位置)。其中有一些還安裝了一個可以在軸上按的按鈕,就像音樂播放器的控制按鈕。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通道是高電平或者是低電平,因爲方向不同,接着它會將計數增加或者減少。爲了檢測到波形,編碼器必須旋轉。

上面代碼的優勢是,他只檢測了關於編碼器的一種可能性(共4種分別是檢測A(上升沿,下降沿)(B+,B-),檢測B(上升沿,下降沿)(A+,A-))。爲了方便解釋,不管紅色還是綠色虛線標註的變換,都是根據編碼器的旋轉方向變化而變化的。One disadvantage of the code above is that it is really only counting one fourth of the possible transitions. In the case of the illustration, either the red or the lime green transitions, depending on which way the encoder is moving.

中斷的例子

下面是使用中斷的代碼. 當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. 上面的程序沒有提到的一點是:輸入端需要串聯上拉電阻(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__


中斷例子(編碼器中斷主線程).

使用兩個外部中斷,僅僅計算一個方向的脈衝.
注意: 儘管代碼感覺比較有效率, 但是由於使用了digitalRead()這個庫函數,根據 http://jeelabs.org/2010/01/06/pin-io-performance/上說的它會比直接讀端口要慢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.) 爲了節約一些處理器資源,我對終端系統做了稍微的修改,去維持在中斷循環外的一對布爾聲明。

我的想法是改變一個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

譯者:下面部分的翻譯在這個鏈接下:http://blog.csdn.net/xuanyuanlei1020/article/details/51725653,希望沒有給你帶來不便。


Interrupt Example (the Encoder interrupts the processor). Uses both External Interrupt pins, simplifies the interrupt service routines of the above.

Tighten up the ISR's

You can reduce the size of the interrupt service routines considerably by looking at A_set == B_set to determine lag vs lead.

The ISR's are then just

// 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;
}

Basically, if the pin that changed now matches the other pin, it's lagging behind it, and if the pin that changed is now different, then it's leading it.

Net result: two lines of code to process the interrupt.

The entire sketch is

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;
}


More encoder links


this code did work better for me than most others, with good explanation

________________________________________________________ ________________________________________________________

Interrupt Example (the Encoder interrupts the processor).

Uses both External Interrupt pins, after the initial read, does not read the state of the pins.

Fast encoder reading: using just interrupts

I also had to face with the problem of reading encoder signals and, after many trials, I'm happy to let you know a new way to deal with them, inspired by all the previous suggestions. I tried it on AMT encoder and it work really good. Unlikely, the other methods failed, the counting rate was too fast. To escape reading Arduino's port programming, I thought it may be even faster just using the interrupt pins. Here is the code:


//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; 
}



I hope this can help you.

by carnevaledaniele [04/2010]

_____________________________________________________________\\

Rotary encoder library for use in loop(). No example sketch given.

Here's a complete library code for working with Encoders:

#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;
};

Interrupt Example (the Encoder interrupts the processor).

Uses both External Interrupt pins. Based on the Circuits@home code. Does not debounce. Notice that the Serial.print() functions can take milliseconds to return, so the interrupt code takes relatively long. This may change the behavior of the code when those statements are removed (as some bounces and even some transitions may be missed if the interrupt routine takes a comparatively long time). -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("----------");
}

-----------------------------------------------------------------------------------------

Interrupt Example (the Encoder interrupts the processor).

Uses 1 Interrupt pin but misses half the state transitions. Claims to be fast but uses digitalRead() instead of direct port manipulation (see http://jeelabs.org/2010/01/06/pin-io-performance/)

 << 2011/06 >>

After try all examples, I think that I have the fastest and working form for the SaprkFunRotary encoder. Only one thing more: IT IS NEEDED TO INSERT A CAPACITOR BETWEEN CENTRAL PAD AND THE PAD B OF THE ENCODER.

#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; 
}

-----------------------------------------------------------------------------------------

Debounce Circuit

7.8.2011 deif

After lots of tries i built a circuit to reduce the bouncing a lot.

This is the minimal code i use:

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
}


Another debouncer

(2011-12-11 by _bse_)

Works very well for me. Used in conjunction with internal pull-ups.


Interrupt Example (the Encoder interrupts the processor). Uses both External Interrupt pins.

Note: Uses digitalRead() instead of direct port manipulation in the interrupt routines (seehttp://jeelabs.org/2010/01/06/pin-io-performance/)

The XOR flavour method

XORing the actual PinB state with the previous PinA state, give us the increase or decrease of the encoder count. Few code lines, full encoder precision and achieve the arduino max aquisition rate.

Have fun!

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--;
}


I don't know how the above code works (XOR), but when combined with the PinChangeIntlibrary, I can use any I/O pin as an interrupt and I was able to create 2 quadrature encoders on my carpet rover that work beautifully.

I am using a Solarbotics SB Freeduino board with an ATMEGA 328P.

I would actually like to know more of how the above XOR code works. I had to say something sometime about this, but this is great.

Thank you!


Interrupt Example using the above "XOR method".

The XOR method driving an LCD and LED

Using the XOR method, by Bruno Chaparro, above, I put together a demo that drives an LCD bargraph and 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;
}

Interrupt Library (the Encoder interrupts the processor).

Utilizes any of the ATmega328p pins via the PinChangeInt library.
by GreyGnome

The AdaEncoder library was created for working with basic 2-pin quadrature encoders such as the following:
https://www.adafruit.com/products/377
http://www.sparkfun.com/products/9117
From the Introduction:
This library interfaces with 2-pin encoders (2 pins A and B, then a common pin C). It does not indicate every state change, rather, it reports only when the decoder is turned from one detent position to the next. It is interrupt-driven and designed to be fast and easy to use. 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");
    }
  }
}

Another Interrupt Library THAT REALLY WORKS (the Encoder interrupts the processor and debounces like there is no tomorrow).

by rafbuff

I tried most of the above but found that they do not reliably count steps up and down. Most have trouble with debuncing. While I use all the tricks regarding interrupt usage and efficiency above, I found this one to work best when precision counts...

/* 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
   [email protected] 20120107

*/

// usually the rotary encoders three pins have the ground pin in the middle
enum PinAssignments {
  encoderPinA = 2,   // rigth
  encoderPinB = 3,   // left
  clearButton = 8    // another two pins
};

volatile unsigned int encoderPos = 0;  // a counter for the dial
unsigned int lastReportedPos = 1;   // change management
static boolean rotating=false;      // debounce management

// interrupt service routine vars
boolean A_set = false;              
boolean B_set = false;


void setup() {

  pinMode(encoderPinA, INPUT); 
  pinMode(encoderPinB, INPUT); 
  pinMode(clearButton, INPUT);
 // turn on pullup resistors
  digitalWrite(encoderPinA, HIGH);
  digitalWrite(encoderPinB, HIGH);
  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);  // output
}

// main loop, work is done by interrupt service routines, this one only prints stuff
void loop() { 
  rotating = true;  // reset the debouncer

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

// Interrupt on A changing state
void doEncoderA(){
  // debounce
  if ( rotating ) delay (1);  // wait a little until the bouncing is done

  // Test transition, did things really change? 
  if( digitalRead(encoderPinA) != A_set ) {  // debounce once more
    A_set = !A_set;

    // adjust counter + if A leads B
    if ( A_set && !B_set ) 
      encoderPos += 1;

    rotating = false;  // no more debouncing until loop() hits again
  }
}

// Interrupt on B changing state, same as A above
void doEncoderB(){
  if ( rotating ) delay (1);
  if( digitalRead(encoderPinB) != B_set ) {
    B_set = !B_set;
    //  adjust counter - 1 if B leads A
    if( B_set && !A_set ) 
      encoderPos -= 1;

    rotating = false;
  }
}


loop() Example, and the Encoder interrupts the processor.

Uses a single External Interrupt pin and also requires loop() for debouncing.

Software Debounce

By jonfraz 09/11

First post from me also, hope it's useful.

I've been playing around with a pretty cheap mechanical encoder and found bouncing was a big issue when I experimented with the other code above.

Ideally I would debounce it with hardware, but I'm a noob and lack the knowledge/components. However, thanks to Hifiduino I managed to get my encoder working pretty well with a simple bit of software debouncing.

The code uses an interrupt to detect any signal change from the encoder, but then waits 2 milliseconds before calculating the encoder position:

*/ Software Debouncing - Mechanical Rotary Encoder */

#define encoder0PinA 2
#define encoder0PinB 4

volatile unsigned int encoder0Pos = 0;
static boolean rotating=false;

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

  attachInterrupt(0, rotEncoder, CHANGE);  
  Serial.begin (9600);
}

void rotEncoder(){
  rotating=true; 
  // If a signal change (noise or otherwise) is detected
  // in the rotary encoder, the flag is set to true
}

void loop() {
  while(rotating) {
    delay(2);
    // When signal changes we wait 2 milliseconds for it to 
    // stabilise before reading (increase this value if there
    // still bounce issues)
    if (digitalRead(encoder0PinA) == digitalRead(encoder0PinB)) {  
      encoder0Pos++;
    } 
    else {                                   
      encoder0Pos--;
    }
    rotating=false; // Reset the flag back to false
    Serial.println(encoder0Pos);
  }
}


Int0 & Int1 example using bitRead() with debounce handling and true Rotary Encoder pulse tracking

J.Carter(of Earth)

  • This tiny amount of code is focused on keeping the interrupts fast and totally responsible for encoder position
// 'threshold' is the De-bounce Adjustment factor for the Rotary Encoder. 
//
// The threshold value I'm using limits it to 100 half pulses a second
//
// My encoder has 12 pulses per 360deg rotation and the specs say
// it is rated at a maximum of 100rpm.
//
// This threshold will permit my encoder to reach 250rpm so if it was connected
// to a motor instead of a manually operated knob I
// might possibly need to adjust it to 25000. However, this threshold
// value is working perfectly for my situation
//
volatile unsigned long threshold = 10000;


// 'rotaryHalfSteps' is the counter of half-steps. The actual
// number of steps will be equal to rotaryHalfSteps / 2
//
volatile long rotaryHalfSteps = 0;


// Working variables for the interrupt routines
//
volatile unsigned long int0time = 0;
volatile unsigned long int1time = 0;
volatile uint8_t int0signal = 0;
volatile uint8_t int1signal = 0;
volatile uint8_t int0history = 0;
volatile uint8_t int1history = 0;

void int0()
	{
	if ( micros() - int0time < threshold )
		return;
	int0history = int0signal;
	int0signal = bitRead(PIND,2);
	if ( int0history==int0signal )
		return;
	int0time = micros();
	if ( int0signal == int1signal )
		rotaryHalfSteps++;
	else
		rotaryHalfSteps--;
	}

void int1()
	{
	if ( micros() - int1time < threshold )
		return;
	int1history = int1signal;
	int1signal = bitRead(PIND,3);
	if ( int1history==int1signal )
		return;
	int1time = micros();
	}


void setup()
	{
	digitalWrite(2, HIGH);
	digitalWrite(3, HIGH);

	attachInterrupt(0, int0, CHANGE);
	attachInterrupt(1, int1, CHANGE);
	}

void loop()
	{
	long actualRotaryTicks = (rotaryHalfSteps / 2);
	}

 

On Interrupts

by GreyGnome

The ATmega328p has two different kinds of interrupts: “external”, and “pin change”. There are only two external interrupt pins, INT0 and INT1, and they are mapped to Arduino pins 2 and 3. These interrupts can be set to trigger on RISING or FALLING signal edges, or on low level. Most of the sketches and libraries given on this page use one or two of the External Interrupt pins. The interrupt is theoretically very quick because you can set the hardware to tell you how you want the interrupt to trigger. Each pin can have a separate interrupt routine associated with it.

On the other hand the pin change interrupts can be enabled on any or all of theATmega328p's signal pins. They are triggered equally on RISING or FALLING signal edges, so it is up to the interrupt code to determine what happened (did the signal rise, or fall?) and handle it properly. Furthermore, the pin change interrupts are grouped into 3 “port”s on the MCU, so there are only 3 interrupt vectors (subroutines) for the entire body of 19 pins. This makes the job of resolving the action on a single interrupt even more complicated. The interrupt routine should be fast, but complication is the enemy of speed.

Interrupts disrupt the normal flow of the program. This can be a bit of a curse, although the compiler adds some code for you in order to take away much of the pain of the interrupt. The benefit is that, unlike polling in loop(), you have a better chance of getting all the transitions from your device.

What if, for example, the Arduino is talking to an I2C device via the Wire library? You may not realize that the I2C library has a busy wait portion in it, which means that your 16MHz processor is busy waiting on a 100khz serial communication. That communication is actually being performed in hardware, so there's no reason that the processor must wait, but that's how the library was designed. So if you use polling in loop() to check on your rotary encoder, you may miss a quick transition if your code is waiting for a communication to end.

This is not an issue with an interrupt, because your rotary encoder will trigger the interrupt routine even if the Wire library is busy in a while() loop. For this reason, you should be careful using code that polls a switch in loop() on more complicated projects, and understand how long each section of your code will take in worse case scenarios.

On Speed

by GreyGnome

If your application is speed-critical, know that digitalRead() and digitalWrite() are relatively slow. See http://jeelabs.org/2010/01/06/pin-io-performance/.

From that link:

Let me just conclude with: there are order-of-magnitude performance implications, depending on how you do things. As long as you keep that in mind, you’ll be fine.

To get a closer look at the implications, I actually measured the speed.

Speed Tests

From http://arduino-pinchangeint.googlecode.com/files/PinChangeInt%20Speed%20Test-1.3.pdf Tested External Interrupts against Pin Change Interrupt, to look at the relative speeds vs. the External Interrupts, and also to better judge the performance hit of using digitalRead() and digitalWrite().

Test Pin    Interrupt Type  Loop time, 100,000     Average time per loop
     trig-                  iterations, ms.        (us)
     gered
1    2      External        1420                   14.20
2    2      Pin Change      2967                   29.67

Next, we turned on the LED Pin using direct port manipulation, then turned it on using digitalWrite() under Arduino 022 and Arduino 1.0. This test is enabled by #define'ing the COMPAREWRITES compiler directive in the source code. If DIRECTPORT is #define'ed the LED pin is turned on and off using direct port manipulation. If #undef'ed, digitalWrite() is used to manipulate the LED pin.

Test Pin    Interrupt Type  Arduino  LED pin turned  Loop time, 100,000     Average time per loop
     trig-                  Version  on using...     iterations, ms.        (us)
     gered
1    2      External        022      Direct Port     1603                   16.03
                                     Manipulation

2    2      External        022      digitalWrite()  2232                   22.32
3    2      External        1.0      digitalWrite()  2245                   22.45

Now read the LED Pin using direct port manipulation, then read it using digitalWrite() under Arduino 022 and Arduino 1.0. This test is enabled by #define'ing the COMPAREREADS compiler directive in the source code. If DIRECTPORT is #define'ed the LED pin is read using direct port manipulation. If #undef'ed, digitalREAD() is used to manipulate the LED pin.

Test Pin    Interrupt Type  Arduino  LED pin turned  Loop time, 100,000     Average time per loop
     trig-                  Version  on using...     iterations, ms.        (us)
     gered
1    2      External        022      Direct Port     1559                   15.59
                                     Manipulation
2    2      External        022      digitalRead()   2188                   21.88
3    2      External        1.0      digitalRead()   2189                   21.89
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章