风筝
发表于: 2016-9-6 22:00:42 | 显示全部楼层

介绍

旋转或编码器是一个角度测量装置. 他用作精确测量电机的旋转角度或者用来控制控制轮子(可以无限旋转,而电位器只能旋转到特定位置)。其中有一些还安装了一个可以在轴上按的按钮,就像音乐播放器的控制按钮。 它们的精度多种多样,有每圈16步到1024步的各种,价格也从2到200欧元不等。

我写了一个小例子去读旋转编码器,并且使将读数通过RS232显示。我们很容易实现当编码器每走一步更新一下计数,并且将它通过串口显示在电脑上(通过串口监视器)。这个程序在ALPS STEC12E08编码器(每圈有24步)上运行良好。但是我认为当它使用在一个有更高精度的编码器上时有可能就会失效或者当电机旋转很快,或者你拓展这个程序以适应多个编码器。请先试试他吧。

我在Arduino distribution(AVRLib的一部分)的encoder.h中学会了怎样操作编码器。谢谢作者:Pascal Stang,感谢他对每一个函数友好而详细的解释。如下:

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


  8. int val;
  9. int encoder0PinA = 3;
  10. int encoder0PinB = 4;
  11. int encoder0Pos = 0;
  12. int encoder0PinALast = LOW;
  13. int n = LOW;

  14. void setup() {
  15.    pinMode (encoder0PinA,INPUT);
  16.    pinMode (encoder0PinB,INPUT);
  17.    Serial.begin (9600);
  18. }

  19. void loop() {
  20.    n = digitalRead(encoder0PinA);
  21.    if ((encoder0PinALast == LOW) && (n == HIGH)) {//上升沿
  22.      if (digitalRead(encoder0PinB) == LOW) {
  23.        encoder0Pos--;
  24.      } else {
  25.        encoder0Pos++;
  26.      }
  27.      Serial.print (encoder0Pos);
  28.      Serial.print ("/");
  29.    }
  30.    encoder0PinALast = n;
  31. }
复制代码

要注意的几点:

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



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

我对于编码器的时序的原理不怎么确定,但是还是将这些内容添加到这里吧. Paul Badger

下图,是 编码器A & B两通道的时序图。

20160621110204732.jpg

下面的描述会更好的解释编码器是怎么运行的。当代码检测到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.

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

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

  8. */

  9. #define encoder0PinA  2
  10. #define encoder0PinB  4

  11. volatile unsigned int encoder0Pos = 0;

  12. void setup() {


  13.   pinMode(encoder0PinA, INPUT);
  14.   digitalWrite(encoder0PinA, HIGH);       // turn on pullup resistor
  15.   pinMode(encoder0PinB, INPUT);
  16.   digitalWrite(encoder0PinB, HIGH);       // turn on pullup resistor

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

  20. }

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

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

  36.   Serial.println (encoder0Pos, DEC);
  37. }

  38. /* See this expanded function to get a better understanding of the
  39. * meanings of the four possible (pinA, pinB) value pairs:
  40. */
  41. void doEncoder_Expanded(){
  42.   if (digitalRead(encoder0PinA) == HIGH) {   // found a low-to-high on channel A
  43.     if (digitalRead(encoder0PinB) == LOW) {  // check channel B to see which way
  44.                                              // encoder is turning
  45.       encoder0Pos = encoder0Pos - 1;         // CCW
  46.     }
  47.     else {
  48.       encoder0Pos = encoder0Pos + 1;         // CW
  49.     }
  50.   }
  51.   else                                        // found a high-to-low on channel A
  52.   {
  53.     if (digitalRead(encoder0PinB) == LOW) {   // check channel B to see which way
  54.                                               // encoder is turning  
  55.       encoder0Pos = encoder0Pos + 1;          // CW
  56.     }
  57.     else {
  58.       encoder0Pos = encoder0Pos - 1;          // CCW
  59.     }

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

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

在中断程序中使用Serial.Print要特别注意,大多数情况下它会失败, 但是有时它会成功, 这是个程序比较糟糕的bugs. 下面连接是解释文档:

https://groups.google.com/a/ardu ... elopers/HKzEcN6gikM

http://forum.arduino.cc/index.php?topic=94459.0

http://forum.jeelabs.net/node/1188.html



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

读编码器,使用2个中断 pin 2 & pin3

注意:下面的程序使用两个中断来使用编码器的最高精度(就像步进电机的细分. 上文的程序使用1个中断. 它仅仅读取一半精度,通过检测EncoderPin A的位置,但是它省一个中断程序。(译者 上面的代码使用long int类型,关于Arduino 数据类型Arduino 数据类型)

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

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

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

  13. void loop(){ //Do stuff here }

  14. void doEncoderA(){

  15.   // look for a low-to-high on channel A
  16.   if (digitalRead(encoder0PinA) == HIGH) {
  17.     // check channel B to see which way encoder is turning
  18.     if (digitalRead(encoder0PinB) == LOW) {  
  19.       encoder0Pos = encoder0Pos + 1;         // CW
  20.     }
  21.     else {
  22.       encoder0Pos = encoder0Pos - 1;         // CCW
  23.     }
  24.   }
  25.   else   // must be a high-to-low edge on channel A                                       
  26.   {
  27.     // check channel B to see which way encoder is turning  
  28.     if (digitalRead(encoder0PinB) == HIGH) {   
  29.       encoder0Pos = encoder0Pos + 1;          // CW
  30.     }
  31.     else {
  32.       encoder0Pos = encoder0Pos - 1;          // CCW
  33.     }
  34.   }
  35.   Serial.println (encoder0Pos, DEC);         
  36.   // use for debugging - remember to comment out
  37. }

  38. void doEncoderB(){

  39.   // look for a low-to-high on channel B
  40.   if (digitalRead(encoder0PinB) == HIGH) {   
  41.    // check channel A to see which way encoder is turning
  42.     if (digitalRead(encoder0PinA) == HIGH) {  
  43.       encoder0Pos = encoder0Pos + 1;         // CW
  44.     }
  45.     else {
  46.       encoder0Pos = encoder0Pos - 1;         // CCW
  47.     }
  48.   }
  49.   // Look for a high-to-low on channel B
  50.   else {
  51.     // check channel B to see which way encoder is turning  
  52.     if (digitalRead(encoder0PinA) == LOW) {   
  53.       encoder0Pos = encoder0Pos + 1;          // CW
  54.     }
  55.     else {
  56.       encoder0Pos = encoder0Pos - 1;          // CCW
  57.     }
  58.   }
  59. }
复制代码


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

使用两个外部中断,仅仅计算一个方向的脉冲.

注意: 尽管代码感觉比较有效率, 但是由于使用了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口的状态,而这个仅仅需要读取一个口的状态)。

不管如何,在代码中有足够的解释,代码如下:

  1. #define encoder0PinA 2
  2. #define encoder0PinB 3

  3. volatile unsigned int encoder0Pos = 0;
  4. unsigned int tmp_Pos = 1;
  5. unsigned int valx;
  6. unsigned int valy;
  7. unsigned int valz;

  8. boolean A_set;
  9. boolean B_set;


  10. void setup() {

  11.   pinMode(encoder0PinA, INPUT);
  12.   pinMode(encoder0PinB, INPUT);

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

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

  17.   Serial.begin (9600);
  18. }


  19. void loop(){
  20. //Check each second for change in position
  21.   if (tmp_Pos != encoder0Pos) {
  22.     Serial.print("Index:"); Serial.print(encoder0Pos, DEC); Serial.print(", Values: ");
  23.     Serial.print(valx, DEC); Serial.print(", ");
  24.     Serial.print(valy, DEC); Serial.print(", ");
  25.     Serial.print(valz, DEC); Serial.println();
  26.     tmp_Pos = encoder0Pos;
  27.   }
  28.   delay(1000);
  29. }


  30. // Interrupt on A changing state
  31. void doEncoderA(){

  32.   // Low to High transition?
  33.   if (digitalRead(encoder0PinA) == HIGH) {
  34.     A_set = true;
  35.     if (!B_set) {
  36.       encoder0Pos = encoder0Pos + 1;
  37.       valx=analogRead(0);
  38.       valy=analogRead(1);
  39.       valz=analogRead(2);
  40.     }        
  41.   }

  42.   // High-to-low transition?
  43.   if (digitalRead(encoder0PinA) == LOW) {
  44.     A_set = false;
  45.   }

  46. }

  47. // Interrupt on B changing state
  48. void doEncoderB(){

  49.   // Low-to-high transition?
  50.   if (digitalRead(encoder0PinB) == HIGH) {   
  51.     B_set = true;
  52.     if (!A_set) {
  53.       encoder0Pos = encoder0Pos - 1;
  54.     }
  55.   }

  56.   // High-to-low transition?
  57.   if (digitalRead(encoder0PinB) == LOW) {
  58.     B_set = false;
  59.   }
  60. }
复制代码

环绕中断代码的其他部分仅仅是为了展示它是怎么工作的。就像我说的,我仅仅希望演示电机正传(在我的电车例子中就是向前走)。当检测到电机是反转的时候,我仅仅是更新那个计数变量。在 loop{} 循环体中,我们每一秒显示一次当前编码器的位置和相应引脚读数数据。但是仅当编码器位置发生改变的时候才去更新,如果位置没有发生变化,则就不更新。你可以试试一秒钟你能转你的编码器多少圈。我的记录是300步或者是1.5圈每秒。

这个代码有一个逻辑问题,如果你频繁改变方向,那么你也许会想知道在每步的中间位置是否改变了方向,而且在那个位置,你的计数参数是不发生改变的。这就是半步滞后现象。当然,在绝大多数情况下,这并不能观察得到或者是重要的,但是思考一下这个问题是比较重要的。
希望这个运行速度的提升能够帮助一些人!m3tr0g33k

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

使用两个中断,简化上面代码的中断响应函数

按照A_set == B_set来判断滞后或提前,你可以简化相当一部分中断程序的代码。

中断程序变为:

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

  8. // Interrupt on B changing state
  9. void doEncoderB(){
  10.   // Test transition
  11.   B_set = digitalRead(encoderPinB) == HIGH;
  12.   // and adjust counter + if B follows A
  13.   encoderPos += (A_set == B_set) ? +1 : -1;
  14. }
复制代码

其基本原理为:当当前的引脚的改变的状态和另一个引脚的状态一致,那么,这个引脚就比另一个引脚滞后。如果状态不一致,那么当前引脚就超前。

最终结果:两行代码便构成了中断程序。

全部代码为:

  1. enum PinAssignments {
  2.   encoderPinA = 2,
  3.   encoderPinB = 3,
  4.   clearButton = 8
  5. };

  6. volatile unsigned int encoderPos = 0;
  7. unsigned int lastReportedPos = 1;

  8. boolean A_set = false;
  9. boolean B_set = false;

  10. void setup() {

  11.   pinMode(encoderPinA, INPUT);
  12.   pinMode(encoderPinB, INPUT);
  13.   pinMode(clearButton, INPUT);
  14.   digitalWrite(encoderPinA, HIGH);  // turn on pullup resistor
  15.   digitalWrite(encoderPinB, HIGH);  // turn on pullup resistor
  16.   digitalWrite(clearButton, HIGH);

  17. // encoder pin on interrupt 0 (pin 2)
  18.   attachInterrupt(0, doEncoderA, CHANGE);
  19. // encoder pin on interrupt 1 (pin 3)
  20.   attachInterrupt(1, doEncoderB, CHANGE);

  21.   Serial.begin(9600);
  22. }


  23. void loop(){
  24.   if (lastReportedPos != encoderPos) {
  25.     Serial.print("Index:");
  26.     Serial.print(encoderPos, DEC);
  27.     Serial.println();
  28.     lastReportedPos = encoderPos;
  29.   }
  30.   if (digitalRead(clearButton) == LOW)  {
  31.     encoderPos = 0;
  32.   }
  33. }

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

  41. // Interrupt on B changing state
  42. void doEncoderB(){
  43.   // Test transition
  44.   B_set = digitalRead(encoderPinB) == HIGH;
  45.   // and adjust counter + if B follows A
  46.   encoderPos += (A_set == B_set) ? +1 : -1;
  47. }
复制代码

原文链接http://blog.csdn.net/xuanyuanlei1020/article/details/51725653

跳转到指定楼层
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

主题 714 | 回复: 1501



手机版|

GMT+8, 2024-12-22 11:10 , Processed in 0.075436 second(s), 9 queries , Gzip On, MemCache On. Powered by Discuz! X3.5

YiBoard一板网 © 2015-2022 地址:河北省石家庄市长安区高营大街 ( 冀ICP备18020117号 )

快速回复 返回顶部 返回列表